From 10712f1725fdef756f20eb4bf1e175ee9ae3c452 Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Tue, 29 Apr 2014 11:32:43 +0100
Subject: [PATCH] New module, fsl.gui. for generic gui stuff. Rolled my own
 Notebook widget, as none of the wx ones were any good. Still having
 auto-layout problems.

---
 fsl/fslview/imagelistpanel.py  |   2 +-
 fsl/gui/__init__.py            |   0
 fsl/{utils => gui}/elistbox.py |   0
 fsl/gui/notebook.py            | 291 +++++++++++++++++++++++++++++++++
 fsl/props/build.py             |  47 ++----
 fsl/utils/notifylist.py        | 116 -------------
 6 files changed, 310 insertions(+), 146 deletions(-)
 create mode 100644 fsl/gui/__init__.py
 rename fsl/{utils => gui}/elistbox.py (100%)
 create mode 100644 fsl/gui/notebook.py
 delete mode 100644 fsl/utils/notifylist.py

diff --git a/fsl/fslview/imagelistpanel.py b/fsl/fslview/imagelistpanel.py
index 1c17a1845..389bd0083 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 000000000..e69de29bb
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 000000000..6c02a0360
--- /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 935f2dc17..9f80f4c50 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 9a7340f54..000000000
--- 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)
-- 
GitLab