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(" ");
383 const auto lst = mTimezone.transitions(now, now.addYears(1));
384 for (const auto &transition : lst) {
385 abbreviations += transition.abbreviation;
386 abbreviations += QLatin1String(", ");
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