Merge pull request #339 from hasB4K/pullrequest-mouse_pos

core: Change command_endpoint and on_click for supporting i3bar mouse positions
This commit is contained in:
enkore 2016-03-26 21:20:44 +01:00
commit 8400d1a53a
4 changed files with 102 additions and 52 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,19 @@ 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"]):
button = cmd["button"]
kwargs = {"button_id": button}
try:
kwargs.update({"pos_x": cmd["x"],
"pos_y": cmd["y"]})
except Exception:
continue
if target_module:
target_module.on_click(button, **kwargs)
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):
@ -26,26 +26,36 @@ class Module(SettingsBase):
settings = ( settings = (
('on_leftclick', "Callback called on left click (see :ref:`callbacks`)"), ('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_rightclick', "Callback called on right click (see :ref:`callbacks`)"),
('on_upscroll', "Callback called on scrolling up (see :ref:`callbacks`)"), ('on_upscroll', "Callback called on scrolling up (see :ref:`callbacks`)"),
('on_downscroll', "Callback called on scrolling down (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_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_doublerightclick', "Callback called on double right click (see :ref:`callbacks`)"),
('on_doubleupscroll', "Callback called on double scroll up (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_doubledownscroll', "Callback called on double scroll down (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."), ('multi_click_timeout', "Time (in seconds) before a single click is executed."),
('hints', "Additional output blocks for module output (see :ref:`hints`)"), ('hints', "Additional output blocks for module output (see :ref:`hints`)"),
) )
on_leftclick = None on_leftclick = None
on_middleclick = None
on_rightclick = None on_rightclick = None
on_upscroll = None on_upscroll = None
on_downscroll = None on_downscroll = None
on_doubleleftclick = None on_doubleleftclick = None
on_doublemiddleclick = None
on_doublerightclick = None on_doublerightclick = None
on_doubleupscroll = None on_doubleupscroll = None
on_doubledownscroll = None on_doubledownscroll = None
on_otherclick = None
on_doubleotherclick = None
multi_click_timeout = 0.25 multi_click_timeout = 0.25
hints = {"markup": "none"} hints = {"markup": "none"}
@ -78,15 +88,37 @@ 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
tmp_cb = wrapped_cb
else:
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.
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 +129,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,20 +165,22 @@ 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.
Currently implemented events are: Currently implemented events are:
=========== ================ ========= ============ ================ =========
Event Callback setting Button ID Event Callback setting Button ID
=========== ================ ========= ============ ================ =========
Left click on_leftclick 1 Left click on_leftclick 1
Middle click on_middleclick 2
Right click on_rightclick 3 Right click on_rightclick 3
Scroll up on_upscroll 4 Scroll up on_upscroll 4
Scroll down on_downscroll 5 Scroll down on_downscroll 5
=========== ================ ========= Others on_otherclick > 5
============ ================ =========
The action is determined by the nature (type and value) of the callback The action is determined by the nature (type and value) of the callback
setting in the following order: setting in the following order:
@ -144,8 +188,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,24 +197,19 @@ 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
""" """
if button == 1: # Left mouse button actions = ['leftclick', 'middleclick', 'rightclick',
action = 'leftclick' 'upscroll', 'downscroll']
elif button == 3: # Right mouse button try:
action = 'rightclick' action = actions[button - 1]
elif button == 4: # mouse wheel up except (TypeError, IndexError):
action = 'upscroll' self.__log_button_event(button, None, None, "Other button")
elif button == 5: # mouse wheel down action = "otherclick"
action = 'downscroll'
else:
self.__log_button_event(button, None, None, "Unhandled button")
return False
m_click = self.__multi_click m_click = self.__multi_click
@ -184,15 +223,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
def move(self, position): def move(self, position):
self.position = position self.position = position

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()