Skip to content

Commit

Permalink
Merge pull request #5675 from OpenShot/wayland-color-picker
Browse files Browse the repository at this point in the history
Adding new Wayland-compatible color picker
  • Loading branch information
jonoomph authored Dec 21, 2024
2 parents 77b9932 + 270868e commit 6b583c3
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 129 deletions.
229 changes: 102 additions & 127 deletions src/windows/color_picker.py
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
Expand All @@ -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
2 changes: 0 additions & 2 deletions src/windows/title_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,6 @@ def btnFontColor_clicked(self):
ColorPicker(
self.font_color_code, parent=self,
title=_("Select a Color"),
extra_options=QColorDialog.ShowAlphaChannel,
callback=callback_func)

def btnBackgroundColor_clicked(self):
Expand All @@ -521,7 +520,6 @@ def btnBackgroundColor_clicked(self):
ColorPicker(
self.bg_color_code, parent=self,
title=_("Select a Color"),
extra_options=QColorDialog.ShowAlphaChannel,
callback=callback_func)

def btnFont_clicked(self):
Expand Down

0 comments on commit 6b583c3

Please sign in to comment.