init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
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>
|
||||
|
||||
API_AVAILABLE(macos(10.12.2))
|
||||
@interface TextFormatPopover : NSPopoverTouchBarItem
|
||||
- (id)init:(NSTouchBarItemIdentifier)identifier;
|
||||
@end
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
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_formatter_item.h"
|
||||
|
||||
#include "base/platform/mac/base_utilities_mac.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "platform/mac/touchbar/mac_touchbar_common.h"
|
||||
|
||||
#import <AppKit/NSCustomTouchBarItem.h>
|
||||
#import <AppKit/NSScrollView.h>
|
||||
#import <AppKit/NSSegmentedControl.h>
|
||||
|
||||
#include <QtWidgets/QApplication>
|
||||
#include <QtWidgets/QTextEdit>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kCommandBold = 0x010;
|
||||
constexpr auto kCommandItalic = 0x011;
|
||||
constexpr auto kCommandUnderline = 0x012;
|
||||
constexpr auto kCommandStrikeOut = 0x013;
|
||||
constexpr auto kCommandBlockquote = 0x014;
|
||||
constexpr auto kCommandMonospace = 0x015;
|
||||
constexpr auto kCommandClear = 0x016;
|
||||
constexpr auto kCommandLink = 0x017;
|
||||
|
||||
const auto kPopoverFormatter = @"popoverInputFormatter";
|
||||
|
||||
void SendKeyEvent(int command) {
|
||||
auto *focused = qobject_cast<QTextEdit*>(QApplication::focusWidget());
|
||||
if (!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 kCommandBlockquote:
|
||||
key = Qt::Key_Period;
|
||||
modifier |= Qt::ShiftModifier;
|
||||
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));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
#pragma mark - TextFormatPopover
|
||||
|
||||
@implementation TextFormatPopover {
|
||||
rpl::lifetime _lifetime;
|
||||
}
|
||||
|
||||
- (id)init:(NSTouchBarItemIdentifier)identifier {
|
||||
self = [super initWithIdentifier:identifier];
|
||||
if (!self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
self.collapsedRepresentationImage = [NSImage
|
||||
imageNamed:NSImageNameTouchBarTextItalicTemplate]; // autorelease];
|
||||
auto *secondaryTouchBar = [[[NSTouchBar alloc] init] autorelease];
|
||||
|
||||
auto *popover = [[[NSCustomTouchBarItem alloc]
|
||||
initWithIdentifier:kPopoverFormatter] autorelease];
|
||||
{
|
||||
auto *scroll = [[[NSScrollView alloc] init] autorelease];
|
||||
auto *segment = [[[NSSegmentedControl alloc] init] autorelease];
|
||||
segment.segmentStyle = NSSegmentStyleRounded;
|
||||
segment.target = self;
|
||||
segment.action = @selector(segmentClicked:);
|
||||
|
||||
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_blockquote,
|
||||
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 = Platform::Q2NSString(s(tr::now));
|
||||
width += TouchBar::WidthFromString(string) * 1.4;
|
||||
[segment setLabel:string forSegment:count++];
|
||||
}
|
||||
segment.frame = NSMakeRect(0, 0, width, TouchBar::kCircleDiameter);
|
||||
[scroll setDocumentView:segment];
|
||||
popover.view = scroll;
|
||||
}
|
||||
|
||||
secondaryTouchBar.templateItems = [NSSet setWithArray:@[popover]];
|
||||
secondaryTouchBar.defaultItemIdentifiers = @[kPopoverFormatter];
|
||||
|
||||
self.popoverTouchBar = secondaryTouchBar;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)segmentClicked:(NSSegmentedControl*)sender {
|
||||
const auto command = int(sender.selectedSegment) + kCommandBold;
|
||||
sender.selectedSegment = -1;
|
||||
SendKeyEvent(command);
|
||||
|
||||
[self dismissPopover:nil];
|
||||
}
|
||||
|
||||
@end // @implementation TextFormatPopover
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
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
|
||||
|
||||
#include <AppKit/NSImageView.h>
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
API_AVAILABLE(macos(10.12.2))
|
||||
@interface PinnedDialogsPanel : NSImageView
|
||||
- (id)init:(not_null<Main::Session*>)session
|
||||
destroyEvent:(rpl::producer<>)touchBarSwitches;
|
||||
@end
|
||||
@@ -0,0 +1,873 @@
|
||||
/*
|
||||
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_pinned_chats_item.h"
|
||||
|
||||
#include "apiwrap.h"
|
||||
#include "base/call_delayed.h"
|
||||
#include "base/timer.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "core/application.h"
|
||||
#include "core/sandbox.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_cloud_file.h"
|
||||
#include "data/data_file_origin.h"
|
||||
#include "data/data_folder.h"
|
||||
#include "data/data_peer_values.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "history/history.h"
|
||||
#include "main/main_session.h"
|
||||
#include "mainwidget.h"
|
||||
#include "platform/mac/touchbar/mac_touchbar_common.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/empty_userpic.h"
|
||||
#include "ui/userpic_view.h"
|
||||
#include "ui/unread_badge_paint.h"
|
||||
#include "ui/painter.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "window/window_session_controller.h"
|
||||
|
||||
#import <AppKit/NSColor.h>
|
||||
#import <AppKit/NSGraphicsContext.h>
|
||||
#import <AppKit/NSPressGestureRecognizer.h>
|
||||
|
||||
using TouchBar::kCircleDiameter;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kPinnedButtonsSpace = 30;
|
||||
constexpr auto kPinnedButtonsLeftSkip = kPinnedButtonsSpace / 2;
|
||||
|
||||
constexpr auto kOnlineCircleSize = 8;
|
||||
constexpr auto kOnlineCircleStrokeWidth = 1.5;
|
||||
constexpr auto kUnreadBadgeSize = 15;
|
||||
|
||||
inline bool IsSelfPeer(PeerData *peer) {
|
||||
return peer && peer->isSelf();
|
||||
}
|
||||
|
||||
inline bool IsRepliesPeer(PeerData *peer) {
|
||||
return peer && peer->isRepliesChat();
|
||||
}
|
||||
|
||||
QImage PrepareImage() {
|
||||
const auto s = kCircleDiameter * style::DevicePixelRatio();
|
||||
auto result = QImage(QSize(s, s), QImage::Format_ARGB32_Premultiplied);
|
||||
result.fill(Qt::transparent);
|
||||
return result;
|
||||
}
|
||||
|
||||
QImage SavedMessagesUserpic() {
|
||||
auto result = PrepareImage();
|
||||
Painter paint(&result);
|
||||
|
||||
const auto s = result.width();
|
||||
Ui::EmptyUserpic::PaintSavedMessages(paint, 0, 0, s, s);
|
||||
return result;
|
||||
}
|
||||
|
||||
QImage RepliesMessagesUserpic() {
|
||||
auto result = PrepareImage();
|
||||
Painter paint(&result);
|
||||
|
||||
const auto s = result.width();
|
||||
Ui::EmptyUserpic::PaintRepliesMessages(paint, 0, 0, s, s);
|
||||
return result;
|
||||
}
|
||||
|
||||
QImage ArchiveUserpic(not_null<Data::Folder*> folder) {
|
||||
auto result = PrepareImage();
|
||||
Painter paint(&result);
|
||||
|
||||
folder->paintUserpic(paint, 0, 0, result.width());
|
||||
return result;
|
||||
}
|
||||
|
||||
QImage UnreadBadge(not_null<PeerData*> peer) {
|
||||
const auto history = peer->owner().history(peer->id);
|
||||
const auto state = history->chatListBadgesState();
|
||||
if (!state.unread) {
|
||||
return QImage();
|
||||
}
|
||||
const auto counter = (state.unreadCounter > 0)
|
||||
? QString::number(state.unreadCounter)
|
||||
: QString();
|
||||
Ui::UnreadBadgeStyle unreadSt;
|
||||
unreadSt.sizeId = Ui::UnreadBadgeSize::TouchBar;
|
||||
unreadSt.muted = state.unreadMuted;
|
||||
// Use constant values to draw badge regardless of cConfigScale().
|
||||
unreadSt.size = kUnreadBadgeSize * float64(style::DevicePixelRatio());
|
||||
unreadSt.padding = 4 * float64(style::DevicePixelRatio());
|
||||
unreadSt.font = style::font(
|
||||
9.5 * float64(style::DevicePixelRatio()),
|
||||
unreadSt.font->flags(),
|
||||
unreadSt.font->family());
|
||||
|
||||
auto result = QImage(
|
||||
QSize(kCircleDiameter, kUnreadBadgeSize) * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
result.fill(Qt::transparent);
|
||||
Painter p(&result);
|
||||
|
||||
Ui::PaintUnreadBadge(
|
||||
p,
|
||||
counter,
|
||||
result.width(),
|
||||
result.height() - unreadSt.size,
|
||||
unreadSt,
|
||||
2);
|
||||
return result;
|
||||
}
|
||||
|
||||
NSRect PeerRectByIndex(int index) {
|
||||
return NSMakeRect(
|
||||
index * (kCircleDiameter + kPinnedButtonsSpace)
|
||||
+ kPinnedButtonsLeftSkip,
|
||||
0,
|
||||
kCircleDiameter,
|
||||
kCircleDiameter);
|
||||
}
|
||||
|
||||
[[nodiscard]] Data::LastseenStatus CalculateLastseenStatus(
|
||||
not_null<PeerData*> peer) {
|
||||
if (const auto user = peer->asUser()) {
|
||||
return user->lastseen();
|
||||
}
|
||||
return Data::LastseenStatus();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
#pragma mark - PinnedDialogsPanel
|
||||
|
||||
@interface PinnedDialogsPanel()
|
||||
@end // @interface PinnedDialogsPanel
|
||||
|
||||
@implementation PinnedDialogsPanel {
|
||||
struct Pin {
|
||||
PeerData *peer = nullptr;
|
||||
Ui::PeerUserpicView userpicView;
|
||||
int index = -1;
|
||||
QImage userpic;
|
||||
QImage unreadBadge;
|
||||
|
||||
Ui::Animations::Simple shiftAnimation;
|
||||
int shift = 0;
|
||||
int finalShift = 0;
|
||||
int deltaShift = 0;
|
||||
int x = 0;
|
||||
int horizontalShift = 0;
|
||||
bool onTop = false;
|
||||
|
||||
Ui::Animations::Simple onlineAnimation;
|
||||
Data::LastseenStatus lastseen;
|
||||
};
|
||||
rpl::lifetime _lifetime;
|
||||
Main::Session *_session;
|
||||
|
||||
std::vector<std::unique_ptr<Pin>> _pins;
|
||||
QImage _savedMessages;
|
||||
QImage _repliesMessages;
|
||||
QImage _archive;
|
||||
|
||||
bool _hasArchive;
|
||||
bool _selfUnpinned;
|
||||
bool _repliesUnpinned;
|
||||
|
||||
rpl::event_stream<not_null<NSEvent*>> _touches;
|
||||
rpl::event_stream<not_null<NSPressGestureRecognizer*>> _gestures;
|
||||
|
||||
CGFloat _r, _g, _b, _a; // The online circle color.
|
||||
}
|
||||
|
||||
- (void)processHorizontalReorder {
|
||||
// This method is a simplified version of the VerticalLayoutReorder class
|
||||
// and is adapatized for horizontal use.
|
||||
enum class State : uchar {
|
||||
Started,
|
||||
Applied,
|
||||
Cancelled,
|
||||
};
|
||||
|
||||
const auto currentStart = _lifetime.make_state<int>(0);
|
||||
const auto currentPeer = _lifetime.make_state<PeerData*>(nullptr);
|
||||
const auto currentState = _lifetime.make_state<State>(State::Cancelled);
|
||||
const auto currentDesiredIndex = _lifetime.make_state<int>(-1);
|
||||
const auto waitForFinish = _lifetime.make_state<bool>(false);
|
||||
const auto isDragging = _lifetime.make_state<bool>(false);
|
||||
|
||||
const auto indexOf = [=](PeerData *p) {
|
||||
const auto i = ranges::find(_pins, p, &Pin::peer);
|
||||
Assert(i != end(_pins));
|
||||
return i - begin(_pins);
|
||||
};
|
||||
|
||||
const auto setHorizontalShift = [=](const auto &pin, int shift) {
|
||||
if (const auto delta = shift - pin->horizontalShift) {
|
||||
pin->horizontalShift = shift;
|
||||
pin->x += delta;
|
||||
|
||||
// Redraw a rectangle
|
||||
// from the beginning point of the pin movement to the end point.
|
||||
auto rect = PeerRectByIndex(indexOf(pin->peer) + [self shift]);
|
||||
const auto absDelta = std::abs(delta);
|
||||
rect.origin.x = pin->x - absDelta;
|
||||
rect.size.width += absDelta * 2;
|
||||
[self setNeedsDisplayInRect:rect];
|
||||
}
|
||||
};
|
||||
|
||||
const auto updateShift = [=](not_null<PeerData*> peer, int indexHint) {
|
||||
Expects(indexHint >= 0 && indexHint < _pins.size());
|
||||
|
||||
const auto index = (_pins[indexHint]->peer->id == peer->id)
|
||||
? indexHint
|
||||
: indexOf(peer);
|
||||
const auto &entry = _pins[index];
|
||||
entry->shift = entry->deltaShift
|
||||
+ base::SafeRound(
|
||||
entry->shiftAnimation.value(entry->finalShift));
|
||||
if (entry->deltaShift && !entry->shiftAnimation.animating()) {
|
||||
entry->finalShift += entry->deltaShift;
|
||||
entry->deltaShift = 0;
|
||||
}
|
||||
setHorizontalShift(entry, entry->shift);
|
||||
};
|
||||
|
||||
const auto moveToShift = [=](int index, int shift) {
|
||||
Core::Sandbox::Instance().customEnterFromEventLoop([=] {
|
||||
auto &entry = _pins[index];
|
||||
if (entry->finalShift + entry->deltaShift == shift) {
|
||||
return;
|
||||
}
|
||||
const auto peer = entry->peer;
|
||||
entry->shiftAnimation.start(
|
||||
[=] { updateShift(peer, index); },
|
||||
entry->finalShift,
|
||||
shift - entry->deltaShift,
|
||||
st::slideWrapDuration);
|
||||
entry->finalShift = shift - entry->deltaShift;
|
||||
});
|
||||
};
|
||||
|
||||
const auto cancelCurrentPeer = [=] {
|
||||
Expects(*currentPeer != nullptr);
|
||||
|
||||
if (*currentState == State::Started) {
|
||||
*currentState = State::Cancelled;
|
||||
}
|
||||
*currentPeer = nullptr;
|
||||
for (auto i = 0, count = int(_pins.size()); i != count; ++i) {
|
||||
moveToShift(i, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const auto cancelCurrent = [=] {
|
||||
if (*currentPeer) {
|
||||
cancelCurrentPeer();
|
||||
}
|
||||
};
|
||||
|
||||
const auto updateOrder = [=](int index, int positionX) {
|
||||
const auto shift = positionX - *currentStart;
|
||||
const auto ¤t = _pins[index];
|
||||
current->shiftAnimation.stop();
|
||||
current->shift = current->finalShift = shift;
|
||||
setHorizontalShift(current, shift);
|
||||
|
||||
const auto count = _pins.size();
|
||||
const auto currentWidth = current->userpic.width();
|
||||
const auto currentMiddle = current->x + currentWidth / 2;
|
||||
*currentDesiredIndex = index;
|
||||
if (shift > 0) {
|
||||
auto top = current->x - shift;
|
||||
for (auto next = index + 1; next != count; ++next) {
|
||||
const auto &entry = _pins[next];
|
||||
top += entry->userpic.width();
|
||||
if (currentMiddle < top) {
|
||||
moveToShift(next, 0);
|
||||
} else {
|
||||
*currentDesiredIndex = next;
|
||||
moveToShift(next, -currentWidth);
|
||||
}
|
||||
}
|
||||
for (auto prev = index - 1; prev >= 0; --prev) {
|
||||
moveToShift(prev, 0);
|
||||
}
|
||||
} else {
|
||||
for (auto next = index + 1; next != count; ++next) {
|
||||
moveToShift(next, 0);
|
||||
}
|
||||
for (auto prev = index - 1; prev >= 0; --prev) {
|
||||
const auto &entry = _pins[prev];
|
||||
if (currentMiddle >= entry->x - entry->shift + currentWidth) {
|
||||
moveToShift(prev, 0);
|
||||
} else {
|
||||
*currentDesiredIndex = prev;
|
||||
moveToShift(prev, currentWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const auto checkForStart = [=](int positionX) {
|
||||
const auto shift = positionX - *currentStart;
|
||||
const auto delta = QApplication::startDragDistance();
|
||||
*isDragging = (std::abs(shift) > delta);
|
||||
if (!*isDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
*currentState = State::Started;
|
||||
*currentStart += (shift > 0) ? delta : -delta;
|
||||
|
||||
const auto index = indexOf(*currentPeer);
|
||||
*currentDesiredIndex = index;
|
||||
|
||||
// Raise the pin.
|
||||
ranges::for_each(_pins, [=](const auto &pin) {
|
||||
pin->onTop = false;
|
||||
});
|
||||
_pins[index]->onTop = true;
|
||||
|
||||
updateOrder(index, positionX);
|
||||
};
|
||||
|
||||
const auto localGuard = _lifetime.make_state<base::has_weak_ptr>();
|
||||
|
||||
const auto finishCurrent = [=] {
|
||||
if (!*currentPeer) {
|
||||
return;
|
||||
}
|
||||
const auto index = indexOf(*currentPeer);
|
||||
if (*currentDesiredIndex == index
|
||||
|| *currentState != State::Started) {
|
||||
cancelCurrentPeer();
|
||||
return;
|
||||
}
|
||||
const auto result = *currentDesiredIndex;
|
||||
*currentState = State::Cancelled;
|
||||
*currentPeer = nullptr;
|
||||
|
||||
const auto ¤t = _pins[index];
|
||||
// Since the width of all elements is the same
|
||||
// we can use a single value.
|
||||
current->finalShift += (index - result) * current->userpic.width();
|
||||
|
||||
if (!(current->finalShift + current->deltaShift)) {
|
||||
current->shift = 0;
|
||||
setHorizontalShift(current, 0);
|
||||
}
|
||||
current->horizontalShift = current->finalShift;
|
||||
base::reorder(_pins, index, result);
|
||||
|
||||
*waitForFinish = true;
|
||||
// Call on end of an animation.
|
||||
base::call_delayed(st::slideWrapDuration, &(*localGuard), [=] {
|
||||
const auto guard = gsl::finally([=] {
|
||||
_session->data().notifyPinnedDialogsOrderUpdated();
|
||||
*waitForFinish = false;
|
||||
});
|
||||
if (index == result) {
|
||||
return;
|
||||
}
|
||||
const auto &order = _session->data().pinnedChatsOrder(nullptr);
|
||||
const auto d = (index < result) ? 1 : -1; // Direction.
|
||||
for (auto i = index; i != result; i += d) {
|
||||
_session->data().chatsList()->pinned()->reorder(
|
||||
order.at(i).history(),
|
||||
order.at(i + d).history());
|
||||
}
|
||||
_session->api().savePinnedOrder(nullptr);
|
||||
});
|
||||
|
||||
moveToShift(result, 0);
|
||||
};
|
||||
|
||||
const auto touchBegan = [=](int touchX) {
|
||||
*isDragging = false;
|
||||
cancelCurrent();
|
||||
*currentStart = touchX;
|
||||
if (_pins.size() < 2) {
|
||||
return;
|
||||
}
|
||||
const auto index = [self indexFromX:*currentStart];
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
*currentPeer = _pins[index]->peer;
|
||||
};
|
||||
|
||||
const auto touchMoved = [=](int touchX) {
|
||||
if (!*currentPeer) {
|
||||
return;
|
||||
} else if (*currentState != State::Started) {
|
||||
checkForStart(touchX);
|
||||
} else {
|
||||
updateOrder(indexOf(*currentPeer), touchX);
|
||||
}
|
||||
};
|
||||
|
||||
const auto touchEnded = [=](int touchX) {
|
||||
if (*isDragging) {
|
||||
finishCurrent();
|
||||
return;
|
||||
}
|
||||
const auto step = QApplication::startDragDistance();
|
||||
if (std::abs(*currentStart - touchX) < step) {
|
||||
[self performAction:touchX];
|
||||
}
|
||||
};
|
||||
|
||||
_gestures.events(
|
||||
) | rpl::filter([=] {
|
||||
return !(*waitForFinish);
|
||||
}) | rpl::on_next([=](
|
||||
not_null<NSPressGestureRecognizer*> gesture) {
|
||||
const auto currentPosition = [gesture locationInView:self].x;
|
||||
|
||||
switch ([gesture state]) {
|
||||
case NSGestureRecognizerStateBegan:
|
||||
return touchBegan(currentPosition);
|
||||
case NSGestureRecognizerStateChanged:
|
||||
return touchMoved(currentPosition);
|
||||
case NSGestureRecognizerStateCancelled:
|
||||
case NSGestureRecognizerStateEnded:
|
||||
return touchEnded(currentPosition);
|
||||
}
|
||||
}, _lifetime);
|
||||
|
||||
_session->data().pinnedDialogsOrderUpdated(
|
||||
) | rpl::on_next(cancelCurrent, _lifetime);
|
||||
|
||||
_lifetime.add([=] {
|
||||
for (const auto &pin : _pins) {
|
||||
pin->shiftAnimation.stop();
|
||||
pin->onlineAnimation.stop();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
- (id)init:(not_null<Main::Session*>)session
|
||||
destroyEvent:(rpl::producer<>)touchBarSwitches {
|
||||
self = [super init];
|
||||
_session = session;
|
||||
_hasArchive = _selfUnpinned = false;
|
||||
_savedMessages = SavedMessagesUserpic();
|
||||
_repliesMessages = RepliesMessagesUserpic();
|
||||
|
||||
auto *gesture = [[[NSPressGestureRecognizer alloc]
|
||||
initWithTarget:self
|
||||
action:@selector(gestureHandler:)] autorelease];
|
||||
gesture.allowedTouchTypes = NSTouchTypeMaskDirect;
|
||||
gesture.minimumPressDuration = 0;
|
||||
gesture.allowableMovement = 0;
|
||||
[self addGestureRecognizer:gesture];
|
||||
|
||||
// For some reason, sometimes a parent deallocates not immediately,
|
||||
// but only after the user's input (mouse movement, key pressing, etc.).
|
||||
// So we have to use a custom event to destroy the current lifetime
|
||||
// manually, before it leads to crashes.
|
||||
std::move(
|
||||
touchBarSwitches
|
||||
) | rpl::on_next([=] {
|
||||
_lifetime.destroy();
|
||||
}, _lifetime);
|
||||
|
||||
using UpdateFlag = Data::PeerUpdate::Flag;
|
||||
|
||||
const auto downloadLifetime = _lifetime.make_state<rpl::lifetime>();
|
||||
const auto peerChangedLifetime = _lifetime.make_state<rpl::lifetime>();
|
||||
const auto lastDialogsCount = _lifetime.make_state<rpl::variable<int>>(0);
|
||||
auto &&peers = ranges::views::all(
|
||||
_pins
|
||||
) | ranges::views::transform(&Pin::peer);
|
||||
|
||||
const auto updatePanelSize = [=] {
|
||||
const auto size = lastDialogsCount->current();
|
||||
if (self.image) {
|
||||
[self.image release];
|
||||
}
|
||||
// TODO: replace it with NSLayoutConstraint.
|
||||
self.image = [[NSImage alloc] initWithSize:NSMakeSize(
|
||||
size * (kCircleDiameter + kPinnedButtonsSpace)
|
||||
+ kPinnedButtonsLeftSkip
|
||||
- kPinnedButtonsSpace / 2,
|
||||
kCircleDiameter)];
|
||||
};
|
||||
lastDialogsCount->changes(
|
||||
) | rpl::on_next(updatePanelSize, _lifetime);
|
||||
const auto singleUserpic = [=](const auto &pin) {
|
||||
if (IsSelfPeer(pin->peer)) {
|
||||
pin->userpic = _savedMessages;
|
||||
return;
|
||||
} else if (IsRepliesPeer(pin->peer)) {
|
||||
pin->userpic = _repliesMessages;
|
||||
return;
|
||||
}
|
||||
auto userpic = PrepareImage();
|
||||
Painter p(&userpic);
|
||||
|
||||
pin->peer->paintUserpic(p, pin->userpicView, 0, 0, userpic.width());
|
||||
userpic.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
pin->userpic = std::move(userpic);
|
||||
const auto userpicIndex = pin->index + [self shift];
|
||||
[self setNeedsDisplayInRect:PeerRectByIndex(userpicIndex)];
|
||||
};
|
||||
const auto updateUserpics = [=] {
|
||||
ranges::for_each(_pins, singleUserpic);
|
||||
*lastDialogsCount = [self shift] + int(std::size(_pins));
|
||||
};
|
||||
const auto updateBadge = [=](const auto &pin) {
|
||||
const auto peer = pin->peer;
|
||||
if (IsSelfPeer(peer)) {
|
||||
return;
|
||||
}
|
||||
pin->unreadBadge = UnreadBadge(peer);
|
||||
|
||||
const auto userpicIndex = pin->index + [self shift];
|
||||
[self setNeedsDisplayInRect:PeerRectByIndex(userpicIndex)];
|
||||
};
|
||||
const auto listenToDownloaderFinished = [=] {
|
||||
_session->downloaderTaskFinished(
|
||||
) | rpl::on_next([=] {
|
||||
const auto all = ranges::all_of(_pins, [=](const auto &pin) {
|
||||
return (!pin->peer->hasUserpic())
|
||||
|| (!Ui::PeerUserpicLoading(pin->userpicView));
|
||||
});
|
||||
if (all) {
|
||||
downloadLifetime->destroy();
|
||||
}
|
||||
updateUserpics();
|
||||
}, *downloadLifetime);
|
||||
};
|
||||
const auto processOnline = [=](const auto &pin) {
|
||||
// TODO: this should be replaced
|
||||
// with the global application timer for online statuses.
|
||||
const auto onlineChanges
|
||||
= peerChangedLifetime->make_state<rpl::event_stream<PeerData*>>();
|
||||
const auto peer = pin->peer;
|
||||
const auto onlineTimer = peerChangedLifetime->make_state<base::Timer>(
|
||||
[=] { onlineChanges->fire_copy({ peer }); });
|
||||
|
||||
const auto callTimer = [=](const auto &pin) {
|
||||
onlineTimer->cancel();
|
||||
if (const auto till = pin->lastseen.onlineTill()) {
|
||||
const auto left = till - base::unixtime::now();
|
||||
if (left > 0) {
|
||||
onlineTimer->callOnce(std::min(86400, left)
|
||||
* crl::time(1000));
|
||||
}
|
||||
}
|
||||
};
|
||||
callTimer(pin);
|
||||
|
||||
using PeerUpdate = Data::PeerUpdate;
|
||||
auto to_peer = rpl::map([=](const PeerUpdate &update) -> PeerData* {
|
||||
return update.peer;
|
||||
});
|
||||
rpl::merge(
|
||||
_session->changes().peerUpdates(
|
||||
pin->peer,
|
||||
UpdateFlag::OnlineStatus) | to_peer,
|
||||
onlineChanges->events()
|
||||
) | rpl::on_next([=](PeerData *peer) {
|
||||
const auto it = ranges::find(_pins, peer, &Pin::peer);
|
||||
if (it == end(_pins)) {
|
||||
return;
|
||||
}
|
||||
const auto &pin = *it;
|
||||
pin->lastseen = CalculateLastseenStatus(pin->peer);
|
||||
|
||||
callTimer(pin);
|
||||
|
||||
if (![NSApplication sharedApplication].active) {
|
||||
pin->onlineAnimation.stop();
|
||||
return;
|
||||
}
|
||||
const auto now = base::unixtime::now();
|
||||
const auto online = pin->lastseen.isOnline(now);
|
||||
if (pin->onlineAnimation.animating()) {
|
||||
pin->onlineAnimation.change(
|
||||
online ? 1. : 0.,
|
||||
st::dialogsOnlineBadgeDuration);
|
||||
} else {
|
||||
const auto s = kOnlineCircleSize + kOnlineCircleStrokeWidth;
|
||||
const auto index = pin->index;
|
||||
Core::Sandbox::Instance().customEnterFromEventLoop([=] {
|
||||
_pins[index]->onlineAnimation.start(
|
||||
[=] {
|
||||
[self setNeedsDisplayInRect:NSMakeRect(
|
||||
_pins[index]->x + kCircleDiameter - s,
|
||||
0,
|
||||
s,
|
||||
s)];
|
||||
},
|
||||
online ? 0. : 1.,
|
||||
online ? 1. : 0.,
|
||||
st::dialogsOnlineBadgeDuration);
|
||||
});
|
||||
}
|
||||
}, *peerChangedLifetime);
|
||||
};
|
||||
|
||||
const auto updatePinnedChats = [=] {
|
||||
_pins = ranges::views::zip(
|
||||
_session->data().pinnedChatsOrder(nullptr),
|
||||
ranges::views::ints(0, ranges::unreachable)
|
||||
) | ranges::views::transform([=](const auto &pair) {
|
||||
const auto index = pair.second;
|
||||
auto peer = pair.first.history()->peer;
|
||||
auto view = peer->createUserpicView();
|
||||
return std::make_unique<Pin>(Pin{
|
||||
.peer = std::move(peer),
|
||||
.userpicView = std::move(view),
|
||||
.index = index,
|
||||
.lastseen = CalculateLastseenStatus(peer),
|
||||
});
|
||||
}) | ranges::to_vector;
|
||||
_selfUnpinned = ranges::none_of(peers, &PeerData::isSelf);
|
||||
_repliesUnpinned = ranges::none_of(peers, &PeerData::isRepliesChat);
|
||||
|
||||
peerChangedLifetime->destroy();
|
||||
for (const auto &pin : _pins) {
|
||||
const auto peer = pin->peer;
|
||||
const auto index = pin->index;
|
||||
|
||||
_session->changes().peerUpdates(
|
||||
peer,
|
||||
UpdateFlag::Photo
|
||||
) | rpl::on_next([=](const Data::PeerUpdate &update) {
|
||||
_pins[index]->userpicView = update.peer->createUserpicView();
|
||||
listenToDownloaderFinished();
|
||||
}, *peerChangedLifetime);
|
||||
|
||||
if (const auto user = peer->asUser()) {
|
||||
if (!user->isServiceUser()
|
||||
&& !user->isBot()
|
||||
&& !peer->isSelf()) {
|
||||
processOnline(pin);
|
||||
}
|
||||
}
|
||||
|
||||
rpl::merge(
|
||||
_session->changes().historyUpdates(
|
||||
_session->data().history(peer),
|
||||
Data::HistoryUpdate::Flag::UnreadView
|
||||
) | rpl::to_empty,
|
||||
_session->changes().peerFlagsValue(
|
||||
peer,
|
||||
UpdateFlag::Notifications
|
||||
) | rpl::to_empty
|
||||
) | rpl::on_next([=] {
|
||||
updateBadge(_pins[index]);
|
||||
}, *peerChangedLifetime);
|
||||
}
|
||||
|
||||
updateUserpics();
|
||||
};
|
||||
|
||||
rpl::single(rpl::empty) | rpl::then(
|
||||
_session->data().pinnedDialogsOrderUpdated()
|
||||
) | rpl::on_next(updatePinnedChats, _lifetime);
|
||||
|
||||
const auto ArchiveId = Data::Folder::kId;
|
||||
rpl::single(
|
||||
_session->data().folderLoaded(ArchiveId)
|
||||
) | rpl::then(
|
||||
_session->data().chatsListChanges()
|
||||
) | rpl::filter([](Data::Folder *folder) {
|
||||
return folder && (folder->id() == ArchiveId);
|
||||
}) | rpl::on_next([=](Data::Folder *folder) {
|
||||
_hasArchive = !folder->chatsList()->empty();
|
||||
if (_archive.isNull()) {
|
||||
_archive = ArchiveUserpic(folder);
|
||||
}
|
||||
updateUserpics();
|
||||
}, _lifetime);
|
||||
|
||||
const auto updateOnlineColor = [=] {
|
||||
auto r = 0, g = 0, b = 0, a = 0;
|
||||
st::dialogsOnlineBadgeFg->c.getRgb(&r, &g, &b, &a);
|
||||
_r = r / 255.;
|
||||
_g = g / 255.;
|
||||
_b = b / 255.;
|
||||
_a = a / 255.;
|
||||
};
|
||||
updateOnlineColor();
|
||||
|
||||
const auto localGuard = _lifetime.make_state<base::has_weak_ptr>();
|
||||
|
||||
style::PaletteChanged(
|
||||
) | rpl::on_next([=] {
|
||||
crl::on_main(&(*localGuard), [=] {
|
||||
updateOnlineColor();
|
||||
if (const auto f = _session->data().folderLoaded(ArchiveId)) {
|
||||
_archive = ArchiveUserpic(f);
|
||||
}
|
||||
_savedMessages = SavedMessagesUserpic();
|
||||
_repliesMessages = RepliesMessagesUserpic();
|
||||
updateUserpics();
|
||||
});
|
||||
}, _lifetime);
|
||||
|
||||
listenToDownloaderFinished();
|
||||
[self processHorizontalReorder];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
if (self.image) {
|
||||
[self.image release];
|
||||
}
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (int)shift {
|
||||
return (_hasArchive ? 1 : 0) + (_selfUnpinned ? 1 : 0);
|
||||
}
|
||||
|
||||
- (void)gestureHandler:(NSPressGestureRecognizer*)gesture {
|
||||
_gestures.fire(std::move(gesture));
|
||||
}
|
||||
|
||||
- (int)indexFromX:(int)position {
|
||||
const auto x = position
|
||||
- kPinnedButtonsLeftSkip
|
||||
+ kPinnedButtonsSpace / 2;
|
||||
return x / (kCircleDiameter + kPinnedButtonsSpace) - [self shift];
|
||||
}
|
||||
|
||||
- (void)performAction:(int)xPosition {
|
||||
const auto index = [self indexFromX:xPosition];
|
||||
const auto peer = (index < 0 || index >= int(std::size(_pins)))
|
||||
? nullptr
|
||||
: _pins[index]->peer;
|
||||
if (!peer && !_hasArchive && !_selfUnpinned) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto active = Core::App().activePrimaryWindow();
|
||||
const auto controller = active ? active->sessionController() : nullptr;
|
||||
const auto openFolder = [=] {
|
||||
const auto folder = _session->data().folderLoaded(Data::Folder::kId);
|
||||
if (folder && controller) {
|
||||
controller->openFolder(folder);
|
||||
}
|
||||
};
|
||||
Core::Sandbox::Instance().customEnterFromEventLoop([=] {
|
||||
if (_hasArchive && (index == (_selfUnpinned ? -2 : -1))) {
|
||||
openFolder();
|
||||
} else {
|
||||
controller->showPeerHistory((_selfUnpinned && index == -1)
|
||||
? _session->user()
|
||||
: peer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (QImage)imageToDraw:(int)i {
|
||||
Expects(i < int(std::size(_pins)));
|
||||
if (i < 0) {
|
||||
if (_hasArchive && (i == -[self shift])) {
|
||||
return _archive;
|
||||
} else if (_selfUnpinned) {
|
||||
return _savedMessages;
|
||||
} else if (_repliesUnpinned) {
|
||||
return _repliesMessages;
|
||||
}
|
||||
}
|
||||
return _pins[i]->userpic;
|
||||
}
|
||||
|
||||
- (void)drawSinglePin:(int)i rect:(NSRect)dirtyRect {
|
||||
const auto rect = [&] {
|
||||
auto rect = PeerRectByIndex(i + [self shift]);
|
||||
if (i < 0) {
|
||||
return rect;
|
||||
}
|
||||
auto &pin = _pins[i];
|
||||
// We can have x = 0 when the pin is dragged.
|
||||
rect.origin.x = ((!pin->x && !pin->onTop) ? rect.origin.x : pin->x);
|
||||
pin->x = rect.origin.x;
|
||||
return rect;
|
||||
}();
|
||||
if (!NSIntersectsRect(rect, dirtyRect)) {
|
||||
return;
|
||||
}
|
||||
CGContextRef context = [[NSGraphicsContext currentContext] CGContext];
|
||||
{
|
||||
CGImageRef image = ([self imageToDraw:i]).toCGImage();
|
||||
CGContextDrawImage(context, rect, image);
|
||||
CGImageRelease(image);
|
||||
}
|
||||
|
||||
if (i >= 0) {
|
||||
const auto &pin = _pins[i];
|
||||
const auto rectRight = NSMaxX(rect);
|
||||
if (!pin->unreadBadge.isNull()) {
|
||||
CGImageRef image = pin->unreadBadge.toCGImage();
|
||||
const auto w = CGImageGetWidth(image)
|
||||
/ float64(style::DevicePixelRatio());
|
||||
const auto borderRect = CGRectMake(
|
||||
rectRight - w,
|
||||
0,
|
||||
w,
|
||||
CGImageGetHeight(image)
|
||||
/ float64(style::DevicePixelRatio()));
|
||||
CGContextDrawImage(context, borderRect, image);
|
||||
CGImageRelease(image);
|
||||
return;
|
||||
}
|
||||
const auto now = base::unixtime::now();
|
||||
const auto online = pin->lastseen.isOnline(now);
|
||||
const auto value = pin->onlineAnimation.value(online ? 1. : 0.);
|
||||
if (value < 0.05) {
|
||||
return;
|
||||
}
|
||||
const auto lineWidth = kOnlineCircleStrokeWidth;
|
||||
const auto circleSize = kOnlineCircleSize;
|
||||
const auto progress = value * circleSize;
|
||||
const auto diff = (circleSize - progress) / 2;
|
||||
const auto borderRect = CGRectMake(
|
||||
rectRight - circleSize + diff - lineWidth / 2,
|
||||
diff,
|
||||
progress,
|
||||
progress);
|
||||
|
||||
CGContextSetRGBStrokeColor(context, 0, 0, 0, 1.0);
|
||||
CGContextSetRGBFillColor(context, _r, _g, _b, _a);
|
||||
CGContextSetLineWidth(context, lineWidth);
|
||||
CGContextFillEllipseInRect(context, borderRect);
|
||||
CGContextStrokeEllipseInRect(context, borderRect);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)drawRect:(NSRect)dirtyRect {
|
||||
const auto shift = [self shift];
|
||||
if (_pins.empty() && !shift) {
|
||||
return;
|
||||
}
|
||||
auto indexToTop = -1;
|
||||
const auto guard = gsl::finally([&] {
|
||||
if (indexToTop >= 0) {
|
||||
[self drawSinglePin:indexToTop rect:dirtyRect];
|
||||
}
|
||||
});
|
||||
for (auto i = -shift; i < int(std::size(_pins)); i++) {
|
||||
if (i >= 0 && _pins[i]->onTop && (indexToTop < 0)) {
|
||||
indexToTop = i;
|
||||
continue;
|
||||
}
|
||||
[self drawSinglePin:i rect:dirtyRect];
|
||||
}
|
||||
}
|
||||
|
||||
@end // @@implementation PinnedDialogsPanel
|
||||
@@ -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
|
||||
@@ -0,0 +1,716 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
#include "api/api_common.h"
|
||||
#include "api/api_sending.h"
|
||||
#include "base/call_delayed.h"
|
||||
#include "base/platform/mac/base_utilities_mac.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/painter.h"
|
||||
#include "chat_helpers/emoji_list_widget.h"
|
||||
#include "core/sandbox.h"
|
||||
#include "core/application.h"
|
||||
#include "core/core_settings.h"
|
||||
#include "data/data_chat_participant_status.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_document_media.h"
|
||||
#include "data/data_file_origin.h"
|
||||
#include "data/data_forum_topic.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/fields/input_field.h"
|
||||
#include "window/section_widget.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>
|
||||
|
||||
#include <QtWidgets/QTextEdit>
|
||||
|
||||
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 || !image.isNull()) {
|
||||
return;
|
||||
}
|
||||
const auto sticker = mediaView->getStickerSmall();
|
||||
if (!sticker) {
|
||||
return;
|
||||
}
|
||||
const auto size = sticker->size()
|
||||
.scaled(kCircleDiameter, kCircleDiameter, Qt::KeepAspectRatio);
|
||||
image = sticker->pixSingle(
|
||||
size,
|
||||
{ .outer = { kCircleDiameter, kCircleDiameter } }).toImage();
|
||||
}
|
||||
|
||||
bool isStickerLoaded() const {
|
||||
return !image.isNull();
|
||||
}
|
||||
|
||||
QString title = QString();
|
||||
|
||||
DocumentData *document = nullptr;
|
||||
std::shared_ptr<Data::DocumentMedia> mediaView = nullptr;
|
||||
QImage image;
|
||||
|
||||
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;
|
||||
using Platform::Q2NSImage;
|
||||
|
||||
NSImage *CreateNSImageFromEmoji(EmojiPtr emoji) {
|
||||
auto image = QImage(
|
||||
QSize(kIdealIconSize, kIdealIconSize) * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
image.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
image.fill(Qt::black);
|
||||
{
|
||||
Painter paint(&image);
|
||||
PainterHighQualityEnabler hq(paint);
|
||||
Ui::Emoji::Draw(
|
||||
paint,
|
||||
emoji,
|
||||
Ui::Emoji::GetSizeTouchbar(),
|
||||
0,
|
||||
0);
|
||||
}
|
||||
return Q2NSImage(image);
|
||||
}
|
||||
|
||||
auto ActiveChat(not_null<Window::Controller*> controller) {
|
||||
if (const auto sessionController = controller->sessionController()) {
|
||||
return sessionController->activeChatCurrent();
|
||||
}
|
||||
return Dialogs::Key();
|
||||
}
|
||||
|
||||
bool CanSendToActiveChat(
|
||||
not_null<Window::Controller*> controller,
|
||||
ChatRestriction right) {
|
||||
if (const auto topic = ActiveChat(controller).topic()) {
|
||||
return Data::CanSend(topic, right);
|
||||
} else if (const auto history = ActiveChat(controller).history()) {
|
||||
return Data::CanSend(history->peer, right);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<QString> RestrictionToSend(
|
||||
not_null<Window::Controller*> controller,
|
||||
ChatRestriction right) {
|
||||
if (const auto peer = ActiveChat(controller).peer()) {
|
||||
if (const auto error = Data::RestrictionError(peer, right)) {
|
||||
return *error;
|
||||
}
|
||||
}
|
||||
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 & Data::StickersSetFlag::Archived) {
|
||||
return;
|
||||
}
|
||||
if (!(set->flags & Data::StickersSetFlag::Installed)) {
|
||||
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));
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] EmojiPack RecentEmojiSection() {
|
||||
const auto list = Core::App().settings().recentEmoji();
|
||||
auto result = EmojiPack();
|
||||
result.reserve(list.size());
|
||||
for (const auto &emoji : list) {
|
||||
if (const auto one = std::get_if<EmojiPtr>(&emoji.id.data)) {
|
||||
result.push_back(*one);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
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)
|
||||
? RecentEmojiSection()
|
||||
: 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];
|
||||
|
||||
const auto kRight = ChatRestriction::SendStickers;
|
||||
if (const auto error = RestrictionToSend(_controller, kRight)) {
|
||||
_error = std::make_unique<PickerScrubberItem>(
|
||||
tr::lng_restricted_send_stickers_all(tr::now));
|
||||
}
|
||||
} else {
|
||||
const auto kRight = ChatRestriction::SendOther;
|
||||
if (const auto error = RestrictionToSend(_controller, kRight)) {
|
||||
_error = std::make_unique<PickerScrubberItem>(
|
||||
tr::lng_restricted_send_message_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 = Q2NSImage(item.image);
|
||||
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 {
|
||||
scrubber.selectedIndex = -1;
|
||||
const auto sticker = _itemsDataSource->at(index, _type);
|
||||
const auto document = sticker.document;
|
||||
const auto emoji = sticker.emoji;
|
||||
const auto kRight = document
|
||||
? ChatRestriction::SendStickers
|
||||
: ChatRestriction::SendOther;
|
||||
if (!CanSendToActiveChat(_controller, kRight) || _error) {
|
||||
return;
|
||||
}
|
||||
auto callback = [=] {
|
||||
if (document) {
|
||||
if (const auto error = RestrictionToSend(_controller, kRight)) {
|
||||
_controller->show(Ui::MakeInformBox(*error));
|
||||
return true;
|
||||
} else if (Window::ShowSendPremiumError(_controller->sessionController(), document)) {
|
||||
return true;
|
||||
}
|
||||
Api::SendExistingDocument(
|
||||
Api::MessageToSend(
|
||||
Api::SendAction(ActiveChat(_controller).history())),
|
||||
document);
|
||||
return true;
|
||||
} else if (emoji) {
|
||||
if (const auto error = RestrictionToSend(_controller, kRight)) {
|
||||
_controller->show(Ui::MakeInformBox(*error));
|
||||
return true;
|
||||
} else if (const auto inputField = qobject_cast<QTextEdit*>(
|
||||
QApplication::focusWidget())) {
|
||||
Ui::InsertEmojiAtCursor(inputField->textCursor(), emoji);
|
||||
Core::App().settings().incrementRecentEmoji({ 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;
|
||||
|
||||
controller->sessionController()->activeChatValue(
|
||||
) | rpl::map([](Dialogs::Key k) {
|
||||
const auto topic = k.topic();
|
||||
const auto peer = k.peer();
|
||||
const auto right = ChatRestriction::SendStickers;
|
||||
return peer
|
||||
&& (topic
|
||||
? Data::CanSend(topic, right)
|
||||
: Data::CanSend(peer, right));
|
||||
}) | rpl::distinct_until_changed(
|
||||
) | rpl::on_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(
|
||||
Data::StickersType::Stickers),
|
||||
_session->data().stickers().recentUpdated(
|
||||
Data::StickersType::Stickers)
|
||||
) | rpl::map_to(ScrubberItemType::Sticker),
|
||||
rpl::merge(
|
||||
Core::App().settings().recentEmojiUpdated(),
|
||||
Ui::Emoji::Updated()
|
||||
) | rpl::map_to(ScrubberItemType::Emoji)
|
||||
) | rpl::on_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::on_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::on_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
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
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/NSTouchBar.h>
|
||||
|
||||
API_AVAILABLE(macos(10.12.2))
|
||||
@interface TouchBarAudioPlayer : NSTouchBar<NSTouchBarDelegate>
|
||||
- (rpl::producer<>)closeRequests;
|
||||
@end
|
||||
176
Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_audio.mm
Normal file
176
Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_audio.mm
Normal file
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
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/mac_touchbar_audio.h"
|
||||
|
||||
#include "media/audio/media_audio.h"
|
||||
#include "media/player/media_player_instance.h"
|
||||
#include "platform/mac/touchbar/mac_touchbar_common.h"
|
||||
#include "platform/mac/touchbar/mac_touchbar_controls.h"
|
||||
#include "styles/style_media_player.h"
|
||||
|
||||
#import <AppKit/NSButton.h>
|
||||
#import <AppKit/NSCustomTouchBarItem.h>
|
||||
#import <AppKit/NSSlider.h>
|
||||
#import <AppKit/NSSliderTouchBarItem.h>
|
||||
|
||||
using TouchBar::kCircleDiameter;
|
||||
using TouchBar::CreateNSImageFromStyleIcon;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kSongType = AudioMsgId::Type::Song;
|
||||
|
||||
const auto *kCustomizationIdPlayer = @"telegram.touchbar";
|
||||
|
||||
inline NSTouchBarItemIdentifier Format(NSString *s) {
|
||||
return [NSString stringWithFormat:@"%@.%@", kCustomizationIdPlayer, s];
|
||||
}
|
||||
const auto kSeekBarItemIdentifier = Format(@"seekbar");
|
||||
const auto kPlayItemIdentifier = Format(@"play");
|
||||
const auto kNextItemIdentifier = Format(@"nextItem");
|
||||
const auto kPreviousItemIdentifier = Format(@"previousItem");
|
||||
const auto kClosePlayerItemIdentifier = Format(@"closePlayer");
|
||||
const auto kCurrentPositionItemIdentifier = Format(@"currentPosition");
|
||||
|
||||
} // namespace
|
||||
|
||||
#pragma mark - TouchBarAudioPlayer
|
||||
|
||||
@interface TouchBarAudioPlayer()
|
||||
@end // @interface TouchBarAudioPlayer
|
||||
|
||||
@implementation TouchBarAudioPlayer {
|
||||
rpl::event_stream<> _closeRequests;
|
||||
rpl::producer< Media::Player::TrackState> _trackState;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
}
|
||||
|
||||
- (id)init {
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
self.delegate = self;
|
||||
self.customizationIdentifier = kCustomizationIdPlayer.lowercaseString;
|
||||
self.defaultItemIdentifiers = @[
|
||||
kPlayItemIdentifier,
|
||||
kPreviousItemIdentifier,
|
||||
kNextItemIdentifier,
|
||||
kSeekBarItemIdentifier,
|
||||
kClosePlayerItemIdentifier];
|
||||
self.customizationAllowedItemIdentifiers = @[
|
||||
kPlayItemIdentifier,
|
||||
kPreviousItemIdentifier,
|
||||
kNextItemIdentifier,
|
||||
kCurrentPositionItemIdentifier,
|
||||
kSeekBarItemIdentifier,
|
||||
kClosePlayerItemIdentifier];
|
||||
|
||||
_trackState = Media::Player::instance()->updatedNotifier(
|
||||
) | rpl::filter([=](const Media::Player::TrackState &state) {
|
||||
return state.id.type() == kSongType;
|
||||
});
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSTouchBarItem*)touchBar:(NSTouchBar*)touchBar
|
||||
makeItemForIdentifier:(NSTouchBarItemIdentifier)itemId {
|
||||
if (!touchBar) {
|
||||
return nil;
|
||||
}
|
||||
const auto mediaPlayer = Media::Player::instance();
|
||||
const auto isEqual = [&](NSString *string) {
|
||||
return [itemId isEqualToString:string];
|
||||
};
|
||||
|
||||
if (isEqual(kSeekBarItemIdentifier)) {
|
||||
auto *item = TouchBar::CreateTouchBarSlider(
|
||||
itemId,
|
||||
_lifetime,
|
||||
[=](bool touchUp, double value, double duration) {
|
||||
if (touchUp) {
|
||||
mediaPlayer->finishSeeking(kSongType, value);
|
||||
} else {
|
||||
mediaPlayer->startSeeking(kSongType);
|
||||
}
|
||||
},
|
||||
rpl::duplicate(_trackState));
|
||||
return [item autorelease];
|
||||
} else if (isEqual(kNextItemIdentifier)
|
||||
|| isEqual(kPreviousItemIdentifier)) {
|
||||
const auto isNext = isEqual(kNextItemIdentifier);
|
||||
auto *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:itemId];
|
||||
|
||||
auto *button = TouchBar::CreateTouchBarButton(
|
||||
isNext
|
||||
? st::touchBarIconPlayerNext
|
||||
: st::touchBarIconPlayerPrevious,
|
||||
_lifetime,
|
||||
[=] { isNext // TODO
|
||||
? mediaPlayer->next(kSongType)
|
||||
: mediaPlayer->previous(kSongType); });
|
||||
rpl::duplicate(
|
||||
_trackState
|
||||
) | rpl::on_next([=] {
|
||||
const auto newValue = isNext
|
||||
? mediaPlayer->nextAvailable(kSongType)
|
||||
: mediaPlayer->previousAvailable(kSongType);
|
||||
if (button.enabled != newValue) {
|
||||
button.enabled = newValue;
|
||||
}
|
||||
}, _lifetime);
|
||||
|
||||
item.view = button;
|
||||
item.customizationLabel = [NSString
|
||||
stringWithFormat:@"%@ Playlist Item",
|
||||
isNext ? @"Next" : @"Previous"];
|
||||
return [item autorelease];
|
||||
} else if (isEqual(kPlayItemIdentifier)) {
|
||||
auto *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:itemId];
|
||||
|
||||
auto *button = TouchBar::CreateTouchBarButtonWithTwoStates(
|
||||
st::touchBarIconPlayerPause,
|
||||
st::touchBarIconPlayerPlay,
|
||||
_lifetime,
|
||||
[=](bool value) { mediaPlayer->playPause(kSongType); },
|
||||
false,
|
||||
rpl::duplicate(
|
||||
_trackState
|
||||
) | rpl::map([](const auto &state) {
|
||||
return (state.state == Media::Player::State::Playing);
|
||||
}) | rpl::distinct_until_changed());
|
||||
|
||||
item.view = button;
|
||||
item.customizationLabel = @"Play/Pause";
|
||||
return [item autorelease];
|
||||
} else if (isEqual(kClosePlayerItemIdentifier)) {
|
||||
auto *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:itemId];
|
||||
auto *button = TouchBar::CreateTouchBarButton(
|
||||
st::touchBarIconPlayerClose,
|
||||
_lifetime,
|
||||
[=] { _closeRequests.fire({}); });
|
||||
|
||||
item.view = button;
|
||||
item.customizationLabel = @"Close Player";
|
||||
return [item autorelease];
|
||||
} else if (isEqual(kCurrentPositionItemIdentifier)) {
|
||||
auto *item = TouchBar::CreateTouchBarTrackPosition(
|
||||
itemId,
|
||||
rpl::duplicate(_trackState));
|
||||
return [item autorelease];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (rpl::producer<>)closeRequests {
|
||||
return _closeRequests.events();
|
||||
}
|
||||
|
||||
@end // @implementation TouchBarAudioPlayer
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
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/NSImage.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
namespace TouchBar {
|
||||
|
||||
constexpr auto kCircleDiameter = 30;
|
||||
|
||||
template <typename Callable>
|
||||
void CustomEnterToCocoaEventLoop(Callable callable) {
|
||||
id block = [^{ callable(); } copy]; // Don't forget to -release.
|
||||
[block
|
||||
performSelectorOnMainThread:@selector(invoke)
|
||||
withObject:nil
|
||||
waitUntilDone:true];
|
||||
// [block performSelector:@selector(invoke) withObject:nil afterDelay:d];
|
||||
[block release];
|
||||
}
|
||||
|
||||
int WidthFromString(NSString *s);
|
||||
|
||||
NSImage *CreateNSImageFromStyleIcon(const style::icon &icon, int size);
|
||||
|
||||
} // namespace TouchBar
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
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/mac_touchbar_common.h"
|
||||
|
||||
#include "base/platform/mac/base_utilities_mac.h"
|
||||
|
||||
#import <AppKit/NSTextField.h>
|
||||
|
||||
namespace TouchBar {
|
||||
|
||||
int WidthFromString(NSString *s) {
|
||||
return (int)ceil(
|
||||
[[NSTextField labelWithString:s] frame].size.width) * 1.2;
|
||||
}
|
||||
|
||||
NSImage *CreateNSImageFromStyleIcon(const style::icon &icon, int size) {
|
||||
auto instance = icon.instance(QColor(255, 255, 255, 255), 100);
|
||||
instance.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
NSImage *image = Platform::Q2NSImage(instance);
|
||||
[image setSize:NSMakeSize(size, size)];
|
||||
return image;
|
||||
}
|
||||
|
||||
} // namespace TouchBar
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
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
|
||||
|
||||
namespace Media {
|
||||
namespace Player {
|
||||
struct TrackState;
|
||||
} // namespace Player
|
||||
} // namespace Media
|
||||
|
||||
@class NSButton;
|
||||
@class NSCustomTouchBarItem;
|
||||
@class NSImage;
|
||||
@class NSSliderTouchBarItem;
|
||||
|
||||
namespace TouchBar {
|
||||
|
||||
[[nodiscard]] API_AVAILABLE(macos(10.12.2))
|
||||
NSButton *CreateTouchBarButton(
|
||||
NSImage *image,
|
||||
rpl::lifetime &lifetime,
|
||||
Fn<void()> callback);
|
||||
|
||||
[[nodiscard]] API_AVAILABLE(macos(10.12.2))
|
||||
NSButton *CreateTouchBarButton(
|
||||
const style::icon &icon,
|
||||
rpl::lifetime &lifetime,
|
||||
Fn<void()> callback);
|
||||
|
||||
[[nodiscard]] API_AVAILABLE(macos(10.12.2))
|
||||
NSButton *CreateTouchBarButtonWithTwoStates(
|
||||
NSImage *icon1,
|
||||
NSImage *icon2,
|
||||
rpl::lifetime &lifetime,
|
||||
Fn<void(bool)> callback,
|
||||
bool firstState,
|
||||
rpl::producer<bool> stateChanged = rpl::never<bool>());
|
||||
|
||||
[[nodiscard]] API_AVAILABLE(macos(10.12.2))
|
||||
NSButton *CreateTouchBarButtonWithTwoStates(
|
||||
const style::icon &icon1,
|
||||
const style::icon &icon2,
|
||||
rpl::lifetime &lifetime,
|
||||
Fn<void(bool)> callback,
|
||||
bool firstState,
|
||||
rpl::producer<bool> stateChanged = rpl::never<bool>());
|
||||
|
||||
[[nodiscard]] API_AVAILABLE(macos(10.12.2))
|
||||
NSSliderTouchBarItem *CreateTouchBarSlider(
|
||||
NSString *itemId,
|
||||
rpl::lifetime &lifetime,
|
||||
Fn<void(bool, double, double)> callback,
|
||||
rpl::producer<Media::Player::TrackState> stateChanged);
|
||||
|
||||
[[nodiscard]] API_AVAILABLE(macos(10.12.2))
|
||||
NSCustomTouchBarItem *CreateTouchBarTrackPosition(
|
||||
NSString *itemId,
|
||||
rpl::producer<Media::Player::TrackState> stateChanged);
|
||||
|
||||
} // namespace TouchBar
|
||||
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
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/mac_touchbar_controls.h"
|
||||
|
||||
#include "base/platform/mac/base_utilities_mac.h" // Q2NSString()
|
||||
#include "core/sandbox.h" // Sandbox::customEnterFromEventLoop()
|
||||
#include "ui/text/format_values.h" // Ui::FormatDurationText()
|
||||
#include "media/audio/media_audio.h"
|
||||
#include "platform/mac/touchbar/mac_touchbar_common.h"
|
||||
|
||||
#import <AppKit/NSButton.h>
|
||||
#import <AppKit/NSCustomTouchBarItem.h>
|
||||
#import <AppKit/NSImage.h>
|
||||
#import <AppKit/NSImageView.h>
|
||||
#import <AppKit/NSSlider.h>
|
||||
#import <AppKit/NSSliderTouchBarItem.h>
|
||||
|
||||
using namespace TouchBar;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kPadding = 7;
|
||||
|
||||
inline NSImage *Icon(const style::icon &icon) {
|
||||
return CreateNSImageFromStyleIcon(icon, kCircleDiameter / 2);
|
||||
}
|
||||
|
||||
inline NSDictionary *Attributes() {
|
||||
return @{
|
||||
NSFontAttributeName: [NSFont systemFontOfSize:14],
|
||||
NSParagraphStyleAttributeName:
|
||||
[NSMutableParagraphStyle defaultParagraphStyle],
|
||||
NSForegroundColorAttributeName: [NSColor whiteColor]
|
||||
};
|
||||
}
|
||||
|
||||
inline NSString *FormatTime(TimeId time) {
|
||||
return Platform::Q2NSString(Ui::FormatDurationText(time));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
#pragma mark - TrackPosition
|
||||
|
||||
@interface TrackPosition : NSImageView
|
||||
@end // @interface TrackPosition
|
||||
|
||||
@implementation TrackPosition {
|
||||
NSMutableString *_text;
|
||||
|
||||
double _width;
|
||||
double _height;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
}
|
||||
|
||||
- (id)init:(rpl::producer< Media::Player::TrackState>)trackState {
|
||||
self = [super init];
|
||||
const auto textLength = _lifetime.make_state<rpl::variable<int>>(0);
|
||||
_width = _height = 0;
|
||||
_text = [[NSMutableString alloc] initWithCapacity:13];
|
||||
|
||||
rpl::combine(
|
||||
rpl::duplicate(
|
||||
trackState
|
||||
) | rpl::map([](const auto &state) {
|
||||
return state.position / 1000;
|
||||
}) | rpl::distinct_until_changed(),
|
||||
std::move(
|
||||
trackState
|
||||
) | rpl::map([](const auto &state) {
|
||||
return state.length / 1000;
|
||||
}) | rpl::distinct_until_changed()
|
||||
) | rpl::on_next([=](int position, int length) {
|
||||
[_text setString:[NSString stringWithFormat:@"%@ / %@",
|
||||
FormatTime(position),
|
||||
FormatTime(length)]];
|
||||
*textLength = _text.length;
|
||||
|
||||
[self display];
|
||||
}, _lifetime);
|
||||
|
||||
textLength->changes(
|
||||
) | rpl::on_next([=] {
|
||||
const auto size = [_text sizeWithAttributes:Attributes()];
|
||||
_width = size.width + kPadding * 2;
|
||||
_height = size.height;
|
||||
|
||||
if (self.image) {
|
||||
[self.image release];
|
||||
}
|
||||
self.image = [[NSImage alloc] initWithSize:NSMakeSize(
|
||||
_width,
|
||||
kCircleDiameter)];
|
||||
}, _lifetime);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)drawRect:(NSRect)dirtyRect {
|
||||
if (!(_text && _text.length && _width && _height)) {
|
||||
return;
|
||||
}
|
||||
const auto size = [_text sizeWithAttributes:Attributes()];
|
||||
const auto rect = CGRectMake(
|
||||
(_width - size.width) / 2,
|
||||
-(kCircleDiameter - _height) / 2,
|
||||
_width,
|
||||
kCircleDiameter);
|
||||
[_text drawInRect:rect withAttributes:Attributes()];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
if (self.image) {
|
||||
[self.image release];
|
||||
}
|
||||
if (_text) {
|
||||
[_text release];
|
||||
}
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@end // @implementation TrackPosition
|
||||
|
||||
namespace TouchBar {
|
||||
|
||||
NSButton *CreateTouchBarButton(
|
||||
// const style::icon &icon,
|
||||
NSImage *image,
|
||||
rpl::lifetime &lifetime,
|
||||
Fn<void()> callback) {
|
||||
id block = [^{
|
||||
Core::Sandbox::Instance().customEnterFromEventLoop(callback);
|
||||
} copy];
|
||||
|
||||
NSButton* button = [NSButton
|
||||
buttonWithImage:image
|
||||
target:block
|
||||
action:@selector(invoke)];
|
||||
lifetime.add([=] {
|
||||
[block release];
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
NSButton *CreateTouchBarButton(
|
||||
const style::icon &icon,
|
||||
rpl::lifetime &lifetime,
|
||||
Fn<void()> callback) {
|
||||
return CreateTouchBarButton(Icon(icon), lifetime, std::move(callback));
|
||||
}
|
||||
|
||||
NSButton *CreateTouchBarButtonWithTwoStates(
|
||||
NSImage *icon1,
|
||||
NSImage *icon2,
|
||||
rpl::lifetime &lifetime,
|
||||
Fn<void(bool)> callback,
|
||||
bool firstState,
|
||||
rpl::producer<bool> stateChanged) {
|
||||
NSButton* button = [NSButton
|
||||
buttonWithImage:(firstState ? icon2 : icon1)
|
||||
target:nil
|
||||
action:nil];
|
||||
|
||||
const auto isFirstState = lifetime.make_state<bool>(firstState);
|
||||
id block = [^{
|
||||
const auto state = *isFirstState;
|
||||
button.image = state ? icon1 : icon2;
|
||||
*isFirstState = !state;
|
||||
Core::Sandbox::Instance().customEnterFromEventLoop([=] {
|
||||
callback(state);
|
||||
});
|
||||
} copy];
|
||||
|
||||
button.target = block;
|
||||
button.action = @selector(invoke);
|
||||
|
||||
std::move(
|
||||
stateChanged
|
||||
) | rpl::on_next([=](bool isChangedToFirstState) {
|
||||
button.image = isChangedToFirstState ? icon1 : icon2;
|
||||
}, lifetime);
|
||||
|
||||
lifetime.add([=] {
|
||||
[block release];
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
NSButton *CreateTouchBarButtonWithTwoStates(
|
||||
const style::icon &icon1,
|
||||
const style::icon &icon2,
|
||||
rpl::lifetime &lifetime,
|
||||
Fn<void(bool)> callback,
|
||||
bool firstState,
|
||||
rpl::producer<bool> stateChanged) {
|
||||
return CreateTouchBarButtonWithTwoStates(
|
||||
Icon(icon1),
|
||||
Icon(icon2),
|
||||
lifetime,
|
||||
std::move(callback),
|
||||
firstState,
|
||||
std::move(stateChanged));
|
||||
}
|
||||
|
||||
NSSliderTouchBarItem *CreateTouchBarSlider(
|
||||
NSString *itemId,
|
||||
rpl::lifetime &lifetime,
|
||||
Fn<void(bool, double, double)> callback,
|
||||
rpl::producer<Media::Player::TrackState> stateChanged) {
|
||||
const auto lastDurationMs = lifetime.make_state<crl::time>(0);
|
||||
|
||||
auto *seekBar = [[NSSliderTouchBarItem alloc] initWithIdentifier:itemId];
|
||||
seekBar.slider.minValue = 0.0f;
|
||||
seekBar.slider.maxValue = 1.0f;
|
||||
seekBar.customizationLabel = @"Seek Bar";
|
||||
|
||||
id block = [^{
|
||||
// https://stackoverflow.com/a/45891017
|
||||
auto *event = [[NSApplication sharedApplication] currentEvent];
|
||||
const auto touchUp = [event
|
||||
touchesMatchingPhase:NSTouchPhaseEnded
|
||||
inView:nil].count > 0;
|
||||
Core::Sandbox::Instance().customEnterFromEventLoop([=] {
|
||||
callback(touchUp, seekBar.slider.doubleValue, *lastDurationMs);
|
||||
});
|
||||
} copy];
|
||||
|
||||
std::move(
|
||||
stateChanged
|
||||
) | rpl::on_next([=](const Media::Player::TrackState &state) {
|
||||
const auto stop = Media::Player::IsStoppedOrStopping(state.state);
|
||||
const auto duration = double(stop ? 0 : state.length);
|
||||
auto slider = seekBar.slider;
|
||||
if (duration <= 0) {
|
||||
slider.enabled = false;
|
||||
slider.doubleValue = 0;
|
||||
} else {
|
||||
slider.enabled = true;
|
||||
if (!slider.highlighted) {
|
||||
const auto pos = stop
|
||||
? 0
|
||||
: std::max(state.position, int64(0));
|
||||
slider.doubleValue = (pos / duration) * slider.maxValue;
|
||||
*lastDurationMs = duration;
|
||||
}
|
||||
}
|
||||
}, lifetime);
|
||||
|
||||
seekBar.target = block;
|
||||
seekBar.action = @selector(invoke);
|
||||
lifetime.add([=] {
|
||||
[block release];
|
||||
});
|
||||
|
||||
return seekBar;
|
||||
}
|
||||
|
||||
NSCustomTouchBarItem *CreateTouchBarTrackPosition(
|
||||
NSString *itemId,
|
||||
rpl::producer<Media::Player::TrackState> stateChanged) {
|
||||
auto *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:itemId];
|
||||
auto *trackPosition = [[[TrackPosition alloc]
|
||||
init:std::move(stateChanged)] autorelease];
|
||||
|
||||
item.view = trackPosition;
|
||||
item.customizationLabel = @"Track Position";
|
||||
return item;
|
||||
}
|
||||
|
||||
} // namespace TouchBar
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
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/NSTouchBar.h>
|
||||
|
||||
namespace Window {
|
||||
class Controller;
|
||||
} // namespace Window
|
||||
|
||||
namespace TouchBar::Main {
|
||||
|
||||
const auto kPinnedPanelItemIdentifier = @"pinnedPanel";
|
||||
const auto kPopoverInputItemIdentifier = @"popoverInput";
|
||||
const auto kPopoverPickerItemIdentifier = @"pickerButtons";
|
||||
|
||||
} // namespace TouchBar::Main
|
||||
|
||||
API_AVAILABLE(macos(10.12.2))
|
||||
@interface TouchBarMain : NSTouchBar
|
||||
- (id)init:(not_null<Window::Controller*>)controller
|
||||
touchBarSwitches:(rpl::producer<>)touchBarSwitches;
|
||||
@end
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
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/mac_touchbar_main.h"
|
||||
|
||||
#include "platform/mac/touchbar/items/mac_formatter_item.h"
|
||||
#include "platform/mac/touchbar/items/mac_pinned_chats_item.h"
|
||||
#include "platform/mac/touchbar/items/mac_scrubber_item.h"
|
||||
#include "platform/mac/touchbar/mac_touchbar_common.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "window/window_session_controller.h"
|
||||
|
||||
#import <AppKit/NSCustomTouchBarItem.h>
|
||||
|
||||
using namespace TouchBar::Main;
|
||||
|
||||
#pragma mark - TouchBarMain
|
||||
|
||||
@interface TouchBarMain()
|
||||
@end // @interface TouchBarMain
|
||||
|
||||
@implementation TouchBarMain
|
||||
|
||||
- (id)init:(not_null<Window::Controller*>)controller
|
||||
touchBarSwitches:(rpl::producer<>)touchBarSwitches {
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
auto *pin = [[[NSCustomTouchBarItem alloc]
|
||||
initWithIdentifier:kPinnedPanelItemIdentifier] autorelease];
|
||||
pin.view = [[[PinnedDialogsPanel alloc]
|
||||
init:(&controller->sessionController()->session())
|
||||
destroyEvent:std::move(touchBarSwitches)] autorelease];
|
||||
|
||||
auto *sticker = [[[StickerEmojiPopover alloc]
|
||||
init:controller
|
||||
identifier:kPopoverPickerItemIdentifier] autorelease];
|
||||
|
||||
auto *format = [[[TextFormatPopover alloc]
|
||||
init:kPopoverInputItemIdentifier] autorelease];
|
||||
|
||||
self.templateItems = [NSSet setWithArray:@[pin, sticker, format]];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end // @implementation TouchBarMain
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
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/NSTouchBar.h>
|
||||
|
||||
namespace Main {
|
||||
class Domain;
|
||||
} // namespace Main
|
||||
|
||||
namespace Window {
|
||||
class Controller;
|
||||
} // namespace Window
|
||||
|
||||
namespace Ui {
|
||||
struct MarkdownEnabledState;
|
||||
} // namespace Ui
|
||||
|
||||
API_AVAILABLE(macos(10.12.2))
|
||||
@interface RootTouchBar : NSTouchBar<NSTouchBarDelegate>
|
||||
- (id)init:(rpl::producer<Ui::MarkdownEnabledState>)markdownState
|
||||
controller:(not_null<Window::Controller*>)controller
|
||||
domain:(not_null<Main::Domain*>)domain;
|
||||
@end
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
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/mac_touchbar_manager.h"
|
||||
|
||||
#include "apiwrap.h" // ApiWrap::updateStickers()
|
||||
#include "core/application.h"
|
||||
#include "data/data_chat_participant_status.h" // Data::CanSendAnyOf.
|
||||
#include "data/data_forum_topic.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/stickers/data_stickers.h" // Stickers::setsRef()
|
||||
#include "main/main_domain.h"
|
||||
#include "main/main_session.h"
|
||||
#include "media/audio/media_audio_capture.h"
|
||||
#include "media/player/media_player_instance.h"
|
||||
#include "platform/mac/touchbar/mac_touchbar_audio.h"
|
||||
#include "platform/mac/touchbar/mac_touchbar_common.h"
|
||||
#include "platform/mac/touchbar/mac_touchbar_main.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "window/window_session_controller.h"
|
||||
|
||||
#import <AppKit/NSGroupTouchBarItem.h>
|
||||
|
||||
using namespace TouchBar::Main;
|
||||
|
||||
namespace {
|
||||
|
||||
const auto kMainItemIdentifier = @"touchbarMain";
|
||||
const auto kAudioItemIdentifier = @"touchbarAudio";
|
||||
|
||||
} // namespace
|
||||
|
||||
@interface GroupTouchBarItem : NSGroupTouchBarItem
|
||||
- (rpl::lifetime &)lifetime;
|
||||
@end // @interface GroupTouchBarItem
|
||||
|
||||
@implementation GroupTouchBarItem {
|
||||
rpl::lifetime _lifetime;
|
||||
}
|
||||
|
||||
- (rpl::lifetime &)lifetime {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
@end // GroupTouchBarItem
|
||||
|
||||
#pragma mark - RootTouchBar
|
||||
|
||||
@interface RootTouchBar()
|
||||
@end // @interface RootTouchBar
|
||||
|
||||
@implementation RootTouchBar {
|
||||
Main::Session *_session;
|
||||
Window::Controller *_controller;
|
||||
|
||||
rpl::variable<Ui::MarkdownEnabledState> _markdownState;
|
||||
rpl::event_stream<> _touchBarSwitches;
|
||||
rpl::lifetime _lifetime;
|
||||
}
|
||||
|
||||
- (id)init:(rpl::producer<Ui::MarkdownEnabledState>)markdownState
|
||||
controller:(not_null<Window::Controller*>)controller
|
||||
domain:(not_null<Main::Domain*>)domain {
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
self.delegate = self;
|
||||
TouchBar::CustomEnterToCocoaEventLoop([=] {
|
||||
self.defaultItemIdentifiers = @[];
|
||||
});
|
||||
_controller = controller;
|
||||
_markdownState = std::move(markdownState);
|
||||
|
||||
auto sessionChanges = domain->activeSessionChanges(
|
||||
) | rpl::map([=](Main::Session *session) {
|
||||
if (session && session->data().stickers().setsRef().empty()) {
|
||||
session->api().updateStickers();
|
||||
}
|
||||
return session;
|
||||
});
|
||||
|
||||
const auto type = AudioMsgId::Type::Song;
|
||||
auto audioPlayer = rpl::merge(
|
||||
Media::Player::instance()->stops(type) | rpl::map_to(false),
|
||||
Media::Player::instance()->startsPlay(type) | rpl::map_to(true)
|
||||
);
|
||||
|
||||
auto voiceRecording = ::Media::Capture::instance()->startedChanges();
|
||||
|
||||
rpl::combine(
|
||||
std::move(sessionChanges),
|
||||
rpl::single(false) | rpl::then(Core::App().passcodeLockChanges()),
|
||||
rpl::single(false) | rpl::then(std::move(audioPlayer)),
|
||||
rpl::single(false) | rpl::then(std::move(voiceRecording))
|
||||
) | rpl::on_next([=](
|
||||
Main::Session *session,
|
||||
bool lock,
|
||||
bool audio,
|
||||
bool recording) {
|
||||
TouchBar::CustomEnterToCocoaEventLoop([=] {
|
||||
_touchBarSwitches.fire({});
|
||||
if (!audio) {
|
||||
self.defaultItemIdentifiers = @[];
|
||||
}
|
||||
self.defaultItemIdentifiers = (lock || recording)
|
||||
? @[]
|
||||
: audio
|
||||
? @[kAudioItemIdentifier]
|
||||
: session
|
||||
? @[kMainItemIdentifier]
|
||||
: @[];
|
||||
});
|
||||
}, _lifetime);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSTouchBarItem*)touchBar:(NSTouchBar*)touchBar
|
||||
makeItemForIdentifier:(NSTouchBarItemIdentifier)itemId {
|
||||
if (!touchBar || !_controller->sessionController()) {
|
||||
return nil;
|
||||
}
|
||||
const auto isEqual = [&](NSString *string) {
|
||||
return [itemId isEqualToString:string];
|
||||
};
|
||||
|
||||
if (isEqual(kMainItemIdentifier)) {
|
||||
auto *item = [[GroupTouchBarItem alloc] initWithIdentifier:itemId];
|
||||
item.groupTouchBar
|
||||
= [[[TouchBarMain alloc]
|
||||
init:_controller
|
||||
touchBarSwitches:_touchBarSwitches.events()] autorelease];
|
||||
rpl::combine(
|
||||
_markdownState.value(),
|
||||
_controller->sessionController()->activeChatValue(
|
||||
) | rpl::map([](Dialogs::Key k) {
|
||||
const auto topic = k.topic();
|
||||
const auto peer = k.peer();
|
||||
const auto rights = ChatRestriction::SendStickers
|
||||
| ChatRestriction::SendOther;
|
||||
return topic
|
||||
? Data::CanSendAnyOf(topic, rights)
|
||||
: (peer && Data::CanSendAnyOf(peer, rights));
|
||||
}) | rpl::distinct_until_changed()
|
||||
) | rpl::on_next([=](
|
||||
Ui::MarkdownEnabledState state,
|
||||
bool hasActiveChat) {
|
||||
item.groupTouchBar.defaultItemIdentifiers = @[
|
||||
kPinnedPanelItemIdentifier,
|
||||
(!state.disabled()
|
||||
? kPopoverInputItemIdentifier
|
||||
: hasActiveChat
|
||||
? kPopoverPickerItemIdentifier
|
||||
: @"")];
|
||||
}, [item lifetime]);
|
||||
|
||||
return [item autorelease];
|
||||
} else if (isEqual(kAudioItemIdentifier)) {
|
||||
auto *item = [[GroupTouchBarItem alloc] initWithIdentifier:itemId];
|
||||
auto *touchBar = [[[TouchBarAudioPlayer alloc] init]
|
||||
autorelease];
|
||||
item.groupTouchBar = touchBar;
|
||||
[touchBar closeRequests] | rpl::on_next([=] {
|
||||
Media::Player::instance()->stopAndClose();
|
||||
}, [item lifetime]);
|
||||
return [item autorelease];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end // @implementation RootTouchBar
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
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
|
||||
|
||||
#include "media/view/media_view_playback_controls.h"
|
||||
#include "media/view/media_view_overlay_widget.h"
|
||||
|
||||
namespace TouchBar {
|
||||
|
||||
void SetupMediaViewTouchBar(
|
||||
WId winId,
|
||||
not_null<Media::View::PlaybackControls::Delegate*> controlsDelegate,
|
||||
rpl::producer<Media::Player::TrackState> trackState,
|
||||
rpl::producer<Media::View::OverlayWidget::TouchBarItemType> display,
|
||||
rpl::producer<bool> fullscreenToggled);
|
||||
|
||||
} // namespace TouchBar
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
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/mac_touchbar_media_view.h"
|
||||
|
||||
#include "media/audio/media_audio.h"
|
||||
#include "platform/mac/touchbar/mac_touchbar_common.h"
|
||||
#include "platform/mac/touchbar/mac_touchbar_controls.h"
|
||||
#include "styles/style_media_player.h"
|
||||
#include "styles/style_media_view.h"
|
||||
|
||||
#import <AppKit/NSButton.h>
|
||||
#import <AppKit/NSCustomTouchBarItem.h>
|
||||
#import <AppKit/NSTouchBar.h>
|
||||
|
||||
using namespace TouchBar;
|
||||
using Delegate = Media::View::PlaybackControls::Delegate;
|
||||
using ItemType = Media::View::OverlayWidget::TouchBarItemType;
|
||||
|
||||
namespace {
|
||||
|
||||
inline NSTouchBarItemIdentifier Format(NSString *s) {
|
||||
return [NSString stringWithFormat:@"button.%@", s];
|
||||
}
|
||||
|
||||
const auto kPlayItemIdentifier = Format(@"playPause");
|
||||
const auto kRotateItemIdentifier = Format(@"rotate");
|
||||
const auto kFullscreenItemIdentifier = Format(@"fullscreen");
|
||||
const auto kPipItemIdentifier = Format(@"pip");
|
||||
const auto kTrackItemIdentifier = @"trackPosition";
|
||||
const auto kSeekItemIdentifier = @"seekBar";
|
||||
|
||||
}
|
||||
|
||||
#pragma mark - MediaViewTouchBar
|
||||
|
||||
@interface MediaViewTouchBar : NSTouchBar
|
||||
- (id)init:(not_null<Delegate*>)controlsDelegate
|
||||
trackState:(rpl::producer<Media::Player::TrackState>)trackState
|
||||
display:(rpl::producer<ItemType>)display
|
||||
fullscreenToggled:(rpl::producer<bool>)fullscreenToggled;
|
||||
@end
|
||||
|
||||
@implementation MediaViewTouchBar {
|
||||
rpl::lifetime _lifetime;
|
||||
}
|
||||
|
||||
- (id)init:(not_null<Delegate*>)controlsDelegate
|
||||
trackState:(rpl::producer<Media::Player::TrackState>)trackState
|
||||
display:(rpl::producer<ItemType>)display
|
||||
fullscreenToggled:(rpl::producer<bool>)fullscreenToggled {
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
const auto allocate = [](NSTouchBarItemIdentifier i) {
|
||||
return [[NSCustomTouchBarItem alloc] initWithIdentifier:i];
|
||||
};
|
||||
|
||||
auto *playPause = allocate(kPlayItemIdentifier);
|
||||
{
|
||||
auto *button = CreateTouchBarButtonWithTwoStates(
|
||||
st::touchBarIconPlayerPause,
|
||||
st::touchBarIconPlayerPlay,
|
||||
_lifetime,
|
||||
[=](bool value) {
|
||||
value
|
||||
? controlsDelegate->playbackControlsPlay()
|
||||
: controlsDelegate->playbackControlsPause();
|
||||
},
|
||||
false,
|
||||
rpl::duplicate(
|
||||
trackState
|
||||
) | rpl::map([](const auto &state) {
|
||||
return (state.state == Media::Player::State::Playing);
|
||||
}) | rpl::distinct_until_changed());
|
||||
playPause.view = button;
|
||||
playPause.customizationLabel = @"Play/Pause";
|
||||
}
|
||||
|
||||
auto *rotate = allocate(kRotateItemIdentifier);
|
||||
{
|
||||
auto *button = CreateTouchBarButton(
|
||||
[NSImage imageNamed:NSImageNameTouchBarRotateLeftTemplate],
|
||||
_lifetime,
|
||||
[=] { controlsDelegate->playbackControlsRotate(); });
|
||||
rotate.view = button;
|
||||
rotate.customizationLabel = @"Rotate";
|
||||
}
|
||||
|
||||
auto *fullscreen = allocate(kFullscreenItemIdentifier);
|
||||
{
|
||||
auto *button = CreateTouchBarButtonWithTwoStates(
|
||||
[NSImage imageNamed:NSImageNameTouchBarExitFullScreenTemplate],
|
||||
[NSImage imageNamed:NSImageNameTouchBarEnterFullScreenTemplate],
|
||||
_lifetime,
|
||||
[=](bool value) {
|
||||
value
|
||||
? controlsDelegate->playbackControlsFromFullScreen()
|
||||
: controlsDelegate->playbackControlsToFullScreen();
|
||||
},
|
||||
true,
|
||||
std::move(fullscreenToggled));
|
||||
fullscreen.view = button;
|
||||
fullscreen.customizationLabel = @"Fullscreen";
|
||||
}
|
||||
|
||||
auto *pip = allocate(kPipItemIdentifier);
|
||||
{
|
||||
auto *button = TouchBar::CreateTouchBarButton(
|
||||
CreateNSImageFromStyleIcon(
|
||||
st::mediaviewPipButton.icon,
|
||||
kCircleDiameter / 4 * 3),
|
||||
_lifetime,
|
||||
[=] { controlsDelegate->playbackControlsToPictureInPicture(); });
|
||||
pip.view = button;
|
||||
pip.customizationLabel = @"Picture-in-Picture";
|
||||
}
|
||||
|
||||
auto *trackPosition = CreateTouchBarTrackPosition(
|
||||
kTrackItemIdentifier,
|
||||
rpl::duplicate(trackState));
|
||||
|
||||
auto *seekBar = TouchBar::CreateTouchBarSlider(
|
||||
kSeekItemIdentifier,
|
||||
_lifetime,
|
||||
[=](bool touchUp, double value, double duration) {
|
||||
const auto progress = value * duration;
|
||||
touchUp
|
||||
? controlsDelegate->playbackControlsSeekFinished(progress)
|
||||
: controlsDelegate->playbackControlsSeekProgress(progress);
|
||||
},
|
||||
std::move(trackState));
|
||||
|
||||
self.templateItems = [NSSet setWithArray:@[
|
||||
playPause,
|
||||
rotate,
|
||||
fullscreen,
|
||||
pip,
|
||||
seekBar,
|
||||
trackPosition]];
|
||||
|
||||
const auto items = [](ItemType type) {
|
||||
switch (type) {
|
||||
case ItemType::Photo: return @[kRotateItemIdentifier];
|
||||
case ItemType::Video: return @[
|
||||
kRotateItemIdentifier,
|
||||
kFullscreenItemIdentifier,
|
||||
kPipItemIdentifier,
|
||||
kPlayItemIdentifier,
|
||||
kSeekItemIdentifier,
|
||||
kTrackItemIdentifier];
|
||||
default: return @[];
|
||||
};
|
||||
};
|
||||
|
||||
std::move(
|
||||
display
|
||||
) | rpl::distinct_until_changed(
|
||||
) | rpl::on_next([=](ItemType type) {
|
||||
TouchBar::CustomEnterToCocoaEventLoop([=] {
|
||||
self.defaultItemIdentifiers = items(type);
|
||||
});
|
||||
}, _lifetime);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end // @implementation MediaViewTouchBar
|
||||
|
||||
namespace TouchBar {
|
||||
|
||||
void SetupMediaViewTouchBar(
|
||||
WId winId,
|
||||
not_null<Delegate*> controlsDelegate,
|
||||
rpl::producer<Media::Player::TrackState> trackState,
|
||||
rpl::producer<ItemType> display,
|
||||
rpl::producer<bool> fullscreenToggled) {
|
||||
auto *window = [reinterpret_cast<NSView*>(winId) window];
|
||||
CustomEnterToCocoaEventLoop([=] {
|
||||
[window setTouchBar:[[[MediaViewTouchBar alloc]
|
||||
init:std::move(controlsDelegate)
|
||||
trackState:std::move(trackState)
|
||||
display:std::move(display)
|
||||
fullscreenToggled:std::move(fullscreenToggled)
|
||||
] autorelease]];
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace TouchBar
|
||||
Reference in New Issue
Block a user