73 Commits

Author SHA1 Message Date
2bc85654cd Merge pull request 'gitlab refs removed' (#2) from gitlab_hunt into master
Reviewed-on: #2
2025-08-27 08:58:57 +02:00
woznic_n
1278648fc6 gitlab refs removed 2025-08-26 11:59:12 +02:00
d7236401b6 also allow ModuleNotFoundError 2025-03-26 11:24:05 +01:00
bd3ccfa407 Update .gitlab-ci.yml 2024-08-12 15:55:19 +02:00
f97d9a3824 Merge branch 'fix-tests' into 'master'
added libxdamage1

See merge request augustin_s/grum!9
2023-08-16 18:57:38 +02:00
5606d0781f added libxdamage1 2023-08-16 18:50:50 +02:00
7a1a9479c8 updated docstring 2023-05-25 11:16:52 +02:00
7ea180bfb8 Merge branch 'images' into 'master'
Images

See merge request augustin_s/grum!8
2023-05-23 08:45:33 +00:00
fcca087b51 adjusted tests 2023-05-23 10:41:06 +02:00
d958d89da4 added and use constants for default sub window size 2023-05-23 10:19:52 +02:00
dcc554eab8 allow to set color map 2023-05-12 14:59:18 +02:00
cf24d54b62 allow to set levels 2023-05-12 14:35:04 +02:00
0742b9de0f fixed axis labels 2023-05-12 14:28:40 +02:00
ae70ac86a8 added labels to example image 2023-05-12 14:28:07 +02:00
438d5c1eea added image example 2023-05-12 11:17:18 +02:00
2d11fe4ff9 order 2023-04-17 23:36:47 +02:00
dee9cbb934 naming 2023-04-17 23:11:04 +02:00
3ef9032499 use single signal for creating new subwins 2023-04-17 23:09:50 +02:00
f80a7759fb use Description base class; change callbacks to concrete arguments 2023-04-17 22:46:56 +02:00
023ecd7618 added a comment 2023-04-15 13:54:08 +02:00
593241d66f added and use a mapping DescType to MDISubType 2023-04-15 13:51:18 +02:00
839b1a1e21 use DESC_TYPES and stored type to choose DescType on file load 2023-04-15 13:45:35 +02:00
4bb052d665 added description type to class mapping 2023-04-15 13:44:41 +02:00
3c951910b0 extract description type and add it to dict representation 2023-04-15 13:44:00 +02:00
4e95c1f8fd added a Description base class 2023-04-14 23:17:02 +02:00
00b12d800f import order 2023-04-14 18:33:22 +02:00
a7c86eb3b1 moved *Description classes into descs folder 2023-04-14 18:25:10 +02:00
da8d7b0a2b better add_new_desc_to_list; fixed multi plots with marked image; typo 2023-04-14 17:57:33 +02:00
7470f1388b first try on adding 2D image support 2023-04-14 14:17:46 +02:00
a80b568894 enable PYTHONFAULTHANDLER for segfault tracebacks 2023-03-18 11:48:03 +01:00
2edc68151d Merge branch 'gitlab-ci-update' into 'master'
ci: improved ci pipeline; added 3.6 to 3.11 tests

See merge request augustin_s/grum!6
2023-03-14 20:26:39 +00:00
aa03291e42 Merge branch 'tests' into 'master'
Tests

See merge request augustin_s/grum!7
2023-03-14 20:26:23 +00:00
5d97dd6224 added test report artifact 2023-03-14 11:52:03 +01:00
bed086f95f do not test the default 3.8 twice 2023-03-14 11:06:10 +01:00
aa79f1b0a1 removed the version check again 2023-03-14 10:58:31 +01:00
bbb6d6c00c lower the required version 2023-03-14 10:47:06 +01:00
c6f034f2d3 require PyQtWebEngine only for Python version were it exists 2023-03-14 10:42:34 +01:00
a624cf37a4 added some more versions 2023-03-14 09:40:32 +01:00
efd023ae3a a bit of formatting and naming 2023-03-14 09:35:44 +01:00
d65e97c9a7 neater 2023-02-10 12:31:42 +01:00
4fcc10f3da added possibility to extend data 2023-02-10 11:21:54 +01:00
c781601246 added set_data; refactored sync_item_and_plots 2023-02-10 10:10:12 +01:00
831b03a744 added data setter 2023-02-10 10:09:43 +01:00
b94294b579 fixed __dir__ 2023-02-10 10:09:22 +01:00
b14f6e68b4 allow tab completion for the exposed functions on the client side 2023-02-08 21:03:03 +01:00
76e3a04d98 added addEntrybox for max #entries 2023-02-04 18:54:36 +01:00
c620f17344 added addEntrybox 2023-02-04 18:53:53 +01:00
495a9a43bc added eviction, attached it to list changes signal 2023-02-04 18:52:49 +01:00
ab1d0d524e correctly set descending order (except for "sort by text") 2023-02-03 00:59:52 +01:00
cf4fbad5e7 update timestamps; added "Sort by timestamp" 2023-02-03 00:56:07 +01:00
5ea830bf2b attached timestamps object 2023-02-03 00:55:08 +01:00
87ae26247d attached timestamps object 2023-02-03 00:54:00 +01:00
23afec2aa4 fill xs and ys as argument 2023-02-03 00:53:19 +01:00
e4ada964e3 (trying to) disable sorting if item manually moved 2023-02-02 17:27:20 +01:00
9b50ade340 introduced "Sort by insertion order" (the default) / "Sorting disabled" keeps current order 2023-02-02 16:48:18 +01:00
26c338c54e added a comment 2023-02-02 14:59:51 +01:00
721f2a864a added docstrings for API methods 2023-02-02 14:59:42 +01:00
4c533489f6 added wildcard (*?) support 2023-02-02 13:28:52 +01:00
c7c17ef221 change sort key on the class (not the instances) 2023-02-02 11:59:05 +01:00
3ba74a1a1e added a tests run script 2023-02-01 16:17:58 +01:00
9e6896c30f fixed position of *args 2023-02-01 16:10:47 +01:00
829c9e2ded use different name inside function 2023-02-01 16:10:24 +01:00
9a66433a3e made LINE_COLORS tuple instead of list 2023-02-01 16:09:59 +01:00
9db8d5b87e cleanup 2023-02-01 15:31:31 +01:00
b380d840fc attach sorting options to menu 2023-02-01 15:26:38 +01:00
31cbe8ac33 added possibility to enabling/disabling sorting for the list 2023-02-01 15:26:17 +01:00
b02d66b95f added possibility for having a sort key 2023-02-01 15:25:37 +01:00
660e8208ca added offline mode (does not start the RPC server) 2023-01-29 16:32:41 +01:00
f491c71462 adjusted test 2023-01-29 14:19:34 +01:00
16c7b4f71c added maximize/restore to MDISubWindow, changed frame_on/frame_off from FramelessWindowHint to custom hint 2023-01-29 14:05:18 +01:00
7d169fdd53 ci: improved ci pipeline; added 3.9 and 3.10 tests 2023-01-28 17:29:56 +01:00
aeaabd028f connect tooltip display with xy coords to plot mouse-over event 2023-01-27 18:50:34 +01:00
4e3d8b9da6 introduced MDISubPlotBase 2023-01-27 18:34:17 +01:00
31 changed files with 605 additions and 113 deletions

View File

@@ -1,29 +1,64 @@
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 libxdamage1
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 junit: report.xml
coverage_report:
coverage_format: cobertura
path: coverage.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

View File

@@ -1,6 +1,6 @@
# grum # grum
<img src="https://gitlab.psi.ch/augustin_s/grum/-/wikis/uploads/1a259a1d74e7b79e0230e7bbad3b1284/screenshot2.png" width="50%" /> <img src="https://gitea.psi.ch/SwissFEL/grum/wiki/raw/uploads%2Fe4cd2be847d26bb7ac7100080edbccce%2Fscreenshot.png" width="50%" />
## Overview ## Overview
@@ -22,11 +22,11 @@ Via the RPC server, new plots can be created and new data appended to existing p
- `new_plot(name, cfg)` - `new_plot(name, cfg)`
Creates a new plot named `name` in the grum list. The configuration dict `cfg` is used as arguments for the constructor of [`PlotDescription`](https://gitlab.psi.ch/augustin_s/grum/-/blob/master/grum/plotdesc.py#L4). Creates a new plot named `name` in the grum list. The configuration dict `cfg` is used as arguments for the constructor of [`PlotDescription`](https://gitea.psi.ch/SwissFEL/grum/src/grum/descs/plotdesc.py#L4).
- `append_data(name, point)` - `append_data(name, point)`
Append data point to the plot named `name`. The new `point` is forwarded to [`PlotDescription.append()`](https://gitlab.psi.ch/augustin_s/grum/-/blob/master/grum/plotdesc.py#L18). Append data point to the plot named `name`. The new `point` is forwarded to [`PlotDescription.append()`](https://gitea.psi.ch/SwissFEL/grum/src/grum/descs/plotdesc.py#L24).
### Utility functions ### Utility functions

View File

@@ -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__

13
grum/descs/__init__.py Normal file
View File

@@ -0,0 +1,13 @@
from .desc import Description
from .imgdesc import ImageDescription
from .plotdesc import PlotDescription
DESC_TYPES = {
ImageDescription.get_type(): ImageDescription,
PlotDescription.get_type(): PlotDescription
}

23
grum/descs/desc.py Normal file
View File

@@ -0,0 +1,23 @@
class Description:
def to_dict(self):
res = {k: v for k, v in self.__dict__.items() if not k.startswith("_") and k != "name" and v is not None}
tn = self.get_type()
res.setdefault("type", tn)
return res
@classmethod
def get_type(cls):
tn = cls.__name__
suffix = "Description"
if not tn.endswith(suffix):
raise ValueError(f'"{tn}" does not end with "{suffix}"')
tn = tn[:-len(suffix)]
tn = tn.casefold()
tn = tn or None
return tn

52
grum/descs/imgdesc.py Normal file
View File

@@ -0,0 +1,52 @@
import numpy as np
import pyqtgraph as pg
from .desc import Description
class ImageDescription(Description):
def __init__(self, name, title=None, xlabel=None, ylabel=None, image=None, levels=None, cmap="viridis"):
self.name = name
self.title = title
self.xlabel = xlabel
self.ylabel = ylabel
self.image = image
self.levels = levels #TODO: might be better to use vmin and vmax
self.cmap = cmap
@property
def data(self):
return np.asarray(self.image)
@data.setter
def data(self, value):
self.image = value
def append(self, xy):
print("ignored image append")
def extend(self, data):
print("ignored image extend")
def make_plot(self, plotwidget, style):
res = plotwidget.setImage(self.data, levels=self.levels)
if self.title:
plotwidget.setTitle(self.title)
if self.xlabel:
plotwidget.getView().setLabel("bottom", self.xlabel)
if self.ylabel:
plotwidget.getView().setLabel("left", self.ylabel)
if self.cmap:
cm = pg.colormap.get(self.cmap)
plotwidget.setColorMap(cm)
return res

View File

@@ -1,5 +1,7 @@
from .desc import Description
class PlotDescription:
class PlotDescription(Description):
def __init__(self, name, title=None, xlabel=None, ylabel=None, xs=None, ys=None): def __init__(self, name, title=None, xlabel=None, ylabel=None, xs=None, ys=None):
self.name = name self.name = name
@@ -14,12 +16,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)
@@ -36,8 +47,4 @@ class PlotDescription:
return res return res
def to_dict(self):
return {k: v for k, v in self.__dict__.items() if not k.startswith("_") and k != "name" and v is not None}

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -1,6 +1,6 @@
import numpy as np import numpy as np
from .plotdesc import PlotDescription from .descs import PlotDescription, ImageDescription
X = np.arange(100) / 10 X = np.arange(100) / 10
@@ -20,15 +20,31 @@ 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
name = "image"
xdim = ydim = 100
size = xdim * ydim
shape = (xdim, ydim)
img = np.arange(size).reshape(shape) / size
img += np.random.random(shape) / 10
exampledata[name] = ImageDescription(
name,
image=img,
xlabel="x",
ylabel="y"
)

View File

@@ -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]

View File

@@ -2,24 +2,34 @@ from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtWidgets import QMainWindow, QSplitter from PyQt5.QtWidgets import QMainWindow, QSplitter
from . import assets from . import assets
from .descs import DESC_TYPES, Description, ImageDescription, PlotDescription
from .dictlist import DictList from .dictlist import DictList
from .exampledata import exampledata from .exampledata import exampledata
from .h5filedlg import open_h5_files_dialog, save_h5_file_dialog from .h5filedlg import open_h5_files_dialog, save_h5_file_dialog
from .io import write_dict, read_dict from .io import write_dict, read_dict
from .mdi import MDIArea, MDISubMultiPlot, MDISubPlot, MDIWindowMode from .mdi import MDIArea, MDISubMultiPlot, MDISubPlot, MDISubImage, MDIWindowMode
from .menus import BarMenu from .menus import BarMenu
from .plotdesc import PlotDescription
from .rpc import RPCServerThread from .rpc import RPCServerThread
from .shortcut import shortcut from .shortcut import shortcut
from .webview import WebView from .webview import WebView
DESC_TYPE_TO_MDI_SUB_TYPE = {
ImageDescription: MDISubImage,
PlotDescription: MDISubPlot
}
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
sig_make_new_plot = pyqtSignal(str, PlotDescription) sig_make_new_subwin = pyqtSignal(str, Description)
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 +49,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 +75,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,12 +87,16 @@ 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_image)
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_subwin.connect(self.on_make_new_subwin)
def keyPressEvent(self, event): def keyPressEvent(self, event):
@@ -77,29 +106,68 @@ class MainWindow(QMainWindow):
# Remote API calls # Remote API calls
def new_image(self, name, cfg):
"""
Create a new image <name> using the configuration dict <cfg>.
The configuration is forwarded to the constructor of ImageDescription.
Allowed keys are: title, xlabel, ylabel, image, levels, cmap.
"""
desc = self.add_new_desc_to_list(ImageDescription, name, cfg)
if self.menu_settings.checkboxes["Open new plots"].isChecked():
sub = self.mdi.findSubWindow(name)
if sub:
sub.pw.setImage(desc.data) #TODO lacks the list sync
else:
self.sig_make_new_subwin.emit(name, desc)
def new_plot(self, name, cfg): def new_plot(self, name, cfg):
desc = self.add_new_desc_to_list(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(PlotDescription, 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_subwin.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
def on_make_new_plot(self, *args, **kwargs): def on_make_new_subwin(self, name, desc):
self.make_subwin(MDISubPlot, *args, **kwargs) DescType = type(desc)
MDISubType = DESC_TYPE_TO_MDI_SUB_TYPE[DescType]
self.make_subwin(MDISubType, name, desc)
def on_dclick_list_item(self, item): def on_dclick_list_item(self, item):
self.plot_single_item(item) self.plot_single_item(item)
@@ -114,6 +182,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 +190,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:
@@ -128,8 +210,10 @@ class MainWindow(QMainWindow):
for fn in fns: for fn in fns:
data = read_dict(fn) data = read_dict(fn)
for k, v in data.items(): for name, cfg in data.items():
self.add_new_desc_to_list(k, v) tn = cfg.pop("type")
DescType = DESC_TYPES[tn]
self.add_new_desc_to_list(DescType, name, cfg)
def on_file_save(self): def on_file_save(self):
@@ -147,26 +231,42 @@ class MainWindow(QMainWindow):
# Plumbing # Plumbing
def add_new_desc_to_list(self, name, cfg): def add_new_desc_to_list(self, DescType, name, cfg):
desc = PlotDescription(name, **cfg) desc = DescType(name, **cfg)
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) DescType = type(desc)
MDISubType = DESC_TYPE_TO_MDI_SUB_TYPE[DescType]
self.activate_or_make_subwin(MDISubType, 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)
items = (i for i in items if isinstance(i.value, PlotDescription)) #TODO: for now, only overlay plots
descs = {i.key: i.value for i in items} descs = {i.key: i.value for i in items}
names = descs.keys() names = descs.keys()
name = " | ".join(names) name = " | ".join(names)
self.activate_or_make_subwin(MDISubMultiPlot, name, descs) self.activate_or_make_subwin(MDISubMultiPlot, name, descs)
#TODO: the following two could be methods to MDIArea? #TODO: the following two could be methods of MDIArea?
def activate_or_make_subwin(self, MDISubType, name, *args, **kwargs): def activate_or_make_subwin(self, MDISubType, name, *args, **kwargs):
sub = self.mdi.findSubWindow(name) sub = self.mdi.findSubWindow(name)

View File

@@ -1,5 +1,6 @@
from .mdiarea import MDIArea, MDIWindowMode from .mdiarea import MDIArea, MDIWindowMode
from .mdisubplot import MDISubPlot, MDISubMultiPlot from .mdisubplot import MDISubPlot, MDISubMultiPlot
from .mdisubimg import MDISubImage

View File

@@ -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()

38
grum/mdi/mdisubimg.py Normal file
View File

@@ -0,0 +1,38 @@
import pyqtgraph as pg
from .mdisubwin import MDISubWindow
from ..theme import pg_plot_style
class MDISubImage(MDISubWindow):
def __init__(self, name, desc, *args, **kwargs):
super().__init__(name, *args, **kwargs)
self.pw = pw = pg.ImageView(view=pg.PlotItem()) # for axis ticks and labels, view needs to be a PlotItem
self.setWidget(pw)
# connect to plot mouse-over event
pw.scene.sigMouseMoved.connect(self.on_hover)
style = pg_plot_style()
plot = desc.make_plot(self.pw, style)
self.plots = {name: plot}
self.image = desc.data
def on_hover(self, event):
coord = self.pw.imageItem.mapFromScene(event)
x = coord.x()
y = coord.y()
x = int(x)
y = int(y)
try:
z = self.image[x, y]
except IndexError:
return
z = round(z, 3)
self.setToolTip(f"x = {x}\ny = {y}\nz = {z}")

View File

@@ -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)}

View File

@@ -4,22 +4,42 @@ from PyQt5.QtWidgets import QMdiSubWindow
from .. import assets from .. import assets
SUB_WIN_WIDTH = 640
SUB_WIN_HEIGHT = 480
class MDISubWindow(QMdiSubWindow): class MDISubWindow(QMdiSubWindow):
def __init__(self, title, *args, **kwargs): def __init__(self, title, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.setWindowTitle(title) self.setWindowTitle(title)
self.setWindowIcon(assets.char()) self.setWindowIcon(assets.char())
self.resize(SUB_WIN_WIDTH, SUB_WIN_HEIGHT)
# 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()

View File

@@ -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()

View File

@@ -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]

View File

@@ -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)

View File

@@ -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

View File

@@ -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,
] )

View File

@@ -2,7 +2,7 @@ from PyQt5.QtCore import QUrl
try: try:
from PyQt5.QtWebKitWidgets import QWebView from PyQt5.QtWebKitWidgets import QWebView
except ImportError: except (ImportError, ModuleNotFoundError):
from PyQt5.QtWebEngineWidgets import QWebEngineView as QWebView from PyQt5.QtWebEngineWidgets import QWebEngineView as QWebView

View File

@@ -4,9 +4,9 @@ version = 0.0.1
description = GUI for Remote Unified Monitoring description = GUI for Remote Unified Monitoring
long_description = file: README.md 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://gitea.psi.ch/SwissFEL/grum
project_urls = project_urls =
Bug Tracker = https://gitlab.psi.ch/augustin_s/grum/issues Bug Tracker = https://gitea.psi.ch/SwissFEL/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 = .

View File

@@ -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
View 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

View File

@@ -14,7 +14,7 @@ from grum.mainwin import MainWindow
from grum.mdi import MDIArea, MDISubMultiPlot, MDISubPlot from grum.mdi import MDIArea, MDISubMultiPlot, MDISubPlot
from grum.menus import BarMenu from grum.menus import BarMenu
from grum.menus.rclickmenu import RClickMenu from grum.menus.rclickmenu import RClickMenu
from grum.plotdesc import PlotDescription from grum.descs import Description, PlotDescription
from grum.rpc import RPCServerThread from grum.rpc import RPCServerThread
@@ -47,7 +47,7 @@ class TestMainWin:
for key in mw.lst.lst.items: for key in mw.lst.lst.items:
assert isinstance(mw.lst.lst.get(key), DictListItem) assert isinstance(mw.lst.lst.get(key), DictListItem)
assert isinstance(mw.lst.lst.get(key).value, PlotDescription) assert isinstance(mw.lst.lst.get(key).value, Description)
assert isinstance(mw.lst.menu, RClickMenu) assert isinstance(mw.lst.menu, RClickMenu)
assert isinstance(mw.menu_settings, BarMenu) assert isinstance(mw.menu_settings, BarMenu)
@@ -65,7 +65,7 @@ class TestMainWin:
xlabel = "xlabel" xlabel = "xlabel"
ylabel = "ylabel" ylabel = "ylabel"
cfg = {"title": title, "xlabel": xlabel, "ylabel": ylabel} cfg = {"title": title, "xlabel": xlabel, "ylabel": ylabel}
spy_sig_make_new_plot = QSignalSpy(mw.sig_make_new_plot) spy_sig_make_new_subwin = QSignalSpy(mw.sig_make_new_subwin)
mw.new_plot(name, cfg=cfg) mw.new_plot(name, cfg=cfg)
@@ -76,28 +76,28 @@ class TestMainWin:
assert mw.lst.lst.get(name).value.ylabel == ylabel assert mw.lst.lst.get(name).value.ylabel == ylabel
assert mw.menu_settings.checkboxes["Open new plots"].isChecked() assert mw.menu_settings.checkboxes["Open new plots"].isChecked()
assert len(spy_sig_make_new_plot) == 1 # assert called once assert len(spy_sig_make_new_subwin) == 1 # assert called once
assert spy_sig_make_new_plot[0][0] == name # assert called with name assert spy_sig_make_new_subwin[0][0] == name # assert called with name
assert isinstance(spy_sig_make_new_plot[0][1], PlotDescription) assert isinstance(spy_sig_make_new_subwin[0][1], PlotDescription)
mw.menu_settings.checkboxes["Open new plots"].setChecked(False) mw.menu_settings.checkboxes["Open new plots"].setChecked(False)
assert mw.menu_settings.checkboxes["Open new plots"].isChecked() == False assert mw.menu_settings.checkboxes["Open new plots"].isChecked() == False
spy_sig_make_new_plot = QSignalSpy(mw.sig_make_new_plot) spy_sig_make_new_subwin = QSignalSpy(mw.sig_make_new_subwin)
mw.new_plot("new_name", cfg) mw.new_plot("new_name", cfg)
assert len(spy_sig_make_new_plot) == 0 # assert not called assert len(spy_sig_make_new_subwin) == 0 # assert not called
mw.menu_settings.checkboxes["Open new plots"].setChecked(True) mw.menu_settings.checkboxes["Open new plots"].setChecked(True)
assert mw.menu_settings.checkboxes["Open new plots"].isChecked() == True assert mw.menu_settings.checkboxes["Open new plots"].isChecked() == True
spy_sig_make_new_plot = QSignalSpy(mw.sig_make_new_plot) spy_sig_make_new_subwin = QSignalSpy(mw.sig_make_new_subwin)
new_name_item = mw.lst.lst.get("new_name") new_name_item = mw.lst.lst.get("new_name")
sub = MDISubPlot("new_name", new_name_item.value) sub = MDISubPlot("new_name", new_name_item.value)
mw.mdi.add(sub) mw.mdi.add(sub)
mw.new_plot("new_name", cfg) mw.new_plot("new_name", cfg)
assert len(spy_sig_make_new_plot) == 0 # assert not called assert len(spy_sig_make_new_subwin) == 0 # assert not called
def test_append_data(self): def test_append_data(self):
@@ -122,17 +122,19 @@ class TestMainWin:
assert sine_item.set_alarm.call_args[0][0] == False assert sine_item.set_alarm.call_args[0][0] == False
def test_on_make_new_plot(self): def test_on_make_new_subwin(self):
mw = self.mw mw = self.mw
mw.make_subwin = mock.MagicMock() mw.make_subwin = mock.MagicMock()
args = (1, 2, "name") name = "test"
kwargs = {"title": "plot_title"} cfg = {"title": "title"}
mw.on_make_new_plot(args, kwargs) desc = PlotDescription(name, *cfg)
mw.make_subwin.assert_called_once_with(MDISubPlot, args, kwargs) mw.on_make_new_subwin(name, desc)
mw.make_subwin.assert_called_once_with(MDISubPlot, name, desc)
def test_on_dclick_list_item(self): def test_on_dclick_list_item(self):

View File

@@ -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()

View File

@@ -6,7 +6,7 @@ import pyqtgraph as pg
from grum import theme from grum import theme
from grum.mainwin import MainWindow from grum.mainwin import MainWindow
from grum.mdi.mdisubplot import MDISubPlot from grum.mdi.mdisubplot import MDISubPlot
from grum.plotdesc import PlotDescription from grum.descs import PlotDescription
from grum.theme import pg_plot_style from grum.theme import pg_plot_style
@@ -78,6 +78,7 @@ def test_to_dict():
"xs": [1, 2], "xs": [1, 2],
"ylabel": "plot_ylabel", "ylabel": "plot_ylabel",
"ys": [3, 4], "ys": [3, 4],
"type": "plot"
} }