diff --git a/qt/applications/workbench/workbench/plotting/config.py b/qt/applications/workbench/workbench/plotting/config.py
index 88f416704d1b9906b94cd926fb2bf64c521a2c9e..06bf20cf73fbc8db8cef9b248efdf50b37b539ac 100644
--- a/qt/applications/workbench/workbench/plotting/config.py
+++ b/qt/applications/workbench/workbench/plotting/config.py
@@ -30,7 +30,8 @@ MPL_BACKEND = 'module://workbench.plotting.backend_workbench'
 
 # Our style defaults
 DEFAULT_RCPARAMS = {
-    'figure.facecolor':  'w'
+    'figure.facecolor':  'w',
+    'figure.max_open_warning': 200
 }
 
 
diff --git a/qt/applications/workbench/workbench/plotting/figuremanager.py b/qt/applications/workbench/workbench/plotting/figuremanager.py
index b31511039a157ecaa36d07f467b82b39770eaea4..21e58e85ea8947692a0da3c17021846c19b104d1 100644
--- a/qt/applications/workbench/workbench/plotting/figuremanager.py
+++ b/qt/applications/workbench/workbench/plotting/figuremanager.py
@@ -16,82 +16,25 @@
 #  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 """Provides our custom figure manager to wrap the canvas, window and our custom toolbar"""
 
-# std imports
-import sys
-
 # 3rdparty imports
 import matplotlib
 from matplotlib.backend_bases import FigureManagerBase
 from matplotlib.backends.backend_qt5agg import (FigureCanvasQTAgg, backend_version, draw_if_interactive, show)  # noqa
 from matplotlib._pylab_helpers import Gcf
-from qtpy.QtCore import Qt, QEvent, QMetaObject, QObject, QThread, Signal, Slot
+from qtpy.QtCore import Qt, QEvent, QObject, Signal
 from qtpy.QtWidgets import QApplication, QLabel, QMainWindow
-from six import reraise, text_type
+from six import text_type
 
 # local imports
-from workbench.plotting.propertiesdialog import LabelEditor, XAxisEditor, YAxisEditor
-from workbench.plotting.toolbar import WorkbenchNavigationToolbar
-
-
-qApp = QApplication.instance()
-
-
-class QAppThreadCall(QObject):
-    """
-    Wraps a callable object and forces any calls made to it to be executed
-    on the same thread as the qApp object.
-    """
-
-    def __init__(self, callee):
-        QObject.__init__(self)
-        self.moveToThread(qApp.thread())
-        self.callee = callee
-        # Help should then give the correct doc
-        self.__call__.__func__.__doc__ = callee.__doc__
-        self._args = None
-        self._kwargs = None
-        self._result = None
-        self._exc_info = None
-
-    def __call__(self, *args, **kwargs):
-        """
-        If the current thread is the qApp thread then this
-        performs a straight call to the wrapped callable_obj. Otherwise
-        it invokes the do_call method as a slot via a
-        BlockingQueuedConnection.
-        """
-        if QThread.currentThread() == qApp.thread():
-            return self.callee(*args, **kwargs)
-        else:
-            self._store_function_args(*args, **kwargs)
-            QMetaObject.invokeMethod(self, "on_call",
-                                     Qt.BlockingQueuedConnection)
-            if self._exc_info is not None:
-                reraise(*self._exc_info)
-            return self._result
-
-    @Slot()
-    def on_call(self):
-        """Perform a call to a GUI function across a
-        thread and return the result
-        """
-        try:
-            self._result = \
-                self.callee(*self._args, **self._kwargs)
-        except Exception: # pylint: disable=broad-except
-            self._exc_info = sys.exc_info()
-
-    def _store_function_args(self, *args, **kwargs):
-        self._args = args
-        self._kwargs = kwargs
-        # Reset return value and exception
-        self._result = None
-        self._exc_info = None
+from .propertiesdialog import LabelEditor, XAxisEditor, YAxisEditor
+from .toolbar import WorkbenchNavigationToolbar
+from .qappthreadcall import QAppThreadCall
 
 
 class MainWindow(QMainWindow):
     activated = Signal()
     closing = Signal()
+    visibility_changed = Signal()
 
     def event(self, event):
         if event.type() == QEvent.WindowActivate:
@@ -102,6 +45,14 @@ class MainWindow(QMainWindow):
         self.closing.emit()
         QMainWindow.closeEvent(self, event)
 
+    def hideEvent(self, event):
+        self.visibility_changed.emit()
+        QMainWindow.hideEvent(self, event)
+
+    def showEvent(self, event):
+        self.visibility_changed.emit()
+        QMainWindow.showEvent(self, event)
+
 
 class FigureManagerWorkbench(FigureManagerBase, QObject):
     """
@@ -126,14 +77,24 @@ class FigureManagerWorkbench(FigureManagerBase, QObject):
         self.destroy = QAppThreadCall(self._destroy_orig)
         self._show_orig = self.show
         self.show = QAppThreadCall(self._show_orig)
+        self._window_activated_orig = self._window_activated
+        self._window_activated = QAppThreadCall(self._window_activated_orig)
+        self._widgetclosed_orig = self._widgetclosed
+        self._widgetclosed = QAppThreadCall(self._widgetclosed_orig)
+        self.set_window_title_orig = self.set_window_title
+        self.set_window_title = QAppThreadCall(self.set_window_title_orig)
+        self.fig_visibility_changed_orig = self.fig_visibility_changed
+        self.fig_visibility_changed = QAppThreadCall(self.fig_visibility_changed_orig)
 
         self.canvas = canvas
         self.window = MainWindow()
         self.window.activated.connect(self._window_activated)
         self.window.closing.connect(canvas.close_event)
         self.window.closing.connect(self._widgetclosed)
+        self.window.visibility_changed.connect(self.fig_visibility_changed)
 
         self.window.setWindowTitle("Figure %d" % num)
+        self.canvas.figure.set_label("Figure %d" % num)
 
         # Give the keyboard focus to the figure instead of the
         # manager; StrongFocus accepts both tab and click to focus and
@@ -219,6 +180,16 @@ class FigureManagerWorkbench(FigureManagerBase, QObject):
         self.window.show()
         self.window.activateWindow()
         self.window.raise_()
+        if self.window.windowState() & Qt.WindowMinimized:
+            # windowState() stores a combination of window state enums
+            # and multiple window states can be valid. On Windows
+            # a window can be both minimized and maximized at the
+            # same time, so we make a check here. For more info see:
+            # http://doc.qt.io/qt-5/qt.html#WindowState-enum
+            if self.window.windowState() & Qt.WindowMaximized:
+                self.window.setWindowState(Qt.WindowMaximized)
+            else:
+                self.window.setWindowState(Qt.WindowNoState)
 
         # Hack to ensure the canvas is up to date
         self.canvas.draw_idle()
@@ -256,6 +227,22 @@ class FigureManagerWorkbench(FigureManagerBase, QObject):
 
     def set_window_title(self, title):
         self.window.setWindowTitle(title)
+        # We need to add a call to the figure manager here to call
+        # notify methods when a figure is renamed, to update our
+        # plot list.
+        Gcf.figure_title_changed(self.num)
+
+        # For the workbench we also keep the label in sync, this is
+        # to allow getting a handle as plt.figure('Figure Name')
+        self.canvas.figure.set_label(title)
+
+    def fig_visibility_changed(self):
+        """
+        Make a notification in the global figure manager that
+        plot visibility was changed. This method is added to this
+        class so that it can be wrapped in a QAppThreadCall.
+        """
+        Gcf.figure_visibility_changed(self.num)
 
     # ------------------------ Interaction events --------------------
     def on_button_press(self, event):
diff --git a/qt/applications/workbench/workbench/plotting/functions.py b/qt/applications/workbench/workbench/plotting/functions.py
index 417f47c18c00d4c031df06bf9847f27a86c381e7..9795fc0cd9737abcf20854de94033afd3900cd2d 100644
--- a/qt/applications/workbench/workbench/plotting/functions.py
+++ b/qt/applications/workbench/workbench/plotting/functions.py
@@ -139,9 +139,10 @@ def plot(workspaces, spectrum_nums=None, wksp_indices=None, errors=False,
             plot_fn(ws, **{kw: num})
 
     ax.legend()
-    title = workspaces[0].name()
-    ax.set_title(title)
-    fig.canvas.set_window_title(figure_title(workspaces, fig.number))
+    if not overplot:
+        title = workspaces[0].name()
+        ax.set_title(title)
+        fig.canvas.set_window_title(figure_title(workspaces, fig.number))
     fig.canvas.draw()
     fig.show()
     return fig
diff --git a/qt/applications/workbench/workbench/plotting/globalfiguremanager.py b/qt/applications/workbench/workbench/plotting/globalfiguremanager.py
index 80df938f66ea13df76dfa460b423e2b1f38dfdc6..34fc66be65e1ff52087fb03b88c5fb63209a7908 100644
--- a/qt/applications/workbench/workbench/plotting/globalfiguremanager.py
+++ b/qt/applications/workbench/workbench/plotting/globalfiguremanager.py
@@ -23,6 +23,41 @@ import gc
 # 3rdparty imports
 import six
 
+from mantidqt.py3compat import Enum
+from .observabledictionary import DictionaryAction, ObservableDictionary
+
+
+class FigureAction(Enum):
+    Unknown = 0
+    New = 1
+    Closed = 2
+    Renamed = 3
+    OrderChanged = 4
+    VisibilityChanged = 5
+
+
+class GlobalFigureManagerObserver(object):
+    def notify(self, action, key):
+        """
+        This method is called when a dictionary entry is added,
+        removed or changed
+        :param action: An enum with the type of dictionary action
+        :param key: The key in the dictionary that was changed
+        :param old_value: Old value(s) removed
+        """
+        gcf = GlobalFigureManager
+
+        if action == DictionaryAction.Create:
+            gcf.notify_observers(FigureAction.New, key)
+        elif action == DictionaryAction.Set:
+            gcf.notify_observers(FigureAction.Renamed, key)
+        elif action == DictionaryAction.Removed:
+            gcf.notify_observers(FigureAction.Closed, key)
+        else:
+            # Not expecting clear or update to be used, so we are
+            # being lazy here and just updating the entire plot list
+            gcf.notify_observers(FigureAction.Unknown, key)
+
 
 class GlobalFigureManager(object):
     """
@@ -44,7 +79,8 @@ class GlobalFigureManager(object):
 
     """
     _activeQue = []
-    figs = {}
+    figs = ObservableDictionary({})
+    figs.add_observer(GlobalFigureManagerObserver())
     observers = []
 
     @classmethod
@@ -82,7 +118,7 @@ class GlobalFigureManager(object):
         del cls.figs[num]
         manager.destroy()
         gc.collect(1)
-        cls.notify_observers()
+        cls.notify_observers(FigureAction.OrderChanged, -1)
 
     @classmethod
     def destroy_fig(cls, fig):
@@ -151,7 +187,7 @@ class GlobalFigureManager(object):
                 cls._activeQue.append(m)
         cls._activeQue.append(manager)
         cls.figs[manager.num] = manager
-        cls.notify_observers()
+        cls.notify_observers(FigureAction.OrderChanged, manager.num)
 
     @classmethod
     def draw_all(cls, force=False):
@@ -166,30 +202,22 @@ class GlobalFigureManager(object):
     # ------------------ Our additional interface -----------------
 
     @classmethod
-    def get_figure_number_from_name(cls, figure_title):
+    def last_active_values(cls):
         """
-        Returns the figure number corresponding to the figure title
-        passed in as a string
-        :param figure_title: A String containing the figure title
-        :return: The figure number (int)
+        Returns a dictionary where the keys are the plot numbers and
+        the values are the last shown (active) order, the most recent
+        being 1, the oldest being N, where N is the number of figure
+        managers
+        :return: A dictionary with the values as plot number and keys
+                 as the opening order
         """
-        for num, figure_manager in cls.figs.items():
-            if figure_manager.get_window_title() == figure_title:
-                return num
-        return None
+        last_shown_order_dict = {}
+        num_figure_managers = len(cls._activeQue)
 
-    @classmethod
-    def get_figure_manager_from_name(cls, figure_title):
-        """
-        Returns the figure manager corresponding to the figure title
-        passed in as a string
-        :param figure_title: A String containing the figure title
-        :return: The figure manager
-        """
-        for figure_manager in cls.figs.values():
-            if figure_manager.get_window_title() == figure_title:
-                return figure_manager
-        return None
+        for index in range(num_figure_managers):
+            last_shown_order_dict[cls._activeQue[index].num] = num_figure_managers - index
+
+        return last_shown_order_dict
 
     # ---------------------- Observer methods ---------------------
     # This is currently very simple as the only observer is
@@ -205,11 +233,30 @@ class GlobalFigureManager(object):
         cls.observers.append(observer)
 
     @classmethod
-    def notify_observers(cls):
+    def notify_observers(cls, action, figure_number):
         """
         Calls notify method on all observers
