1 // ----------------------------------------------------------------------------
2 //      FTextView.cxx
3 //
4 // Copyright (C) 2007-2009
5 //              Stelios Bounanos, M0GLD
6 //
7 // Copyright (C) 2008-2009
8 //              Dave Freese, W1HKJ
9 //
10 // This file is part of fldigi.
11 //
12 // fldigi is free software; you can redistribute it and/or modify
13 // it under the terms of the GNU General Public License as published by
14 // the Free Software Foundation; either version 3 of the License, or
15 // (at your option) any later version.
16 //
17 // fldigi is distributed in the hope that it will be useful,
18 // but WITHOUT ANY WARRANTY; without even the implied warranty of
19 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 // GNU General Public License for more details.
21 //
22 // You should have received a copy of the GNU General Public License
23 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
24 // ----------------------------------------------------------------------------
25 
26 #include <config.h>
27 
28 #include <cstring>
29 #include <cstdlib>
30 #include <cstdio>
31 #include <cmath>
32 #include <sys/stat.h>
33 #include <map>
34 #include <fstream>
35 #include <sstream>
36 #include <iostream>
37 #include <algorithm>
38 #include <iomanip>
39 
40 #include <string>
41 
42 #include <FL/Fl_Tooltip.H>
43 
44 #include "flmisc.h"
45 #include "fileselect.h"
46 #include "font_browser.h"
47 #include "ascii.h"
48 #include "icons.h"
49 #include "gettext.h"
50 #include "macros.h"
51 
52 #include "FTextView.h"
53 
54 #include "debug.h"
55 
56 using namespace std;
57 
58 
59 /// FTextBase constructor.
60 /// Word wrapping is enabled by default at column 80, but see \c reset_wrap_col.
61 /// @param x
62 /// @param y
63 /// @param w
64 /// @param h
65 /// @param l
FTextBase(int x,int y,int w,int h,const char * l)66 FTextBase::FTextBase(int x, int y, int w, int h, const char *l)
67 	: Fl_Text_Editor_mod(x, y, w, h, l),
68           wrap(true), wrap_col(80), max_lines(0), scroll_hint(false)
69 {
70 	oldw = oldh = olds = -1;
71 	oldf = (Fl_Font)-1;
72 	textfont(FL_HELVETICA);
73 	textsize(FL_NORMAL_SIZE);
74 	textcolor(FL_FOREGROUND_COLOR);
75 
76 	tbuf = new Fl_Text_Buffer_mod;
77 	sbuf = new Fl_Text_Buffer_mod;
78 
79 	buffer(tbuf);
80 	highlight_data(sbuf, styles, NATTR, FTEXT_DEF, 0, 0);
81 	cursor_style(Fl_Text_Editor_mod::NORMAL_CURSOR);
82 
83 // reset_styles MUST before the call to wrap_mode or mStyleTable will have
84 // garbage values!
85 
86 	reset_styles(SET_FONT | SET_SIZE | SET_COLOR);
87 	wrap_mode(wrap, wrap_col);
88 	restore_wrap = wrap;
89 }
90 
clear()91 void FTextBase::clear()
92 {
93 	tbuf->text("");
94 	sbuf->text("");
95 	set_word_wrap(restore_wrap);
96 }
97 
handle(int event)98 int FTextBase::handle(int event)
99 {
100         if (event == FL_MOUSEWHEEL && !Fl::event_inside(this))
101                 return 1;
102 
103 	// Fl_Text_Editor::handle() calls window()->cursor(FL_CURSOR_DONE) when
104 	// it receives an FL_KEYBOARD event, which crashes some buggy X drivers
105 	// (e.g. Intel on the Asus Eee PC).  Call handle_key directly to work
106 	// around this problem.
107 	if (event == FL_KEYBOARD)
108 		return Fl_Text_Editor_mod::handle_key();
109 	else
110 		return Fl_Text_Editor_mod::handle(event);
111 }
112 
113 /// @see FTextRX::add
114 ///
115 /// @param s
116 /// @param attr
117 ///
add(const char * s,int attr)118 void FTextBase::add(const char *s, int attr)
119 {
120 	// handle the text attribute first
121 	int n = strlen(s);
122 	char a[n + 1];
123 	memset(a, FTEXT_DEF + attr, n);
124 	a[n] = '\0';
125 	sbuf->replace(insert_position(), insert_position() + n, a);
126 	insert(s);
127 }
128 
129 /// @see FTextBase::add
130 ///
131 /// @param s
132 /// @param attr
133 ///
134 #if FLDIGI_FLTK_API_MAJOR == 1 && FLDIGI_FLTK_API_MINOR >= 3
add(unsigned int c,int attr)135 void FTextBase::add(unsigned int c, int attr)
136 #else
137 void FTextBase::add(unsigned char c, int attr)
138 #endif
139 {
140 	char s[] = { (char)(FTEXT_DEF + attr), '\0' };
141 	sbuf->replace(insert_position(), insert_position() + 1, s);
142 
143 	s[0] = c & 0xFF;
144 	insert(s);
145 }
146 
set_word_wrap(bool b,bool b2)147 void FTextBase::set_word_wrap(bool b, bool b2)
148 {
149 	wrap_mode((wrap = b), wrap_col);
150 	if (b2) restore_wrap = wrap;
151 	show_insert_position();
152 }
153 
setFont(Fl_Font f,int attr)154 void FTextBase::setFont(Fl_Font f, int attr)
155 {
156 	set_style(attr, f, textsize(), textcolor(), SET_FONT);
157 }
158 
setFontSize(int s,int attr)159 void FTextBase::setFontSize(int s, int attr)
160 {
161 	set_style(attr, textfont(), s, textcolor(), SET_SIZE);
162 }
163 
setFontColor(Fl_Color c,int attr)164 void FTextBase::setFontColor(Fl_Color c, int attr)
165 {
166 	set_style(attr, textfont(), textsize(), c, SET_COLOR);
167 }
168 
169 /// Resizes the text widget.
170 /// The real work is done by \c Fl_Text_Editor_mod::resize or, if \c HSCROLLBAR_KLUDGE
171 /// is defined, a version of that code modified so that no horizontal
172 /// scrollbars are displayed when word wrapping.
173 ///
174 /// @param X
175 /// @param Y
176 /// @param W
177 /// @param H
178 ///
resize(int X,int Y,int W,int H)179 void FTextBase::resize(int X, int Y, int W, int H)
180 {
181 	bool need_wrap_reset = false;
182 	bool need_margin_reset = false;
183 
184 	if (unlikely(text_area.w != oldw)) {
185 		oldw = text_area.w;
186 		need_wrap_reset = true;
187 	}
188 	if (unlikely(text_area.h != oldh)) {
189 		oldh = text_area.h;
190 		need_margin_reset = true;
191 	}
192 	if (unlikely(textfont() != oldf || textsize() != olds)) {
193 		oldf = textfont();
194 		olds = textsize();
195 		need_wrap_reset = need_margin_reset = true;
196 	}
197 
198 	if (need_wrap_reset)
199 		reset_wrap_col();
200 
201 	TOP_MARGIN = DEFAULT_TOP_MARGIN;
202 	int r = H - Fl::box_dh(box()) - TOP_MARGIN - BOTTOM_MARGIN;
203 	if (mHScrollBar->visible())
204 		r -= scrollbar_width();
205 	int msize = mMaxsize ? mMaxsize : textsize();
206 	if (!msize) msize = 1;
207 //printf("H %d, textsize %d, lines %d, extra %d\n", r, msize, r / msize, r % msize);
208 	if (r %= msize)
209 		TOP_MARGIN += r;
210 	if (scroll_hint) {
211 		mTopLineNumHint = mNBufferLines;
212 		mHorizOffsetHint = 0;
213 //		display_insert_position_hint = 1;
214 		scroll_hint = false;
215 	}
216 
217 	bool hscroll_visible = mHScrollBar->visible();
218 	Fl_Text_Editor_mod::resize(X, Y, W, H);
219 	if (hscroll_visible != mHScrollBar->visible())
220 		oldh = 0; // reset margins next time
221 }
222 
223 /// Checks the new widget height.
224 /// This is registered with Fl_Tile_check and then called with horizontal
225 /// and vertical size increments every time the Fl_Tile boundary is moved.
226 ///
227 /// @param arg The callback argument; should be a pointer to a FTextBase object
228 /// @param xd The horizontal increment (ignored)
229 /// @param yd The vertical increment
230 ///
231 /// @return True if the widget is visible, and the new text area height would be
232 ///         a multiple of the font height.
233 ///
wheight_mult_tsize(void * arg,int,int yd)234 bool FTextBase::wheight_mult_tsize(void *arg, int, int yd)
235 {
236 	FTextBase *v = reinterpret_cast<FTextBase *>(arg);
237 	if (!v->visible())
238 		return true;
239 	return v->mMaxsize > 0 && (v->text_area.h + yd) % v->mMaxsize == 0;
240 }
241 
242 /// Changes text style attributes
243 ///
244 /// @param attr The attribute name to change, or \c NATTR to change all styles.
245 /// @param f The new font
246 /// @param s The new font size
247 /// @param c The new font color
248 /// @param set One or more (OR'd together) SET operations; @see set_style_op_e
249 ///
set_style(int attr,Fl_Font f,int s,Fl_Color c,int set)250 void FTextBase::set_style(int attr, Fl_Font f, int s, Fl_Color c, int set)
251 {
252 	int start, end;
253 
254 	if (attr == NATTR) { // update all styles
255 		start = 0;
256 		end = NATTR;
257 		if (set & SET_FONT)
258 			Fl_Text_Display_mod::textfont(f);
259 		if (set & SET_SIZE)
260 			textsize(s);
261 		if (set & SET_COLOR)
262 			textcolor(c);
263 	}
264 	else {
265 		start = attr;
266 		end = start + 1;
267 	}
268 	for (int i = start; i < end; i++) {
269 		styles[i].attr = 0;
270 		if (set & SET_FONT)
271 			styles[i].font = f;
272 		if (set & SET_SIZE)
273 			styles[i].size = s;
274 		if (set & SET_COLOR)
275 			styles[i].color = c;
276 		if (i == SKIP) // clickable styles always same as SKIP for now
277 			for (int j = CLICK_START; j < NATTR; j++)
278 				memcpy(&styles[j], &styles[i], sizeof(styles[j]));
279 	}
280 	if (set & SET_COLOR)
281 		mCursor_color = styles[0].color;
282 
283 	resize(x(), y(), w(), h()); // to redraw and recalculate the wrap column
284 }
285 
286 /// Reads a file and inserts its contents.
287 /// change all occurrences of ^ to ^^ to prevent get_tx_char from
288 /// treating the carat as a control sequence, ie: ^r ^R ^t ^T ^L ^C
289 /// get_tx_char passes ^^ as a single ^
290 ///
291 /// @return 0 on success, -1 on error
readFile(const char * fn)292 int FTextBase::readFile(const char* fn)
293 {
294 	set_word_wrap(restore_wrap);
295 
296 	if ( !(fn || (fn = FSEL::select(_("Insert text"), "Text\t*.txt"))) )
297 		return -1;
298 
299 	int ret = 0, pos = insert_position();
300 
301 #ifdef __WOE32__
302 	FILE* tfile = fl_fopen(fn, "rt");
303 #else
304 	FILE* tfile = fl_fopen(fn, "r");
305 #endif
306 	if (!tfile)
307 		return -1;
308 	char buf[BUFSIZ+1];
309 	std::string newbuf;
310 	size_t p;
311 	memset(buf, 0, BUFSIZ+1);
312 	p = 0;
313 	while (fgets(buf, sizeof(buf), tfile)) {
314 		newbuf.append(buf);
315 		memset(buf, 0, BUFSIZ+1);
316 	}
317 	if (ferror(tfile))
318 		return (-1);
319 	fclose(tfile);
320 
321 	while ((p = newbuf.find("^",p)) != string::npos) {
322 		newbuf.insert(p, "^");
323 		p += 2;
324 	}
325 	p = 0;
326 	while ((p = newbuf.find("@^^", p)) != string::npos) {
327 		newbuf.erase(p,2);
328 	}
329 	if (pos == tbuf->length()) { // optimise for append
330 		tbuf->append(newbuf.c_str());
331 		pos = tbuf->length();
332 	}
333 	else {
334 		tbuf->insert(pos, newbuf.c_str());
335 		pos += newbuf.length();
336 	}
337 
338 	insert_position(pos);
339 	show_insert_position();
340 
341 	return ret;
342 }
343 
344 /// Writes all buffer text out to a file.
345 ///
346 ///
saveFile(void)347 void FTextBase::saveFile(void)
348 {
349  	const char *fn = FSEL::saveas(_("Save text as"), "Text\t*.txt");
350 	if (fn) {
351 #ifdef __WOE32__
352 		ofstream tfile(fn);
353 		if (!tfile)
354 			return;
355 
356 		char *p1, *p2, *text = tbuf->text();
357 		for (p1 = p2 = text; *p1; p1 = p2) {
358 			while (*p2 != '\0' && *p2 != '\r')
359 				p2++;
360 			if (*p2 == '\n') {
361 				*p2 = '\0';
362 				tfile << p1 << "\r\n";
363 				p2++;
364 
365 			}
366 			else
367 				tfile << p1;
368 		}
369 		free(text);
370 #else
371 		tbuf->outputfile(fn, 0, tbuf->length());
372 #endif
373 	}
374 }
375 
376 /// Returns a character string containing the selected (n) word(s), if any,
377 /// or the word at (\a x, \a y) relative to the widget's \c x() and \c y().
378 /// If \a ontext is true, this function will return text only if the
379 /// mouse cursor position is inside the text range.
380 ///
381 /// @param x
382 /// @param y
383 ///
384 /// @return The selection, or the word text at (x,y). <b>Must be freed by the caller</b>.
385 ///
get_word(int x,int y,const char * nwchars,int n,bool ontext)386 char* FTextBase::get_word(int x, int y, const char* nwchars, int n, bool ontext)
387 {
388 	int p = xy_to_position(x + this->x(), y + this->y(), Fl_Text_Display_mod::CURSOR_POS);
389 	int start, end;
390 
391 	if (tbuf->selected()) {
392 		if (ontext && (p < start || p >= end) && tbuf->selection_position(&start, &end))
393 			return 0;
394 		else
395 			return tbuf->selection_text();
396 	}
397 
398 	string nonword = nwchars;
399 	nonword.append(" \t\n");
400 	if (!tbuf->findchars_backward(p, nonword.c_str(), &start))
401 		start = 0;
402 	else
403 		start++;
404 	if (!tbuf->findchars_forward(p, nonword.c_str(), &end, n))
405 		return 0;
406 //		end = tbuf->length();
407 
408 	if (start >= end) return 0;
409 
410 	if (ontext && (p < start || p >= end))
411 		return 0;
412 	else
413 		return tbuf->text_range(start, end);
414 }
415 
416 /// Initialised the menu pointed to by \c context_menu.  The menu items' user_data
417 /// field is used to store the initialisation flag.
init_context_menu(void)418 void FTextBase::init_context_menu(void)
419 {
420 	for (int i = 0; i < context_menu->size() - 1; i++) {
421 		if (context_menu[i].user_data() == 0 &&
422 		    context_menu[i].labeltype() == _FL_MULTI_LABEL) {
423 			icons::set_icon_label(&context_menu[i]);
424 			context_menu[i].user_data(this);
425 		}
426 	}
427 }
428 
429 /// Displays the menu pointed to by \c context_menu and calls the menu function;
430 /// @see call_cb.
431 ///
show_context_menu(void)432 void FTextBase::show_context_menu(void)
433 {
434 	const Fl_Menu_Item *m;
435 	int xpos = Fl::event_x();
436 	int ypos = Fl::event_y();
437 
438 	popx = xpos - x();
439 	popy = ypos - y();
440 	window()->cursor(FL_CURSOR_DEFAULT);
441 	m = context_menu->popup(xpos, ypos, 0, 0, 0);
442 	if (m)
443 		menu_cb(m - context_menu);
444 }
445 
446 /// Recalculates the wrap margin when the font is changed or the widget resized.
447 /// Line wrapping works with proportional fonts but may be very slow.
448 ///
reset_wrap_col(void)449 int FTextBase::reset_wrap_col(void)
450 {
451 	if (!wrap || text_area.w == 0)
452 		return wrap_col;
453 
454 	int old_wrap_col = wrap_col;
455 	if (Font_Browser::fixed_width(textfont())) {
456 		fl_font(textfont(), textsize());
457 		wrap_col = (int)floorf(text_area.w / fl_width('X'));
458 	}
459 	else // use slower (but accurate) wrapping for variable width fonts
460 		wrap_col = 0;
461 	// wrap_mode triggers a resize; don't call it if wrap_col hasn't changed
462 	if (old_wrap_col != wrap_col)
463 		wrap_mode(wrap, wrap_col);
464 
465 	return old_wrap_col;
466 }
467 
reset_styles(int set)468 void FTextBase::reset_styles(int set)
469 {
470 	set_style(NATTR, FL_HELVETICA, FL_NORMAL_SIZE, FL_FOREGROUND_COLOR, set);
471 	set_style(XMIT, FL_HELVETICA, FL_NORMAL_SIZE, FL_RED, set);
472 	set_style(CTRL, FL_HELVETICA, FL_NORMAL_SIZE, FL_DARK_GREEN, set);
473 	set_style(SKIP, FL_HELVETICA, FL_NORMAL_SIZE, FL_BLUE, set);
474 	set_style(ALTR, FL_HELVETICA, FL_NORMAL_SIZE, FL_DARK_MAGENTA, set);
475 	set_style(FSQ_TX, FL_HELVETICA, FL_NORMAL_SIZE, FL_RED, set);
476 	set_style(FSQ_DIR, FL_HELVETICA, FL_NORMAL_SIZE, FL_BLUE, set);
477 	set_style(FSQ_UND, FL_HELVETICA, FL_NORMAL_SIZE, FL_DARK_GREEN, set);
478 
479 }
480 
481 // ----------------------------------------------------------------------------
482 
483 Fl_Menu_Item FTextView::menu[] = {
484 	{ icons::make_icon_label(_("Copy"), edit_copy_icon), 0, 0, 0, 0, _FL_MULTI_LABEL },
485 	{ icons::make_icon_label(_("Clear"), edit_clear_icon), 0, 0, 0, 0, _FL_MULTI_LABEL },
486 	{ icons::make_icon_label(_("Select All"), edit_select_all_icon), 0, 0, 0, FL_MENU_DIVIDER, _FL_MULTI_LABEL },
487 	{ icons::make_icon_label(_("Save as..."), save_as_icon), 0, 0, 0, FL_MENU_DIVIDER, _FL_MULTI_LABEL },
488 	{ _("Word wrap"),       0, 0, 0, FL_MENU_TOGGLE, FL_NORMAL_LABEL },
489 	{ 0 }
490 };
491 
492 /// FTextView constructor.
493 /// We remove \c Fl_Text_Display_mod::buffer_modified_cb from the list of callbacks
494 /// because we want to scroll depending on the visibility of the last line; @see
495 /// changed_cb.
496 /// @param x
497 /// @param y
498 /// @param w
499 /// @param h
500 /// @param l
FTextView(int x,int y,int w,int h,const char * l)501 FTextView::FTextView(int x, int y, int w, int h, const char *l)
502         : FTextBase(x, y, w, h, l), quick_entry(false)
503 {
504 	tbuf->remove_modify_callback(buffer_modified_cb, this);
505 	tbuf->add_modify_callback(changed_cb, this);
506 	tbuf->canUndo(0);
507 
508 	// disable some keybindings that are not allowed in FTextView buffers
509 	change_keybindings();
510 
511 	context_menu = menu;
512 	init_context_menu();
513 }
514 
515 /// Handles fltk events for this widget.
516 
517 /// We only care about mouse presses (to display the popup menu and prevent
518 /// pasting) and keyboard events (to make sure no text can be inserted).
519 /// Everything else is passed to the base class handle().
520 ///
521 /// @param event
522 ///
523 /// @return
524 ///
handle(int event)525 int FTextView::handle(int event)
526 {
527 	switch (event) {
528 	case FL_PUSH:
529 		if (!Fl::event_inside(this))
530 			break;
531  		if (Fl::event_button() == FL_RIGHT_MOUSE) {
532 			handle_context_menu();
533 			return 1;
534  		}
535 		if (Fl::event_button() == FL_MIDDLE_MOUSE)
536 			return 1; // ignore mouse2 text pastes inside the received text
537 		break;
538 	case FL_DRAG:
539 		if (Fl::event_button() != FL_LEFT_MOUSE)
540 			return 1;
541 		break;
542 	// catch some text-modifying events that are not handled by kf_* functions
543 	case FL_KEYBOARD:
544 		int k;
545 		if (Fl::compose(k))
546 			return 1;
547 		k = Fl::event_key();
548 		if (k == FL_BackSpace)
549 			return 1;
550 		else if (k == FL_Tab)
551 			return Fl_Widget::handle(event);
552 	}
553 
554 	return FTextBase::handle(event);
555 }
556 
handle_context_menu(void)557 void FTextView::handle_context_menu(void)
558 {
559 	icons::set_active(&menu[VIEW_MENU_COPY], tbuf->selected());
560 	icons::set_active(&menu[VIEW_MENU_CLEAR], tbuf->length());
561 	icons::set_active(&menu[VIEW_MENU_SELECT_ALL], tbuf->length());
562 	icons::set_active(&menu[VIEW_MENU_SAVE], tbuf->length());
563 	if (wrap)
564 		menu[VIEW_MENU_WRAP].set();
565 	else
566 		menu[VIEW_MENU_WRAP].clear();
567 
568 	show_context_menu();
569 }
570 
571 /// The context menu handler
572 ///
573 /// @param val
574 ///
menu_cb(size_t item)575 void FTextView::menu_cb(size_t item)
576 {
577 	switch (item) {
578 	case VIEW_MENU_COPY:
579 		kf_copy(Fl::event_key(), this);
580 		break;
581 	case VIEW_MENU_CLEAR:
582 		clear();
583 		break;
584 	case VIEW_MENU_SELECT_ALL:
585 		tbuf->select(0, tbuf->length());
586 		break;
587 	case VIEW_MENU_SAVE:
588 		saveFile();
589 		break;
590 	case VIEW_MENU_WRAP:
591 		set_word_wrap(!wrap, true);
592 		break;
593 	}
594 }
595 
596 /// Scrolls down if the buffer has been modified and the last line is
597 /// visible. See Fl_Text_Buffer::add_modify_callback() for parameter details.
598 ///
599 /// @param pos
600 /// @param nins
601 /// @param ndel
602 /// @param nsty
603 /// @param dtext
604 /// @param arg
605 ///
606 inline
changed_cb(int pos,int nins,int ndel,int nsty,const char * dtext,void * arg)607 void FTextView::changed_cb(int pos, int nins, int ndel, int nsty, const char *dtext, void *arg)
608 {
609 	FTextView *v = reinterpret_cast<FTextView *>(arg);
610 
611 	if (v->mTopLineNum + v->mNVisibleLines - 1 == v->mNBufferLines)
612 		v->scroll_hint = true;
613 
614 	v->buffer_modified_cb(pos, nins, ndel, nsty, dtext, v);
615 }
616 
617 /// Removes Fl_Text_Edit keybindings that would modify text and put it out of
618 /// sync with the style buffer. At some point we may decide that we want
619 /// FTextView to be editable (e.g., to insert comments about a QSO), in which
620 /// case we'll keep the keybindings and add some code to changed_cb to update
621 /// the style buffer.
622 ///
change_keybindings(void)623 void FTextView::change_keybindings(void)
624 {
625 	Fl_Text_Editor_mod::Key_Func fdelete[] = { Fl_Text_Editor_mod::kf_default,
626 					       Fl_Text_Editor_mod::kf_enter,
627 					       Fl_Text_Editor_mod::kf_delete,
628 					       Fl_Text_Editor_mod::kf_cut,
629 					       Fl_Text_Editor_mod::kf_paste };
630 	int n = sizeof(fdelete) / sizeof(fdelete[0]);
631 
632 	// walk the keybindings linked list and delete items containing elements
633 	// of fdelete
634 loop:
635 	for (Fl_Text_Editor_mod::Key_Binding *k = key_bindings; k; k = k->next) {
636 		for (int i = 0; i < n; i++) {
637 			if (k->function == fdelete[i]) {
638 				remove_key_binding(k->key, k->state);
639 				goto loop;
640 			}
641 		}
642 	}
643 }
644 
645 // ----------------------------------------------------------------------------
646 
647 
648 Fl_Menu_Item FTextEdit::menu[] = {
649 	{ icons::make_icon_label(_("Cut"), edit_cut_icon), 0, 0, 0, 0, _FL_MULTI_LABEL },
650 	{ icons::make_icon_label(_("Copy"), edit_copy_icon), 0, 0, 0, 0, _FL_MULTI_LABEL },
651 	{ icons::make_icon_label(_("Paste"), edit_paste_icon), 0, 0, 0, 0, _FL_MULTI_LABEL },
652 	{ icons::make_icon_label(_("Clear"), edit_clear_icon), 0, 0, 0, FL_MENU_DIVIDER, _FL_MULTI_LABEL },
653 	{ icons::make_icon_label(_("Insert file..."), file_open_icon), 0, 0, 0, FL_MENU_DIVIDER, _FL_MULTI_LABEL },
654 	{ _("Word wrap"), 0, 0, 0, FL_MENU_TOGGLE | FL_MENU_DIVIDER, FL_NORMAL_LABEL } ,
655 	{ 0 }
656 };
657 
FTextEdit(int x,int y,int w,int h,const char * l)658 FTextEdit::FTextEdit(int x, int y, int w, int h, const char *l)
659 	: FTextBase(x, y, w, h, l)
660 {
661 	tbuf->remove_modify_callback(buffer_modified_cb, this);
662 	tbuf->add_modify_callback(changed_cb, this);
663 
664 	ascii_cnt = 0;
665 	ascii_chr = 0;
666 
667 	context_menu = menu;
668 	init_context_menu();
669 
670 	dnd_paste = false;
671 }
672 
673 /// Handles fltk events for this widget.
674 /// We pass keyboard events to handle_key() and handle mouse3 presses to show
675 /// the popup menu. We also disallow mouse2 events in the transmitted text area.
676 /// Everything else is passed to the base class handle().
677 ///
678 /// @param event
679 ///
680 /// @return
681 ///
handle(int event)682 int FTextEdit::handle(int event)
683 {
684 	if ( !(Fl::event_inside(this) || (event == FL_KEYBOARD && Fl::focus() == this)) )
685 		return FTextBase::handle(event);
686 
687 	switch (event) {
688 	case FL_KEYBOARD:
689 		return handle_key(Fl::event_key()) ? 1 : FTextBase::handle(event);
690 	case FL_DND_RELEASE:
691 		dnd_paste = true;
692 		// fall through
693 	case FL_DND_ENTER: case FL_DND_LEAVE:
694 		return 1;
695 	case FL_DND_DRAG:
696 		return handle_dnd_drag(xy_to_position(Fl::event_x(), Fl::event_y(), CHARACTER_POS));
697 	case FL_PASTE:
698 	{
699 		int r = dnd_paste ? handle_dnd_drop() : FTextBase::handle(event);
700 		dnd_paste = false;
701 		return r;
702 	}
703 	case FL_PUSH:
704 	{
705 		int eb = Fl::event_button();
706 		if (eb == FL_RIGHT_MOUSE) {
707 			handle_context_menu();
708 			return 1;
709 		}
710 	}
711 	default:
712 		break;
713 	}
714 
715 	return FTextBase::handle(event);
716 }
717 
718 /// Handles keyboard events to override Fl_Text_Editor_mod's handling of some
719 /// keystrokes.
720 ///
721 /// @param key
722 ///
723 /// @return
724 ///
handle_key(int key)725 int FTextEdit::handle_key(int key)
726 {
727 // read ctl-ddd, where d is a digit, as ascii characters (in base 10)
728 // and insert verbatim; e.g. ctl-001 inserts a <soh>
729 	if (Fl::event_state() & FL_CTRL && (isdigit(key) || isdigit(key - FL_KP)))
730 		return handle_key_ascii(key);
731 	ascii_cnt = 0; // restart the numeric keypad entries.
732 	ascii_chr = 0;
733 
734 	return 0;
735 }
736 
737 /// Composes ascii characters and adds them to the FTextEdit buffer.
738 /// Control characters are inserted with the CTRL style. Values larger than 127
739 /// (0x7f) are ignored. We cannot really add NULs for the time being.
740 ///
741 /// @param key A digit character
742 ///
743 /// @return 1
744 ///
handle_key_ascii(int key)745 int FTextEdit::handle_key_ascii(int key)
746 {
747 	if (key  >= FL_KP)
748 		key -= FL_KP;
749 	key -= '0';
750 	ascii_cnt++;
751 	for (int i = 0; i < 3 - ascii_cnt; i++)
752 		key *= 10;
753 	ascii_chr += key;
754 	if (ascii_cnt == 3) {
755 		if (ascii_chr < 0x100) {
756 			char buff[fl_utf8bytes(ascii_chr) + 1];
757 			int utf8cnt = fl_utf8encode(ascii_chr, buff);
758 			for ( int i = 0; i < utf8cnt; i++)
759 				add(buff[i], (iscntrl(ascii_chr) ? CTRL : RECV));
760 		}
761 		ascii_cnt = ascii_chr = 0;
762 	}
763 
764 	return 1;
765 }
766 
767 /// Handles FL_DND_DRAG events by scrolling and moving the cursor
768 ///
769 /// @return 1
handle_dnd_drag(int pos)770 int FTextEdit::handle_dnd_drag(int pos)
771 {
772 	// Scroll if the pointer is being dragged inside the scrollbars,
773 	// otherwise obtain keyboard focus and set the insert position.
774 	if (mVScrollBar->visible() && Fl::event_inside(mVScrollBar))
775 		mVScrollBar->handle(FL_DRAG);
776 	else if (mHScrollBar->visible() && Fl::event_inside(mHScrollBar))
777 		mHScrollBar->handle(FL_DRAG);
778 	else {
779 		if (Fl::focus() != this)
780 			take_focus();
781 		insert_position(pos);
782 	}
783 
784 	return 1;
785 }
786 
787 /// Handles FL_PASTE events by inserting text
788 ///
789 /// @return 1 or FTextBase::handle(FL_PASTE)
handle_dnd_drop(void)790 int FTextEdit::handle_dnd_drop(void)
791 {
792 // paste verbatim if the shift key was held down during dnd
793 	if (Fl::event_shift())
794 		return FTextBase::handle(FL_PASTE);
795 
796 	string text;
797 	string::size_type p, len;
798 
799 	text = Fl::event_text();
800 
801 	const char sep[] = "\n";
802 #if defined(__APPLE__) || defined(__WOE32__)
803 	text += sep;
804 #endif
805 
806 	len = text.length();
807 	while ((p = text.find(sep)) != string::npos) {
808 		text[p] = '\0';
809 #if !defined(__APPLE__) && !defined(__WOE32__)
810 		if (text.find("file://") == 0) {
811 			text.erase(0, 7);
812 			p -= 7;
813 			len -= 7;
814 		}
815 #endif
816 
817 #ifndef BUILD_FLARQ
818 	if ((text.find(".jpg") != string::npos) ||
819 		(text.find(".JPG") != string::npos) ||
820 		(text.find(".jpeg") != string::npos) ||
821 		(text.find(".JPEG") != string::npos) ||
822 		(text.find(".png") != string::npos) ||
823 		(text.find(".PNG") != string::npos) ||
824 		(text.find(".bmp") != string::npos) ||
825 		(text.find(".BMP") != string::npos) ) {
826 
827 		LOG_INFO("DnD image %s", text.c_str());
828 
829 		if ((p = text.find("file://")) != string::npos)
830 			text.erase(0, p + strlen("file://"));
831 		if ((p = text.find('\r')) != string::npos)
832 			text.erase(p);
833 		if ((p = text.find('\n')) != string::npos)
834 			text.erase(p);
835 		if (text[text.length()-1] == 0) text.erase(text.length() -1);
836 		TxQueINSERTIMAGE(text);
837 		return 1;
838 	}
839 #endif
840 
841 // paste everything verbatim if we cannot read the first file
842 		LOG_INFO("DnD file %s", text.c_str());
843 		if (readFile(text.c_str()) == -1 && len == text.length())
844 			return FTextBase::handle(FL_PASTE);
845 		text.erase(0, p + sizeof(sep) - 1);
846 	}
847 
848 	return 1;
849 }
850 
851 /// Handles mouse-3 clicks by displaying the context menu
852 ///
853 /// @param val
854 ///
handle_context_menu(void)855 void FTextEdit::handle_context_menu(void)
856 {
857 	bool selected = tbuf->selected();
858 std::cout << "FTextEdit::tbuf " << (selected ? "selected" : "not selected") << std::endl;
859 	icons::set_active(&menu[EDIT_MENU_CUT], selected);
860 	icons::set_active(&menu[EDIT_MENU_COPY], selected);
861 	icons::set_active(&menu[EDIT_MENU_CLEAR], tbuf->length());
862 
863 	if (wrap)
864 		menu[EDIT_MENU_WRAP].set();
865 	else
866 		menu[EDIT_MENU_WRAP].clear();
867 
868 	show_context_menu();
869 }
870 
871 /// The context menu handler
872 ///
873 /// @param val
874 ///
menu_cb(size_t item)875 void FTextEdit::menu_cb(size_t item)
876 {
877   	switch (item) {
878 	case EDIT_MENU_CLEAR:
879 		clear();
880 		break;
881 	case EDIT_MENU_CUT:
882 		kf_cut(0, this);
883 		break;
884 	case EDIT_MENU_COPY:
885 		kf_copy(0, this);
886 		break;
887 	case EDIT_MENU_PASTE:
888 		kf_paste(0, this);
889 		break;
890 	case EDIT_MENU_READ:
891 		readFile();
892 		break;
893 	case EDIT_MENU_WRAP:
894 		set_word_wrap(!wrap, true);
895 		break;
896 	default:
897 		if (FTextEdit::menu[item].flags == 0) { // not an FL_SUB_MENU
898 			add(FTextEdit::menu[item].text[0]);
899 			add(FTextEdit::menu[item].text[1]);
900 		}
901 	}
902 }
903 
904 /// This function is called by Fl_Text_Buffer when the buffer is modified, and
905 /// also by nextChar when a character has been passed up the transmit path. In
906 /// the first case either nins or ndel will be nonzero, and we change a
907 /// corresponding amount of text in the style buffer.
908 ///
909 /// In the latter case, nins, ndel, pos and nsty are all zero and we update the
910 /// style buffer to mark the last character in the buffer with the XMIT
911 /// attribute.
912 ///
913 /// @param pos
914 /// @param nins
915 /// @param ndel
916 /// @param nsty
917 /// @param dtext
918 /// @param arg
919 ///
changed_cb(int pos,int nins,int ndel,int nsty,const char * dtext,void * arg)920 void FTextEdit::changed_cb(int pos, int nins, int ndel, int nsty, const char *dtext, void *arg)
921 {
922 	FTextEdit *e = reinterpret_cast<FTextEdit *>(arg);
923 
924 	if (nins == 0 && ndel == 0) {
925 		if (nsty == -1) { // called by nextChar to update transmitted text style
926 			char s[] = { FTEXT_DEF + XMIT, '\0' };
927 			e->sbuf->replace(pos - 1, pos, s);
928 			e->redisplay_range(pos - 1, pos);
929 		}
930 		else if (nsty > 0) // restyled, e.g. selected, text
931 			return e->buffer_modified_cb(pos, nins, ndel, nsty, dtext, e);
932 
933                 // No changes, e.g., a paste with an empty clipboard.
934 		return;
935 	}
936 	else if (nins > 0 && e->sbuf->length() < e->tbuf->length()) {
937 		// New text not inserted by our add() methods, i.e., via a file
938 		// read, mouse-2 paste or, most likely, direct keyboard entry.
939 		int n = e->tbuf->length() - e->sbuf->length();
940 		if (n == 1) {
941 			char s[] = { FTEXT_DEF, '\0' };
942 			e->sbuf->append(s);
943 		}
944 		else {
945 			char *s = new char [n + 1];
946 			memset(s, FTEXT_DEF, n);
947 			s[n] = '\0';
948 			e->sbuf->append(s);
949 			delete [] s;
950 		}
951 	}
952 	else if (ndel > 0)
953 		e->sbuf->remove(pos, pos + ndel);
954 
955 	e->sbuf->select(pos, pos + nins - ndel);
956 
957 	e->buffer_modified_cb(pos, nins, ndel, nsty, dtext, e);
958 	// We may need to scroll if the text was inserted by the
959 	// add() methods, e.g. by a macro
960 	if (e->mTopLineNum + e->mNVisibleLines - 1 <= e->mNBufferLines)
961 		e->show_insert_position();
962 }
963