/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #import "mac_touchbar.h" #import #include "apiwrap.h" #include "main/main_session.h" #include "api/api_sending.h" #include "boxes/confirm_box.h" #include "chat_helpers/emoji_list_widget.h" #include "core/application.h" #include "core/sandbox.h" #include "data/data_document.h" #include "data/data_file_origin.h" #include "data/data_folder.h" #include "data/data_peer_values.h" #include "data/data_session.h" #include "dialogs/dialogs_layout.h" #include "emoji_config.h" #include "history/history.h" #include "lang/lang_keys.h" #include "mainwidget.h" #include "mainwindow.h" #include "observer_peer.h" #include "platform/mac/mac_utilities.h" #include "stickers.h" #include "styles/style_dialogs.h" #include "styles/style_media_player.h" #include "styles/style_settings.h" #include "window/themes/window_theme.h" #include "window/window_session_controller.h" #include "ui/empty_userpic.h" #include "ui/widgets/input_fields.h" NSImage *qt_mac_create_nsimage(const QPixmap &pm); namespace { //https://developer.apple.com/design/human-interface-guidelines/macos/touch-bar/touch-bar-icons-and-images/ constexpr auto kIdealIconSize = 36; constexpr auto kMaximumIconSize = 44; constexpr auto kCircleDiameter = 30; constexpr auto kPinnedButtonsSpace = 30; constexpr auto kCommandPlayPause = 0x002; constexpr auto kCommandPlaylistPrevious = 0x003; constexpr auto kCommandPlaylistNext = 0x004; constexpr auto kCommandClosePlayer = 0x005; constexpr auto kCommandBold = 0x010; constexpr auto kCommandItalic = 0x011; constexpr auto kCommandUnderline = 0x012; constexpr auto kCommandStrikeOut = 0x013; constexpr auto kCommandMonospace = 0x014; constexpr auto kCommandClear = 0x015; constexpr auto kCommandLink = 0x016; constexpr auto kCommandScrubberStickers = 0x020; constexpr auto kCommandScrubberEmoji = 0x021; constexpr auto kMs = 1000; constexpr auto kSongType = AudioMsgId::Type::Song; constexpr auto kSavedMessagesId = 0; constexpr auto kArchiveId = -1; constexpr auto kMaxStickerSets = 5; NSString *const kTypePinned = @"pinned"; NSString *const kTypeSlider = @"slider"; NSString *const kTypeButton = @"button"; NSString *const kTypeText = @"text"; NSString *const kTypeTextButton = @"textButton"; NSString *const kTypeScrubber = @"scrubber"; NSString *const kTypePicker = @"picker"; NSString *const kTypeFormatter = @"formatter"; NSString *const kTypeFormatterSegment = @"formatterSegment"; const NSString *kCustomizationIdPlayer = @"telegram.touchbar"; const NSString *kCustomizationIdMain = @"telegram.touchbarMain"; const NSTouchBarItemIdentifier kSavedMessagesItemIdentifier = [NSString stringWithFormat:@"%@.savedMessages", kCustomizationIdMain]; const NSTouchBarItemIdentifier kArchiveFolderItemIdentifier = [NSString stringWithFormat:@"%@.archiveFolder", kCustomizationIdMain]; const NSTouchBarItemIdentifier kPinnedPanelItemIdentifier = [NSString stringWithFormat:@"%@.pinnedPanel", kCustomizationIdMain]; const NSTouchBarItemIdentifier kSeekBarItemIdentifier = [NSString stringWithFormat:@"%@.seekbar", kCustomizationIdPlayer]; const NSTouchBarItemIdentifier kPlayItemIdentifier = [NSString stringWithFormat:@"%@.play", kCustomizationIdPlayer]; const NSTouchBarItemIdentifier kNextItemIdentifier = [NSString stringWithFormat:@"%@.nextItem", kCustomizationIdPlayer]; const NSTouchBarItemIdentifier kPreviousItemIdentifier = [NSString stringWithFormat:@"%@.previousItem", kCustomizationIdPlayer]; const NSTouchBarItemIdentifier kCommandClosePlayerItemIdentifier = [NSString stringWithFormat:@"%@.closePlayer", kCustomizationIdPlayer]; const NSTouchBarItemIdentifier kCurrentPositionItemIdentifier = [NSString stringWithFormat:@"%@.currentPosition", kCustomizationIdPlayer]; const NSTouchBarItemIdentifier kPopoverInputItemIdentifier = [NSString stringWithFormat:@"%@.popoverInput", kCustomizationIdMain]; const NSTouchBarItemIdentifier kPopoverInputFormatterItemIdentifier = [NSString stringWithFormat:@"%@.popoverInputFormatter", kCustomizationIdMain]; const NSTouchBarItemIdentifier kPickerPopoverItemIdentifier = [NSString stringWithFormat:@"%@.pickerButtons", kCustomizationIdMain]; const NSTouchBarItemIdentifier kScrubberStickersItemIdentifier = [NSString stringWithFormat:@"%@.scrubberStickers", kCustomizationIdMain]; const NSTouchBarItemIdentifier kStickerItemIdentifier = [NSString stringWithFormat:@"%@.stickerItem", kCustomizationIdMain]; const NSTouchBarItemIdentifier kScrubberEmojiItemIdentifier = [NSString stringWithFormat:@"%@.scrubberEmoji", kCustomizationIdMain]; const NSTouchBarItemIdentifier kEmojiItemIdentifier = [NSString stringWithFormat:@"%@.emojiItem", kCustomizationIdMain]; const NSTouchBarItemIdentifier kPickerTitleItemIdentifier = [NSString stringWithFormat:@"%@.pickerTitleItem", kCustomizationIdMain]; struct PickerScrubberItem { PickerScrubberItem(QString title) : title(title) { } PickerScrubberItem(DocumentData* document) : document(document) { } PickerScrubberItem(EmojiPtr emoji) : emoji(emoji) { } QString title = QString(); DocumentData* document = nullptr; EmojiPtr emoji = nullptr; }; enum ScrubberItemType { Emoji, Sticker, }; using Platform::Q2NSString; NSImage *CreateNSImageFromStyleIcon(const style::icon &icon, int size = kIdealIconSize) { const auto instance = icon.instance(QColor(255, 255, 255, 255), 100); auto pixmap = QPixmap::fromImage(instance); pixmap.setDevicePixelRatio(cRetinaFactor()); NSImage *image = [qt_mac_create_nsimage(pixmap) autorelease]; [image setSize:NSMakeSize(size, size)]; return image; } NSImage *CreateNSImageFromEmoji(EmojiPtr emoji) { const auto s = kIdealIconSize * cIntRetinaFactor(); auto pixmap = QPixmap(s, s); pixmap.setDevicePixelRatio(cRetinaFactor()); pixmap.fill(Qt::black); Painter paint(&pixmap); PainterHighQualityEnabler hq(paint); #ifndef OS_MAC_OLD Ui::Emoji::Draw( paint, std::move(emoji), Ui::Emoji::GetSizeTouchbar(), 0, 0); #endif // OS_MAC_OLD return [qt_mac_create_nsimage(pixmap) autorelease]; } int WidthFromString(NSString *s) { return (int)ceil( [[NSTextField labelWithString:s] frame].size.width) * 1.2; } inline bool IsSticker(ScrubberItemType type) { return type == ScrubberItemType::Sticker; } inline bool CurrentSongExists() { return Media::Player::instance()->current(kSongType).audio() != nullptr; } inline bool UseEmptyUserpic(PeerData *peer) { return (peer && (peer->useEmptyUserpic() || peer->isSelf())); } inline bool IsSelfPeer(PeerData *peer) { return (peer && peer->isSelf()); } inline int UnreadCount(not_null peer) { if (const auto history = peer->owner().historyLoaded(peer)) { return history->unreadCountForBadge(); } return 0; } inline auto GetActiveChat() { if (const auto window = App::wnd()) { if (const auto controller = window->sessionController()) { return controller->activeChatCurrent(); } } return Dialogs::Key(); } inline bool CanWriteToActiveChat() { if (const auto history = GetActiveChat().history()) { return history->peer->canWrite(); } return false; } inline std::optional RestrictionToSendStickers() { if (const auto peer = GetActiveChat().peer()) { return Data::RestrictionError( peer, ChatRestriction::f_send_stickers); } return std::nullopt; } QString TitleRecentlyUsed() { const auto &sets = Auth().data().stickerSets(); const auto it = sets.constFind(Stickers::CloudRecentSetId); if (it != sets.cend()) { return it->title; } return tr::lng_recent_stickers(tr::now); } NSString *FormatTime(int time) { const auto seconds = time % 60; const auto minutes = (time / 60) % 60; const auto hours = time / (60 * 60); NSString *stringTime = (hours > 0) ? [NSString stringWithFormat:@"%d:", hours] : @""; stringTime = [NSString stringWithFormat:@"%@%02d:", (stringTime.length > 0 || minutes > 9) ? stringTime : @"", minutes]; stringTime = [NSString stringWithFormat:@"%@%02d", stringTime, seconds]; return stringTime; } bool PaintUnreadBadge(Painter &p, PeerData *peer) { const auto history = peer->owner().history(peer->id); const auto count = history->unreadCountForBadge(); if (!count) { return false; } const auto unread = history->unreadMark() ? QString() : QString::number(count); Dialogs::Layout::UnreadBadgeStyle unreadSt; unreadSt.sizeId = Dialogs::Layout::UnreadBadgeInTouchBar; unreadSt.muted = history->mute(); // Use constant values to draw badge regardless of cConfigScale(). unreadSt.size = 19; unreadSt.padding = 5; unreadSt.font = style::font( 12, unreadSt.font->flags(), unreadSt.font->family()); Dialogs::Layout::paintUnreadCount(p, unread, kIdealIconSize, kIdealIconSize - unreadSt.size, unreadSt, nullptr, 2); return true; } void PaintOnlineCircle(Painter &p) { PainterHighQualityEnabler hq(p); // Use constant values to draw online badge regardless of cConfigScale(). const auto size = 8; const auto paddingSize = 4; const auto circleSize = size + paddingSize; const auto offset = size + paddingSize / 2; p.setPen(Qt::NoPen); p.setBrush(Qt::black); p.drawEllipse( kIdealIconSize - circleSize, kIdealIconSize - circleSize, circleSize, circleSize); p.setBrush(st::dialogsOnlineBadgeFg); p.drawEllipse( kIdealIconSize - offset, kIdealIconSize - offset, size, size); } void SendKeyEvent(int command) { QWidget *focused = QApplication::focusWidget(); if (!qobject_cast(focused)) { return; } auto key = 0; auto modifier = Qt::KeyboardModifiers(0) | Qt::ControlModifier; switch (command) { case kCommandBold: key = Qt::Key_B; break; case kCommandItalic: key = Qt::Key_I; break; case kCommandMonospace: key = Qt::Key_M; modifier |= Qt::ShiftModifier; break; case kCommandClear: key = Qt::Key_N; modifier |= Qt::ShiftModifier; break; case kCommandLink: key = Qt::Key_K; break; case kCommandUnderline: key = Qt::Key_U; break; case kCommandStrikeOut: key = Qt::Key_X; modifier |= Qt::ShiftModifier; break; } QApplication::postEvent(focused, new QKeyEvent(QEvent::KeyPress, key, modifier)); QApplication::postEvent(focused, new QKeyEvent(QEvent::KeyRelease, key, modifier)); } void AppendStickerSet(std::vector &to, uint64 setId) { auto &sets = Auth().data().stickerSets(); auto it = sets.constFind(setId); if (it == sets.cend() || it->stickers.isEmpty()) { return; } if (it->flags & MTPDstickerSet::Flag::f_archived) { return; } if (!(it->flags & MTPDstickerSet::Flag::f_installed_date)) { return; } to.emplace_back(PickerScrubberItem(it->title.isEmpty() ? it->shortName : it->title)); for (const auto sticker : it->stickers) { to.emplace_back(PickerScrubberItem(sticker)); } } void AppendRecentStickers(std::vector &to) { const auto &sets = Auth().data().stickerSets(); const auto cloudIt = sets.constFind(Stickers::CloudRecentSetId); const auto favedIt = sets.constFind(Stickers::FavedSetId); const auto cloudCount = (cloudIt != sets.cend()) ? cloudIt->stickers.size() : 0; if (cloudCount > 0) { to.emplace_back(PickerScrubberItem(cloudIt->title)); auto count = 0; for (const auto document : cloudIt->stickers) { if (Stickers::IsFaved(document)) { continue; } to.emplace_back(PickerScrubberItem(document)); } } for (const auto recent : Stickers::GetRecentPack()) { to.emplace_back(PickerScrubberItem(recent.first)); } } void AppendFavedStickers(std::vector &to) { const auto &sets = Auth().data().stickerSets(); const auto it = sets.constFind(Stickers::FavedSetId); const auto count = (it != sets.cend()) ? it->stickers.size() : 0; if (!count) { return; } to.emplace_back(PickerScrubberItem( tr::lng_mac_touchbar_favorite_stickers(tr::now))); for (const auto document : it->stickers) { to.emplace_back(PickerScrubberItem(document)); } } void AppendEmojiPacks(std::vector &to) { for (auto i = 0; i != ChatHelpers::kEmojiSectionCount; ++i) { const auto section = Ui::Emoji::GetSection( static_cast(i)); const auto title = i ? Ui::Emoji::CategoryTitle(i)(tr::now) : TitleRecentlyUsed(); to.emplace_back(title); for (const auto &emoji : section) { to.emplace_back(PickerScrubberItem(emoji)); } } } } // namespace @interface PinButton : NSButton @end // @interface PinButton @implementation PinButton { int _startPosition; int _tempIndex; bool _orderChanged; } - (void)touchesBeganWithEvent:(NSEvent *)event { if ([event.allTouches allObjects].count > 1) { return; } _orderChanged = false; _tempIndex = self.tag - 1; _startPosition = [self getTouchX:event]; [super touchesBeganWithEvent:event]; } - (void)touchesMovedWithEvent:(NSEvent *)event { if (self.tag <= kSavedMessagesId) { return; } if ([event.allTouches allObjects].count > 1) { return; } const auto currentPosition = [self getTouchX:event]; const auto step = kPinnedButtonsSpace + kCircleDiameter; if (std::abs(_startPosition - currentPosition) > step) { const auto delta = (currentPosition > _startPosition) ? 1 : -1; const auto newIndex = _tempIndex + delta; const auto &order = Auth().data().pinnedChatsOrder(nullptr); // In case the order has been changed from another device // while the user is dragging the dialog. if (_tempIndex >= order.size()) { return; } if (newIndex >= 0 && newIndex < order.size()) { Auth().data().reorderTwoPinnedChats( order.at(_tempIndex).history(), order.at(newIndex).history()); _tempIndex = newIndex; _startPosition = currentPosition; _orderChanged = true; } } } - (void)touchesEndedWithEvent:(NSEvent *)event { if (_orderChanged) { Auth().api().savePinnedOrder(nullptr); } [super touchesEndedWithEvent:event]; } - (int)getTouchX:(NSEvent *)e { return [[[e.allTouches allObjects] objectAtIndex:0] locationInView:self].x; } @end // @implementation PinButton @interface PinnedDialogButton : NSCustomTouchBarItem @property(nonatomic, assign) int number; @property(nonatomic, assign) PeerData *peer; @property(nonatomic, assign) bool isDeletedFromView; @property(nonatomic, assign) QPixmap userpic; - (id) init:(int)num; - (void)buttonActionPin:(NSButton *)sender; - (void)updateUserpic; @end // @interface PinnedDialogButton @implementation PinnedDialogButton { rpl::lifetime _lifetime; rpl::lifetime _peerChangedLifetime; bool isWaitingUserpicLoad; } - (id) init:(int)num { if (num == kSavedMessagesId) { self = [super initWithIdentifier:kSavedMessagesItemIdentifier]; isWaitingUserpicLoad = false; self.customizationLabel = [NSString stringWithFormat:@"Pinned Dialog %d", num]; } else if (num == kArchiveId) { self = [super initWithIdentifier:kArchiveFolderItemIdentifier]; isWaitingUserpicLoad = false; self.customizationLabel = @"Archive Folder"; } else { NSString *identifier = [NSString stringWithFormat:@"%@.pinnedDialog%d", kCustomizationIdMain, num]; self = [super initWithIdentifier:identifier]; isWaitingUserpicLoad = true; self.customizationLabel = @"Saved Messages"; } if (!self) { return nil; } self.number = num; PinButton *button = [[PinButton alloc] initWithFrame:NSZeroRect]; NSButtonCell *cell = [[NSButtonCell alloc] init]; [cell setBezelStyle:NSBezelStyleCircular]; button.cell = cell; button.tag = num; button.target = self; button.action = @selector(buttonActionPin:); self.view = button; base::ObservableViewer( *Window::Theme::Background() ) | rpl::filter([](const Window::Theme::BackgroundUpdate &update) { return update.paletteChanged(); }) | rpl::start_with_next([=] { crl::on_main([=] { if (_number <= kSavedMessagesId || UseEmptyUserpic(_peer)) { [self updateUserpic]; } else if (_peer && (UnreadCount(_peer) || Data::IsPeerAnOnlineUser(_peer))) { [self updateBadge]; } }); }, _lifetime); if (num <= kSavedMessagesId) { [self updateUserpic]; return self; } base::ObservableViewer( Auth().downloaderTaskFinished() ) | rpl::start_with_next([=] { if (isWaitingUserpicLoad) { [self updateUserpic]; } }, _lifetime); return self; } // Setter of peer. - (void) setPeer:(PeerData *)newPeer { if (_peer == newPeer) { return; } _peerChangedLifetime.destroy(); _peer = newPeer; if (!_peer || IsSelfPeer(_peer)) { return; } Notify::PeerUpdateViewer( _peer, Notify::PeerUpdate::Flag::PhotoChanged ) | rpl::start_with_next([=] { isWaitingUserpicLoad = true; [self updateUserpic]; }, _peerChangedLifetime); Notify::PeerUpdateViewer( _peer, Notify::PeerUpdate::Flag::UnreadViewChanged ) | rpl::start_with_next([=] { [self updateBadge]; }, _peerChangedLifetime); Notify::PeerUpdateViewer( _peer, Notify::PeerUpdate::Flag::UserOnlineChanged ) | rpl::filter([=] { return UnreadCount(_peer) == 0; }) | rpl::start_with_next([=] { [self updateBadge]; }, _peerChangedLifetime); } - (void) buttonActionPin:(NSButton *)sender { const auto openFolder = [=] { if (!App::wnd()) { return; } if (const auto folder = Auth().data().folderLoaded(Data::Folder::kId)) { App::wnd()->sessionController()->openFolder(folder); } }; Core::Sandbox::Instance().customEnterFromEventLoop([=] { self.number == kArchiveId ? openFolder() : App::main()->choosePeer(self.number == kSavedMessagesId ? Auth().userPeerId() : self.peer->id, ShowAtUnreadMsgId); }); } - (void) updateUserpic { // Don't draw self userpic if we pin Saved Messages. if (self.number <= kSavedMessagesId || IsSelfPeer(_peer)) { const auto s = kIdealIconSize * cIntRetinaFactor(); _userpic = QPixmap(s, s); Painter paint(&_userpic); paint.fillRect(QRectF(0, 0, s, s), Qt::black); if (self.number != kArchiveId) { Ui::EmptyUserpic::PaintSavedMessages(paint, 0, 0, s, s); } else if (const auto folder = Auth().data().folderLoaded(Data::Folder::kId)) { folder->paintUserpic(paint, 0, 0, s); } _userpic.setDevicePixelRatio(cRetinaFactor()); [self updateImage:_userpic]; return; } if (!self.peer) { return; } isWaitingUserpicLoad = !self.peer->userpicLoaded(); _userpic = self.peer->genUserpic(kIdealIconSize); _userpic.setDevicePixelRatio(cRetinaFactor()); [self updateBadge]; } - (void) updateBadge { if (IsSelfPeer(_peer)) { return; } // Draw unread or online badge. auto pixmap = App::pixmapFromImageInPlace(_userpic.toImage()); Painter p(&pixmap); if (!PaintUnreadBadge(p, _peer) && Data::IsPeerAnOnlineUser(_peer)) { PaintOnlineCircle(p); } [self updateImage:pixmap]; } - (void) updateImage:(QPixmap)pixmap { NSButton *button = self.view; NSImage *image = [qt_mac_create_nsimage(pixmap) autorelease]; [image setSize:NSMakeSize(kCircleDiameter, kCircleDiameter)]; [button.cell setImage:image]; } @end // @implementation PinnedDialogButton @interface PickerScrubberItemView : NSScrubberItemView @property (strong) NSImageView *imageView; @end // @interface PickerScrubberItemView @implementation PickerScrubberItemView { rpl::lifetime _lifetime; Data::FileOrigin _origin; QSize _dimensions; Image *_image; } - (instancetype)initWithFrame:(NSRect)frameRect { self = [super initWithFrame:frameRect]; if (!self) { return self; } _imageView = [NSImageView imageViewWithImage: [[NSImage alloc] initWithSize:frameRect.size]]; [self.imageView setAutoresizingMask: (NSAutoresizingMaskOptions)(NSViewWidthSizable | NSViewHeightSizable)]; [self addSubview:self.imageView]; return self; } - (void)addDocument:(DocumentData *)document { if (!document->sticker()) { return; } _image = document->getStickerSmall(); if (!_image) { return; } _dimensions = document->dimensions; _origin = document->stickerSetOrigin(); _image->load(_origin); if (_image->loaded()) { [self updateImage]; return; } base::ObservableViewer( Auth().downloaderTaskFinished() ) | rpl::start_with_next([=] { if (_image->loaded()) { [self updateImage]; _lifetime.destroy(); } }, _lifetime); } - (void)updateImage { const auto size = _dimensions .scaled(kCircleDiameter, kCircleDiameter, Qt::KeepAspectRatio); _imageView.image = [qt_mac_create_nsimage( _image->pixSingle( _origin, size.width(), size.height(), kCircleDiameter, kCircleDiameter, ImageRoundRadius::None)) autorelease]; } @end // @implementation PickerScrubberItemView @interface PickerCustomTouchBarItem: NSCustomTouchBarItem @end // @interface PickerCustomTouchBarItem #pragma mark - @implementation PickerCustomTouchBarItem { std::vector _stickers; NSPopoverTouchBarItem *_parentPopover; std::unique_ptr _previewTimer; int _highlightedIndex; bool _previewShown; } - (id) init:(ScrubberItemType)type popover:(NSPopoverTouchBarItem *)popover { self = [super initWithIdentifier:IsSticker(type) ? kScrubberStickersItemIdentifier : kScrubberEmojiItemIdentifier]; if (!self) { return self; } _parentPopover = popover; IsSticker(type) ? [self updateStickers] : [self updateEmoji]; NSScrubber *scrubber = [[[NSScrubber alloc] initWithFrame:NSZeroRect] autorelease]; NSScrubberFlowLayout *layout = [[[NSScrubberFlowLayout alloc] init] autorelease]; layout.itemSpacing = 10; scrubber.scrubberLayout = layout; scrubber.mode = NSScrubberModeFree; scrubber.delegate = self; scrubber.dataSource = self; scrubber.floatsSelectionViews = true; scrubber.showsAdditionalContentIndicators = true; scrubber.itemAlignment = NSScrubberAlignmentCenter; [scrubber registerClass:[PickerScrubberItemView class] forItemIdentifier:kStickerItemIdentifier]; [scrubber registerClass:[NSScrubberTextItemView class] forItemIdentifier:kPickerTitleItemIdentifier]; [scrubber registerClass:[NSScrubberImageItemView class] forItemIdentifier:kEmojiItemIdentifier]; _previewShown = false; _highlightedIndex = 0; _previewTimer = !IsSticker(type) ? nullptr : std::make_unique([=] { [self showPreview]; }); self.view = scrubber; return self; } - (void)encodeWithCoder:(nonnull NSCoder *)aCoder { // Has not been implemented. } #pragma mark - NSScrubberDelegate - (NSInteger)numberOfItemsForScrubber:(NSScrubber *)scrubber { return _stickers.size(); } - (NSScrubberItemView *)scrubber:(NSScrubber *)scrubber viewForItemAtIndex:(NSInteger)index { const auto item = _stickers[index]; if (const auto document = item.document) { PickerScrubberItemView *itemView = [scrubber makeItemWithIdentifier:kStickerItemIdentifier owner:nil]; [itemView addDocument:document]; return itemView; } else if (const auto emoji = item.emoji) { NSScrubberImageItemView *itemView = [scrubber makeItemWithIdentifier:kEmojiItemIdentifier owner:nil]; itemView.imageView.image = CreateNSImageFromEmoji(emoji); return itemView; } else { NSScrubberTextItemView *itemView = [scrubber makeItemWithIdentifier:kPickerTitleItemIdentifier owner:nil]; itemView.textField.stringValue = Q2NSString(item.title); return itemView; } } - (NSSize)scrubber:(NSScrubber *)scrubber layout:(NSScrubberFlowLayout *)layout sizeForItemAtIndex:(NSInteger)index { if (const auto t = _stickers[index].title; !t.isEmpty()) { return NSMakeSize( WidthFromString(Q2NSString(t)) + kCircleDiameter, kCircleDiameter); } return NSMakeSize(kCircleDiameter, kCircleDiameter); } - (void)scrubber:(NSScrubber *)scrubber didSelectItemAtIndex:(NSInteger)index { if (!CanWriteToActiveChat()) { return; } scrubber.selectedIndex = -1; if (_previewShown && [self hidePreview]) { return; } if (_previewTimer) { _previewTimer->cancel(); } const auto chat = GetActiveChat(); const auto callback = [&]() -> bool { if (const auto document = _stickers[index].document) { if (const auto error = RestrictionToSendStickers()) { Ui::show(Box(*error)); } Api::SendExistingDocument( Api::MessageToSend(chat.history()), document); return true; } else if (const auto emoji = _stickers[index].emoji) { if (const auto inputField = qobject_cast( QApplication::focusWidget())) { Ui::InsertEmojiAtCursor(inputField->textCursor(), emoji); Ui::Emoji::AddRecent(emoji); return true; } } return false; }; if (!Core::Sandbox::Instance().customEnterFromEventLoop(callback)) { return; } if (_parentPopover) { [_parentPopover dismissPopover:nil]; } } - (void)scrubber:(NSScrubber *)scrubber didHighlightItemAtIndex:(NSInteger)index { if (_previewTimer) { _previewTimer->callOnce(QApplication::startDragTime()); _highlightedIndex = index; } } - (void)scrubber:(NSScrubber *)scrubber didChangeVisibleRange:(NSRange)visibleRange { [self didCancelInteractingWithScrubber:scrubber]; } - (void)didCancelInteractingWithScrubber:(NSScrubber *)scrubber { if (_previewTimer) { _previewTimer->cancel(); } if (_previewShown) { [self hidePreview]; } } - (void)showPreview { if (const auto document = _stickers[_highlightedIndex].document) { if (const auto w = App::wnd()) { w->showMediaPreview(document->stickerSetOrigin(), document); _previewShown = true; } } } - (bool)hidePreview { if (const auto w = App::wnd()) { Core::Sandbox::Instance().customEnterFromEventLoop([=] { w->hideMediaPreview(); }); _previewShown = false; _highlightedIndex = 0; return true; } return false; } - (void)updateStickers { std::vector temp; if (const auto error = RestrictionToSendStickers()) { temp.emplace_back(PickerScrubberItem( tr::lng_restricted_send_stickers_all(tr::now))); _stickers = std::move(temp); return; } AppendFavedStickers(temp); AppendRecentStickers(temp); auto count = 0; for (const auto setId : Auth().data().stickerSetsOrder()) { AppendStickerSet(temp, setId); if (++count == kMaxStickerSets) { break; } } if (!temp.size()) { temp.emplace_back(PickerScrubberItem( tr::lng_stickers_nothing_found(tr::now))); } _stickers = std::move(temp); } - (void)updateEmoji { std::vector temp; AppendEmojiPacks(temp); _stickers = std::move(temp); } @end // @implementation PickerCustomTouchBarItem @interface TouchBar() @end // @interface TouchBar @implementation TouchBar { NSView *_parentView; NSMutableArray *_mainPinnedButtons; NSTouchBar *_touchBarMain; NSTouchBar *_touchBarAudioPlayer; NSPopoverTouchBarItem *_popoverPicker; Platform::TouchBarType _touchBarType; Platform::TouchBarType _touchBarTypeBeforeLock; double _duration; double _position; rpl::lifetime _lifetime; rpl::lifetime _lifetimeSessionControllerChecker; } - (id) init:(NSView *)view { self = [super init]; if (!self) { return nil; } const auto iconSize = kIdealIconSize / 3; _position = 0; _duration = 0; _parentView = view; self.touchBarItems = @{ kPinnedPanelItemIdentifier: [NSMutableDictionary dictionaryWithDictionary:@{ @"type": kTypePinned, }], kSeekBarItemIdentifier: [NSMutableDictionary dictionaryWithDictionary:@{ @"type": kTypeSlider, @"name": @"Seek Bar" }], kPlayItemIdentifier: [NSMutableDictionary dictionaryWithDictionary:@{ @"type": kTypeButton, @"name": @"Play Button", @"cmd": [NSNumber numberWithInt:kCommandPlayPause], @"image": CreateNSImageFromStyleIcon(st::touchBarIconPlayerPause, iconSize), @"imageAlt": CreateNSImageFromStyleIcon(st::touchBarIconPlayerPlay, iconSize), }], kPreviousItemIdentifier: [NSMutableDictionary dictionaryWithDictionary:@{ @"type": kTypeButton, @"name": @"Previous Playlist Item", @"cmd": [NSNumber numberWithInt:kCommandPlaylistPrevious], @"image": CreateNSImageFromStyleIcon(st::touchBarIconPlayerPrevious, iconSize), }], kNextItemIdentifier: [NSMutableDictionary dictionaryWithDictionary:@{ @"type": kTypeButton, @"name": @"Next Playlist Item", @"cmd": [NSNumber numberWithInt:kCommandPlaylistNext], @"image": CreateNSImageFromStyleIcon(st::touchBarIconPlayerNext, iconSize), }], kCommandClosePlayerItemIdentifier: [NSMutableDictionary dictionaryWithDictionary:@{ @"type": kTypeButton, @"name": @"Close Player", @"cmd": [NSNumber numberWithInt:kCommandClosePlayer], @"image": CreateNSImageFromStyleIcon(st::touchBarIconPlayerClose, iconSize), }], kCurrentPositionItemIdentifier: [NSMutableDictionary dictionaryWithDictionary:@{ @"type": kTypeText, @"name": @"Current Position" }], kPopoverInputItemIdentifier: [NSMutableDictionary dictionaryWithDictionary:@{ @"type": kTypeFormatter, @"image": [NSImage imageNamed:NSImageNameTouchBarTextItalicTemplate], }], kPopoverInputFormatterItemIdentifier: [NSMutableDictionary dictionaryWithDictionary:@{ @"type": kTypeFormatterSegment, }], kScrubberStickersItemIdentifier: [NSMutableDictionary dictionaryWithDictionary:@{ @"type": kTypeScrubber, @"cmd": [NSNumber numberWithInt:kCommandScrubberStickers], }], kScrubberEmojiItemIdentifier: [NSMutableDictionary dictionaryWithDictionary:@{ @"type": kTypeScrubber, @"cmd": [NSNumber numberWithInt:kCommandScrubberEmoji], }], kPickerPopoverItemIdentifier: [NSMutableDictionary dictionaryWithDictionary:@{ @"type": kTypePicker, @"name": @"Picker", }] }; [self createTouchBar]; [self setTouchBar:Platform::TouchBarType::Main]; Media::Player::instance()->playerWidgetToggled( ) | rpl::start_with_next([=](bool toggled) { if (!toggled) { [self setTouchBar:Platform::TouchBarType::Main]; } else { [self setTouchBar:Platform::TouchBarType::AudioPlayer]; } }, _lifetime); Media::Player::instance()->updatedNotifier( ) | rpl::start_with_next([=](const Media::Player::TrackState &state) { [self handleTrackStateChange:state]; }, _lifetime); Core::App().passcodeLockChanges( ) | rpl::start_with_next([=](bool locked) { if (locked) { _touchBarTypeBeforeLock = _touchBarType; [self setTouchBar:Platform::TouchBarType::None]; } else { [self setTouchBar:_touchBarTypeBeforeLock]; } }, _lifetime); Auth().data().pinnedDialogsOrderUpdated( ) | rpl::start_with_next([self] { [self updatePinnedButtons]; }, _lifetime); Auth().data().chatsListChanges( ) | rpl::filter([](Data::Folder *folder) { return folder && folder->chatsList() && folder->id() == Data::Folder::kId; }) | rpl::start_with_next([=](Data::Folder *folder) { [self toggleArchiveButton:folder->chatsList()->empty()]; }, _lifetime); // At the time of this touchbar creation the sessionController does // not yet exist. But at the time of chatsListChanges event // the sessionController is valid and we can work with it. // So _lifetimeSessionControllerChecker is needed only once. Auth().data().chatsListChanges( ) | rpl::start_with_next([=] { if (const auto window = App::wnd()) { if (const auto controller = window->sessionController()) { if (!Auth().data().stickerSets().size()) { Auth().api().updateStickers(); } _lifetimeSessionControllerChecker.destroy(); controller->activeChatChanges( ) | rpl::start_with_next([=](Dialogs::Key key) { const auto show = key.peer() && key.history() && key.peer()->canWrite(); [self showPickerItem:show]; }, _lifetime); } } }, _lifetimeSessionControllerChecker); rpl::merge( Auth().data().stickersUpdated(), Auth().data().recentStickersUpdated() ) | rpl::start_with_next([=] { [self updatePickerPopover:ScrubberItemType::Sticker]; }, _lifetime); rpl::merge( Ui::Emoji::UpdatedRecent(), Ui::Emoji::Updated() ) | rpl::start_with_next([=] { [self updatePickerPopover:ScrubberItemType::Emoji]; }, _lifetime); [self updatePinnedButtons]; return self; } - (nullable NSTouchBarItem *) touchBar:(NSTouchBar *)touchBar makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier { const id dictionaryItem = self.touchBarItems[identifier]; const id type = dictionaryItem[@"type"]; const auto isType = [type](NSString *string) { return [type isEqualToString:string]; }; if (isType(kTypeSlider)) { NSSliderTouchBarItem *item = [[NSSliderTouchBarItem alloc] initWithIdentifier:identifier]; item.slider.minValue = 0.0f; item.slider.maxValue = 1.0f; item.target = self; item.action = @selector(seekbarChanged:); item.customizationLabel = dictionaryItem[@"name"]; [dictionaryItem setObject:item.slider forKey:@"view"]; return item; } else if (isType(kTypeButton) || isType(kTypeTextButton)) { NSCustomTouchBarItem *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier]; NSButton *button = isType(kTypeButton) ? [NSButton buttonWithImage:dictionaryItem[@"image"] target:self action:@selector(buttonAction:)] : [NSButton buttonWithTitle:dictionaryItem[@"name"] target:self action:@selector(buttonAction:)]; button.tag = [dictionaryItem[@"cmd"] intValue]; item.view = button; item.customizationLabel = dictionaryItem[@"name"]; [dictionaryItem setObject:button forKey:@"view"]; return item; } else if (isType(kTypeText)) { NSCustomTouchBarItem *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier]; NSTextField *text = [NSTextField labelWithString:@"00:00 / 00:00"]; text.alignment = NSTextAlignmentCenter; item.view = text; item.customizationLabel = dictionaryItem[@"name"]; [dictionaryItem setObject:text forKey:@"view"]; return item; } else if (isType(kTypeFormatter)) { NSPopoverTouchBarItem *item = [[NSPopoverTouchBarItem alloc] initWithIdentifier:identifier]; item.collapsedRepresentationImage = dictionaryItem[@"image"]; NSTouchBar *secondaryTouchBar = [[NSTouchBar alloc] init]; secondaryTouchBar.delegate = self; secondaryTouchBar.defaultItemIdentifiers = @[kPopoverInputFormatterItemIdentifier]; item.popoverTouchBar = secondaryTouchBar; return item; } else if (isType(kTypeFormatterSegment)) { NSCustomTouchBarItem *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier]; NSScrollView *scroll = [[NSScrollView alloc] init]; NSSegmentedControl *segment = [[NSSegmentedControl alloc] init]; segment.segmentStyle = NSSegmentStyleRounded; segment.target = self; segment.action = @selector(formatterClicked:); static const auto strings = { tr::lng_menu_formatting_bold, tr::lng_menu_formatting_italic, tr::lng_menu_formatting_underline, tr::lng_menu_formatting_strike_out, tr::lng_menu_formatting_monospace, tr::lng_menu_formatting_clear, tr::lng_info_link_label, }; segment.segmentCount = strings.size(); auto width = 0; auto count = 0; for (const auto s : strings) { const auto string = Q2NSString(s(tr::now)); width += WidthFromString(string) * 1.4; [segment setLabel:string forSegment:count++]; } segment.frame = NSMakeRect(0, 0, width, kCircleDiameter); [scroll setDocumentView:segment]; item.view = scroll; return item; } else if (isType(kTypeScrubber)) { const auto isSticker = ([dictionaryItem[@"cmd"] intValue] == kCommandScrubberStickers); const auto type = isSticker ? ScrubberItemType::Sticker : ScrubberItemType::Emoji; const auto popover = isSticker ? _popoverPicker : nil; PickerCustomTouchBarItem *item = [[PickerCustomTouchBarItem alloc] init:type popover:popover]; return item; } else if (isType(kTypePicker)) { NSPopoverTouchBarItem *item = [[NSPopoverTouchBarItem alloc] initWithIdentifier:identifier]; _popoverPicker = item; NSSegmentedControl *segment = [[NSSegmentedControl alloc] init]; [self updatePickerPopover:ScrubberItemType::Sticker]; [self updatePickerPopover:ScrubberItemType::Emoji]; const auto imageSize = kIdealIconSize / 3 * 2; segment.segmentStyle = NSSegmentStyleSeparated; segment.segmentCount = 2; [segment setImage:CreateNSImageFromStyleIcon(st::settingsIconStickers, imageSize) forSegment:0]; [segment setImage:CreateNSImageFromStyleIcon(st::settingsIconEmoji, imageSize) forSegment:1]; [segment setWidth:92 forSegment:0]; [segment setWidth:92 forSegment:1]; segment.target = self; segment.action = @selector(segmentClicked:); segment.trackingMode = NSSegmentSwitchTrackingMomentary; item.visibilityPriority = NSTouchBarItemPriorityHigh; item.collapsedRepresentation = segment; return item; } else if (isType(kTypePinned)) { NSCustomTouchBarItem *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier]; _mainPinnedButtons = [[NSMutableArray alloc] init]; NSStackView *stackView = [[NSStackView alloc] init]; for (auto i = kArchiveId; i <= Global::PinnedDialogsCountMax(); i++) { PinnedDialogButton *button = [[[PinnedDialogButton alloc] init:i] autorelease]; [_mainPinnedButtons addObject:button]; if (i == kArchiveId) { button.isDeletedFromView = true; continue; } [stackView addView:button.view inGravity:NSStackViewGravityTrailing]; } const auto space = kPinnedButtonsSpace; [stackView setEdgeInsets:NSEdgeInsetsMake(0, space / 2., 0, space)]; [stackView setSpacing:space]; item.view = stackView; [dictionaryItem setObject:item.view forKey:@"view"]; return item; } return nil; } - (void) createTouchBar { _touchBarMain = [[NSTouchBar alloc] init]; _touchBarMain.delegate = self; [self showItemInMain:nil]; _touchBarAudioPlayer = [[NSTouchBar alloc] init]; _touchBarAudioPlayer.delegate = self; _touchBarAudioPlayer.customizationIdentifier = kCustomizationIdPlayer.lowercaseString; _touchBarAudioPlayer.defaultItemIdentifiers = @[ kPlayItemIdentifier, kPreviousItemIdentifier, kNextItemIdentifier, kSeekBarItemIdentifier, kCommandClosePlayerItemIdentifier]; _touchBarAudioPlayer.customizationAllowedItemIdentifiers = @[ kPlayItemIdentifier, kPreviousItemIdentifier, kNextItemIdentifier, kCurrentPositionItemIdentifier, kSeekBarItemIdentifier, kCommandClosePlayerItemIdentifier]; } - (void) setTouchBar:(Platform::TouchBarType)type { if (_touchBarType == type) { return; } if (type == Platform::TouchBarType::Main) { [_parentView setTouchBar:_touchBarMain]; } else if (type == Platform::TouchBarType::AudioPlayer) { if (!CurrentSongExists() || Media::Player::instance()->getActiveType() != kSongType) { return; } [_parentView setTouchBar:_touchBarAudioPlayer]; } else if (type == Platform::TouchBarType::AudioPlayerForce) { [_parentView setTouchBar:_touchBarAudioPlayer]; _touchBarType = Platform::TouchBarType::AudioPlayer; return; } else if (type == Platform::TouchBarType::None) { [_parentView setTouchBar:nil]; } _touchBarType = type; } - (void) showInputFieldItem:(bool)show { [self showItemInMain: show ? kPopoverInputItemIdentifier : CanWriteToActiveChat() ? kPickerPopoverItemIdentifier : nil]; } - (void) showPickerItem:(bool)show { [self showItemInMain: show ? kPickerPopoverItemIdentifier : nil]; } - (void) showItemInMain:(NSTouchBarItemIdentifier)item { NSMutableArray *items = [NSMutableArray arrayWithArray:@[kPinnedPanelItemIdentifier]]; if (item) { [items addObject:item]; } _touchBarMain.defaultItemIdentifiers = items; } - (void) updatePickerPopover:(ScrubberItemType)type { NSTouchBar *secondaryTouchBar = [[NSTouchBar alloc] init]; secondaryTouchBar.delegate = self; const auto popover = IsSticker(type) ? _popoverPicker : nil; [[PickerCustomTouchBarItem alloc] init:type popover:popover]; const auto identifier = IsSticker(type) ? kScrubberStickersItemIdentifier : kScrubberEmojiItemIdentifier; secondaryTouchBar.defaultItemIdentifiers = @[identifier]; _popoverPicker.popoverTouchBar = secondaryTouchBar; } // Main Touchbar. - (void) toggleArchiveButton:(bool)hide { for (PinnedDialogButton *button in _mainPinnedButtons) { if (button.number == kArchiveId) { NSCustomTouchBarItem *item = [_touchBarMain itemForIdentifier:kPinnedPanelItemIdentifier]; NSStackView *stack = item.view; [button updateUserpic]; if (hide && !button.isDeletedFromView) { button.isDeletedFromView = true; [stack removeView:button.view]; } if (!hide && button.isDeletedFromView) { button.isDeletedFromView = false; [stack insertView:button.view atIndex:(button.number + 1) inGravity:NSStackViewGravityLeading]; } } } } - (void) updatePinnedButtons { const auto &order = Auth().data().pinnedChatsOrder(nullptr); auto isSelfPeerPinned = false; auto isArchivePinned = false; PinnedDialogButton *selfChatButton; NSCustomTouchBarItem *item = [_touchBarMain itemForIdentifier:kPinnedPanelItemIdentifier]; NSStackView *stack = item.view; for (PinnedDialogButton *button in _mainPinnedButtons) { const auto num = button.number; if (num <= kSavedMessagesId) { if (num == kSavedMessagesId) { selfChatButton = button; } else if (num == kArchiveId) { isArchivePinned = !button.isDeletedFromView; } continue; } const auto numIsTooLarge = num > order.size(); [button.view setHidden:numIsTooLarge]; if (numIsTooLarge) { button.peer = nil; continue; } const auto pinned = order.at(num - 1); if (const auto history = pinned.history()) { button.peer = history->peer; [button updateUserpic]; if (history->peer->id == Auth().userPeerId()) { isSelfPeerPinned = true; } } } // If self chat is pinned, delete from view saved messages button. if (isSelfPeerPinned && !selfChatButton.isDeletedFromView) { selfChatButton.isDeletedFromView = true; [stack removeView:selfChatButton.view]; } else if (!isSelfPeerPinned && selfChatButton.isDeletedFromView) { selfChatButton.isDeletedFromView = false; [stack insertView:selfChatButton.view atIndex:(isArchivePinned ? 1 : 0) inGravity:NSStackViewGravityLeading]; } } // Audio Player Touchbar. - (void) handleTrackStateChange:(Media::Player::TrackState)state { if (state.id.type() == kSongType) { [self setTouchBar:Platform::TouchBarType::AudioPlayerForce]; } else { return; } _position = state.position < 0 ? 0 : state.position; _duration = state.length; if (Media::Player::IsStoppedOrStopping(state.state)) { _position = 0; _duration = 0; } [self updateTouchBarTimeItem]; NSButton *playButton = self.touchBarItems[kPlayItemIdentifier][@"view"]; const auto imgButton = (state.state == Media::Player::State::Playing) ? @"image" : @"imageAlt"; playButton.image = self.touchBarItems[kPlayItemIdentifier][imgButton]; [self.touchBarItems[kNextItemIdentifier][@"view"] setEnabled:Media::Player::instance()->nextAvailable(kSongType)]; [self.touchBarItems[kPreviousItemIdentifier][@"view"] setEnabled:Media::Player::instance()->previousAvailable(kSongType)]; } - (void) updateTouchBarTimeItem { const id item = self.touchBarItems[kCurrentPositionItemIdentifier]; NSSlider *seekSlider = self.touchBarItems[kSeekBarItemIdentifier][@"view"]; NSTextField *textField = item[@"view"]; if (_duration <= 0) { seekSlider.enabled = NO; seekSlider.doubleValue = 0; } else { seekSlider.enabled = YES; if (!seekSlider.highlighted) { seekSlider.doubleValue = (_position / _duration) * seekSlider.maxValue; } } const auto timeToString = [&](int t) { return FormatTime((int)floor(t / kMs)); }; textField.stringValue = [NSString stringWithFormat:@"%@ / %@", timeToString(_position), timeToString(_duration)]; NSTextField *field = item[@"view"]; if (!field) { return; } [field removeConstraint:item[@"constrain"]]; NSString *fString = [[textField.stringValue componentsSeparatedByCharactersInSet: [NSCharacterSet decimalDigitCharacterSet]] componentsJoinedByString:@"0"]; NSLayoutConstraint *con = [NSLayoutConstraint constraintWithItem:field attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:WidthFromString(fString)]; [field addConstraint:con]; [item setObject:con forKey:@"constrain"]; } - (void) buttonAction:(NSButton *)sender { const auto command = sender.tag; Core::Sandbox::Instance().customEnterFromEventLoop([=] { switch (command) { case kCommandPlayPause: Media::Player::instance()->playPause(kSongType); break; case kCommandPlaylistPrevious: Media::Player::instance()->previous(kSongType); break; case kCommandPlaylistNext: Media::Player::instance()->next(kSongType); break; case kCommandClosePlayer: App::main()->closeBothPlayers(); break; } }); } - (void) seekbarChanged:(NSSliderTouchBarItem *)sender { // https://stackoverflow.com/a/45891017 NSEvent *event = [[NSApplication sharedApplication] currentEvent]; const auto touchUp = [event touchesMatchingPhase:NSTouchPhaseEnded inView:nil].count > 0; Core::Sandbox::Instance().customEnterFromEventLoop([=] { if (touchUp) { Media::Player::instance()->finishSeeking(kSongType, sender.slider.doubleValue); } else { Media::Player::instance()->startSeeking(kSongType); } }); } - (void) segmentClicked:(NSSegmentedControl *)sender { const auto identifier = sender.selectedSegment ? kScrubberEmojiItemIdentifier : kScrubberStickersItemIdentifier; _popoverPicker.popoverTouchBar.defaultItemIdentifiers = @[identifier]; [_popoverPicker showPopover:nil]; } - (void) formatterClicked:(NSSegmentedControl *)sender { [[_touchBarMain itemForIdentifier:kPopoverInputItemIdentifier] dismissPopover:nil]; const auto command = int(sender.selectedSegment) + kCommandBold; sender.selectedSegment = -1; SendKeyEvent(command); } -(void)dealloc { for (PinnedDialogButton *button in _mainPinnedButtons) { [button release]; } [super dealloc]; } @end // @implementation TouchBar