Added double click support

This commit is contained in:
Nuno Cardoso 2015-11-08 15:12:59 +00:00 committed by enkore
parent 3d22043881
commit 87c01278f7
3 changed files with 224 additions and 33 deletions

View File

@ -1,6 +1,7 @@
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)
from i3pystatus.core.command import execute from i3pystatus.core.command import execute
from i3pystatus.core.command import run_through_shell from i3pystatus.core.command import run_through_shell
@ -14,6 +15,11 @@ class Module(SettingsBase):
('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_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`)"),
('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`)"),
) )
@ -21,11 +27,23 @@ class Module(SettingsBase):
on_rightclick = None on_rightclick = None
on_upscroll = None on_upscroll = None
on_downscroll = None on_downscroll = None
on_doubleleftclick = None
on_doublerightclick = None
on_doubleupscroll = None
on_doubledownscroll = None
multi_click_timeout = 0.25
hints = {"markup": "none"} hints = {"markup": "none"}
def __init__(self, *args, **kwargs):
super(Module, self).__init__(*args, **kwargs)
self.__multi_click = MultiClickHandler(self.__button_callback_handler,
self.multi_click_timeout)
def registered(self, status_handler): def registered(self, status_handler):
"""Called when this module is registered with a status handler""" """Called when this module is registered with a status handler"""
self.__status_handler = status_handler
def inject(self, json): def inject(self, json):
if self.output: if self.output:
@ -46,6 +64,39 @@ class Module(SettingsBase):
def run(self): def run(self):
pass pass
def __log_button_event(self, button, cb, args, action):
msg = "{}: button={}, cb='{}', args={}, type='{}'".format(
self.__name__, button, cb, args, action)
self.logger.debug(msg)
def __button_callback_handler(self, button, cb):
if not cb:
self.__log_button_event(button, None, None,
"No callback attached")
return False
if isinstance(cb, list):
cb, args = (cb[0], cb[1:])
else:
args = []
if callable(cb):
self.__log_button_event(button, cb, args, "Python callback")
cb(self, *args)
elif hasattr(self, cb):
if cb is not "run":
self.__log_button_event(button, cb, args, "Member callback")
getattr(self, cb)(*args)
else:
self.__log_event(button, cb, args, "External command")
execute(cb, detach=True)
# Notify status handler
try:
self.__status_handler.io.async_refresh()
except:
pass
def on_click(self, button): def on_click(self, button):
""" """
Maps a click event with its associated callback. Maps a click event with its associated callback.
@ -83,46 +134,38 @@ class Module(SettingsBase):
""" """
def log_event(name, button, cb, args, action):
msg = "{}: button={}, cb='{}', args={}, type='{}'".format(
name, button, cb, args, action)
self.logger.debug(msg)
def split_callback_and_args(cb):
if isinstance(cb, list):
return cb[0], cb[1:]
else:
return cb, []
cb = None
if button == 1: # Left mouse button if button == 1: # Left mouse button
cb = self.on_leftclick action = 'leftclick'
elif button == 3: # Right mouse button elif button == 3: # Right mouse button
cb = self.on_rightclick action = 'rightclick'
elif button == 4: # mouse wheel up elif button == 4: # mouse wheel up
cb = self.on_upscroll action = 'upscroll'
elif button == 5: # mouse wheel down elif button == 5: # mouse wheel down
cb = self.on_downscroll action = 'downscroll'
else: else:
log_event(self.__name__, button, None, None, "Unhandled button") self.__log_button_event(button, None, None, "Unhandled button")
return False return False
if not cb: m_click = self.__multi_click
log_event(self.__name__, button, None, None, "No callback attached")
return False with m_click.lock:
else: double = m_click.check_double(button)
cb, args = split_callback_and_args(cb) double_action = 'double%s' % action
if double:
action = double_action
# 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)
if delay_execution:
m_click.set_timer(button, cb)
else:
self.__button_callback_handler(button, cb)
if callable(cb):
log_event(self.__name__, button, cb, args, "Python callback")
cb(self, *args)
elif hasattr(self, cb):
if cb is not "run":
log_event(self.__name__, button, cb, args, "Member callback")
getattr(self, cb)(*args)
else:
log_event(self.__name__, button, cb, args, "External command")
execute(cb, detach=True)
return True return True
def move(self, position): def move(self, position):
@ -162,6 +205,7 @@ class IntervalModule(Module):
managers = {} managers = {}
def registered(self, status_handler): def registered(self, status_handler):
super(IntervalModule, self).registered(status_handler)
if self.interval in IntervalModule.managers: if self.interval in IntervalModule.managers:
IntervalModule.managers[self.interval].append(self) IntervalModule.managers[self.interval].append(self)
else: else:

View File

@ -4,6 +4,8 @@ import re
import socket import socket
import string import string
from threading import Timer, RLock
def lchop(string, prefix): def lchop(string, prefix):
"""Removes a prefix from string """Removes a prefix from string
@ -499,3 +501,60 @@ def user_open(url_or_command):
else: else:
import subprocess import subprocess
subprocess.Popen(url_or_command, shell=True) subprocess.Popen(url_or_command, shell=True)
class MultiClickHandler(object):
def __init__(self, callback_handler, timeout):
self.callback_handler = callback_handler
self.timeout = timeout
self.lock = RLock()
self._timer_id = 0
self.timer = None
self.button = None
self.cb = None
def set_timer(self, button, cb):
with self.lock:
self.clear_timer()
self.timer = Timer(self.timeout,
self._timer_function,
args=[self._timer_id])
self.button = button
self.cb = cb
self.timer.start()
def clear_timer(self):
with self.lock:
if self.timer is None:
return
self._timer_id += 1 # Invalidate existent timer
self.timer.cancel() # Cancel the existent timer
self.timer = None
self.button = None
self.cb = None
def _timer_function(self, timer_id):
with self.lock:
if self._timer_id != timer_id:
return
self.callback_handler(self.button, self.cb)
self.clear_timer()
def check_double(self, button):
if self.timer is None:
return False
ret = True
if button != self.button:
self.callback_handler(self.button, self.cb)
ret = False
self.clear_timer()
return ret

View File

@ -0,0 +1,88 @@
import pytest
from i3pystatus import IntervalModule
import time
left_click = 1
right_click = 3
scroll_up = 4
scroll_down = 5
@pytest.mark.parametrize("events, expected", [
# Fast click
(((0, left_click),),
'no action'),
# Slow click
(((0.4, left_click),),
'leftclick'),
# Slow double click
(((0.4, left_click),
(0.4, left_click),),
'leftclick'),
# Fast double click
(((0.2, left_click),
(0, left_click),),
'doubleleftclick'),
# Fast double click + Slow click
(((0.2, left_click),
(0, left_click),
(0.3, left_click),),
'leftclick'),
# Alternate double click
(((0.2, left_click),
(0, right_click),),
'leftclick'),
# Slow click, no callback
(((0.4, right_click),),
'no action'),
# Fast double click
(((0.2, right_click),
(0, right_click),),
'doublerightclick'),
# Fast double click
(((0, scroll_down),
(0, scroll_down),),
'downscroll'),
# Slow click
(((0.4, scroll_up),),
'upscroll'),
# Fast double click
(((0, scroll_up),
(0, scroll_up),),
'doubleupscroll'),
])
def test_clicks(events, expected):
class TestClicks(IntervalModule):
def set_action(self, action):
self._action = action
on_leftclick = [set_action, "leftclick"]
on_doubleleftclick = [set_action, "doubleleftclick"]
# on_rightclick = [set_action, "rightclick"]
on_doublerightclick = [set_action, "doublerightclick"]
on_upscroll = [set_action, "upscroll"]
on_doubleupscroll = [set_action, "doubleupscroll"]
on_downscroll = [set_action, "downscroll"]
# on_doubledownscroll = [set_action, "doubledownscroll"]
_action = 'no action'
m = TestClicks()
for sl, ev in events:
m.on_click(ev)
time.sleep(sl)
assert m._action == expected