Commit be2222ca authored by twanvl's avatar twanvl

Partially working text editor

parent b108304f
......@@ -14,6 +14,7 @@
#include <data/field/color.hpp>
#include <data/field/image.hpp>
#include <data/field/symbol.hpp>
#include <util/tagged_string.hpp>
// ----------------------------------------------------------------------------- : ValueAction
......@@ -35,16 +36,14 @@ class SimpleValueAction : public ValueAction {
swap(static_cast<T&>(*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<const SimpleValueAction*>(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<String>& 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<TextValue&>(*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 += _("</") + tag + _(">");
new_value += str.substr(end);
} else {
// we are inside this tag, _('remove') it
new_value = str.substr(0, start);
new_value += _("</") + tag + _(">");
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 <b></b>, 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);
}
}
}
......@@ -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<Type##Value&>(*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<String>& new_value);
ValueAction* value_action(const ColorValueP& value, const Defaultable<Color>& 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>& color);
TextValueAction(const TextValueP& value, size_t start, size_t end, size_t new_end, const Defaultable<String>& 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> color; ///< The new/old color
inline TextValue& value() const;
size_t new_selection_end;
Defaultable<String> 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
......@@ -11,6 +11,7 @@
#include <util/prec.hpp>
#include <util/defaultable.hpp>
#include <util/rotation.hpp>
#include <data/field.hpp>
#include <data/font.hpp>
#include <data/symbol_font.hpp>
......@@ -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();
};
......
......@@ -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);
......
......@@ -12,6 +12,7 @@
#include <data/field.hpp>
#include <data/stylesheet.hpp>
#include <data/settings.hpp>
#include <wx/caret.h>
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);
......
......@@ -7,7 +7,597 @@
// ----------------------------------------------------------------------------- : Includes
#include <gui/value/text.hpp>
#include <data/action/value.hpp>
#include <util/tagged_string.hpp>
#include <util/window_id.hpp>
#include <wx/clipbrd.h>
#include <wx/caret.h>
// ----------------------------------------------------------------------------- :
// ----------------------------------------------------------------------------- : 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 <sep> 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 <sep> 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(), _("<kw-"), selection_start, selection_start);
if (kwpos != String::npos) {
Char c = String(value().value()).GetChar(kwpos + 4);
m.AppendSeparator();
m.AppendCheckItem(ID_FORMAT_REMINDER, _("&Reminder text"), _("Show or hide reminder text for this keyword"));
m.Check(ID_FORMAT_REMINDER, c == _('1') || c == _('A')); // reminder text currently shown
}
// always show the menu
return true;
}
void TextValueEditor::onMenu(wxCommandEvent& ev) {
if (ev.GetId() == ID_FORMAT_REMINDER) {
// toggle reminder text
size_t kwpos = in_tag(value().value(), _("<kw-"), selection_start, selection_start);
if (kwpos != String::npos) {
// getSet().actions.add(new TextToggleReminderAction(value, kwpos));
}
} else {
ev.Skip();
}
}
// ----------------------------------------------------------------------------- : Other overrides
wxCursor rotated_ibeam;
wxCursor TextValueEditor::cursor() const {
if (viewer.getRotation().sideways() ^ style().getRotation().sideways()) { // 90 or 270 degrees
if (!rotated_ibeam.Ok()) {
rotated_ibeam = wxCursor(_("CUR_ROT_IBEAM"));
}
return rotated_ibeam;
} else {
return wxCURSOR_IBEAM;
}
}
void TextValueEditor::onValueChange() {
TextValueViewer::onValueChange();
selection_start = 0;
selection_end = 0;
}
void TextValueEditor::onAction(const ValueAction& action, bool undone) {
TextValueViewer::onAction(action, undone);
TYPE_CASE(action, TextValueAction) {
selection_start = action.selection_start;
selection_end = action.selection_end;
}
}
// ----------------------------------------------------------------------------- : Clipboard
bool TextValueEditor::canPaste() const {
return wxTheClipboard->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(), _("<b"), selection_start, selection_end) != String::npos;
case ID_FORMAT_ITALIC:
return in_tag(value().value(), _("<i"), selection_start, selection_end) != String::npos;
case ID_FORMAT_SYMBOL:
return in_tag(value().value(), _("<sym"), selection_start, selection_end) != String::npos;
case ID_FORMAT_REMINDER:
return false; // TODO
default:
return false;
}
}
void TextValueEditor::doFormat(int type) {
switch (type) {
case ID_FORMAT_BOLD: {
getSet().actions.add(toggle_format_action(valueP(), _("b"), selection_start, selection_end, _("Bold")));
break;
}
case ID_FORMAT_ITALIC: {
getSet().actions.add(toggle_format_action(valueP(), _("i"), selection_start, selection_end, _("Italic")));
break;
}
case ID_FORMAT_SYMBOL: {
getSet().actions.add(toggle_format_action(valueP(), _("sym"), selection_start, selection_end, _("Symbols")));
break;
}
}
}
// ----------------------------------------------------------------------------- : Selection
void TextValueEditor::showCaret() {
// Rotation
Rotation rot(viewer.getRotation());
Rotater rot2(rot, style().getRotation());
// The caret
wxCaret* caret = editor().GetCaret();
// cursor rectangle
RealRect cursor = v.charRect(selection_end);
cursor.width = 0;
// height may be 0 near a <line>
// 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(_("<sep"));
while (seppos != String::npos) {
size_t sepend = match_close_tag(val, seppos);
if (selection_start <= seppos && selection_end > 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(_("<sep"), seppos + 1);
}
// start or end in an <atom>? if so, move them out
size_t atompos = val.find(_("<atom"));
while (atompos != String::npos) {
size_t atomend = match_close_tag(val, atompos);
if (selection_start > 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(_("<atom"), atompos + 1);
}
// start and end must not be inside or between tags
// TODO
}
size_t TextValueEditor::prevCharBoundry(size_t pos) const {
return max(0, (int)pos - 1);
}
size_t TextValueEditor::nextCharBoundry(size_t pos) const {
return max(value().value().size(), pos + 1);
}
size_t TextValueEditor::prevWordBoundry(size_t pos) const {
const String& val = value().value();
size_t p = val.find_last_not_of(_(" ,.:;()\n"), max(0, (int)(pos - 1))); //note: pos-1 might be < 0
if (p == String::npos) return 0;
p = val.find_last_of(_(" ,.:;()\n"), p);
if (p == String::npos) return 0;
return p + 1;
}
size_t TextValueEditor::nextWordBoundry(size_t pos) const {
const String& val = value().value();
size_t p = val.find_first_of(_(" ,.:;()\n"), pos);
if (p == String::npos) return val.size();
p = val.find_first_not_of(_(" ,.:;()\n"), p);
if (p == String::npos) return val.size();
return p;
}
void TextValueEditor::select(size_t start, size_t end) {
selection_start = start;
selection_end = end;
// TODO : redraw?
}
size_t TextValueEditor::move(size_t pos, size_t start, size_t end, Movement dir) {
if (dir == MOVE_LEFT) return start;
if (dir == MOVE_RIGHT) return end;
if (pos * 2 > 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
......@@ -13,14 +13,116 @@
#include <gui/value/editor.hpp>
#include <render/value/text.hpp>
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();
// --------------------------------------------------- : 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 <sep> or </sep> 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;
// virtual void determineSize();
/// Scroll to the given position, called by scrollbar
void scrollTo(int pos);
};
// ----------------------------------------------------------------------------- : EOF
......
......@@ -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<double>::const_iterator it1 = lower_bound(positions.begin(), positions.end(), x);
if (it1 == positions.end()) return end();
vector<double>::const_iterator it2 = lower_bound(positions.begin(), positions.end(), x);
if (it2 == positions.begin()) return start;
// first index with pos > x
vector<double>::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<double>::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<Line>::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
......
......@@ -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
......
......@@ -25,7 +25,7 @@ class TextValueViewer : public ValueViewer {
virtual void onValueChange();
virtual void onStyleChange();
private:
protected:
TextViewer v;
};
......
......@@ -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
......
......@@ -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 {
......
......@@ -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
......
......@@ -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)?
......
......@@ -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) {
......
......@@ -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
......
......@@ -7,6 +7,7 @@
// ----------------------------------------------------------------------------- : Includes
#include <util/tagged_string.hpp>
#include <stack>
// ----------------------------------------------------------------------------- : 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<String> 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.top() + _(">");
}
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 _("</") + tag.substr(1);
}
String anti_tag(const String& tag) {
if (!tag.empty() && tag.GetChar(0) == _('/')) return _("<") + tag.substr(1) + _(">");
else return _("</") + tag + _(">");
}
// ----------------------------------------------------------------------------- : Global operations
// ----------------------------------------------------------------------------- : Updates
/// Return all open or close tags in the given range from a string
/** for example:
* if close_tags == false, "text<tag>text</tag>text" --> "<tag>"
* if close_tags == true, "text<tag>text</tag>text" --> "</tag>"
*/
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 </tag> is in stack remove it and returns true
// otherwise appends <tag> and returns fales
// (where </tag> 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;
}
......@@ -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 "<x>a</x>" nothing is left,
* but with input "<x>a" -> "<x>" 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 "<tag></tag>" --> ""
/// Simplify a tagged string by merging adjecent open/close tags
/** e.g. "<tag></tag>" --> ""
*/
String simplify_tagged_merge(const String& str);
/// Simplify overlapping formatting tags
/** e.g. "<i>blah<i>blah</i>blah</i>" -> "<i>blahblahblah</i>"
*/
String simplify_tagged_overlap(const String& str);
// ----------------------------------------------------------------------------- : EOF
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment