Krita Source Code Documentation
Loading...
Searching...
No Matches
kis_tool_utils.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2009 Boudewijn Rempt <boud@valdyas.org>
3 * SPDX-FileCopyrightText: 2018 Emmet & Eoin O'Neill <emmetoneill.pdx@gmail.com>
4 *
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8#include <kis_tool_utils.h>
9
10#include <KoMixColorsOp.h>
11#include <kis_group_layer.h>
12#include <kis_transaction.h>
15#include <kconfiggroup.h>
16#include <ksharedconfig.h>
17#include "kis_layer_utils.h"
18#include "kis_command_utils.h"
20
21#include "kis_canvas2.h"
22#include <QPainterPath>
23#include <kis_shape_layer.h>
24#include <KoShapeManager.h>
25#include <KoShape.h>
26#include <KoPathShape.h>
27#include <KoSvgTextShape.h>
28#include <KoSvgTextProperties.h>
29#include <KisViewManager.h>
30#include <kis_node_manager.h>
31#include <KoSelection.h>
32#include <kis_action_registry.h>
33#include <KoToolBase.h>
34
35#include <algorithm>
36
37#include "KisAnimAutoKey.h"
38
39#include <QApplication>
40
41namespace KisToolUtils {
42
43 bool sampleColor(KoColor &out_color, KisPaintDeviceSP dev, const QPoint &pos,
44 KoColor const *const blendColor, int radius, int blend, bool pure)
45 {
46 KIS_ASSERT(dev);
47
48 // Bugfix hack forcing pure on first sample to avoid wrong
49 // format blendColor on newly initialized Krita.
50 static bool firstTime = true;
51 if (firstTime == true) {
52 pure = true;
53 firstTime = false;
54 }
55
56 const KoColorSpace *cs = dev->colorSpace();
57 KoColor sampledColor = KoColor::createTransparent(cs);
58
59 // Wrap around color sampling is supported on any paint device
60 bool oldSupportsWraparound = dev->supportsWraproundMode();
62
63 // Sampling radius.
64 if (!pure && radius > 1) {
65 QScopedPointer<KoMixColorsOp::Mixer> mixer(cs->mixColorsOp()->createMixer());
66
68 const int effectiveRadius = radius - 1;
69
70 const QRect sampleRect(pos.x() - effectiveRadius, pos.y() - effectiveRadius,
71 2 * effectiveRadius + 1, 2 * effectiveRadius + 1);
72 KisSequentialConstIterator it(dev, sampleRect);
73
74 const int radiusSq = pow2(effectiveRadius);
75
76 int nConseqPixels = it.nConseqPixels();
77 while (it.nextPixels(nConseqPixels)) {
78 const QPoint realPos(it.x(), it.y());
79 if (kisSquareDistance(realPos, pos) < radiusSq) {
80 mixer->accumulateAverage(it.oldRawData(), nConseqPixels);
81 }
82 }
83
84 mixer->computeMixedColor(sampledColor.data());
85
86 } else {
87 dev->pixel(pos.x(), pos.y(), &sampledColor);
88 }
89
90 dev->setSupportsWraparoundMode(oldSupportsWraparound);
91
92 // Color blending.
93 if (!pure && blendColor && blend < 100) {
94 //Scale from 0..100% to 0..255 range for mixOp weights.
95 quint8 blendScaled = static_cast<quint8>(blend * 2.55f);
96
97 const quint8 *colors[2];
98 colors[0] = blendColor->data();
99 colors[1] = sampledColor.data();
100 qint16 weights[2];
101 weights[0] = 255 - blendScaled;
102 weights[1] = blendScaled;
103
104 const KoMixColorsOp *mixOp = dev->colorSpace()->mixColorsOp();
105 mixOp->mixColors(colors, weights, 2, sampledColor.data());
106 }
107
108 sampledColor.convertTo(dev->compositionSourceColorSpace());
109
110 bool validColorSampled = sampledColor.opacityU8() != OPACITY_TRANSPARENT_U8;
111
112 if (validColorSampled) {
113 out_color = sampledColor;
114 }
115
116 return validColorSampled;
117 }
118
119 KisNodeSP findNode(KisNodeSP node, const QPoint &point, bool wholeGroup, bool editableOnly)
120 {
121 KisNodeSP foundNode = 0;
122 while (node) {
123 KisLayerSP layer = qobject_cast<KisLayer*>(node.data());
124
125 if (!layer || !layer->isEditable()) {
126 node = node->prevSibling();
127 continue;
128 }
129
130 KoColor color(layer->projection()->colorSpace());
131 layer->projection()->pixel(point.x(), point.y(), &color);
132
133 KisGroupLayerSP group = dynamic_cast<KisGroupLayer*>(layer.data());
134
135 if ((group && group->passThroughMode()) || color.opacityU8() != OPACITY_TRANSPARENT_U8) {
136 if (layer->inherits("KisGroupLayer") && (!editableOnly || layer->isEditable())) {
137 // if this is a group and the pixel is transparent, don't even enter it
138 foundNode = findNode(node->lastChild(), point, wholeGroup, editableOnly);
139 }
140 else {
141 foundNode = !wholeGroup ? node : node->parent();
142 }
143
144 }
145
146 if (foundNode) break;
147
148 node = node->prevSibling();
149 }
150
151 return foundNode;
152 }
153
154 KisNodeList findNodes(KisNodeSP node, const QPoint &point, bool wholeGroup, bool includeGroups, bool editableOnly)
155 {
156 KisNodeList foundNodes;
157 while (node) {
158 KisLayerSP layer = qobject_cast<KisLayer*>(node.data());
159
160 if (!layer || !layer->isEditable()) {
161 node = node->nextSibling();
162 continue;
163 }
164
165 KoColor color(layer->projection()->colorSpace());
166 layer->projection()->pixel(point.x(), point.y(), &color);
167 const bool isTransparent = color.opacityU8() == OPACITY_TRANSPARENT_U8;
168
169 KisGroupLayerSP group = dynamic_cast<KisGroupLayer*>(layer.data());
170
171 if (group) {
172 if (!isTransparent || group->passThroughMode()) {
173 foundNodes << findNodes(node->firstChild(), point, wholeGroup, includeGroups, editableOnly);
174 if (includeGroups) {
175 foundNodes << node;
176 }
177 }
178 } else {
179 if (!isTransparent) {
180 if (wholeGroup) {
181 if (!foundNodes.contains(node->parent())) {
182 foundNodes << node->parent();
183 }
184 } else {
185 foundNodes << node;
186 }
187 }
188 }
189
190 node = node->nextSibling();
191 }
192
193 return foundNodes;
194 }
195
196 bool clearImage(KisImageSP image, KisNodeList nodes, KisSelectionSP selection)
197 {
198 KisNodeList masks;
199
200 Q_FOREACH (KisNodeSP node, nodes) {
201 if (node->inherits("KisMask")) {
202 masks.append(node);
203 }
204 }
205
206 // To prevent deleting same layer multiple times
208 nodes.append(masks);
209
210 if (nodes.isEmpty()) {
211 return false;
212 }
213
216
217 Q_FOREACH (KisNodeSP node, nodes) {
218 KisLayerUtils::recursiveApplyNodes(node, [&applicator, selection, masks] (KisNodeSP node) {
219
220 // applied on masks if selected explicitly
221 if (node->inherits("KisMask") && !masks.contains(node)) {
222 return;
223 }
224
225 if(node->hasEditablePaintDevice()) {
226 KUndo2Command *cmd =
227 new KisCommandUtils::LambdaCommand(kundo2_i18n("Clear"),
228 [node, selection] () {
229 KisPaintDeviceSP device = node->paintDevice();
230
231 QScopedPointer<KisCommandUtils::CompositeCommand> parentCommand(
232 new KisCommandUtils::CompositeCommand());
233
234 KUndo2Command *autoKeyframeCommand = KisAutoKey::tryAutoCreateDuplicatedFrame(device);
235 if (autoKeyframeCommand) {
236 parentCommand->addCommand(autoKeyframeCommand);
237 }
238
239 KisTransaction transaction(kundo2_noi18n("internal-clear-command"), device);
240
241 QRect dirtyRect;
242 if (selection) {
243 dirtyRect = selection->selectedRect();
244 device->clearSelection(selection);
245 } else {
246 dirtyRect = device->extent();
247 device->clear();
248 }
249
250 device->setDirty(dirtyRect);
251 parentCommand->addCommand(transaction.endAndTake());
252
253 return parentCommand.take();
254 });
256 }
257 });
258 }
259
260 applicator.end();
261
262 return true;
263 }
264
265 const QString ColorSamplerConfig::CONFIG_GROUP_NAME = "tool_color_sampler";
266
267 ColorSamplerConfig::ColorSamplerConfig()
268 : toForegroundColor(true)
269 , updateColor(true)
270 , addColorToCurrentPalette(false)
271 , normaliseValues(false)
272 , sampleMerged(true)
273 , radius(1)
274 , blend(100)
275 {
276 }
277
279 {
281 props.setProperty("toForegroundColor", toForegroundColor);
282 props.setProperty("updateColor", updateColor);
283 props.setProperty("addPalette", addColorToCurrentPalette);
284 props.setProperty("normaliseValues", normaliseValues);
285 props.setProperty("sampleMerged", sampleMerged);
286 props.setProperty("radius", radius);
287 props.setProperty("blend", blend);
288
289 KConfigGroup config = KSharedConfig::openConfig()->group(CONFIG_GROUP_NAME);
290
291 config.writeEntry("ColorSamplerDefaultActivation", props.toXML());
292 }
293
295 {
297
298 KConfigGroup config = KSharedConfig::openConfig()->group(CONFIG_GROUP_NAME);
299 props.fromXML(config.readEntry("ColorSamplerDefaultActivation"));
300
301 toForegroundColor = props.getBool("toForegroundColor", true);
302 updateColor = props.getBool("updateColor", true);
303 addColorToCurrentPalette = props.getBool("addPalette", false);
304 normaliseValues = props.getBool("normaliseValues", false);
305 sampleMerged = props.getBool("sampleMerged", true);
306 radius = props.getInt("radius", 1);
307 blend = props.getInt("blend", 100);
308 }
309
310 void KRITAUI_EXPORT setCursorPos(const QPoint &point)
311 {
312 // https://bugreports.qt.io/browse/QTBUG-99009
313 QScreen *screen = qApp->screenAt(point);
314 if (!screen) {
315 screen = qApp->primaryScreen();
316 }
317 QCursor::setPos(screen, point);
318 }
319
320 void KRITAUI_EXPORT showBrushSizeFloatingMessage(KoCanvasBase *canvas, qreal size)
321 {
322 KisCanvas2 *kisCanvas = dynamic_cast<KisCanvas2 *>(canvas);
324 kisCanvas->viewManager()->showFloatingMessage(i18n("Brush Size: %1 px", size),
325 QIcon(),
326 1000,
328 Qt::AlignLeft | Qt::TextWordWrap | Qt::AlignVCenter);
329 }
330
331 // get all shape layers with shapes at point. This is a bit coarser than 'FindNodes',
332 // note that point is in Document coordinates instead of image coordinates.
333 QList<KisShapeLayerSP> findShapeLayers(KisNodeSP root, const QPointF &point, bool editableOnly) {
334 QList<KisShapeLayerSP> foundNodes;
336 if ((node->isEditable(true) && editableOnly) || !editableOnly) {
337
338 KisShapeLayerSP shapeLayer = dynamic_cast<KisShapeLayer*>(node.data());
339 if (shapeLayer && shapeLayer->isEditable() && shapeLayer->shapeManager()->shapeAt(point)) {
340 foundNodes.append(shapeLayer);
341 }
342 }
343 });
344 return foundNodes;
345 }
346
347 QPainterPath shapeHoverInfoCrossLayer(KoCanvasBase *canvas, const QPointF &point, QString &shapeType, bool *isHorizontal, bool skipCurrentShapes)
348 {
349 QPainterPath p;
350 KisCanvas2 *canvas2 = dynamic_cast<KisCanvas2*>(canvas);
351 if (!canvas2) return p;
352
353 QList<KoShape*> currentShapes = canvas->shapeManager()->selection()->selectedShapes();
354 QList<KisShapeLayerSP> candidates = findShapeLayers(canvas2->image()->root(), point, true);
355 KisShapeLayerSP shapeLayer = candidates.isEmpty()? nullptr: candidates.last();
356
357 if (shapeLayer) {
358 KoShape *shape = shapeLayer->shapeManager()->shapeAt(point);
359 if (shape && !(currentShapes.contains(shape) && skipCurrentShapes)) {
360 shapeType = shape->shapeId();
361 KoSvgTextShape *t = dynamic_cast<KoSvgTextShape *>(shape);
362 if (t) {
363 p.addRect(t->boundingRect());
364 if (isHorizontal) {
365 *isHorizontal = t->writingMode() == KoSvgText::HorizontalTB;
366 }
367 if (!t->shapesInside().isEmpty()) {
368 QPainterPath paths;
369 Q_FOREACH(KoShape *s, t->shapesInside()) {
370 KoPathShape *path = dynamic_cast<KoPathShape *>(s);
371 if (path) {
372 paths.addPath(path->absoluteTransformation().map(path->outline()));
373 }
374 }
375 if (!paths.isEmpty()) {
376 p = paths;
377 }
378 }
379 } else {
380 p = shape->absoluteTransformation().map(shape->outline());
381 }
382 }
383 }
384
385 return p;
386 }
387
388 bool selectShapeCrossLayer(KoCanvasBase *canvas, const QPointF &point, const QString &shapeType, bool skipCurrentShapes)
389 {
390 KisCanvas2 *canvas2 = dynamic_cast<KisCanvas2*>(canvas);
391 if (!canvas2) return false;
392
393 QList<KoShape*> currentShapes = canvas->shapeManager()->selection()->selectedShapes();
394 QList<KisShapeLayerSP> candidates = findShapeLayers(canvas2->image()->root(), point, true);
395 KisShapeLayerSP shapeLayer = candidates.isEmpty()? nullptr: candidates.last();
396
397 if (shapeLayer) {
398 KoShape *shape = shapeLayer->shapeManager()->shapeAt(point);
399 if (shape
400 && !(currentShapes.contains(shape) && skipCurrentShapes)
401 && (shapeType.isEmpty() || shapeType == shape->shapeId())) {
402 canvas2->viewManager()->nodeManager()->slotNonUiActivatedNode(shapeLayer);
403 canvas2->shapeManager()->selection()->deselectAll();
404 canvas2->shapeManager()->selection()->select(shape);
405 } else {
406 return false;
407 }
408 } else {
409 return false;
410 }
411
412 return true;
413 }
414
415
417 : m_tool(tool)
418 {
419 }
420
422 {
424 QList<QAction *> actions;
425
426 actions << actionRegistry->makeQAction("movetool-move-up", actionRegistry);
427 actions << actionRegistry->makeQAction("movetool-move-down", actionRegistry);
428 actions << actionRegistry->makeQAction("movetool-move-left", actionRegistry);
429 actions << actionRegistry->makeQAction("movetool-move-right", actionRegistry);
430
431 actions << actionRegistry->makeQAction("movetool-move-up-more", actionRegistry);
432 actions << actionRegistry->makeQAction("movetool-move-down-more", actionRegistry);
433 actions << actionRegistry->makeQAction("movetool-move-left-more", actionRegistry);
434 actions << actionRegistry->makeQAction("movetool-move-right-more", actionRegistry);
435
436 return actions;
437 }
438
440 {
441 m_tool->action("movetool-move-up")->setEnabled(value);
442 m_tool->action("movetool-move-down")->setEnabled(value);
443 m_tool->action("movetool-move-left")->setEnabled(value);
444 m_tool->action("movetool-move-right")->setEnabled(value);
445 m_tool->action("movetool-move-up-more")->setEnabled(value);
446 m_tool->action("movetool-move-down-more")->setEnabled(value);
447 m_tool->action("movetool-move-left-more")->setEnabled(value);
448 m_tool->action("movetool-move-right-more")->setEnabled(value);
449 }
450
451 QString nodeEditableMessage(KisNodeSP node, bool blockedNoIndirectPainting)
452 {
453 QString message;
454 if (!node->isEditable(true) || blockedNoIndirectPainting) {
455 if (!node->visible() && node->userLocked()) {
456 message = i18n("Layer is locked and invisible.");
457 } else if (node->userLocked()) {
458 message = i18n("Layer is locked.");
459 } else if(!node->visible()) {
460 message = i18n("Layer is invisible.");
461 } else if (blockedNoIndirectPainting) {
462 message = i18n("Layer can be painted in Wash Mode only.");
463 } else {
464 message = i18n("Group not editable.");
465 }
466 }
467 return message;
468 }
469
470
472 {
473 int brushSize = minSize;
474 do {
475 m_sizes.push_back(brushSize);
476 int increment = qMax(1, int(std::ceil(qreal(brushSize) / 15)));
477 brushSize += increment;
478 } while (brushSize < maxSize);
479 m_sizes.push_back(maxSize);
480 }
481
483 {
484 std::vector<int>::iterator result = std::upper_bound(m_sizes.begin(), m_sizes.end(), qRound(size));
485 return result != m_sizes.end() ? *result : m_sizes.back();
486 }
487
489 {
490 std::vector<int>::reverse_iterator result =
491 std::upper_bound(m_sizes.rbegin(), m_sizes.rend(), qRound(size), std::greater<int>());
492 return result != m_sizes.rend() ? *result : m_sizes.front();
493 }
494
495
496}
float value(const T *src, size_t ch)
const Params2D p
QVector< KisImageSignalType > KisImageSignalVector
const quint8 OPACITY_TRANSPARENT_U8
QAction * makeQAction(const QString &name, QObject *parent=0)
static KisActionRegistry * instance()
KisImageWSP image() const
KoShapeManager shapeManager
KisViewManager * viewManager() const
void slotNonUiActivatedNode(KisNodeSP node)
bool supportsWraproundMode() const
virtual const KoColorSpace * compositionSourceColorSpace() const
const KoColorSpace * colorSpace() const
void setSupportsWraparoundMode(bool value)
bool pixel(qint32 x, qint32 y, QColor *c) const
void applyCommand(KUndo2Command *command, KisStrokeJobData::Sequentiality sequentiality=KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::Exclusivity exclusivity=KisStrokeJobData::NORMAL)
ALWAYS_INLINE int x() const
ALWAYS_INLINE const quint8 * oldRawData() const
ALWAYS_INLINE int y() const
KoShapeManager * shapeManager() const
static QList< QAction * > createActions()
void setInternalMoveShortcutsEnabled(bool value)
StandardBrushSizes(int minSize, int maxSize)
KisNodeManager * nodeManager() const
The node manager handles everything about nodes.
void showFloatingMessage(const QString &message, const QIcon &icon, int timeout=4500, KisFloatingMessage::Priority priority=KisFloatingMessage::Medium, int alignment=Qt::AlignCenter|Qt::TextWordWrap)
shows a floating message in the top right corner of the canvas
virtual KoShapeManager * shapeManager() const =0
KoMixColorsOp * mixColorsOp
void convertTo(const KoColorSpace *cs, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags)
Definition KoColor.cpp:136
static KoColor createTransparent(const KoColorSpace *cs)
Definition KoColor.cpp:682
quint8 * data()
Definition KoColor.h:144
quint8 opacityU8() const
Definition KoColor.cpp:341
virtual Mixer * createMixer() const =0
virtual void mixColors(const quint8 *const *colors, const qint16 *weights, int nColors, quint8 *dst, int weightSum=255) const =0
The position of a path point within a path shape.
Definition KoPathShape.h:63
void deselectAll()
clear the selections list
void select(KoShape *shape)
const QList< KoShape * > selectedShapes() const
KoShape * shapeAt(const QPointF &position, KoFlake::ShapeSelection selection=KoFlake::ShapeOnTop, bool omitHiddenShapes=true)
KoSelection * selection
QString shapeId() const
Definition KoShape.cpp:875
virtual QPainterPath outline() const
Definition KoShape.cpp:554
QTransform absoluteTransformation() const
Definition KoShape.cpp:330
QRectF boundingRect() const override
Get the bounding box of the shape.
QList< KoShape * > shapesInside
KoSvgText::WritingMode writingMode() const
writingMode There's a number of places we need to check the writing mode to provide proper controls.
QAction * action(const QString &name) const
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:128
#define KIS_ASSERT(cond)
Definition kis_assert.h:33
T pow2(const T &x)
Definition kis_global.h:166
qreal kisSquareDistance(const QPointF &pt1, const QPointF &pt2)
Definition kis_global.h:194
KUndo2MagicString kundo2_i18n(const char *text)
void recursiveApplyNodes(NodePointer node, Functor func)
void filterMergeableNodes(KisNodeList &nodes, bool allowMasks)
void KRITAUI_EXPORT showBrushSizeFloatingMessage(KoCanvasBase *canvas, qreal size)
bool selectShapeCrossLayer(KoCanvasBase *canvas, const QPointF &point, const QString &shapeType, bool skipCurrentShapes)
selectShapeCrossLayer Tries to select a shape under the cursor regardless of which layer it is on,...
bool clearImage(KisImageSP image, KisNodeList nodes, KisSelectionSP selection)
void KRITAUI_EXPORT setCursorPos(const QPoint &point)
KisNodeList findNodes(KisNodeSP node, const QPoint &point, bool wholeGroup, bool includeGroups, bool editableOnly)
QString nodeEditableMessage(KisNodeSP node, bool blockedNoIndirectPainting)
nodeEditableMessage
QPainterPath shapeHoverInfoCrossLayer(KoCanvasBase *canvas, const QPointF &point, QString &shapeType, bool *isHorizontal, bool skipCurrentShapes)
shapeHoverInfoCrossLayer get hover info of shapes on all layers.
KisNodeSP findNode(KisNodeSP node, const QPoint &point, bool wholeGroup, bool editableOnly)
bool sampleColor(KoColor &out_color, KisPaintDeviceSP dev, const QPoint &pos, KoColor const *const blendColor, int radius, int blend, bool pure)
QList< KisShapeLayerSP > findShapeLayers(KisNodeSP root, const QPointF &point, bool editableOnly)
@ HorizontalTB
Definition KoSvgText.h:38
bool isEditable(bool checkVisibility=true) const
bool userLocked() const
virtual bool visible(bool recursive=false) const
bool hasEditablePaintDevice() const
KisPaintDeviceSP projection() const override
Definition kis_layer.cc:826
KisNodeSP prevSibling() const
Definition kis_node.cpp:402
KisNodeSP firstChild() const
Definition kis_node.cpp:361
KisNodeWSP parent
Definition kis_node.cpp:86
KisNodeSP lastChild() const
Definition kis_node.cpp:367
KisNodeSP nextSibling() const
Definition kis_node.cpp:408
void toXML(QDomDocument &, QDomElement &) const override
bool fromXML(const QString &xml, bool clear=true) override
virtual void setProperty(const QString &name, const QVariant &value)
bool getBool(const QString &name, bool def=false) const
int getInt(const QString &name, int def=0) const
static const QString CONFIG_GROUP_NAME