Implement native toolbars on macos on Qt6 to have a modern looking unified toolbars

This commit is contained in:
Luis Ángel San Martín Rodríguez 2025-04-20 09:38:58 +02:00
parent d9b9fda337
commit 3632ebab12
7 changed files with 539 additions and 15 deletions

1
.gitignore vendored
View File

@ -9,6 +9,7 @@ libp7zip
YACReader/build YACReader/build
YACReaderLibrary/build YACReaderLibrary/build
YACReaderLibraryServer/build YACReaderLibraryServer/build
build/
# C++ objects and libs # C++ objects and libs
*.slo *.slo

View File

@ -1319,7 +1319,11 @@ void MainWindowViewer::toggleFitToWidthSlider()
if (zoomSliderAction->isVisible()) { if (zoomSliderAction->isVisible()) {
zoomSliderAction->hide(); zoomSliderAction->hide();
} else { } else {
#if defined(Y_MAC_UI) && (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
zoomSliderAction->move((this->width() - zoomSliderAction->width()) / 2, y);
#else
zoomSliderAction->move(250, y); zoomSliderAction->move(250, y);
#endif
zoomSliderAction->show(); zoomSliderAction->show();
} }
} }

View File

@ -770,19 +770,20 @@ void LibraryWindow::createConnections()
connect(optionsDialog, &YACReaderOptionsDialog::optionsChanged, this, &LibraryWindow::reloadOptions); connect(optionsDialog, &YACReaderOptionsDialog::optionsChanged, this, &LibraryWindow::reloadOptions);
connect(optionsDialog, &YACReaderOptionsDialog::editShortcuts, editShortcutsDialog, &QWidget::show); connect(optionsDialog, &YACReaderOptionsDialog::editShortcuts, editShortcutsDialog, &QWidget::show);
auto searchDebouncer = new KDToolBox::KDSignalDebouncer(this); auto searchDebouncer = new KDToolBox::KDStringSignalDebouncer(this);
searchDebouncer->setTimeout(400); searchDebouncer->setTimeout(400);
// Search filter // Search filter
#ifdef Y_MAC_UI #ifdef Y_MAC_UI
connect(searchEdit, &YACReaderMacOSXSearchLineEdit::filterChanged, searchDebouncer, &KDToolBox::KDSignalThrottler::throttle); connect(libraryToolBar, &YACReaderMacOSXToolbar::filterChanged, searchDebouncer, &KDToolBox::KDStringSignalDebouncer::throttle);
connect(searchDebouncer, &KDToolBox::KDSignalThrottler::triggered, this, [=] { connect(searchEdit, &YACReaderMacOSXSearchLineEdit::filterChanged, searchDebouncer, &KDToolBox::KDStringSignalDebouncer::throttle);
setSearchFilter(searchEdit->text()); connect(searchDebouncer, &KDToolBox::KDStringSignalDebouncer::triggered, this, [=](QString filter) {
setSearchFilter(filter);
}); });
#else #else
connect(searchEdit, &YACReaderSearchLineEdit::filterChanged, searchDebouncer, &KDToolBox::KDSignalThrottler::throttle); connect(searchEdit, &YACReaderSearchLineEdit::filterChanged, searchDebouncer, &KDToolBox::KDStringSignalDebouncer::throttle);
connect(searchDebouncer, &KDToolBox::KDSignalThrottler::triggered, this, [=] { connect(searchDebouncer, &KDToolBox::KDStringSignalDebouncer::triggered, this, [=](QString filter) {
setSearchFilter(searchEdit->text()); setSearchFilter(filter);
}); });
#endif #endif
connect(&comicQueryResultProcessor, &ComicQueryResultProcessor::newData, this, &LibraryWindow::setComicSearchFilterData); connect(&comicQueryResultProcessor, &ComicQueryResultProcessor::newData, this, &LibraryWindow::setComicSearchFilterData);

View File

@ -99,6 +99,7 @@ class YACReaderMacOSXSearchLineEdit : public YACReaderSearchLineEdit
class YACReaderMacOSXToolbar : public YACReaderMainToolBar class YACReaderMacOSXToolbar : public YACReaderMainToolBar
{ {
Q_OBJECT
public: public:
explicit YACReaderMacOSXToolbar(QWidget *parent = 0); explicit YACReaderMacOSXToolbar(QWidget *parent = 0);
QSize sizeHint() const override; QSize sizeHint() const override;
@ -109,20 +110,41 @@ public:
void updateViewSelectorIcon(const QIcon &icon); void updateViewSelectorIcon(const QIcon &icon);
void attachToWindow(QMainWindow *window); void attachToWindow(QMainWindow *window);
void *getSearchEditDelegate() { return searchEditDelegate; };
void emitFilterChange(const QString &filter) { emit filterChanged(filter); };
QAction *actionFromIdentifier(const QString &identifier);
signals:
void filterChanged(QString);
private: private:
void paintEvent(QPaintEvent *) override; void paintEvent(QPaintEvent *) override;
void *searchEditDelegate;
}; };
#else #else
#include <QtWidgets> #include <QtWidgets>
class YACReaderMacOSXToolbar : public QToolBar class YACReaderMacOSXToolbar : public QWidget
{ {
Q_OBJECT
public: public:
explicit YACReaderMacOSXToolbar(QWidget *parent = 0); explicit YACReaderMacOSXToolbar(QWidget *parent = 0);
void attachToWindow(QMainWindow *window); void attachToWindow(QMainWindow *window);
void addStretch(); void addStretch();
void setMovable(bool movable) {};
void addSeparator() {};
void setIconSize(const QSize &size) {};
public slots:
void setHidden(bool hidden);
void show();
void hide();
}; };
#endif #endif

View File

