window_layouter: handle drag/drop as actions

This patch moves the formerly hard-wired drag-and-drop handling
to the configuration level by introducing the actions "drag" and "drop".

To aid the robust handling of release events matching their
corresponding press events, the patch refines the policy-matching of the
current combination of keys against the hierarchy of <press> and
<release> nodes. If no policy for a concrete combination exists, a
release event also considers the policy of its matching <press> node.
This way, the regular drag-and-drop rules can be expressed as

  <press key="BTN_LEFT" action="drag">
     <release key="BTN_LEFT" action="drop"/>
  </press>

This also works when releasing BTN_LEFT while pressing additional keys,
for which no policy exists.

With this change, the layouter supports the matching of multiple key
sequences instead of only one, thereby supporting multiple actions at
once and allowing for decoupling different user interactions in the
configuration.

Issue #5403
This commit is contained in:
Norman Feske 2024-12-10 17:30:10 +01:00 committed by Christian Helmuth
parent 6c7cbb2c5e
commit 7fc060438a
7 changed files with 135 additions and 78 deletions

View File

@ -19,19 +19,24 @@
<assign label_prefix="" target="screen_1" xpos="any" ypos="any"/>
</rules>
<press key="KEY_SCREEN" action="release_grab">
<press key="KEY_SCREEN">
<press key="KEY_TAB" action="next_window">
<release key="KEY_TAB">
<release key="KEY_SCREEN" action="raise_window"/>
</release>
<release key="KEY_SCREEN" action="raise_window"/>
</press>
<press key="KEY_LEFTSHIFT" action="pick_up">
<press key="KEY_LEFTSHIFT">
<press key="KEY_TAB" action="prev_window">
<release key="KEY_TAB">
<release key="KEY_SCREEN" action="raise_window"/>
</release>
</press>
</press>
</press>
<press key="KEY_SCREEN">
<press key="KEY_LEFTSHIFT" action="pick_up">
<press key="KEY_1" action="screen" target="screen_1"/>
<press key="KEY_2" action="screen" target="screen_2"/>
<press key="KEY_3" action="screen" target="screen_3"/>
@ -44,7 +49,6 @@
<press key="KEY_0" action="screen" target="screen_0"/>
<release key="KEY_LEFTSHIFT" action="place_down"/>
</press>
<press key="KEY_ENTER" action="toggle_fullscreen"/>
<press key="KEY_1" action="screen" target="screen_1"/>
<press key="KEY_2" action="screen" target="screen_2"/>
<press key="KEY_3" action="screen" target="screen_3"/>
@ -57,6 +61,14 @@
<press key="KEY_0" action="screen" target="screen_0"/>
</press>
<press key="KEY_SCREEN" action="release_grab">
<press key="KEY_ENTER" action="toggle_fullscreen"/>
</press>
<press key="BTN_LEFT" action="drag">
<release key="BTN_LEFT" action="drop"/>
</press>
<!-- support switching screens while dragging a window -->
<press key="BTN_LEFT">
<press key="KEY_1" action="screen" target="screen_1"/>

View File

@ -19,19 +19,24 @@
<assign label_prefix="" target="screen_1" xpos="any" ypos="any"/>
</rules>
<press key="KEY_SCREEN" action="release_grab">
<press key="KEY_SCREEN">
<press key="KEY_TAB" action="next_window">
<release key="KEY_TAB">
<release key="KEY_SCREEN" action="raise_window"/>
</release>
<release key="KEY_SCREEN" action="raise_window"/>
</press>
<press key="KEY_LEFTSHIFT" action="pick_up">
<press key="KEY_LEFTSHIFT">
<press key="KEY_TAB" action="prev_window">
<release key="KEY_TAB">
<release key="KEY_SCREEN" action="raise_window"/>
</release>
</press>
</press>
</press>
<press key="KEY_SCREEN">
<press key="KEY_LEFTSHIFT" action="pick_up">
<press key="KEY_1" action="screen" target="screen_1"/>
<press key="KEY_2" action="screen" target="screen_2"/>
<press key="KEY_3" action="screen" target="screen_3"/>
@ -44,7 +49,6 @@
<press key="KEY_0" action="screen" target="screen_0"/>
<release key="KEY_LEFTSHIFT" action="place_down"/>
</press>
<press key="KEY_ENTER" action="toggle_fullscreen"/>
<press key="KEY_1" action="screen" target="screen_1"/>
<press key="KEY_2" action="screen" target="screen_2"/>
<press key="KEY_3" action="screen" target="screen_3"/>
@ -57,6 +61,14 @@
<press key="KEY_0" action="screen" target="screen_0"/>
</press>
<press key="KEY_SCREEN" action="release_grab">
<press key="KEY_ENTER" action="toggle_fullscreen"/>
</press>
<press key="BTN_LEFT" action="drag">
<release key="BTN_LEFT" action="drop"/>
</press>
<!-- support switching screens while dragging a window -->
<press key="BTN_LEFT">
<press key="KEY_1" action="screen" target="screen_1"/>

View File

@ -22,7 +22,8 @@ namespace Window_layouter { class Command; }
struct Window_layouter::Command
{
enum Type { NONE, NEXT_WINDOW, PREV_WINDOW, RAISE_WINDOW, TOGGLE_FULLSCREEN,
NEXT_TAB, PREV_TAB, SCREEN, RELEASE_GRAB, PICK_UP, PLACE_DOWN };
NEXT_TAB, PREV_TAB, SCREEN, RELEASE_GRAB, PICK_UP, PLACE_DOWN,
DRAG, DROP };
Type type;
Target::Name target;
@ -39,6 +40,8 @@ struct Window_layouter::Command
if (string == "release_grab") return RELEASE_GRAB;
if (string == "pick_up") return PICK_UP;
if (string == "place_down") return PLACE_DOWN;
if (string == "drag") return DRAG;
if (string == "drop") return DROP;
warning("cannot convert \"", string, "\" to action type");
return NONE;

View File

@ -110,7 +110,7 @@ class Window_layouter::Key_sequence_tracker
bool done = false; /* process the first match only */
curr.for_each_sub_node(node_type, [&] (Xml_node const &node) {
if (!done && node.attribute_value("key", Key_name()) == key) {
if (node.attribute_value("key", Key_name()) == key) {
fn(node);
done = true; } });
@ -118,9 +118,10 @@ class Window_layouter::Key_sequence_tracker
no_match_fn();
}
void _with_match_rec(unsigned const pos, Xml_node const &node, auto const &fn) const
void _with_match_rec(unsigned const pos, unsigned const max_pos,
Xml_node const &node, auto const &fn) const
{
if (pos == _stack.pos) {
if (pos == max_pos) {
fn(node);
return;
}
@ -129,7 +130,7 @@ class Window_layouter::Key_sequence_tracker
_with_matching_sub_node(node, _stack.entries[pos],
[&] (Xml_node const &sub_node) {
if (pos < _stack.pos)
_with_match_rec(pos + 1, sub_node, fn); },
_with_match_rec(pos + 1, max_pos, sub_node, fn); },
[&] { });
};
@ -142,7 +143,15 @@ class Window_layouter::Key_sequence_tracker
*/
void _with_xml_by_path(Xml_node const &config, auto const &fn) const
{
_with_match_rec(0, config, fn);
_with_match_rec(0, _stack.pos, config, fn);
}
void _with_xml_at_press(Xml_node const &config, Input::Keycode key, auto const &fn) const
{
for (unsigned i = 0; i < _stack.pos; i++)
if (_stack.entries[i].press && _stack.entries[i].key == key) {
_with_match_rec(0, i + 1, config, fn);
return; }
}
/**
@ -182,39 +191,59 @@ class Window_layouter::Key_sequence_tracker
_stack.flush(Stack::Entry { .press = false, .key = key });
});
Constructible<Stack::Entry> new_entry { };
_with_xml_by_path(config, [&] (Xml_node const &curr_node) {
ev.handle_press([&] (Input::Keycode key, Codepoint) {
Stack::Entry const press { .press = true, .key = key };
_with_matching_sub_node(curr_node, press,
[&] (Xml_node const &node) { _execute_command(node, fn); },
[&] (Xml_node const &node) {
_execute_command(node, fn); },
[&] { });
_stack.push(press);
new_entry.construct(press);
});
ev.handle_release([&] (Input::Keycode key) {
Stack::Entry const release { .press = false, .key = key };
/*
* If there exists a specific path for the release event,
* follow the path. Otherwise, we remove the released key
* from the sequence.
* follow the path and record the release event. Otherwise,
* 'new_entry' will remain unconstructed so that the
* corresponding press event gets flushed from the stack.
*/
Stack::Entry const release { .press = false, .key = key };
_with_matching_sub_node(curr_node, release,
[&] (Xml_node const &next_node) {
_execute_command(next_node, fn);
_stack.push(release);
if (next_node.num_sub_nodes())
new_entry.construct(release);
},
[&] /* no match */ {
Stack::Entry const press { .press = true, .key = key };
_stack.flush(press);
});
[&] /* no match */ { });
});
});
if (new_entry.constructed()) {
_stack.push(*new_entry);
return;
}
/*
* If no matching <release> node exists for the current combination
* of keys, fall back to a <release> node declared immediately
* inside the corresponding <press> node.
*/
ev.handle_release([&] (Input::Keycode key) {
_with_xml_at_press(config, key, [&] (Xml_node const &press_node) {
_with_matching_sub_node(press_node, { .press = false, .key = key },
[&] (Xml_node const &next_node) {
_execute_command(next_node, fn); },
[&] { }); });
_stack.flush(Stack::Entry { .press = true, .key = key });
_stack.flush(Stack::Entry { .press = false, .key = key });
});
}
};

