diff --git a/.gitignore b/.gitignore index e6caea5..18dd034 100644 --- a/.gitignore +++ b/.gitignore @@ -248,3 +248,6 @@ pyproject.toml*~ .cache /doc/_build/* *pycache* + +# testers +tst_* diff --git a/omc3_gui/plotting/classes.py b/omc3_gui/plotting/classes.py new file mode 100644 index 0000000..f4f2a01 --- /dev/null +++ b/omc3_gui/plotting/classes.py @@ -0,0 +1,66 @@ + + +import pyqtgraph as pg +from accwidgets.graph import StaticPlotWidget +from accwidgets.graph.widgets.plotitem import ExViewBox + +class DualPlot(pg.LayoutWidget): + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + self.top = PlotWidget() + self.bottom = PlotWidget() + + self.addWidget(self.top, row=0, col=0) + self.addWidget(self.bottom, row=1, col=0) + + # self.top.setMouseMode(pg.ViewBox.RectMode) + # self.bottom.setMouseMode(pg.ViewBox.PanMode) + + @property + def plots(self): + return (self.top, self.bottom) + + + def connect_x(self) -> None: + pass + + def connect_y(self) -> None: + pass + + +class PlotWidget(StaticPlotWidget): + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs, viewBox=ZoomingViewBox()) + + # fixes for our plots + self.setBackground("w") + self.plotItem.getViewBox().setMouseMode(ZoomingViewBox.RectMode) + + +class ZoomingViewBox(ExViewBox): + pass + + # def mouseDragEvent(self, ev): + + # if ev.button() == QtCore.Qt.RightButton: + # ev.ignore() + # else: + # pg.ViewBox.mouseDragEvent(self, ev) + + # ev.accept() + # pos = ev.pos() + # if ev.button() == QtCore.Qt.RightButton: + # if ev.isFinish(): + # self.rbScaleBox.hide() + # self.ax = QtCore.QRectF( + # pg.Point(ev.buttonDownPos(ev.button())), pg.Point(pos) + # ) + # self.ax = self.childGroup.mapRectFromParent(self.ax) + # self.Coords = self.ax.getCoords() + # self.getdataInRect() + # self.changePointsColors() + # else: + # self.updateScaleBox(ev.buttonDownPos(), ev.pos()) \ No newline at end of file diff --git a/omc3_gui/sbs_matcher.py b/omc3_gui/sbs_matcher.py new file mode 100644 index 0000000..deb07e8 --- /dev/null +++ b/omc3_gui/sbs_matcher.py @@ -0,0 +1,42 @@ +import logging +import sys +from pathlib import Path + +from qtpy import QtWidgets + +from omc3_gui.segment_by_segment_matcher.constants import LHC_YEARS +from omc3_gui.segment_by_segment_matcher.main import SbSGuiMainController +from omc3_gui.utils import log_handler + +LOGGER = logging.getLogger(__name__) + + +def main(lhc_year=None, match_path=None, input_dir=None): + app = QtWidgets.QApplication(sys.argv) + app.setStyle("fusion") + main_controller = SbSGuiMainController() + if match_path is None or lhc_year is None: + lhc_year, match_path = main_controller.ask_for_initial_config( + lhc_year, + match_path, + ) + if match_path is None or lhc_year is None: + return + match_path = Path(match_path) + log_handler.add_file_handler(match_path) + if lhc_year not in LHC_YEARS: + raise ValueError(f"Invalid lhc mode, must be one of {LHC_YEARS!s}") + LOGGER.info("-------------------- ") + LOGGER.info("Configuration:") + LOGGER.info(f"- LHC year: {lhc_year!s}") + LOGGER.info(f"- Match output path: {match_path!s}") + LOGGER.info("-------------------- ") + main_controller.set_match_path(match_path) + main_controller.set_lhc_mode(lhc_year) + main_controller.set_input_dir(input_dir) + main_controller.show_view() + sys.exit(app.exec_()) + + +if __name__ == "__main__": + main(lhc_year="2018", match_path=Path("/mnt/volume/jdilly/temp/")) diff --git a/omc3_gui/segment_by_segment.py b/omc3_gui/segment_by_segment.py index deb07e8..87be639 100644 --- a/omc3_gui/segment_by_segment.py +++ b/omc3_gui/segment_by_segment.py @@ -1,42 +1,7 @@ +from omc3_gui.segment_by_segment.controller import SbSController +from omc3_gui.utils.log_handler import init_logging import logging -import sys -from pathlib import Path - -from qtpy import QtWidgets - -from omc3_gui.segment_by_segment_matcher.constants import LHC_YEARS -from omc3_gui.segment_by_segment_matcher.main import SbSGuiMainController -from omc3_gui.utils import log_handler - -LOGGER = logging.getLogger(__name__) - - -def main(lhc_year=None, match_path=None, input_dir=None): - app = QtWidgets.QApplication(sys.argv) - app.setStyle("fusion") - main_controller = SbSGuiMainController() - if match_path is None or lhc_year is None: - lhc_year, match_path = main_controller.ask_for_initial_config( - lhc_year, - match_path, - ) - if match_path is None or lhc_year is None: - return - match_path = Path(match_path) - log_handler.add_file_handler(match_path) - if lhc_year not in LHC_YEARS: - raise ValueError(f"Invalid lhc mode, must be one of {LHC_YEARS!s}") - LOGGER.info("-------------------- ") - LOGGER.info("Configuration:") - LOGGER.info(f"- LHC year: {lhc_year!s}") - LOGGER.info(f"- Match output path: {match_path!s}") - LOGGER.info("-------------------- ") - main_controller.set_match_path(match_path) - main_controller.set_lhc_mode(lhc_year) - main_controller.set_input_dir(input_dir) - main_controller.show_view() - sys.exit(app.exec_()) - if __name__ == "__main__": - main(lhc_year="2018", match_path=Path("/mnt/volume/jdilly/temp/")) + init_logging(level=logging.DEBUG) + SbSController.run_application() \ No newline at end of file diff --git a/omc3_gui/segment_by_segment/__init__.py b/omc3_gui/segment_by_segment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/omc3_gui/segment_by_segment/controller.py b/omc3_gui/segment_by_segment/controller.py new file mode 100644 index 0000000..4fc5336 --- /dev/null +++ b/omc3_gui/segment_by_segment/controller.py @@ -0,0 +1,50 @@ +from pathlib import Path +from omc3_gui.utils.base_classes import Controller +from omc3_gui.utils.dialogs import OpenDirectoriesDialog, OpenDirectoryDialog +from omc3_gui.segment_by_segment.view import SbSWindow +from omc3_gui.segment_by_segment.model import Measurement, Settings +from qtpy.QtCore import Qt, Signal, Slot +from qtpy.QtWidgets import QFileDialog +import logging + +LOG = logging.getLogger(__name__) + +class SbSController(Controller): + + settings: Settings + _view: SbSWindow # for the IDE + + def __init__(self): + super().__init__(SbSWindow()) + self.connect_signals() + self.settings = Settings() + self._last_selected_optics_path = None + + + def add_measurement(self, measurement: Measurement): + self._view.get_measurement_list().add_item(measurement) + + + def connect_signals(self): + self._view.sig_load_button_clicked.connect(self.open_measurements) + + + @Slot() + def open_measurements(self): + LOG.debug("OpenButton Clicked. Asking for folder paths.") + filenames = OpenDirectoriesDialog( + parent=self._view, + caption="Select Optics Folders", + directory=str(self._last_selected_optics_path) if self._last_selected_optics_path else None, + ).run_selection_dialog() + + loaded_measurements = self._view.get_measurement_list() + + LOG.debug(f"User selected {len(filenames)} files.") + for filename in filenames: + self._last_selected_optics_path = filename.parent + LOG.debug(f"User selected: {filename}") + loaded_measurements.add_item(Measurement(filename)) + + + diff --git a/omc3_gui/segment_by_segment/model.py b/omc3_gui/segment_by_segment/model.py new file mode 100644 index 0000000..5b9b6c0 --- /dev/null +++ b/omc3_gui/segment_by_segment/model.py @@ -0,0 +1,143 @@ +from dataclasses import dataclass +from pathlib import Path +import types +from typing import Any, Dict, List, Sequence, Union +from accwidgets.graph import StaticPlotWidget +import pyqtgraph as pg +from omc3.segment_by_segment.segments import Segment + +from typing import List +from qtpy import QtCore +from qtpy.QtCore import Qt + + +@dataclass +class Measurement: + measurement_dir: Path + output_dir: Path = None + elements: Dict[str, Segment] = None + segments: Dict[str, Segment] = None + model_dir: Path = None + accel: str = None + year: str = None + ring: int = None + + def __str__(self): + return str(self.measurement_dir) + + + +@dataclass +class Settings: + pass + + + +class ItemDictModel: + + def __init__(self): + self.items = {} + + def try_emit(self, emit: bool = True): + if not emit: + return + + if hasattr(self, "layoutChanged"): + self.layoutChanged.emit() + + def update_item(self, item): + self.items[str(item)] = item + self.try_emit() + + def add_item(self, item, emit: bool = True): + name = str(item) + if name in self.items.keys(): + raise ValueError(f"Item {name} already exists") + self.items[name] = item + self.try_emit(emit) + + def add_items(self, items: Sequence): + for item in items: + self.add_item(item, emit=False) + self.try_emit() + + def remove_item(self, item, emit: bool = True): + self.items.pop(str(item)) + self.try_emit(emit) + + def remove_items(self, items: Sequence): + for item in items: + self.remove_item(item, emit=False) + self.try_emit() + + def remove_all_items(self): + self.items = {} + self.try_emit() + + def remove_item_at(self, index: int): + self.remove_item(self.get_item_at(index)) + + def remove_items_at(self, indices: Sequence): + self.remove_items([self.get_item_at(index) for index in indices]) + + def get_item_at(self, index: int) -> Any: + return list(self.items.values())[index] + + + +class MeasurementListModel(QtCore.QAbstractListModel, ItemDictModel): + + items: Dict[str, Measurement] # for the IDE + + def __init__(self, *args, **kwargs): + super(QtCore.QAbstractListModel, self).__init__(*args, **kwargs) + super(ItemDictModel, self).__init__() + + def data(self, index: QtCore.QModelIndex, role: int = Qt.DisplayRole): + meas: Measurement = self.get_item_at(index.row()) + if role == Qt.DisplayRole: # https://doc.qt.io/qt-5/qt.html#ItemDataRole-enum + return str(meas) + + if role == Qt.EditRole: + return meas + + def rowCount(self, index): + return len(self.items) + + +class SegmentTableModel(QtCore.QAbstractTableModel, ItemDictModel): + + _COLUMNS = {0: "Segment", 1: "Start", 2: "End"} + _COLUMNS_MAP = {0: "name", 1: "start", 2: "end"} + + items: Dict[str, Segment] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + super(QtCore.QAbstractTableModel, self).__init__(*args, **kwargs) + super(ItemDictModel, self).__init__() + + def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole): + if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: + return self._COLUMNS[section] + return super().headerData(section, orientation, role) + + def rowCount(self, parent=QtCore.QModelIndex()): + return len(self.items) + + def columnCount(self, parent=QtCore.QModelIndex()): + return len(self._COLUMNS) + + def data(self, index, role=QtCore.Qt.DisplayRole): + i = index.row() + j = index.column() + segment: Segment = self.get_item_at(i) + + if role == QtCore.Qt.DisplayRole: + return str(getattr(segment, self._COLUMNS_MAP[j])) + + if role == Qt.EditRole: + return segment + + def flags(self, index): + return QtCore.Qt.ItemIsEnabled diff --git a/omc3_gui/segment_by_segment/view.py b/omc3_gui/segment_by_segment/view.py new file mode 100644 index 0000000..e1004d6 --- /dev/null +++ b/omc3_gui/segment_by_segment/view.py @@ -0,0 +1,209 @@ + +# from omc3_gui.segment_by_segment.segment_by_segment_ui import Ui_main_window +import logging +from pathlib import Path +from typing import Dict, List, Tuple + +import pyqtgraph as pg +from accwidgets.graph import StaticPlotWidget +from accwidgets.graph.widgets.plotitem import ExViewBox +from qtpy import QtWidgets, uic +from qtpy.QtCore import Qt, Signal, Slot, QModelIndex + +from omc3_gui.plotting.classes import DualPlot +from omc3_gui.segment_by_segment.model import Measurement, MeasurementListModel, SegmentTableModel +from omc3_gui.utils.base_classes import View +from omc3_gui.utils.counter import HorizontalGridLayoutFiller +from omc3_gui.utils.widgets import EditButton, OpenButton, RemoveButton, RunButton + +LOGGER = logging.getLogger(__name__) + +class Tab: + PHASE: str = "Phase" + + +class SbSWindow(View): + WINDOW_TITLE = "OMC Segment-by-Segment" + + sig_load_button_clicked = Signal() + sig_remove_button_clicked = Signal() + sig_matcher_button_clicked = Signal() + sig_edit_measurement_button_clicked = Signal(Measurement) + sig_list_optics_double_clicked = Signal(Measurement) + + _tabs: Dict[str, DualPlot] + _tabs_widget: QtWidgets.QTabWidget + _list_view_measurements: QtWidgets.QListView + _table_segments: QtWidgets.QTableView + + # Buttons --- + button_load: QtWidgets.QPushButton + button_remove: QtWidgets.QPushButton + button_edit: QtWidgets.QPushButton + button_matcher: QtWidgets.QPushButton + + button_run_segment: QtWidgets.QPushButton + button_remove_segment: QtWidgets.QPushButton + button_copy_segment: QtWidgets.QPushButton + button_new_segment: QtWidgets.QPushButton + + def __init__(self, parent=None): + super().__init__(parent) + self._build_gui() + self.connect_signals() + + self.plot() + + def connect_signals(self): + self.button_load.clicked.connect(self._handle_load_files_button_clicked) + self._list_view_measurements.doubleClicked.connect(self._handle_list_measurements_double_clicked) + + @Slot() + def _handle_load_files_button_clicked(self): + LOGGER.debug("Loading Optics files button clicked.") + self.sig_load_button_clicked.emit() + + @Slot(QModelIndex) + def _handle_list_measurements_double_clicked(self, idx): + LOGGER.debug(f"Entry in Optics List double-clicked: {idx.data(role=Qt.DisplayRole)}") + + + def _build_gui(self): + self.setWindowTitle(self.WINDOW_TITLE) + self._central = QtWidgets.QSplitter(Qt.Horizontal) + + def build_navigation_widget(): # --- Left Hand Side + navigation_widget = QtWidgets.QSplitter(Qt.Vertical) + + def build_navigation_top(): + nav_top = QtWidgets.QWidget() + + layout = QtWidgets.QVBoxLayout() + nav_top.setLayout(layout) + layout.addWidget(QtWidgets.QLabel("Loaded Optics:")) + + self._list_view_measurements = QtWidgets.QListView() + self._list_view_measurements.setModel(MeasurementListModel()) + self._list_view_measurements.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + layout.addWidget(self._list_view_measurements) + + def build_measurement_buttons(): + grid_buttons = QtWidgets.QGridLayout() + grid_buttons_filler = HorizontalGridLayoutFiller(layout=grid_buttons, cols=2) + + load = OpenButton("Load") + grid_buttons_filler.add(load) + self.button_load = load + + remove = RemoveButton() + grid_buttons_filler.add(remove) + self.button_remove = remove + + edit = EditButton() + grid_buttons_filler.add(edit, col_span=2) + self.button_edit = edit + + matcher = RunButton("Run Matcher") + grid_buttons_filler.add(matcher, col_span=2) + self.button_matcher = matcher + + return grid_buttons + + layout.addLayout(build_measurement_buttons()) + return nav_top + navigation_widget.addWidget(build_navigation_top()) + + + def build_navigation_bottom(): + nav_bottom = QtWidgets.QWidget() + + layout = QtWidgets.QVBoxLayout() + nav_bottom.setLayout(layout) + + layout.addWidget(QtWidgets.QLabel("Segments:")) + + self._table_segments = QtWidgets.QTableView() + layout.addWidget(self._table_segments) + self._table_segments.setModel(SegmentTableModel()) + + header = self._table_segments.horizontalHeader() + header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) + header.setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch) + + self._table_segments.setShowGrid(True) + + def build_segment_buttons(): + grid_buttons = QtWidgets.QGridLayout() + grid_buttons_filler = HorizontalGridLayoutFiller(layout=grid_buttons, cols=3) + + run = RunButton("Run Segment(s)") + grid_buttons_filler.add(run, col_span=3) + self.button_run_segment = run + + new = OpenButton("New") + grid_buttons_filler.add(new) + self.button_new_segment = new + + copy = EditButton("Copy") + grid_buttons_filler.add(copy) + self.button_copy_segment = copy + + remove = RemoveButton("Remove") + grid_buttons_filler.add(remove) + self.button_remove_segment = remove + + return grid_buttons + layout.addLayout(build_segment_buttons()) + return nav_bottom + + navigation_widget.addWidget(build_navigation_bottom()) + return navigation_widget + self._central.addWidget(build_navigation_widget()) + + + def build_tabs_widget(): # --- Right Hand Side + self._tabs_widget = QtWidgets.QTabWidget() + self._tabs = self._create_tabs(self._tabs_widget) + return self._tabs_widget + + self._central.addWidget(build_tabs_widget()) + + # Set up main widget layout ---- + self._central.setSizes([300, 1000]) + self._central.setStretchFactor(1, 3) + + 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_measurement_list(self) -> MeasurementListModel: + return self._list_view_measurements.model() + + + + def plot(self): + pass + # for plot in self._tabs["Phase"].plots: + # data = pg.PlotDataItem([1,2,3], [4,5,6], data=["one", "two", "three"], name="Testing Line", symbol='o') + # data.scatter.opts['hoverable'] = True + # # data.sigPointsHovered.connect(self.hovered) + # # data.sigPointsClicked.connect(self.clicked) + # plot.addItem(data) + + + def hovered(self, item, points, ev): + print('hovered') + + def clicked(self, item, points, ev): + print('clicked') + + diff --git a/omc3_gui/utils/base_classes.py b/omc3_gui/utils/base_classes.py new file mode 100644 index 0000000..f1568de --- /dev/null +++ b/omc3_gui/utils/base_classes.py @@ -0,0 +1,89 @@ +import sys +from typing import List, Union +from qtpy.QtCore import QObject +from qtpy.QtWidgets import QApplication, QFileDialog, QMenuBar, QDesktopWidget, QWidgetAction +from omc3_gui import __version__ + +try: + from accwidgets.app_frame import ApplicationFrame + from accwidgets.qt import exec_app_interruptable +except ImportError: + from qtpy.QtWidgets import QMainWindow as ApplicationFrame + exec_app_interruptable = lambda app: app.exec() + + + +class Controller(QObject): + + def __init__(self, view: ApplicationFrame, *args, **kwargs): + super().__init__(*args, **kwargs) + self._view = view + + def show(self): + self._view.show() + + @classmethod + def run_application(cls, *args, **kwargs): + app = QApplication(sys.argv) + controller = cls(*args, **kwargs) + controller.show() + sys.exit(exec_app_interruptable(app)) + + + +class View(ApplicationFrame): + + __app_version__ = __version__ + _menu_bar: QMenuBar + + def __init__(self, *args, **kwargs): + kwargs["use_log_console"] = kwargs.get("use_log_console", True) + try: + super().__init__(*args, **kwargs) # CERN Application Frame + except TypeError: + del kwargs["use_log_console"] + super().__init__(*args, **kwargs) # QT Main window + + if getattr(self, "log_console"): + self.log_console.console.expanded = False + self.log_console.setFeatures( + self.log_console.DockWidgetClosable | self.log_console.DockWidgetMovable + ) + + # Sizing --- + screen_shape = QDesktopWidget().screenGeometry() + self.resize(2 * screen_shape.width() / 3, + 2 * screen_shape.height() / 3) + + self.build_menu_bar() + + def build_menu_bar(self): + self._menu_bar = QMenuBar() + + # File menu --- + file = self._menu_bar.addMenu("File") + quit = file.addAction("Exit", self.close) + quit.setMenuRole(QWidgetAction.QuitRole) + + # View menu --- + view = self._menu_bar.addMenu("View") + toggle_fullscreen = view.addAction("Full Screen", self.toggleFullScreen) + toggle_fullscreen.setCheckable(True) + + # Help menu --- + help = self._menu_bar.addMenu("Help") + about = help.addAction("About", self.showAboutDialog) + about.setMenuRole(QWidgetAction.AboutRole) + + # Set menu bar --- + self.setMenuBar(self._menu_bar) + + + def setWindowTitle(self, title: str): + super().setWindowTitle(f"{title} v{self.__app_version__}") + + def toggleFullScreen(self): + if self.isFullScreen(): + self.showNormal() + else: + self.showFullScreen() \ No newline at end of file diff --git a/omc3_gui/utils/counter.py b/omc3_gui/utils/counter.py new file mode 100644 index 0000000..2bb2398 --- /dev/null +++ b/omc3_gui/utils/counter.py @@ -0,0 +1,61 @@ +from qtpy import QtWidgets + + +class Counter: + """ Simple class to count up integers. Similar to itertools.count """ + + def __init__(self, start=0, end=None): + self.count = start + self._end = end + self._start = start + + def reset(self): + self.count = self._start + + def __next__(self): + self.count += 1 + + if self._end is not None and self.count >= self._end: + raise StopIteration + + return self.count + + def __iter__(self): + return self + + def next(self): + return next(self) + + def current(self): + return self.count + + +class HorizontalGridLayoutFiller: + """Fills a grid-layout with widgets, without having to give row and col positions, + but allows giving a col-span. + """ + + def __init__(self, layout: QtWidgets.QGridLayout, cols: int, rows: int = None): + self._layout = layout + self._cols = cols + self._rows = rows + self._current_col = 0 + self._current_row = 0 + + + def add(self, widget, col_span=1): + self._layout.addWidget(widget, self._current_row, self._current_col, 1, col_span) + self._current_col += col_span + if self._current_col > self._cols: + raise ValueError("Span too large for given columns.") + + if self._current_col == self._cols: + self._current_col = 0 + self._current_row += 1 + if self._rows is not None and self._current_row >= self._rows: + raise ValueError("Grid is already full.") + + + addWidget = add + + \ No newline at end of file diff --git a/omc3_gui/utils/dialogs.py b/omc3_gui/utils/dialogs.py new file mode 100644 index 0000000..5c72f0d --- /dev/null +++ b/omc3_gui/utils/dialogs.py @@ -0,0 +1,53 @@ + +import logging +from pathlib import Path +from typing import List, Optional, Union + +from qtpy.QtWidgets import QApplication, QFileDialog, QStyle + +LOGGER = logging.getLogger(__name__) + + +class OpenAnyFileDialog(QFileDialog): + def __init__(self, **kwargs) -> None: + """ Quick dialog to open any kind of file. + Modifies QFileDialog, and allows only kwargs to be passed. + """ + super().__init__(**kwargs) # parent, caption, directory, filter, options + self.setOption(QFileDialog.Option.DontUseNativeDialog, True) + + def run_selection_dialog(self) -> List[Path]: + if self.exec_(): + return [Path(f) for f in self.selectedFiles()] + return [] + + +class OpenDirectoriesDialog(OpenAnyFileDialog): + def __init__(self, caption = "Select Folders", **kwargs) -> None: + super().__init__(caption=caption, **kwargs) # parent, directory, filter, options + icon = QApplication.style().standardIcon(QStyle.SP_DirIcon) + self.setWindowIcon(icon) + self.setOption(QFileDialog.Option.ShowDirsOnly, True) + self.setFileMode(QFileDialog.ExistingFiles) + + def accept(self): + """This function is called when the user clicks on "Open". + Normally, when selecting a directories, the first directory is followed/opened inside the dialog, + i.e. its content is shown. Overwrite super().accept() to prevent that and close the dialog instead. + """ + for selected in self.selectedFiles(): + if not Path(selected).is_dir(): # this should not happen, as only directories are shown + LOGGER.warning(f"{selected} is not a directory. Try again.") + return + + self.done(QFileDialog.Accepted) + + +class OpenDirectoryDialog(OpenDirectoriesDialog): + def __init__(self, caption = "Select Folder", **kwargs) -> None: + super().__init__(caption=caption, **kwargs) # parent, directory, filter, options + self.setFileMode(QFileDialog.DirectoryOnly) + + def run_selection_dialog(self) -> Path: + return super().run_selection_dialog()[0] + diff --git a/omc3_gui/utils/log_handler.py b/omc3_gui/utils/log_handler.py index 1912579..fdda9d2 100644 --- a/omc3_gui/utils/log_handler.py +++ b/omc3_gui/utils/log_handler.py @@ -31,3 +31,12 @@ def _get_formatter(): ) formatter.datefmt = '%d/%m/%Y %H:%M:%S' return formatter + + +def init_logging(level=logging.INFO): + """ Set up a basic logger. """ + logging.basicConfig( + stream=sys.stdout, + level=level, + format="%(levelname)7s | %(message)s | %(name)s" + ) \ No newline at end of file diff --git a/omc3_gui/utils/threads.py b/omc3_gui/utils/threads.py new file mode 100644 index 0000000..31227a3 --- /dev/null +++ b/omc3_gui/utils/threads.py @@ -0,0 +1,34 @@ +class BackgroundThread(QThread): + + on_exception = Signal([str]) + + def __init__(self, view, function, message=None, + on_end_function=None, on_exception_function=None): + QThread.__init__(self) + self._view = view + self._function = function + self._message = message + self._on_end_function = on_end_function + self._on_exception_function = on_exception_function + + def run(self): + try: + self._function() + except Exception as e: + LOGGER.exception(str(e)) + self.on_exception.emit(str(e)) + + def start(self): + self.finished.connect(self._on_end) + self.on_exception.connect(self._on_exception) + super(BackgroundThread, self).start() + self._view.show_background_task_dialog(self._message) + + def _on_end(self): + self._view.hide_background_task_dialog() + self._on_end_function() + + def _on_exception(self, exception_message): + self._view.hide_background_task_dialog() + self._view.show_error_dialog("Error", exception_message) + self._on_exception_function(exception_message) \ No newline at end of file diff --git a/omc3_gui/utils/widgets.py b/omc3_gui/utils/widgets.py new file mode 100644 index 0000000..6a544e1 --- /dev/null +++ b/omc3_gui/utils/widgets.py @@ -0,0 +1,50 @@ +""" +Widgets +------- + +Pre-Defined Widgets go here. +""" + +from qtpy import QtWidgets + + +# Buttons ---------------------------------------------------------------------- + +class RunButton(QtWidgets.QPushButton): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not args and not "text" in kwargs: + self.setText("Run") + + self.setStyleSheet("background-color: #28642A; color: #fff;") + + +class OpenButton(QtWidgets.QPushButton): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not args and not "text" in kwargs: + self.setText("Open") + + self.setStyleSheet("background-color: #4CAF50; color: #000000;") + + +class RemoveButton(QtWidgets.QPushButton): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not args and not "text" in kwargs: + self.setText("Remove") + + self.setStyleSheet("background-color: #f44336; color: #000000;") + + +class EditButton(QtWidgets.QPushButton): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not args and not "text" in kwargs: + self.setText("Edit") + + self.setStyleSheet("background-color: #2196F3; color: #fff;") \ No newline at end of file