//+----------------------------------------------------------------------------+ //| Description: Magic Set Editor - Program to make Magic (tm) cards | //| Copyright: (C) 2001 - 2006 Twan van Laarhoven | //| License: GNU General Public License 2 or later (see file COPYING) | //+----------------------------------------------------------------------------+ // ----------------------------------------------------------------------------- : Includes #include #include #include #include #include #include // ----------------------------------------------------------------------------- : TextValueEditorScrollBar /// A scrollbar to scroll a TextValueEditor /** implemented as the scrollbar of a Window because that functions better */ class TextValueEditorScrollBar : public wxWindow { public: TextValueEditorScrollBar(TextValueEditor& tve); private: DECLARE_EVENT_TABLE(); TextValueEditor& tve; void onScroll(wxScrollWinEvent&); void onMotion(wxMouseEvent&); }; TextValueEditorScrollBar::TextValueEditorScrollBar(TextValueEditor& te) : wxWindow(&tve.editor(), wxID_ANY, wxDefaultPosition, wxDefaultSize, wxNO_BORDER | wxVSCROLL | wxALWAYS_SHOW_SB) , tve(te) {} void TextValueEditorScrollBar::onScroll(wxScrollWinEvent& ev) { if (ev.GetOrientation() == wxVERTICAL) { tve.scrollTo(ev.GetPosition()); } } void TextValueEditorScrollBar::onMotion(wxMouseEvent& ev) { tve.editor().SetCursor(*wxSTANDARD_CURSOR); ev.Skip(); } BEGIN_EVENT_TABLE(TextValueEditorScrollBar, wxEvtHandler) EVT_SCROLLWIN (TextValueEditorScrollBar::onScroll) EVT_MOTION (TextValueEditorScrollBar::onMotion) END_EVENT_TABLE () // ----------------------------------------------------------------------------- : TextValueEditor IMPLEMENT_VALUE_EDITOR(Text) , selection_start(0), selection_end(0) , scrollbar(nullptr) {} TextValueEditor::~TextValueEditor() { delete scrollbar; } // ----------------------------------------------------------------------------- : Mouse void TextValueEditor::onLeftDown(const RealPoint& pos, wxMouseEvent& ev) { moveSelection(v.indexAt(style().getRotation().trInv(pos)), !ev.ShiftDown(), MOVE_MID); } void TextValueEditor::onLeftUp(const RealPoint& pos, wxMouseEvent&) { // TODO: lookup position of click? } void TextValueEditor::onMotion(const RealPoint& pos, wxMouseEvent& ev) { if (ev.LeftIsDown()) { moveSelection(v.indexAt(style().getRotation().trInv(pos)), false, MOVE_MID); } } void TextValueEditor::onLeftDClick(const RealPoint& pos, wxMouseEvent& ev) { size_t index = v.indexAt(style().getRotation().trInv(pos)); moveSelection(prevWordBoundry(index), true, MOVE_MID); moveSelection(nextWordBoundry(index), false, MOVE_MID); } void TextValueEditor::onRightDown(const RealPoint& pos, wxMouseEvent& ev) { size_t index = v.indexAt(style().getRotation().trInv(pos)); if (index < min(selection_start, selection_end) || index > max(selection_start, selection_end)) { // only move cursor when outside selection moveSelection(index, !ev.ShiftDown(), MOVE_MID); } } // ----------------------------------------------------------------------------- : Keyboard void TextValueEditor::onChar(wxKeyEvent& ev) { fixSelection(); switch (ev.GetKeyCode()) { case WXK_LEFT: // move left (selection?) if (ev.ControlDown()) { moveSelection(prevWordBoundry(selection_end),!ev.ShiftDown(), MOVE_LEFT); } else { moveSelection(prevCharBoundry(selection_end),!ev.ShiftDown(), MOVE_LEFT); } break; case WXK_RIGHT: // move left (selection?) if (ev.ControlDown()) { moveSelection(nextWordBoundry(selection_end),!ev.ShiftDown(), MOVE_RIGHT); } else { moveSelection(nextCharBoundry(selection_end),!ev.ShiftDown(), MOVE_RIGHT); } break; case WXK_UP: moveSelection(v.moveLine(selection_end, -1), !ev.ShiftDown(), MOVE_LEFT); break; case WXK_DOWN: moveSelection(v.moveLine(selection_end, +1), !ev.ShiftDown(), MOVE_RIGHT); break; case WXK_HOME: // move to begining of line / all (if control) if (ev.ControlDown()) { moveSelection(0, !ev.ShiftDown(), MOVE_LEFT); } else { moveSelection(v.lineStart(selection_end), !ev.ShiftDown(), MOVE_LEFT); } break; case WXK_END: // move to end of line / all (if control) if (ev.ControlDown()) { moveSelection(value().value().size(), !ev.ShiftDown(), MOVE_RIGHT); } else { moveSelection(v.lineEnd(selection_end), !ev.ShiftDown(), MOVE_RIGHT); } break; case WXK_BACK: if (selection_start == selection_end) { // if no selection, select previous character moveSelectionNoRedraw(prevCharBoundry(selection_end), false); if (selection_start == selection_end) { // Walk over a as if we are the LEFT key moveSelection(prevCharBoundry(selection_end), true, MOVE_LEFT); return; } } replaceSelection(wxEmptyString, _("Backspace")); break; case WXK_DELETE: if (selection_start == selection_end) { // if no selection select next moveSelectionNoRedraw(nextCharBoundry(selection_end), false); if (selection_start == selection_end) { // Walk over a as if we are the RIGHT key moveSelection(nextCharBoundry(selection_end), true, MOVE_RIGHT); } } replaceSelection(wxEmptyString, _("Delete")); break; case WXK_RETURN: if (field().multi_line) { replaceSelection(_("\n"), _("Enter")); } break; default: if (ev.GetKeyCode() >= _(' ') && ev.GetKeyCode() == (int)ev.GetRawKeyCode()) { // TODO: Find a more correct way to determine normal characters, // this might not work for internationalized input. // It might also not be portable! replaceSelection(String(ev.GetUnicodeKey(), 1), _("Typing")); } } } // ----------------------------------------------------------------------------- : Other events void TextValueEditor::onFocus() { showCaret(); } void TextValueEditor::onLoseFocus() { // hide caret wxCaret* caret = editor().GetCaret(); assert(caret); if (caret->IsVisible()) caret->Hide(); // hide selection selection_start = selection_end = 0; } bool TextValueEditor::onContextMenu(wxMenu& m, wxContextMenuEvent& ev) { // in a keword? => "reminder text" option size_t kwpos = in_tag(value().value(), _("IsSupported(wxDF_TEXT); } bool TextValueEditor::canCopy() const { return selection_start != selection_end; // text is selected } bool TextValueEditor::doPaste() { // get data if (!wxTheClipboard->Open()) return false; wxTextDataObject data; bool ok = wxTheClipboard->GetData(data); wxTheClipboard->Close(); if (!ok) return false; // paste replaceSelection(escape(data.GetText()), _("Paste")); return true; } bool TextValueEditor::doCopy() { // determine string to store if (selection_start > value().value().size()) selection_start = value().value().size(); if (selection_end > value().value().size()) selection_end = value().value().size(); size_t start = min(selection_start, selection_end); size_t end = max(selection_start, selection_end); String str = untag(value().value().substr(start, end - start)); if (str.empty()) return false; // no data to copy // set data if (!wxTheClipboard->Open()) return false; bool ok = wxTheClipboard->SetData(new wxTextDataObject(str)); wxTheClipboard->Close(); return ok; } bool TextValueEditor::doDelete() { replaceSelection(wxEmptyString, _("Cut")); return true; } // ----------------------------------------------------------------------------- : Formatting bool TextValueEditor::canFormat(int type) const { switch (type) { case ID_FORMAT_BOLD: case ID_FORMAT_ITALIC: return !style().always_symbol && style().allow_formating; case ID_FORMAT_SYMBOL: return !style().always_symbol && style().allow_formating && style().symbol_font.valid(); case ID_FORMAT_REMINDER: return false; // TODO default: return false; } } bool TextValueEditor::hasFormat(int type) const { switch (type) { case ID_FORMAT_BOLD: return in_tag(value().value(), _(" // it is not 0 for empty text, because TextRenderer handles that case if (cursor.height == 0) { if (style().always_symbol && style().symbol_font.valid()) { RealSize s = style().symbol_font.font->defaultSymbolSize(viewer.getContext(), rot.trS(1)); cursor.height = s.height; } else { cursor.height = v.heightOfLastLine(); if (cursor.height == 0) { wxClientDC dc(&editor()); // TODO : high quality? dc.SetFont(style().font.font); int hi; dc.GetTextExtent(_(" "), 0, &hi); cursor.height = rot.trS(hi); } } } // clip caret pos and size; show caret if (nativeLook()) { if (cursor.y + cursor.height <= 0 || cursor.y >= style().height) { // caret should be hidden if (caret->IsVisible()) caret->Hide(); return; } else if (cursor.y < 0) { // caret partially hidden, clip cursor.height -= -cursor.y; cursor.y = 0; } else if (cursor.y + cursor.height >= style().height) { // caret partially hidden, clip cursor.height = style().height - cursor.y; } } // rotate cursor = rot.tr(cursor); // set size wxSize size = cursor.size(); size.SetWidth (max(1, size.GetWidth())); size.SetHeight(max(1, size.GetHeight())); // resize, move, show if (size != caret->GetSize()) { caret->SetSize(size); } caret->Move(cursor.position()); if (!caret->IsVisible()) caret->Show(); } void TextValueEditor::replaceSelection(const String& replacement, const String& name) { if (replacement.empty() && selection_start == selection_end) { // no text selected, nothing to delete return; } // fix the selection, it may be changed by undo/redo if (selection_end < selection_start) swap(selection_end, selection_start); fixSelection(); // execute the action before adding it to the stack, // because we want to run scripts before action listeners see the action ValueAction* action = typing_action(valueP(), selection_start, selection_end, replacement, name); if (!action) { // nothing changed, but move the selection anyway moveSelection(selection_start); return; } getSet().actions.add(action); // move cursor if (field().move_cursor_with_sort && replacement.size() == 1) { String val = value().value(); Char typed = replacement.GetChar(0); Char typedU = toUpper(typed); Char cur = val.GetChar(selection_start); // the cursor may have moved because of sorting... // is 'replacement' just after the current cursor? if (selection_start >= 0 && selection_start < val.size() && (cur == typed || cur == typedU)) { // no need to move cursor in a special way selection_end = selection_start = min(selection_end, selection_start) + 1; } else { // find the last occurence of 'replacement' in the value size_t pos = val.find_last_of(typed); if (pos == String::npos) { // try upper case pos = val.find_last_of(typedU); } if (pos != String::npos) { selection_end = selection_start = pos + 1; } else { selection_end = selection_start; } } } else { selection_end = selection_start = min(selection_end, selection_start) + replacement.size(); } // scroll with next update // scrollWithCursor = true; } void TextValueEditor::moveSelection(size_t new_end, bool also_move_start, Movement dir) { if (!isCurrent()) { // selection is only visible for curent editor, we can do a move the simple way moveSelectionNoRedraw(new_end, also_move_start, dir); return; } // First redraw selection wxCaret* caret = editor().GetCaret(); if (caret->IsVisible()) caret->Hide(); { /* DCP dc = editor.overdrawDC(); RotatedDC rdc(*dc, editor.rotation); if (nativeLook) { // clip the dc to the region of this control rdc.SetClippingRegion(style->left, style->top, style->width, style->height); } // clear old v.drawSelection(rdc, style(), selection_start, selection_end); // move */ moveSelectionNoRedraw(new_end, also_move_start, dir); // scroll? // scrollWithCursor = true; // if (onMove()) { // // we can't redraw just the selection because we must scroll // updateScrollbar(); // editor.refreshEditor(); // } else { // // draw new selection // v.drawSelection(rdc, style(), selection_start, selection_end); // } } showCaret(); } void TextValueEditor::moveSelectionNoRedraw(size_t new_end, bool also_move_start, Movement dir) { selection_end = new_end; if (also_move_start) selection_start = selection_end; fixSelection(dir); } void TextValueEditor::fixSelection(Movement dir) { const String& val = value().value(); // value may have become smaller because of undo/redo // make sure the selection stays inside the text size_t size = val.size(); selection_end = min(size, selection_end); selection_start = min(size, selection_start); // start and end must be on the same side of separators size_t seppos = val.find(_(" seppos) selection_end = seppos; // not on same side if (selection_start >= sepend && selection_end < sepend) selection_end = sepend; // not on same side if (selection_start > seppos && selection_start < sepend) { // start inside separator selection_start = move(selection_start, seppos, sepend, dir); } if (selection_end > seppos && selection_end < sepend) { // end inside separator selection_end = selection_start < sepend ? seppos : sepend; } // find next separator seppos = val.find(_("? if so, move them out size_t atompos = val.find(_(" atompos && selection_start < atomend) { // start inside atom selection_start = move(selection_start, atompos, atomend, dir); } if (selection_end > atompos && selection_end < atomend) { // end inside atom selection_end = move(selection_end, atompos, atomend, dir); } // find next atom atompos = val.find(_(" start + end) return end; // past the middle else return start; } // ----------------------------------------------------------------------------- : Native look / scrollbar void TextValueEditor::determineSize() { if (!nativeLook()) return; style().angle = 0; // no rotation in nativeLook if (scrollbar) { // muliline, determine scrollbar size style().height = 100; int sbw = wxSystemSettings::GetMetric(wxSYS_VSCROLL_X); scrollbar->SetSize( style().left + style().width - sbw + 1, style().top - 1, sbw, style().height + 2); // r.reset(); } else { // Height depends on font wxMemoryDC dc; Bitmap bmp(1,1); dc.SelectObject(bmp); dc.SetFont(style().font.font); style().height = dc.GetCharHeight() + 2; } } void TextValueEditor::onShow(bool showing) { if (scrollbar) { // show/hide our scrollbar scrollbar->Show(showing); } } void TextValueEditor::onMouseWheel(const RealPoint& pos, wxMouseEvent& ev) { if (scrollbar) { int toScroll = ev.GetWheelRotation() * ev.GetLinesPerAction() / ev.GetWheelDelta(); // note: up is positive int target = min(max(scrollbar->GetScrollPos(wxVERTICAL) - toScroll, 0), scrollbar->GetScrollRange(wxVERTICAL) - scrollbar->GetScrollThumb(wxVERTICAL)); scrollTo(target); } } void TextValueEditor::scrollTo(int pos) { // scroll // r.scrollTo(pos); // move the cursor if needed // refresh // editor.refreshEditor(); }