diff --git a/Libraries/LibGUI/GTextEditor.cpp b/Libraries/LibGUI/GTextEditor.cpp index ff0654a22c..282b58d3a9 100644 --- a/Libraries/LibGUI/GTextEditor.cpp +++ b/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(*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(*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 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(*this, line_content, GTextPosition(row, column))); + document().insert_line(m_cursor.line() + (at_tail ? 1 : 0), make(document(), new_line_contents)); update(); did_change(); @@ -747,6 +788,14 @@ void GTextEditor::insert_at_cursor(char ch) } auto new_line = make(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 line_content; + for (int i = 0; i < new_line->length(); i++) + line_content.append(new_line->characters()[i]); + add_to_undo_stack(make(*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(*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>()); + 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(*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(*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(*this, document().line(row).characters()[column], GTextPosition(row, column))); + } + + add_to_undo_stack(make(*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(*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 undo_command) +{ + if (m_undo_stack.size() <= m_undo_index) + m_undo_stack.append(make>()); + + 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 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(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 +} diff --git a/Libraries/LibGUI/GTextEditor.h b/Libraries/LibGUI/GTextEditor.h index c4bcb456c0..9d1dc0020a 100644 --- a/Libraries/LibGUI/GTextEditor.h +++ b/Libraries/LibGUI/GTextEditor.h @@ -79,6 +79,8 @@ public: void do_delete(); void delete_current_line(); void select_all(); + void undo(); + void redo(); Function on_change; Function 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& lines() const { return document().lines(); } NonnullOwnPtrVector& 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 line_content, GTextPosition text_position); + virtual void undo() override; + virtual void redo() override; + + private: + Vector m_line_content; + GTextPosition m_text_position; + }; + + 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); @@ -183,6 +244,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; RefPtr m_document;