-
Notifications
You must be signed in to change notification settings - Fork 553
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5675 from OpenShot/wayland-color-picker
Adding new Wayland-compatible color picker
- Loading branch information
Showing
2 changed files
with
102 additions
and
129 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,11 @@ | ||
""" | ||
@file | ||
@brief A non-modal Qt color picker dialog launcher | ||
@author FeRD (Frank Dana) <[email protected]> | ||
@brief A modal Qt color picker dialog launcher, which works in Wayland | ||
@author Jonathan Thomas <[email protected]> | ||
@section LICENSE | ||
Copyright (c) 2008-2020 OpenShot Studios, LLC | ||
Copyright (c) 2008-2024 OpenShot Studios, LLC | ||
(http://www.openshotstudios.com). This file is part of | ||
OpenShot Video Editor (http://www.openshot.org), an open-source project | ||
dedicated to delivering high quality video editing and animation solutions | ||
|
@@ -25,132 +25,107 @@ | |
along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>. | ||
""" | ||
|
||
from PyQt5.QtWidgets import QColorDialog, QPushButton, QDialog | ||
from PyQt5.QtGui import QColor, QPainter, QPen, QCursor | ||
from PyQt5.QtCore import Qt | ||
from classes.logger import log | ||
from classes.app import get_app | ||
|
||
|
||
class ColorPicker(QColorDialog): | ||
def __init__(self, initial_color, parent=None, title=None, callback=None, *args, **kwargs): | ||
super().__init__(initial_color, parent, *args, **kwargs) | ||
self.parent_window = parent | ||
self.callback = callback | ||
self.picked_pixmap = None | ||
|
||
from PyQt5.QtCore import Qt, QTimer, QRect, QPoint | ||
from PyQt5.QtGui import QColor, QBrush, QPen, QPalette, QPainter, QPixmap | ||
from PyQt5.QtWidgets import QColorDialog, QWidget, QLabel | ||
|
||
|
||
class ColorPicker(QWidget): | ||
"""Show a non-modal color picker. | ||
QColorDialog.colorSelected(QColor) is emitted when the user picks a color""" | ||
|
||
def __init__(self, initial_color: QColor, callback, extra_options=0, | ||
parent=None, title=None, *args, **kwargs): | ||
super().__init__(parent=parent, *args, **kwargs) | ||
self.setObjectName("ColorPicker") | ||
# Merge any additional user-supplied options with our own | ||
options = QColorDialog.DontUseNativeDialog | ||
if extra_options > 0: | ||
options = options | extra_options | ||
# Set up non-modal color dialog (to avoid blocking the eyedropper) | ||
log.debug( | ||
"Loading QColorDialog with start value %s", | ||
initial_color.getRgb()) | ||
self.dialog = CPDialog(initial_color, parent) | ||
self.dialog.setObjectName("CPDialog") | ||
if title: | ||
self.dialog.setWindowTitle(title) | ||
self.dialog.setWindowFlags(Qt.Tool) | ||
self.dialog.setOptions(options) | ||
# Avoid signal loops | ||
self.dialog.blockSignals(True) | ||
self.dialog.colorSelected.connect(callback) | ||
self.dialog.finished.connect(self.dialog.deleteLater) | ||
self.dialog.finished.connect(self.deleteLater) | ||
self.dialog.setCurrentColor(initial_color) | ||
self.dialog.blockSignals(False) | ||
self.dialog.open() | ||
# Seems to help if this is done AFTER init() returns | ||
QTimer.singleShot(0, self.add_alpha) | ||
def add_alpha(self): | ||
self.dialog.replace_label() | ||
|
||
|
||
class CPDialog(QColorDialog): | ||
"""Show a modified QColorDialog which supports checkerboard alpha""" | ||
def __init__(self, *args, **kwargs): | ||
super().__init__(*args, **kwargs) | ||
log.debug("CPDialog initialized") | ||
self.alpha_label = CPAlphaShowLabel(self) | ||
self.alpha_label.setObjectName("alpha_label") | ||
self.currentColorChanged.connect(self.alpha_label.updateColor) | ||
def replace_label(self): | ||
log.debug("Beginning discovery for QColorShowLabel widget") | ||
# Find the dialog widget used to display the current | ||
# color, so we can replace it with our implementation | ||
try: | ||
qcs = [ | ||
a for a in self.children() | ||
if hasattr(a, "metaObject") | ||
and a.metaObject().className() == 'QColorShower' | ||
][0] | ||
log.debug("Found QColorShower: %s", qcs) | ||
qcsl = [ | ||
b for b in qcs.children() | ||
if hasattr(b, "metaObject") | ||
and b.metaObject().className() == 'QColorShowLabel' | ||
][0] | ||
log.debug("Found QColorShowLabel: %s", qcsl) | ||
except IndexError as ex: | ||
child_list = [ | ||
a.metaObject().className() | ||
for a in self.children() | ||
if hasattr(a, "metaObject") | ||
] | ||
log.debug("%d children of CPDialog %s", len(child_list), child_list) | ||
raise RuntimeError("Could not find label to replace!") from ex | ||
qcslay = qcs.layout() | ||
log.debug( | ||
"QColorShowLabel found at layout index %d", qcslay.indexOf(qcsl)) | ||
qcs.layout().replaceWidget(qcsl, self.alpha_label) | ||
log.debug("Replaced QColorShowLabel widget, hiding original") | ||
# Make sure it doesn't receive signals while hidden | ||
qcsl.blockSignals(True) | ||
qcsl.hide() | ||
self.alpha_label.show() | ||
|
||
|
||
class CPAlphaShowLabel(QLabel): | ||
"""A replacement for QColorDialog's QColorShowLabel which | ||
displays the currently-active color using checkerboard alpha""" | ||
def __init__(self, *args, **kwargs): | ||
self.setWindowTitle(title) | ||
|
||
self.setOption(QColorDialog.DontUseNativeDialog) | ||
self.colorSelected.connect(self.on_color_selected) | ||
|
||
# Override the "Pick Screen Color" button signal | ||
self._override_pick_screen_color() | ||
|
||
# Automatically open the dialog | ||
self.open() | ||
|
||
def _override_pick_screen_color(self): | ||
# Get first pushbutton (color picker) | ||
color_picker_button = self.findChildren(QPushButton)[0] | ||
log.debug(f"Color picker button text: {color_picker_button.text()}") | ||
|
||
# Connect to button signals | ||
color_picker_button.clicked.disconnect() | ||
color_picker_button.clicked.connect(self.start_color_picking) | ||
log.debug("Overridden the 'Pick Screen Color' button action") | ||
|
||
def start_color_picking(self): | ||
if self.parent_window: | ||
self.picked_pixmap = get_app().window.grab() | ||
self._show_picking_dialog() | ||
else: | ||
log.error("No parent window available for color picking") | ||
|
||
def _show_picking_dialog(self): | ||
dialog = PickingDialog(self.picked_pixmap, self) | ||
dialog.exec_() # Show modal dialog | ||
self.raise_() | ||
|
||
def on_color_selected(self, color): | ||
log.debug(f"Color selected: {color.name()}") | ||
if self.callback: | ||
self.callback(color) | ||
|
||
class PickingDialog(QDialog): | ||
def __init__(self, pixmap, color_picker, *args, **kwargs): | ||
super().__init__(*args, **kwargs) | ||
# Length in pixels of a side of the checkerboard squares | ||
# (Pattern is made up of 2x2 squares, total size 2n x 2n) | ||
self.checkerboard_size = 8 | ||
# Start out transparent by default | ||
self.color = super().parent().currentColor() | ||
self.build_pattern() | ||
log.debug("CPAlphaShowLabel initialized, creating brush") | ||
def updateColor(self, color: QColor): | ||
self.color = color | ||
log.debug("Label color set to %s", str(color.getRgb())) | ||
self.repaint() | ||
def build_pattern(self) -> QPixmap: | ||
"""Construct tileable checkerboard pattern for paint events""" | ||
# Brush will be an nxn checkerboard pattern | ||
n = self.checkerboard_size | ||
pat = QPixmap(2 * n, 2 * n) | ||
p = QPainter(pat) | ||
p.setPen(Qt.NoPen) | ||
# Paint a checkerboard pattern for the color to be overlaid on | ||
self.bg0 = QColor("#aaa") | ||
self.bg1 = QColor("#ccc") | ||
p.fillRect(pat.rect(), self.bg0) | ||
p.fillRect(QRect(0, 0, n, n), self.bg1) | ||
p.fillRect(QRect(n, n, 2 * n, 2 * n), self.bg1) | ||
p.end() | ||
log.debug("Constructed %s checkerboard brush", pat.size()) | ||
self.pattern = pat | ||
self.pixmap = pixmap | ||
self.device_pixel_ratio = pixmap.devicePixelRatio() | ||
self.color_picker = color_picker | ||
self.setWindowModality(Qt.WindowModal) | ||
self.setGeometry(get_app().window.geometry()) | ||
self.setFixedSize(self.size()) | ||
self.setCursor(Qt.CrossCursor) | ||
self.color_preview = QColor("#FFFFFF") | ||
self.setMouseTracking(True) | ||
|
||
# Get first pushbutton (color picker) | ||
color_picker_button = self.color_picker.findChildren(QPushButton)[0] | ||
self.setWindowTitle(f"OpenShot: {color_picker_button.text().replace('&', '')}") | ||
|
||
def paintEvent(self, event): | ||
"""Show the current color, with checkerboard alpha""" | ||
event.accept() | ||
p = QPainter(self) | ||
p.setPen(Qt.NoPen) | ||
if self.color.alphaF() < 1.0: | ||
# Draw a checkerboard pattern under the color | ||
p.drawTiledPixmap(event.rect(), self.pattern, QPoint(4,4)) | ||
p.fillRect(event.rect(), self.color) | ||
p.end() | ||
painter = QPainter(self) | ||
painter.drawPixmap(self.rect(), self.pixmap) | ||
# Draw color preview rectangle near the cursor | ||
if self.color_preview: | ||
pen = QPen(Qt.black, 2) | ||
painter.setPen(pen) | ||
painter.setBrush(self.color_preview) | ||
cursor_pos = self.mapFromGlobal(QCursor.pos()) | ||
painter.drawRect(cursor_pos.x() + 15, cursor_pos.y() + 15, 50, 50) # Rectangle offset from cursor | ||
painter.end() | ||
|
||
def mouseMoveEvent(self, event): | ||
if self.pixmap: | ||
image = self.pixmap.toImage() | ||
# Scale the coordinates for High DPI displays | ||
scaled_x = int(event.x() * self.device_pixel_ratio) | ||
scaled_y = int(event.y() * self.device_pixel_ratio) | ||
if 0 <= scaled_x < image.width() and 0 <= scaled_y < image.height(): | ||
color = QColor(image.pixel(scaled_x, scaled_y)) | ||
self.color_preview = color | ||
self.update() | ||
|
||
def mousePressEvent(self, event): | ||
if self.pixmap: | ||
image = self.pixmap.toImage() | ||
# Scale the coordinates for High DPI displays | ||
scaled_x = int(event.x() * self.device_pixel_ratio) | ||
scaled_y = int(event.y() * self.device_pixel_ratio) | ||
if 0 <= scaled_x < image.width() and 0 <= scaled_y < image.height(): | ||
color = QColor(image.pixel(scaled_x, scaled_y)) | ||
self.color_picker.setCurrentColor(color) | ||
log.debug(f"Picked color: {color.name()}") | ||
self.accept() # Close the dialog |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters