// ---------------------------------------------------------------------------- // 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 . // ---------------------------------------------------------------------------- #include #include #include #include #include #include #include #include #include #include #include #include #include #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 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)->show_marks(b); } void FTextRX::mark(FTextBase::TEXT_ATTR attr) { if (attr == NATTR) attr = CLICK_START; static_cast(mVScrollBar)->mark(styles[attr].color); } void FTextRX::clear(void) { FTextBase::clear(); s_text.clear(); s_style.clear(); static_cast(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 qrg_marks; map::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); } 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; } else if (count_if(s, s + strlen(s), static_cast(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)->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)->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& 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(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 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(0), this); REQ(FTextTX::changed_cb, utf8_txpos+1, 0, 0, -1, static_cast(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 for SC-c case 'm': // add for SC-m case 'n': // add for SC-n case 'r': // add for SC-r case 'l': // add for SC-l case 'h': // add for SC-h case 'a': // add 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 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::const_iterator i = marks.begin(); i != marks.end(); ++i) { ypos = static_cast(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); } }