File indexing completed on 2024-11-24 04:41:33

0001 /*
0002   SPDX-FileCopyrightText: 2001 Cornelius Schumacher <schumacher@kde.org>
0003   SPDX-FileCopyrightText: 2003-2004 Reinhold Kainhofer <reinhold@kainhofer.com>
0004   SPDX-FileCopyrightText: 2007 Bruno Virlet <bruno@virlet.org>
0005 
0006   SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
0007 */
0008 #include "timelabels.h"
0009 #include "agenda.h"
0010 #include "prefs.h"
0011 #include "timelabelszone.h"
0012 #include "timescaleconfigdialog.h"
0013 
0014 #include <KCalUtils/Stringify>
0015 
0016 #include <KLocalizedString>
0017 
0018 #include <QHelpEvent>
0019 #include <QIcon>
0020 #include <QMenu>
0021 #include <QPainter>
0022 #include <QPointer>
0023 #include <QToolTip>
0024 
0025 using namespace EventViews;
0026 
0027 TimeLabels::TimeLabels(const QTimeZone &zone, int rows, TimeLabelsZone *parent, Qt::WindowFlags f)
0028     : QWidget(parent, f)
0029     , mTimezone(zone)
0030 {
0031     mTimeLabelsZone = parent;
0032     mRows = rows;
0033     mMiniWidth = 0;
0034 
0035     mCellHeight = mTimeLabelsZone->preferences()->hourSize() * 4;
0036 
0037     setBackgroundRole(QPalette::Window);
0038 
0039     mMousePos = new QFrame(this);
0040     mMousePos->setLineWidth(1);
0041     mMousePos->setFrameStyle(QFrame::HLine | QFrame::Plain);
0042     mMousePos->setFixedSize(width(), 1);
0043     colorMousePos();
0044     mAgenda = nullptr;
0045 
0046     setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
0047 
0048     updateConfig();
0049 }
0050 
0051 void TimeLabels::mousePosChanged(QPoint pos)
0052 {
0053     colorMousePos();
0054     mMousePos->move(0, pos.y());
0055 
0056     // The repaint somehow prevents that the red line leaves a black artifact when
0057     // moved down. It's not a full solution, though.
0058     repaint();
0059 }
0060 
0061 void TimeLabels::showMousePos()
0062 {
0063     // touch screen have no mouse position
0064     mMousePos->show();
0065 }
0066 
0067 void TimeLabels::hideMousePos()
0068 {
0069     mMousePos->hide();
0070 }
0071 
0072 void TimeLabels::colorMousePos()
0073 {
0074     QPalette pal;
0075     pal.setColor(QPalette::Window, // for Oxygen
0076                  mTimeLabelsZone->preferences()->agendaMarcusBainsLineLineColor());
0077     pal.setColor(QPalette::WindowText, // for Plastique
0078                  mTimeLabelsZone->preferences()->agendaMarcusBainsLineLineColor());
0079     mMousePos->setPalette(pal);
0080 }
0081 
0082 void TimeLabels::setCellHeight(double height)
0083 {
0084     if (mCellHeight != height) {
0085         mCellHeight = height;
0086         updateGeometry();
0087     }
0088 }
0089 
0090 QSize TimeLabels::minimumSizeHint() const
0091 {
0092     QSize sh = QWidget::sizeHint();
0093     sh.setWidth(mMiniWidth);
0094     return sh;
0095 }
0096 
0097 static bool use12Clock()
0098 {
0099     const QString str = QLocale().timeFormat();
0100     // 'A' or 'a' means am/pm is shown (and then 'h' uses 12-hour format)
0101     // but 'H' forces a 24-hour format anyway, even with am/pm shown.
0102     return str.contains(QLatin1Char('a'), Qt::CaseInsensitive) && !str.contains(QLatin1Char('H'));
0103 }
0104 
0105 /** updates widget's internal state */
0106 void TimeLabels::updateConfig()
0107 {
0108     setFont(mTimeLabelsZone->preferences()->agendaTimeLabelsFont());
0109 
0110     QString test = QStringLiteral("20");
0111     if (use12Clock()) {
0112         test = QStringLiteral("12");
0113     }
0114     mMiniWidth = fontMetrics().boundingRect(test).width();
0115     if (use12Clock()) {
0116         test = QStringLiteral("pm");
0117     } else {
0118         test = QStringLiteral("00");
0119     }
0120     QFont sFont = font();
0121     sFont.setPointSize(sFont.pointSize() / 2);
0122     QFontMetrics fmS(sFont);
0123     mMiniWidth += fmS.boundingRect(test).width() + 4;
0124 
0125     /** Can happen if all resources are disabled */
0126     if (!mAgenda) {
0127         return;
0128     }
0129 
0130     // update HourSize
0131     mCellHeight = mTimeLabelsZone->preferences()->hourSize() * 4;
0132     // If the agenda is zoomed out so that more than 24 would be shown,
0133     // the agenda only shows 24 hours, so we need to take the cell height
0134     // from the agenda, which is larger than the configured one!
0135     if (mCellHeight < 4 * mAgenda->gridSpacingY()) {
0136         mCellHeight = 4 * mAgenda->gridSpacingY();
0137     }
0138 
0139     updateGeometry();
0140 
0141     repaint();
0142 }
0143 
0144 /**  */
0145 void TimeLabels::setAgenda(Agenda *agenda)
0146 {
0147     mAgenda = agenda;
0148 
0149     if (mAgenda) {
0150         connect(mAgenda, &Agenda::mousePosSignal, this, &TimeLabels::mousePosChanged);
0151         connect(mAgenda, &Agenda::enterAgenda, this, &TimeLabels::showMousePos);
0152         connect(mAgenda, &Agenda::leaveAgenda, this, &TimeLabels::hideMousePos);
0153         connect(mAgenda, &Agenda::gridSpacingYChanged, this, &TimeLabels::setCellHeight);
0154     }
0155 }
0156 
0157 int TimeLabels::yposToCell(const int ypos) const
0158 {
0159     const KCalendarCore::DateList datelist = mAgenda->dateList();
0160     if (datelist.isEmpty()) {
0161         return 0;
0162     }
0163 
0164     const auto firstDay = QDateTime(datelist.first(), QTime(0, 0, 0), Qt::LocalTime).toUTC();
0165     const int beginning // the hour we start drawing with
0166         = !mTimezone.isValid() ? 0 : (mTimezone.offsetFromUtc(firstDay) - mTimeLabelsZone->preferences()->timeZone().offsetFromUtc(firstDay)) / 3600;
0167 
0168     return static_cast<int>(ypos / mCellHeight) + beginning;
0169 }
0170 
0171 int TimeLabels::cellToHour(const int cell) const
0172 {
0173     int tCell = cell % 24;
0174     // handle different timezones
0175     if (tCell < 0) {
0176         tCell += 24;
0177     }
0178     // handle 24h and am/pm time formats
0179     if (use12Clock()) {
0180         if (tCell == 0) {
0181             tCell = 12;
0182         }
0183         if (tCell < 0) {
0184             tCell += 24;
0185         }
0186         if (tCell > 12) {
0187             tCell %= 12;
0188             if (tCell == 0) {
0189                 tCell = 12;
0190             }
0191         }
0192     }
0193     return tCell;
0194 }
0195 
0196 QString TimeLabels::cellToSuffix(const int cell) const
0197 {
0198     // TODO: rewrite this using QTime's time formats. "am/pm" doesn't make sense
0199     // in some locale's
0200     QString suffix;
0201     if (use12Clock()) {
0202         if ((cell / 12) % 2 != 0) {
0203             suffix = QStringLiteral("pm");
0204         } else {
0205             suffix = QStringLiteral("am");
0206         }
0207     } else {
0208         suffix = QStringLiteral("00");
0209     }
0210     return suffix;
0211 }
0212 
0213 /** This is called in response to repaint() */
0214 void TimeLabels::paintEvent(QPaintEvent *)
0215 {
0216     if (!mAgenda) {
0217         return;
0218     }
0219     const KCalendarCore::DateList datelist = mAgenda->dateList();
0220     if (datelist.isEmpty()) {
0221         return;
0222     }
0223 
0224     QPainter p(this);
0225 
0226     const int ch = height();
0227 
0228     // We won't paint parts that aren't visible
0229     const int cy = -y(); // y() returns a negative value.
0230 
0231     const auto firstDay = QDateTime(datelist.first(), QTime(0, 0, 0), Qt::LocalTime).toUTC();
0232     const int beginning =
0233         !mTimezone.isValid() ? 0 : (mTimezone.offsetFromUtc(firstDay) - mTimeLabelsZone->preferences()->timeZone().offsetFromUtc(firstDay)) / 3600;
0234 
0235     // bug:  the parameters cx and cw are the areas that need to be
0236     //       redrawn, not the area of the widget.  unfortunately, this
0237     //       code assumes the latter...
0238 
0239     // now, for a workaround...
0240     const int cx = 0;
0241     const int cw = width();
0242     // end of workaround
0243 
0244     int cell = yposToCell(cy);
0245     double y = (cell - beginning) * mCellHeight;
0246     QFontMetrics fm = fontMetrics();
0247     QString hour;
0248     int timeHeight = fm.ascent();
0249     QFont hourFont = mTimeLabelsZone->preferences()->agendaTimeLabelsFont();
0250     p.setFont(font());
0251 
0252     // TODO: rewrite this using QTime's time formats. "am/pm" doesn't make sense
0253     // in some locale's
0254     QString suffix;
0255     if (!use12Clock()) {
0256         suffix = QStringLiteral("00");
0257     } else {
0258         suffix = QStringLiteral("am");
0259     }
0260 
0261     // We adjust the size of the hour font to keep it reasonable
0262     if (timeHeight > mCellHeight) {
0263         timeHeight = static_cast<int>(mCellHeight - 1);
0264         int pointS = hourFont.pointSize();
0265         while (pointS > 4) { // TODO: use smallestReadableFont() when added to kdelibs
0266             hourFont.setPointSize(pointS);
0267             fm = QFontMetrics(hourFont);
0268             if (fm.ascent() < mCellHeight) {
0269                 break;
0270             }
0271             --pointS;
0272         }
0273         fm = QFontMetrics(hourFont);
0274         timeHeight = fm.ascent();
0275     }
0276     // timeHeight -= (timeHeight/4-2);
0277     QFont suffixFont = hourFont;
0278     suffixFont.setPointSize(suffixFont.pointSize() / 2);
0279     QFontMetrics fmS(suffixFont);
0280     const int startW = cw - 2;
0281     const int tw2 = fmS.boundingRect(suffix).width();
0282     const int divTimeHeight = (timeHeight - 1) / 2 - 1;
0283     // testline
0284     // p->drawLine(0,0,0,contentsHeight());
0285     while (y < cy + ch + mCellHeight) {
0286         QColor lineColor;
0287         QColor textColor;
0288         textColor = palette().color(QPalette::WindowText);
0289         if (cell < 0 || cell >= 24) {
0290             textColor.setAlphaF(0.5);
0291         }
0292         lineColor = textColor;
0293         lineColor.setAlphaF(lineColor.alphaF() / 5.);
0294         p.setPen(lineColor);
0295 
0296         // hour, full line
0297         p.drawLine(cx, int(y), cw + 2, int(y));
0298 
0299         // set the hour and suffix from the cell
0300         hour.setNum(cellToHour(cell));
0301         suffix = cellToSuffix(cell);
0302 
0303         // draw the time label
0304         p.setPen(textColor);
0305         const int timeWidth = fm.boundingRect(hour).width();
0306         int offset = startW - timeWidth - tw2 - 1;
0307         p.setFont(hourFont);
0308         p.drawText(offset, static_cast<int>(y + timeHeight), hour);
0309         p.setFont(suffixFont);
0310         offset = startW - tw2;
0311         p.drawText(offset, static_cast<int>(y + timeHeight - divTimeHeight), suffix);
0312 
0313         // increment indices
0314         y += mCellHeight;
0315         cell++;
0316     }
0317 }
0318 
0319 QSize TimeLabels::sizeHint() const
0320 {
0321     return {mMiniWidth, static_cast<int>(mRows * mCellHeight)};
0322 }
0323 
0324 void TimeLabels::contextMenuEvent(QContextMenuEvent *event)
0325 {
0326     Q_UNUSED(event)
0327 
0328     QMenu popup(this);
0329     QAction *editTimeZones = popup.addAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18n("&Add Timezones..."));
0330     QAction *removeTimeZone = popup.addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("&Remove Timezone %1", i18n(mTimezone.id().constData())));
0331     if (!mTimezone.isValid() || !mTimeLabelsZone->preferences()->timeScaleTimezones().count() || mTimezone == mTimeLabelsZone->preferences()->timeZone()) {
0332         removeTimeZone->setEnabled(false);
0333     }
0334 
0335     QAction *activatedAction = popup.exec(QCursor::pos());
0336     if (activatedAction == editTimeZones) {
0337         QPointer<TimeScaleConfigDialog> dialog = new TimeScaleConfigDialog(mTimeLabelsZone->preferences(), this);
0338         if (dialog->exec() == QDialog::Accepted) {
0339             mTimeLabelsZone->reset();
0340         }
0341         delete dialog;
0342     } else if (activatedAction == removeTimeZone) {
0343         QStringList list = mTimeLabelsZone->preferences()->timeScaleTimezones();
0344         list.removeAll(QString::fromUtf8(mTimezone.id()));
0345         mTimeLabelsZone->preferences()->setTimeScaleTimezones(list);
0346         mTimeLabelsZone->preferences()->writeConfig();
0347         mTimeLabelsZone->reset();
0348         hide();
0349         deleteLater();
0350     }
0351 }
0352 
0353 QTimeZone TimeLabels::timeZone() const
0354 {
0355     return mTimezone;
0356 }
0357 
0358 QString TimeLabels::header() const
0359 {
0360     return i18n(mTimezone.id().constData());
0361 }
0362 
0363 QString TimeLabels::headerToolTip() const
0364 {
0365     QDateTime now = QDateTime::currentDateTime();
0366     QString toolTip;
0367 
0368     toolTip += QLatin1StringView("<qt>");
0369     toolTip += i18nc("title for timezone info, the timezone id and utc offset",
0370                      "<b>%1 (%2)</b>",
0371                      i18n(mTimezone.id().constData()),
0372                      KCalUtils::Stringify::tzUTCOffsetStr(mTimezone));
0373     toolTip += QLatin1StringView("<hr>");
0374     toolTip += i18nc("heading for timezone display name", "<i>Name:</i> %1", mTimezone.displayName(now, QTimeZone::LongName));
0375     toolTip += QLatin1StringView("<br/>");
0376 
0377     if (mTimezone.territory() != QLocale::AnyCountry) {
0378         toolTip += i18nc("heading for timezone country", "<i>Country:</i> %1", QLocale::territoryToString(mTimezone.territory()));
0379         toolTip += QLatin1StringView("<br/>");
0380     }
0381 
0382     auto abbreviations = QStringLiteral("&nbsp;");
0383     const auto lst = mTimezone.transitions(now, now.addYears(1));
0384     for (const auto &transition : lst) {
0385         abbreviations += transition.abbreviation;
0386         abbreviations += QLatin1StringView(",&nbsp;");
0387     }
0388     abbreviations.chop(7);
0389     if (!abbreviations.isEmpty()) {
0390         toolTip += i18nc("heading for comma-separated list of timezone abbreviations", "<i>Abbreviations:</i>");
0391         toolTip += abbreviations;
0392         toolTip += QLatin1StringView("<br/>");
0393     }
0394     const QString timeZoneComment(mTimezone.comment());
0395     if (!timeZoneComment.isEmpty()) {
0396         toolTip += i18nc("heading for the timezone comment", "<i>Comment:</i> %1", timeZoneComment);
0397     }
0398     toolTip += QLatin1StringView("</qt>");
0399 
0400     return toolTip;
0401 }
0402 
0403 bool TimeLabels::event(QEvent *event)
0404 {
0405     if (event->type() == QEvent::ToolTip) {
0406         auto helpEvent = static_cast<QHelpEvent *>(event);
0407         const int cell = yposToCell(helpEvent->pos().y());
0408 
0409         QString toolTip;
0410         toolTip += QLatin1StringView("<qt>");
0411         toolTip += i18nc("[hour of the day][am/pm/00] [timezone id (timezone-offset)]",
0412                          "%1%2<br/>%3 (%4)",
0413                          cellToHour(cell),
0414                          cellToSuffix(cell),
0415                          i18n(mTimezone.id().constData()),
0416                          KCalUtils::Stringify::tzUTCOffsetStr(mTimezone));
0417         toolTip += QLatin1StringView("</qt>");
0418 
0419         QToolTip::showText(helpEvent->globalPos(), toolTip, this);
0420 
0421         return true;
0422     }
0423     return QWidget::event(event);
0424 }
0425 
0426 #include "moc_timelabels.cpp"