From 3632ebab125a5f50b5aabaf1bb2acff61d5f40ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20A=CC=81ngel=20San=20Marti=CC=81n=20Rodri=CC=81guez?= Date: Sun, 20 Apr 2025 09:38:58 +0200 Subject: [PATCH] Implement native toolbars on macos on Qt6 to have a modern looking unified toolbars --- .gitignore | 1 + YACReader/main_window_viewer.cpp | 4 + YACReaderLibrary/library_window.cpp | 15 +- custom_widgets/yacreader_macosx_toolbar.h | 24 +- custom_widgets/yacreader_macosx_toolbar.mm | 465 +++++++++++++++++++- third_party/KDToolBox/KDSignalThrottler.cpp | 25 ++ third_party/KDToolBox/KDSignalThrottler.h | 20 + 7 files changed, 539 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index c2e46186..f4cf6ce5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ libp7zip YACReader/build YACReaderLibrary/build YACReaderLibraryServer/build +build/ # C++ objects and libs *.slo diff --git a/YACReader/main_window_viewer.cpp b/YACReader/main_window_viewer.cpp index 1ac26eb5..fdbd31c5 100644 --- a/YACReader/main_window_viewer.cpp +++ b/YACReader/main_window_viewer.cpp @@ -1319,7 +1319,11 @@ void MainWindowViewer::toggleFitToWidthSlider() if (zoomSliderAction->isVisible()) { zoomSliderAction->hide(); } 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); +#endif zoomSliderAction->show(); } } diff --git a/YACReaderLibrary/library_window.cpp b/YACReaderLibrary/library_window.cpp index 093dbdd9..5528dc81 100644 --- a/YACReaderLibrary/library_window.cpp +++ b/YACReaderLibrary/library_window.cpp @@ -770,19 +770,20 @@ void LibraryWindow::createConnections() connect(optionsDialog, &YACReaderOptionsDialog::optionsChanged, this, &LibraryWindow::reloadOptions); connect(optionsDialog, &YACReaderOptionsDialog::editShortcuts, editShortcutsDialog, &QWidget::show); - auto searchDebouncer = new KDToolBox::KDSignalDebouncer(this); + auto searchDebouncer = new KDToolBox::KDStringSignalDebouncer(this); searchDebouncer->setTimeout(400); // Search filter #ifdef Y_MAC_UI - connect(searchEdit, &YACReaderMacOSXSearchLineEdit::filterChanged, searchDebouncer, &KDToolBox::KDSignalThrottler::throttle); - connect(searchDebouncer, &KDToolBox::KDSignalThrottler::triggered, this, [=] { - setSearchFilter(searchEdit->text()); + connect(libraryToolBar, &YACReaderMacOSXToolbar::filterChanged, searchDebouncer, &KDToolBox::KDStringSignalDebouncer::throttle); + connect(searchEdit, &YACReaderMacOSXSearchLineEdit::filterChanged, searchDebouncer, &KDToolBox::KDStringSignalDebouncer::throttle); + connect(searchDebouncer, &KDToolBox::KDStringSignalDebouncer::triggered, this, [=](QString filter) { + setSearchFilter(filter); }); #else - connect(searchEdit, &YACReaderSearchLineEdit::filterChanged, searchDebouncer, &KDToolBox::KDSignalThrottler::throttle); - connect(searchDebouncer, &KDToolBox::KDSignalThrottler::triggered, this, [=] { - setSearchFilter(searchEdit->text()); + connect(searchEdit, &YACReaderSearchLineEdit::filterChanged, searchDebouncer, &KDToolBox::KDStringSignalDebouncer::throttle); + connect(searchDebouncer, &KDToolBox::KDStringSignalDebouncer::triggered, this, [=](QString filter) { + setSearchFilter(filter); }); #endif connect(&comicQueryResultProcessor, &ComicQueryResultProcessor::newData, this, &LibraryWindow::setComicSearchFilterData); diff --git a/custom_widgets/yacreader_macosx_toolbar.h b/custom_widgets/yacreader_macosx_toolbar.h index 7ee40cca..45644246 100644 --- a/custom_widgets/yacreader_macosx_toolbar.h +++ b/custom_widgets/yacreader_macosx_toolbar.h @@ -99,6 +99,7 @@ class YACReaderMacOSXSearchLineEdit : public YACReaderSearchLineEdit class YACReaderMacOSXToolbar : public YACReaderMainToolBar { + Q_OBJECT public: explicit YACReaderMacOSXToolbar(QWidget *parent = 0); QSize sizeHint() const override; @@ -109,20 +110,41 @@ public: void updateViewSelectorIcon(const QIcon &icon); 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: void paintEvent(QPaintEvent *) override; + + void *searchEditDelegate; }; #else #include -class YACReaderMacOSXToolbar : public QToolBar +class YACReaderMacOSXToolbar : public QWidget { + Q_OBJECT public: explicit YACReaderMacOSXToolbar(QWidget *parent = 0); void attachToWindow(QMainWindow *window); void addStretch(); + + void setMovable(bool movable) {}; + void addSeparator() {}; + + void setIconSize(const QSize &size) {}; + +public slots: + void setHidden(bool hidden); + void show(); + void hide(); }; #endif diff --git a/custom_widgets/yacreader_macosx_toolbar.mm b/custom_widgets/yacreader_macosx_toolbar.mm index 7f3d6137..e2d07450 100644 --- a/custom_widgets/yacreader_macosx_toolbar.mm +++ b/custom_widgets/yacreader_macosx_toolbar.mm @@ -398,8 +398,294 @@ void MacToolBarItemWrapper::updateIcon(bool enabled) } #else +#import +#import +#import + +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 +@interface YACReaderLibraryToolbarDelegate : NSObject { +@public + YACReaderMacOSXToolbar *mytoolbar; +} + +- (IBAction)itemClicked:(id)sender; + +@end + +@implementation YACReaderLibraryToolbarDelegate + +- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar +{ + Q_UNUSED(toolbar); + + return @[ + @"Back", + @"Forward", + @"Settings", + @"Server", + @"Help", + NSToolbarSpaceItemIdentifier, + @"ToggleView", + NSToolbarSpaceItemIdentifier, + @"Search", + ]; +} + +- (NSArray *)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 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(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(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 { +@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) : YACReaderMainToolBar(parent) { @@ -467,22 +753,159 @@ void YACReaderMacOSXToolbar::updateViewSelectorIcon(const QIcon &icon) void YACReaderMacOSXToolbar::attachToWindow(QMainWindow *window) { - auto toolbar = new QToolBar(); + NSView *nsview = (NSView *)window->winId(); + NSWindow *nswindow = [nsview window]; - toolbar->addWidget(this); - toolbar->setMovable(false); + YACReaderLibrarySearchDelegate *searchDelegate = [[YACReaderLibrarySearchDelegate alloc] init]; + 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 *) { } +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 +@interface YACReaderToolbarDelegate : NSObject { +@public + YACReaderMacOSXToolbar *mytoolbar; +} + +- (IBAction)itemClicked:(id)sender; + +@end + +@implementation YACReaderToolbarDelegate + +- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar +{ + Q_UNUSED(toolbar); + + auto actions = mytoolbar->actions(); + NSMutableArray *identifiers = [NSMutableArray arrayWithCapacity:actions.size()]; + + for (QAction *action : actions) { + [identifiers addObject:[NSString stringWithFormat:@"action_%p", action]]; + } + + return identifiers; +} + +- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar +{ + Q_UNUSED(toolbar); + + auto actions = mytoolbar->actions(); + NSMutableArray *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 *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(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) - : QToolBar(parent) + : QWidget(parent) { setMovable(false); setIconSize(QSize(24, 24)); @@ -490,14 +913,42 @@ YACReaderMacOSXToolbar::YACReaderMacOSXToolbar(QWidget *parent) void YACReaderMacOSXToolbar::attachToWindow(QMainWindow *window) { - window->setUnifiedTitleAndToolBarOnMac(true); - window->addToolBar(this); + NSView *nsview = (NSView *)window->winId(); + 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::setHidden(bool hidden) +{ + NSView *nsView = reinterpret_cast(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 diff --git a/third_party/KDToolBox/KDSignalThrottler.cpp b/third_party/KDToolBox/KDSignalThrottler.cpp index ddadf6d2..c76a3263 100644 --- a/third_party/KDToolBox/KDSignalThrottler.cpp +++ b/third_party/KDToolBox/KDSignalThrottler.cpp @@ -180,4 +180,29 @@ KDSignalLeadingDebouncer::KDSignalLeadingDebouncer(QObject *parent) 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 diff --git a/third_party/KDToolBox/KDSignalThrottler.h b/third_party/KDToolBox/KDSignalThrottler.h index 60e0c1ee..7554f7f7 100644 --- a/third_party/KDToolBox/KDSignalThrottler.h +++ b/third_party/KDToolBox/KDSignalThrottler.h @@ -125,6 +125,26 @@ public: ~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 #endif // KDSIGNALTHROTTLER_H