preferences.cpp 16.9 KB
Newer Older
1 2 3
/*
 *  preferences.cpp  -  program preference settings
 *  Program:  kalarm
4
 *  SPDX-FileCopyrightText: 2001-2020 David Jarvie <djarvie@kde.org>
5
 *
6
 *  SPDX-License-Identifier: GPL-2.0-or-later
7 8
 */

9
#include "preferences.h"
David Jarvie's avatar
David Jarvie committed
10

11
#include "kalarm.h"
12
#include "kamail.h"
13
#include "lib/desktop.h"
David Jarvie's avatar
David Jarvie committed
14
#include "lib/messagebox.h"
15
#include "kalarm_debug.h"
David Jarvie's avatar
David Jarvie committed
16

David Jarvie's avatar
David Jarvie committed
17
#include <KAlarmCal/Identities>
18

David Jarvie's avatar
David Jarvie committed
19 20
#include <KIdentityManagement/Identity>
#include <KIdentityManagement/IdentityManager>
John Layt's avatar
John Layt committed
21
#include <KHolidays/HolidayRegion>
David Jarvie's avatar
David Jarvie committed
22

David Jarvie's avatar
David Jarvie committed
23
#include <KSharedConfig>
24 25
#include <KConfigGroup>
#include <KMessageBox>
26

27
#include <QFile>
28
#include <QSaveFile>
29
#include <QDir>
30 31
#include <QStandardPaths>

32
#include <time.h>
33

34 35
using namespace KHolidays;
using namespace KAlarmCal;
36

37 38
//clazy:excludeall=non-pod-global-static

39
// Config file entry names
David Jarvie's avatar
David Jarvie committed
40
static const char* GENERAL_SECTION  = "General";
41

42
// Config file entry name for temporary use
David Jarvie's avatar
David Jarvie committed
43
static const char* TEMP = "Temp";
44

45 46
static const QString AUTOSTART_FILE(QStringLiteral("kalarm.autostart.desktop"));

47
// Values for EmailFrom entry
David Jarvie's avatar
David Jarvie committed
48 49
static const QString FROM_SYS_SETTINGS(QStringLiteral("@SystemSettings"));
static const QString FROM_KMAIL(QStringLiteral("@KMail"));
50

David Jarvie's avatar
David Jarvie committed
51
// Config file entry names for notification messages
David Jarvie's avatar
David Jarvie committed
52 53 54 55
const QLatin1String Preferences::QUIT_WARN("QuitWarn");
const QLatin1String Preferences::ASK_AUTO_START("AskAutoStart");
const QLatin1String Preferences::CONFIRM_ALARM_DELETION("ConfirmAlarmDeletion");
const QLatin1String Preferences::EMAIL_QUEUED_NOTIFY("EmailQueuedNotify");
David Jarvie's avatar
David Jarvie committed
56 57 58 59 60
const bool  default_quitWarn             = true;
const bool  default_emailQueuedNotify    = false;
const bool  default_confirmAlarmDeletion = true;

static QString translateXTermPath(const QString& cmdline, bool write);
David Jarvie's avatar
David Jarvie committed
61

62

Laurent Montel's avatar
Laurent Montel committed
63
Preferences*   Preferences::mInstance = nullptr;
64
bool           Preferences::mUsingDefaults = false;
Laurent Montel's avatar
Laurent Montel committed
65
HolidayRegion* Preferences::mHolidays = nullptr;   // always non-null after Preferences initialisation
66 67
QString        Preferences::mPreviousVersion;
Preferences::Backend Preferences::mPreviousBackend;
David Jarvie's avatar
David Jarvie committed
68 69
// Change tracking
bool           Preferences::mAutoStartChangedByUser = false;
70

David Jarvie's avatar
David Jarvie committed
71 72

