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