Palette in toolbar

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”

1 Like

Something similar to it is “tool options”
image

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 :slight_smile:
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

3 Likes

The latest “Docker Box” feature addresses this requirement
Add docker box toolbar widget (!2104) · Merge requests · Graphics / Krita · GitLab (kde.org)
image

2 Likes

Closing this feature request as merge request 2104 satisfies this one.

1 Like