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