|
1 | 1 | """
|
2 | 2 | @file
|
3 |
| - @brief A non-modal Qt color picker dialog launcher |
4 |
| - @author FeRD (Frank Dana) <[email protected]> |
| 3 | + @brief A modal Qt color picker dialog launcher, which works in Wayland |
| 4 | + @author Jonathan Thomas <[email protected]> |
5 | 5 |
|
6 | 6 | @section LICENSE
|
7 | 7 |
|
8 |
| - Copyright (c) 2008-2020 OpenShot Studios, LLC |
| 8 | + Copyright (c) 2008-2024 OpenShot Studios, LLC |
9 | 9 | (http://www.openshotstudios.com). This file is part of
|
10 | 10 | OpenShot Video Editor (http://www.openshot.org), an open-source project
|
11 | 11 | dedicated to delivering high quality video editing and animation solutions
|
|
25 | 25 | along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
|
26 | 26 | """
|
27 | 27 |
|
| 28 | +from PyQt5.QtWidgets import QColorDialog, QPushButton, QDialog |
| 29 | +from PyQt5.QtGui import QColor, QPainter, QPen, QCursor |
| 30 | +from PyQt5.QtCore import Qt |
28 | 31 | from classes.logger import log
|
| 32 | +from classes.app import get_app |
| 33 | + |
| 34 | + |
| 35 | +class ColorPicker(QColorDialog): |
| 36 | + def __init__(self, initial_color, parent=None, title=None, callback=None, *args, **kwargs): |
| 37 | + super().__init__(initial_color, parent, *args, **kwargs) |
| 38 | + self.parent_window = parent |
| 39 | + self.callback = callback |
| 40 | + self.picked_pixmap = None |
29 | 41 |
|
30 |
| -from PyQt5.QtCore import Qt, QTimer, QRect, QPoint |
31 |
| -from PyQt5.QtGui import QColor, QBrush, QPen, QPalette, QPainter, QPixmap |
32 |
| -from PyQt5.QtWidgets import QColorDialog, QWidget, QLabel |
33 |
| - |
34 |
| - |
35 |
| -class ColorPicker(QWidget): |
36 |
| - """Show a non-modal color picker. |
37 |
| - QColorDialog.colorSelected(QColor) is emitted when the user picks a color""" |
38 |
| - |
39 |
| - def __init__(self, initial_color: QColor, callback, extra_options=0, |
40 |
| - parent=None, title=None, *args, **kwargs): |
41 |
| - super().__init__(parent=parent, *args, **kwargs) |
42 |
| - self.setObjectName("ColorPicker") |
43 |
| - # Merge any additional user-supplied options with our own |
44 |
| - options = QColorDialog.DontUseNativeDialog |
45 |
| - if extra_options > 0: |
46 |
| - options = options | extra_options |
47 |
| - # Set up non-modal color dialog (to avoid blocking the eyedropper) |
48 |
| - log.debug( |
49 |
| - "Loading QColorDialog with start value %s", |
50 |
| - initial_color.getRgb()) |
51 |
| - self.dialog = CPDialog(initial_color, parent) |
52 |
| - self.dialog.setObjectName("CPDialog") |
53 | 42 | if title:
|
54 |
| - self.dialog.setWindowTitle(title) |
55 |
| - self.dialog.setWindowFlags(Qt.Tool) |
56 |
| - self.dialog.setOptions(options) |
57 |
| - # Avoid signal loops |
58 |
| - self.dialog.blockSignals(True) |
59 |
| - self.dialog.colorSelected.connect(callback) |
60 |
| - self.dialog.finished.connect(self.dialog.deleteLater) |
61 |
| - self.dialog.finished.connect(self.deleteLater) |
62 |
| - self.dialog.setCurrentColor(initial_color) |
63 |
| - self.dialog.blockSignals(False) |
64 |
| - self.dialog.open() |
65 |
| - # Seems to help if this is done AFTER init() returns |
66 |
| - QTimer.singleShot(0, self.add_alpha) |
67 |
| - def add_alpha(self): |
68 |
| - self.dialog.replace_label() |
69 |
| - |
70 |
| - |
71 |
| -class CPDialog(QColorDialog): |
72 |
| - """Show a modified QColorDialog which supports checkerboard alpha""" |
73 |
| - def __init__(self, *args, **kwargs): |
74 |
| - super().__init__(*args, **kwargs) |
75 |
| - log.debug("CPDialog initialized") |
76 |
| - self.alpha_label = CPAlphaShowLabel(self) |
77 |
| - self.alpha_label.setObjectName("alpha_label") |
78 |
| - self.currentColorChanged.connect(self.alpha_label.updateColor) |
79 |
| - def replace_label(self): |
80 |
| - log.debug("Beginning discovery for QColorShowLabel widget") |
81 |
| - # Find the dialog widget used to display the current |
82 |
| - # color, so we can replace it with our implementation |
83 |
| - try: |
84 |
| - qcs = [ |
85 |
| - a for a in self.children() |
86 |
| - if hasattr(a, "metaObject") |
87 |
| - and a.metaObject().className() == 'QColorShower' |
88 |
| - ][0] |
89 |
| - log.debug("Found QColorShower: %s", qcs) |
90 |
| - qcsl = [ |
91 |
| - b for b in qcs.children() |
92 |
| - if hasattr(b, "metaObject") |
93 |
| - and b.metaObject().className() == 'QColorShowLabel' |
94 |
| - ][0] |
95 |
| - log.debug("Found QColorShowLabel: %s", qcsl) |
96 |
| - except IndexError as ex: |
97 |
| - child_list = [ |
98 |
| - a.metaObject().className() |
99 |
| - for a in self.children() |
100 |
| - if hasattr(a, "metaObject") |
101 |
| - ] |
102 |
| - log.debug("%d children of CPDialog %s", len(child_list), child_list) |
103 |
| - raise RuntimeError("Could not find label to replace!") from ex |
104 |
| - qcslay = qcs.layout() |
105 |
| - log.debug( |
106 |
| - "QColorShowLabel found at layout index %d", qcslay.indexOf(qcsl)) |
107 |
| - qcs.layout().replaceWidget(qcsl, self.alpha_label) |
108 |
| - log.debug("Replaced QColorShowLabel widget, hiding original") |
109 |
| - # Make sure it doesn't receive signals while hidden |
110 |
| - qcsl.blockSignals(True) |
111 |
| - qcsl.hide() |
112 |
| - self.alpha_label.show() |
113 |
| - |
114 |
| - |
115 |
| -class CPAlphaShowLabel(QLabel): |
116 |
| - """A replacement for QColorDialog's QColorShowLabel which |
117 |
| - displays the currently-active color using checkerboard alpha""" |
118 |
| - def __init__(self, *args, **kwargs): |
| 43 | + self.setWindowTitle(title) |
| 44 | + |
| 45 | + self.setOption(QColorDialog.DontUseNativeDialog) |
| 46 | + self.colorSelected.connect(self.on_color_selected) |
| 47 | + |
| 48 | + # Override the "Pick Screen Color" button signal |
| 49 | + self._override_pick_screen_color() |
| 50 | + |
| 51 | + # Automatically open the dialog |
| 52 | + self.open() |
| 53 | + |
| 54 | + def _override_pick_screen_color(self): |
| 55 | + # Get first pushbutton (color picker) |
| 56 | + color_picker_button = self.findChildren(QPushButton)[0] |
| 57 | + log.debug(f"Color picker button text: {color_picker_button.text()}") |
| 58 | + |
| 59 | + # Connect to button signals |
| 60 | + color_picker_button.clicked.disconnect() |
| 61 | + color_picker_button.clicked.connect(self.start_color_picking) |
| 62 | + log.debug("Overridden the 'Pick Screen Color' button action") |
| 63 | + |
| 64 | + def start_color_picking(self): |
| 65 | + if self.parent_window: |
| 66 | + self.picked_pixmap = get_app().window.grab() |
| 67 | + self._show_picking_dialog() |
| 68 | + else: |
| 69 | + log.error("No parent window available for color picking") |
| 70 | + |
| 71 | + def _show_picking_dialog(self): |
| 72 | + dialog = PickingDialog(self.picked_pixmap, self) |
| 73 | + dialog.exec_() # Show modal dialog |
| 74 | + self.raise_() |
| 75 | + |
| 76 | + def on_color_selected(self, color): |
| 77 | + log.debug(f"Color selected: {color.name()}") |
| 78 | + if self.callback: |
| 79 | + self.callback(color) |
| 80 | + |
| 81 | +class PickingDialog(QDialog): |
| 82 | + def __init__(self, pixmap, color_picker, *args, **kwargs): |
119 | 83 | super().__init__(*args, **kwargs)
|
120 |
| - # Length in pixels of a side of the checkerboard squares |
121 |
| - # (Pattern is made up of 2x2 squares, total size 2n x 2n) |
122 |
| - self.checkerboard_size = 8 |
123 |
| - # Start out transparent by default |
124 |
| - self.color = super().parent().currentColor() |
125 |
| - self.build_pattern() |
126 |
| - log.debug("CPAlphaShowLabel initialized, creating brush") |
127 |
| - def updateColor(self, color: QColor): |
128 |
| - self.color = color |
129 |
| - log.debug("Label color set to %s", str(color.getRgb())) |
130 |
| - self.repaint() |
131 |
| - def build_pattern(self) -> QPixmap: |
132 |
| - """Construct tileable checkerboard pattern for paint events""" |
133 |
| - # Brush will be an nxn checkerboard pattern |
134 |
| - n = self.checkerboard_size |
135 |
| - pat = QPixmap(2 * n, 2 * n) |
136 |
| - p = QPainter(pat) |
137 |
| - p.setPen(Qt.NoPen) |
138 |
| - # Paint a checkerboard pattern for the color to be overlaid on |
139 |
| - self.bg0 = QColor("#aaa") |
140 |
| - self.bg1 = QColor("#ccc") |
141 |
| - p.fillRect(pat.rect(), self.bg0) |
142 |
| - p.fillRect(QRect(0, 0, n, n), self.bg1) |
143 |
| - p.fillRect(QRect(n, n, 2 * n, 2 * n), self.bg1) |
144 |
| - p.end() |
145 |
| - log.debug("Constructed %s checkerboard brush", pat.size()) |
146 |
| - self.pattern = pat |
| 84 | + self.pixmap = pixmap |
| 85 | + self.device_pixel_ratio = pixmap.devicePixelRatio() |
| 86 | + self.color_picker = color_picker |
| 87 | + self.setWindowModality(Qt.WindowModal) |
| 88 | + self.setGeometry(get_app().window.geometry()) |
| 89 | + self.setFixedSize(self.size()) |
| 90 | + self.setCursor(Qt.CrossCursor) |
| 91 | + self.color_preview = QColor("#FFFFFF") |
| 92 | + self.setMouseTracking(True) |
| 93 | + |
| 94 | + # Get first pushbutton (color picker) |
| 95 | + color_picker_button = self.color_picker.findChildren(QPushButton)[0] |
| 96 | + self.setWindowTitle(f"OpenShot: {color_picker_button.text().replace('&', '')}") |
| 97 | + |
147 | 98 | def paintEvent(self, event):
|
148 |
| - """Show the current color, with checkerboard alpha""" |
149 |
| - event.accept() |
150 |
| - p = QPainter(self) |
151 |
| - p.setPen(Qt.NoPen) |
152 |
| - if self.color.alphaF() < 1.0: |
153 |
| - # Draw a checkerboard pattern under the color |
154 |
| - p.drawTiledPixmap(event.rect(), self.pattern, QPoint(4,4)) |
155 |
| - p.fillRect(event.rect(), self.color) |
156 |
| - p.end() |
| 99 | + painter = QPainter(self) |
| 100 | + painter.drawPixmap(self.rect(), self.pixmap) |
| 101 | + # Draw color preview rectangle near the cursor |
| 102 | + if self.color_preview: |
| 103 | + pen = QPen(Qt.black, 2) |
| 104 | + painter.setPen(pen) |
| 105 | + painter.setBrush(self.color_preview) |
| 106 | + cursor_pos = self.mapFromGlobal(QCursor.pos()) |
| 107 | + painter.drawRect(cursor_pos.x() + 15, cursor_pos.y() + 15, 50, 50) # Rectangle offset from cursor |
| 108 | + painter.end() |
| 109 | + |
| 110 | + def mouseMoveEvent(self, event): |
| 111 | + if self.pixmap: |
| 112 | + image = self.pixmap.toImage() |
| 113 | + # Scale the coordinates for High DPI displays |
| 114 | + scaled_x = int(event.x() * self.device_pixel_ratio) |
| 115 | + scaled_y = int(event.y() * self.device_pixel_ratio) |
| 116 | + if 0 <= scaled_x < image.width() and 0 <= scaled_y < image.height(): |
| 117 | + color = QColor(image.pixel(scaled_x, scaled_y)) |
| 118 | + self.color_preview = color |
| 119 | + self.update() |
| 120 | + |
| 121 | + def mousePressEvent(self, event): |
| 122 | + if self.pixmap: |
| 123 | + image = self.pixmap.toImage() |
| 124 | + # Scale the coordinates for High DPI displays |
| 125 | + scaled_x = int(event.x() * self.device_pixel_ratio) |
| 126 | + scaled_y = int(event.y() * self.device_pixel_ratio) |
| 127 | + if 0 <= scaled_x < image.width() and 0 <= scaled_y < image.height(): |
| 128 | + color = QColor(image.pixel(scaled_x, scaled_y)) |
| 129 | + self.color_picker.setCurrentColor(color) |
| 130 | + log.debug(f"Picked color: {color.name()}") |
| 131 | + self.accept() # Close the dialog |
0 commit comments