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"