// 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 . // pane.cpp -- subwindow management #include #include #include #include #include #include #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], "", 1)) && (p == 1)) { // end of redletter bracket makered = 0; } else if ((! strmatch(&text[i], "", 1)) && (p == 1)) { // end of interpretive text bracket makeital = 0; } else if ((! strmatch(&text[i], "

", 1)) && (p == 0)) { // paragraph break - replace

with '\n' int endindex = strmatch(&text[i], "

", 0); if (endindex != -1) memcpy(&text[i + endindex + 2], ">\n", 2); } else if ((! strmatch(&text[i], "word */ // 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], "", 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], "= 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); }