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 ¤tRange = 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