diff --git a/buildconfig/pycharm.env b/buildconfig/pycharm.env
new file mode 100644
index 0000000000000000000000000000000000000000..85b4c3930ea8c3c5c439bb9a6a4a2194582f3db8
--- /dev/null
+++ b/buildconfig/pycharm.env
@@ -0,0 +1,7 @@
+THIRD_PARTY_DIR=C:\Users\qbr77747\dev\2m\source\external\src\ThirdParty
+QT4_BIN=${THIRD_PARTY_DIR}\lib\qt4\bin;${THIRD_PARTY_DIR}\lib\qt4\lib
+QT5_BIN=${THIRD_PARTY_DIR}\lib\qt5\bin;${THIRD_PARTY_DIR}\lib\qt5\lib
+QT_QPA_PLATFORM_PLUGIN_PATH=${THIRD_PARTY_DIR}\lib\qt5\plugins
+PYTHONHOME=${THIRD_PARTY_DIR}\lib\python2.7
+MISC_BIN=${THIRD_PARTY_DIR}\bin;${THIRD_PARTY_DIR}\bin\mingw
+PATH=${MISC_BIN};${PYTHONHOME};${QT5_BIN};${QT4_BIN};${PATH}
\ No newline at end of file
diff --git a/qt/python/mantidqt/widgets/common/__init__.py b/qt/python/mantidqt/widgets/common/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/qt/python/mantidqt/widgets/common/table_copying.py b/qt/python/mantidqt/widgets/common/table_copying.py
new file mode 100644
index 0000000000000000000000000000000000000000..4c9ac9cf6d9b44a0ce885eed878ead6509a0dc20
--- /dev/null
+++ b/qt/python/mantidqt/widgets/common/table_copying.py
@@ -0,0 +1,137 @@
+from __future__ import (absolute_import, division, print_function)
+
+from qtpy import QtGui
+from qtpy.QtCore import QPoint
+from qtpy.QtGui import QCursor, QFont, QFontMetrics
+from qtpy.QtWidgets import (QTableView, QToolTip)
+
+NO_SELECTION_MESSAGE = "No selection"
+COPY_SUCCESSFUL_MESSAGE = "Copy Successful"
+
+
+def copy_spectrum_values(table, ws_read):
+    """
+    Copies the values selected by the user to the system's clipboard
+
+    :param table: Table from which the selection will be read
+    :param view:
+    :param ws_read: The workspace read function, that is used to access the data directly
+    """
+    selection_model = table.selectionModel()
+    if not selection_model.hasSelection():
+        show_no_selection_to_copy_toast()
+        return
+    selected_rows = selection_model.selectedRows()  # type: list
+    row_data = []
+
+    for index in selected_rows:
+        row = index.row()
+        data = "\t".join(map(str, ws_read(row)))
+
+        row_data.append(data)
+
+    copy_to_clipboard("\n".join(row_data))
+    show_successful_copy_toast()
+
+
+def show_no_selection_to_copy_toast():
+    show_mouse_toast(NO_SELECTION_MESSAGE)
+
+
+def show_successful_copy_toast():
+    show_mouse_toast(COPY_SUCCESSFUL_MESSAGE)
+
+
+def copy_bin_values(table, ws_read, num_rows):
+    selection_model = table.selectionModel()
+    if not selection_model.hasSelection():
+        show_no_selection_to_copy_toast()
+        return
+    selected_columns = selection_model.selectedColumns()  # type: list
+
+    # Qt gives back a QModelIndex, we need to extract the column from it
+    column_data = []
+    for index in selected_columns:
+        column = index.column()
+        data = [str(ws_read(row)[column]) for row in range(num_rows)]
+        column_data.append(data)
+
+    all_string_rows = []
+    for i in range(num_rows):
+        # Appends ONE value from each COLUMN, this is because the final string is being built vertically
+        # the noqa disables a 'data' variable redefined warning
+        all_string_rows.append("\t".join([data[i] for data in column_data]))  # noqa: F812
+
+    # Finally all rows are joined together with a new line at the end of each row
+    final_string = "\n".join(all_string_rows)
+    copy_to_clipboard(final_string)
+    show_successful_copy_toast()
+
+
+def copy_cells(table):
+    """
+    :type table: QTableView
+    :param table: The table from which the data will be copied.
+    :return:
+    """
+    selectionModel = table.selectionModel()
+    if not selectionModel.hasSelection():
+        show_no_selection_to_copy_toast()
+        return
+
+    selection = selectionModel.selection()
+    # TODO show a warning if copying more cells than some number (100? 200? 300? 400??)
+    selectionRange = selection.first()
+
+    top = selectionRange.top()
+    bottom = selectionRange.bottom()
+    left = selectionRange.left()
+    right = selectionRange.right()
+
+    data = []
+    index = selectionModel.currentIndex()
+    for i in range(top, bottom + 1):
+        for j in range(left, right):
+            data.append(index.sibling(i, j).data())
+            data.append("\t")
+        data.append(index.sibling(i, right).data())
+        data.append("\n")
+
+    # strip the string to remove the trailing new line
+    copy_to_clipboard("".join(data).strip())
+    show_successful_copy_toast()
+
+
+def keypress_copy(table, view, ws_read, num_rows):
+    selectionModel = table.selectionModel()
+    if not selectionModel.hasSelection():
+        show_no_selection_to_copy_toast()
+        return
+
+    if len(selectionModel.selectedRows()) > 0:
+        copy_spectrum_values(table, ws_read)
+    elif len(selectionModel.selectedColumns()) > 0:
+        copy_bin_values(table, ws_read, num_rows)
+    else:
+        copy_cells(table)
+
+
+def show_mouse_toast(message):
+    # Creates a text with empty space to get the height of the rendered text - this is used
+    # to provide the same offset for the tooltip, scaled relative to the current resolution and zoom.
+    font_metrics = QFontMetrics(QFont(" "))
+    # The height itself is divided by 2 just to reduce the offset so that the tooltip is
+    # reasonably position relative to the cursor
+    QToolTip.showText(QCursor.pos() + QPoint(font_metrics.height() / 2, 0), message)
+
+
+def copy_to_clipboard(data):
+    """
+    Uses the QGuiApplication to copy to the system clipboard.
+
+    :type data: str
+    :param data: The data that will be copied to the clipboard
+    :return:
+    """
+    cb = QtGui.QGuiApplication.clipboard()
+    cb.setText(data, mode=cb.Clipboard)
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/__init__.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..57d5ae5a28a63ed0dd44886f201c25df7cac61ca
--- /dev/null
+++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/__init__.py
@@ -0,0 +1,9 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantid workbench.
+#
+#
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/__main__.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/__main__.py
new file mode 100644
index 0000000000000000000000000000000000000000..db4d4160e01d4dd435662352eb24a0ac656baae8
--- /dev/null
+++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/__main__.py
@@ -0,0 +1,27 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantid workbench.
+#
+#
+
+# To Run - target this package with PyCharm, and __main__ will be executed
+
+import matplotlib
+
+matplotlib.use('Qt5Agg')
+
+from qtpy.QtWidgets import QApplication  # noqa: F402
+
+from mantid.simpleapi import Load  # noqa: F402
+from mantidqt.widgets.tableworkspacedisplay.presenter import TableWorkspaceDisplay  # noqa: F402
+from workbench.plotting.functions import plot  # noqa: F402
+
+app = QApplication([])
+# DEEE_WS_MON = Load("SavedTableWorkspace.nxs")
+DEEE_WS_MON = Load("SmallPeakWS10.nxs")
+window = TableWorkspaceDisplay(DEEE_WS_MON, plot)
+app.exec_()
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/model.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/model.py
new file mode 100644
index 0000000000000000000000000000000000000000..be03f5bdb6e50052a3d2147cf1c6f202cf53b190
--- /dev/null
+++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/model.py
@@ -0,0 +1,42 @@
+# coding=utf-8
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantid workbench.
+#
+#
+from __future__ import (absolute_import, division, print_function)
+
+from mantid.dataobjects import PeaksWorkspace, TableWorkspace
+
+
+class TableWorkspaceDisplayModel(object):
+    SPECTRUM_PLOT_LEGEND_STRING = '{}-{}'
+    BIN_PLOT_LEGEND_STRING = '{}-bin-{}'
+
+    def __init__(self, ws):
+        if not isinstance(ws, TableWorkspace) and not isinstance(ws, PeaksWorkspace):
+            raise ValueError("The workspace type is not supported: {0}".format(type(ws)))
+
+        self._ws = ws
+
+    def get_name(self):
+        return self._ws.name()
+
+    def get_column_headers(self):
+        return self._ws.getColumnNames()
+
+    def get_column(self, index):
+        return self._ws.column(index)
+
+    def get_number_of_rows(self):
+        return self._ws.rowCount()
+
+    def get_number_of_columns(self):
+        return self._ws.columnCount()
+
+    def is_peaks_workspace(self):
+        return isinstance(self._ws, PeaksWorkspace)
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/presenter.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/presenter.py
new file mode 100644
index 0000000000000000000000000000000000000000..f356745477e8dd98c5dc0a9f9d357954ae9861b1
--- /dev/null
+++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/presenter.py
@@ -0,0 +1,103 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantid workbench.
+#
+#
+from __future__ import absolute_import, division, print_function
+
+from qtpy.QtCore import Qt
+from qtpy.QtWidgets import QTableWidgetItem
+
+from mantidqt.widgets.common.table_copying import copy_cells
+from .model import TableWorkspaceDisplayModel
+from .view import TableWorkspaceDisplayView
+
+
+class TableWorkspaceDisplay(object):
+    A_LOT_OF_THINGS_TO_PLOT_MESSAGE = "You selected {} spectra to plot. Are you sure you want to plot that many?"
+    NUM_SELECTED_FOR_CONFIRMATION = 10
+
+    def __init__(self, ws, plot=None, parent=None, model=None, view=None):
+        # Create model and view, or accept mocked versions
+        self.model = model if model else TableWorkspaceDisplayModel(ws)
+        self.view = view if view else TableWorkspaceDisplayView(self, parent, self.model.get_name())
+        self.plot = plot
+        self.view.set_context_menu_actions(self.view)
+        column_headers = self.model.get_column_headers()
+        self.view.setColumnCount(len(column_headers))
+        self.view.setHorizontalHeaderLabels(["{}[Y]".format(x) for x in column_headers])
+        self.load_data(self.view, self.model.is_peaks_workspace())
+
+    def load_data(self, table, peaks_workspace=False):
+        num_rows = self.model.get_number_of_rows()
+        table.setRowCount(num_rows)
+
+        num_cols = self.model.get_number_of_columns()
+        for col in range(num_cols):
+            column_data = self.model.get_column(col)
+            for row in range(num_rows):
+                item = QTableWidgetItem(str(column_data[row]))
+                if not peaks_workspace or (col != 0 and col != 2 and col != 3 and col != 4):
+                    item.setFlags(item.flags() & ~Qt.ItemIsEditable)
+                table.setItem(row, col, item)
+
+    def action_copy_cells(self, table):
+        copy_cells(table)
+
+    def action_copy_bin_values(self, table):
+        copy_cells(table)
+
+    def action_copy_spectrum_values(self, table):
+        copy_cells(table)
+
+    def action_keypress_copy(self, table):
+        copy_cells(table)
+    # def _do_action_plot(self, table, axis, get_index, plot_errors=False):
+    #     if self.plot is None:
+    #         raise ValueError("Trying to do a plot, but no plotting class dependency was injected in the constructor")
+    #     selection_model = table.selectionModel()
+    #     if not selection_model.hasSelection():
+    #         self.show_no_selection_to_copy_toast()
+    #         return
+    #
+    #     if axis == MantidAxType.SPECTRUM:
+    #         selected = selection_model.selectedRows()  # type: list
+    #     else:
+    #         selected = selection_model.selectedColumns()  # type: list
+    #
+    #     if len(selected) > self.NUM_SELECTED_FOR_CONFIRMATION and not self.view.ask_confirmation(
+    #             self.A_LOT_OF_THINGS_TO_PLOT_MESSAGE.format(len(selected))):
+    #         return
+    #
+    #     plot_kwargs = {"capsize": 3} if plot_errors else {}
+    #     plot_kwargs["axis"] = axis
+    #
+    #     ws_list = [self.model._ws]
+    #     self.plot(ws_list, wksp_indices=[get_index(index) for index in selected], errors=plot_errors,
+    #               plot_kwargs=plot_kwargs)
+    #
+    # def action_plot_spectrum(self, table):
+    #     self._do_action_plot(table, MantidAxType.SPECTRUM, lambda index: index.row())
+    #
+    # def action_plot_spectrum_with_errors(self, table):
+    #     self._do_action_plot(table, MantidAxType.SPECTRUM, lambda index: index.row(), plot_errors=True)
+    #
+    # def action_plot_bin(self, table):
+    #     self._do_action_plot(table, MantidAxType.BIN, lambda index: index.column())
+    #
+    # def action_plot_bin_with_errors(self, table):
+    #     self._do_action_plot(table, MantidAxType.BIN, lambda index: index.column(), plot_errors=True)
+
+    # def _get_ws_read_from_type(self, type):
+    #     if type == TableWorkspaceTableViewModelType.y:
+    #         return self.model._ws.readY
+    #     elif type == TableWorkspaceTableViewModelType.x:
+    #         return self.model._ws.readX
+    #     elif type == TableWorkspaceTableViewModelType.e:
+    #         return self.model._ws.readE
+    #     else:
+    #         raise ValueError("Unknown TableViewModel type {}".format(type))
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/table_view_model.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/table_view_model.py
new file mode 100644
index 0000000000000000000000000000000000000000..64aca887eb963f753f43f8f7e3e929b2caf78393
--- /dev/null
+++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/table_view_model.py
@@ -0,0 +1,208 @@
+# coding=utf-8
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantid workbench.
+#
+#
+from __future__ import (absolute_import, division, print_function)
+
+from qtpy import QtGui
+from qtpy.QtCore import QVariant, Qt, QAbstractTableModel
+from mantid.py3compat import Enum
+
+
+class TableWorkspaceTableViewModelType(Enum):
+    x = 'x'
+    y = 'y'
+    e = 'e'
+
+
+class TableWorkspaceTableViewModel(QAbstractTableModel):
+    HORIZONTAL_HEADER_DISPLAY_STRING = u"{0}\n{1:0.1f}{2}"
+    HORIZONTAL_HEADER_TOOLTIP_STRING = u"index {0}\n{1} {2:0.1f}{3} (bin centre)"
+
+    HORIZONTAL_HEADER_DISPLAY_STRING_FOR_X_VALUES = "{0}"
+    HORIZONTAL_HEADER_TOOLTIP_STRING_FOR_X_VALUES = "index {0}"
+
+    VERTICAL_HEADER_DISPLAY_STRING = "{0} {1}"
+    VERTICAL_HEADER_TOOLTIP_STRING = "index {0}\nspectra no {1}"
+
+    HORIZONTAL_BINS_VARY_DISPLAY_STRING = "{0}\nbins vary"
+    HORIZONTAL_BINS_VARY_TOOLTIP_STRING = "index {0}\nbin centre value varies\nRebin to set common bins"
+
+    MASKED_MONITOR_ROW_STRING = "This is a masked monitor spectrum. "
+    MASKED_ROW_STRING = "This is a masked spectrum. "
+
+    MONITOR_ROW_STRING = "This is a monitor spectrum. "
+    MASKED_BIN_STRING = "This bin is masked. "
+
+    def __init__(self, ws, model_type):
+        """
+        :param ws:
+        :param model_type: TableWorkspaceTableViewModelType
+        :type model_type: TableWorkspaceTableViewModelType
+        """
+        assert model_type in [TableWorkspaceTableViewModelType.x, TableWorkspaceTableViewModelType.y,
+                              TableWorkspaceTableViewModelType.e], "The Model type must be either X, Y or E."
+
+        super(TableWorkspaceTableViewModel, self).__init__()
+
+        self.ws = ws
+        self.ws_spectrum_info = self.ws.spectrumInfo()
+        self.row_count = self.ws.getNumberHistograms()
+        self.column_count = self.ws.blocksize()
+
+        self.masked_rows_cache = []
+        self.monitor_rows_cache = []
+        self.masked_bins_cache = {}
+
+        self.masked_color = QtGui.QColor(240, 240, 240)
+
+        self.monitor_color = QtGui.QColor(255, 253, 209)
+
+        self.type = model_type
+        if self.type == TableWorkspaceTableViewModelType.x:
+            self.relevant_data = self.ws.readX
+        elif self.type == TableWorkspaceTableViewModelType.y:
+            self.relevant_data = self.ws.readY
+        elif self.type == TableWorkspaceTableViewModelType.e:
+            self.relevant_data = self.ws.readE
+        else:
+            raise ValueError("Unknown model type {0}".format(self.type))
+
+    def _makeVerticalHeader(self, section, role):
+        axis_index = 1
+        # check that the vertical axis actually exists in the workspace
+        if self.ws.axes() > axis_index:
+            if role == Qt.DisplayRole:
+                return self.VERTICAL_HEADER_DISPLAY_STRING.format(section, self.ws.getAxis(axis_index).label(section))
+            else:
+                spectrum_number = self.ws.getSpectrum(section).getSpectrumNo()
+                return self.VERTICAL_HEADER_TOOLTIP_STRING.format(section, spectrum_number)
+        else:
+            raise NotImplementedError("What do we do here? Handle if the vertical axis does NOT exist")
+
+    def _makeHorizontalHeader(self, section, role):
+        """
+
+        :param section: The workspace index or bin number
+        :param role: Qt.DisplayRole - is the label for the header
+                      or Qt.TooltipRole - is the tooltip for the header when moused over
+        :return: The formatted header string
+        """
+        # X values get simpler labels
+        if self.type == TableWorkspaceTableViewModelType.x:
+            if role == Qt.DisplayRole:
+                return self.HORIZONTAL_HEADER_DISPLAY_STRING_FOR_X_VALUES.format(section)
+            else:
+                # format for the tooltip
+                return self.HORIZONTAL_HEADER_TOOLTIP_STRING_FOR_X_VALUES.format(section)
+
+        if not self.ws.isCommonBins():
+            if role == Qt.DisplayRole:
+                return self.HORIZONTAL_BINS_VARY_DISPLAY_STRING.format(section)
+            else:
+                # format for the tooltip
+                return self.HORIZONTAL_BINS_VARY_TOOLTIP_STRING.format(section)
+
+        # for the Y and E values, create a label with the units
+        axis_index = 0
+        x_vec = self.ws.readX(0)
+        if self.ws.isHistogramData():
+            bin_centre_value = (x_vec[section] + x_vec[section + 1]) / 2.0
+        else:
+            bin_centre_value = x_vec[section]
+
+        unit = self.ws.getAxis(axis_index).getUnit()
+        if role == Qt.DisplayRole:
+            return self.HORIZONTAL_HEADER_DISPLAY_STRING.format(section, bin_centre_value, unit.symbol().utf8())
+        else:
+            # format for the tooltip
+            return self.HORIZONTAL_HEADER_TOOLTIP_STRING.format(section, unit.caption(), bin_centre_value,
+                                                                unit.symbol().utf8())
+
+    def headerData(self, section, orientation, role=None):
+        if not (role == Qt.DisplayRole or role == Qt.ToolTipRole):
+            return QVariant()
+
+        if orientation == Qt.Vertical:
+            return self._makeVerticalHeader(section, role)
+        else:
+            return self._makeHorizontalHeader(section, role)
+
+    def rowCount(self, parent=None, *args, **kwargs):
+        return self.row_count
+
+    def columnCount(self, parent=None, *args, **kwargs):
+        return self.column_count
+
+    def data(self, index, role=None):
+        row = index.row()
+        if role == Qt.DisplayRole:
+            # DisplayRole determines the text of each cell
+            return str(self.relevant_data(row)[index.column()])
+        elif role == Qt.BackgroundRole:
+            # BackgroundRole determines the background of each cell
+
+            # Checks if the row is MASKED, if so makes it the specified color for masked
+            # The check for masked rows should be first as a monitor row can be masked as well - and we want it to be
+            # colored as a masked row, rather than as a monitor row.
+            # First do the check in the cache, and only if not present go through SpectrumInfo and cache it. This logic
+            # is repeated in the other checks below
+            if self.checkMaskedCache(row):
+                return self.masked_color
+
+            # Checks if the row is a MONITOR, if so makes it the specified color for monitors
+            elif self.checkMonitorCache(row):
+                return self.monitor_color
+
+            # Checks if the BIN is MASKED, if so makes it the specified color for masked
+            elif self.checkMaskedBinCache(row, index):
+                return self.masked_color
+
+        elif role == Qt.ToolTipRole:
+            tooltip = QVariant()
+            if self.checkMaskedCache(row):
+                if self.checkMonitorCache(row):
+                    tooltip = self.MASKED_MONITOR_ROW_STRING
+                else:
+                    tooltip = self.MASKED_ROW_STRING
+            elif self.checkMonitorCache(row):
+                tooltip = self.MONITOR_ROW_STRING
+                if self.checkMaskedBinCache(row, index):
+                    tooltip += self.MASKED_BIN_STRING
+            elif self.checkMaskedBinCache(row, index):
+                tooltip = self.MASKED_BIN_STRING
+            return tooltip
+        else:
+            return QVariant()
+
+    def checkMaskedCache(self, row):
+        if row in self.masked_rows_cache:
+            return True
+        elif self.ws_spectrum_info.hasDetectors(row) and self.ws_spectrum_info.isMasked(row):
+            self.masked_rows_cache.append(row)
+            return True
+
+    def checkMonitorCache(self, row):
+        if row in self.monitor_rows_cache:
+            return True
+        elif self.ws_spectrum_info.hasDetectors(row) and self.ws_spectrum_info.isMonitor(row):
+            self.monitor_rows_cache.append(row)
+            return True
+
+    def checkMaskedBinCache(self, row, index):
+        if row in self.masked_bins_cache:
+            # retrieve the masked bins IDs from the cache
+            if index.column() in self.masked_bins_cache[row]:
+                return True
+
+        elif self.ws.hasMaskedBins(row):
+            masked_bins = self.ws.maskedBinsIndices(row)
+            if index.column() in masked_bins:
+                self.masked_bins_cache[row] = masked_bins
+                return True
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/test/test_tableworkspacedisplay_model.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/test/test_tableworkspacedisplay_model.py
new file mode 100644
index 0000000000000000000000000000000000000000..7a7905053c36a05c500d065d2ab8d519f138723e
--- /dev/null
+++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/test/test_tableworkspacedisplay_model.py
@@ -0,0 +1,66 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantid workbench.
+#
+#
+from __future__ import (absolute_import, division, print_function)
+
+import unittest
+
+from mock import Mock
+
+from mantid.simpleapi import CreateSampleWorkspace
+from mantidqt.widgets.TableWorkspacedisplay.model import TableWorkspaceDisplayModel
+from mantidqt.widgets.TableWorkspacedisplay.table_view_model import TableWorkspaceTableViewModelType
+from mantidqt.widgets.TableWorkspacedisplay.test_helpers.TableWorkspacedisplay_common import \
+    MockWorkspace
+
+
+class TableWorkspaceDisplayModelTest(unittest.TestCase):
+
+    def test_get_name(self):
+        ws = MockWorkspace()
+        expected_name = "TEST_WORKSPACE"
+        ws.name = Mock(return_value=expected_name)
+        model = TableWorkspaceDisplayModel(ws)
+
+        self.assertEqual(expected_name, model.get_name())
+
+    def test_get_item_model(self):
+        ws = MockWorkspace()
+        expected_name = "TEST_WORKSPACE"
+        ws.name = Mock(return_value=expected_name)
+        model = TableWorkspaceDisplayModel(ws)
+
+        x_model, y_model, e_model = model.get_item_model()
+
+        self.assertEqual(x_model.type, TableWorkspaceTableViewModelType.x)
+        self.assertEqual(y_model.type, TableWorkspaceTableViewModelType.y)
+        self.assertEqual(e_model.type, TableWorkspaceTableViewModelType.e)
+
+    def test_raises_with_unsupported_workspace(self):
+        # ws = MockWorkspace()
+        # expected_name = "TEST_WORKSPACE"
+        # ws.name = Mock(return_value=expected_name)
+        self.assertRaises(ValueError, lambda: TableWorkspaceDisplayModel([]))
+        self.assertRaises(ValueError, lambda: TableWorkspaceDisplayModel(1))
+        self.assertRaises(ValueError, lambda: TableWorkspaceDisplayModel("test_string"))
+
+    def test_no_raise_with_supported_workspace(self):
+        ws = MockWorkspace()
+        expected_name = "TEST_WORKSPACE"
+        ws.name = Mock(return_value=expected_name)
+
+        # no need to assert anything - if the constructor raises the test will fail
+        TableWorkspaceDisplayModel(ws)
+
+        ws = CreateSampleWorkspace(NumBanks=1, BankPixelWidth=4, NumEvents=10)
+        TableWorkspaceDisplayModel(ws)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/test/test_tableworkspacedisplay_presenter.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/test/test_tableworkspacedisplay_presenter.py
new file mode 100644
index 0000000000000000000000000000000000000000..b41ab6e8d886ac58cf2b70bf285aff9f0621c641
--- /dev/null
+++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/test/test_tableworkspacedisplay_presenter.py
@@ -0,0 +1,297 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantid workbench.
+#
+#
+from __future__ import (absolute_import, division, print_function)
+
+import unittest
+
+from mock import Mock
+
+from mantidqt.widgets.TableWorkspacedisplay.presenter import TableWorkspaceDisplay
+from mantidqt.widgets.TableWorkspacedisplay.test_helpers.TableWorkspacedisplay_common import MockQModelIndex, \
+    MockWorkspace
+from mantidqt.widgets.TableWorkspacedisplay.test_helpers.mock_TableWorkspacedisplay import \
+    MockTableWorkspaceDisplayView, MockQTableView
+
+
+class TableWorkspaceDisplayPresenterTest(unittest.TestCase):
+    def assertNotCalled(self, mock):
+        self.assertEqual(0, mock.call_count)
+
+    def test_setup_table(self):
+        ws = MockWorkspace()
+        view = MockTableWorkspaceDisplayView()
+        TableWorkspaceDisplay(ws, view=view)
+        self.assertEqual(3, view.set_context_menu_actions.call_count)
+        self.assertEqual(1, view.set_model.call_count)
+
+    def test_action_copy_spectrum_values(self):
+        ws = MockWorkspace()
+        view = MockTableWorkspaceDisplayView()
+        presenter = TableWorkspaceDisplay(ws, view=view)
+
+        mock_table = MockQTableView()
+
+        # two rows are selected in different positions
+        mock_indexes = [MockQModelIndex(0, 1), MockQModelIndex(3, 1)]
+        mock_table.mock_selection_model.selectedRows = Mock(return_value=mock_indexes)
+
+        mock_read = Mock(return_value=[43, 99])
+        presenter._get_ws_read_from_type = Mock(return_value=mock_read)
+        expected_string = "43\t99\n43\t99"
+
+        presenter.action_copy_spectrum_values(mock_table)
+
+        mock_table.selectionModel.assert_called_once_with()
+        mock_table.mock_selection_model.hasSelection.assert_called_once_with()
+        view.copy_to_clipboard.assert_called_once_with(expected_string)
+        view.show_mouse_toast.assert_called_once_with(TableWorkspaceDisplay.COPY_SUCCESSFUL_MESSAGE)
+
+    def test_action_copy_spectrum_values_no_selection(self):
+        ws = MockWorkspace()
+        view = MockTableWorkspaceDisplayView()
+        presenter = TableWorkspaceDisplay(ws, view=view)
+
+        mock_table = MockQTableView()
+        mock_table.mock_selection_model.hasSelection = Mock(return_value=False)
+        mock_table.mock_selection_model.selectedRows = Mock()
+
+        presenter.action_copy_spectrum_values(mock_table)
+
+        mock_table.selectionModel.assert_called_once_with()
+        mock_table.mock_selection_model.hasSelection.assert_called_once_with()
+        # the action should never look for rows if there is no selection
+        self.assertNotCalled(mock_table.mock_selection_model.selectedRows)
+        view.show_mouse_toast.assert_called_once_with(TableWorkspaceDisplay.NO_SELECTION_MESSAGE)
+
+    def test_action_copy_bin_values(self):
+        ws = MockWorkspace()
+        view = MockTableWorkspaceDisplayView()
+        presenter = TableWorkspaceDisplay(ws, view=view)
+        mock_table = MockQTableView()
+
+        # two columns are selected at different positions
+        mock_indexes = [MockQModelIndex(0, 0), MockQModelIndex(0, 3)]
+        mock_table.mock_selection_model.selectedColumns = Mock(return_value=mock_indexes)
+        # change the mock ws to have 3 histograms
+        ws.getNumberHistograms = Mock(return_value=3)
+
+        mock_read = Mock(return_value=[83, 11, 33, 70])
+        presenter._get_ws_read_from_type = Mock(return_value=mock_read)
+        expected_string = "83\t70\n83\t70\n83\t70"
+
+        presenter.action_copy_bin_values(mock_table)
+
+        mock_table.selectionModel.assert_called_once_with()
+        view.copy_to_clipboard.assert_called_once_with(expected_string)
+        mock_table.mock_selection_model.hasSelection.assert_called_once_with()
+        view.show_mouse_toast.assert_called_once_with(TableWorkspaceDisplay.COPY_SUCCESSFUL_MESSAGE)
+
+    def test_action_copy_bin_values_no_selection(self):
+        ws = MockWorkspace()
+        view = MockTableWorkspaceDisplayView()
+        presenter = TableWorkspaceDisplay(ws, view=view)
+
+        mock_table = MockQTableView()
+        mock_table.mock_selection_model.hasSelection = Mock(return_value=False)
+        mock_table.mock_selection_model.selectedColumns = Mock()
+
+        presenter.action_copy_bin_values(mock_table)
+
+        mock_table.selectionModel.assert_called_once_with()
+        mock_table.mock_selection_model.hasSelection.assert_called_once_with()
+        # the action should never look for rows if there is no selection
+        self.assertNotCalled(mock_table.mock_selection_model.selectedColumns)
+        view.show_mouse_toast.assert_called_once_with(TableWorkspaceDisplay.NO_SELECTION_MESSAGE)
+
+    def test_action_copy_cell(self):
+        ws = MockWorkspace()
+        view = MockTableWorkspaceDisplayView()
+        presenter = TableWorkspaceDisplay(ws, view=view)
+        mock_table = MockQTableView()
+
+        # two columns are selected at different positions
+        mock_index = MockQModelIndex(None, None)
+        mock_table.mock_selection_model.currentIndex = Mock(return_value=mock_index)
+
+        presenter.action_copy_cells(mock_table)
+
+        mock_table.selectionModel.assert_called_once_with()
+        self.assertEqual(1, view.copy_to_clipboard.call_count)
+        self.assertEqual(9, mock_index.sibling.call_count)
+        view.show_mouse_toast.assert_called_once_with(TableWorkspaceDisplay.COPY_SUCCESSFUL_MESSAGE)
+
+    def test_action_copy_cell_no_selection(self):
+        ws = MockWorkspace()
+        view = MockTableWorkspaceDisplayView()
+        presenter = TableWorkspaceDisplay(ws, view=view)
+        mock_table = MockQTableView()
+        mock_table.mock_selection_model.hasSelection = Mock(return_value=False)
+
+        presenter.action_copy_cells(mock_table)
+
+        mock_table.selectionModel.assert_called_once_with()
+        mock_table.mock_selection_model.hasSelection.assert_called_once_with()
+        view.show_mouse_toast.assert_called_once_with(TableWorkspaceDisplay.NO_SELECTION_MESSAGE)
+
+        self.assertNotCalled(view.copy_to_clipboard)
+
+    def common_setup_action_plot(self, table_has_selection=True):
+        mock_ws = MockWorkspace()
+        mock_view = MockTableWorkspaceDisplayView()
+        mock_plotter = Mock()
+        presenter = TableWorkspaceDisplay(mock_ws, plot=mock_plotter, view=mock_view)
+
+        # monkey-patch the spectrum plot label to count the number of calls
+        presenter.model.get_spectrum_plot_label = Mock()
+        presenter.model.get_bin_plot_label = Mock()
+
+        mock_table = MockQTableView()
+        # configure the mock return values
+        mock_table.mock_selection_model.hasSelection = Mock(return_value=table_has_selection)
+        return mock_plotter, mock_table, mock_view, presenter
+
+    def setup_mock_selection(self, mock_table, num_selected_rows=None, num_selected_cols=None):
+        """
+        :type mock_table: MockQTableView
+        :type num_selected_rows: int|None
+        :type num_selected_cols: int|None
+        """
+        mock_selected = []
+        if num_selected_rows is not None:
+            for i in range(num_selected_rows):
+                mock_selected.append(MockQModelIndex(i, 1))
+            mock_table.mock_selection_model.selectedRows = Mock(return_value=mock_selected)
+            mock_table.mock_selection_model.selectedColumns = Mock()
+        elif num_selected_cols is not None:
+            for i in range(num_selected_cols):
+                mock_selected.append(MockQModelIndex(1, i))
+            mock_table.mock_selection_model.selectedRows = Mock()
+            mock_table.mock_selection_model.selectedColumns = Mock(return_value=mock_selected)
+        else:
+            mock_table.mock_selection_model.selectedRows = Mock()
+            mock_table.mock_selection_model.selectedColumns = Mock()
+        return mock_selected
+
+    def test_action_plot_spectrum_plot_many_confirmed(self):
+        mock_plot, mock_table, mock_view, presenter = self.common_setup_action_plot()
+        num_selected_rows = TableWorkspaceDisplay.NUM_SELECTED_FOR_CONFIRMATION + 1
+
+        self.setup_mock_selection(mock_table, num_selected_rows)
+
+        # The a lot of things to plot message will show, set that the user will CONFIRM the plot
+        # meaning the rest of the function will execute as normal
+        mock_view.ask_confirmation = Mock(return_value=True)
+
+        presenter.action_plot_spectrum(mock_table)
+
+        mock_table.selectionModel.assert_called_once_with()
+        mock_table.mock_selection_model.hasSelection.assert_called_once_with()
+        mock_table.mock_selection_model.selectedRows.assert_called_once_with()
+
+        mock_view.ask_confirmation.assert_called_once_with(
+            TableWorkspaceDisplay.A_LOT_OF_THINGS_TO_PLOT_MESSAGE.format(num_selected_rows))
+
+        self.assertNotCalled(mock_table.mock_selection_model.selectedColumns)
+        self.assertEqual(1, mock_plot.call_count)
+
+    def test_action_plot_spectrum_plot_many_denied(self):
+        mock_plot, mock_table, mock_view, presenter = self.common_setup_action_plot()
+        num_selected_rows = TableWorkspaceDisplay.NUM_SELECTED_FOR_CONFIRMATION + 1
+
+        # return value unused as most of the function being tested is not executed
+        self.setup_mock_selection(mock_table, num_selected_rows)
+
+        # The a lot of things to plot message will show, set that the user will DENY the plot
+        # meaning the rest of the function will NOT EXECUTE AT ALL
+        mock_view.ask_confirmation = Mock(return_value=False)
+
+        presenter.action_plot_spectrum(mock_table)
+
+        mock_table.selectionModel.assert_called_once_with()
+        mock_table.mock_selection_model.hasSelection.assert_called_once_with()
+        mock_table.mock_selection_model.selectedRows.assert_called_once_with()
+
+        mock_view.ask_confirmation.assert_called_once_with(
+            TableWorkspaceDisplay.A_LOT_OF_THINGS_TO_PLOT_MESSAGE.format(num_selected_rows))
+
+        self.assertNotCalled(mock_table.mock_selection_model.selectedColumns)
+        self.assertNotCalled(mock_plot)
+
+    def test_action_plot_spectrum_no_selection(self):
+        mock_plot, mock_table, mock_view, presenter = self.common_setup_action_plot(table_has_selection=False)
+
+        mock_table.mock_selection_model.selectedRows = Mock()
+        mock_table.mock_selection_model.selectedColumns = Mock()
+
+        presenter.action_plot_spectrum(mock_table)
+
+        mock_view.show_mouse_toast.assert_called_once_with(TableWorkspaceDisplay.NO_SELECTION_MESSAGE)
+        mock_table.selectionModel.assert_called_once_with()
+        mock_table.mock_selection_model.hasSelection.assert_called_once_with()
+
+        self.assertNotCalled(mock_table.mock_selection_model.selectedRows)
+        self.assertNotCalled(mock_table.mock_selection_model.selectedColumns)
+        self.assertNotCalled(mock_plot)
+
+    def test_action_plot_bin_plot_many_confirmed(self):
+        mock_plot, mock_table, mock_view, presenter = self.common_setup_action_plot()
+        num_selected_cols = TableWorkspaceDisplay.NUM_SELECTED_FOR_CONFIRMATION + 1
+        self.setup_mock_selection(mock_table, num_selected_cols=num_selected_cols)
+        mock_view.ask_confirmation = Mock(return_value=True)
+
+        presenter.action_plot_bin(mock_table)
+
+        mock_table.selectionModel.assert_called_once_with()
+        mock_table.mock_selection_model.hasSelection.assert_called_once_with()
+        mock_table.mock_selection_model.selectedColumns.assert_called_once_with()
+        self.assertNotCalled(mock_table.mock_selection_model.selectedRows)
+
+        mock_view.ask_confirmation.assert_called_once_with(
+            TableWorkspaceDisplay.A_LOT_OF_THINGS_TO_PLOT_MESSAGE.format(num_selected_cols))
+        self.assertEqual(1, mock_plot.call_count)
+
+    def test_action_plot_bin_plot_many_denied(self):
+        mock_plot, mock_table, mock_view, presenter = self.common_setup_action_plot()
+        num_selected_cols = TableWorkspaceDisplay.NUM_SELECTED_FOR_CONFIRMATION + 1
+
+        # return value unused as most of the function being tested is not executed
+        self.setup_mock_selection(mock_table, num_selected_cols=num_selected_cols)
+
+        mock_view.ask_confirmation = Mock(return_value=False)
+
+        presenter.action_plot_bin(mock_table)
+
+        mock_table.selectionModel.assert_called_once_with()
+        mock_table.mock_selection_model.hasSelection.assert_called_once_with()
+        mock_table.mock_selection_model.selectedColumns.assert_called_once_with()
+
+        mock_view.ask_confirmation.assert_called_once_with(
+            TableWorkspaceDisplay.A_LOT_OF_THINGS_TO_PLOT_MESSAGE.format(num_selected_cols))
+
+        self.assertNotCalled(mock_table.mock_selection_model.selectedRows)
+        self.assertNotCalled(mock_plot)
+
+    def test_action_plot_bin_no_selection(self):
+        mock_plot, mock_table, mock_view, presenter = self.common_setup_action_plot(table_has_selection=False)
+        self.setup_mock_selection(mock_table, num_selected_rows=None, num_selected_cols=None)
+
+        presenter.action_plot_bin(mock_table)
+
+        mock_view.show_mouse_toast.assert_called_once_with(TableWorkspaceDisplay.NO_SELECTION_MESSAGE)
+        mock_table.selectionModel.assert_called_once_with()
+        mock_table.mock_selection_model.hasSelection.assert_called_once_with()
+
+        self.assertNotCalled(mock_table.mock_selection_model.selectedRows)
+        self.assertNotCalled(mock_table.mock_selection_model.selectedColumns)
+        self.assertNotCalled(mock_plot)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/test/test_tableworkspacedisplay_tableviewmodel.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/test/test_tableworkspacedisplay_tableviewmodel.py
new file mode 100644
index 0000000000000000000000000000000000000000..37e8ece30142f782cb88c8bbb363664652f51676
--- /dev/null
+++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/test/test_tableworkspacedisplay_tableviewmodel.py
@@ -0,0 +1,488 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantid workbench.
+#
+#
+from __future__ import absolute_import, absolute_import, division, division, print_function, print_function
+
+import unittest
+
+import qtpy
+from mock import Mock, call
+from qtpy import QtCore
+from qtpy.QtCore import Qt
+
+from mantidqt.widgets.TableWorkspacedisplay.table_view_model import TableWorkspaceTableViewModel, \
+    TableWorkspaceTableViewModelType
+from mantidqt.widgets.TableWorkspacedisplay.test_helpers.TableWorkspacedisplay_common import \
+    MockQModelIndex, MockWorkspace, setup_common_for_test_data, AXIS_INDEX_FOR_VERTICAL, MockMantidAxis, MockSpectrum, \
+    MockMantidSymbol, AXIS_INDEX_FOR_HORIZONTAL, MockMantidUnit
+
+
+class TableWorkspaceDisplayTableViewModelTest(unittest.TestCase):
+    WORKSPACE = r"C:\Users\qbr77747\dev\m\workbench_TableWorkspace\test_masked_bins.nxs"
+
+    def test_correct_model_type(self):
+        ws = MockWorkspace()
+        model = TableWorkspaceTableViewModel(ws, TableWorkspaceTableViewModelType.x)
+        self.assertEqual(model.type, TableWorkspaceTableViewModelType.x)
+
+        model = TableWorkspaceTableViewModel(ws, TableWorkspaceTableViewModelType.y)
+        self.assertEqual(model.type, TableWorkspaceTableViewModelType.y)
+
+        model = TableWorkspaceTableViewModel(ws, TableWorkspaceTableViewModelType.e)
+        self.assertEqual(model.type, TableWorkspaceTableViewModelType.e)
+
+    def test_correct_cell_colors(self):
+        ws = MockWorkspace()
+        model = TableWorkspaceTableViewModel(ws, TableWorkspaceTableViewModelType.x)
+        self.assertEqual((240, 240, 240, 255), model.masked_color.getRgb())
+        self.assertEqual((255, 253, 209, 255), model.monitor_color.getRgb())
+
+    def test_correct_relevant_data(self):
+        ws = MockWorkspace()
+        model = TableWorkspaceTableViewModel(ws, TableWorkspaceTableViewModelType.x)
+        msg = "The function is not set correctly! The wrong data will be read."
+        self.assertEqual(ws.readX, model.relevant_data, msg=msg)
+        model = TableWorkspaceTableViewModel(ws, TableWorkspaceTableViewModelType.y)
+        self.assertEqual(ws.readY, model.relevant_data, msg=msg)
+        model = TableWorkspaceTableViewModel(ws, TableWorkspaceTableViewModelType.e)
+        self.assertEqual(ws.readE, model.relevant_data, msg=msg)
+
+    def test_invalid_model_type(self):
+        ws = MockWorkspace()
+        with self.assertRaises(AssertionError):
+            TableWorkspaceTableViewModel(ws, "My Model Type")
+
+    def test_data_display_role(self):
+        # Create some mock data for the mock workspace
+        row = 2
+        column = 2
+        # make a workspace with 0s
+        mock_data = [0] * 10
+        # set one of them to be not 0
+        mock_data[column] = 999
+        # pass onto the MockWorkspace so that it returns it when read from the TableViewModel
+        self._check_correct_data_is_displayed(TableWorkspaceTableViewModelType.x, column, mock_data, row)
+        self._check_correct_data_is_displayed(TableWorkspaceTableViewModelType.y, column, mock_data, row)
+        self._check_correct_data_is_displayed(TableWorkspaceTableViewModelType.e, column, mock_data, row)
+
+    def _check_correct_data_is_displayed(self, model_type, column, mock_data, row):
+        ws = MockWorkspace(read_return=mock_data)
+        model = TableWorkspaceTableViewModel(ws, model_type)
+        index = MockQModelIndex(row, column)
+        output = model.data(index, Qt.DisplayRole)
+        model.relevant_data.assert_called_once_with(row)
+        self.assertEqual(str(mock_data[column]), output)
+
+    def test_row_and_column_count(self):
+        ws = MockWorkspace()
+        model_type = TableWorkspaceTableViewModelType.x
+        TableWorkspaceTableViewModel(ws, model_type)
+        # these are called when the TableViewModel is initialised
+        ws.getNumberHistograms.assert_called_once_with()
+        ws.blocksize.assert_called_once_with()
+
+    def test_data_background_role_masked_row(self):
+        ws, model, row, index = setup_common_for_test_data()
+
+        model.ws_spectrum_info.isMasked = Mock(return_value=True)
+
+        output = model.data(index, Qt.BackgroundRole)
+
+        model.ws_spectrum_info.hasDetectors.assert_called_once_with(row)
+        model.ws_spectrum_info.isMasked.assert_called_once_with(row)
+
+        index.row.assert_called_once_with()
+        self.assertFalse(index.column.called)
+
+        self.assertEqual(model.masked_color, output)
+
+        # Just do it a second time -> This time it's cached and should be read off the cache.
+        # If it is not read off the cache the assert_called_once below will fail,
+        # as the functions would be called a 2nd time
+        output = model.data(index, Qt.BackgroundRole)
+
+        model.ws_spectrum_info.hasDetectors.assert_called_once_with(row)
+        model.ws_spectrum_info.isMasked.assert_called_once_with(row)
+        self.assertEqual(model.masked_color, output)
+
+        # assert that the row was called twice with no parameters
+        self.assertEqual(2, index.row.call_count)
+        self.assertFalse(index.column.called)
+
+    def test_data_background_role_monitor_row(self):
+        ws, model, row, index = setup_common_for_test_data()
+
+        model.ws_spectrum_info.isMasked = Mock(return_value=False)
+        model.ws_spectrum_info.isMonitor = Mock(return_value=True)
+
+        output = model.data(index, Qt.BackgroundRole)
+
+        model.ws_spectrum_info.hasDetectors.assert_called_with(row)
+        model.ws_spectrum_info.isMasked.assert_called_once_with(row)
+        model.ws_spectrum_info.isMonitor.assert_called_once_with(row)
+
+        index.row.assert_called_once_with()
+        self.assertFalse(index.column.called)
+
+        self.assertEqual(model.monitor_color, output)
+
+        # Just do it a second time -> This time it's cached and should be read off the cache.
+        # If it is not read off the cache the assert_called_once below will fail,
+        # as the functions would be called a 2nd time
+        output = model.data(index, Qt.BackgroundRole)
+
+        model.ws_spectrum_info.hasDetectors.assert_called_with(row)
+        # assert that it has been called twice with the same parameters
+        model.ws_spectrum_info.isMasked.assert_has_calls([call.do_work(row), call.do_work(row)])
+        # only called once, as the 2nd time should have hit the cached monitor
+        model.ws_spectrum_info.isMonitor.assert_called_once_with(row)
+        self.assertEqual(model.monitor_color, output)
+
+        # assert that the row was called twice with no parameters
+        self.assertEqual(2, index.row.call_count)
+        self.assertFalse(index.column.called)
+
+    def test_data_background_role_masked_bin(self):
+        ws, model, row, index = setup_common_for_test_data()
+
+        model.ws_spectrum_info.isMasked = Mock(return_value=False)
+        model.ws_spectrum_info.isMonitor = Mock(return_value=False)
+
+        output = model.data(index, Qt.BackgroundRole)
+
+        model.ws_spectrum_info.hasDetectors.assert_called_with(row)
+        model.ws_spectrum_info.isMasked.assert_called_once_with(row)
+        model.ws_spectrum_info.isMonitor.assert_called_once_with(row)
+        ws.hasMaskedBins.assert_called_once_with(row)
+        ws.maskedBinsIndices.assert_called_once_with(row)
+
+        index.row.assert_called_once_with()
+        index.column.assert_called_once_with()
+
+        self.assertEqual(model.masked_color, output)
+        # Just do it a second time -> This time it's cached and should be read off the cache.
+        # If it is not read off the cache the assert_called_once below will fail,
+        # as the functions would be called a 2nd time
+        output = model.data(index, Qt.BackgroundRole)
+
+        # masked bins is checked last, so it will call all other functions a second time
+        model.ws_spectrum_info.hasDetectors.assert_has_calls([call.do_work(row), call.do_work(row)])
+        model.ws_spectrum_info.isMasked.assert_has_calls([call.do_work(row), call.do_work(row)])
+        model.ws_spectrum_info.isMonitor.assert_has_calls([call.do_work(row), call.do_work(row)])
+
+        # these, however, should remain at 1 call, as the masked bin cache should have been hit
+        ws.hasMaskedBins.assert_called_once_with(row)
+        ws.maskedBinsIndices.assert_called_once_with(row)
+
+        self.assertEqual(model.masked_color, output)
+
+        self.assertEqual(2, index.row.call_count)
+        self.assertEqual(2, index.column.call_count)
+
+    def test_data_tooltip_role_masked_row(self):
+        if not qtpy.PYQT5:
+            self.skipTest("QVariant cannot be instantiated in QT4, and the test fails with an error.")
+        ws, model, row, index = setup_common_for_test_data()
+
+        model.ws_spectrum_info.hasDetectors = Mock(return_value=True)
+        model.ws_spectrum_info.isMasked = Mock(return_value=True)
+        model.ws_spectrum_info.isMonitor = Mock(return_value=False)
+
+        output = model.data(index, Qt.ToolTipRole)
+
+        model.ws_spectrum_info.hasDetectors.assert_has_calls([call.do_work(row), call.do_work(row)])
+        model.ws_spectrum_info.isMasked.assert_called_once_with(row)
+        model.ws_spectrum_info.isMonitor.assert_called_once_with(row)
+
+        self.assertEqual(TableWorkspaceTableViewModel.MASKED_ROW_STRING, output)
+
+        output = model.data(index, Qt.ToolTipRole)
+
+        # The row was masked so it should have been cached
+        model.ws_spectrum_info.isMasked.assert_called_once_with(row)
+
+        # However it is checked if it is a monitor again
+        self.assertEqual(3, model.ws_spectrum_info.hasDetectors.call_count)
+        self.assertEqual(2, model.ws_spectrum_info.isMonitor.call_count)
+
+        self.assertEqual(TableWorkspaceTableViewModel.MASKED_ROW_STRING, output)
+
+    def test_data_tooltip_role_masked_monitor_row(self):
+        if not qtpy.PYQT5:
+            self.skipTest("QVariant cannot be instantiated in QT4, and the test fails with an error.")
+        ws, model, row, index = setup_common_for_test_data()
+
+        model.ws_spectrum_info.isMasked = Mock(return_value=True)
+        model.ws_spectrum_info.isMonitor = Mock(return_value=True)
+
+        output = model.data(index, Qt.ToolTipRole)
+
+        model.ws_spectrum_info.hasDetectors.assert_has_calls([call.do_work(row), call.do_work(row)])
+        model.ws_spectrum_info.isMasked.assert_called_once_with(row)
+        model.ws_spectrum_info.isMonitor.assert_called_once_with(row)
+
+        self.assertEqual(TableWorkspaceTableViewModel.MASKED_MONITOR_ROW_STRING, output)
+
+        # Doing the same thing a second time should hit the cache, so no additional calls will have been made
+        output = model.data(index, Qt.ToolTipRole)
+        model.ws_spectrum_info.hasDetectors.assert_has_calls([call.do_work(row), call.do_work(row)])
+        model.ws_spectrum_info.isMasked.assert_called_once_with(row)
+        model.ws_spectrum_info.isMonitor.assert_called_once_with(row)
+
+        self.assertEqual(TableWorkspaceTableViewModel.MASKED_MONITOR_ROW_STRING, output)
+
+    def test_data_tooltip_role_monitor_row(self):
+        if not qtpy.PYQT5:
+            self.skipTest("QVariant cannot be instantiated in QT4, and the test fails with an error.")
+        ws, model, row, index = setup_common_for_test_data()
+
+        # necessary otherwise it is returned that there is a masked bin, and we get the wrong output
+        ws.hasMaskedBins = Mock(return_value=False)
+
+        model.ws_spectrum_info.isMasked = Mock(return_value=False)
+        model.ws_spectrum_info.isMonitor = Mock(return_value=True)
+
+        output = model.data(index, Qt.ToolTipRole)
+
+        model.ws_spectrum_info.hasDetectors.assert_has_calls([call.do_work(row), call.do_work(row)])
+        model.ws_spectrum_info.isMasked.assert_called_once_with(row)
+        model.ws_spectrum_info.isMonitor.assert_called_once_with(row)
+
+        self.assertEqual(TableWorkspaceTableViewModel.MONITOR_ROW_STRING, output)
+
+        # Doing the same thing a second time should hit the cache, so no additional calls will have been made
+        output = model.data(index, Qt.ToolTipRole)
+        model.ws_spectrum_info.hasDetectors.assert_has_calls([call.do_work(row), call.do_work(row)])
+        self.assertEqual(2, model.ws_spectrum_info.isMasked.call_count)
+        # This was called only once because the monitor was cached
+        model.ws_spectrum_info.isMonitor.assert_called_once_with(row)
+
+        self.assertEqual(TableWorkspaceTableViewModel.MONITOR_ROW_STRING, output)
+
+    def test_data_tooltip_role_masked_bin_in_monitor_row(self):
+        if not qtpy.PYQT5:
+            self.skipTest("QVariant cannot be instantiated in QT4, and the test fails with an error.")
+
+        ws, model, row, index = setup_common_for_test_data()
+
+        model.ws_spectrum_info.isMasked = Mock(return_value=False)
+        model.ws_spectrum_info.isMonitor = Mock(return_value=True)
+        ws.hasMaskedBins = Mock(return_value=True)
+        ws.maskedBinsIndices = Mock(return_value=[index.column()])
+
+        output = model.data(index, Qt.ToolTipRole)
+
+        model.ws_spectrum_info.hasDetectors.assert_has_calls([call.do_work(row), call.do_work(row)])
+        model.ws_spectrum_info.isMasked.assert_called_once_with(row)
+        model.ws_spectrum_info.isMonitor.assert_called_once_with(row)
+
+        self.assertEqual(
+            TableWorkspaceTableViewModel.MONITOR_ROW_STRING + TableWorkspaceTableViewModel.MASKED_BIN_STRING, output)
+
+        # Doing the same thing a second time should hit the cache, so no additional calls will have been made
+        output = model.data(index, Qt.ToolTipRole)
+        model.ws_spectrum_info.hasDetectors.assert_has_calls([call.do_work(row), call.do_work(row)])
+        self.assertEqual(2, model.ws_spectrum_info.isMasked.call_count)
+        # This was called only once because the monitor was cached
+        model.ws_spectrum_info.isMonitor.assert_called_once_with(row)
+
+        self.assertEqual(
+            TableWorkspaceTableViewModel.MONITOR_ROW_STRING + TableWorkspaceTableViewModel.MASKED_BIN_STRING, output)
+
+    def test_data_tooltip_role_masked_bin(self):
+        if not qtpy.PYQT5:
+            self.skipTest("QVariant cannot be instantiated in QT4, and the test fails with an error.")
+
+        ws, model, row, index = setup_common_for_test_data()
+
+        model.ws_spectrum_info.isMasked = Mock(return_value=False)
+        model.ws_spectrum_info.isMonitor = Mock(return_value=False)
+        ws.hasMaskedBins = Mock(return_value=True)
+        ws.maskedBinsIndices = Mock(return_value=[index.column()])
+
+        output = model.data(index, Qt.ToolTipRole)
+
+        model.ws_spectrum_info.hasDetectors.assert_has_calls([call.do_work(row), call.do_work(row)])
+        model.ws_spectrum_info.isMasked.assert_called_once_with(row)
+        model.ws_spectrum_info.isMonitor.assert_called_once_with(row)
+
+        self.assertEqual(TableWorkspaceTableViewModel.MASKED_BIN_STRING, output)
+
+        # Doing the same thing a second time should hit the cache, so no additional calls will have been made
+        output = model.data(index, Qt.ToolTipRole)
+        self.assertEqual(4, model.ws_spectrum_info.hasDetectors.call_count)
+        self.assertEqual(2, model.ws_spectrum_info.isMasked.call_count)
+        self.assertEqual(2, model.ws_spectrum_info.isMonitor.call_count)
+        # This was called only once because the monitor was cached
+        ws.hasMaskedBins.assert_called_once_with(row)
+        ws.maskedBinsIndices.assert_called_once_with(row)
+
+        self.assertEqual(TableWorkspaceTableViewModel.MASKED_BIN_STRING, output)
+
+    def test_headerData_not_display_or_tooltip(self):
+        if not qtpy.PYQT5:
+            self.skipTest("QVariant cannot be instantiated in QT4, and the test fails with an error.")
+        ws = MockWorkspace()
+        model_type = TableWorkspaceTableViewModelType.x
+        model = TableWorkspaceTableViewModel(ws, model_type)
+        output = model.headerData(0, Qt.Vertical, Qt.BackgroundRole)
+        self.assertTrue(isinstance(output, QtCore.QVariant))
+
+    def test_headerData_vertical_header_display_role(self):
+        ws = MockWorkspace()
+        model_type = TableWorkspaceTableViewModelType.x
+        model = TableWorkspaceTableViewModel(ws, model_type)
+        mock_section = 0
+        output = model.headerData(mock_section, Qt.Vertical, Qt.DisplayRole)
+
+        ws.getAxis.assert_called_once_with(AXIS_INDEX_FOR_VERTICAL)
+        ws.mock_axis.label.assert_called_once_with(mock_section)
+
+        expected_output = TableWorkspaceTableViewModel.VERTICAL_HEADER_DISPLAY_STRING.format(mock_section,
+                                                                                             MockMantidAxis.TEST_LABEL)
+
+        self.assertEqual(expected_output, output)
+
+    def test_headerData_vertical_header_tooltip_role(self):
+        ws = MockWorkspace()
+        model_type = TableWorkspaceTableViewModelType.x
+        model = TableWorkspaceTableViewModel(ws, model_type)
+        mock_section = 0
+        output = model.headerData(mock_section, Qt.Vertical, Qt.ToolTipRole)
+
+        ws.getSpectrum.assert_called_once_with(mock_section)
+        ws.mock_spectrum.getSpectrumNo.assert_called_once_with()
+
+        expected_output = TableWorkspaceTableViewModel.VERTICAL_HEADER_TOOLTIP_STRING.format(mock_section,
+                                                                                             MockSpectrum.TEST_SPECTRUM_NO)
+        self.assertEqual(expected_output, output)
+
+    def test_headerData_horizontal_header_display_role_for_X_values(self):
+        ws = MockWorkspace()
+        model_type = TableWorkspaceTableViewModelType.x
+        model = TableWorkspaceTableViewModel(ws, model_type)
+        mock_section = 0
+        output = model.headerData(mock_section, Qt.Horizontal, Qt.DisplayRole)
+        expected_output = TableWorkspaceTableViewModel.HORIZONTAL_HEADER_DISPLAY_STRING_FOR_X_VALUES.format(
+            mock_section)
+        self.assertEqual(expected_output, output)
+
+    def test_headerData_horizontal_header_tooltip_role_for_X_values(self):
+        ws = MockWorkspace()
+        model_type = TableWorkspaceTableViewModelType.x
+        model = TableWorkspaceTableViewModel(ws, model_type)
+        mock_section = 0
+        output = model.headerData(mock_section, Qt.Horizontal, Qt.ToolTipRole)
+
+        expected_output = TableWorkspaceTableViewModel.HORIZONTAL_HEADER_TOOLTIP_STRING_FOR_X_VALUES.format(
+            mock_section)
+        self.assertEqual(expected_output, output)
+
+    def test_headerData_horizontal_header_display_role_histogram_data(self):
+        mock_section = 0
+        mock_return_values = [0, 1, 2, 3, 4, 5, 6]
+        expected_bin_centre = (mock_return_values[mock_section] + mock_return_values[mock_section + 1]) / 2.0
+        is_histogram_data = True
+
+        self._run_test_headerData_horizontal_header_display_role(is_histogram_data, mock_return_values, mock_section,
+                                                                 expected_bin_centre)
+
+    def test_headerData_horizontal_header_display_role_not_histogram_data(self):
+        mock_section = 0
+        mock_return_values = [0, 1, 2, 3, 4, 5, 6]
+        expected_bin_centre = mock_return_values[mock_section]
+        is_histogram_data = False
+
+        self._run_test_headerData_horizontal_header_display_role(is_histogram_data, mock_return_values, mock_section,
+                                                                 expected_bin_centre)
+
+    def _run_test_headerData_horizontal_header_display_role(self, is_histogram_data, mock_return_values, mock_section,
+                                                            expected_bin_centre):
+        ws = MockWorkspace(read_return=mock_return_values, isHistogramData=is_histogram_data)
+        model_type = TableWorkspaceTableViewModelType.y
+        model = TableWorkspaceTableViewModel(ws, model_type)
+        output = model.headerData(mock_section, Qt.Horizontal, Qt.DisplayRole)
+
+        ws.isHistogramData.assert_called_once_with()
+        ws.readX.assert_called_once_with(0)
+        ws.getAxis.assert_called_once_with(AXIS_INDEX_FOR_HORIZONTAL)
+        ws.mock_axis.getUnit.assert_called_once_with()
+        ws.mock_axis.mock_unit.symbol.assert_called_once_with()
+        ws.mock_axis.mock_unit.mock_symbol.utf8.assert_called_once_with()
+        expected_output = TableWorkspaceTableViewModel \
+            .HORIZONTAL_HEADER_DISPLAY_STRING \
+            .format(mock_section, expected_bin_centre, MockMantidSymbol.TEST_UTF8)
+        self.assertEqual(expected_output, output)
+
+    def test_headerData_horizontal_header_tooltip_role_histogram_data(self):
+        mock_section = 0
+        mock_return_values = [0, 1, 2, 3, 4, 5, 6]
+        expected_bin_centre = (mock_return_values[mock_section] + mock_return_values[mock_section + 1]) / 2.0
+        is_histogram_data = True
+
+        self._run_test_headerData_horizontal_header_tooltip_role(is_histogram_data, mock_return_values, mock_section,
+                                                                 expected_bin_centre)
+
+    def test_headerData_horizontal_header_tooltip_role_not_histogram_data(self):
+        mock_section = 0
+        mock_return_values = [0, 1, 2, 3, 4, 5, 6]
+        expected_bin_centre = mock_return_values[mock_section]
+        is_histogram_data = False
+        self._run_test_headerData_horizontal_header_tooltip_role(is_histogram_data, mock_return_values, mock_section,
+                                                                 expected_bin_centre)
+
+    def _run_test_headerData_horizontal_header_tooltip_role(self, is_histogram_data, mock_return_values, mock_section,
+                                                            expected_bin_centre):
+        ws = MockWorkspace(read_return=mock_return_values, isHistogramData=is_histogram_data)
+        model_type = TableWorkspaceTableViewModelType.y
+        model = TableWorkspaceTableViewModel(ws, model_type)
+        output = model.headerData(mock_section, Qt.Horizontal, Qt.ToolTipRole)
+
+        ws.isHistogramData.assert_called_once_with()
+        ws.readX.assert_called_once_with(0)
+        ws.getAxis.assert_called_once_with(AXIS_INDEX_FOR_HORIZONTAL)
+        ws.mock_axis.getUnit.assert_called_once_with()
+        ws.mock_axis.mock_unit.symbol.assert_called_once_with()
+        ws.mock_axis.mock_unit.caption.assert_called_once_with()
+        ws.mock_axis.mock_unit.mock_symbol.utf8.assert_called_once_with()
+
+        expected_output = TableWorkspaceTableViewModel \
+            .HORIZONTAL_HEADER_TOOLTIP_STRING \
+            .format(mock_section, MockMantidUnit.TEST_CAPTION, expected_bin_centre, MockMantidSymbol.TEST_UTF8)
+        self.assertEqual(expected_output, output)
+
+    def test_not_common_bins_horizontal_display_role(self):
+        mock_section = 0
+        mock_return_values = [0, 1, 2, 3, 4, 5, 6]
+        is_histogram_data = False
+
+        ws = MockWorkspace(read_return=mock_return_values, isHistogramData=is_histogram_data)
+        ws.isCommonBins = Mock(return_value=False)
+        model_type = TableWorkspaceTableViewModelType.y
+        model = TableWorkspaceTableViewModel(ws, model_type)
+        output = model.headerData(mock_section, Qt.Horizontal, Qt.DisplayRole)
+
+        self.assertEqual(TableWorkspaceTableViewModel.HORIZONTAL_BINS_VARY_DISPLAY_STRING.format(mock_section), output)
+
+    def test_not_common_bins_horizontal_tooltip_role(self):
+        mock_section = 0
+        mock_return_values = [0, 1, 2, 3, 4, 5, 6]
+        is_histogram_data = False
+
+        ws = MockWorkspace(read_return=mock_return_values, isHistogramData=is_histogram_data)
+        ws.isCommonBins = Mock(return_value=False)
+        model_type = TableWorkspaceTableViewModelType.y
+        model = TableWorkspaceTableViewModel(ws, model_type)
+        output = model.headerData(mock_section, Qt.Horizontal, Qt.ToolTipRole)
+
+        self.assertEqual(TableWorkspaceTableViewModel.HORIZONTAL_BINS_VARY_TOOLTIP_STRING.format(mock_section), output)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/test_helpers/__init__.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/test_helpers/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..57d5ae5a28a63ed0dd44886f201c25df7cac61ca
--- /dev/null
+++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/test_helpers/__init__.py
@@ -0,0 +1,9 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantid workbench.
+#
+#
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/test_helpers/mock_tableworkspacedisplay.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/test_helpers/mock_tableworkspacedisplay.py
new file mode 100644
index 0000000000000000000000000000000000000000..072b0f512d5eebfecdf5d361441e30d4d081bfe6
--- /dev/null
+++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/test_helpers/mock_tableworkspacedisplay.py
@@ -0,0 +1,81 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantid workbench.
+#
+#
+from mock import Mock
+
+from mantidqt.widgets.TableWorkspacedisplay.table_view_model import TableWorkspaceTableViewModelType
+
+
+class MockQTableHeader(object):
+    def __init__(self):
+        self.addAction = Mock()
+
+
+class MockQSelection:
+    def __init__(self):
+        self.mock_item_range = MockQItemRange()
+        self.first = Mock(return_value=self.mock_item_range)
+
+
+class MockQItemRange(object):
+    def __init__(self):
+        self.top = Mock(return_value=0)
+        self.bottom = Mock(return_value=2)
+        self.left = Mock(return_value=0)
+        self.right = Mock(return_value=2)
+
+
+class MockQSelectionModel:
+    def __init__(self):
+        self.hasSelection = Mock()
+        self.selectedRows = None
+        self.selectedColumns = None
+        self.currentIndex = None
+        self.mock_selection = MockQSelection()
+        self.selection = Mock(return_value=self.mock_selection)
+
+
+class MockQTableViewModel:
+    def __init__(self):
+        self.type = TableWorkspaceTableViewModelType.x
+
+
+class MockQTableView:
+    def __init__(self):
+        self.setContextMenuPolicy = Mock()
+        self.addAction = Mock()
+        self.mock_horizontalHeader = MockQTableHeader()
+        self.mock_verticalHeader = MockQTableHeader()
+        self.horizontalHeader = Mock(return_value=self.mock_horizontalHeader)
+        self.verticalHeader = Mock(return_value=self.mock_verticalHeader)
+        self.setModel = Mock()
+        self.mock_model = MockQTableViewModel()
+        self.model = Mock(return_value=self.mock_model)
+
+        self.mock_selection_model = MockQSelectionModel()
+
+        self.selectionModel = Mock(return_value=self.mock_selection_model)
+
+
+class MockTableWorkspaceDisplayView:
+    def __init__(self):
+        self.set_context_menu_actions = Mock()
+        self.table_x = MockQTableView()
+        self.table_y = MockQTableView()
+        self.table_e = MockQTableView()
+        self.set_model = Mock()
+        self.copy_to_clipboard = Mock()
+        self.show_mouse_toast = Mock()
+        self.ask_confirmation = None
+
+
+class MockTableWorkspaceDisplayModel:
+    def __init__(self):
+        self.get_spectrum_plot_label = Mock()
+        self.get_bin_plot_label = Mock()
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/test_helpers/tableworkspacedisplay_common.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/test_helpers/tableworkspacedisplay_common.py
new file mode 100644
index 0000000000000000000000000000000000000000..9301a4c03ee90961e72f5793fc01068bc4175db1
--- /dev/null
+++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/test_helpers/tableworkspacedisplay_common.py
@@ -0,0 +1,134 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantid workbench.
+#
+#
+from __future__ import (absolute_import, division, print_function)
+
+from mock import Mock
+
+from mantidqt.widgets.TableWorkspacedisplay.table_view_model import TableWorkspaceTableViewModel, \
+    TableWorkspaceTableViewModelType
+
+AXIS_INDEX_FOR_HORIZONTAL = 0
+AXIS_INDEX_FOR_VERTICAL = 1
+
+
+def setup_common_for_test_data():
+    """
+    Common configuration of variables and mocking for testing
+    TableWorkspaceDisplayTableViewModel's data and headerData functions
+    """
+    # Create some mock data for the mock workspace
+    row = 2
+    column = 2
+    # make a workspace with 0s
+    mock_data = [0] * 10
+    # set one of them to be not 0
+    mock_data[column] = 999
+    model_type = TableWorkspaceTableViewModelType.x
+    # pass onto the MockWorkspace so that it returns it when read from the TableViewModel
+    ws = MockWorkspace(read_return=mock_data)
+    ws.hasMaskedBins = Mock(return_value=True)
+    ws.maskedBinsIndices = Mock(return_value=[column])
+    model = TableWorkspaceTableViewModel(ws, model_type)
+    # The model retrieves the spectrumInfo object, and our MockWorkspace has already given it
+    # the MockSpectrumInfo, so all that needs to be done here is to set up the correct method Mocks
+    model.ws_spectrum_info.hasDetectors = Mock(return_value=True)
+    index = MockQModelIndex(row, column)
+    return ws, model, row, index
+
+
+class MockMantidSymbol:
+    TEST_ASCII = "MANTID_ASCII_SYMBOL"
+    TEST_UTF8 = "MANTID_UTF8_SYMBOL"
+
+    def __init__(self):
+        self.utf8 = Mock(return_value=self.TEST_UTF8)
+        self.ascii = Mock(return_value=self.TEST_ASCII)
+
+
+class MockMantidUnit:
+    TEST_CAPTION = "MANTID_TEST_CAPTION"
+
+    def __init__(self):
+        self.mock_symbol = MockMantidSymbol()
+        self.symbol = Mock(return_value=self.mock_symbol)
+        self.caption = Mock(return_value=self.TEST_CAPTION)
+
+
+class MockMantidAxis:
+    TEST_LABEL = "MANTID_TEST_AXIS"
+
+    def __init__(self):
+        self.label = Mock(return_value=self.TEST_LABEL)
+
+        self.mock_unit = MockMantidUnit()
+        self.getUnit = Mock(return_value=self.mock_unit)
+
+
+class MockQModelIndexSibling:
+    TEST_SIBLING_DATA = "MANTID_TEST_SIBLING_DATA"
+
+    def __init__(self):
+        self.data = Mock(return_value=self.TEST_SIBLING_DATA)
+
+
+class MockQModelIndex:
+
+    def __init__(self, row, column):
+        self.row = Mock(return_value=row)
+        self.column = Mock(return_value=column)
+        self.mock_sibling = MockQModelIndexSibling()
+        self.sibling = Mock(return_value=self.mock_sibling)
+
+
+class MockSpectrumInfo:
+    def __init__(self):
+        self.hasDetectors = None
+        self.isMasked = None
+        self.isMonitor = None
+
+
+class MockSpectrum:
+    TEST_SPECTRUM_NO = 123123
+
+    def __init__(self):
+        self.getSpectrumNo = Mock(return_value=self.TEST_SPECTRUM_NO)
+
+
+class MockWorkspace:
+    TEST_NAME = "THISISAtestWORKSPACE"
+
+    @staticmethod
+    def _return_MockSpectrumInfo():
+        return MockSpectrumInfo()
+
+    def __init__(self, read_return=None, axes=2, isHistogramData=True):
+        if read_return is None:
+            read_return = [1, 2, 3, 4, 5]
+        # This is assigned to a function, as the original implementation is a function that returns
+        # the spectrumInfo object
+        self.spectrumInfo = self._return_MockSpectrumInfo
+        self.getNumberHistograms = Mock(return_value=1)
+        self.isHistogramData = Mock(return_value=isHistogramData)
+        self.blocksize = Mock(return_value=len(read_return))
+        self.readX = Mock(return_value=read_return)
+        self.readY = Mock(return_value=read_return)
+        self.readE = Mock(return_value=read_return)
+        self.axes = Mock(return_value=axes)
+        self.hasMaskedBins = None
+        self.maskedBinsIndices = None
+        self.isCommonBins = Mock(return_value=True)
+
+        self.mock_spectrum = MockSpectrum()
+        self.getSpectrum = Mock(return_value=self.mock_spectrum)
+
+        self.mock_axis = MockMantidAxis()
+        self.getAxis = Mock(return_value=self.mock_axis)
+
+        self.name = None
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/view.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/view.py
new file mode 100644
index 0000000000000000000000000000000000000000..a291367eb4f505a1afc80a01950013f3b494d4b6
--- /dev/null
+++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/view.py
@@ -0,0 +1,103 @@
+# Mantid Repository : https://github.com/mantidproject/mantid
+#
+# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
+#     NScD Oak Ridge National Laboratory, European Spallation Source
+#     & Institut Laue - Langevin
+# SPDX - License - Identifier: GPL - 3.0 +
+#  This file is part of the mantid workbench.
+#
+#
+from __future__ import (absolute_import, division, print_function)
+
+from functools import partial
+
+from qtpy import QtGui
+from qtpy.QtCore import QPoint, Qt
+from qtpy.QtGui import QCursor, QFont, QFontMetrics, QKeySequence
+from qtpy.QtWidgets import (QAction, QHeaderView, QMessageBox, QTableView, QTableWidget, QToolTip)
+
+import mantidqt.icons
+
+
+class TableWorkspaceDisplayView(QTableWidget):
+    def __init__(self, presenter, parent=None, name=''):
+        super(TableWorkspaceDisplayView, self).__init__(parent)
+
+        self.presenter = presenter
+        self.COPY_ICON = mantidqt.icons.get_icon("fa.files-o")
+
+        # change the default color of the rows - makes them light blue
+        # monitors and masked rows are colored in the table's custom model
+        # palette = self.palette()
+        # palette.setColor(QtGui.QPalette.Base, QtGui.QColor(128, 255, 255))
+        # self.setPalette(palette)
+
+        self.setWindowTitle("{} - Mantid".format(name))
+        self.setWindowFlags(Qt.Window)
+
+        self.resize(600, 400)
+        self.show()
+
+    def keyPressEvent(self, event):
+        if event.matches(QKeySequence.Copy):
+            self.presenter.action_keypress_copy(self)
+        super(TableWorkspaceDisplayView, self).keyPressEvent(event)
+
+    def set_context_menu_actions(self, table):
+        """
+        Sets up the context menu actions for the table
+        :type table: QTableView
+        :param table: The table whose context menu actions will be set up.
+        :param ws_read_function: The read function used to efficiently retrieve data directly from the workspace
+        """
+        copy_action = QAction(self.COPY_ICON, "Copy", table)
+        # sets the first (table) parameter of the copy action callback
+        # so that each context menu can copy the data from the correct table
+        decorated_copy_action_with_correct_table = partial(self.presenter.action_copy_cells, table)
+        copy_action.triggered.connect(decorated_copy_action_with_correct_table)
+
+        table.setContextMenuPolicy(Qt.ActionsContextMenu)
+        table.addAction(copy_action)
+
+        horizontalHeader = table.horizontalHeader()
+        horizontalHeader.setContextMenuPolicy(Qt.ActionsContextMenu)
+        horizontalHeader.setSectionResizeMode(QHeaderView.Fixed)
+
+        copy_bin_values = QAction(self.COPY_ICON, "Copy", horizontalHeader)
+        copy_bin_values.triggered.connect(partial(self.presenter.action_copy_bin_values, table))
+
+        horizontalHeader.addAction(copy_bin_values)
+
+        verticalHeader = table.verticalHeader()
+        verticalHeader.setContextMenuPolicy(Qt.ActionsContextMenu)
+        verticalHeader.setSectionResizeMode(QHeaderView.Fixed)
+
+        copy_spectrum_values = QAction(self.COPY_ICON, "Copy", verticalHeader)
+        copy_spectrum_values.triggered.connect(partial(self.presenter.action_copy_spectrum_values, table))
+
+        separator1 = QAction(verticalHeader)
+        separator1.setSeparator(True)
+
+        verticalHeader.addAction(copy_spectrum_values)
+        verticalHeader.addAction(separator1)
+
+    @staticmethod
+    def copy_to_clipboard(data):
+        """
+        Uses the QGuiApplication to copy to the system clipboard.
+
+        :type data: str
+        :param data: The data that will be copied to the clipboard
+        :return:
+        """
+        cb = QtGui.QGuiApplication.clipboard()
+        cb.setText(data, mode=cb.Clipboard)
+
+
+    def ask_confirmation(self, message, title="Mantid Workbench"):
+        """
+        :param message:
+        :return:
+        """
+        reply = QMessageBox.question(self, title, message, QMessageBox.Yes, QMessageBox.No)
+        return True if reply == QMessageBox.Yes else False