File indexing completed on 2024-06-02 04:38:12

0001 /* This file is part of Spectacle, the KDE screenshot utility
0002  * SPDX-FileCopyrightText: 2015 Boudhayan Gupta <bgupta@kde.org>
0003  * SPDX-FileCopyrightText: 2019 David Redondo <kde@david-redondo.de>
0004  * SPDX-FileCopyrightText: 2020 Ahmad Samir <a.samirh78@gmail.com>
0005  * SPDX-FileCopyrightText: 2022 Noah Davis <noahadvs@gmail.com>
0006  * SPDX-License-Identifier: LGPL-2.0-or-later
0007  */
0008 
0009 #include "SpectacleWindow.h"
0010 
0011 #include "ExportManager.h"
0012 #include "SpectacleCore.h"
0013 #include "Geometry.h"
0014 #include "Gui/ExportMenu.h"
0015 #include "Gui/HelpMenu.h"
0016 #include "Gui/OptionsMenu.h"
0017 #include "Gui/WidgetWindowUtils.h"
0018 #include "spectacle_gui_debug.h"
0019 
0020 #include <KIO/JobUiDelegateFactory>
0021 #include <KIO/OpenFileManagerWindowJob>
0022 #include <KSystemClipboard>
0023 #include <KWindowSystem>
0024 #include <QMimeData>
0025 
0026 #include <QApplication>
0027 #include <QColorDialog>
0028 #include <QFontDialog>
0029 #include <QtQml>
0030 #include <utility>
0031 
0032 using namespace Qt::StringLiterals;
0033 using G = Geometry;
0034 
0035 QList<SpectacleWindow *> SpectacleWindow::s_spectacleWindowInstances = {};
0036 bool SpectacleWindow::s_synchronizingVisibility = false;
0037 bool SpectacleWindow::s_synchronizingTitle = false;
0038 SpectacleWindow::TitlePreset SpectacleWindow::s_lastTitlePreset = Default;
0039 QString SpectacleWindow::s_previousTitle = QGuiApplication::applicationDisplayName();
0040 bool SpectacleWindow::s_synchronizingAnnotating = false;
0041 bool SpectacleWindow::s_isAnnotating = false;
0042 
0043 SpectacleWindow::SpectacleWindow(QQmlEngine *engine, QWindow *parent)
0044     : QQuickView(engine, parent)
0045     , m_context(new QQmlContext(engine->rootContext(), this))
0046 {
0047     s_spectacleWindowInstances.append(this);
0048 
0049     connect(engine, &QQmlEngine::quit, QCoreApplication::instance(), &QCoreApplication::quit, Qt::QueuedConnection);
0050     connect(this, &QQuickView::statusChanged, this, [](QQuickView::Status status){
0051         if (status == QQuickView::Error) {
0052             QCoreApplication::quit();
0053         }
0054     });
0055     connect(this, &SpectacleWindow::xChanged, this, &SpectacleWindow::logicalXChanged);
0056     connect(this, &SpectacleWindow::yChanged, this, &SpectacleWindow::logicalYChanged);
0057 
0058     setTextRenderType(QQuickWindow::NativeTextRendering);
0059 
0060     // set up QML
0061     setResizeMode(QQuickView::SizeRootObjectToView);
0062     m_context->setContextProperty(u"contextWindow"_s, this);
0063 }
0064 
0065 SpectacleWindow::~SpectacleWindow()
0066 {
0067     s_spectacleWindowInstances.removeOne(this);
0068 }
0069 
0070 qreal SpectacleWindow::logicalX() const
0071 {
0072     return G::mapFromPlatformValue(x(), devicePixelRatio());
0073 }
0074 
0075 qreal SpectacleWindow::logicalY() const
0076 {
0077     return G::mapFromPlatformValue(y(), devicePixelRatio());
0078 }
0079 
0080 bool SpectacleWindow::isAnnotating() const
0081 {
0082     return s_isAnnotating;
0083 }
0084 
0085 void SpectacleWindow::setAnnotating(bool annotating)
0086 {
0087     if (s_synchronizingAnnotating || s_isAnnotating == annotating) {
0088         return;
0089     }
0090     s_synchronizingAnnotating = true;
0091     s_isAnnotating = annotating;
0092     for (auto window : std::as_const(s_spectacleWindowInstances)) {
0093         Q_EMIT window->annotatingChanged();
0094     }
0095     s_synchronizingAnnotating = false;
0096 }
0097 
0098 void SpectacleWindow::unminimize()
0099 {
0100     setVisible(true);
0101     setWindowStates(windowStates().setFlag(Qt::WindowMinimized, false));
0102 }
0103 
0104 QList<SpectacleWindow *> SpectacleWindow::instances()
0105 {
0106     return s_spectacleWindowInstances;
0107 }
0108 
0109 void SpectacleWindow::setVisibilityForAll(QWindow::Visibility visibility)
0110 {
0111     if (s_synchronizingVisibility || s_spectacleWindowInstances.isEmpty()) {
0112         return;
0113     }
0114     s_synchronizingVisibility = true;
0115     for (auto window : std::as_const(s_spectacleWindowInstances)) {
0116         window->setVisibility(visibility);
0117     }
0118     s_synchronizingVisibility = false;
0119 }
0120 
0121 void SpectacleWindow::setTitleForAll(TitlePreset preset, const QString &fileName)
0122 {
0123     if (s_synchronizingTitle || s_spectacleWindowInstances.isEmpty()) {
0124         return;
0125     }
0126     s_synchronizingTitle = true;
0127 
0128     QString newTitle = titlePresetString(preset, fileName);
0129 
0130     if (!newTitle.isEmpty()) {
0131         if (s_lastTitlePreset != TitlePreset::Timer) {
0132             s_previousTitle = s_spectacleWindowInstances.constFirst()->title();
0133         }
0134         s_lastTitlePreset = preset;
0135 
0136         for (auto window : std::as_const(s_spectacleWindowInstances)) {
0137             window->setTitle(newTitle);
0138         }
0139     }
0140 
0141     s_synchronizingTitle = false;
0142 }
0143 
0144 void SpectacleWindow::closeAll()
0145 {
0146     // counting down should prevent invalid memory access
0147     for (int i = s_spectacleWindowInstances.count() - 1; i >= 0; --i) {
0148         s_spectacleWindowInstances[i]->close();
0149     }
0150 }
0151 
0152 qreal SpectacleWindow::dprRound(qreal value) const
0153 {
0154     return G::dprRound(value, devicePixelRatio());
0155 }
0156 
0157 QString SpectacleWindow::baseFileName(const QUrl &url) const
0158 {
0159     return url.fileName();
0160 }
0161 
0162 QString SpectacleWindow::titlePresetString(TitlePreset preset, const QString &fileName)
0163 {
0164     if (preset == TitlePreset::Timer) {
0165         return i18ncp("@title:window", "%1 second", "%1 seconds",
0166                       qCeil(SpectacleCore::instance()->captureTimeRemaining() / 1000.0));
0167     } else if (preset == TitlePreset::Unsaved) {
0168         return i18nc("@title:window Unsaved Screenshot", "Unsaved") + u"*"_s;
0169     } else if (preset == TitlePreset::Saved && !fileName.isEmpty()) {
0170         return fileName;
0171     } else if (preset == TitlePreset::Modified && !fileName.isEmpty()) {
0172         return fileName + u"*"_s;
0173     } else if (preset == TitlePreset::Previous && !s_previousTitle.isEmpty()) {
0174         return s_previousTitle;
0175     }
0176     return QGuiApplication::applicationDisplayName();
0177 }
0178 
0179 void SpectacleWindow::deleter(SpectacleWindow *window)
0180 {
0181     s_spectacleWindowInstances.removeOne(window);
0182     window->deleteLater();
0183 }
0184 
0185 void SpectacleWindow::setSource(const QUrl &source, const QVariantMap &initialProperties)
0186 {
0187     if (source.isEmpty()) {
0188         m_component.reset(nullptr);
0189         QQuickView::setSource(source);
0190         return;
0191     }
0192 
0193     m_component.reset(new QQmlComponent(engine(), source, this));
0194     auto *component = m_component.get();
0195     QObject *object = nullptr;
0196 
0197     if (component->isLoading()) {
0198         connect(component, &QQmlComponent::statusChanged,
0199                 this, [this, component, &source, &initialProperties]() {
0200             disconnect(component, &QQmlComponent::statusChanged, this, nullptr);
0201             QObject *object = nullptr;
0202             if (component->isReady()) {
0203                 if (!initialProperties.isEmpty()) {
0204                     object = component->createWithInitialProperties(initialProperties,
0205                                                                     m_context.get());
0206                 } else {
0207                     object = component->create(m_context.get());
0208                 }
0209             }
0210             setContent(source, component, object);
0211         });
0212     } else if (component->isReady()) {
0213         if (!initialProperties.isEmpty()) {
0214             object = component->createWithInitialProperties(initialProperties, m_context.get());
0215         } else {
0216             object = component->create(m_context.get());
0217         }
0218     }
0219 
0220     setContent(source, component, object);
0221 }
0222 
0223 void SpectacleWindow::save()
0224 {
0225     SpectacleCore::instance()->syncExportImage();
0226     ExportManager::instance()->exportImage(ExportManager::Save | ExportManager::UserAction,
0227                                            SpectacleCore::instance()->outputUrl());
0228 }
0229 
0230 void SpectacleWindow::saveAs()
0231 {
0232     if (SpectacleCore::instance()->videoMode()) {
0233         ExportManager::instance()->exportVideo(ExportManager::SaveAs | ExportManager::UserAction,
0234                                                SpectacleCore::instance()->currentVideo());
0235         return;
0236     }
0237     SpectacleCore::instance()->syncExportImage();
0238     ExportManager::instance()->exportImage(ExportManager::SaveAs | ExportManager::UserAction);
0239 }
0240 
0241 void SpectacleWindow::copyImage()
0242 {
0243     SpectacleCore::instance()->syncExportImage();
0244     ExportManager::instance()->exportImage(ExportManager::CopyImage | ExportManager::UserAction);
0245 }
0246 
0247 void SpectacleWindow::copyLocation()
0248 {
0249     if (SpectacleCore::instance()->videoMode()) {
0250         ExportManager::instance()->exportVideo(ExportManager::CopyPath | ExportManager::UserAction,
0251                                                SpectacleCore::instance()->currentVideo());
0252         return;
0253     }
0254     SpectacleCore::instance()->syncExportImage();
0255     ExportManager::instance()->exportImage(ExportManager::CopyPath | ExportManager::UserAction);
0256 }
0257 
0258 void SpectacleWindow::copyToClipboard(const QVariant &content)
0259 {
0260     auto data = new QMimeData();
0261     if (content.typeId() == QMetaType::QString) {
0262         data->setText(content.toString());
0263     } else if (content.typeId() == QMetaType::QByteArray) {
0264         data->setData(QStringLiteral("application/octet-stream"), content.toByteArray());
0265     }
0266     KSystemClipboard::instance()->setMimeData(data, QClipboard::Clipboard);
0267 }
0268 
0269 void SpectacleWindow::showPrintDialog()
0270 {
0271     SpectacleCore::instance()->syncExportImage();
0272     ExportMenu::instance()->openPrintDialog();
0273 }
0274 
0275 void SpectacleWindow::showPreferencesDialog()
0276 {
0277     OptionsMenu::instance()->showPreferencesDialog();
0278 }
0279 
0280 void SpectacleWindow::showFontDialog()
0281 {
0282     auto tool = SpectacleCore::instance()->annotationDocument()->tool();
0283     auto wrapper = SpectacleCore::instance()->annotationDocument()->selectedItemWrapper();
0284     QFont font;
0285     if (tool->type() == AnnotationTool::SelectTool
0286         || (tool->type() == AnnotationTool::TextTool
0287             && wrapper->options().testFlag(AnnotationTool::TextOption))
0288     ) {
0289         font = wrapper->font();
0290     } else {
0291         font = tool->font();
0292     }
0293     QFontDialog *dialog = new QFontDialog(font);
0294     dialog->setAttribute(Qt::WA_DeleteOnClose);
0295 
0296     setWidgetTransientParent(dialog, this);
0297 
0298     if (flags().testFlag(Qt::WindowStaysOnTopHint)) {
0299         dialog->setWindowFlag(Qt::WindowStaysOnTopHint);
0300     }
0301 
0302     connect(dialog, &QFontDialog::fontSelected, this, [](const QFont &font) {
0303         QFont newFont = font;
0304         // Copied from stripRegularStyleName() in KFontChooserDialog.
0305         // For more details see:
0306         // https://bugreports.qt.io/browse/QTBUG-63792
0307         // https://bugs.kde.org/show_bug.cgi?id=378523
0308         if (newFont.weight() == QFont::Normal
0309             && (newFont.styleName() == "Regular"_L1
0310                 || newFont.styleName() == "Normal"_L1
0311                 || newFont.styleName() == "Book"_L1
0312                 || newFont.styleName() == "Roman"_L1)) {
0313             newFont.setStyleName(QString());
0314         }
0315         auto tool = SpectacleCore::instance()->annotationDocument()->tool();
0316         auto wrapper = SpectacleCore::instance()->annotationDocument()->selectedItemWrapper();
0317         if (tool->type() == AnnotationTool::SelectTool) {
0318             wrapper->setFont(newFont);
0319             wrapper->commitChanges();
0320         } else if (tool->type() == AnnotationTool::TextTool
0321             && wrapper->options().testFlag(AnnotationTool::TextOption)
0322         ) {
0323             tool->setFont(newFont);
0324             wrapper->setFont(newFont);
0325             wrapper->commitChanges();
0326         } else {
0327             tool->setFont(newFont);
0328         }
0329     });
0330 
0331     // BUG https://bugs.kde.org/show_bug.cgi?id=478155:
0332     // Workaround modal font dialog being unusable.
0333     // This should probably be fixed in the plasma-integration.
0334     dialog->setModal(false);
0335     dialog->show();
0336 }
0337 
0338 void SpectacleWindow::showColorDialog(int option)
0339 {
0340     QColorDialog *dialog = nullptr;
0341     auto tool = SpectacleCore::instance()->annotationDocument()->tool();
0342     auto wrapper = SpectacleCore::instance()->annotationDocument()->selectedItemWrapper();
0343 
0344     std::function<QColor()> toolGetter;
0345     std::function<QColor()> wrapperGetter;
0346     std::function<void(const QColor &)> toolSetter;
0347     std::function<void(const QColor &)> wrapperSetter;
0348     using namespace std::placeholders; // for std::placeholders::_1
0349     if (option == AnnotationTool::StrokeOption) {
0350         toolGetter = std::bind(&AnnotationTool::strokeColor, tool);
0351         wrapperGetter = std::bind(&SelectedItemWrapper::strokeColor, wrapper);
0352         toolSetter = std::bind(&AnnotationTool::setStrokeColor, tool, _1);
0353         wrapperSetter = std::bind(&SelectedItemWrapper::setStrokeColor, wrapper, _1);
0354     } else if (option == AnnotationTool::FillOption) {
0355         toolGetter = std::bind(&AnnotationTool::fillColor, tool);
0356         wrapperGetter = std::bind(&SelectedItemWrapper::fillColor, wrapper);
0357         toolSetter = std::bind(&AnnotationTool::setFillColor, tool, _1);
0358         wrapperSetter = std::bind(&SelectedItemWrapper::setFillColor, wrapper, _1);
0359     } else if (option == AnnotationTool::FontOption) {
0360         toolGetter = std::bind(&AnnotationTool::fontColor, tool);
0361         wrapperGetter = std::bind(&SelectedItemWrapper::fontColor, wrapper);
0362         toolSetter = std::bind(&AnnotationTool::setFontColor, tool, _1);
0363         wrapperSetter = std::bind(&SelectedItemWrapper::setFontColor, wrapper, _1);
0364     } else {
0365         qmlWarning(this) << "invalid option argument";
0366         return;
0367     }
0368 
0369     QColor color;
0370     if (tool->type() == AnnotationTool::SelectTool
0371         || (tool->type() == AnnotationTool::TextTool
0372             && wrapper->options().testFlag(AnnotationTool::TextOption))
0373     ) {
0374         color = wrapperGetter();
0375     } else {
0376         color = toolGetter();
0377     }
0378 
0379     dialog = new QColorDialog(color);
0380     dialog->setAttribute(Qt::WA_DeleteOnClose);
0381     dialog->setOption(QColorDialog::ShowAlphaChannel);
0382 
0383     setWidgetTransientParent(dialog, this);
0384 
0385     if (flags().testFlag(Qt::WindowStaysOnTopHint)) {
0386         dialog->setWindowFlag(Qt::WindowStaysOnTopHint);
0387     }
0388 
0389     connect(dialog, &QColorDialog::colorSelected, this, [toolSetter, wrapperSetter](const QColor &color){
0390         auto tool = SpectacleCore::instance()->annotationDocument()->tool();
0391         auto wrapper = SpectacleCore::instance()->annotationDocument()->selectedItemWrapper();
0392         if (tool->type() == AnnotationTool::SelectTool) {
0393             wrapperSetter(color);
0394             wrapper->commitChanges();
0395         } else if (tool->type() == AnnotationTool::TextTool
0396             && wrapper->options().testFlag(AnnotationTool::TextOption)
0397         ) {
0398             toolSetter(color);
0399             wrapperSetter(color);
0400             wrapper->commitChanges();
0401         } else {
0402             toolSetter(color);
0403         }
0404     });
0405 
0406     dialog->open();
0407 }
0408 
0409 void SpectacleWindow::openContainingFolder(const QUrl &url)
0410 {
0411     KIO::highlightInFileManager({url});
0412 }
0413 
0414 void SpectacleWindow::mousePressEvent(QMouseEvent *event)
0415 {
0416     // QMenus need to be closed by hand when used from QML, see plasma-workspace/shellcorona.cpp
0417     if (auto popup = QApplication::activePopupWidget()) {
0418         popup->close();
0419         event->accept();
0420     } else {
0421         QQuickView::mousePressEvent(event);
0422     }
0423 }
0424 
0425 void SpectacleWindow::keyPressEvent(QKeyEvent *event)
0426 {
0427     // Events need to be processed normally first for events to reach items
0428     QQuickView::keyPressEvent(event);
0429     if (event->isAccepted()) {
0430         return;
0431     }
0432     m_pressedKeys = event->key() | event->modifiers();
0433 }
0434 
0435 void SpectacleWindow::keyReleaseEvent(QKeyEvent *event)
0436 {
0437     // Events need to be processed normally first for events to reach items
0438     QQuickView::keyReleaseEvent(event);
0439     if (event->isAccepted()) {
0440         return;
0441     }
0442     // Cancel defaults to Escape in QPlatformTheme.
0443     // Handling this here fixes https://bugs.kde.org/show_bug.cgi?id=428478
0444     if ((event->matches(QKeySequence::Quit)
0445         || event->matches(QKeySequence::Close)
0446         || event->matches(QKeySequence::Cancel))
0447         // We need to check if these were pressed previously or else pressing escape
0448         // in a dialog will quit spectacle when you release the escape key.
0449         && m_pressedKeys == event->key() | event->modifiers()
0450     ) {
0451         event->accept();
0452         auto spectacleCore = SpectacleCore::instance();
0453         spectacleCore->cancelScreenshot();
0454     } else if (event->matches(QKeySequence::Preferences)) {
0455         event->accept();
0456         showPreferencesDialog();
0457     } else if (event->matches(QKeySequence::New)) {
0458         event->accept();
0459         SpectacleCore::instance()->takeNewScreenshot();
0460     } else if (event->matches(QKeySequence::HelpContents)) {
0461         event->accept();
0462         HelpMenu::instance()->showAppHelp();
0463     }
0464     m_pressedKeys = {};
0465 }
0466 
0467 #include "moc_SpectacleWindow.cpp"