1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-30 22:48:11 +00:00

LibGUI: Support multiple GTextEditors editing the same GTextDocument

With this patch, you can now assign the same GTextDocument to multiple
GTextEditor widgets via GTextEditor::set_document().

The editors have independent cursors and selection, but all changes
are shared, and immediately propagate to all editors.

This is very unoptimized and will do lots of unnecessary invalidation,
especially line re-wrapping and repainting over and over again.
This commit is contained in:
Andreas Kling 2019-10-27 19:36:59 +01:00
parent f96c683543
commit 9b13a3905b
4 changed files with 101 additions and 58 deletions

View file

@ -5,7 +5,7 @@ GTextDocument::GTextDocument(Client* client)
{ {
if (client) if (client)
m_clients.set(client); m_clients.set(client);
append_line(make<GTextDocumentLine>()); append_line(make<GTextDocumentLine>(*this));
} }
void GTextDocument::set_text(const StringView& text) void GTextDocument::set_text(const StringView& text)
@ -17,9 +17,9 @@ void GTextDocument::set_text(const StringView& text)
auto add_line = [&](int current_position) { auto add_line = [&](int current_position) {
int line_length = current_position - start_of_current_line; int line_length = current_position - start_of_current_line;
auto line = make<GTextDocumentLine>(); auto line = make<GTextDocumentLine>(*this);
if (line_length) if (line_length)
line->set_text(text.substring_view(start_of_current_line, current_position - start_of_current_line)); line->set_text(*this, text.substring_view(start_of_current_line, current_position - start_of_current_line));
append_line(move(line)); append_line(move(line));
start_of_current_line = current_position + 1; start_of_current_line = current_position + 1;
}; };
@ -40,53 +40,56 @@ int GTextDocumentLine::first_non_whitespace_column() const
return length(); return length();
} }
GTextDocumentLine::GTextDocumentLine() GTextDocumentLine::GTextDocumentLine(GTextDocument& document)
{ {
clear(); clear(document);
} }
GTextDocumentLine::GTextDocumentLine(const StringView& text) GTextDocumentLine::GTextDocumentLine(GTextDocument& document, const StringView& text)
{ {
set_text(text); set_text(document, text);
} }
void GTextDocumentLine::clear() void GTextDocumentLine::clear(GTextDocument& document)
{ {
m_text.clear(); m_text.clear();
m_text.append(0); m_text.append(0);
document.update_views({});
} }
void GTextDocumentLine::set_text(const StringView& text) void GTextDocumentLine::set_text(GTextDocument& document, const StringView& text)
{ {
if (text.length() == length() && !memcmp(text.characters_without_null_termination(), characters(), length())) if (text.length() == length() && !memcmp(text.characters_without_null_termination(), characters(), length()))
return; return;
if (text.is_empty()) { if (text.is_empty()) {
clear(); clear(document);
return; return;
} }
m_text.resize(text.length() + 1); m_text.resize(text.length() + 1);
memcpy(m_text.data(), text.characters_without_null_termination(), text.length() + 1); memcpy(m_text.data(), text.characters_without_null_termination(), text.length() + 1);
document.update_views({});
} }
void GTextDocumentLine::append(const char* characters, int length) void GTextDocumentLine::append(GTextDocument& document, const char* characters, int length)
{ {
int old_length = m_text.size() - 1; int old_length = m_text.size() - 1;
m_text.resize(m_text.size() + length); m_text.resize(m_text.size() + length);
memcpy(m_text.data() + old_length, characters, length); memcpy(m_text.data() + old_length, characters, length);
m_text.last() = 0; m_text.last() = 0;
document.update_views({});
} }
void GTextDocumentLine::append(char ch) void GTextDocumentLine::append(GTextDocument& document, char ch)
{ {
insert(length(), ch); insert(document, length(), ch);
} }
void GTextDocumentLine::prepend(char ch) void GTextDocumentLine::prepend(GTextDocument& document, char ch)
{ {
insert(0, ch); insert(document, 0, ch);
} }
void GTextDocumentLine::insert(int index, char ch) void GTextDocumentLine::insert(GTextDocument& document, int index, char ch)
{ {
if (index == length()) { if (index == length()) {
m_text.last() = ch; m_text.last() = ch;
@ -94,9 +97,10 @@ void GTextDocumentLine::insert(int index, char ch)
} else { } else {
m_text.insert(index, move(ch)); m_text.insert(index, move(ch));
} }
document.update_views({});
} }
void GTextDocumentLine::remove(int index) void GTextDocumentLine::remove(GTextDocument& document, int index)
{ {
if (index == length()) { if (index == length()) {
m_text.take_last(); m_text.take_last();
@ -104,12 +108,14 @@ void GTextDocumentLine::remove(int index)
} else { } else {
m_text.remove(index); m_text.remove(index);
} }
document.update_views({});
} }
void GTextDocumentLine::truncate(int length) void GTextDocumentLine::truncate(GTextDocument& document, int length)
{ {
m_text.resize(length + 1); m_text.resize(length + 1);
m_text.last() = 0; m_text.last() = 0;
document.update_views({});
} }
void GTextDocument::append_line(NonnullOwnPtr<GTextDocumentLine> line) void GTextDocument::append_line(NonnullOwnPtr<GTextDocumentLine> line)
@ -153,3 +159,9 @@ void GTextDocument::unregister_client(Client& client)
{ {
m_clients.remove(&client); m_clients.remove(&client);
} }
void GTextDocument::update_views(Badge<GTextDocumentLine>)
{
for (auto* client : m_clients)
client->document_did_change();
}

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <AK/Badge.h>
#include <AK/HashTable.h> #include <AK/HashTable.h>
#include <AK/NonnullOwnPtrVector.h> #include <AK/NonnullOwnPtrVector.h>
#include <AK/NonnullRefPtr.h> #include <AK/NonnullRefPtr.h>
@ -26,6 +27,7 @@ public:
virtual void document_did_insert_line(int) = 0; virtual void document_did_insert_line(int) = 0;
virtual void document_did_remove_line(int) = 0; virtual void document_did_remove_line(int) = 0;
virtual void document_did_remove_all_lines() = 0; virtual void document_did_remove_all_lines() = 0;
virtual void document_did_change() = 0;
}; };
static NonnullRefPtr<GTextDocument> create(Client* client = nullptr) static NonnullRefPtr<GTextDocument> create(Client* client = nullptr)
@ -55,6 +57,8 @@ public:
void register_client(Client&); void register_client(Client&);
void unregister_client(Client&); void unregister_client(Client&);
void update_views(Badge<GTextDocumentLine>);
private: private:
explicit GTextDocument(Client* client); explicit GTextDocument(Client* client);
@ -69,20 +73,20 @@ class GTextDocumentLine {
friend class GTextDocument; friend class GTextDocument;
public: public:
explicit GTextDocumentLine(); explicit GTextDocumentLine(GTextDocument&);
explicit GTextDocumentLine(const StringView&); explicit GTextDocumentLine(GTextDocument&, const StringView&);
StringView view() const { return { characters(), length() }; } StringView view() const { return { characters(), length() }; }
const char* characters() const { return m_text.data(); } const char* characters() const { return m_text.data(); }
int length() const { return m_text.size() - 1; } int length() const { return m_text.size() - 1; }
void set_text(const StringView&); void set_text(GTextDocument&, const StringView&);
void append(char); void append(GTextDocument&, char);
void prepend(char); void prepend(GTextDocument&, char);
void insert(int index, char); void insert(GTextDocument&, int index, char);
void remove(int index); void remove(GTextDocument&, int index);
void append(const char*, int); void append(GTextDocument&, const char*, int);
void truncate(int length); void truncate(GTextDocument&, int length);
void clear(); void clear(GTextDocument&);
int first_non_whitespace_column() const; int first_non_whitespace_column() const;
private: private:

View file

@ -19,8 +19,7 @@ GTextEditor::GTextEditor(Type type, GWidget* parent)
: GScrollableWidget(parent) : GScrollableWidget(parent)
, m_type(type) , m_type(type)
{ {
m_document = GTextDocument::create(this); set_document(GTextDocument::create());
m_document->register_client(*this);
set_frame_shape(FrameShape::Container); set_frame_shape(FrameShape::Container);
set_frame_shadow(FrameShadow::Sunken); set_frame_shadow(FrameShadow::Sunken);
set_frame_thickness(2); set_frame_thickness(2);
@ -632,7 +631,7 @@ void GTextEditor::keydown_event(GKeyEvent& event)
// Backspace within line // Backspace within line
for (int i = 0; i < erase_count; ++i) { for (int i = 0; i < erase_count; ++i) {
current_line().remove(m_cursor.column() - 1 - i); current_line().remove(document(), m_cursor.column() - 1 - i);
} }
update_content_size(); update_content_size();
set_cursor(m_cursor.line(), m_cursor.column() - erase_count); set_cursor(m_cursor.line(), m_cursor.column() - erase_count);
@ -643,9 +642,8 @@ void GTextEditor::keydown_event(GKeyEvent& event)
// Backspace at column 0; merge with previous line // Backspace at column 0; merge with previous line
auto& previous_line = lines()[m_cursor.line() - 1]; auto& previous_line = lines()[m_cursor.line() - 1];
int previous_length = previous_line.length(); int previous_length = previous_line.length();
previous_line.append(current_line().characters(), current_line().length()); previous_line.append(document(), current_line().characters(), current_line().length());
lines().remove(m_cursor.line()); document().remove_line(m_cursor.line());
m_line_visual_data.remove(m_cursor.line());
update_content_size(); update_content_size();
update(); update();
set_cursor(m_cursor.line() - 1, previous_length); set_cursor(m_cursor.line() - 1, previous_length);
@ -678,10 +676,9 @@ void GTextEditor::delete_current_line()
if (has_selection()) if (has_selection())
return delete_selection(); return delete_selection();
lines().remove(m_cursor.line()); document().remove_line(m_cursor.line());
m_line_visual_data.remove(m_cursor.line());
if (lines().is_empty()) if (lines().is_empty())
document().append_line(make<GTextDocumentLine>()); document().append_line(make<GTextDocumentLine>(document()));
update_content_size(); update_content_size();
update(); update();
@ -697,7 +694,7 @@ void GTextEditor::do_delete()
if (m_cursor.column() < current_line().length()) { if (m_cursor.column() < current_line().length()) {
// Delete within line // Delete within line
current_line().remove(m_cursor.column()); current_line().remove(document(), m_cursor.column());
did_change(); did_change();
update_cursor(); update_cursor();
return; return;
@ -706,9 +703,8 @@ void GTextEditor::do_delete()
// Delete at end of line; merge with next line // Delete at end of line; merge with next line
auto& next_line = lines()[m_cursor.line() + 1]; auto& next_line = lines()[m_cursor.line() + 1];
int previous_length = current_line().length(); int previous_length = current_line().length();
current_line().append(next_line.characters(), next_line.length()); current_line().append(document(), next_line.characters(), next_line.length());
lines().remove(m_cursor.line() + 1); document().remove_line(m_cursor.line() + 1);
m_line_visual_data.remove(m_cursor.line() + 1);
update(); update();
did_change(); did_change();
set_cursor(m_cursor.line(), previous_length); set_cursor(m_cursor.line(), previous_length);
@ -743,15 +739,15 @@ void GTextEditor::insert_at_cursor(char ch)
if (leading_spaces) if (leading_spaces)
new_line_contents = String::repeated(' ', leading_spaces); new_line_contents = String::repeated(' ', leading_spaces);
} }
document().insert_line(m_cursor.line() + (at_tail ? 1 : 0), make<GTextDocumentLine>(new_line_contents)); document().insert_line(m_cursor.line() + (at_tail ? 1 : 0), make<GTextDocumentLine>(document(), new_line_contents));
update(); update();
did_change(); did_change();
set_cursor(m_cursor.line() + 1, lines()[m_cursor.line() + 1].length()); set_cursor(m_cursor.line() + 1, lines()[m_cursor.line() + 1].length());
return; return;
} }
auto new_line = make<GTextDocumentLine>(); auto new_line = make<GTextDocumentLine>(document());
new_line->append(current_line().characters() + m_cursor.column(), current_line().length() - m_cursor.column()); new_line->append(document(), current_line().characters() + m_cursor.column(), current_line().length() - m_cursor.column());
current_line().truncate(m_cursor.column()); current_line().truncate(document(), m_cursor.column());
document().insert_line(m_cursor.line() + 1, move(new_line)); document().insert_line(m_cursor.line() + 1, move(new_line));
update(); update();
did_change(); did_change();
@ -762,13 +758,13 @@ void GTextEditor::insert_at_cursor(char ch)
int next_soft_tab_stop = ((m_cursor.column() + m_soft_tab_width) / m_soft_tab_width) * m_soft_tab_width; int next_soft_tab_stop = ((m_cursor.column() + m_soft_tab_width) / m_soft_tab_width) * m_soft_tab_width;
int spaces_to_insert = next_soft_tab_stop - m_cursor.column(); int spaces_to_insert = next_soft_tab_stop - m_cursor.column();
for (int i = 0; i < spaces_to_insert; ++i) { for (int i = 0; i < spaces_to_insert; ++i) {
current_line().insert(m_cursor.column(), ' '); current_line().insert(document(), m_cursor.column(), ' ');
} }
did_change(); did_change();
set_cursor(m_cursor.line(), next_soft_tab_stop); set_cursor(m_cursor.line(), next_soft_tab_stop);
return; return;
} }
current_line().insert(m_cursor.column(), ch); current_line().insert(document(), m_cursor.column(), ch);
did_change(); did_change();
set_cursor(m_cursor.line(), m_cursor.column() + 1); set_cursor(m_cursor.line(), m_cursor.column() + 1);
} }
@ -993,9 +989,8 @@ String GTextEditor::text() const
void GTextEditor::clear() void GTextEditor::clear()
{ {
lines().clear(); document().remove_all_lines();
m_line_visual_data.clear(); document().append_line(make<GTextDocumentLine>(document()));
document().append_line(make<GTextDocumentLine>());
m_selection.clear(); m_selection.clear();
did_update_selection(); did_update_selection();
set_cursor(0, 0); set_cursor(0, 0);
@ -1030,8 +1025,7 @@ void GTextEditor::delete_selection()
// First delete all the lines in between the first and last one. // First delete all the lines in between the first and last one.
for (int i = selection.start().line() + 1; i < selection.end().line();) { for (int i = selection.start().line() + 1; i < selection.end().line();) {
lines().remove(i); document().remove_line(i);
m_line_visual_data.remove(i);
selection.end().set_line(selection.end().line() - 1); selection.end().set_line(selection.end().line() - 1);
} }
@ -1040,14 +1034,14 @@ void GTextEditor::delete_selection()
auto& line = lines()[selection.start().line()]; auto& line = lines()[selection.start().line()];
bool whole_line_is_selected = selection.start().column() == 0 && selection.end().column() == line.length(); bool whole_line_is_selected = selection.start().column() == 0 && selection.end().column() == line.length();
if (whole_line_is_selected) { if (whole_line_is_selected) {
line.clear(); line.clear(document());
} else { } else {
auto before_selection = String(line.characters(), line.length()).substring(0, selection.start().column()); auto before_selection = String(line.characters(), line.length()).substring(0, selection.start().column());
auto after_selection = String(line.characters(), line.length()).substring(selection.end().column(), line.length() - selection.end().column()); auto after_selection = String(line.characters(), line.length()).substring(selection.end().column(), line.length() - selection.end().column());
StringBuilder builder(before_selection.length() + after_selection.length()); StringBuilder builder(before_selection.length() + after_selection.length());
builder.append(before_selection); builder.append(before_selection);
builder.append(after_selection); builder.append(after_selection);
line.set_text(builder.to_string()); line.set_text(document(), builder.to_string());
} }
} else { } else {
// Delete across a newline, merging lines. // Delete across a newline, merging lines.
@ -1059,13 +1053,12 @@ void GTextEditor::delete_selection()
StringBuilder builder(before_selection.length() + after_selection.length()); StringBuilder builder(before_selection.length() + after_selection.length());
builder.append(before_selection); builder.append(before_selection);
builder.append(after_selection); builder.append(after_selection);
first_line.set_text(builder.to_string()); first_line.set_text(document(), builder.to_string());
lines().remove(selection.end().line()); document().remove_line(selection.end().line());
m_line_visual_data.remove(selection.end().line());
} }
if (lines().is_empty()) { if (lines().is_empty()) {
document().append_line(make<GTextDocumentLine>()); document().append_line(make<GTextDocumentLine>(document()));
} }
m_selection.clear(); m_selection.clear();
@ -1415,20 +1408,50 @@ void GTextEditor::did_change_font()
void GTextEditor::document_did_append_line() void GTextEditor::document_did_append_line()
{ {
m_line_visual_data.append(make<LineVisualData>()); m_line_visual_data.append(make<LineVisualData>());
recompute_all_visual_lines();
update();
} }
void GTextEditor::document_did_remove_line(int line_index) void GTextEditor::document_did_remove_line(int line_index)
{ {
m_line_visual_data.remove(line_index); m_line_visual_data.remove(line_index);
recompute_all_visual_lines();
update();
} }
void GTextEditor::document_did_remove_all_lines() void GTextEditor::document_did_remove_all_lines()
{ {
m_line_visual_data.clear(); m_line_visual_data.clear();
recompute_all_visual_lines();
update();
} }
void GTextEditor::document_did_insert_line(int line_index) void GTextEditor::document_did_insert_line(int line_index)
{ {
m_line_visual_data.insert(line_index, make<LineVisualData>()); m_line_visual_data.insert(line_index, make<LineVisualData>());
recompute_all_visual_lines();
update();
} }
void GTextEditor::document_did_change()
{
recompute_all_visual_lines();
update();
}
void GTextEditor::set_document(GTextDocument& document)
{
if (m_document.ptr() == &document)
return;
if (m_document)
m_document->unregister_client(*this);
m_document = document;
m_line_visual_data.clear();
for (int i = 0; i < m_document->line_count(); ++i) {
m_line_visual_data.append(make<LineVisualData>());
}
m_cursor = { 0, 0 };
recompute_all_visual_lines();
update();
m_document->register_client(*this);
}

View file

@ -37,6 +37,8 @@ public:
const GTextDocument& document() const { return *m_document; } const GTextDocument& document() const { return *m_document; }
GTextDocument& document() { return *m_document; } GTextDocument& document() { return *m_document; }
void set_document(GTextDocument&);
bool is_readonly() const { return m_readonly; } bool is_readonly() const { return m_readonly; }
void set_readonly(bool); void set_readonly(bool);
@ -131,10 +133,12 @@ protected:
private: private:
friend class GTextDocumentLine; friend class GTextDocumentLine;
// ^GTextDocument::Client
virtual void document_did_append_line() override; virtual void document_did_append_line() override;
virtual void document_did_insert_line(int) override; virtual void document_did_insert_line(int) override;
virtual void document_did_remove_line(int) override; virtual void document_did_remove_line(int) override;
virtual void document_did_remove_all_lines() override; virtual void document_did_remove_all_lines() override;
virtual void document_did_change() override;
void create_actions(); void create_actions();
void paint_ruler(Painter&); void paint_ruler(Painter&);