File indexing completed on 2025-01-26 05:09:31

0001 /*
0002  * This file is part of the KDE wacomtablet project. For copyright
0003  * information and license terms see the AUTHORS and COPYING files
0004  * in the top-level directory of this distribution.
0005  *
0006  * This program is free software; you can redistribute it and/or
0007  * modify it under the terms of the GNU General Public License as
0008  * published by the Free Software Foundation; either version 2 of
0009  * the License, or (at your option) any later version.
0010  *
0011  * This program is distributed in the hope that it will be useful,
0012  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0013  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0014  * GNU General Public License for more details.
0015  *
0016  * You should have received a copy of the GNU General Public License
0017  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
0018  */
0019 
0020 #include "kcmwacomtabletwidget.h"
0021 
0022 #include "ui_errorwidget.h"
0023 #include "ui_kcmwacomtabletwidget.h"
0024 
0025 #include "logging.h"
0026 
0027 #include "buttonpagewidget.h"
0028 #include "generalpagewidget.h"
0029 #include "profilemanagement.h"
0030 #include "styluspagewidget.h"
0031 #include "tabletpagewidget.h"
0032 #include "touchpagewidget.h"
0033 
0034 // common
0035 #include "dbustabletinterface.h"
0036 #include "devicetype.h"
0037 
0038 #include <KMessageBox>
0039 
0040 // Qt includes
0041 #include <QDBusReply>
0042 #include <QDialogButtonBox>
0043 #include <QInputDialog>
0044 #include <QLineEdit>
0045 #include <QMessageBox>
0046 #include <QPixmap>
0047 #include <QPointer>
0048 #include <QScrollArea>
0049 #include <QStringList>
0050 #include <kwidgetsaddons_version.h>
0051 
0052 using namespace Wacom;
0053 
0054 namespace Wacom
0055 {
0056 /**
0057  * Private class for the d-pointer.
0058  */
0059 class KCMWacomTabletWidgetPrivate
0060 {
0061 public:
0062     Ui::KCMWacomTabletWidget ui; //!< This user interface.
0063 
0064     GeneralPageWidget generalPage; //!< Widget that shows some basic information about the tablet.
0065     StylusPageWidget stylusPage; //!< Widget for the pen settings (stylus/eraser).
0066     ButtonPageWidget buttonPage; //!< Widget for the express button settings.
0067     TabletPageWidget tabletPage; //!< Widget for the tablet settings.
0068     TouchPageWidget touchPage; //!< Widget for the touch settings.
0069     QWidget deviceErrorWidget; //!< Device error widget.
0070     Ui::ErrorWidget deviceErrorUi; //!< Device error widget ui.
0071     bool profileChanged; //!< True if the profile was changed and not saved yet.
0072 }; // CLASS
0073 } // NAMESPACE
0074 
0075 void makeScrollableTab(QTabWidget *parent, QWidget &tab, const QString &title)
0076 {
0077     auto scrollableTab = new QScrollArea(parent);
0078     scrollableTab->setWidget(&tab);
0079     scrollableTab->setWidgetResizable(true);
0080     scrollableTab->setFrameShadow(QFrame::Shadow::Plain);
0081     parent->addTab(scrollableTab, title);
0082 }
0083 
0084 KCMWacomTabletWidget::KCMWacomTabletWidget(QWidget *parent)
0085     : QWidget(parent)
0086     , d_ptr(new KCMWacomTabletWidgetPrivate)
0087 {
0088     setupUi();
0089     loadTabletInformation();
0090     showHideConfig();
0091 }
0092 
0093 KCMWacomTabletWidget::~KCMWacomTabletWidget()
0094 {
0095     delete this->d_ptr;
0096 }
0097 
0098 void KCMWacomTabletWidget::setupUi()
0099 {
0100     Q_D(KCMWacomTabletWidget);
0101 
0102     DBusTabletInterface *dbusTabletInterface = &DBusTabletInterface::instance();
0103 
0104     if (!dbusTabletInterface->isValid()) {
0105         qCWarning(KCM) << "DBus interface not available";
0106     }
0107 
0108     d->profileChanged = false;
0109 
0110     // setup error widget
0111     d->deviceErrorUi.setupUi(&(d->deviceErrorWidget));
0112     d->deviceErrorUi.errorImage->setPixmap(QIcon::fromTheme(QLatin1String("dialog-warning")).pixmap(48));
0113     connect(d->deviceErrorUi.buttonRunTabletFinder, &QCommandLinkButton::clicked, this, &KCMWacomTabletWidget::showTabletFinder);
0114     d->deviceErrorUi.buttonRunTabletFinder->setVisible(false);
0115 
0116     // setup normal ui
0117     d->ui.setupUi(this);
0118     d->ui.addProfileButton->setIcon(QIcon::fromTheme(QLatin1String("document-new")));
0119     d->ui.delProfileButton->setIcon(QIcon::fromTheme(QLatin1String("edit-delete-page")));
0120 
0121     // connect tablet selector
0122     connect(d->ui.tabletListSelector, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &KCMWacomTabletWidget::onTabletSelectionChanged);
0123 
0124     // connect profile selector
0125     connect(d->ui.addProfileButton, SIGNAL(clicked(bool)), SLOT(addProfile()));
0126     connect(d->ui.delProfileButton, SIGNAL(clicked(bool)), SLOT(delProfile()));
0127     connect(d->ui.profileSelector, qOverload<int>(&QComboBox::currentIndexChanged), this, [this, d](int index) {
0128         switchProfile(d->ui.profileSelector->itemText(index));
0129     });
0130 
0131     // connect configuration tabs
0132     connect(&(d->generalPage), SIGNAL(changed()), SLOT(profileChanged()));
0133     connect(&(d->stylusPage), SIGNAL(changed()), SLOT(profileChanged()));
0134     connect(&(d->buttonPage), SIGNAL(changed()), SLOT(profileChanged()));
0135     connect(&(d->tabletPage), SIGNAL(changed()), SLOT(profileChanged()));
0136     connect(&(d->touchPage), SIGNAL(changed()), SLOT(profileChanged()));
0137 
0138     // connect rotation handling
0139     connect(&(d->tabletPage), SIGNAL(rotationChanged(ScreenRotation)), &(d->touchPage), SLOT(onRotationChanged(ScreenRotation)));
0140 
0141     // connect DBus signals
0142     connect(dbusTabletInterface, SIGNAL(tabletAdded(QString)), SLOT(onTabletAdded(QString)));
0143     connect(dbusTabletInterface, SIGNAL(tabletRemoved(QString)), SLOT(onTabletRemoved(QString)));
0144 }
0145 
0146 void KCMWacomTabletWidget::loadTabletInformation()
0147 {
0148     Q_D(KCMWacomTabletWidget);
0149     QDBusReply<QStringList> connectedTablets = DBusTabletInterface::instance().getTabletList();
0150 
0151     if (!connectedTablets.isValid()) {
0152         return;
0153     }
0154 
0155     d->ui.tabletListSelector->blockSignals(true);
0156     foreach (const QString &tabletId, connectedTablets.value()) {
0157         addTabletToSelector(tabletId);
0158     }
0159     d->ui.tabletListSelector->blockSignals(false);
0160 }
0161 
0162 void KCMWacomTabletWidget::showHideConfig()
0163 {
0164     // request this to see if dbus works and tablets are connected
0165     QDBusReply<QStringList> connectedTablets = DBusTabletInterface::instance().getTabletList();
0166 
0167     if (!connectedTablets.isValid()) {
0168         QString errorTitle = i18n("KDE tablet service not found");
0169         QString errorMsg = i18n(
0170             "Please start the KDE wacom tablet service to use this configuration dialog.\n"
0171             "The service is required for tablet detection and profile support.");
0172         showError(errorTitle, errorMsg);
0173     } else if (connectedTablets.value().count() == 0) {
0174         QString errorTitle = i18n("No tablet device detected");
0175         QString errorMsg = i18n(
0176             "Please connect a tablet device to continue.\n"
0177             "If your device is already connected, it is currently not in the device database.");
0178         showError(errorTitle, errorMsg, true);
0179     } else {
0180         showConfig();
0181     }
0182 }
0183 
0184 void KCMWacomTabletWidget::onTabletAdded(const QString &tabletId)
0185 {
0186     addTabletToSelector(tabletId);
0187 }
0188 
0189 void KCMWacomTabletWidget::onTabletRemoved(const QString &tabletId)
0190 {
0191     Q_D(KCMWacomTabletWidget);
0192 
0193     int index = d->ui.tabletListSelector->findData(tabletId);
0194 
0195     if (index >= 0) {
0196         d->ui.tabletListSelector->removeItem(index);
0197     }
0198 }
0199 
0200 void KCMWacomTabletWidget::onTabletSelectionChanged()
0201 {
0202     Q_D(KCMWacomTabletWidget);
0203 
0204     showSaveChanges();
0205 
0206     // tell all widgets to operate on a different tablet now
0207     QString tabletId = d->ui.tabletListSelector->itemData(d->ui.tabletListSelector->currentIndex()).toString();
0208     d->generalPage.setTabletId(tabletId);
0209     d->stylusPage.setTabletId(tabletId);
0210     d->buttonPage.setTabletId(tabletId);
0211     d->tabletPage.setTabletId(tabletId);
0212     d->touchPage.setTabletId(tabletId);
0213 
0214     showHideConfig();
0215 }
0216 
0217 void KCMWacomTabletWidget::addProfile()
0218 {
0219     bool ok;
0220     QString text = QInputDialog::getText(this, i18n("Add new profile"), i18n("Profile name:"), QLineEdit::Normal, QString(), &ok);
0221     if (!ok || text.isEmpty()) {
0222         return;
0223     }
0224 
0225     ProfileManagement::instance().createNewProfile(text);
0226     refreshProfileSelector(text);
0227     switchProfile(text);
0228 }
0229 
0230 void KCMWacomTabletWidget::delProfile()
0231 {
0232     Q_D(KCMWacomTabletWidget);
0233 
0234     ProfileManagement::instance().deleteProfile();
0235     refreshProfileSelector();
0236     switchProfile(d->ui.profileSelector->currentText());
0237 
0238     // update profile rotation selection
0239     d->generalPage.reloadWidget();
0240 }
0241 
0242 void KCMWacomTabletWidget::saveProfile()
0243 {
0244     Q_D(KCMWacomTabletWidget);
0245 
0246     auto &profileManagement = ProfileManagement::instance();
0247 
0248     d->generalPage.saveToProfile();
0249     d->stylusPage.saveToProfile(profileManagement);
0250     d->buttonPage.saveToProfile(profileManagement);
0251     d->tabletPage.saveToProfile(profileManagement);
0252     d->touchPage.saveToProfile(profileManagement);
0253 
0254     d->profileChanged = false;
0255     emit changed(false);
0256 
0257     applyProfile();
0258 }
0259 
0260 void KCMWacomTabletWidget::switchProfile(const QString &profile)
0261 {
0262     showSaveChanges();
0263 
0264     ProfileManagement::instance().setProfileName(profile);
0265 
0266     reloadProfile();
0267     applyProfile();
0268 }
0269 
0270 void KCMWacomTabletWidget::reloadProfile()
0271 {
0272     Q_D(KCMWacomTabletWidget);
0273 
0274     auto &profileManagement = ProfileManagement::instance();
0275 
0276     d->generalPage.loadFromProfile();
0277     d->stylusPage.loadFromProfile(profileManagement);
0278     d->buttonPage.loadFromProfile(profileManagement);
0279     d->tabletPage.loadFromProfile(profileManagement);
0280     d->touchPage.loadFromProfile(profileManagement);
0281 
0282     d->profileChanged = false;
0283     emit changed(false);
0284 }
0285 
0286 void KCMWacomTabletWidget::applyProfile()
0287 {
0288     Q_D(KCMWacomTabletWidget);
0289 
0290     QString tabletId = d->ui.tabletListSelector->itemData(d->ui.tabletListSelector->currentIndex()).toString();
0291     DBusTabletInterface::instance().setProfile(tabletId, ProfileManagement::instance().profileName());
0292 }
0293 
0294 void KCMWacomTabletWidget::profileChanged()
0295 {
0296     Q_D(KCMWacomTabletWidget);
0297 
0298     d->profileChanged = true;
0299     emit changed(true);
0300 }
0301 
0302 void KCMWacomTabletWidget::showError(const QString &errorTitle, const QString &errorMsg, bool showTabletFinderButton)
0303 {
0304     Q_D(KCMWacomTabletWidget);
0305 
0306     hideError();
0307     hideConfig();
0308 
0309     d->deviceErrorUi.errorTitle->setText(errorTitle);
0310     d->deviceErrorUi.errorText->setText(errorMsg);
0311     d->ui.verticalLayout->addWidget(&(d->deviceErrorWidget));
0312     d->deviceErrorWidget.setVisible(true);
0313     d->deviceErrorUi.buttonRunTabletFinder->setVisible(showTabletFinderButton);
0314 }
0315 
0316 void KCMWacomTabletWidget::hideConfig()
0317 {
0318     Q_D(KCMWacomTabletWidget);
0319 
0320     d->ui.tabletListSelector->setVisible(false);
0321     d->ui.tabletListLabel->setVisible(false);
0322     d->ui.profileSelector->setVisible(false);
0323     d->ui.profileLabel->setVisible(false);
0324     d->ui.addProfileButton->setVisible(false);
0325     d->ui.delProfileButton->setVisible(false);
0326 
0327     d->ui.tabletListSelector->setEnabled(false);
0328     d->ui.profileSelector->setEnabled(false);
0329     d->ui.addProfileButton->setEnabled(false);
0330     d->ui.delProfileButton->setEnabled(false);
0331     d->ui.deviceTabWidget->setVisible(false);
0332 }
0333 
0334 void KCMWacomTabletWidget::hideError()
0335 {
0336     Q_D(KCMWacomTabletWidget);
0337 
0338     d->deviceErrorWidget.setVisible(false);
0339     d->ui.verticalLayout->removeWidget(&(d->deviceErrorWidget));
0340 }
0341 
0342 bool KCMWacomTabletWidget::refreshProfileSelector(const QString &profile)
0343 {
0344     Q_D(KCMWacomTabletWidget);
0345 
0346     int index = -1;
0347     QStringList profiles = ProfileManagement::instance().availableProfiles();
0348 
0349     d->ui.profileSelector->blockSignals(true);
0350     d->ui.profileSelector->clear();
0351     d->ui.profileSelector->addItems(profiles);
0352 
0353     if (!profile.isEmpty()) {
0354         index = d->ui.profileSelector->findText(profile);
0355         d->ui.profileSelector->setCurrentIndex(index);
0356 
0357     } else if (!profiles.isEmpty()) {
0358         index = 0;
0359         d->ui.profileSelector->setCurrentIndex(index);
0360     }
0361 
0362     d->ui.profileSelector->blockSignals(false);
0363 
0364     return (index >= 0);
0365 }
0366 
0367 void KCMWacomTabletWidget::showConfig()
0368 {
0369     Q_D(KCMWacomTabletWidget);
0370 
0371     // make sure no error message is active
0372     hideError();
0373 
0374     // reload profile and widget data
0375     QString tabletId = d->ui.tabletListSelector->itemData(d->ui.tabletListSelector->currentIndex()).toString();
0376     ProfileManagement::instance().setTabletId(tabletId);
0377     ProfileManagement::instance().reload();
0378 
0379     d->generalPage.setTabletId(tabletId);
0380     d->stylusPage.setTabletId(tabletId);
0381     d->buttonPage.setTabletId(tabletId);
0382     d->tabletPage.setTabletId(tabletId);
0383 
0384     QDBusReply<QString> touchDeviceName = DBusTabletInterface::instance().getDeviceName(tabletId, DeviceType::Touch.key());
0385     QDBusReply<QString> touchSensorId = DBusTabletInterface::instance().getTouchSensorId(tabletId);
0386 
0387     const bool hasBuiltInTouch = (touchDeviceName.isValid() && !touchDeviceName.value().isEmpty());
0388     const bool hasPairedTouch = (touchSensorId.isValid() && !touchSensorId.value().isEmpty());
0389 
0390     if (hasPairedTouch) {
0391         d->touchPage.setTabletId(touchSensorId.value());
0392     } else {
0393         d->touchPage.setTabletId(tabletId);
0394     }
0395 
0396     d->generalPage.reloadWidget();
0397     d->stylusPage.reloadWidget();
0398     d->buttonPage.reloadWidget();
0399     d->tabletPage.reloadWidget();
0400     d->touchPage.reloadWidget();
0401 
0402     // show tablet Selector
0403     d->ui.tabletListSelector->setEnabled(true);
0404     d->ui.tabletListLabel->setVisible(true);
0405     d->ui.tabletListSelector->setVisible(true);
0406 
0407     // initialize profile selector
0408     d->ui.profileSelector->setEnabled(true);
0409     d->ui.addProfileButton->setEnabled(true);
0410     d->ui.delProfileButton->setEnabled(true);
0411     d->ui.profileLabel->setVisible(true);
0412     d->ui.profileSelector->setVisible(true);
0413     d->ui.addProfileButton->setVisible(true);
0414     d->ui.delProfileButton->setVisible(true);
0415 
0416     if (ProfileManagement::instance().availableProfiles().isEmpty()) {
0417         ProfileManagement::instance().createNewProfile(i18nc("Name of the default profile that will be created if none exist.", "Default"));
0418         applyProfile();
0419     }
0420 
0421     refreshProfileSelector();
0422 
0423     // initialize configuration tabs
0424     d->ui.deviceTabWidget->clear();
0425     makeScrollableTab(d->ui.deviceTabWidget, d->generalPage, i18nc("Basic overview page for the tablet hardware", "General"));
0426     makeScrollableTab(d->ui.deviceTabWidget, d->stylusPage, i18n("Stylus"));
0427 
0428     QDBusReply<bool> hasPadButtons = DBusTabletInterface::instance().hasPadButtons(tabletId);
0429 
0430     if (hasPadButtons.isValid() && hasPadButtons.value()) {
0431         makeScrollableTab(d->ui.deviceTabWidget, d->buttonPage, i18n("Express Buttons"));
0432     }
0433 
0434     makeScrollableTab(d->ui.deviceTabWidget, d->tabletPage, i18n("Tablet"));
0435 
0436     if (hasBuiltInTouch || hasPairedTouch) {
0437         makeScrollableTab(d->ui.deviceTabWidget, d->touchPage, i18n("Touch"));
0438     }
0439 
0440     d->ui.deviceTabWidget->setEnabled(true);
0441     d->ui.deviceTabWidget->setVisible(true);
0442 
0443     // switch to the currently active profile
0444     QDBusReply<QString> profile = DBusTabletInterface::instance().getProfile(tabletId);
0445     if (profile.isValid()) {
0446         d->ui.profileSelector->setCurrentText(profile);
0447         switchProfile(profile);
0448     }
0449 }
0450 
0451 void KCMWacomTabletWidget::showSaveChanges()
0452 {
0453     Q_D(KCMWacomTabletWidget);
0454 
0455     if (!d->profileChanged) {
0456         return;
0457     }
0458 
0459     // TODO: This should be a proper Yes/No/Cancel dialog
0460     // but this probably requires custom ComboBoxes for canceling selection
0461     if (KMessageBox::questionTwoActions(this,
0462                                         i18n("Save changes to the current profile?"),
0463                                         i18n("Save Profile"),
0464                                         KStandardGuiItem::save(),
0465                                         KStandardGuiItem::discard())
0466         == KMessageBox::ButtonCode::PrimaryAction) {
0467         saveProfile();
0468     }
0469 }
0470 
0471 void KCMWacomTabletWidget::showTabletFinder()
0472 {
0473     const bool success = QProcess::startDetached(QStringLiteral("kde_wacom_tabletfinder"), QStringList());
0474 
0475     if (!success) {
0476         QString err = i18n("Failed to launch Wacom tablet finder tool. Check your installation.");
0477         QMessageBox::warning(QApplication::activeWindow(), QApplication::applicationName(), err);
0478     }
0479 }
0480 
0481 void KCMWacomTabletWidget::addTabletToSelector(const QString &tabletId)
0482 {
0483     Q_D(KCMWacomTabletWidget);
0484 
0485     QDBusReply<QString> deviceName = DBusTabletInterface::instance().getInformation(tabletId, TabletInfo::TabletName.key());
0486     QDBusReply<QStringList> inputDevices = DBusTabletInterface::instance().getDeviceList(tabletId);
0487     QDBusReply<bool> isTouchSensor = DBusTabletInterface::instance().isTouchSensor(tabletId);
0488     if (isTouchSensor.isValid() && isTouchSensor.value()) {
0489         qCDebug(KCM) << "Ignoring tablet" << deviceName << tabletId << "because it's a touch sensor";
0490         return;
0491     }
0492 
0493     qCDebug(KCM) << "Adding tablet" << deviceName << tabletId << "with" << inputDevices.value();
0494 
0495     d->ui.tabletListSelector->addItem(QString::fromLatin1("%1 [%2]").arg(deviceName).arg(tabletId), tabletId);
0496 }
0497 
0498 #include "moc_kcmwacomtabletwidget.cpp"