From 6ac6df83a7259fbcdaa157d9ceedd4a13558e4d2 Mon Sep 17 00:00:00 2001
From: Dimitar Tasev <dimtasev@gmail.com>
Date: Mon, 26 Nov 2018 15:51:24 +0000
Subject: [PATCH] Statistics and plotting, re #24007

---
 .../algorithms/StatisticsOfTableWorkspace.py  |  90 +++++++-----
 .../widgets/tableworkspacedisplay/__main__.py |   7 +-
 .../widgets/tableworkspacedisplay/model.py    |  37 +----
 .../tableworkspacedisplay/plot_type.py        |   7 +
 .../tableworkspacedisplay/presenter.py        | 134 ++++++++++++++----
 .../widgets/tableworkspacedisplay/view.py     |  74 +++++++---
 6 files changed, 239 insertions(+), 110 deletions(-)
 create mode 100644 qt/python/mantidqt/widgets/tableworkspacedisplay/plot_type.py

diff --git a/Framework/PythonInterface/plugins/algorithms/StatisticsOfTableWorkspace.py b/Framework/PythonInterface/plugins/algorithms/StatisticsOfTableWorkspace.py
index f148fc24d9f..35c343bf342 100644
--- a/Framework/PythonInterface/plugins/algorithms/StatisticsOfTableWorkspace.py
+++ b/Framework/PythonInterface/plugins/algorithms/StatisticsOfTableWorkspace.py
@@ -4,30 +4,33 @@
 #     NScD Oak Ridge National Laboratory, European Spallation Source
 #     & Institut Laue - Langevin
 # SPDX - License - Identifier: GPL - 3.0 +
-#pylint: disable=no-init
+# pylint: disable=no-init
 
 from __future__ import (absolute_import, division, print_function)
-from mantid.api import PythonAlgorithm, AlgorithmFactory, ITableWorkspaceProperty
-from mantid.kernel import Direction, Stats
-import mantid.simpleapi as ms
-from mantid import mtd, logger
-import numpy as np
+
 import collections
+
+import numpy as np
 from six import iteritems
 
+import mantid.simpleapi as ms
+from mantid import logger, mtd
+from mantid.api import AlgorithmFactory, ITableWorkspaceProperty, PythonAlgorithm
+from mantid.kernel import Direction, IntArrayBoundedValidator, IntArrayProperty, Stats
+
 
 def _stats_to_dict(stats):
     """
-    Converts a Statstics object to an ordered dictionary.
+    Converts a Statistics object to an ordered dictionary.
     @param stats Statistics object to convertToWaterfall
     @return Dictionary of statistics
     """
     stat_dict = collections.OrderedDict()
-    stat_dict['standard_deviation'] = stats.standard_deviation
-    stat_dict['maximum'] = stats.maximum
-    stat_dict['minimum'] = stats.minimum
-    stat_dict['mean'] = stats.mean
-    stat_dict['median'] = stats.median
+    stat_dict['StandardDev'] = stats.standard_deviation
+    stat_dict['Maximum'] = stats.maximum
+    stat_dict['Minimum'] = stats.minimum
+    stat_dict['Mean'] = stats.mean
+    stat_dict['Median'] = stats.median
     return stat_dict
 
 
@@ -42,38 +45,61 @@ class StatisticsOfTableWorkspace(PythonAlgorithm):
     def PyInit(self):
         self.declareProperty(ITableWorkspaceProperty('InputWorkspace', '', Direction.Input),
                              doc='Input table workspace.')
+        validator = IntArrayBoundedValidator()
+        validator.setLower(0)
+        self.declareProperty(
+            IntArrayProperty('ColumnIndices', values=[], direction=Direction.Input, validator=validator),
+            'Comma separated list of column indices for which statistics will be separated')
         self.declareProperty(ITableWorkspaceProperty('OutputWorkspace', '', Direction.Output),
-                             doc='Output workspace contatining column statitics.')
+                             doc='Output workspace containing column statistics.')
 
     def PyExec(self):
         in_ws = mtd[self.getPropertyValue('InputWorkspace')]
