1// This file is part of SmallBASIC
2//
3// Copyright(C) 2001-2019 Chris Warren-Smith.
4//
5// This program is distributed under the terms of the GPL v2.0 or later
6// Download the GNU Public License (GPL) from www.gnu.org
7//
8
9#include <config.h>
10#include <errno.h>
11#include <ctype.h>
12#include <unistd.h>
13#include <stdlib.h>
14#include <FL/fl_ask.H>
15#include <FL/fl_draw.H>
16#include <FL/Fl_Rect.H>
17#include <FL/Fl_XPM_Image.H>
18#include <FL/Fl_Check_Button.H>
19#include <FL/Fl_Radio_Button.H>
20#include <FL/Fl_Choice.H>
21#include <FL/Fl_Menu_Item.H>
22#include <FL/Fl_Input.H>
23#include <FL/Fl_Output.H>
24#include <FL/Fl_Shared_Image.H>
25#include <FL/Fl_Slider.H>
26#include <FL/Fl_Value_Input.H>
27#include <FL/Fl_Window.H>
28#include <FL/filename.H>
29
30#define Fl_HELP_WIDGET_RESOURCES
31#include "platform/fltk/HelpWidget.h"
32
33#define FOREGROUND_COLOR fl_rgb_color(0x12, 0x12, 0x12)
34#define BACKGROUND_COLOR fl_rgb_color(192, 192, 192)
35#define ANCHOR_COLOR fl_rgb_color(0,0,255)
36#define BUTTON_COLOR fl_rgb_color(0,0,255)
37
38#define DEFAULT_INDENT 8
39#define TABLE_PADDING 4
40#define CODE_PADDING 4
41#define LI_INDENT 18
42#define FONT_SIZE_H1 23
43#define CELL_SPACING 4
44#define INPUT_WIDTH 90
45#define BUTTON_WIDTH 20
46#define SCROLL_SIZE 10000
47#define HSCROLL_STEP 20
48#define ELIPSE_LEN 10
49#define IMG_TEXT_BORDER 25
50#define NO_COLOR 0
51
52Fl_Color getColor(strlib::String *s, Fl_Color def);
53void lineBreak(const char *s, int slen, int width, int &stlen, int &pxlen);
54const char *skipWhite(const char *s);
55bool unquoteTag(const char *tagBegin, const char *&tagEnd);
56Fl_Image *loadImage(const char *imgSrc);
57struct AnchorNode;
58struct InputNode;
59static char truestr[] = "true";
60static char falsestr[] = "false";
61static char spacestr[] = " ";
62static char anglestr[] = "<";
63char rangeValue[20];
64
65//--Display---------------------------------------------------------------------
66
67struct Display {
68 strlib::String *selection;
69 Fl_Group *wnd;
70 AnchorNode *anchor;
71 Fl_Font font;
72 Fl_Color color;
73 Fl_Color background;
74 Fl_Color selectionColor;
75 Fl_Color anchorColor;
76 int16_t x1, x2, y1, y2;
77 int16_t tabW, tabH;
78 int16_t lineHeight;
79 int16_t indent;
80 int16_t baseIndent;
81 uint16_t fontSize;
82 uint16_t tableLevel;
83 uint16_t nodeId;
84 int16_t imgY;
85 uint16_t imgIndent;
86 int16_t markX, markY, pointX, pointY;
87 uint8_t uline;
88 uint8_t center;
89 uint8_t content;
90 uint8_t exposed;
91 uint8_t measure;
92 uint8_t selected;
93 uint8_t invertedSel;
94 uint8_t insideCode;
95 uint8_t insideUl;
96
97 void drawBackground(Fl_Rect &rc) {
98 if (background != NO_COLOR) {
99 Fl_Color oldColor = fl_color();
100 fl_color(background);
101 fl_rectf(rc.x(), rc.y(), rc.w(), rc.h());
102 fl_color(oldColor);
103 }
104 }
105 void endImageFlow() {
106 // end text flow around images
107 if (imgY != -1) {
108 indent = imgIndent;
109 x1 = indent;
110 y1 = imgY + 1;
111 imgY = -1;
112 }
113 }
114
115 void newRow(uint16_t nrows = 1, bool doBackground = true) {
116 int bgY = y1;
117
118 x1 = indent;
119 y1 += nrows *lineHeight;
120 // flow around images
121 if (imgY != -1 && y1 > imgY) {
122 imgY = -1;
123 x1 = indent = imgIndent;
124 }
125
126 if (!measure && background != NO_COLOR && doBackground) {
127 Fl_Rect rc(x1, bgY, x2 - x1 + CELL_SPACING, lineHeight);
128 drawBackground(rc);
129 }
130 }
131
132 // restore previous colors
133 void restoreColors() {
134 fl_color(color);
135 background = oldBackground;
136 }
137
138 void setColors(Fl_Color nodeForeground, Fl_Color nodeBackground) {
139 fl_color(nodeForeground != NO_COLOR ? nodeForeground : color);
140
141 oldBackground = background;
142 if (nodeBackground != NO_COLOR) {
143 background = nodeBackground;
144 }
145 }
146
147private:
148 Fl_Color oldBackground;
149};
150
151//--Attributes------------------------------------------------------------------
152
153struct Value {
154 int value;
155 bool relative;
156};
157
158struct Attributes : public Properties<String *> {
159 Attributes(int growSize) : Properties(growSize) {
160 }
161 strlib::String *getValue() {
162 return get("value");
163 }
164 strlib::String *getName() {
165 return get("name");
166 }
167 strlib::String *getHref() {
168 return get("href");
169 }
170 strlib::String *getType() {
171 return get("type");
172 }
173 strlib::String *getSrc() {
174 return get("src");
175 }
176 strlib::String *getOnclick() {
177 return get("onclick");
178 }
179 strlib::String *getBgColor() {
180 return get("bgcolor");
181 }
182 strlib::String *getFgColor() {
183 return get("fgcolor");
184 }
185 strlib::String *getBackground() {
186 return get("background");
187 }
188 strlib::String *getAlign() {
189 return get("align");
190 }
191 bool isReadonly() {
192 return get("readonly") != 0;
193 }
194 void getValue(strlib::String &s) {
195 s.append(getValue());
196 }
197 void getName(strlib::String &s) {
198 s.append(getName());
199 }
200 void getHref(strlib::String &s) {
201 s.append(getHref());
202 }
203 void getType(strlib::String &s) {
204 s.append(getType());
205 }
206 Value getWidth(int def = -1) {
207 return getValue("width", def);
208 }
209 Value getHeight(int def = -1) {
210 return getValue("height", def);
211 }
212 int getSize(int def = -1) {
213 return getIntValue("size", def);
214 }
215 int getStart(int def = 0) {
216 return getIntValue("start", def);
217 }
218 int getBorder(int def = -1) {
219 return getIntValue("border", def);
220 }
221 int getRows(int def = 1) {
222 return getIntValue("rows", def);
223 }
224 int getCols(int def = 20) {
225 return getIntValue("cols", def);
226 }
227 int getMax(int def = 1) {
228 return getIntValue("min", def);
229 }
230 int getMin(int def = 1) {
231 return getIntValue("max", def);
232 }
233 int getColSpan(int def = 1) {
234 return getIntValue("colspan", def);
235 }
236 int getIntValue(const char *attr, int def);
237 Value getValue(const char *attr, int def);
238};
239
240int Attributes::getIntValue(const char *attr, int def) {
241 strlib::String *s = get(attr);
242 return (s != NULL ? s->toInteger() : def);
243}
244
245Value Attributes::getValue(const char *attr, int def) {
246 Value val;
247 val.relative = false;
248 strlib::String *s = get(attr);
249 if (s) {
250 int ipc = s->indexOf('%', 0);
251 if (ipc != -1) {
252 val.relative = true;
253 }
254 val.value = s->toInteger();
255 } else {
256 val.value = def;
257 }
258 return val;
259}
260
261//--BaseNode--------------------------------------------------------------------
262
263struct BaseNode {
264 virtual ~BaseNode() {}
265 virtual void display(Display *out) {}
266 virtual int indexOf(const char *sFind, uint8_t matchCase) { return -1; }
267 virtual void getText(strlib::String *s) {}
268 virtual int getY() { return -1; }
269};
270
271//-- MeasureNode----------------------------------------------------------------
272
273struct MeasureNode : public BaseNode {
274 MeasureNode() : initX(0), initY(0) {}
275 int16_t initX, initY;
276};
277
278//--CodeNode--------------------------------------------------------------------
279
280struct CodeNode : public MeasureNode {
281 CodeNode(int fontSize) :
282 MeasureNode(),
283 _font(FL_COURIER),
284 _fontSize(fontSize),
285 _nodeId(0),
286 _xEnd(0),
287 _yEnd(0),
288 _sameLine(false) {
289 }
290 void display(Display *out);
291 void doEndBlock(Display *out);
292 Fl_Font _font;
293 uint16_t _fontSize;
294 uint16_t _nodeId;
295 int16_t _xEnd;
296 int16_t _yEnd;
297 bool _sameLine;
298};
299
300void CodeNode::display(Display *out) {
301 _sameLine = false;
302 out->endImageFlow();
303 fl_font(_font, _fontSize);
304
305 if (out->exposed) {
306 // defer drawing in child nodes
307 out->measure = true;
308 initX = out->x1;
309 initY = out->y1;
310 _font = out->font;
311 _nodeId = out->nodeId;
312 } else if (!out->measure) {
313 int textHeight = fl_height() + fl_descent();
314 int xpos = initX;
315 int ypos = (initY - textHeight) + fl_descent() * 2;
316 int width, height;
317 if (_yEnd == initY + (CODE_PADDING * 2)) {
318 _sameLine = true;
319 width = _xEnd - (CODE_PADDING * 2);
320 height = out->lineHeight;
321 } else {
322 width = out->x2 - (DEFAULT_INDENT * 2);
323 height = _yEnd - initY + textHeight - fl_descent();
324 }
325 fl_color(fl_lighter(out->color));
326 fl_rectf(xpos, ypos, width, height);
327 fl_color(out->background);
328 if (!_sameLine) {
329 fl_rect(xpos, ypos, width, height);
330 }
331 }
332
333 out->insideCode = true;
334
335 if (_sameLine) {
336 out->x1 += CODE_PADDING;
337 } else {
338 out->indent += CODE_PADDING;
339 out->x1 = out->indent;
340 out->y1 += CODE_PADDING;
341 }
342}
343
344void CodeNode::doEndBlock(Display *out) {
345 out->insideCode = false;
346
347 if (!_sameLine) {
348 out->indent -= CODE_PADDING;
349 if (out->indent < out->baseIndent) {
350 out->indent = out->baseIndent;
351 }
352 out->y1 += CODE_PADDING;
353 }
354
355 if (out->exposed) {
356 out->measure = false;
357 out->nodeId = _nodeId;
358 _xEnd = out->x1;
359 _yEnd = out->y1;
360 }
361}
362
363struct CodeEndNode : public BaseNode {
364 CodeEndNode(CodeNode *head) : _head(head) {}
365 void display(Display *out);
366 CodeNode *_head;
367};
368
369void CodeEndNode::display(Display *out) {
370 fl_color(out->color);
371 if (_head) {
372 _head->doEndBlock(out);
373 }
374}
375
376//--FontNode--------------------------------------------------------------------
377
378struct FontNode : public BaseNode {
379 FontNode(Fl_Font font, int fontSize, Fl_Color color, bool bold, bool italic);
380 void display(Display *out);
381
382 Fl_Font _font;
383 Fl_Color _color;
384 uint16_t _fontSize;
385};
386
387FontNode::FontNode(Fl_Font font, int fontSize, Fl_Color color, bool bold, bool italic) :
388 BaseNode(),
389 _font(font),
390 _color(color),
391 _fontSize(fontSize) {
392 if (bold) {
393 _font += FL_BOLD;
394 }
395 if (italic) {
396 _font += FL_ITALIC;
397 }
398}
399
400void FontNode::display(Display *out) {
401 fl_font(_font, _fontSize);
402 if (_color == (Fl_Color) - 1) {
403 fl_color(out->color); // </font> restores color
404 } else if (_color != 0) {
405 fl_color(_color);
406 }
407 int oldLineHeight = out->lineHeight;
408 out->lineHeight = fl_height();
409 if (out->lineHeight > oldLineHeight) {
410 out->y1 += (out->lineHeight - oldLineHeight);
411 }
412 out->font = _font;
413 out->fontSize = _fontSize;
414}
415
416//--BrNode----------------------------------------------------------------------
417
418struct BrNode : public BaseNode {
419 BrNode(uint8_t premode) :
420 BaseNode(),
421 premode(premode) {
422 }
423
424 void display(Display *out) {
425 // when <pre> is active don't flow text around images
426 if (premode && out->imgY != -1) {
427 out->endImageFlow();
428 } else {
429 out->newRow(1);
430 }
431 out->lineHeight = fl_height() + fl_descent();
432 }
433 uint8_t premode;
434};
435
436//--AnchorNode------------------------------------------------------------------
437
438struct AnchorNode : public BaseNode {
439 AnchorNode(Attributes &p) :
440 BaseNode(),
441 x1(0),
442 x2(0),
443 y1(0),
444 y2(0),
445 lineHeight(0),
446 wrapxy(0),
447 pushed(0) {
448 p.getName(name);
449 p.getHref(href);
450 }
451
452 void display(Display *out) {
453 if (pushed) {
454 out->uline = true;
455 }
456 out->anchor = this;
457 x1 = x2 = out->x1;
458 y1 = y2 = out->y1 - out->lineHeight;
459 wrapxy = 0;
460 if (href.length() > 0) {
461 fl_color(out->anchorColor);
462 }
463 }
464
465 bool ptInSegment(int x, int y) {
466 if (y > y1 && y < y2) {
467 // found row
468 if ((x < x1 && y < y1 + lineHeight) ||
469 (x > x2 && y > y2 - lineHeight)) {
470 // outside row start or end
471 return false;
472 }
473 return true;
474 }
475 return false;
476 }
477
478 int getY() {
479 return y1;
480 }
481
482 int16_t x1, x2, y1, y2;
483 uint16_t lineHeight;
484 uint8_t wrapxy; // begin on page boundary
485 uint8_t pushed;
486 uint8_t __padding[4];
487 strlib::String name;
488 strlib::String href;
489};
490
491AnchorNode *pushedAnchor = 0;
492
493struct AnchorEndNode : public BaseNode {
494 AnchorEndNode() :
495 BaseNode() {
496 }
497
498 void display(Display *out) {
499 AnchorNode *beginNode = out->anchor;
500 if (beginNode) {
501 beginNode->x2 = out->x1;
502 beginNode->y2 = out->y1;
503 beginNode->lineHeight = out->lineHeight;
504 }
505 out->uline = false;
506 out->anchor = 0;
507 fl_color(out->color);
508 }
509};
510
511//--StyleNode-------------------------------------------------------------------
512
513struct StyleNode : public BaseNode {
514 StyleNode(uint8_t uline, uint8_t center) :
515 BaseNode(),
516 uline(uline), center(center) {
517 }
518
519 void display(Display *out) {
520 out->uline = uline;
521 out->center = center;
522 }
523 uint8_t uline; // 2
524 uint8_t center; // 2
525};
526
527//--LiNode----------------------------------------------------------------------
528
529struct UlNode : public BaseNode {
530 UlNode(Attributes &p, bool ordered) :
531 BaseNode(),
532 start(p.getStart(1)),
533 ordered(ordered) {
534 }
535 void display(Display *out) {
536 nextId = start;
537 out->insideUl = true;
538 out->newRow(1);
539 out->indent += LI_INDENT;
540 }
541 int nextId;
542 int start;
543 bool ordered;
544};
545
546struct UlEndNode : public BaseNode {
547 UlEndNode() :
548 BaseNode() {
549 }
550 void display(Display *out) {
551 out->insideUl = false;
552 out->indent -= LI_INDENT;
553 out->newRow(1);
554 }
555};
556
557struct LiNode : public BaseNode {
558 LiNode(UlNode *ulNode) :
559 ulNode(ulNode) {
560 }
561
562 void display(Display *out) {
563 out->content = true;
564 out->x1 = out->indent;
565 out->y1 += out->lineHeight;
566 int x = out->x1 - (LI_INDENT - DEFAULT_INDENT);
567 int y = out->y1 - fl_height() - fl_descent();
568 if (out->measure == false) {
569 if (ulNode && ulNode->ordered) {
570 char t[10];
571 sprintf(t, "%d.", ulNode->nextId++);
572 fl_draw(t, 2, x - 4, out->y1);
573 } else {
574 dotImage.draw(x - 4, y + fl_height() - fl_descent(), 8, 8);
575 // draw messes with the current font - restore
576 fl_font(out->font, out->fontSize);
577 }
578 }
579 }
580 UlNode *ulNode;
581};
582
583//--ImageNode-------------------------------------------------------------------
584
585struct ImageNode : public BaseNode {
586 ImageNode(strlib::String *docHome, Attributes *a);
587 ImageNode(strlib::String *docHome, strlib::String *src, bool fixed);
588 ImageNode(Fl_Image *image);
589 void makePath(strlib::String *src, strlib::String *docHome);
590 void reload();
591 void display(Display *out);
592 Fl_Image *image;
593 Value w, h;
594 uint8_t background, fixed;
595 uint8_t valign; // 0=top, 1=center, 2=bottom
596 uint8_t __padding[5];
597 strlib::String path, url;
598};
599
600ImageNode::ImageNode(strlib::String *docHome, Attributes *a) :
601 BaseNode(),
602 background(false),
603 fixed(false),
604 valign(0) {
605 makePath(a->getSrc(), docHome);
606 image = loadImage(path.c_str());
607 w = a->getWidth(image->w());
608 h = a->getHeight(image->h());
609}
610
611ImageNode::ImageNode(strlib::String *docHome, strlib::String *src, bool fixed) :
612 BaseNode(),
613 background(false),
614 fixed(false),
615 valign(0) {
616 makePath(src, docHome);
617 image = loadImage(path.c_str());
618 w.value = image->w();
619 h.value = image->h();
620 w.relative = 0;
621 h.relative = 0;
622}
623
624ImageNode::ImageNode(Fl_Image *image) :
625 BaseNode(),
626 image(image),
627 background(false),
628 fixed(false),
629 valign(2) {
630 w.value = image->w();
631 h.value = image->h();
632 w.relative = 0;
633 h.relative = 0;
634 valign = 2;
635}
636
637void ImageNode::makePath(strlib::String *src, strlib::String *docHome) {
638 // <img src=blah/images/g.gif>
639 url.append(src); // html path
640 path.append(docHome); // local file system path
641 if (src) {
642 if ((*src)[0] == '/') {
643 path.append(src->substring(1));
644 } else {
645 path.append(src);
646 }
647 }
648}
649
650void ImageNode::reload() {
651 image = loadImage(path.c_str());
652 int iw = image->w();
653 int ih = image->h();
654 if (w.relative == 0) {
655 w.value = iw;
656 }
657 if (h.relative == 0) {
658 h.value = ih;
659 }
660}
661
662void ImageNode::display(Display *out) {
663 if (image == 0) {
664 return;
665 }
666 int iw = w.relative ? (w.value *(out->x2 - out->x1) / 100) : w.value < out->x2 ? w.value : out->x2;
667 int ih = h.relative ? (h.value *(out->wnd->h() - out->y1) / 100) : h.value;
668 if (out->measure == false) {
669 if (background) {
670 // tile image inside rect x,y,tabW,tabH
671 int x = out->x1 - 1;
672 int y = fixed ? 0 : out->y1 - fl_height();
673 int y1 = y;
674 int numHorz = out->tabW / w.value;
675 int numVert = out->tabH / h.value;
676 for (int iy = 0; iy <= numVert; iy++) {
677 int x1 = x;
678 for (int ix = 0; ix <= numHorz; ix++) {
679 if (x1 + w.value > x + out->tabW) {
680 iw = out->tabW - (x1 - x);
681 } else {
682 iw = w.value;
683 }
684 if (y1 + h.value > y + out->tabH) {
685 ih = out->tabH - (y1 - y);
686 } else {
687 ih = h.value;
688 }
689 image->draw(x1, y1, iw, ih);
690 x1 += w.value;
691 }
692 y1 += h.value;
693 }
694 } else {
695 int x = out->x1 + DEFAULT_INDENT;
696 int y = out->y1;
697 switch (valign) {
698 case 0: // top
699 y -= fl_height();
700 break;
701 case 1: // center
702 break;
703 case 2: // bottom
704 break;
705 }
706 if (out->anchor && out->anchor->pushed) {
707 x += 1;
708 y += 1;
709 }
710 image->draw(x, y, iw, ih);
711 }
712 }
713 if (background == 0) {
714 out->content = true;
715 if (iw + IMG_TEXT_BORDER > out->x2) {
716 out->x1 = out->indent;
717 out->y1 += ih;
718 out->imgY = -1;
719 } else {
720 out->imgY = out->y1 + ih;
721 out->imgIndent = out->indent;
722 out->x1 += iw + DEFAULT_INDENT;
723 out->indent = out->x1;
724 }
725 }
726 fl_font(out->font, out->fontSize); // restore font
727}
728
729//--TextNode--------------------------------------------------------------------
730
731struct TextNode : public BaseNode {
732 TextNode(const char *s, uint16_t textlen);
733 void display(Display *out);
734 void drawSelection(const char *s, uint16_t len, uint16_t width, Display *out);
735 int indexOf(const char *sFind, uint8_t matchCase);
736 void getText(strlib::String *s);
737 int getY();
738
739 const char *s;
740 uint16_t textlen;
741 uint16_t width;
742 int16_t ybegin;
743};
744
745TextNode::TextNode(const char *s, uint16_t textlen) :
746 BaseNode(),
747 s(s),
748 textlen(textlen),
749 width(0),
750 ybegin(0) {
751}
752
753void TextNode::getText(strlib::String *s) {
754 s->append(this->s, this->textlen);
755}
756
757void TextNode::drawSelection(const char *s, uint16_t len, uint16_t width, Display *out) {
758 int out_y = out->y1 - fl_height();
759 if (out->pointY < out_y) {
760 return; // selection above text
761 }
762 if (out->markY > out_y + out->lineHeight) {
763 return; // selection below text
764 }
765
766 Fl_Rect rc(out->x1, out_y, width, out->lineHeight + fl_descent());
767 int selBegin = 0; // selection index into the draw string
768 int selEnd = len;
769
770 if (out->markY > out_y) {
771 if (out->pointY < out_y + out->lineHeight) {
772 // paint single row selection
773 int16_t leftX, rightX;
774 if (out->markX < out->pointX) {
775 leftX = out->markX;
776 rightX = out->pointX;
777 } else { // invert selection
778 leftX = out->pointX;
779 rightX = out->markX;
780 }
781
782 if (leftX > out->x1 + width || rightX < out->x1) {
783 return; // selection left or right of text
784 }
785
786 bool left = true;
787 int x = out->x1;
788 // find the left+right margins
789 if (leftX == rightX) {
790 // double click - select word
791 for (int i = 0; i < len; i++) {
792 int width = fl_width(s + i, 1);
793 x += width;
794 if (left) {
795 if (s[i] == ' ') {
796 rc.x(x);
797 selBegin = i + 1;
798 }
799 if (x > leftX) {
800 left = false;
801 }
802 } else if (s[i] == ' ' && x > rightX) {
803 rc.w(x - rc.x() - width);
804 selEnd = i;
805 break;
806 }
807 }
808 } else {
809 // drag row - draw around character boundary
810 for (int i = 0; i < len; i++) {
811 x += fl_width(s + i, 1);
812 if (left) {
813 if (x < leftX) {
814 rc.x(x);
815 selBegin = i + 1;
816 } else {
817 left = false;
818 }
819 } else if (x > rightX) {
820 rc.w(x - rc.x());
821 selEnd = i + 1;
822 break;
823 }
824 }
825 }
826 } else {
827 // top row multiline - find the left margin
828 int16_t leftX = out->invertedSel ? out->pointX : out->markX;
829 if (leftX > out->x1 + width) {
830 // selection left of text
831 return;
832 }
833 int x = out->x1;
834 int char_w = 0;
835 for (int i = 0; i < len; i++) {
836 char_w = fl_width(s + i, 1);
837 x += char_w;
838 if (x < leftX) {
839 rc.x(x);
840 selBegin = i;
841 } else {
842 break;
843 }
844 }
845 // subtract the left non-selected segement length from the right side
846 rc.w(rc.w() - (x - out->x1) + char_w);
847 }
848 } else {
849 if (out->pointY < out_y + out->lineHeight) {
850 // bottom row multiline - find the right margin
851 int16_t rightX = out->invertedSel ? out->markX : out->pointX;
852 if (rightX < out->x1) {
853 return;
854 }
855 int x = out->x1;
856 for (int i = 0; i < len; i++) {
857 x += fl_width(s + i, 1);
858 if (x > rightX) {
859 rc.w(x - rc.x());
860 selEnd = i + 1;
861 break;
862 }
863 }
864 }
865 // else middle row multiline - fill left+right
866 }
867
868 if (selEnd > selBegin && out->selection != 0) {
869 // capture the selected text
870 out->selection->append(s + selBegin, selEnd - selBegin);
871 }
872
873 fl_color(out->selectionColor);
874 fl_rectf(rc.x(), rc.y(), rc.w(), rc.h());
875 fl_color(out->color);
876}
877
878void TextNode::display(Display *out) {
879 ybegin = out->y1;
880 out->content = true;
881
882 if (width == 0) {
883 width = fl_width(s, textlen);
884 }
885 if (width < out->x2 - out->x1) {
886 // simple non-wrapping textout
887 if (out->center) {
888 int xctr = ((out->x2 - out->x1) - width) / 2;
889 if (xctr > out->x1) {
890 out->x1 = xctr;
891 }
892 }
893 if (out->measure == false) {
894 if (out->selected) {
895 drawSelection(s, textlen, width, out);
896 }
897 fl_draw(s, textlen, out->x1, out->y1);
898 if (out->uline) {
899 fl_line(out->x1, out->y1 + 1, out->x1 + width, out->y1 + 1);
900 }
901 }
902 out->x1 += width;
903 } else {
904 int linelen, linepx, cliplen;
905 int len = textlen;
906 const char *p = s;
907 while (len > 0) {
908 lineBreak(p, len, out->x2 - out->x1, linelen, linepx);
909 cliplen = linelen;
910 if (linepx > out->x2 - out->x1) {
911 // no break point - create new line if not already on one
912 if (out->x1 != out->indent) {
913 out->newRow();
914 // anchor now starts on a new line
915 if (out->anchor && out->anchor->wrapxy == false) {
916 out->anchor->x1 = out->x1;
917 out->anchor->y1 = out->y1 - out->lineHeight;
918 out->anchor->wrapxy = true;
919 }
920 }
921 // clip long text - leave room for elipses
922 int cellW = out->x2 - out->indent - ELIPSE_LEN;
923 if (linepx > cellW) {
924 linepx = 0;
925 cliplen = 0;
926 do {
927 linepx += fl_width(p + cliplen, 1);
928 cliplen++;
929 }
930 while (linepx < cellW);
931 }
932 }
933 if (out->measure == false) {
934 if (out->selected) {
935 drawSelection(p, cliplen, linepx, out);
936 }
937 fl_draw(p, cliplen, out->x1, out->y1);
938 if (out->uline) {
939 fl_line(out->x1, out->y1 + 1, out->x1 + linepx, out->y1 + 1);
940 }
941 if (cliplen != linelen) {
942 fl_point(out->x1 + linepx, out->y1);
943 fl_point(out->x1 + linepx + 2, out->y1);
944 fl_point(out->x1 + linepx + 4, out->y1);
945 }
946 }
947 p += linelen;
948 len -= linelen;
949
950 if (out->anchor) {
951 out->anchor->wrapxy = true;
952 }
953 if (out->x1 + linepx < out->x2) {
954 out->x1 += linepx;
955 } else {
956 out->newRow();
957 }
958 }
959 }
960}
961
962int TextNode::indexOf(const char *sFind, uint8_t matchCase) {
963 int numMatch = 0;
964 int findLen = strlen(sFind);
965 for (int i = 0; i < textlen; i++) {
966 uint8_t equals = matchCase ? s[i] == sFind[numMatch] : toupper(s[i]) == toupper(sFind[numMatch]);
967 numMatch = (equals ? numMatch + 1 : 0);
968 if (numMatch == findLen) {
969 return i + 1;
970 }
971 }
972 return -1;
973}
974
975int TextNode::getY() {
976 return ybegin;
977}
978
979//--HrNode----------------------------------------------------------------------
980
981struct HrNode : public BaseNode {
982 HrNode() :
983 BaseNode() {
984 }
985 void display(Display *out) {
986 if (out->imgY != -1) {
987 out->endImageFlow();
988 out->y1 -= out->lineHeight;
989 }
990 out->y1 += 4;
991 out->x1 = out->indent;
992 if (out->measure == false) {
993 fl_color(FL_DARK1);
994 fl_line(out->x1, out->y1 + 1, out->x2 - 6, out->y1 + 1);
995 fl_color(FL_DARK2);
996 fl_line(out->x1, out->y1 + 2, out->x2 - 6, out->y1 + 2);
997 fl_color(out->color);
998 }
999 out->y1 += out->lineHeight + 2;
1000 }
1001};
1002
1003//--ParagraphNode---------------------------------------------------------------
1004
1005struct ParagraphNode : public BaseNode {
1006 ParagraphNode() : BaseNode() {}
1007 void display(Display *out) {
1008 if (out->imgY != -1) {
1009 out->endImageFlow();
1010 } else if (!out->insideCode && !out->insideUl) {
1011 out->newRow(2);
1012 }
1013 }
1014};
1015
1016//--Table Support---------------------------------------------------------------
1017
1018struct TableNode;
1019
1020struct TrNode : public BaseNode {
1021 TrNode(TableNode *tableNode, Attributes *a);
1022 void display(Display *out);
1023
1024 TableNode *table;
1025 Fl_Color background, foreground;
1026 uint16_t cols, __padding1;
1027 int16_t y1, height;
1028};
1029
1030struct TrEndNode : public BaseNode {
1031 TrEndNode(TrNode *trNode);
1032 void display(Display *out);
1033
1034 TrNode *tr;
1035};
1036
1037struct TdNode : public BaseNode {
1038 TdNode(TrNode *trNode, Attributes *a);
1039 void display(Display *out);
1040
1041 TrNode *tr;
1042 Value width;
1043 Fl_Color background, foreground;
1044 uint16_t colspan;
1045};
1046
1047struct TdEndNode : public BaseNode {
1048 TdEndNode(TdNode *tdNode);
1049 void display(Display *out);
1050
1051 TdNode *td;
1052};
1053
1054struct TableNode : public MeasureNode {
1055 TableNode(Attributes *a);
1056 ~TableNode();
1057 void display(Display *out);
1058 void doEndTD(Display *out, TrNode *tr, Value *tdWidth);
1059 void doEndTable(Display *out);
1060 void setColWidth(Value *width);
1061 void cleanup();
1062
1063 uint16_t *columns;
1064 int16_t *sizes;
1065 uint16_t rows, cols;
1066 uint16_t nextCol;
1067 uint16_t nextRow;
1068 uint16_t width;
1069 uint16_t nodeId;
1070 int16_t maxY; // end of table
1071 int16_t border;
1072};
1073
1074struct TableEndNode : public BaseNode {
1075 TableEndNode(TableNode *tableNode);
1076 void display(Display *out);
1077
1078 TableNode *table;
1079};
1080
1081//--TableNode-------------------------------------------------------------------
1082
1083TableNode::TableNode(Attributes *a) :
1084 MeasureNode(),
1085 columns(0),
1086 sizes(0),
1087 rows(0),
1088 cols(0) {
1089 border = a->getBorder();
1090}
1091
1092TableNode::~TableNode() {
1093 cleanup();
1094}
1095
1096void TableNode::display(Display *out) {
1097 nextCol = 0;
1098 nextRow = 0;
1099 out->endImageFlow();
1100 width = out->x2 - out->indent;
1101 initX = out->indent;
1102 initY = maxY = out->y1;
1103 nodeId = out->nodeId;
1104
1105 if (out->content) {
1106 // update table initial row position- we remember content
1107 // state on re-visiting the table via the value of initY
1108 initY += out->lineHeight + CELL_SPACING;
1109 maxY = initY;
1110 }
1111 out->content = false;
1112
1113 if (cols && (out->exposed || columns == 0)) {
1114 // prepare default column widths
1115 if (out->tableLevel == 0) {
1116 // traverse the table structure to determine widths
1117 out->measure = true;
1118 }
1119 cleanup();
1120 columns = (uint16_t *)malloc(sizeof(uint16_t) * cols);
1121 sizes = (int16_t *)malloc(sizeof(int16_t) * cols);
1122 int cellW = width / cols;
1123 for (int i = 0; i < cols; i++) {
1124 columns[i] = cellW * (i + 1);
1125 sizes[i] = 0;
1126 }
1127 }
1128 int lineHeight = fl_height() + fl_descent();
1129 if (lineHeight > out->lineHeight) {
1130 out->lineHeight = lineHeight;
1131 }
1132 out->tableLevel++;
1133}
1134
1135// called from </td> to prepare for wrapping and resizing
1136void TableNode::doEndTD(Display *out, TrNode *tr, Value *tdWidth) {
1137 int index = nextCol - 1;
1138 if (out->y1 > maxY || tdWidth->value != -1) {
1139 // veto column changes - wrapped or fixed-width cell
1140 sizes[index] = -1;
1141 } else if (out->y1 == tr->y1 && out->x1 < columns[index] && out->x1 > sizes[index] && sizes[index] != -1) {
1142 // largest <td></td> on same line, less than the default width
1143 // add CELL_SPACING*2 since <td> reduces width by CELL_SPACING
1144 sizes[index] = out->x1 + (CELL_SPACING * 3);
1145 }
1146
1147 if (out->y1 > maxY) {
1148 maxY = out->y1; // new max table height
1149 }
1150 // close image flow to prevent bleeding into previous cell
1151 out->imgY = -1;
1152}
1153
1154void TableNode::doEndTable(Display *out) {
1155 out->x2 = width;
1156 out->indent = initX;
1157 out->x1 = initX;
1158 out->y1 = maxY;
1159 if (out->content) {
1160 out->newRow(1, false);
1161 }
1162 out->content = false;
1163 out->tableLevel--;
1164 out->tabH = out->y1 - initY;
1165 out->tabW = width;
1166
1167 if (cols && columns && out->exposed) {
1168 // adjust columns for best fit (left align)
1169 int delta = 0;
1170 for (int i = 0; i < cols - 1; i++) {
1171 if (sizes[i] > 0) {
1172 int spacing = columns[i] - sizes[i];
1173 columns[i] = sizes[i] - delta;
1174 delta += spacing;
1175 } else {
1176 // apply delta only to wrapped column
1177 columns[i] -= delta;
1178 }
1179 }
1180 // redraw outer tables
1181 if (out->tableLevel == 0) {
1182 out->measure = false;
1183 out->nodeId = nodeId;
1184 }
1185 }
1186}
1187
1188void TableNode::setColWidth(Value *colw) {
1189 // set user specified column width
1190 int tdw = colw->relative ? colw->value * width / 100 : colw->value < width ? colw->value : width;
1191 int delta = columns[nextCol] - tdw;
1192 columns[nextCol] = tdw;
1193 for (int i = nextCol + 1; i < cols - 1; i++) {
1194 columns[i] -= delta;
1195 }
1196}
1197
1198void TableNode::cleanup() {
1199 if (columns) {
1200 free(columns);
1201 }
1202 if (sizes) {
1203 free(sizes);
1204 }
1205}
1206
1207TableEndNode::TableEndNode(TableNode *tableNode) :
1208 BaseNode(),
1209 table(tableNode) {
1210}
1211
1212void TableEndNode::display(Display *out) {
1213 if (table) {
1214 table->doEndTable(out);
1215 }
1216}
1217
1218//--TrNode----------------------------------------------------------------------
1219
1220TrNode::TrNode(TableNode *tableNode, Attributes *a) :
1221 BaseNode(),
1222 table(tableNode),
1223 cols(0),
1224 y1(0),
1225 height(0) {
1226 if (table) {
1227 table->rows++;
1228 }
1229 foreground = getColor(a->getFgColor(), NO_COLOR);
1230 background = getColor(a->getBgColor(), NO_COLOR);
1231}
1232
1233void TrNode::display(Display *out) {
1234 out->setColors(foreground, background);
1235
1236 if (table == 0) {
1237 return;
1238 }
1239
1240 if (out->content) {
1241 // move bottom of <tr> to next line
1242 table->maxY += out->lineHeight + TABLE_PADDING;
1243 }
1244 out->content = false;
1245 y1 = table->maxY;
1246 table->nextCol = 0;
1247 table->nextRow++;
1248
1249 if (background && out->measure == false) {
1250 Fl_Rect rc(table->initX, y1 - fl_height(), table->width, out->lineHeight);
1251 out->drawBackground(rc);
1252 }
1253}
1254
1255TrEndNode::TrEndNode(TrNode *trNode) :
1256 BaseNode(),
1257 tr(trNode) {
1258 if (tr && tr->table && tr->cols > tr->table->cols) {
1259 tr->table->cols = tr->cols;
1260 }
1261}
1262
1263void TrEndNode::display(Display *out) {
1264 out->restoreColors();
1265
1266 if (tr && tr->table) {
1267 tr->height = tr->table->maxY - tr->y1 + out->lineHeight;
1268 }
1269}
1270
1271//--TdNode----------------------------------------------------------------------
1272
1273TdNode::TdNode(TrNode *trNode, Attributes *a) :
1274 BaseNode(),
1275 tr(trNode) {
1276 if (tr) {
1277 tr->cols++;
1278 }
1279 foreground = getColor(a->getFgColor(), NO_COLOR);
1280 background = getColor(a->getBgColor(), NO_COLOR);
1281 width = a->getWidth();
1282 colspan = a->getColSpan(1) - 1; // count 1 for each additional col
1283}
1284
1285void TdNode::display(Display *out) {
1286 out->setColors(foreground, background);
1287
1288 if (tr == 0 || tr->table == 0 || tr->table->cols == 0) {
1289 return; // invalid table model
1290 }
1291
1292 TableNode *table = tr->table;
1293 if (out->measure && table->nextRow == 1 && width.value != -1) {
1294 table->setColWidth(&width);
1295 }
1296
1297 out->x1 = table->initX + TABLE_PADDING + (table->nextCol == 0 ? 0 : table->columns[table->nextCol - 1]);
1298 out->y1 = tr->y1; // top+left of next cell
1299
1300 // adjust for colspan attribute
1301 if (colspan) {
1302 table->nextCol += colspan;
1303 }
1304 if (table->nextCol > table->cols - 1) {
1305 table->nextCol = table->cols - 1;
1306 }
1307
1308 out->indent = out->x1;
1309 out->x2 = out->x1 + table->columns[table->nextCol] - CELL_SPACING;
1310 if (out->x2 > out->tabW) {
1311 out->x2 = out->tabW - CELL_SPACING; // stay within table bounds
1312 }
1313 table->nextCol++;
1314
1315 if (out->measure == false) {
1316 Fl_Rect rc(out->indent - CELL_SPACING,
1317 tr->y1 - fl_height() + fl_descent(),
1318 out->x2 - out->indent + (CELL_SPACING * 2),
1319 out->lineHeight);
1320 out->drawBackground(rc);
1321 if (table->border > 0) {
1322 Fl_Color oldColor = fl_color();
1323 fl_color(FL_BLACK);
1324 fl_overlay_rect(rc.x(), rc.y(), rc.w(), rc.h());
1325 fl_color(oldColor);
1326 }
1327 }
1328
1329}
1330
1331TdEndNode::TdEndNode(TdNode *tdNode) :
1332 BaseNode(),
1333 td(tdNode) {
1334}
1335
1336void TdEndNode::display(Display *out) {
1337 out->restoreColors();
1338
1339 if (td && td->tr && td->tr->table) {
1340 td->tr->table->doEndTD(out, td->tr, &td->width);
1341 }
1342}
1343
1344//--NamedInput------------------------------------------------------------------
1345
1346struct NamedInput {
1347 NamedInput(InputNode *node, strlib::String *name) {
1348 this->input = node;
1349 this->name.append(name->c_str());
1350 }
1351 ~NamedInput() {
1352 }
1353 InputNode *input;
1354 strlib::String name;
1355};
1356
1357//--InputNode-------------------------------------------------------------------
1358
1359static void onclick_callback(Fl_Widget *button, void *buttonId) {
1360 ((HelpWidget *)button->parent())->onclick(button);
1361}
1362
1363static void def_button_callback(Fl_Widget *button, void *buttonId) {
1364 // supply "onclick=fff" to make it do something useful
1365 // check for parent of HelpWidget
1366 if (Fl::modal() == (Fl_Window *)button->parent()->parent()) {
1367 Fl::modal()->set_non_modal();
1368 }
1369}
1370
1371struct InputNode : public BaseNode {
1372 InputNode(Fl_Group *parent);
1373 InputNode(Fl_Group *parent, Attributes *a, const char *v, int len);
1374 InputNode(Fl_Group *parent, Attributes *a);
1375 void update(strlib::List<NamedInput *> *namedInputs, Properties<String *> *p, Attributes *a);
1376 void display(Display *out);
1377
1378 Fl_Widget *button;
1379 uint32_t rows, cols;
1380 strlib::String onclick;
1381};
1382
1383// creates either a text, checkbox, radio, hidden or button control
1384InputNode::InputNode(Fl_Group *parent, Attributes *a) :
1385 BaseNode() {
1386 parent->begin();
1387 strlib::String *type = a->getType();
1388 if (type != NULL && type->equals("text")) {
1389 button = new Fl_Input(0, 0, INPUT_WIDTH, 0);
1390 button->argument(ID_TEXTBOX);
1391 } else if (type != NULL && type->equals("readonly")) {
1392 button = new Fl_Input(0, 0, INPUT_WIDTH, 0);
1393 button->argument(ID_READONLY);
1394 } else if (type != NULL && type->equals("checkbox")) {
1395 button = new Fl_Check_Button(0, 0, BUTTON_WIDTH, 0);
1396 button->argument(ID_CHKBOX);
1397 } else if (type != NULL && type->equals("radio")) {
1398 button = new Fl_Radio_Button(0, 0, BUTTON_WIDTH, 0);
1399 button->argument(ID_RADIO);
1400 } else if (type != NULL && type->equals("slider")) {
1401 button = new Fl_Slider(0, 0, BUTTON_WIDTH, 0);
1402 button->argument(ID_RANGEVAL);
1403 } else if (type != NULL && type->equals("valueinput")) {
1404 button = new Fl_Value_Input(0, 0, BUTTON_WIDTH, 0);
1405 button->argument(ID_RANGEVAL);
1406 } else if (type != NULL && type->equals("hidden")) {
1407 button = new Fl_Input(0, 0, 0, 0);
1408 button->argument(ID_HIDDEN);
1409 } else {
1410 button = new Fl_Button(0, 0, 0, 0);
1411 button->argument(ID_BUTTON);
1412 button->callback(def_button_callback);
1413 }
1414 parent->end();
1415}
1416
1417InputNode::InputNode(Fl_Group *parent, Attributes *a, const char *s, int len) :
1418 BaseNode() {
1419 // creates a textarea control
1420 parent->begin();
1421 if (a->isReadonly()) {
1422 strlib::String str;
1423 str.append(s, len);
1424 button = new Fl_Input(0, 0, INPUT_WIDTH, 0);
1425 button->argument(ID_READONLY);
1426 button->copy_label(str.c_str());
1427 } else {
1428 button = new Fl_Input(0, 0, INPUT_WIDTH, 0);
1429 button->argument(ID_TEXTAREA);
1430 ((Fl_Input *)button)->value(s, len);
1431 }
1432 parent->end();
1433}
1434
1435InputNode::InputNode(Fl_Group *parent) :
1436 BaseNode() {
1437 // creates a select control
1438 parent->begin();
1439 button = new Fl_Choice(0, 0, INPUT_WIDTH, 0);
1440 button->argument(ID_SELECT);
1441 parent->end();
1442}
1443
1444void createDropList(InputNode *node, strlib::List<String *> *options) {
1445 Fl_Choice *menu = (Fl_Choice *)node->button;
1446 List_each(String *, it, *options) {
1447 String *s = (*it);
1448 menu->add(s->c_str());
1449 }
1450}
1451
1452void InputNode::update(strlib::List<NamedInput *> *names, Properties<String *> *env, Attributes *a) {
1453 Fl_Valuator *valuator;
1454 Fl_Input *input = NULL;
1455 Fl_Color color;
1456 strlib::String *name = a->getName();
1457 strlib::String *value = a->getValue();
1458 strlib::String *align = a->getAlign();
1459
1460 if (name != NULL) {
1461 names->add(new NamedInput(this, name));
1462 }
1463
1464 if (button == 0) {
1465 return;
1466 }
1467 // value uses environment/external attributes
1468 if (value == 0 && name != 0 && env) {
1469 value = env->get(name->c_str());
1470 }
1471
1472 switch (button->argument()) {
1473 case ID_READONLY:
1474 button->align(FL_ALIGN_LEFT | FL_ALIGN_CLIP);
1475 if (value && value->length()) {
1476 button->copy_label(value->c_str());
1477 }
1478 // fallthru
1479 case ID_TEXTAREA:
1480 button->box(FL_NO_BOX);
1481 rows = a->getRows();
1482 cols = a->getCols();
1483 if (rows > 1) {
1484 button->type(FL_MULTILINE_INPUT);
1485 }
1486 break;
1487 case ID_RANGEVAL:
1488 valuator = (Fl_Valuator *)button;
1489 valuator->minimum(a->getMin());
1490 valuator->maximum(a->getMax());
1491 valuator->step(1);
1492 valuator->align(FL_ALIGN_LEFT);
1493 if (value && value->length()) {
1494 valuator->value(value->toInteger());
1495 }
1496 break;
1497 case ID_TEXTBOX:
1498 input = (Fl_Input *)button;
1499 if (value && value->length()) {
1500 input->value(value->c_str());
1501 }
1502 break;
1503 case ID_BUTTON:
1504 if (value && value->length()) {
1505 button->copy_label(value->c_str());
1506 } else {
1507 button->copy_label(" ");
1508 }
1509 break;
1510 case ID_HIDDEN:
1511 if (value && value->length()) {
1512 button->copy_label(value->c_str());
1513 }
1514 break;
1515 }
1516
1517 // size
1518 int size = a->getSize();
1519 if (size != -1) {
1520 button->size(size, button->h());
1521 }
1522 // set callback
1523 onclick.append(a->getOnclick());
1524 if (onclick.length()) {
1525 button->callback(onclick_callback);
1526 }
1527 // set colors
1528 color = getColor(a->getBgColor(), 0);
1529 if (color) {
1530 button->color(color);
1531 }
1532 color = getColor(a->getFgColor(), 0);
1533 if (color) {
1534 button->labelcolor(color);
1535 }
1536
1537 // set alignment
1538 if (align != 0) {
1539 if (align->equals("right")) {
1540 button->align(FL_ALIGN_RIGHT | FL_ALIGN_CLIP);
1541 } else if (align->equals("center")) {
1542 button->align(FL_ALIGN_CENTER | FL_ALIGN_CLIP);
1543 } else if (align->equals("top")) {
1544 button->align(FL_ALIGN_TOP | FL_ALIGN_CLIP);
1545 } else if (align->equals("bottom")) {
1546 button->align(FL_ALIGN_BOTTOM | FL_ALIGN_CLIP);
1547 } else {
1548 button->align(FL_ALIGN_LEFT | FL_ALIGN_CLIP);
1549 }
1550 }
1551 // set border
1552 switch (a->getBorder(0)) {
1553 case 1:
1554 button->box(FL_BORDER_BOX);
1555 break;
1556 case 2:
1557 button->box(FL_SHADOW_BOX);
1558 break;
1559 case 3:
1560 button->box(FL_ENGRAVED_BOX);
1561 break;
1562 case 4:
1563 button->box(FL_THIN_DOWN_BOX);
1564 break;
1565 }
1566}
1567
1568void InputNode::display(Display *out) {
1569 if (button == 0 || ID_HIDDEN == button->argument()) {
1570 return;
1571 }
1572
1573 int height = 4 + fl_height() + fl_descent();
1574 switch (button->argument()) {
1575 case ID_SELECT:
1576 height += 4;
1577 break;
1578 case ID_BUTTON:
1579 if (button->w() == 0 && button->label()) {
1580 button->size(12 + fl_width(button->label()), button->h());
1581 }
1582 break;
1583 case ID_TEXTAREA:
1584 case ID_READONLY:
1585 button->size(4 + (fl_width("$") * cols), button->h());
1586 height = 4 + (fl_height() + fl_descent() * rows);
1587 break;
1588 case ID_TEXTBOX:
1589 ((Fl_Input *)button)->textfont(out->font);
1590 ((Fl_Input *)button)->textsize(out->fontSize);
1591 break;
1592 default:
1593 break;
1594 }
1595 if (out->x1 != out->indent && button->w() > out->x2 - out->x1) {
1596 out->newRow();
1597 }
1598 out->lineHeight = height;
1599 button->resize(out->x1, out->y1 - fl_height(), button->w(), out->lineHeight - 2);
1600 button->labelfont(out->font);
1601 button->labelsize(out->fontSize);
1602 if (button->y() + button->h() < out->y2 && button->y() >= 0) {
1603 button->set_visible();
1604 } else {
1605 // draw a fake control in case partially visible
1606 fl_color(button->color());
1607 fl_rectf(button->x(), button->y(), button->w(), button->h());
1608 fl_color(out->color);
1609 }
1610 out->x1 += button->w();
1611 out->content = true;
1612}
1613
1614//--EnvNode---------------------------------------------------------------------
1615
1616struct EnvNode : public TextNode {
1617 EnvNode(Properties<String *> *p, const char *s, uint16_t textlen) :
1618 TextNode(0, 0) {
1619 strlib::String var;
1620 var.append(s, textlen);
1621 var.trim();
1622 if (p) {
1623 strlib::String *s = p->get(var.c_str());
1624 value.append(s);
1625 }
1626 if (value.length() == 0) {
1627 value.append(getenv(var.c_str()));
1628 }
1629 this->s = value.c_str();
1630 this->textlen = value.length();
1631 }
1632 // here to provide value cleanup
1633 strlib::String value;
1634};
1635
1636//--HelpWidget------------------------------------------------------------------
1637
1638static void scrollbar_callback(Fl_Widget *scrollBar, void *helpWidget) {
1639 ((HelpWidget *)helpWidget)->redraw();
1640}
1641
1642static void anchor_callback(Fl_Widget *helpWidget, void *target) {
1643 ((HelpWidget *)helpWidget)->navigateTo((const char *)target);
1644}
1645
1646HelpWidget::HelpWidget(Fl_Widget *rect, int fontSize) :
1647 Fl_Group(rect->x(), rect->y(), rect->w(), rect->h()),
1648 background(BACKGROUND_COLOR),
1649 foreground(FOREGROUND_COLOR),
1650 scrollHeight(0),
1651 scrollWindowHeight(0),
1652 markX(0),
1653 markY(0),
1654 pointX(0),
1655 pointY(0),
1656 hscroll(0),
1657 scrollY(0),
1658 mouseMode(mm_select),
1659 nodeList(100),
1660 namedInputs(5),
1661 inputs(5),
1662 anchors(5),
1663 images(5),
1664 cookies(NULL) {
1665 begin();
1666 scrollbar = new Fl_Scrollbar(rect->w() - SCROLL_X, rect->y(), SCROLL_W, rect->h());
1667 scrollbar->type(FL_VERTICAL);
1668 scrollbar->value(0, 1, 0, SCROLL_SIZE);
1669 scrollbar->user_data(this);
1670 scrollbar->callback(scrollbar_callback);
1671 scrollbar->deactivate();
1672 scrollbar->show();
1673 end();
1674 callback(anchor_callback); // default callback
1675 init();
1676 docHome.clear();
1677 labelsize(fontSize);
1678}
1679
1680HelpWidget::~HelpWidget() {
1681 cleanup();
1682}
1683
1684void HelpWidget::init() {
1685 scrollHeight = 0;
1686 scrollWindowHeight = 0;
1687 hscroll = 0;
1688 endSelection();
1689 scrollbar->value(0);
1690}
1691
1692void HelpWidget::setTheme(EditTheme *theme) {
1693 background = get_color(theme->_background);
1694 foreground = get_color(theme->_color);
1695 selection_color(get_color(theme->_selection_color));
1696 labelcolor(get_color(theme->_cursor_color));
1697}
1698
1699void HelpWidget::endSelection() {
1700 markX = pointX = -1;
1701 markY = pointY = -1;
1702 selection.clear();
1703}
1704
1705void HelpWidget::setFontSize(int i) {
1706 labelsize(i);
1707 reloadPage();
1708}
1709
1710void HelpWidget::cleanup() {
1711 List_each(InputNode *, it, inputs) {
1712 InputNode *p = (*it);
1713 if (p->button) {
1714 remove(p->button);
1715 delete p->button;
1716 p->button = 0;
1717 }
1718 }
1719
1720 // button/anchor items destroyed in nodeList
1721 inputs.clear();
1722 anchors.clear();
1723 images.clear();
1724 nodeList.removeAll();
1725 namedInputs.removeAll();
1726 title.clear();
1727}
1728
1729void HelpWidget::reloadPage() {
1730 cleanup();
1731 init();
1732 compile();
1733 redraw();
1734 pushedAnchor = 0;
1735}
1736
1737// returns the control with the given name
1738Fl_Widget *HelpWidget::getInput(const char *name) {
1739 List_each(NamedInput *, it, namedInputs) {
1740 NamedInput *ni = (*it);
1741 if (ni->name.equals(name)) {
1742 return ni->input->button;
1743 }
1744 }
1745 return NULL;
1746}
1747
1748// return the value of the given control
1749const char *HelpWidget::getInputValue(Fl_Widget *widget) {
1750 if (widget == 0) {
1751 return NULL;
1752 }
1753 switch (widget->argument()) {
1754 case ID_TEXTBOX:
1755 case ID_TEXTAREA:
1756 return ((Fl_Input *)widget)->value();
1757 case ID_RADIO:
1758 case ID_CHKBOX:
1759 return ((Fl_Radio_Button *)widget)->value() ? truestr : falsestr;
1760 case ID_SELECT:
1761 return NULL;
1762 case ID_RANGEVAL:
1763 sprintf(rangeValue, "%f", ((Fl_Valuator *)widget)->value());
1764 return rangeValue;
1765 case ID_HIDDEN:
1766 case ID_READONLY:
1767 return widget->label();
1768 }
1769 return NULL;
1770}
1771
1772// return the nth form value
1773const char *HelpWidget::getInputValue(int i) {
1774 int len = namedInputs.size();
1775 if (i < len) {
1776 NamedInput *ni = namedInputs[i];
1777 return getInputValue(ni->input->button);
1778 }
1779 return 0;
1780}
1781
1782// return the name of the given button control
1783const char *HelpWidget::getInputName(Fl_Widget *button) {
1784 List_each(NamedInput *, it, namedInputs) {
1785 NamedInput *ni = (*it);
1786 if (ni->input->button == button) {
1787 return ni->name.c_str();
1788 }
1789 }
1790 return NULL;
1791}
1792
1793// return all of the forms names and values - except hidden ones
1794void HelpWidget::getInputProperties(Properties<String *> *p) {
1795 if (p != 0) {
1796 List_each(NamedInput *, it, namedInputs) {
1797 NamedInput *ni = (*it);
1798 const char *value = getInputValue(ni->input->button);
1799 if (value) {
1800 p->put(ni->name.c_str(), value);
1801 }
1802 }
1803 }
1804}
1805
1806// update a widget's display value using the given string based
1807// assignment statement, eg val=1000
1808bool HelpWidget::setInputValue(const char *assignment) {
1809 strlib::String s = assignment;
1810 strlib::String name = s.leftOf('=');
1811 strlib::String value = s.rightOf('=');
1812 if (value.length() == 0) {
1813 return false;
1814 }
1815
1816 List_each(NamedInput *, it, namedInputs) {
1817 NamedInput *ni = (*it);
1818 if (ni->name.equals(name)) {
1819 Fl_Widget *button = ni->input->button;
1820
1821 switch (button->argument()) {
1822 case ID_TEXTBOX:
1823 case ID_TEXTAREA:
1824 ((Fl_Input *)button)->value(value.c_str());
1825 break;
1826 case ID_RADIO:
1827 case ID_CHKBOX:
1828 ((Fl_Radio_Button *)button)->value(value.equals(truestr) || value.equals("1"));
1829 break;
1830 case ID_SELECT:
1831 break;
1832 case ID_RANGEVAL:
1833 ((Fl_Valuator *)button)->value(value.toNumber());
1834 break;
1835 case ID_READONLY:
1836 button->copy_label(value.c_str());
1837 break;
1838 }
1839 return true;
1840 }
1841 }
1842 return false;
1843}
1844
1845void HelpWidget::scrollTo(const char *anchorName) {
1846 List_each(AnchorNode *, it, anchors) {
1847 AnchorNode *p = (*it);
1848 if (p->name.equals(anchorName)) {
1849 if (p->getY() > scrollHeight) {
1850 vscroll(-scrollHeight);
1851 } else {
1852 vscroll(-p->getY());
1853 }
1854 redraw();
1855 return;
1856 }
1857 }
1858}
1859
1860void HelpWidget::resize(int x, int y, int w, int h) {
1861 Fl_Group::resize(x, y, w, h);
1862 scrollbar->resize(w - SCROLL_X, y, SCROLL_W, h);
1863 endSelection();
1864}
1865
1866void HelpWidget::draw() {
1867 if (damage() == FL_DAMAGE_CHILD) {
1868 Fl_Group::draw();
1869 return;
1870 }
1871
1872 int vscroll = -scrollbar->value();
1873 Display out;
1874 out.uline = false;
1875 out.center = false;
1876 out.wnd = this;
1877 out.anchor = 0;
1878 out.font = FL_HELVETICA;
1879 out.fontSize = (int)labelsize();
1880 out.color = foreground;
1881 out.background = background;
1882 out.selectionColor = selection_color();
1883 out.anchorColor = labelcolor();
1884 out.y2 = h();
1885 out.indent = DEFAULT_INDENT + hscroll;
1886 out.baseIndent = out.indent;
1887 out.x1 = x() + out.indent;
1888 out.x2 = w() - (DEFAULT_INDENT * 2) + hscroll;
1889 out.content = false;
1890 out.measure = false;
1891 out.exposed = exposed();
1892 out.tableLevel = 0;
1893 out.imgY = -1;
1894 out.imgIndent = out.indent;
1895 out.tabW = out.x2;
1896 out.tabH = out.y2;
1897 out.selection = 0;
1898 out.selected = (markX != pointX || markY != pointY);
1899 out.insideCode = false;
1900 out.insideUl = false;
1901
1902 if (Fl::event_clicks() == 1 && damage() == DAMAGE_HIGHLIGHT) {
1903 // double click
1904 out.selected = true;
1905 }
1906 if (out.selected) {
1907 out.markX = markX;
1908 out.pointX = pointX;
1909 if (markY < pointY) {
1910 out.markY = markY;
1911 out.pointY = pointY;
1912 out.invertedSel = false;
1913 } else {
1914 out.markY = pointY;
1915 out.pointY = markY;
1916 out.invertedSel = true;
1917 }
1918 out.markX += hscroll;
1919 out.markY += vscroll;
1920 out.pointX += hscroll;
1921 out.pointY += vscroll;
1922
1923 if (damage() == DAMAGE_HIGHLIGHT) {
1924 // capture new selection text
1925 out.selection = &selection;
1926 out.selection->clear();
1927 }
1928 }
1929 // must call setfont() before getascent() etc
1930 fl_font(out.font, out.fontSize);
1931 out.y1 = y() + fl_height();
1932 out.lineHeight = fl_height() + fl_descent();
1933 out.y1 += vscroll;
1934
1935 fl_push_clip(x(), y(), w() - SCROLL_X, h());
1936 bool havePushedAnchor = false;
1937 if (pushedAnchor && (damage() == DAMAGE_PUSHED)) {
1938 // just draw the anchor-push
1939 int h = (pushedAnchor->y2 - pushedAnchor->y1) + pushedAnchor->lineHeight;
1940 fl_push_clip(x(), y() + pushedAnchor->y1, out.x2, h);
1941 havePushedAnchor = true;
1942 }
1943 // draw the background
1944 fl_color(out.background);
1945 fl_rectf(x(), y(), w() - SCROLL_X, h());
1946 fl_color(out.color);
1947
1948 out.background = NO_COLOR;
1949
1950 // hide any inputs
1951 List_each(InputNode *, it, inputs) {
1952 InputNode *p = (*it);
1953 if (p->button) {
1954 p->button->clear_visible();
1955 }
1956 }
1957
1958 int id = 0;
1959 List_each(BaseNode *, it, nodeList) {
1960 BaseNode *p = (*it);
1961 out.nodeId = id;
1962 p->display(&out);
1963 if (out.nodeId < id) {
1964 // perform second pass on previous outer table
1965 MeasureNode *node = (MeasureNode *)nodeList[out.nodeId];
1966 out.x1 = node->initX;
1967 out.y1 = node->initY;
1968 out.exposed = false;
1969 for (int j = out.nodeId; j <= id; j++) {
1970 p = nodeList[j];
1971 out.nodeId = j;
1972 p->display(&out);
1973 }
1974 out.exposed = exposed();
1975 }
1976 if (out.exposed == false && out.tableLevel == 0 && out.y1 - out.lineHeight > out.y2) {
1977 // clip remaining content
1978 break;
1979 }
1980 id++;
1981 }
1982
1983 if (out.exposed) {
1984 // size has changed or need to recalculate scrollbar
1985 int pageHeight = (out.y1 - vscroll) + out.lineHeight;
1986 int windowHeight = h() - out.lineHeight;
1987 int scrollH = pageHeight;
1988 if (scrollH != scrollHeight || windowHeight != scrollWindowHeight) {
1989 scrollWindowHeight = windowHeight;
1990 scrollHeight = scrollH;
1991 if (scrollHeight < scrollWindowHeight) {
1992 // nothing to scroll
1993 scrollHeight = 0;
1994 scrollbar->deactivate();
1995 scrollbar->value(0, 1, 0, 1);
1996 } else {
1997 scrollbar->activate();
1998 scrollbar->value(-vscroll, scrollWindowHeight, 0, scrollHeight);
1999 scrollbar->linesize(out.lineHeight);
2000 }
2001 scrollbar->redraw();
2002 }
2003 }
2004 if (havePushedAnchor) {
2005 fl_pop_clip();
2006 }
2007 fl_pop_clip();
2008
2009 // draw child controls
2010 draw_child(*scrollbar);
2011
2012 // prevent other child controls from drawing over the scrollbar
2013 fl_push_clip(x(), y(), w() - SCROLL_X, h());
2014 int numchildren = children();
2015 for (int n = 0; n < numchildren; n++) {
2016 Fl_Widget &w = *child(n);
2017 if (&w != scrollbar) {
2018 draw_child(w);
2019 }
2020 }
2021 fl_pop_clip();
2022}
2023
2024void HelpWidget::compile() {
2025 uint8_t pre = !isHtmlFile();
2026 uint8_t bold = false;
2027 uint8_t italic = false;
2028 uint8_t center = false;
2029 uint8_t uline = false;
2030 Fl_Color color = 0;
2031 Fl_Font font = FL_HELVETICA;
2032 int fontSize = (int)labelsize();
2033 int taglen = 0;
2034 int textlen = 0;
2035 bool padlines = false; // padding between line-breaks
2036
2037 strlib::Stack<TableNode *> tableStack(5);
2038 strlib::Stack<TrNode *> trStack(5);
2039 strlib::Stack<TdNode *> tdStack(5);
2040 strlib::Stack<UlNode *> olStack(5);
2041 strlib::Stack<CodeNode *> codeStack(5);
2042 strlib::List<String *> options(5);
2043 Attributes p(5);
2044 strlib::String *prop;
2045 BaseNode *node;
2046 InputNode *inputNode;
2047
2048 const char *text = htmlStr.c_str();
2049 const char *tagBegin = text;
2050 const char *tag;
2051 const char *tagPair = 0;
2052
2053#define ADD_PREV_SEGMENT \
2054 prevlen = i-pindex; \
2055 if (prevlen > 0) { \
2056 nodeList.add(new TextNode(p, prevlen)); \
2057 padlines = true; \
2058 }
2059
2060 while (text && *text) {
2061 // find the start of the next tag
2062 while (*tagBegin != 0 && *tagBegin != '<') {
2063 tagBegin++;
2064 }
2065 const char *tagEnd = tagBegin;
2066 while (*tagEnd != 0 && *tagEnd != '>') {
2067 tagEnd++;
2068 }
2069 if (tagBegin == text) {
2070 if (*tagEnd == 0) {
2071 break; // no tag closure
2072 }
2073 text = tagEnd + 1; // adjoining tags
2074 }
2075 // process open text leading to the found tag
2076 if (tagBegin > text && tagPair == 0) {
2077 textlen = tagBegin - text;
2078 const char *p = text;
2079 int pindex = 0;
2080 int prevlen, ispace;
2081 for (int i = 0; i < textlen; i++) {
2082 switch (text[i]) {
2083 case '&':
2084 // handle entities
2085 if (text[i + 1] == '#') {
2086 ADD_PREV_SEGMENT;
2087 int ch = 0;
2088 i += 2;
2089 while (isdigit(text[i])) {
2090 ch = (ch * 10) + (text[i++] - '0');
2091 }
2092 if (text[i] == ';') {
2093 i++;
2094 }
2095 if (ch == 133) {
2096 node = new ImageNode(&ellipseImage);
2097 nodeList.add(node);
2098 } else if (ch > 129 && ch < 256) {
2099 node = new TextNode(&entityMap[ch].xlat, 1);
2100 nodeList.add(node);
2101 }
2102 pindex = i;
2103 p = text + pindex;
2104 } else {
2105 for (int j = 0; j < entityMapLen; j++) {
2106 if (0 == strncasecmp(text + i + 1, entityMap[j].ent, entityMap[j].elen - 1)) {
2107 ADD_PREV_SEGMENT;
2108 // save entity replacement
2109 node = new TextNode(&entityMap[j].xlat, 1);
2110 nodeList.add(node);
2111 // skip past entity
2112 i += entityMap[j].elen;
2113 pindex = i;
2114 p = text + pindex;
2115 i--;
2116 // stop searching
2117 break;
2118 }
2119 }
2120 }
2121 break;
2122
2123 case '\r':
2124 case '\n':
2125 ADD_PREV_SEGMENT;
2126 if ((prevlen && text[i - 1] == ' ')) {
2127 padlines = false;
2128 }
2129 if (pre) {
2130 nodeList.add(new BrNode(pre));
2131 } else if (padlines == true) {
2132 nodeList.add(new TextNode(spacestr, 1));
2133 padlines = false; // don't add consequtive spacestrs
2134 }
2135 // skip white space
2136 while (i < textlen && (IS_WHITE(text[i + 1]))) {
2137 i++; // ends on final white-char
2138 }
2139
2140 // skip white-char character
2141 pindex = i + 1;
2142 p = text + pindex;
2143 break;
2144
2145 case '-':
2146 case '~':
2147 case ':':
2148 // break into separate segments to cause line-breaks
2149 prevlen = i - pindex + 1;
2150 if (prevlen > 0) {
2151 nodeList.add(new TextNode(p, prevlen));
2152 padlines = true;
2153 }
2154 pindex = i + 1;
2155 p = text + pindex;
2156 break;
2157
2158 case ' ':
2159 case '\t':
2160 if (pre) {
2161 continue;
2162 }
2163 // skip multiple whitespaces
2164 ispace = i;
2165 while (text[ispace + 1] == ' ' || text[ispace + 1] == '\t') {
2166 ispace++;
2167 if (ispace == textlen) {
2168 break;
2169 }
2170 }
2171 if (ispace > i) {
2172 ADD_PREV_SEGMENT;
2173 pindex = i = ispace;
2174 p = text + pindex;
2175 }
2176 break;
2177 }
2178 } // end for (int i=0; i<textlen...
2179
2180 int len = textlen - pindex;
2181 if (len) {
2182 nodeList.add(new TextNode(p, len));
2183 padlines = true;
2184 }
2185 }
2186 // move to after end tag
2187 text = *tagEnd == 0 ? 0 : tagEnd + 1;
2188
2189 // process the tag
2190 taglen = tagEnd - tagBegin - 1;
2191 if (taglen > 0) {
2192 tag = tagBegin + 1;
2193 if (tag[0] == '/') {
2194 // process the end of tag
2195 tag++;
2196 if (0 == strncasecmp(tag, "b>", 2)) {
2197 bold = false;
2198 node = new FontNode(font, fontSize, 0, bold, italic);
2199 nodeList.add(node);
2200 } else if (0 == strncasecmp(tag, "i", 1)) {
2201 italic = false;
2202 node = new FontNode(font, fontSize, 0, bold, italic);
2203 nodeList.add(node);
2204 } else if (0 == strncasecmp(tag, "center", 6)) {
2205 center = false;
2206 nodeList.add(new StyleNode(uline, center));
2207 } else if (0 == strncasecmp(tag, "font", 4) ||
2208 0 == strncasecmp(tag, "blockquote", 10) ||
2209 0 == strncasecmp(tag, "h", 1)) { // </h1>
2210 if (0 == strncasecmp(tag, "h", 1)) {
2211 if (bold > 0) {
2212 bold--;
2213 }
2214 padlines = false;
2215 }
2216 color = (0 == strncasecmp(tag, "f", 1) ? (Fl_Color) - 1 : 0);
2217 font = FL_HELVETICA;
2218 node = new FontNode(font, fontSize, color, bold, italic);
2219 nodeList.add(node);
2220 } else if (0 == strncasecmp(tag, "pre", 3)) {
2221 pre = false;
2222 node = new FontNode(font, fontSize, 0, bold, italic);
2223 nodeList.add(node);
2224 nodeList.add(new BrNode(pre));
2225 } else if (0 == strncasecmp(tag, "a", 1)) {
2226 nodeList.add(new AnchorEndNode());
2227 } else if (0 == strncasecmp(tag, "ul", 2) || 0 == strncasecmp(tag, "ol", 2)) {
2228 nodeList.add(new UlEndNode());
2229 olStack.pop();
2230 padlines = false;
2231 } else if (0 == strncasecmp(tag, "u", 1)) {
2232 uline = false;
2233 nodeList.add(new StyleNode(uline, center));
2234 } else if (0 == strncasecmp(tag, "td", 2)) {
2235 nodeList.add(new TdEndNode((TdNode *)tdStack.pop()));
2236 text = skipWhite(tagEnd + 1);
2237 } else if (0 == strncasecmp(tag, "tr", 2)) {
2238 node = new TrEndNode((TrNode *)trStack.pop());
2239 nodeList.add(node);
2240 padlines = false;
2241 text = skipWhite(tagEnd + 1);
2242 } else if (0 == strncasecmp(tag, "table", 5)) {
2243 node = new TableEndNode((TableNode *)tableStack.pop());
2244 nodeList.add(node);
2245 padlines = false;
2246 text = skipWhite(tagEnd + 1);
2247 } else if (0 == strncasecmp(tag, "textarea", 8) && tagPair) {
2248 inputNode = new InputNode(this, &p, tagPair, tagBegin - tagPair);
2249 nodeList.add(inputNode);
2250 inputs.add(inputNode);
2251 inputNode->update(&namedInputs, cookies, &p);
2252 tagPair = 0;
2253 p.removeAll();
2254 } else if (0 == strncasecmp(tag, "select", 6) && tagPair) {
2255 inputNode = new InputNode(this);
2256 createDropList(inputNode, &options);
2257 nodeList.add(inputNode);
2258 inputs.add(inputNode);
2259 inputNode->update(&namedInputs, cookies, &p);
2260 tagPair = 0;
2261 p.removeAll();
2262 } else if (0 == strncasecmp(tag, "option", 6) && tagPair) {
2263 strlib::String *option = new String();
2264 option->append(tagPair, tagBegin - tagPair);
2265 options.add(option); // continue scan for more options
2266 } else if (0 == strncasecmp(tag, "title", 5) && tagPair) {
2267 title.clear();
2268 title.append(tagPair, tagBegin - tagPair);
2269 tagPair = 0;
2270 } else if (0 == strncasecmp(tag, "code", 4)) {
2271 node = new CodeEndNode((CodeNode *)codeStack.pop());
2272 nodeList.add(node);
2273 } else if (0 == strncasecmp(tag, "script", 6) ||
2274 0 == strncasecmp(tag, "style", 5)) {
2275 tagPair = 0;
2276 }
2277 } else if (isalpha(tag[0]) || tag[0] == '!') {
2278 // process the start of the tag
2279 if (0 == strncasecmp(tag, "br", 2)) {
2280 nodeList.add(new BrNode(pre));
2281 padlines = false;
2282 text = skipWhite(tagEnd + 1);
2283 } else if (0 == strncasecmp(tag, "p>", 2)) {
2284 nodeList.add(new ParagraphNode());
2285 padlines = false;
2286 text = skipWhite(tagEnd + 1);
2287 } else if (0 == strncasecmp(tag, "b>", 2)) {
2288 bold = true;
2289 node = new FontNode(font, fontSize, 0, bold, italic);
2290 nodeList.add(node);
2291 } else if (0 == strncasecmp(tag, "i>", 2)) {
2292 italic = true;
2293 node = new FontNode(font, fontSize, 0, bold, italic);
2294 nodeList.add(node);
2295 } else if (0 == strncasecmp(tag, "center", 6)) {
2296 center = true;
2297 nodeList.add(new StyleNode(uline, center));
2298 } else if (0 == strncasecmp(tag, "hr", 2)) {
2299 nodeList.add(new HrNode());
2300 padlines = false;
2301 } else if (0 == strncasecmp(tag, "title", 5)) {
2302 tagPair = text = skipWhite(tagEnd + 1);
2303 } else if (0 == strncasecmp(tag, "pre", 3)) {
2304 pre = true;
2305 node = new FontNode(FL_COURIER, fontSize, 0, bold, italic);
2306 nodeList.add(node);
2307 nodeList.add(new BrNode(pre));
2308 } else if (0 == strncasecmp(tag, "code", 4)) {
2309 node = new CodeNode(fontSize);
2310 codeStack.push((CodeNode *)node);
2311 nodeList.add(node);
2312 } else if (0 == strncasecmp(tag, "td", 2)) {
2313 p.removeAll();
2314 p.load(tag + 2, taglen - 2);
2315 node = new TdNode((TrNode *)trStack.peek(), &p);
2316 nodeList.add(node);
2317 tdStack.push((TdNode *)node);
2318 text = skipWhite(tagEnd + 1);
2319 } else if (0 == strncasecmp(tag, "tr", 2)) {
2320 p.removeAll();
2321 p.load(tag + 2, taglen - 2);
2322 node = new TrNode((TableNode *)tableStack.peek(), &p);
2323 nodeList.add(node);
2324 trStack.push((TrNode *)node);
2325 text = skipWhite(tagEnd + 1);
2326 } else if (0 == strncasecmp(tag, "table", 5)) {
2327 p.removeAll();
2328 p.load(tag + 5, taglen - 5);
2329 node = new TableNode(&p);
2330 nodeList.add(node);
2331 tableStack.push((TableNode *)node);
2332 padlines = false;
2333 text = skipWhite(tagEnd + 1);
2334 // continue the font in case we resize
2335 node = new FontNode(font, fontSize, 0, bold, italic);
2336 nodeList.add(node);
2337 prop = p.getBackground();
2338 if (prop != NULL) {
2339 node = new ImageNode(&docHome, prop, false);
2340 nodeList.add(node);
2341 images.add((ImageNode *)node);
2342 }
2343 } else if (0 == strncasecmp(tag, "ul>", 3) ||
2344 0 == strncasecmp(tag, "ol ", 3) ||
2345 0 == strncasecmp(tag, "ol>", 3)) {
2346 p.removeAll();
2347 p.load(tag + 2, taglen - 2);
2348 node = new UlNode(p, tag[0] == 'o' || tag[0] == 'O');
2349 olStack.push((UlNode *)node);
2350 nodeList.add(node);
2351 padlines = false;
2352 } else if (0 == strncasecmp(tag, "u>", 2)) {
2353 uline = true;
2354 nodeList.add(new StyleNode(uline, center));
2355 } else if (0 == strncasecmp(tag, "li>", 3)) {
2356 node = new LiNode((UlNode *)olStack.peek());
2357 nodeList.add(node);
2358 padlines = false;
2359 text = skipWhite(tagEnd + 1);
2360 } else if (0 == strncasecmp(tag, "a ", 2)) {
2361 p.removeAll();
2362 p.load(tag + 2, taglen - 2);
2363 node = new AnchorNode(p);
2364 nodeList.add(node);
2365 anchors.add((AnchorNode *)node);
2366 } else if (0 == strncasecmp(tag, "font ", 5)) {
2367 p.removeAll();
2368 p.load(tag + 5, taglen - 5);
2369 color = getColor(p.get("color"), 0);
2370 prop = p.get("font-size");
2371 if (prop != NULL) {
2372 // convert from points to pixels
2373 float h, v;
2374 Fl::screen_dpi(h, v);
2375 fontSize = (int)(prop->toInteger() * v / 72.0);
2376 } else {
2377 prop = p.get("size");
2378 if (prop != NULL) {
2379 fontSize = (int)labelsize() + (prop->toInteger() - 1);
2380 }
2381 }
2382 prop = p.get("face");
2383 if (prop != NULL) {
2384 font = get_font(prop->c_str());
2385 }
2386 node = new FontNode(font, fontSize, color, bold, italic);
2387 nodeList.add(node);
2388 } else if (taglen >= 2 && 0 == strncasecmp(tag, "h", 1)) {
2389 // H1-H6 from large to small
2390 int size = FONT_SIZE_H1 - ((tag[1] - '1') * 2);
2391 nodeList.add(new FontNode(font, size, 0, ++bold, italic));
2392 padlines = false;
2393 } else if (0 == strncasecmp(tag, "blockquote", 10)) {
2394 nodeList.add(new FontNode(font, fontSize, 0, true, true));
2395 padlines = false;
2396 } else if (0 == strncasecmp(tag, "input ", 6)) {
2397 // check for quoted values including '>'
2398 if (unquoteTag(tagBegin + 6, tagEnd)) {
2399 taglen = tagEnd - tagBegin - 1;
2400 text = *tagEnd == 0 ? 0 : tagEnd + 1;
2401 }
2402 p.removeAll();
2403 p.load(tag + 6, taglen - 6);
2404 inputNode = new InputNode(this, &p);
2405 nodeList.add(inputNode);
2406 inputs.add(inputNode);
2407 inputNode->update(&namedInputs, cookies, &p);
2408 } else if (0 == strncasecmp(tag, "textarea", 8)) {
2409 p.load(tag + 8, taglen - 8);
2410 tagPair = text = skipWhite(tagEnd + 1);
2411 } else if (0 == strncasecmp(tag, "select", 6)) {
2412 p.load(tag + 6, taglen - 6);
2413 tagPair = text = skipWhite(tagEnd + 1);
2414 options.removeAll();
2415 } else if (0 == strncasecmp(tag, "option", 6)) {
2416 tagPair = text = skipWhite(tagEnd + 1);
2417 } else if (0 == strncasecmp(tag, "img ", 4)) {
2418 p.removeAll();
2419 p.load(tag + 4, taglen - 4);
2420 node = new ImageNode(&docHome, &p);
2421 nodeList.add(node);
2422 images.add((ImageNode *)node);
2423 } else if (0 == strncasecmp(tag, "body ", 5)) {
2424 p.removeAll();
2425 p.load(tag + 5, taglen - 5);
2426 text = skipWhite(tagEnd + 1);
2427 foreground = getColor(p.getFgColor(), foreground);
2428 background = getColor(p.getBgColor(), background);
2429 prop = p.getBackground();
2430 if (prop != NULL) {
2431 node = new ImageNode(&docHome, prop, true);
2432 nodeList.add(node);
2433 images.add((ImageNode *)node);
2434 }
2435 } else if (0 == strncasecmp(tag, "script", 6) ||
2436 0 == strncasecmp(tag, "style", 5)) {
2437 tagPair = text;
2438 } else {
2439 // unknown tag
2440 text = skipWhite(tagEnd + 1);
2441 }
2442 } else if (tag[0] == '?') {
2443 nodeList.add(new EnvNode(cookies, tag + 1, taglen - 1));
2444 } else {
2445 // '<' is a literal character
2446 nodeList.add(new TextNode(anglestr, 1));
2447 tagEnd = tagBegin;
2448 text = tagBegin + 1;
2449 } // end if-start, else-end tag
2450 } // if found a tag
2451 tagBegin = *tagEnd == 0 ? tagEnd : tagEnd + 1;
2452 }
2453
2454 // prevent nodes from being auto-deleted
2455 codeStack.clear();
2456 olStack.clear();
2457 tdStack.clear();
2458 trStack.clear();
2459 while (tableStack.peek()) {
2460 node = new TableEndNode((TableNode *)tableStack.pop());
2461 nodeList.add(node);
2462 }
2463}
2464
2465// handle click from form button
2466void HelpWidget::onclick(Fl_Widget *button) {
2467 List_each(InputNode *, it, inputs) {
2468 InputNode *p = (*it);
2469 if (p->button == button) {
2470 this->event.clear();
2471 this->event.append(p->onclick.c_str());
2472 user_data((void *)this->event.c_str());
2473 do_callback();
2474 return;
2475 }
2476 }
2477}
2478
2479int HelpWidget::onMove(int event) {
2480 int ex = Fl::event_x();
2481 int ey = Fl::event_y();
2482
2483 if (pushedAnchor && event == FL_DRAG) {
2484 bool pushed = pushedAnchor->ptInSegment(ex, ey);
2485 if (pushedAnchor->pushed != pushed) {
2486 fl_cursor(FL_CURSOR_HAND);
2487 pushedAnchor->pushed = pushed;
2488 damage(DAMAGE_PUSHED);
2489 }
2490 return 1;
2491 } else {
2492 List_each(AnchorNode *, it, anchors) {
2493 AnchorNode *p = (*it);
2494 if (p->ptInSegment(ex, ey)) {
2495 fl_cursor(FL_CURSOR_HAND);
2496 return 1;
2497 }
2498 }
2499 fl_cursor(FL_CURSOR_DEFAULT);
2500 }
2501
2502 int vscroll = -scrollbar->value();
2503 if (event == FL_DRAG) {
2504 switch (mouseMode) {
2505 case mm_select:
2506 // drag text selection
2507 pointX = ex - hscroll;
2508 pointY = ey - vscroll;
2509 damage(DAMAGE_HIGHLIGHT);
2510 break;
2511 case mm_scroll:
2512 // follow the mouse navigation
2513 if (scrollY != ey) {
2514 // scroll up (less -ve) when draged down
2515 int16_t scroll = vscroll + (ey - scrollY);
2516 scrollY = ey;
2517 if (scroll > 0) {
2518 scroll = 0; // too far up
2519 } else if (-scroll > scrollHeight) {
2520 scroll = -scrollHeight; // too far down
2521 }
2522 if (scroll != vscroll) {
2523 damage(FL_DAMAGE_EXPOSE);
2524 }
2525 }
2526 break;
2527 case mm_page:
2528 break;
2529 }
2530 return 1;
2531 }
2532
2533 return 0;
2534}
2535
2536int HelpWidget::onPush(int event) {
2537 pushedAnchor = 0;
2538 int ex = Fl::event_x();
2539 int ey = Fl::event_y();
2540 int vscroll = -scrollbar->value();
2541 int16_t scroll = vscroll;
2542
2543 List_each(AnchorNode *, it, anchors) {
2544 AnchorNode *p = (*it);
2545 if (p->ptInSegment(ex, ey)) {
2546 pushedAnchor = p;
2547 pushedAnchor->pushed = true;
2548 fl_cursor(FL_CURSOR_HAND);
2549 damage(DAMAGE_PUSHED);
2550 return 1;
2551 }
2552 }
2553
2554 switch (mouseMode) {
2555 case mm_select:
2556 // begin/continue text selection
2557 if (Fl::event_state(FL_SHIFT)) {
2558 pointX = (ex - hscroll);
2559 pointY = (ey - vscroll);
2560 } else {
2561 markX = pointX = (ex - hscroll);
2562 markY = pointY = (ey - vscroll);
2563 }
2564 damage(DAMAGE_HIGHLIGHT);
2565 break;
2566
2567 case mm_scroll:
2568 scrollY = ey;
2569 break;
2570
2571 case mm_page:
2572 if (ey > h() / 2) {
2573 // page down/up
2574 scroll += ex > w() / 2 ? -h() : h();
2575 } else {
2576 // home/end
2577 scroll = ex > w() / 2 ? -scrollHeight : 0;
2578 }
2579 if (scroll > 0) {
2580 scroll = 0; // too far up
2581 } else if (-scroll > scrollHeight) {
2582 scroll = -scrollHeight; // too far down
2583 }
2584 if (scroll != scrollbar->value()) {
2585 scrollbar->value(scroll);
2586 damage(FL_DAMAGE_EXPOSE);
2587 }
2588 break;
2589 }
2590 return 1; // return 1 to become the belowmouse
2591}
2592
2593int HelpWidget::handleKeys() {
2594 int result = 0;
2595 switch (Fl::event_key()) {
2596 case FL_Right:
2597 if (-hscroll < w() / 2) {
2598 hscroll -= HSCROLL_STEP;
2599 redraw();
2600 result = 1;
2601 }
2602 break;
2603 case FL_Left:
2604 if (hscroll < 0) {
2605 hscroll += HSCROLL_STEP;
2606 redraw();
2607 result = 1;
2608 }
2609 break;
2610 default:
2611 break;
2612 }
2613
2614 if (!result && Fl::event_state(FL_CTRL)) {
2615 result = 1;
2616 switch (Fl::event_key()) {
2617 case 'u':
2618 vscroll(-scrollWindowHeight);
2619 break;
2620 case 'd':
2621 vscroll(scrollWindowHeight);
2622 break;
2623 case 'r':
2624 reloadPage();
2625 break;
2626 case 'f':
2627 find(fl_input("Find:"), false);
2628 break;
2629 case 'a':
2630 selectAll();
2631 break;
2632 case FL_Insert:
2633 case 'c':
2634 copySelection();
2635 break;
2636 case 'b':
2637 case 'q':
2638 if (Fl::modal() == parent()) {
2639 Fl::modal()->set_non_modal();
2640 }
2641 break;
2642 default:
2643 result = 0;
2644 break;
2645 }
2646 }
2647 return result;
2648}
2649
2650int HelpWidget::handle(int event) {
2651 int handled = Fl_Group::handle(event);
2652 if (handled && event != FL_MOVE) {
2653 return handled;
2654 }
2655
2656 switch (event) {
2657 case EVENT_COPY_TEXT:
2658 copySelection();
2659 return 1;
2660
2661 case EVENT_SEL_ALL_TEXT:
2662 selectAll();
2663 return 1;
2664
2665 case EVENT_FIND:
2666 find(fl_input("Find:"), false);
2667 return 1;
2668
2669 case FL_SHOW:
2670 take_focus();
2671 break;
2672
2673 case FL_FOCUS:
2674 return 1; // aquire focus
2675
2676 case FL_PUSH:
2677 if (Fl::event_x() < w() - SCROLL_X) {
2678 return onPush(event);
2679 }
2680 break;
2681
2682 case FL_ENTER:
2683 return 1;
2684
2685 case FL_KEYDOWN:
2686 if (handleKeys()) {
2687 return 1;
2688 }
2689 break;
2690
2691 case FL_DRAG:
2692 case FL_MOVE:
2693 return onMove(event);
2694
2695 case FL_RELEASE:
2696 if (pushedAnchor) {
2697 fl_cursor(FL_CURSOR_DEFAULT);
2698 bool pushed = pushedAnchor->pushed;
2699 pushedAnchor->pushed = false;
2700 damage(DAMAGE_PUSHED);
2701 if (pushed) {
2702 this->event.clear();
2703 this->event.append(pushedAnchor->href.c_str());
2704 if (this->event.length()) {
2705 // href has been set
2706 user_data((void *)this->event.c_str());
2707 do_callback();
2708 }
2709 }
2710 return 1;
2711 }
2712 }
2713
2714 return scrollbar->active() ? scrollbar->handle(event) : 0;
2715}
2716
2717bool HelpWidget::find(const char *s, bool matchCase) {
2718 if (s == 0 || s[0] == 0) {
2719 return false;
2720 }
2721
2722 int foundRow = 0;
2723 int lineHeight = fl_height() + fl_descent();
2724
2725 List_each(BaseNode *, it, nodeList) {
2726 BaseNode *p = (*it);
2727 if (p->indexOf(s, matchCase) != -1) {
2728 foundRow = p->getY() - scrollbar->value();
2729 if (foundRow > -scrollbar->value() + lineHeight) {
2730 break;
2731 }
2732 }
2733 }
2734
2735 int scroll = scrollbar->value();
2736 if (-scroll == foundRow) {
2737 return false;
2738 }
2739
2740 scroll = foundRow;
2741
2742 // check scroll bounds
2743 if (foundRow) {
2744 scroll += lineHeight;
2745 }
2746 if (-scroll > scrollHeight) {
2747 scroll = -scrollHeight;
2748 }
2749
2750 scrollbar->value(scroll);
2751 redraw();
2752 return true;
2753}
2754
2755void HelpWidget::copySelection() {
2756 Fl::copy(selection.c_str(), selection.length(), true);
2757}
2758
2759void HelpWidget::selectAll() {
2760 markX = markY = 0;
2761 pointX = w();
2762 pointY = scrollHeight + h();
2763 selection.clear();
2764 getText(&selection);
2765 redraw();
2766}
2767
2768void HelpWidget::navigateTo(const char *s) {
2769 if (strncmp(s, "http://", 7) == 0) {
2770 // launch in real browser
2771 browseFile(s);
2772 return;
2773 }
2774
2775 strlib::String path;
2776 path.append(docHome);
2777
2778 int len = path.length();
2779 if (len && path[len - 1] == '/' && s[0] == '/') {
2780 // avoid adding double slashes
2781 path.append(s + 1);
2782 } else {
2783 path.append(s);
2784 }
2785 loadFile(path.c_str());
2786}
2787
2788void HelpWidget::loadBuffer(const char *str) {
2789 if (strncasecmp("file:", str, 5) == 0) {
2790 loadFile(str + 5);
2791 } else {
2792 htmlStr = str;
2793 reloadPage();
2794 }
2795}
2796
2797void HelpWidget::loadFile(const char *f, bool useDocHome) {
2798 FILE *fp;
2799
2800 fileName.clear();
2801 htmlStr.clear();
2802
2803 if (docHome.length() != 0 && useDocHome) {
2804 fileName.append(docHome);
2805 }
2806
2807 if (strncasecmp(f, "file:///", 8) == 0) {
2808 // only supports file protocol
2809 f += 8;
2810 }
2811
2812 const char *target = strrchr(f, '#');
2813 long len = target != NULL ? target - f : strlen(f);
2814 fileName.append(f, len);
2815 fileName.replaceAll('\\', '/');
2816
2817 // update docHome using the given file-name
2818 if (docHome.length() == 0) {
2819 int i = fileName.lastIndexOf('/', 0);
2820 if (i != -1) {
2821 docHome = fileName.substring(0, i + 1);
2822 } else {
2823 docHome.append("./");
2824 }
2825 if (docHome.length() > 0 && docHome[docHome.length() - 1] != '/') {
2826 docHome.append("/");
2827 }
2828 }
2829 if ((fp = fopen(fileName.c_str(), "rb")) != NULL) {
2830 fseek(fp, 0, SEEK_END);
2831 len = ftell(fp);
2832 rewind(fp);
2833 htmlStr.append(fp, len);
2834 fclose(fp);
2835 } else {
2836 htmlStr.append("File not found: \"");
2837 htmlStr.append(fileName.c_str());
2838 htmlStr.append("\" - ");
2839 htmlStr.append(strerror(errno));
2840 }
2841
2842 reloadPage();
2843 if (target) {
2844 // draw to obtain dimensions
2845 Fl::flush();
2846 scrollTo(target + 1);
2847 }
2848}
2849
2850// reload broken images
2851void HelpWidget::reloadImages() {
2852 List_each(ImageNode *, it, images) {
2853 ImageNode *imageNode = (*it);
2854 if (imageNode->image == &brokenImage) {
2855 imageNode->reload();
2856 }
2857 }
2858 redraw();
2859}
2860
2861void HelpWidget::setDocHome(const char *s) {
2862 docHome.clear();
2863 docHome.append(s);
2864 if (s && s[strlen(s) - 1] != '/') {
2865 docHome.append("/");
2866 }
2867}
2868
2869const char *HelpWidget::getAnchor(int index) {
2870 int len = anchors.size();
2871 if (index < len && index > -1) {
2872 return anchors[index]->href.c_str();
2873 }
2874 return NULL;
2875}
2876
2877void HelpWidget::getText(strlib::String *s) {
2878 List_each(BaseNode *, it, nodeList) {
2879 BaseNode *p = (*it);
2880 p->getText(s);
2881 }
2882}
2883
2884bool HelpWidget::isHtmlFile() {
2885 const char *filename = fileName.c_str();
2886 if (!fileName || !fileName[0]) {
2887 return false;
2888 }
2889 int len = strlen(filename);
2890 return (strcasecmp(filename + len - 4, ".htm") == 0 ||
2891 strcasecmp(filename + len - 5, ".html") == 0);
2892}
2893
2894void HelpWidget::vscroll(int offs) {
2895 if (scrollbar->active()) {
2896 int value = scrollbar->value() + offs;
2897 scrollbar->value(value);
2898 }
2899}
2900
2901//--Helper functions------------------------------------------------------------
2902
2903/**
2904 * Returns the number of characters that will fit within
2905 * the gixen pixel width.
2906 */
2907void lineBreak(const char *s, int slen, int width, int &linelen, int &linepx) {
2908 // find the end of the first word
2909 int i = 0;
2910 int txtWidth;
2911 int ibreak = -1;
2912 int breakWidth = -1;
2913
2914 while (i < slen) {
2915 if (s[i++] == ' ') {
2916 ibreak = i;
2917 break;
2918 }
2919 }
2920
2921 // no break point found
2922 if (ibreak == -1) {
2923 linelen = slen;
2924 linepx = fl_width(s, slen);
2925 return;
2926 }
2927 // find the last break-point within the available width
2928 txtWidth = fl_width(s, i);
2929 ibreak = i;
2930 breakWidth = txtWidth;
2931
2932 while (i < slen && txtWidth < width) {
2933 ibreak = i;
2934 breakWidth = txtWidth;
2935 while (i < slen) {
2936 if (s[i++] == ' ') {
2937 break;
2938 }
2939 }
2940 txtWidth += fl_width(s + ibreak, i - ibreak);
2941 }
2942
2943 if (txtWidth < width) {
2944 // entire segment fits
2945 linelen = slen;
2946 linepx = txtWidth;
2947 } else {
2948 // first break-point is after boundary
2949 linelen = ibreak;
2950 linepx = breakWidth;
2951 }
2952}
2953
2954// return a new tagEnd if the current '>' is embedded in quotes
2955bool unquoteTag(const char *tagBegin, const char *&tagEnd) {
2956 bool quote = false;
2957 int len = tagEnd - tagBegin;
2958 int i = 1;
2959 while (i < len) {
2960 switch (tagBegin[i++]) {
2961 case '\'':
2962 case '\"':
2963 quote = !quote;
2964 break;
2965 }
2966 }
2967 // <input type="ffff>"> - move end-tag
2968 // <input type="ffff>text<next-tag> end-tag is unchanged
2969 // <input value='@>>' >
2970 if (quote) {
2971 // found unclosed quote within tag
2972 i = 1;
2973 while (true) {
2974 switch (tagEnd[i]) {
2975 case 0:
2976 case '<':
2977 return false;
2978 case '\'':
2979 case '\"':
2980 quote = !quote;
2981 break;
2982 case '>':
2983 if (quote == false) {
2984 tagEnd += i;
2985 return true;
2986 }
2987 break;
2988 }
2989 i++;
2990 }
2991 }
2992 return false;
2993}
2994
2995/**
2996 * skip white space between tags:
2997 * <table>-skip-<tr>-skip-<td></td>-skip</tr>-skip-</table>
2998 */
2999const char *skipWhite(const char *s) {
3000 if (s == 0 || s[0] == 0) {
3001 return 0;
3002 }
3003 while (IS_WHITE(*s)) {
3004 s++;
3005 }
3006 return s;
3007}
3008
3009Fl_Color getColor(strlib::String *s, Fl_Color def) {
3010 Fl_Color result;
3011 if (s != NULL && s->length()) {
3012 result = get_color(s->c_str(), def);
3013 } else {
3014 result = def;
3015 }
3016 return result;
3017}
3018
3019// image factory based on file extension
3020Fl_Shared_Image *loadImage(const char *name, uchar *buff) {
3021 return Fl_Shared_Image::get(name);
3022}
3023
3024Fl_Image *loadImage(const char *imgSrc) {
3025 if (imgSrc == 0 || access(imgSrc, 0) != 0) {
3026 return &brokenImage;
3027 }
3028 Fl_Image *image = loadImage(imgSrc, 0);
3029 return image != 0 ? image : &brokenImage;
3030}
3031
3032void browseFile(const char *url) {
3033 fl_open_uri(url, nullptr, 0);
3034}
3035