File indexing completed on 2024-06-16 04:16:24

0001 /*
0002  *  SPDX-FileCopyrightText: 2017 Boudewijn Rempt <boud@valdyas.org>
0003  *
0004  *  SPDX-License-Identifier: LGPL-2.0-or-later
0005  */
0006 
0007 #include "TouchDockerDock.h"
0008 
0009 #include <QtQuickWidgets/QQuickWidget>
0010 #include <QQmlEngine>
0011 #include <QQmlContext>
0012 #include <QAction>
0013 #include <QUrl>
0014 #include <QKeyEvent>
0015 #include <QApplication>
0016 #include <QQuickWindow>
0017 
0018 #include <klocalizedstring.h>
0019 #include <kactioncollection.h>
0020 #include <ksharedconfig.h>
0021 #include <kconfiggroup.h>
0022 
0023 #include <kis_action_registry.h>
0024 #include <KoDialog.h>
0025 #include <KoResourcePaths.h>
0026 #include <kis_icon.h>
0027 #include <KoCanvasBase.h>
0028 #include <KoCanvasController.h>
0029 #include <KisViewManager.h>
0030 #include <kis_canvas2.h>
0031 #include <KisMainWindow.h>
0032 #include <kis_config.h>
0033 #include <KisPart.h>
0034 #include <KisDocument.h>
0035 #include <KisMimeDatabase.h>
0036 #include <kis_action_manager.h>
0037 #include <kis_action.h>
0038 
0039 #include <Theme.h>
0040 #include <Settings.h>
0041 #include <DocumentManager.h>
0042 #include <KisSketchView.h>
0043 
0044 #include <QVersionNumber>
0045 
0046 #ifdef Q_OS_WIN
0047 #include "TouchDockerQQuickWidget.h"
0048 #endif
0049 
0050 namespace
0051 {
0052 
0053 bool shouldSetAcceptTouchEvents()
0054 {
0055     // See https://bugreports.qt.io/browse/QTBUG-66718
0056     static QVersionNumber qtVersion = QVersionNumber::fromString(qVersion());
0057     static bool retval = qtVersion > QVersionNumber(5, 9, 3) && qtVersion.normalized() != QVersionNumber(5, 10);
0058     return retval;
0059 }
0060 
0061 } // namespace
0062 
0063 class TouchDockerDock::Private
0064 {
0065 public:
0066     Private()
0067     {
0068     }
0069 
0070     TouchDockerDock *q {nullptr};
0071     bool allowClose {true};
0072     KisSketchView *sketchView {0};
0073     QString currentSketchPage;
0074     KoDialog *openDialog {0};
0075     KoDialog *saveAsDialog {0};
0076 
0077     QMap<QString, QString> buttonMapping;
0078 
0079     bool shiftOn {false};
0080     bool ctrlOn {false};
0081     bool altOn {false};
0082 };
0083 
0084 
0085 TouchDockerDock::TouchDockerDock()
0086     : QDockWidget(i18n("Touch Docker"))
0087     , d(new Private())
0088 {
0089     KisConfig cfg(true);
0090 
0091     QStringList defaultMapping = QStringList() << "decrease_opacity"
0092                                                << "increase_opacity"
0093                                                << "make_brush_color_lighter"
0094                                                << "make_brush_color_darker"
0095                                                << "decrease_brush_size"
0096                                                << "increase_brush_size"
0097                                                << "previous_preset"
0098                                                << "clear";
0099 
0100     QStringList mapping = cfg.readEntry<QString>("touchdockermapping", defaultMapping.join(',')).split(',');
0101     for (int i = 0; i < 8; ++i) {
0102         if (i < mapping.size()) {
0103             d->buttonMapping[QString("%1").arg(i + 1)] = mapping[i];
0104         }
0105         else if (i < defaultMapping.size()) {
0106             d->buttonMapping[QString("%1").arg(i + 1)] = defaultMapping[i];
0107         }
0108     }
0109 
0110     if (!cfg.useOpenGL()) {
0111         QQuickWindow::setSceneGraphBackend(QSGRendererInterface::Software);
0112     }
0113 
0114 #ifdef Q_OS_WIN
0115     m_quickWidget = new TouchDockerQQuickWidget(this);
0116 #else
0117     m_quickWidget = new QQuickWidget(this);
0118 #endif
0119     if (shouldSetAcceptTouchEvents()) {
0120         m_quickWidget->setAttribute(Qt::WA_AcceptTouchEvents);
0121     }
0122     setWidget(m_quickWidget);
0123     setEnabled(true);
0124     m_quickWidget->engine()->rootContext()->setContextProperty("mainWindow", this);
0125 
0126     m_quickWidget->engine()->addImportPath(KoResourcePaths::getApplicationRoot() + "/lib/qml/");
0127     m_quickWidget->engine()->addImportPath(KoResourcePaths::getApplicationRoot() + "/lib64/qml/");
0128 
0129     m_quickWidget->engine()->addPluginPath(KoResourcePaths::getApplicationRoot() + "/lib/qml/");
0130     m_quickWidget->engine()->addPluginPath(KoResourcePaths::getApplicationRoot() + "/lib64/qml/");
0131 
0132     Settings *settings = new Settings(this);
0133     DocumentManager::instance()->setSettingsManager(settings);
0134     m_quickWidget->engine()->rootContext()->setContextProperty("Settings", settings);
0135 
0136     Theme *theme = Theme::load(KSharedConfig::openConfig()->group("General").readEntry<QString>("theme", "default"),
0137                                m_quickWidget->engine());
0138     if (theme) {
0139         settings->setTheme(theme);
0140     }
0141 
0142 
0143     m_quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
0144     m_quickWidget->setSource(QUrl("qrc:/touchstrip.qml"));
0145 
0146 
0147 }
0148 
0149 TouchDockerDock::~TouchDockerDock()
0150 {
0151     // Prevent double free
0152     m_quickWidget->setParent(nullptr);
0153 
0154     // Work around child widget being refreshed *after* its parent is gone
0155     // https://bugreports.qt.io/browse/QTBUG-81247
0156     delete m_quickWidget;
0157 }
0158 
0159 bool TouchDockerDock::allowClose() const
0160 {
0161     return d->allowClose;
0162 }
0163 
0164 void TouchDockerDock::setAllowClose(bool allow)
0165 {
0166     d->allowClose = allow;
0167 }
0168 
0169 QString TouchDockerDock::currentSketchPage() const
0170 {
0171     return d->currentSketchPage;
0172 }
0173 
0174 void TouchDockerDock::setCurrentSketchPage(QString newPage)
0175 {
0176     d->currentSketchPage = newPage;
0177     emit currentSketchPageChanged();
0178 }
0179 
0180 void TouchDockerDock::closeEvent(QCloseEvent* event)
0181 {
0182     if (!d->allowClose) {
0183         event->ignore();
0184         emit closeRequested();
0185     } else {
0186         event->accept();
0187     }
0188 }
0189 
0190 void TouchDockerDock::slotButtonPressed(const QString &id)
0191 {
0192     if (id == "fileOpenButton") {
0193         showFileOpenDialog();
0194     }
0195     else if (id == "fileSaveButton" && m_canvas && m_canvas->viewManager() && m_canvas->viewManager()->document()) {
0196         if(m_canvas->viewManager()->document()->path().isEmpty()) {
0197             showFileSaveAsDialog();
0198         } else {
0199             bool batchMode = m_canvas->viewManager()->document()->fileBatchMode();
0200             m_canvas->viewManager()->document()->setFileBatchMode(true);
0201             m_canvas->viewManager()->document()->save(true, 0);
0202             m_canvas->viewManager()->document()->setFileBatchMode(batchMode);
0203         }
0204     }
0205     else if (id == "fileSaveAsButton" && m_canvas && m_canvas->viewManager() && m_canvas->viewManager()->document()) {
0206         showFileSaveAsDialog();
0207     }
0208     else {
0209         QAction *a = action(id);
0210         if (a) {
0211             if (a->isCheckable()) {
0212                 a->toggle();
0213             }
0214             else {
0215                 a->trigger();
0216             }
0217         }
0218 
0219         else if (id == "shift") {
0220             // set shift state for the next pointer event, somehow
0221             QKeyEvent event(d->shiftOn ? QEvent::KeyRelease : QEvent::KeyPress,
0222                                0,
0223                                Qt::ShiftModifier);
0224             QApplication::sendEvent(KisPart::instance()->currentMainwindow(), &event);
0225             d->shiftOn = !d->shiftOn;
0226         }
0227         else if (id == "ctrl") {
0228             // set ctrl state for the next pointer event, somehow
0229             QKeyEvent event(d->ctrlOn ? QEvent::KeyRelease : QEvent::KeyPress,
0230                                0,
0231                                Qt::ControlModifier);
0232             QApplication::sendEvent(KisPart::instance()->currentMainwindow(), &event);
0233             d->ctrlOn = !d->ctrlOn;
0234         }
0235         else if (id == "alt") {
0236             // set alt state for the next pointer event, somehow
0237             QKeyEvent event(d->altOn ? QEvent::KeyRelease : QEvent::KeyPress,
0238                                0,
0239                                Qt::AltModifier);
0240             QApplication::sendEvent(KisPart::instance()->currentMainwindow(), &event);
0241             d->altOn = !d->altOn;
0242 
0243         }
0244     }
0245 }
0246 
0247 void TouchDockerDock::slotOpenImage(QString path)
0248 {
0249     if (d->openDialog) {
0250         d->openDialog->accept();
0251     }
0252     KisPart::instance()->currentMainwindow()->openDocument(path, KisMainWindow::None);
0253 }
0254 
0255 void TouchDockerDock::slotSaveAs(QString path, QString mime)
0256 {
0257     if (d->saveAsDialog) {
0258         d->saveAsDialog->accept();
0259     }
0260     m_canvas->viewManager()->document()->saveAs(path, mime.toLatin1(), true);
0261     m_canvas->viewManager()->document()->waitForSavingToComplete();
0262 }
0263 
0264 void TouchDockerDock::hideFileOpenDialog()
0265 {
0266     if (d->openDialog) {
0267         d->openDialog->accept();
0268     }
0269 }
0270 
0271 void TouchDockerDock::hideFileSaveAsDialog()
0272 {
0273     if (d->saveAsDialog) {
0274         d->saveAsDialog->accept();
0275     }
0276 }
0277 
0278 QString TouchDockerDock::imageForButton(QString id)
0279 {
0280     if (d->buttonMapping.contains(id)) {
0281         id = d->buttonMapping[id];
0282     }
0283     if (KisActionRegistry::instance()->hasAction(id)) {
0284         QString a = KisActionRegistry::instance()->getActionProperty(id, "icon");
0285         if (!a.isEmpty()) {
0286             return "image://icon/" + a;
0287         }
0288     }
0289     return QString();
0290 }
0291 
0292 QString TouchDockerDock::iconForButton(QString id, bool useDarkIcons)
0293 {
0294     // adapted from kis_icon_utils, as here we need a path to give to qml, not a QIcon
0295     const char * const prefix = useDarkIcons ? "dark_" : "light_";
0296 
0297     QString name = "";
0298 
0299     if (d->buttonMapping.contains(id)) {
0300         name = d->buttonMapping[id];
0301     }
0302     else {
0303         name = id;
0304     }
0305     QString  realName = QLatin1String(prefix) + name;
0306 
0307     // Dark and light, no size specified
0308     const QStringList names = { "pics/" + realName + ".png",
0309                                 "pics/" + realName + ".svg",
0310                                 "pics/" + realName + ".svgz",
0311                                 "pics/" + name + ".png",
0312                                 "pics/" + name + ".svg",
0313                                 "pics/" + name + ".svgz",
0314                                 realName + ".png",
0315                                 realName + ".svg",
0316                                 realName + ".svgz",
0317                                 name,
0318                                 name + ".png",
0319                                 name + ".svg",
0320                                 name + ".svgz"};
0321 
0322     for (const QString &resname : names) {
0323         if (QFile(":/" + resname).exists()) {
0324             return resname;
0325         }
0326     }
0327     return QString();
0328 }
0329 
0330 QString TouchDockerDock::textForButton(QString id)
0331 {
0332     if (d->buttonMapping.contains(id)) {
0333         id = d->buttonMapping[id];
0334     }
0335     if (KisActionRegistry::instance()->hasAction(id)) {
0336         QString a = KisActionRegistry::instance()->getActionProperty(id, "iconText");
0337         if (a.isEmpty()) {
0338             a = KisActionRegistry::instance()->getActionProperty(id, "text");
0339         }
0340         return a;
0341     }
0342 
0343     return id;
0344 }
0345 
0346 QAction *TouchDockerDock::action(QString id) const
0347 {
0348     if (m_canvas && m_canvas->viewManager()) {
0349         if (d->buttonMapping.contains(id)) {
0350             id = d->buttonMapping[id];
0351         }
0352 
0353         QAction *action = m_canvas->viewManager()->actionManager()->actionByName(id);
0354         if (!action) {
0355             return m_canvas->canvasController()->actionCollection()->action(id);
0356         }
0357 
0358         return action;
0359     }
0360     return 0;
0361 }
0362 
0363 void TouchDockerDock::showFileOpenDialog()
0364 {
0365     if (!d->openDialog) {
0366         d->openDialog = createDialog("qrc:/opendialog.qml");
0367     }
0368 
0369     d->openDialog->exec();
0370 }
0371 
0372 void TouchDockerDock::showFileSaveAsDialog()
0373 {
0374     if (!d->saveAsDialog) {
0375         d->saveAsDialog = createDialog("qrc:/saveasdialog.qml");
0376     }
0377     d->saveAsDialog->exec();
0378 }
0379 
0380 void TouchDockerDock::changeEvent(QEvent *event)
0381 {
0382     if (event->type() == QEvent::PaletteChange) {
0383         m_quickWidget->setSource(QUrl("qrc:/touchstrip.qml"));
0384         event->accept();
0385     } else {
0386         event->ignore();
0387     }
0388 }
0389 
0390 void TouchDockerDock::tabletEvent(QTabletEvent *event)
0391 {
0392 #ifdef Q_OS_WIN
0393     /**
0394      * On Windows (only in WinInk mode), unless we accept the tablet event,
0395      * OS will start windows gestures, like click+hold for right click.
0396      * It will block any mouse events generation.
0397      *
0398      * In our own (hacky) implementation, if we accept the event, we block
0399      * the gesture, but still generate a fake mouse event.
0400      */
0401     event->accept();
0402 #else
0403     QDockWidget::tabletEvent(event);
0404 #endif
0405 }
0406 
0407 KoDialog *TouchDockerDock::createDialog(const QString qml)
0408 {
0409     KoDialog *dlg = new KoDialog(this);
0410     dlg->setButtons(KoDialog::None);
0411 
0412     QQuickWidget *quickWidget = new QQuickWidget(this);
0413     if (shouldSetAcceptTouchEvents()) {
0414         quickWidget->setAttribute(Qt::WA_AcceptTouchEvents);
0415     }
0416     dlg->setMainWidget(quickWidget);
0417 
0418     setEnabled(true);
0419     quickWidget->engine()->rootContext()->setContextProperty("mainWindow", this);
0420 
0421     quickWidget->engine()->addImportPath(KoResourcePaths::getApplicationRoot() + "/lib/qml/");
0422     quickWidget->engine()->addImportPath(KoResourcePaths::getApplicationRoot() + "/lib64/qml/");
0423 
0424     quickWidget->engine()->addPluginPath(KoResourcePaths::getApplicationRoot() + "/lib/qml/");
0425     quickWidget->engine()->addPluginPath(KoResourcePaths::getApplicationRoot() + "/lib64/qml/");
0426 
0427 
0428     Settings *settings = new Settings(this);
0429     DocumentManager::instance()->setSettingsManager(settings);
0430     quickWidget->engine()->rootContext()->setContextProperty("Settings", settings);
0431 
0432     Theme *theme = Theme::load(KSharedConfig::openConfig()->group("General").readEntry<QString>("theme", "default"),
0433                                quickWidget->engine());
0434     settings->setTheme(theme);
0435 
0436     quickWidget->setSource(QUrl(qml));
0437     quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
0438 
0439     dlg->setMinimumSize(1280, 768);
0440 
0441     return dlg;
0442 }
0443 
0444 QObject *TouchDockerDock::sketchKisView() const
0445 {
0446     return d->sketchView;
0447 }
0448 
0449 void TouchDockerDock::setSketchKisView(QObject* newView)
0450 {
0451     if (d->sketchView) {
0452         d->sketchView->disconnect(this);
0453     }
0454 
0455     if (d->sketchView != newView) {
0456         d->sketchView = qobject_cast<KisSketchView*>(newView);
0457         emit sketchKisViewChanged();
0458     }
0459 }
0460 
0461 void TouchDockerDock::setCanvas(KoCanvasBase *canvas)
0462 {
0463     setEnabled(true);
0464 
0465     if (m_canvas == canvas) {
0466         return;
0467     }
0468 
0469     if (m_canvas) {
0470         m_canvas->disconnectCanvasObserver(this);
0471     }
0472 
0473     if (!canvas) {
0474         m_canvas = 0;
0475         return;
0476     }
0477 
0478     m_canvas = dynamic_cast<KisCanvas2*>(canvas);
0479 }
0480 
0481 
0482 void TouchDockerDock::unsetCanvas()
0483 {
0484     setEnabled(true);
0485     m_canvas = 0;
0486 }