@ -398,8 +398,294 @@ void MacToolBarItemWrapper::updateIcon(bool enabled)
} }
#else #else
#import <AppKit/AppKit.h>
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
NSImage *QIconToNSImage(const QIcon &icon, const QSize &size, const QColor &color = QColor())
{
QPixmap pixmap = icon.pixmap(size);
QImage qImage = pixmap.toImage().convertToFormat(QImage::Format_RGBA8888);
if (color.isValid()) {
QPainter p;
QImage mask(qImage);
p.begin(&mask);
p.setCompositionMode(QPainter::CompositionMode_SourceIn);
QBrush brush(color);
p.fillRect(QRect(0, 0, size.width(), size.height()), brush);
p.end();
p.begin(&qImage);
p.setCompositionMode(QPainter::CompositionMode_Overlay);
p.drawImage(0, 0, mask);
p.end();
}
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(
(void *)qImage.bits(),
qImage.width(),
qImage.height(),
8,
qImage.bytesPerLine(),
colorSpace,
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGImageRef cgImage = CGBitmapContextCreateImage(context);
NSImage *nsImage = [[NSImage alloc] initWithCGImage:cgImage size:NSMakeSize(qImage.width(), qImage.height())];
// Clean up
CGImageRelease(cgImage);
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
return nsImage;
}
void bindActionToNSToolbarItem(QAction *action, NSToolbarItem *toolbarItem, const QColor &iconColor = QColor())
{
if (action == nullptr || toolbarItem == nil) {
return;
}
auto update = [=] {
toolbarItem.enabled = action->isEnabled();
QString text = action->text();
QString tooltip = action->toolTip();
toolbarItem.label = text.isEmpty() ? @"" : [NSString stringWithUTF8String:text.toUtf8().constData()];
toolbarItem.paletteLabel = toolbarItem.label;
toolbarItem.toolTip = tooltip.isEmpty() ? @"" : [NSString stringWithUTF8String:tooltip.toUtf8().constData()];
QIcon icon = action->icon();
__auto_type image = QIconToNSImage(icon, { 24, 24 }, iconColor);
if (action->isChecked()) {
NSSize size = image.size;
NSImage *decoratedImage = [[NSImage alloc] initWithSize:size];
[decoratedImage lockFocus];
NSRect rect = NSMakeRect(0, 0, size.width, size.height);
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:rect xRadius:8 yRadius:8];
[[NSColor colorWithCalibratedRed:0.8 green:0.8 blue:0.8 alpha:0.9] setFill];
[path fill];
NSRect imageRect = NSMakeRect(4, 4, size.width - 8, size.height - 8);
[image drawInRect:imageRect
fromRect:NSZeroRect
operation:NSCompositingOperationSourceOver
fraction:1.0];
[decoratedImage unlockFocus];
toolbarItem.image = decoratedImage;
} else {
NSSize size = image.size;
NSImage *decoratedImage = [[NSImage alloc] initWithSize:size];
[decoratedImage lockFocus];
NSRect imageRect = NSMakeRect(4, 4, size.width - 8, size.height - 8);
[image drawInRect:imageRect
fromRect:NSZeroRect
operation:NSCompositingOperationSourceOver
fraction:1.0];
[decoratedImage unlockFocus];
toolbarItem.image = decoratedImage;
}
[image release];
};
if (action->isCheckable()) {
QObject::connect(
action, &QAction::triggered,
[=](bool checked) {
update();
});
}
QObject::connect(
action, &QAction::enabledChanged,
[=](bool enabled) {
toolbarItem.enabled = enabled;
});
QObject::connect(
action, &QAction::changed,
[=]() {
update();
});
toolbarItem.bordered = YES;
update();
}
#ifdef YACREADER_LIBRARY #ifdef YACREADER_LIBRARY
@interface YACReaderLibraryToolbarDelegate : NSObject <NSToolbarDelegate> {
@public
YACReaderMacOSXToolbar *mytoolbar;
}
- (IBAction)itemClicked:(id)sender;
@end
@implementation YACReaderLibraryToolbarDelegate
- (NSArray<NSToolbarItemIdentifier> *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar
{
Q_UNUSED(toolbar);
return @[
@"Back",
@"Forward",
@"Settings",
@"Server",
@"Help",
NSToolbarSpaceItemIdentifier,
@"ToggleView",
NSToolbarSpaceItemIdentifier,
@"Search",
];
}
- (NSArray<NSToolbarItemIdentifier> *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar
{
Q_UNUSED(toolbar);
return @[
@"Back",
@"Forward",
@"Settings",
@"Server",
@"Help",
@"ToggleView",
@"Search",
NSToolbarSpaceItemIdentifier,
];
}
/*
- (NSArray *)toolbarSelectableItemIdentifiers: (NSToolbar *)toolbar
{
Q_UNUSED(toolbar);
NSMutableArray *array = [[NSMutableArray alloc] init];
QList<QMacToolBarItem *> items = mytoolbar->items();
foreach (const QMacToolBarItem * item, items) {
[array addObject : item->nativeToolBarItem().itemIdentifier];
}
return array;
//NSMutableArray *array = toolbarPrivate->getItemIdentifiers(toolbarPrivate->items, true);
//[array addObjectsFromArray:toolbarPrivate->getItemIdentifiers(toolbarPrivate->allowedItems, true)];
//return array;
}*/
- (IBAction)itemClicked:(id)sender
{
NSToolbarItem *item = reinterpret_cast<NSToolbarItem *>(sender);
QString identifier = QString::fromNSString([item itemIdentifier]);
QAction *action = mytoolbar->actionFromIdentifier(identifier);
;
if (action != nullptr) {
action->trigger();
}
}
- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)willBeInserted
{
Q_UNUSED(toolbar);
Q_UNUSED(willBeInserted);
QString identifier = QString::fromNSString(itemIdentifier);
if (identifier == "Search") {
NSSearchToolbarItem *searchItem = [[NSSearchToolbarItem alloc] initWithItemIdentifier:itemIdentifier];
searchItem.resignsFirstResponderWithCancel = true;
searchItem.searchField.delegate = id<NSSearchFieldDelegate>(mytoolbar->getSearchEditDelegate());
searchItem.toolTip = @"Search";
return searchItem;
}
NSToolbarItem *toolbarItem = [[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier];
toolbarItem.target = self;
toolbarItem.action = @selector(itemClicked:);
QAction *action = mytoolbar->actionFromIdentifier(identifier);
if (identifier == "Back") {
toolbarItem.navigational = YES;
} else if (identifier == "Forward") {
toolbarItem.navigational = YES;
}
bindActionToNSToolbarItem(action, toolbarItem);
return toolbarItem;
}
- (BOOL)validateToolbarItem:(NSToolbarItem *)item
{
QString identifier = QString::fromNSString([item itemIdentifier]);
if (identifier == "Search") {
return YES;
}
QAction *action = mytoolbar->actionFromIdentifier(identifier);
if (action == nullptr) {
return NO;
}
return action->isEnabled();
}
@end
@interface YACReaderLibrarySearchDelegate : NSObject <NSSearchFieldDelegate> {
@public
YACReaderMacOSXToolbar *mytoolbar;
}
@end
@implementation YACReaderLibrarySearchDelegate
- (void)searchFieldDidStartSearching:(NSSearchField *)sender
{
}
- (void)searchFieldDidEndSearching:(NSSearchField *)sender
{
[sender resignFirstResponder];
}
- (void)controlTextDidChange:(NSNotification *)notification
{
NSSearchField *searchField = notification.object;
NSLog(@"Search text changed: %@", searchField.stringValue);
mytoolbar->emitFilterChange(QString::fromNSString(searchField.stringValue));
}
@end
YACReaderMacOSXToolbar::YACReaderMacOSXToolbar(QWidget *parent) YACReaderMacOSXToolbar::YACReaderMacOSXToolbar(QWidget *parent)
: YACReaderMainToolBar(parent) : YACReaderMainToolBar(parent)
{ {
@ -467,22 +753,159 @@ void YACReaderMacOSXToolbar::updateViewSelectorIcon(const QIcon &icon)
void YACReaderMacOSXToolbar::attachToWindow(QMainWindow *window) void YACReaderMacOSXToolbar::attachToWindow(QMainWindow *window)
{ {
auto toolbar = new QToolBar(); NSView *nsview = (NSView *)window->winId();
NSWindow *nswindow = [nsview window];
toolbar->addWidget(this); YACReaderLibrarySearchDelegate *searchDelegate = [[YACReaderLibrarySearchDelegate alloc] init];
toolbar->setMovable(false); this->searchEditDelegate = searchDelegate;
searchDelegate->mytoolbar = this;
window->addToolBar(toolbar); // Create the NSToolbar
NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier:@"mainToolbar"];
[toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
[toolbar setShowsBaselineSeparator:false];
__auto_type delegate = [[YACReaderLibraryToolbarDelegate alloc] init];
delegate->mytoolbar = this;
[toolbar setDelegate:delegate];
[nswindow setToolbar:toolbar];
} }
void YACReaderMacOSXToolbar::paintEvent(QPaintEvent *) void YACReaderMacOSXToolbar::paintEvent(QPaintEvent *)
{ {
} }
QAction *YACReaderMacOSXToolbar::actionFromIdentifier(const QString &identifier)
{
if (identifier == "Back") {
return backButton->defaultAction();
} else if (identifier == "Forward") {
return forwardButton->defaultAction();
} else if (identifier == "Settings") {
return settingsButton->defaultAction();
} else if (identifier == "Server") {
return serverButton->defaultAction();
} else if (identifier == "Help") {
return helpButton->defaultAction();
} else if (identifier == "ToggleView") {
return toggleComicsViewButton->defaultAction();
}
return nullptr;
}
#else #else
@interface YACReaderToolbarDelegate : NSObject <NSToolbarDelegate> {
@public
YACReaderMacOSXToolbar *mytoolbar;
}
- (IBAction)itemClicked:(id)sender;
@end
@implementation YACReaderToolbarDelegate
- (NSArray<NSToolbarItemIdentifier> *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar
{
Q_UNUSED(toolbar);
auto actions = mytoolbar->actions();
NSMutableArray<NSToolbarItemIdentifier> *identifiers = [NSMutableArray arrayWithCapacity:actions.size()];
for (QAction *action : actions) {
[identifiers addObject:[NSString stringWithFormat:@"action_%p", action]];
}
return identifiers;
}
- (NSArray<NSToolbarItemIdentifier> *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar
{
Q_UNUSED(toolbar);
auto actions = mytoolbar->actions();
NSMutableArray<NSToolbarItemIdentifier> *identifiers = [NSMutableArray arrayWithCapacity:actions.size()];
for (QAction *action : actions) {
[identifiers addObject:[NSString stringWithFormat:@"action_%p", action]];
}
return identifiers;
}
// - (NSArray *)toolbarSelectableItemIdentifiers:(NSToolbar *)toolbar
// {
// Q_UNUSED(toolbar);
// auto actions = mytoolbar->actions();
// NSMutableArray<NSToolbarItemIdentifier> *identifiers = [NSMutableArray arrayWithCapacity:actions.size()];
// for (QAction *action : actions) {
// if (action->isCheckable()) {
// [identifiers addObject:[NSString stringWithFormat:@"action_%p", action]];
// }
// }
// return identifiers;
// }
- (IBAction)itemClicked:(id)sender
{
NSToolbarItem *item = reinterpret_cast<NSToolbarItem *>(sender);
NSString *itemIdentifier = [item itemIdentifier];
auto actions = mytoolbar->actions();
for (QAction *action : actions) {
if ([itemIdentifier isEqualTo:[NSString stringWithFormat:@"action_%p", action]]) {
action->trigger();
}
}
}
- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)willBeInserted
{
Q_UNUSED(toolbar);
Q_UNUSED(willBeInserted);
NSToolbarItem *toolbarItem = [[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier];
toolbarItem.target = self;
toolbarItem.action = @selector(itemClicked:);
auto actions = mytoolbar->actions();
for (QAction *action : actions) {
if ([itemIdentifier isEqualTo:[NSString stringWithFormat:@"action_%p", action]]) {
bindActionToNSToolbarItem(action, toolbarItem, QColor(200, 200, 200));
}
}
return toolbarItem;
}
- (BOOL)validateToolbarItem:(NSToolbarItem *)item
{
NSString *itemIdentifier = [item itemIdentifier];
auto actions = mytoolbar->actions();
for (QAction *action : actions) {
if ([itemIdentifier isEqualTo:[NSString stringWithFormat:@"action_%p", action]]) {
return action->isEnabled();
}
}
return NO;
}
@end
YACReaderMacOSXToolbar::YACReaderMacOSXToolbar(QWidget *parent) YACReaderMacOSXToolbar::YACReaderMacOSXToolbar(QWidget *parent)
: QToolBar(parent) : QWidget(parent)
{ {
setMovable(false); setMovable(false);
setIconSize(QSize(24, 24)); setIconSize(QSize(24, 24));
@ -490,14 +913,42 @@ YACReaderMacOSXToolbar::YACReaderMacOSXToolbar(QWidget *parent)
void YACReaderMacOSXToolbar::attachToWindow(QMainWindow *window) void YACReaderMacOSXToolbar::attachToWindow(QMainWindow *window)
{ {
window->setUnifiedTitleAndToolBarOnMac(true); NSView *nsview = (NSView *)window->winId();
window->addToolBar(this); NSWindow *nswindow = [nsview window];
NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier:@"mainToolbar"];
[toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
[toolbar setShowsBaselineSeparator:false];
__auto_type delegate = [[YACReaderToolbarDelegate alloc] init];
delegate->mytoolbar = this;
[toolbar setDelegate:delegate];
[nswindow setToolbar:toolbar];
} }
void YACReaderMacOSXToolbar::addStretch() void YACReaderMacOSXToolbar::addStretch()
{ {
} }
void YACReaderMacOSXToolbar::setHidden(bool hidden)
{
NSView *nsView = reinterpret_cast<NSView *>(this->winId());
NSWindow *window = [nsView window];
if (window && window.toolbar) {
window.toolbar.visible = !hidden;
}
}
void YACReaderMacOSXToolbar::show()
{
setHidden(false);
}
void YACReaderMacOSXToolbar::hide()
{
setHidden(true);
}
#endif #endif
#endif #endif

View File

@ -180,4 +180,29 @@ KDSignalLeadingDebouncer::KDSignalLeadingDebouncer(QObject *parent)
KDSignalLeadingDebouncer::~KDSignalLeadingDebouncer() = default; KDSignalLeadingDebouncer::~KDSignalLeadingDebouncer() = default;
KDStringSignalDebouncer::KDStringSignalDebouncer(QObject *parent)
: QObject(parent), m_debouncer(KDGenericSignalThrottler::Kind::Debouncer,
KDGenericSignalThrottler::EmissionPolicy::Trailing,
parent)
{
connect(&m_debouncer, &KDGenericSignalThrottler::triggered,
this, [=] {
emit triggered(this->value);
});
}
void KDStringSignalDebouncer::setTimeout(int msec) {
m_debouncer.setTimeout(msec);
}
int KDStringSignalDebouncer::timeout() const {
return m_debouncer.timeout();
}
void KDStringSignalDebouncer::throttle(QString value) {
this->value = value;
m_debouncer.throttle();
}
} // namespace KDToolBox } // namespace KDToolBox

View File

@ -125,6 +125,26 @@ public:
~KDSignalLeadingDebouncer() override; ~KDSignalLeadingDebouncer() override;
}; };
class KDStringSignalDebouncer : public QObject {
Q_OBJECT
public:
explicit KDStringSignalDebouncer(QObject *parent = nullptr);
void setTimeout(int msec);
int timeout() const;
public slots:
void throttle(QString value);
signals:
void triggered(QString value);
private:
QString value;
KDGenericSignalThrottler m_debouncer;
};
} // namespace KDToolBox } // namespace KDToolBox
#endif // KDSIGNALTHROTTLER_H #endif // KDSIGNALTHROTTLER_H