diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index da2f614..aa14f54 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -12,6 +12,10 @@ add_subdirectory(model) add_subdirectory(utils) add_subdirectory(openpgp) +if(APPLE) +add_subdirectory(kdmactouchbar) +endif() + qt5_add_resources(RESOURCES assets.qrc) # Compile source files (.h/.cpp) @@ -201,6 +205,12 @@ target_link_libraries(feather ${QRENCODE_LIBRARY} ) +if(APPLE) + target_link_libraries(feather + KDMacTouchBar + ) +endif() + if(NOT APPLE) target_link_libraries(feather Qt5::QSvgIconPlugin diff --git a/src/kdmactouchbar/CMakeLists.txt b/src/kdmactouchbar/CMakeLists.txt new file mode 100644 index 0000000..682aa07 --- /dev/null +++ b/src/kdmactouchbar/CMakeLists.txt @@ -0,0 +1,46 @@ +cmake_minimum_required(VERSION 2.8.7) + +if("${CMAKE_INSTALL_PREFIX}" STREQUAL "") + set(USE_DEFAULT_INSTALL_LOCATION True) +else() + set(USE_DEFAULT_INSTALL_LOCATION False) +endif() + +project(KDMacTouchBar CXX) + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "" FORCE) +endif() + +set(${PROJECT_NAME}_VERSION_MAJOR 1) +set(${PROJECT_NAME}_VERSION_MINOR 0) +set(${PROJECT_NAME}_VERSION_PATCH 0) +set(${PROJECT_NAME}_VERSION ${${PROJECT_NAME}_VERSION_MAJOR}.${${PROJECT_NAME}_VERSION_MINOR}.${${PROJECT_NAME}_VERSION_PATCH}) + +if(CMAKE_VERSION VERSION_LESS "3.1") + if(CMAKE_COMPILER_IS_GNUCXX) + set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") + endif () +else() + set(CMAKE_CXX_STANDARD 11) +endif() + +if(USE_DEFAULT_INSTALL_LOCATION) + set(CMAKE_INSTALL_PREFIX "/usr/local/KDAB/${PROJECT_NAME}-${${PROJECT_NAME}_VERSION}" CACHE STRING "" FORCE) +endif() + +message(STATUS "Building ${PROJECT_NAME} ${${PROJECT_NAME}_VERSION} in ${CMAKE_BUILD_TYPE} mode. Installing to ${CMAKE_INSTALL_PREFIX}") + +find_package(Qt5Core REQUIRED) +find_package(Qt5Widgets REQUIRED) + +set(CMAKE_AUTOMOC TRUE) +set(QT_LIBRARIES Qt5::Widgets) +set(QT_USE_FILE "${CMAKE_CURRENT_SOURCE_DIR}/cmake/Qt5Portability.cmake") + +# not generated +#install(FILES +# "${CMAKE_CURRENT_SOURCE_DIR}/cmake/KDMacTouchBarConfig.cmake" +# DESTINATION "${CMAKE_INSTALL_PREFIX}/lib/cmake") + +add_subdirectory(src) \ No newline at end of file diff --git a/src/kdmactouchbar/src/CMakeLists.txt b/src/kdmactouchbar/src/CMakeLists.txt new file mode 100644 index 0000000..89208a7 --- /dev/null +++ b/src/kdmactouchbar/src/CMakeLists.txt @@ -0,0 +1,25 @@ +#include(${QT_USE_FILE}) + +set(HEADERS kdmactouchbar.h + kdmactouchbar_global.h) + +add_definitions(-DKDMACTOUCHBAR_BUILD_KDMACTOUCHBAR_LIB -DQT_NO_CAST_TO_ASCII -DQT_ASCII_CAST_WARNING) + +add_library(KDMacTouchBar SHARED + kdmactouchbar.mm + ${HEADERS}) + +install(TARGETS KDMacTouchBar + LIBRARY DESTINATION "${CMAKE_INSTALL_PREFIX}/lib") + +target_link_libraries(KDMacTouchBar ${QT_LIBRARIES} "-framework Cocoa") + +set_target_properties(KDMacTouchBar PROPERTIES + FRAMEWORK TRUE + FRAMEWORK_VERSION 1 + MACOSX_FRAMEWORK_IDENTIFIER com.kdab.KDMacTouchBar + # fails building package on binary-factory + # PUBLIC_HEADER "${HEADERS}" + LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/lib" + MACOSX_RPATH TRUE + ) \ No newline at end of file diff --git a/src/kdmactouchbar/src/kdmactouchbar.h b/src/kdmactouchbar/src/kdmactouchbar.h new file mode 100644 index 0000000..5d98cbf --- /dev/null +++ b/src/kdmactouchbar/src/kdmactouchbar.h @@ -0,0 +1,87 @@ +/**************************************************************************** +** Copyright (C) 2019-2020 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com. +** All rights reserved. +** +** This file is part of the KD MacTouchBar library. +** +** This file may be distributed and/or modified under the terms of the +** GNU Lesser General Public License version 3 as published by the +** Free Software Foundation and appearing in the file LICENSE.LGPL.txt included. +** +** You may even contact us at info@kdab.com for different licensing options. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** Contact info@kdab.com if any conditions of this licensing are not +** clear to you. +** +**********************************************************************/ + +#ifndef KDMACTOUCHBAR_H +#define KDMACTOUCHBAR_H + +#include "kdmactouchbar_global.h" + +#include + +QT_BEGIN_NAMESPACE + +class QDialogButtonBox; +class QMessageBox; +class QTabBar; + + +class KDMACTOUCHBAR_EXPORT KDMacTouchBar : public QWidget +{ + Q_OBJECT + Q_PROPERTY(QAction *principialAction READ principialAction WRITE setPrincipialAction) + Q_PROPERTY(QAction *escapeAction READ escapeAction WRITE setEscapeAction) + Q_PROPERTY(TouchButtonStyle touchButtonStyle READ touchButtonStyle WRITE setTouchButtonStyle) +public: + explicit KDMacTouchBar(QWidget *parent = nullptr); + explicit KDMacTouchBar(QMessageBox *messageBox); + ~KDMacTouchBar(); + + enum TouchButtonStyle + { + IconOnly, + TextOnly, + TextBesideIcon + }; + + static void setAutomaticallyCreateMessageBoxTouchBar(bool automatic); + static bool isAutomacicallyCreatingMessageBoxTouchBar(); + + QAction *addSeparator(); + QAction *addTabBar(QTabBar *tabBar); + void removeTabBar(QTabBar *tabBar); + + QAction *addMessageBox(QMessageBox *messageBox); + void removeMessageBox(QMessageBox *messageBox); + + QAction *addDialogButtonBox(QDialogButtonBox *buttonBox); + void removeDialogButtonBox(QDialogButtonBox *buttonBox); + + void setPrincipialAction(QAction *action); + QAction *principialAction() const; + + void setEscapeAction(QAction *action); + QAction *escapeAction() const; + + void setTouchButtonStyle(TouchButtonStyle touchButtonStyle); + TouchButtonStyle touchButtonStyle() const; + + void clear(); + +protected: + bool event(QEvent *event); + +private: + class Private; + Private *const d; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/kdmactouchbar/src/kdmactouchbar.mm b/src/kdmactouchbar/src/kdmactouchbar.mm new file mode 100644 index 0000000..21c97f4 --- /dev/null +++ b/src/kdmactouchbar/src/kdmactouchbar.mm @@ -0,0 +1,980 @@ +/**************************************************************************** +** Copyright (C) 2019-2020 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com. +** All rights reserved. +** +** This file is part of the KD MacTouchBar library. +** +** This file may be distributed and/or modified under the terms of the +** GNU Lesser General Public License version 3 as published by the +** Free Software Foundation and appearing in the file LICENSE.LGPL.txt included. +** +** You may even contact us at info@kdab.com for different licensing options. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** Contact info@kdab.com if any conditions of this licensing are not +** clear to you. +** +**********************************************************************/ +#include "kdmactouchbar.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#import + +QT_BEGIN_NAMESPACE + +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) +NSImage *qt_mac_create_nsimage(const QIcon &icon, int defaultSize = 0); +#else +// defined in gui/painting/qcoregraphics.mm +@interface NSImage (QtExtras) ++ (instancetype)imageFromQIcon:(const QIcon &)icon; +@end +static NSImage *qt_mac_create_nsimage(const QIcon &icon) +{ + return [NSImage imageFromQIcon:icon]; +} +#endif + +static QString identifierForAction(QObject *action) +{ + return QStringLiteral("0x%1 %2") + .arg((quintptr)action, QT_POINTER_SIZE * 2, 16, QLatin1Char('0')) + .arg(action->objectName()); +} + + +static QString removeMnemonics(const QString &original) +{ + QString returnText(original.size(), 0); + int finalDest = 0; + int currPos = 0; + int l = original.length(); + while (l) { + if (original.at(currPos) == QLatin1Char('&')) { + ++currPos; + --l; + if (l == 0) + break; + } else if (original.at(currPos) == QLatin1Char('(') && l >= 4 && + original.at(currPos + 1) == QLatin1Char('&') && + original.at(currPos + 2) != QLatin1Char('&') && + original.at(currPos + 3) == QLatin1Char(')')) { + /* remove mnemonics its format is "\s*(&X)" */ + int n = 0; + while (finalDest > n && returnText.at(finalDest - n - 1).isSpace()) + ++n; + finalDest -= n; + currPos += 4; + l -= 4; + continue; + } + returnText[finalDest] = original.at(currPos); + ++currPos; + ++finalDest; + --l; + } + returnText.truncate(finalDest); + return returnText; +} + +class TabBarAction : public QAction +{ +public: + TabBarAction(QTabBar *tabBar, QObject *parent) + : QAction(parent) + { + setData(QVariant::fromValue(tabBar)); + connect(tabBar, &QTabBar::currentChanged, this, &TabBarAction::sendDataChanged); + connect(tabBar, &QObject::destroyed, this, &QObject::deleteLater); + } + + void sendDataChanged() + { + QActionEvent e(QEvent::ActionChanged, this); + for (auto w : associatedWidgets()) + QApplication::sendEvent(w, &e); + } +}; + +class ButtonBoxAction : public QAction +{ +public: + ButtonBoxAction(QDialogButtonBox *buttonBox, QObject *parent) + : QAction(parent) + , mButtonBox(buttonBox) + { + setData(QVariant::fromValue(buttonBox)); + + checkStandardButtonAndTexts(); + + auto timer = new QTimer(buttonBox); + timer->start(200); + connect(timer, &QTimer::timeout, this, &ButtonBoxAction::checkStandardButtonAndTexts); + connect(buttonBox, &QObject::destroyed, this, &QObject::deleteLater); + } + + void checkStandardButtonAndTexts() + { + QStringList buttonTexts; + QPushButton *defaultButton = nullptr; + for (auto b : mButtonBox->buttons()) { + buttonTexts.append(b->text()); + if (auto pb = qobject_cast(b)) + if (pb->isDefault()) + defaultButton = pb; + } + + if (buttonTexts != mButtonTexts || defaultButton != mDefaultButton) { + sendDataChanged(); + mButtonTexts = buttonTexts; + mDefaultButton = defaultButton; + } + } + + void sendDataChanged() + { + QActionEvent e(QEvent::ActionChanged, this); + for (auto w : associatedWidgets()) + QApplication::sendEvent(w, &e); + } + + QDialogButtonBox *mButtonBox; + QList mButtons; + QPushButton *mDefaultButton = nullptr; + QStringList mButtonTexts; +}; + +@interface QObjectPointer : NSObject { +} +@property (readonly) QObject *qobject; +@end + +@implementation QObjectPointer +QObject *_qobject; +@synthesize qobject = _qobject; + +- (id)initWithQObject:(QObject *)aQObject +{ + self = [super init]; + _qobject = aQObject; + return self; +} + +@end + +class WidgetActionContainerWidget : public QWidget +{ +public: + WidgetActionContainerWidget(QWidget *widget, NSView *view) + : w(widget) + , v(view) + { + widget->setParent(this); + widget->move(0, 0); + if (!widget->testAttribute(Qt::WA_Resized)) + widget->resize(widget->sizeHint() + .boundedTo(QSize(widget->maximumWidth(), 30)) + .expandedTo(widget->minimumSize())); + setAttribute(Qt::WA_DontShowOnScreen); + setAttribute(Qt::WA_QuitOnClose, false); + ensurePolished(); + setVisible(true); + QPixmap pm(1, 1); + render(&pm, QPoint(), QRegion(), + widget->autoFillBackground() + ? (QWidget::DrawWindowBackground | QWidget::DrawChildren) + : QWidget::DrawChildren); + } + + QSize sizeHint() const { return w->size(); } + + void resizeEvent(QResizeEvent *event) { w->resize(event->size()); } + + bool event(QEvent *event) + { + if (event->type() == QEvent::UpdateRequest) + [v setNeedsDisplay:YES]; + return QWidget::event(event); + } + + QWidget *w; + NSView *v; +}; + +@interface WidgetActionView : NSView +@property WidgetActionContainerWidget *widget; +@property QWidget *touchTarget; +@property QWidgetAction *action; +@property (readonly) NSSize intrinsicContentSize; +@end + +@implementation WidgetActionView +@synthesize action; +@synthesize widget; +@synthesize touchTarget; + +- (NSSize)intrinsicContentSize +{ + return NSMakeSize(widget->width(), widget->height()); +} + +- (id)initWithWidgetAction:(QWidgetAction *)wa +{ + self = [super init]; + action = wa; + widget = new WidgetActionContainerWidget(action->requestWidget(nullptr), self); + if (!widget->testAttribute(Qt::WA_Resized)) + widget->resize(widget->sizeHint() + .boundedTo(QSize(widget->maximumWidth(), 30)) + .expandedTo(widget->minimumSize())); + [self setNeedsDisplay:YES]; + return self; +} + +- (void)dealloc +{ + widget->w->setParent(0); + [super dealloc]; +} + +- (void)drawRect:(NSRect)frame +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + widget->w->resize(frame.size.width, frame.size.height); + QPixmap pm(widget->w->size() * 2); + pm.setDevicePixelRatio(2); + pm.fill(Qt::transparent); + widget->w->render(&pm, QPoint(), QRegion(), + widget->w->autoFillBackground() + ? (QWidget::DrawWindowBackground | QWidget::DrawChildren) + : QWidget::DrawChildren); + CGImageRef cgImage = pm.toImage().toCGImage(); + NSImage *image = [[NSImage alloc] initWithCGImage:cgImage size:NSZeroSize]; + [image drawInRect:frame]; + [pool release]; +} + +- (void)touchesBeganWithEvent:(NSEvent *)event +{ + NSTouch *touch = [[event touchesMatchingPhase:NSTouchPhaseBegan inView:self] anyObject]; + const QPoint point = QPointF::fromCGPoint([touch locationInView:self]).toPoint(); + touchTarget = widget->childAt(point); + if (touchTarget == nullptr) + touchTarget = widget; + QMouseEvent e(QEvent::MouseButtonPress, touchTarget->mapFrom(widget, point), Qt::LeftButton, + Qt::LeftButton, Qt::NoModifier); + qApp->sendEvent(touchTarget, &e); +} +- (void)touchesMovedWithEvent:(NSEvent *)event +{ + NSTouch *touch = [[event touchesMatchingPhase:NSTouchPhaseMoved inView:self] anyObject]; + const QPoint point = QPointF::fromCGPoint([touch locationInView:self]).toPoint(); + QMouseEvent e(QEvent::MouseButtonPress, touchTarget->mapFrom(widget, point), Qt::LeftButton, + Qt::LeftButton, Qt::NoModifier); + qApp->sendEvent(touchTarget, &e); +} +- (void)touchesEndedWithEvent:(NSEvent *)event +{ + NSTouch *touch = [[event touchesMatchingPhase:NSTouchPhaseEnded inView:self] anyObject]; + const QPoint point = QPointF::fromCGPoint([touch locationInView:self]).toPoint(); + QMouseEvent e(QEvent::MouseButtonRelease, touchTarget->mapFrom(widget, point), Qt::LeftButton, + Qt::LeftButton, Qt::NoModifier); + qApp->sendEvent(touchTarget, &e); +} + +@end + +@interface DynamicTouchBarProviderDelegate : NSResponder +@property (strong) NSMutableDictionary *items; +@end + +@implementation DynamicTouchBarProviderDelegate +@synthesize items; + +- (id)initWithItems:(NSMutableDictionary *)i +{ + self = [super init]; + self.items = i; + return self; +} + +- (NSTouchBarItem *)touchBar:(NSTouchBar *)touchBar + makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier +{ + Q_UNUSED(touchBar); + + if ([self.items objectForKey:identifier] != nil) + return [self.items objectForKey:identifier]; + + return nil; +} +@end + +@interface DynamicTouchBarProvider + : NSResponder +@property (strong) NSMutableDictionary *items; +@property (strong) NSMutableDictionary *commands; +@property (strong) NSObject *qtDelegate; +@property (strong, readonly) NSTouchBar *touchBar; +@property (strong) DynamicTouchBarProviderDelegate *delegate; +@property KDMacTouchBar *qMacTouchBar; +@property QStack openedPopOvers; +@end + +@implementation DynamicTouchBarProvider +@synthesize items; +@synthesize commands; +@synthesize touchBar; +@synthesize delegate; +@synthesize qMacTouchBar; +@synthesize openedPopOvers; + +- (id)initWithKDMacTouchBar:(KDMacTouchBar *)bar +{ + self = [super init]; + items = [[NSMutableDictionary alloc] init]; + commands = [[NSMutableDictionary alloc] init]; + qMacTouchBar = bar; + delegate = [[DynamicTouchBarProviderDelegate alloc] initWithItems:items]; + return self; +} + +- (void)addItem:(QAction *)action +{ + // Create custom button item + NSString *identifier = identifierForAction(action).toNSString(); + NSTouchBarItem *item = nil; + NSView *view = nil; + + if (auto wa = qobject_cast(action)) { + NSPopoverTouchBarItem *i = + [[[NSPopoverTouchBarItem alloc] initWithIdentifier:identifier] autorelease]; + item = i; + view = [[WidgetActionView alloc] initWithWidgetAction:wa]; + i.collapsedRepresentation = view; + } else if (auto tb = qobject_cast(action->data().value())) { + NSCustomTouchBarItem *i = + [[[NSCustomTouchBarItem alloc] initWithIdentifier:identifier] autorelease]; + item = i; + NSMutableArray *labels = [[NSMutableArray alloc] init]; + view = + [[NSSegmentedControl segmentedControlWithLabels:labels + trackingMode:NSSegmentSwitchTrackingSelectOne + target:self + action:@selector(tabBarAction:)] autorelease]; + i.view = view; + } else if (auto bb = qobject_cast(action->data().value())) { + NSMutableArray *buttonItems = [[NSMutableArray alloc] init]; + + for (int i = 0; i < bb->layout()->count(); ++i) { + auto layoutItem = bb->layout()->itemAt(i); + if (auto b = qobject_cast(layoutItem->widget())) { + auto buttonIdentifier = identifierForAction(b).toNSString(); + NSCustomTouchBarItem *buttonItem = nil; + NSButton *button = nil; + if ([[items allKeys] containsObject:buttonIdentifier]) { + buttonItem = [items objectForKey:buttonIdentifier]; + button = buttonItem.view; + } else { + buttonItem = [[NSCustomTouchBarItem alloc] initWithIdentifier:buttonIdentifier]; + button = [NSButton buttonWithTitle:removeMnemonics(b->text()).toNSString() + target:self + action:@selector(buttonAction:)]; + } + if (b->isDefault()) + [button setKeyEquivalent:@"\r"]; + button.title = removeMnemonics(b->text()).toNSString(); + buttonItem.view = button; + [buttonItems addObject:buttonItem]; + [commands setObject:[[QObjectPointer alloc] initWithQObject:b] + forKey:[NSValue valueWithPointer:button]]; + [items setObject:buttonItem forKey:buttonIdentifier]; + } + } + + item = [[NSGroupTouchBarItem groupItemWithIdentifier:identifier items:buttonItems] + autorelease]; + } else if (action->isSeparator()) { + NSPopoverTouchBarItem *i = + [[[NSPopoverTouchBarItem alloc] initWithIdentifier:identifier] autorelease]; + item = i; + view = [NSTextField labelWithString:action->text().toNSString()]; + i.collapsedRepresentation = view; + } else { + NSPopoverTouchBarItem *i = + [[[NSPopoverTouchBarItem alloc] initWithIdentifier:identifier] autorelease]; + item = i; + view = [[NSButton buttonWithTitle:removeMnemonics(action->text()).toNSString() + target:self + action:@selector(itemAction:)] autorelease]; + i.collapsedRepresentation = view; + } + + if (view) + [view retain]; + [item retain]; + + [items setObject:item forKey:identifier]; + [commands setObject:[[QObjectPointer alloc] initWithQObject:action] + forKey:[NSValue valueWithPointer:view]]; + + if (!qobject_cast(action->data().value())) + [self changeItem:action]; + else + [self makeTouchBar]; +} + +- (void)changeItem:(QAction *)action +{ + NSString *identifier = identifierForAction(action).toNSString(); + + if (auto wa = qobject_cast(action)) { + + } else if (auto tb = qobject_cast(action->data().value())) { + NSCustomTouchBarItem *item = [items objectForKey:identifier]; + NSSegmentedControl *control = item.view; + control.segmentCount = tb->count(); + for (int i = 0; i < tb->count(); ++i) { + [control setLabel:tb->tabText(i).toNSString() forSegment:i]; + } + control.selectedSegment = tb->currentIndex(); + } else if (auto bb = qobject_cast(action->data().value())) { + // unfortunately we cannot modify a NSGroupTouchBarItem, so we + // have to recreate it from scratch :-( + int index = qMacTouchBar->actions().indexOf(action); + if (index == qMacTouchBar->actions().count() - 1) { + qMacTouchBar->removeAction(action); + qMacTouchBar->addAction(action); + } else { + QAction *before = qMacTouchBar->actions().at(index + 1); + qMacTouchBar->removeAction(action); + qMacTouchBar->insertAction(before, action); + } + } else if (action->isSeparator()) { + NSPopoverTouchBarItem *item = [items objectForKey:identifier]; + NSTextField *field = item.collapsedRepresentation; + field.stringValue = action->text().toNSString(); + } else { + NSPopoverTouchBarItem *item = [items objectForKey:identifier]; + NSButton *button = item.collapsedRepresentation; + button.imagePosition = NSImageLeft; + button.enabled = action->isEnabled(); + button.buttonType = + action->isCheckable() ? NSButtonTypePushOnPushOff : NSButtonTypeAccelerator; + button.bordered = action->isSeparator() && !action->text().isEmpty() ? NO : YES; + button.highlighted = !button.bordered; + button.state = action->isChecked(); + button.hidden = !action->isVisible(); + button.image = qt_mac_create_nsimage(action->icon()); + button.title = removeMnemonics(action->text()).toNSString(); + switch (qMacTouchBar->touchButtonStyle()) + { + case KDMacTouchBar::IconOnly: + button.imagePosition = NSImageOnly; + break; + case KDMacTouchBar::TextOnly: + button.imagePosition = NSNoImage; + break; + case KDMacTouchBar::TextBesideIcon: + button.imagePosition = NSImageLeft; + break; + } + item.showsCloseButton = action->menu() != nullptr; + if (action->menu()) { + item.popoverTouchBar = [[NSTouchBar alloc] init]; + item.popoverTouchBar.delegate = delegate; + // Add ordered items array + NSMutableArray *array = [[NSMutableArray alloc] init]; + for (auto action : action->menu()->actions()) { + if (action->isVisible()) { + [self addItem:action]; + if (action->isSeparator() && action->text().isEmpty()) + [array addObject:NSTouchBarItemIdentifierFixedSpaceLarge]; + else + [array addObject:identifierForAction(action).toNSString()]; + } + } + item.popoverTouchBar.defaultItemIdentifiers = array; + } + } + + [self makeTouchBar]; +} + +- (void)removeItem:(QAction *)action +{ + NSString *identifier = identifierForAction(action).toNSString(); + NSPopoverTouchBarItem *item = [items objectForKey:identifier]; + if (item == nil) + return; + QObjectPointer *command = [commands objectForKey:[NSValue valueWithPointer:item.view]]; + [commands removeObjectForKey:[NSValue valueWithPointer:item.view]]; + [items removeObjectForKey:identifier]; + [command release]; + + [self makeTouchBar]; +} + +- (void)buttonAction:(id)sender +{ + QObjectPointer *qobjectPointer = + (QObjectPointer *)[commands objectForKey:[NSValue valueWithPointer:sender]]; + // Missing entry in commands dict. Should not really happen, but does + if (!qobjectPointer) + return; + + QPushButton *button = qobject_cast(qobjectPointer.qobject); + if (!button) + return; + + button->click(); +} + +- (void)tabBarAction:(id)sender +{ + QObjectPointer *qobjectPointer = + (QObjectPointer *)[commands objectForKey:[NSValue valueWithPointer:sender]]; + // Missing entry in commands dict. Should not really happen, but does + if (!qobjectPointer) + return; + + // Check for deleted QObject + if (!qobjectPointer.qobject) + return; + + QAction *action = static_cast(qobjectPointer.qobject); + if (!action) + return; + QTabBar *tabBar = qobject_cast(action->data().value()); + if (!tabBar) + return; + + NSString *identifier = identifierForAction(action).toNSString(); + NSCustomTouchBarItem *item = [items objectForKey:identifier]; + NSSegmentedControl *control = item.view; + + tabBar->setCurrentIndex(control.selectedSegment); +} + +- (void)itemAction:(id)sender +{ + QObjectPointer *qobjectPointer = + (QObjectPointer *)[commands objectForKey:[NSValue valueWithPointer:sender]]; + // Missing entry in commands dict. Should not really happen, but does + if (!qobjectPointer) + return; + + // Check for deleted QObject + if (!qobjectPointer.qobject) + return; + + QAction *action = static_cast(qobjectPointer.qobject); + if (!action || action->isSeparator()) + return; + if (!action->isEnabled()) + return; + action->activate(QAction::Trigger); + + if (action->menu()) { + NSString *identifier = identifierForAction(action).toNSString(); + NSPopoverTouchBarItem *item = [items objectForKey:identifier]; + [item showPopover:item]; + openedPopOvers.push(item); + } else { + while (!openedPopOvers.isEmpty()) { + auto poppedItem = openedPopOvers.pop(); + [poppedItem dismissPopover:poppedItem]; + } + } +} + +- (void)clearItems +{ + [items removeAllObjects]; + [commands removeAllObjects]; +} + +- (NSTouchBar *)makeTouchBar +{ + // Create the touch bar with this instance as its delegate + if (touchBar == nil) { + touchBar = [[NSTouchBar alloc] init]; + touchBar.delegate = delegate; + } + + // Add ordered items array + NSMutableArray *array = [[NSMutableArray alloc] init]; + for (auto action : qMacTouchBar->actions()) { + if (action->isVisible()) { + if (action->isSeparator() && action->text().isEmpty()) + [array addObject:NSTouchBarItemIdentifierFixedSpaceLarge]; + else + [array addObject:identifierForAction(action).toNSString()]; + } + } + touchBar.defaultItemIdentifiers = array; + + return touchBar; +} + +- (void)installAsDelegateForWindow:(NSWindow *)window +{ + _qtDelegate = window.delegate; // Save current delegate for forwarding + window.delegate = self; +} + +- (void)installAsDelegateForApplication:(NSApplication *)application +{ + _qtDelegate = application.delegate; // Save current delegate for forwarding + application.delegate = self; +} + +- (BOOL)respondsToSelector:(SEL)aSelector +{ + // We want to forward to the qt delegate. Respond to selectors it + // responds to in addition to selectors this instance resonds to. + return [_qtDelegate respondsToSelector:aSelector] || [super respondsToSelector:aSelector]; +} + +- (void)forwardInvocation:(NSInvocation *)anInvocation +{ + // Forward to the existing delegate. This function is only called for selectors + // this instance does not responds to, which means that the qt delegate + // must respond to it (due to the respondsToSelector implementation above). + [anInvocation invokeWithTarget:_qtDelegate]; +} + +@end + +class KDMacTouchBar::Private +{ +public: + DynamicTouchBarProvider *touchBarProvider = nil; + QAction *principialAction = nullptr; + QAction *escapeAction = nullptr; + KDMacTouchBar::TouchButtonStyle touchButtonStyle = TextBesideIcon; + + static bool automaticallyCreateMessageBoxTouchBar; +}; + +bool KDMacTouchBar::Private::automaticallyCreateMessageBoxTouchBar = false; + +class AutomaticMessageBoxTouchBarStyle : public QProxyStyle +{ +public: + using QProxyStyle::QProxyStyle; + + void polish(QWidget *w) override + { + if (auto mb = qobject_cast(w)) { + if (KDMacTouchBar::isAutomacicallyCreatingMessageBoxTouchBar()) + new KDMacTouchBar(mb); + } + QProxyStyle::polish(w); + } +}; + +/*! + \class KDMacTouchBar + \brief The KDMacTouchBar class wraps the native NSTouchBar class. + + KDMacTouchBar provides a Qt-based API for NSTouchBar. The touchbar displays + a number of QActions. Each QAction can have a text and an icon. Alternatively, + the QActions might be separators (with or without text) or QWidgetActions. + + Add actions by calling addAction(). Alternatively, you can use one of the + convenience methods like addDialogButtonBox() or addTabBar() which provide + common use cases. + + If an action with a associated menu is added, its menu items are added as + sub-touchbar. Showing sub-menus of this menu is not supported, due to macOS + system restrictions. + + Usage: (QtWidgets) + \code + QMainWindow *mw = ...; + KDMacTouchBar *touchBar = new KDMacTouchBar(mw); + touchBar->addAction(actionNewFile); + touchBar->addSeparator(); + touchBar->addAction(actionSaveFile); + \endcode +*/ + +/*! +\enum KDMacTouchBar::TouchButtonStyle +\value IconOnly Only display the icon. +\value TextOnly Only display the text. +\value TextBesideIcon The text appears beside the icon. +*/ + +/*! + Constructs a KDMacTouchBar for the window of \a parent. If \a parent is + nullptr, the KDMacTouchBar is shown as soon as this QApplication has focus, + if no other window with an own KDMacTouchBar has focus. +*/ +KDMacTouchBar::KDMacTouchBar(QWidget *parent) + : QWidget(parent) + , d(new Private) +{ + d->touchBarProvider = [[DynamicTouchBarProvider alloc] initWithKDMacTouchBar:this]; + if (parent) { + NSView *view = reinterpret_cast(parent->window()->winId()); + [d->touchBarProvider installAsDelegateForWindow:[view window]]; + } else { + [d->touchBarProvider installAsDelegateForApplication:[NSApplication sharedApplication]]; + } +} + +/*! + Constructs a KDMacTouchBar containing the QDialogButtonBox from inside of + \a messageBox. +*/ +KDMacTouchBar::KDMacTouchBar(QMessageBox *messageBox) + : QWidget( messageBox) + , d(new Private) +{ + d->touchBarProvider = [[DynamicTouchBarProvider alloc] initWithKDMacTouchBar:this]; + NSView *view = reinterpret_cast(messageBox->window()->winId()); + [d->touchBarProvider installAsDelegateForWindow:[view window]]; + setPrincipialAction(addMessageBox(messageBox)); +} + +/*! + Destroys the touch bar. +*/ +KDMacTouchBar::~KDMacTouchBar() +{ + [d->touchBarProvider release]; + delete d; +} + +/*! + This static convenience method controls, whether KDMacTouchBar will + automatically create a touchbar for QMessageBox instances. The + created touchbar contains the buttons of the message box. + This enables to use the static QMessageBox method and still having + a touchbar for them. + + \note When you enable this setting for the first time, KDMacTouchBar will + install a QProxyStyle into the QApplication object to be able to create + the KDMacTouchBar in its polish method. The installed QProxyStyle will + use the existing application style as base style. +*/ +void KDMacTouchBar::setAutomaticallyCreateMessageBoxTouchBar(bool automatic) +{ + static AutomaticMessageBoxTouchBarStyle *touchStyle = nullptr; + if (automatic && !touchStyle) { + qApp->setStyle(touchStyle = new AutomaticMessageBoxTouchBarStyle(qApp->style())); + } + Private::automaticallyCreateMessageBoxTouchBar = automatic; +} + +/*! + Returns whether KDMacTouchBar automatically creates a touchbar for + QMessageBox instances. +*/ +bool KDMacTouchBar::isAutomacicallyCreatingMessageBoxTouchBar() +{ + return Private::automaticallyCreateMessageBoxTouchBar; +} + +/*! + Adds a separator item to the end of the touch bar. +*/ +QAction *KDMacTouchBar::addSeparator() +{ + auto action = new QAction(this); + action->setSeparator(true); + addAction(action); + return action; +} + +/*! \reimp */ +bool KDMacTouchBar::event(QEvent *event) +{ + switch (event->type()) { + case QEvent::ActionAdded: + [d->touchBarProvider addItem:static_cast(event)->action()]; + break; + case QEvent::ActionChanged: + [d->touchBarProvider changeItem:static_cast(event)->action()]; + break; + case QEvent::ActionRemoved: + [d->touchBarProvider removeItem:static_cast(event)->action()]; + break; + default: + break; + } + + return QWidget::event(event); +} + +/*! + Adds a button group controlling a \a tabBar item to the end of the touch bar. +*/ +QAction *KDMacTouchBar::addTabBar(QTabBar *tabBar) +{ + auto a = new TabBarAction(tabBar, this); + addAction(a); + return a; +} + +/*! + Removes \a tabBar from the touch bar. +*/ +void KDMacTouchBar::removeTabBar(QTabBar *tabBar) +{ + for (auto a : actions()) { + if (a->data().value() == tabBar) { + removeAction(a); + return; + } + } +} + +/*! + Adds the QDialogButtonBox of \a messageBox to the end of the touch bar. +*/ +QAction *KDMacTouchBar::addMessageBox(QMessageBox *messageBox) +{ + return addDialogButtonBox(messageBox->findChild(QStringLiteral("qt_msgbox_buttonbox"))); +} + +/*! + Removes the QDialogButtonBox of \a messageBox from the touch bar. +*/ +void KDMacTouchBar::removeMessageBox(QMessageBox *messageBox) +{ + return removeDialogButtonBox(messageBox->findChild(QStringLiteral("qt_msgbox_buttonbox"))); +} + +/*! + Adds a button group controlling \a buttonBox to the end of the touch bar. +*/ +QAction *KDMacTouchBar::addDialogButtonBox(QDialogButtonBox *buttonBox) +{ + auto a = new ButtonBoxAction(buttonBox, this); + addAction(a); + return a; +} + +/*! + Removes the \a buttonBox from the touch bar. +*/ +void KDMacTouchBar::removeDialogButtonBox(QDialogButtonBox *buttonBox) +{ + for (auto a : actions()) { + if (a->data().value() == buttonBox) { + removeAction(a); + return; + } + } +} + +/*! + \property KDMacTouchBar::principialAction + \brief the principial action of the touch bar + + The principial action of the touch bar is the QAction you want the system + to center in the touch bar. + + You need to add the action to the touch bar before you can set it as principial + action. +*/ +void KDMacTouchBar::setPrincipialAction(QAction *action) +{ + d->principialAction = action; + d->touchBarProvider.touchBar.principalItemIdentifier = action ? identifierForAction(action).toNSString() : nil; +} + +QAction *KDMacTouchBar::principialAction() const +{ + return d->principialAction; +} + +/*! + \property KDMacTouchBar::escapeAction + \brief the action used as system escape key action + + By setting a QAction as escapeAction, it is possible to replace the system + escape key with a random action. + + You don't need to add the action to the touch bar before you can set it as + escape action. +*/ +void KDMacTouchBar::setEscapeAction(QAction *action) +{ + if (d->escapeAction == action) + return; + if (d->escapeAction) + [d->touchBarProvider removeItem:d->escapeAction]; + d->escapeAction = action; + if (d->escapeAction) { + [d->touchBarProvider addItem:d->escapeAction]; + d->touchBarProvider.touchBar.escapeKeyReplacementItemIdentifier = + identifierForAction(action).toNSString(); + } else + d->touchBarProvider.touchBar.escapeKeyReplacementItemIdentifier = nil; +} + +QAction *KDMacTouchBar::escapeAction() const +{ + return d->escapeAction; +} + +/*! + \property KDMacTouchBar::touchButtonStyle + This property holds the style of touch bar buttons. + + This property defines the style of all touch buttons that are added as QActions. + Added tab widgets, dialog button boxes, message boxes or QWidgetActions won't + follow this style. + + The default is KDMacTouchBar::TextBesideIcon + */ +void KDMacTouchBar::setTouchButtonStyle(TouchButtonStyle touchButtonStyle) +{ + if (d->touchButtonStyle == touchButtonStyle) + return; + + d->touchButtonStyle = touchButtonStyle; + + for (auto* action : actions()) + [d->touchBarProvider changeItem:action]; + if (d->escapeAction) + [d->touchBarProvider changeItem:d->escapeAction]; +} + +KDMacTouchBar::TouchButtonStyle KDMacTouchBar::touchButtonStyle() const +{ + return d->touchButtonStyle; +} + +/*! + Removes all actions from the touch bar. Removes even the escapeAction. + The principialAction is cleared. +*/ +void KDMacTouchBar::clear() +{ + setEscapeAction(nullptr); + setPrincipialAction(nullptr); + for (auto* action : actions()) + removeAction(action); +} + +QT_END_NAMESPACE diff --git a/src/kdmactouchbar/src/kdmactouchbar_global.h b/src/kdmactouchbar/src/kdmactouchbar_global.h new file mode 100644 index 0000000..1a60669 --- /dev/null +++ b/src/kdmactouchbar/src/kdmactouchbar_global.h @@ -0,0 +1,31 @@ +/**************************************************************************** +** Copyright (C) 2019-2020 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com. +** All rights reserved. +** +** This file is part of the KD MacTouchBar library. +** +** This file may be distributed and/or modified under the terms of the +** GNU Lesser General Public License version 3 as published by the +** Free Software Foundation and appearing in the file LICENSE.LGPL.txt included. +** +** You may even contact us at info@kdab.com for different licensing options. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** Contact info@kdab.com if any conditions of this licensing are not +** clear to you. +** +**********************************************************************/ +#ifndef KDMACTOUCHBAR_GLOBAL_H +#define KDMACTOUCHBAR_GLOBAL_H + +#include + +#ifdef KDMACTOUCHBAR_BUILD_KDMACTOUCHBAR_LIB +# define KDMACTOUCHBAR_EXPORT Q_DECL_EXPORT +#else +# define KDMACTOUCHBAR_EXPORT Q_DECL_IMPORT +#endif + +#endif /* KDMACTOUCHBAR_GLOBAL_H */ diff --git a/src/kdmactouchbar/src/src.pro b/src/kdmactouchbar/src/src.pro new file mode 100644 index 0000000..50fce58 --- /dev/null +++ b/src/kdmactouchbar/src/src.pro @@ -0,0 +1,28 @@ +TEMPLATE = lib +TARGET = KDMacTouchBar +QT += widgets + +include($${TOP_SOURCE_DIR}/kdmactouchbar.pri) + +CONFIG += lib_bundle + +DESTDIR = ../lib + +SOURCES = kdmactouchbar.mm + +HEADERS = kdmactouchbar.h kdmactouchbar_global.h + +LIBS += -framework Cocoa +QMAKE_LFLAGS_SONAME = -Wl,-install_name,@rpath/ + +DEFINES += KDMACTOUCHBAR_BUILD_KDMACTOUCHBAR_LIB QT_NO_CAST_TO_ASCII QT_ASCII_CAST_WARNING + +target.path = "$${INSTALL_PREFIX}/lib" + +INSTALLS += target + +FRAMEWORK_HEADERS.version = Versions +FRAMEWORK_HEADERS.files = $$HEADERS +FRAMEWORK_HEADERS.path = Headers +QMAKE_BUNDLE_DATA += FRAMEWORK_HEADERS +QMAKE_TARGET_BUNDLE_PREFIX = "com.kdab" diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 66ae668..249455a 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -9,6 +9,10 @@ #include #include +#ifdef Q_OS_MAC +#include "kdmactouchbar/src/kdmactouchbar.h" +#endif + #include "mainwindow.h" #include "widgets/ccswidget.h" #include "widgets/redditwidget.h" @@ -355,6 +359,15 @@ MainWindow::MainWindow(AppContext *ctx, QWidget *parent) : this->initMain(); this->initWidgets(); this->initMenu(); + +#ifdef Q_OS_MAC + KDMacTouchBar *touchBar = new KDMacTouchBar(this); + touchBar->addAction(ui->actionCalculator); + touchBar->addAction(ui->actionKeys); + touchBar->addAction(ui->actionDonate_to_Feather); + touchBar->addAction(ui->actionReport_bug); + touchBar->addAction(ui->actionSettings); +#endif } void MainWindow::initMain() {