+        indices_list = self.getPropertyValue('ColumnIndices')
         out_ws_name = self.getPropertyValue('OutputWorkspace')
+        column_names = in_ws.getColumnNames()
+
+        # If column indices are not provided, then default to _ALL_ columns
+        if len(indices_list) > 0:
+            indices_list = [int(x) for x in indices_list.split(',')]
+        else:
+            indices_list = range(len(column_names))
 
         out_ws = ms.CreateEmptyTableWorkspace(OutputWorkspace=out_ws_name)
 
-        out_ws.addColumn('str', 'statistic')
+        out_ws.addColumn('str', 'Statistic')
 
         stats = collections.OrderedDict([
-            ('standard_deviation', collections.OrderedDict()),
-            ('minimum', collections.OrderedDict()),
-            ('median', collections.OrderedDict()),
-            ('maximum', collections.OrderedDict()),
-            ('mean', collections.OrderedDict()),
+            ('StandardDev', collections.OrderedDict()),
+            ('Minimum', collections.OrderedDict()),
+            ('Median', collections.OrderedDict()),
+            ('Maximum', collections.OrderedDict()),
+            ('Mean', collections.OrderedDict()),
         ])
-
-        for name in in_ws.getColumnNames():
+        # PYTHON_ROOT = "c:/users/qbr77747/apps/miniconda3"
+        # import os
+        # import sys
+        # sys.path.append(os.path.join(PYTHON_ROOT, "lib/site-packages"))
+        # import pydevd
+        # pydevd.settrace('localhost', port=44444, stdoutToServer=True, stderrToServer=True)
+
+        for index in indices_list:
+            column_name = column_names[index]
             try:
-                col_stats = _stats_to_dict(Stats.getStatistics(np.array([float(v) for v in in_ws.column(name)])))
-                for statname in stats:
-                    stats[statname][name] = col_stats[statname]
-                out_ws.addColumn('float', name)
-            except ValueError:
-                logger.notice('Column \'%s\' is not numerical, skipping' % name)
-
-        for name, stat in iteritems(stats):
-            stat1 = collections.OrderedDict(stat)
-            stat1['statistic'] = name
-            out_ws.addRow(stat1)
+                column_data = np.array([float(v) for v in in_ws.column(index)])
+                col_stats = _stats_to_dict(Stats.getStatistics(column_data))
+                for stat_name in stats:
+                    stats[stat_name][column_name] = col_stats[stat_name]
+                out_ws.addColumn('float', column_name)
+            except RuntimeError:
+                logger.notice('Column \'%s\' is not numerical, skipping' % column_name)
+            except:
+                logger.notice('Column \'%s\' is not numerical, skipping' % column_name)
+
+        for index, stat_name in iteritems(stats):
+            stat = collections.OrderedDict(stat_name)
+            stat['Statistic'] = index
+            out_ws.addRow(stat)
 
         self.setProperty('OutputWorkspace', out_ws)
 
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/__main__.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/__main__.py
index e42ed294991..f17a54bf05b 100644
--- a/qt/python/mantidqt/widgets/tableworkspacedisplay/__main__.py
+++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/__main__.py
@@ -18,11 +18,12 @@ 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
+# from workbench.plotting.functions import plot  # noqa: F402
+import matplotlib.pyplot as plt
 
 app = QApplication([])
 # DEEE_WS_MON = Load("SavedTableWorkspace.nxs")
 # DEEE_WS_MON = Load("TOPAZ_3007.peaks.nxs")
-DEEE_WS_MON = Load("SmallPeakWS10.nxs")
-window = TableWorkspaceDisplay(DEEE_WS_MON, plot)
+DEEE_WS_MON = Load("SmallPeakWS10_vals.nxs")
+window = TableWorkspaceDisplay(DEEE_WS_MON, plt)
 app.exec_()
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/model.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/model.py
index 3414cb726b5..68cbe49331b 100644
--- a/qt/python/mantidqt/widgets/tableworkspacedisplay/model.py
+++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/model.py
@@ -21,6 +21,9 @@ class ITableDisplayModel(object):
     def get_column_headers(self):
         raise NotImplementedError("This is an interface and should be implemented.")
 
+    def get_column_header(self, index):
+        raise NotImplementedError("This is an interface and should be implemented.")
+
     def get_column(self, index):
         raise NotImplementedError("This is an interface and should be implemented.")
 
@@ -44,8 +47,6 @@ class TableWorkspaceDisplayModel(ITableDisplayModel):
     SPECTRUM_PLOT_LEGEND_STRING = '{}-{}'
     BIN_PLOT_LEGEND_STRING = '{}-bin-{}'
 
-    # PEAKS_WORKSPACE_EDITABLE_COLUMNS = ["RunNumber", "h", "k", "l"]
-
     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)))
@@ -82,33 +83,5 @@ class TableWorkspaceDisplayModel(ITableDisplayModel):
     def get_number_of_columns(self):
         return self.ws_num_cols
 
-# class PeaksWorkspaceDisplayModel(TableWorkspaceDisplayModel):
-#     def __init__(self, ws):
-#         super(PeaksWorkspaceDisplayModel, self).__init__(ws)
-#         self.sigma_col_index = None
-#
-#     def get_column_headers(self):
-#         column_names = self.ws.getColumnNames()
-#         self.sigma_col_index = column_names.index("SigInt") + 1
-#         # insert the intensity/sigma after the sigma column
-#         column_names.insert(self.sigma_col_index, "I/σ")
-#         # update the number of columns
-#         self.ws_num_cols = len(column_names)
-#         return column_names
-#
-#     def get_column(self, index):
-#         """
-#         Get data for a column from the PeaksWorkspace.
-#
-#         Handles the index for the additional column Intensity/Sigma column correctly,
-#         as the column itself is only added
-#         :param index:
-#         :return:
-#         """
-#         if index < self.sigma_col_index:
-#             return self.ws.column(index)
-#         elif index > self.sigma_col_index:
-#             return self.ws.column(index - 1)
-#         else:
-#             num_rows = self.get_number_of_rows()
-#             return [self.ws.getPeak(i).getIntensityOverSigma() for i in range(num_rows)]
+    def get_column_header(self, index):
+        return self.get_column_headers()[index]
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/plot_type.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/plot_type.py
new file mode 100644
index 00000000000..9388850c7a0
--- /dev/null
+++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/plot_type.py
@@ -0,0 +1,7 @@
+from mantid.py3compat import Enum
+
+
+class PlotType(Enum):
+    LINEAR = 1
+    SCATTER = 2
+    LINE_AND_SYMBOL = 3
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/presenter.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/presenter.py
index d548cb95a30..2408dc31b7c 100644
--- a/qt/python/mantidqt/widgets/tableworkspacedisplay/presenter.py
+++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/presenter.py
@@ -9,31 +9,55 @@
 #
 from __future__ import absolute_import, division, print_function
 
+from functools import partial
+
 from qtpy.QtWidgets import QTableWidgetItem
 
-from mantid.simpleapi import DeleteTableRows
-from mantidqt.widgets.common.table_copying import copy_cells, show_no_selection_to_copy_toast
+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.plot_type import PlotType
 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?"
+    TOO_MANY_SELECTED_FOR_X = "Too many columns are selected to set as X. Please select only 1."
+    TOO_MANY_SELECTED_FOR_PLOT = "Too many columns are selected to plot. Please select only 1."
     NUM_SELECTED_FOR_CONFIRMATION = 10
+    NO_COLUMN_MARKED_AS_X = "No column marked as X."
 
