From 8170c2085cd4c83476a4b0c706ea3d2d8a34c177 Mon Sep 17 00:00:00 2001 From: "J.D. Purcell" Date: Sun, 20 Oct 2024 22:08:27 -0400 Subject: [PATCH] macOS: Action to hide/show titlebar --- src/actionmanager.cpp | 9 +++ src/mainwindow.cpp | 137 ++++++++++++++++++++++++++++++++-------- src/mainwindow.h | 11 ++++ src/qvapplication.cpp | 9 +++ src/qvapplication.h | 2 + src/qvcocoafunctions.h | 4 ++ src/qvcocoafunctions.mm | 13 ++++ src/qvgraphicsview.cpp | 38 +++++++++++ src/qvgraphicsview.h | 8 +++ src/shortcutmanager.cpp | 2 + 10 files changed, 207 insertions(+), 26 deletions(-) diff --git a/src/actionmanager.cpp b/src/actionmanager.cpp index ccca4e8e..625f0c10 100644 --- a/src/actionmanager.cpp +++ b/src/actionmanager.cpp @@ -265,6 +265,8 @@ QMenu *ActionManager::buildViewMenu(bool addIcon, QWidget *parent) addCloneOfAction(viewMenu, "mirror"); addCloneOfAction(viewMenu, "flip"); viewMenu->addSeparator(); + if (qvApp->supportsTitlebarHiding()) + addCloneOfAction(viewMenu, "toggletitlebar"); addCloneOfAction(viewMenu, "fullscreen"); menuCloneLibrary.insert(viewMenu->menuAction()->data().toString(), viewMenu); @@ -628,6 +630,8 @@ void ActionManager::actionTriggered(QAction *triggeredAction, MainWindow *releva relevantWindow->mirror(); } else if (key == "flip") { relevantWindow->flip(); + } else if (key == "toggletitlebar") { + relevantWindow->toggleTitlebarHidden(); } else if (key == "fullscreen") { relevantWindow->toggleFullScreen(); } else if (key == "firstfile") { @@ -760,8 +764,13 @@ void ActionManager::initializeActionLibrary() flipAction->setData({"disable"}); actionLibrary.insert("flip", flipAction); + auto *toggleTitlebarAction = new QAction(tr("Hide Title&bar")); + toggleTitlebarAction->setData({"windowdisable"}); + actionLibrary.insert("toggletitlebar", toggleTitlebarAction); + auto *fullScreenAction = new QAction(QIcon::fromTheme("view-fullscreen"), tr("Enter F&ull Screen")); fullScreenAction->setMenuRole(QAction::NoRole); + fullScreenAction->setData({"windowdisable"}); actionLibrary.insert("fullscreen", fullScreenAction); auto *firstFileAction = new QAction(QIcon::fromTheme("go-first"), tr("&First File")); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index c87f86a2..02b163ad 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -50,6 +50,7 @@ MainWindow::MainWindow(QWidget *parent) : // Initialize variables justLaunchedWithImage = false; storedWindowState = Qt::WindowNoState; + storedTitlebarHidden = false; // Initialize graphicsviewkDefaultBufferAlignment graphicsView = new QVGraphicsView(this); @@ -66,7 +67,7 @@ MainWindow::MainWindow(QWidget *parent) : // Initialize escape shortcut escShortcut = new QShortcut(Qt::Key_Escape, this); connect(escShortcut, &QShortcut::activated, this, [this](){ - if (windowState() == Qt::WindowFullScreen) + if (windowState().testFlag(Qt::WindowFullScreen)) toggleFullScreen(); }); @@ -131,6 +132,9 @@ MainWindow::MainWindow(QWidget *parent) : ActionManager::actionTriggered(triggeredAction, this); }); + // Enable actions related to having a window + disableActions(); + // Connect functions to application components connect(&qvApp->getShortcutManager(), &ShortcutManager::shortcutsUpdated, this, &MainWindow::shortcutsUpdated); connect(&qvApp->getSettingsManager(), &SettingsManager::settingsUpdated, this, &MainWindow::settingsUpdated); @@ -234,24 +238,12 @@ void MainWindow::changeEvent(QEvent *event) { if (event->type() == QEvent::WindowStateChange) { - const auto fullscreenActions = qvApp->getActionManager().getAllClonesOfAction("fullscreen", this); - for (const auto &fullscreenAction : fullscreenActions) - { - if (windowState() == Qt::WindowFullScreen) - { - fullscreenAction->setText(tr("Exit F&ull Screen")); - fullscreenAction->setIcon(QIcon::fromTheme("view-restore")); - } - else - { - fullscreenAction->setText(tr("Enter F&ull Screen")); - fullscreenAction->setIcon(QIcon::fromTheme("view-fullscreen")); - } - } - - if (qvApp->getSettingsManager().getBoolean("fullscreendetails")) - ui->fullscreenLabel->setVisible(windowState() == Qt::WindowFullScreen); + const auto *changeEvent = static_cast(event); + if (windowState().testFlag(Qt::WindowFullScreen) != changeEvent->oldState().testFlag(Qt::WindowFullScreen)) + fullscreenChanged(); } + + QMainWindow::changeEvent(event); } void MainWindow::mousePressEvent(QMouseEvent *event) @@ -314,6 +306,23 @@ void MainWindow::paintEvent(QPaintEvent *event) } } +void MainWindow::fullscreenChanged() +{ + const bool isFullscreen = windowState().testFlag(Qt::WindowFullScreen); + const auto fullscreenActions = qvApp->getActionManager().getAllClonesOfAction("fullscreen", this); + for (const auto &fullscreenAction : fullscreenActions) + { + fullscreenAction->setText(isFullscreen ? tr("Exit F&ull Screen") : tr("Enter F&ull Screen")); + fullscreenAction->setIcon(isFullscreen ? QIcon::fromTheme("view-restore") : QIcon::fromTheme("view-fullscreen")); + } + ui->fullscreenLabel->setVisible(isFullscreen && qvApp->getSettingsManager().getBoolean("fullscreendetails")); + if (!isFullscreen && storedTitlebarHidden) + { + setTitlebarHidden(true); + storedTitlebarHidden = false; + } +} + void MainWindow::openFile(const QString &fileName) { graphicsView->loadFile(fileName); @@ -348,7 +357,7 @@ void MainWindow::settingsUpdated() slideshowTimer->setInterval(static_cast(settingsManager.getDouble("slideshowtimer")*1000)); - ui->fullscreenLabel->setVisible(qvApp->getSettingsManager().getBoolean("fullscreendetails") && (windowState() == Qt::WindowFullScreen)); + ui->fullscreenLabel->setVisible(qvApp->getSettingsManager().getBoolean("fullscreendetails") && windowState().testFlag(Qt::WindowFullScreen)); setWindowSize(); @@ -387,6 +396,7 @@ void MainWindow::fileChanged() if (info->isVisible()) refreshProperties(); buildWindowTitle(); + updateWindowFilePath(); // repaint to handle error message update(); @@ -422,6 +432,10 @@ void MainWindow::disableActions() { clone->setEnabled(!getCurrentFileDetails().folderFileInfoList.isEmpty()); } + else if (cloneData.last() == "windowdisable") + { + clone->setEnabled(true); + } } } } @@ -522,14 +536,62 @@ void MainWindow::buildWindowTitle() // Update fullscreen label to titlebar text as well ui->fullscreenLabel->setText(newString); +} - if (windowHandle() != nullptr) - { - if (getCurrentFileDetails().isPixmapLoaded) - windowHandle()->setFilePath(getCurrentFileDetails().fileInfo.absoluteFilePath()); +void MainWindow::updateWindowFilePath() +{ + if (!windowHandle()) + return; + + const bool shouldPopulate = getCurrentFileDetails().isPixmapLoaded && !getTitlebarHidden(); + windowHandle()->setFilePath(shouldPopulate ? getCurrentFileDetails().fileInfo.absoluteFilePath() : ""); +} + +bool MainWindow::getTitlebarHidden() const +{ + if (!windowHandle()) + return false; + +#if defined COCOA_LOADED && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + return QVCocoaFunctions::getTitlebarHidden(windowHandle()); +#else + return !windowFlags().testFlag(Qt::WindowTitleHint); +#endif +} + +void MainWindow::setTitlebarHidden(const bool shouldHide) +{ + if (!windowHandle()) + return; + + auto customizeWindowFlags = [this](const Qt::WindowFlags flagsToChange, const bool on) { + Qt::WindowFlags newFlags = windowFlags() | Qt::CustomizeWindowHint; + if (on) + newFlags |= flagsToChange; else - windowHandle()->setFilePath(""); + newFlags &= ~flagsToChange; + overrideWindowFlags(newFlags); + windowHandle()->setFlags(newFlags); + }; + +#if defined COCOA_LOADED && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QVCocoaFunctions::setTitlebarHidden(windowHandle(), shouldHide); + customizeWindowFlags(Qt::WindowCloseButtonHint | Qt::WindowMinMaxButtonsHint | Qt::WindowFullscreenButtonHint, !shouldHide); +#elif defined WIN32_LOADED + customizeWindowFlags(Qt::WindowTitleHint | Qt::WindowMinMaxButtonsHint, !shouldHide); +#else + customizeWindowFlags(Qt::WindowTitleHint, !shouldHide); +#endif + + const auto toggleTitlebarActions = qvApp->getActionManager().getAllClonesOfAction("toggletitlebar", this); + for (const auto &toggleTitlebarAction : toggleTitlebarActions) + { + toggleTitlebarAction->setText(shouldHide ? tr("Show Title&bar") : tr("Hide Title&bar")); } + + updateWindowFilePath(); + update(); + resetZoom(); } void MainWindow::setWindowSize() @@ -545,7 +607,7 @@ void MainWindow::setWindowSize() justLaunchedWithImage = false; //check if window is maximized or fullscreened - if (windowState() == Qt::WindowMaximized || windowState() == Qt::WindowFullScreen) + if (windowState().testFlag(Qt::WindowMaximized) || windowState().testFlag(Qt::WindowFullScreen)) return; @@ -1181,15 +1243,38 @@ void MainWindow::increaseSpeed() void MainWindow::toggleFullScreen() { - if (windowState() == Qt::WindowFullScreen) + // Note: This is only triggered by the menu action, so the logic here should be kept to a minimum. Anything that + // needs to run even if the window manager initiated the change should be triggered by QEvent::WindowStateChange. + + // Disable updates during window state change to resolve visual glitches on macOS if the titlebar is hidden + setUpdatesEnabled(false); + + if (windowState().testFlag(Qt::WindowFullScreen)) { setWindowState(storedWindowState); } else { storedWindowState = windowState(); + + // Restore the titlebar if it was hidden because the window manager might do something special with the + // titlebar (e.g. macOS) in fullscreen mode or get confused by the titlebar being hidden (e.g. Windows). + storedTitlebarHidden = getTitlebarHidden(); + if (storedTitlebarHidden) + setTitlebarHidden(false); + showFullScreen(); } + + setUpdatesEnabled(true); +} + +void MainWindow::toggleTitlebarHidden() +{ + if (windowState().testFlag(Qt::WindowFullScreen)) + return; + + setTitlebarHidden(!getTitlebarHidden()); } int MainWindow::getTitlebarOverlap() const diff --git a/src/mainwindow.h b/src/mainwindow.h index 8870f369..cfd04181 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -37,6 +37,12 @@ class MainWindow : public QMainWindow void buildWindowTitle(); + void updateWindowFilePath(); + + bool getTitlebarHidden() const; + + void setTitlebarHidden(const bool shouldHide); + void setWindowSize(); bool getIsPixmapLoaded() const; @@ -111,6 +117,8 @@ class MainWindow : public QMainWindow void toggleFullScreen(); + void toggleTitlebarHidden(); + int getTitlebarOverlap() const; const QVImageCore::FileDetails& getCurrentFileDetails() const { return graphicsView->getCurrentFileDetails(); } @@ -145,6 +153,8 @@ public slots: void paintEvent(QPaintEvent *event) override; + void fullscreenChanged(); + protected slots: void settingsUpdated(); void shortcutsUpdated(); @@ -167,6 +177,7 @@ protected slots: bool justLaunchedWithImage; Qt::WindowStates storedWindowState; + bool storedTitlebarHidden; QNetworkAccessManager networkAccessManager; diff --git a/src/qvapplication.cpp b/src/qvapplication.cpp index b448fff8..3a43bd14 100644 --- a/src/qvapplication.cpp +++ b/src/qvapplication.cpp @@ -404,6 +404,15 @@ void QVApplication::defineFilterLists() nameFilterList << tr("All Files") + " (*)"; } +bool QVApplication::supportsTitlebarHiding() +{ +#if defined COCOA_LOADED && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + return true; +#else + return false; +#endif +} + qreal QVApplication::getPerceivedBrightness(const QColor &color) { return (color.red() * 0.299 + color.green() * 0.587 + color.blue() * 0.114) / 255.0; diff --git a/src/qvapplication.h b/src/qvapplication.h index 9a7fcb3d..e33a1a88 100644 --- a/src/qvapplication.h +++ b/src/qvapplication.h @@ -75,6 +75,8 @@ class QVApplication : public QApplication ActionManager &getActionManager() { return actionManager; } + static bool supportsTitlebarHiding(); + static qreal getPerceivedBrightness(const QColor &color); private: diff --git a/src/qvcocoafunctions.h b/src/qvcocoafunctions.h index 015bc5a3..6f1b6670 100644 --- a/src/qvcocoafunctions.h +++ b/src/qvcocoafunctions.h @@ -15,6 +15,10 @@ class QVCocoaFunctions static void setFullSizeContentView(QWindow *window, const bool enable); + static bool getTitlebarHidden(QWindow *window); + + static void setTitlebarHidden(QWindow *window, const bool shouldHide); + static void setVibrancy(bool alwaysDark, QWindow *window); static int getObscuredHeight(QWindow *window); diff --git a/src/qvcocoafunctions.mm b/src/qvcocoafunctions.mm index f25e1f3d..5704b37f 100644 --- a/src/qvcocoafunctions.mm +++ b/src/qvcocoafunctions.mm @@ -108,6 +108,19 @@ static void fixNativeMenuEccentricities(QMenu *menu, NSMenu *nativeMenu) #endif } +bool QVCocoaFunctions::getTitlebarHidden(QWindow *window) +{ + auto *view = reinterpret_cast(window->winId()); + return view.window.titleVisibility == NSWindowTitleHidden; +} + +void QVCocoaFunctions::setTitlebarHidden(QWindow *window, const bool shouldHide) +{ + auto *view = reinterpret_cast(window->winId()); + view.window.titleVisibility = shouldHide ? NSWindowTitleHidden : NSWindowTitleVisible; + view.window.titlebarAppearsTransparent = shouldHide; +} + void QVCocoaFunctions::setVibrancy(bool alwaysDark, QWindow *window) { auto *view = reinterpret_cast(window->winId()); diff --git a/src/qvgraphicsview.cpp b/src/qvgraphicsview.cpp index cebcc24a..432f40e6 100644 --- a/src/qvgraphicsview.cpp +++ b/src/qvgraphicsview.cpp @@ -48,6 +48,8 @@ QVGraphicsView::QVGraphicsView(QWidget *parent) : QGraphicsView(parent) lastZoomEventPos = QPoint(-1, -1); lastZoomRoundingError = QPointF(); lastScrollRoundingError = QPointF(); + mousePressButton = Qt::MouseButton::NoButton; + mousePressModifiers = Qt::KeyboardModifier::NoModifier; zoomBasisScaleFactor = 1.0; @@ -119,12 +121,48 @@ void QVGraphicsView::enterEvent(QEnterEvent *event) viewport()->setCursor(Qt::ArrowCursor); } +void QVGraphicsView::mousePressEvent(QMouseEvent *event) +{ + const auto initializeDrag = [this, event]() { + mousePressButton = event->button(); + mousePressModifiers = event->modifiers(); + mousePressPosition = event->pos(); + viewport()->setCursor(Qt::ClosedHandCursor); + }; + + if (event->button() == Qt::LeftButton && event->modifiers().testFlag(Qt::ControlModifier)) + { + initializeDrag(); + return; + } + + QGraphicsView::mousePressEvent(event); +} + void QVGraphicsView::mouseReleaseEvent(QMouseEvent *event) { + if (mousePressButton != Qt::NoButton) + { + mousePressButton = Qt::NoButton; + mousePressModifiers = Qt::NoModifier; + } + QGraphicsView::mouseReleaseEvent(event); viewport()->setCursor(Qt::ArrowCursor); } +void QVGraphicsView::mouseMoveEvent(QMouseEvent *event) +{ + if (mousePressButton == Qt::LeftButton && mousePressModifiers.testFlag(Qt::ControlModifier)) + { + const QPoint delta = event->pos() - mousePressPosition; + window()->move(window()->pos() + delta); + return; + } + + QGraphicsView::mouseMoveEvent(event); +} + bool QVGraphicsView::event(QEvent *event) { //this is for touchpad pinch gestures diff --git a/src/qvgraphicsview.h b/src/qvgraphicsview.h index 9cb65e2a..df650784 100644 --- a/src/qvgraphicsview.h +++ b/src/qvgraphicsview.h @@ -92,8 +92,12 @@ class QVGraphicsView : public QGraphicsView void enterEvent(QEnterEvent *event) override; #endif + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + bool event(QEvent *event) override; void fitInViewMarginless(const QRectF &rect); @@ -146,5 +150,9 @@ private slots: QTimer *expensiveScaleTimerNew; QPointF centerPoint; + + Qt::MouseButton mousePressButton; + Qt::KeyboardModifiers mousePressModifiers; + QPoint mousePressPosition; }; #endif // QVGRAPHICSVIEW_H diff --git a/src/shortcutmanager.cpp b/src/shortcutmanager.cpp index 0fb999d7..ec4ef287 100644 --- a/src/shortcutmanager.cpp +++ b/src/shortcutmanager.cpp @@ -93,6 +93,8 @@ void ShortcutManager::initializeShortcutsList() shortcutsList.append({tr("Rotate Left"), "rotateleft", QStringList(QKeySequence(Qt::Key_Down).toString()), {}}); shortcutsList.append({tr("Mirror"), "mirror", QStringList(QKeySequence(Qt::Key_F).toString()), {}}); shortcutsList.append({tr("Flip"), "flip", QStringList(QKeySequence(Qt::CTRL | Qt::Key_F).toString()), {}}); + if (qvApp->supportsTitlebarHiding()) + shortcutsList.append({tr("Toggle Titlebar Hidden"), "toggletitlebar", {}, {}}); shortcutsList.append({tr("Full Screen"), "fullscreen", keyBindingsToStringList(QKeySequence::FullScreen), {}}); //Fixes alt+enter only working with numpad enter when using qt's standard keybinds #ifdef Q_OS_WIN