File indexing completed on 2024-04-21 14:54:22

0001 /*
0002     SPDX-FileCopyrightText: 2000-2003 Matthias Hoelzer-Kluepfel <mhk@kde.org>
0003     SPDX-FileCopyrightText: 2000-2003 Tobias Koenig <tokoe@kde.org>
0004     SPDX-FileCopyrightText: 2000-2003 Daniel Molkentin <molkentin@kde.org>
0005     SPDX-FileCopyrightText: 2008 Urs Wolfer <uwolfer @ kde.org>
0006 
0007     SPDX-License-Identifier: MIT
0008 */
0009 
0010 #include "ktipdialog.h"
0011 #include "kconfigwidgets_debug.h"
0012 
0013 #if KCONFIGWIDGETS_BUILD_DEPRECATED_SINCE(5, 83)
0014 
0015 #include <QApplication>
0016 #include <QCheckBox>
0017 #include <QFile>
0018 #include <QHBoxLayout>
0019 #include <QKeyEvent>
0020 #include <QLabel>
0021 #include <QPushButton>
0022 #include <QRandomGenerator>
0023 #include <QRegularExpression>
0024 #include <QScreen>
0025 #include <QTextBrowser>
0026 #include <QVBoxLayout>
0027 
0028 #include <KConfig>
0029 #include <KConfigGroup>
0030 #include <KLocalizedString>
0031 #include <KRandom>
0032 #include <KSeparator>
0033 #include <KSharedConfig>
0034 #include <KStandardGuiItem>
0035 
0036 class KTipDatabasePrivate
0037 {
0038 public:
0039     void loadTips(const QString &tipFile);
0040     void addTips(const QString &tipFile);
0041 
0042     QStringList tips;
0043     int currentTip;
0044 };
0045 
0046 void KTipDatabasePrivate::loadTips(const QString &tipFile)
0047 {
0048     tips.clear();
0049     addTips(tipFile);
0050 }
0051 
0052 /**
0053  * If you change something here, please update the script
0054  * preparetips5, which depends on extracting exactly the same
0055  * text as done here.
0056  */
0057 void KTipDatabasePrivate::addTips(const QString &tipFile)
0058 {
0059     const QString fileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, tipFile);
0060 
0061     if (fileName.isEmpty()) {
0062         qCDebug(KCONFIG_WIDGETS_LOG) << "KTipDatabase::addTips: can't find '" << tipFile << "' in standard dirs";
0063         return;
0064     }
0065 
0066     QFile file(fileName);
0067     if (!file.open(QIODevice::ReadOnly)) {
0068         qCDebug(KCONFIG_WIDGETS_LOG) << "KTipDatabase::addTips: can't open '" << fileName << "' for reading";
0069         return;
0070     }
0071 
0072     QByteArray data = file.readAll();
0073     QString content = QString::fromUtf8(data.constData(), data.size());
0074     const QRegularExpression rx(QStringLiteral("\\n{2,}"));
0075 
0076     int pos = -1;
0077     const QLatin1Char newline('\n');
0078     while ((pos = content.indexOf(QLatin1String("<html>"), pos + 1, Qt::CaseInsensitive)) != -1) {
0079         /**
0080          * To make translations work, tip extraction here must exactly
0081          * match what is done by the preparetips5 script.
0082          */
0083         QString tip = content.mid(pos + 6, content.indexOf(QLatin1String("</html>"), pos, Qt::CaseInsensitive) - pos - 6).replace(rx, QStringLiteral("\n"));
0084 
0085         if (!tip.endsWith(newline)) {
0086             tip += newline;
0087         }
0088 
0089         if (tip.startsWith(newline)) {
0090             tip.remove(0, 1);
0091         }
0092 
0093         if (tip.isEmpty()) {
0094             qCDebug(KCONFIG_WIDGETS_LOG) << "Empty tip found! Skipping! " << pos;
0095             continue;
0096         }
0097 
0098         tips.append(tip);
0099     }
0100 
0101     file.close();
0102 }
0103 
0104 KTipDatabase::KTipDatabase(const QString &_tipFile)
0105     : d(new KTipDatabasePrivate)
0106 {
0107     QString tipFile = _tipFile;
0108 
0109     if (tipFile.isEmpty()) {
0110         tipFile = QCoreApplication::applicationName() + QStringLiteral("/tips");
0111     }
0112 
0113     d->loadTips(tipFile);
0114 
0115     if (!d->tips.isEmpty()) {
0116         d->currentTip = QRandomGenerator::global()->bounded(d->tips.count());
0117     }
0118 }
0119 
0120 KTipDatabase::KTipDatabase(const QStringList &tipsFiles)
0121     : d(new KTipDatabasePrivate)
0122 {
0123     if (tipsFiles.isEmpty() || ((tipsFiles.count() == 1) && tipsFiles.first().isEmpty())) {
0124         d->addTips(QCoreApplication::applicationName() + QStringLiteral("/tips"));
0125     } else {
0126         for (QStringList::ConstIterator it = tipsFiles.begin(); it != tipsFiles.end(); ++it) {
0127             d->addTips(*it);
0128         }
0129     }
0130 
0131     if (!d->tips.isEmpty()) {
0132         d->currentTip = QRandomGenerator::global()->bounded(d->tips.count());
0133     }
0134 }
0135 
0136 KTipDatabase::~KTipDatabase() = default;
0137 
0138 void KTipDatabase::nextTip()
0139 {
0140     if (d->tips.isEmpty()) {
0141         return;
0142     }
0143 
0144     d->currentTip += 1;
0145 
0146     if (d->currentTip >= d->tips.count()) {
0147         d->currentTip = 0;
0148     }
0149 }
0150 
0151 void KTipDatabase::prevTip()
0152 {
0153     if (d->tips.isEmpty()) {
0154         return;
0155     }
0156 
0157     d->currentTip -= 1;
0158 
0159     if (d->currentTip < 0) {
0160         d->currentTip = d->tips.count() - 1;
0161     }
0162 }
0163 
0164 QString KTipDatabase::tip() const
0165 {
0166     if (d->tips.isEmpty()) {
0167         return QString();
0168     }
0169 
0170     return d->tips[d->currentTip];
0171 }
0172 
0173 class KTipDialogPrivate
0174 {
0175 public:
0176     KTipDialogPrivate(KTipDialog *qq)
0177         : q(qq)
0178     {
0179     }
0180     ~KTipDialogPrivate()
0181     {
0182         delete database;
0183     }
0184 
0185     void _k_nextTip();
0186     void _k_prevTip();
0187     void _k_showOnStart(bool);
0188 
0189     KTipDialog *const q;
0190     KTipDatabase *database;
0191     QCheckBox *tipOnStart;
0192     QTextBrowser *tipText;
0193 
0194     static KTipDialog *mInstance;
0195 };
0196 
0197 KTipDialog *KTipDialogPrivate::mInstance = nullptr;
0198 
0199 void KTipDialogPrivate::_k_prevTip()
0200 {
0201     database->prevTip();
0202     tipText->setHtml(
0203         QStringLiteral("<html><body>%1</body></html>").arg(i18nd(KLocalizedString::applicationDomain().constData(), database->tip().toUtf8().constData())));
0204 }
0205 
0206 void KTipDialogPrivate::_k_nextTip()
0207 {
0208     database->nextTip();
0209     tipText->setHtml(
0210         QStringLiteral("<html><body>%1</body></html>").arg(i18nd(KLocalizedString::applicationDomain().constData(), database->tip().toUtf8().constData())));
0211 }
0212 
0213 void KTipDialogPrivate::_k_showOnStart(bool on)
0214 {
0215     q->setShowOnStart(on);
0216 }
0217 
0218 KTipDialog::KTipDialog(KTipDatabase *database, QWidget *parent)
0219     : QDialog(parent)
0220     , d(new KTipDialogPrivate(this))
0221 {
0222     setWindowTitle(i18nc("@title:window", "Tip of the Day"));
0223 
0224     /**
0225      * Parent is 0L when TipDialog is used as a mainWidget. This should
0226      * be the case only in ktip, so let's use the ktip layout.
0227      */
0228     bool isTipDialog = (parent != nullptr);
0229 
0230     d->database = database;
0231 
0232     setWindowIcon(QIcon::fromTheme(QStringLiteral("ktip")));
0233 
0234     QVBoxLayout *mainLayout = new QVBoxLayout(this);
0235 
0236     if (isTipDialog) {
0237         QLabel *titleLabel = new QLabel(this);
0238         titleLabel->setText(i18nc("@title", "Did you know...?\n"));
0239         titleLabel->setFont(QFont(qApp->font().family(), 20, QFont::Bold));
0240         titleLabel->setAlignment(Qt::AlignCenter);
0241         mainLayout->addWidget(titleLabel);
0242     }
0243 
0244     QHBoxLayout *browserLayout = new QHBoxLayout();
0245     mainLayout->addLayout(browserLayout);
0246 
0247     d->tipText = new QTextBrowser(this);
0248 
0249     d->tipText->setOpenExternalLinks(true);
0250 
0251     d->tipText->setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
0252 
0253     QStringList paths;
0254     paths << QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("icons"), QStandardPaths::LocateDirectory)
0255           << QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kdewizard/pics"), QStandardPaths::LocateDirectory);
0256 
0257     d->tipText->setSearchPaths(paths);
0258 
0259     d->tipText->setFrameStyle(QFrame::NoFrame);
0260     d->tipText->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0261     QPalette tipPal(d->tipText->palette());
0262     tipPal.setColor(QPalette::Base, Qt::transparent);
0263     tipPal.setColor(QPalette::Text, tipPal.color(QPalette::WindowText));
0264     d->tipText->setPalette(tipPal);
0265 
0266     browserLayout->addWidget(d->tipText);
0267 
0268     QLabel *label = new QLabel(this);
0269     label->setPixmap(QStringLiteral(":/kconfigwidgets/pics/ktip-bulb.png"));
0270     label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
0271     browserLayout->addWidget(label);
0272 
0273     if (!isTipDialog) {
0274         resize(520, 280);
0275         QSize sh = size();
0276 
0277         QScreen *screen = QGuiApplication::screenAt(QCursor::pos());
0278         if (screen) {
0279             const QRect rect = screen->geometry();
0280             move(rect.x() + (rect.width() - sh.width()) / 2, rect.y() + (rect.height() - sh.height()) / 2);
0281         }
0282     }
0283 
0284     KSeparator *sep = new KSeparator(Qt::Horizontal);
0285     mainLayout->addWidget(sep);
0286 
0287     QHBoxLayout *buttonLayout = new QHBoxLayout();
0288 
0289     mainLayout->addLayout(buttonLayout);
0290 
0291     d->tipOnStart = new QCheckBox(i18nc("@option:check", "&Show tips on startup"));
0292     buttonLayout->addWidget(d->tipOnStart, 1);
0293 
0294     QPushButton *prev = new QPushButton;
0295     KGuiItem::assign(prev, KStandardGuiItem::back(KStandardGuiItem::UseRTL));
0296     prev->setText(i18nc("@action:button Goes to previous tip", "&Previous"));
0297     buttonLayout->addWidget(prev);
0298 
0299     QPushButton *next = new QPushButton;
0300     KGuiItem::assign(next, KStandardGuiItem::forward(KStandardGuiItem::UseRTL));
0301     next->setText(i18nc("@action:button Goes to next tip, opposite to previous", "&Next"));
0302     buttonLayout->addWidget(next);
0303 
0304     QPushButton *ok = new QPushButton;
0305     KGuiItem::assign(ok, KStandardGuiItem::close());
0306     ok->setDefault(true);
0307     buttonLayout->addWidget(ok);
0308 
0309     KConfigGroup config(KSharedConfig::openConfig(), "TipOfDay");
0310     d->tipOnStart->setChecked(config.readEntry("RunOnStart", true));
0311 
0312     connect(next, &QPushButton::clicked, this, [this]() {
0313         d->_k_nextTip();
0314     });
0315     connect(prev, &QPushButton::clicked, this, [this]() {
0316         d->_k_prevTip();
0317     });
0318     connect(ok, &QAbstractButton::clicked, this, &QDialog::accept);
0319     connect(d->tipOnStart, &QCheckBox::toggled, this, [this](bool state) {
0320         d->_k_showOnStart(state);
0321     });
0322 
0323     ok->setFocus();
0324 
0325     d->_k_nextTip();
0326 }
0327 
0328 KTipDialog::~KTipDialog()
0329 {
0330     if (KTipDialogPrivate::mInstance == this) {
0331         KTipDialogPrivate::mInstance = nullptr;
0332     }
0333 }
0334 
0335 /**
0336  * use the one with a parent window as parameter instead of this one
0337  * or you will get focus problems
0338  */
0339 void KTipDialog::showTip(const QString &tipFile, bool force)
0340 {
0341     showTip(nullptr, tipFile, force);
0342 }
0343 
0344 void KTipDialog::showTip(QWidget *parent, const QString &tipFile, bool force)
0345 {
0346     showMultiTip(parent, QStringList(tipFile), force);
0347 }
0348 
0349 void KTipDialog::showMultiTip(QWidget *parent, const QStringList &tipFiles, bool force)
0350 {
0351     KConfigGroup configGroup(KSharedConfig::openConfig(), "TipOfDay");
0352 
0353     const bool runOnStart = configGroup.readEntry("RunOnStart", true);
0354 
0355     if (!force) {
0356         if (!runOnStart) {
0357             return;
0358         }
0359 
0360         // showing the tooltips on startup suggests the tooltip
0361         // will be shown *each time* on startup, not $random days later
0362         // TODO either remove or uncomment this code, but make the situation clear
0363         /*bool hasLastShown = configGroup.hasKey( "TipLastShown" );
0364         if ( hasLastShown ) {
0365           const int oneDay = 24 * 60 * 60;
0366           QDateTime lastShown = configGroup.readEntry( "TipLastShown", QDateTime() );
0367 
0368           // Show tip roughly once a week
0369           if ( lastShown.secsTo( QDateTime::currentDateTime() ) < (oneDay + (QRandomGenerator::global()->bounded(10 * oneDay))) )
0370             return;
0371         }
0372 
0373         configGroup.writeEntry( "TipLastShown", QDateTime::currentDateTime() );
0374 
0375         if ( !hasLastShown )
0376           return; // Don't show tip on first start*/
0377     }
0378 
0379     if (!KTipDialogPrivate::mInstance) {
0380         KTipDialogPrivate::mInstance = new KTipDialog(new KTipDatabase(tipFiles), parent);
0381     } else
0382     // The application might have changed the RunOnStart option in its own
0383     // configuration dialog, so we should update the checkbox.
0384     {
0385         KTipDialogPrivate::mInstance->d->tipOnStart->setChecked(runOnStart);
0386     }
0387 
0388     KTipDialogPrivate::mInstance->show();
0389     KTipDialogPrivate::mInstance->raise();
0390 }
0391 
0392 void KTipDialog::setShowOnStart(bool on)
0393 {
0394     KConfigGroup config(KSharedConfig::openConfig(), "TipOfDay");
0395     config.writeEntry("RunOnStart", on);
0396 }
0397 
0398 bool KTipDialog::eventFilter(QObject *object, QEvent *event)
0399 {
0400     if (object == d->tipText && event->type() == QEvent::KeyPress
0401         && (((QKeyEvent *)event)->key() == Qt::Key_Return || ((QKeyEvent *)event)->key() == Qt::Key_Space)) {
0402         accept();
0403     }
0404 
0405     /**
0406      * If the user presses Return or Space, we close the dialog as if the
0407      * default button was pressed even if the QTextBrowser has the keyboard
0408      * focus. This could have the bad side-effect that the user cannot use the
0409      * keyboard to open urls in the QTextBrowser, so we just let it handle
0410      * the key event _additionally_. (Antonio)
0411      */
0412 
0413     return QWidget::eventFilter(object, event);
0414 }
0415 
0416 #include "moc_ktipdialog.cpp"
0417 #endif