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