1 /* This file is part of the KDE project
2  * Copyright (C) 2007,2011 Jan Hambrecht <jaham@gmx.net>
3  * Copyright (C) 2008 Rob Buis <buis@kde.org>
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Library General Public
7  * License as published by the Free Software Foundation; either
8  * version 2 of the License, or (at your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Library General Public License for more details.
14  *
15  * You should have received a copy of the GNU Library General Public License
16  * along with this library; see the file COPYING.LIB.  If not, write to
17  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18  * Boston, MA 02110-1301, USA.
19  */
20 
21 #include "ArtisticTextTool.h"
22 #include "ArtisticTextToolSelection.h"
23 #include "AttachTextToPathCommand.h"
24 #include "DetachTextFromPathCommand.h"
25 #include "AddTextRangeCommand.h"
26 #include "RemoveTextRangeCommand.h"
27 #include "ArtisticTextShapeConfigWidget.h"
28 #include "ArtisticTextShapeOnPathWidget.h"
29 #include "MoveStartOffsetStrategy.h"
30 #include "SelectTextStrategy.h"
31 #include "ChangeTextOffsetCommand.h"
32 #include "ChangeTextFontCommand.h"
33 #include "ChangeTextAnchorCommand.h"
34 #include "ReplaceTextRangeCommand.h"
35 
36 #include <KoCanvasBase.h>
37 #include <KoSelection.h>
38 #include <KoShapeManager.h>
39 #include <KoSelectedShapesProxy.h>
40 #include <KoPointerEvent.h>
41 #include <KoPathShape.h>
42 #include <KoShapeBackground.h>
43 #include <KoShapeController.h>
44 #include <KoShapeContainer.h>
45 #include <KoInteractionStrategy.h>
46 #include <KoIcon.h>
47 #include <KoViewConverter.h>
48 #include "kis_action_registry.h"
49 
50 #include <klocalizedstring.h>
51 #include <kstandardaction.h>
52 #include <QAction>
53 #include <QDebug>
54 
55 #include <QGridLayout>
56 #include <QToolButton>
57 #include <QCheckBox>
58 #include <QPainter>
59 #include <QPainterPath>
60 #include <kundo2command.h>
61 
62 #include <float.h>
63 #include <math.h>
64 
65 const int BlinkInterval = 500;
66 
hit(const QKeySequence & input,KStandardShortcut::StandardShortcut shortcut)67 static bool hit(const QKeySequence &input, KStandardShortcut::StandardShortcut shortcut)
68 {
69     foreach (const QKeySequence &ks, KStandardShortcut::shortcut(shortcut)) {
70         if (input == ks) {
71             return true;
72         }
73     }
74     return false;
75 }
76 
ArtisticTextTool(KoCanvasBase * canvas)77 ArtisticTextTool::ArtisticTextTool(KoCanvasBase *canvas)
78     : KoToolBase(canvas)
79     , m_selection(canvas, this)
80     , m_currentShape(0)
81     , m_hoverText(0)
82     , m_hoverPath(0)
83     , m_hoverHandle(false)
84     , m_textCursor(-1)
85     , m_showCursor(true)
86     , m_currentStrategy(0)
87 {
88     KisActionRegistry *actionRegistry = KisActionRegistry::instance();
89     m_detachPath  = actionRegistry->makeQAction("artistictext_detach_from_path", this);
90     m_detachPath->setEnabled(false);
91     connect(m_detachPath, SIGNAL(triggered()), this, SLOT(detachPath()));
92 //    addAction("artistictext_detach_from_path", m_detachPath);
93 
94     m_convertText  = actionRegistry->makeQAction("artistictext_convert_to_path", this);
95     m_convertText->setEnabled(false);
96     connect(m_convertText, SIGNAL(triggered()), this, SLOT(convertText()));
97 //    addAction("artistictext_convert_to_path", m_convertText);
98 
99     m_fontBold = actionRegistry->makeQAction("artistictext_font_bold", this);
100     connect(m_fontBold, SIGNAL(toggled(bool)), this, SLOT(toggleFontBold(bool)));
101 //    addAction("artistictext_font_bold", m_fontBold);
102 
103     m_fontItalic = actionRegistry->makeQAction("artistictext_font_italic", this);
104     connect(m_fontItalic, SIGNAL(toggled(bool)), this, SLOT(toggleFontItalic(bool)));
105 //    addAction("artistictext_font_italic", m_fontItalic);
106 
107     m_superScript = actionRegistry->makeQAction("artistictext_superscript", this);
108     connect(m_superScript, SIGNAL(triggered()), this, SLOT(setSuperScript()));
109 //    addAction("artistictext_superscript", m_superScript);
110 
111     m_subScript = actionRegistry->makeQAction("artistictext_subscript", this);
112     connect(m_subScript, SIGNAL(triggered()), this, SLOT(setSubScript()));
113 //    addAction("artistictext_subscript", m_subScript);
114 
115     QAction *anchorStart = actionRegistry->makeQAction("artistictext_anchor_start", this);
116     anchorStart->setData(ArtisticTextShape::AnchorStart);
117 //    addAction("artistictext_anchor_start", anchorStart);
118 
119     QAction *anchorMiddle = actionRegistry->makeQAction("artistictext_anchor_middle", this);
120     anchorMiddle->setData(ArtisticTextShape::AnchorMiddle);
121 //    addAction("artistictext_anchor_middle", anchorMiddle);
122 
123     QAction *anchorEnd = actionRegistry->makeQAction("artistictext_anchor_end", this);
124     anchorEnd->setData(ArtisticTextShape::AnchorEnd);
125 //    addAction("artistictext_anchor_end", anchorEnd);
126 
127     m_anchorGroup = new QActionGroup(this);
128     m_anchorGroup->setExclusive(true);
129     m_anchorGroup->addAction(anchorStart);
130     m_anchorGroup->addAction(anchorMiddle);
131     m_anchorGroup->addAction(anchorEnd);
132     connect(m_anchorGroup, SIGNAL(triggered(QAction*)), this, SLOT(anchorChanged(QAction*)));
133 
134     connect(canvas->selectedShapesProxy(), SIGNAL(selectionContentChanged()), this, SLOT(textChanged()));
135 
136 //    addAction("edit_select_all", KStandardAction::selectAll(this, SLOT(selectAll()), this));
137 //    addAction("edit_deselect_all", KStandardAction::deselect(this, SLOT(deselectAll()), this));
138 
139     setTextMode(true);
140 }
141 
~ArtisticTextTool()142 ArtisticTextTool::~ArtisticTextTool()
143 {
144     delete m_currentStrategy;
145 }
146 
cursorTransform() const147 QTransform ArtisticTextTool::cursorTransform() const
148 {
149     if (!m_currentShape) {
150         return QTransform();
151     }
152 
153     QTransform transform;
154 
155     const int textLength = m_currentShape->plainText().length();
156     if (m_textCursor <= textLength) {
157         const QPointF pos = m_currentShape->charPositionAt(m_textCursor);
158         const qreal angle = m_currentShape->charAngleAt(m_textCursor);
159         QFontMetrics metrics(m_currentShape->fontAt(m_textCursor));
160 
161         transform.translate(pos.x() - 1, pos.y());
162         transform.rotate(360. - angle);
163         transform.translate(0, metrics.descent());
164     } else if (m_textCursor <= textLength + m_linefeedPositions.size()) {
165         const QPointF pos = m_linefeedPositions.value(m_textCursor - textLength - 1);
166         QFontMetrics metrics(m_currentShape->fontAt(textLength - 1));
167         transform.translate(pos.x(), pos.y());
168         transform.translate(0, metrics.descent());
169     }
170 
171     return transform * m_currentShape->absoluteTransformation();
172 }
173 
paint(QPainter & painter,const KoViewConverter & converter)174 void ArtisticTextTool::paint(QPainter &painter, const KoViewConverter &converter)
175 {
176     if (! m_currentShape) {
177         return;
178     }
179 
180     if (m_showCursor && m_blinkingCursor.isActive() && !m_currentStrategy) {
181         painter.save();
182         painter.setTransform(converter.documentToView(), true);
183         painter.setBrush(Qt::black);
184         painter.setWorldTransform(cursorTransform(), true);
185         painter.setClipping(false);
186         painter.drawPath(m_textCursorShape);
187         painter.restore();
188     }
189     m_showCursor = !m_showCursor;
190 
191     if (m_currentShape->isOnPath()) {
192         painter.save();
193         painter.setTransform(converter.documentToView(), true);
194         if (!m_currentShape->baselineShape()) {
195             painter.setPen(Qt::DotLine);
196             painter.setBrush(Qt::NoBrush);
197             painter.drawPath(m_currentShape->baseline());
198         }
199         painter.setPen(Qt::blue);
200         painter.setBrush(m_hoverHandle ? Qt::red : Qt::white);
201         painter.drawPath(offsetHandleShape());
202         painter.restore();
203     }
204     if (m_selection.hasSelection()) {
205         painter.save();
206         painter.setTransform(converter.documentToView(), true);
207         m_selection.paint(painter);
208         painter.restore();
209     }
210 }
211 
repaintDecorations()212 void ArtisticTextTool::repaintDecorations()
213 {
214     canvas()->updateCanvas(offsetHandleShape().boundingRect());
215     if (m_currentShape && m_currentShape->isOnPath()) {
216         if (!m_currentShape->baselineShape()) {
217             canvas()->updateCanvas(m_currentShape->baseline().boundingRect());
218         }
219     }
220     m_selection.repaintDecoration();
221 }
222 
cursorFromMousePosition(const QPointF & mousePosition)223 int ArtisticTextTool::cursorFromMousePosition(const QPointF &mousePosition)
224 {
225     if (!m_currentShape) {
226         return -1;
227     }
228 
229     const QPointF pos = m_currentShape->documentToShape(mousePosition);
230     const int len = m_currentShape->plainText().length();
231     int hit = -1;
232     qreal mindist = DBL_MAX;
233     for (int i = 0; i <= len; ++i) {
234         QPointF center = pos - m_currentShape->charPositionAt(i);
235         if ((fabs(center.x()) + fabs(center.y())) < mindist) {
236             hit = i;
237             mindist = fabs(center.x()) + fabs(center.y());
238         }
239     }
240     return hit;
241 }
242 
mousePressEvent(KoPointerEvent * event)243 void ArtisticTextTool::mousePressEvent(KoPointerEvent *event)
244 {
245     if (m_hoverHandle) {
246         m_currentStrategy = new MoveStartOffsetStrategy(this, m_currentShape);
247     }
248     if (m_hoverText) {
249         KoSelection *selection = canvas()->selectedShapesProxy()->selection();
250         if (m_hoverText != m_currentShape) {
251             // if we hit another text shape, select that shape
252             selection->deselectAll();
253             setCurrentShape(m_hoverText);
254             selection->select(m_currentShape);
255         }
256         // change the text cursor position
257         int hitCursorPos = cursorFromMousePosition(event->point);
258         if (hitCursorPos >= 0) {
259             setTextCursorInternal(hitCursorPos);
260             m_selection.clear();
261         }
262         m_currentStrategy = new SelectTextStrategy(this, m_textCursor);
263     }
264     event->ignore();
265 }
266 
mouseMoveEvent(KoPointerEvent * event)267 void ArtisticTextTool::mouseMoveEvent(KoPointerEvent *event)
268 {
269     m_hoverPath = 0;
270     m_hoverText = 0;
271 
272     if (m_currentStrategy) {
273         m_currentStrategy->handleMouseMove(event->point, event->modifiers());
274         return;
275     }
276 
277     const bool textOnPath = m_currentShape && m_currentShape->isOnPath();
278     if (textOnPath) {
279         QPainterPath handle = offsetHandleShape();
280         QPointF handleCenter = handle.boundingRect().center();
281         if (handleGrabRect(event->point).contains(handleCenter)) {
282             // mouse is on offset handle
283             if (!m_hoverHandle) {
284                 canvas()->updateCanvas(handle.boundingRect());
285             }
286             m_hoverHandle = true;
287         } else {
288             if (m_hoverHandle) {
289                 canvas()->updateCanvas(handle.boundingRect());
290             }
291             m_hoverHandle = false;
292         }
293     }
294     if (!m_hoverHandle) {
295         // find text or path shape at cursor position
296         QList<KoShape *> shapes = canvas()->shapeManager()->shapesAt(handleGrabRect(event->point));
297         if (shapes.contains(m_currentShape)) {
298             m_hoverText = m_currentShape;
299         } else {
300             Q_FOREACH (KoShape *shape, shapes) {
301                 ArtisticTextShape *text = dynamic_cast<ArtisticTextShape *>(shape);
302                 if (text && !m_hoverText) {
303                     m_hoverText = text;
304                 }
305                 KoPathShape *path = dynamic_cast<KoPathShape *>(shape);
306                 if (path && !m_hoverPath) {
307                     m_hoverPath = path;
308                 }
309                 if (m_hoverPath && m_hoverText) {
310                     break;
311                 }
312             }
313         }
314     }
315 
316     const bool hoverOnBaseline = textOnPath && m_currentShape && m_currentShape->baselineShape() == m_hoverPath;
317     // update cursor and status text
318     if (m_hoverText) {
319         useCursor(QCursor(Qt::IBeamCursor));
320         if (m_hoverText == m_currentShape) {
321             emit statusTextChanged(i18n("Click to change cursor position."));
322         } else {
323             emit statusTextChanged(i18n("Click to select text shape."));
324         }
325     } else if (m_hoverPath && m_currentShape && !hoverOnBaseline) {
326         useCursor(QCursor(Qt::PointingHandCursor));
327         emit statusTextChanged(i18n("Double click to put text on path."));
328     } else  if (m_hoverHandle) {
329         useCursor(QCursor(Qt::ArrowCursor));
330         emit statusTextChanged(i18n("Drag handle to change start offset."));
331     } else {
332         useCursor(QCursor(Qt::ArrowCursor));
333         if (m_currentShape) {
334             emit statusTextChanged(i18n("Press escape to finish editing."));
335         } else {
336             emit statusTextChanged(QString());
337         }
338     }
339 }
340 
mouseReleaseEvent(KoPointerEvent * event)341 void ArtisticTextTool::mouseReleaseEvent(KoPointerEvent *event)
342 {
343     if (m_currentStrategy) {
344         m_currentStrategy->finishInteraction(event->modifiers());
345         KUndo2Command *cmd = m_currentStrategy->createCommand();
346         if (cmd) {
347             canvas()->addCommand(cmd);
348         }
349         delete m_currentStrategy;
350         m_currentStrategy = 0;
351     }
352     updateActions();
353 }
354 
shortcutOverrideEvent(QKeyEvent * event)355 void ArtisticTextTool::shortcutOverrideEvent(QKeyEvent *event)
356 {
357     QKeySequence item(event->key() | ((Qt::ControlModifier | Qt::AltModifier) & event->modifiers()));
358     if (hit(item, KStandardShortcut::Begin) ||
359             hit(item, KStandardShortcut::End)) {
360         event->accept();
361     }
362 }
363 
mouseDoubleClickEvent(KoPointerEvent *)364 void ArtisticTextTool::mouseDoubleClickEvent(KoPointerEvent */*event*/)
365 {
366     if (m_hoverPath && m_currentShape) {
367         if (!m_currentShape->isOnPath() || m_currentShape->baselineShape() != m_hoverPath) {
368             m_blinkingCursor.stop();
369             m_showCursor = false;
370             updateTextCursorArea();
371             canvas()->addCommand(new AttachTextToPathCommand(m_currentShape, m_hoverPath));
372             m_blinkingCursor.start(BlinkInterval);
373             updateActions();
374             m_hoverPath = 0;
375             m_linefeedPositions.clear();
376             return;
377         }
378     }
379 }
380 
keyPressEvent(QKeyEvent * event)381 void ArtisticTextTool::keyPressEvent(QKeyEvent *event)
382 {
383     if (event->key() == Qt::Key_Escape) {
384         event->ignore();
385         return;
386     }
387 
388     event->accept();
389     if (m_currentShape && textCursor() > -1) {
390         switch (event->key()) {
391         case Qt::Key_Delete:
392             if (m_selection.hasSelection()) {
393                 removeFromTextCursor(m_selection.selectionStart(), m_selection.selectionCount());
394             } else if (textCursor() >= 0 && textCursor() < m_currentShape->plainText().length()) {
395                 removeFromTextCursor(textCursor(), 1);
396             }
397             break;
398         case Qt::Key_Backspace:
399             if (m_selection.hasSelection()) {
400                 removeFromTextCursor(m_selection.selectionStart(), m_selection.selectionCount());
401             } else {
402                 removeFromTextCursor(textCursor() - 1, 1);
403             }
404             break;
405         case Qt::Key_Right:
406             if (event->modifiers() & Qt::ShiftModifier) {
407                 int selectionStart, selectionEnd;
408                 if (m_selection.hasSelection()) {
409                     selectionStart = m_selection.selectionStart();
410                     selectionEnd = selectionStart + m_selection.selectionCount();
411                     if (textCursor() == selectionStart) {
412                         selectionStart = textCursor() + 1;
413                     } else if (textCursor() == selectionEnd) {
414                         selectionEnd = textCursor() + 1;
415                     }
416                 } else {
417                     selectionStart = textCursor();
418                     selectionEnd = textCursor() + 1;
419                 }
420                 m_selection.selectText(selectionStart, selectionEnd);
421             } else {
422                 m_selection.clear();
423             }
424             setTextCursor(m_currentShape, textCursor() + 1);
425             break;
426         case Qt::Key_Left:
427             if (event->modifiers() & Qt::ShiftModifier) {
428                 int selectionStart, selectionEnd;
429                 if (m_selection.hasSelection()) {
430                     selectionStart = m_selection.selectionStart();
431                     selectionEnd = selectionStart + m_selection.selectionCount();
432                     if (textCursor() == selectionStart) {
433                         selectionStart = textCursor() - 1;
434                     } else if (textCursor() == selectionEnd) {
435                         selectionEnd = textCursor() - 1;
436                     }
437                 } else {
438                     selectionEnd = textCursor();
439                     selectionStart = textCursor() - 1;
440                 }
441                 m_selection.selectText(selectionStart, selectionEnd);
442             } else {
443                 m_selection.clear();
444             }
445             setTextCursor(m_currentShape, textCursor() - 1);
446             break;
447         case Qt::Key_Home:
448             if (event->modifiers() & Qt::ShiftModifier) {
449                 const int selectionStart = 0;
450                 const int selectionEnd = m_selection.hasSelection() ? m_selection.selectionStart() + m_selection.selectionCount() : m_textCursor;
451                 m_selection.selectText(selectionStart, selectionEnd);
452             } else {
453                 m_selection.clear();
454             }
455             setTextCursor(m_currentShape, 0);
456             break;
457         case Qt::Key_End:
458             if (event->modifiers() & Qt::ShiftModifier) {
459                 const int selectionStart = m_selection.hasSelection() ? m_selection.selectionStart() : m_textCursor;
460                 const int selectionEnd = m_currentShape->plainText().length();
461                 m_selection.selectText(selectionStart, selectionEnd);
462             } else {
463                 m_selection.clear();
464             }
465             setTextCursor(m_currentShape, m_currentShape->plainText().length());
466             break;
467         case Qt::Key_Return:
468         case Qt::Key_Enter: {
469             const int textLength = m_currentShape->plainText().length();
470             if (m_textCursor >= textLength) {
471                 // get font metrics for last character
472                 QFontMetrics metrics(m_currentShape->fontAt(textLength - 1));
473                 const qreal offset = m_currentShape->size().height() + (m_linefeedPositions.size() + 1) * metrics.height();
474                 m_linefeedPositions.append(QPointF(0, offset));
475                 setTextCursor(m_currentShape, textCursor() + 1);
476             }
477             break;
478         }
479         default:
480             if (event->text().isEmpty()) {
481                 event->ignore();
482                 return;
483             }
484             if (m_selection.hasSelection()) {
485                 removeFromTextCursor(m_selection.selectionStart(), m_selection.selectionCount());
486             }
487             addToTextCursor(event->text());
488         }
489     } else {
490         event->ignore();
491     }
492 }
493 
activate(ToolActivation activation,const QSet<KoShape * > & shapes)494 void ArtisticTextTool::activate(ToolActivation activation, const QSet<KoShape *> &shapes)
495 {
496     KoToolBase::activate(activation, shapes);
497 
498     foreach (KoShape *shape, shapes) {
499         ArtisticTextShape *text = dynamic_cast<ArtisticTextShape *>(shape);
500         if (text) {
501             setCurrentShape(text);
502             break;
503         }
504     }
505     if (!m_currentShape) {
506         // none found
507         emit done();
508         return;
509     }
510 
511     m_hoverText = 0;
512     m_hoverPath = 0;
513 
514     updateActions();
515     emit statusTextChanged(i18n("Press return to finish editing."));
516     repaintDecorations();
517 
518     connect(canvas()->selectedShapesProxy(), SIGNAL(selectionChanged()), this, SLOT(shapeSelectionChanged()));
519 }
520 
blinkCursor()521 void ArtisticTextTool::blinkCursor()
522 {
523     updateTextCursorArea();
524 }
525 
deactivate()526 void ArtisticTextTool::deactivate()
527 {
528     if (m_currentShape) {
529         if (m_currentShape->plainText().isEmpty()) {
530             canvas()->addCommand(canvas()->shapeController()->removeShape(m_currentShape));
531         }
532         setCurrentShape(0);
533     }
534     m_hoverPath = 0;
535     m_hoverText = 0;
536 
537     disconnect(canvas()->selectedShapesProxy(), SIGNAL(selectionChanged()), this, SLOT(shapeSelectionChanged()));
538 
539     KoToolBase::deactivate();
540 }
541 
updateActions()542 void ArtisticTextTool::updateActions()
543 {
544     if (m_currentShape) {
545         const QFont font = m_currentShape->fontAt(textCursor());
546         const CharIndex index = m_currentShape->indexOfChar(textCursor());
547         ArtisticTextRange::BaselineShift baselineShift = ArtisticTextRange::None;
548         if (index.first >= 0) {
549             baselineShift = m_currentShape->text().at(index.first).baselineShift();
550         }
551         m_fontBold->blockSignals(true);
552         m_fontBold->setChecked(font.bold());
553         m_fontBold->blockSignals(false);
554         m_fontBold->setEnabled(true);
555         m_fontItalic->blockSignals(true);
556         m_fontItalic->setChecked(font.italic());
557         m_fontItalic->blockSignals(false);
558         m_fontItalic->setEnabled(true);
559         m_detachPath->setEnabled(m_currentShape->isOnPath());
560         m_convertText->setEnabled(true);
561         m_anchorGroup->blockSignals(true);
562         Q_FOREACH (QAction *action, m_anchorGroup->actions()) {
563             if (action->data().toInt() == m_currentShape->textAnchor()) {
564                 action->setChecked(true);
565             }
566         }
567         m_anchorGroup->blockSignals(false);
568         m_anchorGroup->setEnabled(true);
569         m_superScript->blockSignals(true);
570         m_superScript->setChecked(baselineShift == ArtisticTextRange::Super);
571         m_superScript->blockSignals(false);
572         m_subScript->blockSignals(true);
573         m_subScript->setChecked(baselineShift == ArtisticTextRange::Sub);
574         m_subScript->blockSignals(false);
575         m_superScript->setEnabled(true);
576         m_subScript->setEnabled(true);
577     } else {
578         m_detachPath->setEnabled(false);
579         m_convertText->setEnabled(false);
580         m_fontBold->setEnabled(false);
581         m_fontItalic->setEnabled(false);
582         m_anchorGroup->setEnabled(false);
583         m_superScript->setEnabled(false);
584         m_subScript->setEnabled(false);
585     }
586 }
587 
detachPath()588 void ArtisticTextTool::detachPath()
589 {
590     if (m_currentShape && m_currentShape->isOnPath()) {
591         canvas()->addCommand(new DetachTextFromPathCommand(m_currentShape));
592         updateActions();
593     }
594 }
595 
convertText()596 void ArtisticTextTool::convertText()
597 {
598     if (! m_currentShape) {
599         return;
600     }
601 
602     KoPathShape *path = KoPathShape::createShapeFromPainterPath(m_currentShape->outline());
603     path->setZIndex(m_currentShape->zIndex());
604     path->setStroke(m_currentShape->stroke());
605     path->setBackground(m_currentShape->background());
606     path->setTransformation(m_currentShape->transformation());
607     path->setShapeId(KoPathShapeId);
608 
609     KUndo2Command *cmd = canvas()->shapeController()->addShapeDirect(path, 0);
610     cmd->setText(kundo2_i18n("Convert to Path"));
611     canvas()->shapeController()->removeShape(m_currentShape, cmd);
612     canvas()->addCommand(cmd);
613 
614     emit done();
615 }
616 
createOptionWidgets()617 QList<QPointer<QWidget> > ArtisticTextTool::createOptionWidgets()
618 {
619     QList<QPointer<QWidget> > widgets;
620 
621     ArtisticTextShapeConfigWidget *configWidget = new ArtisticTextShapeConfigWidget(this);
622     configWidget->setObjectName("ArtisticTextConfigWidget");
623     configWidget->setWindowTitle(i18n("Text Properties"));
624     connect(configWidget, SIGNAL(fontFamilyChanged(QFont)), this, SLOT(setFontFamiliy(QFont)));
625     connect(configWidget, SIGNAL(fontSizeChanged(int)), this, SLOT(setFontSize(int)));
626     connect(this, SIGNAL(shapeSelected()), configWidget, SLOT(updateWidget()));
627     connect(canvas()->selectedShapesProxy(), SIGNAL(selectionContentChanged()),
628             configWidget, SLOT(updateWidget()));
629     widgets.append(configWidget);
630 
631     ArtisticTextShapeOnPathWidget *pathWidget = new ArtisticTextShapeOnPathWidget(this);
632     pathWidget->setObjectName("ArtisticTextPathWidget");
633     pathWidget->setWindowTitle(i18n("Text On Path"));
634     connect(pathWidget, SIGNAL(offsetChanged(int)), this, SLOT(setStartOffset(int)));
635     connect(this, SIGNAL(shapeSelected()), pathWidget, SLOT(updateWidget()));
636     connect(canvas()->selectedShapesProxy(), SIGNAL(selectionContentChanged()),
637             pathWidget, SLOT(updateWidget()));
638     widgets.append(pathWidget);
639 
640     if (m_currentShape) {
641         pathWidget->updateWidget();
642         configWidget->updateWidget();
643     }
644 
645     return widgets;
646 }
647 
selection()648 KoToolSelection *ArtisticTextTool::selection()
649 {
650     return &m_selection;
651 }
652 
enableTextCursor(bool enable)653 void ArtisticTextTool::enableTextCursor(bool enable)
654 {
655     if (enable) {
656         if (m_currentShape) {
657             setTextCursorInternal(m_currentShape->plainText().length());
658         }
659         connect(&m_blinkingCursor, SIGNAL(timeout()), this, SLOT(blinkCursor()));
660         m_blinkingCursor.start(BlinkInterval);
661     } else {
662         m_blinkingCursor.stop();
663         disconnect(&m_blinkingCursor, SIGNAL(timeout()), this, SLOT(blinkCursor()));
664         setTextCursorInternal(-1);
665         m_showCursor = false;
666     }
667 }
668 
setTextCursor(ArtisticTextShape * textShape,int textCursor)669 void ArtisticTextTool::setTextCursor(ArtisticTextShape *textShape, int textCursor)
670 {
671     if (!m_currentShape || textShape != m_currentShape) {
672         return;
673     }
674     if (m_textCursor == textCursor || textCursor < 0) {
675         return;
676     }
677     const int textLength = m_currentShape->plainText().length() + m_linefeedPositions.size();
678     if (textCursor > textLength) {
679         return;
680     }
681     setTextCursorInternal(textCursor);
682 }
683 
textCursor() const684 int ArtisticTextTool::textCursor() const
685 {
686     return m_textCursor;
687 }
688 
updateTextCursorArea() const689 void ArtisticTextTool::updateTextCursorArea() const
690 {
691     if (! m_currentShape || m_textCursor < 0) {
692         return;
693     }
694 
695     QRectF bbox = cursorTransform().mapRect(m_textCursorShape.boundingRect());
696     canvas()->updateCanvas(bbox);
697 }
698 
setCurrentShape(ArtisticTextShape * currentShape)699 void ArtisticTextTool::setCurrentShape(ArtisticTextShape *currentShape)
700 {
701     if (m_currentShape == currentShape) {
702         return;
703     }
704     enableTextCursor(false);
705     m_currentShape = currentShape;
706     m_selection.setSelectedShape(m_currentShape);
707     if (m_currentShape) {
708         enableTextCursor(true);
709     }
710     emit shapeSelected();
711 }
712 
setTextCursorInternal(int textCursor)713 void ArtisticTextTool::setTextCursorInternal(int textCursor)
714 {
715     updateTextCursorArea();
716     m_textCursor = textCursor;
717     createTextCursorShape();
718     updateTextCursorArea();
719     updateActions();
720     emit shapeSelected();
721 }
722 
createTextCursorShape()723 void ArtisticTextTool::createTextCursorShape()
724 {
725     if (m_textCursor < 0 || ! m_currentShape) {
726         return;
727     }
728     const QRectF extents = m_currentShape->charExtentsAt(m_textCursor);
729     m_textCursorShape = QPainterPath();
730     m_textCursorShape.addRect(0, 0, 1, -extents.height());
731     m_textCursorShape.closeSubpath();
732 }
733 
removeFromTextCursor(int from,unsigned int count)734 void ArtisticTextTool::removeFromTextCursor(int from, unsigned int count)
735 {
736     if (from >= 0) {
737         if (m_selection.hasSelection()) {
738             // clear selection before text is removed, or else selection will be invalid
739             m_selection.clear();
740         }
741         KUndo2Command *cmd = new RemoveTextRangeCommand(this, m_currentShape, from, count);
742         canvas()->addCommand(cmd);
743     }
744 }
745 
addToTextCursor(const QString & str)746 void ArtisticTextTool::addToTextCursor(const QString &str)
747 {
748     if (!str.isEmpty() && m_textCursor > -1) {
749         QString printable;
750         for (int i = 0; i < str.length(); i++) {
751             if (str[i].isPrint()) {
752                 printable.append(str[i]);
753             }
754         }
755         unsigned int len = printable.length();
756         if (len) {
757             const int textLength = m_currentShape->plainText().length();
758             if (m_textCursor <= textLength) {
759                 KUndo2Command *cmd = new AddTextRangeCommand(this, m_currentShape, printable, m_textCursor);
760                 canvas()->addCommand(cmd);
761             } else if (m_textCursor <= textLength + m_linefeedPositions.size()) {
762                 const QPointF pos = m_linefeedPositions.value(m_textCursor - textLength - 1);
763                 ArtisticTextRange newLine(printable, m_currentShape->fontAt(textLength - 1));
764                 newLine.setXOffsets(QList<qreal>() << pos.x(), ArtisticTextRange::AbsoluteOffset);
765                 newLine.setYOffsets(QList<qreal>() << pos.y() - m_currentShape->baselineOffset(), ArtisticTextRange::AbsoluteOffset);
766                 KUndo2Command *cmd = new AddTextRangeCommand(this, m_currentShape, newLine, m_textCursor);
767                 canvas()->addCommand(cmd);
768                 m_linefeedPositions.clear();
769             }
770         }
771     }
772 }
773 
textChanged()774 void ArtisticTextTool::textChanged()
775 {
776     if (!m_currentShape) {
777         return;
778     }
779 
780     const QString currentText = m_currentShape->plainText();
781     if (m_textCursor > currentText.length()) {
782         setTextCursorInternal(currentText.length());
783     }
784 }
785 
shapeSelectionChanged()786 void ArtisticTextTool::shapeSelectionChanged()
787 {
788     KoSelection *selection = canvas()->selectedShapesProxy()->selection();
789     if (selection->isSelected(m_currentShape)) {
790         return;
791     }
792 
793     foreach (KoShape *shape, selection->selectedShapes()) {
794         ArtisticTextShape *text = dynamic_cast<ArtisticTextShape *>(shape);
795         if (text) {
796             setCurrentShape(text);
797             break;
798         }
799     }
800 }
801 
offsetHandleShape()802 QPainterPath ArtisticTextTool::offsetHandleShape()
803 {
804     QPainterPath handle;
805     if (!m_currentShape || !m_currentShape->isOnPath()) {
806         return handle;
807     }
808 
809     const QPainterPath baseline = m_currentShape->baseline();
810     const qreal offset = m_currentShape->startOffset();
811     QPointF offsetPoint = baseline.pointAtPercent(offset);
812     QSizeF paintSize = handlePaintRect(QPointF()).size();
813 
814     handle.moveTo(0, 0);
815     handle.lineTo(0.5 * paintSize.width(), paintSize.height());
816     handle.lineTo(-0.5 * paintSize.width(), paintSize.height());
817     handle.closeSubpath();
818 
819     QTransform transform;
820     transform.translate(offsetPoint.x(), offsetPoint.y());
821     transform.rotate(360. - baseline.angleAtPercent(offset));
822 
823     return transform.map(handle);
824 }
825 
setStartOffset(int offset)826 void ArtisticTextTool::setStartOffset(int offset)
827 {
828     if (!m_currentShape || !m_currentShape->isOnPath()) {
829         return;
830     }
831 
832     const qreal newOffset = static_cast<qreal>(offset) / 100.0;
833     if (newOffset != m_currentShape->startOffset()) {
834         canvas()->addCommand(new ChangeTextOffsetCommand(m_currentShape, m_currentShape->startOffset(), newOffset));
835     }
836 }
837 
changeFontProperty(FontProperty property,const QVariant & value)838 void ArtisticTextTool::changeFontProperty(FontProperty property, const QVariant &value)
839 {
840     if (!m_currentShape || !m_selection.hasSelection()) {
841         return;
842     }
843 
844     // build font ranges
845     const int selectedCharCount = m_selection.selectionCount();
846     const int selectedCharStart = m_selection.selectionStart();
847     QList<ArtisticTextRange> ranges = m_currentShape->text();
848     CharIndex index = m_currentShape->indexOfChar(selectedCharStart);
849     if (index.first < 0) {
850         return;
851     }
852 
853     KUndo2Command *cmd = new KUndo2Command;
854     int collectedCharCount = 0;
855     while (collectedCharCount < selectedCharCount) {
856         ArtisticTextRange &range = ranges[index.first];
857         QFont font = range.font();
858         switch (property) {
859         case BoldProperty:
860             font.setBold(value.toBool());
861             break;
862         case ItalicProperty:
863             font.setItalic(value.toBool());
864             break;
865         case FamiliyProperty:
866             font.setFamily(value.toString());
867             break;
868         case SizeProperty:
869             font.setPointSize(value.toInt());
870             break;
871         }
872 
873         const int changeCount = qMin(selectedCharCount - collectedCharCount, range.text().count() - index.second);
874         const int changeStart = selectedCharStart + collectedCharCount;
875         new ChangeTextFontCommand(m_currentShape, changeStart, changeCount, font, cmd);
876         index.first++;
877         index.second = 0;
878         collectedCharCount += changeCount;
879     }
880 
881     canvas()->addCommand(cmd);
882 }
883 
toggleFontBold(bool enabled)884 void ArtisticTextTool::toggleFontBold(bool enabled)
885 {
886     changeFontProperty(BoldProperty, QVariant(enabled));
887 }
888 
toggleFontItalic(bool enabled)889 void ArtisticTextTool::toggleFontItalic(bool enabled)
890 {
891     changeFontProperty(ItalicProperty, QVariant(enabled));
892 }
893 
anchorChanged(QAction * action)894 void ArtisticTextTool::anchorChanged(QAction *action)
895 {
896     if (!m_currentShape) {
897         return;
898     }
899 
900     ArtisticTextShape::TextAnchor newAnchor = static_cast<ArtisticTextShape::TextAnchor>(action->data().toInt());
901     if (newAnchor != m_currentShape->textAnchor()) {
902         canvas()->addCommand(new ChangeTextAnchorCommand(m_currentShape, newAnchor));
903     }
904 }
905 
setFontFamiliy(const QFont & font)906 void ArtisticTextTool::setFontFamiliy(const QFont &font)
907 {
908     changeFontProperty(FamiliyProperty, QVariant(font.family()));
909 }
910 
setFontSize(int size)911 void ArtisticTextTool::setFontSize(int size)
912 {
913     changeFontProperty(SizeProperty, QVariant(size));
914 }
915 
setSuperScript()916 void ArtisticTextTool::setSuperScript()
917 {
918     toggleSubSuperScript(ArtisticTextRange::Super);
919 }
920 
setSubScript()921 void ArtisticTextTool::setSubScript()
922 {
923     toggleSubSuperScript(ArtisticTextRange::Sub);
924 }
925 
toggleSubSuperScript(ArtisticTextRange::BaselineShift mode)926 void ArtisticTextTool::toggleSubSuperScript(ArtisticTextRange::BaselineShift mode)
927 {
928     if (!m_currentShape || !m_selection.hasSelection()) {
929         return;
930     }
931 
932     const int from = m_selection.selectionStart();
933     const int count = m_selection.selectionCount();
934 
935     QList<ArtisticTextRange> ranges = m_currentShape->copyText(from, count);
936     const int rangeCount = ranges.count();
937     if (!rangeCount) {
938         return;
939     }
940 
941     // determine if we want to disable the specified mode
942     const bool disableMode = ranges.first().baselineShift() == mode;
943     const qreal fontSize = m_currentShape->defaultFont().pointSizeF();
944 
945     for (int i = 0; i < rangeCount; ++i) {
946         ArtisticTextRange &currentRange = ranges[i];
947         QFont font = currentRange.font();
948         if (disableMode) {
949             currentRange.setBaselineShift(ArtisticTextRange::None);
950             font.setPointSizeF(fontSize);
951         } else {
952             currentRange.setBaselineShift(mode);
953             font.setPointSizeF(fontSize * ArtisticTextRange::subAndSuperScriptSizeFactor());
954         }
955         currentRange.setFont(font);
956     }
957     canvas()->addCommand(new ReplaceTextRangeCommand(m_currentShape, ranges, from, count, this));
958 }
959 
selectAll()960 void ArtisticTextTool::selectAll()
961 {
962     if (m_currentShape) {
963         m_selection.selectText(0, m_currentShape->plainText().count());
964     }
965 }
966 
deselectAll()967 void ArtisticTextTool::deselectAll()
968 {
969     if (m_currentShape) {
970         m_selection.clear();
971     }
972 }
973 
inputMethodQuery(Qt::InputMethodQuery query,const KoViewConverter & converter) const974 QVariant ArtisticTextTool::inputMethodQuery(Qt::InputMethodQuery query, const KoViewConverter &converter) const
975 {
976     if (!m_currentShape) {
977         return QVariant();
978     }
979 
980     switch (query) {
981     case Qt::ImMicroFocus: {
982         // The rectangle covering the area of the input cursor in widget coordinates.
983         QRectF rect = m_textCursorShape.boundingRect();
984         rect.moveTop(rect.bottom());
985         QTransform shapeMatrix = m_currentShape->absoluteTransformation();
986         qreal zoomX, zoomY;
987         converter.zoom(&zoomX, &zoomY);
988         shapeMatrix.scale(zoomX, zoomY);
989         rect = shapeMatrix.mapRect(rect);
990         return rect.toRect();
991     }
992     case Qt::ImFont:
993         // The currently used font for text input.
994         return m_currentShape->fontAt(m_textCursor);
995     case Qt::ImCursorPosition:
996         // The logical position of the cursor within the text surrounding the input area (see ImSurroundingText).
997         return m_currentShape->charPositionAt(m_textCursor);
998     case Qt::ImSurroundingText:
999         // The plain text around the input area, for example the current paragraph.
1000         return m_currentShape->plainText();
1001     case Qt::ImCurrentSelection:
1002         // The currently selected text.
1003         return QVariant();
1004     default:
1005         ; // Qt 4.6 adds ImMaximumTextLength and ImAnchorPosition
1006     }
1007     return QVariant();
1008 }
1009