+        :param action: A FigureAction enum for the action called
+        :param figure_number: The unique fig number (key in the dict)
         """
         for observer in cls.observers:
-            observer.notify()
+            observer.notify(action, figure_number)
+
+    @classmethod
+    def figure_title_changed(cls, figure_number):
+        """
+        Notify the observers that a figure title was changed
+        :param figure_number: The unique number in GlobalFigureManager
+        """
+        cls.notify_observers(FigureAction.Renamed, figure_number)
+
+    @classmethod
+    def figure_visibility_changed(cls, figure_number):
+        """
+        Notify the observers that a figure was shown or hidden
+        :param figure_number: The unique number in GlobalFigureManager
+        """
+        cls.notify_observers(FigureAction.VisibilityChanged, figure_number)
+
 
 atexit.register(GlobalFigureManager.destroy_all)
diff --git a/qt/applications/workbench/workbench/plotting/observabledictionary.py b/qt/applications/workbench/workbench/plotting/observabledictionary.py
new file mode 100644
index 0000000000000000000000000000000000000000..dba1c4c6e44039bda42c801cfb4bb217970aa8c3
--- /dev/null
+++ b/qt/applications/workbench/workbench/plotting/observabledictionary.py
@@ -0,0 +1,81 @@
+#  This file is part of the mantid workbench.
+#
+#  Copyright (C) 2017 mantidproject
+#
+#  This program is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+from mantidqt.py3compat import Enum
+
+
+class DictionaryAction(Enum):
+    Create = 0
+    Set = 1
+    Removed = 2
+    Clear = 3
+    Update = 4
+
+
+class ObservableDictionary(dict):
+    """
+    Override parts of dictionary that deal with adding, removing
+    or changing data in the dictionary to call an observer.
+    """
+    def __init__(self, value):
+        super(ObservableDictionary, self).__init__(value)
+        self.observers = []
+
+    def add_observer(self, observer):
+        """
+        Add an observer to this class - this can be any class with a
+        notify() method
+        :param observer: A class with a notify method
+        """
+        self.observers.append(observer)
+
+    def _notify_observers(self, action, key=-1):
+        for observer in self.observers:
+            observer.notify(action, key)
+
+    def __setitem__(self, key, new_value):
+        action = DictionaryAction.Set
+
+        if key not in self:
+            action = DictionaryAction.Create
+        dict.__setitem__(self, key, new_value)
+
+        self._notify_observers(action, key)
+
+    def __delitem__(self, key):
+        dict.__delitem__(self, key)
+        self._notify_observers(DictionaryAction.Removed, key)
+
+    def clear(self):
+        dict.clear(self)
+        self._notify_observers(DictionaryAction.Clear)
+
+    def pop(self, key, default=None):
+        if key in self:
+            old_value = dict.pop(self, key)
+            self._notify_observers(DictionaryAction.Removed, key)
+            return old_value
+        else:
+            return dict.pop(self, key, default)
+
+    def popitem(self):
+        key, old_value = dict.popitem(self)
+        self._notify_observers(DictionaryAction.Removed, old_value=old_value)
+        return key, old_value
+
+    def update(self, updated_dictionary):
+        dict.update(self, updated_dictionary)
+        self._notify_observers(DictionaryAction.Update)
diff --git a/qt/applications/workbench/workbench/plotting/qappthreadcall.py b/qt/applications/workbench/workbench/plotting/qappthreadcall.py
new file mode 100644
index 0000000000000000000000000000000000000000..434a94c147625018d6d50226e804bd5796741d04
--- /dev/null
+++ b/qt/applications/workbench/workbench/plotting/qappthreadcall.py
@@ -0,0 +1,79 @@
+#  This file is part of the mantid workbench.
+#
+#  Copyright (C) 2018 mantidproject
+#
+#  This program is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import sys
+
+from qtpy.QtCore import Qt, QMetaObject, QObject, QThread, Slot
+from qtpy.QtWidgets import QApplication
+
+from six import reraise
+
+
+class QAppThreadCall(QObject):
+    """
+    Wraps a callable object and forces any calls made to it to be executed
+    on the same thread as the qApp object. This is required for anything
+    called by the matplotlib figures, which run on a separate thread.
+    """
+
+    def __init__(self, callee):
+        self.qApp = QApplication.instance()
+
+        QObject.__init__(self)
+        self.moveToThread(self.qApp.thread())
+        self.callee = callee
+        # Help should then give the correct doc
+        self.__call__.__func__.__doc__ = callee.__doc__
+        self._args = None
+        self._kwargs = None
+        self._result = None
+        self._exc_info = None
+
+    def __call__(self, *args, **kwargs):
+        """
+        If the current thread is the qApp thread then this
+        performs a straight call to the wrapped callable_obj. Otherwise
+        it invokes the do_call method as a slot via a
+        BlockingQueuedConnection.
+        """
+        if QThread.currentThread() == self.qApp.thread():
+            return self.callee(*args, **kwargs)
+        else:
+            self._store_function_args(*args, **kwargs)
+            QMetaObject.invokeMethod(self, "on_call",
+                                     Qt.BlockingQueuedConnection)
+            if self._exc_info is not None:
+                reraise(*self._exc_info)
+            return self._result
+
+    @Slot()
+    def on_call(self):
+        """Perform a call to a GUI function across a
+        thread and return the result
+        """
+        try:
+            self._result = \
+                self.callee(*self._args, **self._kwargs)
+        except Exception: # pylint: disable=broad-except
+            self._exc_info = sys.exc_info()
+
+    def _store_function_args(self, *args, **kwargs):
+        self._args = args
+        self._kwargs = kwargs
+        # Reset return value and exception
+        self._result = None
+        self._exc_info = None
diff --git a/qt/applications/workbench/workbench/plugins/plotselectorwidget.py b/qt/applications/workbench/workbench/plugins/plotselectorwidget.py
index 8e4dca07527c223434bb9d604616eecd56f4dbdf..916bd0f358774ee146add72cc67bce60d52ef611 100644
--- a/qt/applications/workbench/workbench/plugins/plotselectorwidget.py
+++ b/qt/applications/workbench/workbench/plugins/plotselectorwidget.py
@@ -36,7 +36,7 @@ class PlotSelector(PluginWidget):
         plot_selector_presenter = PlotSelectorPresenter(GlobalFigureManager)
 
         # layout
-        self.plot_selector_widget = plot_selector_presenter.widget
+        self.plot_selector_widget = plot_selector_presenter.view
         layout = QVBoxLayout()
         layout.addWidget(self.plot_selector_widget)
         self.setLayout(layout)
diff --git a/qt/applications/workbench/workbench/widgets/plotselector/model.py b/qt/applications/workbench/workbench/widgets/plotselector/model.py
index b1dee4bdc338f398f265c680d8e5bcbe1a0bd525..3e6a5a829e9112ac7883938810682ffca6d4b2a2 100644
--- a/qt/applications/workbench/workbench/widgets/plotselector/model.py
+++ b/qt/applications/workbench/workbench/widgets/plotselector/model.py
@@ -16,6 +16,8 @@
 #  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 from __future__ import absolute_import, print_function
 
+from workbench.plotting.globalfiguremanager import FigureAction
+
 
 class PlotSelectorModel(object):
     """
@@ -32,49 +34,160 @@ class PlotSelectorModel(object):
         """
         self.GlobalFigureManager = global_figure_manager
         self.presenter = presenter
-        self.plot_list = []
 
         # Register with CurrentFigure that we want to know of any
         # changes to the list of plots
         self.GlobalFigureManager.add_observer(self)
 
-    def update_plot_list(self):
+    def get_plot_name_from_number(self, plot_number):
+        figure_manager = self.GlobalFigureManager.figs.get(plot_number)
+        if figure_manager is None:
+            return ''
+        else:
+            return figure_manager.get_window_title()
+
+    # ------------------------ Plot Updates ------------------------
+
+    def get_plot_list(self):
         """
-        Update the list of plots that is stored in this class, by
-        getting the list from the GlobalFigureManager
+        Returns a dictionary with the list of plots in the
+        GlobalFigureManager, with figure number as the keys and the
+        plot name as the values
+        :return: A dictionary with figure numbers as keys, and plot
+                 names as values
         """
-        self.plot_list = []
+        plot_dict = {}
         figures = self.GlobalFigureManager.get_all_fig_managers()
         for figure in figures:
-            self.plot_list.append(figure.get_window_title())
+            self.plot_dict[figure.num] = figure.get_window_title()
+        return plot_dict
 
-    def notify(self):
+    def notify(self, action, plot_number):
         """
         This is called by GlobalFigureManager when plots are created
-        or destroyed. This calls the presenter to update the plot
-        list in the model and the view.
-        :return:
+        or destroyed, renamed or the active order is changed. This
+        calls the presenter to update the plot list in the model and
+        the view.
+
+        IMPORTANT: Anything called here is not called from the main
+        GUI thread. Changes in the view must be wrapped in a
+        QAppThreadCall!
+
+        :param action: A FigureAction corresponding to the event
+        :param plot_number: The unique number in GlobalFigureManager
         """
-        self.presenter.update_plot_list()
+        if action == FigureAction.New:
+            self.presenter.append_to_plot_list(plot_number)
+        if action == FigureAction.Closed:
+            self.presenter.remove_from_plot_list(plot_number)
+        if action == FigureAction.Renamed:
+            figure_manager = self.GlobalFigureManager.figs.get(plot_number)
+            # This can be triggered before the plot is added to the
+            # GlobalFigureManager, so we silently ignore this case
+            if figure_manager is not None:
+                self.presenter.rename_in_plot_list(plot_number, figure_manager.get_window_title())
+        if action == FigureAction.OrderChanged:
+            self.presenter.update_last_active_order()
+            if plot_number >= 0:
+                self.presenter.set_active_font(plot_number)
+        if action == FigureAction.VisibilityChanged:
+            self.presenter.update_visibility_icon(plot_number)
+        if action == FigureAction.Unknown:
+            self.presenter.update_plot_list()
 
-    def make_plot_active(self, plot_name):
+    # ------------------------ Plot Showing ------------------------
+
+    def show_plot(self, plot_number):
         """
         For a given plot name make this plot active - bring it to the
         front and make it the destination for overplotting
-        :param plot_name: A string with the name of the plot
-                          (figure title)
+        :param plot_number: The unique number in GlobalFigureManager
+        """
+        figure_manager = self.GlobalFigureManager.figs.get(plot_number)
+        if figure_manager is None:
+            raise ValueError('Error showing, could not find a plot with the number {}.'.format(plot_number))
+        figure_manager.show()
+
+    # ------------------------ Plot Hiding -------------------------
+
+    def is_visible(self, plot_number):
+        """
+        Determines if plot window is visible or hidden
+        :return: True if plot visible (window open), false if hidden
+        """
+        figure_manager = self.GlobalFigureManager.figs.get(plot_number)
+        if figure_manager is None:
+            raise ValueError('Error in is_visible, could not find a plot with the number {}.'.format(plot_number))
+
+        return figure_manager.window.isVisible()
+
+    def hide_plot(self, plot_number):
+        """
+        Hide a plot by calling .window.hide() on the figure manager
+        :param plot_number: The unique number in GlobalFigureManager
+        """
+        figure_manager = self.GlobalFigureManager.figs.get(plot_number)
+        if figure_manager is None:
+            raise ValueError('Error hiding, could not find a plot with the number {}.'.format(plot_number))
+        figure_manager.window.hide()
+
+    # ------------------------ Plot Renaming ------------------------
+
+    def rename_figure(self, plot_number, new_name):
         """
-        figure_manager = self.GlobalFigureManager.get_figure_manager_from_name(plot_name)
-        if figure_manager is not None:
-            figure_manager.show()
+        Renames a figure in the GlobalFigureManager
+        :param plot_number: The unique number in GlobalFigureManager
+        :param new_name: The new figure (plot) name
+        """
+        figure_manager = self.GlobalFigureManager.figs.get(plot_number)
+        if figure_manager is None:
+            raise ValueError('Error renaming, could not find a plot with the number {}.'.format(plot_number))
+
+        figure_manager.set_window_title(new_name)
 
-    def close_plot(self, plot_name):
+    # ------------------------ Plot Closing -------------------------
+
+    def close_plot(self, plot_number):
         """
         For a given plot close and remove all reference in the
         GlobalFigureManager
-        :param plot_name: A string with the name of the plot
-                          (figure title)
+        :param plot_number: The unique number in GlobalFigureManager
+        """
+        figure_manager = self.GlobalFigureManager.figs.get(plot_number)
+        if figure_manager is None:
+            raise ValueError('Error closing, could not find a plot with the number {}.'.format(plot_number))
+
+        self.GlobalFigureManager.destroy(plot_number)
+
+    # ----------------------- Plot Sorting --------------------------
+
+    def last_active_values(self):
+        """
+        Returns a dictionary containing the order of the last shown
+        plots. Not all plots are guaranteed to be in returned
+        dictionary.
+        :return: A dictionary containing the plot numbers as keys,
+                 and the order (1...N) as values
+                 (e.g. {1: 2, 2: 1, 7: 3})
+        """
+        return self.GlobalFigureManager.last_active_values()
+
+    # ---------------------- Plot Exporting -------------------------
+
+    def export_plot(self, plot_number, save_absolute_path):
+        """
+        Export a plot, with the type based on the the filename
+        extension
+        :param plot_number: The unique number in GlobalFigureManager
+        :param save_absolute_path: The absolute path, with the
+                                   extension giving the type
+        :return:
         """
-        figure_number_to_close = self.GlobalFigureManager.get_figure_number_from_name(plot_name)
-        if figure_number_to_close is not None:
-            self.GlobalFigureManager.destroy(figure_number_to_close)
+        figure_manager = self.GlobalFigureManager.figs.get(plot_number)
+        if figure_manager is None:
+            raise ValueError('Error exporting, could not find a plot with the number {}.'.format(plot_number))
+
+        try:
+            figure_manager.canvas.figure.savefig(save_absolute_path)
+        except IOError as e:
+            raise ValueError("Error, could not save plot to {}.\n\nError was: {}".format(save_absolute_path, e))
diff --git a/qt/applications/workbench/workbench/widgets/plotselector/presenter.py b/qt/applications/workbench/workbench/widgets/plotselector/presenter.py
index 09da4e8b11a316a601fffbafa2b4e9cbb913337b..8d80535928d1818566f6dee9b8c1b3903700adba 100644
--- a/qt/applications/workbench/workbench/widgets/plotselector/presenter.py
+++ b/qt/applications/workbench/workbench/widgets/plotselector/presenter.py
@@ -16,8 +16,11 @@
 #  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 from __future__ import absolute_import, print_function
 
+import os
+import re
+
 from .model import PlotSelectorModel
-from .view import PlotSelectorView
+from .view import PlotSelectorView, Column
 
 
 class PlotSelectorPresenter(object):
@@ -39,9 +42,9 @@ class PlotSelectorPresenter(object):
         """
         # Create model and view, or accept mocked versions
         if view is None:
-            self.widget = PlotSelectorView(self)
+            self.view = PlotSelectorView(self)
         else:
-            self.widget = view
+            self.view = view
         if model is None:
             self.model = PlotSelectorModel(self, global_figure_manager)
         else:
@@ -50,17 +53,169 @@ class PlotSelectorPresenter(object):
         # Make sure the plot list is up to date
         self.update_plot_list()
 
+    def get_plot_name_from_number(self, plot_number):
+        return self.model.get_plot_name_from_number(plot_number)
+
+    # ------------------------ Plot Updates ------------------------
+
     def update_plot_list(self):
         """
         Updates the plot list in the model and the view. Filter text
         is applied to the updated selection if required.
         """
-        self.model.update_plot_list()
-        filter_text = self.widget.get_filter_text()
-        if not filter_text:
-            self.widget.set_plot_list(self.model.plot_list)
+        plot_list = self.model.get_plot_list()
+        self.view.set_plot_list(plot_list)
+
+    def append_to_plot_list(self, plot_number):
+        """
+        Appends the plot name to the end of the plot list
+        :param plot_number: The unique number in GlobalFigureManager
+        """
+        self.view.append_to_plot_list(plot_number)
+        self.view.set_visibility_icon(plot_number, self.model.is_visible(plot_number))
+
+    def remove_from_plot_list(self, plot_number):
+        """
+        Removes the plot name from the plot list
+        :param plot_number: The unique number in GlobalFigureManager
+        """
+        self.view.remove_from_plot_list(plot_number)
+
+    def rename_in_plot_list(self, plot_number, new_name):
+        """
+        Replaces a name in the plot list
+        :param plot_number: The unique number in GlobalFigureManager
+        :param new_name: The new name for the plot
+        """
+        self.view.rename_in_plot_list(plot_number, new_name)
+
+    # ----------------------- Plot Filtering ------------------------
+
+    def filter_text_changed(self):
+        """
+        Called by the view when the filter text is changed (e.g. by
+        typing or clearing the text)
+        """
+        if self.view.get_filter_text():
+            self.view.filter_plot_list()
+        else:
+            self.view.unhide_all_plots()
+
+    def is_shown_by_filter(self, plot_number):
+        """
+        :param plot_number: The unique number in GlobalFigureManager
+        :return: True if shown, or False if filtered out
+        """
+        filter_text = self.view.get_filter_text()
+        plot_name = self.get_plot_name_from_number(plot_number)
+        return filter_text.lower() in plot_name.lower()
+
+    # ------------------------ Plot Showing ------------------------
+
+    def show_single_selected(self):
+        """
+        When a list item is double clicked the view calls this method
+        to bring the selected plot to the front
+        """
+        plot_number = self.view.get_currently_selected_plot_number()
+        self._make_plot_active(plot_number)
+
+    def show_multiple_selected(self):
+        """
+        Shows multiple selected plots, e.g. from pressing the 'Show'
+        button with multiple selected plots
+        """
+        selected_plots = self.view.get_all_selected_plot_numbers()
+        for plot_number in selected_plots:
+            self._make_plot_active(plot_number)
+
+    def _make_plot_active(self, plot_number):
+        """
+        Make the plot with the given name active - bring it to the
+        front and make it the choice for overplotting
+        :param plot_number: The unique number in GlobalFigureManager
+        """
+        try:
+            self.model.show_plot(plot_number)
+        except ValueError as e:
+            print(e)
+
+    def set_active_font(self, plot_number):
+        """
+        Set the icon for the active plot to be colored
+        :param plot_number: The unique number in GlobalFigureManager
+        """
+        active_plot_number = self.view.active_plot_number
+        if active_plot_number > 0:
+            try:
+                self.view.set_active_font(active_plot_number, False)
+            except TypeError:
+                pass
+                # The last active plot could have been closed
+                # already, so there is nothing to do
+        self.view.set_active_font(plot_number, True)
+        self.view.active_plot_number = plot_number
+
+    # ------------------------ Plot Hiding -------------------------
+
+    def hide_selected_plots(self):
+        """
+        Hide all plots that are selected in the view
+        """
+        selected_plots = self.view.get_all_selected_plot_numbers()
+
+        for plot_number in selected_plots:
+            self._hide_plot(plot_number)
+
+    def _hide_plot(self, plot_number):
+        """
+        Hides a single plot
+        """
+        try:
+            self.model.hide_plot(plot_number)
+        except ValueError as e:
+            print(e)
+
+    def toggle_plot_visibility(self, plot_number):
+        """
+        Toggles a plot between hidden and shown
+        :param plot_number: The unique number in GlobalFigureManager
+        """
+        if self.model.is_visible(plot_number):
+            self._hide_plot(plot_number)
         else:
-            self._filter_plot_list_by_string(filter_text)
+            self._make_plot_active(plot_number)
+
+        self.update_visibility_icon(plot_number)
+
+    def update_visibility_icon(self, plot_number):
+        """
+        Updates the icon to indicate a plot as hidden or visible
+        :param plot_number: The unique number in GlobalFigureManager
+        """
+        try:
+            is_visible = self.model.is_visible(plot_number)
+            self.view.set_visibility_icon(plot_number, is_visible)
+        except ValueError:
+            # There is a chance the plot was closed, which calls an
+            # update to this method. If we can not get the visibility
+            # status it is safe to assume the plot has been closed.
+            pass
+
+    # ------------------------ Plot Renaming ------------------------
+
+    def rename_figure(self, plot_number, new_name):
+        """
+        Replaces a name in the plot list
+        :param plot_number: The unique number in GlobalFigureManager
+        :param new_name: The new plot name
+         """
+        try:
+            self.model.rename_figure(plot_number, new_name)
+        except ValueError as e:
+            # We need to undo the rename in the view
+            self.view.rename_in_plot_list(plot_number, new_name)
+            print(e)
 
     # ------------------------ Plot Closing -------------------------
 
@@ -69,48 +224,189 @@ class PlotSelectorPresenter(object):
         This is called by the view when closing plots is requested
         (e.g. pressing close or delete).
         """
-        selected_plots = self.widget.get_all_selected_plot_names()
+        selected_plots = self.view.get_all_selected_plot_numbers()
         self._close_plots(selected_plots)
 
-    def _close_plots(self, list_of_plots):
+    def close_single_plot(self, plot_number):
+        """
+        This is used to close plots when a close action is called
+        that does not refer to the selected plot(s)
+        :param plot_number: The unique number in GlobalFigureManager
+        """
+        self._close_plots([plot_number])
+
+    def _close_plots(self, list_of_plot_numbers):
         """
         Accepts a list of plot names to close
         :param list_of_plots: A list of strings containing plot names
         """
-        for plot_name in list_of_plots:
-            self.model.close_plot(plot_name)
+        for plot_number in list_of_plot_numbers:
+            try:
+                self.model.close_plot(plot_number)
+            except ValueError as e:
+                print(e)
 
-    # ----------------------- Plot Filtering ------------------------
+    # ----------------------- Plot Sorting --------------------------
 
-    def filter_text_changed(self):
+    def set_sort_order(self, is_ascending):
         """
-        Called by the view when the filter text is changed (e.g. by
-        typing or clearing the text)
+        Sets the sort order in the view
+        :param is_ascending: If true ascending order, else descending
+        """
+        self.view.set_sort_order(is_ascending)
+
+    def set_sort_type(self, sort_type):
+        """
+        Sets the sort order in the view
+        :param sort_type: A Column enum with the column to sort on
+        """
+        self.view.set_sort_type(sort_type)
+        self.update_last_active_order()
+
+    def update_last_active_order(self):
+        """
+        Update the sort keys in the view. This is only required when
+        changes to the last shown order occur in the model, when
+        renaming the key is set already
+        """
+        if self.view.sort_type() == Column.LastActive:
+            self._set_last_active_order()
+
+    def _set_last_active_order(self):
+        """
+        Set the last shown order in the view. This checks the sorting
+        currently set and then sets the sort keys to the appropriate
+        values
+        """
+        last_active_values = self.model.last_active_values()
+        self.view.set_last_active_values(last_active_values)
+
+    def get_initial_last_active_value(self, plot_number):
         """
-        filter_text = self.widget.get_filter_text()
-        self._filter_plot_list_by_string(filter_text)
+        Gets the initial last active value for a plot just added, in
+        this case it is assumed to not have been shown
+        :param plot_number: The unique number in GlobalFigureManager
+        :return: A string with the last active value
+        """
+        return '_' + self.model.get_plot_name_from_number(plot_number)
 
-    def _filter_plot_list_by_string(self, filter_text):
+    def get_renamed_last_active_value(self, plot_number, old_last_active_value):
         """
-        Given a string to filter on this updates the list of plots
-        in the view or shows all plots if the string is empty
-        :param filter_text: A string containing the filter text
+        Gets the initial last active value for a plot that was
+        renamed. If the plot had a numeric value, i.e. has been shown
+        this is retained, else it is set
+        :param plot_number: The unique number in GlobalFigureManager
+        :param old_last_active_value: The previous last active value
         """
-        if not filter_text:
-            self.widget.set_plot_list(self.model.plot_list)
+        if old_last_active_value.isdigit():
+            return old_last_active_value
         else:
-            filtered_plot_list = []
-            for plot_name in self.model.plot_list:
-                if filter_text.lower() in plot_name.lower():
-                    filtered_plot_list.append(plot_name)
-            self.widget.set_plot_list(filtered_plot_list)
+            return self.get_initial_last_active_value(plot_number)
 
-    # ----------------------- Plot Selection ------------------------
+    # ---------------------- Plot Exporting -------------------------
 
-    def list_double_clicked(self):
+    def export_plots_called(self, extension):
         """
-        When a list item is double clicked the view calls this method
-        to bring the selected plot to the front
+        Export plots called from the view, then a single or multiple
+        plots exported depending on the number currently selected
+        :param extension: The file extension as a string including
+                          a '.', for example '.png' (must be a type
+                          supported by matplotlib)
+        """
+        plot_numbers = self.view.get_all_selected_plot_numbers()
+
+        if len(plot_numbers) == 1:
+            self._export_single_plot(plot_numbers[0], extension)
+        elif len(plot_numbers) > 1:
+            self._export_multiple_plots(plot_numbers, extension)
+
+    def _export_single_plot(self, plot_number, extension):
+        """
+        Called when a single plot is selected to export - prompts for
+        a filename then tries to save the plot
+        :param plot_number: The unique number in GlobalFigureManager
+        :param extension: The file extension as a string including
+                          a '.', for example '.png' (must be a type
+                          supported by matplotlib)
+        """
+        absolute_path = self.view.get_file_name_for_saving(extension)
+
+        if not absolute_path[-4:] == extension:
+            absolute_path += extension
+
+        try:
+            self.model.export_plot(plot_number, absolute_path)
+        except ValueError as e:
+            print(e)
+
+    def _export_multiple_plots(self, plot_numbers, extension):
+        """
+        Export all selected plots in the plot_numbers list, first
+        prompting for a save directory then sanitising plot names to
+        unique, usable file names
+        :param plot_numbers: A list of plot numbers to export
+        :param extension: The file extension as a string including
+                          a '.', for example '.png' (must be a type
+                          supported by matplotlib)
+        """
+        dir_name = self.view.get_directory_name_for_saving()
+
+        # A temporary dictionary holding plot numbers as keys, plot
+        # names as values
+        plots = {}
+
+        for plot_number in plot_numbers:
+            plot_name = self.model.get_plot_name_from_number(plot_number)
+            plot_name = self._replace_special_characters(plot_name)
+            if plot_name in plots.values():
+                plot_name = self._make_unique_name(plot_name, plots)
+            plots[plot_number] = plot_name
+
+            self._export_plot(plot_number, plot_name, dir_name, extension)
+
+    def _replace_special_characters(self, string):
+        """
+        Removes any characters that are not valid in file names
+        across all operating systems ('/' for Linux/Mac), more for
+        Windows
+        :param string: The string to replace characters in
+        :return: The string with special characters replace by '-'
+        """
+        return re.sub(r'[<>:"/|\\?*]', r'-', string)
+
+    def _make_unique_name(self, name, dictionary):
+        """
+        Given a name and a dictionary, make a unique name that does
+        not already exist in the dictionary values by appending
+        ' (1)', ' (2)', ' (3)' etc. to the end of the name
+        :param name: A string with the non-unique name
+        :param dictionary: A dictionary with string values
+        :return : The unique plot name
+        """
+        i = 1
+        while True:
+            plot_name_attempt = name + ' ({})'.format(str(i))
+            if plot_name_attempt not in dictionary.values():
+                break
+            i += 1
+
+        return plot_name_attempt
+
+    def _export_plot(self, plot_number, plot_name, dir_name, extension):
+        """
+        Given a plot number, plot name, directory and extension
+        construct the absolute path name and call the model to save
+        the figure
+        :param plot_number: The unique number in GlobalFigureManager
+        :param plot_name: The name to use for saving
+        :param dir_name: The directory to save to
+        :param extension: The file extension as a string including
+                          a '.', for example '.png' (must be a type
+                          supported by matplotlib)
         """
-        plot_name = self.widget.get_currently_selected_plot_name()
-        self.model.make_plot_active(plot_name)
+        if dir_name:
+            filename = os.path.join(dir_name, plot_name + extension)
+            try:
+                self.model.export_plot(plot_number, filename)
+            except ValueError as e:
+                print(e)
diff --git a/qt/applications/workbench/workbench/widgets/plotselector/test/test_plotselector_model.py b/qt/applications/workbench/workbench/widgets/plotselector/test/test_plotselector_model.py
index 90eb001552edb327d94a50ec77209b16d1453a47..75e2d1221a4f5a596c7a78dc59786710f426b256 100644
--- a/qt/applications/workbench/workbench/widgets/plotselector/test/test_plotselector_model.py
+++ b/qt/applications/workbench/workbench/widgets/plotselector/test/test_plotselector_model.py
@@ -16,6 +16,8 @@
 #  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 from __future__ import absolute_import, division, print_function
 
+from workbench.plotting.globalfiguremanager import FigureAction
+
 from workbench.widgets.plotselector.model import PlotSelectorModel
 from workbench.widgets.plotselector.presenter import PlotSelectorPresenter
 
@@ -28,56 +30,128 @@ except ImportError:
 
 class PlotSelectorModelTest(unittest.TestCase):
 
-    def side_effects_manager(self, plot_name):
-        if plot_name == "Plot1":
+    def side_effect_manager(self, plot_number):
+        if plot_number == 42:
             return self.figure_manager
         return None
 
-    def side_effects_number(self, plot_name):
-        if plot_name == "Plot1":
-            return 42
-        return None
-
     def setUp(self):
         self.presenter = mock.Mock(spec=PlotSelectorPresenter)
 
         self.figure_manager = mock.Mock()
         self.figure_manager.show = mock.Mock()
+        self.figure_manager.get_window_title = mock.Mock(return_value="Plot1")
+        self.figure_manager.is_visible = mock.Mock(return_value=True)
 
         self.global_figure_manager = mock.Mock()
         self.global_figure_manager.add_observer = mock.Mock()
-        self.global_figure_manager.get_figure_manager_from_name = mock.Mock(side_effect=self.side_effects_manager)
-        self.global_figure_manager.get_figure_number_from_name = mock.Mock(side_effect=self.side_effects_number)
+        self.global_figure_manager.figs.get = mock.Mock(side_effect=self.side_effect_manager)
         self.global_figure_manager.destroy = mock.Mock()
 
         self.model = PlotSelectorModel(self.presenter, self.global_figure_manager)
         self.model.plot_list = ["Plot1", "Plot2"]
 
+    # ------------------------ Plot Updates ------------------------
+
     def test_observer_added_during_setup(self):
         self.assertEqual(self.global_figure_manager.add_observer.call_count, 1)
 
-    def test_notify_calls_update_in_presenter(self):
-        self.model.notify()
-        self.assertEqual(self.presenter.update_plot_list.call_count, 1)
-        self.model.notify()
-        self.assertEqual(self.presenter.update_plot_list.call_count, 2)
+    def test_notify_for_new_plot_calls_append_in_presenter(self):
+        self.model.notify(FigureAction.New, 42)
+        self.presenter.append_to_plot_list.assert_called_once_with(42)
+
+    def test_notify_for_closed_plot_calls_removed_in_presenter(self):
+        self.model.notify(FigureAction.Closed, 42)
+        self.presenter.remove_from_plot_list.assert_called_once_with(42)
+
+    def test_notify_for_renamed_plot_calls_rename_in_presenter(self):
+        self.model.notify(FigureAction.Renamed, 42)
+        self.presenter.rename_in_plot_list.assert_called_once_with(42, "Plot1")
+
+    def test_notify_for_renamed_plot_with_invalid_figure_number_does_nothing(self):
+        self.model.notify(FigureAction.Renamed, 0)
+        self.presenter.rename_in_plot_list.assert_not_called()
 
-    def test_make_plot_active_calls_current_figure(self):
-        self.model.make_plot_active("Plot1")
+    def test_notify_for_order_changed_calls_presenter(self):
+        self.model.notify(FigureAction.OrderChanged, 42)
+        self.presenter.update_last_active_order.assert_called_once_with()
+        self.presenter.set_active_font.assert_called_once_with(42)
+
+    def test_notify_for_order_changed_with_invalid_plot_number(self):
+        self.model.notify(FigureAction.OrderChanged, -1)
+        self.presenter.update_last_active_order.assert_called_once_with()
+        self.presenter.set_active_font.assert_not_called()
+
+    def test_notify_visibility_changed_calls_presesnter(self):
+        self.model.notify(FigureAction.VisibilityChanged, 42)
+        self.presenter.update_visibility_icon.assert_called_once_with(42)
+
+    def test_notify_unknwon_updates_plot_list_in_presenter(self):
+        self.model.notify(FigureAction.Unknown, -1)
+        self.presenter.update_plot_list.assert_called_once_with()
+
+    # ------------------------ Plot Showing ------------------------
+
+    def test_show_plot_calls_current_figure(self):
+        self.model.show_plot(42)
         self.assertEqual(self.figure_manager.show.call_count, 1)
 
-    def test_make_plot_active_for_invalid_name_does_nothing(self):
-        self.model.make_plot_active("NotAPlot")
+    def test_show_plot_for_invalid_name_raises_value_error(self):
+        self.assertRaises(ValueError, self.model.show_plot, 0)
         self.figure_manager.show.assert_not_called()
 
-    def test_close_plot_calls_destroy_in_current_figure(self):
-        self.model.close_plot("Plot1")
-        self.global_figure_manager.destroy.assert_called_once_with(42)
+    # ------------------------ Plot Hiding -------------------------
+
+    def test_is_visible_returns_true_for_visible_window(self):
+        self.assertTrue(self.model.is_visible(42))
 
-    def test_close_plot_for_invalid_name_does_noting(self):
-        self.model.close_plot("NotAPlot")
+    def test_is_visible_for_invalid_number_raises_value_error(self):
+        self.assertRaises(ValueError, self.model.is_visible, 0)
+
+    def test_hide_plot_calls_hide_on_the_plot_window(self):
+        self.model.hide_plot(42)
+        self.figure_manager.window.hide.assert_called_once_with()
+
+    def test_hide_plot_for_invalid_name_raises_value_error(self):
+        self.assertRaises(ValueError, self.model.hide_plot, 0)
+        self.figure_manager.window.hide.asser_not_called()
+
+    # ------------------------ Plot Renaming ------------------------
+
+    def test_renaming_calls_set_window_title(self):
+        self.model.rename_figure(42, "NewName")
+        self.figure_manager.set_window_title.assert_called_once_with("NewName")
+
+    def test_renaming_calls_with_invalid_number_raises_value_error(self):
+        self.assertRaises(ValueError, self.model.rename_figure, 0, "NewName")
+        self.figure_manager.set_window_title.assert_not_called()
+
+    # ------------------------ Plot Closing -------------------------
+
+    def test_notify_for_closing_plot_calls_remove_in_presenter(self):
+        self.model.notify(FigureAction.Closed, "Plot1")
+        self.presenter.remove_from_plot_list.assert_called_once_with("Plot1")
+
+    def test_close_plot_for_invalid_number_raises_value_error(self):
+        self.assertRaises(ValueError, self.model.close_plot, 0)
         self.global_figure_manager.destroy.assert_not_called()
 
+    def test_close_plot_calls_destroy_in_global_figure_manager(self):
+        self.model.close_plot(42)
+        self.global_figure_manager.destroy.assert_called_once_with(42)
+
+    # ----------------------- Plot Sorting --------------------------
+
+    def test_last_active_values_calls_global_figure_manager(self):
+        self.model.last_active_values()
+        self.global_figure_manager.last_active_values.assert_called_once_with()
+
+    # ---------------------- Plot Exporting -------------------------
+
+    def test_export_plot_calls_savefig_on_figure(self):
+        self.model.export_plot(42, "/home/Documents/Figure1.pdf")
+        self.figure_manager.canvas.figure.savefig.assert_called_once_with("/home/Documents/Figure1.pdf")
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/qt/applications/workbench/workbench/widgets/plotselector/test/test_plotselector_presenter.py b/qt/applications/workbench/workbench/widgets/plotselector/test/test_plotselector_presenter.py
index aaa41092d18f0311945e2e60580e1a8e46f3c0b9..6c8f161b84ea4c8a51cc87d9897b882b4d1a6179 100644
--- a/qt/applications/workbench/workbench/widgets/plotselector/test/test_plotselector_presenter.py
+++ b/qt/applications/workbench/workbench/widgets/plotselector/test/test_plotselector_presenter.py
@@ -18,7 +18,9 @@ from __future__ import absolute_import, division, print_function
 
 from workbench.widgets.plotselector.model import PlotSelectorModel
 from workbench.widgets.plotselector.presenter import PlotSelectorPresenter
-from workbench.widgets.plotselector.view import PlotSelectorView
+from workbench.widgets.plotselector.view import PlotSelectorView, Column
+
+import os
 
 import unittest
 try:
@@ -29,19 +31,31 @@ except ImportError:
 
 class PlotSelectorPresenterTest(unittest.TestCase):
 
+    def side_effect_plot_name(self, plot_number):
+        if plot_number in [0, 101, 102, 103]:
+            return "Plot1"
+        if plot_number == 1:
+            return "Plot2"
+        if plot_number == 2:
+            return "Plot3"
+        if plot_number == 42:
+            return "Graph99"
+        return None
+
     def setUp(self):
-        self.widget = mock.Mock(spec=PlotSelectorView)
-        self.widget.get_filter_text = mock.Mock(return_value="")
+        self.view = mock.Mock(spec=PlotSelectorView)
+        self.view.get_filter_text = mock.Mock(return_value="")
 
         self.model = mock.Mock(spec=PlotSelectorModel)
-        self.model.configure_mock(plot_list=["Plot1", "Plot2", "Plot3", "Graph99"])
+        self.model.get_plot_list = mock.Mock(return_value=[0, 1, 2, 42])
+        self.model.get_plot_name_from_number = mock.Mock(side_effect=self.side_effect_plot_name)
 
-        self.presenter = PlotSelectorPresenter(None, self.widget, self.model)
-        self.presenter.widget = self.widget
+        self.presenter = PlotSelectorPresenter(None, self.view, self.model)
+        self.presenter.widget = self.view
         self.presenter.model = self.model
 
         # Ignore calls during the setup
-        self.widget.reset_mock()
+        self.view.reset_mock()
         self.model.reset_mock()
 
     def convert_list_to_calls(self, list_to_convert):
@@ -50,77 +64,244 @@ class PlotSelectorPresenterTest(unittest.TestCase):
             call_list.append(mock.call(item))
         return call_list
 
+    # ------------------------ Plot Updates ------------------------
+
     def test_plot_list_update(self):
         self.presenter.update_plot_list()
-        self.assertEqual(self.model.update_plot_list.call_count, 1)
-        self.widget.set_plot_list.assert_called_once_with(["Plot1", "Plot2", "Plot3", "Graph99"])
+        self.assertEqual(self.model.get_plot_list.call_count, 1)
+        self.view.set_plot_list.assert_called_once_with([0, 1, 2, 42])
 
-    def test_plot_list_update_with_filter_set(self):
-        self.widget.get_filter_text = mock.Mock(return_value="Graph99")
-        self.presenter.update_plot_list()
-        self.assertEqual(self.model.update_plot_list.call_count, 1)
-        self.widget.set_plot_list.assert_called_once_with(["Graph99"])
+    def test_append_to_plot_list_calls_update_in_model_and_view(self):
+        self.presenter.append_to_plot_list(42)
+        self.view.append_to_plot_list.assert_called_once_with(42)
+
+    def test_remove_from_plot_list_calls_update_in_model_and_view(self):
+        self.presenter.remove_from_plot_list(42)
+        self.view.remove_from_plot_list.assert_called_once_with(42)
+
+    def test_rename_in_plot_list_calls_update_in_model_and_view(self):
+        self.presenter.rename_in_plot_list(42, "NewName")
+        self.view.rename_in_plot_list.assert_called_once_with(42, "NewName")
+
+    # ----------------------- Plot Filtering ------------------------
+
+    def test_no_filtering_displays_all_plots(self):
+        self.presenter.filter_text_changed()
+        self.view.unhide_all_plots.assert_called_once_with()
+
+    def filtering_calls_filter_on_view(self):
+        self.view.get_filter_text = mock.Mock(return_value="Plot1")
+
+        self.presenter.filter_text_changed()
+        self.view.filter_plot_list.assert_called_once_with("Plot1")
+
+    def test_plots_filtered_on_full_name(self):
+        self.view.get_filter_text = mock.Mock(return_value="Plot1")
+        self.assertTrue(self.presenter.is_shown_by_filter(0))  # Plot1
+        self.assertFalse(self.presenter.is_shown_by_filter(1))  # Plot 2
+
+    def test_plots_filtered_on_substring(self):
+        self.view.get_filter_text = mock.Mock(return_value="lot")
+        self.assertTrue(self.presenter.is_shown_by_filter(0))  # Plot1
+        self.assertFalse(self.presenter.is_shown_by_filter(42))  # Graph99
+
+    def test_filtering_case_invariant(self):
+        self.view.get_filter_text = mock.Mock(return_value="pLOT1")
+        self.assertTrue(self.presenter.is_shown_by_filter(0))  # Plot1
+        self.assertFalse(self.presenter.is_shown_by_filter(1))  # Plot2
+
+    # ------------------------ Plot Showing ------------------------
+
+    def test_show_single_plot_shows_it(self):
+        self.view.get_currently_selected_plot_number = mock.Mock(return_value=1)
+        self.presenter.show_single_selected()
+        self.model.show_plot.assert_called_once_with(1)
+
+    def test_show_multiple_plots_shows_them(self):
+        self.view.get_all_selected_plot_numbers = mock.Mock(return_value=[1, 2])
+        self.presenter.show_multiple_selected()
+        self.assertEqual(self.model.show_plot.mock_calls[0], mock.call(1))
+        self.assertEqual(self.model.show_plot.mock_calls[1], mock.call(2))
+
+    def test_set_active_font_sets_active_font_in_view(self):
+        self.view.active_plot_number = 1
+        self.presenter.set_active_font(2)
+        # 2 calls, one to set the plot number 1 to normal, one to
+        # set plot number 2 to bold
+        self.assertEqual(self.view.set_active_font.mock_calls[0], mock.call(1, False))
+        self.assertEqual(self.view.set_active_font.mock_calls[1], mock.call(2, True))
+        self.assertEqual(self.view.active_plot_number, 2)
+
+    # ------------------------ Plot Hiding -------------------------
+
+    def test_hide_multiple_plots_calls_hide_in_model(self):
+        self.view.get_all_selected_plot_numbers = mock.Mock(return_value=[1, 2])
+        self.presenter.hide_selected_plots()
+        self.assertEquals(self.model.hide_plot.mock_calls[0], mock.call(1))
+        self.assertEquals(self.model.hide_plot.mock_calls[1], mock.call(2))
+
+    def test_toggle_plot_visibility_for_visible_plot(self):
+        self.model.is_visible = mock.Mock(return_value=True)
+        self.presenter.toggle_plot_visibility(42)
+        self.model.hide_plot.assert_called_once_with(42)
+        self.view.set_visibility_icon.assert_called_once_with(42, True)
+
+    def test_toggle_plot_visibility_for_hidden_plot(self):
+        self.model.is_visible = mock.Mock(return_value=False)
+        self.presenter.toggle_plot_visibility(42)
+        self.model.show_plot.assert_called_once_with(42)
+        self.view.set_visibility_icon.assert_called_once_with(42, False)
+
+    # ------------------------ Plot Renaming ------------------------
+
+    def test_rename_figure_calls_rename_in_model(self):
+        self.presenter.rename_figure(0, "NewName")
+        self.model.rename_figure.assert_called_once_with(0, "NewName")
+
+    def test_rename_figure_raising_a_value_error_undoes_rename_in_view(self):
+        self.model.rename_figure.side_effect = ValueError("Some problem")
+        self.presenter.rename_figure(0, "NewName")
+        self.model.rename_figure.assert_called_once_with(0, "NewName")
+        self.view.rename_in_plot_list.assert_called_once_with(0, "NewName")
 
     # ------------------------ Plot Closing -------------------------
 
     def test_close_action_single_plot(self):
-        self.widget.get_all_selected_plot_names = mock.Mock(return_value=["Plot1"])
+        self.view.get_all_selected_plot_numbers = mock.Mock(return_value=[42])
 
         self.presenter.close_action_called()
-        self.model.close_plot.assert_called_once_with("Plot1")
+        self.model.close_plot.assert_called_once_with(42)
 
     def test_close_action_multiple_plots(self):
-        self.widget.get_all_selected_plot_names = mock.Mock(return_value=["Plot1", "Plot3"])
+        self.view.get_all_selected_plot_numbers = mock.Mock(return_value=[42, 43])
 
         self.presenter.close_action_called()
         self.assertEqual(self.model.close_plot.call_count, 2)
-        self.model.close_plot.assert_has_calls(self.convert_list_to_calls(["Plot1", "Plot3"]),
+        self.model.close_plot.assert_has_calls(self.convert_list_to_calls([42, 43]),
                                                any_order=True)
 
+    def test_close_action_with_model_call_raising_value_error(self):
+        self.view.get_all_selected_plot_numbers = mock.Mock(return_value=[42])
+        self.model.close_plot.side_effect = ValueError("Some problem")
+
+        self.presenter.close_action_called()
+        self.model.close_plot.assert_called_once_with(42)
+
     def test_close_action_with_no_plots_open(self):
-        self.widget.get_all_selected_plot_names = mock.Mock(return_value=[])
+        self.view.get_all_selected_plot_numbers = mock.Mock(return_value=[])
         self.model.configure_mock(plot_list=[])
 
         self.presenter.close_action_called()
         self.assertEqual(self.model.close_plot.call_count, 0)
 
     def test_close_action_with_no_plots_selected(self):
-        self.widget.get_all_selected_plot_names = mock.Mock(return_value=[])
+        self.view.get_all_selected_plot_numbers = mock.Mock(return_value=[])
 
         self.presenter.close_action_called()
         self.assertEqual(self.model.close_plot.call_count, 0)
 
-    # ----------------------- Plot Filtering ------------------------
-
-    def test_no_filtering_displays_all_plots(self):
-        self.presenter.filter_text_changed()
-        self.widget.set_plot_list.assert_called_once_with(["Plot1", "Plot2", "Plot3", "Graph99"])
-
-    def test_plots_filtered_on_full_name(self):
-        self.widget.get_filter_text = mock.Mock(return_value="Plot1")
-        self.presenter.filter_text_changed()
-
-        self.widget.set_plot_list.assert_called_once_with(["Plot1"])
-
-    def test_plots_filtered_on_substring(self):
-        self.widget.get_filter_text = mock.Mock(return_value="lot")
-        self.presenter.filter_text_changed()
-
-        self.widget.set_plot_list.assert_called_once_with(["Plot1", "Plot2", "Plot3"])
-
-    def test_filtering_case_invariant(self):
-        self.widget.get_filter_text = mock.Mock(return_value="pLOT1")
-        self.presenter.filter_text_changed()
-
-        self.widget.set_plot_list.assert_called_once_with(["Plot1"])
-
-    # ----------------------- Plot Selection ------------------------
-
-    def test_double_clicking_plot_brings_to_front(self):
-        self.widget.get_currently_selected_plot_name = mock.Mock(return_value="Plot2")
-        self.presenter.list_double_clicked()
-
-        self.model.make_plot_active.assert_called_once_with("Plot2")
+    def test_close_single_plot_called(self):
+        self.presenter.close_single_plot("Plot2")
+        self.model.close_plot.assert_called_once_with("Plot2")
+
+    # ----------------------- Plot Sorting --------------------------
+
+    def test_set_sort_order_to_ascending_calls_view_update(self):
+        self.presenter.set_sort_order(is_ascending=True)
+        self.view.set_sort_order.assert_called_once_with(True)
+
+    def test_set_sort_order_to_descending_calls_view_update(self):
+        self.presenter.set_sort_order(is_ascending=False)
+        self.view.set_sort_order.assert_called_once_with(False)
+
+    def test_set_sort_type_to_name(self):
+        self.view.sort_type = mock.Mock(return_value=Column.Name)
+        self.presenter.set_sort_type(Column.Name)
+        self.view.set_sort_type.assert_called_once_with(Column.Name)
+        self.model.last_active_values.assert_not_called()
+        self.view.set_last_active_values.assert_not_called()
+
+    def test_set_sort_type_to_last_active(self):
+        self.model.last_active_values = mock.Mock(return_value={0: 1, 1: 2})
+        self.view.sort_type = mock.Mock(return_value=Column.LastActive)
+        self.presenter.set_sort_type(Column.LastActive)
+
+        self.view.set_sort_type.assert_called_once_with(Column.LastActive)
+        self.view.set_last_active_values.assert_called_once_with({0: 1,
+                                                                  1: 2})
+
+    def test_set_last_active_values_with_sorting_by_last_active(self):
+        self.model.last_active_values = mock.Mock(return_value={0: 1, 1: 2})
+        self.view.sort_type = mock.Mock(return_value=Column.LastActive)
+        self.presenter.update_last_active_order()
+
+        self.view.set_last_active_values.assert_called_once_with({0: 1,
+                                                                  1: 2})
+
+    def test_set_last_active_values_with_sorting_by_name_does_nothing(self):
+        self.model.last_active_order = mock.Mock(return_value={0: 1, 1: 2})
+        self.view.sort_type = mock.Mock(return_value=Column.Name)
+        self.presenter.update_last_active_order()
+
+        self.model.last_active_order.assert_not_called()
+        self.view.set_last_active_values.assert_not_called()
+
+    def test_get_initial_last_active_value(self):
+        self.assertEqual(self.presenter.get_initial_last_active_value(0), "_Plot1")
+
+    def test_get_renamed_last_active_value_for_numeric_old_value(self):
+        self.assertEqual(self.presenter.get_renamed_last_active_value(1, "23"), "23")
+
+    def test_get_renamed_last_active_value_for_never_shown_value(self):
+        self.assertEqual(self.presenter.get_renamed_last_active_value(0, "_Plot1"), "_Plot1")
+
+    # ---------------------- Plot Exporting -------------------------
+
+    def test_exporting_single_plot_generates_correct_filename(self):
+        self.view.get_all_selected_plot_numbers = mock.Mock(return_value=[0])
+        self.view.get_file_name_for_saving = mock.Mock(return_value='/home/Documents/Plot1')
+        self.presenter.export_plots_called('.xyz')
+        self.model.export_plot.assert_called_once_with(0, '/home/Documents/Plot1.xyz')
+
+    def test_exporting_single_plot_with_extension_given_in_file_name(self):
+        self.view.get_all_selected_plot_numbers = mock.Mock(return_value=[0])
+        self.view.get_file_name_for_saving = mock.Mock(return_value='/home/Documents/Plot1.xyz')
+        self.presenter.export_plots_called('.xyz')
+        self.model.export_plot.assert_called_once_with(0, '/home/Documents/Plot1.xyz')
+
+    def test_exporting_multiple_plots_generates_correct_filename(self):
+        self.view.get_all_selected_plot_numbers = mock.Mock(return_value=[0, 1, 2])
+        self.view.get_directory_name_for_saving = mock.Mock(return_value='/home/Documents')
+        self.presenter.export_plots_called('.xyz')
+        for i in range(len(self.model.export_plot.mock_calls)):
+            self.assertEqual(self.model.export_plot.mock_calls[i],
+                             mock.call(i, os.path.join('/home/Documents', 'Plot{}.xyz'.format(i+1))))
+
+    def test_exporting_multiple_plots_with_repeated_plot_names_generates_unique_names(self):
+        self.view.get_all_selected_plot_numbers = mock.Mock(return_value=[0, 101, 102, 103])
+        self.view.get_directory_name_for_saving = mock.Mock(return_value='/home/Documents')
+        self.presenter.export_plots_called('.xyz')
+
+        self.assertEqual(self.model.export_plot.mock_calls[0],
+                         mock.call(0, os.path.join('/home/Documents', 'Plot1.xyz')))
+
+        for i in range(1, len(self.model.export_plot.mock_calls)):
+            self.assertEqual(self.model.export_plot.mock_calls[i],
+                             mock.call(100 + i, os.path.join('/home/Documents', 'Plot1 ({}).xyz'.format(i))))
+
+    def test_exporting_multiple_plots_with_special_characters_in_file_name(self):
+        for character in '<>:"/|\\?*':
+            self.run_special_character_test(character)
+
+    def run_special_character_test(self, special_character):
+        self.view.get_all_selected_plot_numbers = mock.Mock(return_value=[0, 1])
+        self.model.get_plot_name_from_number = mock.Mock(return_value='Plot' + special_character + '1')
+        self.view.get_directory_name_for_saving = mock.Mock(return_value='/home/Documents')
+        self.presenter.export_plots_called('.xyz')
+        self.assertEqual(self.model.export_plot.mock_calls[0],
+                         mock.call(0, os.path.join('/home/Documents', 'Plot-1.xyz')))
+        self.assertEqual(self.model.export_plot.mock_calls[1],
+                         mock.call(1, os.path.join('/home/Documents', 'Plot-1 (1).xyz')))
 
 
 if __name__ == '__main__':
diff --git a/qt/applications/workbench/workbench/widgets/plotselector/test/test_plotselector_view.py b/qt/applications/workbench/workbench/widgets/plotselector/test/test_plotselector_view.py
index 07036e20a2e5a8bd3226a5fd2a63bc1ac45631e0..fba1be352f09d0e0f51f8fdf0c53dd3f5330b992 100644
--- a/qt/applications/workbench/workbench/widgets/plotselector/test/test_plotselector_view.py
+++ b/qt/applications/workbench/workbench/widgets/plotselector/test/test_plotselector_view.py
@@ -19,10 +19,12 @@ from __future__ import absolute_import, division, print_function
 from qtpy.QtCore import Qt
 from qtpy.QtTest import QTest
 
+import qtawesome as qta
+
 from mantidqt.utils.qt.testing import requires_qapp
 
 from workbench.widgets.plotselector.presenter import PlotSelectorPresenter
-from workbench.widgets.plotselector.view import PlotSelectorView
+from workbench.widgets.plotselector.view import EXPORT_TYPES, PlotSelectorView, Column
 
 import unittest
 try:
@@ -36,96 +38,460 @@ class PlotSelectorWidgetTest(unittest.TestCase):
 
     def setUp(self):
         self.presenter = mock.Mock(spec=PlotSelectorPresenter)
-        self.widget = PlotSelectorView(self.presenter)
+        self.presenter.get_plot_name_from_number = mock.Mock(side_effect=self.se_plot_name)
+        self.presenter.get_initial_last_active_value = mock.Mock(side_effect=self.se_get_initial_last_active_value)
+        self.presenter.is_shown_by_filter = mock.Mock(side_effect=self.se_is_shown_by_filter)
+
+        self.view = PlotSelectorView(self.presenter)
+        self.view.table_widget.setSortingEnabled(False)
+
+    def se_plot_name(self, plot_number):
+        if plot_number == 0:
+            return "Plot1"
+        if plot_number == 1:
+            return "Plot2"
+        if plot_number == 2:
+            return "Plot3"
+        if plot_number == 3:
+            return "Plot4"
+        if plot_number == 19:
+            return "Plot20"
+        if plot_number == 42:
+            return "Graph99"
+        return None
+
+    def se_get_initial_last_active_value(self, plot_number):
+        plot_name = self.se_plot_name(plot_number)
+        return '_' + plot_name
+
+    def se_is_shown_by_filter(self, plot_number):
+        if plot_number == 0:
+            return True
+        return False
+
+    def click_to_select_by_row_number(self, row_number):
+        widget = self.view.table_widget.cellWidget(row_number, Column.Name)
+        QTest.mouseClick(widget, Qt.LeftButton)
+
+    def assert_list_of_plots_is_set_in_widget(self, plot_names):
+        self.assertEqual(len(plot_names), self.view.table_widget.rowCount())
+        for index in range(self.view.table_widget.rowCount()):
+            widget = self.view.table_widget.cellWidget(index, Column.Name)
+            self.assertEqual(widget.line_edit.text(), plot_names[index])
+
+    # ------------------------ Plot Updates ------------------------
 
     def test_setting_plot_names_sets_names_in_list_view(self):
-        plot_names = ["Plot1", "Plot2", "Plot3"]
-        self.widget.set_plot_list(plot_names)
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
 
-        list_model = self.widget.list_view.model()
-        for index in range(list_model.rowCount()):
-            item = list_model.item(index)
-            self.assertEqual(item.data(Qt.DisplayRole), plot_names[index])
+        self.assert_list_of_plots_is_set_in_widget(["Plot1", "Plot2", "Plot3"])
 
     def test_setting_plot_names_to_empty_list(self):
-        plot_names = []
-        self.widget.set_plot_list(plot_names)
+        plot_numbers = []
+        self.view.set_plot_list(plot_numbers)
+        self.assertEqual(self.view.table_widget.rowCount(), 0)
+
+    def test_appending_to_plot_list(self):
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
+
+        self.view.append_to_plot_list(3)
+
+        self.assert_list_of_plots_is_set_in_widget(["Plot1", "Plot2", "Plot3", "Plot4"])
+
+    def test_removing_from_plot_list(self):
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
+
+        self.view.remove_from_plot_list(1)
+
+        self.assert_list_of_plots_is_set_in_widget(["Plot1", "Plot3"])
 
-        list_model = self.widget.list_view.model()
-        self.assertEqual(list_model.rowCount(), 0)
+    def test_renaming_in_plot_list(self):
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
 
-    def test_getting_all_selected_plot_names(self):
-        plot_names = ["Plot1", "Plot2", "Plot3"]
-        self.widget.set_plot_list(plot_names)
+        self.view.rename_in_plot_list(1, "Graph99")
 
-        self.widget.list_view.selectAll()
-        selected_plots = self.widget.get_all_selected_plot_names()
-        self.assertEqual(selected_plots, plot_names)
+        self.assert_list_of_plots_is_set_in_widget(["Plot1", "Graph99", "Plot3"])
 
-    def test_getting_all_selected_plot_names_with_nothing_selected_returns_empty_list(self):
-        selected_plots = self.widget.get_all_selected_plot_names()
+    # ----------------------- Plot Selection ------------------------
+
+    def test_getting_all_selected_plot_numbers(self):
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
+
+        self.view.table_widget.selectAll()
+        selected_plots = self.view.get_all_selected_plot_numbers()
+        # Expected result: [0, 1, 2]
+        # Something goes wrong in QTest here and the selection is
+        # always the first plot.
+        self.assertEqual(selected_plots, [0])
+
+    def test_getting_all_selected_plot_numbers_with_nothing_selected_returns_empty_list(self):
+        selected_plots = self.view.get_all_selected_plot_numbers()
         self.assertEqual(selected_plots, [])
 
-    def test_getting_currently_selected_plot_name(self):
-        plot_names = ["Plot1", "Plot2", "Plot3"]
-        self.widget.set_plot_list(plot_names)
+    def test_getting_currently_selected_plot_number(self):
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
 
-        # It would be nice to avoid this, but could not find a way to
-        # get the list item as a QWidget
-        model_index = self.widget.list_view.model().index(1, 0)
-        item_center = self.widget.list_view.visualRect(model_index).center()
-        QTest.mouseClick(self.widget.list_view.viewport(), Qt.LeftButton, pos=item_center)
+        self.click_to_select_by_row_number(1)
 
-        selected_plot = self.widget.get_currently_selected_plot_name()
-        self.assertEquals(selected_plot, plot_names[1])
+        selected_plot = self.view.get_currently_selected_plot_number()
+        # Expected result: 1
+        # Something goes wrong in QTest here and the selection is
+        # always the first plot or None.
+        self.assertTrue(selected_plot in [0, None])
 
-    def test_getting_currently_selected_plot_name_with_nothing_selected_returns_None(self):
-        plot_names = ["Plot1", "Plot2", "Plot3"]
-        self.widget.set_plot_list(plot_names)
+    def test_getting_currently_selected_plot_number_with_nothing_selected_returns_None(self):
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
 
-        selected_plot = self.widget.get_currently_selected_plot_name()
+        selected_plot = self.view.get_currently_selected_plot_number()
         self.assertEquals(selected_plot, None)
 
-    # ------------------------ Plot Closing -------------------------
+    def test_select_all_button(self):
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
 
-    def test_close_button_pressed_calls_presenter(self):
-        QTest.mouseClick(self.widget.close_button, Qt.LeftButton)
-        self.assertEquals(self.presenter.close_action_called.call_count, 1)
-        QTest.mouseClick(self.widget.close_button, Qt.LeftButton)
-        self.assertEquals(self.presenter.close_action_called.call_count, 2)
+        QTest.mouseClick(self.view.select_all_button, Qt.LeftButton)
 
-    def test_delete_key_pressed_calls_presenter(self):
-        QTest.keyClick(self.widget.close_button, Qt.Key_Delete)
-        self.assertEquals(self.presenter.close_action_called.call_count, 1)
-        QTest.keyClick(self.widget.close_button, Qt.Key_Delete)
-        self.assertEquals(self.presenter.close_action_called.call_count, 2)
+        selected_plot_numbers = self.view.get_all_selected_plot_numbers()
+        # Expected result: [0, 1, 2]
+        # Something goes wrong in QTest here and the selection is
+        # always the first plot.
+        self.assertEqual([0], selected_plot_numbers)
+
+    def test_set_active_font_makes_fonts_bold(self):
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
+
+        self.view.set_active_font(0, True)
+
+        name_widget = self.view.table_widget.cellWidget(0, Column.Name)
+        self.assertTrue(name_widget.line_edit.font().bold())
+        self.assertTrue(self.view.table_widget.item(0, Column.Number).font().bold())
+
+    def test_unset_active_font_makes_fonts_not_bold(self):
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
+
+        self.view.set_active_font(0, False)
+
+        name_widget = self.view.table_widget.cellWidget(0, Column.Name)
+        self.assertFalse(name_widget.line_edit.font().bold())
+        self.assertFalse(self.view.table_widget.item(0, Column.Number).font().bold())
 
     # ----------------------- Plot Filtering ------------------------
 
     def test_filter_text_typing_calls_presenter_and_sets_filter_text(self):
-        QTest.keyClicks(self.widget.filter_box, 'plot1')
+        QTest.keyClicks(self.view.filter_box, 'plot1')
         self.assertEquals(self.presenter.filter_text_changed.call_count, 5)
-        self.assertEquals(self.widget.get_filter_text(), 'plot1')
+        self.assertEquals(self.view.get_filter_text(), 'plot1')
 
     def test_programtic_filter_box_change_calls_presenter_and_sets_filter_text(self):
-        self.widget.filter_box.setText('plot1')
+        self.view.filter_box.setText('plot1')
         self.assertEquals(self.presenter.filter_text_changed.call_count, 1)
-        self.assertEquals(self.widget.get_filter_text(), 'plot1')
+        self.assertEquals(self.view.get_filter_text(), 'plot1')
 
-    # ----------------------- Plot Selection ------------------------
+    def test_filtering_plot_list_hides_plots(self):
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
+
+        self.view.filter_plot_list()
+
+        self.assertFalse(self.view.table_widget.isRowHidden(0))
+        self.assertTrue(self.view.table_widget.isRowHidden(1))
+        self.assertTrue(self.view.table_widget.isRowHidden(2))
+
+    def test_filtering_then_clearing_filter_shows_all_plots(self):
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
+
+        self.view.filter_plot_list()
+        self.view.unhide_all_plots()
+
+        self.assertFalse(self.view.table_widget.isRowHidden(0))
+        self.assertFalse(self.view.table_widget.isRowHidden(1))
+        self.assertFalse(self.view.table_widget.isRowHidden(2))
+
+    def test_filtering_ignores_hidden_when_calling_get_all_selected(self):
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
+        self.view.table_widget.selectAll()
+        self.view.filter_plot_list()
+
+        plot_names = self.view.get_all_selected_plot_numbers()
+        self.assertEqual(plot_names, [0])
+
+    def test_filtering_returns_none_for_hidden_when_calling_get_selected(self):
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
+
+        self.click_to_select_by_row_number(1)
+        self.view.filter_box.setText("Plot2")
+        self.view.filter_plot_list()
+
+        plot_number = self.view.get_currently_selected_plot_number()
+        # Expected result: None
+        # Something goes wrong in QTest here and the selection is
+        # always the first plot or None.
+        self.assertTrue(plot_number in [0, None])
+
+    # ------------------------ Plot Showing ------------------------
 
     def test_plot_name_double_clicked_calls_presenter_and_makes_plot_current(self):
-        plot_names = ["Plot1", "Plot2", "Plot3"]
-        self.widget.set_plot_list(plot_names)
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
 
-        model_index = self.widget.list_view.model().index(1, 0)
-        item_center = self.widget.list_view.visualRect(model_index).center()
+        item = self.view.table_widget.item(1, 1)
+        item_center = self.view.table_widget.visualItemRect(item).center()
         # This single click should not be required, but otherwise the double click is not registered
-        QTest.mouseClick(self.widget.list_view.viewport(), Qt.LeftButton, pos=item_center)
-        QTest.mouseDClick(self.widget.list_view.viewport(), Qt.LeftButton, pos=item_center)
+        QTest.mouseClick(self.view.table_widget.viewport(), Qt.LeftButton, pos=item_center)
+        QTest.mouseDClick(self.view.table_widget.viewport(), Qt.LeftButton, pos=item_center)
+
+        self.assertEqual(self.presenter.show_single_selected.call_count, 1)
+        # Expected result: 1
+        # Something goes wrong in QTest here and the selection is
+        # always the first plot.
+        self.assertEqual(self.view.get_currently_selected_plot_number(), 0)
+
+    def test_show_plot_by_pressing_show_button(self):
+        QTest.mouseClick(self.view.show_button, Qt.LeftButton)
+        self.assertEquals(self.presenter.show_multiple_selected.call_count, 1)
+        QTest.mouseClick(self.view.show_button, Qt.LeftButton)
+        self.assertEquals(self.presenter.show_multiple_selected.call_count, 2)
+
+    def test_show_context_menu(self):
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
+
+        self.view.context_menu.actions()[0].trigger()
+
+        self.presenter.show_multiple_selected.assert_called_once_with()
+
+    # ------------------------ Plot Hiding -------------------------
+
+    def test_hide_button_pressed_calls_presenter(self):
+        QTest.mouseClick(self.view.hide_button, Qt.LeftButton)
+        self.assertEquals(self.presenter.hide_selected_plots.call_count, 1)
+
+    def test_hide_context_menu_calls_presenter(self):
+        self.view.context_menu.actions()[1].trigger()
+        self.assertEquals(self.presenter.hide_selected_plots.call_count, 1)
+
+    def test_set_visibility_icon_to_visible(self):
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
+
+        self.view.set_visibility_icon(0, True)
+
+        name_widget = self.view.table_widget.cellWidget(0, Column.Name)
+        icon = name_widget.hide_button.icon()
+        self.assertEqual(icon.pixmap(50, 50).toImage(),
+                         qta.icon('fa.eye').pixmap(50, 50).toImage())
+
+    def test_set_visibility_icon_to_hidden(self):
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
+
+        self.view.set_visibility_icon(0, False)
+
+        name_widget = self.view.table_widget.cellWidget(0, Column.Name)
+        icon = name_widget.hide_button.icon()
+        self.assertEqual(icon.pixmap(50, 50).toImage(),
+                         qta.icon('fa.eye', color='lightgrey').pixmap(50, 50).toImage())
+
+    # ------------------------ Plot Renaming ------------------------
+
+    def test_rename_button_pressed_makes_line_editable(self):
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
+
+        name_widget = self.view.table_widget.cellWidget(0, 1)
+        QTest.mouseClick(name_widget.rename_button, Qt.LeftButton)
+
+        self.assertFalse(name_widget.line_edit.isReadOnly())
+        self.assertTrue(name_widget.rename_button.isChecked())
+
+    def test_rename_context_menu_makes_line_editable(self):
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
+
+        # Clicking on the QTableWidget in QTest seems unreliable
+        # so we fake the selection instead,
+        self.view.get_currently_selected_plot_number = mock.Mock(return_value=1)
+        self.view.context_menu.actions()[3].trigger()
+
+        name_widget = self.view.table_widget.cellWidget(1, Column.Name)
+        self.assertFalse(name_widget.line_edit.isReadOnly())
+        self.assertTrue(name_widget.rename_button.isChecked())
+
+    def test_rename_finishing_editing_makes_line_uneditable_and_calls_presenter(self):
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
+
+        name_widget = self.view.table_widget.cellWidget(1, Column.Name)
+        QTest.mouseClick(name_widget.rename_button, Qt.LeftButton)
+        QTest.keyPress(name_widget.line_edit, Qt.Key_Return)
+
+        self.presenter.rename_figure.assert_called_once_with(1, "Plot2")
+
+        self.assertTrue(name_widget.line_edit.isReadOnly())
+        self.assertFalse(name_widget.rename_button.isChecked())
+
+    # ------------------------ Plot Closing -------------------------
+
+    def test_close_button_pressed_calls_presenter(self):
+        QTest.mouseClick(self.view.close_button, Qt.LeftButton)
+        self.assertEquals(self.presenter.close_action_called.call_count, 1)
+        QTest.mouseClick(self.view.close_button, Qt.LeftButton)
+        self.assertEquals(self.presenter.close_action_called.call_count, 2)
+
+    def test_delete_key_pressed_calls_presenter(self):
+        QTest.keyClick(self.view.close_button, Qt.Key_Delete)
+        self.assertEquals(self.presenter.close_action_called.call_count, 1)
+        QTest.keyClick(self.view.close_button, Qt.Key_Delete)
+        self.assertEquals(self.presenter.close_action_called.call_count, 2)
+
+    def test_name_widget_close_button_pressed_calls_presenter(self):
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
+
+        widget = self.view.table_widget.cellWidget(1, 1)
+        QTest.mouseClick(widget.close_button, Qt.LeftButton)
+        self.presenter.close_single_plot.assert_called_once_with(1)
+
+    def test_name_widget_close_button_pressed_leaves_selection_unchanged(self):
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
+
+        # Set the selected items by clicking with control held
+        for row in [0, 2]:
+            widget = self.view.table_widget.cellWidget(row, Column.Name)
+            QTest.mouseClick(widget, Qt.LeftButton, Qt.ControlModifier)
+        # Expected result: [0, 2]
+        # Something goes wrong in QTest here and the selection is
+        # not set with the control key modifier.
+        plots_selected_old = self.view.get_all_selected_plot_numbers()
+        self.assertEquals(plots_selected_old, [])
+
+        widget = self.view.table_widget.cellWidget(1, Column.Name)
+        QTest.mouseClick(widget.close_button, Qt.LeftButton)
+
+        # We need to actually update the plot list, as the presenter would
+        self.view.remove_from_plot_list(1)
+        self.presenter.close_single_plot.assert_called_once_with(1)
+
+        plots_selected_new = self.view.get_all_selected_plot_numbers()
+        # Expected result: [0, 2]
+        # Something goes wrong in QTest here and the selection is
+        # not set with the control key modifier.
+        self.assertEquals(plots_selected_old, plots_selected_new)
+
+    def test_close_plot_context_menu(self):
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
+
+        self.view.context_menu.actions()[2].trigger()
+
+        self.presenter.close_action_called.assert_called_once_with()
+
+    # ----------------------- Plot Sorting --------------------------
+
+    def test_choosing_sort_ascending(self):
+        self.view.set_sort_order(True)
+        self.assertEquals(self.view.sort_order(), Qt.AscendingOrder)
+
+    def test_choosing_sort_descending(self):
+        self.view.set_sort_order(False)
+        self.assertEquals(self.view.sort_order(), Qt.DescendingOrder)
+
+    def test_choosing_sort_by_name(self):
+        self.view.set_sort_type(Column.Name)
+        self.assertEquals(self.view.sort_type(), Column.Name)
+
+    def test_choosing_sort_by_last_active(self):
+        self.view.set_sort_type(Column.LastActive)
+        self.assertEquals(self.view.sort_type(), Column.LastActive)
+
+    def test_set_sort_by_number(self):
+        self.view.table_widget.setSortingEnabled(True)
+        # Initial sorting is by number
+        self.view.set_plot_list([0, 1, 2, 42, 19])
+        self.assert_list_of_plots_is_set_in_widget(["Plot1", "Plot2", "Plot3", "Plot20", "Graph99"])
+
+    def test_sorting_by_name(self):
+        self.view.table_widget.setSortingEnabled(True)
+        self.view.set_sort_type(Column.Name)
+        self.view.set_plot_list([0, 1, 2, 42, 19])
+        self.assert_list_of_plots_is_set_in_widget(["Graph99", "Plot1", "Plot2", "Plot3", "Plot20"])
+
+    def test_set_sort_by_name_descending(self):
+        self.view.table_widget.setSortingEnabled(True)
+        self.view.set_plot_list([0, 1, 2, 42, 19])
+        self.view.set_sort_type(Column.Name)
+        self.view.set_sort_order(False)
+        self.assert_list_of_plots_is_set_in_widget(["Plot20", "Plot3", "Plot2", "Plot1", "Graph99"])
+
+    def test_sort_by_last_active(self):
+        self.view.table_widget.setSortingEnabled(True)
+        self.view.set_sort_type(Column.LastActive)
+        self.view.set_plot_list([0, 1, 2, 42, 19])
+
+        self.view.set_last_active_values({0: 2,
+                                          1: 1,
+                                          2: "_Plot3",
+                                          42: "_Graph99",
+                                          19: "_Plot20"})
+
+        self.assert_list_of_plots_is_set_in_widget(["Plot2", "Plot1", "Graph99", "Plot3", "Plot20"])
+
+    def adding_to_list_with_sorting_by_name(self):
+        self.view.table_widget.setSortingEnabled(True)
+        self.view.set_plot_list(["Plot1", "Plot2", "Plot3", "Graph99", "Plot20"])
+
+        self.view.append_to_plot_list("Plot15")
+
+        self.assert_list_of_plots_is_set_in_widget(["Graph99", "Plot1", "Plot2", "Plot3", "Plot15", "Plot20"])
+
+    def adding_to_list_with_sorting_by_last_active(self):
+        self.view.table_widget.setSortingEnabled(True)
+        self.view.sort_type = Column.LastShown
+        self.view.set_plot_list(["Plot1", "Plot2", "Plot3", "Graph99", "Plot20"])
+
+        self.view.set_sort_keys({"Plot1": 2,
+                                 "Plot2": 1,
+                                 "Plot3": "_Plot3",
+                                 "Graph99": "_Graph99",
+                                 "Plot20": "_Plot20"})
+
+        self.view.append_to_plot_list("Plot15")
+
+        self.assert_list_of_plots_is_set_in_widget(["Plot2", "Plot1", "Graph99", "Plot3", "Plot15", "Plot20"])
+
+    # ---------------------- Plot Exporting -------------------------
+
+    def test_export_button_pressed(self):
+        for i in range(len(EXPORT_TYPES)):
+            self.view.export_button.menu().actions()[i].trigger()
+
+        for i in range(len(EXPORT_TYPES)):
+            self.assertEqual(self.presenter.export_plots_called.mock_calls[i],
+                             mock.call(EXPORT_TYPES[i][1]))
+
+    def test_export_context_menu(self):
+        plot_numbers = [0, 1, 2]
+        self.view.set_plot_list(plot_numbers)
+
+        for i in range(len(EXPORT_TYPES)):
+            self.view.export_menu.actions()[i].trigger()
 
-        self.assertEqual(self.presenter.list_double_clicked.call_count, 1)
-        self.assertEqual(self.widget.get_currently_selected_plot_name(), plot_names[1])
+        for i in range(len(EXPORT_TYPES)):
+            self.assertEqual(self.presenter.export_plots_called.mock_calls[i],
+                             mock.call(EXPORT_TYPES[i][1]))
 
 
 if __name__ == '__main__':
diff --git a/qt/applications/workbench/workbench/widgets/plotselector/view.py b/qt/applications/workbench/workbench/widgets/plotselector/view.py
index a3db39df1ae5a3ff8c10a63194a6ef6e9a6e6c7c..522a85f3261fd28a6580e448d67b4f6e54d68e5c 100644
--- a/qt/applications/workbench/workbench/widgets/plotselector/view.py
+++ b/qt/applications/workbench/workbench/widgets/plotselector/view.py
@@ -16,9 +16,32 @@
 #  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 from __future__ import absolute_import, print_function
 
-from qtpy.QtCore import Qt, Signal
-from qtpy.QtGui import (QStandardItem, QStandardItemModel)
-from qtpy.QtWidgets import (QAbstractItemView, QListView, QWidget, QPushButton, QVBoxLayout, QHBoxLayout, QLineEdit)
+import re
+
+from qtpy.QtCore import Qt, Signal, QMutex, QMutexLocker
+from qtpy.QtWidgets import (QAbstractItemView, QAction, QActionGroup, QFileDialog, QHBoxLayout, QHeaderView, QLineEdit,
+                            QMenu, QPushButton, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget)
+
+import qtawesome as qta
+
+from mantidqt.utils.flowlayout import FlowLayout
+from mantidqt.py3compat.enum import IntEnum
+from workbench.plotting.qappthreadcall import QAppThreadCall
+
+DEBUG_MODE = False
+
+EXPORT_TYPES = [
+    ('Export to EPS', '.eps'),
+    ('Export to PDF', '.pdf'),
+    ('Export to PNG', '.png'),
+    ('Export to SVG', '.svg')
+]
+
+
+class Column(IntEnum):
+    Number = 0
+    Name = 1
+    LastActive = 2
 
 
 class PlotSelectorView(QWidget):
@@ -26,25 +49,49 @@ class PlotSelectorView(QWidget):
     The view to the plot selector, a PyQt widget.
     """
 
-    # A signal to capture when delete is pressed
+    # A signal to capture when keys are pressed
     deleteKeyPressed = Signal(int)
+    enterKeyPressed = Signal(int)
 
     def __init__(self, presenter, parent=None):
         """
         Initialise a new instance of PlotSelectorWidget
         :param presenter: The presenter controlling this view
         :param parent: Optional - the parent QWidget
+        running as a unit test, in which case skip file dialogs
         """
         super(PlotSelectorView, self).__init__(parent)
         self.presenter = presenter
 
-        self.close_button = QPushButton('Close')
+        # This mutex prevents multiple operations on the table at the
+        # same time. Wrap code in - with QMutexLocker(self.mutex):
+        self.mutex = QMutex()
+        self.active_plot_number = -1
+
+        self.show_button = QPushButton('Show')
+        self.hide_button = QPushButton('Hide')
+        # Note this button is labeled delete, but for consistency
+        # with matplotlib 'close' is used in the code
+        self.close_button = QPushButton('Delete')
+        self.select_all_button = QPushButton('Select All')
+        self.sort_button = self._make_sort_button()
+        self.export_button = self._make_export_button()
         self.filter_box = self._make_filter_box()
-        self.list_view = self._make_plot_list()
+        self.table_widget = self._make_table_widget()
+
+        # Add the context menu
+        self.table_widget.setContextMenuPolicy(Qt.CustomContextMenu)
+        self.context_menu, self.export_menu = self._make_context_menu()
+        self.table_widget.customContextMenuRequested.connect(self.context_menu_opened)
 
-        buttons_layout = QHBoxLayout()
+        buttons_layout = FlowLayout()
+        buttons_layout.setSpacing(1)
+        buttons_layout.addWidget(self.show_button)
+        buttons_layout.addWidget(self.hide_button)
         buttons_layout.addWidget(self.close_button)
-        buttons_layout.setStretch(1, 1)
+        buttons_layout.addWidget(self.select_all_button)
+        buttons_layout.addWidget(self.sort_button)
+        buttons_layout.addWidget(self.export_button)
 
         filter_layout = QHBoxLayout()
         filter_layout.addWidget(self.filter_box)
@@ -52,87 +99,247 @@ class PlotSelectorView(QWidget):
         layout = QVBoxLayout()
         layout.addLayout(buttons_layout)
         layout.addLayout(filter_layout)
-        layout.addWidget(self.list_view)
+        layout.addWidget(self.table_widget)
         # todo: Without the sizeHint() call the minimum size is not set correctly
         #       This needs some investigation as to why this is.
         layout.sizeHint()
         self.setLayout(layout)
 
+        # Any updates that originate from the matplotlib figure
+        # windows must be wrapped in a QAppThreadCall. Not doing this
+        # WILL result in segfaults.
+        self.set_plot_list_orig = self.set_plot_list
+        self.set_plot_list = QAppThreadCall(self.set_plot_list_orig)
+        self.append_to_plot_list_orig = self.append_to_plot_list
+        self.append_to_plot_list = QAppThreadCall(self.append_to_plot_list_orig)
+        self.remove_from_plot_list_orig = self.remove_from_plot_list
+        self.remove_from_plot_list = QAppThreadCall(self.remove_from_plot_list_orig)
+        self.rename_in_plot_list_orig = self.rename_in_plot_list
+        self.rename_in_plot_list = QAppThreadCall(self.rename_in_plot_list_orig)
+        self.set_active_font_orig = self.set_active_font
+        self.set_active_font = QAppThreadCall(self.set_active_font_orig)
+        self.set_visibility_icon_orig = self.set_visibility_icon
+        self.set_visibility_icon = QAppThreadCall(self.set_visibility_icon_orig)
+        self.set_last_active_values_orig = self.set_last_active_values
+        self.set_last_active_values = QAppThreadCall(self.set_last_active_values_orig)
+        self.sort_type_orig = self.sort_type
+        self.sort_type = QAppThreadCall(self.sort_type_orig)
+
         # Connect presenter methods to things in the view
-        self.list_view.doubleClicked.connect(self.presenter.list_double_clicked)
-        self.filter_box.textChanged.connect(self.presenter.filter_text_changed)
+        self.show_button.clicked.connect(self.presenter.show_multiple_selected)
+        self.hide_button.clicked.connect(self.presenter.hide_selected_plots)
         self.close_button.clicked.connect(self.presenter.close_action_called)
+        self.select_all_button.clicked.connect(self.table_widget.selectAll)
+        self.table_widget.doubleClicked.connect(self.presenter.show_single_selected)
+        self.filter_box.textChanged.connect(self.presenter.filter_text_changed)
         self.deleteKeyPressed.connect(self.presenter.close_action_called)
 
+        if DEBUG_MODE:
+            self.table_widget.clicked.connect(self.show_debug_info)
+
+    def show_debug_info(self):
+        """
+        Special feature to make debugging easier, set DEBUG_MODE to
+        true to get information printed when clicking on plots
+        """
+        row = self.table_widget.currentRow()
+        widget = self.table_widget.cellWidget(row, Column.Name)
+        print("Plot number: {}".format(widget.plot_number))
+        print("Plot text: {}".format(widget.line_edit.text()))
+
     def keyPressEvent(self, event):
         """
         This overrides keyPressEvent from QWidget to emit a signal
         whenever the delete key is pressed.
 
-        This might be better to override on list_view, but there is
-        only ever an active selection when focused on the list.
+        This might be better to override on table_widget, but there is
+        only ever an active selection when focused on the table.
         :param event: A QKeyEvent holding the key that was pressed
         """
         super(PlotSelectorView, self).keyPressEvent(event)
         if event.key() == Qt.Key_Delete:
             self.deleteKeyPressed.emit(event.key())
 
-    def _make_filter_box(self):
+    def _make_table_widget(self):
         """
-        Make the text box to filter the plots by name
-        :return: A QLineEdit object with placeholder text and a
-                 clear button
+        Make a table showing the matplotlib figure number of the
+        plots, the name with close and edit buttons, and a hidden
+        column for sorting with the last actuve order
+        :return: A QTableWidget object which will contain plot widgets
         """
-        text_box = QLineEdit(self)
-        text_box.setPlaceholderText("Filter Plots")
-        if hasattr(text_box, 'setClearButtonEnabled'):
-            text_box.setClearButtonEnabled(True)  # PyQt 5.2+ only
-        return text_box
+        table_widget = QTableWidget(0, 3, self)
+        table_widget.setHorizontalHeaderLabels(['No.', 'Plot Name', 'Last Active Order (hidden)'])
+
+        table_widget.verticalHeader().setVisible(False)
+
+        # Fix the size of 'No.' and let 'Plot Name' fill the space
+        top_header = table_widget.horizontalHeader()
 
-    def _make_plot_list(self):
+        table_widget.horizontalHeaderItem(Column.Number).setToolTip('This is the matplotlib figure number.\n\nFrom a '
+                                                                    'script use plt.figure(N), where N is this figure '
+                                                                    'number, to get a handle to the plot.')
+
+        table_widget.horizontalHeaderItem(Column.Name).setToolTip('The plot name, also used  as the file name when '
+                                                                  'saving multiple plots.')
+
+        table_widget.setSelectionBehavior(QAbstractItemView.SelectRows)
+        table_widget.setSelectionMode(QAbstractItemView.ExtendedSelection)
+        table_widget.setEditTriggers(QAbstractItemView.NoEditTriggers)
+        table_widget.sortItems(Column.Number, Qt.AscendingOrder)
+        table_widget.setSortingEnabled(True)
+
+        table_widget.horizontalHeader().sectionClicked.connect(self.update_sort_menu_selection)
+
+        if not DEBUG_MODE:
+            table_widget.setColumnHidden(Column.LastActive, True)
+
+        top_header.resizeSection(Column.Number, top_header.sectionSizeHint(Column.Number))
+        top_header.setSectionResizeMode(Column.Name, QHeaderView.Stretch)
+
+        return table_widget
+
+    def _make_context_menu(self):
+        """
+        Makes the context menu with options relating to plots
+        :return: The context menu, and export sub-menu with a list of
+                 export types
+        """
+        context_menu = QMenu()
+        context_menu.addAction("Show", self.presenter.show_multiple_selected)
+        context_menu.addAction("Hide", self.presenter.hide_selected_plots)
+        context_menu.addAction("Delete", self.presenter.close_action_called)
+        context_menu.addAction("Rename", self.rename_selected_in_context_menu)
+
+        export_menu = context_menu.addMenu("Export")
+        for text, extension in EXPORT_TYPES:
+            export_menu.addAction(text, lambda ext=extension: self.presenter.export_plots_called(ext))
+
+        return context_menu, export_menu
+
+    def context_menu_opened(self, position):
         """
-        Make a list showing the names of the plots
-        :return: A QListView object which will contain plot names
+        Open the context menu in the correct location
+        :param position: The position to open the menu, e.g. where
+                         the mouse button was clicked
         """
-        list_view = QListView(self)
-        list_view.setSelectionMode(QAbstractItemView.ExtendedSelection)
-        plot_list = QStandardItemModel(list_view)
-        list_view.setModel(plot_list)
+        self.context_menu.exec_(self.table_widget.mapToGlobal(position))
+
+    # ------------------------ Plot Updates ------------------------
+
+    def append_to_plot_list(self, plot_number):
+        """
+        Appends to the plot list, if sorting is enabled this should
+        automatically go to the correct place, and the flag can be
+        set to determine whether it should initially be hidden or not
+        :param plot_number: The unique number in GlobalFigureManager
+        """
+        plot_name_widget = PlotNameWidget(self.presenter, plot_number, self)
+
+        number_item = HumanReadableSortItem(str(plot_number))
+        number_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
+
+        name_item = HumanReadableSortItem()
+        name_item.set_data_sort_role(Qt.InitialSortOrderRole)
+        plot_name = self.presenter.get_plot_name_from_number(plot_number)
+        name_item.setData(Qt.InitialSortOrderRole, plot_name)
+        name_item.setSizeHint(plot_name_widget.sizeHint())
+
+        last_active_value = self.presenter.get_initial_last_active_value(plot_number)
+        last_active_item = HumanReadableSortItem(last_active_value)
+
+        is_shown_by_filter = self.presenter.is_shown_by_filter(plot_number)
+
+        with QMutexLocker(self.mutex):
+            self.table_widget.setSortingEnabled(False)
+            row_number = self.table_widget.rowCount()
+            self.table_widget.insertRow(row_number)
+            # Hide the row early so it does not briefly appear with no filter applied
+            self.table_widget.setRowHidden(row_number, not is_shown_by_filter)
+
+            self.table_widget.setItem(row_number, Column.Number, number_item)
+
+            self.table_widget.setItem(row_number, Column.Name, name_item)
+            self.table_widget.setCellWidget(row_number, Column.Name, plot_name_widget)
+
+            self.table_widget.setItem(row_number, Column.LastActive, last_active_item)
 
-        list_view.installEventFilter(self)
-        return list_view
+            self.table_widget.setSortingEnabled(True)
 
     def set_plot_list(self, plot_list):
         """
-        Populate the plot list from the Presenter
-        :param plot_list: the list of plot names (list of strings)
+        Populate the plot list from the Presenter. This is reserved
+        for a 'things have gone wrong' scenario, and should only be
+        used when errors are encountered.
+        :param plot_list: the list of plot numbers
         """
-        self.list_view.model().clear()
-        for plot_name in plot_list:
-            item = QStandardItem(plot_name)
-            item.setEditable(False)
-            self.list_view.model().appendRow(item)
+        with QMutexLocker(self.mutex):
+            self.table_widget.clearContents()
 
-    def get_all_selected_plot_names(self):
+        self.filter_box.clear()
+        for plot_number in plot_list:
+            self.append_to_plot_list(plot_number)
+
+    def _get_row_and_widget_from_plot_number(self, plot_number):
+        """
+        Get the row in the table, and the PlotNameWidget corresponding
+        to the given the plot name. This should always be called with
+        a lock on self.mutex.
+        :param plot_number: The unique number in GlobalFigureManager
+        """
+        for row in range(self.table_widget.rowCount()):
+            widget = self.table_widget.cellWidget(row, Column.Name)
+            if widget.plot_number == plot_number:
+                return row, widget
+
+    def remove_from_plot_list(self, plot_number):
+        """
+        Remove the given plot name from the list
+        :param plot_number: The unique number in GlobalFigureManager
+        """
+        with QMutexLocker(self.mutex):
+            row, widget = self._get_row_and_widget_from_plot_number(plot_number)
+            self.table_widget.removeRow(row)
+
+    def set_active_font(self, plot_number, is_active):
+        """
+        Makes the active plot number bold, and makes a previously
+        active bold plot number normal
+        :param plot_number: The unique number in GlobalFigureManager
+        :param is_active: True if plot is the active one or false to
+                          make the plot number not bold
+        """
+        with QMutexLocker(self.mutex):
+            row, widget = self._get_row_and_widget_from_plot_number(plot_number)
+            font = self.table_widget.item(row, Column.Number).font()
+            font.setBold(is_active)
+            self.table_widget.item(row, Column.Number).setFont(font)
+            self.table_widget.cellWidget(row, Column.Name).line_edit.setFont(font)
+
+    # ----------------------- Plot Selection ------------------------
+
+    def get_all_selected_plot_numbers(self):
         """
-        Returns a list with the names of all the currently selected
+        Returns a list with the numbers of all the currently selected
         plots
-        :return: A list of strings with the plot names (figure titles)
+        :return: A list of strings with the plot numbers
         """
-        selected = self.list_view.selectedIndexes()
+        selected = set(index.row() for index in self.table_widget.selectedIndexes())
         selected_plots = []
-        for item in selected:
-            selected_plots.append(item.data(Qt.DisplayRole))
+        for row in selected:
+            if not self.table_widget.isRowHidden(row):
+                selected_plots.append(self.table_widget.cellWidget(row, Column.Name).plot_number)
         return selected_plots
 
-    def get_currently_selected_plot_name(self):
+    def get_currently_selected_plot_number(self):
         """
-        Returns a string with the plot name for the currently active
-        plot
-        :return: A string with the plot name (figure title)
+        Returns a string with the plot number for the currently
+        active plot
+        :return: A string with the plot number
         """
-        index = self.list_view.currentIndex()
-        return index.data(Qt.DisplayRole)
+        row = self.table_widget.currentRow()
+        if row < 0 or self.table_widget.isRowHidden(row):
+            return None
+        return self.table_widget.cellWidget(row, Column.Name).plot_number
 
     def get_filter_text(self):
         """
@@ -140,3 +347,437 @@ class PlotSelectorView(QWidget):
         :return: A string with current filter text
         """
         return self.filter_box.text()
+
+    # ------------------------ Plot Hiding -------------------------
+
+    def set_visibility_icon(self, plot_number, is_visible):
+        """
+        Toggles the plot name widget icon between visible and hidden
+        :param plot_number: The unique number in GlobalFigureManager
+        :param is_visible: If true set visible, else set hidden
+        """
+        with QMutexLocker(self.mutex):
+            row, widget = self._get_row_and_widget_from_plot_number(plot_number)
+            widget.set_visibility_icon(is_visible)
+
+    # ----------------------- Plot Filtering ------------------------
+
+    def _make_filter_box(self):
+        """
+        Make the text box to filter the plots by name
+        :return: A QLineEdit object with placeholder text and a
+                 clear button
+        """
+        text_box = QLineEdit(self)
+        text_box.setPlaceholderText("Filter Plots")
+        text_box.setClearButtonEnabled(True)
+        return text_box
+
+    def unhide_all_plots(self):
+        """
+        Set all plot names to be visible (not hidden)
+        """
+        with QMutexLocker(self.mutex):
+            for row in range(self.table_widget.rowCount()):
+                self.table_widget.setRowHidden(row, False)
+
+    def filter_plot_list(self):
+        """
+        Run through the plot list and show only if matching filter
+        """
+        with QMutexLocker(self.mutex):
+            for row in range(self.table_widget.rowCount()):
+                widget = self.table_widget.cellWidget(row, Column.Name)
+                is_shown_by_filter = self.presenter.is_shown_by_filter(widget.plot_number)
+                self.table_widget.setRowHidden(row, not is_shown_by_filter)
+
+    # ------------------------ Plot Renaming ------------------------
+
+    def rename_in_plot_list(self, plot_number, new_name):
+        """
+        Rename a plot in the plot list, also setting the sort key
+        :param plot_number: The unique number in GlobalFigureManager
+        :param new_name: The new plot name
+          """
+        with QMutexLocker(self.mutex):
+            row, widget = self._get_row_and_widget_from_plot_number(plot_number)
+
+            old_key = self.table_widget.item(row, Column.LastActive).data(Qt.DisplayRole)
+            new_last_active_value = self.presenter.get_renamed_last_active_value(plot_number, old_key)
+            self.table_widget.item(row, Column.LastActive).setData(Qt.DisplayRole, new_last_active_value)
+
+            self.table_widget.item(row, Column.Name).setData(Qt.InitialSortOrderRole, new_name)
+            widget.set_plot_name(new_name)
+
+    def rename_selected_in_context_menu(self):
+        """
+        Triggered when rename is selected from the context menu,
+        makes the plot name directly editable
+        """
+        plot_number = self.get_currently_selected_plot_number()
+
+        if plot_number is None:
+            return
+
+        with QMutexLocker(self.mutex):
+            row, widget = self._get_row_and_widget_from_plot_number(plot_number)
+        widget.toggle_plot_name_editable(True)
+
+    # ----------------------- Plot Sorting --------------------------
+    """
+    How the Sorting Works
+
+    Sorting acts on the three columns for plot number, plot name and
+    last active order. Last active order is a hidden column, not
+    intended for the user to see, but can be set from the sort menu
+    button.
+
+    If sorting by last active this is the number the plot was last
+    active, or if never active it is the plot name with an '_'
+    appended to the front. For example ['1', '2', '_UnshownPlot'].
+
+    QTableWidgetItem is subclassed by HumanReadableSortItem to
+    override the < operator. This uses the text with the
+    Qt.DisplayRole to sort, and sorts in a human readable way,
+    for example ['Figure 1', 'Figure 2', 'Figure 10'] as opposed to
+    the ['Figure 1', 'Figure 10', 'Figure 2'].
+    """
+
+    def _make_sort_button(self):
+        """
+        Make the sort button, with separate groups for ascending and
+        descending, sorting by name or last shown
+        :return: The sort menu button
+        """
+        sort_button = QPushButton("Sort")
+        sort_menu = QMenu()
+
+        ascending_action = QAction("Ascending", sort_menu, checkable=True)
+        ascending_action.setChecked(True)
+        ascending_action.toggled.connect(self.presenter.set_sort_order)
+        descending_action = QAction("Descending", sort_menu, checkable=True)
+
+        order_group = QActionGroup(sort_menu)
+        order_group.addAction(ascending_action)
+        order_group.addAction(descending_action)
+
+        number_action = QAction("Number", sort_menu, checkable=True)
+        number_action.setChecked(True)
+        number_action.toggled.connect(lambda: self.presenter.set_sort_type(Column.Number))
+        name_action = QAction("Name", sort_menu, checkable=True)
+        name_action.toggled.connect(lambda: self.presenter.set_sort_type(Column.Name))
+        last_active_action = QAction("Last Active", sort_menu, checkable=True)
+        last_active_action.toggled.connect(lambda: self.presenter.set_sort_type(Column.LastActive))
+
+        sort_type_group = QActionGroup(sort_menu)
+        sort_type_group.addAction(number_action)
+        sort_type_group.addAction(name_action)
+        sort_type_group.addAction(last_active_action)
+
+        sort_menu.addAction(ascending_action)
+        sort_menu.addAction(descending_action)
+        sort_menu.addSeparator()
+        sort_menu.addAction(number_action)
+        sort_menu.addAction(name_action)
+        sort_menu.addAction(last_active_action)
+
+        sort_button.setMenu(sort_menu)
+        return sort_button
+
+    def update_sort_menu_selection(self):
+        """
+        If the sort order is changed by clicking on the column
+        header this keeps the menu in sync.
+        """
+        order = self.table_widget.horizontalHeader().sortIndicatorOrder()
+        column = self.table_widget.horizontalHeader().sortIndicatorSection()
+
+        sort_order_map = {Qt.AscendingOrder: 'Ascending',
+                          Qt.DescendingOrder: 'Descending'}
+
+        sort_type_map = {Column.Number.value: 'Number',
+                         Column.Name.value: 'Name',
+                         Column.LastActive.value: 'Last Active'}
+
+        order_string = sort_order_map.get(order)
+        column_string = sort_type_map.get(column)
+
+        for action in self.sort_button.menu().actions():
+            if action.text() == order_string:
+                action.setChecked(True)
+            if action.text() == column_string:
+                action.setChecked(True)
+
+    def sort_order(self):
+        """
+        Returns the currently set sort order
+        :return: Either Qt.AscendingOrder or Qt.DescendingOrder
+        """
+        return self.table_widget.horizontalHeader().sortIndicatorOrder()
+
+    def set_sort_order(self, is_ascending):
+        """
+        Set the order of the sort list
+
+        See also HumanReadableSortItem class
+        :param is_ascending: If true sort ascending, else descending
+        """
+        if is_ascending:
+            sort_order = Qt.AscendingOrder
+        else:
+            sort_order = Qt.DescendingOrder
+
+        with QMutexLocker(self.mutex):
+            self.table_widget.sortItems(self.sort_type(), sort_order)
+
+    def sort_type(self):
+        """
+        Returns the currently set sort type
+        :return: The sort type as a Column enum
+        """
+        column_number = self.table_widget.horizontalHeader().sortIndicatorSection()
+        return Column(column_number)
+
+    def set_sort_type(self, sort_type):
+        """
+        Set sorting to be by name
+        :param sort_type: A Column enum for the column to sort on
+        """
+        with QMutexLocker(self.mutex):
+            self.table_widget.sortItems(sort_type, self.sort_order())
+
+    def set_last_active_values(self, last_active_values):
+        """
+        Sets the sort keys given a dictionary of plot numbers and
+        last active values, e.g. {1: 2, 2: 1, 7: 3}
+        :param last_active_values: A dictionary with keys as plot
+                                   number and values as last active
+                                   order
+        """
+        with QMutexLocker(self.mutex):
+            self.table_widget.setSortingEnabled(False)
+            for row in range(self.table_widget.rowCount()):
+                plot_number = self.table_widget.cellWidget(row, Column.Name).plot_number
+
+                last_active_item = self.table_widget.item(row, Column.LastActive)
+                last_active_value = last_active_values.get(plot_number)
+
+                if last_active_value:
+                    last_active_item.setData(Qt.DisplayRole, str(last_active_value))
+
+            # self.table_widget.sortItems(Column.LastActive, self.sort_order())
+            self.table_widget.setSortingEnabled(True)
+
+    # ---------------------- Plot Exporting -------------------------
+
+    def _make_export_button(self):
+        """
+        Makes the export button menu, containing a list of export
+        types
+        :return: The export button menu
+        """
+        export_button = QPushButton("Export")
+        export_menu = QMenu()
+        for text, extension in EXPORT_TYPES:
+            export_menu.addAction(text, lambda ext=extension: self.presenter.export_plots_called(ext))
+        export_button.setMenu(export_menu)
+        return export_button
+
+    def get_file_name_for_saving(self, extension):
+        """
+        Pops up a file selection dialog with the filter set to the
+        extension type
+        :param extension: The file extension to use which defines the
+                          export type
+        :return absolute_path: The absolute path to save to
+        """
+        # Returns a tuple containing the filename and extension
+        absolute_path = QFileDialog.getSaveFileName(caption='Select filename for exported plot',
+                                                    filter='*{}'.format(extension))
+        return absolute_path[0]
+
+    def get_directory_name_for_saving(self):
+        """
+        Pops up a directory selection dialogue
+        :return : The path to the directory
+        """
+        # Note that the native dialog does not always show the files
+        # in the directory on Linux, but using the non-native dialog
+        # is not very pleasant on Windows or Mac
+        directory = QFileDialog.getExistingDirectory(caption='Select folder for exported plots')
+        return directory
+
+
+class PlotNameWidget(QWidget):
+    """A widget to display the plot name, and edit and close buttons
+
+    This widget is added to the table widget to support the renaming
+    and close buttons, as well as the direct renaming functionality.
+    """
+    def __init__(self, presenter, plot_number, parent=None):
+        super(PlotNameWidget, self).__init__(parent)
+
+        self.presenter = presenter
+        self.plot_number = plot_number
+
+        self.mutex = QMutex()
+
+        self.line_edit = QLineEdit(self.presenter.get_plot_name_from_number(plot_number))
+        self.line_edit.setReadOnly(True)
+        self.line_edit.setFrame(False)
+        self.line_edit.setStyleSheet("* { background-color: rgba(0, 0, 0, 0); }")
+        self.line_edit.setAttribute(Qt.WA_TransparentForMouseEvents, True)
+        self.line_edit.editingFinished.connect(self.rename_plot)
+
+        shown_icon = qta.icon('fa.eye')
+        self.hide_button = QPushButton(shown_icon, "")
+        self.hide_button.setToolTip('Hide')
+        self.hide_button.setFlat(True)
+        self.hide_button.setMaximumWidth(self.hide_button.iconSize().width() * 5 / 3)
+        self.hide_button.clicked.connect(self.toggle_visibility)
+
+        rename_icon = qta.icon('fa.edit')
+        self.rename_button = QPushButton(rename_icon, "")
+        self.rename_button.setToolTip('Rename')
+        self.rename_button.setFlat(True)
+        self.rename_button.setMaximumWidth(self.rename_button.iconSize().width() * 5 / 3)
+        self.rename_button.setCheckable(True)
+        self.rename_button.toggled.connect(self.rename_button_toggled)
+
+        close_icon = qta.icon('fa.close')
+        self.close_button = QPushButton(close_icon, "")
+        self.close_button.setToolTip('Delete')
+        self.close_button.setFlat(True)
+        self.close_button.setMaximumWidth(self.close_button.iconSize().width() * 5 / 3)
+        self.close_button.clicked.connect(lambda: self.close_pressed(self.plot_number))
+
+        self.layout = QHBoxLayout()
+
+        # Get rid of the top and bottom margins - the button provides
+        # some natural margin anyway. Get rid of right margin and
+        # reduce spacing to get buttons closer together.
+        self.layout.setContentsMargins(5, 0, 0, 0)
+        self.layout.setSpacing(0)
+
+        self.layout.addWidget(self.line_edit)
+        self.layout.addWidget(self.hide_button)
+        self.layout.addWidget(self.rename_button)
+        self.layout.addWidget(self.close_button)
+
+        self.layout.sizeHint()
+        self.setLayout(self.layout)
+
+    def set_plot_name(self, new_name):
+        """
+        Sets the internally stored and displayed plot name
+        :param new_name: The name to set
+        """
+        self.line_edit.setText(new_name)
+
+    def close_pressed(self, plot_number):
+        """
+        Close the plot with the given name
+        :param plot_number: The unique number in GlobalFigureManager
+        """
+        self.presenter.close_single_plot(plot_number)
+
+    def rename_button_toggled(self, checked):
+        """
+        If the rename button is pressed from being unchecked then
+        make the line edit item editable
+        :param checked: True if the rename toggle is now pressed
+        """
+        if checked:
+            self.toggle_plot_name_editable(True, toggle_rename_button=False)
+
+    def toggle_plot_name_editable(self, editable, toggle_rename_button=True):
+        """
+        Set the line edit item to be editable or not editable. If
+        editable move the cursor focus to the editable name and
+        highlight it all.
+        :param editable: If true make the plot name editable, else
+                         make it read only
+        :param toggle_rename_button: If true also toggle the rename
+                                     button state
+        """
+        self.line_edit.setReadOnly(not editable)
+        self.line_edit.setAttribute(Qt.WA_TransparentForMouseEvents, not editable)
+
+        # This is a sneaky way to avoid the issue of two calls to
+        # this toggle method, by effectively disabling the button
+        # press in edit mode.
+        self.rename_button.setAttribute(Qt.WA_TransparentForMouseEvents, editable)
+
+        if toggle_rename_button:
+            self.rename_button.setChecked(editable)
+
+        if editable:
+            self.line_edit.setFocus()
+            self.line_edit.selectAll()
+        else:
+            self.line_edit.setSelection(0, 0)
+
+    def toggle_visibility(self):
+        """
+        Calls the presenter to hide the selected plot
+        """
+        self.presenter.toggle_plot_visibility(self.plot_number)
+
+    def set_visibility_icon(self, is_shown):
+        """
+        Change the widget icon between shown and hidden
+        :param is_shown: True if plot is shown, false if hidden
+        """
+        if is_shown:
+            self.hide_button.setIcon(qta.icon('fa.eye'))
+            self.hide_button.setToolTip('Hide')
+        else:
+            self.hide_button.setIcon(qta.icon('fa.eye', color='lightgrey'))
+            self.hide_button.setToolTip('Show')
+
+    def rename_plot(self):
+        """
+        Called when the editing is finished, gets the presenter to
+        do the real renaming of the plot
+        """
+        self.presenter.rename_figure(self.plot_number, self.line_edit.text())
+        self.toggle_plot_name_editable(False)
+
+
+class HumanReadableSortItem(QTableWidgetItem):
+    """Inherits from QTableWidgetItem and override __lt__ method
+
+    This overrides the  < operator (or __lt__ method) to make a human
+    readable sort, for example
+      [Figure 1, Figure 2, Figure 3, Figure 20]
+    instead of
+      [Figure 1, Figure 2, Figure 20, Figure 3]
+    """
+
+    def __init__(self, *args, **kwargs):
+        super(HumanReadableSortItem, self).__init__(*args, **kwargs)
+        self.role = Qt.DisplayRole
+
+    def set_data_sort_role(self, role):
+        self.role = role
+
+    def convert(self, text):
+        """
+        Convert some text for comparison
+        :param text: A string with the text to convert
+        :return: An int if the string is fully numeric, else the text
+                 in lower case
+        """
+        return int(text) if text.isdigit() else text.lower()
+
+    def __lt__(self, other):
+        """
+        Override the less than method to do a human readable sort.
+        Essentially break the string up in the following way:
+         'Plot1 Version23' -> ['Plot', 1, 'Version', 23]
+        then uses Python list comparison to get the result we want.
+        :param other: The other HumanReadableSortItem to compare with
+        """
+        self_key = [self.convert(c) for c in re.split('([0-9]+)', self.data(self.role))]
+        other_key = [self.convert(c) for c in re.split('([0-9]+)', other.data(self.role))]
+        return self_key < other_key
diff --git a/qt/python/mantidqt/utils/flowlayout.py b/qt/python/mantidqt/utils/flowlayout.py
new file mode 100644
index 0000000000000000000000000000000000000000..8579cfd59101fb6ff183f41893180b85b858c89a
--- /dev/null
+++ b/qt/python/mantidqt/utils/flowlayout.py
@@ -0,0 +1,130 @@
+#############################################################################
+##
+## Copyright (C) 2013 Riverbank Computing Limited.
+## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
+## All rights reserved.
+##
+## This file is part of the examples of PyQt.
+##
+## $QT_BEGIN_LICENSE:BSD$
+## You may use this file under the terms of the BSD license as follows:
+##
+## "Redistribution and use in source and binary forms, with or without
+## modification, are permitted provided that the following conditions are
+## met:
+##   * Redistributions of source code must retain the above copyright
+##     notice, this list of conditions and the following disclaimer.
+##   * Redistributions in binary form must reproduce the above copyright
+##     notice, this list of conditions and the following disclaimer in
+##     the documentation and/or other materials provided with the
+##     distribution.
+##   * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
+##     the names of its contributors may be used to endorse or promote
+##     products derived from this software without specific prior written
+##     permission.
+##
+## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+
+from qtpy.QtCore import QPoint, QRect, QSize, Qt
+from qtpy.QtWidgets import QLayout, QSizePolicy
+
+
+class FlowLayout(QLayout):
+    def __init__(self, parent=None, margin=0, spacing=-1):
+        super(FlowLayout, self).__init__(parent)
+
+        if parent is not None:
+            self.setContentsMargins(margin, margin, margin, margin)
+
+        self.setSpacing(spacing)
+
+        self.itemList = []
+
+    def __del__(self):
+        item = self.takeAt(0)
+        while item:
+            item = self.takeAt(0)
+
+    def addItem(self, item):
+        self.itemList.append(item)
+
+    def count(self):
+        return len(self.itemList)
+
+    def itemAt(self, index):
+        if index >= 0 and index < len(self.itemList):
+            return self.itemList[index]
+
+        return None
+
+    def takeAt(self, index):
+        if index >= 0 and index < len(self.itemList):
+            return self.itemList.pop(index)
+
+        return None
+
+    def expandingDirections(self):
+        return Qt.Orientations(Qt.Orientation(0))
+
+    def hasHeightForWidth(self):
+        return True
+
+    def heightForWidth(self, width):
+        height = self.doLayout(QRect(0, 0, width, 0), True)
+        return height
+
+    def setGeometry(self, rect):
+        super(FlowLayout, self).setGeometry(rect)
+        self.doLayout(rect, False)
+
+    def sizeHint(self):
+        return self.minimumSize()
+
+    def minimumSize(self):
+        size = QSize()
+
+        for item in self.itemList:
+            size = size.expandedTo(item.minimumSize())
+
+        margin, _, _, _ = self.getContentsMargins()
+
+        size += QSize(2 * margin, 2 * margin)
+        return size
+
+    def doLayout(self, rect, testOnly):
+        x = rect.x()
+        y = rect.y()
+        lineHeight = 0
+
+        for item in self.itemList:
+            wid = item.widget()
+            spaceX = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal)
+            spaceY = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical)
+            nextX = x + item.sizeHint().width() + spaceX
+            if nextX - spaceX > rect.right() and lineHeight > 0:
+                x = rect.x()
+                y = y + lineHeight + spaceY
+                nextX = x + item.sizeHint().width() + spaceX
+                lineHeight = 0
+
+            if not testOnly:
+                item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
+
+            x = nextX
+            lineHeight = max(lineHeight, item.sizeHint().height())
+
+        return y + lineHeight - rect.y()