Ticket #3374: mousemodes.diff
File mousemodes.diff, 32.5 KB (added by , 5 years ago) |
---|
-
src/bundles/mouse_modes/src/mousemodes.py
diff --git a/src/bundles/mouse_modes/src/mousemodes.py b/src/bundles/mouse_modes/src/mousemodes.py old mode 100644 new mode 100755 index eea9b8496..8cd52f68c
a b class MouseMode: 64 64 65 65 def enable(self): 66 66 ''' 67 Supported API. 67 Supported API. 68 68 Called when mouse mode is enabled. 69 69 Override if mode wants to know that it has been bound to a mouse button. 70 70 ''' … … class MouseMode: 139 139 def uses_wheel(self): 140 140 '''Return True if derived class implements the wheel() method.''' 141 141 return getattr(self, 'wheel') != MouseMode.wheel 142 142 143 143 def pause(self, position): 144 144 ''' 145 145 Supported API. … … class MouseMode: 156 156 ''' 157 157 pass 158 158 159 def touchpad_two_finger_scale(self, scale): 160 ''' 161 Supported API. 162 Override this method to take action when a two-finger pinching motion 163 is used on a multitouch touchpad. The scale parameter is a float, where 164 values larger than 1 indicate fingers moving apart and values less than 165 1 indicate fingers moving together. 166 ''' 167 pass 168 169 def touchpad_two_finger_twist(self, angle): 170 ''' 171 Supported API. 172 Override this method to take action when a two-finger twisting motion 173 is used on a multitouch touchpad. The angle parameter is the rotation 174 angle in degrees. 175 ''' 176 pass 177 178 def touchpad_two_finger_trans(self, move): 179 ''' 180 Supported API. 181 Override this method to take action when a two-finger swiping motion is 182 used on a multitouch touchpad. The move parameter is a tuple of two 183 floats: (delta_x, delta_y), where delta_x and delta_y are 184 distances expressed as fractions of the total width of the trackpad 185 ''' 186 pass 187 188 def touchpad_three_finger_trans(self, move): 189 ''' 190 Supported API. 191 Override this method to take action when a three-finger swiping motion 192 is used on a multitouch touchpad. The move parameter is a tuple of two 193 floats: (delta_x, delta_y) representing the distance moved on the 194 touchpad as a fraction of its width. 195 ''' 196 pass 197 198 def touchpad_four_finger_trans(self, move): 199 ''' 200 Supported API. 201 Override this method to take action when a four-finger swiping motion 202 is used on a multitouch touchpad. The move parameter is a tuple of two 203 floats: (delta_x, delta_y) representing the distance moved on the 204 touchpad as a fraction of its width. 205 ''' 206 pass 207 208 159 209 def pixel_size(self, center = None, min_scene_frac = 1e-5): 160 210 ''' 161 211 Supported API. … … class MouseMode: 201 251 cfile = inspect.getfile(cls) 202 252 p = path.join(path.dirname(cfile), file) 203 253 return p 204 254 205 255 class MouseBinding: 206 256 ''' 207 257 Associates a mouse button ('left', 'middle', 'right', 'wheel', 'pause') and … … class MouseBinding: 227 277 ''' 228 278 return button == self.button and set(modifiers) == set(self.modifiers) 229 279 280 281 282 283 230 284 class MouseModes: 231 285 ''' 232 286 Keep the list of available mouse modes and also which mode is bound … … class MouseModes: 243 297 self._available_modes = [mode(session) for mode in standard_mouse_mode_classes()] 244 298 245 299 self._bindings = [] # List of MouseBinding instances 300 self._trackpad_bindings = [] # List of MultitouchBinding instances 246 301 247 302 from PyQt5.QtCore import Qt 248 303 # Qt maps control to meta on Mac... 249 self._modifier_bits = []250 for keyfunc in ["alt", "control", "command", "shift"]:251 self._modifier_bits.append((mod_key_info(keyfunc)[0], keyfunc))252 304 253 305 # Mouse pause parameters 254 306 self._last_mouse_time = None … … class MouseModes: 261 313 self._last_mode = None # Remember mode at mouse down and stay with it until mouse up 262 314 263 315 from .trackpad import MultitouchTrackpad 264 self.trackpad = MultitouchTrackpad(session )316 self.trackpad = MultitouchTrackpad(session, self) 265 317 266 def bind_mouse_mode(self, button, modifiers, mode): 318 def bind_mouse_mode(self, mouse_button=None, mouse_modifiers=[], mode=None, 319 trackpad_action=None, trackpad_modifiers=[]): 320 ''' 321 Bind a MouseMode to a mouse click and/or a multitouch trackpad action 322 with optional modifier keys. 323 324 mouse_button is either None or one of ("left", "middle", "right", "wheel", or "pause"). 325 326 trackpad_action is either None or one of ("pinch", "twist", "two finger swipe", 327 "three finger swipe" or "four finger swipe"). 328 329 mouse_modifiers and trackpad_modifiers are each a list of 0 or more of 330 ("alt", "command", "control" or "shift"). 331 332 mode is a MouseMode instance. 333 ''' 334 if mouse_button is not None: 335 self._bind_mouse_mode(mouse_button, mouse_modifiers, mode) 336 if trackpad_action is not None: 337 self._bind_trackpad_mode(trackpad_action, trackpad_modifiers, mode) 338 339 def _bind_mouse_mode(self, button, modifiers, mode): 267 340 ''' 268 341 Button is "left", "middle", "right", "wheel", or "pause". 269 342 Modifiers is a list 0 or more of 'alt', 'command', 'control', 'shift'. … … class MouseModes: 279 352 if button == "right" and not modifiers: 280 353 self.session.triggers.activate_trigger("set right mouse", mode) 281 354 355 def _bind_trackpad_mode(self, action, modifiers, mode): 356 ''' 357 Action is one of ("pinch", "twist", "two finger swipe", 358 "three finger swipe" or "four finger swipe"). Modifiers is a list of 359 0 or more of ("alt", "command", "control" or "shift"). Mode is a 360 MouseMode instance. 361 ''' 362 self.remove_binding(trackpad_action=action, trackpad_modifiers=modifiers) 363 if mode is not None: 364 from .std_modes import NullMouseMode 365 if not isinstance(mode, NullMouseMode): 366 from .trackpad import MultitouchBinding 367 b = MultitouchBinding(action, modifiers, mode) 368 self._trackpad_bindings.append(b) 369 mode.enable() 370 282 371 def bind_standard_mouse_modes(self, buttons = ('left', 'middle', 'right', 'wheel', 'pause')): 283 372 ''' 284 373 Bind the standard mouse modes: left = rotate, ctrl-left = select, middle = translate, 285 374 right = zoom, wheel = zoom, pause = identify object. 286 375 ''' 287 376 standard_modes = ( 288 ('left', [], 'rotate'), 289 ('left', ['control'], 'select'), 290 ('middle', [], 'translate'), 291 ('right', [], 'translate'), 292 ('wheel', [], 'zoom'), 293 ('pause', [], 'identify object'), 377 ('left', [], 'two finger swipe', [], 'rotate'), 378 (None, [], 'twist', [], 'rotate'), 379 ('left', ['control'], None, [], 'select'), 380 ('middle', [], 'three finger swipe', [], 'translate'), 381 ('right', [], None, [], 'translate'), 382 ('wheel', [], 'pinch', [], 'zoom'), 383 ('pause', [], None, [], 'identify object'), 294 384 ) 295 385 mmap = {m.name:m for m in self.modes} 296 for button, modifiers, mode_name in standard_modes: 297 if button in buttons: 298 self.bind_mouse_mode(button, modifiers, mmap[mode_name]) 386 for button, modifiers, trackpad_action, trackpad_modifiers, mode_name in standard_modes: 387 self.bind_mouse_mode(button, modifiers, mmap[mode_name], trackpad_action, trackpad_modifiers) 299 388 300 389 def add_mode(self, mode): 301 390 '''Supported API. Add a MouseMode instance to the list of available modes.''' … … class MouseModes: 321 410 m = None 322 411 return m 323 412 413 def trackpad_mode(self, action, modifiers=[], exact=False): 414 ''' 415 Return the MouseMode associated with a specific multitouch action and 416 modifiers, or None if no mode is bound. 417 ''' 418 if exact: 419 mb = [b for b in self._trackpad_bindings if b.exact_match(action, modifiers)] 420 else: 421 mb = [b for b in self._trackpad_bindings if b.matches(action, modifiers)] 422 if len(mb) == 1: 423 m = mb[0].mode 424 elif len(mb) > 1: 425 m = max(mb, key = lambda b: len(b.modifiers)).mode 426 else: 427 m = None 428 return m 429 324 430 @property 325 431 def modes(self): 326 432 '''List of MouseMode instances.''' … … class MouseModes: 331 437 if m.name == name: 332 438 return m 333 439 return None 334 440 335 441 def mouse_pause_tracking(self): 336 442 ''' 337 443 Called periodically to check for mouse pause and invoke pause mode. … … class MouseModes: 362 468 self._mouse_pause() 363 469 self._paused = True 364 470 365 def remove_binding(self, button, modifiers): 471 def remove_binding(self, button=None, modifiers=[], 472 trackpad_action=None, trackpad_modifiers=[]): 366 473 ''' 367 474 Unbind the mouse button and modifier key combination. 368 475 No mode will be associated with this button and modifier. 369 476 ''' 370 self._bindings = [b for b in self.bindings if not b.exact_match(button, modifiers)] 477 if button is not None: 478 self._bindings = [b for b in self.bindings if not b.exact_match(button, modifiers)] 479 if trackpad_action is not None: 480 self._trackpad_bindings = [b for b in self._trackpad_bindings if not b.exact_match(trackpad_action, trackpad_modifiers)] 371 481 372 482 def remove_mode(self, mode): 373 483 '''Remove a MouseMode instance from the list of available modes.''' … … class MouseModes: 382 492 def _mouse_buttons_down(self): 383 493 from PyQt5.QtCore import Qt 384 494 return self.session.ui.mouseButtons() != Qt.NoButton 385 495 386 496 def _dispatch_mouse_event(self, event, action): 387 497 button, modifiers = self._event_type(event) 388 498 if button is None: … … class MouseModes: 404 514 self._last_mode = None 405 515 406 516 def _event_type(self, event): 407 modifiers = self._key_modifiers(event)517 modifiers = key_modifiers(event) 408 518 409 519 # button() gives press/release buttons; buttons() gives move buttons 410 520 from PyQt5.QtCore import Qt … … class MouseModes: 449 559 450 560 return button, modifiers 451 561 562 def _dispatch_touch_event(self, touch_event): 563 te = touch_event 564 from .trackpad import touch_action_to_property 565 for action, prop in touch_action_to_property.items(): 566 data = getattr(touch_event, prop) 567 if getattr(touch_event, prop) is None: 568 continue 569 m = self.trackpad_mode(action, te.modifiers) 570 if m is not None: 571 f = getattr(m, 'touchpad_'+prop) 572 f(data) 573 574 575 # t_string = ('Registered touch event: \n' 576 # 'modifer keys pressed: {}\n' 577 # 'wheel_value: {}\n' 578 # 'two_finger_trans: {}\n' 579 # 'two_finger_scale: {}\n' 580 # 'two_finger_twist: {}\n' 581 # 'three_finger_trans: {}\n' 582 # 'four_finger_trans: {}').format( 583 # ', '.join(te._modifiers), 584 # te.wheel_value, 585 # te.two_finger_trans, 586 # te.two_finger_scale, 587 # te.two_finger_twist, 588 # te.three_finger_trans, 589 # te.four_finger_trans 590 # ) 591 # print(t_string) 592 593 452 594 def _have_mode(self, button, modifier): 453 595 for b in self.bindings: 454 596 if b.exact_match(button, [modifier]): 455 597 return True 456 598 return False 457 599 458 def _key_modifiers(self, event):459 mod = event.modifiers()460 modifiers = [mod_name for bit, mod_name in self._modifier_bits if bit & mod]461 return modifiers462 463 600 def _mouse_pause(self): 464 601 m = self.mode('pause') 465 602 if m: … … class MouseModes: 482 619 def _wheel_event(self, event): 483 620 if self.trackpad.discard_trackpad_wheel_event(event): 484 621 return # Trackpad processing handled this event 485 f = self.mode('wheel', self._key_modifiers(event))622 f = self.mode('wheel', key_modifiers(event)) 486 623 if f: 487 624 f.wheel(MouseEvent(event)) 488 625 … … class MouseEvent: 498 635 # for mouse button emulation. 499 636 self._position = position # x,y in pixels, can be None 500 637 self._wheel_value = wheel_value # wheel clicks (usually 1 click equals 15 degrees rotation). 501 638 502 639 def shift_down(self): 503 640 ''' 504 641 Supported API. … … class MouseEvent: 556 693 delta = min(deltas.x(), deltas.y()) 557 694 return delta/120.0 # Usually one wheel click is delta of 120 558 695 return 0 559 696 560 697 def mod_key_info(key_function): 561 698 """Qt swaps control/meta on Mac, so centralize that knowledge here. 562 699 The possible "key_functions" are: alt, control, command, and shift … … def mod_key_info(key_function): 584 721 return Qt.ControlModifier, "control" 585 722 return Qt.MetaModifier, command_name 586 723 724 _function_keys = ["alt", "control", "command", "shift"] 725 _modifier_bits = [(mod_key_info(fkey)[0], fkey) for fkey in _function_keys] 726 727 728 def key_modifiers(event): 729 return decode_modifier_bits(event.modifiers()) 730 731 def decode_modifier_bits(mod): 732 modifiers = [mod_name for bit, mod_name in _modifier_bits if bit & mod] 733 return modifiers 734 735 587 736 def keyboard_modifier_names(qt_keyboard_modifiers): 588 737 from PyQt5.QtCore import Qt 589 738 import sys … … def keyboard_modifier_names(qt_keyboard_modifiers): 601 750 mnames = [mname for mflag, mname in modifiers if mflag & qt_keyboard_modifiers] 602 751 return mnames 603 752 753 754 755 756 604 757 def unpickable(drawing): 605 758 return not getattr(drawing, 'pickable', True) 606 759 607 760 def picked_object(window_x, window_y, view, max_transparent_layers = 3, exclude = unpickable): 608 761 xyz1, xyz2 = view.clip_plane_points(window_x, window_y) 609 762 if xyz1 is None or xyz2 is None: … … def picked_object_on_segment(xyz1, xyz2, view, max_transparent_layers = 3, exclu 621 774 else: 622 775 break 623 776 return p2 if p2 else p 624 -
src/bundles/mouse_modes/src/std_modes.py
diff --git a/src/bundles/mouse_modes/src/std_modes.py b/src/bundles/mouse_modes/src/std_modes.py index c68bd96f6..9826f6072 100644
a b class SelectSubtractMouseMode(SelectMouseMode): 172 172 '''Mouse mode to subtract objects from selection by clicking on them.''' 173 173 name = 'select subtract' 174 174 icon_file = None 175 175 176 176 class SelectToggleMouseMode(SelectMouseMode): 177 177 '''Mouse mode to toggle selected objects by clicking on them.''' 178 178 name = 'select toggle' … … class MoveMouseMode(MouseMode): 264 264 # Undo 265 265 self._starting_atom_scene_coords = None 266 266 self._starting_model_positions = None 267 267 268 268 def mouse_down(self, event): 269 269 MouseMode.mouse_down(self, event) 270 270 if self.action(event) == 'rotate': … … class MoveMouseMode(MouseMode): 283 283 self._translate(shift) 284 284 self._moved = True 285 285 286 286 287 def mouse_up(self, event): 287 288 if self.click_to_select: 288 289 if event.position() == self.mouse_down_position: … … class MoveMouseMode(MouseMode): 294 295 295 296 if self.move_atoms: 296 297 self._atoms = None 297 298 298 299 def wheel(self, event): 299 300 d = event.wheel_value() 300 301 if self.move_atoms: … … class MoveMouseMode(MouseMode): 311 312 # Holding shift key switches between rotation and translation 312 313 a = 'translate' if a == 'rotate' else 'rotate' 313 314 return a 314 315 316 def touchpad_two_finger_trans(self, move): 317 if self.mouse_action=='rotate': 318 tp = self.session.ui.mouse_modes.trackpad 319 from math import sqrt 320 dx, dy = move 321 turns = sqrt(dx*dx + dy*dy)*tp.full_width_translation_distance/tp.full_rotation_distance 322 angle = tp.trackpad_speed*360*turns 323 self._rotate((dy, dx, 0), angle) 324 325 def touchpad_three_finger_trans(self, move): 326 dx, dy = move 327 if self.mouse_action=='translate': 328 tp = self.session.ui.mouse_modes.trackpad 329 ww = self.session.view.window_size[0] # window width in pixels 330 s = tp.trackpad_speed*ww 331 self._translate((s*dx, -s*dy, 0)) 332 333 def touchpad_two_finger_twist(self, angle): 334 if self.mouse_action=='rotate': 335 self._rotate((0,0,1), angle) 336 337 315 338 def _set_z_rotation(self, event): 316 339 x,y = event.position() 317 340 w,h = self.view.window_size … … class MoveMouseMode(MouseMode): 372 395 self._move_atoms(translation(step)) 373 396 else: 374 397 self.view.translate(step, self.models()) 375 398 376 399 def _translation(self, event): 377 400 '''Returned shift is in camera coordinates.''' 378 401 dx, dy = self.mouse_motion(event) … … class MoveMouseMode(MouseMode): 401 424 @property 402 425 def _moving_atoms(self): 403 426 return self.move_atoms and self._atoms is not None and len(self._atoms) > 0 404 427 405 428 def _move_atoms(self, transform): 406 429 atoms = self._atoms 407 430 atoms.scene_coords = transform * atoms.scene_coords … … class MoveMouseMode(MouseMode): 444 467 from chimerax.atomic import selected_atoms 445 468 self._atoms = selected_atoms(self.session) 446 469 self._undo_start() 447 470 448 471 def vr_motion(self, event): 449 472 # Virtual reality hand controller motion. 450 473 if self._moving_atoms: … … class MoveMouseMode(MouseMode): 452 475 else: 453 476 self.view.move(event.motion, self.models()) 454 477 self._moved = True 455 478 456 479 def vr_release(self, event): 457 480 # Virtual reality hand controller button release. 458 481 self._undo_save() 459 482 460 483 class RotateMouseMode(MoveMouseMode): 461 484 ''' 462 485 Mouse mode to rotate objects (actually the camera is moved) by dragging. … … class RotateSelectedAtomsMouseMode(RotateMouseMode): 560 583 name = 'rotate selected atoms' 561 584 icon_file = 'icons/rotate_atoms.png' 562 585 move_atoms = True 563 586 564 587 class ZoomMouseMode(MouseMode): 565 588 ''' 566 589 Mouse mode to move objects in z, actually the camera is moved … … class ZoomMouseMode(MouseMode): 572 595 MouseMode.__init__(self, session) 573 596 self.speed = 1 574 597 575 def mouse_drag(self, event): 598 def mouse_drag(self, event): 576 599 577 600 dx, dy = self.mouse_motion(event) 578 601 psize = self.pixel_size() … … class ZoomMouseMode(MouseMode): 585 608 delta_z = 100*d*psize*self.speed 586 609 self.zoom(delta_z, stereo_scaling = not event.alt_down()) 587 610 611 def touchpad_two_finger_scale(self, scale): 612 v = self.session.view 613 wpix = v.window_size[0] 614 psize = v.pixel_size() 615 d = (scale-1)*wpix*psize 616 self.zoom(d) 617 588 618 def zoom(self, delta_z, stereo_scaling = False): 589 619 v = self.view 590 620 c = v.camera … … class ZoomMouseMode(MouseMode): 597 627 else: 598 628 shift = c.position.transform_vector((0, 0, delta_z)) 599 629 v.translate(shift) 600 630 601 631 class ObjectIdMouseMode(MouseMode): 602 632 ''' 603 633 Mouse mode to that shows the name of an object in a popup window … … class ObjectIdMouseMode(MouseMode): 607 637 def __init__(self, session): 608 638 MouseMode.__init__(self, session) 609 639 session.triggers.add_trigger('mouse hover') 610 640 611 641 def pause(self, position): 612 642 ui = self.session.ui 613 643 if ui.activeWindow() is None: … … class AtomCenterOfRotationMode(MouseMode): 677 707 return 678 708 from chimerax.std_commands import cofr 679 709 cofr.cofr(self.session, pivot=xyz) 680 710 681 711 class NullMouseMode(MouseMode): 682 712 '''Used to assign no mode to a mouse button.''' 683 713 name = 'none' … … class ClipMouseMode(MouseMode): 720 750 front_shift = 1 if shift or not alt else 0 721 751 back_shift = 0 if not (alt or shift) else (1 if alt and shift else -1) 722 752 return front_shift, back_shift 723 753 724 754 def wheel(self, event): 725 755 d = event.wheel_value() 726 756 psize = self.pixel_size() … … class ClipMouseMode(MouseMode): 766 796 use_scene_planes = (clip_settings.mouse_clip_plane_type == 'scene planes') 767 797 else: 768 798 use_scene_planes = (p.find_plane('front') or p.find_plane('back')) 769 799 770 800 pfname, pbname = ('front','back') if use_scene_planes else ('near','far') 771 801 772 802 pf, pb = p.find_plane(pfname), p.find_plane(pbname) 773 803 from chimerax.std_commands.clip import adjust_plane 774 804 c = v.camera -
src/bundles/mouse_modes/src/trackpad.py
diff --git a/src/bundles/mouse_modes/src/trackpad.py b/src/bundles/mouse_modes/src/trackpad.py old mode 100644 new mode 100755 index d0e2109d3..c85d90fb3
a b class MultitouchTrackpad: 17 17 and three finger drag translate scene, 18 18 and two finger pinch zoom scene. 19 19 ''' 20 def __init__(self, session): 20 21 def __init__(self, session, mouse_mode_mgr): 21 22 self._session = session 23 self._mouse_mode_mgr = mouse_mode_mgr 22 24 self._view = session.main_view 23 25 self._recent_touches = [] # List of Touch instances 26 self._modifier_keys = [] 24 27 self._last_touch_locations = {} # Map touch id -> (x,y) 25 28 from .settings import settings 26 29 self.trackpad_speed = settings.trackpad_sensitivity # Trackpad position sensitivity … … class MultitouchTrackpad: 34 37 self._touch_handler = None 35 38 self._received_touch_event = False 36 39 40 @property 41 def full_width_translation_distance(self): 42 return self._full_width_translation_distance 43 44 @property 45 def full_rotation_distance(self): 46 return self._full_rotation_distance 47 37 48 def set_graphics_window(self, graphics_window): 38 49 graphics_window.touchEvent = self._touch_event 39 50 self._enable_touch_events(graphics_window) … … class MultitouchTrackpad: 50 61 t.remove_handler(h) 51 62 h = None 52 63 self._touch_handler = h 53 64 54 65 def _enable_touch_events(self, graphics_window): 55 66 from sys import platform 56 67 if platform == 'darwin': … … class MultitouchTrackpad: 69 80 w.setAttribute(Qt.WA_AcceptTouchEvents) 70 81 print('graphics widget touch enabled', w.testAttribute(Qt.WA_AcceptTouchEvents)) 71 82 ''' 72 83 73 84 # Appears that Qt has disabled touch events on Mac due to unresolved scrolling lag problems. 74 85 # Searching for qt setAcceptsTouchEvents shows they were disabled Oct 17, 2012. 75 86 # A patch that allows an environment variable QT_MAC_ENABLE_TOUCH_EVENTS to allow touch … … class MultitouchTrackpad: 84 95 85 96 from PyQt5.QtCore import QEvent 86 97 t = event.type() 98 # For some unfathomable reason the QTouchEvent.modifiers() method always 99 # returns zero (QTBUG-60389, unresolved since 2017). So we need to do a 100 # little hacky workaround 101 102 from .mousemodes import decode_modifier_bits 103 # session.ui.keyboardModifiers() does *not* work here (always returns 0) 104 mb = int(self._session.ui.queryKeyboardModifiers()) 105 self._modifier_keys = decode_modifier_bits(mb) 106 107 87 108 if t == QEvent.TouchUpdate: 88 # On Mac touch events get backlogged in queue when the events cause 109 # On Mac touch events get backlogged in queue when the events cause 89 110 # time consuming computatation. It appears Qt does not collapse the events. 90 111 # So event processing can get tens of seconds behind. To reduce this problem 91 112 # we only handle the most recent touch update per redraw. … … class MultitouchTrackpad: 100 121 def _collapse_touch_events(self): 101 122 touches = self._recent_touches 102 123 if touches: 103 self._process_touches(touches)124 event = self._process_touches(touches) 104 125 self._recent_touches = [] 126 self._mouse_mode_mgr._dispatch_touch_event(event) 105 127 106 128 def _process_touches(self, touches): 129 pinch = twist = scroll = None 130 two_swipe = None 131 three_swipe = None 132 four_swipe = None 107 133 n = len(touches) 108 134 speed = self.trackpad_speed 109 135 moves = [t.move(self._last_touch_locations) for t in touches] 136 dx = sum(x for x,y in moves)/n 137 dy = sum(y for x,y in moves)/n 138 110 139 if n == 2: 111 140 (dx0,dy0),(dx1,dy1) = moves[0], moves[1] 112 141 from math import sqrt, exp, atan2, pi 113 142 l0,l1 = sqrt(dx0*dx0 + dy0*dy0),sqrt(dx1*dx1 + dy1*dy1) 114 143 d12 = dx0*dx1+dy0*dy1 115 144 if d12 < 0: 116 # Finger moving in opposite directions: pinch ortwist145 # Finger moving in opposite directions: pinch/twist 117 146 (x0,y0),(x1,y1) = [(t.x,t.y) for t in touches[:2]] 118 147 sx,sy = x1-x0,y1-y0 119 148 sn = sqrt(sx*sx + sy*sy) 120 149 sd0,sd1 = sx*dx0 + sy*dy0, sx*dx1 + sy*dy1 121 if abs(sd0) > 0.5*sn*l0 and abs(sd1) > 0.5*sn*l1: 122 # Fingers move along line between them: pinch to zoom 123 zf = 1 + speed * self._zoom_scaling * (l0+l1) / self._full_width_translation_distance 124 if sd1 < 0: 125 zf = 1/zf 126 self._zoom(zf) 127 else: 128 # Fingers move perpendicular to line between them: twist 129 rot = atan2(-sy*dx1+sx*dy1,sn*sn) + atan2(sy*dx0-sx*dy0,sn*sn) 130 a = -speed * self._twist_scaling * rot * 180 / pi 131 zaxis = (0,0,1) 132 self._rotate(zaxis, a) 133 return 134 # Fingers moving in same direction: rotation 135 dx = sum(x for x,y in moves)/n 136 dy = sum(y for x,y in moves)/n 137 from math import sqrt 138 turns = sqrt(dx*dx + dy*dy)/self._full_rotation_distance 139 angle = speed*360*turns 140 self._rotate((dy, dx, 0), angle) 150 zf = 1 + speed * self._zoom_scaling * (l0+l1) / self._full_width_translation_distance 151 if sd1 < 0: 152 zf = 1/zf 153 pinch = zf 154 rot = atan2(-sy*dx1+sx*dy1,sn*sn) + atan2(sy*dx0-sx*dy0,sn*sn) 155 a = -speed * self._twist_scaling * rot * 180 / pi 156 twist = a 157 else: 158 two_swipe = tuple([d/self._full_width_translation_distance for d in (dx, dy)]) 159 scroll = speed * dy / self._wheel_click_pixels 141 160 elif n == 3: 142 dx = sum(x for x,y in moves)/n 143 dy = sum(y for x,y in moves)/n 144 ww = self._view.window_size[0] # Window width in pixels 145 s = speed * ww / self._full_width_translation_distance 146 self._translate((s*dx, -s*dy, 0)) 161 three_swipe = tuple([d/self._full_width_translation_distance for d in (dx, dy)]) 147 162 elif n == 4: 148 # Use scrollwheel mouse mode 149 ses = self._session 150 from .mousemodes import keyboard_modifier_names, MouseEvent 151 modifiers = keyboard_modifier_names(ses.ui.queryKeyboardModifiers()) 152 scrollwheel_mode = ses.ui.mouse_modes.mode(button = 'wheel', modifiers = modifiers) 153 if scrollwheel_mode: 154 xy = (sum(t.x for t in touches)/n, sum(t.y for t in touches)/n) 155 dy = sum(y for x,y in moves)/n # pixels 156 delta = speed * dy / self._wheel_click_pixels # wheel clicks 157 scrollwheel_mode.wheel(MouseEvent(position = xy, wheel_value = delta, modifiers = modifiers)) 163 four_swipe = tuple([d/self._full_width_translation_distance for d in (dx, dy)]) 164 165 return MultitouchEvent(modifiers=self._modifier_keys, 166 wheel_value=scroll, two_finger_trans=two_swipe, two_finger_scale=pinch, 167 two_finger_twist=twist, three_finger_trans=three_swipe, 168 four_finger_trans=four_swipe) 169 170 return pinch, twist, scroll, two_swipe, three_swipe, four_swipe 158 171 159 172 def _rotate(self, screen_axis, angle): 160 173 if angle == 0: … … class Touch: 230 243 x,y = self.x, self.y 231 244 last_touch_locations[id] = (x,y) 232 245 return (x-lx, y-ly) 246 247 touch_action_to_property = { 248 'pinch': 'two_finger_scale', 249 'twist': 'two_finger_twist', 250 'two finger swipe': 'two_finger_trans', 251 'three finger swipe': 'three_finger_trans', 252 'four finger swipe': 'four_finger_trans', 253 } 254 255 256 class MultitouchBinding: 257 ''' 258 Associates an action on a multitouch trackpad and a set of modifier keys 259 ('alt', 'command', 'control', 'shift') with a MouseMode. 260 ''' 261 valid_actions = list(touch_action_to_property.keys()) 262 263 def __init__(self, action, modifiers, mode): 264 if action not in self.valid_actions: 265 from chimerax.core.errors import UserError 266 raise UserError('Unrecognised touchpad action! Must be one of: {}'.format( 267 ', '.join(valid_actions) 268 )) 269 self.action = action 270 self.modifiers = modifiers 271 self.mode = mode 272 def matches(self, action, modifiers): 273 ''' 274 Does this binding match the specified action and modifiers? 275 A match requires all of the binding modifiers keys are among 276 the specified modifiers (and possibly more). 277 ''' 278 return (action==self.action and 279 len([k for k in self.modifiers if not k in modifiers]) == 0 280 ) 281 def exact_match(self, action, modifiers): 282 ''' 283 Does this binding exactly match the specified action and modifiers? 284 An exact match requires the binding modifiers keys are exactly the 285 same set as the specified modifier keys. 286 ''' 287 return action == self.action and set(modifiers) == set(self.modifiers) 288 289 290 from .mousemodes import MouseEvent 291 class MultitouchEvent(MouseEvent): 292 ''' 293 Provides an interface to events fired by multi-touch trackpads and modifier 294 keys so that mouse modes do not directly depend on details of the window 295 toolkit or trackpad implementation. 296 ''' 297 def __init__(self, modifiers = None, wheel_value = None, 298 two_finger_trans=None, two_finger_scale=None, two_finger_twist=None, 299 three_finger_trans=None, four_finger_trans=None): 300 super().__init__(event=None, modifiers=modifiers, position=None, wheel_value=wheel_value) 301 self._two_finger_trans = two_finger_trans 302 self._two_finger_scale = two_finger_scale 303 self._two_finger_twist = two_finger_twist 304 self._three_finger_trans = three_finger_trans 305 self._four_finger_trans = four_finger_trans 306 307 @property 308 def modifiers(self): 309 return self._modifiers 310 311 # @property 312 # def event(self): 313 # ''' 314 # The core QTouchEvent object 315 # ''' 316 # return self._event 317 318 @property 319 def wheel_value(self): 320 ''' 321 Supported API. 322 Effective mouse wheel value if two-finger vertical swipe is to be 323 interpreted as a scrolling action. 324 ''' 325 return self._wheel_value 326 327 @property 328 def two_finger_trans(self): 329 ''' 330 Supported API. 331 Returns a tuple (delta_x, delta_y) in screen coordinates representing 332 the movement when a two-finger swipe is interpreted as a translation 333 action. 334 ''' 335 return self._two_finger_trans 336 337 @property 338 def two_finger_scale(self): 339 ''' 340 Supported API 341 Returns a float representing the change in a two-finger pinching action. 342 ''' 343 return self._two_finger_scale 344 345 @property 346 def two_finger_twist(self): 347 ''' 348 Supported API 349 Returns the rotation in degrees defined by a two-finger twisting action. 350 ''' 351 return self._two_finger_twist 352 353 @property 354 def three_finger_trans(self): 355 ''' 356 Supported API 357 Returns a tuple (delta_x, delta_y) in screen coordinates representing 358 the translation in a 3-fingered swipe. 359 ''' 360 return self._three_finger_trans 361 362 @property 363 def four_finger_trans(self): 364 ''' 365 Supported API 366 Returns a tuple (delta_x, delta_y) in screen coordinates representing 367 the translation in a 3-fingered swipe. 368 ''' 369 return self._four_finger_trans