Preferences* Preferences::self()
73
{
74 75 76
    if (!mInstance)
    {
        // Set the default button for the Quit warning message box to Cancel
David Jarvie's avatar
David Jarvie committed
77 78 79 80
        KAMessageBox::setContinueDefault(QUIT_WARN, KMessageBox::Cancel);
        KAMessageBox::setDefaultShouldBeShownContinue(QUIT_WARN, default_quitWarn);
        KAMessageBox::setDefaultShouldBeShownContinue(EMAIL_QUEUED_NOTIFY, default_emailQueuedNotify);
        KAMessageBox::setDefaultShouldBeShownContinue(CONFIRM_ALARM_DELETION, default_confirmAlarmDeletion);
81 82 83 84

        mInstance = new Preferences;
    }
    return mInstance;
85 86
}

David Jarvie's avatar
David Jarvie committed
87
Preferences::Preferences()
88
{
Laurent Montel's avatar
Laurent Montel committed
89 90 91 92
    QObject::connect(this, &Preferences::base_StartOfDayChanged, this, &Preferences::startDayChange);
    QObject::connect(this, &Preferences::base_TimeZoneChanged, this, &Preferences::timeZoneChange);
    QObject::connect(this, &Preferences::base_HolidayRegionChanged, this, &Preferences::holidaysChange);
    QObject::connect(this, &Preferences::base_WorkTimeChanged, this, &Preferences::workTimeChange);
93

Laurent Montel's avatar
Laurent Montel committed
94
    load();
95 96 97 98 99 100
    // Fetch the KAlarm version and backend which wrote the previous config file
    mPreviousVersion = version();
    mPreviousBackend = backend();
    // Update the KAlarm version in the config file, but don't call
    // writeConfig() here - leave it to be written only if the config file
    // is updated with other data.
David Jarvie's avatar
David Jarvie committed
101
    setVersion(QStringLiteral(KALARM_VERSION));
102 103
}

104 105 106 107 108 109
/******************************************************************************
* Auto hiding of the system tray icon is only allowed on desktops which provide
* GUI controls to show hidden icons.
*/
int Preferences::autoHideSystemTray()
{
110
    if (noAutoHideSystemTrayDesktops().contains(Desktop::currentIdentityName()))
111 112 113 114 115 116 117 118 119 120 121
        return 0;   // never hide
    return self()->mBase_AutoHideSystemTray;
}

/******************************************************************************
* Auto hiding of the system tray icon is only allowed on desktops which provide
* GUI controls to show hidden icons, so while KAlarm is running on such a
* desktop, don't allow changes to the setting.
*/
void Preferences::setAutoHideSystemTray(int timeout)
{
122
    if (noAutoHideSystemTrayDesktops().contains(Desktop::currentIdentityName()))
123 124 125 126
        return;
    self()->setBase_AutoHideSystemTray(timeout);
}

127 128
void Preferences::setAskAutoStart(bool yes)
{
David Jarvie's avatar
David Jarvie committed
129
    KAMessageBox::saveDontShowAgainYesNo(ASK_AUTO_START, !yes);
130 131
}

