1688 lines
70 KiB
Python
1688 lines
70 KiB
Python
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||
|
|
#
|
||
|
|
# HCL Sliders is a Krita plugin for color selection.
|
||
|
|
# Copyright (C) 2024 Lucifer <krita-artists.org/u/Lucifer>
|
||
|
|
#
|
||
|
|
# This program is free software: you can redistribute it and/or modify
|
||
|
|
# it under the terms of the GNU General Public License as published by
|
||
|
|
# the Free Software Foundation, either version 3 of the License, or
|
||
|
|
# (at your option) any later version.
|
||
|
|
#
|
||
|
|
# This program is distributed in the hope that it will be useful,
|
||
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
|
# GNU General Public License for more details.
|
||
|
|
#
|
||
|
|
# You should have received a copy of the GNU General Public License
|
||
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
|
#
|
||
|
|
# This file incorporates work covered by the following copyright and
|
||
|
|
# permission notice:
|
||
|
|
#
|
||
|
|
# Pigment.O is a Krita plugin and it is a Color Picker and Color Mixer.
|
||
|
|
# Copyright ( C ) 2020 Ricardo Jeremias.
|
||
|
|
#
|
||
|
|
# This program is free software: you can redistribute it and/or modify
|
||
|
|
# it under the terms of the GNU General Public License as published by
|
||
|
|
# the Free Software Foundation, either version 3 of the License, or
|
||
|
|
# ( at your option ) any later version.
|
||
|
|
#
|
||
|
|
# This program is distributed in the hope that it will be useful,
|
||
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
|
# GNU General Public License for more details.
|
||
|
|
#
|
||
|
|
# You should have received a copy of the GNU General Public License
|
||
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
|
|
||
|
|
from PyQt5.QtCore import Qt, pyqtSignal, QTimer, QSize
|
||
|
|
from PyQt5.QtGui import QPainter, QBrush, QColor, QLinearGradient, QPixmap, QIcon
|
||
|
|
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QDoubleSpinBox, QLabel, QLineEdit,
|
||
|
|
QPushButton, QListWidget, QListWidgetItem, QDialog, QStackedWidget,
|
||
|
|
QTabWidget, QCheckBox, QGroupBox, QRadioButton, QSpinBox)
|
||
|
|
from krita import DockWidget, ManagedColor
|
||
|
|
|
||
|
|
from .colorconversion import Convert
|
||
|
|
|
||
|
|
DOCKER_NAME = 'HCL Sliders'
|
||
|
|
TIME = 100 # ms time for plugin to update color from krita, faster updates may make krita slower
|
||
|
|
DELAY = 300 # ms delay updating color history to prevent flooding when using the color picker
|
||
|
|
# DISPLAY_HEIGHT = 25 # px for color display panel at the top
|
||
|
|
DISPLAY_HEIGHT = 65 # px for color display panel at the top
|
||
|
|
# CHANNEL_HEIGHT = 19 # px for channels, also influences hex/ok syntax box and buttons
|
||
|
|
CHANNEL_HEIGHT = 39 # px for channels, also influences hex/ok syntax box and buttons
|
||
|
|
HISTORY_HEIGHT = 16 # px for color history and area of each color box
|
||
|
|
# compatible color profiles in krita
|
||
|
|
SRGB = ('sRGB-elle-V2-srgbtrc.icc', 'sRGB built-in')
|
||
|
|
LINEAR = ('sRGB-elle-V2-g10.icc', 'krita-2.5, lcms sRGB built-in with linear gamma TRC')
|
||
|
|
NOTATION = ('HEX', 'OKLAB', 'OKLCH')
|
||
|
|
|
||
|
|
|
||
|
|
class ColorDisplay(QWidget):
|
||
|
|
|
||
|
|
def __init__(self, parent):
|
||
|
|
super().__init__(parent)
|
||
|
|
|
||
|
|
self.hcl = parent
|
||
|
|
self.current = None
|
||
|
|
self.recent = None
|
||
|
|
self.foreground = None
|
||
|
|
self.background = None
|
||
|
|
|
||
|
|
def setCurrentColor(self, color=None):
|
||
|
|
self.current = color
|
||
|
|
self.update()
|
||
|
|
|
||
|
|
def setForeGroundColor(self, color=None):
|
||
|
|
self.foreground = color
|
||
|
|
self.update()
|
||
|
|
|
||
|
|
def setBackGroundColor(self, color=None):
|
||
|
|
self.background = color
|
||
|
|
self.update()
|
||
|
|
|
||
|
|
def resetColors(self):
|
||
|
|
self.current = None
|
||
|
|
self.recent = None
|
||
|
|
self.foreground = None
|
||
|
|
self.background = None
|
||
|
|
self.update()
|
||
|
|
|
||
|
|
def isChanged(self):
|
||
|
|
if self.current is None:
|
||
|
|
return True
|
||
|
|
if self.current.components() != self.foreground.components():
|
||
|
|
return True
|
||
|
|
if self.current.colorModel() != self.foreground.colorModel():
|
||
|
|
return True
|
||
|
|
if self.current.colorDepth() != self.foreground.colorDepth():
|
||
|
|
return True
|
||
|
|
if self.current.colorProfile() != self.foreground.colorProfile():
|
||
|
|
return True
|
||
|
|
return False
|
||
|
|
|
||
|
|
def isChanging(self):
|
||
|
|
if self.recent is None:
|
||
|
|
return False
|
||
|
|
if self.recent.components() != self.current.components():
|
||
|
|
return True
|
||
|
|
if self.recent.colorModel() != self.current.colorModel():
|
||
|
|
return True
|
||
|
|
if self.recent.colorDepth() != self.current.colorDepth():
|
||
|
|
return True
|
||
|
|
if self.recent.colorProfile() != self.current.colorProfile():
|
||
|
|
return True
|
||
|
|
return False
|
||
|
|
|
||
|
|
def paintEvent(self, event):
|
||
|
|
painter = QPainter(self)
|
||
|
|
painter.setPen(Qt.PenStyle.NoPen)
|
||
|
|
width = self.width()
|
||
|
|
halfwidth = round(width / 2.0)
|
||
|
|
height = self.height()
|
||
|
|
# foreground color from krita
|
||
|
|
if self.foreground:
|
||
|
|
painter.setBrush(QBrush(self.foreground.colorForCanvas(self.hcl.canvas())))
|
||
|
|
else:
|
||
|
|
painter.setBrush( QBrush(QColor(0, 0, 0)))
|
||
|
|
painter.drawRect(0, 0, width, height)
|
||
|
|
# current color from sliders
|
||
|
|
if self.current:
|
||
|
|
painter.setBrush(QBrush(self.current.colorForCanvas(self.hcl.canvas())))
|
||
|
|
painter.drawRect(0, 0, halfwidth, height)
|
||
|
|
if self.background:
|
||
|
|
painter.setBrush(QBrush(self.background.colorForCanvas(self.hcl.canvas())))
|
||
|
|
painter.drawRect(halfwidth, 0, width - halfwidth, height)
|
||
|
|
|
||
|
|
|
||
|
|
class ColorHistory(QListWidget):
|
||
|
|
|
||
|
|
def __init__(self, hcl, parent=None):
|
||
|
|
super().__init__(parent)
|
||
|
|
# should not pass in hcl as parent if it can be hidden
|
||
|
|
self.hcl = hcl
|
||
|
|
self.index = -1
|
||
|
|
self.modifier = None
|
||
|
|
self.start = 0
|
||
|
|
self.position = 0
|
||
|
|
self.setFlow(QListWidget.Flow.LeftToRight)
|
||
|
|
self.setFixedHeight(HISTORY_HEIGHT)
|
||
|
|
self.setViewportMargins(-2, 0, 0, 0)
|
||
|
|
# grid width + 2 to make gaps between swatches
|
||
|
|
self.setGridSize(QSize(HISTORY_HEIGHT + 2, HISTORY_HEIGHT))
|
||
|
|
self.setUniformItemSizes(True)
|
||
|
|
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||
|
|
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||
|
|
self.setHorizontalScrollMode(QListWidget.ScrollMode.ScrollPerPixel)
|
||
|
|
self.setSelectionMode(QListWidget.SelectionMode.NoSelection)
|
||
|
|
|
||
|
|
def startScrollShift(self, event):
|
||
|
|
self.start = self.horizontalScrollBar().value()
|
||
|
|
self.position = event.x()
|
||
|
|
|
||
|
|
def keyPressEvent(self, event):
|
||
|
|
# disable keyboard interactions
|
||
|
|
pass
|
||
|
|
|
||
|
|
def mousePressEvent(self, event):
|
||
|
|
self.hcl.setPressed(True)
|
||
|
|
item = self.itemAt(event.pos())
|
||
|
|
index = self.row(item)
|
||
|
|
|
||
|
|
if index != -1:
|
||
|
|
if (event.buttons() == Qt.MouseButton.LeftButton and
|
||
|
|
event.modifiers() == Qt.KeyboardModifier.NoModifier):
|
||
|
|
color = self.hcl.makeManagedColor(*self.hcl.pastColors[index])
|
||
|
|
self.hcl.color.setCurrentColor(color)
|
||
|
|
self.index = index
|
||
|
|
self.modifier = Qt.KeyboardModifier.NoModifier
|
||
|
|
elif (event.buttons() == Qt.MouseButton.LeftButton and
|
||
|
|
event.modifiers() == Qt.KeyboardModifier.ControlModifier):
|
||
|
|
color = self.hcl.makeManagedColor(*self.hcl.pastColors[index])
|
||
|
|
self.hcl.color.setBackGroundColor(color)
|
||
|
|
self.index = index
|
||
|
|
self.modifier = Qt.KeyboardModifier.ControlModifier
|
||
|
|
elif (event.buttons() == Qt.MouseButton.LeftButton and
|
||
|
|
event.modifiers() == Qt.KeyboardModifier.AltModifier):
|
||
|
|
self.index = index
|
||
|
|
self.modifier = Qt.KeyboardModifier.AltModifier
|
||
|
|
self.startScrollShift(event)
|
||
|
|
|
||
|
|
def mouseMoveEvent(self, event):
|
||
|
|
if (event.buttons() == Qt.MouseButton.LeftButton and
|
||
|
|
event.modifiers() == Qt.KeyboardModifier.ShiftModifier):
|
||
|
|
position = 0
|
||
|
|
bar = self.horizontalScrollBar()
|
||
|
|
if bar.maximum():
|
||
|
|
# speed of grid width squared seems good
|
||
|
|
speed = (HISTORY_HEIGHT + 2) ** 2
|
||
|
|
# move bar at constant speed
|
||
|
|
shift = float(self.position - event.x()) / self.width()
|
||
|
|
position = round(self.start + shift * speed)
|
||
|
|
bar.setValue(position)
|
||
|
|
else:
|
||
|
|
self.startScrollShift(event)
|
||
|
|
|
||
|
|
def mouseReleaseEvent(self, event):
|
||
|
|
item = self.itemAt(event.pos())
|
||
|
|
index = self.row(item)
|
||
|
|
|
||
|
|
if index == self.index and index != -1:
|
||
|
|
if (event.modifiers() == Qt.KeyboardModifier.NoModifier and
|
||
|
|
self.modifier == Qt.KeyboardModifier.NoModifier):
|
||
|
|
self.hcl.setPastColorToFG(index)
|
||
|
|
elif (event.modifiers() == Qt.KeyboardModifier.ControlModifier and
|
||
|
|
self.modifier == Qt.KeyboardModifier.ControlModifier):
|
||
|
|
self.hcl.setPastColorToBG()
|
||
|
|
|
||
|
|
if (event.modifiers() == Qt.KeyboardModifier.AltModifier and
|
||
|
|
self.modifier == Qt.KeyboardModifier.AltModifier):
|
||
|
|
if self.index != -1 and index != -1 :
|
||
|
|
start = index
|
||
|
|
stop = self.index
|
||
|
|
if self.index > index:
|
||
|
|
start = self.index
|
||
|
|
stop = index
|
||
|
|
for i in range(start, stop - 1, -1):
|
||
|
|
self.takeItem(i)
|
||
|
|
self.hcl.pastColors.pop(i)
|
||
|
|
|
||
|
|
if self.modifier == Qt.KeyboardModifier.NoModifier:
|
||
|
|
# prevent setHistory when krita fg color not changed
|
||
|
|
self.hcl.color.current = self.hcl.color.foreground
|
||
|
|
elif self.modifier == Qt.KeyboardModifier.ControlModifier:
|
||
|
|
self.hcl.color.setBackGroundColor()
|
||
|
|
|
||
|
|
self.modifier = None
|
||
|
|
self.index = -1
|
||
|
|
self.hcl.setPressed(False)
|
||
|
|
|
||
|
|
|
||
|
|
class ChannelSlider(QWidget):
|
||
|
|
|
||
|
|
valueChanged = pyqtSignal(float)
|
||
|
|
mousePressed = pyqtSignal(bool)
|
||
|
|
|
||
|
|
def __init__(self, limit: float, parent=None):
|
||
|
|
super().__init__(parent)
|
||
|
|
|
||
|
|
self.value = 0.0
|
||
|
|
self.limit = limit
|
||
|
|
self.interval = 0.1
|
||
|
|
self.displacement = 0
|
||
|
|
self.start = 0.0
|
||
|
|
self.position = 0
|
||
|
|
self.shift = 0.1
|
||
|
|
self.colors = []
|
||
|
|
|
||
|
|
def setGradientColors(self, colors: list):
|
||
|
|
if self.colors:
|
||
|
|
self.colors = []
|
||
|
|
for rgb in colors:
|
||
|
|
# using rgbF as is may result in black as colors are out of gamut
|
||
|
|
color = QColor(*rgb)
|
||
|
|
self.colors.append(color)
|
||
|
|
self.update()
|
||
|
|
|
||
|
|
def setValue(self, value: float):
|
||
|
|
self.value = value
|
||
|
|
self.update()
|
||
|
|
|
||
|
|
def setLimit(self, value: float):
|
||
|
|
self.limit = value
|
||
|
|
self.update()
|
||
|
|
|
||
|
|
def setInterval(self, interval: float):
|
||
|
|
limit = 100.0 if self.limit < 360 else 360.0
|
||
|
|
if interval < 0.1:
|
||
|
|
interval = 0.1
|
||
|
|
elif interval > limit:
|
||
|
|
interval = limit
|
||
|
|
self.interval = interval
|
||
|
|
|
||
|
|
def setDisplacement(self, displacement: float):
|
||
|
|
limit = 99.9 if self.limit < 360 else 359.9
|
||
|
|
if displacement < 0:
|
||
|
|
displacement = 0
|
||
|
|
elif displacement > limit:
|
||
|
|
displacement = limit
|
||
|
|
self.displacement = displacement
|
||
|
|
|
||
|
|
def emitValueChanged(self, event):
|
||
|
|
position = event.x()
|
||
|
|
width = self.width()
|
||
|
|
if position > width:
|
||
|
|
position = width
|
||
|
|
elif position < 0:
|
||
|
|
position = 0.0
|
||
|
|
self.value = round((position / width) * self.limit, 3)
|
||
|
|
self.valueChanged.emit(self.value)
|
||
|
|
self.mousePressed.emit(True)
|
||
|
|
|
||
|
|
def emitValueSnapped(self, event):
|
||
|
|
position = event.x()
|
||
|
|
width = self.width()
|
||
|
|
if position > width:
|
||
|
|
position = width
|
||
|
|
elif position < 0:
|
||
|
|
position = 0.0
|
||
|
|
value = round((position / width) * self.limit, 3)
|
||
|
|
|
||
|
|
if value != 0 and value != self.limit:
|
||
|
|
interval = self.interval if self.interval != 0 else self.limit
|
||
|
|
if self.limit < 100:
|
||
|
|
interval = (self.interval / 100) * self.limit
|
||
|
|
displacement = (value - self.displacement) % interval
|
||
|
|
if displacement < interval / 2:
|
||
|
|
value -= displacement
|
||
|
|
else:
|
||
|
|
value += interval - displacement
|
||
|
|
if value > self.limit:
|
||
|
|
value = self.limit
|
||
|
|
elif value < 0:
|
||
|
|
value = 0.0
|
||
|
|
|
||
|
|
self.value = value
|
||
|
|
self.valueChanged.emit(self.value)
|
||
|
|
self.mousePressed.emit(True)
|
||
|
|
|
||
|
|
def startValueShift(self, event):
|
||
|
|
self.start = self.value
|
||
|
|
self.position = event.x()
|
||
|
|
|
||
|
|
def emitValueShifted(self, event):
|
||
|
|
position = event.x()
|
||
|
|
vector = position - self.position
|
||
|
|
value = self.start + (vector * self.shift)
|
||
|
|
|
||
|
|
if value < 0:
|
||
|
|
if self.limit == 360:
|
||
|
|
value += self.limit
|
||
|
|
else:
|
||
|
|
value = 0
|
||
|
|
elif value > self.limit:
|
||
|
|
if self.limit == 360:
|
||
|
|
value -= self.limit
|
||
|
|
else:
|
||
|
|
value = self.limit
|
||
|
|
|
||
|
|
self.value = value
|
||
|
|
self.valueChanged.emit(self.value)
|
||
|
|
self.mousePressed.emit(True)
|
||
|
|
|
||
|
|
def mousePressEvent(self, event):
|
||
|
|
if (event.buttons() == Qt.MouseButton.LeftButton and
|
||
|
|
event.modifiers() == Qt.KeyboardModifier.NoModifier):
|
||
|
|
self.emitValueChanged(event)
|
||
|
|
elif (event.buttons() == Qt.MouseButton.LeftButton and
|
||
|
|
event.modifiers() == Qt.KeyboardModifier.ControlModifier):
|
||
|
|
self.emitValueSnapped(event)
|
||
|
|
self.startValueShift(event)
|
||
|
|
self.update()
|
||
|
|
|
||
|
|
def mouseMoveEvent(self, event):
|
||
|
|
if (event.buttons() == Qt.MouseButton.LeftButton and
|
||
|
|
event.modifiers() == Qt.KeyboardModifier.NoModifier):
|
||
|
|
self.emitValueChanged(event)
|
||
|
|
self.startValueShift(event)
|
||
|
|
elif (event.buttons() == Qt.MouseButton.LeftButton and
|
||
|
|
event.modifiers() == Qt.KeyboardModifier.ControlModifier):
|
||
|
|
self.emitValueSnapped(event)
|
||
|
|
self.startValueShift(event)
|
||
|
|
elif (event.buttons() == Qt.MouseButton.LeftButton and
|
||
|
|
event.modifiers() == Qt.KeyboardModifier.ShiftModifier):
|
||
|
|
self.shift = 0.1
|
||
|
|
self.emitValueShifted(event)
|
||
|
|
elif (event.buttons() == Qt.MouseButton.LeftButton and
|
||
|
|
event.modifiers() == Qt.KeyboardModifier.AltModifier):
|
||
|
|
self.shift = 0.01
|
||
|
|
self.emitValueShifted(event)
|
||
|
|
self.update()
|
||
|
|
|
||
|
|
def mouseReleaseEvent(self, event):
|
||
|
|
self.mousePressed.emit(False)
|
||
|
|
|
||
|
|
def paintEvent(self, event):
|
||
|
|
painter = QPainter(self)
|
||
|
|
width = self.width()
|
||
|
|
height = self.height()
|
||
|
|
# background
|
||
|
|
painter.setPen(Qt.PenStyle.NoPen)
|
||
|
|
painter.setBrush( QBrush(QColor(0, 0, 0, 50)))
|
||
|
|
painter.drawRect(0, 1, width, height - 2)
|
||
|
|
# gradient
|
||
|
|
gradient = QLinearGradient(0, 0, width, 0)
|
||
|
|
if self.colors:
|
||
|
|
for index, color in enumerate(self.colors):
|
||
|
|
gradient.setColorAt(index / (len(self.colors) - 1), color)
|
||
|
|
painter.setBrush(QBrush(gradient))
|
||
|
|
painter.drawRect(1, 2, width - 2, height - 4)
|
||
|
|
# cursor
|
||
|
|
if self.limit:
|
||
|
|
position = round((self.value / self.limit) * (width - 2))
|
||
|
|
painter.setBrush( QBrush(QColor(0, 0, 0, 100)))
|
||
|
|
painter.drawRect(position - 2, 0, 6, height)
|
||
|
|
painter.setBrush(QBrush(QColor(255, 255, 255, 200)))
|
||
|
|
painter.drawRect(position, 1, 2, height - 2)
|
||
|
|
|
||
|
|
|
||
|
|
class ColorChannel:
|
||
|
|
|
||
|
|
channelList = None
|
||
|
|
|
||
|
|
def __init__(self, name: str, parent):
|
||
|
|
self.name = name
|
||
|
|
self.update = parent.updateChannels
|
||
|
|
self.refresh = parent.updateChannelGradients
|
||
|
|
wrap = False
|
||
|
|
interval = 10.0
|
||
|
|
displacement = 0.0
|
||
|
|
self.scale = True
|
||
|
|
self.clip = 0.0
|
||
|
|
self.colorful = False
|
||
|
|
self.luma = False
|
||
|
|
self.limit = 100.0
|
||
|
|
if self.name[-3:] == "Hue":
|
||
|
|
wrap = True
|
||
|
|
interval = 30.0
|
||
|
|
if self.name[:2] == "ok":
|
||
|
|
interval = 40.0
|
||
|
|
displacement = 25.0
|
||
|
|
self.limit = 360.0
|
||
|
|
elif self.name[-6:] == "Chroma":
|
||
|
|
self.limit = 0.0
|
||
|
|
self.layout = QHBoxLayout()
|
||
|
|
self.layout.setSpacing(2)
|
||
|
|
|
||
|
|
if self.name[:2] == "ok":
|
||
|
|
tip = f"{self.name[:5].upper()} {self.name[5:]}"
|
||
|
|
letter = self.name[5:6]
|
||
|
|
else:
|
||
|
|
tip = f"{self.name[:3].upper()} {self.name[3:]}"
|
||
|
|
if self.name[-4:] == "Luma":
|
||
|
|
letter = "Y"
|
||
|
|
else:
|
||
|
|
letter = self.name[3:4]
|
||
|
|
self.label = QLabel(letter)
|
||
|
|
self.label.setFixedHeight(CHANNEL_HEIGHT - 1)
|
||
|
|
self.label.setFixedWidth(11)
|
||
|
|
self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
|
|
self.label.setToolTip(tip)
|
||
|
|
|
||
|
|
self.slider = ChannelSlider(self.limit)
|
||
|
|
self.slider.setFixedHeight(CHANNEL_HEIGHT)
|
||
|
|
self.slider.setMinimumWidth(100)
|
||
|
|
self.slider.setInterval(interval)
|
||
|
|
self.slider.setDisplacement(displacement)
|
||
|
|
self.slider.mousePressed.connect(parent.setPressed)
|
||
|
|
|
||
|
|
self.spinBox = QDoubleSpinBox()
|
||
|
|
if self.name[-6:] == "Chroma":
|
||
|
|
self.spinBox.setDecimals(3)
|
||
|
|
self.spinBox.setMaximum(self.limit)
|
||
|
|
self.spinBox.setWrapping(wrap)
|
||
|
|
self.spinBox.setFixedHeight(CHANNEL_HEIGHT)
|
||
|
|
self.spinBox.setFixedWidth(63)
|
||
|
|
self.spinBox.editingFinished.connect(parent.finishEditing)
|
||
|
|
|
||
|
|
self.slider.valueChanged.connect(self.updateSpinBox)
|
||
|
|
self.spinBox.valueChanged.connect(self.updateSlider)
|
||
|
|
ColorChannel.updateList(name)
|
||
|
|
|
||
|
|
def value(self):
|
||
|
|
return self.spinBox.value()
|
||
|
|
|
||
|
|
def setValue(self, value: float):
|
||
|
|
if self.name[-6:] == "Chroma" and self.limit >= 10:
|
||
|
|
value = round(value, 2)
|
||
|
|
self.slider.setValue(value)
|
||
|
|
self.spinBox.setValue(value)
|
||
|
|
|
||
|
|
def setLimit(self, value: float):
|
||
|
|
decimal = 2 if value >= 10 else 3
|
||
|
|
self.limit = round(value, decimal)
|
||
|
|
self.slider.setLimit(self.limit)
|
||
|
|
self.spinBox.setDecimals(decimal)
|
||
|
|
self.spinBox.setMaximum(self.limit)
|
||
|
|
self.spinBox.setSingleStep(self.limit / 100)
|
||
|
|
|
||
|
|
def clipChroma(self, clip: bool):
|
||
|
|
# do not set chroma channel itself to clip as the clip value will not be updated when adjusting
|
||
|
|
self.scale = not clip
|
||
|
|
self.refresh()
|
||
|
|
|
||
|
|
def colorfulHue(self, colorful: bool):
|
||
|
|
self.colorful = colorful
|
||
|
|
self.refresh()
|
||
|
|
|
||
|
|
def updateSlider(self, value: float):
|
||
|
|
self.update(value, self.name, "slider")
|
||
|
|
|
||
|
|
def updateSpinBox(self, value: float):
|
||
|
|
self.update(value, self.name, "spinBox")
|
||
|
|
|
||
|
|
def updateGradientColors(self, firstConst: float, lastConst: float, trc: str, ChromaLimit: float=-1):
|
||
|
|
colors = []
|
||
|
|
if self.name[-3:] == "Hue":
|
||
|
|
if self.name[:2] == "ok":
|
||
|
|
# oklab hue needs more points for qcolor to blend more accurately
|
||
|
|
# range of 0 to 25 - 345 in 15deg increments to 360
|
||
|
|
points = 26
|
||
|
|
increment = self.limit / (points - 2)
|
||
|
|
displacement = increment - 25
|
||
|
|
|
||
|
|
if self.colorful:
|
||
|
|
for number in range(points):
|
||
|
|
hue = (number - 1) * increment - displacement
|
||
|
|
if hue < 0:
|
||
|
|
hue = 0
|
||
|
|
elif hue > self.limit:
|
||
|
|
hue = self.limit
|
||
|
|
rgb = Convert.okhsvToRgbF(hue, 100.0, 100.0, trc)
|
||
|
|
colors.append(Convert.rgbFToInt8(*rgb, trc))
|
||
|
|
elif self.name[:5] == "okhcl":
|
||
|
|
for number in range(points):
|
||
|
|
hue = (number - 1) * increment - displacement
|
||
|
|
if hue < 0:
|
||
|
|
hue = 0
|
||
|
|
elif hue > self.limit:
|
||
|
|
hue = self.limit
|
||
|
|
rgb = Convert.okhclToRgbF(hue, firstConst, lastConst, ChromaLimit, trc)
|
||
|
|
colors.append(Convert.rgbFToInt8(*rgb, trc))
|
||
|
|
elif self.name[:5] == "okhsv":
|
||
|
|
for number in range(points):
|
||
|
|
hue = (number - 1) * increment - displacement
|
||
|
|
if hue < 0:
|
||
|
|
hue = 0
|
||
|
|
elif hue > self.limit:
|
||
|
|
hue = self.limit
|
||
|
|
rgb = Convert.okhsvToRgbF(hue, firstConst, lastConst, trc)
|
||
|
|
colors.append(Convert.rgbFToInt8(*rgb, trc))
|
||
|
|
elif self.name[:5] == "okhsl":
|
||
|
|
for number in range(points):
|
||
|
|
hue = (number - 1) * increment - displacement
|
||
|
|
if hue < 0:
|
||
|
|
hue = 0
|
||
|
|
elif hue > self.limit:
|
||
|
|
hue = self.limit
|
||
|
|
rgb = Convert.okhslToRgbF(hue, firstConst, lastConst, trc)
|
||
|
|
colors.append(Convert.rgbFToInt8(*rgb, trc))
|
||
|
|
else:
|
||
|
|
# range of 0 to 360deg incrementing by 30deg
|
||
|
|
points = 13
|
||
|
|
increment = self.limit / (points - 1)
|
||
|
|
|
||
|
|
if self.colorful:
|
||
|
|
if self.name[:3] != "hcy":
|
||
|
|
for number in range(points):
|
||
|
|
rgb = Convert.hsvToRgbF(number * increment, 100.0, 100.0, trc)
|
||
|
|
colors.append(Convert.rgbFToInt8(*rgb, trc))
|
||
|
|
else:
|
||
|
|
for number in range(points):
|
||
|
|
rgb = Convert.hcyToRgbF(number * increment, 100.0, -1, -1, trc, self.luma)
|
||
|
|
colors.append(Convert.rgbFToInt8(*rgb, trc))
|
||
|
|
elif self.name[:3] == "hsv":
|
||
|
|
for number in range(points):
|
||
|
|
rgb = Convert.hsvToRgbF(number * increment, firstConst, lastConst, trc)
|
||
|
|
colors.append(Convert.rgbFToInt8(*rgb, trc))
|
||
|
|
elif self.name[:3] == "hsl":
|
||
|
|
for number in range(points):
|
||
|
|
rgb = Convert.hslToRgbF(number * increment, firstConst, lastConst, trc)
|
||
|
|
colors.append(Convert.rgbFToInt8(*rgb, trc))
|
||
|
|
elif self.name[:3] == "hcy":
|
||
|
|
for number in range(points):
|
||
|
|
rgb = Convert.hcyToRgbF(number * increment, firstConst, lastConst,
|
||
|
|
ChromaLimit, trc, self.luma)
|
||
|
|
colors.append(Convert.rgbFToInt8(*rgb, trc))
|
||
|
|
else:
|
||
|
|
# range of 0 to 100% incrementing by 10%
|
||
|
|
points = 11
|
||
|
|
increment = self.limit / (points - 1)
|
||
|
|
|
||
|
|
if self.name[:3] == "hsv":
|
||
|
|
if self.name[3:] == "Saturation":
|
||
|
|
for number in range(points):
|
||
|
|
rgb = Convert.hsvToRgbF(firstConst, number * increment, lastConst, trc)
|
||
|
|
colors.append(Convert.rgbFToInt8(*rgb, trc))
|
||
|
|
elif self.name[3:] == "Value":
|
||
|
|
for number in range(points):
|
||
|
|
rgb = Convert.hsvToRgbF(firstConst, lastConst, number * increment, trc)
|
||
|
|
colors.append(Convert.rgbFToInt8(*rgb, trc))
|
||
|
|
elif self.name[:3] == "hsl":
|
||
|
|
if self.name[3:] == "Saturation":
|
||
|
|
for number in range(points):
|
||
|
|
rgb = Convert.hslToRgbF(firstConst, number * increment, lastConst, trc)
|
||
|
|
colors.append(Convert.rgbFToInt8(*rgb, trc))
|
||
|
|
elif self.name[3:] == "Lightness":
|
||
|
|
for number in range(points):
|
||
|
|
rgb = Convert.hslToRgbF(firstConst, lastConst, number * increment, trc)
|
||
|
|
colors.append(Convert.rgbFToInt8(*rgb, trc))
|
||
|
|
elif self.name[:3] == "hcy":
|
||
|
|
if self.name[3:] == "Chroma":
|
||
|
|
for number in range(points):
|
||
|
|
rgb = Convert.hcyToRgbF(firstConst, number * increment, lastConst,
|
||
|
|
ChromaLimit, trc, self.luma)
|
||
|
|
colors.append(Convert.rgbFToInt8(*rgb, trc))
|
||
|
|
elif self.name[3:] == "Luma":
|
||
|
|
for number in range(points):
|
||
|
|
rgb = Convert.hcyToRgbF(firstConst, lastConst, number * increment,
|
||
|
|
ChromaLimit, trc, self.luma)
|
||
|
|
colors.append(Convert.rgbFToInt8(*rgb, trc))
|
||
|
|
elif self.name[:5] == "okhcl":
|
||
|
|
if self.name[5:] == "Chroma":
|
||
|
|
for number in range(points):
|
||
|
|
rgb = Convert.okhclToRgbF(firstConst, number * increment, lastConst,
|
||
|
|
ChromaLimit, trc)
|
||
|
|
colors.append(Convert.rgbFToInt8(*rgb, trc))
|
||
|
|
elif self.name[5:] == "Lightness":
|
||
|
|
for number in range(points):
|
||
|
|
rgb = Convert.okhclToRgbF(firstConst, lastConst, number * increment,
|
||
|
|
ChromaLimit, trc)
|
||
|
|
colors.append(Convert.rgbFToInt8(*rgb, trc))
|
||
|
|
elif self.name[:5] == "okhsv":
|
||
|
|
if self.name[5:] == "Saturation":
|
||
|
|
for number in range(points):
|
||
|
|
rgb = Convert.okhsvToRgbF(firstConst, number * increment, lastConst, trc)
|
||
|
|
colors.append(Convert.rgbFToInt8(*rgb, trc))
|
||
|
|
elif self.name[5:] == "Value":
|
||
|
|
for number in range(points):
|
||
|
|
rgb = Convert.okhsvToRgbF(firstConst, lastConst, number * increment, trc)
|
||
|
|
colors.append(Convert.rgbFToInt8(*rgb, trc))
|
||
|
|
elif self.name[:5] == "okhsl":
|
||
|
|
if self.name[5:] == "Saturation":
|
||
|
|
for number in range(points):
|
||
|
|
rgb = Convert.okhslToRgbF(firstConst, number * increment, lastConst, trc)
|
||
|
|
colors.append(Convert.rgbFToInt8(*rgb, trc))
|
||
|
|
elif self.name[5:] == "Lightness":
|
||
|
|
for number in range(points):
|
||
|
|
rgb = Convert.okhslToRgbF(firstConst, lastConst, number * increment, trc)
|
||
|
|
colors.append(Convert.rgbFToInt8(*rgb, trc))
|
||
|
|
|
||
|
|
self.slider.setGradientColors(colors)
|
||
|
|
|
||
|
|
def blockSignals(self, block: bool):
|
||
|
|
self.slider.blockSignals(block)
|
||
|
|
self.spinBox.blockSignals(block)
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def updateList(cls, name: str):
|
||
|
|
if cls.channelList is None:
|
||
|
|
cls.channelList = []
|
||
|
|
cls.channelList.append(name)
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def getList(cls):
|
||
|
|
return cls.channelList.copy()
|
||
|
|
|
||
|
|
|
||
|
|
class SliderConfig(QDialog):
|
||
|
|
|
||
|
|
def __init__(self, parent):
|
||
|
|
super().__init__(parent)
|
||
|
|
|
||
|
|
self.hcl = parent
|
||
|
|
self.setWindowTitle("Configure HCL Sliders")
|
||
|
|
self.setFixedSize(468, 230)
|
||
|
|
self.mainLayout = QHBoxLayout(self)
|
||
|
|
self.loadPages()
|
||
|
|
|
||
|
|
def loadPages(self):
|
||
|
|
self.pageList = QListWidget()
|
||
|
|
self.pageList.setFixedWidth(76)
|
||
|
|
self.pageList.setDragEnabled(True)
|
||
|
|
self.pageList.viewport().setAcceptDrops(True)
|
||
|
|
self.pageList.setDropIndicatorShown(True)
|
||
|
|
self.pageList.setDragDropMode(QListWidget.DragDropMode.InternalMove)
|
||
|
|
self.pages = QStackedWidget()
|
||
|
|
|
||
|
|
hidden = ColorChannel.getList()
|
||
|
|
self.models = {}
|
||
|
|
for name in self.hcl.displayOrder:
|
||
|
|
if name[:2] == "ok":
|
||
|
|
self.models.setdefault(name[:5].upper(), []).append(name)
|
||
|
|
else:
|
||
|
|
self.models.setdefault(name[:3].upper(), []).append(name)
|
||
|
|
hidden.remove(name)
|
||
|
|
visible = list(self.models.keys())
|
||
|
|
for name in hidden:
|
||
|
|
if name[:2] == "ok":
|
||
|
|
self.models.setdefault(name[:5].upper(), []).append(name)
|
||
|
|
else:
|
||
|
|
self.models.setdefault(name[:3].upper(), []).append(name)
|
||
|
|
|
||
|
|
self.checkBoxes = {}
|
||
|
|
for model, channels in self.models.items():
|
||
|
|
tabs = QTabWidget()
|
||
|
|
tabs.setMovable(True)
|
||
|
|
|
||
|
|
for name in channels:
|
||
|
|
tab = QWidget()
|
||
|
|
tabLayout = QVBoxLayout()
|
||
|
|
tabLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||
|
|
tab.setLayout(tabLayout)
|
||
|
|
channel: ColorChannel = getattr(self.hcl, name)
|
||
|
|
|
||
|
|
snapGroup = QGroupBox("Cursor Snapping")
|
||
|
|
snapGroup.setFixedHeight(64)
|
||
|
|
snapGroup.setToolTip("Ctrl + Click to snap cursor at intervals")
|
||
|
|
snapLayout = QHBoxLayout()
|
||
|
|
interval = QDoubleSpinBox()
|
||
|
|
interval.setFixedWidth(72)
|
||
|
|
interval.setDecimals(1)
|
||
|
|
interval.setMinimum(0.1)
|
||
|
|
snapLayout.addWidget(interval)
|
||
|
|
intervalLabel = QLabel("Interval")
|
||
|
|
intervalLabel.setToolTip("Sets the snap interval to amount")
|
||
|
|
snapLayout.addWidget(intervalLabel)
|
||
|
|
displacement = QDoubleSpinBox()
|
||
|
|
displacement.setFixedWidth(72)
|
||
|
|
displacement.setDecimals(1)
|
||
|
|
snapLayout.addWidget(displacement)
|
||
|
|
DisplacementLabel = QLabel("Displacement")
|
||
|
|
DisplacementLabel.setToolTip("Displaces the snap positions by amount")
|
||
|
|
snapLayout.addWidget(DisplacementLabel)
|
||
|
|
snapGroup.setLayout(snapLayout)
|
||
|
|
tabLayout.addWidget(snapGroup)
|
||
|
|
|
||
|
|
param = name[len(model):]
|
||
|
|
if (model == 'HCY' or model == 'OKHCL') and param != 'Chroma':
|
||
|
|
radioGroup = QGroupBox("Chroma Mode")
|
||
|
|
radioGroup.setFixedHeight(64)
|
||
|
|
radioGroup.setToolTip("Switches how chroma is adjusted \
|
||
|
|
to stay within the sRGB gamut")
|
||
|
|
radioLayout = QHBoxLayout()
|
||
|
|
clip = QRadioButton("Clip")
|
||
|
|
clip.setToolTip("Clips chroma if it exceeds the srgb gamut when adjusting")
|
||
|
|
radioLayout.addWidget(clip)
|
||
|
|
scale = QRadioButton("Scale")
|
||
|
|
scale.setToolTip("Scales chroma to maintain constant saturation when adjusting")
|
||
|
|
radioLayout.addWidget(scale)
|
||
|
|
if channel.scale:
|
||
|
|
scale.setChecked(True)
|
||
|
|
else:
|
||
|
|
clip.setChecked(True)
|
||
|
|
clip.toggled.connect(channel.clipChroma)
|
||
|
|
radioGroup.setLayout(radioLayout)
|
||
|
|
tabLayout.addWidget(radioGroup)
|
||
|
|
|
||
|
|
if model == 'HCY' and param == 'Luma':
|
||
|
|
luma = QCheckBox("Always Luma")
|
||
|
|
luma.setToolTip("Transfer components to sRGB in linear TRCs")
|
||
|
|
luma.setChecked(channel.luma)
|
||
|
|
luma.toggled.connect(self.hcl.setLuma)
|
||
|
|
tabLayout.addWidget(luma)
|
||
|
|
|
||
|
|
if param == 'Hue':
|
||
|
|
interval.setMaximum(360.0)
|
||
|
|
interval.setSuffix(u'\N{DEGREE SIGN}')
|
||
|
|
displacement.setMaximum(359.9)
|
||
|
|
displacement.setSuffix(u'\N{DEGREE SIGN}')
|
||
|
|
colorful = QCheckBox("Colorful Gradient")
|
||
|
|
colorful.setToolTip("Gradient colors will always be at max chroma")
|
||
|
|
colorful.setChecked(channel.colorful)
|
||
|
|
colorful.toggled.connect(channel.colorfulHue)
|
||
|
|
tabLayout.addStretch()
|
||
|
|
tabLayout.addWidget(colorful)
|
||
|
|
else:
|
||
|
|
interval.setMaximum(100.0)
|
||
|
|
interval.setSuffix('%')
|
||
|
|
displacement.setSuffix('%')
|
||
|
|
|
||
|
|
interval.setValue(channel.slider.interval)
|
||
|
|
interval.valueChanged.connect(channel.slider.setInterval)
|
||
|
|
displacement.setValue(channel.slider.displacement)
|
||
|
|
displacement.valueChanged.connect(channel.slider.setDisplacement)
|
||
|
|
|
||
|
|
tabs.addTab(tab, param)
|
||
|
|
checkBox = QCheckBox()
|
||
|
|
checkBox.setChecked(not((model in visible) and (name in hidden)))
|
||
|
|
tab.setEnabled(checkBox.isChecked())
|
||
|
|
self.checkBoxes[name] = checkBox
|
||
|
|
tabs.tabBar().setTabButton(tabs.tabBar().count() - 1,
|
||
|
|
tabs.tabBar().ButtonPosition.LeftSide, checkBox)
|
||
|
|
checkBox.toggled.connect(tab.setEnabled)
|
||
|
|
checkBox.stateChanged.connect(self.reorderSliders)
|
||
|
|
|
||
|
|
tabs.tabBar().tabMoved.connect(self.reorderSliders)
|
||
|
|
self.pages.addWidget(tabs)
|
||
|
|
self.pageList.addItem(model)
|
||
|
|
item = self.pageList.item(self.pageList.count() - 1)
|
||
|
|
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
|
||
|
|
item.setCheckState(Qt.CheckState.Checked) if model in visible else item.setCheckState(
|
||
|
|
Qt.CheckState.Unchecked)
|
||
|
|
tabs.setEnabled(item.checkState() == Qt.CheckState.Checked)
|
||
|
|
self.pageList.model().rowsMoved.connect(self.reorderSliders)
|
||
|
|
self.pageList.itemPressed.connect(self.changePage)
|
||
|
|
self.pageList.currentTextChanged.connect(self.changePage)
|
||
|
|
self.pageList.itemChanged.connect(self.toggleModel)
|
||
|
|
|
||
|
|
self.others = QPushButton("Others")
|
||
|
|
self.others.setAutoDefault(False)
|
||
|
|
self.others.setCheckable(True)
|
||
|
|
self.others.setFixedWidth(76)
|
||
|
|
self.others.clicked.connect(self.changeOthers)
|
||
|
|
|
||
|
|
history = QGroupBox("Color History")
|
||
|
|
history.setFixedHeight(64)
|
||
|
|
history.setToolTip("Records foreground color when changed")
|
||
|
|
history.setCheckable(True)
|
||
|
|
history.setChecked(self.hcl.history.isEnabled())
|
||
|
|
history.toggled.connect(self.refreshOthers)
|
||
|
|
memory = QSpinBox()
|
||
|
|
memory.setFixedWidth(72)
|
||
|
|
memory.setMaximum(999)
|
||
|
|
memory.setValue(self.hcl.memory)
|
||
|
|
memory.valueChanged.connect(self.hcl.setMemory)
|
||
|
|
memoryLabel = QLabel("Memory")
|
||
|
|
memoryLabel.setToolTip("Limits color history, set to 0 for unlimited")
|
||
|
|
clearButton = QPushButton("Clear History")
|
||
|
|
clearButton.setAutoDefault(False)
|
||
|
|
clearButton.setToolTip("Removes all colors in history")
|
||
|
|
clearButton.clicked.connect(self.hcl.clearHistory)
|
||
|
|
historyLayout = QHBoxLayout()
|
||
|
|
historyLayout.addWidget(memory)
|
||
|
|
historyLayout.addWidget(memoryLabel)
|
||
|
|
historyLayout.addWidget(clearButton)
|
||
|
|
history.setLayout(historyLayout)
|
||
|
|
syntax = QCheckBox("Color Syntax")
|
||
|
|
syntax.setToolTip("Panel for hex/oklab/oklch css syntax")
|
||
|
|
syntax.setChecked(self.hcl.syntax.isEnabled())
|
||
|
|
syntax.stateChanged.connect(self.refreshOthers)
|
||
|
|
|
||
|
|
othersTab = QWidget()
|
||
|
|
pageLayout = QVBoxLayout()
|
||
|
|
pageLayout.addSpacing(12)
|
||
|
|
pageLayout.addWidget(history)
|
||
|
|
pageLayout.addStretch()
|
||
|
|
pageLayout.addWidget(syntax)
|
||
|
|
pageLayout.addStretch()
|
||
|
|
othersTab.setLayout(pageLayout)
|
||
|
|
othersPage = QTabWidget()
|
||
|
|
othersPage.addTab(othersTab, "Other Settings")
|
||
|
|
self.pages.addWidget(othersPage)
|
||
|
|
|
||
|
|
listLayout = QVBoxLayout()
|
||
|
|
listLayout.addWidget(self.pageList)
|
||
|
|
listLayout.addWidget(self.others)
|
||
|
|
self.mainLayout.addLayout(listLayout)
|
||
|
|
self.mainLayout.addWidget(self.pages)
|
||
|
|
|
||
|
|
def changePage(self, item: str|QListWidgetItem):
|
||
|
|
if isinstance(item, QListWidgetItem):
|
||
|
|
item = item.text()
|
||
|
|
self.pages.setCurrentIndex(list(self.models.keys()).index(item))
|
||
|
|
self.others.setChecked(False)
|
||
|
|
|
||
|
|
def changeOthers(self):
|
||
|
|
self.others.setChecked(True)
|
||
|
|
self.pages.setCurrentIndex(self.pages.count() - 1)
|
||
|
|
self.pageList.clearSelection()
|
||
|
|
|
||
|
|
def refreshOthers(self, state: bool|int):
|
||
|
|
# toggled vs stateChanged
|
||
|
|
if isinstance(state, bool):
|
||
|
|
self.hcl.history.setEnabled(state)
|
||
|
|
else:
|
||
|
|
state = state == Qt.CheckState.Checked
|
||
|
|
self.hcl.syntax.setEnabled(state)
|
||
|
|
# Refresh hcl layout
|
||
|
|
self.hcl.clearOthers()
|
||
|
|
self.hcl.displayOthers()
|
||
|
|
|
||
|
|
def reorderSliders(self):
|
||
|
|
# Get new display order
|
||
|
|
self.hcl.displayOrder = []
|
||
|
|
for row in range(self.pageList.count()):
|
||
|
|
item = self.pageList.item(row)
|
||
|
|
if item.checkState() == Qt.CheckState.Checked:
|
||
|
|
model = item.text()
|
||
|
|
tabs = self.pages.widget(list(self.models.keys()).index(model))
|
||
|
|
for index in range(tabs.count()):
|
||
|
|
# visible tabs have '&' in text used for shortcut
|
||
|
|
param = tabs.tabText(index).replace('&', '')
|
||
|
|
name = f"{model.lower()}{param}"
|
||
|
|
if self.checkBoxes[name].isChecked():
|
||
|
|
self.hcl.displayOrder.append(name)
|
||
|
|
# Refresh channel layout
|
||
|
|
self.hcl.clearChannels()
|
||
|
|
self.hcl.displayChannels()
|
||
|
|
|
||
|
|
def toggleModel(self, item: QListWidgetItem):
|
||
|
|
tabs = self.pages.widget(list(self.models.keys()).index(item.text()))
|
||
|
|
tabs.setEnabled(item.checkState() == Qt.CheckState.Checked)
|
||
|
|
|
||
|
|
self.reorderSliders()
|
||
|
|
|
||
|
|
def closeEvent(self, event):
|
||
|
|
self.hcl.writeSettings()
|
||
|
|
event.accept()
|
||
|
|
|
||
|
|
|
||
|
|
class HCLSliders(DockWidget):
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
super().__init__()
|
||
|
|
self.setWindowTitle(DOCKER_NAME)
|
||
|
|
mainWidget = QWidget(self)
|
||
|
|
mainWidget.setContentsMargins(2, 1, 2, 1)
|
||
|
|
self.setWidget(mainWidget)
|
||
|
|
self.mainLayout = QVBoxLayout(mainWidget)
|
||
|
|
self.mainLayout.setSpacing(2)
|
||
|
|
self.config = None
|
||
|
|
self.document = None
|
||
|
|
self.memory = 30
|
||
|
|
self.trc = "sRGB"
|
||
|
|
self.notation = NOTATION[0]
|
||
|
|
self.text = ""
|
||
|
|
self.pressed = False
|
||
|
|
self.editing = False
|
||
|
|
self.pastColors = []
|
||
|
|
self.loadChannels()
|
||
|
|
self.history = ColorHistory(self)
|
||
|
|
self.loadSyntax()
|
||
|
|
self.readSettings()
|
||
|
|
self.displayChannels()
|
||
|
|
self.displayOthers()
|
||
|
|
self.updateNotations()
|
||
|
|
|
||
|
|
def colorDisplay(self):
|
||
|
|
# load into channel layout to prevent alignment issue when channels empty
|
||
|
|
layout = QHBoxLayout()
|
||
|
|
layout.setSpacing(2)
|
||
|
|
|
||
|
|
self.color = ColorDisplay(self)
|
||
|
|
self.color.setFixedHeight(DISPLAY_HEIGHT)
|
||
|
|
layout.addWidget(self.color)
|
||
|
|
|
||
|
|
button = QPushButton()
|
||
|
|
button.setIcon(Application.icon('configure'))
|
||
|
|
button.setFlat(True)
|
||
|
|
button.setFixedSize(DISPLAY_HEIGHT, DISPLAY_HEIGHT)
|
||
|
|
button.setIconSize(QSize(DISPLAY_HEIGHT - 2, DISPLAY_HEIGHT - 2))
|
||
|
|
button.setToolTip("Configure HCL Sliders")
|
||
|
|
button.clicked.connect(self.openConfig)
|
||
|
|
layout.addWidget(button)
|
||
|
|
|
||
|
|
self.timer = QTimer()
|
||
|
|
self.timer.timeout.connect(self.getKritaColors)
|
||
|
|
self.singleShot = QTimer()
|
||
|
|
self.singleShot.setSingleShot(True)
|
||
|
|
self.singleShot.timeout.connect(self.setHistory)
|
||
|
|
return layout
|
||
|
|
|
||
|
|
def loadChannels(self):
|
||
|
|
self.channelLayout = QVBoxLayout()
|
||
|
|
self.channelLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||
|
|
self.channelLayout.setSpacing(2)
|
||
|
|
self.channelLayout.addLayout(self.colorDisplay())
|
||
|
|
self.channelLayout.addSpacing(1)
|
||
|
|
|
||
|
|
self.hsvHue = ColorChannel("hsvHue", self)
|
||
|
|
self.hsvSaturation = ColorChannel("hsvSaturation", self)
|
||
|
|
self.hsvValue = ColorChannel("hsvValue", self)
|
||
|
|
|
||
|
|
self.hslHue = ColorChannel("hslHue", self)
|
||
|
|
self.hslSaturation = ColorChannel("hslSaturation", self)
|
||
|
|
self.hslLightness = ColorChannel("hslLightness", self)
|
||
|
|
|
||
|
|
self.hcyHue = ColorChannel("hcyHue", self)
|
||
|
|
self.hcyHue.scale = False
|
||
|
|
self.hcyChroma = ColorChannel("hcyChroma", self)
|
||
|
|
self.hcyLuma = ColorChannel("hcyLuma", self)
|
||
|
|
self.hcyLuma.scale = False
|
||
|
|
|
||
|
|
self.okhclHue = ColorChannel("okhclHue", self)
|
||
|
|
self.okhclHue.scale = False
|
||
|
|
self.okhclChroma = ColorChannel("okhclChroma", self)
|
||
|
|
self.okhclLightness = ColorChannel("okhclLightness", self)
|
||
|
|
self.okhclLightness.scale = False
|
||
|
|
|
||
|
|
self.okhsvHue = ColorChannel("okhsvHue", self)
|
||
|
|
self.okhsvSaturation = ColorChannel("okhsvSaturation", self)
|
||
|
|
self.okhsvValue = ColorChannel("okhsvValue", self)
|
||
|
|
|
||
|
|
self.okhslHue = ColorChannel("okhslHue", self)
|
||
|
|
self.okhslSaturation = ColorChannel("okhslSaturation", self)
|
||
|
|
self.okhslLightness = ColorChannel("okhslLightness", self)
|
||
|
|
|
||
|
|
self.mainLayout.addLayout(self.channelLayout)
|
||
|
|
|
||
|
|
def loadSyntax(self):
|
||
|
|
self.prevNotation = QPushButton()
|
||
|
|
self.prevNotation.setFlat(True)
|
||
|
|
self.prevNotation.setFixedSize(CHANNEL_HEIGHT - 1, CHANNEL_HEIGHT - 1)
|
||
|
|
self.prevNotation.setIcon(Application.icon('arrow-left'))
|
||
|
|
self.prevNotation.setIconSize(QSize(CHANNEL_HEIGHT - 5, CHANNEL_HEIGHT - 5))
|
||
|
|
self.prevNotation.clicked.connect(self.switchNotation)
|
||
|
|
|
||
|
|
self.nextNotation = QPushButton()
|
||
|
|
self.nextNotation.setFlat(True)
|
||
|
|
self.nextNotation.setFixedSize(CHANNEL_HEIGHT - 1, CHANNEL_HEIGHT - 1)
|
||
|
|
self.nextNotation.setIcon(Application.icon('arrow-right'))
|
||
|
|
self.nextNotation.setIconSize(QSize(CHANNEL_HEIGHT - 5, CHANNEL_HEIGHT - 5))
|
||
|
|
self.nextNotation.clicked.connect(self.switchNotation)
|
||
|
|
|
||
|
|
self.syntax = QLineEdit()
|
||
|
|
self.syntax.setFixedHeight(CHANNEL_HEIGHT - 1)
|
||
|
|
self.syntax.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
|
|
self.syntax.editingFinished.connect(self.parseSyntax)
|
||
|
|
|
||
|
|
def readSettings(self):
|
||
|
|
channels = ColorChannel.getList()
|
||
|
|
for name in channels:
|
||
|
|
settings: list = Application.readSetting(DOCKER_NAME, name, "").split(",")
|
||
|
|
if len(settings) > 1:
|
||
|
|
channel: ColorChannel = getattr(self, name)
|
||
|
|
try:
|
||
|
|
channel.slider.setInterval(float(settings[0]))
|
||
|
|
except ValueError:
|
||
|
|
print(f"Invalid interval amount for {name}")
|
||
|
|
|
||
|
|
try:
|
||
|
|
channel.slider.setDisplacement(float(settings[1]))
|
||
|
|
except ValueError:
|
||
|
|
print(f"Invalid displacement amount for {name}")
|
||
|
|
|
||
|
|
if (name[:3] == "hcy" or name[:5] == "okhcl") and name[-6:] != "Chroma":
|
||
|
|
channel.scale = settings[2] == "True"
|
||
|
|
|
||
|
|
if name[-3:] == "Hue":
|
||
|
|
if len(settings) > 3:
|
||
|
|
channel.colorful = settings[3] == "True"
|
||
|
|
else:
|
||
|
|
channel.colorful = settings[2] == "True"
|
||
|
|
|
||
|
|
if name[:3] == "hcy":
|
||
|
|
channel.luma = settings[-1] == "True"
|
||
|
|
|
||
|
|
self.displayOrder = []
|
||
|
|
empty = False
|
||
|
|
displayed = Application.readSetting(DOCKER_NAME, "displayed", "").split(",")
|
||
|
|
for name in displayed:
|
||
|
|
if name in channels:
|
||
|
|
self.displayOrder.append(name)
|
||
|
|
elif name == "None":
|
||
|
|
empty = True
|
||
|
|
break
|
||
|
|
if not self.displayOrder and not empty:
|
||
|
|
self.displayOrder = channels
|
||
|
|
|
||
|
|
history = Application.readSetting(DOCKER_NAME, "history", "").split(",")
|
||
|
|
if len(history) == 2:
|
||
|
|
self.history.setEnabled(history[0] != "False")
|
||
|
|
try:
|
||
|
|
memory = int(history[1])
|
||
|
|
if 0 <= memory <= 999:
|
||
|
|
self.memory = memory
|
||
|
|
except ValueError:
|
||
|
|
("Invalid memory value")
|
||
|
|
|
||
|
|
syntax = Application.readSetting(DOCKER_NAME, "syntax", "").split(",")
|
||
|
|
if len(syntax) == 2:
|
||
|
|
self.syntax.setEnabled(syntax[0] != "False")
|
||
|
|
notation = syntax[1]
|
||
|
|
if notation in NOTATION:
|
||
|
|
self.notation = notation
|
||
|
|
|
||
|
|
def writeSettings(self):
|
||
|
|
Application.writeSetting(DOCKER_NAME, "displayed", ",".join(self.displayOrder) if self.displayOrder else "None")
|
||
|
|
|
||
|
|
for name in ColorChannel.getList():
|
||
|
|
settings = []
|
||
|
|
channel: ColorChannel = getattr(self, name)
|
||
|
|
settings.append(str(channel.slider.interval))
|
||
|
|
settings.append(str(channel.slider.displacement))
|
||
|
|
|
||
|
|
if (name[:3] == "hcy" or name[:5] == "okhcl") and name[-6:] != "Chroma":
|
||
|
|
settings.append(str(channel.scale))
|
||
|
|
if name[-3:] == "Hue":
|
||
|
|
settings.append(str(channel.colorful))
|
||
|
|
if name[:3] == "hcy":
|
||
|
|
settings.append(str(channel.luma))
|
||
|
|
|
||
|
|
Application.writeSetting(DOCKER_NAME, name, ",".join(settings))
|
||
|
|
|
||
|
|
history = [str(self.history.isEnabled()), str(self.memory)]
|
||
|
|
Application.writeSetting(DOCKER_NAME, "history", ",".join(history))
|
||
|
|
|
||
|
|
syntax = [str(self.syntax.isEnabled()), self.notation]
|
||
|
|
Application.writeSetting(DOCKER_NAME, "syntax", ",".join(syntax))
|
||
|
|
|
||
|
|
def displayChannels(self):
|
||
|
|
for name in self.displayOrder:
|
||
|
|
channel = getattr(self, name)
|
||
|
|
channel.layout.addWidget(channel.label)
|
||
|
|
channel.layout.addWidget(channel.slider)
|
||
|
|
channel.layout.addWidget(channel.spinBox)
|
||
|
|
self.channelLayout.addLayout(channel.layout)
|
||
|
|
|
||
|
|
def clearChannels(self):
|
||
|
|
# first 2 items in channelLayout is color display and spacing
|
||
|
|
for i in reversed(range(self.channelLayout.count() - 2)):
|
||
|
|
item = self.channelLayout.itemAt(i + 2)
|
||
|
|
layout = item.layout()
|
||
|
|
for index in reversed(range(layout.count())):
|
||
|
|
widget = layout.itemAt(index).widget()
|
||
|
|
layout.removeWidget(widget)
|
||
|
|
widget.setParent(None)
|
||
|
|
self.channelLayout.removeItem(item)
|
||
|
|
|
||
|
|
def displayOthers(self):
|
||
|
|
if self.history.isEnabled():
|
||
|
|
self.mainLayout.addSpacing(1)
|
||
|
|
self.mainLayout.addWidget(self.history)
|
||
|
|
if self.syntax.isEnabled():
|
||
|
|
self.mainLayout.addSpacing(1)
|
||
|
|
syntaxLayout = QHBoxLayout()
|
||
|
|
syntaxLayout.addWidget(self.prevNotation)
|
||
|
|
syntaxLayout.addWidget(self.syntax)
|
||
|
|
syntaxLayout.addWidget(self.nextNotation)
|
||
|
|
self.mainLayout.addLayout(syntaxLayout)
|
||
|
|
|
||
|
|
def clearOthers(self):
|
||
|
|
# first item in mainLayout is channelLayout
|
||
|
|
for i in reversed(range(self.mainLayout.count() - 1)):
|
||
|
|
item = self.mainLayout.itemAt(i + 1)
|
||
|
|
widget = item.widget()
|
||
|
|
if widget:
|
||
|
|
self.mainLayout.removeWidget(widget)
|
||
|
|
widget.setParent(None)
|
||
|
|
else:
|
||
|
|
layout = item.layout()
|
||
|
|
if layout:
|
||
|
|
for index in reversed(range(layout.count())):
|
||
|
|
widget = layout.itemAt(index).widget()
|
||
|
|
layout.removeWidget(widget)
|
||
|
|
widget.setParent(None)
|
||
|
|
self.mainLayout.removeItem(item)
|
||
|
|
|
||
|
|
def openConfig(self):
|
||
|
|
if self.config is None:
|
||
|
|
self.config = SliderConfig(self)
|
||
|
|
self.config.show()
|
||
|
|
|
||
|
|
def profileTRC(self, profile: str):
|
||
|
|
if profile in SRGB:
|
||
|
|
return "sRGB"
|
||
|
|
elif profile in LINEAR:
|
||
|
|
return "linear"
|
||
|
|
print("Incompatible profile")
|
||
|
|
return self.trc
|
||
|
|
|
||
|
|
def setMemory(self, memory: int):
|
||
|
|
self.memory = memory
|
||
|
|
|
||
|
|
def setPressed(self, pressed: bool):
|
||
|
|
self.pressed = pressed
|
||
|
|
|
||
|
|
def finishEditing(self):
|
||
|
|
self.editing = False
|
||
|
|
|
||
|
|
def getKritaColors(self):
|
||
|
|
view = Application.activeWindow().activeView()
|
||
|
|
if not view.visible():
|
||
|
|
return
|
||
|
|
|
||
|
|
if not self.pressed and not self.editing:
|
||
|
|
# add to color history after slider sets color
|
||
|
|
if self.color.isChanged() and self.color.current:
|
||
|
|
self.setHistory()
|
||
|
|
|
||
|
|
foreground = view.foregroundColor()
|
||
|
|
self.color.setForeGroundColor(foreground)
|
||
|
|
|
||
|
|
if self.color.isChanged():
|
||
|
|
self.color.setCurrentColor(foreground)
|
||
|
|
rgb = tuple(foreground.componentsOrdered()[:3])
|
||
|
|
trc = self.profileTRC(foreground.colorProfile())
|
||
|
|
self.updateSyntax(rgb, trc)
|
||
|
|
if trc != self.trc:
|
||
|
|
rgb = Convert.rgbToTRC(rgb, self.trc)
|
||
|
|
self.updateChannels(rgb)
|
||
|
|
# add to color history after krita changes color
|
||
|
|
if not self.singleShot.isActive():
|
||
|
|
self.color.recent = foreground
|
||
|
|
self.singleShot.start(DELAY)
|
||
|
|
|
||
|
|
def blockChannels(self, block: bool):
|
||
|
|
# hsv
|
||
|
|
self.hsvHue.blockSignals(block)
|
||
|
|
self.hsvSaturation.blockSignals(block)
|
||
|
|
self.hsvValue.blockSignals(block)
|
||
|
|
# hsl
|
||
|
|
self.hslHue.blockSignals(block)
|
||
|
|
self.hslSaturation.blockSignals(block)
|
||
|
|
self.hslLightness.blockSignals(block)
|
||
|
|
# hcy
|
||
|
|
self.hcyHue.blockSignals(block)
|
||
|
|
self.hcyChroma.blockSignals(block)
|
||
|
|
self.hcyLuma.blockSignals(block)
|
||
|
|
# okhcl
|
||
|
|
self.okhclHue.blockSignals(block)
|
||
|
|
self.okhclChroma.blockSignals(block)
|
||
|
|
self.okhclLightness.blockSignals(block)
|
||
|
|
# okhsv
|
||
|
|
self.okhsvHue.blockSignals(block)
|
||
|
|
self.okhsvSaturation.blockSignals(block)
|
||
|
|
self.okhsvValue.blockSignals(block)
|
||
|
|
# okhsl
|
||
|
|
self.okhslHue.blockSignals(block)
|
||
|
|
self.okhslSaturation.blockSignals(block)
|
||
|
|
self.okhslLightness.blockSignals(block)
|
||
|
|
|
||
|
|
def updateChannels(self, values: tuple|float, name: str=None, widget: str=None):
|
||
|
|
self.timer.stop()
|
||
|
|
self.blockChannels(True)
|
||
|
|
|
||
|
|
if type(values) is tuple:
|
||
|
|
# update color from krita that is not adjusted by this plugin
|
||
|
|
self.setChannelValues("hsv", values)
|
||
|
|
self.setChannelValues("hsl", values)
|
||
|
|
self.setChannelValues("hcy", values)
|
||
|
|
self.setChannelValues("okhcl", values)
|
||
|
|
self.setChannelValues("okhsv", values)
|
||
|
|
self.setChannelValues("okhsl", values)
|
||
|
|
else:
|
||
|
|
# update slider if spinbox adjusted vice versa
|
||
|
|
channel: ColorChannel = getattr(self, name)
|
||
|
|
channelWidget = getattr(channel, widget)
|
||
|
|
channelWidget.setValue(values)
|
||
|
|
if widget == "slider":
|
||
|
|
# prevent getKritaColors when still editing spinBox
|
||
|
|
self.editing = True
|
||
|
|
# adjusting hsv sliders
|
||
|
|
if name[:3] == "hsv":
|
||
|
|
hue = self.hsvHue.value()
|
||
|
|
rgb = Convert.hsvToRgbF(hue, self.hsvSaturation.value(),
|
||
|
|
self.hsvValue.value(), self.trc)
|
||
|
|
self.setKritaFGColor(rgb)
|
||
|
|
self.setChannelValues("hsl", rgb, hue)
|
||
|
|
if self.hcyLuma.luma or self.trc == "sRGB":
|
||
|
|
self.setChannelValues("hcy", rgb, hue)
|
||
|
|
else:
|
||
|
|
self.setChannelValues("hcy", rgb)
|
||
|
|
self.setChannelValues("okhcl", rgb)
|
||
|
|
self.setChannelValues("okhsv", rgb)
|
||
|
|
self.setChannelValues("okhsl", rgb)
|
||
|
|
# adjusting hsl sliders
|
||
|
|
elif name[:3] == "hsl":
|
||
|
|
hue = self.hslHue.value()
|
||
|
|
rgb = Convert.hslToRgbF(hue, self.hslSaturation.value(),
|
||
|
|
self.hslLightness.value(), self.trc)
|
||
|
|
self.setKritaFGColor(rgb)
|
||
|
|
self.setChannelValues("hsv", rgb, hue)
|
||
|
|
if self.hcyLuma.luma or self.trc == "sRGB":
|
||
|
|
self.setChannelValues("hcy", rgb, hue)
|
||
|
|
else:
|
||
|
|
self.setChannelValues("hcy", rgb)
|
||
|
|
self.setChannelValues("okhcl", rgb)
|
||
|
|
self.setChannelValues("okhsv", rgb)
|
||
|
|
self.setChannelValues("okhsl", rgb)
|
||
|
|
# adjusting hcy sliders
|
||
|
|
elif name[:3] == "hcy":
|
||
|
|
hue = self.hcyHue.value()
|
||
|
|
chroma = self.hcyChroma.value()
|
||
|
|
limit = -1
|
||
|
|
if channel.scale:
|
||
|
|
if self.hcyChroma.limit > 0:
|
||
|
|
self.hcyChroma.clip = chroma
|
||
|
|
limit = self.hcyChroma.limit
|
||
|
|
else:
|
||
|
|
if self.hcyChroma.clip == 0:
|
||
|
|
self.hcyChroma.clip = chroma
|
||
|
|
else:
|
||
|
|
chroma = self.hcyChroma.clip
|
||
|
|
rgb = Convert.hcyToRgbF(hue, chroma, self.hcyLuma.value(),
|
||
|
|
limit, self.trc, channel.luma)
|
||
|
|
self.setKritaFGColor(rgb)
|
||
|
|
if name[-6:] != "Chroma":
|
||
|
|
hcy = Convert.rgbFToHcy(*rgb, hue, self.trc, channel.luma)
|
||
|
|
self.hcyChroma.setLimit(hcy[3])
|
||
|
|
self.hcyChroma.setValue(hcy[1])
|
||
|
|
# relative luminance doesnt match luma in hue
|
||
|
|
if channel.luma or self.trc == "sRGB":
|
||
|
|
self.setChannelValues("hsv", rgb, hue)
|
||
|
|
self.setChannelValues("hsl", rgb, hue)
|
||
|
|
else:
|
||
|
|
self.setChannelValues("hsv", rgb)
|
||
|
|
self.setChannelValues("hsl", rgb)
|
||
|
|
self.setChannelValues("okhcl", rgb)
|
||
|
|
self.setChannelValues("okhsv", rgb)
|
||
|
|
self.setChannelValues("okhsl", rgb)
|
||
|
|
# adjusting okhcl sliders
|
||
|
|
elif name[:5] == "okhcl":
|
||
|
|
hue = self.okhclHue.value()
|
||
|
|
chroma = self.okhclChroma.value()
|
||
|
|
limit = -1
|
||
|
|
if channel.scale:
|
||
|
|
if self.okhclChroma.limit > 0:
|
||
|
|
self.okhclChroma.clip = chroma
|
||
|
|
limit = self.okhclChroma.limit
|
||
|
|
else:
|
||
|
|
if self.okhclChroma.clip == 0:
|
||
|
|
self.okhclChroma.clip = chroma
|
||
|
|
else:
|
||
|
|
chroma = self.okhclChroma.clip
|
||
|
|
rgb = Convert.okhclToRgbF(hue, chroma, self.okhclLightness.value(), limit, self.trc)
|
||
|
|
self.setKritaFGColor(rgb)
|
||
|
|
if name[-6:] != "Chroma":
|
||
|
|
okhcl = Convert.rgbFToOkhcl(*rgb, hue, self.trc)
|
||
|
|
self.okhclChroma.setLimit(okhcl[3])
|
||
|
|
self.okhclChroma.setValue(okhcl[1])
|
||
|
|
self.setChannelValues("hsv", rgb)
|
||
|
|
self.setChannelValues("hsl", rgb)
|
||
|
|
self.setChannelValues("hcy", rgb)
|
||
|
|
self.setChannelValues("okhsv", rgb, hue)
|
||
|
|
self.setChannelValues("okhsl", rgb, hue)
|
||
|
|
# adjusting okhsv sliders
|
||
|
|
elif name[:5] == "okhsv":
|
||
|
|
hue = self.okhsvHue.value()
|
||
|
|
rgb = Convert.okhsvToRgbF(hue, self.okhsvSaturation.value(),
|
||
|
|
self.okhsvValue.value(), self.trc)
|
||
|
|
self.setKritaFGColor(rgb)
|
||
|
|
self.setChannelValues("hsv", rgb)
|
||
|
|
self.setChannelValues("hsl", rgb)
|
||
|
|
self.setChannelValues("hcy", rgb)
|
||
|
|
self.setChannelValues("okhcl", rgb, hue)
|
||
|
|
self.setChannelValues("okhsl", rgb, hue)
|
||
|
|
# adjusting okhsl sliders
|
||
|
|
elif name[:5] == "okhsl":
|
||
|
|
hue = self.okhslHue.value()
|
||
|
|
rgb = Convert.okhslToRgbF(hue, self.okhslSaturation.value(),
|
||
|
|
self.okhslLightness.value(), self.trc)
|
||
|
|
self.setKritaFGColor(rgb)
|
||
|
|
self.setChannelValues("hsv", rgb)
|
||
|
|
self.setChannelValues("hsl", rgb)
|
||
|
|
self.setChannelValues("hcy", rgb)
|
||
|
|
self.setChannelValues("okhcl", rgb, hue)
|
||
|
|
self.setChannelValues("okhsv", rgb, hue)
|
||
|
|
|
||
|
|
self.updateChannelGradients()
|
||
|
|
self.blockChannels(False)
|
||
|
|
if TIME:
|
||
|
|
self.timer.start(TIME)
|
||
|
|
|
||
|
|
def updateChannelGradients(self, channels: str=None):
|
||
|
|
if not channels or channels == "hsv":
|
||
|
|
self.hsvHue.updateGradientColors(self.hsvSaturation.value(), self.hsvValue.value(),
|
||
|
|
self.trc)
|
||
|
|
self.hsvSaturation.updateGradientColors(self.hsvHue.value(), self.hsvValue.value(),
|
||
|
|
self.trc)
|
||
|
|
self.hsvValue.updateGradientColors(self.hsvHue.value(), self.hsvSaturation.value(),
|
||
|
|
self.trc)
|
||
|
|
if not channels or channels == "hsl":
|
||
|
|
self.hslHue.updateGradientColors(self.hslSaturation.value(), self.hslLightness.value(),
|
||
|
|
self.trc)
|
||
|
|
self.hslSaturation.updateGradientColors(self.hslHue.value(), self.hslLightness.value(),
|
||
|
|
self.trc)
|
||
|
|
self.hslLightness.updateGradientColors(self.hslHue.value(), self.hslSaturation.value(),
|
||
|
|
self.trc)
|
||
|
|
if not channels or channels == "hcy":
|
||
|
|
hcyClip = self.hcyChroma.value()
|
||
|
|
if self.hcyChroma.clip > 0:
|
||
|
|
hcyClip = self.hcyChroma.clip
|
||
|
|
if self.hcyHue.scale:
|
||
|
|
self.hcyHue.updateGradientColors(self.hcyChroma.value(), self.hcyLuma.value(),
|
||
|
|
self.trc, self.hcyChroma.limit)
|
||
|
|
else:
|
||
|
|
self.hcyHue.updateGradientColors(hcyClip, self.hcyLuma.value(), self.trc)
|
||
|
|
self.hcyChroma.updateGradientColors(self.hcyHue.value(), self.hcyLuma.value(),
|
||
|
|
self.trc, self.hcyChroma.limit)
|
||
|
|
if self.hcyLuma.scale:
|
||
|
|
self.hcyLuma.updateGradientColors(self.hcyHue.value(), self.hcyChroma.value(),
|
||
|
|
self.trc, self.hcyChroma.limit)
|
||
|
|
else:
|
||
|
|
self.hcyLuma.updateGradientColors(self.hcyHue.value(), hcyClip, self.trc)
|
||
|
|
if not channels or channels == "okhcl":
|
||
|
|
okhclClip = self.okhclChroma.value()
|
||
|
|
if self.okhclChroma.clip > 0:
|
||
|
|
okhclClip = self.okhclChroma.clip
|
||
|
|
if self.okhclHue.scale:
|
||
|
|
self.okhclHue.updateGradientColors(self.okhclChroma.value(), self.okhclLightness.value(),
|
||
|
|
self.trc, self.okhclChroma.limit)
|
||
|
|
else:
|
||
|
|
self.okhclHue.updateGradientColors(okhclClip, self.okhclLightness.value(), self.trc)
|
||
|
|
self.okhclChroma.updateGradientColors(self.okhclHue.value(), self.okhclLightness.value(),
|
||
|
|
self.trc, self.okhclChroma.limit)
|
||
|
|
if self.okhclLightness.scale:
|
||
|
|
self.okhclLightness.updateGradientColors(self.okhclHue.value(), self.okhclChroma.value(),
|
||
|
|
self.trc, self.okhclChroma.limit)
|
||
|
|
else:
|
||
|
|
self.okhclLightness.updateGradientColors(self.okhclHue.value(), okhclClip, self.trc)
|
||
|
|
if not channels or channels == "okhsv":
|
||
|
|
self.okhsvHue.updateGradientColors(self.okhsvSaturation.value(),
|
||
|
|
self.okhsvValue.value(), self.trc)
|
||
|
|
self.okhsvSaturation.updateGradientColors(self.okhsvHue.value(),
|
||
|
|
self.okhsvValue.value(), self.trc)
|
||
|
|
self.okhsvValue.updateGradientColors(self.okhsvHue.value(),
|
||
|
|
self.okhsvSaturation.value(), self.trc)
|
||
|
|
if not channels or channels == "okhsl":
|
||
|
|
self.okhslHue.updateGradientColors(self.okhslSaturation.value(),
|
||
|
|
self.okhslLightness.value(), self.trc)
|
||
|
|
self.okhslSaturation.updateGradientColors(self.okhslHue.value(),
|
||
|
|
self.okhslLightness.value(), self.trc)
|
||
|
|
self.okhslLightness.updateGradientColors(self.okhslHue.value(),
|
||
|
|
self.okhslSaturation.value(), self.trc)
|
||
|
|
|
||
|
|
def setChannelValues(self, channels: str, rgb: tuple, hue: float=-1):
|
||
|
|
if channels == "hsv":
|
||
|
|
hsv = Convert.rgbFToHsv(*rgb, self.trc)
|
||
|
|
if hue != -1:
|
||
|
|
self.hsvHue.setValue(hue)
|
||
|
|
if hsv[2] == 0:
|
||
|
|
self.hsvValue.setValue(0)
|
||
|
|
elif hsv[1] == 0:
|
||
|
|
self.hsvSaturation.setValue(0)
|
||
|
|
self.hsvValue.setValue(hsv[2])
|
||
|
|
else:
|
||
|
|
if hue == -1:
|
||
|
|
self.hsvHue.setValue(hsv[0])
|
||
|
|
self.hsvSaturation.setValue(hsv[1])
|
||
|
|
self.hsvValue.setValue(hsv[2])
|
||
|
|
elif channels == "hsl":
|
||
|
|
hsl = Convert.rgbFToHsl(*rgb, self.trc)
|
||
|
|
if hue != -1:
|
||
|
|
self.hslHue.setValue(hue)
|
||
|
|
if hsl[2] == 0 or hsl[2] == 1:
|
||
|
|
self.hslLightness.setValue(hsl[2])
|
||
|
|
elif hsl[1] == 0:
|
||
|
|
self.hslSaturation.setValue(0)
|
||
|
|
self.hslLightness.setValue(hsl[2])
|
||
|
|
else:
|
||
|
|
if hue == -1:
|
||
|
|
self.hslHue.setValue(hsl[0])
|
||
|
|
self.hslSaturation.setValue(hsl[1])
|
||
|
|
self.hslLightness.setValue(hsl[2])
|
||
|
|
elif channels == "hcy":
|
||
|
|
self.hcyChroma.clip = 0.0
|
||
|
|
hcy = Convert.rgbFToHcy(*rgb, self.hcyHue.value(), self.trc, self.hcyLuma.luma)
|
||
|
|
if hue != -1:
|
||
|
|
self.hcyHue.setValue(hue)
|
||
|
|
if hcy[1] == 0:
|
||
|
|
self.hcyChroma.setLimit(hcy[3])
|
||
|
|
self.hcyChroma.setValue(hcy[1])
|
||
|
|
self.hcyLuma.setValue(hcy[2])
|
||
|
|
else:
|
||
|
|
if hue == -1:
|
||
|
|
self.hcyHue.setValue(hcy[0])
|
||
|
|
# must always set limit before setting chroma value
|
||
|
|
self.hcyChroma.setLimit(hcy[3])
|
||
|
|
self.hcyChroma.setValue(hcy[1])
|
||
|
|
self.hcyLuma.setValue(hcy[2])
|
||
|
|
elif channels == "okhcl":
|
||
|
|
self.okhclChroma.clip = 0.0
|
||
|
|
okhcl = Convert.rgbFToOkhcl(*rgb, self.okhclHue.value(), self.trc)
|
||
|
|
if hue != -1:
|
||
|
|
self.okhclHue.setValue(hue)
|
||
|
|
else:
|
||
|
|
self.okhclHue.setValue(okhcl[0])
|
||
|
|
# must always set limit before setting chroma value
|
||
|
|
self.okhclChroma.setLimit(okhcl[3])
|
||
|
|
self.okhclChroma.setValue(okhcl[1])
|
||
|
|
self.okhclLightness.setValue(okhcl[2])
|
||
|
|
elif channels == "okhsv":
|
||
|
|
okhsv = Convert.rgbFToOkhsv(*rgb, self.trc)
|
||
|
|
if hue != -1:
|
||
|
|
self.okhsvHue.setValue(hue)
|
||
|
|
if okhsv[2] == 0:
|
||
|
|
self.okhsvValue.setValue(0)
|
||
|
|
elif okhsv[1] == 0:
|
||
|
|
self.okhsvSaturation.setValue(0)
|
||
|
|
self.okhsvValue.setValue(okhsv[2])
|
||
|
|
else:
|
||
|
|
if hue == -1:
|
||
|
|
self.okhsvHue.setValue(okhsv[0])
|
||
|
|
self.okhsvSaturation.setValue(okhsv[1])
|
||
|
|
self.okhsvValue.setValue(okhsv[2])
|
||
|
|
elif channels == "okhsl":
|
||
|
|
okhsl = Convert.rgbFToOkhsl(*rgb, self.trc)
|
||
|
|
if hue != -1:
|
||
|
|
self.okhslHue.setValue(hue)
|
||
|
|
if okhsl[2] == 0 or okhsl[2] == 1:
|
||
|
|
self.okhslLightness.setValue(okhsl[2])
|
||
|
|
elif okhsl[1] == 0:
|
||
|
|
self.okhslSaturation.setValue(0)
|
||
|
|
self.okhslLightness.setValue(okhsl[2])
|
||
|
|
else:
|
||
|
|
if hue == -1:
|
||
|
|
self.okhslHue.setValue(okhsl[0])
|
||
|
|
self.okhslSaturation.setValue(okhsl[1])
|
||
|
|
self.okhslLightness.setValue(okhsl[2])
|
||
|
|
|
||
|
|
def makeManagedColor(self, rgb: tuple, profile: str=None):
|
||
|
|
model = self.document.colorModel()
|
||
|
|
# support for other models in the future
|
||
|
|
if model == "RGBA":
|
||
|
|
if not profile:
|
||
|
|
profile = self.document.colorProfile()
|
||
|
|
color = ManagedColor(model, self.document.colorDepth(), profile)
|
||
|
|
components = color.components()
|
||
|
|
# unordered sequence is BGRA
|
||
|
|
components[0] = rgb[2]
|
||
|
|
components[1] = rgb[1]
|
||
|
|
components[2] = rgb[0]
|
||
|
|
components[3] = 1.0
|
||
|
|
|
||
|
|
color.setComponents(components)
|
||
|
|
return color
|
||
|
|
|
||
|
|
def setKritaFGColor(self, rgb: tuple):
|
||
|
|
view = Application.activeWindow().activeView()
|
||
|
|
if not view.visible():
|
||
|
|
return
|
||
|
|
|
||
|
|
color = self.makeManagedColor(rgb)
|
||
|
|
self.color.setCurrentColor(color)
|
||
|
|
self.updateSyntax(rgb, self.trc)
|
||
|
|
view.setForeGroundColor(color)
|
||
|
|
self.color.recent = color
|
||
|
|
|
||
|
|
def setLuma(self, luma: bool):
|
||
|
|
self.timer.stop()
|
||
|
|
self.blockChannels(True)
|
||
|
|
|
||
|
|
self.hcyHue.luma = luma
|
||
|
|
self.hcyChroma.luma = luma
|
||
|
|
self.hcyLuma.luma = luma
|
||
|
|
|
||
|
|
if self.color.current:
|
||
|
|
rgb = tuple(self.color.current.componentsOrdered()[:3])
|
||
|
|
trc = self.profileTRC(self.color.current.colorProfile())
|
||
|
|
if trc != self.trc:
|
||
|
|
rgb = Convert.rgbToTRC(rgb, self.trc)
|
||
|
|
if luma or self.trc == "sRGB":
|
||
|
|
self.setChannelValues("hcy", rgb, self.hsvHue.value())
|
||
|
|
else:
|
||
|
|
self.setChannelValues("hcy", rgb)
|
||
|
|
self.updateChannelGradients("hcy")
|
||
|
|
|
||
|
|
self.blockChannels(False)
|
||
|
|
if TIME:
|
||
|
|
self.timer.start(TIME)
|
||
|
|
|
||
|
|
def setHistory(self):
|
||
|
|
if self.color.isChanging():
|
||
|
|
# allow getKritaColors to start timer for set history
|
||
|
|
self.color.current = None
|
||
|
|
return
|
||
|
|
|
||
|
|
rgb = tuple(self.color.current.componentsOrdered()[:3])
|
||
|
|
profile = self.color.current.colorProfile()
|
||
|
|
color = (rgb, profile)
|
||
|
|
if color in self.pastColors:
|
||
|
|
index = self.pastColors.index(color)
|
||
|
|
if index:
|
||
|
|
self.pastColors.pop(index)
|
||
|
|
self.pastColors.insert(0, color)
|
||
|
|
item = self.history.takeItem(index)
|
||
|
|
self.history.insertItem(0, item)
|
||
|
|
else:
|
||
|
|
self.pastColors.insert(0, color)
|
||
|
|
pixmap = QPixmap(HISTORY_HEIGHT, HISTORY_HEIGHT)
|
||
|
|
pixmap.fill(QColor(*Convert.rgbFToInt8(*rgb, self.profileTRC(profile))))
|
||
|
|
item = QListWidgetItem()
|
||
|
|
item.setIcon(QIcon(pixmap))
|
||
|
|
self.history.insertItem(0, item)
|
||
|
|
if self.memory:
|
||
|
|
for i in reversed(range(self.history.count())):
|
||
|
|
if i > self.memory - 1:
|
||
|
|
self.history.takeItem(i)
|
||
|
|
self.pastColors.pop()
|
||
|
|
else:
|
||
|
|
break
|
||
|
|
self.history.horizontalScrollBar().setValue(0)
|
||
|
|
|
||
|
|
def setPastColorToFG(self, index: int):
|
||
|
|
view = Application.activeWindow().activeView()
|
||
|
|
if not view.visible():
|
||
|
|
return
|
||
|
|
|
||
|
|
self.history.takeItem(index)
|
||
|
|
color = self.pastColors.pop(index)
|
||
|
|
rgb = color[0]
|
||
|
|
trc = self.profileTRC(color[1])
|
||
|
|
self.updateSyntax(rgb, trc)
|
||
|
|
if trc != self.trc:
|
||
|
|
rgb = Convert.rgbToTRC(rgb, self.trc)
|
||
|
|
self.updateChannels(rgb)
|
||
|
|
view.setForeGroundColor(self.color.current)
|
||
|
|
# prevent setHistory again during getKritaColors
|
||
|
|
self.color.setForeGroundColor(self.color.current)
|
||
|
|
self.color.recent = self.color.current
|
||
|
|
self.setHistory()
|
||
|
|
|
||
|
|
def setPastColorToBG(self):
|
||
|
|
view = Application.activeWindow().activeView()
|
||
|
|
if not view.visible():
|
||
|
|
return
|
||
|
|
|
||
|
|
view.setBackGroundColor(self.color.background)
|
||
|
|
|
||
|
|
def clearHistory(self):
|
||
|
|
self.history.clear()
|
||
|
|
self.pastColors = []
|
||
|
|
|
||
|
|
def updateSyntax(self, rgb: tuple, trc: str):
|
||
|
|
if self.notation == NOTATION[0]:
|
||
|
|
self.text = Convert.rgbFToHexS(*rgb, trc)
|
||
|
|
elif self.notation == NOTATION[1]:
|
||
|
|
self.text = Convert.rgbFToOklabS(*rgb, trc)
|
||
|
|
elif self.notation == NOTATION[2]:
|
||
|
|
self.text = Convert.rgbFToOklchS(*rgb, trc)
|
||
|
|
self.syntax.setText(self.text)
|
||
|
|
|
||
|
|
def switchNotation(self):
|
||
|
|
view = Application.activeWindow().activeView()
|
||
|
|
if not view.visible():
|
||
|
|
return
|
||
|
|
|
||
|
|
notation = self.sender().toolTip()
|
||
|
|
self.setNotation(notation)
|
||
|
|
self.updateNotations()
|
||
|
|
color = view.foregroundColor()
|
||
|
|
trc = self.profileTRC(color.colorProfile())
|
||
|
|
self.updateSyntax(color.componentsOrdered()[:3], trc)
|
||
|
|
|
||
|
|
def setNotation(self, notation: str):
|
||
|
|
self.notation = notation
|
||
|
|
# syntax needs to be on to set notation currently
|
||
|
|
Application.writeSetting(DOCKER_NAME, "syntax", ",".join(["True", notation]))
|
||
|
|
|
||
|
|
def updateNotations(self):
|
||
|
|
i = NOTATION.index(self.notation)
|
||
|
|
if i == 0:
|
||
|
|
self.prevNotation.setToolTip(NOTATION[len(NOTATION) - 1])
|
||
|
|
self.nextNotation.setToolTip(NOTATION[i + 1])
|
||
|
|
elif i == len(NOTATION) - 1:
|
||
|
|
self.prevNotation.setToolTip(NOTATION[i - 1])
|
||
|
|
self.nextNotation.setToolTip(NOTATION[0])
|
||
|
|
else:
|
||
|
|
self.prevNotation.setToolTip(NOTATION[i - 1])
|
||
|
|
self.nextNotation.setToolTip(NOTATION[i + 1])
|
||
|
|
|
||
|
|
def parseSyntax(self):
|
||
|
|
view = Application.activeWindow().activeView()
|
||
|
|
if not view.visible():
|
||
|
|
return
|
||
|
|
syntax = self.syntax.text().strip()
|
||
|
|
if syntax == self.text:
|
||
|
|
return
|
||
|
|
|
||
|
|
rgb = None
|
||
|
|
notation = self.notation
|
||
|
|
if syntax[:1] == "#":
|
||
|
|
self.setNotation(NOTATION[0])
|
||
|
|
rgb = Convert.hexSToRgbF(syntax, self.trc)
|
||
|
|
elif syntax[:5].upper() == NOTATION[1]:
|
||
|
|
self.setNotation(NOTATION[1])
|
||
|
|
rgb = Convert.oklabSToRgbF(syntax, self.trc)
|
||
|
|
elif syntax[:5].upper() == NOTATION[2]:
|
||
|
|
self.setNotation(NOTATION[2])
|
||
|
|
rgb = Convert.oklchSToRgbF(syntax, self.trc)
|
||
|
|
|
||
|
|
if notation != self.notation:
|
||
|
|
self.updateNotations()
|
||
|
|
if rgb:
|
||
|
|
self.setKritaFGColor(rgb)
|
||
|
|
self.updateChannels(rgb)
|
||
|
|
else:
|
||
|
|
color = view.foregroundColor()
|
||
|
|
trc = self.profileTRC(color.colorProfile())
|
||
|
|
self.updateSyntax(color.componentsOrdered()[:3], trc)
|
||
|
|
|
||
|
|
def showEvent(self, event):
|
||
|
|
if TIME:
|
||
|
|
self.timer.start(TIME)
|
||
|
|
|
||
|
|
def closeEvent(self, event):
|
||
|
|
self.timer.stop()
|
||
|
|
|
||
|
|
def canvasChanged(self, canvas):
|
||
|
|
if self.document != Application.activeDocument():
|
||
|
|
self.document = Application.activeDocument()
|
||
|
|
self.trc = self.profileTRC(self.document.colorProfile())
|
||
|
|
self.color.resetColors()
|
||
|
|
self.syntax.setText("")
|
||
|
|
self.getKritaColors()
|
||
|
|
|