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/src/bundles/mouse_modes/src/mousemodes.py
+++ b/src/bundles/mouse_modes/src/mousemodes.py
@@ -64,7 +64,7 @@ class MouseMode:
 
     def enable(self):
         '''
-        Supported API. 
+        Supported API.
         Called when mouse mode is enabled.
         Override if mode wants to know that it has been bound to a mouse button.
         '''
@@ -139,7 +139,7 @@ class MouseMode:
     def uses_wheel(self):
         '''Return True if derived class implements the wheel() method.'''
         return getattr(self, 'wheel') != MouseMode.wheel
-    
+
     def pause(self, position):
         '''
         Supported API.
@@ -156,6 +156,56 @@ class MouseMode:
         '''
         pass
 
+    def touchpad_two_finger_scale(self, scale):
+        '''
+        Supported API.
+        Override this method to take action when a two-finger pinching motion
+        is used on a multitouch touchpad. The scale parameter is a float, where
+        values larger than 1 indicate fingers moving apart and values less than
+        1 indicate fingers moving together.
+        '''
+        pass
+
+    def touchpad_two_finger_twist(self, angle):
+        '''
+        Supported API.
+        Override this method to take action when a two-finger twisting motion
+        is used on a multitouch touchpad. The angle parameter is the rotation
+        angle in degrees.
+        '''
+        pass
+
+    def touchpad_two_finger_trans(self, move):
+        '''
+        Supported API.
+        Override this method to take action when a two-finger swiping motion  is
+        used on a multitouch touchpad. The move parameter is a tuple of two
+        floats: (delta_x, delta_y), where delta_x and delta_y are
+        distances expressed as fractions of the total width of the trackpad
+        '''
+        pass
+
+    def touchpad_three_finger_trans(self, move):
+        '''
+        Supported API.
+        Override this method to take action when a three-finger swiping motion
+        is used on a multitouch touchpad. The move parameter is a tuple of two
+        floats: (delta_x, delta_y) representing the distance moved on the
+        touchpad as a fraction of its width.
+        '''
+        pass
+
+    def touchpad_four_finger_trans(self, move):
+        '''
+        Supported API.
+        Override this method to take action when a four-finger swiping motion
+        is used on a multitouch touchpad. The move parameter is a tuple of two
+        floats: (delta_x, delta_y) representing the distance moved on the
+        touchpad as a fraction of its width.
+        '''
+        pass
+
+
     def pixel_size(self, center = None, min_scene_frac = 1e-5):
         '''
         Supported API.
@@ -201,7 +251,7 @@ class MouseMode:
         cfile = inspect.getfile(cls)
         p = path.join(path.dirname(cfile), file)
         return p
-    
+
 class MouseBinding:
     '''
     Associates a mouse button ('left', 'middle', 'right', 'wheel', 'pause') and
@@ -227,6 +277,10 @@ class MouseBinding:
         '''
         return button == self.button and set(modifiers) == set(self.modifiers)
 
+
+
+
+
 class MouseModes:
     '''
     Keep the list of available mouse modes and also which mode is bound
@@ -243,12 +297,10 @@ class MouseModes:
         self._available_modes = [mode(session) for mode in standard_mouse_mode_classes()]
 
         self._bindings = []  # List of MouseBinding instances
+        self._trackpad_bindings = [] # List of MultitouchBinding instances
 
         from PyQt5.QtCore import Qt
         # Qt maps control to meta on Mac...
-        self._modifier_bits = []
-        for keyfunc in ["alt", "control", "command", "shift"]:
-            self._modifier_bits.append((mod_key_info(keyfunc)[0], keyfunc))
 
         # Mouse pause parameters
         self._last_mouse_time = None
@@ -261,9 +313,30 @@ class MouseModes:
         self._last_mode = None			# Remember mode at mouse down and stay with it until mouse up
 
         from .trackpad import MultitouchTrackpad
-        self.trackpad = MultitouchTrackpad(session)
+        self.trackpad = MultitouchTrackpad(session, self)
 
-    def bind_mouse_mode(self, button, modifiers, mode):
+    def bind_mouse_mode(self, mouse_button=None, mouse_modifiers=[], mode=None,
+            trackpad_action=None, trackpad_modifiers=[]):
+        '''
+        Bind a MouseMode to a mouse click and/or a multitouch trackpad action
+        with optional modifier keys.
+
+        mouse_button is either None or one of ("left", "middle", "right", "wheel", or "pause").
+
+        trackpad_action is either None or one of ("pinch", "twist", "two finger swipe",
+        "three finger swipe" or "four finger swipe").
+
+        mouse_modifiers and trackpad_modifiers are each a list of 0 or more of
+        ("alt", "command", "control" or "shift").
+
+        mode is a MouseMode instance.
+        '''
+        if mouse_button is not None:
+            self._bind_mouse_mode(mouse_button, mouse_modifiers, mode)
+        if trackpad_action is not None:
+            self._bind_trackpad_mode(trackpad_action, trackpad_modifiers, mode)
+
+    def _bind_mouse_mode(self, button, modifiers, mode):
         '''
         Button is "left", "middle", "right", "wheel", or "pause".
         Modifiers is a list 0 or more of 'alt', 'command', 'control', 'shift'.
@@ -279,23 +352,39 @@ class MouseModes:
         if button == "right" and not modifiers:
             self.session.triggers.activate_trigger("set right mouse", mode)
 
+    def _bind_trackpad_mode(self, action, modifiers, mode):
+        '''
+        Action is one of ("pinch", "twist", "two finger swipe",
+        "three finger swipe" or "four finger swipe"). Modifiers is a list of
+        0 or more of ("alt", "command", "control" or "shift"). Mode is a
+        MouseMode instance.
+        '''
+        self.remove_binding(trackpad_action=action, trackpad_modifiers=modifiers)
+        if mode is not None:
+            from .std_modes import NullMouseMode
+            if not isinstance(mode, NullMouseMode):
+                from .trackpad import MultitouchBinding
+                b = MultitouchBinding(action, modifiers, mode)
+                self._trackpad_bindings.append(b)
+                mode.enable()
+
     def bind_standard_mouse_modes(self, buttons = ('left', 'middle', 'right', 'wheel', 'pause')):
         '''
         Bind the standard mouse modes: left = rotate, ctrl-left = select, middle = translate,
         right = zoom, wheel = zoom, pause = identify object.
         '''
         standard_modes = (
-            ('left', [], 'rotate'),
-            ('left', ['control'], 'select'),
-            ('middle', [], 'translate'),
-            ('right', [], 'translate'),
-            ('wheel', [], 'zoom'),
-            ('pause', [], 'identify object'),
+            ('left', [], 'two finger swipe', [], 'rotate'),
+            (None, [], 'twist', [], 'rotate'),
+            ('left', ['control'], None, [], 'select'),
+            ('middle', [], 'three finger swipe', [], 'translate'),
+            ('right', [], None, [], 'translate'),
+            ('wheel', [], 'pinch', [], 'zoom'),
+            ('pause', [], None, [], 'identify object'),
             )
         mmap = {m.name:m for m in self.modes}
-        for button, modifiers, mode_name in standard_modes:
-            if button in buttons:
-                self.bind_mouse_mode(button, modifiers, mmap[mode_name])
+        for button, modifiers, trackpad_action, trackpad_modifiers, mode_name in standard_modes:
+            self.bind_mouse_mode(button, modifiers, mmap[mode_name], trackpad_action, trackpad_modifiers)
 
     def add_mode(self, mode):
         '''Supported API. Add a MouseMode instance to the list of available modes.'''
@@ -321,6 +410,23 @@ class MouseModes:
             m = None
         return m
 
+    def trackpad_mode(self, action, modifiers=[], exact=False):
+        '''
+        Return the MouseMode associated with a specific multitouch action and
+        modifiers, or None if no mode is bound.
+        '''
+        if exact:
+            mb = [b for b in self._trackpad_bindings if b.exact_match(action, modifiers)]
+        else:
+            mb = [b for b in self._trackpad_bindings if b.matches(action, modifiers)]
+        if len(mb) == 1:
+            m = mb[0].mode
+        elif len(mb) > 1:
+            m = max(mb, key = lambda b: len(b.modifiers)).mode
+        else:
+            m = None
+        return m
+
     @property
     def modes(self):
         '''List of MouseMode instances.'''
@@ -331,7 +437,7 @@ class MouseModes:
             if m.name == name:
                 return m
         return None
-    
+
     def mouse_pause_tracking(self):
         '''
         Called periodically to check for mouse pause and invoke pause mode.
@@ -362,12 +468,16 @@ class MouseModes:
                 self._mouse_pause()
                 self._paused = True
 
-    def remove_binding(self, button, modifiers):
+    def remove_binding(self, button=None, modifiers=[],
+            trackpad_action=None, trackpad_modifiers=[]):
         '''
         Unbind the mouse button and modifier key combination.
         No mode will be associated with this button and modifier.
         '''
-        self._bindings = [b for b in self.bindings if not b.exact_match(button, modifiers)]
+        if button is not None:
+            self._bindings = [b for b in self.bindings if not b.exact_match(button, modifiers)]
+        if trackpad_action is not None:
+            self._trackpad_bindings = [b for b in self._trackpad_bindings if not b.exact_match(trackpad_action, trackpad_modifiers)]
 
     def remove_mode(self, mode):
         '''Remove a MouseMode instance from the list of available modes.'''
@@ -382,7 +492,7 @@ class MouseModes:
     def _mouse_buttons_down(self):
         from PyQt5.QtCore import Qt
         return self.session.ui.mouseButtons() != Qt.NoButton
-        
+
     def _dispatch_mouse_event(self, event, action):
         button, modifiers = self._event_type(event)
         if button is None:
@@ -404,7 +514,7 @@ class MouseModes:
             self._last_mode = None
 
     def _event_type(self, event):
-        modifiers = self._key_modifiers(event)
+        modifiers = key_modifiers(event)
 
         # button() gives press/release buttons; buttons() gives move buttons
         from PyQt5.QtCore import Qt
@@ -449,17 +559,44 @@ class MouseModes:
 
         return button, modifiers
 
+    def _dispatch_touch_event(self, touch_event):
+        te = touch_event
+        from .trackpad import touch_action_to_property
+        for action, prop in touch_action_to_property.items():
+            data = getattr(touch_event, prop)
+            if getattr(touch_event, prop) is None:
+                continue
+            m = self.trackpad_mode(action, te.modifiers)
+            if m is not None:
+                f = getattr(m, 'touchpad_'+prop)
+                f(data)
+
+
+        # t_string = ('Registered touch event: \n'
+        #     'modifer keys pressed: {}\n'
+        #     'wheel_value: {}\n'
+        #     'two_finger_trans: {}\n'
+        #     'two_finger_scale: {}\n'
+        #     'two_finger_twist: {}\n'
+        #     'three_finger_trans: {}\n'
+        #     'four_finger_trans: {}').format(
+        #         ', '.join(te._modifiers),
+        #         te.wheel_value,
+        #         te.two_finger_trans,
+        #         te.two_finger_scale,
+        #         te.two_finger_twist,
+        #         te.three_finger_trans,
+        #         te.four_finger_trans
+        #     )
+        # print(t_string)
+
+
     def _have_mode(self, button, modifier):
         for b in self.bindings:
             if b.exact_match(button, [modifier]):
                 return True
         return False
 
-    def _key_modifiers(self, event):
-        mod = event.modifiers()
-        modifiers = [mod_name for bit, mod_name in self._modifier_bits if bit & mod]
-        return modifiers
-
     def _mouse_pause(self):
         m = self.mode('pause')
         if m:
@@ -482,7 +619,7 @@ class MouseModes:
     def _wheel_event(self, event):
         if self.trackpad.discard_trackpad_wheel_event(event):
             return	# Trackpad processing handled this event
-        f = self.mode('wheel', self._key_modifiers(event))
+        f = self.mode('wheel', key_modifiers(event))
         if f:
             f.wheel(MouseEvent(event))
 
@@ -498,7 +635,7 @@ class MouseEvent:
                                         # for mouse button emulation.
         self._position = position	# x,y in pixels, can be None
         self._wheel_value = wheel_value # wheel clicks (usually 1 click equals 15 degrees rotation).
-        
+
     def shift_down(self):
         '''
         Supported API.
@@ -556,7 +693,7 @@ class MouseEvent:
                 delta = min(deltas.x(), deltas.y())
             return delta/120.0   # Usually one wheel click is delta of 120
         return 0
-        
+
 def mod_key_info(key_function):
     """Qt swaps control/meta on Mac, so centralize that knowledge here.
     The possible "key_functions" are: alt, control, command, and shift
@@ -584,6 +721,18 @@ def mod_key_info(key_function):
             return Qt.ControlModifier, "control"
         return Qt.MetaModifier, command_name
 
+_function_keys = ["alt", "control", "command", "shift"]
+_modifier_bits = [(mod_key_info(fkey)[0], fkey) for fkey in _function_keys]
+
+
+def key_modifiers(event):
+    return decode_modifier_bits(event.modifiers())
+
+def decode_modifier_bits(mod):
+    modifiers = [mod_name for bit, mod_name in _modifier_bits if bit & mod]
+    return modifiers
+
+
 def keyboard_modifier_names(qt_keyboard_modifiers):
     from PyQt5.QtCore import Qt
     import sys
@@ -601,9 +750,13 @@ def keyboard_modifier_names(qt_keyboard_modifiers):
     mnames = [mname for mflag, mname in modifiers if mflag & qt_keyboard_modifiers]
     return mnames
 
+
+
+
+
 def unpickable(drawing):
     return not getattr(drawing, 'pickable', True)
-    
+
 def picked_object(window_x, window_y, view, max_transparent_layers = 3, exclude = unpickable):
     xyz1, xyz2 = view.clip_plane_points(window_x, window_y)
     if xyz1 is None or xyz2 is None:
@@ -621,4 +774,3 @@ def picked_object_on_segment(xyz1, xyz2, view, max_transparent_layers = 3, exclu
         else:
             break
     return p2 if p2 else p
-
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/src/bundles/mouse_modes/src/std_modes.py
+++ b/src/bundles/mouse_modes/src/std_modes.py
@@ -172,7 +172,7 @@ class SelectSubtractMouseMode(SelectMouseMode):
     '''Mouse mode to subtract objects from selection by clicking on them.'''
     name = 'select subtract'
     icon_file = None
-    
+
 class SelectToggleMouseMode(SelectMouseMode):
     '''Mouse mode to toggle selected objects by clicking on them.'''
     name = 'select toggle'
@@ -264,7 +264,7 @@ class MoveMouseMode(MouseMode):
         # Undo
         self._starting_atom_scene_coords = None
         self._starting_model_positions = None
-        
+
     def mouse_down(self, event):
         MouseMode.mouse_down(self, event)
         if self.action(event) == 'rotate':
@@ -283,6 +283,7 @@ class MoveMouseMode(MouseMode):
             self._translate(shift)
         self._moved = True
 
+
     def mouse_up(self, event):
         if self.click_to_select:
             if event.position() == self.mouse_down_position:
@@ -294,7 +295,7 @@ class MoveMouseMode(MouseMode):
 
         if self.move_atoms:
             self._atoms = None
-        
+
     def wheel(self, event):
         d = event.wheel_value()
         if self.move_atoms:
@@ -311,7 +312,29 @@ class MoveMouseMode(MouseMode):
             # Holding shift key switches between rotation and translation
             a = 'translate' if a == 'rotate' else 'rotate'
         return a
-    
+
+    def touchpad_two_finger_trans(self, move):
+        if self.mouse_action=='rotate':
+            tp = self.session.ui.mouse_modes.trackpad
+            from math import sqrt
+            dx, dy = move
+            turns = sqrt(dx*dx + dy*dy)*tp.full_width_translation_distance/tp.full_rotation_distance
+            angle = tp.trackpad_speed*360*turns
+            self._rotate((dy, dx, 0), angle)
+
+    def touchpad_three_finger_trans(self, move):
+        dx, dy = move
+        if self.mouse_action=='translate':
+            tp = self.session.ui.mouse_modes.trackpad
+            ww = self.session.view.window_size[0] # window width in pixels
+            s = tp.trackpad_speed*ww
+            self._translate((s*dx, -s*dy, 0))
+
+    def touchpad_two_finger_twist(self, angle):
+        if self.mouse_action=='rotate':
+            self._rotate((0,0,1), angle)
+
+
     def _set_z_rotation(self, event):
         x,y = event.position()
         w,h = self.view.window_size
@@ -372,7 +395,7 @@ class MoveMouseMode(MouseMode):
             self._move_atoms(translation(step))
         else:
             self.view.translate(step, self.models())
-        
+
     def _translation(self, event):
         '''Returned shift is in camera coordinates.'''
         dx, dy = self.mouse_motion(event)
@@ -401,7 +424,7 @@ class MoveMouseMode(MouseMode):
     @property
     def _moving_atoms(self):
         return self.move_atoms and self._atoms is not None and len(self._atoms) > 0
-        
+
     def _move_atoms(self, transform):
         atoms = self._atoms
         atoms.scene_coords = transform * atoms.scene_coords
@@ -444,7 +467,7 @@ class MoveMouseMode(MouseMode):
             from chimerax.atomic import selected_atoms
             self._atoms = selected_atoms(self.session)
         self._undo_start()
-        
+
     def vr_motion(self, event):
         # Virtual reality hand controller motion.
         if self._moving_atoms:
@@ -452,11 +475,11 @@ class MoveMouseMode(MouseMode):
         else:
             self.view.move(event.motion, self.models())
         self._moved = True
-        
+
     def vr_release(self, event):
         # Virtual reality hand controller button release.
         self._undo_save()
-        
+
 class RotateMouseMode(MoveMouseMode):
     '''
     Mouse mode to rotate objects (actually the camera is moved) by dragging.
@@ -560,7 +583,7 @@ class RotateSelectedAtomsMouseMode(RotateMouseMode):
     name = 'rotate selected atoms'
     icon_file = 'icons/rotate_atoms.png'
     move_atoms = True
-        
+
 class ZoomMouseMode(MouseMode):
     '''
     Mouse mode to move objects in z, actually the camera is moved
@@ -572,7 +595,7 @@ class ZoomMouseMode(MouseMode):
         MouseMode.__init__(self, session)
         self.speed = 1
 
-    def mouse_drag(self, event):        
+    def mouse_drag(self, event):
 
         dx, dy = self.mouse_motion(event)
         psize = self.pixel_size()
@@ -585,6 +608,13 @@ class ZoomMouseMode(MouseMode):
         delta_z = 100*d*psize*self.speed
         self.zoom(delta_z, stereo_scaling = not event.alt_down())
 
+    def touchpad_two_finger_scale(self, scale):
+        v = self.session.view
+        wpix = v.window_size[0]
+        psize = v.pixel_size()
+        d = (scale-1)*wpix*psize
+        self.zoom(d)
+
     def zoom(self, delta_z, stereo_scaling = False):
         v = self.view
         c = v.camera
@@ -597,7 +627,7 @@ class ZoomMouseMode(MouseMode):
         else:
             shift = c.position.transform_vector((0, 0, delta_z))
             v.translate(shift)
-        
+
 class ObjectIdMouseMode(MouseMode):
     '''
     Mouse mode to that shows the name of an object in a popup window
@@ -607,7 +637,7 @@ class ObjectIdMouseMode(MouseMode):
     def __init__(self, session):
         MouseMode.__init__(self, session)
         session.triggers.add_trigger('mouse hover')
-        
+
     def pause(self, position):
         ui = self.session.ui
         if ui.activeWindow() is None:
@@ -677,7 +707,7 @@ class AtomCenterOfRotationMode(MouseMode):
             return
         from chimerax.std_commands import cofr
         cofr.cofr(self.session, pivot=xyz)
-           
+
 class NullMouseMode(MouseMode):
     '''Used to assign no mode to a mouse button.'''
     name = 'none'
@@ -720,7 +750,7 @@ class ClipMouseMode(MouseMode):
         front_shift = 1 if shift or not alt else 0
         back_shift = 0 if not (alt or shift) else (1 if alt and shift else -1)
         return front_shift, back_shift
-    
+
     def wheel(self, event):
         d = event.wheel_value()
         psize = self.pixel_size()
