[Sugar-devel] [PATCH sugar-toolkit PoC v2] Tap&hold for Palettes

Sascha Silbe silbe at activitycentral.com
Fri Nov 4 06:15:38 EDT 2011


Most touch screens only have a single kind of action, no direct equivalent to
the secondary (even n-ary) actions ("right-click") of standard PC mice.

Tap&hold is a well-established way on touch screen oriented user interfaces
of getting responses similar to what right click would do on WIMP interaces.

This patch is a hack to add tap&hold support to Sugar palettes for evaluation
purposes. The real thing (TM) should hook into GTK instead in order to make
it work beyond just Sugar palettes.

Signed-off-by: Sascha Silbe <silbe at activitycentral.com>
---
v1->v2: Added previously uncommitted fixes to make it actually work. Sorry.

 src/sugar/graphics/palettewindow.py |   89 ++++++++++++++++++++++++++++-------
 1 files changed, 71 insertions(+), 18 deletions(-)

diff --git a/src/sugar/graphics/palettewindow.py b/src/sugar/graphics/palettewindow.py
index 5281e54..4955966 100644
--- a/src/sugar/graphics/palettewindow.py
+++ b/src/sugar/graphics/palettewindow.py
@@ -22,6 +22,7 @@ STABLE.
 """

 import logging
+import time

 import gtk
 import gobject
@@ -32,6 +33,12 @@ from sugar.graphics import animator
 from sugar.graphics import style


+# Shorter than 350ms is considered a tap, longer than 1s hold. Anything in
+# between is ignored as ambiguous.
+_TAP_TIMEOUT_MS = 350
+_HOLD_TIMEOUT_MS = 1000
+
+
 def _calculate_gap(a, b):
     """Helper function to find the gap position and size of widget a"""
     # Test for each side if the palette and invoker are
@@ -460,6 +467,8 @@ class Invoker(gobject.GObject):
         self._cursor_y = -1
         self._palette = None
         self._cache_palette = True
+        self._last_tap = None
+        self._hold_timeout_hid = None

     def attach(self, parent):
         self.parent = parent
@@ -630,6 +639,10 @@ class Invoker(gobject.GObject):
         self.emit('mouse-enter')

     def notify_mouse_leave(self):
+        if self._last_tap is not None:
+            self._last_tap = None
+            gobject.source_remove(self._hold_timeout_hid)
+
         self.emit('mouse-leave')

     def notify_right_click(self):
@@ -676,6 +689,40 @@ class Invoker(gobject.GObject):
         if not self.props.cache_palette:
             self.set_palette(None)

+    def _button_release_event_cb(self, widget, event):
+        logging.debug('_button_release_event_cb %d', event.button)
+        if event.button == 3:
+            logging.debug('right click detected')
+            self.notify_right_click()
+            return True
+        elif self._last_tap is not None:
+            time_elapsed = time.time() - self._last_tap
+            self._last_tap = None
+            gobject.source_remove(self._hold_timeout_hid)
+            return time_elapsed >= (_TAP_TIMEOUT_MS / 1000.)
+        return False
+
+    def _button_press_event_cb(self, widget, event):
+        logging.debug('_button_press_event_cb %d', event.button)
+        if event.button != 1:
+            return False
+
+        self._last_tap = time.time()
+        self._hold_timeout_hid = gobject.timeout_add(_HOLD_TIMEOUT_MS,
+                                                     self._tap_hold_cb)
+        return False
+
+    def _tap_hold_cb(self):
+        logging.debug('_tap_hold_cb')
+
+        if time.time() - self._last_tap < (_HOLD_TIMEOUT_MS / 1000.):
+            logging.debug('too early')
+            return False
+
+        logging.debug('triggering after %.1fs', time.time() - self._last_tap)
+        self.notify_right_click()
+        return False
+

 class WidgetInvoker(Invoker):

@@ -686,6 +733,7 @@ class WidgetInvoker(Invoker):
         self._enter_hid = None
         self._leave_hid = None
         self._release_hid = None
+        self._press_hid = None

         if parent or widget:
             self.attach_widget(parent, widget)
@@ -698,12 +746,16 @@ class WidgetInvoker(Invoker):

         self.notify('widget')

+        self._widget.add_events(gtk.gdk.BUTTON_PRESS_MASK)
         self._enter_hid = self._widget.connect('enter-notify-event',
             self.__enter_notify_event_cb)
         self._leave_hid = self._widget.connect('leave-notify-event',
             self.__leave_notify_event_cb)
         self._release_hid = self._widget.connect('button-release-event',
-            self.__button_release_event_cb)
+            self._button_release_event_cb)
+        self._press_hid = self._widget.connect('button-press-event',
+            self._button_press_event_cb)
+        logging.debug('press handler connected')

         self.attach(parent)

@@ -712,6 +764,7 @@ class WidgetInvoker(Invoker):
         self._widget.disconnect(self._enter_hid)
         self._widget.disconnect(self._leave_hid)
         self._widget.disconnect(self._release_hid)
+        self._widget.disconnect(self._press_hid)

     def get_rect(self):
         allocation = self._widget.get_allocation()
@@ -764,13 +817,6 @@ class WidgetInvoker(Invoker):
     def __leave_notify_event_cb(self, widget, event):
         self.notify_mouse_leave()

-    def __button_release_event_cb(self, widget, event):
-        if event.button == 3:
-            self.notify_right_click()
-            return True
-        else:
-            return False
-
     def get_toplevel(self):
         return self._widget.get_toplevel()

@@ -795,6 +841,8 @@ class CanvasInvoker(Invoker):
         self._position_hint = self.AT_CURSOR
         self._motion_hid = None
         self._release_hid = None
+        self._press_hid = None
+        self._last_tap = None
         self._item = None

         if parent:
@@ -804,15 +852,19 @@ class CanvasInvoker(Invoker):
         Invoker.attach(self, parent)

         self._item = parent
+#        self._item.add_events(gtk.gdk.BUTTON_PRESS_MASK)
         self._motion_hid = self._item.connect('motion-notify-event',
                                               self.__motion_notify_event_cb)
         self._release_hid = self._item.connect('button-release-event',
-                                               self.__button_release_event_cb)
+                                               self._button_release_event_cb)
+        self._press_hid = self._item.connect('button-press-event',
+                                             self._button_press_event_cb)

     def detach(self):
         Invoker.detach(self)
         self._item.disconnect(self._motion_hid)
         self._item.disconnect(self._release_hid)
+        self._item.disconnect(self._press_hid)

     def get_default_position(self):
         return self.AT_CURSOR
@@ -834,13 +886,6 @@ class CanvasInvoker(Invoker):

         return False

-    def __button_release_event_cb(self, button, event):
-        if event.button == 3:
-            self.notify_right_click()
-            return True
-        else:
-            return False
-
     def get_toplevel(self):
         return hippo.get_canvas_for_item(self._item).get_toplevel()

@@ -878,18 +923,22 @@ class CellRendererInvoker(Invoker):
         self._motion_hid = None
         self._leave_hid = None
         self._release_hid = None
+        self._press_hid = None
         self.path = None

     def attach_cell_renderer(self, tree_view, cell_renderer):
         self._tree_view = tree_view
         self._cell_renderer = cell_renderer

+        tree_view.add_events(gtk.gdk.BUTTON_PRESS_MASK)
         self._motion_hid = tree_view.connect('motion-notify-event',
                                              self.__motion_notify_event_cb)
         self._leave_hid = tree_view.connect('leave-notify-event',
                                             self.__leave_notify_event_cb)
         self._release_hid = tree_view.connect('button-release-event',
-                                              self.__button_release_event_cb)
+                                              self._button_release_event_cb)
+        self._press_hid = tree_view.connect('button-press-event',
+                                            self._button_press_event_cb)

         self.attach(cell_renderer)

@@ -898,6 +947,7 @@ class CellRendererInvoker(Invoker):
         self._tree_view.disconnect(self._motion_hid)
         self._tree_view.disconnect(self._leave_hid)
         self._tree_view.disconnect(self._release_hid)
+        self._tree_view.disconnect(self._press_hid)

     def get_rect(self):
         allocation = self._tree_view.get_allocation()
@@ -957,7 +1007,10 @@ class CellRendererInvoker(Invoker):
     def __leave_notify_event_cb(self, widget, event):
         self.notify_mouse_leave()

-    def __button_release_event_cb(self, widget, event):
+    def _button_release_event_cb(self, widget, event):
+        if Invoker._button_release_event_cb(self, widget, event):
+            return True
+
         if event.button == 1 and self._point_in_cell_renderer(event.x,
             event.y):
             tree_view = self._tree_view
--
1.7.7



More information about the Sugar-devel mailing list