Source code for editor.widgets.complex.color_picker

from PySide6.QtWidgets import *
from PySide6.QtCore import *
from PySide6.QtGui import *
from editor.common.math import clamp, locAt, vecAngle, lerpI, getDir
from editor.common.util import getIde, requestTransparentBgBrush, toInt, toFloat, isParentOfWidget
from editor.common.icon_cache import getThemePixmap


#####################  INTERNAL  #####################
_ColorSpaceLabels = ['RGB 0-255', 'RGB 0-1.0', 'HSV      ']
_ColorSpaceRgb, _ColorSpaceRgbF, _ColorSpaceHsv = range(3)

class _ColorRing(QWidget):
	def color(self): return self.parent().currentColor
	def setColor(self, h, s, v, a):
		parent = self.parent()
		parent.currentColor.setHsvF(h, s, v, a)
		parent.updateCurrentColor('ring')

	def __init__(self, parent):
		super().__init__(parent)
		size = parent.width()
		self.ringWidth = 24
		self.ringRadius = 96
		self.setFixedSize(size, size)
		self.buildColorRing()
		# self.updateColorRectImage()
		self.mouseTarget = None
		self.overridePaintFunc = None
		self.cachedHue = None
		parent.colorChanged.connect(self.update)

	def buildColorRing(self):
		path = QPainterPath()
		rw = self.ringWidth
		x = self.width() / 2 - self.ringRadius - rw / 2
		w = self.ringRadius * 2 + rw
		rect = QRectF(x, x, w, w)
		path.addEllipse(rect)
		path.addEllipse(rect.adjusted(rw, rw, -rw, -rw))

		gradient = QConicalGradient(rect.center(), 0)
		gradient.setColorAt(0,   QColor(255,   0,   0))
		gradient.setColorAt(1/6, QColor(255, 255,   0))
		gradient.setColorAt(2/6, QColor(  0, 255,   0))
		gradient.setColorAt(3/6, QColor(  0, 255, 255))
		gradient.setColorAt(4/6, QColor(  0,   0, 255))
		gradient.setColorAt(5/6, QColor(255,   0, 255))
		gradient.setColorAt(1,   QColor(255,   0,   0))

		self.ringPath = path
		self.ringGradient = gradient

		offset = 57
		self.colorRect = rect.toRect().adjusted(offset, offset, -offset, -offset)
		self.colorRectImage = QImage(self.colorRect.size(), QImage.Format_RGB888)

	def updateColorRectImage(self, hue):
		w, h = self.colorRect.width(), self.colorRect.height()
		# hue = clamp(self.color().hsvHueF(), 0, 1)
		for x in range(h):
			saturation = x / (h-1)
			for y in range(w):
				value = 1 - y / (w-1)
				self.colorRectImage.setPixelColor(x, y, QColor.fromHsvF(hue, saturation, value))

	def paintEvent(self, evt):
		hue = clamp(self.color().hsvHueF(), 0, 1)
		if self.cachedHue != hue or self.cachedHue == None:
			self.updateColorRectImage(hue)

		painter = QPainter(self)
		if self.overridePaintFunc: return self.overridePaintFunc(painter)
		painter.setRenderHint(QPainter.Antialiasing)
		painter.fillPath(self.ringPath, self.ringGradient)
		painter.drawImage(self.colorRect, self.colorRectImage)

		# draw ring pos
		angle = self.color().hue()
		dx, dy = vecAngle(angle, self.ringRadius)
		center = self.width() / 2
		x, y = round(center + dx), round(center - dy)
		r = self.ringWidth // 2 - 2
		painter.setPen(QPen(Qt.white, 2))
		painter.drawEllipse(x - r, y - r, r*2, r*2)

		# draw rect pos
		x = self.color().saturationF()
		y = self.color().valueF()
		x1, x2 = self.colorRect.left(), self.colorRect.right()
		y1, y2 = self.colorRect.top(), self.colorRect.bottom()
		x, y = lerpI(x1, x2, x), lerpI(y2, y1, y)
		r = 6
		R, G, B, A = self.color().getRgbF()
		brightness = 0.2126*R + 0.7152*G + 0.0722*B
		if brightness < 0.5:
			painter.setPen(QPen(Qt.white, 1.2))
		else:
			painter.setPen(QPen(Qt.black, 1.2))
		painter.drawEllipse(x - r, y - r, r*2, r*2)

	def checkInsideRing(self, pos):
		center = self.width() / 2
		dx, dy = pos.x() - center, pos.y() - center
		dist2 = dx * dx + dy * dy
		rin = self.ringRadius - self.ringWidth / 2
		rout = self.ringRadius + self.ringWidth / 2
		return rin * rin <= dist2 <= rout * rout

	def mousePressEvent(self, evt):
		pos = evt.pos()
		if self.colorRect.contains(pos):
			x1, x2 = self.colorRect.left(), self.colorRect.right()
			y1, y2 = self.colorRect.top(), self.colorRect.bottom()
			x, y = locAt(x1, x2, pos.x()), locAt(y2, y1, pos.y())
			h, s, v, a = self.color().getHsvF()
			self.setColor(h, x, y, a)
			self.mouseTarget = 'rect'

		elif self.checkInsideRing(pos):
			center = self.width() / 2
			dx, dy = pos.x() - center, pos.y() - center
			angle = getDir(dx, -dy) % 360
			h, s, v, a = self.color().getHsvF()
			self.setColor(angle/360, s, v, a)
			self.mouseTarget = 'ring'
			# self.updateColorRectImage()

		else:
			self.mouseTarget = None

	def mouseMoveEvent(self, evt):
		pos = evt.pos()
		if self.mouseTarget == 'rect':
			x1, x2 = self.colorRect.left(), self.colorRect.right()
			y1, y2 = self.colorRect.top(), self.colorRect.bottom()
			x, y = locAt(x1, x2, pos.x()), locAt(y2, y1, pos.y())
			h, s, v, a = self.color().getHsvF()
			self.setColor(clamp(h, 0, 1), clamp(x, 0, 1), clamp(y, 0, 1), a)

		elif self.mouseTarget == 'ring':
			center = self.width() / 2
			dx, dy = pos.x() - center, pos.y() - center
			angle = getDir(dx, -dy) % 360
			h, s, v, a = self.color().getHsvF()
			self.setColor(angle/360, s, v, a)
			# self.updateColorRectImage()

class _ColorPickerBtn(QWidget):
	clicked = Signal()

	def __init__(self, parent):
		super().__init__(parent)
		self.setFixedSize(20, 20)

	def mousePressEvent(self, evt):
		if evt.button() == Qt.LeftButton:
			self.clicked.emit()

	def paintEvent(self, evt):
		painter = QPainter(self)
		# painter.setRenderHint(QPainter.Antialiasing)
		painter.drawPixmap(self.rect(), getThemePixmap('color_picker.png'))

class _ColorPreview(QWidget):
	def __init__(self, parent):
		super().__init__(parent)
		self.setFixedSize(76, 24)
		self.setMouseTracking(True)
		self.tooltipL = 'The original color. Click to reset to this.'
		self.tooltipR = 'The new color.'
		self.useTooltipL = True
		self.setToolTip(self.tooltipL)
		parent.colorChanged.connect(self.update)

	def mousePressEvent(self, evt):
		if evt.button() == Qt.LeftButton:
			x = evt.x()
			if x < self.width() // 2:
				self.parent().revertColor()

	def mouseMoveEvent(self, evt):
		x = evt.x()
		inLeft = x < self.width() // 2
		if inLeft and not self.useTooltipL:
			self.useTooltipL = True
			self.setToolTip(self.tooltipL)
		elif not inLeft and self.useTooltipL:
			self.useTooltipL = False
			self.setToolTip(self.tooltipR)

	def paintEvent(self, evt):
		painter = QPainter(self)
		# painter.setRenderHint(QPainter.Antialiasing)
		path = QPainterPath()
		w, h = self.width(), self.height()
		path.addRoundedRect(0, 0, w, h, 2, 2)
		painter.setClipPath(path)

		wHalf = w // 2
		parent = self.parent()
		brush = requestTransparentBgBrush()
		# painter.setPen(Qt.transparent)
		# painter.setBrush(brush)
		# painter.drawRoundedRect(0, 0, w, h, 5, 5)
		painter.fillRect(0, 0, w, h, brush)
		painter.fillRect(0, 0, wHalf, h, parent.initColor)
		painter.fillRect(wHalf, 0, wHalf, h, parent.currentColor)

class _ColorSpaceDropDown(QPushButton):
	def __init__(self, parent):
		super().__init__(parent)
		self.setFocusPolicy(Qt.NoFocus)
		self.setText(_ColorSpaceLabels[ColorPicker.ColorSpace])
		self.pressed.connect(self.showMenu)
		self.setFixedSize(88, 24)
		self.initMenu()

	def initMenu(self):
		actions = []
		menu = QMenu(self)
		menu.setAttribute(Qt.WA_TranslucentBackground)
		menu.setWindowFlag(Qt.FramelessWindowHint, True)
		for label in _ColorSpaceLabels: actions.append(menu.addAction(label))
		for act in actions: act.setCheckable(True)
		actions[ColorPicker.ColorSpace].setChecked(True)
		menu.triggered.connect(self.onActionTrigger)
		self.setMenu(menu)

	def onActionTrigger(self, action):
		actions = self.menu().actions()
		self.setText(action.text())
		for act in actions: act.setChecked(act == action)
		self.parent().updateColorSpace(actions.index(action))

class _ColorCptSlot(QWidget):
	def __init__(self, parent, isAlpha = False):
		super().__init__(parent)
		self.value = 0
		self.isAlpha = isAlpha
		self.handleWidth = 4

	def updateValue(self, value, updateColor = False):
		self.update()
		if value == self.value: return
		self.value = value
		if updateColor:
			cpt = self.parent().cpt
			editor = self.parent().parent()
			if cpt == _ColorComponentEdit.CptA:
				r, g, b, a = editor.currentColor.getRgbF()
				editor.currentColor.setRgbF(r, g, b, value)
			elif cpt == _ColorComponentEdit.CptR:
				r, g, b, a = editor.currentColor.getRgbF()
				editor.currentColor.setRgbF(value, g, b, a)
			elif cpt == _ColorComponentEdit.CptG:
				r, g, b, a = editor.currentColor.getRgbF()
				editor.currentColor.setRgbF(r, value, b, a)
			elif cpt == _ColorComponentEdit.CptB:
				r, g, b, a = editor.currentColor.getRgbF()
				editor.currentColor.setRgbF(r, g, value, a)
			elif cpt == _ColorComponentEdit.CptH:
				h, s, v, a = editor.currentColor.getHsvF()
				editor.currentColor.setHsvF(value, s, v, a)
			elif cpt == _ColorComponentEdit.CptS:
				h, s, v, a = editor.currentColor.getHsvF()
				editor.currentColor.setHsvF(h, value, v, a)
			elif cpt == _ColorComponentEdit.CptV:
				h, s, v, a = editor.currentColor.getHsvF()
				editor.currentColor.setHsvF(h, s, value, a)
			editor.updateCurrentColor(f'slot{cpt}')

	def buildGradient(self):
		gradient = QLinearGradient()
		cpt = self.parent().cpt
		color = self.parent().parent().currentColor
		r, g, b, a = color.getRgbF()
		if cpt == _ColorComponentEdit.CptA:
			gradient.setColorAt(0, QColor.fromRgbF(r, g, b, 0))
			gradient.setColorAt(1, QColor.fromRgbF(r, g, b, 1))
		elif cpt == _ColorComponentEdit.CptR:
			gradient.setColorAt(0, QColor.fromRgbF(0, g, b))
			gradient.setColorAt(1, QColor.fromRgbF(1, g, b))
		elif cpt == _ColorComponentEdit.CptG:
			gradient.setColorAt(0, QColor.fromRgbF(r, 0, b))
			gradient.setColorAt(1, QColor.fromRgbF(r, 1, b))
		elif cpt == _ColorComponentEdit.CptB:
			gradient.setColorAt(0, QColor.fromRgbF(r, g, 0))
			gradient.setColorAt(1, QColor.fromRgbF(r, g, 1))
		elif cpt == _ColorComponentEdit.CptH:
			gradient.setColorAt(0,   QColor.fromHsvF(0,   1, 1))
			gradient.setColorAt(1/6, QColor.fromHsvF(1/6, 1, 1))
			gradient.setColorAt(2/6, QColor.fromHsvF(2/6, 1, 1))
			gradient.setColorAt(3/6, QColor.fromHsvF(3/6, 1, 1))
			gradient.setColorAt(4/6, QColor.fromHsvF(4/6, 1, 1))
			gradient.setColorAt(5/6, QColor.fromHsvF(5/6, 1, 1))
			gradient.setColorAt(1,   QColor.fromHsvF(1,   1, 1))
		elif cpt == _ColorComponentEdit.CptS:
			h, s, v, a = color.getHsvF()
			gradient.setColorAt(0, QColor.fromHsvF(h, 0, v))
			gradient.setColorAt(1, QColor.fromHsvF(h, 1, v))
		elif cpt == _ColorComponentEdit.CptV:
			h, s, v, a = color.getHsvF()
			gradient.setColorAt(0, QColor.fromHsvF(h, s, 0))
			gradient.setColorAt(1, QColor.fromHsvF(h, s, 1))
		return gradient

	def paintEvent(self, evt):
		rect = self.rect()
		w, h = rect.width(), rect.height()
		painter = QPainter(self)
		if self.isAlpha:
			brush = requestTransparentBgBrush()
			painter.fillRect(rect, brush)

		hw = self.handleWidth
		gradient = self.buildGradient()
		gradient.setStart(hw/2, 0)
		gradient.setFinalStop(w - hw/2, 0)
		painter.fillRect(rect, gradient)

		painter.setPen(QColor('#222'))
		painter.drawRect(0, 0, w-1, h-1)

		painter.setPen(QColor('#aaa'))
		painter.setBrush(QColor('#ccc'))
		x = round((w - hw) * self.value)
		painter.drawRect(x, 0, hw, h-1)

	def mousePressEvent(self, evt):
		x = evt.x()
		hhw = self.handleWidth / 2
		value = locAt(hhw, self.width() - hhw, x)
		self.updateValue(clamp(value, 0, 1), True)
	def mouseMoveEvent(self, evt):
		x = evt.x()
		hhw = self.handleWidth / 2
		value = locAt(hhw, self.width() - hhw, x)
		self.updateValue(clamp(value, 0, 1), True)

class _ColorCptLineEdit(QLineEdit):
	def __init__(self, parent):
		super().__init__(parent)
		self.setFocusPolicy(Qt.ClickFocus)
		self.value = None
		self.editingFinished.connect(self.onEditingFinish)
		self.textChanged.connect(self.onTextChange)
		self.ignoreUpdate = True
		# self.setStyleSheet('padding-right: -10px;')

	def parseText(self, text):
		space = ColorPicker.ColorSpace
		if space == _ColorSpaceRgb:
			return clamp(toInt(text), 0, 255)
		elif space == _ColorSpaceRgbF:
			return clamp(toFloat(text), 0.0, 1.0)
		else:
			hue = self.parent().cpt == _ColorComponentEdit.CptH
			return clamp(toInt(text), 0, hue and 360 or 100)

	def onTextChange(self, text):
		value = self.parseText(text)
		if self.value == value: return
		self.value = value
		if self.ignoreUpdate: return
		cpt = self.parent().cpt
		space = ColorPicker.ColorSpace
		editor = self.parent().parent()
		if space == _ColorSpaceRgb:
			r, g, b, a = editor.currentColor.getRgb()
			if cpt == _ColorComponentEdit.CptR:
				editor.currentColor.setRgb(value, g, b, a)
			elif cpt == _ColorComponentEdit.CptG:
				editor.currentColor.setRgb(r, value, b, a)
			elif cpt == _ColorComponentEdit.CptB:
				editor.currentColor.setRgb(r, g, value, a)
			elif cpt == _ColorComponentEdit.CptA:
				editor.currentColor.setRgb(r, g, b, value)

		elif space == _ColorSpaceRgbF:
			r, g, b, a = editor.currentColor.getRgbF()
			if cpt == _ColorComponentEdit.CptR:
				editor.currentColor.setRgbF(value, g, b, a)
			elif cpt == _ColorComponentEdit.CptG:
				editor.currentColor.setRgbF(r, value, b, a)
			elif cpt == _ColorComponentEdit.CptB:
				editor.currentColor.setRgbF(r, g, value, a)
			elif cpt == _ColorComponentEdit.CptA:
				editor.currentColor.setRgbF(r, g, b, value)

		else:
			h, s, v, a = editor.currentColor.getHsv()
			if cpt == _ColorComponentEdit.CptH:
				editor.currentColor.setHsv(value, s, v, a)
			elif cpt == _ColorComponentEdit.CptS:
				editor.currentColor.setHsv(h, value, v, a)
			elif cpt == _ColorComponentEdit.CptV:
				editor.currentColor.setHsv(h, s, value, a)
			elif cpt == _ColorComponentEdit.CptA:
				editor.currentColor.setHsv(h, s, v, value)

		editor.updateCurrentColor(f'edit{cpt}')

	def onEditingFinish(self):
		self.ignoreUpdate = True
		self.parent().updateLineEdit()
		self.setText(str(self.value))
		self.home(False)
		self.clearFocus()

	def mouseDoubleClickEvent(self, evt):
		if evt.button() == Qt.LeftButton: return self.selectAll()
		super().mouseDoubleClickEvent(evt)

	def keyPressEvent(self, evt):
		if evt.key() == Qt.Key_Escape:
			self.ignoreUpdate = True
			self.setText(self.originText)
			self.home(False)
			self.clearFocus()
		else:
			super().keyPressEvent(evt)

	def focusInEvent(self, evt):
		super().focusInEvent(evt)
		self.originText = self.text()
		self.ignoreUpdate = False

	def focusOutEvent(self, evt):
		super().focusOutEvent(evt)
		self.ignoreUpdate = True

	def wheelEvent(self, evt):
		if not self.hasFocus(): return
		dy = sign(evt.angleDelta().y())
		self.updateWithDelta(dy)
		# self.home(False)

	def updateWithDelta(self, deltaDir):
		space = ColorPicker.ColorSpace
		if space == _ColorSpaceRgb:
			value = clamp(self.value + deltaDir * 5, 0, 255)
			self.setText(str(value))
		elif space == _ColorSpaceRgbF:
			value = clamp(self.value + deltaDir * 0.05, 0.0, 1.0)
			value = round(value * 10000) / 10000
			self.setText(str(value))
		else:
			if self.parent().cpt == _ColorComponentEdit.CptH:
				value = clamp(self.value + deltaDir * 5, 0, 360)
				self.setText(str(value))
			else:
				value = clamp(self.value + deltaDir * 5, 0, 100)
				self.setText(str(value))

class _ColorCptLabel(QLabel):
	moveTriggered = Signal(int)

	def __init__(self, parent):
		super().__init__(parent)
		self.setStyleSheet('padding-bottom: 2px; color: #ccc;')
		self.setCursor(QCursor(getThemePixmap('field_cursor.png'), 8, 2))
		self.mouseDownPos = None
		self.prevMousePos = None
		# self.moveTriggered.connect(lambda dm: print(dm))

	def mousePressEvent(self, evt):
		if evt.button() != Qt.LeftButton: return
		self.mouseDownPos = evt.pos()
		self.prevMousePos = None

	def mouseMoveEvent(self, evt):
		# if not self.mouseDownPos: return
		pos = evt.pos()
		if self.prevMousePos:
			dx = pos.x() - self.prevMousePos.x()
			if dx > 3:
				self.prevMousePos = pos
				self.moveTriggered.emit(1)
			elif dx < -3:
				self.prevMousePos = pos
				self.moveTriggered.emit(-1)
		else:
			dx = pos.x() - self.mouseDownPos.x()
			if dx > 10:
				self.prevMousePos = pos
				self.moveTriggered.emit(1)
			elif dx < -10:
				self.prevMousePos = pos
				self.moveTriggered.emit(-1)

class _ColorComponentEdit(QWidget):
	CptA, CptR, CptG, CptB, CptH, CptS, CptV = range(7)
	CptLabels = ['A', 'R', 'G', 'B', 'H', 'S', 'V']

	def componentValue(self):
		parent = self.parent()
		color = parent.currentColor
		space = parent.ColorSpace
		if self.cpt == self.CptA:
			if   space == _ColorSpaceRgbF: return color.alphaF()
			elif space == _ColorSpaceHsv: return round(color.alphaF() * 100)
			else: return color.alpha()
		elif self.cpt == self.CptR:
			if space == _ColorSpaceRgbF: return color.redF()
			else: return color.red()
		elif self.cpt == self.CptG:
			if space == _ColorSpaceRgbF: return color.greenF()
			else: return color.green()
		elif self.cpt == self.CptB:
			if space == _ColorSpaceRgbF: return color.blueF()
			else: return color.blue()
		elif self.cpt == self.CptH:
			return max(color.hue(), 0)
		elif self.cpt == self.CptS:
			return round(color.saturationF() * 100)
		elif self.cpt == self.CptV:
			return round(color.valueF() * 100)

	def componentValueF(self):
		color = self.parent().currentColor
		if self.cpt == self.CptA:
			return color.alphaF()
		elif self.cpt == self.CptR:
			return color.redF()
		elif self.cpt == self.CptG:
			return color.greenF()
		elif self.cpt == self.CptB:
			return color.blueF()
		elif self.cpt == self.CptH:
			return color.hueF()
		elif self.cpt == self.CptS:
			return color.saturationF()
		elif self.cpt == self.CptV:
			return color.valueF()

	def __init__(self, parent, cpt):
		super().__init__(parent)
		layout = QHBoxLayout(self)
		layout.setContentsMargins(8, 0, 10, 8)
		layout.setSpacing(6)

		label = _ColorCptLabel(self)
		label.setFixedWidth(13)
		layout.addWidget(label)

		slot = _ColorCptSlot(self, cpt == self.CptA)
		slot.setFixedHeight(18)
		layout.addWidget(slot)
		layout.setStretch(1, 1)

		lineEdit = _ColorCptLineEdit(self)
		lineEdit.setFixedSize(43, 18)
		layout.addWidget(lineEdit)

		self.label = label
		self.slot = slot
		self.lineEdit = lineEdit
		self.cpt = None
		self.updateComponent(cpt)
		# slot.updateValue(self.componentValueF())
		def onLabelMove(delta):
			lineEdit.ignoreUpdate = False
			lineEdit.updateWithDelta(delta)
			lineEdit.ignoreUpdate = True
		label.moveTriggered.connect(onLabelMove)
		parent.colorChanged.connect(self.onCurrentColorChange)

	def updateLineEdit(self):
		space = ColorPicker.ColorSpace
		if space == _ColorSpaceRgbF:
			value = round(self.componentValue() * 1000) / 1000
			self.lineEdit.setText(str(value))
		else:
			self.lineEdit.setText(str(self.componentValue()))

		self.lineEdit.home(False)

	def updateComponent(self, cpt):
		# if self.cpt == cpt: return
		self.cpt = cpt
		self.label.setText(self.CptLabels[cpt])
		self.slot.updateValue(self.componentValueF())
		self.updateLineEdit()

	def onCurrentColorChange(self, color, reason):
		if reason != f'slot{self.cpt}': self.slot.updateValue(self.componentValueF())
		if reason != f'edit{self.cpt}': self.updateLineEdit()

class _ColorHexEditValidator(QRegularExpressionValidator):
	def __init__(self, parent):
		super().__init__(QRegularExpression('[A-Fa-f0-9]{,6}'), parent)

	def validate(self, string, pos):
		state, text, pos = super().validate(string, pos)
		return state, text.upper(), pos

class _ColorHexEdit(QLineEdit):
	def __init__(self, parent):
		super().__init__(parent)
		self.setFocusPolicy(Qt.ClickFocus)
		self.setValidator(_ColorHexEditValidator(self))
		self.setStyleSheet('padding-left: 12px;')
		self.onCurrentColorChange(parent.currentColor, None)
		parent.colorChanged.connect(self.onCurrentColorChange)
		self.editingFinished.connect(self.onEditingFinish)
		self.textChanged.connect(self.onTextChange)
		self.ignoreUpdate = True
		self.value = None

	def parseText(self, text):
		length = len(text)
		if length == 6 or length == 3: return text
		return False

	def onTextChange(self, text):
		tlen = len(text)
		if tlen != 6 and tlen != 3: return
		if self.value == text: return
		self.value = text
		if self.ignoreUpdate: return
		parent = self.parent()
		parent.currentColor.setNamedColor('#' + text)
		parent.updateCurrentColor('hex')

	def onEditingFinish(self):
		self.ignoreUpdate = True
		self.setText(self.value)
		self.clearFocus()

	def onCurrentColorChange(self, color, reason):
		if reason == 'hex': return
		self.setText(color.name()[1:].upper())

	def mouseDoubleClickEvent(self, evt):
		if evt.button() == Qt.LeftButton: return self.selectAll()
		super().mouseDoubleClickEvent(evt)

	def keyPressEvent(self, evt):
		if evt.key() == Qt.Key_Escape:
			self.ignoreUpdate = True
			self.setText(self.originText)
			self.clearFocus()
		else:
			super().keyPressEvent(evt)

	def focusInEvent(self, evt):
		super().focusInEvent(evt)
		self.originText = self.text()
		self.ignoreUpdate = False

	def focusOutEvent(self, evt):
		super().focusOutEvent(evt)
		self.ignoreUpdate = True

	def paintEvent(self, evt):
		super().paintEvent(evt)
		painter = QPainter(self)
		painter.setPen(QColor('#bbb'))
		painter.setFont(self.font())
		painter.drawText(5, 14, '#')


#####################  PUBLIC  #####################
[docs]class ColorPicker(QWidget):
[docs] colorChanged = Signal(QColor, str) # color: QColor, reason: str
[docs] ColorSpace = _ColorSpaceRgb
[docs] Instanced = False
[docs] PrevLoc = None
def __init__(self, initColor): ide = getIde() super().__init__(ide.activeWindow()) ide.focusChanged.connect(self.onFocusChange) self.setAttribute(Qt.WA_DeleteOnClose, True) self.setWindowFlags(Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint | Qt.CustomizeWindowHint) # self.setWindowFlags(Qt.Drawer | Qt.WindowCloseButtonHint | Qt.WindowTitleHint | Qt.CustomizeWindowHint) self.setWindowTitle('Color Editor') self.setFocusPolicy(Qt.ClickFocus) self.setFocus() self.setFixedSize(234, 464) self.initColor = QColor(initColor) self.currentColor = QColor(initColor) if ColorPicker.PrevLoc: self.move(ColorPicker.PrevLoc) ColorPicker.Instanced = True btn = _ColorPickerBtn(self) btn.clicked.connect(self.pickScreenshotColor) preview = _ColorPreview(self) ring = _ColorRing(self) space = _ColorSpaceDropDown(self) cptA = _ColorComponentEdit(self, _ColorComponentEdit.CptA) cpt1, cpt2, cpt3 = None, None, None if self.ColorSpace == _ColorSpaceHsv: cpt1 = _ColorComponentEdit(self, _ColorComponentEdit.CptH) cpt2 = _ColorComponentEdit(self, _ColorComponentEdit.CptS) cpt3 = _ColorComponentEdit(self, _ColorComponentEdit.CptV) else: cpt1 = _ColorComponentEdit(self, _ColorComponentEdit.CptR) cpt2 = _ColorComponentEdit(self, _ColorComponentEdit.CptG) cpt3 = _ColorComponentEdit(self, _ColorComponentEdit.CptB) layout = QVBoxLayout(self) layout.setAlignment(Qt.AlignTop) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) layoutH = QHBoxLayout() layoutH.setContentsMargins(10, 12, 10, 2) layoutH.setSpacing(0) layoutH.addWidget(btn) layoutH.addStretch() layoutH.addWidget(preview) layout.addLayout(layoutH) layout.addWidget(ring) layoutH = QHBoxLayout() layoutH.setContentsMargins(0, 5, 10, 0) layoutH.setSpacing(0) layoutH.addStretch() layoutH.addWidget(space) layout.addLayout(layoutH) layout.addSpacing(7) layout.addWidget(cpt1) layout.addWidget(cpt2) layout.addWidget(cpt3) layout.addWidget(cptA) hexLabel = QLabel('Hexadecimal', self) hexLabel.setStyleSheet('color: #ccc;') hexEdit = _ColorHexEdit(self) hexEdit.setFixedWidth(80) layoutH = QHBoxLayout() layoutH.setContentsMargins(11, 0, 10, 7) layoutH.setSpacing(0) layoutH.addWidget(hexLabel) layoutH.addStretch() layoutH.addWidget(hexEdit) layout.addLayout(layoutH) presetBtn = QPushButton('Color Presets Editor', self) presetBtn.setStyleSheet('QPushButton { margin: 0 10 0 10; height: 12px; }') presetBtn.clicked.connect(self.toggleColorPresetEditor) layout.addWidget(presetBtn) self.colorRing = ring self.colorCpt1 = cpt1 self.colorCpt2 = cpt2 self.colorCpt3 = cpt3 self.colorCptA = cptA self.presetEditor = None
[docs] def paintEvent(self, evt): painter = QPainter(self) painter.fillRect(self.rect(), QColor('#444')) super().paintEvent(evt)
[docs] def closeEvent(self, evt): super().closeEvent(evt) getIde().focusChanged.disconnect(self.onFocusChange) ColorPicker.PrevLoc = self.pos() ColorPicker.Instanced = False
[docs] def onFocusChange(self, old, now): if not now: return if not isinstance(now, QWidget): return if now != self and not isParentOfWidget(self, now): self.close()
[docs] def pickScreenshotColor(self): def onPickedColorUpdate(color, finish): if not finish: return if self.currentColor == color: return self.currentColor.setRgba(color.rgba()) self.updateCurrentColor('screen_pick') ScreenColorPicker(self.currentColor, onPickedColorUpdate, False, self.colorRing).show()
[docs] def updateCurrentColor(self, reason): self.colorChanged.emit(self.currentColor, reason)
[docs] def updateColorSpace(self, space): if ColorPicker.ColorSpace == space: return ColorPicker.ColorSpace = space if space == _ColorSpaceHsv: self.colorCpt1.updateComponent(_ColorComponentEdit.CptH) self.colorCpt2.updateComponent(_ColorComponentEdit.CptS) self.colorCpt3.updateComponent(_ColorComponentEdit.CptV) self.colorCptA.updateLineEdit() else: self.colorCpt1.updateComponent(_ColorComponentEdit.CptR) self.colorCpt2.updateComponent(_ColorComponentEdit.CptG) self.colorCpt3.updateComponent(_ColorComponentEdit.CptB) self.colorCptA.updateLineEdit()
[docs] def revertColor(self): self.currentColor.setRgba(self.initColor.rgba()) self.updateCurrentColor('revert')
[docs] def toggleColorPresetEditor(self): if self.presetEditor: self.presetEditor.close() self.presetEditor = None else: self.presetEditor = ColorPresetEditor(self) self.presetEditor.show()
[docs] def moveEvent(self, evt): super().moveEvent(evt) if self.presetEditor: # self.presetEditor.updateLoc() dm = evt.pos() - evt.oldPos() pos = self.presetEditor.pos() self.presetEditor.move(pos + dm)
[docs] def keyPressEvent(self, evt): if evt.key() == Qt.Key_Escape: self.close() super().keyPressEvent(evt)
[docs]class ScreenColorPicker(QWidget):
[docs] colorUpdated = Signal(QColor, bool)
def __init__(self, defaultColor, onColorUpdate, showPreview = True, overridePaint = None): super().__init__(getIde().activeWindow()) self.setAttribute(Qt.WA_DeleteOnClose) self.setAttribute(Qt.WA_TranslucentBackground) self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) self.setGeometry(QApplication.primaryScreen().virtualGeometry()) self.setMouseTracking(True) self.screen = self.grabScreen().toImage() self.defaultColor = QColor(defaultColor) if onColorUpdate: self.colorUpdated.connect(onColorUpdate) self.showPreview = showPreview self.overridePaint = overridePaint if overridePaint: overridePaint.overridePaintFunc = self.drawPreview
[docs] def closeEvent(self, evt): super().closeEvent(evt) if not self.overridePaint: return self.overridePaint.overridePaintFunc = None
[docs] def grabScreen(self): final = QPixmap(self.size()) painter = QPainter(final) for screen in QApplication.screens(): rect = screen.geometry() screenshot = screen.grabWindow() painter.drawPixmap(rect, screenshot) return final
[docs] def keyPressEvent(self, evt): key = evt.key() if key == Qt.Key_Escape: self.colorUpdated.emit(self.defaultColor, True) self.close()
[docs] def mousePressEvent(self, evt): btn = evt.button() if btn == Qt.RightButton: self.colorUpdated.emit(self.defaultColor, True) self.close() elif btn == Qt.MiddleButton: evt.ignore() else: color = self.screen.pixelColor(evt.pos()) self.colorUpdated.emit(color, True) self.close()
[docs] def mouseMoveEvent(self, evt): # self.screen = self.grabScreen().toImage() self.colorUpdated.emit(self.screen.pixelColor(evt.pos()), False) if self.overridePaint: self.overridePaint.update() self.update()
[docs] def paintEvent(self, evt): rect = self.rect() painter = QPainter(self) painter.fillRect(rect, QColor(255, 255, 255, 1)) if not self.showPreview: return pos = QCursor.pos() px, py = pos.x(), pos.y() offset, size, halfSize = 20, 110, 55 x, y = px + offset, py + offset w, h = rect.width(), rect.height() if x + size > w: x = px - offset - size if y + size > h - 45: y = py - offset - size - 35 painter.setPen(QColor('#0a0')) tarRect = QRect(x, y, size, size) srcRect = QRect(px - 5, py - 5, 10 + 1, 10 + 1) painter.drawImage(tarRect, self.screen, srcRect) painter.drawLine(x, y, x + size, y) painter.drawLine(x + size, y, x + size, y + size) painter.drawLine(x + size, y + size, x, y + size) painter.drawLine(x, y + size, x, y) painter.drawLine(x + halfSize, y, x + halfSize, y + size) painter.drawLine(x, y + halfSize, x + size, y + halfSize)
[docs] def drawPreview(self, painter): margin = 11 x, y = margin, margin size = self.overridePaint.width() - margin * 2 pos = QCursor.pos() px, py = pos.x(), pos.y() painter.setPen(QColor(120, 120, 120, 160)) srcRect = QRect(px - 10, py - 10, 20 + 1, 20 + 1) tarRect = QRect(x, y, size, size) painter.drawImage(tarRect, self.screen, srcRect) ds = size / (20 + 1) for i in range(20 + 2): d = round(i * ds) painter.drawLine(x, y + d, x + size, y + d) painter.drawLine(x + d, y, x + d, y + size) # painter.setPen(QColor('#080')) # d = size // 2 # painter.drawLine(x, y + d, x + size, y + d) # painter.drawLine(x + d, y, x + d, y + size) painter.setPen(QColor('#080')) painter.setBrush(Qt.transparent) offset = round(10 * ds) - 1 ids = round(ds) + 2 painter.drawRect(x + offset, y + offset, ids, ids)
[docs]class ColorPresetEditor(QWidget): def __init__(self, parent): super().__init__(parent) self.setFixedSize(400, parent.height()) self.updateLoc() self.setAttribute(Qt.WA_DeleteOnClose, True) self.setWindowFlags(Qt.Tool | Qt.WindowCloseButtonHint | Qt.WindowTitleHint | Qt.CustomizeWindowHint) self.setWindowTitle('Color Presets')
[docs] def updateLoc(self): parent = self.parent() x, y = parent.width(), parent.frameGeometry().height() - self.frameGeometry().height() px, py = parent.x() + 1, parent.y() - 1 self.move(px + x, py + y)
[docs] def paintEvent(self, evt): painter = QPainter(self) painter.fillRect(self.rect(), QColor('#444'))
[docs] def closeEvent(self, evt): super().closeEvent(evt) self.parent().presetEditor = None
[docs]def createColorPicker(initColor): if ColorPicker.Instanced: return ColorPicker(initColor).show()