// ----------------------------------------------------------------------------
//      FTextRXTX.cxx
//
// Copyright (C) 2007-2010
//              Stelios Bounanos, M0GLD
//
// Copyright (C) 2008-2010
//              Dave Freese, W1HKJ
//
// This file is part of fldigi.
//
// fldigi is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3 of the License, or
// (at your option) any later version.
//
// fldigi is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
// ----------------------------------------------------------------------------

#include <config.h>

#include <string>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <sys/stat.h>
#include <map>
#include <iostream>
#include <fstream>
#include <sstream>
#include <algorithm>
#include <iomanip>

#include <FL/Fl_Tooltip.H>

#include "FTextView.h"
#include "main.h"
#include "trx.h"
#include "macros.h"
#include "main.h"
#include "fl_digi.h"

#include "cw.h"

#include "fileselect.h"
#include "font_browser.h"

#include "ascii.h"
#include "configuration.h"
#include "qrunner.h"

#include "mfsk.h"
#include "icons.h"
#include "globals.h"
#include "re.h"
#include "strutil.h"
#include "dxcc.h"
#include "locator.h"
#include "logsupport.h"
#include "status.h"
#include "gettext.h"
#include "arq_io.h"
#include "fl_digi.h"

#include "debug.h"


using namespace std;


// Fl_Scrollbar wrapper to draw marks on the slider background.
// Currently only implemented for a vertical scrollbar.
class MVScrollbar : public Fl_Scrollbar
{
	struct mark_t {
		double pos;
		Fl_Color color;
		mark_t(double pos_, Fl_Color color_) : pos(pos_), color(color_) { }
	};

public:
	MVScrollbar(int X, int Y, int W, int H, const char* l = 0)
		: Fl_Scrollbar(X, Y, W, H, l), draw_marks(false) { }

	void draw(void);
	void mark(Fl_Color c) { marks.push_back(mark_t(maximum() - 1.0, c)); redraw(); }
	bool has_marks(void) { return !marks.empty(); }
	void show_marks(bool b) { draw_marks = b; redraw(); }
	void clear(void) { marks.clear(); redraw(); }

private:
	vector<mark_t> marks;
	bool draw_marks;
};

Fl_Menu_Item FTextRX::menu[] = {
	{ icons::make_icon_label(_("Look up call"), net_icon), 0, 0, 0, FL_MENU_DIVIDER, _FL_MULTI_LABEL },
	{ icons::make_icon_label(_("Call"), enter_key_icon), 0, 0, 0, 0, _FL_MULTI_LABEL },
	{ icons::make_icon_label(_("Name"), enter_key_icon), 0, 0, 0, 0, _FL_MULTI_LABEL },
	{ icons::make_icon_label(_("QTH"), enter_key_icon), 0, 0, 0, 0, _FL_MULTI_LABEL },
	{ icons::make_icon_label(_("State"), enter_key_icon), 0, 0, 0, 0, _FL_MULTI_LABEL },
	{ icons::make_icon_label(_("Province"), enter_key_icon), 0, 0, 0, 0, _FL_MULTI_LABEL },
	{ icons::make_icon_label(_("Country"), enter_key_icon), 0, 0, 0, 0, _FL_MULTI_LABEL },
	{ icons::make_icon_label(_("Locator"), enter_key_icon), 0, 0, 0, 0, _FL_MULTI_LABEL },
	{ icons::make_icon_label(_("RST(r)"), enter_key_icon), 0, 0, 0,  FL_MENU_DIVIDER, _FL_MULTI_LABEL },
	{ icons::make_icon_label(_("Exchange In"), enter_key_icon), 0, 0, 0, 0, _FL_MULTI_LABEL },
	{ icons::make_icon_label(_("Serial number"), enter_key_icon), 0, 0, 0, FL_MENU_DIVIDER, _FL_MULTI_LABEL },
	{ icons::make_icon_label(_("Insert marker"), insert_link_icon), 0, 0, 0, 0, _FL_MULTI_LABEL },

	{ 0 }, // VIEW_MENU_COPY
	{ 0 }, // VIEW_MENU_CLEAR
	{ 0 }, // VIEW_MENU_SELECT_ALL
	{ 0 }, // VIEW_MENU_SAVE
	{ 0 }, // VIEW_MENU_WRAP

	{ _("Quick entry"),      0, 0, 0, FL_MENU_TOGGLE, FL_NORMAL_LABEL },
	{ _("Scroll hints"),      0, 0, 0, FL_MENU_TOGGLE, FL_NORMAL_LABEL },
	{ 0 }
};

/// FTextRX constructor.
/// We remove \c Fl_Text_Display_mod::buffer_modified_cb from the list of callbacks
/// because we want to scroll depending on the visibility of the last line; @see
/// changed_cb.
/// @param x
/// @param y
/// @param w
/// @param h
/// @param l
FTextRX::FTextRX(int x, int y, int w, int h, const char *l)
        : FTextView(x, y, w, h, l)
{
	memcpy(menu + RX_MENU_COPY, FTextView::menu, (FTextView::menu->size() - 1) * sizeof(*FTextView::menu));
	context_menu = menu;
	init_context_menu();
	menu[RX_MENU_QUICK_ENTRY].clear();
	menu[RX_MENU_SCROLL_HINTS].clear();
	menu[RX_MENU_WRAP].hide();
	// Replace the scrollbar widget
	MVScrollbar* mvsb = new MVScrollbar(mVScrollBar->x(), mVScrollBar->y(),
					    mVScrollBar->w(), mVScrollBar->h(), NULL);
	mvsb->show_marks(false);
	mvsb->callback(mVScrollBar->callback(), mVScrollBar->user_data());
	remove(mVScrollBar);
	delete mVScrollBar;
	Fl_Group::add(mVScrollBar = mvsb);
	mFastDisplay = 1;
}

FTextRX::~FTextRX()
{
}

/// Handles fltk events for this widget.

