Compare commits
46 Commits
tests
...
segfault-d
| Author | SHA1 | Date | |
|---|---|---|---|
| 72c51c5744 | |||
| 1e203873c9 | |||
| a80b568894 | |||
| 2edc68151d | |||
| aa03291e42 | |||
| 5d97dd6224 | |||
| bed086f95f | |||
| aa79f1b0a1 | |||
| bbb6d6c00c | |||
| c6f034f2d3 | |||
| a624cf37a4 | |||
| efd023ae3a | |||
| d65e97c9a7 | |||
| 4fcc10f3da | |||
| c781601246 | |||
| 831b03a744 | |||
| b94294b579 | |||
| b14f6e68b4 | |||
| 76e3a04d98 | |||
| c620f17344 | |||
| 495a9a43bc | |||
| ab1d0d524e | |||
| cf4fbad5e7 | |||
| 5ea830bf2b | |||
| 87ae26247d | |||
| 23afec2aa4 | |||
| e4ada964e3 | |||
| 9b50ade340 | |||
| 26c338c54e | |||
| 721f2a864a | |||
| 4c533489f6 | |||
| c7c17ef221 | |||
| 3ba74a1a1e | |||
| 9e6896c30f | |||
| 829c9e2ded | |||
| 9a66433a3e | |||
| 9db8d5b87e | |||
| b380d840fc | |||
| 31cbe8ac33 | |||
| b02d66b95f | |||
| 660e8208ca | |||
| f491c71462 | |||
| 16c7b4f71c | |||
| 7d169fdd53 | |||
| aeaabd028f | |||
| 4e3d8b9da6 |
@@ -1,29 +1,62 @@
|
|||||||
stages:
|
stages:
|
||||||
- Test
|
- Tests
|
||||||
|
- OptionalTests
|
||||||
|
|
||||||
|
.install-grum-test: &install-grum-test
|
||||||
|
- pip install pytest pytest-random-order pytest-cov
|
||||||
|
- pip install -e ./
|
||||||
|
- apt-get update
|
||||||
|
- apt-get install -y ffmpeg libnss3 libxcomposite1 libxtst6
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
stage: Test
|
stage: Tests
|
||||||
|
image: python:3.8
|
||||||
variables:
|
variables:
|
||||||
QT_QPA_PLATFORM: "offscreen"
|
QT_QPA_PLATFORM: "offscreen"
|
||||||
XDG_RUNTIME_DIR: "/tmp/runtime-root"
|
XDG_RUNTIME_DIR: "/tmp/runtime-root"
|
||||||
|
PYTHONFAULTHANDLER: 1
|
||||||
script:
|
script:
|
||||||
- pip install pytest pytest-random-order pytest-cov
|
- *install-grum-test
|
||||||
- pip install -e ./
|
- coverage run --source=./grum -m pytest ./tests --junitxml=report-junit.xml
|
||||||
- pip install PyQtWebEngine
|
|
||||||
|
|
||||||
- apt-get update
|
|
||||||
- apt-get install -y ffmpeg libnss3 libxcomposite1 libxtst6
|
|
||||||
|
|
||||||
# - python -m unittest discover -f ./tests
|
|
||||||
# - coverage run --source=./grum -m unittest discover -f ./tests
|
|
||||||
- coverage run --source=./grum -m pytest ./tests
|
|
||||||
- coverage report
|
- coverage report
|
||||||
- coverage xml
|
- coverage xml
|
||||||
|
|
||||||
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
|
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
|
||||||
artifacts:
|
artifacts:
|
||||||
|
when: always
|
||||||
reports:
|
reports:
|
||||||
cobertura: coverage.xml
|
cobertura: coverage.xml
|
||||||
|
junit: report-junit.xml
|
||||||
|
|
||||||
|
tests-3.6:
|
||||||
|
stage: OptionalTests
|
||||||
|
image: python:3.6
|
||||||
|
needs: ["tests"]
|
||||||
|
allow_failure: true
|
||||||
|
variables:
|
||||||
|
QT_QPA_PLATFORM: "offscreen"
|
||||||
|
XDG_RUNTIME_DIR: "/tmp/runtime-root"
|
||||||
|
PYTHONFAULTHANDLER: 1
|
||||||
|
script:
|
||||||
|
- *install-grum-test
|
||||||
|
- pytest ./tests
|
||||||
|
|
||||||
|
tests-3.7:
|
||||||
|
extends: "tests-3.6"
|
||||||
|
image: python:3.7
|
||||||
|
|
||||||
|
#tests-3.8:
|
||||||
|
# extends: "tests-3.6"
|
||||||
|
# image: python:3.8
|
||||||
|
|
||||||
|
tests-3.9:
|
||||||
|
extends: "tests-3.6"
|
||||||
|
image: python:3.9
|
||||||
|
|
||||||
|
tests-3.10:
|
||||||
|
extends: "tests-3.6"
|
||||||
|
image: python:3.10
|
||||||
|
|
||||||
|
tests-3.11:
|
||||||
|
extends: "tests-3.6"
|
||||||
|
image: python:3.11
|
||||||
|
|
||||||
|
|||||||
11
grum/cli.py
11
grum/cli.py
@@ -25,13 +25,14 @@ def handle_clargs():
|
|||||||
DESC = "grum - GUI for Remote Unified Monitoring"
|
DESC = "grum - GUI for Remote Unified Monitoring"
|
||||||
parser = argparse.ArgumentParser(description=DESC, formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
parser = argparse.ArgumentParser(description=DESC, formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
|
||||||
parser.add_argument("-H", "--host", default="localhost", help="Server host name")
|
parser.add_argument("-H", "--host", default="localhost", help="RPC server host name")
|
||||||
parser.add_argument("-P", "--port", default=8000, type=int, help="Server port number")
|
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("-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("--no-theme", action="store_true", help="disable theming")
|
||||||
|
|
||||||
return parser.parse_args().__dict__
|
return parser.parse_args().__dict__
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
from PyQt5.QtWidgets import QWidget, QLineEdit, QVBoxLayout
|
import fnmatch
|
||||||
|
import re
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import QWidget, QVBoxLayout
|
||||||
|
|
||||||
from .dictlistwidget import DictListWidget
|
from .dictlistwidget import DictListWidget
|
||||||
from .searchbox import SearchBox
|
from .searchbox import SearchBox
|
||||||
@@ -31,11 +34,26 @@ class DictList(QWidget):
|
|||||||
|
|
||||||
|
|
||||||
def hide_not_matching(self, pattern):
|
def hide_not_matching(self, pattern):
|
||||||
pattern = pattern.casefold()
|
g = Globber(pattern)
|
||||||
for name, itm in self.lst.items.items():
|
for name, itm in self.lst.items.items():
|
||||||
name = name.casefold()
|
state = g.match(name)
|
||||||
state = (pattern not in name)
|
itm.setHidden(not state)
|
||||||
itm.setHidden(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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from PyQt5.QtWidgets import QListWidgetItem
|
|||||||
from PyQt5.QtGui import QIcon, QPixmap, QPainter
|
from PyQt5.QtGui import QIcon, QPixmap, QPainter
|
||||||
|
|
||||||
from ..theme import DOT_PEN, DOT_BRUSH, DOT_SIZE
|
from ..theme import DOT_PEN, DOT_BRUSH, DOT_SIZE
|
||||||
|
from .timestamps import Timestamps
|
||||||
|
|
||||||
|
|
||||||
class DictListItem(QListWidgetItem):
|
class DictListItem(QListWidgetItem):
|
||||||
@@ -11,9 +12,25 @@ class DictListItem(QListWidgetItem):
|
|||||||
super().__init__(key, *args, **kwargs)
|
super().__init__(key, *args, **kwargs)
|
||||||
self.key = key
|
self.key = key
|
||||||
self.value = value
|
self.value = value
|
||||||
|
self.timestamps = Timestamps()
|
||||||
self.set_alarm(False)
|
self.set_alarm(False)
|
||||||
|
|
||||||
|
|
||||||
|
_sort_key = None
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
assert self._sort_key == other._sort_key
|
||||||
|
sk = self._sort_key
|
||||||
|
if sk:
|
||||||
|
return sk(self) < sk(other)
|
||||||
|
else:
|
||||||
|
return super().__lt__(other)
|
||||||
|
|
||||||
|
|
||||||
def set_alarm(self, state):
|
def set_alarm(self, state):
|
||||||
self.set_bold(state)
|
self.set_bold(state)
|
||||||
self.set_dot(state)
|
self.set_dot(state)
|
||||||
|
|||||||
@@ -14,8 +14,12 @@ class DictListWidget(QListWidget):
|
|||||||
self.setSelectionMode(QListWidget.ExtendedSelection)
|
self.setSelectionMode(QListWidget.ExtendedSelection)
|
||||||
self.items = {}
|
self.items = {}
|
||||||
self._add_menu()
|
self._add_menu()
|
||||||
|
|
||||||
shortcut(self, "Del", self.delete_selected, context=Qt.WidgetShortcut)
|
shortcut(self, "Del", self.delete_selected, context=Qt.WidgetShortcut)
|
||||||
|
|
||||||
|
self.nkeep = None
|
||||||
|
self.model().rowsInserted.connect(self.on_evict)
|
||||||
|
|
||||||
|
|
||||||
def _add_menu(self):
|
def _add_menu(self):
|
||||||
self.menu = menu = RClickMenu(self)
|
self.menu = menu = RClickMenu(self)
|
||||||
@@ -72,4 +76,69 @@ class DictListWidget(QListWidget):
|
|||||||
i.set_alarm(state)
|
i.set_alarm(state)
|
||||||
|
|
||||||
|
|
||||||
|
def set_sort_key(self, sk, reverse=True):
|
||||||
|
DictListItem.set_sort_key(sk)
|
||||||
|
|
||||||
|
if sk is None:
|
||||||
|
self.setSortingEnabled(False)
|
||||||
|
else:
|
||||||
|
self.setSortingEnabled(True)
|
||||||
|
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(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.disable_sorting()
|
||||||
|
|
||||||
|
|
||||||
|
def enable_sort_by_text(self):
|
||||||
|
self.set_sort_key(lambda x: x.text(), reverse=False)
|
||||||
|
|
||||||
|
|
||||||
|
def enable_sort_by_timestamp(self):
|
||||||
|
# fall back to name for identical timestamps
|
||||||
|
self.set_sort_key(lambda x: (x.timestamps.max(), x.text()))
|
||||||
|
|
||||||
|
|
||||||
|
def disable_sorting(self):
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class SearchBox(QWidget):
|
|||||||
|
|
||||||
class SquareButton(QPushButton):
|
class SquareButton(QPushButton):
|
||||||
|
|
||||||
def resizeEvent(self, e):
|
def resizeEvent(self, _event):
|
||||||
height = self.height()
|
height = self.height()
|
||||||
self.setMinimumWidth(height)
|
self.setMinimumWidth(height)
|
||||||
|
|
||||||
|
|||||||
@@ -20,15 +20,15 @@ for i in range(20):
|
|||||||
|
|
||||||
|
|
||||||
exampledata = {}
|
exampledata = {}
|
||||||
for k, v in exampledata_raw.items():
|
for name, (xs, ys) in exampledata_raw.items():
|
||||||
pd = PlotDescription(
|
exampledata[name] = PlotDescription(
|
||||||
k,
|
name,
|
||||||
# title=k,
|
# title=name,
|
||||||
xlabel="x",
|
xlabel="x",
|
||||||
ylabel="y"
|
ylabel="y",
|
||||||
|
xs=xs,
|
||||||
|
ys=ys
|
||||||
)
|
)
|
||||||
pd.xs, pd.ys = v
|
|
||||||
exampledata[k] = pd
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ def read_dict(fn):
|
|||||||
data = node[()]
|
data = node[()]
|
||||||
if isinstance(data, bytes):
|
if isinstance(data, bytes):
|
||||||
data = data.decode("utf-8")
|
data = data.decode("utf-8")
|
||||||
res[node.name] = data
|
res[name] = data
|
||||||
|
|
||||||
with h5py.File(fn, "r") as f:
|
with h5py.File(fn, "r") as f:
|
||||||
f.visititems(visit)
|
f.visititems(visit)
|
||||||
@@ -44,7 +44,7 @@ def unflatten_dict(d, sep="/"):
|
|||||||
for k, v in d.items():
|
for k, v in d.items():
|
||||||
current = res
|
current = res
|
||||||
levels = k.split(sep)
|
levels = k.split(sep)
|
||||||
for l in levels[1:-1]:
|
for l in levels[:-1]:
|
||||||
if l not in current:
|
if l not in current:
|
||||||
current[l] = {}
|
current[l] = {}
|
||||||
current = current[l]
|
current = current[l]
|
||||||
|
|||||||
@@ -18,8 +18,12 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
sig_make_new_plot = pyqtSignal(str, PlotDescription)
|
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)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if offline:
|
||||||
|
title = f"{title} (offline)"
|
||||||
|
|
||||||
self.setWindowTitle(title)
|
self.setWindowTitle(title)
|
||||||
self.setWindowIcon(assets.icon())
|
self.setWindowIcon(assets.icon())
|
||||||
|
|
||||||
@@ -39,6 +43,19 @@ class MainWindow(QMainWindow):
|
|||||||
lst_menu.addSeparator()
|
lst_menu.addSeparator()
|
||||||
lst_menu.addAction("Mark selected as seen", self.on_mark_selected_as_seen)
|
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.addAction("Mark selected as not seen", self.on_mark_selected_as_not_seen)
|
||||||
|
lst_menu.addSeparator()
|
||||||
|
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
|
||||||
|
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)
|
shortcut(self, "Ctrl+P", self.on_plot_selected)
|
||||||
|
|
||||||
@@ -52,6 +69,8 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
self.menu_settings = menu = BarMenu(bar, "&Settings")
|
self.menu_settings = menu = BarMenu(bar, "&Settings")
|
||||||
menu.addCheckbox("Open new plots", state=True)
|
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)
|
self.mdi = mdi = MDIArea(bar, window_mode=window_mode)
|
||||||
|
|
||||||
@@ -62,10 +81,13 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
self.setCentralWidget(splitter)
|
self.setCentralWidget(splitter)
|
||||||
|
|
||||||
self.rst = rst = RPCServerThread(host, port, doc_title_suffix=title)
|
if not offline:
|
||||||
rst.start()
|
self.rst = rst = RPCServerThread(host, port, doc_title_suffix=title)
|
||||||
rst.server.register_function(self.new_plot)
|
rst.start()
|
||||||
rst.server.register_function(self.append_data)
|
rst.server.register_function(self.new_plot)
|
||||||
|
rst.server.register_function(self.append_data)
|
||||||
|
rst.server.register_function(self.extend_data)
|
||||||
|
rst.server.register_function(self.set_data)
|
||||||
|
|
||||||
self.sig_make_new_plot.connect(self.on_make_new_plot)
|
self.sig_make_new_plot.connect(self.on_make_new_plot)
|
||||||
|
|
||||||
@@ -78,22 +100,45 @@ class MainWindow(QMainWindow):
|
|||||||
# Remote API calls
|
# Remote API calls
|
||||||
|
|
||||||
def new_plot(self, name, cfg):
|
def new_plot(self, name, cfg):
|
||||||
|
"""
|
||||||
|
Create a new plot <name> using the configuration dict <cfg>.
|
||||||
|
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)
|
desc = self.add_new_desc_to_list(name, cfg)
|
||||||
if self.menu_settings.checkboxes["Open new plots"].isChecked():
|
if self.menu_settings.checkboxes["Open new plots"].isChecked():
|
||||||
if not self.mdi.findSubWindow(name):
|
if not self.mdi.findSubWindow(name):
|
||||||
self.sig_make_new_plot.emit(name, desc)
|
self.sig_make_new_plot.emit(name, desc)
|
||||||
|
|
||||||
def append_data(self, name, point):
|
def append_data(self, name, point):
|
||||||
|
"""
|
||||||
|
Append a new data point <point> to the (existing) plot <name>.
|
||||||
|
The point is forwarded to the append method of PlotDescription.
|
||||||
|
"""
|
||||||
item = self.lst.get(name)
|
item = self.lst.get(name)
|
||||||
desc = item.value
|
desc = item.value
|
||||||
desc.append(point)
|
desc.append(point)
|
||||||
alarm = True
|
self.sync_item_and_plots(item)
|
||||||
for sub in self.mdi.subWindowList():
|
|
||||||
if name in sub.plots:
|
def extend_data(self, name, data):
|
||||||
plot = sub.plots[name]
|
"""
|
||||||
plot.setData(*desc.data)
|
Extend the current data of the (existing) plot <name>.by <data>
|
||||||
alarm = False
|
The data is forwarded to the extend method of PlotDescription.
|
||||||
item.set_alarm(alarm)
|
"""
|
||||||
|
item = self.lst.get(name)
|
||||||
|
desc = item.value
|
||||||
|
desc.extend(data)
|
||||||
|
self.sync_item_and_plots(item)
|
||||||
|
|
||||||
|
def set_data(self, name, data):
|
||||||
|
"""
|
||||||
|
Set <data> as the data of the (existing) plot <name>.
|
||||||
|
The data is assigned to the data attribute of PlotDescription.
|
||||||
|
"""
|
||||||
|
item = self.lst.get(name)
|
||||||
|
desc = item.value
|
||||||
|
desc.data = data
|
||||||
|
self.sync_item_and_plots(item)
|
||||||
|
|
||||||
|
|
||||||
# Signal callbacks
|
# Signal callbacks
|
||||||
@@ -114,6 +159,7 @@ class MainWindow(QMainWindow):
|
|||||||
else:
|
else:
|
||||||
self.plot_multiple_items(selected)
|
self.plot_multiple_items(selected)
|
||||||
|
|
||||||
|
|
||||||
def on_mark_selected_as_seen(self):
|
def on_mark_selected_as_seen(self):
|
||||||
self.lst.set_alarm_for_selected(False)
|
self.lst.set_alarm_for_selected(False)
|
||||||
|
|
||||||
@@ -121,6 +167,19 @@ class MainWindow(QMainWindow):
|
|||||||
self.lst.set_alarm_for_selected(True)
|
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.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()
|
||||||
|
|
||||||
|
|
||||||
def on_file_open(self):
|
def on_file_open(self):
|
||||||
fns = open_h5_files_dialog(self)
|
fns = open_h5_files_dialog(self)
|
||||||
if not fns:
|
if not fns:
|
||||||
@@ -152,13 +211,26 @@ class MainWindow(QMainWindow):
|
|||||||
self.lst.set(name, desc)
|
self.lst.set(name, desc)
|
||||||
return desc
|
return desc
|
||||||
|
|
||||||
|
def sync_item_and_plots(self, item):
|
||||||
|
name, desc = item.key, item.value
|
||||||
|
alarm = True
|
||||||
|
for sub in self.mdi.subWindowList():
|
||||||
|
if name in sub.plots:
|
||||||
|
plot = sub.plots[name]
|
||||||
|
plot.setData(*desc.data)
|
||||||
|
alarm = False
|
||||||
|
item.timestamps.modification.update()
|
||||||
|
item.set_alarm(alarm)
|
||||||
|
|
||||||
def plot_single_item(self, item):
|
def plot_single_item(self, item):
|
||||||
|
item.timestamps.access.update()
|
||||||
item.set_alarm(False)
|
item.set_alarm(False)
|
||||||
name, desc = item.key, item.value
|
name, desc = item.key, item.value
|
||||||
self.activate_or_make_subwin(MDISubPlot, name, desc)
|
self.activate_or_make_subwin(MDISubPlot, name, desc)
|
||||||
|
|
||||||
def plot_multiple_items(self, items):
|
def plot_multiple_items(self, items):
|
||||||
for i in items:
|
for i in items:
|
||||||
|
i.timestamps.access.update()
|
||||||
i.set_alarm(False)
|
i.set_alarm(False)
|
||||||
descs = {i.key: i.value for i in items}
|
descs = {i.key: i.value for i in items}
|
||||||
names = descs.keys()
|
names = descs.keys()
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class MDIWindowMode(str, enum.Enum):
|
|||||||
|
|
||||||
class MDIArea(QMdiArea):
|
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)
|
super().__init__(*args, **kwargs)
|
||||||
self.logo = assets.logo()
|
self.logo = assets.logo()
|
||||||
self.setTabsClosable(True)
|
self.setTabsClosable(True)
|
||||||
@@ -67,6 +67,7 @@ class MDIArea(QMdiArea):
|
|||||||
self.menu.checkboxes["Multiple windows"].setChecked(True)
|
self.menu.checkboxes["Multiple windows"].setChecked(True)
|
||||||
self.enable_subwindow_view()
|
self.enable_subwindow_view()
|
||||||
for sub in self.subWindowList():
|
for sub in self.subWindowList():
|
||||||
|
sub.restore()
|
||||||
sub.frame_on()
|
sub.frame_on()
|
||||||
|
|
||||||
def enable_single_window_mode(self):
|
def enable_single_window_mode(self):
|
||||||
@@ -75,7 +76,7 @@ class MDIArea(QMdiArea):
|
|||||||
self.closeInactiveSubWindows()
|
self.closeInactiveSubWindows()
|
||||||
active = self.activeSubWindow()
|
active = self.activeSubWindow()
|
||||||
if active:
|
if active:
|
||||||
active.showMaximized()
|
active.maximize()
|
||||||
active.frame_off()
|
active.frame_off()
|
||||||
|
|
||||||
def enable_tabbed_mode(self):
|
def enable_tabbed_mode(self):
|
||||||
@@ -104,7 +105,7 @@ class MDIArea(QMdiArea):
|
|||||||
def add_single(self, sub):
|
def add_single(self, sub):
|
||||||
self.closeAllSubWindows()
|
self.closeAllSubWindows()
|
||||||
self.addSubWindow(sub)
|
self.addSubWindow(sub)
|
||||||
sub.showMaximized()
|
sub.maximize()
|
||||||
sub.frame_off()
|
sub.frame_off()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,30 +3,48 @@ from .mdisubwin import MDISubWindow
|
|||||||
from ..theme import pg_plot_style, pg_plot_style_cycler, pg_legend_style
|
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)
|
||||||
|
|
||||||
|
# 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):
|
||||||
|
|
||||||
def __init__(self, name, desc, *args, **kwargs):
|
def __init__(self, name, desc, *args, **kwargs):
|
||||||
super().__init__(name, *args, **kwargs)
|
super().__init__(name, *args, **kwargs)
|
||||||
pw = pg.PlotWidget()
|
|
||||||
self.setWidget(pw)
|
|
||||||
|
|
||||||
style = pg_plot_style()
|
style = pg_plot_style()
|
||||||
|
|
||||||
plot = desc.make_plot(pw, style)
|
plot = desc.make_plot(self.pw, style)
|
||||||
self.plots = {name: plot}
|
self.plots = {name: plot}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class MDISubMultiPlot(MDISubWindow):
|
class MDISubMultiPlot(MDISubPlotBase):
|
||||||
|
|
||||||
def __init__(self, name, descs, *args, **kwargs):
|
def __init__(self, name, descs, *args, **kwargs):
|
||||||
super().__init__(name, *args, **kwargs)
|
super().__init__(name, *args, **kwargs)
|
||||||
pw = pg.PlotWidget()
|
|
||||||
self.setWidget(pw)
|
|
||||||
|
|
||||||
ls = pg_legend_style()
|
ls = pg_legend_style()
|
||||||
psc = pg_plot_style_cycler()
|
psc = pg_plot_style_cycler()
|
||||||
|
|
||||||
|
pw = self.pw
|
||||||
pw.addLegend(**ls)
|
pw.addLegend(**ls)
|
||||||
self.plots = {name: desc.make_plot(pw, style) for (name, desc), style in zip(descs.items(), psc)}
|
self.plots = {name: desc.make_plot(pw, style) for (name, desc), style in zip(descs.items(), psc)}
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,27 @@ class MDISubWindow(QMdiSubWindow):
|
|||||||
# without this, the SubWindow is not removed from the subWindowList
|
# without this, the SubWindow is not removed from the subWindowList
|
||||||
self.setAttribute(Qt.WA_DeleteOnClose)
|
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):
|
def frame_on(self):
|
||||||
self.setWindowFlag(Qt.FramelessWindowHint, False)
|
self.hide()
|
||||||
|
self.setWindowFlags(Qt.SubWindow)
|
||||||
|
self.show()
|
||||||
|
|
||||||
def frame_off(self):
|
def frame_off(self):
|
||||||
self.setWindowFlag(Qt.FramelessWindowHint, True)
|
self.hide()
|
||||||
|
self.setWindowFlags(Qt.CustomizeWindowHint | Qt.WindowTitleHint)
|
||||||
|
self.show()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from PyQt5.QtGui import QKeySequence
|
from PyQt5.QtGui import QKeySequence, QIntValidator
|
||||||
from PyQt5.QtWidgets import QAction, QActionGroup
|
from PyQt5.QtWidgets import QAction, QActionGroup, QWidgetAction, QLineEdit
|
||||||
|
|
||||||
|
|
||||||
class MenuBase:
|
class MenuBase:
|
||||||
@@ -31,6 +31,48 @@ class MenuBase:
|
|||||||
self.checkboxes[name] = action
|
self.checkboxes[name] = action
|
||||||
return 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):
|
def addSeparator(self):
|
||||||
self.qmenu.addSeparator()
|
self.qmenu.addSeparator()
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,21 @@ class PlotDescription:
|
|||||||
def data(self):
|
def data(self):
|
||||||
return (self.xs, self.ys)
|
return (self.xs, self.ys)
|
||||||
|
|
||||||
|
@data.setter
|
||||||
|
def data(self, value):
|
||||||
|
self.xs, self.ys = value
|
||||||
|
|
||||||
|
|
||||||
def append(self, xy):
|
def append(self, xy):
|
||||||
x, y = xy
|
x, y = xy
|
||||||
self.xs.append(x)
|
self.xs.append(x)
|
||||||
self.ys.append(y)
|
self.ys.append(y)
|
||||||
|
|
||||||
|
def extend(self, data):
|
||||||
|
xs, ys = data
|
||||||
|
self.xs.extend(xs)
|
||||||
|
self.ys.extend(ys)
|
||||||
|
|
||||||
|
|
||||||
def make_plot(self, plotwidget, style):
|
def make_plot(self, plotwidget, style):
|
||||||
res = plotwidget.plot(self.xs, self.ys, name=self.name, **style)
|
res = plotwidget.plot(self.xs, self.ys, name=self.name, **style)
|
||||||
|
|||||||
@@ -16,4 +16,10 @@ class RPCClient(xrc.ServerProxy):
|
|||||||
return head + help
|
return head + help
|
||||||
|
|
||||||
|
|
||||||
|
def __dir__(self):
|
||||||
|
d1 = super().__dir__()
|
||||||
|
d2 = self.utils.info().keys()
|
||||||
|
return [*d1, *d2]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from inspect import getdoc, signature
|
|||||||
|
|
||||||
class RPCServer(xrs.DocXMLRPCServer):
|
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)
|
addr = (host, port)
|
||||||
kwargs.setdefault("allow_none", True)
|
kwargs.setdefault("allow_none", True)
|
||||||
super().__init__(addr, *args, **kwargs)
|
super().__init__(addr, *args, **kwargs)
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ from PyQt5.QtWidgets import QShortcut
|
|||||||
|
|
||||||
def shortcut(parent, key_sequence, triggered, **kwargs):
|
def shortcut(parent, key_sequence, triggered, **kwargs):
|
||||||
key_sequence = QKeySequence(key_sequence)
|
key_sequence = QKeySequence(key_sequence)
|
||||||
shortcut = QShortcut(key_sequence, parent, **kwargs)
|
sc = QShortcut(key_sequence, parent, **kwargs)
|
||||||
shortcut.activated.connect(triggered)
|
sc.activated.connect(triggered)
|
||||||
return shortcut
|
return sc
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ DOT_PEN = GREY5
|
|||||||
DOT_BRUSH = HIGHLIGHT
|
DOT_BRUSH = HIGHLIGHT
|
||||||
DOT_SIZE = 8
|
DOT_SIZE = 8
|
||||||
|
|
||||||
LINE_COLORS = [
|
LINE_COLORS = (
|
||||||
C.BLUE,
|
C.BLUE,
|
||||||
C.BASE_GREEN,
|
C.BASE_GREEN,
|
||||||
C.RED,
|
C.RED,
|
||||||
@@ -38,7 +38,7 @@ LINE_COLORS = [
|
|||||||
C.COOL3,
|
C.COOL3,
|
||||||
C.BROWN1,
|
C.BROWN1,
|
||||||
C.BASE_ORANGE,
|
C.BASE_ORANGE,
|
||||||
]
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ long_description = file: README.md
|
|||||||
long_description_content_type = text/markdown
|
long_description_content_type = text/markdown
|
||||||
url = https://gitlab.psi.ch/augustin_s/grum
|
url = https://gitlab.psi.ch/augustin_s/grum
|
||||||
project_urls =
|
project_urls =
|
||||||
Bug Tracker = https://gitlab.psi.ch/augustin_s/grum/issues
|
Bug Tracker = https://gitlab.psi.ch/augustin_s/grum/issues
|
||||||
classifiers =
|
classifiers =
|
||||||
Programming Language :: Python :: 3
|
Programming Language :: Python :: 3
|
||||||
License :: OSI Approved :: MIT License
|
License :: OSI Approved :: MIT License
|
||||||
@@ -16,7 +16,7 @@ classifiers =
|
|||||||
package_dir =
|
package_dir =
|
||||||
= .
|
= .
|
||||||
packages = find:
|
packages = find:
|
||||||
python_requires = >=3.8
|
python_requires = >=3.6
|
||||||
|
|
||||||
[options.packages.find]
|
[options.packages.find]
|
||||||
where = .
|
where = .
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
setup(
|
setup(
|
||||||
install_requires=["pyqt5", "pyqtgraph", "h5py"],
|
install_requires=["pyqt5", "pyqtgraph", "h5py", "PyQtWebEngine"],
|
||||||
entry_points={"console_scripts": ["grum=grum:main"]},
|
entry_points={"console_scripts": ["grum=grum:main"]},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
12
tests/run.sh
Executable file
12
tests/run.sh
Executable file
@@ -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
|
||||||
|
|
||||||
|
|
||||||
@@ -15,13 +15,13 @@ class TestMDIArea:
|
|||||||
def setup_method(self):
|
def setup_method(self):
|
||||||
print("setup")
|
print("setup")
|
||||||
self.app = QApplication(sys.argv)
|
self.app = QApplication(sys.argv)
|
||||||
self.mw = MainWindow(add_examples=True)
|
self.mw = MainWindow(add_examples=True, offline=True)
|
||||||
self.mw.show()
|
self.mw.show()
|
||||||
|
|
||||||
|
|
||||||
def teardown_method(self):
|
def teardown_method(self):
|
||||||
print("teardown")
|
print("teardown")
|
||||||
self.mw.rst.wait_for_stop()
|
# self.mw.rst.wait_for_stop()
|
||||||
self.app.quit()
|
self.app.quit()
|
||||||
print("app quit called")
|
print("app quit called")
|
||||||
del self.mw
|
del self.mw
|
||||||
@@ -143,14 +143,14 @@ class TestMDIArea:
|
|||||||
mdi.closeAllSubWindows = mock.MagicMock()
|
mdi.closeAllSubWindows = mock.MagicMock()
|
||||||
sine_item = self.mw.lst.lst.get("sine")
|
sine_item = self.mw.lst.lst.get("sine")
|
||||||
sub = MDISubPlot("sine", sine_item.value)
|
sub = MDISubPlot("sine", sine_item.value)
|
||||||
sub.showMaximized = mock.MagicMock()
|
sub.maximize = mock.MagicMock()
|
||||||
sub.frame_off = mock.MagicMock()
|
sub.frame_off = mock.MagicMock()
|
||||||
|
|
||||||
mdi.add_single(sub)
|
mdi.add_single(sub)
|
||||||
|
|
||||||
mdi.closeAllSubWindows.assert_called_once()
|
mdi.closeAllSubWindows.assert_called_once()
|
||||||
mdi.addSubWindow.assert_called_once_with(sub)
|
mdi.addSubWindow.assert_called_once_with(sub)
|
||||||
sub.showMaximized.assert_called_once()
|
sub.maximize.assert_called_once()
|
||||||
sub.frame_off.assert_called_once()
|
sub.frame_off.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ def test_make_plot():
|
|||||||
)
|
)
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
theme.apply(app)
|
theme.apply(app)
|
||||||
mw = MainWindow(add_examples=True)
|
mw = MainWindow(add_examples=True, offline=True)
|
||||||
|
|
||||||
mdi_sub = MDISubPlot("mdi", pd)
|
mdi_sub = MDISubPlot("mdi", pd)
|
||||||
pw = pg.PlotWidget()
|
pw = pg.PlotWidget()
|
||||||
|
|||||||
Reference in New Issue
Block a user