diff --git a/src/data/action/value.cpp b/src/data/action/value.cpp index f5d053ec..175fea79 100644 --- a/src/data/action/value.cpp +++ b/src/data/action/value.cpp @@ -14,6 +14,7 @@ #include #include #include +#include // ----------------------------------------------------------------------------- : ValueAction @@ -35,16 +36,14 @@ class SimpleValueAction : public ValueAction { swap(static_cast(*valueP).*member, new_value); } - virtual bool merge(const Action* action) { + virtual bool merge(const Action& action) { if (!ALLOW_MERGE) return false; - if (const SimpleValueAction* sva = dynamic_cast(action)) { - if (sva->valueP == valueP) { + TYPE_CASE(action, SimpleValueAction) { + if (action.valueP == valueP) { // adjacent actions on the same value, discard the other one, // because it only keeps an intermediate value return true; } - } else { - return false; } return false; } @@ -60,3 +59,85 @@ ValueAction* value_action(const SymbolValueP& value, const FileName& // ----------------------------------------------------------------------------- : Text + +TextValueAction::TextValueAction(const TextValueP& value, size_t start, size_t end, size_t new_end, const Defaultable& new_value, const String& name) + : ValueAction(value), new_value(new_value), name(name) + , selection_start(start), selection_end(end), new_selection_end(new_end) +{} + +String TextValueAction::getName(bool to_undo) const { return name; } + +void TextValueAction::perform(bool to_undo) { + swap(value().value, new_value); +// if (value().value.age < new_value.age) value().value.age = Age(); + swap(selection_end, new_selection_end); +} + +bool TextValueAction::merge(const Action& action) { + TYPE_CASE(action, TextValueAction) { + if (&action.value() == &value() && action.name == name && action.selection_start == selection_end) { + // adjacent edits, keep old value of this, it is older + selection_end = action.selection_end; + return true; + } + } + return false; +} + +TextValue& TextValueAction::value() const { + return static_cast(*valueP); +} + + +TextValueAction* toggle_format_action(const TextValueP& value, const String& tag, size_t start, size_t end, const String& action_name) { + if (start > end) swap(start, end); + String new_value; + const String& str = value->value(); + // Are we inside the tag we are toggling? + size_t tagpos = in_tag(str, _("<") + tag, start, end); + if (tagpos == String::npos) { + // we are not inside this tag, add it + new_value = str.substr(0, start); + new_value += _("<") + tag + _(">"); + new_value += str.substr(start, end - start); + new_value += _(""); + new_value += str.substr(end); + } else { + // we are inside this tag, _('remove') it + new_value = str.substr(0, start); + new_value += _(""); + new_value += str.substr(start, end - start); + new_value += _("<") + tag + _(">"); + new_value += str.substr(end); + } + // Build action + if (start != end) { + // don't simplify if start == end, this way we insert , allowing the + // user to press Ctrl+B and start typing bold text + new_value = simplify_tagged(new_value); + } + if (value->value() == new_value) { + return nullptr; // no changes + } else { + return new TextValueAction(value, start, end, end, new_value, action_name); + } +} + +TextValueAction* typing_action(const TextValueP& value, size_t start, size_t end, const String& replacement, const String& action_name) { + bool reverse = start > end; + if (reverse) swap(start, end); + String new_value = tagged_substr_replace(value->value(), start, end, replacement); + if (value->value() == new_value) { + // no change + return nullptr; + } else { +// if (name == _("Backspace")) { +// // HACK: put start after end + if (reverse) { + return new TextValueAction(value, end, start, start+replacement.size(), new_value, action_name); + } else { + return new TextValueAction(value, start, end, start+replacement.size(), new_value, action_name); + } + } +} + diff --git a/src/data/action/value.hpp b/src/data/action/value.hpp index 10b87ef5..426fc62b 100644 --- a/src/data/action/value.hpp +++ b/src/data/action/value.hpp @@ -37,18 +37,9 @@ class ValueAction : public Action { const ValueP valueP; ///< The modified value }; -/// Utility macro for declaring classes derived from ValueAction -#define DECLARE_VALUE_ACTION(Type) \ - protected: \ - inline Type##Value& value() const { \ - return static_cast(*valueP); \ - } \ - public: \ - virtual void perform(bool to_undo) - - // ----------------------------------------------------------------------------- : Simple +/// Action that updates a Value to a new value ValueAction* value_action(const ChoiceValueP& value, const Defaultable& new_value); ValueAction* value_action(const ColorValueP& value, const Defaultable& new_value); ValueAction* value_action(const ImageValueP& value, const FileName& new_value); @@ -56,17 +47,30 @@ ValueAction* value_action(const SymbolValueP& value, const FileName& // ----------------------------------------------------------------------------- : Text -/* -class ColorValueAction : public ValueAction { +/// An action that changes a TextValue +class TextValueAction : public ValueAction { public: - ColorValueAction(const ColorValueP& value, const Defaultable& color); + TextValueAction(const TextValueP& value, size_t start, size_t end, size_t new_end, const Defaultable& new_value, const String& name); - DECLARE_VALUE_ACTION(Color); + virtual String getName(bool to_undo) const; + virtual void perform(bool to_undo); + virtual bool merge(const Action& action); + /// The modified selection + size_t selection_start, selection_end; private: - Defaultable color; ///< The new/old color + inline TextValue& value() const; + + size_t new_selection_end; + Defaultable new_value; + String name; }; -*/ + +/// Action for toggleing some formating tag on or off in some range +TextValueAction* toggle_format_action(const TextValueP& value, const String& tag, size_t start, size_t end, const String& action_name); + +/// Typing in a TextValue, replace the selection [start...end) with replacement +TextValueAction* typing_action(const TextValueP& value, size_t start, size_t end, const String& replacement, const String& action_name); // ----------------------------------------------------------------------------- : EOF #endif diff --git a/src/data/field/text.hpp b/src/data/field/text.hpp index 2864f1fa..52a069d0 100644 --- a/src/data/field/text.hpp +++ b/src/data/field/text.hpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -68,6 +69,11 @@ class TextStyle : public Style { virtual bool update(Context&); virtual void initDependencies(Context&, const Dependency&) const; + /// The rotation to use when drawing + inline Rotation getRotation() const { + return Rotation(angle, getRect()); + } + private: DECLARE_REFLECTION(); }; diff --git a/src/data/symbol_font.hpp b/src/data/symbol_font.hpp index e722c8ea..03825a90 100644 --- a/src/data/symbol_font.hpp +++ b/src/data/symbol_font.hpp @@ -76,6 +76,7 @@ class SymbolFont : public Packaged { /// Size of a single symbol RealSize symbolSize (Context& ctx, double font_size, const DrawableSymbol& sym); + public: /// Size of the default symbol RealSize defaultSymbolSize(Context& ctx, double font_size); diff --git a/src/gui/control/card_editor.cpp b/src/gui/control/card_editor.cpp index 10cf03de..3d1b7b94 100644 --- a/src/gui/control/card_editor.cpp +++ b/src/gui/control/card_editor.cpp @@ -12,6 +12,7 @@ #include #include #include +#include DECLARE_TYPEOF_COLLECTION(ValueViewerP); DECLARE_TYPEOF_COLLECTION(ValueViewer*); @@ -22,7 +23,10 @@ DataEditor::DataEditor(Window* parent, int id, long style) : CardViewer(parent, id, style) , current_viewer(nullptr) , current_editor(nullptr) -{} +{ + // Create a caret + SetCaret(new wxCaret(this,1,1)); +} ValueViewerP DataEditor::makeViewer(const StyleP& style) { return style->makeEditor(*this, style); diff --git a/src/gui/value/text.cpp b/src/gui/value/text.cpp index 620b2841..281c3f4b 100644 --- a/src/gui/value/text.cpp +++ b/src/gui/value/text.cpp @@ -7,7 +7,597 @@ // ----------------------------------------------------------------------------- : Includes #include +#include +#include +#include +#include +#include -// ----------------------------------------------------------------------------- : +// ----------------------------------------------------------------------------- : TextValueEditorScrollBar -IMPLEMENT_VALUE_EDITOR(Text) {} +/// 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; + 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(); +} \ No newline at end of file diff --git a/src/gui/value/text.hpp b/src/gui/value/text.hpp index d4510acb..d90d41ff 100644 --- a/src/gui/value/text.hpp +++ b/src/gui/value/text.hpp @@ -13,14 +13,116 @@ #include #include +class TextValueEditorScrollBar; + // ----------------------------------------------------------------------------- : TextValueEditor +/// Directions of cursor movement +enum Movement +{ MOVE_LEFT ///< Always move the cursor to the left +, MOVE_MID ///< Move in whichever direction the distance to move is shorter (TODO: define shorter) +, MOVE_RIGHT ///< Always move the cursor to the right +}; + /// An editor 'control' for editing TextValues +/** Okay, this class responds to pretty much every event available... :) + */ class TextValueEditor : public TextValueViewer, public ValueEditor { public: DECLARE_VALUE_EDITOR(Text); + ~TextValueEditor(); -// virtual void determineSize(); + // --------------------------------------------------- : Events + + virtual void onFocus(); + virtual void onLoseFocus(); + + virtual void onLeftDown (const RealPoint& pos, wxMouseEvent&); + virtual void onLeftUp (const RealPoint& pos, wxMouseEvent&); + virtual void onLeftDClick(const RealPoint& pos, wxMouseEvent&); + virtual void onRightDown (const RealPoint& pos, wxMouseEvent&); + virtual void onMotion (const RealPoint& pos, wxMouseEvent&); + virtual void onMouseWheel(const RealPoint& pos, wxMouseEvent& ev); + + virtual bool onContextMenu(wxMenu& m, wxContextMenuEvent&); + virtual void onMenu(wxCommandEvent&); + + virtual void onChar(wxKeyEvent&); + + // --------------------------------------------------- : Actions + + virtual void onValueChange(); + virtual void onAction(const ValueAction&, bool undone); + + // --------------------------------------------------- : Clipboard + + virtual bool canCopy() const; + virtual bool canPaste() const; + virtual bool doCopy(); + virtual bool doPaste(); + virtual bool doDelete(); + + // --------------------------------------------------- : Formating + + virtual bool canFormat(int type) const; + virtual bool hasFormat(int type) const; + virtual void doFormat(int type); + + // --------------------------------------------------- : Selection + + virtual void select(size_t start, size_t end); + virtual size_t selectionStart() const { return selection_start; } + virtual size_t selectionEnd() const { return selection_end; } + + // --------------------------------------------------- : Other + + virtual wxCursor cursor() const; + virtual void determineSize(); + virtual void onShow(bool); + + // --------------------------------------------------- : Data + private: + size_t selection_start, selection_end; ///< Cursor position/selection (if any) + TextValueEditorScrollBar* scrollbar; ///< Scrollbar for multiline fields in native look + + // --------------------------------------------------- : Selection / movement + + /// Move the selection to a new location, clears the previously drawn selection + void moveSelection(size_t new_end, bool also_move_start=true, Movement dir = MOVE_MID); + /// Move the selection to a new location, but does not redraw + void moveSelectionNoRedraw(size_t new_end, bool also_move_start=true, Movement dir = MOVE_MID); + + /// Replace the current selection with 'replacement', name the action + void replaceSelection(const String& replacement, const String& name); + + /// Make sure the selection satisfies its constraints + /** - selection_start and selection_end are inside the text + * - not inside tags + * - the selection does not contain a or tag + * + * When correcting the selection, move in the given direction + */ + void fixSelection(Movement dir = MOVE_MID); + + /// Return a position resulting from moving pos outside the range [start...end), in the direction dir + static size_t move(size_t pos, size_t start, size_t end, Movement dir); + + /// Move the caret to the selection_end position and show it + void showCaret(); + + /// Position of previous visible & selectable character + size_t prevCharBoundry(size_t pos) const; + size_t nextCharBoundry(size_t pos) const; + /// Front of previous word, used witch Ctrl+Left/right + size_t prevWordBoundry(size_t pos) const; + size_t nextWordBoundry(size_t pos) const; + + // --------------------------------------------------- : Scrolling + + friend class TextValueEditorScrollBar; + + /// Scroll to the given position, called by scrollbar + void scrollTo(int pos); }; // ----------------------------------------------------------------------------- : EOF diff --git a/src/render/text/viewer.cpp b/src/render/text/viewer.cpp index ba223dd2..6e2a4930 100644 --- a/src/render/text/viewer.cpp +++ b/src/render/text/viewer.cpp @@ -32,7 +32,7 @@ struct TextViewer::Line { /// Index just beyond the last character on this line size_t end() const { return start + positions.size() - 1; } /// Find the index of the character at the given position on this line - /** Always returns a value in the range [start..end()) */ + /** Always returns a value in the range [start..end()] */ size_t posToIndex(double x) const; /// Is this line visible using the given rectangle? @@ -47,13 +47,12 @@ struct TextViewer::Line { size_t TextViewer::Line::posToIndex(double x) const { // largest index with pos <= x - vector::const_iterator it1 = lower_bound(positions.begin(), positions.end(), x); - if (it1 == positions.end()) return end(); + vector::const_iterator it2 = lower_bound(positions.begin(), positions.end(), x); + if (it2 == positions.begin()) return start; // first index with pos > x - vector::const_iterator it2 = it1 + 1; - if (it2 == positions.end()) return it1 - positions.begin(); - if (x - *it1 <= *it2 - x) return it1 - positions.begin(); // it1 is closer - else return it2 - positions.begin(); // it2 is closer + vector::const_iterator it1 = it2 - 1; + if (x - *it1 <= *it2 - x) return it1 - positions.begin() + start; // it1 is closer + else return it2 - positions.begin() + start; // it2 is closer } // ----------------------------------------------------------------------------- : TextViewer @@ -65,7 +64,7 @@ TextViewer::~TextViewer() {} // ----------------------------------------------------------------------------- : Drawing void TextViewer::draw(RotatedDC& dc, const String& text, const TextStyle& style, Context& ctx, DrawWhat what) { - Rotater r(dc, Rotation(style.angle, style.getRect())); + Rotater r(dc, style.getRotation()); if (lines.empty()) { // not prepared yet prepareElements(text, style, ctx); @@ -81,7 +80,7 @@ void TextViewer::draw(RotatedDC& dc, const String& text, const TextStyle& style, } void TextViewer::drawSelection(RotatedDC& dc, const TextStyle& style, size_t sel_start, size_t sel_end) { - Rotater r(dc, Rotation(style.angle, style.getRect())); + Rotater r(dc, style.getRotation()); if (sel_start == sel_end) return; if (sel_end < sel_start) swap(sel_start, sel_end); dc.SetBrush(*wxBLACK_BRUSH); @@ -109,6 +108,25 @@ void TextViewer::reset() { // ----------------------------------------------------------------------------- : Positions +const TextViewer::Line& TextViewer::findLine(size_t index) const { + FOR_EACH_CONST(l, lines) { + if (l.end() > index) return l; + } + return lines.front(); +} + +size_t TextViewer::moveLine(size_t index, int delta) const { + const Line* line1 = &findLine(index); + const Line* line2 = line1 + delta; + if (line2 >= &lines.front() && line2 <= &lines.back()) { + size_t idx = index - line1->start; + if (idx < 0 || idx >= line1->positions.size()) return index; // can't move + return line2->posToIndex(line1->positions[idx]); // character at the same position + } else { + return index; // can't move + } +} + size_t TextViewer::lineStart(size_t index) const { if (lines.empty()) return 0; return findLine(index).start; @@ -119,11 +137,32 @@ size_t TextViewer::lineEnd(size_t index) const { return findLine(index).end(); } -const TextViewer::Line& TextViewer::findLine(size_t index) const { - FOR_EACH_CONST(l, lines) { - if (l.end() > index) return l; +struct CompareTop { + inline bool operator () (const TextViewer::Line& l, double y) const { return l.top < y; } + inline bool operator () (double y, const TextViewer::Line& l) const { return y < l.top; } +}; +size_t TextViewer::indexAt(const RealPoint& pos) const { + // 1. find the line + vector::const_iterator l = lower_bound(lines.begin(), lines.end(), pos.y, CompareTop()); + if (l != lines.begin()) l--; + assert(l != lines.end()); + // 2. find char on line + return l->posToIndex(pos.x); +} + +RealRect TextViewer::charRect(size_t index) const { + const Line& l = findLine(index); + size_t pos = index - l.start; + if (pos >= l.positions.size()) { + return RealRect(l.positions.back(), l.top, 0, l.line_height); + } else { + return RealRect(l.positions[pos], l.top, l.positions[pos + 1] - l.positions[pos], l.line_height); } - return lines.front(); +} + +double TextViewer::heightOfLastLine() const { + if (lines.empty()) return 0; + else return lines.back().line_height; } // ----------------------------------------------------------------------------- : Elements diff --git a/src/render/text/viewer.hpp b/src/render/text/viewer.hpp index 20cf72aa..b6c91cf3 100644 --- a/src/render/text/viewer.hpp +++ b/src/render/text/viewer.hpp @@ -56,16 +56,32 @@ class TextViewer { // --------------------------------------------------- : Positions - /// Find the character index that is before the given index, and which has a nonzero width - size_t moveLeft(size_t index) const; + /// Find the character index that is before/after the given index, and which has a nonzero width +// size_t moveChar(size_t index, int delta) const; /// Find the character index that is on a line above/below index /** If this would move outisde the text, returns the input index */ size_t moveLine(size_t index, int delta) const; + /// The character index of the start of the line that character #index is on size_t lineStart(size_t index) const; /// The character index past the end of the line that character #index is on size_t lineEnd (size_t index) const; + /// Find the index of the character at the given position + /** If the position is before everything returns 0, + * if it is after everything returns text.size(). + * The position is in internal coordinates */ + size_t indexAt(const RealPoint& pos) const; + /// Find the position of the character at the given index + /** The position is in internal coordinates */ + RealPoint posOf(size_t index) const; + + /// Return the rectangle around a single character + RealRect charRect(size_t index) const; + + /// Return the height of the last line + double heightOfLastLine() const; + private: // --------------------------------------------------- : More drawing double scale; /// < Scale when drawing diff --git a/src/render/value/text.hpp b/src/render/value/text.hpp index f29792b9..55d708f0 100644 --- a/src/render/value/text.hpp +++ b/src/render/value/text.hpp @@ -25,7 +25,7 @@ class TextValueViewer : public ValueViewer { virtual void onValueChange(); virtual void onStyleChange(); - private: + protected: TextViewer v; }; diff --git a/src/render/value/viewer.cpp b/src/render/value/viewer.cpp index 6fb79f65..fe9585a6 100644 --- a/src/render/value/viewer.cpp +++ b/src/render/value/viewer.cpp @@ -41,7 +41,7 @@ RealRect ValueViewer::boundingBox() const { void ValueViewer::drawFieldBorder(RotatedDC& dc) { if (viewer.drawBorders() && getField()->editable) { - dc.SetPen(viewer.borderPen(viewer.focusedViewer() == this)); + dc.SetPen(viewer.borderPen(isCurrent())); dc.SetBrush(*wxTRANSPARENT_BRUSH); dc.DrawRectangle(styleP->getRect().grow(dc.trInvS(1))); } @@ -50,6 +50,9 @@ void ValueViewer::drawFieldBorder(RotatedDC& dc) { bool ValueViewer::nativeLook() const { return viewer.nativeLook(); } +bool ValueViewer::isCurrent() const { + return viewer.focusedViewer() == this; +} // ----------------------------------------------------------------------------- : Type dispatch diff --git a/src/util/action_stack.cpp b/src/util/action_stack.cpp index 484f67d2..d4248446 100644 --- a/src/util/action_stack.cpp +++ b/src/util/action_stack.cpp @@ -33,7 +33,7 @@ void ActionStack::add(Action* action, bool allow_merge) { FOR_EACH(a, redo_actions) delete a; redo_actions.clear(); // try to merge? - if (allow_merge && !undo_actions.empty() && undo_actions.back()->merge(action)) { + if (allow_merge && !undo_actions.empty() && undo_actions.back()->merge(*action)) { // merged with top undo action delete action; } else { diff --git a/src/util/action_stack.hpp b/src/util/action_stack.hpp index 3fd54b12..d4f8c62e 100644 --- a/src/util/action_stack.hpp +++ b/src/util/action_stack.hpp @@ -40,7 +40,7 @@ class Action { /** Either: return false and do nothing * Or: return true and change this action to incorporate both actions */ - virtual bool merge(const Action* action) { return false; } + virtual bool merge(const Action& action) { return false; } }; // ----------------------------------------------------------------------------- : Action listeners diff --git a/src/util/rotation.hpp b/src/util/rotation.hpp index 7b9c18c1..27a30409 100644 --- a/src/util/rotation.hpp +++ b/src/util/rotation.hpp @@ -73,9 +73,12 @@ class Rotation { friend class Rotater; + public: /// Is the rotation sideways (90 or 270 degrees)? // Note: angle & 2 == 0 for angle in {0, 180} and != 0 for angle in {90, 270) inline bool sideways() const { return (angle & 2) != 0; } + + protected: /// Is the x axis 'reversed' (after turning sideways)? inline bool revX() const { return angle >= 180; } /// Is the y axis 'reversed' (after turning sideways)? diff --git a/src/util/string.cpp b/src/util/string.cpp index 668987b0..48cba79e 100644 --- a/src/util/string.cpp +++ b/src/util/string.cpp @@ -62,6 +62,10 @@ String trim_left(const String& s) { } } +String substr_replace(const String& input, size_t start, size_t end, const String& replacement) { + return input.substr(0,start) + replacement + input.substr(end); +} + // ----------------------------------------------------------------------------- : Words String last_word(const String& s) { diff --git a/src/util/string.hpp b/src/util/string.hpp index 5c80470d..7a602615 100644 --- a/src/util/string.hpp +++ b/src/util/string.hpp @@ -81,6 +81,9 @@ String trim(const String&); /// Remove whitespace from the start of a string String trim_left(const String&); +/// Replace the substring [start...end) of 'input' with 'replacement' +String substr_replace(const String& input, size_t start, size_t end, const String& replacement); + // ----------------------------------------------------------------------------- : Words /// Returns the last word in a string diff --git a/src/util/tagged_string.cpp b/src/util/tagged_string.cpp index 9f39fd37..de79b925 100644 --- a/src/util/tagged_string.cpp +++ b/src/util/tagged_string.cpp @@ -7,6 +7,7 @@ // ----------------------------------------------------------------------------- : Includes #include +#include // ----------------------------------------------------------------------------- : Conversion to/from normal string @@ -41,6 +42,44 @@ String escape(const String& str) { return ret; } +String fix_old_tags(const String& str) { + String ret; ret.reserve(str.size()); + stack tags; + bool intag = false; + // invariant : intag => !tags.empty() + for (size_t i = 0 ; i < str.size() ; ++i) { + Char c = str.GetChar(i); + if (is_substr(str, i, _(""))) { + i += 2; + // old style close tag, replace by the correct tag type + if (!tags.empty()) { + // need a close tag? + if (!tags.top().empty()) { + ret += _(""); + } + tags.pop(); + } + intag = false; + } else { + ret += c; + if (c==_('<')) { + intag = true; + tags.push(wxEmptyString); + } else if (c==_('>') && intag) { + intag = false; + if (!starts_with(tags.top(), _("kw")) && !starts_with(tags.top(), _("atom"))) { + // only keep keyword related stuff + ret.resize(ret.size() - tags.top().size() - 2); // remove from output + tags.top() = wxEmptyString; + } + } else if (intag) { + tags.top() += c; + } + } + } + return ret; +} + // ----------------------------------------------------------------------------- : Finding tags size_t skip_tag(const String& str, size_t start) { @@ -49,5 +88,179 @@ size_t skip_tag(const String& str, size_t start) { return end == String::npos ? String::npos : end + 1; } +size_t match_close_tag(const String& str, size_t start) { + String tag = tag_type_at(str, start); + String ctag = _("/") + tag; + size_t size = str.size(); + int taglevel = 1; + for (size_t pos = start + tag.size() + 2 ; pos < size ; ++pos) { + Char c = str.GetChar(pos); + if (c == _('<')) { + if (is_substr(str, pos + 1, tag)) { + ++taglevel; + pos += tag.size() + 1; + } else if (is_substr(str, pos + 1, ctag)) { + --taglevel; // close tag + if (taglevel == 0) return pos; + pos += ctag.size() + 1; + } + } + } + return String::npos; +} + +size_t last_start_tag_before(const String& str, const String& tag, size_t start) { + start = min(str.size(), start); + for (size_t pos = start ; pos > 0 ; --pos) { + if (is_substr(str, pos - 1, tag)) { + return pos - 1; + } + } + return String::npos; +} + +size_t in_tag(const String& str, const String& tag, size_t start, size_t end) { + if (start > end) swap(start, end); + size_t pos = last_start_tag_before(str, tag, start); + if (pos == String::npos) return String::npos; // no tag found before start + size_t posE = match_close_tag(str, pos); + if (posE < end) return String::npos; // the tag ends before end + return pos; +} + + +String tag_at(const String& str, size_t pos) { + size_t end = str.find_first_of(_(">"), pos); + if (end == String::npos) return wxEmptyString; + return str.substr(pos + 1, end - pos - 1); +} + +String tag_type_at(const String& str, size_t pos) { + size_t end = str.find_first_of(_(">-"), pos); + if (end == String::npos) return wxEmptyString; + return str.substr(pos + 1, end - pos - 1); +} + +String close_tag(const String& tag) { + if (tag.size() < 1) return _(""); + else return _(""); + else return _(""); +} + // ----------------------------------------------------------------------------- : Global operations +// ----------------------------------------------------------------------------- : Updates + +/// Return all open or close tags in the given range from a string +/** for example: + * if close_tags == false, "texttexttext" --> "" + * if close_tags == true, "texttexttext" --> "" + */ +String get_tags(const String& str, size_t start, size_t end, bool close_tags) { + String ret; + bool intag = false; + bool keeptag = false; + for (size_t i = start ; i < end ; ++i) { + Char c = str.GetChar(i); + if (c == _('<') && !intag) { + intag = true; + // is this tag an open tag? + if (i + 1 < end && (str.GetChar(i + 1) == _('/')) == close_tags) { + keeptag = true; + } + } + if (intag && keeptag) ret += c; + if (c == _('>')) intag = false; + } + return ret; +} + +String tagged_substr_replace(const String& input, size_t start, size_t end, const String& replacement) { + assert(start <= end); + size_t size = input.size(); + String ret; ret.reserve(size + replacement.size() - (end - start)); // estimated size + return simplify_tagged( + substr_replace(input, start, end, + get_tags(input, start, end, true) + // close tags + escape(replacement) + + get_tags(input, start, end, false) // open tags + )); +} + + // ----------------------------------------------------------------------------- : Simplification + +String simplify_tagged(const String& str) { + return simplify_tagged_overlap(simplify_tagged_merge(str)); +} + +// Add a tag to a stack of tags, try to cancel it out +// If is in stack remove it and returns true +// otherwise appends and returns fales +// (where is the negation of tag) +bool add_or_cancel_tag(const String& tag, String& stack) { + String anti = anti_tag(tag); + size_t pos = stack.find(anti); + if (pos == String::npos) { + stack += _("<") + tag + _(">"); + return false; + } else { + // cancel out with anti tag + stack = stack.substr(0, pos) + stack.substr(pos + anti.size()); + return true; + } +} + +String simplify_tagged_merge(const String& str) { + String ret; ret.reserve(str.size()); + String waiting_tags; // tags that are waiting to be written to the output + size_t size = str.size(); + for (size_t i = 0 ; i < size ; ++i) { + Char c = str.GetChar(i); + if (c == _('<')) { + String tag = tag_at(str, i); + add_or_cancel_tag(tag, waiting_tags); + i += tag.size() + 1; + } else { + ret += waiting_tags; + waiting_tags.clear(); + ret += c; + } + } + return ret + waiting_tags; +} + +String simplify_tagged_overlap(const String& str) { + String ret; ret.reserve(str.size()); + String open_tags; // tags we are in + size_t size = str.size(); + for (size_t i = 0 ; i < size ; ++i) { + Char c = str.GetChar(i); + if (c == _('<')) { + String tag = tag_at(str, i); + if (starts_with(tag, _("b")) || starts_with(tag, _("i")) || starts_with(tag, _("sym")) || + starts_with(tag, _("/b")) || starts_with(tag, _("/i")) || starts_with(tag, _("/sym"))) { + // optimize this tag + if (open_tags.find(_("<") + tag + _(">")) == String::npos) { + // we are not already inside this tag + add_or_cancel_tag(tag, open_tags); + if (open_tags.find(anti_tag(tag)) != String::npos) { + // still not canceled out + i += tag.size() + 2; + continue; + } + } else { + // skip this tag, doubling it has no effect + i += tag.size() + 2; + add_or_cancel_tag(tag, open_tags); + continue; + } + } + } + ret += c; + } + return ret; +} diff --git a/src/util/tagged_string.hpp b/src/util/tagged_string.hpp index d27580be..7ad5f697 100644 --- a/src/util/tagged_string.hpp +++ b/src/util/tagged_string.hpp @@ -46,10 +46,17 @@ String fix_old_tags(const String&); size_t skip_tag(const String& str, size_t start); /// Find the position of the closing tag matching the tag at start -/** If not found returns String::npos - */ +/** If not found returns String::npos */ size_t match_close_tag(const String& str, size_t start); +/// Find the last start tag before position start +/** If not found returns String::npos */ +size_t last_start_tag_before(const String& str, const String& tag, size_t start); + +/// Is the given range entirely contained in a given tag? +/** If so: return the start position of that tag, otherwise returns String::npos */ +size_t in_tag(const String& str, const String& tag, size_t start, size_t end); + /// Return the tag at the given position (without the <>) String tag_at(const String& str, size_t pos); @@ -80,6 +87,17 @@ String remove_tag_exact(const String& str, const String& tag); */ String remove_tag_contents(const String& str, const String& tag); +// ----------------------------------------------------------------------------- : Updates + +/// Replace a subsection of 'input' with 'replacement'. +/** The section to replace is indicated by [start...end). + * This function makes sure tags still match. It also attempts to cancel out tags. + * This means that when removing "a" nothing is left, + * but with input "a" -> "" and "a" -> "". + * Escapes the replacement, i.e. all < in become \1. + */ +String tagged_substr_replace(const String& input, size_t start, size_t end, const String& replacement); + // ----------------------------------------------------------------------------- : Simplification /// Verify that a string is correctly tagged, if it is not, change it so it is @@ -95,10 +113,14 @@ String verify_tagged(const String& str); */ String simplify_tagged(const String& str); -/// Simplify a tagged string by merging adjecent open/close tags "" --> "" +/// Simplify a tagged string by merging adjecent open/close tags +/** e.g. "" --> "" + */ String simplify_tagged_merge(const String& str); /// Simplify overlapping formatting tags +/** e.g. "blahblahblah" -> "blahblahblah" + */ String simplify_tagged_overlap(const String& str); // ----------------------------------------------------------------------------- : EOF