File indexing completed on 2024-06-02 05:19:22

0001 /*
0002  *  resourcedatamodelbase.cpp  -  base for models containing calendars and events
0003  *  Program:  kalarm
0004  *  SPDX-FileCopyrightText: 2007-2023 David Jarvie <djarvie@kde.org>
0005  *
0006  *  SPDX-License-Identifier: GPL-2.0-or-later
0007  */
0008 
0009 #include "resourcedatamodelbase.h"
0010 
0011 #include "resources.h"
0012 #include "lib/desktop.h"
0013 #include "lib/messagebox.h"
0014 #include "kalarmcalendar/alarmtext.h"
0015 #include "kalarmcalendar/kaevent.h"
0016 #include "kalarm_debug.h"
0017 
0018 #include <KLocalizedString>
0019 
0020 #include <QApplication>
0021 #include <QIcon>
0022 #include <QRegularExpression>
0023 
0024 
0025 /*=============================================================================
0026 = Class: ResourceDataModelBase
0027 =============================================================================*/
0028 
0029 ResourceDataModelBase* ResourceDataModelBase::mInstance = nullptr;
0030 
0031 QPixmap* ResourceDataModelBase::mTextIcon    = nullptr;
0032 QPixmap* ResourceDataModelBase::mFileIcon    = nullptr;
0033 QPixmap* ResourceDataModelBase::mCommandIcon = nullptr;
0034 QPixmap* ResourceDataModelBase::mEmailIcon   = nullptr;
0035 QPixmap* ResourceDataModelBase::mAudioIcon   = nullptr;
0036 QSize    ResourceDataModelBase::mIconSize;
0037 
0038 /******************************************************************************
0039 * Constructor.
0040 */
0041 ResourceDataModelBase::ResourceDataModelBase()
0042 {
0043     if (!mTextIcon)
0044     {
0045         mTextIcon    = new QPixmap(QIcon::fromTheme(QStringLiteral("dialog-information")).pixmap(16, 16));
0046         mFileIcon    = new QPixmap(QIcon::fromTheme(QStringLiteral("document-open")).pixmap(16, 16));
0047         mCommandIcon = new QPixmap(QIcon::fromTheme(QStringLiteral("system-run")).pixmap(16, 16));
0048         mEmailIcon   = new QPixmap(QIcon::fromTheme(QStringLiteral("mail-unread")).pixmap(16, 16));
0049         mAudioIcon   = new QPixmap(QIcon::fromTheme(QStringLiteral("audio-x-generic")).pixmap(16, 16));
0050         mIconSize = mTextIcon->size().expandedTo(mFileIcon->size()).expandedTo(mCommandIcon->size()).expandedTo(mEmailIcon->size()).expandedTo(mAudioIcon->size());
0051     }
0052 }
0053 
0054 ResourceDataModelBase::~ResourceDataModelBase()
0055 {
0056 }
0057 
0058 /******************************************************************************
0059 * Create a bulleted list of alarm types for insertion into <para>...</para>.
0060 */
0061 QString ResourceDataModelBase::typeListForDisplay(CalEvent::Types alarmTypes)
0062 {
0063     QString list;
0064     if (alarmTypes & CalEvent::ACTIVE)
0065         list += QLatin1String("<item>") + i18nc("@item:intext", "Active Alarms") + QLatin1String("</item>");
0066     if (alarmTypes & CalEvent::ARCHIVED)
0067         list += QLatin1String("<item>") + i18nc("@item:intext", "Archived Alarms") + QLatin1String("</item>");
0068     if (alarmTypes & CalEvent::TEMPLATE)
0069         list += QLatin1String("<item>") + i18nc("@item:intext", "Alarm Templates") + QLatin1String("</item>");
0070     if (!list.isEmpty())
0071         list = QLatin1String("<list>") + list + QLatin1String("</list>");
0072     return list;
0073 }
0074 
0075 /******************************************************************************
0076 * Return the read-only status tooltip for a collection, determined by the
0077 * read-write permissions and the KAlarm calendar format compatibility.
0078 * A null string is returned if the collection is read-write and compatible.
0079 */
0080 QString ResourceDataModelBase::readOnlyTooltip(const Resource& resource)
0081 {
0082     switch (resource.compatibility())
0083     {
0084         case KACalendar::Current:
0085             return resource.readOnly() ? i18nc("@item:intext Calendar status", "Read-only") : QString();
0086         case KACalendar::Converted:
0087         case KACalendar::Convertible:
0088             return i18nc("@item:intext Calendar status", "Read-only (old format)");
0089         case KACalendar::Incompatible:
0090         default:
0091             return i18nc("@item:intext Calendar status", "Read-only (other format)");
0092     }
0093 }
0094 
0095 /******************************************************************************
0096 * Return data for a column heading.
0097 */
0098 QVariant ResourceDataModelBase::headerData(int section, Qt::Orientation orientation, int role, bool eventHeaders, bool& handled)
0099 {
0100     if (orientation == Qt::Horizontal)
0101     {
0102         handled = true;
0103         if (eventHeaders)
0104         {
0105             // Event column headers
0106             if (section < 0  ||  section >= ColumnCount)
0107                 return {};
0108             if (role == Qt::DisplayRole  ||  role == ColumnTitleRole)
0109             {
0110                 switch (section)
0111                 {
0112                     case TimeColumn:
0113                         return i18nc("@title:column", "Time");
0114                     case TimeToColumn:
0115                         return i18nc("@title:column", "Time To");
0116                     case RepeatColumn:
0117                         return i18nc("@title:column", "Repeat");
0118                     case ColourColumn:
0119                         return (role == Qt::DisplayRole) ? QString() : i18nc("@title:column", "Color");
0120                     case TypeColumn:
0121                         return (role == Qt::DisplayRole) ? QString() : i18nc("@title:column", "Type");
0122                     case NameColumn:
0123                         return i18nc("@title:column", "Name");
0124                     case TextColumn:
0125                         return i18nc("@title:column", "Message, File or Command");
0126                     case TemplateNameColumn:
0127                         return i18nc("@title:column Template name", "Name");
0128                 }
0129             }
0130             else if (role == Qt::WhatsThisRole)
0131                 return whatsThisText(section);
0132         }
0133         else
0134         {
0135             // Calendar column headers
0136             if (section != 0)
0137                 return {};
0138             if (role == Qt::DisplayRole)
0139                 return i18nc("@title:column", "Calendars");
0140         }
0141     }
0142 
0143     handled = false;
0144     return {};
0145 }
0146 
0147 /******************************************************************************
0148 * Return whether resourceData() or eventData() handle a role.
0149 */
0150 bool ResourceDataModelBase::roleHandled(int role) const
0151 {
0152     switch (role)
0153     {
0154         case Qt::WhatsThisRole:
0155         case Qt::ForegroundRole:
0156         case Qt::BackgroundRole:
0157         case Qt::DisplayRole:
0158         case Qt::TextAlignmentRole:
0159         case Qt::DecorationRole:
0160         case Qt::SizeHintRole:
0161         case Qt::AccessibleTextRole:
0162         case Qt::ToolTipRole:
0163         case ItemTypeRole:
0164         case ResourceIdRole:
0165         case BaseColourRole:
0166         case TimeDisplayRole:
0167         case SortRole:
0168         case StatusRole:
0169         case ValueRole:
0170         case EventIdRole:
0171         case ParentResourceIdRole:
0172         case EnabledRole:
0173         case AlarmActionsRole:
0174         case AlarmSubActionRole:
0175         case CommandErrorRole:
0176             return true;
0177         default:
0178             return false;
0179     }
0180 }
0181 
0182 /******************************************************************************
0183 * Return the data for a given role, for a specified resource.
0184 */
0185 QVariant ResourceDataModelBase::resourceData(int& role, const Resource& resource, bool& handled) const
0186 {
0187     if (roleHandled(role))   // Ensure that roleHandled() is coded correctly
0188     {
0189         handled = true;
0190         switch (role)
0191         {
0192             case Qt::DisplayRole:
0193                 return resource.displayName();
0194             case BaseColourRole:
0195                 role = Qt::BackgroundRole;   // use base model background colour
0196                 break;
0197             case Qt::BackgroundRole:
0198             {
0199                 const QColor colour = resource.backgroundColour();
0200                 if (colour.isValid())
0201                     return colour;
0202                 break;    // use base model background colour
0203             }
0204             case Qt::ForegroundRole:
0205                 return resource.foregroundColour();
0206             case Qt::ToolTipRole:
0207                 return tooltip(resource, CalEvent::ACTIVE | CalEvent::ARCHIVED | CalEvent::TEMPLATE);
0208             case ItemTypeRole:
0209                 return static_cast<int>(Type::Resource);
0210             case ResourceIdRole:
0211                 return resource.id();
0212             default:
0213                 break;
0214         }
0215     }
0216 
0217     handled = false;
0218     return {};
0219 }
0220 
0221 /******************************************************************************
0222 * Return the data for a given role, for a specified event.
0223 */
0224 QVariant ResourceDataModelBase::eventData(int role, int column, const KAEvent& event,
0225                                           const Resource& resource, bool& handled) const
0226 {
0227     if (roleHandled(role))   // Ensure that roleHandled() is coded correctly
0228     {
0229         handled = true;
0230         bool calendarColour = false;
0231 
0232         switch (role)
0233         {
0234             case Qt::WhatsThisRole:
0235                 return whatsThisText(column);
0236             case ItemTypeRole:
0237                 return static_cast<int>(Type::Event);
0238             default:
0239                 break;
0240         }
0241         if (!event.isValid())
0242             return {};
0243         switch (role)
0244         {
0245             case EventIdRole:
0246                 return event.id();
0247             case StatusRole:
0248                 return event.category();
0249             case AlarmActionsRole:
0250                 return static_cast<int>(event.actionTypes());
0251             case AlarmSubActionRole:
0252                 return static_cast<int>(event.actionSubType());
0253             case CommandErrorRole:
0254                 return static_cast<int>(event.commandError());
0255             default:
0256                 break;
0257         }
0258         switch (column)
0259         {
0260             case TimeColumn:
0261                 switch (role)
0262                 {
0263                     case Qt::BackgroundRole:
0264                         calendarColour = true;
0265                         break;
0266                     case Qt::DisplayRole:
0267                         if (event.expired())
0268                             return alarmTimeText(event.startDateTime(), '0');
0269                         return alarmTimeText(event.nextTrigger(KAEvent::Trigger::Display), '0');
0270                     case TimeDisplayRole:
0271                         if (event.expired())
0272                             return alarmTimeText(event.startDateTime(), '~');
0273                         return alarmTimeText(event.nextTrigger(KAEvent::Trigger::Display), '~');
0274                     case Qt::TextAlignmentRole:
0275                         return Qt::AlignLeft;
0276                     case SortRole:
0277                     {
0278                         DateTime due;
0279                         if (event.expired())
0280                             due = event.startDateTime();
0281                         else
0282                             due = event.nextTrigger(KAEvent::Trigger::Display);
0283                         return due.isValid() ? due.effectiveKDateTime().toUtc().qDateTime()
0284                                              : QDateTime(QDate(9999,12,31), QTime(0,0,0));
0285                     }
0286                     default:
0287                         break;
0288                 }
0289                 break;
0290             case TimeToColumn:
0291                 switch (role)
0292                 {
0293                     case Qt::BackgroundRole:
0294                         calendarColour = true;
0295                         break;
0296                     case Qt::DisplayRole:
0297                         if (event.expired())
0298                             return QString();
0299                         return timeToAlarmText(event.nextTrigger(KAEvent::Trigger::Display));
0300                     case Qt::TextAlignmentRole:
0301                         return Qt::AlignRight;
0302                     case SortRole:
0303                     {
0304                         if (event.expired())
0305                             return -1;
0306                         const DateTime due = event.nextTrigger(KAEvent::Trigger::Display);
0307                         const KADateTime now = KADateTime::currentUtcDateTime();
0308                         if (due.isDateOnly())
0309                             return now.date().daysTo(due.date()) * 1440;
0310                         return (now.secsTo(due.effectiveKDateTime()) + 59) / 60;
0311                     }
0312                 }
0313                 break;
0314             case RepeatColumn:
0315                 switch (role)
0316                 {
0317                     case Qt::BackgroundRole:
0318                         calendarColour = true;
0319                         break;
0320                     case Qt::DisplayRole:
0321                         return repeatText(event);
0322                     case Qt::TextAlignmentRole:
0323                         return Qt::AlignHCenter;
0324                     case SortRole:
0325                         return repeatOrder(event);
0326                 }
0327                 break;
0328             case ColourColumn:
0329                 switch (role)
0330                 {
0331                     case Qt::BackgroundRole:
0332                     {
0333                         const KAEvent::Action type = event.actionTypes();
0334                         if (type & KAEvent::Action::Display)
0335                             return event.bgColour();
0336                         if (type == KAEvent::Action::Command)
0337                         {
0338                             if (event.commandError() != KAEvent::CmdErr::None)
0339                                 return QColor(Qt::red);
0340                         }
0341                         break;
0342                     }
0343                     case Qt::ForegroundRole:
0344                         if (event.commandError() != KAEvent::CmdErr::None)
0345                         {
0346                             if (event.actionTypes() == KAEvent::Action::Command)
0347                                 return QColor(Qt::white);
0348                             QColor colour = Qt::red;
0349                             int r, g, b;
0350                             event.bgColour().getRgb(&r, &g, &b);
0351                             if (r > 128  &&  g <= 128  &&  b <= 128)
0352                                 colour = QColor(Qt::white);
0353                             return colour;
0354                         }
0355                         break;
0356                     case Qt::DisplayRole:
0357                         if (event.commandError() != KAEvent::CmdErr::None)
0358                             return QLatin1String("!");
0359                         break;
0360                     case Qt::TextAlignmentRole:
0361                         return Qt::AlignCenter;
0362                     case SortRole:
0363                     {
0364                         const unsigned i = (event.actionTypes() == KAEvent::Action::Display)
0365                                            ? event.bgColour().rgb() : 0;
0366                         return QStringLiteral("%1").arg(i, 6, 10, QLatin1Char('0'));
0367                     }
0368                     default:
0369                         break;
0370                 }
0371                 break;
0372             case TypeColumn:
0373                 switch (role)
0374                 {
0375                     case Qt::BackgroundRole:
0376                         calendarColour = true;
0377                         break;
0378                     case Qt::DecorationRole:
0379                     {
0380                         QVariant v;
0381                         v.setValue(*eventIcon(event));
0382                         return v;
0383                     }
0384                     case Qt::TextAlignmentRole:
0385                         return Qt::AlignHCenter;
0386                     case Qt::SizeHintRole:
0387                         return mIconSize;
0388                     case Qt::AccessibleTextRole:
0389 //TODO: Implement accessibility
0390                         return QString();
0391                     case ValueRole:
0392                         return static_cast<int>(event.actionSubType());
0393                     case SortRole:
0394                         return QStringLiteral("%1").arg(static_cast<int>(event.actionSubType()), 2, 10, QLatin1Char('0'));
0395                 }
0396                 break;
0397             case NameColumn:
0398             case TemplateNameColumn:
0399                 switch (role)
0400                 {
0401                     case Qt::BackgroundRole:
0402                         calendarColour = true;
0403                         break;
0404                     case Qt::DisplayRole:
0405                     case Qt::ToolTipRole:
0406                         return event.name();
0407                     case SortRole:
0408                         return event.name().toUpper();
0409                 }
0410                 break;
0411             case TextColumn:
0412                 switch (role)
0413                 {
0414                     case Qt::BackgroundRole:
0415                         calendarColour = true;
0416                         break;
0417                     case Qt::DisplayRole:
0418                     case SortRole:
0419                         return AlarmText::summary(event, 1);
0420                     case Qt::ToolTipRole:
0421                         return AlarmText::summary(event, 10);
0422                     default:
0423                         break;
0424                 }
0425                 break;
0426             default:
0427                 break;
0428         }
0429 
0430         if (calendarColour)
0431         {
0432             const QColor colour = resource.backgroundColour();
0433             if (colour.isValid())
0434                 return colour;
0435         }
0436 
0437         switch (role)
0438         {
0439             case Qt::ForegroundRole:
0440                 if (!event.enabled())
0441                    return Preferences::disabledColour();
0442                 if (event.expired())
0443                    return Preferences::archivedColour();
0444                 break;   // use the default for normal active alarms
0445             case Qt::ToolTipRole:
0446                 // Show the last command execution error message
0447                 switch (event.commandError())
0448                 {
0449                     case KAEvent::CmdErr::Fail:
0450                         return i18nc("@info:tooltip", "Command execution failed");
0451                     case KAEvent::CmdErr::Pre:
0452                         return i18nc("@info:tooltip", "Pre-alarm action execution failed");
0453                     case KAEvent::CmdErr::Post:
0454                         return i18nc("@info:tooltip", "Post-alarm action execution failed");
0455                     case KAEvent::CmdErr::PrePost:
0456                         return i18nc("@info:tooltip", "Pre- and post-alarm action execution failed");
0457                     default:
0458                     case KAEvent::CmdErr::None:
0459                         // Return empty string to cancel any previous tooltip -
0460                         // returning QVariant() leaves tooltip unchanged.
0461                         return QString();
0462                 }
0463                 break;
0464             case EnabledRole:
0465                 return event.enabled();
0466             default:
0467                 break;
0468         }
0469     }
0470 
0471     handled = false;
0472     return {};
0473 }
0474 
0475 /******************************************************************************
0476 * Return a resource's tooltip text. The resource's enabled status is
0477 * evaluated for specified alarm types.
0478 */
0479 QString ResourceDataModelBase::tooltip(const Resource& resource, CalEvent::Types types) const
0480 {
0481     const QString name     = QLatin1Char('@') + resource.displayName();   // insert markers for stripping out name
0482     const QString type     = QLatin1Char('@') + resource.storageTypeString(false);   // file/directory/URL etc.
0483     const QString locn     = resource.displayLocation();
0484     const bool    inactive = !(resource.enabledTypes() & types);
0485     const QString readonly = readOnlyTooltip(resource);
0486     const bool    writable = readonly.isEmpty();
0487     const QString disabled = i18nc("@item:intext Calendar status", "Disabled");
0488     if (inactive  ||  !writable)
0489         return xi18nc("@info:tooltip",
0490                       "%1"
0491                       "<nl/>%2: <filename>%3</filename>"
0492                       "<nl/>%4",
0493                       name, type, locn, (inactive ? disabled : readonly));
0494     return xi18nc("@info:tooltip",
0495                   "%1"
0496                   "<nl/>%2: <filename>%3</filename>",
0497                   name, type, locn);
0498 }
0499 
0500 /******************************************************************************
0501 * Return the repetition text.
0502 */
0503 QString ResourceDataModelBase::repeatText(const KAEvent& event)
0504 {
0505     const QString repText = event.recurrenceText(true);
0506     return repText.isEmpty() ? event.repetitionText(true) : repText;
0507 }
0508 
0509 /******************************************************************************
0510 * Return a string for sorting the repetition column.
0511 */
0512 QString ResourceDataModelBase::repeatOrder(const KAEvent& event)
0513 {
0514     int repOrder = 0;
0515     int repInterval = 0;
0516     if (event.repeatAtLogin())
0517         repOrder = 1;
0518     else
0519     {
0520         repInterval = event.recurInterval();
0521         switch (event.recurType())
0522         {
0523             case KARecurrence::MINUTELY:
0524                 repOrder = 2;
0525                 break;
0526             case KARecurrence::DAILY:
0527                 repOrder = 3;
0528                 break;
0529             case KARecurrence::WEEKLY:
0530                 repOrder = 4;
0531                 break;
0532             case KARecurrence::MONTHLY_DAY:
0533             case KARecurrence::MONTHLY_POS:
0534                 repOrder = 5;
0535                 break;
0536             case KARecurrence::ANNUAL_DATE:
0537             case KARecurrence::ANNUAL_POS:
0538                 repOrder = 6;
0539                 break;
0540             case KARecurrence::NO_RECUR:
0541             default:
0542                 break;
0543         }
0544     }
0545     return QStringLiteral("%1%2").arg(static_cast<char>('0' + repOrder)).arg(repInterval, 8, 10, QLatin1Char('0'));
0546 }
0547 
0548 /******************************************************************************
0549 * Returns the QWhatsThis text for a specified column.
0550 */
0551 QString ResourceDataModelBase::whatsThisText(int column)
0552 {
0553     switch (column)
0554     {
0555         case TimeColumn:
0556             return i18nc("@info:whatsthis", "Next scheduled date and time of the alarm");
0557         case TimeToColumn:
0558             return i18nc("@info:whatsthis", "How long until the next scheduled trigger of the alarm");
0559         case RepeatColumn:
0560             return i18nc("@info:whatsthis", "How often the alarm recurs");
0561         case ColourColumn:
0562             return i18nc("@info:whatsthis", "Background color of alarm message");
0563         case TypeColumn:
0564             return i18nc("@info:whatsthis", "Alarm type (message, file, command or email)");
0565         case NameColumn:
0566             return i18nc("@info:whatsthis", "Alarm name, or alarm text if name is blank");
0567         case TextColumn:
0568             return i18nc("@info:whatsthis", "Alarm message text, URL of text file to display, command to execute, or email subject line");
0569         case TemplateNameColumn:
0570             return i18nc("@info:whatsthis", "Name of the alarm template");
0571         default:
0572             return {};
0573     }
0574 }
0575 
0576 /******************************************************************************
0577 * Return the icon associated with an event's action.
0578 */
0579 QPixmap* ResourceDataModelBase::eventIcon(const KAEvent& event)
0580 {
0581     switch (event.actionTypes())
0582     {
0583         case KAEvent::Action::Email:
0584             return mEmailIcon;
0585         case KAEvent::Action::Audio:
0586             return mAudioIcon;
0587         case KAEvent::Action::Command:
0588             return mCommandIcon;
0589         case KAEvent::Action::Display:
0590             if (event.actionSubType() == KAEvent::SubAction::File)
0591                 return mFileIcon;
0592             [[fallthrough]];    // fall through to DisplayCommand
0593         case KAEvent::Action::DisplayCommand:
0594         default:
0595             return mTextIcon;
0596     }
0597 }
0598 
0599 /******************************************************************************
0600 * Display a message to the user.
0601 */
0602 void ResourceDataModelBase::handleResourceMessage(ResourceType::MessageType type, const QString& message, const QString& details)
0603 {
0604     if (type == ResourceType::MessageType::Error)
0605     {
0606         qCDebug(KALARM_LOG) << "Resource Error!" << message << details;
0607         KAMessageBox::detailedError(Desktop::mainWindow(), message, details);
0608     }
0609     else if (type == ResourceType::MessageType::Info)
0610     {
0611         qCDebug(KALARM_LOG) << "Resource user message:" << message << details;
0612         // KMessageBox::informationList looks bad, so use our own formatting.
0613         const QString msg = details.isEmpty() ? message : message + QStringLiteral("\n\n") + details;
0614         KAMessageBox::information(Desktop::mainWindow(), msg);
0615     }
0616 }
0617 
0618 bool ResourceDataModelBase::isMigrationComplete() const
0619 {
0620     return mMigrationStatus == 1;
0621 }
0622 
0623 bool ResourceDataModelBase::isMigrating() const
0624 {
0625     return mMigrationStatus == 0;
0626 }
0627 
0628 void ResourceDataModelBase::setMigrationInitiated(bool started)
0629 {
0630     mMigrationStatus = (started ? 0 : -1);
0631 }
0632 
0633 void ResourceDataModelBase::setMigrationComplete()
0634 {
0635     mMigrationStatus = 1;
0636     if (mCreationStatus)
0637         Resources::notifyResourcesCreated();
0638 }
0639 
0640 void ResourceDataModelBase::setCalendarsCreated()
0641 {
0642     mCreationStatus = true;
0643     if (mMigrationStatus == 1)
0644         Resources::notifyResourcesCreated();
0645 }
0646 
0647 /******************************************************************************
0648 * Return the alarm time text in the form "date time".
0649 * Parameters:
0650 *   dateTime    = the date/time to format.
0651 *   leadingZero = the character to represent a leading zero, or '\0' for no leading zeroes.
0652 */
0653 QString ResourceDataModelBase::alarmTimeText(const DateTime& dateTime, char leadingZero)
0654 {
0655     // Whether the date and time contain leading zeroes.
0656     static bool    leadingZeroesChecked = false;
0657     static QString timeFormat;      // time format for current locale
0658     static QString timeFullFormat;  // time format with leading zero, if different from 'timeFormat'
0659     static int     hourOffset = 0;  // offset within time string to the hour
0660 
0661     if (!dateTime.isValid())
0662         return i18nc("@info Alarm never occurs", "Never");
0663     QLocale locale;
0664     if (!leadingZeroesChecked)
0665     {
0666         // Check whether the day number and/or hour have no leading zeroes, if
0667         // they are at the start of the date/time. If no leading zeroes, they
0668         // will need to be padded when displayed, so that displayed dates/times
0669         // can be aligned with each other.
0670         // Note that if leading zeroes are not included in other components, no
0671         // alignment will be attempted.
0672 
0673         // Check the time format.
0674         // Remove all but hours, minutes and AM/PM, since alarms are on minute
0675         // boundaries. Preceding separators are also removed.
0676         timeFormat = locale.timeFormat(QLocale::ShortFormat);
0677         for (int del = 0, predel = 0, c = 0;  c < timeFormat.size();  ++c)
0678         {
0679             char ch = timeFormat.at(c).toLatin1();
0680             switch (ch)
0681             {
0682                 case 'H':
0683                 case 'h':
0684                 case 'm':
0685                 case 'a':
0686                 case 'A':
0687                     if (predel == 1)
0688                     {
0689                         timeFormat.remove(del, c - del);
0690                         c = del;
0691                     }
0692                     del = c + 1;   // start deleting from the next character
0693                     if ((ch == 'A'  &&  del < timeFormat.size()  &&  timeFormat.at(del).toLatin1() == 'P')
0694                     ||  (ch == 'a'  &&  del < timeFormat.size()  &&  timeFormat.at(del).toLatin1() == 'p'))
0695                         ++c, ++del;
0696                     predel = -1;
0697                     break;
0698 
0699                 case 's':
0700                 case 'z':
0701                 case 't':
0702                     timeFormat.remove(del, c + 1 - del);
0703                     c = del - 1;
0704                     if (!predel)
0705                         predel = 1;
0706                     break;
0707 
0708                 default:
0709                     break;
0710             }
0711         }
0712 
0713         // 'HH' and 'hh' provide leading zeroes; single 'H' or 'h' provide no
0714         // leading zeroes.
0715         static const QRegularExpression hourReg(QStringLiteral("[hH]"));
0716         int i = timeFormat.indexOf(hourReg);
0717         static const QRegularExpression hourMinAmPmReg(QStringLiteral("[hHmaA]"));
0718         int first = timeFormat.indexOf(hourMinAmPmReg);
0719         if (i >= 0  &&  i == first  &&  (i == timeFormat.size() - 1  ||  timeFormat.at(i) != timeFormat.at(i + 1)))
0720         {
0721             timeFullFormat = timeFormat;
0722             timeFullFormat.insert(i, timeFormat.at(i));
0723             // Find index to hour in formatted times
0724             const QTime t(1,30,30);
0725             const QString nozero = locale.toString(t, timeFormat);
0726             const QString zero   = locale.toString(t, timeFullFormat);
0727             for (int j = 0; j < nozero.size(); ++j)
0728                 if (nozero[j] != zero[j])
0729                 {
0730                     hourOffset = j;
0731                     break;
0732                 }
0733         }
0734 
0735         leadingZeroesChecked = true;
0736     }
0737 
0738     const KADateTime kdt = dateTime.effectiveKDateTime().toTimeSpec(Preferences::timeSpec());
0739     QString dateTimeText = locale.toString(kdt.date(), QLocale::ShortFormat);
0740 
0741     if (!dateTime.isDateOnly()  ||  kdt.utcOffset() != dateTime.utcOffset())
0742     {
0743         // Display the time of day if it's a date/time value, or if it's
0744         // a date-only value but it's in a different time zone
0745         dateTimeText += QLatin1Char(' ');
0746         // Don't try to align right-to-left languages...
0747         const bool useFullFormat = QApplication::isLeftToRight() && leadingZero && !timeFullFormat.isEmpty();
0748         QString timeText = locale.toString(kdt.time(), (useFullFormat ? timeFullFormat : timeFormat));
0749         if (useFullFormat  &&  leadingZero != '0'  &&  timeText.at(hourOffset) == QLatin1Char('0'))
0750             timeText[hourOffset] = QChar::fromLatin1(leadingZero);
0751         dateTimeText += timeText;
0752     }
0753     return dateTimeText + QLatin1Char(' ');
0754 }
0755 
0756 /******************************************************************************
0757 * Return the time-to-alarm text.
0758 */
0759 QString ResourceDataModelBase::timeToAlarmText(const DateTime& dateTime)
0760 {
0761     if (!dateTime.isValid())
0762         return i18nc("@info Alarm never occurs", "Never");
0763     const KADateTime now = KADateTime::currentUtcDateTime();
0764     if (dateTime.isDateOnly())
0765     {
0766         const int days = now.date().daysTo(dateTime.date());
0767         // xgettext: no-c-format
0768         return i18nc("@info n days", "%1d", days);
0769     }
0770     const int mins = (now.secsTo(dateTime.effectiveKDateTime()) + 59) / 60;
0771     if (mins <= 0)
0772         return {};
0773     QLocale locale;
0774     QString minutes = locale.toString(mins % 60);
0775     if (minutes.size() == 1)
0776         minutes.prepend(locale.zeroDigit());
0777     if (mins < 24*60)
0778         return i18nc("@info hours:minutes", "%1:%2", mins/60, minutes);
0779     // If we render a day count, then we zero-pad the hours, to make the days line up and be more scanable.
0780     const int hrs = mins / 60;
0781     QString hours = locale.toString(hrs % 24);
0782     if (hours.size() == 1)
0783         hours.prepend(locale.zeroDigit());
0784     const QString days = locale.toString(hrs / 24);
0785     return i18nc("@info days hours:minutes", "%1d %2:%3", days, hours, minutes);
0786 }
0787 
0788 // vim: et sw=4: