1 /* This file is part of the KDE project
2  * Copyright (C) 2009-2012 Pierre Stirnweiss <pstirnweiss@googlemail.com>
3  * Copyright (C) 2006-2010 Thomas Zander <zander@kde.org>
4  * Copyright (c) 2011 Boudewijn Rempt <boud@kogmbh.com>
5  * Copyright (C) 2011-2012 C. Boemann <cbo@boemann.dk>
6  * Copyright (C) 2014 Denis Kuplyakov <dener.kup@gmail.com>
7  *
8  * This library is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU Library General Public
10  * License as published by the Free Software Foundation; either
11  * version 2 of the License, or (at your option) any later version.
12  *
13  * This library is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16  * Library General Public License for more details.
17  *
18  * You should have received a copy of the GNU Library General Public License
19  * along with this library; see the file COPYING.LIB.  If not, write to
20  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21  * Boston, MA 02110-1301, USA.
22  */
23 
24 #include "KoTextEditor.h"
25 #include "KoTextEditor_p.h"
26 
27 #include "styles/KoCharacterStyle.h"
28 #include "styles/KoParagraphStyle.h"
29 #include "styles/KoStyleManager.h"
30 #include "commands/ParagraphFormattingCommand.h"
31 
32 #include <klocalizedstring.h>
33 
34 #include <QFontDatabase>
35 #include <QTextBlock>
36 #include <QTextBlockFormat>
37 #include <QTextCharFormat>
38 #include <QTextFormat>
39 #include <QTextList>
40 
41 #include "TextDebug.h"
42 #include "KoTextDebug.h"
43 
44 
clearCharFormatProperty(int property)45 void KoTextEditor::Private::clearCharFormatProperty(int property)
46 {
47     class PropertyWiper : public CharFormatVisitor
48     {
49     public:
50         PropertyWiper(int propertyId) : propertyId(propertyId) {}
51         void visit(QTextCharFormat &format) const override {
52             format.clearProperty(propertyId);
53         }
54 
55         int propertyId;
56     };
57     PropertyWiper wiper(property);
58     CharFormatVisitor::visitSelection(q, wiper, KUndo2MagicString(), false);
59 }
60 
bold(bool bold)61 void KoTextEditor::bold(bool bold)
62 {
63     if (isEditProtected()) {
64         return;
65     }
66 
67     d->updateState(KoTextEditor::Private::Format, kundo2_i18n("Bold"));
68     QTextCharFormat format;
69     format.setFontWeight(bold ? QFont::Bold : QFont::Normal);
70     mergeAutoStyle(format);
71     d->updateState(KoTextEditor::Private::NoOp);
72 }
73 
italic(bool italic)74 void KoTextEditor::italic(bool italic)
75 {
76     if (isEditProtected()) {
77         return;
78     }
79 
80     d->updateState(KoTextEditor::Private::Format, kundo2_i18n("Italic"));
81     QTextCharFormat format;
82     format.setFontItalic(italic);
83     mergeAutoStyle(format);
84     d->updateState(KoTextEditor::Private::NoOp);
85 }
86 
underline(bool underline)87 void KoTextEditor::underline(bool underline)
88 {
89     if (isEditProtected()) {
90         return;
91     }
92 
93     d->updateState(KoTextEditor::Private::Format, kundo2_i18n("Underline"));
94     QTextCharFormat format;
95     if (underline) {
96         format.setProperty(KoCharacterStyle::UnderlineType, KoCharacterStyle::SingleLine);
97         format.setProperty(KoCharacterStyle::UnderlineStyle, KoCharacterStyle::SolidLine);
98     } else {
99         format.setProperty(KoCharacterStyle::UnderlineType, KoCharacterStyle::NoLineType);
100         format.setProperty(KoCharacterStyle::UnderlineStyle, KoCharacterStyle::NoLineStyle);
101     }
102     mergeAutoStyle(format);
103     d->updateState(KoTextEditor::Private::NoOp);
104 }
105 
strikeOut(bool strikeout)106 void KoTextEditor::strikeOut(bool strikeout)
107 {
108     if (isEditProtected()) {
109         return;
110     }
111 
112     d->updateState(KoTextEditor::Private::Format, kundo2_i18n("Strike Out"));
113     QTextCharFormat format;
114     if (strikeout) {
115         format.setProperty(KoCharacterStyle::StrikeOutType, KoCharacterStyle::SingleLine);
116         format.setProperty(KoCharacterStyle::StrikeOutStyle, KoCharacterStyle::SolidLine);
117     } else {
118         format.setProperty(KoCharacterStyle::StrikeOutType, KoCharacterStyle::NoLineType);
119         format.setProperty(KoCharacterStyle::StrikeOutStyle, KoCharacterStyle::NoLineStyle);
120     }
121     mergeAutoStyle(format);
122     d->updateState(KoTextEditor::Private::NoOp);
123 }
124 
setHorizontalTextAlignment(Qt::Alignment align)125 void KoTextEditor::setHorizontalTextAlignment(Qt::Alignment align)
126 {
127     if (isEditProtected()) {
128         return;
129     }
130 
131     class Aligner : public BlockFormatVisitor
132     {
133     public:
134         Aligner(Qt::Alignment align) : alignment(align) {}
135         void visit(QTextBlock &block) const override {
136             QTextBlockFormat format = block.blockFormat();
137             format.setAlignment(alignment);
138             QTextCursor cursor(block);
139             cursor.setBlockFormat(format);
140         }
141         Qt::Alignment alignment;
142     };
143 
144     Aligner aligner(align);
145     d->updateState(KoTextEditor::Private::Format, kundo2_i18n("Change Alignment"));
146     BlockFormatVisitor::visitSelection(this, aligner, kundo2_i18n("Change Alignment"));
147     d->updateState(KoTextEditor::Private::NoOp);
148     emit textFormatChanged();
149 }
150 
setVerticalTextAlignment(Qt::Alignment align)151 void KoTextEditor::setVerticalTextAlignment(Qt::Alignment align)
152 {
153     if (isEditProtected()) {
154         return;
155     }
156 
157     QTextCharFormat::VerticalAlignment charAlign = QTextCharFormat::AlignNormal;
158     if (align == Qt::AlignTop)
159         charAlign = QTextCharFormat::AlignSuperScript;
160     else if (align == Qt::AlignBottom)
161         charAlign = QTextCharFormat::AlignSubScript;
162 
163     d->updateState(KoTextEditor::Private::Format, kundo2_i18n("Set Vertical Alignment"));
164     QTextCharFormat format;
165     format.setVerticalAlignment(charAlign);
166     mergeAutoStyle(format);
167     d->updateState(KoTextEditor::Private::NoOp);
168 }
169 
decreaseIndent()170 void KoTextEditor::decreaseIndent()
171 {
172     if (isEditProtected()) {
173         return;
174     }
175 
176     class Indenter : public BlockFormatVisitor
177     {
178     public:
179         void visit(QTextBlock &block) const override {
180             QTextBlockFormat format = block.blockFormat();
181             // TODO make the 10 configurable.
182             format.setLeftMargin(qMax(qreal(0.0), format.leftMargin() - 10));
183 
184             if (block.textList()) {
185                 const QTextListFormat listFormat = block.textList()->format();
186                 if (format.leftMargin() < listFormat.doubleProperty(KoListStyle::Margin)) {
187                     format.setLeftMargin(listFormat.doubleProperty(KoListStyle::Margin));
188                 }
189             }
190             QTextCursor cursor(block);
191             cursor.setBlockFormat(format);
192         }
193         Qt::Alignment alignment;
194     };
195 
196     Indenter indenter;
197     d->updateState(KoTextEditor::Private::Format, kundo2_i18n("Decrease Indent"));
198     BlockFormatVisitor::visitSelection(this, indenter, kundo2_i18n("Decrease Indent"));
199     d->updateState(KoTextEditor::Private::NoOp);
200     emit textFormatChanged();
201 }
202 
increaseIndent()203 void KoTextEditor::increaseIndent()
204 {
205     if (isEditProtected()) {
206         return;
207     }
208 
209     class Indenter : public BlockFormatVisitor
210     {
211     public:
212         void visit(QTextBlock &block) const override {
213             QTextBlockFormat format = block.blockFormat();
214             // TODO make the 10 configurable.
215 
216             if (!block.textList()) {
217                 format.setLeftMargin(format.leftMargin() + 10);
218             } else {
219                 const QTextListFormat listFormat = block.textList()->format();
220                 if (format.leftMargin() == 0) {
221                     format.setLeftMargin(listFormat.doubleProperty(KoListStyle::Margin) + 10);
222                 } else {
223                     format.setLeftMargin(format.leftMargin() + 10);
224                 }
225             }
226             QTextCursor cursor(block);
227             cursor.setBlockFormat(format);
228         }
229         Qt::Alignment alignment;
230     };
231 
232     Indenter indenter;
233     d->updateState(KoTextEditor::Private::Format, kundo2_i18n("Increase Indent"));
234     BlockFormatVisitor::visitSelection(this, indenter, kundo2_i18n("Increase Indent"));
235     d->updateState(KoTextEditor::Private::NoOp);
236     emit textFormatChanged();
237 }
238 
239 class FontResizer : public CharFormatVisitor
240 {
241 public:
242     enum Type { Grow, Shrink };
FontResizer(Type type_)243     FontResizer(Type type_) : type(type_) {
244         QFontDatabase fontDB;
245         defaultSizes = fontDB.standardSizes();
246     }
visit(QTextCharFormat & format) const247     void visit(QTextCharFormat &format) const override {
248         const qreal current = format.fontPointSize();
249         int prev = 1;
250         foreach(int pt, defaultSizes) {
251             if ((type == Grow && pt > current) || (type == Shrink && pt >= current)) {
252                 format.setFontPointSize(type == Grow ? pt : prev);
253                 return;
254             }
255             prev = pt;
256         }
257     }
258 
259     QList<int> defaultSizes;
260     const Type type;
261 };
262 
decreaseFontSize()263 void KoTextEditor::decreaseFontSize()
264 {
265     if (isEditProtected()) {
266         return;
267     }
268 
269     d->updateState(KoTextEditor::Private::Format, kundo2_i18n("Decrease font size"));
270     FontResizer sizer(FontResizer::Shrink);
271     CharFormatVisitor::visitSelection(this, sizer, kundo2_i18n("Decrease font size"));
272     d->updateState(KoTextEditor::Private::NoOp);
273     emit textFormatChanged();
274 }
275 
increaseFontSize()276 void KoTextEditor::increaseFontSize()
277 {
278     if (isEditProtected()) {
279         return;
280     }
281 
282     d->updateState(KoTextEditor::Private::Format, kundo2_i18n("Increase font size"));
283     FontResizer sizer(FontResizer::Grow);
284     CharFormatVisitor::visitSelection(this, sizer, kundo2_i18n("Increase font size"));
285     d->updateState(KoTextEditor::Private::NoOp);
286     emit textFormatChanged();
287 }
288 
setFontFamily(const QString & font)289 void KoTextEditor::setFontFamily(const QString &font)
290 {
291     if (isEditProtected()) {
292         return;
293     }
294 
295     d->updateState(KoTextEditor::Private::Format, kundo2_i18n("Set Font"));
296     QTextCharFormat format;
297     format.setFontFamily(font);
298     mergeAutoStyle(format);
299     d->updateState(KoTextEditor::Private::NoOp);
300 }
301 
setFontSize(qreal size)302 void KoTextEditor::setFontSize(qreal size)
303 {
304     if (isEditProtected()) {
305         return;
306     }
307 
308     d->updateState(KoTextEditor::Private::Format, kundo2_i18n("Set Font Size"));
309     QTextCharFormat format;
310     format.setFontPointSize(size);
311     mergeAutoStyle(format);
312     d->updateState(KoTextEditor::Private::NoOp);
313 }
314 
setTextBackgroundColor(const QColor & color)315 void KoTextEditor::setTextBackgroundColor(const QColor &color)
316 {
317     if (isEditProtected()) {
318         return;
319     }
320 
321     d->updateState(KoTextEditor::Private::Format, kundo2_i18n("Set Background Color"));
322     QTextCharFormat format;
323     format.setBackground(QBrush(color));
324     mergeAutoStyle(format);
325     d->updateState(KoTextEditor::Private::NoOp);
326 }
327 
setTextColor(const QColor & color)328 void KoTextEditor::setTextColor(const QColor &color)
329 {
330     if (isEditProtected()) {
331         return;
332     }
333 
334     d->updateState(KoTextEditor::Private::Format, kundo2_i18n("Set Text Color"));
335     QTextCharFormat format;
336     format.setForeground(QBrush(color));
337     mergeAutoStyle(format);
338     d->updateState(KoTextEditor::Private::NoOp);
339 }
340 
341 class SetCharacterStyleVisitor : public KoTextVisitor
342 {
343 public:
SetCharacterStyleVisitor(KoTextEditor * editor,KoCharacterStyle * style)344     SetCharacterStyleVisitor(KoTextEditor *editor, KoCharacterStyle *style)
345         : KoTextVisitor(editor)
346         , m_style(style)
347     {
348     }
349 
visitBlock(QTextBlock & block,const QTextCursor & caret)350     void visitBlock(QTextBlock &block, const QTextCursor &caret) override
351     {
352         m_newFormat = block.charFormat();
353         m_style->applyStyle(m_newFormat);
354         m_style->ensureMinimalProperties(m_newFormat);
355 
356         KoTextVisitor::visitBlock(block, caret);
357 
358         QList<QTextCharFormat>::Iterator it = m_formats.begin();
359         foreach(QTextCursor cursor, m_cursors) {
360             QTextFormat prevFormat(cursor.charFormat());
361             cursor.setCharFormat(*it);
362             editor()->registerTrackedChange(cursor, KoGenChange::FormatChange, kundo2_i18n("Set Character Style"), *it, prevFormat, false);
363             ++it;
364         }
365     }
366 
visitFragmentSelection(QTextCursor & fragmentSelection)367     void visitFragmentSelection(QTextCursor &fragmentSelection) override
368     {
369         QTextCharFormat format = m_newFormat;
370 
371         QVariant v;
372         v = fragmentSelection.charFormat().property(KoCharacterStyle::InlineInstanceId);
373         if (!v.isNull()) {
374             format.setProperty(KoCharacterStyle::InlineInstanceId, v);
375         }
376 
377         v = fragmentSelection.charFormat().property(KoCharacterStyle::ChangeTrackerId);
378         if (!v.isNull()) {
379             format.setProperty(KoCharacterStyle::ChangeTrackerId, v);
380         }
381 
382         if (fragmentSelection.charFormat().isAnchor()) {
383             format.setAnchor(true);
384             format.setProperty(KoCharacterStyle::AnchorType, fragmentSelection.charFormat().intProperty(KoCharacterStyle::AnchorType));
385             format.setAnchorHref(fragmentSelection.charFormat().anchorHref());
386         }
387 
388         m_formats.append(format);
389         m_cursors.append(fragmentSelection);
390     }
391 
392     KoCharacterStyle *m_style;
393     QTextCharFormat m_newFormat;
394     QList<QTextCharFormat> m_formats;
395     QList<QTextCursor> m_cursors;
396 };
397 
setStyle(KoCharacterStyle * style)398 void KoTextEditor::setStyle(KoCharacterStyle *style)
399 {
400     Q_ASSERT(style);
401     d->updateState(KoTextEditor::Private::Custom, kundo2_i18n("Set Character Style"));
402 
403     int caretAnchor = d->caret.anchor();
404     int caretPosition = d->caret.position();
405 
406     SetCharacterStyleVisitor visitor(this, style);
407 
408     recursivelyVisitSelection(d->document->rootFrame()->begin(), visitor);
409 
410     if (!isEditProtected() && caretAnchor == caretPosition) { //if there is no selection, it can happen that the caret does not get the proper style applied (beginning of a block). We need to force it.
411          //applying a style is absolute, so first initialise the caret with the frame's style, then apply the paragraph's. Finally apply the character style
412         QTextCharFormat charFormat = KoTextDocument(d->document).frameCharFormat();
413         KoStyleManager *styleManager = KoTextDocument(d->document).styleManager();
414         KoParagraphStyle *paragraphStyle = styleManager->paragraphStyle(d->caret.charFormat().intProperty(KoParagraphStyle::StyleId));
415         if (paragraphStyle) {
416             paragraphStyle->KoCharacterStyle::applyStyle(charFormat);
417         }
418         d->caret.setCharFormat(charFormat);
419         style->applyStyle(&(d->caret));
420     }
421     else { //if the caret has a selection, the visitor has already applied the style, reset the caret's position so it picks the proper style.
422         d->caret.setPosition(caretAnchor);
423         d->caret.setPosition(caretPosition, QTextCursor::KeepAnchor);
424     }
425 
426     d->updateState(KoTextEditor::Private::NoOp);
427     emit textFormatChanged();
428     emit characterStyleApplied(style);
429 }
430 
431 
432 class SetParagraphStyleVisitor : public KoTextVisitor
433 {
434 public:
SetParagraphStyleVisitor(KoTextEditor * editor,KoStyleManager * styleManager,KoParagraphStyle * style)435     SetParagraphStyleVisitor(KoTextEditor *editor, KoStyleManager *styleManager, KoParagraphStyle *style)
436         : KoTextVisitor(editor)
437         , m_styleManager(styleManager)
438         , m_style(style)
439     {
440     }
441 
visitBlock(QTextBlock & block,const QTextCursor &)442     void visitBlock(QTextBlock &block, const QTextCursor &) override
443     {
444         if (m_styleManager) {
445             QTextBlockFormat bf = block.blockFormat();
446             KoParagraphStyle *old = m_styleManager->paragraphStyle(bf.intProperty(KoParagraphStyle::StyleId));
447             if (old)
448                 old->unapplyStyle(block);
449         }
450         // The above should unapply the style and it's lists part, but we want to clear everything
451         // except section info.
452         QTextCursor cursor(block);
453         QVariant sectionStartings = cursor.blockFormat().property(KoParagraphStyle::SectionStartings);
454         QVariant sectionEndings = cursor.blockFormat().property(KoParagraphStyle::SectionEndings);
455         QTextBlockFormat fmt;
456         fmt.setProperty(KoParagraphStyle::SectionStartings, sectionStartings);
457         fmt.setProperty(KoParagraphStyle::SectionEndings, sectionEndings);
458         cursor.setBlockFormat(fmt);
459         m_style->applyStyle(block);
460     }
461 
462     KoStyleManager *m_styleManager;
463     KoParagraphStyle *m_style;
464 };
465 
setStyle(KoParagraphStyle * style)466 void KoTextEditor::setStyle(KoParagraphStyle *style)
467 {
468     d->updateState(KoTextEditor::Private::Custom, kundo2_i18n("Set Paragraph Style"));
469 
470     int caretAnchor = d->caret.anchor();
471     int caretPosition = d->caret.position();
472     KoStyleManager *styleManager = KoTextDocument(d->document).styleManager();
473     SetParagraphStyleVisitor visitor(this, styleManager, style);
474 
475     recursivelyVisitSelection(d->document->rootFrame()->begin(), visitor);
476 
477     if (!isEditProtected() && caretAnchor == caretPosition) { //if there is no selection, it can happen that the caret does not get the proper style applied (beginning of a block). We need to force it.
478         //applying a style is absolute, so first initialise the caret with the frame's style, then apply the paragraph style
479         QTextCharFormat charFormat = KoTextDocument(d->document).frameCharFormat();
480         d->caret.setCharFormat(charFormat);
481         style->KoCharacterStyle::applyStyle(&(d->caret));
482     }
483     else {
484         d->caret.setPosition(caretAnchor);
485         d->caret.setPosition(caretPosition, QTextCursor::KeepAnchor);
486     }
487 
488     d->updateState(KoTextEditor::Private::NoOp);
489     emit paragraphStyleApplied(style);
490     emit textFormatChanged();
491 }
492 
493 class MergeAutoCharacterStyleVisitor : public KoTextVisitor
494 {
495 public:
MergeAutoCharacterStyleVisitor(KoTextEditor * editor,QTextCharFormat deltaCharFormat)496     MergeAutoCharacterStyleVisitor(KoTextEditor *editor, QTextCharFormat deltaCharFormat)
497         : KoTextVisitor(editor)
498         , m_deltaCharFormat(deltaCharFormat)
499     {
500     }
501 
visitBlock(QTextBlock & block,const QTextCursor & caret)502     void visitBlock(QTextBlock &block, const QTextCursor &caret) override
503     {
504         KoTextVisitor::visitBlock(block, caret);
505 
506         QList<QTextCharFormat>::Iterator it = m_formats.begin();
507         foreach(QTextCursor cursor, m_cursors) {
508             QTextFormat prevFormat(cursor.charFormat());
509             cursor.setCharFormat(*it);
510             ++it;
511         }
512     }
513 
visitFragmentSelection(QTextCursor & fragmentSelection)514     void visitFragmentSelection(QTextCursor &fragmentSelection) override
515     {
516         QTextCharFormat format = fragmentSelection.charFormat();
517         format.merge(m_deltaCharFormat);
518 
519         m_formats.append(format);
520         m_cursors.append(fragmentSelection);
521     }
522 
523     QTextCharFormat m_deltaCharFormat;
524     QList<QTextCharFormat> m_formats;
525     QList<QTextCursor> m_cursors;
526 };
527 
mergeAutoStyle(const QTextCharFormat & deltaCharFormat)528 void KoTextEditor::mergeAutoStyle(const QTextCharFormat &deltaCharFormat)
529 {
530     d->updateState(KoTextEditor::Private::Custom, kundo2_i18n("Formatting"));
531 
532     int caretAnchor = d->caret.anchor();
533     int caretPosition = d->caret.position();
534     MergeAutoCharacterStyleVisitor visitor(this, deltaCharFormat);
535 
536     recursivelyVisitSelection(d->document->rootFrame()->begin(), visitor);
537 
538     if (!isEditProtected() && caretAnchor == caretPosition) { //if there is no selection, it can happen that the caret does not get the proper style applied (beginning of a block). We need to force it.
539         d->caret.mergeCharFormat(deltaCharFormat);
540     }
541     else {
542         d->caret.setPosition(caretAnchor);
543         d->caret.setPosition(caretPosition, QTextCursor::KeepAnchor);
544     }
545 
546     d->updateState(KoTextEditor::Private::NoOp);
547     emit textFormatChanged();
548 }
549 
550 
applyDirectFormatting(const QTextCharFormat & deltaCharFormat,const QTextBlockFormat & deltaBlockFormat,const KoListLevelProperties & llp)551 void KoTextEditor::applyDirectFormatting(const QTextCharFormat &deltaCharFormat,
552                                   const QTextBlockFormat &deltaBlockFormat,
553                                   const KoListLevelProperties &llp)
554 {
555     addCommand(new ParagraphFormattingCommand(this, deltaCharFormat, deltaBlockFormat, llp));
556     emit textFormatChanged();
557 }
558 
blockCharFormat() const559 QTextCharFormat KoTextEditor::blockCharFormat() const
560 {
561     return d->caret.blockCharFormat();
562 }
563 
blockFormat() const564 QTextBlockFormat KoTextEditor::blockFormat() const
565 {
566      return d->caret.blockFormat();
567 }
568 
charFormat() const569 QTextCharFormat KoTextEditor::charFormat() const
570 {
571     return d->caret.charFormat();
572 }
573 
574 
mergeBlockFormat(const QTextBlockFormat & modifier)575 void KoTextEditor::mergeBlockFormat(const QTextBlockFormat &modifier)
576 {
577     if (isEditProtected()) {
578         return;
579     }
580     d->caret.mergeBlockFormat(modifier);
581     emit textFormatChanged();
582 }
583 
584 
setBlockFormat(const QTextBlockFormat & format)585 void KoTextEditor::setBlockFormat(const QTextBlockFormat &format)
586 {
587     if (isEditProtected()) {
588         return;
589     }
590 
591     Q_UNUSED(format)
592     d->caret.setBlockFormat(format);
593     emit textFormatChanged();
594 }
595 
setCharFormat(const QTextCharFormat & format)596 void KoTextEditor::setCharFormat(const QTextCharFormat &format)
597 {
598     if (isEditProtected()) {
599         return;
600     }
601 
602     d->caret.setCharFormat(format);
603     emit textFormatChanged();
604 }
605