Skip to content

Improve scratchpad method

Grum 999 requested to merge grum/krita:grum999/improve_scratchpad_api into master


Trying to enter in Krita's code to improve some Python API, I've started with Scratchpad.

The Scratchpad could be very useful but currently have some serious limitation.

Here the list of improvement implemented.

Implement/Expose methods

  • fillDefault()

    Fill the entire scratchpad with default color

  • fillGradient()

    Fill the entire scratchpad with current gradient

  • fillTransparent()

    Fill the entire scratchpad with transparent color

  • fillBackground()

    Fill the entire scratchpad with current background color

  • fillForeground()

    Fill the entire scratchpad with current foreground color

  • fillDocument()

    Fill the entire scratchpad with current document projection content

  • fillLayer()

    Fill the entire scratchpad with current active layer projection content

  • fillPattern()

    Fill the entire scratchpad with current pattern

  • setCanvasZoomLink()

    Makes a connection between the zoom of the canvas and scratchpad area so they zoom in sync

    This method is aimed to replace existing linkCanvasZoom() for which name is confusing

  • canvasZoomLink()

    Return if scratchpad zoom level is linked with current view zoom level

  • scale()

    Return current zoom level applied on scratchpad (whatever the zoom source is: view zoom level or set manually)

  • setScale()

    Allow to manually set scratchpad zoom level (independently of current canvas zoom level)

  • scaleToFit()

    Calculate scale automatically to fit scratchpad content in scratchpad viewport

  • scaleReset()

    Reset scale and pan to origin

  • panTo()

    Pan scratchpad content to top-left position of scratchpad viewport

  • panCenter()

    Pan scratchpad content to center content in viewport

  • viewportBounds()

    Return the viewport bounds, indicates which part of scratchpad content is visible.

  • contentBounds()

    Return the content of scratchpad, can be bigger or smaller than scratchpad dimension.

Implement/Expose signals

  • scaleChanged()

    Emitted when scratchpad scale is changed (from zoom canvas -if linked to- or manually)

  • contentChanged()

    Emitted when scratchpad content is changed (stroke or fill)

  • viewportChanged()

    Emitted when scratchpad viewport has been modified (pan, zoom, resize)


  • Implement zoom in/out in scratchpad using mouse wheel

Points of attention

In file libs/ui/widgets/kis_scratch_pad.cpp, method QRect KisScratchPad::contentBounds() const I was not able to get bound properly, and finally code a solution for which I'm not satisfied with.

Explanation of encountered problem and solution is explained in comments... It works, but I'm really sure there's a better solution for this. If someone can take a look...

Another point is in file plugins/extensions/pykrita/sip/krita/Scratchpad.sip I've tried to provide default value for some methods, like:

void fillPattern(QTransform transform = QTransform());

Normally allowing to call method in python with or without parameter.

But it doesn't work, if I implement a default value, a call to sp.fillPattern(t) generate error message Scratchpad.fillPattern() called with 1 arguments but 0 expected.

The problem is not blocking (then I didn't provided default value in committed code) but I'm really confused about what I have to code to be able to have it.

Test Plan

Scratchpad API can be tested in Scripter with following code:

import krita
from PyQt5.Qt import *
from PyQt5.QtGui import *

def gradient1(sp):
    viewportBounds = sp.viewportBounds()
    viewportBounds.setLeft(viewportBounds.left() + int(viewportBounds.width() * 0.3) )
    viewportBounds.setRight(viewportBounds.right() - int(viewportBounds.width() * 0.3))
    sp.fillGradient(viewportBounds.topLeft(), viewportBounds.bottomRight(), "bilinear", "none", False, False)

def switchCanvasZoomLink(sp, btn, dsbScale):
    current = sp.canvasZoomLink()
    qDebug(f"Current CanvasZoomLink: {current}")

    current = not current


def updateScale(scale, dsbScale):

def addRowWidgets(layout, *widgets):
    rowLayout = QHBoxLayout()
    for w in widgets:

view = None
window = Krita.instance().activeWindow()
if window:
    view = window.activeView()

if view:

    sp=krita.Scratchpad(view, QColor('#ffffff'), dlg)
    sp.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
    sp.scaleChanged.connect(lambda x: updateScale(x, dsbScale))
    sp.contentChanged.connect(lambda: qDebug(f"Content changed: {sp.contentBounds()}"))
    sp.viewportChanged.connect(lambda vp: qDebug(f"Viewport changed: {vp}"))

    btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum)

    btnLayout.setContentsMargins(0, 0, 0, 0)
    wbtn = QWidget()
    wbtn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum)

    btnClear = QPushButton('Clear')
    btnClear.clicked.connect(lambda x: sp.clear())

    btnColorWhite = QPushButton('SetFill:white')
    btnColorWhite.clicked.connect(lambda x: sp.setFillColor(QColor('#ffffff')))

    btnColorTransparent = QPushButton('SetFill:transparent')
    btnColorTransparent.clicked.connect(lambda x: sp.setFillColor(QColor(Qt.transparent)))

    addRowWidgets(btnLayout, btnClear, btnColorWhite, btnColorTransparent)

    btnFillFg = QPushButton('Fill:FG')
    btnFillFg.clicked.connect(lambda x: sp.fillForeground())

    btnFillBg = QPushButton('Fill:BG')
    btnFillBg.clicked.connect(lambda x: sp.fillBackground())

    addRowWidgets(btnLayout, btnFillFg, btnFillBg)

    btnFillGradient = QPushButton('Fill:Gradient(1)')
    btnFillGradient.clicked.connect(lambda x: gradient1(sp))

    btnFillGradient2 = QPushButton('Fill:Gradient(2)')
    btnFillGradient2.clicked.connect(lambda x: sp.fillGradient(QPoint(500, 500), QPoint(750,750), "radial", "alternate", False, True))

    addRowWidgets(btnLayout, btnFillGradient, btnFillGradient2)

    btnFillDoc = QPushButton('Fill:Document(crop)')
    btnFillDoc.clicked.connect(lambda x: sp.fillDocument(False))

    btnFillDocF = QPushButton('Fill:Document(full)')
    btnFillDocF.clicked.connect(lambda x: sp.fillDocument(True))

    btnFillLayer = QPushButton('Fill:Layer(crop)')
    btnFillLayer.clicked.connect(lambda x: sp.fillLayer(False))

    btnFillLayerF = QPushButton('Fill:Layer(full)')
    btnFillLayerF.clicked.connect(lambda x: sp.fillLayer(True))

    addRowWidgets(btnLayout, btnFillDoc, btnFillDocF, btnFillLayer, btnFillLayerF)

    btnFillLayerP = QPushButton('Fill:pattern(1)')
    btnFillLayerP.clicked.connect(lambda x: sp.fillPattern(QTransform()))

    t = QTransform()
    t.scale(1.5, 1.5)
    btnFillLayerP2 = QPushButton('Fill:pattern(2)')
    btnFillLayerP2.clicked.connect(lambda x: sp.fillPattern(t))

    addRowWidgets(btnLayout, btnFillLayerP, btnFillLayerP2)

    btnLinkC= QPushButton('CanvasZoomLink(true)')
    btnLinkC.clicked.connect(lambda x: switchCanvasZoomLink(sp, btnLinkC, dsbScale))

    dsbScale = QDoubleSpinBox()
    dsbScale.valueChanged.connect(lambda x: sp.setScale(x))

    addRowWidgets(btnLayout, btnLinkC, dsbScale)

    btnScaleFit= QPushButton('scaleFit()')
    btnScaleFit.clicked.connect(lambda x: sp.scaleToFit())

    btnScaleReset= QPushButton('scaleReset()')
    btnScaleReset.clicked.connect(lambda x: sp.scaleReset())

    addRowWidgets(btnLayout, btnScaleFit, btnScaleReset)

    btnPanToOrig= QPushButton('panTo(0, 0)')
    btnPanToOrig.clicked.connect(lambda x: sp.panTo(0, 0))

    btnPanTo= QPushButton('panTo(500, 1500)')
    btnPanTo.clicked.connect(lambda x: sp.panTo(500, 1500))

    btnPanCenter = QPushButton('panCenter()')
    btnPanCenter.clicked.connect(lambda x: sp.panCenter())

    addRowWidgets(btnLayout, btnPanToOrig, btnPanTo, btnPanCenter)

    layout.setContentsMargins(0, 0, 0, 0)


    dlg.resize(800, 800)
    print('need an active document!')

Example of UI for testing:


Formalities Checklist

  • I confirmed this builds.
  • I confirmed Krita ran and the relevant functions work.
  • I tested the relevant unit tests and can confirm they are not broken. (If not possible, don't hesitate to ask for help!)
  • I made sure my commits build individually and have good descriptions as per KDE guidelines.
  • I made sure my code conforms to the standards set in the HACKING file.
  • I can confirm the code is licensed and attributed appropriately, and that unattributed code is mine, as per KDE Licensing Policy.

Reminder: the reviewer is responsible for merging the patch, this is to ensure at the least two people can build the patch. In case a patch breaks the build, both the author and the reviewer should be contacted to fix the build. If this is not possible, the commits shall be reverted, and a notification with the reasoning and any relevant logs shall be sent to the mailing list,

Merge request reports