diff --git a/fsl/fsleyes/profiles/lightboxviewprofile.py b/fsl/fsleyes/profiles/lightboxviewprofile.py index 5d032af1fd169af189e97c32aac0695e67ebbfc4..742a241ee65d38ef966f934960c2dbf09f6eff03 100644 --- a/fsl/fsleyes/profiles/lightboxviewprofile.py +++ b/fsl/fsleyes/profiles/lightboxviewprofile.py @@ -11,6 +11,7 @@ import logging import fsl.fsleyes.profiles as profiles +import fsl.utils.async as async log = logging.getLogger(__name__) @@ -76,7 +77,12 @@ class LightBoxViewProfile(profiles.Profile): if wheel > 0: wheel = -1 elif wheel < 0: wheel = 1 - self._viewPanel.getCanvas().topRow += wheel + # See comment in OrthoViewProfile._zoomModeMouseWheel + # about timeout + def update(): + self._viewPanel.getCanvas().topRow += wheel + + async.idle(update, timeout=0.1) def _viewModeLeftMouseDrag(self, ev, canvas, mousePos, canvasPos): @@ -106,4 +112,10 @@ class LightBoxViewProfile(profiles.Profile): if wheel > 0: wheel = 50 elif wheel < 0: wheel = -50 - self._viewPanel.getSceneOptions().zoom += wheel + + # see comment in OrthoViewProfile._zoomModeMouseWheel + # about timeout + def update(): + self._viewPanel.getSceneOptions().zoom += wheel + + async.idle(update, timeout=0.1) diff --git a/fsl/fsleyes/profiles/orthoeditprofile.py b/fsl/fsleyes/profiles/orthoeditprofile.py index 62c1947e23741653e10fc9130a616b0a8ba12ad6..2b8caa54f51ce15dfb934908fcac2d17c35bd93d 100644 --- a/fsl/fsleyes/profiles/orthoeditprofile.py +++ b/fsl/fsleyes/profiles/orthoeditprofile.py @@ -17,6 +17,7 @@ import numpy as np import props import fsl.data.image as fslimage import fsl.data.strings as strings +import fsl.utils.async as async import fsl.utils.dialog as fsldlg import fsl.utils.status as status import fsl.fsleyes.actions as actions @@ -868,10 +869,17 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile): voxel = self.__getVoxelLocation(canvasPos) - if voxel is not None: + if voxel is None: + return + + # See comment in OrthoViewProfile._zoomModeMouseWheel + # about timeout + def update(): self.__drawCursorAnnotation(canvas, voxel) self.__refreshCanvases(ev, canvas) + async.idle(update, timeout=0.1) + def _deselModeLeftMouseDown(self, ev, canvas, mousePos, canvasPos): """Handles mouse down events in ``desel`` mode. @@ -1056,17 +1064,24 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile): dataRange = opts.dataMax - opts.dataMin step = 0.01 * dataRange - if wheel > 0: self.intensityThres += step - elif wheel < 0: self.intensityThres -= step + if wheel > 0: offset = step + elif wheel < 0: offset = -step else: return - if self.__selecting: - - voxel = self.__getVoxelLocation(canvasPos) - if voxel is not None: - self.__selintSelect(voxel, canvas) - self.__refreshCanvases(ev, canvas) + # See comment in OrthoViewProfile._zoomModeMouseWheel + # about timeout + def update(): + self.intensityThres += offset + if self.__selecting: + + voxel = self.__getVoxelLocation(canvasPos) + + if voxel is not None: + self.__selintSelect(voxel, canvas) + self.__refreshCanvases(ev, canvas) + + async.idle(update, timeout=0.1) def _chradModeMouseWheel(self, ev, canvas, wheel, mousePos, canvasPos): @@ -1077,14 +1092,22 @@ class OrthoEditProfile(orthoviewprofile.OrthoViewProfile): select-by-intensity is re-run at the current mouse location. """ - if wheel > 0: self.searchRadius -= 5 - elif wheel < 0: self.searchRadius += 5 + if wheel > 0: offset = -5 + elif wheel < 0: offset = 5 else: return - if self.__selecting: - - voxel = self.__getVoxelLocation(canvasPos) + # See comment in OrthoViewProfile._zoomModeMouseWheel + # about timeout + def update(): + + self.searchRadius += offset + + if self.__selecting: + + voxel = self.__getVoxelLocation(canvasPos) + + if voxel is not None: + self.__selintSelect(voxel, canvas) + self.__refreshCanvases(ev, canvas) - if voxel is not None: - self.__selintSelect(voxel, canvas) - self.__refreshCanvases(ev, canvas) + async.idle(update, timeout=0.1) diff --git a/fsl/fsleyes/profiles/orthoviewprofile.py b/fsl/fsleyes/profiles/orthoviewprofile.py index 1e42294ce64f8ffaf96f50395b7f2d8dec271d7d..d4bee5dbc8e1f5cbf8923abfca22bedf3c59d0fa 100644 --- a/fsl/fsleyes/profiles/orthoviewprofile.py +++ b/fsl/fsleyes/profiles/orthoviewprofile.py @@ -16,6 +16,7 @@ import numpy as np import fsl.fsleyes.profiles as profiles import fsl.fsleyes.actions as actions +import fsl.utils.async as async import fsl.data.image as fslimage import fsl.data.constants as constants @@ -332,7 +333,11 @@ class OrthoViewProfile(profiles.Profile): elif ch in ('+', '='): dirs[canvas.zax] = 1 elif ch in ('-', '_'): dirs[canvas.zax] = -1 - self._displayCtx.location.xyz = self.__offsetLocation(*dirs) + def update(): + self._displayCtx.location.xyz = self.__offsetLocation(*dirs) + + # See comment in _zoomModeMouseWheel about timeout + async.idle(update, timeout=0.1) ##################### @@ -357,7 +362,11 @@ class OrthoViewProfile(profiles.Profile): pos = self.__offsetLocation(*dirs) - self._displayCtx.location[canvas.zax] = pos[canvas.zax] + def update(): + self._displayCtx.location[canvas.zax] = pos[canvas.zax] + + # See comment in _zoomModeMouseWheel about timeout + async.idle(update, timeout=0.1) #################### @@ -378,7 +387,17 @@ class OrthoViewProfile(profiles.Profile): """ if wheel > 0: wheel = 50 elif wheel < 0: wheel = -50 - canvas.zoom += wheel + + # Over SSH/X11, mouse wheel events seem to get queued, + # and continue to get processed after the user has + # stopped spinning the mouse wheel, which is super + # frustrating. So we do the update asynchronously, and + # set a time out to drop the event, and prevent the + # horribleness from happening. + def update(): + canvas.zoom += wheel + + async.idle(update, timeout=0.1) def _zoomModeChar(self, ev, canvas, key): @@ -500,7 +519,11 @@ class OrthoViewProfile(profiles.Profile): elif key == wx.WXK_RIGHT: xoff = 2 else: return - canvas.panDisplayBy(xoff, yoff) + def update(): + canvas.panDisplayBy(xoff, yoff) + + # See comment in _zoomModeMouseWheel about timeout + async.idle(update, timeout=0.1) ############# diff --git a/fsl/utils/async.py b/fsl/utils/async.py index 73330044b4653df87e8142df5a3178b8a367ae05..801d0debbc0516623ca095390c0ea5431182b7a3 100644 --- a/fsl/utils/async.py +++ b/fsl/utils/async.py @@ -31,6 +31,8 @@ task to run. It waits until all the threads have finished, and then runs the task (via :func:`idle`). """ + +import time import Queue import logging import threading @@ -125,12 +127,18 @@ def _wxIdleLoop(ev): ev.Skip() - try: task, args, kwargs = _idleQueue.get_nowait() - except Queue.Empty: return + try: + task, schedtime, timeout, args, kwargs = _idleQueue.get_nowait() + except Queue.Empty: + return + + name = getattr(task, '__name__', '<unknown>') + now = time.time() + elapsed = now - schedtime - name = getattr(task, '__name__', '<unknown>') - log.debug('Running function ({}) on wx idle loop'.format(name)) - task(*args, **kwargs) + if timeout == 0 or (elapsed < timeout): + log.debug('Running function ({}) on wx idle loop'.format(name)) + task(*args, **kwargs) if _idleQueue.qsize() > 0: ev.RequestMore() @@ -141,6 +149,12 @@ def idle(task, *args, **kwargs): :arg task: The task to run. + :arg timeout: Optional. If provided, must be provided as a keyword + argument. Specifies a time out, in seconds. If this + amount of time passes before the function gets + scheduled to be called on the idle loop, the function + is not called, and is dropped from the queue. + All other arguments are passed through to the task function. If a ``wx.App`` is not running, the task is called directly. @@ -149,6 +163,9 @@ def idle(task, *args, **kwargs): global _idleRegistered global _idleTasks + schedtime = time.time() + timeout = kwargs.pop('timeout', 0) + if _haveWX(): import wx @@ -159,7 +176,7 @@ def idle(task, *args, **kwargs): name = getattr(task, '__name__', '<unknown>') log.debug('Scheduling idle task ({}) on wx idle loop'.format(name)) - _idleQueue.put_nowait((task, args, kwargs)) + _idleQueue.put_nowait((task, schedtime, timeout, args, kwargs)) else: log.debug('Running idle task directly')