ladybird/Libraries/LibGUI/GTextEditor.cpp
Andreas Kling 00a91bb02c LibGUI: Consolidate and simplify commands used for insertion/removal
This patch adds InsertTextCommand and RemoveTextCommand.
These two commands are used to ... insert and remove text :^)

The bulk of the logic is moved into GTextDocument, and we now use the
command's redo() virtual to perform the action. Or in other words, when
you type into the text editor, we create an InsertTextCommand, push it
onto the undo stack, and call redo() on it immediately. That's how the
text gets inserted.

This makes it quite easy to implement more commands, as there is no
distinction between a redo() and the initial application.
2019-11-30 16:54:05 +01:00

1419 lines
46 KiB
C++

#include <AK/QuickSort.h>
#include <AK/StringBuilder.h>
#include <Kernel/KeyCode.h>
#include <LibGUI/GAction.h>
#include <LibGUI/GClipboard.h>
#include <LibGUI/GFontDatabase.h>
#include <LibGUI/GMenu.h>
#include <LibGUI/GPainter.h>
#include <LibGUI/GScrollBar.h>
#include <LibGUI/GTextEditor.h>
#include <LibGUI/GWindow.h>
#include <ctype.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
//#define DEBUG_GTEXTEDITOR
GTextEditor::GTextEditor(GWidget* parent)
: GTextEditor(Type::MultiLine, parent)
{
}
GTextEditor::GTextEditor(Type type, GWidget* parent)
: GScrollableWidget(parent)
, m_type(type)
{
set_document(GTextDocument::create());
set_frame_shape(FrameShape::Container);
set_frame_shadow(FrameShadow::Sunken);
set_frame_thickness(2);
set_scrollbars_enabled(is_multi_line());
set_font(GFontDatabase::the().get_by_name("Csilla Thin"));
// FIXME: Recompute vertical scrollbar step size on font change.
vertical_scrollbar().set_step(line_height());
m_cursor = { 0, 0 };
create_actions();
}
GTextEditor::~GTextEditor()
{
if (m_document)
m_document->unregister_client(*this);
}
void GTextEditor::create_actions()
{
m_undo_action = GCommonActions::make_undo_action([&](auto&) { undo(); }, this);
m_redo_action = GCommonActions::make_redo_action([&](auto&) { redo(); }, this);
m_undo_action->set_enabled(false);
m_redo_action->set_enabled(false);
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);
m_delete_action = GCommonActions::make_delete_action([&](auto&) { do_delete(); }, this);
}
void GTextEditor::set_text(const StringView& text)
{
if (is_single_line() && text.length() == line(0).length() && !memcmp(text.characters_without_null_termination(), line(0).characters(), text.length()))
return;
m_selection.clear();
document().set_text(text);
update_content_size();
recompute_all_visual_lines();
if (is_single_line())
set_cursor(0, line(0).length());
else
set_cursor(0, 0);
did_update_selection();
update();
}
void GTextEditor::update_content_size()
{
int content_width = 0;
int content_height = 0;
for (auto& line : m_line_visual_data) {
content_width = max(line.visual_rect.width(), content_width);
content_height += line.visual_rect.height();
}
content_width += m_horizontal_content_padding * 2;
if (is_right_text_alignment(m_text_alignment))
content_width = max(frame_inner_rect().width(), content_width);
set_content_size({ content_width, content_height });
set_size_occupied_by_fixed_elements({ ruler_width(), 0 });
}
GTextPosition GTextEditor::text_position_at(const Point& a_position) const
{
auto position = a_position;
position.move_by(horizontal_scrollbar().value(), vertical_scrollbar().value());
position.move_by(-(m_horizontal_content_padding + ruler_width()), 0);
position.move_by(-frame_thickness(), -frame_thickness());
int line_index = -1;
if (is_line_wrapping_enabled()) {
for (int i = 0; i < lines().size(); ++i) {
auto& rect = m_line_visual_data[i].visual_rect;
if (position.y() >= rect.top() && position.y() <= rect.bottom()) {
line_index = i;
break;
}
if (position.y() > rect.bottom())
line_index = lines().size() - 1;
}
} else {
line_index = position.y() / line_height();
}
line_index = max(0, min(line_index, line_count() - 1));
int column_index;
switch (m_text_alignment) {
case TextAlignment::CenterLeft:
column_index = (position.x() + glyph_width() / 2) / glyph_width();
if (is_line_wrapping_enabled()) {
for_each_visual_line(line_index, [&](const Rect& rect, const StringView&, int start_of_line) {
if (rect.contains_vertically(position.y())) {
column_index += start_of_line;
return IterationDecision::Break;
}
return IterationDecision::Continue;
});
}
break;
case TextAlignment::CenterRight:
// FIXME: Support right-aligned line wrapping, I guess.
ASSERT(!is_line_wrapping_enabled());
column_index = (position.x() - content_x_for_position({ line_index, 0 }) + glyph_width() / 2) / glyph_width();
break;
default:
ASSERT_NOT_REACHED();
}
column_index = max(0, min(column_index, lines()[line_index].length()));
return { line_index, column_index };
}
void GTextEditor::doubleclick_event(GMouseEvent& event)
{
if (event.button() != GMouseButton::Left)
return;
// NOTE: This ensures that spans are updated before we look at them.
flush_pending_change_notification_if_needed();
m_triple_click_timer.start();
m_in_drag_select = false;
auto start = text_position_at(event.position());
auto end = start;
auto& line = lines()[start.line()];
if (!document().has_spans()) {
while (start.column() > 0) {
if (isspace(line.characters()[start.column() - 1]))
break;
start.set_column(start.column() - 1);
}
while (end.column() < line.length()) {
if (isspace(line.characters()[end.column()]))
break;
end.set_column(end.column() + 1);
}
} else {
for (auto& span : document().spans()) {
if (!span.range.contains(start))
continue;
start = span.range.start();
end = span.range.end();
end.set_column(end.column() + 1);
break;
}
}
m_selection.set(start, end);
set_cursor(end);
update();
did_update_selection();
}
void GTextEditor::mousedown_event(GMouseEvent& event)
{
if (event.button() != GMouseButton::Left) {
return;
}
if (m_triple_click_timer.is_valid() && m_triple_click_timer.elapsed() < 250) {
m_triple_click_timer = CElapsedTimer();
GTextPosition start;
GTextPosition end;
if (is_multi_line()) {
// select *current* line
start = GTextPosition(m_cursor.line(), 0);
end = GTextPosition(m_cursor.line(), lines()[m_cursor.line()].length());
} else {
// select *whole* line
start = GTextPosition(0, 0);
end = GTextPosition(line_count() - 1, lines()[line_count() - 1].length());
}
m_selection.set(start, end);
set_cursor(end);
return;
}
if (event.modifiers() & Mod_Shift) {
if (!has_selection())
m_selection.set(m_cursor, {});
} else {
m_selection.clear();
}
m_in_drag_select = true;
set_cursor(text_position_at(event.position()));
if (!(event.modifiers() & Mod_Shift)) {
if (!has_selection())
m_selection.set(m_cursor, {});
}
if (m_selection.start().is_valid() && m_selection.start() != m_cursor)
m_selection.set_end(m_cursor);
// FIXME: Only update the relevant rects.
update();
did_update_selection();
}
void GTextEditor::mouseup_event(GMouseEvent& event)
{
if (event.button() == GMouseButton::Left) {
if (m_in_drag_select) {
m_in_drag_select = false;
}
return;
}
}
void GTextEditor::mousemove_event(GMouseEvent& event)
{
if (m_in_drag_select) {
set_cursor(text_position_at(event.position()));
m_selection.set_end(m_cursor);
did_update_selection();
update();
return;
}
}
int GTextEditor::ruler_width() const
{
if (!m_ruler_visible)
return 0;
// FIXME: Resize based on needed space.
return 5 * font().glyph_width('x') + 4;
}
Rect GTextEditor::ruler_content_rect(int line_index) const
{
if (!m_ruler_visible)
return {};
return {
0 - ruler_width() + horizontal_scrollbar().value(),
line_content_rect(line_index).y(),
ruler_width(),
line_content_rect(line_index).height()
};
}
Rect GTextEditor::ruler_rect_in_inner_coordinates() const
{
return { 0, 0, ruler_width(), height() - height_occupied_by_horizontal_scrollbar() };
}
Rect GTextEditor::visible_text_rect_in_inner_coordinates() const
{
return {
m_horizontal_content_padding + (m_ruler_visible ? (ruler_rect_in_inner_coordinates().right() + 1) : 0),
0,
frame_inner_rect().width() - (m_horizontal_content_padding * 2) - width_occupied_by_vertical_scrollbar() - ruler_width(),
frame_inner_rect().height() - height_occupied_by_horizontal_scrollbar()
};
}
void GTextEditor::paint_event(GPaintEvent& event)
{
// NOTE: This ensures that spans are updated before we look at them.
flush_pending_change_notification_if_needed();
GFrame::paint_event(event);
GPainter painter(*this);
painter.add_clip_rect(widget_inner_rect());
painter.add_clip_rect(event.rect());
painter.fill_rect(event.rect(), Color::White);
painter.translate(frame_thickness(), frame_thickness());
auto ruler_rect = ruler_rect_in_inner_coordinates();
if (m_ruler_visible) {
painter.fill_rect(ruler_rect, Color::WarmGray);
painter.draw_line(ruler_rect.top_right(), ruler_rect.bottom_right(), Color::DarkGray);
}
painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value());
if (m_ruler_visible)
painter.translate(ruler_width(), 0);
int first_visible_line = text_position_at(event.rect().top_left()).line();
int last_visible_line = text_position_at(event.rect().bottom_right()).line();
auto selection = normalized_selection();
bool has_selection = selection.is_valid();
if (m_ruler_visible) {
for (int i = first_visible_line; i <= last_visible_line; ++i) {
bool is_current_line = i == m_cursor.line();
auto ruler_line_rect = ruler_content_rect(i);
painter.draw_text(
ruler_line_rect.shrunken(2, 0).translated(0, m_line_spacing / 2),
String::number(i + 1),
is_current_line ? Font::default_bold_font() : font(),
TextAlignment::TopRight,
is_current_line ? Color::DarkGray : Color::MidGray);
}
}
Rect text_clip_rect {
(m_ruler_visible ? (ruler_rect_in_inner_coordinates().right() + frame_thickness() + 1) : frame_thickness()),
frame_thickness(),
width() - width_occupied_by_vertical_scrollbar() - ruler_width(),
height() - height_occupied_by_horizontal_scrollbar()
};
painter.add_clip_rect(text_clip_rect);
for (int line_index = first_visible_line; line_index <= last_visible_line; ++line_index) {
auto& line = lines()[line_index];
bool physical_line_has_selection = has_selection && line_index >= selection.start().line() && line_index <= selection.end().line();
int first_visual_line_with_selection = -1;
int last_visual_line_with_selection = -1;
if (physical_line_has_selection) {
if (selection.start().line() < line_index)
first_visual_line_with_selection = 0;
else
first_visual_line_with_selection = visual_line_containing(line_index, selection.start().column());
if (selection.end().line() > line_index)
last_visual_line_with_selection = m_line_visual_data[line_index].visual_line_breaks.size();
else
last_visual_line_with_selection = visual_line_containing(line_index, selection.end().column());
}
int selection_start_column_within_line = selection.start().line() == line_index ? selection.start().column() : 0;
int selection_end_column_within_line = selection.end().line() == line_index ? selection.end().column() : line.length();
int visual_line_index = 0;
for_each_visual_line(line_index, [&](const Rect& visual_line_rect, const StringView& visual_line_text, int start_of_visual_line) {
if (is_multi_line() && line_index == m_cursor.line())
painter.fill_rect(visual_line_rect, Color(230, 230, 230));
#ifdef DEBUG_GTEXTEDITOR
painter.draw_rect(visual_line_rect, Color::Cyan);
#endif
if (!document().has_spans()) {
// Fast-path for plain text
painter.draw_text(visual_line_rect, visual_line_text, m_text_alignment, Color::Black);
} else {
int advance = font().glyph_width(' ') + font().glyph_spacing();
Rect character_rect = { visual_line_rect.location(), { font().glyph_width(' '), line_height() } };
for (int i = 0; i < visual_line_text.length(); ++i) {
const Font* font = &this->font();
Color color;
Optional<Color> background_color;
GTextPosition physical_position(line_index, start_of_visual_line + i);
// FIXME: This is *horribly* inefficient.
for (auto& span : document().spans()) {
if (!span.range.contains(physical_position))
continue;
color = span.color;
if (span.font)
font = span.font;
background_color = span.background_color;
break;
}
if (background_color.has_value())
painter.fill_rect(character_rect, background_color.value());
painter.draw_text(character_rect, visual_line_text.substring_view(i, 1), *font, m_text_alignment, color);
character_rect.move_by(advance, 0);
}
}
bool physical_line_has_selection = has_selection && line_index >= selection.start().line() && line_index <= selection.end().line();
if (physical_line_has_selection) {
bool current_visual_line_has_selection = (line_index != selection.start().line() && line_index != selection.end().line())
|| (visual_line_index >= first_visual_line_with_selection && visual_line_index <= last_visual_line_with_selection);
if (current_visual_line_has_selection) {
bool selection_begins_on_current_visual_line = visual_line_index == first_visual_line_with_selection;
bool selection_ends_on_current_visual_line = visual_line_index == last_visual_line_with_selection;
int selection_left = selection_begins_on_current_visual_line
? content_x_for_position({ line_index, selection_start_column_within_line })
: m_horizontal_content_padding;
int selection_right = selection_ends_on_current_visual_line
? content_x_for_position({ line_index, selection_end_column_within_line })
: visual_line_rect.right() + 1;
Rect selection_rect {
selection_left,
visual_line_rect.y(),
selection_right - selection_left,
visual_line_rect.height()
};
painter.fill_rect(selection_rect, Color::from_rgb(0x955233));
int start_of_selection_within_visual_line = max(0, selection_start_column_within_line - start_of_visual_line);
int end_of_selection_within_visual_line = selection_end_column_within_line - start_of_visual_line;
StringView visual_selected_text {
visual_line_text.characters_without_null_termination() + start_of_selection_within_visual_line,
end_of_selection_within_visual_line - start_of_selection_within_visual_line
};
painter.draw_text(selection_rect, visual_selected_text, TextAlignment::CenterLeft, Color::White);
}
}
++visual_line_index;
return IterationDecision::Continue;
});
}
if (is_focused() && m_cursor_state)
painter.fill_rect(cursor_content_rect(), Color::Red);
}
void GTextEditor::toggle_selection_if_needed_for_event(const GKeyEvent& event)
{
if (event.shift() && !m_selection.is_valid()) {
m_selection.set(m_cursor, {});
did_update_selection();
update();
return;
}
if (!event.shift() && m_selection.is_valid()) {
m_selection.clear();
did_update_selection();
update();
return;
}
}
void GTextEditor::select_all()
{
GTextPosition start_of_document { 0, 0 };
GTextPosition end_of_document { line_count() - 1, lines()[line_count() - 1].length() };
m_selection.set(start_of_document, end_of_document);
did_update_selection();
set_cursor(end_of_document);
update();
}
void GTextEditor::get_selection_line_boundaries(int& first_line, int& last_line)
{
auto selection = normalized_selection();
if (!selection.is_valid()) {
first_line = m_cursor.line();
last_line = m_cursor.line();
return;
}
first_line = selection.start().line();
last_line = selection.end().line();
if (first_line != last_line && selection.end().column() == 0)
last_line -= 1;
}
void GTextEditor::move_selected_lines_up()
{
int first_line;
int last_line;
get_selection_line_boundaries(first_line, last_line);
if (first_line == 0)
return;
auto& lines = document().lines();
lines.insert(last_line, lines.take(first_line - 1));
m_cursor = { first_line - 1, 0 };
if (has_selection()) {
m_selection.set_start({ first_line - 1, 0 });
m_selection.set_end({ last_line - 1, line(last_line - 1).length() });
}
did_change();
update();
}
void GTextEditor::move_selected_lines_down()
{
int first_line;
int last_line;
get_selection_line_boundaries(first_line, last_line);
auto& lines = document().lines();
if (last_line >= (lines.size() - 1))
return;
lines.insert(first_line, lines.take(last_line + 1));
m_cursor = { first_line + 1, 0 };
if (has_selection()) {
m_selection.set_start({ first_line + 1, 0 });
m_selection.set_end({ last_line + 1, line(last_line + 1).length() });
}
did_change();
update();
}
void GTextEditor::sort_selected_lines()
{
if (is_readonly())
return;
if (!has_selection())
return;
int first_line;
int last_line;
get_selection_line_boundaries(first_line, last_line);
auto& lines = document().lines();
auto start = lines.begin() + first_line;
auto end = lines.begin() + last_line + 1;
quick_sort(start, end, [](auto& a, auto& b) {
return strcmp(a.characters(), b.characters()) < 0;
});
did_change();
update();
}
void GTextEditor::keydown_event(GKeyEvent& event)
{
if (is_single_line() && event.key() == KeyCode::Key_Tab)
return GWidget::keydown_event(event);
if (is_single_line() && event.key() == KeyCode::Key_Return) {
if (on_return_pressed)
on_return_pressed();
return;
}
if (event.key() == KeyCode::Key_Escape) {
if (on_escape_pressed)
on_escape_pressed();
return;
}
if (event.key() == KeyCode::Key_Up) {
if (m_cursor.line() > 0) {
if (event.ctrl() && event.shift()) {
move_selected_lines_up();
return;
}
int new_line = m_cursor.line() - 1;
int new_column = min(m_cursor.column(), lines()[new_line].length());
toggle_selection_if_needed_for_event(event);
set_cursor(new_line, new_column);
if (event.shift() && m_selection.start().is_valid()) {
m_selection.set_end(m_cursor);
did_update_selection();
}
}
return;
}
if (event.key() == KeyCode::Key_Down) {
if (m_cursor.line() < (lines().size() - 1)) {
if (event.ctrl() && event.shift()) {
move_selected_lines_down();
return;
}
int new_line = m_cursor.line() + 1;
int new_column = min(m_cursor.column(), lines()[new_line].length());
toggle_selection_if_needed_for_event(event);
set_cursor(new_line, new_column);
if (event.shift() && m_selection.start().is_valid()) {
m_selection.set_end(m_cursor);
did_update_selection();
}
}
return;
}
if (event.key() == KeyCode::Key_PageUp) {
if (m_cursor.line() > 0) {
int new_line = max(0, m_cursor.line() - visible_content_rect().height() / line_height());
int new_column = min(m_cursor.column(), lines()[new_line].length());
toggle_selection_if_needed_for_event(event);
set_cursor(new_line, new_column);
if (event.shift() && m_selection.start().is_valid()) {
m_selection.set_end(m_cursor);
did_update_selection();
}
}
return;
}
if (event.key() == KeyCode::Key_PageDown) {
if (m_cursor.line() < (lines().size() - 1)) {
int new_line = min(line_count() - 1, m_cursor.line() + visible_content_rect().height() / line_height());
int new_column = min(m_cursor.column(), lines()[new_line].length());
toggle_selection_if_needed_for_event(event);
set_cursor(new_line, new_column);
if (event.shift() && m_selection.start().is_valid()) {
m_selection.set_end(m_cursor);
did_update_selection();
}
}
return;
}
if (event.key() == KeyCode::Key_Left) {
if (event.ctrl() && document().has_spans()) {
// FIXME: Do something nice when the document has no spans.
auto span = document().first_non_skippable_span_before(m_cursor);
GTextPosition new_cursor = !span.has_value()
? GTextPosition(0, 0)
: span.value().range.start();
toggle_selection_if_needed_for_event(event);
set_cursor(new_cursor);
if (event.shift() && m_selection.start().is_valid()) {
m_selection.set_end(m_cursor);
did_update_selection();
}
return;
}
if (m_cursor.column() > 0) {
int new_column = m_cursor.column() - 1;
toggle_selection_if_needed_for_event(event);
set_cursor(m_cursor.line(), new_column);
if (event.shift() && m_selection.start().is_valid()) {
m_selection.set_end(m_cursor);
did_update_selection();
}
} else if (m_cursor.line() > 0) {
int new_line = m_cursor.line() - 1;
int new_column = lines()[new_line].length();
toggle_selection_if_needed_for_event(event);
set_cursor(new_line, new_column);
if (event.shift() && m_selection.start().is_valid()) {
m_selection.set_end(m_cursor);
did_update_selection();
}
}
return;
}
if (event.key() == KeyCode::Key_Right) {
if (event.ctrl() && document().has_spans()) {
// FIXME: Do something nice when the document has no spans.
auto span = document().first_non_skippable_span_after(m_cursor);
GTextPosition new_cursor = !span.has_value()
? document().spans().last().range.end()
: span.value().range.start();
toggle_selection_if_needed_for_event(event);
set_cursor(new_cursor);
if (event.shift() && m_selection.start().is_valid()) {
m_selection.set_end(m_cursor);
did_update_selection();
}
return;
}
int new_line = m_cursor.line();
int new_column = m_cursor.column();
if (m_cursor.column() < current_line().length()) {
new_line = m_cursor.line();
new_column = m_cursor.column() + 1;
} else if (m_cursor.line() != line_count() - 1) {
new_line = m_cursor.line() + 1;
new_column = 0;
}
toggle_selection_if_needed_for_event(event);
set_cursor(new_line, new_column);
if (event.shift() && m_selection.start().is_valid()) {
m_selection.set_end(m_cursor);
did_update_selection();
}
return;
}
if (!event.ctrl() && event.key() == KeyCode::Key_Home) {
int first_nonspace_column = current_line().first_non_whitespace_column();
toggle_selection_if_needed_for_event(event);
if (m_cursor.column() == first_nonspace_column)
set_cursor(m_cursor.line(), 0);
else
set_cursor(m_cursor.line(), first_nonspace_column);
if (event.shift() && m_selection.start().is_valid()) {
m_selection.set_end(m_cursor);
did_update_selection();
}
return;
}
if (!event.ctrl() && event.key() == KeyCode::Key_End) {
toggle_selection_if_needed_for_event(event);
set_cursor(m_cursor.line(), current_line().length());
if (event.shift() && m_selection.start().is_valid()) {
m_selection.set_end(m_cursor);
did_update_selection();
}
return;
}
if (event.ctrl() && event.key() == KeyCode::Key_Home) {
toggle_selection_if_needed_for_event(event);
set_cursor(0, 0);
if (event.shift() && m_selection.start().is_valid()) {
m_selection.set_end(m_cursor);
did_update_selection();
}
return;
}
if (event.ctrl() && event.key() == KeyCode::Key_End) {
toggle_selection_if_needed_for_event(event);
set_cursor(line_count() - 1, lines()[line_count() - 1].length());
if (event.shift() && m_selection.start().is_valid()) {
m_selection.set_end(m_cursor);
did_update_selection();
}
return;
}
if (event.modifiers() == Mod_Ctrl && event.key() == KeyCode::Key_A) {
select_all();
return;
}
if (event.alt() && event.shift() && event.key() == KeyCode::Key_S) {
sort_selected_lines();
return;
}
if (event.key() == KeyCode::Key_Backspace) {
if (is_readonly())
return;
if (has_selection()) {
delete_selection();
did_update_selection();
return;
}
if (m_cursor.column() > 0) {
int erase_count = 1;
if (current_line().first_non_whitespace_column() >= m_cursor.column()) {
int new_column;
if (m_cursor.column() % m_soft_tab_width == 0)
new_column = m_cursor.column() - m_soft_tab_width;
else
new_column = (m_cursor.column() / m_soft_tab_width) * m_soft_tab_width;
erase_count = m_cursor.column() - new_column;
}
// Backspace within line
GTextRange erased_range({ m_cursor.line(), m_cursor.column() - erase_count }, m_cursor);
auto erased_text = document().text_in_range(erased_range);
execute<RemoveTextCommand>(erased_text, erased_range);
return;
}
if (m_cursor.column() == 0 && m_cursor.line() != 0) {
// Backspace at column 0; merge with previous line
int previous_length = line(m_cursor.line() - 1).length();
GTextRange erased_range({ m_cursor.line() - 1, previous_length }, m_cursor);
execute<RemoveTextCommand>("\n", erased_range);
return;
}
return;
}
if (event.modifiers() == Mod_Shift && event.key() == KeyCode::Key_Delete) {
if (is_readonly())
return;
delete_current_line();
return;
}
if (event.key() == KeyCode::Key_Delete) {
if (is_readonly())
return;
do_delete();
return;
}
if (!is_readonly() && !event.ctrl() && !event.alt() && !event.text().is_empty())
insert_at_cursor_or_replace_selection(event.text());
}
void GTextEditor::delete_current_line()
{
if (has_selection())
return delete_selection();
document().remove_line(m_cursor.line());
if (lines().is_empty())
document().append_line(make<GTextDocumentLine>(document()));
m_cursor.set_column(0);
update_content_size();
update();
}
void GTextEditor::do_delete()
{
if (is_readonly())
return;
if (has_selection())
return delete_selection();
if (m_cursor.column() < current_line().length()) {
// Delete within line
current_line().remove(document(), m_cursor.column());
did_change();
update_cursor();
return;
}
if (m_cursor.column() == current_line().length() && m_cursor.line() != line_count() - 1) {
// Delete at end of line; merge with next line
auto& next_line = lines()[m_cursor.line() + 1];
int previous_length = current_line().length();
current_line().append(document(), next_line.characters(), next_line.length());
document().remove_line(m_cursor.line() + 1);
update();
did_change();
set_cursor(m_cursor.line(), previous_length);
return;
}
}
int GTextEditor::content_x_for_position(const GTextPosition& position) const
{
auto& line = lines()[position.line()];
int x_offset = -1;
switch (m_text_alignment) {
case TextAlignment::CenterLeft:
for_each_visual_line(position.line(), [&](const Rect&, const StringView& view, int start_of_visual_line) {
if (position.column() >= start_of_visual_line && ((position.column() - start_of_visual_line) <= view.length())) {
x_offset = (position.column() - start_of_visual_line) * glyph_width();
return IterationDecision::Break;
}
return IterationDecision::Continue;
});
return m_horizontal_content_padding + x_offset;
case TextAlignment::CenterRight:
// FIXME
ASSERT(!is_line_wrapping_enabled());
return content_width() - m_horizontal_content_padding - (line.length() * glyph_width()) + (position.column() * glyph_width());
default:
ASSERT_NOT_REACHED();
}
}
Rect GTextEditor::content_rect_for_position(const GTextPosition& position) const
{
if (!position.is_valid())
return {};
ASSERT(!lines().is_empty());
ASSERT(position.column() <= (current_line().length() + 1));
int x = content_x_for_position(position);
if (is_single_line()) {
Rect rect { x, 0, 1, font().glyph_height() + 2 };
rect.center_vertically_within({ {}, frame_inner_rect().size() });
return rect;
}
Rect rect;
for_each_visual_line(position.line(), [&](const Rect& visual_line_rect, const StringView& view, int start_of_visual_line) {
if (position.column() >= start_of_visual_line && ((position.column() - start_of_visual_line) <= view.length())) {
// NOTE: We have to subtract the horizontal padding here since it's part of the visual line rect
// *and* included in what we get from content_x_for_position().
rect = {
visual_line_rect.x() + x - (m_horizontal_content_padding),
visual_line_rect.y(),
1,
line_height()
};
return IterationDecision::Break;
}
return IterationDecision::Continue;
});
return rect;
}
Rect GTextEditor::cursor_content_rect() const
{
return content_rect_for_position(m_cursor);
}
Rect GTextEditor::line_widget_rect(int line_index) const
{
auto rect = line_content_rect(line_index);
rect.set_x(frame_thickness());
rect.set_width(frame_inner_rect().width());
rect.move_by(0, -(vertical_scrollbar().value()));
rect.move_by(0, frame_thickness());
rect.intersect(frame_inner_rect());
return rect;
}
void GTextEditor::scroll_position_into_view(const GTextPosition& position)
{
auto rect = content_rect_for_position(position);
if (position.column() == 0)
rect.set_x(content_x_for_position({ position.line(), 0 }) - 2);
else if (position.column() == lines()[position.line()].length())
rect.set_x(content_x_for_position({ position.line(), lines()[position.line()].length() }) + 2);
scroll_into_view(rect, true, true);
}
void GTextEditor::scroll_cursor_into_view()
{
scroll_position_into_view(m_cursor);
}
Rect GTextEditor::line_content_rect(int line_index) const
{
auto& line = lines()[line_index];
if (is_single_line()) {
Rect line_rect = { content_x_for_position({ line_index, 0 }), 0, line.length() * glyph_width(), font().glyph_height() + 2 };
line_rect.center_vertically_within({ {}, frame_inner_rect().size() });
return line_rect;
}
if (is_line_wrapping_enabled())
return m_line_visual_data[line_index].visual_rect;
return {
content_x_for_position({ line_index, 0 }),
line_index * line_height(),
line.length() * glyph_width(),
line_height()
};
}
void GTextEditor::update_cursor()
{
update(line_widget_rect(m_cursor.line()));
}
void GTextEditor::set_cursor(int line, int column)
{
set_cursor({ line, column });
}
void GTextEditor::set_cursor(const GTextPosition& a_position)
{
ASSERT(!lines().is_empty());
GTextPosition position = a_position;
if (position.line() >= lines().size())
position.set_line(lines().size() - 1);
if (position.column() > lines()[position.line()].length())
position.set_column(lines()[position.line()].length());
if (m_cursor != position) {
// NOTE: If the old cursor is no longer valid, repaint everything just in case.
auto old_cursor_line_rect = m_cursor.line() < lines().size()
? line_widget_rect(m_cursor.line())
: rect();
m_cursor = position;
m_cursor_state = true;
scroll_cursor_into_view();
update(old_cursor_line_rect);
update_cursor();
}
cursor_did_change();
if (on_cursor_change)
on_cursor_change();
}
void GTextEditor::focusin_event(CEvent&)
{
update_cursor();
start_timer(500);
}
void GTextEditor::focusout_event(CEvent&)
{
stop_timer();
}
void GTextEditor::timer_event(CTimerEvent&)
{
m_cursor_state = !m_cursor_state;
if (is_focused())
update_cursor();
}
bool GTextEditor::write_to_file(const StringView& path)
{
int fd = open_with_path_length(path.characters_without_null_termination(), path.length(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return false;
}
// Compute the final file size and ftruncate() to make writing fast.
// FIXME: Remove this once the kernel is smart enough to do this instead.
off_t file_size = 0;
for (int i = 0; i < lines().size(); ++i)
file_size += lines()[i].length();
file_size += lines().size() - 1;
int rc = ftruncate(fd, file_size);
if (rc < 0) {
perror("ftruncate");
return false;
}
for (int i = 0; i < lines().size(); ++i) {
auto& line = lines()[i];
if (line.length()) {
ssize_t nwritten = write(fd, line.characters(), line.length());
if (nwritten < 0) {
perror("write");
close(fd);
return false;
}
}
if (i != lines().size() - 1) {
char ch = '\n';
ssize_t nwritten = write(fd, &ch, 1);
if (nwritten != 1) {
perror("write");
close(fd);
return false;
}
}
}
close(fd);
return true;
}
String GTextEditor::text() const
{
StringBuilder builder;
for (int i = 0; i < line_count(); ++i) {
auto& line = lines()[i];
builder.append(line.characters(), line.length());
if (i != line_count() - 1)
builder.append('\n');
}
return builder.to_string();
}
void GTextEditor::clear()
{
document().remove_all_lines();
document().append_line(make<GTextDocumentLine>(document()));
m_selection.clear();
did_update_selection();
set_cursor(0, 0);
update();
}
String GTextEditor::selected_text() const
{
if (!has_selection())
return {};
return document().text_in_range(m_selection);
}
void GTextEditor::delete_selection()
{
auto selection = normalized_selection();
execute<RemoveTextCommand>(selected_text(), selection);
m_selection.clear();
did_update_selection();
did_change();
set_cursor(selection.start());
update();
}
void GTextEditor::insert_at_cursor_or_replace_selection(const StringView& text)
{
ASSERT(!is_readonly());
if (has_selection())
delete_selection();
execute<InsertTextCommand>(text, m_cursor);
}
void GTextEditor::cut()
{
if (is_readonly())
return;
auto selected_text = this->selected_text();
printf("Cut: \"%s\"\n", selected_text.characters());
GClipboard::the().set_data(selected_text);
delete_selection();
}
void GTextEditor::copy()
{
auto selected_text = this->selected_text();
printf("Copy: \"%s\"\n", selected_text.characters());
GClipboard::the().set_data(selected_text);
}
void GTextEditor::paste()
{
if (is_readonly())
return;
auto paste_text = GClipboard::the().data();
printf("Paste: \"%s\"\n", paste_text.characters());
insert_at_cursor_or_replace_selection(paste_text);
}
void GTextEditor::enter_event(CEvent&)
{
ASSERT(window());
window()->set_override_cursor(GStandardCursor::IBeam);
}
void GTextEditor::leave_event(CEvent&)
{
ASSERT(window());
window()->set_override_cursor(GStandardCursor::None);
}
void GTextEditor::did_change()
{
ASSERT(!is_readonly());
update_content_size();
recompute_all_visual_lines();
m_undo_action->set_enabled(can_undo());
m_redo_action->set_enabled(can_redo());
if (!m_has_pending_change_notification) {
m_has_pending_change_notification = true;
deferred_invoke([this](auto&) {
if (!m_has_pending_change_notification)
return;
if (on_change)
on_change();
m_has_pending_change_notification = false;
});
}
}
void GTextEditor::set_readonly(bool readonly)
{
if (m_readonly == readonly)
return;
m_readonly = readonly;
m_cut_action->set_enabled(!is_readonly() && has_selection());
m_delete_action->set_enabled(!is_readonly());
m_paste_action->set_enabled(!is_readonly());
}
void GTextEditor::did_update_selection()
{
m_cut_action->set_enabled(!is_readonly() && has_selection());
m_copy_action->set_enabled(has_selection());
if (on_selection_change)
on_selection_change();
if (is_line_wrapping_enabled()) {
// FIXME: Try to repaint less.
update();
}
}
void GTextEditor::context_menu_event(GContextMenuEvent& event)
{
if (!m_context_menu) {
m_context_menu = make<GMenu>();
m_context_menu->add_action(undo_action());
m_context_menu->add_action(redo_action());
m_context_menu->add_separator();
m_context_menu->add_action(cut_action());
m_context_menu->add_action(copy_action());
m_context_menu->add_action(paste_action());
m_context_menu->add_action(delete_action());
if (!m_custom_context_menu_actions.is_empty()) {
m_context_menu->add_separator();
for (auto& action : m_custom_context_menu_actions) {
m_context_menu->add_action(action);
}
}
}
m_context_menu->popup(event.screen_position());
}
void GTextEditor::set_text_alignment(TextAlignment alignment)
{
if (m_text_alignment == alignment)
return;
m_text_alignment = alignment;
update();
}
void GTextEditor::resize_event(GResizeEvent& event)
{
GScrollableWidget::resize_event(event);
update_content_size();
recompute_all_visual_lines();
}
void GTextEditor::set_selection(const GTextRange& selection)
{
if (m_selection == selection)
return;
m_selection = selection;
set_cursor(m_selection.end());
scroll_position_into_view(normalized_selection().start());
update();
}
void GTextEditor::recompute_all_visual_lines()
{
int y_offset = 0;
for (int line_index = 0; line_index < line_count(); ++line_index) {
recompute_visual_lines(line_index);
m_line_visual_data[line_index].visual_rect.set_y(y_offset);
y_offset += m_line_visual_data[line_index].visual_rect.height();
}
update_content_size();
}
void GTextEditor::ensure_cursor_is_valid()
{
auto new_cursor = m_cursor;
if (new_cursor.line() >= lines().size())
new_cursor.set_line(lines().size() - 1);
if (new_cursor.column() > lines()[new_cursor.line()].length())
new_cursor.set_column(lines()[new_cursor.line()].length());
if (m_cursor != new_cursor)
set_cursor(new_cursor);
}
int GTextEditor::visual_line_containing(int line_index, int column) const
{
int visual_line_index = 0;
for_each_visual_line(line_index, [&](const Rect&, const StringView& view, int start_of_visual_line) {
if (column >= start_of_visual_line && ((column - start_of_visual_line) < view.length()))
return IterationDecision::Break;
++visual_line_index;
return IterationDecision::Continue;
});
return visual_line_index;
}
void GTextEditor::recompute_visual_lines(int line_index)
{
auto& line = document().line(line_index);
auto& visual_data = m_line_visual_data[line_index];
visual_data.visual_line_breaks.clear_with_capacity();
int available_width = visible_text_rect_in_inner_coordinates().width();
if (is_line_wrapping_enabled()) {
int line_width_so_far = 0;
for (int i = 0; i < line.length(); ++i) {
auto ch = line.characters()[i];
auto glyph_width = font().glyph_width(ch);
if ((line_width_so_far + glyph_width) > available_width) {
visual_data.visual_line_breaks.append(i);
line_width_so_far = glyph_width;
continue;
}
line_width_so_far += glyph_width;
}
}
visual_data.visual_line_breaks.append(line.length());
if (is_line_wrapping_enabled())
visual_data.visual_rect = { m_horizontal_content_padding, 0, available_width, visual_data.visual_line_breaks.size() * line_height() };
else
visual_data.visual_rect = { m_horizontal_content_padding, 0, font().width(line.view()), line_height() };
}
template<typename Callback>
void GTextEditor::for_each_visual_line(int line_index, Callback callback) const
{
auto editor_visible_text_rect = visible_text_rect_in_inner_coordinates();
int start_of_line = 0;
int visual_line_index = 0;
auto& line = document().line(line_index);
auto& visual_data = m_line_visual_data[line_index];
for (auto visual_line_break : visual_data.visual_line_breaks) {
auto visual_line_view = StringView(line.characters() + start_of_line, visual_line_break - start_of_line);
Rect visual_line_rect {
visual_data.visual_rect.x(),
visual_data.visual_rect.y() + (visual_line_index * line_height()),
font().width(visual_line_view),
line_height()
};
if (is_right_text_alignment(text_alignment()))
visual_line_rect.set_right_without_resize(editor_visible_text_rect.right());
if (!is_multi_line())
visual_line_rect.center_vertically_within(editor_visible_text_rect);
if (callback(visual_line_rect, visual_line_view, start_of_line) == IterationDecision::Break)
break;
start_of_line = visual_line_break;
++visual_line_index;
}
}
void GTextEditor::set_line_wrapping_enabled(bool enabled)
{
if (m_line_wrapping_enabled == enabled)
return;
m_line_wrapping_enabled = enabled;
horizontal_scrollbar().set_visible(!m_line_wrapping_enabled);
update_content_size();
recompute_all_visual_lines();
update();
}
void GTextEditor::add_custom_context_menu_action(GAction& action)
{
m_custom_context_menu_actions.append(action);
}
void GTextEditor::did_change_font()
{
vertical_scrollbar().set_step(line_height());
GWidget::did_change_font();
}
void GTextEditor::document_did_append_line()
{
m_line_visual_data.append(make<LineVisualData>());
recompute_all_visual_lines();
update();
}
void GTextEditor::document_did_remove_line(int line_index)
{
m_line_visual_data.remove(line_index);
recompute_all_visual_lines();
update();
}
void GTextEditor::document_did_remove_all_lines()
{
m_line_visual_data.clear();
recompute_all_visual_lines();
update();
}
void GTextEditor::document_did_insert_line(int line_index)
{
m_line_visual_data.insert(line_index, make<LineVisualData>());
recompute_all_visual_lines();
update();
}
void GTextEditor::document_did_change()
{
ensure_cursor_is_valid();
recompute_all_visual_lines();
update();
undo_action().set_enabled(can_undo());
redo_action().set_enabled(can_redo());
}
void GTextEditor::document_did_set_text()
{
m_line_visual_data.clear();
for (int i = 0; i < m_document->line_count(); ++i)
m_line_visual_data.append(make<LineVisualData>());
document_did_change();
}
void GTextEditor::document_did_set_cursor(const GTextPosition& position)
{
set_cursor(position);
}
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);
}
void GTextEditor::flush_pending_change_notification_if_needed()
{
if (!m_has_pending_change_notification)
return;
if (on_change)
on_change();
m_has_pending_change_notification = false;
}