diff --git a/fsl/fslview/imagelistpanel.py b/fsl/fslview/imagelistpanel.py index 1c17a1845b27f46d776768684d2d36c08f314c28..389bd00835a2c372f9f22212d8301f6c391235c6 100644 --- a/fsl/fslview/imagelistpanel.py +++ b/fsl/fslview/imagelistpanel.py @@ -13,7 +13,7 @@ import wx import fsl.data.fslimage as fslimage import fsl.data.imagefile as imagefile -import fsl.utils.elistbox as elistbox +import fsl.gui.elistbox as elistbox import fsl.props as props diff --git a/fsl/gui/__init__.py b/fsl/gui/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/fsl/utils/elistbox.py b/fsl/gui/elistbox.py similarity index 100% rename from fsl/utils/elistbox.py rename to fsl/gui/elistbox.py diff --git a/fsl/gui/notebook.py b/fsl/gui/notebook.py new file mode 100644 index 0000000000000000000000000000000000000000..6c02a0360b4ddd7789330d094680c46156f91199 --- /dev/null +++ b/fsl/gui/notebook.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python +# +# notebook.py - Re-implementation of the wx.Notebook widget, which +# supports page enabling/disabling, and page visibility. +# +# I didn't want it to come to this, but both the wx.lib.agw.aui.AuiNotebook +# and wx.lib.agw.flatnotebook are too difficult to use. The AuiNotebook +# requires me to use an AuiManager for layout, and the flatnotebook has +# layout/fitting issues. +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# +import wx + + +class Notebook(wx.Panel): + """ + A wx.Panel whcih provides Notebook-like functionality. Manages the + display of multiple child windows. A row of buttons along the top + allows the user to select which child window to display. + """ + + def __init__(self, parent): + + wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER) + + self.buttonPanel = wx.Panel(self) + + self.sizer = wx.BoxSizer(wx.VERTICAL) + self.buttonSizer = wx.BoxSizer(wx.HORIZONTAL) + + self. SetSizer(self.sizer) + self.buttonPanel.SetSizer(self.buttonSizer) + + # a row of buttons along the top + self.sizer.Add( + self.buttonPanel, + border=5, + flag=wx.EXPAND | wx.ALIGN_CENTER | wx.TOP | wx.RIGHT | wx.LEFT) + + # a horizontal line separating the buttons from the pages + self.sizer.Add( + wx.StaticLine(self, style=wx.LI_HORIZONTAL), + border=5, + flag=wx.EXPAND | wx.ALIGN_CENTER | wx.LEFT | wx.RIGHT) + + # a vertical line at the start of the button row + self.buttonSizer.Insert( + 0, + wx.StaticLine(self.buttonPanel, style=wx.VERTICAL), + border=3, + flag=wx.EXPAND | wx.ALIGN_CENTER | wx.LEFT | wx.RIGHT | wx.TOP) + + self._pages = [] + self._buttons = [] + self._selected = None + + + def DoGetBestClientSize(self): + """ + Calculate and return the best (minimum) size for the Notebook widget. + The returned size is the minimum size of the largest page, plus + the size of the button panel. + """ + + # TODO not taking into account divider + # line between buttons and pages + + buttonSize = self.buttonPanel.GetBestSize() + pageSizes = map(lambda p: p.GetBestSize(), self._pages) + + buttonWidth = buttonSize.GetWidth() + buttonHeight = buttonSize.GetHeight() + + pageWidths = map(lambda ps: ps.GetWidth(), pageSizes) + pageHeights = map(lambda ps: ps.GetHeight(), pageSizes) + + pageHeights = [ph + buttonHeight for ph in pageHeights] + + myWidth = max([buttonWidth] + pageWidths) + myHeight = max(pageHeights) + return wx.Size(myWidth, myHeight) + + + def FindPage(self, page): + """ + Returns the index of the given page, or wx.NOT_FOUND if + the page is not in this notebook. + """ + try: return self._pages.index(page) + except: return wx.NOT_FOUND + + + def InsertPage(self, index, page, text): + """ + Inserts the given page into the notebook at the specified index. + A button for the page is also added to the button row, with the + specified text. + """ + + if (index > len(self._pages)) or (index < 0): + raise IndexError('Index out of range: {}'.format(index)) + + # index * 2 because we add a vertical + # line after every button (and + 1 for + # the line at the start of the button row) + button = wx.StaticText(self.buttonPanel, label=text) + buttonIdx = index * 2 + 1 + + self._pages. insert(index, page) + self._buttons.insert(index, button) + + # index + 2 to account for the button panel and + # the horizontal divider line (see __init__) + self.sizer.Insert( + index + 2, page, border=5, flag=wx.EXPAND | wx.ALL, proportion=1) + + self.buttonSizer.Insert( + buttonIdx, + button, + flag=wx.ALIGN_CENTER) + + # A vertical line at the end of every button + self.buttonSizer.Insert( + buttonIdx + 1, + wx.StaticLine(self.buttonPanel, style=wx.VERTICAL), + border=3, + flag=wx.EXPAND | wx.ALIGN_CENTER | wx.LEFT | wx.RIGHT | wx.TOP) + + # When the button is pushed, show the page + # (unless the button has been disabled) + def _showPage(ev): + if not button.IsEnabled(): return + self.SetSelection(self.FindPage(page)) + + button.Bind(wx.EVT_LEFT_DOWN, _showPage) + + page.Layout() + page.Fit() + + self.buttonPanel.Layout() + self.buttonPanel.Fit() + + self.SetMinClientSize(self.DoGetBestClientSize()) + + self.Layout() + self.Fit() + + + def AddPage(self, page, text): + """ + Adds the given page (and a corresponding button with the + given text) to the end of the notebook. + """ + self.InsertPage(len(self._pages), page, text) + + + def RemovePage(self, index): + """ + Removes the page at the specified index, but does not destroy it. + """ + + if (index >= len(self._pages)) or (index < 0): + raise IndexError('Index out of range: {}'.format(index)) + + buttonIdx = index * 2 + 1 + pageIdx = index + 2 + + self._buttons.pop(index) + self._pages .pop(index) + + # Destroy the button for this page (and the + # vertical line that comes after the button) + self.buttonSizer.Remove(buttonIdx) + self.buttonSizer.Remove(buttonIdx + 1) + + # Remove the page but do not destroy it + self.pagePanel .Detach(pageIdx) + + + def DeletePage(self, index): + """ + Removes the page at the specified index, and (attempts to) destroy it. + """ + page = self._pages[index] + self.RemovePage(index) + page.Destroy() + + + def GetSelection(self): + """ + Returns the index of the currently selected page. + """ + return self._selected + + + def SetSelection(self, index): + """ + Sets the displayed page to the one at the specified index. + """ + + if index < 0 or index >= len(self._pages): + raise IndexError('Index out of range: {}'.format(index)) + + self._selected = index + + for i in range(len(self._pages)): + + page = self._pages[ i] + button = self._buttons[i] + showThis = i == self._selected + + if showThis: + button.SetBackgroundColour('#ffffff') + page.Show() + else: + button.SetBackgroundColour(None) + page.Hide() + + button.Layout() + self.buttonPanel.Layout() + self.Layout() + self.Refresh() + + + def AdvanceSelection(self, forward=True): + """ + Selects the next (or previous, if forward is False) enabled page. + """ + + if forward: offset = 1 + else: offset = -1 + + newSelection = (self.GetSelection() + offset) % len(self._pages) + + while newSelection != self._selected: + + if self._buttons[newSelection].IsEnabled(): + break + + newSelection = (self._selected + offset) % len(self._pages) + + self.SetSelection(newSelection) + + + def EnablePage(self, index): + """ + Enables the page at the specified index. + """ + self._buttons[index].Enable() + + + def DisablePage(self, index): + """ + Disables the page at the specified index. + """ + self._buttons[index].Disable() + + if self.GetSelection() == index: + self.AdvanceSelection() + + self.Refresh() + + + def ShowPage(self, index): + """ + Shows the page at the specified index. + """ + self.EnablePage(index) + self._buttons[index].Show() + self._pages[ index].Show() + self.buttonPanel.Layout() + self.Refresh() + + + def HidePage(self, index): + """ + Hides the page at the specified index. + """ + + self._buttons[index].Hide() + self._pages[ index].Hide() + + # we disable the page as well as hiding it,, as the + # AdvanceSelection method, and button handlers, use + # button.IsEnabled to determine whether a page is + # active or not. + self.DisablePage(index) + + self.buttonPanel.Layout() + self.buttonPanel.Refresh() diff --git a/fsl/props/build.py b/fsl/props/build.py index 935f2dc17e50b8687496c2be792a4e3b58008e16..9f80f4c50fd4fa552ac32ab02f4d0562f8d8ea8d 100644 --- a/fsl/props/build.py +++ b/fsl/props/build.py @@ -28,10 +28,11 @@ import sys import wx -import wx.lib.agw.flatnotebook as wxnb import widgets +import fsl.gui.notebook as nb + class ViewItem(object): """ Superclass for Widgets, Buttons, Labels and Groups. Represents an @@ -213,7 +214,7 @@ def _configureEnabledWhen(viewItem, guiObj, hasProps): if viewItem.enabledWhen is None: return None parent = guiObj.GetParent() - isNotebookPage = isinstance(parent, wxnb.FlatNotebook) + isNotebookPage = isinstance(parent, nb.Notebook) def _toggleEnabled(): """ @@ -225,22 +226,10 @@ def _configureEnabledWhen(viewItem, guiObj, hasProps): if viewItem.enabledWhen(hasProps): state = True else: state = False - # TODO The wx.lib.agw.flatnotebook seems to be a little - # flaky for enable/disable support. It may be a better - # option to use the standard wx.Notebook class, with - # some custom event handlers for preventing access to - # a disabled tab. if isNotebookPage: + if state: parent.EnablePage( parent.FindPage(guiObj)) + else: parent.DisablePage(parent.FindPage(guiObj)) - isCurrent = parent.GetCurrentPage() == guiObj - isEnabled = parent.GetEnabled(guiObj._notebookIdx) - - if isEnabled != state: - parent.EnableTab(guiObj._notebookIdx, state) - - if not state and isCurrent: - parent.AdvanceSelection() - elif guiObj.IsEnabled() != state: guiObj.Enable(state) @@ -255,16 +244,18 @@ def _configureVisibleWhen(viewItem, guiObj, hasProps): if viewItem.visibleWhen is None: return None - if isinstance(guiObj.GetParent(), wxnb.FlatNotebook): - raise TypeError('Visibility of notebook pages is not ' - 'configurable - use enabledWhen instead.') + parent = guiObj.GetParent() + isNotebookPage = isinstance(parent, nb.Notebook) def _toggleVis(): visible = viewItem.visibleWhen(hasProps) - parent = guiObj.GetParent() - if visible != guiObj.IsShown(): + if isNotebookPage: + if visible: parent.ShowPage(parent.FindPage(guiObj)) + else: parent.HidePage(parent.FindPage(guiObj)) + + elif visible != guiObj.IsShown(): parent.GetSizer().Show(guiObj, visible) parent.GetSizer().Layout() @@ -356,15 +347,11 @@ def _createNotebookGroup(parent, group, hasProps, propGui): calls to the _create function. """ - nbStyle = wxnb.FNB_NO_X_BUTTON | \ - wxnb.FNB_NO_NAV_BUTTONS | \ - wxnb.FNB_NODRAG - if group.border: borderPanel, notebook = _makeGroupBorder( - parent, group, wxnb.FlatNotebook, agwStyle=nbStyle) + parent, group, nb.Notebook) else: - notebook = wxnb.FlatNotebook(parent, agwStyle=nbStyle) + notebook = nb.Notebook(parent) for i, child in enumerate(group.children): @@ -375,10 +362,12 @@ def _createNotebookGroup(parent, group, hasProps, propGui): child.border = False page = _create(notebook, child, hasProps, propGui) - notebook.InsertPage(i, page, text=pageLabel) + notebook.InsertPage(i, page, pageLabel) page._notebookIdx = i notebook.SetSelection(0) + notebook.Layout() + notebook.Fit() if group.border: return borderPanel else: return notebook @@ -645,7 +634,7 @@ def _prepareEvents(hasProps, propGui): def onChange(*a): for cb in propGui.onChangeCallbacks: cb() - propGui.topLevel.GetSizer().Layout() + propGui.topLevel.Layout() propGui.topLevel.Refresh() propGui.topLevel.Update() diff --git a/fsl/utils/notifylist.py b/fsl/utils/notifylist.py deleted file mode 100644 index 9a7340f54deaea8c2d9725861b56a644c73b83d9..0000000000000000000000000000000000000000 --- a/fsl/utils/notifylist.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python -# -# notifylist.py - -# -# Author: Paul McCarthy <pauldmccarthy@gmail.com> -# - -import collections -import logging -import unittest - -log = logging.getLogger(__name__) - -class NotifyList(object): - """ - """ - - def __init__(self, items=None, validateFunc=None): - """ - """ - - if items is None: items = [] - if validateFunc is None: validateFunc = lambda v: v - - if not isinstance(items, collections.Iterable): - raise TypeError('items must be a sequence') - - map(validateFunc, items) - - self._validate = validateFunc - self._items = items - self._listeners = [] - - - def __len__ (self): return self._items.__len__() - def __getitem__ (self, key): return self._items.__getitem__(key) - def __iter__ (self): return self._items.__iter__() - def __contains__(self, item): return self._items.__contains__(item) - def __eq__ (self, other): return self._items.__eq__(other) - def __str__ (self): return self._items.__str__() - def __repr__ (self): return self._items.__repr__() - - - def append(self, item): - self._validate(item) - - log.debug('Item appended: {}'.format(item)) - self._items.append(item) - self._notify() - - - def pop(self, index=-1): - - item = self._items.pop(index) - log.debug('Item popped: {} (index {})'.format(item, index)) - self._notify() - return item - - - def insert(self, index, item): - self._validate(item) - self._items.insert(index, item) - log.debug('Item inserted: {} (index {})'.format(item, index)) - self._notify() - - - def extend(self, items): - map(self._validate, items) - self._items.extend(items) - log.debug('List extended: {}'.format(', '.join([str(i) for i in item]))) - self._notify() - - - def move(self, from_, to): - """ - Move the item from 'from_' to 'to'. - """ - - item = self._items.pop(from_) - self._items.insert(to, item) - log.debug('Item moved: {} (from: {} to: {})'.format(item, from_, to)) - self._notify() - - - def addListener (self, listener): self._listeners.append(listener) - def removeListener(self, listener): self._listeners.remove(listener) - def _notify (self): - for listener in self._listeners: - try: - listener(self) - except e: - log.debug('Listener raised exception: {}'.format(e.message)) - - -class TestNotifyList(unittest.TestCase): - - def setUp(self): - self.listlen = 5 - self.thelist = NotifyList(range(self.listlen)) - - def test_move(self): - - for i in range(self.listlen): - for j in range(self.listlen): - - self.setUp() - self.thelist.move(i, j) - - demo = range(self.listlen) - - val = demo.pop(i) - demo.insert(j, val) - - print '{} -> {}: {} <-> {}'.format(i, j, self.thelist, demo) - - self.assertEqual(self.thelist, demo)