@@ -766,9 +796,9 @@ class ClipMouseMode(MouseMode):
             use_scene_planes = (clip_settings.mouse_clip_plane_type == 'scene planes')
         else:
             use_scene_planes = (p.find_plane('front') or p.find_plane('back'))
-                
+
         pfname, pbname = ('front','back') if use_scene_planes else ('near','far')
-        
+
         pf, pb = p.find_plane(pfname), p.find_plane(pbname)
         from chimerax.std_commands.clip import adjust_plane
         c = v.camera
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/src/bundles/mouse_modes/src/trackpad.py
+++ b/src/bundles/mouse_modes/src/trackpad.py
@@ -17,10 +17,13 @@ class MultitouchTrackpad:
     and three finger drag translate scene,
     and two finger pinch zoom scene.
     '''
-    def __init__(self, session):
+
+    def __init__(self, session, mouse_mode_mgr):
         self._session = session
+        self._mouse_mode_mgr = mouse_mode_mgr
         self._view = session.main_view
         self._recent_touches = []	# List of Touch instances
+        self._modifier_keys = []
         self._last_touch_locations = {}	# Map touch id -> (x,y)
         from .settings import settings
         self.trackpad_speed = settings.trackpad_sensitivity   	# Trackpad position sensitivity
@@ -34,6 +37,14 @@ class MultitouchTrackpad:
         self._touch_handler = None
         self._received_touch_event = False
 
+    @property
+    def full_width_translation_distance(self):
+        return self._full_width_translation_distance
+
+    @property
+    def full_rotation_distance(self):
+        return self._full_rotation_distance
+
     def set_graphics_window(self, graphics_window):
         graphics_window.touchEvent = self._touch_event
         self._enable_touch_events(graphics_window)
@@ -50,7 +61,7 @@ class MultitouchTrackpad:
             t.remove_handler(h)
             h = None
         self._touch_handler = h
-    
+
     def _enable_touch_events(self, graphics_window):
         from sys import platform
         if platform == 'darwin':
@@ -69,7 +80,7 @@ class MultitouchTrackpad:
         w.setAttribute(Qt.WA_AcceptTouchEvents)
         print('graphics widget touch enabled', w.testAttribute(Qt.WA_AcceptTouchEvents))
         '''
-        
+
     # Appears that Qt has disabled touch events on Mac due to unresolved scrolling lag problems.
     # Searching for qt setAcceptsTouchEvents shows they were disabled Oct 17, 2012.
     # A patch that allows an environment variable QT_MAC_ENABLE_TOUCH_EVENTS to allow touch
@@ -84,8 +95,18 @@ class MultitouchTrackpad:
 
         from PyQt5.QtCore import QEvent
         t = event.type()
+        # For some unfathomable reason the QTouchEvent.modifiers() method always
+        # returns zero (QTBUG-60389, unresolved since 2017). So we need to do a
+        # little hacky workaround
+
+        from .mousemodes import decode_modifier_bits
+        # session.ui.keyboardModifiers() does *not* work here (always returns 0)
+        mb = int(self._session.ui.queryKeyboardModifiers())
+        self._modifier_keys = decode_modifier_bits(mb)
+
+
         if t == QEvent.TouchUpdate:
-            # On Mac touch events get backlogged in queue when the events cause 
+            # On Mac touch events get backlogged in queue when the events cause
             # time consuming computatation.  It appears Qt does not collapse the events.
             # So event processing can get tens of seconds behind.  To reduce this problem
             # we only handle the most recent touch update per redraw.
@@ -100,61 +121,53 @@ class MultitouchTrackpad:
     def _collapse_touch_events(self):
         touches = self._recent_touches
         if touches:
-            self._process_touches(touches)
+            event = self._process_touches(touches)
             self._recent_touches = []
+            self._mouse_mode_mgr._dispatch_touch_event(event)
 
     def _process_touches(self, touches):
+        pinch = twist = scroll = None
+        two_swipe = None
+        three_swipe = None
+        four_swipe = None
         n = len(touches)
         speed = self.trackpad_speed
         moves = [t.move(self._last_touch_locations) for t in touches]
