From 2a74aa0bad5422d01212c69457b15385c170f52b Mon Sep 17 00:00:00 2001 From: Matthew White Date: Thu, 30 Apr 2020 18:49:56 +0200 Subject: [PATCH] Add support for showing the mouse clicks Based on @ziadkh0's MR !65 (aka @zaygraveyard on GitHub). --- README.rst | 8 ++ Screenkey/inputlistener.py | 33 +++++-- Screenkey/labelmanager.py | 32 +++++-- Screenkey/screenkey.py | 192 ++++++++++++++++++++++++++++++++++--- images/mouse.original.svg | 167 ++++++++++++++++++++++++++++++++ images/mouse.svg | 10 ++ screenkey | 6 +- 7 files changed, 423 insertions(+), 25 deletions(-) create mode 100644 images/mouse.original.svg create mode 100644 images/mouse.svg diff --git a/README.rst b/README.rst index e305dbc..9a04c1f 100644 --- a/README.rst +++ b/README.rst @@ -19,6 +19,7 @@ This is an almost-complete rewrite of screenkey_ 0.2, featuring: - Switch for visible shift and modifier sequences only - Repeats compression - Countless bug fixes +- Mouse buttons support Installation and basic usage @@ -156,6 +157,13 @@ Compress repeats: requested threshold. A counter of total occurrences is shown instead, which is generally more legible. +Show mouse: + When enabled, the mouse buttons are shown on the left of the output window. + +Hide duration: + Duration (in seconds) of the fade-out animation when a button is released. + Defaults to 1 second. + Advanced usage -------------- diff --git a/Screenkey/inputlistener.py b/Screenkey/inputlistener.py index af4da3e..e00d03b 100644 --- a/Screenkey/inputlistener.py +++ b/Screenkey/inputlistener.py @@ -155,6 +155,12 @@ class KeyData(): self.modifiers = modifiers +class ButtonData(): + def __init__(self, btn, pressed): + self.btn = btn + self.pressed = pressed == xlib.ButtonPress + + class InputType: keyboard = 0b001 button = 0b010 @@ -164,9 +170,11 @@ class InputType: class InputListener(threading.Thread): - def __init__(self, callback, input_types=InputType.all, kbd_compose=True, kbd_translate=True): + def __init__(self, kbd_callback, btn_callback, input_types=InputType.all, + kbd_compose=True, kbd_translate=True): super(InputListener, self).__init__() - self.callback = callback + self.kbd_callback = kbd_callback + self.btn_callback = btn_callback self.input_types = input_types self.kbd_compose = kbd_compose self.kbd_translate = kbd_translate @@ -189,15 +197,19 @@ class InputListener(threading.Thread): xlib.XSendEvent(self.replay_dpy, self.replay_win, False, 0, fwd_ev) - def _event_callback(self, data): - self.callback(data) + def _kbd_event_callback(self, data): + self.kbd_callback(data) + return False + + def _btn_event_callback(self, data): + self.btn_callback(data) return False def _event_processed(self, data): data.symbol = xlib.XKeysymToString(data.keysym) if data.string is None: data.string = keysym_to_unicode(data.keysym) - glib.idle_add(self._event_callback, data) + glib.idle_add(self._kbd_event_callback, data) def _event_modifiers(self, kev, data): @@ -314,6 +326,12 @@ class InputListener(threading.Thread): self._kbd_last_ev = ev + def _btn_process(self, ev): + if ev.type in [xlib.ButtonPress, xlib.ButtonRelease]: + data = ButtonData(ev.xbutton.button, ev.type) + glib.idle_add(self._btn_event_callback, data) + + def run(self): # control connection self.control_dpy = xlib.XOpenDisplay(None) @@ -336,7 +354,8 @@ class InputListener(threading.Thread): xlib.XCloseDisplay(self.replay_dpy) # cheap wakeup() equivalent for compatibility - glib.idle_add(self._event_callback, None) + glib.idle_add(self._kbd_event_callback, None) + glib.idle_add(self._btn_event_callback, None) self.stopped = True self.lock.release() @@ -385,6 +404,8 @@ class InputListener(threading.Thread): xlib.XNextEvent(self.replay_dpy, xlib.byref(ev)) if self.input_types & InputType.keyboard: self._kbd_process(ev) + if self.input_types & InputType.button: + self._btn_process(ev) # finalize self.lock.acquire() diff --git a/Screenkey/labelmanager.py b/Screenkey/labelmanager.py index a5c4f90..11ba640 100644 --- a/Screenkey/labelmanager.py +++ b/Screenkey/labelmanager.py @@ -22,6 +22,7 @@ from datetime import datetime ReplData = namedtuple('ReplData', ['value', 'font', 'suffix']) KeyRepl = namedtuple('KeyRepl', ['bk_stop', 'silent', 'spaced', 'repl']) KeyData = namedtuple('KeyData', ['stamp', 'is_ctrl', 'bk_stop', 'silent', 'spaced', 'markup']) +ButtonData = namedtuple('ButtonData', ['stamp', 'btn', 'pressed']) REPLACE_SYMS = { # Regular keys @@ -141,13 +142,15 @@ def keysym_to_mod(keysym): class LabelManager(object): - def __init__(self, listener, logger, key_mode, bak_mode, mods_mode, mods_only, - multiline, vis_shift, vis_space, recent_thr, compr_cnt, ignore, pango_ctx): + def __init__(self, label_listener, image_listener, logger, key_mode, + bak_mode, mods_mode, mods_only, multiline, vis_shift, + vis_space, recent_thr, compr_cnt, ignore, pango_ctx): self.key_mode = key_mode self.bak_mode = bak_mode self.mods_mode = mods_mode self.logger = logger - self.listener = listener + self.label_listener = label_listener + self.image_listener = image_listener self.data = [] self.enabled = True self.mods_only = mods_only @@ -170,7 +173,9 @@ class LabelManager(object): self.stop() compose = (self.key_mode == 'composed') translate = (self.key_mode in ['composed', 'translated']) - self.kl = InputListener(self.key_press, InputType.keyboard, compose, translate) + self.kl = InputListener(self.key_press, self.btn_press, + InputType.keyboard | InputType.button, compose, + translate) self.kl.start() self.logger.debug("Thread started.") @@ -279,7 +284,7 @@ class LabelManager(object): if recent: markup += '' self.logger.debug("Label updated: %s." % repr(markup)) - self.listener(markup, synthetic) + self.label_listener(markup, synthetic) def queue_update(self): @@ -289,7 +294,7 @@ class LabelManager(object): def key_press(self, event): if event is None: self.logger.debug("inputlistener failure: {}".format(str(self.kl.error))) - self.listener(None, None) + self.label_listener(None, None) return symbol = event.symbol.decode() if event.pressed == False: @@ -468,3 +473,18 @@ class LabelManager(object): value = event.string or symbol self.data.append(KeyData(datetime.now(), True, True, True, True, value)) return True + + + def btn_press(self, event): + if not self.enabled: + return False + + if event.pressed: + action = "pressed" + else: + action = "released" + self.logger.debug("Mouse button %d %s" % (event.btn, action)) + + self.image_listener( + ButtonData(datetime.now(), event.btn, event.pressed) + ) diff --git a/Screenkey/screenkey.py b/Screenkey/screenkey.py index 8b721fc..f21835b 100644 --- a/Screenkey/screenkey.py +++ b/Screenkey/screenkey.py @@ -8,10 +8,12 @@ from . import * from .labelmanager import LabelManager from threading import Timer +from datetime import datetime import json import os import subprocess import numbers +from tempfile import NamedTemporaryFile import gi gi.require_version('Gtk', '3.0') @@ -20,7 +22,7 @@ gi.require_version('Pango', '1.0') from gi.repository import GLib GLib.threads_init() -from gi.repository import Gtk, Gdk, Pango +from gi.repository import Gtk, Gdk, GdkPixbuf, Pango import cairo @@ -36,6 +38,38 @@ HORIZONTAL = Gtk.Orientation.HORIZONTAL VERTICAL = Gtk.Orientation.VERTICAL IF_VALID = Gtk.SpinButtonUpdatePolicy.IF_VALID +BUTTONS_SVG = None + +def load_button_pixbufs(color): + global BUTTONS_SVG + + if BUTTONS_SVG is None: + with open('/usr/share/images/screenkey/mouse.svg', 'r') as svg_file: + BUTTONS_SVG = svg_file.readlines() + + if not isinstance(color, str): + # Gdk.Color + color = 'rgb({}, {}, {})'.format( + round(color.red_float * 255), + round(color.green_float * 255), + round(color.blue_float * 255) + ) + button_pixbufs = [] + svg = NamedTemporaryFile(mode='w', suffix='.svg') + for line in BUTTONS_SVG[1:-1]: + svg.seek(0) + svg.truncate() + svg.writelines(( + BUTTONS_SVG[0], + line.replace('#fff', color), + BUTTONS_SVG[-1], + )) + svg.flush() + os.fsync(svg.fileno()) + button_pixbufs.append(GdkPixbuf.Pixbuf.new_from_file(svg.name)) + svg.close() + return button_pixbufs + class Screenkey(Gtk.Window): STATE_FILE = os.path.join(GLib.get_user_config_dir(), 'screenkey.json') @@ -68,7 +102,9 @@ class Screenkey(Gtk.Window): 'vis_shift': False, 'vis_space': True, 'geometry': None, - 'screen': 0}) + 'screen': 0, + 'mouse': False, + 'button_hide_duration': 1}) self.options = self.load_state() if self.options is None: self.options = defaults @@ -88,14 +124,23 @@ class Screenkey(Gtk.Window): self.set_focus_on_map(False) self.set_app_paintable(True) + self.button_pixbufs = [] + self.button_states = [None] * 8 + self.img = Gtk.Image() + self.update_image_tag = None + + self.box = Gtk.HBox(homogeneous=False) + self.box.show() + self.add(self.box) + self.label = Gtk.Label() self.label.set_ellipsize(Pango.EllipsizeMode.START) self.label.set_justify(Gtk.Justification.CENTER) self.label.show() - self.add(self.label) self.font = Pango.FontDescription(self.options.font_desc) self.update_colors() + self.update_mouse_enabled() self.set_size_request(0, 0) self.set_gravity(Gdk.Gravity.CENTER) @@ -111,6 +156,9 @@ class Screenkey(Gtk.Window): if visual is not None: self.set_visual(visual) + self.box.pack_start(self.img, expand=False, fill=True, padding=0) + self.box.pack_end(self.label, expand=False, fill=True, padding=0) + self.labelmngr = None self.enabled = True self.on_change_mode() @@ -177,6 +225,21 @@ class Screenkey(Gtk.Window): self.set_active_monitor(self.monitor) + def update_mouse_enabled(self): + if self.options.mouse: + if not self.button_pixbufs: + self.button_pixbufs = load_button_pixbufs( + Gdk.color_parse(self.options.font_color) + ) + self.img.show() + self.update_image_tag = GLib.idle_add(self.update_image) + else: + self.img.hide() + if self.update_image_tag is not None: + GLib.source_remove(self.update_image_tag) + self.update_image_tag = None + + def update_font(self): _, window_height = self.get_size() text = self.label.get_text() @@ -185,9 +248,59 @@ class Screenkey(Gtk.Window): self.label.get_pango_context().set_font_description(self.font) + def update_image(self): + if not self.button_pixbufs: + self.update_image_tag = None + return False + + pixbuf = self.button_pixbufs[0] + copied = False + + for index, button_state in enumerate(self.button_states): + if button_state is None: + continue + if button_state.pressed: + alpha = 255 + else: + if self.options.button_hide_duration > 0: + delta_time = (datetime.now() - button_state.stamp).total_seconds() + hide_time = delta_time / self.options.button_hide_duration + else: + hide_time = 1 + if hide_time < 1: + alpha = int(255 * (1 - hide_time)) + else: + self.button_states[index] = None + continue + + if not copied: + pixbuf = pixbuf.copy() + copied = True + self.button_pixbufs[button_state.btn].composite( + pixbuf, 0, 0, pixbuf.get_width(), pixbuf.get_height(), + 0, 0, 1, 1, + GdkPixbuf.InterpType.NEAREST, alpha + ) + + _, height = self.get_size() + scale = height / pixbuf.get_height() + if scale != 1: + width = int(pixbuf.get_width() * scale) + pixbuf = pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR) + self.img.set_from_pixbuf(pixbuf) + + if not copied: + self.update_image_tag = None + return False + return True + + def update_colors(self): - self.label.modify_fg(Gtk.StateFlags.NORMAL, Gdk.color_parse(self.options.font_color)) + font_color = Gdk.color_parse(self.options.font_color) + self.label.modify_fg(Gtk.StateFlags.NORMAL, font_color) self.bg_color = Gdk.color_parse(self.options.bg_color) + if self.options.mouse and self.button_pixbufs: + self.button_pixbufs = load_button_pixbufs(font_color) self.queue_draw() @@ -212,6 +325,7 @@ class Screenkey(Gtk.Window): self.label.set_padding(window_width // 100, 0) self.update_font() + self.update_image() def update_geometry(self, configure=False): @@ -283,6 +397,16 @@ class Screenkey(Gtk.Window): self.quit(exit_status=os.EX_SOFTWARE) + def timed_show(self): + if not self.get_property('visible'): + self.show() + if self.timer_hide: + self.timer_hide.cancel() + if self.options.timeout > 0 and not any(b and b.pressed for b in self.button_states): + self.timer_hide = Timer(self.options.timeout, self.on_timeout_main) + self.timer_hide.start() + + def on_label_change(self, markup, synthetic): if markup is None: self.on_labelmngr_error() @@ -293,13 +417,7 @@ class Screenkey(Gtk.Window): self.label.set_attributes(attr) self.update_font() - if not self.get_property('visible'): - self.show() - if self.timer_hide: - self.timer_hide.cancel() - if self.options.timeout > 0: - self.timer_hide = Timer(self.options.timeout, self.on_timeout_main) - self.timer_hide.start() + self.timed_show() if self.timer_min: self.timer_min.cancel() if not synthetic: @@ -307,6 +425,14 @@ class Screenkey(Gtk.Window): self.timer_min.start() + def on_image_change(self, button_state): + self.button_states[button_state.btn] = button_state + if self.options.mouse: + if not self.update_image_tag: + self.update_image_tag = GLib.idle_add(self.update_image) + self.timed_show() + + def on_timeout_main(self): if not self.options.persist: self.hide() @@ -322,7 +448,9 @@ class Screenkey(Gtk.Window): self.logger.debug("Restarting LabelManager.") if self.labelmngr: self.labelmngr.stop() - self.labelmngr = LabelManager(self.on_label_change, logger=self.logger, + self.labelmngr = LabelManager(self.on_label_change, + self.on_image_change, + logger=self.logger, key_mode=self.options.key_mode, bak_mode=self.options.bak_mode, mods_mode=self.options.mods_mode, @@ -506,6 +634,15 @@ class Screenkey(Gtk.Window): self.font = widget.props.font_desc self.update_font() + def on_cbox_mouse_changed(widget, data=None): + self.options.mouse = widget.get_active() + self.logger.debug("Mouse changed: %s." % self.options.mouse) + self.update_mouse_enabled() + + def on_sb_mouse_duration_changed(widget, data=None): + self.options.button_hide_duration = widget.get_value() + self.logger.debug("Button hide duration value changed: %f." % self.options.button_hide_duration) + frm_time = Gtk.Frame(label_widget=Gtk.Label("%s" % _("Time"), use_markup=True), border_width=4, @@ -723,6 +860,36 @@ class Screenkey(Gtk.Window): grid_color.attach_next_to(adj_scale, lbl_opacity, RIGHT, 1, 1) frm_color.add(grid_color) + frm_mouse = Gtk.Frame(label_widget=Gtk.Label("%s" % _("Mouse"), + use_markup=True), + border_width=4, + shadow_type=Gtk.ShadowType.NONE, + margin=6, hexpand=True) + vbox_mouse = Gtk.VBox(spacing=6) + + chk_mouse = Gtk.CheckButton(_("Show Mouse")) + chk_mouse.connect("toggled", on_cbox_mouse_changed) + chk_mouse.set_active(self.options.mouse) + vbox_mouse.pack_start(chk_mouse, expand=False, fill=True, padding=0) + + hbox_mouse = Gtk.HBox() + lbl_mouse1 = Gtk.Label(_("Hide duration")) + lbl_mouse2 = Gtk.Label(_("seconds")) + sb_mouse = Gtk.SpinButton(digits=1) + sb_mouse.set_increments(0.5, 1.0) + sb_mouse.set_range(0.0, 2.0) + sb_mouse.set_numeric(True) + sb_mouse.set_update_policy(Gtk.SpinButtonUpdatePolicy.IF_VALID) + sb_mouse.set_value(self.options.button_hide_duration) + sb_mouse.connect("value-changed", on_sb_mouse_duration_changed) + hbox_mouse.pack_start(lbl_mouse1, expand=False, fill=False, padding=6) + hbox_mouse.pack_start(sb_mouse, expand=False, fill=False, padding=4) + hbox_mouse.pack_start(lbl_mouse2, expand=False, fill=False, padding=4) + vbox_mouse.pack_start(hbox_mouse, expand=False, fill=False, padding=6) + + frm_mouse.add(vbox_mouse) + frm_mouse.show_all() + hbox_main = Gtk.Grid(column_homogeneous=True) vbox_main = Gtk.Grid(orientation=VERTICAL) vbox_main.add(frm_time) @@ -732,6 +899,7 @@ class Screenkey(Gtk.Window): vbox_main = Gtk.Grid(orientation=VERTICAL) vbox_main.add(frm_kbd) vbox_main.add(frm_color) + vbox_main.add(frm_mouse) hbox_main.add(vbox_main) box = prefs.get_content_area() diff --git a/images/mouse.original.svg b/images/mouse.original.svg new file mode 100644 index 0000000..39c9fa2 --- /dev/null +++ b/images/mouse.original.svg @@ -0,0 +1,167 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/mouse.svg b/images/mouse.svg new file mode 100644 index 0000000..99d2b8f --- /dev/null +++ b/images/mouse.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/screenkey b/screenkey index 6ff4847..620aec9 100755 --- a/screenkey +++ b/screenkey @@ -81,6 +81,10 @@ def main(): help=_("Ignore the specified KeySym")) ap.add_argument("--compr-cnt", type=int, metavar='COUNT', help=_("Compress key repeats after the specified count")) + ap.add_argument("-M", "--mouse", action="store_true", default=None, + help=_("show the mouse buttons")) + ap.add_argument("--mouse-fade", type=float, dest='button_hide_duration', + help=_("Mouse buttons fade duration in seconds")) args = ap.parse_args() # Set options @@ -88,7 +92,7 @@ def main(): for arg in ['timeout', 'position', 'persist', 'font_desc', 'font_color', 'bg_color', 'font_size', 'geometry', 'key_mode', 'bak_mode', 'mods_mode', 'mods_only', 'multiline', 'vis_shift', 'vis_space', 'screen', 'no_systray', - 'opacity', 'ignore', 'compr_cnt']: + 'opacity', 'ignore', 'compr_cnt', 'mouse', 'button_hide_duration']: if getattr(args, arg) is not None: options[arg] = getattr(args, arg) -- 2.26.2