Krita Source Code Documentation
Loading...
Searching...
No Matches
kis_selection_actions_panel.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2025 Ross Rosales <ross.erosales@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
8
10#include "KisDocument.h"
11#include "KisViewManager.h"
12#include "kis_canvas2.h"
14#include "kis_icon_utils.h"
15#include "kis_selection.h"
18#include <QList>
19#include <QMouseEvent>
20#include <QPointF>
21#include <QPushButton>
22#include <QTabletEvent>
23#include <QTouchEvent>
24#include <QTransform>
25#include <kactioncollection.h>
26#include <kis_algebra_2d.h>
27#include <klocalizedstring.h>
28#include <ktoggleaction.h>
29
30#include <QApplication>
31#include <QPainter>
32#include <QPainterPath>
33
34static constexpr int BUTTON_SIZE = 25;
35static constexpr int BUFFER_SPACE = 5;
36
38
46
49 {
50 }
51
54
56 bool m_pressed = false;
57 bool m_visible = false;
58 bool m_enabled = true;
59
60 struct DragHandle {
61 QPoint position = QPoint(0, 0);
62 QPoint dragOrigin = QPoint(0, 0);
63 };
64
65 QScopedPointer<DragHandle> m_dragHandle;
66
69 {
70 static const QVector<ActionButtonData> data = {
71 {"select-all", i18n("Select All"), &KisSelectionManager::selectAll},
72 {"select-invert", i18n("Invert Selection"), &KisSelectionManager::invert},
73 {"select-clear", i18n("Deselect"), &KisSelectionManager::deselect},
74 {"krita_tool_color_fill", i18n("Fill Selection with Color"), &KisSelectionManager::fillForegroundColor},
75 {"draw-eraser", i18n("Clear Selection"), &KisSelectionManager::clear},
76 {"duplicatelayer", i18n("Copy To New Layer"), &KisSelectionManager::copySelectionToNewLayer},
77 {"tool_crop", i18n("Crop to Selection"), &KisSelectionManager::imageResizeToSelection}};
78 return data;
79 }
80 int m_buttonCount = buttonData().size() + 1; // buttons + drag handle
81
83};
84
86 : QWidget(parent)
87 , d(new Private)
88{
89 d->m_viewManager = viewManager;
90 d->m_selectionManager = viewManager->selectionManager();
91
92 QWidget *canvasWidget = dynamic_cast<QWidget *>(viewManager->canvas());
93
94 // Setup buttons...
95 for (const ActionButtonData &buttonData : Private::buttonData()) {
96 QPushButton *button = createButton(buttonData.iconName, buttonData.tooltip);
97 connect(button, &QPushButton::clicked, d->m_selectionManager, buttonData.slot);
98 button->setParent(canvasWidget);
99 d->m_buttons.append(button);
100 }
101}
102
106
107void KisSelectionActionsPanel::draw(QPainter &painter)
108{
109 KisSelectionSP selection = d->m_viewManager->selection();
110
111 if (!selection || !d->m_enabled || !d->m_visible) {
112 return;
113 }
114
116
117 for (int i = 0; i < d->m_buttons.size(); i++) {
118 QPushButton *button = d->m_buttons[i];
119 int buttonPosition = i * BUTTON_SIZE;
120 button->move(d->m_dragHandle->position.x() + buttonPosition, d->m_dragHandle->position.y());
121 button->show();
122 }
123}
124
126{
127 QWidget *canvasWidget = dynamic_cast<QWidget *>(d->m_viewManager->canvas());
128 if (!canvasWidget) {
129 return;
130 }
131
132 p_visible &= d->m_enabled;
133
134 const bool VISIBILITY_CHANGED = d->m_visible != p_visible;
135 if (!VISIBILITY_CHANGED) {
136 return;
137 }
138
139 if (d->m_viewManager->selection() && p_visible) { // Now visible!
140 canvasWidget->installEventFilter(this);
141
142 if (!d->m_dragHandle) {
143 d->m_dragHandle.reset(new Private::DragHandle());
144 d->m_dragHandle->position = initialDragHandlePosition();
145 }
146 } else { // Now hidden!
147 canvasWidget->removeEventFilter(this);
148
149 for (QPushButton *button : d->m_buttons) {
150 button->hide();
151 }
152
153 d->m_pressed = false;
154 d->m_dragHandle.reset();
155 }
156
157 d->m_visible = p_visible;
158}
159
161{
162 bool configurationChanged = enabled != d->m_enabled;
163 d->m_enabled = enabled;
164 if (configurationChanged) {
165 // Reset visibility when configuration changes
166 setVisible(d->m_visible);
167 }
168}
169
170bool KisSelectionActionsPanel::eventFilter(QObject *obj, QEvent *event)
171{
172 switch (event->type()) {
173 case QEvent::FocusIn:
174 event->accept();
175 return true;
176
177 case QEvent::MouseButtonPress: {
178 const QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
179 return handlePress(event, mouseEventPos(mouseEvent), mouseEvent->button());
180 }
181 case QEvent::TabletPress: {
182 const QTabletEvent *tabletEvent = static_cast<QTabletEvent *>(event);
183 return handlePress(event, tabletEventPos(tabletEvent));
184 }
185 case QEvent::TouchBegin: {
186 const QTouchEvent *touchEvent = static_cast<QTouchEvent *>(event);
187 QPoint pos;
188 if (touchEventPos(touchEvent, pos)) {
189 return handlePress(event, pos);
190 }
191 break;
192 }
193
194 case QEvent::MouseMove:
195 if (d->m_pressed) {
196 const QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
197 return handleMove(event, mouseEventPos(mouseEvent), obj);
198 }
199 break;
200 case QEvent::TabletMove:
201 if (d->m_pressed) {
202 const QTabletEvent *tabletEvent = static_cast<QTabletEvent *>(event);
203 return handleMove(event, tabletEventPos(tabletEvent), obj);
204 }
205 break;
206 case QEvent::TouchUpdate:
207 if (d->m_pressed) {
208 const QTouchEvent *touchEvent = static_cast<QTouchEvent *>(event);
209 QPoint pos;
210 if (touchEventPos(touchEvent, pos)) {
211 return handleMove(event, pos, obj);
212 }
213 }
214 break;
215
216 case QEvent::MouseButtonRelease:
217 case QEvent::TabletRelease:
218 case QEvent::TouchEnd:
219 case QEvent::TouchCancel:
220 if (d->m_pressed) {
221 if (d->m_pressedIndex >= 0 && d->m_pressedIndex < d->m_buttons.size()) {
222 // A button was pressed, trigger it on the next event loop.
223 QTimer::singleShot(0, d->m_buttons[d->m_pressedIndex], &QPushButton::click);
224 }
225 d->m_pressed = false;
226 d->m_pressedIndex = -1;
227 event->accept();
228 return true;
229 }
230 break;
231
232 default:
233 break;
234 }
235 return false;
236}
237
238QPoint KisSelectionActionsPanel::updateCanvasBoundaries(QPoint position, QWidget *canvasWidget) const
239{
240 QRect canvasBounds = canvasWidget->rect();
241
242 const int ACTION_BAR_WIDTH = d->m_actionBarWidth;
243 const int ACTION_BAR_HEIGHT = BUTTON_SIZE;
244
245 int pos_x_min = canvasBounds.left() + BUFFER_SPACE;
246 int pos_x_max = canvasBounds.right() - ACTION_BAR_WIDTH - BUFFER_SPACE;
247
248 int pos_y_min = canvasBounds.top() + BUFFER_SPACE;
249 int pos_y_max = canvasBounds.bottom() - ACTION_BAR_HEIGHT - BUFFER_SPACE;
250
251 //Ensure that max is always bigger than min
252 //If the window is small enough max could be smaller than min
253 if (pos_x_max < pos_x_min) {
254 pos_x_max = pos_x_min;
255 }
256
257 //It is pretty implausible for it to happen vertically but better safe than sorry
258 if (pos_y_max < pos_y_min) {
259 pos_y_max = pos_y_min;
260 }
261
262 position.setX(qBound(pos_x_min, position.x(), pos_x_max));
263 position.setY(qBound(pos_y_min, position.y(), pos_y_max));
264
265 return position;
266}
267
269{
270 KisSelectionSP selection = d->m_viewManager->selection();
271 KisCanvasWidgetBase *canvas = dynamic_cast<KisCanvasWidgetBase*>(d->m_viewManager->canvas());
272 KIS_ASSERT(selection);
273 KIS_ASSERT(canvas);
274
275 QRectF selectionBounds = selection->selectedRect();
276 int selectionBottom = selectionBounds.bottom();
277 QPointF selectionCenter = selectionBounds.center();
278 QPointF bottomCenter(selectionCenter.x(), selectionBottom);
279
280 QPointF widgetBottomCenter = canvas->coordinatesConverter()->imageToWidget(bottomCenter); // converts current selection's QPointF into canvasWidget's QPointF space
281
282 widgetBottomCenter.setX(widgetBottomCenter.x() - (d->m_actionBarWidth / 2)); // centers toolbar midpoint with the selection center
283 widgetBottomCenter.setY(widgetBottomCenter.y() + BUFFER_SPACE);
284
285 return updateCanvasBoundaries(widgetBottomCenter.toPoint(), d->m_viewManager->canvas());
286}
287
288QPushButton *KisSelectionActionsPanel::createButton(const QString &iconName, const QString &tooltip)
289{
290 QPushButton *button = new QPushButton();
291 button->setIcon(KisIconUtils::loadIcon(iconName));
292 button->setFixedSize(BUTTON_SIZE, BUTTON_SIZE);
293 button->setToolTip(tooltip);
294 return button;
295}
296
298{
299 const int CORNER_RADIUS = 4;
300 const int PEN_WIDTH = 5;
301 const QColor BACKGROUND_COLOR = Qt::darkGray;
302 const QColor OUTLINE_COLOR(60, 60, 60, 80);
303 const QColor DOT_COLOR = Qt::lightGray;
304 const int DOT_SIZE = 4;
305 const int DOT_SPACING = 5;
306 const QPoint DRAG_HANDLE_RECT_DOTS_OFFSET(10, 10);
307
308 QRectF actionBarRect(d->m_dragHandle->position, QSize(d->m_actionBarWidth, BUTTON_SIZE));
309 QPainterPath bgPath;
310 bgPath.addRoundedRect(actionBarRect, CORNER_RADIUS, CORNER_RADIUS);
311 painter.fillPath(bgPath, BACKGROUND_COLOR);
312
313 QPen pen(OUTLINE_COLOR);
314 pen.setWidth(PEN_WIDTH);
315 painter.setPen(pen);
316 painter.drawPath(bgPath);
317
318 QRectF dragHandleRect(
319 QPoint(d->m_dragHandle->position.x() + d->m_actionBarWidth - BUTTON_SIZE, d->m_dragHandle->position.y()),
320 QSize(BUTTON_SIZE, BUTTON_SIZE));
321 QPainterPath dragHandlePath;
322 dragHandlePath.addRect(dragHandleRect);
323 painter.fillPath(dragHandlePath, BACKGROUND_COLOR);
324
325 const std::list<std::pair<int, int>> DOT_OFFSETS = {{0, 0},
326 {DOT_SPACING, 0},
327 {-DOT_SPACING, 0},
328 {0, DOT_SPACING},
329 {0, -DOT_SPACING},
330 {DOT_SPACING, DOT_SPACING},
331 {DOT_SPACING, -DOT_SPACING},
332 {-DOT_SPACING, DOT_SPACING},
333 {-DOT_SPACING, -DOT_SPACING}};
334
335 QPainterPath dragHandleRectDots;
336 for (const std::pair<int, int> &offset : DOT_OFFSETS) {
337 dragHandleRectDots.addEllipse(offset.first, offset.second, DOT_SIZE, DOT_SIZE);
338 };
339
340 dragHandleRectDots.translate(dragHandleRect.topLeft() + DRAG_HANDLE_RECT_DOTS_OFFSET);
341 painter.fillPath(dragHandleRectDots, DOT_COLOR);
342}
343
344bool KisSelectionActionsPanel::handlePress(QEvent *event, const QPoint &pos, Qt::MouseButton button)
345{
346 if (d->m_pressed) {
347 event->accept();
348 return true;
349 }
350
351 if (button == Qt::LeftButton) {
352 QRect targetRect(d->m_dragHandle->position, QSize(BUTTON_SIZE * d->m_buttonCount, BUTTON_SIZE));
353 if (targetRect.contains(pos)) {
354 d->m_pressed = true;
355
356 d->m_pressedIndex = (pos.x() - targetRect.left()) / BUTTON_SIZE;
357 if (d->m_pressedIndex < 0 || d->m_pressedIndex >= d->m_buttons.size()) {
358 d->m_dragHandle->dragOrigin = pos - d->m_dragHandle->position;
359 }
360
361 event->accept();
362 return true;
363 }
364 }
365
366 return false;
367}
368
369bool KisSelectionActionsPanel::handleMove(QEvent *event, const QPoint &pos, QObject *obj)
370{
371 // Are we dragging the bar or was a button pressed?
372 if (d->m_pressedIndex < 0 || d->m_pressedIndex >= d->m_buttons.size()) {
373 // bound actionBar to stay within canvas space
374 QWidget *canvasWidget = d->m_viewManager->canvas();
375
376 // Explicitly casting just in case inheritance adjusts the pointer weirdly
377 // (MSVC does spicy things like that sometimes to keep you on your toes.)
378 if (obj == static_cast<QObject *>(canvasWidget)) {
379 QPoint newPos = pos - d->m_dragHandle->dragOrigin;
380 d->m_dragHandle->position = updateCanvasBoundaries(newPos, canvasWidget);
381 canvasWidget->update();
382 event->accept();
383 return true;
384 } else {
385 return false;
386 }
387 } else {
388 // Button was pressed, we're just waiting for a release.
389 event->accept();
390 return true;
391 }
392}
393
394QPoint KisSelectionActionsPanel::mouseEventPos(const QMouseEvent *mouseEvent)
395{
396#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
397 return mouseEvent->position().toPoint();
398#else
399 return mouseEvent->pos();
400#endif
401}
402
403QPoint KisSelectionActionsPanel::tabletEventPos(const QTabletEvent *tabletEvent)
404{
405#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
406 return tabletEvent->position().toPoint();
407#else
408 return tabletEvent->pos();
409#endif
410}
411
412bool KisSelectionActionsPanel::touchEventPos(const QTouchEvent *touchEvent, QPoint &outPos)
413{
414#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
415 if (touchEvent->pointCount() < 1) {
416 return false;
417 } else {
418 outPos = touchEvent->points().first().position().toPoint();
419 return true;
420 }
421#else
422 const QList<QTouchEvent::TouchPoint> &touchPoints = touchEvent->touchPoints();
423 if (touchPoints.isEmpty()) {
424 return false;
425 } else {
426 outPos = touchPoints.first().pos().toPoint();
427 return true;
428 }
429#endif
430}
KisCoordinatesConverter * coordinatesConverter() const
_Private::Traits< T >::Result imageToWidget(const T &obj) const
bool handlePress(QEvent *event, const QPoint &pos, Qt::MouseButton button=Qt::LeftButton)
QPoint updateCanvasBoundaries(QPoint position, QWidget *canvasWidget) const
bool eventFilter(QObject *obj, QEvent *event) override
static QPoint tabletEventPos(const QTabletEvent *tabletEvent)
static QPoint mouseEventPos(const QMouseEvent *mouseEvent)
static bool touchEventPos(const QTouchEvent *touchEvent, QPoint &outPos)
bool handleMove(QEvent *event, const QPoint &pos, QObject *obj)
void drawActionBarBackground(QPainter &gc) const
QPushButton * createButton(const QString &iconName, const QString &tooltip)
QWidget * canvas() const
Return the actual widget that is displaying the current image.
KisSelectionManager * selectionManager()
#define KIS_ASSERT(cond)
Definition kis_assert.h:33
typedef void(QOPENGLF_APIENTRYP PFNGLINVALIDATEBUFFERDATAPROC)(GLuint buffer)
static constexpr int BUFFER_SPACE
static constexpr int BUTTON_SIZE
QString button(const QWheelEvent &ev)
QIcon loadIcon(const QString &name)
void(KisSelectionManager::*)() TargetSlot
static const QVector< ActionButtonData > & buttonData()
QRect selectedRect() const