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 <mathis.felardos@gmail.com>
This commit is contained in:
Mathis FELARDOS 2016-03-23 08:00:59 +01:00
parent 0b49c4058a
commit d15b3173f1
4 changed files with 67 additions and 29 deletions

View File

@ -39,6 +39,7 @@ Kenneth Lyons
krypt-n krypt-n
Lukáš Mandák Lukáš Mandák
Łukasz Jędrzejewski Łukasz Jędrzejewski
Mathis Felardos
Matthias Pronk Matthias Pronk
Matthieu Coudron Matthieu Coudron
Matus Telgarsky Matus Telgarsky

View File

@ -29,9 +29,16 @@ class CommandEndpoint:
self.thread.start() self.thread.start()
def _command_endpoint(self): def _command_endpoint(self):
for command in self.io_handler_factory().read(): for cmd in self.io_handler_factory().read():
target_module = self.modules.get(command["instance"]) target_module = self.modules.get(cmd["instance"])
if target_module and target_module.on_click(command["button"]):
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() target_module.run()
self.io.async_refresh() self.io.async_refresh()

View File

@ -1,11 +1,11 @@
import inspect import inspect
import traceback
from i3pystatus.core.settings import SettingsBase from i3pystatus.core.settings import SettingsBase
from i3pystatus.core.threading import Manager from i3pystatus.core.threading import Manager
from i3pystatus.core.util import (convert_position, from i3pystatus.core.util import (convert_position,
MultiClickHandler) MultiClickHandler)
from i3pystatus.core.command import execute from i3pystatus.core.command import execute
from i3pystatus.core.command import run_through_shell
def is_method_of(method, object): def is_method_of(method, object):
@ -78,15 +78,32 @@ class Module(SettingsBase):
def run(self): def run(self):
pass pass
def __log_button_event(self, button, cb, args, action): def __log_button_event(self, button, cb, args, action, **kwargs):
msg = "{}: button={}, cb='{}', args={}, type='{}'".format( msg = "{}: button={}, cb='{}', args={}, kwargs={}, type='{}'".format(
self.__name__, button, cb, args, action) self.__name__, button, cb, args, kwargs, action)
self.logger.debug(msg) 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: if not cb:
self.__log_button_event(button, None, None, self.__log_button_event(button, None, None,
"No callback attached") "No callback attached", **kwargs)
return False return False
if isinstance(cb, list): if isinstance(cb, list):
@ -97,25 +114,35 @@ class Module(SettingsBase):
try: try:
our_method = is_method_of(cb, self) our_method = is_method_of(cb, self)
if callable(cb) and not our_method: if callable(cb) and not our_method:
self.__log_button_event(button, cb, args, "Python callback") self.__log_button_event(button, cb, args,
cb(*args) "Python callback", **kwargs)
call_callback(cb, *args, **kwargs)
elif our_method: 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): elif hasattr(self, cb):
if cb is not "run": if cb is not "run":
# CommandEndpoint already calls run() after every # CommandEndpoint already calls run() after every
# callback to instantly update any changed state due # callback to instantly update any changed state due
# to the callback's actions. # to the callback's actions.
self.__log_button_event(button, cb, args, "Member callback") self.__log_button_event(button, cb, args,
getattr(self, cb)(*args) "Member callback", **kwargs)
call_callback(getattr(self, cb), *args, **kwargs)
else: else:
self.__log_button_event(button, cb, args, "External command") self.__log_button_event(button, cb, args,
"External command", **kwargs)
if hasattr(self, "data"): if hasattr(self, "data"):
args = [arg.format(**self.data) for arg in args] kwargs.update(self.data)
cb = cb.format(**self.data)
args = [str(arg).format(**kwargs) for arg in args]
cb = cb.format(**kwargs)
execute(cb + " " + " ".join(args), detach=True) execute(cb + " " + " ".join(args), detach=True)
except Exception as e: 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 # Notify status handler
try: try:
@ -123,7 +150,7 @@ class Module(SettingsBase):
except: except:
pass pass
def on_click(self, button): def on_click(self, button, **kwargs):
""" """
Maps a click event with its associated callback. Maps a click event with its associated callback.
@ -144,8 +171,8 @@ class Module(SettingsBase):
1. If null callback (``None``), no action is taken. 1. If null callback (``None``), no action is taken.
2. If it's a `python function`, call it and pass any additional 2. If it's a `python function`, call it and pass any additional
arguments. arguments.
3. If it's name of a `member method` of current module (string), call it 3. If it's name of a `member method` of current module (string), call
and pass any additional arguments. it and pass any additional arguments.
4. If the name does not match with `member method` name execute program 4. If the name does not match with `member method` name execute program
with such name. with such name.
@ -153,7 +180,8 @@ class Module(SettingsBase):
callback settings and examples. callback settings and examples.
:param button: The ID of button event received from i3bar. :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. :return: Returns ``True`` if a valid callback action was executed.
``False`` otherwise. ``False`` otherwise.
:rtype: bool :rtype: bool
@ -184,13 +212,13 @@ class Module(SettingsBase):
# Get callback function # Get callback function
cb = getattr(self, 'on_%s' % action, None) cb = getattr(self, 'on_%s' % action, None)
has_double_handler = getattr(self, 'on_%s' % double_action, None) is not None double_handler = getattr(self, 'on_%s' % double_action, None)
delay_execution = (not double and has_double_handler) delay_execution = (not double and double_handler)
if delay_execution: if delay_execution:
m_click.set_timer(button, cb) m_click.set_timer(button, cb, **kwargs)
else: else:
self.__button_callback_handler(button, cb) self.__button_callback_handler(button, cb, **kwargs)
return True return True

View File

@ -514,8 +514,9 @@ class MultiClickHandler(object):
self.timer = None self.timer = None
self.button = None self.button = None
self.cb = None self.cb = None
self.kwargs = None
def set_timer(self, button, cb): def set_timer(self, button, cb, **kwargs):
with self.lock: with self.lock:
self.clear_timer() self.clear_timer()
@ -524,6 +525,7 @@ class MultiClickHandler(object):
args=[self._timer_id]) args=[self._timer_id])
self.button = button self.button = button
self.cb = cb self.cb = cb
self.kwargs = kwargs
self.timer.start() self.timer.start()
@ -544,7 +546,7 @@ class MultiClickHandler(object):
with self.lock: with self.lock:
if self._timer_id != timer_id: if self._timer_id != timer_id:
return return
self.callback_handler(self.button, self.cb) self.callback_handler(self.button, self.cb, **self.kwargs)
self.clear_timer() self.clear_timer()
def check_double(self, button): def check_double(self, button):
@ -553,7 +555,7 @@ class MultiClickHandler(object):
ret = True ret = True
if button != self.button: if button != self.button:
self.callback_handler(self.button, self.cb) self.callback_handler(self.button, self.cb, **self.kwargs)
ret = False ret = False
self.clear_timer() self.clear_timer()