1 /** -*- mode: c++ ; c-basic-offset: 2 -*-
2  *
3  *  @file PointParameter.cpp
4  *
5  *  Copyright 2017 Sebastien Fourey
6  *
7  *  This file is part of G'MIC-Qt, a generic plug-in for raster graphics
8  *  editors, offering hundreds of filters thanks to the underlying G'MIC
9  *  image processing framework.
10  *
11  *  gmic_qt is free software: you can redistribute it and/or modify
12  *  it under the terms of the GNU General Public License as published by
13  *  the Free Software Foundation, either version 3 of the License, or
14  *  (at your option) any later version.
15  *
16  *  gmic_qt is distributed in the hope that it will be useful,
17  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
18  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19  *  GNU General Public License for more details.
20  *
21  *  You should have received a copy of the GNU General Public License
22  *  along with gmic_qt.  If not, see <http://www.gnu.org/licenses/>.
23  *
24  */
25 #include "FilterParameters/PointParameter.h"
26 #include <QApplication>
27 #include <QColorDialog>
28 #include <QDebug>
29 #include <QFont>
30 #include <QFontMetrics>
31 #include <QGridLayout>
32 #include <QHBoxLayout>
33 #include <QLabel>
34 #include <QPainter>
35 #include <QPushButton>
36 #include <QSpacerItem>
37 #include <QWidget>
38 #include <cmath>
39 #include <cstdio>
40 #include <cstdlib>
41 #include "Common.h"
42 #include "DialogSettings.h"
43 #include "FilterTextTranslator.h"
44 #include "HtmlTranslator.h"
45 #include "KeypointList.h"
46 
47 namespace GmicQt
48 {
49 
50 int PointParameter::_defaultColorNextIndex = 0;
51 unsigned long PointParameter::_randomSeed = 12345;
52 
PointParameter(QObject * parent)53 PointParameter::PointParameter(QObject * parent) : AbstractParameter(parent), _defaultPosition(0, 0), _position(0, 0), _removable(false), _burst(false)
54 {
55   _label = nullptr;
56   _colorLabel = nullptr;
57   _labelX = nullptr;
58   _labelY = nullptr;
59   _spinBoxX = nullptr;
60   _spinBoxY = nullptr;
61   _removeButton = nullptr;
62   _rowCell = nullptr;
63   _notificationEnabled = true;
64   _connected = false;
65   _defaultRemovedStatus = false;
66   _radius = KeypointList::Keypoint::DefaultRadius;
67   _keepOpacityWhenSelected = false;
68   _removed = false;
69   setRemoved(false);
70 }
71 
~PointParameter()72 PointParameter::~PointParameter()
73 {
74   delete _label;
75   delete _rowCell;
76 }
77 
size() const78 int PointParameter::size() const
79 {
80   return 2;
81 }
82 
addTo(QWidget * widget,int row)83 bool PointParameter::addTo(QWidget * widget, int row)
84 {
85   _grid = dynamic_cast<QGridLayout *>(widget->layout());
86   Q_ASSERT_X(_grid, __PRETTY_FUNCTION__, "No grid layout in widget");
87   _row = row;
88   delete _label;
89   delete _rowCell;
90 
91   _rowCell = new QWidget(widget);
92   auto hbox = new QHBoxLayout(_rowCell);
93   hbox->setMargin(0);
94   hbox->addWidget(_colorLabel = new QLabel(_rowCell));
95 
96   QFontMetrics fm(widget->font());
97   QRect r = fm.boundingRect("CLR");
98   _colorLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
99   QPixmap pixmap(r.width(), r.height());
100   QPainter painter(&pixmap);
101   painter.setBrush(QColor(_color.red(), _color.green(), _color.blue()));
102   painter.setPen(Qt::black);
103   painter.drawRect(0, 0, pixmap.width() - 1, pixmap.height() - 1);
104   _colorLabel->setPixmap(pixmap);
105 
106   hbox->addWidget(_labelX = new QLabel("X", _rowCell));
107   hbox->addWidget(_spinBoxX = new QDoubleSpinBox(_rowCell));
108   hbox->addWidget(_labelY = new QLabel("Y", _rowCell));
109   hbox->addWidget(_spinBoxY = new QDoubleSpinBox(_rowCell));
110   if (_removable) {
111     hbox->addWidget(_removeButton = new QToolButton(_rowCell));
112     _removeButton->setCheckable(true);
113     _removeButton->setChecked(_removed);
114     _removeButton->setIcon(DialogSettings::RemoveIcon);
115   } else {
116     _removeButton = nullptr;
117   }
118   hbox->addSpacerItem(new QSpacerItem(1, 1, QSizePolicy::Expanding, QSizePolicy::Fixed));
119   _spinBoxX->setRange(-200.0, 300.0);
120   _spinBoxY->setRange(-200.0, 300.0);
121   _spinBoxX->setValue(_position.x());
122   _spinBoxY->setValue(_position.y());
123   _grid->addWidget(_label = new QLabel(_name, widget), row, 0, 1, 1);
124   _grid->addWidget(_rowCell, row, 1, 1, 2);
125 
126 #ifdef _GMIC_QT_DEBUG_
127   _label->setToolTip(QString("Burst: %1").arg(_burst ? "on" : "off"));
128 #endif
129 
130   setRemoved(_removed);
131   connectSpinboxes();
132   return true;
133 }
134 
addToKeypointList(KeypointList & list) const135 void PointParameter::addToKeypointList(KeypointList & list) const
136 {
137   if (_removable && _removed) {
138     list.add(KeypointList::Keypoint(_color, _removable, _burst, _radius, _keepOpacityWhenSelected));
139   } else {
140     list.add(KeypointList::Keypoint(_position.x(), _position.y(), _color, _removable, _burst, _radius, _keepOpacityWhenSelected));
141   }
142 }
143 
extractPositionFromKeypointList(KeypointList & list)144 void PointParameter::extractPositionFromKeypointList(KeypointList & list)
145 {
146   Q_ASSERT_X(!list.isEmpty(), __PRETTY_FUNCTION__, "Keypoint list is empty");
147   enableNotifications(false);
148   KeypointList::Keypoint kp = list.front();
149   if (!kp.isNaN()) {
150     _position.setX(kp.x);
151     _position.setY(kp.y);
152     if (_spinBoxX) {
153       _spinBoxX->setValue(kp.x);
154       _spinBoxY->setValue(kp.y);
155     }
156   }
157   list.pop_front();
158   enableNotifications(true);
159 }
160 
value() const161 QString PointParameter::value() const
162 {
163   if (_removed) {
164     return "nan,nan";
165   }
166   return QString("%1,%2").arg(_position.x()).arg(_position.y());
167 }
168 
defaultValue() const169 QString PointParameter::defaultValue() const
170 {
171   return QString("%1,%2").arg(_defaultPosition.x()).arg(_defaultPosition.y());
172 }
173 
setValue(const QString & value)174 void PointParameter::setValue(const QString & value)
175 {
176   QStringList list = value.split(",");
177   if (list.size() == 2) {
178     bool ok;
179     float x = list[0].toFloat(&ok);
180     bool xNaN = (list[0].toUpper() == "NAN");
181     if (ok && !xNaN) {
182       _position.setX(x);
183     }
184     float y = list[1].toFloat(&ok);
185     bool yNaN = (list[1].toUpper() == "NAN");
186     if (ok && !yNaN) {
187       _position.setY(y);
188     }
189     _removed = (_removable && xNaN && yNaN);
190 
191     updateView();
192   }
193 }
194 
setVisibilityState(AbstractParameter::VisibilityState state)195 void PointParameter::setVisibilityState(AbstractParameter::VisibilityState state)
196 {
197   AbstractParameter::setVisibilityState(state);
198   if (state == VisibilityState::Visible) {
199     updateView();
200   }
201 }
202 
updateView()203 void PointParameter::updateView()
204 {
205   if (not _spinBoxX) {
206     return;
207   }
208   disconnectSpinboxes();
209   if (_removeButton) {
210     setRemoved(_removed);
211     _removeButton->setChecked(_removed);
212   }
213   if (!_removed) {
214     _spinBoxX->setValue(_position.x());
215     _spinBoxY->setValue(_position.y());
216   }
217   connectSpinboxes();
218 }
219 
reset()220 void PointParameter::reset()
221 {
222   _position = _defaultPosition;
223   enableNotifications(false);
224   if (_spinBoxX) {
225     _spinBoxX->setValue(_defaultPosition.rx());
226     _spinBoxY->setValue(_defaultPosition.ry());
227   }
228   if (_removeButton && _removable) {
229     _removeButton->setChecked((_removed = _defaultRemovedStatus));
230   }
231   enableNotifications(true);
232 }
233 
234 // P = point(x,y,removable{(0),1},burst{(0),1},r,g,b,a{negative->keepOpacityWhenSelected},radius,widget_visible{0|(1)})
initFromText(const char * text,int & textLength)235 bool PointParameter::initFromText(const char * text, int & textLength)
236 {
237   QList<QString> list = parseText("point", text, textLength);
238   if (list.isEmpty()) {
239     return false;
240   }
241   _name = HtmlTranslator::html2txt(FilterTextTranslator::translate(list[0]));
242   QList<QString> params = list[1].split(",");
243 
244   bool ok = true;
245 
246   _defaultPosition.setX(50.0);
247   _defaultPosition.setY(50.0);
248   _defaultPosition = _position;
249   _color.setRgb(255, 255, 255, 255);
250   _burst = false;
251   _removable = false;
252   _radius = KeypointList::Keypoint::DefaultRadius;
253   _keepOpacityWhenSelected = false;
254 
255   float x = 50.0f;
256   float y = 50.0f;
257   _removed = false;
258   bool xNaN = true;
259   bool yNaN = true;
260 
261   if (!params.isEmpty()) {
262     x = params[0].toFloat(&ok);
263     xNaN = (params[0].toUpper() == "NAN");
264     if (!ok) {
265       return false;
266     }
267     if (xNaN) {
268       x = 50.0;
269     }
270   }
271 
272   if (params.size() >= 2) {
273     y = params[1].toFloat(&ok);
274     yNaN = (params[1].toUpper() == "NAN");
275     if (!ok) {
276       return false;
277     }
278     if (yNaN) {
279       y = 50.0;
280     }
281   }
282 
283   _defaultPosition.setX(static_cast<qreal>(x));
284   _defaultPosition.setY(static_cast<qreal>(y));
285   _removed = _defaultRemovedStatus = (xNaN || yNaN);
286 
287   if (params.size() >= 3) {
288     int removable = params[2].toInt(&ok);
289     if (!ok) {
290       return false;
291     }
292     switch (removable) {
293     case -1:
294       _removable = _removed = _defaultRemovedStatus = true;
295       break;
296     case 0:
297       _removable = _removed = false;
298       break;
299     case 1:
300       _removable = true;
301       _defaultRemovedStatus = _removed = (xNaN && yNaN);
302       break;
303     default:
304       return false;
305     }
306   }
307 
308   if (params.size() >= 4) {
309     bool burst = params[3].toInt(&ok);
310     if (!ok) {
311       return false;
312     }
313     _burst = burst;
314   }
315 
316   if (params.size() >= 5) {
317     int red = params[4].toInt(&ok);
318     if (!ok) {
319       return false;
320     }
321     _color.setRed(red);
322     _color.setGreen(red);
323     _color.setBlue(red);
324   } else {
325     pickColorFromDefaultColormap();
326   }
327 
328   if (params.size() >= 6) {
329     int green = params[5].toInt(&ok);
330     if (!ok) {
331       return false;
332     }
333     _color.setGreen(green);
334     _color.setBlue(0);
335   }
336 
337   if (params.size() >= 7) {
338     int blue = params[6].toInt(&ok);
339     if (!ok) {
340       return false;
341     }
342     _color.setBlue(blue);
343   }
344 
345   if (params.size() >= 8) {
346     int alpha = params[7].toInt(&ok);
347     if (!ok) {
348       return false;
349     }
350     if (params[7].trimmed().startsWith("-") || (alpha < 0)) {
351       _keepOpacityWhenSelected = true;
352     }
353     _color.setAlpha(std::abs(alpha));
354   }
355 
356   if (params.size() >= 9) {
357     QString s = params[8].trimmed();
358     if (s.endsWith("%")) {
359       s.chop(1);
360       _radius = -s.toFloat(&ok);
361     } else {
362       _radius = s.toFloat(&ok);
363     }
364     if (!ok) {
365       return false;
366     }
367   }
368 
369   _position = _defaultPosition;
370   return true;
371 }
372 
enableNotifications(bool on)373 void PointParameter::enableNotifications(bool on)
374 {
375   _notificationEnabled = on;
376 }
377 
onSpinBoxChanged()378 void PointParameter::onSpinBoxChanged()
379 {
380   _position = QPointF(_spinBoxX->value(), _spinBoxY->value());
381   if (_notificationEnabled) {
382     notifyIfRelevant();
383   }
384 }
385 
setRemoved(bool on)386 void PointParameter::setRemoved(bool on)
387 {
388   _removed = on;
389   if (_spinBoxX) {
390     _spinBoxX->setDisabled(on);
391     _spinBoxY->setDisabled(on);
392     _labelX->setDisabled(on);
393     _labelY->setDisabled(on);
394     if (_removeButton) {
395       _removeButton->setIcon(on ? DialogSettings::AddIcon : DialogSettings::RemoveIcon);
396     }
397   }
398 }
399 
resetDefaultColorIndex()400 void PointParameter::resetDefaultColorIndex()
401 {
402   _defaultColorNextIndex = 0;
403   _randomSeed = 12345;
404 }
405 
onRemoveButtonToggled(bool on)406 void PointParameter::onRemoveButtonToggled(bool on)
407 {
408   setRemoved(on);
409   notifyIfRelevant();
410 }
411 
randomChannel()412 int PointParameter::randomChannel()
413 {
414   int value = (_randomSeed / 65536) % 256;
415   _randomSeed = _randomSeed * 1103515245 + 12345;
416   return value;
417 }
418 
connectSpinboxes()419 void PointParameter::connectSpinboxes()
420 {
421   if (_connected || !_spinBoxX) {
422     return;
423   }
424   connect(_spinBoxX, SIGNAL(valueChanged(double)), this, SLOT(onSpinBoxChanged()));
425   connect(_spinBoxY, SIGNAL(valueChanged(double)), this, SLOT(onSpinBoxChanged()));
426   if (_removable && _removeButton) {
427     connect(_removeButton, SIGNAL(toggled(bool)), this, SLOT(onRemoveButtonToggled(bool)));
428   }
429   _connected = true;
430 }
431 
disconnectSpinboxes()432 void PointParameter::disconnectSpinboxes()
433 {
434   if (!_connected || !_spinBoxX) {
435     return;
436   }
437   _spinBoxX->disconnect(this);
438   _spinBoxY->disconnect(this);
439   if (_removable && _removeButton) {
440     _removeButton->disconnect(this);
441   }
442   _connected = false;
443 }
444 
pickColorFromDefaultColormap()445 void PointParameter::pickColorFromDefaultColormap()
446 {
447   switch (_defaultColorNextIndex) {
448   case 0:
449     _color.setRgb(255, 255, 255, 255);
450     break;
451   case 1:
452     _color = Qt::red;
453     break;
454   case 2:
455     _color = Qt::green;
456     break;
457   case 3:
458     _color.setRgb(64, 64, 255, 255);
459     break;
460   case 4:
461     _color = Qt::cyan;
462     break;
463   case 5:
464     _color = Qt::magenta;
465     break;
466   case 6:
467     _color = Qt::yellow;
468     break;
469   default:
470     _color.setRgb(randomChannel(), randomChannel(), randomChannel());
471   }
472   ++_defaultColorNextIndex;
473 }
474 
475 } // namespace GmicQt
476