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