GUACAMOLE-573: Allow text selection to be expanded using Shift (fixes GUACAMOLE-191).

This commit is contained in:
Michael Jumper 2018-06-16 22:50:20 -07:00
parent c0d323828e
commit 6f08ef2a07
6 changed files with 391 additions and 140 deletions

View File

@ -37,54 +37,6 @@
#include <guacamole/socket.h>
#include <pango/pangocairo.h>
/**
* Clears the currently-selected region, removing the highlight.
*/
static void __guac_terminal_display_clear_select(guac_terminal_display* display) {
guac_socket* socket = display->client->socket;
guac_layer* select_layer = display->select_layer;
guac_protocol_send_rect(socket, select_layer, 0, 0, 1, 1);
guac_protocol_send_cfill(socket, GUAC_COMP_SRC, select_layer,
0x00, 0x00, 0x00, 0x00);
guac_client_end_frame(display->client);
guac_socket_flush(socket);
/* Text is no longer selected */
display->text_selected =
display->selection_committed = false;
}
/**
* Returns whether at least one character within the given range is selected.
*/
static bool __guac_terminal_display_selected_contains(guac_terminal_display* display,
int start_row, int start_column, int end_row, int end_column) {
/* If test range starts after highlight ends, does not intersect */
if (start_row > display->selection_end_row)
return false;
if (start_row == display->selection_end_row
&& start_column > display->selection_end_column)
return false;
/* If test range ends before highlight starts, does not intersect */
if (end_row < display->selection_start_row)
return false;
if (end_row == display->selection_start_row
&& end_column < display->selection_start_column)
return false;
/* Otherwise, does intersect */
return true;
}
/* Maps any codepoint onto a number between 0 and 511 inclusive */
int __guac_terminal_hash_codepoint(int codepoint) {
@ -310,8 +262,7 @@ guac_terminal_display* guac_terminal_display_alloc(guac_client* client,
display->operations = NULL;
/* Initially nothing selected */
display->text_selected =
display->selection_committed = false;
display->text_selected = false;
return display;
@ -413,11 +364,6 @@ void guac_terminal_display_copy_columns(guac_terminal_display* display, int row,
}
/* If selection visible and committed, clear if update touches selection */
if (display->text_selected && display->selection_committed &&
__guac_terminal_display_selected_contains(display, row, start_column, row, end_column))
__guac_terminal_display_clear_select(display);
}
void guac_terminal_display_copy_rows(guac_terminal_display* display,
@ -463,11 +409,6 @@ void guac_terminal_display_copy_rows(guac_terminal_display* display,
}
/* If selection visible and committed, clear if update touches selection */
if (display->text_selected && display->selection_committed &&
__guac_terminal_display_selected_contains(display, start_row, 0, end_row, display->width - 1))
__guac_terminal_display_clear_select(display);
}
void guac_terminal_display_set_columns(guac_terminal_display* display, int row,
@ -502,11 +443,6 @@ void guac_terminal_display_set_columns(guac_terminal_display* display, int row,
}
/* If selection visible and committed, clear if update touches selection */
if (display->text_selected && display->selection_committed &&
__guac_terminal_display_selected_contains(display, row, start_column, row, end_column))
__guac_terminal_display_clear_select(display);
}
void guac_terminal_display_resize(guac_terminal_display* display, int width, int height) {
@ -570,10 +506,6 @@ void guac_terminal_display_resize(guac_terminal_display* display, int width, int
display->char_width * width,
display->char_height * height);
/* If selection visible and committed, clear */
if (display->text_selected && display->selection_committed)
__guac_terminal_display_clear_select(display);
}
void __guac_terminal_display_flush_copy(guac_terminal_display* display) {
@ -899,16 +831,20 @@ void guac_terminal_display_dup(guac_terminal_display* display, guac_user* user,
}
void guac_terminal_display_commit_select(guac_terminal_display* display) {
display->selection_committed = true;
}
void guac_terminal_display_select(guac_terminal_display* display,
int start_row, int start_col, int end_row, int end_col) {
guac_socket* socket = display->client->socket;
guac_layer* select_layer = display->select_layer;
/* Do nothing if selection is unchanged */
if (display->text_selected
&& display->selection_start_row == start_row
&& display->selection_start_column == start_col
&& display->selection_end_row == end_row
&& display->selection_end_column == end_col)
return;
/* Text is now selected */
display->text_selected = true;
@ -989,8 +925,24 @@ void guac_terminal_display_select(guac_terminal_display* display,
guac_protocol_send_cfill(socket, GUAC_COMP_SRC, select_layer,
0x00, 0x80, 0xFF, 0x60);
guac_client_end_frame(display->client);
guac_socket_flush(socket);
}
void guac_terminal_display_clear_select(guac_terminal_display* display) {
/* Do nothing if nothing is selected */
if (!display->text_selected)
return;
guac_socket* socket = display->client->socket;
guac_layer* select_layer = display->select_layer;
guac_protocol_send_rect(socket, select_layer, 0, 0, 1, 1);
guac_protocol_send_cfill(socket, GUAC_COMP_SRC, select_layer,
0x00, 0x00, 0x00, 0x00);
/* Text is no longer selected */
display->text_selected = false;
}

View File

@ -30,21 +30,93 @@
#include <guacamole/socket.h>
#include <guacamole/unicode.h>
#include <stdbool.h>
/**
* Returns the coordinates for the currently-selected range of text within the
* given terminal, normalized such that the start coordinate is before the end
* coordinate and the end coordinate takes into account character width. If no
* text is currently selected, the behavior of this function is undefined.
*
* @param terminal
* The guac_terminal instance whose selected text coordinates should be
* retrieved in normalized form.
*
* @param start_row
* A pointer to an int which should receive the row number of the first
* character of text selected within the terminal, where the first
* (top-most) row in the terminal is row 0. Rows within the scrollback
* buffer (above the top-most row of the terminal) will be negative.
*
* @param start_col
* A pointer to an int which should receive the column number of the first
* character of text selected within terminal, where 0 is the first
* (left-most) column within the row.
*
* @param end_row
* A pointer to an int which should receive the row number of the last
* character of text selected within the terminal, where the first
* (top-most) row in the terminal is row 0. Rows within the scrollback
* buffer (above the top-most row of the terminal) will be negative.
*
* @param end_col
* A pointer to an int which should receive the column number of the first
* character of text selected within terminal, taking into account the
* width of that character, where 0 is the first (left-most) column within
* the row.
*/
static void guac_terminal_select_normalized_range(guac_terminal* terminal,
int* start_row, int* start_col, int* end_row, int* end_col) {
/* Pass through start/end coordinates if they are already in the expected
* order, adjusting only for final character width */
if (terminal->selection_start_row < terminal->selection_end_row
|| (terminal->selection_start_row == terminal->selection_end_row
&& terminal->selection_start_column < terminal->selection_end_column)) {
*start_row = terminal->selection_start_row;
*start_col = terminal->selection_start_column;
*end_row = terminal->selection_end_row;
*end_col = terminal->selection_end_column + terminal->selection_end_width - 1;
}
/* Coordinates must otherwise be swapped in addition to adjusting for
* final character width */
else {
*end_row = terminal->selection_start_row;
*end_col = terminal->selection_start_column + terminal->selection_start_width - 1;
*start_row = terminal->selection_end_row;
*start_col = terminal->selection_end_column;
}
}
void guac_terminal_select_redraw(guac_terminal* terminal) {
int start_row = terminal->selection_start_row + terminal->scroll_offset;
int start_column = terminal->selection_start_column;
/* Update the selected region of the display if text is currently
* selected */
if (terminal->text_selected) {
int end_row = terminal->selection_end_row + terminal->scroll_offset;
int end_column = terminal->selection_end_column;
int start_row = terminal->selection_start_row + terminal->scroll_offset;
int start_column = terminal->selection_start_column;
/* Update start/end columns to include character width */
if (start_row > end_row || (start_row == end_row && start_column > end_column))
start_column += terminal->selection_start_width - 1;
int end_row = terminal->selection_end_row + terminal->scroll_offset;
int end_column = terminal->selection_end_column;
/* Update start/end columns to include character width */
if (start_row > end_row || (start_row == end_row && start_column > end_column))
start_column += terminal->selection_start_width - 1;
else
end_column += terminal->selection_end_width - 1;
guac_terminal_display_select(terminal->display, start_row, start_column, end_row, end_column);
}
/* Clear the display selection if no text is currently selected */
else
end_column += terminal->selection_end_width - 1;
guac_terminal_display_select(terminal->display, start_row, start_column, end_row, end_column);
guac_terminal_display_clear_select(terminal->display);
}
@ -52,8 +124,28 @@ void guac_terminal_select_redraw(guac_terminal* terminal) {
* Locates the beginning of the character at the given row and column, updating
* the column to the starting column of that character. The width, if available,
* is returned. If the character has no defined width, 1 is returned.
*
* @param terminal
* The guac_terminal in which the character should be located.
*
* @param row
* The row number of the desired character, where the first (top-most) row
* in the terminal is row 0. Rows within the scrollback buffer (above the
* top-most row of the terminal) will be negative.
*
* @param column
* A pointer to an int containing the column number of the desired
* character, where 0 is the first (left-most) column within the row. If
* the character is a multi-column character, the value of this int will be
* adjusted as necessary such that it contains the column number of the
* first column containing the character.
*
* @return
* The width of the specified character, in columns, or 1 if the character
* has no defined width.
*/
static int __guac_terminal_find_char(guac_terminal* terminal, int row, int* column) {
static int guac_terminal_find_char(guac_terminal* terminal,
int row, int* column) {
int start_column = *column;
@ -82,7 +174,7 @@ static int __guac_terminal_find_char(guac_terminal* terminal, int row, int* colu
void guac_terminal_select_start(guac_terminal* terminal, int row, int column) {
int width = __guac_terminal_find_char(terminal, row, &column);
int width = guac_terminal_find_char(terminal, row, &column);
terminal->selection_start_row =
terminal->selection_end_row = row;
@ -93,9 +185,9 @@ void guac_terminal_select_start(guac_terminal* terminal, int row, int column) {
terminal->selection_start_width =
terminal->selection_end_width = width;
terminal->text_selected = true;
guac_terminal_select_redraw(terminal);
terminal->text_selected = false;
terminal->selection_committed = false;
guac_terminal_notify(terminal);
}
@ -106,17 +198,59 @@ void guac_terminal_select_update(guac_terminal* terminal, int row, int column) {
|| column < terminal->selection_end_column
|| column >= terminal->selection_end_column + terminal->selection_end_width) {
int width = __guac_terminal_find_char(terminal, row, &column);
int width = guac_terminal_find_char(terminal, row, &column);
terminal->selection_end_row = row;
terminal->selection_end_column = column;
terminal->selection_end_width = width;
terminal->text_selected = true;
guac_terminal_notify(terminal);
guac_terminal_select_redraw(terminal);
}
}
void guac_terminal_select_resume(guac_terminal* terminal, int row, int column) {
int selection_start_row;
int selection_start_column;
int selection_end_row;
int selection_end_column;
/* No need to test coordinates if no text is selected at all */
if (!terminal->text_selected)
return;
/* Use normalized coordinates for sake of simple comparison */
guac_terminal_select_normalized_range(terminal,
&selection_start_row, &selection_start_column,
&selection_end_row, &selection_end_column);
/* Prefer to expand from start, such that attempting to resume a selection
* within the existing selection preserves the top-most portion of the
* selection */
if (row > selection_start_row ||
(row == selection_start_row && column > selection_start_column)) {
terminal->selection_start_row = selection_start_row;
terminal->selection_start_column = selection_start_column;
}
/* Expand from bottom-most portion of selection if doing otherwise would
* reduce the size of the selection */
else {
terminal->selection_start_row = selection_end_row;
terminal->selection_start_column = selection_end_column;
}
/* Selection is again in-progress */
terminal->selection_committed = false;
/* Update selection to contain given character */
guac_terminal_select_update(terminal, row, column);
}
/**
* Appends the text within the given subsection of a terminal row to the
* clipboard. The provided coordinates are considered inclusiveley (the
@ -200,33 +334,22 @@ void guac_terminal_select_end(guac_terminal* terminal) {
guac_client* client = terminal->client;
guac_socket* socket = client->socket;
/* If no text is selected, nothing to do */
if (!terminal->text_selected)
return;
/* Selection is now committed */
terminal->selection_committed = true;
/* Reset current clipboard contents */
guac_common_clipboard_reset(terminal->clipboard, "text/plain");
/* Deselect */
terminal->text_selected = false;
guac_terminal_display_commit_select(terminal->display);
int start_row, start_col;
int end_row, end_col;
/* Ensure proper ordering of start and end coords */
if (terminal->selection_start_row < terminal->selection_end_row
|| (terminal->selection_start_row == terminal->selection_end_row
&& terminal->selection_start_column < terminal->selection_end_column)) {
start_row = terminal->selection_start_row;
start_col = terminal->selection_start_column;
end_row = terminal->selection_end_row;
end_col = terminal->selection_end_column + terminal->selection_end_width - 1;
}
else {
end_row = terminal->selection_start_row;
end_col = terminal->selection_start_column + terminal->selection_start_width - 1;
start_row = terminal->selection_end_row;
start_col = terminal->selection_end_column;
}
guac_terminal_select_normalized_range(terminal,
&start_row, &start_col, &end_row, &end_col);
/* If only one row, simply copy */
if (end_row == start_row)
@ -254,5 +377,63 @@ void guac_terminal_select_end(guac_terminal* terminal) {
guac_common_clipboard_send(terminal->clipboard, client);
guac_socket_flush(socket);
guac_terminal_notify(terminal);
}
bool guac_terminal_select_contains(guac_terminal* terminal,
int start_row, int start_column, int end_row, int end_column) {
int selection_start_row;
int selection_start_column;
int selection_end_row;
int selection_end_column;
/* No need to test coordinates if no text is selected at all */
if (!terminal->text_selected)
return false;
/* Use normalized coordinates for sake of simple comparison */
guac_terminal_select_normalized_range(terminal,
&selection_start_row, &selection_start_column,
&selection_end_row, &selection_end_column);
/* If test range starts after highlight ends, does not intersect */
if (start_row > selection_end_row)
return false;
if (start_row == selection_end_row && start_column > selection_end_column)
return false;
/* If test range ends before highlight starts, does not intersect */
if (end_row < selection_start_row)
return false;
if (end_row == selection_start_row && end_column < selection_start_column)
return false;
/* Otherwise, does intersect */
return true;
}
void guac_terminal_select_touch(guac_terminal* terminal,
int start_row, int start_column, int end_row, int end_column) {
/* Only clear selection if selection is committed */
if (!terminal->selection_committed)
return;
/* Clear selection if it contains any characters within the given region */
if (guac_terminal_select_contains(terminal, start_row, start_column,
end_row, end_column)) {
/* Text is no longer selected */
terminal->text_selected = false;
terminal->selection_committed = false;
guac_terminal_notify(terminal);
}
}

View File

@ -61,6 +61,9 @@ static void __guac_terminal_set_columns(guac_terminal* terminal, int row,
guac_terminal_buffer_set_columns(terminal->buffer, row,
start_column, end_column, character);
/* Clear selection if region is modified */
guac_terminal_select_touch(terminal, row, start_column, row, end_column);
}
/**
@ -173,6 +176,7 @@ void guac_terminal_reset(guac_terminal* term) {
/* Reset flags */
term->text_selected = false;
term->selection_committed = false;
term->application_cursor_keys = false;
term->automatic_carriage_return = false;
term->insert_mode = false;
@ -1279,6 +1283,9 @@ void guac_terminal_copy_columns(guac_terminal* terminal, int row,
guac_terminal_buffer_copy_columns(terminal->buffer, row,
start_column, end_column, offset);
/* Clear selection if region is modified */
guac_terminal_select_touch(terminal, row, start_column, row, end_column);
/* Update cursor location if within region */
if (row == terminal->visible_cursor_row &&
terminal->visible_cursor_col >= start_column &&
@ -1300,6 +1307,10 @@ void guac_terminal_copy_rows(guac_terminal* terminal,
guac_terminal_buffer_copy_rows(terminal->buffer,
start_row, end_row, offset);
/* Clear selection if region is modified */
guac_terminal_select_touch(terminal, start_row, 0, end_row,
terminal->term_width);
/* Update cursor location if within region */
if (terminal->visible_cursor_row >= start_row &&
terminal->visible_cursor_row <= end_row)
@ -1532,6 +1543,7 @@ void guac_terminal_flush(guac_terminal* terminal) {
guac_terminal_typescript_flush(terminal->typescript);
/* Flush display state */
guac_terminal_select_redraw(terminal);
guac_terminal_commit_cursor(terminal);
guac_terminal_display_flush(terminal->display);
guac_terminal_scrollbar_flush(terminal->scrollbar);
@ -1764,28 +1776,33 @@ static int __guac_terminal_send_mouse(guac_terminal* term, guac_user* user,
if ((released_mask & GUAC_CLIENT_MOUSE_RIGHT) || (released_mask & GUAC_CLIENT_MOUSE_MIDDLE))
return guac_terminal_send_data(term, term->clipboard->buffer, term->clipboard->length);
/* If text selected, change state based on left mouse mouse button */
if (term->text_selected) {
/* If left mouse button was just released, stop selection */
if (released_mask & GUAC_CLIENT_MOUSE_LEFT)
guac_terminal_select_end(term);
/* If mouse button released, stop selection */
if (released_mask & GUAC_CLIENT_MOUSE_LEFT)
guac_terminal_select_end(term);
/* Update selection state contextually while the left mouse button is
* pressed */
else if (mask & GUAC_CLIENT_MOUSE_LEFT) {
/* Otherwise, just update */
int row = y / term->display->char_height - term->scroll_offset;
int col = x / term->display->char_width;
/* If mouse button was already just pressed, start a new selection or
* resume the existing selection depending on whether shift is held */
if (pressed_mask & GUAC_CLIENT_MOUSE_LEFT) {
if (term->mod_shift)
guac_terminal_select_resume(term, row, col);
else
guac_terminal_select_start(term, row, col);
}
/* In all other cases, simply update the existing selection as long as
* the mouse button is pressed */
else
guac_terminal_select_update(term,
y / term->display->char_height - term->scroll_offset,
x / term->display->char_width);
guac_terminal_select_update(term, row, col);
}
/* Otherwise, if mouse button pressed AND moved, start selection */
else if (!(pressed_mask & GUAC_CLIENT_MOUSE_LEFT) &&
mask & GUAC_CLIENT_MOUSE_LEFT)
guac_terminal_select_start(term,
y / term->display->char_height - term->scroll_offset,
x / term->display->char_width);
/* Scroll up if wheel moved up */
if (released_mask & GUAC_CLIENT_MOUSE_SCROLL_UP)
guac_terminal_scroll_display_up(term, GUAC_TERMINAL_WHEEL_SCROLL_AMOUNT);

View File

@ -182,17 +182,10 @@ typedef struct guac_terminal_display {
guac_layer* select_layer;
/**
* Whether text is being selected.
* Whether text is currently selected.
*/
bool text_selected;
/**
* Whether the selection is finished, and will no longer be modified. A
* committed selection remains highlighted for reference, but the
* highlight will be removed when the display changes.
*/
bool selection_committed;
/**
* The row that the selection starts at.
*/
@ -333,10 +326,13 @@ void guac_terminal_display_select(guac_terminal_display* display,
int start_row, int start_col, int end_row, int end_col);
/**
* Commits the select rectangle, allowing the display to clear it when
* necessary.
* Clears the currently-selected region, removing the highlight.
*
* @param display
* The guac_terminal_display whose currently-selected region should be
* cleared.
*/
void guac_terminal_display_commit_select(guac_terminal_display* display);
void guac_terminal_display_clear_select(guac_terminal_display* display);
#endif

View File

@ -24,6 +24,20 @@
#include "config.h"
#include "terminal.h"
#include <stdbool.h>
/**
* Forwards the visible portion of the text selection rectangle to the
* underlying terminal display, requesting that it be redrawn. If no
* visible change would result from redrawing the selection rectangle,
* this function may have no effect.
*
* @param terminal
* The guac_terminal whose text selection rectangle should be
* redrawn.
*/
void guac_terminal_select_redraw(guac_terminal* terminal);
/**
* Marks the start of text selection at the given row and column. Any existing
* selection is cleared. This function should only be invoked while the
@ -65,6 +79,27 @@ void guac_terminal_select_start(guac_terminal* terminal, int row, int column);
*/
void guac_terminal_select_update(guac_terminal* terminal, int row, int column);
/**
* Resumes selecting text, expanding the existing selected region from the
* closest end to additionally contain the given character. This function
* should only be invoked while the guac_terminal is locked through a call to
* guac_terminal_lock().
*
* @param terminal
* The guac_terminal instance associated with the text being selected.
*
* @param row
* The row number of the character to include within the text selection,
* where the first (top-most) row in the terminal is row 0. Rows within the
* scrollback buffer (above the top-most row of the terminal) will be
* negative.
*
* @param column
* The column number of the character to include within the text selection,
* where the first (left-most) column in the terminal is column 0.
*/
void guac_terminal_select_resume(guac_terminal* terminal, int row, int column);
/**
* Ends text selection, removing any highlight and storing the selected
* character data within the clipboard associated with the given terminal. If
@ -78,5 +113,67 @@ void guac_terminal_select_update(guac_terminal* terminal, int row, int column);
*/
void guac_terminal_select_end(guac_terminal* terminal);
/**
* Returns whether at least one character within the given range is currently
* selected.
*
* @param terminal
* The guac_terminal instance associated with the text being selected.
*
* @param start_row
* The first row of the region to test, inclusive, where the first
* (top-most) row in the terminal is row 0. Rows within the scrollback
* buffer (above the top-most row of the terminal) will be negative.
*
* @param start_column
* The first column of the region to test, inclusive, where the first
* (left-most) column in the terminal is column 0.
*
* @param end_row
* The last row of the region to test, inclusive, where the first
* (top-most) row in the terminal is row 0. Rows within the scrollback
* buffer (above the top-most row of the terminal) will be negative.
*
* @param end_column
* The last column of the region to test, inclusive, where the first
* (left-most) column in the terminal is column 0.
*
* @return
* true if at least one character within the given range is currently
* selected, false otherwise.
*/
bool guac_terminal_select_contains(guac_terminal* terminal,
int start_row, int start_column, int end_row, int end_column);
/**
* Clears the current selection if it contains at least one character within
* the given region. If no text is currently selected, the selection has not
* yet been committed, or the region does not contain at least one selected
* character, this function has no effect.
*
* @param terminal
* The guac_terminal instance associated with the text being selected.
*
* @param start_row
* The first row of the region, inclusive, where the first (top-most) row
* in the terminal is row 0. Rows within the scrollback buffer (above the
* top-most row of the terminal) will be negative.
*
* @param start_column
* The first column of the region, inclusive, where the first (left-most)
* column in the terminal is column 0.
*
* @param end_row
* The last row of the region, inclusive, where the first (top-most) row in
* the terminal is row 0. Rows within the scrollback buffer (above the
* top-most row of the terminal) will be negative.
*
* @param end_column
* The last column of the region, inclusive, where the first (left-most)
* column in the terminal is column 0.
*/
void guac_terminal_select_touch(guac_terminal* terminal,
int start_row, int start_column, int end_row, int end_column);
#endif

View File

@ -367,10 +367,18 @@ struct guac_terminal {
int active_char_set;
/**
* Whether text is being selected.
* Whether text is currently selected.
*/
bool text_selected;
/**
* Whether the selection is finished, and will no longer be modified. A
* committed selection remains highlighted for reference, but the
* highlight will be removed if characters within the selected region are
* modified.
*/
bool selection_committed;
/**
* The row that the selection starts at.
*/