+        dx = sum(x for x,y in moves)/n
+        dy = sum(y for x,y in moves)/n
+
         if n == 2:
             (dx0,dy0),(dx1,dy1) = moves[0], moves[1]
             from math import sqrt, exp, atan2, pi
             l0,l1 = sqrt(dx0*dx0 + dy0*dy0),sqrt(dx1*dx1 + dy1*dy1)
             d12 = dx0*dx1+dy0*dy1
             if d12 < 0:
-                # Finger moving in opposite directions: pinch or twist
+                # Finger moving in opposite directions: pinch/twist
                 (x0,y0),(x1,y1) = [(t.x,t.y) for t in touches[:2]]
                 sx,sy = x1-x0,y1-y0
                 sn = sqrt(sx*sx + sy*sy)
                 sd0,sd1 = sx*dx0 + sy*dy0, sx*dx1 + sy*dy1
-                if abs(sd0) > 0.5*sn*l0 and abs(sd1) > 0.5*sn*l1:
-                    # Fingers move along line between them: pinch to zoom
-                    zf = 1 + speed * self._zoom_scaling * (l0+l1) / self._full_width_translation_distance
-                    if sd1 < 0:
-                        zf = 1/zf
-                    self._zoom(zf)
-                else:
-                    # Fingers move perpendicular to line between them: twist
-                    rot = atan2(-sy*dx1+sx*dy1,sn*sn) + atan2(sy*dx0-sx*dy0,sn*sn)
-                    a = -speed * self._twist_scaling * rot * 180 / pi
-                    zaxis = (0,0,1)
-                    self._rotate(zaxis, a)
-                return
-            # Fingers moving in same direction: rotation
-            dx = sum(x for x,y in moves)/n
-            dy = sum(y for x,y in moves)/n
-            from math import sqrt
-            turns = sqrt(dx*dx + dy*dy)/self._full_rotation_distance
-            angle = speed*360*turns
-            self._rotate((dy, dx, 0), angle)
+                zf = 1 + speed * self._zoom_scaling * (l0+l1) / self._full_width_translation_distance
+                if sd1 < 0:
+                    zf = 1/zf
+                pinch = zf
+                rot = atan2(-sy*dx1+sx*dy1,sn*sn) + atan2(sy*dx0-sx*dy0,sn*sn)
+                a = -speed * self._twist_scaling * rot * 180 / pi
+                twist = a
+            else:
+                two_swipe = tuple([d/self._full_width_translation_distance for d in (dx, dy)])
+                scroll = speed * dy / self._wheel_click_pixels
         elif n == 3:
-            dx = sum(x for x,y in moves)/n
-            dy = sum(y for x,y in moves)/n
-            ww = self._view.window_size[0]	# Window width in pixels
-            s = speed * ww / self._full_width_translation_distance
-            self._translate((s*dx, -s*dy, 0))
+            three_swipe = tuple([d/self._full_width_translation_distance for d in (dx, dy)])
         elif n == 4:
-            # Use scrollwheel mouse mode
-            ses = self._session
-            from .mousemodes import keyboard_modifier_names, MouseEvent
-            modifiers = keyboard_modifier_names(ses.ui.queryKeyboardModifiers())
-            scrollwheel_mode = ses.ui.mouse_modes.mode(button = 'wheel', modifiers = modifiers)
-            if scrollwheel_mode:
-                xy = (sum(t.x for t in touches)/n, sum(t.y for t in touches)/n)
-                dy = sum(y for x,y in moves)/n			# pixels
-                delta = speed * dy / self._wheel_click_pixels	# wheel clicks
-                scrollwheel_mode.wheel(MouseEvent(position = xy, wheel_value = delta, modifiers = modifiers))
+            four_swipe = tuple([d/self._full_width_translation_distance for d in (dx, dy)])
+
+        return MultitouchEvent(modifiers=self._modifier_keys,
+            wheel_value=scroll, two_finger_trans=two_swipe, two_finger_scale=pinch,
+            two_finger_twist=twist, three_finger_trans=three_swipe,
+            four_finger_trans=four_swipe)
+
+        return pinch, twist, scroll, two_swipe, three_swipe, four_swipe
 
     def _rotate(self, screen_axis, angle):
         if angle == 0:
