1 /*
2  *  Copyright (c) 2017 Laurent Valentin Jospin <laurent.valentin@famillejospin.ch>
3  *
4  *  This program is free software; you can redistribute it and/or modify
5  *  it under the terms of the GNU General Public License as published by
6  *  the Free Software Foundation; either version 2 of the License, or
7  *  (at your option) any later version.
8  *
9  *  This program is distributed in the hope that it will be useful,
10  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
11  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  *  GNU General Public License for more details.
13  *
14  *  You should have received a copy of the GNU General Public License
15  *  along with this program; if not, write to the Free Software
16  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17  */
18 
19 #include "kis_spin_box_unit_manager.h"
20 
21 #include "KoUnit.h"
22 #include <klocalizedstring.h>
23 
24 #include <QtMath>
25 
26 
27 KisSpinBoxUnitManagerBuilder* KisSpinBoxUnitManagerFactory::builder = nullptr;
28 
buildDefaultUnitManager(QObject * parent)29 KisSpinBoxUnitManager* KisSpinBoxUnitManagerFactory::buildDefaultUnitManager(QObject* parent)
30 {
31     if (builder == nullptr) {
32         return new KisSpinBoxUnitManager(parent);
33     }
34 
35     return builder->buildUnitManager(parent);
36 }
37 
setDefaultUnitManagerBuilder(KisSpinBoxUnitManagerBuilder * pBuilder)38 void KisSpinBoxUnitManagerFactory::setDefaultUnitManagerBuilder(KisSpinBoxUnitManagerBuilder* pBuilder)
39 {
40     if (builder != nullptr) {
41         delete builder; //The factory took over the lifecycle of the builder, so it delete it when replaced.
42     }
43 
44     builder = pBuilder;
45 }
46 
clearUnitManagerBuilder()47 void KisSpinBoxUnitManagerFactory::clearUnitManagerBuilder()
48 {
49     if (builder != nullptr) {
50         delete builder; //The factory took over the lifecycle of the builder, so it delete it when replaced.
51     }
52 
53     builder = nullptr;
54 }
55 
56 const QStringList KisSpinBoxUnitManager::referenceUnitSymbols = {"pt", "px", "°", "frame"};
57 
58 const QStringList KisSpinBoxUnitManager::documentRelativeLengthUnitSymbols = {"px", "vw", "vh"}; //px are relative to the resolution, vw and vh to the width and height.
59 const QStringList KisSpinBoxUnitManager::documentRelativeTimeUnitSymbols = {"s", "%"}; //secondes are relative to the framerate, % to the sequence length.
60 
61 class Q_DECL_HIDDEN KisSpinBoxUnitManager::Private
62 {
63 public:
Private(KisSpinBoxUnitManager::UnitDimension pDim=KisSpinBoxUnitManager::LENGTH,QString pUnitSymbol="pt",double pConv=1.0)64     Private(KisSpinBoxUnitManager::UnitDimension pDim = KisSpinBoxUnitManager::LENGTH,
65             QString pUnitSymbol = "pt",
66             double pConv = 1.0):
67         dim(pDim),
68         unitSymbol(pUnitSymbol),
69         conversionFactor(pConv)
70     {
71 
72     }
73 
74     KisSpinBoxUnitManager::UnitDimension dim;
75 
76     QString unitSymbol;
77     mutable double conversionFactor;
78     bool conversionFactorIsFixed {true}; //tell if it's possible to trust the conversion factor stored or if it's needed to recompute it.
79     mutable double conversionConstant {0};
80     bool conversionConstantIsFixed {true}; //tell if it's possible to trust the conversion constant stored or if it's needed to recompute it.
81 
82     KisSpinBoxUnitManager::Constrains constrains { KisSpinBoxUnitManager::NOCONSTR };
83 
84     mutable QStringList unitList;
85     mutable bool unitListCached {false};
86 
87     mutable QStringList unitListWithName;
88     mutable bool unitListWithNameCached {false};
89 
90     //it's possible to store a reference for the % unit, for length.
91     bool hasHundredPercent {false};
92     qreal hundredPercent {0};
93 
94     bool canAccessDocument {false};
95 
96     QVector<KisSpinBoxUnitManager*> connectedUnitManagers;
97 };
98 
KisSpinBoxUnitManager(QObject * parent)99 KisSpinBoxUnitManager::KisSpinBoxUnitManager(QObject *parent) : QAbstractListModel(parent)
100 {
101     d = new Private();
102 
103     connect(this, (void (KisSpinBoxUnitManager::*)( QString )) &KisSpinBoxUnitManager::unitChanged, this, &KisSpinBoxUnitManager::newUnitSymbolToUnitIndex);
104 }
~KisSpinBoxUnitManager()105 KisSpinBoxUnitManager::~KisSpinBoxUnitManager()
106 {
107     delete d;
108 }
109 
getUnitDimensionType() const110 int KisSpinBoxUnitManager::getUnitDimensionType() const
111 {
112     return d->dim;
113 }
114 
getReferenceUnitSymbol() const115 QString KisSpinBoxUnitManager::getReferenceUnitSymbol() const
116 {
117     return referenceUnitSymbols[d->dim];
118 }
119 
getApparentUnitSymbol() const120 QString KisSpinBoxUnitManager::getApparentUnitSymbol() const
121 {
122     return d->unitSymbol;
123 }
124 
getApparentUnitId() const125 int KisSpinBoxUnitManager::getApparentUnitId() const
126 {
127     QStringList list = getsUnitSymbolList();
128     return list.indexOf(d->unitSymbol);
129 }
130 
getApparentUnitRecommandedDecimals() const131 int KisSpinBoxUnitManager::getApparentUnitRecommandedDecimals() const {
132 
133     switch (d->dim) {
134 
135     case LENGTH:
136         if (d->unitSymbol == "px") {
137             return 0;
138         } else {
139             return 2;
140         }
141     case IMLENGTH:
142         if (d->unitSymbol == "px") {
143             return 0;
144         } else {
145             return 2;
146         }
147     default: //by default return 2.
148         break;
149     }
150 
151     return 2;
152 
153 }
154 
getsUnitSymbolList(bool withName) const155 QStringList KisSpinBoxUnitManager::getsUnitSymbolList(bool withName) const{
156 
157     QStringList list;
158 
159     if (withName) {
160         if (d->unitListWithNameCached) {
161             return d->unitListWithName;
162         }
163     } else {
164         if (d->unitListCached) {
165             return d->unitList;
166         }
167     }
168 
169     switch (d->dim) {
170 
171     case LENGTH:
172 
173         for (int i = 0; i < KoUnit::TypeCount; i++) {
174 
175             if (KoUnit::Type(i) == KoUnit::Pixel) {
176                 continue; //skip pixel, which is a document relative unit, in the base class.
177             }
178 
179             if (withName) {
180                 list << KoUnit::unitDescription(KoUnit::Type(i));
181             } else {
182                 list << KoUnit(KoUnit::Type(i)).symbol();
183             }
184         }
185 
186         if (hasPercent(LENGTH)) {
187 
188             if (withName) {
189                 list << i18n("percent (%)");
190             } else {
191                 list << "%";
192             }
193 
194         }
195 
196         if (d->canAccessDocument) {
197             // ad document relative units
198             if (withName) {
199                 list << KoUnit::unitDescription(KoUnit::Pixel) << i18n("percent of view width (vw)") << i18n("percent of view height (vh)");
200             } else {
201                 list << documentRelativeLengthUnitSymbols;
202             }
203         }
204 
205         break;
206 
207     case IMLENGTH:
208 
209         if (withName) {
210             list << KoUnit::unitDescription(KoUnit::Pixel);
211         } else {
212             list << "px";
213         }
214 
215         if (hasPercent(IMLENGTH)) {
216 
217             if (withName) {
218                 list << i18n("percent (%)");
219             } else {
220                 list << "%";
221             }
222 
223         }
224 
225         if (d->canAccessDocument) {
226             // ad document relative units
227             if (withName) {
228                 list << i18n("percent of view width (vw)") << i18n("percent of view height (vh)");
229             } else {
230                 list << "vw" << "vh";
231             }
232         }
233         break;
234 
235     case ANGLE:
236 
237         if (withName) {
238             list << i18n("degrees (°)") << i18n("radians (rad)") << i18n("gons (gon)") << i18n("percent of circle (%)");
239         } else {
240             list << "°" << "rad" << "gon" << "%";
241         }
242         break;
243 
244     case TIME:
245 
246         if (withName) {
247             list << i18n("frames (f)");
248         } else {
249             list << "f";
250         }
251 
252         if (d->canAccessDocument) {
253             if (withName) {
254                 list << i18n("seconds (s)") << i18n("percent of animation (%)");
255             } else {
256                 list << documentRelativeTimeUnitSymbols;
257             }
258         }
259 
260         break;
261 
262     }
263 
264     if (withName) {
265         d->unitListWithName = list;
266         d->unitListWithNameCached = true;
267     } else {
268         d->unitList = list;
269         d->unitListCached = true;
270     }
271 
272     return list;
273 
274 }
275 
getConversionConstant(int dim,QString symbol) const276 qreal KisSpinBoxUnitManager::getConversionConstant(int dim, QString symbol) const
277 {
278     Q_UNUSED(dim);
279     Q_UNUSED(symbol);
280 
281     return 0; // all units managed here are transform via a linear function, so this will always be 0 in this class.
282 }
283 
getReferenceValue(double apparentValue) const284 qreal KisSpinBoxUnitManager::getReferenceValue(double apparentValue) const
285 {
286     if (!d->conversionFactorIsFixed) {
287         recomputeConversionFactor();
288     }
289 
290     if(!d->conversionConstantIsFixed) {
291         recomputeConvesrionConstant();
292     }
293 
294     qreal v = (apparentValue - d->conversionConstant)/d->conversionFactor;
295 
296     if (d->constrains &= REFISINT) {
297         v = qFloor(v);
298     }
299 
300     return v;
301 
302 }
303 
rowCount(const QModelIndex & parent) const304 int KisSpinBoxUnitManager::rowCount(const QModelIndex &parent) const {
305     if (parent == QModelIndex()) {
306         return getsUnitSymbolList().size();
307     }
308     return 0;
309 }
310 
data(const QModelIndex & index,int role) const311 QVariant KisSpinBoxUnitManager::data(const QModelIndex &index, int role) const {
312 
313     if (role == Qt::DisplayRole) {
314 
315         return getsUnitSymbolList(false).at(index.row());
316 
317     } else if (role == Qt::ToolTipRole) {
318 
319         return getsUnitSymbolList(true).at(index.row());
320 
321     }
322 
323     return QVariant();
324 }
325 
getApparentValue(double refValue) const326 qreal KisSpinBoxUnitManager::getApparentValue(double refValue) const
327 {
328     if (!d->conversionFactorIsFixed) {
329         recomputeConversionFactor();
330     }
331 
332     if(!d->conversionConstantIsFixed) {
333         recomputeConvesrionConstant();
334     }
335 
336     qreal v = refValue*d->conversionFactor + d->conversionConstant;
337 
338     if (d->constrains &= VALISINT) {
339         v = qFloor(v);
340     }
341 
342     return v;
343 }
344 
getConversionFactor(int dim,QString symbol) const345 qreal KisSpinBoxUnitManager::getConversionFactor(int dim, QString symbol) const
346 {
347 
348     qreal factor = -1;
349 
350     switch (dim) {
351 
352     case LENGTH:
353         do {
354             if (symbol == "px") {
355                 break;
356             }
357 
358             bool ok;
359             KoUnit unit = KoUnit::fromSymbol(symbol, &ok);
360             if (! ok) {
361                 break;
362             }
363             factor = unit.toUserValuePrecise(1.0); // use the precise function
364         } while (0) ;
365         break;
366 
367     case IMLENGTH:
368         if (symbol == "px") {
369             factor = 1;
370         }
371         break;
372 
373     case ANGLE:
374         if (symbol == "°") {
375             factor = 1.0;
376             break;
377         }
378         if (symbol == "rad") {
379             factor = acos(-1)/90.0;
380             break;
381         }
382         if (symbol == "gon") {
383             factor = 10.0/9.0;
384             break;
385         }
386         if (symbol == "%") {
387             factor = 2.5/9.0; //(25% of circle is 90°)
388             break;
389         }
390         break;
391 
392     case TIME:
393 
394         if (symbol != "f") { //we have only frames for the moment.
395             break;
396         }
397         factor = 1.0;
398         break;
399 
400     default:
401         break;
402     }
403 
404     return factor;
405 }
406 
407 
setUnitDimension(UnitDimension dimension)408 void KisSpinBoxUnitManager::setUnitDimension(UnitDimension dimension)
409 {
410     if (dimension == d->dim) {
411         return;
412     }
413 
414     d->dim = dimension;
415     d->unitSymbol = referenceUnitSymbols[d->dim]; //Active dim is reference dim when just changed.
416     d->conversionFactor = 1.0;
417 
418     emit unitDimensionChanged(d->dim);
419 
420 }
421 
setApparentUnitFromSymbol(QString pSymbol)422 void KisSpinBoxUnitManager::setApparentUnitFromSymbol(QString pSymbol)
423 {
424 
425     QString symbol = pSymbol.trimmed();
426 
427     if (symbol == d->unitSymbol) {
428         return;
429     }
430 
431     emit unitAboutToChange();
432 
433     QString newSymb = "";
434 
435     switch (d->dim) {
436 
437     case ANGLE:
438         if (symbol.toLower() == "deg") {
439             newSymb = "°";
440             break;
441         }
442         goto default_indentifier; //always do default after handling possible special cases.
443 
444 default_indentifier:
445     default:
446         QStringList list = getsUnitSymbolList();
447         if (list.contains(symbol, Qt::CaseInsensitive)) {
448             for (QString str : list) {
449                 if (str.toLower() == symbol.toLower()) {
450                     newSymb = str; //official symbol may contain capitals letters, so better take the official version.
451                     break;
452                 }
453             }
454             break;
455         }
456 
457     }
458 
459     if(newSymb.isEmpty()) {
460         return; //abort if it was impossible to locate the correct symbol.
461     }
462 
463     if (d->canAccessDocument) {
464         //manage document relative units.
465 
466         QStringList speUnits;
467 
468         switch (d->dim) {
469 
470         case LENGTH:
471             speUnits = documentRelativeLengthUnitSymbols;
472             goto default_identifier_conv_fact;
473 
474         case IMLENGTH:
475             speUnits << "vw" << "vh";
476             goto default_identifier_conv_fact;
477 
478         case TIME:
479             speUnits = documentRelativeTimeUnitSymbols;
480             goto default_identifier_conv_fact;
481 
482 default_identifier_conv_fact:
483         default:
484 
485             if (speUnits.isEmpty()) {
486                 d->conversionFactorIsFixed = true;
487                 break;
488             }
489 
490             if (speUnits.contains(newSymb)) {
491                 d->conversionFactorIsFixed = false;
492                 break;
493             }
494 
495             d->conversionFactorIsFixed = true;
496             break;
497         }
498 
499         if (d->dim == TIME) {
500             if (newSymb == "%") {
501                 d->conversionConstantIsFixed = false;
502             }
503         } else {
504             d->conversionConstantIsFixed = true;
505         }
506 
507     }
508 
509     qreal conversFact = getConversionFactor(d->dim, newSymb);
510     qreal oldConversFact = d->conversionFactor;
511 
512     d->conversionFactor = conversFact;
513     emit conversionFactorChanged(d->conversionFactor, oldConversFact);
514 
515     d->unitSymbol = newSymb;
516     emit unitChanged(newSymb);
517 
518 }
519 
selectApparentUnitFromIndex(int index)520 void KisSpinBoxUnitManager::selectApparentUnitFromIndex(int index) {
521 
522     if (index >= 0 && index < rowCount()) {
523         setApparentUnitFromSymbol(getsUnitSymbolList().at(index));
524     }
525 
526 }
527 
528 
syncWithOtherUnitManager(KisSpinBoxUnitManager * other)529 void KisSpinBoxUnitManager::syncWithOtherUnitManager(KisSpinBoxUnitManager* other) {
530 
531     if (d->connectedUnitManagers.indexOf(other) >= 0) {
532         return;
533     }
534 
535     if (other->getUnitDimensionType() == getUnitDimensionType()) { //sync only unitmanager of the same type.
536         if (other->getsUnitSymbolList() == getsUnitSymbolList()) { //and if we have identical units available.
537 
538             connect(this, SIGNAL(unitChanged(int)), other, SLOT(selectApparentUnitFromIndex(int))); //sync units.
539             connect(other, SIGNAL(unitChanged(int)), this, SLOT(selectApparentUnitFromIndex(int))); //sync units.
540 
541             d->connectedUnitManagers.append(other);
542 
543         }
544     }
545 
546 }
547 
clearSyncWithOtherUnitManager(KisSpinBoxUnitManager * other)548 void KisSpinBoxUnitManager::clearSyncWithOtherUnitManager(KisSpinBoxUnitManager* other) {
549 
550     int id = d->connectedUnitManagers.indexOf(other);
551 
552     if (id < 0) {
553         return;
554     }
555 
556     disconnect(this, SIGNAL(unitChanged(int)), other, SLOT(selectApparentUnitFromIndex(int))); //unsync units.
557     disconnect(other, SIGNAL(unitChanged(int)), this, SLOT(selectApparentUnitFromIndex(int))); //unsync units.
558 
559     d->connectedUnitManagers.removeAt(id);
560 
561 }
562 
newUnitSymbolToUnitIndex(QString symbol)563 void KisSpinBoxUnitManager::newUnitSymbolToUnitIndex(QString symbol) {
564     int id = getsUnitSymbolList().indexOf(symbol);
565 
566     if (id >= 0) {
567         emit unitChanged(id);
568     }
569 }
570 
hasPercent(int unitDim) const571 bool KisSpinBoxUnitManager::hasPercent(int unitDim) const {
572 
573     if (unitDim == IMLENGTH || unitDim == LENGTH) {
574         return false;
575     }
576 
577     if (unitDim == TIME) {
578         return d->canAccessDocument;
579     }
580 
581     if (unitDim == ANGLE) {
582         return true; //percent is fixed when considering angles.
583     }
584 
585     return false;
586 }
587 
recomputeConversionFactor() const588 void KisSpinBoxUnitManager::recomputeConversionFactor() const
589 {
590     if (d->conversionFactorIsFixed) {
591         return;
592     }
593 
594     qreal oldConversionFactor = d->conversionFactor;
595 
596     d->conversionFactor = getConversionFactor(d->dim, d->unitSymbol);
597 
598     if (oldConversionFactor != d->conversionFactor) {
599         emit conversionFactorChanged(d->conversionFactor, oldConversionFactor);
600     }
601 }
602 
recomputeConvesrionConstant() const603 void KisSpinBoxUnitManager::recomputeConvesrionConstant() const
604 {
605     if (d->conversionConstantIsFixed) {
606         return;
607     }
608 
609     qreal oldConversionConstant = d->conversionConstant;
610 
611     d->conversionConstant = getConversionConstant(d->dim, d->unitSymbol);
612 
613     if (oldConversionConstant != d->conversionConstant) {
614         emit conversionConstantChanged(d->conversionConstant, oldConversionConstant);
615     }
616 }
617 
grantDocumentRelativeUnits()618 void KisSpinBoxUnitManager::grantDocumentRelativeUnits()
619 {
620     d->canAccessDocument = true;
621 }
622