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
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
/*
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/linux/current_geo_location_linux.h"
#include "core/current_geo_location.h"
#include "base/platform/linux/base_linux_library.h"
#include <gio/gio.h>
namespace Platform {
namespace {
typedef struct _GClueSimple GClueSimple;
typedef struct _GClueLocation GClueLocation;
typedef enum {
GCLUE_ACCURACY_LEVEL_NONE = 0,
GCLUE_ACCURACY_LEVEL_COUNTRY = 1,
GCLUE_ACCURACY_LEVEL_CITY = 4,
GCLUE_ACCURACY_LEVEL_NEIGHBORHOOD = 5,
GCLUE_ACCURACY_LEVEL_STREET = 6,
GCLUE_ACCURACY_LEVEL_EXACT = 8,
} GClueAccuracyLevel;
void (*gclue_simple_new)(
const char *desktop_id,
GClueAccuracyLevel accuracy_level,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data);
GClueSimple *(*gclue_simple_new_finish)(GAsyncResult *result, GError **error);
GClueLocation *(*gclue_simple_get_location)(GClueSimple *simple);
gdouble (*gclue_location_get_latitude)(GClueLocation *loc);
gdouble (*gclue_location_get_longitude)(GClueLocation *loc);
} // namespace
void ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback) {
static const auto Inited = [] {
const auto lib = base::Platform::LoadLibrary(
"libgeoclue-2.so.0",
RTLD_NODELETE);
return lib
&& LOAD_LIBRARY_SYMBOL(lib, gclue_simple_new)
&& LOAD_LIBRARY_SYMBOL(lib, gclue_simple_new_finish)
&& LOAD_LIBRARY_SYMBOL(lib, gclue_simple_get_location)
&& LOAD_LIBRARY_SYMBOL(lib, gclue_location_get_latitude)
&& LOAD_LIBRARY_SYMBOL(lib, gclue_location_get_longitude);
}();
if (!Inited) {
callback({});
return;
}
gclue_simple_new(
QGuiApplication::desktopFileName().toUtf8().constData(),
GCLUE_ACCURACY_LEVEL_EXACT,
nullptr,
GAsyncReadyCallback(+[](
GObject *object,
GAsyncResult* res,
Fn<void(Core::GeoLocation)> *callback) {
const auto callbackGuard = gsl::finally([&] {
delete callback;
});
const auto simple = gclue_simple_new_finish(res, nullptr);
if (!simple) {
(*callback)({});
return;
}
const auto simpleGuard = gsl::finally([&] {
g_object_unref(simple);
});
const auto location = gclue_simple_get_location(simple);
(*callback)({
.point = {
gclue_location_get_latitude(location),
gclue_location_get_longitude(location),
},
.accuracy = Core::GeoLocationAccuracy::Exact,
});
}),
new Fn(callback));
}
void ResolveLocationAddress(
const Core::GeoLocation &location,
const QString &language,
Fn<void(Core::GeoAddress)> callback) {
callback({});
}
} // namespace Platform

View File

@@ -0,0 +1,10 @@
/*
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 "platform/platform_current_geo_location.h"

View File

@@ -0,0 +1,126 @@
/*
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/linux/file_utilities_linux.h"
#include "base/platform/base_platform_info.h"
#include "base/platform/linux/base_linux_xdp_utilities.h"
#include "base/platform/linux/base_linux_xdg_activation_token.h"
#include "base/random.h"
#include <fcntl.h>
#include <xdpopenuri/xdpopenuri.hpp>
#include <xdprequest/xdprequest.hpp>
namespace Platform {
namespace File {
namespace {
using namespace gi::repository;
using base::Platform::XdgActivationToken;
} // namespace
bool UnsafeShowOpenWith(const QString &filepath) {
auto proxy = XdpOpenURI::OpenURIProxy::new_for_bus_sync(
Gio::BusType::SESSION_,
Gio::DBusProxyFlags::NONE_,
base::Platform::XDP::kService,
base::Platform::XDP::kObjectPath,
nullptr);
if (!proxy) {
return false;
}
auto interface = XdpOpenURI::OpenURI(proxy);
if (interface.get_version() < 3) {
return false;
}
const auto fd = open(
QFile::encodeName(filepath).constData(),
O_RDONLY | O_CLOEXEC);
if (fd == -1) {
return false;
}
const auto handleToken = "tdesktop"
+ std::to_string(base::RandomValue<uint>());
std::string uniqueName = proxy.get_connection().get_unique_name();
uniqueName.erase(0, 1);
uniqueName.replace(uniqueName.find('.'), 1, 1, '_');
auto request = XdpRequest::Request(
XdpRequest::RequestProxy::new_sync(
proxy.get_connection(),
Gio::DBusProxyFlags::NONE_,
base::Platform::XDP::kService,
base::Platform::XDP::kObjectPath
+ std::string("/request/")
+ uniqueName
+ '/'
+ handleToken,
nullptr,
nullptr));
if (!request) {
close(fd);
return false;
}
auto loop = GLib::MainLoop::new_();
const auto signalId = request.signal_response().connect([=](
XdpRequest::Request,
guint,
GLib::Variant) mutable {
loop.quit();
});
const auto signalGuard = gsl::finally([&] {
request.disconnect(signalId);
});
auto result = interface.call_open_file_sync(
base::Platform::XDP::ParentWindowID(),
GLib::Variant::new_handle(0),
GLib::Variant::new_array({
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("handle_token"),
GLib::Variant::new_variant(
GLib::Variant::new_string(handleToken))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("activation_token"),
GLib::Variant::new_variant(
GLib::Variant::new_string(
XdgActivationToken().toStdString()))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("ask"),
GLib::Variant::new_variant(
GLib::Variant::new_boolean(true))),
}),
Gio::UnixFDList::new_from_array(&fd, 1),
nullptr);
if (!result) {
return false;
}
QWidget window;
window.setAttribute(Qt::WA_DontShowOnScreen);
window.setWindowModality(Qt::ApplicationModal);
window.show();
loop.run();
return true;
}
} // namespace File
} // namespace Platform

View File

@@ -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
#include "platform/platform_file_utilities.h"
namespace Platform {
namespace File {
inline QString UrlToLocal(const QUrl &url) {
return ::File::internal::UrlToLocalDefault(url);
}
inline void UnsafeOpenUrl(const QString &url) {
return ::File::internal::UnsafeOpenUrlDefault(url);
}
inline void UnsafeOpenEmailLink(const QString &email) {
return ::File::internal::UnsafeOpenEmailLinkDefault(email);
}
inline bool UnsafeShowOpenWithDropdown(const QString &filepath) {
return false;
}
inline void UnsafeLaunch(const QString &filepath) {
return ::File::internal::UnsafeLaunchDefault(filepath);
}
inline void PostprocessDownloaded(const QString &filepath) {
}
} // namespace File
namespace FileDialog {
inline void InitLastPath() {
::FileDialog::internal::InitLastPathDefault();
}
inline bool Get(
QPointer<QWidget> parent,
QStringList &files,
QByteArray &remoteContent,
const QString &caption,
const QString &filter,
::FileDialog::internal::Type type,
QString startFile) {
return ::FileDialog::internal::GetDefault(
parent,
files,
remoteContent,
caption,
filter,
type,
startFile);
}
} // namespace FileDialog
} // namespace Platform

View File

@@ -0,0 +1,268 @@
/*
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/linux/integration_linux.h"
#include "platform/platform_integration.h"
#include "base/platform/base_platform_info.h"
#include "base/platform/linux/base_linux_xdp_utilities.h"
#include "core/sandbox.h"
#include "core/application.h"
#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
#include "core/core_settings.h"
#endif
#include "base/random.h"
#include "base/qt_connection.h"
#include <QtCore/QAbstractEventDispatcher>
#include <gio/gio.hpp>
#include <xdpinhibit/xdpinhibit.hpp>
#ifdef __GLIBC__
#include <malloc.h>
#endif // __GLIBC__
namespace Platform {
namespace {
using namespace gi::repository;
namespace GObject = gi::repository::GObject;
class Application : public Gio::impl::ApplicationImpl {
public:
Application();
void before_emit_(GLib::Variant platformData) noexcept override {
if (Platform::IsWayland()) {
static const auto keys = {
"activation-token",
"desktop-startup-id",
};
for (const auto &key : keys) {
if (auto token = platformData.lookup_value(key)) {
qputenv(
"XDG_ACTIVATION_TOKEN",
token.get_string(nullptr).c_str());
break;
}
}
}
}
void activate_() noexcept override {
Core::Sandbox::Instance().customEnterFromEventLoop([] {
Core::App().activate();
});
}
void open_(
gi::Collection<gi::DSpan, ::GFile*, gi::transfer_none_t> files,
const gi::cstring_v hint) noexcept override {
for (auto file : files) {
QFileOpenEvent e(QUrl(QString::fromStdString(file.get_uri())));
QGuiApplication::sendEvent(qApp, &e);
}
}
void add_platform_data_(
GLib::VariantBuilder_Ref builder) noexcept override {
if (Platform::IsWayland()) {
const auto token = qgetenv("XDG_ACTIVATION_TOKEN");
if (!token.isEmpty()) {
builder.add_value(
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("activation-token"),
GLib::Variant::new_variant(
GLib::Variant::new_string(token.toStdString()))));
qunsetenv("XDG_ACTIVATION_TOKEN");
}
}
}
};
Application::Application()
: Gio::impl::ApplicationImpl(this) {
const auto appId = QGuiApplication::desktopFileName().toStdString();
if (Gio::Application::id_is_valid(appId)) {
set_application_id(appId);
}
set_flags(Gio::ApplicationFlags::HANDLES_OPEN_);
auto actionMap = Gio::ActionMap(*this);
auto quitAction = Gio::SimpleAction::new_("quit");
quitAction.signal_activate().connect([](
Gio::SimpleAction,
GLib::Variant parameter) {
Core::Sandbox::Instance().customEnterFromEventLoop([] {
Core::Quit();
});
});
actionMap.add_action(quitAction);
const auto notificationIdVariantType = GLib::VariantType::new_("a{sv}");
auto notificationActivateAction = Gio::SimpleAction::new_(
"notification-activate",
notificationIdVariantType);
actionMap.add_action(notificationActivateAction);
auto notificationMarkAsReadAction = Gio::SimpleAction::new_(
"notification-mark-as-read",
notificationIdVariantType);
actionMap.add_action(notificationMarkAsReadAction);
}
gi::ref_ptr<Application> MakeApplication() {
const auto result = gi::make_ref<Application>();
if (const auto registered = result->register_(); !registered) {
LOG(("App Error: Failed to register: %1").arg(
registered.error().message_().c_str()));
return nullptr;
}
return result;
}
class LinuxIntegration final : public Integration, public base::has_weak_ptr {
public:
LinuxIntegration();
void init() override;
private:
[[nodiscard]] XdpInhibit::Inhibit inhibit() {
return _inhibitProxy;
}
void initInhibit();
const gi::ref_ptr<Application> _application;
XdpInhibit::InhibitProxy _inhibitProxy;
#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
base::Platform::XDP::SettingWatcher _darkModeWatcher;
#endif // Qt < 6.5.0
#ifdef __GLIBC__
base::qt_connection _memoryTrim;
crl::time _memoryTrimmed = 0;
#endif // __GLIBC__
};
LinuxIntegration::LinuxIntegration()
: _application(MakeApplication())
#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
, _darkModeWatcher(
"org.freedesktop.appearance",
"color-scheme",
[](GLib::Variant value) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
Core::App().settings().setSystemDarkMode(value.get_uint32() == 1);
});
})
#endif // Qt < 6.5.0
{
LOG(("Icon theme: %1").arg(QIcon::themeName()));
LOG(("Fallback icon theme: %1").arg(QIcon::fallbackThemeName()));
if (!QCoreApplication::eventDispatcher()->inherits(
"QEventDispatcherGlib")) {
g_warning("Qt is running without GLib event loop integration, "
"expect various functionality to not to work.");
}
#ifdef __GLIBC__
_memoryTrim = QObject::connect(
QCoreApplication::eventDispatcher(),
&QAbstractEventDispatcher::aboutToBlock,
[=] {
if (crl::now() - _memoryTrimmed >= 10000) {
malloc_trim(0);
_memoryTrimmed = crl::now();
}
});
#endif // __GLIBC__
}
void LinuxIntegration::init() {
XdpInhibit::InhibitProxy::new_for_bus(
Gio::BusType::SESSION_,
Gio::DBusProxyFlags::NONE_,
base::Platform::XDP::kService,
base::Platform::XDP::kObjectPath,
crl::guard(this, [=](GObject::Object, Gio::AsyncResult res) {
_inhibitProxy = XdpInhibit::InhibitProxy::new_for_bus_finish(
res,
nullptr);
initInhibit();
}));
}
void LinuxIntegration::initInhibit() {
if (!_inhibitProxy) {
return;
}
std::string uniqueName = _inhibitProxy.get_connection().get_unique_name();
uniqueName.erase(0, 1);
uniqueName.replace(uniqueName.find('.'), 1, 1, '_');
const auto handleToken = "tdesktop"
+ std::to_string(base::RandomValue<uint>());
const auto sessionHandleToken = "tdesktop"
+ std::to_string(base::RandomValue<uint>());
const auto sessionHandle = base::Platform::XDP::kObjectPath
+ std::string("/session/")
+ uniqueName
+ '/'
+ sessionHandleToken;
inhibit().signal_state_changed().connect([
mySessionHandle = sessionHandle
](
XdpInhibit::Inhibit,
const std::string &sessionHandle,
GLib::Variant state) {
if (sessionHandle != mySessionHandle) {
return;
}
Core::App().setScreenIsLocked(
GLib::VariantDict::new_(
state
).lookup_value(
"screensaver-active"
).get_boolean()
);
});
inhibit().call_create_monitor(
"",
GLib::Variant::new_array({
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("handle_token"),
GLib::Variant::new_variant(
GLib::Variant::new_string(handleToken))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("session_handle_token"),
GLib::Variant::new_variant(
GLib::Variant::new_string(sessionHandleToken))),
}),
nullptr);
}
} // namespace
std::unique_ptr<Integration> CreateIntegration() {
return std::make_unique<LinuxIntegration>();
}
} // namespace Platform

View File

@@ -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
namespace Platform {
class Integration;
[[nodiscard]] std::unique_ptr<Integration> CreateIntegration();
} // namespace Platform

View File

@@ -0,0 +1,147 @@
/*
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/linux/launcher_linux.h"
#include "core/crash_reports.h"
#include "core/update_checker.h"
#include "webview/platform/linux/webview_linux_webkitgtk.h"
#include <QtWidgets/QApplication>
#include <glib/glib.hpp>
#ifdef __GLIBC__
#include <malloc.h>
#endif // __GLIBC__
using namespace gi::repository;
namespace Platform {
Launcher::Launcher(int argc, char *argv[])
: Core::Launcher(argc, argv) {
#ifdef __GLIBC__
mallopt(M_ARENA_MAX, 1);
#endif // __GLIBC__
}
int Launcher::exec() {
for (auto i = arguments().begin(), e = arguments().end(); i != e; ++i) {
if (*i == u"-webviewhelper"_q && std::distance(i, e) > 1) {
Webview::WebKitGTK::SetSocketPath((i + 1)->toStdString());
return Webview::WebKitGTK::Exec();
}
}
return Core::Launcher::exec();
}
bool Launcher::launchUpdater(UpdaterLaunch action) {
if (cExeName().isEmpty()) {
return false;
}
const auto justRelaunch = action == UpdaterLaunch::JustRelaunch;
if (action == UpdaterLaunch::PerformUpdate) {
_updating = true;
}
std::vector<std::string> argumentsList;
// What we are launching.
const auto launching = justRelaunch
? (cExeDir() + cExeName())
: cWriteProtected()
? GLib::find_program_in_path("run0")
? u"run0"_q
: u"pkexec"_q
: (cExeDir() + u"Updater"_q);
argumentsList.push_back(launching.toStdString());
if (justRelaunch) {
// argv[0] that is passed to what we are launching.
// It should be added explicitly in case of FILE_AND_ARGV_ZERO_.
const auto argv0 = !arguments().isEmpty()
? arguments().first()
: launching;
argumentsList.push_back(argv0.toStdString());
} else if (cWriteProtected()) {
// Elevated process that run0/pkexec should launch.
const auto elevated = cWorkingDir() + u"tupdates/temp/Updater"_q;
argumentsList.push_back(elevated.toStdString());
}
if (Logs::DebugEnabled()) {
argumentsList.push_back("-debug");
}
if (justRelaunch) {
if (cLaunchMode() == LaunchModeAutoStart) {
argumentsList.push_back("-autostart");
}
if (cStartInTray()) {
argumentsList.push_back("-startintray");
}
if (cDataFile() != u"data"_q) {
argumentsList.push_back("-key");
argumentsList.push_back(cDataFile().toStdString());
}
if (!_updating) {
argumentsList.push_back("-noupdate");
argumentsList.push_back("-tosettings");
}
if (customWorkingDir()) {
argumentsList.push_back("-workdir");
argumentsList.push_back(cWorkingDir().toStdString());
}
} else {
// Don't relaunch Telegram.
argumentsList.push_back("-justupdate");
argumentsList.push_back("-workpath");
argumentsList.push_back(cWorkingDir().toStdString());
argumentsList.push_back("-exename");
argumentsList.push_back(cExeName().toStdString());
argumentsList.push_back("-exepath");
argumentsList.push_back(cExeDir().toStdString());
if (cWriteProtected()) {
argumentsList.push_back("-writeprotected");
}
}
Logs::closeMain();
CrashReports::Finish();
int waitStatus = 0;
if (justRelaunch) {
return GLib::spawn_async(
initialWorkingDir().toStdString(),
argumentsList,
{},
GLib::SpawnFlags::FILE_AND_ARGV_ZERO_,
nullptr,
nullptr,
nullptr);
} else if (!GLib::spawn_sync(
argumentsList,
{},
// if the spawn is sync, working directory is not set
// and GLib::SpawnFlags::LEAVE_DESCRIPTORS_OPEN_ is set,
// it goes through an optimized code path
GLib::SpawnFlags::SEARCH_PATH_
| GLib::SpawnFlags::LEAVE_DESCRIPTORS_OPEN_,
nullptr,
nullptr,
nullptr,
&waitStatus,
nullptr) || !g_spawn_check_exit_status(waitStatus, nullptr)) {
return false;
}
return launchUpdater(UpdaterLaunch::JustRelaunch);
}
} // namespace

View File

@@ -0,0 +1,27 @@
/*
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 "core/launcher.h"
namespace Platform {
class Launcher : public Core::Launcher {
public:
Launcher(int argc, char *argv[]);
int exec() override;
private:
bool launchUpdater(UpdaterLaunch action) override;
bool _updating = false;
};
} // namespace Platform

View File

@@ -0,0 +1,574 @@
/*
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/linux/main_window_linux.h"
#include "styles/style_window.h"
#include "platform/linux/specific_linux.h"
#include "history/history.h"
#include "history/history_widget.h"
#include "history/history_inner_widget.h"
#include "main/main_account.h" // Account::sessionChanges.
#include "main/main_session.h"
#include "mainwindow.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "core/sandbox.h"
#include "boxes/peer_list_controllers.h"
#include "boxes/about_box.h"
#include "lang/lang_keys.h"
#include "storage/localstorage.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "base/platform/base_platform_info.h"
#include "base/event_filter.h"
#include "ui/platform/ui_platform_window_title.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/ui_utility.h"
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
#include "base/platform/linux/base_linux_xcb_utilities.h"
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
#include <QtCore/QSize>
#include <QtCore/QMimeData>
#include <QtGui/QWindow>
#include <QtWidgets/QMenuBar>
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QTextEdit>
#include <gio/gio.hpp>
namespace Platform {
namespace {
using WorkMode = Core::Settings::WorkMode;
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
void XCBSkipTaskbar(QWindow *window, bool skip) {
const base::Platform::XCB::Connection connection;
if (!connection || xcb_connection_has_error(connection)) {
return;
}
const auto root = base::Platform::XCB::GetRootWindow(connection);
if (!root) {
return;
}
const auto stateAtom = base::Platform::XCB::GetAtom(
connection,
"_NET_WM_STATE");
if (!stateAtom) {
return;
}
const auto skipTaskbarAtom = base::Platform::XCB::GetAtom(
connection,
"_NET_WM_STATE_SKIP_TASKBAR");
if (!skipTaskbarAtom) {
return;
}
xcb_client_message_event_t xev;
xev.response_type = XCB_CLIENT_MESSAGE;
xev.type = stateAtom;
xev.sequence = 0;
xev.window = window->winId();
xev.format = 32;
xev.data.data32[0] = skip ? 1 : 0;
xev.data.data32[1] = skipTaskbarAtom;
xev.data.data32[2] = 0;
xev.data.data32[3] = 0;
xev.data.data32[4] = 0;
free(
xcb_request_check(
connection,
xcb_send_event_checked(
connection,
false,
root,
XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT
| XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY,
reinterpret_cast<const char*>(&xev))));
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
void SkipTaskbar(QWindow *window, bool skip) {
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
if (IsX11()) {
XCBSkipTaskbar(window, skip);
return;
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
}
void SendKeySequence(
Qt::Key key,
Qt::KeyboardModifiers modifiers = Qt::NoModifier) {
const auto focused = QApplication::focusWidget();
if (qobject_cast<QLineEdit*>(focused)
|| qobject_cast<QTextEdit*>(focused)
|| dynamic_cast<HistoryInner*>(focused)) {
QApplication::postEvent(
focused,
new QKeyEvent(QEvent::KeyPress, key, modifiers));
QApplication::postEvent(
focused,
new QKeyEvent(QEvent::KeyRelease, key, modifiers));
}
}
void ForceDisabled(QAction *action, bool disabled) {
if (action->isEnabled()) {
if (disabled) action->setDisabled(true);
} else if (!disabled) {
action->setDisabled(false);
}
}
} // namespace
MainWindow::MainWindow(not_null<Window::Controller*> controller)
: Window::MainWindow(controller) {
}
void MainWindow::workmodeUpdated(Core::Settings::WorkMode mode) {
if (!TrayIconSupported()) {
return;
}
SkipTaskbar(windowHandle(), mode == WorkMode::TrayOnly);
}
void MainWindow::unreadCounterChangedHook() {
updateUnityCounter();
}
void MainWindow::updateWindowIcon() {
const auto session = sessionController()
? &sessionController()->session()
: nullptr;
setWindowIcon(Window::CreateIcon(session));
}
void MainWindow::updateUnityCounter() {
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
qApp->setBadgeNumber(Core::App().unreadBadge());
#else // Qt >= 6.6.0
using namespace gi::repository;
static const auto djbStringHash = [](const std::string &string) {
uint hash = 5381;
for (const auto &curChar : string) {
hash = (hash << 5) + hash + curChar;
}
return hash;
};
const auto launcherUrl = "application://"
+ QGuiApplication::desktopFileName().toStdString()
+ ".desktop";
const auto counterSlice = std::min(Core::App().unreadBadge(), 9999);
auto connection = Gio::bus_get_sync(Gio::BusType::SESSION_, nullptr);
if (!connection) {
return;
}
connection.emit_signal(
{},
"/com/canonical/unity/launcherentry/"
+ std::to_string(djbStringHash(launcherUrl)),
"com.canonical.Unity.LauncherEntry",
"Update",
GLib::Variant::new_tuple({
GLib::Variant::new_string(launcherUrl),
GLib::Variant::new_array({
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("count"),
GLib::Variant::new_variant(
GLib::Variant::new_int64(counterSlice))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("count-visible"),
GLib::Variant::new_variant(
GLib::Variant::new_boolean(counterSlice))),
}),
}));
#endif // Qt < 6.6.0
}
void MainWindow::createGlobalMenu() {
const auto ensureWindowShown = [=] {
if (isHidden()) {
showFromTray();
}
};
psMainMenu = new QMenuBar(this);
psMainMenu->hide();
auto file = psMainMenu->addMenu(tr::lng_mac_menu_file(tr::now));
psLogout = file->addAction(
tr::lng_mac_menu_logout(tr::now),
this,
[=] {
ensureWindowShown();
controller().showLogoutConfirmation();
});
auto quit = file->addAction(
tr::lng_mac_menu_quit_telegram(tr::now, lt_telegram, u"Telegram"_q),
this,
[=] { quitFromTray(); },
QKeySequence::Quit);
quit->setMenuRole(QAction::QuitRole);
quit->setShortcutContext(Qt::WidgetShortcut);
auto edit = psMainMenu->addMenu(tr::lng_mac_menu_edit(tr::now));
psUndo = edit->addAction(
tr::lng_linux_menu_undo(tr::now),
[] { SendKeySequence(Qt::Key_Z, Qt::ControlModifier); },
QKeySequence::Undo);
psUndo->setShortcutContext(Qt::WidgetShortcut);
psRedo = edit->addAction(
tr::lng_linux_menu_redo(tr::now),
[] {
SendKeySequence(
Qt::Key_Z,
Qt::ControlModifier | Qt::ShiftModifier);
},
QKeySequence::Redo);
psRedo->setShortcutContext(Qt::WidgetShortcut);
edit->addSeparator();
psCut = edit->addAction(
tr::lng_mac_menu_cut(tr::now),
[] { SendKeySequence(Qt::Key_X, Qt::ControlModifier); },
QKeySequence::Cut);
psCut->setShortcutContext(Qt::WidgetShortcut);
psCopy = edit->addAction(
tr::lng_mac_menu_copy(tr::now),
[] { SendKeySequence(Qt::Key_C, Qt::ControlModifier); },
QKeySequence::Copy);
psCopy->setShortcutContext(Qt::WidgetShortcut);
psPaste = edit->addAction(
tr::lng_mac_menu_paste(tr::now),
[] { SendKeySequence(Qt::Key_V, Qt::ControlModifier); },
QKeySequence::Paste);
psPaste->setShortcutContext(Qt::WidgetShortcut);
psDelete = edit->addAction(
tr::lng_mac_menu_delete(tr::now),
[] { SendKeySequence(Qt::Key_Delete); },
QKeySequence(Qt::ControlModifier | Qt::Key_Backspace));
psDelete->setShortcutContext(Qt::WidgetShortcut);
edit->addSeparator();
psBold = edit->addAction(
tr::lng_menu_formatting_bold(tr::now),
[] { SendKeySequence(Qt::Key_B, Qt::ControlModifier); },
QKeySequence::Bold);
psBold->setShortcutContext(Qt::WidgetShortcut);
psItalic = edit->addAction(
tr::lng_menu_formatting_italic(tr::now),
[] { SendKeySequence(Qt::Key_I, Qt::ControlModifier); },
QKeySequence::Italic);
psItalic->setShortcutContext(Qt::WidgetShortcut);
psUnderline = edit->addAction(
tr::lng_menu_formatting_underline(tr::now),
[] { SendKeySequence(Qt::Key_U, Qt::ControlModifier); },
QKeySequence::Underline);
psUnderline->setShortcutContext(Qt::WidgetShortcut);
psStrikeOut = edit->addAction(
tr::lng_menu_formatting_strike_out(tr::now),
[] {
SendKeySequence(
Qt::Key_X,
Qt::ControlModifier | Qt::ShiftModifier);
},
Ui::kStrikeOutSequence);
psStrikeOut->setShortcutContext(Qt::WidgetShortcut);
psBlockquote = edit->addAction(
tr::lng_menu_formatting_blockquote(tr::now),
[] {
SendKeySequence(
Qt::Key_Period,
Qt::ControlModifier | Qt::ShiftModifier);
},
Ui::kBlockquoteSequence);
psBlockquote->setShortcutContext(Qt::WidgetShortcut);
psMonospace = edit->addAction(
tr::lng_menu_formatting_monospace(tr::now),
[] {
SendKeySequence(
Qt::Key_M,
Qt::ControlModifier | Qt::ShiftModifier);
},
Ui::kMonospaceSequence);
psMonospace->setShortcutContext(Qt::WidgetShortcut);
psClearFormat = edit->addAction(
tr::lng_menu_formatting_clear(tr::now),
[] {
SendKeySequence(
Qt::Key_N,
Qt::ControlModifier | Qt::ShiftModifier);
},
Ui::kClearFormatSequence);
psClearFormat->setShortcutContext(Qt::WidgetShortcut);
edit->addSeparator();
psSelectAll = edit->addAction(
tr::lng_mac_menu_select_all(tr::now),
[] { SendKeySequence(Qt::Key_A, Qt::ControlModifier); },
QKeySequence::SelectAll);
psSelectAll->setShortcutContext(Qt::WidgetShortcut);
edit->addSeparator();
auto prefs = edit->addAction(
tr::lng_mac_menu_preferences(tr::now),
this,
[=] {
ensureWindowShown();
controller().showSettings();
},
QKeySequence(Qt::ControlModifier | Qt::Key_Comma));
prefs->setMenuRole(QAction::PreferencesRole);
prefs->setShortcutContext(Qt::WidgetShortcut);
auto tools = psMainMenu->addMenu(tr::lng_linux_menu_tools(tr::now));
psContacts = tools->addAction(
tr::lng_mac_menu_contacts(tr::now),
crl::guard(this, [=] {
if (isHidden()) {
showFromTray();
}
if (!sessionController()) {
return;
}
sessionController()->show(
PrepareContactsBox(sessionController()));
}));
psAddContact = tools->addAction(
tr::lng_mac_menu_add_contact(tr::now),
this,
[=] {
Expects(sessionController() != nullptr);
ensureWindowShown();
sessionController()->showAddContact();
});
tools->addSeparator();
psNewGroup = tools->addAction(
tr::lng_mac_menu_new_group(tr::now),
this,
[=] {
Expects(sessionController() != nullptr);
ensureWindowShown();
sessionController()->showNewGroup();
});
psNewChannel = tools->addAction(
tr::lng_mac_menu_new_channel(tr::now),
this,
[=] {
Expects(sessionController() != nullptr);
ensureWindowShown();
sessionController()->showNewChannel();
});
auto help = psMainMenu->addMenu(tr::lng_linux_menu_help(tr::now));
auto about = help->addAction(
tr::lng_mac_menu_about_telegram(
tr::now,
lt_telegram,
u"Telegram"_q),
[=] {
ensureWindowShown();
controller().show(Box(AboutBox));
});
about->setMenuRole(QAction::AboutQtRole);
updateGlobalMenu();
}
void MainWindow::updateGlobalMenuHook() {
if (!positionInited()) {
return;
}
const auto focused = QApplication::focusWidget();
auto canUndo = false;
auto canRedo = false;
auto canCut = false;
auto canCopy = false;
auto canPaste = false;
auto canDelete = false;
auto canSelectAll = false;
const auto mimeData = QGuiApplication::clipboard()->mimeData();
const auto clipboardHasText = mimeData ? mimeData->hasText() : false;
auto markdownState = Ui::MarkdownEnabledState();
if (const auto edit = qobject_cast<QLineEdit*>(focused)) {
canCut = canCopy = canDelete = edit->hasSelectedText();
canSelectAll = !edit->text().isEmpty();
canUndo = edit->isUndoAvailable();
canRedo = edit->isRedoAvailable();
canPaste = clipboardHasText;
} else if (const auto edit = qobject_cast<QTextEdit*>(focused)) {
canCut = canCopy = canDelete = edit->textCursor().hasSelection();
canSelectAll = !edit->document()->isEmpty();
canUndo = edit->document()->isUndoAvailable();
canRedo = edit->document()->isRedoAvailable();
canPaste = clipboardHasText;
if (canCopy) {
if (const auto inputField = dynamic_cast<Ui::InputField*>(
focused->parentWidget())) {
markdownState = inputField->markdownEnabledState();
}
}
} else if (const auto list = dynamic_cast<HistoryInner*>(focused)) {
canCopy = list->canCopySelected();
canDelete = list->canDeleteSelected();
}
updateIsActive();
const auto logged = (sessionController() != nullptr);
const auto inactive = !logged || controller().locked();
const auto support = logged
&& sessionController()->session().supportMode();
ForceDisabled(psLogout, !logged && !Core::App().passcodeLocked());
ForceDisabled(psUndo, !canUndo);
ForceDisabled(psRedo, !canRedo);
ForceDisabled(psCut, !canCut);
ForceDisabled(psCopy, !canCopy);
ForceDisabled(psPaste, !canPaste);
ForceDisabled(psDelete, !canDelete);
ForceDisabled(psSelectAll, !canSelectAll);
ForceDisabled(psContacts, inactive || support);
ForceDisabled(psAddContact, inactive);
ForceDisabled(psNewGroup, inactive || support);
ForceDisabled(psNewChannel, inactive || support);
const auto diabled = [=](const QString &tag) {
return !markdownState.enabledForTag(tag);
};
using Field = Ui::InputField;
ForceDisabled(psBold, diabled(Field::kTagBold));
ForceDisabled(psItalic, diabled(Field::kTagItalic));
ForceDisabled(psUnderline, diabled(Field::kTagUnderline));
ForceDisabled(psStrikeOut, diabled(Field::kTagStrikeOut));
ForceDisabled(psBlockquote, diabled(Field::kTagBlockquote));
ForceDisabled(
psMonospace,
diabled(Field::kTagPre) || diabled(Field::kTagCode));
ForceDisabled(psClearFormat, markdownState.disabled());
}
bool MainWindow::eventFilter(QObject *obj, QEvent *evt) {
const auto t = evt->type();
if (t == QEvent::FocusIn || t == QEvent::FocusOut) {
if (qobject_cast<QLineEdit*>(obj)
|| qobject_cast<QTextEdit*>(obj)
|| dynamic_cast<HistoryInner*>(obj)) {
if (QApplication::focusWidget()) {
updateGlobalMenu();
}
}
} else if (obj == this && t == QEvent::Paint) {
if (!_exposed) {
_exposed = true;
SkipTaskbar(
windowHandle(),
(Core::App().settings().workMode() == WorkMode::TrayOnly)
&& TrayIconSupported());
}
} else if (obj == this && t == QEvent::Hide) {
_exposed = false;
} else if (obj == this && t == QEvent::ThemeChange) {
updateWindowIcon();
}
return Window::MainWindow::eventFilter(obj, evt);
}
MainWindow::~MainWindow() {
}
int32 ScreenNameChecksum(const QString &name) {
return Window::DefaultScreenNameChecksum(name);
}
int32 ScreenNameChecksum(const QScreen *screen) {
return ScreenNameChecksum(screen->name());
}
QString ScreenDisplayLabel(const QScreen *screen) {
if (!screen) {
return QString();
}
const auto model = (screen->manufacturer()
+ ' '
+ screen->model()).simplified();
if (!model.isEmpty()) {
if (!screen->name().isEmpty()) {
return (model
+ ' '
+ QChar(8212)
+ ' '
+ screen->name()).simplified();
}
return model;
}
return screen->name();
}
} // namespace Platform

View File

@@ -0,0 +1,71 @@
/*
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 "platform/platform_main_window.h"
#include "base/unique_qptr.h"
class QMenuBar;
namespace Ui {
class PopupMenu;
} // namespace Ui
namespace Platform {
class MainWindow : public Window::MainWindow {
public:
explicit MainWindow(not_null<Window::Controller*> controller);
~MainWindow();
void updateWindowIcon() override;
protected:
bool eventFilter(QObject *obj, QEvent *evt) override;
void unreadCounterChangedHook() override;
void updateGlobalMenuHook() override;
void workmodeUpdated(Core::Settings::WorkMode mode) override;
void createGlobalMenu() override;
private:
void updateUnityCounter();
QMenuBar *psMainMenu = nullptr;
QAction *psLogout = nullptr;
QAction *psUndo = nullptr;
QAction *psRedo = nullptr;
QAction *psCut = nullptr;
QAction *psCopy = nullptr;
QAction *psPaste = nullptr;
QAction *psDelete = nullptr;
QAction *psSelectAll = nullptr;
QAction *psContacts = nullptr;
QAction *psAddContact = nullptr;
QAction *psNewGroup = nullptr;
QAction *psNewChannel = nullptr;
QAction *psBold = nullptr;
QAction *psItalic = nullptr;
QAction *psUnderline = nullptr;
QAction *psStrikeOut = nullptr;
QAction *psBlockquote = nullptr;
QAction *psMonospace = nullptr;
QAction *psClearFormat = nullptr;
bool _exposed = false;
};
[[nodiscard]] int32 ScreenNameChecksum(const QString &name);
[[nodiscard]] int32 ScreenNameChecksum(const QScreen *screen);
[[nodiscard]] QString ScreenDisplayLabel(const QScreen *screen);
} // namespace Platform

View File

@@ -0,0 +1,938 @@
/*
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/linux/notifications_manager_linux.h"
#include "base/options.h"
#include "base/platform/base_platform_info.h"
#include "base/platform/linux/base_linux_dbus_utilities.h"
#include "platform/platform_specific.h"
#include "core/application.h"
#include "core/sandbox.h"
#include "data/data_forum_topic.h"
#include "data/data_saved_sublist.h"
#include "data/data_peer.h"
#include "history/history.h"
#include "history/history_item.h"
#include "main/main_session.h"
#include "media/audio/media_audio_local_cache.h"
#include "lang/lang_keys.h"
#include "base/weak_ptr.h"
#include "window/notifications_utilities.h"
#include <QtCore/QBuffer>
#include <QtCore/QVersionNumber>
#include <QtGui/QGuiApplication>
#include <ksandbox.h>
#include <xdgnotifications/xdgnotifications.hpp>
#include <dlfcn.h>
namespace Platform {
namespace Notifications {
namespace {
using namespace gi::repository;
namespace GObject = gi::repository::GObject;
constexpr auto kService = "org.freedesktop.Notifications";
constexpr auto kObjectPath = "/org/freedesktop/Notifications";
struct ServerInformation {
std::string name;
std::string vendor;
QVersionNumber version;
QVersionNumber specVersion;
};
bool ServiceRegistered = false;
ServerInformation CurrentServerInformation;
std::vector<std::string> CurrentCapabilities;
[[nodiscard]] bool HasCapability(const char *value) {
return ranges::contains(CurrentCapabilities, value);
}
std::optional<base::Platform::DBus::ServiceWatcher> CreateServiceWatcher() {
auto connection = Gio::bus_get_sync(Gio::BusType::SESSION_, nullptr);
if (!connection) {
return {};
}
const auto activatable = [&] {
const auto names = base::Platform::DBus::ListActivatableNames(
connection.gobj_());
if (!names) {
// avoid service restart loop in sandboxed environments
return true;
}
return ranges::contains(*names, kService);
}();
return std::make_optional<base::Platform::DBus::ServiceWatcher>(
connection.gobj_(),
kService,
[=](
const std::string &service,
const std::string &oldOwner,
const std::string &newOwner) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
if (activatable && newOwner.empty()) {
Core::App().notifications().clearAll();
} else {
Core::App().notifications().createManager();
}
});
});
}
void StartServiceAsync(Gio::DBusConnection connection, Fn<void()> callback) {
namespace DBus = base::Platform::DBus;
DBus::StartServiceByNameAsync(
connection.gobj_(),
kService,
[=](Fn<DBus::Result<DBus::StartReply>()> result) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
// get the error if any
if (const auto ret = result(); !ret) {
const auto &error = *static_cast<GLib::Error*>(
ret.error().get());
if (error.gobj_()->domain != G_DBUS_ERROR
|| error.code_()
!= G_DBUS_ERROR_SERVICE_UNKNOWN) {
Gio::DBusErrorNS_::strip_remote_error(error);
LOG(("Native Notification Error: %1").arg(
error.message_().c_str()));
}
}
callback();
});
});
}
std::string GetImageKey() {
const auto &specVersion = CurrentServerInformation.specVersion;
if (specVersion >= QVersionNumber(1, 2)) {
return "image-data";
} else if (specVersion == QVersionNumber(1, 1)) {
return "image_data";
}
return "icon_data";
}
bool UseGNotification() {
if (!Gio::Application::get_default()) {
return false;
}
if (Window::Notifications::OptionGNotification.value()) {
return true;
}
return KSandbox::isFlatpak() && !ServiceRegistered;
}
} // namespace
class Manager::Private : public base::has_weak_ptr {
public:
explicit Private(not_null<Manager*> manager);
void init(XdgNotifications::NotificationsProxy proxy);
void showNotification(
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView);
void clearAll();
void clearFromItem(not_null<HistoryItem*> item);
void clearFromTopic(not_null<Data::ForumTopic*> topic);
void clearFromSublist(not_null<Data::SavedSublist*> sublist);
void clearFromHistory(not_null<History*> history);
void clearFromSession(not_null<Main::Session*> session);
void clearNotification(NotificationId id);
void invokeIfNotInhibited(Fn<void()> callback);
private:
struct NotificationData : public base::has_weak_ptr {
std::variant<v::null_t, uint, std::string> id;
rpl::lifetime lifetime;
};
using Notification = std::unique_ptr<NotificationData>;
const not_null<Manager*> _manager;
Gio::Application _application;
XdgNotifications::NotificationsProxy _proxy;
XdgNotifications::Notifications _interface;
Media::Audio::LocalDiskCache _sounds;
base::flat_map<
ContextId,
base::flat_map<MsgId, Notification>> _notifications;
rpl::lifetime _lifetime;
};
bool SkipToastForCustom() {
return false;
}
void MaybePlaySoundForCustom(Fn<void()> playSound) {
playSound();
}
void MaybeFlashBounceForCustom(Fn<void()> flashBounce) {
flashBounce();
}
bool WaitForInputForCustom() {
return true;
}
bool Supported() {
return ServiceRegistered || UseGNotification();
}
bool Enforced() {
// Wayland doesn't support positioning
// and custom notifications don't work here
return IsWayland()
|| (Gio::Application::get_default()
&& Window::Notifications::OptionGNotification.value());
}
bool ByDefault() {
// The capabilities are static, equivalent to 'body' and 'actions' only
if (UseGNotification()) {
return false;
}
// A list of capabilities that offer feature parity
// with custom notifications
return ranges::all_of(std::array{
// To show message content
"body",
// To have buttons on notifications
"actions",
// To have quick reply
"inline-reply",
}, HasCapability) && ranges::any_of(std::array{
// To not to play sound with Don't Disturb activated
"sound",
"inhibitions",
}, HasCapability);
}
bool VolumeSupported() {
return UseGNotification() || !HasCapability("sound");
}
void Create(Window::Notifications::System *system) {
static const auto ServiceWatcher = CreateServiceWatcher();
const auto managerSetter = [=](
XdgNotifications::NotificationsProxy proxy) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
system->setManager([=] {
auto manager = std::make_unique<Manager>(system);
manager->_private->init(proxy);
return manager;
});
});
};
const auto counter = std::make_shared<int>(2);
const auto oneReady = [=](XdgNotifications::NotificationsProxy proxy) {
if (!--*counter) {
managerSetter(proxy);
}
};
XdgNotifications::NotificationsProxy::new_for_bus(
Gio::BusType::SESSION_,
Gio::DBusProxyFlags::NONE_,
kService,
kObjectPath,
[=](GObject::Object, Gio::AsyncResult res) {
auto result =
XdgNotifications::NotificationsProxy::new_for_bus_finish(res);
if (result) {
ServiceRegistered = bool(result->get_name_owner());
} else {
Gio::DBusErrorNS_::strip_remote_error(result.error());
LOG(("Native Notification Error: %1").arg(
result.error().message_().c_str()));
ServiceRegistered = false;
}
if (!ServiceRegistered) {
CurrentServerInformation = {};
CurrentCapabilities = {};
managerSetter({});
return;
}
auto proxy = *result;
auto interface = XdgNotifications::Notifications(proxy);
interface.call_get_server_information([=](
GObject::Object,
Gio::AsyncResult res) mutable {
const auto result =
interface.call_get_server_information_finish(res);
if (result) {
CurrentServerInformation = {
std::get<1>(*result),
std::get<2>(*result),
QVersionNumber::fromString(
QString::fromStdString(std::get<3>(*result))
).normalized(),
QVersionNumber::fromString(
QString::fromStdString(std::get<4>(*result))
).normalized(),
};
} else {
Gio::DBusErrorNS_::strip_remote_error(result.error());
LOG(("Native Notification Error: %1").arg(
result.error().message_().c_str()));
CurrentServerInformation = {};
}
oneReady(proxy);
});
interface.call_get_capabilities([=](
GObject::Object,
Gio::AsyncResult res) mutable {
const auto result = interface.call_get_capabilities_finish(
res);
if (result) {
CurrentCapabilities = std::get<1>(*result)
| ranges::to<std::vector<std::string>>;
} else {
Gio::DBusErrorNS_::strip_remote_error(result.error());
LOG(("Native Notification Error: %1").arg(
result.error().message_().c_str()));
CurrentCapabilities = {};
}
oneReady(proxy);
});
});
}
Manager::Private::Private(not_null<Manager*> manager)
: _manager(manager)
, _application(UseGNotification()
? Gio::Application::get_default()
: nullptr)
, _sounds(cWorkingDir() + u"tdata/audio_cache"_q) {
const auto &serverInformation = CurrentServerInformation;
if (!serverInformation.name.empty()) {
LOG(("Notification daemon product name: %1")
.arg(serverInformation.name.c_str()));
}
if (!serverInformation.vendor.empty()) {
LOG(("Notification daemon vendor name: %1")
.arg(serverInformation.vendor.c_str()));
}
if (!serverInformation.version.isNull()) {
LOG(("Notification daemon version: %1")
.arg(serverInformation.version.toString()));
}
if (!serverInformation.specVersion.isNull()) {
LOG(("Notification daemon specification version: %1")
.arg(serverInformation.specVersion.toString()));
}
if (!CurrentCapabilities.empty()) {
LOG(("Notification daemon capabilities: %1").arg(
ranges::fold_left(
CurrentCapabilities,
"",
[](const std::string &a, const std::string &b) {
return a + (a.empty() ? "" : ", ") + b;
}).c_str()));
}
if (auto actionMap = Gio::ActionMap(_application)) {
const auto dictToNotificationId = [](GLib::VariantDict dict) {
return NotificationId{
.contextId = ContextId{
.sessionId = dict.lookup_value("session").get_uint64(),
.peerId = PeerId(dict.lookup_value("peer").get_uint64()),
.topicRootId = MsgId(
dict.lookup_value("topic").get_int64()),
.monoforumPeerId = PeerId(dict.lookup_value(
"monoforumpeer").get_uint64()),
},
.msgId = dict.lookup_value("msgid").get_int64(),
};
};
auto activate = gi::object_cast<Gio::SimpleAction>(
actionMap.lookup_action("notification-activate"));
const auto activateSig = activate.signal_activate().connect([=](
Gio::SimpleAction,
GLib::Variant parameter) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
_manager->notificationActivated(
dictToNotificationId(GLib::VariantDict::new_(parameter)));
});
});
_lifetime.add([=]() mutable {
activate.disconnect(activateSig);
});
auto markAsRead = gi::object_cast<Gio::SimpleAction>(
actionMap.lookup_action("notification-mark-as-read"));
const auto markAsReadSig = markAsRead.signal_activate().connect([=](
Gio::SimpleAction,
GLib::Variant parameter) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
_manager->notificationReplied(
dictToNotificationId(GLib::VariantDict::new_(parameter)),
{});
});
});
_lifetime.add([=]() mutable {
markAsRead.disconnect(markAsReadSig);
});
}
}
void Manager::Private::init(XdgNotifications::NotificationsProxy proxy) {
_proxy = proxy;
_interface = proxy;
if (_application || !_interface) {
return;
}
const auto actionInvoked = _interface.signal_action_invoked().connect([=](
XdgNotifications::Notifications,
uint id,
std::string actionName) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
for (const auto &[key, notifications] : _notifications) {
for (const auto &[msgId, notification] : notifications) {
const auto &nid = notification->id;
if (v::is<uint>(nid) && v::get<uint>(nid) == id) {
if (actionName == "default") {
_manager->notificationActivated({ key, msgId });
} else if (actionName == "mail-mark-read") {
_manager->notificationReplied({ key, msgId }, {});
}
return;
}
}
}
});
});
_lifetime.add([=] {
_interface.disconnect(actionInvoked);
});
const auto replied = _interface.signal_notification_replied().connect([=](
XdgNotifications::Notifications,
uint id,
std::string text) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
for (const auto &[key, notifications] : _notifications) {
for (const auto &[msgId, notification] : notifications) {
const auto &nid = notification->id;
if (v::is<uint>(nid) && v::get<uint>(nid) == id) {
_manager->notificationReplied(
{ key, msgId },
{ QString::fromStdString(text), {} });
return;
}
}
}
});
});
_lifetime.add([=] {
_interface.disconnect(replied);
});
const auto tokenSignal = _interface.signal_activation_token().connect([=](
XdgNotifications::Notifications,
uint id,
std::string token) {
for (const auto &[key, notifications] : _notifications) {
for (const auto &[msgId, notification] : notifications) {
const auto &nid = notification->id;
if (v::is<uint>(nid) && v::get<uint>(nid) == id) {
GLib::setenv("XDG_ACTIVATION_TOKEN", token, true);
return;
}
}
}
});
_lifetime.add([=] {
_interface.disconnect(tokenSignal);
});
const auto closed = _interface.signal_notification_closed().connect([=](
XdgNotifications::Notifications,
uint id,
uint reason) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
for (const auto &[key, notifications] : _notifications) {
for (const auto &[msgId, notification] : notifications) {
/*
* From: https://specifications.freedesktop.org/notification-spec/latest/ar01s09.html
* The reason the notification was closed
* 1 - The notification expired.
* 2 - The notification was dismissed by the user.
* 3 - The notification was closed by a call to CloseNotification.
* 4 - Undefined/reserved reasons.
*
* If the notification was dismissed by the user (reason == 2), the notification is not kept in notification history.
* We do not need to send a "CloseNotification" call later to clear it from history.
* Therefore we can drop the notification reference now.
* In all other cases we keep the notification reference so that we may clear the notification later from history,
* if the message for that notification is read (e.g. chat is opened or read from another device).
*/
const auto &nid = notification->id;
if (v::is<uint>(nid) && v::get<uint>(nid) == id && reason == 2) {
clearNotification({ key, msgId });
return;
}
}
}
});
});
_lifetime.add([=] {
_interface.disconnect(closed);
});
}
void Manager::Private::showNotification(
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView) {
const auto peer = info.peer;
const auto options = info.options;
const auto key = ContextId{
.sessionId = peer->session().uniqueId(),
.peerId = peer->id,
.topicRootId = info.topicRootId,
.monoforumPeerId = info.monoforumPeerId,
};
const auto notificationId = NotificationId{
.contextId = key,
.msgId = info.itemId,
};
auto notification = _application
? Gio::Notification::new_(info.title.toStdString())
: Gio::Notification();
std::vector<gi::cstring> actions;
auto hints = GLib::VariantDict::new_();
if (notification) {
notification.set_body(info.subtitle.isEmpty()
? info.message.toStdString()
: tr::lng_dialogs_text_with_from(
tr::now,
lt_from_part,
tr::lng_dialogs_text_from_wrapped(
tr::now,
lt_from,
info.subtitle),
lt_message,
info.message).toStdString());
notification.set_icon(
Gio::ThemedIcon::new_(ApplicationIconName().toStdString()));
// for chat messages, according to
// https://docs.gtk.org/gio/enum.NotificationPriority.html
notification.set_priority(Gio::NotificationPriority::HIGH_);
// glib 2.70+, we keep glib 2.56+ compatibility
static const auto set_category = [] {
// reset dlerror after dlsym call
const auto guard = gsl::finally([] { dlerror(); });
return reinterpret_cast<void(*)(GNotification*, const gchar*)>(
dlsym(RTLD_DEFAULT, "g_notification_set_category"));
}();
if (set_category) {
set_category(notification.gobj_(), "im.received");
}
const auto notificationVariant = GLib::Variant::new_array({
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("session"),
GLib::Variant::new_variant(
GLib::Variant::new_uint64(peer->session().uniqueId()))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("peer"),
GLib::Variant::new_variant(
GLib::Variant::new_uint64(peer->id.value))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("peer"),
GLib::Variant::new_variant(
GLib::Variant::new_uint64(peer->id.value))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("topic"),
GLib::Variant::new_variant(
GLib::Variant::new_int64(info.topicRootId.bare))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("monoforumpeer"),
GLib::Variant::new_variant(
GLib::Variant::new_uint64(info.monoforumPeerId.value))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("msgid"),
GLib::Variant::new_variant(
GLib::Variant::new_int64(info.itemId.bare))),
});
notification.set_default_action_and_target(
"app.notification-activate",
notificationVariant);
if (!options.hideMarkAsRead) {
notification.add_button_with_target(
tr::lng_context_mark_read(tr::now).toStdString(),
"app.notification-mark-as-read",
notificationVariant);
}
} else {
if (HasCapability("actions")) {
actions.push_back("default");
actions.push_back(tr::lng_open_link(tr::now).toStdString());
if (!options.hideMarkAsRead) {
// icon name according to https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html
actions.push_back("mail-mark-read");
actions.push_back(
tr::lng_context_mark_read(tr::now).toStdString());
}
if (HasCapability("inline-reply")
&& !options.hideReplyButton) {
actions.push_back("inline-reply");
actions.push_back(
tr::lng_notification_reply(tr::now).toStdString());
}
}
actions.push_back({});
if (HasCapability("action-icons")) {
hints.insert_value(
"action-icons",
GLib::Variant::new_boolean(true));
}
if (HasCapability("sound")) {
const auto sound = info.sound
? info.sound()
: Media::Audio::LocalSound();
const auto path = sound
? _sounds.path(sound).toStdString()
: std::string();
if (!path.empty()) {
hints.insert_value(
"sound-file",
GLib::Variant::new_string(path));
} else {
hints.insert_value(
"suppress-sound",
GLib::Variant::new_boolean(true));
}
}
if (HasCapability("x-canonical-append")) {
hints.insert_value(
"x-canonical-append",
GLib::Variant::new_string("true"));
}
hints.insert_value(
"category",
GLib::Variant::new_string("im.received"));
hints.insert_value("desktop-entry", GLib::Variant::new_string(
QGuiApplication::desktopFileName().toStdString()));
}
const auto imageKey = GetImageKey();
if (!options.hideNameAndPhoto) {
if (notification) {
QByteArray imageData;
QBuffer buffer(&imageData);
buffer.open(QIODevice::WriteOnly);
Window::Notifications::GenerateUserpic(peer, userpicView).save(
&buffer,
"PNG");
notification.set_icon(
Gio::BytesIcon::new_(
GLib::Bytes::new_with_free_func(
reinterpret_cast<const uchar*>(imageData.constData()),
imageData.size(),
[imageData] {})));
} else if (!imageKey.empty()) {
const auto image = Window::Notifications::GenerateUserpic(
peer,
userpicView
).convertToFormat(QImage::Format_RGBA8888);
hints.insert_value(imageKey, GLib::Variant::new_tuple({
GLib::Variant::new_int32(image.width()),
GLib::Variant::new_int32(image.height()),
GLib::Variant::new_int32(image.bytesPerLine()),
GLib::Variant::new_boolean(true),
GLib::Variant::new_int32(8),
GLib::Variant::new_int32(4),
GLib::Variant::new_from_data(
GLib::VariantType::new_("ay"),
reinterpret_cast<const uchar*>(image.constBits()),
image.sizeInBytes(),
true,
[image] {}),
}));
}
}
const auto &data
= _notifications[key][info.itemId]
= std::make_unique<NotificationData>();
data->lifetime.add([=, notification = data.get()] {
v::match(notification->id, [&](const std::string &id) {
_application.withdraw_notification(id);
}, [&](uint id) {
_interface.call_close_notification(id, nullptr);
}, [](v::null_t) {});
});
if (notification) {
const auto id = Gio::dbus_generate_guid();
data->id = id;
_application.send_notification(id, notification);
} else {
// work around snap's activation restriction
const auto weak = base::make_weak(data);
StartServiceAsync(
_proxy.get_connection(),
crl::guard(weak, [=]() mutable {
const auto hasImage = !imageKey.empty()
&& hints.lookup_value(imageKey);
const auto callbackWrap = gi::unwrap(
Gio::AsyncReadyCallback(
crl::guard(this, [=](
GObject::Object,
Gio::AsyncResult res) {
auto &sandbox = Core::Sandbox::Instance();
sandbox.customEnterFromEventLoop([&] {
const auto result
= _interface.call_notify_finish(res);
if (!result) {
Gio::DBusErrorNS_::strip_remote_error(
result.error());
LOG(("Native Notification Error: %1").arg(
result.error().message_().c_str()));
clearNotification(notificationId);
return;
}
if (!weak) {
_interface.call_close_notification(
std::get<1>(*result),
nullptr);
return;
}
weak->id = std::get<1>(*result);
});
})),
gi::scope_async);
xdg_notifications_notifications_call_notify(
_interface.gobj_(),
AppName.data(),
0,
(!hasImage
? ApplicationIconName().toStdString()
: std::string()).c_str(),
info.title.toStdString().c_str(),
(HasCapability("body-markup")
? info.subtitle.isEmpty()
? info.message.toHtmlEscaped().toStdString()
: u"<b>%1</b>\n%2"_q.arg(
info.subtitle.toHtmlEscaped(),
info.message.toHtmlEscaped()).toStdString()
: info.subtitle.isEmpty()
? info.message.toStdString()
: tr::lng_dialogs_text_with_from(
tr::now,
lt_from_part,
tr::lng_dialogs_text_from_wrapped(
tr::now,
lt_from,
info.subtitle),
lt_message,
info.message).toStdString()).c_str(),
(actions
| ranges::views::transform(&gi::cstring::c_str)
| ranges::to_vector).data(),
hints.end().gobj_(),
-1,
nullptr,
&callbackWrap->wrapper,
callbackWrap);
}));
}
}
void Manager::Private::clearAll() {
_notifications.clear();
}
void Manager::Private::clearFromItem(not_null<HistoryItem*> item) {
const auto i = _notifications.find(ContextId{
.sessionId = item->history()->session().uniqueId(),
.peerId = item->history()->peer->id,
.topicRootId = item->topicRootId(),
.monoforumPeerId = item->sublistPeerId(),
});
if (i != _notifications.cend()
&& i->second.remove(item->id)
&& i->second.empty()) {
_notifications.erase(i);
}
}
void Manager::Private::clearFromTopic(not_null<Data::ForumTopic*> topic) {
_notifications.remove(ContextId{
.sessionId = topic->session().uniqueId(),
.peerId = topic->history()->peer->id,
.topicRootId = topic->rootId(),
});
}
void Manager::Private::clearFromSublist(
not_null<Data::SavedSublist*> sublist) {
_notifications.remove(ContextId{
.sessionId = sublist->session().uniqueId(),
.peerId = sublist->owningHistory()->peer->id,
.monoforumPeerId = sublist->sublistPeer()->id,
});
}
void Manager::Private::clearFromHistory(not_null<History*> history) {
const auto sessionId = history->session().uniqueId();
const auto peerId = history->peer->id;
auto i = _notifications.lower_bound(ContextId{
.sessionId = sessionId,
.peerId = peerId,
});
while (i != _notifications.cend()
&& i->first.sessionId == sessionId
&& i->first.peerId == peerId) {
i = _notifications.erase(i);
}
}
void Manager::Private::clearFromSession(not_null<Main::Session*> session) {
const auto sessionId = session->uniqueId();
auto i = _notifications.lower_bound(ContextId{
.sessionId = sessionId,
});
while (i != _notifications.cend() && i->first.sessionId == sessionId) {
i = _notifications.erase(i);
}
}
void Manager::Private::clearNotification(NotificationId id) {
auto i = _notifications.find(id.contextId);
if (i != _notifications.cend()
&& i->second.remove(id.msgId)
&& i->second.empty()) {
_notifications.erase(i);
}
}
void Manager::Private::invokeIfNotInhibited(Fn<void()> callback) {
if (!_interface.get_inhibited()) {
callback();
}
}
Manager::Manager(not_null<Window::Notifications::System*> system)
: NativeManager(system)
, _private(std::make_unique<Private>(this)) {
}
Manager::~Manager() = default;
void Manager::doShowNativeNotification(
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView) {
_private->showNotification(std::move(info), userpicView);
}
void Manager::doClearAllFast() {
_private->clearAll();
}
void Manager::doClearFromItem(not_null<HistoryItem*> item) {
_private->clearFromItem(item);
}
void Manager::doClearFromTopic(not_null<Data::ForumTopic*> topic) {
_private->clearFromTopic(topic);
}
void Manager::doClearFromSublist(not_null<Data::SavedSublist*> sublist) {
_private->clearFromSublist(sublist);
}
void Manager::doClearFromHistory(not_null<History*> history) {
_private->clearFromHistory(history);
}
void Manager::doClearFromSession(not_null<Main::Session*> session) {
_private->clearFromSession(session);
}
bool Manager::doSkipToast() const {
return false;
}
void Manager::doMaybePlaySound(Fn<void()> playSound) {
_private->invokeIfNotInhibited(std::move(playSound));
}
void Manager::doMaybeFlashBounce(Fn<void()> flashBounce) {
_private->invokeIfNotInhibited(std::move(flashBounce));
}
} // namespace Notifications
} // namespace Platform

View File

@@ -0,0 +1,42 @@
/*
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 "platform/platform_notifications_manager.h"
namespace Platform {
namespace Notifications {
class Manager : public Window::Notifications::NativeManager {
public:
Manager(not_null<Window::Notifications::System*> system);
~Manager();
protected:
void doShowNativeNotification(
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView) override;
void doClearAllFast() override;
void doClearFromItem(not_null<HistoryItem*> item) override;
void doClearFromTopic(not_null<Data::ForumTopic*> topic) override;
void doClearFromSublist(not_null<Data::SavedSublist*> sublist) override;
void doClearFromHistory(not_null<History*> history) override;
void doClearFromSession(not_null<Main::Session*> session) override;
bool doSkipToast() const override;
void doMaybePlaySound(Fn<void()> playSound) override;
void doMaybeFlashBounce(Fn<void()> flashBounce) override;
private:
friend void Create(Window::Notifications::System *system);
class Private;
const std::unique_ptr<Private> _private;
};
} // namespace Notifications
} // namespace Platform

View File

@@ -0,0 +1,59 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.freedesktop.Notifications">
<signal name="NotificationClosed">
<arg direction="out" type="u" name="id"/>
<arg direction="out" type="u" name="reason"/>
</signal>
<signal name="ActionInvoked">
<arg direction="out" type="u" name="id"/>
<arg direction="out" type="s" name="action_key"/>
</signal>
<signal name="NotificationReplied">
<arg direction="out" type="u" name="id"/>
<arg direction="out" type="s" name="text"/>
</signal>
<signal name="ActivationToken">
<arg direction="out" type="u" name="id"/>
<arg direction="out" type="s" name="activation_token"/>
</signal>
<method name="Notify">
<annotation value="QVariantMap" name="org.qtproject.QtDBus.QtTypeName.In6"/>
<arg direction="out" type="u"/>
<arg direction="in" type="s" name="app_name"/>
<arg direction="in" type="u" name="replaces_id"/>
<arg direction="in" type="s" name="app_icon"/>
<arg direction="in" type="s" name="summary"/>
<arg direction="in" type="s" name="body"/>
<arg direction="in" type="as" name="actions"/>
<arg direction="in" type="a{sv}" name="hints"/>
<arg direction="in" type="i" name="timeout"/>
</method>
<method name="CloseNotification">
<arg direction="in" type="u" name="id"/>
</method>
<method name="GetCapabilities">
<arg direction="out" type="as" name="caps"/>
</method>
<method name="GetServerInformation">
<arg direction="out" type="s" name="name"/>
<arg direction="out" type="s" name="vendor"/>
<arg direction="out" type="s" name="version"/>
<arg direction="out" type="s" name="spec_version"/>
</method>
<method name="Inhibit">
<annotation value="QVariantMap" name="org.qtproject.QtDBus.QtTypeName.In2"/>
<arg direction="out" type="u"/>
<arg direction="in" type="s" name="desktop_entry"/>
<arg direction="in" type="s" name="reason"/>
<arg direction="in" type="a{sv}" name="hints"/>
</method>
<method name="UnInhibit">
<arg direction="in" type="u"/>
</method>
<property access="read" type="b" name="Inhibited">
<annotation value="true" name="org.freedesktop.DBus.Property.EmitsChangedSignal"/>
</property>
</interface>
</node>

View File

@@ -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
namespace Platform {
inline std::unique_ptr<OverlayWidgetHelper> CreateOverlayWidgetHelper(
not_null<Ui::RpWindow*> window,
Fn<void(bool)> maximize) {
return std::make_unique<DefaultOverlayWidgetHelper>(
window,
std::move(maximize));
}
} // namespace Platform

View File

@@ -0,0 +1,915 @@
/*
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/linux/specific_linux.h"
#include "base/openssl_help.h"
#include "base/random.h"
#include "base/platform/base_platform_info.h"
#include "base/platform/linux/base_linux_dbus_utilities.h"
#include "base/platform/linux/base_linux_xdp_utilities.h"
#include "base/platform/linux/base_linux_app_launch_context.h"
#include "lang/lang_keys.h"
#include "core/launcher.h"
#include "core/sandbox.h"
#include "core/application.h"
#include "core/update_checker.h"
#include "data/data_location.h"
#include "window/window_controller.h"
#include "webview/platform/linux/webview_linux_webkitgtk.h"
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
#include "base/platform/linux/base_linux_xcb_utilities.h"
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
#include <QtWidgets/QApplication>
#include <QtWidgets/QSystemTrayIcon>
#include <QtGui/QDesktopServices>
#include <QtCore/QStandardPaths>
#include <QtCore/QProcess>
#include <kshell.h>
#include <ksandbox.h>
#include <xdgdbus/xdgdbus.hpp>
#include <xdpbackground/xdpbackground.hpp>
#include <xdpopenuri/xdpopenuri.hpp>
#include <xdprequest/xdprequest.hpp>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/un.h>
#include <cstdlib>
#include <unistd.h>
#include <dirent.h>
#include <pwd.h>
namespace {
using namespace gi::repository;
namespace GObject = gi::repository::GObject;
using namespace Platform;
void PortalAutostart(bool enabled, Fn<void(bool)> done) {
const auto executable = ExecutablePathForShortcuts();
if (executable.isEmpty()) {
if (done) {
done(false);
}
return;
}
XdpBackground::BackgroundProxy::new_for_bus(
Gio::BusType::SESSION_,
Gio::DBusProxyFlags::NONE_,
base::Platform::XDP::kService,
base::Platform::XDP::kObjectPath,
[=](GObject::Object, Gio::AsyncResult res) {
auto proxy = XdpBackground::BackgroundProxy::new_for_bus_finish(
res);
if (!proxy) {
if (done) {
Gio::DBusErrorNS_::strip_remote_error(proxy.error());
LOG(("Portal Autostart Error: %1").arg(
proxy.error().message_().c_str()));
done(false);
}
return;
}
auto interface = XdpBackground::Background(*proxy);
const auto handleToken = "tdesktop"
+ std::to_string(base::RandomValue<uint>());
auto uniqueName = std::string(
proxy->get_connection().get_unique_name());
uniqueName.erase(0, 1);
uniqueName.replace(uniqueName.find('.'), 1, 1, '_');
const auto parent = []() -> QPointer<QWidget> {
const auto active = Core::App().activeWindow();
if (!active) {
return nullptr;
}
return active->widget().get();
}();
const auto window = std::make_shared<base::unique_qptr<QWidget>>(
std::in_place,
parent);
auto &raw = **window;
raw.setAttribute(Qt::WA_DontShowOnScreen);
raw.setWindowFlag(Qt::Window);
raw.setWindowModality(Qt::WindowModal);
raw.show();
XdpRequest::RequestProxy::new_(
proxy->get_connection(),
Gio::DBusProxyFlags::NONE_,
base::Platform::XDP::kService,
base::Platform::XDP::kObjectPath
+ std::string("/request/")
+ uniqueName
+ '/'
+ handleToken,
nullptr,
[=](GObject::Object, Gio::AsyncResult res) mutable {
auto requestProxy = XdpRequest::RequestProxy::new_finish(
res);
if (!requestProxy) {
if (done) {
Gio::DBusErrorNS_::strip_remote_error(
requestProxy.error());
LOG(("Portal Autostart Error: %1").arg(
requestProxy.error().message_().c_str()));
done(false);
}
return;
}
auto request = XdpRequest::Request(*requestProxy);
const auto signalId = std::make_shared<ulong>();
*signalId = request.signal_response().connect([=](
XdpRequest::Request,
guint response,
GLib::Variant) mutable {
auto &sandbox = Core::Sandbox::Instance();
sandbox.customEnterFromEventLoop([&] {
(void)window; // don't destroy until finish
if (response) {
if (done) {
LOG(("Portal Autostart Error: "
"Request denied"));
done(false);
}
} else if (done) {
done(enabled);
}
request.disconnect(*signalId);
});
});
std::vector<std::string> commandline;
commandline.push_back(executable.toStdString());
if (Core::Launcher::Instance().customWorkingDir()) {
commandline.push_back("-workdir");
commandline.push_back(cWorkingDir().toStdString());
}
commandline.push_back("-autostart");
interface.call_request_background(
base::Platform::XDP::ParentWindowID(parent
? parent->windowHandle()
: nullptr),
GLib::Variant::new_array({
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("handle_token"),
GLib::Variant::new_variant(
GLib::Variant::new_string(handleToken))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("reason"),
GLib::Variant::new_variant(
GLib::Variant::new_string(
tr::lng_settings_auto_start(tr::now)
.toStdString()))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("autostart"),
GLib::Variant::new_variant(
GLib::Variant::new_boolean(enabled))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("commandline"),
GLib::Variant::new_variant(
GLib::Variant::new_strv(commandline))),
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("dbus-activatable"),
GLib::Variant::new_variant(
GLib::Variant::new_boolean(false))),
}),
[=](GObject::Object, Gio::AsyncResult res) mutable {
auto &sandbox = Core::Sandbox::Instance();
sandbox.customEnterFromEventLoop([&] {
const auto result =
interface.call_request_background_finish(
res);
if (!result) {
if (done) {
const auto &error = result.error();
Gio::DBusErrorNS_::strip_remote_error(
error);
LOG(("Portal Autostart Error: %1").arg(
error.message_().c_str()));
done(false);
}
request.disconnect(*signalId);
}
});
});
});
});
}
bool GenerateDesktopFile(
const QString &targetPath,
const QStringList &args = {},
bool onlyMainGroup = false,
bool silent = false) {
const auto executable = ExecutablePathForShortcuts();
if (targetPath.isEmpty() || executable.isEmpty()) {
return false;
}
DEBUG_LOG(("App Info: placing .desktop file to %1").arg(targetPath));
if (!QDir(targetPath).exists()) QDir().mkpath(targetPath);
const auto sourceFile = u":/misc/org.telegram.desktop.desktop"_q;
const auto targetFile = targetPath
+ QGuiApplication::desktopFileName()
+ u".desktop"_q;
const auto sourceText = [&] {
QFile source(sourceFile);
if (source.open(QIODevice::ReadOnly)) {
return source.readAll().toStdString();
}
return std::string();
}();
if (sourceText.empty()) {
if (!silent) {
LOG(("App Error: Could not open '%1' for read").arg(sourceFile));
}
return false;
}
auto target = GLib::KeyFile::new_();
const auto loaded = target.load_from_data(
sourceText,
-1,
GLib::KeyFileFlags::KEEP_COMMENTS_
| GLib::KeyFileFlags::KEEP_TRANSLATIONS_);
if (!loaded) {
if (!silent) {
LOG(("App Error: %1").arg(loaded.error().message_().c_str()));
}
return false;
}
for (const auto &group : target.get_groups(nullptr)) {
if (onlyMainGroup && group != "Desktop Entry") {
const auto removed = target.remove_group(group);
if (!removed) {
if (!silent) {
LOG(("App Error: %1").arg(
removed.error().message_().c_str()));
}
return false;
}
continue;
}
if (target.has_key(group, "TryExec", nullptr)) {
target.set_string(
group,
"TryExec",
KShell::joinArgs({ executable }).replace(
'\\',
qstr("\\\\")).toStdString());
}
if (target.has_key(group, "Exec", nullptr)) {
if (group == "Desktop Entry" && !args.isEmpty()) {
QStringList exec;
exec.append(executable);
if (Core::Launcher::Instance().customWorkingDir()) {
exec.append(u"-workdir"_q);
exec.append(cWorkingDir());
}
exec.append(args);
target.set_string(
group,
"Exec",
KShell::joinArgs(exec).replace(
'\\',
qstr("\\\\")).toStdString());
} else {
auto exec = KShell::splitArgs(
QString::fromStdString(
target.get_string(group, "Exec", nullptr)
).replace(
qstr("\\\\"),
qstr("\\")));
if (!exec.isEmpty()) {
exec[0] = executable;
if (Core::Launcher::Instance().customWorkingDir()) {
exec.insert(1, u"-workdir"_q);
exec.insert(2, cWorkingDir());
}
target.set_string(
group,
"Exec",
KShell::joinArgs(exec).replace(
'\\',
qstr("\\\\")).toStdString());
}
}
}
}
if (!args.isEmpty()) {
target.remove_key("Desktop Entry", "DBusActivatable");
}
const auto saved = target.save_to_file(targetFile.toStdString());
if (!saved) {
if (!silent) {
LOG(("App Error: %1").arg(saved.error().message_().c_str()));
}
return false;
}
QFile::setPermissions(
targetFile,
QFile::permissions(targetFile)
| QFileDevice::ExeOwner
| QFileDevice::ExeGroup
| QFileDevice::ExeOther);
if (!Core::UpdaterDisabled()) {
DEBUG_LOG(("App Info: removing old .desktop files"));
QFile::remove(u"%1telegram.desktop"_q.arg(targetPath));
QFile::remove(u"%1telegramdesktop.desktop"_q.arg(targetPath));
const auto appimagePath = u"file://%1%2"_q.arg(
cExeDir(),
cExeName()).toUtf8();
char md5Hash[33] = { 0 };
hashMd5Hex(
appimagePath.constData(),
appimagePath.size(),
md5Hash);
QFile::remove(u"%1appimagekit_%2-%3.desktop"_q.arg(
targetPath,
md5Hash,
AppName.utf16().replace(' ', '_')));
const auto d = QFile::encodeName(QDir(cWorkingDir()).absolutePath());
hashMd5Hex(d.constData(), d.size(), md5Hash);
if (!Core::Launcher::Instance().customWorkingDir()) {
QFile::remove(u"%1org.telegram.desktop._%2.desktop"_q.arg(
targetPath,
md5Hash));
const auto exePath = QFile::encodeName(
cExeDir() + cExeName());
hashMd5Hex(exePath.constData(), exePath.size(), md5Hash);
}
QFile::remove(u"%1org.telegram.desktop.%2.desktop"_q.arg(
targetPath,
md5Hash));
}
return true;
}
bool GenerateServiceFile(bool silent = false) {
const auto executable = ExecutablePathForShortcuts();
if (executable.isEmpty()) {
return false;
}
const auto targetPath = QStandardPaths::writableLocation(
QStandardPaths::GenericDataLocation) + u"/dbus-1/services/"_q;
const auto targetFile = targetPath
+ QGuiApplication::desktopFileName()
+ u".service"_q;
DEBUG_LOG(("App Info: placing D-Bus service file to %1").arg(targetPath));
if (!QDir(targetPath).exists()) QDir().mkpath(targetPath);
auto target = GLib::KeyFile::new_();
constexpr auto group = "D-BUS Service";
target.set_string(
group,
"Name",
QGuiApplication::desktopFileName().toStdString());
QStringList exec;
exec.append(executable);
if (Core::Launcher::Instance().customWorkingDir()) {
exec.append(u"-workdir"_q);
exec.append(cWorkingDir());
}
target.set_string(
group,
"Exec",
KShell::joinArgs(exec).toStdString());
const auto saved = target.save_to_file(targetFile.toStdString());
if (!saved) {
if (!silent) {
LOG(("App Error: %1").arg(saved.error().message_().c_str()));
}
return false;
}
if (!Core::UpdaterDisabled()
&& !Core::Launcher::Instance().customWorkingDir()) {
DEBUG_LOG(("App Info: removing old D-Bus service files"));
char md5Hash[33] = { 0 };
const auto d = QFile::encodeName(QDir(cWorkingDir()).absolutePath());
hashMd5Hex(d.constData(), d.size(), md5Hash);
QFile::remove(u"%1org.telegram.desktop._%2.service"_q.arg(
targetPath,
md5Hash));
}
XdgDBus::DBusProxy::new_for_bus(
Gio::BusType::SESSION_,
Gio::DBusProxyFlags::NONE_,
base::Platform::DBus::kService,
base::Platform::DBus::kObjectPath,
[=](GObject::Object, Gio::AsyncResult res) {
auto interface = XdgDBus::DBus(
XdgDBus::DBusProxy::new_for_bus_finish(res, nullptr));
if (!interface) {
return;
}
interface.call_reload_config(nullptr);
});
return true;
}
void InstallLauncher() {
static const auto DisabledByEnv = !qEnvironmentVariableIsEmpty(
"DESKTOPINTEGRATION");
// don't update desktop file for alpha version or if updater is disabled
if (cAlphaVersion() || Core::UpdaterDisabled() || DisabledByEnv) {
return;
}
const auto applicationsPath = QStandardPaths::writableLocation(
QStandardPaths::ApplicationsLocation) + '/';
GenerateDesktopFile(applicationsPath);
GenerateServiceFile();
const auto icons = QStandardPaths::writableLocation(
QStandardPaths::GenericDataLocation) + u"/icons/"_q;
const auto appIcons = icons + u"/hicolor/256x256/apps/"_q;
if (!QDir(appIcons).exists()) QDir().mkpath(appIcons);
const auto icon = appIcons + ApplicationIconName() + u".png"_q;
QFile::remove(icon);
QFile::remove(icons + u"telegram.png"_q);
if (QFile::copy(u":/gui/art/logo_256.png"_q, icon)) {
DEBUG_LOG(("App Info: Icon copied to '%1'").arg(icon));
}
const auto symbolicIcons = icons + u"/hicolor/symbolic/apps/"_q;
if (!QDir().exists(symbolicIcons)) QDir().mkpath(symbolicIcons);
const auto monochromeIcons = {
QString(),
u"attention"_q,
u"mute"_q,
};
for (const auto &icon : monochromeIcons) {
QFile::copy(
u":/gui/icons/tray/monochrome%1.svg"_q.arg(
!icon.isEmpty() ? u"_"_q + icon : QString()),
symbolicIcons
+ ApplicationIconName()
+ (!icon.isEmpty() ? u"-"_q + icon : QString())
+ u"-symbolic.svg"_q);
}
QProcess::execute("update-desktop-database", {
applicationsPath
});
}
[[nodiscard]] QByteArray HashForSocketPath() {
constexpr auto kHashForSocketPathLength = 24;
const auto binary = openssl::Sha256(
bytes::make_span(Core::Launcher::Instance().instanceHash()));
const auto base64 = QByteArray(
reinterpret_cast<const char*>(binary.data()),
binary.size()).toBase64(QByteArray::Base64UrlEncoding);
return base64.mid(0, kHashForSocketPathLength);
}
void AppInfoCheckScheme(
const std::string &scheme,
Fn<void(Gio::AppInfo, Fn<void()>)> callback,
Fn<void()> fail) {
// TODO: use get_default_for_uri_scheme_async once we can use GLib 2.74
if (auto appInfo = Gio::AppInfo::get_default_for_uri_scheme(scheme)) {
callback(appInfo, fail);
return;
}
fail();
}
void PortalCheckScheme(
const std::string &scheme,
Fn<void(Fn<void()>)> callback,
Fn<void()> fail) {
XdpOpenURI::OpenURIProxy::new_for_bus(
Gio::BusType::SESSION_,
Gio::DBusProxyFlags::NONE_,
base::Platform::XDP::kService,
base::Platform::XDP::kObjectPath,
[=](GObject::Object, Gio::AsyncResult res) {
auto interface = XdpOpenURI::OpenURI(
XdpOpenURI::OpenURIProxy::new_for_bus_finish(res, nullptr));
if (!interface) {
fail();
return;
}
interface.call_scheme_supported(
scheme,
GLib::Variant::new_array(
GLib::VariantType::new_("{sv}"),
{}),
[=](GObject::Object, Gio::AsyncResult res) mutable {
const auto result
= interface.call_scheme_supported_finish(res);
if (!result || !std::get<1>(*result)) {
fail();
return;
}
callback(fail);
});
});
}
} // namespace
namespace Platform {
void SetApplicationIcon(const QIcon &icon) {
QApplication::setWindowIcon(icon);
}
QString SingleInstanceLocalServerName(const QString &hash) {
#if defined Q_OS_LINUX && QT_VERSION >= QT_VERSION_CHECK(6, 2, 0)
if (KSandbox::isSnap()) {
return u"snap."_q
+ qEnvironmentVariable("SNAP_INSTANCE_NAME")
+ '.'
+ hash;
}
return hash + '-' + QCoreApplication::applicationName();
#else // Q_OS_LINUX && Qt >= 6.2.0
return QDir::tempPath()
+ '/'
+ hash
+ '-'
+ QCoreApplication::applicationName();
#endif // !Q_OS_LINUX || Qt < 6.2.0
}
#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
std::optional<bool> IsDarkMode() {
auto result = base::Platform::XDP::ReadSetting(
"org.freedesktop.appearance",
"color-scheme");
return result.has_value()
? std::make_optional(result->get_uint32() == 1)
: std::nullopt;
}
#endif // Qt < 6.5.0
bool AutostartSupported() {
return true;
}
void AutostartToggle(bool enabled, Fn<void(bool)> done) {
if (KSandbox::isFlatpak()) {
PortalAutostart(enabled, done);
return;
}
const auto success = [&] {
const auto autostart = QStandardPaths::writableLocation(
QStandardPaths::GenericConfigLocation)
+ u"/autostart/"_q;
if (!enabled) {
return QFile::remove(
autostart
+ QGuiApplication::desktopFileName()
+ u".desktop"_q);
}
return GenerateDesktopFile(
autostart,
{ u"-autostart"_q },
true,
!done);
}();
if (done) {
done(enabled && success);
}
}
bool AutostartSkip() {
return !cAutoStart();
}
bool TrayIconSupported() {
return QSystemTrayIcon::isSystemTrayAvailable();
}
bool SkipTaskbarSupported() {
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
if (IsX11()) {
return base::Platform::XCB::IsSupportedByWM(
base::Platform::XCB::Connection(),
"_NET_WM_STATE_SKIP_TASKBAR");
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
return false;
}
QString ExecutablePathForShortcuts() {
if (Core::UpdaterDisabled()) {
const auto &arguments = Core::Launcher::Instance().arguments();
if (!arguments.isEmpty()) {
const auto result = QFileInfo(arguments.first()).fileName();
if (!result.isEmpty()) {
return result;
}
}
return cExeName();
}
return cExeDir() + cExeName();
}
} // namespace Platform
QString psAppDataPath() {
// Previously we used ~/.TelegramDesktop, so look there first.
// If we find data there, we should still use it.
auto home = QDir::homePath();
if (!home.isEmpty()) {
auto oldPath = home + u"/.TelegramDesktop/"_q;
auto oldSettingsBase = oldPath + u"tdata/settings"_q;
if (QFile::exists(oldSettingsBase + '0')
|| QFile::exists(oldSettingsBase + '1')
|| QFile::exists(oldSettingsBase + 's')) {
return oldPath;
}
}
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + '/';
}
void psDoCleanup() {
try {
Platform::AutostartToggle(false);
psSendToMenu(false, true);
} catch (...) {
}
}
int psCleanup() {
psDoCleanup();
return 0;
}
void psDoFixPrevious() {
}
int psFixPrevious() {
psDoFixPrevious();
return 0;
}
namespace Platform {
void start() {
QGuiApplication::setDesktopFileName([&] {
if (KSandbox::isFlatpak()) {
return qEnvironmentVariable("FLATPAK_ID");
}
if (KSandbox::isSnap()) {
return qEnvironmentVariable("SNAP_INSTANCE_NAME")
+ '_'
+ cExeName();
}
if (!Core::UpdaterDisabled()) {
return u"org.telegram.desktop._%1"_q.arg(
Core::Launcher::Instance().instanceHash().constData());
}
return u"org.telegram.desktop"_q;
}());
LOG(("App ID: %1").arg(QGuiApplication::desktopFileName()));
if (!qEnvironmentVariableIsSet("XDG_ACTIVATION_TOKEN")
&& qEnvironmentVariableIsSet("DESKTOP_STARTUP_ID")) {
qputenv("XDG_ACTIVATION_TOKEN", qgetenv("DESKTOP_STARTUP_ID"));
}
qputenv("PULSE_PROP_application.name", AppName.utf8());
qputenv(
"PULSE_PROP_application.icon_name",
ApplicationIconName().toUtf8());
GLib::set_prgname(cExeName().toStdString());
GLib::set_application_name(AppName.data());
Webview::WebKitGTK::SetSocketPath(u"%1/%2-%3-webview-{}"_q.arg(
QDir::tempPath(),
HashForSocketPath(),
u"TD"_q).toStdString());
InstallLauncher();
}
void finish() {
}
PermissionStatus GetPermissionStatus(PermissionType type) {
return PermissionStatus::Granted;
}
void RequestPermission(PermissionType type, Fn<void(PermissionStatus)> resultCallback) {
resultCallback(PermissionStatus::Granted);
}
void OpenSystemSettingsForPermission(PermissionType type) {
}
bool OpenSystemSettings(SystemSettingsType type) {
if (type == SystemSettingsType::Audio) {
struct Command {
QString command;
QStringList arguments;
};
auto options = std::vector<Command>();
const auto add = [&](const char *option, const char *arg = nullptr) {
auto command = Command{ .command = option };
if (arg) {
command.arguments.push_back(arg);
}
options.push_back(std::move(command));
};
add("unity-control-center", "sound");
add("kcmshell6", "kcm_pulseaudio");
add("kcmshell5", "kcm_pulseaudio");
add("kcmshell4", "phonon");
add("gnome-control-center", "sound");
add("cinnamon-settings", "sound");
add("mate-volume-control");
add("pavucontrol-qt");
add("pavucontrol");
add("alsamixergui");
return ranges::any_of(options, [](const Command &command) {
return QProcess::startDetached(
command.command,
command.arguments);
});
}
return true;
}
void NewVersionLaunched(int oldVersion) {
if (oldVersion <= 5014003 && cAutoStart()) {
AutostartToggle(true);
}
}
QImage DefaultApplicationIcon() {
return Window::Logo();
}
QString ApplicationIconName() {
static const auto Result = KSandbox::isSnap()
? u"snap.%1."_q.arg(qEnvironmentVariable("SNAP_INSTANCE_NAME"))
: QGuiApplication::desktopFileName().remove(
u"._"_q + Core::Launcher::Instance().instanceHash());
return Result;
}
void LaunchMaps(const Data::LocationPoint &point, Fn<void()> fail) {
const auto url = QUrl(
u"geo:%1,%2"_q.arg(point.latAsString(), point.lonAsString()));
AppInfoCheckScheme(url.scheme().toStdString(), [=](
Gio::AppInfo appInfo,
Fn<void()> fail) {
// TODO: use launch_uris_async once we can use GLib 2.60
if (!appInfo.launch_uris(
{ url.toString().toStdString() },
base::Platform::AppLaunchContext(),
nullptr)) {
fail();
}
}, [=] {
PortalCheckScheme(url.scheme().toStdString(), [=](Fn<void()> fail) {
if (!QDesktopServices::openUrl(url)) {
fail();
}
}, fail);
});
}
namespace ThirdParty {
void start() {
}
} // namespace ThirdParty
} // namespace Platform
void psSendToMenu(bool send, bool silent) {
}
bool linuxMoveFile(const char *from, const char *to) {
FILE *ffrom = fopen(from, "rb"), *fto = fopen(to, "wb");
if (!ffrom) {
if (fto) fclose(fto);
return false;
}
if (!fto) {
fclose(ffrom);
return false;
}
static const int BufSize = 65536;
char buf[BufSize];
while (size_t size = fread(buf, 1, BufSize, ffrom)) {
fwrite(buf, 1, size, fto);
}
struct stat fst; // from http://stackoverflow.com/questions/5486774/keeping-fileowner-and-permissions-after-copying-file-in-c
//let's say this wont fail since you already worked OK on that fp
if (fstat(fileno(ffrom), &fst) != 0) {
fclose(ffrom);
fclose(fto);
return false;
}
//update to the same uid/gid
if (fchown(fileno(fto), fst.st_uid, fst.st_gid) != 0) {
fclose(ffrom);
fclose(fto);
return false;
}
//update the permissions
if (fchmod(fileno(fto), fst.st_mode) != 0) {
fclose(ffrom);
fclose(fto);
return false;
}
fclose(ffrom);
fclose(fto);
if (unlink(from)) {
return false;
}
return true;
}

View File

@@ -0,0 +1,58 @@
/*
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 "platform/platform_specific.h"
namespace Platform {
inline void IgnoreApplicationActivationRightNow() {
}
inline void WriteCrashDumpDetails() {
}
inline void AutostartRequestStateFromSystem(Fn<void(bool)> callback) {
}
inline bool PreventsQuit(Core::QuitReason reason) {
return false;
}
inline void ActivateThisProcess() {
}
inline uint64 ActivationWindowId(not_null<QWidget*> window) {
return 1;
}
inline void ActivateOtherProcess(uint64 processId, uint64 windowId) {
}
} // namespace Platform
inline void psCheckLocalSocket(const QString &serverName) {
QFile address(serverName);
if (address.exists()) {
address.remove();
}
}
QString psAppDataPath();
void psSendToMenu(bool send, bool silent = false);
int psCleanup();
int psFixPrevious();
inline QByteArray psDownloadPathBookmark(const QString &path) {
return QByteArray();
}
inline void psDownloadPathEnableAccess() {
}
bool linuxMoveFile(const char *from, const char *to);

View File

@@ -0,0 +1,24 @@
/*
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 "platform/platform_text_recognition.h"
namespace Platform {
namespace TextRecognition {
inline bool IsAvailable() {
return false;
}
inline Result RecognizeText(const QImage &image) {
return {};
}
} // namespace TextRecognition
} // namespace Platform

View File

@@ -0,0 +1,422 @@
/*
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/linux/tray_linux.h"
#include "base/invoke_queued.h"
#include "base/qt_signal_producer.h"
#include "base/platform/linux/base_linux_dbus_utilities.h"
#include "core/application.h"
#include "core/sandbox.h"
#include "platform/platform_specific.h"
#include "ui/ui_utility.h"
#include "ui/widgets/popup_menu.h"
#include "window/window_controller.h"
#include "styles/style_window.h"
#include <QtCore/QCoreApplication>
#include <QtWidgets/QMenu>
#include <QtWidgets/QSystemTrayIcon>
#include <gio/gio.hpp>
namespace Platform {
namespace {
using namespace gi::repository;
[[nodiscard]] QString PanelIconName(int counter, bool muted) {
return ApplicationIconName() + ((counter > 0)
? (muted
? u"-mute"_q
: u"-attention"_q)
: QString()) + u"-symbolic"_q;
}
} // namespace
class IconGraphic final {
public:
explicit IconGraphic();
~IconGraphic();
void updateState();
[[nodiscard]] bool isRefreshNeeded() const;
[[nodiscard]] QIcon trayIcon();
private:
struct State {
QIcon systemIcon;
QString iconThemeName;
bool monochrome = false;
int32 counter = 0;
bool muted = false;
};
[[nodiscard]] QIcon systemIcon() const;
[[nodiscard]] bool isCounterNeeded(const State &state) const;
[[nodiscard]] int counterSlice(int counter) const;
[[nodiscard]] QSize dprSize(const QImage &image) const;
const int _iconSizes[7];
base::flat_map<int, QImage> _imageBack;
QIcon _trayIcon;
State _current;
State _new;
};
IconGraphic::IconGraphic()
: _iconSizes{ 16, 22, 32, 48, 64, 128, 256 } {
updateState();
}
IconGraphic::~IconGraphic() = default;
QIcon IconGraphic::systemIcon() const {
if (_new.iconThemeName == _current.iconThemeName
&& _new.monochrome == _current.monochrome
&& (_new.counter > 0) == (_current.counter > 0)
&& _new.muted == _current.muted) {
return _current.systemIcon;
}
const auto candidates = {
_new.monochrome ? PanelIconName(_new.counter, _new.muted) : QString(),
ApplicationIconName(),
};
for (const auto &candidate : candidates) {
if (candidate.isEmpty()) {
continue;
}
const auto icon = QIcon::fromTheme(candidate, QIcon());
if (icon.name() == candidate) {
return icon;
}
}
return QIcon();
}
bool IconGraphic::isCounterNeeded(const State &state) const {
return state.systemIcon.name() != PanelIconName(
state.counter,
state.muted);
}
int IconGraphic::counterSlice(int counter) const {
return (counter >= 100)
? (100 + (counter % 10))
: counter;
}
QSize IconGraphic::dprSize(const QImage &image) const {
return image.size() / image.devicePixelRatio();
}
void IconGraphic::updateState() {
_new.iconThemeName = QIcon::themeName();
_new.monochrome = Core::App().settings().trayIconMonochrome();
_new.counter = Core::App().unreadBadge();
_new.muted = Core::App().unreadBadgeMuted();
_new.systemIcon = systemIcon();
}
bool IconGraphic::isRefreshNeeded() const {
return _trayIcon.isNull()
|| _new.iconThemeName != _current.iconThemeName
|| _new.systemIcon.name() != _current.systemIcon.name()
|| (isCounterNeeded(_new)
? _new.muted != _current.muted
|| counterSlice(_new.counter) != counterSlice(
_current.counter)
: false);
}
QIcon IconGraphic::trayIcon() {
if (!isRefreshNeeded()) {
return _trayIcon;
}
const auto guard = gsl::finally([&] {
_current = _new;
});
if (!isCounterNeeded(_new)) {
_trayIcon = _new.systemIcon;
return _trayIcon;
}
QIcon result;
for (const auto iconSize : _iconSizes) {
auto &currentImageBack = _imageBack[iconSize];
const auto desiredSize = QSize(iconSize, iconSize);
if (currentImageBack.isNull()
|| _new.iconThemeName != _current.iconThemeName
|| _new.systemIcon.name() != _current.systemIcon.name()) {
currentImageBack = {};
if (!_new.systemIcon.isNull()) {
// We can't use QIcon::actualSize here
// since it works incorrectly with svg icon themes
currentImageBack = _new.systemIcon
.pixmap(desiredSize)
.toImage();
const auto firstAttemptSize = dprSize(currentImageBack);
// if current icon theme is not a svg one, Qt can return
// a pixmap that less in size even if there are a bigger one
if (firstAttemptSize.width() < desiredSize.width()) {
const auto availableSizes
= _new.systemIcon.availableSizes();
const auto biggestSize = ranges::max_element(
availableSizes,
std::less<>(),
&QSize::width);
if (biggestSize->width() > firstAttemptSize.width()) {
currentImageBack = _new.systemIcon
.pixmap(*biggestSize)
.toImage();
}
}
}
if (currentImageBack.isNull()) {
currentImageBack = Window::Logo();
}
if (dprSize(currentImageBack) != desiredSize) {
currentImageBack = currentImageBack.scaled(
desiredSize * currentImageBack.devicePixelRatio(),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
}
result.addPixmap(Ui::PixmapFromImage(_new.counter > 0
? Window::WithSmallCounter(std::move(currentImageBack), {
.size = iconSize,
.count = _new.counter,
.bg = _new.muted ? st::trayCounterBgMute : st::trayCounterBg,
.fg = st::trayCounterFg,
}) : std::move(currentImageBack)));
}
_trayIcon = result;
return _trayIcon;
}
class TrayEventFilter final : public QObject {
public:
TrayEventFilter(not_null<QObject*> parent);
[[nodiscard]] rpl::producer<> contextMenuFilters() const;
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
private:
const QString _iconObjectName;
rpl::event_stream<> _contextMenuFilters;
};
TrayEventFilter::TrayEventFilter(not_null<QObject*> parent)
: QObject(parent)
, _iconObjectName("QSystemTrayIconSys") {
parent->installEventFilter(this);
}
bool TrayEventFilter::eventFilter(QObject *obj, QEvent *event) {
if (event->type() == QEvent::MouseButtonPress
&& obj->objectName() == _iconObjectName) {
const auto m = static_cast<QMouseEvent*>(event);
if (m->button() == Qt::RightButton) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
_contextMenuFilters.fire({});
});
return true;
}
}
return false;
}
rpl::producer<> TrayEventFilter::contextMenuFilters() const {
return _contextMenuFilters.events();
}
Tray::Tray() {
auto connection = Gio::bus_get_sync(Gio::BusType::SESSION_, nullptr);
if (connection) {
_sniWatcher = std::make_unique<base::Platform::DBus::ServiceWatcher>(
connection.gobj_(),
"org.kde.StatusNotifierWatcher",
[=](
const std::string &service,
const std::string &oldOwner,
const std::string &newOwner) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
if (hasIcon()) {
destroyIcon();
createIcon();
}
});
});
}
}
void Tray::createIcon() {
if (!_icon) {
LOG(("System tray available: %1").arg(Logs::b(TrayIconSupported())));
if (!_iconGraphic) {
_iconGraphic = std::make_unique<IconGraphic>();
}
const auto showCustom = [=] {
_aboutToShowRequests.fire({});
InvokeQueued(_menuCustom.get(), [=] {
_menuCustom->popup(QCursor::pos());
});
};
_icon = base::make_unique_q<QSystemTrayIcon>(nullptr);
_icon->setIcon(_iconGraphic->trayIcon());
_icon->setToolTip(AppName.utf16());
using Reason = QSystemTrayIcon::ActivationReason;
base::qt_signal_producer(
_icon.get(),
&QSystemTrayIcon::activated
) | rpl::on_next([=](Reason reason) {
if (reason == QSystemTrayIcon::Context) {
showCustom();
} else {
_iconClicks.fire({});
}
}, _lifetime);
_icon->setContextMenu(_menu.get());
if (!_eventFilter) {
_eventFilter = base::make_unique_q<TrayEventFilter>(
QCoreApplication::instance());
_eventFilter->contextMenuFilters(
) | rpl::on_next([=] {
showCustom();
}, _lifetime);
}
}
updateIcon();
_icon->show();
}
void Tray::destroyIcon() {
_icon = nullptr;
}
void Tray::updateIcon() {
if (!_icon || !_iconGraphic) {
return;
}
_iconGraphic->updateState();
if (_iconGraphic->isRefreshNeeded()) {
_icon->setIcon(_iconGraphic->trayIcon());
}
}
void Tray::createMenu() {
if (!_menu) {
_menu = base::make_unique_q<QMenu>(nullptr);
}
if (!_menuCustom) {
_menuCustom = base::make_unique_q<Ui::PopupMenu>(nullptr);
_menuCustom->deleteOnHide(false);
}
}
void Tray::destroyMenu() {
_menuCustom = nullptr;
if (_menu) {
_menu->clear();
}
_actionsLifetime.destroy();
}
void Tray::addAction(rpl::producer<QString> text, Fn<void()> &&callback) {
if (_menuCustom) {
const auto action = _menuCustom->addAction(QString(), callback);
rpl::duplicate(
text
) | rpl::on_next([=](const QString &text) {
action->setText(text);
}, _actionsLifetime);
}
if (_menu) {
const auto action = _menu->addAction(QString(), std::move(callback));
std::move(
text
) | rpl::on_next([=](const QString &text) {
action->setText(text);
}, _actionsLifetime);
}
}
void Tray::showTrayMessage() const {
}
bool Tray::hasTrayMessageSupport() const {
return false;
}
rpl::producer<> Tray::aboutToShowRequests() const {
return rpl::merge(
_aboutToShowRequests.events(),
_menu
? base::qt_signal_producer(_menu.get(), &QMenu::aboutToShow)
: rpl::never<>() | rpl::type_erased);
}
rpl::producer<> Tray::showFromTrayRequests() const {
return rpl::never<>();
}
rpl::producer<> Tray::hideToTrayRequests() const {
return rpl::never<>();
}
rpl::producer<> Tray::iconClicks() const {
return _iconClicks.events();
}
bool Tray::hasIcon() const {
return _icon;
}
rpl::lifetime &Tray::lifetime() {
return _lifetime;
}
Tray::~Tray() = default;
bool HasMonochromeSetting() {
return QIcon::hasThemeIcon(
PanelIconName(
Core::App().unreadBadge(),
Core::App().unreadBadgeMuted()));
}
} // namespace Platform

View File

@@ -0,0 +1,75 @@
/*
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 "platform/platform_tray.h"
#include "base/unique_qptr.h"
namespace base::Platform::DBus {
class ServiceWatcher;
} // namespace base::Platform::DBus
namespace Ui {
class PopupMenu;
} // namespace Ui
class QMenu;
class QSystemTrayIcon;
namespace Platform {
class IconGraphic;
class TrayEventFilter;
class Tray final {
public:
Tray();
~Tray();
[[nodiscard]] rpl::producer<> aboutToShowRequests() const;
[[nodiscard]] rpl::producer<> showFromTrayRequests() const;
[[nodiscard]] rpl::producer<> hideToTrayRequests() const;
[[nodiscard]] rpl::producer<> iconClicks() const;
[[nodiscard]] bool hasIcon() const;
void createIcon();
void destroyIcon();
void updateIcon();
void createMenu();
void destroyMenu();
void addAction(rpl::producer<QString> text, Fn<void()> &&callback);
void showTrayMessage() const;
[[nodiscard]] bool hasTrayMessageSupport() const;
[[nodiscard]] rpl::lifetime &lifetime();
private:
std::unique_ptr<base::Platform::DBus::ServiceWatcher> _sniWatcher;
std::unique_ptr<IconGraphic> _iconGraphic;
base::unique_qptr<QSystemTrayIcon> _icon;
base::unique_qptr<QMenu> _menu;
base::unique_qptr<Ui::PopupMenu> _menuCustom;
base::unique_qptr<TrayEventFilter> _eventFilter;
rpl::event_stream<> _iconClicks;
rpl::event_stream<> _aboutToShowRequests;
rpl::lifetime _actionsLifetime;
rpl::lifetime _lifetime;
};
} // namespace Platform

View File

@@ -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/platform_webauthn.h"
namespace Platform::WebAuthn {
bool IsSupported() {
return false;
}
void RegisterKey(
const Data::Passkey::RegisterData &data,
Fn<void(RegisterResult result)> callback) {
callback({});
}
void Login(
const Data::Passkey::LoginData &data,
Fn<void(LoginResult result)> callback) {
callback({});
}
} // namespace Platform::WebAuthn

View File

@@ -0,0 +1,10 @@
/*
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 "platform/platform_current_geo_location.h"

View File

@@ -0,0 +1,154 @@
/*
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/current_geo_location_mac.h"
#include "base/platform/mac/base_utilities_mac.h"
#include "core/current_geo_location.h"
#include <CoreLocation/CoreLocation.h>
@interface LocationDelegate : NSObject<CLLocationManagerDelegate>
- (id) initWithCallback:(Fn<void(Core::GeoLocation)>)callback;
- (void) locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations;
- (void) locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error;
- (void) locationManager:(CLLocationManager *) manager didChangeAuthorizationStatus:(CLAuthorizationStatus) status;
- (void) dealloc;
@end
@implementation LocationDelegate {
CLLocationManager *_manager;
Fn<void(Core::GeoLocation)> _callback;
}
- (void) fail {
[_manager stopUpdatingLocation];
const auto onstack = _callback;
[self release];
onstack({});
}
- (void) processWithStatus:(CLAuthorizationStatus)status {
switch (status) {
case kCLAuthorizationStatusNotDetermined:
if (@available(macOS 10.15, *)) {
[_manager requestWhenInUseAuthorization];
} else {
[_manager startUpdatingLocation];
}
break;
case kCLAuthorizationStatusAuthorizedAlways:
[_manager startUpdatingLocation];
return;
case kCLAuthorizationStatusRestricted:
case kCLAuthorizationStatusDenied:
default:
[self fail];
return;
}
}
- (id) initWithCallback:(Fn<void(Core::GeoLocation)>)callback {
if (self = [super init]) {
_callback = std::move(callback);
_manager = [[CLLocationManager alloc] init];
_manager.desiredAccuracy = kCLLocationAccuracyThreeKilometers;
_manager.delegate = self;
if ([CLLocationManager locationServicesEnabled]) {
if (@available(macOS 11, *)) {
[self processWithStatus:[_manager authorizationStatus]];
} else {
[self processWithStatus:[CLLocationManager authorizationStatus]];
}
} else {
[self fail];
}
}
return self;
}
- (void) locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation*>*)locations {
[_manager stopUpdatingLocation];
auto result = Core::GeoLocation();
if ([locations count] > 0) {
const auto coordinate = [locations lastObject].coordinate;
result.accuracy = Core::GeoLocationAccuracy::Exact;
result.point = QPointF(coordinate.latitude, coordinate.longitude);
}
const auto onstack = _callback;
[self release];
onstack(result);
}
- (void) locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error {
if (error.code != kCLErrorLocationUnknown) {
[self fail];
}
}
- (void) locationManager:(CLLocationManager *) manager didChangeAuthorizationStatus:(CLAuthorizationStatus) status {
[self processWithStatus:status];
}
- (void) dealloc {
if (_manager) {
_manager.delegate = nil;
[_manager release];
}
[super dealloc];
}
@end
namespace Platform {
void ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback) {
[[LocationDelegate alloc] initWithCallback:std::move(callback)];
}
void ResolveLocationAddress(
const Core::GeoLocation &location,
const QString &language,
Fn<void(Core::GeoAddress)> callback) {
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
CLLocation *request = [[CLLocation alloc]
initWithLatitude:location.point.x()
longitude:location.point.y()];
[geocoder reverseGeocodeLocation:request completionHandler:^(
NSArray<CLPlacemark*> * __nullable placemarks,
NSError * __nullable error) {
if (placemarks && [placemarks count] > 0) {
CLPlacemark *placemark = [placemarks firstObject];
auto list = QStringList();
const auto push = [&](NSString *text) {
if (text) {
const auto qt = NS2QString(text);
if (!qt.isEmpty()) {
list.push_back(qt);
}
}
};
push([placemark thoroughfare]);
push([placemark locality]);
push([placemark country]);
callback({ .name = list.join(u", "_q) });
} else {
callback({});
}
[geocoder release];
}];
[request release];
}
} // namespace Platform

View File

@@ -0,0 +1,33 @@
/*
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 Platform {
class FileBookmark final {
public:
FileBookmark(const QByteArray &bookmark);
~FileBookmark();
[[nodiscard]] bool check() const;
bool enable() const;
void disable() const;
[[nodiscard]] const QString &name(const QString &original) const;
[[nodiscard]] QByteArray bookmark() const;
private:
#ifdef OS_MAC_STORE
struct Data;
Data *data = nullptr;
#endif // OS_MAC_STORE
};
[[nodiscard]] QByteArray PathBookmark(const QString &path);
} // namespace Platform

View File

@@ -0,0 +1,133 @@
/*
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/file_bookmark_mac.h"
#include "base/platform/mac/base_utilities_mac.h"
#include "logs.h"
#include <QtCore/QMutex>
#include <Cocoa/Cocoa.h>
#include <CoreFoundation/CFURL.h>
namespace Platform {
namespace {
#ifdef OS_MAC_STORE
QMutex BookmarksMutex;
#endif // OS_MAC_STORE
} // namespace
#ifdef OS_MAC_STORE
struct FileBookmark::Data {
~Data() {
if (url) [url release];
}
NSURL *url = nil;
QString name;
QByteArray bookmark;
int counter = 0;
};
#endif // OS_MAC_STORE
FileBookmark::FileBookmark(const QByteArray &bookmark) {
#ifdef OS_MAC_STORE
if (bookmark.isEmpty()) return;
BOOL isStale = NO;
NSError *error = nil;
NSURL *url = [NSURL URLByResolvingBookmarkData:bookmark.toNSData() options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&error];
if (!url) return;
if ([url startAccessingSecurityScopedResource]) {
data = new Data();
data->url = [url retain];
data->name = NS2QString([url path]);
data->bookmark = bookmark;
[url stopAccessingSecurityScopedResource];
}
#endif // OS_MAC_STORE
}
bool FileBookmark::check() const {
if (enable()) {
disable();
return true;
}
return false;
}
bool FileBookmark::enable() const {
#ifndef OS_MAC_STORE
return true;
#else // OS_MAC_STORE
if (!data) return false;
QMutexLocker lock(&BookmarksMutex);
if (data->counter > 0 || [data->url startAccessingSecurityScopedResource] == YES) {
++data->counter;
return true;
}
return false;
#endif // OS_MAC_STORE
}
void FileBookmark::disable() const {
#ifdef OS_MAC_STORE
if (!data) return;
QMutexLocker lock(&BookmarksMutex);
if (data->counter > 0) {
--data->counter;
if (!data->counter) {
[data->url stopAccessingSecurityScopedResource];
}
}
#endif // OS_MAC_STORE
}
const QString &FileBookmark::name(const QString &original) const {
#ifndef OS_MAC_STORE
return original;
#else // OS_MAC_STORE
return (data && !data->name.isEmpty()) ? data->name : original;
#endif // OS_MAC_STORE
}
QByteArray FileBookmark::bookmark() const {
#ifndef OS_MAC_STORE
return QByteArray();
#else // OS_MAC_STORE
return data ? data->bookmark : QByteArray();
#endif // OS_MAC_STORE
}
FileBookmark::~FileBookmark() {
#ifdef OS_MAC_STORE
if (data && data->counter > 0) {
LOG(("Did not disable() bookmark, counter: %1").arg(data->counter));
[data->url stopAccessingSecurityScopedResource];
}
#endif // OS_MAC_STORE
}
QByteArray PathBookmark(const QString &path) {
#ifndef OS_MAC_STORE
return QByteArray();
#else // OS_MAC_STORE
NSURL *url = [NSURL fileURLWithPath:[NSString stringWithUTF8String:path.toUtf8().constData()]];
if (!url) return QByteArray();
NSError *error = nil;
NSData *data = [url bookmarkDataWithOptions:(NSURLBookmarkCreationWithSecurityScope | NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess) includingResourceValuesForKeys:nil relativeToURL:nil error:&error];
return data ? QByteArray::fromNSData(data) : QByteArray();
#endif // OS_MAC_STORE
}
} // namespace Platform

View File

@@ -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
*/
#pragma once
#include "platform/platform_file_utilities.h"
namespace Platform {
namespace File {
inline void UnsafeOpenUrl(const QString &url) {
return ::File::internal::UnsafeOpenUrlDefault(url);
}
inline void UnsafeOpenEmailLink(const QString &email) {
return ::File::internal::UnsafeOpenEmailLinkDefault(email);
}
inline void PostprocessDownloaded(const QString &filepath) {
}
} // namespace File
namespace FileDialog {
inline void InitLastPath() {
::FileDialog::internal::InitLastPathDefault();
}
inline bool Get(
QPointer<QWidget> parent,
QStringList &files,
QByteArray &remoteContent,
const QString &caption,
const QString &filter,
::FileDialog::internal::Type type,
QString startFile) {
return ::FileDialog::internal::GetDefault(
parent,
files,
remoteContent,
caption,
filter,
type,
startFile);
}
} // namespace FileDialog
} // namespace Platform

View File

@@ -0,0 +1,569 @@
/*
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/file_utilities_mac.h"
#include "base/platform/mac/base_utilities_mac.h"
#include "lang/lang_keys.h"
#include "styles/style_window.h"
#include <Cocoa/Cocoa.h>
#include <CoreFoundation/CFURL.h>
namespace {
using namespace Platform;
QString strNeedToReload() {
const uint32 letters[] = { 0xAD92C02B, 0xA2217C97, 0x5E55F4F5, 0x2207DAAC, 0xD18BA536, 0x03E41869, 0xB96D2BFD, 0x810C7284, 0xE412099E, 0x5AAD0837, 0xE6637AEE, 0x8E5E2FF5, 0xE3BDA123, 0x94A5CE38, 0x4A42F7D1, 0xCE4677DC, 0x40A81701, 0x9C5B38CD, 0x61801E1A, 0x6FF16179 };
return MakeFromLetters(letters);
}
QString strNeedToRefresh1() {
const uint32 letters[] = { 0xEDDFCD66, 0x434DF1FB, 0x820B76AB, 0x48CE7965, 0x3609C0BA, 0xFC9A990C, 0x3EDD1C51, 0xE2BDA036, 0x7140CEE9, 0x65DB414D, 0x88592EC3, 0x2CB2613A };
return MakeFromLetters(letters);
}
QString strNeedToRefresh2() {
const uint32 letters[] = { 0x8AE4915D, 0x7159D7EF, 0x79C74167, 0x29B7611C, 0x0E6B9ADD, 0x0D93610F, 0xEBEAFE7A, 0x5BD17540, 0x121EF3B7, 0x61B02E26, 0x2174AAEE, 0x61AD3325 };
return MakeFromLetters(letters);
}
} // namespace
@interface OpenWithApp : NSObject {
NSString *fullname;
NSURL *app;
NSImage *icon;
}
@property (nonatomic, retain) NSString *fullname;
@property (nonatomic, retain) NSURL *app;
@property (nonatomic, retain) NSImage *icon;
@end // @interface OpenWithApp
@implementation OpenWithApp
@synthesize fullname, app, icon;
- (void) dealloc {
[fullname release];
[app release];
[icon release];
[super dealloc];
}
@end // @implementation OpenWithApp
@interface OpenFileWithInterface : NSObject {
}
- (id) init:(NSString *)file;
- (BOOL) popupAt:(NSPoint)point;
- (void) itemChosen:(id)sender;
- (void) dealloc;
@end // @interface OpenFileWithInterface
@implementation OpenFileWithInterface {
NSString *toOpen;
NSURL *defUrl;
NSString *defBundle, *defName, *defVersion;
NSImage *defIcon;
NSMutableArray *apps;
NSMenu *menu;
}
- (void) fillAppByUrl:(NSURL*)url bundle:(NSString**)bundle name:(NSString**)name version:(NSString**)version icon:(NSImage**)icon {
NSBundle *b = [NSBundle bundleWithURL:url];
if (b) {
NSString *path = [url path];
*name = [[NSFileManager defaultManager] displayNameAtPath: path];
if (!*name) *name = (NSString*)[b objectForInfoDictionaryKey:@"CFBundleDisplayName"];
if (!*name) *name = (NSString*)[b objectForInfoDictionaryKey:@"CFBundleName"];
if (*name) {
*bundle = [b bundleIdentifier];
if (bundle) {
*version = (NSString*)[b objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
*icon = [[NSWorkspace sharedWorkspace] iconForFile: path];
if (*icon && [*icon isValid]) [*icon setSize: CGSizeMake(16., 16.)];
return;
}
}
}
*bundle = *name = *version = nil;
*icon = nil;
}
- (id) init:(NSString*)file {
toOpen = [file retain];
if (self = [super init]) {
NSURL *url = [NSURL fileURLWithPath:file];
defUrl = [[NSWorkspace sharedWorkspace] URLForApplicationToOpenURL:url];
if (defUrl) {
[self fillAppByUrl:defUrl bundle:&defBundle name:&defName version:&defVersion icon:&defIcon];
if (!defBundle || !defName) {
defUrl = nil;
}
}
NSArray *appsList = (NSArray*)LSCopyApplicationURLsForURL(CFURLRef(url), kLSRolesAll);
NSMutableDictionary *data = [NSMutableDictionary dictionaryWithCapacity:16];
int fullcount = 0;
for (id app in appsList) {
if (fullcount > 15) break;
NSString *bundle = nil, *name = nil, *version = nil;
NSImage *icon = nil;
[self fillAppByUrl:(NSURL*)app bundle:&bundle name:&name version:&version icon:&icon];
if (bundle && name) {
if ([bundle isEqualToString:defBundle] && [version isEqualToString:defVersion]) continue;
NSString *key = [[NSArray arrayWithObjects:bundle, name, nil] componentsJoinedByString:@"|"];
if (!version) version = @"";
NSMutableDictionary *versions = (NSMutableDictionary*)[data objectForKey:key];
if (!versions) {
versions = [NSMutableDictionary dictionaryWithCapacity:2];
[data setValue:versions forKey:key];
}
if (![versions objectForKey:version]) {
[versions setValue:[NSArray arrayWithObjects:name, icon, app, nil] forKey:version];
++fullcount;
}
}
}
if (fullcount || defUrl) {
apps = [NSMutableArray arrayWithCapacity:fullcount];
for (id key in data) {
NSMutableDictionary *val = (NSMutableDictionary*)[data objectForKey:key];
for (id ver in val) {
NSArray *app = (NSArray*)[val objectForKey:ver];
OpenWithApp *a = [[OpenWithApp alloc] init];
NSString *fullname = (NSString*)[app objectAtIndex:0], *version = (NSString*)ver;
BOOL showVersion = ([val count] > 1);
if (!showVersion) {
NSError *error = NULL;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^\\d+\\.\\d+\\.\\d+(\\.\\d+)?$" options:NSRegularExpressionCaseInsensitive error:&error];
showVersion = ![regex numberOfMatchesInString:version options:NSMatchingWithoutAnchoringBounds range:{0,[version length]}];
}
if (showVersion) fullname = [[NSArray arrayWithObjects:fullname, @" (", version, @")", nil] componentsJoinedByString:@""];
[a setFullname:fullname];
[a setIcon:(NSImage*)[app objectAtIndex:1]];
[a setApp:(NSURL*)[app objectAtIndex:2]];
[apps addObject:a];
[a release];
}
}
}
[apps sortUsingDescriptors:[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"fullname" ascending:YES]]];
[appsList release];
menu = nil;
}
return self;
}
- (BOOL) popupAt:(NSPoint)point {
if (![apps count] && !defName) return NO;
menu = [[NSMenu alloc] initWithTitle:@"Open With"];
int index = 0;
if (defName) {
NSMenuItem *item = [menu insertItemWithTitle:[[NSArray arrayWithObjects:defName, @" (default)", nil] componentsJoinedByString:@""] action:@selector(itemChosen:) keyEquivalent:@"" atIndex:index++];
if (defIcon) [item setImage:defIcon];
[item setTarget:self];
[menu insertItem:[NSMenuItem separatorItem] atIndex:index++];
}
if ([apps count]) {
for (id a in apps) {
OpenWithApp *app = (OpenWithApp*)a;
NSMenuItem *item = [menu insertItemWithTitle:[a fullname] action:@selector(itemChosen:) keyEquivalent:@"" atIndex:index++];
if ([app icon]) [item setImage:[app icon]];
[item setTarget:self];
}
[menu insertItem:[NSMenuItem separatorItem] atIndex:index++];
}
NSMenuItem *item = [menu insertItemWithTitle:Q2NSString(tr::lng_mac_choose_program_menu(tr::now)) action:@selector(itemChosen:) keyEquivalent:@"" atIndex:index++];
[item setTarget:self];
[menu popUpMenuPositioningItem:nil atLocation:point inView:nil];
return YES;
}
- (void) itemChosen:(id)sender {
NSArray *items = [menu itemArray];
NSURL *url = nil;
for (int i = 0, l = [items count]; i < l; ++i) {
if ([items objectAtIndex:i] == sender) {
if (defName) i -= 2;
if (i < 0) {
url = defUrl;
} else if (i < int([apps count])) {
url = [(OpenWithApp*)[apps objectAtIndex:i] app];
}
break;
}
}
if (url) {
[[NSWorkspace sharedWorkspace] openFile:toOpen withApplication:[url path]];
} else if (!Platform::File::UnsafeShowOpenWith(NS2QString(toOpen))) {
Platform::File::UnsafeLaunch(NS2QString(toOpen));
}
}
- (void) dealloc {
[toOpen release];
if (menu) [menu release];
[super dealloc];
}
@end // @implementation OpenFileWithInterface
@interface NSURL(CompareUrls)
- (BOOL) isEquivalent:(NSURL *)aURL;
@end // @interface NSURL(CompareUrls)
@implementation NSURL(CompareUrls)
- (BOOL) isEquivalent:(NSURL *)aURL {
if ([self isEqual:aURL]) return YES;
if ([[self scheme] caseInsensitiveCompare:[aURL scheme]] != NSOrderedSame) return NO;
if ([[self host] caseInsensitiveCompare:[aURL host]] != NSOrderedSame) return NO;
if ([[self path] compare:[aURL path]] != NSOrderedSame) return NO;
if ([[self port] compare:[aURL port]] != NSOrderedSame) return NO;
if ([[self query] compare:[aURL query]] != NSOrderedSame) return NO;
return YES;
}
@end // @implementation NSURL(CompareUrls)
@interface ChooseApplicationDelegate : NSObject<NSOpenSavePanelDelegate> {
}
- (id) init:(NSArray *)recommendedApps withPanel:(NSOpenPanel *)creator withSelector:(NSPopUpButton *)menu withGood:(NSTextField *)goodLabel withBad:(NSTextField *)badLabel withIcon:(NSImageView *)badIcon withAccessory:(NSView *)acc;
- (BOOL) panel:(id)sender shouldEnableURL:(NSURL *)url;
- (void) panelSelectionDidChange:(id)sender;
- (void) menuDidClose;
- (void) dealloc;
@end // @interface ChooseApplicationDelegate
@implementation ChooseApplicationDelegate {
BOOL onlyRecommended;
NSArray *apps;
NSOpenPanel *panel;
NSPopUpButton *selector;
NSTextField *good, *bad;
NSImageView *icon;
NSString *recom;
NSView *accessory;
}
- (id) init:(NSArray *)recommendedApps withPanel:(NSOpenPanel *)creator withSelector:(NSPopUpButton *)menu withGood:(NSTextField *)goodLabel withBad:(NSTextField *)badLabel withIcon:(NSImageView *)badIcon withAccessory:(NSView *)acc {
if (self = [super init]) {
onlyRecommended = YES;
recom = [Q2NSString(tr::lng_mac_recommended_apps(tr::now)) copy];
apps = recommendedApps;
panel = creator;
selector = menu;
good = goodLabel;
bad = badLabel;
icon = badIcon;
accessory = acc;
[selector setAction:@selector(menuDidClose)];
}
return self;
}
- (BOOL) isRecommended:(NSURL *)url {
if (apps) {
for (id app in apps) {
if ([(NSURL*)app isEquivalent:url]) {
return YES;
}
}
}
return NO;
}
- (BOOL) panel:(id)sender shouldEnableURL:(NSURL *)url {
NSNumber *isDirectory;
if ([url getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil] && isDirectory != nil && [isDirectory boolValue]) {
if (onlyRecommended) {
NSNumber *isPackage;
if ([url getResourceValue:&isPackage forKey:NSURLIsPackageKey error:nil] && isPackage != nil && [isPackage boolValue]) {
return [self isRecommended:url];
}
}
return YES;
}
return NO;
}
- (void) panelSelectionDidChange:(id)sender {
NSArray *urls = [panel URLs];
if ([urls count]) {
if ([self isRecommended:[urls firstObject]]) {
[bad removeFromSuperview];
[icon removeFromSuperview];
[accessory addSubview:good];
} else {
[good removeFromSuperview];
[accessory addSubview:bad];
[accessory addSubview:icon];
}
} else {
[good removeFromSuperview];
[bad removeFromSuperview];
[icon removeFromSuperview];
}
}
- (void) menuDidClose {
onlyRecommended = [[[selector selectedItem] title] isEqualToString:recom];
[self refreshPanelTable];
}
- (BOOL) refreshDataInViews: (NSArray*)subviews {
for (id view in subviews) {
NSString *cls = [view className];
if ([cls isEqualToString:Q2NSString(strNeedToReload())]) {
[view reloadData];
} else if ([cls isEqualToString:Q2NSString(strNeedToRefresh1())] || [cls isEqualToString:Q2NSString(strNeedToRefresh2())]) {
[view reloadData];
return YES;
} else {
NSArray *next = [view subviews];
if ([next count] && [self refreshDataInViews:next]) {
return YES;
}
}
}
return NO;
}
- (void) refreshPanelTable {
@autoreleasepool {
[self refreshDataInViews:[[panel contentView] subviews]];
[panel validateVisibleColumns];
}
}
- (void) dealloc {
if (apps) {
[apps release];
[recom release];
}
[super dealloc];
}
@end // @implementation ChooseApplicationDelegate
namespace Platform {
namespace File {
QString UrlToLocal(const QUrl &url) {
auto result = url.toLocalFile();
if (result.startsWith(u"/.file/id="_q)) {
NSString *nsurl = [[[NSURL URLWithString: [NSString stringWithUTF8String: (u"file://"_q + result).toUtf8().constData()]] filePathURL] path];
if (!nsurl) return QString();
return NS2QString(nsurl);
}
return result;
}
bool UnsafeShowOpenWithDropdown(const QString &filepath) {
@autoreleasepool {
NSString *file = Q2NSString(filepath);
@try {
OpenFileWithInterface *menu = [[[OpenFileWithInterface alloc] init:file] autorelease];
return !![menu popupAt:[NSEvent mouseLocation]];
}
@catch (NSException *exception) {
}
@finally {
}
}
return false;
}
bool UnsafeShowOpenWith(const QString &filepath) {
@autoreleasepool {
NSString *file = Q2NSString(filepath);
@try {
NSURL *url = [NSURL fileURLWithPath:file];
NSString *ext = [url pathExtension];
NSArray *names = [url pathComponents];
NSString *name = [names count] ? [names lastObject] : @"";
NSArray *apps = (NSArray*)LSCopyApplicationURLsForURL(CFURLRef(url), kLSRolesAll);
NSOpenPanel *openPanel = [NSOpenPanel openPanel];
NSRect fullRect = { { 0., 0. }, { st::macAccessoryWidth, st::macAccessoryHeight } };
NSView *accessory = [[NSView alloc] initWithFrame:fullRect];
[accessory setAutoresizesSubviews:YES];
NSPopUpButton *selector = [[NSPopUpButton alloc] init];
[accessory addSubview:selector];
[selector addItemWithTitle:Q2NSString(tr::lng_mac_recommended_apps(tr::now))];
[selector addItemWithTitle:Q2NSString(tr::lng_mac_all_apps(tr::now))];
[selector sizeToFit];
NSTextField *enableLabel = [[NSTextField alloc] init];
[accessory addSubview:enableLabel];
[enableLabel setStringValue:Q2NSString(tr::lng_mac_enable_filter(tr::now))];
[enableLabel setFont:[selector font]];
[enableLabel setBezeled:NO];
[enableLabel setDrawsBackground:NO];
[enableLabel setEditable:NO];
[enableLabel setSelectable:NO];
[enableLabel sizeToFit];
NSRect selectorFrame = [selector frame], enableFrame = [enableLabel frame];
enableFrame.size.width += st::macEnableFilterAdd;
enableFrame.origin.x = (fullRect.size.width - selectorFrame.size.width - enableFrame.size.width) / 2.;
selectorFrame.origin.x = (fullRect.size.width - selectorFrame.size.width + enableFrame.size.width) / 2.;
enableFrame.origin.y = fullRect.size.height - selectorFrame.size.height - st::macEnableFilterTop + (selectorFrame.size.height - enableFrame.size.height) / 2.;
selectorFrame.origin.y = fullRect.size.height - selectorFrame.size.height - st::macSelectorTop;
[enableLabel setFrame:enableFrame];
[enableLabel setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
[selector setFrame:selectorFrame];
[selector setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
NSButton *button = [[NSButton alloc] init];
[accessory addSubview:button];
[button setButtonType:NSSwitchButton];
[button setFont:[selector font]];
[button setTitle:Q2NSString(tr::lng_mac_always_open_with(tr::now))];
[button sizeToFit];
NSRect alwaysRect = [button frame];
alwaysRect.origin.x = (fullRect.size.width - alwaysRect.size.width) / 2;
alwaysRect.origin.y = selectorFrame.origin.y - alwaysRect.size.height - st::macAlwaysThisAppTop;
[button setFrame:alwaysRect];
[button setAutoresizingMask:NSViewMinXMargin|NSViewMaxXMargin];
#ifdef OS_MAC_STORE
[button setHidden:YES];
#endif // OS_MAC_STORE
NSTextField *goodLabel = [[NSTextField alloc] init];
[goodLabel setStringValue:Q2NSString(tr::lng_mac_this_app_can_open(tr::now, lt_file, NS2QString(name)))];
[goodLabel setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
[goodLabel setBezeled:NO];
[goodLabel setDrawsBackground:NO];
[goodLabel setEditable:NO];
[goodLabel setSelectable:NO];
[goodLabel sizeToFit];
NSRect goodFrame = [goodLabel frame];
goodFrame.origin.x = (fullRect.size.width - goodFrame.size.width) / 2.;
goodFrame.origin.y = alwaysRect.origin.y - goodFrame.size.height - st::macAppHintTop;
[goodLabel setFrame:goodFrame];
NSTextField *badLabel = [[NSTextField alloc] init];
[badLabel setStringValue:Q2NSString(tr::lng_mac_not_known_app(tr::now, lt_file, NS2QString(name)))];
[badLabel setFont:[goodLabel font]];
[badLabel setBezeled:NO];
[badLabel setDrawsBackground:NO];
[badLabel setEditable:NO];
[badLabel setSelectable:NO];
[badLabel sizeToFit];
NSImageView *badIcon = [[NSImageView alloc] init];
NSImage *badImage = [NSImage imageNamed:NSImageNameCaution];
[badIcon setImage:badImage];
[badIcon setFrame:NSMakeRect(0, 0, st::macCautionIconSize, st::macCautionIconSize)];
NSRect badFrame = [badLabel frame], badIconFrame = [badIcon frame];
badFrame.origin.x = (fullRect.size.width - badFrame.size.width + badIconFrame.size.width) / 2.;
badIconFrame.origin.x = (fullRect.size.width - badFrame.size.width - badIconFrame.size.width) / 2.;
badFrame.origin.y = alwaysRect.origin.y - badFrame.size.height - st::macAppHintTop;
badIconFrame.origin.y = badFrame.origin.y;
[badLabel setFrame:badFrame];
[badIcon setFrame:badIconFrame];
[openPanel setAccessoryView:accessory];
ChooseApplicationDelegate *delegate = [[ChooseApplicationDelegate alloc] init:apps withPanel:openPanel withSelector:selector withGood:goodLabel withBad:badLabel withIcon:badIcon withAccessory:accessory];
[openPanel setDelegate:delegate];
[openPanel setCanChooseDirectories:NO];
[openPanel setCanChooseFiles:YES];
[openPanel setAllowsMultipleSelection:NO];
[openPanel setResolvesAliases:YES];
[openPanel setTitle:Q2NSString(tr::lng_mac_choose_app(tr::now))];
[openPanel setMessage:Q2NSString(tr::lng_mac_choose_text(tr::now, lt_file, NS2QString(name)))];
NSArray *appsPaths = [[NSFileManager defaultManager] URLsForDirectory:NSApplicationDirectory inDomains:NSLocalDomainMask];
if ([appsPaths count]) [openPanel setDirectoryURL:[appsPaths firstObject]];
[openPanel beginWithCompletionHandler:^(NSInteger result){
if (result == NSModalResponseOK) {
if ([[openPanel URLs] count] > 0) {
NSURL *app = [[openPanel URLs] objectAtIndex:0];
NSString *path = [app path];
if ([button state] == NSOnState) {
NSArray *UTIs = (NSArray *)UTTypeCreateAllIdentifiersForTag(kUTTagClassFilenameExtension,
(CFStringRef)ext,
nil);
for (NSString *UTI in UTIs) {
OSStatus result = LSSetDefaultRoleHandlerForContentType((CFStringRef)UTI,
kLSRolesAll,
(CFStringRef)[[NSBundle bundleWithPath:path] bundleIdentifier]);
DEBUG_LOG(("App Info: set default handler for '%1' UTI result: %2").arg(NS2QString(UTI)).arg(result));
}
[UTIs release];
}
[[NSWorkspace sharedWorkspace] openFile:file withApplication:[app path]];
}
}
[selector release];
[button release];
[enableLabel release];
[goodLabel release];
[badLabel release];
[badIcon release];
[accessory release];
[delegate release];
}];
}
@catch (NSException *exception) {
[[NSWorkspace sharedWorkspace] openFile:file];
}
@finally {
}
}
return YES;
}
void UnsafeLaunch(const QString &filepath) {
@autoreleasepool {
NSString *file = Q2NSString(filepath);
if ([[NSWorkspace sharedWorkspace] openFile:file] == NO) {
UnsafeShowOpenWith(filepath);
}
}
}
} // namespace File
} // namespace Platform

View File

@@ -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
namespace Platform {
class Integration;
[[nodiscard]] std::unique_ptr<Integration> CreateIntegration();
} // namespace Platform

View File

@@ -0,0 +1,24 @@
/*
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/integration_mac.h"
#include "platform/platform_integration.h"
namespace Platform {
namespace {
class MacIntegration final : public Integration {
};
} // namespace
std::unique_ptr<Integration> CreateIntegration() {
return std::make_unique<MacIntegration>();
}
} // namespace Platform

View File

@@ -0,0 +1,25 @@
/*
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 "core/launcher.h"
namespace Platform {
class Launcher : public Core::Launcher {
public:
Launcher(int argc, char *argv[]);
private:
void initHook() override;
bool launchUpdater(UpdaterLaunch action) override;
};
} // namespace Platform

View File

@@ -0,0 +1,90 @@
/*
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/launcher_mac.h"
#include "core/crash_reports.h"
#include "core/update_checker.h"
#include "base/base_file_utilities.h"
#include "base/platform/base_platform_file_utilities.h"
#include "base/platform/mac/base_utilities_mac.h"
#include <Cocoa/Cocoa.h>
#include <CoreFoundation/CFURL.h>
#include <sys/sysctl.h>
namespace Platform {
Launcher::Launcher(int argc, char *argv[])
: Core::Launcher(argc, argv) {
}
void Launcher::initHook() {
base::RegisterBundledResources(u"Telegram.rcc"_q);
}
bool Launcher::launchUpdater(UpdaterLaunch action) {
if (cExeName().isEmpty()) {
return false;
}
@autoreleasepool {
#ifdef OS_MAC_STORE
// In AppStore version we don't have Updater.
// We just relaunch our app.
if (action == UpdaterLaunch::JustRelaunch) {
NSDictionary *conf = [NSDictionary dictionaryWithObject:[NSArray array] forKey:NSWorkspaceLaunchConfigurationArguments];
[[NSWorkspace sharedWorkspace] launchApplicationAtURL:[NSURL fileURLWithPath:Q2NSString(cExeDir() + cExeName())] options:NSWorkspaceLaunchAsync | NSWorkspaceLaunchNewInstance configuration:conf error:0];
return true;
}
#endif // OS_MAC_STORE
NSString *path = @"", *args = @"";
@try {
path = [[NSBundle mainBundle] bundlePath];
if (!path) {
LOG(("Could not get bundle path!!"));
return false;
}
path = [path stringByAppendingString:@"/Contents/Frameworks/Updater"];
base::Platform::RemoveQuarantine(QFile::decodeName([path fileSystemRepresentation]));
NSMutableArray *args = [[NSMutableArray alloc] initWithObjects:@"-workpath", Q2NSString(cWorkingDir()), @"-procid", nil];
[args addObject:[NSString stringWithFormat:@"%d", [[NSProcessInfo processInfo] processIdentifier]]];
if (cRestartingToSettings()) [args addObject:@"-tosettings"];
if (action == UpdaterLaunch::JustRelaunch) [args addObject:@"-noupdate"];
if (cLaunchMode() == LaunchModeAutoStart) [args addObject:@"-autostart"];
if (Logs::DebugEnabled()) [args addObject:@"-debug"];
if (cStartInTray()) [args addObject:@"-startintray"];
if (cDataFile() != u"data"_q) {
[args addObject:@"-key"];
[args addObject:Q2NSString(cDataFile())];
}
if (customWorkingDir()) {
[args addObject:@"-workdir_custom"];
}
DEBUG_LOG(("Application Info: executing %1 %2").arg(NS2QString(path)).arg(NS2QString([args componentsJoinedByString:@" "])));
Logs::closeMain();
CrashReports::Finish();
if (![NSTask launchedTaskWithLaunchPath:path arguments:args]) {
DEBUG_LOG(("Task not launched while executing %1 %2").arg(NS2QString(path)).arg(NS2QString([args componentsJoinedByString:@" "])));
return false;
}
}
@catch (NSException *exception) {
LOG(("Exception caught while executing %1 %2").arg(NS2QString(path)).arg(NS2QString(args)));
return false;
}
@finally {
}
}
return true;
}
} // namespace Platform

View File

@@ -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
*/
#include <iconv.h>
#ifdef iconv_open
#undef iconv_open
#endif // iconv_open
#ifdef iconv
#undef iconv
#endif // iconv
#ifdef iconv_close
#undef iconv_close
#endif // iconv_close
iconv_t iconv_open(const char* tocode, const char* fromcode) {
return libiconv_open(tocode, fromcode);
}
size_t iconv(iconv_t cd, char** inbuf, size_t *inbytesleft, char** outbuf, size_t *outbytesleft) {
return libiconv(cd, inbuf, inbytesleft, outbuf, outbytesleft);
}
int iconv_close(iconv_t cd) {
return libiconv_close(cd);
}

View File

@@ -0,0 +1,101 @@
/*
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 "platform/platform_main_window.h"
#include "platform/mac/specific_mac_p.h"
#include "base/timer.h"
#include <QtWidgets/QMenuBar>
#include <QtCore/QTimer>
namespace Platform {
class MainWindow : public Window::MainWindow {
public:
explicit MainWindow(not_null<Window::Controller*> controller);
int getCustomTitleHeight() const {
return _customTitleHeight;
}
~MainWindow();
void updateWindowIcon() override;
rpl::producer<QPoint> globalForceClicks() override {
return _forceClicks.events();
}
class Private;
protected:
bool eventFilter(QObject *obj, QEvent *evt) override;
void stateChangedHook(Qt::WindowState state) override;
void initHook() override;
void unreadCounterChangedHook() override;
void updateGlobalMenuHook() override;
void closeWithoutDestroy() override;
void createGlobalMenu() override;
private:
friend class Private;
bool nativeEvent(
const QByteArray &eventType,
void *message,
qintptr *result) override;
void hideAndDeactivate();
void updateDockCounter();
std::unique_ptr<Private> _private;
mutable bool psIdle;
mutable QTimer psIdleTimer;
base::Timer _hideAfterFullScreenTimer;
QMenuBar psMainMenu;
QAction *psLogout = nullptr;
QAction *psUndo = nullptr;
QAction *psRedo = nullptr;
QAction *psCut = nullptr;
QAction *psCopy = nullptr;
QAction *psPaste = nullptr;
QAction *psDelete = nullptr;
QAction *psSelectAll = nullptr;
QAction *psContacts = nullptr;
QAction *psAddContact = nullptr;
QAction *psNewGroup = nullptr;
QAction *psNewChannel = nullptr;
QAction *psShowTelegram = nullptr;
QAction *psBold = nullptr;
QAction *psItalic = nullptr;
QAction *psUnderline = nullptr;
QAction *psStrikeOut = nullptr;
QAction *psBlockquote = nullptr;
QAction *psMonospace = nullptr;
QAction *psClearFormat = nullptr;
rpl::event_stream<QPoint> _forceClicks;
int _customTitleHeight = 0;
int _lastPressureStage = 0;
};
[[nodiscard]] int32 ScreenNameChecksum(const QString &name);
[[nodiscard]] int32 ScreenNameChecksum(const QScreen *screen);
[[nodiscard]] QString ScreenDisplayLabel(const QScreen *screen);
} // namespace Platform

View File

@@ -0,0 +1,690 @@
/*
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/main_window_mac.h"
#include "data/data_session.h"
#include "core/application.h"
#include "core/sandbox.h"
#include "main/main_session.h"
#include "history/history_widget.h"
#include "history/history_inner_widget.h"
#include "main/main_account.h"
#include "main/main_domain.h" // Domain::activeSessionValue
#include "media/player/media_player_instance.h"
#include "media/audio/media_audio.h"
#include "storage/localstorage.h"
#include "ui/text/text_utilities.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "platform/mac/touchbar/mac_touchbar_manager.h"
#include "platform/platform_specific.h"
#include "platform/platform_notifications_manager.h"
#include "base/platform/base_platform_info.h"
#include "base/options.h"
#include "boxes/peer_list_controllers.h"
#include "boxes/about_box.h"
#include "lang/lang_keys.h"
#include "base/platform/mac/base_utilities_mac.h"
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QTextEdit>
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
#include <qpa/qwindowsysteminterface.h>
#endif // Qt < 6.6.0
#include <Cocoa/Cocoa.h>
#include <CoreFoundation/CFURL.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/hidsystem/ev_keymap.h>
@interface MainWindowObserver : NSObject {
}
- (id) init:(MainWindow::Private*)window;
- (void) activeSpaceDidChange:(NSNotification *)aNotification;
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
- (void) darkModeChanged:(NSNotification *)aNotification;
#endif // Qt < 6.6.0
- (void) screenIsLocked:(NSNotification *)aNotification;
- (void) screenIsUnlocked:(NSNotification *)aNotification;
@end // @interface MainWindowObserver
namespace Platform {
namespace {
// When we close a window that is fullscreen we first leave the fullscreen
// mode and after that hide the window. This is a timeout for elaving the
// fullscreen mode, after that we'll hide the window no matter what.
constexpr auto kHideAfterFullscreenTimeoutMs = 3000;
[[nodiscard]] bool PossiblyTextTypingEvent(NSEvent *e) {
if ([e type] != NSEventTypeKeyDown) {
return false;
}
NSEventModifierFlags flags = [e modifierFlags]
& NSEventModifierFlagDeviceIndependentFlagsMask;
if ((flags & ~NSEventModifierFlagShift) != 0) {
return false;
}
NSString *text = [e characters];
const auto length = int([text length]);
for (auto i = 0; i != length; ++i) {
const auto utf16 = [text characterAtIndex:i];
if (utf16 >= 32) {
return true;
}
}
return false;
}
} // namespace
class MainWindow::Private {
public:
explicit Private(not_null<MainWindow*> window);
void setNativeWindow(NSWindow *window, NSView *view);
void initTouchBar(
NSWindow *window,
not_null<Window::Controller*> controller);
void setWindowBadge(const QString &str);
void setMarkdownEnabledState(Ui::MarkdownEnabledState state);
bool clipboardHasText();
~Private();
private:
not_null<MainWindow*> _public;
friend class MainWindow;
rpl::variable<Ui::MarkdownEnabledState> _markdownState;
NSWindow * __weak _nativeWindow = nil;
NSView * __weak _nativeView = nil;
MainWindowObserver *_observer = nullptr;
NSPasteboard *_generalPasteboard = nullptr;
int _generalPasteboardChangeCount = -1;
bool _generalPasteboardHasText = false;
};
} // namespace Platform
@implementation MainWindowObserver {
MainWindow::Private *_private;
}
- (id) init:(MainWindow::Private*)window {
if (self = [super init]) {
_private = window;
}
return self;
}
- (void) activeSpaceDidChange:(NSNotification *)aNotification {
}
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
- (void) darkModeChanged:(NSNotification *)aNotification {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
QWindowSystemInterface::handleThemeChange();
#else // Qt >= 6.5.0
Core::App().settings().setSystemDarkMode(Platform::IsDarkMode());
#endif // Qt < 6.5.0
});
}
#endif // Qt < 6.6.0
- (void) screenIsLocked:(NSNotification *)aNotification {
Core::App().setScreenIsLocked(true);
}
- (void) screenIsUnlocked:(NSNotification *)aNotification {
Core::App().setScreenIsLocked(false);
}
@end // @implementation MainWindowObserver
namespace Platform {
namespace {
void SendKeySequence(
Qt::Key key,
Qt::KeyboardModifiers modifiers = Qt::NoModifier) {
const auto focused = QApplication::focusWidget();
if (qobject_cast<QLineEdit*>(focused)
|| qobject_cast<QTextEdit*>(focused)
|| dynamic_cast<HistoryInner*>(focused)) {
QApplication::postEvent(
focused,
new QKeyEvent(QEvent::KeyPress, key, modifiers));
QApplication::postEvent(
focused,
new QKeyEvent(QEvent::KeyRelease, key, modifiers));
}
}
void ForceDisabled(QAction *action, bool disabled) {
if (action->isEnabled()) {
if (disabled) action->setDisabled(true);
} else if (!disabled) {
action->setDisabled(false);
}
}
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
QString strNotificationAboutThemeChange() {
const uint32 letters[] = { 0x75E86256, 0xD03E11B1, 0x4D92201D, 0xA2144987, 0x99D5B34F, 0x037589C3, 0x38ED2A7C, 0xD2371ABC, 0xDC98BB02, 0x27964E1B, 0x01748AED, 0xE06679F8, 0x761C9580, 0x4F2595BF, 0x6B5FCBF4, 0xE4D9C24E, 0xBA2F6AB5, 0xE6E3FA71, 0xF2CFC255, 0x56A50C19, 0x43AE1239, 0x77CA4254, 0x7D189A89, 0xEA7663EE, 0x84CEB554, 0xA0ADF236, 0x886512D4, 0x7D3FBDAF, 0x85C4BE4F, 0x12C8255E, 0x9AD8BD41, 0xAC154683, 0xB117598B, 0xDFD9F947, 0x63F06C7B, 0x6340DCD6, 0x3AAE6B3E, 0x26CB125A };
return Platform::MakeFromLetters(letters);
}
#endif // Qt < 6.6.0
QString strNotificationAboutScreenLocked() {
const uint32 letters[] = { 0x34B47F28, 0x47E95179, 0x73D05C42, 0xB4E2A933, 0x924F22D1, 0x4265D8EA, 0x9E4D2CC2, 0x02E8157B, 0x35BF7525, 0x75901A41, 0xB0400FCC, 0xE801169D, 0x4E04B589, 0xC1CEF054, 0xAB2A7EB0, 0x5C67C4F6, 0xA4E2B954, 0xB35E12D2, 0xD598B22B, 0x4E3B8AAB, 0xBEA5E439, 0xFDA8AA3C, 0x1632DBA8, 0x88FE8965 };
return Platform::MakeFromLetters(letters);
}
QString strNotificationAboutScreenUnlocked() {
const uint32 letters[] = { 0xF897900B, 0x19A04630, 0x144DA6DF, 0x643CA7ED, 0x81DDA343, 0x88C6B149, 0x5F9A3A15, 0x31804E13, 0xDF2202B8, 0x9BD1B500, 0x61B92735, 0x7DDF5D43, 0xB74E06C3, 0x16FF1665, 0x9098F702, 0x4461DAF0, 0xA3134FA5, 0x52B01D3C, 0x6BC35769, 0xA7CC945D, 0x8B5327C0, 0x7630B9A0, 0x4E52E3CE, 0xED7765E3, 0xCEB7862D, 0xA06B34F0 };
return Platform::MakeFromLetters(letters);
}
} // namespace
MainWindow::Private::Private(not_null<MainWindow*> window)
: _public(window)
, _observer([[MainWindowObserver alloc] init:this]) {
_generalPasteboard = [NSPasteboard generalPasteboard];
@autoreleasepool {
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:_observer selector:@selector(activeSpaceDidChange:) name:NSWorkspaceActiveSpaceDidChangeNotification object:nil];
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
[[NSDistributedNotificationCenter defaultCenter] addObserver:_observer selector:@selector(darkModeChanged:) name:Q2NSString(strNotificationAboutThemeChange()) object:nil];
#endif // Qt < 6.6.0
[[NSDistributedNotificationCenter defaultCenter] addObserver:_observer selector:@selector(screenIsLocked:) name:Q2NSString(strNotificationAboutScreenLocked()) object:nil];
[[NSDistributedNotificationCenter defaultCenter] addObserver:_observer selector:@selector(screenIsUnlocked:) name:Q2NSString(strNotificationAboutScreenUnlocked()) object:nil];
}
}
void MainWindow::Private::setWindowBadge(const QString &str) {
@autoreleasepool {
[[NSApp dockTile] setBadgeLabel:Q2NSString(str)];
}
}
void MainWindow::Private::setNativeWindow(NSWindow *window, NSView *view) {
_nativeWindow = window;
_nativeView = view;
auto inner = [_nativeWindow contentLayoutRect];
auto full = [_nativeView frame];
_public->_customTitleHeight = qMax(qRound(full.size.height - inner.size.height), 0);
}
void MainWindow::Private::initTouchBar(
NSWindow *window,
not_null<Window::Controller*> controller) {
if (!IsMac10_13OrGreater()) {
return;
}
[NSApplication sharedApplication]
.automaticCustomizeTouchBarMenuItemEnabled = true;
[window
performSelectorOnMainThread:@selector(setTouchBar:)
withObject:[[[RootTouchBar alloc]
init:_markdownState.value()
controller:controller
domain:(&Core::App().domain())] autorelease]
waitUntilDone:true];
}
void MainWindow::Private::setMarkdownEnabledState(
Ui::MarkdownEnabledState state) {
_markdownState = state;
}
bool MainWindow::Private::clipboardHasText() {
auto currentChangeCount = static_cast<int>([_generalPasteboard changeCount]);
if (_generalPasteboardChangeCount != currentChangeCount) {
_generalPasteboardChangeCount = currentChangeCount;
_generalPasteboardHasText = !QGuiApplication::clipboard()->text().isEmpty();
}
return _generalPasteboardHasText;
}
MainWindow::Private::~Private() {
[_observer release];
}
MainWindow::MainWindow(not_null<Window::Controller*> controller)
: Window::MainWindow(controller)
, _private(std::make_unique<Private>(this))
, psMainMenu(this) {
_hideAfterFullScreenTimer.setCallback([this] { hideAndDeactivate(); });
}
void MainWindow::closeWithoutDestroy() {
NSWindow *nsWindow = [reinterpret_cast<NSView*>(winId()) window];
auto isFullScreen = (([nsWindow styleMask] & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen);
if (isFullScreen) {
_hideAfterFullScreenTimer.callOnce(kHideAfterFullscreenTimeoutMs);
[nsWindow toggleFullScreen:nsWindow];
} else {
hideAndDeactivate();
}
}
void MainWindow::stateChangedHook(Qt::WindowState state) {
if (_hideAfterFullScreenTimer.isActive()) {
_hideAfterFullScreenTimer.callOnce(0);
}
}
void MainWindow::initHook() {
_customTitleHeight = 0;
if (auto view = reinterpret_cast<NSView*>(winId())) {
if (auto window = [view window]) {
_private->setNativeWindow(window, view);
if (!base::options::lookup<bool>(
Window::kOptionDisableTouchbar).value()) {
_private->initTouchBar(window, &controller());
} else {
LOG(("Touch Bar was disabled from Experimental Settings."));
}
}
}
}
void MainWindow::updateWindowIcon() {
}
bool MainWindow::nativeEvent(
const QByteArray &eventType,
void *message,
qintptr *result) {
if (message && eventType == "NSEvent") {
const auto event = static_cast<NSEvent*>(message);
if (PossiblyTextTypingEvent(event)) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
imeCompositionStartReceived();
});
} else if ([event type] == NSEventTypePressure) {
const auto stage = [event stage];
if (_lastPressureStage != stage) {
_lastPressureStage = stage;
if (stage == 2) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
_forceClicks.fire(QCursor::pos());
});
}
}
}
}
return false;
}
void MainWindow::hideAndDeactivate() {
hide();
}
void MainWindow::unreadCounterChangedHook() {
updateDockCounter();
}
void MainWindow::updateDockCounter() {
const auto counter = Core::App().unreadBadge();
const auto string = !counter
? QString()
: (counter < 1000)
? QString("%1").arg(counter)
: QString("..%1").arg(counter % 100, 2, 10, QChar('0'));
_private->setWindowBadge(string);
}
void MainWindow::createGlobalMenu() {
const auto ensureWindowShown = [=] {
if (isHidden()) {
showFromTray();
}
};
auto main = psMainMenu.addMenu(u"Telegram"_q);
{
auto callback = [=] {
ensureWindowShown();
controller().show(Box(AboutBox));
};
main->addAction(
tr::lng_mac_menu_about_telegram(
tr::now,
lt_telegram,
u"Telegram"_q),
std::move(callback))
->setMenuRole(QAction::AboutQtRole);
}
main->addSeparator();
{
auto callback = [=] {
ensureWindowShown();
controller().showSettings();
};
auto prefs = main->addAction(
tr::lng_mac_menu_preferences(tr::now),
this,
std::move(callback),
QKeySequence(Qt::ControlModifier | Qt::Key_Comma));
prefs->setMenuRole(QAction::PreferencesRole);
prefs->setShortcutContext(Qt::WidgetShortcut);
}
QMenu *file = psMainMenu.addMenu(tr::lng_mac_menu_file(tr::now));
{
auto callback = [=] {
ensureWindowShown();
controller().showLogoutConfirmation();
};
psLogout = file->addAction(
tr::lng_mac_menu_logout(tr::now),
this,
std::move(callback));
}
QMenu *edit = psMainMenu.addMenu(tr::lng_mac_menu_edit(tr::now));
psUndo = edit->addAction(
tr::lng_mac_menu_undo(tr::now),
this,
[] { SendKeySequence(Qt::Key_Z, Qt::ControlModifier); },
QKeySequence::Undo);
psUndo->setShortcutContext(Qt::WidgetShortcut);
psRedo = edit->addAction(
tr::lng_mac_menu_redo(tr::now),
this,
[] {
SendKeySequence(
Qt::Key_Z,
Qt::ControlModifier | Qt::ShiftModifier);
},
QKeySequence::Redo);
psRedo->setShortcutContext(Qt::WidgetShortcut);
edit->addSeparator();
psCut = edit->addAction(
tr::lng_mac_menu_cut(tr::now),
this,
[] { SendKeySequence(Qt::Key_X, Qt::ControlModifier); },
QKeySequence::Cut);
psCut->setShortcutContext(Qt::WidgetShortcut);
psCopy = edit->addAction(
tr::lng_mac_menu_copy(tr::now),
this,
[] { SendKeySequence(Qt::Key_C, Qt::ControlModifier); },
QKeySequence::Copy);
psCopy->setShortcutContext(Qt::WidgetShortcut);
psPaste = edit->addAction(
tr::lng_mac_menu_paste(tr::now),
this,
[] { SendKeySequence(Qt::Key_V, Qt::ControlModifier); },
QKeySequence::Paste);
psPaste->setShortcutContext(Qt::WidgetShortcut);
psDelete = edit->addAction(
tr::lng_mac_menu_delete(tr::now),
this,
[] { SendKeySequence(Qt::Key_Delete); },
QKeySequence(Qt::ControlModifier | Qt::Key_Backspace));
psDelete->setShortcutContext(Qt::WidgetShortcut);
edit->addSeparator();
psBold = edit->addAction(
tr::lng_menu_formatting_bold(tr::now),
this,
[] { SendKeySequence(Qt::Key_B, Qt::ControlModifier); },
QKeySequence::Bold);
psBold->setShortcutContext(Qt::WidgetShortcut);
psItalic = edit->addAction(
tr::lng_menu_formatting_italic(tr::now),
this,
[] { SendKeySequence(Qt::Key_I, Qt::ControlModifier); },
QKeySequence::Italic);
psItalic->setShortcutContext(Qt::WidgetShortcut);
psUnderline = edit->addAction(
tr::lng_menu_formatting_underline(tr::now),
this,
[] { SendKeySequence(Qt::Key_U, Qt::ControlModifier); },
QKeySequence::Underline);
psUnderline->setShortcutContext(Qt::WidgetShortcut);
psStrikeOut = edit->addAction(
tr::lng_menu_formatting_strike_out(tr::now),
this,
[] {
SendKeySequence(
Qt::Key_X,
Qt::ControlModifier | Qt::ShiftModifier);
},
Ui::kStrikeOutSequence);
psStrikeOut->setShortcutContext(Qt::WidgetShortcut);
psBlockquote = edit->addAction(
tr::lng_menu_formatting_blockquote(tr::now),
this,
[] {
SendKeySequence(
Qt::Key_Period,
Qt::ControlModifier | Qt::ShiftModifier);
},
Ui::kBlockquoteSequence);
psBlockquote->setShortcutContext(Qt::WidgetShortcut);
psMonospace = edit->addAction(
tr::lng_menu_formatting_monospace(tr::now),
this,
[] {
SendKeySequence(
Qt::Key_M,
Qt::ControlModifier | Qt::ShiftModifier);
},
Ui::kMonospaceSequence);
psMonospace->setShortcutContext(Qt::WidgetShortcut);
psClearFormat = edit->addAction(
tr::lng_menu_formatting_clear(tr::now),
this,
[] {
SendKeySequence(
Qt::Key_N,
Qt::ControlModifier | Qt::ShiftModifier);
},
Ui::kClearFormatSequence);
psClearFormat->setShortcutContext(Qt::WidgetShortcut);
edit->addSeparator();
psSelectAll = edit->addAction(
tr::lng_mac_menu_select_all(tr::now),
this,
[] { SendKeySequence(Qt::Key_A, Qt::ControlModifier); },
QKeySequence::SelectAll);
psSelectAll->setShortcutContext(Qt::WidgetShortcut);
edit->addSeparator();
edit->addAction(
tr::lng_mac_menu_emoji_and_symbols(
tr::now,
Ui::Text::FixAmpersandInAction),
this,
[] { [NSApp orderFrontCharacterPalette:nil]; },
QKeySequence(Qt::MetaModifier | Qt::ControlModifier | Qt::Key_Space)
)->setShortcutContext(Qt::WidgetShortcut);
QMenu *window = psMainMenu.addMenu(tr::lng_mac_menu_window(tr::now));
window->addAction(
tr::lng_mac_menu_fullscreen(tr::now),
this,
[=] {
NSWindow *nsWindow = [reinterpret_cast<NSView*>(winId()) window];
[nsWindow toggleFullScreen:nsWindow];
},
QKeySequence(Qt::MetaModifier | Qt::ControlModifier | Qt::Key_F)
)->setShortcutContext(Qt::WidgetShortcut);
window->addSeparator();
psContacts = window->addAction(tr::lng_mac_menu_contacts(tr::now));
connect(psContacts, &QAction::triggered, psContacts, crl::guard(this, [=] {
Expects(sessionController() != nullptr && !controller().locked());
ensureWindowShown();
sessionController()->show(PrepareContactsBox(sessionController()));
}));
{
auto callback = [=] {
Expects(sessionController() != nullptr && !controller().locked());
ensureWindowShown();
sessionController()->showAddContact();
};
psAddContact = window->addAction(
tr::lng_mac_menu_add_contact(tr::now),
this,
std::move(callback));
}
window->addSeparator();
{
auto callback = [=] {
Expects(sessionController() != nullptr && !controller().locked());
ensureWindowShown();
sessionController()->showNewGroup();
};
psNewGroup = window->addAction(
tr::lng_mac_menu_new_group(tr::now),
this,
std::move(callback));
}
{
auto callback = [=] {
Expects(sessionController() != nullptr && !controller().locked());
ensureWindowShown();
sessionController()->showNewChannel();
};
psNewChannel = window->addAction(
tr::lng_mac_menu_new_channel(tr::now),
this,
std::move(callback));
}
window->addSeparator();
psShowTelegram = window->addAction(
tr::lng_mac_menu_show(tr::now),
this,
[=] { showFromTray(); });
updateGlobalMenu();
}
void MainWindow::updateGlobalMenuHook() {
if (!positionInited()) {
return;
}
auto focused = QApplication::focusWidget();
bool canUndo = false, canRedo = false, canCut = false, canCopy = false, canPaste = false, canDelete = false, canSelectAll = false;
auto clipboardHasText = _private->clipboardHasText();
auto markdownState = Ui::MarkdownEnabledState();
if (auto edit = qobject_cast<QLineEdit*>(focused)) {
canCut = canCopy = canDelete = edit->hasSelectedText();
canSelectAll = !edit->text().isEmpty();
canUndo = edit->isUndoAvailable();
canRedo = edit->isRedoAvailable();
canPaste = clipboardHasText;
} else if (auto edit = qobject_cast<QTextEdit*>(focused)) {
canCut = canCopy = canDelete = edit->textCursor().hasSelection();
canSelectAll = !edit->document()->isEmpty();
canUndo = edit->document()->isUndoAvailable();
canRedo = edit->document()->isRedoAvailable();
canPaste = clipboardHasText;
if (canCopy) {
if (const auto inputField = dynamic_cast<Ui::InputField*>(
focused->parentWidget())) {
markdownState = inputField->markdownEnabledState();
}
}
} else if (auto list = dynamic_cast<HistoryInner*>(focused)) {
canCopy = list->canCopySelected();
canDelete = list->canDeleteSelected();
}
_private->setMarkdownEnabledState(markdownState);
updateIsActive();
const auto logged = (sessionController() != nullptr);
const auto inactive = !logged || controller().locked();
const auto support = logged
&& sessionController()->session().supportMode();
ForceDisabled(psLogout, !logged && !Core::App().passcodeLocked());
ForceDisabled(psUndo, !canUndo);
ForceDisabled(psRedo, !canRedo);
ForceDisabled(psCut, !canCut);
ForceDisabled(psCopy, !canCopy);
ForceDisabled(psPaste, !canPaste);
ForceDisabled(psDelete, !canDelete);
ForceDisabled(psSelectAll, !canSelectAll);
ForceDisabled(psContacts, inactive || support);
ForceDisabled(psAddContact, inactive);
ForceDisabled(psNewGroup, inactive || support);
ForceDisabled(psNewChannel, inactive || support);
ForceDisabled(psShowTelegram, isActive());
const auto diabled = [=](const QString &tag) {
return !markdownState.enabledForTag(tag);
};
using Field = Ui::InputField;
ForceDisabled(psBold, diabled(Field::kTagBold));
ForceDisabled(psItalic, diabled(Field::kTagItalic));
ForceDisabled(psUnderline, diabled(Field::kTagUnderline));
ForceDisabled(psStrikeOut, diabled(Field::kTagStrikeOut));
ForceDisabled(psBlockquote, diabled(Field::kTagBlockquote));
ForceDisabled(
psMonospace,
diabled(Field::kTagPre) || diabled(Field::kTagCode));
ForceDisabled(psClearFormat, markdownState.disabled());
}
bool MainWindow::eventFilter(QObject *obj, QEvent *evt) {
QEvent::Type t = evt->type();
if (t == QEvent::FocusIn || t == QEvent::FocusOut) {
if (qobject_cast<QLineEdit*>(obj) || qobject_cast<QTextEdit*>(obj) || dynamic_cast<HistoryInner*>(obj)) {
updateGlobalMenu();
}
}
return Window::MainWindow::eventFilter(obj, evt);
}
MainWindow::~MainWindow() {
}
int32 ScreenNameChecksum(const QString &name) {
return Window::DefaultScreenNameChecksum(name);
}
int32 ScreenNameChecksum(const QScreen *screen) {
return ScreenNameChecksum(screen->name());
}
QString ScreenDisplayLabel(const QScreen *screen) {
return screen ? screen->name() : QString();
}
} // namespace

View File

@@ -0,0 +1,43 @@
/*
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 "platform/platform_notifications_manager.h"
#include "base/weak_ptr.h"
namespace Platform {
namespace Notifications {
class Manager : public Window::Notifications::NativeManager, public base::has_weak_ptr {
public:
Manager(Window::Notifications::System *system);
~Manager();
protected:
void doShowNativeNotification(
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView) override;
void doClearAllFast() override;
void doClearFromItem(not_null<HistoryItem*> item) override;
void doClearFromTopic(not_null<Data::ForumTopic*> topic) override;
void doClearFromSublist(not_null<Data::SavedSublist*> sublist) override;
void doClearFromHistory(not_null<History*> history) override;
void doClearFromSession(not_null<Main::Session*> session) override;
QString accountNameSeparator() override;
bool doSkipToast() const override;
void doMaybePlaySound(Fn<void()> playSound) override;
void doMaybeFlashBounce(Fn<void()> flashBounce) override;
private:
class Private;
const std::unique_ptr<Private> _private;
};
} // namespace Notifications
} // namespace Platform

View File

@@ -0,0 +1,705 @@
/*
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/notifications_manager_mac.h"
#include "base/platform/base_platform_info.h"
#include "base/options.h"
#include "base/platform/mac/base_utilities_mac.h"
#include "base/random.h"
#include "base/unixtime.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "data/data_forum_topic.h"
#include "data/data_peer.h"
#include "data/data_saved_sublist.h"
#include "history/history_item.h"
#include "history/history.h"
#include "lang/lang_cloud_manager.h"
#include "lang/lang_instance.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "mainwindow.h"
#include "platform/platform_specific.h"
#include "ui/empty_userpic.h"
#include "window/notifications_utilities.h"
#include "styles/style_window.h"
#include <thread>
#include <Cocoa/Cocoa.h>
namespace {
constexpr auto kQuerySettingsEachMs = crl::time(1000);
constexpr auto kCacheExpirationWeeks = 5;
constexpr auto kCacheExpirationSeconds = kCacheExpirationWeeks * 7 * 24 * 60 * 60;
NSString *const kTelegramMarkAsReadText = @"TelegramMarkAsReadText";
NSString *const kTelegramMarkAsReadTimestamp = @"TelegramMarkAsReadTimestamp";
NSString *const kTelegramMarkAsReadLanguageCode = @"TelegramMarkAsReadLanguageCode";
crl::time LastSettingsQueryMs/* = 0*/;
bool DoNotDisturbEnabled/* = false*/;
[[nodiscard]] bool ShouldQuerySettings() {
const auto now = crl::now();
if (LastSettingsQueryMs > 0 && now <= LastSettingsQueryMs + kQuerySettingsEachMs) {
return false;
}
LastSettingsQueryMs = now;
return true;
}
[[nodiscard]] QString LibraryPath() {
static const auto result = [] {
NSURL *url = [[NSFileManager defaultManager] URLForDirectory:NSLibraryDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
return url
? QString::fromUtf8([[url path] fileSystemRepresentation])
: QString();
}();
return result;
}
void queryDoNotDisturbState() {
if (!ShouldQuerySettings()) {
return;
}
Boolean isKeyValid;
const auto doNotDisturb = CFPreferencesGetAppBooleanValue(
CFSTR("doNotDisturb"),
CFSTR("com.apple.notificationcenterui"),
&isKeyValid);
DoNotDisturbEnabled = isKeyValid
? doNotDisturb
: false;
}
using Manager = Platform::Notifications::Manager;
} // namespace
@interface NotificationDelegate : NSObject<NSUserNotificationCenterDelegate> {
}
- (id) initWithManager:(base::weak_ptr<Manager>)manager managerId:(uint64)managerId;
- (void) userNotificationCenter:(NSUserNotificationCenter*)center didActivateNotification:(NSUserNotification*)notification;
- (BOOL) userNotificationCenter:(NSUserNotificationCenter*)center shouldPresentNotification:(NSUserNotification*)notification;
@end // @interface NotificationDelegate
@implementation NotificationDelegate {
base::weak_ptr<Manager> _manager;
uint64 _managerId;
}
- (id) initWithManager:(base::weak_ptr<Manager>)manager managerId:(uint64)managerId {
if (self = [super init]) {
_manager = manager;
_managerId = managerId;
}
return self;
}
- (void) userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification {
NSDictionary *notificationUserInfo = [notification userInfo];
NSNumber *managerIdObject = [notificationUserInfo objectForKey:@"manager"];
auto notificationManagerId = managerIdObject ? [managerIdObject unsignedLongLongValue] : 0ULL;
DEBUG_LOG(("Received notification with instance %1, mine: %2").arg(notificationManagerId).arg(_managerId));
if (notificationManagerId != _managerId) { // other app instance notification
crl::on_main([] {
// Usually we show and activate main window when the application
// is activated (receives applicationDidBecomeActive: notification).
//
// This is used for window show in Cmd+Tab switching to the application.
//
// But when a notification arrives sometimes macOS still activates the app
// and we receive applicationDidBecomeActive: notification even if the
// notification was sent by another instance of the application. In that case
// we set a flag for a couple of seconds to ignore this app activation.
objc_ignoreApplicationActivationRightNow();
});
return;
}
NSNumber *sessionObject = [notificationUserInfo objectForKey:@"session"];
const auto notificationSessionId = sessionObject ? [sessionObject unsignedLongLongValue] : 0;
if (!notificationSessionId) {
LOG(("App Error: A notification with unknown session was received"));
return;
}
NSNumber *peerObject = [notificationUserInfo objectForKey:@"peer"];
const auto notificationPeerId = peerObject ? [peerObject unsignedLongLongValue] : 0ULL;
if (!notificationPeerId) {
LOG(("App Error: A notification with unknown peer was received"));
return;
}
NSNumber *topicObject = [notificationUserInfo objectForKey:@"topic"];
if (!topicObject) {
LOG(("App Error: A notification with unknown topic was received"));
return;
}
const auto notificationTopicRootId = [topicObject longLongValue];
NSNumber *monoforumPeerObject = [notificationUserInfo objectForKey:@"monoforumpeer"];
if (!monoforumPeerObject) {
LOG(("App Error: A notification with unknown monoforum peer was received"));
return;
}
const auto notificationMonoforumPeerId = [monoforumPeerObject unsignedLongLongValue];
NSNumber *msgObject = [notificationUserInfo objectForKey:@"msgid"];
const auto notificationMsgId = msgObject ? [msgObject longLongValue] : 0LL;
const auto my = Window::Notifications::Manager::NotificationId{
.contextId = Manager::ContextId{
.sessionId = notificationSessionId,
.peerId = PeerId(notificationPeerId),
.topicRootId = MsgId(notificationTopicRootId),
.monoforumPeerId = PeerId(notificationMonoforumPeerId),
},
.msgId = notificationMsgId,
};
if (notification.activationType == NSUserNotificationActivationTypeReplied) {
const auto notificationReply = QString::fromUtf8([[[notification response] string] UTF8String]);
const auto manager = _manager;
crl::on_main(manager, [=] {
manager->notificationReplied(my, { notificationReply, {} });
});
} else if (notification.activationType == NSUserNotificationActivationTypeContentsClicked) {
const auto manager = _manager;
crl::on_main(manager, [=] {
manager->notificationActivated(my);
});
}
if (notification.activationType == NSUserNotificationActivationTypeAdditionalActionClicked
|| notification.activationType == NSUserNotificationActivationTypeActionButtonClicked) {
const auto manager = _manager;
NSString *actionId = [notificationUserInfo objectForKey:@"actionId"];
if ([actionId isEqualToString:@"markAsRead"]) {
crl::on_main(manager, [=] {
manager->notificationReplied(my, {});
});
}
}
[center removeDeliveredNotification: notification];
}
- (BOOL) userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification {
return YES;
}
@end // @implementation NotificationDelegate
namespace Platform {
namespace Notifications {
bool SkipToastForCustom() {
return false;
}
void MaybePlaySoundForCustom(Fn<void()> playSound) {
playSound();
}
void MaybeFlashBounceForCustom(Fn<void()> flashBounce) {
flashBounce();
}
bool WaitForInputForCustom() {
return true;
}
bool Supported() {
return true;
}
bool Enforced() {
return Supported();
}
bool ByDefault() {
return Supported();
}
bool VolumeSupported() {
return false;
}
void Create(Window::Notifications::System *system) {
system->setManager([=] { return std::make_unique<Manager>(system); });
}
class Manager::Private : public QObject {
public:
Private(Manager *manager);
void showNotification(
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView);
void clearAll();
void clearFromItem(not_null<HistoryItem*> item);
void clearFromTopic(not_null<Data::ForumTopic*> topic);
void clearFromSublist(not_null<Data::SavedSublist*> sublist);
void clearFromHistory(not_null<History*> history);
void clearFromSession(not_null<Main::Session*> session);
void updateDelegate();
~Private();
private:
template <typename Task>
void putClearTask(Task task);
void clearingThreadLoop();
void initCachedMarkAsReadText();
const uint64 _managerId = 0;
QString _managerIdString;
NotificationDelegate *_delegate = nullptr;
std::thread _clearingThread;
std::mutex _clearingMutex;
std::condition_variable _clearingCondition;
struct ClearFromItem {
NotificationId id;
};
struct ClearFromTopic {
ContextId contextId;
};
struct ClearFromSublist {
ContextId contextId;
};
struct ClearFromHistory {
ContextId partialContextId;
};
struct ClearFromSession {
uint64 sessionId = 0;
};
struct ClearAll {
};
struct ClearFinish {
};
using ClearTask = std::variant<
ClearFromItem,
ClearFromTopic,
ClearFromSublist,
ClearFromHistory,
ClearFromSession,
ClearAll,
ClearFinish>;
std::vector<ClearTask> _clearingTasks;
Media::Audio::LocalDiskCache _sounds;
NSString *_cachedMarkAsReadText = nil;
rpl::lifetime _lifetime;
};
[[nodiscard]] QString ResolveSoundsFolder() {
NSArray *paths = NSSearchPathForDirectoriesInDomains(
NSLibraryDirectory,
NSUserDomainMask,
YES);
NSString *library = [paths firstObject];
NSString *sounds = [library stringByAppendingPathComponent : @"Sounds"];
return NS2QString(sounds);
}
void AddActionIdToNotification(
NSUserNotification *notification,
NSString *actionId) {
NSMutableDictionary *mutableUserInfo
= [[notification userInfo] mutableCopy];
[mutableUserInfo setObject:actionId forKey:@"actionId"];
[notification setUserInfo:mutableUserInfo];
[mutableUserInfo release];
}
Manager::Private::Private(Manager *manager)
: _managerId(base::RandomValue<uint64>())
, _managerIdString(QString::number(_managerId))
, _delegate([[NotificationDelegate alloc] initWithManager:manager managerId:_managerId])
, _sounds(ResolveSoundsFolder()) {
Core::App().settings().workModeValue(
) | rpl::on_next([=](Core::Settings::WorkMode mode) {
// We need to update the delegate _after_ the tray icon change was done in Qt.
// Because Qt resets the delegate.
crl::on_main(this, [=] {
updateDelegate();
});
}, _lifetime);
initCachedMarkAsReadText();
}
void Manager::Private::showNotification(
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView) {
@autoreleasepool {
const auto peer = info.peer;
NSUserNotification *notification = [[[NSUserNotification alloc] init] autorelease];
if ([notification respondsToSelector:@selector(setIdentifier:)]) {
auto identifier = _managerIdString
+ '_'
+ QString::number(peer->id.value)
+ '_'
+ QString::number(info.itemId.bare);
auto identifierValue = Q2NSString(identifier);
[notification setIdentifier:identifierValue];
}
[notification setUserInfo:
[NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithUnsignedLongLong:peer->session().uniqueId()],
@"session",
[NSNumber numberWithUnsignedLongLong:peer->id.value],
@"peer",
[NSNumber numberWithLongLong:info.topicRootId.bare],
@"topic",
[NSNumber numberWithUnsignedLongLong:info.monoforumPeerId.value],
@"monoforumpeer",
[NSNumber numberWithLongLong:info.itemId.bare],
@"msgid",
[NSNumber numberWithUnsignedLongLong:_managerId],
@"manager",
nil]];
[notification setTitle:Q2NSString(info.title)];
[notification setSubtitle:Q2NSString(info.subtitle)];
[notification setInformativeText:Q2NSString(info.message)];
if (!info.options.hideNameAndPhoto
&& [notification respondsToSelector:@selector(setContentImage:)]) {
NSImage *img = Q2NSImage(
Window::Notifications::GenerateUserpic(peer, userpicView));
[notification setContentImage:img];
}
if (!info.options.hideReplyButton
&& !info.options.hideMarkAsRead
&& [notification respondsToSelector:@selector(setHasReplyButton:)]
&& [notification respondsToSelector:@selector(setAdditionalActions:)]) {
[notification setHasReplyButton:YES];
AddActionIdToNotification(notification, @"markAsRead");
[notification setAdditionalActions:@[
[NSUserNotificationAction
actionWithIdentifier:@"markAsRead"
title:_cachedMarkAsReadText]
]];
} else if (!info.options.hideReplyButton
&& [notification respondsToSelector:@selector(setHasReplyButton:)]) {
[notification setHasReplyButton:YES];
} else if (!info.options.hideMarkAsRead
&& [notification respondsToSelector:@selector(setHasActionButton:)]) {
[notification setHasActionButton:YES];
[notification
setActionButtonTitle:_cachedMarkAsReadText];
AddActionIdToNotification(notification, @"markAsRead");
}
const auto sound = info.sound ? info.sound() : Media::Audio::LocalSound();
if (sound) {
[notification setSoundName:Q2NSString(_sounds.name(sound))];
} else {
[notification setSoundName:nil];
}
NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
[center deliverNotification:notification];
}
}
void Manager::Private::clearingThreadLoop() {
auto finished = false;
while (!finished) {
auto clearAll = false;
auto clearFromItems = base::flat_set<NotificationId>();
auto clearFromTopics = base::flat_set<ContextId>();
auto clearFromSublists = base::flat_set<ContextId>();
auto clearFromHistories = base::flat_set<ContextId>();
auto clearFromSessions = base::flat_set<uint64>();
{
std::unique_lock<std::mutex> lock(_clearingMutex);
while (_clearingTasks.empty()) {
_clearingCondition.wait(lock);
}
for (auto &task : _clearingTasks) {
v::match(task, [&](ClearFinish) {
finished = true;
clearAll = true;
}, [&](ClearAll) {
clearAll = true;
}, [&](const ClearFromItem &value) {
clearFromItems.emplace(value.id);
}, [&](const ClearFromTopic &value) {
clearFromTopics.emplace(value.contextId);
}, [&](const ClearFromSublist &value) {
clearFromSublists.emplace(value.contextId);
}, [&](const ClearFromHistory &value) {
clearFromHistories.emplace(value.partialContextId);
}, [&](const ClearFromSession &value) {
clearFromSessions.emplace(value.sessionId);
});
}
_clearingTasks.clear();
}
@autoreleasepool {
auto clearBySpecial = [&](NSDictionary *notificationUserInfo) {
NSNumber *sessionObject = [notificationUserInfo objectForKey:@"session"];
const auto notificationSessionId = sessionObject ? [sessionObject unsignedLongLongValue] : 0;
if (!notificationSessionId) {
return true;
}
NSNumber *peerObject = [notificationUserInfo objectForKey:@"peer"];
const auto notificationPeerId = peerObject ? [peerObject unsignedLongLongValue] : 0;
if (!notificationPeerId) {
return true;
}
NSNumber *topicObject = [notificationUserInfo objectForKey:@"topic"];
if (!topicObject) {
return true;
}
const auto notificationTopicRootId = [topicObject longLongValue];
NSNumber *monoforumPeerObject = [notificationUserInfo objectForKey:@"monoforumpeer"];
if (!monoforumPeerObject) {
return true;
}
const auto notificationMonoforumPeerId = [monoforumPeerObject unsignedLongLongValue];
NSNumber *msgObject = [notificationUserInfo objectForKey:@"msgid"];
const auto msgId = msgObject ? [msgObject longLongValue] : 0LL;
const auto partialContextId = ContextId{
.sessionId = notificationSessionId,
.peerId = PeerId(notificationPeerId),
};
const auto contextId = notificationTopicRootId
? ContextId{
.sessionId = notificationSessionId,
.peerId = PeerId(notificationPeerId),
.topicRootId = MsgId(notificationTopicRootId),
}
: notificationMonoforumPeerId
? ContextId{
.sessionId = notificationSessionId,
.peerId = PeerId(notificationPeerId),
.monoforumPeerId = PeerId(notificationMonoforumPeerId),
}
: partialContextId;
const auto id = NotificationId{ contextId, MsgId(msgId) };
return clearFromSessions.contains(notificationSessionId)
|| clearFromHistories.contains(partialContextId)
|| clearFromTopics.contains(contextId)
|| clearFromSublists.contains(contextId)
|| (msgId && clearFromItems.contains(id));
};
NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
NSArray *notificationsList = [center deliveredNotifications];
for (id notification in notificationsList) {
NSDictionary *notificationUserInfo = [notification userInfo];
NSNumber *managerIdObject = [notificationUserInfo objectForKey:@"manager"];
auto notificationManagerId = managerIdObject ? [managerIdObject unsignedLongLongValue] : 0ULL;
if (notificationManagerId == _managerId) {
if (clearAll || clearBySpecial(notificationUserInfo)) {
[center removeDeliveredNotification:notification];
}
}
}
}
}
}
template <typename Task>
void Manager::Private::putClearTask(Task task) {
if (!_clearingThread.joinable()) {
_clearingThread = std::thread([this] { clearingThreadLoop(); });
}
std::unique_lock<std::mutex> lock(_clearingMutex);
_clearingTasks.push_back(task);
_clearingCondition.notify_one();
}
void Manager::Private::clearAll() {
putClearTask(ClearAll());
}
void Manager::Private::clearFromItem(not_null<HistoryItem*> item) {
putClearTask(ClearFromItem{ ContextId{
.sessionId = item->history()->session().uniqueId(),
.peerId = item->history()->peer->id,
.topicRootId = item->topicRootId(),
.monoforumPeerId = item->sublistPeerId(),
}, item->id });
}
void Manager::Private::clearFromTopic(not_null<Data::ForumTopic*> topic) {
putClearTask(ClearFromTopic{ ContextId{
.sessionId = topic->session().uniqueId(),
.peerId = topic->history()->peer->id,
.topicRootId = topic->rootId(),
} });
}
void Manager::Private::clearFromSublist(
not_null<Data::SavedSublist*> sublist) {
putClearTask(ClearFromSublist{ ContextId{
.sessionId = sublist->session().uniqueId(),
.peerId = sublist->owningHistory()->peer->id,
.monoforumPeerId = sublist->sublistPeer()->id,
} });
}
void Manager::Private::clearFromHistory(not_null<History*> history) {
putClearTask(ClearFromHistory{ ContextId{
.sessionId = history->session().uniqueId(),
.peerId = history->peer->id,
} });
}
void Manager::Private::clearFromSession(not_null<Main::Session*> session) {
putClearTask(ClearFromSession{ session->uniqueId() });
}
void Manager::Private::updateDelegate() {
NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
[center setDelegate:_delegate];
}
void Manager::Private::initCachedMarkAsReadText() {
const auto langId = Lang::GetInstance().id();
const auto preferredLang = [[NSLocale preferredLanguages] firstObject];
const auto languageCode
= [[NSLocale localeWithLocaleIdentifier:preferredLang] languageCode];
const auto defaults = [NSUserDefaults standardUserDefaults];
const auto cachedText = static_cast<NSString*>([defaults
stringForKey:kTelegramMarkAsReadText]);
const auto cachedTimestamp = static_cast<NSNumber*>([defaults
objectForKey:kTelegramMarkAsReadTimestamp]);
const auto cachedLanguageCode = static_cast<NSString*>([defaults
stringForKey:kTelegramMarkAsReadLanguageCode]);
const auto now = base::unixtime::now();
const auto shouldRefresh = !cachedTimestamp
|| (now - [cachedTimestamp longLongValue]) > kCacheExpirationSeconds
|| ![cachedLanguageCode isEqualToString:languageCode];
if (cachedText && !shouldRefresh) {
_cachedMarkAsReadText = [cachedText retain];
} else {
_cachedMarkAsReadText
= [Q2NSString(tr::lng_context_mark_read(tr::now)) retain];
}
if (langId == NS2QString(languageCode)) {
[defaults
setObject:Q2NSString(tr::lng_context_mark_read(tr::now))
forKey:kTelegramMarkAsReadText];
[defaults
setObject:@(base::unixtime::now())
forKey:kTelegramMarkAsReadTimestamp];
[defaults
setObject:languageCode
forKey:kTelegramMarkAsReadLanguageCode];
} else if (shouldRefresh) {
Lang::CurrentCloudManager().getValueForLang(
u"lng_context_mark_read"_q,
NS2QString(languageCode),
[=](const QString &r) {
if (r != NS2QString(_cachedMarkAsReadText)) {
[_cachedMarkAsReadText release];
_cachedMarkAsReadText = [Q2NSString(r) retain];
}
[defaults
setObject:Q2NSString(r)
forKey:kTelegramMarkAsReadText];
[defaults
setObject:@(base::unixtime::now())
forKey:kTelegramMarkAsReadTimestamp];
[defaults
setObject:languageCode
forKey:kTelegramMarkAsReadLanguageCode];
});
}
}
Manager::Private::~Private() {
if (_clearingThread.joinable()) {
putClearTask(ClearFinish());
_clearingThread.join();
}
NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
[center setDelegate:nil];
[_delegate release];
[_cachedMarkAsReadText release];
}
Manager::Manager(Window::Notifications::System *system) : NativeManager(system)
, _private(std::make_unique<Private>(this)) {
}
Manager::~Manager() = default;
void Manager::doShowNativeNotification(
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView) {
_private->showNotification(std::move(info), userpicView);
}
void Manager::doClearAllFast() {
_private->clearAll();
}
void Manager::doClearFromItem(not_null<HistoryItem*> item) {
_private->clearFromItem(item);
}
void Manager::doClearFromTopic(not_null<Data::ForumTopic*> topic) {
_private->clearFromTopic(topic);
}
void Manager::doClearFromSublist(not_null<Data::SavedSublist*> sublist) {
_private->clearFromSublist(sublist);
}
void Manager::doClearFromHistory(not_null<History*> history) {
_private->clearFromHistory(history);
}
void Manager::doClearFromSession(not_null<Main::Session*> session) {
_private->clearFromSession(session);
}
QString Manager::accountNameSeparator() {
return QString::fromUtf8(" \xE2\x86\x92 ");
}
bool Manager::doSkipToast() const {
return false;
}
void Manager::doMaybePlaySound(Fn<void()> playSound) {
playSound();
}
void Manager::doMaybeFlashBounce(Fn<void()> flashBounce) {
flashBounce();
}
} // namespace Notifications
} // namespace Platform

View File

@@ -0,0 +1,58 @@
/*
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 "platform/platform_overlay_widget.h"
template <typename Object>
class object_ptr;
namespace Ui {
class AbstractButton;
} // namespace Ui
namespace Ui::Platform {
enum class TitleControl;
} // namespace Ui::Platform
namespace Platform {
class MacOverlayWidgetHelper final : public OverlayWidgetHelper {
public:
MacOverlayWidgetHelper(
not_null<Ui::RpWindow*> window,
Fn<void(bool)> maximize);
~MacOverlayWidgetHelper();
void beforeShow(bool fullscreen) override;
void afterShow(bool fullscreen) override;
void notifyFileDialogShown(bool shown) override;
void minimize(not_null<Ui::RpWindow*> window) override;
void clearState() override;
void setControlsOpacity(float64 opacity) override;
rpl::producer<bool> controlsSideRightValue() override;
rpl::producer<int> topNotchSkipValue() override;
private:
using Control = Ui::Platform::TitleControl;
struct Data;
void activate(Control control);
void resolveNative();
void updateStyles(bool fullscreen);
void refreshButtons(bool fullscreen);
object_ptr<Ui::AbstractButton> create(
not_null<QWidget*> parent,
Control control);
std::unique_ptr<Data> _data;
};
} // namespace Platform

View File

@@ -0,0 +1,296 @@
/*
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/overlay_widget_mac.h"
#include "base/object_ptr.h"
#include "ui/platform/ui_platform_window_title.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/rp_window.h"
#include "styles/style_media_view.h"
#include <QtGui/QWindow>
#include <Cocoa/Cocoa.h>
namespace Platform {
namespace {
using namespace Media::View;
} // namespace
struct MacOverlayWidgetHelper::Data {
const not_null<Ui::RpWindow*> window;
const Fn<void(bool)> maximize;
object_ptr<Ui::AbstractButton> buttonClose = { nullptr };
object_ptr<Ui::AbstractButton> buttonMinimize = { nullptr };
object_ptr<Ui::AbstractButton> buttonMaximize = { nullptr };
rpl::event_stream<> activations;
rpl::variable<float64> masterOpacity = 1.;
rpl::variable<bool> maximized = false;
rpl::event_stream<> clearStateRequests;
bool anyOver = false;
NSWindow * __weak native = nil;
rpl::variable<int> topNotchSkip;
};
MacOverlayWidgetHelper::MacOverlayWidgetHelper(
not_null<Ui::RpWindow*> window,
Fn<void(bool)> maximize)
: _data(std::make_unique<Data>(Data{
.window = window,
.maximize = std::move(maximize),
})) {
_data->buttonClose = create(window, Control::Close);
_data->buttonMinimize = create(window, Control::Minimize);
_data->buttonMaximize = create(window, Control::Maximize);
}
MacOverlayWidgetHelper::~MacOverlayWidgetHelper() = default;
void MacOverlayWidgetHelper::activate(Control control) {
const auto fullscreen = (_data->window->windowHandle()->flags() & Qt::FramelessWindowHint);
switch (control) {
case Control::Close: _data->window->close(); return;
case Control::Minimize: [_data->native miniaturize:_data->native]; return;
case Control::Maximize: _data->maximize(!fullscreen); return;
}
}
void MacOverlayWidgetHelper::beforeShow(bool fullscreen) {
_data->window->setAttribute(Qt::WA_MacAlwaysShowToolWindow, !fullscreen);
_data->window->windowHandle()->setFlag(Qt::FramelessWindowHint, fullscreen);
updateStyles(fullscreen);
clearState();
}
void MacOverlayWidgetHelper::afterShow(bool fullscreen) {
updateStyles(fullscreen);
refreshButtons(fullscreen);
_data->window->activateWindow();
}
void MacOverlayWidgetHelper::resolveNative() {
if (const auto handle = _data->window->winId()) {
_data->native = [reinterpret_cast<NSView*>(handle) window];
}
}
void MacOverlayWidgetHelper::updateStyles(bool fullscreen) {
_data->maximized = fullscreen;
resolveNative();
if (!_data->native) {
return;
}
const auto window = _data->native;
const auto level = !fullscreen
? NSNormalWindowLevel
: NSPopUpMenuWindowLevel;
[window setLevel:level];
[window setHidesOnDeactivate:!_data->window->testAttribute(Qt::WA_MacAlwaysShowToolWindow)];
[window setTitleVisibility:NSWindowTitleHidden];
[window setTitlebarAppearsTransparent:YES];
[window setStyleMask:[window styleMask] | NSWindowStyleMaskFullSizeContentView];
if (@available(macOS 12.0, *)) {
_data->topNotchSkip = [[window screen] safeAreaInsets].top;
}
}
void MacOverlayWidgetHelper::refreshButtons(bool fullscreen) {
Expects(_data->native != nullptr);
const auto window = _data->native;
const auto process = [&](NSWindowButton type) {
if (const auto button = [window standardWindowButton:type]) {
[button setHidden:YES];
}
};
process(NSWindowCloseButton);
process(NSWindowMiniaturizeButton);
process(NSWindowZoomButton);
_data->buttonClose->moveToLeft(0, 0);
_data->buttonClose->raise();
_data->buttonClose->show();
_data->buttonMinimize->moveToLeft(_data->buttonClose->width(), 0);
_data->buttonMinimize->raise();
_data->buttonMinimize->show();
_data->buttonMaximize->moveToLeft(_data->buttonClose->width() + _data->buttonMinimize->width(), 0);
_data->buttonMaximize->raise();
_data->buttonMaximize->show();
}
void MacOverlayWidgetHelper::notifyFileDialogShown(bool shown) {
resolveNative();
if (_data->native && !_data->window->isHidden()) {
const auto level = [_data->native level];
if (level != NSNormalWindowLevel) {
const auto level = shown
? NSModalPanelWindowLevel
: NSPopUpMenuWindowLevel;
[_data->native setLevel:level];
}
}
}
void MacOverlayWidgetHelper::minimize(not_null<Ui::RpWindow*> window) {
resolveNative();
if (_data->native) {
[_data->native miniaturize:_data->native];
}
}
void MacOverlayWidgetHelper::clearState() {
_data->clearStateRequests.fire({});
}
void MacOverlayWidgetHelper::setControlsOpacity(float64 opacity) {
_data->masterOpacity = opacity;
}
rpl::producer<bool> MacOverlayWidgetHelper::controlsSideRightValue() {
return rpl::single(false);
}
rpl::producer<int> MacOverlayWidgetHelper::topNotchSkipValue() {
return _data->topNotchSkip.value();
}
object_ptr<Ui::AbstractButton> MacOverlayWidgetHelper::create(
not_null<QWidget*> parent,
Control control) {
auto result = object_ptr<Ui::AbstractButton>(parent);
const auto raw = result.data();
raw->setClickedCallback([=] { activate(control); });
struct State {
Ui::Animations::Simple animation;
float64 progress = -1.;
QImage frame;
bool maximized = false;
bool anyOver = false;
bool over = false;
};
const auto state = raw->lifetime().make_state<State>();
rpl::merge(
_data->masterOpacity.changes() | rpl::to_empty,
_data->maximized.changes() | rpl::to_empty
) | rpl::on_next([=] {
raw->update();
}, raw->lifetime());
_data->clearStateRequests.events(
) | rpl::on_next([=] {
raw->clearState();
raw->update();
state->over = raw->isOver();
_data->anyOver = false;
state->animation.stop();
}, raw->lifetime());
struct Info {
const style::icon *icon = nullptr;
style::margins padding;
};
const auto info = [&]() -> Info {
switch (control) {
case Control::Minimize:
return { &st::mediaviewTitleMinimizeMac, st::mediaviewTitleMinimizeMacPadding };
case Control::Maximize:
return { &st::mediaviewTitleMaximizeMac, st::mediaviewTitleMaximizeMacPadding };
case Control::Close:
return { &st::mediaviewTitleCloseMac, st::mediaviewTitleCloseMacPadding };
}
Unexpected("Value in DefaultOverlayWidgetHelper::Buttons::create.");
}();
const auto icon = info.icon;
raw->resize(QRect(QPoint(), icon->size()).marginsAdded(info.padding).size());
state->frame = QImage(
icon->size() * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
state->frame.setDevicePixelRatio(style::DevicePixelRatio());
const auto updateOver = [=] {
const auto over = raw->isOver();
if (state->over == over) {
return;
}
state->over = over;
const auto anyOver = over
|| _data->buttonClose->isOver()
|| _data->buttonMinimize->isOver()
|| _data->buttonMaximize->isOver();
if (_data->anyOver != anyOver) {
_data->anyOver = anyOver;
_data->buttonClose->update();
_data->buttonMinimize->update();
_data->buttonMaximize->update();
}
state->animation.start(
[=] { raw->update(); },
state->over ? 0. : 1.,
state->over ? 1. : 0.,
st::mediaviewFadeDuration);
};
const auto prepareFrame = [=] {
const auto progress = state->animation.value(state->over ? 1. : 0.);
const auto maximized = _data->maximized.current();
const auto anyOver = _data->anyOver;
if (state->progress == progress
&& state->maximized == maximized
&& state->anyOver == anyOver) {
return;
}
state->progress = progress;
state->maximized = maximized;
state->anyOver = anyOver;
auto current = icon;
if (control == Control::Maximize) {
current = maximized ? &st::mediaviewTitleRestoreMac : icon;
}
state->frame.fill(Qt::transparent);
auto q = QPainter(&state->frame);
const auto normal = maximized
? kMaximizedIconOpacity
: kNormalIconOpacity;
q.setOpacity(progress + (1 - progress) * normal);
st::mediaviewTitleButtonMac.paint(q, 0, 0, raw->width());
if (anyOver) {
q.setOpacity(1.);
current->paint(q, 0, 0, raw->width());
}
q.end();
};
raw->paintRequest(
) | rpl::on_next([=, padding = info.padding] {
updateOver();
prepareFrame();
auto p = QPainter(raw);
p.setOpacity(_data->masterOpacity.current());
p.drawImage(padding.left(), padding.top(), state->frame);
}, raw->lifetime());
return result;
}
std::unique_ptr<OverlayWidgetHelper> CreateOverlayWidgetHelper(
not_null<Ui::RpWindow*> window,
Fn<void(bool)> maximize) {
return std::make_unique<MacOverlayWidgetHelper>(
window,
std::move(maximize));
}
} // namespace Platform

View File

@@ -0,0 +1,70 @@
/*
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 "platform/platform_specific.h"
#include "platform/mac/specific_mac_p.h"
namespace Platform {
inline bool AutostartSupported() {
return false;
}
inline void AutostartRequestStateFromSystem(Fn<void(bool)> callback) {
}
inline bool TrayIconSupported() {
return true;
}
inline bool SkipTaskbarSupported() {
return false;
}
void ActivateThisProcess();
inline uint64 ActivationWindowId(not_null<QWidget*> window) {
return 1;
}
inline void ActivateOtherProcess(uint64 processId, uint64 windowId) {
}
inline QString ApplicationIconName() {
return {};
}
inline QString ExecutablePathForShortcuts() {
return cExeDir() + cExeName();
}
namespace ThirdParty {
inline void start() {
}
} // namespace ThirdParty
} // namespace Platform
inline void psCheckLocalSocket(const QString &serverName) {
QFile address(serverName);
if (address.exists()) {
address.remove();
}
}
QString psAppDataPath();
void psSendToMenu(bool send, bool silent = false);
int psCleanup();
int psFixPrevious();
void psDownloadPathEnableAccess();
QByteArray psDownloadPathBookmark(const QString &path);
QByteArray psPathBookmark(const QString &path);

View File

@@ -0,0 +1,302 @@
/*
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/specific_mac.h"
#include "lang/lang_keys.h"
#include "mainwidget.h"
#include "history/history_widget.h"
#include "core/crash_reports.h"
#include "core/sandbox.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "storage/localstorage.h"
#include "window/window_controller.h"
#include "mainwindow.h"
#include "history/history_location_manager.h"
#include "base/platform/mac/base_confirm_quit.h"
#include "base/platform/mac/base_utilities_mac.h"
#include "base/platform/base_platform_info.h"
#include "main/main_session.h"
#include "window/window_session_controller.h"
#include "base/options.h"
#include <QtGui/QDesktopServices>
#include <QtWidgets/QApplication>
#include <cstdlib>
#include <execinfo.h>
#include <sys/xattr.h>
#include <Cocoa/Cocoa.h>
#include <CoreFoundation/CFURL.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/hidsystem/ev_keymap.h>
#include <mach-o/dyld.h>
#include <AVFoundation/AVFoundation.h>
namespace {
[[nodiscard]] QImage ImageFromNS(NSImage *icon) {
CGImageRef image = [icon CGImageForProposedRect:NULL context:nil hints:nil];
const int width = CGImageGetWidth(image);
const int height = CGImageGetHeight(image);
auto result = QImage(width, height, QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::transparent);
CGColorSpaceRef space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
CGBitmapInfo info = CGBitmapInfo(kCGImageAlphaPremultipliedFirst) | kCGBitmapByteOrder32Host;
CGContextRef context = CGBitmapContextCreate(
result.bits(),
width,
height,
8,
result.bytesPerLine(),
space,
info);
CGRect rect = CGRectMake(0, 0, width, height);
CGContextDrawImage(context, rect, image);
CFRelease(space);
CFRelease(context);
return result;
}
[[nodiscard]] QImage ResolveBundleIconDefault() {
NSString *path = [[NSBundle mainBundle] bundlePath];
NSString *icon = [path stringByAppendingString:@"/Contents/Resources/Icon.icns"];
NSImage *image = [[NSImage alloc] initWithContentsOfFile:icon];
if (!image) {
return Window::Logo();
}
auto result = ImageFromNS(image);
[image release];
return result;
}
} // namespace
QString psAppDataPath() {
return objc_appDataPath();
}
void psDoCleanup() {
try {
Platform::AutostartToggle(false);
psSendToMenu(false, true);
} catch (...) {
}
}
int psCleanup() {
psDoCleanup();
return 0;
}
void psDoFixPrevious() {
}
int psFixPrevious() {
psDoFixPrevious();
return 0;
}
namespace Platform {
void start() {
objc_start();
}
void finish() {
objc_finish();
}
QString SingleInstanceLocalServerName(const QString &hash) {
#ifndef OS_MAC_STORE
return u"/tmp/"_q + hash + '-' + cGUIDStr();
#else // OS_MAC_STORE
return objc_documentsPath() + hash.left(4);
#endif // OS_MAC_STORE
}
#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
namespace {
QString strStyleOfInterface() {
const uint32 letters[] = { 0x3BBB7F05, 0xED4C5EC3, 0xC62C15A3, 0x5D10B283, 0x1BB35729, 0x63FB674D, 0xDBE5C174, 0x401EA195, 0x87B0C82A, 0x311BD596, 0x7063ECFA, 0x4AB90C27, 0xDA587DC4, 0x0B6296F8, 0xAA5603FA, 0xE1140A9F, 0x3D12D094, 0x339B5708, 0x712BA5B1 };
return Platform::MakeFromLetters(letters);
}
bool IsDarkMenuBar() {
bool result = false;
@autoreleasepool {
NSDictionary *dict = [[NSUserDefaults standardUserDefaults] persistentDomainForName:NSGlobalDomain];
id style = [dict objectForKey:Q2NSString(strStyleOfInterface())];
BOOL darkModeOn = (style && [style isKindOfClass:[NSString class]] && NSOrderedSame == [style caseInsensitiveCompare:@"dark"]);
result = darkModeOn ? true : false;
}
return result;
}
} // namespace
std::optional<bool> IsDarkMode() {
return IsMac10_14OrGreater()
? std::make_optional(IsDarkMenuBar())
: std::nullopt;
}
#endif // Qt < 6.5.0
void WriteCrashDumpDetails() {
#ifndef TDESKTOP_DISABLE_CRASH_REPORTS
double v = objc_appkitVersion();
CrashReports::dump() << "OS-Version: " << v;
#endif // TDESKTOP_DISABLE_CRASH_REPORTS
}
// I do check for availability, just not in the exact way clang is content with
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
PermissionStatus GetPermissionStatus(PermissionType type) {
switch (type) {
case PermissionType::Microphone:
case PermissionType::Camera:
const auto nativeType = (type == PermissionType::Microphone)
? AVMediaTypeAudio
: AVMediaTypeVideo;
if ([AVCaptureDevice respondsToSelector: @selector(authorizationStatusForMediaType:)]) { // Available starting with 10.14
switch ([AVCaptureDevice authorizationStatusForMediaType:nativeType]) {
case AVAuthorizationStatusNotDetermined:
return PermissionStatus::CanRequest;
case AVAuthorizationStatusAuthorized:
return PermissionStatus::Granted;
case AVAuthorizationStatusDenied:
case AVAuthorizationStatusRestricted:
return PermissionStatus::Denied;
}
}
break;
}
return PermissionStatus::Granted;
}
void RequestPermission(PermissionType type, Fn<void(PermissionStatus)> resultCallback) {
switch (type) {
case PermissionType::Microphone:
case PermissionType::Camera:
const auto nativeType = (type == PermissionType::Microphone)
? AVMediaTypeAudio
: AVMediaTypeVideo;
if ([AVCaptureDevice respondsToSelector: @selector(requestAccessForMediaType:completionHandler:)]) { // Available starting with 10.14
[AVCaptureDevice requestAccessForMediaType:nativeType completionHandler:^(BOOL granted) {
crl::on_main([=] {
resultCallback(granted ? PermissionStatus::Granted : PermissionStatus::Denied);
});
}];
}
break;
}
resultCallback(PermissionStatus::Granted);
}
#pragma clang diagnostic pop // -Wunguarded-availability
void OpenSystemSettingsForPermission(PermissionType type) {
switch (type) {
case PermissionType::Microphone:
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone"]];
break;
case PermissionType::Camera:
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"x-apple.systempreferences:com.apple.preference.security?Privacy_Camera"]];
break;
}
}
bool OpenSystemSettings(SystemSettingsType type) {
switch (type) {
case SystemSettingsType::Audio:
[[NSWorkspace sharedWorkspace] openFile:@"/System/Library/PreferencePanes/Sound.prefPane"];
break;
}
return true;
}
void IgnoreApplicationActivationRightNow() {
objc_ignoreApplicationActivationRightNow();
}
void AutostartToggle(bool enabled, Fn<void(bool)> done) {
if (done) {
done(false);
}
}
bool AutostartSkip() {
return !cAutoStart();
}
void NewVersionLaunched(int oldVersion) {
if (const auto window = Core::App().activeWindow()) {
if (const auto controller = window->sessionController()) {
const auto userId = controller->session().userId().bare;
const auto hash = std::hash<uint64>{}(userId);
if ((hash % 100) < 15 || (userId % 100) == 91) {
base::options::lookup<bool>("text-recognition-mac").set(true);
}
}
}
}
QImage DefaultApplicationIcon() {
static auto result = ResolveBundleIconDefault();
return result;
}
bool PreventsQuit(Core::QuitReason reason) {
// Thanks Chromium, see
// chromium.org/developers/design-documents/confirm-to-quit-experiment
return (reason == Core::QuitReason::QtQuitEvent)
&& Core::App().settings().macWarnBeforeQuit()
&& ([[NSApp currentEvent] type] == NSEventTypeKeyDown)
&& !ConfirmQuit::RunModal(
tr::lng_mac_hold_to_quit(
tr::now,
lt_text,
ConfirmQuit::QuitKeysString()));
}
void ActivateThisProcess() {
const auto window = Core::App().activeWindow();
objc_activateProgram(window ? window->widget()->winId() : 0);
}
void LaunchMaps(const Data::LocationPoint &point, Fn<void()> fail) {
if (!QDesktopServices::openUrl(
u"https://maps.apple.com/?q=Point&z=16&ll=%1,%2"_q.arg(
point.latAsString(),
point.lonAsString()))) {
fail();
}
}
} // namespace Platform
void psSendToMenu(bool send, bool silent) {
}
void psDownloadPathEnableAccess() {
objc_downloadPathEnableAccess(Core::App().settings().downloadPathBookmark());
}
QByteArray psDownloadPathBookmark(const QString &path) {
return objc_downloadPathBookmark(path);
}

View File

@@ -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
// e is NSEvent*
bool objc_handleMediaKeyEvent(void *e);
void objc_debugShowAlert(const QString &str);
void objc_outputDebugString(const QString &str);
void objc_start();
void objc_ignoreApplicationActivationRightNow();
void objc_finish();
void objc_activateProgram(WId winId);
bool objc_moveFile(const QString &from, const QString &to);
double objc_appkitVersion();
QString objc_documentsPath();
QString objc_appDataPath();
QByteArray objc_downloadPathBookmark(const QString &path);
void objc_downloadPathEnableAccess(const QByteArray &bookmark);

View File

@@ -0,0 +1,461 @@
/*
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/specific_mac_p.h"
#include "mainwindow.h"
#include "mainwidget.h"
#include "main/main_account.h"
#include "main/main_domain.h"
#include "main/main_session.h"
#include "data/data_user.h"
#include "calls/calls_instance.h"
#include "core/sandbox.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "core/crash_reports.h"
#include "storage/localstorage.h"
#include "media/audio/media_audio.h"
#include "media/player/media_player_instance.h"
#include "window/window_controller.h"
#include "base/platform/mac/base_utilities_mac.h"
#include "base/platform/base_platform_info.h"
#include "lang/lang_keys.h"
#include "base/timer.h"
#include "styles/style_window.h"
#include "platform/platform_specific.h"
#include <QtGui/QWindow>
#include <QtWidgets/QApplication>
#if __has_include(<QtCore/QOperatingSystemVersion>)
#include <QtCore/QOperatingSystemVersion>
#endif // __has_include(<QtCore/QOperatingSystemVersion>)
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
#include <qpa/qwindowsysteminterface.h>
#endif // Qt < 6.6.0
#include <Cocoa/Cocoa.h>
#include <CoreFoundation/CFURL.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/hidsystem/ev_keymap.h>
using Platform::Q2NSString;
using Platform::NS2QString;
namespace {
constexpr auto kIgnoreActivationTimeoutMs = 500;
NSMenuItem *CreateMenuItem(
QString title,
rpl::lifetime &lifetime,
Fn<void()> callback,
bool enabled = true) {
id block = [^{
Core::Sandbox::Instance().customEnterFromEventLoop(callback);
} copy];
NSMenuItem *item = [[NSMenuItem alloc]
initWithTitle:Q2NSString(title)
action:@selector(invoke)
keyEquivalent:@""];
[item setTarget:block];
[item setEnabled:enabled];
lifetime.add([=] {
[block release];
});
return [item autorelease];
}
} // namespace
@interface RpMenu : NSMenu {
}
- (rpl::lifetime &) lifetime;
@end // @interface Menu
@implementation RpMenu {
rpl::lifetime _lifetime;
}
- (rpl::lifetime &) lifetime {
return _lifetime;
}
@end // @implementation Menu
@interface qVisualize : NSObject {
}
+ (id)str:(const QString &)str;
- (id)initWithString:(const QString &)str;
+ (id)bytearr:(const QByteArray &)arr;
- (id)initWithByteArray:(const QByteArray &)arr;
- (id)debugQuickLookObject;
@end // @interface qVisualize
@implementation qVisualize {
NSString *value;
}
+ (id)bytearr:(const QByteArray &)arr {
return [[qVisualize alloc] initWithByteArray:arr];
}
- (id)initWithByteArray:(const QByteArray &)arr {
if (self = [super init]) {
value = [NSString stringWithUTF8String:arr.constData()];
}
return self;
}
+ (id)str:(const QString &)str {
return [[qVisualize alloc] initWithString:str];
}
- (id)initWithString:(const QString &)str {
if (self = [super init]) {
value = [NSString stringWithUTF8String:str.toUtf8().constData()];
}
return self;
}
- (id)debugQuickLookObject {
return value;
}
@end // @implementation qVisualize
@interface ApplicationDelegate : NSObject<NSApplicationDelegate> {
}
- (BOOL) applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)flag;
- (void) applicationDidBecomeActive:(NSNotification *)aNotification;
- (void) applicationDidResignActive:(NSNotification *)aNotification;
- (void) receiveWakeNote:(NSNotification*)note;
- (void) ignoreApplicationActivationRightNow;
- (NSMenu *) applicationDockMenu:(NSApplication *)sender;
@end // @interface ApplicationDelegate
ApplicationDelegate *_sharedDelegate = nil;
@implementation ApplicationDelegate {
bool _ignoreActivation;
base::Timer _ignoreActivationStop;
}
- (instancetype) init {
_ignoreActivation = false;
_ignoreActivationStop.setCallback([self] {
_ignoreActivation = false;
});
return [super init];
}
- (BOOL) applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)flag {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
if (Core::IsAppLaunched()) {
if (const auto window = Core::App().activeWindow()) {
if (window->widget()->isHidden()) {
window->widget()->showFromTray();
}
}
}
});
return YES;
}
- (void) applicationDidBecomeActive:(NSNotification *)aNotification {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
if (Core::IsAppLaunched() && !_ignoreActivation) {
Core::App().handleAppActivated();
if (const auto window = Core::App().activeWindow()) {
if (window->widget()->isHidden()) {
if (Core::App().calls().hasVisiblePanel()) {
Core::App().calls().activateCurrentCall();
} else {
window->widget()->showFromTray();
}
}
}
}
});
}
- (void) applicationDidResignActive:(NSNotification *)aNotification {
}
- (void) receiveWakeNote:(NSNotification*)aNotification {
if (!Core::IsAppLaunched()) {
return;
}
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
Core::App().checkLocalTime();
LOG(("Audio Info: "
"-receiveWakeNote: received, scheduling detach from audio device"));
Media::Audio::ScheduleDetachFromDeviceSafe();
#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
Core::App().settings().setSystemDarkMode(Platform::IsDarkMode());
#elif QT_VERSION < QT_VERSION_CHECK(6, 6, 0) // Qt < 6.5.0
QWindowSystemInterface::handleThemeChange();
#endif // Qt < 6.6.0
});
}
- (void) ignoreApplicationActivationRightNow {
_ignoreActivation = true;
_ignoreActivationStop.callOnce(kIgnoreActivationTimeoutMs);
}
- (NSMenu *) applicationDockMenu:(NSApplication *)sender {
if (!Core::IsAppLaunched()) {
return nil;
}
RpMenu* dockMenu = [[[RpMenu alloc] initWithTitle: @""] autorelease];
[dockMenu setAutoenablesItems:false];
const auto accounts = Core::App().domain().orderedAccounts();
if (accounts.size() > 1) {
[dockMenu addItem:[NSMenuItem separatorItem]];
NSMenuItem *profilesHeader = [[NSMenuItem alloc]
initWithTitle:@"" action:nil keyEquivalent:@""];
NSDictionary *attributes = @{
NSFontAttributeName: [NSFont
menuFontOfSize:[NSFont smallSystemFontSize]],
NSForegroundColorAttributeName: [NSColor secondaryLabelColor]
};
NSAttributedString *attrTitle = [[NSAttributedString alloc]
initWithString:Q2NSString(tr::lng_mac_menu_profiles(tr::now))
attributes:attributes];
[profilesHeader setAttributedTitle:attrTitle];
[attrTitle release];
[profilesHeader setEnabled:NO];
[dockMenu addItem:[profilesHeader autorelease]];
constexpr auto kMaxLength = 30;
for (const auto &account : accounts) {
if (account->sessionExists()) {
auto name = account->session().user()->name();
[dockMenu addItem:CreateMenuItem(
(name.size() > kMaxLength)
? (name.mid(0, kMaxLength) + Ui::kQEllipsis)
: name,
[dockMenu lifetime],
[account] {
Core::App().ensureSeparateWindowFor(account);
})];
}
}
[dockMenu addItem:[NSMenuItem separatorItem]];
}
auto notifyCallback = [] {
auto &settings = Core::App().settings();
settings.setDesktopNotify(!settings.desktopNotify());
};
[dockMenu addItem:CreateMenuItem(
Core::App().settings().desktopNotify()
? tr::lng_disable_notifications_from_tray(tr::now)
: tr::lng_enable_notifications_from_tray(tr::now),
[dockMenu lifetime],
std::move(notifyCallback))];
using namespace Media::Player;
const auto state = instance()->getState(instance()->getActiveType());
if (!IsStoppedOrStopping(state.state)) {
[dockMenu addItem:[NSMenuItem separatorItem]];
[dockMenu addItem:CreateMenuItem(
tr::lng_mac_menu_player_previous(tr::now),
[dockMenu lifetime],
[] { instance()->previous(); },
instance()->previousAvailable(instance()->getActiveType()))];
[dockMenu addItem:CreateMenuItem(
IsPausedOrPausing(state.state)
? tr::lng_mac_menu_player_resume(tr::now)
: tr::lng_mac_menu_player_pause(tr::now),
[dockMenu lifetime],
[] { instance()->playPause(); })];
[dockMenu addItem:CreateMenuItem(
tr::lng_mac_menu_player_next(tr::now),
[dockMenu lifetime],
[] { instance()->next(); },
instance()->nextAvailable(instance()->getActiveType()))];
}
return dockMenu;
}
@end // @implementation ApplicationDelegate
namespace Platform {
void SetApplicationIcon(const QIcon &icon) {
NSImage *image = nil;
if (!icon.isNull()) {
auto pixmap = icon.pixmap(1024, 1024);
pixmap.setDevicePixelRatio(style::DevicePixelRatio());
image = Q2NSImage(pixmap.toImage());
}
[[NSApplication sharedApplication] setApplicationIconImage:image];
}
} // namespace Platform
void objc_debugShowAlert(const QString &str) {
@autoreleasepool {
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = @"Debug Message";
alert.informativeText = Q2NSString(str);
[alert runModal];
}
}
void objc_outputDebugString(const QString &str) {
@autoreleasepool {
NSLog(@"%@", Q2NSString(str));
}
}
void objc_start() {
// Patch: Fix macOS regression. On 10.14.4, it crashes on GPU switches.
// See https://bugreports.qt.io/browse/QTCREATORBUG-22215
const auto version = QOperatingSystemVersion::current();
if (version.majorVersion() == 10
&& version.minorVersion() == 14
&& version.microVersion() == 4) {
qputenv("QT_MAC_PRO_WEBENGINE_WORKAROUND", "1");
}
_sharedDelegate = [[ApplicationDelegate alloc] init];
[[NSApplication sharedApplication] setDelegate:_sharedDelegate];
[[[NSWorkspace sharedWorkspace] notificationCenter]
addObserver: _sharedDelegate
selector: @selector(receiveWakeNote:)
name: NSWorkspaceDidWakeNotification object: NULL];
}
void objc_ignoreApplicationActivationRightNow() {
if (_sharedDelegate) {
[_sharedDelegate ignoreApplicationActivationRightNow];
}
}
namespace {
NSURL *_downloadPathUrl = nil;
}
void objc_finish() {
[_sharedDelegate release];
_sharedDelegate = nil;
if (_downloadPathUrl) {
[_downloadPathUrl stopAccessingSecurityScopedResource];
_downloadPathUrl = nil;
}
}
void objc_activateProgram(WId winId) {
[NSApp activateIgnoringOtherApps:YES];
if (winId) {
NSWindow *w = [reinterpret_cast<NSView*>(winId) window];
[w makeKeyAndOrderFront:NSApp];
}
}
bool objc_moveFile(const QString &from, const QString &to) {
@autoreleasepool {
NSString *f = Q2NSString(from), *t = Q2NSString(to);
if ([[NSFileManager defaultManager] fileExistsAtPath:t]) {
NSData *data = [NSData dataWithContentsOfFile:f];
if (data) {
if ([data writeToFile:t atomically:YES]) {
if ([[NSFileManager defaultManager] removeItemAtPath:f error:nil]) {
return true;
}
}
}
} else {
if ([[NSFileManager defaultManager] moveItemAtPath:f toPath:t error:nil]) {
return true;
}
}
}
return false;
}
double objc_appkitVersion() {
return NSAppKitVersionNumber;
}
QString objc_documentsPath() {
NSURL *url = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:nil];
if (url) {
return QString::fromUtf8([[url path] fileSystemRepresentation]) + '/';
}
return QString();
}
QString objc_appDataPath() {
NSURL *url = [[NSFileManager defaultManager] URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:nil];
if (url) {
return QString::fromUtf8([[url path] fileSystemRepresentation]) + '/' + AppName.utf16() + '/';
}
return QString();
}
QByteArray objc_downloadPathBookmark(const QString &path) {
#ifndef OS_MAC_STORE
return QByteArray();
#else // OS_MAC_STORE
NSURL *url = [NSURL fileURLWithPath:[NSString stringWithUTF8String:path.toUtf8().constData()] isDirectory:YES];
if (!url) return QByteArray();
NSError *error = nil;
NSData *data = [url bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&error];
return data ? QByteArray::fromNSData(data) : QByteArray();
#endif // OS_MAC_STORE
}
void objc_downloadPathEnableAccess(const QByteArray &bookmark) {
#ifdef OS_MAC_STORE
if (bookmark.isEmpty()) return;
BOOL isStale = NO;
NSError *error = nil;
NSURL *url = [NSURL URLByResolvingBookmarkData:bookmark.toNSData() options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&error];
if (!url) return;
if ([url startAccessingSecurityScopedResource]) {
if (_downloadPathUrl) {
[_downloadPathUrl stopAccessingSecurityScopedResource];
}
_downloadPathUrl = [url retain];
Core::App().settings().setDownloadPath(NS2QString([_downloadPathUrl path]) + '/');
if (isStale) {
NSData *data = [_downloadPathUrl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:&error];
if (data) {
Core::App().settings().setDownloadPathBookmark(QByteArray::fromNSData(data));
Local::writeSettings();
}
}
}
#endif // OS_MAC_STORE
}

View File

@@ -0,0 +1,8 @@
/*
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

View File

@@ -0,0 +1,103 @@
/*
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/platform_text_recognition.h"
#include "base/platform/mac/base_utilities_mac.h"
#include "base/options.h"
#import <Foundation/Foundation.h>
#import <Vision/Vision.h>
#import <CoreImage/CoreImage.h>
namespace Platform {
namespace TextRecognition {
namespace {
base::options::toggle TextRecognitionOption({
.id = "text-recognition-mac",
.name = "Text Recognition",
.description = "Enable text recognition from images on macOS 10.15+.",
.defaultValue = false,
.scope = base::options::macos,
});
} // namespace
bool IsAvailable() {
if (@available(macOS 10.15, *)) {
return TextRecognitionOption.value();
}
return false;
}
Result RecognizeText(const QImage &image) {
auto result = Result();
if (!IsAvailable()) {
return result;
}
@autoreleasepool {
CGImageRef cgImage = image.toCGImage();
if (!cgImage) {
return result;
}
CIImage *image = [CIImage imageWithCGImage:cgImage];
CFRelease(cgImage);
if (!image) {
return result;
}
if (@available(macOS 10.15, *)) {
VNRecognizeTextRequest *request
= [[VNRecognizeTextRequest alloc] init];
request.recognitionLevel = VNRequestTextRecognitionLevelAccurate;
VNImageRequestHandler *handler = [[VNImageRequestHandler alloc]
initWithCIImage:image options:@{}];
NSError *error = nil;
const auto success
= [handler performRequests:@[request] error:&error];
if (success && !error) {
const auto imageSize = image.extent.size;
for (VNRecognizedTextObservation *obs in request.results) {
VNRecognizedText *recognizedText = [obs
topCandidates:1].firstObject;
if (recognizedText) {
const auto text = recognizedText.string;
const auto boundingBox = obs.boundingBox;
const auto x = boundingBox.origin.x * imageSize.width;
const auto y = (1.0 - boundingBox.origin.y
- boundingBox.size.height) * imageSize.height;
const auto width = boundingBox.size.width
* imageSize.width;
const auto height = boundingBox.size.height
* imageSize.height;
result.items.push_back({
NS2QString(text),
QRect(x, y, width, height)
});
}
}
result.success = true;
}
[request release];
[handler release];
}
}
return result;
}
} // namespace TextRecognition
} // namespace Platform

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 &current = _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 &current = _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

View File

@@ -0,0 +1,21 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#import <AppKit/NSPopoverTouchBarItem.h>
#import <AppKit/NSTouchBar.h>
namespace Window {
class Controller;
} // namespace Window
API_AVAILABLE(macos(10.12.2))
@interface StickerEmojiPopover : NSPopoverTouchBarItem<NSTouchBarDelegate>
- (id)init:(not_null<Window::Controller*>)controller
identifier:(NSTouchBarItemIdentifier)identifier;
@end

View File

@@ -0,0 +1,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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,62 @@
/*
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 "platform/platform_tray.h"
#include "base/unique_qptr.h"
class QMenu;
namespace Platform {
class NativeIcon;
class Tray final {
public:
Tray();
~Tray();
[[nodiscard]] rpl::producer<> aboutToShowRequests() const;
[[nodiscard]] rpl::producer<> showFromTrayRequests() const;
[[nodiscard]] rpl::producer<> hideToTrayRequests() const;
[[nodiscard]] rpl::producer<> iconClicks() const;
[[nodiscard]] bool hasIcon() const;
void createIcon();
void destroyIcon();
void updateIcon();
void createMenu();
void destroyMenu();
void addAction(rpl::producer<QString> text, Fn<void()> &&callback);
void showTrayMessage() const;
[[nodiscard]] bool hasTrayMessageSupport() const;
[[nodiscard]] rpl::lifetime &lifetime();
private:
std::unique_ptr<NativeIcon> _nativeIcon;
base::unique_qptr<QMenu> _menu;
rpl::event_stream<> _showFromTrayRequests;
rpl::lifetime _actionsLifetime;
rpl::lifetime _lifetime;
};
inline bool HasMonochromeSetting() {
return false;
}
} // namespace Platform

View File

@@ -0,0 +1,422 @@
/*
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/tray_mac.h"
#include "base/platform/mac/base_utilities_mac.h"
#include "core/application.h"
#include "core/sandbox.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "ui/painter.h"
#include "styles/style_window.h"
#include <QtWidgets/QMenu>
#import <AppKit/NSMenu.h>
#import <AppKit/NSStatusItem.h>
@interface CommonDelegate : NSObject<NSMenuDelegate> {
}
- (void) menuDidClose:(NSMenu *)menu;
- (void) menuWillOpen:(NSMenu *)menu;
- (void) observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
context:(void *)context;
- (rpl::producer<>) closes;
- (rpl::producer<>) aboutToShowRequests;
- (rpl::producer<>) appearanceChanges;
@end // @interface CommonDelegate
@implementation CommonDelegate {
rpl::event_stream<> _closes;
rpl::event_stream<> _aboutToShowRequests;
rpl::event_stream<> _appearanceChanges;
}
- (void) menuDidClose:(NSMenu *)menu {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
_closes.fire({});
});
}
- (void) menuWillOpen:(NSMenu *)menu {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
_aboutToShowRequests.fire({});
});
}
// Thanks https://stackoverflow.com/a/64525038
- (void) observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
context:(void *)context {
if ([keyPath isEqualToString:@"button.effectiveAppearance"]) {
_appearanceChanges.fire({});
}
}
- (rpl::producer<>) closes {
return _closes.events();
}
- (rpl::producer<>) aboutToShowRequests {
return _aboutToShowRequests.events();
}
- (rpl::producer<>) appearanceChanges {
return _appearanceChanges.events();
}
@end // @implementation MenuDelegate
namespace Platform {
namespace {
[[nodiscard]] bool IsAnyActiveForTrayMenu() {
for (const NSWindow *w in [[NSApplication sharedApplication] windows]) {
if (w.isKeyWindow) {
return true;
}
}
return false;
}
[[nodiscard]] QImage TrayIconBack(bool darkMode) {
static const auto WithColor = [](QColor color) {
return st::macTrayIcon.instance(color, 100);
};
static const auto DarkModeResult = WithColor({ 255, 255, 255 });
static const auto LightModeResult = WithColor({ 0, 0, 0, 180 });
auto result = darkMode ? DarkModeResult : LightModeResult;
result.detach();
return result;
}
void PlaceCounter(
QImage &img,
int size,
int count,
style::color bg,
style::color color) {
if (!count) {
return;
}
const auto savedRatio = img.devicePixelRatio();
img.setDevicePixelRatio(1.);
{
Painter p(&img);
PainterHighQualityEnabler hq(p);
const auto cnt = (count < 100)
? QString("%1").arg(count)
: QString("..%1").arg(count % 100, 2, 10, QChar('0'));
const auto cntSize = cnt.size();
p.setBrush(bg);
p.setPen(Qt::NoPen);
int32 fontSize, skip;
if (size == 22) {
skip = 1;
fontSize = 8;
} else {
skip = 2;
fontSize = 16;
}
style::font f(fontSize, 0, 0);
int32 w = f->width(cnt), d, r;
if (size == 22) {
d = (cntSize < 2) ? 3 : 2;
r = (cntSize < 2) ? 6 : 5;
} else {
d = (cntSize < 2) ? 6 : 5;
r = (cntSize < 2) ? 9 : 11;
}
p.drawRoundedRect(
QRect(
size - w - d * 2 - skip,
size - f->height - skip,
w + d * 2,
f->height),
r,
r);
p.setCompositionMode(QPainter::CompositionMode_Source);
p.setFont(f);
p.setPen(color);
p.drawText(
size - w - d - skip,
size - f->height + f->ascent - skip,
cnt);
}
img.setDevicePixelRatio(savedRatio);
}
void UpdateIcon(const NSStatusItem *status) {
if (!status) {
return;
}
const auto appearance = status.button.effectiveAppearance;
const auto darkMode = [[appearance.name lowercaseString]
containsString:@"dark"];
// The recommended maximum title bar icon height is 18 points
// (device independent pixels). The menu height on past and
// current OS X versions is 22 points. Provide some future-proofing
// by deriving the icon height from the menu height.
const int padding = 0;
const int menuHeight = NSStatusBar.systemStatusBar.thickness;
// [[status.button window] backingScaleFactor];
const int maxImageHeight = (menuHeight - padding)
* style::DevicePixelRatio();
// Select pixmap based on the device pixel height. Ideally we would use
// the devicePixelRatio of the target screen, but that value is not
// known until draw time. Use qApp->devicePixelRatio, which returns the
// devicePixelRatio for the "best" screen on the system.
const auto side = 22 * style::DevicePixelRatio();
const auto selectedSize = QSize(side, side);
auto result = TrayIconBack(darkMode);
auto resultActive = result;
resultActive.detach();
const auto counter = Core::App().unreadBadge();
const auto muted = Core::App().unreadBadgeMuted();
const auto &bg = (muted ? st::trayCounterBgMute : st::trayCounterBg);
const auto &fg = st::trayCounterFg;
const auto &fgInvert = st::trayCounterFgMacInvert;
const auto &bgInvert = st::trayCounterBgMacInvert;
const auto &resultFg = !darkMode ? fg : muted ? fgInvert : fg;
PlaceCounter(result, side, counter, bg, resultFg);
PlaceCounter(resultActive, side, counter, bgInvert, fgInvert);
// Scale large pixmaps to fit the available menu bar area.
if (result.height() > maxImageHeight) {
result = result.scaledToHeight(
maxImageHeight,
Qt::SmoothTransformation);
}
if (resultActive.height() > maxImageHeight) {
resultActive = resultActive.scaledToHeight(
maxImageHeight,
Qt::SmoothTransformation);
}
status.button.image = Q2NSImage(result);
status.button.alternateImage = Q2NSImage(resultActive);
status.button.imageScaling = NSImageScaleProportionallyDown;
}
} // namespace
class NativeIcon final {
public:
NativeIcon();
~NativeIcon();
void updateIcon();
void showMenu(not_null<QMenu*> menu);
void deactivateButton();
[[nodiscard]] rpl::producer<> clicks() const;
[[nodiscard]] rpl::producer<> aboutToShowRequests() const;
private:
CommonDelegate *_delegate;
NSStatusItem *_status;
rpl::event_stream<> _clicks;
rpl::lifetime _lifetime;
};
NativeIcon::NativeIcon()
: _delegate([[CommonDelegate alloc] init])
, _status([
[NSStatusBar.systemStatusBar
statusItemWithLength:NSSquareStatusItemLength] retain]) {
[_status
addObserver:_delegate
forKeyPath:@"button.effectiveAppearance"
options:0
| NSKeyValueObservingOptionNew
| NSKeyValueObservingOptionInitial
context:nil];
[_delegate closes] | rpl::on_next([=] {
_status.menu = nil;
}, _lifetime);
[_delegate appearanceChanges] | rpl::on_next([=] {
updateIcon();
}, _lifetime);
const auto masks = NSEventMaskLeftMouseDown
| NSEventMaskLeftMouseUp
| NSEventMaskRightMouseDown
| NSEventMaskRightMouseUp
| NSEventMaskOtherMouseUp;
[_status.button sendActionOn:masks];
id buttonCallback = [^{
const auto type = NSApp.currentEvent.type;
if ((type == NSEventTypeLeftMouseDown)
|| (type == NSEventTypeRightMouseDown)) {
Core::Sandbox::Instance().customEnterFromEventLoop([=] {
_clicks.fire({});
});
}
} copy];
_lifetime.add([=] {
[buttonCallback release];
});
_status.button.target = buttonCallback;
_status.button.action = @selector(invoke);
_status.button.toolTip = Q2NSString(AppName.utf16());
}
NativeIcon::~NativeIcon() {
[_status
removeObserver:_delegate
forKeyPath:@"button.effectiveAppearance"];
[NSStatusBar.systemStatusBar removeStatusItem:_status];
[_status release];
[_delegate release];
}
void NativeIcon::updateIcon() {
UpdateIcon(_status);
}
void NativeIcon::showMenu(not_null<QMenu*> menu) {
_status.menu = menu->toNSMenu();
_status.menu.delegate = _delegate;
[_status.button performClick:nil];
}
void NativeIcon::deactivateButton() {
[_status.button highlight:false];
}
rpl::producer<> NativeIcon::clicks() const {
return _clicks.events();
}
rpl::producer<> NativeIcon::aboutToShowRequests() const {
return [_delegate aboutToShowRequests];
}
Tray::Tray() {
}
void Tray::createIcon() {
if (!_nativeIcon) {
_nativeIcon = std::make_unique<NativeIcon>();
// On macOS we are activating the window on click
// instead of showing the menu, when the window is not activated.
_nativeIcon->clicks(
) | rpl::on_next([=] {
if (IsAnyActiveForTrayMenu()) {
_nativeIcon->showMenu(_menu.get());
} else {
_nativeIcon->deactivateButton();
_showFromTrayRequests.fire({});
}
}, _lifetime);
}
updateIcon();
}
void Tray::destroyIcon() {
_nativeIcon = nullptr;
}
void Tray::updateIcon() {
if (_nativeIcon) {
_nativeIcon->updateIcon();
}
}
void Tray::createMenu() {
if (!_menu) {
_menu = base::make_unique_q<QMenu>(nullptr);
}
}
void Tray::destroyMenu() {
if (_menu) {
_menu->clear();
}
_actionsLifetime.destroy();
}
void Tray::addAction(rpl::producer<QString> text, Fn<void()> &&callback) {
if (!_menu) {
return;
}
const auto action = _menu->addAction(QString(), std::move(callback));
std::move(
text
) | rpl::on_next([=](const QString &text) {
action->setText(text);
}, _actionsLifetime);
}
void Tray::showTrayMessage() const {
}
bool Tray::hasTrayMessageSupport() const {
return false;
}
rpl::producer<> Tray::aboutToShowRequests() const {
return _nativeIcon
? _nativeIcon->aboutToShowRequests()
: rpl::never<>();
}
rpl::producer<> Tray::showFromTrayRequests() const {
return _showFromTrayRequests.events();
}
rpl::producer<> Tray::hideToTrayRequests() const {
return rpl::never<>();
}
rpl::producer<> Tray::iconClicks() const {
return rpl::never<>();
}
bool Tray::hasIcon() const {
return _nativeIcon != nullptr;
}
rpl::lifetime &Tray::lifetime() {
return _lifetime;
}
Tray::~Tray() = default;
} // namespace Platform

View File

@@ -0,0 +1,297 @@
/*
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/platform_webauthn.h"
#if 0
#include "data/data_passkey_deserialize.h"
#include "base/options.h"
#import <AuthenticationServices/AuthenticationServices.h>
#import <Foundation/Foundation.h>
@interface WebAuthnDelegate : NSObject<
ASAuthorizationControllerDelegate,
ASAuthorizationControllerPresentationContextProviding>
@property (nonatomic, copy) void (^completionRegister)(
const Platform::WebAuthn::RegisterResult&);
@property (nonatomic, copy) void (^completionLogin)(
const Platform::WebAuthn::LoginResult&);
@property (nonatomic, strong) ASAuthorizationController *controller API_AVAILABLE(macos(10.15));
@property (nonatomic, strong)
ASAuthorizationPlatformPublicKeyCredentialProvider *provider API_AVAILABLE(macos(12.0));
@end
@implementation WebAuthnDelegate
- (void)authorizationController:(ASAuthorizationController *)controller
didCompleteWithAuthorization:(ASAuthorization *)authorization
API_AVAILABLE(macos(12.0)) {
if (self.completionRegister) {
if ([authorization.credential conformsToProtocol:
@protocol(ASAuthorizationPublicKeyCredentialRegistration)]) {
auto credential
= (id<ASAuthorizationPublicKeyCredentialRegistration>)
authorization.credential;
auto result = Platform::WebAuthn::RegisterResult();
result.success = true;
result.credentialId = QByteArray::fromNSData(
credential.credentialID);
result.attestationObject = QByteArray::fromNSData(
credential.rawAttestationObject);
result.clientDataJSON = QByteArray::fromNSData(
credential.rawClientDataJSON);
self.completionRegister(result);
self.completionRegister = nil;
}
self.controller = nil;
if (@available(macOS 12.0, *)) {
self.provider = nil;
}
} else if (self.completionLogin) {
if ([authorization.credential conformsToProtocol:
@protocol(ASAuthorizationPublicKeyCredentialAssertion)]) {
auto credential
= (id<ASAuthorizationPublicKeyCredentialAssertion>)
authorization.credential;
auto result = Platform::WebAuthn::LoginResult();
result.credentialId = QByteArray::fromNSData(
credential.credentialID);
result.authenticatorData = QByteArray::fromNSData(
credential.rawAuthenticatorData);
result.signature = QByteArray::fromNSData(
credential.signature);
result.clientDataJSON = QByteArray::fromNSData(
credential.rawClientDataJSON);
result.userHandle = QByteArray::fromNSData(credential.userID);
self.completionLogin(result);
self.completionLogin = nil;
}
self.controller = nil;
if (@available(macOS 12.0, *)) {
self.provider = nil;
}
}
}
- (void)authorizationController:(ASAuthorizationController *)controller
didCompleteWithError:(NSError *)error
API_AVAILABLE(macos(10.15)) {
const auto isCancelled = (error.code == ASAuthorizationErrorCanceled);
const auto isUnsigned = (error.code == 1004 || error.code == 1009);
if (!isCancelled) {
NSLog(@"WebAuthn error: %@ (code: %ld)",
error.localizedDescription, (long)error.code);
}
if (self.completionRegister) {
auto result = Platform::WebAuthn::RegisterResult();
result.success = false;
result.error = isUnsigned
? Platform::WebAuthn::Error::UnsignedBuild
: (isCancelled
? Platform::WebAuthn::Error::Cancelled
: Platform::WebAuthn::Error::Other);
self.completionRegister(result);
self.completionRegister = nil;
} else if (self.completionLogin) {
auto result = Platform::WebAuthn::LoginResult();
result.error = isUnsigned
? Platform::WebAuthn::Error::UnsignedBuild
: (isCancelled
? Platform::WebAuthn::Error::Cancelled
: Platform::WebAuthn::Error::Other);
self.completionLogin(result);
self.completionLogin = nil;
}
self.controller = nil;
if (@available(macOS 12.0, *)) {
self.provider = nil;
}
}
- (ASPresentationAnchor)presentationAnchorForAuthorizationController:
(ASAuthorizationController *)controller
API_AVAILABLE(macos(10.15)) {
return [NSApp mainWindow];
}
@end
namespace {
base::options::toggle WebAuthnMacOption({
.id = "webauthn-mac",
.name = "Enable Passkey on macOS",
.description = "Enable Passkey support on macOS 12.0+. Experimental feature that may cause crash.",
.defaultValue = false,
.scope = base::options::macos,
});
} // namespace
namespace Platform::WebAuthn {
bool IsSupported() {
if (!WebAuthnMacOption.value()) {
return false;
}
if (@available(macOS 12.0, *)) {
return true;
}
return false;
}
void RegisterKey(
const Data::Passkey::RegisterData &data,
Fn<void(RegisterResult result)> callback) {
if (@available(macOS 12.0, *)) {
auto rpId = data.rp.id.toNSString();
auto userName = data.user.name.toNSString();
auto userDisplayName = data.user.displayName.toNSString();
auto userId = [NSData dataWithBytes:data.user.id.constData()
length:data.user.id.size()];
auto challenge = [NSData dataWithBytes:data.challenge.constData()
length:data.challenge.size()];
if (!userId || !challenge) {
auto result = RegisterResult();
result.success = false;
callback(result);
return;
}
auto provider = [[ASAuthorizationPlatformPublicKeyCredentialProvider
alloc] initWithRelyingPartyIdentifier:rpId];
auto request = [provider
createCredentialRegistrationRequestWithChallenge:challenge
name:userName
userID:userId];
if (userDisplayName && userDisplayName.length > 0) {
request.displayName = userDisplayName;
}
if (@available(macOS 13.5, *)) {
request.attestationPreference =
ASAuthorizationPublicKeyCredentialAttestationKindNone;
}
auto controller = [[ASAuthorizationController alloc]
initWithAuthorizationRequests:@[request]];
auto delegate = [[WebAuthnDelegate alloc] init];
if (@available(macOS 12.0, *)) {
delegate.provider = provider;
}
delegate.controller = controller;
delegate.completionRegister = ^(const RegisterResult &result) {
callback(result);
[delegate release];
};
controller.delegate = delegate;
controller.presentationContextProvider = delegate;
[controller performRequests];
} else {
auto result = RegisterResult();
result.success = false;
callback(result);
}
}
void Login(
const Data::Passkey::LoginData &data,
Fn<void(LoginResult result)> callback) {
if (@available(macOS 12.0, *)) {
auto challenge = [NSData dataWithBytes:data.challenge.constData()
length:data.challenge.size()];
if (!challenge) {
auto result = LoginResult();
callback(result);
return;
}
auto provider = [[ASAuthorizationPlatformPublicKeyCredentialProvider
alloc] initWithRelyingPartyIdentifier:data.rpId.toNSString()];
auto request = [provider
createCredentialAssertionRequestWithChallenge:challenge];
if (!data.allowCredentials.empty()) {
NSMutableArray *credentialIds = [NSMutableArray array];
for (const auto &cred : data.allowCredentials) {
auto credId = [NSData dataWithBytes:cred.id.constData()
length:cred.id.size()];
if (credId) {
[credentialIds addObject:credId];
}
}
if (credentialIds.count > 0) {
request.allowedCredentials = credentialIds;
}
}
if (data.userVerification == "required") {
request.userVerificationPreference =
ASAuthorizationPublicKeyCredentialUserVerificationPreferenceRequired;
} else if (data.userVerification == "preferred") {
request.userVerificationPreference =
ASAuthorizationPublicKeyCredentialUserVerificationPreferencePreferred;
} else if (data.userVerification == "discouraged") {
request.userVerificationPreference =
ASAuthorizationPublicKeyCredentialUserVerificationPreferenceDiscouraged;
}
auto controller = [[ASAuthorizationController alloc]
initWithAuthorizationRequests:@[request]];
auto delegate = [[WebAuthnDelegate alloc] init];
if (@available(macOS 12.0, *)) {
delegate.provider = provider;
}
delegate.controller = controller;
delegate.completionLogin = ^(const LoginResult &result) {
callback(result);
[delegate release];
};
controller.delegate = delegate;
controller.presentationContextProvider = delegate;
[controller performRequests];
} else {
auto result = LoginResult();
callback(result);
}
}
} // namespace Platform::WebAuthn
#endif
namespace Platform::WebAuthn {
bool IsSupported() {
return false;
}
void RegisterKey(
const Data::Passkey::RegisterData &data,
Fn<void(RegisterResult result)> callback) {
}
void Login(
const Data::Passkey::LoginData &data,
Fn<void(LoginResult result)> callback) {
}
} // namespace Platform::WebAuthn

View File

@@ -0,0 +1,179 @@
/*
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/platform_window_title.h"
#include "ui/image/image_prepare.h"
#include "ui/painter.h"
#include "core/application.h"
#include "styles/style_window.h"
#include "styles/style_media_view.h"
#include "window/window_controller.h"
#include <Cocoa/Cocoa.h>
namespace Platform {
// All the window decorations preview is done without taking cScale() into
// account, with 100% scale and without "px" dimensions, because thats
// how it will look in real launched macOS app.
int PreviewTitleHeight() {
if (const auto window = Core::App().activePrimaryWindow()) {
if (const auto height = window->widget()->getCustomTitleHeight()) {
return height;
}
}
return 22;
}
QImage PreviewWindowSystemButton(QColor inner, QColor border) {
auto buttonSize = 12;
auto fullSize = buttonSize * style::DevicePixelRatio();
auto result = QImage(fullSize, fullSize, QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::transparent);
{
Painter p(&result);
PainterHighQualityEnabler hq(p);
p.setPen(border);
p.setBrush(inner);
p.drawEllipse(QRectF(0.5, 0.5, fullSize - 1., fullSize - 1.));
}
result.setDevicePixelRatio(style::DevicePixelRatio());
return result;
}
void PreviewWindowTitle(Painter &p, const style::palette &palette, QRect body, int titleHeight, int outerWidth) {
auto titleRect = QRect(body.x(), body.y() - titleHeight, body.width(), titleHeight);
p.fillRect(titleRect, QColor(0, 0, 0));
p.fillRect(titleRect, st::titleBgActive[palette]);
p.fillRect(titleRect.x(), titleRect.y() + titleRect.height() - st::lineWidth, titleRect.width(), st::lineWidth, st::titleShadow[palette]);
QFont font;
const auto families = QStringList{
u".AppleSystemUIFont"_q,
u".SF NS Text"_q,
u"Helvetica Neue"_q,
};
for (auto family : families) {
font.setFamily(family);
if (QFontInfo(font).family() == font.family()) {
break;
}
}
if (QFontInfo(font).family() != font.family()) {
font = st::semiboldFont;
font.setPixelSize(13);
} else if (font.family() == u".AppleSystemUIFont"_q) {
font.setBold(true);
font.setPixelSize(13);
} else {
font.setPixelSize((titleHeight * 15) / 24);
}
p.setPen(st::titleFgActive[palette]);
p.setFont(font);
p.drawText(titleRect, u"Telegram"_q, style::al_center);
auto isGraphite = ([NSColor currentControlTint] == NSGraphiteControlTint);
auto buttonSkip = 8;
auto graphiteInner = QColor(141, 141, 146);
auto graphiteBorder = QColor(104, 104, 109);
auto closeInner = isGraphite ? graphiteInner : QColor(252, 96, 92);
auto closeBorder = isGraphite ? graphiteBorder : QColor(222, 64, 59);
auto minimizeInner = isGraphite ? graphiteInner : QColor(254, 192, 65);
auto minimizeBorder = isGraphite ? graphiteBorder : QColor(221, 152, 25);
auto maximizeInner = isGraphite ? graphiteInner : QColor(52, 200, 74);
auto maximizeBorder = isGraphite ? graphiteBorder : QColor(21, 164, 41);
auto close = PreviewWindowSystemButton(closeInner, closeBorder);
auto left = buttonSkip;
p.drawImage(
titleRect.x() + left,
titleRect.y()
+ (titleRect.height()
- (close.height() / style::DevicePixelRatio())) / 2,
close);
left += (close.width() / style::DevicePixelRatio()) + buttonSkip;
auto minimize = PreviewWindowSystemButton(minimizeInner, minimizeBorder);
p.drawImage(
titleRect.x() + left,
titleRect.y()
+ (titleRect.height()
- (minimize.height() / style::DevicePixelRatio())) / 2,
minimize);
left += (minimize.width() / style::DevicePixelRatio()) + buttonSkip;
auto maximize = PreviewWindowSystemButton(maximizeInner, maximizeBorder);
p.drawImage(
titleRect.x() + left,
titleRect.y()
+ (titleRect.height()
- (maximize.height() / style::DevicePixelRatio())) / 2,
maximize);
}
void PreviewWindowFramePaint(QImage &preview, const style::palette &palette, QRect body, int outerWidth) {
auto retina = style::DevicePixelRatio();
auto titleHeight = PreviewTitleHeight();
{
Painter p(&preview);
PreviewWindowTitle(p, palette, body, titleHeight, outerWidth);
}
auto inner = QRect(body.x(), body.y() - titleHeight, body.width(), body.height() + titleHeight);
auto retinaRadius = st::macWindowRoundRadius * retina;
auto roundMask = QImage(2 * retinaRadius, 2 * retinaRadius, QImage::Format_ARGB32_Premultiplied);
roundMask.setDevicePixelRatio(style::DevicePixelRatio());
roundMask.fill(Qt::transparent);
{
Painter p(&roundMask);
PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen);
p.setBrush(QColor(255, 255, 255));
p.drawRoundedRect(0, 0, 2 * st::macWindowRoundRadius, 2 * st::macWindowRoundRadius, st::macWindowRoundRadius, st::macWindowRoundRadius);
}
QImage corners[4];
corners[0] = roundMask.copy(0, 0, retinaRadius, retinaRadius);
corners[1] = roundMask.copy(retinaRadius, 0, retinaRadius, retinaRadius);
corners[2] = roundMask.copy(0, retinaRadius, retinaRadius, retinaRadius);
corners[3] = roundMask.copy(retinaRadius, retinaRadius, retinaRadius, retinaRadius);
auto rounded = Images::Round(
preview.copy(
inner.x() * retina,
inner.y() * retina,
inner.width() * retina,
inner.height() * retina),
corners);
rounded.setDevicePixelRatio(style::DevicePixelRatio());
preview.fill(st::themePreviewBg->c);
auto topLeft = st::macWindowShadowTopLeft.instance(QColor(0, 0, 0), 100);
auto topRight = topLeft.mirrored(true, false);
auto bottomLeft = topLeft.mirrored(false, true);
auto bottomRight = bottomLeft.mirrored(true, false);
auto extend = QMargins(37, 28, 37, 28);
auto left = topLeft.copy(0, topLeft.height() - retina, extend.left() * retina, retina);
auto top = topLeft.copy(topLeft.width() - retina, 0, retina, extend.top() * retina);
auto right = topRight.copy(topRight.width() - (extend.right() * retina), topRight.height() - retina, extend.right() * retina, retina);
auto bottom = bottomRight.copy(0, bottomRight.height() - (extend.bottom() * retina), retina, extend.bottom() * retina);
{
Painter p(&preview);
p.drawImage(inner.x() - extend.left(), inner.y() - extend.top(), topLeft);
p.drawImage(inner.x() + inner.width() + extend.right() - (topRight.width() / retina), inner.y() - extend.top(), topRight);
p.drawImage(inner.x() - extend.left(), inner.y() + inner.height() + extend.bottom() - (bottomLeft.height() / retina), bottomLeft);
p.drawImage(inner.x() + inner.width() + extend.right() - (bottomRight.width() / retina), inner.y() + inner.height() + extend.bottom() - (bottomRight.height() / retina), bottomRight);
p.drawImage(QRect(inner.x() - extend.left(), inner.y() - extend.top() + (topLeft.height() / retina), extend.left(), extend.top() + inner.height() + extend.bottom() - (topLeft.height() / retina) - (bottomLeft.height() / retina)), left);
p.drawImage(QRect(inner.x() - extend.left() + (topLeft.width() / retina), inner.y() - extend.top(), extend.left() + inner.width() + extend.right() - (topLeft.width() / retina) - (topRight.width() / retina), extend.top()), top);
p.drawImage(QRect(inner.x() + inner.width(), inner.y() - extend.top() + (topRight.height() / retina), extend.right(), extend.top() + inner.height() + extend.bottom() - (topRight.height() / retina) - (bottomRight.height() / retina)), right);
p.drawImage(QRect(inner.x() - extend.left() + (bottomLeft.width() / retina), inner.y() + inner.height(), extend.left() + inner.width() + extend.right() - (bottomLeft.width() / retina) - (bottomRight.width() / retina), extend.bottom()), bottom);
p.drawImage(inner.x(), inner.y(), rounded);
}
}
} // namespace Platform

View File

@@ -0,0 +1,23 @@
/*
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 Core {
struct GeoLocation;
struct GeoAddress;
} // namespace Core
namespace Platform {
void ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback);
void ResolveLocationAddress(
const Core::GeoLocation &location,
const QString &language,
Fn<void(Core::GeoAddress)> callback);
} // namespace Platform

View File

@@ -0,0 +1,43 @@
/*
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
#ifdef Q_OS_MAC
#include "platform/mac/file_bookmark_mac.h"
#else // Q_OS_MAC
namespace Platform {
class FileBookmark {
public:
FileBookmark(const QByteArray &bookmark) {
}
bool check() const {
return true;
}
bool enable() const {
return true;
}
void disable() const {
}
const QString &name(const QString &original) const {
return original;
}
QByteArray bookmark() const {
return QByteArray();
}
};
[[nodiscard]] inline QByteArray PathBookmark(const QString &path) {
return QByteArray();
}
} // namespace Platform
#endif // Q_OS_MAC

View File

@@ -0,0 +1,52 @@
/*
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 "core/file_utilities.h"
namespace Platform {
namespace File {
QString UrlToLocal(const QUrl &url);
// All these functions may enter a nested event loop. Use with caution.
void UnsafeOpenUrl(const QString &url);
void UnsafeOpenEmailLink(const QString &email);
bool UnsafeShowOpenWithDropdown(const QString &filepath);
bool UnsafeShowOpenWith(const QString &filepath);
void UnsafeLaunch(const QString &filepath);
void PostprocessDownloaded(const QString &filepath);
} // namespace File
namespace FileDialog {
void InitLastPath();
bool Get(
QPointer<QWidget> parent,
QStringList &files,
QByteArray &remoteContent,
const QString &caption,
const QString &filter,
::FileDialog::internal::Type type,
QString startFile = QString());
} // namespace FileDialog
} // namespace Platform
// Platform dependent implementations.
#if defined Q_OS_WINRT || defined Q_OS_WIN
#include "platform/win/file_utilities_win.h"
#elif defined Q_OS_MAC // Q_OS_WINRT || Q_OS_WIN
#include "platform/mac/file_utilities_mac.h"
#else // Q_OS_WINRT || Q_OS_WIN || Q_OS_MAC
#include "platform/linux/file_utilities_linux.h"
#endif // else for Q_OS_WINRT || Q_OS_WIN || Q_OS_MAC

View File

@@ -0,0 +1,43 @@
/*
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/platform_integration.h"
#if defined Q_OS_WINRT || defined Q_OS_WIN
#include "platform/win/integration_win.h"
#elif defined Q_OS_MAC // Q_OS_WINRT || Q_OS_WIN
#include "platform/mac/integration_mac.h"
#else // Q_OS_WINRT || Q_OS_WIN || Q_OS_MAC
#include "platform/linux/integration_linux.h"
#endif // else Q_OS_WINRT || Q_OS_WIN || Q_OS_MAC
namespace Platform {
namespace {
Integration *GlobalInstance/* = nullptr*/;
} // namespace
Integration::~Integration() {
GlobalInstance = nullptr;
}
std::unique_ptr<Integration> Integration::Create() {
Expects(GlobalInstance == nullptr);
auto result = CreateIntegration();
GlobalInstance = result.get();
return result;
}
Integration &Integration::Instance() {
Expects(GlobalInstance != nullptr);
return *GlobalInstance;
};
} // namespace Platform

View File

@@ -0,0 +1,23 @@
/*
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 Platform {
class Integration {
public:
virtual void init() {
}
virtual ~Integration();
[[nodiscard]] static std::unique_ptr<Integration> Create();
[[nodiscard]] static Integration &Instance();
};
} // namespace Platform

View File

@@ -0,0 +1,30 @@
/*
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 Platform {
//class Launcher : public Core::Launcher {
//public:
// Launcher(int argc, char *argv[]);
//
// ...
//
//};
} // namespace Platform
// Platform dependent implementations.
#if defined Q_OS_WINRT || defined Q_OS_WIN
#include "platform/win/launcher_win.h"
#elif defined Q_OS_MAC // Q_OS_WINRT || Q_OS_WIN
#include "platform/mac/launcher_mac.h"
#else // Q_OS_WINRT || Q_OS_WIN || Q_OS_MAC
#include "platform/linux/launcher_linux.h"
#endif // else for Q_OS_WINRT || Q_OS_WIN || Q_OS_MAC

View File

@@ -0,0 +1,26 @@
/*
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 "window/main_window.h"
namespace Platform {
class MainWindow;
} // namespace Platform
// Platform dependent implementations.
#ifdef Q_OS_WIN
#include "platform/win/main_window_win.h"
#elif defined Q_OS_MAC // Q_OS_WIN
#include "platform/mac/main_window_mac.h"
#else // Q_OS_WIN || Q_OS_MAC
#include "platform/linux/main_window_linux.h"
#endif // else Q_OS_WIN || Q_OS_MAC

View File

@@ -0,0 +1,37 @@
/*
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 "window/notifications_manager.h"
namespace Platform {
namespace Notifications {
[[nodiscard]] bool SkipToastForCustom();
void MaybePlaySoundForCustom(Fn<void()> playSound);
void MaybeFlashBounceForCustom(Fn<void()> flashBounce);
[[nodiscard]] bool WaitForInputForCustom();
[[nodiscard]] bool Supported();
[[nodiscard]] bool Enforced();
[[nodiscard]] bool ByDefault();
[[nodiscard]] bool VolumeSupported();
void Create(Window::Notifications::System *system);
} // namespace Notifications
} // namespace Platform
// Platform dependent implementations.
#ifdef Q_OS_WIN
#include "platform/win/notifications_manager_win.h"
#elif defined Q_OS_MAC // Q_OS_MAC
#include "platform/mac/notifications_manager_mac.h"
#else // Q_OS_WIN || Q_OS_MAC
#include "platform/linux/notifications_manager_linux.h"
#endif // else for Q_OS_WIN || Q_OS_MAC

View File

@@ -0,0 +1,259 @@
/*
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/platform_overlay_widget.h"
#include "ui/effects/animations.h"
#include "ui/platform/ui_platform_window_title.h"
#include "ui/widgets/rp_window.h"
#include "ui/abstract_button.h"
#include "styles/style_media_view.h"
namespace Media::View {
QColor OverBackgroundColor() {
auto c1 = st::mediaviewBg->c;
auto c2 = QColor(255, 255, 255);
const auto mix = [&](int a, int b) {
constexpr auto k1 = 0.15 * 0.85 / (1. - 0.85 * 0.85);
constexpr auto k2 = 0.15 / (1. - 0.85 * 0.85);
return int(a * k1 + b * k2);
};
return QColor(
mix(c1.red(), c2.red()),
mix(c1.green(), c2.green()),
mix(c1.blue(), c2.blue()));
}
} // namespace Media::View
namespace Platform {
namespace {
using namespace Media::View;
} // namespace
class DefaultOverlayWidgetHelper::Buttons final
: public Ui::Platform::AbstractTitleButtons {
public:
using Control = Ui::Platform::TitleControl;
object_ptr<Ui::AbstractButton> create(
not_null<QWidget*> parent,
Control control,
const style::WindowTitle &st) override;
void updateState(
bool active,
bool maximized,
const style::WindowTitle &st) override;
void notifySynteticOver(Control control, bool over) override;
void setMasterOpacity(float64 opacity);
[[nodiscard]] rpl::producer<> activations() const;
void clearState();
private:
rpl::event_stream<> _activations;
rpl::variable<float64> _masterOpacity = 1.;
rpl::variable<bool> _maximized = false;
rpl::event_stream<> _clearStateRequests;
};
object_ptr<Ui::AbstractButton> DefaultOverlayWidgetHelper::Buttons::create(
not_null<QWidget*> parent,
Control control,
const style::WindowTitle &st) {
auto result = object_ptr<Ui::AbstractButton>(parent);
const auto raw = result.data();
struct State {
Ui::Animations::Simple animation;
float64 progress = -1.;
QImage frame;
bool maximized = false;
bool over = false;
};
const auto state = raw->lifetime().make_state<State>();
rpl::merge(
_masterOpacity.changes() | rpl::to_empty,
_maximized.changes() | rpl::to_empty
) | rpl::on_next([=] {
raw->update();
}, raw->lifetime());
_clearStateRequests.events(
) | rpl::on_next([=] {
raw->clearState();
raw->update();
state->over = raw->isOver();
state->animation.stop();
}, raw->lifetime());
const auto icon = [&] {
switch (control) {
case Control::Minimize: return &st::mediaviewTitleMinimize;
case Control::Maximize: return &st::mediaviewTitleMaximize;
case Control::Close: return &st::mediaviewTitleClose;
}
Unexpected("Value in DefaultOverlayWidgetHelper::Buttons::create.");
}();
raw->resize(icon->size());
state->frame = QImage(
icon->size() * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
state->frame.setDevicePixelRatio(style::DevicePixelRatio());
const auto updateOver = [=] {
const auto over = raw->isOver();
if (state->over == over) {
return;
}
state->over = over;
state->animation.start(
[=] { raw->update(); },
state->over ? 0. : 1.,
state->over ? 1. : 0.,
st::mediaviewFadeDuration);
};
const auto prepareFrame = [=] {
const auto progress = state->animation.value(state->over ? 1. : 0.);
const auto maximized = _maximized.current();
if (state->progress == progress && state->maximized == maximized) {
return;
}
state->progress = progress;
state->maximized = maximized;
auto current = icon;
if (control == Control::Maximize) {
current = maximized ? &st::mediaviewTitleRestore : icon;
}
const auto alpha = progress * kOverBackgroundOpacity;
auto color = OverBackgroundColor();
color.setAlpha(anim::interpolate(0, 255, alpha));
state->frame.fill(color);
auto q = QPainter(&state->frame);
const auto normal = maximized
? kMaximizedIconOpacity
: kNormalIconOpacity;
q.setOpacity(progress + (1 - progress) * normal);
current->paint(q, 0, 0, raw->width());
q.end();
};
raw->paintRequest(
) | rpl::on_next([=] {
updateOver();
prepareFrame();
auto p = QPainter(raw);
p.setOpacity(_masterOpacity.current());
p.drawImage(0, 0, state->frame);
}, raw->lifetime());
return result;
}
void DefaultOverlayWidgetHelper::Buttons::updateState(
bool active,
bool maximized,
const style::WindowTitle &st) {
_maximized = maximized;
}
void DefaultOverlayWidgetHelper::Buttons::notifySynteticOver(
Ui::Platform::TitleControl control,
bool over) {
if (over) {
_activations.fire({});
}
}
void DefaultOverlayWidgetHelper::Buttons::clearState() {
_clearStateRequests.fire({});
}
void DefaultOverlayWidgetHelper::Buttons::setMasterOpacity(float64 opacity) {
_masterOpacity = opacity;
}
rpl::producer<> DefaultOverlayWidgetHelper::Buttons::activations() const {
return _activations.events();
}
void OverlayWidgetHelper::minimize(not_null<Ui::RpWindow*> window) {
window->setWindowState(window->windowState() | Qt::WindowMinimized);
}
DefaultOverlayWidgetHelper::DefaultOverlayWidgetHelper(
not_null<Ui::RpWindow*> window,
Fn<void(bool)> maximize)
: _buttons(new DefaultOverlayWidgetHelper::Buttons())
, _controls(Ui::Platform::SetupSeparateTitleControls(
window,
std::make_unique<Ui::Platform::SeparateTitleControls>(
window->body(),
st::mediaviewTitle,
std::unique_ptr<DefaultOverlayWidgetHelper::Buttons>(_buttons.get()),
std::move(maximize)))) {
}
DefaultOverlayWidgetHelper::~DefaultOverlayWidgetHelper() = default;
void DefaultOverlayWidgetHelper::orderWidgets() {
_controls->wrap.raise();
}
bool DefaultOverlayWidgetHelper::skipTitleHitTest(QPoint position) {
using namespace Ui::Platform;
return _controls->controls.hitTest(position) != HitTestResult::None;
}
rpl::producer<> DefaultOverlayWidgetHelper::controlsActivations() {
return _buttons->activations();
}
rpl::producer<bool> DefaultOverlayWidgetHelper::controlsSideRightValue() {
return _controls->controls.layout().value(
) | rpl::map([=](const auto &layout) {
return !layout.onLeft();
}) | rpl::distinct_until_changed();
}
void DefaultOverlayWidgetHelper::beforeShow(bool fullscreen) {
_buttons->clearState();
}
void DefaultOverlayWidgetHelper::clearState() {
_buttons->clearState();
}
void DefaultOverlayWidgetHelper::setControlsOpacity(float64 opacity) {
_buttons->setMasterOpacity(opacity);
}
auto DefaultOverlayWidgetHelper::mouseEvents() const
-> rpl::producer<not_null<QMouseEvent*>> {
return _controls->wrap.events(
) | rpl::filter([](not_null<QEvent*> e) {
const auto type = e->type();
return (type == QEvent::MouseButtonPress)
|| (type == QEvent::MouseButtonRelease)
|| (type == QEvent::MouseMove)
|| (type == QEvent::MouseButtonDblClick);
}) | rpl::map([](not_null<QEvent*> e) {
return not_null{ static_cast<QMouseEvent*>(e.get()) };
});
}
} // namespace Platform

View File

@@ -0,0 +1,104 @@
/*
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 Ui {
class RpWindow;
} // namespace Ui
namespace Ui::Platform {
struct SeparateTitleControls;
} // namespace Ui::Platform
namespace Media::View {
inline constexpr auto kMaximizedIconOpacity = 0.6;
inline constexpr auto kNormalIconOpacity = 0.9;
inline constexpr auto kOverBackgroundOpacity = 0.2775;
inline constexpr auto kStoriesNavOpacity = 0.3;
inline constexpr auto kStoriesNavOverOpacity = 0.7;
[[nodiscard]] QColor OverBackgroundColor();
} // namespace Media::View
namespace Platform {
class OverlayWidgetHelper {
public:
virtual ~OverlayWidgetHelper() = default;
virtual void orderWidgets() {
}
[[nodiscard]] virtual bool skipTitleHitTest(QPoint position) {
return false;
}
[[nodiscard]] virtual rpl::producer<> controlsActivations() {
return rpl::never<>();
}
[[nodiscard]] virtual rpl::producer<bool> controlsSideRightValue() {
return rpl::single(true);
}
virtual void beforeShow(bool fullscreen) {
}
virtual void afterShow(bool fullscreen) {
}
virtual void notifyFileDialogShown(bool shown) {
}
virtual void minimize(not_null<Ui::RpWindow*> window);
virtual void clearState() {
}
virtual void setControlsOpacity(float64 opacity) {
}
[[nodiscard]] virtual auto mouseEvents() const
-> rpl::producer<not_null<QMouseEvent*>> {
return rpl::never<not_null<QMouseEvent*>>();
}
[[nodiscard]] virtual rpl::producer<int> topNotchSkipValue() {
return rpl::single(0);
}
};
[[nodiscard]] std::unique_ptr<OverlayWidgetHelper> CreateOverlayWidgetHelper(
not_null<Ui::RpWindow*> window,
Fn<void(bool)> maximize);
class DefaultOverlayWidgetHelper final : public OverlayWidgetHelper {
public:
DefaultOverlayWidgetHelper(
not_null<Ui::RpWindow*> window,
Fn<void(bool)> maximize);
~DefaultOverlayWidgetHelper();
void orderWidgets() override;
bool skipTitleHitTest(QPoint position) override;
rpl::producer<> controlsActivations() override;
void beforeShow(bool fullscreen) override;
void clearState() override;
void setControlsOpacity(float64 opacity) override;
rpl::producer<bool> controlsSideRightValue() override;
rpl::producer<not_null<QMouseEvent*>> mouseEvents() const override;
private:
class Buttons;
const not_null<Buttons*> _buttons;
const std::unique_ptr<Ui::Platform::SeparateTitleControls> _controls;
};
} // namespace Platform
// Platform dependent implementations.
#ifdef Q_OS_WIN
#include "platform/win/overlay_widget_win.h"
#elif defined Q_OS_MAC // Q_OS_WIN
#include "platform/mac/overlay_widget_mac.h"
#else // Q_OS_WIN || Q_OS_MAC
#include "platform/linux/overlay_widget_linux.h"
#endif // else for Q_OS_WIN || Q_OS_MAC

View File

@@ -0,0 +1,77 @@
/*
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 Core {
enum class QuitReason;
} // namespace Core
namespace Data {
class LocationPoint;
} // namespace Data
namespace Platform {
void start();
void finish();
enum class PermissionStatus {
Granted,
CanRequest,
Denied,
};
enum class PermissionType {
Microphone,
Camera,
};
enum class SystemSettingsType {
Audio,
};
void SetApplicationIcon(const QIcon &icon);
[[nodiscard]] QString SingleInstanceLocalServerName(const QString &hash);
[[nodiscard]] PermissionStatus GetPermissionStatus(PermissionType type);
void RequestPermission(PermissionType type, Fn<void(PermissionStatus)> resultCallback);
void OpenSystemSettingsForPermission(PermissionType type);
bool OpenSystemSettings(SystemSettingsType type);
void IgnoreApplicationActivationRightNow();
[[nodiscard]] bool AutostartSupported();
void AutostartRequestStateFromSystem(Fn<void(bool)> callback);
void AutostartToggle(bool enabled, Fn<void(bool)> done = nullptr);
[[nodiscard]] bool AutostartSkip();
[[nodiscard]] bool TrayIconSupported();
[[nodiscard]] bool SkipTaskbarSupported();
void WriteCrashDumpDetails();
void NewVersionLaunched(int oldVersion);
[[nodiscard]] QImage DefaultApplicationIcon();
[[nodiscard]] QString ApplicationIconName();
[[nodiscard]] bool PreventsQuit(Core::QuitReason reason);
[[nodiscard]] QString ExecutablePathForShortcuts();
void LaunchMaps(const Data::LocationPoint &point, Fn<void()> fail);
#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
[[nodiscard]] std::optional<bool> IsDarkMode();
#endif // Qt < 6.5.0
namespace ThirdParty {
void start();
void finish();
} // namespace ThirdParty
} // namespace Platform
#ifdef Q_OS_WIN
#include "platform/win/specific_win.h"
#elif defined Q_OS_MAC // Q_OS_WIN
#include "platform/mac/specific_mac.h"
#else // Q_OS_WIN || Q_OS_MAC
#include "platform/linux/specific_linux.h"
#endif // else for Q_OS_WIN || Q_OS_MAC

View File

@@ -0,0 +1,41 @@
/*
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 Platform {
namespace TextRecognition {
struct RectWithText {
QString text;
QRect rect;
};
struct Result {
std::vector<RectWithText> items;
bool success = false;
inline operator bool() const {
return success;
}
};
[[nodiscard]] bool IsAvailable();
[[nodiscard]] Result RecognizeText(const QImage &image);
} // namespace TextRecognition
} // namespace Platform
// Platform dependent implementations.
#if defined Q_OS_WINRT || defined Q_OS_WIN
#include "platform/win/text_recognition_win.h"
#elif defined Q_OS_MAC // Q_OS_WINRT || Q_OS_WIN
#include "platform/mac/text_recognition_mac.h"
#else // Q_OS_WINRT || Q_OS_WIN || Q_OS_MAC
#include "platform/linux/text_recognition_linux.h"
#endif // else for Q_OS_WINRT || Q_OS_WIN || Q_OS_MAC

View File

@@ -0,0 +1,26 @@
/*
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 Platform {
class Tray;
[[nodiscard]] bool HasMonochromeSetting();
} // namespace Platform
// Platform dependent implementations.
#ifdef Q_OS_WIN
#include "platform/win/tray_win.h"
#elif defined Q_OS_MAC // Q_OS_WIN
#include "platform/mac/tray_mac.h"
#else // Q_OS_WIN || Q_OS_MAC
#include "platform/linux/tray_linux.h"
#endif // else for Q_OS_WIN || Q_OS_MAC

View File

@@ -0,0 +1,50 @@
/*
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 Data::Passkey {
struct RegisterData;
struct LoginData;
} // namespace Data::Passkey
namespace Platform::WebAuthn {
enum class Error {
None,
Cancelled,
UnsignedBuild,
Other,
};
struct LoginResult {
QByteArray clientDataJSON;
QByteArray credentialId;
QByteArray authenticatorData;
QByteArray signature;
QByteArray userHandle;
Error error = Error::None;
};
struct RegisterResult {
QByteArray credentialId;
QByteArray attestationObject;
QByteArray clientDataJSON;
bool success = false;
Error error = Error::None;
};
[[nodiscard]] bool IsSupported();
void RegisterKey(
const Data::Passkey::RegisterData &data,
Fn<void(RegisterResult result)> callback);
void Login(
const Data::Passkey::LoginData &data,
Fn<void(LoginResult result)> callback);
} // namespace Platform::WebAuthn

View File

@@ -0,0 +1,40 @@
/*
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 "window/themes/window_theme_preview.h"
#include "base/platform/base_platform_info.h"
namespace Platform {
inline bool NativeTitleRequiresShadow() {
return Platform::IsWindows();
}
int PreviewTitleHeight();
void PreviewWindowFramePaint(QImage &preview, const style::palette &palette, QRect body, int outerWidth);
} // namespace Platform
// Platform dependent implementations.
#ifndef Q_OS_MAC
namespace Platform {
inline int PreviewTitleHeight() {
return Window::Theme::DefaultPreviewTitleHeight();
}
inline void PreviewWindowFramePaint(QImage &preview, const style::palette &palette, QRect body, int outerWidth) {
return Window::Theme::DefaultPreviewWindowFramePaint(preview, palette, body, outerWidth);
}
} // namespace Platform
#endif // !Q_OS_MAC

View File

@@ -0,0 +1,69 @@
/*
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/win/current_geo_location_win.h"
#include "base/platform/win/base_windows_winrt.h"
#include "core/current_geo_location.h"
#include <winrt/Windows.Devices.Geolocation.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Services.Maps.h>
#include <winrt/Windows.Foundation.Collections.h>
namespace Platform {
void ResolveCurrentExactLocation(Fn<void(Core::GeoLocation)> callback) {
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Devices::Geolocation;
const auto success = base::WinRT::Try([&] {
Geolocator geolocator;
geolocator.DesiredAccuracy(PositionAccuracy::High);
if (geolocator.LocationStatus() == PositionStatus::NotAvailable) {
callback({});
return;
}
geolocator.GetGeopositionAsync().Completed([=](
IAsyncOperation<Geoposition> that,
AsyncStatus status) {
if (status != AsyncStatus::Completed) {
crl::on_main([=] {
callback({});
});
return;
}
const auto point = base::WinRT::Try([&] {
const auto coordinate = that.GetResults().Coordinate();
return coordinate.Point().Position();
});
crl::on_main([=] {
if (!point) {
callback({});
} else {
callback({
.point = { point->Latitude, point->Longitude },
.accuracy = Core::GeoLocationAccuracy::Exact,
});
}
});
});
});
if (!success) {
callback({});
}
}
void ResolveLocationAddress(
const Core::GeoLocation &location,
const QString &language,
Fn<void(Core::GeoAddress)> callback) {
callback({});
}
} // namespace Platform

View File

@@ -0,0 +1,10 @@
/*
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 "platform/platform_current_geo_location.h"

View File

@@ -0,0 +1,473 @@
/*
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/win/file_utilities_win.h"
#include "mainwindow.h"
#include "storage/localstorage.h"
#include "platform/win/windows_dlls.h"
#include "lang/lang_keys.h"
#include "core/application.h"
#include "core/crash_reports.h"
#include "window/window_controller.h"
#include "ui/ui_utility.h"
#include <QtWidgets/QFileDialog>
#include <QtGui/QDesktopServices>
#include <QtCore/QSettings>
#include <QtCore/QStandardPaths>
#include <Shlwapi.h>
#include <Windowsx.h>
HBITMAP qt_pixmapToWinHBITMAP(const QPixmap &, int hbitmapFormat);
namespace Platform {
namespace File {
namespace {
class OpenWithApp {
public:
OpenWithApp(const QString &name, IAssocHandler *handler, HBITMAP icon = nullptr)
: _name(name)
, _handler(handler)
, _icon(icon) {
}
OpenWithApp(OpenWithApp &&other)
: _name(base::take(other._name))
, _handler(base::take(other._handler))
, _icon(base::take(other._icon)) {
}
OpenWithApp &operator=(OpenWithApp &&other) {
_name = base::take(other._name);
_icon = base::take(other._icon);
_handler = base::take(other._handler);
return (*this);
}
OpenWithApp(const OpenWithApp &other) = delete;
OpenWithApp &operator=(const OpenWithApp &other) = delete;
~OpenWithApp() {
if (_icon) {
DeleteBitmap(_icon);
}
if (_handler) {
_handler->Release();
}
}
const QString &name() const {
return _name;
}
HBITMAP icon() const {
return _icon;
}
IAssocHandler *handler() const {
return _handler;
}
private:
QString _name;
IAssocHandler *_handler = nullptr;
HBITMAP _icon = nullptr;
};
HBITMAP IconToBitmap(LPWSTR icon, int iconindex) {
if (!icon) return 0;
WCHAR tmpIcon[4096];
if (icon[0] == L'@' && SUCCEEDED(SHLoadIndirectString(icon, tmpIcon, 4096, 0))) {
icon = tmpIcon;
}
int32 w = GetSystemMetrics(SM_CXSMICON), h = GetSystemMetrics(SM_CYSMICON);
HICON ico = ExtractIcon(0, icon, iconindex);
if (!ico) {
if (!iconindex) { // try to read image
QImage img(QString::fromWCharArray(icon));
if (!img.isNull()) {
return qt_pixmapToWinHBITMAP(
Ui::PixmapFromImage(
img.scaled(
w,
h,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation)),
/* HBitmapAlpha */ 2);
}
}
return 0;
}
HDC screenDC = GetDC(0), hdc = CreateCompatibleDC(screenDC);
HBITMAP result = CreateCompatibleBitmap(screenDC, w, h);
HGDIOBJ was = SelectObject(hdc, result);
DrawIconEx(hdc, 0, 0, ico, w, h, 0, NULL, DI_NORMAL);
SelectObject(hdc, was);
DeleteDC(hdc);
ReleaseDC(0, screenDC);
DestroyIcon(ico);
return (HBITMAP)CopyImage(result, IMAGE_BITMAP, 0, 0, LR_DEFAULTSIZE | LR_CREATEDIBSECTION);
}
bool ShouldSaveZoneInformation() {
// Check if the "Do not preserve zone information in file attachments" policy is enabled.
const auto keyName = L"Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\Attachments";
const auto valueName = L"SaveZoneInformation";
auto key = HKEY();
auto result = RegOpenKeyEx(HKEY_CURRENT_USER, keyName, 0, KEY_READ, &key);
if (result != ERROR_SUCCESS) {
// If the registry key cannot be opened, assume the default behavior:
// Windows preserves zone information for downloaded files.
return true;
}
DWORD value = 0, type = 0, size = sizeof(value);
result = RegQueryValueEx(key, valueName, 0, &type, (LPBYTE)&value, &size);
RegCloseKey(key);
if (result != ERROR_SUCCESS || type != REG_DWORD) {
return true;
}
return (value != 1);
}
} // namespace
void UnsafeOpenEmailLink(const QString &email) {
auto url = QUrl(qstr("mailto:") + email);
if (!QDesktopServices::openUrl(url)) {
auto wstringUrl = url.toString(QUrl::FullyEncoded).toStdWString();
if (Dlls::SHOpenWithDialog) {
OPENASINFO info;
info.oaifInFlags = OAIF_ALLOW_REGISTRATION
| OAIF_REGISTER_EXT
| OAIF_EXEC
#if WINVER >= 0x0602
| OAIF_FILE_IS_URI
#endif // WINVER >= 0x602
| OAIF_URL_PROTOCOL;
info.pcszClass = NULL;
info.pcszFile = wstringUrl.c_str();
Dlls::SHOpenWithDialog(0, &info);
} else if (Dlls::OpenAs_RunDLL) {
Dlls::OpenAs_RunDLL(0, 0, wstringUrl.c_str(), SW_SHOWNORMAL);
} else {
ShellExecute(0, L"open", wstringUrl.c_str(), 0, 0, SW_SHOWNORMAL);
}
}
}
bool UnsafeShowOpenWithDropdown(const QString &filepath) {
if (!Dlls::SHAssocEnumHandlers || !Dlls::SHCreateItemFromParsingName) {
return false;
}
auto window = Core::App().activeWindow();
if (!window) {
return false;
}
auto parentHWND = window->widget()->psHwnd();
auto wstringPath = QDir::toNativeSeparators(filepath).toStdWString();
auto result = false;
std::vector<OpenWithApp> handlers;
IShellItem* pItem = nullptr;
if (SUCCEEDED(Dlls::SHCreateItemFromParsingName(wstringPath.c_str(), nullptr, IID_PPV_ARGS(&pItem)))) {
IEnumAssocHandlers *assocHandlers = nullptr;
if (SUCCEEDED(pItem->BindToHandler(nullptr, BHID_EnumAssocHandlers, IID_PPV_ARGS(&assocHandlers)))) {
HRESULT hr = S_FALSE;
do {
IAssocHandler *handler = nullptr;
ULONG ulFetched = 0;
hr = assocHandlers->Next(1, &handler, &ulFetched);
if (FAILED(hr) || hr == S_FALSE || !ulFetched) break;
LPWSTR name = 0;
if (SUCCEEDED(handler->GetUIName(&name))) {
LPWSTR icon = 0;
int iconindex = 0;
if (SUCCEEDED(handler->GetIconLocation(&icon, &iconindex)) && icon) {
handlers.push_back(OpenWithApp(QString::fromWCharArray(name), handler, IconToBitmap(icon, iconindex)));
CoTaskMemFree(icon);
} else {
handlers.push_back(OpenWithApp(QString::fromWCharArray(name), handler));
}
CoTaskMemFree(name);
} else {
handler->Release();
}
} while (hr != S_FALSE);
assocHandlers->Release();
}
if (!handlers.empty()) {
HMENU menu = CreatePopupMenu();
ranges::sort(handlers, [](const OpenWithApp &a, auto &b) {
return a.name() < b.name();
});
for (int32 i = 0, l = handlers.size(); i < l; ++i) {
MENUITEMINFO menuInfo = { 0 };
menuInfo.cbSize = sizeof(menuInfo);
menuInfo.fMask = MIIM_STRING | MIIM_DATA | MIIM_ID;
menuInfo.fType = MFT_STRING;
menuInfo.wID = i + 1;
if (auto icon = handlers[i].icon()) {
menuInfo.fMask |= MIIM_BITMAP;
menuInfo.hbmpItem = icon;
}
auto name = handlers[i].name();
if (name.size() > 512) name = name.mid(0, 512);
WCHAR nameArr[1024];
name.toWCharArray(nameArr);
nameArr[name.size()] = 0;
menuInfo.dwTypeData = nameArr;
InsertMenuItem(menu, GetMenuItemCount(menu), TRUE, &menuInfo);
}
MENUITEMINFO sepInfo = { 0 };
sepInfo.cbSize = sizeof(sepInfo);
sepInfo.fMask = MIIM_STRING | MIIM_DATA;
sepInfo.fType = MFT_SEPARATOR;
InsertMenuItem(menu, GetMenuItemCount(menu), true, &sepInfo);
MENUITEMINFO menuInfo = { 0 };
menuInfo.cbSize = sizeof(menuInfo);
menuInfo.fMask = MIIM_STRING | MIIM_DATA | MIIM_ID;
menuInfo.fType = MFT_STRING;
menuInfo.wID = handlers.size() + 1;
QString name = tr::lng_wnd_choose_program_menu(tr::now);
if (name.size() > 512) name = name.mid(0, 512);
WCHAR nameArr[1024];
name.toWCharArray(nameArr);
nameArr[name.size()] = 0;
menuInfo.dwTypeData = nameArr;
InsertMenuItem(menu, GetMenuItemCount(menu), TRUE, &menuInfo);
POINT position;
GetCursorPos(&position);
int sel = TrackPopupMenu(menu, TPM_LEFTALIGN | TPM_TOPALIGN | TPM_LEFTBUTTON | TPM_RETURNCMD, position.x, position.y, 0, parentHWND, 0);
DestroyMenu(menu);
if (sel > 0) {
if (sel <= handlers.size()) {
IDataObject *dataObj = 0;
if (SUCCEEDED(pItem->BindToHandler(nullptr, BHID_DataObject, IID_PPV_ARGS(&dataObj))) && dataObj) {
handlers[sel - 1].handler()->Invoke(dataObj);
dataObj->Release();
result = true;
}
}
} else {
result = true;
}
}
pItem->Release();
}
return result;
}
bool UnsafeShowOpenWith(const QString &filepath) {
auto wstringPath = QDir::toNativeSeparators(filepath).toStdWString();
if (Dlls::SHOpenWithDialog) {
OPENASINFO info;
info.oaifInFlags = OAIF_ALLOW_REGISTRATION | OAIF_REGISTER_EXT | OAIF_EXEC;
info.pcszClass = NULL;
info.pcszFile = wstringPath.c_str();
Dlls::SHOpenWithDialog(0, &info);
return true;
} else if (Dlls::OpenAs_RunDLL) {
Dlls::OpenAs_RunDLL(0, 0, wstringPath.c_str(), SW_SHOWNORMAL);
return true;
}
return false;
}
void UnsafeLaunch(const QString &filepath) {
auto wstringPath = QDir::toNativeSeparators(filepath).toStdWString();
ShellExecute(0, L"open", wstringPath.c_str(), 0, 0, SW_SHOWNORMAL);
}
void PostprocessDownloaded(const QString &filepath) {
// Mark file saved to the NTFS file system as originating from the Internet security zone
// unless this feature is disabled by Group Policy.
if (!ShouldSaveZoneInformation()) {
return;
}
auto wstringZoneFile = QDir::toNativeSeparators(filepath).toStdWString() + L":Zone.Identifier";
auto f = CreateFile(wstringZoneFile.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
if (f == INVALID_HANDLE_VALUE) { // :(
return;
}
const char data[] = "[ZoneTransfer]\r\nZoneId=3\r\n";
DWORD written = 0;
BOOL result = WriteFile(f, data, sizeof(data), &written, NULL);
CloseHandle(f);
if (!result || written != sizeof(data)) { // :(
return;
}
}
} // namespace File
namespace FileDialog {
namespace {
using Type = ::FileDialog::internal::Type;
} // namespace
void InitLastPath() {
// hack to restore previous dir without hurting performance
QSettings settings(QSettings::UserScope, qstr("QtProject"));
settings.beginGroup(qstr("Qt"));
QByteArray sd = settings.value(qstr("filedialog")).toByteArray();
QDataStream stream(&sd, QIODevice::ReadOnly);
if (!stream.atEnd()) {
int version = 3, _QFileDialogMagic = 190;
QByteArray splitterState;
QByteArray headerData;
QList<QUrl> bookmarks;
QStringList history;
QString currentDirectory;
qint32 marker;
qint32 v;
qint32 viewMode;
stream >> marker;
stream >> v;
if (marker == _QFileDialogMagic && v == version) {
stream >> splitterState
>> bookmarks
>> history
>> currentDirectory
>> headerData
>> viewMode;
cSetDialogLastPath(currentDirectory);
}
}
if (cDialogHelperPath().isEmpty()) {
QDir temppath(cWorkingDir() + "tdata/tdummy/");
if (!temppath.exists()) {
temppath.mkpath(temppath.absolutePath());
}
if (temppath.exists()) {
cSetDialogHelperPath(temppath.absolutePath());
}
}
}
bool Get(
QPointer<QWidget> parent,
QStringList &files,
QByteArray &remoteContent,
const QString &caption,
const QString &filter,
::FileDialog::internal::Type type,
QString startFile) {
if (cDialogLastPath().isEmpty()) {
Platform::FileDialog::InitLastPath();
}
// A hack for fast dialog create. There was some huge performance problem
// if we open a file dialog in some folder with a large amount of files.
// Some internal Qt watcher iterated over all of them, querying some information
// that forced file icon and maybe other properties being resolved and this was
// a blocking operation.
auto helperPath = cDialogHelperPathFinal();
QFileDialog dialog(parent, caption, helperPath, filter);
dialog.setModal(true);
if (type == Type::ReadFile || type == Type::ReadFiles) {
dialog.setFileMode((type == Type::ReadFiles) ? QFileDialog::ExistingFiles : QFileDialog::ExistingFile);
dialog.setAcceptMode(QFileDialog::AcceptOpen);
} else if (type == Type::ReadFolder) { // save dir
dialog.setAcceptMode(QFileDialog::AcceptOpen);
dialog.setFileMode(QFileDialog::Directory);
dialog.setOption(QFileDialog::ShowDirsOnly);
} else { // save file
dialog.setFileMode(QFileDialog::AnyFile);
dialog.setAcceptMode(QFileDialog::AcceptSave);
}
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
dialog.show();
#endif // Qt < 6.0.0
auto realLastPath = [=] {
// If we're given some non empty path containing a folder - use it.
if (!startFile.isEmpty() && (startFile.indexOf('/') >= 0 || startFile.indexOf('\\') >= 0)) {
return QFileInfo(startFile).dir().absolutePath();
}
return cDialogLastPath();
}();
if (realLastPath.isEmpty() || realLastPath.endsWith(qstr("/tdummy"))) {
realLastPath = QStandardPaths::writableLocation(
QStandardPaths::DownloadLocation);
}
dialog.setDirectory(realLastPath);
auto toSelect = startFile;
if (type == Type::WriteFile) {
const auto lastSlash = toSelect.lastIndexOf('/');
if (lastSlash >= 0) {
toSelect = toSelect.mid(lastSlash + 1);
}
const auto lastBackSlash = toSelect.lastIndexOf('\\');
if (lastBackSlash >= 0) {
toSelect = toSelect.mid(lastBackSlash + 1);
}
dialog.selectFile(toSelect);
}
CrashReports::SetAnnotation(
"file_dialog",
QString("caption:%1;helper:%2;filter:%3;real:%4;select:%5"
).arg(caption
).arg(helperPath
).arg(filter
).arg(realLastPath
).arg(toSelect));
const auto result = dialog.exec();
CrashReports::ClearAnnotation("file_dialog");
if (type != Type::ReadFolder) {
// Save last used directory for all queries except directory choosing.
const auto path = dialog.directory().absolutePath();
if (path != cDialogLastPath()) {
cSetDialogLastPath(path);
Local::writeSettings();
}
}
if (result == QDialog::Accepted) {
if (type == Type::ReadFiles) {
files = dialog.selectedFiles();
} else {
files = dialog.selectedFiles().mid(0, 1);
}
//if (type == Type::ReadFile || type == Type::ReadFiles) {
// remoteContent = dialog.selectedRemoteContent();
//}
return true;
}
files = QStringList();
remoteContent = QByteArray();
return false;
}
} // namespace FileDialog
} // namespace Platform

View File

@@ -0,0 +1,24 @@
/*
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 "platform/platform_file_utilities.h"
namespace Platform {
namespace File {
inline QString UrlToLocal(const QUrl &url) {
return ::File::internal::UrlToLocalDefault(url);
}
inline void UnsafeOpenUrl(const QString &url) {
return ::File::internal::UnsafeOpenUrlDefault(url);
}
} // namespace File
} // namespace Platform

View File

@@ -0,0 +1,189 @@
/*
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/win/integration_win.h"
#include "base/platform/win/base_windows_winrt.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "core/sandbox.h"
#include "lang/lang_keys.h"
#include "platform/win/windows_app_user_model_id.h"
#include "platform/win/tray_win.h"
#include "platform/platform_integration.h"
#include "platform/platform_specific.h"
#include "tray.h"
#include "styles/style_window.h"
#include <QtCore/QAbstractNativeEventFilter>
#include <private/qguiapplication_p.h>
#include <propvarutil.h>
#include <propkey.h>
namespace Platform {
void WindowsIntegration::init() {
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
using namespace QNativeInterface::Private;
const auto native = qApp->nativeInterface<QWindowsApplication>();
if (native) {
native->setHasBorderInFullScreenDefault(true);
}
#endif // Qt >= 6.5.0
QCoreApplication::instance()->installNativeEventFilter(this);
_taskbarCreatedMsgId = RegisterWindowMessage(L"TaskbarButtonCreated");
}
ITaskbarList3 *WindowsIntegration::taskbarList() const {
return _taskbarList.get();
}
WindowsIntegration &WindowsIntegration::Instance() {
return static_cast<WindowsIntegration&>(Integration::Instance());
}
bool WindowsIntegration::nativeEventFilter(
const QByteArray &eventType,
void *message,
native_event_filter_result *result) {
return Core::Sandbox::Instance().customEnterFromEventLoop([&] {
const auto msg = static_cast<MSG*>(message);
return processEvent(
msg->hwnd,
msg->message,
msg->wParam,
msg->lParam,
(LRESULT*)result);
});
}
void WindowsIntegration::createCustomJumpList() {
_jumpList = base::WinRT::TryCreateInstance<ICustomDestinationList>(
CLSID_DestinationList);
if (_jumpList) {
refreshCustomJumpList();
}
}
void WindowsIntegration::refreshCustomJumpList() {
auto added = false;
auto maxSlots = UINT();
auto removed = (IObjectArray*)nullptr;
auto hr = _jumpList->BeginList(&maxSlots, IID_PPV_ARGS(&removed));
if (!SUCCEEDED(hr)) {
return;
}
const auto guard = gsl::finally([&] {
if (added) {
_jumpList->CommitList();
} else {
_jumpList->AbortList();
}
});
auto shellLink = base::WinRT::TryCreateInstance<IShellLink>(
CLSID_ShellLink);
if (!shellLink) {
return;
}
// Set the path to your application and the command-line argument for quitting
const auto exe = QDir::toNativeSeparators(cExeDir() + cExeName());
const auto dir = QDir::toNativeSeparators(QDir(cWorkingDir()).absolutePath());
const auto icon = Tray::QuitJumpListIconPath();
shellLink->SetArguments(L"-quit");
shellLink->SetPath(exe.toStdWString().c_str());
shellLink->SetWorkingDirectory(dir.toStdWString().c_str());
shellLink->SetIconLocation(icon.toStdWString().c_str(), 0);
if (const auto propertyStore = shellLink.try_as<IPropertyStore>()) {
auto appIdPropVar = PROPVARIANT();
hr = InitPropVariantFromString(
AppUserModelId::Id().c_str(),
&appIdPropVar);
if (SUCCEEDED(hr)) {
hr = propertyStore->SetValue(
AppUserModelId::Key(),
appIdPropVar);
PropVariantClear(&appIdPropVar);
}
auto titlePropVar = PROPVARIANT();
hr = InitPropVariantFromString(
tr::lng_quit_from_tray(tr::now).toStdWString().c_str(),
&titlePropVar);
if (SUCCEEDED(hr)) {
hr = propertyStore->SetValue(PKEY_Title, titlePropVar);
PropVariantClear(&titlePropVar);
}
propertyStore->Commit();
}
auto collection = base::WinRT::TryCreateInstance<IObjectCollection>(
CLSID_EnumerableObjectCollection);
if (!collection) {
return;
}
collection->AddObject(shellLink.get());
_jumpList->AddUserTasks(collection.get());
added = true;
}
bool WindowsIntegration::processEvent(
HWND hWnd,
UINT msg,
WPARAM wParam,
LPARAM lParam,
LRESULT *result) {
if (msg && msg == _taskbarCreatedMsgId && !_taskbarList) {
_taskbarList = base::WinRT::TryCreateInstance<ITaskbarList3>(
CLSID_TaskbarList,
CLSCTX_ALL);
if (_taskbarList) {
createCustomJumpList();
}
}
switch (msg) {
case WM_ENDSESSION:
Core::Quit();
break;
case WM_TIMECHANGE:
Core::App().checkAutoLockIn(100);
break;
case WM_WTSSESSION_CHANGE:
if (wParam == WTS_SESSION_LOGOFF
|| wParam == WTS_SESSION_LOCK) {
Core::App().setScreenIsLocked(true);
} else if (wParam == WTS_SESSION_LOGON
|| wParam == WTS_SESSION_UNLOCK) {
Core::App().setScreenIsLocked(false);
}
break;
case WM_SETTINGCHANGE:
RefreshTaskbarThemeValue();
#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
Core::App().settings().setSystemDarkMode(Platform::IsDarkMode());
#endif // Qt < 6.5.0
Core::App().tray().updateIconCounters();
if (_jumpList) {
refreshCustomJumpList();
}
break;
}
return false;
}
std::unique_ptr<Integration> CreateIntegration() {
return std::make_unique<WindowsIntegration>();
}
} // namespace Platform

View File

@@ -0,0 +1,51 @@
/*
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 "base/platform/win/base_windows_shlobj_h.h"
#include "base/platform/win/base_windows_winrt.h"
#include "platform/platform_integration.h"
#include <QAbstractNativeEventFilter>
namespace Platform {
class WindowsIntegration final
: public Integration
, public QAbstractNativeEventFilter {
public:
void init() override;
[[nodiscard]] ITaskbarList3 *taskbarList() const;
[[nodiscard]] static WindowsIntegration &Instance();
private:
bool nativeEventFilter(
const QByteArray &eventType,
void *message,
native_event_filter_result *result) override;
bool processEvent(
HWND hWnd,
UINT msg,
WPARAM wParam,
LPARAM lParam,
LRESULT *result);
void createCustomJumpList();
void refreshCustomJumpList();
uint32 _taskbarCreatedMsgId = 0;
winrt::com_ptr<ITaskbarList3> _taskbarList;
winrt::com_ptr<ICustomDestinationList> _jumpList;
};
[[nodiscard]] std::unique_ptr<Integration> CreateIntegration();
} // namespace Platform

View File

@@ -0,0 +1,134 @@
/*
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/win/launcher_win.h"
#include "core/crash_reports.h"
#include "core/update_checker.h"
#include <windows.h>
#include <shellapi.h>
#include <VersionHelpers.h>
namespace Platform {
Launcher::Launcher(int argc, char *argv[])
: Core::Launcher(argc, argv) {
}
std::optional<QStringList> Launcher::readArgumentsHook(
int argc,
char *argv[]) const {
auto count = 0;
if (const auto list = CommandLineToArgvW(GetCommandLine(), &count)) {
const auto guard = gsl::finally([&] { LocalFree(list); });
if (count > 0) {
auto result = QStringList();
result.reserve(count);
for (auto i = 0; i != count; ++i) {
result.push_back(QString::fromWCharArray(list[i]));
}
return result;
}
}
return std::nullopt;
}
bool Launcher::launchUpdater(UpdaterLaunch action) {
if (cExeName().isEmpty()) {
return false;
}
const auto operation = (action == UpdaterLaunch::JustRelaunch)
? QString()
: (cWriteProtected()
? u"runas"_q
: QString());
const auto binaryPath = (action == UpdaterLaunch::JustRelaunch)
? (cExeDir() + cExeName())
: (cWriteProtected()
? (cWorkingDir() + u"tupdates/temp/Updater.exe"_q)
: (cExeDir() + u"Updater.exe"_q));
auto argumentsList = QStringList();
const auto pushArgument = [&](const QString &argument) {
argumentsList.push_back(argument.trimmed());
};
if (cLaunchMode() == LaunchModeAutoStart) {
pushArgument(u"-autostart"_q);
}
if (Logs::DebugEnabled()) {
pushArgument(u"-debug"_q);
}
if (cStartInTray()) {
pushArgument(u"-startintray"_q);
}
if (customWorkingDir()) {
pushArgument(u"-workdir"_q);
pushArgument('"' + cWorkingDir() + '"');
}
if (cDataFile() != u"data"_q) {
pushArgument(u"-key"_q);
pushArgument('"' + cDataFile() + '"');
}
if (action == UpdaterLaunch::JustRelaunch) {
pushArgument(u"-noupdate"_q);
if (cRestartingToSettings()) {
pushArgument(u"-tosettings"_q);
}
} else {
pushArgument(u"-update"_q);
pushArgument(u"-exename"_q);
pushArgument('"' + cExeName() + '"');
if (cWriteProtected()) {
pushArgument(u"-writeprotected"_q);
pushArgument('"' + cExeDir() + '"');
}
}
return launch(operation, binaryPath, argumentsList);
}
bool Launcher::launch(
const QString &operation,
const QString &binaryPath,
const QStringList &argumentsList) {
const auto convertPath = [](const QString &path) {
return QDir::toNativeSeparators(path).toStdWString();
};
const auto nativeBinaryPath = convertPath(binaryPath);
const auto nativeWorkingDir = convertPath(cWorkingDir());
const auto arguments = argumentsList.join(' ');
DEBUG_LOG(("Application Info: executing %1 %2"
).arg(binaryPath
).arg(arguments
));
Logs::closeMain();
CrashReports::Finish();
const auto hwnd = HWND(0);
const auto result = ShellExecute(
hwnd,
operation.isEmpty() ? nullptr : operation.toStdWString().c_str(),
nativeBinaryPath.c_str(),
arguments.toStdWString().c_str(),
nativeWorkingDir.empty() ? nullptr : nativeWorkingDir.c_str(),
SW_SHOWNORMAL);
if (int64(result) < 32) {
DEBUG_LOG(("Application Error: failed to execute %1, working directory: '%2', result: %3"
).arg(binaryPath
).arg(cWorkingDir()
).arg(int64(result)
));
return false;
}
return true;
}
} // namespace Platform

View File

@@ -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
#include "core/launcher.h"
namespace Platform {
class Launcher : public Core::Launcher {
public:
Launcher(int argc, char *argv[]);
private:
std::optional<QStringList> readArgumentsHook(
int argc,
char *argv[]) const override;
bool launchUpdater(UpdaterLaunch action) override;
bool launch(
const QString &operation,
const QString &binaryPath,
const QStringList &argumentsList);
};
} // namespace Platform

View File

@@ -0,0 +1,849 @@
/*
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/win/main_window_win.h"
#include "styles/style_window.h"
#include "platform/platform_specific.h"
#include "platform/platform_notifications_manager.h"
#include "platform/win/tray_win.h"
#include "platform/win/windows_dlls.h"
#include "platform/win/integration_win.h"
#include "window/notifications_manager.h"
#include "window/window_session_controller.h"
#include "mainwindow.h"
#include "main/main_session.h"
#include "base/crc32hash.h"
#include "base/platform/win/base_windows_wrl.h"
#include "base/platform/base_platform_info.h"
#include "core/application.h"
#include "core/sandbox.h"
#include "lang/lang_keys.h"
#include "storage/localstorage.h"
#include "ui/widgets/popup_menu.h"
#include "ui/ui_utility.h"
#include "window/themes/window_theme.h"
#include "window/window_controller.h"
#include "history/history.h"
#include <QtWidgets/QStyleFactory>
#include <QtWidgets/QApplication>
#include <QtGui/QWindow>
#include <QtGui/QScreen>
#include <QtCore/QOperatingSystemVersion>
#include <Shobjidl.h>
#include <shellapi.h>
#include <WtsApi32.h>
#include <dwmapi.h>
#include <windows.ui.viewmanagement.h>
#include <UIViewSettingsInterop.h>
#include <Windowsx.h>
#include <VersionHelpers.h>
// Taken from qtbase/src/gui/image/qpixmap_win.cpp
HICON qt_pixmapToWinHICON(const QPixmap &);
HBITMAP qt_imageToWinHBITMAP(const QImage &, int hbitmapFormat);
namespace ViewManagement = ABI::Windows::UI::ViewManagement;
namespace Platform {
namespace {
// Mouse down on tray icon deactivates the application.
// So there is no way to know for sure if the tray icon was clicked from
// active application or from inactive application. So we assume that
// if the application was deactivated less than 0.5s ago, then the tray
// icon click (both left or right button) was made from the active app.
constexpr auto kKeepActiveForTrayIcon = crl::time(500);
using namespace Microsoft::WRL;
// Taken from qtbase/src/gui/image/qpixmap_win.cpp
enum HBitmapFormat {
HBitmapNoAlpha,
HBitmapPremultipliedAlpha,
HBitmapAlpha
};
class EventFilter final : public QAbstractNativeEventFilter {
public:
explicit EventFilter(not_null<MainWindow*> window);
private:
bool nativeEventFilter(
const QByteArray &eventType,
void *message,
native_event_filter_result *result) override;
bool mainWindowEvent(
HWND hWnd,
UINT msg,
WPARAM wParam,
LPARAM lParam,
LRESULT *result);
const not_null<MainWindow*> _window;
};
[[nodiscard]] HICON NativeIcon(const QIcon &icon, QSize size) {
if (!icon.isNull()) {
const auto pixmap = icon.pixmap(icon.actualSize(size));
if (!pixmap.isNull()) {
return qt_pixmapToWinHICON(pixmap);
}
}
return nullptr;
}
struct RealSize {
QSize value;
bool maximized = false;
};
[[nodiscard]] RealSize DetectRealSize(HWND hwnd) {
auto result = RECT();
auto placement = WINDOWPLACEMENT();
if (!GetWindowPlacement(hwnd, &placement)) {
return {};
} else if (placement.flags & WPF_RESTORETOMAXIMIZED) {
const auto monitor = MonitorFromRect(
&placement.rcNormalPosition,
MONITOR_DEFAULTTONULL);
if (!monitor) {
return {};
}
auto info = MONITORINFO{ .cbSize = sizeof(MONITORINFO) };
if (!GetMonitorInfo(monitor, &info)) {
return {};
}
result = info.rcWork;
} else {
CopyRect(&result, &placement.rcNormalPosition);
}
return {
{ int(result.right - result.left), int(result.bottom - result.top) },
((placement.flags & WPF_RESTORETOMAXIMIZED) != 0)
};
}
[[nodiscard]] QImage PrepareLogoPreview(
QSize size,
QImage::Format format,
int radius = 0) {
auto result = QImage(size, QImage::Format_RGB32);
result.fill(st::windowBg->c);
const auto logo = Window::Logo();
const auto width = size.width();
const auto height = size.height();
const auto side = logo.width();
const auto skip = width / 8;
const auto use = std::min({ width - skip, height - skip, side });
auto p = QPainter(&result);
if (use == side) {
p.drawImage((width - side) / 2, (height - side) / 2, logo);
} else {
const auto scaled = logo.scaled(
use,
use,
Qt::KeepAspectRatio,
Qt::SmoothTransformation);
p.drawImage((width - use) / 2, (height - use) / 2, scaled);
}
p.end();
return radius
? Images::Round(std::move(result), Images::CornersMask(radius))
: result;
}
EventFilter::EventFilter(not_null<MainWindow*> window) : _window(window) {
}
bool EventFilter::nativeEventFilter(
const QByteArray &eventType,
void *message,
native_event_filter_result *result) {
return Core::Sandbox::Instance().customEnterFromEventLoop([&] {
const auto msg = static_cast<MSG*>(message);
if (msg->hwnd == _window->psHwnd()
|| msg->hwnd && !_window->psHwnd()) {
return mainWindowEvent(
msg->hwnd,
msg->message,
msg->wParam,
msg->lParam,
(LRESULT*)result);
}
return false;
});
}
bool EventFilter::mainWindowEvent(
HWND hWnd,
UINT msg,
WPARAM wParam,
LPARAM lParam,
LRESULT *result) {
switch (msg) {
case WM_DESTROY: {
_window->destroyedFromSystem();
} return false;
case WM_ACTIVATE: {
if (LOWORD(wParam) != WA_INACTIVE) {
_window->shadowsActivate();
} else {
_window->shadowsDeactivate();
}
} return false;
case WM_SIZE: {
if (wParam == SIZE_MAXIMIZED || wParam == SIZE_RESTORED || wParam == SIZE_MINIMIZED) {
if (wParam == SIZE_RESTORED && _window->windowState() == Qt::WindowNoState) {
_window->positionUpdated();
}
}
} return false;
case WM_MOVE: {
_window->positionUpdated();
} return false;
case WM_DWMSENDICONICTHUMBNAIL: {
if (!Core::App().passcodeLocked()) {
return false;
}
const auto size = QSize(int(HIWORD(lParam)), int(LOWORD(lParam)));
return _window->setDwmThumbnail(size);
}
case WM_DWMSENDICONICLIVEPREVIEWBITMAP: {
if (!Core::App().passcodeLocked()) {
return false;
}
const auto size = DetectRealSize(hWnd);
const auto radius = size.maximized ? 0 : style::ConvertScale(8);
return _window->setDwmPreview(size.value, radius);
}
}
return false;
}
[[nodiscard]] QString HumanReadableDisplayName(HMONITOR hMonitor) {
// https://github.com/qt/qtbase/commit/6136b92f540c15835e0c7eb7e01ab7b58fbea685
auto monitorInfo = MONITORINFOEX{};
monitorInfo.cbSize = sizeof(MONITORINFOEX);
if (!GetMonitorInfo(hMonitor, &monitorInfo)) {
return QString();
}
// Try Display Configuration API first (Windows 7+).
auto numPathArrayElements = UINT32(0);
auto numModeInfoArrayElements = UINT32(0);
if (GetDisplayConfigBufferSizes(
QDC_ONLY_ACTIVE_PATHS,
&numPathArrayElements,
&numModeInfoArrayElements) != ERROR_SUCCESS) {
return QString();
}
auto pathInfos = std::vector<DISPLAYCONFIG_PATH_INFO>(
numPathArrayElements);
auto modeInfos = std::vector<DISPLAYCONFIG_MODE_INFO>(
numModeInfoArrayElements);
if (QueryDisplayConfig(
QDC_ONLY_ACTIVE_PATHS,
&numPathArrayElements,
pathInfos.data(),
&numModeInfoArrayElements,
modeInfos.data(),
nullptr) != ERROR_SUCCESS) {
return QString();
}
// Find matching path.
for (const auto &path : pathInfos) {
auto deviceName = DISPLAYCONFIG_SOURCE_DEVICE_NAME{};
deviceName.header.type = static_cast<DISPLAYCONFIG_DEVICE_INFO_TYPE>(
DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME);
deviceName.header.size = sizeof(DISPLAYCONFIG_SOURCE_DEVICE_NAME);
deviceName.header.adapterId = path.sourceInfo.adapterId;
deviceName.header.id = path.sourceInfo.id;
if (DisplayConfigGetDeviceInfo(&deviceName.header) != ERROR_SUCCESS
|| wcscmp(
monitorInfo.szDevice,
deviceName.viewGdiDeviceName) != 0) {
continue;
}
auto targetName = DISPLAYCONFIG_TARGET_DEVICE_NAME{};
targetName.header.type = static_cast<DISPLAYCONFIG_DEVICE_INFO_TYPE>(
DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME);
targetName.header.size = sizeof(DISPLAYCONFIG_TARGET_DEVICE_NAME);
targetName.header.adapterId = path.targetInfo.adapterId;
targetName.header.id = path.targetInfo.id;
if (DisplayConfigGetDeviceInfo(&targetName.header) == ERROR_SUCCESS) {
const auto friendlyName = QString::fromWCharArray(
targetName.monitorFriendlyDeviceName);
if (!friendlyName.isEmpty()) {
return friendlyName;
}
}
}
// Fallback to legacy method.
auto displayDevice = DISPLAY_DEVICE{};
displayDevice.cb = sizeof(DISPLAY_DEVICE);
for (auto deviceIndex = DWORD(0);
EnumDisplayDevices(
monitorInfo.szDevice,
deviceIndex,
&displayDevice,
0);
deviceIndex++) {
if (!(displayDevice.StateFlags & DISPLAY_DEVICE_ACTIVE)) {
continue;
}
const auto deviceName = QString::fromWCharArray(
displayDevice.DeviceString);
if (!deviceName.isEmpty()
&& deviceName != u"Generic PnP Monitor"_q) {
return deviceName;
}
break;
}
return QString();
}
} // namespace
struct MainWindow::Private {
explicit Private(not_null<MainWindow*> window) : filter(window) {
}
EventFilter filter;
ComPtr<ViewManagement::IUIViewSettings> viewSettings;
};
MainWindow::BitmapPointer::BitmapPointer(HBITMAP value) : _value(value) {
}
MainWindow::BitmapPointer::BitmapPointer(BitmapPointer &&other)
: _value(base::take(other._value)) {
}
MainWindow::BitmapPointer &MainWindow::BitmapPointer::operator=(
BitmapPointer &&other) {
if (_value != other._value) {
reset();
_value = base::take(other._value);
}
return *this;
}
MainWindow::BitmapPointer::~BitmapPointer() {
reset();
}
HBITMAP MainWindow::BitmapPointer::get() const {
return _value;
}
MainWindow::BitmapPointer::operator bool() const {
return _value != nullptr;
}
void MainWindow::BitmapPointer::release() {
_value = nullptr;
}
void MainWindow::BitmapPointer::reset(HBITMAP value) {
if (_value != value) {
if (const auto old = std::exchange(_value, value)) {
DeleteObject(old);
}
}
}
MainWindow::MainWindow(not_null<Window::Controller*> controller)
: Window::MainWindow(controller)
, _private(std::make_unique<Private>(this))
, _taskbarHiderWindow(std::make_unique<QWindow>()) {
qApp->installNativeEventFilter(&_private->filter);
setupNativeWindowFrame();
SetWindowPriority(this, controller->isPrimary() ? 2 : 1);
using namespace rpl::mappers;
Core::App().appDeactivatedValue(
) | rpl::distinct_until_changed(
) | rpl::filter(_1) | rpl::on_next([=] {
_lastDeactivateTime = crl::now();
}, lifetime());
setupPreviewPasscodeLock();
}
void MainWindow::setupPreviewPasscodeLock() {
Core::App().passcodeLockValue(
) | rpl::on_next([=](bool locked) {
// Use iconic bitmap instead of the window content if passcoded.
BOOL fForceIconic = locked ? TRUE : FALSE;
BOOL fHasIconicBitmap = fForceIconic;
DwmSetWindowAttribute(
_hWnd,
DWMWA_FORCE_ICONIC_REPRESENTATION,
&fForceIconic,
sizeof(fForceIconic));
DwmSetWindowAttribute(
_hWnd,
DWMWA_HAS_ICONIC_BITMAP,
&fHasIconicBitmap,
sizeof(fHasIconicBitmap));
}, lifetime());
}
void MainWindow::setupNativeWindowFrame() {
auto nativeFrame = rpl::single(
Core::App().settings().nativeWindowFrame()
) | rpl::then(
Core::App().settings().nativeWindowFrameChanges()
);
rpl::combine(
std::move(nativeFrame),
Window::Theme::IsNightModeValue()
) | rpl::skip(1) | rpl::on_next([=](bool native, bool night) {
validateWindowTheme(native, night);
}, lifetime());
}
void MainWindow::shadowsActivate() {
_hasActiveFrame = true;
}
void MainWindow::shadowsDeactivate() {
_hasActiveFrame = false;
}
void MainWindow::destroyedFromSystem() {
if (!Core::App().closeNonLastAsync(&controller())) {
Core::Quit();
}
}
bool MainWindow::setDwmThumbnail(QSize size) {
validateDwmPreviewColors();
if (size.isEmpty()) {
return false;
} else if (!_dwmThumbnail || _dwmThumbnailSize != size) {
const auto result = PrepareLogoPreview(size, QImage::Format_RGB32);
const auto bitmap = qt_imageToWinHBITMAP(result, HBitmapNoAlpha);
if (!bitmap) {
return false;
}
_dwmThumbnail.reset(bitmap);
_dwmThumbnailSize = size;
}
DwmSetIconicThumbnail(_hWnd, _dwmThumbnail.get(), NULL);
return true;
}
bool MainWindow::setDwmPreview(QSize size, int radius) {
Expects(radius >= 0);
validateDwmPreviewColors();
if (size.isEmpty()) {
return false;
} else if (!_dwmPreview
|| _dwmPreviewSize != size
|| _dwmPreviewRadius != radius) {
const auto format = (radius > 0)
? QImage::Format_ARGB32_Premultiplied
: QImage::Format_RGB32;
const auto result = PrepareLogoPreview(size, format, radius);
const auto bitmap = qt_imageToWinHBITMAP(
result,
(radius > 0) ? HBitmapPremultipliedAlpha : HBitmapNoAlpha);
if (!bitmap) {
return false;
}
_dwmPreview.reset(bitmap);
_dwmPreviewRadius = radius;
_dwmPreviewSize = size;
}
const auto flags = 0;
DwmSetIconicLivePreviewBitmap(_hWnd, _dwmPreview.get(), NULL, flags);
return true;
}
void MainWindow::validateDwmPreviewColors() {
if (_dwmBackground == st::windowBg->c) {
return;
}
_dwmBackground = st::windowBg->c;
_dwmThumbnail.reset();
_dwmPreview.reset();
}
void MainWindow::forceIconRefresh() {
const auto refresher = std::make_unique<QWidget>(this);
refresher->setWindowFlags(
static_cast<Qt::WindowFlags>(Qt::Tool) | Qt::FramelessWindowHint);
refresher->setGeometry(x() + 1, y() + 1, 1, 1);
auto palette = refresher->palette();
palette.setColor(
QPalette::Window,
(isActiveWindow() ? st::titleBgActive : st::titleBg)->c);
refresher->setPalette(palette);
refresher->show();
refresher->raise();
refresher->activateWindow();
updateTaskbarAndIconCounters();
}
void MainWindow::workmodeUpdated(Core::Settings::WorkMode mode) {
using WorkMode = Core::Settings::WorkMode;
switch (mode) {
case WorkMode::WindowAndTray: {
HWND psOwner = (HWND)GetWindowLongPtr(_hWnd, GWLP_HWNDPARENT);
if (psOwner) {
SetWindowLongPtr(_hWnd, GWLP_HWNDPARENT, 0);
windowHandle()->setTransientParent(nullptr);
forceIconRefresh();
}
} break;
case WorkMode::TrayOnly: {
HWND psOwner = (HWND)GetWindowLongPtr(_hWnd, GWLP_HWNDPARENT);
if (!psOwner) {
const auto hwnd = _taskbarHiderWindow->winId();
SetWindowLongPtr(_hWnd, GWLP_HWNDPARENT, (LONG_PTR)hwnd);
windowHandle()->setTransientParent(_taskbarHiderWindow.get());
}
} break;
case WorkMode::WindowOnly: {
HWND psOwner = (HWND)GetWindowLongPtr(_hWnd, GWLP_HWNDPARENT);
if (psOwner) {
SetWindowLongPtr(_hWnd, GWLP_HWNDPARENT, 0);
windowHandle()->setTransientParent(nullptr);
forceIconRefresh();
}
} break;
}
}
bool MainWindow::hasTabletView() const {
if (!_private->viewSettings) {
return false;
}
auto mode = ViewManagement::UserInteractionMode();
_private->viewSettings->get_UserInteractionMode(&mode);
return (mode == ViewManagement::UserInteractionMode_Touch);
}
bool MainWindow::initGeometryFromSystem() {
if (!hasTabletView() || !screen()) {
return false;
}
Ui::RpWidget::setGeometry(screen()->availableGeometry());
return true;
}
bool MainWindow::nativeEvent(
const QByteArray &eventType,
void *message,
native_event_filter_result *result) {
if (message) {
const auto msg = static_cast<MSG*>(message);
if (msg->message == WM_IME_STARTCOMPOSITION) {
Core::Sandbox::Instance().customEnterFromEventLoop([&] {
imeCompositionStartReceived();
});
}
}
return false;
}
void MainWindow::updateWindowIcon() {
updateTaskbarAndIconCounters();
}
bool MainWindow::isActiveForTrayMenu() {
return !_lastDeactivateTime
|| (_lastDeactivateTime + kKeepActiveForTrayIcon >= crl::now());
}
void MainWindow::unreadCounterChangedHook() {
updateTaskbarAndIconCounters();
}
void MainWindow::updateTaskbarAndIconCounters() {
const auto counter = Core::App().unreadBadge();
const auto muted = Core::App().unreadBadgeMuted();
const auto controller = sessionController();
const auto session = controller ? &controller->session() : nullptr;
const auto iconSizeSmall = QSize(
GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON));
const auto iconSizeBig = QSize(
GetSystemMetrics(SM_CXICON),
GetSystemMetrics(SM_CYICON));
const auto supportMode = session && session->supportMode();
auto iconSmallPixmap16 = Tray::IconWithCounter(
Tray::CounterLayerArgs(16, counter, muted),
true,
false,
supportMode);
auto iconSmallPixmap32 = Tray::IconWithCounter(
Tray::CounterLayerArgs(32, counter, muted),
true,
false,
supportMode);
QIcon iconSmall, iconBig;
iconSmall.addPixmap(iconSmallPixmap16);
iconSmall.addPixmap(iconSmallPixmap32);
const auto integration = &Platform::WindowsIntegration::Instance();
const auto taskbarList = integration->taskbarList();
const auto bigCounter = taskbarList ? 0 : counter;
iconBig.addPixmap(Tray::IconWithCounter(
Tray::CounterLayerArgs(32, bigCounter, muted),
false,
false,
supportMode));
iconBig.addPixmap(Tray::IconWithCounter(
Tray::CounterLayerArgs(64, bigCounter, muted),
false,
false,
supportMode));
destroyCachedIcons();
_iconSmall = NativeIcon(iconSmall, iconSizeSmall);
_iconBig = NativeIcon(iconBig, iconSizeBig);
SendMessage(_hWnd, WM_SETICON, ICON_SMALL, (LPARAM)_iconSmall);
SendMessage(_hWnd, WM_SETICON, ICON_BIG, (LPARAM)(_iconBig ? _iconBig : _iconSmall));
if (taskbarList) {
if (counter > 0) {
const auto pixmap = [&](int size) {
return Ui::PixmapFromImage(Window::GenerateCounterLayer(
Tray::CounterLayerArgs(size, counter, muted)));
};
QIcon iconOverlay;
iconOverlay.addPixmap(pixmap(16));
iconOverlay.addPixmap(pixmap(32));
_iconOverlay = NativeIcon(iconOverlay, iconSizeSmall);
}
const auto description = (counter > 0)
? tr::lng_unread_bar(tr::now, lt_count, counter).toStdWString()
: std::wstring();
taskbarList->SetOverlayIcon(_hWnd, _iconOverlay, description.c_str());
}
SetWindowPos(_hWnd, 0, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
}
void MainWindow::initHook() {
_hWnd = reinterpret_cast<HWND>(winId());
if (!_hWnd) {
return;
}
WTSRegisterSessionNotification(_hWnd, NOTIFY_FOR_THIS_SESSION);
using namespace base::Platform;
auto factory = ComPtr<IUIViewSettingsInterop>();
if (SupportsWRL()) {
ABI::Windows::Foundation::GetActivationFactory(
StringReferenceWrapper(
RuntimeClass_Windows_UI_ViewManagement_UIViewSettings).Get(),
&factory);
if (factory) {
// NB! No such method (or IUIViewSettingsInterop) in C++/WinRT :(
factory->GetForWindow(
_hWnd,
IID_PPV_ARGS(&_private->viewSettings));
}
}
validateWindowTheme(
Core::App().settings().nativeWindowFrame(),
Window::Theme::IsNightMode());
}
void MainWindow::validateWindowTheme(bool native, bool night) {
if (!IsWindows8OrGreater()) {
const auto empty = native ? nullptr : L" ";
SetWindowTheme(_hWnd, empty, empty);
QApplication::setStyle(QStyleFactory::create(u"Windows"_q));
#if 0
} else if (!Core::App().settings().systemDarkMode().has_value()/*
|| (!Dlls::AllowDarkModeForApp && !Dlls::SetPreferredAppMode)
|| !Dlls::AllowDarkModeForWindow
|| !Dlls::RefreshImmersiveColorPolicyState
|| !Dlls::FlushMenuThemes*/) {
return;
#endif
} else if (!native) {
SetWindowTheme(_hWnd, nullptr, nullptr);
return;
}
// See "https://github.com/microsoft/terminal/blob/"
// "eb480b6bbbd83a2aafbe62992d360838e0ab9da5/"
// "src/interactivity/win32/windowtheme.cpp#L43-L63"
auto darkValue = BOOL(night ? TRUE : FALSE);
const auto updateStyle = [&] {
static const auto kSystemVersion = QOperatingSystemVersion::current();
if (kSystemVersion.microVersion() >= 18875 && Dlls::SetWindowCompositionAttribute) {
Dlls::WINDOWCOMPOSITIONATTRIBDATA data = {
Dlls::WINDOWCOMPOSITIONATTRIB::WCA_USEDARKMODECOLORS,
&darkValue,
sizeof(darkValue)
};
Dlls::SetWindowCompositionAttribute(_hWnd, &data);
} else if (kSystemVersion.microVersion() >= 17763) {
static const auto kDWMWA_USE_IMMERSIVE_DARK_MODE = (kSystemVersion.microVersion() >= 18985)
? DWORD(20)
: DWORD(19);
DwmSetWindowAttribute(
_hWnd,
kDWMWA_USE_IMMERSIVE_DARK_MODE,
&darkValue,
sizeof(darkValue));
}
};
updateStyle();
// See "https://osdn.net/projects/tortoisesvn/scm/svn/blobs/28812/"
// "trunk/src/TortoiseIDiff/MainWindow.cpp"
//
// But for now it works event with a small part of that.
//
//const auto updateWindowTheme = [&] {
// const auto set = [&](LPCWSTR name) {
// return SetWindowTheme(_hWnd, name, nullptr);
// };
// if (!night || FAILED(set(L"DarkMode_Explorer"))) {
// set(L"Explorer");
// }
//};
//
//if (night) {
// if (Dlls::SetPreferredAppMode) {
// Dlls::SetPreferredAppMode(Dlls::PreferredAppMode::AllowDark);
// } else {
// Dlls::AllowDarkModeForApp(TRUE);
// }
// Dlls::AllowDarkModeForWindow(_hWnd, TRUE);
// updateWindowTheme();
// updateStyle();
// Dlls::FlushMenuThemes();
// Dlls::RefreshImmersiveColorPolicyState();
//} else {
// updateWindowTheme();
// Dlls::AllowDarkModeForWindow(_hWnd, FALSE);
// updateStyle();
// Dlls::FlushMenuThemes();
// Dlls::RefreshImmersiveColorPolicyState();
// if (Dlls::SetPreferredAppMode) {
// Dlls::SetPreferredAppMode(Dlls::PreferredAppMode::Default);
// } else {
// Dlls::AllowDarkModeForApp(FALSE);
// }
//}
// Didn't find any other way to definitely repaint with the new style.
SendMessage(_hWnd, WM_NCACTIVATE, _hasActiveFrame ? 0 : 1, 0);
SendMessage(_hWnd, WM_NCACTIVATE, _hasActiveFrame ? 1 : 0, 0);
}
HWND MainWindow::psHwnd() const {
return _hWnd;
}
void MainWindow::destroyCachedIcons() {
const auto destroy = [](HICON &icon) {
if (icon) {
DestroyIcon(icon);
icon = nullptr;
}
};
destroy(_iconBig);
destroy(_iconSmall);
destroy(_iconOverlay);
}
MainWindow::~MainWindow() {
WTSUnRegisterSessionNotification(_hWnd);
_private->viewSettings.Reset();
destroyCachedIcons();
}
int32 ScreenNameChecksum(const QString &name) {
constexpr int DeviceNameSize = base::array_size(MONITORINFOEX().szDevice);
wchar_t buffer[DeviceNameSize] = { 0 };
if (name.size() < DeviceNameSize) {
name.toWCharArray(buffer);
} else {
memcpy(buffer, name.toStdWString().data(), sizeof(buffer));
}
return base::crc32(buffer, sizeof(buffer));
}
int32 ScreenNameChecksum(const QScreen *screen) {
return ScreenNameChecksum(screen->name());
}
QString ScreenDisplayLabel(const QScreen *screen) {
if (!screen) {
return QString();
}
const auto geometry = screen->geometry();
const auto hMonitor = MonitorFromPoint(
POINT{
geometry.x() + geometry.width() / 2,
geometry.y() + geometry.height() / 2,
},
MONITOR_DEFAULTTONEAREST);
const auto displayName = HumanReadableDisplayName(hMonitor);
if (!displayName.isEmpty()) {
return displayName;
}
const auto name = screen->name();
const auto genericName = u"\\\\.\\DISPLAY"_q;
if (name.startsWith(genericName)) {
const auto displayNum = name.mid(genericName.size());
return u"Display %1"_q.arg(displayNum);
}
return name;
}
} // namespace Platform

View File

@@ -0,0 +1,113 @@
/*
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 "platform/platform_main_window.h"
#include "base/flags.h"
#include <windows.h>
namespace Ui {
class PopupMenu;
} // namespace Ui
namespace Platform {
class MainWindow : public Window::MainWindow {
public:
explicit MainWindow(not_null<Window::Controller*> controller);
HWND psHwnd() const;
void updateWindowIcon() override;
bool isActiveForTrayMenu() override;
// Custom shadows.
void shadowsActivate();
void shadowsDeactivate();
[[nodiscard]] bool hasTabletView() const;
void destroyedFromSystem();
bool setDwmThumbnail(QSize size);
bool setDwmPreview(QSize size, int radius);
~MainWindow();
protected:
void initHook() override;
void unreadCounterChangedHook() override;
void workmodeUpdated(Core::Settings::WorkMode mode) override;
bool initGeometryFromSystem() override;
bool nativeEvent(
const QByteArray &eventType,
void *message,
native_event_filter_result *result) override;
private:
struct Private;
class BitmapPointer {
public:
BitmapPointer(HBITMAP value = nullptr);
BitmapPointer(BitmapPointer &&other);
BitmapPointer& operator=(BitmapPointer &&other);
~BitmapPointer();
[[nodiscard]] HBITMAP get() const;
[[nodiscard]] explicit operator bool() const;
void release();
void reset(HBITMAP value = nullptr);
private:
HBITMAP _value = nullptr;
};
void setupNativeWindowFrame();
void setupPreviewPasscodeLock();
void updateTaskbarAndIconCounters();
void validateWindowTheme(bool native, bool night);
void forceIconRefresh();
void destroyCachedIcons();
void validateDwmPreviewColors();
const std::unique_ptr<Private> _private;
const std::unique_ptr<QWindow> _taskbarHiderWindow;
HWND _hWnd = nullptr;
HICON _iconBig = nullptr;
HICON _iconSmall = nullptr;
HICON _iconOverlay = nullptr;
BitmapPointer _dwmThumbnail;
BitmapPointer _dwmPreview;
QSize _dwmThumbnailSize;
QSize _dwmPreviewSize;
QColor _dwmBackground;
int _dwmPreviewRadius = 0;
// Workarounds for activation from tray icon.
crl::time _lastDeactivateTime = 0;
bool _hasActiveFrame = false;
};
[[nodiscard]] int32 ScreenNameChecksum(const QString &name);
[[nodiscard]] int32 ScreenNameChecksum(const QScreen *screen);
[[nodiscard]] QString ScreenDisplayLabel(const QScreen *screen);
} // namespace Platform

View File

@@ -0,0 +1,978 @@
/*
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/win/notifications_manager_win.h"
#include "window/notifications_utilities.h"
#include "window/window_session_controller.h"
#include "base/platform/win/base_windows_co_task_mem.h"
#include "base/platform/win/base_windows_rpcndr_h.h"
#include "base/platform/win/base_windows_winrt.h"
#include "base/platform/base_platform_info.h"
#include "base/platform/win/wrl/wrl_module_h.h"
#include "base/qthelp_url.h"
#include "platform/win/windows_app_user_model_id.h"
#include "platform/win/windows_toast_activator.h"
#include "platform/win/windows_dlls.h"
#include "platform/win/specific_win.h"
#include "data/data_forum_topic.h"
#include "data/data_saved_sublist.h"
#include "data/data_peer.h"
#include "history/history.h"
#include "history/history_item.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "mainwindow.h"
#include "windows_quiethours_h.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include <QtCore/QOperatingSystemVersion>
#include <Shobjidl.h>
#include <shellapi.h>
#include <strsafe.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Data.Xml.Dom.h>
#include <winrt/Windows.UI.Notifications.h>
HICON qt_pixmapToWinHICON(const QPixmap &);
using namespace winrt::Windows::UI::Notifications;
using namespace winrt::Windows::Data::Xml::Dom;
using namespace winrt::Windows::Foundation;
using winrt::com_ptr;
namespace Platform {
namespace Notifications {
namespace {
constexpr auto kQuerySettingsEachMs = 1000;
crl::time LastSettingsQueryMs/* = 0*/;
[[nodiscard]] bool ShouldQuerySettings() {
const auto now = crl::now();
if (LastSettingsQueryMs > 0 && now <= LastSettingsQueryMs + kQuerySettingsEachMs) {
return false;
}
LastSettingsQueryMs = now;
return true;
}
[[nodiscard]] std::wstring NotificationTemplate(
QString id,
Window::Notifications::Manager::DisplayOptions options) {
const auto wid = id.replace('&', "&amp;").toStdWString();
const auto fastReply = LR"(
<input id="fastReply" type="text" placeHolderContent=""/>
<action
content="Send"
arguments="action=reply&amp;)" + wid + LR"("
activationType="background"
imageUri=""
hint-inputId="fastReply"/>
)";
const auto markAsRead = LR"(
<action
content=""
arguments="action=mark&amp;)" + wid + LR"("
activationType="background"/>
)";
const auto actions = (options.hideReplyButton ? L"" : fastReply)
+ (options.hideMarkAsRead ? L"" : markAsRead);
return LR"(
<toast launch="action=open&amp;)" + wid + LR"(">
<visual>
<binding template="ToastGeneric">
<image placement="appLogoOverride" hint-crop="circle" src=""/>
<text hint-maxLines="1"></text>
<text></text>
<text></text>
</binding>
</visual>
)" + (actions.empty()
? L""
: (L"<actions>" + actions + L"</actions>")) + LR"(
<audio silent="true"/>
</toast>
)";
}
bool init() {
if (!IsWindows8OrGreater() || !base::WinRT::Supported()) {
return false;
}
{
using namespace Microsoft::WRL;
const auto hr = Module<OutOfProc>::GetModule().RegisterObjects();
if (!SUCCEEDED(hr)) {
LOG(("App Error: Object registration failed."));
}
}
if (!AppUserModelId::ValidateShortcut()) {
LOG(("App Error: Shortcut validation failed."));
return false;
}
PWSTR appUserModelId = {};
if (!SUCCEEDED(GetCurrentProcessExplicitAppUserModelID(&appUserModelId))) {
return false;
}
const auto appUserModelIdGuard = gsl::finally([&] {
CoTaskMemFree(appUserModelId);
});
if (AppUserModelId::Id() != appUserModelId) {
return false;
}
return true;
}
// Throws.
void SetNodeValueString(
const XmlDocument &xml,
const IXmlNode &node,
const std::wstring &text) {
node.AppendChild(xml.CreateTextNode(text).as<IXmlNode>());
}
// Throws.
void SetAudioSilent(const XmlDocument &toastXml) {
const auto nodeList = toastXml.GetElementsByTagName(L"audio");
if (const auto audioNode = nodeList.Item(0)) {
audioNode.as<IXmlElement>().SetAttribute(L"silent", L"true");
} else {
auto audioElement = toastXml.CreateElement(L"audio");
audioElement.SetAttribute(L"silent", L"true");
auto nodeList = toastXml.GetElementsByTagName(L"toast");
nodeList.Item(0).AppendChild(audioElement.as<IXmlNode>());
}
}
// Throws.
void SetImageSrc(const XmlDocument &toastXml, const std::wstring &path) {
const auto nodeList = toastXml.GetElementsByTagName(L"image");
const auto attributes = nodeList.Item(0).Attributes();
return SetNodeValueString(
toastXml,
attributes.GetNamedItem(L"src"),
L"file:///" + path);
}
// Throws.
void SetReplyIconSrc(const XmlDocument &toastXml, const std::wstring &path) {
const auto nodeList = toastXml.GetElementsByTagName(L"action");
const auto length = int(nodeList.Length());
for (auto i = 0; i != length; ++i) {
const auto attributes = nodeList.Item(i).Attributes();
if (const auto uri = attributes.GetNamedItem(L"imageUri")) {
return SetNodeValueString(toastXml, uri, L"file:///" + path);
}
}
}
// Throws.
void SetReplyPlaceholder(
const XmlDocument &toastXml,
const std::wstring &placeholder) {
const auto nodeList = toastXml.GetElementsByTagName(L"input");
const auto attributes = nodeList.Item(0).Attributes();
return SetNodeValueString(
toastXml,
attributes.GetNamedItem(L"placeHolderContent"),
placeholder);
}
// Throws.
void SetAction(const XmlDocument &toastXml, const QString &id) {
auto nodeList = toastXml.GetElementsByTagName(L"toast");
if (const auto toast = nodeList.Item(0).try_as<XmlElement>()) {
toast.SetAttribute(L"launch", L"action=open&" + id.toStdWString());
}
}
// Throws.
void SetMarkAsReadText(
const XmlDocument &toastXml,
const std::wstring &text) {
const auto nodeList = toastXml.GetElementsByTagName(L"action");
const auto length = int(nodeList.Length());
for (auto i = 0; i != length; ++i) {
const auto attributes = nodeList.Item(i).Attributes();
if (!attributes.GetNamedItem(L"imageUri")) {
return SetNodeValueString(
toastXml,
attributes.GetNamedItem(L"content"),
text);
}
}
}
auto Checked = false;
auto InitSucceeded = false;
void Check() {
InitSucceeded = init();
}
bool QuietHoursEnabled = false;
DWORD QuietHoursValue = 0;
[[nodiscard]] bool UseQuietHoursRegistryEntry() {
static const bool result = [] {
const auto version = QOperatingSystemVersion::current();
// At build 17134 (Redstone 4) the "Quiet hours" was replaced
// by "Focus assist" and it looks like it doesn't use registry.
return (version.majorVersion() == 10)
&& (version.minorVersion() == 0)
&& (version.microVersion() < 17134);
}();
return result;
}
// Thanks https://stackoverflow.com/questions/35600128/get-windows-quiet-hours-from-win32-or-c-sharp-api
void QueryQuietHours() {
if (!UseQuietHoursRegistryEntry()) {
// There are quiet hours in Windows starting from Windows 8.1
// But there were several reports about the notifications being shut
// down according to the registry while no quiet hours were enabled.
// So we try this method only starting with Windows 10.
return;
}
LPCWSTR lpKeyName = L"Software\\Microsoft\\Windows\\CurrentVersion\\Notifications\\Settings";
LPCWSTR lpValueName = L"NOC_GLOBAL_SETTING_TOASTS_ENABLED";
HKEY key;
auto result = RegOpenKeyEx(HKEY_CURRENT_USER, lpKeyName, 0, KEY_READ, &key);
if (result != ERROR_SUCCESS) {
return;
}
DWORD value = 0, type = 0, size = sizeof(value);
result = RegQueryValueEx(key, lpValueName, 0, &type, (LPBYTE)&value, &size);
RegCloseKey(key);
auto quietHoursEnabled = (result == ERROR_SUCCESS) && (value == 0);
if (QuietHoursEnabled != quietHoursEnabled) {
QuietHoursEnabled = quietHoursEnabled;
QuietHoursValue = value;
LOG(("Quiet hours changed, entry value: %1").arg(value));
} else if (QuietHoursValue != value) {
QuietHoursValue = value;
LOG(("Quiet hours value changed, was value: %1, entry value: %2").arg(QuietHoursValue).arg(value));
}
}
bool FocusAssistBlocks = false;
// Thanks https://www.withinrafael.com/2019/09/19/determine-if-your-app-is-in-a-focus-assist-profiles-priority-list/
void QueryFocusAssist() {
const auto quietHoursSettings = base::WinRT::TryCreateInstance<
IQuietHoursSettings
>(CLSID_QuietHoursSettings, CLSCTX_LOCAL_SERVER);
if (!quietHoursSettings) {
return;
}
auto profileId = base::CoTaskMemString();
auto hr = quietHoursSettings->get_UserSelectedProfile(profileId.put());
if (FAILED(hr) || !profileId) {
return;
}
const auto profileName = QString::fromWCharArray(profileId.data());
if (profileName.endsWith(".alarmsonly", Qt::CaseInsensitive)) {
if (!FocusAssistBlocks) {
LOG(("Focus Assist: Alarms Only."));
FocusAssistBlocks = true;
}
return;
} else if (!profileName.endsWith(".priorityonly", Qt::CaseInsensitive)) {
if (!profileName.endsWith(".unrestricted", Qt::CaseInsensitive)) {
LOG(("Focus Assist Warning: Unknown profile '%1'"
).arg(profileName));
}
if (FocusAssistBlocks) {
LOG(("Focus Assist: Unrestricted."));
FocusAssistBlocks = false;
}
return;
}
const auto appUserModelId = AppUserModelId::Id();
auto blocked = true;
const auto guard = gsl::finally([&] {
if (FocusAssistBlocks != blocked) {
LOG(("Focus Assist: %1, AppUserModelId: %2, Blocks: %3"
).arg(profileName
).arg(QString::fromStdWString(appUserModelId)
).arg(Logs::b(blocked)));
FocusAssistBlocks = blocked;
}
});
com_ptr<IQuietHoursProfile> profile;
hr = quietHoursSettings->GetProfile(profileId.data(), profile.put());
if (FAILED(hr) || !profile) {
return;
}
auto apps = base::CoTaskMemStringArray();
hr = profile->GetAllowedApps(apps.put_size(), apps.put());
if (FAILED(hr) || !apps) {
return;
}
for (const auto &app : apps) {
if (app && app.data() == appUserModelId) {
blocked = false;
break;
}
}
}
QUERY_USER_NOTIFICATION_STATE UserNotificationState
= QUNS_ACCEPTS_NOTIFICATIONS;
void QueryUserNotificationState() {
if (Dlls::SHQueryUserNotificationState != nullptr) {
QUERY_USER_NOTIFICATION_STATE state;
if (SUCCEEDED(Dlls::SHQueryUserNotificationState(&state))) {
UserNotificationState = state;
}
}
}
void QuerySystemNotificationSettings() {
if (!ShouldQuerySettings()) {
return;
}
QueryQuietHours();
QueryFocusAssist();
QueryUserNotificationState();
}
bool SkipSoundForCustom() {
QuerySystemNotificationSettings();
return (UserNotificationState == QUNS_NOT_PRESENT)
|| (UserNotificationState == QUNS_PRESENTATION_MODE)
|| (FocusAssistBlocks && Core::App().settings().skipToastsInFocus())
|| Core::App().screenIsLocked();
}
bool SkipFlashBounceForCustom() {
return SkipToastForCustom();
}
} // namespace
void MaybePlaySoundForCustom(Fn<void()> playSound) {
if (!SkipSoundForCustom()) {
playSound();
}
}
bool SkipToastForCustom() {
QuerySystemNotificationSettings();
return (UserNotificationState == QUNS_PRESENTATION_MODE)
|| (UserNotificationState == QUNS_RUNNING_D3D_FULL_SCREEN)
|| (FocusAssistBlocks && Core::App().settings().skipToastsInFocus());
}
void MaybeFlashBounceForCustom(Fn<void()> flashBounce) {
if (!SkipFlashBounceForCustom()) {
flashBounce();
}
}
bool WaitForInputForCustom() {
QuerySystemNotificationSettings();
return UserNotificationState != QUNS_BUSY;
}
bool Supported() {
if (!Checked) {
Checked = true;
Check();
}
return InitSucceeded;
}
bool Enforced() {
return false;
}
bool ByDefault() {
return false;
}
bool VolumeSupported() {
return true;
}
void Create(Window::Notifications::System *system) {
system->setManager([=] {
auto result = std::make_unique<Manager>(system);
return result->init() ? std::move(result) : nullptr;
});
}
class Manager::Private {
public:
using Info = Window::Notifications::NativeManager::NotificationInfo;
explicit Private(Manager *instance);
bool init();
bool showNotification(Info &&info, Ui::PeerUserpicView &userpicView);
void clearAll();
void clearFromItem(not_null<HistoryItem*> item);
void clearFromTopic(not_null<Data::ForumTopic*> topic);
void clearFromSublist(not_null<Data::SavedSublist*> sublist);
void clearFromHistory(not_null<History*> history);
void clearFromSession(not_null<Main::Session*> session);
void beforeNotificationActivated(NotificationId id);
void afterNotificationActivated(
NotificationId id,
not_null<Window::SessionController*> window);
void clearNotification(NotificationId id);
void handleActivation(const ToastActivation &activation);
~Private();
private:
bool showNotificationInTryCatch(
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView);
void tryHide(const ToastNotification &notification);
[[nodiscard]] std::wstring ensureSendButtonIcon();
Window::Notifications::CachedUserpics _cachedUserpics;
std::wstring _sendButtonIconPath;
std::shared_ptr<Manager*> _guarded;
ToastNotifier _notifier = nullptr;
base::flat_map<
ContextId,
base::flat_map<MsgId, ToastNotification>> _notifications;
rpl::lifetime _lifetime;
};
Manager::Private::Private(Manager *instance)
: _guarded(std::make_shared<Manager*>(instance)) {
ToastActivations(
) | rpl::on_next([=](const ToastActivation &activation) {
handleActivation(activation);
}, _lifetime);
}
bool Manager::Private::init() {
return base::WinRT::Try([&] {
_notifier = ToastNotificationManager::CreateToastNotifier(
AppUserModelId::Id());
});
}
Manager::Private::~Private() {
clearAll();
_notifications.clear();
_notifier = nullptr;
}
void Manager::Private::clearAll() {
if (!_notifier) {
return;
}
for (const auto &[key, notifications] : base::take(_notifications)) {
for (const auto &[msgId, notification] : notifications) {
tryHide(notification);
}
}
}
void Manager::Private::clearFromItem(not_null<HistoryItem*> item) {
if (!_notifier) {
return;
}
auto i = _notifications.find(ContextId{
.sessionId = item->history()->session().uniqueId(),
.peerId = item->history()->peer->id,
.topicRootId = item->topicRootId(),
.monoforumPeerId = item->sublistPeerId(),
});
if (i == _notifications.cend()) {
return;
}
const auto j = i->second.find(item->id);
if (j == end(i->second)) {
return;
}
const auto taken = std::exchange(j->second, nullptr);
i->second.erase(j);
if (i->second.empty()) {
_notifications.erase(i);
}
tryHide(taken);
}
void Manager::Private::clearFromTopic(not_null<Data::ForumTopic*> topic) {
if (!_notifier) {
return;
}
const auto i = _notifications.find(ContextId{
.sessionId = topic->session().uniqueId(),
.peerId = topic->history()->peer->id,
.topicRootId = topic->rootId(),
});
if (i != _notifications.cend()) {
const auto temp = base::take(i->second);
_notifications.erase(i);
for (const auto &[msgId, notification] : temp) {
tryHide(notification);
}
}
}
void Manager::Private::clearFromSublist(
not_null<Data::SavedSublist*> sublist) {
if (!_notifier) {
return;
}
const auto i = _notifications.find(ContextId{
.sessionId = sublist->session().uniqueId(),
.peerId = sublist->owningHistory()->peer->id,
.monoforumPeerId = sublist->sublistPeer()->id,
});
if (i != _notifications.cend()) {
const auto temp = base::take(i->second);
_notifications.erase(i);
for (const auto &[msgId, notification] : temp) {
tryHide(notification);
}
}
}
void Manager::Private::clearFromHistory(not_null<History*> history) {
if (!_notifier) {
return;
}
const auto sessionId = history->session().uniqueId();
const auto peerId = history->peer->id;
auto i = _notifications.lower_bound(ContextId{
.sessionId = sessionId,
.peerId = peerId,
});
while (i != _notifications.cend()
&& i->first.sessionId == sessionId
&& i->first.peerId == peerId) {
const auto temp = base::take(i->second);
i = _notifications.erase(i);
for (const auto &[msgId, notification] : temp) {
tryHide(notification);
}
}
}
void Manager::Private::clearFromSession(not_null<Main::Session*> session) {
if (!_notifier) {
return;
}
const auto sessionId = session->uniqueId();
auto i = _notifications.lower_bound(ContextId{
.sessionId = sessionId,
});
while (i != _notifications.cend() && i->first.sessionId == sessionId) {
const auto temp = base::take(i->second);
i = _notifications.erase(i);
for (const auto &[msgId, notification] : temp) {
tryHide(notification);
}
}
}
void Manager::Private::beforeNotificationActivated(NotificationId id) {
clearNotification(id);
}
void Manager::Private::afterNotificationActivated(
NotificationId id,
not_null<Window::SessionController*> window) {
SetForegroundWindow(window->widget()->psHwnd());
}
void Manager::Private::clearNotification(NotificationId id) {
auto i = _notifications.find(id.contextId);
if (i != _notifications.cend()) {
i->second.remove(id.msgId);
if (i->second.empty()) {
_notifications.erase(i);
}
}
}
void Manager::Private::handleActivation(const ToastActivation &activation) {
const auto parsed = qthelp::url_parse_params(activation.args);
const auto pid = parsed.value("pid").toULong();
const auto my = GetCurrentProcessId();
if (pid != my) {
DEBUG_LOG(("Toast Info: "
"Got activation \"%1\", my %2, activating %3."
).arg(activation.args
).arg(my
).arg(pid));
const auto processId = pid;
const auto windowId = 0; // Activate some window.
Platform::ActivateOtherProcess(processId, windowId);
return;
}
const auto action = parsed.value("action");
const auto id = NotificationId{
.contextId = ContextId{
.sessionId = parsed.value("session").toULongLong(),
.peerId = PeerId(parsed.value("peer").toULongLong()),
.topicRootId = MsgId(parsed.value("topic").toLongLong()),
.monoforumPeerId = PeerId(
parsed.value("monoforumpeer").toULongLong()),
},
.msgId = MsgId(parsed.value("msg").toLongLong()),
};
if (!id.contextId.sessionId || !id.contextId.peerId || !id.msgId) {
DEBUG_LOG(("Toast Info: Got activation \"%1\", my %1, skipping."
).arg(activation.args
).arg(pid));
return;
}
DEBUG_LOG(("Toast Info: Got activation \"%1\", my %1, handling."
).arg(activation.args
).arg(pid));
auto text = TextWithTags();
for (const auto &entry : activation.input) {
if (entry.key == "fastReply") {
text.text = entry.value;
}
}
const auto i = _notifications.find(id.contextId);
if (i == _notifications.cend() || !i->second.contains(id.msgId)) {
return;
}
const auto manager = *_guarded;
if (action == "reply") {
manager->notificationReplied(id, text);
} else if (action == "mark") {
manager->notificationReplied(id, TextWithTags());
} else {
manager->notificationActivated(id, {
.draft = std::move(text),
});
}
}
bool Manager::Private::showNotification(
Info &&info,
Ui::PeerUserpicView &userpicView) {
if (!_notifier) {
return false;
}
return base::WinRT::Try([&] {
return showNotificationInTryCatch(std::move(info), userpicView);
}).value_or(false);
}
std::wstring Manager::Private::ensureSendButtonIcon() {
if (_sendButtonIconPath.empty()) {
const auto path = cWorkingDir() + u"tdata/temp/fast_reply.png"_q;
st::historySendIcon.instance(Qt::white, 300).save(path, "PNG");
_sendButtonIconPath = path.toStdWString();
}
return _sendButtonIconPath;
}
bool Manager::Private::showNotificationInTryCatch(
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView) {
const auto withSubtitle = !info.subtitle.isEmpty();
const auto peer = info.peer;
auto toastXml = XmlDocument();
const auto key = ContextId{
.sessionId = peer->session().uniqueId(),
.peerId = peer->id,
.topicRootId = info.topicRootId,
.monoforumPeerId = info.monoforumPeerId,
};
const auto notificationId = NotificationId{
.contextId = key,
.msgId = info.itemId,
};
const auto idString = u"pid=%1&session=%2&peer=%3&topic=%4&monoforumpeer=%5&msg=%6"_q
.arg(GetCurrentProcessId())
.arg(key.sessionId)
.arg(key.peerId.value)
.arg(info.topicRootId.bare)
.arg(info.monoforumPeerId.value)
.arg(info.itemId.bare);
const auto modern = Platform::IsWindows10OrGreater();
if (modern) {
toastXml.LoadXml(NotificationTemplate(idString, info.options));
} else {
toastXml = ToastNotificationManager::GetTemplateContent(
(withSubtitle
? ToastTemplateType::ToastImageAndText04
: ToastTemplateType::ToastImageAndText02));
SetAudioSilent(toastXml);
SetAction(toastXml, idString);
}
const auto userpicKey = info.options.hideNameAndPhoto
? InMemoryKey()
: peer->userpicUniqueKey(userpicView);
const auto userpicPath = _cachedUserpics.get(
userpicKey,
peer,
userpicView);
const auto userpicPathWide = QDir::toNativeSeparators(
userpicPath).toStdWString();
if (modern && !info.options.hideReplyButton) {
SetReplyIconSrc(toastXml, ensureSendButtonIcon());
SetReplyPlaceholder(
toastXml,
tr::lng_message_ph(tr::now).toStdWString());
}
if (modern && !info.options.hideMarkAsRead) {
SetMarkAsReadText(
toastXml,
tr::lng_context_mark_read(tr::now).toStdWString());
}
SetImageSrc(toastXml, userpicPathWide);
const auto nodeList = toastXml.GetElementsByTagName(L"text");
if (nodeList.Length() < (withSubtitle ? 3U : 2U)) {
return false;
}
SetNodeValueString(
toastXml,
nodeList.Item(0),
info.title.toStdWString());
if (withSubtitle) {
SetNodeValueString(
toastXml,
nodeList.Item(1),
info.subtitle.toStdWString());
}
SetNodeValueString(
toastXml,
nodeList.Item(withSubtitle ? 2 : 1),
info.message.toStdWString());
const auto weak = std::weak_ptr(_guarded);
const auto performOnMainQueue = [=](FnMut<void(Manager *manager)> task) {
crl::on_main(weak, [=, task = std::move(task)]() mutable {
task(*weak.lock());
});
};
auto toast = ToastNotification(toastXml);
const auto token1 = toast.Activated([=](
const ToastNotification &sender,
const winrt::Windows::Foundation::IInspectable &object) {
auto activation = ToastActivation();
const auto string = &ToastActivation::String;
if (const auto args = object.try_as<ToastActivatedEventArgs>()) {
activation.args = string(args.Arguments().c_str());
const auto args2 = args.try_as<IToastActivatedEventArgs2>();
if (!args2 && activation.args.startsWith("action=reply&")) {
LOG(("WinRT Error: "
"FastReply without IToastActivatedEventArgs2 support."));
return;
}
const auto input = args2 ? args2.UserInput() : nullptr;
const auto reply = input
? input.TryLookup(L"fastReply")
: nullptr;
const auto data = reply
? reply.try_as<IReference<winrt::hstring>>()
: nullptr;
if (data) {
activation.input.push_back({
.key = u"fastReply"_q,
.value = string(data.GetString().c_str()),
});
}
} else {
activation.args = "action=open&" + idString;
}
crl::on_main([=, activation = std::move(activation)]() mutable {
if (const auto strong = weak.lock()) {
(*strong)->handleActivation(activation);
}
});
});
const auto token2 = toast.Dismissed([=](
const ToastNotification &sender,
const ToastDismissedEventArgs &args) {
const auto reason = args.Reason();
switch (reason) {
case ToastDismissalReason::ApplicationHidden:
case ToastDismissalReason::TimedOut: // Went to Action Center.
break;
case ToastDismissalReason::UserCanceled:
default:
performOnMainQueue([notificationId](Manager *manager) {
manager->clearNotification(notificationId);
});
break;
}
});
const auto token3 = toast.Failed([=](
const ToastNotification &sender,
const ToastFailedEventArgs &args) {
performOnMainQueue([notificationId](Manager *manager) {
manager->clearNotification(notificationId);
});
});
auto i = _notifications.find(key);
if (i != _notifications.cend()) {
auto j = i->second.find(info.itemId);
if (j != i->second.end()) {
const auto existing = j->second;
i->second.erase(j);
tryHide(existing);
i = _notifications.find(key);
}
}
if (i == _notifications.cend()) {
i = _notifications.emplace(
key,
base::flat_map<MsgId, ToastNotification>()).first;
}
if (!base::WinRT::Try([&] { _notifier.Show(toast); })) {
i = _notifications.find(key);
if (i != _notifications.cend() && i->second.empty()) {
_notifications.erase(i);
}
return false;
}
i->second.emplace(info.itemId, toast);
return true;
}
void Manager::Private::tryHide(const ToastNotification &notification) {
base::WinRT::Try([&] {
_notifier.Hide(notification);
});
}
Manager::Manager(Window::Notifications::System *system)
: NativeManager(system)
, _private(std::make_unique<Private>(this)) {
}
bool Manager::init() {
return _private->init();
}
void Manager::clearNotification(NotificationId id) {
_private->clearNotification(id);
}
void Manager::handleActivation(const ToastActivation &activation) {
_private->handleActivation(activation);
}
Manager::~Manager() = default;
void Manager::doShowNativeNotification(
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView) {
_private->showNotification(std::move(info), userpicView);
}
void Manager::doClearAllFast() {
_private->clearAll();
}
void Manager::doClearFromItem(not_null<HistoryItem*> item) {
_private->clearFromItem(item);
}
void Manager::doClearFromTopic(not_null<Data::ForumTopic*> topic) {
_private->clearFromTopic(topic);
}
void Manager::doClearFromSublist(not_null<Data::SavedSublist*> sublist) {
_private->clearFromSublist(sublist);
}
void Manager::doClearFromHistory(not_null<History*> history) {
_private->clearFromHistory(history);
}
void Manager::doClearFromSession(not_null<Main::Session*> session) {
_private->clearFromSession(session);
}
void Manager::onBeforeNotificationActivated(NotificationId id) {
_private->beforeNotificationActivated(id);
}
void Manager::onAfterNotificationActivated(
NotificationId id,
not_null<Window::SessionController*> window) {
_private->afterNotificationActivated(id, window);
}
bool Manager::doSkipToast() const {
return false;
}
void Manager::doMaybePlaySound(Fn<void()> playSound) {
const auto skip = SkipSoundForCustom()
|| QuietHoursEnabled
|| FocusAssistBlocks;
if (!skip) {
playSound();
}
}
void Manager::doMaybeFlashBounce(Fn<void()> flashBounce) {
const auto skip = SkipFlashBounceForCustom()
|| QuietHoursEnabled
|| FocusAssistBlocks;
if (!skip) {
flashBounce();
}
}
} // namespace Notifications
} // namespace Platform

View File

@@ -0,0 +1,52 @@
/*
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 "platform/platform_notifications_manager.h"
struct ToastActivation;
namespace Platform {
namespace Notifications {
class Manager : public Window::Notifications::NativeManager {
public:
Manager(Window::Notifications::System *system);
~Manager();
bool init();
void clearNotification(NotificationId id);
void handleActivation(const ToastActivation &activation);
protected:
void doShowNativeNotification(
NotificationInfo &&info,
Ui::PeerUserpicView &userpicView) override;
void doClearAllFast() override;
void doClearFromItem(not_null<HistoryItem*> item) override;
void doClearFromTopic(not_null<Data::ForumTopic*> topic) override;
void doClearFromSublist(not_null<Data::SavedSublist*> sublist) override;
void doClearFromHistory(not_null<History*> history) override;
void doClearFromSession(not_null<Main::Session*> session) override;
void onBeforeNotificationActivated(NotificationId id) override;
void onAfterNotificationActivated(
NotificationId id,
not_null<Window::SessionController*> window) override;
bool doSkipToast() const override;
void doMaybePlaySound(Fn<void()> playSound) override;
void doMaybeFlashBounce(Fn<void()> flashBounce) override;
private:
class Private;
const std::unique_ptr<Private> _private;
};
} // namespace Notifications
} // namespace Platform

View File

@@ -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
namespace Platform {
inline std::unique_ptr<OverlayWidgetHelper> CreateOverlayWidgetHelper(
not_null<Ui::RpWindow*> window,
Fn<void(bool)> maximize) {
return std::make_unique<DefaultOverlayWidgetHelper>(
window,
std::move(maximize));
}
} // namespace Platform

View File

@@ -0,0 +1,728 @@
/*
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/win/specific_win.h"
#include "platform/win/main_window_win.h"
#include "platform/win/notifications_manager_win.h"
#include "platform/win/windows_app_user_model_id.h"
#include "platform/win/windows_dlls.h"
#include "platform/win/windows_autostart_task.h"
#include "base/platform/base_platform_info.h"
#include "base/platform/win/base_windows_co_task_mem.h"
#include "base/platform/win/base_windows_shlobj_h.h"
#include "base/platform/win/base_windows_winrt.h"
#include "base/call_delayed.h"
#include "ui/boxes/confirm_box.h"
#include "lang/lang_keys.h"
#include "mainwindow.h"
#include "mainwidget.h"
#include "history/history_location_manager.h"
#include "storage/localstorage.h"
#include "core/application.h"
#include "window/window_controller.h"
#include "core/crash_reports.h"
#include <QtCore/QOperatingSystemVersion>
#include <QtWidgets/QApplication>
#include <QtGui/QDesktopServices>
#include <QtGui/QWindow>
#include <Shobjidl.h>
#include <ShObjIdl_core.h>
#include <shellapi.h>
#include <openssl/conf.h>
#include <openssl/engine.h>
#include <openssl/err.h>
#include <dbghelp.h>
#include <Shlwapi.h>
#include <Strsafe.h>
#include <Windowsx.h>
#include <WtsApi32.h>
#include <SDKDDKVer.h>
#include <sal.h>
#include <Psapi.h>
#include <strsafe.h>
#include <ObjBase.h>
#include <propvarutil.h>
#include <functiondiscoverykeys.h>
#include <intsafe.h>
#include <guiddef.h>
#include <locale.h>
#include <ShellScalingApi.h>
#ifndef DCX_USESTYLE
#define DCX_USESTYLE 0x00010000
#endif
#ifndef WM_NCPOINTERUPDATE
#define WM_NCPOINTERUPDATE 0x0241
#define WM_NCPOINTERDOWN 0x0242
#define WM_NCPOINTERUP 0x0243
#endif
using namespace ::Platform;
namespace {
bool themeInited = false;
bool finished = true;
QMargins simpleMargins, margins;
HICON bigIcon = 0, smallIcon = 0, overlayIcon = 0;
[[nodiscard]] uint64 WindowIdFromHWND(HWND value) {
return (reinterpret_cast<uint64>(value) & 0xFFFFFFFFULL);
}
struct FindToActivateRequest {
uint64 processId = 0;
uint64 windowId = 0;
HWND result = nullptr;
uint32 resultLevel = 0; // Larger is better.
};
BOOL CALLBACK FindToActivate(HWND hwnd, LPARAM lParam) {
const auto request = reinterpret_cast<FindToActivateRequest*>(lParam);
DWORD dwProcessId;
::GetWindowThreadProcessId(hwnd, &dwProcessId);
if ((uint64)dwProcessId != request->processId) {
return TRUE;
}
// Found a Top-Level window.
if (WindowIdFromHWND(hwnd) == request->windowId) {
request->result = hwnd;
request->resultLevel = 3;
return FALSE;
}
const auto data = static_cast<uint32>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
if ((data != 1 && data != 2) || (data <= request->resultLevel)) {
return TRUE;
}
request->result = hwnd;
request->resultLevel = data;
return TRUE;
}
void DeleteMyModules() {
constexpr auto kMaxPathLong = 32767;
auto exePath = std::array<WCHAR, kMaxPathLong + 1>{ 0 };
const auto exeLength = GetModuleFileName(
nullptr,
exePath.data(),
kMaxPathLong + 1);
if (!exeLength || exeLength >= kMaxPathLong + 1) {
return;
}
const auto exe = std::wstring(exePath.data());
const auto last1 = exe.find_last_of('\\');
const auto last2 = exe.find_last_of('/');
const auto last = std::max(
(last1 == std::wstring::npos) ? -1 : int(last1),
(last2 == std::wstring::npos) ? -1 : int(last2));
if (last < 0) {
return;
}
const auto modules = exe.substr(0, last + 1) + L"modules";
const auto deleteOne = [&](const wchar_t *name, const wchar_t *arch) {
const auto path = modules + L'\\' + arch + L'\\' + name;
DeleteFile(path.c_str());
};
const auto deleteBoth = [&](const wchar_t *name) {
deleteOne(name, L"x86");
deleteOne(name, L"x64");
};
const auto removeOne = [&](const std::wstring &name) {
const auto path = modules + L'\\' + name;
RemoveDirectory(path.c_str());
};
const auto removeBoth = [&](const std::wstring &name) {
removeOne(L"x86\\" + name);
removeOne(L"x64\\" + name);
};
deleteBoth(L"d3d\\d3dcompiler_47.dll");
removeBoth(L"d3d");
removeOne(L"x86");
removeOne(L"x64");
RemoveDirectory(modules.c_str());
}
bool ManageAppLink(
bool create,
bool silent,
const GUID &folderId,
const wchar_t *args,
const wchar_t *description) {
if (cExeName().isEmpty()) {
return false;
}
PWSTR startupFolder;
HRESULT hr = SHGetKnownFolderPath(
folderId,
KF_FLAG_CREATE,
nullptr,
&startupFolder);
const auto guard = gsl::finally([&] {
CoTaskMemFree(startupFolder);
});
if (!SUCCEEDED(hr)) {
WCHAR buffer[64];
const auto size = base::array_size(buffer) - 1;
const auto length = StringFromGUID2(folderId, buffer, size);
if (length > 0 && length <= size) {
buffer[length] = 0;
if (!silent) LOG(("App Error: could not get %1 folder: %2").arg(buffer).arg(hr));
}
return false;
}
const auto lnk = QString::fromWCharArray(startupFolder)
+ '\\'
+ AppFile.utf16()
+ u".lnk"_q;
if (!create) {
QFile::remove(lnk);
return true;
}
const auto shellLink = base::WinRT::TryCreateInstance<IShellLink>(
CLSID_ShellLink);
if (!shellLink) {
if (!silent) LOG(("App Error: could not create instance of IID_IShellLink %1").arg(hr));
return false;
}
QString exe = QDir::toNativeSeparators(cExeDir() + cExeName()), dir = QDir::toNativeSeparators(QDir(cWorkingDir()).absolutePath());
shellLink->SetArguments(args);
shellLink->SetPath(exe.toStdWString().c_str());
shellLink->SetWorkingDirectory(dir.toStdWString().c_str());
shellLink->SetDescription(description);
if (const auto propertyStore = shellLink.try_as<IPropertyStore>()) {
PROPVARIANT appIdPropVar;
hr = InitPropVariantFromString(AppUserModelId::Id().c_str(), &appIdPropVar);
if (SUCCEEDED(hr)) {
hr = propertyStore->SetValue(AppUserModelId::Key(), appIdPropVar);
PropVariantClear(&appIdPropVar);
if (SUCCEEDED(hr)) {
hr = propertyStore->Commit();
}
}
}
const auto persistFile = shellLink.try_as<IPersistFile>();
if (!persistFile) {
if (!silent) LOG(("App Error: could not create interface IID_IPersistFile %1").arg(hr));
return false;
}
hr = persistFile->Save(lnk.toStdWString().c_str(), TRUE);
if (!SUCCEEDED(hr)) {
if (!silent) LOG(("App Error: could not save IPersistFile to path %1").arg(lnk));
return false;
}
return true;
}
} // namespace
QString psAppDataPath() {
static const int maxFileLen = MAX_PATH * 10;
WCHAR wstrPath[maxFileLen];
if (GetEnvironmentVariable(L"APPDATA", wstrPath, maxFileLen)) {
QDir appData(QString::fromStdWString(std::wstring(wstrPath)));
#ifdef OS_WIN_STORE
return appData.absolutePath() + u"/Telegram Desktop UWP/"_q;
#else // OS_WIN_STORE
return appData.absolutePath() + '/' + AppName.utf16() + '/';
#endif // OS_WIN_STORE
}
return QString();
}
QString psAppDataPathOld() {
static const int maxFileLen = MAX_PATH * 10;
WCHAR wstrPath[maxFileLen];
if (GetEnvironmentVariable(L"APPDATA", wstrPath, maxFileLen)) {
QDir appData(QString::fromStdWString(std::wstring(wstrPath)));
return appData.absolutePath() + '/' + AppNameOld.utf16() + '/';
}
return QString();
}
void psDoCleanup() {
try {
Platform::AutostartToggle(false);
psSendToMenu(false, true);
AppUserModelId::CleanupShortcut();
DeleteMyModules();
} catch (...) {
}
}
int psCleanup() {
__try
{
psDoCleanup();
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
return 0;
}
return 0;
}
void psDoFixPrevious() {
try {
static const int bufSize = 4096;
DWORD checkType = 0;
DWORD checkSize = bufSize * 2;
WCHAR checkStr[bufSize] = { 0 };
HKEY newKey1 = nullptr;
HKEY newKey2 = nullptr;
HKEY oldKey1 = nullptr;
HKEY oldKey2 = nullptr;
const auto appId = AppId.utf16();
const auto newKeyStr1 = QString("Software\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\%1_is1").arg(appId).toStdWString();
const auto newKeyStr2 = QString("Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\%1_is1").arg(appId).toStdWString();
const auto oldKeyStr1 = QString("SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\%1_is1").arg(appId).toStdWString();
const auto oldKeyStr2 = QString("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\%1_is1").arg(appId).toStdWString();
const auto newKeyRes1 = RegOpenKeyEx(HKEY_CURRENT_USER, newKeyStr1.c_str(), 0, KEY_READ, &newKey1);
const auto newKeyRes2 = RegOpenKeyEx(HKEY_CURRENT_USER, newKeyStr2.c_str(), 0, KEY_READ, &newKey2);
const auto oldKeyRes1 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, oldKeyStr1.c_str(), 0, KEY_READ, &oldKey1);
const auto oldKeyRes2 = RegOpenKeyEx(HKEY_LOCAL_MACHINE, oldKeyStr2.c_str(), 0, KEY_READ, &oldKey2);
const auto existNew1 = (newKeyRes1 == ERROR_SUCCESS) && (RegQueryValueEx(newKey1, L"InstallDate", 0, &checkType, (BYTE*)checkStr, &checkSize) == ERROR_SUCCESS); checkSize = bufSize * 2;
const auto existNew2 = (newKeyRes2 == ERROR_SUCCESS) && (RegQueryValueEx(newKey2, L"InstallDate", 0, &checkType, (BYTE*)checkStr, &checkSize) == ERROR_SUCCESS); checkSize = bufSize * 2;
const auto existOld1 = (oldKeyRes1 == ERROR_SUCCESS) && (RegQueryValueEx(oldKey1, L"InstallDate", 0, &checkType, (BYTE*)checkStr, &checkSize) == ERROR_SUCCESS); checkSize = bufSize * 2;
const auto existOld2 = (oldKeyRes2 == ERROR_SUCCESS) && (RegQueryValueEx(oldKey2, L"InstallDate", 0, &checkType, (BYTE*)checkStr, &checkSize) == ERROR_SUCCESS); checkSize = bufSize * 2;
if (newKeyRes1 == ERROR_SUCCESS) RegCloseKey(newKey1);
if (newKeyRes2 == ERROR_SUCCESS) RegCloseKey(newKey2);
if (oldKeyRes1 == ERROR_SUCCESS) RegCloseKey(oldKey1);
if (oldKeyRes2 == ERROR_SUCCESS) RegCloseKey(oldKey2);
if (existNew1 || existNew2) {
if (existOld1) RegDeleteKey(HKEY_LOCAL_MACHINE, oldKeyStr1.c_str());
if (existOld2) RegDeleteKey(HKEY_LOCAL_MACHINE, oldKeyStr2.c_str());
}
QString userDesktopLnk, commonDesktopLnk;
WCHAR userDesktopFolder[MAX_PATH], commonDesktopFolder[MAX_PATH];
HRESULT userDesktopRes = SHGetFolderPath(0, CSIDL_DESKTOPDIRECTORY, 0, SHGFP_TYPE_CURRENT, userDesktopFolder);
HRESULT commonDesktopRes = SHGetFolderPath(0, CSIDL_COMMON_DESKTOPDIRECTORY, 0, SHGFP_TYPE_CURRENT, commonDesktopFolder);
if (SUCCEEDED(userDesktopRes)) {
userDesktopLnk = QString::fromWCharArray(userDesktopFolder) + "\\Telegram.lnk";
}
if (SUCCEEDED(commonDesktopRes)) {
commonDesktopLnk = QString::fromWCharArray(commonDesktopFolder) + "\\Telegram.lnk";
}
QFile userDesktopFile(userDesktopLnk), commonDesktopFile(commonDesktopLnk);
if (QFile::exists(userDesktopLnk) && QFile::exists(commonDesktopLnk) && userDesktopLnk != commonDesktopLnk) {
QFile::remove(commonDesktopLnk);
}
} catch (...) {
}
}
int psFixPrevious() {
__try
{
psDoFixPrevious();
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
return 0;
}
return 0;
}
namespace Platform {
namespace ThirdParty {
namespace {
void StartOpenSSL() {
// Don't use dynamic OpenSSL config, it can load unwanted DLLs.
OPENSSL_load_builtin_modules();
ENGINE_load_builtin_engines();
ERR_clear_error();
OPENSSL_no_config();
}
} // namespace
void start() {
StartOpenSSL();
Dlls::CheckLoadedModules();
}
} // namespace ThirdParty
void start() {
const auto supported = base::WinRT::Supported();
LOG(("WinRT Supported: %1").arg(Logs::b(supported)));
// https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/setlocale-wsetlocale#utf-8-support
setlocale(LC_ALL, ".UTF8");
const auto appUserModelId = AppUserModelId::Id();
SetCurrentProcessExplicitAppUserModelID(appUserModelId.c_str());
LOG(("AppUserModelID: %1").arg(appUserModelId));
}
void finish() {
}
void SetApplicationIcon(const QIcon &icon) {
QApplication::setWindowIcon(icon);
}
QString SingleInstanceLocalServerName(const QString &hash) {
return u"Global\\"_q + hash + '-' + cGUIDStr();
}
#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
std::optional<bool> IsDarkMode() {
static const auto kSystemVersion = QOperatingSystemVersion::current();
static const auto kDarkModeAddedVersion = QOperatingSystemVersion(
QOperatingSystemVersion::Windows,
10,
0,
17763);
static const auto kSupported = (kSystemVersion >= kDarkModeAddedVersion);
if (!kSupported) {
return std::nullopt;
}
HIGHCONTRAST hcf = {};
hcf.cbSize = static_cast<UINT>(sizeof(HIGHCONTRAST));
if (SystemParametersInfo(SPI_GETHIGHCONTRAST, hcf.cbSize, &hcf, FALSE)
&& (hcf.dwFlags & HCF_HIGHCONTRASTON)) {
return std::nullopt;
}
const auto keyName = L""
"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
const auto valueName = L"AppsUseLightTheme";
auto key = HKEY();
auto result = RegOpenKeyEx(HKEY_CURRENT_USER, keyName, 0, KEY_READ, &key);
if (result != ERROR_SUCCESS) {
return std::nullopt;
}
DWORD value = 0, type = 0, size = sizeof(value);
result = RegQueryValueEx(key, valueName, 0, &type, (LPBYTE)&value, &size);
RegCloseKey(key);
if (result != ERROR_SUCCESS) {
return std::nullopt;
}
return (value == 0);
}
#endif // Qt < 6.5.0
bool AutostartSupported() {
return true;
}
void AutostartRequestStateFromSystem(Fn<void(bool)> callback) {
#ifdef OS_WIN_STORE
AutostartTask::RequestState([=](bool enabled) {
crl::on_main([=] {
callback(enabled);
});
});
#endif // OS_WIN_STORE
}
void AutostartToggle(bool enabled, Fn<void(bool)> done) {
#ifdef OS_WIN_STORE
const auto requested = enabled;
const auto callback = [=](bool enabled) { crl::on_main([=] {
if (!Core::IsAppLaunched()) {
return;
}
done(enabled);
if (!requested || enabled) {
return;
} else if (const auto window = Core::App().activeWindow()) {
window->show(Ui::MakeConfirmBox({
.text = tr::lng_settings_auto_start_disabled_uwp(),
.confirmed = [](Fn<void()> close) {
AutostartTask::OpenSettings();
close();
},
.confirmText = tr::lng_settings_open_system_settings(),
}));
}
}); };
AutostartTask::Toggle(
enabled,
done ? Fn<void(bool)>(callback) : nullptr);
#else // OS_WIN_STORE
const auto silent = !done;
const auto success = ManageAppLink(
enabled,
silent,
FOLDERID_Startup,
L"-autostart",
L"Telegram autorun link.\n"
"You can disable autorun in Telegram settings.");
if (done) {
done(enabled && success);
}
#endif // OS_WIN_STORE
}
bool AutostartSkip() {
#ifdef OS_WIN_STORE
return false;
#else // OS_WIN_STORE
return !cAutoStart();
#endif // OS_WIN_STORE
}
void WriteCrashDumpDetails() {
#ifndef TDESKTOP_DISABLE_CRASH_REPORTS
PROCESS_MEMORY_COUNTERS data = { 0 };
if (Dlls::GetProcessMemoryInfo
&& Dlls::GetProcessMemoryInfo(
GetCurrentProcess(),
&data,
sizeof(data))) {
const auto mb = 1024 * 1024;
CrashReports::dump()
<< "Memory-usage: "
<< (data.PeakWorkingSetSize / mb)
<< " MB (peak), "
<< (data.WorkingSetSize / mb)
<< " MB (current)\n";
CrashReports::dump()
<< "Pagefile-usage: "
<< (data.PeakPagefileUsage / mb)
<< " MB (peak), "
<< (data.PagefileUsage / mb)
<< " MB (current)\n";
}
#endif // TDESKTOP_DISABLE_CRASH_REPORTS
}
void SetWindowPriority(not_null<QWidget*> window, uint32 priority) {
const auto hwnd = reinterpret_cast<HWND>(window->winId());
Assert(hwnd != nullptr);
SetWindowLongPtr(hwnd, GWLP_USERDATA, static_cast<LONG_PTR>(priority));
}
uint64 ActivationWindowId(not_null<QWidget*> window) {
return WindowIdFromHWND(reinterpret_cast<HWND>(window->winId()));
}
void ActivateOtherProcess(uint64 processId, uint64 windowId) {
auto request = FindToActivateRequest{
.processId = processId,
.windowId = windowId,
};
::EnumWindows((WNDENUMPROC)FindToActivate, (LPARAM)&request);
if (const auto hwnd = request.result) {
::SetForegroundWindow(hwnd);
::SetFocus(hwnd);
}
}
} // namespace Platform
namespace {
void _psLogError(const char *str, LSTATUS code) {
LPWSTR errorTextFormatted = nullptr;
auto formatFlags = FORMAT_MESSAGE_FROM_SYSTEM
| FORMAT_MESSAGE_ALLOCATE_BUFFER
| FORMAT_MESSAGE_IGNORE_INSERTS;
FormatMessage(
formatFlags,
NULL,
code,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&errorTextFormatted,
0,
0);
auto errorText = errorTextFormatted
? errorTextFormatted
: L"(Unknown error)";
LOG((str).arg(code).arg(QString::fromStdWString(errorText)));
LocalFree(errorTextFormatted);
}
bool _psOpenRegKey(LPCWSTR key, PHKEY rkey) {
DEBUG_LOG(("App Info: opening reg key %1...").arg(QString::fromStdWString(key)));
LSTATUS status = RegOpenKeyEx(HKEY_CURRENT_USER, key, 0, KEY_QUERY_VALUE | KEY_WRITE, rkey);
if (status != ERROR_SUCCESS) {
if (status == ERROR_FILE_NOT_FOUND) {
status = RegCreateKeyEx(HKEY_CURRENT_USER, key, 0, 0, REG_OPTION_NON_VOLATILE, KEY_QUERY_VALUE | KEY_WRITE, 0, rkey, 0);
if (status != ERROR_SUCCESS) {
QString msg = u"App Error: could not create '%1' registry key, error %2"_q.arg(QString::fromStdWString(key)).arg(u"%1: %2"_q);
_psLogError(msg.toUtf8().constData(), status);
return false;
}
} else {
QString msg = u"App Error: could not open '%1' registry key, error %2"_q.arg(QString::fromStdWString(key)).arg(u"%1: %2"_q);
_psLogError(msg.toUtf8().constData(), status);
return false;
}
}
return true;
}
bool _psSetKeyValue(HKEY rkey, LPCWSTR value, QString v) {
static const int bufSize = 4096;
DWORD defaultType, defaultSize = bufSize * 2;
WCHAR defaultStr[bufSize] = { 0 };
if (RegQueryValueEx(rkey, value, 0, &defaultType, (BYTE*)defaultStr, &defaultSize) != ERROR_SUCCESS || defaultType != REG_SZ || defaultSize != (v.size() + 1) * 2 || QString::fromStdWString(defaultStr) != v) {
WCHAR tmp[bufSize] = { 0 };
if (!v.isEmpty()) StringCbPrintf(tmp, bufSize, v.replace(QChar('%'), u"%%"_q).toStdWString().c_str());
LSTATUS status = RegSetValueEx(rkey, value, 0, REG_SZ, (BYTE*)tmp, (wcslen(tmp) + 1) * sizeof(WCHAR));
if (status != ERROR_SUCCESS) {
QString msg = u"App Error: could not set %1, error %2"_q.arg(value ? ('\'' + QString::fromStdWString(value) + '\'') : u"(Default)"_q).arg("%1: %2");
_psLogError(msg.toUtf8().constData(), status);
return false;
}
}
return true;
}
}
namespace Platform {
PermissionStatus GetPermissionStatus(PermissionType type) {
if (type == PermissionType::Microphone) {
PermissionStatus result = PermissionStatus::Granted;
HKEY hKey;
LSTATUS res = RegOpenKeyEx(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\CapabilityAccessManager\\ConsentStore\\microphone", 0, KEY_QUERY_VALUE, &hKey);
if (res == ERROR_SUCCESS) {
wchar_t buf[20];
DWORD length = sizeof(buf);
res = RegQueryValueEx(hKey, L"Value", NULL, NULL, (LPBYTE)buf, &length);
if (res == ERROR_SUCCESS) {
if (wcscmp(buf, L"Deny") == 0) {
result = PermissionStatus::Denied;
}
}
RegCloseKey(hKey);
}
return result;
}
return PermissionStatus::Granted;
}
void RequestPermission(PermissionType type, Fn<void(PermissionStatus)> resultCallback) {
resultCallback(PermissionStatus::Granted);
}
void OpenSystemSettingsForPermission(PermissionType type) {
if (type == PermissionType::Microphone) {
crl::on_main([] {
ShellExecute(
nullptr,
L"open",
L"ms-settings:privacy-microphone",
nullptr,
nullptr,
SW_SHOWDEFAULT);
});
}
}
bool OpenSystemSettings(SystemSettingsType type) {
if (type == SystemSettingsType::Audio) {
crl::on_main([] {
WinExec("control.exe mmsys.cpl", SW_SHOW);
//QDesktopServices::openUrl(QUrl("ms-settings:sound"));
});
}
return true;
}
void NewVersionLaunched(int oldVersion) {
if (oldVersion <= 4009009) {
AppUserModelId::CheckPinned();
}
if (oldVersion > 0 && oldVersion < 2008012) {
// Reset icons cache, because we've changed the application icon.
if (Dlls::SHChangeNotify) {
Dlls::SHChangeNotify(
SHCNE_ASSOCCHANGED,
SHCNF_IDLIST,
nullptr,
nullptr);
}
}
}
QImage DefaultApplicationIcon() {
return Window::Logo();
}
void LaunchMaps(const Data::LocationPoint &point, Fn<void()> fail) {
const auto aar = base::WinRT::TryCreateInstance<
IApplicationAssociationRegistration
>(CLSID_ApplicationAssociationRegistration);
if (!aar) {
fail();
return;
}
auto handler = base::CoTaskMemString();
const auto result = aar->QueryCurrentDefault(
L"bingmaps",
AT_URLPROTOCOL,
AL_EFFECTIVE,
handler.put());
if (FAILED(result)
|| !handler
|| !handler.data()
|| std::wstring(handler.data()) == L"bingmaps") {
fail();
return;
}
const auto url = u"bingmaps:?lvl=16&collection=point.%1_%2_Point"_q;
if (!QDesktopServices::openUrl(
url.arg(point.latAsString(), point.lonAsString()))) {
fail();
}
}
} // namespace Platform
void psSendToMenu(bool send, bool silent) {
ManageAppLink(
send,
silent,
FOLDERID_SendTo,
L"--",
L"Telegram send to link.\n"
"You can disable send to menu item in Telegram settings.");
}
// Stub while we still support Windows 7.
extern "C" {
STDAPI GetDpiForMonitor(
_In_ HMONITOR hmonitor,
_In_ MONITOR_DPI_TYPE dpiType,
_Out_ UINT *dpiX,
_Out_ UINT *dpiY) {
return Dlls::GetDpiForMonitor
? Dlls::GetDpiForMonitor(hmonitor, dpiType, dpiX, dpiY)
: E_FAIL;
}
} // extern "C"

View File

@@ -0,0 +1,71 @@
/*
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 "platform/platform_specific.h"
#include <windows.h>
namespace Platform {
inline void IgnoreApplicationActivationRightNow() {
}
inline bool TrayIconSupported() {
return true;
}
inline bool SkipTaskbarSupported() {
return true;
}
inline bool PreventsQuit(Core::QuitReason reason) {
return false;
}
inline void ActivateThisProcess() {
}
// 1 - secondary, 2 - primary.
void SetWindowPriority(not_null<QWidget*> window, uint32 priority);
[[nodiscard]] uint64 ActivationWindowId(not_null<QWidget*> window);
// Activate window with windowId (if found) or the largest priority.
void ActivateOtherProcess(uint64 processId, uint64 windowId);
inline QString ApplicationIconName() {
return {};
}
inline QString ExecutablePathForShortcuts() {
return cExeDir() + cExeName();
}
namespace ThirdParty {
void start();
} // namespace ThirdParty
} // namespace Platform
inline void psCheckLocalSocket(const QString &) {
}
QString psAppDataPath();
QString psAppDataPathOld();
void psSendToMenu(bool send, bool silent = false);
int psCleanup();
int psFixPrevious();
inline QByteArray psDownloadPathBookmark(const QString &path) {
return QByteArray();
}
inline void psDownloadPathEnableAccess() {
}

View File

@@ -0,0 +1,24 @@
/*
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 "platform/platform_text_recognition.h"
namespace Platform {
namespace TextRecognition {
inline bool IsAvailable() {
return false;
}
inline Result RecognizeText(const QImage &image) {
return {};
}
} // namespace TextRecognition
} // namespace Platform

View File

@@ -0,0 +1,448 @@
/*
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/win/tray_win.h"
#include "base/invoke_queued.h"
#include "base/qt_signal_producer.h"
#include "core/application.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "storage/localstorage.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "ui/widgets/popup_menu.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "styles/style_window.h"
#include <qpa/qplatformscreen.h>
#include <qpa/qplatformsystemtrayicon.h>
#include <qpa/qplatformtheme.h>
#include <private/qguiapplication_p.h>
#include <private/qhighdpiscaling_p.h>
#include <QSvgRenderer>
#include <QBuffer>
namespace Platform {
namespace {
constexpr auto kTooltipDelay = crl::time(10000);
std::optional<bool> DarkTaskbar;
bool DarkTasbarValueValid/* = false*/;
[[nodiscard]] std::optional<bool> ReadDarkTaskbarValue() {
const auto keyName = L""
"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
const auto valueName = L"SystemUsesLightTheme";
auto key = HKEY();
auto result = RegOpenKeyEx(HKEY_CURRENT_USER, keyName, 0, KEY_READ, &key);
if (result != ERROR_SUCCESS) {
return std::nullopt;
}
DWORD value = 0, type = 0, size = sizeof(value);
result = RegQueryValueEx(key, valueName, 0, &type, (LPBYTE)&value, &size);
RegCloseKey(key);
if (result != ERROR_SUCCESS) {
return std::nullopt;
}
return (value == 0);
}
[[nodiscard]] std::optional<bool> IsDarkTaskbar() {
static const auto kSystemVersion = QOperatingSystemVersion::current();
static const auto kDarkModeAddedVersion = QOperatingSystemVersion(
QOperatingSystemVersion::Windows,
10,
0,
18282);
static const auto kSupported = (kSystemVersion >= kDarkModeAddedVersion);
if (!kSupported) {
return std::nullopt;
} else if (!DarkTasbarValueValid) {
DarkTasbarValueValid = true;
DarkTaskbar = ReadDarkTaskbarValue();
}
return DarkTaskbar;
}
[[nodiscard]] QImage MonochromeIconFor(int size, bool darkMode) {
Expects(size > 0);
static const auto Content = [&] {
auto f = QFile(u":/gui/icons/tray/monochrome.svg"_q);
return f.open(QIODevice::ReadOnly) ? f.readAll() : QByteArray();
}();
static auto Mask = QImage();
static auto Size = 0;
if (Mask.isNull() || Size != size) {
Size = size;
Mask = QImage(size, size, QImage::Format_ARGB32_Premultiplied);
Mask.fill(Qt::transparent);
auto p = QPainter(&Mask);
QSvgRenderer(Content).render(&p, QRectF(0, 0, size, size));
}
static auto Colored = QImage();
static auto ColoredDark = QImage();
auto &use = darkMode ? ColoredDark : Colored;
if (use.size() != Mask.size()) {
const auto color = darkMode ? 255 : 0;
const auto alpha = darkMode ? 255 : 228;
use = style::colorizeImage(Mask, { color, color, color, alpha });
}
return use;
}
[[nodiscard]] QImage MonochromeWithDot(QImage image, style::color color) {
auto p = QPainter(&image);
auto hq = PainterHighQualityEnabler(p);
const auto xm = image.width() / 16.;
const auto ym = image.height() / 16.;
p.setBrush(color);
p.setPen(Qt::NoPen);
p.drawEllipse(QRectF( // cx=3.9, cy=12.7, r=2.2
1.7 * xm,
10.5 * ym,
4.4 * xm,
4.4 * ym));
return image;
}
[[nodiscard]] QImage ImageIconWithCounter(
Window::CounterLayerArgs &&args,
bool supportMode,
bool smallIcon,
bool monochrome) {
static auto ScaledLogo = base::flat_map<int, QImage>();
static auto ScaledLogoNoMargin = base::flat_map<int, QImage>();
static auto ScaledLogoDark = base::flat_map<int, QImage>();
static auto ScaledLogoLight = base::flat_map<int, QImage>();
const auto darkMode = IsDarkTaskbar();
auto &scaled = (monochrome && darkMode)
? (*darkMode
? ScaledLogoDark
: ScaledLogoLight)
: smallIcon
? ScaledLogoNoMargin
: ScaledLogo;
auto result = [&] {
if (const auto it = scaled.find(args.size); it != scaled.end()) {
return it->second;
} else if (monochrome && darkMode) {
return MonochromeIconFor(args.size, *darkMode);
}
return scaled.emplace(
args.size,
(smallIcon
? Window::LogoNoMargin()
: Window::Logo()
).scaledToWidth(args.size, Qt::SmoothTransformation)
).first->second;
}();
if ((!monochrome || !darkMode) && supportMode) {
Window::ConvertIconToBlack(result);
}
if (!args.count) {
return result;
} else if (smallIcon) {
if (monochrome && darkMode) {
return MonochromeWithDot(std::move(result), args.bg);
}
return Window::WithSmallCounter(std::move(result), std::move(args));
}
QPainter p(&result);
const auto half = args.size / 2;
args.size = half;
p.drawPixmap(
half,
half,
Ui::PixmapFromImage(Window::GenerateCounterLayer(std::move(args))));
return result;
}
} // namespace
Tray::Tray() {
}
void Tray::createIcon() {
if (!_icon) {
if (const auto theme = QGuiApplicationPrivate::platformTheme()) {
_icon.reset(theme->createPlatformSystemTrayIcon());
}
if (!_icon) {
return;
}
_icon->init();
updateIcon();
_icon->updateToolTip(AppName.utf16());
using Reason = QPlatformSystemTrayIcon::ActivationReason;
base::qt_signal_producer(
_icon.get(),
&QPlatformSystemTrayIcon::activated
) | rpl::filter(
rpl::mappers::_1 != Reason::Context
) | rpl::map_to(
rpl::empty
) | rpl::start_to_stream(_iconClicks, _lifetime);
base::qt_signal_producer(
_icon.get(),
&QPlatformSystemTrayIcon::contextMenuRequested
) | rpl::filter([=] {
return _menu != nullptr;
}) | rpl::on_next([=](
QPoint globalNativePosition,
const QPlatformScreen *screen) {
_aboutToShowRequests.fire({});
const auto position = QHighDpi::fromNativePixels(
globalNativePosition,
screen ? screen->screen() : nullptr);
InvokeQueued(_menu.get(), [=] {
_menu->popup(position);
});
}, _lifetime);
} else {
updateIcon();
}
}
void Tray::destroyIcon() {
_icon = nullptr;
}
void Tray::updateIcon() {
if (!_icon) {
return;
}
const auto controller = Core::App().activePrimaryWindow();
const auto session = !controller
? nullptr
: !controller->sessionController()
? nullptr
: &controller->sessionController()->session();
// Force Qt to use right icon size, not the larger one.
QIcon forTrayIcon;
forTrayIcon.addPixmap(
Tray::IconWithCounter(
CounterLayerArgs(
GetSystemMetrics(SM_CXSMICON),
Core::App().unreadBadge(),
Core::App().unreadBadgeMuted()),
true,
Core::App().settings().trayIconMonochrome(),
session && session->supportMode()));
_icon->updateIcon(forTrayIcon);
}
void Tray::createMenu() {
if (!_menu) {
_menu = base::make_unique_q<Ui::PopupMenu>(nullptr);
_menu->deleteOnHide(false);
}
}
void Tray::destroyMenu() {
_menu = nullptr;
_actionsLifetime.destroy();
}
void Tray::addAction(rpl::producer<QString> text, Fn<void()> &&callback) {
if (!_menu) {
return;
}
// If we try to activate() window before the _menu is hidden,
// then the window will be shown in semi-active state (Qt bug).
// It will receive input events, but it will be rendered as inactive.
auto callbackLater = crl::guard(_menu.get(), [=] {
using namespace rpl::mappers;
_callbackFromTrayLifetime = _menu->shownValue(
) | rpl::filter(!_1) | rpl::take(1) | rpl::on_next([=] {
callback();
});
});
const auto action = _menu->addAction(QString(), std::move(callbackLater));
std::move(
text
) | rpl::on_next([=](const QString &text) {
action->setText(text);
}, _actionsLifetime);
}
void Tray::showTrayMessage() const {
if (!cSeenTrayTooltip() && _icon) {
_icon->showMessage(
AppName.utf16(),
tr::lng_tray_icon_text(tr::now),
QIcon(),
QPlatformSystemTrayIcon::Information,
kTooltipDelay);
cSetSeenTrayTooltip(true);
Local::writeSettings();
}
}
bool Tray::hasTrayMessageSupport() const {
return !cSeenTrayTooltip();
}
rpl::producer<> Tray::aboutToShowRequests() const {
return _aboutToShowRequests.events();
}
rpl::producer<> Tray::showFromTrayRequests() const {
return rpl::never<>();
}
rpl::producer<> Tray::hideToTrayRequests() const {
return rpl::never<>();
}
rpl::producer<> Tray::iconClicks() const {
return _iconClicks.events();
}
bool Tray::hasIcon() const {
return _icon;
}
rpl::lifetime &Tray::lifetime() {
return _lifetime;
}
Window::CounterLayerArgs Tray::CounterLayerArgs(
int size,
int counter,
bool muted) {
return Window::CounterLayerArgs{
.size = size,
.count = counter,
.bg = muted ? st::trayCounterBgMute : st::trayCounterBg,
.fg = st::trayCounterFg,
};
}
QPixmap Tray::IconWithCounter(
Window::CounterLayerArgs &&args,
bool smallIcon,
bool monochrome,
bool supportMode) {
return Ui::PixmapFromImage(ImageIconWithCounter(
std::move(args),
supportMode,
smallIcon,
monochrome));
}
void WriteIco(const QString &path, std::vector<QImage> images) {
Expects(!images.empty());
auto buffer = QByteArray();
const auto write = [&](auto value) {
buffer.append(reinterpret_cast<const char*>(&value), sizeof(value));
};
const auto count = int(images.size());
auto full = 0;
auto pngs = std::vector<QByteArray>();
pngs.reserve(count);
for (const auto &image : images) {
pngs.emplace_back();
{
auto buffer = QBuffer(&pngs.back());
image.save(&buffer, "PNG");
}
full += pngs.back().size();
}
// Images directory
constexpr auto entry = sizeof(int8)
+ sizeof(int8)
+ sizeof(int8)
+ sizeof(int8)
+ sizeof(int16)
+ sizeof(int16)
+ sizeof(uint32)
+ sizeof(uint32);
static_assert(entry == 16);
auto offset = 3 * sizeof(int16) + count * entry;
full += offset;
buffer.reserve(full);
// Thanks https://stackoverflow.com/a/54289564/6509833
write(int16(0));
write(int16(1));
write(int16(count));
for (auto i = 0; i != count; ++i) {
const auto &image = images[i];
Assert(image.width() <= 256 && image.height() <= 256);
write(int8(image.width() == 256 ? 0 : image.width()));
write(int8(image.height() == 256 ? 0 : image.height()));
write(int8(0)); // palette size
write(int8(0)); // reserved
write(int16(1)); // color planes
write(int16(image.depth())); // bits-per-pixel
write(uint32(pngs[i].size())); // size of image in bytes
write(uint32(offset)); // offset
offset += pngs[i].size();
}
for (auto i = 0; i != count; ++i) {
buffer.append(pngs[i]);
}
const auto dir = QFileInfo(path).dir();
dir.mkpath(dir.absolutePath());
auto f = QFile(path);
if (f.open(QIODevice::WriteOnly)) {
f.write(buffer);
}
}
QString Tray::QuitJumpListIconPath() {
const auto dark = IsDarkTaskbar();
const auto key = !dark ? 0 : *dark ? 1 : 2;
const auto path = cWorkingDir() + u"tdata/temp/quit_%1.ico"_q.arg(key);
if (QFile::exists(path)) {
return path;
}
const auto color = !dark
? st::trayCounterBg->c
: *dark
? QColor(255, 255, 255)
: QColor(0, 0, 0, 228);
WriteIco(path, {
st::winQuitIcon.instance(color, 100, true),
st::winQuitIcon.instance(color, 200, true),
st::winQuitIcon.instance(color, 300, true),
});
return path;
}
bool HasMonochromeSetting() {
return IsDarkTaskbar().has_value();
}
void RefreshTaskbarThemeValue() {
DarkTasbarValueValid = false;
}
} // namespace Platform

View File

@@ -0,0 +1,79 @@
/*
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 "platform/platform_tray.h"
#include "base/unique_qptr.h"
namespace Window {
struct CounterLayerArgs;
} // namespace Window
namespace Ui {
class PopupMenu;
} // namespace Ui
class QPlatformSystemTrayIcon;
namespace Platform {
class Tray final {
public:
Tray();
[[nodiscard]] rpl::producer<> aboutToShowRequests() const;
[[nodiscard]] rpl::producer<> showFromTrayRequests() const;
[[nodiscard]] rpl::producer<> hideToTrayRequests() const;
[[nodiscard]] rpl::producer<> iconClicks() const;
[[nodiscard]] bool hasIcon() const;
void createIcon();
void destroyIcon();
void updateIcon();
void createMenu();
void destroyMenu();
void addAction(rpl::producer<QString> text, Fn<void()> &&callback);
void showTrayMessage() const;
[[nodiscard]] bool hasTrayMessageSupport() const;
[[nodiscard]] rpl::lifetime &lifetime();
// Windows only.
[[nodiscard]] static Window::CounterLayerArgs CounterLayerArgs(
int size,
int counter,
bool muted);
[[nodiscard]] static QPixmap IconWithCounter(
Window::CounterLayerArgs &&args,
bool smallIcon,
bool monochrome,
bool supportMode);
[[nodiscard]] static QString QuitJumpListIconPath();
private:
base::unique_qptr<QPlatformSystemTrayIcon> _icon;
base::unique_qptr<Ui::PopupMenu> _menu;
rpl::event_stream<> _iconClicks;
rpl::event_stream<> _aboutToShowRequests;
rpl::lifetime _callbackFromTrayLifetime;
rpl::lifetime _actionsLifetime;
rpl::lifetime _lifetime;
};
void RefreshTaskbarThemeValue();
} // namespace Platform

View File

@@ -0,0 +1,297 @@
/*
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/platform_webauthn.h"
#include "base/platform/win/base_windows_safe_library.h"
#include "data/data_passkey_deserialize.h"
#include <windows.h>
#include <combaseapi.h>
#include <webauthn.h>
#include <QWindow>
#include <QGuiApplication>
namespace Platform::WebAuthn {
namespace {
HRESULT(__stdcall *WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable)(
BOOL *pbIsUserVerifyingPlatformAuthenticatorAvailable);
HRESULT(__stdcall *WebAuthNAuthenticatorMakeCredential)(
HWND hWnd,
PCWEBAUTHN_RP_ENTITY_INFORMATION pRpInformation,
PCWEBAUTHN_USER_ENTITY_INFORMATION pUserInformation,
PCWEBAUTHN_COSE_CREDENTIAL_PARAMETERS pPubKeyCredParams,
PCWEBAUTHN_CLIENT_DATA pWebAuthNClientData,
PCWEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS
pWebAuthNCredentialOptions,
PWEBAUTHN_CREDENTIAL_ATTESTATION *ppWebAuthNCredentialAttestation);
void(__stdcall *WebAuthNFreeCredentialAttestation)(
PWEBAUTHN_CREDENTIAL_ATTESTATION pWebAuthNCredentialAttestation);
HRESULT(__stdcall *WebAuthNAuthenticatorGetAssertion)(
HWND hWnd,
LPCWSTR pwszRpId,
PCWEBAUTHN_CLIENT_DATA pWebAuthNClientData,
PCWEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS
pWebAuthNGetAssertionOptions,
PWEBAUTHN_ASSERTION *ppWebAuthNAssertion);
void(__stdcall *WebAuthNFreeAssertion)(
PWEBAUTHN_ASSERTION pWebAuthNAssertion);
[[nodiscard]] bool Resolve() {
const auto webauthn = base::Platform::SafeLoadLibrary(L"webauthn.dll");
if (!webauthn) {
return false;
}
auto total = 0, resolved = 0;
#define LOAD_SYMBOL(name) \
++total; \
if (base::Platform::LoadMethod(webauthn, #name, name)) ++resolved;
LOAD_SYMBOL(WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable);
LOAD_SYMBOL(WebAuthNAuthenticatorMakeCredential);
LOAD_SYMBOL(WebAuthNFreeCredentialAttestation);
LOAD_SYMBOL(WebAuthNAuthenticatorGetAssertion);
LOAD_SYMBOL(WebAuthNFreeAssertion);
#undef LOAD_SYMBOL
return (total == resolved);
}
[[nodiscard]] bool Supported() {
static const auto Result = Resolve();
return Result;
}
} // namespace
bool IsSupported() {
if (!Supported()) {
return false;
}
auto available = (BOOL)(FALSE);
return SUCCEEDED(
WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable(&available))
&& available;
}
void RegisterKey(
const Data::Passkey::RegisterData &data,
Fn<void(RegisterResult result)> callback) {
if (!Supported()) {
callback({});
return;
}
auto rpId = data.rp.id.toStdWString();
auto rpName = data.rp.name.toStdWString();
auto userName = data.user.name.toStdWString();
auto userDisplayName = data.user.displayName.toStdWString();
auto rpInfo = WEBAUTHN_RP_ENTITY_INFORMATION();
memset(&rpInfo, 0, sizeof(rpInfo));
rpInfo.dwVersion =
WEBAUTHN_RP_ENTITY_INFORMATION_CURRENT_VERSION;
rpInfo.pwszId = rpId.c_str();
rpInfo.pwszName = rpName.c_str();
auto userInfo = WEBAUTHN_USER_ENTITY_INFORMATION();
memset(&userInfo, 0, sizeof(userInfo));
userInfo.dwVersion =
WEBAUTHN_USER_ENTITY_INFORMATION_CURRENT_VERSION;
userInfo.cbId = data.user.id.size();
userInfo.pbId = (PBYTE)data.user.id.data();
userInfo.pwszName = userName.c_str();
userInfo.pwszDisplayName = userDisplayName.c_str();
auto credParams = std::vector<WEBAUTHN_COSE_CREDENTIAL_PARAMETER>();
for (const auto &param : data.pubKeyCredParams) {
auto cp = WEBAUTHN_COSE_CREDENTIAL_PARAMETER{};
cp.dwVersion =
WEBAUTHN_COSE_CREDENTIAL_PARAMETER_CURRENT_VERSION;
auto type = param.type.toStdWString();
cp.pwszCredentialType = type == L"public-key"
? WEBAUTHN_CREDENTIAL_TYPE_PUBLIC_KEY
: L"";
cp.lAlg = param.alg;
credParams.push_back(cp);
}
auto credParamsList = WEBAUTHN_COSE_CREDENTIAL_PARAMETERS();
memset(&credParamsList, 0, sizeof(credParamsList));
credParamsList.cCredentialParameters = credParams.size();
credParamsList.pCredentialParameters = credParams.data();
auto clientDataJson = Data::Passkey::SerializeClientDataCreate(
data.challenge);
auto clientData = WEBAUTHN_CLIENT_DATA();
memset(&clientData, 0, sizeof(clientData));
clientData.dwVersion = WEBAUTHN_CLIENT_DATA_CURRENT_VERSION;
clientData.cbClientDataJSON = clientDataJson.size();
clientData.pbClientDataJSON = (PBYTE)clientDataJson.data();
clientData.pwszHashAlgId = WEBAUTHN_HASH_ALGORITHM_SHA_256;
auto cancellationId = GUID();
CoCreateGuid(&cancellationId);
auto options = WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS();
memset(&options, 0, sizeof(options));
options.dwVersion =
WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_CURRENT_VERSION;
options.dwTimeoutMilliseconds = data.timeout;
options.dwAuthenticatorAttachment =
WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY;
options.bRequireResidentKey = FALSE;
options.dwUserVerificationRequirement =
WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED;
options.dwAttestationConveyancePreference =
WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE;
options.pCancellationId = &cancellationId;
#if defined(WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_4) \
|| defined(WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_5) \
|| defined(WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_6) \
|| defined(WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_7) \
|| defined(WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_8) \
|| defined(WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_VERSION_9)
options.bPreferResidentKey = FALSE;
#endif
auto hwnd = (HWND)(nullptr);
if (auto window = QGuiApplication::topLevelWindows().value(0)) {
hwnd = (HWND)window->winId();
if (hwnd) {
SetForegroundWindow(hwnd);
SetFocus(hwnd);
}
}
auto attestation = (PWEBAUTHN_CREDENTIAL_ATTESTATION)(nullptr);
auto hr = (HRESULT)(WebAuthNAuthenticatorMakeCredential)(
hwnd,
&rpInfo,
&userInfo,
&credParamsList,
&clientData,
&options,
&attestation);
if (SUCCEEDED(hr) && attestation) {
auto result = RegisterResult();
result.success = true;
result.credentialId = QByteArray(
(char*)attestation->pbCredentialId,
attestation->cbCredentialId);
result.attestationObject = QByteArray(
(char*)attestation->pbAttestationObject,
attestation->cbAttestationObject);
result.clientDataJSON = QByteArray::fromStdString(clientDataJson);
WebAuthNFreeCredentialAttestation(attestation);
callback(result);
} else {
callback({});
}
}
void Login(
const Data::Passkey::LoginData &data,
Fn<void(LoginResult result)> callback) {
if (!Supported()) {
callback({});
return;
}
auto rpId = data.rpId.toStdWString();
auto clientDataJson = Data::Passkey::SerializeClientDataGet(
data.challenge);
auto clientData = WEBAUTHN_CLIENT_DATA();
memset(&clientData, 0, sizeof(clientData));
clientData.dwVersion = WEBAUTHN_CLIENT_DATA_CURRENT_VERSION;
clientData.cbClientDataJSON = clientDataJson.size();
clientData.pbClientDataJSON = (PBYTE)clientDataJson.data();
clientData.pwszHashAlgId = WEBAUTHN_HASH_ALGORITHM_SHA_256;
auto allowCredentials = std::vector<WEBAUTHN_CREDENTIAL>();
auto credentialIds = std::vector<QByteArray>();
for (const auto &cred : data.allowCredentials) {
credentialIds.push_back(cred.id);
auto credential = WEBAUTHN_CREDENTIAL();
memset(&credential, 0, sizeof(credential));
credential.dwVersion = WEBAUTHN_CREDENTIAL_CURRENT_VERSION;
credential.cbId = cred.id.size();
credential.pbId = (PBYTE)credentialIds.back().data();
credential.pwszCredentialType = WEBAUTHN_CREDENTIAL_TYPE_PUBLIC_KEY;
allowCredentials.push_back(credential);
}
auto cancellationId = GUID();
CoCreateGuid(&cancellationId);
auto options = WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS();
memset(&options, 0, sizeof(options));
options.dwVersion =
WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_CURRENT_VERSION;
options.dwTimeoutMilliseconds = data.timeout;
if (!allowCredentials.empty()) {
options.CredentialList.cCredentials = allowCredentials.size();
options.CredentialList.pCredentials = allowCredentials.data();
}
options.pCancellationId = &cancellationId;
if (data.userVerification == "required") {
options.dwUserVerificationRequirement =
WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED;
} else if (data.userVerification == "preferred") {
options.dwUserVerificationRequirement =
WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED;
} else {
options.dwUserVerificationRequirement =
WEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED;
}
auto hwnd = (HWND)(nullptr);
if (auto window = QGuiApplication::topLevelWindows().value(0)) {
hwnd = (HWND)window->winId();
if (hwnd) {
SetForegroundWindow(hwnd);
SetFocus(hwnd);
}
}
auto assertion = (PWEBAUTHN_ASSERTION)(nullptr);
auto hr = (HRESULT)(WebAuthNAuthenticatorGetAssertion)(
hwnd,
rpId.c_str(),
&clientData,
&options,
&assertion);
if (SUCCEEDED(hr) && assertion) {
auto result = LoginResult();
result.clientDataJSON = QByteArray::fromStdString(clientDataJson);
result.credentialId = QByteArray(
(char*)assertion->Credential.pbId,
assertion->Credential.cbId);
result.authenticatorData = QByteArray(
(char*)assertion->pbAuthenticatorData,
assertion->cbAuthenticatorData);
result.signature = QByteArray(
(char*)assertion->pbSignature,
assertion->cbSignature);
result.userHandle = assertion->cbUserId > 0
? QByteArray((char*)assertion->pbUserId, assertion->cbUserId)
: QByteArray();
WebAuthNFreeAssertion(assertion);
callback(result);
} else {
callback({});
}
}
} // namespace Platform::WebAuthn

View File

@@ -0,0 +1,507 @@
/*
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/win/windows_app_user_model_id.h"
#include "platform/win/windows_dlls.h"
#include "platform/win/windows_toast_activator.h"
#include "base/platform/win/base_windows_winrt.h"
#include "core/launcher.h"
#include <propvarutil.h>
#include <propkey.h>
namespace Platform {
namespace AppUserModelId {
namespace {
constexpr auto kMaxFileLen = MAX_PATH * 2;
const PROPERTYKEY pkey_AppUserModel_ID = { { 0x9F4C2855, 0x9F79, 0x4B39, { 0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3 } }, 5 };
const PROPERTYKEY pkey_AppUserModel_StartPinOption = { { 0x9F4C2855, 0x9F79, 0x4B39, { 0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3 } }, 12 };
const PROPERTYKEY pkey_AppUserModel_ToastActivator = { { 0x9F4C2855, 0x9F79, 0x4B39, { 0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3 } }, 26 };
#ifdef OS_WIN_STORE
const WCHAR AppUserModelIdBase[] = L"Telegram.TelegramDesktop.Store";
#else // OS_WIN_STORE
const WCHAR AppUserModelIdBase[] = L"Telegram.TelegramDesktop";
#endif // OS_WIN_STORE
[[nodiscard]] QString PinnedIconsPath() {
WCHAR wstrPath[kMaxFileLen] = {};
if (GetEnvironmentVariable(L"APPDATA", wstrPath, kMaxFileLen)) {
auto appData = QDir(QString::fromStdWString(std::wstring(wstrPath)));
return appData.absolutePath()
+ u"/Microsoft/Internet Explorer/Quick Launch/User Pinned/TaskBar/"_q;
}
return QString();
}
} // namespace
const std::wstring &MyExecutablePath() {
static const auto Path = [&] {
auto result = std::wstring(kMaxFileLen, 0);
const auto length = GetModuleFileName(
GetModuleHandle(nullptr),
result.data(),
kMaxFileLen);
if (!length || length == kMaxFileLen) {
result.clear();
} else {
result.resize(length + 1);
}
return result;
}();
return Path;
}
UniqueFileId MyExecutablePathId() {
return GetUniqueFileId(MyExecutablePath().c_str());
}
UniqueFileId GetUniqueFileId(LPCWSTR path) {
auto info = BY_HANDLE_FILE_INFORMATION{};
const auto file = CreateFile(
path,
0,
0,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr);
if (file == INVALID_HANDLE_VALUE) {
return {};
}
const auto result = GetFileInformationByHandle(file, &info);
CloseHandle(file);
if (!result) {
return {};
}
return {
.part1 = info.dwVolumeSerialNumber,
.part2 = ((std::uint64_t(info.nFileIndexLow) << 32)
| std::uint64_t(info.nFileIndexHigh)),
};
}
void CheckPinned() {
if (!SUCCEEDED(CoInitialize(0))) {
return;
}
const auto coGuard = gsl::finally([] {
CoUninitialize();
});
const auto path = PinnedIconsPath();
const auto native = QDir::toNativeSeparators(path).toStdWString();
const auto srcid = MyExecutablePathId();
if (!srcid) {
return;
}
LOG(("Checking..."));
WIN32_FIND_DATA findData;
HANDLE findHandle = FindFirstFileEx(
(native + L"*").c_str(),
FindExInfoStandard,
&findData,
FindExSearchNameMatch,
0,
0);
if (findHandle == INVALID_HANDLE_VALUE) {
LOG(("Init Error: could not find files in pinned folder"));
return;
}
do {
std::wstring fname = native + findData.cFileName;
LOG(("Checking %1").arg(QString::fromStdWString(fname)));
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
continue;
} else {
DWORD attributes = GetFileAttributes(fname.c_str());
if (attributes >= 0xFFFFFFF) {
continue; // file does not exist
}
auto shellLink = base::WinRT::TryCreateInstance<IShellLink>(
CLSID_ShellLink);
if (!shellLink) {
continue;
}
auto persistFile = shellLink.try_as<IPersistFile>();
if (!persistFile) {
continue;
}
auto hr = persistFile->Load(fname.c_str(), STGM_READWRITE);
if (!SUCCEEDED(hr)) continue;
WCHAR dst[MAX_PATH] = { 0 };
hr = shellLink->GetPath(dst, MAX_PATH, nullptr, 0);
if (!SUCCEEDED(hr)) continue;
if (GetUniqueFileId(dst) == srcid) {
auto propertyStore = shellLink.try_as<IPropertyStore>();
if (!propertyStore) {
return;
}
PROPVARIANT appIdPropVar;
hr = propertyStore->GetValue(Key(), &appIdPropVar);
if (!SUCCEEDED(hr)) return;
LOG(("Reading..."));
WCHAR already[MAX_PATH];
hr = PropVariantToString(appIdPropVar, already, MAX_PATH);
if (SUCCEEDED(hr)) {
if (Id() == already) {
LOG(("Already!"));
PropVariantClear(&appIdPropVar);
return;
}
}
if (appIdPropVar.vt != VT_EMPTY) {
PropVariantClear(&appIdPropVar);
return;
}
PropVariantClear(&appIdPropVar);
hr = InitPropVariantFromString(Id().c_str(), &appIdPropVar);
if (!SUCCEEDED(hr)) return;
hr = propertyStore->SetValue(Key(), appIdPropVar);
PropVariantClear(&appIdPropVar);
if (!SUCCEEDED(hr)) return;
hr = propertyStore->Commit();
if (!SUCCEEDED(hr)) return;
if (persistFile->IsDirty() == S_OK) {
persistFile->Save(fname.c_str(), TRUE);
}
return;
}
}
} while (FindNextFile(findHandle, &findData));
DWORD errorCode = GetLastError();
if (errorCode && errorCode != ERROR_NO_MORE_FILES) {
LOG(("Init Error: could not find some files in pinned folder"));
return;
}
FindClose(findHandle);
}
QString systemShortcutPath() {
WCHAR wstrPath[kMaxFileLen] = {};
if (GetEnvironmentVariable(L"APPDATA", wstrPath, kMaxFileLen)) {
auto appData = QDir(QString::fromStdWString(std::wstring(wstrPath)));
const auto path = appData.absolutePath();
return path + u"/Microsoft/Windows/Start Menu/Programs/"_q;
}
return QString();
}
void CleanupShortcut() {
const auto myid = MyExecutablePathId();
if (!myid) {
return;
}
QString path = systemShortcutPath() + u"Telegram.lnk"_q;
std::wstring p = QDir::toNativeSeparators(path).toStdWString();
DWORD attributes = GetFileAttributes(p.c_str());
if (attributes >= 0xFFFFFFF) return; // file does not exist
auto shellLink = base::WinRT::TryCreateInstance<IShellLink>(
CLSID_ShellLink);
if (!shellLink) {
return;
}
auto persistFile = shellLink.try_as<IPersistFile>();
if (!persistFile) {
return;
}
auto hr = persistFile->Load(p.c_str(), STGM_READWRITE);
if (!SUCCEEDED(hr)) return;
WCHAR szGotPath[MAX_PATH];
hr = shellLink->GetPath(szGotPath, MAX_PATH, nullptr, 0);
if (!SUCCEEDED(hr)) return;
if (GetUniqueFileId(szGotPath) == myid) {
QFile().remove(path);
}
}
bool validateShortcutAt(const QString &path) {
const auto native = QDir::toNativeSeparators(path).toStdWString();
DWORD attributes = GetFileAttributes(native.c_str());
if (attributes >= 0xFFFFFFF) {
return false; // file does not exist
}
auto shellLink = base::WinRT::TryCreateInstance<IShellLink>(
CLSID_ShellLink);
if (!shellLink) {
return false;
}
auto persistFile = shellLink.try_as<IPersistFile>();
if (!persistFile) {
return false;
}
auto hr = persistFile->Load(native.c_str(), STGM_READWRITE);
if (!SUCCEEDED(hr)) return false;
WCHAR szGotPath[kMaxFileLen] = { 0 };
hr = shellLink->GetPath(szGotPath, kMaxFileLen, nullptr, 0);
if (!SUCCEEDED(hr)) {
return false;
}
if (GetUniqueFileId(szGotPath) != MyExecutablePathId()) {
return false;
}
auto propertyStore = shellLink.try_as<IPropertyStore>();
if (!propertyStore) {
return false;
}
PROPVARIANT appIdPropVar;
PROPVARIANT toastActivatorPropVar;
hr = propertyStore->GetValue(Key(), &appIdPropVar);
if (!SUCCEEDED(hr)) return false;
hr = propertyStore->GetValue(
pkey_AppUserModel_ToastActivator,
&toastActivatorPropVar);
if (!SUCCEEDED(hr)) return false;
WCHAR already[MAX_PATH];
hr = PropVariantToString(appIdPropVar, already, MAX_PATH);
const auto good1 = SUCCEEDED(hr) && (Id() == already);
const auto bad1 = !good1 && (appIdPropVar.vt != VT_EMPTY);
PropVariantClear(&appIdPropVar);
auto clsid = CLSID();
hr = PropVariantToCLSID(toastActivatorPropVar, &clsid);
const auto good2 = SUCCEEDED(hr) && (clsid == __uuidof(ToastActivator));
const auto bad2 = !good2 && (toastActivatorPropVar.vt != VT_EMPTY);
PropVariantClear(&toastActivatorPropVar);
if (good1 && good2) {
LOG(("App Info: Shortcut validated at \"%1\"").arg(path));
return true;
} else if (bad1 || bad2) {
return false;
}
hr = InitPropVariantFromString(Id().c_str(), &appIdPropVar);
if (!SUCCEEDED(hr)) return false;
hr = propertyStore->SetValue(Key(), appIdPropVar);
PropVariantClear(&appIdPropVar);
if (!SUCCEEDED(hr)) return false;
hr = InitPropVariantFromCLSID(
__uuidof(ToastActivator),
&toastActivatorPropVar);
if (!SUCCEEDED(hr)) return false;
hr = propertyStore->SetValue(
pkey_AppUserModel_ToastActivator,
toastActivatorPropVar);
PropVariantClear(&toastActivatorPropVar);
if (!SUCCEEDED(hr)) return false;
hr = propertyStore->Commit();
if (!SUCCEEDED(hr)) return false;
if (persistFile->IsDirty() == S_OK) {
hr = persistFile->Save(native.c_str(), TRUE);
if (!SUCCEEDED(hr)) return false;
}
LOG(("App Info: Shortcut set and validated at \"%1\"").arg(path));
return true;
}
bool checkInstalled(QString path = {}) {
if (path.isEmpty()) {
path = systemShortcutPath();
if (path.isEmpty()) {
return false;
}
}
const auto installed = u"Telegram Desktop/Telegram.lnk"_q;
const auto old = u"Telegram Win (Unofficial)/Telegram.lnk"_q;
return validateShortcutAt(path + installed)
|| validateShortcutAt(path + old);
}
bool ValidateShortcut() {
QString path = systemShortcutPath();
if (path.isEmpty() || cExeName().isEmpty()) {
return false;
}
if (cAlphaVersion()) {
path += u"TelegramAlpha.lnk"_q;
if (validateShortcutAt(path)) {
return true;
}
} else {
if (checkInstalled(path)) {
return true;
}
path += u"Telegram.lnk"_q;
if (validateShortcutAt(path)) {
return true;
}
}
auto shellLink = base::WinRT::TryCreateInstance<IShellLink>(
CLSID_ShellLink);
if (!shellLink) {
return false;
}
auto hr = shellLink->SetPath(MyExecutablePath().c_str());
if (!SUCCEEDED(hr)) {
return false;
}
hr = shellLink->SetArguments(L"");
if (!SUCCEEDED(hr)) {
return false;
}
hr = shellLink->SetWorkingDirectory(
QDir::toNativeSeparators(
QDir(cWorkingDir()).absolutePath()).toStdWString().c_str());
if (!SUCCEEDED(hr)) {
return false;
}
auto propertyStore = shellLink.try_as<IPropertyStore>();
if (!propertyStore) {
return false;
}
PROPVARIANT appIdPropVar;
hr = InitPropVariantFromString(Id().c_str(), &appIdPropVar);
if (!SUCCEEDED(hr)) {
return false;
}
hr = propertyStore->SetValue(Key(), appIdPropVar);
PropVariantClear(&appIdPropVar);
if (!SUCCEEDED(hr)) {
return false;
}
PROPVARIANT startPinPropVar;
hr = InitPropVariantFromUInt32(
APPUSERMODEL_STARTPINOPTION_NOPINONINSTALL,
&startPinPropVar);
if (!SUCCEEDED(hr)) {
return false;
}
hr = propertyStore->SetValue(
pkey_AppUserModel_StartPinOption,
startPinPropVar);
PropVariantClear(&startPinPropVar);
if (!SUCCEEDED(hr)) {
return false;
}
PROPVARIANT toastActivatorPropVar{};
hr = InitPropVariantFromCLSID(
__uuidof(ToastActivator),
&toastActivatorPropVar);
if (!SUCCEEDED(hr)) {
return false;
}
hr = propertyStore->SetValue(
pkey_AppUserModel_ToastActivator,
toastActivatorPropVar);
PropVariantClear(&toastActivatorPropVar);
if (!SUCCEEDED(hr)) {
return false;
}
hr = propertyStore->Commit();
if (!SUCCEEDED(hr)) {
return false;
}
auto persistFile = shellLink.try_as<IPersistFile>();
if (!persistFile) {
return false;
}
hr = persistFile->Save(
QDir::toNativeSeparators(path).toStdWString().c_str(),
TRUE);
if (!SUCCEEDED(hr)) {
return false;
}
LOG(("App Info: Shortcut created and validated at \"%1\"").arg(path));
return true;
}
const std::wstring &Id() {
static const auto BaseId = std::wstring(AppUserModelIdBase);
static auto CheckingInstalled = false;
if (CheckingInstalled) {
return BaseId;
}
static const auto Installed = [] {
#ifdef OS_WIN_STORE
return true;
#else // OS_WIN_STORE
CheckingInstalled = true;
const auto guard = gsl::finally([] {
CheckingInstalled = false;
});
if (!SUCCEEDED(CoInitialize(nullptr))) {
return false;
}
const auto coGuard = gsl::finally([] {
CoUninitialize();
});
return checkInstalled();
#endif
}();
if (Installed) {
return BaseId;
}
static const auto PortableId = [] {
const auto h = Core::Launcher::Instance().instanceHash();
return BaseId + L'.' + std::wstring(h.begin(), h.end());
}();
return PortableId;
}
const PROPERTYKEY &Key() {
return pkey_AppUserModel_ID;
}
} // namespace AppUserModelId
} // namespace Platform

Some files were not shown because too many files have changed in this diff Show More