Ticket #3269: mousemodes-1.diff
| File mousemodes-1.diff, 35.1 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..ecb1d0e68
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, event): 160 ''' 161 Supported API. 162 Override this method to take action when a two-finger pinching motion is 163 used on a multitouch touchpad. The scale parameter is available as 164 event.two_finger_scale and is a float, where values larger than 1 165 indicate fingers moving apart and values less than 1 indicate fingers 166 moving together. 167 ''' 168 pass 169 170 def touchpad_two_finger_twist(self, event): 171 ''' 172 Supported API. 173 Override this method to take action when a two-finger twisting motion 174 is used on a multitouch touchpad. The angle parameter is available as 175 event.two_finger_twist, and is a float representing the rotation 176 angle in degrees. 177 ''' 178 pass 179 180 def touchpad_two_finger_trans(self, event): 181 ''' 182 Supported API. 183 Override this method to take action when a two-finger swiping motion is 184 used on a multitouch touchpad. This method should use either 185 event.two_finger_trans (a tuple of two floats delta_x and delta_y) or 186 event.wheel_value (an effective mouse wheel value synthesized from 187 delta_y). delta_x and delta_y are distances expressed as fractions of 188 the total width of the trackpad. 189 ''' 190 pass 191 192 def touchpad_three_finger_trans(self, event): 193 ''' 194 Supported API. 195 Override this method to take action when a three-finger swiping motion 196 is used on a multitouch touchpad. The move parameter is available as 197 event.three_finger_trans and is a tuple of two floats: (delta_x, 198 delta_y) representing the distance moved on the touchpad as a fraction 199 of its width. 200 ''' 201 pass 202 203 def touchpad_four_finger_trans(self, event): 204 ''' 205 Supported API. 206 Override this method to take action when a four-finger swiping motion 207 is used on a multitouch touchpad. The move parameter is available as 208 event.three_finger_trans and is a tuple of two floats: (delta_x, 209 delta_y) representing the distance moved on the touchpad as a fraction 210 of its width. 211 ''' 212 pass 213 214 159 215 def pixel_size(self, center = None, min_scene_frac = 1e-5): 160 216 ''' 161 217 Supported API. … … class MouseMode: 201 257 cfile = inspect.getfile(cls) 202 258 p = path.join(path.dirname(cfile), file) 203 259 return p 204 260 205 261 class MouseBinding: 206 262 ''' 207 263 Associates a mouse button ('left', 'middle', 'right', 'wheel', 'pause') and … … class MouseBinding: 227 283 ''' 228 284 return button == self.button and set(modifiers) == set(self.modifiers) 229 285 286 287 288 289 230 290 class MouseModes: 231 291 ''' 232 292 Keep the list of available mouse modes and also which mode is bound … … class MouseModes: 243 303 self._available_modes = [mode(session) for mode in standard_mouse_mode_classes()] 244 304 245 305 self._bindings = [] # List of MouseBinding instances 306 self._trackpad_bindings = [] # List of MultitouchBinding instances 246 307 247 308 from PyQt5.QtCore import Qt 248 309 # 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 310 253 311 # Mouse pause parameters 254 312 self._last_mouse_time = None … … class MouseModes: 261 319 self._last_mode = None # Remember mode at mouse down and stay with it until mouse up 262 320 263 321 from .trackpad import MultitouchTrackpad 264 self.trackpad = MultitouchTrackpad(session )322 self.trackpad = MultitouchTrackpad(session, self) 265 323 266 def bind_mouse_mode(self, button, modifiers, mode): 324 def bind_mouse_mode(self, mouse_button=None, mouse_modifiers=[], mode=None, 325 trackpad_action=None, trackpad_modifiers=[]): 326 ''' 327 Bind a MouseMode to a mouse click and/or a multitouch trackpad action 328 with optional modifier keys. 329 330 mouse_button is either None or one of ("left", "middle", "right", "wheel", or "pause"). 331 332 trackpad_action is either None or one of ("pinch", "twist", "two finger swipe", 333 "three finger swipe" or "four finger swipe"). 334 335 mouse_modifiers and trackpad_modifiers are each a list of 0 or more of 336 ("alt", "command", "control" or "shift"). 337 338 mode is a MouseMode instance. 339 ''' 340 if mouse_button is not None: 341 self._bind_mouse_mode(mouse_button, mouse_modifiers, mode) 342 if trackpad_action is not None: 343 self._bind_trackpad_mode(trackpad_action, trackpad_modifiers, mode) 344 345 def _bind_mouse_mode(self, button, modifiers, mode): 267 346 ''' 268 347 Button is "left", "middle", "right", "wheel", or "pause". 269 348 Modifiers is a list 0 or more of 'alt', 'command', 'control', 'shift'. … … class MouseModes: 279 358 if button == "right" and not modifiers: 280 359 self.session.triggers.activate_trigger("set right mouse", mode) 281 360 361 def _bind_trackpad_mode(self, action, modifiers, mode): 362 ''' 363 Action is one of ("pinch", "twist", "two finger swipe", 364 "three finger swipe" or "four finger swipe"). Modifiers is a list of 365 0 or more of ("alt", "command", "control" or "shift"). Mode is a 366 MouseMode instance. 367 ''' 368 self.remove_binding(trackpad_action=action, trackpad_modifiers=modifiers) 369 if mode is not None: 370 from .std_modes import NullMouseMode 371 if not isinstance(mode, NullMouseMode): 372 from .trackpad import MultitouchBinding 373 b = MultitouchBinding(action, modifiers, mode) 374 self._trackpad_bindings.append(b) 375 mode.enable() 376 282 377 def bind_standard_mouse_modes(self, buttons = ('left', 'middle', 'right', 'wheel', 'pause')): 283 378 ''' 284 379 Bind the standard mouse modes: left = rotate, ctrl-left = select, middle = translate, 285 380 right = zoom, wheel = zoom, pause = identify object. 286 381 ''' 287 382 standard_modes = ( 288 ('left', [], 'rotate'), 289 ('left', ['control'], 'select'), 290 ('middle', [], 'translate'), 291 ('right', [], 'translate'), 292 ('wheel', [], 'zoom'), 293 ('pause', [], 'identify object'), 383 ('left', [], 'two finger swipe', [], 'rotate'), 384 (None, [], 'twist', [], 'rotate'), 385 ('left', ['control'], None, [], 'select'), 386 ('middle', [], 'three finger swipe', [], 'translate'), 387 ('right', [], None, [], 'translate'), 388 ('wheel', [], 'pinch', [], 'zoom'), 389 ('pause', [], None, [], 'identify object'), 390 (None, [], 'four finger swipe', [], 'swipe as scroll') 294 391 ) 295 392 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]) 393 for button, modifiers, trackpad_action, trackpad_modifiers, mode_name in standard_modes: 394 self.bind_mouse_mode(button, modifiers, mmap[mode_name], trackpad_action, trackpad_modifiers) 299 395 300 396 def add_mode(self, mode): 301 397 '''Supported API. Add a MouseMode instance to the list of available modes.''' … … class MouseModes: 321 417 m = None 322 418 return m 323 419 420 def trackpad_mode(self, action, modifiers=[], exact=False): 421 ''' 422 Return the MouseMode associated with a specific multitouch action and 423 modifiers, or None if no mode is bound. 424 ''' 425 if exact: 426 mb = [b for b in self._trackpad_bindings if b.exact_match(action, modifiers)] 427 else: 428 mb = [b for b in self._trackpad_bindings if b.matches(action, modifiers)] 429 if len(mb) == 1: 430 m = mb[0].mode 431 elif len(mb) > 1: 432 m = max(mb, key = lambda b: len(b.modifiers)).mode 433 else: 434 m = None 435 return m 436 324 437 @property 325 438 def modes(self): 326 439 '''List of MouseMode instances.''' … … class MouseModes: 331 444 if m.name == name: 332 445 return m 333 446 return None 334 447 335 448 def mouse_pause_tracking(self): 336 449 ''' 337 450 Called periodically to check for mouse pause and invoke pause mode. … … class MouseModes: 362 475 self._mouse_pause() 363 476 self._paused = True 364 477 365 def remove_binding(self, button, modifiers): 478 def remove_binding(self, button=None, modifiers=[], 479 trackpad_action=None, trackpad_modifiers=[]): 366 480 ''' 367 481 Unbind the mouse button and modifier key combination. 368 482 No mode will be associated with this button and modifier. 369 483 ''' 370 self._bindings = [b for b in self.bindings if not b.exact_match(button, modifiers)] 484 if button is not None: 485 self._bindings = [b for b in self.bindings if not b.exact_match(button, modifiers)] 486 if trackpad_action is not None: 487 self._trackpad_bindings = [b for b in self._trackpad_bindings if not b.exact_match(trackpad_action, trackpad_modifiers)] 371 488 372 489 def remove_mode(self, mode): 373 490 '''Remove a MouseMode instance from the list of available modes.''' … … class MouseModes: 382 499 def _mouse_buttons_down(self): 383 500 from PyQt5.QtCore import Qt 384 501 return self.session.ui.mouseButtons() != Qt.NoButton 385 502 386 503 def _dispatch_mouse_event(self, event, action): 387 504 button, modifiers = self._event_type(event) 388 505 if button is None: … … class MouseModes: 404 521 self._last_mode = None 405 522 406 523 def _event_type(self, event): 407 modifiers = self._key_modifiers(event)524 modifiers = key_modifiers(event) 408 525 409 526 # button() gives press/release buttons; buttons() gives move buttons 410 527 from PyQt5.QtCore import Qt … … class MouseModes: 449 566 450 567 return button, modifiers 451 568 569 def _dispatch_touch_event(self, touch_event): 570 te = touch_event 571 from .trackpad import touch_action_to_property 572 for action, prop in touch_action_to_property.items(): 573 data = getattr(te, prop) 574 if getattr(touch_event, prop) is None: 575 continue 576 m = self.trackpad_mode(action, te.modifiers) 577 if m is not None: 578 f = getattr(m, 'touchpad_'+prop) 579 f(te) 580 581 582 # t_string = ('Registered touch event: \n' 583 # 'modifer keys pressed: {}\n' 584 # 'wheel_value: {}\n' 585 # 'two_finger_trans: {}\n' 586 # 'two_finger_scale: {}\n' 587 # 'two_finger_twist: {}\n' 588 # 'three_finger_trans: {}\n' 589 # 'four_finger_trans: {}').format( 590 # ', '.join(te._modifiers), 591 # te.wheel_value, 592 # te.two_finger_trans, 593 # te.two_finger_scale, 594 # te.two_finger_twist, 595 # te.three_finger_trans, 596 # te.four_finger_trans 597 # ) 598 # print(t_string) 599 600 452 601 def _have_mode(self, button, modifier): 453 602 for b in self.bindings: 454 603 if b.exact_match(button, [modifier]): 455 604 return True 456 605 return False 457 606 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 607 def _mouse_pause(self): 464 608 m = self.mode('pause') 465 609 if m: … … class MouseModes: 482 626 def _wheel_event(self, event): 483 627 if self.trackpad.discard_trackpad_wheel_event(event): 484 628 return # Trackpad processing handled this event 485 f = self.mode('wheel', self._key_modifiers(event))629 f = self.mode('wheel', key_modifiers(event)) 486 630 if f: 487 631 f.wheel(MouseEvent(event)) 488 632 … … class MouseEvent: 498 642 # for mouse button emulation. 499 643 self._position = position # x,y in pixels, can be None 500 644 self._wheel_value = wheel_value # wheel clicks (usually 1 click equals 15 degrees rotation). 501 645 502 646 def shift_down(self): 503 647 ''' 504 648 Supported API. … … class MouseEvent: 556 700 delta = min(deltas.x(), deltas.y()) 557 701 return delta/120.0 # Usually one wheel click is delta of 120 558 702 return 0 559 703 560 704 def mod_key_info(key_function): 561 705 """Qt swaps control/meta on Mac, so centralize that knowledge here. 562 706 The possible "key_functions" are: alt, control, command, and shift … … def mod_key_info(key_function): 584 728 return Qt.ControlModifier, "control" 585 729 return Qt.MetaModifier, command_name 586 730 731 _function_keys = ["alt", "control", "command", "shift"] 732 _modifier_bits = [(mod_key_info(fkey)[0], fkey) for fkey in _function_keys] 733 734 735 def key_modifiers(event): 736 return decode_modifier_bits(event.modifiers()) 737 738 def decode_modifier_bits(mod): 739 modifiers = [mod_name for bit, mod_name in _modifier_bits if bit & mod] 740 return modifiers 741 742 587 743 def keyboard_modifier_names(qt_keyboard_modifiers): 588 744 from PyQt5.QtCore import Qt 589 745 import sys … … def keyboard_modifier_names(qt_keyboard_modifiers): 601 757 mnames = [mname for mflag, mname in modifiers if mflag & qt_keyboard_modifiers] 602 758 return mnames 603 759 760 761 762 763 604 764 def unpickable(drawing): 605 765 return not getattr(drawing, 'pickable', True) 606 766 607 767 def picked_object(window_x, window_y, view, max_transparent_layers = 3, exclude = unpickable): 608 768 xyz1, xyz2 = view.clip_plane_points(window_x, window_y) 609 769 if xyz1 is None or xyz2 is None: … … def picked_object_on_segment(xyz1, xyz2, view, max_transparent_layers = 3, exclu 621 781 else: 622 782 break 623 783 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..5b0dff48a 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, event): 317 move = event.two_finger_trans 318 if self.mouse_action=='rotate': 319 tp = self.session.ui.mouse_modes.trackpad 320 from math import sqrt 321 dx, dy = move 322 turns = sqrt(dx*dx + dy*dy)*tp.full_width_translation_distance/tp.full_rotation_distance 323 angle = tp.trackpad_speed*360*turns 324 self._rotate((dy, dx, 0), angle) 325 326 def touchpad_three_finger_trans(self, event): 327 dx, dy = event.three_finger_trans 328 if self.mouse_action=='translate': 329 tp = self.session.ui.mouse_modes.trackpad 330 ww = self.session.view.window_size[0] # window width in pixels 331 s = tp.trackpad_speed*ww 332 self._translate((s*dx, -s*dy, 0)) 333 334 def touchpad_two_finger_twist(self, event): 335 angle = event.two_finger_twist 336 if self.mouse_action=='rotate': 337 self._rotate((0,0,1), angle) 338 339 315 340 def _set_z_rotation(self, event): 316 341 x,y = event.position() 317 342 w,h = self.view.window_size … … class MoveMouseMode(MouseMode): 372 397 self._move_atoms(translation(step)) 373 398 else: 374 399 self.view.translate(step, self.models()) 375 400 376 401 def _translation(self, event): 377 402 '''Returned shift is in camera coordinates.''' 378 403 dx, dy = self.mouse_motion(event) … … class MoveMouseMode(MouseMode): 401 426 @property 402 427 def _moving_atoms(self): 403 428 return self.move_atoms and self._atoms is not None and len(self._atoms) > 0 404 429 405 430 def _move_atoms(self, transform): 406 431 atoms = self._atoms 407 432 atoms.scene_coords = transform * atoms.scene_coords … … class MoveMouseMode(MouseMode): 444 469 from chimerax.atomic import selected_atoms 445 470 self._atoms = selected_atoms(self.session) 446 471 self._undo_start() 447 472 448 473 def vr_motion(self, event): 449 474 # Virtual reality hand controller motion. 450 475 if self._moving_atoms: … … class MoveMouseMode(MouseMode): 452 477 else: 453 478 self.view.move(event.motion, self.models()) 454 479 self._moved = True 455 480 456 481 def vr_release(self, event): 457 482 # Virtual reality hand controller button release. 458 483 self._undo_save() 459 484 460 485 class RotateMouseMode(MoveMouseMode): 461 486 ''' 462 487 Mouse mode to rotate objects (actually the camera is moved) by dragging. … … class RotateSelectedAtomsMouseMode(RotateMouseMode): 560 585 name = 'rotate selected atoms' 561 586 icon_file = 'icons/rotate_atoms.png' 562 587 move_atoms = True 563 588 564 589 class ZoomMouseMode(MouseMode): 565 590 ''' 566 591 Mouse mode to move objects in z, actually the camera is moved … … class ZoomMouseMode(MouseMode): 572 597 MouseMode.__init__(self, session) 573 598 self.speed = 1 574 599 575 def mouse_drag(self, event): 600 def mouse_drag(self, event): 576 601 577 602 dx, dy = self.mouse_motion(event) 578 603 psize = self.pixel_size() … … class ZoomMouseMode(MouseMode): 585 610 delta_z = 100*d*psize*self.speed 586 611 self.zoom(delta_z, stereo_scaling = not event.alt_down()) 587 612 613 def touchpad_two_finger_scale(self, event): 614 scale = event.two_finger_scale 615 v = self.session.view 616 wpix = v.window_size[0] 617 psize = v.pixel_size() 618 d = (scale-1)*wpix*psize 619 self.zoom(d) 620 588 621 def zoom(self, delta_z, stereo_scaling = False): 589 622 v = self.view 590 623 c = v.camera … … class ZoomMouseMode(MouseMode): 597 630 else: 598 631 shift = c.position.transform_vector((0, 0, delta_z)) 599 632 v.translate(shift) 600 633 601 634 class ObjectIdMouseMode(MouseMode): 602 635 ''' 603 636 Mouse mode to that shows the name of an object in a popup window … … class ObjectIdMouseMode(MouseMode): 607 640 def __init__(self, session): 608 641 MouseMode.__init__(self, session) 609 642 session.triggers.add_trigger('mouse hover') 610 643 611 644 def pause(self, position): 612 645 ui = self.session.ui 613 646 if ui.activeWindow() is None: … … class AtomCenterOfRotationMode(MouseMode): 677 710 return 678 711 from chimerax.std_commands import cofr 679 712 cofr.cofr(self.session, pivot=xyz) 680 713 681 714 class NullMouseMode(MouseMode): 682 715 '''Used to assign no mode to a mouse button.''' 683 716 name = 'none' … … class ClipMouseMode(MouseMode): 720 753 front_shift = 1 if shift or not alt else 0 721 754 back_shift = 0 if not (alt or shift) else (1 if alt and shift else -1) 722 755 return front_shift, back_shift 723 756 724 757 def wheel(self, event): 725 758 d = event.wheel_value() 726 759 psize = self.pixel_size() … … class ClipMouseMode(MouseMode): 766 799 use_scene_planes = (clip_settings.mouse_clip_plane_type == 'scene planes') 767 800 else: 768 801 use_scene_planes = (p.find_plane('front') or p.find_plane('back')) 769 802 770 803 pfname, pbname = ('front','back') if use_scene_planes else ('near','far') 771 804 772 805 pf, pb = p.find_plane(pfname), p.find_plane(pbname) 773 806 from chimerax.std_commands.clip import adjust_plane 774 807 c = v.camera … … class ClipRotateMouseMode(MouseMode): 897 930 p.normal = move.transform_vector(p.normal) 898 931 p.plane_point = move * p.plane_point 899 932 933 class SwipeAsScrollMouseMode(MouseMode): 934 ''' 935 Reinterprets the vertical component of a multi-touch swiping action as a 936 mouse scroll, and passes it on to the currently-mapped mode. 937 ''' 938 name = 'swipe as scroll' 939 def _scrollwheel_mode(self, modifiers): 940 return self.session.ui.mouse_modes.mode(button='wheel', modifiers=modifiers) 941 942 def _wheel_value(self, dy): 943 tp = self.session.ui.mouse_modes.trackpad 944 speed = tp.trackpad_speed 945 wcp = tp.wheel_click_pixels 946 fwd = tp.full_width_translation_distance 947 delta = speed * dy * fwd / wcp 948 return delta 949 950 def touchpad_two_finger_trans(self, event): 951 self._pass_event(event, 'two_finger_trans') 952 953 def touchpad_three_finger_trans(self, event): 954 self._pass_event(event, 'three_finger_trans') 955 956 def touchpad_four_finger_trans(self, event): 957 self._pass_event(event, 'four_finger_trans') 958 959 def _pass_event(self, event, event_type): 960 _, dy = getattr(event, event_type) 961 swm = self._scrollwheel_mode(event.modifiers) 962 if swm: 963 wv = self._wheel_value(dy) 964 from .mousemodes import MouseEvent 965 swm.wheel(MouseEvent(position=event.position, wheel_value=wv, modifiers=event.modifiers)) 966 967 968 900 969 def standard_mouse_mode_classes(): 901 970 '''List of core MouseMode classes.''' 902 971 mode_classes = [ … … def standard_mouse_mode_classes(): 918 987 ClipRotateMouseMode, 919 988 ObjectIdMouseMode, 920 989 AtomCenterOfRotationMode, 990 SwipeAsScrollMouseMode, 921 991 NullMouseMode, 922 992 ] 923 993 return mode_classes -
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..c3e52f941
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 48 @property 49 def wheel_click_pixels(self): 50 return self._wheel_click_pixels 51 37 52 def set_graphics_window(self, graphics_window): 38 53 graphics_window.touchEvent = self._touch_event 39 54 self._enable_touch_events(graphics_window) … … class MultitouchTrackpad: 50 65 t.remove_handler(h) 51 66 h = None 52 67 self._touch_handler = h 53 68 54 69 def _enable_touch_events(self, graphics_window): 55 70 from sys import platform 56 71 if platform == 'darwin': … … class MultitouchTrackpad: 69 84 w.setAttribute(Qt.WA_AcceptTouchEvents) 70 85 print('graphics widget touch enabled', w.testAttribute(Qt.WA_AcceptTouchEvents)) 71 86 ''' 72 87 73 88 # Appears that Qt has disabled touch events on Mac due to unresolved scrolling lag problems. 74 89 # Searching for qt setAcceptsTouchEvents shows they were disabled Oct 17, 2012. 75 90 # A patch that allows an environment variable QT_MAC_ENABLE_TOUCH_EVENTS to allow touch … … class MultitouchTrackpad: 84 99 85 100 from PyQt5.QtCore import QEvent 86 101 t = event.type() 102 # For some unfathomable reason the QTouchEvent.modifiers() method always 103 # returns zero (QTBUG-60389, unresolved since 2017). So we need to do a 104 # little hacky workaround 105 106 from .mousemodes import decode_modifier_bits 107 # session.ui.keyboardModifiers() does *not* work here (always returns 0) 108 mb = int(self._session.ui.queryKeyboardModifiers()) 109 self._modifier_keys = decode_modifier_bits(mb) 110 111 87 112 if t == QEvent.TouchUpdate: 88 # On Mac touch events get backlogged in queue when the events cause 113 # On Mac touch events get backlogged in queue when the events cause 89 114 # time consuming computatation. It appears Qt does not collapse the events. 90 115 # So event processing can get tens of seconds behind. To reduce this problem 91 116 # we only handle the most recent touch update per redraw. … … class MultitouchTrackpad: 100 125 def _collapse_touch_events(self): 101 126 touches = self._recent_touches 102 127 if touches: 103 self._process_touches(touches)128 event = self._process_touches(touches) 104 129 self._recent_touches = [] 130 self._mouse_mode_mgr._dispatch_touch_event(event) 105 131 106 132 def _process_touches(self, touches): 133 pinch = twist = scroll = None 134 two_swipe = None 135 three_swipe = None 136 four_swipe = None 107 137 n = len(touches) 108 138 speed = self.trackpad_speed 139 position = (sum(t.x for t in touches)/n, sum(t.y for t in touches)/n) 109 140 moves = [t.move(self._last_touch_locations) for t in touches] 141 dx = sum(x for x,y in moves)/n 142 dy = sum(y for x,y in moves)/n 143 110 144 if n == 2: 111 145 (dx0,dy0),(dx1,dy1) = moves[0], moves[1] 112 146 from math import sqrt, exp, atan2, pi 113 147 l0,l1 = sqrt(dx0*dx0 + dy0*dy0),sqrt(dx1*dx1 + dy1*dy1) 114 148 d12 = dx0*dx1+dy0*dy1 115 149 if d12 < 0: 116 # Finger moving in opposite directions: pinch ortwist150 # Finger moving in opposite directions: pinch/twist 117 151 (x0,y0),(x1,y1) = [(t.x,t.y) for t in touches[:2]] 118 152 sx,sy = x1-x0,y1-y0 119 153 sn = sqrt(sx*sx + sy*sy) 120 154 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) 155 zf = 1 + speed * self._zoom_scaling * (l0+l1) / self._full_width_translation_distance 156 if sd1 < 0: 157 zf = 1/zf 158 pinch = zf 159 rot = atan2(-sy*dx1+sx*dy1,sn*sn) + atan2(sy*dx0-sx*dy0,sn*sn) 160 a = -speed * self._twist_scaling * rot * 180 / pi 161 twist = a 162 else: 163 two_swipe = tuple([d/self._full_width_translation_distance for d in (dx, dy)]) 164 scroll = speed * dy / self._wheel_click_pixels 141 165 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)) 166 three_swipe = tuple([d/self._full_width_translation_distance for d in (dx, dy)]) 147 167 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)) 168 four_swipe = tuple([d/self._full_width_translation_distance for d in (dx, dy)]) 169 170 return MultitouchEvent(modifiers=self._modifier_keys, 171 position=position, wheel_value=scroll, two_finger_trans=two_swipe, two_finger_scale=pinch, 172 two_finger_twist=twist, three_finger_trans=three_swipe, 173 four_finger_trans=four_swipe) 174 175 return pinch, twist, scroll, two_swipe, three_swipe, four_swipe 158 176 159 177 def _rotate(self, screen_axis, angle): 160 178 if angle == 0: … … class Touch: 230 248 x,y = self.x, self.y 231 249 last_touch_locations[id] = (x,y) 232 250 return (x-lx, y-ly) 251 252 touch_action_to_property = { 253 'pinch': 'two_finger_scale', 254 'twist': 'two_finger_twist', 255 'two finger swipe': 'two_finger_trans', 256 'three finger swipe': 'three_finger_trans', 257 'four finger swipe': 'four_finger_trans', 258 } 259 260 261 class MultitouchBinding: 262 ''' 263 Associates an action on a multitouch trackpad and a set of modifier keys 264 ('alt', 'command', 'control', 'shift') with a MouseMode. 265 ''' 266 valid_actions = list(touch_action_to_property.keys()) 267 268 def __init__(self, action, modifiers, mode): 269 if action not in self.valid_actions: 270 from chimerax.core.errors import UserError 271 raise UserError('Unrecognised touchpad action! Must be one of: {}'.format( 272 ', '.join(valid_actions) 273 )) 274 self.action = action 275 self.modifiers = modifiers 276 self.mode = mode 277 def matches(self, action, modifiers): 278 ''' 279 Does this binding match the specified action and modifiers? 280 A match requires all of the binding modifiers keys are among 281 the specified modifiers (and possibly more). 282 ''' 283 return (action==self.action and 284 len([k for k in self.modifiers if not k in modifiers]) == 0 285 ) 286 def exact_match(self, action, modifiers): 287 ''' 288 Does this binding exactly match the specified action and modifiers? 289 An exact match requires the binding modifiers keys are exactly the 290 same set as the specified modifier keys. 291 ''' 292 return action == self.action and set(modifiers) == set(self.modifiers) 293 294 295 from .mousemodes import MouseEvent 296 class MultitouchEvent(MouseEvent): 297 ''' 298 Provides an interface to events fired by multi-touch trackpads and modifier 299 keys so that mouse modes do not directly depend on details of the window 300 toolkit or trackpad implementation. 301 ''' 302 def __init__(self, modifiers = None, position=None, wheel_value = None, 303 two_finger_trans=None, two_finger_scale=None, two_finger_twist=None, 304 three_finger_trans=None, four_finger_trans=None): 305 super().__init__(event=None, modifiers=modifiers, position=position, wheel_value=wheel_value) 306 self._two_finger_trans = two_finger_trans 307 self._two_finger_scale = two_finger_scale 308 self._two_finger_twist = two_finger_twist 309 self._three_finger_trans = three_finger_trans 310 self._four_finger_trans = four_finger_trans 311 312 @property 313 def modifiers(self): 314 return self._modifiers 315 316 # @property 317 # def event(self): 318 # ''' 319 # The core QTouchEvent object 320 # ''' 321 # return self._event 322 323 @property 324 def wheel_value(self): 325 ''' 326 Supported API. 327 Effective mouse wheel value if two-finger vertical swipe is to be 328 interpreted as a scrolling action. 329 ''' 330 return self._wheel_value 331 332 @property 333 def two_finger_trans(self): 334 ''' 335 Supported API. 336 Returns a tuple (delta_x, delta_y) in screen coordinates representing 337 the movement when a two-finger swipe is interpreted as a translation 338 action. 339 ''' 340 return self._two_finger_trans 341 342 @property 343 def two_finger_scale(self): 344 ''' 345 Supported API 346 Returns a float representing the change in a two-finger pinching action. 347 ''' 348 return self._two_finger_scale 349 350 @property 351 def two_finger_twist(self): 352 ''' 353 Supported API 354 Returns the rotation in degrees defined by a two-finger twisting action. 355 ''' 356 return self._two_finger_twist 357 358 @property 359 def three_finger_trans(self): 360 ''' 361 Supported API 362 Returns a tuple (delta_x, delta_y) in screen coordinates representing 363 the translation in a 3-fingered swipe. 364 ''' 365 return self._three_finger_trans 366 367 @property 368 def four_finger_trans(self): 369 ''' 370 Supported API 371 Returns a tuple (delta_x, delta_y) in screen coordinates representing 372 the translation in a 3-fingered swipe. 373 ''' 374 return self._four_finger_trans