-    def __init__(self, ws, plot=None, parent=None, model=None, view=None):
+    def __init__(self, ws, plot=None, parent=None, model=None, view=None, name=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.name = self.model.get_name() if name is None else name
+        self.view = view if view else TableWorkspaceDisplayView(self, parent, self.name)
+        self.parent = parent
         self.plot = plot
         self.view.set_context_menu_actions(self.view)
-        column_headers = self.model.get_column_headers()
-        # self.editable_columns = self.model.get_editable_columns(column_headers)
-        self.view.setColumnCount(len(column_headers))
-        self.view.setHorizontalHeaderLabels(column_headers)
+        self.update_column_headers()
         # self.view.setHorizontalHeaderLabels(["{}[Y]".format(x) for x in column_headers])
         self.load_data(self.view)
 
+        self.column_marked_as_x = None
+
+    def update_column_headers(self, extra_labels=None):
+        """
+        :param extra_labels: Extra labels to be appended to the column headers.
+                             Expected format: [(id, label), (2, "X"),...]
+        :type extra_labels: List[Tuple[int, str]]
+        :return:
+        """
+        column_headers = self.model.get_column_headers()
+        num_headers = len(column_headers)
+        self.view.setColumnCount(num_headers)
+
+        if extra_labels:
+            for index, label in extra_labels:
+                column_headers[index] += label
+
+        self.view.setHorizontalHeaderLabels(column_headers)
+
     def load_data(self, table):
         num_rows = self.model.get_number_of_rows()
         table.setRowCount(num_rows)
@@ -41,26 +65,21 @@ class TableWorkspaceDisplay(object):
         num_cols = self.model.get_number_of_columns()
         for col in range(num_cols):
             column_data = self.model.get_column(col)
-            # editable = False
-            # if peaks_workspace and col in self.editable_columns:
-            #     editable = True
             for row in range(num_rows):
                 item = QTableWidgetItem(str(column_data[row]))
-                # if not editable:
-                #     item.setFlags(item.flags() & ~Qt.ItemIsEditable)
                 table.setItem(row, col, item)
 
-    def action_copy_cells(self, table):
-        copy_cells(table)
+    def action_copy_cells(self):
+        copy_cells(self.view)
 
-    def action_copy_bin_values(self, table):
-        copy_cells(table)
+    def action_copy_bin_values(self):
+        copy_cells(self.view)
 
-    def action_copy_spectrum_values(self, table):
-        copy_cells(table)
+    def action_copy_spectrum_values(self):
+        copy_cells(self.view)
 
-    def action_keypress_copy(self, table):
-        copy_cells(table)
+    def action_keypress_copy(self):
+        copy_cells(self.view)
 
     def action_delete_row(self):
         selection_model = self.view.selectionModel()
@@ -78,15 +97,80 @@ class TableWorkspaceDisplay(object):
         for row in reversed(selected_rows_list):
             self.view.removeRow(row)
 
-    def action_statistics_on_rows(self):
+    def action_statistics_on_columns(self):
         selection_model = self.view.selectionModel()
         if not selection_model.hasSelection():
             show_no_selection_to_copy_toast()
             return
 
-        selected_rows = selection_model.selectedRows()
-        selected_rows_list = [index.row() for index in selected_rows]
-        num_cols = self.model.get_number_of_columns()
+        selected_columns = selection_model.selectedColumns()
+        selected_columns_list = [index.column() for index in selected_columns]
+        stats = StatisticsOfTableWorkspace(self.model.ws, selected_columns_list)
+        TableWorkspaceDisplay(stats, parent=self.parent, name="Column Statistics of {}".format(self.name))
+
+    def action_hide_column(self):
+        selection_model = self.view.selectionModel()
+        if not selection_model.hasSelection():
+            show_no_selection_to_copy_toast()
+            return
+
+        selected_columns = selection_model.selectedColumns()
+        selected_columns_list = [index.column() for index in selected_columns]
+        for column_index in selected_columns_list:
+            self.view.hideColumn(column_index)
+
+    def action_show_all_columns(self):
+        for column_index in range(self.view.columnCount()):
+            self.view.showColumn(column_index)
+
+    def action_set_as_x(self):
+        selection_model = self.view.selectionModel()
+        if not selection_model.hasSelection():
+            show_no_selection_to_copy_toast()
+            return
+
+        selected_columns = selection_model.selectedColumns()
+        if len(selected_columns) > 1:
+            show_mouse_toast(self.TOO_MANY_SELECTED_FOR_X)
+            return
+        self.column_marked_as_x = selected_columns[0].column()
+
+        self.update_column_headers([(self.column_marked_as_x, "[X]")])
+
+    def action_plot(self, type):
+        if self.column_marked_as_x is None:
+            show_mouse_toast(self.NO_COLUMN_MARKED_AS_X)
+            return
+
+        selection_model = self.view.selectionModel()
+        if not selection_model.hasSelection():
+            show_no_selection_to_copy_toast()
+            return
+
+        x = self.model.get_column(self.column_marked_as_x)
+        fig, ax = self.plot.subplots(subplot_kw={'projection': 'mantid'})
+        ax.set_xlabel(self.model.get_column_header(self.column_marked_as_x))
+
+        if type == PlotType.LINEAR:
+            plot_func = ax.plot
+        elif type == PlotType.SCATTER:
+            plot_func = ax.scatter
+        elif type == PlotType.LINE_AND_SYMBOL:
+            plot_func = partial(ax.plot, marker='o')
+        else:
+            raise ValueError("Plot Type: {} not currently supported!".format(type))
+
+        selected_columns = [col_index.column() for col_index in selection_model.selectedColumns()]
+        for column in selected_columns:
+            if column == self.column_marked_as_x:
+                # TODO log that `column` has been skipped as it's the same as X
+                pass
+            y = self.model.get_column(column)
+            column_label = self.model.get_column_header(column)
+            plot_func(x, y, label='Column {}'.format(column_label))
+            ax.set_ylabel(column_label)
+        ax.legend()
+        fig.show()
 
     # def _do_action_plot(self, table, axis, get_index, plot_errors=False):
     #     if self.plot is None:
diff --git a/qt/python/mantidqt/widgets/tableworkspacedisplay/view.py b/qt/python/mantidqt/widgets/tableworkspacedisplay/view.py
index d00626bac1a..c9080fb5459 100644
--- a/qt/python/mantidqt/widgets/tableworkspacedisplay/view.py
+++ b/qt/python/mantidqt/widgets/tableworkspacedisplay/view.py
@@ -14,9 +14,10 @@ from functools import partial
 from qtpy import QtGui
 from qtpy.QtCore import Qt
 from qtpy.QtGui import QKeySequence
-from qtpy.QtWidgets import (QAction, QHeaderView, QMessageBox, QTableView, QTableWidget)
+from qtpy.QtWidgets import (QAction, QHeaderView, QMenu, QMessageBox, QTableView, QTableWidget)
 
 import mantidqt.icons
+from mantidqt.widgets.tableworkspacedisplay.plot_type import PlotType
 
 
 class TableWorkspaceDisplayView(QTableWidget):
@@ -27,6 +28,7 @@ class TableWorkspaceDisplayView(QTableWidget):
         self.COPY_ICON = mantidqt.icons.get_icon("fa.files-o")
         self.DELETE_ROW = mantidqt.icons.get_icon("fa.minus-square-o")
         self.STATISTICS_ON_ROW = mantidqt.icons.get_icon('fa.fighter-jet')
+        self.GRAPH_ICON = mantidqt.icons.get_icon('fa.line-chart')
 
         # change the default color of the rows - makes them light blue
         # monitors and masked rows are colored in the table's custom model
@@ -64,13 +66,9 @@ class TableWorkspaceDisplayView(QTableWidget):
         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(self.presenter.action_copy_bin_values)
-
-        horizontalHeader.addAction(copy_bin_values)
+        horizontalHeader.setContextMenuPolicy(Qt.CustomContextMenu)
+        horizontalHeader.customContextMenuRequested.connect(self.custom_context_menu)
+        # horizontalHeader.setSectionResizeMode(QHeaderView.Fixed)
 
         verticalHeader = table.verticalHeader()
         verticalHeader.setContextMenuPolicy(Qt.ActionsContextMenu)
@@ -82,19 +80,59 @@ class TableWorkspaceDisplayView(QTableWidget):
         delete_row = QAction(self.DELETE_ROW, "Delete Row", verticalHeader)
         delete_row.triggered.connect(self.presenter.action_delete_row)
 
-        statistics_on_rows = QAction(self.STATISTICS_ON_ROW, "Statistics on Rows", verticalHeader)
-        statistics_on_rows.triggered.connect(self.presenter.action_statistics_on_rows)
-
-        separator1 = QAction(verticalHeader)
-        separator1.setSeparator(True)
-        separator2 = QAction(verticalHeader)
-        separator2.setSeparator(True)
+        separator2 = self.make_separator(verticalHeader)
 
         verticalHeader.addAction(copy_spectrum_values)
-        verticalHeader.addAction(separator1)
-        verticalHeader.addAction(delete_row)
         verticalHeader.addAction(separator2)
-        verticalHeader.addAction(statistics_on_rows)
+        verticalHeader.addAction(delete_row)
+
+    def custom_context_menu(self, position):
+        main_menu = QMenu()
+        plot = QMenu("Plot...", main_menu)
+        plot_line = QAction(self.GRAPH_ICON, "Line", plot)
+        plot_line.triggered.connect(partial(self.presenter.action_plot, PlotType.LINEAR))
+
+        plot_scatter = QAction(self.GRAPH_ICON, "Scatter", plot)
+        plot_scatter.triggered.connect(partial(self.presenter.action_plot, PlotType.SCATTER))
+
+        plot_line_and_points = QAction(self.GRAPH_ICON, "Line + Symbol", plot)
+        plot_line_and_points.triggered.connect(partial(self.presenter.action_plot, PlotType.LINE_AND_SYMBOL))
+
+        plot.addAction(plot_line)
+        plot.addAction(plot_scatter)
+        plot.addAction(plot_line_and_points)
+        main_menu.addMenu(plot)
+
+        copy_bin_values = QAction(self.COPY_ICON, "Copy", main_menu)
+        copy_bin_values.triggered.connect(self.presenter.action_copy_bin_values)
+
+        set_as_x = QAction(self.COPY_ICON, "Set as X", main_menu)
+        set_as_x.triggered.connect(self.presenter.action_set_as_x)
+
+        statistics_on_columns = QAction(self.STATISTICS_ON_ROW, "Statistics on Columns", main_menu)
+        statistics_on_columns.triggered.connect(self.presenter.action_statistics_on_columns)
+
+        hide_column = QAction(self.STATISTICS_ON_ROW, "Hide Column", main_menu)
+        hide_column.triggered.connect(self.presenter.action_hide_column)
+
+        show_all_columns = QAction(self.STATISTICS_ON_ROW, "Show All Columns", main_menu)
+        show_all_columns.triggered.connect(self.presenter.action_show_all_columns)
+
+        main_menu.addAction(copy_bin_values)
+        main_menu.addAction(self.make_separator(main_menu))
+        main_menu.addAction(set_as_x)
+        main_menu.addAction(self.make_separator(main_menu))
+        main_menu.addAction(statistics_on_columns)
+        main_menu.addAction(self.make_separator(main_menu))
+        main_menu.addAction(hide_column)
+        main_menu.addAction(show_all_columns)
+
+        main_menu.exec_(self.mapToGlobal(position))
+
+    def make_separator(self, horizontalHeader):
+        separator1 = QAction(horizontalHeader)
+        separator1.setSeparator(True)
+        return separator1
 
     @staticmethod
     def copy_to_clipboard(data):
-- 
GitLab