From d15b3173f1564bf1fc7e091cfa5cfa47e0ebefbd Mon Sep 17 00:00:00 2001 From: Mathis FELARDOS Date: Wed, 23 Mar 2016 08:00:59 +0100 Subject: [PATCH 1/4] core: Change command_endpoint and on_click for supporting i3bar mouse positions * command_endpoint: get the position from the mouse when the click occured. Parameters names are set here: pos_x pos_y. Positions are passed to on_click through keyword arguments. * Module: - change __log_button_event, __button_callback_handler and on_click methods for handling keyword arguments. - "Member", "Method" and "Python" callbacks are handled by detecting if they have pos_x or pos_y as parameters, or if they have a keyword arguments. The special case of wrapped callbacks (made with get_module decorator for example) is handled in a similar way. - "External command" is handled by considering the position as a format dictionary. Actually no distinctions are made of how self.data and the new keyword argument are treated on this. - the parameter kwargs as been added to the doc string of on_click. * MultiClickHandler: now handle keyword arguments. Signed-off-by: Mathis FELARDOS --- CONTRIBUTORS | 1 + i3pystatus/core/__init__.py | 13 +++++-- i3pystatus/core/modules.py | 74 +++++++++++++++++++++++++------------ i3pystatus/core/util.py | 8 ++-- 4 files changed, 67 insertions(+), 29 deletions(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 66e1301..22d9d7a 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -39,6 +39,7 @@ Kenneth Lyons krypt-n Lukáš Mandák Łukasz Jędrzejewski +Mathis Felardos Matthias Pronk Matthieu Coudron Matus Telgarsky diff --git a/i3pystatus/core/__init__.py b/i3pystatus/core/__init__.py index 16e3bd3..5800500 100644 --- a/i3pystatus/core/__init__.py +++ b/i3pystatus/core/__init__.py @@ -29,9 +29,16 @@ class CommandEndpoint: self.thread.start() def _command_endpoint(self): - for command in self.io_handler_factory().read(): - target_module = self.modules.get(command["instance"]) - if target_module and target_module.on_click(command["button"]): + for cmd in self.io_handler_factory().read(): + target_module = self.modules.get(cmd["instance"]) + + try: + pos = {"pos_x": int(cmd["x"]), + "pos_y": int(cmd["y"])} + except Exception: + pos = {} + + if target_module and target_module.on_click(cmd["button"], **pos): target_module.run() self.io.async_refresh() diff --git a/i3pystatus/core/modules.py b/i3pystatus/core/modules.py index 952b221..1461d5b 100644 --- a/i3pystatus/core/modules.py +++ b/i3pystatus/core/modules.py @@ -1,11 +1,11 @@ import inspect +import traceback from i3pystatus.core.settings import SettingsBase from i3pystatus.core.threading import Manager from i3pystatus.core.util import (convert_position, MultiClickHandler) from i3pystatus.core.command import execute -from i3pystatus.core.command import run_through_shell def is_method_of(method, object): @@ -78,15 +78,32 @@ class Module(SettingsBase): def run(self): pass - def __log_button_event(self, button, cb, args, action): - msg = "{}: button={}, cb='{}', args={}, type='{}'".format( - self.__name__, button, cb, args, action) + def __log_button_event(self, button, cb, args, action, **kwargs): + msg = "{}: button={}, cb='{}', args={}, kwargs={}, type='{}'".format( + self.__name__, button, cb, args, kwargs, action) self.logger.debug(msg) - def __button_callback_handler(self, button, cb): + def __button_callback_handler(self, button, cb, **kwargs): + + def call_callback(cb, *args, **kwargs): + # Recover the function if wrapped (with get_module for example) + wrapped_cb = getattr(cb, "__wrapped__", None) + if wrapped_cb: + locals()["self"] = self # Add self to the local stack frame + args_spec = inspect.getargspec(wrapped_cb) + else: + args_spec = inspect.getargspec(cb) + + # Remove all variables present in kwargs that are not used in the + # callback, except if there is a keyword argument. + if not args_spec.keywords: + kwargs = {k: v for k, v in kwargs.items() + if k in args_spec.args} + cb(*args, **kwargs) + if not cb: self.__log_button_event(button, None, None, - "No callback attached") + "No callback attached", **kwargs) return False if isinstance(cb, list): @@ -97,25 +114,35 @@ class Module(SettingsBase): try: our_method = is_method_of(cb, self) if callable(cb) and not our_method: - self.__log_button_event(button, cb, args, "Python callback") - cb(*args) + self.__log_button_event(button, cb, args, + "Python callback", **kwargs) + call_callback(cb, *args, **kwargs) elif our_method: - cb(self, *args) + self.__log_button_event(button, cb, args, + "Method callback", **kwargs) + call_callback(cb, self, *args, **kwargs) elif hasattr(self, cb): if cb is not "run": # CommandEndpoint already calls run() after every # callback to instantly update any changed state due # to the callback's actions. - self.__log_button_event(button, cb, args, "Member callback") - getattr(self, cb)(*args) + self.__log_button_event(button, cb, args, + "Member callback", **kwargs) + call_callback(getattr(self, cb), *args, **kwargs) else: - self.__log_button_event(button, cb, args, "External command") + self.__log_button_event(button, cb, args, + "External command", **kwargs) + if hasattr(self, "data"): - args = [arg.format(**self.data) for arg in args] - cb = cb.format(**self.data) + kwargs.update(self.data) + + args = [str(arg).format(**kwargs) for arg in args] + cb = cb.format(**kwargs) execute(cb + " " + " ".join(args), detach=True) except Exception as e: - self.logger.critical("Exception while processing button callback: {!r}".format(e)) + self.logger.critical("Exception while processing button " + "callback: {!r}".format(e)) + self.logger.critical(traceback.format_exc()) # Notify status handler try: @@ -123,7 +150,7 @@ class Module(SettingsBase): except: pass - def on_click(self, button): + def on_click(self, button, **kwargs): """ Maps a click event with its associated callback. @@ -144,8 +171,8 @@ class Module(SettingsBase): 1. If null callback (``None``), no action is taken. 2. If it's a `python function`, call it and pass any additional arguments. - 3. If it's name of a `member method` of current module (string), call it - and pass any additional arguments. + 3. If it's name of a `member method` of current module (string), call + it and pass any additional arguments. 4. If the name does not match with `member method` name execute program with such name. @@ -153,7 +180,8 @@ class Module(SettingsBase): callback settings and examples. :param button: The ID of button event received from i3bar. - :type button: int + :param kwargs: Further information received from i3bar like the + positions of the mouse where the click occured. :return: Returns ``True`` if a valid callback action was executed. ``False`` otherwise. :rtype: bool @@ -184,13 +212,13 @@ class Module(SettingsBase): # Get callback function cb = getattr(self, 'on_%s' % action, None) - has_double_handler = getattr(self, 'on_%s' % double_action, None) is not None - delay_execution = (not double and has_double_handler) + double_handler = getattr(self, 'on_%s' % double_action, None) + delay_execution = (not double and double_handler) if delay_execution: - m_click.set_timer(button, cb) + m_click.set_timer(button, cb, **kwargs) else: - self.__button_callback_handler(button, cb) + self.__button_callback_handler(button, cb, **kwargs) return True diff --git a/i3pystatus/core/util.py b/i3pystatus/core/util.py index a3c40bd..d9b4d37 100644 --- a/i3pystatus/core/util.py +++ b/i3pystatus/core/util.py @@ -514,8 +514,9 @@ class MultiClickHandler(object): self.timer = None self.button = None self.cb = None + self.kwargs = None - def set_timer(self, button, cb): + def set_timer(self, button, cb, **kwargs): with self.lock: self.clear_timer() @@ -524,6 +525,7 @@ class MultiClickHandler(object): args=[self._timer_id]) self.button = button self.cb = cb + self.kwargs = kwargs self.timer.start() @@ -544,7 +546,7 @@ class MultiClickHandler(object): with self.lock: if self._timer_id != timer_id: return - self.callback_handler(self.button, self.cb) + self.callback_handler(self.button, self.cb, **self.kwargs) self.clear_timer() def check_double(self, button): @@ -553,7 +555,7 @@ class MultiClickHandler(object): ret = True if button != self.button: - self.callback_handler(self.button, self.cb) + self.callback_handler(self.button, self.cb, **self.kwargs) ret = False self.clear_timer() From 98e8a1cc048fda88b800663a60229a57852432db Mon Sep 17 00:00:00 2001 From: Mathis FELARDOS Date: Wed, 23 Mar 2016 10:14:14 +0100 Subject: [PATCH 2/4] core: handle callbacks that are not functions on Python 3.3 * Fix inspect.getargspec issue for non functions callbacks by creating an empty ArgSpec. There for we ignore all kwargs parameters. Signed-off-by: Mathis FELARDOS --- i3pystatus/core/modules.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/i3pystatus/core/modules.py b/i3pystatus/core/modules.py index 1461d5b..d32a491 100644 --- a/i3pystatus/core/modules.py +++ b/i3pystatus/core/modules.py @@ -90,9 +90,14 @@ class Module(SettingsBase): wrapped_cb = getattr(cb, "__wrapped__", None) if wrapped_cb: locals()["self"] = self # Add self to the local stack frame - args_spec = inspect.getargspec(wrapped_cb) + tmp_cb = wrapped_cb else: - args_spec = inspect.getargspec(cb) + tmp_cb = cb + + try: + args_spec = inspect.getargspec(tmp_cb) + except Exception: + args_spec = inspect.ArgSpec([], None, None, None) # Remove all variables present in kwargs that are not used in the # callback, except if there is a keyword argument. From 95f625cd6bb13abaa65a96e03c8607a81416870e Mon Sep 17 00:00:00 2001 From: Mathis FELARDOS Date: Fri, 25 Mar 2016 04:32:29 +0100 Subject: [PATCH 3/4] core: Add the middle click support and unhandled button support * This commit fix #259 * Support of middle click button * Add an unhandled click events for all button that will not be handled * Remove the return type of on_click: it became useless now * Fix the unique call of on_click in CommandEndpoint Signed-off-by: Mathis FELARDOS --- i3pystatus/core/__init__.py | 3 ++- i3pystatus/core/modules.py | 30 ++++++++++++++++-------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/i3pystatus/core/__init__.py b/i3pystatus/core/__init__.py index 5800500..f0c7173 100644 --- a/i3pystatus/core/__init__.py +++ b/i3pystatus/core/__init__.py @@ -38,7 +38,8 @@ class CommandEndpoint: except Exception: pos = {} - if target_module and target_module.on_click(cmd["button"], **pos): + if target_module: + target_module.on_click(cmd["button"], **pos) target_module.run() self.io.async_refresh() diff --git a/i3pystatus/core/modules.py b/i3pystatus/core/modules.py index d32a491..cb29164 100644 --- a/i3pystatus/core/modules.py +++ b/i3pystatus/core/modules.py @@ -26,26 +26,36 @@ class Module(SettingsBase): settings = ( ('on_leftclick', "Callback called on left click (see :ref:`callbacks`)"), + ('on_middleclick', "Callback called on middle click (see :ref:`callbacks`)"), ('on_rightclick', "Callback called on right click (see :ref:`callbacks`)"), ('on_upscroll', "Callback called on scrolling up (see :ref:`callbacks`)"), ('on_downscroll', "Callback called on scrolling down (see :ref:`callbacks`)"), ('on_doubleleftclick', "Callback called on double left click (see :ref:`callbacks`)"), + ('on_doubleleftclick', "Callback called on double left click (see :ref:`callbacks`)"), + ('on_doublemiddleclick', "Callback called on double middle click (see :ref:`callbacks`)"), ('on_doublerightclick', "Callback called on double right click (see :ref:`callbacks`)"), ('on_doubleupscroll', "Callback called on double scroll up (see :ref:`callbacks`)"), ('on_doubledownscroll', "Callback called on double scroll down (see :ref:`callbacks`)"), + ('on_unhandledclick', "Callback called on unhandled click (see :ref:`callbacks`)"), + ('on_doubleunhandledclick', "Callback called on double unhandled click (see :ref:`callbacks`)"), ('multi_click_timeout', "Time (in seconds) before a single click is executed."), ('hints', "Additional output blocks for module output (see :ref:`hints`)"), ) on_leftclick = None + on_middleclick = None on_rightclick = None on_upscroll = None on_downscroll = None on_doubleleftclick = None + on_doublemiddleclick = None on_doublerightclick = None on_doubleupscroll = None on_doubledownscroll = None + on_unhandledclick = None + on_doubleunhandledclick = None + multi_click_timeout = 0.25 hints = {"markup": "none"} @@ -189,21 +199,15 @@ class Module(SettingsBase): positions of the mouse where the click occured. :return: Returns ``True`` if a valid callback action was executed. ``False`` otherwise. - :rtype: bool - """ - if button == 1: # Left mouse button - action = 'leftclick' - elif button == 3: # Right mouse button - action = 'rightclick' - elif button == 4: # mouse wheel up - action = 'upscroll' - elif button == 5: # mouse wheel down - action = 'downscroll' - else: + actions = ['leftclick', 'middleclick', 'rightclick', + 'upscroll', 'downscroll'] + try: + action = actions[button - 1] + except (TypeError, IndexError): self.__log_button_event(button, None, None, "Unhandled button") - return False + action = "unhandled" m_click = self.__multi_click @@ -225,8 +229,6 @@ class Module(SettingsBase): else: self.__button_callback_handler(button, cb, **kwargs) - return True - def move(self, position): self.position = position return self From f26a9f9d1d9bf910fd97a8512ee2b516f6db875b Mon Sep 17 00:00:00 2001 From: Mathis FELARDOS Date: Fri, 25 Mar 2016 22:18:14 +0100 Subject: [PATCH 4/4] core: Improve the support of other button * This commit fix #259 * Change 'unhandled' callback by 'other' * Add the an optional parameter 'button_id' for all callbacks Signed-off-by: Mathis FELARDOS --- i3pystatus/core/__init__.py | 10 ++++++---- i3pystatus/core/modules.py | 30 ++++++++++++++++-------------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/i3pystatus/core/__init__.py b/i3pystatus/core/__init__.py index f0c7173..4bd54a6 100644 --- a/i3pystatus/core/__init__.py +++ b/i3pystatus/core/__init__.py @@ -32,14 +32,16 @@ class CommandEndpoint: for cmd in self.io_handler_factory().read(): target_module = self.modules.get(cmd["instance"]) + button = cmd["button"] + kwargs = {"button_id": button} try: - pos = {"pos_x": int(cmd["x"]), - "pos_y": int(cmd["y"])} + kwargs.update({"pos_x": cmd["x"], + "pos_y": cmd["y"]}) except Exception: - pos = {} + continue if target_module: - target_module.on_click(cmd["button"], **pos) + target_module.on_click(button, **kwargs) target_module.run() self.io.async_refresh() diff --git a/i3pystatus/core/modules.py b/i3pystatus/core/modules.py index cb29164..41a5936 100644 --- a/i3pystatus/core/modules.py +++ b/i3pystatus/core/modules.py @@ -36,8 +36,8 @@ class Module(SettingsBase): ('on_doublerightclick', "Callback called on double right click (see :ref:`callbacks`)"), ('on_doubleupscroll', "Callback called on double scroll up (see :ref:`callbacks`)"), ('on_doubledownscroll', "Callback called on double scroll down (see :ref:`callbacks`)"), - ('on_unhandledclick', "Callback called on unhandled click (see :ref:`callbacks`)"), - ('on_doubleunhandledclick', "Callback called on double unhandled click (see :ref:`callbacks`)"), + ('on_otherclick', "Callback called on other click (see :ref:`callbacks`)"), + ('on_doubleotherclick', "Callback called on double other click (see :ref:`callbacks`)"), ('multi_click_timeout', "Time (in seconds) before a single click is executed."), ('hints', "Additional output blocks for module output (see :ref:`hints`)"), ) @@ -53,8 +53,8 @@ class Module(SettingsBase): on_doubleupscroll = None on_doubledownscroll = None - on_unhandledclick = None - on_doubleunhandledclick = None + on_otherclick = None + on_doubleotherclick = None multi_click_timeout = 0.25 @@ -171,14 +171,16 @@ class Module(SettingsBase): Currently implemented events are: - =========== ================ ========= - Event Callback setting Button ID - =========== ================ ========= - Left click on_leftclick 1 - Right click on_rightclick 3 - Scroll up on_upscroll 4 - Scroll down on_downscroll 5 - =========== ================ ========= + ============ ================ ========= + Event Callback setting Button ID + ============ ================ ========= + Left click on_leftclick 1 + Middle click on_middleclick 2 + Right click on_rightclick 3 + Scroll up on_upscroll 4 + Scroll down on_downscroll 5 + Others on_otherclick > 5 + ============ ================ ========= The action is determined by the nature (type and value) of the callback setting in the following order: @@ -206,8 +208,8 @@ class Module(SettingsBase): try: action = actions[button - 1] except (TypeError, IndexError): - self.__log_button_event(button, None, None, "Unhandled button") - action = "unhandled" + self.__log_button_event(button, None, None, "Other button") + action = "otherclick" m_click = self.__multi_click