diff --git a/i18n.py b/i18n.py index 763d833..5d231ce 100644 --- a/i18n.py +++ b/i18n.py @@ -208,30 +208,25 @@ "de": "Bild laden (oder per Drag & Drop)", "es": "Cargar imagen (o arrastrar y soltar)", }, "img.delete": { - "fr": "🗑 Supprimer l'image", "en": "🗑 Delete Image", - "de": "🗑 Bild löschen", "es": "🗑 Eliminar imagen", + "fr": "Supprimer l'image", "en": "Delete Image", + "de": "Bild löschen", "es": "Eliminar imagen", }, - "img.rotation": {"fr": "Rotation:", "en": "Rotation:", "de": "Drehung:", "es": "Rotación:"}, "img.rotate_left": { - "fr": "↶ Pivoter 90°", "en": "↶ Rotate 90°", - "de": "↶ Um 90° drehen", "es": "↶ Girar 90°", + "fr": "Pivoter l'image de 90° vers la gauche", "en": "Rotate Image 90° Left", + "de": "Bild um 90° nach links drehen", "es": "Girar la imagen 90° a la izquierda", }, "img.rotate_right": { - "fr": "Pivoter 90° ↷", "en": "Rotate 90° ↷", - "de": "Um 90° drehen ↷", "es": "Girar 90° ↷", + "fr": "Pivoter l'image de 90° vers la droite", "en": "Rotate Image 90° Right", + "de": "Bild um 90° nach rechts drehen", "es": "Girar la imagen 90° a la derecha", }, "img.mirror_h": { - "fr": "Miroir Horizontal", "en": "Horizontal Mirror", + "fr": "Miroir horizontal", "en": "Horizontal Mirror", "de": "Horizontal spiegeln", "es": "Espejo horizontal", }, "img.mirror_v": { - "fr": "Miroir Vertical", "en": "Vertical Mirror", + "fr": "Miroir vertical", "en": "Vertical Mirror", "de": "Vertikal spiegeln", "es": "Espejo vertical", }, - "img.mirror_mode": { - "fr": "Mode Miroir:", "en": "Mirror Mode:", - "de": "Spiegelmodus:", "es": "Modo espejo:", - }, "img.invert": { "fr": "Inverser Couleurs (Négatif)", "en": "Invert Colors (Negative)", "de": "Farben invertieren (Negativ)", "es": "Invertir colores (Negativo)", diff --git a/image_processing_mixin.py b/image_processing_mixin.py index 8cefd0d..5139271 100644 --- a/image_processing_mixin.py +++ b/image_processing_mixin.py @@ -21,8 +21,8 @@ def reset_image_settings(self): self.combo_material_bg.setCurrentIndex(self.combo_material_bg.findData("light")) self.combo_material_bg.blockSignals(False) self.chk_invert.setChecked(False) - self.chk_mirror_h.setChecked(False) - self.chk_mirror_v.setChecked(False) + self.action_mirror_h.setChecked(False) + self.action_mirror_v.setChecked(False) def get_material_bg(self): """Matière choisie : "dark" (fond noir) ou "light" (fond clair).""" @@ -291,6 +291,7 @@ def clear_image(self): self.processed_array = None self.view_preview_src.clear_image() self.view_preview_dither.clear_image() + self.update_image_actions_state() self.txt_console.append(tr("image.deleted")) def load_image_from_path(self, path): @@ -357,6 +358,8 @@ def display_source_image(self): img = self.raw_image.copy() qimg = QImage(img.tobytes(), img.width, img.height, img.width, QImage.Format.Format_Grayscale8) self.view_preview_src.set_image(qimg) + # Chargement par fichier, projet ou onglet Séparation : tous passent par ici. + self.update_image_actions_state() def _capture_image_settings_snapshot(self): """État des réglages de tramage proprement dits (pas les dimensions @@ -369,8 +372,8 @@ def _capture_image_settings_snapshot(self): "sharp": self.slider_sharp.value(), "invert": self.chk_invert.isChecked(), "material_bg": self.get_material_bg(), - "mirror_h": self.chk_mirror_h.isChecked(), - "mirror_v": self.chk_mirror_v.isChecked(), + "mirror_h": self.action_mirror_h.isChecked(), + "mirror_v": self.action_mirror_v.isChecked(), "algo": self.combo_algo.currentText(), } @@ -387,8 +390,8 @@ def _restore_image_settings_snapshot(self, snap): self.set_material_bg(snap.get("material_bg", "light")) for chk, key in ( (self.chk_invert, "invert"), - (self.chk_mirror_h, "mirror_h"), - (self.chk_mirror_v, "mirror_v"), + (self.action_mirror_h, "mirror_h"), + (self.action_mirror_v, "mirror_v"), ): chk.blockSignals(True) chk.setChecked(snap[key]) @@ -460,8 +463,8 @@ def _do_update_image_processing(self): 'w_mm': self.spin_w.value(), 'h_mm': self.spin_h.value(), 'lmm': self.spin_lmm.value(), - 'mirror_h': self.chk_mirror_h.isChecked(), - 'mirror_v': self.chk_mirror_v.isChecked(), + 'mirror_h': self.action_mirror_h.isChecked(), + 'mirror_v': self.action_mirror_v.isChecked(), 'bright': self.slider_bright.value(), 'contrast': self.slider_contrast.value(), 'gamma': self.slider_gamma.value(), diff --git a/png2svg_mixin.py b/png2svg_mixin.py index b9dad3e..689239e 100644 --- a/png2svg_mixin.py +++ b/png2svg_mixin.py @@ -32,6 +32,7 @@ def _init_png2svg_tab(self): self.png2svg_widget = PngToSvgWidget() self.png2svg_widget.sendToVectorEditor.connect(self._on_png2svg_send_to_vector) + self.png2svg_widget.imageChanged.connect(self.update_image_actions_state) layout.addWidget(self.png2svg_widget) return self.tab_png2svg diff --git a/png2svg_widget.py b/png2svg_widget.py index 2a21fba..807f566 100644 --- a/png2svg_widget.py +++ b/png2svg_widget.py @@ -17,7 +17,7 @@ import tempfile from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QByteArray, QSettings, QRectF -from PyQt6.QtGui import QPixmap, QPainter, QImage, QShortcut, QKeySequence +from PyQt6.QtGui import QPixmap, QPainter, QImage, QShortcut, QKeySequence, QTransform from PyQt6.QtWidgets import ( QApplication, QWidget, QLabel, QPushButton, QSlider, QComboBox, QCheckBox, QSpinBox, QDoubleSpinBox, QVBoxLayout, QHBoxLayout, @@ -64,6 +64,12 @@ def load_svg_bytes(self, svg_bytes: bytes): if was_empty: self.zoom_fit() + def clear(self): + """Vide l'aperçu (même nom que QLabel.clear, utilisé quand QtSvgWidgets manque).""" + self._renderer = QSvgRenderer() + self._svg_item.setSharedRenderer(self._renderer) + self._has_content = False + def zoom_fit(self): if self._scene.sceneRect().isEmpty(): return @@ -88,6 +94,7 @@ class PngToSvgWidget(QWidget): svgGenerated = pyqtSignal(str) # émis à chaque régénération réussie du SVG sendToVectorEditor = pyqtSignal(str) # émis quand l'utilisateur clique "Envoyer vers l'Éditeur Vectoriel" + imageChanged = pyqtSignal() # émis quand une image est chargée ou retirée def __init__(self, parent=None): super().__init__(parent) @@ -440,6 +447,26 @@ def _load_image_path(self, path): ) self.btn_export.setEnabled(True) self._regenerate() + self.imageChanged.emit() + + def rotate_image(self, angle): + """Pivote l'image chargée de `angle` degrés (positif = sens horaire) et relance la + conversion. L'image pivotée passe par un fichier temporaire, comme un collage.""" + if not self.image_path: + return + self._load_image_from_qimage(QImage(self.image_path).transformed(QTransform().rotate(angle))) + + def clear_image(self): + """Retire l'image chargée et l'aperçu SVG.""" + self._debounce.stop() + self.image_path = None + self.last_result = None + self.original_view.clear() + self.svg_view.clear() + self.btn_export.setEnabled(False) + self.btn_send_vector.setEnabled(False) + self.status_label.setText(tr("png2svg.no_image")) + self.imageChanged.emit() def _load_image_from_qimage(self, qimage: QImage): """Reçoit une image déjà en mémoire (glisser-déposer d'une image diff --git a/project_io_mixin.py b/project_io_mixin.py index b0aca84..daf3475 100644 --- a/project_io_mixin.py +++ b/project_io_mixin.py @@ -36,8 +36,8 @@ def _get_project_data(self, encode_image=True): "safe_cut_order": self.chk_safe_cut_order.isChecked(), "invert": self.chk_invert.isChecked(), "material_bg": self.get_material_bg(), - "mirror_h": self.chk_mirror_h.isChecked(), - "mirror_v": self.chk_mirror_v.isChecked(), + "mirror_h": self.action_mirror_h.isChecked(), + "mirror_v": self.action_mirror_v.isChecked(), "algo": self.combo_algo.currentText(), "lmm": self.spin_lmm.value(), "speed_engrave": self.spin_speed_engrave.value(), @@ -130,8 +130,8 @@ def _load_project_from_path(self, path): # Anciens projets sans "material_bg" : déduit du négatif sauvegardé. self.set_material_bg(proj_data.get("material_bg", "dark" if proj_data.get("invert", False) else "light")) self.chk_invert.setChecked(proj_data.get("invert", False)) - self.chk_mirror_h.setChecked(proj_data.get("mirror_h", False)) - self.chk_mirror_v.setChecked(proj_data.get("mirror_v", False)) + self.action_mirror_h.setChecked(proj_data.get("mirror_h", False)) + self.action_mirror_v.setChecked(proj_data.get("mirror_v", False)) self.combo_algo.setCurrentText(proj_data.get("algo", "Floyd-Steinberg")) self.spin_lmm.setValue(proj_data.get("lmm", 10.0)) diff --git a/separation_mixin.py b/separation_mixin.py index 5dd2bd2..1e8c3a5 100644 --- a/separation_mixin.py +++ b/separation_mixin.py @@ -370,6 +370,43 @@ def separation_load_path(self, path): if self.sep_keep_ratio.isChecked(): self._sep_apply_ratio("w") self.separation_push_tone() + self.update_image_actions_state() + + def separation_rotate_image(self, angle): + """Pivote l'image source de `angle` degrés (positif = sens antihoraire, comme PIL) + et échange la largeur et la hauteur de la plaque.""" + if self.sep_src is None: + return + self.sep_src = self.sep_src.rotate(angle, expand=True) + small = self.sep_src.copy() + small.thumbnail((2500, 2500)) + self.sep_src_small = small + self.sep_version += 1 + self._sep_block = True + try: + w_mm, h_mm = self.sep_w.value(), self.sep_h.value() + self.sep_w.setValue(h_mm) + self.sep_h.setValue(w_mm) + finally: + self._sep_block = False + self.separation_push_tone() + + def separation_clear_image(self): + """Retire l'image source et vide les aperçus. L'image déjà envoyée à + Image & Filtres y reste (elle se supprime depuis cet onglet).""" + self.sep_push_timer.stop() + self.sep_preview_timer.stop() + self.sep_src = None + self.sep_src_small = None + self.sep_path = "" + self.sep_version += 1 + self._sep_cache = None + self._sep_stale = set() + self.sep_lbl_file.setText(tr("sep.no_image_loaded")) + for view in self.sep_views: + view.clear_image() + self._sep_view_sizes = {} + self.update_image_actions_state() def _sep_apply_glass_settings(self): """Applique matière « fond noir » + négatif + miroir horizontal. @@ -380,13 +417,13 @@ def _sep_apply_glass_settings(self): self._sep_prev_image_settings = { "material": self.get_material_bg() if hasattr(self, "get_material_bg") else None, "invert": self.chk_invert.isChecked(), - "mirror_h": self.chk_mirror_h.isChecked(), + "mirror_h": self.action_mirror_h.isChecked(), } self._sep_glass_settings_active = True if hasattr(self, "set_material_bg"): self.set_material_bg("dark") # ne touche pas à la case, réglée juste après self.chk_invert.setChecked(True) - self.chk_mirror_h.setChecked(True) + self.action_mirror_h.setChecked(True) def separation_release_image_settings(self): """Appelé quand une image est chargée côté Images Tramage : rend les @@ -401,7 +438,7 @@ def separation_release_image_settings(self): if prev.get("material") and hasattr(self, "set_material_bg"): self.set_material_bg(prev["material"]) self.chk_invert.setChecked(prev["invert"]) - self.chk_mirror_h.setChecked(prev["mirror_h"]) + self.action_mirror_h.setChecked(prev["mirror_h"]) def separation_push_tone(self): """Envoie la tonalité (canal V, cadrée sur la plaque) à Image & Filtres @@ -524,9 +561,9 @@ def _sep_render_current(self): t_b = time.perf_counter() grav = Image.fromarray(arr).resize((pw, ph), Image.BOX) lum = (255.0 - np.asarray(grav, dtype=np.float32)) / 255.0 - if self.chk_mirror_h.isChecked(): + if self.action_mirror_h.isChecked(): lum = lum[:, ::-1] - if self.chk_mirror_v.isChecked(): + if self.action_mirror_v.isChecked(): lum = lum[::-1, :] lum = np.ascontiguousarray(lum) dpi_prev = pw / (self.sep_w.value() / 25.4) @@ -567,7 +604,7 @@ def separation_gain_test(self): la résolution, la vitesse, la puissance et la machine actuelles. Une fois gravée, on repère la ligne dont les cases passent régulièrement du noir au clair, et on règle le curseur sur la valeur écrite à gauche.""" - mirror = self.chk_mirror_h.isChecked() + mirror = self.action_mirror_h.isChecked() arr, w_mm, h_mm = build_gain_test_array(self.spin_lmm.value(), mirror=mirror) bed_w, bed_h = self.spin_machine_w.value(), self.spin_machine_h.value() if w_mm > bed_w or h_mm > bed_h: diff --git a/tests/test_settings_windows.py b/tests/test_settings_windows.py index 13a4d56..3659c63 100644 --- a/tests/test_settings_windows.py +++ b/tests/test_settings_windows.py @@ -41,6 +41,18 @@ def added_to(menu, method, key): added_to("self.tools_menu", "addAction", "menu.tools.stencil_studio") self.assertNotIn("menu_bar.addAction(", body) + def test_image_actions_are_in_the_toolbar_not_the_image_tab(self): + src = read("ui_setup_mixin.py") + toolbar = src[src.index("def _build_main_toolbar"):src.index("def _on_language_selected")] + self.assertIn("self._on_toolbar_delete_image", toolbar) + self.assertEqual(toolbar.count("self._on_toolbar_rotate_image("), 2) + for name in ("action_mirror_h", "action_mirror_v"): + self.assertIn(f"self.{name} = toolbar.addAction(", toolbar) + # Plus aucun bouton dans l'onglet Image & Filtres : seul l'aiguillage les appelle. + for call in ("self.rotate_image_left", "self.rotate_image_right", "self.clear_image()"): + self.assertEqual(src.count(call), 1, call) + self.assertNotIn("chk_mirror", src) + def test_pages_are_no_longer_tabs_of_the_left_column(self): src = read("ui_setup_mixin.py") self.assertNotIn("tabs.addTab(tab_mach,", src) @@ -115,6 +127,52 @@ def test_all_four_menu_windows_are_wired_to_their_pages(self): self.assertIs(pages["machine_params"], obj.tab_machine_params) self.assertIs(pages["test_matrix"], obj.tab_matrix) + def image_tabs(self): + obj = self.make() + obj._image_actions = (mock.Mock(), mock.Mock(), mock.Mock()) + obj._mirror_actions = (mock.Mock(), mock.Mock()) + obj.tab_previews, obj.tab_png2svg, obj.tab_separation = object(), object(), object() + obj.main_tabs_view = mock.Mock() + obj.raw_image = obj.sep_src = None + obj.png2svg_widget = mock.Mock(image_path=None) + return obj + + def assert_enabled(self, obj, tab, image_actions, mirrors): + obj.main_tabs_view.currentWidget.return_value = tab + obj.update_image_actions_state() + for action in obj._image_actions: + action.setEnabled.assert_called_with(image_actions) + for action in obj._mirror_actions: + action.setEnabled.assert_called_with(mirrors) + + def test_image_actions_need_an_image_on_the_current_tab(self): + obj = self.image_tabs() + for tab in (obj.tab_previews, obj.tab_png2svg, obj.tab_separation, object()): + self.assert_enabled(obj, tab, False, False) + obj.raw_image = "img" + self.assert_enabled(obj, obj.tab_previews, True, True) + self.assert_enabled(obj, obj.tab_separation, False, False) # l'image d'un autre onglet ne compte pas + self.assert_enabled(obj, object(), False, False) + obj.sep_src = "img" + self.assert_enabled(obj, obj.tab_separation, True, True) + obj.png2svg_widget.image_path = "/tmp/a.png" + self.assert_enabled(obj, obj.tab_png2svg, True, False) # pas de miroir de gravure en PNG ⇒ SVG + + def test_toolbar_rotation_targets_the_current_tab(self): + obj = self.image_tabs() + obj.rotate_image_left, obj.rotate_image_right = mock.Mock(), mock.Mock() + obj.separation_rotate_image = mock.Mock() + obj.main_tabs_view.currentWidget.return_value = obj.tab_previews + obj._on_toolbar_rotate_image(clockwise=True) + obj.rotate_image_right.assert_called_once() + obj.main_tabs_view.currentWidget.return_value = obj.tab_png2svg + obj._on_toolbar_rotate_image(clockwise=True) + obj.png2svg_widget.rotate_image.assert_called_once_with(90) + obj.main_tabs_view.currentWidget.return_value = obj.tab_separation + obj._on_toolbar_rotate_image(clockwise=True) + obj.separation_rotate_image.assert_called_once_with(-90) # PIL : positif = antihoraire + obj.rotate_image_left.assert_not_called() + def test_missing_page_is_ignored(self): self.Recorder.created = [] obj = self.make(with_pages=False) diff --git a/ui_setup_mixin.py b/ui_setup_mixin.py index dffc1e0..7199a7c 100644 --- a/ui_setup_mixin.py +++ b/ui_setup_mixin.py @@ -81,6 +81,65 @@ def _update_bottom_panel_visibility(self, *_args): self._bottom_panel_sizes = sizes panel.setVisible(False) + def _image_tab_kind(self): + """Onglet courant s'il affiche une image transformable : "previews" (Images & Tramage), + "png2svg", "separation", ou None pour les autres onglets.""" + tabs = getattr(self, "main_tabs_view", None) + current = tabs.currentWidget() if tabs is not None else None + for kind in ("previews", "png2svg", "separation"): + if current is not None and current is getattr(self, f"tab_{kind}", None): + return kind + return None + + def _image_tab_has_image(self, kind): + if kind == "previews": + return self.raw_image is not None + if kind == "png2svg": + return bool(self.png2svg_widget.image_path) + if kind == "separation": + return self.sep_src is not None + return False + + def update_image_actions_state(self, *_args): + """Active les actions de la barre d'outils sur l'image seulement si l'onglet courant + affiche une image chargée. Les miroirs (miroir de gravure) restent grisés dans PNG ⇒ SVG, + dont la conversion ne les utilise pas. Appelée au changement d'onglet, au chargement et à + la suppression d'une image.""" + if not getattr(self, "_image_actions", ()): + return + kind = self._image_tab_kind() + enabled = self._image_tab_has_image(kind) + for action in self._image_actions: + action.setEnabled(enabled) + for action in self._mirror_actions: + action.setEnabled(enabled and kind != "png2svg") + + def _on_toolbar_rotate_image(self, clockwise): + kind = self._image_tab_kind() + if kind == "previews": + (self.rotate_image_right if clockwise else self.rotate_image_left)() + elif kind == "png2svg": + self.png2svg_widget.rotate_image(90 if clockwise else -90) + elif kind == "separation": + self.separation_rotate_image(-90 if clockwise else 90) + + def _on_toolbar_delete_image(self): + kind = self._image_tab_kind() + if kind == "previews": + self.clear_image() # demande elle-même confirmation + return + if kind not in ("png2svg", "separation"): + return + reply = QMessageBox.question( + self, tr("image.delete_title"), tr("image.delete_question"), + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + if reply != QMessageBox.StandardButton.Yes: + return + if kind == "png2svg": + self.png2svg_widget.clear_image() + else: + self.separation_clear_image() + def save_splitter_sizes(self, *_args): settings = QSettings("LaserStudioPro", "UIConfig") settings.setValue("main_splitter_sizes", self.main_splitter.sizes()) @@ -397,7 +456,8 @@ def std_icon(theme_icon, fallback=None): def _build_main_toolbar(self): """Barre d'outils sous le menu : raccourcis vers les entrées des menus Fichier, Paramètres et - Outils. Elle réutilise les mêmes QAction (icône, libellé, raccourci et action identiques).""" + Outils (mêmes QAction : icône, libellé, raccourci et action identiques), puis les actions + sur l'image chargée.""" toolbar = QToolBar(tr("toolbar.main"), self) toolbar.setObjectName("main_toolbar") toolbar.setMovable(False) @@ -412,6 +472,33 @@ def _build_main_toolbar(self): toolbar.addSeparator() for action in self.tools_menu.actions(): toolbar.addAction(action) + toolbar.addSeparator() + # Image chargée (anciens boutons et cases de l'onglet Image & Filtres) : suppression, + # rotation, puis miroirs. Les miroirs sont des actions cochables qui gardent l'état. + # Suppression et rotation agissent sur l'image de l'onglet affiché (_image_tab_kind). + action_delete = toolbar.addAction( + QIcon.fromTheme(QIcon.ThemeIcon.EditDelete, + self.style().standardIcon(QStyle.StandardPixmap.SP_TrashIcon)), + tr("img.delete"), self._on_toolbar_delete_image) + action_rotate_left = toolbar.addAction(QIcon.fromTheme(QIcon.ThemeIcon.ObjectRotateLeft), + tr("img.rotate_left"), + lambda: self._on_toolbar_rotate_image(clockwise=False)) + action_rotate_right = toolbar.addAction(QIcon.fromTheme(QIcon.ThemeIcon.ObjectRotateRight), + tr("img.rotate_right"), + lambda: self._on_toolbar_rotate_image(clockwise=True)) + # Pas d'équivalent QIcon.ThemeIcon pour les miroirs : nom d'icône freedesktop. + flip_h_icon = QIcon.fromTheme("object-flip-horizontal") + flip_v_icon = QIcon.fromTheme("object-flip-vertical") + self.action_mirror_h = toolbar.addAction(flip_h_icon, tr("img.mirror_h")) + self.action_mirror_v = toolbar.addAction(flip_v_icon, tr("img.mirror_v")) + for action in (self.action_mirror_h, self.action_mirror_v): + action.setCheckable(True) + action.toggled.connect(self.update_image_processing) + # Activées seulement avec une image chargée, sur un onglet qui l'affiche (update_image_actions_state). + self._image_actions = (action_delete, action_rotate_left, action_rotate_right) + self._mirror_actions = (self.action_mirror_h, self.action_mirror_v) + for action in self._image_actions + self._mirror_actions: + action.setEnabled(False) self.addToolBar(Qt.ToolBarArea.TopToolBarArea, toolbar) self.main_toolbar = toolbar @@ -611,27 +698,6 @@ def init_ui(self): btn_img.clicked.connect(self.load_image) layout_img.addRow("", btn_img) - btn_clear_img = QPushButton(tr("img.delete")) - btn_clear_img.clicked.connect(self.clear_image) - layout_img.addRow("", btn_clear_img) - - rot_layout = QHBoxLayout() - btn_rot_left = QPushButton(tr("img.rotate_left")) - btn_rot_left.clicked.connect(self.rotate_image_left) - btn_rot_right = QPushButton(tr("img.rotate_right")) - btn_rot_right.clicked.connect(self.rotate_image_right) - rot_layout.addWidget(btn_rot_left) - rot_layout.addWidget(btn_rot_right) - layout_img.addRow(tr("img.rotation"), rot_layout) - - mirror_layout = QHBoxLayout() - self.chk_mirror_h = QCheckBox(tr("img.mirror_h")) - self.chk_mirror_h.stateChanged.connect(self.update_image_processing) - self.chk_mirror_v = QCheckBox(tr("img.mirror_v")) - self.chk_mirror_v.stateChanged.connect(self.update_image_processing) - mirror_layout.addWidget(self.chk_mirror_h) - mirror_layout.addWidget(self.chk_mirror_v) - layout_img.addRow(tr("img.mirror_mode"), mirror_layout) self.slider_bright = QSlider(Qt.Orientation.Horizontal); self.slider_bright.setRange(-100, 100); self.slider_bright.setValue(0) self.slider_bright.valueChanged.connect(self.update_image_processing) @@ -1605,7 +1671,9 @@ def init_ui(self): self.center_bottom_widget = center_bottom_widget self.main_tabs_view.currentChanged.connect(self._update_bottom_panel_visibility) self.main_tabs_view.currentChanged.connect(self._on_main_tab_changed) + self.main_tabs_view.currentChanged.connect(self.update_image_actions_state) self._update_bottom_panel_visibility() + self.update_image_actions_state() splitter.addWidget(left_widget) splitter.addWidget(center_widget)