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