Unlimited undo/redo for Fl_Input_ and Fl_Text_Buffer (#558) (#676)

This commit is contained in:
Matthias Melcher 2023-02-10 17:13:20 +01:00 committed by GitHub
parent 72f8604381
commit 7f87c847ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 357 additions and 50 deletions

View File

@ -1,7 +1,7 @@
//
// Input header file for the Fast Light Tool Kit (FLTK).
//
// Copyright 1998-2021 by Bill Spitzak and others.
// Copyright 1998-2023 by Bill Spitzak and others.
//
// This library is free software. Distribution and use rights are outlined in
// the file "COPYING" which should have been included with this file. If this

View File

@ -39,6 +39,7 @@
#define FL_MULTILINE_OUTPUT_WRAP (FL_MULTILINE_INPUT | FL_INPUT_READONLY | FL_INPUT_WRAP)
class Fl_Input_Undo_Action;
class Fl_Input_Undo_Action_List;
/**
This class provides a low-overhead text input field.
@ -148,6 +149,8 @@ class FL_EXPORT Fl_Input_ : public Fl_Widget {
/** \internal local undo event */
Fl_Input_Undo_Action* undo_;
Fl_Input_Undo_Action_List* undo_list_;
Fl_Input_Undo_Action_List* redo_list_;
/** \internal Horizontal cursor position in pixels while moving up or down. */
static double up_down_pos;
@ -212,6 +215,9 @@ protected:
/* Return the number of lines displayed on a single page. */
int linesPerPage();
/* Apply the current undo/redo operation, called from undo() or redo() */
int apply_undo();
public:
/* Change the size of the widget. */
@ -378,6 +384,9 @@ public:
/* Undo previous changes to the text buffer. */
int undo();
/* Redo previous undo operations. */
int redo();
/* Copy the yank buffer to the clipboard. */
int copy_cuts();

View File

@ -1,7 +1,7 @@
//
// Header file for Fl_Text_Buffer class.
//
// Copyright 2001-2021 by Bill Spitzak and others.
// Copyright 2001-2023 by Bill Spitzak and others.
// Original code Copyright Mark Edel. Permission to distribute under
// the LGPL for the FLTK library granted by Mark Edel.
//
@ -61,6 +61,7 @@
#include "Fl_Export.H"
class Fl_Text_Undo_Action_List;
class Fl_Text_Undo_Action;
/**
@ -326,6 +327,11 @@ public:
*/
int undo(int *cp=0);
/**
Redo previous undo action.
*/
int redo(int *cp=0);
/**
Lets the undo system know if we can undo changes
*/
@ -813,6 +819,11 @@ protected:
*/
void update_selections(int pos, int nDeleted, int nInserted);
/**
Apply the current undo/redo operation, called from undo() or redo().
*/
int apply_undo(Fl_Text_Undo_Action* action, int* cursorPos);
Fl_Text_Selection mPrimary; /**< highlighted areas */
Fl_Text_Selection mSecondary; /**< highlighted areas */
Fl_Text_Selection mHighlight; /**< highlighted areas */
@ -841,6 +852,8 @@ protected:
bytes and should only be increased if frequent
and large changes in buffer size are expected */
Fl_Text_Undo_Action* mUndo; /**< local undo event */
Fl_Text_Undo_Action_List* mUndoList; /**< List of undo event */
Fl_Text_Undo_Action_List* mRedoList; /**< List of redo event */
};
#endif

View File

