Forráskód Böngészése

TextEditor: Added undo functionality

Created a stack where a vector of undo actions are stored.
rhin123 5 éve
szülő
commit
9e608885d1
2 módosított fájl, 277 hozzáadás és 7 törlés
  1. 212 7
      Libraries/LibGUI/GTextEditor.cpp
  2. 65 0
      Libraries/LibGUI/GTextEditor.h

+ 212 - 7
Libraries/LibGUI/GTextEditor.cpp

@@ -39,12 +39,8 @@ GTextEditor::~GTextEditor()
 
 void GTextEditor::create_actions()
 {
-    m_undo_action = GCommonActions::make_undo_action([&](auto&) {
-        // FIXME: Undo
-    });
-    m_redo_action = GCommonActions::make_redo_action([&](auto&) {
-        // FIXME: Undo
-    });
+    m_undo_action = GCommonActions::make_undo_action([&](auto&) { undo(); }, this);
+    m_redo_action = GCommonActions::make_redo_action([&](auto&) { redo(); }, this);
     m_cut_action = GCommonActions::make_cut_action([&](auto&) { cut(); }, this);
     m_copy_action = GCommonActions::make_copy_action([&](auto&) { copy(); }, this);
     m_paste_action = GCommonActions::make_paste_action([&](auto&) { paste(); }, this);
@@ -458,6 +454,35 @@ void GTextEditor::select_all()
     update();
 }
 
+void GTextEditor::undo()
+{
+    if (m_undo_stack.size() <= 0)
+        return;
+
+    auto& undo_vector = m_undo_stack[m_undo_index];
+
+    //If we try to undo a empty vector, delete it and skip over.
+    if (undo_vector.size() <= 0 && m_undo_index > 0) {
+        m_undo_index--;
+        undo();
+        return;
+    }
+
+    for (int i = 0; i < undo_vector.size(); i++) {
+        auto& undo_command = undo_vector[i];
+        undo_command.undo();
+    }
+
+    undo_vector.clear();
+    m_undo_stack.remove(m_undo_index);
+    if (m_undo_index > 0)
+        m_undo_index--;
+}
+
+void GTextEditor::redo()
+{
+}
+
 void GTextEditor::keydown_event(GKeyEvent& event)
 {
     if (is_single_line() && event.key() == KeyCode::Key_Tab)
@@ -631,6 +656,9 @@ void GTextEditor::keydown_event(GKeyEvent& event)
 
             // Backspace within line
             for (int i = 0; i < erase_count; ++i) {
+                int row = m_cursor.line();
+                int column = m_cursor.column() - 1 - i;
+                add_to_undo_stack(make<RemoveCharacterCommand>(*this, document().line(row).characters()[column], GTextPosition(row, column)));
                 current_line().remove(document(), m_cursor.column() - 1 - i);
             }
             update_content_size();
@@ -642,6 +670,11 @@ void GTextEditor::keydown_event(GKeyEvent& event)
             // Backspace at column 0; merge with previous line
             auto& previous_line = lines()[m_cursor.line() - 1];
             int previous_length = previous_line.length();
+
+            int row = m_cursor.line();
+            int column = previous_length;
+            add_to_undo_stack(make<RemoveLineCommand>(*this, String(lines()[m_cursor.line()].view()), GTextPosition(row, column), true));
+
             previous_line.append(document(), current_line().characters(), current_line().length());
             document().remove_line(m_cursor.line());
             update_content_size();
@@ -739,6 +772,14 @@ void GTextEditor::insert_at_cursor(char ch)
                 if (leading_spaces)
                     new_line_contents = String::repeated(' ', leading_spaces);
             }
+
+            int row = m_cursor.line();
+            int column = m_cursor.column() + 1;
+            Vector<char> line_content;
+            for (int i = m_cursor.column(); i < document().lines()[row].length(); i++)
+                line_content.append(document().lines()[row].characters()[i]);
+            add_to_undo_stack(make<CreateLineCommand>(*this, line_content, GTextPosition(row, column)));
+
             document().insert_line(m_cursor.line() + (at_tail ? 1 : 0), make<GTextDocumentLine>(document(), new_line_contents));
             update();
             did_change();
@@ -747,6 +788,14 @@ void GTextEditor::insert_at_cursor(char ch)
         }
         auto new_line = make<GTextDocumentLine>(document());
         new_line->append(document(), current_line().characters() + m_cursor.column(), current_line().length() - m_cursor.column());
+
+        int row = m_cursor.line();
+        int column = m_cursor.column() + 1;
+        Vector<char> line_content;
+        for (int i = 0; i < new_line->length(); i++)
+            line_content.append(new_line->characters()[i]);
+        add_to_undo_stack(make<CreateLineCommand>(*this, line_content, GTextPosition(row, column)));
+
         current_line().truncate(document(), m_cursor.column());
         document().insert_line(m_cursor.line() + 1, move(new_line));
         update();
@@ -767,6 +816,8 @@ void GTextEditor::insert_at_cursor(char ch)
     current_line().insert(document(), m_cursor.column(), ch);
     did_change();
     set_cursor(m_cursor.line(), m_cursor.column() + 1);
+
+    add_to_undo_stack(make<InsertCharacterCommand>(*this, ch, m_cursor));
 }
 
 int GTextEditor::content_x_for_position(const GTextPosition& position) const
@@ -879,6 +930,27 @@ void GTextEditor::update_cursor()
     update(line_widget_rect(m_cursor.line()));
 }
 
