From 96c1e36ed3d5a09459cd7cdb9dc1bfe89a6b93da Mon Sep 17 00:00:00 2001 From: rhin123 Date: Tue, 5 Nov 2019 16:37:47 -0600 Subject: [PATCH] TextEditor: Added redo functionality & proper stack manipulation Added redo functionality & added m_undo_stack_index that moves back & forth on the stack depending on the undo's & redo's. --- Libraries/LibGUI/GTextEditor.cpp | 127 +++++++++++++++++++++---------- Libraries/LibGUI/GTextEditor.h | 14 +++- 2 files changed, 97 insertions(+), 44 deletions(-) diff --git a/Libraries/LibGUI/GTextEditor.cpp b/Libraries/LibGUI/GTextEditor.cpp index d6804fe54b..c64e073a99 100644 --- a/Libraries/LibGUI/GTextEditor.cpp +++ b/Libraries/LibGUI/GTextEditor.cpp @@ -29,6 +29,13 @@ GTextEditor::GTextEditor(Type type, GWidget* parent) vertical_scrollbar().set_step(line_height()); m_cursor = { 0, 0 }; create_actions(); + + // TODO: Instead of a repating timer, this we should call a delayed 2 sec timer when the user types. + m_undo_timer = CTimer::construct( + 2000, [&] { + update_undo_timer(); + }, + this); } GTextEditor::~GTextEditor() @@ -456,14 +463,15 @@ void GTextEditor::select_all() void GTextEditor::undo() { - if (m_undo_stack.size() <= 0) + if (m_undo_stack_index >= m_undo_stack.size() || m_undo_stack.is_empty()) return; - auto& undo_vector = m_undo_stack[m_undo_index]; + auto& undo_container = m_undo_stack[m_undo_stack_index]; + auto& undo_vector = undo_container.m_undo_vector; //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--; + if (undo_vector.is_empty()) { + m_undo_stack.remove(m_undo_stack_index); undo(); return; } @@ -473,14 +481,23 @@ void GTextEditor::undo() undo_command.undo(); } - undo_vector.clear(); - m_undo_stack.remove(m_undo_index); - if (m_undo_index > 0) - m_undo_index--; + m_undo_stack_index++; } void GTextEditor::redo() { + if (m_undo_stack_index <= 0 || m_undo_stack.is_empty()) + return; + + auto& undo_container = m_undo_stack[m_undo_stack_index - 1]; + auto& redo_vector = undo_container.m_undo_vector; + + for (int i = redo_vector.size() - 1; i >= 0; i--) { + auto& undo_command = redo_vector[i]; + undo_command.redo(); + } + + m_undo_stack_index--; } void GTextEditor::keydown_event(GKeyEvent& event) @@ -933,23 +950,23 @@ void GTextEditor::update_cursor() void GTextEditor::update_undo_timer() { - if (m_undo_stack.size() <= 0) + if (m_undo_stack.is_empty()) return; - if (m_undo_timer == 0) - m_prev_undo_stack_size = m_undo_stack[m_undo_index].size(); + auto& undo_vector = m_undo_stack[0].m_undo_vector; - if (m_undo_timer >= 2 && m_undo_stack[m_undo_index].size() > 0) { + if (undo_vector.size() == m_last_updated_undo_vector_size && !undo_vector.is_empty()) { + auto undo_commands_container = make(); + m_undo_stack.prepend(move(undo_commands_container)); + // Note: Remove dbg() if we're 100% sure there are no bugs left. + dbg() << "Undo stack increased to " << m_undo_stack.size(); - if (m_undo_stack[m_undo_index].size() == m_prev_undo_stack_size) { - dbg() << "Increased Undo Index"; - m_undo_stack.append(make>()); - m_undo_index++; - } - m_undo_timer = -1; + // Shift the index to the left since we're adding an empty container. + if (m_undo_stack_index > 0) + m_undo_stack_index++; } - m_undo_timer++; + m_last_updated_undo_vector_size = undo_vector.size(); } void GTextEditor::set_cursor(int line, int column) @@ -998,10 +1015,8 @@ 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) @@ -1296,12 +1311,29 @@ void GTextEditor::recompute_all_visual_lines() update_content_size(); } +void GTextEditor::ensure_cursor_is_valid() +{ + if (cursor().column() > lines()[cursor().line()].length()) + set_cursor(cursor().line(), cursor().column() - (lines()[cursor().line()].length() - cursor().column())); +} + void GTextEditor::add_to_undo_stack(NonnullOwnPtr undo_command) { - if (m_undo_stack.size() <= m_undo_index) - m_undo_stack.append(make>()); + if (m_undo_stack.is_empty()) { + auto undo_commands_container = make(); + m_undo_stack.prepend(move(undo_commands_container)); + } - m_undo_stack[(m_undo_index)].insert(0, move(undo_command)); + // Clear the elements of the stack before the m_undo_stack_index (Excluding our new element) + for (int i = 1; i < m_undo_stack_index; i++) + m_undo_stack.remove(1); + + if (m_undo_stack_index > 0 && !m_undo_stack.is_empty()) + m_undo_stack[0].m_undo_vector.clear(); + + m_undo_stack_index = 0; + + m_undo_stack[0].m_undo_vector.prepend(move(undo_command)); } int GTextEditor::visual_line_containing(int line_index, int column) const @@ -1494,16 +1526,13 @@ GTextEditor::CreateLineCommand::CreateLineCommand(GTextEditor& text_editor, Vect 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)); + m_text_editor.ensure_cursor_is_valid(); } void GTextEditor::InsertCharacterCommand::redo() { - //TOOD: Redo implementation + m_text_editor.lines()[m_text_position.line()].insert(m_text_editor.document(), m_text_position.column() - 1, m_character); } void GTextEditor::RemoveCharacterCommand::undo() @@ -1513,40 +1542,58 @@ void GTextEditor::RemoveCharacterCommand::undo() void GTextEditor::RemoveCharacterCommand::redo() { - //TOOD: Redo implementation + m_text_editor.lines()[m_text_position.line()].remove(m_text_editor.document(), (m_text_position.column())); + m_text_editor.ensure_cursor_is_valid(); } void GTextEditor::RemoveLineCommand::undo() { - - //Insert back the line + // Insert back the line m_text_editor.document().insert_line(m_text_position.line(), make(m_text_editor.document(), m_line_content)); - //Remove the merged line contents - if (m_has_merged_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 + // Remove the created line + m_text_editor.document().remove_line(m_text_position.line()); + + // Add back the line contents + if (m_has_merged_content) { + for (int i = 0; i < m_line_content.length(); i++) + m_text_editor.document().lines()[m_text_position.line() - 1].insert(m_text_editor.document(), (m_text_position.column()) + i, m_line_content[i]); + } } void GTextEditor::CreateLineCommand::undo() { - //Insert back the created line portion + // 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 + // Move the cursor up a row back before the split. m_text_editor.set_cursor(m_text_position.line(), m_text_editor.document().lines()[m_text_position.line()].length()); - //Remove the created line + // Remove the created line m_text_editor.document().remove_line(m_text_position.line() + 1); } void GTextEditor::CreateLineCommand::redo() { - //TOOD: Redo implementation + // Remove the characters that we're inserted back + for (int i = m_line_content.size() - 1; i >= 0; i--) + m_text_editor.document().lines()[m_text_position.line()].remove(m_text_editor.document(), (m_text_position.column()) + i); + + m_text_editor.ensure_cursor_is_valid(); + + // Then we want to add BACK the created line + m_text_editor.document().insert_line(m_text_position.line() + 1, make(m_text_editor.document(), "")); + + for (int i = 0; i < m_line_content.size(); i++) + m_text_editor.document().lines()[m_text_position.line() + 1].insert(m_text_editor.document(), i, m_line_content[i]); } diff --git a/Libraries/LibGUI/GTextEditor.h b/Libraries/LibGUI/GTextEditor.h index 9d1dc0020a..2a67c94c48 100644 --- a/Libraries/LibGUI/GTextEditor.h +++ b/Libraries/LibGUI/GTextEditor.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -158,6 +159,7 @@ private: Rect ruler_rect_in_inner_coordinates() const; Rect visible_text_rect_in_inner_coordinates() const; void recompute_all_visual_lines(); + void ensure_cursor_is_valid(); class UndoCommand { @@ -216,6 +218,10 @@ private: GTextPosition m_text_position; }; + struct UndoCommandsContainer { + NonnullOwnPtrVector m_undo_vector; + }; + void add_to_undo_stack(NonnullOwnPtr undo_command); int visual_line_containing(int line_index, int column) const; void recompute_visual_lines(int line_index); @@ -244,10 +250,10 @@ private: RefPtr m_delete_action; CElapsedTimer m_triple_click_timer; NonnullRefPtrVector m_custom_context_menu_actions; - NonnullOwnPtrVector> m_undo_stack; - int m_undo_index = 0; - int m_undo_timer = 0; - int m_prev_undo_stack_size = 0; + NonnullOwnPtrVector m_undo_stack; + int m_undo_stack_index = 0; + RefPtr m_undo_timer; + int m_last_updated_undo_vector_size = 0; RefPtr m_document;