Home | Trees | Indices | Help |
---|
|
1 #!/usr/bin/env python 2 3 4 import sys; 5 import time; 6 import math; 7 8 from python_qt_binding import QtCore, QtGui 9 from QtCore import QCoreApplication, QEvent, QEventTransition, QObject, QPoint, QSignalTransition, QState, QStateMachine, QTimer, Signal, SIGNAL, Slot, Qt 10 from QtGui import QApplication, QCursor, QPushButton, QWidget, QHoverEvent, QMouseEvent 11 12 from qt_comm_channel.commChannel import CommChannel;16 17 # Signals emitted when GestureButton user flicks up and down, left-right, etc: 18 flickSig = Signal(QPushButton, int); 19 20 # Signal emitted when the button is first entered. Subsequent 21 # flicks will not re-send this signal. 22 buttonEnteredSig = Signal(QPushButton); 23 24 # Signal emitted when the button is left for good, that is 25 # when return for a flick is possible. 26 buttonExitedSig = Signal(QPushButton);2728 #********************* 29 # def __init__(self): 30 # super(GestureSignals, self).__init__() 31 # self.flickSig; 32 # self.buttonEnteredSig; 33 # self.buttonExitedSig; 34 #********************* 35 36 -class FlickDirection:37 NORTH = 0; 38 SOUTH = 1; 39 EAST = 2; 40 WEST = 3; 41 42 legalValues = [NORTH, SOUTH, EAST, WEST]; 43 44 @staticmethod5646 if flickDirection == FlickDirection.EAST: 47 return "<EAST>"; 48 elif flickDirection == FlickDirection.NORTH: 49 return "<NORTH>"; 50 elif flickDirection == FlickDirection.SOUTH: 51 return "<SOUTH>"; 52 elif flickDirection == FlickDirection.WEST: 53 return "<WEST>"; 54 else: 55 raise ValueError("Value other than a FlickDirection passed to FlickDirection.toString()");60 61 # Get a new event type that is shared among 62 # all the instances of this class: 63 userEventTypeID = QEvent.registerEventType(); 647166 ''' 67 Create a new instance of FlickDeterminationDone. The 68 event type will always be the constant FlickDeterminationDone.eventType 69 ''' 70 super(FlickDeterminationDone, self).__init__(QEvent.Type(FlickDeterminationDone.userEventTypeID));73 74 FLICK_TIME_THRESHOLD_MASTER = 0.5; # seconds 7529877 super(GestureButton, self).__init__(label, parent=parent); 78 79 #tmpStyle = "QPushButton {background-color: red}"; 80 #self.setStyleSheet(tmpStyle); 81 82 self.setFlicksEnabled(True); 83 84 # Timer for deciding when mouse has left this 85 # button long enough that even returning to this 86 # button will not be counted as a flick, but as 87 # a new entrance. 88 # NOTE: QTimer emits the signal "timeout()" when it 89 # expires. In contrast, QBasicTimer emits a timer *event* 90 # For use with state transitions, use QEventTransition 91 # for QBasicTimer, but QSignalTransition for QTimer. 92 93 self.maxFlickDurationTimer = QTimer(); 94 self.maxFlickDurationTimer.setSingleShot(True); 95 96 # Bit of a hack to get around a Pyside bug/missing-feature: 97 # Timer used with zero delay to trigger a QSignalTransition 98 # with a standard signal (no custom signals seem to work): 99 self.flickDetectDoneTrigger = QTimer(); 100 self.flickDetectDoneTrigger.setSingleShot(True); 101 102 self.setMouseTracking(True); 103 self.latestMousePos = QPoint(0,0); 104 105 self.connectWidgets(); 106 self.initSignals(); 107 108 # ------------------ State Definitions ------------------ 109 110 # Parent state for north/south/east/west states: 111 self.stateGestureActivatePending = QState(); 112 #self.stateGestureActivatePending.assignProperty(self, "text", "Toggle pending"); 113 114 # States for: mouse left button; will mouse flick back? 115 self.stateNorthExit = QState(self.stateGestureActivatePending); 116 self.stateSouthExit = QState(self.stateGestureActivatePending); 117 self.stateEastExit = QState(self.stateGestureActivatePending); 118 self.stateWestExit = QState(self.stateGestureActivatePending); 119 120 self.stateGestureActivatePending.setInitialState(self.stateNorthExit); 121 122 # State: mouse entered button area not as part of a flick: 123 self.stateEntered = QState(); 124 #self.stateEntered.assignProperty(self, "text", "Entered"); 125 126 # State: mouse re-entered button after leaving briefly: 127 self.stateReEntered = QState(); 128 #self.stateReEntered.assignProperty(self, "text", "Re-Entered"); 129 130 # State: mouse outside for longer than GestureButton.FLICK_TIME_THRESHOLD seconds: 131 self.stateIdle = QState(); 132 #self.stateIdle.assignProperty(self, "text", "Idle"); 133 134 # ------------------ Transition Definitions ------------------ 135 136 # From Idle to Entered: triggered automatically by mouse entering button area: 137 self.toEnteredTrans = HotEntryTransition(self, QEvent.Enter, sourceState=self.stateIdle); 138 self.toEnteredTrans.setTargetState(self.stateEntered); 139 140 # From Entered to GestureActivatePending: mouse cursor left button area, 141 # and the user may or may not return the cursor to the button area 142 # in time to trigger a flick. Transition triggered automatically by 143 # mouse leaving button area. A timer is set. If it runs to 0, 144 # a transition to Idle will be triggered. But if mouse re-enters button 145 # area before the timer expires, the ReEntered state will become active: 146 self.toGAPendingTrans = TimeSettingStateTransition(self, QEvent.Leave, sourceState=self.stateEntered); 147 self.toGAPendingTrans.setTargetState(self.stateGestureActivatePending); 148 149 # From GestureActivatePending to ReEntered. Triggered if mouse cursor 150 # re-enters button area after having left before the maxFlickDurationTimer 151 # has run down. Triggered automatically by mouse entering the button area: 152 self.toReEnteredTrans = TimeReadingStateTransition(self, 153 QEvent.Enter, 154 self.toGAPendingTrans, 155 sourceState=self.stateGestureActivatePending); 156 self.toReEnteredTrans.setTargetState(self.stateReEntered); 157 158 # From GestureActivePending to Idle. Triggered by maxFlickDurationTimer running to 0 159 # before mouse cursor re-entered button area after leaving. Triggered by timer that 160 # is set when state GestureActivatePending is entered. 161 self.toIdleTrans = HotExitTransition(self, 162 self.maxFlickDurationTimer, 163 sourceState=self.stateGestureActivatePending); 164 self.toIdleTrans.setTargetState(self.stateIdle); 165 166 # From ReEntered to Entered. Triggered a zero-delay timer timeout set 167 # in TimeReadingStateTransition after that transition has determined 168 # whether a mouse cursor entry into the button space was a flick, or not. 169 # (Note: In the PySide version 170 # current at this writing, neither custom events nor custom signals 171 # seem to work for triggering QEventTransaction or QSignalTransaction, 172 # respectively) 173 self.toEnteredFromReEnteredTrans = QSignalTransition(self.flickDetectDoneTrigger, 174 SIGNAL("timeout()"), 175 sourceState=self.stateReEntered); 176 self.toEnteredFromReEnteredTrans.setTargetState(self.stateEntered); 177 178 # ---------------------- State Machine -------QtCore.signal-------------- 179 180 self.stateMachine = QStateMachine(); 181 self.stateMachine.addState(self.stateGestureActivatePending); 182 self.stateMachine.addState(self.stateEntered); 183 self.stateMachine.addState(self.stateReEntered); 184 self.stateMachine.addState(self.stateIdle); 185 186 self.installEventFilter(self); 187 188 self.stateMachine.setInitialState(self.stateIdle); 189 self.stateMachine.start();190 191 #self.setGeometry(500, 500, 200,100); 192 #self.show() 193 194 # ------------------------ Public Methods ------------------------------- 195 196 @staticmethod198 ''' 199 Controls whether flicking in and out of buttons is enabled. 200 If flicks are enabled, then mouse-left-button signals are delayed 201 for FLICK_TIME_THRESHOLD seconds. If flicks are disabled, then 202 those signals are delivered immediately after the cursor leaves 203 a button. 204 @param doEnable: set to True if the flick feature is to be enabled. Else set to False 205 @type doEnable: boolean 206 ''' 207 if doEnable: 208 GestureButton.flickEnabled = True; 209 GestureButton.FLICK_TIME_THRESHOLD = GestureButton.FLICK_TIME_THRESHOLD_MASTER; 210 else: 211 GestureButton.flickEnabled = True; 212 GestureButton.FLICK_TIME_THRESHOLD = 0.0;213 214 @staticmethod216 ''' 217 Returns whether the flick feature is enabled. 218 @return: True if flicking is enabled, else False. 219 @rtype: boolean 220 ''' 221 return GestureButton.flickEnabled;222 223 # ------------------------ Private Methods ------------------------------- 224226 #******************* 227 # self.signals = GestureSignals(); 228 # CommChannel.getInstance().registerSignals(self.signals); 229 #CommChannel.getInstance().registerSignals(GestureSignals); 230 CommChannel.registerSignals(GestureSignals);231 232 #******************* 233 234 238240 if target == self and (event.__class__ == QMouseEvent): 241 self.latestMousePos = event.pos(); 242 return False;243 244 # def mouseMoveEvent(self, mouseEvent): 245 # self.latestMousePos = mouseEvent.pos(); 246 # super(GestureButton, self).mouseMoveEvent(mouseEvent); 247 248 @Slot(QPushButton) 252254 ''' 255 Retrieves current mouse position, which is assumed to have been 256 saved in the gesture button's lastMousePos instance variable by 257 a mouseMove signal handler or event filter. Compares this mouse position with the 258 four button borders. Returns a FlickDirection to indicate which 259 border is closest to the mouse. All this uses global coordinates. 260 @return: FlickDirection member. 261 @raise ValueError: if minimum distance cannot be determined. 262 ''' 263 # Global mouse position: 264 mousePos = self.mapToGlobal(self.latestMousePos) 265 # Get: (upperLeftGlobal_x, upperLeftGlobal_y, width, height): 266 gestureButtonRect = self.geometry() 267 # Recompute upper left and lower right in global coords 268 # each time, b/c window may have moved: 269 #topLeftPtGlobal = self.mapToGlobal(QPoint(gestureButtonRect.x(), gestureButtonRect.y())) 270 topLeftPtGlobal = gestureButtonRect.topLeft() 271 #bottomRightPtGlobal = QPoint(topLeftPtGlobal.x() + gestureButtonRect.width(), topLeftPtGlobal.y() + gestureButtonRect.height()) 272 bottomRightPtGlobal = gestureButtonRect.bottomRight() 273 # Do mouse coord absolute value compare with button edges, 274 # because the last recorded mouse event is often just still 275 # inside the button when this 'mouse left' signal arrives: 276 distMouseFromTop = math.fabs(mousePos.y() - topLeftPtGlobal.y()) 277 distMouseFromBottom = math.fabs(mousePos.y() - bottomRightPtGlobal.y()) 278 distMouseFromLeft = math.fabs(mousePos.x() - topLeftPtGlobal.x()) 279 distMouseFromRight = math.fabs(mousePos.x() - bottomRightPtGlobal.x()) 280 minDist = min(distMouseFromTop, distMouseFromBottom, distMouseFromLeft, distMouseFromRight) 281 if minDist == distMouseFromTop: 282 return FlickDirection.NORTH 283 elif minDist == distMouseFromBottom: 284 return FlickDirection.SOUTH 285 elif minDist == distMouseFromLeft: 286 return FlickDirection.WEST 287 elif minDist == distMouseFromRight: 288 return FlickDirection.EAST 289 else: 290 raise ValueError("Failed to compute closest button border.")291 292299 300 # -------------------------- Specialized States --------------------------- 301 302 303 # -------------------------- Specialized Transitions --------------------------- 304 305 -class HotEntryTransition(QEventTransition):306 ''' 307 Tansition handling entry into this button from 308 an idle state. 309 ''' 310331312 super(HotEntryTransition, self).__init__(gestureButtonObj, event, sourceState=sourceState); 313 self.gestureButtonObj = gestureButtonObj;314316 ''' 317 First entry to button from an idle state (as opposed to 318 from a re-entry (i.e. flick activity) state. 319 @param wrappedEventObj: Entry event 320 @type wrappedEventObj: QWrappedEvent 321 ''' 322 try: 323 super(HotEntryTransition, self).onTransition(wrappedEventObj); 324 except TypeError: 325 # Printing this will Segfault. Though this branch 326 # should not execute if Qt passes in the properly typed wrappedEventObj: 327 # It is supposed to be a QEvent, but sometimes comes as QListWidgetItem: 328 #print "Type error in HotEntryXition: expected wrappedEventObj, got QListWidgetItem: " + str(wrappedEventObj.text()) 329 pass 330 CommChannel.getSignal('GestureSignals.buttonEnteredSig').emit(self.gestureButtonObj);333341335 super(HotExitTransition, self).__init__(timerObj, SIGNAL("timeout()"), sourceState=sourceState); 336 self.gestureButtonObj = gestureButtonObj;337339 super(HotExitTransition, self).onTransition(wrappedEventObj); 340 CommChannel.getSignal('GestureSignals.buttonExitedSig').emit(self.gestureButtonObj);343375345 super(TimeSettingStateTransition, self).__init__(gestureButtonObj, event, sourceState=sourceState); 346 self.gestureButtonObj = gestureButtonObj; 347 self.transitionTime = None; 348 self.flickDirection = None;349351 ''' 352 Called when mouse cursor leaves the button area. Determine whether mouse 353 left the button across the upper, lower, left, or right border. Note the 354 time when this method call occurred. The time is needed later when the mouse 355 possibly re-enters the button, signaling a flick. 356 @param wrappedEventObj: wrapper around the event that triggered the transition. Unfortunately we 357 don't seem able to extract the corresponding mouseMove subclass event from this parameter. 358 @type wrappedEventObj: QWrappedEvent 359 ''' 360 try: 361 super(TimeSettingStateTransition, self).onTransition(wrappedEventObj); 362 except TypeError: 363 # Printing this will Segfault. Though this branch 364 # should not execute if Qt passes in the properly typed wrappedEventObj: 365 # It is supposed to be a QEvent, but sometimes comes as QListWidgetItem: 366 #print "Type error in TimeSettingXition: expected wrappedEventObj, got QListWidgetItem: " + str(wrappedEventObj.text()); 367 pass; 368 self.transitionTime = time.time(); 369 370 self.flickDirection = self.gestureButtonObj.findClosestButtonBorder() 371 372 # Start a timer that will trigger a transition to the idle state, unless 373 # the mouse returns to the button area within GestureButton.FLICK_TIME_THRESHOLD: 374 self.gestureButtonObj.maxFlickDurationTimer.start(GestureButton.FLICK_TIME_THRESHOLD * 1000);377409 410 # -------------------------- Testing --------------------------- 411 412 if __name__ == "__main__": 413 414 @QtCore.Slot(QPushButton) 417 418 @QtCore.Slot(QPushButton) 421 422 @QtCore.Slot(QPushButton,int) 425 426 427 app = QApplication(sys.argv); 428 b = GestureButton("Foo"); 429 b.setFixedSize(200,250); 430 enteredSig = CommChannel.getSignal('GestureSignals.buttonEnteredSig'); 431 enteredSig.connect(ackButtonEntered); 432 CommChannel.getSignal('GestureSignals.buttonExitedSig').connect(ackButtonExited); 433 CommChannel.getSignal('GestureSignals.flickSig').connect(ackButtonFlicked); 434 b.show() 435 app.exec_(); 436 sys.exit(); 437379 super(TimeReadingStateTransition, self).__init__(gestureButtonObj, event, sourceState=sourceState); 380 self.timeSettingTransition = timeSettingStateTransition; 381 self.gestureButtonObj = gestureButtonObj;382384 super(TimeReadingStateTransition, self).onTransition(eventEnumConstant); 385 now = time.time(); 386 transitionTime = self.timeSettingTransition.transitionTime; 387 if (now - transitionTime) > GestureButton.FLICK_TIME_THRESHOLD: 388 #print "Too slow" 389 pass 390 else: 391 #print "Fast enough" 392 if self.timeSettingTransition.flickDirection == FlickDirection.NORTH: 393 CommChannel.getSignal('GestureSignals.flickSig').emit(self.gestureButtonObj, FlickDirection.NORTH); 394 elif self.timeSettingTransition.flickDirection == FlickDirection.SOUTH: 395 CommChannel.getSignal('GestureSignals.flickSig').emit(self.gestureButtonObj, FlickDirection.SOUTH); 396 elif self.timeSettingTransition.flickDirection == FlickDirection.WEST: 397 CommChannel.getSignal('GestureSignals.flickSig').emit(self.gestureButtonObj, FlickDirection.WEST); 398 elif self.timeSettingTransition.flickDirection == FlickDirection.EAST: 399 CommChannel.getSignal('GestureSignals.flickSig').emit(self.gestureButtonObj, FlickDirection.EAST); 400 401 # Signal that this transition is done determining whether 402 # a ReEntry event was quick enough to be a flick. 403 # The timeout signal for this 0-delay timer will cause a 404 # transition to the Entered state. (Note: In the PySide version 405 # current at this writing, neither custom events nor custom signals 406 # seem to work for triggering QEventTransaction or QSignalTransaction, 407 # respectively, therefore this hack using a 0-delay timer.): 408 self.gestureButtonObj.flickDetectDoneTrigger.start(0);
Home | Trees | Indices | Help |
---|
Generated by Epydoc 3.0.1 on Thu Feb 21 11:45:19 2013 | http://epydoc.sourceforge.net |