I have some long Palettes. If you put them in the docker, it is very narrow, unless I put it side by side with the docker of the “layer” and switch repeatedly. Or take up a lot of space to place it.
So I hope it can exist in the toolbar. The height is about the same as the “choose brush preset”
Something like this?
This is something that can be made through a plugin.
Here the really quick & dirty code to get result from video.
import math
from PyQt5.Qt import *
from krita import *
from PyQt5.QtCore import (
pyqtSignal as Signal
)
def checkerBoardBrush(size=32, color1=QColor(255,255,255), color2=QColor(220,220,220), strictSize=True):
"""Return a checker board brush"""
s1 = size>>1
if strictSize:
s2 = size - s1
else:
s2 = s1
size=s1+s2
tmpPixmap = QPixmap(size,size)
tmpPixmap.fill(color1)
brush = QBrush(color2)
canvas = QPainter()
canvas.begin(tmpPixmap)
canvas.setPen(Qt.NoPen)
canvas.setRenderHint(QPainter.Antialiasing, False)
canvas.fillRect(QRect(0, 0, s1, s1), brush)
canvas.fillRect(QRect(s1, s1, s2, s2), brush)
canvas.end()
return QBrush(tmpPixmap)
def checkerBoardImage(size, checkerSize=32):
"""Return a checker board image"""
if isinstance(size, int):
size = QSize(size, size)
if not isinstance(size, QSize):
return None
pixmap = QPixmap(size)
painter = QPainter()
painter.begin(pixmap)
painter.fillRect(pixmap.rect(), checkerBoardBrush(checkerSize))
painter.end()
return pixmap
class WColorPalette(QWidget):
"""A simple widget to manage palettes"""
colorOver = Signal(int, Swatch, QColor) # when mouse is over a color (color index, color swatch, color)
colorClicked = Signal(int, Swatch, QColor, int) # when a color has been clicked (color index, color swatch, color, mouse button)
paletteChanged = Signal(str) # when palette has been changed,
class WPaletteGrid(QWidget):
"""A palette widget
Because signals for PaletteView class are not working...
"""
colorOver = Signal(int, Swatch, QColor) # when mouse is over a color (color index, color swatch, color)
colorClicked = Signal(int, Swatch, QColor, int) # when a color has been clicked (color index, color swatch, color, mouse button)
def __init__(self, parent=None):
super(WColorPalette.WPaletteGrid, self).__init__(parent)
# track mouse move event on widget
self.setMouseTracking(True)
# current palette
self.__palette=None
# number of colors, columns and rows for palette
self.__nbColors=0
self.__columns=0
self.__rows=0
# cell size in pixel for palette grid
self.__cellSize=0
# color index on which mouse is over; -1 means outside palette grid
self.__overIndex=-1
# color cell coordinates on which mouse is over; as tuple(row, column) or None if no index
self.__overCell=None
# rendered grid in a pixmap cache
self.__cachedGrid=None
# QPen used for mouse over rendering
self.__qPalette=QApplication.palette()
self.__penOver=QPen(self.__qPalette.color(QPalette.Base))
self.__penOver.setWidth(3)
self.__idealSize=QSize()
def __colorRect(self, row, column):
"""Return a QRect for a color square in grid"""
return QRect(column * (1 + self.__cellSize), row *(1 + self.__cellSize), self.__cellSize, self.__cellSize)
def __renderCache(self):
"""Render current grid in cache"""
if self.__palette is None or self.__columns==0:
self.__cachedGrid=None
return
# generate pixmap cache
self.__cachedGrid=QPixmap(self.__idealSize)
self.__cachedGrid.fill(self.__qPalette.color(QPalette.Base))
noColorPixMap=checkerBoardImage(self.__cellSize, self.__cellSize)
painter=QPainter()
painter.begin(self.__cachedGrid)
for row in range(self.__rows):
for col in range(self.__columns):
color=self.colorFromRowColumn(row, col)
if color:
painter.fillRect(self.__colorRect(row, col), QBrush(color))
else:
# no color defined, let the checker board be displayed
painter.drawPixmap(self.__colorRect(row, col).topLeft(), noColorPixMap)
painter.end()
def invalidate(self):
self.__cachedGrid=None
# calculate pixel size of a color square
# total width - number of columns ==> because keep 1 pixel per column
# as separator
self.__cellSize=(self.width() - self.__columns)//self.__columns
# recalculate size according to:
# - current width
# - current cell size
# - number of rows
self.__idealSize=QSize(self.width(), (self.__cellSize+1)*self.__rows)
# and set ideal height as minimal height for widget
self.setMinimumHeight(self.__idealSize.height())
def resizeEvent(self, event):
"""Widget is resized, need to invalidate pixmap cache"""
self.__cachedGrid=None
super(WColorPalette.WPaletteGrid, self).resizeEvent(event)
self.invalidate()
def paintEvent(self, event):
"""refresh widget content"""
if self.__cachedGrid is None:
# cache is not valid anymore, regenerate it
self.__renderCache()
if self.__cachedGrid is None:
# wow big problem here!
# hope it will never occur :)
super(WColorPalette.WPaletteGrid, self).paintEvent(event)
return
painter = QPainter(self)
painter.drawPixmap(QPoint(0, 0), self.__cachedGrid)
if not self.__overCell is None:
painter.setPen(self.__penOver)
painter.setBrush(QBrush(Qt.NoBrush))
painter.drawRect(self.__colorRect(self.__overCell[0], self.__overCell[1])-QMargins(0,0,1,1))
def mousePressEvent(self, event):
"""A mouse button is clicked on widget"""
if isinstance(self.__overIndex, int) and self.__overIndex>-1:
swatch=self.colorFromIndex(self.__overIndex, False)
if swatch.isValid():
qColor=self.colorFromIndex(self.__overIndex, True)
else:
qColor=QColor(Qt.transparent)
self.colorClicked.emit(self.__overIndex, swatch, qColor, event.buttons())
else:
super(WColorPalette.WPaletteGrid, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
"""Mouse has been moved over widget"""
pos=event.localPos()
# calculate (row,column) cell in grid from current mouse position
row=int(pos.y()//(self.__cellSize+1))
column=int(pos.x()//(self.__cellSize+1))
# determinate color index
overIndex=self.colorIndex(row, column)
if overIndex>-1:
self.__overIndex=overIndex
self.__overCell=(row, column)
elif self.__overIndex>-1:
self.__overIndex=-1
self.__overCell=None
else:
return
# redraw palette to display marker over cell
self.update()
if isinstance(self.__overIndex, int) and self.__overIndex>-1:
swatch=self.colorFromIndex(self.__overIndex, False)
if swatch.isValid():
qColor=self.colorFromIndex(self.__overIndex, True)
else:
qColor=QColor(Qt.transparent)
self.colorOver.emit(self.__overIndex, swatch, qColor)
else:
self.colorOver.emit(-1, Swatch(), QColor())
def leaveEvent(self, event):
"""Mouse is not over widget anymore"""
self.__overIndex=-1
self.__overCell=None
self.update()
self.colorOver.emit(-1, Swatch(), QColor())
def idealSize(self):
"""Return ideal size"""
return self.__idealSize
def colorIndex(self, row, column):
"""return color index for given row/column
If no color exist for given row/column, -1 is returned
"""
if column<0 or column>=self.__columns or row<0 or row>=self.__rows:
return -1
return int(row * self.__columns + column)
def colorCoordinates(self, index):
"""Return a tuple(row, column) of given color `index` in grid
If index is not valid, return None
"""
if index<0 or index>=self.__nbColors:
return None
row = int(index//self.__columns)
column = int(index - (self.__columns * row))
return (row, column)
def colorFromIndex(self, index, asQColor=True):
"""Return color from given index
If `asQColor`is True, return a QColor otherwise return a krita Swatch
Return None is index is not valid or (if asked for QColor) if no color is defined for index
"""
if index<0 or self.__palette is None:
return None
color=self.__palette.colorSetEntryByIndex(index)
if asQColor:
if not color.isValid():
return None
if Krita.instance().activeWindow():
if Krita.instance().activeWindow().activeView():
return color.color().colorForCanvas(Krita.instance().activeWindow().activeView().canvas())
else:
return color
return None
def colorFromRowColumn(self, row, column, asQColor=True):
"""Return QColor for given row color
If no color exist for given row/column, None is returned
If `asQColor` is True (default), return a QColor otherwise return swatch
"""
colorIndex=self.colorIndex(row, column)
if colorIndex<0 or self.__palette is None:
return None
return self.colorFromIndex(colorIndex, asQColor)
def setPalette(self, palette):
"""Set current palette"""
if isinstance(palette, Palette):
self.__palette=palette
self.__nbColors=self.__palette.colorsCountTotal()
self.__columns=self.__palette.columnCount()
self.__rows=math.ceil(self.__nbColors/self.__columns)
self.invalidate()
#self.update()
def palette(self):
"""Return current applied palette"""
return self.__palette
def __init__(self, parent=None):
super(WColorPalette, self).__init__(parent)
self.__layout = QVBoxLayout(self)
self.__cbPalettes = QComboBox()
self.__scrollArea = QScrollArea()
self.__pgPalette = WColorPalette.WPaletteGrid(self.__scrollArea)
self.__scrollArea.setWidgetResizable(True)
self.__scrollArea.setWidget(self.__pgPalette)
self.__scrollArea.setFrameStyle(QFrame.NoFrame)
# note:
# QScrollArea > QWidget > QScrollBar { background: 0; }
# => setting a number allows to keep the default scrollbar style
self.__scrollArea.setStyleSheet("""
QScrollArea { background: transparent; }
QScrollArea > QWidget > QWidget { background: transparent; }
QScrollArea > QWidget > QScrollBar { background: 0; }
""")
self.__layout.addWidget(self.__cbPalettes)
self.__layout.addWidget(self.__scrollArea)
self.__layout.setContentsMargins(0, 0, 0, 0)
self.__layout.setSpacing(3)
# list of palettes (key=palette name / value=Palette())
self.__palettes={}
# current palette (name)
self.__palette=None
self.__cbPalettes.currentTextChanged.connect(self.__paletteChanged)
self.__pgPalette.colorOver.connect(self.__colorOver)
self.__pgPalette.colorClicked.connect(self.__colorClicked)
self.setSizePolicy(QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding))
self.setMaximumHeight(16777215)
self.setPalettes()
def __paletteChanged(self, palette):
"""Palette has been changed in list"""
self.__palette=palette
self.__pgPalette.setPalette(self.__palettes[self.__palette])
self.paletteChanged.emit(palette)
def __colorOver(self, index, swatch, color):
"""Mouse over a color"""
self.colorOver.emit(index, swatch, color)
def __colorClicked(self, index, swatch, color, buttons):
"""Mouse over a color"""
self.colorClicked.emit(index, swatch, color, buttons)
def updateHeight(self):
iSize=self.__pgPalette.idealSize()
if iSize.width()==-1:
height=int(0.7 * self.width())
else:
height=min(iSize.height(), int(0.7 * self.width()))
if self.__cbPalettes.isVisible():
height+=self.__cbPalettes.height()+3
self.setMinimumHeight(height + self.__cbPalettes.height())
def palette(self):
"""Return current selected palette"""
return self.__palette
def setPalette(self, palette):
"""Set current selected palette"""
if palette in self.__palettes and palette!=self.__palette:
self.__cbPalettes.setCurrentText(palette)
def palettes(self):
"""Return a dictionary of palettes resources managed by widget"""
return {name: self.__palette }
def setPalettes(self, palettes=None):
"""Set list of palettes managed by widgets
If `palettes` is None, widget will manage and expose all Krita's palettes
If `palettes` is an empty list, widget will manage the "Default" palette only
If `palettes` is a list(<str>), widget will manage the palettes from list
"""
allPalettes=Krita.instance().resources("palette")
if palettes is None:
self.__palettes={palette: Palette(allPalettes[palette]) for palette in allPalettes}
elif isinstance(palettes, str) and palettes.strip()!='':
# use the default
self.setPalettes([palettes])
elif isinstance(palettes, list) and len(palettes)==0:
# use the default
self.setPalettes(['Default'])
elif isinstance(palettes, list) and len(palettes)>1:
# use the default
self.__palettes={palette: Palette(allPalettes[palette]) for palette in palettes if palette in allPalettes}
if len(self.__palettes)==0:
# None of given palettes is available??
self.setPalettes(['Default'])
# Initialise combox
self.__cbPalettes.clear()
for palette in self.__palettes:
self.__cbPalettes.addItem(palette)
self.__cbPalettes.model().sort(0)
self.__cbPalettes.setVisible(len(self.__palettes)>1)
if 'Default' in self.__palettes:
self.setPalette('Default')
else:
self.setPalette(list(self.__palettes.keys())[0])
class WMenuPalette(QWidgetAction):
"""Encapsulate a WColorPalette as a menu item"""
def __init__(self, parent=None):
super(WMenuPalette, self).__init__(parent)
self.__colorPalette=WColorPalette()
self.setDefaultWidget(self.__colorPalette)
def colorPalette(self):
return self.__colorPalette
def colorClicked(colorIndex, colorSwatch, color, mouseButton):
view=Krita.instance().activeWindow().activeView()
if mouseButton==Qt.LeftButton:
view.setForeGroundColor(colorSwatch.color())
elif mouseButton==Qt.RightButton:
view.setBackGroundColor(colorSwatch.color())
def tweakToolBar(toolbar):
menu=QMenu()
menuPalette=WMenuPalette(menu)
menuPalette.colorPalette().setMinimumSize(460,920)
menuPalette.colorPalette().colorClicked.connect(colorClicked)
menu.addAction(menuPalette)
tb = QToolButton()
tb.setIcon(Krita.instance().icon('krita_tool_grid'))
tb.setMenu(menu)
tb.setPopupMode(QToolButton.InstantPopup)
toolbar.addSeparator()
toolbar.addWidget(tb)
wHandle=Krita.instance().activeWindow().qwindow()
toolbars=wHandle.findChildren(QToolBar)
for toolbar in toolbars:
if toolbar.objectName()=='BrushesAndStuff':
tweakToolBar(toolbar)
But it need to be embedded in a plugin to be really useable, and ensure that’s working properly in all case.
Also note, you only have here palette selection, no palette management.
And dimensions are hard-coded
Need some modification to get a dynamic size according to current selected palette and screen maximum size…
In conclusion: a really quick & dirty example
Unfortunately, I don’t have the time to code a plugin properly, but if anyone is courageaous enough the provided code can be used as a basis to write a plugin properly.
Grum999
The latest “Docker Box” feature addresses this requirement
Add docker box toolbar widget (!2104) · Merge requests · Graphics / Krita · GitLab (kde.org)
Closing this feature request as merge request 2104 satisfies this one.