mirror of
https://github.com/amyinspace/MagicSetEditor2.git
synced 2026-06-10 13:06:59 -04:00
Partially working text editor
git-svn-id: svn://svn.code.sf.net/p/magicseteditor/code/trunk@99 0fc631ac-6414-0410-93d0-97cfa31319b6
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+20
-16
@@ -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);
|
||||
|
||||
+592
-2
@@ -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();
|
||||
}
|
||||
+103
-1
@@ -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();
|
||||
|
||||
// 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 <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;
|
||||
|
||||
/// Scroll to the given position, called by scrollbar
|
||||
void scrollTo(int pos);
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------- : EOF
|
||||
|
||||
+52
-13
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user