File indexing completed on 2024-05-12 05:21:22

0001 /*
0002   This file is part of KOrganizer.
0003 
0004   SPDX-FileCopyrightText: 2004 Tobias Koenig <tokoe@kde.org>
0005   SPDX-FileCopyrightText: 2004 Cornelius Schumacher <schumacher@kde.org>
0006 
0007   SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 #include "kcmdesignerfields.h"
0011 
0012 #include "korganizer_debug.h"
0013 #include <KAboutData>
0014 #include <KDialogJobUiDelegate>
0015 #include <KDirWatch>
0016 #include <KIO/CommandLauncherJob>
0017 #include <KIO/FileCopyJob>
0018 #include <KJobWidgets>
0019 #include <KLocalizedString>
0020 #include <KMessageBox>
0021 #include <KShell>
0022 #include <QFileDialog>
0023 
0024 #include <QDir>
0025 #include <QGroupBox>
0026 #include <QHBoxLayout>
0027 #include <QHeaderView>
0028 #include <QLabel>
0029 #include <QPushButton>
0030 #include <QStandardPaths>
0031 #include <QTreeWidget>
0032 #include <QUiLoader>
0033 #include <QWhatsThis>
0034 
0035 class PageItem : public QTreeWidgetItem
0036 {
0037 public:
0038     PageItem(QTreeWidget *parent, const QString &path)
0039         : QTreeWidgetItem(parent)
0040         , mPath(path)
0041     {
0042         setFlags(flags() | Qt::ItemIsUserCheckable);
0043         setCheckState(0, Qt::Unchecked);
0044         mName = path.mid(path.lastIndexOf(QLatin1Char('/')) + 1);
0045 
0046         QFile f(mPath);
0047         if (!f.open(QFile::ReadOnly)) {
0048             return;
0049         }
0050         QUiLoader builder;
0051         QWidget *wdg = builder.load(&f, nullptr);
0052         f.close();
0053         if (wdg) {
0054             setText(0, wdg->windowTitle());
0055 
0056             QPixmap pm = wdg->grab();
0057             const QImage img = pm.toImage().scaled(300, 300, Qt::KeepAspectRatio, Qt::SmoothTransformation);
0058             mPreview = QPixmap::fromImage(img);
0059 
0060             QMap<QString, QString> allowedTypes;
0061             allowedTypes.insert(QStringLiteral("QLineEdit"), i18n("Text"));
0062             allowedTypes.insert(QStringLiteral("QTextEdit"), i18n("Text"));
0063             allowedTypes.insert(QStringLiteral("QSpinBox"), i18n("Numeric Value"));
0064             allowedTypes.insert(QStringLiteral("QCheckBox"), i18n("Boolean"));
0065             allowedTypes.insert(QStringLiteral("QComboBox"), i18n("Selection"));
0066             allowedTypes.insert(QStringLiteral("QDateTimeEdit"), i18n("Date & Time"));
0067             allowedTypes.insert(QStringLiteral("KLineEdit"), i18n("Text"));
0068             allowedTypes.insert(QStringLiteral("KTextEdit"), i18n("Text"));
0069             allowedTypes.insert(QStringLiteral("KDateTimeWidget"), i18n("Date & Time"));
0070             allowedTypes.insert(QStringLiteral("KDatePicker"), i18n("Date"));
0071 
0072             const QList<QWidget *> list = wdg->findChildren<QWidget *>();
0073             for (QWidget *it : list) {
0074                 if (allowedTypes.contains(QLatin1StringView(it->metaObject()->className()))) {
0075                     const QString objectName = it->objectName();
0076                     if (objectName.startsWith(QLatin1StringView("X_"))) {
0077                         new QTreeWidgetItem(this,
0078                                             QStringList() << objectName << allowedTypes[QLatin1StringView(it->metaObject()->className())]
0079                                                           << QLatin1StringView(it->metaObject()->className()) << it->whatsThis());
0080                     }
0081                 }
0082             }
0083         }
0084     }
0085 
0086     [[nodiscard]] QString name() const
0087     {
0088         return mName;
0089     }
0090 
0091     [[nodiscard]] QString path() const
0092     {
0093         return mPath;
0094     }
0095 
0096     [[nodiscard]] QPixmap preview() const
0097     {
0098         return mPreview;
0099     }
0100 
0101     void setIsActive(bool isActive)
0102     {
0103         mIsActive = isActive;
0104     }
0105 
0106     [[nodiscard]] bool isActive() const
0107     {
0108         return mIsActive;
0109     }
0110 
0111     [[nodiscard]] bool isOn() const
0112     {
0113         return checkState(0) == Qt::Checked;
0114     }
0115 
0116 private:
0117     QString mName;
0118     const QString mPath;
0119     QPixmap mPreview;
0120     bool mIsActive = false;
0121 };
0122 
0123 KCMDesignerFields::KCMDesignerFields(QObject *parent, const KPluginMetaData &data)
0124     : KCModule(parent, data)
0125 {
0126 }
0127 
0128 void KCMDesignerFields::delayedInit()
0129 {
0130     qCDebug(KORGANIZER_LOG) << "KCMDesignerFields::delayedInit()";
0131 
0132     initGUI();
0133 
0134     connect(mPageView, &QTreeWidget::itemSelectionChanged, this, &KCMDesignerFields::updatePreview);
0135     connect(mPageView, &QTreeWidget::itemClicked, this, &KCMDesignerFields::itemClicked);
0136 
0137     connect(mDeleteButton, &QPushButton::clicked, this, &KCMDesignerFields::deleteFile);
0138     connect(mImportButton, &QPushButton::clicked, this, &KCMDesignerFields::importFile);
0139     connect(mDesignerButton, &QPushButton::clicked, this, &KCMDesignerFields::startDesigner);
0140 
0141     load();
0142 
0143     // Install a dirwatcher that will detect newly created or removed designer files
0144     auto dw = new KDirWatch(this);
0145     QDir().mkpath(localUiDir());
0146     dw->addDir(localUiDir(), KDirWatch::WatchFiles);
0147     connect(dw, &KDirWatch::created, this, &KCMDesignerFields::rebuildList);
0148     connect(dw, &KDirWatch::deleted, this, &KCMDesignerFields::rebuildList);
0149     connect(dw, &KDirWatch::dirty, this, &KCMDesignerFields::rebuildList);
0150 }
0151 
0152 void KCMDesignerFields::deleteFile()
0153 {
0154     const auto selectedItems = mPageView->selectedItems();
0155     for (QTreeWidgetItem *item : selectedItems) {
0156         auto pageItem = static_cast<PageItem *>(item->parent() ? item->parent() : item);
0157         if (KMessageBox::warningContinueCancel(widget(),
0158                                                i18n("<qt>Do you really want to delete '<b>%1</b>'?</qt>", pageItem->text(0)),
0159                                                QString(),
0160                                                KStandardGuiItem::del())
0161             == KMessageBox::Continue) {
0162             QFile::remove(pageItem->path());
0163         }
0164     }
0165     // The actual view refresh will be done automagically by the slots connected to kdirwatch
0166 }
0167 
0168 void KCMDesignerFields::importFile()
0169 {
0170     const QUrl src = QFileDialog::getOpenFileUrl(widget(),
0171                                                  i18n("Import Page"),
0172                                                  QUrl::fromLocalFile(QDir::homePath()),
0173                                                  QStringLiteral("%1 (*.ui)").arg(i18n("Designer Files")));
0174 
0175     QUrl dest = QUrl::fromLocalFile(localUiDir());
0176     QDir().mkpath(localUiDir());
0177     dest = dest.adjusted(QUrl::RemoveFilename);
0178     dest.setPath(src.fileName());
0179     KIO::Job *job = KIO::file_copy(src, dest, -1, KIO::Overwrite);
0180     KJobWidgets::setWindow(job, widget());
0181     job->exec();
0182     // The actual view refresh will be done automagically by the slots connected to kdirwatch
0183 }
0184 
0185 void KCMDesignerFields::loadUiFiles()
0186 {
0187     const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, uiPath(), QStandardPaths::LocateDirectory);
0188     for (const QString &dir : dirs) {
0189         const QStringList fileNames = QDir(dir).entryList(QStringList() << QStringLiteral("*.ui"));
0190         for (const QString &file : fileNames) {
0191             new PageItem(mPageView, dir + QLatin1Char('/') + file);
0192         }
0193     }
0194 }
0195 
0196 void KCMDesignerFields::rebuildList()
0197 {
0198     // If nothing is initialized there is no need to do something
0199     if (mPageView) {
0200         const QStringList ai = saveActivePages();
0201         updatePreview();
0202         mPageView->clear();
0203         loadUiFiles();
0204         loadActivePages(ai);
0205     }
0206 }
0207 
0208 void KCMDesignerFields::loadActivePages(const QStringList &ai)
0209 {
0210     QTreeWidgetItemIterator it(mPageView);
0211     while (*it) {
0212         if ((*it)->parent() == nullptr) {
0213             auto item = static_cast<PageItem *>(*it);
0214             if (ai.contains(item->name())) {
0215                 item->setCheckState(0, Qt::Checked);
0216                 item->setIsActive(true);
0217             }
0218         }
0219 
0220         ++it;
0221     }
0222 }
0223 
0224 void KCMDesignerFields::load()
0225 {
0226     // see KCModule::showEvent()
0227     if (!mPageView) {
0228         delayedInit();
0229     }
0230     loadActivePages(readActivePages());
0231 }
0232 
0233 QStringList KCMDesignerFields::saveActivePages()
0234 {
0235     QTreeWidgetItemIterator it(mPageView, QTreeWidgetItemIterator::Checked | QTreeWidgetItemIterator::Selectable);
0236 
0237     QStringList activePages;
0238     while (*it) {
0239         if ((*it)->parent() == nullptr) {
0240             auto item = static_cast<PageItem *>(*it);
0241             activePages.append(item->name());
0242         }
0243 
0244         ++it;
0245     }
0246 
0247     return activePages;
0248 }
0249 
0250 void KCMDesignerFields::save()
0251 {
0252     writeActivePages(saveActivePages());
0253 }
0254 
0255 void KCMDesignerFields::defaults()
0256 {
0257 }
0258 
0259 void KCMDesignerFields::initGUI()
0260 {
0261     auto layout = new QVBoxLayout(widget());
0262 
0263     const bool noDesigner = QStandardPaths::findExecutable(QStringLiteral("designer")).isEmpty();
0264 
0265     if (noDesigner) {
0266         const QString txt = i18n(
0267             "<qt><b>Warning:</b> Qt Designer could not be found. It is probably not "
0268             "installed. You will only be able to import existing designer files.</qt>");
0269         auto lbl = new QLabel(txt, widget());
0270         layout->addWidget(lbl);
0271     }
0272 
0273     auto hbox = new QHBoxLayout();
0274     layout->addLayout(hbox);
0275 
0276     mPageView = new QTreeWidget(widget());
0277     mPageView->setHeaderLabel(i18n("Available Pages"));
0278     mPageView->setRootIsDecorated(true);
0279     mPageView->setAllColumnsShowFocus(true);
0280     mPageView->header()->setSectionResizeMode(QHeaderView::Stretch);
0281     hbox->addWidget(mPageView);
0282 
0283     auto box = new QGroupBox(i18n("Preview of Selected Page"), widget());
0284     auto boxLayout = new QVBoxLayout(box);
0285 
0286     mPagePreview = new QLabel(box);
0287     mPagePreview->setMinimumWidth(300);
0288     boxLayout->addWidget(mPagePreview);
0289 
0290     mPageDetails = new QLabel(box);
0291     boxLayout->addWidget(mPageDetails);
0292     boxLayout->addStretch(1);
0293 
0294     hbox->addWidget(box);
0295 
0296     loadUiFiles();
0297 
0298     hbox = new QHBoxLayout();
0299     layout->addLayout(hbox);
0300 
0301     const QString cwHowto = i18n(
0302         "<qt><p>This section allows you to add your own GUI"
0303         " Elements ('<i>Widgets</i>') to store your own values"
0304         " into %1. Proceed as described below:</p>"
0305         "<ol>"
0306         "<li>Click on '<i>Edit with Qt Designer</i>'</li>"
0307         "<li>In the dialog, select '<i>Widget</i>', then click <i>OK</i></li>"
0308         "<li>Add your widgets to the form</li>"
0309         "<li>Save the file in the directory proposed by Qt Designer</li>"
0310         "<li>Close Qt Designer</li>"
0311         "</ol>"
0312         "<p>In case you already have a designer file (*.ui) located"
0313         " somewhere on your hard disk, simply choose '<i>Import Page</i>'</p>"
0314         "<p><b>Important:</b> The name of each input widget you place within"
0315         " the form must start with '<i>X_</i>'; so if you want the widget to"
0316         " correspond to your custom entry '<i>X-Foo</i>', set the widget's"
0317         " <i>name</i> property to '<i>X_Foo</i>'.</p>"
0318         "<p><b>Important:</b> The widget will edit custom fields with an"
0319         " application name of %2.  To change the application name"
0320         " to be edited, set the widget name in Qt Designer.</p></qt>",
0321         applicationName(),
0322         applicationName());
0323 
0324     auto activeLabel = new QLabel(i18n("<a href=\"whatsthis:%1\">How does this work?</a>", cwHowto), widget());
0325     activeLabel->setTextInteractionFlags(Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard);
0326     connect(activeLabel, &QLabel::linkActivated, this, &KCMDesignerFields::showWhatsThis);
0327     activeLabel->setContextMenuPolicy(Qt::NoContextMenu);
0328     hbox->addWidget(activeLabel);
0329 
0330     // ### why is this needed? Looks like a KActiveLabel bug...
0331     activeLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
0332 
0333     hbox->addStretch(1);
0334 
0335     mDeleteButton = new QPushButton(i18n("Delete Page"), widget());
0336     mDeleteButton->setEnabled(false);
0337     hbox->addWidget(mDeleteButton);
0338     mImportButton = new QPushButton(i18n("Import Page..."), widget());
0339     hbox->addWidget(mImportButton);
0340     mDesignerButton = new QPushButton(i18n("Edit with Qt Designer..."), widget());
0341     hbox->addWidget(mDesignerButton);
0342 
0343     if (noDesigner) {
0344         mDesignerButton->setEnabled(false);
0345     }
0346 }
0347 
0348 void KCMDesignerFields::updatePreview()
0349 {
0350     QTreeWidgetItem *item = nullptr;
0351     if (mPageView->selectedItems().size() == 1) {
0352         item = mPageView->selectedItems().first();
0353     }
0354     bool widgetItemSelected = false;
0355 
0356     if (item) {
0357         if (item->parent()) {
0358             const QString details = QStringLiteral(
0359                                         "<qt><table>"
0360                                         "<tr><td align=\"right\"><b>%1</b></td><td>%2</td></tr>"
0361                                         "<tr><td align=\"right\"><b>%3</b></td><td>%4</td></tr>"
0362                                         "<tr><td align=\"right\"><b>%5</b></td><td>%6</td></tr>"
0363                                         "<tr><td align=\"right\"><b>%7</b></td><td>%8</td></tr>"
0364                                         "</table></qt>")
0365                                         .arg(i18n("Key:"),
0366                                              item->text(0).replace(QLatin1StringView("X_"), QStringLiteral("X-")),
0367                                              i18n("Type:"),
0368                                              item->text(1),
0369                                              i18n("Classname:"),
0370                                              item->text(2),
0371                                              i18n("Description:"),
0372                                              item->text(3));
0373 
0374             mPageDetails->setText(details);
0375 
0376             auto pageItem = static_cast<PageItem *>(item->parent());
0377             mPagePreview->setWindowIcon(pageItem->preview());
0378         } else {
0379             mPageDetails->setText(QString());
0380 
0381             auto pageItem = static_cast<PageItem *>(item);
0382             mPagePreview->setWindowIcon(pageItem->preview());
0383 
0384             widgetItemSelected = true;
0385         }
0386 
0387         mPagePreview->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken);
0388     } else {
0389         mPagePreview->setWindowIcon(QPixmap());
0390         mPagePreview->setFrameStyle(0);
0391         mPageDetails->setText(QString());
0392     }
0393 
0394     mDeleteButton->setEnabled(widgetItemSelected);
0395 }
0396 
0397 void KCMDesignerFields::itemClicked(QTreeWidgetItem *item)
0398 {
0399     if (!item || item->parent() != nullptr) {
0400         return;
0401     }
0402 
0403     auto pageItem = static_cast<PageItem *>(item);
0404 
0405     if (pageItem->isOn() != pageItem->isActive()) {
0406         markAsChanged();
0407         pageItem->setIsActive(pageItem->isOn());
0408     }
0409 }
0410 
0411 void KCMDesignerFields::startDesigner()
0412 {
0413     // check if path exists and create one if not.
0414     QString cepPath = localUiDir();
0415     if (!QDir(cepPath).exists()) {
0416         QDir().mkdir(cepPath);
0417     }
0418 
0419     // finally jump there
0420     QDir::setCurrent(QLatin1StringView(cepPath.toLocal8Bit()));
0421 
0422     QStringList args;
0423     QTreeWidgetItem *item = nullptr;
0424     if (mPageView->selectedItems().size() == 1) {
0425         item = mPageView->selectedItems().constFirst();
0426     }
0427     if (item) {
0428         auto pageItem = static_cast<PageItem *>(item->parent() ? item->parent() : item);
0429         args.append(pageItem->path());
0430     }
0431 
0432     auto job = new KIO::CommandLauncherJob(QStringLiteral("designer"), args, this);
0433     job->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, widget()));
0434     job->start();
0435 }
0436 
0437 void KCMDesignerFields::showWhatsThis(const QString &href)
0438 {
0439     if (href.startsWith(QLatin1StringView("whatsthis:"))) {
0440         const QPoint pos = QCursor::pos();
0441         QWhatsThis::showText(pos, href.mid(10), widget());
0442     }
0443 }
0444 
0445 #include "moc_kcmdesignerfields.cpp"