From 710b379c93a742e6ece1ec1c1f4a91e20f7904e0 Mon Sep 17 00:00:00 2001 From: ldam <ldam@passcal.nmt.edu> Date: Fri, 3 Nov 2023 08:54:05 -0600 Subject: [PATCH] dialog to edit valueColors for multiColorDots plot types --- .../multi_color_dot_dialog.py | 504 ++++++++++++++++++ 1 file changed, 504 insertions(+) create mode 100644 sohstationviewer/view/db_config/value_color_helper/edit_value_color_dialog/multi_color_dot_dialog.py diff --git a/sohstationviewer/view/db_config/value_color_helper/edit_value_color_dialog/multi_color_dot_dialog.py b/sohstationviewer/view/db_config/value_color_helper/edit_value_color_dialog/multi_color_dot_dialog.py new file mode 100644 index 000000000..8d50f0b74 --- /dev/null +++ b/sohstationviewer/view/db_config/value_color_helper/edit_value_color_dialog/multi_color_dot_dialog.py @@ -0,0 +1,504 @@ +import sys +import platform +import os + +from typing import List, Dict + +from PySide2 import QtWidgets, QtCore, QtGui +from PySide2.QtWidgets import QWidget +from PySide2.QtGui import QDoubleValidator + +from sohstationviewer.view.db_config.value_color_helper.\ + edit_value_color_dialog.edit_value_color_dialog_super_class import \ + EditValueColorDialog, display_color + + +TOTAL = 7 + + +class BoundValidator(QtGui.QValidator): + """ + Validator that allow float value and empty string. + Range of value is from -10, 10 because -20/20 are the values assigned to + higher bounds of the previous/next when the current row is 0 and last + so the editable higher bounds can't ever be reached to. + Value '-' to allow typing negative float. If user type '-' only, it will + be checked when editing is finished. + """ + def validate(self, input, pos): + if input in ['', '-']: + return QtGui.QValidator.Acceptable + try: + input = float(input) + except ValueError: + return QtGui.QValidator.Invalid + if -10 <= input <= 10: + return QtGui.QValidator.Acceptable + else: + return QtGui.QValidator.Invalid + + +class MultiColorDotDialog(EditValueColorDialog): + def __init__( + self, parent: QWidget, value_color_str: str, upper_equal: bool): + """ + Dialog to edit color for Multi-color Dot Plot + + :param parent: the parent widget + :param value_color_str: string for value color to be saved in DB + :param upper_equal: flag to know if equal is on upper bound + or lower_bound + """ + self.upper_equal = upper_equal + + # list of widgets to display lower bound which is read only of the + # value range of rows + self.lower_bound_lnedits: List[QtWidgets.QLineEdit] = [] + # list of widgets to display lower bound of the value range of rows + self.higher_bound_lnedits: List[QtWidgets.QLineEdit] = [] + # List of buttons that allow user to add/edit color + self.select_color_btns: List[QtWidgets.QPushButton] = [] + # list of labels to display color of the data points defined by + # the value range of rows + self.color_labels: List[QtWidgets.QLabel] = [] + + # After a higher bound widget is edited and enter is hitted, button's + # clicked signal will be called before higher_bound_editting_finished + # is reached. The following flags are to change that order. + # These flags are also used to prevent the case hitting Select Color or + # Save right after user done with editing. In case there is error when + # checking the value, the button click will be canceled too. + self.changed_rowid = None + self.select_color_btn_clicked = False + self.save_colors_btn_clicked = False + + # Check box for plotting data point less than all the rest + self.include_less_than_chkbox = QtWidgets.QCheckBox('Include') + + # Check box for including data point greater than all the rest + self.include_greater_than_chkbox = QtWidgets.QCheckBox('Include') + + # Keep track of value by rowid + self.rowid_value_dict: Dict[int, float] = {} + + super(MultiColorDotDialog, self).__init__(parent, value_color_str) + self.setWindowTitle("Edit Multi Color Dot Plotting") + + # set focus policy to not be clicked by hitting enter in higher bound + self.cancel_btn.setFocusPolicy(QtCore.Qt.NoFocus) + self.save_colors_btn.setFocusPolicy(QtCore.Qt.NoFocus) + + self.set_value_rowid_dict() + + def setup_ui(self) -> None: + + for i in range(TOTAL): + if i == 0: + self.main_layout.addWidget( + self.include_less_than_chkbox, 0, 0, 1, 1) + self.include_less_than_chkbox.setChecked(True) + if i == TOTAL - 1: + self.main_layout.addWidget( + self.include_greater_than_chkbox, TOTAL - 1, 0, 1, 1) + (lower_bound_lnedit, higher_bound_lnedit, + select_color_btn, color_label) = self.add_row(i) + self.lower_bound_lnedits.append(lower_bound_lnedit) + self.higher_bound_lnedits.append(higher_bound_lnedit) + self.select_color_btns.append(select_color_btn) + self.color_labels.append(color_label) + self.set_color_enabled(i, False) + + self.setup_complete_buttons(TOTAL) + + def connect_signals(self) -> None: + self.include_greater_than_chkbox.clicked.connect( + lambda: self.on_include(TOTAL - 1, + self.include_greater_than_chkbox)) + self.include_less_than_chkbox.clicked.connect( + lambda: self.on_include(0, self.include_less_than_chkbox)) + + for i in range(TOTAL): + self.higher_bound_lnedits[i].textChanged.connect( + lambda arg, idx=i: setattr(self, 'changed_rowid', idx)) + self.higher_bound_lnedits[i].editingFinished.connect( + lambda idx=i: self.on_higher_bound_editing_finished(idx)) + # some how pass False to on_select_color, have to handle this + # in add_row() instead + # self.select_color_btns[i].clicked.connect( + # lambda idx=i: self.on_select_color(idx)) + + super().connect_signals() + + def on_higher_bound_editing_finished(self, rowid: int): + """ + Check value entered for higher bound: + + If the last row is empty string, that value will be eliminated + from condition. + + If value not greater than previous row and less than latter row, + give error message. -20 and 20 are given to previous or latter + row if not exist b/c they are out of range of bound [-10, 10] + Call set_value_rowid_dict to keep track of value by rowid. + + :param rowid: rowid is + """ + if self.changed_rowid is None: + # When the function isn't called from user's changing text on + # a higher_bound_lnedit, this function will be ignored. + return + + if len(self.rowid_value_dict) < self.changed_rowid < TOTAL - 1: + # When user edit the row that skips some row from the last row + # the current higher_bound_lnedit will be cleared + self.higher_bound_lnedits[self.changed_rowid].setText('') + self.changed_rowid = None + return + self.changed_rowid = None + + prev_higher_bound = ( + float(self.higher_bound_lnedits[rowid - 1].text()) + if rowid > 0 else -20) + + try: + curr_higher_bound = float(self.higher_bound_lnedits[rowid].text()) + except ValueError: + # When the current higher_bound_lnedit is cleared. + if rowid < len(self.rowid_value_dict) - 1: + # If the cleared one isn't the last one, it shouldn't be + # allowed. A warning should be given. + msg = "Higher bound must be a number" + QtWidgets.QMessageBox.information(self, "Error", msg) + self.higher_bound_lnedits[rowid].setText( + str(self.rowid_value_dict[rowid])) + else: + # If the cleared one is the last one, the lower_bound_lnedit + # of the next row will be cleared too. + if rowid == 0: + # If the cleared one is the only one, this shouldn't be + # allowed. A warning should be given. + msg = ("There should be at least one value" + " for this plot type.") + QtWidgets.QMessageBox.information(self, "Error", msg) + return + self.set_color_enabled(rowid, False) + self.lower_bound_lnedits[rowid + 1].setText('') + self.set_value_rowid_dict() + self.select_color_btn_clicked = False + self.save_colors_btn_clicked = False + return + if rowid >= len(self.rowid_value_dict) - 1: + # When the current higher_bound_lnedits is on the last row + # set the limit 20 to be the next_higher_bound + next_higher_bound = 20 + else: + next_higher_bound = float( + self.higher_bound_lnedits[rowid + 1].text()) + + if (curr_higher_bound <= prev_higher_bound + or curr_higher_bound >= next_higher_bound): + # If value enter to the current higher_bound_lnedit make it out + # of increasing sorting, a warning will be given, and the + # widget will be set to the original value. + cond = (f"{prev_higher_bound} < " + if prev_higher_bound != -20 else '') + cond += (f"d < {next_higher_bound}" + if next_higher_bound != 20 else 'd') + msg = f"Value entered must be: {cond}" + QtWidgets.QMessageBox.information(self, "Error", msg) + try: + self.higher_bound_lnedits[rowid].setText( + str(self.rowid_value_dict[rowid])) + except KeyError: + # If the current row is the last one, there's no original value + # So the widget will be cleared. + self.higher_bound_lnedits[rowid].setText('') + self.select_color_btn_clicked = False + self.save_colors_btn_clicked = False + return + if ((rowid == 0 and self.include_less_than_chkbox.isChecked()) + or rowid != 0): + # Enable button Select Color unless the row is the first row but + # Include checkbox isn't checked + self.set_color_enabled(rowid, True) + self.lower_bound_lnedits[rowid + 1].setText(str(curr_higher_bound)) + self.set_value_rowid_dict() + if self.save_colors_btn_clicked: + self.on_save_color() + if self.select_color_btn_clicked: + self.on_select_color(rowid) + + def set_value_rowid_dict(self): + """ + Update rowid_value_dict to the current higher bound + Update lower bound of the last row which is for greater than all the + rest to be the max of all higher bound + """ + self.rowid_value_dict = {i: float(self.higher_bound_lnedits[i].text()) + for i in range(TOTAL - 1) + if self.higher_bound_lnedits[i].text() != ''} + last_row_lnedit = (self.lower_bound_lnedits[TOTAL - 1] + if self.upper_equal + else self.higher_bound_lnedits[TOTAL - 1]) + if len(self.rowid_value_dict) == 0: + last_row_lnedit.clear() + else: + last_row_lnedit.setText(str(max(self.rowid_value_dict.values()))) + + def set_color_enabled(self, rowid: int, enabled: bool): + """ + Enable color: allow to edit and display color + Disable color: disallow to edit and hide color + """ + self.select_color_btns[rowid].setEnabled(enabled) + self.color_labels[rowid].setHidden(not enabled) + + def on_select_color(self, rowid: int): + """ + When clicking on Select Color button, Color Picker will pop up with + the default color is color_label's color. + User will select a color then save to update the selected color to + the color_label. + """ + if self.changed_rowid is not None: + self.select_color_btn_clicked = True + self.on_higher_bound_editing_finished(self.changed_rowid) + return + self.select_color_btn_clicked = False + super().on_select_color(self.color_labels[rowid]) + + def on_include(self, rowid: int, chkbox: QtWidgets.QCheckBox): + """ + When include_less_than_chkbox/include_greater_than_chkbox is clicked + decide to allow user select color based on the status of the + check box. + """ + self.set_color_enabled(rowid, chkbox.isChecked()) + # to be able to click on buttons after include checkbox is checked + self.changed_rowid = None + + def set_row(self, vc_idx: int, value: float, color: str): + """ + Add values to widgets in a row + + row 0: consider uncheck include checkbox if color='not plot' and + check otherwise. + + row TOTAL-1: check include checkbox and set value for lower bound + + all row other than TOTAL-1, set value for higher bound and + next row's lower bound, enable and display color + """ + if vc_idx == 0: + if color == 'not plot': + self.include_less_than_chkbox.setChecked(False) + else: + self.include_less_than_chkbox.setChecked(True) + if vc_idx < TOTAL - 1: + self.higher_bound_lnedits[vc_idx].setText(str(value)) + self.lower_bound_lnedits[vc_idx + 1].setText(str(value)) + else: + self.include_greater_than_chkbox.setChecked(True) + if self.upper_equal: + self.lower_bound_lnedits[TOTAL - 1].setText(str(value)) + else: + self.higher_bound_lnedits[TOTAL - 1].setText(str(value)) + if color == 'not plot': + self.set_color_enabled(vc_idx, False) + else: + self.set_color_enabled(vc_idx, True) + display_color(self.color_labels[vc_idx], color) + + def on_save_color(self): + """ + Create value_color_str from GUI's info and close the GUI. + + Skip row that has no color + + color = color_label's color name + + if include_less_than_chkbox is not checked, 'not_plot' will be + set for the first color + + Format for value_color of all row other than TOTAL - 1th: + <=value:color + + If include_greater_than_chkbox is checked, format will be: + value<:color + """ + if self.changed_rowid and self.changed_rowid < TOTAL - 1: + self.save_colors_btn_clicked = True + self.on_higher_bound_editing_finished(self.changed_rowid) + return + self.save_colors_btn_clicked = False + value_color_list = [] + for i in range(TOTAL - 1): + if self.color_labels[i].isHidden() and i != 0: + continue + value = self.higher_bound_lnedits[i].text() + color = self.color_labels[i].palette( + ).window().color().name() + if i == 0 and not self.include_less_than_chkbox.isChecked(): + color = 'not plot' + operator = '<=' if self.upper_equal else '<' + value_color_list.append(f"{operator}{value}:{color}") + if self.include_greater_than_chkbox.isChecked(): + + color = self.color_labels[TOTAL - 1].palette().window( + ).color().name() + if self.upper_equal: + val = f"{self.lower_bound_lnedits[TOTAL - 1].text()}" + value_color_list.append(f"{val}<:{color}") + else: + val = f"{self.higher_bound_lnedits[TOTAL - 1].text()}" + value_color_list.append(f"={val}:{color}") + + self.value_color_str = '|'.join(value_color_list) + self.close() + + +class MultiColorDotLowerEqualDialog(MultiColorDotDialog): + def __init__(self, parent, value_color_str): + super(MultiColorDotLowerEqualDialog, self).__init__( + parent, value_color_str, upper_equal=False) + + def add_row(self, rowid): + """ + Adding a row including Lower bound line dit, comparing operator label, + higher bound line edit, select color button, display color label + """ + lower_bound_lnedit = QtWidgets.QLineEdit() + self.main_layout.addWidget(lower_bound_lnedit, rowid, 1, 1, 1) + lower_bound_lnedit.setReadOnly(True) + if rowid == 0: + lower_bound_lnedit.setHidden(True) + comp_text = " d <" + elif rowid < TOTAL - 1: + lower_bound_lnedit.setEnabled(False) + comp_text = "<= d <" + else: + lower_bound_lnedit.setHidden(True) + comp_text = " d =" + self.main_layout.addWidget(QtWidgets.QLabel(comp_text), rowid, 2, 1, 1) + + higher_bound_lnedit = QtWidgets.QLineEdit() + validator = BoundValidator + + higher_bound_lnedit.setValidator(validator) + self.main_layout.addWidget(higher_bound_lnedit, rowid, 3, 1, 1) + if rowid == TOTAL - 1: + higher_bound_lnedit.setEnabled(False) + + select_color_btn = QtWidgets.QPushButton("Select Color") + # set focus policy to not be clicked by hitting enter in higher bound + select_color_btn.setFocusPolicy(QtCore.Qt.NoFocus) + self.main_layout.addWidget(select_color_btn, rowid, 4, 1, 1) + + color_label = QtWidgets.QLabel() + color_label.setFixedWidth(30) + color_label.setAutoFillBackground(True) + self.main_layout.addWidget(color_label, rowid, 5, 1, 1) + + select_color_btn.clicked.connect( + lambda: self.on_select_color(rowid)) + return (lower_bound_lnedit, higher_bound_lnedit, + select_color_btn, color_label) + + def set_value(self): + """ + Change the corresponding color_labels's color, higher/lower bound, + include check boxes according to value_color_str. + """ + self.include_greater_than_chkbox.setChecked(False) + self.include_less_than_chkbox.setChecked(False) + if self.value_color_str == "": + return + vc_parts = self.value_color_str.split('|') + count = 0 + for vc_str in vc_parts: + value, color = vc_str.split(':') + if value.startswith('<'): + # Ex: <1:#00FFFF + # Ex: <0:not plot (can be on first value only) + value = value.replace('<', '') + self.set_row(count, float(value), color) + count += 1 + else: + # Ex: =1:#FF00FF + value = value.replace('=', '') + self.set_row(TOTAL - 1, float(value), color) + + +class MultiColorDotUpperEqualDialog(MultiColorDotDialog): + def __init__(self, parent, value_color_str): + super(MultiColorDotUpperEqualDialog, self).__init__( + parent, value_color_str, upper_equal=True) + + def add_row(self, rowid): + """ + Adding a row including Lower bound line dit, comparing operator label, + higher bound line edit, select color button, display color label + """ + lower_bound_lnedit = QtWidgets.QLineEdit() + self.main_layout.addWidget(lower_bound_lnedit, rowid, 1, 1, 1) + lower_bound_lnedit.setReadOnly(True) + if rowid == 0: + lower_bound_lnedit.setHidden(True) + comp_text = " d <=" + elif rowid < TOTAL - 1: + lower_bound_lnedit.setEnabled(False) + comp_text = "< d <=" + else: + lower_bound_lnedit.setReadOnly(True) + comp_text = "< d " + self.main_layout.addWidget(QtWidgets.QLabel(comp_text), rowid, 2, 1, 1) + + higher_bound_lnedit = QtWidgets.QLineEdit() + validator = BoundValidator() + higher_bound_lnedit.setValidator(validator) + self.main_layout.addWidget(higher_bound_lnedit, rowid, 3, 1, 1) + if rowid == TOTAL - 1: + higher_bound_lnedit.setHidden(True) + + select_color_btn = QtWidgets.QPushButton("Select Color") + # set focus policy to not be clicked by hitting enter in higher bound + select_color_btn.setFocusPolicy(QtCore.Qt.NoFocus) + self.main_layout.addWidget(select_color_btn, rowid, 4, 1, 1) + + color_label = QtWidgets.QLabel() + color_label.setFixedWidth(30) + color_label.setAutoFillBackground(True) + self.main_layout.addWidget(color_label, rowid, 5, 1, 1) + + select_color_btn.clicked.connect( + lambda: self.on_select_color(rowid)) + return (lower_bound_lnedit, higher_bound_lnedit, + select_color_btn, color_label) + + def set_value(self): + """ + Change the corresponding color_labels's color, higher/lower bound, + include check boxes according to value_color_str. + """ + self.include_greater_than_chkbox.setChecked(False) + self.include_less_than_chkbox.setChecked(False) + if self.value_color_str == "": + return + vc_parts = self.value_color_str.split('|') + count = 0 + for vc_str in vc_parts: + value, color = vc_str.split(':') + if value.startswith('<='): + # Ex: <=1:#00FFFF + # Ex: <=0:not plot (can be on first value only) + value = value.replace('<=', '') + self.set_row(count, float(value), color) + count += 1 + else: + # Ex: 1<:#FF00FF + value = value.replace('<', '') + self.set_row(TOTAL - 1, float(value), color) + + +if __name__ == '__main__': + os_name, version, *_ = platform.platform().split('-') + if os_name == 'macOS': + os.environ['QT_MAC_WANTS_LAYER'] = '1' + app = QtWidgets.QApplication(sys.argv) + # test = MultiColorDotLowerEqualDialog( + # None, '<3:#FF0000|<3.3:#FFFF00|=3.3:#00FF00') + test = MultiColorDotUpperEqualDialog( + None, '<=0:not plot|<=1:#FFFF00|<=2:#00FF00|2<:#FF00FF') + test.exec_() + print("result:", test.value_color_str) + sys.exit(app.exec_()) -- GitLab