mirror of https://github.com/buggins/dlangui.git
Undo/Redo for editors, part 1
This commit is contained in:
parent
c335a6dd5f
commit
83b1651969
|
@ -44,6 +44,8 @@ import std.algorithm;
|
|||
struct Collection(T) {
|
||||
private T[] _items;
|
||||
private size_t _len;
|
||||
/// returns true if there are no items in collection
|
||||
@property bool empty() { return _len == 0; }
|
||||
/// returns number of items in collection
|
||||
@property size_t length() { return _len; }
|
||||
/// returns currently allocated capacity (may be more than length)
|
||||
|
@ -151,6 +153,48 @@ struct Collection(T) {
|
|||
_len = 0;
|
||||
_items = null;
|
||||
}
|
||||
|
||||
//===================================
|
||||
// stack/queue-like ops
|
||||
|
||||
/// remove first item
|
||||
@property T popFront() {
|
||||
if (empty)
|
||||
return T.init; // no items
|
||||
return remove(0);
|
||||
}
|
||||
|
||||
/// insert item at beginning of collection
|
||||
void pushFront(T item) {
|
||||
add(item, 0);
|
||||
}
|
||||
|
||||
/// remove last item
|
||||
@property T popBack() {
|
||||
if (empty)
|
||||
return T.init; // no items
|
||||
return remove(length - 1);
|
||||
}
|
||||
|
||||
/// insert item at end of collection
|
||||
void pushBack(T item) {
|
||||
add(item);
|
||||
}
|
||||
|
||||
/// peek first item
|
||||
@property T front() {
|
||||
if (empty)
|
||||
return T.init; // no items
|
||||
return _items[0];
|
||||
}
|
||||
|
||||
/// peek last item
|
||||
@property T back() {
|
||||
if (empty)
|
||||
return T.init; // no items
|
||||
return _items[_len - 1];
|
||||
}
|
||||
|
||||
~this() {
|
||||
clear();
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ module dlangui.widgets.editors;
|
|||
import dlangui.widgets.widget;
|
||||
import dlangui.widgets.controls;
|
||||
import dlangui.core.signals;
|
||||
import dlangui.core.collections;
|
||||
import dlangui.platforms.common.platform;
|
||||
|
||||
import std.algorithm;
|
||||
|
@ -135,16 +136,28 @@ enum EditAction {
|
|||
|
||||
/// edit operation details for EditableContent
|
||||
class EditOperation {
|
||||
/// action performed
|
||||
protected EditAction _action;
|
||||
/// action performed
|
||||
@property EditAction action() { return _action; }
|
||||
/// range
|
||||
protected TextRange _range;
|
||||
|
||||
/// source range to replace with new content
|
||||
@property ref TextRange range() { return _range; }
|
||||
protected TextRange _newRange;
|
||||
|
||||
/// new range after operation applied
|
||||
@property ref TextRange newRange() { return _newRange; }
|
||||
@property void newRange(TextRange range) { _newRange = range; }
|
||||
|
||||
/// new content for range (if required for this action)
|
||||
protected dstring[] _content;
|
||||
@property ref dstring[] content() { return _content; }
|
||||
|
||||
/// old content for range
|
||||
protected dstring[] _oldContent;
|
||||
@property ref dstring[] oldContent() { return _oldContent; }
|
||||
@property void oldContent(dstring[] content) { _oldContent = content; }
|
||||
|
||||
this(EditAction action) {
|
||||
_action = action;
|
||||
}
|
||||
|
@ -162,21 +175,90 @@ class EditOperation {
|
|||
_range = range;
|
||||
_content = text;
|
||||
}
|
||||
/// try to merge two operations (simple entering of characters in the same line), return true if succeded
|
||||
bool merge(EditOperation op) {
|
||||
if (!_range.empty || !op._range.empty)
|
||||
return false; // only merge simple single character append operations
|
||||
if (_content.length != 1 || op._content.length != 1 || op._content[0].length != 1)
|
||||
return false;
|
||||
if (_range.start.line != op._range.start.line) // both ops whould be on the same line
|
||||
return false;
|
||||
if (_range.start.pos + _content[0].length != op._range.start.pos)
|
||||
return false;
|
||||
_content[0] ~= op._content[0];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Undo/Redo buffer
|
||||
class UndoBuffer {
|
||||
protected Collection!EditOperation _undoList;
|
||||
protected Collection!EditOperation _redoList;
|
||||
|
||||
/// returns true if buffer contains any undo items
|
||||
@property bool hasUndo() {
|
||||
return !_undoList.empty;
|
||||
}
|
||||
|
||||
/// returns true if buffer contains any redo items
|
||||
@property bool hasRedo() {
|
||||
return !_redoList.empty;
|
||||
}
|
||||
|
||||
/// adds undo operation
|
||||
void saveForUndo(EditOperation op) {
|
||||
if (!_undoList.empty) {
|
||||
if (_undoList.back.merge(op)) {
|
||||
_redoList.clear();
|
||||
return; // merged - no need to add new operation
|
||||
}
|
||||
}
|
||||
_undoList.pushBack(op);
|
||||
}
|
||||
|
||||
/// returns operation to be undone (put it to redo), null if no undo ops available
|
||||
EditOperation undo() {
|
||||
if (!hasUndo)
|
||||
return null; // no undo operations
|
||||
EditOperation res = _undoList.popBack();
|
||||
_redoList.pushBack(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
/// returns operation to be redone (put it to undo), null if no undo ops available
|
||||
EditOperation redo() {
|
||||
if (!hasRedo)
|
||||
return null; // no undo operations
|
||||
EditOperation res = _redoList.popBack();
|
||||
_undoList.pushBack(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
/// clears both undo and redo buffers
|
||||
void clear() {
|
||||
_undoList.clear();
|
||||
_redoList.clear();
|
||||
}
|
||||
}
|
||||
|
||||
interface EditableContentListener {
|
||||
bool onContentChange(EditableContent content, EditOperation operation, ref TextRange rangeBefore, ref TextRange rangeAfter);
|
||||
}
|
||||
|
||||
/// editable plain text (multiline)
|
||||
/// editable plain text (singleline/multiline)
|
||||
class EditableContent {
|
||||
/// listeners for edit operations
|
||||
Signal!EditableContentListener contentChangeListeners;
|
||||
|
||||
this(bool multiline) {
|
||||
_multiline = multiline;
|
||||
_lines.length = 1; // initial state: single empty line
|
||||
_undoBuffer = new UndoBuffer();
|
||||
}
|
||||
|
||||
protected UndoBuffer _undoBuffer;
|
||||
|
||||
/// listeners for edit operations
|
||||
Signal!EditableContentListener contentChangeListeners;
|
||||
|
||||
protected bool _multiline;
|
||||
/// returns true if miltyline content is supported
|
||||
@property bool multiline() { return _multiline; }
|
||||
|
@ -325,12 +407,52 @@ class EditableContent {
|
|||
// same line
|
||||
rangeAfter.end.pos = rangeAfter.start.pos + cast(int)newcontent[0].length;
|
||||
}
|
||||
op.newRange = rangeAfter;
|
||||
op.oldContent = oldcontent;
|
||||
replaceRange(rangeBefore, rangeAfter, newcontent);
|
||||
handleContentChange(op, rangeBefore, rangeAfter);
|
||||
_undoBuffer.saveForUndo(op);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// return true if there is at least one operation in undo buffer
|
||||
@property bool hasUndo() {
|
||||
return _undoBuffer.hasUndo;
|
||||
}
|
||||
/// return true if there is at least one operation in redo buffer
|
||||
@property bool hasRedo() {
|
||||
return _undoBuffer.hasRedo;
|
||||
}
|
||||
/// undoes last change
|
||||
bool undo() {
|
||||
if (!hasUndo)
|
||||
return false;
|
||||
EditOperation op = _undoBuffer.undo();
|
||||
TextRange rangeBefore = op.newRange;
|
||||
dstring[] oldcontent = op.content;
|
||||
dstring[] newcontent = op.oldContent;
|
||||
TextRange rangeAfter = op.range;
|
||||
Log.d("Undoing op rangeBefore=", rangeBefore, " contentBefore=`", oldcontent, "` rangeAfter=", rangeAfter, " contentAfter=`", newcontent, "`");
|
||||
replaceRange(rangeBefore, rangeAfter, newcontent);
|
||||
handleContentChange(op, rangeBefore, rangeAfter);
|
||||
return true;
|
||||
}
|
||||
/// redoes last undone change
|
||||
bool redo() {
|
||||
if (!hasUndo)
|
||||
return false;
|
||||
EditOperation op = _undoBuffer.redo();
|
||||
TextRange rangeBefore = op.range;
|
||||
dstring[] oldcontent = op.oldContent;
|
||||
dstring[] newcontent = op.content;
|
||||
TextRange rangeAfter = op.newRange;
|
||||
Log.d("Redoing op rangeBefore=", rangeBefore, " contentBefore=`", oldcontent, "` rangeAfter=", rangeAfter, " contentAfter=`", newcontent, "`");
|
||||
replaceRange(rangeBefore, rangeAfter, newcontent);
|
||||
handleContentChange(op, rangeBefore, rangeAfter);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Editor action codes
|
||||
|
@ -400,14 +522,22 @@ enum EditorActions {
|
|||
DelPrevWord,
|
||||
/// delete char after cursor (ctrl + del key)
|
||||
DelNextWord,
|
||||
|
||||
/// insert new line (Enter)
|
||||
InsertNewLine,
|
||||
InsertNewLine,
|
||||
/// insert new line after current position (Ctrl+Enter)
|
||||
PrependNewLine,
|
||||
|
||||
/// Copy selection to clipboard
|
||||
Copy,
|
||||
/// Cut selection to clipboard
|
||||
Cut,
|
||||
/// Paste selection from clipboard
|
||||
Paste,
|
||||
/// Undo last change
|
||||
Undo,
|
||||
/// Redo last undoed change
|
||||
Redo,
|
||||
}
|
||||
|
||||
/// base for all editor widgets
|
||||
|
@ -455,19 +585,31 @@ class EditWidgetBase : WidgetGroup, EditableContentListener {
|
|||
new Action(EditorActions.SelectDocumentEnd, KeyCode.END, KeyFlag.Control | KeyFlag.Shift),
|
||||
|
||||
new Action(EditorActions.InsertNewLine, KeyCode.RETURN, 0),
|
||||
new Action(EditorActions.InsertNewLine, KeyCode.RETURN, KeyFlag.Shift),
|
||||
new Action(EditorActions.PrependNewLine, KeyCode.RETURN, KeyFlag.Control),
|
||||
|
||||
// Backspace/Del
|
||||
new Action(EditorActions.DelPrevChar, KeyCode.BACK, 0),
|
||||
new Action(EditorActions.DelNextChar, KeyCode.DEL, 0),
|
||||
new Action(EditorActions.DelPrevWord, KeyCode.BACK, KeyFlag.Control),
|
||||
new Action(EditorActions.DelNextWord, KeyCode.DEL, KeyFlag.Control),
|
||||
|
||||
// Copy/Paste
|
||||
new Action(EditorActions.Copy, KeyCode.KEY_C, KeyFlag.Control),
|
||||
new Action(EditorActions.Copy, KeyCode.KEY_C, KeyFlag.Control|KeyFlag.Shift),
|
||||
new Action(EditorActions.Copy, KeyCode.INS, KeyFlag.Control),
|
||||
new Action(EditorActions.Cut, KeyCode.KEY_X, KeyFlag.Control),
|
||||
new Action(EditorActions.Cut, KeyCode.KEY_X, KeyFlag.Control|KeyFlag.Shift),
|
||||
new Action(EditorActions.Cut, KeyCode.DEL, KeyFlag.Shift),
|
||||
new Action(EditorActions.Paste, KeyCode.KEY_V, KeyFlag.Control),
|
||||
new Action(EditorActions.Paste, KeyCode.KEY_V, KeyFlag.Control|KeyFlag.Shift),
|
||||
new Action(EditorActions.Paste, KeyCode.INS, KeyFlag.Shift),
|
||||
|
||||
// Undo/Redo
|
||||
new Action(EditorActions.Undo, KeyCode.KEY_Z, KeyFlag.Control),
|
||||
new Action(EditorActions.Redo, KeyCode.KEY_Y, KeyFlag.Control),
|
||||
new Action(EditorActions.Redo, KeyCode.KEY_Z, KeyFlag.Control|KeyFlag.Shift),
|
||||
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -742,6 +884,16 @@ class EditWidgetBase : WidgetGroup, EditableContentListener {
|
|||
EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, lines);
|
||||
_content.performOperation(op);
|
||||
}
|
||||
return true;
|
||||
case EditorActions.Undo:
|
||||
{
|
||||
_content.undo();
|
||||
}
|
||||
return true;
|
||||
case EditorActions.Redo:
|
||||
{
|
||||
_content.redo();
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
|
@ -1142,6 +1294,14 @@ class EditBox : EditWidgetBase, OnScrollHandler {
|
|||
TextPosition oldCaretPos = _caretPos;
|
||||
dstring currentLine = _content[_caretPos.line];
|
||||
switch (a.id) {
|
||||
case EditorActions.PrependNewLine:
|
||||
{
|
||||
correctCaretPos();
|
||||
_caretPos.pos = 0;
|
||||
EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [""d, ""d]);
|
||||
_content.performOperation(op);
|
||||
}
|
||||
return true;
|
||||
case EditorActions.InsertNewLine:
|
||||
{
|
||||
correctCaretPos();
|
||||
|
|
Loading…
Reference in New Issue