diff --git a/sohstationviewer/attributions.txt b/sohstationviewer/attributions.txt new file mode 100644 index 0000000000000000000000000000000000000000..48af1494cb338bb032cccf6bef2cfde19296570e --- /dev/null +++ b/sohstationviewer/attributions.txt @@ -0,0 +1 @@ +<a href="https://www.flaticon.com/free-icons/restart" title="restart icons">Restart icons created by Icon Mart - Flaticon</a> diff --git a/sohstationviewer/images/revert_icon_black_background.png b/sohstationviewer/images/revert_icon_black_background.png new file mode 100644 index 0000000000000000000000000000000000000000..877b4af0689ef05c10d5ede8fa3cb2a208669f82 Binary files /dev/null and b/sohstationviewer/images/revert_icon_black_background.png differ diff --git a/sohstationviewer/images/revert_icon_white_background.png b/sohstationviewer/images/revert_icon_white_background.png new file mode 100644 index 0000000000000000000000000000000000000000..3953f2709b70eedc55703b3516705fc3f7ec9fdd Binary files /dev/null and b/sohstationviewer/images/revert_icon_white_background.png differ diff --git a/sohstationviewer/view/db_config/value_color_helper/functions.py b/sohstationviewer/view/db_config/value_color_helper/functions.py index 5d9684cdca04962f417614becb43f40b8354b834..41889f31a94e200682fecfa2f00b70ea45c549fc 100644 --- a/sohstationviewer/view/db_config/value_color_helper/functions.py +++ b/sohstationviewer/view/db_config/value_color_helper/functions.py @@ -79,7 +79,7 @@ def prepare_value_color_html(value_colors: str) -> str: else: c_html = ( f"{value}:" - f"<span style='color: {vcs[1]}; font-size:25px;'>∎" + f"<span style='color: {vcs[1]}; font-size:18px;'>■" f"</span>{size}") html_color_parts.append(c_html) diff --git a/sohstationviewer/view/db_config/value_color_helper/value_color_edit.py b/sohstationviewer/view/db_config/value_color_helper/value_color_edit.py index 5c2ed843e42b4d39671bbd62cac3cbdb6bc9c49f..851ca6e5b65e134b14474f3e632a3d0abbc9004f 100644 --- a/sohstationviewer/view/db_config/value_color_helper/value_color_edit.py +++ b/sohstationviewer/view/db_config/value_color_helper/value_color_edit.py @@ -2,36 +2,38 @@ from typing import Optional, Type from PySide6 import QtWidgets, QtGui, QtCore from PySide6.QtCore import Qt -from PySide6.QtWidgets import QWidget, QTextEdit, QMessageBox +from PySide6.QtGui import QPalette, QColor +from PySide6.QtWidgets import ( + QWidget, QMessageBox, QLabel, QFrame, +) from sohstationviewer.conf.constants import ROOT_PATH -from sohstationviewer.view.db_config.value_color_helper.\ +from sohstationviewer.view.db_config.param_helper import \ + validate_value_color_str +from sohstationviewer.view.db_config.value_color_helper. \ + edit_value_color_dialog import DotForTimeDialog +from sohstationviewer.view.db_config.value_color_helper. \ edit_value_color_dialog import EditValueColorDialog -from sohstationviewer.view.db_config.value_color_helper.\ +from sohstationviewer.view.db_config.value_color_helper. \ edit_value_color_dialog import LineDotDialog -from sohstationviewer.view.db_config.value_color_helper.\ - edit_value_color_dialog import ( - MultiColorDotLowerEqualDialog, MultiColorDotUpperEqualDialog) -from sohstationviewer.view.db_config.value_color_helper.\ +from sohstationviewer.view.db_config.value_color_helper. \ + edit_value_color_dialog import (MultiColorDotLowerEqualDialog, + MultiColorDotUpperEqualDialog) +from sohstationviewer.view.db_config.value_color_helper. \ edit_value_color_dialog import TriColorLinesDialog -from sohstationviewer.view.db_config.value_color_helper.\ +from sohstationviewer.view.db_config.value_color_helper. \ edit_value_color_dialog import UpDownDialog -from sohstationviewer.view.db_config.value_color_helper.\ - edit_value_color_dialog import DotForTimeDialog from sohstationviewer.view.db_config.value_color_helper.functions import \ prepare_value_color_html -from sohstationviewer.view.db_config.param_helper import \ - validate_value_color_str -from sohstationviewer.view.util.plot_type_info import plot_types from sohstationviewer.view.util.color import COLOR - +from sohstationviewer.view.util.plot_type_info import plot_types plot_types_with_value_colors = [ p for p in plot_types if 'pattern' in plot_types[p]] -class ValueColorEdit(QTextEdit): +class ValueColorEdit(QFrame): """ Widget to display valueColors and call a dialog to edit tha value """ @@ -54,19 +56,37 @@ class ValueColorEdit(QTextEdit): :param errmsg_header: header for error message to help user identify the box that has value color string error. """ - QtWidgets.QTextEdit.__init__(self, parent) + super().__init__(parent) + # Background color for setting border color match with background - self.background: str = 'black' if background == 'B' else 'white' - self.set_background_color(background) + self.background: QColor = ( + QColor(Qt.GlobalColor.black) + if background == 'B' + else QColor(Qt.GlobalColor.white) + ) # Type of channel's plot self.plot_type: str = plot_type - # Original value color string to know if value color string has been - # changed. - self.org_value_color_str: str = value_color_str + # Value colors string given as the input. Only used to determine if the + # currently displayed value colors string is different from the one + # given as the input. + self.original_value_color_str: str = value_color_str # Current value color string displayed on widget self.value_color_str: str = '' # Flag showing if original value color string passes validation self.is_valid: bool = True + # dialog that pop up when clicking on edit_button to help edit color + # and value + self.edit_value_color_dialog: Optional[QWidget] = None + # Component widgets of this widget + self.value_color_display = QLabel(self) + self.revert_button = QtWidgets.QToolButton(self) + self.edit_button = QtWidgets.QToolButton(self) + + self.setup_ui(background, plot_type) + + # Value colors string to revert to. Might be different from the input + # because we always want to revert to a valid value colors string. + self.base_value_color_str = '' if plot_type in plot_types_with_value_colors: if value_color_str == '': # Use default value_color_str for the empty one @@ -78,7 +98,7 @@ class ValueColorEdit(QTextEdit): raise ValueError( f"{errmsg_header}{err_msg}\n\n The ValueColors string " f"will be replaced with {plot_type}'s default one.") - self.set_value_color(value_color_str) + self.base_value_color_str = value_color_str except ValueError as e: QMessageBox.critical( self, f'Invalid ValueColors:{plot_type}', str(e)) @@ -86,27 +106,53 @@ class ValueColorEdit(QTextEdit): # We set the value colors to the default value (specified in # plot_type_info.py) when there is a problem with the given # value colors string. - self.set_value_color( + self.base_value_color_str = ( plot_types[plot_type]['default_value_color'] ) + self.set_value_color(self.base_value_color_str) - # dialog that pop up when clicking on edit_button to help edit color - # and value - self.edit_value_color_dialog: Optional[QWidget] = None + def setup_ui(self, background: str, plot_type: str): + # Set up the border of the widget. + self.setFrameShape(QFrame.Shape.Box) + self.setFrameShadow(QFrame.Shadow.Plain) + self.set_border_color(self.background) + # If we make the border disappear altogether when it is not needed, the + # buttons will be moved a bit when the border actually shows up. This + # does not look the best when we have multiple of this widget in a row + # (e.g. in the Params table editor). Instead, we make it so that the + # border always shows up but blend in with the widget's background + # unless needed. + self.setLineWidth(4) - self.setReadOnly(True) - # change cursor to Arrow so user know they can't edit directly - self.viewport().setCursor( - QtGui.QCursor(QtCore.Qt.CursorShape.ArrowCursor) - ) - # to see all info - self.setFixedHeight(28) - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - self.verticalScrollBar().setDisabled(True) + # Without this line, the background of the widget will not be filled + # with the color we want. + self.setAutoFillBackground(True) + self.set_background_color(background) - self.edit_button = QtWidgets.QToolButton(self) - self.edit_button.setCursor(Qt.PointingHandCursor) + # QLabels expands only enough to fit their content, so we need to + # expand their width manually to make space for longer value colors + # strings. + # The actual width was chosen pretty arbitrarily. It is simply the + # first value we tested that makes the UI looks good. + self.value_color_display.setFixedWidth(250) + + self.revert_button.setCursor(Qt.PointingHandCursor) + if background == 'B': + img_file = f"{ROOT_PATH}/images/revert_icon_black_background.png" + else: + img_file = f"{ROOT_PATH}/images/revert_icon_white_background.png" + self.revert_button.setIcon(QtGui.QIcon(img_file)) + self.revert_button.setStyleSheet( + "background: transparent; border: none;") + self.revert_button.clicked.connect(self.revert) + # Make it so that the widget does not change size when the revert + # button shows up after being hidden. + size_policy = self.revert_button.sizePolicy() + size_policy.setRetainSizeWhenHidden(True) + self.revert_button.setSizePolicy(size_policy) + self.revert_button.hide() + + self.edit_button.setCursor(Qt.CursorShape.PointingHandCursor) if background == 'B': img_file = f"{ROOT_PATH}/images/edit_icon_black_background.png" else: @@ -119,21 +165,30 @@ class ValueColorEdit(QTextEdit): self.edit_button.hide() layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(self.edit_button, 0, Qt.AlignRight) - layout.setSpacing(0) - layout.setContentsMargins(5, 5, 5, 5) + layout.setContentsMargins(5, 0, 5, 0) - self.textChanged.connect( - lambda: self.value_color_edited.emit(self.value_color_str) - ) + layout.addWidget(self.value_color_display) + layout.addWidget(self.revert_button) + layout.addWidget(self.edit_button) - def set_border_color(self, color): + def set_border_color(self, color: QColor): """ Set border color for the widget. :param color: color to set to border """ - self.setStyleSheet("QTextEdit {border: 4px solid %s;}" % color) + palette = self.palette() + # The border color of a QFrame is handled by its foreground color + # roles. This is not explicitly documented in the official QT doc. + # Instead, this fact is only hinted. The stackoverflow question below + # is where this information is found. + # https://stackoverflow.com/questions/51056997/how-to-set-color-of-frame-of-qframe # noqa + # The color role used for the border color of a QFrame depends on + # whether it is embedded in a QTableWidget or is used as a standalone + # widget. This is understandable, but not entirely expected. + palette.setColor(QPalette.ColorRole.Text, color) + palette.setColor(QPalette.ColorRole.WindowText, color) + self.setPalette(palette) def set_background_color(self, background: str): """ @@ -143,13 +198,23 @@ class ValueColorEdit(QTextEdit): :param background: 'B'/'W': sign for background color """ palette = self.palette() + display_palette = self.value_color_display.palette() + # The color role actually used in a palette depends on whether the + # widget is embedded in a QTableWidget (and maybe other views) or is + # used as a standalone widget. This is understandable, but not entirely + # expected. if background == 'B': - palette.setColor(QtGui.QPalette.ColorRole.Text, Qt.white) - palette.setColor(QtGui.QPalette.ColorRole.Base, Qt.black) + display_palette.setColor(QPalette.ColorRole.Text, Qt.white) + display_palette.setColor(QPalette.ColorRole.WindowText, Qt.white) + palette.setColor(QPalette.ColorRole.Base, Qt.black) + palette.setColor(QPalette.ColorRole.Window, Qt.black) else: - palette.setColor(QtGui.QPalette.ColorRole.Text, Qt.black) - palette.setColor(QtGui.QPalette.ColorRole.Base, Qt.white) + display_palette.setColor(QPalette.ColorRole.Text, Qt.black) + display_palette.setColor(QPalette.ColorRole.WindowText, Qt.black) + palette.setColor(QPalette.ColorRole.Base, Qt.white) + palette.setColor(QPalette.ColorRole.Window, Qt.white) self.setPalette(palette) + self.value_color_display.setPalette(display_palette) def set_value_color(self, value_color_str: str) -> None: """ @@ -160,8 +225,13 @@ class ValueColorEdit(QTextEdit): """ self.value_color_str = value_color_str value_color_html = prepare_value_color_html(self.value_color_str) - self.setHtml(value_color_html) + self.value_color_display.setText(value_color_html) + self.value_color_edited.emit(self.value_color_str) self.set_border_color_according_to_value_color_status() + if value_color_str != self.base_value_color_str: + self.revert_button.show() + else: + self.revert_button.hide() def set_border_color_according_to_value_color_status(self): """ @@ -169,13 +239,16 @@ class ValueColorEdit(QTextEdit): + Blue for changed + Same color as background (no border) for other case. """ - if self.value_color_str != self.org_value_color_str: + if self.value_color_str != self.original_value_color_str: # Show that value_color_str has been changed - self.set_border_color(COLOR['changedStatus']) + self.set_border_color(QColor(COLOR['changedStatus'])) else: # Reset border the same color as background self.set_border_color(self.background) + def revert(self): + self.set_value_color(self.base_value_color_str) + def edit(self): """ Show user an editor to edit the value colors of this widget. diff --git a/tests/view/db_config/value_color_helper/test_functions.py b/tests/view/db_config/value_color_helper/test_functions.py index 91ae92c148c16192893ba515e1b2ef1d4c233532..829e36e5ea791048e82542366bd254032929b38e 100644 --- a/tests/view/db_config/value_color_helper/test_functions.py +++ b/tests/view/db_config/value_color_helper/test_functions.py @@ -10,74 +10,74 @@ class TestPrepareValueColorHTML(BaseTestCase): with self.subTest("Line and dot values"): expected_value_colors = ( "<p>Line:" - "<span style='color: #00FF00; font-size:25px;'>∎</span>" + "<span style='color: #00FF00; font-size:18px;'>■</span>" "|Dot:" - "<span style='color: #00FF00; font-size:25px;'>∎</span>" + "<span style='color: #00FF00; font-size:18px;'>■</span>" "</p>") result = prepare_value_color_html("Line:#00FF00|Dot:#00FF00") - self.assertEqual(result, expected_value_colors) + self.assertEqual(expected_value_colors, result) with self.subTest("Line value"): expected_value_colors = ( - "<p>Line:<span style='color: #00FF00; font-size:25px;'>∎" + "<p>Line:<span style='color: #00FF00; font-size:18px;'>■" "</span></p>") result = prepare_value_color_html("Line:#00FF00") - self.assertEqual(result, expected_value_colors) + self.assertEqual(expected_value_colors, result) def test_up_down_dots(self): expected_value_colors = ( "<p>Down:" - "<span style='color: #FF0000; font-size:25px;'>∎</span>" + "<span style='color: #FF0000; font-size:18px;'>■</span>" "|Up:" - "<span style='color: #00FF00; font-size:25px;'>∎</span>" + "<span style='color: #00FF00; font-size:18px;'>■</span>" "</p>") result = prepare_value_color_html("Down:#FF0000|Up:#00FF00") - self.assertEqual(result, expected_value_colors) + self.assertEqual(expected_value_colors, result) def test_multi_color_dots_equal_on_upper_bound(self): expected_value_colors = ( "<p>≤0:not plot" "|≤1:" - "<span style='color: #FFFF00; font-size:25px;'>∎</span>" + "<span style='color: #FFFF00; font-size:18px;'>■</span>" ":▲|≤2:" - "<span style='color: #00FF00; font-size:25px;'>∎</span>" + "<span style='color: #00FF00; font-size:18px;'>■</span>" "|2<:" - "<span style='color: #FF00FF; font-size:25px;'>∎</span>" + "<span style='color: #FF00FF; font-size:18px;'>■</span>" "</p>") result = prepare_value_color_html( '<=0:not plot|<=1:#FFFF00:bigger|<=2:#00FF00|2<:#FF00FF') - self.assertEqual(result, expected_value_colors) + self.assertEqual(expected_value_colors, result) def test_multi_color_dots_equal_on_lower_bound(self): expected_value_colors = ( "<p><3:not plot" "|<3.3:" - "<span style='color: #FFFF00; font-size:25px;'>∎</span>" + "<span style='color: #FFFF00; font-size:18px;'>■</span>" "|=3.3:" - "<span style='color: #00FF00; font-size:25px;'>∎</span>" + "<span style='color: #00FF00; font-size:18px;'>■</span>" ":▼</p>") result = prepare_value_color_html( '<3:not plot|<3.3:#FFFF00|=3.3:#00FF00:smaller') - self.assertEqual(result, expected_value_colors) + self.assertEqual(expected_value_colors, result) def test_tri_color_lines(self): expected_value_colors = ( "<p>-1:" - "<span style='color: #FF00FF; font-size:25px;'>∎</span>" - "|0:<span style='color: #FF0000; font-size:25px;'>∎</span>" - "|1:<span style='color: #00FF00; font-size:25px;'>∎</span>" + "<span style='color: #FF00FF; font-size:18px;'>■</span>" + "|0:<span style='color: #FF0000; font-size:18px;'>■</span>" + "|1:<span style='color: #00FF00; font-size:18px;'>■</span>" "</p>" ) result = prepare_value_color_html('-1:#FF00FF|0:#FF0000|1:#00FF00') - self.assertEqual(result, expected_value_colors) + self.assertEqual(expected_value_colors, result) def test_dot_for_time(self): expected_value_colors = ( "<p>C:" - "<span style='color: #FF00FF; font-size:25px;'>∎</span>" + "<span style='color: #FF00FF; font-size:18px;'>■</span>" "</p>" ) result = prepare_value_color_html('C:#FF00FF') - self.assertEqual(result, expected_value_colors) + self.assertEqual(expected_value_colors, result) def test_empty_input(self): expected = ''