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(" "); 0383 const auto lst = mTimezone.transitions(now, now.addYears(1)); 0384 for (const auto &transition : lst) { 0385 abbreviations += transition.abbreviation; 0386 abbreviations += QLatin1StringView(", "); 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"