diff --git a/src/main/python/main/ayab/ayab.py b/src/main/python/main/ayab/ayab.py index 49ca5e4b..010c0ecc 100644 --- a/src/main/python/main/ayab/ayab.py +++ b/src/main/python/main/ayab/ayab.py @@ -176,9 +176,7 @@ def set_image_dimensions(self) -> None: width, height = self.scene.ayabimage.image.size self.engine.config.update_needles() # in case machine width changed self.engine.config.set_image_dimensions(width, height) - self.progbar.row = self.scene.row_progress + 1 - self.progbar.total = height - self.progbar.refresh() + self.progbar.update(self.engine.status) self.notify( QCoreApplication.translate("Scene", "Image dimensions") + f": {width} x {height}", @@ -191,8 +189,9 @@ def reverse_image(self) -> None: self.scene.reverse() def update_start_row(self, start_row: int) -> None: - self.progbar.update(start_row) self.scene.row_progress = start_row + self.engine.status.current_row = start_row + self.progbar.update(self.engine.status) def notify(self, text: str, log: bool = True) -> None: """Update the notification field.""" diff --git a/src/main/python/main/ayab/engine/control.py b/src/main/python/main/ayab/engine/control.py index 6c9fb694..0f290f1a 100644 --- a/src/main/python/main/ayab/engine/control.py +++ b/src/main/python/main/ayab/engine/control.py @@ -243,6 +243,7 @@ def cnf_line_API6(self, line_number: int) -> bool: return True # pattern finished def __update_status(self, line_number: int, color: int, bits: bitarray) -> None: + self.status.total_rows = self.pat_height self.status.current_row = self.pat_row + 1 self.status.line_number = line_number if self.inf_repeat: diff --git a/src/main/python/main/ayab/engine/engine.py b/src/main/python/main/ayab/engine/engine.py index 3ae3bea2..805d712b 100644 --- a/src/main/python/main/ayab/engine/engine.py +++ b/src/main/python/main/ayab/engine/engine.py @@ -142,9 +142,9 @@ def knit_config(self, image: Image.Image) -> None: self.pattern.alignment = self.config.alignment # update progress bar - self.emit_progress_bar_updater( - self.config.start_row + 1, self.pattern.pat_height, 0, "" - ) + data = Status() + data.copy(self.status) + self.emit_progress_bar_updater(data) # switch to status tab # if self.config.continuous_reporting: @@ -214,9 +214,7 @@ def __handle_status(self) -> None: self.control.midline, self.config.auto_mirror, ) - self.emit_progress_bar_updater( - data.current_row, self.pattern.pat_height, data.repeats, data.color_symbol - ) + self.emit_progress_bar_updater(data) def cancel(self) -> None: self.__canceled = True diff --git a/src/main/python/main/ayab/engine/status.py b/src/main/python/main/ayab/engine/status.py index f8b11c4b..511e5812 100644 --- a/src/main/python/main/ayab/engine/status.py +++ b/src/main/python/main/ayab/engine/status.py @@ -165,6 +165,8 @@ def copy(self, status: Status) -> None: self.carriage_type = status.carriage_type self.carriage_position = status.carriage_position self.carriage_direction = status.carriage_direction + self.total_rows = status.total_rows + self.mirror = status.mirror def parse_device_state_API6(self, state: Any, msg: bytes) -> None: if not (self.active): diff --git a/src/main/python/main/ayab/knitprogress.py b/src/main/python/main/ayab/knitprogress.py index b295b0e3..5fb02fa9 100644 --- a/src/main/python/main/ayab/knitprogress.py +++ b/src/main/python/main/ayab/knitprogress.py @@ -19,12 +19,15 @@ # https://github.com/AllYarnsAreBeautiful/ayab-desktop from __future__ import annotations -from PySide6.QtCore import QCoreApplication, QRect, QSize -from PySide6.QtWidgets import QTableWidget, QTableWidgetItem, QLabel, QHeaderView -from typing import TYPE_CHECKING, Optional, cast +from PySide6.QtCore import QCoreApplication, QRect, Qt +from PySide6.QtWidgets import QTableWidget, QTableWidgetItem, QHeaderView +from PySide6.QtGui import QBrush, QColor +from typing import TYPE_CHECKING, Optional, cast, List +from math import floor if TYPE_CHECKING: from .ayab import GuiMain + from .preferences import Preferences from .engine.status import Status @@ -43,17 +46,12 @@ def __init__(self, parent: GuiMain): super().__init__(parent.ui.graphics_splitter) self.clear() self.setRowCount(0) + self.__prefs: Preferences = parent.prefs self.setGeometry(QRect(0, 0, 700, 220)) self.setContentsMargins(1, 1, 1, 1) self.verticalHeader().setSectionResizeMode( - QHeaderView.ResizeMode.ResizeToContents + QHeaderView.ResizeMode.Fixed ) - self.verticalHeader().setVisible(False) - self.setColumnCount(6) - for r in range(6): - blank = QTableWidgetItem() - blank.setSizeHint(QSize(0, 0)) - self.setHorizontalHeaderItem(r, blank) self.previousStatus: Optional[Status] = None self.scene = parent.scene @@ -61,7 +59,6 @@ def start(self) -> None: self.clearContents() self.clearSelection() self.setRowCount(0) - self.horizontalHeader().setSectionHidden(5, False) self.setCurrentCell(-1, -1) self.color = True @@ -95,83 +92,129 @@ def update_progress( if status.current_row < 0: return - # else - tr_ = QCoreApplication.translate - row, swipe = divmod(status.line_number, row_multiplier) - - columns = [] - - # row - columns.append(tr_("KnitProgress", "Row") + " " + str(status.current_row)) - # pass - columns.append(tr_("KnitProgress", "Pass") + " " + str(swipe + 1)) - - # color + columns: List[QTableWidgetItem] = [] if status.color_symbol == "": self.color = False else: self.color = True - coltext = tr_("KnitProgress", "Color") + " " + status.color_symbol - columns.append(coltext) - carriage = status.carriage_type - direction = status.carriage_direction - columns.append(carriage.symbol + " " + direction.symbol) + midline = self.load_columns_from_status(status, midline, columns) - # graph line of stitches - midline = len(status.bits) - midline - - table_text = ( - "" - ) - for c in range(0, midline): - table_text += self.__stitch( - status.color, cast(bool, status.bits[c]), status.alt_color - ) - table_text += "
" - left_side = QLabel(table_text) - - table_text = ( - "" - ) - for c in range(midline, len(status.bits)): - table_text += self.__stitch( - status.color, cast(bool, status.bits[c]), status.alt_color - ) - table_text += "
" - right_side = QLabel(table_text) + # For the top row (row idx 0), we show the row header as "To Be Selected", + # When we show a new row, we recover the header info and recombine it with its row (now row idx 2) + self.make_row_with_spacer() - self.insertRow(0) - for i, col in enumerate(columns): - self.setItem(0, i, self.__item(col)) + self.instantiate_row_from_columns(midline, columns) + if self.columnCount() != len(columns): + self.setColumnCount(len(columns)) n_cols = len(columns) - self.setCellWidget(0, n_cols, left_side) - self.setCellWidget(0, n_cols + 1, right_side) - if row_multiplier == 1: - self.hideColumn(1) if n_cols < 4: self.hideColumn(5) - self.resizeColumnsToContents() self.previousStatus = status + self.previous_row_mulitplier = row_multiplier # update bar in Scene self.scene.row_progress = status.current_row - def __item(self, text: str) -> QTableWidgetItem: - item = QTableWidgetItem(text) - return item + def load_columns_from_status(self, status: Status, midline: int, columns: List[QTableWidgetItem]) -> int: + midline = len(status.bits) - midline + + for c in range(0, midline): + columns.append(self.__stitch( + status.color, cast(bool, status.bits[c]), status.alt_color, self.__alternate_bg_colors(midline-c, self.orange) + )) + + # if we are only working on the right side, midline is negative. + green_start = midline + if green_start < 0: + green_start = 0 + for c in range(green_start, len(status.bits)): + columns.append(self.__stitch( + status.color, cast(bool, status.bits[c]), status.alt_color, self.__alternate_bg_colors(c-green_start, self.green) + )) + + return midline + + def instantiate_row_from_columns(self, midline: int, columns: List[QTableWidgetItem]) -> None: + self.setVerticalHeaderItem(0, QTableWidgetItem("To Be Selected")) + for i, col in enumerate(columns): + self.setItem(0, i, col) + self.setColumnWidth(i, cast(int, self.__prefs.settings.value("lower_display_stitch_width"))) + # when width is under 20, the column numbers are unreadable. + if self.columnWidth(i) < 20: + self.horizontalHeader().setVisible(False) + continue + self.horizontalHeader().setVisible(True) + if i < midline: + header = QTableWidgetItem(f"{(midline)-(i)}") + header.font().setBold(True) + header.setForeground(QBrush(QColor(f"#{self.orange:06x}"))) + header.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + self.setHorizontalHeaderItem(i, header) + self.horizontalHeader().setMinimumSectionSize(0) + else: + header = QTableWidgetItem(f"{(i+1)-(midline)}") + header.setForeground(QBrush(QColor(f"#{self.green:06x}"))) + header.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + self.setHorizontalHeaderItem(i, header) + + def make_row_with_spacer(self) -> None: + self.removeRow(1) + self.insertRow(0) + self.insertRow(1) + if self.rowCount() > 2: + self.setVerticalHeaderItem(2, self.format_row_header_text(self.previousStatus, self.previous_row_mulitplier)) + self.verticalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed) + self.verticalHeader().setMinimumSectionSize(0) + self.verticalHeader().resizeSection(1, 5) + + def format_row_header_text(self, status: Optional[Status], row_multiplier: int) -> QTableWidgetItem: + if status is None: + return QTableWidgetItem("") + tr_ = QCoreApplication.translate + info_header = QTableWidgetItem() + info_text = "" + row, swipe = divmod(status.line_number, row_multiplier) + # row "Row [1]" + info_text = (tr_("KnitProgress", "Row") + " " + str(status.current_row)) - def __stitch(self, color: int, bit: bool, alt_color: Optional[int] = None) -> str: - # FIXME: borders are not visible - text = "" + if bg_color is not None: + stitch.setBackground(QBrush(bg_color)) + return stitch diff --git a/src/main/python/main/ayab/preferences.py b/src/main/python/main/ayab/preferences.py index 1c6c3a12..d0e032de 100644 --- a/src/main/python/main/ayab/preferences.py +++ b/src/main/python/main/ayab/preferences.py @@ -27,7 +27,7 @@ from __future__ import annotations from PySide6.QtCore import Qt, QSettings, QCoreApplication -from PySide6.QtWidgets import QDialog, QFormLayout, QLabel, QCheckBox, QComboBox +from PySide6.QtWidgets import QDialog, QFormLayout, QLabel, QCheckBox, QComboBox, QSpinBox from .prefs_gui import Ui_Prefs from .signal_sender import SignalSender @@ -67,11 +67,12 @@ def str2bool(qvariant: str | bool) -> bool: "quiet_mode", "disable_hardware_beep", ] +PreferencesDictIntKeys: TypeAlias = Literal["lower_display_stitch_width"] PreferencesDictObjKeys: TypeAlias = Literal[ "aspect_ratio", "default_alignment", "default_knitting_mode", "machine" ] PreferencesDictKeys: TypeAlias = Literal[ - PreferencesDictBoolKeys, PreferencesDictObjKeys, "language" + PreferencesDictBoolKeys, PreferencesDictObjKeys, PreferencesDictIntKeys, "language" ] PreferencesDict = TypedDict( @@ -87,6 +88,7 @@ def str2bool(qvariant: str | bool) -> bool: "quiet_mode": type[bool], "disable_hardware_beep": type[bool], "language": type[Language], + "lower_display_stitch_width": type[int] }, ) @@ -116,13 +118,14 @@ class Preferences(SignalSender): "quiet_mode": bool, "disable_hardware_beep": bool, "language": Language, + "lower_display_stitch_width": int } def __init__(self, parent: GuiMain): super().__init__(parent.signal_receiver) self.parent = parent self.languages = Language(self.parent.app_context) - self.settings = QSettings() + self.settings: QSettings = QSettings() self.settings.setFallbacksEnabled(False) self.refresh() @@ -148,23 +151,23 @@ def value(self, var: PreferencesDictKeys) -> Any: else: return self.default_value(var) - def convert(self, var: PreferencesDictKeys) -> Callable[[T], Any]: + def convert(self, var: PreferencesDictKeys) -> Callable[[object], Any]: try: cls = self.variables[var] except KeyError: return str # else if cls == bool: - return cast(Callable[[T], Any], str2bool) + return cast(Callable[[object], Any], str2bool) # else if cls == Language: return str # else - return cast(Callable[[T], Any], int) + return cast(Callable[[object], Any], int) def default_value( self, var: PreferencesDictKeys - ) -> Optional[bool | str | Literal[0]]: + ) -> Optional[bool | str | int | Literal[0]]: try: cls = self.variables[var] except KeyError: @@ -175,6 +178,8 @@ def default_value( # else if cls == Language: return self.languages.default_language() + if cls == int: + return 20 # else return 0 @@ -226,6 +231,8 @@ def __make_widget(self, var: PreferencesDictKeys) -> PrefsWidgetTypes: cls = self.__prefs.variables[var] if cls == bool: return PrefsBoolWidget(self.__prefs, cast(PreferencesDictBoolKeys, var)) + elif cls == int: + return PrefsIntWidget(self.__prefs, cast(PreferencesDictIntKeys, var)) elif cls == Language: return PrefsLangWidget(self.__prefs) else: @@ -250,7 +257,7 @@ class PrefsBoolWidget(QCheckBox): def __init__(self, prefs: Preferences, var: PreferencesDictBoolKeys): super().__init__() - self.var = var + self.var: PreferencesDictBoolKeys = var self.prefs = prefs def connectChange(self) -> None: @@ -269,6 +276,29 @@ def refresh(self) -> None: self.setCheckState(Qt.CheckState.Unchecked) +class PrefsIntWidget(QSpinBox): + """Spinbox for Integer preferences setting. + + @author Sam Bonfante + @date July 2024 + """ + + def __init__(self, prefs: Preferences, var: PreferencesDictIntKeys): + super().__init__() + self.var: PreferencesDictIntKeys = var + self.prefs = prefs + self.setMinimum(2) + + def connectChange(self) -> None: + self.valueChanged.connect(self.update_setting) + + def update_setting(self, new_value: int) -> None: + self.prefs.settings.setValue(self.var, new_value) + + def refresh(self) -> None: + self.setValue(self.prefs.value(self.var)) + + class PrefsComboWidget(QComboBox): """ComboBox for categorical preferences setting. @@ -278,7 +308,7 @@ class PrefsComboWidget(QComboBox): def __init__(self, prefs: Preferences, var: PreferencesDictObjKeys): super().__init__() - self.var = var + self.var: PreferencesDictObjKeys = var self.prefs = prefs cls = self.prefs.variables[self.var] cls.add_items(self) @@ -315,4 +345,4 @@ def refresh(self) -> None: self.setCurrentIndex(self.findData(self.prefs.value("language"))) -PrefsWidgetTypes: TypeAlias = PrefsBoolWidget | PrefsLangWidget | PrefsComboWidget +PrefsWidgetTypes: TypeAlias = PrefsBoolWidget | PrefsLangWidget | PrefsComboWidget | PrefsIntWidget diff --git a/src/main/python/main/ayab/progressbar.py b/src/main/python/main/ayab/progressbar.py index e45543c6..4d723bb5 100644 --- a/src/main/python/main/ayab/progressbar.py +++ b/src/main/python/main/ayab/progressbar.py @@ -20,10 +20,11 @@ from __future__ import annotations from typing import TYPE_CHECKING +from PySide6.QtGui import QColor if TYPE_CHECKING: from .ayab import GuiMain - from .engine.status import ColorSymbolType + from .engine.status import Status class ProgressBar(object): @@ -40,23 +41,22 @@ def reset(self) -> None: self.total = -1 self.repeats = -1 self.color = "" + self.background_color = 0xFFFFFF self.__row_label.setText("") self.__color_label.setText("") self.__status_label.setText("") def update( self, - row: int, - total: int = 0, - repeats: int = 0, - color_symbol: ColorSymbolType = "", + status: Status ) -> bool: - if row < 0: + if status.current_row < 0: return False - self.row = row - self.total = total - self.repeats = repeats - self.color = color_symbol + self.row = status.current_row + self.total = status.total_rows + self.repeats = status.repeats + self.color = status.color_symbol + self.background_color = status.color self.refresh() return True @@ -69,6 +69,12 @@ def refresh(self) -> None: color_text = "" else: color_text = "Color " + self.color + bg_color = QColor.fromRgb(self.background_color) + if bg_color.lightness() < 128: + fg_color = 0xffffff + else: fg_color = 0x000000 + self.__color_label.setStyleSheet("QLabel {background-color: "+f"#{self.background_color:06x}"+f";color:#{fg_color:06x}"+";}") + self.__color_label.setText(color_text) # Update labels diff --git a/src/main/python/main/ayab/signal_receiver.py b/src/main/python/main/ayab/signal_receiver.py index 187392ef..226645cb 100644 --- a/src/main/python/main/ayab/signal_receiver.py +++ b/src/main/python/main/ayab/signal_receiver.py @@ -43,7 +43,7 @@ class SignalReceiver(QObject): # signals are defined as class attributes which are # over-ridden by instance attributes with the same name start_row_updater = Signal(int) - progress_bar_updater = Signal(int, int, int, str) + progress_bar_updater = Signal(Status) knit_progress_updater = Signal(Status, int, int, bool) notifier = Signal(str, bool) # statusbar_updater = Signal('QString', bool) diff --git a/src/main/python/main/ayab/signal_sender.py b/src/main/python/main/ayab/signal_sender.py index 973eab38..064caf1e 100644 --- a/src/main/python/main/ayab/signal_sender.py +++ b/src/main/python/main/ayab/signal_sender.py @@ -50,10 +50,10 @@ def emit_start_row_updater(self, start_row: int) -> None: self.__signal_receiver.start_row_updater.emit(start_row) def emit_progress_bar_updater( - self, row: int, total: int, repeats: int, color_symbol: ColorSymbolType + self, status: Status ) -> None: self.__signal_receiver.progress_bar_updater.emit( - row, total, repeats, color_symbol + status ) def emit_knit_progress_updater(