diff --git a/CMakeLists.txt b/CMakeLists.txt index 23ad938..2e8d07b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,130 +1,131 @@ # KDE Application Version, managed by release script set(KDE_APPLICATIONS_VERSION_MAJOR "19") set(KDE_APPLICATIONS_VERSION_MINOR "08") set(KDE_APPLICATIONS_VERSION_MICRO "1") set(KDE_APPLICATIONS_VERSION "${KDE_APPLICATIONS_VERSION_MAJOR}.${KDE_APPLICATIONS_VERSION_MINOR}.${KDE_APPLICATIONS_VERSION_MICRO}") set(SPECTACLE_VERSION ${KDE_APPLICATIONS_VERSION}) # minimum requirements cmake_minimum_required (VERSION 3.5 FATAL_ERROR) # Spectacle project project(Spectacle VERSION ${SPECTACLE_VERSION}) set(QT_MIN_VERSION "5.10.0") set(KF5_MIN_VERSION "5.53.0") set(PLASMA_MIN_VERSION "5.12.0") find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) set( CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH} ) # require c++14 set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) # set up kf5 include(KDEInstallDirs) include(KDECMakeSettings) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(ECMInstallIcons) include(ECMSetupVersion) include(FeatureSummary) include(ECMQtDeclareLoggingCategory) include(ECMAddTests) add_definitions(-DQT_NO_URL_CAST_FROM_STRING -DQT_NO_CAST_FROM_ASCII) find_package( Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED Core Widgets DBus PrintSupport Test ) find_package( KF5 ${KF5_MIN_VERSION} REQUIRED CoreAddons WidgetsAddons DBusAddons Notifications Config I18n KIO WindowSystem DocTools NewStuff GlobalAccel XmlGui + Wayland ) if(${KF5Config_VERSION} STRGREATER "5.56.0") add_definitions(-DQT_NO_FOREACH) endif() # optional components find_package(KF5Kipi) if (KF5Kipi_FOUND) set(KIPI_FOUND 1) endif () find_package(KF5Purpose) if (KF5Purpose_FOUND) set(PURPOSE_FOUND 1) endif() find_package(XCB COMPONENTS XFIXES IMAGE UTIL CURSOR) set(XCB_COMPONENTS_ERRORS FALSE) if (XCB_FOUND) find_package(Qt5X11Extras ${QT_MIN_VERSION} REQUIRED) endif() set(XCB_COMPONENTS_FOUND TRUE) if(NOT XCB_XFIXES_FOUND) set(XCB_COMPONENTS_ERRORS "${XCB_COMPONENTS_ERRORS} XCB-XFIXES ") set(XCB_COMPONENTS_FOUND FALSE) endif() if(NOT XCB_IMAGE_FOUND) set(XCB_COMPONENTS_ERRORS "${XCB_COMPONENTS_ERRORS} XCB-IMAGE ") set(XCB_COMPONENTS_FOUND FALSE) endif() if(NOT XCB_UTIL_FOUND) set(XCB_COMPONENTS_ERRORS "${XCB_COMPONENTS_ERRORS} XCB-UTIL ") set(XCB_COMPONENTS_FOUND FALSE) endif() if(NOT XCB_CURSOR_FOUND) set(XCB_COMPONENTS_ERRORS "${XCB_COMPONENTS_ERRORS} XCB-CURSOR ") set(XCB_COMPONENTS_FOUND FALSE) endif() # fail build if none of the platform backends can be found if (NOT XCB_FOUND OR NOT XCB_COMPONENTS_FOUND) message(FATAL_ERROR "No suitable backend platform was found. Currently supported platforms are: XCB Components Required: ${XCB_COMPONENTS_ERRORS}") endif() # hand off to subdirectories add_subdirectory(src) add_subdirectory(dbus) add_subdirectory(desktop) add_subdirectory(icons) add_subdirectory(doc) add_subdirectory(tests) if (${ECM_VERSION} STRGREATER "5.58.0") install(FILES spectacle.categories DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}) else() install(FILES spectacle.categories DESTINATION ${KDE_INSTALL_CONFDIR}) endif() # summaries feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index feeefaf..c93eca1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,113 +1,114 @@ # common - configure file and version definitions configure_file(Config.h.in ${CMAKE_CURRENT_BINARY_DIR}/Config.h) set(CMAKE_AUTORCC 1) # target if(XCB_FOUND) set( SPECTACLE_SRCS_PLATFORM_XCB Platforms/PlatformXcb.cpp ) endif() set( SPECTACLE_SRCS_PLATFORM Platforms/PlatformLoader.cpp Platforms/Platform.cpp Platforms/PlatformNull.cpp Platforms/PlatformKWinWayland.cpp ${SPECTACLE_SRCS_PLATFORM_XCB} ) set( SPECTACLE_SRCS_DEFAULT Main.cpp ExportManager.cpp SpectacleCore.cpp SpectacleConfig.cpp SpectacleDBusAdapter.cpp ${SPECTACLE_SRCS_PLATFORM} Gui/KSMainWindow.cpp Gui/KSWidget.cpp Gui/KSImageWidget.cpp Gui/ExportMenu.cpp Gui/SmartSpinBox.cpp Gui/SettingsDialog/SettingsDialog.cpp Gui/SettingsDialog/SettingsPage.cpp Gui/SettingsDialog/SaveOptionsPage.cpp Gui/SettingsDialog/GeneralOptionsPage.cpp Gui/SettingsDialog/ShortcutsOptionsPage.cpp QuickEditor/QuickEditor.cpp ) ecm_qt_declare_logging_category(SPECTACLE_SRCS_DEFAULT HEADER spectacle_core_debug.h IDENTIFIER SPECTACLE_CORE_LOG CATEGORY_NAME org.kde.spectacle.core) ecm_qt_declare_logging_category(SPECTACLE_SRCS_DEFAULT HEADER spectacle_gui_debug.h IDENTIFIER SPECTACLE_GUI_LOG CATEGORY_NAME org.kde.spectacle.gui) if(KIPI_FOUND) set( SPECTACLE_SRCS_KIPI KipiInterface/KSGKipiInterface.cpp KipiInterface/KSGKipiInfoShared.cpp KipiInterface/KSGKipiImageCollectionShared.cpp KipiInterface/KSGKipiImageCollectionSelector.cpp ) endif() set( SPECTACLE_SRCS_ALL ${SPECTACLE_SRCS_DEFAULT} ${SPECTACLE_SRCS_KIPI} ) add_executable( spectacle ${SPECTACLE_SRCS_ALL} ) # link libraries target_link_libraries( spectacle Qt5::DBus Qt5::PrintSupport KF5::CoreAddons KF5::DBusAddons KF5::WidgetsAddons KF5::Notifications KF5::ConfigCore KF5::I18n KF5::KIOWidgets KF5::WindowSystem KF5::NewStuff KF5::GlobalAccel KF5::XmlGui + KF5::WaylandClient ) if(XCB_FOUND) target_link_libraries( spectacle XCB::XFIXES XCB::IMAGE XCB::CURSOR XCB::UTIL Qt5::X11Extras ) endif() if(KIPI_FOUND) target_link_libraries ( spectacle KF5::Kipi ) endif() if(PURPOSE_FOUND) target_link_libraries ( spectacle KF5::PurposeWidgets ) endif() install(TARGETS spectacle ${INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/src/QuickEditor/QuickEditor.cpp b/src/QuickEditor/QuickEditor.cpp index 36c263b..6494f33 100644 --- a/src/QuickEditor/QuickEditor.cpp +++ b/src/QuickEditor/QuickEditor.cpp @@ -1,847 +1,861 @@ /* * Copyright (C) 2018 Ambareesh "Amby" Balaji * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include +#include +#include #include #include #include "QuickEditor.h" #include "SpectacleConfig.h" const qreal QuickEditor::mouseAreaSize = 20.0; const qreal QuickEditor::cornerHandleRadius = 8.0; const qreal QuickEditor::midHandleRadius = 5.0; const int QuickEditor::selectionSizeThreshold = 100; const int QuickEditor::selectionBoxPaddingX = 5; const int QuickEditor::selectionBoxPaddingY = 4; const int QuickEditor::selectionBoxMarginY = 2; bool QuickEditor::bottomHelpTextPrepared = false; const int QuickEditor::bottomHelpBoxPaddingX = 12; const int QuickEditor::bottomHelpBoxPaddingY = 8; const int QuickEditor::bottomHelpBoxPairSpacing = 6; const int QuickEditor::bottomHelpBoxMarginBottom = 5; const int QuickEditor::midHelpTextFontSize = 12; const int QuickEditor::magnifierLargeStep = 15; const int QuickEditor::magZoom = 5; const int QuickEditor::magPixels = 16; const int QuickEditor::magOffset = 32; -QuickEditor::QuickEditor(const QPixmap& thePixmap, QWidget *parent) : +QuickEditor::QuickEditor(const QPixmap &thePixmap, KWayland::Client::PlasmaShell *plasmashell, QWidget *parent) : QWidget(parent), mMaskColor(QColor::fromRgbF(0, 0, 0, 0.15)), mStrokeColor(palette().highlight().color()), mCrossColor(QColor::fromRgbF(mStrokeColor.redF(), mStrokeColor.greenF(), mStrokeColor.blueF(), 0.7)), mLabelBackgroundColor(QColor::fromRgbF( palette().light().color().redF(), palette().light().color().greenF(), palette().light().color().blueF(), 0.85 )), mLabelForegroundColor(palette().windowText().color()), mMidHelpText(i18n("Click and drag to draw a selection rectangle,\nor press Esc to quit")), mMidHelpTextFont(font()), mBottomHelpTextFont(font()), mBottomHelpGridLeftWidth(0), mMouseDragState(MouseState::None), mPixmap(thePixmap), mMagnifierAllowed(false), mShowMagnifier(SpectacleConfig::instance()->showMagnifierChecked()), mToggleMagnifier(false), mReleaseToCapture(SpectacleConfig::instance()->useReleaseToCapture()), mRememberRegion(SpectacleConfig::instance()->alwaysRememberRegion() || SpectacleConfig::instance()->rememberLastRectangularRegion()), mDisableArrowKeys(false), mPrimaryScreenGeo(QGuiApplication::primaryScreen()->geometry()), mbottomHelpLength(bottomHelpMaxLength) { SpectacleConfig *config = SpectacleConfig::instance(); if (config->useLightRegionMaskColour()) { mMaskColor = QColor(255, 255, 255, 100); } setMouseTracking(true); setAttribute(Qt::WA_StaticContents); setWindowFlags(Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint | Qt::Popup | Qt::WindowStaysOnTopHint); dprI = 1.0 / devicePixelRatioF(); setGeometry(0, 0, static_cast(mPixmap.width() * dprI), static_cast(mPixmap.height() * dprI)); - + // TODO This is a hack until a better interface is available + if (plasmashell) { + using namespace KWayland::Client; + winId(); + auto surface = Surface::fromWindow(windowHandle()); + if (!surface) { + return; + } + PlasmaShellSurface *plasmashellSurface = plasmashell->createSurface(surface, this); + plasmashellSurface->setRole(PlasmaShellSurface::Role::Panel); + plasmashellSurface->setPanelTakesFocus(true); + plasmashellSurface->setPosition(geometry().topLeft()); + } if (config->rememberLastRectangularRegion()) { QRect cropRegion = config->cropRegion(); if (!cropRegion.isEmpty()) { mSelection = QRectF( cropRegion.x() * dprI, cropRegion.y() * dprI, cropRegion.width() * dprI, cropRegion.height() * dprI ).intersected(geometry()); } setMouseCursor(QCursor::pos()); } else { setCursor(Qt::CrossCursor); } setBottomHelpText(); mMidHelpTextFont.setPointSize(midHelpTextFontSize); if (!bottomHelpTextPrepared) { bottomHelpTextPrepared = true; const auto prepare = [this](QStaticText& item) { item.prepare(QTransform(), mBottomHelpTextFont); item.setPerformanceHint(QStaticText::AggressiveCaching); }; for (auto& pair : mBottomHelpText) { prepare(pair.first); for (auto &item : pair.second) { prepare(item); } } } layoutBottomHelpText(); update(); } void QuickEditor::acceptSelection() { if (!mSelection.isEmpty()) { const qreal dpr = devicePixelRatioF(); QRect scaledCropRegion = QRect( qRound(mSelection.x() * dpr), qRound(mSelection.y() * dpr), qRound(mSelection.width() * dpr), qRound(mSelection.height() * dpr) ); SpectacleConfig::instance()->setCropRegion(scaledCropRegion); emit grabDone(mPixmap.copy(scaledCropRegion)); } } void QuickEditor::keyPressEvent(QKeyEvent* event) { const auto modifiers = event->modifiers(); const bool shiftPressed = modifiers & Qt::ShiftModifier; if (shiftPressed) { mToggleMagnifier = true; } switch(event->key()) { case Qt::Key_Escape: emit grabCancelled(); break; case Qt::Key_Return: case Qt::Key_Enter: acceptSelection(); break; case Qt::Key_Up: { if(mDisableArrowKeys) { update(); break; } const qreal step = (shiftPressed ? 1 : magnifierLargeStep); const int newPos = boundsUp(qRound(mSelection.top() * devicePixelRatioF() - step), false); if (modifiers & Qt::AltModifier) { mSelection.setBottom(dprI * newPos + mSelection.height()); mSelection = mSelection.normalized(); } else { mSelection.moveTop(dprI * newPos); } update(); break; } case Qt::Key_Right: { if(mDisableArrowKeys) { update(); break; } const qreal step = (shiftPressed ? 1 : magnifierLargeStep); const int newPos = boundsRight(qRound(mSelection.left() * devicePixelRatioF() + step), false); if (modifiers & Qt::AltModifier) { mSelection.setRight(dprI * newPos + mSelection.width()); } else { mSelection.moveLeft(dprI * newPos); } update(); break; } case Qt::Key_Down: { if(mDisableArrowKeys) { update(); break; } const qreal step = (shiftPressed ? 1 : magnifierLargeStep); const int newPos = boundsDown(qRound(mSelection.top() * devicePixelRatioF() + step), false); if (modifiers & Qt::AltModifier) { mSelection.setBottom(dprI * newPos + mSelection.height()); } else { mSelection.moveTop(dprI * newPos); } update(); break; } case Qt::Key_Left: { if(mDisableArrowKeys) { update(); break; } const qreal step = (shiftPressed ? 1 : magnifierLargeStep); const int newPos = boundsLeft(qRound(mSelection.left() * devicePixelRatioF() - step), false); if (modifiers & Qt::AltModifier) { mSelection.setRight(dprI * newPos + mSelection.width()); mSelection = mSelection.normalized(); } else { mSelection.moveLeft(dprI * newPos); } update(); break; } default: break; } event->accept(); } void QuickEditor::keyReleaseEvent(QKeyEvent* event) { if (mToggleMagnifier && !(event->modifiers() & Qt::ShiftModifier)) { mToggleMagnifier = false; update(); } event->accept(); } int QuickEditor::boundsLeft(int newTopLeftX, const bool mouse) { if (newTopLeftX < 0) { if (mouse) { // tweak startPos to prevent rectangle from getting stuck mStartPos.setX(mStartPos.x() + newTopLeftX * dprI); } newTopLeftX = 0; } return newTopLeftX; } int QuickEditor::boundsRight(int newTopLeftX, const bool mouse) { // the max X coordinate of the top left point const int realMaxX = qRound((width() - mSelection.width()) * devicePixelRatioF()); const int xOffset = newTopLeftX - realMaxX; if (xOffset > 0) { if (mouse) { mStartPos.setX(mStartPos.x() + xOffset * dprI); } newTopLeftX = realMaxX; } return newTopLeftX; } int QuickEditor::boundsUp(int newTopLeftY, const bool mouse) { if (newTopLeftY < 0) { if (mouse) { mStartPos.setY(mStartPos.y() + newTopLeftY * dprI); } newTopLeftY = 0; } return newTopLeftY; } int QuickEditor::boundsDown(int newTopLeftY, const bool mouse) { // the max Y coordinate of the top left point const int realMaxY = qRound((height() - mSelection.height()) * devicePixelRatioF()); const int yOffset = newTopLeftY - realMaxY; if (yOffset > 0) { if (mouse) { mStartPos.setY(mStartPos.y() + yOffset * dprI); } newTopLeftY = realMaxY; } return newTopLeftY; } void QuickEditor::mousePressEvent(QMouseEvent* event) { if (event->button() & Qt::LeftButton) { /* NOTE Workaround for Bug 407843 * If we show the selection Widget when a right click menu is open we lose focus on X. * When the user clicks we get the mouse back. We can only grab the keyboard if we already * have mouse focus. So just grab it undconditionally here. */ grabKeyboard(); const QPointF& pos = event->pos(); mMousePos = pos; mMagnifierAllowed = true; mMouseDragState = mouseLocation(pos); mDisableArrowKeys = true; switch(mMouseDragState) { case MouseState::Outside: mStartPos = pos; break; case MouseState::Inside: mStartPos = pos; mMagnifierAllowed = false; mInitialTopLeft = mSelection.topLeft(); setCursor(Qt::ClosedHandCursor); break; case MouseState::Top: case MouseState::Left: case MouseState::TopLeft: mStartPos = mSelection.bottomRight(); break; case MouseState::Bottom: case MouseState::Right: case MouseState::BottomRight: mStartPos = mSelection.topLeft(); break; case MouseState::TopRight: mStartPos = mSelection.bottomLeft(); break; case MouseState::BottomLeft: mStartPos = mSelection.topRight(); break; default: break; } } if (mMagnifierAllowed) { update(); } event->accept(); } void QuickEditor::mouseMoveEvent(QMouseEvent* event) { const QPointF& pos = event->pos(); mMousePos = pos; mMagnifierAllowed = true; switch (mMouseDragState) { case MouseState::None: { setMouseCursor(pos); mMagnifierAllowed = false; break; } case MouseState::TopLeft: case MouseState::TopRight: case MouseState::BottomRight: case MouseState::BottomLeft: { const bool afterX = pos.x() >= mStartPos.x(); const bool afterY = pos.y() >= mStartPos.y(); mSelection.setRect( afterX ? mStartPos.x() : pos.x(), afterY ? mStartPos.y() : pos.y(), qAbs(pos.x() - mStartPos.x()) + (afterX ? dprI : 0), qAbs(pos.y() - mStartPos.y()) + (afterY ? dprI : 0) ); update(); break; } case MouseState::Outside: { mSelection.setRect( qMin(pos.x(), mStartPos.x()), qMin(pos.y(), mStartPos.y()), qAbs(pos.x() - mStartPos.x()) + dprI, qAbs(pos.y() - mStartPos.y()) + dprI ); update(); break; } case MouseState::Top: case MouseState::Bottom: { const bool afterY = pos.y() >= mStartPos.y(); mSelection.setRect( mSelection.x(), afterY ? mStartPos.y() : pos.y(), mSelection.width(), qAbs(pos.y() - mStartPos.y()) + (afterY ? dprI : 0) ); update(); break; } case MouseState::Right: case MouseState::Left: { const bool afterX = pos.x() >= mStartPos.x(); mSelection.setRect( afterX ? mStartPos.x() : pos.x(), mSelection.y(), qAbs(pos.x() - mStartPos.x()) + (afterX ? dprI : 0), mSelection.height() ); update(); break; } case MouseState::Inside: { mMagnifierAllowed = false; // We use some math here to figure out if the diff with which we // move the rectangle with moves it out of bounds, // in which case we adjust the diff to not let that happen const qreal dpr = devicePixelRatioF(); // new top left point of the rectangle QPoint newTopLeft = ((pos - mStartPos + mInitialTopLeft) * dpr).toPoint(); int newTopLeftX = boundsLeft(newTopLeft.x()); if (newTopLeftX != 0) { newTopLeftX = boundsRight(newTopLeftX); } int newTopLeftY = boundsUp(newTopLeft.y()); if (newTopLeftY != 0) { newTopLeftY = boundsDown(newTopLeftY); } const auto newTopLeftF = QPointF(newTopLeftX * dprI, newTopLeftY * dprI); mSelection.moveTo(newTopLeftF); update(); break; } default: break; } event->accept(); } void QuickEditor::mouseReleaseEvent(QMouseEvent* event) { const auto button = event->button(); if (button == Qt::LeftButton) { mDisableArrowKeys = false; if(mMouseDragState == MouseState::Inside) { setCursor(Qt::OpenHandCursor); } else if(mMouseDragState == MouseState::Outside && mReleaseToCapture) { event->accept(); mMouseDragState = MouseState::None; return acceptSelection(); } } else if (button == Qt::RightButton) { mSelection.setWidth(0); mSelection.setHeight(0); } event->accept(); mMouseDragState = MouseState::None; update(); } void QuickEditor::mouseDoubleClickEvent(QMouseEvent* event) { event->accept(); if (event->button() == Qt::LeftButton && mSelection.contains(event->pos())) { acceptSelection(); } } void QuickEditor::paintEvent(QPaintEvent*) { QPainter painter(this); painter.setRenderHints(QPainter::Antialiasing); QBrush brush(mPixmap); brush.setTransform(QTransform().scale(dprI, dprI)); painter.setBackground(brush); painter.eraseRect(geometry()); if (!mSelection.size().isEmpty() || mMouseDragState != MouseState::None) { painter.fillRect(mSelection, mStrokeColor); const QRectF innerRect = mSelection.adjusted(1, 1, -1, -1); if (innerRect.width() > 0 && innerRect.height() > 0) { painter.eraseRect(mSelection.adjusted(1, 1, -1, -1)); } QRectF top(0, 0, width(), mSelection.top()); QRectF right(mSelection.right(), mSelection.top(), width() - mSelection.right(), mSelection.height()); QRectF bottom(0, mSelection.bottom(), width(), height() - mSelection.bottom()); QRectF left(0, mSelection.top(), mSelection.left(), mSelection.height()); for (const auto& rect : { top, right, bottom, left }) { painter.fillRect(rect, mMaskColor); } drawSelectionSizeTooltip(painter); if (mMouseDragState == MouseState::None) { // mouse is up if ((mSelection.width() > 20) && (mSelection.height() > 20)) { drawDragHandles(painter); } } else if (mMagnifierAllowed && (mShowMagnifier ^ mToggleMagnifier)) { drawMagnifier(painter); } drawBottomHelpText(painter); } else { drawMidHelpText(painter); } } void QuickEditor::layoutBottomHelpText() { int maxRightWidth = 0; int contentWidth = 0; int contentHeight = 0; mBottomHelpGridLeftWidth = 0; for (int i = 0; i < mbottomHelpLength; i++) { const auto& item = mBottomHelpText[i]; const auto& left = item.first; const auto& right = item.second; const auto leftSize = left.size().toSize(); mBottomHelpGridLeftWidth = qMax(mBottomHelpGridLeftWidth, leftSize.width()); for (const auto& item : right) { const auto rightItemSize = item.size().toSize(); maxRightWidth = qMax(maxRightWidth, rightItemSize.width()); contentHeight += rightItemSize.height(); } contentWidth = qMax(contentWidth, mBottomHelpGridLeftWidth + maxRightWidth + bottomHelpBoxPairSpacing); contentHeight += (i != bottomHelpMaxLength ? bottomHelpBoxMarginBottom : 0); } mBottomHelpContentPos.setX((mPrimaryScreenGeo.width() - contentWidth) / 2 + mPrimaryScreenGeo.x()); mBottomHelpContentPos.setY(height() - contentHeight - 8); mBottomHelpGridLeftWidth += mBottomHelpContentPos.x(); mBottomHelpBorderBox.setRect( mBottomHelpContentPos.x() - bottomHelpBoxPaddingX, mBottomHelpContentPos.y() - bottomHelpBoxPaddingY, contentWidth + bottomHelpBoxPaddingX * 2, contentHeight + bottomHelpBoxPaddingY * 2 - 1 ); } void QuickEditor::setBottomHelpText() { if (mReleaseToCapture) { if(mRememberRegion && !mSelection.size().isEmpty()) { //Release to capture enabled and saved region available mBottomHelpText[0] = {QStaticText(i18nc("Mouse action", "Click and drag,")),{QStaticText(i18n(" "))}}; mBottomHelpText[1] = {QStaticText(i18nc("Keyboard/mouse action", "Enter, double-click:")), {QStaticText(i18n("Take screenshot"))}}; mBottomHelpText[2] = {QStaticText(i18nc("Keyboard action", "Shift:")), { QStaticText(i18nc("Shift key action first half", "Hold to toggle magnifier")), QStaticText(i18nc("Shift key action second half", "while dragging selection handles")) }}; mBottomHelpText[3] = {QStaticText(i18nc("Keyboard action", "Arrow keys:")), { QStaticText(i18nc("Shift key action first line", "Move selection rectangle")), QStaticText(i18nc("Shift key action second line", "Hold Alt to resize, Shift to fine‑tune")) }}; mBottomHelpText[4] = {QStaticText(i18nc("Mouse action", "Right-click:")), {QStaticText(i18n("Reset selection"))}}; mBottomHelpText[5] = {QStaticText(i18nc("Keyboard action", "Esc:")), {QStaticText(i18n("Cancel"))}}; } else { //Release to capture enabled and NO saved region available mbottomHelpLength = 4; mBottomHelpText[0] = {QStaticText(i18nc("Keyboard/mouse action", "Release left-click, Enter:")), {QStaticText(i18n("Take Screenshot"))}}; mBottomHelpText[1] = {QStaticText(i18nc("Keyboard action", "Shift:")), { QStaticText(i18nc("Shift key action first half", "Hold to toggle magnifier"))}}; mBottomHelpText[2] = {QStaticText(i18nc("Mouse action", "Right-click:")), {QStaticText(i18n("Reset selection"))}}; mBottomHelpText[3] = {QStaticText(i18nc("Keyboard action", "Esc:")), {QStaticText(i18n("Cancel"))}}; } }else { //Default text, Release to capture option disabled mbottomHelpLength = 5; mBottomHelpText[0] = {QStaticText(i18nc("Keyboard/mouse action", "Enter, double-click:")), {QStaticText(i18n("Take screenshot"))}}; mBottomHelpText[1] = {QStaticText(i18nc("Keyboard action", "Shift:")), { QStaticText(i18nc("Shift key action first half", "Hold to toggle magnifier")), QStaticText(i18nc("Shift key action second half", "while dragging selection handles")) }}; mBottomHelpText[2] = {QStaticText(i18nc("Keyboard action", "Arrow keys:")), { QStaticText(i18nc("Shift key action first line", "Move selection rectangle")), QStaticText(i18nc("Shift key action second line", "Hold Alt to resize, Shift to fine‑tune")) }}; mBottomHelpText[3] = {QStaticText(i18nc("Mouse action", "Right-click:")), {QStaticText(i18n("Reset selection"))}}; mBottomHelpText[4] = {QStaticText(i18nc("Keyboard action", "Esc:")), {QStaticText(i18n("Cancel"))}}; } } void QuickEditor::drawBottomHelpText(QPainter &painter) { if (mSelection.intersects(mBottomHelpBorderBox)) { return; } painter.setBrush(mLabelBackgroundColor); painter.setPen(mLabelForegroundColor); painter.setFont(mBottomHelpTextFont); painter.setRenderHint(QPainter::Antialiasing, false); painter.drawRect(mBottomHelpBorderBox); painter.setRenderHint(QPainter::Antialiasing, true); int topOffset = mBottomHelpContentPos.y(); for (int i = 0; i < mbottomHelpLength; i++) { const auto& item = mBottomHelpText[i]; const auto& left = item.first; const auto& right = item.second; const auto leftSize = left.size().toSize(); painter.drawStaticText(mBottomHelpGridLeftWidth - leftSize.width(), topOffset, left); for (const auto& item : right) { const auto rightItemSize = item.size().toSize(); painter.drawStaticText(mBottomHelpGridLeftWidth + bottomHelpBoxPairSpacing, topOffset, item); topOffset += rightItemSize.height(); } if (i != bottomHelpMaxLength) { topOffset += bottomHelpBoxMarginBottom; } } } void QuickEditor::drawDragHandles(QPainter& painter) { const qreal left = mSelection.x(); const qreal width = mSelection.width(); const qreal centerX = left + width / 2.0; const qreal right = left + width; const qreal top = mSelection.y(); const qreal height = mSelection.height(); const qreal centerY = top + height / 2.0; const qreal bottom = top + height; // start a path QPainterPath path; const qreal cornerHandleDiameter = 2 * cornerHandleRadius; // x and y coordinates of handle arcs const qreal leftHandle = left - cornerHandleRadius; const qreal topHandle = top - cornerHandleRadius; const qreal rightHandle = right - cornerHandleRadius; const qreal bottomHandle = bottom - cornerHandleRadius; const qreal centerHandleX = centerX - midHandleRadius; const qreal centerHandleY = centerY - midHandleRadius; // top-left handle path.moveTo(left, top); path.arcTo(leftHandle, topHandle, cornerHandleDiameter, cornerHandleDiameter, 0, -90); // top-right handle path.moveTo(right, top); path.arcTo(rightHandle, topHandle, cornerHandleDiameter, cornerHandleDiameter, 180, 90); // bottom-left handle path.moveTo(left, bottom); path.arcTo(leftHandle, bottomHandle, cornerHandleDiameter, cornerHandleDiameter, 0, 90); // bottom-right handle path.moveTo(right, bottom); path.arcTo(rightHandle, bottomHandle, cornerHandleDiameter, cornerHandleDiameter, 180, -90); const qreal midHandleDiameter = 2 * midHandleRadius; // top-center handle path.moveTo(centerX, top); path.arcTo(centerHandleX, top - midHandleRadius, midHandleDiameter, midHandleDiameter, 0, -180); // right-center handle path.moveTo(right, centerY); path.arcTo(right - midHandleRadius, centerHandleY, midHandleDiameter, midHandleDiameter, 90, 180); // bottom-center handle path.moveTo(centerX, bottom); path.arcTo(centerHandleX, bottom - midHandleRadius, midHandleDiameter, midHandleDiameter, 0, 180); // left-center handle path.moveTo(left, centerY); path.arcTo(left - midHandleRadius, centerHandleY, midHandleDiameter, midHandleDiameter, 90, -180); // draw the path painter.fillPath(path, mStrokeColor); } void QuickEditor::drawMagnifier(QPainter &painter) { const int pixels = 2 * magPixels + 1; int magX = static_cast(mMousePos.x() * devicePixelRatioF() - magPixels); int offsetX = 0; if (magX < 0) { offsetX = magX; magX = 0; } else { const int maxX = mPixmap.width() - pixels; if (magX > maxX) { offsetX = magX - maxX; magX = maxX; } } int magY = static_cast(mMousePos.y() * devicePixelRatioF() - magPixels); int offsetY = 0; if (magY < 0) { offsetY = magY; magY = 0; } else { const int maxY = mPixmap.height() - pixels; if (magY > maxY) { offsetY = magY - maxY; magY = maxY; } } QRectF magniRect(magX, magY, pixels, pixels); qreal drawPosX = mMousePos.x() + magOffset + pixels * magZoom / 2; if (drawPosX > width() - pixels * magZoom / 2) { drawPosX = mMousePos.x() - magOffset - pixels * magZoom / 2; } qreal drawPosY = mMousePos.y() + magOffset + pixels * magZoom / 2; if (drawPosY > height() - pixels * magZoom / 2) { drawPosY = mMousePos.y() - magOffset - pixels * magZoom / 2; } QPointF drawPos(drawPosX, drawPosY); QRectF crossHairTop(drawPos.x() + magZoom * (offsetX - 0.5), drawPos.y() - magZoom * (magPixels + 0.5), magZoom, magZoom * (magPixels + offsetY)); QRectF crossHairRight(drawPos.x() + magZoom * (0.5 + offsetX), drawPos.y() + magZoom * (offsetY - 0.5), magZoom * (magPixels - offsetX), magZoom); QRectF crossHairBottom(drawPos.x() + magZoom * (offsetX - 0.5), drawPos.y() + magZoom * (0.5 + offsetY), magZoom, magZoom * (magPixels - offsetY)); QRectF crossHairLeft(drawPos.x() - magZoom * (magPixels + 0.5), drawPos.y() + magZoom * (offsetY - 0.5), magZoom * (magPixels + offsetX), magZoom); QRectF crossHairBorder(drawPos.x() - magZoom * (magPixels + 0.5) - 1, drawPos.y() - magZoom * (magPixels + 0.5) - 1, pixels * magZoom + 2, pixels * magZoom + 2); const auto frag = QPainter::PixmapFragment::create(drawPos, magniRect, magZoom, magZoom); painter.fillRect(crossHairBorder, mLabelForegroundColor); painter.drawPixmapFragments(&frag, 1, mPixmap, QPainter::OpaqueHint); painter.setCompositionMode(QPainter::CompositionMode_SourceOver); for (auto& rect : { crossHairTop, crossHairRight, crossHairBottom, crossHairLeft }) { painter.fillRect(rect, mCrossColor); } } void QuickEditor::drawMidHelpText(QPainter &painter) { painter.fillRect(geometry(), mMaskColor); painter.setFont(mMidHelpTextFont); QRect textSize = painter.boundingRect(QRect(), Qt::AlignCenter, mMidHelpText); QPoint pos((mPrimaryScreenGeo.width() - textSize.width()) / 2 + mPrimaryScreenGeo.x(), (height() - textSize.height()) / 2); painter.setBrush(mLabelBackgroundColor); QPen pen(mLabelForegroundColor); pen.setWidth(2); painter.setPen(pen); painter.drawRoundedRect(QRect(pos.x() - 20, pos.y() - 20, textSize.width() + 40, textSize.height() + 40), 4, 4); painter.setCompositionMode(QPainter::CompositionMode_Source); painter.drawText(QRect(pos, textSize.size()), Qt::AlignCenter, mMidHelpText); } void QuickEditor::drawSelectionSizeTooltip(QPainter &painter) { // Set the selection size and finds the most appropriate position: // - vertically centered inside the selection if the box is not covering the a large part of selection // - on top of the selection if the selection x position fits the box height plus some margin // - at the bottom otherwise const qreal dpr = devicePixelRatioF(); QString selectionSizeText = ki18n("%1×%2").subs(qRound(mSelection.width() * dpr)).subs(qRound(mSelection.height() * dpr)).toString(); const QRect selectionSizeTextRect = painter.boundingRect(QRect(), 0, selectionSizeText); const int selectionBoxWidth = selectionSizeTextRect.width() + selectionBoxPaddingX * 2; const int selectionBoxHeight = selectionSizeTextRect.height() + selectionBoxPaddingY * 2; const int selectionBoxX = qBound( 0, static_cast(mSelection.x()) + (static_cast(mSelection.width()) - selectionSizeTextRect.width()) / 2 - selectionBoxPaddingX, width() - selectionBoxWidth ); int selectionBoxY; if ((mSelection.width() >= selectionSizeThreshold) && (mSelection.height() >= selectionSizeThreshold)) { // show inside the box selectionBoxY = static_cast(mSelection.y() + (mSelection.height() - selectionSizeTextRect.height()) / 2); } else { // show on top by default selectionBoxY = static_cast(mSelection.y() - selectionBoxHeight - selectionBoxMarginY); if (selectionBoxY < 0) { // show at the bottom selectionBoxY = static_cast(mSelection.y() + mSelection.height() + selectionBoxMarginY); } } // Now do the actual box, border, and text drawing painter.setBrush(mLabelBackgroundColor); painter.setPen(mLabelForegroundColor); const QRect selectionBoxRect( selectionBoxX, selectionBoxY, selectionBoxWidth, selectionBoxHeight ); painter.setRenderHint(QPainter::Antialiasing, false); painter.drawRect(selectionBoxRect); painter.setRenderHint(QPainter::Antialiasing, true); painter.drawText(selectionBoxRect, Qt::AlignCenter, selectionSizeText); } void QuickEditor::setMouseCursor(const QPointF& pos) { MouseState mouseState = mouseLocation(pos); if (mouseState == MouseState::Outside) { setCursor(Qt::CrossCursor); } else if (MouseState::TopLeftOrBottomRight & mouseState) { setCursor(Qt::SizeFDiagCursor); } else if (MouseState::TopRightOrBottomLeft & mouseState) { setCursor(Qt::SizeBDiagCursor); } else if (MouseState::TopOrBottom & mouseState) { setCursor(Qt::SizeVerCursor); } else if (MouseState::RightOrLeft & mouseState) { setCursor(Qt::SizeHorCursor); } else { setCursor(Qt::OpenHandCursor); } } QuickEditor::MouseState QuickEditor::mouseLocation(const QPointF& pos) { if (mSelection.contains(pos)) { const qreal verSize = qMin(mouseAreaSize, mSelection.height() / 2); const qreal horSize = qMin(mouseAreaSize, mSelection.width() / 2); auto withinThreshold = [](const qreal offset, const qreal size) { return offset <= size && offset >= 0; }; const bool withinTopEdge = withinThreshold(pos.y() - mSelection.top(), verSize); const bool withinRightEdge = withinThreshold(mSelection.right() - pos.x(), horSize); const bool withinBottomEdge = !withinTopEdge && withinThreshold(mSelection.bottom() - pos.y(), verSize); const bool withinLeftEdge = !withinRightEdge && withinThreshold(pos.x() - mSelection.left(), horSize); if (withinTopEdge) { if (withinRightEdge) { return MouseState::TopRight; } else if (withinLeftEdge) { return MouseState::TopLeft; } else { return MouseState::Top; } } else if (withinBottomEdge) { if (withinRightEdge) { return MouseState::BottomRight; } else if (withinLeftEdge) { return MouseState::BottomLeft; } else { return MouseState::Bottom; } } else if (withinRightEdge) { return MouseState::Right; } else if (withinLeftEdge) { return MouseState::Left; } else { return MouseState::Inside; } } else { return MouseState::Outside; } } diff --git a/src/QuickEditor/QuickEditor.h b/src/QuickEditor/QuickEditor.h index d467954..c251e01 100644 --- a/src/QuickEditor/QuickEditor.h +++ b/src/QuickEditor/QuickEditor.h @@ -1,140 +1,146 @@ /* * Copyright (C) 2018 Ambareesh "Amby" Balaji * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef QUICKEDITOR_H #define QUICKEDITOR_H #include #include #include #include #include #include class QMouseEvent; +namespace KWayland { +namespace Client { +class PlasmaShell; +} +} + class QuickEditor: public QWidget { Q_OBJECT public: - explicit QuickEditor(const QPixmap &thePixmap, QWidget *parent = nullptr); + explicit QuickEditor(const QPixmap &thePixmap, KWayland::Client::PlasmaShell *plasmashell, QWidget *parent = nullptr); virtual ~QuickEditor() = default; private: enum MouseState : short { None = 0, // 0000 Inside = 1 << 0, // 0001 Outside = 1 << 1, // 0010 TopLeft = 5, //101 Top = 17, // 10001 TopRight = 9, // 1001 Right = 33, // 100001 BottomRight = 6, // 110 Bottom = 18, // 10010 BottomLeft = 10, // 1010 Left = 34, // 100010 TopLeftOrBottomRight = TopLeft & BottomRight, // 100 TopRightOrBottomLeft = TopRight & BottomLeft, // 1000 TopOrBottom = Top & Bottom, // 10000 RightOrLeft = Right & Left, // 100000 }; void acceptSelection(); int boundsLeft(int newTopLeftX, const bool mouse = true); int boundsRight(int newTopLeftX, const bool mouse = true); int boundsUp(int newTopLeftY, const bool mouse = true); int boundsDown(int newTopLeftY, const bool mouse = true); void keyPressEvent(QKeyEvent* event) override; void keyReleaseEvent(QKeyEvent* event) override; void mousePressEvent(QMouseEvent* event) override; void mouseMoveEvent(QMouseEvent* event) override; void mouseReleaseEvent(QMouseEvent* event) override; void mouseDoubleClickEvent(QMouseEvent* event) override; void paintEvent(QPaintEvent*) override; void drawBottomHelpText(QPainter& painter); void drawDragHandles(QPainter& painter); void drawMagnifier(QPainter& painter); void drawMidHelpText(QPainter& painter); void drawSelectionSizeTooltip(QPainter& painter); void setBottomHelpText(); void layoutBottomHelpText(); void setMouseCursor(const QPointF& pos); MouseState mouseLocation(const QPointF& pos); static const qreal mouseAreaSize; static const qreal cornerHandleRadius; static const qreal midHandleRadius; static const int selectionSizeThreshold; static const int selectionBoxPaddingX; static const int selectionBoxPaddingY; static const int selectionBoxMarginY; static const int bottomHelpMaxLength = 6; static bool bottomHelpTextPrepared; static const int bottomHelpBoxPaddingX; static const int bottomHelpBoxPaddingY; static const int bottomHelpBoxPairSpacing; static const int bottomHelpBoxMarginBottom; static const int midHelpTextFontSize; static const int magnifierLargeStep; static const int magZoom; static const int magPixels; static const int magOffset; QColor mMaskColor; QColor mStrokeColor; QColor mCrossColor; QColor mLabelBackgroundColor; QColor mLabelForegroundColor; QRectF mSelection; QPointF mStartPos; QPointF mInitialTopLeft; QString mMidHelpText; QFont mMidHelpTextFont; std::pair> mBottomHelpText[bottomHelpMaxLength]; QFont mBottomHelpTextFont; QRect mBottomHelpBorderBox; QPoint mBottomHelpContentPos; int mBottomHelpGridLeftWidth; MouseState mMouseDragState; QPixmap mPixmap; qreal dprI; QPointF mMousePos; bool mMagnifierAllowed; bool mShowMagnifier; bool mToggleMagnifier; bool mReleaseToCapture; bool mRememberRegion; bool mDisableArrowKeys; QRect mPrimaryScreenGeo; int mbottomHelpLength; Q_SIGNALS: void grabDone(const QPixmap &thePixmap); void grabCancelled(); }; #endif // QUICKEDITOR_H diff --git a/src/SpectacleCore.cpp b/src/SpectacleCore.cpp index 21a2983..c7d315e 100644 --- a/src/SpectacleCore.cpp +++ b/src/SpectacleCore.cpp @@ -1,412 +1,434 @@ /* * Copyright (C) 2015 Boudhayan Gupta * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "SpectacleCore.h" #include "spectacle_core_debug.h" #include "Config.h" #include #include #include #include #include +#include +#include +#include #include #include #include #include #include #include #include #include #include SpectacleCore::SpectacleCore(StartMode theStartMode, Spectacle::CaptureMode theCaptureMode, QString &theSaveFileName, qint64 theDelayMsec, bool theNotifyOnGrab, bool theCopyToClipboard, QObject *parent) : QObject(parent), mStartMode(theStartMode), mNotify(theNotifyOnGrab), mPlatform(loadPlatform()), mMainWindow(nullptr), mIsGuiInited(false), - mCopyToClipboard(theCopyToClipboard) + mCopyToClipboard(theCopyToClipboard), + mWaylandPlasmashell(nullptr) { auto lConfig = KSharedConfig::openConfig(QStringLiteral("spectaclerc")); KConfigGroup lGuiConfig(lConfig, "GuiConfig"); if (!(theSaveFileName.isEmpty() || theSaveFileName.isNull())) { if (QDir::isRelativePath(theSaveFileName)) { theSaveFileName = QDir::current().absoluteFilePath(theSaveFileName); } setFilename(theSaveFileName); } // essential connections connect(this, &SpectacleCore::errorMessage, this, &SpectacleCore::showErrorMessage); connect(mPlatform.get(), &Platform::newScreenshotTaken, this, &SpectacleCore::screenshotUpdated); connect(mPlatform.get(), &Platform::newScreenshotFailed, this, &SpectacleCore::screenshotFailed); auto lImmediateAvailable = mPlatform->supportedShutterModes().testFlag(Platform::ShutterMode::Immediate); auto lOnClickAvailable = mPlatform->supportedShutterModes().testFlag(Platform::ShutterMode::OnClick); if ((!lOnClickAvailable) && (theDelayMsec < 0)) { theDelayMsec = 0; } // reset last region if it should not be remembered across restarts auto lSpectacleConfig = SpectacleConfig::instance(); if(!lSpectacleConfig->alwaysRememberRegion()) { lSpectacleConfig->setCropRegion(QRect()); } // set up the export manager auto lExportManager = ExportManager::instance(); lExportManager->setCaptureMode(theCaptureMode); connect(lExportManager, &ExportManager::errorMessage, this, &SpectacleCore::showErrorMessage); connect(lExportManager, &ExportManager::imageSaved, this, &SpectacleCore::doCopyPath); connect(lExportManager, &ExportManager::forceNotify, this, &SpectacleCore::doNotify); connect(mPlatform.get(), &Platform::windowTitleChanged, lExportManager, &ExportManager::setWindowTitle); + // Needed so the QuickEditor can go fullscreen on wayland + if (KWindowSystem::isPlatformWayland()) { + using namespace KWayland::Client; + ConnectionThread *connection = ConnectionThread::fromApplication(this); + if (!connection) { + return; + } + Registry *registry = new Registry(this); + registry->create(connection); + connect(registry, &Registry::plasmaShellAnnounced, this, + [this, registry] (quint32 name, quint32 version) { + mWaylandPlasmashell = registry->createPlasmaShell(name, version, this); + } + ); + registry->setup(); + connection->roundtrip(); + } + switch (theStartMode) { case StartMode::DBus: break; case StartMode::Background: { auto lMsec = (KWindowSystem::compositingActive() ? 200 : 50) + theDelayMsec; auto lShutterMode = lImmediateAvailable ? Platform::ShutterMode::Immediate : Platform::ShutterMode::OnClick; auto lIncludePointer = lGuiConfig.readEntry("includePointer", true); auto lIncludeDecorations = lGuiConfig.readEntry("includeDecorations", true); const Platform::GrabMode lCaptureMode = toPlatformGrabMode(theCaptureMode); QTimer::singleShot(lMsec, this, [ this, lCaptureMode, lShutterMode, lIncludePointer, lIncludeDecorations ]() { mPlatform->doGrab(lShutterMode, lCaptureMode, lIncludePointer, lIncludeDecorations); }); } break; case StartMode::Gui: initGui(lGuiConfig.readEntry("includePointer", true), lGuiConfig.readEntry("includeDecorations", true)); break; } setUpShortcuts(); } void SpectacleCore::setUpShortcuts() { SpectacleConfig* config = SpectacleConfig::instance(); QAction* openAction = config->shortCutActions->action(QStringLiteral("_launch")); KGlobalAccel::self()->setGlobalShortcut(openAction, Qt::Key_Print); QAction* fullScreenAction = config->shortCutActions->action(QStringLiteral("FullScreenScreenShot")); KGlobalAccel::self()->setGlobalShortcut(fullScreenAction, Qt::SHIFT + Qt::Key_Print); QAction* activeWindowAction = config->shortCutActions->action(QStringLiteral("ActiveWindowScreenShot")); KGlobalAccel::self()->setGlobalShortcut(activeWindowAction, Qt::META + Qt::Key_Print); QAction* regionAction = config->shortCutActions->action(QStringLiteral("RectangularRegionScreenShot")); KGlobalAccel::self()->setGlobalShortcut(regionAction, Qt::META + Qt::SHIFT + Qt::Key_Print); QAction* currentScreenAction = config->shortCutActions->action(QStringLiteral("CurrentMonitorScreenShot")); KGlobalAccel::self()->setGlobalShortcut(currentScreenAction, QList()); } QString SpectacleCore::filename() const { return mFileNameString; } void SpectacleCore::setFilename(const QString &filename) { mFileNameString = filename; mFileNameUrl = QUrl::fromUserInput(filename); } // Slots void SpectacleCore::dbusStartAgent() { qApp->setQuitOnLastWindowClosed(true); auto lConfig = KSharedConfig::openConfig(QStringLiteral("spectaclerc")); KConfigGroup lGuiConfig(lConfig, "GuiConfig"); auto lIncludePointer = lGuiConfig.readEntry("includePointer", true); auto lIncludeDecorations = lGuiConfig.readEntry("includeDecorations", true); if (!(mStartMode == StartMode::Gui)) { mStartMode = StartMode::Gui; initGui(lIncludePointer, lIncludeDecorations); } else { using Actions = SpectacleConfig::PrintKeyActionRunning; switch (SpectacleConfig::instance()->printKeyActionRunning()) { case Actions::TakeNewScreenshot: { auto lShutterMode = mPlatform->supportedShutterModes().testFlag(Platform::ShutterMode::Immediate) ? Platform::ShutterMode::Immediate : Platform::ShutterMode::OnClick; auto lGrabMode = toPlatformGrabMode(ExportManager::instance()->captureMode()); QTimer::singleShot(KWindowSystem::compositingActive() ? 200 : 50, this, [this, lShutterMode, lGrabMode, lIncludePointer, lIncludeDecorations]() { mPlatform->doGrab(lShutterMode, lGrabMode, lIncludePointer, lIncludeDecorations); }); break; } case Actions::FocusWindow: if (mMainWindow->isMinimized()) { mMainWindow->setWindowState(mMainWindow->windowState() & ~Qt::WindowMinimized); } mMainWindow->activateWindow(); break; case Actions::StartNewInstance: QProcess newInstance; newInstance.setProgram(QStringLiteral("spectacle")); newInstance.startDetached(); break; } } } void SpectacleCore::takeNewScreenshot(Spectacle::CaptureMode theCaptureMode, int theTimeout, bool theIncludePointer, bool theIncludeDecorations) { ExportManager::instance()->setCaptureMode(theCaptureMode); auto lGrabMode = toPlatformGrabMode(theCaptureMode); if (theTimeout < 0) { mPlatform->doGrab(Platform::ShutterMode::OnClick, lGrabMode, theIncludePointer, theIncludeDecorations); return; } // when compositing is enabled, we need to give it enough time for the window // to disappear and all the effects are complete before we take the shot. there's // no way of knowing how long the disappearing effects take, but as per default // settings (and unless the user has set an extremely slow effect), 200 // milliseconds is a good amount of wait time. auto lMsec = KWindowSystem::compositingActive() ? 200 : 50; QTimer::singleShot(theTimeout + lMsec, this, [this, lGrabMode, theIncludePointer, theIncludeDecorations]() { mPlatform->doGrab(Platform::ShutterMode::Immediate, lGrabMode, theIncludePointer, theIncludeDecorations); }); } void SpectacleCore::showErrorMessage(const QString &theErrString) { qCDebug(SPECTACLE_CORE_LOG) << "ERROR: " << theErrString; if (mStartMode == StartMode::Gui) { KMessageBox::error(nullptr, theErrString); } } void SpectacleCore::screenshotUpdated(const QPixmap &thePixmap) { auto lExportManager = ExportManager::instance(); // if we were running in rectangular crop mode, now would be // the time to further process the image if (lExportManager->captureMode() == Spectacle::CaptureMode::RectangularRegion) { if(!mQuickEditor) { - mQuickEditor = std::make_unique(thePixmap); + mQuickEditor = std::make_unique(thePixmap, mWaylandPlasmashell); connect(mQuickEditor.get(), &QuickEditor::grabDone, this, &SpectacleCore::screenshotUpdated); connect(mQuickEditor.get(), &QuickEditor::grabCancelled, this, &SpectacleCore::screenshotFailed); mQuickEditor->show(); return; } else { mQuickEditor->hide(); mQuickEditor.reset(nullptr); } } lExportManager->setPixmap(thePixmap); lExportManager->updatePixmapTimestamp(); switch (mStartMode) { case StartMode::Background: case StartMode::DBus: { if (mNotify) { connect(lExportManager, &ExportManager::imageSaved, this, &SpectacleCore::doNotify); } if (mCopyToClipboard) { lExportManager->doCopyToClipboard(mNotify); } else { QUrl lSavePath = (mStartMode == StartMode::Background && mFileNameUrl.isValid() && mFileNameUrl.isLocalFile()) ? mFileNameUrl : QUrl(); lExportManager->doSave(lSavePath); } // if we notify, we emit allDone only if the user either dismissed the notification or pressed // the "Open" button, otherwise the app closes before it can react to it. if (!mNotify) { emit allDone(); } } break; case StartMode::Gui: mMainWindow->setScreenshotAndShow(thePixmap); } } void SpectacleCore::screenshotFailed() { if (ExportManager::instance()->captureMode() == Spectacle::CaptureMode::RectangularRegion && mQuickEditor) { mQuickEditor->hide(); mQuickEditor.reset(nullptr); } switch (mStartMode) { case StartMode::Background: showErrorMessage(i18n("Screenshot capture canceled or failed")); emit allDone(); return; case StartMode::DBus: emit grabFailed(); emit allDone(); return; case StartMode::Gui: mMainWindow->setScreenshotAndShow(QPixmap()); } } void SpectacleCore::doNotify(const QUrl &theSavedAt) { KNotification *lNotify = new KNotification(QStringLiteral("newScreenshotSaved")); switch(ExportManager::instance()->captureMode()) { case Spectacle::CaptureMode::AllScreens: lNotify->setTitle(i18nc("The entire screen area was captured, heading", "Full Screen Captured")); break; case Spectacle::CaptureMode::CurrentScreen: lNotify->setTitle(i18nc("The current screen was captured, heading", "Current Screen Captured")); break; case Spectacle::CaptureMode::ActiveWindow: lNotify->setTitle(i18nc("The active window was captured, heading", "Active Window Captured")); break; case Spectacle::CaptureMode::WindowUnderCursor: case Spectacle::CaptureMode::TransientWithParent: lNotify->setTitle(i18nc("The window under the mouse was captured, heading", "Window Under Cursor Captured")); break; case Spectacle::CaptureMode::RectangularRegion: lNotify->setTitle(i18nc("A rectangular region was captured, heading", "Rectangular Region Captured")); break; case Spectacle::CaptureMode::InvalidChoice: break; } // a speaking message is prettier than a URL, special case for copy to clipboard and the default pictures location const QString &lSavePath = theSavedAt.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path(); if (mCopyToClipboard) { lNotify->setText(i18n("A screenshot was saved to your clipboard.")); } else if (lSavePath == QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)) { lNotify->setText(i18nc("Placeholder is filename", "A screenshot was saved as '%1' to your Pictures folder.", theSavedAt.fileName())); } else { lNotify->setText(i18n("A screenshot was saved as '%1' to '%2'.", theSavedAt.fileName(), lSavePath)); } if (!mCopyToClipboard) { lNotify->setUrls({theSavedAt}); lNotify->setDefaultAction(i18nc("Open the screenshot we just saved", "Open")); connect(lNotify, QOverload::of(&KNotification::activated), this, [this, theSavedAt](uint index) { if (index == 0) { new KRun(theSavedAt, nullptr); QTimer::singleShot(250, this, [this] { if (mStartMode != StartMode::Gui) { emit allDone(); } }); } }); } connect(lNotify, &QObject::destroyed, this, [this] { if (mStartMode != StartMode::Gui) { emit allDone(); } }); lNotify->sendEvent(); } void SpectacleCore::doCopyPath(const QUrl &savedAt) { if (SpectacleConfig::instance()->copySaveLocationToClipboard()) { qApp->clipboard()->setText(savedAt.toLocalFile()); } } void SpectacleCore::doStartDragAndDrop() { auto lExportManager = ExportManager::instance(); QUrl lTempFile = lExportManager->tempSave(); if (!lTempFile.isValid()) { return; } auto lMimeData = new QMimeData; lMimeData->setUrls(QList { lTempFile }); lMimeData->setData(QStringLiteral("application/x-kde-suggestedfilename"), QFile::encodeName(lTempFile.fileName())); auto lDragHandler = new QDrag(this); lDragHandler->setMimeData(lMimeData); lDragHandler->setPixmap(lExportManager->pixmap().scaled(256, 256, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); lDragHandler->exec(Qt::CopyAction); } // Private Platform::GrabMode SpectacleCore::toPlatformGrabMode(Spectacle::CaptureMode theCaptureMode) { switch(theCaptureMode) { case Spectacle::CaptureMode::InvalidChoice: return Platform::GrabMode::InvalidChoice; case Spectacle::CaptureMode::AllScreens: case Spectacle::CaptureMode::RectangularRegion: return Platform::GrabMode::AllScreens; case Spectacle::CaptureMode::TransientWithParent: return Platform::GrabMode::TransientWithParent; case Spectacle::CaptureMode::CurrentScreen: return Platform::GrabMode::CurrentScreen; case Spectacle::CaptureMode::ActiveWindow: return Platform::GrabMode::ActiveWindow; case Spectacle::CaptureMode::WindowUnderCursor: return Platform::GrabMode::WindowUnderCursor; } return Platform::GrabMode::InvalidChoice; } void SpectacleCore::initGui(bool theIncludePointer, bool theIncludeDecorations) { if (!mIsGuiInited) { mMainWindow = std::make_unique(mPlatform->supportedGrabModes(), mPlatform->supportedShutterModes()); connect(mMainWindow.get(), &KSMainWindow::newScreenshotRequest, this, &SpectacleCore::takeNewScreenshot); connect(mMainWindow.get(), &KSMainWindow::dragAndDropRequest, this, &SpectacleCore::doStartDragAndDrop); mIsGuiInited = true; auto lShutterMode = mPlatform->supportedShutterModes().testFlag(Platform::ShutterMode::Immediate) ? Platform::ShutterMode::Immediate : Platform::ShutterMode::OnClick; auto lGrabMode = toPlatformGrabMode(ExportManager::instance()->captureMode()); QTimer::singleShot(0, this, [this, lShutterMode, lGrabMode, theIncludePointer, theIncludeDecorations]() { mPlatform->doGrab(lShutterMode, lGrabMode, theIncludePointer, theIncludeDecorations); }); } } diff --git a/src/SpectacleCore.h b/src/SpectacleCore.h index d85b014..8a9ac68 100644 --- a/src/SpectacleCore.h +++ b/src/SpectacleCore.h @@ -1,94 +1,101 @@ /* This file is part of Spectacle, the KDE screenshot utility * Copyright (C) 2019 Boudhayan Gupta * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * * SPDX-License-Identifier: LGPL-2.0-or-later */ #pragma once #include #include "ExportManager.h" #include "Gui/KSMainWindow.h" #include "QuickEditor/QuickEditor.h" #include "Platforms/PlatformLoader.h" #include +namespace KWayland { +namespace Client { +class PlasmaShell; +} +} + using MainWindowPtr = std::unique_ptr; using EditorPtr = std::unique_ptr; class SpectacleCore: public QObject { Q_OBJECT public: enum class StartMode { Gui = 0, DBus = 1, Background = 2 }; explicit SpectacleCore(StartMode theStartMode, Spectacle::CaptureMode theCaptureMode, QString &theSaveFileName, qint64 theDelayMsec, bool theNotifyOnGrab, bool theCopyToClipboard, QObject *parent = nullptr); virtual ~SpectacleCore() = default; QString filename() const; void setFilename(const QString &filename); Q_SIGNALS: void errorMessage(const QString &errString); void allDone(); void filenameChanged(const QString &filename); void grabFailed(); public Q_SLOTS: void takeNewScreenshot(Spectacle::CaptureMode theCaptureMode, int theTimeout, bool theIncludePointer, bool theIncludeDecorations); void showErrorMessage(const QString &theErrString); void screenshotUpdated(const QPixmap &thePixmap); void screenshotFailed(); void dbusStartAgent(); void doStartDragAndDrop(); void doNotify(const QUrl &theSavedAt); void doCopyPath(const QUrl &savedAt); private: void initGui(bool theIncludePointer, bool theIncludeDecorations); Platform::GrabMode toPlatformGrabMode(Spectacle::CaptureMode theCaptureMode); void setUpShortcuts(); StartMode mStartMode; bool mNotify; QString mFileNameString; QUrl mFileNameUrl; PlatformPtr mPlatform; MainWindowPtr mMainWindow; EditorPtr mQuickEditor; bool mIsGuiInited; bool mCopyToClipboard; + KWayland::Client::PlasmaShell *mWaylandPlasmashell; };