1 /*
2   SPDX-FileCopyrightText: 1998 Preston Brown <pbrown@kde.org>
3   SPDX-FileCopyrightText: 2003 Reinhold Kainhofer <reinhold@kainhofer.com>
4   SPDX-FileCopyrightText: 2008 Ron Goodheart <rong.dev@gmail.com>
5   SPDX-FileCopyrightText: 2012-2013 Allen Winter <winter@kde.org>
6 
7   SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
8 */
9 
10 #include "calprintpluginbase.h"
11 #include "cellitem.h"
12 #include "kcalprefs.h"
13 #include "utils.h"
14 
15 #include <Akonadi/Item>
16 
17 #include "calendarsupport_debug.h"
18 #include <KConfig>
19 #include <KConfigGroup>
20 #include <KWordWrap>
21 
22 #include <KLocalizedString>
23 #include <QAbstractTextDocumentLayout>
24 #include <QFrame>
25 #include <QLabel>
26 #include <QLocale>
27 #include <QTextCursor>
28 #include <QTextDocument>
29 #include <QTextDocumentFragment>
30 #include <QTimeZone>
31 #include <QVBoxLayout>
32 #include <qmath.h> // qCeil krazy:exclude=camelcase since no QMath
33 
34 using namespace CalendarSupport;
35 
cleanStr(const QString & instr)36 static QString cleanStr(const QString &instr)
37 {
38     QString ret = instr;
39     return ret.replace(QLatin1Char('\n'), QLatin1Char(' '));
40 }
41 
42 const QColor CalPrintPluginBase::sHolidayBackground = QColor(244, 244, 244);
43 
44 /******************************************************************
45  **              The Todo positioning structure                  **
46  ******************************************************************/
47 class CalPrintPluginBase::TodoParentStart
48 {
49 public:
TodoParentStart(QRect pt=QRect (),bool hasLine=false,bool page=true)50     TodoParentStart(QRect pt = QRect(), bool hasLine = false, bool page = true)
51         : mRect(pt)
52         , mHasLine(hasLine)
53         , mSamePage(page)
54     {
55     }
56 
57     QRect mRect;
58     bool mHasLine;
59     bool mSamePage;
60 };
61 
62 /******************************************************************
63  **                     The Print item                           **
64  ******************************************************************/
65 
66 class PrintCellItem : public CellItem
67 {
68 public:
PrintCellItem(const KCalendarCore::Event::Ptr & event,const QDateTime & start,const QDateTime & end)69     PrintCellItem(const KCalendarCore::Event::Ptr &event, const QDateTime &start, const QDateTime &end)
70         : mEvent(event)
71         , mStart(start)
72         , mEnd(end)
73     {
74     }
75 
event() const76     KCalendarCore::Event::Ptr event() const
77     {
78         return mEvent;
79     }
80 
label() const81     QString label() const override
82     {
83         return mEvent->summary();
84     }
85 
start() const86     QDateTime start() const
87     {
88         return mStart;
89     }
90 
end() const91     QDateTime end() const
92     {
93         return mEnd;
94     }
95 
96     /** Calculate the start and end date/time of the recurrence that
97         happens on the given day */
overlaps(CellItem * o) const98     bool overlaps(CellItem *o) const override
99     {
100         auto other = static_cast<PrintCellItem *>(o);
101         return !(other->start() >= end() || other->end() <= start());
102     }
103 
104 private:
105     KCalendarCore::Event::Ptr mEvent;
106     QDateTime mStart, mEnd;
107 };
108 
109 /******************************************************************
110  **                    The Print plugin                          **
111  ******************************************************************/
112 
CalPrintPluginBase()113 CalPrintPluginBase::CalPrintPluginBase()
114     : PrintPlugin()
115     , mUseColors(true)
116     , mPrintFooter(true)
117     , mHeaderHeight(-1)
118     , mSubHeaderHeight(SUBHEADER_HEIGHT)
119     , mFooterHeight(-1)
120     , mMargin(MARGIN_SIZE)
121     , mPadding(PADDING_SIZE)
122 {
123 }
124 
~CalPrintPluginBase()125 CalPrintPluginBase::~CalPrintPluginBase()
126 {
127 }
128 
createConfigWidget(QWidget * w)129 QWidget *CalPrintPluginBase::createConfigWidget(QWidget *w)
130 {
131     auto wdg = new QFrame(w);
132     auto layout = new QVBoxLayout(wdg);
133 
134     auto title = new QLabel(description(), wdg);
135     QFont titleFont(title->font());
136     titleFont.setPointSize(20);
137     titleFont.setBold(true);
138     title->setFont(titleFont);
139 
140     layout->addWidget(title);
141     layout->addWidget(new QLabel(info(), wdg));
142     layout->addSpacing(20);
143     layout->addWidget(new QLabel(i18n("This printing style does not have any configuration options."), wdg));
144     layout->addStretch();
145     return wdg;
146 }
147 
doPrint(QPrinter * printer)148 void CalPrintPluginBase::doPrint(QPrinter *printer)
149 {
150     if (!printer) {
151         return;
152     }
153     mPrinter = printer;
154     QPainter p;
155 
156     mPrinter->setColorMode(mUseColors ? QPrinter::Color : QPrinter::GrayScale);
157 
158     p.begin(mPrinter);
159     // TODO: Fix the margins!!!
160     // the painter initially begins at 72 dpi per the Qt docs.
161     // we want half-inch margins.
162     int margins = margin();
163     p.setViewport(margins, margins, p.viewport().width() - 2 * margins, p.viewport().height() - 2 * margins);
164     //   QRect vp( p.viewport() );
165     // vp.setRight( vp.right()*2 );
166     // vp.setBottom( vp.bottom()*2 );
167     //   p.setWindow( vp );
168     int pageWidth = p.window().width();
169     int pageHeight = p.window().height();
170     //   int pageWidth = p.viewport().width();
171     //   int pageHeight = p.viewport().height();
172 
173     print(p, pageWidth, pageHeight);
174 
175     p.end();
176     mPrinter = nullptr;
177 }
178 
doLoadConfig()179 void CalPrintPluginBase::doLoadConfig()
180 {
181     if (mConfig) {
182         KConfigGroup group(mConfig, groupName());
183         mConfig->sync();
184         QDateTime dt = QDateTime::currentDateTime();
185         mFromDate = group.readEntry("FromDate", dt).date();
186         mToDate = group.readEntry("ToDate", dt).date();
187         mUseColors = group.readEntry("UseColors", true);
188         mPrintFooter = group.readEntry("PrintFooter", true);
189         mShowNoteLines = group.readEntry("Note Lines", false);
190         mExcludeConfidential = group.readEntry("Exclude confidential", true);
191         mExcludePrivate = group.readEntry("Exclude private", true);
192     } else {
193         qCDebug(CALENDARSUPPORT_LOG) << "No config available in loadConfig!!!!";
194     }
195 }
196 
doSaveConfig()197 void CalPrintPluginBase::doSaveConfig()
198 {
199     if (mConfig) {
200         KConfigGroup group(mConfig, groupName());
201         QDateTime dt = QDateTime::currentDateTime(); // any valid QDateTime will do
202         dt.setDate(mFromDate);
203         group.writeEntry("FromDate", dt);
204         dt.setDate(mToDate);
205         group.writeEntry("ToDate", dt);
206         group.writeEntry("UseColors", mUseColors);
207         group.writeEntry("PrintFooter", mPrintFooter);
208         group.writeEntry("Note Lines", mShowNoteLines);
209         group.writeEntry("Exclude confidential", mExcludeConfidential);
210         group.writeEntry("Exclude private", mExcludePrivate);
211         mConfig->sync();
212     } else {
213         qCDebug(CALENDARSUPPORT_LOG) << "No config available in saveConfig!!!!";
214     }
215 }
216 
useColors() const217 bool CalPrintPluginBase::useColors() const
218 {
219     return mUseColors;
220 }
221 
setUseColors(bool useColors)222 void CalPrintPluginBase::setUseColors(bool useColors)
223 {
224     mUseColors = useColors;
225 }
226 
printFooter() const227 bool CalPrintPluginBase::printFooter() const
228 {
229     return mPrintFooter;
230 }
231 
setPrintFooter(bool printFooter)232 void CalPrintPluginBase::setPrintFooter(bool printFooter)
233 {
234     mPrintFooter = printFooter;
235 }
236 
orientation() const237 QPageLayout::Orientation CalPrintPluginBase::orientation() const
238 {
239     return mPrinter ? mPrinter->pageLayout().orientation() : QPageLayout::Portrait;
240 }
241 
getTextColor(const QColor & c) const242 QColor CalPrintPluginBase::getTextColor(const QColor &c) const
243 {
244     double luminance = (c.red() * 0.299) + (c.green() * 0.587) + (c.blue() * 0.114);
245     return (luminance > 128.0) ? QColor(0, 0, 0) : QColor(255, 255, 255);
246 }
247 
dayStart() const248 QTime CalPrintPluginBase::dayStart() const
249 {
250     QTime start(8, 0, 0);
251     QDateTime dayBegins = KCalPrefs::instance()->dayBegins();
252     if (dayBegins.isValid()) {
253         start = dayBegins.time();
254     }
255     return start;
256 }
257 
setColorsByIncidenceCategory(QPainter & p,const KCalendarCore::Incidence::Ptr & incidence) const258 void CalPrintPluginBase::setColorsByIncidenceCategory(QPainter &p, const KCalendarCore::Incidence::Ptr &incidence) const
259 {
260     QColor bgColor = categoryBgColor(incidence);
261     if (bgColor.isValid()) {
262         p.setBrush(bgColor);
263     }
264     QColor tColor(getTextColor(bgColor));
265     if (tColor.isValid()) {
266         p.setPen(tColor);
267     }
268 }
269 
categoryColor(const QStringList & categories) const270 QColor CalPrintPluginBase::categoryColor(const QStringList &categories) const
271 {
272     if (categories.isEmpty()) {
273         return KCalPrefs::instance()->unsetCategoryColor();
274     }
275     // FIXME: Correctly treat events with multiple categories
276     const QString cat = categories.at(0);
277     QColor bgColor;
278     if (cat.isEmpty()) {
279         bgColor = KCalPrefs::instance()->unsetCategoryColor();
280     } else {
281         bgColor = KCalPrefs::instance()->categoryColor(cat);
282     }
283     return bgColor;
284 }
285 
categoryBgColor(const KCalendarCore::Incidence::Ptr & incidence) const286 QColor CalPrintPluginBase::categoryBgColor(const KCalendarCore::Incidence::Ptr &incidence) const
287 {
288     if (incidence) {
289         QColor backColor = categoryColor(incidence->categories());
290         if (incidence->type() == KCalendarCore::Incidence::TypeTodo) {
291             if ((incidence.staticCast<KCalendarCore::Todo>())->isOverdue()) {
292                 backColor = QColor(255, 100, 100); // was KOPrefs::instance()->todoOverdueColor();
293             }
294         }
295         return backColor;
296     } else {
297         return QColor();
298     }
299 }
300 
holidayString(QDate date) const301 QString CalPrintPluginBase::holidayString(QDate date) const
302 {
303     const QStringList lst = holiday(date);
304     return lst.join(i18nc("@item:intext delimiter for joining holiday names", ","));
305 }
306 
holidayEvent(QDate date) const307 KCalendarCore::Event::Ptr CalPrintPluginBase::holidayEvent(QDate date) const
308 {
309     QString hstring(holidayString(date));
310     if (hstring.isEmpty()) {
311         return KCalendarCore::Event::Ptr();
312     }
313 
314     KCalendarCore::Event::Ptr holiday(new KCalendarCore::Event);
315     holiday->setSummary(hstring);
316     holiday->setCategories(i18n("Holiday"));
317 
318     QDateTime kdt(date, QTime(0, 0), Qt::LocalTime);
319     holiday->setDtStart(kdt);
320     holiday->setDtEnd(kdt);
321     holiday->setAllDay(true);
322 
323     return holiday;
324 }
325 
headerHeight() const326 int CalPrintPluginBase::headerHeight() const
327 {
328     if (mHeaderHeight >= 0) {
329         return mHeaderHeight;
330     } else if (orientation() == QPageLayout::Portrait) {
331         return PORTRAIT_HEADER_HEIGHT;
332     } else {
333         return LANDSCAPE_HEADER_HEIGHT;
334     }
335 }
336 
setHeaderHeight(const int height)337 void CalPrintPluginBase::setHeaderHeight(const int height)
338 {
339     mHeaderHeight = height;
340 }
341 
subHeaderHeight() const342 int CalPrintPluginBase::subHeaderHeight() const
343 {
344     return mSubHeaderHeight;
345 }
346 
setSubHeaderHeight(const int height)347 void CalPrintPluginBase::setSubHeaderHeight(const int height)
348 {
349     mSubHeaderHeight = height;
350 }
351 
footerHeight() const352 int CalPrintPluginBase::footerHeight() const
353 {
354     if (!mPrintFooter) {
355         return 0;
356     }
357 
358     if (mFooterHeight >= 0) {
359         return mFooterHeight;
360     } else if (orientation() == QPageLayout::Portrait) {
361         return PORTRAIT_FOOTER_HEIGHT;
362     } else {
363         return LANDSCAPE_FOOTER_HEIGHT;
364     }
365 }
366 
setFooterHeight(const int height)367 void CalPrintPluginBase::setFooterHeight(const int height)
368 {
369     mFooterHeight = height;
370 }
371 
margin() const372 int CalPrintPluginBase::margin() const
373 {
374     return mMargin;
375 }
376 
setMargin(const int margin)377 void CalPrintPluginBase::setMargin(const int margin)
378 {
379     mMargin = margin;
380 }
381 
padding() const382 int CalPrintPluginBase::padding() const
383 {
384     return mPadding;
385 }
386 
setPadding(const int padding)387 void CalPrintPluginBase::setPadding(const int padding)
388 {
389     mPadding = padding;
390 }
391 
borderWidth() const392 int CalPrintPluginBase::borderWidth() const
393 {
394     return mBorder;
395 }
396 
setBorderWidth(const int borderwidth)397 void CalPrintPluginBase::setBorderWidth(const int borderwidth)
398 {
399     mBorder = borderwidth;
400 }
401 
drawBox(QPainter & p,int linewidth,QRect rect)402 void CalPrintPluginBase::drawBox(QPainter &p, int linewidth, QRect rect)
403 {
404     QPen pen(p.pen());
405     QPen oldpen(pen);
406     // no border
407     if (linewidth >= 0) {
408         pen.setWidth(linewidth);
409         p.setPen(pen);
410     } else {
411         p.setPen(Qt::NoPen);
412     }
413     p.drawRect(rect);
414     p.setPen(oldpen);
415 }
416 
drawShadedBox(QPainter & p,int linewidth,const QBrush & brush,QRect rect)417 void CalPrintPluginBase::drawShadedBox(QPainter &p, int linewidth, const QBrush &brush, QRect rect)
418 {
419     QBrush oldbrush(p.brush());
420     p.setBrush(brush);
421     drawBox(p, linewidth, rect);
422     p.setBrush(oldbrush);
423 }
424 
printEventString(QPainter & p,QRect box,const QString & str,int flags)425 void CalPrintPluginBase::printEventString(QPainter &p, QRect box, const QString &str, int flags)
426 {
427     QRect newbox(box);
428     newbox.adjust(3, 1, -1, -1);
429     p.drawText(newbox, (flags == -1) ? (Qt::AlignTop | Qt::AlignLeft | Qt::TextWordWrap) : flags, str);
430 }
431 
showEventBox(QPainter & p,int linewidth,QRect box,const KCalendarCore::Incidence::Ptr & incidence,const QString & str,int flags)432 void CalPrintPluginBase::showEventBox(QPainter &p, int linewidth, QRect box, const KCalendarCore::Incidence::Ptr &incidence, const QString &str, int flags)
433 {
434     QPen oldpen(p.pen());
435     QBrush oldbrush(p.brush());
436     QColor bgColor(categoryBgColor(incidence));
437     if (mUseColors && bgColor.isValid()) {
438         p.setBrush(bgColor);
439     } else {
440         p.setBrush(QColor(232, 232, 232));
441     }
442     drawBox(p, (linewidth > 0) ? linewidth : EVENT_BORDER_WIDTH, box);
443     if (mUseColors && bgColor.isValid()) {
444         p.setPen(getTextColor(bgColor));
445     }
446     printEventString(p, box, str, flags);
447     p.setPen(oldpen);
448     p.setBrush(oldbrush);
449 }
450 
drawSubHeaderBox(QPainter & p,const QString & str,QRect box)451 void CalPrintPluginBase::drawSubHeaderBox(QPainter &p, const QString &str, QRect box)
452 {
453     drawShadedBox(p, BOX_BORDER_WIDTH, QColor(232, 232, 232), box);
454     QFont oldfont(p.font());
455     p.setFont(QFont(QStringLiteral("sans-serif"), 10, QFont::Bold));
456     p.drawText(box, Qt::AlignHCenter | Qt::AlignTop, str);
457     p.setFont(oldfont);
458 }
459 
drawVerticalBox(QPainter & p,int linewidth,QRect box,const QString & str,int flags)460 void CalPrintPluginBase::drawVerticalBox(QPainter &p, int linewidth, QRect box, const QString &str, int flags)
461 {
462     p.save();
463     p.rotate(-90);
464     QRect rotatedBox(-box.top() - box.height(), box.left(), box.height(), box.width());
465     showEventBox(p, linewidth, rotatedBox, KCalendarCore::Incidence::Ptr(), str, (flags == -1) ? Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine : flags);
466 
467     p.restore();
468 }
469 
470 /*
471  * Return value: If expand, bottom of the printed box, otherwise vertical end
472  * of the printed contents inside the box.
473  */
drawBoxWithCaption(QPainter & p,QRect allbox,const QString & caption,const QString & contents,bool sameLine,bool expand,const QFont & captionFont,const QFont & textFont,bool richContents)474 int CalPrintPluginBase::drawBoxWithCaption(QPainter &p,
475                                            QRect allbox,
476                                            const QString &caption,
477                                            const QString &contents,
478                                            bool sameLine,
479                                            bool expand,
480                                            const QFont &captionFont,
481                                            const QFont &textFont,
482                                            bool richContents)
483 {
484     QFont oldFont(p.font());
485     //   QFont captionFont( "sans-serif", 11, QFont::Bold );
486     //   QFont textFont( "sans-serif", 11, QFont::Normal );
487     //   QFont captionFont( "Tahoma", 11, QFont::Bold );
488     //   QFont textFont( "Tahoma", 11, QFont::Normal );
489 
490     QRect box(allbox);
491 
492     // Bounding rectangle for caption, single-line, clip on the right
493     QRect captionBox(box.left() + padding(), box.top() + padding(), 0, 0);
494     p.setFont(captionFont);
495     captionBox = p.boundingRect(captionBox, Qt::AlignLeft | Qt::AlignTop | Qt::TextSingleLine, caption);
496     p.setFont(oldFont);
497     if (captionBox.right() > box.right()) {
498         captionBox.setRight(box.right());
499     }
500     if (expand && captionBox.bottom() + padding() > box.bottom()) {
501         box.setBottom(captionBox.bottom() + padding());
502     }
503 
504     // Bounding rectangle for the contents (if any), word break, clip on the bottom
505     QRect textBox(captionBox);
506     if (!contents.isEmpty()) {
507         if (sameLine) {
508             textBox.setLeft(captionBox.right() + padding());
509         } else {
510             textBox.setTop(captionBox.bottom() + padding());
511         }
512         textBox.setRight(box.right());
513     }
514     drawBox(p, BOX_BORDER_WIDTH, box);
515     p.setFont(captionFont);
516     p.drawText(captionBox, Qt::AlignLeft | Qt::AlignTop | Qt::TextSingleLine, caption);
517 
518     if (!contents.isEmpty()) {
519         if (sameLine) {
520             QString contentText = toPlainText(contents);
521             p.setFont(textFont);
522             p.drawText(textBox, Qt::AlignLeft | Qt::AlignTop | Qt::TextSingleLine, contentText);
523         } else {
524             QTextDocument rtb;
525             int borderWidth = 2 * BOX_BORDER_WIDTH;
526             if (richContents) {
527                 rtb.setHtml(contents);
528             } else {
529                 rtb.setPlainText(contents);
530             }
531             int boxHeight = allbox.height();
532             if (!sameLine) {
533                 boxHeight -= captionBox.height();
534             }
535             rtb.setPageSize(QSize(textBox.width(), boxHeight));
536             rtb.setDefaultFont(textFont);
537             p.save();
538             p.translate(textBox.x() - borderWidth, textBox.y());
539             QRect clipBox(0, 0, box.width(), boxHeight);
540             QAbstractTextDocumentLayout::PaintContext ctx;
541             ctx.palette.setColor(QPalette::Text, p.pen().color());
542             p.setClipRect(clipBox);
543             ctx.clip = clipBox;
544             rtb.documentLayout()->draw(&p, ctx);
545             p.restore();
546             textBox.setBottom(textBox.y() + rtb.documentLayout()->documentSize().height());
547         }
548     }
549     p.setFont(oldFont);
550 
551     if (expand) {
552         return box.bottom();
553     } else {
554         return textBox.bottom();
555     }
556 }
557 
drawHeader(QPainter & p,const QString & title,QDate month1,QDate month2,QRect allbox,bool expand,QColor backColor)558 int CalPrintPluginBase::drawHeader(QPainter &p, const QString &title, QDate month1, QDate month2, QRect allbox, bool expand, QColor backColor)
559 {
560     // print previous month for month view, print current for to-do, day and week
561     int smallMonthWidth = (allbox.width() / 4) - 10;
562     if (smallMonthWidth > 100) {
563         smallMonthWidth = 100;
564     }
565 
566     QRect box(allbox);
567     QRect textRect(allbox);
568 
569     QFont oldFont(p.font());
570     QFont newFont(QStringLiteral("sans-serif"), (textRect.height() < 60) ? 16 : 18, QFont::Bold);
571     if (expand) {
572         p.setFont(newFont);
573         QRect boundingR = p.boundingRect(textRect, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextWordWrap, title);
574         p.setFont(oldFont);
575         int h = boundingR.height();
576         if (h > allbox.height()) {
577             box.setHeight(h);
578             textRect.setHeight(h);
579         }
580     }
581 
582     if (!backColor.isValid()) {
583         backColor = QColor(232, 232, 232);
584     }
585 
586     drawShadedBox(p, BOX_BORDER_WIDTH, backColor, box);
587 
588     const auto oldPen {p.pen()};
589     p.setPen(getTextColor(backColor));
590 
591     // prev month left, current month centered, next month right
592     QRect monthbox2(box.right() - 10 - smallMonthWidth, box.top(), smallMonthWidth, box.height());
593     if (month2.isValid()) {
594         drawSmallMonth(p, QDate(month2.year(), month2.month(), 1), monthbox2);
595         textRect.setRight(monthbox2.left());
596     }
597     QRect monthbox1(box.left() + 10, box.top(), smallMonthWidth, box.height());
598     if (month1.isValid()) {
599         drawSmallMonth(p, QDate(month1.year(), month1.month(), 1), monthbox1);
600         textRect.setLeft(monthbox1.right());
601     }
602 
603     // Set the margins
604     p.setFont(newFont);
605     p.drawText(textRect, Qt::AlignCenter | Qt::AlignVCenter | Qt::TextWordWrap, title);
606 
607     p.setPen(oldPen);
608     p.setFont(oldFont);
609 
610     return textRect.bottom();
611 }
612 
drawFooter(QPainter & p,QRect footbox)613 int CalPrintPluginBase::drawFooter(QPainter &p, QRect footbox)
614 {
615     QFont oldfont(p.font());
616     p.setFont(QFont(QStringLiteral("sans-serif"), 6));
617     QString dateStr = QLocale::system().toString(QDateTime::currentDateTime(), QLocale::LongFormat);
618     p.drawText(footbox, Qt::AlignCenter | Qt::AlignVCenter | Qt::TextSingleLine, i18nc("print date: formatted-datetime", "printed: %1", dateStr));
619     p.setFont(oldfont);
620 
621     return footbox.bottom();
622 }
623 
drawSmallMonth(QPainter & p,QDate qd,QRect box)624 void CalPrintPluginBase::drawSmallMonth(QPainter &p, QDate qd, QRect box)
625 {
626     int weekdayCol = weekdayColumn(qd.dayOfWeek());
627     int month = qd.month();
628     QDate monthDate(QDate(qd.year(), qd.month(), 1));
629     // correct begin of week
630     QDate monthDate2(monthDate.addDays(-weekdayCol));
631 
632     double cellWidth = double(box.width()) / double(7);
633     int rownr = 3 + (qd.daysInMonth() + weekdayCol - 1) / 7;
634     // 3 Pixel after month name, 2 after day names, 1 after the calendar
635     double cellHeight = (box.height() - 5) / rownr;
636     QFont oldFont(p.font());
637     auto newFont = QFont(QStringLiteral("sans-serif"));
638     newFont.setPixelSize(cellHeight);
639     p.setFont(newFont);
640 
641     // draw the title
642     QRect titleBox(box);
643     titleBox.setHeight(p.fontMetrics().height());
644     p.drawText(titleBox, Qt::AlignTop | Qt::AlignHCenter, QLocale::system().monthName(month));
645 
646     // draw days of week
647     QRect wdayBox(box);
648     wdayBox.setTop(int(box.top() + 3 + cellHeight));
649     wdayBox.setHeight(int(2 * cellHeight) - int(cellHeight));
650 
651     for (int col = 0; col < 7; ++col) {
652         QString tmpStr = QLocale::system().dayName(monthDate2.dayOfWeek())[0].toUpper();
653         wdayBox.setLeft(int(box.left() + col * cellWidth));
654         wdayBox.setRight(int(box.left() + (col + 1) * cellWidth));
655         p.drawText(wdayBox, Qt::AlignCenter, tmpStr);
656         monthDate2 = monthDate2.addDays(1);
657     }
658 
659     // draw separator line
660     int calStartY = wdayBox.bottom() + 2;
661     p.drawLine(box.left(), calStartY, box.right(), calStartY);
662     monthDate = monthDate.addDays(-weekdayCol);
663 
664     for (int row = 0; row < (rownr - 2); row++) {
665         for (int col = 0; col < 7; col++) {
666             if (monthDate.month() == month) {
667                 QRect dayRect(int(box.left() + col * cellWidth), int(calStartY + row * cellHeight), 0, 0);
668                 dayRect.setRight(int(box.left() + (col + 1) * cellWidth));
669                 dayRect.setBottom(int(calStartY + (row + 1) * cellHeight));
670                 p.drawText(dayRect, Qt::AlignCenter, QString::number(monthDate.day()));
671             }
672             monthDate = monthDate.addDays(1);
673         }
674     }
675     p.setFont(oldFont);
676 }
677 
678 /*
679  * This routine draws a header box over the main part of the calendar
680  * containing the days of the week.
681  */
drawDaysOfWeek(QPainter & p,QDate fromDate,QDate toDate,QRect box)682 void CalPrintPluginBase::drawDaysOfWeek(QPainter &p, QDate fromDate, QDate toDate, QRect box)
683 {
684     double cellWidth = double(box.width() - 1) / double(fromDate.daysTo(toDate) + 1);
685     QDate cellDate(fromDate);
686     QRect dateBox(box);
687     int i = 0;
688 
689     while (cellDate <= toDate) {
690         dateBox.setLeft(box.left() + int(i * cellWidth));
691         dateBox.setRight(box.left() + int((i + 1) * cellWidth));
692         drawDaysOfWeekBox(p, cellDate, dateBox);
693         cellDate = cellDate.addDays(1);
694         ++i;
695     }
696 }
697 
drawDaysOfWeekBox(QPainter & p,QDate qd,QRect box)698 void CalPrintPluginBase::drawDaysOfWeekBox(QPainter &p, QDate qd, QRect box)
699 {
700     drawSubHeaderBox(p, QLocale::system().dayName(qd.dayOfWeek()), box);
701 }
702 
drawTimeLine(QPainter & p,QTime fromTime,QTime toTime,QRect box)703 void CalPrintPluginBase::drawTimeLine(QPainter &p, QTime fromTime, QTime toTime, QRect box)
704 {
705     drawBox(p, BOX_BORDER_WIDTH, box);
706 
707     int totalsecs = fromTime.secsTo(toTime);
708     float minlen = (float)box.height() * 60. / (float)totalsecs;
709     float cellHeight = (60. * (float)minlen);
710     float currY = box.top();
711     // TODO: Don't use half of the width, but less, for the minutes!
712     int xcenter = box.left() + box.width() / 2;
713 
714     QTime curTime(fromTime);
715     QTime endTime(toTime);
716     if (fromTime.minute() > 30) {
717         curTime = QTime(fromTime.hour() + 1, 0, 0);
718     } else if (fromTime.minute() > 0) {
719         curTime = QTime(fromTime.hour(), 30, 0);
720         float yy = currY + minlen * (float)fromTime.secsTo(curTime) / 60.;
721         p.drawLine(xcenter, (int)yy, box.right(), (int)yy);
722         curTime = QTime(fromTime.hour() + 1, 0, 0);
723     }
724     currY += (float(fromTime.secsTo(curTime) * minlen) / 60.);
725 
726     while (curTime < endTime) {
727         p.drawLine(box.left(), (int)currY, box.right(), (int)currY);
728         int newY = (int)(currY + cellHeight / 2.);
729         QString numStr;
730         if (newY < box.bottom()) {
731             QFont oldFont(p.font());
732             // draw the time:
733             if (!QLocale().timeFormat().contains(QLatin1String("AP"))) { // 12h clock
734                 p.drawLine(xcenter, (int)newY, box.right(), (int)newY);
735                 numStr.setNum(curTime.hour());
736                 if (cellHeight > 30) {
737                     p.setFont(QFont(QStringLiteral("sans-serif"), 14, QFont::Bold));
738                 } else {
739                     p.setFont(QFont(QStringLiteral("sans-serif"), 12, QFont::Bold));
740                 }
741                 p.drawText(box.left() + 4, (int)currY + 2, box.width() / 2 - 2, (int)cellHeight, Qt::AlignTop | Qt::AlignRight, numStr);
742                 p.setFont(QFont(QStringLiteral("helvetica"), 10, QFont::Normal));
743                 p.drawText(xcenter + 4, (int)currY + 2, box.width() / 2 + 2, (int)(cellHeight / 2) - 3, Qt::AlignTop | Qt::AlignLeft, QStringLiteral("00"));
744             } else {
745                 p.drawLine(box.left(), (int)newY, box.right(), (int)newY);
746                 QTime time(curTime.hour(), 0);
747                 numStr = QLocale::system().toString(time, QLocale::ShortFormat);
748                 if (box.width() < 60) {
749                     p.setFont(QFont(QStringLiteral("sans-serif"), 7, QFont::Bold)); // for weekprint
750                 } else {
751                     p.setFont(QFont(QStringLiteral("sans-serif"), 12, QFont::Bold)); // for dayprint
752                 }
753                 p.drawText(box.left() + 2, (int)currY + 2, box.width() - 4, (int)cellHeight / 2 - 3, Qt::AlignTop | Qt::AlignLeft, numStr);
754             }
755             currY += cellHeight;
756             p.setFont(oldFont);
757         } // enough space for half-hour line and time
758         if (curTime.secsTo(endTime) > 3600) {
759             curTime = curTime.addSecs(3600);
760         } else {
761             curTime = endTime;
762         }
763     }
764 }
765 
drawAgendaDayBox(QPainter & p,const KCalendarCore::Event::List & events,QDate qd,bool expandable,QTime fromTime,QTime toTime,QRect oldbox,bool includeDescription,bool includeCategories,bool excludeTime,const QList<QDate> & workDays)766 void CalPrintPluginBase::drawAgendaDayBox(QPainter &p,
767                                           const KCalendarCore::Event::List &events,
768                                           QDate qd,
769                                           bool expandable,
770                                           QTime fromTime,
771                                           QTime toTime,
772                                           QRect oldbox,
773                                           bool includeDescription,
774                                           bool includeCategories,
775                                           bool excludeTime,
776                                           const QList<QDate> &workDays)
777 {
778     QTime myFromTime;
779     QTime myToTime;
780     if (fromTime.isValid()) {
781         myFromTime = fromTime;
782     } else {
783         myFromTime = QTime(0, 0, 0);
784     }
785     if (toTime.isValid()) {
786         myToTime = toTime;
787     } else {
788         myToTime = QTime(23, 59, 59);
789     }
790 
791     if (!workDays.contains(qd)) {
792         drawShadedBox(p, BOX_BORDER_WIDTH, sHolidayBackground, oldbox);
793     } else {
794         drawBox(p, BOX_BORDER_WIDTH, oldbox);
795     }
796     QRect box(oldbox);
797     // Account for the border with and cut away that margin from the interior
798     //   box.setRight( box.right()-BOX_BORDER_WIDTH );
799 
800     if (expandable) {
801         // Adapt start/end times to include complete events
802         for (const KCalendarCore::Event::Ptr &event : std::as_const(events)) {
803             Q_ASSERT(event);
804             if (!event
805                 || (mExcludeConfidential && event->secrecy() == KCalendarCore::Incidence::SecrecyConfidential)
806                 || (mExcludePrivate && event->secrecy() == KCalendarCore::Incidence::SecrecyPrivate)) {
807                 continue;
808             }
809             // skip items without times so that we do not adjust for all day items
810             if (event->allDay()) {
811                 continue;
812             }
813             if (event->dtStart().time() < myFromTime) {
814                 myFromTime = event->dtStart().time();
815             }
816             if (event->dtEnd().time() > myToTime) {
817                 myToTime = event->dtEnd().time();
818             }
819         }
820     }
821 
822     // calculate the height of a cell and of a minute
823     int totalsecs = myFromTime.secsTo(myToTime);
824     float minlen = box.height() * 60. / totalsecs;
825     float cellHeight = 60. * minlen;
826     float currY = box.top();
827 
828     // print grid:
829     QTime curTime(QTime(myFromTime.hour(), 0, 0));
830     currY += myFromTime.secsTo(curTime) * minlen / 60;
831 
832     while (curTime < myToTime && curTime.isValid()) {
833         if (currY > box.top()) {
834             p.drawLine(box.left(), int(currY), box.right(), int(currY));
835         }
836         currY += cellHeight / 2;
837         if ((currY > box.top()) && (currY < box.bottom())) {
838             // enough space for half-hour line
839             QPen oldPen(p.pen());
840             p.setPen(QColor(192, 192, 192));
841             p.drawLine(box.left(), int(currY), box.right(), int(currY));
842             p.setPen(oldPen);
843         }
844         if (curTime.secsTo(myToTime) > 3600) {
845             curTime = curTime.addSecs(3600);
846         } else {
847             curTime = myToTime;
848         }
849         currY += cellHeight / 2;
850     }
851 
852     QDateTime startPrintDate = QDateTime(qd, myFromTime);
853     QDateTime endPrintDate = QDateTime(qd, myToTime);
854 
855     // Calculate horizontal positions and widths of events taking into account
856     // overlapping events
857 
858     QList<CellItem *> cells;
859 
860     for (const KCalendarCore::Event::Ptr &event : std::as_const(events)) {
861         if (!event
862             || (mExcludeConfidential && event->secrecy() == KCalendarCore::Incidence::SecrecyConfidential)
863             || (mExcludePrivate && event->secrecy() == KCalendarCore::Incidence::SecrecyPrivate)) {
864             continue;
865         }
866         if (event->allDay()) {
867             continue;
868         }
869         QList<QDateTime> times = event->startDateTimesForDate(qd, QTimeZone::systemTimeZone());
870         cells.reserve(times.count());
871         for (auto it = times.constBegin(); it != times.constEnd(); ++it) {
872             cells.append(new PrintCellItem(event, (*it).toLocalTime(), event->endDateForStart(*it).toLocalTime()));
873         }
874     }
875 
876     QListIterator<CellItem *> it1(cells);
877     while (it1.hasNext()) {
878         CellItem *placeItem = it1.next();
879         CellItem::placeItem(cells, placeItem);
880     }
881 
882     QListIterator<CellItem *> it2(cells);
883     while (it2.hasNext()) {
884         auto placeItem = static_cast<PrintCellItem *>(it2.next());
885         drawAgendaItem(placeItem, p, startPrintDate, endPrintDate, minlen, box, includeDescription, includeCategories, excludeTime);
886     }
887 }
888 
drawAgendaItem(PrintCellItem * item,QPainter & p,const QDateTime & startPrintDate,const QDateTime & endPrintDate,float minlen,QRect box,bool includeDescription,bool includeCategories,bool excludeTime)889 void CalPrintPluginBase::drawAgendaItem(PrintCellItem *item,
890                                         QPainter &p,
891                                         const QDateTime &startPrintDate,
892                                         const QDateTime &endPrintDate,
893                                         float minlen,
894                                         QRect box,
895                                         bool includeDescription,
896                                         bool includeCategories,
897                                         bool excludeTime)
898 {
899     KCalendarCore::Event::Ptr event = item->event();
900 
901     // start/end of print area for event
902     QDateTime startTime = item->start();
903     QDateTime endTime = item->end();
904     if ((startTime < endPrintDate && endTime > startPrintDate) || (endTime > startPrintDate && startTime < endPrintDate)) {
905         if (startTime < startPrintDate) {
906             startTime = startPrintDate;
907         }
908         if (endTime > endPrintDate) {
909             endTime = endPrintDate;
910         }
911         int currentWidth = box.width() / item->subCells();
912         int currentX = box.left() + item->subCell() * currentWidth;
913         int currentYPos = int(box.top() + startPrintDate.secsTo(startTime) * minlen / 60.);
914         int currentHeight = int(box.top() + startPrintDate.secsTo(endTime) * minlen / 60.) - currentYPos;
915 
916         QRect eventBox(currentX, currentYPos, currentWidth, currentHeight);
917         QString str;
918         if (excludeTime) {
919             if (event->location().isEmpty()) {
920                 str = cleanStr(event->summary());
921             } else {
922                 str = i18nc("summary, location", "%1, %2", cleanStr(event->summary()), cleanStr(event->location()));
923             }
924         } else {
925             if (event->location().isEmpty()) {
926                 str = i18nc("starttime - endtime summary",
927                             "%1-%2 %3",
928                             QLocale::system().toString(item->start().time(), QLocale::ShortFormat),
929                             QLocale::system().toString(item->end().time(), QLocale::ShortFormat),
930                             cleanStr(event->summary()));
931             } else {
932                 str = i18nc("starttime - endtime summary, location",
933                             "%1-%2 %3, %4",
934                             QLocale::system().toString(item->start().time(), QLocale::ShortFormat),
935                             QLocale::system().toString(item->end().time(), QLocale::ShortFormat),
936                             cleanStr(event->summary()),
937                             cleanStr(event->location()));
938             }
939         }
940         if (includeCategories && !event->categoriesStr().isEmpty()) {
941                 str = i18nc("summary, categories", "%1, %2", str, event->categoriesStr());
942         }
943         if (includeDescription && !event->description().isEmpty()) {
944             str += QLatin1Char('\n');
945             if (event->descriptionIsRich()) {
946                 str += toPlainText(event->description());
947             } else {
948                 str += event->description();
949             }
950         }
951         QFont oldFont(p.font());
952         if (eventBox.height() < 24) {
953             if (eventBox.height() < 12) {
954                 if (eventBox.height() < 8) {
955                     p.setFont(QFont(QStringLiteral("sans-serif"), 4));
956                 } else {
957                     p.setFont(QFont(QStringLiteral("sans-serif"), 5));
958                 }
959             } else {
960                 p.setFont(QFont(QStringLiteral("sans-serif"), 6));
961             }
962         } else {
963             p.setFont(QFont(QStringLiteral("sans-serif"), 8));
964         }
965         showEventBox(p, EVENT_BORDER_WIDTH, eventBox, event, str);
966         p.setFont(oldFont);
967     }
968 }
969 
drawDayBox(QPainter & p,QDate qd,QTime fromTime,QTime toTime,QRect box,bool fullDate,bool printRecurDaily,bool printRecurWeekly,bool singleLineLimit,bool includeDescription,bool includeCategories)970 void CalPrintPluginBase::drawDayBox(QPainter &p,
971                                     QDate qd,
972                                     QTime fromTime,
973                                     QTime toTime,
974                                     QRect box,
975                                     bool fullDate,
976                                     bool printRecurDaily,
977                                     bool printRecurWeekly,
978                                     bool singleLineLimit,
979                                     bool includeDescription,
980                                     bool includeCategories)
981 {
982     QString dayNumStr;
983     const auto local = QLocale::system();
984 
985     QTime myFromTime;
986     QTime myToTime;
987     if (fromTime.isValid()) {
988         myFromTime = fromTime;
989     } else {
990         myFromTime = QTime(0, 0, 0);
991     }
992     if (toTime.isValid()) {
993         myToTime = toTime;
994     } else {
995         myToTime = QTime(23, 59, 59);
996     }
997 
998     if (fullDate) {
999         dayNumStr = i18nc("weekday, shortmonthname daynumber",
1000                           "%1, %2 %3",
1001                           QLocale::system().dayName(qd.dayOfWeek()),
1002                           QLocale::system().monthName(qd.month(), QLocale::ShortFormat),
1003                           QString::number(qd.day()));
1004     } else {
1005         dayNumStr = QString::number(qd.day());
1006     }
1007 
1008     QRect subHeaderBox(box);
1009     subHeaderBox.setHeight(mSubHeaderHeight);
1010     drawShadedBox(p, BOX_BORDER_WIDTH, p.background(), box);
1011     drawShadedBox(p, 0, QColor(232, 232, 232), subHeaderBox);
1012     drawBox(p, BOX_BORDER_WIDTH, box);
1013     QString hstring(holidayString(qd));
1014     const QFont oldFont(p.font());
1015 
1016     QRect headerTextBox(subHeaderBox.adjusted(5, 0, -5, 0));
1017     p.setFont(QFont(QStringLiteral("sans-serif"), 10, QFont::Bold));
1018     QRect dayNumRect;
1019     p.drawText(headerTextBox, Qt::AlignRight | Qt::AlignVCenter, dayNumStr, &dayNumRect);
1020     if (!hstring.isEmpty()) {
1021         p.setFont(QFont(QStringLiteral("sans-serif"), 8, QFont::Bold, true));
1022         QFontMetrics fm(p.font());
1023         hstring = fm.elidedText(hstring, Qt::ElideRight, headerTextBox.width() - dayNumRect.width() - 5);
1024         p.drawText(headerTextBox, Qt::AlignLeft | Qt::AlignVCenter, hstring);
1025         p.setFont(QFont(QStringLiteral("sans-serif"), 10, QFont::Bold));
1026     }
1027 
1028     const KCalendarCore::Event::List eventList =
1029         mCalendar->events(qd, QTimeZone::systemTimeZone(), KCalendarCore::EventSortStartDate, KCalendarCore::SortDirectionAscending);
1030 
1031     QString timeText;
1032     p.setFont(QFont(QStringLiteral("sans-serif"), 7));
1033 
1034     int textY = mSubHeaderHeight; // gives the relative y-coord of the next printed entry
1035     unsigned int visibleEventsCounter = 0;
1036     for (const KCalendarCore::Event::Ptr &currEvent : std::as_const(eventList)) {
1037         Q_ASSERT(currEvent);
1038         if (!currEvent->allDay()) {
1039             if (currEvent->dtEnd().toLocalTime().time() <= myFromTime || currEvent->dtStart().toLocalTime().time() > myToTime) {
1040                 continue;
1041             }
1042         }
1043         if ((!printRecurDaily && currEvent->recurrenceType() == KCalendarCore::Recurrence::rDaily)
1044             || (!printRecurWeekly && currEvent->recurrenceType() == KCalendarCore::Recurrence::rWeekly)) {
1045             continue;
1046         }
1047         if ((mExcludeConfidential && currEvent->secrecy() == KCalendarCore::Incidence::SecrecyConfidential)
1048             || (mExcludePrivate && currEvent->secrecy() == KCalendarCore::Incidence::SecrecyPrivate)) {
1049             continue;
1050         }
1051         if (currEvent->allDay() || currEvent->isMultiDay()) {
1052             timeText.clear();
1053         } else {
1054             timeText = local.toString(currEvent->dtStart().toLocalTime().time(), QLocale::ShortFormat) + QLatin1Char(' ');
1055         }
1056         p.save();
1057         if (mUseColors) {
1058             setColorsByIncidenceCategory(p, currEvent);
1059         }
1060         QString summaryStr = currEvent->summary();
1061         if (!currEvent->location().isEmpty()) {
1062             summaryStr = i18nc("summary, location", "%1, %2", summaryStr, currEvent->location());
1063         }
1064         if (includeCategories && !currEvent->categoriesStr().isEmpty()) {
1065             summaryStr = i18nc("summary, categories", "%1, %2", summaryStr, currEvent->categoriesStr());
1066         }
1067         drawIncidence(p, box, timeText, summaryStr, currEvent->description(), textY, singleLineLimit, includeDescription, currEvent->descriptionIsRich());
1068         p.restore();
1069         visibleEventsCounter++;
1070 
1071         if (textY >= box.height()) {
1072             const QChar downArrow(0x21e3);
1073 
1074             const unsigned int invisibleIncidences = (eventList.count() - visibleEventsCounter) + mCalendar->todos(qd).count();
1075             if (invisibleIncidences > 0) {
1076                 const QString warningMsg = QStringLiteral("%1 (%2)").arg(downArrow).arg(invisibleIncidences);
1077 
1078                 QFontMetrics fm(p.font());
1079                 QRect msgRect = fm.boundingRect(warningMsg);
1080                 msgRect.setRect(box.right() - msgRect.width() - 2, box.bottom() - msgRect.height() - 2, msgRect.width(), msgRect.height());
1081 
1082                 p.save();
1083                 p.setPen(Qt::red); // krazy:exclude=qenums we don't allow custom print colors
1084                 p.drawText(msgRect, Qt::AlignLeft, warningMsg);
1085                 p.restore();
1086             }
1087             break;
1088         }
1089     }
1090 
1091     if (textY < box.height()) {
1092         KCalendarCore::Todo::List todos = mCalendar->todos(qd);
1093         for (const KCalendarCore::Todo::Ptr &todo : std::as_const(todos)) {
1094             if (!todo->allDay()) {
1095                 if ((todo->hasDueDate() && todo->dtDue().toLocalTime().time() <= myFromTime)
1096                     || (todo->hasStartDate() && todo->dtStart().toLocalTime().time() > myToTime)) {
1097                     continue;
1098                 }
1099             }
1100             if ((!printRecurDaily && todo->recurrenceType() == KCalendarCore::Recurrence::rDaily)
1101                 || (!printRecurWeekly && todo->recurrenceType() == KCalendarCore::Recurrence::rWeekly)) {
1102                 continue;
1103             }
1104             if ((mExcludeConfidential && todo->secrecy() == KCalendarCore::Incidence::SecrecyConfidential)
1105                 || (mExcludePrivate && todo->secrecy() == KCalendarCore::Incidence::SecrecyPrivate)) {
1106                 continue;
1107             }
1108             if (todo->hasStartDate() && !todo->allDay()) {
1109                 timeText = QLocale().toString(todo->dtStart().toLocalTime().time(), QLocale::ShortFormat) + QLatin1Char(' ');
1110             } else {
1111                 timeText.clear();
1112             }
1113             p.save();
1114             if (mUseColors) {
1115                 setColorsByIncidenceCategory(p, todo);
1116             }
1117             QString summaryStr = todo->summary();
1118             if (!todo->location().isEmpty()) {
1119                 summaryStr = i18nc("summary, location", "%1, %2", summaryStr, todo->location());
1120             }
1121 
1122             QString str;
1123             if (todo->hasDueDate()) {
1124                 if (!todo->allDay()) {
1125                     str = i18nc("to-do summary (Due: datetime)",
1126                                 "%1 (Due: %2)",
1127                                 summaryStr,
1128                                 QLocale().toString(todo->dtDue().toLocalTime(), QLocale::ShortFormat));
1129                 } else {
1130                     str = i18nc("to-do summary (Due: date)",
1131                                 "%1 (Due: %2)",
1132                                 summaryStr,
1133                                 QLocale().toString(todo->dtDue().toLocalTime().date(), QLocale::ShortFormat));
1134                 }
1135             } else {
1136                 str = summaryStr;
1137             }
1138             drawIncidence(p, box, timeText, i18n("To-do: %1", str), todo->description(), textY, singleLineLimit, includeDescription, todo->descriptionIsRich());
1139             p.restore();
1140         }
1141     }
1142     if (mShowNoteLines) {
1143         drawNoteLines(p, box, box.y() + textY);
1144     }
1145 
1146     p.setFont(oldFont);
1147 }
1148 
drawIncidence(QPainter & p,QRect dayBox,const QString & time,const QString & summary,const QString & description,int & textY,bool singleLineLimit,bool includeDescription,bool richDescription)1149 void CalPrintPluginBase::drawIncidence(QPainter &p,
1150                                        QRect dayBox,
1151                                        const QString &time,
1152                                        const QString &summary,
1153                                        const QString &description,
1154                                        int &textY,
1155                                        bool singleLineLimit,
1156                                        bool includeDescription,
1157                                        bool richDescription)
1158 {
1159     qCDebug(CALENDARSUPPORT_LOG) << "summary =" << summary << ", singleLineLimit=" << singleLineLimit;
1160 
1161     int flags = Qt::AlignLeft | Qt::OpaqueMode;
1162     QFontMetrics fm = p.fontMetrics();
1163     const int borderWidth = p.pen().width() + 1;
1164 
1165     QString firstLine {time};
1166     if (!firstLine.isEmpty()) {
1167         firstLine += QStringLiteral(" ");
1168     }
1169     firstLine += summary;
1170 
1171     if (singleLineLimit) {
1172         if (includeDescription && !description.isEmpty()) {
1173             firstLine += QStringLiteral(". ") + toPlainText(description);
1174         }
1175 
1176         int totalHeight = fm.height() + borderWidth;
1177         int textBoxHeight = (totalHeight > (dayBox.height() - textY)) ? dayBox.height() - textY : totalHeight;
1178         QRect boxRect(dayBox.x() + p.pen().width(), dayBox.y() + textY, dayBox.width(), textBoxHeight);
1179         drawBox(p, 1, boxRect);
1180         p.drawText(boxRect.adjusted(3, 0, -3, 0), flags, firstLine);
1181         textY += textBoxHeight;
1182     } else {
1183         QTextDocument textDoc;
1184         QTextCursor textCursor(&textDoc);
1185         textCursor.insertText(firstLine);
1186         if (includeDescription && !description.isEmpty()) {
1187             textCursor.insertText(QStringLiteral("\n"));
1188             if (richDescription) {
1189                 textCursor.insertHtml(description);
1190             } else {
1191                 textCursor.insertText(toPlainText(description));
1192             }
1193         }
1194 
1195         QRect textBox = QRect(dayBox.x(), dayBox.y() + textY + 1, dayBox.width(), dayBox.height() - textY);
1196         textDoc.setPageSize(QSize(textBox.width(), textBox.height()));
1197 
1198         textBox.setHeight(textDoc.documentLayout()->documentSize().height());
1199         if (textBox.bottom() > dayBox.bottom()) {
1200             textBox.setBottom(dayBox.bottom());
1201         }
1202 
1203         QRect boxRext(dayBox.x() + p.pen().width(), dayBox.y() + textY, dayBox.width(), textBox.height());
1204         drawBox(p, 1, boxRext);
1205 
1206         QRect clipRect(0, 0, textBox.width(), textBox.height());
1207         QAbstractTextDocumentLayout::PaintContext ctx;
1208         ctx.palette.setColor(QPalette::Text, p.pen().color());
1209         ctx.clip = clipRect;
1210         p.save();
1211         p.translate(textBox.x(), textBox.y());
1212         p.setClipRect(clipRect);
1213         textDoc.documentLayout()->draw(&p, ctx);
1214         p.restore();
1215 
1216         textY += textBox.height();
1217 
1218         if (textDoc.pageCount() > 1) {
1219             // show that we have overflowed the box
1220             QPolygon poly(3);
1221             int x = dayBox.x() + dayBox.width();
1222             int y = dayBox.y() + dayBox.height();
1223             poly.setPoint(0, x - 10, y);
1224             poly.setPoint(1, x, y - 10);
1225             poly.setPoint(2, x, y);
1226             QBrush oldBrush(p.brush());
1227             p.setBrush(QBrush(Qt::black));
1228             p.drawPolygon(poly);
1229             p.setBrush(oldBrush);
1230             textY = dayBox.height();
1231         }
1232     }
1233 }
1234 
1235 class MonthEventStruct
1236 {
1237 public:
MonthEventStruct()1238     MonthEventStruct()
1239         : event(nullptr)
1240     {
1241     }
1242 
MonthEventStruct(const QDateTime & s,const QDateTime & e,const KCalendarCore::Event::Ptr & ev)1243     MonthEventStruct(const QDateTime &s, const QDateTime &e, const KCalendarCore::Event::Ptr &ev)
1244     {
1245         event = ev;
1246         start = s;
1247         end = e;
1248         if (event->allDay()) {
1249             start = QDateTime(start.date(), QTime(0, 0, 0));
1250             end = QDateTime(end.date().addDays(1), QTime(0, 0, 0)).addSecs(-1);
1251         }
1252     }
1253 
operator <(const MonthEventStruct & mes)1254     bool operator<(const MonthEventStruct &mes)
1255     {
1256         return start < mes.start;
1257     }
1258 
1259     QDateTime start;
1260     QDateTime end;
1261     KCalendarCore::Event::Ptr event;
1262 };
1263 
drawMonth(QPainter & p,QDate dt,QRect box,int maxdays,int subDailyFlags,int holidaysFlags)1264 void CalPrintPluginBase::drawMonth(QPainter &p,
1265                                    QDate dt,
1266                                    QRect box,
1267                                    int maxdays,
1268                                    int subDailyFlags,
1269                                    int holidaysFlags)
1270 {
1271     p.save();
1272     QRect subheaderBox(box);
1273     subheaderBox.setHeight(subHeaderHeight());
1274     QRect borderBox(box);
1275     borderBox.setTop(subheaderBox.bottom() + 1);
1276     drawSubHeaderBox(p, QLocale::system().monthName(dt.month()), subheaderBox);
1277     // correct for half the border width
1278     int correction = (BOX_BORDER_WIDTH /*-1*/) / 2;
1279     QRect daysBox(borderBox);
1280     daysBox.adjust(correction, correction, -correction, -correction);
1281 
1282     int daysinmonth = dt.daysInMonth();
1283     if (maxdays <= 0) {
1284         maxdays = daysinmonth;
1285     }
1286 
1287     float dayheight = float(daysBox.height()) / float(maxdays);
1288 
1289     QColor holidayColor(240, 240, 240);
1290     QColor workdayColor(255, 255, 255);
1291     int dayNrWidth = p.fontMetrics().boundingRect(QStringLiteral("99")).width();
1292 
1293     // Fill the remaining space (if a month has less days than others) with a crossed-out pattern
1294     if (daysinmonth < maxdays) {
1295         QRect dayBox(box.left(), daysBox.top() + qRound(dayheight * daysinmonth), box.width(), 0);
1296         dayBox.setBottom(daysBox.bottom());
1297         p.fillRect(dayBox, Qt::DiagCrossPattern);
1298     }
1299     // Backgrounded boxes for each day, plus day numbers
1300     QBrush oldbrush(p.brush());
1301 
1302     QList<QDate> workDays;
1303 
1304     {
1305         QDate startDate(dt.year(), dt.month(), 1);
1306         QDate endDate(dt.year(), dt.month(), daysinmonth);
1307 
1308         workDays = CalendarSupport::workDays(startDate, endDate);
1309     }
1310 
1311     for (int d = 0; d < daysinmonth; ++d) {
1312         QDate day(dt.year(), dt.month(), d + 1);
1313         QRect dayBox(daysBox.left() /*+rand()%50*/, daysBox.top() + qRound(dayheight * d), daysBox.width() /*-rand()%50*/, 0);
1314         // FIXME: When using a border width of 0 for event boxes,
1315         // don't let the rectangles overlap, i.e. subtract 1 from the top or bottom!
1316         dayBox.setBottom(daysBox.top() + qRound(dayheight * (d + 1)) - 1);
1317 
1318         p.setBrush(workDays.contains(day) ? workdayColor : holidayColor);
1319         p.drawRect(dayBox);
1320         QRect dateBox(dayBox);
1321         dateBox.setWidth(dayNrWidth + 3);
1322         p.drawText(dateBox, Qt::AlignRight | Qt::AlignVCenter | Qt::TextSingleLine, QString::number(d + 1));
1323     }
1324     p.setBrush(oldbrush);
1325     int xstartcont = box.left() + dayNrWidth + 5;
1326 
1327     QDate start(dt.year(), dt.month(), 1);
1328     QDate end = start.addMonths(1);
1329     end = end.addDays(-1);
1330 
1331     const KCalendarCore::Event::List events = mCalendar->events(start, end);
1332     QMap<int, QStringList> textEvents;
1333     QList<CellItem *> timeboxItems;
1334 
1335     // 1) For multi-day events, show boxes spanning several cells, use CellItem
1336     //    print the summary vertically
1337     // 2) For sub-day events, print the concated summaries into the remaining
1338     //    space of the box (optional, depending on the given flags)
1339     // 3) Draw some kind of timeline showing free and busy times
1340 
1341     // Holidays
1342     // QList<KCalendarCore::Event::Ptr> holidays;
1343     for (QDate d(start); d <= end; d = d.addDays(1)) {
1344         KCalendarCore::Event::Ptr e = holidayEvent(d);
1345         if (e) {
1346             // holidays.append(e);
1347             if (holidaysFlags & TimeBoxes) {
1348                 timeboxItems.append(new PrintCellItem(e, QDateTime(d, QTime(0, 0, 0)), QDateTime(d.addDays(1), QTime(0, 0, 0))));
1349             }
1350             if (holidaysFlags & Text) {
1351                 textEvents[d.day()] << e->summary();
1352             }
1353         }
1354     }
1355 
1356     QVector<MonthEventStruct> monthentries;
1357 
1358     for (const KCalendarCore::Event::Ptr &e : std::as_const(events)) {
1359         if (!e
1360             || (mExcludeConfidential && e->secrecy() == KCalendarCore::Incidence::SecrecyConfidential)
1361             || (mExcludePrivate && e->secrecy() == KCalendarCore::Incidence::SecrecyPrivate)) {
1362             continue;
1363         }
1364         if (e->recurs()) {
1365             if (e->recursOn(start, QTimeZone::systemTimeZone())) {
1366                 // This occurrence has possibly started before the beginning of the
1367                 // month, so obtain the start date before the beginning of the month
1368                 QList<QDateTime> starttimes = e->startDateTimesForDate(start, QTimeZone::systemTimeZone());
1369                 for (auto it = starttimes.constBegin(); it != starttimes.constEnd(); ++it) {
1370                     monthentries.append(MonthEventStruct((*it).toLocalTime(), e->endDateForStart(*it).toLocalTime(), e));
1371                 }
1372             }
1373             // Loop through all remaining days of the month and check if the event
1374             // begins on that day (don't use Event::recursOn, as that will
1375             // also return events that have started earlier. These start dates
1376             // however, have already been treated!
1377             KCalendarCore::Recurrence *recur = e->recurrence();
1378             QDate d1(start.addDays(1));
1379             while (d1 <= end) {
1380                 if (recur->recursOn(d1, QTimeZone::systemTimeZone())) {
1381                     KCalendarCore::TimeList times(recur->recurTimesOn(d1, QTimeZone::systemTimeZone()));
1382                     for (KCalendarCore::TimeList::ConstIterator it = times.constBegin(); it != times.constEnd(); ++it) {
1383                         QDateTime d1start(d1, *it, Qt::LocalTime);
1384                         monthentries.append(MonthEventStruct(d1start, e->endDateForStart(d1start).toLocalTime(), e));
1385                     }
1386                 }
1387                 d1 = d1.addDays(1);
1388             }
1389         } else {
1390             monthentries.append(MonthEventStruct(e->dtStart().toLocalTime(), e->dtEnd().toLocalTime(), e));
1391         }
1392     }
1393 
1394     // TODO: to port the month entries sorting
1395 
1396     //  qSort( monthentries.begin(), monthentries.end() );
1397 
1398     QVector<MonthEventStruct>::ConstIterator mit = monthentries.constBegin();
1399     QDateTime endofmonth(end, QTime(0, 0, 0));
1400     endofmonth = endofmonth.addDays(1);
1401     for (; mit != monthentries.constEnd(); ++mit) {
1402         if ((*mit).start.date() == (*mit).end.date()) {
1403             // Show also single-day events as time line boxes
1404             if (subDailyFlags & TimeBoxes) {
1405                 timeboxItems.append(new PrintCellItem((*mit).event, (*mit).start, (*mit).end));
1406             }
1407             // Show as text in the box
1408             if (subDailyFlags & Text) {
1409                 textEvents[(*mit).start.date().day()] << (*mit).event->summary();
1410             }
1411         } else {
1412             // Multi-day events are always shown as time line boxes
1413             QDateTime thisstart((*mit).start);
1414             QDateTime thisend((*mit).end);
1415             if (thisstart.date() < start) {
1416                 thisstart.setDate(start);
1417             }
1418             if (thisend > endofmonth) {
1419                 thisend = endofmonth;
1420             }
1421             timeboxItems.append(new PrintCellItem((*mit).event, thisstart, thisend));
1422         }
1423     }
1424 
1425     // For Multi-day events, line them up nicely so that the boxes don't overlap
1426     QListIterator<CellItem *> it1(timeboxItems);
1427     while (it1.hasNext()) {
1428         CellItem *placeItem = it1.next();
1429         CellItem::placeItem(timeboxItems, placeItem);
1430     }
1431     QDateTime starttime(start, QTime(0, 0, 0));
1432     int newxstartcont = xstartcont;
1433 
1434     QFont oldfont(p.font());
1435     p.setFont(QFont(QStringLiteral("sans-serif"), 7));
1436     while (it1.hasNext()) {
1437         auto placeItem = static_cast<PrintCellItem *>(it1.next());
1438         int minsToStart = starttime.secsTo(placeItem->start()) / 60;
1439         int minsToEnd = starttime.secsTo(placeItem->end()) / 60;
1440 
1441         QRect eventBox(xstartcont + placeItem->subCell() * 17,
1442                        daysBox.top() + qRound(double(minsToStart * daysBox.height()) / double(maxdays * 24 * 60)),
1443                        14,
1444                        0);
1445         eventBox.setBottom(daysBox.top() + qRound(double(minsToEnd * daysBox.height()) / double(maxdays * 24 * 60)));
1446         drawVerticalBox(p, 0, eventBox, placeItem->event()->summary());
1447         newxstartcont = qMax(newxstartcont, eventBox.right());
1448     }
1449     xstartcont = newxstartcont;
1450 
1451     // For Single-day events, simply print their summaries into the remaining
1452     // space of the day's cell
1453     for (int d = 0; d < daysinmonth; ++d) {
1454         QStringList dayEvents(textEvents[d + 1]);
1455         QString txt = dayEvents.join(QLatin1String(", "));
1456         QRect dayBox(xstartcont, daysBox.top() + qRound(dayheight * d), 0, 0);
1457         dayBox.setRight(box.right());
1458         dayBox.setBottom(daysBox.top() + qRound(dayheight * (d + 1)));
1459         printEventString(p, dayBox, txt, Qt::AlignTop | Qt::AlignLeft | Qt::TextWrapAnywhere);
1460     }
1461     p.setFont(oldfont);
1462     drawBox(p, BOX_BORDER_WIDTH, borderBox);
1463     p.restore();
1464 }
1465 
drawMonthTable(QPainter & p,QDate qd,QTime fromTime,QTime toTime,bool weeknumbers,bool recurDaily,bool recurWeekly,bool singleLineLimit,bool includeDescription,bool includeCategories,QRect box)1466 void CalPrintPluginBase::drawMonthTable(QPainter &p,
1467                                         QDate qd,
1468                                         QTime fromTime,
1469                                         QTime toTime,
1470                                         bool weeknumbers,
1471                                         bool recurDaily,
1472                                         bool recurWeekly,
1473                                         bool singleLineLimit,
1474                                         bool includeDescription,
1475                                         bool includeCategories,
1476                                         QRect box)
1477 {
1478     int yoffset = mSubHeaderHeight;
1479     int xoffset = 0;
1480     QDate monthDate(QDate(qd.year(), qd.month(), 1));
1481     QDate monthFirst(monthDate);
1482     QDate monthLast(monthDate.addMonths(1).addDays(-1));
1483 
1484     int weekdayCol = weekdayColumn(monthDate.dayOfWeek());
1485     monthDate = monthDate.addDays(-weekdayCol);
1486 
1487     if (weeknumbers) {
1488         xoffset += 14;
1489     }
1490 
1491     int rows = (weekdayCol + qd.daysInMonth() - 1) / 7 + 1;
1492     double cellHeight = (box.height() - yoffset) / (1. * rows);
1493     double cellWidth = (box.width() - xoffset) / 7.;
1494 
1495     // Precalculate the grid...
1496     // rows is at most 6, so using 8 entries in the array is fine, too!
1497     int coledges[8];
1498     int rowedges[8];
1499     for (int i = 0; i <= 7; ++i) {
1500         rowedges[i] = int(box.top() + yoffset + i * cellHeight);
1501         coledges[i] = int(box.left() + xoffset + i * cellWidth);
1502     }
1503 
1504     if (weeknumbers) {
1505         QFont oldFont(p.font());
1506         QFont newFont(p.font());
1507         newFont.setPointSize(6);
1508         p.setFont(newFont);
1509         QDate weekDate(monthDate);
1510         for (int row = 0; row < rows; ++row) {
1511             int calWeek = weekDate.weekNumber();
1512             QRect rc(box.left(), rowedges[row], coledges[0] - 3 - box.left(), rowedges[row + 1] - rowedges[row]);
1513             p.drawText(rc, Qt::AlignRight | Qt::AlignVCenter, QString::number(calWeek));
1514             weekDate = weekDate.addDays(7);
1515         }
1516         p.setFont(oldFont);
1517     }
1518 
1519     QRect daysOfWeekBox(box);
1520     daysOfWeekBox.setHeight(mSubHeaderHeight);
1521     daysOfWeekBox.setLeft(box.left() + xoffset);
1522     drawDaysOfWeek(p, monthDate, monthDate.addDays(6), daysOfWeekBox);
1523 
1524     QColor back = p.background().color();
1525     bool darkbg = false;
1526     for (int row = 0; row < rows; ++row) {
1527         for (int col = 0; col < 7; ++col) {
1528             // show days from previous/next month with a grayed background
1529             if ((monthDate < monthFirst) || (monthDate > monthLast)) {
1530                 p.setBackground(back.darker(120));
1531                 darkbg = true;
1532             }
1533             QRect dayBox(coledges[col], rowedges[row], coledges[col + 1] - coledges[col], rowedges[row + 1] - rowedges[row]);
1534             drawDayBox(p,
1535                        monthDate,
1536                        fromTime,
1537                        toTime,
1538                        dayBox,
1539                        false,
1540                        recurDaily,
1541                        recurWeekly,
1542                        singleLineLimit,
1543                        includeDescription,
1544                        includeCategories);
1545             if (darkbg) {
1546                 p.setBackground(back);
1547                 darkbg = false;
1548             }
1549             monthDate = monthDate.addDays(1);
1550         }
1551     }
1552 }
1553 
drawTodoLines(QPainter & p,const QString & entry,int x,int & y,int width,int pageHeight,bool richTextEntry,QList<TodoParentStart * > & startPoints,bool connectSubTodos)1554 void CalPrintPluginBase::drawTodoLines(QPainter &p,
1555                                        const QString &entry,
1556                                        int x,
1557                                        int &y,
1558                                        int width,
1559                                        int pageHeight,
1560                                        bool richTextEntry,
1561                                        QList<TodoParentStart *> &startPoints,
1562                                        bool connectSubTodos)
1563 {
1564     QString plainEntry = (richTextEntry) ? toPlainText(entry) : entry;
1565 
1566     QRect textrect(0, 0, width, -1);
1567     int flags = Qt::AlignLeft;
1568     QFontMetrics fm = p.fontMetrics();
1569 
1570     QStringList lines = plainEntry.split(QLatin1Char('\n'));
1571     for (int currentLine = 0; currentLine < lines.count(); currentLine++) {
1572         // split paragraphs into lines
1573         KWordWrap ww = KWordWrap::formatText(fm, textrect, flags, lines[currentLine]);
1574         QStringList textLine = ww.wrappedString().split(QLatin1Char('\n'));
1575 
1576         // print each individual line
1577         for (int lineCount = 0; lineCount < textLine.count(); lineCount++) {
1578             if (y >= pageHeight) {
1579                 if (connectSubTodos) {
1580                     for (int i = 0; i < startPoints.size(); ++i) {
1581                         TodoParentStart *rct;
1582                         rct = startPoints.at(i);
1583                         int start = rct->mRect.bottom() + 1;
1584                         int center = rct->mRect.left() + (rct->mRect.width() / 2);
1585                         int to = y;
1586                         if (!rct->mSamePage) {
1587                             start = 0;
1588                         }
1589                         if (rct->mHasLine) {
1590                             p.drawLine(center, start, center, to);
1591                         }
1592                         rct->mSamePage = false;
1593                     }
1594                 }
1595                 y = 0;
1596                 mPrinter->newPage();
1597             }
1598             y += fm.height();
1599             p.drawText(x, y, textLine[lineCount]);
1600         }
1601     }
1602 }
1603 
drawTodo(int & count,const KCalendarCore::Todo::Ptr & todo,QPainter & p,KCalendarCore::TodoSortField sortField,KCalendarCore::SortDirection sortDir,bool connectSubTodos,bool strikeoutCompleted,bool desc,int posPriority,int posSummary,int posCategories,int posStartDt,int posDueDt,int posPercentComplete,int level,int x,int & y,int width,int pageHeight,const KCalendarCore::Todo::List & todoList,TodoParentStart * r)1604 void CalPrintPluginBase::drawTodo(int &count,
1605                                   const KCalendarCore::Todo::Ptr &todo,
1606                                   QPainter &p,
1607                                   KCalendarCore::TodoSortField sortField,
1608                                   KCalendarCore::SortDirection sortDir,
1609                                   bool connectSubTodos,
1610                                   bool strikeoutCompleted,
1611                                   bool desc,
1612                                   int posPriority,
1613                                   int posSummary,
1614                                   int posCategories,
1615                                   int posStartDt,
1616                                   int posDueDt,
1617                                   int posPercentComplete,
1618                                   int level,
1619                                   int x,
1620                                   int &y,
1621                                   int width,
1622                                   int pageHeight,
1623                                   const KCalendarCore::Todo::List &todoList,
1624                                   TodoParentStart *r)
1625 {
1626     QString outStr;
1627     const auto locale = QLocale::system();
1628     QRect rect;
1629     TodoParentStart startpt;
1630     // This list keeps all starting points of the parent to-dos so the connection
1631     // lines of the tree can easily be drawn (needed if a new page is started)
1632     static QList<TodoParentStart *> startPoints;
1633     if (level < 1) {
1634         startPoints.clear();
1635     }
1636 
1637     y += 10;
1638 
1639     int left = posSummary + (level * 10);
1640 
1641     // If this is a sub-to-do, r will not be 0, and we want the LH side
1642     // of the priority line up to the RH side of the parent to-do's priority
1643     int lhs = posPriority;
1644     if (r) {
1645         lhs = r->mRect.right() + 1;
1646     }
1647 
1648     outStr.setNum(todo->priority());
1649     rect = p.boundingRect(lhs, y + 10, 5, -1, Qt::AlignCenter, outStr);
1650     // Make it a more reasonable size
1651     rect.setWidth(18);
1652     rect.setHeight(18);
1653     const int top = rect.top();
1654 
1655     // Draw a checkbox
1656     p.setBrush(QBrush(Qt::NoBrush));
1657     p.drawRect(rect);
1658     if (todo->isCompleted()) {
1659         // cross out the rectangle for completed to-dos
1660         p.drawLine(rect.topLeft(), rect.bottomRight());
1661         p.drawLine(rect.topRight(), rect.bottomLeft());
1662     }
1663     lhs = rect.right() + 5;
1664 
1665     // Priority
1666     if (posPriority >= 0 && todo->priority() > 0) {
1667         p.drawText(rect, Qt::AlignCenter, outStr);
1668     }
1669     startpt.mRect = rect; // save for later
1670 
1671     // Connect the dots
1672     if (r && level > 0 && connectSubTodos) {
1673         int bottom;
1674         int center(r->mRect.left() + (r->mRect.width() / 2));
1675         int to(rect.top() + (rect.height() / 2));
1676         int endx(rect.left());
1677         p.drawLine(center, to, endx, to); // side connector
1678         if (r->mSamePage) {
1679             bottom = r->mRect.bottom() + 1;
1680         } else {
1681             bottom = 0;
1682         }
1683         p.drawLine(center, bottom, center, to);
1684     }
1685 
1686     int posSoFar = width;  // Position of leftmost optional field.
1687 
1688     // due date
1689     if (posDueDt >= 0 && todo->hasDueDate()) {
1690         outStr = locale.toString(todo->dtDue().toLocalTime().date(), QLocale::ShortFormat);
1691         rect = p.boundingRect(posDueDt, top, x + width, -1, Qt::AlignTop | Qt::AlignLeft, outStr);
1692         p.drawText(rect, Qt::AlignTop | Qt::AlignLeft, outStr);
1693         posSoFar = posDueDt;
1694     }
1695 
1696     // start date
1697     if (posStartDt >= 0 && todo->hasStartDate()) {
1698         outStr = locale.toString(todo->dtStart().toLocalTime().date(), QLocale::ShortFormat);
1699         rect = p.boundingRect(posStartDt, top, x + width, -1, Qt::AlignTop | Qt::AlignLeft, outStr);
1700         p.drawText(rect, Qt::AlignTop | Qt::AlignLeft, outStr);
1701         posSoFar = posStartDt;
1702     }
1703 
1704     // percentage completed
1705     if (posPercentComplete >= 0) {
1706         int lwidth = 24;
1707         int lheight = p.fontMetrics().ascent();
1708         // first, draw the progress bar
1709         int progress = static_cast<int>(((lwidth * todo->percentComplete()) / 100.0 + 0.5));
1710 
1711         p.setBrush(QBrush(Qt::NoBrush));
1712         p.drawRect(posPercentComplete, top, lwidth, lheight);
1713         if (progress > 0) {
1714             p.setBrush(QColor(128, 128, 128));
1715             p.drawRect(posPercentComplete, top, progress, lheight);
1716         }
1717 
1718         // now, write the percentage
1719         outStr = i18n("%1%", todo->percentComplete());
1720         rect = p.boundingRect(posPercentComplete + lwidth + 3, top, x + width, -1, Qt::AlignTop | Qt::AlignLeft, outStr);
1721         p.drawText(rect, Qt::AlignTop | Qt::AlignLeft, outStr);
1722         posSoFar = posPercentComplete;
1723     }
1724 
1725     // categories
1726     QRect categoriesRect {0, 0, 0, 0};
1727     if (posCategories >= 0) {
1728         outStr = todo->categoriesStr();
1729         outStr.replace(QLatin1Char(','), QLatin1Char('\n'));
1730         rect = p.boundingRect(posCategories, top, posSoFar - posCategories, -1, Qt::TextWordWrap, outStr);
1731         p.drawText(rect, Qt::TextWordWrap, outStr, &categoriesRect);
1732         posSoFar = posCategories;
1733     }
1734 
1735     // summary
1736     outStr = todo->summary();
1737     rect = p.boundingRect(lhs, top, posSoFar - lhs - 5, -1, Qt::TextWordWrap, outStr);
1738     QFont oldFont(p.font());
1739     if (strikeoutCompleted && todo->isCompleted()) {
1740         QFont newFont(p.font());
1741         newFont.setStrikeOut(true);
1742         p.setFont(newFont);
1743     }
1744     QRect summaryRect;
1745     p.drawText(rect, Qt::TextWordWrap, outStr, &summaryRect);
1746     p.setFont(oldFont);
1747 
1748     y = std::max(categoriesRect.bottom(), summaryRect.bottom());
1749 
1750     // description
1751     if (desc && !todo->description().isEmpty()) {
1752         drawTodoLines(p, todo->description(), left, y, width - (left + 10 - x), pageHeight, todo->descriptionIsRich(), startPoints, connectSubTodos);
1753     }
1754 
1755     // Make a list of all the sub-to-dos related to this to-do.
1756     KCalendarCore::Todo::List t;
1757     const KCalendarCore::Incidence::List relations = mCalendar->childIncidences(todo->uid());
1758 
1759     for (const KCalendarCore::Incidence::Ptr &incidence : relations) {
1760         // In the future, to-dos might also be related to events
1761         // Manually check if the sub-to-do is in the list of to-dos to print
1762         // The problem is that relations() does not apply filters, so
1763         // we need to compare manually with the complete filtered list!
1764         KCalendarCore::Todo::Ptr subtodo = incidence.dynamicCast<KCalendarCore::Todo>();
1765         if (!subtodo) {
1766             continue;
1767         }
1768 #ifdef AKONADI_PORT_DISABLED
1769         if (subtodo && todoList.contains(subtodo)) {
1770 #else
1771         bool subtodoOk = false;
1772         if (subtodo) {
1773             for (const KCalendarCore::Todo::Ptr &tt : std::as_const(todoList)) {
1774                 if (tt == subtodo) {
1775                     subtodoOk = true;
1776                     break;
1777                 }
1778             }
1779         }
1780         if (subtodoOk) {
1781 #endif
1782             if ((mExcludeConfidential && subtodo->secrecy() == KCalendarCore::Incidence::SecrecyConfidential)
1783                 || (mExcludePrivate && subtodo->secrecy() == KCalendarCore::Incidence::SecrecyPrivate)) {
1784                 continue;
1785             }
1786             t.append(subtodo);
1787         }
1788     }
1789 
1790     // has sub-todos?
1791     startpt.mHasLine = (relations.size() > 0);
1792     startPoints.append(&startpt);
1793 
1794     // Sort the sub-to-dos and print them
1795 #ifdef AKONADI_PORT_DISABLED
1796     KCalendarCore::Todo::List sl = mCalendar->sortTodos(&t, sortField, sortDir);
1797 #else
1798     KCalendarCore::Todo::List tl;
1799     tl.reserve(t.count());
1800     for (const KCalendarCore::Todo::Ptr &todo : std::as_const(t)) {
1801         tl.append(todo);
1802     }
1803     KCalendarCore::Todo::List sl = mCalendar->sortTodos(tl, sortField, sortDir);
1804 #endif
1805 
1806     int subcount = 0;
1807     for (const KCalendarCore::Todo::Ptr &isl : std::as_const(sl)) {
1808         count++;
1809         if (++subcount == sl.size()) {
1810             startpt.mHasLine = false;
1811         }
1812         drawTodo(count,
1813                  isl,
1814                  p,
1815                  sortField,
1816                  sortDir,
1817                  connectSubTodos,
1818                  strikeoutCompleted,
1819                  desc,
1820                  posPriority,
1821                  posSummary,
1822                  posCategories,
1823                  posStartDt,
1824                  posDueDt,
1825                  posPercentComplete,
1826                  level + 1,
1827                  x,
1828                  y,
1829                  width,
1830                  pageHeight,
1831                  todoList,
1832                  &startpt);
1833     }
1834     startPoints.removeAll(&startpt);
1835 }
1836 
1837 int CalPrintPluginBase::weekdayColumn(int weekday)
1838 {
1839     int w = weekday + 7 - QLocale().firstDayOfWeek();
1840     return w % 7;
1841 }
1842 
1843 void CalPrintPluginBase::drawTextLines(QPainter &p, const QString &entry, int x, int &y, int width, int pageHeight, bool richTextEntry)
1844 {
1845     QString plainEntry = (richTextEntry) ? toPlainText(entry) : entry;
1846 
1847     QRect textrect(0, 0, width, -1);
1848     int flags = Qt::AlignLeft;
1849     QFontMetrics fm = p.fontMetrics();
1850 
1851     QStringList lines = plainEntry.split(QLatin1Char('\n'));
1852     for (int currentLine = 0; currentLine < lines.count(); currentLine++) {
1853         // split paragraphs into lines
1854         KWordWrap ww = KWordWrap::formatText(fm, textrect, flags, lines[currentLine]);
1855         QStringList textLine = ww.wrappedString().split(QLatin1Char('\n'));
1856         // print each individual line
1857         for (int lineCount = 0; lineCount < textLine.count(); lineCount++) {
1858             y += fm.height();
1859             if (y >= pageHeight) {
1860                 if (mPrintFooter) {
1861                     drawFooter(p, {0, pageHeight, width, footerHeight()});
1862                 }
1863                 y = fm.height();
1864                 mPrinter->newPage();
1865             }
1866             p.drawText(x, y, textLine[lineCount]);
1867         }
1868     }
1869 }
1870 
1871 void CalPrintPluginBase::drawSplitHeaderRight(QPainter &p, QDate fd, QDate td, QDate, int width, int height)
1872 {
1873     QFont oldFont(p.font());
1874 
1875     QPen oldPen(p.pen());
1876     QPen pen(Qt::black, 4);
1877 
1878     QString title;
1879     QLocale locale;
1880     if (fd.month() == td.month()) {
1881         title = i18nc("Date range: Month dayStart - dayEnd",
1882                       "%1 %2\u2013%3",
1883                       locale.monthName(fd.month(), QLocale::LongFormat),
1884                       locale.toString(fd, QStringLiteral("dd")),
1885                       locale.toString(td, QStringLiteral("dd")));
1886     } else {
1887         title = i18nc("Date range: monthStart dayStart - monthEnd dayEnd",
1888                       "%1 %2\u2013%3 %4",
1889                       locale.monthName(fd.month(), QLocale::LongFormat),
1890                       locale.toString(fd, QStringLiteral("dd")),
1891                       locale.monthName(td.month(), QLocale::LongFormat),
1892                       locale.toString(td, QStringLiteral("dd")));
1893     }
1894 
1895     if (height < 60) {
1896         p.setFont(QFont(QStringLiteral("Times"), 22));
1897     } else {
1898         p.setFont(QFont(QStringLiteral("Times"), 28));
1899     }
1900 
1901     int lineSpacing = p.fontMetrics().lineSpacing();
1902     p.drawText(0, 0, width, lineSpacing, Qt::AlignRight | Qt::AlignTop, title);
1903 
1904     title.truncate(0);
1905 
1906     p.setPen(pen);
1907     p.drawLine(300, lineSpacing, width, lineSpacing);
1908     p.setPen(oldPen);
1909 
1910     if (height < 60) {
1911         p.setFont(QFont(QStringLiteral("Times"), 14, QFont::Bold, true));
1912     } else {
1913         p.setFont(QFont(QStringLiteral("Times"), 18, QFont::Bold, true));
1914     }
1915 
1916     title += QString::number(fd.year());
1917     p.drawText(0, lineSpacing + padding(), width, lineSpacing, Qt::AlignRight | Qt::AlignTop, title);
1918 
1919     p.setFont(oldFont);
1920 }
1921 
1922 void CalPrintPluginBase::drawNoteLines(QPainter &p, QRect box, int startY)
1923 {
1924     int lineHeight = int(p.fontMetrics().lineSpacing() * 1.5);
1925     int linePos = box.y();
1926     int startPos = startY;
1927     // adjust line to start at multiple from top of box for alignment
1928     while (linePos < startPos) {
1929         linePos += lineHeight;
1930     }
1931     QPen oldPen(p.pen());
1932     p.setPen(Qt::DotLine);
1933     while (linePos < box.bottom()) {
1934         p.drawLine(box.left() + padding(), linePos, box.right() - padding(), linePos);
1935         linePos += lineHeight;
1936     }
1937     p.setPen(oldPen);
1938 }
1939 
1940 QString CalPrintPluginBase::toPlainText(const QString &htmlText)
1941 {
1942     // this converts possible rich text to plain text
1943     return QTextDocumentFragment::fromHtml(htmlText).toPlainText();
1944 }
1945