feat: add option to provide a titlebar (default on)

This commit is contained in:
2026-06-29 19:17:23 +08:00
parent 6da53ced81
commit bbe5e485f4
11 changed files with 324 additions and 21 deletions

View File

@@ -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

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
});

View File

@@ -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;

218
app/titlebar.cpp Normal file
View File

@@ -0,0 +1,218 @@
// SPDX-FileCopyrightText: 2025 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#include "titlebar.h"
#include "opacityhelper.h"
#include <QPainter>
#include <QStyle>
#include <QMouseEvent>
#include <QEvent>
#include <QEnterEvent>
#include <QWindow>
#include <QCursor>
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);
}

60
app/titlebar.h Normal file
View File

@@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: 2025 Gary Wang <git@blumia.net>
//
// SPDX-License-Identifier: MIT
#ifndef TITLEBAR_H
#define TITLEBAR_H
#include <QWidget>
#include <QIcon>
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

View File

@@ -11,7 +11,7 @@
#include <QGraphicsOpacityEffect>
#include <QPropertyAnimation>
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);
}

View File

@@ -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: