diff --git a/Framework/PythonInterface/mantid/api/src/Exports/ITableWorkspace.cpp b/Framework/PythonInterface/mantid/api/src/Exports/ITableWorkspace.cpp index d1a9f270e68717e259c0964afd9f10fea8b747ca..8b71a0dd7bf215abc686775ab48cfc3d5f32de73 100644 --- a/Framework/PythonInterface/mantid/api/src/Exports/ITableWorkspace.cpp +++ b/Framework/PythonInterface/mantid/api/src/Exports/ITableWorkspace.cpp @@ -296,6 +296,28 @@ PyObject *row(ITableWorkspace &self, int row) { return result; } +/** + * Access a cell and return a corresponding Python type + * @param self A reference to the TableWorkspace python object that we were + * called on + * @param row An integer giving the row + */ +PyObject *columnTypes(ITableWorkspace &self) { + int numCols = static_cast<int>(self.columnCount()); + + PyObject *list = PyList_New(numCols); + + for (int col = 0; col < numCols; col++) { + Mantid::API::Column_const_sptr column = self.getColumn(col); + const std::type_info &typeID = column->get_type_info(); + if (PyList_SetItem(list, col, PyString_FromString(typeID.name()))) { + throw std::runtime_error("Error while building list"); + } + } + + return list; +} + /** * Adds a new row in the table, where the items are given in a dictionary * object mapping {column name:value}. It must contain a key-value entry for @@ -629,6 +651,9 @@ void export_ITableWorkspace() { .def("row", &row, (arg("self"), arg("row")), "Return all values of a specific row as a dict.") + .def("columnTypes", &columnTypes, arg("self"), + "Return the types of the columns as a list") + // FromSequence must come first since it takes an object parameter // Otherwise, FromDict will never be called as object accepts anything .def("addRow", &addRowFromSequence, (arg("self"), arg("row_items_seq")), diff --git a/buildconfig/CMake/Bootstrap.cmake b/buildconfig/CMake/Bootstrap.cmake index 36d10e1ed04468bf8af62222534d87fd7580b9cf..017f8f1ad14708c40f28aff535aed4ca299de16b 100644 --- a/buildconfig/CMake/Bootstrap.cmake +++ b/buildconfig/CMake/Bootstrap.cmake @@ -10,7 +10,7 @@ if( MSVC ) include ( ExternalProject ) set( EXTERNAL_ROOT ${PROJECT_SOURCE_DIR}/external CACHE PATH "Location to clone third party dependencies to" ) set( THIRD_PARTY_GIT_URL "https://github.com/mantidproject/thirdparty-msvc2015.git" ) - set ( THIRD_PARTY_GIT_SHA1 cee87b544761b0311dd3664f9dbbd2f11e0a780a ) + set ( THIRD_PARTY_GIT_SHA1 11e8ce35bcfe21c26b503d09f424a96c70590523 ) set ( THIRD_PARTY_DIR ${EXTERNAL_ROOT}/src/ThirdParty ) # Generates a script to do the clone/update in tmp set ( _project_name ThirdParty ) diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/model.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/model.py index 5a75197a9199f5e01397d0ee57d478265b6bd065..59203b78880325766877fd81d0e930bd852d5679 100644 --- a/qt/python/mantidqt/widgets/tableworkspacedisplay/model.py +++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/model.py @@ -10,35 +10,73 @@ # from __future__ import (absolute_import, division, print_function) + from mantid.dataobjects import PeaksWorkspace, TableWorkspace -from mantid.py3compat import Enum -# TODO remove transient mock dependency -> it is not available on Ubuntu fresh install -# take MWD approach -from mantidqt.widgets.matrixworkspacedisplay.test_helpers.matrixworkspacedisplay_common import MockWorkspace from mantidqt.widgets.tableworkspacedisplay.marked_columns import MarkedColumns +from mantid.kernel import V3D +import six -class TableDisplayColumnType(Enum): - NUMERIC = 1 - TEST = 123 +if six.PY2: + from functools32 import lru_cache +else: + from functools import lru_cache class TableWorkspaceDisplayModel: SPECTRUM_PLOT_LEGEND_STRING = '{}-{}' BIN_PLOT_LEGEND_STRING = '{}-bin-{}' + ALLOWED_WORKSPACE_TYPES = [PeaksWorkspace, TableWorkspace] + def __init__(self, ws): - if not isinstance(ws, TableWorkspace) \ - and not isinstance(ws, PeaksWorkspace) \ - and not isinstance(ws, MockWorkspace): - raise ValueError("The workspace type is not supported: {0}".format(type(ws))) + if not any(isinstance(ws, allowed_type) for allowed_type in self.ALLOWED_WORKSPACE_TYPES): + raise ValueError("The workspace type is not supported: {0}".format(ws)) self.ws = ws self.ws_num_rows = self.ws.rowCount() self.ws_num_cols = self.ws.columnCount() + self.ws_column_types = self.ws.columnTypes() + self.convert_types = self.map_from_type_name(self.ws_column_types) self.marked_columns = MarkedColumns() self._original_column_headers = self.get_column_headers() + def _get_bool_from_str(self, string): + string = string.lower() + if string == "true": + return True + elif string == "false": + return False + else: + raise ValueError("'{}' is not a valid bool string.".format(string)) + + def _get_v3d_from_str(self, string): + if '[' in string and ']' in string: + string = string[1:-1] + if ',' in string: + return V3D(*[float(x) for x in string.split(',')]) + else: + raise RuntimeError("'{}' is not a valid V3D string.".format(string)) + + def map_from_type_name(self, column_types): + convert_types = [] + for type in column_types: + type = type.lower() + if 'int' in type: + convert_types.append(int) + elif 'double' in type or 'float' in type: + convert_types.append(float) + elif 'string' in type: + convert_types.append(str) + elif 'bool' in type: + convert_types.append(self._get_bool_from_str) + elif 'v3d' in type: + convert_types.append(self._get_v3d_from_str) + else: + raise ValueError("Trying to set data for unknown column type {}".format(type)) + + return convert_types + def original_column_headers(self): return self._original_column_headers[:] @@ -62,3 +100,11 @@ class TableWorkspaceDisplayModel: def get_column_header(self, index): return self.get_column_headers()[index] + + @lru_cache(maxsize=1) + def is_peaks_workspace(self): + return isinstance(self.ws, PeaksWorkspace) + + def set_cell_data(self, row, col, data): + data = self.convert_types[col](data) + self.ws.setCell(row, col, data) diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/presenter.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/presenter.py index 6fef3b2e01bcc9ad50694d70289312de9658aded..7229723f017836d43db46016f19155adb68e8758 100644 --- a/qt/python/mantidqt/widgets/tableworkspacedisplay/presenter.py +++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/presenter.py @@ -14,6 +14,7 @@ from functools import partial from qtpy.QtCore import Qt from qtpy.QtWidgets import QTableWidgetItem +from mantid.kernel import V3D from mantid.simpleapi import DeleteTableRows, StatisticsOfTableWorkspace from mantidqt.widgets.common.table_copying import copy_cells, show_mouse_toast, show_no_selection_to_copy_toast from mantidqt.widgets.tableworkspacedisplay.error_column import ErrorColumn @@ -22,13 +23,35 @@ from .model import TableWorkspaceDisplayModel from .view import TableWorkspaceDisplayView -class TableItem(QTableWidgetItem): +class WorkbenchTableWidgetItem(QTableWidgetItem): + def __init__(self, data, editable=False): + # if not editable just initialise the ItemWidget as string + if not editable: + QTableWidgetItem.__init__(self, str(data)) + self.setFlags(self.flags() & ~Qt.ItemIsEditable) + return + + QTableWidgetItem.__init__(self) + + if isinstance(data, V3D) or isinstance(data, float): + data = str(data) + + self.original_data = data + # this will correctly turn all number cells into number types + self.reset() + def __lt__(self, other): try: # if the data can be parsed as numbers then compare properly, otherwise default to the Qt implementation return float(self.data(Qt.DisplayRole)) < float(other.data(Qt.DisplayRole)) except: - return super(TableItem, self).__lt__(other) + return super(WorkbenchTableWidgetItem, self).__lt__(other) + + def reset(self): + self.setData(Qt.DisplayRole, self.original_data) + + def update(self): + self.original_data = self.data(Qt.DisplayRole) class TableWorkspaceDisplay(object): @@ -51,6 +74,28 @@ class TableWorkspaceDisplay(object): self.update_column_headers() self.load_data(self.view) + # connect to cellChanged signal after the data has been loaded + # all consecutive triggers will be from user actions + self.view.itemChanged.connect(self.handleItemChanged) + + def handleItemChanged(self, item): + """ + :type item: WorkbenchTableWidgetItem + :param item: + :return: + """ + print("Changed data in cell:", item, "ops:", dir(item)) + + try: + self.model.set_cell_data(item.row(), item.column(), item.data(Qt.DisplayRole)) + item.update() + except ValueError: + show_mouse_toast("Error: Trying to set non-numeric data into a numeric column.") + except Exception as x: + show_mouse_toast("Unknown error occurred: {}".format(x)) + finally: + item.reset() + def update_column_headers(self): """ :param extra_labels: Extra labels to be appended to the column headers. @@ -75,10 +120,14 @@ class TableWorkspaceDisplay(object): table.setRowCount(num_rows) num_cols = self.model.get_number_of_columns() + + # the table should be editable if the ws is not PeaksWS + editable = not self.model.is_peaks_workspace() + for col in range(num_cols): column_data = self.model.get_column(col) for row in range(num_rows): - item = TableItem(str(column_data[row])) + item = WorkbenchTableWidgetItem(column_data[row], editable=editable) table.setItem(row, col, item) def action_copy_cells(self): @@ -272,7 +321,7 @@ class TableWorkspaceDisplay(object): except ValueError as e: # TODO log error? self.view.show_warning( - "One or more of the columns being plotted contain invalid data for MatPlotLib." \ + "One or more of the columns being plotted contain invalid data for MatPlotLib." "\n\nError message:\n{}".format(e), "Invalid data - Mantid Workbench") return diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/test/test_tableworkspacedisplay_model.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/test/test_tableworkspacedisplay_model.py index a3e64bd712db29f992ad258ce2e51d1b10617565..befbaf27538f13580a6b53556accb9ae2b30b1c4 100644 --- a/qt/python/mantidqt/widgets/tableworkspacedisplay/test/test_tableworkspacedisplay_model.py +++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/test/test_tableworkspacedisplay_model.py @@ -18,10 +18,11 @@ from mantidqt.widgets.matrixworkspacedisplay.test_helpers.matrixworkspacedisplay from mantidqt.widgets.tableworkspacedisplay.model import TableWorkspaceDisplayModel -# from mantid.simpleapi import CreateSampleWorkspace - - class TableWorkspaceDisplayModelTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Allow the MockWorkspace to work within the model + TableWorkspaceDisplayModel.ALLOWED_WORKSPACE_TYPES.append(MockWorkspace) def test_get_name(self): ws = MockWorkspace() @@ -35,17 +36,26 @@ class TableWorkspaceDisplayModelTest(unittest.TestCase): 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) + + +class TableWorkspaceDisplayModelTestWithFramework(unittest.TestCase): + + @classmethod + def setUpClass(cls): + # Allow the MockWorkspace to work within the model + TableWorkspaceDisplayModel.ALLOWED_WORKSPACE_TYPES.append(MockWorkspace) + + def test_no_raise_with_supported_workspace(self): + from mantid.simpleapi import CreateSampleWorkspace # noqa + 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__': diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/test_helpers/__init__.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/test_helpers/__init__.py deleted file mode 100644 index 57d5ae5a28a63ed0dd44886f201c25df7cac61ca..0000000000000000000000000000000000000000 --- a/qt/python/mantidqt/widgets/tableworkspacedisplay/test_helpers/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# 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 deleted file mode 100644 index 072b0f512d5eebfecdf5d361441e30d4d081bfe6..0000000000000000000000000000000000000000 --- a/qt/python/mantidqt/widgets/tableworkspacedisplay/test_helpers/mock_tableworkspacedisplay.py +++ /dev/null @@ -1,81 +0,0 @@ -# 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/view.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/view.py index 75ac2cd389451e9276580f5bc854021b30aa3e54..8f567eaa40fe89ab1d6d6ffc02a2cd7e8e6af3ad 100644 --- a/qt/python/mantidqt/widgets/tableworkspacedisplay/view.py +++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/view.py @@ -43,6 +43,7 @@ class TableWorkspaceDisplayView(QTableWidget): self.resize(600, 400) self.show() + def doubleClickedHeader(self): print("Double clicked WOO") @@ -114,8 +115,8 @@ class TableWorkspaceDisplayView(QTableWidget): set_as_y = QAction(self.TBD, "Set as Y", menu_main) set_as_y.triggered.connect(self.presenter.action_set_as_y) - set_as_nein = QAction(self.TBD, "Set as None", menu_main) - set_as_nein.triggered.connect(self.presenter.action_set_as_none) + set_as_none = QAction(self.TBD, "Set as None", menu_main) + set_as_none.triggered.connect(self.presenter.action_set_as_none) statistics_on_columns = QAction(self.STATISTICS_ON_ROW, "Statistics on Columns", menu_main) statistics_on_columns.triggered.connect(self.presenter.action_statistics_on_columns) @@ -137,21 +138,24 @@ class TableWorkspaceDisplayView(QTableWidget): menu_main.addAction(set_as_x) menu_main.addAction(set_as_y) - # get the columns marked as Y marked_y_cols = self.presenter.get_columns_marked_as_y() num_y_cols = len(marked_y_cols) + + # If any columns are marked as Y then generate the set error menu if num_y_cols > 0: menu_set_as_y_err = QMenu("Set error for Y...") for col in range(num_y_cols): - # get the real index of the column - # display the real index in the menu set_as_y_err = QAction(self.TBD, "Y{}".format(col), menu_main) + # the column index of the column relative to the whole table, this is necessary + # so that later the data of the column marked as error can be retrieved real_column_index = marked_y_cols[col] + # col here holds the index in the LABEL (multiple Y columns have labels Y0, Y1, YN...) + # this is NOT the same as the column relative to the WHOLE table set_as_y_err.triggered.connect(partial(self.presenter.action_set_as_y_err, real_column_index, col)) menu_set_as_y_err.addAction(set_as_y_err) menu_main.addMenu(menu_set_as_y_err) - menu_main.addAction(set_as_nein) + menu_main.addAction(set_as_none) menu_main.addAction(self.make_separator(menu_main)) menu_main.addAction(statistics_on_columns) menu_main.addAction(self.make_separator(menu_main)) @@ -189,4 +193,4 @@ class TableWorkspaceDisplayView(QTableWidget): return True if reply == QMessageBox.Yes else False def show_warning(self, message, title="Mantid Workbench"): - QMessageBox.warning(self, title, message) \ No newline at end of file + QMessageBox.warning(self, title, message)