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”

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

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

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