diff --git a/omc3_gui/segment_by_segment/view.py b/omc3_gui/segment_by_segment/view.py index eac3c6c..ea2f44a 100644 --- a/omc3_gui/segment_by_segment/view.py +++ b/omc3_gui/segment_by_segment/view.py @@ -16,6 +16,7 @@ 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 +from omc3_gui.utils import colors LOGGER = logging.getLogger(__name__) @@ -235,13 +236,13 @@ def __init__(self): self.setModel(MeasurementListModel()) self.setItemDelegate(ColoredItemDelegate()) self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) - tooltip_style = """ - QToolTip { - background-color: #F0F0F0; /* Light gray background */ - color: #333333; /* Dark gray text */ - border: 1px solid #808080; /* Gray border */ + tooltip_style = f""" + QToolTip {{ + background-color: {colors.TOOLTIP_BACKGROUND}; /* Light gray background */ + color: {colors.TOOLTIP_TEXT}; /* Dark gray text */ + border: 1px solid {colors.TOOLTIP_BORDER}; /* Gray border */ font-family: "Courier New", monospace; /* Monospaced font */ - } + }} """ self.setStyleSheet(tooltip_style) @@ -249,14 +250,13 @@ def __init__(self): class ColoredItemDelegate(QtWidgets.QStyledItemDelegate): COLOR_MAP = { - MeasurementListModel.ColorIDs.NONE: "#000000", - MeasurementListModel.ColorIDs.BEAM1: "#0000ff", - MeasurementListModel.ColorIDs.BEAM2: "#ff0000", - # todo: what are the PSB ring colors? - MeasurementListModel.ColorIDs.RING1: "#4CAF50", - MeasurementListModel.ColorIDs.RING2: "#FF9800", - MeasurementListModel.ColorIDs.RING3: "#673AB7", - MeasurementListModel.ColorIDs.RING4: "#E91E63", + MeasurementListModel.ColorIDs.NONE: colors.TEXT_DARK, + MeasurementListModel.ColorIDs.BEAM1: colors.BEAM1, + MeasurementListModel.ColorIDs.BEAM2: colors.BEAM2, + MeasurementListModel.ColorIDs.RING1: colors.RING1, + MeasurementListModel.ColorIDs.RING2: colors.RING2, + MeasurementListModel.ColorIDs.RING3: colors.RING3, + MeasurementListModel.ColorIDs.RING4: colors.RING4, } def paint(self, painter, option, index): # Customize the text color diff --git a/omc3_gui/utils/colors.py b/omc3_gui/utils/colors.py new file mode 100644 index 0000000..ec9e1f1 --- /dev/null +++ b/omc3_gui/utils/colors.py @@ -0,0 +1,40 @@ +BLACK_87 = "#212121" +BLACK_54 = "#757575" +BLACK_38 = "#9e9e9e" +BLACK_26 = "#bdbdbd" +BLACK_12 = "#e1e1e1" +WHITE = "#FFFFFF" + +GREEN_DARK = "#28642A" +GREEN_LIGHT = "#4CAF50" + +RED_DARK = "#B71C1C" +RED_LIGHT = "#F44336" + +BLUE_DARK = "#0D47A1" +BLUE_LIGHT = "#2196F3" + +# Machine Colors --- +# LHC Colors +BEAM1 = "#0000ff" +BEAM2 = "#ff0000" + +# PSB Ring Colors (TODO?) +RING1 = "#4CAF50" +RING2 = "#FF9800" +RING3 = "#673AB7" +RING4 = "#E91E63" + +# Light Background, dark text --- +TEXT_DARK = BLACK_87 +SECONDARY_TEXT_DARK = BLACK_54 +GREYED_OUT_TEXT_DARK = BLACK_26 + +# Dark Background, light text --- +TEXT_LIGHT = WHITE +SECONDARY_TEXT_LIGHT = BLACK_12 + +# Tooltips --- +TOOLTIP_TEXT = BLACK_87 +TOOLTIP_BACKGROUND = BLACK_12 +TOOLTIP_BORDER = BLACK_38 \ No newline at end of file diff --git a/omc3_gui/utils/dataclass_ui.py b/omc3_gui/utils/dataclass_ui.py index b1623aa..61df357 100644 --- a/omc3_gui/utils/dataclass_ui.py +++ b/omc3_gui/utils/dataclass_ui.py @@ -1,13 +1,15 @@ +from functools import partial import inspect import re from dataclasses import MISSING, Field, dataclass, field, fields from pathlib import Path from typing import Callable, Dict, Optional, Sequence, Tuple, Union -from PyQt5.QtWidgets import QWidget +from omc3_gui.utils import file_dialogs from qtpy import QtWidgets from omc3_gui.utils.widgets import HorizontalSeparator +from omc3_gui.utils import colors import logging LOGGER = logging.getLogger(__name__) @@ -71,7 +73,7 @@ class FieldUI: label: QtWidgets.QLabel # label-widget of the field get_value: Callable # getter for the widget value, returns the value as appropriate type for the dataclass set_value: Callable # setter for the widget value - text_color: Optional[str] = "black" # default text-color for both widget and label + text_color: Optional[str] = colors.TEXT_DARK # default text-color for both widget and label modified: bool = False # flag indicating if the widget-content has been modified by the user def __post_init__(self): @@ -85,7 +87,7 @@ def __post_init__(self): def has_changed(self): """ Triggered when the widget has been modified. - Sets then the label font to italic. + Sets then the modified flag and changes label font. """ self.modified = True font = self.label.font() @@ -201,7 +203,6 @@ def build_dataclass_ui(cls, except AttributeError: widget.setEnabled(field.editable) - layout.addWidget(widget, idx_row, 1) get_value, set_value = build_getter_setter(widget, field_type) dataclass_ui.fields[field.name] = FieldUI( @@ -209,10 +210,33 @@ def build_dataclass_ui(cls, label=qlabel, set_value=set_value, get_value=get_value, - text_color="#000000" if field.editable else "#bbbbbb" + text_color=colors.TEXT_DARK if field.editable else colors.GREYED_OUT_TEXT_DARK ) + + if not issubclass(field_type, Path) or not field.editable: + layout.addWidget(widget, idx_row, 1, 1, 2) + else: + layout.addWidget(widget, idx_row, 1) + # Add Path selection button --- + button = QtWidgets.QPushButton("...") + button.setFixedWidth(30) + + if field_type is FilePath: + dialog = file_dialogs.OpenFileDialog + + elif field_type is DirectoryPath: + dialog = file_dialogs.OpenDirectoryDialog + + else: + dialog = file_dialogs.OpenAnySingleDialog + + button.clicked.connect(partial( + run_dialog, + dialog=dialog, + get_value=get_value, + set_value=set_value)) + layout.addWidget(button, idx_row, 2) - # TODO: add path button return dataclass_ui @@ -264,6 +288,12 @@ def set_value(value): return get_value, set_value +def run_dialog(dialog: file_dialogs.OpenFilesDialog, get_value: Callable, set_value: Callable): + """ Asks the user to select a directory/file. """ + path = dialog(directory=get_value().parent).run_selection_dialog() + if path is not None: + set_value(path) + # Other ------------------------------------------------------------------------ diff --git a/omc3_gui/utils/file_dialogs.py b/omc3_gui/utils/file_dialogs.py index bdfb454..3ec4a6c 100644 --- a/omc3_gui/utils/file_dialogs.py +++ b/omc3_gui/utils/file_dialogs.py @@ -9,12 +9,15 @@ # Open Dialog Windows ---------------------------------------------------------- -class OpenAnyFileDialog(QFileDialog): +class OpenFilesDialog(QFileDialog): + """ Quick dialog to open any kind of file. + Modifies QFileDialog, and allows only kwargs to be passed. + """ def __init__(self, **kwargs) -> None: - """ Quick dialog to open any kind of file. - Modifies QFileDialog, and allows only kwargs to be passed. - """ + if "directory" in kwargs and isinstance(kwargs["directory"], Path): + kwargs["directory"] = str(kwargs["directory"]) # allow giving Paths + super().__init__(**kwargs) # parent, caption, directory, filter, options self.setOption(QFileDialog.Option.DontUseNativeDialog, True) @@ -24,7 +27,23 @@ def run_selection_dialog(self) -> List[Path]: return [] -class OpenDirectoriesDialog(OpenAnyFileDialog): +class OpenFileDialog(OpenFilesDialog): + """ Open a single file. """ + + def __init__(self, caption = "Select File", **kwargs) -> None: + super().__init__(caption=caption, **kwargs) # parent, directory, filter, options + self.setFileMode(QFileDialog.ExistingFile) + + def run_selection_dialog(self) -> Path: + selected = super().run_selection_dialog() + if selected: + return selected[0] + return None + + +class OpenDirectoriesDialog(OpenFilesDialog): + """ Open multiple directories. """ + def __init__(self, caption = "Select Folders", **kwargs) -> None: super().__init__(caption=caption, **kwargs) # parent, directory, filter, options icon = QApplication.style().standardIcon(QStyle.SP_DirIcon) @@ -46,17 +65,47 @@ def accept(self): class OpenDirectoryDialog(OpenDirectoriesDialog): + """ Open a single directory. """ + 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] + selected = super().run_selection_dialog() + if selected: + return selected[0] + return None -class OpenFilesDialog(OpenAnyFileDialog): +class OpenAnyMultiDialog(OpenFilesDialog): + """ Open multiple files/folders. """ + def __init__(self, caption = "Select Files", **kwargs) -> None: super().__init__(caption=caption, **kwargs) # parent, directory, filter, options self.setFileMode(QFileDialog.ExistingFiles) - icon = QApplication.style().standardIcon(QStyle.SP_FileIcon) - self.setWindowIcon(icon) + + 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. + """ + if not self.selectedFiles(): + LOGGER.warning(f"Nothing selected. Try again or cancel.") + return + + self.done(QFileDialog.Accepted) + + +class OpenAnySingleDialog(OpenAnyMultiDialog): + """ Open a single file/folder. """ + + def __init__(self, caption = "Select File", **kwargs) -> None: + super().__init__(caption=caption, **kwargs) # parent, directory, filter, options + self.setFileMode(QFileDialog.ExistingFile) + + def run_selection_dialog(self) -> Path: + selected = super().run_selection_dialog() + if selected: + return selected[0] + return None diff --git a/omc3_gui/utils/widgets.py b/omc3_gui/utils/widgets.py index 711b9bb..b180dc0 100644 --- a/omc3_gui/utils/widgets.py +++ b/omc3_gui/utils/widgets.py @@ -6,7 +6,7 @@ """ from qtpy import QtWidgets -from qtpy import QtCore +from omc3_gui.utils import colors # Buttons ---------------------------------------------------------------------- @@ -17,7 +17,7 @@ def __init__(self, *args, **kwargs): if not args and not "text" in kwargs: self.setText("Run") - self.setStyleSheet("background-color: #28642A; color: #fff;") + self.setStyleSheet(f"background-color: {colors.GREEN_DARK}; color: {colors.TEXT_LIGHT};") class OpenButton(QtWidgets.QPushButton): @@ -27,7 +27,7 @@ def __init__(self, *args, **kwargs): if not args and not "text" in kwargs: self.setText("Open") - self.setStyleSheet("background-color: #4CAF50; color: #000000;") + self.setStyleSheet(f"background-color: {colors.GREEN_LIGHT}; color: {colors.TEXT_DARK};") class RemoveButton(QtWidgets.QPushButton): @@ -36,8 +36,9 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if not args and not "text" in kwargs: self.setText("Remove") + + self.setStyleSheet(f"background-color: {colors.RED_LIGHT}; color: {colors.TEXT_DARK};") - self.setStyleSheet("background-color: #f44336; color: #000000;") class EditButton(QtWidgets.QPushButton): @@ -47,7 +48,7 @@ def __init__(self, *args, **kwargs): if not args and not "text" in kwargs: self.setText("Edit") - self.setStyleSheet("background-color: #2196F3; color: #fff;") + self.setStyleSheet(f"background-color: {colors.BLUE_DARK}; color: {colors.TEXT_LIGHT};") # Filler -----------------------------------------------------------------------