├── LICENSE ├── NEWS ├── README.md ├── example ├── actions-wstroke-2 └── meson.build ├── icons ├── 128x128 │ └── wstroke.png ├── 160x160 │ └── wstroke.png ├── 192x192 │ └── wstroke.png ├── 48x48 │ └── wstroke.png ├── 64x64 │ └── wstroke.png ├── 72x72 │ └── wstroke.png ├── 96x96 │ └── wstroke.png ├── convert-icons.fsh ├── meson.build ├── plugin-wstroke.svg └── wstroke.svg ├── input-inhibitor ├── input_inhibitor.c ├── input_inhibitor.h ├── meson.build └── wlr-input-inhibitor-unstable-v1.xml ├── meson.build ├── src ├── actiondb.cc ├── actiondb.h ├── actiondb_config.cc ├── actiondb_plugin.cc ├── actions.cc ├── actions.h ├── appchooser.cc ├── appchooser.h ├── cellrenderertextish.vala ├── config.h.in ├── convert_keycodes.cc ├── convert_keycodes.h ├── easystroke_gestures.cpp ├── gesture.cc ├── gesture.h ├── gui.glade ├── input_events.cpp ├── input_events.hpp ├── input_inhibitor.vapi ├── main.cc ├── meson.build ├── resources.xml ├── stroke.c ├── stroke.h ├── stroke_draw.cc ├── stroke_draw.h ├── stroke_drawing_area.cpp └── stroke_drawing_area.h ├── toplevel-grabber ├── meson.build ├── toplevel-grabber-test.c ├── toplevel-grabber.c ├── toplevel-grabber.h └── wlr-foreign-toplevel-management-unstable-v1.xml ├── wstroke-config.1 ├── wstroke-config.desktop └── wstroke.xml /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2009, Thomas Jaeger 2 | Copyright (c) 2020-2023, Daniel Kondor 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 10 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 12 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 13 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 14 | PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | 2.3.0 2 | ========= 3 | * Adapt to JSON API changes in Wayfire 4 | * Depend on Wayfire version >= 0.10.0 (in development) and wlroots version = 0.18 5 | 6 | 2.2.1 7 | ========= 8 | * Fix a build issue 9 | * Build now requires meson version >= 1.0.0 10 | 11 | 2.2.0 12 | ========= 13 | * Support wlroots 0.18 (if used by Wayfire) 14 | * Add a "rotate cube" action 15 | 16 | 2.1.3 17 | ========= 18 | * Add man page 19 | * Make rebuilding Vala code the default 20 | 21 | 2.1.2 22 | ========= 23 | * Bug fix: avoid crashing if a gesture is started on a popup 24 | 25 | 2.1.1 26 | ========= 27 | * WM Action / Move: avoid a spurious move of the view when the action is initiated 28 | 29 | 2.1 30 | ========= 31 | * Command action: allow to select an app to run graphically 32 | 33 | 2.0.2 34 | ========= 35 | * Avoid a potential edge case where refocusing the original view conflicts with the Command action. 36 | * Refactor the use of GtkApplication. 37 | 38 | 2.0.1 39 | ========= 40 | * Allow recording strokes with the same button that is used by the plugin 41 | 42 | 2.0 43 | ========= 44 | * Refactor internal storage and clean code 45 | * Gestures can be freely reordered 46 | * Fix issues related to the display of apps and groups 47 | * Slight updates to the GUI 48 | * Add "Touchpad Gesture" action (including the "Scroll" action from Easystroke) 49 | * Add the possibility to import / export the gesture configuration 50 | 51 | 1.0 52 | ========= 53 | * Original release, most features work 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wstroke 2 | 3 | Port of [Easystroke mouse gestures](https://github.com/thjaeger/easystroke) as a plugin for [Wayfire](https://github.com/WayfireWM/wayfire). Mouse gestures are shapes drawn on the screen while holding down one of the buttons (typically the right or middle button). This plugin allows associating such gestures with various actions. See the [Wiki](https://github.com/dkondor/wstroke/wiki) for more explanations and examples. 4 | 5 | Packages are available for: 6 | - Ubuntu 24.04: https://launchpad.net/~kondor-dani/+archive/ubuntu/ppa-wstroke 7 | - Debian (testing and unstable): in the official [repository](https://packages.debian.org/testing/wstroke). 8 | 9 | ### Dependencies 10 | 11 | - [Wayfire](https://github.com/WayfireWM/wayfire), the current development version, i.e. 0.10.0, after commit [fe33cfa](https://github.com/WayfireWM/wayfire/commit/fe33cfa5b03f11cb9749694929cc33583464bda0) (see below for compiling on older Wayfire versions) 12 | - [wlroots](https://gitlab.freedesktop.org/wlroots/wlroots) version [0.18](https://gitlab.freedesktop.org/wlroots/wlroots/-/tree/0.18?ref_type=heads). 13 | - Development libraries for GTK, GDK, glib, gtkmm, gdkmm and boost-serialization (Ubuntu packages: `libglib2.0-dev, libgtk-3-dev, libgtkmm-3.0-dev, libboost-serialization-dev`) 14 | - `glib-compile-resources` (Ubuntu package: `libglib2.0-dev-bin`) 15 | - [Vala](https://vala.dev/) compiler (for building, Ubuntu package: `valac`; or use the [no_vala](https://github.com/dkondor/wstroke/tree/no_vala) branch instead) 16 | - Optional, but highly recommended: [WCM](https://github.com/WayfireWM/wcm) for basic configuration 17 | - Optionally [libinput](https://www.freedesktop.org/wiki/Software/libinput/) version [1.17](https://lists.freedesktop.org/archives/wayland-devel/2021-February/041733.html) or higher for improved touchpad support (to allow tap-and-drag for the right and middle buttons, required for drawing gestures without physical buttons) 18 | 19 | ### Building and installing 20 | 21 | ``` 22 | meson build 23 | ninja -C build 24 | sudo ninja -C build install 25 | ``` 26 | 27 | If you get build errors, your Wayfire version might be too old (or too new). For older Wayfire versions, try the following: 28 | - For version 0.7.0, use the [wayfire-0.7 branch](https://github.com/dkondor/wstroke/tree/wayfire-0.7) (run `git checkout wayfire-0.7` before building). 29 | - For older Wayfire versions of the 0.8.0 series (between commits [3cca6c9](https://github.com/WayfireWM/wayfire/commit/3cca6c9fee35ea8671da2b1c3f56ca61045ea693) and [d1f33e5](https://github.com/WayfireWM/wayfire/commit/d1f33e58326175f6437d0345ac78b0bb9f03b889)), use [this state](https://github.com/dkondor/wstroke/tree/4f2e8f00e4c734ac6fc3698bc4cfc504fe47a311) (run `git checkout 4f2e8f0` before building). If using multiple monitors, you can separately apply the fix to [issue #5](https://github.com/dkondor/wstroke/issues/5): `git cherry-pick 1c02905a4e` 30 | - For moderately old versions of Wayfire (between commits [d1f33e5](https://github.com/WayfireWM/wayfire/commit/d1f33e58326175f6437d0345ac78b0bb9f03b889) and 31 | [3ac0284](https://github.com/WayfireWM/wayfire/commit/3ac028406cc3697dd40c128721fb6e681b00c337)), use [this state](https://github.com/dkondor/wstroke/tree/0401b4f608c7d265a10fa2e7f4ce2dafb9caca4b) (run `git checkout 0401b4f` before building). If using multiple monitors, you can separately apply the fix to [issue #5](https://github.com/dkondor/wstroke/issues/5): `git cherry-pick 1c02905a4e` 32 | - For version [0.8.0](https://github.com/WayfireWM/wayfire/tree/v0.8.0), [0.9.0](https://github.com/WayfireWM/wayfire/tree/v0.9.0), or the development version up to commit [448ce8b](https://github.com/WayfireWM/wayfire/commit/448ce8b5be341f91b9f3b10ee9d4f3ea8cd57819), use releases of the 2.2 branch, the latest available one is [2.2.1](https://github.com/dkondor/wstroke/tree/v2.2.1) (run `git checkout v2.2.1` before building). This version supports building against wlroots versions 0.16-0.18. However, the version of wlroots should be the same that was used for building Wayfire (this should be detected during compilation). 33 | - For recent versions of Wayfire (0.10.0 or newer, after commit [fe33cfa](https://github.com/WayfireWM/wayfire/commit/fe33cfa5b03f11cb9749694929cc33583464bda0)), use this branch (and report issues for build failures). 34 | 35 | 36 | ### Running 37 | 38 | If correctly installed, it will show up as "Mouse Gestures" plugin in WCM and can be enabled from there, or with adding `wstroke` to the list of plugins (in `[core]`) in `~/.config/wayfire.ini`. 39 | 40 | ### Configuration 41 | 42 | Basic options such as the button used for gestures, or the target of gestures can be changed with WCM as the "Mouse Gestures" plugin, or by manually editing the `[wstroke]` section in `~/.config/wayfire.ini`. Note: trying to set a button will likely make WCM to show a warning (e.g. "Attempting to bind `BTN_RIGHT` without modifier"); it is safe to ignore this, since wstroke will forward button clicks when needed. 43 | 44 | Gestures can be configured by the standalone program `wstroke-config`. Recommended ways to obtain an initial configuration: 45 | - If you have Easystroke installed, `wstroke-config` will attempt to import gestures from it, by looking for any of the `actions*` files under `~/.easystroke`. Even without Easystroke installed, copying the content of this directory from a previous installation can be used to import gestures. It is recommended to check that importing is done correctly. 46 | - An example configuration file is under [example/actions-wstroke-2](example/actions-wstroke-2). This is installed automatically and will be used by `wstroke-config` as default if no other configuration exists. You can copy this file to `~/.config/wstroke` manually as well. 47 | 48 | Gestures are stored under `wstroke/actions-wstroke-2` in the directory given by the `XDG_CONFIG_HOME` environment variable (`~/.config` by default). It is recommended not to edit this file manually, but it can be copied between different computers, or backed up and restored manually. 49 | 50 | #### Focus settings #### 51 | For a better experience, it is recommended to disable the "click-to-focus" feature in Wayfire for the mouse button used for gestures. This will allow wstroke to manage focus when using this button and set the target of the gesture as requested by the user. 52 | 53 | To do this, under the "Core" tab of WCM, change the option "Mouse button to focus views" to *not* include the button used for gestures. E.g. if the right button is used, the setting here should not contain `BTN_RIGHT`, so it might look like `BTN_LEFT | BTN_MIDDLE` (note: it is best to set this by clicking on the edit button on the right of the setting and manually editing the text that corresponds to this setting). Don't be alarmed by the warning that appears ("Attempting to bind `BTN_LEFT | BTN_MIDDLE` without modifier"); this is exactly the intended behavior in this case. 54 | 55 | The same can be achieved by editing the option `focus_buttons` in the `[core]` section of `~/.config/wayfire.ini`. 56 | 57 | ### What works 58 | 59 | - Importing saved strokes from "actions" files created with Easystroke (just run `wstroke-config`). 60 | - Drawing and recognizing strokes. 61 | - Actions on the active view: close, minimize, (un)maximize, move, resize (select "WM Action" and the appropriate action). 62 | - Actions to activate another Wayfire plugin (typical desktop interactions are under "Global Action"; "Custom Plugin" can be used with giving the plugin activator name directly), only supported for some plugins, see [here](https://github.com/WayfireWM/wayfire/issues/1811). 63 | - Generating keypresses ("Key" action). 64 | - Generating mouse clicks ("Button" action). 65 | - Generating modifiers ("Ignore" action -- only works in combination with mouse clicks, not the keyboard). 66 | - Emulating touchpad "gestures" with mouse movement, such as scrolling or pinch zoom in apps that support it ("Touchpad Gesture" action; "Scroll" action from Easystroke will be converted to this). 67 | - Running commands as a gesture action. 68 | - Getting keybindings and mouse button bindings in the configuration for actions. 69 | - Recording strokes (slight change: these have to be recorded on a "canvas", cannot be drawn anywhere like with Easystroke; also, recording strokes requires using a different mouse button). 70 | - Identifying views and using application specific gestures or excluding certain apps completely; setting these in `wstroke-config` by interactively grabbing the app-id of an open view. 71 | - Option to target either the view under the mouse when starting the gesture (original Easystroke behavior) or the currently active one. 72 | - Option to change focus to the view under the mouse after a gesture. 73 | - Basic timeouts (move the mouse after clicking to have a gesture / end the gesture if not moving within a timeout). 74 | 75 | ### What does not work 76 | 77 | - SendText action (removed from settings, will be converted to Global) 78 | - Ignore action in combination with keyboard keypresses. 79 | - Individual settings (which button, timeout) for each pointing device 80 | - Advanced gestures 81 | - Touchscreen and pen / stylus support 82 | 83 | -------------------------------------------------------------------------------- /example/meson.build: -------------------------------------------------------------------------------- 1 | install_data('actions-wstroke-2', install_dir: conf_data.get('DATA_DIR')) 2 | -------------------------------------------------------------------------------- /icons/128x128/wstroke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkondor/wstroke/e7cf0eeb967ba92ab798944f364232eb9524358f/icons/128x128/wstroke.png -------------------------------------------------------------------------------- /icons/160x160/wstroke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkondor/wstroke/e7cf0eeb967ba92ab798944f364232eb9524358f/icons/160x160/wstroke.png -------------------------------------------------------------------------------- /icons/192x192/wstroke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkondor/wstroke/e7cf0eeb967ba92ab798944f364232eb9524358f/icons/192x192/wstroke.png -------------------------------------------------------------------------------- /icons/48x48/wstroke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkondor/wstroke/e7cf0eeb967ba92ab798944f364232eb9524358f/icons/48x48/wstroke.png -------------------------------------------------------------------------------- /icons/64x64/wstroke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkondor/wstroke/e7cf0eeb967ba92ab798944f364232eb9524358f/icons/64x64/wstroke.png -------------------------------------------------------------------------------- /icons/72x72/wstroke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkondor/wstroke/e7cf0eeb967ba92ab798944f364232eb9524358f/icons/72x72/wstroke.png -------------------------------------------------------------------------------- /icons/96x96/wstroke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkondor/wstroke/e7cf0eeb967ba92ab798944f364232eb9524358f/icons/96x96/wstroke.png -------------------------------------------------------------------------------- /icons/convert-icons.fsh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/fish 2 | for a in 48 64 72 96 128 160 192 3 | set s "$a"x"$a" 4 | set d (math $a \* 2.25) 5 | convert -background none -density $d wstroke.svg $s/wstroke.png 6 | end 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /icons/meson.build: -------------------------------------------------------------------------------- 1 | install_data('plugin-wstroke.svg', install_dir: wayfire.get_variable(pkgconfig: 'icondir')) 2 | install_data('wstroke.svg', install_dir: join_paths(icon_dir, 'hicolor', 'scalable', 'apps')) 3 | install_data(join_paths('48x48', 'wstroke.png'), install_dir: join_paths(icon_dir, 'hicolor', '48x48', 'apps')) 4 | install_data(join_paths('64x64', 'wstroke.png'), install_dir: join_paths(icon_dir, 'hicolor', '64x64', 'apps')) 5 | install_data(join_paths('72x72', 'wstroke.png'), install_dir: join_paths(icon_dir, 'hicolor', '72x72', 'apps')) 6 | install_data(join_paths('96x96', 'wstroke.png'), install_dir: join_paths(icon_dir, 'hicolor', '96x96', 'apps')) 7 | install_data(join_paths('128x128', 'wstroke.png'), install_dir: join_paths(icon_dir, 'hicolor', '128x128', 'apps')) 8 | install_data(join_paths('160x160', 'wstroke.png'), install_dir: join_paths(icon_dir, 'hicolor', '160x160', 'apps')) 9 | install_data(join_paths('192x192', 'wstroke.png'), install_dir: join_paths(icon_dir, 'hicolor', '192x192', 'apps')) 10 | -------------------------------------------------------------------------------- /icons/plugin-wstroke.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 51 | 54 | 60 | 66 | 72 | 78 | 84 | 90 | 96 | 102 | 108 | 114 | 120 | 126 | 132 | 138 | 144 | 150 | 156 | 162 | 168 | 174 | 180 | 186 | 192 | 198 | 204 | 210 | 216 | 222 | 228 | 234 | 240 | 246 | 252 | 258 | 264 | 270 | 276 | 282 | 288 | 294 | 295 | 296 | -------------------------------------------------------------------------------- /icons/wstroke.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /input-inhibitor/input_inhibitor.c: -------------------------------------------------------------------------------- 1 | /* 2 | * input_inhibitor.c -- inhibitor for grabbing key combinations 3 | * 4 | * Copyright 2020 Daniel Kondor 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 13 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 15 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 16 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | * 18 | */ 19 | 20 | 21 | #include 22 | #include 23 | #include 24 | 25 | static struct zwlr_input_inhibitor_v1* grab = NULL; 26 | static struct zwlr_input_inhibit_manager_v1* inhibitor = NULL; 27 | 28 | 29 | static void _add(G_GNUC_UNUSED void *data, struct wl_registry *registry, 30 | uint32_t name, const char *interface, G_GNUC_UNUSED uint32_t version) { 31 | if(strcmp(interface, zwlr_input_inhibit_manager_v1_interface.name) == 0) 32 | inhibitor = (struct zwlr_input_inhibit_manager_v1*)wl_registry_bind(registry, name, &zwlr_input_inhibit_manager_v1_interface, 1u); 33 | } 34 | 35 | static void _remove(G_GNUC_UNUSED void *data, G_GNUC_UNUSED struct wl_registry *registry, G_GNUC_UNUSED uint32_t name) { } 36 | 37 | static struct wl_registry_listener listener = { &_add, &_remove }; 38 | 39 | 40 | gboolean input_inhibitor_init() { 41 | struct wl_display* display = gdk_wayland_display_get_wl_display(gdk_display_get_default()); 42 | if(!display) return FALSE; 43 | 44 | struct wl_registry* registry = wl_display_get_registry(display); 45 | if(!registry) return FALSE; 46 | 47 | wl_registry_add_listener(registry, &listener, NULL); 48 | wl_display_dispatch(display); 49 | wl_display_roundtrip(display); 50 | if (!inhibitor) return FALSE; 51 | return TRUE; 52 | } 53 | 54 | gboolean input_inhibitor_grab() { 55 | if(!inhibitor) return FALSE; 56 | if(grab) return TRUE; 57 | grab = zwlr_input_inhibit_manager_v1_get_inhibitor(inhibitor); 58 | if(!grab) return FALSE; 59 | return TRUE; 60 | } 61 | 62 | void input_inhibitor_ungrab() { 63 | if(grab) { 64 | zwlr_input_inhibitor_v1_destroy(grab); 65 | wl_display_flush(gdk_wayland_display_get_wl_display(gdk_display_get_default())); 66 | grab = NULL; 67 | } 68 | } 69 | 70 | 71 | -------------------------------------------------------------------------------- /input-inhibitor/input_inhibitor.h: -------------------------------------------------------------------------------- 1 | /* 2 | * input_inhibitor.h -- inhibitor for grabbing key combinations 3 | * 4 | * Copyright 2020 Daniel Kondor 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 13 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 15 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 16 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | * 18 | */ 19 | 20 | 21 | #ifndef INPUT_INHIBITOR_H 22 | #define INPUT_INHIBITOR_H 23 | 24 | #include 25 | 26 | #ifdef __cplusplus 27 | extern "C" { 28 | #endif 29 | 30 | /* Try to initialize inhibitor (keyboard grabber) interface. */ 31 | gboolean input_inhibitor_init(); 32 | 33 | /* Try to grab keyboard. */ 34 | gboolean input_inhibitor_grab(); 35 | 36 | /* Release an existing grab. */ 37 | void input_inhibitor_ungrab(); 38 | 39 | #ifdef __cplusplus 40 | } 41 | #endif 42 | 43 | #endif 44 | 45 | -------------------------------------------------------------------------------- /input-inhibitor/meson.build: -------------------------------------------------------------------------------- 1 | client_protocols = [ 2 | './wlr-input-inhibitor-unstable-v1.xml' 3 | ] 4 | 5 | wl_protos_client_src = [] 6 | wl_protos_headers = [] 7 | 8 | foreach p : client_protocols 9 | xml = join_paths(p) 10 | wl_protos_headers += wayland_scanner_client.process(xml) 11 | wl_protos_client_src += wayland_scanner_code.process(xml) 12 | endforeach 13 | 14 | lib_inhibitor_protos = static_library('wl_inhibitor_protos', wl_protos_client_src + wl_protos_headers, 15 | dependencies: [wayland_client]) # for the include directory 16 | 17 | protos = declare_dependency( 18 | link_with: lib_inhibitor_protos, 19 | sources: wl_protos_headers, 20 | ) 21 | 22 | input_inhibitor = static_library('input_inhibitor', 'input_inhibitor.c', 23 | dependencies: [wayland_client, protos, gdk]) 24 | 25 | input_inhibitor_dep = declare_dependency( 26 | link_with: input_inhibitor, 27 | include_directories: include_directories('.') 28 | ) 29 | -------------------------------------------------------------------------------- /input-inhibitor/wlr-input-inhibitor-unstable-v1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright © 2018 Drew DeVault 5 | 6 | Permission to use, copy, modify, distribute, and sell this 7 | software and its documentation for any purpose is hereby granted 8 | without fee, provided that the above copyright notice appear in 9 | all copies and that both that copyright notice and this permission 10 | notice appear in supporting documentation, and that the name of 11 | the copyright holders not be used in advertising or publicity 12 | pertaining to distribution of the software without specific, 13 | written prior permission. The copyright holders make no 14 | representations about the suitability of this software for any 15 | purpose. It is provided "as is" without express or implied 16 | warranty. 17 | 18 | THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS 19 | SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 20 | FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 22 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 23 | AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 24 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 25 | THIS SOFTWARE. 26 | 27 | 28 | 29 | 30 | Clients can use this interface to prevent input events from being sent to 31 | any surfaces but its own, which is useful for example in lock screen 32 | software. It is assumed that access to this interface will be locked down 33 | to whitelisted clients by the compositor. 34 | 35 | 36 | 37 | 38 | Activates the input inhibitor. As long as the inhibitor is active, the 39 | compositor will not send input events to other clients. 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | While this resource exists, input to clients other than the owner of the 52 | inhibitor resource will not receive input events. The client that owns 53 | this resource will receive all input events normally. The compositor will 54 | also disable all of its own input processing (such as keyboard shortcuts) 55 | while the inhibitor is active. 56 | 57 | The compositor may continue to send input events to selected clients, 58 | such as an on-screen keyboard (via the input-method protocol). 59 | 60 | 61 | 62 | 63 | Destroy the inhibitor and allow other clients to receive input. 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'wstroke', 3 | 'c', 4 | 'cpp', 5 | 'vala', 6 | version: '2.3.0', 7 | license: 'MIT', 8 | meson_version: '>=1.0.0', 9 | default_options: [ 10 | 'cpp_std=c++17', 11 | 'c_std=c11', 12 | 'warning_level=2', 13 | 'werror=false', 14 | ], 15 | ) 16 | 17 | # paths (only needed to install icon and desktop file) 18 | prefix = get_option('prefix') 19 | datadir = join_paths(prefix, get_option('datadir')) 20 | icon_dir = join_paths(datadir, 'icons') 21 | desktop_dir = join_paths(datadir, 'applications') 22 | 23 | # dependencies for loadable plugin 24 | boost = dependency('boost', modules: ['serialization'], static: false) 25 | wayfire = dependency('wayfire', version: '>=0.10.0') 26 | wlroots = dependency('wlroots-0.18') 27 | wlroots_headers = wlroots.partial_dependency(includes: true, compile_args: true) 28 | wlserver = dependency('wayland-server') 29 | glibmm = dependency('glibmm-2.4') 30 | 31 | # additional dependencies for GUI 32 | gtkmm = dependency('gtkmm-3.0') 33 | gdkmm = dependency('gdkmm-3.0') 34 | glib = dependency('glib-2.0') 35 | gobject = dependency('gobject-2.0') 36 | gtk = dependency('gtk+-3.0') 37 | gdk = dependency('gdk-3.0') 38 | 39 | gnome = import('gnome') 40 | 41 | # filesystem library support 42 | # note: on Ubuntu 18.04 this only works with clang++ 43 | cpp = meson.get_compiler('cpp') 44 | if cpp.has_link_argument('-lc++fs') 45 | add_project_link_arguments(['-lc++fs'], language: 'cpp') 46 | elif cpp.has_link_argument('-lc++experimental') 47 | add_project_link_arguments(['-lc++experimental'], language: 'cpp') 48 | elif cpp.has_link_argument('-lstdc++fs') 49 | add_project_link_arguments(['-lstdc++fs'], language: 'cpp') 50 | endif 51 | 52 | 53 | # wayland-scanner -- needed for keyboard grabber and input inhibitor 54 | wayland_client = dependency('wayland-client') 55 | wayland_scanner = find_program('wayland-scanner') 56 | 57 | wayland_scanner_code = generator( 58 | wayland_scanner, 59 | output: '@BASENAME@-protocol.c', 60 | arguments: ['private-code', '@INPUT@', '@OUTPUT@'], 61 | ) 62 | 63 | wayland_scanner_client = generator( 64 | wayland_scanner, 65 | output: '@BASENAME@-client-protocol.h', 66 | arguments: ['client-header', '@INPUT@', '@OUTPUT@'], 67 | ) 68 | 69 | 70 | add_project_arguments(['--vapidir', meson.current_source_dir() + '/src'], language: 'vala') 71 | add_project_arguments(['--pkg', 'input_inhibitor'], language: 'vala') 72 | 73 | subdir('input-inhibitor') 74 | subdir('toplevel-grabber') 75 | subdir('src') 76 | subdir('example') 77 | 78 | 79 | install_data('wstroke.xml', install_dir: wayfire.get_variable(pkgconfig: 'metadatadir')) 80 | install_data('wstroke-config.desktop', install_dir: desktop_dir) 81 | install_man('wstroke-config.1') 82 | subdir('icons') 83 | -------------------------------------------------------------------------------- /src/actiondb.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2009, Thomas Jaeger 3 | * Copyright (c) 2020-2023, Daniel Kondor 4 | * 5 | * Permission to use, copy, modify, and/or distribute this software for any 6 | * purpose with or without fee is hereby granted, provided that the above 7 | * copyright notice and this permission notice appear in all copies. 8 | * 9 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 12 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 14 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 15 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | */ 17 | #include "actiondb.h" 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | #ifdef ACTIONDB_CONVERT_CODES 34 | #include "convert_keycodes.h" 35 | 36 | static inline uint32_t convert_modifier(uint32_t mod) { 37 | return KeyCodes::convert_modifier(mod); 38 | } 39 | 40 | static inline uint32_t convert_keysym(uint32_t key) { 41 | return KeyCodes::convert_keysym(key); 42 | } 43 | 44 | #else 45 | 46 | static inline uint32_t convert_modifier(G_GNUC_UNUSED uint32_t mod) { 47 | throw std::runtime_error("unsupported action DB version!\nrun the wstroke-config program first to convert it to the new format\n"); 48 | } 49 | 50 | static inline uint32_t convert_keysym(G_GNUC_UNUSED uint32_t key) { 51 | throw std::runtime_error("unsupported action DB version!\nrun the wstroke-config program first to convert it to the new format\n"); 52 | } 53 | 54 | #endif 55 | 56 | 57 | BOOST_CLASS_EXPORT(Action) 58 | BOOST_CLASS_EXPORT(Command) 59 | BOOST_CLASS_EXPORT(ModAction) 60 | BOOST_CLASS_EXPORT(SendKey) 61 | BOOST_CLASS_EXPORT(SendText) 62 | BOOST_CLASS_EXPORT(Scroll) 63 | BOOST_CLASS_EXPORT(Ignore) 64 | BOOST_CLASS_EXPORT(Button) 65 | BOOST_CLASS_EXPORT(Misc) 66 | BOOST_CLASS_EXPORT(Global) 67 | BOOST_CLASS_EXPORT(View) 68 | BOOST_CLASS_EXPORT(Plugin) 69 | BOOST_CLASS_EXPORT(Touchpad) 70 | 71 | 72 | template void Action::serialize(G_GNUC_UNUSED Archive & ar, G_GNUC_UNUSED unsigned int version) {} 73 | 74 | template void Command::serialize(Archive & ar, unsigned int version) { 75 | ar & boost::serialization::base_object(*this); 76 | ar & cmd; 77 | if(version > 0) ar & desktop_file; 78 | } 79 | 80 | template void Plugin::serialize(Archive & ar, G_GNUC_UNUSED unsigned int version) { 81 | ar & boost::serialization::base_object(*this); 82 | ar & cmd; 83 | } 84 | 85 | template void ModAction::load(Archive & ar, G_GNUC_UNUSED unsigned int version) { 86 | ar & boost::serialization::base_object(*this); 87 | ar & mods; 88 | if (version < 1) mods = convert_modifier(mods); 89 | } 90 | 91 | template void ModAction::save(Archive & ar, G_GNUC_UNUSED unsigned int version) const { 92 | ar & boost::serialization::base_object(*this); 93 | ar & mods; 94 | } 95 | 96 | template void SendKey::load(Archive & ar, const unsigned int version) { 97 | ar & boost::serialization::base_object(*this); 98 | ar & key; 99 | if (version < 2) { 100 | uint32_t code; 101 | ar & code; 102 | if (version < 1) { 103 | bool xtest; 104 | ar & xtest; 105 | } 106 | key = convert_keysym(key); 107 | } 108 | } 109 | 110 | template void SendKey::save(Archive & ar, G_GNUC_UNUSED unsigned int version) const { 111 | ar & boost::serialization::base_object(*this); 112 | ar & key; 113 | } 114 | 115 | template void SendText::serialize(Archive & ar, G_GNUC_UNUSED unsigned int version) { 116 | ar & boost::serialization::base_object(*this); 117 | ar & text; 118 | } 119 | 120 | template void Scroll::serialize(Archive & ar, G_GNUC_UNUSED unsigned int version) { 121 | ar & boost::serialization::base_object(*this); 122 | } 123 | 124 | template void Ignore::serialize(Archive & ar, G_GNUC_UNUSED unsigned int version) { 125 | ar & boost::serialization::base_object(*this); 126 | } 127 | 128 | template void Button::serialize(Archive & ar, G_GNUC_UNUSED unsigned int version) { 129 | ar & boost::serialization::base_object(*this); 130 | ar & button; 131 | } 132 | 133 | template void Misc::serialize(Archive & ar, G_GNUC_UNUSED unsigned int version) { 134 | ar & boost::serialization::base_object(*this); 135 | ar & type; 136 | } 137 | 138 | std::unique_ptr Misc::convert() const { 139 | switch(type) { 140 | case SHOWHIDE: 141 | return Global::create(Global::Type::SHOW_CONFIG); 142 | case NONE: 143 | case DISABLE: 144 | case UNMINIMIZE: 145 | default: 146 | return Global::create(Global::Type::NONE); 147 | } 148 | } 149 | 150 | template void Global::load(Archive & ar, G_GNUC_UNUSED unsigned int version) { 151 | ar & boost::serialization::base_object(*this); 152 | ar & type; 153 | /* allow later extensions to add more types that might not be supported in older versions */ 154 | if((uint32_t)type >= n_actions) type = Type::NONE; 155 | } 156 | 157 | template void Global::save(Archive & ar, G_GNUC_UNUSED unsigned int version) const { 158 | ar & boost::serialization::base_object(*this); 159 | ar & type; 160 | } 161 | 162 | template void View::load(Archive & ar, G_GNUC_UNUSED unsigned int version) { 163 | ar & boost::serialization::base_object(*this); 164 | ar & type; 165 | /* allow later extensions to add more types that might not be supported in older versions */ 166 | if((uint32_t)type >= n_actions) type = Type::NONE; 167 | } 168 | 169 | template void View::save(Archive & ar, G_GNUC_UNUSED unsigned int version) const { 170 | ar & boost::serialization::base_object(*this); 171 | ar & type; 172 | } 173 | 174 | template void Touchpad::load(Archive & ar, G_GNUC_UNUSED unsigned int version) { 175 | ar & boost::serialization::base_object(*this); 176 | ar & type; 177 | /* allow later extensions to add more types that might not be supported in older versions */ 178 | if((uint32_t)type >= n_actions) type = Type::NONE; 179 | ar & fingers; 180 | } 181 | 182 | template void Touchpad::save(Archive & ar, G_GNUC_UNUSED unsigned int version) const { 183 | ar & boost::serialization::base_object(*this); 184 | ar & type; 185 | ar & fingers; 186 | } 187 | 188 | 189 | class StrokeSet : public std::set> { 190 | friend class boost::serialization::access; 191 | template void serialize(Archive & ar, const unsigned int version); 192 | }; 193 | BOOST_CLASS_EXPORT(StrokeSet) 194 | 195 | template void StrokeSet::serialize(Archive & ar, G_GNUC_UNUSED unsigned int version) { 196 | ar & boost::serialization::base_object > >(*this); 197 | } 198 | 199 | template void StrokeInfo::load(Archive & ar, const unsigned int version) { 200 | if (version >= 4) { 201 | ar & stroke; 202 | ar & action; 203 | } 204 | else { 205 | StrokeSet strokes; 206 | ar & strokes; 207 | 208 | if(strokes.size() && *strokes.begin()) stroke = std::move(**strokes.begin()); 209 | 210 | boost::shared_ptr action2; 211 | ar & action2; 212 | if(version < 2) { 213 | /* convert Misc actions to new types */ 214 | Misc* misc = dynamic_cast(action2.get()); 215 | if(misc) action = misc->convert(); 216 | } 217 | if(!action && version < 3) { 218 | /* convert Scroll and Text actions to Global / None -- they are not supported */ 219 | Scroll* scroll = dynamic_cast(action2.get()); 220 | if(scroll) action = Touchpad::create(Touchpad::Type::SCROLL, 2, scroll->get_mods()); 221 | else { 222 | SendText* text = dynamic_cast(action2.get()); 223 | if(text) action = Global::create(Global::Type::NONE); 224 | } 225 | } 226 | if(!action) action = action2->clone(); 227 | } 228 | if (version == 0) return; 229 | ar & name; 230 | } 231 | 232 | class Unique { 233 | friend class boost::serialization::access; 234 | template void serialize(Archive & ar, const unsigned int version); 235 | public: 236 | int level; /* not used */ 237 | int i; /* (not saved in the archive) */ 238 | }; 239 | 240 | template void Unique::serialize(G_GNUC_UNUSED Archive & ar, G_GNUC_UNUSED unsigned int version) {} 241 | 242 | 243 | template<> 244 | ActionListDiff* ActionListDiff::add_child(std::string name, bool app) { 245 | children.emplace_back(); 246 | ActionListDiff *child = &(children.back()); 247 | child->name = name; 248 | child->app = app; 249 | child->parent = this; 250 | child->level = level + 1; 251 | return child; 252 | } 253 | 254 | void ActionDB::convert_actionlist(ActionListDiff& dst, ActionListDiff& src, 255 | std::unordered_map& mapping, std::unordered_set& extra_unique) { 256 | for(Unique* x : src.order) { 257 | if(mapping.count(x)) throw std::runtime_error("Unique added multiple times!\n"); 258 | stroke_id z = get_next_id(); 259 | stroke_order.push_back(z); 260 | stroke_map[z] = std::pair(z, &dst); 261 | dst.order.push_back(z); 262 | mapping[x] = z; 263 | } 264 | 265 | for(Unique* x : src.deleted) { 266 | auto it = mapping.find(x); 267 | /* Note: due to how deletions are handled in earlier versions, 268 | * a Unique can end up staying in the deleted list of a child 269 | * even after it was deleted from the parent (this happens if it 270 | * is first deleted from the child and then the parent). In this 271 | * case, it is safe to just ignore it. */ 272 | if(it != mapping.end()) dst.deleted.insert(it->second); 273 | else extra_unique.insert(x); 274 | } 275 | 276 | for(auto& x : src.added) { 277 | auto it = mapping.find(x.first); 278 | if(it == mapping.end()) throw std::runtime_error("Unique not found!\n"); 279 | dst.added.insert(std::make_pair(it->second, std::move(x.second))); 280 | } 281 | 282 | for(auto& x : src.children) { 283 | ActionListDiff* y = dst.add_child(x.name, x.app); 284 | convert_actionlist(*y, x, mapping, extra_unique); 285 | } 286 | } 287 | 288 | template void ActionDB::load(Archive & ar, const unsigned int version) { 289 | if (version > 5) throw std::runtime_error("ActionDB::load(): unsupported archive version, maybe it was created with a newer version of WStroke?\n"); 290 | if (version == 5) { 291 | ar & root; 292 | ar & exclude_apps; 293 | if(next_id) { 294 | // read the order of strokes -- only matters for the GUI 295 | ar & stroke_order; 296 | ar & stroke_map; 297 | // recreate IDs mapping 298 | for(stroke_id x : stroke_order) if(x + 1 > next_id) next_id = x + 1; 299 | for(stroke_id x = 1; x < next_id; x++) if(!stroke_map.count(x)) available_ids.push_back(x); 300 | } 301 | } 302 | else if (version >= 2) { 303 | ActionListDiff root_tmp; 304 | ar & root_tmp; 305 | std::unordered_map mapping; 306 | std::unordered_set extra_unique; 307 | convert_actionlist(root, root_tmp, mapping, extra_unique); 308 | if (version >= 4) ar & exclude_apps; 309 | 310 | for(auto& x : mapping) delete x.first; 311 | for(Unique* x : extra_unique) delete x; 312 | } 313 | if (version == 1) { 314 | std::map strokes; 315 | ar & strokes; 316 | for (std::map::iterator i = strokes.begin(); i != strokes.end(); ++i) 317 | add_stroke(&root, std::move(i->second)); 318 | } 319 | if (version == 0) { 320 | std::map strokes; 321 | ar & strokes; 322 | for (std::map::iterator i = strokes.begin(); i != strokes.end(); ++i) { 323 | i->second.name = i->first; 324 | add_stroke(&root, std::move(i->second)); 325 | } 326 | } 327 | 328 | root.add_apps(apps); 329 | root.name = _("Default"); 330 | read_version = version; 331 | } 332 | 333 | char const * const ActionDB::wstroke_actions_versions[3] = { "actions-wstroke-2", "actions-wstroke", nullptr }; 334 | char const * const ActionDB::easystroke_actions_versions[5] = { "actions-0.5.6", "actions-0.4.1", "actions-0.4.0", "actions", nullptr }; 335 | 336 | bool ActionDB::read(const std::string& config_file_name, bool readonly) { 337 | clear(); 338 | next_id = readonly ? 0 : 1; 339 | if(!std::filesystem::exists(config_file_name)) return false; 340 | if(!std::filesystem::is_regular_file(config_file_name)) return false; 341 | std::ifstream ifs(config_file_name.c_str(), std::ios::binary); 342 | boost::archive::text_iarchive ia(ifs); 343 | ia >> *this; 344 | return true; 345 | } 346 | 347 | stroke_id ActionDB::add_stroke(ActionListDiff* parent, StrokeInfo&& si, stroke_id before) { 348 | stroke_id new_id = get_next_id(); 349 | parent->added.emplace(new_id, std::move(si)); 350 | unsigned int order = 0; 351 | 352 | auto it = stroke_order.end(); 353 | if(before) for(auto it = stroke_order.begin(); it != stroke_order.end(); ++it) 354 | if(*it == before) break; 355 | 356 | if(it != stroke_order.end()) order = stroke_map.at(*it).first; 357 | else if(!stroke_order.empty()) order = stroke_map.at(stroke_order.back()).first + 1; 358 | 359 | stroke_map[new_id] = std::pair(order, parent); 360 | it = stroke_order.insert(it, new_id); 361 | /* if the new element was inserted in the middle, the sort order 362 | * for all later elements needs to be updated */ 363 | for(++it, ++order; it != stroke_order.end(); ++it, ++order) { 364 | /* optimization: if the sort order is already large enough, we 365 | * do not need to update anymore (this can happen if strokes were 366 | * removed before */ 367 | auto& x = stroke_map.at(*it); 368 | if(x.first >= order) break; 369 | x.first = order; 370 | } 371 | 372 | return new_id; 373 | } 374 | 375 | -------------------------------------------------------------------------------- /src/actiondb.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2009, Thomas Jaeger 3 | * Copyright (c) 2020-2023, Daniel Kondor 4 | * 5 | * Permission to use, copy, modify, and/or distribute this software for any 6 | * purpose with or without fee is hereby granted, provided that the above 7 | * copyright notice and this permission notice appear in all copies. 8 | * 9 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 12 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 14 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 15 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | */ 17 | #ifndef __STROKEDB_H__ 18 | #define __STROKEDB_H__ 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | #include "gesture.h" 33 | 34 | class Action; 35 | class Command; 36 | class SendKey; 37 | class SendText; 38 | class Scroll; 39 | class Ignore; 40 | class Button; 41 | class Misc; 42 | class Global; 43 | class View; 44 | class Plugin; 45 | class Ranking; 46 | class Touchpad; 47 | 48 | 49 | class ActionVisitor { 50 | public: 51 | virtual ~ActionVisitor() { } 52 | virtual void visit(const Command* action) = 0; 53 | virtual void visit(const SendKey* action) = 0; 54 | virtual void visit(const SendText* action) = 0; 55 | virtual void visit(const Scroll* action) = 0; 56 | virtual void visit(const Ignore* action) = 0; 57 | virtual void visit(const Button* action) = 0; 58 | virtual void visit(const Global* action) = 0; 59 | virtual void visit(const View* action) = 0; 60 | virtual void visit(const Plugin* action) = 0; 61 | virtual void visit(const Touchpad* action) = 0; 62 | }; 63 | 64 | class Action { 65 | friend class boost::serialization::access; 66 | template void serialize(Archive & ar, const unsigned int version); 67 | public: 68 | virtual void visit(ActionVisitor* visitor) const = 0; 69 | virtual std::string get_type() const = 0; 70 | virtual ~Action() {} 71 | virtual std::unique_ptr clone() const = 0; 72 | }; 73 | 74 | class Command : public Action { 75 | friend class boost::serialization::access; 76 | template void serialize(Archive & ar, const unsigned int version); 77 | Command(const std::string &c, const std::string& fn) : cmd(c), desktop_file(fn) {} 78 | Command(const Command&) = default; 79 | public: 80 | std::string cmd; 81 | std::string desktop_file; // name of the .desktop file that belongs to this command 82 | Command() {} 83 | static std::unique_ptr create(const std::string& c, const std::string& fn = "") { return std::unique_ptr(new Command(c, fn)); } 84 | void visit(ActionVisitor* visitor) const override { visitor->visit(this); } 85 | std::string get_type() const override { return "Command"; } 86 | const std::string& get_cmd() const { return cmd; } 87 | std::unique_ptr clone() const override { return std::unique_ptr(new Command(*this)); } 88 | }; 89 | BOOST_CLASS_VERSION(Command, 1) 90 | 91 | class ModAction : public Action { 92 | friend class boost::serialization::access; 93 | BOOST_SERIALIZATION_SPLIT_MEMBER() 94 | template void load(Archive & ar, const unsigned int version); 95 | template void save(Archive & ar, const unsigned int version) const; 96 | protected: 97 | ModAction() {} 98 | uint32_t mods = 0; 99 | ModAction(uint32_t mods_) : mods(mods_) {} 100 | ModAction(const ModAction&) = default; 101 | public: 102 | uint32_t get_mods() const { return mods; } 103 | }; 104 | BOOST_CLASS_VERSION(ModAction, 1) 105 | /* version 1: save modifiers as enum wlr_keyboard_modifier 106 | * this notably does not support Gdk's "virtual" modifiers */ 107 | 108 | 109 | class SendKey : public ModAction { 110 | friend class boost::serialization::access; 111 | uint32_t key; 112 | BOOST_SERIALIZATION_SPLIT_MEMBER() 113 | template void load(Archive & ar, const unsigned int version); 114 | template void save(Archive & ar, const unsigned int version) const; 115 | SendKey(uint32_t key_, uint32_t mods) : 116 | ModAction(mods), key(key_) {} 117 | SendKey(const SendKey&) = default; 118 | public: 119 | SendKey() {} 120 | static std::unique_ptr create(uint32_t key, uint32_t mods) { 121 | return std::unique_ptr(new SendKey(key, mods)); 122 | } 123 | 124 | std::string get_type() const override { return "SendKey"; } 125 | uint32_t get_key() const { return key; } 126 | void visit(ActionVisitor* visitor) const override { visitor->visit(this); } 127 | std::unique_ptr clone() const override { return std::unique_ptr(new SendKey(*this)); } 128 | }; 129 | BOOST_CLASS_VERSION(SendKey, 2) 130 | /* version 2: save hardware keycode in key, omit separate code variable */ 131 | 132 | class SendText : public Action { 133 | friend class boost::serialization::access; 134 | std::string text; 135 | template void serialize(Archive & ar, const unsigned int version); 136 | SendText(Glib::ustring text_) : text(text_) {} 137 | SendText(const SendText&) = default; 138 | public: 139 | SendText() {} 140 | static std::unique_ptr create(Glib::ustring text) { return std::unique_ptr(new SendText(text)); } 141 | 142 | std::string get_type() const override { return "SendText"; } 143 | const Glib::ustring get_text() const { return text; } 144 | void visit(ActionVisitor* visitor) const override { visitor->visit(this); } 145 | std::unique_ptr clone() const override { return std::unique_ptr(new SendText(*this)); } 146 | }; 147 | 148 | class Scroll : public ModAction { 149 | friend class boost::serialization::access; 150 | template void serialize(Archive & ar, const unsigned int version); 151 | Scroll(uint32_t mods) : ModAction(mods) {} 152 | Scroll(const Scroll&) = default; 153 | public: 154 | Scroll() {} 155 | static std::unique_ptr create(uint32_t mods) { return std::unique_ptr(new Scroll(mods)); } 156 | std::string get_type() const override { return "Scroll"; } 157 | void visit(ActionVisitor* visitor) const override { visitor->visit(this); } 158 | std::unique_ptr clone() const override { return std::unique_ptr(new Scroll(*this)); } 159 | }; 160 | 161 | class Touchpad : public ModAction { 162 | friend class boost::serialization::access; 163 | public: 164 | enum Type { NONE, SCROLL, SWIPE, PINCH }; 165 | Type type = NONE; 166 | uint32_t fingers = 0; 167 | private: 168 | BOOST_SERIALIZATION_SPLIT_MEMBER() 169 | template void load(Archive & ar, const unsigned int version); 170 | template void save(Archive & ar, const unsigned int version) const; 171 | Touchpad(uint32_t mods, uint32_t fingers_, Type t) : ModAction(mods), type(t), fingers(fingers_) {} 172 | Touchpad(const Touchpad&) = default; 173 | public: 174 | Touchpad() {} 175 | static constexpr uint32_t n_actions = static_cast(Type::PINCH) + 1; 176 | static std::unique_ptr create(Type t, uint32_t fingers, uint32_t mods) { return std::unique_ptr(new Touchpad(mods, fingers, t)); } 177 | std::string get_type() const override { return "Touchpad"; } 178 | Type get_action_type() const { return type; } 179 | void visit(ActionVisitor* visitor) const override { visitor->visit(this); } 180 | std::unique_ptr clone() const override { return std::unique_ptr(new Touchpad(*this)); } 181 | }; 182 | 183 | class Ignore : public ModAction { 184 | friend class boost::serialization::access; 185 | template void serialize(Archive & ar, const unsigned int version); 186 | Ignore(uint32_t mods) : ModAction(mods) {} 187 | Ignore(const Ignore&) = default; 188 | public: 189 | Ignore() {} 190 | static std::unique_ptr create(uint32_t mods) { return std::unique_ptr(new Ignore(mods)); } 191 | std::string get_type() const override { return "Ignore"; } 192 | void visit(ActionVisitor* visitor) const override { visitor->visit(this); } 193 | std::unique_ptr clone() const override { return std::unique_ptr(new Ignore(*this)); } 194 | }; 195 | 196 | class Button : public ModAction { 197 | friend class boost::serialization::access; 198 | template void serialize(Archive & ar, const unsigned int version); 199 | Button(uint32_t mods, uint32_t button_) : ModAction(mods), button(button_) {} 200 | Button(const Button&) = default; 201 | uint32_t button = 0; 202 | public: 203 | Button() {} 204 | static std::unique_ptr create(uint32_t mods, uint32_t button_) { return std::unique_ptr(new Button(mods, button_)); } 205 | std::string get_type() const override { return "Button"; } 206 | uint32_t get_button() const { return button; } 207 | void visit(ActionVisitor* visitor) const override { visitor->visit(this); } 208 | std::unique_ptr clone() const override { return std::unique_ptr(new Button(*this)); } 209 | }; 210 | 211 | /* Misc action -- not used anymore, kept only for compatibility with old Easystroke config files */ 212 | class Misc : public Action { 213 | friend class boost::serialization::access; 214 | public: 215 | enum Type { NONE, UNMINIMIZE, SHOWHIDE, DISABLE }; 216 | Type type; 217 | private: 218 | template void serialize(Archive & ar, const unsigned int version); 219 | Misc(Type t) : type(t) {} 220 | public: 221 | Misc() {} 222 | std::string get_type() const override { return "Misc"; } 223 | static std::unique_ptr create(Type t) { return std::unique_ptr(new Misc(t)); } 224 | /* does nothing */ 225 | void visit(G_GNUC_UNUSED ActionVisitor* visitor) const override { return; } 226 | /* convert old Misc actions to new representation */ 227 | std::unique_ptr convert() const; 228 | std::unique_ptr clone() const override { return std::unique_ptr(new Misc(*this)); } 229 | }; 230 | 231 | /* new version: instead of Misc, we have Global Actions, View Actions, and Custom Plugin */ 232 | class Global : public Action { 233 | friend class boost::serialization::access; 234 | public: 235 | enum class Type : uint32_t { NONE, EXPO, SCALE, SCALE_ALL, SHOW_CONFIG, SHOW_DESKTOP, CUBE }; 236 | protected: 237 | Type type; 238 | BOOST_SERIALIZATION_SPLIT_MEMBER() 239 | template void load(Archive & ar, const unsigned int version); 240 | template void save(Archive & ar, const unsigned int version) const; 241 | Global(Type t): type(t) { } 242 | Global(): type(Type::NONE) { } 243 | Global(const Global&) = default; 244 | public: 245 | static constexpr uint32_t n_actions = static_cast(Type::CUBE) + 1; 246 | static const char* types[n_actions]; 247 | static const char* get_type_str(Type type); 248 | std::string get_type() const override { return "Global Action"; } 249 | static std::unique_ptr create(Type t) { return std::unique_ptr(new Global(t)); } 250 | Type get_action_type() const { return type; } 251 | void visit(ActionVisitor* visitor) const override { visitor->visit(this); } 252 | std::unique_ptr clone() const override { return std::unique_ptr(new Global(*this)); } 253 | }; 254 | 255 | /* actions performed on the active view (either directly or via another plugin) */ 256 | class View : public Action { 257 | friend class boost::serialization::access; 258 | public: 259 | enum class Type : uint32_t { NONE, CLOSE, MAXIMIZE, MOVE, RESIZE, MINIMIZE, FULLSCREEN, SEND_TO_BACK, ALWAYS_ON_TOP, STICKY }; 260 | protected: 261 | Type type; 262 | BOOST_SERIALIZATION_SPLIT_MEMBER() 263 | template void load(Archive & ar, const unsigned int version); 264 | template void save(Archive & ar, const unsigned int version) const; 265 | View(Type t): type(t) { } 266 | View(): type(Type::NONE) { } 267 | View(const View&) = default; 268 | public: 269 | static constexpr uint32_t n_actions = static_cast(Type::STICKY) + 1; 270 | static const char* types[n_actions]; 271 | static const char* get_type_str(Type type); 272 | std::string get_type() const override { return "View Action"; } 273 | static std::unique_ptr create(Type t) { return std::unique_ptr(new View(t)); } 274 | Type get_action_type() const { return type; } 275 | void visit(ActionVisitor* visitor) const override { visitor->visit(this); } 276 | std::unique_ptr clone() const override { return std::unique_ptr(new View(*this)); } 277 | }; 278 | 279 | /* custom plugin activator */ 280 | class Plugin : public Action { 281 | friend class boost::serialization::access; 282 | protected: 283 | template void serialize(Archive & ar, const unsigned int version); 284 | std::string cmd; 285 | Plugin() {} 286 | Plugin(const std::string &c) : cmd(c) {} 287 | Plugin(const Plugin&) = default; 288 | public: 289 | static std::unique_ptr create(const std::string &c) { return std::unique_ptr(new Plugin(c)); } 290 | void visit(ActionVisitor* visitor) const override { visitor->visit(this); } 291 | std::string get_type() const override { return "Custom Plugin Action"; } 292 | const std::string& get_action() const { return cmd; } 293 | std::unique_ptr clone() const override { return std::unique_ptr(new Plugin(*this)); } 294 | }; 295 | 296 | class StrokeInfo { 297 | private: 298 | friend class ActionDB; 299 | template friend class ActionListDiff; 300 | friend class boost::serialization::access; 301 | BOOST_SERIALIZATION_SPLIT_MEMBER() 302 | template void load(Archive & ar, const unsigned int version); 303 | template void save(Archive & ar, const unsigned int version) const; 304 | 305 | public: 306 | StrokeInfo(std::unique_ptr&& a) : action(std::move(a)) { } 307 | StrokeInfo() {} 308 | 309 | std::unique_ptr action; 310 | Stroke stroke; 311 | std::string name; 312 | }; 313 | BOOST_CLASS_VERSION(StrokeInfo, 4) 314 | 315 | struct StrokeRow { 316 | const Stroke* stroke = nullptr; 317 | const std::string* name = nullptr; 318 | const Action* action = nullptr; 319 | bool deleted = false; 320 | bool stroke_overwrite = false; 321 | bool name_overwrite = false; 322 | bool action_overwrite = false; 323 | }; 324 | 325 | class Ranking { 326 | int x, y; 327 | public: 328 | const Stroke *stroke, *best_stroke; 329 | Action* action; 330 | double score; 331 | std::string name; 332 | std::multimap > r; 333 | }; 334 | 335 | 336 | typedef uint32_t stroke_id; 337 | class Unique; 338 | class ActionDB; 339 | 340 | template 341 | class ActionListDiff { 342 | private: 343 | friend class boost::serialization::access; 344 | friend class ActionDB; 345 | using unique_t = typename std::conditional::type; 346 | 347 | template void serialize(Archive & ar, const unsigned int version) { 348 | ar & deleted; 349 | ar & added; 350 | ar & name; 351 | ar & children; 352 | ar & app; 353 | if constexpr (!uptr) { 354 | ar & parent; 355 | return; 356 | } 357 | if (version == 0) return; 358 | ar & order; 359 | } 360 | 361 | ActionListDiff *parent = nullptr; 362 | std::set deleted; 363 | std::map added; 364 | std::list order; // only for old version (uptr == true) 365 | std::list children; 366 | 367 | void remove(unique_t id, bool really, ActionListDiff* skip = nullptr); 368 | public: 369 | int level = 0; 370 | bool app = false; 371 | std::string name; 372 | 373 | typedef typename std::list::iterator iterator; 374 | iterator begin() { return children.begin(); } 375 | iterator end() { return children.end(); } 376 | ActionListDiff *get_parent() { return parent; } 377 | 378 | StrokeRow get_info(unique_t id, bool need_attr = true) const; 379 | const std::string& get_stroke_name(unique_t id) const; 380 | Action* get_stroke_action(unique_t id) const { 381 | auto it = added.find(id); 382 | if(it != added.end() && it->second.action) return it->second.action.get(); 383 | //!! TODO: check for non-null parent ?? 384 | return parent->get_stroke_action(id); 385 | } 386 | bool has_stroke(unique_t id) const { 387 | auto it = added.find(id); 388 | if(it != added.end() && !it->second.stroke.trivial()) return true; 389 | return false; 390 | } 391 | 392 | int size_rec() const { 393 | int size = added.size(); 394 | for (auto i = children.begin(); i != children.end(); i++) 395 | size += i->size_rec(); 396 | return size; 397 | } 398 | bool resettable(unique_t id) const { return parent && (added.count(id) || deleted.count(id)) && parent->contains(id); } 399 | 400 | void set_action(unique_t id, std::unique_ptr&& action) { added[id].action = std::move(action); } 401 | void set_stroke(unique_t id, Stroke&& stroke) { added[id].stroke = std::move(stroke); } 402 | void set_name(unique_t id, std::string name) { added[id].name = std::move(name); } 403 | bool contains(unique_t id) const { 404 | if (deleted.count(id)) 405 | return false; 406 | if (added.count(id)) 407 | return true; 408 | return parent && parent->contains(id); 409 | } 410 | void reset(unique_t id); 411 | void add_apps(std::map &apps) { 412 | if (app) apps[name] = this; 413 | for (auto& x : children) x.add_apps(apps); 414 | } 415 | ActionListDiff *add_child(std::string name, bool app); 416 | 417 | std::map get_strokes() const; 418 | std::set get_ids(bool include_deleted) const; 419 | int count_actions() const { 420 | if(parent) return get_ids(false).size(); 421 | else return added.size(); 422 | } 423 | Action* handle(const Stroke& s, Ranking* r) const; 424 | 425 | template 426 | void visit_all_actions(CB&& cb) const { 427 | for(const auto& x : added) if(x.second.action) cb(x.second.action.get()); 428 | } 429 | }; 430 | BOOST_CLASS_VERSION(ActionListDiff, 1) 431 | BOOST_CLASS_VERSION(ActionListDiff, 1) 432 | 433 | 434 | class ActionDB { 435 | private: 436 | /* input / output via boost */ 437 | friend class boost::serialization::access; 438 | friend class ActionListDiff; 439 | template void load(Archive & ar, const unsigned int version); 440 | template void save(Archive & ar, const unsigned int version) const; 441 | BOOST_SERIALIZATION_SPLIT_MEMBER() 442 | 443 | /* Recursively convert an ActionListDiff tree from an older version. 444 | * This can be called from the load() function above. */ 445 | void convert_actionlist(ActionListDiff& dst, ActionListDiff& src, 446 | std::unordered_map& mapping, std::unordered_set& extra_unique); 447 | unsigned int read_version = 0; 448 | 449 | /* Main storage */ 450 | std::map*> apps; 451 | ActionListDiff root; 452 | std::unordered_set exclude_apps; 453 | 454 | /* Storage of stroke_ids. 455 | * We store all stroke_ids in the order they should appear in the 456 | * gesture list along with a mapping from stroke_id to their sort order. */ 457 | std::list stroke_order; 458 | /* Each stroke_id has an "owner", that is the ActionListDiff where it was added. */ 459 | std::unordered_map*>> stroke_map; 460 | /* Next available ID. Setting this to 0 means no strokes can be added. */ 461 | stroke_id next_id = 1; 462 | /* Available (previously deleted) stroke IDs that can be reused. */ 463 | std::vector available_ids; 464 | 465 | /* Get a new stroke ID (for adding a new stroke). */ 466 | stroke_id get_next_id() { 467 | if(!next_id) throw std::runtime_error("ActionDB: read-only database!\n"); 468 | stroke_id ret = next_id; 469 | if(available_ids.size()) { 470 | ret = available_ids.back(); 471 | available_ids.pop_back(); 472 | } 473 | else next_id++; 474 | return ret; 475 | } 476 | /* Remove a stroke ID that is no longer used. */ 477 | void free_id(stroke_id id) { 478 | if(id >= next_id) throw std::runtime_error("ActionDB: too large ID to remove!\n"); 479 | if(id + 1 == next_id) next_id--; 480 | else available_ids.push_back(id); 481 | } 482 | 483 | /* Remove a set of strokes from the stroke_order list. Uses a sort 484 | * if the number of strokes is large to avoid quadratic runtime. */ 485 | template void remove_strokes_helper(iter&& begin, iter&& end); 486 | template void remove_strokes_from_order(iter begin, iter end, bool readonly = false); 487 | 488 | /* Helper to remove an app. */ 489 | void remove_app_r(ActionListDiff* app); 490 | 491 | /* Helper for merging two ActionDBs. */ 492 | void merge_actions_r(ActionListDiff* dst, ActionListDiff* src, std::unordered_map& id_map); 493 | 494 | /* Needed for clear(). */ 495 | ActionDB(ActionDB&&) = default; 496 | ActionDB& operator = (ActionDB&&) = default; 497 | 498 | public: 499 | /****************************************************************** 500 | * Input / output */ 501 | 502 | /* Try to read actions from the given config file. Returns false if 503 | * no config file found, throws an exception on other errors. 504 | * Note: this will clear any existing actions first. */ 505 | bool read(const std::string& config_file_name, bool readonly = false); 506 | /* Try to save actions to the config file; throws exception on failure. */ 507 | void write(const std::string& config_file_name) const; 508 | /* During read(), the version of the archive is stored. It can be retrieved here 509 | * and used to decide if a conversion from an older took place during loading. */ 510 | unsigned int get_read_version() const { return read_version; } 511 | /* Merge or replace the contents of this ActionDB with the given other one. */ 512 | void merge_actions(ActionDB&& other); 513 | void overwrite_actions(ActionDB&& other); 514 | 515 | 516 | /****************************************************************** 517 | * Handling apps and groups of apps */ 518 | 519 | const ActionListDiff *get_action_list(const std::string& wm_class) const { 520 | auto i = apps.find(wm_class); 521 | return i == apps.end() ? nullptr : i->second; 522 | } 523 | 524 | const ActionListDiff *get_root() const { return &root; } 525 | ActionListDiff *get_root() { return &root; } 526 | 527 | /* Add a new ActionListDiff corresponding to the given name with 528 | * the given parent. 529 | * Preconditions: parent must belong to this ActionDB instance and 530 | * name must not have previously been added. */ 531 | ActionListDiff* add_app(ActionListDiff* parent, const std::string& name, bool real_app); 532 | 533 | /* Remove an app or group that belongs to this ActionDB and is not the root. */ 534 | void remove_app(ActionListDiff* app); 535 | 536 | /* Manage apps that are excluded. */ 537 | const std::unordered_set& get_exclude_apps() const { return exclude_apps; } 538 | bool exclude_app(const std::string& cl) const { return exclude_apps.count(cl); } 539 | bool add_exclude_app(const std::string& cl) { return exclude_apps.insert(cl).second; } 540 | bool remove_exclude_app(const std::string& cl) { return exclude_apps.erase(cl); } 541 | 542 | 543 | /****************************************************************** 544 | * Manage strokes. */ 545 | const ActionListDiff* get_stroke_owner(stroke_id id) const { return stroke_map.at(id).second; } 546 | unsigned int get_stroke_order(stroke_id id) const { return stroke_map.at(id).first; } 547 | unsigned int count_owned_strokes(const ActionListDiff* parent) const; 548 | 549 | /* Add a new stroke owned by the ActionList given as parent. */ 550 | stroke_id add_stroke(ActionListDiff* parent, StrokeInfo&& si, stroke_id before = 0); 551 | 552 | /* Remove a set of strokes together (avoiding quadratic runtime). */ 553 | template 554 | void remove_strokes(ActionListDiff* parent, it&& begin, it&& end); 555 | 556 | /* Remove or disable the given stroke from the ActionList given. 557 | * If the stroke is owned by this ActionList, it is deleted recursively; 558 | * otherwise, it is only disabled. */ 559 | void remove_stroke(ActionListDiff* parent, stroke_id id); 560 | 561 | /* move one stroke, changing the ordering of strokes */ 562 | void move_stroke(stroke_id id, stroke_id before, bool after); 563 | 564 | /* move a set of strokes from their position to be before dst */ 565 | template 566 | void move_strokes(it&& begin, it&& end, stroke_id before, bool after); 567 | 568 | /* Move or copy strokes between apps / groups. Returns if the stroke 569 | * was removed from src. */ 570 | bool move_stroke_to_app(ActionListDiff* src, ActionListDiff* dst, stroke_id id); 571 | 572 | void clear() { *this = ActionDB(); } 573 | 574 | ActionDB() { root.name = _("Default"); } 575 | 576 | /* Config file names */ 577 | static char const * const wstroke_actions_versions[3]; 578 | static char const * const easystroke_actions_versions[5]; 579 | }; 580 | BOOST_CLASS_VERSION(ActionDB, 5) 581 | 582 | #endif 583 | 584 | -------------------------------------------------------------------------------- /src/actiondb_config.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2009, Thomas Jaeger 3 | * Copyright (c) 2020-2023, Daniel Kondor 4 | * 5 | * Permission to use, copy, modify, and/or distribute this software for any 6 | * purpose with or without fee is hereby granted, provided that the above 7 | * copyright notice and this permission notice appear in all copies. 8 | * 9 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 12 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 14 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 15 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | */ 17 | 18 | #include "actiondb.h" 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | 35 | template void StrokeInfo::save(Archive & ar, G_GNUC_UNUSED const unsigned int version) const { 36 | ar & stroke; 37 | ar & action; 38 | ar & name; 39 | } 40 | 41 | template void ActionDB::save(Archive & ar, G_GNUC_UNUSED unsigned int version) const { 42 | ar & root; 43 | ar & exclude_apps; 44 | ar & stroke_order; 45 | ar & stroke_map; 46 | } 47 | 48 | void ActionDB::write(const std::string& config_file_name) const { 49 | if(!next_id) throw std::runtime_error("ActionDB::write(): missing information!\n"); 50 | std::string tmp = config_file_name + ".tmp"; 51 | std::ofstream ofs(tmp.c_str()); 52 | boost::archive::text_oarchive oa(ofs); 53 | oa << *this; 54 | ofs.close(); 55 | if (rename(tmp.c_str(), config_file_name.c_str())) 56 | throw std::runtime_error(_("rename() failed")); 57 | printf("Saved actions.\n"); 58 | } 59 | 60 | template<> 61 | void ActionListDiff::remove(unique_t id, bool really, ActionListDiff* skip) { 62 | if(!really) deleted.insert(id); 63 | else deleted.erase(id); 64 | added.erase(id); 65 | for(auto& c : children) if(&c != skip) c.remove(id, true); 66 | } 67 | 68 | template<> 69 | bool ActionListDiff::has_stroke(unique_t id) const { 70 | auto it = added.find(id); 71 | if(it != added.end() && !it->second.stroke.trivial()) return true; 72 | return false; 73 | } 74 | 75 | template<> 76 | std::set ActionListDiff::get_ids(bool include_deleted) const { 77 | std::set ids = parent ? parent->get_ids(false) : std::set(); 78 | if(!include_deleted) for(const auto& x : deleted) ids.erase(x); 79 | for(const auto& x : added) ids.insert(x.first); 80 | return ids; 81 | } 82 | 83 | template<> 84 | StrokeRow ActionListDiff::get_info(stroke_id id, bool need_attr) const { 85 | StrokeRow si = parent ? parent->get_info(id, false) : StrokeRow(); 86 | if(need_attr) si.deleted = this->deleted.count(id); 87 | 88 | auto i = added.find(id); 89 | if(i == added.end()) return si; 90 | if(!parent || i->second.name != "") { 91 | si.name = &(i->second.name); 92 | if(need_attr) si.name_overwrite = (parent != nullptr); 93 | } 94 | if(!parent || !i->second.stroke.trivial()) { 95 | si.stroke = &i->second.stroke; 96 | if(need_attr) si.stroke_overwrite = (parent != nullptr); 97 | } 98 | if(i->second.action) { 99 | si.action = i->second.action.get(); 100 | if(need_attr) si.action_overwrite = (parent != nullptr); 101 | } 102 | return si; 103 | } 104 | 105 | template<> 106 | int ActionListDiff::size_rec() const { 107 | int size = added.size(); 108 | for (auto i = children.begin(); i != children.end(); i++) 109 | size += i->size_rec(); 110 | return size; 111 | } 112 | 113 | template<> 114 | void ActionListDiff::reset(unique_t id) { 115 | if(!parent) return; 116 | added.erase(id); 117 | deleted.erase(id); 118 | } 119 | 120 | 121 | template 122 | void ActionDB::remove_strokes_helper(iter&& begin, iter&& end) { 123 | std::sort(begin, end, [this](stroke_id x, stroke_id y) { 124 | return stroke_map.at(x).first < stroke_map.at(y).first; 125 | }); 126 | auto it = stroke_order.begin(); 127 | for(; begin != end; ++begin) { 128 | stroke_id x = *begin; 129 | while(true) { 130 | if(it == stroke_order.end()) throw std::runtime_error("ActionDB::remove_strokes_from_order(): missing stroke!\n"); 131 | if(*it == x) break; 132 | ++it; 133 | } 134 | it = stroke_order.erase(it); 135 | } 136 | } 137 | 138 | template 139 | void ActionDB::remove_strokes_from_order(iter begin, iter end, bool readonly) { 140 | if(begin == end) return; 141 | if(end - begin > 20) { 142 | if(readonly) { 143 | std::vector tmp; 144 | tmp.insert(tmp.end(), begin, end); 145 | remove_strokes_helper(tmp.begin(), tmp.end()); 146 | } 147 | else remove_strokes_helper(begin, end); 148 | } 149 | else for(auto it = stroke_order.begin(); it != stroke_order.end(); ) 150 | if(std::find(begin, end, *it) != end) 151 | it = stroke_order.erase(it); 152 | else ++it; 153 | } 154 | 155 | void ActionDB::remove_app_r(ActionListDiff* app) { 156 | /* 1. Remove all stroke_ids that are owned by app */ 157 | for(auto it = stroke_order.begin(); it != stroke_order.end(); ) { 158 | auto owner = stroke_map.at(*it).second; 159 | if(owner == app) { 160 | /* Remove this ID */ 161 | free_id(*it); 162 | stroke_map.erase(*it); 163 | it = stroke_order.erase(it); 164 | } 165 | else ++it; 166 | } 167 | /* 2. Remove from the list of apps */ 168 | if(app->app) apps.erase(app->name); 169 | /* 3. Remove all child apps */ 170 | for(auto& c : app->children) remove_app(&c); 171 | } 172 | 173 | ActionListDiff* ActionDB::add_app(ActionListDiff* parent, const std::string& name, bool real_app) { 174 | auto ret = parent->add_child(name, real_app); 175 | apps[name] = ret; 176 | return ret; 177 | } 178 | 179 | void ActionDB::remove_app(ActionListDiff* app) { 180 | /* Recursively remove all stroke_ids from this subtree and from the apps map */ 181 | remove_app_r(app); 182 | /* Remove the app from its parent -- this will recursively free memory */ 183 | auto parent = app->parent; /* parent is not null as app != root */ 184 | for(auto it = parent->children.begin(); it != parent->children.end(); ++it) 185 | if(&*it == app) { 186 | parent->children.erase(it); 187 | return; 188 | } 189 | throw std::runtime_error("ActionDB::remove_app(): app not found!\n"); 190 | } 191 | 192 | unsigned int ActionDB::count_owned_strokes(const ActionListDiff* parent) const { 193 | unsigned int ret = 0; 194 | for(const auto& x : parent->added) if(stroke_map.at(x.first).second == parent) ret++; 195 | return ret; 196 | } 197 | 198 | template 199 | void ActionDB::remove_strokes(ActionListDiff* parent, it&& begin, it&& end) { 200 | std::vector to_delete; 201 | end = std::remove_if(begin, end, [this, parent](stroke_id id) { 202 | bool really = (stroke_map.at(id).second == parent); 203 | parent->remove(id, really); 204 | return !really; 205 | }); 206 | remove_strokes_from_order(begin, end); 207 | for(; begin != end; ++begin) { 208 | stroke_id x = *begin; 209 | stroke_map.erase(x); 210 | free_id(x); 211 | } 212 | } 213 | 214 | template void ActionDB::remove_strokes::iterator>(ActionListDiff* parent, std::vector::iterator&& begin, std::vector::iterator&& end); 215 | 216 | 217 | void ActionDB::remove_stroke(ActionListDiff* parent, stroke_id id) { 218 | std::array tmp = {id}; 219 | remove_strokes(parent, tmp.begin(), tmp.end()); 220 | } 221 | 222 | void ActionDB::move_stroke(stroke_id id, stroke_id before, bool after) { 223 | if(id == before) return; 224 | std::list::iterator src = stroke_order.end(); 225 | std::list::iterator dst = stroke_order.end(); 226 | for(auto it = stroke_order.begin(); it != stroke_order.end(); ++it) { 227 | if(*it == id) src = it; 228 | if(*it == before) dst = it; 229 | } 230 | if(after && dst != stroke_order.end()) ++dst; 231 | if(src == stroke_order.end()) throw std::runtime_error("ActionDB::move_stroke(): stroke ID not found!\n"); 232 | src = stroke_order.erase(src); 233 | unsigned int order = 0; 234 | if(dst == stroke_order.end()) { 235 | if(!stroke_order.empty()) order = stroke_map.at(stroke_order.back()).first + 1; 236 | } 237 | else order = stroke_map.at(*dst).first; 238 | stroke_map.at(id).first = order; 239 | stroke_order.insert(dst, id); 240 | for(++order; dst != stroke_order.end(); ++dst, ++order) { 241 | auto& x = stroke_map.at(*dst); 242 | if(x.first >= order) break; 243 | x.first = order; 244 | } 245 | } 246 | 247 | template 248 | void ActionDB::move_strokes(it&& begin, it&& end, stroke_id before, bool after) { 249 | remove_strokes_from_order(begin, end, true); // note: we need to keep the order 250 | auto dst = stroke_order.end(); 251 | for(auto it2 = stroke_order.begin(); it2 != stroke_order.end(); ++it2) 252 | if(*it2 == before) { 253 | dst = it2; 254 | break; 255 | } 256 | if(after && dst != stroke_order.end()) ++dst; 257 | stroke_order.insert(dst, begin, end); 258 | 259 | /* recalculate the sort order for all elements */ 260 | unsigned int order = 0; 261 | for(stroke_id id : stroke_order) stroke_map.at(id).first = order++; 262 | } 263 | 264 | template void ActionDB::move_strokes::iterator>(std::vector::iterator&& begin, std::vector::iterator&& end, stroke_id before, bool after); 265 | template void ActionDB::move_strokes::reverse_iterator>(std::vector::reverse_iterator&& begin, std::vector::reverse_iterator&& end, stroke_id before, bool after); 266 | 267 | 268 | bool ActionDB::move_stroke_to_app(ActionListDiff* src, ActionListDiff* dst, stroke_id id) { 269 | if(src == dst) return false; 270 | if(!src->contains(id)) return false; 271 | 272 | /* Main cases: 273 | * 1. src is the ancestor of dst or vice versa -> we move all information from src to dst 274 | * (note: this might affect other descendants of src, and also potentially overwrites any 275 | * information already at dst) 276 | * 2. src and dst are "independent" -> in this case, we make a copy (potentially overwriting 277 | * any information in dst) 278 | * In either cases, the goal is to make dst look exactly like src was. 279 | */ 280 | 281 | auto parent = src->parent; 282 | while(parent && parent != dst) parent = parent->parent; 283 | if(parent == dst) { 284 | /* dst is the parent of src */ 285 | if(stroke_map.at(id).second == src) { 286 | /* simple case, just move everything */ 287 | dst->added[id] = std::move(src->added.at(id)); 288 | src->added.erase(id); 289 | stroke_map.at(id).second = dst; 290 | } 291 | else { 292 | /* move any properties that are overridden in src or in any of its parents */ 293 | StrokeInfo info; 294 | auto tmp = src; 295 | bool change_owner = false; 296 | do { 297 | auto it = tmp->added.find(id); 298 | if(it != tmp->added.end()) { 299 | if(stroke_map.at(id).second == tmp) change_owner = true; 300 | bool erase = true; 301 | if(!it->second.stroke.trivial()) { 302 | if(info.stroke.trivial()) info.stroke = std::move(it->second.stroke); 303 | else erase = false; 304 | } 305 | if(it->second.name != "") { 306 | if(info.name == "") info.name = std::move(it->second.name); 307 | else erase = false; 308 | } 309 | if(it->second.action) { 310 | if(!info.action) info.action = std::move(it->second.action); 311 | else erase = false; 312 | } 313 | if(erase) tmp->added.erase(it); 314 | } 315 | tmp = tmp->parent; 316 | } while(tmp != dst); 317 | StrokeInfo& di = dst->added[id]; // this might add a new element to dst->added 318 | if(!info.stroke.trivial()) di.stroke = std::move(info.stroke); 319 | if(info.name != "") di.name = std::move(info.name); 320 | if(info.action) di.action = std::move(info.action); 321 | if(change_owner) stroke_map.at(id).second = dst; 322 | } 323 | return false; 324 | } 325 | 326 | parent = dst->parent; 327 | while(parent && parent != src) parent = parent->parent; 328 | if(parent == src) { 329 | /* src is the parent of dst */ 330 | if(stroke_map.at(id).second == src) { 331 | /* simple case, just move down everything */ 332 | dst->added[id] = std::move(src->added.at(id)); 333 | src->remove(id, true, dst); 334 | stroke_map.at(id).second = dst; 335 | return true; 336 | } 337 | else { 338 | /* check if the stroke is deleted in any of dst's ancestors 339 | * (if yes, we will copy this stroke) */ 340 | bool deleted = false; 341 | auto tmp = dst->parent; 342 | while(tmp != src) if(tmp->deleted.count(id)) { 343 | deleted = true; 344 | break; 345 | } 346 | 347 | if(!deleted) { 348 | StrokeRow r = src->get_info(id, false); 349 | StrokeInfo& info = dst->added[id]; 350 | auto it = src->added.find(id); 351 | if(it != src->added.end()) { 352 | info = std::move(it->second); 353 | src->added.erase(it); 354 | } 355 | // we need to copy all things from r that are 356 | // (1) not already in info; and (2) overridden between src and dst 357 | bool copy_stroke = false; 358 | bool copy_name = false; 359 | bool copy_action = false; 360 | tmp = dst->parent; 361 | while(tmp != src) { 362 | auto it2 = tmp->added.find(id); 363 | if(!it2->second.stroke.trivial()) copy_stroke = true; 364 | if(it2->second.name != "") copy_name = true; 365 | if(it2->second.action) copy_action = true; 366 | } 367 | if(copy_stroke && info.stroke.trivial()) info.stroke = r.stroke->clone(); 368 | if(copy_name && info.name == "") info.name = *r.name; 369 | if(copy_action && !info.action) info.action = r.action->clone(); 370 | dst->deleted.erase(id); 371 | return false; 372 | } 373 | } 374 | } 375 | 376 | /* Here, dst and src are "unrelated": we need to copy all the info or 377 | * the whole stroke to dst */ 378 | dst->deleted.erase(id); 379 | if(dst->contains(id)) { 380 | /* This ID is already present, we need to copy parts of the info which 381 | * are not already the same. */ 382 | StrokeRow rsrc = src->get_info(id, false); 383 | StrokeRow rdst = dst->get_info(id, false); 384 | StrokeInfo* info = nullptr; 385 | /* Note: members of rsrc and rdst are pointers to the actual objects 386 | * which contain the relevant info; these can be used to compare if 387 | * anything needs to be copied. */ 388 | if(rsrc.stroke != rdst.stroke) { 389 | if(!info) info = &(dst->added[id]); 390 | info->stroke = rsrc.stroke->clone(); 391 | } 392 | if(rsrc.name != rdst.name) { 393 | if(!info) info = &(dst->added[id]); 394 | info->name = *rsrc.name; 395 | } 396 | if(rsrc.action != rdst.action) { 397 | if(!info) info = &(dst->added[id]); 398 | info->action = rsrc.action->clone(); 399 | } 400 | } 401 | else { 402 | // we copy the whole stroke 403 | StrokeRow r = src->get_info(id, false); 404 | StrokeInfo info; 405 | info.name = *r.name; 406 | info.action = r.action->clone(); 407 | info.stroke = r.stroke->clone(); 408 | add_stroke(dst, std::move(info)); 409 | } 410 | return false; 411 | } 412 | 413 | void ActionDB::merge_actions_r(ActionListDiff* dst, ActionListDiff* src, std::unordered_map& id_map) { 414 | auto new_dst = add_app(dst, src->name, src->app); 415 | for(auto& x : src->added) { 416 | auto it = id_map.find(x.first); 417 | if(it != id_map.end()) { 418 | new_dst->added[it->second] = std::move(x.second); 419 | } 420 | else { 421 | // note: this stroke should be owned by src in this case 422 | id_map[x.first] = add_stroke(new_dst, std::move(x.second)); 423 | /* StrokeRow r = src->get_info(x.first); 424 | StrokeInfo info; 425 | if(r.name) info.name = *r.name; 426 | if(r.action) info.action = r.action->clone(); 427 | if(r.stroke) info.stroke = r.stroke->clone(); 428 | add_stroke(action_list, std::move(info)); */ 429 | } 430 | } 431 | for(auto x : src->deleted) new_dst->deleted.insert(id_map.at(x)); 432 | 433 | for(auto& x : src->children) merge_actions_r(new_dst, &x, id_map); 434 | } 435 | 436 | 437 | void ActionDB::merge_actions(ActionDB&& other) { 438 | std::unordered_map id_map; 439 | for(const auto& x : other.exclude_apps) exclude_apps.insert(x); 440 | for(auto& x : other.root.added) id_map[x.first] = add_stroke(&root, std::move(x.second)); 441 | 442 | /* First we copy apps that exist in the current actions 443 | * (we don't try to merge groups since the tree structure can differ) */ 444 | auto other_apps = std::move(other.apps); // hack to avoid erasing while iterating (this works, since only erase() is called on other.apps) 445 | for(auto& x : other_apps) { 446 | auto it = apps.find(x.first); 447 | if(it == apps.end()) continue; 448 | auto action_list = it->second; 449 | 450 | /* Instead of only considering the apps added in this node, 451 | * we consider all strokes, since some properties can be overriden 452 | * on higher level */ 453 | for(auto id : other.stroke_order) { 454 | if(x.second->contains(id)) { 455 | StrokeRow r = x.second->get_info(id); 456 | if(! (r.stroke || r.name || r.action) ) continue; // no info (already copied in root) 457 | auto it2 = id_map.find(id); 458 | if(it2 != id_map.end()) { 459 | /* note: we use clone, since this might be an inherited stroke */ 460 | if(r.stroke) action_list->set_stroke(it2->second, r.stroke->clone()); 461 | if(r.name) action_list->set_name(it2->second, *r.name); 462 | if(r.action) action_list->set_action(it2->second, r.action->clone()); 463 | } 464 | else { 465 | /* copy this stroke */ 466 | StrokeInfo info; 467 | if(r.name) info.name = *r.name; 468 | if(r.action) info.action = r.action->clone(); 469 | if(r.stroke) info.stroke = r.stroke->clone(); 470 | add_stroke(action_list, std::move(info)); 471 | } 472 | } 473 | } 474 | 475 | other.remove_app(x.second); 476 | } 477 | 478 | /* copy the remaining tree structure */ 479 | for(auto& x : other.root.children) merge_actions_r(&root, &x, id_map); 480 | } 481 | 482 | void ActionDB::overwrite_actions(ActionDB&& other) { 483 | *this = std::move(other); 484 | for(auto& c : root.children) c.parent = &root; 485 | for(auto& x : stroke_map) if(x.second.second == &other.root) x.second.second = &root; 486 | } 487 | 488 | 489 | 490 | -------------------------------------------------------------------------------- /src/actiondb_plugin.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2009, Thomas Jaeger 3 | * Copyright (c) 2023, Daniel Kondor 4 | * 5 | * Permission to use, copy, modify, and/or distribute this software for any 6 | * purpose with or without fee is hereby granted, provided that the above 7 | * copyright notice and this permission notice appear in all copies. 8 | * 9 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 12 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 14 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 15 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | */ 17 | 18 | #include "gesture.h" 19 | #include "actiondb.h" 20 | 21 | template<> 22 | const std::string& ActionListDiff::get_stroke_name(unique_t id) const { 23 | auto it = added.find(id); 24 | if(it != added.end() && it->second.name != "") return it->second.name; 25 | //!! TODO: check for non-null parent ?? 26 | return parent->get_stroke_name(id); 27 | } 28 | 29 | template<> 30 | std::map ActionListDiff::get_strokes() const { 31 | std::map strokes = parent ? parent->get_strokes() : std::map(); 32 | for(const auto& x : deleted) strokes.erase(x); 33 | for(const auto& x : added) if(!x.second.stroke.trivial()) strokes[x.first] = &x.second.stroke; 34 | return strokes; 35 | } 36 | 37 | template<> 38 | Action* ActionListDiff::handle(const Stroke& s, Ranking* r) const { 39 | double best_score = 0.0; 40 | Action* ret = nullptr; 41 | if(r) r->stroke = &s; 42 | const auto strokes = get_strokes(); 43 | for(const auto& x : strokes) { 44 | const Stroke& y = *x.second; 45 | double score; 46 | int match = Stroke::compare(s, y, score); 47 | if (match < 0) 48 | continue; 49 | bool new_best = false; 50 | if(score > best_score) { 51 | new_best = true; 52 | best_score = score; 53 | ret = get_stroke_action(x.first); 54 | if(r) r->best_stroke = &y; 55 | } 56 | if(r) { 57 | const std::string& name = get_stroke_name(x.first); 58 | r->r.insert(std::pair > 59 | (score, std::pair(name, &y))); 60 | if(new_best) r->name = name; 61 | } 62 | } 63 | 64 | if(r) { 65 | r->score = best_score; 66 | r->action = ret; 67 | } 68 | return ret; 69 | } 70 | -------------------------------------------------------------------------------- /src/actions.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2009, Thomas Jaeger 3 | * Copyright (c) 2020-2023, Daniel Kondor 4 | * 5 | * Permission to use, copy, modify, and/or distribute this software for any 6 | * purpose with or without fee is hereby granted, provided that the above 7 | * copyright notice and this permission notice appear in all copies. 8 | * 9 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 12 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 14 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 15 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | */ 17 | #ifndef __ACTIONS_H__ 18 | #define __ACTIONS_H__ 19 | 20 | #include 21 | #include 22 | #include 23 | #include "actiondb.h" 24 | #include "appchooser.h" 25 | 26 | class TreeViewMulti : public Gtk::TreeView { 27 | bool pending; 28 | Gtk::TreePath path; 29 | virtual bool on_button_press_event(GdkEventButton* event); 30 | virtual bool on_button_release_event(GdkEventButton* event); 31 | virtual void on_drag_begin(const Glib::RefPtr &context); 32 | public: 33 | TreeViewMulti(); 34 | }; 35 | 36 | class Actions { 37 | public: 38 | Actions(const std::string& config_dir_, Glib::RefPtr& widgets_) : chooser(widgets_), widgets(widgets_), config_dir(config_dir_) { } 39 | void startup(Gtk::Application* app, Gtk::Dialog* message_dialog = nullptr); 40 | private: 41 | void on_button_delete(); 42 | void on_button_new(); 43 | void on_selection_changed(); 44 | void on_name_edited(const Glib::ustring& path, const Glib::ustring& new_text); 45 | void on_type_edited(const Glib::ustring& path, const Glib::ustring& new_text); 46 | void on_row_activated(Gtk::TreeRow& row); 47 | void on_cell_data_name(Gtk::CellRenderer* cell, const Gtk::TreeModel::iterator& iter); 48 | void on_cell_data_type(Gtk::CellRenderer* cell, const Gtk::TreeModel::iterator& iter); 49 | void save_actions(); 50 | void update_actions() { actions_changed = true; } 51 | public: 52 | void on_accel_edited(const gchar *path_string, guint accel_key, GdkModifierType accel_mods); 53 | void on_combo_edited(const gchar *path_string, guint item); 54 | void on_arg_editing_started(GtkCellEditable *editable, const gchar *path); 55 | void on_text_edited(const gchar *path, const gchar *new_text); 56 | void on_cell_data_arg(GtkCellRenderer *cell, gchar *path); 57 | void on_stroke_editing(const char* path); 58 | 59 | Gtk::Window* get_main_win() { return main_win.get(); } 60 | void exit() { exiting = true; save_actions(); } 61 | 62 | ActionDB actions; 63 | 64 | private: 65 | int compare_ids(const Gtk::TreeModel::iterator &a, const Gtk::TreeModel::iterator &b); 66 | class OnStroke; 67 | 68 | void focus(stroke_id id, int col, bool edit); 69 | 70 | void on_add_app(); 71 | void on_add_group(); 72 | void on_apps_selection_changed(); 73 | void load_app_list(const Gtk::TreeNodeChildren &ch, ActionListDiff *actions); 74 | void update_action_list(); 75 | void update_row(const Gtk::TreeRow& row); 76 | void update_counts(); 77 | void on_remove_app(); 78 | 79 | bool select_exclude_row(const Gtk::TreeModel::Path& path, const Gtk::TreeModel::iterator& iter, const std::string& name); 80 | void on_add_exclude(); 81 | void on_remove_exclude(); 82 | 83 | class ModelColumns : public Gtk::TreeModel::ColumnRecord { 84 | public: 85 | ModelColumns() { 86 | add(stroke); add(name); add(type); add(arg); add(cmd_save); add(id); 87 | add(name_bold); add(action_bold); add(deactivated); add(action_icon); 88 | add(cmd_path); add(custom_command); 89 | } 90 | Gtk::TreeModelColumn > stroke, action_icon; 91 | Gtk::TreeModelColumn name, type, arg, cmd_save, plugin_action_save, cmd_path; 92 | Gtk::TreeModelColumn id; 93 | Gtk::TreeModelColumn name_bold, action_bold; 94 | Gtk::TreeModelColumn deactivated, custom_command; 95 | }; 96 | class Store : public Gtk::ListStore { 97 | Actions *parent; 98 | public: 99 | Store(const Gtk::TreeModelColumnRecord &columns, Actions *p) : Gtk::ListStore(columns), parent(p) {} 100 | static Glib::RefPtr create(const Gtk::TreeModelColumnRecord &columns, Actions *parent) { 101 | return Glib::RefPtr(new Store(columns, parent)); 102 | } 103 | protected: 104 | bool row_draggable_vfunc(const Gtk::TreeModel::Path&) const; 105 | bool row_drop_possible_vfunc(const Gtk::TreeModel::Path &dest, const Gtk::SelectionData&) const; 106 | bool drag_data_received_vfunc(const Gtk::TreeModel::Path &dest, const Gtk::SelectionData& selection); 107 | }; 108 | class AppsStore : public Gtk::TreeStore { 109 | Actions *parent; 110 | public: 111 | AppsStore(const Gtk::TreeModelColumnRecord &columns, Actions *p) : Gtk::TreeStore(columns), parent(p) {} 112 | static Glib::RefPtr create(const Gtk::TreeModelColumnRecord &columns, Actions *parent) { 113 | return Glib::RefPtr(new AppsStore(columns, parent)); 114 | } 115 | protected: 116 | bool row_drop_possible_vfunc(const Gtk::TreeModel::Path &dest, const Gtk::SelectionData &selection) const; 117 | bool drag_data_received_vfunc(const Gtk::TreeModel::Path &dest, const Gtk::SelectionData& selection); 118 | }; 119 | ModelColumns cols; 120 | TreeViewMulti tv; 121 | Glib::RefPtr tm; 122 | 123 | /* Special casing to store additional info about Command actions */ 124 | struct CommandInfo { 125 | Glib::ustring name; 126 | Glib::RefPtr icon; 127 | }; 128 | std::unordered_map command_info; 129 | void load_command_infos_r(ActionListDiff& x); 130 | void load_command_infos(); 131 | /* helper for the app chooser */ 132 | AppChooser chooser; 133 | 134 | Gtk::TreeView *apps_view = nullptr; 135 | Glib::RefPtr apps_model; 136 | /* helper to find a given app in apps_view / apps_model */ 137 | bool get_action_item(const ActionListDiff* x, Gtk::TreeIter& it); 138 | 139 | class Single : public Gtk::TreeModel::ColumnRecord { 140 | public: 141 | Single() { add(type); } 142 | Gtk::TreeModelColumn type; 143 | }; 144 | Single type; 145 | 146 | class Apps : public Gtk::TreeModel::ColumnRecord { 147 | public: 148 | Apps() { add(app); add(actions); add(count); } 149 | Gtk::TreeModelColumn app; 150 | Gtk::TreeModelColumn*> actions; 151 | Gtk::TreeModelColumn count; 152 | }; 153 | Apps ca; 154 | 155 | /* exception list */ 156 | Single exclude_cols; 157 | Glib::RefPtr exclude_tm; 158 | Gtk::TreeView* exclude_tv; 159 | 160 | struct Focus; 161 | 162 | Glib::RefPtr type_store; 163 | 164 | Gtk::Button *button_record, *button_delete, *button_remove_app, *button_reset_actions; 165 | Gtk::CheckButton *check_show_deleted; 166 | Gtk::Expander *expander_apps; 167 | Gtk::VPaned *vpaned_apps; 168 | 169 | int vpaned_position; 170 | bool editing_new = false; 171 | bool editing = false; 172 | 173 | ActionListDiff* action_list; 174 | Glib::RefPtr widgets; 175 | 176 | /* import / export */ 177 | Gtk::Window* import_dialog; 178 | Gtk::Button* button_import_cancel; 179 | Gtk::Button* button_import_import; 180 | Gtk::FileChooserButton* import_file_chooser; 181 | Gtk::RadioButton* import_add; 182 | Gtk::InfoBar* import_info; 183 | Gtk::Label* import_info_label; 184 | void try_import(); 185 | void try_export(); 186 | 187 | /* main window */ 188 | std::unique_ptr main_win; 189 | const std::string config_dir; 190 | Glib::RefPtr timeout; /* timeout for saving changes */ 191 | bool actions_changed = false; 192 | bool exiting = false; 193 | bool save_error = false; 194 | }; 195 | 196 | #endif 197 | 198 | -------------------------------------------------------------------------------- /src/appchooser.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Daniel Kondor 3 | * 4 | * Permission to use, copy, modify, and/or distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 11 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 13 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 14 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | 18 | #include "appchooser.h" 19 | #include 20 | 21 | AppChooser::AppBox::AppBox(Glib::RefPtr& app_) : Gtk::Box(Gtk::Orientation::ORIENTATION_VERTICAL), app(app_) { 22 | auto image = new Gtk::Image(app->get_icon(), Gtk::IconSize(Gtk::ICON_SIZE_DIALOG)); 23 | image->set_pixel_size(48); 24 | this->add(*image); 25 | const std::string& name = app->get_name(); 26 | auto label = new Gtk::Label(name.length() > 23 ? name.substr(0, 20) + "..." : name); 27 | this->add(*label); 28 | name_lower = Glib::ustring(app->get_name()).lowercase(); 29 | } 30 | 31 | void AppChooser::update_apps() { 32 | bool tmp; 33 | mutex.lock(); 34 | tmp = thread_running; 35 | if(tmp) more_work = true; 36 | mutex.unlock(); 37 | 38 | if(!tmp) { 39 | if(thread.joinable()) thread.join(); 40 | thread_running = true; 41 | more_work = false; 42 | thread = std::thread(&AppChooser::thread_func, this); 43 | } 44 | update_pending = false; 45 | } 46 | 47 | void AppChooser::thread_func() { 48 | while(true) { 49 | if(exit_request.load()) break; 50 | 51 | AppContent* tmp = new AppContent(); 52 | tmp->apps = Gio::AppInfo::get_all(); 53 | if(exit_request.load()) { 54 | delete tmp; 55 | break; 56 | } 57 | 58 | auto it = std::remove_if(tmp->apps.begin(), tmp->apps.end(), [](const Glib::RefPtr& p) { return !p->should_show(); }); 59 | tmp->apps.erase(it, tmp->apps.end()); 60 | 61 | tmp->flowbox.reset(new Gtk::FlowBox()); 62 | 63 | for(auto& a : tmp->apps) { 64 | if(exit_request.load()) break; 65 | auto box = new AppBox(a); 66 | tmp->flowbox->add(*box); 67 | } 68 | 69 | std::lock_guard lock(mutex); 70 | tmp = apps_pending.exchange(tmp); 71 | if(tmp) delete tmp; 72 | if(!more_work) { 73 | thread_running = false; 74 | break; 75 | } 76 | more_work = false; 77 | } 78 | } 79 | 80 | 81 | void on_apps_changed(GAppInfoMonitor*, void* p) { 82 | AppChooser* chooser = (AppChooser*)p; 83 | if(chooser->update_pending) return; 84 | chooser->update_pending = true; 85 | 86 | Glib::signal_timeout().connect_seconds_once([chooser](){ chooser->update_apps(); }, 4); 87 | } 88 | 89 | void AppChooser::startup() { 90 | update_apps(); 91 | monitor = g_app_info_monitor_get(); 92 | g_signal_connect(monitor, "changed", G_CALLBACK(on_apps_changed), this); 93 | 94 | /* basic setup for the GUI */ 95 | widgets->get_widget("dialog_appchooser", dialog); 96 | widgets->get_widget("header_appchooser", header); 97 | widgets->get_widget("entry_appchooser", entry); 98 | widgets->get_widget("checkbutton_appchooser", cb); 99 | widgets->get_widget("scrolledwindow_appchooser", sw); 100 | widgets->get_widget("appchooser_ok", select_ok); 101 | widgets->get_widget("searchentry_appchooser", searchentry); 102 | 103 | cb->signal_toggled().connect([this]() { 104 | entry->set_sensitive(cb->get_active()); 105 | }); 106 | 107 | searchentry->signal_search_changed().connect([this]() { 108 | filter_lower = searchentry->get_text().lowercase(); 109 | if(apps) apps->flowbox->invalidate_filter(); 110 | }); 111 | searchentry->signal_stop_search().connect([this]() { 112 | searchentry->set_text(Glib::ustring()); 113 | }); 114 | } 115 | 116 | int AppChooser::apps_sort(const Gtk::FlowBoxChild* x, const Gtk::FlowBoxChild* y) { 117 | const AppBox* a = dynamic_cast(x->get_child()); 118 | const AppBox* b = dynamic_cast(y->get_child()); 119 | if(!(a && b)) return 0; // or throw an exception? 120 | return a->compare(*b); 121 | } 122 | 123 | bool AppChooser::apps_filter(const Gtk::FlowBoxChild* x) const { 124 | if(filter_lower.empty()) return true; 125 | const AppBox* a = dynamic_cast(x->get_child()); 126 | return a && a->filter(filter_lower); 127 | } 128 | 129 | bool AppChooser::run(const Glib::ustring& gesture_name, const Glib::ustring& custom_command) { 130 | if(!apps && !apps_pending) thread.join(); // in this case, the worker thread should be running 131 | 132 | AppContent* tmp = nullptr; 133 | tmp = apps_pending.exchange(tmp); 134 | if(tmp) { 135 | sw->remove(); 136 | apps.reset(tmp); 137 | apps->flowbox->signal_child_activated().connect([this](Gtk::FlowBoxChild*) { 138 | dialog->response(Gtk::RESPONSE_OK); 139 | }); 140 | apps->flowbox->set_valign(Gtk::ALIGN_START); 141 | apps->flowbox->set_homogeneous(true); 142 | apps->flowbox->set_activate_on_single_click(false); 143 | apps->flowbox->set_sort_func(&apps_sort); 144 | apps->flowbox->set_filter_func([this](const Gtk::FlowBoxChild* x) { return apps_filter(x); }); 145 | sw->add(*apps->flowbox); 146 | } 147 | 148 | if(!apps) return false; 149 | 150 | if(custom_command.empty()) { 151 | cb->set_active(false); 152 | entry->set_sensitive(false); 153 | } 154 | else { 155 | cb->set_active(true); 156 | entry->set_sensitive(true); 157 | } 158 | entry->set_text(custom_command); 159 | 160 | select_ok->grab_default(); 161 | Glib::ustring str = Glib::ustring::compose(_("Choose app to run for gesture %1"), gesture_name); 162 | header->set_subtitle(str); 163 | 164 | dialog->show_all(); 165 | 166 | auto x = dialog->run(); 167 | dialog->hide(); 168 | 169 | if(x == Gtk::RESPONSE_OK) { 170 | if(cb->get_active()) { 171 | res_cmdline = entry->get_text(); 172 | custom_res = true; 173 | res_app.reset(); 174 | return true; 175 | } 176 | else { 177 | custom_res = false; 178 | auto tmp = apps->flowbox->get_selected_children(); 179 | if(tmp.size()) { 180 | auto selected = tmp.front(); 181 | const AppBox* box = dynamic_cast(selected->get_child()); 182 | if(box) { 183 | res_app = box->get_app(); 184 | res_cmdline = res_app->get_commandline(); 185 | // remove placeholders (%f, %F, %u and %U for files, and misc; note: in theory, we should properly parse %i, %c and %k) 186 | auto i = res_cmdline.begin(); 187 | for(auto j = res_cmdline.begin(); j != res_cmdline.end(); ++j) { 188 | if(*j == '%') { 189 | ++j; 190 | if(j == res_cmdline.end()) break; 191 | if(*j != '%') continue; 192 | } 193 | if(j != i) *i = *j; 194 | ++i; 195 | } 196 | res_cmdline.erase(i, res_cmdline.end()); 197 | return true; 198 | } 199 | } 200 | } 201 | } 202 | res_app.reset(); 203 | res_cmdline = ""; 204 | custom_res = false; 205 | return false; 206 | } 207 | 208 | 209 | 210 | AppChooser::~AppChooser() { 211 | if(monitor) g_object_unref(monitor); 212 | exit_request.store(true); 213 | if(thread.joinable()) thread.join(); 214 | AppContent* tmp = apps_pending; 215 | if(tmp) delete tmp; 216 | sw->remove(); 217 | } 218 | 219 | 220 | -------------------------------------------------------------------------------- /src/appchooser.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Daniel Kondor 3 | * 4 | * Permission to use, copy, modify, and/or distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 11 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 13 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 14 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | 18 | #ifndef APPCHOOSER_H 19 | #define APPCHOOSER_H 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | class AppChooser { 28 | private: 29 | Glib::RefPtr widgets; 30 | Gtk::Dialog* dialog; 31 | Gtk::HeaderBar* header; 32 | Gtk::ScrolledWindow* sw; 33 | Gtk::SearchEntry* searchentry; 34 | Gtk::Entry* entry; 35 | Gtk::CheckButton* cb; 36 | Gtk::Button *select_ok; 37 | Glib::ustring filter_lower; 38 | 39 | GAppInfoMonitor* monitor = nullptr; 40 | 41 | class AppBox : public Gtk::Box { 42 | private: 43 | Glib::RefPtr app; 44 | Glib::ustring name_lower; 45 | public: 46 | AppBox(Glib::RefPtr& app_); 47 | Glib::RefPtr get_app() const { return app; } 48 | bool filter(const Glib::ustring& filter) const { return name_lower.find(filter) != name_lower.npos; } // filter already lowercase 49 | int compare(const AppBox& box) const { return name_lower.compare(box.name_lower); } 50 | }; 51 | 52 | struct AppContent { 53 | std::vector > apps; 54 | std::unique_ptr flowbox; 55 | }; 56 | 57 | std::unique_ptr apps; 58 | std::atomic apps_pending; 59 | 60 | std::thread thread; 61 | std::mutex mutex; 62 | bool thread_running = false; 63 | bool more_work = false; 64 | std::atomic exit_request{false}; 65 | bool first_run = false; 66 | 67 | bool update_pending = false; 68 | 69 | void update_apps(); 70 | void thread_func(); 71 | 72 | friend void on_apps_changed(GAppInfoMonitor*, void* p); 73 | 74 | static int apps_sort(const Gtk::FlowBoxChild* x, const Gtk::FlowBoxChild* y); 75 | bool apps_filter(const Gtk::FlowBoxChild* x) const; 76 | 77 | public: 78 | Glib::RefPtr res_app; 79 | std::string res_cmdline; 80 | bool custom_res = false; 81 | 82 | AppChooser(Glib::RefPtr& w) : widgets(w) { } 83 | ~AppChooser(); 84 | 85 | void startup(); 86 | 87 | bool run(const Glib::ustring& gesture_name, const Glib::ustring& custom_command); 88 | }; 89 | 90 | #endif 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/cellrenderertextish.vala: -------------------------------------------------------------------------------- 1 | /* compile with valac -c cellrenderertextish.vala --pkg gtk+-3.0 --vapidir . --pkg input_inhibitor -C -H cellrenderertextish.h */ 2 | 3 | public class CellRendererTextish : Gtk.CellRendererText { 4 | public enum Mode { Text, Key, Popup, Combo } 5 | public new Mode mode; 6 | public unowned string[] items; 7 | 8 | public Gdk.Pixbuf? icon { get; set; default = null; } 9 | 10 | public signal void key_edited(string path, Gdk.ModifierType mods, uint code); 11 | public signal void combo_edited(string path, uint row); 12 | 13 | private Gtk.CellEditable? cell; 14 | 15 | public CellRendererTextish() { 16 | mode = Mode.Text; 17 | cell = null; 18 | items = null; 19 | } 20 | 21 | public CellRendererTextish.with_items(string[] items) { 22 | mode = Mode.Text; 23 | cell = null; 24 | this.items = items; 25 | } 26 | 27 | public void set_items(string[] items_) { 28 | items = items_; 29 | } 30 | 31 | public override unowned Gtk.CellEditable? start_editing(Gdk.Event? event, Gtk.Widget widget, string path, Gdk.Rectangle background_area, Gdk.Rectangle cell_area, Gtk.CellRendererState flags) { 32 | cell = null; 33 | if (!editable) 34 | return cell; 35 | switch (mode) { 36 | case Mode.Text: 37 | cell = base.start_editing(event, widget, path, background_area, cell_area, flags); 38 | break; 39 | case Mode.Key: 40 | cell = new CellEditableAccel(this, path, widget); 41 | break; 42 | case Mode.Combo: 43 | cell = new CellEditableCombo(this, path, widget, items); 44 | break; 45 | case Mode.Popup: 46 | cell = new CellEditableDummy(); 47 | break; 48 | } 49 | return cell; 50 | } 51 | 52 | public override void render(Cairo.Context ctx, Gtk.Widget widget, Gdk.Rectangle background_area, Gdk.Rectangle cell_area, Gtk.CellRendererState flags) { 53 | Gdk.cairo_rectangle(ctx, cell_area); 54 | if(icon != null) { 55 | Gdk.cairo_set_source_pixbuf(ctx, icon, cell_area.x, cell_area.y + cell_area.height / 2 - icon.height / 2); 56 | ctx.fill(); 57 | cell_area.x += icon.width + 4; 58 | } 59 | base.render(ctx, widget, background_area, cell_area, flags); 60 | } 61 | 62 | public override void get_size(Gtk.Widget widget, Gdk.Rectangle? cell_area, out int x_offset, out int y_offset, out int width, out int height) { 63 | base.get_size(widget, cell_area, out x_offset, out y_offset, out width, out height); 64 | if(icon != null) { 65 | width += icon.width; 66 | height = int.max(height, icon.height); 67 | } 68 | } 69 | 70 | public override void get_preferred_height(Gtk.Widget widget, out int minimum_size, out int natural_size) { 71 | base.get_preferred_height(widget, out minimum_size, out natural_size); 72 | if(icon != null) { 73 | minimum_size = int.max(minimum_size, icon.height); 74 | natural_size = int.max(natural_size, icon.height); 75 | } 76 | } 77 | public override void get_preferred_height_for_width(Gtk.Widget widget, int width, out int minimum_height, out int natural_height) { 78 | base.get_preferred_height_for_width(widget, width, out minimum_height, out natural_height); 79 | if(icon != null) { 80 | minimum_height = int.max(minimum_height, icon.height); 81 | natural_height = int.max(natural_height, icon.height); 82 | } 83 | } 84 | public override void get_preferred_width(Gtk.Widget widget, out int minimum_size, out int natural_size) { 85 | base.get_preferred_width(widget, out minimum_size, out natural_size); 86 | if(icon != null) { 87 | minimum_size += icon.width; 88 | natural_size += icon.width; 89 | } 90 | } 91 | public override void get_preferred_width_for_height(Gtk.Widget widget, int height, out int minimum_width, out int natural_width) { 92 | base.get_preferred_width_for_height(widget, height, out minimum_width, out natural_width); 93 | if(icon != null) { 94 | minimum_width += icon.width; 95 | natural_width += icon.width; 96 | } 97 | } 98 | } 99 | 100 | class CellEditableDummy : Gtk.EventBox, Gtk.CellEditable { 101 | public bool editing_canceled { get; set; } 102 | protected virtual void start_editing(Gdk.Event? event) { 103 | editing_done(); 104 | remove_widget(); 105 | } 106 | } 107 | 108 | class CellEditableAccel : Gtk.EventBox, Gtk.CellEditable { 109 | public bool editing_canceled { get; set; } 110 | new CellRendererTextish parent; 111 | new string path; 112 | 113 | public CellEditableAccel(CellRendererTextish parent, string path, Gtk.Widget widget) { 114 | this.parent = parent; 115 | this.path = path; 116 | editing_done.connect(on_editing_done); 117 | Gtk.Label label = new Gtk.Label("Key combination..."); 118 | label.set_alignment(0.0f, 0.5f); 119 | add(label); 120 | override_background_color(Gtk.StateFlags.NORMAL, widget.get_style_context().get_background_color(Gtk.StateFlags.SELECTED)); 121 | label.override_color(Gtk.StateFlags.NORMAL, widget.get_style_context().get_color(Gtk.StateFlags.SELECTED)); 122 | show_all(); 123 | } 124 | 125 | protected virtual void start_editing(Gdk.Event? event) { 126 | Gtk.grab_add(this); 127 | Gdk.keyboard_grab(get_window(), false, event != null ? event.get_time() : Gdk.CURRENT_TIME); 128 | 129 | Inhibitor.grab(); 130 | 131 | /* 132 | Gdk.DeviceManager dm = get_window().get_display().get_device_manager(); 133 | foreach (Gdk.Device dev in dm.list_devices(Gdk.DeviceType.SLAVE)) 134 | Gtk.device_grab_add(this, dev, true); 135 | */ 136 | key_press_event.connect(on_key); 137 | } 138 | 139 | bool on_key(Gdk.EventKey event) { 140 | if (event.is_modifier != 0) 141 | return true; 142 | switch (event.keyval) { 143 | case Gdk.Key.Super_L: 144 | case Gdk.Key.Super_R: 145 | case Gdk.Key.Hyper_L: 146 | case Gdk.Key.Hyper_R: 147 | return true; 148 | } 149 | Gdk.ModifierType mods = event.state; /* & Gtk.accelerator_get_default_mod_mask(); -- does not work! */ 150 | 151 | editing_done(); 152 | remove_widget(); 153 | 154 | parent.key_edited(path, mods, event.hardware_keycode); 155 | return true; 156 | } 157 | void on_editing_done() { 158 | Gtk.grab_remove(this); 159 | Gdk.keyboard_ungrab(Gdk.CURRENT_TIME); 160 | Inhibitor.ungrab(); 161 | 162 | /* 163 | Gdk.DeviceManager dm = get_window().get_display().get_device_manager(); 164 | foreach (Gdk.Device dev in dm.list_devices(Gdk.DeviceType.SLAVE)) 165 | Gtk.device_grab_remove(this, dev); 166 | */ 167 | } 168 | } 169 | 170 | 171 | class CellEditableCombo : Gtk.ComboBoxText, Gtk.CellEditable { 172 | new CellRendererTextish parent; 173 | new string path; 174 | 175 | public CellEditableCombo(CellRendererTextish parent, string path, Gtk.Widget widget, string[] items) { 176 | this.parent = parent; 177 | this.path = path; 178 | foreach (string item in items) { 179 | append_text(item); 180 | } 181 | changed.connect(() => parent.combo_edited(path, active)); 182 | } 183 | 184 | public virtual void start_editing(Gdk.Event? event) { 185 | base.start_editing(event); 186 | show_all(); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/config.h.in: -------------------------------------------------------------------------------- 1 | #ifndef CONFIG_H 2 | #define CONFIG_H 3 | 4 | #define DATA_DIR "@DATA_DIR@" 5 | 6 | #endif 7 | -------------------------------------------------------------------------------- /src/convert_keycodes.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * convert_keycodes.cc 3 | * 4 | * Copyright 2020 Daniel Kondor 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 13 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 15 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 16 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | * 18 | */ 19 | 20 | 21 | #include "convert_keycodes.h" 22 | #include 23 | extern "C" 24 | { 25 | #include 26 | } 27 | 28 | 29 | GdkKeymap* KeyCodes::keymap = nullptr; 30 | unsigned int KeyCodes::keycode_errors = 0; 31 | 32 | void KeyCodes::init() { 33 | if(keymap) return; 34 | GdkDisplay* dpy = gdk_display_get_default(); 35 | keymap = gdk_keymap_get_for_display(dpy); 36 | } 37 | 38 | static constexpr std::array, 10> modifier_match = { 39 | std::pair(GDK_SHIFT_MASK, WLR_MODIFIER_SHIFT), 40 | std::pair(GDK_LOCK_MASK, WLR_MODIFIER_CAPS), 41 | std::pair(GDK_CONTROL_MASK, WLR_MODIFIER_CTRL), 42 | std::pair(GDK_MOD1_MASK, WLR_MODIFIER_ALT), 43 | std::pair(GDK_META_MASK, WLR_MODIFIER_ALT), 44 | std::pair(GDK_MOD2_MASK, WLR_MODIFIER_MOD2), 45 | std::pair(GDK_MOD3_MASK, WLR_MODIFIER_MOD3), 46 | std::pair(GDK_MOD5_MASK, WLR_MODIFIER_MOD5), 47 | std::pair(GDK_MOD4_MASK, WLR_MODIFIER_LOGO), 48 | std::pair(GDK_SUPER_MASK, WLR_MODIFIER_LOGO) 49 | }; 50 | 51 | uint32_t KeyCodes::convert_modifier(uint32_t mod) { 52 | uint32_t ret = 0; 53 | for(auto p : modifier_match) if(mod & p.first) ret |= p.second; 54 | return ret; 55 | } 56 | 57 | uint32_t KeyCodes::convert_keysym(uint32_t key) { 58 | if(!keymap) return 0; 59 | uint32_t ret = 0; 60 | GdkKeymapKey* keys = nullptr; 61 | gint n_keys = 0; 62 | if(gdk_keymap_get_entries_for_keyval(keymap, key, &keys, &n_keys) && n_keys && keys) { 63 | for(gint i = 0; i < n_keys; i++) { 64 | if(keys[i].group == 0 || keys[i].level == 0) { 65 | ret = keys[i].keycode; 66 | break; 67 | } 68 | } 69 | } 70 | if(keys) g_free(keys); 71 | if(!ret) { 72 | keycode_errors++; 73 | fprintf(stderr, "KeyCodes::convert_keysym(): could not convert %u\n", key); 74 | } 75 | return ret; 76 | } 77 | 78 | uint32_t KeyCodes::convert_keycode(uint32_t code) { 79 | if(!keymap) return 0; 80 | GdkKeymapKey key; 81 | key.keycode = code; 82 | key.level = 0; 83 | key.group = 0; 84 | return gdk_keymap_lookup_key(keymap, &key); 85 | } 86 | 87 | uint32_t KeyCodes::add_virtual_modifiers(uint32_t mod) { 88 | /* currently this only takes care of super 89 | * additional logic might be needed on e.g. Apple keyboards */ 90 | if(mod & WLR_MODIFIER_LOGO) { 91 | mod ^= WLR_MODIFIER_LOGO; 92 | mod |= GDK_SUPER_MASK; 93 | } 94 | return mod; 95 | } 96 | 97 | -------------------------------------------------------------------------------- /src/convert_keycodes.h: -------------------------------------------------------------------------------- 1 | /* 2 | * convert_keycodes.h 3 | * 4 | * Copyright 2020 Daniel Kondor 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 13 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 15 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 16 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | * 18 | */ 19 | 20 | 21 | #include 22 | #include 23 | 24 | #ifndef CONVERT_KEYCODES_H 25 | #define CONVERT_KEYCODES_H 26 | 27 | class KeyCodes { 28 | public: 29 | /* convert from a combination of Gdk modifier constants to the 30 | * WLR modifier enum constants (take care of "virtual" modifiers 31 | * like SUPER, ALT, etc.) */ 32 | static uint32_t convert_modifier(uint32_t mod); 33 | /* add back "virtual" modifiers -- calls the corresponding GDK function */ 34 | static uint32_t add_virtual_modifiers(uint32_t mod); 35 | /* try to convert a keysym to a hardware keycode; returns the 36 | * keycode or zero if it was not found */ 37 | static uint32_t convert_keysym(uint32_t key); 38 | /* convert a hardware keycode to a keysym (using level = 0 and 39 | * group = 0) for the purpose of displaying it to the user */ 40 | static uint32_t convert_keycode(uint32_t code); 41 | 42 | static void init(); 43 | 44 | static unsigned int keycode_errors; 45 | protected: 46 | static GdkKeymap* keymap; 47 | }; 48 | 49 | 50 | #endif 51 | 52 | -------------------------------------------------------------------------------- /src/gesture.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2009, Thomas Jaeger 3 | * Copyright (c) 2020-2023, Daniel Kondor 4 | * 5 | * Permission to use, copy, modify, and/or distribute this software for any 6 | * purpose with or without fee is hereby granted, provided that the above 7 | * copyright notice and this permission notice appear in all copies. 8 | * 9 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 12 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 14 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 15 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | */ 17 | #include "gesture.h" 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | BOOST_CLASS_EXPORT(Stroke) 27 | 28 | Stroke::Stroke(const PreStroke &ps) : stroke(nullptr, stroke_deleter()) { 29 | if (ps.size() >= 2) { 30 | stroke_t *s = stroke_alloc(ps.size()); 31 | for (const auto& t : ps) 32 | stroke_add_point(s, t.x, t.y); 33 | stroke_finish(s); 34 | stroke.reset(s); 35 | } 36 | } 37 | 38 | int Stroke::compare(const Stroke& a, const Stroke& b, double &score) { 39 | score = 0.0; 40 | if (!a.stroke || !b.stroke) { 41 | if (!a.stroke && !b.stroke) { 42 | score = 1.0; 43 | return 1; 44 | } 45 | return -1; 46 | } 47 | double cost = stroke_compare(a.stroke.get(), b.stroke.get(), nullptr, nullptr); 48 | if (cost >= stroke_infinity) 49 | return -1; 50 | score = std::max(1.0 - 2.5*cost, 0.0); 51 | return score > 0.7; 52 | } 53 | 54 | /* 55 | Stroke Stroke::trefoil() { 56 | PreStroke s; 57 | const unsigned int n = 40; 58 | s.reserve(n); 59 | for (unsigned int i = 0; i<=n; i++) { 60 | double phi = M_PI*(-4.0*i/n)-2.7; 61 | double r = exp(1.0 + sin(6.0*M_PI*i/n)) + 2.0; 62 | s.add(Point{(float)(r*cos(phi)), (float)(r*sin(phi))}); 63 | } 64 | return Stroke(s); 65 | } 66 | */ 67 | 68 | -------------------------------------------------------------------------------- /src/gesture.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2009, Thomas Jaeger 3 | * Copyright (c) 2020-2023, Daniel Kondor 4 | * 5 | * Permission to use, copy, modify, and/or distribute this software for any 6 | * purpose with or without fee is hereby granted, provided that the above 7 | * copyright notice and this permission notice appear in all copies. 8 | * 9 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 12 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 14 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 15 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | */ 17 | #ifndef __GESTURE_H__ 18 | #define __GESTURE_H__ 19 | 20 | #include "stroke.h" 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | #define STROKE_SIZE 64 30 | 31 | class Stroke { 32 | friend class boost::serialization::access; 33 | public: 34 | struct Point { 35 | double x; 36 | double y; 37 | Point operator+(const Point &p) { 38 | Point sum = { x + p.x, y + p.y }; 39 | return sum; 40 | } 41 | Point operator-(const Point &p) { 42 | Point sum = { x - p.x, y - p.y }; 43 | return sum; 44 | } 45 | Point operator*(const double a) { 46 | Point product = { x * a, y * a }; 47 | return product; 48 | } 49 | template void serialize(Archive & ar, const unsigned int version) { 50 | ar & x; ar & y; 51 | if (version == 0) { 52 | double time; 53 | ar & time; 54 | } 55 | } 56 | }; 57 | 58 | using PreStroke = std::vector; 59 | 60 | private: 61 | BOOST_SERIALIZATION_SPLIT_MEMBER() 62 | template void load(Archive & ar, const unsigned int version) { 63 | if(version >= 6) { 64 | unsigned int n; 65 | ar & n; 66 | if(n) { 67 | stroke_t* s = stroke_alloc(n); 68 | for(unsigned int i = 0; i < n; i++) { 69 | double x, y; 70 | ar & x; 71 | ar & y; 72 | stroke_add_point(s, x, y); 73 | } 74 | stroke_finish(s); 75 | stroke.reset(s); 76 | } 77 | return; 78 | } 79 | 80 | std::vector ps; 81 | ar & ps; 82 | if (ps.size()) { 83 | stroke_t *s = stroke_alloc(ps.size()); 84 | for (std::vector::iterator i = ps.begin(); i != ps.end(); ++i) 85 | stroke_add_point(s, i->x, i->y); 86 | stroke_finish(s); 87 | stroke.reset(s); 88 | } 89 | if (version == 0) return; 90 | 91 | int trigger; 92 | int button; 93 | unsigned int modifiers; 94 | bool timeout; 95 | 96 | ar & button; 97 | if (version >= 2) ar & trigger; 98 | if (version < 3) return; 99 | ar & timeout; 100 | if (version < 5) return; 101 | ar & modifiers; 102 | 103 | } 104 | template void save(Archive & ar, __attribute__((unused)) unsigned int version) const { 105 | unsigned int n = size(); 106 | ar & n; 107 | for(unsigned int i = 0; i < n; i++) { 108 | Point p = points(i); 109 | ar & p.x; 110 | ar & p.y; 111 | } 112 | } 113 | 114 | struct stroke_deleter { 115 | void operator()(stroke_t* s) const { stroke_free(s); } 116 | }; 117 | 118 | public: 119 | std::unique_ptr stroke; 120 | 121 | Stroke() : stroke(nullptr, stroke_deleter()) { } 122 | Stroke(const PreStroke &s); 123 | Stroke clone() const { Stroke s; if(stroke) s.stroke.reset(stroke_copy(stroke.get())); return s; } 124 | 125 | static Stroke trefoil(); 126 | static int compare(const Stroke&, const Stroke&, double &); 127 | 128 | unsigned int size() const { return stroke ? stroke_get_size(stroke.get()) : 0; } 129 | bool trivial() const { return size() == 0 ; } 130 | Point points(int n) const { Point p; stroke_get_point(stroke.get(), n, &p.x, &p.y); return p; } 131 | double time(int n) const { return stroke_get_time(stroke.get(), n); } 132 | }; 133 | BOOST_CLASS_VERSION(Stroke, 6) 134 | BOOST_CLASS_VERSION(Stroke::Point, 1) 135 | 136 | #endif 137 | -------------------------------------------------------------------------------- /src/input_events.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * input_events.cpp -- interface to generate input events in Wayfire 3 | * 4 | * Copyright 2020-2024 Daniel Kondor 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 13 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 15 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 16 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | * 18 | */ 19 | 20 | #include "input_events.hpp" 21 | 22 | #include 23 | 24 | extern "C" { 25 | #define static 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #undef static 33 | } 34 | 35 | #include 36 | #include 37 | 38 | static const struct wlr_pointer_impl ws_headless_pointer_impl = { 39 | .name = "wstroke-pointer", 40 | }; 41 | 42 | static const struct wlr_keyboard_impl ws_headless_keyboard_impl = { 43 | .name = "wstroke-keyboard", 44 | .led_update = nullptr 45 | }; 46 | 47 | 48 | void input_headless::init() { 49 | auto& core = wf::compositor_core_t::get(); 50 | /* 1. create headless backend */ 51 | headless_backend = wlr_headless_backend_create(core.ev_loop); 52 | if(!headless_backend) { 53 | LOGE("Cannot create headless wlroots backend!"); 54 | return; 55 | } 56 | /* 2. add to the core backend */ 57 | if(!wlr_multi_backend_add(core.backend, headless_backend)) { 58 | LOGE("Cannot add headless wlroots backend!"); 59 | wlr_backend_destroy(headless_backend); 60 | headless_backend = nullptr; 61 | return; 62 | } 63 | /* 3. start the new headless backend */ 64 | start_backend(); 65 | 66 | /* 4. create the new input device */ 67 | input_pointer = (struct wlr_pointer*)calloc(1, sizeof(struct wlr_pointer)); 68 | if(!input_pointer) { 69 | LOGE("Cannot create pointer device!"); 70 | fini(); 71 | return; 72 | } 73 | wlr_pointer_init(input_pointer, &ws_headless_pointer_impl, ws_headless_pointer_impl.name); 74 | 75 | 76 | input_keyboard = (struct wlr_keyboard*)calloc(1, sizeof(struct wlr_keyboard)); 77 | if(!input_keyboard) { 78 | LOGE("Cannot create keyboard device!"); 79 | fini(); 80 | return; 81 | } 82 | wlr_keyboard_init(input_keyboard, &ws_headless_keyboard_impl, ws_headless_keyboard_impl.name); 83 | 84 | wl_signal_emit_mutable(&headless_backend->events.new_input, input_keyboard); 85 | wl_signal_emit_mutable(&headless_backend->events.new_input, input_pointer); 86 | } 87 | 88 | void input_headless::start_backend() { 89 | if(!wlr_backend_start(headless_backend)) { 90 | LOGE("Cannot start headless wlroots backend!"); 91 | fini(); 92 | } 93 | } 94 | 95 | void input_headless::fini() { 96 | if(input_pointer) { 97 | wlr_pointer_finish(input_pointer); 98 | free(input_pointer); 99 | input_pointer = nullptr; 100 | } 101 | if(input_keyboard) { 102 | wlr_keyboard_finish(input_keyboard); 103 | free(input_keyboard); 104 | input_keyboard = nullptr; 105 | } 106 | if(headless_backend) { 107 | auto& core = wf::compositor_core_t::get(); 108 | wlr_multi_backend_remove(core.backend, headless_backend); 109 | wlr_backend_destroy(headless_backend); 110 | headless_backend = nullptr; 111 | } 112 | } 113 | 114 | void input_headless::pointer_button(uint32_t time_msec, uint32_t button, enum WSTROKE_BUTTON_STATE state) { 115 | if(!(input_pointer && headless_backend)) { 116 | LOGW("No input device created!"); 117 | return; 118 | } 119 | LOGD("Emitting pointer button event"); 120 | wlr_pointer_button_event ev; 121 | ev.pointer = input_pointer; 122 | ev.button = button; 123 | ev.state = state; 124 | ev.time_msec = time_msec; 125 | wl_signal_emit(&(input_pointer->events.button), &ev); 126 | } 127 | 128 | void input_headless::pointer_scroll(uint32_t time_msec, double delta, enum WSTROKE_AXIS_ORIENTATION o) { 129 | if(!(input_pointer && headless_backend)) { 130 | LOGW("No input device created!"); 131 | return; 132 | } 133 | LOGD("Emitting pointer scroll event"); 134 | wlr_pointer_axis_event ev; 135 | ev.pointer = input_pointer; 136 | ev.time_msec = time_msec; 137 | ev.source = WL_POINTER_AXIS_SOURCE_CONTINUOUS; 138 | ev.orientation = o; 139 | ev.delta = delta; 140 | ev.delta_discrete = delta * WLR_POINTER_AXIS_DISCRETE_STEP;; 141 | wl_signal_emit(&(input_pointer->events.axis), &ev); 142 | } 143 | 144 | void input_headless::pointer_start_swipe(uint32_t time_msec, uint32_t fingers) { 145 | if(!(input_pointer && headless_backend)) { 146 | LOGW("No input device created!"); 147 | return; 148 | } 149 | LOGD("Emitting pointer swipe begin event"); 150 | wlr_pointer_swipe_begin_event ev; 151 | ev.pointer = input_pointer; 152 | ev.time_msec = time_msec; 153 | ev.fingers = fingers; 154 | wl_signal_emit(&(input_pointer->events.swipe_begin), &ev); 155 | } 156 | 157 | void input_headless::pointer_update_swipe(uint32_t time_msec, uint32_t fingers, double dx, double dy) { 158 | if(!(input_pointer && headless_backend)) { 159 | LOGW("No input device created!"); 160 | return; 161 | } 162 | LOGD("Emitting pointer swipe update event"); 163 | wlr_pointer_swipe_update_event ev; 164 | ev.pointer = input_pointer; 165 | ev.time_msec = time_msec; 166 | ev.fingers = fingers; 167 | ev.dx = dx; 168 | ev.dy = dy; 169 | wl_signal_emit(&(input_pointer->events.swipe_update), &ev); 170 | } 171 | 172 | void input_headless::pointer_end_swipe(uint32_t time_msec, bool cancelled) { 173 | if(!(input_pointer && headless_backend)) { 174 | LOGW("No input device created!"); 175 | return; 176 | } 177 | LOGD("Emitting pointer swipe end event"); 178 | wlr_pointer_swipe_end_event ev; 179 | ev.pointer = input_pointer; 180 | ev.time_msec = time_msec; 181 | ev.cancelled = cancelled; //!! note: conversion from C++ bool to C99/C23 bool !! 182 | wl_signal_emit(&(input_pointer->events.swipe_end), &ev); 183 | } 184 | 185 | void input_headless::pointer_start_pinch(uint32_t time_msec, uint32_t fingers) { 186 | if(!(input_pointer && headless_backend)) { 187 | LOGW("No input device created!"); 188 | return; 189 | } 190 | LOGD("Emitting pointer pinch begin event"); 191 | wlr_pointer_pinch_begin_event ev; 192 | ev.pointer = input_pointer; 193 | ev.time_msec = time_msec; 194 | ev.fingers = fingers; 195 | wl_signal_emit(&(input_pointer->events.pinch_begin), &ev); 196 | } 197 | 198 | void input_headless::pointer_update_pinch(uint32_t time_msec, uint32_t fingers, double dx, double dy, double scale, double rotation) { 199 | if(!(input_pointer && headless_backend)) { 200 | LOGW("No input device created!"); 201 | return; 202 | } 203 | LOGD("Emitting pointer pinch update event"); 204 | wlr_pointer_pinch_update_event ev; 205 | ev.pointer = input_pointer; 206 | ev.time_msec = time_msec; 207 | ev.fingers = fingers; 208 | ev.dx = dx; 209 | ev.dy = dy; 210 | ev.scale = scale; 211 | ev.rotation = rotation; 212 | wl_signal_emit(&(input_pointer->events.pinch_update), &ev); 213 | } 214 | 215 | void input_headless::pointer_end_pinch(uint32_t time_msec, bool cancelled) { 216 | if(!(input_pointer && headless_backend)) { 217 | LOGW("No input device created!"); 218 | return; 219 | } 220 | LOGD("Emitting pointer pinch end event"); 221 | wlr_pointer_pinch_end_event ev; 222 | ev.pointer = input_pointer; 223 | ev.time_msec = time_msec; 224 | ev.cancelled = cancelled; //!! note: conversion from C++ bool to C99/C23 bool !! 225 | wl_signal_emit(&(input_pointer->events.pinch_end), &ev); 226 | } 227 | 228 | void input_headless::keyboard_key(uint32_t time_msec, uint32_t key, enum wl_keyboard_key_state state) { 229 | if(!(input_keyboard && headless_backend)) { 230 | LOGW("No input device created!"); 231 | return; 232 | } 233 | LOGD("Emitting keyboard event ", key, state == WL_KEYBOARD_KEY_STATE_PRESSED ? ", pressed" : ", released"); 234 | wlr_keyboard_key_event ev; 235 | ev.keycode = key; 236 | ev.state = (decltype(ev.state))state; 237 | ev.update_state = true; 238 | ev.time_msec = time_msec; 239 | wl_signal_emit(&(input_keyboard->events.key), &ev); 240 | } 241 | 242 | void input_headless::keyboard_mods(uint32_t mods_depressed, uint32_t mods_latched, uint32_t mods_locked) { 243 | if(!(input_keyboard && headless_backend)) { 244 | LOGW("No input device created!"); 245 | return; 246 | } 247 | LOGD("Changing keyboard modifiers"); 248 | wlr_keyboard_notify_modifiers(input_keyboard, mods_depressed, mods_latched, mods_locked, 0); 249 | /* struct wlr_seat* seat = wf::get_core().get_current_seat(); -- does not work: combining with the "real" keyboard 250 | struct wlr_keyboard_modifiers modifiers; 251 | modifiers.depressed = mods_depressed; 252 | modifiers.latched = mods_latched; 253 | modifiers.locked = mods_locked; 254 | modifiers.group = 0; // ?? 255 | wlr_seat_keyboard_notify_modifiers(seat, &modifiers); */ 256 | } 257 | 258 | -------------------------------------------------------------------------------- /src/input_events.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * input_events.hpp -- interface to generate input events in Wayfire 3 | * 4 | * Copyright 2020-2024 Daniel Kondor 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 13 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 15 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 16 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | * 18 | */ 19 | 20 | 21 | #ifndef INPUT_EVENTS_HPP 22 | #define INPUT_EVENTS_HPP 23 | 24 | extern "C" { 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | } 31 | 32 | #define WSTROKE_AXIS_HORIZONTAL wl_pointer_axis::WL_POINTER_AXIS_HORIZONTAL_SCROLL 33 | #define WSTROKE_AXIS_VERTICAL wl_pointer_axis::WL_POINTER_AXIS_VERTICAL_SCROLL 34 | #define WSTROKE_BUTTON_STATE wl_pointer_button_state 35 | #define WSTROKE_AXIS_ORIENTATION wl_pointer_axis 36 | 37 | class input_headless { 38 | public: 39 | /* init internals, create headless wlroots backend with fake 40 | * pointer and add it to the backends managed by Wayfire */ 41 | void init(); 42 | /* remove the headless backend created by this class and 43 | * delete it */ 44 | void fini(); 45 | /* emit a mouse button event */ 46 | void pointer_button(uint32_t time_msec, uint32_t button, enum WSTROKE_BUTTON_STATE state); 47 | /* emit a pointer scroll event */ 48 | void pointer_scroll(uint32_t time_msec, double delta, enum WSTROKE_AXIS_ORIENTATION o); 49 | /* emit a sequence of swipe events */ 50 | void pointer_start_swipe(uint32_t time_msec, uint32_t fingers); 51 | void pointer_update_swipe(uint32_t time_msec, uint32_t fingers, double dx, double dy); 52 | void pointer_end_swipe(uint32_t time_msec, bool cancelled); 53 | /* emit a sequence of pinch events */ 54 | void pointer_start_pinch(uint32_t time_msec, uint32_t fingers); 55 | void pointer_update_pinch(uint32_t time_msec, uint32_t fingers, double dx, double dy, double scale, double rotation); 56 | void pointer_end_pinch(uint32_t time_msec, bool cancelled); 57 | /* emit a keyboard event */ 58 | void keyboard_key(uint32_t time_msec, uint32_t key, enum wl_keyboard_key_state state); 59 | /* modify the modifier state of the keyboard */ 60 | void keyboard_mods(uint32_t mods_depressed, uint32_t mods_latched, uint32_t mods_locked); 61 | /* return if a pointer event was generated by us */ 62 | bool is_own_event_btn(const wlr_pointer_button_event* ev) const { return ev && (ev->pointer == input_pointer); } 63 | 64 | ~input_headless() { fini(); } 65 | 66 | protected: 67 | void start_backend(); 68 | struct wlr_backend* headless_backend = nullptr; 69 | struct wlr_pointer* input_pointer = nullptr; 70 | struct wlr_keyboard* input_keyboard = nullptr; 71 | }; 72 | 73 | #endif 74 | 75 | -------------------------------------------------------------------------------- /src/input_inhibitor.vapi: -------------------------------------------------------------------------------- 1 | 2 | [CCode (cheader_filename = "input_inhibitor.h")] 3 | namespace Inhibitor { 4 | [CCode (cname = "input_inhibitor_grab")] 5 | bool grab(); 6 | [CCode (cname = "input_inhibitor_ungrab")] 7 | void ungrab(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * main.cc 3 | * 4 | * Copyright 2020-2023 Daniel Kondor 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 13 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 15 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 16 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | * 18 | */ 19 | 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include "actions.h" 30 | #include "actiondb.h" 31 | #include "ecres.h" 32 | #include "convert_keycodes.h" 33 | #include "input_inhibitor.h" 34 | #include "config.h" 35 | 36 | static void error_dialog(const Glib::ustring &text) { 37 | Gtk::MessageDialog dialog(text, false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); 38 | dialog.show(); 39 | dialog.run(); 40 | } 41 | 42 | /* Display a dialog with an error text if the configuration cannot be read. */ 43 | bool config_error_dialog(const Glib::ustring& fn, const Glib::ustring& err, Gtk::Builder* widgets) { 44 | std::unique_ptr dialog; 45 | { 46 | Gtk::Dialog* tmp; 47 | widgets->get_widget("dialog_config_error", tmp); 48 | dialog.reset(tmp); 49 | } 50 | 51 | Glib::ustring msg = Glib::ustring::compose(_("The gesture configuration file \"%1\" exists but cannot be read. The following error was encountered:"), fn); 52 | Gtk::Label* label; 53 | Gtk::TextView* tv; 54 | widgets->get_widget("label_config_error", label); 55 | widgets->get_widget("textview_config_error", tv); 56 | tv->get_buffer()->set_text(err); 57 | label->set_text(msg); 58 | dialog->show(); 59 | return (dialog->run() == 1); // note: response == 1 means that user clicked on the "Overwrite" button 60 | } 61 | 62 | 63 | void startup(Gtk::Application* app, Actions** p_actions) 64 | { 65 | char* xdg_config = getenv("XDG_CONFIG_HOME"); 66 | std::string home_dir = getenv("HOME"); 67 | std::string old_config_dir = home_dir + "/.easystroke/"; 68 | std::string config_dir = xdg_config ? std::string(xdg_config) + "/wstroke/" : 69 | home_dir + "/.config/wstroke/"; 70 | 71 | // ensure that config dir exists 72 | std::error_code ec; 73 | if(std::filesystem::exists(config_dir, ec)) { 74 | if(!std::filesystem::is_directory(config_dir, ec)) { 75 | error_dialog(Glib::ustring::compose(_( "Path for config files (%1) is not a directory! " 76 | "Cannot store configuration. " 77 | "You can change the configuration directory " 78 | "using the XDG_CONFIG_HOME environment variable." 79 | ), config_dir)); 80 | return; // note: not creating a main window will result in automatically exiting 81 | } 82 | } 83 | else { 84 | if(!std::filesystem::create_directories(config_dir, ec)) { 85 | error_dialog(Glib::ustring::compose(_( "Cannot create configuration directory \"%1\"! " 86 | "Cannot store the configuration. " 87 | "You can change the configuration directory " 88 | "using the XDG_CONFIG_HOME environment variable." 89 | ), config_dir)); 90 | return; // note: not creating a main window will result in automatically exiting 91 | } 92 | } 93 | 94 | Glib::RefPtr widgets = Gtk::Builder::create_from_resource("/easystroke/gui.glade"); 95 | auto actions = new Actions(config_dir, widgets); 96 | *p_actions = actions; 97 | ActionDB& actions_db = actions->actions; 98 | KeyCodes::init(); 99 | bool config_read; 100 | std::string config_err_msg; 101 | std::string easystroke_convert_msg; 102 | std::string keycode_err_msg; 103 | 104 | for(const char* const * x = ActionDB::wstroke_actions_versions; *x; ++x) { 105 | std::string fn = config_dir + *x; 106 | try { 107 | config_read = actions_db.read(fn); 108 | } 109 | catch(std::exception& e) { 110 | fprintf(stderr, "%s\n", e.what()); 111 | config_read = false; 112 | actions_db.clear(); 113 | 114 | if(x == ActionDB::wstroke_actions_versions) { 115 | /* In this case, the error is with reading the current config 116 | * (which would be overwritten by us). Signal an error to the user */ 117 | if(!config_error_dialog(fn, e.what(), widgets.get())) return; 118 | 119 | /* move the configuration file -- try to assign a new filename 120 | * in a naive way (we assume that there is no gain from TOCTOU 121 | * attacks here :) */ 122 | std::string new_fn = fn; 123 | new_fn += ".bak"; 124 | if(std::filesystem::exists(new_fn, ec)) { 125 | std::default_random_engine rng(time(0)); 126 | std::uniform_int_distribution dd(1, 999999); 127 | new_fn += "-"; 128 | while(true) { 129 | std::string tmp; 130 | tmp = new_fn + std::to_string(dd(rng)); 131 | if(!std::filesystem::exists(tmp, ec)) { 132 | new_fn = tmp; 133 | break; 134 | } 135 | } 136 | } 137 | rename(fn.c_str(), new_fn.c_str()); 138 | fprintf(stderr, "Moved unreadable config file to new location: %s\n", new_fn.c_str()); 139 | config_err_msg = "Created a backup of the previous, unreadable config file here:\n" + new_fn; 140 | } 141 | } 142 | if(config_read) break; 143 | } 144 | if(!config_read) { 145 | if(std::filesystem::exists(old_config_dir, ec) && std::filesystem::is_directory(old_config_dir, ec)) { 146 | KeyCodes::keycode_errors = 0; 147 | for(const char* const * x = ActionDB::easystroke_actions_versions; *x; ++x) { 148 | std::string fn = old_config_dir + *x; 149 | try { 150 | config_read = actions_db.read(fn); 151 | } 152 | catch(std::exception& e) { 153 | fprintf(stderr, "%s\n", e.what()); 154 | config_read = false; 155 | actions_db.clear(); 156 | } 157 | if(config_read) { 158 | easystroke_convert_msg = "Imported gestures from Easystroke's configuration:\n" + fn; 159 | easystroke_convert_msg += "\nPlease check that all actions were interpreted correctly."; 160 | break; 161 | } 162 | } 163 | } 164 | if(!config_read) { 165 | try { 166 | config_read = actions_db.read(std::string(DATA_DIR) + "/" + ActionDB::wstroke_actions_versions[0]); 167 | } 168 | catch(std::exception& e) { 169 | fprintf(stderr, "%s\n", e.what()); 170 | } 171 | } 172 | } 173 | if(KeyCodes::keycode_errors) keycode_err_msg = _("Could not convert some keycodes. " 174 | "Some Key actions have missing values"); 175 | 176 | Gtk::Dialog* d = nullptr; 177 | if(!(keycode_err_msg.empty() && easystroke_convert_msg.empty() && config_err_msg.empty())) { 178 | std::string text; 179 | if(!config_err_msg.empty()) text += (config_err_msg + "\n\n"); 180 | if(!easystroke_convert_msg.empty()) text += (easystroke_convert_msg + "\n\n"); 181 | if(!keycode_err_msg.empty()) text += (keycode_err_msg + "\n\n"); 182 | d = new Gtk::MessageDialog(text, false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); 183 | } 184 | 185 | if(!input_inhibitor_init()) 186 | fprintf(stderr, _("Could not initialize keyboard grabber interface. Assigning key combinations might not work.\n")); 187 | 188 | actions->startup(app, d); 189 | } 190 | 191 | int main(int argc, char **argv) { 192 | Actions* actions = nullptr; 193 | auto app = Gtk::Application::create(argc, argv, "org.wstroke.config"); 194 | app->signal_startup().connect([&app, &actions]() { startup(app.get(), &actions); }); 195 | app->signal_activate().connect([&actions]() { if(actions) actions->get_main_win()->present(); }); 196 | int ret = app->run(); 197 | if(actions) { 198 | actions->exit(); 199 | delete actions; 200 | } 201 | return ret; 202 | } 203 | 204 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | # resources (GUI layout) 2 | econf_res = gnome.compile_resources( 3 | 'ecres', 'resources.xml', 4 | source_dir: 'data', 5 | c_name: 'econf' 6 | ) 7 | 8 | conf_data = configuration_data() 9 | conf_data.set('DATA_DIR', join_paths(datadir, 'wstroke')) 10 | configure_file(input: 'config.h.in', 11 | output: 'config.h', 12 | install: false, 13 | configuration: conf_data) 14 | 15 | # note: this is code generated by Vala, compile it separately to 16 | # silence warnings 17 | cellib = static_library('cellib', 'cellrenderertextish.vala', 18 | vala_header: 'cellrenderertextish.h', 19 | dependencies: [glib, gobject, gtk, input_inhibitor_dep]) 20 | 21 | wconf_sources = ['main.cc', 'actiondb_config.cc', 'actiondb.cc', 'actions.cc', 22 | 'appchooser.cc', 'gesture.cc', 'stroke_draw.cc', 'stroke.c', 23 | 'convert_keycodes.cc', 'stroke_drawing_area.cpp', econf_res] 24 | wconf = executable('wstroke-config', wconf_sources, 25 | dependencies: [gtkmm, gdkmm, wlroots_headers, boost, protos, input_inhibitor_dep, toplevel_grabber_dep], 26 | install: true, 27 | cpp_args: ['-DACTIONDB_CONVERT_CODES', '-DWLR_USE_UNSTABLE'], 28 | link_with: cellib) 29 | 30 | 31 | wslib_sources = ['easystroke_gestures.cpp', 'input_events.cpp', 'actiondb.cc', 'actiondb_plugin.cc', 'gesture.cc', 'stroke.c'] 32 | wslib = shared_module('wstroke', wslib_sources, 33 | dependencies: [wayfire, wlroots, wlserver, boost, glibmm], 34 | install: true, 35 | install_dir: wayfire.get_variable(pkgconfig: 'plugindir'), 36 | cpp_args: ['-Wno-unused-parameter', '-Wno-format-security','-DWAYFIRE_PLUGIN', '-DWLR_USE_UNSTABLE'], 37 | link_args: '-rdynamic') 38 | 39 | -------------------------------------------------------------------------------- /src/resources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | gui.glade 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/stroke.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2009, Thomas Jaeger 3 | * Copyright (c) 2023, Daniel Kondor 4 | * 5 | * Permission to use, copy, modify, and/or distribute this software for any 6 | * purpose with or without fee is hereby granted, provided that the above 7 | * copyright notice and this permission notice appear in all copies. 8 | * 9 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 12 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 14 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 15 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | */ 17 | 18 | #define _GNU_SOURCE 19 | 20 | #include "stroke.h" 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | const double stroke_infinity = 0.2; 28 | #define EPS 0.000001 29 | 30 | struct point { 31 | double x; 32 | double y; 33 | double t; 34 | double dt; 35 | double alpha; 36 | }; 37 | 38 | struct _stroke_t { 39 | int n; 40 | int capacity; 41 | struct point *p; 42 | }; 43 | 44 | stroke_t *stroke_alloc(int n) { 45 | assert(n > 0); 46 | stroke_t *s = malloc(sizeof(stroke_t)); 47 | s->n = 0; 48 | s->capacity = n; 49 | s->p = calloc(n, sizeof(struct point)); 50 | return s; 51 | } 52 | 53 | void stroke_add_point(stroke_t *s, double x, double y) { 54 | assert(s->capacity > s->n); 55 | s->p[s->n].x = x; 56 | s->p[s->n].y = y; 57 | s->n++; 58 | } 59 | 60 | static inline double angle_difference(double alpha, double beta) { 61 | double d = alpha - beta; 62 | if (d < -1.0) 63 | d += 2.0; 64 | else if (d > 1.0) 65 | d -= 2.0; 66 | return d; 67 | } 68 | 69 | void stroke_finish(stroke_t *s) { 70 | assert(s->capacity > 0); 71 | s->capacity = -1; 72 | 73 | int n = s->n - 1; 74 | double total = 0.0; 75 | s->p[0].t = 0.0; 76 | for (int i = 0; i < n; i++) { 77 | total += hypot(s->p[i+1].x - s->p[i].x, s->p[i+1].y - s->p[i].y); 78 | s->p[i+1].t = total; 79 | } 80 | for (int i = 0; i <= n; i++) 81 | s->p[i].t /= total; 82 | double minX = s->p[0].x, minY = s->p[0].y, maxX = minX, maxY = minY; 83 | for (int i = 1; i <= n; i++) { 84 | if (s->p[i].x < minX) minX = s->p[i].x; 85 | if (s->p[i].x > maxX) maxX = s->p[i].x; 86 | if (s->p[i].y < minY) minY = s->p[i].y; 87 | if (s->p[i].y > maxY) maxY = s->p[i].y; 88 | } 89 | double scaleX = maxX - minX; 90 | double scaleY = maxY - minY; 91 | double scale = (scaleX > scaleY) ? scaleX : scaleY; 92 | if (scale < 0.001) scale = 1; 93 | for (int i = 0; i <= n; i++) { 94 | s->p[i].x = (s->p[i].x-(minX+maxX)/2)/scale + 0.5; 95 | s->p[i].y = (s->p[i].y-(minY+maxY)/2)/scale + 0.5; 96 | } 97 | 98 | for (int i = 0; i < n; i++) { 99 | s->p[i].dt = s->p[i+1].t - s->p[i].t; 100 | s->p[i].alpha = atan2(s->p[i+1].y - s->p[i].y, s->p[i+1].x - s->p[i].x)/M_PI; 101 | } 102 | 103 | } 104 | 105 | void stroke_free(stroke_t *s) { 106 | if (s) 107 | free(s->p); 108 | free(s); 109 | } 110 | 111 | stroke_t *stroke_copy(const stroke_t *stroke) { 112 | if(!stroke) return NULL; 113 | stroke_t *s = malloc(sizeof(stroke_t)); 114 | if(!s) return NULL; 115 | s->p = calloc(stroke->n, sizeof(struct point)); 116 | if(!(s->p)) { 117 | free(s); 118 | return NULL; 119 | } 120 | s->n = stroke->n; 121 | s->capacity = s->n; 122 | memcpy(s->p, stroke->p, s->n * sizeof(struct point)); 123 | return s; 124 | } 125 | 126 | 127 | int stroke_get_size(const stroke_t *s) { return s->n; } 128 | 129 | void stroke_get_point(const stroke_t *s, int n, double *x, double *y) { 130 | assert(n < s->n); 131 | if (x) 132 | *x = s->p[n].x; 133 | if (y) 134 | *y = s->p[n].y; 135 | } 136 | 137 | double stroke_get_time(const stroke_t *s, int n) { 138 | assert(n < s->n); 139 | return s->p[n].t; 140 | } 141 | 142 | double stroke_get_angle(const stroke_t *s, int n) { 143 | assert(n+1 < s->n); 144 | return s->p[n].alpha; 145 | } 146 | 147 | inline static double sqr(double x) { return x*x; } 148 | 149 | double stroke_angle_difference(const stroke_t *a, const stroke_t *b, int i, int j) { 150 | return fabs(angle_difference(stroke_get_angle(a, i), stroke_get_angle(b, j))); 151 | } 152 | 153 | static inline void step(const stroke_t *a, 154 | const stroke_t *b, 155 | const int N, 156 | double *dist, 157 | int *prev_x, 158 | int *prev_y, 159 | const int x, 160 | const int y, 161 | const double tx, 162 | const double ty, 163 | int *k, 164 | const int x2, 165 | const int y2) 166 | { 167 | double dtx = a->p[x2].t - tx; 168 | double dty = b->p[y2].t - ty; 169 | if (dtx >= dty * 2.2 || dty >= dtx * 2.2 || dtx < EPS || dty < EPS) 170 | return; 171 | (*k)++; 172 | 173 | double d = 0.0; 174 | int i = x, j = y; 175 | double next_tx = (a->p[i+1].t - tx) / dtx; 176 | double next_ty = (b->p[j+1].t - ty) / dty; 177 | double cur_t = 0.0; 178 | 179 | for (;;) { 180 | double ad = sqr(angle_difference(a->p[i].alpha, b->p[j].alpha)); 181 | double next_t = next_tx < next_ty ? next_tx : next_ty; 182 | bool done = next_t >= 1.0 - EPS; 183 | if (done) 184 | next_t = 1.0; 185 | d += (next_t - cur_t)*ad; 186 | if (done) 187 | break; 188 | cur_t = next_t; 189 | if (next_tx < next_ty) 190 | next_tx = (a->p[++i+1].t - tx) / dtx; 191 | else 192 | next_ty = (b->p[++j+1].t - ty) / dty; 193 | } 194 | double new_dist = dist[x*N+y] + d * (dtx + dty); 195 | if (new_dist != new_dist) abort(); 196 | 197 | if (new_dist >= dist[x2*N+y2]) 198 | return; 199 | 200 | prev_x[x2*N+y2] = x; 201 | prev_y[x2*N+y2] = y; 202 | dist[x2*N+y2] = new_dist; 203 | } 204 | 205 | /* To compare two gestures, we use dynamic programming to minimize (an 206 | * approximation) of the integral over square of the angle difference among 207 | * (roughly) all reparametrizations whose slope is always between 1/2 and 2. 208 | */ 209 | double stroke_compare(const stroke_t *a, const stroke_t *b, int *path_x, int *path_y) { 210 | const int M = a->n; 211 | const int N = b->n; 212 | const int m = M - 1; 213 | const int n = N - 1; 214 | 215 | double* dist = malloc(M * N * sizeof(double)); 216 | int* prev_x = malloc(M * N * sizeof(int)); 217 | int* prev_y = malloc(M * N * sizeof(int)); 218 | for (int i = 0; i < m; i++) 219 | for (int j = 0; j < n; j++) 220 | dist[i*N+j] = stroke_infinity; 221 | dist[M*N-1] = stroke_infinity; 222 | dist[0] = 0.0; 223 | 224 | for (int x = 0; x < m; x++) { 225 | for (int y = 0; y < n; y++) { 226 | if (dist[x*N+y] >= stroke_infinity) 227 | continue; 228 | double tx = a->p[x].t; 229 | double ty = b->p[y].t; 230 | int max_x = x; 231 | int max_y = y; 232 | int k = 0; 233 | 234 | while (k < 4) { 235 | if (a->p[max_x+1].t - tx > b->p[max_y+1].t - ty) { 236 | max_y++; 237 | if (max_y == n) { 238 | step(a, b, N, dist, prev_x, prev_y, x, y, tx, ty, &k, m, n); 239 | break; 240 | } 241 | for (int x2 = x+1; x2 <= max_x; x2++) 242 | step(a, b, N, dist, prev_x, prev_y, x, y, tx, ty, &k, x2, max_y); 243 | } else { 244 | max_x++; 245 | if (max_x == m) { 246 | step(a, b, N, dist, prev_x, prev_y, x, y, tx, ty, &k, m, n); 247 | break; 248 | } 249 | for (int y2 = y+1; y2 <= max_y; y2++) 250 | step(a, b, N, dist, prev_x, prev_y, x, y, tx, ty, &k, max_x, y2); 251 | } 252 | } 253 | } 254 | } 255 | double cost = dist[M*N-1]; 256 | if (path_x && path_y) { 257 | if (cost < stroke_infinity) { 258 | int x = m; 259 | int y = n; 260 | int k = 0; 261 | while (x || y) { 262 | int old_x = x; 263 | x = prev_x[x*N+y]; 264 | y = prev_y[old_x*N+y]; 265 | path_x[k] = x; 266 | path_y[k] = y; 267 | k++; 268 | } 269 | } else { 270 | path_x[0] = 0; 271 | path_y[0] = 0; 272 | } 273 | } 274 | 275 | free(prev_y); 276 | free(prev_x); 277 | free(dist); 278 | 279 | return cost; 280 | } 281 | -------------------------------------------------------------------------------- /src/stroke.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2009, Thomas Jaeger 3 | * Copyright (c) 2023, Daniel Kondor 4 | * 5 | * Permission to use, copy, modify, and/or distribute this software for any 6 | * purpose with or without fee is hereby granted, provided that the above 7 | * copyright notice and this permission notice appear in all copies. 8 | * 9 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 12 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 14 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 15 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | */ 17 | #ifndef __STROKE_H__ 18 | #define __STROKE_H__ 19 | 20 | #ifdef __cplusplus 21 | extern "C" { 22 | #endif 23 | 24 | struct _stroke_t; 25 | 26 | typedef struct _stroke_t stroke_t; 27 | 28 | stroke_t *stroke_alloc(int n); 29 | void stroke_add_point(stroke_t *stroke, double x, double y); 30 | void stroke_finish(stroke_t *stroke); 31 | void stroke_free(stroke_t *stroke); 32 | stroke_t *stroke_copy(const stroke_t *stroke); 33 | 34 | int stroke_get_size(const stroke_t *stroke); 35 | void stroke_get_point(const stroke_t *stroke, int n, double *x, double *y); 36 | double stroke_get_time(const stroke_t *stroke, int n); 37 | double stroke_get_angle(const stroke_t *stroke, int n); 38 | double stroke_angle_difference(const stroke_t *a, const stroke_t *b, int i, int j); 39 | 40 | double stroke_compare(const stroke_t *a, const stroke_t *b, int *path_x, int *path_y); 41 | 42 | extern const double stroke_infinity; 43 | 44 | #ifdef __cplusplus 45 | } 46 | #endif 47 | #endif 48 | -------------------------------------------------------------------------------- /src/stroke_draw.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * stroke_draw.cc 3 | * 4 | * Copyright (c) 2008-2009, Thomas Jaeger 5 | * Copyright (c) 2020-2023 Daniel Kondor 6 | * 7 | * Permission to use, copy, modify, and/or distribute this software for any 8 | * purpose with or without fee is hereby granted, provided that the above 9 | * copyright notice and this permission notice appear in all copies. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 12 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 13 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 14 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 15 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 16 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 17 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 18 | * 19 | */ 20 | 21 | 22 | #include "stroke_draw.h" 23 | #include 24 | 25 | Glib::RefPtr StrokeDrawer::pbEmpty; 26 | 27 | Glib::RefPtr StrokeDrawer::drawEmpty(int size) { 28 | if (size != STROKE_SIZE) 29 | return drawEmpty_(size); 30 | if (pbEmpty) 31 | return pbEmpty; 32 | pbEmpty = drawEmpty_(size); 33 | return pbEmpty; 34 | } 35 | 36 | 37 | Glib::RefPtr StrokeDrawer::draw(const Stroke* stroke, int size, double width) { 38 | Glib::RefPtr pb = drawEmpty_(size); 39 | int w = size; 40 | int h = size; 41 | int stride = pb->get_rowstride(); 42 | guint8 *row = pb->get_pixels(); 43 | // This is all pretty messed up 44 | // http://www.archivum.info/gtkmm-list@gnome.org/2007-05/msg00112.html 45 | Cairo::RefPtr surface = Cairo::ImageSurface::create(row, Cairo::FORMAT_ARGB32, w, h, stride); 46 | draw(stroke, surface, 0, 0, pb->get_width(), size, width); 47 | for (int i = 0; i < w; i++) { 48 | guint8 *px = row; 49 | for (int j = 0; j < h; j++) { 50 | guint8 a = px[3]; 51 | guint8 r = px[2]; 52 | guint8 g = px[1]; 53 | guint8 b = px[0]; 54 | if (a) { 55 | px[0] = ((((guint)r) << 8) - r) / a; 56 | px[1] = ((((guint)g) << 8) - g) / a; 57 | px[2] = ((((guint)b) << 8) - b) / a; 58 | } 59 | px += 4; 60 | } 61 | row += stride; 62 | } 63 | return pb; 64 | } 65 | 66 | Glib::RefPtr StrokeDrawer::drawEmpty_(int size) { 67 | Glib::RefPtr pb = Gdk::Pixbuf::create(Gdk::COLORSPACE_RGB,true,8,size,size); 68 | pb->fill(0x00000000); 69 | return pb; 70 | } 71 | 72 | 73 | void StrokeDrawer::draw(const Stroke* stroke, Cairo::RefPtr surface, int x, int y, int w, int h, double width) { 74 | const Cairo::RefPtr ctx = Cairo::Context::create (surface); 75 | x += width; y += width; w -= 2*width; h -= 2*width; 76 | ctx->save(); 77 | ctx->translate(x,y); 78 | ctx->scale(w,h); 79 | ctx->set_line_width(2.0*width/(w+h)); 80 | if (stroke->size()) { 81 | ctx->set_line_cap(Cairo::LINE_CAP_ROUND); 82 | int n = stroke->size(); 83 | float lambda = sqrt(3)-2.0; 84 | float sum = lambda / (1 - lambda); 85 | std::vector y(n); 86 | y[0] = stroke->points(0) * sum; 87 | for (int j = 0; j < n-1; j++) 88 | y[j+1] = (y[j] + stroke->points(j)) * lambda; 89 | std::vector z(n); 90 | z[n-1] = stroke->points(n-1) * (-sum); 91 | for (int j = n-1; j > 0; j--) 92 | z[j-1] = (z[j] - stroke->points(j)) * lambda; 93 | for (int j = 0; j < n-1; j++) { 94 | // j -> j+1 95 | ctx->set_source_rgba(0.0, stroke->time(j), 1.0-stroke->time(j), 1.0); 96 | Stroke::Point p[4]; 97 | p[0] = stroke->points(j); 98 | p[3] = stroke->points(j+1); 99 | p[1] = p[0] + y[j] + z[j]; 100 | p[2] = p[3] - y[j+1] - z[j+1]; 101 | ctx->move_to(p[0].x, p[0].y); 102 | ctx->curve_to(p[1].x, p[1].y, p[2].x, p[2].y, p[3].x, p[3].y); 103 | ctx->stroke(); 104 | } 105 | } else { 106 | ctx->set_source_rgba(0.0, 0.0, 1.0, 1.0); 107 | ctx->move_to(0.33, 0.33); 108 | ctx->line_to(0.67, 0.67); 109 | ctx->move_to(0.33, 0.67); 110 | ctx->line_to(0.67, 0.33); 111 | ctx->stroke(); 112 | } 113 | ctx->restore(); 114 | } 115 | 116 | void StrokeDrawer::draw_svg(const Stroke* stroke, std::string filename) { 117 | const int S = 32; 118 | const int B = 1; 119 | Cairo::RefPtr s = Cairo::SvgSurface::create(filename, S, S); 120 | draw(stroke, s, B, B, S-2*B, S-2*B); 121 | } 122 | 123 | 124 | -------------------------------------------------------------------------------- /src/stroke_draw.h: -------------------------------------------------------------------------------- 1 | /* 2 | * stroke_draw.h 3 | * 4 | * Copyright (c) 2008-2009, Thomas Jaeger 5 | * Copyright (c) 2020-2023 Daniel Kondor 6 | * 7 | * Permission to use, copy, modify, and/or distribute this software for any 8 | * purpose with or without fee is hereby granted, provided that the above 9 | * copyright notice and this permission notice appear in all copies. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 12 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 13 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 14 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 15 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 16 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 17 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 18 | * 19 | */ 20 | 21 | 22 | #ifndef STROKE_DRAW_H 23 | #define STROKE_DRAW_H 24 | 25 | #include "gesture.h" 26 | #include 27 | 28 | class StrokeDrawer { 29 | protected: 30 | static Glib::RefPtr drawEmpty_(int); 31 | static Glib::RefPtr pbEmpty; 32 | 33 | public: 34 | static Glib::RefPtr draw(const Stroke* stroke, int size, double width = 2.0); 35 | static void draw(const Stroke* stroke, Cairo::RefPtr surface, int x, int y, int w, int h, double width = 2.0); 36 | static void draw_svg(const Stroke* stroke, std::string filename); 37 | static Glib::RefPtr drawEmpty(int); 38 | }; 39 | 40 | #endif 41 | 42 | -------------------------------------------------------------------------------- /src/stroke_drawing_area.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * stroke_drawing_area.cpp -- Gtk.DrawingArea adapted to record new strokes 3 | * 4 | * Copyright 2020-2023 Daniel Kondor 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 13 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 15 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 16 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | * 18 | */ 19 | 20 | 21 | #include "stroke_drawing_area.h" 22 | #include 23 | 24 | SRArea::SRArea() { 25 | // signal_configure_event().connect(sigc::mem_fun(*this, &SRArea::configure_event)); 26 | add_events(Gdk::EventMask::BUTTON_PRESS_MASK | Gdk::EventMask::BUTTON_RELEASE_MASK | Gdk::EventMask::BUTTON_MOTION_MASK); 27 | } 28 | 29 | bool SRArea::on_configure_event(GdkEventConfigure* event) { 30 | auto win = get_window(); 31 | surface = win->create_similar_surface(Cairo::Content::CONTENT_COLOR, event->width, event->height); 32 | clear(); 33 | return true; 34 | } 35 | 36 | bool SRArea::on_draw(const Cairo::RefPtr& cr) { 37 | if(surface) { 38 | cr->set_source(surface, 0, 0); 39 | cr->paint(); 40 | } 41 | return true; 42 | } 43 | 44 | bool SRArea::on_button_press_event(GdkEventButton* event) { 45 | if(current_button) return true; 46 | current_button = event->button; 47 | last_x = event->x; 48 | last_y = event->y; 49 | ps.clear(); 50 | stroke = Stroke(); 51 | ps.push_back(Stroke::Point {last_x, last_y}); 52 | return true; 53 | } 54 | 55 | bool SRArea::on_button_release_event(GdkEventButton* event) { 56 | if(event->button != current_button) return true; 57 | draw_line(event->x, event->y); 58 | current_button = 0; 59 | stroke = Stroke(ps); 60 | ps.clear(); 61 | stroke_recorded.emit(&stroke); 62 | return true; 63 | } 64 | 65 | bool SRArea::on_motion_notify_event(GdkEventMotion* event) { 66 | if(current_button) draw_line(event->x, event->y); 67 | return true; 68 | } 69 | 70 | void SRArea::draw_line(gdouble x, gdouble y) { 71 | if(surface && (x != last_x || y != last_y)) { 72 | auto cr = Cairo::Context::create(surface); 73 | cr->set_source_rgb(0.8, 0, 0); 74 | cr->move_to(last_x, last_y); 75 | cr->line_to(x, y); 76 | cr->stroke(); 77 | 78 | int x1 = std::floor(std::min(x, last_x)) - 2; 79 | int y1 = std::floor(std::min(y, last_y)) - 2; 80 | int w = std::ceil(std::abs(x - last_x)) + 4; 81 | int h = std::ceil(std::abs(y - last_y)) + 4; 82 | queue_draw_area(x1, y1, w, h); 83 | 84 | ps.push_back(Stroke::Point {last_x, last_y}); 85 | 86 | last_x = x; 87 | last_y = y; 88 | } 89 | } 90 | 91 | void SRArea::clear() { 92 | if(surface) { 93 | auto cr = Cairo::Context::create(surface); 94 | cr->set_source_rgb(1, 1, 1); 95 | cr->paint(); 96 | } 97 | ps.clear(); 98 | stroke = Stroke(); 99 | } 100 | 101 | -------------------------------------------------------------------------------- /src/stroke_drawing_area.h: -------------------------------------------------------------------------------- 1 | /* 2 | * stroke_drawing_area.h -- Gtk.DrawingArea adapted to record new strokes 3 | * 4 | * Copyright 2020-2023 Daniel Kondor 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 13 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 15 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 16 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | * 18 | */ 19 | 20 | 21 | #ifndef STROKE_DRAWING_AREA_H 22 | #define STROKE_DRAWING_AREA_H 23 | 24 | #include 25 | #include "gesture.h" 26 | 27 | class SRArea : public Gtk::DrawingArea { 28 | public: 29 | SRArea(); 30 | void clear(); 31 | Stroke* get_stroke() { return &stroke; } 32 | sigc::signal stroke_recorded; 33 | virtual ~SRArea() { } 34 | protected: 35 | bool on_draw(const Cairo::RefPtr& cr) override; 36 | bool on_button_press_event(GdkEventButton* event) override; 37 | bool on_button_release_event(GdkEventButton* event) override; 38 | bool on_motion_notify_event(GdkEventMotion* event) override; 39 | bool on_configure_event(GdkEventConfigure* event) override; 40 | 41 | void draw_line(gdouble x, gdouble y); 42 | 43 | Cairo::RefPtr surface; 44 | guint current_button = 0; 45 | gdouble last_x; 46 | gdouble last_y; 47 | 48 | Stroke::PreStroke ps; 49 | Stroke stroke; 50 | }; 51 | 52 | 53 | #endif 54 | 55 | -------------------------------------------------------------------------------- /toplevel-grabber/meson.build: -------------------------------------------------------------------------------- 1 | # toplevel-grabber library and example program 2 | grabber_protocols = [ 3 | './wlr-foreign-toplevel-management-unstable-v1.xml' 4 | ] 5 | 6 | grabber_protos_client_src = [] 7 | grabber_protos_headers = [] 8 | 9 | foreach p : grabber_protocols 10 | xml = join_paths(p) 11 | grabber_protos_headers += wayland_scanner_client.process(xml) 12 | grabber_protos_client_src += wayland_scanner_code.process(xml) 13 | endforeach 14 | 15 | lib_grabber_protos = static_library('grabber_protos', grabber_protos_client_src + grabber_protos_headers, 16 | dependencies: [wayland_client]) # for the include directory 17 | 18 | lib_grabber_protos_dep = declare_dependency( 19 | link_with: lib_grabber_protos, 20 | sources: grabber_protos_headers, 21 | ) 22 | 23 | toplevel_grabber = static_library('toplevel_grabber', 'toplevel-grabber.c', 24 | dependencies: [wayland_client, lib_grabber_protos_dep], 25 | c_args: ['-D_POSIX_C_SOURCE=200809L']) 26 | 27 | toplevel_grabber_dep = declare_dependency( 28 | link_with: toplevel_grabber, 29 | include_directories: include_directories('.') 30 | ) 31 | 32 | toplevel_grabber_test = executable('tl_grabber_test', 'toplevel-grabber-test.c', 33 | dependencies: [toplevel_grabber_dep], 34 | install: false) 35 | -------------------------------------------------------------------------------- /toplevel-grabber/toplevel-grabber-test.c: -------------------------------------------------------------------------------- 1 | /* 2 | * toplevel-grabber-test.c -- test selecting a toplevel view based on 3 | * user interaction 4 | * 5 | * Copyright 2020 Daniel Kondor 6 | * 7 | * Permission to use, copy, modify, and/or distribute this software for any 8 | * purpose with or without fee is hereby granted, provided that the above 9 | * copyright notice and this permission notice appear in all copies. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 12 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 13 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 14 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 15 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 16 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 17 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 18 | * 19 | */ 20 | 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | static int is_done = 0; 28 | 29 | static void tl_cb(void* data, struct tl_grabber* gr) { 30 | char* app_id = toplevel_grabber_get_app_id(gr); 31 | printf("Activated app: %s\n", app_id ? app_id : "(null)"); 32 | if(app_id) free(app_id); 33 | toplevel_grabber_free(gr); 34 | is_done = 1; 35 | } 36 | 37 | 38 | int main() { 39 | struct wl_display* dpy = wl_display_connect(NULL); 40 | if(!dpy) { 41 | fprintf(stderr, "Cannot connect to display!\n"); 42 | return 1; 43 | } 44 | 45 | struct tl_grabber* gr = toplevel_grabber_new(dpy, NULL, NULL); 46 | if(!gr) { 47 | fprintf(stderr, "Cannot create grabber interface!\n"); 48 | return 1; 49 | } 50 | printf("Starting grabber, click to select a toplevel view\n"); 51 | toplevel_grabber_set_callback(gr, tl_cb, NULL); 52 | 53 | while(!(is_done || wl_display_dispatch(dpy) == -1)); 54 | if(!is_done) toplevel_grabber_free(gr); 55 | 56 | return 0; 57 | } 58 | 59 | -------------------------------------------------------------------------------- /toplevel-grabber/toplevel-grabber.c: -------------------------------------------------------------------------------- 1 | /* 2 | * toplevel-grabber.c -- library using the wlr-foreign-toplevel 3 | * interface to get the ID of an activated toplevel view 4 | * 5 | * Copyright 2020 Daniel Kondor 6 | * 7 | * Permission to use, copy, modify, and/or distribute this software for any 8 | * purpose with or without fee is hereby granted, provided that the above 9 | * copyright notice and this permission notice appear in all copies. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 12 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 13 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 14 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 15 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 16 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 17 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 18 | * 19 | */ 20 | 21 | 22 | #include 23 | #include 24 | #include 25 | #include "wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" 26 | #include 27 | 28 | typedef struct zwlr_foreign_toplevel_handle_v1 wfthandle; 29 | 30 | /* struct to hold information for one instance of the grabber */ 31 | struct tl_grabber { 32 | struct zwlr_foreign_toplevel_manager_v1* manager; 33 | struct wl_list toplevels; 34 | void (*callback)(void* data, struct tl_grabber* gr); 35 | void* data; 36 | struct toplevel* active; 37 | int init_done; 38 | }; 39 | 40 | /* struct to hold information about one toplevel */ 41 | struct toplevel { 42 | char* app_id; 43 | wfthandle* handle; 44 | wfthandle* parent; 45 | struct tl_grabber* gr; 46 | int init_done; 47 | struct wl_list link; 48 | }; 49 | 50 | 51 | #ifndef G_GNUC_UNUSED 52 | #define G_GNUC_UNUSED __attribute__((unused)) 53 | #endif 54 | 55 | /* callbacks */ 56 | 57 | static void title_cb(G_GNUC_UNUSED void* data, G_GNUC_UNUSED wfthandle* handle, 58 | G_GNUC_UNUSED const char* title) { 59 | /* don't care */ 60 | } 61 | 62 | static void appid_cb(void* data, G_GNUC_UNUSED wfthandle* handle, const char* app_id) { 63 | if(!(app_id && data)) return; 64 | struct toplevel* tl = (struct toplevel*)data; 65 | if(tl->app_id) free(tl->app_id); 66 | tl->app_id = strdup(app_id); 67 | } 68 | 69 | void output_enter_cb(G_GNUC_UNUSED void* data, G_GNUC_UNUSED wfthandle* handle, 70 | G_GNUC_UNUSED struct wl_output* output) { 71 | /* don't care */ 72 | } 73 | void output_leave_cb(G_GNUC_UNUSED void* data, G_GNUC_UNUSED wfthandle* handle, 74 | G_GNUC_UNUSED struct wl_output* output) { 75 | /* don't care */ 76 | } 77 | 78 | void state_cb(void* data, G_GNUC_UNUSED wfthandle* handle, struct wl_array* state) { 79 | if(!(data && state)) return; 80 | struct toplevel* tl = (struct toplevel*)data; 81 | struct tl_grabber* gr = tl->gr; 82 | if(!gr) return; 83 | int activated = 0; 84 | int i; 85 | uint32_t* stdata = (uint32_t*)state->data; 86 | for(i = 0; i*sizeof(uint32_t) < state->size; i++) { 87 | if(stdata[i] == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ACTIVATED) { 88 | activated = 1; 89 | break; 90 | } 91 | } 92 | if(activated) { 93 | int orig_init_done = tl->init_done; 94 | struct toplevel* new_active = NULL; 95 | while(tl) { 96 | new_active = tl; 97 | if(!tl->parent) break; 98 | tl = zwlr_foreign_toplevel_handle_v1_get_user_data(tl->parent); 99 | } 100 | if(new_active != gr->active) { 101 | gr->active = new_active; 102 | if(orig_init_done && gr->callback) gr->callback(gr->data, gr); 103 | } 104 | } 105 | } 106 | 107 | void done_cb(void* data, G_GNUC_UNUSED wfthandle* handle) { 108 | if(!data) return; 109 | struct toplevel* tl = (struct toplevel*)data; 110 | tl->init_done = 1; 111 | } 112 | 113 | void closed_cb(void* data, G_GNUC_UNUSED wfthandle* handle) { 114 | if(!data) return; 115 | struct toplevel* tl = (struct toplevel*)data; 116 | /* note: we can assume that this toplevel is not set as the parent 117 | * of any existing toplevels at this point */ 118 | wl_list_remove(&(tl->link)); 119 | zwlr_foreign_toplevel_handle_v1_destroy(tl->handle); 120 | if(tl->app_id) free(tl->app_id); 121 | free(tl); 122 | } 123 | 124 | void parent_cb(void* data, G_GNUC_UNUSED wfthandle* handle, wfthandle* parent) { 125 | if(!data) return; 126 | struct toplevel* tl = (struct toplevel*)data; 127 | tl->parent = parent; 128 | } 129 | 130 | struct zwlr_foreign_toplevel_handle_v1_listener toplevel_handle_interface = { 131 | .title = title_cb, 132 | .app_id = appid_cb, 133 | .output_enter = output_enter_cb, 134 | .output_leave = output_leave_cb, 135 | .state = state_cb, 136 | .done = done_cb, 137 | .closed = closed_cb, 138 | .parent = parent_cb 139 | }; 140 | 141 | /* register new toplevel */ 142 | static void new_toplevel(void *data, G_GNUC_UNUSED struct zwlr_foreign_toplevel_manager_v1 *manager, 143 | wfthandle *handle) { 144 | if(!handle) return; 145 | if(!data) { 146 | /* if we unset the user data pointer, then we don't care anymore */ 147 | zwlr_foreign_toplevel_handle_v1_destroy(handle); 148 | return; 149 | } 150 | struct tl_grabber* gr = (struct tl_grabber*)data; 151 | struct toplevel* tl = (struct toplevel*)malloc(sizeof(struct toplevel)); 152 | if(!tl) { 153 | /* TODO: error message */ 154 | return; 155 | } 156 | tl->app_id = NULL; 157 | tl->handle = handle; 158 | tl->parent = NULL; 159 | tl->init_done = 0; 160 | tl->gr = gr; 161 | wl_list_insert(&(gr->toplevels), &(tl->link)); 162 | 163 | /* note: we cannot do anything as long as we get app_id */ 164 | zwlr_foreign_toplevel_handle_v1_add_listener(handle, &toplevel_handle_interface, tl); 165 | } 166 | 167 | /* sent when toplevel management is no longer available -- this will happen after stopping */ 168 | static void toplevel_manager_finished(G_GNUC_UNUSED void *data, 169 | struct zwlr_foreign_toplevel_manager_v1 *manager) { 170 | zwlr_foreign_toplevel_manager_v1_destroy(manager); 171 | } 172 | 173 | static struct zwlr_foreign_toplevel_manager_v1_listener toplevel_manager_interface = { 174 | .toplevel = new_toplevel, 175 | .finished = toplevel_manager_finished, 176 | }; 177 | 178 | static void registry_global_add_cb(void *data, struct wl_registry *registry, 179 | uint32_t id, const char *interface, uint32_t version) { 180 | struct tl_grabber* gr = (struct tl_grabber*)data; 181 | if(!strcmp(interface, zwlr_foreign_toplevel_manager_v1_interface.name)) { 182 | uint32_t v = zwlr_foreign_toplevel_manager_v1_interface.version; 183 | if(version < v) v = version; 184 | gr->manager = wl_registry_bind(registry, id, &zwlr_foreign_toplevel_manager_v1_interface, v); 185 | if(gr->manager) 186 | zwlr_foreign_toplevel_manager_v1_add_listener(gr->manager, &toplevel_manager_interface, gr); 187 | else { /* TODO: handle error */ } 188 | } 189 | gr->init_done = 0; 190 | } 191 | 192 | static void registry_global_remove_cb(G_GNUC_UNUSED void *data, 193 | G_GNUC_UNUSED struct wl_registry *registry, G_GNUC_UNUSED uint32_t id) { 194 | /* don't care */ 195 | } 196 | 197 | static const struct wl_registry_listener registry_listener = { 198 | registry_global_add_cb, 199 | registry_global_remove_cb 200 | }; 201 | 202 | struct tl_grabber* toplevel_grabber_new(struct wl_display* dpy, 203 | void (*callback)(void* data, struct tl_grabber* gr), void* data) { 204 | if(!dpy) { 205 | /* TODO: get display! */ 206 | return NULL; 207 | } 208 | 209 | struct tl_grabber* gr = (struct tl_grabber*)malloc(sizeof(struct tl_grabber)); 210 | if(!gr) return NULL; 211 | 212 | gr->manager = NULL; 213 | wl_list_init(&(gr->toplevels)); 214 | gr->callback = callback; 215 | gr->data = data; 216 | gr->active = NULL; 217 | 218 | struct wl_registry* registry = wl_display_get_registry(dpy); 219 | wl_registry_add_listener(registry, ®istry_listener, gr); 220 | do { 221 | gr->init_done = 1; 222 | wl_display_roundtrip(dpy); 223 | } 224 | while(!gr->init_done); 225 | return gr; 226 | } 227 | 228 | char* toplevel_grabber_get_app_id(struct tl_grabber* gr) { 229 | char* ret = NULL; 230 | if(gr && gr->active && gr->active->app_id) ret = strdup(gr->active->app_id); 231 | return ret; 232 | } 233 | 234 | /* 235 | void toplevel_grabber_reset(struct tl_grabber* gr) { 236 | if(gr) gr->active = NULL; 237 | } 238 | */ 239 | 240 | void toplevel_grabber_set_callback(struct tl_grabber* gr, 241 | void (*callback)(void* data, struct tl_grabber* gr), void* data) { 242 | if(gr) { 243 | gr->callback = callback; 244 | gr->data = data; 245 | } 246 | } 247 | 248 | int toplevel_grabber_activate_app(struct tl_grabber* gr, 249 | const char* app_id, struct wl_seat* wl_seat, int parent) { 250 | if(!(gr && app_id)) return -1; 251 | struct toplevel* tl; 252 | wl_list_for_each(tl, &(gr->toplevels), link) { 253 | if(!strcmp(app_id, tl->app_id)) { 254 | if(parent) while(tl->parent) { 255 | struct toplevel* tmp = zwlr_foreign_toplevel_handle_v1_get_user_data(tl->parent); 256 | if(!tmp) break; 257 | tl = tmp; 258 | } 259 | zwlr_foreign_toplevel_handle_v1_activate (tl->handle, wl_seat); 260 | return 0; 261 | } 262 | } 263 | return -1; 264 | } 265 | 266 | void toplevel_grabber_free(struct tl_grabber* gr) { 267 | if(!gr) return; 268 | /* stop listening and also free all existing toplevels */ 269 | /* set user data to null -- this will stop adding newly reported toplevels */ 270 | zwlr_foreign_toplevel_manager_v1_set_user_data(gr->manager, NULL); 271 | /* this will send the finished signal and result in destroying manager later */ 272 | zwlr_foreign_toplevel_manager_v1_stop(gr->manager); 273 | gr->manager = NULL; 274 | /* destroy all existing toplevel handles */ 275 | struct toplevel* tl; 276 | struct toplevel* tmp; 277 | wl_list_for_each_safe(tl, tmp, &(gr->toplevels), link) { 278 | wl_list_remove(&(tl->link)); 279 | zwlr_foreign_toplevel_handle_v1_destroy(tl->handle); 280 | if(tl->app_id) free(tl->app_id); 281 | free(tl); 282 | } 283 | } 284 | 285 | 286 | -------------------------------------------------------------------------------- /toplevel-grabber/toplevel-grabber.h: -------------------------------------------------------------------------------- 1 | /* 2 | * toplevel-grabber.h -- library using the wlr-foreign-toplevel 3 | * interface to get the ID of an activated toplevel view 4 | * 5 | * Copyright 2020 Daniel Kondor 6 | * 7 | * Permission to use, copy, modify, and/or distribute this software for any 8 | * purpose with or without fee is hereby granted, provided that the above 9 | * copyright notice and this permission notice appear in all copies. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 12 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 13 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 14 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 15 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 16 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 17 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 18 | * 19 | */ 20 | 21 | 22 | #ifndef TOPLEVEL_GRABBER_H 23 | #define TOPLEVEL_GRABBER_H 24 | 25 | #ifdef __cplusplus 26 | extern "C" { 27 | #endif 28 | 29 | #include 30 | 31 | struct tl_grabber; 32 | 33 | /* 34 | * Create a new grabber and start listening to events about toplevels. 35 | * Parameters: 36 | * dpy -- Wayland display connectoin to use (must be given by the caller) 37 | * callback -- function to be called when a new toplevel is activated (can be NULL) 38 | * data -- user data to pass to the callback function 39 | */ 40 | struct tl_grabber* toplevel_grabber_new(struct wl_display* dpy, 41 | void (*callback)(void* data, struct tl_grabber* gr), void* data); 42 | 43 | /* 44 | * Get the last activated app ID (if any). Returns a copy of the app ID, 45 | * or NULL if no app was activated or no app ID can be determined. If the 46 | * return value is non-NULL, the caller must free() it. 47 | */ 48 | char* toplevel_grabber_get_app_id(struct tl_grabber* gr); 49 | 50 | /* 51 | * Reset the currently active app, i.e. toplevel_grabber_get_app_id() 52 | * will return NULL until a new toplevel is activated again. 53 | * void toplevel_grabber_reset(struct tl_grabber* gr); 54 | */ 55 | 56 | /* 57 | * Set the callback function to be called when a new toplevel is activated. 58 | */ 59 | void toplevel_grabber_set_callback(struct tl_grabber* gr, 60 | void (*callback)(void* data, struct tl_grabber* gr), void* data); 61 | 62 | /* 63 | * Activate any toplevel view with a matching app_id on the given seat. 64 | * If parent is true, it selects the topmost parent if multiple views 65 | * in a hierarchy have the same app ID. 66 | * This is useful to re-show the caller's view after the user has 67 | * selected another app by activating it. 68 | * Returns 0 on success or -1 if the given app ID was not found. 69 | */ 70 | int toplevel_grabber_activate_app(struct tl_grabber* gr, 71 | const char* app_id, struct wl_seat* wl_seat, int parent); 72 | 73 | /* Stop listening to toplevel events and free all resources associated 74 | * with this instance. 75 | */ 76 | void toplevel_grabber_free(struct tl_grabber* gr); 77 | 78 | #ifdef __cplusplus 79 | } 80 | #endif 81 | 82 | #endif 83 | 84 | -------------------------------------------------------------------------------- /toplevel-grabber/wlr-foreign-toplevel-management-unstable-v1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright © 2018 Ilia Bozhinov 5 | 6 | Permission to use, copy, modify, distribute, and sell this 7 | software and its documentation for any purpose is hereby granted 8 | without fee, provided that the above copyright notice appear in 9 | all copies and that both that copyright notice and this permission 10 | notice appear in supporting documentation, and that the name of 11 | the copyright holders not be used in advertising or publicity 12 | pertaining to distribution of the software without specific, 13 | written prior permission. The copyright holders make no 14 | representations about the suitability of this software for any 15 | purpose. It is provided "as is" without express or implied 16 | warranty. 17 | 18 | THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS 19 | SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 20 | FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 22 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 23 | AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 24 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 25 | THIS SOFTWARE. 26 | 27 | 28 | 29 | 30 | The purpose of this protocol is to enable the creation of taskbars 31 | and docks by providing them with a list of opened applications and 32 | letting them request certain actions on them, like maximizing, etc. 33 | 34 | After a client binds the zwlr_foreign_toplevel_manager_v1, each opened 35 | toplevel window will be sent via the toplevel event 36 | 37 | 38 | 39 | 40 | This event is emitted whenever a new toplevel window is created. It 41 | is emitted for all toplevels, regardless of the app that has created 42 | them. 43 | 44 | All initial details of the toplevel(title, app_id, states, etc.) will 45 | be sent immediately after this event via the corresponding events in 46 | zwlr_foreign_toplevel_handle_v1. 47 | 48 | 49 | 50 | 51 | 52 | 53 | Indicates the client no longer wishes to receive events for new toplevels. 54 | However the compositor may emit further toplevel_created events, until 55 | the finished event is emitted. 56 | 57 | The client must not send any more requests after this one. 58 | 59 | 60 | 61 | 62 | 63 | This event indicates that the compositor is done sending events to the 64 | zwlr_foreign_toplevel_manager_v1. The server will destroy the object 65 | immediately after sending this request, so it will become invalid and 66 | the client should free any resources associated with it. 67 | 68 | 69 | 70 | 71 | 72 | 73 | A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel 74 | window. Each app may have multiple opened toplevels. 75 | 76 | Each toplevel has a list of outputs it is visible on, conveyed to the 77 | client with the output_enter and output_leave events. 78 | 79 | 80 | 81 | 82 | This event is emitted whenever the title of the toplevel changes. 83 | 84 | 85 | 86 | 87 | 88 | 89 | This event is emitted whenever the app-id of the toplevel changes. 90 | 91 | 92 | 93 | 94 | 95 | 96 | This event is emitted whenever the toplevel becomes visible on 97 | the given output. A toplevel may be visible on multiple outputs. 98 | 99 | 100 | 101 | 102 | 103 | 104 | This event is emitted whenever the toplevel stops being visible on 105 | the given output. It is guaranteed that an entered-output event 106 | with the same output has been emitted before this event. 107 | 108 | 109 | 110 | 111 | 112 | 113 | Requests that the toplevel be maximized. If the maximized state actually 114 | changes, this will be indicated by the state event. 115 | 116 | 117 | 118 | 119 | 120 | Requests that the toplevel be unmaximized. If the maximized state actually 121 | changes, this will be indicated by the state event. 122 | 123 | 124 | 125 | 126 | 127 | Requests that the toplevel be minimized. If the minimized state actually 128 | changes, this will be indicated by the state event. 129 | 130 | 131 | 132 | 133 | 134 | Requests that the toplevel be unminimized. If the minimized state actually 135 | changes, this will be indicated by the state event. 136 | 137 | 138 | 139 | 140 | 141 | Request that this toplevel be activated on the given seat. 142 | There is no guarantee the toplevel will be actually activated. 143 | 144 | 145 | 146 | 147 | 148 | 149 | The different states that a toplevel can have. These have the same meaning 150 | as the states with the same names defined in xdg-toplevel 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | This event is emitted immediately after the zlw_foreign_toplevel_handle_v1 162 | is created and each time the toplevel state changes, either because of a 163 | compositor action or because of a request in this protocol. 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | This event is sent after all changes in the toplevel state have been 172 | sent. 173 | 174 | This allows changes to the zwlr_foreign_toplevel_handle_v1 properties 175 | to be seen as atomic, even if they happen via multiple events. 176 | 177 | 178 | 179 | 180 | 181 | Send a request to the toplevel to close itself. The compositor would 182 | typically use a shell-specific method to carry out this request, for 183 | example by sending the xdg_toplevel.close event. However, this gives 184 | no guarantees the toplevel will actually be destroyed. If and when 185 | this happens, the zwlr_foreign_toplevel_handle_v1.closed event will 186 | be emitted. 187 | 188 | 189 | 190 | 191 | 192 | The rectangle of the surface specified in this request corresponds to 193 | the place where the app using this protocol represents the given toplevel. 194 | It can be used by the compositor as a hint for some operations, e.g 195 | minimizing. The client is however not required to set this, in which 196 | case the compositor is free to decide some default value. 197 | 198 | If the client specifies more than one rectangle, only the last one is 199 | considered. 200 | 201 | The dimensions are given in surface-local coordinates. 202 | Setting width=height=0 removes the already-set rectangle. 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 215 | 216 | 217 | 218 | 219 | This event means the toplevel has been destroyed. It is guaranteed there 220 | won't be any more events for this zwlr_foreign_toplevel_handle_v1. The 221 | toplevel itself becomes inert so any requests will be ignored except the 222 | destroy request. 223 | 224 | 225 | 226 | 227 | 228 | Destroys the zwlr_foreign_toplevel_handle_v1 object. 229 | 230 | This request should be called either when the client does not want to 231 | use the toplevel anymore or after the closed event to finalize the 232 | destruction of the object. 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | Requests that the toplevel be fullscreened on the given output. If the 241 | fullscreen state and/or the outputs the toplevel is visible on actually 242 | change, this will be indicated by the state and output_enter/leave 243 | events. 244 | 245 | The output parameter is only a hint to the compositor. Also, if output 246 | is NULL, the compositor should decide which output the toplevel will be 247 | fullscreened on, if at all. 248 | 249 | 250 | 251 | 252 | 253 | 254 | Requests that the toplevel be unfullscreened. If the fullscreen state 255 | actually changes, this will be indicated by the state event. 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | This event is emitted whenever the parent of the toplevel changes. 264 | 265 | 266 | 267 | 268 | 269 | -------------------------------------------------------------------------------- /wstroke-config.1: -------------------------------------------------------------------------------- 1 | .TH WSTROKE-CONFIG 1 2024-08-11 2 | .SH NAME 3 | wstroke-config \- graphical configuration of mouse gestures 4 | .SH SYNOPSIS 5 | .BR wstroke-config 6 | .SH DESCRIPTION 7 | .BR wstroke-config 8 | is a graphical utility to configure gestures recognized by 9 | .BR wstroke , 10 | a mouse gesture plugin for the 11 | .BR wayfire (1) 12 | compositor. 13 | Note that additional options that affect the behavior of 14 | .BR wstroke 15 | are configured using wayfire's regular config system, e.g. by 16 | .BR wcm. 17 | .SH ENVIRONMENT VARIABLES 18 | .BR wstroke-config 19 | respects the XDG_CONFIG_HOME environment variable: configuration is stored under 20 | ${XDG_CONFIG_HOME}/wstroke. 21 | -------------------------------------------------------------------------------- /wstroke-config.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | Name=WStroke config 4 | Type=Application 5 | Terminal=false 6 | Exec=wstroke-config 7 | Icon=wstroke 8 | Categories=GTK;Utility;Accessibility; 9 | Comment=Configure mouse gestures used with Wayfire 10 | Keywords=gestures;input;wayfire; 11 | -------------------------------------------------------------------------------- /wstroke.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <_short>Mouse Gestures 5 | <_long>A plugin to identify complex gestures drawn by the mouse and associate them with actions. 6 | Accessibility 7 | 8 | <_short>General 9 | 14 | 19 | 25 | 30 | 51 | 56 | 61 | 62 | 63 | <_short>Action preferences 64 | 89 | 96 | 97 | 104 | 105 | 106 | 107 | --------------------------------------------------------------------------------