From bbe5e485f48ab6e7c9b29d8c484dc24d9cbf751a Mon Sep 17 00:00:00 2001 From: Gary Wang Date: Mon, 29 Jun 2026 19:17:23 +0800 Subject: [PATCH] feat: add option to provide a titlebar (default on) --- CMakeLists.txt | 2 + app/mainwindow.cpp | 32 +++--- app/mainwindow.h | 3 +- app/settings.cpp | 11 +++ app/settings.h | 2 + app/settingsdialog.cpp | 7 ++ app/settingsdialog.h | 1 + app/titlebar.cpp | 218 +++++++++++++++++++++++++++++++++++++++++ app/titlebar.h | 60 ++++++++++++ app/toolbutton.cpp | 7 +- app/toolbutton.h | 2 +- 11 files changed, 324 insertions(+), 21 deletions(-) create mode 100644 app/titlebar.cpp create mode 100644 app/titlebar.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 27959e7..004e817 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,7 @@ set (PPIC_CPP_FILES app/graphicsview.cpp app/graphicsscene.cpp app/bottombuttongroup.cpp + app/titlebar.cpp app/navigatorview.cpp app/opacityhelper.cpp app/toolbutton.cpp @@ -65,6 +66,7 @@ set (PPIC_HEADER_FILES app/graphicsview.h app/graphicsscene.h app/bottombuttongroup.h + app/titlebar.h app/navigatorview.h app/opacityhelper.h app/toolbutton.h diff --git a/app/mainwindow.cpp b/app/mainwindow.cpp index ff904dc..49aca52 100644 --- a/app/mainwindow.cpp +++ b/app/mainwindow.cpp @@ -7,6 +7,7 @@ #include "settings.h" #include "toolbutton.h" #include "bottombuttongroup.h" +#include "titlebar.h" #include "graphicsview.h" #include "navigatorview.h" #include "graphicsscene.h" @@ -101,20 +102,20 @@ MainWindow::MainWindow(QWidget *parent) connect(m_graphicsView, &GraphicsView::viewportRectChanged, m_gv, &NavigatorView::updateMainViewportRegion); - m_closeButton = new ToolButton(true, m_graphicsView); - m_closeButton->setIconSize(QSize(32, 32)); - m_closeButton->setFixedSize(QSize(50, 50)); - m_closeButton->setIconResourcePath(":/icons/window-close.svg"); + m_titleBar = new TitleBar(this); + m_titleBar->setCloseButtonOnly(!Settings::instance()->showTitleBar()); - connect(m_closeButton, &QAbstractButton::clicked, + connect(m_titleBar, &TitleBar::closeRequested, this, &MainWindow::closeWindow); + connect(m_titleBar, &TitleBar::maximizeToggleRequested, + this, &MainWindow::toggleMaximize); - m_prevButton = new ToolButton(false, m_graphicsView); + m_prevButton = new ToolButton(m_graphicsView); m_prevButton->setIconSize(QSize(75, 75)); m_prevButton->setIconResourcePath(":/icons/go-previous.svg"); m_prevButton->setVisible(false); m_prevButton->setOpacity(0, false); - m_nextButton = new ToolButton(false, m_graphicsView); + m_nextButton = new ToolButton(m_graphicsView); m_nextButton->setIconSize(QSize(75, 75)); m_nextButton->setIconResourcePath(":/icons/go-next.svg"); m_nextButton->setVisible(false); @@ -138,7 +139,7 @@ MainWindow::MainWindow(QWidget *parent) m_bottomButtonGroup->setOpacity(0, false); m_gv->setOpacity(0, false); - m_closeButton->setOpacity(0, false); + m_titleBar->setOpacity(0, false); connect(m_pm, &PlaylistManager::totalCountChanged, this, &MainWindow::updateGalleryButtonsVisibility); @@ -161,7 +162,7 @@ MainWindow::MainWindow(QWidget *parent) }); // allow some mouse events can go through these widgets for resizing window. - installResizeCapture(m_closeButton); + installResizeCapture(m_titleBar); installResizeCapture(m_graphicsView); installResizeCapture(m_graphicsView->viewport()); installResizeCapture(m_gv); @@ -323,7 +324,7 @@ void MainWindow::enterEvent(QEnterEvent *event) m_bottomButtonGroup->setOpacity(1); m_gv->setOpacity(1); - m_closeButton->setOpacity(1); + m_titleBar->setOpacity(1); m_prevButton->setOpacity(1); m_nextButton->setOpacity(1); @@ -335,7 +336,7 @@ void MainWindow::leaveEvent(QEvent *event) m_bottomButtonGroup->setOpacity(0); m_gv->setOpacity(0); - m_closeButton->setOpacity(0); + m_titleBar->setOpacity(0); m_prevButton->setOpacity(0); m_nextButton->setOpacity(0); @@ -612,7 +613,12 @@ void MainWindow::closeWindow() void MainWindow::updateWidgetsPosition() { - m_closeButton->move(width() - m_closeButton->width(), 0); + if (m_titleBar->closeButtonOnly()) { + const int bw = m_titleBar->closeButtonWidth(); + m_titleBar->setGeometry(width() - bw, 0, bw, m_titleBar->height()); + } else { + m_titleBar->setGeometry(0, 0, width(), m_titleBar->height()); + } m_prevButton->move(25, (height() - m_prevButton->sizeHint().height()) / 2); m_nextButton->move(width() - m_nextButton->sizeHint().width() - 25, (height() - m_prevButton->sizeHint().height()) / 2); @@ -624,7 +630,7 @@ void MainWindow::updateWidgetsPosition() void MainWindow::toggleProtectedMode() { m_protectedMode = !m_protectedMode; - m_closeButton->setVisible(!m_protectedMode); + m_titleBar->setCloseButtonVisible(!m_protectedMode); updateGalleryButtonsVisibility(); } diff --git a/app/mainwindow.h b/app/mainwindow.h index 71833f8..ac5c4fd 100644 --- a/app/mainwindow.h +++ b/app/mainwindow.h @@ -20,6 +20,7 @@ QT_END_NAMESPACE class ActionManager; class PlaylistManager; class ToolButton; +class TitleBar; class GraphicsView; class NavigatorView; class BottomButtonGroup; @@ -123,9 +124,9 @@ private: QPropertyAnimation *m_floatUpAnimation; QParallelAnimationGroup *m_exitAnimationGroup; QFileSystemWatcher *m_fileSystemWatcher; - ToolButton *m_closeButton; ToolButton *m_prevButton; ToolButton *m_nextButton; + TitleBar *m_titleBar; GraphicsView *m_graphicsView; NavigatorView *m_gv; BottomButtonGroup *m_bottomButtonGroup; diff --git a/app/settings.cpp b/app/settings.cpp index c3b2987..3f9f759 100644 --- a/app/settings.cpp +++ b/app/settings.cpp @@ -55,6 +55,11 @@ bool Settings::useBuiltInCloseAnimation() const return m_qsettings->value("use_built_in_close_animation", true).toBool(); } +bool Settings::showTitleBar() const +{ + return m_qsettings->value("show_title_bar", true).toBool(); +} + bool Settings::useLightCheckerboard() const { return m_qsettings->value("use_light_checkerboard", false).toBool(); @@ -123,6 +128,12 @@ void Settings::setUseBuiltInCloseAnimation(bool on) m_qsettings->sync(); } +void Settings::setShowTitleBar(bool on) +{ + m_qsettings->setValue("show_title_bar", on); + m_qsettings->sync(); +} + void Settings::setUseLightCheckerboard(bool light) { m_qsettings->setValue("use_light_checkerboard", light); diff --git a/app/settings.h b/app/settings.h index e3e57b9..aa1f889 100644 --- a/app/settings.h +++ b/app/settings.h @@ -36,6 +36,7 @@ public: bool stayOnTop() const; bool useBuiltInCloseAnimation() const; + bool showTitleBar() const; bool useLightCheckerboard() const; bool loopGallery() const; bool autoLongImageMode() const; @@ -47,6 +48,7 @@ public: void setStayOnTop(bool on); void setUseBuiltInCloseAnimation(bool on); + void setShowTitleBar(bool on); void setUseLightCheckerboard(bool light); void setLoopGallery(bool on); void setAutoLongImageMode(bool on); diff --git a/app/settingsdialog.cpp b/app/settingsdialog.cpp index fd4b160..a61e8d2 100644 --- a/app/settingsdialog.cpp +++ b/app/settingsdialog.cpp @@ -21,6 +21,7 @@ SettingsDialog::SettingsDialog(QWidget *parent) : QDialog(parent) , m_stayOnTop(new QCheckBox) , m_useBuiltInCloseAnimation(new QCheckBox) + , m_showTitleBar(new QCheckBox) , m_useLightCheckerboard(new QCheckBox) , m_loopGallery(new QCheckBox) , m_autoLongImageMode(new QCheckBox) @@ -123,6 +124,7 @@ SettingsDialog::SettingsDialog(QWidget *parent) settingsForm->addRow(tr("Stay on top when start-up"), m_stayOnTop); settingsForm->addRow(tr("Use built-in close window animation"), m_useBuiltInCloseAnimation); + settingsForm->addRow(tr("Show title bar"), m_showTitleBar); settingsForm->addRow(tr("Use light-color checkerboard"), m_useLightCheckerboard); settingsForm->addRow(tr("Loop the loaded gallery"), m_loopGallery); settingsForm->addRow(tr("Auto long image mode"), m_autoLongImageMode); @@ -134,6 +136,7 @@ SettingsDialog::SettingsDialog(QWidget *parent) m_stayOnTop->setChecked(Settings::instance()->stayOnTop()); m_useBuiltInCloseAnimation->setChecked(Settings::instance()->useBuiltInCloseAnimation()); + m_showTitleBar->setChecked(Settings::instance()->showTitleBar()); m_useLightCheckerboard->setChecked(Settings::instance()->useLightCheckerboard()); m_loopGallery->setChecked(Settings::instance()->loopGallery()); m_autoLongImageMode->setChecked(Settings::instance()->autoLongImageMode()); @@ -172,6 +175,10 @@ SettingsDialog::SettingsDialog(QWidget *parent) Settings::instance()->setUseBuiltInCloseAnimation(state == Qt::Checked); }); + connect(m_showTitleBar, &QCHECKBOX_CHECKSTATECHANGED, this, [ = ](QT_CHECKSTATE state){ + Settings::instance()->setShowTitleBar(state == Qt::Checked); + }); + connect(m_useLightCheckerboard, &QCHECKBOX_CHECKSTATECHANGED, this, [ = ](QT_CHECKSTATE state){ Settings::instance()->setUseLightCheckerboard(state == Qt::Checked); }); diff --git a/app/settingsdialog.h b/app/settingsdialog.h index 351fd01..1f0b089 100644 --- a/app/settingsdialog.h +++ b/app/settingsdialog.h @@ -24,6 +24,7 @@ public slots: private: QCheckBox * m_stayOnTop = nullptr; QCheckBox * m_useBuiltInCloseAnimation = nullptr; + QCheckBox * m_showTitleBar = nullptr; QCheckBox * m_useLightCheckerboard = nullptr; QCheckBox * m_loopGallery = nullptr; QCheckBox * m_autoLongImageMode = nullptr; diff --git a/app/titlebar.cpp b/app/titlebar.cpp new file mode 100644 index 0000000..6ada30e --- /dev/null +++ b/app/titlebar.cpp @@ -0,0 +1,218 @@ +// SPDX-FileCopyrightText: 2025 Gary Wang +// +// SPDX-License-Identifier: MIT + +#include "titlebar.h" + +#include "opacityhelper.h" + +#include +#include +#include +#include +#include +#include +#include + +TitleBar::TitleBar(QWidget *parent) + : QWidget(parent) + , m_opacityHelper(new OpacityHelper(this)) + , m_closeIcon(QStringLiteral(":/icons/window-close.svg")) +{ + setFixedHeight(32); + setMouseTracking(true); + setAttribute(Qt::WA_Hover, true); + + if (QWidget *win = window()) + win->installEventFilter(this); +} + +void TitleBar::setOpacity(qreal opacity, bool animated) +{ + m_opacityHelper->setOpacity(opacity, animated); +} + +void TitleBar::setCloseButtonVisible(bool visible) +{ + if (m_closeButtonVisible == visible) + return; + m_closeButtonVisible = visible; + if (!visible) { + m_closeHovered = false; + m_closePressed = false; + } + update(); +} + +void TitleBar::setCloseButtonOnly(bool only) +{ + if (m_closeButtonOnly == only) + return; + m_closeButtonOnly = only; + update(); +} + +QRect TitleBar::closeButtonRect() const +{ + const int btnWidth = closeButtonWidth(); + return QRect(width() - btnWidth, 0, btnWidth, height()); +} + +void TitleBar::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + QPainter painter(this); + + // Subtle translucent backdrop so the title bar region is distinguishable + // (similar to the bottom button group). Skipped in close-button-only mode. + if (!m_closeButtonOnly) { + painter.fillRect(rect(), QColor(0, 0, 0, 120)); + } + + const QRect closeRect = closeButtonRect(); + + // Title text (leave room for the close button). + QRect labelRect = rect().adjusted(8, 0, 0, 0); + if (m_closeButtonVisible) + labelRect.setRight(closeRect.left() - 2); + + const QString title = window() ? window()->windowTitle() : QString(); + if (!m_closeButtonOnly && !title.isEmpty()) { + const QString elided = painter.fontMetrics().elidedText(title, Qt::ElideRight, labelRect.width()); + const int flags = Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine; + painter.setPen(Qt::black); + painter.drawText(labelRect.adjusted(1, 1, 1, 1), flags, elided); + painter.setPen(Qt::white); + painter.drawText(labelRect, flags, elided); + } + + if (m_closeButtonVisible) { + if (m_closeHovered) { + painter.fillRect(closeRect, + m_closePressed ? QColor(0xC5, 0x0F, 0x1F) + : QColor(0xE8, 0x11, 0x23)); + } + const int sz = height() / 3 * 2; + const QRect iconRect = QStyle::alignedRect(layoutDirection(), Qt::AlignCenter, + QSize(sz, sz), closeRect); + m_closeIcon.paint(&painter, iconRect); + } +} + +void TitleBar::mousePressEvent(QMouseEvent *event) +{ + if (event->button() != Qt::LeftButton) { + QWidget::mousePressEvent(event); + return; + } + + if (m_closeButtonVisible && closeButtonRect().contains(event->pos())) { + m_closePressed = true; + m_dragPending = false; + update(); + event->accept(); + return; + } + + m_dragPending = true; + m_moveStartPos = event->pos(); + event->accept(); +} + +void TitleBar::mouseMoveEvent(QMouseEvent *event) +{ + if (m_closeButtonVisible) { + const bool hovered = closeButtonRect().contains(event->pos()); + if (hovered != m_closeHovered) { + m_closeHovered = hovered; + update(); + } + } + + if (event->buttons() & Qt::LeftButton && m_dragPending + && !window()->isMaximized() && !window()->isFullScreen()) { + if (QWindow *wh = window()->windowHandle()) { + if (!wh->startSystemMove()) + window()->move(event->globalPosition().toPoint() - m_moveStartPos); + } + event->accept(); + } + + QWidget::mouseMoveEvent(event); +} + +void TitleBar::mouseReleaseEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + const bool wasClosePressed = m_closePressed; + m_closePressed = false; + m_dragPending = false; + update(); + if (wasClosePressed && m_closeButtonVisible + && closeButtonRect().contains(event->pos())) { + emit closeRequested(); + event->accept(); + return; + } + } + + QWidget::mouseReleaseEvent(event); +} + +void TitleBar::mouseDoubleClickEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton + && !(m_closeButtonVisible && closeButtonRect().contains(event->pos()))) { + emit maximizeToggleRequested(); + event->accept(); + return; + } + + QWidget::mouseDoubleClickEvent(event); +} + +void TitleBar::enterEvent(QEnterEvent *event) +{ + Q_UNUSED(event); + if (m_closeButtonVisible) { + const bool hovered = closeButtonRect().contains(mapFromGlobal(QCursor::pos())); + if (hovered != m_closeHovered) { + m_closeHovered = hovered; + update(); + } + } + + QWidget::enterEvent(event); +} + +void TitleBar::leaveEvent(QEvent *event) +{ + if (m_closeHovered) { + m_closeHovered = false; + update(); + } + + QWidget::leaveEvent(event); +} + +bool TitleBar::eventFilter(QObject *watched, QEvent *event) +{ + if (watched == window()) { + switch (event->type()) { + case QEvent::WindowTitleChange: + case QEvent::WindowStateChange: + case QEvent::ActivationChange: + update(); + break; + default: + break; + } + } + + return QWidget::eventFilter(watched, event); +} + +QSize TitleBar::sizeHint() const +{ + return QSize(0, 32); +} diff --git a/app/titlebar.h b/app/titlebar.h new file mode 100644 index 0000000..e311473 --- /dev/null +++ b/app/titlebar.h @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2025 Gary Wang +// +// SPDX-License-Identifier: MIT + +#ifndef TITLEBAR_H +#define TITLEBAR_H + +#include +#include + +QT_BEGIN_NAMESPACE +class QPaintEvent; +class QMouseEvent; +class QEvent; +class QEnterEvent; +QT_END_NAMESPACE + +class OpacityHelper; + +class TitleBar : public QWidget +{ + Q_OBJECT +public: + explicit TitleBar(QWidget *parent = nullptr); + + void setOpacity(qreal opacity, bool animated = true); + void setCloseButtonVisible(bool visible); + bool closeButtonOnly() const { return m_closeButtonOnly; } + void setCloseButtonOnly(bool only); + int closeButtonWidth() const { return qMax(height(), 46); } + +signals: + void closeRequested(); + void maximizeToggleRequested(); + +protected: + void paintEvent(QPaintEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseDoubleClickEvent(QMouseEvent *event) override; + void enterEvent(QEnterEvent *event) override; + void leaveEvent(QEvent *event) override; + bool eventFilter(QObject *watched, QEvent *event) override; + QSize sizeHint() const override; + +private: + QRect closeButtonRect() const; + + OpacityHelper *m_opacityHelper; + QIcon m_closeIcon; + bool m_closeButtonVisible = true; + bool m_closeButtonOnly = false; + bool m_closeHovered = false; + bool m_closePressed = false; + bool m_dragPending = false; + QPoint m_moveStartPos; +}; + +#endif // TITLEBAR_H diff --git a/app/toolbutton.cpp b/app/toolbutton.cpp index db3778b..15f4256 100644 --- a/app/toolbutton.cpp +++ b/app/toolbutton.cpp @@ -11,7 +11,7 @@ #include #include -ToolButton::ToolButton(bool hoverColor, QWidget *parent) +ToolButton::ToolButton(QWidget *parent) : QPushButton(parent) , m_opacityHelper(new OpacityHelper(this)) { @@ -19,11 +19,6 @@ ToolButton::ToolButton(bool hoverColor, QWidget *parent) QString qss = "QPushButton {" "background: transparent;" "}"; - if (hoverColor) { - qss += "QPushButton:hover {" - "background: red;" - "}"; - } setStyleSheet(qss); } diff --git a/app/toolbutton.h b/app/toolbutton.h index e53b70f..f236d78 100644 --- a/app/toolbutton.h +++ b/app/toolbutton.h @@ -12,7 +12,7 @@ class ToolButton : public QPushButton { Q_OBJECT public: - ToolButton(bool hoverColor = false, QWidget * parent = nullptr); + ToolButton(QWidget * parent = nullptr); void setIconResourcePath(const QString &iconp); public slots: