606 lines
17 KiB
C++
606 lines
17 KiB
C++
// Copyright 2021, Paul Mosier
|
|
//
|
|
// This file is part of Scriptura, a ncurses-based Bible study
|
|
// software for the libsword backend.
|
|
//
|
|
// Scriptura is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, under version 2 of the License.
|
|
//
|
|
// Scriptura is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Scriptura. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
// pane.cpp -- subwindow management
|
|
|
|
#include <stdlib.h>
|
|
#include <ncurses.h>
|
|
#include <menu.h>
|
|
#include <form.h>
|
|
#include <string.h>
|
|
#include <wchar.h>
|
|
|
|
#include "pane.h"
|
|
|
|
// constructor
|
|
pane::pane(int starty, int startx, int lines, int cols, const char* title,
|
|
modkey defmod) {
|
|
|
|
buflocx = buflocy = 0;
|
|
hasFocus = false;
|
|
rawtext = NULL;
|
|
|
|
// set up the window -- resize will give us some initializing
|
|
setTitle(title);
|
|
pad = NULL;
|
|
resize(starty, startx, lines, cols);
|
|
redraw(true);
|
|
|
|
// forms/menus send in a null searchkey
|
|
if (defmod.searchkey != NULL) {
|
|
setModkey(defmod, title);
|
|
}
|
|
}
|
|
|
|
// handle pane resizing
|
|
void pane::resize(int starty, int startx, int lines, int cols) {
|
|
y = starty;
|
|
x = startx;
|
|
height = lines;
|
|
width = cols;
|
|
|
|
win = newwin(height, width, y, x);
|
|
|
|
/* if pad is a curses subwindow (made by derwin) then we close it rather than
|
|
* resize it, so we only check for pads proper to manage text handling */
|
|
if (is_pad(pad)) renderText();
|
|
}
|
|
|
|
/* trap new text to render, and then pass along to our render function */
|
|
void pane::loadText(const char* stext) {
|
|
free(rawtext);
|
|
rawtext = (char*) malloc(sizeof(char*) * strlen(stext) + 1);
|
|
if (! rawtext) wrapup(1, "Error allocating memory in loadText.\n");
|
|
memset(rawtext, '\0', strlen(stext) + 1);
|
|
strncpy(rawtext, stext, strlen(stext));
|
|
free((void*) stext); // alloc'd in scabbard::getSpan
|
|
renderText();
|
|
}
|
|
|
|
/* create a new pad with the boundaries tightly fitting around a block of
|
|
* formatted text */
|
|
void pane::renderText() {
|
|
// pad is an ncurses pad - set what dimensions we know right now
|
|
pady = y + 1;
|
|
padx = x + 1;
|
|
padrows = height - 2;
|
|
padcols = width - 2;
|
|
bufsizex = padcols;
|
|
buflocx = 0;
|
|
buflocy = 0;
|
|
|
|
// here we define some counters & flags
|
|
int length = strlen(rawtext);
|
|
int numlines = 0; // number of newlines
|
|
int linelength = 0; // how long the current line is
|
|
int lastspaceidx = 0; // where was the last space relative to the string
|
|
int printable = 0; // index of printed characters
|
|
int lastprintspace = 0; // where was the last space relative to printed chars
|
|
int inmarkup = 0; // are we in a non-printed markup block
|
|
int makered = 0; // redletter flag
|
|
int makeital = 0; // italic flag
|
|
|
|
// pull config variables
|
|
int rawonly = parseConf(config["markup"]["rawtext"]);
|
|
int redletter = parseConf(config["markup"]["redletters"]);
|
|
int strongs = parseConf(config["markup"]["strongs"]);
|
|
int nontextual = parseConf(config["markup"]["nontextual"]);
|
|
// int footnotes = 0; not implemented yet
|
|
|
|
// copy our unformatted text to local scope for window formatting
|
|
char* text = (char*) malloc(sizeof(char*) * length + 1);
|
|
if (! text) wrapup(1, "Error allocating memory in renderText.\n");
|
|
memset(text, '\0', strlen(rawtext) + 1);
|
|
strncpy(text, rawtext, length);
|
|
|
|
// kept for debugging
|
|
//fwprintf(stderr, L"(O): %ls\n\n", text);
|
|
|
|
/* here's the workhorse - loop through the text twice: first to alter the
|
|
* text for word wrapping and to count the number of lines we have so we
|
|
* can define the pad size; second to print the text with the correct
|
|
* markup */
|
|
for (int p = 0; p < 2; p++) {
|
|
|
|
// loop through the text
|
|
int i = 0;
|
|
while (i < length) {
|
|
|
|
/* check if we're in markup - it's not printed and it doesn't
|
|
* affect our line lengths, so spin through it */
|
|
if (inmarkup) {
|
|
if (text[i] == '>') inmarkup = 0;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
// check if we're entering markup - if so, figure out what
|
|
if ((text[i] == '<') && (! rawonly)) {
|
|
inmarkup = 1;
|
|
|
|
if ((! strmatch(&text[i], "</q>", 1)) && (p == 1)) {
|
|
// end of redletter bracket
|
|
makered = 0;
|
|
|
|
} else if ((! strmatch(&text[i], "</transChange>", 1))
|
|
&& (p == 1)) {
|
|
// end of interpretive text bracket
|
|
makeital = 0;
|
|
|
|
} else if ((! strmatch(&text[i], "<p>", 1)) && (p == 0)) {
|
|
// paragraph break - replace </p> with </>'\n'
|
|
int endindex = strmatch(&text[i], "</p>", 0);
|
|
if (endindex != -1)
|
|
memcpy(&text[i + endindex + 2], ">\n", 2);
|
|
|
|
} else if ((! strmatch(&text[i], "<w savlm=", 1))
|
|
&& (p == 0) && (strongs == 1)) {
|
|
/* Strong's number - the format is below, but note
|
|
* there may be 1+ strong:[G|H]NNNN(N)'s to find,
|
|
* and other junk to ignore both within the same
|
|
* parameter, and before the word
|
|
* <w savlm="strong:[G|H]NNNN(N)">word</w> */
|
|
|
|
// 1. get boundary of open tag
|
|
int endbracket = strmatch(&text[i], ">", 0);
|
|
|
|
// 2. get Strong's parameters
|
|
char* num = (char*) malloc(sizeof(char*) * 100);
|
|
if (! num) wrapup(1,
|
|
"Error declaring memory in renderText.\n");
|
|
int numidx = 0;
|
|
|
|
int strnum = strmatch(&text[i], "strong:", 0) + 7;
|
|
while ((strnum < endbracket) && (strnum != -1)) {
|
|
int nextspace = strmatch(&text[i+strnum], " ", 0);
|
|
int space1 = strmatch(&text[i+strnum], "\"", 0);
|
|
int len = ((nextspace == -1) || (space1 < nextspace)
|
|
? space1
|
|
: nextspace);
|
|
|
|
if (numidx != 0) memcpy(&num[numidx++], " ", 1);
|
|
memcpy(&num[numidx], &text[i+strnum], len);
|
|
numidx += len;
|
|
|
|
int nextnum =
|
|
strmatch(&text[i+strnum], "strong:", 0);
|
|
strnum = (nextnum != -1
|
|
? strnum + nextnum + 7
|
|
: -1);
|
|
}
|
|
|
|
/* 3. determine if tag is empty closed-element,
|
|
* if so, rewrite in place by making wordlen
|
|
* below zero, otherwise get word boundaries */
|
|
int wordstart = endbracket + 1;
|
|
int endtag =
|
|
(endbracket > strmatch(&text[i], "/", 0)
|
|
? endbracket + 1
|
|
: strmatch(&text[i], "</w>", 0));
|
|
|
|
// 4. determine word boundaries & rewrite
|
|
int wordlen = endtag - wordstart;
|
|
char* word = (char*) malloc(sizeof(char*)
|
|
* (wordlen == 0 ? 1 : wordlen));
|
|
|
|
if (! word) wrapup(1,
|
|
"Error rewriting markup in renderText.\n");
|
|
memcpy(word, &text[i+wordstart], wordlen);
|
|
|
|
// rewrite
|
|
int start = i + endtag - wordlen - numidx - 4;
|
|
memcpy(&text[start], "\"", 1);
|
|
memcpy(&text[start + 1], ">", 1);
|
|
memcpy(&text[start + 2], word, wordlen);
|
|
memcpy(&text[start + 2 + wordlen], "[", 1);
|
|
memcpy(&text[start + 3 + wordlen], num, numidx);
|
|
memcpy(&text[start + 3 + wordlen + numidx], "]", 1);
|
|
|
|
// clean up so we can do this again
|
|
free(num);
|
|
free(word);
|
|
|
|
// kept for debugging
|
|
//fwprintf(stderr, L"(I): %ls\n", text);
|
|
|
|
} else if ((! strmatch(&text[i], "<q marker", 1))
|
|
&& (p == 1) && (redletter == 1)) {
|
|
// start of redletter bracket
|
|
makered = 1;
|
|
|
|
} else if ((! strmatch(&text[i], "<transChange type=\"added\"", 1))
|
|
&& (p == 1) && (nontextual == 1)) {
|
|
// start of interpretive text bracket
|
|
makeital = 1;
|
|
}
|
|
|
|
// don't print the angle bracket
|
|
continue;
|
|
} // markup check
|
|
|
|
/* determine how large this multibyte character is -- we will need it to
|
|
* determine our advance amount in the loop */
|
|
int offset = 0;
|
|
size_t charlen = mbrlen(&text[i], 5, NULL);
|
|
while (((int) mbrlen(&text[i+offset], 5, NULL) == -1) &&
|
|
(i + offset < length)) {
|
|
/* mbrlen() says the next char is not a valid multibyte char, so find
|
|
* the length by figuring out where the succeeding character is */
|
|
offset++;
|
|
}
|
|
|
|
if (p == 0) {
|
|
// handle word wrapping
|
|
if (text[i] == ' ') {
|
|
/* if under the buffer width, note index & continue
|
|
* if over, set the preceeding space to a newline */
|
|
if (linelength >= bufsizex) {
|
|
text[lastspaceidx] = '\n';
|
|
numlines++;
|
|
// start counting from the beginning of the last word
|
|
linelength = printable - lastprintspace;
|
|
} else {
|
|
lastspaceidx = i;
|
|
lastprintspace = printable;
|
|
}
|
|
}
|
|
// reset for newlines
|
|
if (text[i] == '\n') {
|
|
/* check to see if we're still over the length but hit a
|
|
* newline before a space */
|
|
if (linelength >= bufsizex) {
|
|
text[lastspaceidx] = '\n';
|
|
numlines++;
|
|
}
|
|
numlines++;
|
|
linelength = 0;
|
|
lastspaceidx = i;
|
|
lastprintspace = printable;
|
|
}
|
|
|
|
linelength++;
|
|
|
|
// various word wrapping debugging statements
|
|
//fprintf(stderr, "[ %c ] : 0x%X\n", text[i], text[i]);
|
|
//fprintf(stderr, "Char: %c I: %d Numlines: %d "
|
|
// "Linelength: %d Lastspaceidx: %d\n",
|
|
// text[i], printable, numlines, linelength, lastspaceidx);
|
|
|
|
} else {
|
|
// printing -- pull out the single character we care about
|
|
wattrset(pad, COLOR_PAIR(makered)
|
|
| (makeital ? A_ITALIC : 0));
|
|
|
|
if (((int) charlen == -1) && (offset == 1)) {
|
|
/* This is an extended ascii character (probably Latin-1) and not
|
|
* UTF-8. In setting up ncurses for UTF-8 we set a locale and seem to
|
|
* make these characters unprintable. To avoid massive rendering
|
|
* errors we have to substitute them with something else. */
|
|
waddstr(pad, "?");
|
|
|
|
} else waddnstr(pad, &text[i], charlen);
|
|
}
|
|
|
|
printable++;
|
|
i += ((int) charlen == -1 ? offset : (int) charlen);
|
|
} // text loop
|
|
|
|
// text rewriting debugging
|
|
//fwprintf(stderr, L"\n(F): %ls\n\n\n", text);
|
|
|
|
if (p == 0) {
|
|
// now that we have the number of newlines, create the pad to fit
|
|
bufsizey = numlines;
|
|
pad = newpad(bufsizey + 1, bufsizex);
|
|
if (! pad) wrapup(1, "Error constructing pad in renderText.\n");
|
|
scrollok(pad, true);
|
|
wmove(pad, 0, 0);
|
|
}
|
|
|
|
} // workhorse
|
|
free(text);
|
|
redraw();
|
|
}
|
|
|
|
void pane::setTitle(const char* newtitle) {
|
|
// NOTE -- hardcoded length for title string
|
|
if (strlen(newtitle) < 40) {
|
|
strcpy(titlebar, newtitle);
|
|
} else {
|
|
strncpy(titlebar, newtitle, 36);
|
|
strcpy(titlebar+36, "...");
|
|
}
|
|
}
|
|
|
|
void pane::retitle() {
|
|
(hasFocus ? wattrset(win, A_STANDOUT) : standend());
|
|
mvwprintw(win, 0, 1, "%s%s%s", " ", titlebar, " ");
|
|
wstandend(win);
|
|
}
|
|
|
|
void pane::redraw(bool borders) {
|
|
if (borders) {
|
|
box(win, 0, 0);
|
|
retitle();
|
|
}
|
|
wrefresh(win);
|
|
if (is_pad(pad)) {
|
|
prefresh(pad, buflocy, buflocx, pady, padx, pady + height - 3,
|
|
padx + width - 1);
|
|
} else {
|
|
wrefresh(pad);
|
|
}
|
|
}
|
|
|
|
void pane::nextPage() {
|
|
buflocy = (bufsizey - buflocy > 2 * padrows
|
|
? buflocy + padrows - 1
|
|
: bufsizey - padrows);
|
|
redraw();
|
|
}
|
|
|
|
void pane::scrollDown() {
|
|
if (bufsizey - buflocy > padrows) buflocy++;
|
|
redraw();
|
|
}
|
|
|
|
void pane::scrollUp() {
|
|
if (buflocy > 0) buflocy--;
|
|
redraw();
|
|
}
|
|
|
|
void pane::prevPage() {
|
|
buflocy = (buflocy > padrows ? buflocy - padrows : 0);
|
|
redraw();
|
|
}
|
|
|
|
void pane::toggleFocus() {
|
|
hasFocus = !hasFocus;
|
|
retitle();
|
|
redraw();
|
|
}
|
|
|
|
int pane::loadMenu(starray opts) {
|
|
// instruction text
|
|
const char* inst = "(Up/Down - scroll PgUp/PgDn - page up/down"
|
|
" Enter - select ESC - cancel)";
|
|
pady = printInstructions(2, inst);
|
|
|
|
// construct list of items
|
|
ITEM **items = (ITEM**) malloc(sizeof(ITEM*) * (opts.length+1));
|
|
if (! items) wrapup(1, "Error allocating memory in loadMenu.\n");
|
|
for (int i = 0; i < opts.length; i++)
|
|
items[i] = new_item((opts.strings)[i], (opts.strings)[i]);
|
|
items[opts.length] = (ITEM*) NULL;
|
|
|
|
// pad is a ncurses subwindow - set coords relative to win's X & Y
|
|
pady++;
|
|
padx = 4;
|
|
padrows = height - 6;
|
|
padcols = width - 6;
|
|
|
|
pad = derwin(win, padrows, padcols, pady, padx);
|
|
keypad(pad, true);
|
|
|
|
// create the menu & header
|
|
MENU *menu = new_menu((ITEM**) items);
|
|
menu_opts_off(menu, O_SHOWDESC);
|
|
set_menu_win(menu, win);
|
|
set_menu_sub(menu, pad);
|
|
set_menu_mark(menu, "-> ");
|
|
set_menu_format(menu, padrows, 1); // set scrolling
|
|
post_menu(menu);
|
|
redraw();
|
|
|
|
// hit escape to exit without a choice
|
|
int ch;
|
|
while (((ch = getch()) != 27) && (ch != KEY_RESIZE)) {
|
|
switch (ch) {
|
|
|
|
case KEY_PPAGE:
|
|
menu_driver(menu, REQ_SCR_UPAGE);
|
|
break;
|
|
|
|
case KEY_UP:
|
|
menu_driver(menu, REQ_UP_ITEM);
|
|
break;
|
|
|
|
case KEY_DOWN:
|
|
menu_driver(menu, REQ_DOWN_ITEM);
|
|
break;
|
|
|
|
case KEY_NPAGE:
|
|
menu_driver(menu, REQ_SCR_DPAGE);
|
|
break;
|
|
|
|
case '\n': // enter
|
|
int retval = item_index(current_item(menu));
|
|
menuClean(menu, items, opts.length);
|
|
return retval;
|
|
break;
|
|
}
|
|
redraw();
|
|
}
|
|
|
|
// user backed out, return nothing
|
|
menuClean(menu, items, opts.length);
|
|
return (ch == KEY_RESIZE ? -2 : -1);
|
|
}
|
|
|
|
int pane::printInstructions(int y, const char* inst) {
|
|
// NOTE -- starting indentation is hardcoded at two characters in
|
|
long unsigned int strctr = 0;
|
|
long unsigned int line = width - 3;
|
|
while (strlen(&inst[strctr]) > line) {
|
|
// set write length to be up to the last space, for word wrapping
|
|
while ((inst[line] != ' ') && (line > 0)) line--;
|
|
if (line == 0) line = width - 3;
|
|
|
|
mvwaddnstr(win, y++, 2, &inst[strctr], line);
|
|
|
|
strctr += line;
|
|
line = width - 3;
|
|
}
|
|
|
|
if (strlen(inst) != strctr)
|
|
mvwaddnstr(win, y++, 2, &inst[strctr], -1);
|
|
|
|
return y;
|
|
}
|
|
|
|
starray pane::loadForm(starray inputs, const char* secondinst) {
|
|
/* instruction text -- add text using mvwaddnstr because we don't know how
|
|
* large the window is */
|
|
const char* firstinst = "(Up/Down - scroll Enter - select ESC - cancel)";
|
|
pady = printInstructions(2, firstinst);
|
|
pady = printInstructions(pady, secondinst);
|
|
|
|
// pad is a ncurses subwindow - set coords relative to win's X & Y
|
|
padx = 40;
|
|
padrows = height - 5;
|
|
padcols = width - 45;
|
|
|
|
pad = derwin(win, padrows, padcols, pady, padx);
|
|
keypad(win, true);
|
|
curs_set(1);
|
|
|
|
// find out how many valid fields we have
|
|
int validfields = 1;
|
|
for (int i = 0; i < inputs.length; i++) {
|
|
// find out how many extra lines we'll need to put in the form
|
|
if (strcmp((inputs.strings)[i], "SPACE") != 0) validfields++;
|
|
}
|
|
FIELD *fields[validfields];
|
|
|
|
// construct list of fields
|
|
int yoffset = 1;
|
|
int fieldnum = 0;
|
|
for (int i = 0; i < inputs.length; i++) {
|
|
if (strcmp((inputs.strings)[i], "SPACE") == 0) {
|
|
// put in the space
|
|
yoffset++;
|
|
continue;
|
|
}
|
|
fields[fieldnum] = new_field(1, 30, yoffset, 1, 0, 0);
|
|
mvwprintw(win, yoffset + pady, 2, "%s", (inputs.strings)[i]);
|
|
yoffset++;
|
|
set_field_back(fields[fieldnum], A_UNDERLINE);
|
|
field_opts_off(fields[fieldnum], O_AUTOSKIP);
|
|
fieldnum++;
|
|
}
|
|
fields[validfields-1] = NULL;
|
|
|
|
// create the form
|
|
FORM *form = new_form(fields);
|
|
set_form_win(form, win);
|
|
set_form_sub(form, pad);
|
|
post_form(form);
|
|
|
|
// show everything
|
|
redraw();
|
|
|
|
// hit escape or resize terminal to exit without a choice
|
|
int ch;
|
|
starray retval;
|
|
retval = stinit(retval);
|
|
while (((ch = wgetch(win)) != 27) && (ch != KEY_RESIZE)) {
|
|
switch (ch) {
|
|
|
|
case KEY_UP:
|
|
form_driver(form, REQ_PREV_FIELD);
|
|
form_driver(form, REQ_END_LINE);
|
|
break;
|
|
|
|
case KEY_DOWN:
|
|
form_driver(form, REQ_NEXT_FIELD);
|
|
form_driver(form, REQ_END_LINE);
|
|
break;
|
|
|
|
case KEY_BACKSPACE:
|
|
case 127: // these are all backspace - it's terminfo dependent
|
|
case '\b':
|
|
form_driver(form, REQ_DEL_PREV);
|
|
break;
|
|
|
|
case '\n': // enter key - exit out here
|
|
// force validation to ensure last field is written to buffer
|
|
form_driver(form, REQ_VALIDATION);
|
|
|
|
// get inputs
|
|
for (int i = 0; i < validfields-1; i++) {
|
|
char* inp = strdup(field_buffer(fields[i], 0));
|
|
trim(inp);
|
|
retval = stappend(retval, inp);
|
|
}
|
|
|
|
// clean up
|
|
formClean(form, fields, validfields);
|
|
return retval;
|
|
break;
|
|
|
|
default:
|
|
// add character to input
|
|
form_driver(form, ch);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// user backed out, return nothing
|
|
formClean(form, fields, validfields);
|
|
if (ch == KEY_RESIZE) retval.length = -1;
|
|
return retval;
|
|
}
|
|
|
|
void pane::menuClean(MENU* menu, ITEM** items, int numitems) {
|
|
unpost_menu(menu);
|
|
free_menu(menu);
|
|
for (int i = 0; i < numitems + 1; i++) free_item(items[i]);
|
|
free(items);
|
|
}
|
|
|
|
void pane::formClean(FORM* form, FIELD** fields, int numfields) {
|
|
unpost_form(form);
|
|
free_form(form);
|
|
curs_set(0);
|
|
for (int i = 0; i < numfields; i++) free_field(fields[i]);
|
|
}
|
|
|
|
void pane::setModule(const char* newtitle, const char* newmod,
|
|
int keytype) {
|
|
mod.modname = newmod;
|
|
mod.keytype = keytype;
|
|
setTitle(newtitle);
|
|
// update titlebar
|
|
redraw(true);
|
|
}
|
|
|
|
void pane::setKey(const char* newkey) {
|
|
mod.searchkey = newkey;
|
|
setSearch(DEFSEARCH, "Gen 1:1 - Rev 22:21");
|
|
}
|
|
|
|
void pane::setSearch(int type, const char* scope) {
|
|
mod.searchtype = type;
|
|
mod.scope = scope;
|
|
}
|
|
|
|
void pane::setModkey(modkey newmod, const char* newtitle) {
|
|
setModule(newtitle, newmod.modname, newmod.keytype);
|
|
setKey(newmod.searchkey);
|
|
setSearch(newmod.searchtype, newmod.scope);
|
|
}
|