Skip to content
Snippets Groups Projects
Commit 2eeb30ad authored by Paul McCarthy's avatar Paul McCarthy
Browse files

Removed fsl.utils modules that have been migrated to the new fsleyes-widgets project

parent 31f4a4ed
No related branches found
No related tags found
No related merge requests found
Showing
with 0 additions and 2570 deletions
:orphan:
fsl.utils.colourbarbitmap module
================================
.. automodule:: fsl.utils.colourbarbitmap
:members:
:undoc-members:
:show-inheritance:
:orphan:
fsl.utils.dialog module
=======================
.. automodule:: fsl.utils.dialog
:members:
:undoc-members:
:show-inheritance:
:orphan:
fsl.utils.imagepanel module
===========================
.. automodule:: fsl.utils.imagepanel
:members:
:undoc-members:
:show-inheritance:
:orphan:
fsl.utils.layout module
=======================
.. automodule:: fsl.utils.layout
:members:
:undoc-members:
:show-inheritance:
:orphan:
fsl.utils.runwindow module
==========================
.. automodule:: fsl.utils.runwindow
:members:
:undoc-members:
:show-inheritance:
:orphan:
fsl.utils.status module
=======================
.. automodule:: fsl.utils.status
:members:
:undoc-members:
:show-inheritance:
:orphan:
fsl.utils.textbitmap module
===========================
.. automodule:: fsl.utils.textbitmap
:members:
:undoc-members:
:show-inheritance:
:orphan:
fsl.utils.typedict module
=========================
.. automodule:: fsl.utils.typedict
:members:
:undoc-members:
:show-inheritance:
:orphan:
fsl.utils.webpage module
========================
.. automodule:: fsl.utils.webpage
:members:
:undoc-members:
:show-inheritance:
doc/images/colourbarbitmap.png

4.05 KiB

doc/images/fsldirdialog.png

47.1 KiB

doc/images/simplemessagedialog.png

10.4 KiB

doc/images/texteditdialog.png

216 KiB

#!/usr/bin/env python
#
# colourbarbitmap.py - A function which renders a colour bar using
# matplotlib as an RGBA bitmap.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides a single function, :func:`colourBarBitmap`, which uses
:mod:`matplotlib` to plot a colour bar. The colour bar is rendered off-screen
and returned as an RGBA bitmap.
"""
def colourBarBitmap(cmap,
width,
height,
cmapResolution=256,
negCmap=None,
invert=False,
ticks=None,
ticklabels=None,
tickalign=None,
label=None,
orientation='vertical',
labelside='top',
alpha=1.0,
fontsize=10,
bgColour=None,
textColour='#ffffff'):
"""Plots a colour bar using :mod:`matplotlib`.
The rendered colour bar is returned as a RGBA bitmap within a
``numpy.uint8`` array of size :math:`w \\times h \\times 4`, with the
top-left pixel located at index ``[0, 0, :]``.
A rendered colour bar will look something like this:
.. image:: images/colourbarbitmap.png
:scale: 50%
:align: center
:arg cmap: Name of a registered :mod:`matplotlib` colour map.
:arg width: Colour bar width in pixels.
:arg height: Colour bar height in pixels.
:arg negCmap: If provided, two colour maps are drawn, centered at 0.
:arg invert: If ``True``, the colour map is inverted.
:arg ticks: Locations of tick labels.
:arg ticklabels: Tick labels.
:arg tickalign: Tick alignment (one for each tick, either ``'left'`` or
``'right'``).
:arg label: Text label to show next to the colour bar.
:arg orientation: Either ``vertical`` or ``horizontal``.
:arg labelside: If ``orientation`` is ``vertical`` ``labelSide`` may
be either ``left`` or ``right``. Otherwise, if
``orientation`` is ``horizontal``, ``labelSide`` may
be ``top`` or ``bottom``.
:arg alpha: Colour bar transparency, in the range ``[0.0 - 1.0]``.
:arg fontsize: Label font size in points.
:arg bgColour: Background colour - can be any colour specification
that is accepted by :mod:`matplotlib`.
:arg textColour: Label colour - can be any colour specification that
is accepted by :mod:`matplotlib`.
"""
# These imports are expensive, so we're
# importing at the function level.
import numpy as np
import matplotlib.backends.backend_agg as mplagg
import matplotlib.figure as mplfig
if orientation not in ['vertical', 'horizontal']:
raise ValueError('orientation must be vertical or horizontal')
if orientation == 'horizontal':
if labelside not in ['top', 'bottom']:
raise ValueError('labelside must be top or bottom')
else:
if labelside not in ['left', 'right']:
raise ValueError('labelside must be left or right')
# vertical plots are rendered horizontally,
# and then simply rotated at the end
if orientation == 'vertical':
width, height = height, width
if labelside == 'left': labelside = 'top'
else: labelside = 'bottom'
dpi = 96.0
ncols = cmapResolution
data = genColours(cmap, ncols, invert, alpha)
if negCmap is not None:
ndata = genColours(negCmap, ncols, not invert, alpha)
data = np.concatenate((ndata, data), axis=1)
ncols *= 2
fig = mplfig.Figure(figsize=(width / dpi, height / dpi), dpi=dpi)
canvas = mplagg.FigureCanvasAgg(fig)
ax = fig.add_subplot(111)
if bgColour is not None:
fig.patch.set_facecolor(bgColour)
else:
fig.patch.set_alpha(0)
# draw the colour bar
ax.imshow(data,
aspect='auto',
origin='lower',
interpolation='nearest')
ax.set_xlim((0, ncols - 1))
ax.set_yticks([])
ax.tick_params(colors=textColour, labelsize=fontsize, length=0)
if labelside == 'top':
ax.xaxis.tick_top()
ax.xaxis.set_label_position('top')
va = 'top'
else:
ax.xaxis.tick_bottom()
ax.xaxis.set_label_position('bottom')
va = 'bottom'
if label is not None:
ax.set_xlabel(label,
fontsize=fontsize,
color=textColour,
va=va)
label = ax.xaxis.get_label()
if ticks is None or ticklabels is None:
ax.set_xticks([])
else:
ax.set_xticks(np.array(ticks) * ncols)
ax.set_xticklabels(ticklabels)
ticklabels = ax.xaxis.get_ticklabels()
try:
fig.tight_layout()
except:
pass
# Adjust the x label after tight_layout,
# otherwise it will overlap with the tick
# labels. I don't understand why, but I
# have to set va to the opposite of what
# I would have thought.
if label is not None and ticklabels is not None:
if labelside == 'top':
label.set_va('bottom')
label.set_position((0.5, 0.97))
else:
label.set_va('top')
label.set_position((0.5, 0.03))
# This must be done *after* calling
# tick_top/tick_bottom, as I think
# the label bjects get recreated.
if ticklabels is not None and tickalign is not None:
for l, a in zip(ticklabels, tickalign):
l.set_horizontalalignment(a)
canvas.draw()
buf = canvas.tostring_argb()
ncols, nrows = canvas.get_width_height()
bitmap = np.fromstring(buf, dtype=np.uint8)
bitmap = bitmap.reshape(nrows, ncols, 4).transpose([1, 0, 2])
# the bitmap is in argb order,
# but we want it in rgba
rgb = bitmap[:, :, 1:]
a = bitmap[:, :, 0]
bitmap = np.dstack((rgb, a))
if orientation == 'vertical':
bitmap = np.flipud(bitmap.transpose([1, 0, 2]))
bitmap = np.rot90(bitmap, 2)
return bitmap
def genColours(cmap, cmapResolution, invert, alpha):
"""Generate an array containing ``cmapResolution`` colours from the given
colour map object/function.
"""
import numpy as np
import matplotlib.cm as cm
ncols = cmapResolution
cmap = cm.get_cmap(cmap)
data = np.linspace(0.0, 1.0, ncols)
if invert:
data = data[::-1]
data = np.repeat(data.reshape(ncols, 1), 2, axis=1)
data = data.transpose()
data = cmap(data)
data[:, :, 3] = alpha
return data
This diff is collapsed.
#!/usr/bin/env python
#
# imagepanel.py - A panel for displaying a wx.Image.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`ImagePanel` class, for displaying a
:class:`wx.Image`.
"""
import logging
import wx
from fsl.utils.platform import platform as fslplatform
log = logging.getLogger(__name__)
if fslplatform.wxFlavour == fslplatform.WX_PHOENIX: ImagePanelBase = wx.Panel
else: ImagePanelBase = wx.PyPanel
class ImagePanel(ImagePanelBase):
"""A :class:`wx.Panel` which may be used to display a resizeable
:class:`wx.Image`. The image is scaled to the size of the panel.
"""
def __init__(self, parent, image=None):
"""Create an ``ImagePanel``.
If the ``image`` is not passed in here, it can be set later with the
:meth:`SetImage` method.
:arg parent: The :mod:`wx` parent object.
:arg image: The :class:`wx.Image` object to display.
"""
ImagePanelBase.__init__(self, parent)
self.Bind(wx.EVT_PAINT, self.Draw)
self.Bind(wx.EVT_SIZE, self.__onSize)
self.SetImage(image)
def SetImage(self, image):
"""Set the image that is displayed on this ``ImagePanel``.
:arg image: The :class:`wx.Image` object to display.
"""
self.__image = image
if image is not None: self.SetMinSize(image.GetSize())
else: self.SetMinSize((0, 0))
self.Refresh()
def __onSize(self, ev):
"""Redraw this panel when it is sized, so the image is scaled
appropriately - see the :meth:`Draw` method.
"""
self.Refresh()
ev.Skip()
def DoGetBestSize(self):
"""Returns the size of the image being displayed.
"""
if self.__image is None: return (0, 0)
else: return self.__image.GetSize()
def Draw(self, ev=None):
"""Draws this ``ImagePanel``. The image is scaled to the current panel
size.
"""
self.ClearBackground()
if self.__image is None:
return
if ev is None: dc = wx.ClientDC(self)
else: dc = wx.PaintDC( self)
if not dc.IsOk():
return
width, height = dc.GetSize().Get()
if width == 0 or height == 0:
return
bitmap = self.__image.Scale(width, height).ConvertToBitmap()
dc.DrawBitmap(bitmap, 0, 0, False)
#!/usr/bin/env python
#
# layout.py - Utility functions for calculating canvas sizes and laying them
# out.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""Utility functions for calculating canvas sizes and laying them out.
This module implements a simple layout manager, for laying out canvases and
associated orientation labels. It is used primarily by the :mod:`.render`
application, for off-screen rendering.
You can use the following classes to define a layout:
.. autosummary::
:nosignatures:
Bitmap
Space
HBox
VBox
And the following functions to generate layouts and bitmaps:
.. autosummary::
:nosignatures:
buildOrthoLayout
buildCanvasBox
padBitmap
layoutToBitmap
A few functions are also provided for calculating the display size, in pixels,
of one or more canvases which are displaying a defined coordinate system. The
canvas sizes are calculated so that their aspect ratio, relative to the
respective horizontal/vertical display axes, are maintained, and that the
canvases are sized proportionally with respect to each other. The following
size calculation functions are available:
.. autosummary::
:nosignatures:
calcSizes
calcGridSizes
calcHorizontalSizes
calcVerticalSizes
calcPixWidth
calcPixHeight
"""
import logging
import numpy as np
log = logging.getLogger(__name__)
class Bitmap(object):
"""A class which encapsulates a RGBA bitmap, assumed to be a
``numpy.uint8`` array of shape :math:`height \\times width \\times 4`).
.. warning:: Note the unusual array shape - height is the first axis,
and width the second!
A ``Bitmap`` instance has the following attributes:
- ``bitmap``: The bitmap data
- ``width``: Bitmap width in pixels
- ``height``: Bitmap height in pixels
"""
def __init__(self, bitmap):
"""Create a ``Bitmap``.
:arg bitmap: :mod:`numpy` array containing the bitmap data.
"""
self.bitmap = bitmap
self.width = bitmap.shape[1]
self.height = bitmap.shape[0]
class Space(object):
"""A class which represents empty space of a specific width/height.
A ``Space`` instance has the following attributes:
- ``width``: Width in pixels.
- ``height``: Height in pixels.
"""
def __init__(self, width, height):
"""Creat a ``Space``.
:arg width: Width in pixels.
:arg height: Height in pixels.
"""
self.width = width
self.height = height
class HBox(object):
"""A class which contains items to be laid out horizontally.
After creation, new items should be added via the :meth:`append` method.
A ``HBox`` instance has the following attributes:
- ``width``: Total width in pixels.
- ``height``: Total height in pixels.
- ``items``: List of items in this ``HBox``.
"""
def __init__(self, items=None):
"""Create a ``HBox``.
:arg items: List of items contained in this ``HBox``.
"""
self.width = 0
self.height = 0
self.items = []
if items is not None: map(self.append, items)
def append(self, item):
"""Append a new item to this ``HBox``. """
self.items.append(item)
self.width = self.width + item.width
if item.height > self.height:
self.height = item.height
class VBox(object):
"""A class which contains items to be laid out vertically.
After creation, new items can be added via the :meth:`append` method.
A ``VBox`` instance has the following attributes:
- ``width``: Total width in pixels.
- ``height``: Total height in pixels.
- ``items``: List of items in this ``VBox``.
"""
def __init__(self, items=None):
"""Create a ``VBox``.
:arg items: List of items contained in this ``VBox``.
"""
self.width = 0
self.height = 0
self.items = []
if items is not None: map(self.append, items)
def append(self, item):
"""Append a new item to this ``VBox``. """
self.items.append(item)
self.height = self.height + item.height
if item.width > self.width:
self.width = item.width
def padBitmap(bitmap, width, height, vert, bgColour):
"""Pads the given bitmap with zeros along the secondary axis (specified
with the ``vert`` parameter), so that it fits in the given
``width``/``height``.
:arg bitmap: A ``numpy.array`` of size :math:`x \\times y \\times 4`
containing a RGBA bitmap.
:arg width: Desired width in pixels.
:arg height: Desired height in pixels.
:arg vert: If ``vert`` is ``True``, the bitmap is padded
horizontally to fit ``width``. Otherwise, the
bitmap is padded vertically to fit ``height``.
:arg bgColour: Background colour to use for padding. Must be
a ``(r, g, b, a)`` tuple with each channel in
the range ``[0 - 255]``.
"""
iheight = bitmap.shape[0]
iwidth = bitmap.shape[1]
if vert:
if iwidth < width:
lpad = int(np.floor((width - iwidth) / 2.0))
rpad = int(np.ceil( (width - iwidth) / 2.0))
lpad = np.zeros((iheight, lpad, 4), dtype=np.uint8)
rpad = np.zeros((iheight, rpad, 4), dtype=np.uint8)
lpad[:] = bgColour
rpad[:] = bgColour
bitmap = np.hstack((lpad, bitmap, rpad))
else:
if iheight < height:
tpad = int(np.floor((height - iheight) / 2.0))
bpad = int(np.ceil(( height - iheight) / 2.0))
tpad = np.zeros((tpad, iwidth, 4), dtype=np.uint8)
bpad = np.zeros((bpad, iwidth, 4), dtype=np.uint8)
tpad[:] = bgColour
bpad[:] = bgColour
bitmap = np.vstack((tpad, bitmap, bpad))
return bitmap
def layoutToBitmap(layout, bgColour):
"""Recursively turns the given ``layout`` object into a bitmap.
:arg layout: A :class:`Bitmap`, :class:`Space`, :class:`HBox` or
:class:`VBox` instance.
:arg bgColour: Background colour used to fill in empty space. Must be
a ``(r, g, b, a)`` tuple with channel values in the range
``[0, 255]``. Defaults to transparent.
:returns: a ``numpy.uint8`` array of size
:math:`height \\times width \\times 4`.
"""
if bgColour is None: bgColour = [0, 0, 0, 0]
bgColour = np.array(bgColour, dtype=np.uint8)
# Space is easy
if isinstance(layout, Space):
space = np.zeros((layout.height, layout.width, 4), dtype=np.uint8)
space[:] = bgColour
return space
# Bitmap is easy
elif isinstance(layout, Bitmap):
return np.array(layout.bitmap, dtype=np.uint8)
# Boxes require a bit of work
if isinstance(layout, HBox): vert = False
elif isinstance(layout, VBox): vert = True
# Recursively bitmapify the children of the box
itemBmps = map(lambda i: layoutToBitmap(i, bgColour), layout.items)
# Pad each of the bitmaps so they are all the same
# size along the secondary axis (which is width
# if the layout is a VBox, and height if the layout
# is a HBox).
width = layout.width
height = layout.height
itemBmps = map(lambda bmp: padBitmap(bmp, width, height, vert, bgColour),
itemBmps)
if vert: return np.vstack(itemBmps)
else: return np.hstack(itemBmps)
def buildCanvasBox(canvasBmp, labelBmps, showLabels, labelSize):
"""Builds a layout containing the given canvas bitmap, and orientation
labels (if ``showLabels`` is ``True``).
:arg canvasBmp: A ``numpy.uint8`` array containing a bitmap.
:arg labelBmps: Only used if ``showLabels`` is ``True``. ``numpy.uint8``
arrays containing label bitmaps. Must be a
dictionary of ``{side : numpy.uint8}`` mappings,
and must have keys ``top``, ``bottom``, ``left`` and
``right``.
:arg showLabels: If ``True``, the orientation labels provided in
``labelBmps`` are added to the layout.
:arg labelSize: Label sizes - the ``left``/``right`` label widths,
and ``top``/``bottom`` label heights are padded to this
size using ``Space`` objects.
:returns: A :class:`Bitmap` or :class:`VBox` instance.
"""
if not showLabels: return Bitmap(canvasBmp)
row1Box = HBox([Space(labelSize, labelSize),
Bitmap(labelBmps['top']),
Space(labelSize, labelSize)])
row2Box = HBox([Bitmap(labelBmps['left']),
Bitmap(canvasBmp),
Bitmap(labelBmps['right'])])
row3Box = HBox([Space(labelSize, labelSize),
Bitmap(labelBmps['bottom']),
Space(labelSize, labelSize)])
return VBox((row1Box, row2Box, row3Box))
def buildOrthoLayout(canvasBmps,
labelBmps,
layout,
showLabels,
labelSize):
"""Builds a layout containing the given canvas bitmaps, label bitmaps, and
colour bar bitmap.
:arg canvasBmps: A list of ``numpy.uint8`` arrays containing the canvas
bitmaps to be laid out.
:arg layout: One of ``'horizontal'``, ``'vertical'``, or ``'grid'``.
See the :func:`buildCanvasBox` for details on the other parameters.
:returns: A :class:`HBox` or :class:`VBox` describing the layout.
"""
if labelBmps is None:
labelBmps = [None] * len(canvasBmps)
showLabels = False
canvasBoxes = map(lambda cbmp, lbmps: buildCanvasBox(cbmp,
lbmps,
showLabels,
labelSize),
canvasBmps,
labelBmps)
if layout == 'horizontal': canvasBox = HBox(canvasBoxes)
elif layout == 'vertical': canvasBox = VBox(canvasBoxes)
elif layout == 'grid':
row1Box = HBox([canvasBoxes[0], canvasBoxes[1]])
row2Box = HBox([canvasBoxes[2], Space(canvasBoxes[1].width,
canvasBoxes[2].height)])
canvasBox = VBox((row1Box, row2Box))
return canvasBox
def calcSizes(layout, canvasaxes, bounds, width, height):
"""Convenience function which, based upon whether the `layout` argument
is ``'horizontal'``, ``'vertical'``, or ``'grid'``, respectively calls
one of:
- :func:`calcHorizontalSizes`
- :func:`calcVerticalSizes`
- :func:`calcGridSizes`
:arg layout: String specifying the layout type.
:arg canvsaxes: A list of tuples, one for each canvas to be laid out.
Each tuple contains two values, ``(i, j)``, where ``i``
is an index, into ``bounds``, specifying the canvas
width, and ``j`` is an index into ``bounds``, specifying
the canvas height, in the display coordinate system.
:arg bounds: A list of three values specifying the size of the display
space.
:arg width: Maximum width in pixels.
:arg height: Maximum height in pixels.
:returns: A list of ``(width, height)`` tuples, one for each canvas,
each specifying the canvas width and height in pixels.
"""
layout = layout.lower()
func = None
if layout == 'horizontal': func = calcHorizontalSizes
elif layout == 'vertical': func = calcVerticalSizes
elif layout == 'grid': func = calcGridSizes
# a bad value for layout
# will result in an error
sizes = func(canvasaxes, bounds, width, height)
log.debug('For space ({}, {}) and {} layout, pixel '
'sizes for canvases {} ({}) are: {}'.format(
width, height, layout, canvasaxes, bounds, sizes))
return sizes
def calcGridSizes(canvasaxes, bounds, width, height):
"""Calculates the size of three canvases so that they are laid
out in a grid, i.e.:
0 1
2
.. note:: If less than three canvases are specified, they are passed to
the :func:`calcHorizontalLayout` function.
See :func:`calcSizes` for details on the arguments.
"""
if len(canvasaxes) < 3:
return calcHorizontalSizes(canvasaxes, bounds, width, height)
canvasWidths = [bounds[c[0]] for c in canvasaxes]
canvasHeights = [bounds[c[1]] for c in canvasaxes]
ttlWidth = float(canvasWidths[ 0] + canvasWidths[ 1])
ttlHeight = float(canvasHeights[0] + canvasHeights[2])
sizes = []
for i in range(len(canvasaxes)):
cw = width * (canvasWidths[ i] / ttlWidth)
ch = height * (canvasHeights[i] / ttlHeight)
acw, ach = _adjustPixelSize(canvasWidths[ i],
canvasHeights[i],
cw,
ch)
if (float(cw) / ch) > (float(acw) / ach): cw, ch = cw, ach
else: cw, ch = acw, ch
sizes.append((cw, ch))
return sizes
def calcVerticalSizes(canvasaxes, bounds, width, height):
"""Calculates the size of up to three canvases so they are laid out
vertically.
See :func:`calcSizes` for details on the arguments.
"""
return _calcFlatSizes(canvasaxes, bounds, width, height, True)
def calcHorizontalSizes(canvasaxes, bounds, width, height):
"""Calculates the size of up to three canvases so they are laid out
horizontally.
See :func:`calcSizes` for details on the arguments.
"""
return _calcFlatSizes(canvasaxes, bounds, width, height, False)
def _calcFlatSizes(canvasaxes, bounds, width, height, vert=True):
"""Used by :func:`calcVerticalSizes` and :func:`calcHorizontalSizes`.
Calculates the width and height, in pixels, of each canvas.
:arg vert: If ``True`` the sizes are calculated for a vertical layout;
otherwise they are calculated for a horizontal layout.
See :func:`calcSizes` for details on the other arguments.
:returns: A list of ``(width, height)`` tuples, one for each canvas,
each specifying the canvas width and height in pixels.
"""
# Get the canvas dimensions in world space
canvasWidths = [bounds[c[0]] for c in canvasaxes]
canvasHeights = [bounds[c[1]] for c in canvasaxes]
maxWidth = float(max(canvasWidths))
maxHeight = float(max(canvasHeights))
ttlWidth = float(sum(canvasWidths))
ttlHeight = float(sum(canvasHeights))
if vert: ttlWidth = maxWidth
else: ttlHeight = maxHeight
sizes = []
for i in range(len(canvasaxes)):
if ttlWidth == 0: cw = 0
else: cw = width * (canvasWidths[ i] / ttlWidth)
if ttlHeight == 0: ch = 0
else: ch = height * (canvasHeights[i] / ttlHeight)
sizes.append((cw, ch))
return sizes
def calcPixWidth(wldWidth, wldHeight, pixHeight):
"""Given the dimensions of a space to be displayed, and the available
height in pixels, calculates the required pixel width.
:arg wldWidth: Width of the display coordinate system
:arg wldHeight: Height of the display coordinate system
:arg pixHeight: Available height in pixels.
:returns: The required width in pixels.
"""
return _adjustPixelSize(wldWidth,
wldHeight,
pixHeight * (2 ** 32),
pixHeight)[0]
def calcPixHeight(wldWidth, wldHeight, pixWidth):
"""Given the dimensions of a space to be displayed, and the available
width in pixels, calculates the required pixel height.
:arg wldWidth: Width of the display coordinate system
:arg wldHeight: Height of the display coordinate system
:arg pixWidth: Available width in pixels.
:returns: The required height in pixels.
"""
return _adjustPixelSize(wldWidth,
wldHeight,
pixWidth,
pixWidth * (2 ** 32))[1]
def _adjustPixelSize(wldWidth, wldHeight, pixWidth, pixHeight):
"""Used by :func:`calcPixelWidth` and :func:`calcPixelHeight`.
Potentially reduces the given pixel width/height such that the
display space aspect ratio is maintained.
"""
if any((pixWidth == 0,
pixHeight == 0,
wldWidth == 0,
wldHeight == 0)):
return 0, 0
pixRatio = float(pixWidth) / pixHeight
wldRatio = float(wldWidth) / wldHeight
if pixRatio > wldRatio:
pixWidth = wldWidth * (pixHeight / wldHeight)
elif pixRatio < wldRatio:
pixHeight = wldHeight * (pixWidth / wldWidth)
return pixWidth, pixHeight
#!/usr/bin/env python
#
# runwindow.py - Run a process, display its output in a wx window.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides classes and functions for running a non-interactive
process, and displaying its output.
This module provides the :class:`RunPanel` and :class:`ProcessManager`
classes, and a couple of associated convenience functions.
.. autosummary::
:nosignatures:
RunPanel
ProcessManager
run
checkAndRun
"""
import os
import signal
import logging
import subprocess as subp
import threading as thread
try: import queue
except: import Queue as queue
import wx
log = logging.getLogger(__name__)
class RunPanel(wx.Panel):
"""A panel which displays a multiline text control, and a couple of
buttons along the bottom. ``RunPanel`` instances are created by the
:func:`run` function, and used/controlled by the :class:`ProcessManager`.
One of the buttons is intended to closes the window in which this panel
is contained. The second button is intended to terminate the running
process. Both buttons are unbound by default, so must be manually
configured by the creator.
The text panel and buttons are available as the following attributes:
- ``text``: The text panel.
- ``closeButton``: The `Close window` button.
- ``killButton``: The `Terminate process` button.
"""
def __init__(self, parent):
"""Create a ``RunPanel``.
:arg parent: The :mod:`wx` parent object.
"""
wx.Panel.__init__(self, parent)
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.SetSizer(self.sizer)
# Horizontal scrolling no work in OSX
# mavericks. I think it's a wxwidgets bug.
self.text = wx.TextCtrl(self,
style=wx.TE_MULTILINE |
wx.TE_READONLY |
wx.TE_DONTWRAP |
wx.HSCROLL)
self.sizer.Add(self.text, flag=wx.EXPAND, proportion=1)
self.btnPanel = wx.Panel(self)
self.btnSizer = wx.BoxSizer(wx.HORIZONTAL)
self.btnPanel.SetSizer(self.btnSizer)
self.sizer.Add(self.btnPanel, flag=wx.EXPAND)
self.killButton = wx.Button(self.btnPanel, label='Terminate process')
self.closeButton = wx.Button(self.btnPanel, label='Close window')
self.btnSizer.Add(self.killButton, flag=wx.EXPAND, proportion=1)
self.btnSizer.Add(self.closeButton, flag=wx.EXPAND, proportion=1)
class ProcessManager(thread.Thread):
"""A thread which manages the execution of a child process, and capture
of its output.
The process output is displayed in a :class:`RunPanel` which must be
passed to the ``ProcessManager`` on creation.
The :meth:`termProc` method can be used to terminate the child process
before it has completed.
"""
def __init__(self, cmd, parent, runPanel, onFinish):
"""Create a ``ProcessManager``.
:arg cmd: String or list of strings, the command to be
executed.
:arg parent: :mod:`wx` parent object.
:arg runPanel: A :class:`RunPanel` instance , for displaying the
child process output.
:arg onFinish: Callback function to be called when the process
finishes. May be ``None``. Must accept two parameters,
the GUI ``parent`` object, and the process return code.
"""
thread.Thread.__init__(self, name=cmd[0])
self.cmd = cmd
self.parent = parent
self.runPanel = runPanel
self.onFinish = onFinish
# Handle to the Popen object which represents
# the child process. Created in run().
self.proc = None
# A queue for sharing data between the thread which
# is blocking on process output (this thread object),
# and the wx main thread which writes that output to
# the runPanel
self.outq = queue.Queue()
# Put the command string at the top of the text control
self.outq.put(' '.join(self.cmd) + '\n\n')
wx.CallAfter(self.__writeToPanel)
def __writeToPanel(self):
"""Reads a string from the output queue, and appends it
to the :class:`RunPanel`. This method is intended to be
executed via :func:`wx.CallAfter`.
"""
try: output = self.outq.get_nowait()
except queue.Empty: output = None
if output is None: return
# ignore errors - the user may have closed the
# runPanel window before the process has completed
try: self.runPanel.text.WriteText(output)
except: pass
def run(self):
"""Starts the process, then reads its output line by line, writing
each line asynchronously to the :class:`RunPanel`. When the
process ends, the ``onFinish`` method (if there is one) is called.
If the process finishes abnormally (with a non-0 exit code) a warning
dialog is displayed.
"""
# Run the command. The preexec_fn parameter creates
# a process group, so we are able to kill the child
# process, and all of its children, if necessary.
log.debug('Running process: "{}"'.format(' '.join(self.cmd)))
self.proc = subp.Popen(self.cmd,
stdout=subp.PIPE,
bufsize=1,
stderr=subp.STDOUT,
preexec_fn=os.setsid)
# read process output, line by line, pushing
# each line onto the output queue and
# asynchronously writing it to the runPanel
for line in self.proc.stdout:
log.debug('Process output: {}'.format(line.strip()))
self.outq.put(line)
wx.CallAfter(self.__writeToPanel)
# When the above for loop ends, it means that the stdout
# pipe has been broken. But it doesn't mean that the
# subprocess is finished. So here, we wait until the
# subprocess terminates, before continuing,
self.proc.wait()
retcode = self.proc.returncode
log.debug( 'Process finished with return code {}'.format(retcode))
self.outq.put('Process finished with return code {}'.format(retcode))
wx.CallAfter(self.__writeToPanel)
# Disable the 'terminate' button on the run panel
def updateKillButton():
# ignore errors - see __writeToPanel
try: self.runPanel.killButton.Enable(False)
except: pass
wx.CallAfter(updateKillButton)
# Run the onFinish handler, if there is one
if self.onFinish is not None:
wx.CallAfter(self.onFinish, self.parent, retcode)
def termProc(self):
"""Attempts to kill the running child process."""
log.debug('Attempting to send SIGTERM to '
'process group with pid {}'.format(self.proc.pid))
os.killpg(self.proc.pid, signal.SIGTERM)
# put a message on the runPanel
self.outq.put('\nSIGTERM sent to process\n\n')
wx.CallAfter(self.__writeToPanel)
def run(name, cmd, parent, onFinish=None, modal=True):
"""Runs the given command, displaying the output in a :class:`RunPanel`.
:arg name: Name of the tool to be run, used in the window title.
:arg cmd: String or list of strings, specifying the command to be
executed.
:arg parent: :mod:`wx` parent object.
:arg modal: If ``True``, the frame which contains the ``RunPanel``
will be modal.
:arg onFinish: Function to be called when the process ends. Must
accept two parameters - a reference to the :mod:`wx`
frame/dialog displaying the process output, and
the exit code of the application.
"""
# Create the GUI - if modal, the easiest
# approach is to use a wx.Dialog
if modal:
frame = wx.Dialog(
parent,
title=name,
style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
else:
frame = wx.Frame(parent, title=name)
panel = RunPanel(frame)
# Create the thread which runs the child process
mgr = ProcessManager(cmd, parent, panel, onFinish)
# Bind the panel control buttons so they do stuff
panel.closeButton.Bind(wx.EVT_BUTTON, lambda e: frame.Close())
panel.killButton .Bind(wx.EVT_BUTTON, lambda e: mgr.termProc())
# Run the thread which runs the child process
mgr.start()
# layout and show the window
frame.Layout()
if modal: frame.ShowModal()
else: frame.Show()
def checkAndRun(name,
opts,
parent,
cmdFunc,
optLabels={},
modal=True,
onFinish=None):
"""Validates the given options. If invalid, a dialog is shown,
informing the user about the errors. Otherwise, the tool is
executed, and its output shown in a dialog window. Parameters:
:arg opts: A :class:`props.HasProperties` object to be
validated.
:arg cmdFunc: Function which takes a :class:`props.HasProperties`
object, and returns a command to be executed (as a
list of strings), which will be passed to the :func:`run`
function.
:arg optLabels: Dictionary containing property ``{name : label}`` mappings.
Used in the error dialog, if any options are invalid.
See :func:`run` for details on the other arguments.
"""
errors = opts.validateAll()
if len(errors) > 0:
msg = 'There are numerous errors which need '\
'to be fixed before {} can be run:\n'.format(name)
for opt, error in errors:
if opt in optLabels: name = optLabels[opt]
msg = msg + '\n - {}: {}'.format(opt, error)
wx.MessageDialog(
parent,
message=msg,
style=wx.OK | wx.ICON_ERROR).ShowModal()
else:
cmd = cmdFunc(opts)
run(name, cmd, parent, onFinish, modal)
#!/usr/bin/env python
#
# status.py - A simple interface for displaying messages.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This is a little module which provides an interface for displaying a
message, or status update, to the user. The ``status`` module provides the
following functions:
.. autosummary::
:nosignatures:
setTarget
update
clearStatus
A couple of other functions are also provided, for reporting error messages
to the user:
.. autosummary::
:nosignatures:
reportError
reportIfError
reportErrorDecorator
The :func:`update` function may be used to display a message. By default, the
message is simply logged (via the ``logging`` module). However, if a status
target has been set via the :func:`setTarget` function, the message is also
passed to this target.
.. warning:: If the status update target is a ``wx`` GUI object, you must
make sure that it is updated asynchronously (e.g. via
``wx.CallAfter``).
"""
import threading
import contextlib
import logging
import inspect
import os.path as op
log = logging.getLogger(__name__)
_statusUpdateTarget = None
"""A reference to the status update target - this is ``None`` by default, and
can be set via :func:`setTarget`.
"""
_clearThread = None
"""Reference to a :class:`ClearThread`, which is a daemon thread that clears
the status after the timeout passed to the :func:`update` function.
"""
def setTarget(target):
"""Set a target function to receive status updates. The ``target`` must
be a function which accepts a string as its sole parameter.
"""
global _statusUpdateTarget
_statusUpdateTarget = target
def update(message, timeout=1.0):
"""Display a status update to the user. The message is logged and,
if a status update target has been set, passed to the target.
:arg timeout: Timeout (in seconds) after which the status will be
cleared (via the :class:`ClearThread`). Pass in ``None``
to disable this behaviour.
.. note:: The ``timeout`` method only makes sense to use if the status
target is a GUI widget of some sort.
"""
global _clearThread
global _statusUpdateTarget
if log.getEffectiveLevel() == logging.DEBUG:
frame = inspect.stack()[1]
module = frame[1]
linenum = frame[2]
module = op.basename(module)
log.debug('[{}:{}] {}'.format(module, linenum, message))
if _statusUpdateTarget is None:
return
_statusUpdateTarget(message)
if timeout is not None:
log.debug('timeout is not None - starting clear thread')
if _clearThread is None:
_clearThread = ClearThread()
_clearThread.start()
_clearThread.clear(timeout)
else:
if _clearThread is not None:
_clearThread.veto()
log.debug('No timeout - vetoing clear thread')
def clearStatus():
"""Clear the status. If a status update target has been set, it is passed
the empty string.
"""
if _statusUpdateTarget is None:
return
_statusUpdateTarget('')
def reportError(title, msg, err):
"""Reports an error to the user in a generic manner. If a GUI is available,
(see the :meth.`.Platform.haveGui` attribute), a ``wx.MessageBox`` is
shown. Otherwise a log message is generated.
"""
from .platform import platform as fslplatform
from . import async
if fslplatform.haveGui:
msg = '{}\n\nDetails: {}'.format(msg, str(err))
import wx
async.idle(wx.MessageBox, msg, title, wx.ICON_ERROR | wx.OK)
@contextlib.contextmanager
def reportIfError(title, msg, raiseError=True, report=True):
"""A context manager which calls :func:`reportError` if the enclosed code
raises an ``Exception``.
:arg raiseError: If ``True``, the ``Exception`` which was raised is
propagated upwards.
:arg report: Defaults to ``True``. If ``False``, an error message
is logged, but :func:`reportError` is not called.
"""
try:
yield
except Exception as e:
log.error('{}: {}'.format(title, msg), exc_info=True)
if report:
reportError(title, msg, e)
if raiseError:
raise
def reportErrorDecorator(*args, **kwargs):
"""A decorator which wraps the decorated function with
:func:`reportIfError`.
"""
def decorator(func):
def wrapper(*wargs, **wkwargs):
with reportIfError(*args, **kwargs):
func(*wargs, **wkwargs)
return wrapper
return decorator
class ClearThread(threading.Thread):
"""The ``ClearThread`` is a daemon thread used by the :func:`update`
function. Only one ``ClearThread`` is ever started - it is started on the
first call to ``update`` when a timeout is specified.
The ``ClearThread`` waits until the :meth:`clear` method is called.
It then waits for the specified timeout and, unless another call to
:meth:`clear`, or a call to :meth:`veto` has been made, clears the
status via a call to :func:`clearStatus`.
"""
def __init__(self):
"""Create a ``ClearThread``. """
threading.Thread.__init__(self)
self.daemon = True
self.__clearEvent = threading.Event()
self.__vetoEvent = threading.Event()
self.__timeout = None
def clear(self, timeout):
"""Clear the status after the specified timeout (in seconds). """
self.__timeout = timeout
self.__vetoEvent .clear()
self.__clearEvent.set()
def veto(self):
"""If this ``ClearThread`` is waiting on a timeout to clear
the status, a call to ``veto`` will prevent it from doing so.
"""
self.__vetoEvent.set()
def run(self):
"""The ``ClearThread`` function. Infinite loop which waits until
the :meth:`clear` method is called, and then clears the status
(via a call to :func:`clearStatus`).
"""
while True:
self.__vetoEvent .clear()
self.__clearEvent.wait()
self.__clearEvent.clear()
# http://bugs.python.org/issue14623
#
# When the main thread exits, daemon threads will
# continue to run after the threading module is
# destroyed. Calls to the Event methods can thus
# result in errors.
try:
if not self.__clearEvent.wait(self.__timeout) and \
not self.__vetoEvent.isSet():
log.debug('Timeout - clearing status')
clearStatus()
except TypeError:
return
#!/usr/bin/env python
#
# textbitmap.py - A function which renders some text using matplotlib, and
# returns it as an RGBA bitmap.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides a single function, :func:`textBitmap`, which renders
some text off-screen using :mod:`matplotlib`, and returns it as an RGBA bitmap.
"""
def textBitmap(text,
width,
height,
fontSize,
fgColour,
bgColour,
alpha=1.0):
"""Draw some text using :mod:`matplotlib`.
The rendered text is returned as a RGBA bitmap within a ``numpy.uint8``
array of size :math:`w \\times h \\times 4`, with the top-left pixel
located at index ``[0, 0, :]``.
:arg text: Text to render.
:arg width: Width in pixels.
:arg height: Height in pixels.
:arg fontSize: Font size in points.
:arg fgColour: Foreground (text) colour - can be any colour specification
that is accepted by :mod:`matplotlib`.
:arg bgColour: Background colour - can be any colour specification that
is accepted by :mod:`matplotlib`..
:arg alpha: Text transparency, in the range ``[0.0 - 1.0]``.
"""
# Imports are expensive
import numpy as np
import matplotlib.backends.backend_agg as mplagg
import matplotlib.figure as mplfig
dpi = 96.0
fig = mplfig.Figure(figsize=(width / dpi, height / dpi),
dpi=dpi)
canvas = mplagg.FigureCanvasAgg(fig)
ax = fig.add_axes([0, 0, 1, 1])
ax.axis('off')
if bgColour is not None: fig.patch.set_facecolor(bgColour)
else: fig.patch.set_alpha(0)
ax.set_xticks([])
ax.set_yticks([])
ax.text(0.5,
0.5,
text,
fontsize=fontSize,
verticalalignment='center',
horizontalalignment='center',
transform=ax.transAxes,
color=fgColour,
alpha=alpha)
try: fig.tight_layout()
except: pass
canvas.draw()
buf = canvas.tostring_argb()
ncols, nrows = canvas.get_width_height()
bitmap = np.fromstring(buf, dtype=np.uint8)
bitmap = bitmap.reshape(nrows, ncols, 4)
rgb = bitmap[:, :, 1:]
a = bitmap[:, :, 0]
bitmap = np.dstack((rgb, a))
return bitmap
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment