From 2bc03a004c7233cfb3053934f34d00b978dc771a Mon Sep 17 00:00:00 2001
From: JoschD <26184899+JoschD@users.noreply.github.com>
Date: Thu, 14 Dec 2023 16:47:44 +0100
Subject: [PATCH] better tab handling, copy function updated
---
omc3_gui/plotting/classes.py | 2 +-
.../segment_by_segment/main_controller.py | 54 +++++++++---------
omc3_gui/segment_by_segment/main_view.py | 28 +++++-----
omc3_gui/segment_by_segment/segment_model.py | 55 ++++++++++++++-----
omc3_gui/utils/iteration_classes.py | 53 ++++++++++++++++++
.../{base_classes.py => ui_base_classes.py} | 0
6 files changed, 138 insertions(+), 54 deletions(-)
create mode 100644 omc3_gui/utils/iteration_classes.py
rename omc3_gui/utils/{base_classes.py => ui_base_classes.py} (100%)
diff --git a/omc3_gui/plotting/classes.py b/omc3_gui/plotting/classes.py
index f4f2a01..70c79ef 100644
--- a/omc3_gui/plotting/classes.py
+++ b/omc3_gui/plotting/classes.py
@@ -63,4 +63,4 @@ class ZoomingViewBox(ExViewBox):
# self.getdataInRect()
# self.changePointsColors()
# else:
- # self.updateScaleBox(ev.buttonDownPos(), ev.pos())
\ No newline at end of file
+ # self.updateScaleBox(ev.buttonDownPos(), ev.pos())
diff --git a/omc3_gui/segment_by_segment/main_controller.py b/omc3_gui/segment_by_segment/main_controller.py
index e060a4f..6c55d45 100644
--- a/omc3_gui/segment_by_segment/main_controller.py
+++ b/omc3_gui/segment_by_segment/main_controller.py
@@ -1,7 +1,7 @@
import logging
from functools import partial
from pathlib import Path
-from typing import Any, Dict, List, Sequence
+from typing import Any, Dict, List, Sequence, Tuple
from omc3.sbs_propagation import segment_by_segment
from qtpy import QtWidgets
@@ -13,10 +13,12 @@
from omc3_gui.segment_by_segment.measurement_model import OpticsMeasurement
from omc3_gui.segment_by_segment.measurement_view import OpticsMeasurementDialog
from omc3_gui.segment_by_segment.segment_model import SegmentDataModel, SegmentItemModel, compare_segments
-from omc3_gui.utils.base_classes import Controller
+from omc3_gui.utils.ui_base_classes import Controller
from omc3_gui.utils.file_dialogs import OpenDirectoriesDialog
from omc3_gui.utils.threads import BackgroundThread
from omc3_gui.segment_by_segment.segment_view import SegmentDialog
+from omc3.definitions.optics import ColumnsAndLabels
+from omc3_gui.plotting.classes import DualPlot
LOGGER = logging.getLogger(__name__)
@@ -231,7 +233,11 @@ def segment_selection_changed(self, segments: Sequence[SegmentItemModel] = None)
if len(segments) > 1:
LOGGER.debug("More than one segment selected. Clearing Plots.")
return
+
# Plot segements
+ def_and_widget: Tuple[ColumnsAndLabels, DualPlot] = self._view.get_current_tab()
+ definition, widget = def_and_widget
+ # plot_segments()
@Slot()
def add_default_segments(self):
@@ -248,7 +254,7 @@ def add_default_segments(self):
continue
for segment_tuple in DEFAULT_SEGMENTS:
- segment = SegmentDataModel(*segment_tuple)
+ segment = SegmentDataModel(measurement, *segment_tuple)
segment.start = f"{segment.start}.B{beam}"
segment.end = f"{segment.end}.B{beam}"
measurement.try_add_segment(segment)
@@ -291,15 +297,26 @@ def copy_segment(self, segments: Sequence[SegmentItemModel] = None):
LOGGER.error("Please select at least one measurement.")
return
- for measurement in selected_measurements:
- for segment_item in segments:
+ for segment_item in segments:
+ new_segment_name = f"{segment_item.name} - Copy"
+ for measurement in selected_measurements:
+ # Check if copied segment name already exists in one of the measurements
try:
- meas_segment = measurement.get_segment_by_name(segment_item.name)
- except NameError as e:
- LOGGER.warning(f"Could not copy segment. {e!s}")
- continue
- new_segment = get_segment_copy_with_unique_name(meas_segment, measurement)
- measurement.try_add_segment(new_segment)
+ measurement.get_segment_by_name(new_segment_name)
+ except NameError:
+ pass
+ else:
+ LOGGER.error(
+ f"Could not create copy \"{new_segment_name}\" as it already exists in {measurement.display()}."
+ )
+ break
+ else:
+ # None of the measurements have the copied segment name, so add to the measurements
+ for measurement in selected_measurements:
+ for segment in segment_item.segments:
+ new_segment = segment.copy()
+ new_segment.name = new_segment_name
+ measurement.try_add_segment(new_segment)
self.measurement_selection_changed(selected_measurements)
@@ -324,7 +341,7 @@ def remove_segment(self, segments: Sequence[SegmentItemModel] = None):
self.measurement_selection_changed(selected_measurements)
@Slot()
- def run_segments(self, segments: Sequence[SegmentDataModel] = None):
+ def run_segments(self, segments: Sequence[SegmentItemModel] = None):
if segments is None:
segments = self._view.get_selected_segments()
if not segments:
@@ -354,16 +371,3 @@ def run_segments(self, segments: Sequence[SegmentDataModel] = None):
LOGGER.info(f"Starting {measurement_task.message}")
measurement_task.start()
-
-
-def get_segment_copy_with_unique_name(segment: SegmentDataModel, measurement: OpticsMeasurement) -> SegmentDataModel:
- new_segment = segment.copy()
- idx = 0
- segment_names = [s.name for s in measurement.segments]
- new_name = new_segment.name
- while new_name in segment_names:
- idx += 1
- new_name = f"{segment.name}_{idx}"
- new_segment.name = new_name
- return new_segment
-
diff --git a/omc3_gui/segment_by_segment/main_view.py b/omc3_gui/segment_by_segment/main_view.py
index 3614fe1..4f57af9 100644
--- a/omc3_gui/segment_by_segment/main_view.py
+++ b/omc3_gui/segment_by_segment/main_view.py
@@ -1,26 +1,28 @@
# from omc3_gui.segment_by_segment.segment_by_segment_ui import Ui_main_window
import logging
-from typing import Dict, List, Sequence, Tuple
+from typing import Dict, Iterator, List, Sequence, Tuple
from PyQt5 import QtGui
from qtpy import QtGui, QtWidgets
-from qtpy.QtCore import QItemSelectionModel, QModelIndex, Qt, Signal, Slot
+from qtpy.QtCore import QItemSelectionModel, QModelIndex, Qt, Signal, Slot, QEvent
from omc3_gui.plotting.classes import DualPlot
from omc3_gui.segment_by_segment.main_model import MeasurementListModel, SegmentTableModel
from omc3_gui.segment_by_segment.measurement_model import OpticsMeasurement
from omc3_gui.segment_by_segment.segment_model import SegmentItemModel
from omc3_gui.utils import colors
-from omc3_gui.utils.base_classes import View
+from omc3_gui.utils.ui_base_classes import View
from omc3_gui.utils.counter import HorizontalGridLayoutFiller
from omc3_gui.utils.styles import MONOSPACED_TOOLTIP
from omc3_gui.utils.widgets import (DefaultButton, EditButton, OpenButton, RemoveButton, RunButton)
+from omc3.definitions.optics import ColumnsAndLabels, PHASE_COLUMN
+from omc3_gui.utils.iteration_classes import IterClass
LOGGER = logging.getLogger(__name__)
-class Tab:
- PHASE: str = "Phase"
+class Tabs(IterClass):
+ PHASE: ColumnsAndLabels = PHASE_COLUMN
class SbSWindow(View):
@@ -41,7 +43,6 @@ def __init__(self, parent=None):
# Widgets ---
self._cental: QtWidgets.QSplitter = None
self._tabs_widget: QtWidgets.QTabWidget = None
- self._tabs: Dict[str, DualPlot] = None
self._list_view_measurements: QtWidgets.QListView = None
self._table_segments: QtWidgets.QTableView = None
@@ -191,7 +192,8 @@ def build_segment_buttons():
def build_tabs_widget(): # --- Right Hand Side
self._tabs_widget = QtWidgets.QTabWidget()
- self._tabs = self._create_tabs(self._tabs_widget)
+ for tab in Tabs.values():
+ self._tabs_widget.addTab(DualPlot(), tab.text_label.capitalize())
return self._tabs_widget
self._central.addWidget(build_tabs_widget())
@@ -202,14 +204,10 @@ def build_tabs_widget(): # --- Right Hand Side
self.setCentralWidget(self._central)
- def _create_tabs(self, tab_widget: QtWidgets.QTabWidget) -> Dict[str, "DualPlot"]:
- tabs = {}
-
- new_plot = DualPlot()
- tab_widget.addTab(new_plot, Tab.PHASE)
- tabs[Tab.PHASE] = new_plot
-
- return tabs
+ def get_current_tab(self) -> Tuple[ColumnsAndLabels, DualPlot]:
+ widget = self._tabs_widget.currentWidget()
+ index = self._tabs_widget.currentIndex()
+ return list(Tabs.values())[index], widget
# Getters and Setters
def set_measurements(self, measurement_model: MeasurementListModel):
diff --git a/omc3_gui/segment_by_segment/segment_model.py b/omc3_gui/segment_by_segment/segment_model.py
index 62c1b1a..8e0ad12 100644
--- a/omc3_gui/segment_by_segment/segment_model.py
+++ b/omc3_gui/segment_by_segment/segment_model.py
@@ -1,9 +1,10 @@
from __future__ import annotations # Together with TYPE_CHECKING: allow circular imports for type-hints
-from dataclasses import dataclass
+from dataclasses import dataclass, field
from typing import List, Optional, Union
from omc3_gui.utils.dataclass_ui import metafield
from omc3_gui.utils import colors
+from omc3.segment_by_segment.segments import SegmentDiffs
from typing import TYPE_CHECKING
@@ -11,9 +12,11 @@
from omc3_gui.segment_by_segment.measurement_model import OpticsMeasurement
-
-OK = f"✓"
-NO = f"✗"
+# HTML in tooltips does not work for me, and I cannot figure out why (jdilly)
+# OK = f"✓"
+# NO = f"✗"
+OK = "✓"
+NO = "✗"
def not_empty(value):
return value != ""
@@ -25,23 +28,33 @@ class SegmentDataModel:
name: str = metafield("Name", "Name of the Segment", validate=not_empty)
start: Optional[str] = metafield("Start", "Start of the Segment", default=None, validate=not_empty)
end: Optional[str] = metafield("End", "End of the Segment", default=None, validate=not_empty)
- _data: Optional[dict] = None
+ _data: Optional[SegmentDiffs] = None
def __str__(self):
return self.name
def is_element(self):
- return self.start is None or self.end is None
+ return is_element(self)
def to_input_string(self):
""" String representation of the segment as used in inputs."""
- if self.is_element():
- return self.name
- return f"{self.name},{self.start},{self.end}"
+ return to_input_string(self)
+
+ @property
+ def data(self) -> SegmentDiffs:
+ if self._data is None or self._data.directory != self.measurement.output_dir or self._data.segment_name != self.name:
+ self._data = SegmentDiffs(self.measurement.output_dir, self.name)
+ return self._data
def has_run(self) -> bool:
- return bool(self._data)
+ try:
+ return self.data.get_path("phase_x").is_file()
+ except AttributeError:
+ return False
+ def clear_data(self):
+ self._data = None
+
def copy(self):
return SegmentDataModel(measurement=self.measurement, name=self.name, start=self.start, end=self.end)
@@ -112,8 +125,7 @@ def append_segment(self, segment: SegmentDataModel):
if not compare_segments(self, segment):
raise ValueError(f"Given segment has a different definition than this {self.__class__.name}.")
self.segments.append(segment)
-
- @property
+
def id(self) -> str:
""" Unique identifier for the measurement, used in the ItemModel. """
return self.name + str(self.start) + str(self.end)
@@ -121,9 +133,26 @@ def id(self) -> str:
def tooltip(self) -> str:
""" Returns a string with information about the segment,
as to be used in a tool-tip. """
- parts = [f"{OK if segment.has_run() else NO} {segment.measurement.display()}" for segment in self.segments]
+ parts = [f" {OK if segment.has_run() else NO} {segment.measurement.display()}" for segment in self.segments]
return "Run | Contained in:\n" + "\n".join(parts)
+ def is_element(self):
+ return is_element(self)
+
+ def to_input_string(self):
+ """ String representation of the segment as used in inputs."""
+ return to_input_string(self)
+
def compare_segments(a: Union[SegmentDataModel, SegmentItemModel], b: Union[SegmentDataModel, SegmentItemModel]):
return a.name == b.name and a.start == b.start and a.end == b.end
+
+# Common functions -------------------------------------------------------------
+
+def is_element(segment: [SegmentItemModel, SegmentDataModel]):
+ return segment.start is None or segment.end is None
+
+def to_input_string(segment: [SegmentItemModel, SegmentDataModel]):
+ if is_element(segment):
+ return segment.name
+ return f"{segment.name},{segment.start},{segment.end}"
\ No newline at end of file
diff --git a/omc3_gui/utils/iteration_classes.py b/omc3_gui/utils/iteration_classes.py
new file mode 100644
index 0000000..806dda1
--- /dev/null
+++ b/omc3_gui/utils/iteration_classes.py
@@ -0,0 +1,53 @@
+from typing import Iterator, Tuple, Any
+
+EXCLUDED_NAME = "EXCLUDED_ATTRIBUTES"
+
+# Metaclasses ------------------------------------------------------------------
+
+class IterableAttributeNames(type):
+ """ Makes the class itself iterable over its attribute names. """
+
+
+ def __iter__(self) -> Iterator[str]:
+ for attr in dir(self):
+ if not attr.startswith("__") and attr != EXCLUDED_NAME and attr not in getattr(self, EXCLUDED_NAME, []):
+ yield attr
+
+
+class IterableAttributeValues(type):
+ """ Makes the class itself iterable over its attribute values. """
+ def __iter__(self) -> Iterator[Any]:
+ for attr, value in self.__dict__.items():
+ if not attr.startswith("__"):
+ yield value
+
+
+class IterableAttributeItems(type):
+ """ Makes the class itself iterable over its attribute name and values. """
+ def __iter__(self) -> Iterator[Tuple[str, Any]]:
+ for attr, value in self.__dict__.items():
+ if not attr.startswith("__"):
+ yield attr, value
+
+
+# Iterable Class ---------------------------------------------------------------
+
+
+class IterClass(metaclass=IterableAttributeNames):
+
+ EXCLUDED_ATTRIBUTES = ["keys", "values", "items"]
+
+ @classmethod
+ def keys(cls) -> Iterator[str]:
+ for attr in cls:
+ yield attr
+
+ @classmethod
+ def values(cls) -> Iterator[Any]:
+ for attr in cls:
+ yield getattr(cls, attr)
+
+ @classmethod
+ def items(cls) -> Iterator[Tuple[str, Any]]:
+ for attr in cls:
+ yield attr, getattr(cls, attr)
\ No newline at end of file
diff --git a/omc3_gui/utils/base_classes.py b/omc3_gui/utils/ui_base_classes.py
similarity index 100%
rename from omc3_gui/utils/base_classes.py
rename to omc3_gui/utils/ui_base_classes.py