Krita Source Code Documentation
Loading...
Searching...
No Matches
KisAngleSelector.cpp
Go to the documentation of this file.
1/*
2 * KDE. Krita Project.
3 *
4 * SPDX-FileCopyrightText: 2020 Deif Lou <ginoba@gmail.com>
5 *
6 * SPDX-License-Identifier: GPL-2.0-or-later
7 */
8
9#include <cmath>
10#include <functional>
11
12#include <QHBoxLayout>
13#include <QToolButton>
14#include <QAction>
15#include <QEvent>
16#include <QMenu>
17#include <QLineEdit>
18#include <QStyleOptionSpinBox>
19#include <QStyle>
20#include <QContextMenuEvent>
21
22#include <kis_icon_utils.h>
23#include <kis_signals_blocker.h>
24
25#include <klocalizedstring.h>
26
27#include "KisAngleSelector.h"
28
30{
32 bool isFlat;
36
38 {
39 if (!isFlat || hasFocus || isHovered) {
40 q->setStyleSheet("QDoubleSpinBox{}");
41 } else {
42 q->setStyleSheet(
43 "QDoubleSpinBox{background:transparent; border:transparent;}"
44 "QDoubleSpinBox::up-button{background:transparent; border:transparent;}"
45 "QDoubleSpinBox::down-button{background:transparent; border:transparent;}"
46 );
47 }
48 q->lineEdit()->setStyleSheet("QLineEdit{background:transparent;}");
49 }
50};
51
53 : KisDoubleParseSpinBox(parent)
54 , m_d(new Private)
55{
56 m_d->q = this;
57 m_d->isFlat = false;
58 m_d->hasFocus = false;
59 m_d->isHovered = false;
60 m_d->updateStyleSheet();
61}
62
65
66void KisAngleSelectorSpinBox::setRange(double min, double max)
67{
68 m_d->cachedSizeHint = QSize();
69 KisDoubleParseSpinBox::setRange(min, max);
70}
71
72double KisAngleSelectorSpinBox::valueFromText(const QString & text) const
73{
74 const double v = KisDoubleParseSpinBox::valueFromText(text);
75 return KisAngleSelector::closestCoterminalAngleInRange(v, minimum(), maximum());
76}
77
79{
80 return m_d->isFlat;
81}
82
84{
85 m_d->isFlat = newFlat;
86 m_d->updateStyleSheet();
87}
88#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
90#else
92#endif
93{
94 m_d->isHovered = true;
95 m_d->updateStyleSheet();
96 KisDoubleParseSpinBox::enterEvent(e);
97}
98
100{
101 m_d->isHovered = false;
102 m_d->updateStyleSheet();
103 KisDoubleParseSpinBox::leaveEvent(e);
104}
105
107{
108 m_d->hasFocus = true;
109 m_d->updateStyleSheet();
110 KisDoubleParseSpinBox::focusInEvent(e);
111}
112
114{
115 m_d->hasFocus = false;
116 m_d->updateStyleSheet();
117 KisDoubleParseSpinBox::focusOutEvent(e);
118}
119
121{
122 if (m_d->cachedSizeHint.isEmpty()) {
123 ensurePolished();
124
125 const QFontMetrics fm(fontMetrics());
126 int h = lineEdit()->minimumSizeHint().height();
127 int w = 0;
128
129 QString s;
130 QString fixedContent = prefix() + suffix() + QLatin1Char(' ');
131 s = textFromValue(minimum());
132 s.truncate(18);
133 s += fixedContent;
134 w = qMax(w, fm.horizontalAdvance(s));
135 s = textFromValue(maximum());
136 s.truncate(18);
137 s += fixedContent;
138 w = qMax(w, fm.horizontalAdvance(s));
139
140 w += 2; // cursor blinking space
141
142 QStyleOptionSpinBox option;
143 initStyleOption(&option);
144
145 QSize hint(w, h);
146
148 m_d->cachedSizeHint = style()->sizeFromContents(QStyle::CT_SpinBox, &option, hint, &tmp);
149 }
150
151 return m_d->cachedSizeHint;
152}
153
155{
156 return minimumSizeHint();
157}
158
160{
161 m_d->cachedSizeHint = QSize();
162 updateGeometry();
163 m_d->updateStyleSheet();
164}
165
193
195 : QWidget(parent)
196 , m_d(new Private)
197{
198 m_d->q = this;
199
200 QHBoxLayout *mainLayout = new QHBoxLayout;
201 mainLayout->setSpacing(5);
202 mainLayout->setContentsMargins(0, 0, 0, 0);
203
204 m_d->angleGauge = new KisAngleGauge(this);
205 m_d->angleGauge->installEventFilter(this);
206
207 m_d->spinBox = new KisAngleSelectorSpinBox(this);
208 m_d->spinBox->setSuffix(i18nc("Degrees symbol", "˚"));
209 m_d->spinBox->setRange(0, 360);
210 m_d->spinBox->setWrapping(true);
211
212 m_d->actionFlipHorizontally = new QAction(this);
213 m_d->actionFlipHorizontally->setText(
214 i18nc(
215 "Flips the angle horizontally, around the vertical axis",
216 "Flip the angle horizontally"
217 )
218 );
219 m_d->actionFlipVertically = new QAction(this);
220 m_d->actionFlipVertically->setText(
221 i18nc(
222 "Flips the angle vertically, around the horizontal axis",
223 "Flip the angle vertically"
224 )
225 );
226 m_d->actionFlipHorizontallyAndVertically = new QAction(this);
227 m_d->actionFlipHorizontallyAndVertically->setText(
228 i18nc(
229 "Flips the angle horizontally and vertically",
230 "Flip the angle horizontally and vertically"
231 )
232 );
233 QAction *menuSeparator = new QAction(this);
234 menuSeparator->setSeparator(true);
235 m_d->actionResetAngle = new QAction(this);
236 m_d->actionResetAngle->setText(
237 i18nc(
238 "Reset the angle to a predefined value",
239 "Reset angle"
240 )
241 );
242 m_d->menuFlip = new QMenu(this);
243 m_d->menuFlip->addAction(m_d->actionFlipHorizontally);
244 m_d->menuFlip->addAction(m_d->actionFlipVertically);
245 m_d->menuFlip->addAction(m_d->actionFlipHorizontallyAndVertically);
246 m_d->menuFlip->addAction(menuSeparator);
247 m_d->menuFlip->addAction(m_d->actionResetAngle);
248
249 QHBoxLayout *layoutFlipButtons = new QHBoxLayout;
250 layoutFlipButtons->setSpacing(1);
251 layoutFlipButtons->setContentsMargins(0, 0, 0, 0);
252
253 m_d->toolButtonFlipOptions = new QToolButton(this);
254 m_d->toolButtonFlipOptions->setPopupMode(QToolButton::InstantPopup);
255 m_d->toolButtonFlipOptions->setAutoRaise(true);
256 m_d->toolButtonFlipOptions->setIcon(KisIconUtils::loadIcon("view-choose"));
257 m_d->toolButtonFlipOptions->setStyleSheet("QToolButton::menu-indicator { image: none; }");
258 m_d->toolButtonFlipOptions->setMenu(m_d->menuFlip);
259 m_d->toolButtonFlipOptions->setFocusPolicy(Qt::StrongFocus);
260
261 m_d->toolButtonFlipHorizontally = new QToolButton(this);
262 m_d->toolButtonFlipHorizontally->setAutoRaise(true);
263 m_d->toolButtonFlipHorizontally->setIcon(KisIconUtils::loadIcon("flip_angle_h"));
264 m_d->toolButtonFlipHorizontally->setIconSize(QSize(20, 20));
265 m_d->toolButtonFlipHorizontally->setToolTip(m_d->actionFlipHorizontally->text());
266 m_d->toolButtonFlipHorizontally->setFocusPolicy(Qt::StrongFocus);
267
268 m_d->toolButtonFlipVertically = new QToolButton(this);
269 m_d->toolButtonFlipVertically->setAutoRaise(true);
270 m_d->toolButtonFlipVertically->setIcon(KisIconUtils::loadIcon("flip_angle_v"));
271 m_d->toolButtonFlipVertically->setIconSize(QSize(20, 20));
272 m_d->toolButtonFlipVertically->setToolTip(m_d->actionFlipVertically->text());
273 m_d->toolButtonFlipVertically->setFocusPolicy(Qt::StrongFocus);
274
275 m_d->toolButtonFlipHorizontallyAndVertically = new QToolButton(this);
276 m_d->toolButtonFlipHorizontallyAndVertically->setAutoRaise(true);
277 m_d->toolButtonFlipHorizontallyAndVertically->setIcon(KisIconUtils::loadIcon("flip_angle_hv"));
278 m_d->toolButtonFlipHorizontallyAndVertically->setIconSize(QSize(20, 20));
279 m_d->toolButtonFlipHorizontallyAndVertically->setToolTip(m_d->actionFlipHorizontallyAndVertically->text());
280 m_d->toolButtonFlipHorizontallyAndVertically->setFocusPolicy(Qt::StrongFocus);
281
282 layoutFlipButtons->addWidget(m_d->toolButtonFlipOptions);
283 layoutFlipButtons->addWidget(m_d->toolButtonFlipHorizontally);
284 layoutFlipButtons->addWidget(m_d->toolButtonFlipVertically);
285 layoutFlipButtons->addWidget(m_d->toolButtonFlipHorizontallyAndVertically);
286
287 mainLayout->addWidget(m_d->angleGauge);
288 mainLayout->addWidget(m_d->spinBox);
289 mainLayout->addLayout(layoutFlipButtons);
290
291 setLayout(mainLayout);
292
293 setTabOrder(m_d->angleGauge, m_d->spinBox);
294 setTabOrder(m_d->spinBox, m_d->toolButtonFlipOptions);
295 setTabOrder(m_d->toolButtonFlipOptions, m_d->toolButtonFlipHorizontally);
296 setTabOrder(m_d->toolButtonFlipHorizontally, m_d->toolButtonFlipVertically);
297 setTabOrder(m_d->toolButtonFlipVertically, m_d->toolButtonFlipHorizontallyAndVertically);
298
301
302 using namespace std::placeholders;
303 connect(
304 m_d->angleGauge,
306 std::bind(&Private::on_angleGauge_angleChanged, m_d.data(), _1)
307 );
308 connect(
309 m_d->spinBox,
310 QOverload<double>::of(&KisDoubleParseSpinBox::valueChanged),
311 std::bind(&Private::on_spinBox_valueChanged, m_d.data(), _1)
312 );
313 connect(
314 m_d->actionFlipHorizontally,
315 &QAction::triggered,
317 );
318 connect(
319 m_d->actionFlipVertically,
320 &QAction::triggered,
322 );
323 connect(
324 m_d->actionFlipHorizontallyAndVertically,
325 &QAction::triggered,
327 );
328 connect(m_d->actionResetAngle, SIGNAL(triggered()), SLOT(reset()));
329 connect(m_d->toolButtonFlipHorizontally, SIGNAL(clicked()), m_d->actionFlipHorizontally, SLOT(trigger()));
330 connect(m_d->toolButtonFlipVertically, SIGNAL(clicked()), m_d->actionFlipVertically, SLOT(trigger()));
331 connect(m_d->toolButtonFlipHorizontallyAndVertically, SIGNAL(clicked()), m_d->actionFlipHorizontallyAndVertically, SLOT(trigger()));
332}
333
336
338{
339 return m_d->spinBox->value();
340}
341
343{
344 return m_d->angleGauge->snapAngle();
345}
346
348{
349 return m_d->angleGauge->resetAngle();
350}
351
353{
354 return m_d->spinBox->decimals();
355}
356
358{
359 return m_d->spinBox->maximum();
360}
361
363{
364 return m_d->spinBox->minimum();
365}
366
368{
369 return m_d->spinBox->prefix();
370}
371
373{
374 return m_d->spinBox->wrapping();
375}
376
378{
379 return m_d->flipOptionsMode;
380}
381
383{
384 return m_d->commonWidgetsHeight;
385}
386
388{
389 return m_d->angleGauge->increasingDirection();
390}
391
393{
394 return m_d->spinBox->isFlat();
395}
396
397void KisAngleSelector::setAngle(qreal newAngle)
398{
399 KisSignalsBlocker angleGaugeSignalsBlocker(m_d->angleGauge);
400 KisSignalsBlocker spinBoxSignalsBlocker(m_d->spinBox);
401
402 const qreal oldAngle = m_d->spinBox->value();
403
404 m_d->spinBox->setValue(newAngle);
405 m_d->angleGauge->setAngle(m_d->spinBox->value());
406
407 if (qFuzzyCompare(oldAngle, m_d->spinBox->value())) {
408 return;
409 }
410
411 Q_EMIT angleChanged(m_d->spinBox->value());
412}
413
414void KisAngleSelector::setSnapAngle(qreal newSnapAngle)
415{
416 m_d->angleGauge->setSnapAngle(newSnapAngle);
417}
418
419void KisAngleSelector::setResetAngle(qreal newResetAngle)
420{
421 m_d->angleGauge->setResetAngle(newResetAngle);
422}
423
424void KisAngleSelector::setDecimals(int newNumberOfDecimals)
425{
426 m_d->spinBox->setDecimals(newNumberOfDecimals);
427}
428
429void KisAngleSelector::setMaximum(qreal newMaximum)
430{
431 m_d->spinBox->setMaximum(newMaximum);
432}
433
434void KisAngleSelector::setMinimum(qreal newMinimum)
435{
436 m_d->spinBox->setMinimum(newMinimum);
437}
438
439void KisAngleSelector::setRange(qreal newMinimum, qreal newMaximum)
440{
441 m_d->spinBox->setRange(newMinimum, newMaximum);
442}
443
444void KisAngleSelector::setPrefix(const QString &newPrefix)
445{
446 m_d->spinBox->setPrefix(newPrefix);
447}
448
449void KisAngleSelector::setWrapping(bool newWrapping)
450{
451 m_d->spinBox->setWrapping(newWrapping);
452}
453
455{
456 m_d->flipOptionsMode = newMode;
457
458 m_d->toolButtonFlipOptions->setVisible(newMode == FlipOptionsMode_MenuButton);
459
460 bool useButtons = newMode == FlipOptionsMode_Buttons;
461 m_d->toolButtonFlipHorizontally->setVisible(useButtons);
462 m_d->toolButtonFlipVertically->setVisible(useButtons);
463 m_d->toolButtonFlipHorizontallyAndVertically->setVisible(useButtons);
464
465 bool showMenus = newMode != FlipOptionsMode_NoFlipOptions;
466 m_d->actionFlipHorizontally->setVisible(showMenus);
467 m_d->actionFlipVertically->setVisible(showMenus);
468 m_d->actionFlipHorizontallyAndVertically->setVisible(showMenus);
469}
470
472{
473 if (newHeight < 0) {
474 return;
475 }
476 m_d->commonWidgetsHeight = newHeight;
477 m_d->resizeWidgets();
478}
479
481{
482 m_d->angleGauge->setIncreasingDirection(newIncreasingDirection);
483}
484
485void KisAngleSelector::useFlatSpinBox(bool newUseFlatSpinBox)
486{
487 m_d->spinBox->setFlat(newUseFlatSpinBox);
488}
489
491{
492 m_d->angleGauge->reset();
493}
494
495qreal KisAngleSelector::closestCoterminalAngleInRange(qreal angle, qreal minimum, qreal maximum, bool *ok)
496{
497 bool hasCoterminalAngleInRange = true;
498
499 if (angle < minimum) {
500 const qreal d = minimum - angle;
501 const qreal cycles = std::floor(d / 360.0) + 1;
502 angle += cycles * 360.0;
503 if (angle > maximum) {
504 hasCoterminalAngleInRange = false;
505 angle = minimum;
506 }
507 } else if (angle > maximum) {
508 const qreal d = angle - maximum;
509 const qreal cycles = std::floor(d / 360.0) + 1;
510 angle -= cycles * 360.0;
511 if (angle < minimum) {
512 hasCoterminalAngleInRange = false;
513 angle = maximum;
514 }
515 }
516
517 if (ok) {
518 *ok = hasCoterminalAngleInRange;
519 }
520 return angle;
521}
522
523qreal KisAngleSelector::closestCoterminalAngleInRange(qreal angle, bool *ok) const
524{
525 return closestCoterminalAngleInRange(angle, m_d->spinBox->minimum(), m_d->spinBox->maximum(), ok);
526}
527
528qreal KisAngleSelector::flipAngle(qreal angle, Qt::Orientations orientations)
529{
530 if ((orientations & Qt::Horizontal) && (orientations & Qt::Vertical)) {
531 angle += 180.0;
532 } else if (orientations & Qt::Horizontal) {
533 qreal a = std::fmod(angle, 360.0);
534 if (a < 0) {
535 a += 360.0;
536 }
537 if (a > 270.0) {
538 angle -= 2.0 * (a - 270.0);
539 } else if (a > 180.0) {
540 angle += 2.0 * (270.0 - a);
541 } else if (a > 90.0) {
542 angle -= 2.0 * (a - 90.0);
543 } else {
544 angle += 2.0 * (90.0 - a);
545 }
546 } else if (orientations & Qt::Vertical) {
547 qreal a = std::fmod(angle, 360.0);
548 if (a < 0) {
549 a += 360.0;
550 }
551 if (a > 270.0) {
552 angle += 2.0 * (360.0 - a);
553 } else if (a > 180.0) {
554 angle -= 2.0 * (a - 180.0);
555 } else if (a > 90.0) {
556 angle += 2.0 * (180.0 - a);
557 } else {
558 angle -= 2.0 * a;
559 }
560 }
561
562 return angle;
563}
564
565qreal KisAngleSelector::flipAngle(qreal angle, qreal minimum, qreal maximum, Qt::Orientations orientations, bool *ok)
566{
567 return closestCoterminalAngleInRange(flipAngle(angle, orientations), minimum, maximum, ok);
568}
569
570void KisAngleSelector::flip(Qt::Orientations orientations)
571{
572 bool ok = false;
573 qreal flippedAngle = flipAngle(angle(), minimum(), maximum(), orientations, &ok);
574 if (ok) {
575 setAngle(flippedAngle);
576 }
577}
578
580{
581 if (e->type() == QEvent::PaletteChange) {
582 // For some reason the spinbox, that uses stylesheets, doesn't update
583 // on palette changes, so we reset the stylesheet to force an update.
584 // Calling m_d->spinBox->update() doesn't work
585 m_d->spinBox->refreshStyle();
586 } else if (e->type() == QEvent::StyleChange || e->type() == QEvent::FontChange) {
587 // Temporarily reset the spin box style so that we can get its
588 // height size hint
589 m_d->spinBox->refreshStyle();
590 m_d->resizeWidgets();
591 }
592 return QWidget::event(e);
593}
594
595bool KisAngleSelector::eventFilter(QObject *o, QEvent *e)
596{
597 QWidget *w = qobject_cast<QWidget*>(o);
598 if (w != m_d->angleGauge || !w->isEnabled() || !e || e->type() != QEvent::ContextMenu) {
599 return false;
600 }
601 QContextMenuEvent *cme = static_cast<QContextMenuEvent*>(e);
602
603 m_d->menuFlip->exec(cme->globalPos());
604 return true;
605}
606
611
613{
614 KisSignalsBlocker angleGaugeSignalsBlocker(angleGauge);
615
616 angleGauge->setAngle(value);
617
618 Q_EMIT q->angleChanged(value);
619}
620
622{
623 q->flip(Qt::Horizontal);
624}
625
627{
628 q->flip(Qt::Vertical);
629}
630
632{
633 q->flip(Qt::Horizontal | Qt::Vertical);
634}
635
637{
638 const int h = (commonWidgetsHeight != 0) ? commonWidgetsHeight : spinBox->sizeHint().height();
639
640 angleGauge->setFixedSize(h, h);
641 spinBox->setFixedHeight(h);
642 toolButtonFlipOptions->setFixedHeight(h);
643 toolButtonFlipHorizontally->setFixedHeight(h);
644 toolButtonFlipVertically->setFixedHeight(h);
645 toolButtonFlipHorizontallyAndVertically->setFixedHeight(h);
646}
float value(const T *src, size_t ch)
qreal v
connect(this, SIGNAL(optionsChanged()), this, SLOT(saveOptions()))
A circular widget that allows to choose an angle.
void angleChanged(qreal angle)
Signal emitted when the angle has changed.
QSize sizeHint() const override
void setRange(double min, double max)
void leaveEvent(QEvent *e) override
const QScopedPointer< Private > m_d
void setFlat(bool newFlat)
KisAngleSelectorSpinBox(QWidget *parent=0)
void focusInEvent(QFocusEvent *e) override
QSize minimumSizeHint() const override
void focusOutEvent(QFocusEvent *e) override
double valueFromText(const QString &text) const override
void enterEvent(QEnterEvent *e) override
A widget with several options to select an angle.
void setResetAngle(qreal newResetAngle)
Sets the angle that is used to reset the current angle.
static qreal flipAngle(qreal angle, Qt::Orientations orientations)
Flips an angle horizontally, vertically, or both.
void flip(Qt::Orientations orientations)
Flips the angle horizontally, vertically, or both.
FlipOptionsMode
Options to select how the flip options should be presented.
@ FlipOptionsMode_NoFlipOptions
There is no flip options available.
@ FlipOptionsMode_MenuButton
The flip options are shown as a menu accessible via a options button.
@ FlipOptionsMode_Buttons
The flip options are shown as individual buttons.
void setPrefix(const QString &newPrefix)
Sets the prefix shown in the spin box.
bool eventFilter(QObject *o, QEvent *e) override
bool wrapping() const
Gets if the angle should wrap pass the minimum or maximum angles.
int widgetsHeight() const
Gets the common height of the widgets inside this angle selector.
void setWrapping(bool newWrapping)
Sets if the angle should wrap pass the minimum or maximum angles.
qreal angle() const
Gets the current angle.
void setFlipOptionsMode(FlipOptionsMode newMode)
Sets the mode in which the flip options should be shown.
FlipOptionsMode flipOptionsMode() const
Gets the mode in which the flip options should be shown.
void setSnapAngle(qreal newSnapAngle)
Sets the angle to which multiples the selected angle will snap.
void setIncreasingDirection(KisAngleGauge::IncreasingDirection newIncreasingDirection)
Sets the increasing direction in the angle gauge.
KisAngleSelector(QWidget *parent=0)
Construct a new KisAngleSelector widget.
bool event(QEvent *e) override
qreal resetAngle() const
Gets the angle that is used to reset the current angle.
QString prefix() const
Gets the prefix shown in the spin box.
void useFlatSpinBox(bool newUseFlatSpinBox)
Sets if the spin box should be flat.
qreal maximum() const
Gets the maximum value for the angle.
qreal minimum() const
Gets the minimum value for the angle.
void setAngle(qreal newAngle)
Sets the current angle.
void setRange(qreal newMinimum, qreal newMaximum)
Sets the minimum and maximum values for the angle.
void setMaximum(qreal newMaximum)
Sets the maximum value for the angle.
qreal snapAngle() const
Gets the angle to which multiples the selected angle will snap.
void setWidgetsHeight(int newHeight)
Sets the common height of the widgets inside this angle selector. Use 0 to reset widgets to default h...
static qreal closestCoterminalAngleInRange(qreal angle, qreal minimum, qreal maximum, bool *ok=nullptr)
Gets the closest coterminal angle to the provided angle that is in the range provided.
void setDecimals(int newNumberOfDecimals)
Sets the number of decimals (precision) used by the angle.
void reset()
Sets the current angle to the reset angle.
bool isUsingFlatSpinBox() const
Gets if the spin box is flat (no border or background)
const QScopedPointer< Private > m_d
KisAngleGauge::IncreasingDirection increasingDirection() const
Gets the direction in which the angle increases in the angle gauge.
void angleChanged(qreal angle)
void setMinimum(qreal newMinimum)
Sets the minimum value for the angle.
int decimals() const
Gets the number of decimals (precision) used by the angle.
The KisDoubleParseSpinBox class is a cleverer doubleSpinBox, able to parse arithmetic expressions.
QString textFromValue(double value) const override
double valueFromText(const QString &text) const override
static bool qFuzzyCompare(half p1, half p2)
QIcon loadIcon(const QString &name)
void on_spinBox_valueChanged(double value)
KisAngleSelectorSpinBox * spinBox
KisAngleSelector::FlipOptionsMode flipOptionsMode
QToolButton * toolButtonFlipHorizontallyAndVertically
void on_actionFlipHorizontallyAndVertically_triggered()
void on_angleGauge_angleChanged(qreal angle)