#
# canvaspanel.py - Base class for all panels that display overlay data.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`CanvasPanel` class, which is the base
class for all panels which display overlays using ``OpenGL``.
"""
import logging
import wx
import fsl.utils.idle as idle
import fsleyes_props as props
import fsleyes.actions as actions
import fsleyes.displaycontext as displayctx
from . import colourbarpanel
from . import viewpanel
log = logging.getLogger(__name__)
[docs]class CanvasPanel(viewpanel.ViewPanel):
"""The ``CanvasPanel`` class is a :class:`.ViewPanel` which is the base
class for all panels which display overlays using ``OpenGL``
(e.g. the :class:`.OrthoPanel` and the :class:`.LightBoxPanel`). A
``CanvasPanel`` instance uses a :class:`.SceneOpts` instance to control
much of its functionality. The ``SceneOpts`` instance used by a
``CanvasPanel`` can be accessed via the :meth:`sceneOpts` property.
The ``CanvasPanel`` class contains settings and functionality common to
all sub-classes, including *movie mode* (see :attr:`movieMode`), the
ability to show a colour bar (a :class:`.ColourBarPanel`; see
:attr:`.SceneOpts.showColourBar`), and a number of actions.
**Sub-class implementations**
Sub-classes of the ``CanvasPanel`` must do the following:
1. Add their content to the panel that is accessible via the
:meth:`contentPanel` property (see the note on
:ref:`adding content <canvaspanel-adding-content>`).
2. Override the :meth:`getGLCanvases` method.
3. Call the :meth:`centrePanelLayout` method in their ``__init__``
method.
4. Override the :meth:`centrePanelLayout` method if any custom layout is
necessary.
5. Call :meth:`.ViewPanel.initProfile` with the initial interaction
profile
**Actions**
The following actions are available through a ``CanvasPanel`` (see
the :mod:`.actions` module):
.. autosummary::
:nosignatures:
screenshot
movieGif
showCommandLineArgs
toggleMovieMode
toggleDisplaySync
toggleVolumeSync
.. _canvaspanel-adding-content:
**Adding content**
To support colour bar and screenshot functionality, the ``CanvasPanel``
uses a hierarchy of ``wx.Panel`` instances, depicted in the following
containment hierarchy diagram:
.. graphviz::
digraph canvasPanel {
graph [size=""];
node [style="filled",
shape="box",
fillcolor="#ddffdd",
fontname="sans"];
rankdir="BT";
1 [label="CanvasPanel"];
2 [label="Centre panel"];
3 [label="Custom content (for complex layouts)"];
4 [label="Container panel"];
5 [label="ColourBarPanel"];
6 [label="Content panel"];
7 [label="Content added by sub-classes"];
2 -> 1;
3 -> 2;
4 -> 2;
5 -> 4;
6 -> 4;
7 -> 6;
}
As depicted in the diagram, sub-classes need to add their content to the
*content panel*. This panel is accessible via the :meth:`contentPanel`
property.
The *centre panel* is the :meth:`.ViewPanel.centrePanel`. The *container
panel* is also available, via :meth:`containerPanel`. Everything in
the container panel will appear in screenshots (see the :meth:`screenshot`
method).
The :meth:`centrePanelLayout` method lays out the centre panel, using the
:meth:`layoutContainerPanel` method to lay out the colour bar and the
content panel. The ``centrePanelLayout`` method simply adds the canvas
container directly to the centre panel. Sub-classes which have more
advanced layout requirements (e.g. the :class:`.LightBoxPanel` needs a
scrollbar) may override the :meth:`centrePanelLayout` method to implement
their own layout. These sub-class implementations must:
1. Call the :meth:`layoutContainerPanel` method.
2. Add the container panel (accessed via :meth:`containerPanel`)
to the centre panel (accessed via :meth:`centrePanel`).
3. Add any other custom content to the centre panel.
"""
syncLocation = props.Boolean(default=True)
"""If ``True`` (the default), the :attr:`.DisplayContext.location` for
this ``CanvasPanel`` is linked to the master ``DisplayContext`` location.
"""
syncOverlayOrder = props.Boolean(default=True)
"""If ``True`` (the default), the :attr:`.DisplayContext.overlayOrder`
for this ``CanvasPanel`` is linked to the master ``DisplayContext``
overlay order.
"""
syncOverlayDisplay = props.Boolean(default=True)
"""If ``True`` (the default), the properties of the :class:`.Display`
and :class:`.DisplayOpts` instances for every overlay, as managed
by the :attr:`.DisplayContext` for this ``CanvasPanel``, are linked to
the properties of all ``Display`` and ``DisplayOpts`` instances managed
by the master ``DisplayContext`` instance.
"""
syncOverlayVolume = props.Boolean(default=True)
"""If ``True`` (the default), the volume/timepoint properties of the
:class:`.DisplayOpts` instances for every overlay, as managed by the
:attr:`.DisplayContext` for this ``CanvasPanel``, are linked to the
properties of all ``DisplayOpts`` instances managed by the master
``DisplayContext`` instance.
"""
movieMode = props.Boolean(default=False)
"""If ``True``, and the currently selected overlay (see
:attr:`.DisplayContext.selectedOverlay`) is a :class:`.Image` instance
with its display managed by a :class:`.VolumeOpts` instance, the displayed
volume is changed periodically, according to the :attr:`movieRate`
property.
The update is performed on the main application thread via
``wx.CallLater``.
"""
movieRate = props.Int(minval=10, maxval=500, default=400, clamped=True)
"""The movie update rate in milliseconds. The value of this property is
inverted so that a high value corresponds to a fast rate, which makes
more sense when displayed as an option to the user.
"""
movieAxis = props.Choice((0, 1, 2, 3), default=3)
"""Axis along which the movie should be played, relative to the
currently selected :class:`.Image`.
"""
movieSyncRefresh = props.Boolean(default=True)
"""Whether, when in movie mode, to synchronise the refresh for GL
canvases. This is not possible in some platforms/environments.
"""
[docs] def __init__(self, parent, overlayList, displayCtx, frame, sceneOpts):
"""Create a ``CanvasPanel``.
:arg parent: The :mod:`wx` parent object.
:arg overlayList: The :class:`.OverlayList` instance.
:arg displayCtx: The :class:`.DisplayContext` instance.
:arb frame: The :class:`.FSLeyesFrame` instance.
:arg sceneOpts: A :class:`.SceneOpts` instance for this
``CanvasPanel`` - must be created by
sub-classes.
"""
viewpanel.ViewPanel.__init__(
self, parent, overlayList, displayCtx, frame)
self.__opts = sceneOpts
# Use this name for listener registration,
# in case subclasses use the FSLeyesPanel.name
self.__name = 'CanvasPanel_{}'.format(self.name)
# Bind the sync* properties of this
# CanvasPanel to the corresponding
# properties on the DisplayContext
# instance.
if displayCtx.getParent() is not None:
self.bindProps('syncLocation',
displayCtx,
displayCtx.getSyncPropertyName('worldLocation'))
self.bindProps('syncOverlayOrder',
displayCtx,
displayCtx.getSyncPropertyName('overlayOrder'))
self.bindProps('syncOverlayDisplay', displayCtx)
self.bindProps('syncOverlayVolume', displayCtx)
# If the displayCtx instance does not
# have a parent, this means that it is
# a top level instance
else:
self.disableProperty('syncLocation')
self.disableProperty('syncOverlayOrder')
self.disableProperty('syncOverlayDisplay')
self.disableProperty('syncOverlayVolume')
import fsleyes.actions.moviegif as moviegif
self.centrePanel = wx.Panel(self)
self.__containerPanel = wx.Panel(self.centrePanel)
self.__contentPanel = wx.Panel(self.__containerPanel)
self.__movieGifAction = moviegif.MovieGifAction(
overlayList, displayCtx, self)
self.bindProps('movieSyncRefresh', sceneOpts)
self.toggleMovieMode .bindProps('toggled', self, 'movieMode')
self.toggleDisplaySync.bindProps('toggled', self, 'syncOverlayDisplay')
self.toggleVolumeSync .bindProps('toggled', self, 'syncOverlayVolume')
self.movieGif .bindProps('enabled', self.__movieGifAction)
# the __movieModeChanged method is called
# when movieMode changes, but also when
# the movie axis, overlay list, or selected
# overlay changes. This is because, if movie
# mode is on, but no overlay, or an
# incompatible overlay, is selected, the
# movie loop stops. So it needs to be
# re-started if/when a compatible overlay is
# selected.
self.__movieRunning = False
self .addListener('movieMode',
self.__name,
self.__movieModeChanged)
self .addListener('movieAxis',
self.__name,
self.__movieModeChanged)
self.overlayList.addListener('overlays',
self.__name,
self.__movieModeChanged)
self.displayCtx .addListener('selectedOverlay',
self.__name,
self.__movieModeChanged)
# Canvas/colour bar layout is managed
# in the layoutContainerPanel method
self.__colourBar = None
self.__opts.addListener('colourBarLocation',
self.__name,
self.__colourBarPropsChanged)
self.__opts.addListener('showColourBar',
self.__name,
self.__colourBarPropsChanged)
self.__opts.addListener('bgColour',
self.__name,
self.__bgfgColourChanged)
self.__opts.addListener('fgColour',
self.__name,
self.__bgfgColourChanged)
self.__opts.addListener('labelSize',
self.__name,
self.__labelSizeChanged)
idle.idle(self.__bgfgColourChanged)
[docs] def destroy(self):
"""Makes sure that any remaining control panels are destroyed
cleanly, and calls :meth:`.ViewPanel.destroy`.
"""
if self.__colourBar is not None:
self.__colourBar.destroy()
self .removeListener('movieMode', self.__name)
self .removeListener('movieAxis', self.__name)
self.overlayList.removeListener('overlays', self.__name)
self.displayCtx .removeListener('selectedOverlay', self.__name)
self.sceneOpts .removeListener('colourBarLocation', self.__name)
self.sceneOpts .removeListener('showColourBar', self.__name)
self.sceneOpts .removeListener('bgColour', self.__name)
self.sceneOpts .removeListener('fgColour', self.__name)
self.sceneOpts .removeListener('labelSize', self.__name)
self.__movieGifAction.destroy()
self.__opts = None
self.__movieGifAction = None
viewpanel.ViewPanel.destroy(self)
[docs] @actions.action
def screenshot(self):
"""Takes a screenshot of the currently displayed scene on this
``CanvasPanel``.
See the :class:`.ScreenshotAction`.
"""
from fsleyes.actions.screenshot import ScreenshotAction
ScreenshotAction(self.overlayList, self.displayCtx, self)()
[docs] @actions.action
def movieGif(self):
"""Generates an animated GIF of the currently displayed scene and
movie mode settings on this ``CanvasPanel``.
See the :class:`.MovieGifAction`.
"""
self.__movieGifAction()
[docs] @actions.action
def showCommandLineArgs(self):
"""Shows the command line arguments which can be used to re-create
the currently displayed scene. See the :class:`.ShowCommandLineAction`
class.
"""
from fsleyes.actions.showcommandline import ShowCommandLineAction
ShowCommandLineAction(self.overlayList, self.displayCtx, self)()
[docs] @actions.action
def applyCommandLineArgs(self):
"""Shows the command line arguments which can be used to re-create
the currently displayed scene. See the :class:`.ApplyCommandLineAction`
class.
"""
from fsleyes.actions.applycommandline import ApplyCommandLineAction
ApplyCommandLineAction(self.overlayList, self.displayCtx, self)()
[docs] @actions.toggleAction
def toggleMovieMode(self):
"""Toggles the value of :attr:`movieMode`. """
# The state of this action gets bound to
# the movieMode attribute in __init__
pass
[docs] @actions.toggleAction
def toggleDisplaySync(self):
"""Toggles the value of :attr:`syncOverlayDisplay`. """
# The state of this action gets bound to
# the syncOverlayDisplay attribute in __init__
pass
[docs] @actions.toggleAction
def toggleVolumeSync(self):
"""Toggles the value of :attr:`syncOverlayVolume`. """
# The state of this action gets bound to
# the syncOverlayVolume attribute in __init__
pass
@property
def sceneOpts(self):
"""Returns the :class:`.SceneOpts` instance used by this
``CanvasPanel``.
"""
return self.__opts
@property
def contentPanel(self):
"""Returns the ``wx.Panel`` to which sub-classes must add their content.
See the note on :ref:`adding content <canvaspanel-adding-content>`.
"""
return self.__contentPanel
@property
def containerPanel(self):
"""Returns the ``wx.Panel`` which contains the
:class:`.ColourBarPanel` if it is being displayed, and the content
panel. See the note on
:ref:`adding content <canvaspanel-adding-content>`.
"""
return self.__containerPanel
@property
def colourBarCanvas(self):
"""If a colour bar is being displayed, this method returns
the :class:`.ColourBarCanvas` instance which is used by the
:class:`.ColourBarPanel` to render the colour bar.
Otherwise, ``None`` is returned.
"""
if self.__colourBar is not None:
return self.__colourBar.getCanvas()
return None
[docs] def getGLCanvases(self):
"""This method must be overridden by subclasses, and must return a
list containing all :class:`.SliceCanvas` instances which are being
displayed.
"""
raise NotImplementedError(
'getGLCanvases has not been implemented '
'by {}'.format(type(self).__name__))
[docs] def centrePanelLayout(self):
"""Lays out the centre panel. This method may be overridden by
sub-classes which need more advanced layout logic. See the note on
:ref:`adding content <canvaspanel-adding-content>`
"""
self.layoutContainerPanel()
sizer = self.centrePanel.GetSizer()
if sizer is not None:
sizer.Clear()
sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(self.__containerPanel, flag=wx.EXPAND, proportion=1)
self.centrePanel.SetSizer(sizer)
self.PostSizeEvent()
[docs] def layoutContainerPanel(self):
"""Creates a ``wx.Sizer``, and uses it to lay out the colour bar panel
and canvas panel. The sizer object is returned.
This method is used by the default :meth:`centrePanelLayout` method,
and is available for custom sub-class implementations to use.
"""
sizer = self.__containerPanel.GetSizer()
if sizer is not None:
sizer.Clear()
sizer = None
sopts = self.sceneOpts
if not sopts.showColourBar:
if self.__colourBar is not None:
sopts.unbindProps('colourBarLabelSide',
self.__colourBar.colourBar,
'labelSide')
sopts.unbindProps('colourBarSize',
self.__colourBar.canvas,
'barSize')
sopts.unbindProps('highDpi', self.__colourBar.canvas)
self.__colourBar.destroy()
self.__colourBar.Destroy()
self.__colourBar = None
sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(self.__contentPanel, flag=wx.EXPAND, proportion=1)
self.__containerPanel.SetSizer(sizer)
return
if self.__colourBar is None:
self.__colourBar = colourbarpanel.ColourBarPanel(
self.__containerPanel,
self.overlayList,
self.displayCtx,
self.frame)
bg = sopts.bgColour
fg = sopts.fgColour
fs = sopts.labelSize
self.__colourBar.colourBar.textColour = fg
self.__colourBar.colourBar.bgColour = bg
self.__colourBar.colourBar.fontSize = fs
sopts.bindProps('colourBarLabelSide',
self.__colourBar.colourBar,
'labelSide')
sopts.bindProps('colourBarSize',
self.__colourBar.canvas,
'barSize')
sopts.bindProps('highDpi', self.__colourBar.canvas)
if sopts.colourBarLocation in ('top', 'bottom'):
self.__colourBar.colourBar.orientation = 'horizontal'
elif sopts.colourBarLocation in ('left', 'right'):
self.__colourBar.colourBar.orientation = 'vertical'
if sopts.colourBarLocation in ('top', 'bottom'):
sizer = wx.BoxSizer(wx.VERTICAL)
else:
sizer = wx.BoxSizer(wx.HORIZONTAL)
if sopts.colourBarLocation in ('top', 'left'):
sizer.Add(self.__colourBar, flag=wx.EXPAND)
sizer.Add(self.__contentPanel, flag=wx.EXPAND, proportion=1)
else:
sizer.Add(self.__contentPanel, flag=wx.EXPAND, proportion=1)
sizer.Add(self.__colourBar, flag=wx.EXPAND)
self.__containerPanel.SetSizer(sizer)
def __colourBarPropsChanged(self, *a):
"""Called when any colour bar display properties are changed (see
:class:`.SceneOpts`). Calls :meth:`canvasPanelLayout`.
"""
self.centrePanelLayout()
def __labelSizeChanged(self, *a, **kwa):
"""Called when the :class:`.SceneOpts.lablSize` changes. If a colour
bar is being displayed, it is updated, and the panel layout is
refreshed.
"""
sopts = self.sceneOpts
if self.__colourBar is not None:
self.__colourBar.canvas.colourBar.fontSize = sopts.labelSize
wx.CallAfter(self.Layout)
def __bgfgColourChanged(self, *a, **kwa):
"""Called when the :class:`.SceneOpts.bgColour` or
:class:`.SceneOpts.fgColour` properties change. Updates
background/foreground colours.
The :attr:`.SliceCanvasOpts.bgColour` properties are bound to
``SceneOpts.bgColour``,(see :meth:`.HasProperties.bindProps`), so we
don't need to manually update them.
:arg refresh: Must be passed as a keyword argument. If ``True`` (the
default), this ``OrthoPanel`` is refreshed.
"""
if self.destroyed:
return
refresh = kwa.pop('refresh', True)
sceneOpts = self.sceneOpts
cpanel = self.contentPanel
canvases = self.getGLCanvases()
bg = sceneOpts.bgColour
fg = sceneOpts.fgColour
cpanel.SetBackgroundColour([round(c * 255) for c in bg])
cpanel.SetForegroundColour([round(c * 255) for c in fg])
if self.__colourBar is not None:
canvas = self.__colourBar.canvas
cbar = self.__colourBar.colourBar
cbar.textColour = fg
cbar.bgColour = bg
canvases.append(canvas)
if refresh:
self.Refresh()
self.Update()
def __movieModeChanged(self, *a):
"""Called when the :attr:`movieMode` property changes. If it has been
enabled, calls :meth:`__movieUpdate`, to start the movie loop.
"""
# The fsl.utils.idle idle loop timeout
# defaults to 200 milliseconds, which can
# cause delays in frame updates. So when
# movie mode is on, we bump up the rate.
def startMovie():
idle.idleLoop.callRate = 10
if not self.__movieLoop(startLoop=True):
idle.idleLoop.callRate = None
# The __movieModeChanged method is called
# on the props event queue. Here we make
# sure that __movieLoop() is called *off*
# the props event queue, by calling it from
# the idle loop.
if self.movieMode: idle.idle(startMovie)
else: idle.idleLoop.callRate = None
def __movieLoop(self, startLoop=False):
"""Manages the triggering of the next movie frame. This method is
called by :meth:`__movieModeChanged` when :attr:`movieMode` changes
and when the selected overlay changes, and also by
:meth:`__syncMovieRefresh` and :meth:`__unsyncMovieRefresh` while
the movie loop is running, to trigger the next frame.
:arg startLoop: This is set to ``True`` when called from
:meth:`__movieModeChanged`. If ``True``, and the movie
loop is already running, this method does nothing.
"""
# Movie loop is already running, nothing to do.
if startLoop and self.__movieRunning:
return True
# Attempt to show the next frame -
# __movieFrame returns True if the
# movie is continuing, False if it
# has ended.
self.__movieRunning = self.__movieFrame()
return self.__movieRunning
[docs] def canRunMovie(self, overlay, opts):
"""Returns ``True`` or ``False``, depending on whether movie mode
is possible with the given z`overlay`` and ``opts``.
"""
import fsl.data.image as fslimage
import fsl.data.mesh as fslmesh
axis = self.movieAxis
# 3D movies are good for all overlays
if axis < 3:
return True
# 4D Nifti images are all good
if isinstance(overlay, fslimage.Nifti) and \
len(overlay.shape) > 3 and \
overlay.shape[3] > 1 and \
isinstance(opts, displayctx.VolumeOpts):
return True
# Mesh surfaces with N-D
# vertex data are all good
if isinstance(overlay, fslmesh.Mesh) and \
opts.vertexDataLen() > 1:
return True
return False
[docs] def getMovieFrame(self, overlay, opts):
"""Returns the current movie frame for the given overlay.
A movie frame is typically a sequentially increasing number in
some minimum/maximum range, e.g. a voxel or volume index.
This method may be overridden by sub-classes for custom behaviour
(e.g. the :class:`.Scene3DPanel`).
"""
axis = self.movieAxis
def nifti():
if axis < 3: return opts.getVoxel(vround=False)[axis]
else: return opts.volume
def mesh():
if axis < 3: return other()
else: return opts.vertexDataIndex
def other():
return self.displayCtx.location.getPos(axis)
import fsl.data.image as fslimage
import fsl.data.mesh as fslmesh
if isinstance(overlay, fslimage.Nifti): return nifti()
elif isinstance(overlay, fslmesh.Mesh): return mesh()
else: return other()
[docs] def doMovieUpdate(self, overlay, opts):
"""Called by :meth:`__movieFrame`. Updates the properties on the
given ``opts`` instance to move forward one frame in the movie.
This method may be overridden by sub-classes for custom behaviour
(e.g. the :class:`.Scene3DPanel`).
:returns: A value which identifies the current movie frame. This may
be a volume or voxel index, or a world coordinate location
on one axis.
"""
axis = self.movieAxis
def nifti():
limit = overlay.shape[axis]
# This method has been called off the props
# event queue (see __movieModeChanged).
# Therefore, all listeners on the opts.volume
# or DisplayContext.location properties
# should be called immediately, in these
# assignments.
#
# When the movie axis == 3 (time), this means
# that image texture refreshes should be
# triggered and, after the opts.volume
# assignment, all affected GLObjects should
# return ready() == False.
if axis == 3:
if opts.volume >= limit - 1: opts.volume = 0
else: opts.volume += 1
frame = opts.volume
else:
voxel = opts.getVoxel()
if voxel[axis] >= limit - 1: voxel[axis] = 0
else: voxel[axis] += 1
self.displayCtx.location = opts.transformCoords(
voxel, 'voxel', 'display')
frame = voxel[axis]
return frame
def mesh():
if axis == 3:
limit = opts.vertexDataLen()
val = opts.vertexDataIndex
if val >= limit - 1: val = 0
else: val += 1
opts.vertexDataIndex = val
return val
else:
return other()
def other():
bmin, bmax = opts.bounds.getRange(axis)
delta = (bmax - bmin) / 75.0
pos = self.displayCtx.location.getPos(axis)
if pos >= bmax: pos = bmin
else: pos = pos + delta
self.displayCtx.location.setPos(axis, pos)
return pos
import fsl.data.image as fslimage
import fsl.data.mesh as fslmesh
if isinstance(overlay, fslimage.Nifti): frame = nifti()
elif isinstance(overlay, fslmesh.Mesh): frame = mesh()
else: frame = other()
return frame
def __movieFrame(self):
"""Called by :meth:`__movieLoop`.
If the currently selected overlay (see
:attr:`.DisplayContext.selectedOverlay`) is a 4D :class:`.Image` being
displayed as a ``volume`` (see the :class:`.VolumeOpts` class), the
:attr:`.NiftiOpts.volume` property is incremented and all
GL canvases in this ``CanvasPanel`` are refreshed.
:returns: ``True`` if the movie loop was started, ``False`` otherwise.
"""
from . import scene3dpanel
if self.destroyed: return False
if not self.movieMode: return False
overlay = self.displayCtx.getSelectedOverlay()
canvases = self.getGLCanvases()
if overlay is None:
return False
opts = self.displayCtx.getOpts(overlay)
if not self.canRunMovie(overlay, opts):
return False
# We want the canvas refreshes to be
# synchronised. So we 'freeze' them
# while changing the image volume, and
# then refresh them all afterwards.
for c in canvases:
c.FreezeDraw()
c.FreezeSwapBuffers()
self.doMovieUpdate(overlay, opts)
# Now we get refs to *all* GLObjects managed
# by every canvas - we have to wait until
# they are all ready to be drawn before we
# can refresh the canvases. Note that this
# is only necessary when the movie axis == 3
globjs = [c.getGLObject(o)
for c in canvases
for o in self.overlayList]
globjs = [g for g in globjs if g is not None]
def allReady():
return all([g.ready() for g in globjs])
# Figure out the movie rate - the
# number of seconds to wait until
# triggering the next frame.
rate = self.movieRate
rateMin = self.getAttribute('movieRate', 'minval')
rateMax = self.getAttribute('movieRate', 'maxval')
# Special case/hack - if this is a Scene3DPanel,
# and the movie axis is X/Y/Z, we always
# use a fast rate. Instead, the Scene3dPanel
# will increase/decrease the rotation angle
# to speed up/slow down the movie instead.
if isinstance(self, scene3dpanel.Scene3DPanel) and self.movieAxis < 3:
rate = rateMax
rate = (rateMin + (rateMax - rate)) / 1000.0
def update():
self.movieSync()
idle.idle(self.__movieLoop, after=rate)
# Refresh the canvases when all
# GLObjects are ready to be drawn.
idle.idleWhen(update, allReady, pollTime=rate / 10)
return True
[docs] def movieSync(self):
"""Called by :meth:`__movieUpdate`. Updates all GL canvases, attempting
to refresh them in a synchronised manner.
Ideally all canvases should be drawn off-screen (i.e. rendered to the
back buffer), and then all refreshed together (back and front buffers
swapped). Unfortunately some OpenGL drivers seem to have trouble with
this approach, and require drawing and front/back buffer swaps to be
done at the same time.
This method will refresh the GL canvases in either a synchronised or
unsynchronised manner, depending upon the value of the
:attr:`movieSyncRefresh` property.
"""
canvases = self.getGLCanvases()
if self.movieSyncRefresh:
for c in canvases:
c.ThawDraw()
c.Refresh()
for c in canvases:
c.ThawSwapBuffers()
c.SwapBuffers()
else:
for c in canvases:
c.ThawDraw()
c.ThawSwapBuffers()
c.Refresh()