mirror of
https://github.com/genodelabs/genode.git
synced 2025-01-20 03:36:33 +00:00
text_area: a simple text viewer / editor
The new text_area component is able to view and edit files. Internally, it employs a menu_view for the graphics output. Only basic notepad-like text-editing functions are supported. At the current time, it is solely meant as a companion of the Sculpt manager. Issue #3650
This commit is contained in:
parent
b2bc718c1f
commit
5eaaee0dbe
3
repos/gems/recipes/src/text_area/content.mk
Normal file
3
repos/gems/recipes/src/text_area/content.mk
Normal file
@ -0,0 +1,3 @@
|
||||
SRC_DIR := src/app/text_area
|
||||
|
||||
include $(GENODE_DIR)/repos/base/recipes/src/content.inc
|
1
repos/gems/recipes/src/text_area/hash
Normal file
1
repos/gems/recipes/src/text_area/hash
Normal file
@ -0,0 +1 @@
|
||||
2020-02-12 ca4f34ab1f8c172a367b4c955920a466dbb3ab18
|
9
repos/gems/recipes/src/text_area/used_apis
Normal file
9
repos/gems/recipes/src/text_area/used_apis
Normal file
@ -0,0 +1,9 @@
|
||||
base
|
||||
os
|
||||
vfs
|
||||
report_session
|
||||
file_system_session
|
||||
timer_session
|
||||
nitpicker_session
|
||||
input_session
|
||||
framebuffer_session
|
152
repos/gems/run/text_area.run
Normal file
152
repos/gems/run/text_area.run
Normal file
@ -0,0 +1,152 @@
|
||||
create_boot_directory
|
||||
|
||||
import_from_depot [depot_user]/src/[base_src] \
|
||||
[depot_user]/pkg/[drivers_interactive_pkg] \
|
||||
[depot_user]/pkg/fonts_fs \
|
||||
[depot_user]/src/init \
|
||||
[depot_user]/src/report_rom \
|
||||
[depot_user]/src/nitpicker \
|
||||
[depot_user]/src/libc \
|
||||
[depot_user]/src/libpng \
|
||||
[depot_user]/src/zlib \
|
||||
[depot_user]/src/vfs_import
|
||||
|
||||
install_config {
|
||||
<config>
|
||||
<parent-provides>
|
||||
<service name="PD"/>
|
||||
<service name="CPU"/>
|
||||
<service name="ROM"/>
|
||||
<service name="RM"/>
|
||||
<service name="LOG"/>
|
||||
<service name="IRQ"/>
|
||||
<service name="IO_MEM"/>
|
||||
<service name="IO_PORT"/>
|
||||
</parent-provides>
|
||||
|
||||
<default caps="100"/>
|
||||
|
||||
<default-route>
|
||||
<any-service> <parent/> <any-child/> </any-service>
|
||||
</default-route>
|
||||
|
||||
<start name="timer">
|
||||
<resource name="RAM" quantum="1M"/>
|
||||
<provides><service name="Timer"/></provides>
|
||||
</start>
|
||||
|
||||
<start name="drivers" caps="1000">
|
||||
<resource name="RAM" quantum="32M" constrain_phys="yes"/>
|
||||
<binary name="init"/>
|
||||
<route>
|
||||
<service name="ROM" label="config"> <parent label="drivers.config"/> </service>
|
||||
<service name="Timer"> <child name="timer"/> </service>
|
||||
<any-service> <parent/> </any-service>
|
||||
</route>
|
||||
<provides>
|
||||
<service name="Input"/> <service name="Framebuffer"/>
|
||||
</provides>
|
||||
</start>
|
||||
|
||||
<start name="report_rom">
|
||||
<resource name="RAM" quantum="1M"/>
|
||||
<provides> <service name="Report"/> <service name="ROM"/> </provides>
|
||||
<config verbose="yes">
|
||||
<policy label="text_area.1 -> hover" report="nitpicker -> hover"/>
|
||||
<policy label="text_area.2 -> clipboard" report="text_area.2 -> clipboard"/>
|
||||
</config>
|
||||
</start>
|
||||
|
||||
<start name="nitpicker">
|
||||
<resource name="RAM" quantum="4M"/>
|
||||
<provides><service name="Nitpicker"/></provides>
|
||||
<config focus="rom">
|
||||
<report hover="yes"/>
|
||||
<background color="#123456"/>
|
||||
<domain name="pointer" layer="1" content="client" label="no" origin="pointer" />
|
||||
<domain name="default" layer="3" content="client" label="no" hover="always" />
|
||||
<domain name="second" layer="2" xpos="200" ypos="300" content="client" label="no" hover="always" />
|
||||
|
||||
<policy label_prefix="pointer" domain="pointer"/>
|
||||
<policy label_prefix="text_area.2" domain="second"/>
|
||||
<default-policy domain="default"/>
|
||||
</config>
|
||||
</start>
|
||||
|
||||
<start name="pointer">
|
||||
<resource name="RAM" quantum="1M"/>
|
||||
<route>
|
||||
<service name="Nitpicker"> <child name="nitpicker" /> </service>
|
||||
<any-service> <parent/> <any-child/> </any-service>
|
||||
</route>
|
||||
</start>
|
||||
|
||||
<start name="fonts_fs" caps="300">
|
||||
<resource name="RAM" quantum="8M"/>
|
||||
<binary name="vfs"/>
|
||||
<route>
|
||||
<service name="ROM" label="config"> <parent label="fonts_fs.config"/> </service>
|
||||
<any-service> <parent/> </any-service>
|
||||
</route>
|
||||
<provides> <service name="File_system"/> </provides>
|
||||
</start>
|
||||
|
||||
<start name="data_fs" caps="300">
|
||||
<resource name="RAM" quantum="8M"/>
|
||||
<binary name="vfs"/>
|
||||
<config>
|
||||
<vfs>
|
||||
<ram/>
|
||||
<import>
|
||||
<rom name="drivers.config" binary="no"/>
|
||||
</import>
|
||||
</vfs>
|
||||
<default-policy root="/" writeable="yes"/>
|
||||
</config>
|
||||
<route> <any-service> <parent/> </any-service> </route>
|
||||
<provides> <service name="File_system"/> </provides>
|
||||
</start>
|
||||
|
||||
<start name="text_area.1" caps="250">
|
||||
<binary name="text_area"/>
|
||||
<resource name="RAM" quantum="8M"/>
|
||||
<config path="/data/drivers.config" max_lines="20" min_width="500" min_height="300" watch="yes">
|
||||
<vfs> <dir name="data"> <fs label="data"/> </dir> </vfs>
|
||||
</config>
|
||||
<route>
|
||||
<service name="ROM" label="hover"> <child name="report_rom"/> </service>
|
||||
<service name="File_system" label="fonts"> <child name="fonts_fs"/> </service>
|
||||
<service name="File_system" label="data"> <child name="data_fs"/> </service>
|
||||
<any-service> <parent/> <any-child/> </any-service>
|
||||
</route>
|
||||
</start>
|
||||
|
||||
<start name="text_area.2" caps="250">
|
||||
<binary name="text_area"/>
|
||||
<resource name="RAM" quantum="8M"/>
|
||||
<config path="/data/drivers.config" copy="yes" paste="yes"
|
||||
max_lines="10" min_width="600">
|
||||
<report saved="yes"/>
|
||||
<save version="1"/>
|
||||
<vfs> <dir name="data"> <fs label="data"/> </dir> </vfs>
|
||||
</config>
|
||||
<route>
|
||||
<service name="Report"> <child name="report_rom"/> </service>
|
||||
<service name="ROM" label="clipboard"> <child name="report_rom"/> </service>
|
||||
<service name="File_system" label="fonts"> <child name="fonts_fs"/> </service>
|
||||
<service name="File_system" label="data"> <child name="data_fs"/> </service>
|
||||
<any-service> <parent/> <any-child/> </any-service>
|
||||
</route>
|
||||
</start>
|
||||
|
||||
</config>}
|
||||
|
||||
set fd [open [run_dir]/genode/focus w]
|
||||
puts $fd "<focus label=\"text_area.2 -> \"/>"
|
||||
close $fd
|
||||
|
||||
build { app/text_area app/menu_view }
|
||||
|
||||
build_boot_image { text_area sandbox.lib.so vfs.lib.so menu_view menu_view_styles.tar }
|
||||
|
||||
run_genode_until forever
|
124
repos/gems/src/app/text_area/child_state.h
Normal file
124
repos/gems/src/app/text_area/child_state.h
Normal file
@ -0,0 +1,124 @@
|
||||
/*
|
||||
* \brief Runtime state of a child hosted in the runtime subsystem
|
||||
* \author Norman Feske
|
||||
* \date 2018-09-03
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (C) 2018 Genode Labs GmbH
|
||||
*
|
||||
* This file is part of the Genode OS framework, which is distributed
|
||||
* under the terms of the GNU Affero General Public License version 3.
|
||||
*/
|
||||
|
||||
#ifndef _CHILD_STATE_H_
|
||||
#define _CHILD_STATE_H_
|
||||
|
||||
/* Genode includes */
|
||||
#include <util/xml_node.h>
|
||||
#include <util/noncopyable.h>
|
||||
#include <util/string.h>
|
||||
#include <base/registry.h>
|
||||
#include <base/quota_guard.h>
|
||||
|
||||
/* local includes */
|
||||
#include "types.h"
|
||||
|
||||
namespace Text_area { struct Child_state; }
|
||||
|
||||
struct Text_area::Child_state : Noncopyable
|
||||
{
|
||||
private:
|
||||
|
||||
using Start_name = String<128>;
|
||||
|
||||
Registry<Child_state>::Element _element;
|
||||
|
||||
Start_name const _name;
|
||||
|
||||
Ram_quota const _initial_ram_quota;
|
||||
Cap_quota const _initial_cap_quota;
|
||||
|
||||
Ram_quota _ram_quota = _initial_ram_quota;
|
||||
Cap_quota _cap_quota = _initial_cap_quota;
|
||||
|
||||
struct Version { unsigned value; } _version { 0 };
|
||||
|
||||
public:
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* \param ram_quota initial RAM quota
|
||||
* \param cap_quota initial capability quota
|
||||
*/
|
||||
Child_state(Registry<Child_state> ®istry, Start_name const &name,
|
||||
Ram_quota ram_quota, Cap_quota cap_quota)
|
||||
:
|
||||
_element(registry, *this),
|
||||
_name(name),
|
||||
_initial_ram_quota(ram_quota), _initial_cap_quota(cap_quota)
|
||||
{ }
|
||||
|
||||
void trigger_restart()
|
||||
{
|
||||
_version.value++;
|
||||
_ram_quota = _initial_ram_quota;
|
||||
_cap_quota = _initial_cap_quota;
|
||||
}
|
||||
|
||||
void gen_start_node_version(Xml_generator &xml) const
|
||||
{
|
||||
if (_version.value)
|
||||
xml.attribute("version", _version.value);
|
||||
}
|
||||
|
||||
void gen_start_node_content(Xml_generator &xml) const
|
||||
{
|
||||
xml.attribute("name", _name);
|
||||
|
||||
gen_start_node_version(xml);
|
||||
|
||||
xml.attribute("caps", _cap_quota.value);
|
||||
xml.node("resource", [&] () {
|
||||
xml.attribute("name", "RAM");
|
||||
Number_of_bytes const bytes(_ram_quota.value);
|
||||
xml.attribute("quantum", String<64>(bytes)); });
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt runtime state information to the child
|
||||
*
|
||||
* This method responds to RAM and cap-resource requests by increasing
|
||||
* the resource quotas as needed.
|
||||
*
|
||||
* \param child child node of the runtime'r state report
|
||||
* \return true if runtime must be reconfigured so that the changes
|
||||
* can take effect
|
||||
*/
|
||||
bool apply_child_state_report(Xml_node child)
|
||||
{
|
||||
bool result = false;
|
||||
|
||||
if (child.attribute_value("name", Start_name()) != _name)
|
||||
return false;
|
||||
|
||||
if (child.has_sub_node("ram") && child.sub_node("ram").has_attribute("requested")) {
|
||||
_ram_quota.value *= 2;
|
||||
result = true;
|
||||
}
|
||||
|
||||
if (child.has_sub_node("caps") && child.sub_node("caps").has_attribute("requested")) {
|
||||
_cap_quota.value += 100;
|
||||
result = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Ram_quota ram_quota() const { return _ram_quota; }
|
||||
|
||||
Start_name name() const { return _name; }
|
||||
};
|
||||
|
||||
#endif /* _CHILD_STATE_H_ */
|
734
repos/gems/src/app/text_area/dialog.cc
Normal file
734
repos/gems/src/app/text_area/dialog.cc
Normal file
@ -0,0 +1,734 @@
|
||||
/*
|
||||
* \brief Text dialog
|
||||
* \author Norman Feske
|
||||
* \date 2020-01-14
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (C) 2020 Genode Labs GmbH
|
||||
*
|
||||
* This file is part of the Genode OS framework, which is distributed
|
||||
* under the terms of the GNU Affero General Public License version 3.
|
||||
*/
|
||||
|
||||
/* local includes */
|
||||
#include <dialog.h>
|
||||
|
||||
using namespace Text_area;
|
||||
|
||||
|
||||
enum {
|
||||
CODEPOINT_BACKSPACE = 8, CODEPOINT_NEWLINE = 10,
|
||||
CODEPOINT_UP = 0xf700, CODEPOINT_DOWN = 0xf701,
|
||||
CODEPOINT_LEFT = 0xf702, CODEPOINT_RIGHT = 0xf703,
|
||||
CODEPOINT_HOME = 0xf729, CODEPOINT_INSERT = 0xf727,
|
||||
CODEPOINT_DELETE = 0xf728, CODEPOINT_END = 0xf72b,
|
||||
CODEPOINT_PAGEUP = 0xf72c, CODEPOINT_PAGEDOWN = 0xf72d,
|
||||
};
|
||||
|
||||
|
||||
static bool movement_codepoint(Codepoint code)
|
||||
{
|
||||
auto v = code.value;
|
||||
return (v == CODEPOINT_UP) || (v == CODEPOINT_DOWN) ||
|
||||
(v == CODEPOINT_LEFT) || (v == CODEPOINT_RIGHT) ||
|
||||
(v == CODEPOINT_HOME) || (v == CODEPOINT_END) ||
|
||||
(v == CODEPOINT_PAGEUP) || (v == CODEPOINT_PAGEDOWN);
|
||||
}
|
||||
|
||||
|
||||
static bool shift_key(Input::Keycode key)
|
||||
{
|
||||
return (key == Input::KEY_LEFTSHIFT) || (key == Input::KEY_RIGHTSHIFT);
|
||||
}
|
||||
|
||||
|
||||
static bool control_key(Input::Keycode key)
|
||||
{
|
||||
return (key == Input::KEY_LEFTCTRL) || (key == Input::KEY_RIGHTCTRL);
|
||||
}
|
||||
|
||||
|
||||
template <typename T>
|
||||
static void swap(T &v1, T &v2) { auto tmp = v1; v1 = v2; v2 = tmp; };
|
||||
|
||||
|
||||
template <typename FN>
|
||||
void Dialog::Selection::for_each_selected_line(FN const &fn) const
|
||||
{
|
||||
if (!defined())
|
||||
return;
|
||||
|
||||
unsigned start_y = start->y.value, end_y = end->y.value;
|
||||
|
||||
if (end_y < start_y)
|
||||
swap(start_y, end_y);
|
||||
|
||||
if (end_y < start_y)
|
||||
return;
|
||||
|
||||
for (unsigned i = start_y; i <= end_y; i++)
|
||||
fn(Text::Index { i }, (i == end_y));
|
||||
}
|
||||
|
||||
|
||||
template <typename FN>
|
||||
void Dialog::Selection::with_selection_at_line(Text::Index y, Line const &line,
|
||||
FN const &fn) const
|
||||
{
|
||||
if (!defined())
|
||||
return;
|
||||
|
||||
Line::Index start_x = start->x, end_x = end->x;
|
||||
Text::Index start_y = start->y, end_y = end->y;
|
||||
|
||||
if (end_y.value < start_y.value) {
|
||||
swap(start_x, end_x);
|
||||
swap(start_y, end_y);
|
||||
}
|
||||
|
||||
if (y.value < start_y.value || y.value > end_y.value)
|
||||
return;
|
||||
|
||||
if (y.value > start_y.value)
|
||||
start_x = Line::Index { 0 };
|
||||
|
||||
if (y.value < end_y.value)
|
||||
end_x = line.upper_bound();
|
||||
|
||||
if (start_x.value > end_x.value)
|
||||
swap(start_x, end_x);
|
||||
|
||||
fn(start_x, end_x.value - start_x.value);
|
||||
}
|
||||
|
||||
|
||||
void Dialog::Selection::gen_selected_line(Xml_generator &xml,
|
||||
Text::Index y, Line const &line) const
|
||||
{
|
||||
with_selection_at_line(y, line, [&] (Line::Index const start_x, const unsigned n) {
|
||||
xml.node("selection", [&] () {
|
||||
xml.attribute("at", start_x.value);
|
||||
xml.attribute("length", n);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void Dialog::produce_xml(Xml_generator &xml)
|
||||
{
|
||||
auto gen_line = [&] (Text::Index at, Line const &line)
|
||||
{
|
||||
xml.node("hbox", [&] () {
|
||||
xml.attribute("name", at.value - _scroll.y.value);
|
||||
xml.node("float", [&] () {
|
||||
xml.attribute("north", "yes");
|
||||
xml.attribute("south", "yes");
|
||||
xml.attribute("west", "yes");
|
||||
xml.node("label", [&] () {
|
||||
xml.attribute("font", "monospace/regular");
|
||||
xml.attribute("text", String<512>(line));
|
||||
|
||||
if (_cursor.y.value == at.value)
|
||||
xml.node("cursor", [&] () {
|
||||
xml.attribute("name", "cursor");
|
||||
xml.attribute("at", _cursor.x.value); });
|
||||
|
||||
if (_hovered_position.constructed())
|
||||
if (_hovered_position->y.value == at.value)
|
||||
xml.node("cursor", [&] () {
|
||||
xml.attribute("name", "hover");
|
||||
xml.attribute("style", "hover");
|
||||
xml.attribute("at", _hovered_position->x.value); });
|
||||
|
||||
_selection.gen_selected_line(xml, at, line);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
xml.node("frame", [&] () {
|
||||
xml.node("button", [&] () {
|
||||
xml.attribute("name", "text");
|
||||
|
||||
if (_text_hovered)
|
||||
xml.attribute("hovered", "yes");
|
||||
|
||||
xml.node("float", [&] () {
|
||||
xml.attribute("north", "yes");
|
||||
xml.attribute("east", "yes");
|
||||
xml.attribute("west", "yes");
|
||||
xml.node("vbox", [&] () {
|
||||
Dynamic_array<Line>::Range const range { .at = _scroll.y,
|
||||
.length = _max_lines };
|
||||
_text.for_each(range, gen_line);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void Dialog::_delete_selection()
|
||||
{
|
||||
if (!_editable)
|
||||
return;
|
||||
|
||||
if (!_selection.defined())
|
||||
return;
|
||||
|
||||
_modification_count++;
|
||||
|
||||
/*
|
||||
* Clear all characters within the selection
|
||||
*/
|
||||
|
||||
unsigned num_lines = 0;
|
||||
Text::Index first_y { 0 };
|
||||
|
||||
_selection.for_each_selected_line([&] (Text::Index const y, bool) {
|
||||
|
||||
_text.apply(y, [&] (Line &line) {
|
||||
|
||||
_selection.with_selection_at_line(y, line,
|
||||
[&] (Line::Index x, unsigned n) {
|
||||
for (unsigned i = 0; i < n; i++) {
|
||||
line.destruct(Line::Index { x.value });
|
||||
|
||||
bool const cursor_right_of_deleted_character =
|
||||
(_cursor.y.value == y.value) && (_cursor.x.value > x.value);
|
||||
|
||||
if (cursor_right_of_deleted_character)
|
||||
_cursor.x.value--;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (num_lines == 0)
|
||||
first_y = y;
|
||||
|
||||
num_lines++;
|
||||
});
|
||||
|
||||
/*
|
||||
* Remove all selected lines, joining the remaining characters at the
|
||||
* bounds of the selection.
|
||||
*/
|
||||
|
||||
if (num_lines > 1) {
|
||||
|
||||
Text::Index const next_y { first_y.value + 1 };
|
||||
|
||||
while (--num_lines) {
|
||||
|
||||
bool const cursor_at_deleted_line = (_cursor.y.value == next_y.value);
|
||||
bool const cursor_below_deleted_line = (_cursor.y.value > next_y.value);
|
||||
|
||||
_text.apply(first_y, [&] (Line &first) {
|
||||
|
||||
if (cursor_at_deleted_line)
|
||||
_cursor = { .x = first.upper_bound(),
|
||||
.y = first_y };
|
||||
|
||||
_text.apply(next_y, [&] (Line &next) {
|
||||
_move_characters(next, first); });
|
||||
});
|
||||
|
||||
_text.destruct(next_y);
|
||||
|
||||
if (cursor_below_deleted_line)
|
||||
_cursor.y.value--;
|
||||
}
|
||||
}
|
||||
|
||||
_selection.clear();
|
||||
}
|
||||
|
||||
|
||||
void Dialog::_insert_printable(Codepoint code)
|
||||
{
|
||||
_tie_cursor_to_end_of_line();
|
||||
|
||||
_text.apply(_cursor.y, [&] (Line &line) {
|
||||
line.insert(_cursor.x, Character(code)); });
|
||||
|
||||
_cursor.x.value++;
|
||||
}
|
||||
|
||||
|
||||
void Dialog::_handle_printable(Codepoint code)
|
||||
{
|
||||
if (!_editable)
|
||||
return;
|
||||
|
||||
_modification_count++;
|
||||
|
||||
_delete_selection();
|
||||
_insert_printable(code);
|
||||
}
|
||||
|
||||
|
||||
void Dialog::_move_characters(Line &from, Line &to)
|
||||
{
|
||||
/* move all characters of line 'from' to the end of line 'to' */
|
||||
Line::Index const first { 0 };
|
||||
while (from.exists(first)) {
|
||||
from.apply(first, [&] (Codepoint &code) {
|
||||
to.append(code); });
|
||||
from.destruct(first);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Dialog::_handle_backspace()
|
||||
{
|
||||
if (!_editable)
|
||||
return;
|
||||
|
||||
_modification_count++;
|
||||
|
||||
/* eat backspace when deleting a selection */
|
||||
if (_selection.defined()) {
|
||||
_delete_selection();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cursor.x.value > 0) {
|
||||
_cursor.x.value--;
|
||||
|
||||
_text.apply(_cursor.y, [&] (Line &line) {
|
||||
line.destruct(_cursor.x); });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cursor.y.value == 0)
|
||||
return;
|
||||
|
||||
/* join line with previous line */
|
||||
Text::Index const prev_y { _cursor.y.value - 1 };
|
||||
|
||||
_text.apply(prev_y, [&] (Line &prev_line) {
|
||||
|
||||
_cursor.x = prev_line.upper_bound();
|
||||
|
||||
_text.apply(_cursor.y, [&] (Line &line) {
|
||||
_move_characters(line, prev_line); });
|
||||
});
|
||||
|
||||
_text.destruct(_cursor.y);
|
||||
|
||||
_cursor.y = prev_y;
|
||||
}
|
||||
|
||||
|
||||
void Dialog::_handle_delete()
|
||||
{
|
||||
if (!_editable)
|
||||
return;
|
||||
|
||||
_modification_count++;
|
||||
|
||||
/* eat delete when deleting a selection */
|
||||
if (_selection.defined()) {
|
||||
_delete_selection();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_end_of_text())
|
||||
return;
|
||||
|
||||
_handle_right();
|
||||
_handle_backspace();
|
||||
}
|
||||
|
||||
|
||||
void Dialog::_handle_newline()
|
||||
{
|
||||
if (!_editable)
|
||||
return;
|
||||
|
||||
_modification_count++;
|
||||
|
||||
_delete_selection();
|
||||
|
||||
/* create new line at cursor position */
|
||||
Text::Index const new_y { _cursor.y.value + 1 };
|
||||
_text.insert(new_y, _alloc);
|
||||
|
||||
/* take the characters after the cursor to the new line */
|
||||
_text.apply(_cursor.y, [&] (Line &line) {
|
||||
_text.apply(new_y, [&] (Line &new_line) {
|
||||
while (line.exists(_cursor.x)) {
|
||||
line.apply(_cursor.x, [&] (Codepoint code) {
|
||||
new_line.append(code); });
|
||||
line.destruct(_cursor.x);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
_cursor.y = new_y;
|
||||
_cursor.x.value = 0;
|
||||
}
|
||||
|
||||
|
||||
void Dialog::_handle_left()
|
||||
{
|
||||
_tie_cursor_to_end_of_line();
|
||||
|
||||
if (_cursor.x.value == 0) {
|
||||
if (_cursor.y.value > 0) {
|
||||
_cursor.y.value--;
|
||||
_text.apply(_cursor.y, [&] (Line &line) {
|
||||
_cursor.x = line.upper_bound(); });
|
||||
}
|
||||
} else {
|
||||
_cursor.x.value--;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Dialog::_handle_right()
|
||||
{
|
||||
if (!_cursor_at_end_of_line()) {
|
||||
_cursor.x.value++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_cursor_at_last_line()) {
|
||||
_cursor.x.value = 0;
|
||||
_cursor.y.value++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Dialog::_handle_up()
|
||||
{
|
||||
if (_cursor.y.value > 0)
|
||||
_cursor.y.value--;
|
||||
}
|
||||
|
||||
|
||||
void Dialog::_handle_down()
|
||||
{
|
||||
if (_cursor.y.value + 1 < _text.upper_bound().value)
|
||||
_cursor.y.value++;
|
||||
}
|
||||
|
||||
|
||||
void Dialog::_handle_pageup()
|
||||
{
|
||||
if (_max_lines != ~0U) {
|
||||
for (unsigned i = 0; i < _max_lines; i++)
|
||||
_handle_up();
|
||||
} else {
|
||||
_cursor.y.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Dialog::_handle_pagedown()
|
||||
{
|
||||
if (_max_lines != ~0U) {
|
||||
for (unsigned i = 0; i < _max_lines; i++)
|
||||
_handle_down();
|
||||
} else {
|
||||
_cursor.y.value = _text.upper_bound().value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Dialog::_handle_home()
|
||||
{
|
||||
_cursor.x.value = 0;
|
||||
}
|
||||
|
||||
|
||||
void Dialog::_handle_end()
|
||||
{
|
||||
_text.apply(_cursor.y, [&] (Line &line) {
|
||||
_cursor.x = line.upper_bound(); });
|
||||
}
|
||||
|
||||
|
||||
void Dialog::handle_input_event(Input::Event const &event)
|
||||
{
|
||||
bool update_dialog = false;
|
||||
|
||||
Position const orig_cursor = _cursor;
|
||||
|
||||
auto cursor_to_hovered_position = [&] ()
|
||||
{
|
||||
if (_hovered_position.constructed()) {
|
||||
_cursor.x = _hovered_position->x;
|
||||
_cursor.y = _hovered_position->y;
|
||||
update_dialog = true;
|
||||
}
|
||||
};
|
||||
|
||||
event.handle_press([&] (Input::Keycode key, Codepoint code) {
|
||||
|
||||
bool key_has_visible_effect = true;
|
||||
|
||||
if (shift_key(key)) {
|
||||
_shift = true;
|
||||
if (!_selection.defined())
|
||||
_selection.start.construct(_cursor);
|
||||
}
|
||||
|
||||
if (control_key(key))
|
||||
_control = true;
|
||||
|
||||
if (!_control) {
|
||||
|
||||
if (!_shift && movement_codepoint(code))
|
||||
_selection.clear();
|
||||
|
||||
if (_printable(code)) {
|
||||
_handle_printable(code);
|
||||
}
|
||||
else if (code.value == CODEPOINT_BACKSPACE) { _handle_backspace(); }
|
||||
else if (code.value == CODEPOINT_DELETE) { _handle_delete(); }
|
||||
else if (code.value == CODEPOINT_NEWLINE) { _handle_newline(); }
|
||||
else if (code.value == CODEPOINT_LEFT) { _handle_left(); }
|
||||
else if (code.value == CODEPOINT_UP) { _handle_up(); }
|
||||
else if (code.value == CODEPOINT_DOWN) { _handle_down(); }
|
||||
else if (code.value == CODEPOINT_RIGHT) { _handle_right(); }
|
||||
else if (code.value == CODEPOINT_PAGEDOWN) { _handle_pagedown(); }
|
||||
else if (code.value == CODEPOINT_PAGEUP) { _handle_pageup(); }
|
||||
else if (code.value == CODEPOINT_HOME) { _handle_home(); }
|
||||
else if (code.value == CODEPOINT_END) { _handle_end(); }
|
||||
else if (code.value == CODEPOINT_INSERT) { _trigger_paste.trigger_paste(); }
|
||||
else {
|
||||
key_has_visible_effect = false;
|
||||
}
|
||||
|
||||
if (_shift && movement_codepoint(code))
|
||||
_selection.end.construct(_cursor);
|
||||
}
|
||||
|
||||
if (_control) {
|
||||
|
||||
if (code.value == 'c')
|
||||
_trigger_copy.trigger_copy();
|
||||
|
||||
if (code.value == 'x') {
|
||||
_trigger_copy.trigger_copy();
|
||||
_delete_selection();
|
||||
}
|
||||
|
||||
if (code.value == 'v')
|
||||
_trigger_paste.trigger_paste();
|
||||
|
||||
if (code.value == 's')
|
||||
_trigger_save.trigger_save();
|
||||
}
|
||||
|
||||
if (key_has_visible_effect)
|
||||
update_dialog = true;
|
||||
|
||||
bool const click = (key == Input::BTN_LEFT);
|
||||
if (click && _hovered_position.constructed()) {
|
||||
|
||||
if (_shift)
|
||||
_selection.end.construct(*_hovered_position);
|
||||
else
|
||||
_selection.start.construct(*_hovered_position);
|
||||
|
||||
_drag = true;
|
||||
}
|
||||
|
||||
bool const middle_click = (key == Input::BTN_MIDDLE);
|
||||
if (middle_click) {
|
||||
cursor_to_hovered_position();
|
||||
_trigger_paste.trigger_paste();
|
||||
}
|
||||
});
|
||||
|
||||
if (_drag && _hovered_position.constructed()) {
|
||||
_selection.end.construct(*_hovered_position);
|
||||
update_dialog = true;
|
||||
}
|
||||
|
||||
bool const clack = event.key_release(Input::BTN_LEFT);
|
||||
if (clack) {
|
||||
cursor_to_hovered_position();
|
||||
_drag = false;
|
||||
|
||||
if (_selection.defined())
|
||||
_trigger_copy.trigger_copy();
|
||||
}
|
||||
|
||||
event.handle_release([&] (Input::Keycode key) {
|
||||
if (shift_key(key)) _shift = false;
|
||||
if (control_key(key)) _control = false;
|
||||
});
|
||||
|
||||
bool const all_lines_visible =
|
||||
(_max_lines == ~0U) || (_text.upper_bound().value <= _max_lines);
|
||||
|
||||
if (!all_lines_visible) {
|
||||
event.handle_wheel([&] (int, int y) {
|
||||
|
||||
/* scroll at granulatory of 1/5th of vertical view size */
|
||||
y *= max(1U, _max_lines / 5);
|
||||
|
||||
if (y < 0)
|
||||
_scroll.y.value += -y;
|
||||
|
||||
if (y > 0)
|
||||
_scroll.y.value -= min((int)_scroll.y.value, y);
|
||||
|
||||
update_dialog = true;
|
||||
});
|
||||
}
|
||||
|
||||
/* adjust scroll position */
|
||||
if (all_lines_visible) {
|
||||
_scroll.y.value = 0;
|
||||
|
||||
} else if (orig_cursor != _cursor) {
|
||||
|
||||
/* ensure that the cursor remains visible */
|
||||
if (_cursor.y.value > 0)
|
||||
if (_scroll.y.value > _cursor.y.value - 1)
|
||||
_scroll.y.value = _cursor.y.value - 1;
|
||||
|
||||
if (_cursor.y.value == 0)
|
||||
_scroll.y.value = 0;
|
||||
|
||||
if (_scroll.y.value + _max_lines < _cursor.y.value + 2)
|
||||
_scroll.y.value = _cursor.y.value - _max_lines + 2;
|
||||
}
|
||||
|
||||
_clamp_scroll_position_to_upper_bound();
|
||||
|
||||
if (update_dialog)
|
||||
rom_session.trigger_update();
|
||||
}
|
||||
|
||||
|
||||
void Dialog::handle_hover(Xml_node const &hover)
|
||||
{
|
||||
Constructible<Position> orig_pos { };
|
||||
|
||||
if (_hovered_position.constructed())
|
||||
orig_pos.construct(*_hovered_position);
|
||||
|
||||
_hovered_position.destruct();
|
||||
|
||||
auto with_hovered_line = [&] (Xml_node node)
|
||||
{
|
||||
Text::Index const y {
|
||||
node.attribute_value("name", _text.upper_bound().value)
|
||||
+ _scroll.y.value };
|
||||
|
||||
_text.apply(y, [&] (Line const &line) {
|
||||
|
||||
Line::Index const max_x = line.upper_bound();
|
||||
|
||||
_hovered_position.construct(max_x, y);
|
||||
|
||||
node.with_sub_node("float", [&] (Xml_node node) {
|
||||
node.with_sub_node("label", [&] (Xml_node node) {
|
||||
|
||||
Line::Index const x {
|
||||
node.attribute_value("at", max_x.value) };
|
||||
|
||||
_hovered_position.construct(x, y);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
bool const hover_changed =
|
||||
(orig_pos.constructed() != _hovered_position.constructed());
|
||||
|
||||
bool const position_changed = orig_pos.constructed()
|
||||
&& _hovered_position.constructed()
|
||||
&& (*orig_pos != *_hovered_position);
|
||||
|
||||
bool const orig_text_hovered = _text_hovered;
|
||||
|
||||
_text_hovered = false;
|
||||
|
||||
hover.with_sub_node("frame", [&] (Xml_node node) {
|
||||
node.with_sub_node("button", [&] (Xml_node node) {
|
||||
|
||||
_text_hovered = true;
|
||||
|
||||
node.with_sub_node("float", [&] (Xml_node node) {
|
||||
node.with_sub_node("vbox", [&] (Xml_node node) {
|
||||
node.with_sub_node("hbox", [&] (Xml_node node) {
|
||||
with_hovered_line(node); }); }); }); }); });
|
||||
|
||||
if (hover_changed || position_changed || (_text_hovered != orig_text_hovered))
|
||||
rom_session.trigger_update();
|
||||
}
|
||||
|
||||
|
||||
Dialog::Dialog(Entrypoint &ep, Ram_allocator &ram, Region_map &rm,
|
||||
Allocator &alloc, Trigger_copy &trigger_copy,
|
||||
Trigger_paste &trigger_paste, Trigger_save &trigger_save)
|
||||
:
|
||||
Xml_producer("dialog"),
|
||||
rom_session(ep, ram, rm, *this),
|
||||
_alloc(alloc),
|
||||
_trigger_copy(trigger_copy),
|
||||
_trigger_paste(trigger_paste),
|
||||
_trigger_save(trigger_save)
|
||||
{
|
||||
clear();
|
||||
}
|
||||
|
||||
|
||||
void Dialog::clear()
|
||||
{
|
||||
Text::Index const first { 0 };
|
||||
|
||||
while (_text.exists(first))
|
||||
_text.destruct(first);
|
||||
|
||||
_cursor.x.value = 0;
|
||||
_cursor.y.value = 0;
|
||||
}
|
||||
|
||||
|
||||
void Dialog::insert_at_cursor_position(Codepoint c)
|
||||
{
|
||||
if (_printable(c)) {
|
||||
_insert_printable(c);
|
||||
_modification_count++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (c.value == CODEPOINT_NEWLINE)
|
||||
_handle_newline();
|
||||
}
|
||||
|
||||
|
||||
void Dialog::gen_clipboard_content(Xml_generator &xml) const
|
||||
{
|
||||
if (!_selection.defined())
|
||||
return;
|
||||
|
||||
auto for_each_selected_character = [&] (auto fn)
|
||||
{
|
||||
_selection.for_each_selected_line([&] (Text::Index const y, bool const last) {
|
||||
_text.apply(y, [&] (Line const &line) {
|
||||
_selection.with_selection_at_line(y, line, [&] (Line::Index x, unsigned n) {
|
||||
for (unsigned i = 0; i < n; i++)
|
||||
line.apply(Line::Index { x.value + i }, [&] (Codepoint c) {
|
||||
fn(c); }); }); });
|
||||
|
||||
if (!last)
|
||||
fn(Codepoint{'\n'});
|
||||
});
|
||||
};
|
||||
|
||||
for_each_selected_character([&] (Codepoint c) {
|
||||
String<10> const utf8(c);
|
||||
if (utf8.valid())
|
||||
xml.append_sanitized(utf8.string(), utf8.length() - 1);
|
||||
});
|
||||
}
|
254
repos/gems/src/app/text_area/dialog.h
Normal file
254
repos/gems/src/app/text_area/dialog.h
Normal file
@ -0,0 +1,254 @@
|
||||
/*
|
||||
* \brief Text dialog
|
||||
* \author Norman Feske
|
||||
* \date 2020-01-14
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (C) 2020 Genode Labs GmbH
|
||||
*
|
||||
* This file is part of the Genode OS framework, which is distributed
|
||||
* under the terms of the GNU Affero General Public License version 3.
|
||||
*/
|
||||
|
||||
#ifndef _DIALOG_H_
|
||||
#define _DIALOG_H_
|
||||
|
||||
/* Genode includes */
|
||||
#include <base/component.h>
|
||||
#include <base/session_object.h>
|
||||
#include <os/buffered_xml.h>
|
||||
#include <os/sandbox.h>
|
||||
#include <os/dynamic_rom_session.h>
|
||||
#include <input/event.h>
|
||||
#include <util/utf8.h>
|
||||
|
||||
/* local includes */
|
||||
#include <dynamic_array.h>
|
||||
#include <report.h>
|
||||
#include <types.h>
|
||||
|
||||
namespace Text_area { struct Dialog; }
|
||||
|
||||
|
||||
struct Text_area::Dialog : private Dynamic_rom_session::Xml_producer
|
||||
{
|
||||
public:
|
||||
|
||||
Dynamic_rom_session rom_session;
|
||||
|
||||
struct Trigger_copy : Interface, Noncopyable
|
||||
{
|
||||
virtual void trigger_copy() = 0;
|
||||
};
|
||||
|
||||
struct Trigger_paste : Interface, Noncopyable
|
||||
{
|
||||
virtual void trigger_paste() = 0;
|
||||
};
|
||||
|
||||
struct Trigger_save : Interface, Noncopyable
|
||||
{
|
||||
virtual void trigger_save() = 0;
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
Allocator &_alloc;
|
||||
|
||||
Trigger_copy &_trigger_copy;
|
||||
Trigger_paste &_trigger_paste;
|
||||
Trigger_save &_trigger_save;
|
||||
|
||||
struct Character : Codepoint
|
||||
{
|
||||
Character(Codepoint &codepoint) : Codepoint(codepoint) { }
|
||||
|
||||
void print(Output &out) const
|
||||
{
|
||||
if (value == '"')
|
||||
Genode::print(out, """);
|
||||
else if (value == 9)
|
||||
Genode::print(out, " ");
|
||||
else
|
||||
Codepoint::print(out);
|
||||
}
|
||||
};
|
||||
|
||||
using Line = Dynamic_array<Character>;
|
||||
using Text = Dynamic_array<Line>;
|
||||
|
||||
Text _text { _alloc };
|
||||
|
||||
struct Position
|
||||
{
|
||||
Line::Index x;
|
||||
Text::Index y;
|
||||
|
||||
Position(Line::Index x, Text::Index y) : x(x), y(y) { }
|
||||
|
||||
Position(Position const &other) : x(other.x), y(other.y) { }
|
||||
|
||||
bool operator != (Position const &other) const
|
||||
{
|
||||
return (x.value != other.x.value) || (y.value != other.y.value);
|
||||
}
|
||||
};
|
||||
|
||||
Position _cursor { { 0 }, { 0 } };
|
||||
|
||||
Position _scroll { { 0 }, { 0 } };
|
||||
|
||||
Constructible<Position> _hovered_position { };
|
||||
|
||||
unsigned _max_lines = ~0U;
|
||||
|
||||
bool _editable = false;
|
||||
|
||||
unsigned _modification_count = 0;
|
||||
|
||||
struct Selection
|
||||
{
|
||||
Constructible<Position> start;
|
||||
Constructible<Position> end;
|
||||
|
||||
void clear()
|
||||
{
|
||||
start.destruct();
|
||||
end .destruct();
|
||||
}
|
||||
|
||||
bool defined() const
|
||||
{
|
||||
return start.constructed() && end.constructed() && (*start != *end);
|
||||
}
|
||||
|
||||
template <typename FN>
|
||||
void for_each_selected_line(FN const &) const;
|
||||
|
||||
template <typename FN>
|
||||
void with_selection_at_line(Text::Index y, Line const &, FN const &) const;
|
||||
|
||||
/* generate dialog model */
|
||||
void gen_selected_line(Xml_generator &, Text::Index, Line const &) const;
|
||||
};
|
||||
|
||||
bool _drag = false;
|
||||
bool _shift = false;
|
||||
bool _control = false;
|
||||
bool _text_hovered = false;
|
||||
|
||||
Selection _selection { };
|
||||
|
||||
void produce_xml(Xml_generator &xml) override;
|
||||
|
||||
static bool _printable(Codepoint code)
|
||||
{
|
||||
if (!code.valid())
|
||||
return false;
|
||||
|
||||
if (code.value == '\t')
|
||||
return true;
|
||||
|
||||
return (code.value >= 0x20 && code.value < 0xf000);
|
||||
}
|
||||
|
||||
bool _cursor_at_last_line() const
|
||||
{
|
||||
return (_cursor.y.value + 1 >= _text.upper_bound().value);
|
||||
}
|
||||
|
||||
bool _cursor_at_end_of_line() const
|
||||
{
|
||||
bool result = false;
|
||||
_text.apply(_cursor.y, [&] (Line &line) {
|
||||
result = (_cursor.x.value >= line.upper_bound().value); });
|
||||
return result;
|
||||
}
|
||||
|
||||
void _tie_cursor_to_end_of_line()
|
||||
{
|
||||
_text.apply(_cursor.y, [&] (Line &line) {
|
||||
if (_cursor.x.value > line.upper_bound().value)
|
||||
_cursor.x = line.upper_bound(); });
|
||||
}
|
||||
|
||||
bool _end_of_text() const
|
||||
{
|
||||
return _cursor_at_last_line() && _cursor_at_end_of_line();
|
||||
}
|
||||
|
||||
void _clamp_scroll_position_to_upper_bound()
|
||||
{
|
||||
if (_max_lines != ~0U)
|
||||
if (_scroll.y.value + _max_lines > _text.upper_bound().value)
|
||||
_scroll.y.value = max(_text.upper_bound().value, _max_lines)
|
||||
- _max_lines;
|
||||
}
|
||||
|
||||
void _move_characters(Line &, Line &);
|
||||
void _delete_selection();
|
||||
void _insert_printable(Codepoint);
|
||||
void _handle_printable(Codepoint);
|
||||
void _handle_backspace();
|
||||
void _handle_delete();
|
||||
void _handle_newline();
|
||||
void _handle_left();
|
||||
void _handle_right();
|
||||
void _handle_up();
|
||||
void _handle_down();
|
||||
void _handle_pageup();
|
||||
void _handle_pagedown();
|
||||
void _handle_home();
|
||||
void _handle_end();
|
||||
|
||||
public:
|
||||
|
||||
Dialog(Entrypoint &, Ram_allocator &, Region_map &, Allocator &,
|
||||
Trigger_copy &, Trigger_paste &, Trigger_save &);
|
||||
|
||||
void editable(bool editable) { _editable = editable; }
|
||||
|
||||
unsigned modification_count() const { return _modification_count; }
|
||||
|
||||
void max_lines(unsigned max_lines) { _max_lines = max_lines; }
|
||||
|
||||
void handle_input_event(Input::Event const &);
|
||||
|
||||
void handle_hover(Xml_node const &hover);
|
||||
|
||||
void clear();
|
||||
|
||||
void append_newline()
|
||||
{
|
||||
_text.append(_alloc);
|
||||
}
|
||||
|
||||
void append_character(Codepoint c)
|
||||
{
|
||||
if (_printable(c)) {
|
||||
Text::Index const y { _text.upper_bound().value - 1 };
|
||||
_text.apply(y, [&] (Line &line) {
|
||||
line.append(c); });
|
||||
}
|
||||
}
|
||||
|
||||
/* insert character and advance cursor */
|
||||
void insert_at_cursor_position(Codepoint);
|
||||
|
||||
void gen_clipboard_content(Xml_generator &xml) const;
|
||||
|
||||
template <typename FN>
|
||||
void for_each_character(FN const &fn) const
|
||||
{
|
||||
_text.for_each([&] (Text::Index at, Line const &line) {
|
||||
line.for_each([&] (Line::Index, Character c) {
|
||||
fn(c); });
|
||||
|
||||
if (at.value + 1 < _text.upper_bound().value)
|
||||
fn(Codepoint{'\n'});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
#endif /* _DIALOG_H_ */
|
194
repos/gems/src/app/text_area/dynamic_array.h
Normal file
194
repos/gems/src/app/text_area/dynamic_array.h
Normal file
@ -0,0 +1,194 @@
|
||||
/*
|
||||
* \brief Dynamically growing array
|
||||
* \author Norman Feske
|
||||
* \date 2020-01-12
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (C) 2020 Genode Labs GmbH
|
||||
*
|
||||
* This file is part of the Genode OS framework, which is distributed
|
||||
* under the terms of the GNU Affero General Public License version 3.
|
||||
*/
|
||||
|
||||
#ifndef _DYNAMIC_ARRAY_H_
|
||||
#define _DYNAMIC_ARRAY_H_
|
||||
|
||||
/* Genode includes */
|
||||
#include <base/allocator.h>
|
||||
|
||||
namespace Text_area {
|
||||
|
||||
using namespace Genode;
|
||||
|
||||
template <typename>
|
||||
struct Dynamic_array;
|
||||
}
|
||||
|
||||
|
||||
template <typename ET>
|
||||
struct Text_area::Dynamic_array
|
||||
{
|
||||
public:
|
||||
|
||||
struct Index { unsigned value; };
|
||||
|
||||
private:
|
||||
|
||||
Allocator &_alloc;
|
||||
|
||||
using Element = Constructible<ET>;
|
||||
|
||||
Element *_array = nullptr;
|
||||
|
||||
unsigned _capacity = 0;
|
||||
unsigned _upper_bound = 0; /* index after last used element */
|
||||
|
||||
bool _index_valid(Index at) const
|
||||
{
|
||||
return (at.value < _upper_bound) && _array[at.value].constructed();
|
||||
}
|
||||
|
||||
/*
|
||||
* Noncopyable
|
||||
*/
|
||||
Dynamic_array(Dynamic_array const &other);
|
||||
void operator = (Dynamic_array const &);
|
||||
|
||||
public:
|
||||
|
||||
/**
|
||||
* Moving constructor
|
||||
*/
|
||||
Dynamic_array(Dynamic_array &other)
|
||||
:
|
||||
_alloc(other._alloc), _array(other._array),
|
||||
_capacity(other._capacity), _upper_bound(other._upper_bound)
|
||||
{
|
||||
other._array = nullptr;
|
||||
other._capacity = 0;
|
||||
other._upper_bound = 0;
|
||||
}
|
||||
|
||||
Dynamic_array(Allocator &alloc) : _alloc(alloc) { }
|
||||
|
||||
~Dynamic_array()
|
||||
{
|
||||
if (!_array)
|
||||
return;
|
||||
|
||||
clear();
|
||||
|
||||
_alloc.free(_array, _capacity*sizeof(Element));
|
||||
}
|
||||
|
||||
void clear()
|
||||
{
|
||||
if (_upper_bound > 0)
|
||||
for (unsigned i = _upper_bound; i > 0; i--)
|
||||
destruct(Index{i - 1});
|
||||
}
|
||||
|
||||
template <typename... ARGS>
|
||||
void insert(Index at, ARGS &&... args)
|
||||
{
|
||||
/* grow array if index exceeds current capacity or if it's full */
|
||||
if (at.value >= _capacity || _upper_bound == _capacity) {
|
||||
|
||||
size_t const new_capacity =
|
||||
2 * max(_capacity, max(8U, at.value));
|
||||
|
||||
Element *new_array = nullptr;
|
||||
try {
|
||||
(void)_alloc.alloc(sizeof(Element)*new_capacity, &new_array);
|
||||
|
||||
for (unsigned i = 0; i < new_capacity; i++)
|
||||
construct_at<Element>(&new_array[i]);
|
||||
}
|
||||
catch (... /* Out_of_ram, Out_of_caps */ ) { throw; }
|
||||
|
||||
if (_array) {
|
||||
for (unsigned i = 0; i < _upper_bound; i++)
|
||||
new_array[i].construct(*_array[i]);
|
||||
|
||||
_alloc.free(_array, sizeof(Element)*_capacity);
|
||||
}
|
||||
|
||||
_array = new_array;
|
||||
_capacity = new_capacity;
|
||||
}
|
||||
|
||||
/* make room for new element */
|
||||
if (_upper_bound > 0)
|
||||
for (unsigned i = _upper_bound; i > at.value; i--)
|
||||
_array[i].construct(*_array[i - 1]);
|
||||
|
||||
_array[at.value].construct(args...);
|
||||
|
||||
_upper_bound = max(at.value + 1, _upper_bound + 1);
|
||||
}
|
||||
|
||||
template <typename... ARGS>
|
||||
void append(ARGS &&... args) { insert(Index{_upper_bound}, args...); }
|
||||
|
||||
bool exists(Index at) const { return _index_valid(at); }
|
||||
|
||||
Index upper_bound() const { return Index { _upper_bound }; }
|
||||
|
||||
void destruct(Index at)
|
||||
{
|
||||
if (!_index_valid(at))
|
||||
return;
|
||||
|
||||
_array[at.value].destruct();
|
||||
|
||||
if (_upper_bound > 0)
|
||||
for (unsigned i = at.value; i < _upper_bound - 1; i++)
|
||||
_array[i].construct(*_array[i + 1]);
|
||||
|
||||
_upper_bound--;
|
||||
_array[_upper_bound].destruct();
|
||||
}
|
||||
|
||||
template <typename FN>
|
||||
void apply(Index at, FN const &fn)
|
||||
{
|
||||
if (_index_valid(at))
|
||||
fn(*_array[at.value]);
|
||||
}
|
||||
|
||||
template <typename FN>
|
||||
void apply(Index at, FN const &fn) const
|
||||
{
|
||||
if (_index_valid(at))
|
||||
fn(*_array[at.value]);
|
||||
}
|
||||
|
||||
struct Range { Index at; unsigned length; };
|
||||
|
||||
template <typename FN>
|
||||
void for_each(Range range, FN const &fn) const
|
||||
{
|
||||
unsigned const first = range.at.value;
|
||||
unsigned const limit = min(_upper_bound, first + range.length);
|
||||
|
||||
for (unsigned i = first; i < limit; i++)
|
||||
if (_array[i].constructed())
|
||||
fn(Index{i}, *_array[i]);
|
||||
}
|
||||
|
||||
template <typename FN>
|
||||
void for_each(FN const &fn) const
|
||||
{
|
||||
for_each(Range { .at = { 0U }, .length = ~0U }, fn);
|
||||
}
|
||||
|
||||
void print(Output &out) const
|
||||
{
|
||||
for (unsigned i = 0; i < _upper_bound; i++)
|
||||
if (_array[i].constructed())
|
||||
Genode::print(out, *_array[i]);
|
||||
}
|
||||
};
|
||||
|
||||
#endif /* _DYNAMIC_ARRAY_H_ */
|
28
repos/gems/src/app/text_area/input_event_handler.h
Normal file
28
repos/gems/src/app/text_area/input_event_handler.h
Normal file
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* \brief Interface for handling input events
|
||||
* \author Norman Feske
|
||||
* \date 2018-05-02
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (C) 2018 Genode Labs GmbH
|
||||
*
|
||||
* This file is part of the Genode OS framework, which is distributed
|
||||
* under the terms of the GNU Affero General Public License version 3.
|
||||
*/
|
||||
|
||||
#ifndef _INPUT_EVENT_HANDLER_H_
|
||||
#define _INPUT_EVENT_HANDLER_H_
|
||||
|
||||
/* Genode includes */
|
||||
#include <util/interface.h>
|
||||
#include <input/event.h>
|
||||
|
||||
namespace Nitpicker { struct Input_event_handler; }
|
||||
|
||||
struct Nitpicker::Input_event_handler : Genode::Interface
|
||||
{
|
||||
virtual void handle_input_event(Input::Event const &) = 0;
|
||||
};
|
||||
|
||||
#endif /* _INPUT_EVENT_HANDLER_H_ */
|
525
repos/gems/src/app/text_area/main.cc
Normal file
525
repos/gems/src/app/text_area/main.cc
Normal file
@ -0,0 +1,525 @@
|
||||
/*
|
||||
* \brief Simple text viewer and editor
|
||||
* \author Norman Feske
|
||||
* \date 2020-01-12
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (C) 2020 Genode Labs GmbH
|
||||
*
|
||||
* This file is part of the Genode OS framework, which is distributed
|
||||
* under the terms of the GNU Affero General Public License version 3.
|
||||
*/
|
||||
|
||||
/* Genode includes */
|
||||
#include <base/component.h>
|
||||
#include <base/session_object.h>
|
||||
#include <base/attached_rom_dataspace.h>
|
||||
#include <base/buffered_output.h>
|
||||
#include <os/buffered_xml.h>
|
||||
#include <os/sandbox.h>
|
||||
#include <os/dynamic_rom_session.h>
|
||||
#include <os/vfs.h>
|
||||
#include <os/reporter.h>
|
||||
|
||||
/* local includes */
|
||||
#include <nitpicker.h>
|
||||
#include <report.h>
|
||||
#include <dialog.h>
|
||||
#include <new_file.h>
|
||||
#include <child_state.h>
|
||||
|
||||
namespace Text_area { struct Main; }
|
||||
|
||||
struct Text_area::Main : Sandbox::Local_service_base::Wakeup,
|
||||
Sandbox::State_handler,
|
||||
Nitpicker::Input_event_handler,
|
||||
Dialog::Trigger_copy, Dialog::Trigger_paste,
|
||||
Dialog::Trigger_save
|
||||
{
|
||||
Env &_env;
|
||||
|
||||
Heap _heap { _env.ram(), _env.rm() };
|
||||
|
||||
Attached_rom_dataspace _config { _env, "config" };
|
||||
|
||||
Root_directory _vfs { _env, _heap, _config.xml().sub_node("vfs") };
|
||||
|
||||
unsigned _min_width = 0;
|
||||
unsigned _min_height = 0;
|
||||
|
||||
Registry<Child_state> _children { };
|
||||
|
||||
Child_state _menu_view_child_state { _children, "menu_view",
|
||||
Ram_quota { 4*1024*1024 },
|
||||
Cap_quota { 200 } };
|
||||
/**
|
||||
* Sandbox::State_handler
|
||||
*/
|
||||
void handle_sandbox_state() override
|
||||
{
|
||||
/* obtain current sandbox state */
|
||||
Buffered_xml state(_heap, "state", [&] (Xml_generator &xml) {
|
||||
_sandbox.generate_state_report(xml);
|
||||
});
|
||||
|
||||
bool reconfiguration_needed = false;
|
||||
|
||||
state.with_xml_node([&] (Xml_node state) {
|
||||
state.for_each_sub_node("child", [&] (Xml_node const &child) {
|
||||
if (_menu_view_child_state.apply_child_state_report(child))
|
||||
reconfiguration_needed = true; }); });
|
||||
|
||||
if (reconfiguration_needed)
|
||||
_update_sandbox_config();
|
||||
}
|
||||
|
||||
Sandbox _sandbox { _env, *this };
|
||||
|
||||
typedef Sandbox::Local_service<Nitpicker::Session_component> Nitpicker_service;
|
||||
|
||||
Nitpicker_service _nitpicker_service { _sandbox, *this };
|
||||
|
||||
typedef Sandbox::Local_service<Dynamic_rom_session> Rom_service;
|
||||
|
||||
Rom_service _rom_service { _sandbox, *this };
|
||||
|
||||
typedef Sandbox::Local_service<Report::Session_component> Report_service;
|
||||
|
||||
Report_service _report_service { _sandbox, *this };
|
||||
|
||||
void _handle_hover(Xml_node const &node)
|
||||
{
|
||||
if (!node.has_sub_node("dialog"))
|
||||
_dialog.handle_hover(Xml_node("<empty/>"));
|
||||
|
||||
node.with_sub_node("dialog", [&] (Xml_node const &dialog) {
|
||||
_dialog.handle_hover(dialog); });
|
||||
}
|
||||
|
||||
Report::Session_component::Xml_handler<Main>
|
||||
_hover_handler { *this, &Main::_handle_hover };
|
||||
|
||||
Dialog _dialog { _env.ep(), _env.ram(), _env.rm(), _heap, *this, *this, *this };
|
||||
|
||||
Constructible<Expanding_reporter> _saved_reporter { };
|
||||
|
||||
struct Saved_version { unsigned value; } _saved_version { 0 };
|
||||
|
||||
/*
|
||||
* The dialog's modification count at the time of last saving
|
||||
*/
|
||||
struct Modification_count { unsigned value; } _saved_modification_count { 0 };
|
||||
|
||||
bool _modified() const
|
||||
{
|
||||
return _dialog.modification_count() != _saved_modification_count.value;
|
||||
}
|
||||
|
||||
void _generate_saved_report()
|
||||
{
|
||||
if (!_saved_reporter.constructed())
|
||||
return;
|
||||
|
||||
_saved_reporter->generate([&] (Xml_generator &xml) {
|
||||
xml.attribute("version", _saved_version.value);
|
||||
if (_modified())
|
||||
xml.attribute("modified", "yes");
|
||||
});
|
||||
}
|
||||
|
||||
void _generate_sandbox_config(Xml_generator &xml) const
|
||||
{
|
||||
xml.node("report", [&] () {
|
||||
xml.attribute("child_ram", "yes");
|
||||
xml.attribute("child_caps", "yes");
|
||||
xml.attribute("delay_ms", 20*1000);
|
||||
});
|
||||
xml.node("parent-provides", [&] () {
|
||||
|
||||
auto service_node = [&] (char const *name) {
|
||||
xml.node("service", [&] () {
|
||||
xml.attribute("name", name); }); };
|
||||
|
||||
service_node("ROM");
|
||||
service_node("CPU");
|
||||
service_node("PD");
|
||||
service_node("LOG");
|
||||
service_node("File_system");
|
||||
service_node("Nitpicker");
|
||||
service_node("Timer");
|
||||
service_node("Report");
|
||||
});
|
||||
|
||||
xml.node("start", [&] () {
|
||||
_menu_view_child_state.gen_start_node_content(xml);
|
||||
|
||||
xml.node("config", [&] () {
|
||||
xml.attribute("xpos", "100");
|
||||
xml.attribute("ypos", "50");
|
||||
|
||||
if (_min_width) xml.attribute("width", _min_width);
|
||||
if (_min_height) xml.attribute("height", _min_height);
|
||||
|
||||
xml.node("report", [&] () {
|
||||
xml.attribute("hover", "yes"); });
|
||||
|
||||
xml.node("libc", [&] () {
|
||||
xml.attribute("stderr", "/dev/log"); });
|
||||
|
||||
xml.node("vfs", [&] () {
|
||||
xml.node("tar", [&] () {
|
||||
xml.attribute("name", "menu_view_styles.tar"); });
|
||||
xml.node("dir", [&] () {
|
||||
xml.attribute("name", "dev");
|
||||
xml.node("log", [&] () { });
|
||||
});
|
||||
xml.node("dir", [&] () {
|
||||
xml.attribute("name", "fonts");
|
||||
xml.node("fs", [&] () {
|
||||
xml.attribute("label", "fonts");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
xml.node("route", [&] () {
|
||||
|
||||
xml.node("service", [&] () {
|
||||
xml.attribute("name", "ROM");
|
||||
xml.attribute("label", "dialog");
|
||||
xml.node("local", [&] () { });
|
||||
});
|
||||
|
||||
xml.node("service", [&] () {
|
||||
xml.attribute("name", "Report");
|
||||
xml.attribute("label", "hover");
|
||||
xml.node("local", [&] () { });
|
||||
});
|
||||
|
||||
xml.node("service", [&] () {
|
||||
xml.attribute("name", "Nitpicker");
|
||||
xml.node("local", [&] () { });
|
||||
});
|
||||
|
||||
xml.node("service", [&] () {
|
||||
xml.attribute("name", "File_system");
|
||||
xml.attribute("label", "fonts");
|
||||
xml.node("parent", [&] () {
|
||||
xml.attribute("label", "fonts"); });
|
||||
});
|
||||
|
||||
xml.node("any-service", [&] () {
|
||||
xml.node("parent", [&] () { }); });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sandbox::Local_service_base::Wakeup interface
|
||||
*/
|
||||
void wakeup_local_service() override
|
||||
{
|
||||
_rom_service.for_each_requested_session([&] (Rom_service::Request &request) {
|
||||
|
||||
if (request.label == "menu_view -> dialog")
|
||||
request.deliver_session(_dialog.rom_session);
|
||||
else
|
||||
request.deny();
|
||||
});
|
||||
|
||||
_report_service.for_each_requested_session([&] (Report_service::Request &request) {
|
||||
|
||||
if (request.label == "menu_view -> hover") {
|
||||
Report::Session_component &session = *new (_heap)
|
||||
Report::Session_component(_env, _hover_handler,
|
||||
_env.ep(),
|
||||
request.resources, "", request.diag);
|
||||
request.deliver_session(session);
|
||||
}
|
||||
});
|
||||
|
||||
_report_service.for_each_session_to_close([&] (Report::Session_component &session) {
|
||||
|
||||
destroy(_heap, &session);
|
||||
return Report_service::Close_response::CLOSED;
|
||||
});
|
||||
|
||||
_nitpicker_service.for_each_requested_session([&] (Nitpicker_service::Request &request) {
|
||||
|
||||
Nitpicker::Session_component &session = *new (_heap)
|
||||
Nitpicker::Session_component(_env, *this, _env.ep(),
|
||||
request.resources, "", request.diag);
|
||||
|
||||
request.deliver_session(session);
|
||||
});
|
||||
|
||||
_nitpicker_service.for_each_upgraded_session([&] (Nitpicker::Session_component &session,
|
||||
Session::Resources const &amount) {
|
||||
session.upgrade(amount);
|
||||
return Nitpicker_service::Upgrade_response::CONFIRMED;
|
||||
});
|
||||
|
||||
_nitpicker_service.for_each_session_to_close([&] (Nitpicker::Session_component &session) {
|
||||
|
||||
destroy(_heap, &session);
|
||||
return Nitpicker_service::Close_response::CLOSED;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Nitpicker::Input_event_handler interface
|
||||
*/
|
||||
void handle_input_event(Input::Event const &event) override
|
||||
{
|
||||
bool const orig_modified = _modified();
|
||||
|
||||
_dialog.handle_input_event(event);
|
||||
|
||||
if (_modified() != orig_modified)
|
||||
_generate_saved_report();
|
||||
}
|
||||
|
||||
Directory::Path _path() const
|
||||
{
|
||||
return _config.xml().attribute_value("path", Directory::Path());
|
||||
}
|
||||
|
||||
void _watch(bool enabled)
|
||||
{
|
||||
_watch_handler.conditional(enabled, _vfs, _path(), *this, &Main::_handle_watch);
|
||||
}
|
||||
|
||||
bool _editable() const { return !_watch_handler.constructed(); }
|
||||
|
||||
void _load()
|
||||
{
|
||||
struct Max_line_len_exceeded : Exception { };
|
||||
|
||||
try {
|
||||
File_content content(_heap, _vfs, _path(), File_content::Limit{1024*1024});
|
||||
|
||||
enum { MAX_LINE_LEN = 1000 };
|
||||
typedef String<MAX_LINE_LEN + 1> Content_line;
|
||||
|
||||
_dialog.clear();
|
||||
content.for_each_line<Content_line>([&] (Content_line const &line) {
|
||||
|
||||
if (line.length() == Content_line::capacity()) {
|
||||
warning("maximum line length ", (size_t)MAX_LINE_LEN, " exceeded");
|
||||
throw Max_line_len_exceeded();
|
||||
}
|
||||
|
||||
_dialog.append_newline();
|
||||
|
||||
for (Utf8_ptr utf8(line.string()); utf8.complete(); utf8 = utf8.next())
|
||||
_dialog.append_character(utf8.codepoint());
|
||||
});
|
||||
|
||||
}
|
||||
catch (...) {
|
||||
warning("failed to load file ", _path());
|
||||
_dialog.clear();
|
||||
}
|
||||
|
||||
_dialog.rom_session.trigger_update();
|
||||
}
|
||||
|
||||
Constructible<Watch_handler<Main>> _watch_handler { };
|
||||
|
||||
void _handle_watch() { _load(); }
|
||||
|
||||
/*
|
||||
* Copy
|
||||
*/
|
||||
|
||||
Constructible<Expanding_reporter> _clipboard_reporter { };
|
||||
|
||||
/**
|
||||
* Dialog::Trigger_copy interface
|
||||
*/
|
||||
void trigger_copy() override
|
||||
{
|
||||
if (!_clipboard_reporter.constructed())
|
||||
return;
|
||||
|
||||
_clipboard_reporter->generate([&] (Xml_generator &xml) {
|
||||
_dialog.gen_clipboard_content(xml); });
|
||||
}
|
||||
|
||||
/*
|
||||
* Paste
|
||||
*/
|
||||
|
||||
Constructible<Attached_rom_dataspace> _clipboard_rom { };
|
||||
|
||||
enum { PASTE_BUFFER_SIZE = 64*1024 };
|
||||
struct Paste_buffer { char buffer[PASTE_BUFFER_SIZE]; } _paste_buffer { };
|
||||
|
||||
/**
|
||||
* Dialog::Trigger_paste interface
|
||||
*/
|
||||
void trigger_paste() override
|
||||
{
|
||||
if (!_editable())
|
||||
return;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
for (Utf8_ptr utf8(_paste_buffer.buffer); utf8.complete(); utf8 = utf8.next())
|
||||
_dialog.insert_at_cursor_position(utf8.codepoint());
|
||||
|
||||
_dialog.rom_session.trigger_update();
|
||||
}
|
||||
|
||||
/*
|
||||
* Save
|
||||
*/
|
||||
|
||||
void _save_to_file(Directory::Path const &path)
|
||||
{
|
||||
bool write_error = false;
|
||||
|
||||
try {
|
||||
New_file new_file(_vfs, path);
|
||||
|
||||
auto write = [&] (char const *cstring)
|
||||
{
|
||||
switch (new_file.append(cstring, strlen(cstring))) {
|
||||
case New_file::Append_result::OK: break;
|
||||
case New_file::Append_result::WRITE_ERROR:
|
||||
write_error = true;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
Buffered_output<1024, decltype(write)> output(write);
|
||||
|
||||
_dialog.for_each_character([&] (Codepoint c) { print(output, c); });
|
||||
}
|
||||
catch (New_file::Create_failed) {
|
||||
error("file creation failed while saving file"); }
|
||||
|
||||
if (write_error) {
|
||||
error("write error while saving ", _path());
|
||||
return;
|
||||
}
|
||||
|
||||
_saved_modification_count.value = _dialog.modification_count();
|
||||
|
||||
_generate_saved_report();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog::Trigger_save interface
|
||||
*/
|
||||
void trigger_save() override
|
||||
{
|
||||
if (!_editable())
|
||||
return;
|
||||
|
||||
_saved_version.value++;
|
||||
_save_to_file(_path());
|
||||
}
|
||||
|
||||
bool _initial_config = true;
|
||||
|
||||
void _handle_config()
|
||||
{
|
||||
_config.update();
|
||||
|
||||
Xml_node const config = _config.xml();
|
||||
|
||||
_min_width = config.attribute_value("min_width", 0U);
|
||||
_min_height = config.attribute_value("min_height", 0U);
|
||||
|
||||
bool const copy_enabled = config.attribute_value("copy", false);
|
||||
bool const paste_enabled = config.attribute_value("paste", false);
|
||||
|
||||
_clipboard_reporter.conditional(copy_enabled, _env, "clipboard", "clipboard");
|
||||
_clipboard_rom .conditional(paste_enabled, _env, "clipboard");
|
||||
|
||||
_dialog.max_lines(config.attribute_value("max_lines", ~0U));
|
||||
|
||||
_watch(config.attribute_value("watch", false));
|
||||
|
||||
_dialog.editable(_editable());
|
||||
|
||||
if (_editable()) {
|
||||
bool const orig_saved_reporter_enabled = _saved_reporter.constructed();
|
||||
|
||||
config.with_sub_node("report", [&] (Xml_node const &node) {
|
||||
_saved_reporter.conditional(node.attribute_value("saved", false),
|
||||
_env, "saved", "saved"); });
|
||||
|
||||
bool const saved_report_out_of_date =
|
||||
!orig_saved_reporter_enabled && _saved_reporter.constructed();
|
||||
|
||||
Saved_version const orig_saved_version = _saved_version;
|
||||
|
||||
config.with_sub_node("save", [&] (Xml_node const &node) {
|
||||
_saved_version.value =
|
||||
node.attribute_value("version", _saved_version.value); });
|
||||
|
||||
bool const saved_version_changed =
|
||||
(_saved_version.value != orig_saved_version.value);
|
||||
|
||||
if (saved_version_changed || saved_report_out_of_date) {
|
||||
|
||||
if (!_initial_config)
|
||||
_save_to_file(_path());
|
||||
else
|
||||
_generate_saved_report();
|
||||
}
|
||||
}
|
||||
|
||||
_initial_config = false;
|
||||
}
|
||||
|
||||
Signal_handler<Main> _config_handler {
|
||||
_env.ep(), *this, &Main::_handle_config };
|
||||
|
||||
void _update_sandbox_config()
|
||||
{
|
||||
Buffered_xml const config { _heap, "config", [&] (Xml_generator &xml) {
|
||||
_generate_sandbox_config(xml); } };
|
||||
|
||||
config.with_xml_node([&] (Xml_node const &config) {
|
||||
_sandbox.apply_config(config); });
|
||||
}
|
||||
|
||||
Main(Env &env)
|
||||
:
|
||||
_env(env)
|
||||
{
|
||||
/*
|
||||
* The '_load' must be performed before '_handle_config' because
|
||||
* '_handle_config' may call '_save_to_file' if the <config> contains a
|
||||
* <saved> node.
|
||||
*/
|
||||
_load();
|
||||
|
||||
_config.sigh(_config_handler);
|
||||
_handle_config();
|
||||
_update_sandbox_config();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
void Component::construct(Genode::Env &env) { static Text_area::Main main(env); }
|
||||
|
147
repos/gems/src/app/text_area/new_file.h
Normal file
147
repos/gems/src/app/text_area/new_file.h
Normal file
@ -0,0 +1,147 @@
|
||||
/*
|
||||
* \brief Utility for writing data to a file via the Genode VFS library
|
||||
* \author Norman Feske
|
||||
* \date 2020-01-25
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (C) 2020 Genode Labs GmbH
|
||||
*
|
||||
* This file is part of the Genode OS framework, which is distributed
|
||||
* under the terms of the GNU Affero General Public License version 3.
|
||||
*/
|
||||
|
||||
#ifndef _NEW_FILE_H_
|
||||
#define _NEW_FILE_H_
|
||||
|
||||
#include <os/vfs.h>
|
||||
|
||||
namespace Genode { class New_file; }
|
||||
|
||||
class Genode::New_file : Noncopyable
|
||||
{
|
||||
public:
|
||||
|
||||
struct Create_failed : Exception { };
|
||||
|
||||
private:
|
||||
|
||||
Entrypoint &_ep;
|
||||
Allocator &_alloc;
|
||||
Vfs::File_system &_fs;
|
||||
Vfs::Vfs_handle &_handle;
|
||||
|
||||
Vfs::Vfs_handle &_init_handle(Directory::Path const &path)
|
||||
{
|
||||
unsigned mode = Vfs::Directory_service::OPEN_MODE_WRONLY;
|
||||
|
||||
Vfs::Directory_service::Stat stat { };
|
||||
if (_fs.stat(path.string(), stat) != Vfs::Directory_service::STAT_OK)
|
||||
mode |= Vfs::Directory_service::OPEN_MODE_CREATE;
|
||||
|
||||
Vfs::Vfs_handle *handle_ptr = nullptr;
|
||||
Vfs::Directory_service::Open_result const res =
|
||||
_fs.open(path.string(), mode, &handle_ptr, _alloc);
|
||||
|
||||
if (res != Vfs::Directory_service::OPEN_OK || (handle_ptr == nullptr)) {
|
||||
error("failed to create file '", path, "'");
|
||||
throw Create_failed();
|
||||
}
|
||||
|
||||
handle_ptr->fs().ftruncate(handle_ptr, 0);
|
||||
|
||||
return *handle_ptr;
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* \throw Open_failed
|
||||
*/
|
||||
New_file(Vfs::Env &env, Directory::Path const &path)
|
||||
:
|
||||
_ep(env.env().ep()), _alloc(env.alloc()), _fs(env.root_dir()),
|
||||
_handle(_init_handle(path))
|
||||
{ }
|
||||
|
||||
~New_file()
|
||||
{
|
||||
while (_handle.fs().queue_sync(&_handle) == false)
|
||||
_ep.wait_and_dispatch_one_io_signal();
|
||||
|
||||
for (bool sync_done = false; !sync_done; ) {
|
||||
|
||||
switch (_handle.fs().complete_sync(&_handle)) {
|
||||
|
||||
case Vfs::File_io_service::SYNC_QUEUED:
|
||||
break;
|
||||
|
||||
case Vfs::File_io_service::SYNC_ERR_INVALID:
|
||||
warning("could not complete file sync operation");
|
||||
sync_done = true;
|
||||
break;
|
||||
|
||||
case Vfs::File_io_service::SYNC_OK:
|
||||
sync_done = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!sync_done)
|
||||
_ep.wait_and_dispatch_one_io_signal();
|
||||
}
|
||||
_handle.ds().close(&_handle);
|
||||
}
|
||||
|
||||
enum class Append_result { OK, WRITE_ERROR };
|
||||
|
||||
Append_result append(char const *src, size_t size)
|
||||
{
|
||||
bool write_error = false;
|
||||
|
||||
size_t remaining_bytes = size;
|
||||
|
||||
while (remaining_bytes > 0 && !write_error) {
|
||||
|
||||
bool stalled = false;
|
||||
|
||||
try {
|
||||
Vfs::file_size out_count = 0;
|
||||
|
||||
using Write_result = Vfs::File_io_service::Write_result;
|
||||
|
||||
switch (_handle.fs().write(&_handle, src, remaining_bytes,
|
||||
out_count)) {
|
||||
|
||||
case Write_result::WRITE_ERR_AGAIN:
|
||||
case Write_result::WRITE_ERR_WOULD_BLOCK:
|
||||
stalled = true;
|
||||
break;
|
||||
|
||||
case Write_result::WRITE_ERR_INVALID:
|
||||
case Write_result::WRITE_ERR_IO:
|
||||
case Write_result::WRITE_ERR_INTERRUPT:
|
||||
write_error = true;
|
||||
break;
|
||||
|
||||
case Write_result::WRITE_OK:
|
||||
out_count = min(remaining_bytes, out_count);
|
||||
remaining_bytes -= out_count;
|
||||
src += out_count;
|
||||
_handle.advance_seek(out_count);
|
||||
break;
|
||||
};
|
||||
}
|
||||
catch (Vfs::File_io_service::Insufficient_buffer) {
|
||||
stalled = true; }
|
||||
|
||||
if (stalled)
|
||||
_ep.wait_and_dispatch_one_io_signal();
|
||||
}
|
||||
return write_error ? Append_result::WRITE_ERROR
|
||||
: Append_result::OK;
|
||||
}
|
||||
};
|
||||
|
||||
#endif /* _NEW_FILE_H_ */
|
122
repos/gems/src/app/text_area/nitpicker.h
Normal file
122
repos/gems/src/app/text_area/nitpicker.h
Normal file
@ -0,0 +1,122 @@
|
||||
/*
|
||||
* \brief Nitpicker wrapper for monitoring the user input of GUI components
|
||||
* \author Norman Feske
|
||||
* \date 2020-01-12
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (C) 2020 Genode Labs GmbH
|
||||
*
|
||||
* This file is part of the Genode OS framework, which is distributed
|
||||
* under the terms of the GNU Affero General Public License version 3.
|
||||
*/
|
||||
|
||||
#ifndef _NITPICKER_H_
|
||||
#define _NITPICKER_H_
|
||||
|
||||
/* Genode includes */
|
||||
#include <input/component.h>
|
||||
#include <base/session_object.h>
|
||||
#include <nitpicker_session/connection.h>
|
||||
|
||||
/* local includes */
|
||||
#include <input_event_handler.h>
|
||||
|
||||
namespace Nitpicker {
|
||||
|
||||
using namespace Genode;
|
||||
|
||||
struct Session_component;
|
||||
}
|
||||
|
||||
|
||||
struct Nitpicker::Session_component : Session_object<Nitpicker::Session>
|
||||
{
|
||||
Env &_env;
|
||||
|
||||
Input_event_handler &_event_handler;
|
||||
|
||||
Nitpicker::Connection _connection;
|
||||
|
||||
Input::Session_component _input_component { _env, _env.ram() };
|
||||
|
||||
Signal_handler<Session_component> _input_handler {
|
||||
_env.ep(), *this, &Session_component::_handle_input };
|
||||
|
||||
void _handle_input()
|
||||
{
|
||||
_connection.input()->for_each_event([&] (Input::Event ev) {
|
||||
|
||||
/* handle event locally within the sculpt manager */
|
||||
_event_handler.handle_input_event(ev);
|
||||
|
||||
_input_component.submit(ev);
|
||||
});
|
||||
}
|
||||
|
||||
template <typename... ARGS>
|
||||
Session_component(Env &env, Input_event_handler &event_handler, ARGS &&... args)
|
||||
:
|
||||
Session_object(args...),
|
||||
_env(env), _event_handler(event_handler),
|
||||
_connection(env, _label.string())
|
||||
{
|
||||
_connection.input()->sigh(_input_handler);
|
||||
_env.ep().manage(_input_component);
|
||||
_input_component.event_queue().enabled(true);
|
||||
}
|
||||
|
||||
~Session_component() { _env.ep().dissolve(_input_component); }
|
||||
|
||||
void upgrade(Session::Resources const &resources)
|
||||
{
|
||||
_connection.upgrade(resources);
|
||||
}
|
||||
|
||||
Framebuffer::Session_capability framebuffer_session() override {
|
||||
return _connection.framebuffer_session(); }
|
||||
|
||||
Input::Session_capability input_session() override {
|
||||
return _input_component.cap(); }
|
||||
|
||||
View_handle create_view(View_handle parent) override {
|
||||
return _connection.create_view(parent); }
|
||||
|
||||
void destroy_view(View_handle view) override {
|
||||
_connection.destroy_view(view); }
|
||||
|
||||
View_handle view_handle(View_capability view_cap, View_handle handle) override {
|
||||
return _connection.view_handle(view_cap, handle); }
|
||||
|
||||
View_capability view_capability(View_handle view) override {
|
||||
return _connection.view_capability(view); }
|
||||
|
||||
void release_view_handle(View_handle view) override {
|
||||
_connection.release_view_handle(view); }
|
||||
|
||||
Dataspace_capability command_dataspace() override {
|
||||
return _connection.command_dataspace(); }
|
||||
|
||||
void execute() override {
|
||||
_connection.execute(); }
|
||||
|
||||
Framebuffer::Mode mode() override {
|
||||
return _connection.mode(); }
|
||||
|
||||
void mode_sigh(Signal_context_capability sigh) override {
|
||||
_connection.mode_sigh(sigh); }
|
||||
|
||||
void buffer(Framebuffer::Mode mode, bool use_alpha) override
|
||||
{
|
||||
/*
|
||||
* Do not call 'Connection::buffer' to avoid paying session quota
|
||||
* from our own budget.
|
||||
*/
|
||||
_connection.Client::buffer(mode, use_alpha);
|
||||
}
|
||||
|
||||
void focus(Capability<Nitpicker::Session> session) override {
|
||||
_connection.focus(session); }
|
||||
};
|
||||
|
||||
#endif /* _NITPICKER_H_ */
|
89
repos/gems/src/app/text_area/report.h
Normal file
89
repos/gems/src/app/text_area/report.h
Normal file
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* \brief Report session provided to the sandbox
|
||||
* \author Norman Feske
|
||||
* \date 2020-01-14
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (C) 2020 Genode Labs GmbH
|
||||
*
|
||||
* This file is part of the Genode OS framework, which is distributed
|
||||
* under the terms of the GNU Affero General Public License version 3.
|
||||
*/
|
||||
|
||||
#ifndef _REPORT_H_
|
||||
#define _REPORT_H_
|
||||
|
||||
/* Genode includes */
|
||||
#include <base/session_object.h>
|
||||
#include <report_session/report_session.h>
|
||||
|
||||
namespace Report {
|
||||
|
||||
using namespace Genode;
|
||||
|
||||
struct Session_component;
|
||||
}
|
||||
|
||||
|
||||
class Report::Session_component : public Session_object<Report::Session>
|
||||
{
|
||||
public:
|
||||
|
||||
struct Handler_base : Interface, Genode::Noncopyable
|
||||
{
|
||||
virtual void handle_report(char const *, size_t) = 0;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct Xml_handler : Handler_base
|
||||
{
|
||||
T &_obj;
|
||||
void (T::*_member) (Xml_node const &);
|
||||
|
||||
Xml_handler(T &obj, void (T::*member)(Xml_node const &))
|
||||
: _obj(obj), _member(member) { }
|
||||
|
||||
void handle_report(char const *start, size_t length) override
|
||||
{
|
||||
(_obj.*_member)(Xml_node(start, length));
|
||||
}
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
Attached_ram_dataspace _ds;
|
||||
|
||||
Handler_base &_handler;
|
||||
|
||||
|
||||
/*******************************
|
||||
** Report::Session interface **
|
||||
*******************************/
|
||||
|
||||
Dataspace_capability dataspace() override { return _ds.cap(); }
|
||||
|
||||
void submit(size_t length) override
|
||||
{
|
||||
_handler.handle_report(_ds.local_addr<char const>(),
|
||||
min(_ds.size(), length));
|
||||
}
|
||||
|
||||
void response_sigh(Signal_context_capability) override { }
|
||||
|
||||
size_t obtain_response() override { return 0; }
|
||||
|
||||
public:
|
||||
|
||||
template <typename... ARGS>
|
||||
Session_component(Env &env, Handler_base &handler,
|
||||
Entrypoint &ep, Resources const &resources,
|
||||
ARGS &&... args)
|
||||
:
|
||||
Session_object(ep, resources, args...),
|
||||
_ds(env.ram(), env.rm(), resources.ram_quota.value),
|
||||
_handler(handler)
|
||||
{ }
|
||||
};
|
||||
|
||||
#endif /* _REPORT_H_ */
|
4
repos/gems/src/app/text_area/target.mk
Normal file
4
repos/gems/src/app/text_area/target.mk
Normal file
@ -0,0 +1,4 @@
|
||||
TARGET = text_area
|
||||
SRC_CC = main.cc dialog.cc
|
||||
LIBS += base sandbox vfs
|
||||
INC_DIR += $(PRG_DIR)
|
21
repos/gems/src/app/text_area/types.h
Normal file
21
repos/gems/src/app/text_area/types.h
Normal file
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* \brief Common types
|
||||
* \author Norman Feske
|
||||
* \date 2020-01-14
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (C) 2012 Genode Labs GmbH
|
||||
*
|
||||
* This file is part of the Genode OS framework, which is distributed
|
||||
* under the terms of the GNU Affero General Public License version 3.
|
||||
*/
|
||||
|
||||
#ifndef _TYPES_H_
|
||||
#define _TYPES_H_
|
||||
|
||||
namespace Genode { }
|
||||
|
||||
namespace Text_area { using namespace Genode; }
|
||||
|
||||
#endif /* _TYPES_H_ */
|
Loading…
Reference in New Issue
Block a user