132 133 134 135 136 137
/******************************************************************************
* Set the NoAutoStart condition.
* On KDE desktops, the "X-KDE-autostart-condition" entry in
* kalarm.autostart.desktop references this to determine whether to autostart KAlarm.
* On non-KDE desktops, the "X-KDE-autostart-condition" entry in
* kalarm.autostart.desktop doesn't have any effect, so that KAlarm will be
138 139 140
* autostarted even if it is set not to autostart. Adding a "Hidden" entry to,
* and removing the "OnlyShowIn=KDE" entry from, a user-modifiable copy of the
* file fixes this.
141 142 143 144
*/
void Preferences::setNoAutoStart(bool yes)
{
    // Find the existing kalarm.autostart.desktop file, and whether it's writable.
145
    bool existingRO = true;   // whether the existing file is read-only
146
    QString autostartFile;
147
    QString configDirRW;
148
    const QStringList autostartDirs = QStandardPaths::standardLocations(QStandardPaths::GenericConfigLocation);
Laurent Montel's avatar
Laurent Montel committed
149
    for (const QString& dir : autostartDirs)
150
    {
151
        const QString file = dir + QLatin1String("/autostart/") + AUTOSTART_FILE;
152 153 154 155 156 157
        if (QFile::exists(file))
        {
            QFileInfo info(file);
            if (info.isReadable())
            {
                autostartFile = file;
158
                existingRO = !info.isWritable();
159 160
                if (!existingRO)
                    configDirRW = dir;
161 162 163 164 165
                break;
            }
        }
    }

166 167 168
    // If the existing file isn't writable, find the path to create a writable copy
    QString autostartFileRW = autostartFile;
    if (existingRO)
169
    {
170 171 172
        configDirRW = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation);
        autostartFileRW = configDirRW + QLatin1String("/autostart/") + AUTOSTART_FILE;
        if (configDirRW.isEmpty())
173
        {
174
            qCWarning(KALARM_LOG) << "Preferences::setNoAutoStart: No writable autostart file path";
175 176 177 178 179
            return;
        }
        if (QFile::exists(autostartFileRW))
        {
            QFileInfo info(autostartFileRW);
180 181
            if (!info.isReadable() || !info.isWritable())
            {
182
                qCWarning(KALARM_LOG) << "Preferences::setNoAutoStart: Autostart file is not read/write:" << autostartFileRW;
183 184 185 186 187
                return;
            }
        }
    }

188 189 190
    // Read the existing file and remove any "Hidden=" and "OnlyShowIn=" entries
    bool update = false;
    QStringList lines;
191
    {
192 193 194
        QFile file(autostartFile);
        if (!file.open(QIODevice::ReadOnly))
        {
195
            qCWarning(KALARM_LOG) << "Preferences::setNoAutoStart: Error reading autostart file:" << autostartFile;
196 197
            return;
        }
198 199 200
        QTextStream stream(&file);
        stream.setCodec("UTF-8");
        stream.setAutoDetectUnicode(true);
201 202
        lines = stream.readAll().split(QLatin1Char('\n'));
        for (int i = 0; i < lines.size(); ++i)
203
        {
David Jarvie's avatar
David Jarvie committed
204
            const QString line = lines.at(i).trimmed();
205
            if (line.isEmpty())
206
            {
207 208
                lines.removeAt(i);
                --i;
209
            }
210 211
            else if (line.startsWith(QLatin1String("Hidden="))
                 ||  line.startsWith(QLatin1String("OnlyShowIn=")))
212
            {
213 214 215
                lines.removeAt(i);
                update = true;
                --i;
216 217
            }
        }
218 219 220 221 222 223 224 225 226 227 228 229
    }

    if (yes)
    {
        // Add a "Hidden" entry to the local kalarm.autostart.desktop file, to
        // prevent autostart from happening.
        lines += QStringLiteral("Hidden=true");
        update = true;
    }
    if (update)
    {
        // Write the updated file
230 231 232 233 234 235 236 237 238 239
        QFileInfo info(configDirRW + QLatin1String("/autostart"));
        if (!info.exists())
        {
            // First, create the directory for it.
            if (!QDir(configDirRW).mkdir(QStringLiteral("autostart")))
            {
                qCWarning(KALARM_LOG) << "Preferences::setNoAutoStart: Error creating autostart file directory:" << info.filePath();
                return;
            }
        }
240
        QSaveFile file(autostartFileRW);
241
        if (!file.open(QIODevice::WriteOnly))
242
        {
243
            qCWarning(KALARM_LOG) << "Preferences::setNoAutoStart: Error writing autostart file:" << autostartFileRW;
244
            return;
245
        }
246 247 248
        QTextStream stream(&file);
        stream.setCodec("UTF-8");
        stream << lines.join(QLatin1Char('\n')) << "\n";
249 250 251
        // QSaveFile doesn't report a write error when the device is full (see Qt
        // bug 75077), so check that the data can actually be written by flush().
        if (!file.flush()  ||  !file.commit())   // save the file
252 253 254 255
        {
            qCWarning(KALARM_LOG) << "Preferences::setNoAutoStart: Error writing autostart file:" << autostartFileRW;
            return;
        }
256
        qCDebug(KALARM_LOG) << "Preferences::setNoAutoStart: Written" << autostartFileRW;
257
    }
