From 08c04f0a41135057e1a8bbec33094df0df404c2b Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sun, 23 Jun 2019 09:18:17 +0200 Subject: [PATCH] Terminal: Add basic mouse selection with copy and paste. Left mouse button selects (and copies the selection on mouse up). The right mouse button then pastes whatever's on the clipboard. I always liked this behavior in PuTTY, so now we have it here as well :^) --- Applications/Terminal/Terminal.cpp | 99 ++++++++++++++++++++++++++++-- Applications/Terminal/Terminal.h | 61 ++++++++++++++++++ 2 files changed, 156 insertions(+), 4 deletions(-) diff --git a/Applications/Terminal/Terminal.cpp b/Applications/Terminal/Terminal.cpp index 45c019fab9..45e0b0b04c 100644 --- a/Applications/Terminal/Terminal.cpp +++ b/Applications/Terminal/Terminal.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -1068,16 +1069,17 @@ void Terminal::paint_event(GPaintEvent& event) painter.fill_rect(row_rect(row), lookup_color(line.attributes[0].background_color).with_alpha(255 * m_opacity)); for (word column = 0; column < m_columns; ++column) { char ch = line.characters[column]; - bool should_reverse_fill_for_cursor = m_cursor_blink_state && m_in_active_window && row == m_cursor_row && column == m_cursor_column; + bool should_reverse_fill_for_cursor_or_selection = (m_cursor_blink_state && m_in_active_window && row == m_cursor_row && column == m_cursor_column) + || selection_contains({ row, column }); auto& attribute = line.attributes[column]; auto character_rect = glyph_rect(row, column); - if (!has_only_one_background_color || should_reverse_fill_for_cursor) { + if (!has_only_one_background_color || should_reverse_fill_for_cursor_or_selection) { auto cell_rect = character_rect.inflated(0, m_line_spacing); - painter.fill_rect(cell_rect, lookup_color(should_reverse_fill_for_cursor ? attribute.foreground_color : attribute.background_color).with_alpha(255 * m_opacity)); + painter.fill_rect(cell_rect, lookup_color(should_reverse_fill_for_cursor_or_selection ? attribute.foreground_color : attribute.background_color).with_alpha(255 * m_opacity)); } if (ch == ' ') continue; - painter.draw_glyph(character_rect.location(), ch, lookup_color(should_reverse_fill_for_cursor ? attribute.background_color : attribute.foreground_color)); + painter.draw_glyph(character_rect.location(), ch, lookup_color(should_reverse_fill_for_cursor_or_selection ? attribute.background_color : attribute.foreground_color)); } } @@ -1150,3 +1152,92 @@ void Terminal::set_opacity(float opacity) m_opacity = opacity; force_repaint(); } + +BufferPosition Terminal::normalized_selection_start() const +{ + if (m_selection_start < m_selection_end) + return m_selection_start; + return m_selection_end; +} + +BufferPosition Terminal::normalized_selection_end() const +{ + if (m_selection_start < m_selection_end) + return m_selection_end; + return m_selection_start; +} + +bool Terminal::has_selection() const +{ + return m_selection_start.is_valid() && m_selection_end.is_valid(); +} + +bool Terminal::selection_contains(const BufferPosition& position) const +{ + if (!has_selection()) + return false; + + return position >= normalized_selection_start() && position <= normalized_selection_end(); +} + +BufferPosition Terminal::buffer_position_at(const Point& position) const +{ + auto adjusted_position = position.translated(-(frame_thickness() + m_inset), -(frame_thickness() + m_inset)); + int row = adjusted_position.y() / m_line_height; + int column = adjusted_position.x() / font().glyph_width('x'); + return { row, column }; +} + +void Terminal::mousedown_event(GMouseEvent& event) +{ + if (event.button() == GMouseButton::Left) { + m_selection_start = buffer_position_at(event.position()); + m_selection_end = {}; + update(); + } else if (event.button() == GMouseButton::Right) { + auto text = GClipboard::the().data(); + if (text.is_empty()) + return; + int nwritten = write(m_ptm_fd, text.characters(), text.length()); + if (nwritten < 0) { + perror("write"); + ASSERT_NOT_REACHED(); + } + } +} + +void Terminal::mousemove_event(GMouseEvent& event) +{ + if (!(event.buttons() & GMouseButton::Left)) + return; + + auto old_selection_end = m_selection_end; + m_selection_end = buffer_position_at(event.position()); + if (old_selection_end != m_selection_end) + update(); +} + +void Terminal::mouseup_event(GMouseEvent& event) +{ + if (event.button() != GMouseButton::Left) + return; + if (!has_selection()) + return; + GClipboard::the().set_data(selected_text()); +} + +String Terminal::selected_text() const +{ + StringBuilder builder; + auto start = normalized_selection_start(); + auto end = normalized_selection_end(); + + for (int row = start.row(); row <= end.row(); ++row) { + int first_column = row == start.row() ? start.column() : 0; + int last_column = row == end.row() ? end.column() : m_columns - 1; + for (int column = first_column; column <= last_column; ++column) + builder.append(line(row).characters[column]); + } + + return builder.to_string(); +} diff --git a/Applications/Terminal/Terminal.h b/Applications/Terminal/Terminal.h index 388570589b..5e584ab9aa 100644 --- a/Applications/Terminal/Terminal.h +++ b/Applications/Terminal/Terminal.h @@ -12,6 +12,49 @@ class Font; +class BufferPosition { +public: + BufferPosition() {} + BufferPosition(int row, int column) + : m_row(row) + , m_column(column) + { + } + + bool is_valid() const { return m_row >= 0 && m_column >= 0; } + int row() const { return m_row; } + int column() const { return m_column; } + + bool operator<(const BufferPosition& other) const + { + return m_row < other.m_row || (m_row == other.m_row && m_column < other.m_column); + } + + bool operator<=(const BufferPosition& other) const + { + return *this < other || *this == other; + } + + bool operator>=(const BufferPosition& other) const + { + return !(*this < other); + } + + bool operator==(const BufferPosition& other) const + { + return m_row == other.m_row && m_column == other.m_column; + } + + bool operator!=(const BufferPosition& other) const + { + return !(*this == other); + } + +private: + int m_row { -1 }; + int m_column { -1 }; +}; + class Terminal final : public GFrame { public: explicit Terminal(int ptm_fd, RefPtr config); @@ -32,6 +75,13 @@ public: RefPtr config() const { return m_config; } + bool has_selection() const; + bool selection_contains(const BufferPosition&) const; + String selected_text() const; + BufferPosition buffer_position_at(const Point&) const; + BufferPosition normalized_selection_start() const; + BufferPosition normalized_selection_end() const; + private: typedef Vector ParamVector; @@ -39,6 +89,9 @@ private: virtual void paint_event(GPaintEvent&) override; virtual void resize_event(GResizeEvent&) override; virtual void keydown_event(GKeyEvent&) override; + virtual void mousedown_event(GMouseEvent&) override; + virtual void mousemove_event(GMouseEvent&) override; + virtual void mouseup_event(GMouseEvent&) override; virtual const char* class_name() const override { return "Terminal"; } void scroll_up(); @@ -139,9 +192,17 @@ private: ASSERT(index < m_rows); return *m_lines[index]; } + const Line& line(size_t index) const + { + ASSERT(index < m_rows); + return *m_lines[index]; + } Vector> m_lines; + BufferPosition m_selection_start; + BufferPosition m_selection_end; + int m_scroll_region_top { 0 }; int m_scroll_region_bottom { 0 };