terminal: clipboard support

Fixes #2079
This commit is contained in:
Norman Feske 2019-06-25 16:46:04 +02:00 committed by Christian Helmuth
parent 6399fc12ac
commit fc7b983a40
6 changed files with 323 additions and 5 deletions

View File

@ -5,5 +5,6 @@ input_session
nitpicker_gfx
terminal_session
timer_session
report_session
vfs
gems

View File

@ -1,14 +1,14 @@
This is a graphical terminal implementation. It provides the Terminal
service and uses a nitpicker session for screen representation.
Configuration
~~~~~~~~~~~~~
Color configuration
~~~~~~~~~~~~~~~~~~~
The default color palette can be configured via the <palette> XML
configuration node like follows. There are 16 colors configurable -
index 0-7 normal color and index 8-15 bright (bold) colors.
! <config>
! <palette>
! <color index="0" value="#000000"/> <!-- black is real black -->
@ -17,3 +17,17 @@ index 0-7 normal color and index 8-15 bright (bold) colors.
! ...
! </config>
Clipboard support
~~~~~~~~~~~~~~~~~
With the '<config>' attribute 'copy="yes"' specified, the terminal allows
the user to select text to be reported to a "clipboard" report. The selection
mode is activated by holding the left shift key. While the selection mode
is active, the text position under mouse pointer is highlighted and the
user can select text via the left mouse button. Upon release of the mouse
button, the selection is reported.
Vice versa, with the '<config>' attribute 'paste="yes"' specified, the
terminal allows the user to paste the content of a "clipboard" ROM session
to the terminal client by pressing the middle mouse button.

View File