+void GTextEditor::update_undo_timer()
+{
+    if (m_undo_stack.size() <= 0)
+        return;
+
+    if (m_undo_timer == 0)
+        m_prev_undo_stack_size = m_undo_stack[m_undo_index].size();
+
+    if (m_undo_timer >= 2 && m_undo_stack[m_undo_index].size() > 0) {
+
+        if (m_undo_stack[m_undo_index].size() == m_prev_undo_stack_size) {
+            dbg() << "Increased Undo Index";
+            m_undo_stack.append(make<NonnullOwnPtrVector<UndoCommand>>());
+            m_undo_index++;
+        }
+        m_undo_timer = -1;
+    }
+
+    m_undo_timer++;
+}
+
 void GTextEditor::set_cursor(int line, int column)
 {
     set_cursor({ line, column });
@@ -925,8 +997,10 @@ void GTextEditor::focusout_event(CEvent&)
 void GTextEditor::timer_event(CTimerEvent&)
 {
     m_cursor_state = !m_cursor_state;
-    if (is_focused())
+    if (is_focused()) {
         update_cursor();
+        update_undo_timer();
+    }
 }
 
 bool GTextEditor::write_to_file(const StringView& path)
@@ -1014,6 +1088,10 @@ void GTextEditor::delete_selection()
 
     // First delete all the lines in between the first and last one.
     for (int i = selection.start().line() + 1; i < selection.end().line();) {
+        int row = i;
+        int column = lines()[i].length();
+        add_to_undo_stack(make<RemoveLineCommand>(*this, String(lines()[i].view()), GTextPosition(row, column), false));
+
         document().remove_line(i);
         selection.end().set_line(selection.end().line() - 1);
     }
@@ -1022,6 +1100,13 @@ void GTextEditor::delete_selection()
         // Delete within same line.
         auto& line = lines()[selection.start().line()];
         bool whole_line_is_selected = selection.start().column() == 0 && selection.end().column() == line.length();
+
+        for (int i = selection.end().column() - 1; i >= selection.start().column(); i--) {
+            int row = selection.start().line();
+            int column = i;
+            add_to_undo_stack(make<RemoveCharacterCommand>(*this, document().line(row).characters()[column], GTextPosition(row, column)));
+        }
+
         if (whole_line_is_selected) {
             line.clear(document());
         } else {
@@ -1042,8 +1127,20 @@ void GTextEditor::delete_selection()
         StringBuilder builder(before_selection.length() + after_selection.length());
         builder.append(before_selection);
         builder.append(after_selection);
+
+        for (int i = first_line.length() - 1; i > selection.start().column() - 1; i--) {
+            int row = selection.start().line();
+            int column = i;
+            add_to_undo_stack(make<RemoveCharacterCommand>(*this, document().line(row).characters()[column], GTextPosition(row, column)));
+        }
+
+        add_to_undo_stack(make<RemoveLineCommand>(*this, String(second_line.view()), selection.end(), false));
+
         first_line.set_text(document(), builder.to_string());
         document().remove_line(selection.end().line());
+
+        for (int i = (first_line.length()) - after_selection.length(); i < first_line.length(); i++)
+            add_to_undo_stack(make<InsertCharacterCommand>(*this, first_line.characters()[i], GTextPosition(selection.start().line(), i + 1)));
     }
 
     if (lines().is_empty()) {
@@ -1198,6 +1295,14 @@ void GTextEditor::recompute_all_visual_lines()
     update_content_size();
 }
 
+void GTextEditor::add_to_undo_stack(NonnullOwnPtr<UndoCommand> undo_command)
+{
+    if (m_undo_stack.size() <= m_undo_index)
+        m_undo_stack.append(make<NonnullOwnPtrVector<UndoCommand>>());
+
+    m_undo_stack[(m_undo_index)].insert(0, move(undo_command));
+}
+
 int GTextEditor::visual_line_containing(int line_index, int column) const
 {
     int visual_line_index = 0;
@@ -1344,3 +1449,103 @@ void GTextEditor::set_document(GTextDocument& document)
     update();
     m_document->register_client(*this);
 }
+
+GTextEditor::UndoCommand::UndoCommand(GTextEditor& text_editor)
+    : m_text_editor(text_editor)
+{
+}
+
+GTextEditor::UndoCommand::~UndoCommand()
+{
+}
+
+void GTextEditor::UndoCommand::undo() {}
+void GTextEditor::UndoCommand::redo() {}
+
+GTextEditor::InsertCharacterCommand::InsertCharacterCommand(GTextEditor& text_editor, char ch, GTextPosition text_position)
+    : UndoCommand(text_editor)
+    , m_character(ch)
+    , m_text_position(text_position)
+{
+}
+
+GTextEditor::RemoveCharacterCommand::RemoveCharacterCommand(GTextEditor& text_editor, char ch, GTextPosition text_position)
+    : UndoCommand(text_editor)
+    , m_character(ch)
+    , m_text_position(text_position)
+{
+}
+
+GTextEditor::RemoveLineCommand::RemoveLineCommand(GTextEditor& text_editor, String line_content, GTextPosition text_position, bool has_merged_content)
+    : UndoCommand(text_editor)
+    , m_line_content(line_content)
+    , m_text_position(text_position)
+    , m_has_merged_content(has_merged_content)
+{
+}
+
+GTextEditor::CreateLineCommand::CreateLineCommand(GTextEditor& text_editor, Vector<char> line_content, GTextPosition text_position)
+    : UndoCommand(text_editor)
+    , m_line_content(line_content)
+    , m_text_position(text_position)
+{
+}
+
+void GTextEditor::InsertCharacterCommand::undo()
+{
+    //Move back the cursor if it's inside in deleted content
+    if (m_text_editor.cursor().column() >= m_text_position.column())
+        m_text_editor.set_cursor(m_text_position.line(), m_text_position.column() - 1);
+
+    m_text_editor.lines()[m_text_position.line()].remove(m_text_editor.document(), (m_text_position.column() - 1));
+}
+
+void GTextEditor::InsertCharacterCommand::redo()
+{
+    //TOOD: Redo implementation
+}
+
+void GTextEditor::RemoveCharacterCommand::undo()
+{
+    m_text_editor.lines()[m_text_position.line()].insert(m_text_editor.document(), m_text_position.column(), m_character);
+}
+
+void GTextEditor::RemoveCharacterCommand::redo()
+{
+    //TOOD: Redo implementation
+}
+
+void GTextEditor::RemoveLineCommand::undo()
+{
+
+    //Insert back the line
+    m_text_editor.document().insert_line(m_text_position.line(), make<GTextDocumentLine>(m_text_editor.document(), m_line_content));
+
+    //Remove the merged line contents
+    if (m_has_merged_content)
+        for (int i = m_line_content.length() - 1; i >= 0; i--)
+            m_text_editor.document().lines()[m_text_position.line() - 1].remove(m_text_editor.document(), (m_text_position.column()) + i);
+}
+
+void GTextEditor::RemoveLineCommand::redo()
+{
+    //TOOD: Redo implementation
+}
+
+void GTextEditor::CreateLineCommand::undo()
+{
+    //Insert back the created line portion
+    for (int i = 0; i < m_line_content.size(); i++)
+        m_text_editor.document().lines()[m_text_position.line()].insert(m_text_editor.document(), (m_text_position.column() - 1) + i, m_line_content[i]);
+
+    //Set the cursor back before the selection
+    m_text_editor.set_cursor(m_text_position.line(), m_text_editor.document().lines()[m_text_position.line()].length());
+
+    //Remove the created line
+    m_text_editor.document().remove_line(m_text_position.line() + 1);
+}
+
+void GTextEditor::CreateLineCommand::redo()
+{
+    //TOOD: Redo implementation
+}

+ 65 - 0
Libraries/LibGUI/GTextEditor.h

@@ -79,6 +79,8 @@ public:
     void do_delete();
     void delete_current_line();
     void select_all();
+    void undo();
+    void redo();
 
     Function<void()> on_change;
     Function<void()> on_return_pressed;
@@ -137,6 +139,7 @@ private:
     Rect cursor_content_rect() const;
     Rect content_rect_for_position(const GTextPosition&) const;
     void update_cursor();
+    void update_undo_timer();
     const NonnullOwnPtrVector<GTextDocumentLine>& lines() const { return document().lines(); }
     NonnullOwnPtrVector<GTextDocumentLine>& lines() { return document().lines(); }
     GTextDocumentLine& line(int index) { return document().line(index); }
@@ -156,6 +159,64 @@ private:
     Rect visible_text_rect_in_inner_coordinates() const;
     void recompute_all_visual_lines();
 
+    class UndoCommand {
+
+    public:
+        UndoCommand(GTextEditor& text_editor);
+        virtual ~UndoCommand();
+        virtual void undo();
+        virtual void redo();
+
+    protected:
+        GTextEditor& m_text_editor;
+    };
+
+    class InsertCharacterCommand : public UndoCommand {
+    public:
+        InsertCharacterCommand(GTextEditor& text_editor, char ch, GTextPosition text_position);
+        virtual void undo() override;
+        virtual void redo() override;
+
+    private:
+        char m_character;
+        GTextPosition m_text_position;
+    };
+
+    class RemoveCharacterCommand : public UndoCommand {
+    public:
+        RemoveCharacterCommand(GTextEditor& text_editor, char ch, GTextPosition text_position);
+        virtual void undo() override;
+        virtual void redo() override;
+
+    private:
+        char m_character;
+        GTextPosition m_text_position;
+    };
+
+    class RemoveLineCommand : public UndoCommand {
+    public:
+        RemoveLineCommand(GTextEditor& text_editor, String, GTextPosition text_position, bool has_merged_content);
+        virtual void undo() override;
+        virtual void redo() override;
+
+    private:
+        String m_line_content;
+        GTextPosition m_text_position;
+        bool m_has_merged_content;
+    };
+
+    class CreateLineCommand : public UndoCommand {
+    public:
+        CreateLineCommand(GTextEditor& text_editor, Vector<char> line_content, GTextPosition text_position);
+        virtual void undo() override;
+        virtual void redo() override;
+
+    private:
+        Vector<char> m_line_content;
+        GTextPosition m_text_position;
+    };
+
+    void add_to_undo_stack(NonnullOwnPtr<UndoCommand> undo_command);
     int visual_line_containing(int line_index, int column) const;
     void recompute_visual_lines(int line_index);
 
@@ -183,6 +244,10 @@ private:
     RefPtr<GAction> m_delete_action;
     CElapsedTimer m_triple_click_timer;
     NonnullRefPtrVector<GAction> m_custom_context_menu_actions;
+    NonnullOwnPtrVector<NonnullOwnPtrVector<UndoCommand>> m_undo_stack;
+    int m_undo_index = 0;
+    int m_undo_timer = 0;
+    int m_prev_undo_stack_size = 0;
 
     RefPtr<GTextDocument> m_document;