|
| 1 | +from PyQt6.QtWidgets import ( |
| 2 | + QFrame, QVBoxLayout, QHBoxLayout, QPushButton, |
| 3 | + QLabel, QSpacerItem, QSizePolicy, QButtonGroup |
| 4 | +) |
| 5 | +from PyQt6.QtCore import ( |
| 6 | + pyqtSignal, Qt, QPropertyAnimation, QParallelAnimationGroup, QEasingCurve, QSize |
| 7 | +) |
| 8 | +from PyQt6.QtGui import QIcon |
| 9 | + |
| 10 | +class SidebarButton(QPushButton): |
| 11 | + """ |
| 12 | + Przycisk w sidebar. |
| 13 | + - Może być checkable |
| 14 | + - Tekst pojawia się po rozwinięciu sidebar |
| 15 | + """ |
| 16 | + def __init__(self, icon=None, text="", parent=None, checkable=False): |
| 17 | + super().__init__(parent) |
| 18 | + self._original_text = text |
| 19 | + self.setObjectName("SidebarButton") |
| 20 | + self.setCheckable(checkable) |
| 21 | + self.setMinimumHeight(30) |
| 22 | + |
| 23 | + if icon is not None: |
| 24 | + self.setIcon(icon) |
| 25 | + self.setIconSize(QSize(26, 26)) |
| 26 | + |
| 27 | + self.show_text(False) |
| 28 | + |
| 29 | + def show_text(self, visible: bool): |
| 30 | + """Włącza/wyłącza oryginalny tekst (obok ikony).""" |
| 31 | + self.setText(self._original_text if visible else "") |
| 32 | + |
| 33 | + |
| 34 | +class CollapsibleSidebar(QFrame): |
| 35 | + """ |
| 36 | + Rozwijany sidebar (50 -> 200 px). |
| 37 | +
|
| 38 | + Parametr 'initial_mode' decyduje, czy mamy tylko |
| 39 | + podstawowe przyciski (InitialSetup) czy pełen zestaw (MainWindow). |
| 40 | + """ |
| 41 | + |
| 42 | + # Sygnały do MainWindow / Initial |
| 43 | + sig_select_lettuce = pyqtSignal() |
| 44 | + sig_select_schej = pyqtSignal() |
| 45 | + sig_load_csv = pyqtSignal() |
| 46 | + sig_export_csv = pyqtSignal() |
| 47 | + sig_export_html = pyqtSignal() |
| 48 | + sig_export_png = pyqtSignal() |
| 49 | + sig_fire_mode = pyqtSignal() |
| 50 | + sig_colorize = pyqtSignal() |
| 51 | + sig_toggle_params = pyqtSignal() |
| 52 | + sig_documentation = pyqtSignal() |
| 53 | + sig_go_initial = pyqtSignal() |
| 54 | + |
| 55 | + def __init__(self, parent=None, initial_mode=False): |
| 56 | + super().__init__(parent) |
| 57 | + self.setObjectName("CollapsibleSidebar") |
| 58 | + |
| 59 | + self._collapsed_width = 50 |
| 60 | + self._expanded_width = 200 |
| 61 | + self._expanded = False |
| 62 | + |
| 63 | + self.lettuce_icon_light = "assets/icons/light/lettuce_light.png" |
| 64 | + self.lettuce_icon_dark = "assets/icons/dark/lettuce_dark.png" |
| 65 | + self.schej_icon_light = "assets/icons/light/schej_light.png" |
| 66 | + self.schej_icon_dark = "assets/icons/dark/schej_dark.png" |
| 67 | + |
| 68 | + self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding) |
| 69 | + self.setMinimumWidth(self._collapsed_width) |
| 70 | + self.setMaximumWidth(self._collapsed_width) |
| 71 | + |
| 72 | + # Layout |
| 73 | + self.main_layout = QVBoxLayout(self) |
| 74 | + self.main_layout.setContentsMargins(0, 0, 0, 0) |
| 75 | + self.main_layout.setSpacing(0) |
| 76 | + |
| 77 | + # --- Pasek górny (toggle + label) --- |
| 78 | + top_widget = QFrame() |
| 79 | + top_layout = QHBoxLayout(top_widget) |
| 80 | + top_layout.setContentsMargins(0, 0, 0, 0) |
| 81 | + top_layout.setSpacing(5) |
| 82 | + |
| 83 | + menu_icon = QIcon("assets/icons/light/menu_light.png") |
| 84 | + self.toggle_btn = QPushButton() |
| 85 | + self.toggle_btn.setObjectName("SidebarToggleBtn") |
| 86 | + self.toggle_btn.setIcon(menu_icon) |
| 87 | + self.toggle_btn.setIconSize(QSize(24, 24)) |
| 88 | + self.toggle_btn.clicked.connect(self.toggle_sidebar) |
| 89 | + top_layout.addWidget(self.toggle_btn, alignment=Qt.AlignmentFlag.AlignLeft) |
| 90 | + |
| 91 | + self.app_label = QLabel("Harmobot") |
| 92 | + self.app_label.setObjectName("SidebarAppLabel") |
| 93 | + self.app_label.setVisible(False) |
| 94 | + top_layout.addWidget(self.app_label, alignment=Qt.AlignmentFlag.AlignVCenter) |
| 95 | + |
| 96 | + self.main_layout.addWidget(top_widget) |
| 97 | + |
| 98 | + # Separator |
| 99 | + sep_top = QFrame() |
| 100 | + sep_top.setFrameShape(QFrame.Shape.HLine) |
| 101 | + sep_top.setFrameShadow(QFrame.Shadow.Sunken) |
| 102 | + self.main_layout.addWidget(sep_top) |
| 103 | + |
| 104 | + # --- Taby Lettuce / Schej --- |
| 105 | + engine_group = QButtonGroup(self) |
| 106 | + engine_group.setExclusive(True) |
| 107 | + |
| 108 | + # Lettuce |
| 109 | + self.btn_lettuce = SidebarButton( |
| 110 | + icon=QIcon(self.lettuce_icon_light), |
| 111 | + text="lettucemeet", |
| 112 | + checkable=True |
| 113 | + ) |
| 114 | + self.btn_lettuce.setChecked(True) |
| 115 | + self.btn_lettuce.clicked.connect(self._on_lettuce_clicked) |
| 116 | + engine_group.addButton(self.btn_lettuce) |
| 117 | + self.main_layout.addWidget(self.btn_lettuce) |
| 118 | + |
| 119 | + # Schej |
| 120 | + self.btn_schej = SidebarButton( |
| 121 | + icon=QIcon(self.schej_icon_light), |
| 122 | + text="schej", |
| 123 | + checkable=True |
| 124 | + ) |
| 125 | + self.btn_schej.setChecked(False) |
| 126 | + self.btn_schej.clicked.connect(self._on_schej_clicked) |
| 127 | + engine_group.addButton(self.btn_schej) |
| 128 | + self.main_layout.addWidget(self.btn_schej) |
| 129 | + |
| 130 | + # Wczytaj CSV |
| 131 | + csv_icon = QIcon("assets/icons/light/load_csv_light.png") |
| 132 | + self.btn_load_csv = SidebarButton(icon=csv_icon, text="Wczytaj CSV", checkable=False) |
| 133 | + self.btn_load_csv.clicked.connect(lambda: self.sig_load_csv.emit()) |
| 134 | + self.main_layout.addWidget(self.btn_load_csv) |
| 135 | + |
| 136 | + # Jeśli main_window => eksporty |
| 137 | + if not initial_mode: |
| 138 | + exp_csv_icon = QIcon("assets/icons/light/export_csv_light.png") |
| 139 | + self.btn_export_csv = SidebarButton(icon=exp_csv_icon, text="Eksport CSV", checkable=False) |
| 140 | + self.btn_export_csv.clicked.connect(lambda: self.sig_export_csv.emit()) |
| 141 | + self.main_layout.addWidget(self.btn_export_csv) |
| 142 | + |
| 143 | + exp_html_icon = QIcon("assets/icons/light/export_html_light.png") |
| 144 | + self.btn_export_html = SidebarButton(icon=exp_html_icon, text="Eksport HTML", checkable=False) |
| 145 | + self.btn_export_html.clicked.connect(lambda: self.sig_export_html.emit()) |
| 146 | + self.main_layout.addWidget(self.btn_export_html) |
| 147 | + |
| 148 | + exp_png_icon = QIcon("assets/icons/light/export_png_light.png") |
| 149 | + self.btn_export_png = SidebarButton(icon=exp_png_icon, text="Eksport PNG", checkable=False) |
| 150 | + self.btn_export_png.clicked.connect(lambda: self.sig_export_png.emit()) |
| 151 | + self.main_layout.addWidget(self.btn_export_png) |
| 152 | + |
| 153 | + # spacer |
| 154 | + self.main_layout.addSpacerItem(QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)) |
| 155 | + |
| 156 | + # FireMode |
| 157 | + fire_icon = QIcon("assets/icons/light/theme_toggle_light.png") |
| 158 | + self.btn_fire_mode = SidebarButton(icon=fire_icon, text="FireMode", checkable=False) |
| 159 | + self.btn_fire_mode.clicked.connect(lambda: self.sig_fire_mode.emit()) |
| 160 | + self.main_layout.addWidget(self.btn_fire_mode) |
| 161 | + |
| 162 | + # Kolor czipów – checkable |
| 163 | + if not initial_mode: |
| 164 | + color_icon = QIcon("assets/icons/light/colorize_chips_light.png") |
| 165 | + self.btn_colorize = SidebarButton(icon=color_icon, text="Kolor czipów", checkable=True) |
| 166 | + self.btn_colorize.setChecked(False) |
| 167 | + self.btn_colorize.clicked.connect(lambda: self.sig_colorize.emit()) |
| 168 | + self.main_layout.addWidget(self.btn_colorize) |
| 169 | + |
| 170 | + # Parametry – checkable |
| 171 | + if not initial_mode: |
| 172 | + params_icon = QIcon("assets/icons/light/parameters_light.png") |
| 173 | + self.btn_params = SidebarButton(icon=params_icon, text="Parametry", checkable=True) |
| 174 | + self.btn_params.setChecked(True) |
| 175 | + self.btn_params.clicked.connect(lambda: self.sig_toggle_params.emit()) |
| 176 | + self.main_layout.addWidget(self.btn_params) |
| 177 | + |
| 178 | + # Separator |
| 179 | + sep_bottom = QFrame() |
| 180 | + sep_bottom.setFrameShape(QFrame.Shape.HLine) |
| 181 | + sep_bottom.setFrameShadow(QFrame.Shadow.Sunken) |
| 182 | + self.main_layout.addWidget(sep_bottom) |
| 183 | + |
| 184 | + # Dokumentacja |
| 185 | + doc_icon = QIcon("assets/icons/light/docs_light.png") |
| 186 | + self.btn_doc = SidebarButton(icon=doc_icon, text="Dokumentacja", checkable=False) |
| 187 | + self.btn_doc.clicked.connect(lambda: self.sig_documentation.emit()) |
| 188 | + self.main_layout.addWidget(self.btn_doc) |
| 189 | + |
| 190 | + # Powrót do initial – only main |
| 191 | + if not initial_mode: |
| 192 | + back_icon = QIcon("assets/icons/light/back_light.png") |
| 193 | + self.btn_go_initial = SidebarButton(icon=back_icon, text="Powrót", checkable=False) |
| 194 | + self.btn_go_initial.clicked.connect(lambda: self.sig_go_initial.emit()) |
| 195 | + self.main_layout.addWidget(self.btn_go_initial) |
| 196 | + |
| 197 | + def _on_lettuce_clicked(self): |
| 198 | + """Zaznacz Lettuce, odznacz Schej, emit sig_select_lettuce.""" |
| 199 | + if not self.btn_lettuce.isChecked(): |
| 200 | + self.btn_lettuce.setChecked(True) |
| 201 | + self.btn_schej.setChecked(False) |
| 202 | + self.sig_select_lettuce.emit() |
| 203 | + |
| 204 | + def _on_schej_clicked(self): |
| 205 | + """Zaznacz Schej, odznacz Lettuce, emit sig_select_schej.""" |
| 206 | + if not self.btn_schej.isChecked(): |
| 207 | + self.btn_schej.setChecked(True) |
| 208 | + self.btn_lettuce.setChecked(False) |
| 209 | + self.sig_select_schej.emit() |
| 210 | + |
| 211 | + def toggle_sidebar(self): |
| 212 | + """ |
| 213 | + Główna metoda rozwijania/zwijania sidebar. |
| 214 | + Faktycznie zmieniamy min/maxWidth z animacją |
| 215 | + i na końcu ustawiamy setFixedWidth. |
| 216 | + """ |
| 217 | + end_width = self._expanded_width if not self._expanded else self._collapsed_width |
| 218 | + self._expanded = not self._expanded |
| 219 | + |
| 220 | + self._anim_min = QPropertyAnimation(self, b"minimumWidth") |
| 221 | + self._anim_min.setDuration(250) |
| 222 | + self._anim_min.setEasingCurve(QEasingCurve.Type.InOutQuad) |
| 223 | + self._anim_min.setStartValue(self.width()) |
| 224 | + self._anim_min.setEndValue(end_width) |
| 225 | + |
| 226 | + self._anim_max = QPropertyAnimation(self, b"maximumWidth") |
| 227 | + self._anim_max.setDuration(250) |
| 228 | + self._anim_max.setEasingCurve(QEasingCurve.Type.InOutQuad) |
| 229 | + self._anim_max.setStartValue(self.width()) |
| 230 | + self._anim_max.setEndValue(end_width) |
| 231 | + |
| 232 | + self._anim_group = QParallelAnimationGroup(self) |
| 233 | + self._anim_group.addAnimation(self._anim_min) |
| 234 | + self._anim_group.addAnimation(self._anim_max) |
| 235 | + self._anim_group.finished.connect(self._on_animation_finished) |
| 236 | + self._anim_group.start() |
| 237 | + |
| 238 | + self.app_label.setVisible(self._expanded) |
| 239 | + for attr_name in [ |
| 240 | + "btn_lettuce", "btn_schej", "btn_load_csv", |
| 241 | + "btn_export_csv", "btn_export_html", "btn_export_png", |
| 242 | + "btn_fire_mode", "btn_colorize", "btn_params", |
| 243 | + "btn_doc", "btn_go_initial" |
| 244 | + ]: |
| 245 | + btn = getattr(self, attr_name, None) |
| 246 | + if btn is not None: |
| 247 | + btn.show_text(self._expanded) |
| 248 | + |
| 249 | + def _on_animation_finished(self): |
| 250 | + """ |
| 251 | + Po zakończeniu animacji wymuszamy 'setFixedWidth', |
| 252 | + co w praktyce zapewnia, że layout rodzica nie narzuci |
| 253 | + ponownie starej szerokości. |
| 254 | + """ |
| 255 | + final_w = self._expanded_width if self._expanded else self._collapsed_width |
| 256 | + self.setFixedWidth(final_w) |
| 257 | + |
| 258 | + def disable_api_tabs(self, disabled: bool): |
| 259 | + """Blokuje klikanie w Lettuce i Schej.""" |
| 260 | + if hasattr(self, "btn_lettuce"): |
| 261 | + self.btn_lettuce.setEnabled(not disabled) |
| 262 | + if hasattr(self, "btn_schej"): |
| 263 | + self.btn_schej.setEnabled(not disabled) |
| 264 | + |
| 265 | + def set_dark_mode_icon(self, dark: bool): |
| 266 | + """ |
| 267 | + Przełącza WYŁĄCZNIE ikony Lettuce i Schej między wersją light i dark. |
| 268 | + Reszta przycisków pozostaje bez zmian. |
| 269 | + """ |
| 270 | + if dark: |
| 271 | + self.btn_lettuce.setIcon(QIcon(self.lettuce_icon_dark)) |
| 272 | + self.btn_schej.setIcon(QIcon(self.schej_icon_dark)) |
| 273 | + else: |
| 274 | + self.btn_lettuce.setIcon(QIcon(self.lettuce_icon_light)) |
| 275 | + self.btn_schej.setIcon(QIcon(self.schej_icon_light)) |
0 commit comments