@ -21,6 +21,7 @@
#include <base/attached_rom_dataspace.h>
#include <base/attached_ram_dataspace.h>
#include <input/event.h>
#include <os/reporter.h>
#include <gems/vfs.h>
#include <gems/vfs_font.h>
#include <gems/cached_font.h>
@ -72,6 +73,9 @@ struct Terminal::Main : Character_consumer
Color_palette _color_palette { };
Constructible<Attached_rom_dataspace> _clipboard_rom { };
Constructible<Expanding_reporter> _clipboard_reporter { };
void _handle_config();
Signal_handler<Main> _config_handler {
@ -82,6 +86,14 @@ struct Terminal::Main : Character_consumer
Framebuffer _framebuffer { _env, _config_handler };
Point _pointer { }; /* pointer positon in pixels */
bool _shift_pressed = false;
bool _selecting = false;
struct Paste_buffer { char buffer[READ_BUFFER_SIZE]; } _paste_buffer { };
typedef Pixel_rgb565 PT;
Constructible<Text_screen_surface<PT>> _text_screen_surface { };
@ -139,6 +151,9 @@ struct Terminal::Main : Character_consumer
Signal_handler<Main> _input_handler {
_env.ep(), *this, &Main::_handle_input };
void _report_clipboard_selection();
void _paste_clipboard_content();
Main(Env &env) : _env(env)
{
_timer .sigh(_flush_handler);
@ -170,6 +185,12 @@ void Terminal::Main::_handle_config()
_font.construct(_heap, _root_dir, cache_limit);
_clipboard_reporter.conditional(config.attribute_value("copy", false),
_env, "clipboard", "clipboard");
_clipboard_rom.conditional(config.attribute_value("paste", false),
_env, "clipboard");
/*
* Adapt terminal to font or framebuffer mode changes
*/
@ -252,6 +273,56 @@ void Terminal::Main::_handle_input()
{
_input.for_each_event([&] (Input::Event const &event) {
event.handle_absolute_motion([&] (int x, int y) {
_pointer = Point(x, y);
if (_shift_pressed) {
_text_screen_surface->pointer(_pointer);
_schedule_flush();
}
if (_selecting) {
_text_screen_surface->define_selection(_pointer);
_schedule_flush();
}
});
if (event.key_press(Input::KEY_LEFTSHIFT)) {
if (_clipboard_reporter.constructed()) {
_shift_pressed = true;
_text_screen_surface->clear_selection();
_text_screen_surface->pointer(_pointer);
_schedule_flush();
}
}
if (event.key_release(Input::KEY_LEFTSHIFT)) {
_shift_pressed = false;
_text_screen_surface->pointer(Point(-1, -1));
_schedule_flush();
}
if (event.key_press(Input::BTN_LEFT)) {
if (_shift_pressed) {
_selecting = true;
_text_screen_surface->start_selection(_pointer);
} else {
_text_screen_surface->clear_selection();
}
_schedule_flush();
}
if (event.key_release(Input::BTN_LEFT)) {
if (_selecting) {
_selecting = false;
_report_clipboard_selection();
}
}
if (event.key_press(Input::BTN_MIDDLE))
_paste_clipboard_content();
event.handle_press([&] (Input::Keycode, Codepoint codepoint) {
/* function-key unicodes */
@ -304,4 +375,56 @@ void Terminal::Main::_handle_input()
}
void Terminal::Main::_report_clipboard_selection()
{
if (!_clipboard_reporter.constructed())
return;
_clipboard_reporter->generate([&] (Xml_generator &xml) {
_text_screen_surface->for_each_selected_character([&] (Codepoint c) {
String<10> const utf8(c);
if (utf8.valid())
xml.append_sanitized(utf8.string(), utf8.length() - 1);
});
});
}
void Terminal::Main::_paste_clipboard_content()
{
if (!_clipboard_rom.constructed())
return;
_clipboard_rom->update();
_paste_buffer = { };
/* leave last byte as zero-termination in tact */
size_t const max_len = sizeof(_paste_buffer.buffer) - 1;
size_t const len =
_clipboard_rom->xml().decoded_content(_paste_buffer.buffer, max_len);
if (len == max_len) {
warning("clipboard content exceeds paste buffer");
return;
}
if (len >= (size_t)_read_buffer.avail_capacity()) {
warning("clipboard content exceeds read-buffer capacity");
return;
}
for (Utf8_ptr utf8(_paste_buffer.buffer); utf8.complete(); utf8 = utf8.next()) {
Codepoint const c = utf8.codepoint();
/* filter out control characters */
if (c.value < 32 && c.value != 10)
continue;
_read_buffer.add(c);
}
}
void Component::construct(Genode::Env &env) { static Terminal::Main main(env); }

View File

@ -91,6 +91,17 @@ class Terminal::Text_screen_surface
Point start() const { return Point(1, 1); }
bool valid() const { return columns*lines > 0; }
/**
* Return character position at given pixel coordinates
*/
Position position(Point p) const
{
if (char_width.value == 0 || char_height == 0)
return Position { };
return Position((p.x() << 8) / char_width.value, p.y() / char_height);
}
};
/**
@ -133,6 +144,29 @@ class Terminal::Text_screen_surface
Decoder _decoder { _character_screen };
struct Selection
{
Position start { };
Position end { };
bool defined = false;
bool selected(Position pos) const
{
return defined && pos.in_range(start, end);
}
template <typename FN>
void for_each_line(FN const &fn) const
{
for (int i = min(start.y, end.y); i <= max(start.y, end.y); i++)
fn(i);
}
} _selection { };
Position _pointer { -1, -1 };
public:
/**
@ -199,7 +233,20 @@ class Terminal::Text_screen_surface
Char_cell const cell = _cell_array.get_cell(column, line);
_font.apply_glyph(cell.codepoint(), [&] (Glyph_painter::Glyph const &glyph) {
Codepoint codepoint = cell.codepoint();
/* display absent codepoints as whitespace */
bool const codepoint_valid = (codepoint.value != 0);
bool const selected = _selection.selected(Position(column, line))
&& codepoint_valid;
bool const pointer = (_pointer == Position(column, line));
if (!codepoint_valid)
codepoint = Codepoint{' '};
_font.apply_glyph(codepoint, [&] (Glyph_painter::Glyph const &glyph) {
Color_palette::Highlighted const highlighted { cell.highlight() };
@ -216,6 +263,16 @@ class Terminal::Text_screen_surface
Color fg_color = _palette.foreground(fg_idx, highlighted);
Color bg_color = _palette.background(bg_idx, highlighted);
if (selected) {
bg_color = Color(180, 180, 180);
fg_color = Color( 50, 50, 50);
}
if (pointer) {
bg_color = Color(220, 220, 220);
fg_color = Color( 50, 50, 50);
}
if (cell.has_cursor()) {
fg_color = Color( 63, 63, 63);
bg_color = Color(255, 255, 255);
@ -271,6 +328,8 @@ class Terminal::Text_screen_surface
void apply_character(Character c)
{
clear_selection();
/* submit character to sequence decoder */
_decoder.insert(c);
}
@ -284,6 +343,110 @@ class Terminal::Text_screen_surface
* Return size in colums/rows
*/
Area size() const { return _geometry.size(); }
/**
* Set pointer position in pixels (to show the cursor)
*/
void pointer(Point pointer)
{
auto position_valid = [&] (Position pos) {
return pos.y >= 0 && pos.y < (int)_geometry.lines; };
/* update old position */
if (position_valid(_pointer))
_cell_array.mark_line_as_dirty(_pointer.y);
_pointer = _geometry.position(pointer);
/* update new position */
if (position_valid(_pointer))
_cell_array.mark_line_as_dirty(_pointer.y);
}
/**
* Set anchor point of selection
*
* \param pointer pointer position in pixels
*/
void start_selection(Point pointer)
{
if (_selection.defined)
clear_selection();
_selection.start = _geometry.position(pointer);
define_selection(pointer);
}
/**
* Set end position of current selection
*
* \param pointer pointer position in pixels
*/
void define_selection(Point pointer)
{
_selection.for_each_line([&] (int line) {
_cell_array.mark_line_as_dirty(line); });
_selection.end = _geometry.position(pointer);
_selection.defined = true;
_selection.for_each_line([&] (int line) {
_cell_array.mark_line_as_dirty(line); });
}
void clear_selection()
{
if (!_selection.defined)
return;
_selection.for_each_line([&] (int line) {
_cell_array.mark_line_as_dirty(line); });
_selection.defined = false;
}
template <typename FN>
void for_each_selected_character(FN const &fn) const
{
for (unsigned row = 0; row < _geometry.lines; row++) {
bool skip_remaining_chars_on_row = false;
for (unsigned column = 0; column < _geometry.columns; column++) {
if (skip_remaining_chars_on_row)
continue;
if (!_selection.selected(Position(column, row)))
continue;
Codepoint const c { _cell_array.get_cell(column, row).value };
if (c.value == 0) {
auto remaining_line_empty = [&] ()
{
for (unsigned i = column + 1; i < _geometry.columns; i++)
if (_cell_array.get_cell(i, row).value)
return false;
return true;
};
/* generate one line break at the end of a selected line */
if (remaining_line_empty()) {
fn(Codepoint{'\n'});
skip_remaining_chars_on_row = true;
} else {
fn(Codepoint{' '});
}
} else {
fn(c);
}
}
}
}
};
#endif /* _TEXT_SCREEN_SURFACE_H_ */

View File

@ -20,7 +20,7 @@
struct Char_cell
{
Genode::uint16_t value { ' ' };
Genode::uint16_t value { 0 };
unsigned char attr;
unsigned char color;

View File

@ -76,6 +76,23 @@ struct Terminal::Position
bool operator != (Position const &pos) const {
return (pos.x != x) || (pos.y != y); }
bool operator >= (Position const &other) const
{
if (y > other.y)
return true;
if (y == other.y && x >= other.x)
return true;
return false;
}
bool in_range(Position start, Position end) const
{
return (end >= start) ? *this >= start && end >= *this
: *this >= end && start >= *this;
}
/**
* Return true if position lies within the specified boundaries
*/