creatumlibre.ui.dialogs.color_adjustment_dialog

  1from PyQt6.QtCore import Qt
  2from PyQt6.QtWidgets import (
  3    QDialog,
  4    QHBoxLayout,
  5    QLabel,
  6    QPushButton,
  7    QSlider,
  8    QVBoxLayout,
  9)
 10
 11from creatumlibre.graphics.filters.enhancers import (
 12    adjust_brightness,
 13    adjust_contrast,
 14    adjust_rgb,
 15    adjust_saturation,
 16)
 17from creatumlibre.ui.dialogs.color_adjustment_dialog_css import (
 18    BTN_APPLY,
 19    BTN_CANCEL,
 20    MAIN_DIALOG,
 21)
 22
 23
 24class ColorAdjustmentDialog(QDialog):
 25    """Dialog for adjusting brightness, saturation, contrast, and color balance."""
 26
 27    def __init__(self, parent, tab_manager):
 28        super().__init__(parent)
 29        self.tab_manager = tab_manager  # Reference to the tab manager
 30        self.apply_button = QPushButton("Apply")
 31        self.cancel_button = QPushButton("Cancel")
 32        self.sliders = {}
 33        self.labels = {}
 34        self.base_image_snapshot = None  # unchanged image
 35
 36        self._init_layout()
 37
 38    def _init_layout(self):
 39
 40        self.setWindowTitle("Color Adjustments")
 41        self.setFixedSize(300, 480)
 42        self.setStyleSheet(MAIN_DIALOG)
 43
 44        button_layout = QHBoxLayout()
 45
 46        self.apply_button.setStyleSheet(BTN_APPLY)
 47        self.apply_button.clicked.connect(self.apply_changes)
 48        button_layout.addWidget(self.apply_button)
 49
 50        self.cancel_button.setStyleSheet(BTN_CANCEL)
 51        self.cancel_button.clicked.connect(self.cancel_changes)
 52        button_layout.addWidget(self.cancel_button)
 53
 54        self.slider_settings = {
 55            "Brightness": {
 56                "default": 0,
 57                "range": (-100, 100),
 58                "tick_interval": 10,
 59                "scale_function": lambda value: 1 + (value / 100 * 1),
 60                "apply_function": adjust_brightness,
 61            },
 62            "Saturation": {
 63                "default": 0,
 64                "range": (-100, 100),
 65                "tick_interval": 10,
 66                "scale_function": lambda value: 1 + (value / 100 * 2),
 67                "apply_function": adjust_saturation,
 68            },
 69            "Contrast": {
 70                "default": 0,
 71                "range": (-100, 100),
 72                "tick_interval": 10,
 73                "scale_function": lambda value: 1 + (value / 100 * 1.0),
 74                "apply_function": adjust_contrast,
 75            },
 76            "Red": {
 77                "default": 0,
 78                "range": (-100, 100),
 79                "tick_interval": 10,
 80                "scale_function": lambda value: value / 100,
 81                "apply_function": lambda img, value: adjust_rgb(img, value, "Red"),
 82            },
 83            "Green": {
 84                "default": 0,
 85                "range": (-100, 100),
 86                "tick_interval": 10,
 87                "scale_function": lambda value: value / 100,
 88                "apply_function": lambda img, value: adjust_rgb(img, value, "Green"),
 89            },
 90            "Blue": {
 91                "default": 0,
 92                "range": (-100, 100),
 93                "tick_interval": 10,
 94                "scale_function": lambda value: value / 100,
 95                "apply_function": lambda img, value: adjust_rgb(img, value, "Blue"),
 96            },
 97        }
 98
 99        dialog_layout = QVBoxLayout(self)
100
101        for adjustment, config in self.slider_settings.items():
102            label = QLabel(
103                adjustment
104            )  #  This will be replaced with translation support later
105            self.labels[adjustment] = label
106            slider = QSlider(Qt.Orientation.Horizontal)
107            slider.setTickPosition(QSlider.TickPosition.TicksBelow)
108            slider.setTickInterval(config["tick_interval"])
109            slider.setRange(*config["range"])  #  Uses dict values
110            slider.setValue(config["default"])
111            self.labels[adjustment].setText(f"{adjustment} [{slider.value():.2f}]")
112
113            slider.valueChanged.connect(self.apply_all_adjustments)
114
115            dialog_layout.addWidget(label)
116            dialog_layout.addWidget(slider)
117            self.sliders[adjustment] = slider
118
119        dialog_layout.addLayout(button_layout)  # add buttons
120        self.setLayout(dialog_layout)
121
122    def showEvent(self, event):
123        """Runs logic when the dialog is shown."""
124        super().showEvent(event)
125
126        promoted = self.get_promoted_object()
127        if promoted:
128            self.base_image_snapshot = promoted.get_image().copy()
129            self.apply_all_adjustments()
130
131            return  # Already has a promoted object
132
133        # Promote base image if no selection is active
134        active_tab = self.tab_manager.get_active_tab()
135        if not active_tab:
136            return
137
138        manager = active_tab.get("manager")
139        base_object = manager.get_base_object()
140
141        if base_object:
142            # Promote full image as fallback
143            height, width = base_object.get_image().shape[:2]
144            fake_selection = (0, 0, width, height)
145
146            # Make RegionManager believe the full image is selected
147            base_object.region_manager.set_bounding_rect(*fake_selection)
148            base_object.region_manager.initialize_mask(base_object.get_image().shape)
149
150            new_obj = base_object.extract_selection_as_new_image()
151            if new_obj:
152                manager.add_object(new_obj)
153                self.base_image_snapshot = new_obj.get_image().copy()
154                self.apply_all_adjustments()
155                self.tab_manager.refresh_tab_display(
156                    self.tab_manager.get_active_tab_index()
157                )
158
159    def apply_all_adjustments(self):
160        promoted = self.get_promoted_object()
161        if promoted is None:
162            return
163
164        image = self.base_image_snapshot.copy()  # Work on a copy for preview
165
166        for name, config in self.slider_settings.items():
167            value = self.sliders[name].value()
168            scale = config["scale_function"](value)
169            image = config["apply_function"](image, scale)
170            mapped_value = config["scale_function"](value)
171            self.labels[name].setText(f"{name} [{mapped_value:.2f}]")
172
173        promoted.set_image(image)
174
175        self.tab_manager.refresh_tab_display(self.tab_manager.get_active_tab_index())
176
177    def get_promoted_object(self):
178        """Finds the promoted object in the current tab's object manager."""
179        active_tab = self.tab_manager.get_active_tab()
180        if not active_tab:
181            return None
182
183        object_manager = active_tab.get("manager")
184        if not object_manager:
185            return None
186
187        for obj in object_manager.object_list:
188            if getattr(obj, "is_promoted", False):
189                return obj
190        return None
191
192    def apply_changes(self):
193        obj_manager = self.tab_manager.get_active_tab().get("manager")
194        obj_manager.merge_selection()
195        self.tab_manager.refresh_tab_display(self.tab_manager.get_active_tab_index())
196        self.accept()
197
198    def cancel_changes(self):
199        """Cancel adjustments, delete the promoted layer"""
200        promoted = self.get_promoted_object()
201        if promoted:
202            active_tab = self.tab_manager.get_active_tab()
203            object_manager = active_tab.get("manager")
204            object_manager.object_list.remove(promoted)
205            self.tab_manager.refresh_tab_display(
206                self.tab_manager.get_active_tab_index()
207            )
208            self.reject()
class ColorAdjustmentDialog(PyQt6.QtWidgets.QDialog):
 25class ColorAdjustmentDialog(QDialog):
 26    """Dialog for adjusting brightness, saturation, contrast, and color balance."""
 27
 28    def __init__(self, parent, tab_manager):
 29        super().__init__(parent)
 30        self.tab_manager = tab_manager  # Reference to the tab manager
 31        self.apply_button = QPushButton("Apply")
 32        self.cancel_button = QPushButton("Cancel")
 33        self.sliders = {}
 34        self.labels = {}
 35        self.base_image_snapshot = None  # unchanged image
 36
 37        self._init_layout()
 38
 39    def _init_layout(self):
 40
 41        self.setWindowTitle("Color Adjustments")
 42        self.setFixedSize(300, 480)
 43        self.setStyleSheet(MAIN_DIALOG)
 44
 45        button_layout = QHBoxLayout()
 46
 47        self.apply_button.setStyleSheet(BTN_APPLY)
 48        self.apply_button.clicked.connect(self.apply_changes)
 49        button_layout.addWidget(self.apply_button)
 50
 51        self.cancel_button.setStyleSheet(BTN_CANCEL)
 52        self.cancel_button.clicked.connect(self.cancel_changes)
 53        button_layout.addWidget(self.cancel_button)
 54
 55        self.slider_settings = {
 56            "Brightness": {
 57                "default": 0,
 58                "range": (-100, 100),
 59                "tick_interval": 10,
 60                "scale_function": lambda value: 1 + (value / 100 * 1),
 61                "apply_function": adjust_brightness,
 62            },
 63            "Saturation": {
 64                "default": 0,
 65                "range": (-100, 100),
 66                "tick_interval": 10,
 67                "scale_function": lambda value: 1 + (value / 100 * 2),
 68                "apply_function": adjust_saturation,
 69            },
 70            "Contrast": {
 71                "default": 0,
 72                "range": (-100, 100),
 73                "tick_interval": 10,
 74                "scale_function": lambda value: 1 + (value / 100 * 1.0),
 75                "apply_function": adjust_contrast,
 76            },
 77            "Red": {
 78                "default": 0,
 79                "range": (-100, 100),
 80                "tick_interval": 10,
 81                "scale_function": lambda value: value / 100,
 82                "apply_function": lambda img, value: adjust_rgb(img, value, "Red"),
 83            },
 84            "Green": {
 85                "default": 0,
 86                "range": (-100, 100),
 87                "tick_interval": 10,
 88                "scale_function": lambda value: value / 100,
 89                "apply_function": lambda img, value: adjust_rgb(img, value, "Green"),
 90            },
 91            "Blue": {
 92                "default": 0,
 93                "range": (-100, 100),
 94                "tick_interval": 10,
 95                "scale_function": lambda value: value / 100,
 96                "apply_function": lambda img, value: adjust_rgb(img, value, "Blue"),
 97            },
 98        }
 99
100        dialog_layout = QVBoxLayout(self)
101
102        for adjustment, config in self.slider_settings.items():
103            label = QLabel(
104                adjustment
105            )  #  This will be replaced with translation support later
106            self.labels[adjustment] = label
107            slider = QSlider(Qt.Orientation.Horizontal)
108            slider.setTickPosition(QSlider.TickPosition.TicksBelow)
109            slider.setTickInterval(config["tick_interval"])
110            slider.setRange(*config["range"])  #  Uses dict values
111            slider.setValue(config["default"])
112            self.labels[adjustment].setText(f"{adjustment} [{slider.value():.2f}]")
113
114            slider.valueChanged.connect(self.apply_all_adjustments)
115
116            dialog_layout.addWidget(label)
117            dialog_layout.addWidget(slider)
118            self.sliders[adjustment] = slider
119
120        dialog_layout.addLayout(button_layout)  # add buttons
121        self.setLayout(dialog_layout)
122
123    def showEvent(self, event):
124        """Runs logic when the dialog is shown."""
125        super().showEvent(event)
126
127        promoted = self.get_promoted_object()
128        if promoted:
129            self.base_image_snapshot = promoted.get_image().copy()
130            self.apply_all_adjustments()
131
132            return  # Already has a promoted object
133
134        # Promote base image if no selection is active
135        active_tab = self.tab_manager.get_active_tab()
136        if not active_tab:
137            return
138
139        manager = active_tab.get("manager")
140        base_object = manager.get_base_object()
141
142        if base_object:
143            # Promote full image as fallback
144            height, width = base_object.get_image().shape[:2]
145            fake_selection = (0, 0, width, height)
146
147            # Make RegionManager believe the full image is selected
148            base_object.region_manager.set_bounding_rect(*fake_selection)
149            base_object.region_manager.initialize_mask(base_object.get_image().shape)
150
151            new_obj = base_object.extract_selection_as_new_image()
152            if new_obj:
153                manager.add_object(new_obj)
154                self.base_image_snapshot = new_obj.get_image().copy()
155                self.apply_all_adjustments()
156                self.tab_manager.refresh_tab_display(
157                    self.tab_manager.get_active_tab_index()
158                )
159
160    def apply_all_adjustments(self):
161        promoted = self.get_promoted_object()
162        if promoted is None:
163            return
164
165        image = self.base_image_snapshot.copy()  # Work on a copy for preview
166
167        for name, config in self.slider_settings.items():
168            value = self.sliders[name].value()
169            scale = config["scale_function"](value)
170            image = config["apply_function"](image, scale)
171            mapped_value = config["scale_function"](value)
172            self.labels[name].setText(f"{name} [{mapped_value:.2f}]")
173
174        promoted.set_image(image)
175
176        self.tab_manager.refresh_tab_display(self.tab_manager.get_active_tab_index())
177
178    def get_promoted_object(self):
179        """Finds the promoted object in the current tab's object manager."""
180        active_tab = self.tab_manager.get_active_tab()
181        if not active_tab:
182            return None
183
184        object_manager = active_tab.get("manager")
185        if not object_manager:
186            return None
187
188        for obj in object_manager.object_list:
189            if getattr(obj, "is_promoted", False):
190                return obj
191        return None
192
193    def apply_changes(self):
194        obj_manager = self.tab_manager.get_active_tab().get("manager")
195        obj_manager.merge_selection()
196        self.tab_manager.refresh_tab_display(self.tab_manager.get_active_tab_index())
197        self.accept()
198
199    def cancel_changes(self):
200        """Cancel adjustments, delete the promoted layer"""
201        promoted = self.get_promoted_object()
202        if promoted:
203            active_tab = self.tab_manager.get_active_tab()
204            object_manager = active_tab.get("manager")
205            object_manager.object_list.remove(promoted)
206            self.tab_manager.refresh_tab_display(
207                self.tab_manager.get_active_tab_index()
208            )
209            self.reject()

Dialog for adjusting brightness, saturation, contrast, and color balance.

ColorAdjustmentDialog(parent, tab_manager)
28    def __init__(self, parent, tab_manager):
29        super().__init__(parent)
30        self.tab_manager = tab_manager  # Reference to the tab manager
31        self.apply_button = QPushButton("Apply")
32        self.cancel_button = QPushButton("Cancel")
33        self.sliders = {}
34        self.labels = {}
35        self.base_image_snapshot = None  # unchanged image
36
37        self._init_layout()
tab_manager
apply_button
cancel_button
sliders
labels
base_image_snapshot
def showEvent(self, event):
123    def showEvent(self, event):
124        """Runs logic when the dialog is shown."""
125        super().showEvent(event)
126
127        promoted = self.get_promoted_object()
128        if promoted:
129            self.base_image_snapshot = promoted.get_image().copy()
130            self.apply_all_adjustments()
131
132            return  # Already has a promoted object
133
134        # Promote base image if no selection is active
135        active_tab = self.tab_manager.get_active_tab()
136        if not active_tab:
137            return
138
139        manager = active_tab.get("manager")
140        base_object = manager.get_base_object()
141
142        if base_object:
143            # Promote full image as fallback
144            height, width = base_object.get_image().shape[:2]
145            fake_selection = (0, 0, width, height)
146
147            # Make RegionManager believe the full image is selected
148            base_object.region_manager.set_bounding_rect(*fake_selection)
149            base_object.region_manager.initialize_mask(base_object.get_image().shape)
150
151            new_obj = base_object.extract_selection_as_new_image()
152            if new_obj:
153                manager.add_object(new_obj)
154                self.base_image_snapshot = new_obj.get_image().copy()
155                self.apply_all_adjustments()
156                self.tab_manager.refresh_tab_display(
157                    self.tab_manager.get_active_tab_index()
158                )

Runs logic when the dialog is shown.

def apply_all_adjustments(self):
160    def apply_all_adjustments(self):
161        promoted = self.get_promoted_object()
162        if promoted is None:
163            return
164
165        image = self.base_image_snapshot.copy()  # Work on a copy for preview
166
167        for name, config in self.slider_settings.items():
168            value = self.sliders[name].value()
169            scale = config["scale_function"](value)
170            image = config["apply_function"](image, scale)
171            mapped_value = config["scale_function"](value)
172            self.labels[name].setText(f"{name} [{mapped_value:.2f}]")
173
174        promoted.set_image(image)
175
176        self.tab_manager.refresh_tab_display(self.tab_manager.get_active_tab_index())
def get_promoted_object(self):
178    def get_promoted_object(self):
179        """Finds the promoted object in the current tab's object manager."""
180        active_tab = self.tab_manager.get_active_tab()
181        if not active_tab:
182            return None
183
184        object_manager = active_tab.get("manager")
185        if not object_manager:
186            return None
187
188        for obj in object_manager.object_list:
189            if getattr(obj, "is_promoted", False):
190                return obj
191        return None

Finds the promoted object in the current tab's object manager.

def apply_changes(self):
193    def apply_changes(self):
194        obj_manager = self.tab_manager.get_active_tab().get("manager")
195        obj_manager.merge_selection()
196        self.tab_manager.refresh_tab_display(self.tab_manager.get_active_tab_index())
197        self.accept()
def cancel_changes(self):
199    def cancel_changes(self):
200        """Cancel adjustments, delete the promoted layer"""
201        promoted = self.get_promoted_object()
202        if promoted:
203            active_tab = self.tab_manager.get_active_tab()
204            object_manager = active_tab.get("manager")
205            object_manager.object_list.remove(promoted)
206            self.tab_manager.refresh_tab_display(
207                self.tab_manager.get_active_tab_index()
208            )
209            self.reject()

Cancel adjustments, delete the promoted layer