@@ -230,3 +243,127 @@ class Touch:
         x,y = self.x, self.y
         last_touch_locations[id] = (x,y)
         return (x-lx, y-ly)
+
+touch_action_to_property = {
+    'pinch':    'two_finger_scale',
+    'twist':    'two_finger_twist',
+    'two finger swipe': 'two_finger_trans',
+    'three finger swipe':   'three_finger_trans',
+    'four finger swipe':    'four_finger_trans',
+}
+
+
+class MultitouchBinding:
+    '''
+    Associates an action on a multitouch trackpad and a set of modifier keys
+    ('alt', 'command', 'control', 'shift') with a MouseMode.
+    '''
+    valid_actions = list(touch_action_to_property.keys())
+
+    def __init__(self, action, modifiers, mode):
+        if action not in self.valid_actions:
+            from chimerax.core.errors import UserError
+            raise UserError('Unrecognised touchpad action! Must be one of: {}'.format(
+                ', '.join(valid_actions)
+            ))
+        self.action = action
+        self.modifiers = modifiers
+        self.mode = mode
+    def matches(self, action, modifiers):
+        '''
+        Does this binding match the specified action and modifiers?
+        A match requires all of the binding modifiers keys are among
+        the specified modifiers (and possibly more).
+        '''
+        return (action==self.action and
+            len([k for k in self.modifiers if not k in modifiers]) == 0
+        )
+    def exact_match(self, action, modifiers):
+        '''
+        Does this binding exactly match the specified action and modifiers?
+        An exact match requires the binding modifiers keys are exactly the
+        same set as the specified modifier keys.
+        '''
+        return action == self.action and set(modifiers) == set(self.modifiers)
+
+
+from .mousemodes import MouseEvent
+class MultitouchEvent(MouseEvent):
+    '''
+    Provides an interface to events fired by multi-touch trackpads and modifier
+    keys so that mouse modes do not directly depend on details of the window
+    toolkit or trackpad implementation.
+    '''
+    def __init__(self, modifiers = None,  wheel_value = None,
+            two_finger_trans=None, two_finger_scale=None, two_finger_twist=None,
+            three_finger_trans=None, four_finger_trans=None):
+        super().__init__(event=None, modifiers=modifiers, position=None, wheel_value=wheel_value)
+        self._two_finger_trans = two_finger_trans
+        self._two_finger_scale = two_finger_scale
+        self._two_finger_twist = two_finger_twist
+        self._three_finger_trans = three_finger_trans
+        self._four_finger_trans = four_finger_trans
+
+    @property
+    def modifiers(self):
+        return self._modifiers
+
+    # @property
+    # def event(self):
+    #     '''
+    #     The core QTouchEvent object
+    #     '''
+    #     return self._event
+
+    @property
+    def wheel_value(self):
+        '''
+        Supported API.
+        Effective mouse wheel value if two-finger vertical swipe is to be
+        interpreted as a scrolling action.
+        '''
+        return self._wheel_value
+
+    @property
+    def two_finger_trans(self):
+        '''
+        Supported API.
+        Returns a tuple (delta_x, delta_y) in screen coordinates representing
+        the movement when a two-finger swipe is interpreted as a translation
+        action.
+        '''
+        return self._two_finger_trans
+
+    @property
+    def two_finger_scale(self):
+        '''
+        Supported API
+        Returns a float representing the change in a two-finger pinching action.
+        '''
+        return self._two_finger_scale
+
+    @property
+    def two_finger_twist(self):
+        '''
+        Supported API
+        Returns the rotation in degrees defined by a two-finger twisting action.
+        '''
+        return self._two_finger_twist
+
+    @property
+    def three_finger_trans(self):
+        '''
+        Supported API
+        Returns a tuple (delta_x, delta_y) in screen coordinates representing
+        the translation in a 3-fingered swipe.
+        '''
+        return self._three_finger_trans
+
+    @property
+    def four_finger_trans(self):
+        '''
+        Supported API
+        Returns a tuple (delta_x, delta_y) in screen coordinates representing
+        the translation in a 3-fingered swipe.
+        '''
+        return self._four_finger_trans