View File

@ -243,59 +243,6 @@ void Window_layouter::User_state::_handle_event(Input::Event const &e,
if (e.press()) _key_cnt++;
if (e.release()) _key_cnt--;
/* handle pointer click */
if (e.key_press(Input::BTN_LEFT) && _key_cnt == 1) {
/*
* Initiate drag operation if possible
*/
_drag_state = true;
_pointer_clicked = _pointer_curr;
if (_hovered_window_id.valid()) {
/*
* Initiate drag operation
*
* If the hovered window is known at the time of the press event,
* we can initiate the drag operation immediately. Otherwise,
* the initiation is deferred to the next update of the hover
* model.
*/
_initiate_drag(_hovered_window_id, _hovered_element);
} else {
/*
* If the hovering state is undefined at the time of the click,
* we defer the drag handling until the next update of the hover
* state. This intermediate state is captured by '_drag_init_done'.
*/
_drag_init_done = false;
_dragged_window_id = Window_id();
_dragged_element = Window::Element(Window::Element::UNDEFINED);
}
}
/* detect end of drag operation */
if (e.release() && _key_cnt == 0) {
if (_drag_state && _dragged_window_id.valid()) {
_drag_state = false;
/*
* Issue resize to 0x0 when releasing the the window closer
*/
if (_dragged_element.closer())
if (_dragged_element == _hovered_element)
_action.close(_dragged_window_id);
_action.finalize_drag(_dragged_window_id, _dragged_element,
_pointer_clicked, _pointer_curr);
}
}
/* handle key sequences */
if (_key(e)) {
@ -348,6 +295,54 @@ void Window_layouter::User_state::_handle_event(Input::Event const &e,
}
return;
case Command::DRAG:
_drag_state = true;
_pointer_clicked = _pointer_curr;
if (_hovered_window_id.valid()) {
/*
* Initiate drag operation
*
* If the hovered window is known at the time of the press event,
* we can initiate the drag operation immediately. Otherwise,
* the initiation is deferred to the next update of the hover
* model.
*/
_initiate_drag(_hovered_window_id, _hovered_element);
} else {
/*
* If the hovering state is undefined at the time of the click,
* we defer the drag handling until the next update of the hover
* state. This intermediate state is captured by '_drag_init_done'.
*/
_drag_init_done = false;
_dragged_window_id = Window_id();
_dragged_element = Window::Element(Window::Element::UNDEFINED);
}
return;
case Command::DROP:
if (_drag_state && _dragged_window_id.valid()) {
_drag_state = false;
/*
* Issue resize to 0x0 when releasing the the window closer
*/
if (_dragged_element.closer())
if (_dragged_element == _hovered_element)
_action.close(_dragged_window_id);
_action.finalize_drag(_dragged_window_id, _dragged_element,
_pointer_clicked, _pointer_curr);
}
return;
default:
warning("command ", (int)command.type, " unhanded");
}

View File

@ -31,6 +31,9 @@ proc qt5_layouter_config { } {
<screen name="screen"/>
<assign label_prefix="" target="screen" xpos="any" ypos="any"/>
</rules>
<press key="BTN_LEFT" action="drag">
<release key="BTN_LEFT" action="drop"/>
</press>
</config>}
}

View File

@ -31,6 +31,9 @@ proc qt6_layouter_config { } {
<screen name="screen"/>
<assign label_prefix="" target="screen" xpos="any" ypos="any"/>
</rules>
<press key="BTN_LEFT" action="drag">
<release key="BTN_LEFT" action="drop"/>
</press>
</config>}
}