From 4e3d8b9da6e8fbe7634a302fbe7136b56648fb1e Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Fri, 27 Jan 2023 18:34:17 +0100 Subject: [PATCH 01/27] introduced MDISubPlotBase --- grum/mdi/mdisubplot.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/grum/mdi/mdisubplot.py b/grum/mdi/mdisubplot.py index 1c77d0f..57ca35d 100644 --- a/grum/mdi/mdisubplot.py +++ b/grum/mdi/mdisubplot.py @@ -3,30 +3,36 @@ from .mdisubwin import MDISubWindow from ..theme import pg_plot_style, pg_plot_style_cycler, pg_legend_style -class MDISubPlot(MDISubWindow): +class MDISubPlotBase(MDISubWindow): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.pw = pw = pg.PlotWidget() + self.setWidget(pw) + + + +class MDISubPlot(MDISubPlotBase): def __init__(self, name, desc, *args, **kwargs): super().__init__(name, *args, **kwargs) - pw = pg.PlotWidget() - self.setWidget(pw) style = pg_plot_style() - plot = desc.make_plot(pw, style) + plot = desc.make_plot(self.pw, style) self.plots = {name: plot} -class MDISubMultiPlot(MDISubWindow): +class MDISubMultiPlot(MDISubPlotBase): def __init__(self, name, descs, *args, **kwargs): super().__init__(name, *args, **kwargs) - pw = pg.PlotWidget() - self.setWidget(pw) ls = pg_legend_style() psc = pg_plot_style_cycler() + pw = self.pw pw.addLegend(**ls) self.plots = {name: desc.make_plot(pw, style) for (name, desc), style in zip(descs.items(), psc)} From aeaabd028f7f627c4fe06f32330800aa0c30facc Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Fri, 27 Jan 2023 18:50:34 +0100 Subject: [PATCH 02/27] connect tooltip display with xy coords to plot mouse-over event --- grum/mdi/mdisubplot.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/grum/mdi/mdisubplot.py b/grum/mdi/mdisubplot.py index 57ca35d..53bc67e 100644 --- a/grum/mdi/mdisubplot.py +++ b/grum/mdi/mdisubplot.py @@ -10,6 +10,18 @@ class MDISubPlotBase(MDISubWindow): self.pw = pw = pg.PlotWidget() self.setWidget(pw) + # connect to plot mouse-over event + pw.scene().sigMouseMoved.connect(self.on_hover) + + + def on_hover(self, event): + coord = self.pw.plotItem.vb.mapSceneToView(event) + x = coord.x() + y = coord.y() + x = round(x, 3) + y = round(y, 3) + self.setToolTip(f"x = {x}\ny = {y}") + class MDISubPlot(MDISubPlotBase): From 16c7b4f71c2311978b4280661de1cf209aaaa44d Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Sun, 29 Jan 2023 14:05:18 +0100 Subject: [PATCH 03/27] added maximize/restore to MDISubWindow, changed frame_on/frame_off from FramelessWindowHint to custom hint --- grum/mdi/mdiarea.py | 5 +++-- grum/mdi/mdisubwin.py | 19 +++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/grum/mdi/mdiarea.py b/grum/mdi/mdiarea.py index c7527ee..fa24c4a 100644 --- a/grum/mdi/mdiarea.py +++ b/grum/mdi/mdiarea.py @@ -67,6 +67,7 @@ class MDIArea(QMdiArea): self.menu.checkboxes["Multiple windows"].setChecked(True) self.enable_subwindow_view() for sub in self.subWindowList(): + sub.restore() sub.frame_on() def enable_single_window_mode(self): @@ -75,7 +76,7 @@ class MDIArea(QMdiArea): self.closeInactiveSubWindows() active = self.activeSubWindow() if active: - active.showMaximized() + active.maximize() active.frame_off() def enable_tabbed_mode(self): @@ -104,7 +105,7 @@ class MDIArea(QMdiArea): def add_single(self, sub): self.closeAllSubWindows() self.addSubWindow(sub) - sub.showMaximized() + sub.maximize() sub.frame_off() diff --git a/grum/mdi/mdisubwin.py b/grum/mdi/mdisubwin.py index 66728e1..64c45a1 100644 --- a/grum/mdi/mdisubwin.py +++ b/grum/mdi/mdisubwin.py @@ -14,12 +14,27 @@ class MDISubWindow(QMdiSubWindow): # without this, the SubWindow is not removed from the subWindowList self.setAttribute(Qt.WA_DeleteOnClose) + self._previous_state = None + + + def maximize(self): + self._previous_state = state = self.windowState() + self.setWindowState(state | Qt.WindowMaximized) + + def restore(self): + if self._previous_state: + self.setWindowState(self._previous_state) + def frame_on(self): - self.setWindowFlag(Qt.FramelessWindowHint, False) + self.hide() + self.setWindowFlags(Qt.SubWindow) + self.show() def frame_off(self): - self.setWindowFlag(Qt.FramelessWindowHint, True) + self.hide() + self.setWindowFlags(Qt.CustomizeWindowHint | Qt.WindowTitleHint) + self.show() From f491c7146242cf0347b768c7e069cb12a549bea2 Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Sun, 29 Jan 2023 14:19:34 +0100 Subject: [PATCH 04/27] adjusted test --- tests/test_mdiarea.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_mdiarea.py b/tests/test_mdiarea.py index 4f30954..26fd6d0 100644 --- a/tests/test_mdiarea.py +++ b/tests/test_mdiarea.py @@ -143,14 +143,14 @@ class TestMDIArea: mdi.closeAllSubWindows = mock.MagicMock() sine_item = self.mw.lst.lst.get("sine") sub = MDISubPlot("sine", sine_item.value) - sub.showMaximized = mock.MagicMock() + sub.maximize = mock.MagicMock() sub.frame_off = mock.MagicMock() mdi.add_single(sub) mdi.closeAllSubWindows.assert_called_once() mdi.addSubWindow.assert_called_once_with(sub) - sub.showMaximized.assert_called_once() + sub.maximize.assert_called_once() sub.frame_off.assert_called_once() From 660e8208ca8b29c4d39d0a6c0bbe9909d07aefbb Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Sun, 29 Jan 2023 16:32:41 +0100 Subject: [PATCH 05/27] added offline mode (does not start the RPC server) --- grum/cli.py | 11 ++++++----- grum/mainwin.py | 15 ++++++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/grum/cli.py b/grum/cli.py index e1df4ae..fb2d9b9 100644 --- a/grum/cli.py +++ b/grum/cli.py @@ -25,13 +25,14 @@ def handle_clargs(): DESC = "grum - GUI for Remote Unified Monitoring" parser = argparse.ArgumentParser(description=DESC, formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument("-H", "--host", default="localhost", help="Server host name") - parser.add_argument("-P", "--port", default=8000, type=int, help="Server port number") + parser.add_argument("-H", "--host", default="localhost", help="RPC server host name") + parser.add_argument("-P", "--port", default=8000, type=int, help="RPC server port number") + parser.add_argument("-o", "--offline", action="store_true", help="offline mode (do not run RPC server)") - parser.add_argument("-w", "--window-mode", default="multi", choices=MDIWindowMode.values(), type=unambiguous_window_mode, help="Set the initial window mode") + parser.add_argument("-w", "--window-mode", default="multi", choices=MDIWindowMode.values(), type=unambiguous_window_mode, help="set the initial window mode") - parser.add_argument("-e", "--examples", dest="add_examples", action="store_true", help="Add example data") - parser.add_argument("--no-theme", action="store_true", help="Disable theming") + parser.add_argument("-e", "--examples", dest="add_examples", action="store_true", help="add example data") + parser.add_argument("--no-theme", action="store_true", help="disable theming") return parser.parse_args().__dict__ diff --git a/grum/mainwin.py b/grum/mainwin.py index cd7ed77..b15ea6b 100644 --- a/grum/mainwin.py +++ b/grum/mainwin.py @@ -18,8 +18,12 @@ class MainWindow(QMainWindow): sig_make_new_plot = pyqtSignal(str, PlotDescription) - def __init__(self, *args, title="grum", host="localhost", port=8000, add_examples=False, window_mode=MDIWindowMode.MULTI, **kwargs): + def __init__(self, *args, title="grum", host="localhost", port=8000, offline=False, add_examples=False, window_mode=MDIWindowMode.MULTI, **kwargs): super().__init__(*args, **kwargs) + + if offline: + title = f"{title} (offline)" + self.setWindowTitle(title) self.setWindowIcon(assets.icon()) @@ -62,10 +66,11 @@ class MainWindow(QMainWindow): self.setCentralWidget(splitter) - self.rst = rst = RPCServerThread(host, port, doc_title_suffix=title) - rst.start() - rst.server.register_function(self.new_plot) - rst.server.register_function(self.append_data) + if not offline: + self.rst = rst = RPCServerThread(host, port, doc_title_suffix=title) + rst.start() + rst.server.register_function(self.new_plot) + rst.server.register_function(self.append_data) self.sig_make_new_plot.connect(self.on_make_new_plot) From b02d66b95f62a774e68b116ed2909f44d1173ead Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Wed, 1 Feb 2023 15:25:37 +0100 Subject: [PATCH 06/27] added possibility for having a sort key --- grum/dictlist/dictlistitem.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/grum/dictlist/dictlistitem.py b/grum/dictlist/dictlistitem.py index 982e24e..e8760da 100644 --- a/grum/dictlist/dictlistitem.py +++ b/grum/dictlist/dictlistitem.py @@ -12,6 +12,18 @@ class DictListItem(QListWidgetItem): self.key = key self.value = value self.set_alarm(False) + self._sort_key = None + + + def set_sort_key(self, sk): + self._sort_key = sk + + def __lt__(self, other): + sk = self._sort_key + if sk: + return sk(self) < sk(other) + else: + return super().__lt__(other) def set_alarm(self, state): From 31cbe8ac33ca6ec69616035e0d3505d1d08664ae Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Wed, 1 Feb 2023 15:26:17 +0100 Subject: [PATCH 07/27] added possibility to enabling/disabling sorting for the list --- grum/dictlist/dictlistwidget.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/grum/dictlist/dictlistwidget.py b/grum/dictlist/dictlistwidget.py index dabc018..17270f2 100644 --- a/grum/dictlist/dictlistwidget.py +++ b/grum/dictlist/dictlistwidget.py @@ -72,4 +72,27 @@ class DictListWidget(QListWidget): i.set_alarm(state) + def set_sort_key(self, sk): + for i in self.items.values(): + i.set_sort_key(sk) + + if sk is None: + self.setSortingEnabled(False) + else: + self.setSortingEnabled(True) + self.sortItems() + + + def disable_sorting(self): + # map order in dict to indices + mapping = enumerate(reversed(self.items.values())) + mapping = {item.text(): index for index, item in mapping} + + def unsort(x): + return mapping[x.text()] + + self.set_sort_key(unsort) + self.set_sort_key(None) + + From b380d840fc4baff0bcaf1b5f53d6ceccad975d00 Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Wed, 1 Feb 2023 15:26:38 +0100 Subject: [PATCH 08/27] attach sorting options to menu --- grum/mainwin.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/grum/mainwin.py b/grum/mainwin.py index b15ea6b..fd31ff4 100644 --- a/grum/mainwin.py +++ b/grum/mainwin.py @@ -43,6 +43,10 @@ class MainWindow(QMainWindow): lst_menu.addSeparator() lst_menu.addAction("Mark selected as seen", self.on_mark_selected_as_seen) lst_menu.addAction("Mark selected as not seen", self.on_mark_selected_as_not_seen) + lst_menu.addSeparator() + sort_group = lst_menu.addGroup() + sort_group.addCheckbox("Sorting disabled", triggered=self.on_sorting_disabled, state=True) + sort_group.addCheckbox("Sort by name", triggered=self.on_sort_by_name) shortcut(self, "Ctrl+P", self.on_plot_selected) @@ -119,6 +123,7 @@ class MainWindow(QMainWindow): else: self.plot_multiple_items(selected) + def on_mark_selected_as_seen(self): self.lst.set_alarm_for_selected(False) @@ -126,6 +131,13 @@ class MainWindow(QMainWindow): self.lst.set_alarm_for_selected(True) + def on_sort_by_name(self): + self.lst.set_sort_key(lambda x: x.value.name) + + def on_sorting_disabled(self): + self.lst.disable_sorting() + + def on_file_open(self): fns = open_h5_files_dialog(self) if not fns: From 9db8d5b87e4d44b03eda80e467b437887e5d5cdb Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Wed, 1 Feb 2023 15:31:31 +0100 Subject: [PATCH 09/27] cleanup --- grum/dictlist/dictlist.py | 2 +- grum/dictlist/searchbox.py | 2 +- grum/io.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/grum/dictlist/dictlist.py b/grum/dictlist/dictlist.py index aa7c706..91b188d 100644 --- a/grum/dictlist/dictlist.py +++ b/grum/dictlist/dictlist.py @@ -1,4 +1,4 @@ -from PyQt5.QtWidgets import QWidget, QLineEdit, QVBoxLayout +from PyQt5.QtWidgets import QWidget, QVBoxLayout from .dictlistwidget import DictListWidget from .searchbox import SearchBox diff --git a/grum/dictlist/searchbox.py b/grum/dictlist/searchbox.py index 76a8619..fcfd742 100644 --- a/grum/dictlist/searchbox.py +++ b/grum/dictlist/searchbox.py @@ -47,7 +47,7 @@ class SearchBox(QWidget): class SquareButton(QPushButton): - def resizeEvent(self, e): + def resizeEvent(self, _event): height = self.height() self.setMinimumWidth(height) diff --git a/grum/io.py b/grum/io.py index 5b67a48..950a804 100644 --- a/grum/io.py +++ b/grum/io.py @@ -31,7 +31,7 @@ def read_dict(fn): data = node[()] if isinstance(data, bytes): data = data.decode("utf-8") - res[node.name] = data + res[name] = data with h5py.File(fn, "r") as f: f.visititems(visit) @@ -44,7 +44,7 @@ def unflatten_dict(d, sep="/"): for k, v in d.items(): current = res levels = k.split(sep) - for l in levels[1:-1]: + for l in levels[:-1]: if l not in current: current[l] = {} current = current[l] From 9a66433a3e9a89e6ca3b35914f9b8c98b570d613 Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Wed, 1 Feb 2023 16:09:59 +0100 Subject: [PATCH 10/27] made LINE_COLORS tuple instead of list --- grum/theme.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grum/theme.py b/grum/theme.py index fe4dd90..002b07f 100644 --- a/grum/theme.py +++ b/grum/theme.py @@ -27,7 +27,7 @@ DOT_PEN = GREY5 DOT_BRUSH = HIGHLIGHT DOT_SIZE = 8 -LINE_COLORS = [ +LINE_COLORS = ( C.BLUE, C.BASE_GREEN, C.RED, @@ -38,7 +38,7 @@ LINE_COLORS = [ C.COOL3, C.BROWN1, C.BASE_ORANGE, -] +) From 829c9e2ded09720aa090cfc7afbbeebed1caa955 Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Wed, 1 Feb 2023 16:10:24 +0100 Subject: [PATCH 11/27] use different name inside function --- grum/shortcut.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/grum/shortcut.py b/grum/shortcut.py index 6845b4c..ec71c5a 100644 --- a/grum/shortcut.py +++ b/grum/shortcut.py @@ -4,9 +4,9 @@ from PyQt5.QtWidgets import QShortcut def shortcut(parent, key_sequence, triggered, **kwargs): key_sequence = QKeySequence(key_sequence) - shortcut = QShortcut(key_sequence, parent, **kwargs) - shortcut.activated.connect(triggered) - return shortcut + sc = QShortcut(key_sequence, parent, **kwargs) + sc.activated.connect(triggered) + return sc From 9e6896c30f79dc17b8eb0020c706df0093b4e484 Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Wed, 1 Feb 2023 16:10:47 +0100 Subject: [PATCH 12/27] fixed position of *args --- grum/mdi/mdiarea.py | 2 +- grum/rpc/rpcserver.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/grum/mdi/mdiarea.py b/grum/mdi/mdiarea.py index fa24c4a..a728c78 100644 --- a/grum/mdi/mdiarea.py +++ b/grum/mdi/mdiarea.py @@ -20,7 +20,7 @@ class MDIWindowMode(str, enum.Enum): class MDIArea(QMdiArea): - def __init__(self, bar, window_mode=MDIWindowMode.MULTI, *args, **kwargs): + def __init__(self, bar, *args, window_mode=MDIWindowMode.MULTI, **kwargs): super().__init__(*args, **kwargs) self.logo = assets.logo() self.setTabsClosable(True) diff --git a/grum/rpc/rpcserver.py b/grum/rpc/rpcserver.py index f8ac55d..2e3496b 100644 --- a/grum/rpc/rpcserver.py +++ b/grum/rpc/rpcserver.py @@ -4,7 +4,7 @@ from inspect import getdoc, signature class RPCServer(xrs.DocXMLRPCServer): - def __init__(self, host, port, doc_title_suffix="", *args, **kwargs): + def __init__(self, host, port, *args, doc_title_suffix="", **kwargs): addr = (host, port) kwargs.setdefault("allow_none", True) super().__init__(addr, *args, **kwargs) From 3ba74a1a1e3daea87835bbcea2d44a9a11bc1cb6 Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Wed, 1 Feb 2023 16:17:58 +0100 Subject: [PATCH 13/27] added a tests run script --- tests/run.sh | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100755 tests/run.sh diff --git a/tests/run.sh b/tests/run.sh new file mode 100755 index 0000000..a13dfdb --- /dev/null +++ b/tests/run.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +cd $(dirname $0) +cd .. + +export QT_QPA_PLATFORM=offscreen + +coverage run --source=./grum/ -m pytest ./tests/ +echo +coverage report + + From c7c17ef221ad50569fd2ed07938fc2a5a483ac5c Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Thu, 2 Feb 2023 11:59:05 +0100 Subject: [PATCH 14/27] change sort key on the class (not the instances) --- grum/dictlist/dictlistitem.py | 9 ++++++--- grum/dictlist/dictlistwidget.py | 3 +-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/grum/dictlist/dictlistitem.py b/grum/dictlist/dictlistitem.py index e8760da..9bfdf39 100644 --- a/grum/dictlist/dictlistitem.py +++ b/grum/dictlist/dictlistitem.py @@ -12,13 +12,16 @@ class DictListItem(QListWidgetItem): self.key = key self.value = value self.set_alarm(False) - self._sort_key = None - def set_sort_key(self, sk): - self._sort_key = sk + _sort_key = None + + @classmethod + def set_sort_key(cls, sk): + cls._sort_key = staticmethod(sk) # need to attach as staticmethod, otherwise self is inserted as first argument def __lt__(self, other): + assert self._sort_key == other._sort_key sk = self._sort_key if sk: return sk(self) < sk(other) diff --git a/grum/dictlist/dictlistwidget.py b/grum/dictlist/dictlistwidget.py index 17270f2..6d8d834 100644 --- a/grum/dictlist/dictlistwidget.py +++ b/grum/dictlist/dictlistwidget.py @@ -73,8 +73,7 @@ class DictListWidget(QListWidget): def set_sort_key(self, sk): - for i in self.items.values(): - i.set_sort_key(sk) + DictListItem.set_sort_key(sk) if sk is None: self.setSortingEnabled(False) From 4c533489f643bac80b1454fb1f12edefe238c06e Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Thu, 2 Feb 2023 13:28:52 +0100 Subject: [PATCH 15/27] added wildcard (*?) support --- grum/dictlist/dictlist.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/grum/dictlist/dictlist.py b/grum/dictlist/dictlist.py index 91b188d..b9b8d53 100644 --- a/grum/dictlist/dictlist.py +++ b/grum/dictlist/dictlist.py @@ -1,3 +1,6 @@ +import fnmatch +import re + from PyQt5.QtWidgets import QWidget, QVBoxLayout from .dictlistwidget import DictListWidget @@ -31,11 +34,26 @@ class DictList(QWidget): def hide_not_matching(self, pattern): - pattern = pattern.casefold() + g = Globber(pattern) for name, itm in self.lst.items.items(): - name = name.casefold() - state = (pattern not in name) - itm.setHidden(state) + state = g.match(name) + itm.setHidden(not state) + + + +class Globber: + + def __init__(self, pattern): + pattern = pattern.casefold() + pattern = "*" + pattern + "*" + regex = fnmatch.translate(pattern) + self.pattern = re.compile(regex) + + def match(self, string): + string = string.casefold() + matches = self.pattern.match(string) + state = bool(matches) + return state From 721f2a864a4eb7fc7668ae8a8749388e13ac0980 Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Thu, 2 Feb 2023 14:59:42 +0100 Subject: [PATCH 16/27] added docstrings for API methods --- grum/mainwin.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/grum/mainwin.py b/grum/mainwin.py index fd31ff4..9793517 100644 --- a/grum/mainwin.py +++ b/grum/mainwin.py @@ -87,12 +87,21 @@ class MainWindow(QMainWindow): # Remote API calls def new_plot(self, name, cfg): + """ + Create a new plot using the configuration dict . + The configuration is forwarded to the constructor of PlotDescription. + Allowed keys are: title, xlabel, ylabel, xs, ys. + """ desc = self.add_new_desc_to_list(name, cfg) if self.menu_settings.checkboxes["Open new plots"].isChecked(): if not self.mdi.findSubWindow(name): self.sig_make_new_plot.emit(name, desc) def append_data(self, name, point): + """ + Append a new data point to the (existing) plot . + The point is forwarded to the append method of PlotDescription. + """ item = self.lst.get(name) desc = item.value desc.append(point) From 26c338c54e47ffbff52a54eb22c234afae3691a7 Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Thu, 2 Feb 2023 14:59:51 +0100 Subject: [PATCH 17/27] added a comment --- grum/dictlist/dictlistitem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grum/dictlist/dictlistitem.py b/grum/dictlist/dictlistitem.py index 9bfdf39..59faf2c 100644 --- a/grum/dictlist/dictlistitem.py +++ b/grum/dictlist/dictlistitem.py @@ -16,7 +16,7 @@ class DictListItem(QListWidgetItem): _sort_key = None - @classmethod + @classmethod # need to update the class so that all instances are always consistent def set_sort_key(cls, sk): cls._sort_key = staticmethod(sk) # need to attach as staticmethod, otherwise self is inserted as first argument From 9b50ade34028bebf00874e389dbdc2f209f0d95a Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Thu, 2 Feb 2023 16:48:18 +0100 Subject: [PATCH 18/27] introduced "Sort by insertion order" (the default) / "Sorting disabled" keeps current order --- grum/dictlist/dictlistwidget.py | 10 +++++++++- grum/mainwin.py | 8 ++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/grum/dictlist/dictlistwidget.py b/grum/dictlist/dictlistwidget.py index 6d8d834..09a0990 100644 --- a/grum/dictlist/dictlistwidget.py +++ b/grum/dictlist/dictlistwidget.py @@ -82,7 +82,7 @@ class DictListWidget(QListWidget): self.sortItems() - def disable_sorting(self): + def enable_sort_by_insertion_order(self): # map order in dict to indices mapping = enumerate(reversed(self.items.values())) mapping = {item.text(): index for index, item in mapping} @@ -91,6 +91,14 @@ class DictListWidget(QListWidget): return mapping[x.text()] self.set_sort_key(unsort) + self.disable_sorting() + + + def enable_sort_by_text(self): + self.set_sort_key(lambda x: x.text()) + + + def disable_sorting(self): self.set_sort_key(None) diff --git a/grum/mainwin.py b/grum/mainwin.py index 9793517..a4d0bfe 100644 --- a/grum/mainwin.py +++ b/grum/mainwin.py @@ -45,8 +45,9 @@ class MainWindow(QMainWindow): lst_menu.addAction("Mark selected as not seen", self.on_mark_selected_as_not_seen) lst_menu.addSeparator() sort_group = lst_menu.addGroup() - sort_group.addCheckbox("Sorting disabled", triggered=self.on_sorting_disabled, state=True) + sort_group.addCheckbox("Sort by insertion order", triggered=self.on_sort_by_insertion_order, state=True) sort_group.addCheckbox("Sort by name", triggered=self.on_sort_by_name) + sort_group.addCheckbox("Sorting disabled", triggered=self.on_sorting_disabled) shortcut(self, "Ctrl+P", self.on_plot_selected) @@ -140,8 +141,11 @@ class MainWindow(QMainWindow): self.lst.set_alarm_for_selected(True) + def on_sort_by_insertion_order(self): + self.lst.enable_sort_by_insertion_order() + def on_sort_by_name(self): - self.lst.set_sort_key(lambda x: x.value.name) + self.lst.enable_sort_by_text() def on_sorting_disabled(self): self.lst.disable_sorting() From e4ada964e3de57c3f0bb20d5c6f917420cd306e9 Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Thu, 2 Feb 2023 17:27:20 +0100 Subject: [PATCH 19/27] (trying to) disable sorting if item manually moved --- grum/mainwin.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/grum/mainwin.py b/grum/mainwin.py index a4d0bfe..d2b8f0f 100644 --- a/grum/mainwin.py +++ b/grum/mainwin.py @@ -49,6 +49,13 @@ class MainWindow(QMainWindow): sort_group.addCheckbox("Sort by name", triggered=self.on_sort_by_name) sort_group.addCheckbox("Sorting disabled", triggered=self.on_sorting_disabled) + #TODO: clean up + def on_item_about_to_be_moved(): + sort_group.checkboxes["Sorting disabled"].setChecked(True) + self.on_sorting_disabled() + + self.lst.model().rowsAboutToBeMoved.connect(on_item_about_to_be_moved) + shortcut(self, "Ctrl+P", self.on_plot_selected) bar = self.menuBar() From 23afec2aa463af4ac83caa0c342611eb5f501906 Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Fri, 3 Feb 2023 00:53:19 +0100 Subject: [PATCH 20/27] fill xs and ys as argument --- grum/exampledata.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/grum/exampledata.py b/grum/exampledata.py index f6bb3b4..8eb609c 100644 --- a/grum/exampledata.py +++ b/grum/exampledata.py @@ -20,15 +20,15 @@ for i in range(20): exampledata = {} -for k, v in exampledata_raw.items(): - pd = PlotDescription( - k, -# title=k, +for name, (xs, ys) in exampledata_raw.items(): + exampledata[name] = PlotDescription( + name, +# title=name, xlabel="x", - ylabel="y" + ylabel="y", + xs=xs, + ys=ys ) - pd.xs, pd.ys = v - exampledata[k] = pd From 87ae26247d829b5588693647db29fbb67ed97244 Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Fri, 3 Feb 2023 00:54:00 +0100 Subject: [PATCH 21/27] attached timestamps object --- grum/dictlist/dictlistitem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/grum/dictlist/dictlistitem.py b/grum/dictlist/dictlistitem.py index 59faf2c..facf765 100644 --- a/grum/dictlist/dictlistitem.py +++ b/grum/dictlist/dictlistitem.py @@ -3,6 +3,7 @@ from PyQt5.QtWidgets import QListWidgetItem from PyQt5.QtGui import QIcon, QPixmap, QPainter from ..theme import DOT_PEN, DOT_BRUSH, DOT_SIZE +from .timestamps import Timestamps class DictListItem(QListWidgetItem): @@ -12,6 +13,7 @@ class DictListItem(QListWidgetItem): self.key = key self.value = value self.set_alarm(False) + self.timestamps = Timestamps() _sort_key = None From 5ea830bf2bf09daf000f95852feed3d8525b898b Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Fri, 3 Feb 2023 00:55:08 +0100 Subject: [PATCH 22/27] attached timestamps object --- grum/dictlist/dictlistitem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grum/dictlist/dictlistitem.py b/grum/dictlist/dictlistitem.py index facf765..e736e1c 100644 --- a/grum/dictlist/dictlistitem.py +++ b/grum/dictlist/dictlistitem.py @@ -12,8 +12,8 @@ class DictListItem(QListWidgetItem): super().__init__(key, *args, **kwargs) self.key = key self.value = value - self.set_alarm(False) self.timestamps = Timestamps() + self.set_alarm(False) _sort_key = None From cf4fbad5e7257c974b16d1709394d786b9969fa6 Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Fri, 3 Feb 2023 00:56:07 +0100 Subject: [PATCH 23/27] update timestamps; added "Sort by timestamp" --- grum/mainwin.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/grum/mainwin.py b/grum/mainwin.py index d2b8f0f..5de09de 100644 --- a/grum/mainwin.py +++ b/grum/mainwin.py @@ -47,6 +47,7 @@ class MainWindow(QMainWindow): sort_group = lst_menu.addGroup() sort_group.addCheckbox("Sort by insertion order", triggered=self.on_sort_by_insertion_order, state=True) sort_group.addCheckbox("Sort by name", triggered=self.on_sort_by_name) + sort_group.addCheckbox("Sort by timestamp", triggered=self.on_sort_by_timestamp) sort_group.addCheckbox("Sorting disabled", triggered=self.on_sorting_disabled) #TODO: clean up @@ -119,6 +120,7 @@ class MainWindow(QMainWindow): plot = sub.plots[name] plot.setData(*desc.data) alarm = False + item.timestamps.modification.update() item.set_alarm(alarm) @@ -154,6 +156,9 @@ class MainWindow(QMainWindow): def on_sort_by_name(self): self.lst.enable_sort_by_text() + def on_sort_by_timestamp(self): + self.lst.enable_sort_by_timestamp() + def on_sorting_disabled(self): self.lst.disable_sorting() @@ -190,12 +195,14 @@ class MainWindow(QMainWindow): return desc def plot_single_item(self, item): + item.timestamps.access.update() item.set_alarm(False) name, desc = item.key, item.value self.activate_or_make_subwin(MDISubPlot, name, desc) def plot_multiple_items(self, items): for i in items: + i.timestamps.access.update() i.set_alarm(False) descs = {i.key: i.value for i in items} names = descs.keys() From ab1d0d524e4785e1ca650b6ea5bedeea91f49466 Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Fri, 3 Feb 2023 00:57:26 +0100 Subject: [PATCH 24/27] correctly set descending order (except for "sort by text") --- grum/dictlist/dictlistwidget.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/grum/dictlist/dictlistwidget.py b/grum/dictlist/dictlistwidget.py index 09a0990..b1af848 100644 --- a/grum/dictlist/dictlistwidget.py +++ b/grum/dictlist/dictlistwidget.py @@ -72,19 +72,20 @@ class DictListWidget(QListWidget): i.set_alarm(state) - def set_sort_key(self, sk): + def set_sort_key(self, sk, reverse=True): DictListItem.set_sort_key(sk) if sk is None: self.setSortingEnabled(False) else: self.setSortingEnabled(True) - self.sortItems() + order = Qt.DescendingOrder if reverse else Qt.AscendingOrder + self.sortItems(order) def enable_sort_by_insertion_order(self): # map order in dict to indices - mapping = enumerate(reversed(self.items.values())) + mapping = enumerate(self.items.values()) mapping = {item.text(): index for index, item in mapping} def unsort(x): @@ -95,7 +96,11 @@ class DictListWidget(QListWidget): def enable_sort_by_text(self): - self.set_sort_key(lambda x: x.text()) + self.set_sort_key(lambda x: x.text(), reverse=False) + + + def enable_sort_by_timestamp(self): + self.set_sort_key(lambda x: (x.timestamps.max(), x.text())) def disable_sorting(self): From 495a9a43bcb9f959d547fc65629dea860647d1dd Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Sat, 4 Feb 2023 18:52:49 +0100 Subject: [PATCH 25/27] added eviction, attached it to list changes signal --- grum/dictlist/dictlistwidget.py | 34 +++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/grum/dictlist/dictlistwidget.py b/grum/dictlist/dictlistwidget.py index b1af848..a27f14d 100644 --- a/grum/dictlist/dictlistwidget.py +++ b/grum/dictlist/dictlistwidget.py @@ -14,8 +14,12 @@ class DictListWidget(QListWidget): self.setSelectionMode(QListWidget.ExtendedSelection) self.items = {} self._add_menu() + shortcut(self, "Del", self.delete_selected, context=Qt.WidgetShortcut) + self.nkeep = None + self.model().rowsInserted.connect(self.on_evict) + def _add_menu(self): self.menu = menu = RClickMenu(self) @@ -100,6 +104,7 @@ class DictListWidget(QListWidget): def enable_sort_by_timestamp(self): + # fall back to name for identical timestamps self.set_sort_key(lambda x: (x.timestamps.max(), x.text())) @@ -107,4 +112,33 @@ class DictListWidget(QListWidget): self.set_sort_key(None) + def set_nkeep(self, n): + self.nkeep = n + self.on_evict() + + + def on_evict(self): + if self.nkeep: + self.evict(self.nkeep) + + + def evict(self, nkeep): + items = self.items.values() + + # map order in dict to indices + mapping = enumerate(items) + mapping = {item.text(): index for index, item in mapping} + + def sk(x): + # fall back to insertion order for identical timestamps + return (x.timestamps.max(), mapping[x.text()]) + + items = sorted(items, key=sk, reverse=True) + items_to_evict = items[nkeep:] + + for i in items_to_evict: + self.items.pop(i.key) + self.deleteItem(i) + + From c620f17344a50d3ec3032047046ca41d5370a2ce Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Sat, 4 Feb 2023 18:53:53 +0100 Subject: [PATCH 26/27] added addEntrybox --- grum/menus/menubase.py | 46 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/grum/menus/menubase.py b/grum/menus/menubase.py index 0642c86..f84298a 100644 --- a/grum/menus/menubase.py +++ b/grum/menus/menubase.py @@ -1,5 +1,5 @@ -from PyQt5.QtGui import QKeySequence -from PyQt5.QtWidgets import QAction, QActionGroup +from PyQt5.QtGui import QKeySequence, QIntValidator +from PyQt5.QtWidgets import QAction, QActionGroup, QWidgetAction, QLineEdit class MenuBase: @@ -31,6 +31,48 @@ class MenuBase: self.checkboxes[name] = action return action + + def addEntrybox(self, name, placeholder=None, state=False, triggered=None): + cb = self.addCheckbox(name, state=state) + + edit = QLineEdit(self.qmenu) + edit.setValidator(QIntValidator()) #TODO: make optional + edit.setEnabled(state) + if placeholder: + edit.setPlaceholderText(placeholder) + edit.setContentsMargins(8, 5, 8, 5) # mimic margins of other actions TODO: this probably depends on the theme + + @cb.toggled.connect + def propagate_state_and_keep_menu_open_and_focus_edit(checked): + edit.setEnabled(checked) + if checked: + self.qmenu.show() + self.qmenu.setActiveAction(cb) + edit.setFocus() + + @edit.returnPressed.connect + def close_menu(): + self.qmenu.close() + + if triggered: + @cb.triggered.connect + def confirm_disabled(checked): + if not checked: + triggered(None) + + if triggered: + @edit.returnPressed.connect + def confirm_value(): + value = edit.text() + value = int(value) + triggered(value) + + action = QWidgetAction(self.qmenu) + action.setDefaultWidget(edit) + self.qmenu.addAction(action) + return action + + def addSeparator(self): self.qmenu.addSeparator() From 76e3a04d98b5794ad224c3579ee21964296596aa Mon Sep 17 00:00:00 2001 From: Sven Augustin Date: Sat, 4 Feb 2023 18:54:36 +0100 Subject: [PATCH 27/27] added addEntrybox for max #entries --- grum/mainwin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/grum/mainwin.py b/grum/mainwin.py index 5de09de..b4a3651 100644 --- a/grum/mainwin.py +++ b/grum/mainwin.py @@ -69,6 +69,8 @@ class MainWindow(QMainWindow): self.menu_settings = menu = BarMenu(bar, "&Settings") menu.addCheckbox("Open new plots", state=True) + menu.addSeparator() + menu.addEntrybox("Limit number of entries", placeholder="Maximum number of entries", triggered=lst.set_nkeep) self.mdi = mdi = MDIArea(bar, window_mode=window_mode)