1 /*
2   SPDX-FileCopyrightText: 2001 Cornelius Schumacher <schumacher@kde.org>
3   SPDX-FileCopyrightText: 2003-2004 Reinhold Kainhofer <reinhold@kainhofer.com>
4   SPDX-FileCopyrightText: 2007 Bruno Virlet <bruno@virlet.org>
5 
6   SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
7 */
8 #include "timelabels.h"
9 #include "agenda.h"
10 #include "prefs.h"
11 #include "timelabelszone.h"
12 #include "timescaleconfigdialog.h"
13 
14 #include <KCalUtils/Stringify>
15 
16 #include <KLocalizedString>
17 
18 #include <QHelpEvent>
19 #include <QIcon>
20 #include <QMenu>
21 #include <QPainter>
22 #include <QPointer>
23 #include <QToolTip>
24 
25 using namespace EventViews;
26 
TimeLabels(const QTimeZone & zone,int rows,TimeLabelsZone * parent,Qt::WindowFlags f)27 TimeLabels::TimeLabels(const QTimeZone &zone, int rows, TimeLabelsZone *parent, Qt::WindowFlags f)
28     : QWidget(parent, f)
29     , mTimezone(zone)
30 {
31     mTimeLabelsZone = parent;
32     mRows = rows;
33     mMiniWidth = 0;
34 
35     mCellHeight = mTimeLabelsZone->preferences()->hourSize() * 4;
36 
37     setBackgroundRole(QPalette::Window);
38 
39     mMousePos = new QFrame(this);
40     mMousePos->setLineWidth(1);
41     mMousePos->setFrameStyle(QFrame::HLine | QFrame::Plain);
42     mMousePos->setFixedSize(width(), 1);
43     colorMousePos();
44     mAgenda = nullptr;
45 
46     setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
47 
48     updateConfig();
49 }
50 
mousePosChanged(QPoint pos)51 void TimeLabels::mousePosChanged(QPoint pos)
52 {
53     colorMousePos();
54     mMousePos->move(0, pos.y());
55 
56     // The repaint somehow prevents that the red line leaves a black artifact when
57     // moved down. It's not a full solution, though.
58     repaint();
59 }
60 
showMousePos()61 void TimeLabels::showMousePos()
62 {
63     // touch screen have no mouse position
64     mMousePos->show();
65 }
66 
hideMousePos()67 void TimeLabels::hideMousePos()
68 {
69     mMousePos->hide();
70 }
71 
colorMousePos()72 void TimeLabels::colorMousePos()
73 {
74     QPalette pal;
75     pal.setColor(QPalette::Window, // for Oxygen
76                  mTimeLabelsZone->preferences()->agendaMarcusBainsLineLineColor());
77     pal.setColor(QPalette::WindowText, // for Plastique
78                  mTimeLabelsZone->preferences()->agendaMarcusBainsLineLineColor());
79     mMousePos->setPalette(pal);
80 }
81 
setCellHeight(double height)82 void TimeLabels::setCellHeight(double height)
83 {
84     if (mCellHeight != height) {
85         mCellHeight = height;
86         updateGeometry();
87     }
88 }
89 
minimumSizeHint() const90 QSize TimeLabels::minimumSizeHint() const
91 {
92     QSize sh = QWidget::sizeHint();
93     sh.setWidth(mMiniWidth);
94     return sh;
95 }
96 
use12Clock()97 static bool use12Clock()
98 {
99     const QString str = QLocale().timeFormat();
100     // 'A' or 'a' means am/pm is shown (and then 'h' uses 12-hour format)
101     // but 'H' forces a 24-hour format anyway, even with am/pm shown.
102     return str.contains(QLatin1Char('a'), Qt::CaseInsensitive) && !str.contains(QLatin1Char('H'));
103 }
104 
105 /** updates widget's internal state */
updateConfig()106 void TimeLabels::updateConfig()
107 {
108     setFont(mTimeLabelsZone->preferences()->agendaTimeLabelsFont());
109 
110     QString test = QStringLiteral("20");
111     if (use12Clock()) {
112         test = QStringLiteral("12");
113     }
114     mMiniWidth = fontMetrics().boundingRect(test).width();
115     if (use12Clock()) {
116         test = QStringLiteral("pm");
117     } else {
118         test = QStringLiteral("00");
119     }
120     QFont sFont = font();
121     sFont.setPointSize(sFont.pointSize() / 2);
122     QFontMetrics fmS(sFont);
123     mMiniWidth += fmS.boundingRect(test).width() + 4;
124 
125     /** Can happen if all resources are disabled */
126     if (!mAgenda) {
127         return;
128     }
129 
130     // update HourSize
131     mCellHeight = mTimeLabelsZone->preferences()->hourSize() * 4;
132     // If the agenda is zoomed out so that more than 24 would be shown,
133     // the agenda only shows 24 hours, so we need to take the cell height
134     // from the agenda, which is larger than the configured one!
135     if (mCellHeight < 4 * mAgenda->gridSpacingY()) {
136         mCellHeight = 4 * mAgenda->gridSpacingY();
137     }
138 
139     updateGeometry();
140 
141     repaint();
142 }
143 
144 /**  */
setAgenda(Agenda * agenda)145 void TimeLabels::setAgenda(Agenda *agenda)
146 {
147     mAgenda = agenda;
148 
149     if (mAgenda) {
150         connect(mAgenda, &Agenda::mousePosSignal, this, &TimeLabels::mousePosChanged);
151         connect(mAgenda, &Agenda::enterAgenda, this, &TimeLabels::showMousePos);
152         connect(mAgenda, &Agenda::leaveAgenda, this, &TimeLabels::hideMousePos);
153         connect(mAgenda, &Agenda::gridSpacingYChanged, this, &TimeLabels::setCellHeight);
154     }
155 }
156 
yposToCell(const int ypos) const157 int TimeLabels::yposToCell(const int ypos) const
158 {
159     const KCalendarCore::DateList datelist = mAgenda->dateList();
160     if (datelist.isEmpty()) {
161         return 0;
162     }
163 
164     const auto firstDay = QDateTime(datelist.first(), QTime(0, 0, 0), Qt::LocalTime).toUTC();
165     const int beginning // the hour we start drawing with
166         = !mTimezone.isValid() ? 0 : (mTimezone.offsetFromUtc(firstDay) - mTimeLabelsZone->preferences()->timeZone().offsetFromUtc(firstDay)) / 3600;
167 
168     return static_cast<int>(ypos / mCellHeight) + beginning;
169 }
170 
cellToHour(const int cell) const171 int TimeLabels::cellToHour(const int cell) const
172 {
173     int tCell = cell % 24;
174     // handle different timezones
175     if (tCell < 0) {
176         tCell += 24;
177     }
178     // handle 24h and am/pm time formats
179     if (use12Clock()) {
180         if (tCell == 0) {
181             tCell = 12;
182         }
183         if (tCell < 0) {
184             tCell += 24;
185         }
186         if (tCell > 12) {
187             tCell %= 12;
188             if (tCell == 0) {
189                 tCell = 12;
190             }
191         }
192     }
193     return tCell;
194 }
195 
cellToSuffix(const int cell) const196 QString TimeLabels::cellToSuffix(const int cell) const
197 {
198     // TODO: rewrite this using QTime's time formats. "am/pm" doesn't make sense
199     // in some locale's
200     QString suffix;
201     if (use12Clock()) {
202         if ((cell / 12) % 2 != 0) {
203             suffix = QStringLiteral("pm");
204         } else {
205             suffix = QStringLiteral("am");
206         }
207     } else {
208         suffix = QStringLiteral("00");
209     }
210     return suffix;
211 }
212 
213 /** This is called in response to repaint() */
paintEvent(QPaintEvent *)214 void TimeLabels::paintEvent(QPaintEvent *)
215 {
216     if (!mAgenda) {
217         return;
218     }
219     const KCalendarCore::DateList datelist = mAgenda->dateList();
220     if (datelist.isEmpty()) {
221         return;
222     }
223 
224     QPainter p(this);
225 
226     const int ch = height();
227 
228     // We won't paint parts that aren't visible
229     const int cy = -y(); // y() returns a negative value.
230 
231     const auto firstDay = QDateTime(datelist.first(), QTime(0, 0, 0), Qt::LocalTime).toUTC();
232     const int beginning =
233         !mTimezone.isValid() ? 0 : (mTimezone.offsetFromUtc(firstDay) - mTimeLabelsZone->preferences()->timeZone().offsetFromUtc(firstDay)) / 3600;
234 
235     // bug:  the parameters cx and cw are the areas that need to be
236     //       redrawn, not the area of the widget.  unfortunately, this
237     //       code assumes the latter...
238 
239     // now, for a workaround...
240     const int cx = 0;
241     const int cw = width();
242     // end of workaround
243 
244     int cell = yposToCell(cy);
245     double y = (cell - beginning) * mCellHeight;
246     QFontMetrics fm = fontMetrics();
247     QString hour;
248     int timeHeight = fm.ascent();
249     QFont hourFont = mTimeLabelsZone->preferences()->agendaTimeLabelsFont();
250     p.setFont(font());
251 
252     // TODO: rewrite this using QTime's time formats. "am/pm" doesn't make sense
253     // in some locale's
254     QString suffix;
255     if (!use12Clock()) {
256         suffix = QStringLiteral("00");
257     } else {
258         suffix = QStringLiteral("am");
259     }
260 
261     // We adjust the size of the hour font to keep it reasonable
262     if (timeHeight > mCellHeight) {
263         timeHeight = static_cast<int>(mCellHeight - 1);
264         int pointS = hourFont.pointSize();
265         while (pointS > 4) { // TODO: use smallestReadableFont() when added to kdelibs
266             hourFont.setPointSize(pointS);
267             fm = QFontMetrics(hourFont);
268             if (fm.ascent() < mCellHeight) {
269                 break;
270             }
271             --pointS;
272         }
273         fm = QFontMetrics(hourFont);
274         timeHeight = fm.ascent();
275     }
276     // timeHeight -= (timeHeight/4-2);
277     QFont suffixFont = hourFont;
278     suffixFont.setPointSize(suffixFont.pointSize() / 2);
279     QFontMetrics fmS(suffixFont);
280     const int startW = cw - 2;
281     const int tw2 = fmS.boundingRect(suffix).width();
282     const int divTimeHeight = (timeHeight - 1) / 2 - 1;
283     // testline
284     // p->drawLine(0,0,0,contentsHeight());
285     while (y < cy + ch + mCellHeight) {
286         QColor lineColor;
287         QColor textColor;
288         textColor = palette().color(QPalette::WindowText);
289         if (cell < 0 || cell >= 24) {
290             textColor.setAlphaF(0.5);
291         }
292         lineColor = textColor;
293         lineColor.setAlphaF(lineColor.alphaF() / 5.);
294         p.setPen(lineColor);
295 
296         // hour, full line
297         p.drawLine(cx, int(y), cw + 2, int(y));
298 
299         // set the hour and suffix from the cell
300         hour.setNum(cellToHour(cell));
301         suffix = cellToSuffix(cell);
302 
303         // draw the time label
304         p.setPen(textColor);
305         const int timeWidth = fm.boundingRect(hour).width();
306         int offset = startW - timeWidth - tw2 - 1;
307         p.setFont(hourFont);
308         p.drawText(offset, static_cast<int>(y + timeHeight), hour);
309         p.setFont(suffixFont);
310         offset = startW - tw2;
311         p.drawText(offset, static_cast<int>(y + timeHeight - divTimeHeight), suffix);
312 
313         // increment indices
314         y += mCellHeight;
315         cell++;
316     }
317 }
318 
sizeHint() const319 QSize TimeLabels::sizeHint() const
320 {
321     return {mMiniWidth, static_cast<int>(mRows * mCellHeight)};
322 }
323 
contextMenuEvent(QContextMenuEvent * event)324 void TimeLabels::contextMenuEvent(QContextMenuEvent *event)
325 {
326     Q_UNUSED(event)
327 
328     QMenu popup(this);
329     QAction *editTimeZones = popup.addAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18n("&Add Timezones..."));
330     QAction *removeTimeZone = popup.addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("&Remove Timezone %1", i18n(mTimezone.id().constData())));
331     if (!mTimezone.isValid() || !mTimeLabelsZone->preferences()->timeScaleTimezones().count() || mTimezone == mTimeLabelsZone->preferences()->timeZone()) {
332         removeTimeZone->setEnabled(false);
333     }
334 
335     QAction *activatedAction = popup.exec(QCursor::pos());
336     if (activatedAction == editTimeZones) {
337         QPointer<TimeScaleConfigDialog> dialog = new TimeScaleConfigDialog(mTimeLabelsZone->preferences(), this);
338         if (dialog->exec() == QDialog::Accepted) {
339             mTimeLabelsZone->reset();
340         }
341         delete dialog;
342     } else if (activatedAction == removeTimeZone) {
343         QStringList list = mTimeLabelsZone->preferences()->timeScaleTimezones();
344         list.removeAll(QString::fromUtf8(mTimezone.id()));
345         mTimeLabelsZone->preferences()->setTimeScaleTimezones(list);
346         mTimeLabelsZone->preferences()->writeConfig();
347         mTimeLabelsZone->reset();
348         hide();
349         deleteLater();
350     }
351 }
352 
timeZone() const353 QTimeZone TimeLabels::timeZone() const
354 {
355     return mTimezone;
356 }
357 
header() const358 QString TimeLabels::header() const
359 {
360     return i18n(mTimezone.id().constData());
361 }
362 
headerToolTip() const363 QString TimeLabels::headerToolTip() const
364 {
365     QDateTime now = QDateTime::currentDateTime();
366     QString toolTip;
367 
368     toolTip += QLatin1String("<qt>");
369     toolTip += i18nc("title for timezone info, the timezone id and utc offset",
370                      "<b>%1 (%2)</b>",
371                      i18n(mTimezone.id().constData()),
372                      KCalUtils::Stringify::tzUTCOffsetStr(mTimezone));
373     toolTip += QLatin1String("<hr>");
374     toolTip += i18nc("heading for timezone display name", "<i>Name:</i> %1", mTimezone.displayName(now, QTimeZone::LongName));
375     toolTip += QLatin1String("<br/>");
376 
377     if (mTimezone.country() != QLocale::AnyCountry) {
378         toolTip += i18nc("heading for timezone country", "<i>Country:</i> %1", QLocale::countryToString(mTimezone.country()));
379         toolTip += QLatin1String("<br/>");
380     }
381 
382     auto abbreviations = QStringLiteral("&nbsp;");
383     const auto lst = mTimezone.transitions(now, now.addYears(1));
384     for (const auto &transition : lst) {
385         abbreviations += transition.abbreviation;
386         abbreviations += QLatin1String(",&nbsp;");
387     }
388     abbreviations.chop(7);
389     if (!abbreviations.isEmpty()) {
390         toolTip += i18nc("heading for comma-separated list of timezone abbreviations", "<i>Abbreviations:</i>");
391         toolTip += abbreviations;
392         toolTip += QLatin1String("<br/>");
393     }
394     const QString timeZoneComment(mTimezone.comment());
395     if (!timeZoneComment.isEmpty()) {
396         toolTip += i18nc("heading for the timezone comment", "<i>Comment:</i> %1", timeZoneComment);
397     }
398     toolTip += QLatin1String("</qt>");
399 
400     return toolTip;
401 }
402 
event(QEvent * event)403 bool TimeLabels::event(QEvent *event)
404 {
405     if (event->type() == QEvent::ToolTip) {
406         auto helpEvent = static_cast<QHelpEvent *>(event);
407         const int cell = yposToCell(helpEvent->pos().y());
408 
409         QString toolTip;
410         toolTip += QLatin1String("<qt>");
411         toolTip += i18nc("[hour of the day][am/pm/00] [timezone id (timezone-offset)]",
412                          "%1%2<br/>%3 (%4)",
413                          cellToHour(cell),
414                          cellToSuffix(cell),
415                          i18n(mTimezone.id().constData()),
416                          KCalUtils::Stringify::tzUTCOffsetStr(mTimezone));
417         toolTip += QLatin1String("</qt>");
418 
419         QToolTip::showText(helpEvent->globalPos(), toolTip, this);
420 
421         return true;
422     }
423     return QWidget::event(event);
424 }
425