Moved touchbar scrubber item for stickers and emoji to separate file.

This commit is contained in:
23rd 2020-07-05 20:26:31 +03:00 committed by John Preston
parent 54149fb156
commit c50df6a6bc
3 changed files with 706 additions and 0 deletions

View File

@ -893,6 +893,8 @@ PRIVATE
platform/mac/window_title_mac.h
platform/mac/touchbar/items/mac_pinned_chats_item.h
platform/mac/touchbar/items/mac_pinned_chats_item.mm
platform/mac/touchbar/items/mac_scrubber_item.h
platform/mac/touchbar/items/mac_scrubber_item.mm
platform/mac/touchbar/mac_touchbar_audio.h
platform/mac/touchbar/mac_touchbar_audio.mm
platform/mac/touchbar/mac_touchbar_common.h

View File

@ -0,0 +1,21 @@
/*
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
*/
#pragma once
#import <AppKit/NSPopoverTouchBarItem.h>
#import <AppKit/NSTouchBar.h>
namespace Window {
class Controller;
} // namespace Window
API_AVAILABLE(macos(10.12.2))
@interface StickerEmojiPopover : NSPopoverTouchBarItem<NSTouchBarDelegate>
- (id)init:(not_null<Window::Controller*>)controller
identifier:(NSTouchBarItemIdentifier)identifier;
@end

View File

@ -0,0 +1,683 @@
/*
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
*/
#include "platform/mac/touchbar/items/mac_scrubber_item.h"
#ifndef OS_OSX
#include "api/api_common.h"
#include "api/api_sending.h"
#include "base/call_delayed.h"
#include "base/platform/mac/base_utilities_mac.h"
#include "boxes/confirm_box.h"
#include "chat_helpers/emoji_list_widget.h"
#include "core/sandbox.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_file_origin.h"
#include "data/data_session.h"
#include "data/stickers/data_stickers.h"
#include "history/history.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "platform/mac/touchbar/mac_touchbar_common.h"
#include "styles/style_basic.h"
#include "styles/style_settings.h"
#include "ui/widgets/input_fields.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#import <AppKit/NSCustomTouchBarItem.h>
#import <AppKit/NSGestureRecognizer.h>
#import <AppKit/NSImage.h>
#import <AppKit/NSImageView.h>
#import <AppKit/NSPressGestureRecognizer.h>
#import <AppKit/NSScrollView.h>
#import <AppKit/NSScrubber.h>
#import <AppKit/NSScrubberItemView.h>
#import <AppKit/NSScrubberLayout.h>
#import <AppKit/NSSegmentedControl.h>
#import <AppKit/NSTextField.h>
NSImage *qt_mac_create_nsimage(const QPixmap &pm);
using TouchBar::kCircleDiameter;
using TouchBar::CreateNSImageFromStyleIcon;
namespace {
//https://developer.apple.com/design/human-interface-guidelines/macos/touch-bar/touch-bar-icons-and-images/
constexpr auto kIdealIconSize = 36;
constexpr auto kSegmentIconSize = 25;
constexpr auto kSegmentSize = 92;
constexpr auto kMaxStickerSets = 5;
constexpr auto kGestureStateProcessed = {
NSGestureRecognizerStateChanged,
NSGestureRecognizerStateBegan,
};
constexpr auto kGestureStateFinished = {
NSGestureRecognizerStateEnded,
NSGestureRecognizerStateCancelled,
NSGestureRecognizerStateFailed,
};
const auto kStickersScrubber = @"scrubberStickers";
const auto kEmojiScrubber = @"scrubberEmoji";
const auto kStickerItemIdentifier = @"stickerItem";
const auto kEmojiItemIdentifier = @"emojiItem";
const auto kPickerTitleItemIdentifier = @"pickerTitleItem";
enum ScrubberItemType {
Emoji,
Sticker,
None,
};
inline bool IsSticker(ScrubberItemType type) {
return type == ScrubberItemType::Sticker;
}
struct PickerScrubberItem {
PickerScrubberItem(QString title) : title(title) {
}
PickerScrubberItem(DocumentData *document) : document(document) {
mediaView = document->createMediaView();
mediaView->checkStickerSmall();
updateThumbnail();
}
PickerScrubberItem(EmojiPtr emoji) : emoji(emoji) {
}
void updateThumbnail() {
if (!document || !qpixmap.isNull()) {
return;
}
const auto image = mediaView->getStickerSmall();
if (!image) {
return;
}
const auto size = image->size()
.scaled(kCircleDiameter, kCircleDiameter, Qt::KeepAspectRatio);
qpixmap = image->pixSingle(
size.width(),
size.height(),
kCircleDiameter,
kCircleDiameter,
ImageRoundRadius::None);
}
bool isStickerLoaded() const {
return !qpixmap.isNull();
}
QString title = QString();
DocumentData *document = nullptr;
std::shared_ptr<Data::DocumentMedia> mediaView = nullptr;
QPixmap qpixmap;
EmojiPtr emoji = nullptr;
};
struct PickerScrubberItemsHolder {
std::vector<PickerScrubberItem> stickers;
std::vector<PickerScrubberItem> emoji;
int size(ScrubberItemType type) {
return IsSticker(type) ? stickers.size() : emoji.size();
}
auto at(int index, ScrubberItemType type) {
return IsSticker(type) ? stickers[index] : emoji[index];
}
};
using Platform::Q2NSString;
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);
Ui::Emoji::Draw(
paint,
std::move(emoji),
Ui::Emoji::GetSizeTouchbar(),
0,
0);
return [qt_mac_create_nsimage(pixmap) autorelease];
}
auto ActiveChat(not_null<Window::Controller*> controller) {
if (const auto sessionController = controller->sessionController()) {
return sessionController->activeChatCurrent();
}
return Dialogs::Key();
}
bool CanWriteToActiveChat(not_null<Window::Controller*> controller) {
if (const auto history = ActiveChat(controller).history()) {
return history->peer->canWrite();
}
return false;
}
std::optional<QString> RestrictionToSendStickers(not_null<PeerData*> peer) {
return Data::RestrictionError(
peer,
ChatRestriction::f_send_stickers);
}
std::optional<QString> RestrictionToSendStickers(
not_null<Window::Controller*> controller) {
if (const auto peer = ActiveChat(controller).peer()) {
return RestrictionToSendStickers(peer);
}
return std::nullopt;
}
QString TitleRecentlyUsed(const Data::StickersSets &sets) {
const auto it = sets.find(Data::Stickers::CloudRecentSetId);
return (it != sets.cend())
? it->second->title
: tr::lng_recent_stickers(tr::now);
}
void AppendStickerSet(
const Data::StickersSets &sets,
std::vector<PickerScrubberItem> &to,
uint64 setId) {
const auto it = sets.find(setId);
if (it == sets.cend() || it->second->stickers.isEmpty()) {
return;
}
const auto set = it->second.get();
if (set->flags & MTPDstickerSet::Flag::f_archived) {
return;
}
if (!(set->flags & MTPDstickerSet::Flag::f_installed_date)) {
return;
}
to.emplace_back(PickerScrubberItem(set->title.isEmpty()
? set->shortName
: set->title));
for (const auto sticker : set->stickers) {
to.emplace_back(PickerScrubberItem(sticker));
}
}
void AppendRecentStickers(
const Data::StickersSets &sets,
RecentStickerPack &recentPack,
std::vector<PickerScrubberItem> &to) {
const auto cloudIt = sets.find(Data::Stickers::CloudRecentSetId);
const auto cloudCount = (cloudIt != sets.cend())
? cloudIt->second->stickers.size()
: 0;
if (cloudCount > 0) {
to.emplace_back(PickerScrubberItem(cloudIt->second->title));
auto count = 0;
for (const auto document : cloudIt->second->stickers) {
if (document->owner().stickers().isFaved(document)) {
continue;
}
to.emplace_back(PickerScrubberItem(document));
}
}
for (const auto recent : recentPack) {
to.emplace_back(PickerScrubberItem(recent.first));
}
}
void AppendFavedStickers(
const Data::StickersSets &sets,
std::vector<PickerScrubberItem> &to) {
const auto it = sets.find(Data::Stickers::FavedSetId);
const auto count = (it != sets.cend())
? it->second->stickers.size()
: 0;
if (!count) {
return;
}
to.emplace_back(PickerScrubberItem(
tr::lng_mac_touchbar_favorite_stickers(tr::now)));
for (const auto document : it->second->stickers) {
to.emplace_back(PickerScrubberItem(document));
}
}
void AppendEmojiPacks(
const Data::StickersSets &sets,
std::vector<PickerScrubberItem> &to) {
for (auto i = 0; i != ChatHelpers::kEmojiSectionCount; ++i) {
const auto section = static_cast<Ui::Emoji::Section>(i);
const auto list = (section == Ui::Emoji::Section::Recent)
? GetRecentEmojiSection()
: Ui::Emoji::GetSection(section);
const auto title = (section == Ui::Emoji::Section::Recent)
? TitleRecentlyUsed(sets)
: ChatHelpers::EmojiCategoryTitle(i)(tr::now);
to.emplace_back(title);
for (const auto &emoji : list) {
to.emplace_back(PickerScrubberItem(emoji));
}
}
}
} // namespace
@interface PickerScrubberItemView : NSScrubberImageItemView {
@public
DocumentId documentId;
}
@end // @interface PickerScrubberItemView
@implementation PickerScrubberItemView
@end // @implementation PickerScrubberItemView
#pragma mark - PickerCustomTouchBarItem
@interface PickerCustomTouchBarItem : NSCustomTouchBarItem
<NSScrubberDelegate,
NSScrubberDataSource,
NSScrubberFlowLayoutDelegate>
@end // @interface PickerCustomTouchBarItem
@implementation PickerCustomTouchBarItem {
ScrubberItemType _type;
std::shared_ptr<PickerScrubberItemsHolder> _itemsDataSource;
std::unique_ptr<PickerScrubberItem> _error;
DocumentId _lastPreviewedSticker;
Window::Controller *_controller;
History *_history;
rpl::event_stream<> _closeRequests;
rpl::lifetime _lifetime;
}
- (id)init:(ScrubberItemType)type
controller:(not_null<Window::Controller*>)controller
items:(std::shared_ptr<PickerScrubberItemsHolder>)items {
Expects(controller->sessionController() != nullptr);
self = [super initWithIdentifier:IsSticker(type)
? kStickersScrubber
: kEmojiScrubber];
if (!self) {
return self;
}
_type = type;
_controller = controller;
_itemsDataSource = items;
auto *scrubber = [[[NSScrubber alloc] initWithFrame:NSZeroRect]
autorelease];
auto *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];
if (IsSticker(type)) {
auto *gesture = [[[NSPressGestureRecognizer alloc]
initWithTarget:self
action:@selector(gesturePreviewHandler:)] autorelease];
gesture.allowedTouchTypes = NSTouchTypeMaskDirect;
gesture.minimumPressDuration = QApplication::startDragTime() / 1000.;
gesture.allowableMovement = 0;
[scrubber addGestureRecognizer:gesture];
if (const auto error = RestrictionToSendStickers(_controller)) {
_error = std::make_unique<PickerScrubberItem>(
tr::lng_restricted_send_stickers_all(tr::now));
}
}
_lastPreviewedSticker = 0;
self.view = scrubber;
return self;
}
- (PickerScrubberItem)itemAt:(int)index {
return _error ? *_error : _itemsDataSource->at(index, _type);
}
- (void)gesturePreviewHandler:(NSPressGestureRecognizer*)gesture {
const auto customEnter = [=](auto &&callback) {
Core::Sandbox::Instance().customEnterFromEventLoop([=] {
if (_controller) {
callback();
}
});
};
const auto checkState = [&](const auto &states) {
return ranges::contains(states, gesture.state);
};
if (checkState(kGestureStateProcessed)) {
NSScrollView *scrollView = self.view;
auto *container = scrollView.documentView.subviews.firstObject;
if (!container) {
return;
}
const auto point = [gesture locationInView:container];
for (PickerScrubberItemView *item in container.subviews) {
if (![item isMemberOfClass:[PickerScrubberItemView class]]
|| (item->documentId == _lastPreviewedSticker)
|| !NSPointInRect(point, item.frame)) {
continue;
}
_lastPreviewedSticker = item->documentId;
auto &owner = _controller->sessionController()->session().data();
const auto doc = owner.document(item->documentId);
customEnter([=] {
_controller->widget()->showMediaPreview(
Data::FileOrigin(),
doc);
});
break;
}
} else if (checkState(kGestureStateFinished)) {
customEnter([=] { _controller->widget()->hideMediaPreview(); });
_lastPreviewedSticker = 0;
}
}
- (void)encodeWithCoder:(nonnull NSCoder*)aCoder {
// Has not been implemented.
}
#pragma mark - NSScrubberDelegate
- (NSInteger)numberOfItemsForScrubber:(NSScrubber*)scrubber {
return _error ? 1 : _itemsDataSource->size(_type);
}
- (NSScrubberItemView*)scrubber:(NSScrubber*)scrubber
viewForItemAtIndex:(NSInteger)index {
const auto item = [self itemAt:index];
if (const auto document = item.document) {
PickerScrubberItemView *itemView = [scrubber
makeItemWithIdentifier:kStickerItemIdentifier
owner:self];
itemView.imageView.image = [qt_mac_create_nsimage(item.qpixmap)
autorelease];
itemView->documentId = document->id;
return itemView;
} else if (const auto emoji = item.emoji) {
NSScrubberImageItemView *itemView = [scrubber
makeItemWithIdentifier:kEmojiItemIdentifier
owner:self];
itemView.imageView.image = CreateNSImageFromEmoji(emoji);
return itemView;
} else {
NSScrubberTextItemView *itemView = [scrubber
makeItemWithIdentifier:kPickerTitleItemIdentifier
owner:self];
itemView.textField.stringValue = Q2NSString(item.title);
return itemView;
}
}
- (NSSize)scrubber:(NSScrubber*)scrubber
layout:(NSScrubberFlowLayout*)layout
sizeForItemAtIndex:(NSInteger)index {
const auto t = [self itemAt:index].title;
const auto w = t.isEmpty() ? 0 : TouchBar::WidthFromString(Q2NSString(t));
return NSMakeSize(kCircleDiameter + w, kCircleDiameter);
}
- (void)scrubber:(NSScrubber*)scrubber
didSelectItemAtIndex:(NSInteger)index {
if (!CanWriteToActiveChat(_controller) || _error) {
return;
}
scrubber.selectedIndex = -1;
const auto sticker = _itemsDataSource->at(index, _type);
const auto document = sticker.document;
const auto emoji = sticker.emoji;
auto callback = [=] {
if (document) {
if (const auto error = RestrictionToSendStickers(_controller)) {
Ui::show(Box<InformBox>(*error));
return true;
}
Api::SendExistingDocument(
Api::MessageToSend(ActiveChat(_controller).history()),
document);
return true;
} else if (emoji) {
if (const auto inputField = qobject_cast<QTextEdit*>(
QApplication::focusWidget())) {
Ui::InsertEmojiAtCursor(inputField->textCursor(), emoji);
AddRecentEmoji(emoji);
return true;
}
}
return false;
};
if (!Core::Sandbox::Instance().customEnterFromEventLoop(
std::move(callback))) {
return;
}
_closeRequests.fire({});
}
- (rpl::producer<>)closeRequests {
return _closeRequests.events();
}
- (rpl::lifetime &)lifetime {
return _lifetime;
}
@end // @implementation PickerCustomTouchBarItem
#pragma mark - StickerEmojiPopover
@implementation StickerEmojiPopover {
Window::Controller *_controller;
Main::Session *_session;
std::shared_ptr<PickerScrubberItemsHolder> _itemsDataSource;
ScrubberItemType _waitingForUpdate;
rpl::lifetime _lifetime;
}
- (id)init:(not_null<Window::Controller*>)controller
identifier:(NSTouchBarItemIdentifier)identifier {
self = [super initWithIdentifier:identifier];
if (!self) {
return nil;
}
_controller = controller;
_session = &controller->sessionController()->session();
_waitingForUpdate = ScrubberItemType::None;
auto *segment = [[[NSSegmentedControl alloc] init] autorelease];
const auto size = kSegmentIconSize;
segment.segmentStyle = NSSegmentStyleSeparated;
segment.segmentCount = 2;
[segment
setImage:CreateNSImageFromStyleIcon(st::settingsIconStickers, size)
forSegment:0];
[segment
setImage:CreateNSImageFromStyleIcon(st::settingsIconEmoji, size)
forSegment:1];
[segment setWidth:kSegmentSize forSegment:0];
[segment setWidth:kSegmentSize forSegment:1];
segment.target = self;
segment.action = @selector(segmentClicked:);
segment.trackingMode = NSSegmentSwitchTrackingMomentary;
self.visibilityPriority = NSTouchBarItemPriorityHigh;
self.collapsedRepresentation = segment;
self.popoverTouchBar = [[[NSTouchBar alloc] init] autorelease];
self.popoverTouchBar.delegate = self;
rpl::single(
controller->sessionController()->activeChatCurrent()
) | rpl::then(
controller->sessionController()->activeChatChanges()
) | rpl::map([](Dialogs::Key k) {
return k.peer()
&& k.history()
&& k.peer()->canWrite()
&& !RestrictionToSendStickers(k.peer());
}) | rpl::distinct_until_changed(
) | rpl::start_with_next([=](bool value) {
[self dismissPopover:nil];
}, _lifetime);
_itemsDataSource = std::make_shared<PickerScrubberItemsHolder>();
const auto localGuard = _lifetime.make_state<base::has_weak_ptr>();
// Workaround.
// A little waiting for the sticker sets and the ending animation.
base::call_delayed(st::slideDuration, &(*localGuard), [=] {
[self updateStickers];
[self updateEmoji];
});
rpl::merge(
rpl::merge(
_session->data().stickers().updated(),
_session->data().stickers().recentUpdated()
) | rpl::map_to(ScrubberItemType::Sticker),
rpl::merge(
UpdatedRecentEmoji(),
Ui::Emoji::Updated()
) | rpl::map_to(ScrubberItemType::Emoji)
) | rpl::start_with_next([=](ScrubberItemType type) {
_waitingForUpdate = type;
}, _lifetime);
return self;
}
- (NSTouchBarItem*)touchBar:(NSTouchBar*)touchBar
makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier {
if (!touchBar) {
return nil;
}
const auto isEqual = [&](NSString *string) {
return [identifier isEqualToString:string];
};
if (isEqual(kStickersScrubber)) {
auto *item = [[[PickerCustomTouchBarItem alloc]
init:(ScrubberItemType::Sticker)
controller:_controller
items:_itemsDataSource] autorelease];
auto &lifetime = [item lifetime];
[item closeRequests] | rpl::start_with_next([=] {
[self dismissPopover:nil];
[self updateStickers];
}, lifetime);
return item;
} else if (isEqual(kEmojiScrubber)) {
return [[[PickerCustomTouchBarItem alloc]
init:(ScrubberItemType::Emoji)
controller:_controller
items:_itemsDataSource] autorelease];
}
return nil;
}
- (void)segmentClicked:(NSSegmentedControl*)sender {
self.popoverTouchBar.defaultItemIdentifiers = @[];
const auto identifier = sender.selectedSegment
? kEmojiScrubber
: kStickersScrubber;
if (sender.selectedSegment
&& _waitingForUpdate == ScrubberItemType::Emoji) {
[self updateEmoji];
} else if (!sender.selectedSegment
&& _waitingForUpdate == ScrubberItemType::Sticker) {
[self updateStickers];
}
self.popoverTouchBar.defaultItemIdentifiers = @[identifier];
[self showPopover:nil];
}
- (void)addDownloadHandler {
const auto loadingLifetime = _lifetime.make_state<rpl::lifetime>();
const auto checkLoaded = [=](const auto &sticker) {
return !sticker.document || sticker.isStickerLoaded();
};
const auto isPerformedOnMain = loadingLifetime->make_state<bool>(true);
const auto localGuard = loadingLifetime->make_state<base::has_weak_ptr>();
_session->downloaderTaskFinished(
) | rpl::start_with_next(crl::guard(&(*localGuard), [=] {
if (*isPerformedOnMain) {
crl::on_main(&(*localGuard), [=] {
for (auto &sticker : _itemsDataSource->stickers) {
sticker.updateThumbnail();
}
if (ranges::all_of(_itemsDataSource->stickers, checkLoaded)) {
loadingLifetime->destroy();
return;
}
*isPerformedOnMain = true;
});
}
*isPerformedOnMain = false;
}), *loadingLifetime);
}
- (void)updateStickers {
auto &stickers = _session->data().stickers();
std::vector<PickerScrubberItem> temp;
AppendFavedStickers(stickers.sets(), temp);
AppendRecentStickers(stickers.sets(), stickers.getRecentPack(), temp);
auto count = 0;
for (const auto setId : stickers.setsOrderRef()) {
AppendStickerSet(stickers.sets(), temp, setId);
if (++count == kMaxStickerSets) {
break;
}
}
if (!temp.size()) {
temp.emplace_back(PickerScrubberItem(
tr::lng_stickers_nothing_found(tr::now)));
}
_itemsDataSource->stickers = std::move(temp);
_waitingForUpdate = ScrubberItemType::None;
[self addDownloadHandler];
}
- (void)updateEmoji {
std::vector<PickerScrubberItem> temp;
AppendEmojiPacks(_session->data().stickers().sets(), temp);
_itemsDataSource->emoji = std::move(temp);
_waitingForUpdate = ScrubberItemType::None;
}
@end // @implementation StickerEmojiPopover
#endif // OS_OSX