1 /* This file is part of the KDE project
2  * Copyright (C) 2018 Dag Andersen <danders@get2net.dk>
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Library General Public
6  * License as published by the Free Software Foundation; either
7  * version 2 of the License, or (at your option) any later version.
8  *
9  * This library 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 GNU
12  * Library General Public License for more details.
13  *
14  * You should have received a copy of the GNU Library General Public License
15  * along with this library; see the file COPYING.LIB.  If not, write to
16  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17  * Boston, MA 02110-1301, USA.
18  */
19 
20 #include "CalloutContainerModel.h"
21 
22 #include "CalloutShape.h"
23 #include "EnhancedPathShape.h"
24 #include "CalloutDebug.h"
25 
26 #include <KoTextShapeDataBase.h>
27 #include <KoShape.h>
28 #include <KoPathPoint.h>
29 
30 #include <QSizeF>
31 #include <QRectF>
32 #include <QTransform>
33 #include <QtMath>
34 #include <QDebug>
35 
decompose(const QTransform & m,qreal & scaleX,qreal & scaleY,qreal & rotation,qreal & skewX,qreal & skewY,qreal & transX,qreal & transY)36 void decompose(const QTransform &m, qreal &scaleX, qreal &scaleY, qreal &rotation, qreal &skewX, qreal &skewY, qreal &transX, qreal &transY)
37 {
38     scaleX=0; scaleY=1; rotation=0; skewX=0; skewY=0; transX=0; transY=0;
39     qreal a = m.m11();
40     qreal b = m.m12();
41     qreal c = m.m21();
42     qreal d = m.m22();
43     qreal e = m.m31();
44     qreal f = m.m32();
45 
46     qreal Delta = a * d - b * c;
47 
48     if (a != 0 || b != 0) {
49         qreal r = qSqrt(a*a+b*b);
50         rotation = b > 0 ? qAcos(a/r) : -qAcos(a/r);
51         scaleX = r;
52         scaleY = Delta/r;
53         skewX = qAtan((a*c+b*d)/(r*r));
54     } else if (c != 0 || d != 0) {
55         qreal s = qSqrt(c*c+d*d);
56         rotation = M_PI_2 - (d > 0 ? qAcos(-c/s) : -qAcos(c/s));
57         scaleX = Delta/s;
58         scaleY = s;
59         skewY = qAtan((a*c+b*d)/(s*s));
60     } else { // a = b = c = d = 0
61         scaleX = 0.0;
62         scaleY = 0.0;
63     }
64     debugCallout<<"decomposed:"<<m<<endl<<'\t'<<scaleX<<scaleY<<qRadiansToDegrees(rotation)<<qRadiansToDegrees(skewX)<<qRadiansToDegrees(skewY)<<transX<<transY;
65 }
66 
normalize(const QTransform & m)67 QTransform normalize(const QTransform &m)
68 {
69     qreal scaleX, scaleY, rotation, skewX, skewY, transX, transY;
70     decompose(m, scaleX, scaleY, rotation, skewX, skewY, transX, transY);
71     QTransform n;
72     //n.scale(-scaleX, -scaleY); //TODO
73     n.rotateRadians(-rotation);
74     n.shear(-skewX, -skewY);
75     n = m * n;
76     debugCallout<<"normalized:"<<n;
77     return n;
78 }
79 
position(const KoShapeContainer * container)80 QPointF position(const KoShapeContainer *container)
81 {
82     QTransform m = normalize(container->transformation());
83     QPointF center(0.5*container->size().width(), 0.5*container->size().height());
84     return m.map(center) - center;
85 }
86 
CalloutContainerModel()87 CalloutContainerModel::CalloutContainerModel()
88     : KoShapeContainerDefaultModel()
89     , m_resizing(false)
90 {
91 }
92 
setIgnore(KoShape * shape,bool state)93 void CalloutContainerModel::setIgnore(KoShape *shape, bool state)
94 {
95     m_ignore.insert(shape, state);
96 }
97 
ignore(KoShape * shape) const98 bool CalloutContainerModel::ignore(KoShape *shape) const
99 {
100     return m_ignore.contains(shape) && m_ignore[shape];
101 }
102 
containerChanged(KoShapeContainer * container,KoShape::ChangeType type)103 void CalloutContainerModel::containerChanged(KoShapeContainer *container, KoShape::ChangeType type)
104 {
105     switch (type) {
106         case KoShape::PositionChanged: {
107             QPointF center(0.5*container->size().width(), 0.5*container->size().height());
108             QPointF pos = container->position();
109             m_prevPosition = position(container);
110             debugCallout<<type<<"org:"<<pos<<"norm:"<<m_prevPosition;
111             break;
112         }
113         case KoShape::SizeChanged:
114             if (!m_resizing) {
115                 CalloutShape *callout = dynamic_cast<CalloutShape*>(container);
116                 Q_ASSERT(callout);
117                 QPointF newPos = position(callout);
118                 resizePath(callout->pathShape(), newPos, callout->size());
119                 m_prevPosition = newPos;
120                 m_prevSize = container->size();
121             }
122             break;
123         case KoShape::BeginResize: {
124             m_resizing = true;
125             m_prevPosition = position(container);
126             m_prevSize = container->size();
127             break;
128         }
129         case KoShape::EndResize: {
130             debugCalloutF<<type<<">>>";
131             CalloutShape *callout = dynamic_cast<CalloutShape*>(container);
132             QPointF newPos = position(callout);
133             resizePath(callout->pathShape(), newPos, callout->size());
134             m_prevPosition = newPos;
135             m_prevSize = container->size();
136             m_resizing = false;
137             debugCalloutF<<type<<"<<<";
138             break;
139         }
140         case KoShape::GenericMatrixChange:
141         case KoShape::RotationChanged: {
142             //debugCalloutF<<type<<container->position()<<container->size()<<'('<<m_prevPosition<<m_prevSize<<')'<<endl<<'\t'<<container->absoluteTransformation(0);
143             //debugCalloutF<<type<<container->absolutePosition();
144             break;
145         }
146         default:
147             //debugCalloutF<<type;
148             break;
149     }
150 }
151 
childChanged(KoShape * shape,KoShape::ChangeType type)152 void CalloutContainerModel::childChanged(KoShape *shape, KoShape::ChangeType type)
153 {
154 //     debugCalloutF<<type;
155 //     if (type == KoShape::Deleted) {
156 //         return;
157 //     }
158 //     if (type != KoShape::ParameterChanged) {
159 // //         return;
160 //     }
161 //     PathShape *path = dynamic_cast<PathShape*>(shape);
162 //     debugCalloutF<<type<<path->outlineRect();
163 //     if (!path) {
164 //         return;
165 //     }
166     //KoProperties params = path->parameters();
167     //debugCalloutF<<params.property("modifiers");
168 
169 }
170 
171 // void decompose(const QTransform &m, qreal &scaleX, qreal &scaleY, qreal &rotation, qreal &skewX, qreal &skewY. qreal &transX, qreal &transY)
172 // {
173 //     E=(m00+m11)/2;
174 //     F=(m00-m11)/2;
175 //     G=(m10+m01)/2;
176 //     H=(m10-m01)/2;
177 //     Q=sqrt(E^2+H^2);
178 //     R=sqrt(F^2+G^2);
179 //     sx=Q+R;
180 //     sy=Q-R;
181 //     a1=atan2(G,F);
182 //     a2=atan2(H,E);
183 //     theta=(a2-a1)/2;
184 //     phi=(a2+a1)/2;
185 // }
186 
187 
isChildLocked(const KoShape * child) const188 bool CalloutContainerModel::isChildLocked(const KoShape *child) const
189 {
190     Q_UNUSED(child)
191     return false;
192 }
193 
194 // TODO: Shearing Y does not work
resizePath(PathShape * path,const QPointF & newPos,const QSizeF & newSize)195 void CalloutContainerModel::resizePath(PathShape *path, const QPointF &newPos, const QSizeF &newSize)
196 {
197     debugCalloutF<<">>>>>";
198 
199     if (newSize == m_prevSize) {
200         debugCalloutF<<"No change"<<"<<<<<";
201         return;
202     }
203     KoProperties params = path->parameters();
204     QSizeF prevPathSize = path->size();
205     QSizeF newPathSize;
206     debugCallout<<'\t'<<"prev:"<<m_prevPosition<<m_prevSize<<"new:"<<newPos<<newSize<<"diff:"<<(newSize-m_prevSize);
207     QList<qreal> modifiers = path->modifiers();
208     Q_ASSERT(modifiers.count() >= 2);
209 
210     QVariant viewboxData;
211     params.property("viewBox", viewboxData);
212     QRect viewBox = path->viewBox();
213 
214     qreal currViewboxModifierRatioX = modifiers[0] / viewBox.width();
215     qreal currViewboxModifierRatioY = modifiers[1] / viewBox.height();
216     qreal viewboxModifierRatioX = 1.0;
217     qreal viewboxModifierRatioY = 1.0;
218     if (m_prevSize.isValid()) {
219         bool pointerLeft = modifiers[0] < viewBox.left();
220         bool pointerRight = modifiers[0] > viewBox.right();
221         bool pointerTop = modifiers[1] < viewBox.top();
222         bool pointerBottom = modifiers[1] > viewBox.bottom();
223         if (pointerTop) debugCallout<<'\t'<<"pointer above";
224         if (pointerRight) debugCallout<<'\t'<<"pointer right";
225         if (pointerLeft) debugCallout<<'\t'<<"pointer left";
226         if (pointerBottom) debugCallout<<'\t'<<"pointer below";
227 
228         bool movedX = qAbs(m_prevPosition.x() - newPos.x()) > 0.001;
229         bool movedY = qAbs(m_prevPosition.y() - newPos.y()) > 0.001;
230         if (movedX) debugCallout<<'\t'<<"x moved"<<"prev:"<<m_prevPosition.x()<<"new:"<<newPos.x();
231         if (movedY) debugCallout<<'\t'<<"y moved"<<"prev:"<<m_prevPosition.y()<<"new:"<<newPos.y();
232 
233         if ((pointerLeft && !movedX) || (pointerRight && movedX)) {
234             // Width has been changed in a way that affects path size
235             newPathSize.setWidth(prevPathSize.width() + (newSize.width() - m_prevSize.width()));
236             debugCallout<<'\t'<<"Width changed:"<<"new width:"<<newPathSize.width();
237         } else {
238             newPathSize.setWidth(prevPathSize.width());
239             debugCallout<<'\t'<<"Width not changed:"<<"Keep path width";
240         }
241 
242         if ((pointerTop && !movedY) || (pointerBottom && movedY)) {
243             // Height has been changed in a way that affects path size
244             newPathSize.setHeight(prevPathSize.height() + (newSize.height() - m_prevSize.height()));
245             debugCallout<<'\t'<<"Height changed:"<<"new height:"<<newPathSize.height();
246         } else {
247             newPathSize.setHeight(prevPathSize.height());
248             debugCallout<<'\t'<<"Height not changed:"<<"Keep path height";
249         }
250         //debugCallout<<'\t'<<"Path size:"<<"prev:"<<prevPathSize<<"new:"<<newPathSize<<"diff:"<<(newPathSize-prevPathSize);
251 
252         // Calculate new modifiers (the callout pointer tip)
253         // The pointer tip shall stay in the same *global* position
254         // so since it is given in viewbox coordinates (which corresponds to the bubble part),
255         // it needs to recalculated when the bubble part is moved/resized.
256         // Note: the viewbox coordinates never changes.
257         //debugCallout<<'\t'<<"Current modifiers:"<<modifiers;
258         if (pointerLeft || pointerRight) {
259             debugCallout<<'\t'<<"modifier X is ouside viewbox";
260             qreal ax = newSize.width();
261             qreal bx = newPathSize.width();
262             qreal vbx = viewBox.width();
263             qreal rx = (bx - ax) / ax;
264             qreal mx = pointerLeft ? -vbx * rx : vbx + (vbx * rx);
265             modifiers[0] = mx;
266         } else if (movedX) {
267             debugCallout<<'\t'<<"left side resize and modifier X is inside viewbox";
268             qreal ax = newSize.width();
269             qreal bx = m_prevSize.width();
270             qreal vbx = viewBox.width();
271             qreal rvbx = ax / bx;
272             qreal vbxDiff = (vbx * rvbx) - vbx;
273             qreal x = modifiers[0] + vbxDiff;
274             qreal rx = x / (vbx + vbxDiff);
275             qreal mx = vbx * rx;
276             modifiers[0] = mx;
277             debugCallout<<'\t'<<"vbxDiff:"<<vbxDiff<<"x:"<<x<<"rx:"<<rx;
278         } else {
279             debugCallout<<'\t'<<"right side resize and modifier X is inside viewbox";
280             qreal ax = newSize.width();
281             qreal bx = m_prevSize.width();
282             qreal vbx = viewBox.width();
283             qreal rx = ax / bx;
284             qreal x = modifiers[0] - viewBox.left();
285             qreal mx = x / rx;
286             modifiers[0] = mx;
287             debugCallout<<'\t'<<"rx:"<<rx;
288         }
289         if (pointerTop || pointerBottom) {
290             debugCallout<<'\t'<<"modifier Y is ouside viewbox";
291             qreal ay = newSize.height();
292             qreal by = newPathSize.height();
293             qreal vby = viewBox.height();
294             qreal ry = (by - ay) / ay;
295             qreal my = pointerTop ? -vby * ry : vby + (vby * ry);
296             modifiers[1] = my;
297         } else if (movedY) {
298             debugCallout<<'\t'<<"top side resize and modifier Y is inside viewbox";
299             qreal a = newSize.height();
300             qreal b = m_prevSize.height();
301             qreal vb = viewBox.height();
302             qreal rvb = a / b;
303             qreal vbDiff = (vb * rvb) - vb;
304             qreal y = modifiers[1] + vbDiff;
305             qreal ry = y / (vb + vbDiff);
306             qreal m = vb * ry;
307             modifiers[1] = m;
308             debugCallout<<'\t'<<"vbDiff:"<<vbDiff<<"y:"<<y<<"ry:"<<ry;
309         } else {
310             debugCallout<<'\t'<<"bottom side resize and modifier Y is inside viewbox";
311             qreal a = newSize.height();
312             qreal b = m_prevSize.height();
313             qreal vb = viewBox.height();
314             qreal ry = a / b;
315             qreal y = modifiers[1] - viewBox.top();
316             qreal m = y / ry;
317             modifiers[1] = m;
318             debugCallout<<'\t'<<"ry:"<<ry;
319         }
320         debugCallout<<'\t'<<"New modifiers:"<<modifiers;
321 
322         path->setModifiers(modifiers);
323         params = path->parameters(); // get the new parameters
324         path->setParameters(params); // set new parameters to get everything updated
325     }
326 
327     QRectF pathRect(viewBox);
328     if (modifiers[0] < pathRect.left()) {
329         pathRect.setLeft(modifiers[0]);
330     } else if (modifiers[0] > pathRect.right()) {
331         pathRect.setRight(modifiers[0]);
332     }
333     if (modifiers[1] < pathRect.top()) {
334         pathRect.setTop(modifiers[1]);
335     } else if (modifiers[1] > pathRect.bottom()) {
336         pathRect.setBottom(modifiers[1]);
337     }
338     //debugCallout<<'\t'<<"pathrect:"<<pathRect<<"viewbox:"<<viewBox;
339     qreal size_ratioX = pathRect.width() / viewBox.width();
340     qreal size_ratioY = pathRect.height() / viewBox.height();
341     qreal pw = newSize.width() * size_ratioX;
342     qreal ph = newSize.height() * size_ratioY;
343     QSizeF pathSize = QSizeF(pw, ph);
344     //debugCallout<<'\t'<<newSize<<"size ratio:"<<size_ratioX<<size_ratioY<<"path size:"<<pathSize;
345     path->setSize(pathSize);
346 
347     qreal x = 0.0;
348     qreal y = 0.0;
349     if (pathRect.left() < 0.0) {
350         x = newSize.width() - pathSize.width();
351     }
352     if (pathRect.top() < 0.0) {
353         y = newSize.height() - pathSize.height();
354     }
355     path->setPosition(QPointF(x, y));
356 
357     KoShape *textShape = path->text();
358     if (textShape) {
359         textShape->setSize(newSize);
360     }
361     debugCalloutF<<"<<<<";
362 }
363 
364