1 /**
2 * \file gradient_editor.cpp
3 *
4 * \author Mattia Basaglia
5 *
6 * \copyright Copyright (C) 2013-2020 Mattia Basaglia
7 *
8 * This program is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU Lesser General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU Lesser General Public License for more details.
17 *
18 * You should have received a copy of the GNU Lesser General Public License
19 * along with this program. If not, see <http://www.gnu.org/licenses/>.
20 *
21 */
22 #include "QtColorWidgets/gradient_editor.hpp"
23
24 #include <QPainter>
25 #include <QStyleOptionSlider>
26 #include <QLinearGradient>
27 #include <QMouseEvent>
28 #include <QApplication>
29 #include <QDrag>
30 #include <QMimeData>
31 #include <QDropEvent>
32 #include <QDragEnterEvent>
33
34 #include "QtColorWidgets/gradient_helper.hpp"
35 #include "QtColorWidgets/color_dialog.hpp"
36
37 namespace color_widgets {
38
39 class GradientEditor::Private
40 {
41 public:
42 QGradientStops stops;
43 QBrush back;
44 Qt::Orientation orientation;
45 int highlighted = -1;
46 QLinearGradient gradient;
47 int selected = -1;
48 int drop_index = -1;
49 QColor drop_color;
50 qreal drop_pos = 0;
51 ColorDialog color_dialog;
52 int dialog_selected = -1;
53
Private()54 Private() :
55 back(Qt::darkGray, Qt::DiagCrossPattern)
56 {
57 back.setTexture(QPixmap(QStringLiteral(":/color_widgets/alphaback.png")));
58 gradient.setCoordinateMode(QGradient::StretchToDeviceMode);
59 gradient.setSpread(QGradient::RepeatSpread);
60 }
61
refresh_gradient()62 void refresh_gradient()
63 {
64 gradient.setStops(stops);
65 }
66
paint_pos(const QGradientStop & stop,const GradientEditor * owner)67 qreal paint_pos(const QGradientStop& stop, const GradientEditor* owner)
68 {
69 return 2.5 + stop.first * (owner->geometry().width() - 5);
70 }
71
closest(const QPoint & p,GradientEditor * owner)72 int closest(const QPoint& p, GradientEditor* owner)
73 {
74 if ( stops.empty() )
75 return -1;
76 if ( stops.size() == 1 || owner->geometry().width() <= 5 )
77 return 0;
78 qreal pos = move_pos(p, owner);
79
80 int i = 1;
81 for ( ; i < stops.size()-1; i++ )
82 if ( stops[i].first >= pos )
83 break;
84
85 if ( stops[i].first - pos < pos - stops[i-1].first )
86 return i;
87 return i-1;
88 }
89
move_pos(const QPoint & p,GradientEditor * owner)90 qreal move_pos(const QPoint& p, GradientEditor* owner)
91 {
92 int width;
93 qreal x;
94 if ( orientation == Qt::Horizontal )
95 {
96 width = owner->geometry().width();
97 x = p.x();
98 }
99 else
100 {
101 width = owner->geometry().height();
102 x = p.y();
103 }
104 return (width > 5) ? qMax(qMin((x - 2.5) / (width - 5), 1.0), 0.0) : 0;
105 }
106
drop_event(QDropEvent * event,GradientEditor * owner)107 void drop_event(QDropEvent* event, GradientEditor* owner)
108 {
109 drop_index = closest(event->pos(), owner);
110 drop_pos = move_pos(event->pos(), owner);
111 if ( drop_index == -1 )
112 drop_index = stops.size();
113
114 // Gather up the color
115 if ( event->mimeData()->hasColor() )
116 drop_color = event->mimeData()->colorData().value<QColor>();
117 else if ( event->mimeData()->hasText() )
118 drop_color = QColor(event->mimeData()->text());
119
120 owner->update();
121 }
122
clear_drop(GradientEditor * owner)123 void clear_drop(GradientEditor* owner)
124 {
125 drop_index = -1;
126 drop_color = QColor();
127 owner->update();
128 }
129
add_stop_data(int & index,qreal & pos,QColor & color)130 void add_stop_data(int& index, qreal& pos, QColor& color)
131 {
132 if ( stops.empty() )
133 {
134 index = 0;
135 pos = 0;
136 color = Qt::black;
137 return;
138 }
139 if ( stops.size() == 1 )
140 {
141 color = stops[0].second;
142 if ( stops[0].first == 1 )
143 {
144 index = 0;
145 pos = 0.5;
146 }
147 else
148 {
149 index = 1;
150 pos = (stops[0].first + 1) / 2;
151 }
152 return;
153 }
154
155 int i_before = selected;
156 if ( i_before == -1 )
157 i_before = stops.size() - 1;
158
159 if ( i_before == stops.size() - 1 )
160 {
161 if ( stops[i_before].first < 1 )
162 {
163 color = stops[i_before].second;
164 pos = (stops[i_before].first + 1) / 2;
165 index = stops.size();
166 return;
167 }
168 i_before--;
169 }
170
171 index = i_before + 1;
172 pos = (stops[i_before].first + stops[i_before+1].first) / 2;
173 color = blendColors(stops[i_before].second, stops[i_before+1].second, 0.5);
174 }
175 };
176
GradientEditor(QWidget * parent)177 GradientEditor::GradientEditor(QWidget *parent) :
178 GradientEditor(Qt::Horizontal, parent)
179 {}
180
GradientEditor(Qt::Orientation orientation,QWidget * parent)181 GradientEditor::GradientEditor(Qt::Orientation orientation, QWidget *parent) :
182 QWidget(parent), p(new Private)
183 {
184 p->orientation = orientation;
185 setMouseTracking(true);
186 resize(sizeHint());
187 setAcceptDrops(true);
188
189 p->color_dialog.setParent(this);
190 p->color_dialog.setWindowFlags(Qt::Dialog);
191 p->color_dialog.setWindowModality(Qt::WindowModal);
192
193 connect(&p->color_dialog, &ColorDialog::colorSelected, this, &GradientEditor::dialogUpdate);
194 }
195
~GradientEditor()196 GradientEditor::~GradientEditor()
197 {
198 p->color_dialog.setParent(nullptr);
199 delete p;
200 }
201
dialogUpdate(const QColor & c)202 void GradientEditor::dialogUpdate(const QColor& c)
203 {
204 if ( p->dialog_selected != -1 )
205 {
206 p->stops[p->dialog_selected].second = c;
207 p->dialog_selected = -1;
208 p->refresh_gradient();
209 Q_EMIT stopsChanged(p->stops);
210 update();
211 }
212 }
213
mouseDoubleClickEvent(QMouseEvent * ev)214 void GradientEditor::mouseDoubleClickEvent(QMouseEvent *ev)
215 {
216 if ( ev->button() == Qt::LeftButton )
217 {
218 ev->accept();
219 if ( p->highlighted != -1 )
220 {
221 qreal highlighted_pos = p->paint_pos(p->stops[p->highlighted], this);
222 qreal mouse_pos = orientation() == Qt::Vertical ? ev->pos().y() : ev->pos().x();
223 qreal tolerance = 4;
224 auto xfabs = [](qreal r) { // see https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=248190
225 return r>=0 ? r : -r;
226 };
227 if ( xfabs(mouse_pos - highlighted_pos) <= tolerance )
228 {
229 p->dialog_selected = p->highlighted;
230 p->color_dialog.setColor(p->stops[p->highlighted].second);
231 p->color_dialog.show();
232 return;
233 }
234 }
235
236 qreal pos = p->move_pos(ev->pos(), this);
237 auto info = gradientBlendedColorInsert(p->stops, pos);
238 p->stops.insert(info.first, info.second);
239 p->selected = p->highlighted = info.first;
240 p->refresh_gradient();
241 Q_EMIT selectedStopChanged(p->selected);
242 update();
243 }
244 else
245 {
246 QWidget::mousePressEvent(ev);
247 }
248 }
249
mousePressEvent(QMouseEvent * ev)250 void GradientEditor::mousePressEvent(QMouseEvent *ev)
251 {
252 if ( ev->button() == Qt::LeftButton )
253 {
254 ev->accept();
255 p->selected = p->highlighted = p->closest(ev->pos(), this);
256 emit selectedStopChanged(p->selected);
257 update();
258 }
259 else
260 {
261 QWidget::mousePressEvent(ev);
262 }
263 }
264
mouseMoveEvent(QMouseEvent * ev)265 void GradientEditor::mouseMoveEvent(QMouseEvent *ev)
266 {
267 if ( ev->buttons() & Qt::LeftButton && p->selected != -1 )
268 {
269 ev->accept();
270 qreal pos = p->move_pos(ev->pos(), this);
271 if ( p->selected > 0 && pos < p->stops[p->selected-1].first )
272 {
273 std::swap(p->stops[p->selected], p->stops[p->selected-1]);
274 p->selected--;
275 emit selectedStopChanged(p->selected);
276 }
277 else if ( p->selected < p->stops.size()-1 && pos > p->stops[p->selected+1].first )
278 {
279 std::swap(p->stops[p->selected], p->stops[p->selected+1]);
280 p->selected++;
281 emit selectedStopChanged(p->selected);
282 }
283 p->highlighted = p->selected;
284 p->stops[p->selected].first = pos;
285 p->refresh_gradient();
286 update();
287 }
288 else
289 {
290 p->highlighted = p->closest(ev->pos(), this);
291 update();
292 }
293 }
294
mouseReleaseEvent(QMouseEvent * ev)295 void GradientEditor::mouseReleaseEvent(QMouseEvent *ev)
296 {
297 if ( ev->button() == Qt::LeftButton && p->selected != -1 )
298 {
299 ev->accept();
300 QRect bound_rect = rect();
301 QPoint localpt = ev->localPos().toPoint();
302 const int w_margin = 24;
303 const int h_margin = 8;
304 if ( !bound_rect.contains(localpt) && p->stops.size() > 1 && (
305 localpt.x() < -w_margin || localpt.x() > bound_rect.width() + w_margin ||
306 localpt.y() < -h_margin || localpt.y() > bound_rect.height() + h_margin
307 ) )
308 {
309 p->stops.remove(p->selected);
310 p->highlighted = p->selected = p->dialog_selected = -1;
311 p->refresh_gradient();
312 emit selectedStopChanged(p->selected);
313 }
314 emit stopsChanged(p->stops);
315 update();
316 }
317 else
318 {
319 QWidget::mousePressEvent(ev);
320 }
321 }
322
leaveEvent(QEvent *)323 void GradientEditor::leaveEvent(QEvent*)
324 {
325 p->highlighted = -1;
326 update();
327 }
328
329
background() const330 QBrush GradientEditor::background() const
331 {
332 return p->back;
333 }
334
setBackground(const QBrush & bg)335 void GradientEditor::setBackground(const QBrush &bg)
336 {
337 p->back = bg;
338 update();
339 Q_EMIT backgroundChanged(bg);
340 }
341
stops() const342 QGradientStops GradientEditor::stops() const
343 {
344 return p->stops;
345 }
346
setStops(const QGradientStops & colors)347 void GradientEditor::setStops(const QGradientStops &colors)
348 {
349 p->selected = p->highlighted = p->dialog_selected = -1;
350 p->stops = colors;
351 p->refresh_gradient();
352 emit selectedStopChanged(p->selected);
353 emit stopsChanged(p->stops);
354 update();
355 }
356
gradient() const357 QLinearGradient GradientEditor::gradient() const
358 {
359 return p->gradient;
360 }
361
setGradient(const QLinearGradient & gradient)362 void GradientEditor::setGradient(const QLinearGradient &gradient)
363 {
364 setStops(gradient.stops());
365 }
366
orientation() const367 Qt::Orientation GradientEditor::orientation() const
368 {
369 return p->orientation;
370 }
371
setOrientation(Qt::Orientation orientation)372 void GradientEditor::setOrientation(Qt::Orientation orientation)
373 {
374 p->orientation = orientation;
375 update();
376 }
377
378
paintEvent(QPaintEvent *)379 void GradientEditor::paintEvent(QPaintEvent *)
380 {
381 QPainter painter(this);
382
383 QStyleOptionFrame panel;
384 panel.initFrom(this);
385 panel.lineWidth = 1;
386 panel.midLineWidth = 0;
387 panel.state |= QStyle::State_Sunken;
388 style()->drawPrimitive(QStyle::PE_Frame, &panel, &painter, this);
389 QRect r = style()->subElementRect(QStyle::SE_FrameContents, &panel, this);
390 painter.setClipRect(r);
391
392
393 if(orientation() == Qt::Horizontal)
394 p->gradient.setFinalStop(1, 0);
395 else
396 p->gradient.setFinalStop(0, -1);
397
398 painter.setPen(Qt::NoPen);
399 painter.setBrush(p->back);
400 painter.drawRect(1,1,geometry().width()-2,geometry().height()-2);
401 painter.setBrush(p->gradient);
402 painter.drawRect(1,1,geometry().width()-2,geometry().height()-2);
403
404 /// \todo Take orientation into account
405 int i = 0;
406 for ( const QGradientStop& stop : p->stops )
407 {
408 QColor color = stop.second;
409 Qt::GlobalColor border_color = Qt::black;
410 Qt::GlobalColor core_color = Qt::white;
411
412 if ( color.valueF() <= 0.5 && color.alphaF() >= 0.5 )
413 std::swap(core_color, border_color);
414
415 QPointF p1 = QPointF(p->paint_pos(stop, this), 2.5);
416 QPointF p2 = p1 + QPointF(0, geometry().height() - 5);
417 if ( i == p->selected )
418 {
419 painter.setPen(QPen(border_color, 5));
420 painter.drawLine(p1, p2);
421 painter.setPen(QPen(core_color, 3));
422 painter.drawLine(p1, p2);
423 }
424 else if ( i == p->highlighted )
425 {
426 painter.setPen(QPen(border_color, 3));
427 painter.drawLine(p1, p2);
428 painter.setPen(QPen(core_color, 1));
429 painter.drawLine(p1, p2);
430 }
431 else
432 {
433 painter.setPen(QPen(border_color, 3));
434 painter.drawLine(p1, p2);
435 }
436
437 i++;
438 }
439
440 if ( p->drop_index != -1 && p->drop_color.isValid() )
441 {
442 qreal pos = p->drop_pos * (geometry().width() - 5);
443 painter.setPen(QPen(p->drop_color, 3));
444 QPointF p1 = QPointF(2.5, 2.5) + QPointF(pos, 0);
445 QPointF p2 = p1 + QPointF(0, geometry().height() - 5);
446 painter.drawLine(p1, p2);
447 }
448
449 }
450
sizeHint() const451 QSize GradientEditor::sizeHint() const
452 {
453 QStyleOptionSlider opt;
454 opt.orientation = p->orientation;
455
456 int w = style()->pixelMetric(QStyle::PM_SliderThickness, &opt, this);
457 int h = std::max(84, style()->pixelMetric(QStyle::PM_SliderLength, &opt, this));
458 if ( p->orientation == Qt::Horizontal )
459 {
460 std::swap(w, h);
461 }
462 QSlider s;
463 return style()->sizeFromContents(QStyle::CT_Slider, &opt, QSize(w, h), &s)
464 .expandedTo(QApplication::globalStrut());
465 }
466
selectedStop() const467 int GradientEditor::selectedStop() const
468 {
469 return p->selected;
470 }
471
setSelectedStop(int stop)472 void GradientEditor::setSelectedStop(int stop)
473 {
474 if ( stop >= -1 && stop < p->stops.size() )
475 {
476 p->selected = stop;
477 emit selectedStopChanged(p->selected);
478 }
479 }
480
selectedColor() const481 QColor GradientEditor::selectedColor() const
482 {
483 if ( p->selected != -1 )
484 return p->stops[p->selected].second;
485 return {};
486 }
487
setSelectedColor(const QColor & color)488 void GradientEditor::setSelectedColor(const QColor& color)
489 {
490 if ( p->selected != -1 )
491 {
492 p->stops[p->selected].second = color;
493 p->refresh_gradient();
494 update();
495 }
496 }
497
498
dragEnterEvent(QDragEnterEvent * event)499 void GradientEditor::dragEnterEvent(QDragEnterEvent *event)
500 {
501 p->drop_event(event, this);
502
503 if ( p->drop_color.isValid() && p->drop_index != -1 )
504 {
505 event->setDropAction(Qt::CopyAction);
506 event->accept();
507 }
508 }
509
dragMoveEvent(QDragMoveEvent * event)510 void GradientEditor::dragMoveEvent(QDragMoveEvent* event)
511 {
512 p->drop_event(event, this);
513 }
514
dragLeaveEvent(QDragLeaveEvent *)515 void GradientEditor::dragLeaveEvent(QDragLeaveEvent *)
516 {
517 p->clear_drop(this);
518 }
519
dropEvent(QDropEvent * event)520 void GradientEditor::dropEvent(QDropEvent *event)
521 {
522 p->drop_event(event, this);
523
524 if ( !p->drop_color.isValid() || p->drop_index == -1 )
525 return;
526
527 p->stops.insert(p->drop_index, {p->drop_pos, p->drop_color});
528 p->refresh_gradient();
529 p->selected = p->drop_index;
530 event->accept();
531 p->clear_drop(this);
532 emit selectedStopChanged(p->selected);
533 }
534
addStop()535 void GradientEditor::addStop()
536 {
537 int index = -1;
538 qreal pos = 0;
539 QColor color;
540 p->add_stop_data(index, pos, color);
541 p->stops.insert(index, {pos, color});
542 p->selected = p->highlighted = index;
543 p->refresh_gradient();
544 update();
545 emit selectedStopChanged(p->selected);
546 }
547
removeStop()548 void GradientEditor::removeStop()
549 {
550 if ( p->stops.size() < 2 )
551 return;
552
553 int selected = p->selected;
554 if ( selected == -1 )
555 selected = p->stops.size() - 1;
556 p->stops.remove(selected);
557 p->refresh_gradient();
558
559 if ( p->selected != -1 )
560 {
561 p->selected = -1;
562 emit selectedStopChanged(p->selected);
563 }
564
565 p->dialog_selected = -1;
566
567 update();
568
569 }
570
dialog() const571 ColorDialog * GradientEditor::dialog() const
572 {
573 return &p->color_dialog;
574 }
575
576
577 } // namespace color_widgets
578