1515:class:`plotpy.plot.PlotWidget`.
1616"""
1717
18+ from __future__ import annotations
19+
1820import weakref
1921
2022from qtpy import QtCore as QC
2628CursorShape = type (QC .Qt .CursorShape .ArrowCursor )
2729
2830
29- def buttons_to_str (buttons ):
31+ def is_touch_screen_available () -> bool :
32+ """Check if a touch screen is available.
33+
34+ Returns:
35+ bool: True if a touch screen is available, False otherwise.
36+ """
37+ touch_devices = QG .QTouchDevice .devices ()
38+ return any (device .type () == QG .QTouchDevice .TouchScreen for device in touch_devices )
39+
40+
41+ def buttons_to_str (buttons : int ) -> str :
3042 """Conversion des flags Qt en chaine"""
3143 string = ""
3244 if buttons & QC .Qt .LeftButton :
@@ -38,7 +50,7 @@ def buttons_to_str(buttons):
3850 return string
3951
4052
41- def evt_type_to_str (type ) :
53+ def evt_type_to_str (type : int ) -> str :
4254 """Représentation textuelle d'un type d'événement (debug)"""
4355 if type == QC .QEvent .MouseButtonPress :
4456 return "Mpress"
@@ -52,14 +64,14 @@ def evt_type_to_str(type):
5264 return f"{ type :d} "
5365
5466
55- # Sélection d'événements ---------
67+ # Event matching classes - ---------
5668class EventMatch :
5769 """A callable returning true if it matches an event"""
5870
5971 def __call__ (self , event ):
6072 raise NotImplementedError
6173
62- def get_event_types (self ):
74+ def get_event_types (self ) -> frozenset [ int ] :
6375 """Returns a set of event types handled by this
6476 EventMatch.
6577 This is used to quickly optimize events not handled
@@ -102,7 +114,7 @@ def get_event_types(self):
102114 """
103115 return frozenset ((QC .QEvent .KeyPress ,))
104116
105- def __call__ (self , event ) :
117+ def __call__ (self , event : QG . QKeyEvent ) -> bool :
106118 if event .type () == QC .QEvent .KeyPress :
107119 my_key = event .key ()
108120 my_mod = event .modifiers ()
@@ -183,7 +195,62 @@ def __call__(self, event):
183195 return False
184196
185197
186- # Machine d'état ----------
198+ class GestureEventMatch (EventMatch ):
199+ """Base class for matching gesture events"""
200+
201+ def __init__ (self , gesture_type , gesture_state ):
202+ super ().__init__ ()
203+ self .evt_type = QC .QEvent .Gesture
204+ self .gesture_type = gesture_type
205+ self .gesture_state = gesture_state
206+
207+ @staticmethod
208+ def __get_type_str (gesture_type ):
209+ """Return text representation for gesture type"""
210+ for attr in (
211+ "TapGesture" ,
212+ "TapAndHoldGesture" ,
213+ "PanGesture" ,
214+ "PinchGesture" ,
215+ "SwipeGesture" ,
216+ "CustomGesture" ,
217+ ):
218+ if gesture_type == getattr (QC .Qt , attr ):
219+ return attr
220+
221+ @staticmethod
222+ def __get_state_str (gesture_state ):
223+ """Return text representation for gesture state"""
224+ for attr in (
225+ "GestureStarted" ,
226+ "GestureUpdated" ,
227+ "GestureFinished" ,
228+ "GestureCanceled" ,
229+ ):
230+ if gesture_state == getattr (QC .Qt , attr ):
231+ return attr
232+
233+ def get_event_types (self ):
234+ return frozenset ((self .evt_type ,))
235+
236+ def __call__ (self , event ):
237+ # print(event)
238+ if event .type () == QC .QEvent .Gesture :
239+ # print(event.gestures()[0].gestureType())
240+ gesture = event .gesture (self .gesture_type )
241+ # print(gesture)
242+ if gesture :
243+ print (gesture .hotSpot (), self .__get_state_str (gesture .state ()))
244+ return gesture and gesture .state () == self .gesture_state
245+ return False
246+
247+ def __repr__ (self ):
248+ type_str = self .__get_type_str (self .gesture_type )
249+ state_str = self .__get_state_str (self .gesture_state )
250+ return "<GestureMatch: %s:%s>" % (type_str , state_str )
251+
252+
253+ # Finite state machine for event handling ----------
187254class StatefulEventFilter (QC .QObject ):
188255 """Gestion d'une machine d'état pour les événements
189256 d'un canvas
@@ -296,6 +363,12 @@ def mouse_release(self, btn, modifiers=QC.Qt.NoModifier):
296363 MouseEventMatch (QC .QEvent .MouseButtonRelease , btn , modifiers ),
297364 )
298365
366+ def gesture (self , kind , state ):
367+ """Création d'un filtre pour l'événement pincement"""
368+ return self .events .setdefault (
369+ ("gesture" , kind , state ), GestureEventMatch (kind , state )
370+ )
371+
299372 def nothing (self , filter , event ):
300373 """A nothing filter, provided to help removing duplicate handlers"""
301374 pass
@@ -450,6 +523,126 @@ def move(self, filter, event):
450523 filter .plot .do_zoom_view (x_state , y_state )
451524
452525
526+ class GestureHandler (QC .QObject ):
527+ """Classe de base pour les gestionnaires d'événements du type tactile"""
528+
529+ kind = None
530+
531+ def __init__ (self , filter , start_state = 0 ):
532+ super (GestureHandler , self ).__init__ ()
533+ filter .plot .canvas ().grabGesture (self .kind )
534+ self .state0 = filter .add_event (
535+ start_state ,
536+ filter .gesture (self .kind , QC .Qt .GestureStarted ),
537+ self .start_tracking ,
538+ )
539+ self .state1 = filter .add_event (
540+ self .state0 ,
541+ filter .gesture (self .kind , QC .Qt .GestureUpdated ),
542+ self .start_moving ,
543+ )
544+ filter .add_event (
545+ self .state1 ,
546+ filter .gesture (self .kind , QC .Qt .GestureUpdated ),
547+ self .move ,
548+ self .state1 ,
549+ )
550+ filter .add_event (
551+ self .state0 ,
552+ filter .gesture (self .kind , QC .Qt .GestureFinished ),
553+ self .stop_notmoving ,
554+ start_state ,
555+ )
556+ filter .add_event (
557+ self .state1 ,
558+ filter .gesture (self .kind , QC .Qt .GestureFinished ),
559+ self .stop_moving ,
560+ start_state ,
561+ )
562+ filter .add_event (
563+ self .state0 ,
564+ filter .gesture (self .kind , QC .Qt .GestureCanceled ),
565+ self .stop_notmoving ,
566+ start_state ,
567+ )
568+ filter .add_event (
569+ self .state1 ,
570+ filter .gesture (self .kind , QC .Qt .GestureCanceled ),
571+ self .stop_notmoving ,
572+ start_state ,
573+ )
574+ self .start = None # first gesture position
575+ self .last = None # gesture position seen during last event
576+ self .parent_tracking = None
577+
578+ def get_gesture (self , event ):
579+ return event .gesture (self .kind )
580+
581+ def get_move_state (self , filter , gesture ):
582+ raise NotImplementedError
583+
584+ def start_tracking (self , filter , event ):
585+ # print("Getting event for start tracking")
586+ origin = self .get_gesture (event ).hotSpot ()
587+ self .start = self .last = filter .plot .mapFromGlobal (origin .toPoint ())
588+
589+ def start_moving (self , filter , event ):
590+ return self .move (filter , event )
591+
592+ def stop_tracking (self , _filter , _event ):
593+ pass
594+ # filter.plot.canvas().setMouseTracking(self.parent_tracking)
595+
596+ def stop_notmoving (self , filter , event ):
597+ self .stop_tracking (filter , event )
598+
599+ def stop_moving (self , filter , event ):
600+ self .stop_tracking (filter , event )
601+
602+ def move (self , filter , event ):
603+ raise NotImplementedError
604+
605+
606+ class PinchZoomHandler (GestureHandler ):
607+ """Classe de base pour les gestionnaires d'événements du type tactile"""
608+
609+ kind = QC .Qt .PinchGesture
610+
611+ def get_move_state (self , filter , gesture ):
612+ rct = filter .plot .contentsRect ()
613+ xshift = rct .width () * (gesture .scaleFactor () - 1 )
614+ yshift = rct .height () * (gesture .scaleFactor () - 1 )
615+ pos = QC .QPointF (self .last .x () + xshift , self .last .y () + yshift )
616+ dx = (pos .x (), self .last .x (), self .start .x (), rct .width ())
617+ dy = (pos .y (), self .last .y (), self .start .y (), rct .height ())
618+ self .last = QC .QPointF (pos )
619+ return dx , dy
620+
621+ def move (self , filter , event ):
622+ gesture = self .get_gesture (event )
623+ x_state , y_state = self .get_move_state (filter , gesture )
624+ filter .plot .do_zoom_view (x_state , y_state )
625+
626+
627+ class PanGestureHandler (GestureHandler ):
628+ """Classe de base pour les gestionnaires d'événements du type tactile"""
629+
630+ kind = QC .Qt .PanGesture
631+
632+ def get_move_state (self , filter , gesture ):
633+ rct = filter .plot .contentsRect ()
634+ pos = gesture .offset ()
635+ self .last = gesture .lastOffset ()
636+ dx = (pos .x (), self .last .x (), self .start .x (), rct .width ())
637+ dy = (pos .y (), self .last .y (), self .start .y (), rct .height ())
638+ return dx , dy
639+
640+ def move (self , filter , event ):
641+ gesture = self .get_gesture (event )
642+ x_state , y_state = self .get_move_state (filter , gesture )
643+ filter .plot .do_pan_view (x_state , y_state )
644+
645+
453646class MenuHandler (ClickHandler ):
454647 def click (self , filter , event ):
455648 """
@@ -962,6 +1155,13 @@ def setup_standard_tool_filter(filter, start_state):
9621155 ZoomHandler (filter , QC .Qt .RightButton , start_state = start_state )
9631156 MenuHandler (filter , QC .Qt .RightButton , start_state = start_state )
9641157
1158+ if is_touch_screen_available ():
1159+ # Gestes
1160+ PinchZoomHandler (filter , start_state = start_state )
1161+ # FIXME: Pinch/PanZoomHandler are currently mutually exclusive: when both
1162+ # are enabled, it doesn't work ; when only one is enabled, it works
1163+ PanGestureHandler (filter , start_state = start_state )
1164+
9651165 # Autres (touches, move)
9661166 MoveHandler (filter , start_state = start_state )
9671167 MoveHandler (filter , start_state = start_state , mods = QC .Qt .ShiftModifier )
0 commit comments