258

259 260 261
    self()->setBase_NoAutoStart(yes);
}

262
/******************************************************************************
David Jarvie's avatar
David Jarvie committed
263
* Get the user's time zone, or if none has been chosen, the system time zone.
264
* Reply = time zone, or invalid to use the local time zone.
265
*/
266
KADateTime::Spec Preferences::timeSpec()
267
{
268 269
    const QByteArray zoneId = self()->mBase_TimeZone.toLatin1();
    return zoneId.isEmpty() ? KADateTime::LocalZone : KADateTime::Spec(QTimeZone(zoneId));
270 271
}

272
QTimeZone Preferences::timeSpecAsZone()
273
{
274 275
    const QByteArray zoneId = self()->mBase_TimeZone.toLatin1();
    return zoneId.isEmpty() ? QTimeZone::systemTimeZone() : QTimeZone(zoneId);
276 277
}

278
void Preferences::setTimeSpec(const KADateTime::Spec& spec)
279
{
280
    self()->setBase_TimeZone(spec.type() == KADateTime::TimeZone ? QString::fromLatin1(spec.timeZone().id()) : QString());
David Jarvie's avatar
David Jarvie committed
281
}
Stephan Kulow's avatar
Stephan Kulow committed
282

283 284
void Preferences::timeZoneChange(const QString& zone)
{
285
    Q_UNUSED(zone);
286
    Q_EMIT mInstance->timeZoneChanged(timeSpec());
287 288
}

Allen Winter's avatar
Allen Winter committed
289
const HolidayRegion& Preferences::holidays()
David Jarvie's avatar
David Jarvie committed
290
{
291 292 293 294 295 296 297
    QString regionCode = self()->mBase_HolidayRegion;
    if (!mHolidays  ||  mHolidays->regionCode() != regionCode)
    {
        delete mHolidays;
        mHolidays = new HolidayRegion(regionCode);
    }
    return *mHolidays;
David Jarvie's avatar
David Jarvie committed
298 299 300 301
}

void Preferences::setHolidayRegion(const QString& regionCode)
{
302
    self()->setBase_HolidayRegion(regionCode);
David Jarvie's avatar
David Jarvie committed
303 304 305 306
}

void Preferences::holidaysChange(const QString& regionCode)
{
307
    Q_UNUSED(regionCode);
Laurent Montel's avatar
Laurent Montel committed
308
    Q_EMIT mInstance->holidaysChanged(holidays());
David Jarvie's avatar
David Jarvie committed
309 310
}

David Jarvie's avatar
David Jarvie committed
311
void Preferences::setStartOfDay(const QTime& t)
David Jarvie's avatar
David Jarvie committed
312
{
313 314 315
    if (t != self()->mBase_StartOfDay.time())
    {
        self()->setBase_StartOfDay(QDateTime(QDate(1900,1,1), t));
Laurent Montel's avatar
Laurent Montel committed
316
        Q_EMIT mInstance->startOfDayChanged(t);
317
    }
David Jarvie's avatar
David Jarvie committed
318 319
}

David Jarvie's avatar
David Jarvie committed
320 321
// Called when the start of day value has changed in the config file
void Preferences::startDayChange(const QDateTime& dt)
David Jarvie's avatar
David Jarvie committed
322
{
Laurent Montel's avatar
Laurent Montel committed
323
    Q_EMIT mInstance->startOfDayChanged(dt.time());
324 325
}

326 327
QBitArray Preferences::workDays()
{
328 329 330 331 332
    unsigned days = self()->base_WorkDays();
    QBitArray dayBits(7);
    for (int i = 0;  i < 7;  ++i)
        dayBits.setBit(i, days & (1 << i));
    return dayBits;
333 334 335 336
}