/// We only care about mouse presses (to display the popup menu and prevent
/// pasting) and keyboard events (to make sure no text can be inserted).
/// Everything else is passed to the base class handle().
///
/// @param event
///
/// @return
///
int FTextRX::handle(int event)
{
	static Fl_Cursor cursor;

	switch (event) {
	case FL_DRAG:
		if (Fl::event_button() != FL_LEFT_MOUSE)
			return 1;
		break;
	case FL_PUSH:
		if (!Fl::event_inside(this))
			break;
		switch (Fl::event_button()) {
		case FL_LEFT_MOUSE:
			if (Fl::event_shift() || progdefaults.rxtext_clicks_qso_data) {
				if (handle_clickable(Fl::event_x() - x(), Fl::event_y() - y()))
					return 1;
				if (handle_qso_data(Fl::event_x() - x(), Fl::event_y() - y()))
					return 1;
			}
			goto out;
		case FL_MIDDLE_MOUSE:
			if (cursor != FL_CURSOR_HAND) {
				if (handle_qso_data(Fl::event_x() - x(), Fl::event_y() - y())) {
					return 1;
				}
			}
			goto out;
 		case FL_RIGHT_MOUSE:
			handle_context_menu();
			return 1;
 		default:
 			goto out;
 		}
		break;
	case FL_RELEASE:
			break;
	case FL_MOVE: {
		int p = xy_to_position(Fl::event_x(), Fl::event_y(), Fl_Text_Display_mod::CURSOR_POS);
		if ((unsigned char)sbuf->byte_at(p) >= CLICK_START + FTEXT_DEF) {
			if (cursor != FL_CURSOR_HAND)
				window()->cursor(cursor = FL_CURSOR_HAND);
			return 1;
		}
		else
			cursor = FL_CURSOR_INSERT;
		break;
	}
	// catch some text-modifying events that are not handled by kf_* functions
	case FL_KEYBOARD:
		break;
	case FL_ENTER:
		if (!progdefaults.rxtext_tooltips || Fl_Tooltip::delay() == 0.0f)
			break;
		tooltips.enabled = Fl_Tooltip::enabled();
		tooltips.delay = Fl_Tooltip::delay();
		Fl_Tooltip::enable(1);
		Fl_Tooltip::delay(0.0f);
		Fl::add_timeout(tooltips.delay / 2.0, dxcc_tooltip, this);
		break;
	case FL_LEAVE:
		if (!progdefaults.rxtext_tooltips || Fl_Tooltip::delay() != 0.0f)
			break;
		Fl_Tooltip::enable(tooltips.enabled);
		Fl_Tooltip::delay(tooltips.delay);
		Fl::remove_timeout(dxcc_tooltip, this);
		break;
	}

out:
	return FTextView::handle(event);
}

/// Adds a char to the buffer
///
/// @param c The character
/// @param attr The attribute (@see enum text_attr_e); RECV if omitted.
///

void FTextRX::add(unsigned int c, int attr)
{
	if (c == '\r')
		return;

	char s[] = { '\0', '\0', char( FTEXT_DEF + attr ), '\0' };
	const char *cp = &s[0];

	// The user may have moved the cursor by selecting text or
	// scrolling. Place it at the end of the buffer.
	if (mCursorPos != tbuf->length())
		insert_position(tbuf->length());

	switch (c) {
	case '\b':
		// we don't call kf_backspace because it kills selected text
		if (s_text.length()) {
			int character_start = tbuf->utf8_align(tbuf->length() - 1);
			int character_length = fl_utf8len1(tbuf->byte_at(character_start));

			tbuf->remove(character_start, tbuf->length());
			sbuf->remove(character_start, sbuf->length());
			s_text.resize(s_text.length() - character_length);
			s_style.resize(s_style.length() - character_length);
		}
		break;
	case '\n':
		// maintain the scrollback limit, if we have one
		if (max_lines > 0 && tbuf->count_lines(0, tbuf->length()) >= max_lines) {
			int le = tbuf->line_end(0) + 1; // plus 1 for the newline
			tbuf->remove(0, le);
			sbuf->remove(0, le);
		}
		s_text.clear();
		s_style.clear();
		insert("\n");
		sbuf->append(s + 2);
		break;
	default:
		if ((c < ' ' || c == 127) && attr != CTRL) // look it up
			cp = ascii[(unsigned char)c];
		else  // insert verbatim
			s[0] = c;

		for (int i = 0; cp[i]; ++i) {
			s_text += cp[i];
			s_style += s[2];
		}

		fl_font( textfont(), textsize() );
		int lwidth = (int)fl_width( s_text.c_str(), s_text.length());
		bool wrapped = false;
		if ( lwidth >= (text_area.w - mVScrollBar->w() - LEFT_MARGIN - RIGHT_MARGIN)) {
			if (c != ' ') {
				size_t p = s_text.rfind(' ');
				if (p != string::npos) {
					s_text.erase(0, p+1);
					s_style.erase(0, p+1);
					if (s_text.length() < 10) { // wrap and delete trailing space
						tbuf->remove(tbuf->length() - s_text.length(), tbuf->length());
						sbuf->remove(sbuf->length() - s_style.length(), sbuf->length());
						insert("\n"); // always insert new line
						sbuf->append(s + 2);
						insert(s_text.c_str());
						sbuf->append(s_style.c_str());
						wrapped = true;
					}
				}
			}
			if (!wrapped) { // add a new line if not wrapped
				insert("\n");
				sbuf->append(s + 2);
				s_text.clear();
				s_style.clear();
				if (c != ' ') { // add character if not a space (no leading spaces)
					for (int i = 0; cp[i]; ++i) {
						sbuf->append(s + 2);
						s_style.append(s + 2);
					}
					s_text.append(cp);
					insert(cp);
				}
			}
		} else {
			for (int i = 0; cp[i]; ++i)
				sbuf->append(s + 2);
			insert(cp);
		}
		break;
	}

// test for bottom of text visibility
	if (// !mFastDisplay &&
		(mVScrollBar->value() >= mNBufferLines - mNVisibleLines + mVScrollBar->linesize() - 1))
		show_insert_position();
}

void FTextRX::set_quick_entry(bool b)
{
	if (b)
		menu[RX_MENU_QUICK_ENTRY].set();
	else
		menu[RX_MENU_QUICK_ENTRY].clear();
}

void FTextRX::set_scroll_hints(bool b)
{
	if (b)
		menu[RX_MENU_SCROLL_HINTS].set();
	else
		menu[RX_MENU_SCROLL_HINTS].clear();
	static_cast<MVScrollbar*>(mVScrollBar)->show_marks(b);
}

void FTextRX::mark(FTextBase::TEXT_ATTR attr)
{
	if (attr == NATTR)
		attr = CLICK_START;
	static_cast<MVScrollbar*>(mVScrollBar)->mark(styles[attr].color);
}

void FTextRX::clear(void)
{
	FTextBase::clear();
	s_text.clear();
	s_style.clear();
	static_cast<MVScrollbar*>(mVScrollBar)->clear();
}

void FTextRX::setFont(Fl_Font f, int attr)
{
	FTextBase::setFont(f, attr);
}

int FTextRX::handle_clickable(int x, int y)
{
	int pos;
	unsigned int style;

	pos = xy_to_position(x + this->x(), y + this->y(), CURSOR_POS);
	// return unless clickable style
	if ((style = (unsigned char)sbuf->byte_at(pos)) < CLICK_START + FTEXT_DEF)
		return 0;

	int start, end;
	for (start = pos-1; start >= 0; start--)
		if ((unsigned char)sbuf->byte_at(start) != style)
			break;
	start++;
	int len = sbuf->length();
	for (end = pos+1; end < len; end++)
		if ((unsigned char)sbuf->byte_at(end) != style)
			break;

	switch (style - FTEXT_DEF) {
	case QSY:
		handle_qsy(start, end);
		return 1;
		break;
	// ...
	default:
		break;
	}
	return 0;
}

void FTextRX::handle_qsy(int start, int end)
{
	char* text = tbuf->text_range(start, end);

	extern map<string, qrg_mode_t> qrg_marks;
	map<string, qrg_mode_t>::const_iterator i;
	if ((i = qrg_marks.find(text)) != qrg_marks.end()) {
		const qrg_mode_t& m = i->second;
		if (active_modem->get_mode() != m.mode)
			init_modem_sync(m.mode);
		qsy(m.rfcarrier, m.carrier);
	}

	free(text);
}

static fre_t rst("^[1-5][1-9]{2}$", REG_EXTENDED | REG_NOSUB);
static fre_t loc("[a-r]{2}[[:digit:]]{2}([a-x]{2})?", REG_EXTENDED | REG_ICASE);
static fre_t call("([[:alnum:]]?[[:alpha:]/]+[[:digit:]]+[[:alnum:]/]+)", REG_EXTENDED);

int FTextRX::handle_qso_data(int start, int end)
{
	char* s = get_word(start, end, progdefaults.nonwordchars.c_str());
	if (!s)
		return 0;
	char* p = s;

	Fl_Input2* target = 0;

	if (QsoInfoFrame1B->visible()) {
		if (call.match(s)) { // point p to substring
			const regmatch_t& offsets = call.suboff()[1];
			p = s + offsets.rm_so;
			*(s + offsets.rm_eo) = '\0';
			inpCall->value(p);
			log_callback(inpCall);
			Fl::copy(p, strlen(p), 1);  // copy to clipboard
		} else {
			inpXchgIn->position(inpXchgIn->size());
			if (inpXchgIn->size()) inpXchgIn->insert(" ", 1);
			inpXchgIn->insert(s);
			log_callback(inpXchgIn);
		}
	} else {
		if (rst.match(s))
			target = inpRstIn;
		else if (loc.match(s))
			target = inpLoc;
		else if (call.match(s)) { // point p to substring
			const regmatch_t& offsets = call.suboff()[1];
			p = s + offsets.rm_so;
			*(s + offsets.rm_eo) = '\0';
			target = inpCall;
			Fl::copy(p, strlen(p), 1);  // copy to clipboard
		}
		else if (count_if(s, s + strlen(s), static_cast<int(*)(int)>(isdigit)))
			target = inpQth;
		else
			target = *inpName->value() ? inpQth : inpName;
		if (target) {
			target->value(p);
			log_callback(target);
			free(s);
			restoreFocus(91);
			return 1;
		}
	}
	free(s);
	return 0;
}

void FTextRX::handle_context_menu(void)
{
	bool contest_ui = progStatus.Rig_Contest_UI ||
		(progStatus.contest && !progStatus.NO_RIGLOG && !progStatus.Rig_Log_UI);

	unsigned shown = 0;

#define show_item(x_) (shown |=  (1 << x_))
#define hide_item(x_) (shown &= ~(1 << x_))
#define test_item(x_) (shown &   (1 << x_))

	show_item(RX_MENU_CALL);

	if (contest_ui || (progStatus.contest && !progStatus.NO_RIGLOG && !progStatus.Rig_Log_UI)) {
		show_item(RX_MENU_SERIAL);
		show_item(RX_MENU_XCHG);
	}
// "Look up call" shown only in non-contest mode
	else if (progdefaults.QRZWEB != QRZWEBNONE || progdefaults.QRZXML != QRZXMLNONE)
		show_item(RX_MENU_QRZ_THIS);

	if (menu[RX_MENU_QUICK_ENTRY].value()) {
		for (size_t i = RX_MENU_NAME; i <= RX_MENU_RST_IN; i++)
			show_item(i);
		menu[RX_MENU_CALL].flags &= ~FL_MENU_DIVIDER;
	}
	else {
		if (!contest_ui && !progStatus.contest) {
			show_item(RX_MENU_NAME);
			show_item(RX_MENU_QTH);
			show_item(RX_MENU_RST_IN);
		}
		menu[RX_MENU_CALL].flags |= FL_MENU_DIVIDER;
	}

	if (static_cast<MVScrollbar*>(mVScrollBar)->has_marks())
		menu[RX_MENU_SCROLL_HINTS].show();
	else
		menu[RX_MENU_SCROLL_HINTS].hide();

	for (size_t i = RX_MENU_QRZ_THIS; i <= RX_MENU_XCHG; i++) {
		if (test_item(i))
			menu[i].show();
		else
			menu[i].hide();
	}

#undef show_item
#undef hide_item
#undef test_item

	// availability of editing items depend on buffer state
	icons::set_active(&menu[RX_MENU_COPY], tbuf->selected());
	icons::set_active(&menu[RX_MENU_CLEAR], tbuf->length());
	icons::set_active(&menu[RX_MENU_SELECT_ALL], tbuf->length());
	icons::set_active(&menu[RX_MENU_SAVE], tbuf->length());

	if (wrap)
		menu[RX_MENU_WRAP].set();
	else
		menu[RX_MENU_WRAP].clear();

	show_context_menu();
}

/// The context menu handler
///
/// @param val
///
void FTextRX::menu_cb(size_t item)
{
	Fl_Input2* input = 0;
	switch (item) {
	case RX_MENU_QRZ_THIS:
		menu_cb(RX_MENU_CALL);
		extern void CALLSIGNquery();
		CALLSIGNquery();
		break;
	case RX_MENU_CALL:
		input = inpCall;
		break;
	case RX_MENU_NAME:
		input = inpName;
		break;
	case RX_MENU_QTH:
		input = inpQth;
		break;
	case RX_MENU_STATE:
		input = inpState;
		break;
	case RX_MENU_PROVINCE:
		input = inpVEprov;
		break;
	case RX_MENU_COUNTRY:
		input = inpCountry;
		break;
	case RX_MENU_LOC:
		input = inpLoc;
		break;
	case RX_MENU_RST_IN:
		input = inpRstIn;
		break;

	case RX_MENU_SERIAL:
		input = inpSerNo;
		break;
	case RX_MENU_XCHG:
		input = inpXchgIn;
		break;

	case RX_MENU_DIV:
		note_qrg(false, "\n", "\n");
		break;
	case RX_MENU_COPY:
		kf_copy(Fl::event_key(), this);
		break;
	case RX_MENU_CLEAR:
		clear();
		break;
	case RX_MENU_SELECT_ALL:
		tbuf->select(0, tbuf->length());
		break;

	case RX_MENU_SAVE:
		saveFile();
		break;

	case RX_MENU_QUICK_ENTRY:
		menu[item].flags ^= FL_MENU_VALUE;
		if (menu[item].value())
			handle_context_menu();
		break;

	case RX_MENU_WRAP:
		set_word_wrap(!wrap, true);
		break;

	case RX_MENU_SCROLL_HINTS:
		menu[item].flags ^= FL_MENU_VALUE;
		static_cast<MVScrollbar*>(mVScrollBar)->show_marks(menu[item].value());
		break;
	}

	restoreFocus(92);

	if (!input)
		return;
	char* s = get_word(popx, popy, progdefaults.nonwordchars.c_str());
	if (!s)
		return;

	if (item == RX_MENU_XCHG) { // append
		input->position(input->size());
		if (input->size())
			input->insert(" ", 1);
		input->insert(s);
	}
	else
		input->value(s);
	log_callback(input);
	free(s);
}

const char* FTextRX::dxcc_lookup_call(int x, int y)
{
	char* s = get_word(x - this->x(), y - this->y(), progdefaults.nonwordchars.c_str());
	char* mem = s;
	if (!(s && *s && call.match(s))) {
		free(s);
		return 0;
	}

	double lon1, lat1, lon2 = 360.0, lat2 = 360.0, distance, azimuth;
	static string tip;
	ostringstream stip;
	const dxcc* e = 0;
	cQsoRec* qso = 0;
	unsigned char qsl;

	// prevent locator-only lookup if Ctrl is held
	if (!(Fl::event_state() & FL_CTRL) && loc.match(s)) {
		const vector<regmatch_t>& v = loc.suboff();
		s += v[0].rm_so;
		*(s + v[0].rm_eo) = '\0';
		if (QRB::locator2longlat(&lon2, &lat2, s) != QRB::QRB_OK)
			goto ret;
		e = 0; qsl = 0; qso = 0;
	}
	else {
		e = dxcc_lookup(s);
		qsl = qsl_lookup(s);
		qso = SearchLog(s);
	}

	if (qso && QRB::locator2longlat(&lon2, &lat2, qso->getField(GRIDSQUARE)) != QRB::QRB_OK)
		lon2 = lat2 = 360.0;

	if (e) {
		// use dxcc data if we didn't have a good locator string in the log file
		if (lon2 == 360.0)
			lon2 = -e->longitude;
		if (lat2 == 360.0)
			lat2 = e->latitude;
		stip << e->country << " (" << e->continent
		     << " GMT" << fixed << showpos << setprecision(1) << -e->gmt_offset << noshowpos
		     << ") CQ-" << e->cq_zone << " ITU-" << e->itu_zone << '\n';
	}

	if (QRB::locator2longlat(&lon1, &lat1, progdefaults.myLocator.c_str()) == QRB::QRB_OK &&
	    QRB::qrb(lon1, lat1, lon2, lat2, &distance, &azimuth) == QRB::QRB_OK) {
			if (progdefaults.us_units) {
				stip << "QTE " << fixed << setprecision(0) << azimuth << '\260' << " ("
					<< QRB::azimuth_long_path(azimuth) << '\260' << ")  QRB "
					<< distance * 0.62168188 << "mi"<< " (" <<
					QRB::distance_long_path(distance) * 0.62168188 <<
					"mi)\n";
			}
			else {
				stip << "QTE " << fixed << setprecision(0) << azimuth << '\260' << " ("
					<< QRB::azimuth_long_path(azimuth) << '\260' << ")  QRB "
					<< distance << "km(" <<
					QRB::distance_long_path(distance) << "km)\n";
			}
	}

	if (qso) {
		const char* info[] = {
			qso->getField(NAME), qso->getField(QTH), qso->getField(QSO_DATE),
			qso->getField(BAND), qso->getField(MODE)
		};
		// name & qth
		if (*info[0])
			join(stip << "* ", info, 2, _(" in "), true) << '\n';
		// other info
		join(stip << "* " << _("Last QSO") << ": ", info+2, 3, ", ", true) << '\n';
	}
	if (qsl) {
		stip << "* QSL: ";
		for (unsigned char i = 0; i < QSL_END; i++)
			if (qsl & (1 << i))
				stip << qsl_names[i] << ' ';
		stip << '\n';
	}

ret:
	free(mem);
	tip = stip.str();
	return tip.empty() ? 0 : tip.c_str();
}

void FTextRX::dxcc_tooltip(void* obj)
{
	struct point {
		int x, y;
		bool operator==(const point& p) { return x == p.x && y == p.y; }
		bool operator!=(const point& p) { return !(*this == p); }
	};
	static point p[3] = { {0, 0}, {0, 0}, {0, 0} };

	memmove(p, p+1, 2 * sizeof(point));
	p[2].x = Fl::event_x(); p[2].y = Fl::event_y();

	static const char* tip = 0;
	FTextRX* v = reinterpret_cast<FTextRX*>(obj);
	// look up word under cursor if we have been called twice with the cursor
	// at the same position, and if the cursor was previously somewhere else
	if (p[2] == p[1] && p[2] != p[0]  &&  ((tip = v->dxcc_lookup_call(p[2].x, p[2].y))))
		Fl_Tooltip::enter_area(v, p[2].x, p[2].y, 100, 100, tip);
	else if (p[2] != p[1])
		Fl_Tooltip::exit(v);

	Fl::repeat_timeout(tip ? Fl_Tooltip::hoverdelay() : v->tooltips.delay / 2.0, dxcc_tooltip, obj);
}


// ----------------------------------------------------------------------------


Fl_Menu_Item FTextTX::menu[] = {
	{ icons::make_icon_label(_("Transmit"), tx_icon), 0, 0, 0, 0, _FL_MULTI_LABEL },
	{ icons::make_icon_label(_("Receive"), rx_icon), 0, 0, 0, 0, _FL_MULTI_LABEL },
	{ icons::make_icon_label(_("Abort"), process_stop_icon), 0, 0, 0, 0, _FL_MULTI_LABEL },
	{ icons::make_icon_label(_("Send image..."), image_icon), 0, 0, 0, FL_MENU_DIVIDER, _FL_MULTI_LABEL },

	{ 0 }, // EDIT_MENU_CUT
	{ 0 }, // EDIT_MENU_COPY
	{ 0 }, // EDIT_MENU_PASTE
	{ 0 }, // EDIT_MENU_CLEAR
	{ 0 }, // EDIT_MENU_READ
	{ 0 }, // EDIT_MENU_WRAP

	{ _("Spec Char"), 0, 0, 0, FL_SUBMENU, FL_NORMAL_LABEL },
		{ "¢ - cent", 0, 0, 0, 0, FL_NORMAL_LABEL },
		{ "£ - pound", 0, 0, 0, 0, FL_NORMAL_LABEL },
		{ "µ - micro", 0, 0, 0, 0, FL_NORMAL_LABEL },
		{ "° - degree", 0, 0, 0, 0, FL_NORMAL_LABEL },
		{ "¿ - iques", 0, 0, 0, 0, FL_NORMAL_LABEL },
		{ "× - times", 0, 0, 0, 0, FL_NORMAL_LABEL },
		{ "÷ - divide", 0, 0, 0, 0, FL_NORMAL_LABEL },
		{ _("A"), 0, 0, 0, FL_SUBMENU, FL_NORMAL_LABEL },
			{ "À - grave", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "à - grave", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "Á - acute", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "á - acute", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "Â - circ", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "â - circ", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "Ã - tilde", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "ã - tilde", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "Ä - umlaut", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "ä - umlaut", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "Å - ring", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "å - ring", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "Æ - aelig", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "æ - aelig", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{0,0,0,0,0,0,0,0,0},
		{ _("E"), 0, 0, 0, FL_SUBMENU, FL_NORMAL_LABEL },
			{ "È - grave", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "è - grave", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "É - acute", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "é - acute", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "Ê - circ", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "Ê - circ", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "Ë - umlaut", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "ë - umlaut", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{0,0,0,0,0,0,0,0,0},
		{ _("I"), 0, 0, 0, FL_SUBMENU, FL_NORMAL_LABEL },
			{ "Ì - grave", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "ì - grave", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "Í - acute", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "í - acute", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "Î - circ", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "î - circ", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "Ï - umlaut", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "ï - umlaut", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{0,0,0,0,0,0,0,0,0},
		{ _("N"), 0, 0, 0, FL_SUBMENU, FL_NORMAL_LABEL },
			{ "Ñ - tilde", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "ñ - tilde", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{0,0,0,0,0,0,0,0,0},
		{ _("O"), 0, 0, 0, FL_SUBMENU, FL_NORMAL_LABEL },
			{ "Ò - grave", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "ò - grave", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "Ó - acute", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "ó - acute", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "Ô - circ", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "ô - circ", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "Õ - tilde", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "õ - tilde", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "Ö - umlaut", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "ö - umlaut", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "Ø - slash", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "ø - slash", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{0,0,0,0,0,0,0,0,0},
		{ _("U"), 0, 0, 0, FL_SUBMENU, FL_NORMAL_LABEL },
			{ "Ù - grave", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "ù - grave", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "Ú - acute", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "ú - acute", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "Û - circ", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "û - circ", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "Ü - umlaut", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "ü - umlaut", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{0,0,0,0,0,0,0,0,0},
		{ _("Y"), 0, 0, 0, FL_SUBMENU, FL_NORMAL_LABEL },
			{ "Ý - acute", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "ý - acute", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "ÿ - umlaut", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{0,0,0,0,0,0,0,0,0},
		{ _("Other"), 0, 0, 0, FL_SUBMENU, FL_NORMAL_LABEL },
			{ "ß - szlig", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "Ç - cedil", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "ç - cedil", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "Ð - eth", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "ð - eth", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{ "Þ - thorn", 0, 0, 0, 0, FL_NORMAL_LABEL },
			{0,0,0,0,0,0,0,0,0},
		{0,0,0,0,0,0,0,0,0},
	{ 0 }
};

// needed by our static kf functions, which may restrict editing depending on
// the transmit cursor position
int *FTextTX::ptxpos;

FTextTX::FTextTX(int x, int y, int w, int h, const char *l)
	: FTextEdit(x, y, w, h, l),
          PauseBreak(false), txpos(0), bkspaces(0)
{
	ptxpos = &txpos;

	change_keybindings();

	memcpy(menu + TX_MENU_CUT, FTextEdit::menu, (FTextEdit::menu->size() - 1) * sizeof(*FTextEdit::menu));
	context_menu = menu;
	init_context_menu();
	utf8_txpos = txpos = 0;
}

/// Handles fltk events for this widget.
/// We pass keyboard events to handle_key() and handle mouse3 presses to show
/// the popup menu. We also disallow mouse2 events in the transmitted text area.
/// Everything else is passed to the base class handle().
///
/// @param event
///
/// @return
///
int FTextTX::handle(int event)
{
	if ( !(Fl::event_inside(this) || (event == FL_KEYBOARD && Fl::focus() == this)) )
		return FTextEdit::handle(event);

	switch (event) {
	case FL_KEYBOARD:
		if (active_modem->get_mode() == MODE_FSQ) {
			if (Fl::event_key() == FL_Enter || Fl::event_key() == FL_KP_Enter) {
				fsq_transmit(0);
				return 1;
			}
		}
		return handle_key(Fl::event_key()) ? 1 : FTextEdit::handle(event);
	case FL_PUSH:
		if (Fl::event_button() == FL_MIDDLE_MOUSE &&
		    xy_to_position(Fl::event_x(), Fl::event_y(), CHARACTER_POS) < txpos)
			return 1; // ignore mouse2 text pastes inside the transmitted text
	}

	return FTextEdit::handle(event);
}

/// Clears the buffer.
/// Also resets the transmit position, stored backspaces and tx pause flag.
///
void FTextTX::clear(void)
{
	FTextEdit::clear();
	txpos = 0;
	utf8_txpos = 0;
	bkspaces = 0;
	PauseBreak = false;
}

/// Clears the sent text.
/// Also resets the transmit position, stored backspaces and tx pause flag.
///
void FTextTX::clear_sent(void)
{
 	tbuf->remove(0, utf8_txpos);
 	sbuf->remove(0, utf8_txpos);
	txpos = 0;
	utf8_txpos = 0;
	bkspaces = 0;
	PauseBreak = false;
	set_word_wrap(restore_wrap);
}

/// Returns boolean <eot> end of text
///
/// true if empty buffer
/// false if characters remain
///
bool FTextTX::eot(void)
{
	return (insert_position() == txpos);
}

/// Returns the next character to be transmitted.
///
/// @return The next character, or ETX if the transmission has been paused, or
/// NUL if no text should be transmitted.
///
int FTextTX::nextChar(void)
{
	int c;

	if (bkspaces) {
		--bkspaces;
		c = '\b';
	}
	else if (PauseBreak) {
		PauseBreak = false;
		c = GET_TX_CHAR_ETX;//0x03;
	} else if (insert_position() <= utf8_txpos) { // empty buffer or cursor inside transmitted text
		c = -1;
	} else {
		if ((c = tbuf->char_at(utf8_txpos)) > 0) {
			int n = fl_utf8bytes(c);

			REQ(FTextTX::changed_cb, utf8_txpos, 0, 0, -1, static_cast<const char *>(0), this);
			REQ(FTextTX::changed_cb, utf8_txpos+1, 0, 0, -1, static_cast<const char *>(0), this);
			++txpos;
			utf8_txpos += n;
		} else
			c = -1;
	}
	return c;
}

// called by xmlrpc thread
// called by macro execution
void FTextTX::add_text(string s)
{
	for (size_t n = 0; n < s.length(); n++) {
		if (s[n] == '\b') {
			int ipos = insert_position();
			if (tbuf->length()) {
				if (ipos > 0 && txpos == ipos) {
					bkspaces++;
					txpos--;
					int nn;
					tbuf->get_char_at(utf8_txpos, nn);
					utf8_txpos -= nn;
				}
				tbuf->remove(tbuf->length() - 1, tbuf->length());
				sbuf->remove(sbuf->length() - 1, sbuf->length());
				redraw();
			}
		} else {
//LOG_DEBUG("%04x ", s[n] & 0x00FF);
			add(s[n] & 0xFF, RECV);
		}
	}
}

void FTextTX::setFont(Fl_Font f, int attr)
{
	FTextBase::setFont(f, attr);
}

/// Handles keyboard shorcuts
///
/// @param key
//   pressed key
///
/// @return
//   1 if shortcut is handled, otherwise 0.
///
int FTextTX::handle_key_shortcuts(int key)
{
	std::string etag = "";

	switch (key) {
	case 'c': // add <CALL> for SC-c
	case 'm': // add <MYCALL> for SC-m
	case 'n': // add <NAME> for SC-n
	case 'r': // add <RST> for SC-r
	case 'l': // add <MYLOC> for SC-l
	case 'h': // add <MYQTH> for SC-h
	case 'a': // add <ANTENNA> for SC-a
		if ((Fl::event_state() & FL_CTRL) && (Fl::event_state() & FL_SHIFT))
//		if ((Fl::event_state() & (FL_CTRL | FL_SHIFT))) // investigate why this doesn't work...
		{
			switch (key)
			{
			case 'c':
				etag = inpCall->value();
				break;
			case 'm':
				etag = progdefaults.myCall;
				break;
			case 'n':
				etag = inpName->value();
				break;
			case 'r':
			        {
					std::string s;
					etag = (s = inpRstIn->value()).length() ? s : std::string("599");
				}
				break;
			case 'l':
				etag = progdefaults.myLocator;
				break;
			case 'h':
				etag = progdefaults.myQth;
				break;
			case 'a':
				etag = progdefaults.myAntenna;
			default:
				break;
			}

			// Add text + space if length is > 0
			if (etag.length())
				add_text(etag + std::string(" "));

			return 1;
		}
		break;

	default:
		break;
	}

	return 0;
}

/// Handles keyboard events to override Fl_Text_Editor_mod's handling of some
/// keystrokes.
///
/// @param key
///
/// @return
///
int FTextTX::handle_key(int key)
{

	if (handle_key_shortcuts(key))
	    return 1;

	switch (key) {
	case FL_Escape: // set stop flag and clear
	{
		static time_t t[2] = { 0, 0 };
		static unsigned char i = 0;
		if (t[i] == time(&t[!i])) { // two presses in a second: abort transmission
			if (trx_state == STATE_TX)
				menu_cb(TX_MENU_ABORT);
			t[i = !i] = 0;
			return 1;
		}
		i = !i;
	}

		if (trx_state == STATE_TX && active_modem->get_stopflag() == false) {
			kf_select_all(0, this);
			kf_copy(0, this);
			clear();
			if (arq_text_available)
				AbortARQ();
			active_modem->set_stopflag(true);
		}

		if (trx_state == STATE_TUNE)
			abort_tx();

		stopMacroTimer();
		return 1;

	case 't': // transmit for C-t
		if (trx_state == STATE_RX && Fl::event_state() & FL_CTRL) {
			menu_cb(TX_MENU_TX);
			return 1;
		}
		break;
	case 'r':// receive for C-r
		if (Fl::event_state() & FL_CTRL) {
			menu_cb(TX_MENU_RX);
			return 1;
		}
		else if (!(Fl::event_state() & (FL_META | FL_ALT)))
			break;
		// fall through to (un)pause for M-r or A-r

	case FL_Pause:
		if (trx_state != STATE_TX) {
			start_tx();
		}
		else
			PauseBreak = true;
		return 1;
	case (FL_KP + '+'):
		if (active_modem == cw_modem)
			active_modem->incWPM();
		return 1;
	case (FL_KP + '-'):
		if (active_modem == cw_modem)
			active_modem->decWPM();
		return 1;
	case (FL_KP + '*'):
		if (active_modem == cw_modem)
			active_modem->toggleWPM();
		return 1;
	case FL_Tab:
		if (active_modem == fsq_modem) return 1;
		// In non-CW modes: Tab and Ctrl-tab both pause until user moves the
		// cursor to let some more text through. Another (ctrl-)tab goes back to
		// the end of the buffer and resumes sending.

		// In CW mode: Tab pauses, skips rest of buffer, applies the
		// SKIP style, then resumes sending when new text is entered.
		// Ctrl-tab does the same thing as for all other modes.
		if (utf8_txpos != insert_position())
			insert_position(utf8_txpos);
		else
			insert_position(tbuf->length());
		if (!(Fl::event_state() & FL_CTRL) && active_modem == cw_modem) {
			int n = tbuf->length() - utf8_txpos;
			char s[n + 1];
			memset(s, FTEXT_DEF + SKIP, n);
			s[n] = 0;
			sbuf->replace(utf8_txpos, sbuf->length(), s);
			insert_position(tbuf->length());
			redisplay_range(utf8_txpos, insert_position());
			utf8_txpos = insert_position();
		}
		return 1;
	// Move cursor, or search up/down with the Meta/Alt modifiers
	case FL_Left:
		if (Fl::event_state() & (FL_META | FL_ALT)) {
			if (active_modem == fsq_modem) return 1;
			active_modem->searchDown();
			return 1;
		}
		return 0;
	case FL_Right:
		if (Fl::event_state() & (FL_META | FL_ALT)) {
			if (active_modem == fsq_modem) return 1;
			active_modem->searchUp();
			return 1;
		}
		return 0;
		// queue a BS and decr. the txpos, unless the cursor is in the tx text
	case FL_BackSpace:
	{
		int ipos = insert_position();
		if (utf8_txpos > 0 && utf8_txpos == ipos) {
			bkspaces++;
			utf8_txpos = tbuf->prev_char(ipos);
			txpos--;
		}
		return 0;
	}
// alt - 1 / 2 changes macro sets
	case '1':
	case '2':
	case '3':
	case '4':
		if (Fl::event_state() & FL_ALT) {
			if (active_modem == fsq_modem) return 1;
			static char lbl[2] = "1";
			altMacros = key - '1';
			if (progdefaults.mbar_scheme > MACRO_SINGLE_BAR_MAX) {
				if (!altMacros) altMacros = 1;
				for (int i = 0; i < NUMMACKEYS; i++) {
					btnMacro[NUMMACKEYS + i]->label(
						macros.name[(altMacros * NUMMACKEYS) + i].c_str());
					btnMacro[NUMMACKEYS + i]->redraw_label();
				}
				lbl[0] = key;
				btnAltMacros2->label(lbl);
				btnAltMacros2->redraw_label();
			} else {
				for (int i = 0; i < NUMMACKEYS; i++) {
					btnMacro[i]->label(
						macros.name[(altMacros * NUMMACKEYS) + i].c_str());
					btnMacro[i]->redraw_label();
				}
				lbl[0] = key;
				btnAltMacros1->label(lbl);
				btnAltMacros1->redraw_label();
			}
			return 1;
		}
		break;
	default:
		break;
	}

	if (insert_position() < txpos)
		return 1;

// insert a macro
	if (key >= FL_F && key <= FL_F_Last) {
		return handle_key_macro(key);
	}
// read ctl-ddd, where d is a digit, as ascii characters (in base 10)
// and insert verbatim; e.g. ctl-001 inserts a <soh>
	if (Fl::event_state() & FL_CTRL && (key >= FL_KP) && (key <= FL_KP + '9'))
		return handle_key_ascii(key);

// restart the numeric keypad entries.
	ascii_cnt = 0;
	ascii_chr = 0;

	return 0;
}

/// Inserts the macro for function key \c key.
///
/// @param key An integer in the range [FL_F, FL_F_Last]
///
/// @return 1
///
int FTextTX::handle_key_macro(int key)
{
	key -= FL_F + 1;

	if (active_modem == fsq_modem) {
		if (key == 0) fsq_repeat_last_heard();
		if (key == 1) fsq_repeat_last_command();
		return 1;
	}
	if (key > 11)
		return 0;

	if (progdefaults.mbar_scheme > MACRO_SINGLE_BAR_MAX) {
		if (Fl::event_state(FL_SHIFT))
			key += altMacros * NUMMACKEYS;
	} else {
		key += altMacros * NUMMACKEYS;
	}
	if (!(macros.text[key]).empty())
		macros.execute(key);

	return 1;
}

int FTextTX::handle_dnd_drag(int pos)
{
	if (pos >= txpos) {
		return FTextEdit::handle_dnd_drag(pos);
	}
	else // refuse drop inside transmitted text
		return 0;
}

/// Handles mouse-3 clicks by displaying the context menu
///
/// @param val
///
void FTextTX::handle_context_menu(void)
{
	// adjust Abort/Transmit/Receive menu items
	switch (trx_state) {
	case STATE_TX:
		menu[TX_MENU_TX].hide();
		menu[TX_MENU_RX].show();
		menu[TX_MENU_ABORT].show();
		break;
	case STATE_TUNE:
		menu[TX_MENU_TX].hide();
		menu[TX_MENU_RX].show();
		menu[TX_MENU_ABORT].hide();
		break;
	default:
		menu[TX_MENU_TX].show();
		menu[TX_MENU_RX].hide();
		menu[TX_MENU_ABORT].hide();
		break;
	}

	bool modify_text_ok = insert_position() >= txpos;
	bool selected = tbuf->selected();
 	icons::set_active(&menu[TX_MENU_MFSK16_IMG], active_modem->get_cap() & modem::CAP_IMG);
	icons::set_active(&menu[TX_MENU_CLEAR], tbuf->length());
	icons::set_active(&menu[TX_MENU_CUT], selected && modify_text_ok);
	icons::set_active(&menu[TX_MENU_COPY], selected);
	icons::set_active(&menu[TX_MENU_PASTE], modify_text_ok);
	icons::set_active(&menu[TX_MENU_READ], modify_text_ok);

	if (wrap)
		menu[TX_MENU_WRAP].set();
	else
		menu[TX_MENU_WRAP].clear();

	show_context_menu();
}

/// The context menu handler
///
/// @param val
///
void FTextTX::menu_cb(size_t item)
{
  	switch (item) {
  	case TX_MENU_TX:
		active_modem->set_stopflag(false);
		start_tx();
		break;
	case TX_MENU_ABORT:
		char panic[200];
		snprintf(panic, sizeof(panic), "*** Don't panic *** %s", progdefaults.myName.c_str());
		put_status(panic, 5.0);
		abort_tx();
  		break;
  	case TX_MENU_RX:
 		if (trx_state == STATE_TX) {
 			insert_position(tbuf->length());
 			add("^r", CTRL);
 		}
 		else
 			abort_tx();
  		break;
	case TX_MENU_MFSK16_IMG:
		{
			trx_mode md = active_modem->get_mode();
		if (md == MODE_IFKP)
			ifkp_showTxViewer();
		else if (md >= MODE_THOR_FIRST && md <= MODE_THOR_LAST)
			thor_showTxViewer();
		else
			showTxViewer(0, 0);
		break;
		}
	case TX_MENU_CLEAR:
		clear();
		break;
	case TX_MENU_CUT:
		kf_cut(0, this);
		break;
	case TX_MENU_COPY:
		kf_copy(0, this);
		break;
	case TX_MENU_PASTE:
		kf_paste(0, this);
		break;
	case TX_MENU_READ: {
		restore_wrap = wrap;
		set_word_wrap(false);
		readFile();
		break;
	}
	case TX_MENU_WRAP:
		set_word_wrap(!wrap, true);
		break;
	default:
		if (FTextTX::menu[item].flags == 0) // not an FL_SUB_MENU
			add(FTextTX::menu[item].text[0]); add(FTextTX::menu[item].text[1]);
	}
}

/// Overrides some useful Fl_Text_Edit keybindings that we want to keep working,
/// provided that they don't try to change chunks of transmitted text.
///
void FTextTX::change_keybindings(void)
{
	struct {
		Fl_Text_Editor_mod::Key_Func function, override;
	} fbind[] = {
		{ Fl_Text_Editor_mod::kf_default, FTextTX::kf_default },
		{ Fl_Text_Editor_mod::kf_enter,   FTextTX::kf_enter   },
		{ Fl_Text_Editor_mod::kf_delete,  FTextTX::kf_delete  },
		{ Fl_Text_Editor_mod::kf_cut,     FTextTX::kf_cut     },
		{ Fl_Text_Editor_mod::kf_paste,   FTextTX::kf_paste   }
	};
	int n = sizeof(fbind) / sizeof(fbind[0]);

	// walk the keybindings linked list and replace items containing
	// functions for which we have an override in fbind
	for (Fl_Text_Editor_mod::Key_Binding *k = key_bindings; k; k = k->next) {
		for (int i = 0; i < n; i++)
			if (fbind[i].function == k->function)
				k->function = fbind[i].override;
	}
}

// The kf_* functions below call the corresponding Fl_Text_Editor_mod routines, but
// may make adjustments so that no transmitted text is modified.

int FTextTX::kf_default(int c, Fl_Text_Editor_mod* e)
{
	return e->insert_position() < *ptxpos ? 1 : Fl_Text_Editor_mod::kf_default(c, e);
}

int FTextTX::kf_enter(int c, Fl_Text_Editor_mod* e)
{
	return e->insert_position() < *ptxpos ? 1 : Fl_Text_Editor_mod::kf_enter(c, e);
}

int FTextTX::kf_delete(int c, Fl_Text_Editor_mod* e)
{
	// single character
	if (!e->buffer()->selected()) {
                if (e->insert_position() >= *ptxpos &&
                    e->insert_position() != e->buffer()->length())
                        return Fl_Text_Editor_mod::kf_delete(c, e);
                else
                        return 1;
        }

	// region: delete as much as we can
	int start, end;
	e->buffer()->selection_position(&start, &end);
	if (*ptxpos >= end)
		return 1;
	if (*ptxpos > start)
		e->buffer()->select(*ptxpos, end);

	return Fl_Text_Editor_mod::kf_delete(c, e);
}

int FTextTX::kf_cut(int c, Fl_Text_Editor_mod* e)
{
	if (e->buffer()->selected()) {
		int start, end;
		e->buffer()->selection_position(&start, &end);
		if (*ptxpos >= end)
			return 1;
		if (*ptxpos > start)
			e->buffer()->select(*ptxpos, end);
	}

	return Fl_Text_Editor_mod::kf_cut(c, e);
}

int FTextTX::kf_paste(int c, Fl_Text_Editor_mod* e)
{
	return e->insert_position() < *ptxpos ? 1 : Fl_Text_Editor_mod::kf_paste(c, e);
}

// ----------------------------------------------------------------------------


void MVScrollbar::draw(void)
{
	Fl_Scrollbar::draw();

	if (marks.empty() || !draw_marks)
		return;

	assert((type() & FL_HOR_SLIDER) == 0);

	// Calculate the slider knob position and height.  For a vertical scrollbar,
	// the scroll buttons' height is the scrollbar width and the minimum knob
	// height is half that.
	int H = h() - Fl::box_dh(box()) - 2 * w(); // internal height (minus buttons)
	int slider_h = (int)(slider_size() * H + 0.5);
	int min_h = (w() - Fl::box_dw(box())) / 2 + 1;
	if (slider_h < min_h)
		slider_h = min_h;
	double val = (Fl_Slider::value() - minimum()) / (maximum() - minimum());
	int slider_y = (int)(val * (H - slider_h) + 0.5) + w(); // relative to y()
	// This would draw a green rectangle around the slider knob:
	// fl_color(FL_GREEN);
	// fl_rect(x(), y() + slider_y, w() - Fl::box_dw(box()), slider_h);

	int x1 = x() + Fl::box_dx(box()), x2 = x1 + w() - Fl::box_dw(box()) - 1, ypos;
	// Convert stored scrollbar values to vertical positions and draw
	// lines inside the widget if they don't overlap with the knob area.
	for (vector<mark_t>::const_iterator i = marks.begin(); i != marks.end(); ++i) {
		ypos = static_cast<int>(w() + H * i->pos / maximum());
		// Don't draw over slider knob
		if ((ypos > slider_y && ypos < slider_y + slider_h) ||
		    (ypos < slider_y + slider_h && ypos > slider_y))
			continue;
		ypos += y();
		fl_color(i->color);
		fl_line(x1, ypos, x2, ypos);
	}
}
