scriptura/pane.cpp

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);
}