@ -1,7 +1,7 @@
//
// Header file for Fl_Text_Editor class.
//
// Copyright 2001-2010 by Bill Spitzak and others.
// Copyright 2001-2023 by Bill Spitzak and others.
// Original code Copyright Mark Edel. Permission to distribute under
// the LGPL for the FLTK library granted by Mark Edel.
//
@ -110,6 +110,7 @@ class FL_EXPORT Fl_Text_Editor : public Fl_Text_Display {
static int kf_paste(int c, Fl_Text_Editor* e);
static int kf_select_all(int c, Fl_Text_Editor* e);
static int kf_undo(int c, Fl_Text_Editor* e);
static int kf_redo(int c, Fl_Text_Editor* e);
protected:
int handle_key();

View File

@ -1,7 +1,7 @@
//
// Input widget for the Fast Light Tool Kit (FLTK).
//
// Copyright 1998-2021 by Bill Spitzak and others.
// Copyright 1998-2023 by Bill Spitzak and others.
//
// This library is free software. Distribution and use rights are outlined in
// the file "COPYING" which should have been included with this file. If this
@ -275,10 +275,10 @@ int Fl_Input::kf_undo() {
return undo();
}
// Redo. (currently unimplemented.. toggles undo() instead)
// Redo.
int Fl_Input::kf_redo() {
if (readonly()) { fl_beep(); return 1; }
return kf_undo(); // currently we don't support multilevel undo
return redo();
}
// Do a copy operation

View File

@ -33,6 +33,7 @@ extern void fl_draw(const char*, int, float, float);
////////////////////////////////////////////////////////////////
// see: Fl_Text_Undo_Action
class Fl_Input_Undo_Action {
public:
Fl_Input_Undo_Action() :
@ -71,6 +72,50 @@ public:
}
};
// see: Fl_Text_Undo_Action_List
class Fl_Input_Undo_Action_List {
Fl_Input_Undo_Action** list_;
int list_size_;
int list_capacity_;
public:
Fl_Input_Undo_Action_List() :
list_(NULL),
list_size_(0),
list_capacity_(0)
{ }
~Fl_Input_Undo_Action_List() {
clear();
}
void push(Fl_Input_Undo_Action* action) {
if (list_size_ == list_capacity_) {
list_capacity_ += 25;
list_ = (Fl_Input_Undo_Action**)realloc(list_, list_capacity_ * sizeof(Fl_Input_Undo_Action*));
}
list_[list_size_++] = action;
}
Fl_Input_Undo_Action* pop() {
if (list_size_ > 0)
return list_[--list_size_];
else
return NULL;
}
void clear() {
if (list_) {
for (int i=0; i<list_size_; i++) {
delete list_[i];
}
delete list_;
}
list_ = NULL;
list_size_ = 0;
list_capacity_ = 0;
}
};
/** \internal
Converts a given text segment into the text that will be rendered on screen.
@ -186,7 +231,7 @@ double Fl_Input_::expandpos(
/** \internal
Marks a range of characters for update.
This call marks all characters from \p to the end of the
This call marks all characters from \p p to the end of the
text buffer for update. At least these characters
will be redrawn in the next update cycle.
@ -891,6 +936,9 @@ int Fl_Input_::replace(int b, int e, const char* text, int ilen) {
} else if (e == undo_->undoat && (e-b)<undo_->undoinsert) {
undo_->undoinsert -= e-b;
} else {
redo_list_->clear();
undo_list_->push(undo_);
undo_ = new Fl_Input_Undo_Action();
undo_->undobuffersize(e-b);
memcpy(undo_->undobuffer, value_+b, e-b);
undo_->undocut = e-b;
@ -903,9 +951,12 @@ int Fl_Input_::replace(int b, int e, const char* text, int ilen) {
}
if (ilen) {
if (b == undo_->undoat)
if (b == undo_->undoat) {
undo_->undoinsert += ilen;
else {
} else {
redo_list_->clear();
undo_list_->push(undo_);
undo_ = new Fl_Input_Undo_Action();
undo_->undocut = 0;
undo_->undoinsert = ilen;
}
@ -947,13 +998,13 @@ int Fl_Input_::replace(int b, int e, const char* text, int ilen) {
}
/**
Undoes previous changes to the text buffer.
Apply the current undo/redo operation
This call undoes a number of previous calls to replace().
It's up to undo() and redo() to push and pop actions to and from the lists.
\return non-zero if any change was made.
*/
int Fl_Input_::undo() {
\return 1 if the current action changed any text.
\see undo(), redo() */
int Fl_Input_::apply_undo() {
was_up_down = 0;
if (!undo_->undocut && !undo_->undoinsert) return 0;
@ -962,6 +1013,8 @@ int Fl_Input_::undo() {
int b = undo_->undoat-xlen;
int b1 = b;
minimal_update(position_);
put_in_buffer(size_+ilen);
if (ilen) {
@ -989,10 +1042,52 @@ int Fl_Input_::undo() {
while (b1 > 0 && index(b1)!='\n') b1--;
minimal_update(b1);
set_changed();
if (when()&FL_WHEN_CHANGED) do_callback(FL_REASON_CHANGED);
return 1;
}
/**
Undoes previous changes to the text buffer.
This call undoes a number of previous calls to replace().
\return non-zero if any change was made.
*/
int Fl_Input_::undo() {
if (apply_undo() == 0)
return 0;
redo_list_->push(undo_);
undo_ = undo_list_->pop();
if (!undo_) undo_ = new Fl_Input_Undo_Action();
if (when()&FL_WHEN_CHANGED) do_callback(FL_REASON_CHANGED);
return 1;
}
/**
Redo previous undo operation.
This call reapplies previously executed undo operations.
\return non-zero if any change was made.
*/
int Fl_Input_::redo() {
Fl_Input_Undo_Action *redo_action = redo_list_->pop();
if (!redo_action)
return 0;
if (undo_->undocut || undo_->undoinsert)
undo_list_->push(undo_);
undo_ = redo_action;
int ret = apply_undo();
if (ret && (when()&FL_WHEN_CHANGED)) do_callback(FL_REASON_CHANGED);
return ret;
}
/**
Copies the \e yank buffer to the clipboard.
@ -1168,6 +1263,8 @@ Fl_Input_::Fl_Input_(int X, int Y, int W, int H, const char* l)
xscroll_ = yscroll_ = 0;
maximum_size_ = 32767;
shortcut_ = 0;
undo_list_ = new Fl_Input_Undo_Action_List();
redo_list_ = new Fl_Input_Undo_Action_List();
undo_ = new Fl_Input_Undo_Action();
set_flag(SHORTCUT_LABEL);
set_flag(MAC_USE_ACCENTS_MENU);
@ -1236,6 +1333,8 @@ void Fl_Input_::put_in_buffer(int len) {
int Fl_Input_::static_value(const char* str, int len) {
clear_changed();
undo_->clear();
undo_list_->clear();
redo_list_->clear();
if (str == value_ && len == size_) return 0;
if (len) { // non-empty new value:
if (xscroll_ || yscroll_) {
@ -1336,6 +1435,8 @@ void Fl_Input_::resize(int X, int Y, int W, int H) {
from the parent Fl_Group.
*/
Fl_Input_::~Fl_Input_() {
delete undo_list_;
delete redo_list_;
delete undo_;
if (bufsize) free((void*)buffer);
}

View File

@ -1,5 +1,5 @@
//
// Copyright 2001-2017 by Bill Spitzak and others.
// Copyright 2001-2023 by Bill Spitzak and others.
// Original code Copyright Mark Edel. Permission to distribute under
// the LGPL for the FLTK library granted by Mark Edel.
//
@ -66,6 +66,23 @@ static int min(int i1, int i2)
#endif
/*
Undo/Redo is handled with Fl_Text_Undo_Action. The names of the class members
relate to the original action.
Deleting text will store the number of bytes deleted in `undocut`, and store
the deleted text in `undobuffer`. `undoat` is the insertion position.
Inserting text will store the number of bytes inserted in `undoinsert` and
`undoat` will point after the inserted text.
If text is deleted first and then text is inserted at the same position, it's
called a yankcut, and the number of bytes that were deleted is stored in
`undoyankcut`, again storing the deleted text in `undobuffer`.
If an undo action is run, text is deleted and inserted via the normal
Fl_Text_Editor methods, generating the inverse undo action (redo) in mUndo.
*/
class Fl_Text_Undo_Action {
public:
Fl_Text_Undo_Action() :
@ -102,6 +119,76 @@ public:
void clear() {
undocut = undoinsert = 0;
}
bool empty() {
return (!undocut && !undoinsert);
}
};
/*
Undo events are stored in a Last In - First Out stack.
Any insertion or deletion of text will either add to the current undo event
in mUndo, or generate a new undo event if cursor positions are not consecutive.
The previously current undo event will then be pushed to the undo list and
the redo event list is purged.
If the user calls undo(), the current undo event in mUndo will be run,
generating a matching redo event in mUndo. The redo event is then pushed into
the redo list, and the next undo event is popped from the undo list and made
current.
A list can be locked to be protected from purging while running an undo event.
*/
class Fl_Text_Undo_Action_List {
Fl_Text_Undo_Action** list_;
int list_size_;
int list_capacity_;
bool locked_;
public:
Fl_Text_Undo_Action_List() :
list_(NULL),
list_size_(0),
list_capacity_(0),
locked_(false)
{ }
~Fl_Text_Undo_Action_List() {
unlock();
clear();
}
void push(Fl_Text_Undo_Action* action) {
if (list_size_ == list_capacity_) {
list_capacity_ += 25;
list_ = (Fl_Text_Undo_Action**)realloc(list_, list_capacity_ * sizeof(Fl_Text_Undo_Action*));
}
list_[list_size_++] = action;
}
Fl_Text_Undo_Action* pop() {
if (list_size_ > 0) {
Fl_Text_Undo_Action *action = list_[list_size_-1];
return list_[--list_size_];
} else
return NULL;
}
void clear() {
if (locked_) return;
if (list_) {
for (int i=0; i<list_size_; i++) {
delete list_[i];
}
delete list_;
}
list_ = NULL;
list_size_ = 0;
list_capacity_ = 0;
}
void lock() { locked_ = true; }
void unlock() { locked_ = false; }
};
@ -136,6 +223,8 @@ Fl_Text_Buffer::Fl_Text_Buffer(int requestedSize, int preferredGapSize)
mCursorPosHint = 0;
mCanUndo = 1;
mUndo = new Fl_Text_Undo_Action();
mUndoList = new Fl_Text_Undo_Action_List();
mRedoList = new Fl_Text_Undo_Action_List();
input_file_was_transcoded = 0;
transcoding_warning_action = def_transcoding_warning_action;
}
@ -156,6 +245,8 @@ Fl_Text_Buffer::~Fl_Text_Buffer()
delete[] mPredeleteCbArgs;
}
delete mUndo;
delete mUndoList;
delete mRedoList;
}
@ -205,8 +296,11 @@ void Fl_Text_Buffer::text(const char *t)
call_modify_callbacks(0, deletedLength, insertedLength, 0, deletedText);
free((void *) deletedText);
if (mCanUndo)
if (mCanUndo) {
mUndo->clear();
mUndoList->clear();
mRedoList->clear();
}
}
@ -459,51 +553,97 @@ void Fl_Text_Buffer::copy(Fl_Text_Buffer * fromBuf, int fromStart,
}
/*
Take the previous changes and undo them. Return the previous
cursor position in cursorPos. Returns 1 if the undo was applied.
CursorPos will be at a character boundary.
/**
Apply the current undo/redo operation, called from undo() or redo().
*/
int Fl_Text_Buffer::undo(int *cursorPos)
int Fl_Text_Buffer::apply_undo(Fl_Text_Undo_Action* action, int* cursorPos)
{
if (!mCanUndo)
if (action->empty())
return 0;
if (!mUndo->undocut && !mUndo->undoinsert)
return 0;
mRedoList->lock();
int ilen = mUndo->undocut;
int xlen = mUndo->undoinsert;
int b = mUndo->undoat - xlen;
int ilen = action->undocut;
int xlen = action->undoinsert;
int b = action->undoat - xlen;
if (xlen && mUndo->undoyankcut && !ilen) {
ilen = mUndo->undoyankcut;
if (xlen && action->undoyankcut && !ilen) {
ilen = action->undoyankcut;
}
if (xlen && ilen) {
mUndo->undobuffersize(ilen + 1);
mUndo->undobuffer[ilen] = 0;
char *tmp = fl_strdup(mUndo->undobuffer);
replace(b, mUndo->undoat, tmp);
action->undobuffersize(ilen + 1);
action->undobuffer[ilen] = 0;
char *tmp = fl_strdup(action->undobuffer);
replace(b, action->undoat, tmp);
if (cursorPos)
*cursorPos = mCursorPosHint;
free(tmp);
} else if (xlen) {
remove(b, mUndo->undoat);
remove(b, action->undoat);
if (cursorPos)
*cursorPos = mCursorPosHint;
} else if (ilen) {
mUndo->undobuffersize(ilen + 1);
mUndo->undobuffer[ilen] = 0;
insert(mUndo->undoat, mUndo->undobuffer);
action->undobuffersize(ilen + 1);
action->undobuffer[ilen] = 0;
insert(action->undoat, action->undobuffer);
if (cursorPos)
*cursorPos = mCursorPosHint;
mUndo->undoyankcut = 0;
action->undoyankcut = 0;
}
mRedoList->unlock();
return 1;
}
/**
Take the previous changes and undo them. Return the previous
cursor position in cursorPos. Returns 1 if the undo was applied.
CursorPos will be at a character boundary.
*/
int Fl_Text_Buffer::undo(int *cursorPos) {
if (!mCanUndo || mUndo->empty())
return 0;
// save the current undo action and add an empty action to avoid generating yankcuts
Fl_Text_Undo_Action* action = mUndo;
mUndo = new Fl_Text_Undo_Action();
int ret = apply_undo(action, cursorPos);
delete action;
if (ret) {
// push the generated undo action to the redo list
mRedoList->push(mUndo);
// drop the empty action we previously created
mUndo = mUndoList->pop();
if (mUndo) {
delete mUndo;
// pop the undo action before that and make it the current undo action
mUndo = mUndoList->pop();
if (!mUndo) mUndo = new Fl_Text_Undo_Action();
}
}
return ret;
}
/**
Redo previous undo action.
*/
int Fl_Text_Buffer::redo(int *cursorPos) {
if (!mCanUndo)
return 0;
Fl_Text_Undo_Action *redo_action = mRedoList->pop();
if (!redo_action)
return 0;
// running the redo action will also generate a new undo action
// Note: there is a slight chance that the current undo action and the
// generated action merge into one.
return apply_undo(redo_action, cursorPos);
}
/*
Set a flag if undo function will work.
@ -1218,10 +1358,20 @@ int Fl_Text_Buffer::insert_(int pos, const char *text)
if (mCanUndo) {
if (mUndo->undoat == pos && mUndo->undoinsert) {
// continue inserting text at the given cursor position
mUndo->undoinsert += insertedLength;
} else {
int yankcut = (mUndo->undoat == pos) ? mUndo->undocut : 0;
if (!yankcut) {
// insert text at a new position, so generate a new undo action
mRedoList->clear();
mUndoList->push(mUndo);
mUndo = new Fl_Text_Undo_Action();
} else {
// we deleted and inserted at the same position, making this a yankcut
}
mUndo->undoinsert = insertedLength;
mUndo->undoyankcut = (mUndo->undoat == pos) ? mUndo->undocut : 0;
mUndo->undoyankcut = yankcut;
}
mUndo->undoat = pos + insertedLength;
mUndo->undocut = 0;
@ -1241,10 +1391,15 @@ void Fl_Text_Buffer::remove_(int start, int end)
if (mCanUndo) {
if (mUndo->undoat == end && mUndo->undocut) {
// continue to remove text at the same cursor position
mUndo->undobuffersize(mUndo->undocut + end - start + 1);
memmove(mUndo->undobuffer + end - start, mUndo->undobuffer, mUndo->undocut);
mUndo->undocut += end - start;
} else {
// remove text at a new position, so generate a new undo action
mRedoList->clear();
mUndoList->push(mUndo);
mUndo = new Fl_Text_Undo_Action();
mUndo->undocut = end - start;
mUndo->undobuffersize(mUndo->undocut);
}

View File

@ -1,5 +1,5 @@
//
// Copyright 2001-2018 by Bill Spitzak and others.
// Copyright 2001-2023 by Bill Spitzak and others.
//
// Original code Copyright Mark Edel. Permission to distribute under
// the LGPL for the FLTK library granted by Mark Edel.
@ -132,7 +132,9 @@ static struct {
{ FL_Page_Down, FL_CTRL|FL_SHIFT, Fl_Text_Editor::kf_c_s_move },
//{ FL_Clear, 0, Fl_Text_Editor::delete_to_eol },
{ 'z', FL_CTRL, Fl_Text_Editor::kf_undo },
{ '/', FL_CTRL, Fl_Text_Editor::kf_undo },
{ 'z', FL_CTRL|FL_SHIFT, Fl_Text_Editor::kf_redo }, // MSWindows screen driver also defines Ctrl-Y
{ '/', FL_CTRL, Fl_Text_Editor::kf_undo }, // Emacs
{ '?', FL_CTRL, Fl_Text_Editor::kf_redo }, // Emacs
{ 'x', FL_CTRL, Fl_Text_Editor::kf_cut },
{ FL_Delete, FL_SHIFT, Fl_Text_Editor::kf_cut },
{ 'c', FL_CTRL, Fl_Text_Editor::kf_copy },
@ -596,13 +598,13 @@ int Fl_Text_Editor::kf_select_all(int, Fl_Text_Editor* e) {
}
/** Undo last edit in the current buffer of editor \p 'e'.
Also deselects previous selection.
The key value \p 'c' is currently unused.
*/
Also deselects previous selection.
The key value \p 'c' is currently unused.
*/
int Fl_Text_Editor::kf_undo(int , Fl_Text_Editor* e) {
e->buffer()->unselect();
Fl::copy("", 0, 0);
int crsr;
int crsr = e->insert_position();
int ret = e->buffer()->undo(&crsr);
e->insert_position(crsr);
e->show_insert_position();
@ -611,6 +613,22 @@ int Fl_Text_Editor::kf_undo(int , Fl_Text_Editor* e) {
return ret;
}
/** Redo last undo action.
Also deselects previous selection.
The key value \p 'c' is currently unused.
*/
int Fl_Text_Editor::kf_redo(int , Fl_Text_Editor* e) {
e->buffer()->unselect();
Fl::copy("", 0, 0);
int crsr = e->insert_position();
int ret = e->buffer()->redo(&crsr);
e->insert_position(crsr);
e->show_insert_position();
e->set_changed();
if (e->when()&FL_WHEN_CHANGED) e->do_callback();
return ret;
}
/** Handles a key press in the editor */
int Fl_Text_Editor::handle_key() {
// Call FLTK's rules to try to turn this into a printing character.

View File

@ -1,7 +1,7 @@
//
// Definition of Apple Cocoa Screen interface.
//
// Copyright 1998-2022 by Bill Spitzak and others.
// Copyright 1998-2023 by Bill Spitzak and others.
//
// This library is free software. Distribution and use rights are outlined in
// the file "COPYING" which should have been included with this file. If this
@ -79,6 +79,7 @@ Fl_Screen_Driver::Keyname darwin_key_table[] = {
static Fl_Text_Editor::Key_Binding extra_bindings[] = {
// Define CMD+key accelerators...
{ 'z', FL_COMMAND, Fl_Text_Editor::kf_undo ,0},
{ 'z', FL_COMMAND|FL_SHIFT, Fl_Text_Editor::kf_redo ,0},
{ 'x', FL_COMMAND, Fl_Text_Editor::kf_cut ,0},
{ 'c', FL_COMMAND, Fl_Text_Editor::kf_copy ,0},
{ 'v', FL_COMMAND, Fl_Text_Editor::kf_paste ,0},

View File

@ -41,9 +41,7 @@ protected:
public:
float dpi[MAX_SCREENS][2];
Fl_WinAPI_Screen_Driver() : Fl_Screen_Driver() {
for (int i = 0; i < MAX_SCREENS; i++) scale_of_screen[i] = 1;
}
Fl_WinAPI_Screen_Driver();
// --- display management
int visual(int flags) FL_OVERRIDE;
// --- screen configuration

View File

@ -38,6 +38,17 @@ extern const char *fl_bg2;
# include <multimon.h>
#endif // !HMONITOR_DECLARED && _WIN32_WINNT < 0x0500
static Fl_Text_Editor::Key_Binding extra_bindings[] = {
// Define MS Windows specific accelerators...
{ 'y', FL_CTRL, Fl_Text_Editor::kf_redo ,0},
{ 0, 0, 0 ,0}
};
Fl_WinAPI_Screen_Driver::Fl_WinAPI_Screen_Driver() : Fl_Screen_Driver() {
text_editor_extra_key_bindings = extra_bindings;
for (int i = 0; i < MAX_SCREENS; i++) scale_of_screen[i] = 1;
}
int Fl_WinAPI_Screen_Driver::visual(int flags)
{