void Preferences::setWorkDays(const QBitArray& dayBits)
{
David Jarvie's avatar
David Jarvie committed
337 338 339 340 341
    if (dayBits.size() != 7)
    {
        qCWarning(KALARM_LOG) << "Preferences::setWorkDays: Error! 'dayBits' parameter must have 7 elements: actual size" << dayBits.size();
        return;
    }
342 343 344 345 346
    unsigned days = 0;
    for (int i = 0;  i < 7;  ++i)
        if (dayBits.testBit(i))
            days |= 1 << i;
    self()->setBase_WorkDays(days);
347 348 349 350
}

void Preferences::workTimeChange(const QDateTime& start, const QDateTime& end, int days)
{
351 352 353 354
    QBitArray dayBits(7);
    for (int i = 0;  i < 7;  ++i)
        if (days & (1 << i))
            dayBits.setBit(i);
Laurent Montel's avatar
Laurent Montel committed
355
    Q_EMIT mInstance->workTimeChanged(start.time(), end.time(), dayBits);
356 357
}

David Jarvie's avatar
David Jarvie committed
358
Preferences::MailFrom Preferences::emailFrom()
359
{
David Jarvie's avatar
David Jarvie committed
360
    const QString from = self()->mBase_EmailFrom;
361 362 363 364 365
    if (from == FROM_KMAIL)
        return MAIL_FROM_KMAIL;
    if (from == FROM_SYS_SETTINGS)
        return MAIL_FROM_SYS_SETTINGS;
    return MAIL_FROM_ADDR;
366 367 368
}

/******************************************************************************
369
* Get user's default 'From' email address.
370
*/
371
QString Preferences::emailAddress()
372
{
David Jarvie's avatar
David Jarvie committed
373
    const QString from = self()->mBase_EmailFrom;
374
    if (from == FROM_KMAIL)
375
        return Identities::identityManager()->defaultIdentity().fullEmailAddr();
376 377 378
    if (from == FROM_SYS_SETTINGS)
        return KAMail::controlCentreAddress();
    return from;
David Jarvie's avatar
David Jarvie committed
379 380 381 382
}

void Preferences::setEmailAddress(Preferences::MailFrom from, const QString& address)
{
383 384 385 386 387 388 389 390 391
    QString out;
    switch (from)
    {
        case MAIL_FROM_KMAIL:        out = FROM_KMAIL; break;
        case MAIL_FROM_SYS_SETTINGS: out = FROM_SYS_SETTINGS; break;
        case MAIL_FROM_ADDR:         out = address; break;
        default:  return;
    }
    self()->setBase_EmailFrom(out);
David Jarvie's avatar
David Jarvie committed
392 393 394 395
}

Preferences::MailFrom Preferences::emailBccFrom()
{
David Jarvie's avatar
David Jarvie committed
396
    const QString from = self()->mBase_EmailBccAddress;
397 398 399
    if (from == FROM_SYS_SETTINGS)
        return MAIL_FROM_SYS_SETTINGS;
    return MAIL_FROM_ADDR;
400 401
}

402
QString Preferences::emailBccAddress()
403
{
David Jarvie's avatar
David Jarvie committed
404
    const QString from = self()->mBase_EmailBccAddress;
405 406 407
    if (from == FROM_SYS_SETTINGS)
        return KAMail::controlCentreAddress();
    return from;
408 409
}

410
bool Preferences::emailBccUseSystemSettings()
411
{
412
    return self()->mBase_EmailBccAddress == FROM_SYS_SETTINGS;
413
}
David Jarvie's avatar
David Jarvie committed
414

415
void Preferences::setEmailBccAddress(bool useSystemSettings, const QString& address)
David Jarvie's avatar
David Jarvie committed
416
{
417 418 419 420 421 422
    QString out;
    if (useSystemSettings)
        out = FROM_SYS_SETTINGS;
    else
        out = address;
    self()->setBase_EmailBccAddress(out);
David Jarvie's avatar
David Jarvie committed
423 424 425 426
}

QString Preferences::cmdXTermCommand()
{
427
    return translateXTermPath(self()->mBase_CmdXTermCommand, false);
David Jarvie's avatar
David Jarvie committed
428 429 430 431
}

void Preferences::setCmdXTermCommand(const QString& cmd)
{
432
    self()->setBase_CmdXTermCommand(translateXTermPath(cmd, true));
David Jarvie's avatar
David Jarvie committed
433 434 435 436 437
}


void Preferences::connect(const char* signal, const QObject* receiver, const char* member)
{
438
    QObject::connect(self(), signal, receiver, member);
David Jarvie's avatar
David Jarvie committed
439 440
}

David Jarvie's avatar
David Jarvie committed
441
/******************************************************************************
442
* Called to allow or suppress output of the specified message dialog, where the
David Jarvie's avatar
David Jarvie committed
443 444
* dialog has a checkbox to turn notification off.
*/
445
void Preferences::setNotify(const QString& messageID, bool notify)
David Jarvie's avatar
David Jarvie committed
446
{
447
    KAMessageBox::saveDontShowAgainContinue(messageID, !notify);
David Jarvie's avatar
David Jarvie committed
448 449 450 451 452
}

/******************************************************************************
* Return whether the specified message dialog is output, where the dialog has
* a checkbox to turn notification off.
David Jarvie's avatar
David Jarvie committed
453
* Reply = false if message has been suppressed (by preferences or by selecting
454
*               "don't ask again")
David Jarvie's avatar
David Jarvie committed
455
*       = true in all other cases.
David Jarvie's avatar
David Jarvie committed
456
*/
457
bool Preferences::notifying(const QString& messageID)
David Jarvie's avatar
David Jarvie committed
458
{
459
    return KAMessageBox::shouldBeShownContinue(messageID);
David Jarvie's avatar
David Jarvie committed
460
}
461

462 463 464 465 466 467 468 469 470
/******************************************************************************
* Translate an X terminal command path to/from config file format.
* Note that only a home directory specification at the start of the path is
* translated, so there's no need to worry about missing out some of the
* executable's path due to quotes etc.
* N.B. Calling KConfig::read/writePathEntry() on the entire command line
*      causes a crash on some systems, so it's necessary to extract the
*      executable path first before processing.
*/
David Jarvie's avatar
David Jarvie committed
471
QString translateXTermPath(const QString& cmdline, bool write)
472
{
473 474 475 476 477
    QString params;
    QString cmd = cmdline;
    if (cmdline.isEmpty())
        return cmdline;
    // Strip any leading quote
David Jarvie's avatar
David Jarvie committed
478 479 480
    const QChar quote = cmdline[0];
    const char q = quote.toLatin1();
    const bool quoted = (q == '"' || q == '\'');
481 482 483 484 485
    if (quoted)
        cmd = cmdline.mid(1);
    // Split the command at the first non-escaped space
    for (int i = 0, count = cmd.length();  i < count;  ++i)
    {
David Jarvie's avatar
David Jarvie committed
486
        switch (cmd.at(i).toLatin1())
487 488 489 490 491 492
        {
            case '\\':
                ++i;
                continue;
            case '"':
            case '\'':
David Jarvie's avatar
David Jarvie committed
493
                if (cmd.at(i) != quote)
494 495
                    continue;
                // fall through to ' '
496
                Q_FALLTHROUGH();
497 498
            case ' ':
                params = cmd.mid(i);
499
                cmd.truncate(i);
500 501 502 503 504 505 506 507
                break;
            default:
                continue;
        }
        break;
    }
    // Translate any home directory specification at the start of the
    // executable's path.
508
    KConfigGroup group(KSharedConfig::openConfig(), GENERAL_SECTION);
509 510 511 512 513 514 515 516 517 518 519 520 521 522 523
    if (write)
    {
        group.writePathEntry(TEMP, cmd);
        cmd = group.readEntry(TEMP, QString());
    }
    else
    {
        group.writeEntry(TEMP, cmd);
        cmd = group.readPathEntry(TEMP, QString());
    }
    group.deleteEntry(TEMP);
    if (quoted)
        return quote + cmd + params;
    else
        return cmd + params;
524
}
Laurent Montel's avatar
Laurent Montel committed
525

526
// vim: et sw=4: