From 927195c32394b91a549b305881b0d8581073f2db Mon Sep 17 00:00:00 2001 From: Eugene-msc Date: Mon, 17 Nov 2014 16:34:03 +0300 Subject: [PATCH] Selection and Copy Normal, Word and Line selections. Copy of selected text. --- CMakeLists.txt | 1 + data/KeyBindings/default.ftkeys | 13 ++-- src/FinalTerm.vala | 10 ++- src/LineView.vala | 122 +++++++++++++++++++++++++++++++- src/Selection.vala | 100 ++++++++++++++++++++++++++ src/TerminalView.vala | 107 ++++++++++++++++++++++++++++ src/TerminalWidget.vala | 4 ++ 7 files changed, 345 insertions(+), 12 deletions(-) create mode 100644 src/Selection.vala diff --git a/CMakeLists.txt b/CMakeLists.txt index 1de6862..796b8e3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,6 +75,7 @@ vala_precompile(VALA_C src/Command.vala src/Settings.vala src/Metrics.vala + src/Selection.vala PACKAGES ${PKGS} posix diff --git a/data/KeyBindings/default.ftkeys b/data/KeyBindings/default.ftkeys index 54c82be..1e3638f 100644 --- a/data/KeyBindings/default.ftkeys +++ b/data/KeyBindings/default.ftkeys @@ -50,8 +50,9 @@ F11 = TOGGLE_FULLSCREEN KP_Delete = SEND_TO_SHELL "\\033[3~" Delete = SEND_TO_SHELL "\\033[3~" # TODO: Copy *selection* to clipboard -#Insert = COPY_TO_CLIPBOARD -#C = COPY_TO_CLIPBOARD + +Insert = COPY_TO_CLIPBOARD +C = COPY_TO_CLIPBOARD Insert = PASTE_FROM_CLIPBOARD V = PASTE_FROM_CLIPBOARD @@ -136,10 +137,10 @@ Right { cursor } = SEND_TO_SHELL "\\033OC" Return { crlf } = SEND_TO_SHELL "\\033\\r\\n" Return { ~crlf } = SEND_TO_SHELL "\\r" Return { crlf } = SEND_TO_SHELL "\\r\\n" -Insert { ~keypad } = SEND_TO_SHELL "\\033[4l" -Insert { keypad } = SEND_TO_SHELL "\\033[2\;2~" -Insert { ~keypad } = SEND_TO_SHELL "\\033[L" -Insert { keypad } = SEND_TO_SHELL "\\033[2\;5~" +#Insert { ~keypad } = SEND_TO_SHELL "\\033[4l" +#Insert { keypad } = SEND_TO_SHELL "\\033[2\;2~" +#Insert { ~keypad } = SEND_TO_SHELL "\\033[L" +#Insert { keypad } = SEND_TO_SHELL "\\033[2\;5~" Insert { ~keypad } = SEND_TO_SHELL "\\033[4h" Insert { keypad } = SEND_TO_SHELL "\\033[2~" Delete { ~keypad } = SEND_TO_SHELL "\\033[M" diff --git a/src/FinalTerm.vala b/src/FinalTerm.vala index 505d5d8..304709e 100644 --- a/src/FinalTerm.vala +++ b/src/FinalTerm.vala @@ -373,9 +373,13 @@ public class FinalTerm : Gtk.Application { return; case Command.CommandType.COPY_TO_CLIPBOARD: - if (command.parameters.is_empty) - return; - Utilities.set_clipboard_text(command.parameters.get(0)); + string text = active_terminal_widget.get_selected_text(); + if (text == "") { + text = command.parameters.get(0); + } + if (text != "") { + Utilities.set_clipboard_text(text); + } return; case Command.CommandType.PASTE_FROM_CLIPBOARD: diff --git a/src/LineView.vala b/src/LineView.vala index 44646c2..3b320ab 100644 --- a/src/LineView.vala +++ b/src/LineView.vala @@ -30,6 +30,13 @@ public class LineView : Clutter.Actor { private Mx.Button collapse_button = null; private Clutter.Text text_container; + private struct LineSelection { + int start; + int end; + } + + private LineSelection selection; + public bool is_prompt_line { get {return original_output_line.is_prompt_line;}} public LineView(TerminalOutput.OutputLine output_line, LineContainer line_container) { @@ -57,6 +64,8 @@ public class LineView : Clutter.Actor { on_settings_changed(null); Settings.get_default().changed.connect(on_settings_changed); + + selection = LineSelection(); } public void get_character_coordinates(int character_index, out int x, out int y) { @@ -75,6 +84,44 @@ public class LineView : Clutter.Actor { y = (int)(character_y + text_container.allocation.get_y()); } + public int get_coordinates_character(float x, float y) { + // TODO: coords_to_position seems to be buggy when working with non-latin characters + // return text_container.coords_to_position(x - text_container.get_x(), y - text_container.get_y()); + int byte_index; + int trailing; + + float text_container_x; + float text_container_y; + text_container.get_transformed_position(out text_container_x, out text_container_y); + + text_container.get_layout().xy_to_index( + (int)(x - text_container_x) * Pango.SCALE, + (int)(y - text_container_y) * Pango.SCALE, + out byte_index, out trailing); + + return Utilities.byte_index_to_character_index(output_line.get_text(), byte_index) + 1; + } + + public void set_selection(int start, int end, Selection.SelectionMode mode) { + selection.start = start; + selection.end = end; + + if (mode == Selection.SelectionMode.WORD && + (selection.start != 0 || selection.end != 0)) { + string text = output_line.get_text(); + + while (selection.start > 0 && + !" \t\n\r-:\'\"".contains(text.substring(selection.start-1, 1))) { + selection.start--; + } + while (selection.end < text.char_count() && + selection.end > 0 && + !" \t\n\r-:\'\"".contains(text.substring(selection.end, 1))) { + selection.end++; + } + } + } + private bool on_text_container_motion_event(Clutter.MotionEvent event) { // Apparently, motion event coordinates are relative to the stage // (the Clutter documentation does not specify this) @@ -167,6 +214,35 @@ public class LineView : Clutter.Actor { text_container.set_markup(get_markup(output_line)); } + public string get_selected_text() { + int element_offset = 0; + int len = 0; + int offset = 0; + string text = ""; + foreach (var text_element in output_line) { + // TODO: make this block of code less ugly + len = offset = 0; + if ((selection.end >= element_offset || selection.end == -1)&& + selection.start < element_offset + text_element.text.char_count()) { + offset = int.max(selection.start - element_offset, 0); + if (selection.end == -1 || + selection.end > element_offset + text_element.text.char_count()) { + len = text_element.text.char_count() - offset; + } else + len = selection.end - offset - element_offset; + } + + var offset_index = text_element.text.index_of_nth_char(offset); + var len_index = text_element.text.index_of_nth_char(offset + len); + + text += text_element.text.substring(offset_index, len_index - offset_index); + + element_offset += text_element.text.char_count(); + } + + return text; + } + private void update_collapse_button() { collapse_button.style = Settings.get_default().theme.style; @@ -179,20 +255,60 @@ public class LineView : Clutter.Actor { private string get_markup(TerminalOutput.OutputLine output_line) { var markup_builder = new StringBuilder(); + int element_offset = 0; + int len = 0; + int offset = 0; + foreach (var text_element in output_line) { + // TODO: make this block of code less ugly + len = offset = 0; + if ((selection.end >= element_offset || selection.end == -1)&& + selection.start < element_offset + text_element.text.char_count()) { + offset = int.max(selection.start - element_offset, 0); + if (selection.end == -1 || + selection.end > element_offset + text_element.text.char_count()) { + len = text_element.text.char_count() - offset; + } else { + len = selection.end - offset - element_offset; + } + } + + var offset_index = text_element.text.index_of_nth_char(offset); + var len_index = text_element.text.index_of_nth_char(offset + len); + var text_attributes = text_element.attributes.get_text_attributes( Settings.get_default().color_scheme, Settings.get_default().dark); var markup_attributes = text_attributes.get_markup_attributes( Settings.get_default().color_scheme, Settings.get_default().dark); + var pre_selection_text = Markup.escape_text(text_element.text.substring(0, offset_index)); + var selection_text = Markup.escape_text(text_element.text.substring(offset_index, len_index - offset_index)); + var post_selection_text = Markup.escape_text(text_element.text.substring(len_index)); + + // TODO: make selection stylable if (markup_attributes.length > 0) { markup_builder.append( "" + - Markup.escape_text(text_element.text) + - ""); + pre_selection_text + + "" + + "" + + selection_text + + "" + + "" + + post_selection_text + + "" + ); } else { - markup_builder.append(Markup.escape_text(text_element.text)); + markup_builder.append( + pre_selection_text + + "" + + selection_text + + "" + + post_selection_text + ); + } + element_offset += text_element.text.char_count(); } return markup_builder.str; diff --git a/src/Selection.vala b/src/Selection.vala new file mode 100644 index 0000000..bd68e7f --- /dev/null +++ b/src/Selection.vala @@ -0,0 +1,100 @@ +/* + * Copyright © 2013–2014 Philipp Emanuel Weidmann + * + * Nemo vir est qui mundum non reddat meliorem. + * + * + * This file is part of Final Term. + * + * Final Term is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Final Term is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Final Term. If not, see . + */ + +public class Selection { + private TerminalOutput.CursorPosition start; + private TerminalOutput.CursorPosition finish; + + public enum SelectionMode { + NORMAL, + WORD, + LINE, + COLUMN // Does not work + } + + public SelectionMode mode {get; set;} + + public Selection(TerminalOutput.CursorPosition position, SelectionMode mode) { + start = position; + this.mode = mode; + if(this.mode == SelectionMode.LINE) { + start.column = 0; + finish = start; + finish.column = 1; + } + } + + public void update(TerminalOutput.CursorPosition position) { + finish = position; + } + + public void get_line_range(int line, out int from, out int to) { + TerminalOutput.CursorPosition beginning = TerminalOutput.CursorPosition(); + TerminalOutput.CursorPosition end = TerminalOutput.CursorPosition(); + + get_range(out beginning, out end); + + from = to = 0; + + if (mode == SelectionMode.COLUMN) { + if (line >= beginning.line && line <= end.line) { + from = int.min(beginning.column, end.column); + to = int.max(beginning.column, end.column); + } + } else if (mode == SelectionMode.WORD) { + if (line == beginning.line) { + from = beginning.column; + } + if (line >= beginning.line && line < end.line) { + to = -1; + } else if (line == end.line) { + to = end.column; + } + } else if (mode == SelectionMode.LINE) { + if (line >= beginning.line && line <= end.line) { + from = 0; + to = -1; + } + } else { + if (line == beginning.line) { + from = beginning.column; + } + if (line >= beginning.line && line < end.line) { + to = -1; + } else if (line == end.line) { + to = end.column; + } + } + } + + // end position can be before or after start position + // this function returns them in correct order + public void get_range(out TerminalOutput.CursorPosition beginning, out TerminalOutput.CursorPosition end) { + if (start.compare(finish) > 0) { + beginning = finish; + end = start; + } else { + beginning = start; + end = finish; + } + } +} \ No newline at end of file diff --git a/src/TerminalView.vala b/src/TerminalView.vala index e61f0a1..5139320 100644 --- a/src/TerminalView.vala +++ b/src/TerminalView.vala @@ -113,6 +113,10 @@ public class TerminalView : Mx.BoxLayout { return (clutter_embed.get_toplevel() as Gtk.Window).has_toplevel_focus; } + public string get_selected_text() { + return terminal_output_view.get_selected_text(); + } + private void on_settings_changed(string? key) { gutter.width = Settings.get_default().theme.gutter_size; gutter.color = Settings.get_default().theme.gutter_color; @@ -146,6 +150,8 @@ public class TerminalOutputView : Mx.ScrollView { private Gee.Set updated_lines = new Gee.HashSet(); + private bool is_selecting = false; + public TerminalOutputView(Terminal terminal, GtkClutter.Embed clutter_embed) { this.terminal = terminal; this.clutter_embed = clutter_embed; @@ -210,8 +216,42 @@ public class TerminalOutputView : Mx.ScrollView { on_settings_changed(null); Settings.get_default().changed.connect(on_settings_changed); + + motion_event.connect(on_motion_event); + button_press_event.connect(on_button_press_event); + button_release_event.connect(on_button_release_event); + } + + public string get_selected_text() { + return line_container.get_selected_text(); + } + + private bool on_motion_event(Clutter.MotionEvent event) { + if (is_selecting) { + line_container.selecting(event.x, event.y); + } + return true; + } + + private bool on_button_press_event(Clutter.ButtonEvent event) { + is_selecting = true; + + Selection.SelectionMode mode = Selection.SelectionMode.NORMAL; + if(event.click_count == 2) + mode = Selection.SelectionMode.WORD; + if(event.click_count == 3) + mode = Selection.SelectionMode.LINE; + + line_container.selection_start(event.x, event.y, mode); + return true; } + private bool on_button_release_event(Clutter.ButtonEvent event) { + is_selecting = false; + line_container.selection_end(event.x, event.y); + return true; + } + private void on_menu_button_clicked() { if (menu_button.toggled) { text_menu.text = menu_button_text; @@ -561,6 +601,8 @@ public class LineContainer : Clutter.Actor, Mx.Scrollable { private Gee.List line_views = new Gee.ArrayList(); + private Selection current_selection; + // PERFORMANCE: This data structure allows for efficient determination // of which children are inside the scrolled area, making it // possible to paint only those children that are visible to the user @@ -611,6 +653,71 @@ public class LineContainer : Clutter.Actor, Mx.Scrollable { return line_views.size; } + public void selecting(float x, double y) { + current_selection.update(get_coordinates_position(x, y)); + + int from = 0; + int to = 0; + + for (int i = 0; i < get_line_count(); i++) { + if (!line_views[i].visible) { + continue; + } + + current_selection.get_line_range(i, out from, out to); + + line_views[i].set_selection(from, to, current_selection.mode); + line_views[i].render_line(); + } + + } + + public void selection_start(float x, double y, Selection.SelectionMode mode) { + current_selection = new Selection(get_coordinates_position(x, y), mode); + selecting(x,y); + } + + public void selection_end(float x, double y) { + + } + + public string get_selected_text() { + string text = ""; + TerminalOutput.CursorPosition beginning = TerminalOutput.CursorPosition(); + TerminalOutput.CursorPosition end = TerminalOutput.CursorPosition(); + current_selection.get_range(out beginning, out end); + + for (int i = beginning.line; i <= end.line; i++) { + var line_text = line_views[i].get_selected_text().strip(); + if (line_text != "") { + text += line_text + "\n"; + } + } + return text.strip(); + } + + private TerminalOutput.CursorPosition get_coordinates_position(float x, double y) { + Mx.Adjustment hadjustment = new Mx.Adjustment(); + Mx.Adjustment vadjustment = new Mx.Adjustment(); + get_adjustments(out hadjustment, out vadjustment); + + TerminalOutput.CursorPosition position = TerminalOutput.CursorPosition(); + + for (int i = 0; i < get_line_count(); i++) { + if (!line_views[i].visible) { + continue; + } + + if (line_views[i].get_height() + line_views[i].get_allocation_box().get_y() >= y + vadjustment.value) { + position.line = i; + position.column = line_views[i].get_coordinates_character(x, (float)y); + break; + } + } + + return position; + } + protected override void allocate(Clutter.ActorBox box, Clutter.AllocationFlags flags) { base.allocate(box, flags); diff --git a/src/TerminalWidget.vala b/src/TerminalWidget.vala index 05105e4..3503337 100644 --- a/src/TerminalWidget.vala +++ b/src/TerminalWidget.vala @@ -118,6 +118,10 @@ public class TerminalWidget : GtkClutter.Embed, NestingContainerChild { terminal.send_text(text); } + public string get_selected_text() { + return terminal_view.get_selected_text(); + } + public TerminalOutput.TerminalMode get_terminal_modes() { return terminal.terminal_output.terminal_modes; }