Skip to content

Commit 6b583c3

Browse files
authored
Merge pull request #5675 from OpenShot/wayland-color-picker
Adding new Wayland-compatible color picker
2 parents 77b9932 + 270868e commit 6b583c3

File tree

2 files changed

+102
-129
lines changed

2 files changed

+102
-129
lines changed

src/windows/color_picker.py

+102-127
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
"""
22
@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]>
55
66
@section LICENSE
77
8-
Copyright (c) 2008-2020 OpenShot Studios, LLC
8+
Copyright (c) 2008-2024 OpenShot Studios, LLC
99
(http://www.openshotstudios.com). This file is part of
1010
OpenShot Video Editor (http://www.openshot.org), an open-source project
1111
dedicated to delivering high quality video editing and animation solutions
@@ -25,132 +25,107 @@
2525
along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
2626
"""
2727

28+
from PyQt5.QtWidgets import QColorDialog, QPushButton, QDialog
29+
from PyQt5.QtGui import QColor, QPainter, QPen, QCursor
30+
from PyQt5.QtCore import Qt
2831
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
2941

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")
5342
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):
11983
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+
14798
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

src/windows/title_editor.py

-2
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,6 @@ def btnFontColor_clicked(self):
505505
ColorPicker(
506506
self.font_color_code, parent=self,
507507
title=_("Select a Color"),
508-
extra_options=QColorDialog.ShowAlphaChannel,
509508
callback=callback_func)
510509

511510
def btnBackgroundColor_clicked(self):
@@ -521,7 +520,6 @@ def btnBackgroundColor_clicked(self):
521520
ColorPicker(
522521
self.bg_color_code, parent=self,
523522
title=_("Select a Color"),
524-
extra_options=QColorDialog.ShowAlphaChannel,
525523
callback=callback_func)
526524

527525
def btnFont_clicked(self):

0 commit comments

Comments
 (0)