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
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:
@@ -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
|
||||
@@ -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"
|
||||
126
Telegram/SourceFiles/platform/linux/file_utilities_linux.cpp
Normal file
126
Telegram/SourceFiles/platform/linux/file_utilities_linux.cpp
Normal 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
|
||||
65
Telegram/SourceFiles/platform/linux/file_utilities_linux.h
Normal file
65
Telegram/SourceFiles/platform/linux/file_utilities_linux.h
Normal 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
|
||||
268
Telegram/SourceFiles/platform/linux/integration_linux.cpp
Normal file
268
Telegram/SourceFiles/platform/linux/integration_linux.cpp
Normal 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
|
||||
16
Telegram/SourceFiles/platform/linux/integration_linux.h
Normal file
16
Telegram/SourceFiles/platform/linux/integration_linux.h
Normal 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
|
||||
147
Telegram/SourceFiles/platform/linux/launcher_linux.cpp
Normal file
147
Telegram/SourceFiles/platform/linux/launcher_linux.cpp
Normal 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
|
||||
27
Telegram/SourceFiles/platform/linux/launcher_linux.h
Normal file
27
Telegram/SourceFiles/platform/linux/launcher_linux.h
Normal 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
|
||||
574
Telegram/SourceFiles/platform/linux/main_window_linux.cpp
Normal file
574
Telegram/SourceFiles/platform/linux/main_window_linux.cpp
Normal 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
|
||||
71
Telegram/SourceFiles/platform/linux/main_window_linux.h
Normal file
71
Telegram/SourceFiles/platform/linux/main_window_linux.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
20
Telegram/SourceFiles/platform/linux/overlay_widget_linux.h
Normal file
20
Telegram/SourceFiles/platform/linux/overlay_widget_linux.h
Normal 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
|
||||
915
Telegram/SourceFiles/platform/linux/specific_linux.cpp
Normal file
915
Telegram/SourceFiles/platform/linux/specific_linux.cpp
Normal 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;
|
||||
}
|
||||
58
Telegram/SourceFiles/platform/linux/specific_linux.h
Normal file
58
Telegram/SourceFiles/platform/linux/specific_linux.h
Normal 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);
|
||||
24
Telegram/SourceFiles/platform/linux/text_recognition_linux.h
Normal file
24
Telegram/SourceFiles/platform/linux/text_recognition_linux.h
Normal 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
|
||||
422
Telegram/SourceFiles/platform/linux/tray_linux.cpp
Normal file
422
Telegram/SourceFiles/platform/linux/tray_linux.cpp
Normal 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 ¤tImageBack = _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
|
||||
75
Telegram/SourceFiles/platform/linux/tray_linux.h
Normal file
75
Telegram/SourceFiles/platform/linux/tray_linux.h
Normal 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
|
||||
29
Telegram/SourceFiles/platform/linux/webauthn_linux.cpp
Normal file
29
Telegram/SourceFiles/platform/linux/webauthn_linux.cpp
Normal 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
|
||||
10
Telegram/SourceFiles/platform/mac/current_geo_location_mac.h
Normal file
10
Telegram/SourceFiles/platform/mac/current_geo_location_mac.h
Normal 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"
|
||||
154
Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm
Normal file
154
Telegram/SourceFiles/platform/mac/current_geo_location_mac.mm
Normal 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
|
||||
33
Telegram/SourceFiles/platform/mac/file_bookmark_mac.h
Normal file
33
Telegram/SourceFiles/platform/mac/file_bookmark_mac.h
Normal 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
|
||||
133
Telegram/SourceFiles/platform/mac/file_bookmark_mac.mm
Normal file
133
Telegram/SourceFiles/platform/mac/file_bookmark_mac.mm
Normal 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
|
||||
53
Telegram/SourceFiles/platform/mac/file_utilities_mac.h
Normal file
53
Telegram/SourceFiles/platform/mac/file_utilities_mac.h
Normal 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
|
||||
569
Telegram/SourceFiles/platform/mac/file_utilities_mac.mm
Normal file
569
Telegram/SourceFiles/platform/mac/file_utilities_mac.mm
Normal 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
|
||||
16
Telegram/SourceFiles/platform/mac/integration_mac.h
Normal file
16
Telegram/SourceFiles/platform/mac/integration_mac.h
Normal 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
|
||||
24
Telegram/SourceFiles/platform/mac/integration_mac.mm
Normal file
24
Telegram/SourceFiles/platform/mac/integration_mac.mm
Normal 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
|
||||
25
Telegram/SourceFiles/platform/mac/launcher_mac.h
Normal file
25
Telegram/SourceFiles/platform/mac/launcher_mac.h
Normal 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
|
||||
90
Telegram/SourceFiles/platform/mac/launcher_mac.mm
Normal file
90
Telegram/SourceFiles/platform/mac/launcher_mac.mm
Normal 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
|
||||
32
Telegram/SourceFiles/platform/mac/mac_iconv_helper.c
Normal file
32
Telegram/SourceFiles/platform/mac/mac_iconv_helper.c
Normal 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);
|
||||
}
|
||||
101
Telegram/SourceFiles/platform/mac/main_window_mac.h
Normal file
101
Telegram/SourceFiles/platform/mac/main_window_mac.h
Normal 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
|
||||
690
Telegram/SourceFiles/platform/mac/main_window_mac.mm
Normal file
690
Telegram/SourceFiles/platform/mac/main_window_mac.mm
Normal 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
|
||||
@@ -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
|
||||
705
Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm
Normal file
705
Telegram/SourceFiles/platform/mac/notifications_manager_mac.mm
Normal 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
|
||||
58
Telegram/SourceFiles/platform/mac/overlay_widget_mac.h
Normal file
58
Telegram/SourceFiles/platform/mac/overlay_widget_mac.h
Normal 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
|
||||
296
Telegram/SourceFiles/platform/mac/overlay_widget_mac.mm
Normal file
296
Telegram/SourceFiles/platform/mac/overlay_widget_mac.mm
Normal 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
|
||||
70
Telegram/SourceFiles/platform/mac/specific_mac.h
Normal file
70
Telegram/SourceFiles/platform/mac/specific_mac.h
Normal 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);
|
||||
302
Telegram/SourceFiles/platform/mac/specific_mac.mm
Normal file
302
Telegram/SourceFiles/platform/mac/specific_mac.mm
Normal 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);
|
||||
}
|
||||
28
Telegram/SourceFiles/platform/mac/specific_mac_p.h
Normal file
28
Telegram/SourceFiles/platform/mac/specific_mac_p.h
Normal 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);
|
||||
461
Telegram/SourceFiles/platform/mac/specific_mac_p.mm
Normal file
461
Telegram/SourceFiles/platform/mac/specific_mac_p.mm
Normal 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
|
||||
}
|
||||
8
Telegram/SourceFiles/platform/mac/text_recognition_mac.h
Normal file
8
Telegram/SourceFiles/platform/mac/text_recognition_mac.h
Normal 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
|
||||
103
Telegram/SourceFiles/platform/mac/text_recognition_mac.mm
Normal file
103
Telegram/SourceFiles/platform/mac/text_recognition_mac.mm
Normal 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
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#import <AppKit/NSPopoverTouchBarItem.h>
|
||||
#import <AppKit/NSTouchBar.h>
|
||||
|
||||
API_AVAILABLE(macos(10.12.2))
|
||||
@interface TextFormatPopover : NSPopoverTouchBarItem
|
||||
- (id)init:(NSTouchBarItemIdentifier)identifier;
|
||||
@end
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "platform/mac/touchbar/items/mac_formatter_item.h"
|
||||
|
||||
#include "base/platform/mac/base_utilities_mac.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "platform/mac/touchbar/mac_touchbar_common.h"
|
||||
|
||||
#import <AppKit/NSCustomTouchBarItem.h>
|
||||
#import <AppKit/NSScrollView.h>
|
||||
#import <AppKit/NSSegmentedControl.h>
|
||||
|
||||
#include <QtWidgets/QApplication>
|
||||
#include <QtWidgets/QTextEdit>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kCommandBold = 0x010;
|
||||
constexpr auto kCommandItalic = 0x011;
|
||||
constexpr auto kCommandUnderline = 0x012;
|
||||
constexpr auto kCommandStrikeOut = 0x013;
|
||||
constexpr auto kCommandBlockquote = 0x014;
|
||||
constexpr auto kCommandMonospace = 0x015;
|
||||
constexpr auto kCommandClear = 0x016;
|
||||
constexpr auto kCommandLink = 0x017;
|
||||
|
||||
const auto kPopoverFormatter = @"popoverInputFormatter";
|
||||
|
||||
void SendKeyEvent(int command) {
|
||||
auto *focused = qobject_cast<QTextEdit*>(QApplication::focusWidget());
|
||||
if (!focused) {
|
||||
return;
|
||||
}
|
||||
auto key = 0;
|
||||
auto modifier = Qt::KeyboardModifiers(0) | Qt::ControlModifier;
|
||||
switch (command) {
|
||||
case kCommandBold:
|
||||
key = Qt::Key_B;
|
||||
break;
|
||||
case kCommandItalic:
|
||||
key = Qt::Key_I;
|
||||
break;
|
||||
case kCommandBlockquote:
|
||||
key = Qt::Key_Period;
|
||||
modifier |= Qt::ShiftModifier;
|
||||
break;
|
||||
case kCommandMonospace:
|
||||
key = Qt::Key_M;
|
||||
modifier |= Qt::ShiftModifier;
|
||||
break;
|
||||
case kCommandClear:
|
||||
key = Qt::Key_N;
|
||||
modifier |= Qt::ShiftModifier;
|
||||
break;
|
||||
case kCommandLink:
|
||||
key = Qt::Key_K;
|
||||
break;
|
||||
case kCommandUnderline:
|
||||
key = Qt::Key_U;
|
||||
break;
|
||||
case kCommandStrikeOut:
|
||||
key = Qt::Key_X;
|
||||
modifier |= Qt::ShiftModifier;
|
||||
break;
|
||||
}
|
||||
QApplication::postEvent(
|
||||
focused,
|
||||
new QKeyEvent(QEvent::KeyPress, key, modifier));
|
||||
QApplication::postEvent(
|
||||
focused,
|
||||
new QKeyEvent(QEvent::KeyRelease, key, modifier));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
#pragma mark - TextFormatPopover
|
||||
|
||||
@implementation TextFormatPopover {
|
||||
rpl::lifetime _lifetime;
|
||||
}
|
||||
|
||||
- (id)init:(NSTouchBarItemIdentifier)identifier {
|
||||
self = [super initWithIdentifier:identifier];
|
||||
if (!self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
self.collapsedRepresentationImage = [NSImage
|
||||
imageNamed:NSImageNameTouchBarTextItalicTemplate]; // autorelease];
|
||||
auto *secondaryTouchBar = [[[NSTouchBar alloc] init] autorelease];
|
||||
|
||||
auto *popover = [[[NSCustomTouchBarItem alloc]
|
||||
initWithIdentifier:kPopoverFormatter] autorelease];
|
||||
{
|
||||
auto *scroll = [[[NSScrollView alloc] init] autorelease];
|
||||
auto *segment = [[[NSSegmentedControl alloc] init] autorelease];
|
||||
segment.segmentStyle = NSSegmentStyleRounded;
|
||||
segment.target = self;
|
||||
segment.action = @selector(segmentClicked:);
|
||||
|
||||
static const auto strings = {
|
||||
tr::lng_menu_formatting_bold,
|
||||
tr::lng_menu_formatting_italic,
|
||||
tr::lng_menu_formatting_underline,
|
||||
tr::lng_menu_formatting_strike_out,
|
||||
tr::lng_menu_formatting_blockquote,
|
||||
tr::lng_menu_formatting_monospace,
|
||||
tr::lng_menu_formatting_clear,
|
||||
tr::lng_info_link_label,
|
||||
};
|
||||
segment.segmentCount = strings.size();
|
||||
auto width = 0;
|
||||
auto count = 0;
|
||||
for (const auto &s : strings) {
|
||||
const auto string = Platform::Q2NSString(s(tr::now));
|
||||
width += TouchBar::WidthFromString(string) * 1.4;
|
||||
[segment setLabel:string forSegment:count++];
|
||||
}
|
||||
segment.frame = NSMakeRect(0, 0, width, TouchBar::kCircleDiameter);
|
||||
[scroll setDocumentView:segment];
|
||||
popover.view = scroll;
|
||||
}
|
||||
|
||||
secondaryTouchBar.templateItems = [NSSet setWithArray:@[popover]];
|
||||
secondaryTouchBar.defaultItemIdentifiers = @[kPopoverFormatter];
|
||||
|
||||
self.popoverTouchBar = secondaryTouchBar;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)segmentClicked:(NSSegmentedControl*)sender {
|
||||
const auto command = int(sender.selectedSegment) + kCommandBold;
|
||||
sender.selectedSegment = -1;
|
||||
SendKeyEvent(command);
|
||||
|
||||
[self dismissPopover:nil];
|
||||
}
|
||||
|
||||
@end // @implementation TextFormatPopover
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <AppKit/NSImageView.h>
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
API_AVAILABLE(macos(10.12.2))
|
||||
@interface PinnedDialogsPanel : NSImageView
|
||||
- (id)init:(not_null<Main::Session*>)session
|
||||
destroyEvent:(rpl::producer<>)touchBarSwitches;
|
||||
@end
|
||||
@@ -0,0 +1,873 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "platform/mac/touchbar/items/mac_pinned_chats_item.h"
|
||||
|
||||
#include "apiwrap.h"
|
||||
#include "base/call_delayed.h"
|
||||
#include "base/timer.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "core/application.h"
|
||||
#include "core/sandbox.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_cloud_file.h"
|
||||
#include "data/data_file_origin.h"
|
||||
#include "data/data_folder.h"
|
||||
#include "data/data_peer_values.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "history/history.h"
|
||||
#include "main/main_session.h"
|
||||
#include "mainwidget.h"
|
||||
#include "platform/mac/touchbar/mac_touchbar_common.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/empty_userpic.h"
|
||||
#include "ui/userpic_view.h"
|
||||
#include "ui/unread_badge_paint.h"
|
||||
#include "ui/painter.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "window/window_session_controller.h"
|
||||
|
||||
#import <AppKit/NSColor.h>
|
||||
#import <AppKit/NSGraphicsContext.h>
|
||||
#import <AppKit/NSPressGestureRecognizer.h>
|
||||
|
||||
using TouchBar::kCircleDiameter;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kPinnedButtonsSpace = 30;
|
||||
constexpr auto kPinnedButtonsLeftSkip = kPinnedButtonsSpace / 2;
|
||||
|
||||
constexpr auto kOnlineCircleSize = 8;
|
||||
constexpr auto kOnlineCircleStrokeWidth = 1.5;
|
||||
constexpr auto kUnreadBadgeSize = 15;
|
||||
|
||||
inline bool IsSelfPeer(PeerData *peer) {
|
||||
return peer && peer->isSelf();
|
||||
}
|
||||
|
||||
inline bool IsRepliesPeer(PeerData *peer) {
|
||||
return peer && peer->isRepliesChat();
|
||||
}
|
||||
|
||||
QImage PrepareImage() {
|
||||
const auto s = kCircleDiameter * style::DevicePixelRatio();
|
||||
auto result = QImage(QSize(s, s), QImage::Format_ARGB32_Premultiplied);
|
||||
result.fill(Qt::transparent);
|
||||
return result;
|
||||
}
|
||||
|
||||
QImage SavedMessagesUserpic() {
|
||||
auto result = PrepareImage();
|
||||
Painter paint(&result);
|
||||
|
||||
const auto s = result.width();
|
||||
Ui::EmptyUserpic::PaintSavedMessages(paint, 0, 0, s, s);
|
||||
return result;
|
||||
}
|
||||
|
||||
QImage RepliesMessagesUserpic() {
|
||||
auto result = PrepareImage();
|
||||
Painter paint(&result);
|
||||
|
||||
const auto s = result.width();
|
||||
Ui::EmptyUserpic::PaintRepliesMessages(paint, 0, 0, s, s);
|
||||
return result;
|
||||
}
|
||||
|
||||
QImage ArchiveUserpic(not_null<Data::Folder*> folder) {
|
||||
auto result = PrepareImage();
|
||||
Painter paint(&result);
|
||||
|
||||
folder->paintUserpic(paint, 0, 0, result.width());
|
||||
return result;
|
||||
}
|
||||
|
||||
QImage UnreadBadge(not_null<PeerData*> peer) {
|
||||
const auto history = peer->owner().history(peer->id);
|
||||
const auto state = history->chatListBadgesState();
|
||||
if (!state.unread) {
|
||||
return QImage();
|
||||
}
|
||||
const auto counter = (state.unreadCounter > 0)
|
||||
? QString::number(state.unreadCounter)
|
||||
: QString();
|
||||
Ui::UnreadBadgeStyle unreadSt;
|
||||
unreadSt.sizeId = Ui::UnreadBadgeSize::TouchBar;
|
||||
unreadSt.muted = state.unreadMuted;
|
||||
// Use constant values to draw badge regardless of cConfigScale().
|
||||
unreadSt.size = kUnreadBadgeSize * float64(style::DevicePixelRatio());
|
||||
unreadSt.padding = 4 * float64(style::DevicePixelRatio());
|
||||
unreadSt.font = style::font(
|
||||
9.5 * float64(style::DevicePixelRatio()),
|
||||
unreadSt.font->flags(),
|
||||
unreadSt.font->family());
|
||||
|
||||
auto result = QImage(
|
||||
QSize(kCircleDiameter, kUnreadBadgeSize) * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
result.fill(Qt::transparent);
|
||||
Painter p(&result);
|
||||
|
||||
Ui::PaintUnreadBadge(
|
||||
p,
|
||||
counter,
|
||||
result.width(),
|
||||
result.height() - unreadSt.size,
|
||||
unreadSt,
|
||||
2);
|
||||
return result;
|
||||
}
|
||||
|
||||
NSRect PeerRectByIndex(int index) {
|
||||
return NSMakeRect(
|
||||
index * (kCircleDiameter + kPinnedButtonsSpace)
|
||||
+ kPinnedButtonsLeftSkip,
|
||||
0,
|
||||
kCircleDiameter,
|
||||
kCircleDiameter);
|
||||
}
|
||||
|
||||
[[nodiscard]] Data::LastseenStatus CalculateLastseenStatus(
|
||||
not_null<PeerData*> peer) {
|
||||
if (const auto user = peer->asUser()) {
|
||||
return user->lastseen();
|
||||
}
|
||||
return Data::LastseenStatus();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
#pragma mark - PinnedDialogsPanel
|
||||
|
||||
@interface PinnedDialogsPanel()
|
||||
@end // @interface PinnedDialogsPanel
|
||||
|
||||
@implementation PinnedDialogsPanel {
|
||||
struct Pin {
|
||||
PeerData *peer = nullptr;
|
||||
Ui::PeerUserpicView userpicView;
|
||||
int index = -1;
|
||||
QImage userpic;
|
||||
QImage unreadBadge;
|
||||
|
||||
Ui::Animations::Simple shiftAnimation;
|
||||
int shift = 0;
|
||||
int finalShift = 0;
|
||||
int deltaShift = 0;
|
||||
int x = 0;
|
||||
int horizontalShift = 0;
|
||||
bool onTop = false;
|
||||
|
||||
Ui::Animations::Simple onlineAnimation;
|
||||
Data::LastseenStatus lastseen;
|
||||
};
|
||||
rpl::lifetime _lifetime;
|
||||
Main::Session *_session;
|
||||
|
||||
std::vector<std::unique_ptr<Pin>> _pins;
|
||||
QImage _savedMessages;
|
||||
QImage _repliesMessages;
|
||||
QImage _archive;
|
||||
|
||||
bool _hasArchive;
|
||||
bool _selfUnpinned;
|
||||
bool _repliesUnpinned;
|
||||
|
||||
rpl::event_stream<not_null<NSEvent*>> _touches;
|
||||
rpl::event_stream<not_null<NSPressGestureRecognizer*>> _gestures;
|
||||
|
||||
CGFloat _r, _g, _b, _a; // The online circle color.
|
||||
}
|
||||
|
||||
- (void)processHorizontalReorder {
|
||||
// This method is a simplified version of the VerticalLayoutReorder class
|
||||
// and is adapatized for horizontal use.
|
||||
enum class State : uchar {
|
||||
Started,
|
||||
Applied,
|
||||
Cancelled,
|
||||
};
|
||||
|
||||
const auto currentStart = _lifetime.make_state<int>(0);
|
||||
const auto currentPeer = _lifetime.make_state<PeerData*>(nullptr);
|
||||
const auto currentState = _lifetime.make_state<State>(State::Cancelled);
|
||||
const auto currentDesiredIndex = _lifetime.make_state<int>(-1);
|
||||
const auto waitForFinish = _lifetime.make_state<bool>(false);
|
||||
const auto isDragging = _lifetime.make_state<bool>(false);
|
||||
|
||||
const auto indexOf = [=](PeerData *p) {
|
||||
const auto i = ranges::find(_pins, p, &Pin::peer);
|
||||
Assert(i != end(_pins));
|
||||
return i - begin(_pins);
|
||||
};
|
||||
|
||||
const auto setHorizontalShift = [=](const auto &pin, int shift) {
|
||||
if (const auto delta = shift - pin->horizontalShift) {
|
||||
pin->horizontalShift = shift;
|
||||
pin->x += delta;
|
||||
|
||||
// Redraw a rectangle
|
||||
// from the beginning point of the pin movement to the end point.
|
||||
auto rect = PeerRectByIndex(indexOf(pin->peer) + [self shift]);
|
||||
const auto absDelta = std::abs(delta);
|
||||
rect.origin.x = pin->x - absDelta;
|
||||
rect.size.width += absDelta * 2;
|
||||
[self setNeedsDisplayInRect:rect];
|
||||
}
|
||||
};
|
||||
|
||||
const auto updateShift = [=](not_null<PeerData*> peer, int indexHint) {
|
||||
Expects(indexHint >= 0 && indexHint < _pins.size());
|
||||
|
||||
const auto index = (_pins[indexHint]->peer->id == peer->id)
|
||||
? indexHint
|
||||
: indexOf(peer);
|
||||
const auto &entry = _pins[index];
|
||||
entry->shift = entry->deltaShift
|
||||
+ base::SafeRound(
|
||||
entry->shiftAnimation.value(entry->finalShift));
|
||||
if (entry->deltaShift && !entry->shiftAnimation.animating()) {
|
||||
entry->finalShift += entry->deltaShift;
|
||||
entry->deltaShift = 0;
|
||||
}
|
||||
setHorizontalShift(entry, entry->shift);
|
||||
};
|
||||
|
||||
const auto moveToShift = [=](int index, int shift) {
|
||||
Core::Sandbox::Instance().customEnterFromEventLoop([=] {
|
||||
auto &entry = _pins[index];
|
||||
if (entry->finalShift + entry->deltaShift == shift) {
|
||||
return;
|
||||
}
|
||||
const auto peer = entry->peer;
|
||||
entry->shiftAnimation.start(
|
||||
[=] { updateShift(peer, index); },
|
||||
entry->finalShift,
|
||||
shift - entry->deltaShift,
|
||||
st::slideWrapDuration);
|
||||
entry->finalShift = shift - entry->deltaShift;
|
||||
});
|
||||
};
|
||||
|
||||
const auto cancelCurrentPeer = [=] {
|
||||
Expects(*currentPeer != nullptr);
|
||||
|
||||
if (*currentState == State::Started) {
|
||||
*currentState = State::Cancelled;
|
||||
}
|
||||
*currentPeer = nullptr;
|
||||
for (auto i = 0, count = int(_pins.size()); i != count; ++i) {
|
||||
moveToShift(i, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const auto cancelCurrent = [=] {
|
||||
if (*currentPeer) {
|
||||
cancelCurrentPeer();
|
||||
}
|
||||
};
|
||||
|
||||
const auto updateOrder = [=](int index, int positionX) {
|
||||
const auto shift = positionX - *currentStart;
|
||||
const auto ¤t = _pins[index];
|
||||
current->shiftAnimation.stop();
|
||||
current->shift = current->finalShift = shift;
|
||||
setHorizontalShift(current, shift);
|
||||
|
||||
const auto count = _pins.size();
|
||||
const auto currentWidth = current->userpic.width();
|
||||
const auto currentMiddle = current->x + currentWidth / 2;
|
||||
*currentDesiredIndex = index;
|
||||
if (shift > 0) {
|
||||
auto top = current->x - shift;
|
||||
for (auto next = index + 1; next != count; ++next) {
|
||||
const auto &entry = _pins[next];
|
||||
top += entry->userpic.width();
|
||||
if (currentMiddle < top) {
|
||||
moveToShift(next, 0);
|
||||
} else {
|
||||
*currentDesiredIndex = next;
|
||||
moveToShift(next, -currentWidth);
|
||||
}
|
||||
}
|
||||
for (auto prev = index - 1; prev >= 0; --prev) {
|
||||
moveToShift(prev, 0);
|
||||
}
|
||||
} else {
|
||||
for (auto next = index + 1; next != count; ++next) {
|
||||
moveToShift(next, 0);
|
||||
}
|
||||
for (auto prev = index - 1; prev >= 0; --prev) {
|
||||
const auto &entry = _pins[prev];
|
||||
if (currentMiddle >= entry->x - entry->shift + currentWidth) {
|
||||
moveToShift(prev, 0);
|
||||
} else {
|
||||
*currentDesiredIndex = prev;
|
||||
moveToShift(prev, currentWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const auto checkForStart = [=](int positionX) {
|
||||
const auto shift = positionX - *currentStart;
|
||||
const auto delta = QApplication::startDragDistance();
|
||||
*isDragging = (std::abs(shift) > delta);
|
||||
if (!*isDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
*currentState = State::Started;
|
||||
*currentStart += (shift > 0) ? delta : -delta;
|
||||
|
||||
const auto index = indexOf(*currentPeer);
|
||||
*currentDesiredIndex = index;
|
||||
|
||||
// Raise the pin.
|
||||
ranges::for_each(_pins, [=](const auto &pin) {
|
||||
pin->onTop = false;
|
||||
});
|
||||
_pins[index]->onTop = true;
|
||||
|
||||
updateOrder(index, positionX);
|
||||
};
|
||||
|
||||
const auto localGuard = _lifetime.make_state<base::has_weak_ptr>();
|
||||
|
||||
const auto finishCurrent = [=] {
|
||||
if (!*currentPeer) {
|
||||
return;
|
||||
}
|
||||
const auto index = indexOf(*currentPeer);
|
||||
if (*currentDesiredIndex == index
|
||||
|| *currentState != State::Started) {
|
||||
cancelCurrentPeer();
|
||||
return;
|
||||
}
|
||||
const auto result = *currentDesiredIndex;
|
||||
*currentState = State::Cancelled;
|
||||
*currentPeer = nullptr;
|
||||
|
||||
const auto ¤t = _pins[index];
|
||||
// Since the width of all elements is the same
|
||||
// we can use a single value.
|
||||
current->finalShift += (index - result) * current->userpic.width();
|
||||
|
||||
if (!(current->finalShift + current->deltaShift)) {
|
||||
current->shift = 0;
|
||||
setHorizontalShift(current, 0);
|
||||
}
|
||||
current->horizontalShift = current->finalShift;
|
||||
base::reorder(_pins, index, result);
|
||||
|
||||
*waitForFinish = true;
|
||||
// Call on end of an animation.
|
||||
base::call_delayed(st::slideWrapDuration, &(*localGuard), [=] {
|
||||
const auto guard = gsl::finally([=] {
|
||||
_session->data().notifyPinnedDialogsOrderUpdated();
|
||||
*waitForFinish = false;
|
||||
});
|
||||
if (index == result) {
|
||||
return;
|
||||
}
|
||||
const auto &order = _session->data().pinnedChatsOrder(nullptr);
|
||||
const auto d = (index < result) ? 1 : -1; // Direction.
|
||||
for (auto i = index; i != result; i += d) {
|
||||
_session->data().chatsList()->pinned()->reorder(
|
||||
order.at(i).history(),
|
||||
order.at(i + d).history());
|
||||
}
|
||||
_session->api().savePinnedOrder(nullptr);
|
||||
});
|
||||
|
||||
moveToShift(result, 0);
|
||||
};
|
||||
|
||||
const auto touchBegan = [=](int touchX) {
|
||||
*isDragging = false;
|
||||
cancelCurrent();
|
||||
*currentStart = touchX;
|
||||
if (_pins.size() < 2) {
|
||||
return;
|
||||
}
|
||||
const auto index = [self indexFromX:*currentStart];
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
*currentPeer = _pins[index]->peer;
|
||||
};
|
||||
|
||||
const auto touchMoved = [=](int touchX) {
|
||||
if (!*currentPeer) {
|
||||
return;
|
||||
} else if (*currentState != State::Started) {
|
||||
checkForStart(touchX);
|
||||
} else {
|
||||
updateOrder(indexOf(*currentPeer), touchX);
|
||||
}
|
||||
};
|
||||
|
||||
const auto touchEnded = [=](int touchX) {
|
||||
if (*isDragging) {
|
||||
finishCurrent();
|
||||
return;
|
||||
}
|
||||
const auto step = QApplication::startDragDistance();
|
||||
if (std::abs(*currentStart - touchX) < step) {
|
||||
[self performAction:touchX];
|
||||
}
|
||||
};
|
||||
|
||||
_gestures.events(
|
||||
) | rpl::filter([=] {
|
||||
return !(*waitForFinish);
|
||||
}) | rpl::on_next([=](
|
||||
not_null<NSPressGestureRecognizer*> gesture) {
|
||||
const auto currentPosition = [gesture locationInView:self].x;
|
||||
|
||||
switch ([gesture state]) {
|
||||
case NSGestureRecognizerStateBegan:
|
||||
return touchBegan(currentPosition);
|
||||
case NSGestureRecognizerStateChanged:
|
||||
return touchMoved(currentPosition);
|
||||
case NSGestureRecognizerStateCancelled:
|
||||
case NSGestureRecognizerStateEnded:
|
||||
return touchEnded(currentPosition);
|
||||
}
|
||||
}, _lifetime);
|
||||
|
||||
_session->data().pinnedDialogsOrderUpdated(
|
||||
) | rpl::on_next(cancelCurrent, _lifetime);
|
||||
|
||||
_lifetime.add([=] {
|
||||
for (const auto &pin : _pins) {
|
||||
pin->shiftAnimation.stop();
|
||||
pin->onlineAnimation.stop();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
- (id)init:(not_null<Main::Session*>)session
|
||||
destroyEvent:(rpl::producer<>)touchBarSwitches {
|
||||
self = [super init];
|
||||
_session = session;
|
||||
_hasArchive = _selfUnpinned = false;
|
||||
_savedMessages = SavedMessagesUserpic();
|
||||
_repliesMessages = RepliesMessagesUserpic();
|
||||
|
||||
auto *gesture = [[[NSPressGestureRecognizer alloc]
|
||||
initWithTarget:self
|
||||
action:@selector(gestureHandler:)] autorelease];
|
||||
gesture.allowedTouchTypes = NSTouchTypeMaskDirect;
|
||||
gesture.minimumPressDuration = 0;
|
||||
gesture.allowableMovement = 0;
|
||||
[self addGestureRecognizer:gesture];
|
||||
|
||||
// For some reason, sometimes a parent deallocates not immediately,
|
||||
// but only after the user's input (mouse movement, key pressing, etc.).
|
||||
// So we have to use a custom event to destroy the current lifetime
|
||||
// manually, before it leads to crashes.
|
||||
std::move(
|
||||
touchBarSwitches
|
||||
) | rpl::on_next([=] {
|
||||
_lifetime.destroy();
|
||||
}, _lifetime);
|
||||
|
||||
using UpdateFlag = Data::PeerUpdate::Flag;
|
||||
|
||||
const auto downloadLifetime = _lifetime.make_state<rpl::lifetime>();
|
||||
const auto peerChangedLifetime = _lifetime.make_state<rpl::lifetime>();
|
||||
const auto lastDialogsCount = _lifetime.make_state<rpl::variable<int>>(0);
|
||||
auto &&peers = ranges::views::all(
|
||||
_pins
|
||||
) | ranges::views::transform(&Pin::peer);
|
||||
|
||||
const auto updatePanelSize = [=] {
|
||||
const auto size = lastDialogsCount->current();
|
||||
if (self.image) {
|
||||
[self.image release];
|
||||
}
|
||||
// TODO: replace it with NSLayoutConstraint.
|
||||
self.image = [[NSImage alloc] initWithSize:NSMakeSize(
|
||||
size * (kCircleDiameter + kPinnedButtonsSpace)
|
||||
+ kPinnedButtonsLeftSkip
|
||||
- kPinnedButtonsSpace / 2,
|
||||
kCircleDiameter)];
|
||||
};
|
||||
lastDialogsCount->changes(
|
||||
) | rpl::on_next(updatePanelSize, _lifetime);
|
||||
const auto singleUserpic = [=](const auto &pin) {
|
||||
if (IsSelfPeer(pin->peer)) {
|
||||
pin->userpic = _savedMessages;
|
||||
return;
|
||||
} else if (IsRepliesPeer(pin->peer)) {
|
||||
pin->userpic = _repliesMessages;
|
||||
return;
|
||||
}
|
||||
auto userpic = PrepareImage();
|
||||
Painter p(&userpic);
|
||||
|
||||
pin->peer->paintUserpic(p, pin->userpicView, 0, 0, userpic.width());
|
||||
userpic.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
pin->userpic = std::move(userpic);
|
||||
const auto userpicIndex = pin->index + [self shift];
|
||||
[self setNeedsDisplayInRect:PeerRectByIndex(userpicIndex)];
|
||||
};
|
||||
const auto updateUserpics = [=] {
|
||||
ranges::for_each(_pins, singleUserpic);
|
||||
*lastDialogsCount = [self shift] + int(std::size(_pins));
|
||||
};
|
||||
const auto updateBadge = [=](const auto &pin) {
|
||||
const auto peer = pin->peer;
|
||||
if (IsSelfPeer(peer)) {
|
||||
return;
|
||||
}
|
||||
pin->unreadBadge = UnreadBadge(peer);
|
||||
|
||||
const auto userpicIndex = pin->index + [self shift];
|
||||
[self setNeedsDisplayInRect:PeerRectByIndex(userpicIndex)];
|
||||
};
|
||||
const auto listenToDownloaderFinished = [=] {
|
||||
_session->downloaderTaskFinished(
|
||||
) | rpl::on_next([=] {
|
||||
const auto all = ranges::all_of(_pins, [=](const auto &pin) {
|
||||
return (!pin->peer->hasUserpic())
|
||||
|| (!Ui::PeerUserpicLoading(pin->userpicView));
|
||||
});
|
||||
if (all) {
|
||||
downloadLifetime->destroy();
|
||||
}
|
||||
updateUserpics();
|
||||
}, *downloadLifetime);
|
||||
};
|
||||
const auto processOnline = [=](const auto &pin) {
|
||||
// TODO: this should be replaced
|
||||
// with the global application timer for online statuses.
|
||||
const auto onlineChanges
|
||||
= peerChangedLifetime->make_state<rpl::event_stream<PeerData*>>();
|
||||
const auto peer = pin->peer;
|
||||
const auto onlineTimer = peerChangedLifetime->make_state<base::Timer>(
|
||||
[=] { onlineChanges->fire_copy({ peer }); });
|
||||
|
||||
const auto callTimer = [=](const auto &pin) {
|
||||
onlineTimer->cancel();
|
||||
if (const auto till = pin->lastseen.onlineTill()) {
|
||||
const auto left = till - base::unixtime::now();
|
||||
if (left > 0) {
|
||||
onlineTimer->callOnce(std::min(86400, left)
|
||||
* crl::time(1000));
|
||||
}
|
||||
}
|
||||
};
|
||||
callTimer(pin);
|
||||
|
||||
using PeerUpdate = Data::PeerUpdate;
|
||||
auto to_peer = rpl::map([=](const PeerUpdate &update) -> PeerData* {
|
||||
return update.peer;
|
||||
});
|
||||
rpl::merge(
|
||||
_session->changes().peerUpdates(
|
||||
pin->peer,
|
||||
UpdateFlag::OnlineStatus) | to_peer,
|
||||
onlineChanges->events()
|
||||
) | rpl::on_next([=](PeerData *peer) {
|
||||
const auto it = ranges::find(_pins, peer, &Pin::peer);
|
||||
if (it == end(_pins)) {
|
||||
return;
|
||||
}
|
||||
const auto &pin = *it;
|
||||
pin->lastseen = CalculateLastseenStatus(pin->peer);
|
||||
|
||||
callTimer(pin);
|
||||
|
||||
if (![NSApplication sharedApplication].active) {
|
||||
pin->onlineAnimation.stop();
|
||||
return;
|
||||
}
|
||||
const auto now = base::unixtime::now();
|
||||
const auto online = pin->lastseen.isOnline(now);
|
||||
if (pin->onlineAnimation.animating()) {
|
||||
pin->onlineAnimation.change(
|
||||
online ? 1. : 0.,
|
||||
st::dialogsOnlineBadgeDuration);
|
||||
} else {
|
||||
const auto s = kOnlineCircleSize + kOnlineCircleStrokeWidth;
|
||||
const auto index = pin->index;
|
||||
Core::Sandbox::Instance().customEnterFromEventLoop([=] {
|
||||
_pins[index]->onlineAnimation.start(
|
||||
[=] {
|
||||
[self setNeedsDisplayInRect:NSMakeRect(
|
||||
_pins[index]->x + kCircleDiameter - s,
|
||||
0,
|
||||
s,
|
||||
s)];
|
||||
},
|
||||
online ? 0. : 1.,
|
||||
online ? 1. : 0.,
|
||||
st::dialogsOnlineBadgeDuration);
|
||||
});
|
||||
}
|
||||
}, *peerChangedLifetime);
|
||||
};
|
||||
|
||||
const auto updatePinnedChats = [=] {
|
||||
_pins = ranges::views::zip(
|
||||
_session->data().pinnedChatsOrder(nullptr),
|
||||
ranges::views::ints(0, ranges::unreachable)
|
||||
) | ranges::views::transform([=](const auto &pair) {
|
||||
const auto index = pair.second;
|
||||
auto peer = pair.first.history()->peer;
|
||||
auto view = peer->createUserpicView();
|
||||
return std::make_unique<Pin>(Pin{
|
||||
.peer = std::move(peer),
|
||||
.userpicView = std::move(view),
|
||||
.index = index,
|
||||
.lastseen = CalculateLastseenStatus(peer),
|
||||
});
|
||||
}) | ranges::to_vector;
|
||||
_selfUnpinned = ranges::none_of(peers, &PeerData::isSelf);
|
||||
_repliesUnpinned = ranges::none_of(peers, &PeerData::isRepliesChat);
|
||||
|
||||
peerChangedLifetime->destroy();
|
||||
for (const auto &pin : _pins) {
|
||||
const auto peer = pin->peer;
|
||||
const auto index = pin->index;
|
||||
|
||||
_session->changes().peerUpdates(
|
||||
peer,
|
||||
UpdateFlag::Photo
|
||||
) | rpl::on_next([=](const Data::PeerUpdate &update) {
|
||||
_pins[index]->userpicView = update.peer->createUserpicView();
|
||||
listenToDownloaderFinished();
|
||||
}, *peerChangedLifetime);
|
||||
|
||||
if (const auto user = peer->asUser()) {
|
||||
if (!user->isServiceUser()
|
||||
&& !user->isBot()
|
||||
&& !peer->isSelf()) {
|
||||
processOnline(pin);
|
||||
}
|
||||
}
|
||||
|
||||
rpl::merge(
|
||||
_session->changes().historyUpdates(
|
||||
_session->data().history(peer),
|
||||
Data::HistoryUpdate::Flag::UnreadView
|
||||
) | rpl::to_empty,
|
||||
_session->changes().peerFlagsValue(
|
||||
peer,
|
||||
UpdateFlag::Notifications
|
||||
) | rpl::to_empty
|
||||
) | rpl::on_next([=] {
|
||||
updateBadge(_pins[index]);
|
||||
}, *peerChangedLifetime);
|
||||
}
|
||||
|
||||
updateUserpics();
|
||||
};
|
||||
|
||||
rpl::single(rpl::empty) | rpl::then(
|
||||
_session->data().pinnedDialogsOrderUpdated()
|
||||
) | rpl::on_next(updatePinnedChats, _lifetime);
|
||||
|
||||
const auto ArchiveId = Data::Folder::kId;
|
||||
rpl::single(
|
||||
_session->data().folderLoaded(ArchiveId)
|
||||
) | rpl::then(
|
||||
_session->data().chatsListChanges()
|
||||
) | rpl::filter([](Data::Folder *folder) {
|
||||
return folder && (folder->id() == ArchiveId);
|
||||
}) | rpl::on_next([=](Data::Folder *folder) {
|
||||
_hasArchive = !folder->chatsList()->empty();
|
||||
if (_archive.isNull()) {
|
||||
_archive = ArchiveUserpic(folder);
|
||||
}
|
||||
updateUserpics();
|
||||
}, _lifetime);
|
||||
|
||||
const auto updateOnlineColor = [=] {
|
||||
auto r = 0, g = 0, b = 0, a = 0;
|
||||
st::dialogsOnlineBadgeFg->c.getRgb(&r, &g, &b, &a);
|
||||
_r = r / 255.;
|
||||
_g = g / 255.;
|
||||
_b = b / 255.;
|
||||
_a = a / 255.;
|
||||
};
|
||||
updateOnlineColor();
|
||||
|
||||
const auto localGuard = _lifetime.make_state<base::has_weak_ptr>();
|
||||
|
||||
style::PaletteChanged(
|
||||
) | rpl::on_next([=] {
|
||||
crl::on_main(&(*localGuard), [=] {
|
||||
updateOnlineColor();
|
||||
if (const auto f = _session->data().folderLoaded(ArchiveId)) {
|
||||
_archive = ArchiveUserpic(f);
|
||||
}
|
||||
_savedMessages = SavedMessagesUserpic();
|
||||
_repliesMessages = RepliesMessagesUserpic();
|
||||
updateUserpics();
|
||||
});
|
||||
}, _lifetime);
|
||||
|
||||
listenToDownloaderFinished();
|
||||
[self processHorizontalReorder];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
if (self.image) {
|
||||
[self.image release];
|
||||
}
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (int)shift {
|
||||
return (_hasArchive ? 1 : 0) + (_selfUnpinned ? 1 : 0);
|
||||
}
|
||||
|
||||
- (void)gestureHandler:(NSPressGestureRecognizer*)gesture {
|
||||
_gestures.fire(std::move(gesture));
|
||||
}
|
||||
|
||||
- (int)indexFromX:(int)position {
|
||||
const auto x = position
|
||||
- kPinnedButtonsLeftSkip
|
||||
+ kPinnedButtonsSpace / 2;
|
||||
return x / (kCircleDiameter + kPinnedButtonsSpace) - [self shift];
|
||||
}
|
||||
|
||||
- (void)performAction:(int)xPosition {
|
||||
const auto index = [self indexFromX:xPosition];
|
||||
const auto peer = (index < 0 || index >= int(std::size(_pins)))
|
||||
? nullptr
|
||||
: _pins[index]->peer;
|
||||
if (!peer && !_hasArchive && !_selfUnpinned) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto active = Core::App().activePrimaryWindow();
|
||||
const auto controller = active ? active->sessionController() : nullptr;
|
||||
const auto openFolder = [=] {
|
||||
const auto folder = _session->data().folderLoaded(Data::Folder::kId);
|
||||
if (folder && controller) {
|
||||
controller->openFolder(folder);
|
||||
}
|
||||
};
|
||||
Core::Sandbox::Instance().customEnterFromEventLoop([=] {
|
||||
if (_hasArchive && (index == (_selfUnpinned ? -2 : -1))) {
|
||||
openFolder();
|
||||
} else {
|
||||
controller->showPeerHistory((_selfUnpinned && index == -1)
|
||||
? _session->user()
|
||||
: peer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (QImage)imageToDraw:(int)i {
|
||||
Expects(i < int(std::size(_pins)));
|
||||
if (i < 0) {
|
||||
if (_hasArchive && (i == -[self shift])) {
|
||||
return _archive;
|
||||
} else if (_selfUnpinned) {
|
||||
return _savedMessages;
|
||||
} else if (_repliesUnpinned) {
|
||||
return _repliesMessages;
|
||||
}
|
||||
}
|
||||
return _pins[i]->userpic;
|
||||
}
|
||||
|
||||
- (void)drawSinglePin:(int)i rect:(NSRect)dirtyRect {
|
||||
const auto rect = [&] {
|
||||
auto rect = PeerRectByIndex(i + [self shift]);
|
||||
if (i < 0) {
|
||||
return rect;
|
||||
}
|
||||
auto &pin = _pins[i];
|
||||
// We can have x = 0 when the pin is dragged.
|
||||
rect.origin.x = ((!pin->x && !pin->onTop) ? rect.origin.x : pin->x);
|
||||
pin->x = rect.origin.x;
|
||||
return rect;
|
||||
}();
|
||||
if (!NSIntersectsRect(rect, dirtyRect)) {
|
||||
return;
|
||||
}
|
||||
CGContextRef context = [[NSGraphicsContext currentContext] CGContext];
|
||||
{
|
||||
CGImageRef image = ([self imageToDraw:i]).toCGImage();
|
||||
CGContextDrawImage(context, rect, image);
|
||||
CGImageRelease(image);
|
||||
}
|
||||
|
||||
if (i >= 0) {
|
||||
const auto &pin = _pins[i];
|
||||
const auto rectRight = NSMaxX(rect);
|
||||
if (!pin->unreadBadge.isNull()) {
|
||||
CGImageRef image = pin->unreadBadge.toCGImage();
|
||||
const auto w = CGImageGetWidth(image)
|
||||
/ float64(style::DevicePixelRatio());
|
||||
const auto borderRect = CGRectMake(
|
||||
rectRight - w,
|
||||
0,
|
||||
w,
|
||||
CGImageGetHeight(image)
|
||||
/ float64(style::DevicePixelRatio()));
|
||||
CGContextDrawImage(context, borderRect, image);
|
||||
CGImageRelease(image);
|
||||
return;
|
||||
}
|
||||
const auto now = base::unixtime::now();
|
||||
const auto online = pin->lastseen.isOnline(now);
|
||||
const auto value = pin->onlineAnimation.value(online ? 1. : 0.);
|
||||
if (value < 0.05) {
|
||||
return;
|
||||
}
|
||||
const auto lineWidth = kOnlineCircleStrokeWidth;
|
||||
const auto circleSize = kOnlineCircleSize;
|
||||
const auto progress = value * circleSize;
|
||||
const auto diff = (circleSize - progress) / 2;
|
||||
const auto borderRect = CGRectMake(
|
||||
rectRight - circleSize + diff - lineWidth / 2,
|
||||
diff,
|
||||
progress,
|
||||
progress);
|
||||
|
||||
CGContextSetRGBStrokeColor(context, 0, 0, 0, 1.0);
|
||||
CGContextSetRGBFillColor(context, _r, _g, _b, _a);
|
||||
CGContextSetLineWidth(context, lineWidth);
|
||||
CGContextFillEllipseInRect(context, borderRect);
|
||||
CGContextStrokeEllipseInRect(context, borderRect);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)drawRect:(NSRect)dirtyRect {
|
||||
const auto shift = [self shift];
|
||||
if (_pins.empty() && !shift) {
|
||||
return;
|
||||
}
|
||||
auto indexToTop = -1;
|
||||
const auto guard = gsl::finally([&] {
|
||||
if (indexToTop >= 0) {
|
||||
[self drawSinglePin:indexToTop rect:dirtyRect];
|
||||
}
|
||||
});
|
||||
for (auto i = -shift; i < int(std::size(_pins)); i++) {
|
||||
if (i >= 0 && _pins[i]->onTop && (indexToTop < 0)) {
|
||||
indexToTop = i;
|
||||
continue;
|
||||
}
|
||||
[self drawSinglePin:i rect:dirtyRect];
|
||||
}
|
||||
}
|
||||
|
||||
@end // @@implementation PinnedDialogsPanel
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#import <AppKit/NSPopoverTouchBarItem.h>
|
||||
#import <AppKit/NSTouchBar.h>
|
||||
|
||||
namespace Window {
|
||||
class Controller;
|
||||
} // namespace Window
|
||||
|
||||
API_AVAILABLE(macos(10.12.2))
|
||||
@interface StickerEmojiPopover : NSPopoverTouchBarItem<NSTouchBarDelegate>
|
||||
- (id)init:(not_null<Window::Controller*>)controller
|
||||
identifier:(NSTouchBarItemIdentifier)identifier;
|
||||
@end
|
||||
@@ -0,0 +1,716 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "platform/mac/touchbar/items/mac_scrubber_item.h"
|
||||
|
||||
#include "api/api_common.h"
|
||||
#include "api/api_sending.h"
|
||||
#include "base/call_delayed.h"
|
||||
#include "base/platform/mac/base_utilities_mac.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/painter.h"
|
||||
#include "chat_helpers/emoji_list_widget.h"
|
||||
#include "core/sandbox.h"
|
||||
#include "core/application.h"
|
||||
#include "core/core_settings.h"
|
||||
#include "data/data_chat_participant_status.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_document_media.h"
|
||||
#include "data/data_file_origin.h"
|
||||
#include "data/data_forum_topic.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/stickers/data_stickers.h"
|
||||
#include "history/history.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "platform/mac/touchbar/mac_touchbar_common.h"
|
||||
#include "styles/style_basic.h"
|
||||
#include "styles/style_settings.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "window/section_widget.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "window/window_session_controller.h"
|
||||
|
||||
#import <AppKit/NSCustomTouchBarItem.h>
|
||||
#import <AppKit/NSGestureRecognizer.h>
|
||||
#import <AppKit/NSImage.h>
|
||||
#import <AppKit/NSImageView.h>
|
||||
#import <AppKit/NSPressGestureRecognizer.h>
|
||||
#import <AppKit/NSScrollView.h>
|
||||
#import <AppKit/NSScrubber.h>
|
||||
#import <AppKit/NSScrubberItemView.h>
|
||||
#import <AppKit/NSScrubberLayout.h>
|
||||
#import <AppKit/NSSegmentedControl.h>
|
||||
#import <AppKit/NSTextField.h>
|
||||
|
||||
#include <QtWidgets/QTextEdit>
|
||||
|
||||
using TouchBar::kCircleDiameter;
|
||||
using TouchBar::CreateNSImageFromStyleIcon;
|
||||
|
||||
namespace {
|
||||
|
||||
//https://developer.apple.com/design/human-interface-guidelines/macos/touch-bar/touch-bar-icons-and-images/
|
||||
constexpr auto kIdealIconSize = 36;
|
||||
constexpr auto kSegmentIconSize = 25;
|
||||
constexpr auto kSegmentSize = 92;
|
||||
|
||||
constexpr auto kMaxStickerSets = 5;
|
||||
|
||||
constexpr auto kGestureStateProcessed = {
|
||||
NSGestureRecognizerStateChanged,
|
||||
NSGestureRecognizerStateBegan,
|
||||
};
|
||||
|
||||
constexpr auto kGestureStateFinished = {
|
||||
NSGestureRecognizerStateEnded,
|
||||
NSGestureRecognizerStateCancelled,
|
||||
NSGestureRecognizerStateFailed,
|
||||
};
|
||||
|
||||
const auto kStickersScrubber = @"scrubberStickers";
|
||||
const auto kEmojiScrubber = @"scrubberEmoji";
|
||||
|
||||
const auto kStickerItemIdentifier = @"stickerItem";
|
||||
const auto kEmojiItemIdentifier = @"emojiItem";
|
||||
const auto kPickerTitleItemIdentifier = @"pickerTitleItem";
|
||||
|
||||
enum ScrubberItemType {
|
||||
Emoji,
|
||||
Sticker,
|
||||
None,
|
||||
};
|
||||
|
||||
inline bool IsSticker(ScrubberItemType type) {
|
||||
return type == ScrubberItemType::Sticker;
|
||||
}
|
||||
|
||||
struct PickerScrubberItem {
|
||||
PickerScrubberItem(QString title) : title(title) {
|
||||
}
|
||||
PickerScrubberItem(DocumentData *document) : document(document) {
|
||||
mediaView = document->createMediaView();
|
||||
mediaView->checkStickerSmall();
|
||||
updateThumbnail();
|
||||
}
|
||||
PickerScrubberItem(EmojiPtr emoji) : emoji(emoji) {
|
||||
}
|
||||
|
||||
void updateThumbnail() {
|
||||
if (!document || !image.isNull()) {
|
||||
return;
|
||||
}
|
||||
const auto sticker = mediaView->getStickerSmall();
|
||||
if (!sticker) {
|
||||
return;
|
||||
}
|
||||
const auto size = sticker->size()
|
||||
.scaled(kCircleDiameter, kCircleDiameter, Qt::KeepAspectRatio);
|
||||
image = sticker->pixSingle(
|
||||
size,
|
||||
{ .outer = { kCircleDiameter, kCircleDiameter } }).toImage();
|
||||
}
|
||||
|
||||
bool isStickerLoaded() const {
|
||||
return !image.isNull();
|
||||
}
|
||||
|
||||
QString title = QString();
|
||||
|
||||
DocumentData *document = nullptr;
|
||||
std::shared_ptr<Data::DocumentMedia> mediaView = nullptr;
|
||||
QImage image;
|
||||
|
||||
EmojiPtr emoji = nullptr;
|
||||
};
|
||||
|
||||
struct PickerScrubberItemsHolder {
|
||||
std::vector<PickerScrubberItem> stickers;
|
||||
std::vector<PickerScrubberItem> emoji;
|
||||
|
||||
int size(ScrubberItemType type) {
|
||||
return IsSticker(type) ? stickers.size() : emoji.size();
|
||||
}
|
||||
|
||||
auto at(int index, ScrubberItemType type) {
|
||||
return IsSticker(type) ? stickers[index] : emoji[index];
|
||||
}
|
||||
};
|
||||
|
||||
using Platform::Q2NSString;
|
||||
using Platform::Q2NSImage;
|
||||
|
||||
NSImage *CreateNSImageFromEmoji(EmojiPtr emoji) {
|
||||
auto image = QImage(
|
||||
QSize(kIdealIconSize, kIdealIconSize) * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
image.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
image.fill(Qt::black);
|
||||
{
|
||||
Painter paint(&image);
|
||||
PainterHighQualityEnabler hq(paint);
|
||||
Ui::Emoji::Draw(
|
||||
paint,
|
||||
emoji,
|
||||
Ui::Emoji::GetSizeTouchbar(),
|
||||
0,
|
||||
0);
|
||||
}
|
||||
return Q2NSImage(image);
|
||||
}
|
||||
|
||||
auto ActiveChat(not_null<Window::Controller*> controller) {
|
||||
if (const auto sessionController = controller->sessionController()) {
|
||||
return sessionController->activeChatCurrent();
|
||||
}
|
||||
return Dialogs::Key();
|
||||
}
|
||||
|
||||
bool CanSendToActiveChat(
|
||||
not_null<Window::Controller*> controller,
|
||||
ChatRestriction right) {
|
||||
if (const auto topic = ActiveChat(controller).topic()) {
|
||||
return Data::CanSend(topic, right);
|
||||
} else if (const auto history = ActiveChat(controller).history()) {
|
||||
return Data::CanSend(history->peer, right);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<QString> RestrictionToSend(
|
||||
not_null<Window::Controller*> controller,
|
||||
ChatRestriction right) {
|
||||
if (const auto peer = ActiveChat(controller).peer()) {
|
||||
if (const auto error = Data::RestrictionError(peer, right)) {
|
||||
return *error;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QString TitleRecentlyUsed(const Data::StickersSets &sets) {
|
||||
const auto it = sets.find(Data::Stickers::CloudRecentSetId);
|
||||
return (it != sets.cend())
|
||||
? it->second->title
|
||||
: tr::lng_recent_stickers(tr::now);
|
||||
}
|
||||
|
||||
void AppendStickerSet(
|
||||
const Data::StickersSets &sets,
|
||||
std::vector<PickerScrubberItem> &to,
|
||||
uint64 setId) {
|
||||
const auto it = sets.find(setId);
|
||||
if (it == sets.cend() || it->second->stickers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto set = it->second.get();
|
||||
if (set->flags & Data::StickersSetFlag::Archived) {
|
||||
return;
|
||||
}
|
||||
if (!(set->flags & Data::StickersSetFlag::Installed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
to.emplace_back(PickerScrubberItem(set->title.isEmpty()
|
||||
? set->shortName
|
||||
: set->title));
|
||||
for (const auto sticker : set->stickers) {
|
||||
to.emplace_back(PickerScrubberItem(sticker));
|
||||
}
|
||||
}
|
||||
|
||||
void AppendRecentStickers(
|
||||
const Data::StickersSets &sets,
|
||||
RecentStickerPack &recentPack,
|
||||
std::vector<PickerScrubberItem> &to) {
|
||||
const auto cloudIt = sets.find(Data::Stickers::CloudRecentSetId);
|
||||
const auto cloudCount = (cloudIt != sets.cend())
|
||||
? cloudIt->second->stickers.size()
|
||||
: 0;
|
||||
if (cloudCount > 0) {
|
||||
to.emplace_back(PickerScrubberItem(cloudIt->second->title));
|
||||
for (const auto document : cloudIt->second->stickers) {
|
||||
if (document->owner().stickers().isFaved(document)) {
|
||||
continue;
|
||||
}
|
||||
to.emplace_back(PickerScrubberItem(document));
|
||||
}
|
||||
}
|
||||
for (const auto &recent : recentPack) {
|
||||
to.emplace_back(PickerScrubberItem(recent.first));
|
||||
}
|
||||
}
|
||||
|
||||
void AppendFavedStickers(
|
||||
const Data::StickersSets &sets,
|
||||
std::vector<PickerScrubberItem> &to) {
|
||||
const auto it = sets.find(Data::Stickers::FavedSetId);
|
||||
const auto count = (it != sets.cend())
|
||||
? it->second->stickers.size()
|
||||
: 0;
|
||||
if (!count) {
|
||||
return;
|
||||
}
|
||||
to.emplace_back(PickerScrubberItem(
|
||||
tr::lng_mac_touchbar_favorite_stickers(tr::now)));
|
||||
for (const auto document : it->second->stickers) {
|
||||
to.emplace_back(PickerScrubberItem(document));
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] EmojiPack RecentEmojiSection() {
|
||||
const auto list = Core::App().settings().recentEmoji();
|
||||
auto result = EmojiPack();
|
||||
result.reserve(list.size());
|
||||
for (const auto &emoji : list) {
|
||||
if (const auto one = std::get_if<EmojiPtr>(&emoji.id.data)) {
|
||||
result.push_back(*one);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void AppendEmojiPacks(
|
||||
const Data::StickersSets &sets,
|
||||
std::vector<PickerScrubberItem> &to) {
|
||||
for (auto i = 0; i != ChatHelpers::kEmojiSectionCount; ++i) {
|
||||
const auto section = static_cast<Ui::Emoji::Section>(i);
|
||||
const auto list = (section == Ui::Emoji::Section::Recent)
|
||||
? RecentEmojiSection()
|
||||
: Ui::Emoji::GetSection(section);
|
||||
const auto title = (section == Ui::Emoji::Section::Recent)
|
||||
? TitleRecentlyUsed(sets)
|
||||
: ChatHelpers::EmojiCategoryTitle(i)(tr::now);
|
||||
to.emplace_back(title);
|
||||
for (const auto &emoji : list) {
|
||||
to.emplace_back(PickerScrubberItem(emoji));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@interface PickerScrubberItemView : NSScrubberImageItemView {
|
||||
@public
|
||||
DocumentId documentId;
|
||||
}
|
||||
@end // @interface PickerScrubberItemView
|
||||
@implementation PickerScrubberItemView
|
||||
@end // @implementation PickerScrubberItemView
|
||||
|
||||
#pragma mark - PickerCustomTouchBarItem
|
||||
|
||||
@interface PickerCustomTouchBarItem : NSCustomTouchBarItem
|
||||
<NSScrubberDelegate,
|
||||
NSScrubberDataSource,
|
||||
NSScrubberFlowLayoutDelegate>
|
||||
@end // @interface PickerCustomTouchBarItem
|
||||
|
||||
@implementation PickerCustomTouchBarItem {
|
||||
ScrubberItemType _type;
|
||||
std::shared_ptr<PickerScrubberItemsHolder> _itemsDataSource;
|
||||
std::unique_ptr<PickerScrubberItem> _error;
|
||||
DocumentId _lastPreviewedSticker;
|
||||
Window::Controller *_controller;
|
||||
History *_history;
|
||||
|
||||
rpl::event_stream<> _closeRequests;
|
||||
rpl::lifetime _lifetime;
|
||||
}
|
||||
|
||||
- (id)init:(ScrubberItemType)type
|
||||
controller:(not_null<Window::Controller*>)controller
|
||||
items:(std::shared_ptr<PickerScrubberItemsHolder>)items {
|
||||
Expects(controller->sessionController() != nullptr);
|
||||
self = [super initWithIdentifier:IsSticker(type)
|
||||
? kStickersScrubber
|
||||
: kEmojiScrubber];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
_type = type;
|
||||
_controller = controller;
|
||||
_itemsDataSource = items;
|
||||
|
||||
auto *scrubber = [[[NSScrubber alloc] initWithFrame:NSZeroRect]
|
||||
autorelease];
|
||||
auto *layout = [[[NSScrubberFlowLayout alloc] init] autorelease];
|
||||
layout.itemSpacing = 10;
|
||||
scrubber.scrubberLayout = layout;
|
||||
scrubber.mode = NSScrubberModeFree;
|
||||
scrubber.delegate = self;
|
||||
scrubber.dataSource = self;
|
||||
scrubber.floatsSelectionViews = true;
|
||||
scrubber.showsAdditionalContentIndicators = true;
|
||||
scrubber.itemAlignment = NSScrubberAlignmentCenter;
|
||||
|
||||
[scrubber registerClass:[PickerScrubberItemView class]
|
||||
forItemIdentifier:kStickerItemIdentifier];
|
||||
[scrubber registerClass:[NSScrubberTextItemView class]
|
||||
forItemIdentifier:kPickerTitleItemIdentifier];
|
||||
[scrubber registerClass:[NSScrubberImageItemView class]
|
||||
forItemIdentifier:kEmojiItemIdentifier];
|
||||
|
||||
if (IsSticker(type)) {
|
||||
auto *gesture = [[[NSPressGestureRecognizer alloc]
|
||||
initWithTarget:self
|
||||
action:@selector(gesturePreviewHandler:)] autorelease];
|
||||
gesture.allowedTouchTypes = NSTouchTypeMaskDirect;
|
||||
gesture.minimumPressDuration = QApplication::startDragTime() / 1000.;
|
||||
gesture.allowableMovement = 0;
|
||||
[scrubber addGestureRecognizer:gesture];
|
||||
|
||||
const auto kRight = ChatRestriction::SendStickers;
|
||||
if (const auto error = RestrictionToSend(_controller, kRight)) {
|
||||
_error = std::make_unique<PickerScrubberItem>(
|
||||
tr::lng_restricted_send_stickers_all(tr::now));
|
||||
}
|
||||
} else {
|
||||
const auto kRight = ChatRestriction::SendOther;
|
||||
if (const auto error = RestrictionToSend(_controller, kRight)) {
|
||||
_error = std::make_unique<PickerScrubberItem>(
|
||||
tr::lng_restricted_send_message_all(tr::now));
|
||||
}
|
||||
}
|
||||
_lastPreviewedSticker = 0;
|
||||
|
||||
self.view = scrubber;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (PickerScrubberItem)itemAt:(int)index {
|
||||
return _error ? *_error : _itemsDataSource->at(index, _type);
|
||||
}
|
||||
|
||||
- (void)gesturePreviewHandler:(NSPressGestureRecognizer*)gesture {
|
||||
const auto customEnter = [=](auto &&callback) {
|
||||
Core::Sandbox::Instance().customEnterFromEventLoop([=] {
|
||||
if (_controller) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const auto checkState = [&](const auto &states) {
|
||||
return ranges::contains(states, gesture.state);
|
||||
};
|
||||
|
||||
if (checkState(kGestureStateProcessed)) {
|
||||
NSScrollView *scrollView = self.view;
|
||||
auto *container = scrollView.documentView.subviews.firstObject;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
const auto point = [gesture locationInView:container];
|
||||
|
||||
for (PickerScrubberItemView *item in container.subviews) {
|
||||
if (![item isMemberOfClass:[PickerScrubberItemView class]]
|
||||
|| (item->documentId == _lastPreviewedSticker)
|
||||
|| !NSPointInRect(point, item.frame)) {
|
||||
continue;
|
||||
}
|
||||
_lastPreviewedSticker = item->documentId;
|
||||
auto &owner = _controller->sessionController()->session().data();
|
||||
const auto doc = owner.document(item->documentId);
|
||||
customEnter([=] {
|
||||
_controller->widget()->showMediaPreview(
|
||||
Data::FileOrigin(),
|
||||
doc);
|
||||
});
|
||||
break;
|
||||
}
|
||||
} else if (checkState(kGestureStateFinished)) {
|
||||
customEnter([=] { _controller->widget()->hideMediaPreview(); });
|
||||
_lastPreviewedSticker = 0;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(nonnull NSCoder*)aCoder {
|
||||
// Has not been implemented.
|
||||
}
|
||||
|
||||
#pragma mark - NSScrubberDelegate
|
||||
|
||||
- (NSInteger)numberOfItemsForScrubber:(NSScrubber*)scrubber {
|
||||
return _error ? 1 : _itemsDataSource->size(_type);
|
||||
}
|
||||
|
||||
- (NSScrubberItemView*)scrubber:(NSScrubber*)scrubber
|
||||
viewForItemAtIndex:(NSInteger)index {
|
||||
const auto item = [self itemAt:index];
|
||||
if (const auto document = item.document) {
|
||||
PickerScrubberItemView *itemView = [scrubber
|
||||
makeItemWithIdentifier:kStickerItemIdentifier
|
||||
owner:self];
|
||||
itemView.imageView.image = Q2NSImage(item.image);
|
||||
itemView->documentId = document->id;
|
||||
return itemView;
|
||||
} else if (const auto emoji = item.emoji) {
|
||||
NSScrubberImageItemView *itemView = [scrubber
|
||||
makeItemWithIdentifier:kEmojiItemIdentifier
|
||||
owner:self];
|
||||
itemView.imageView.image = CreateNSImageFromEmoji(emoji);
|
||||
return itemView;
|
||||
} else {
|
||||
NSScrubberTextItemView *itemView = [scrubber
|
||||
makeItemWithIdentifier:kPickerTitleItemIdentifier
|
||||
owner:self];
|
||||
itemView.textField.stringValue = Q2NSString(item.title);
|
||||
return itemView;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSSize)scrubber:(NSScrubber*)scrubber
|
||||
layout:(NSScrubberFlowLayout*)layout
|
||||
sizeForItemAtIndex:(NSInteger)index {
|
||||
const auto t = [self itemAt:index].title;
|
||||
const auto w = t.isEmpty() ? 0 : TouchBar::WidthFromString(Q2NSString(t));
|
||||
return NSMakeSize(kCircleDiameter + w, kCircleDiameter);
|
||||
}
|
||||
|
||||
- (void)scrubber:(NSScrubber*)scrubber
|
||||
didSelectItemAtIndex:(NSInteger)index {
|
||||
scrubber.selectedIndex = -1;
|
||||
const auto sticker = _itemsDataSource->at(index, _type);
|
||||
const auto document = sticker.document;
|
||||
const auto emoji = sticker.emoji;
|
||||
const auto kRight = document
|
||||
? ChatRestriction::SendStickers
|
||||
: ChatRestriction::SendOther;
|
||||
if (!CanSendToActiveChat(_controller, kRight) || _error) {
|
||||
return;
|
||||
}
|
||||
auto callback = [=] {
|
||||
if (document) {
|
||||
if (const auto error = RestrictionToSend(_controller, kRight)) {
|
||||
_controller->show(Ui::MakeInformBox(*error));
|
||||
return true;
|
||||
} else if (Window::ShowSendPremiumError(_controller->sessionController(), document)) {
|
||||
return true;
|
||||
}
|
||||
Api::SendExistingDocument(
|
||||
Api::MessageToSend(
|
||||
Api::SendAction(ActiveChat(_controller).history())),
|
||||
document);
|
||||
return true;
|
||||
} else if (emoji) {
|
||||
if (const auto error = RestrictionToSend(_controller, kRight)) {
|
||||
_controller->show(Ui::MakeInformBox(*error));
|
||||
return true;
|
||||
} else if (const auto inputField = qobject_cast<QTextEdit*>(
|
||||
QApplication::focusWidget())) {
|
||||
Ui::InsertEmojiAtCursor(inputField->textCursor(), emoji);
|
||||
Core::App().settings().incrementRecentEmoji({ emoji });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!Core::Sandbox::Instance().customEnterFromEventLoop(
|
||||
std::move(callback))) {
|
||||
return;
|
||||
}
|
||||
|
||||
_closeRequests.fire({});
|
||||
}
|
||||
|
||||
- (rpl::producer<>)closeRequests {
|
||||
return _closeRequests.events();
|
||||
}
|
||||
|
||||
- (rpl::lifetime &)lifetime {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
@end // @implementation PickerCustomTouchBarItem
|
||||
|
||||
#pragma mark - StickerEmojiPopover
|
||||
|
||||
@implementation StickerEmojiPopover {
|
||||
Window::Controller *_controller;
|
||||
Main::Session *_session;
|
||||
std::shared_ptr<PickerScrubberItemsHolder> _itemsDataSource;
|
||||
ScrubberItemType _waitingForUpdate;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
}
|
||||
|
||||
- (id)init:(not_null<Window::Controller*>)controller
|
||||
identifier:(NSTouchBarItemIdentifier)identifier {
|
||||
self = [super initWithIdentifier:identifier];
|
||||
if (!self) {
|
||||
return nil;
|
||||
}
|
||||
_controller = controller;
|
||||
_session = &controller->sessionController()->session();
|
||||
_waitingForUpdate = ScrubberItemType::None;
|
||||
|
||||
auto *segment = [[[NSSegmentedControl alloc] init] autorelease];
|
||||
const auto size = kSegmentIconSize;
|
||||
segment.segmentStyle = NSSegmentStyleSeparated;
|
||||
segment.segmentCount = 2;
|
||||
[segment
|
||||
setImage:CreateNSImageFromStyleIcon(st::settingsIconStickers, size)
|
||||
forSegment:0];
|
||||
[segment
|
||||
setImage:CreateNSImageFromStyleIcon(st::settingsIconEmoji, size)
|
||||
forSegment:1];
|
||||
[segment setWidth:kSegmentSize forSegment:0];
|
||||
[segment setWidth:kSegmentSize forSegment:1];
|
||||
segment.target = self;
|
||||
segment.action = @selector(segmentClicked:);
|
||||
segment.trackingMode = NSSegmentSwitchTrackingMomentary;
|
||||
self.visibilityPriority = NSTouchBarItemPriorityHigh;
|
||||
self.collapsedRepresentation = segment;
|
||||
|
||||
self.popoverTouchBar = [[[NSTouchBar alloc] init] autorelease];
|
||||
self.popoverTouchBar.delegate = self;
|
||||
|
||||
controller->sessionController()->activeChatValue(
|
||||
) | rpl::map([](Dialogs::Key k) {
|
||||
const auto topic = k.topic();
|
||||
const auto peer = k.peer();
|
||||
const auto right = ChatRestriction::SendStickers;
|
||||
return peer
|
||||
&& (topic
|
||||
? Data::CanSend(topic, right)
|
||||
: Data::CanSend(peer, right));
|
||||
}) | rpl::distinct_until_changed(
|
||||
) | rpl::on_next([=](bool value) {
|
||||
[self dismissPopover:nil];
|
||||
}, _lifetime);
|
||||
|
||||
|
||||
_itemsDataSource = std::make_shared<PickerScrubberItemsHolder>();
|
||||
const auto localGuard = _lifetime.make_state<base::has_weak_ptr>();
|
||||
// Workaround.
|
||||
// A little waiting for the sticker sets and the ending animation.
|
||||
base::call_delayed(st::slideDuration, &(*localGuard), [=] {
|
||||
[self updateStickers];
|
||||
[self updateEmoji];
|
||||
});
|
||||
|
||||
rpl::merge(
|
||||
rpl::merge(
|
||||
_session->data().stickers().updated(
|
||||
Data::StickersType::Stickers),
|
||||
_session->data().stickers().recentUpdated(
|
||||
Data::StickersType::Stickers)
|
||||
) | rpl::map_to(ScrubberItemType::Sticker),
|
||||
rpl::merge(
|
||||
Core::App().settings().recentEmojiUpdated(),
|
||||
Ui::Emoji::Updated()
|
||||
) | rpl::map_to(ScrubberItemType::Emoji)
|
||||
) | rpl::on_next([=](ScrubberItemType type) {
|
||||
_waitingForUpdate = type;
|
||||
}, _lifetime);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSTouchBarItem*)touchBar:(NSTouchBar*)touchBar
|
||||
makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier {
|
||||
if (!touchBar) {
|
||||
return nil;
|
||||
}
|
||||
const auto isEqual = [&](NSString *string) {
|
||||
return [identifier isEqualToString:string];
|
||||
};
|
||||
|
||||
if (isEqual(kStickersScrubber)) {
|
||||
auto *item = [[[PickerCustomTouchBarItem alloc]
|
||||
init:(ScrubberItemType::Sticker)
|
||||
controller:_controller
|
||||
items:_itemsDataSource] autorelease];
|
||||
auto &lifetime = [item lifetime];
|
||||
[item closeRequests] | rpl::on_next([=] {
|
||||
[self dismissPopover:nil];
|
||||
[self updateStickers];
|
||||
}, lifetime);
|
||||
return item;
|
||||
} else if (isEqual(kEmojiScrubber)) {
|
||||
return [[[PickerCustomTouchBarItem alloc]
|
||||
init:(ScrubberItemType::Emoji)
|
||||
controller:_controller
|
||||
items:_itemsDataSource] autorelease];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)segmentClicked:(NSSegmentedControl*)sender {
|
||||
self.popoverTouchBar.defaultItemIdentifiers = @[];
|
||||
const auto identifier = sender.selectedSegment
|
||||
? kEmojiScrubber
|
||||
: kStickersScrubber;
|
||||
|
||||
if (sender.selectedSegment
|
||||
&& _waitingForUpdate == ScrubberItemType::Emoji) {
|
||||
[self updateEmoji];
|
||||
} else if (!sender.selectedSegment
|
||||
&& _waitingForUpdate == ScrubberItemType::Sticker) {
|
||||
[self updateStickers];
|
||||
}
|
||||
|
||||
self.popoverTouchBar.defaultItemIdentifiers = @[identifier];
|
||||
[self showPopover:nil];
|
||||
}
|
||||
|
||||
- (void)addDownloadHandler {
|
||||
const auto loadingLifetime = _lifetime.make_state<rpl::lifetime>();
|
||||
const auto checkLoaded = [=](const auto &sticker) {
|
||||
return !sticker.document || sticker.isStickerLoaded();
|
||||
};
|
||||
const auto isPerformedOnMain = loadingLifetime->make_state<bool>(true);
|
||||
const auto localGuard = loadingLifetime->make_state<base::has_weak_ptr>();
|
||||
_session->downloaderTaskFinished(
|
||||
) | rpl::on_next(crl::guard(&(*localGuard), [=] {
|
||||
if (*isPerformedOnMain) {
|
||||
crl::on_main(&(*localGuard), [=] {
|
||||
for (auto &sticker : _itemsDataSource->stickers) {
|
||||
sticker.updateThumbnail();
|
||||
}
|
||||
if (ranges::all_of(_itemsDataSource->stickers, checkLoaded)) {
|
||||
loadingLifetime->destroy();
|
||||
return;
|
||||
}
|
||||
*isPerformedOnMain = true;
|
||||
});
|
||||
}
|
||||
*isPerformedOnMain = false;
|
||||
}), *loadingLifetime);
|
||||
}
|
||||
|
||||
- (void)updateStickers {
|
||||
auto &stickers = _session->data().stickers();
|
||||
std::vector<PickerScrubberItem> temp;
|
||||
AppendFavedStickers(stickers.sets(), temp);
|
||||
AppendRecentStickers(stickers.sets(), stickers.getRecentPack(), temp);
|
||||
auto count = 0;
|
||||
for (const auto setId : stickers.setsOrderRef()) {
|
||||
AppendStickerSet(stickers.sets(), temp, setId);
|
||||
if (++count == kMaxStickerSets) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!temp.size()) {
|
||||
temp.emplace_back(PickerScrubberItem(
|
||||
tr::lng_stickers_nothing_found(tr::now)));
|
||||
}
|
||||
_itemsDataSource->stickers = std::move(temp);
|
||||
_waitingForUpdate = ScrubberItemType::None;
|
||||
[self addDownloadHandler];
|
||||
}
|
||||
|
||||
- (void)updateEmoji {
|
||||
std::vector<PickerScrubberItem> temp;
|
||||
AppendEmojiPacks(_session->data().stickers().sets(), temp);
|
||||
_itemsDataSource->emoji = std::move(temp);
|
||||
_waitingForUpdate = ScrubberItemType::None;
|
||||
}
|
||||
|
||||
@end // @implementation StickerEmojiPopover
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#import <AppKit/NSTouchBar.h>
|
||||
|
||||
API_AVAILABLE(macos(10.12.2))
|
||||
@interface TouchBarAudioPlayer : NSTouchBar<NSTouchBarDelegate>
|
||||
- (rpl::producer<>)closeRequests;
|
||||
@end
|
||||
176
Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_audio.mm
Normal file
176
Telegram/SourceFiles/platform/mac/touchbar/mac_touchbar_audio.mm
Normal file
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "platform/mac/touchbar/mac_touchbar_audio.h"
|
||||
|
||||
#include "media/audio/media_audio.h"
|
||||
#include "media/player/media_player_instance.h"
|
||||
#include "platform/mac/touchbar/mac_touchbar_common.h"
|
||||
#include "platform/mac/touchbar/mac_touchbar_controls.h"
|
||||
#include "styles/style_media_player.h"
|
||||
|
||||
#import <AppKit/NSButton.h>
|
||||
#import <AppKit/NSCustomTouchBarItem.h>
|
||||
#import <AppKit/NSSlider.h>
|
||||
#import <AppKit/NSSliderTouchBarItem.h>
|
||||
|
||||
using TouchBar::kCircleDiameter;
|
||||
using TouchBar::CreateNSImageFromStyleIcon;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kSongType = AudioMsgId::Type::Song;
|
||||
|
||||
const auto *kCustomizationIdPlayer = @"telegram.touchbar";
|
||||
|
||||
inline NSTouchBarItemIdentifier Format(NSString *s) {
|
||||
return [NSString stringWithFormat:@"%@.%@", kCustomizationIdPlayer, s];
|
||||
}
|
||||
const auto kSeekBarItemIdentifier = Format(@"seekbar");
|
||||
const auto kPlayItemIdentifier = Format(@"play");
|
||||
const auto kNextItemIdentifier = Format(@"nextItem");
|
||||
const auto kPreviousItemIdentifier = Format(@"previousItem");
|
||||
const auto kClosePlayerItemIdentifier = Format(@"closePlayer");
|
||||
const auto kCurrentPositionItemIdentifier = Format(@"currentPosition");
|
||||
|
||||
} // namespace
|
||||
|
||||
#pragma mark - TouchBarAudioPlayer
|
||||
|
||||
@interface TouchBarAudioPlayer()
|
||||
@end // @interface TouchBarAudioPlayer
|
||||
|
||||
@implementation TouchBarAudioPlayer {
|
||||
rpl::event_stream<> _closeRequests;
|
||||
rpl::producer< Media::Player::TrackState> _trackState;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
}
|
||||
|
||||
- (id)init {
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
self.delegate = self;
|
||||
self.customizationIdentifier = kCustomizationIdPlayer.lowercaseString;
|
||||
self.defaultItemIdentifiers = @[
|
||||
kPlayItemIdentifier,
|
||||
kPreviousItemIdentifier,
|
||||
kNextItemIdentifier,
|
||||
kSeekBarItemIdentifier,
|
||||
kClosePlayerItemIdentifier];
|
||||
self.customizationAllowedItemIdentifiers = @[
|
||||
kPlayItemIdentifier,
|
||||
kPreviousItemIdentifier,
|
||||
kNextItemIdentifier,
|
||||
kCurrentPositionItemIdentifier,
|
||||
kSeekBarItemIdentifier,
|
||||
kClosePlayerItemIdentifier];
|
||||
|
||||
_trackState = Media::Player::instance()->updatedNotifier(
|
||||
) | rpl::filter([=](const Media::Player::TrackState &state) {
|
||||
return state.id.type() == kSongType;
|
||||
});
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSTouchBarItem*)touchBar:(NSTouchBar*)touchBar
|
||||
makeItemForIdentifier:(NSTouchBarItemIdentifier)itemId {
|
||||
if (!touchBar) {
|
||||
return nil;
|
||||
}
|
||||
const auto mediaPlayer = Media::Player::instance();
|
||||
const auto isEqual = [&](NSString *string) {
|
||||
return [itemId isEqualToString:string];
|
||||
};
|
||||
|
||||
if (isEqual(kSeekBarItemIdentifier)) {
|
||||
auto *item = TouchBar::CreateTouchBarSlider(
|
||||
itemId,
|
||||
_lifetime,
|
||||
[=](bool touchUp, double value, double duration) {
|
||||
if (touchUp) {
|
||||
mediaPlayer->finishSeeking(kSongType, value);
|
||||
} else {
|
||||
mediaPlayer->startSeeking(kSongType);
|
||||
}
|
||||
},
|
||||
rpl::duplicate(_trackState));
|
||||
return [item autorelease];
|
||||
} else if (isEqual(kNextItemIdentifier)
|
||||
|| isEqual(kPreviousItemIdentifier)) {
|
||||
const auto isNext = isEqual(kNextItemIdentifier);
|
||||
auto *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:itemId];
|
||||
|
||||
auto *button = TouchBar::CreateTouchBarButton(
|
||||
isNext
|
||||
? st::touchBarIconPlayerNext
|
||||
: st::touchBarIconPlayerPrevious,
|
||||
_lifetime,
|
||||
[=] { isNext // TODO
|
||||
? mediaPlayer->next(kSongType)
|
||||
: mediaPlayer->previous(kSongType); });
|
||||
rpl::duplicate(
|
||||
_trackState
|
||||
) | rpl::on_next([=] {
|
||||
const auto newValue = isNext
|
||||
? mediaPlayer->nextAvailable(kSongType)
|
||||
: mediaPlayer->previousAvailable(kSongType);
|
||||
if (button.enabled != newValue) {
|
||||
button.enabled = newValue;
|
||||
}
|
||||
}, _lifetime);
|
||||
|
||||
item.view = button;
|
||||
item.customizationLabel = [NSString
|
||||
stringWithFormat:@"%@ Playlist Item",
|
||||
isNext ? @"Next" : @"Previous"];
|
||||
return [item autorelease];
|
||||
} else if (isEqual(kPlayItemIdentifier)) {
|
||||
auto *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:itemId];
|
||||
|
||||
auto *button = TouchBar::CreateTouchBarButtonWithTwoStates(
|
||||
st::touchBarIconPlayerPause,
|
||||
st::touchBarIconPlayerPlay,
|
||||
_lifetime,
|
||||
[=](bool value) { mediaPlayer->playPause(kSongType); },
|
||||
false,
|
||||
rpl::duplicate(
|
||||
_trackState
|
||||
) | rpl::map([](const auto &state) {
|
||||
return (state.state == Media::Player::State::Playing);
|
||||
}) | rpl::distinct_until_changed());
|
||||
|
||||
item.view = button;
|
||||
item.customizationLabel = @"Play/Pause";
|
||||
return [item autorelease];
|
||||
} else if (isEqual(kClosePlayerItemIdentifier)) {
|
||||
auto *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:itemId];
|
||||
auto *button = TouchBar::CreateTouchBarButton(
|
||||
st::touchBarIconPlayerClose,
|
||||
_lifetime,
|
||||
[=] { _closeRequests.fire({}); });
|
||||
|
||||
item.view = button;
|
||||
item.customizationLabel = @"Close Player";
|
||||
return [item autorelease];
|
||||
} else if (isEqual(kCurrentPositionItemIdentifier)) {
|
||||
auto *item = TouchBar::CreateTouchBarTrackPosition(
|
||||
itemId,
|
||||
rpl::duplicate(_trackState));
|
||||
return [item autorelease];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (rpl::producer<>)closeRequests {
|
||||
return _closeRequests.events();
|
||||
}
|
||||
|
||||
@end // @implementation TouchBarAudioPlayer
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#import <AppKit/NSImage.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
namespace TouchBar {
|
||||
|
||||
constexpr auto kCircleDiameter = 30;
|
||||
|
||||
template <typename Callable>
|
||||
void CustomEnterToCocoaEventLoop(Callable callable) {
|
||||
id block = [^{ callable(); } copy]; // Don't forget to -release.
|
||||
[block
|
||||
performSelectorOnMainThread:@selector(invoke)
|
||||
withObject:nil
|
||||
waitUntilDone:true];
|
||||
// [block performSelector:@selector(invoke) withObject:nil afterDelay:d];
|
||||
[block release];
|
||||
}
|
||||
|
||||
int WidthFromString(NSString *s);
|
||||
|
||||
NSImage *CreateNSImageFromStyleIcon(const style::icon &icon, int size);
|
||||
|
||||
} // namespace TouchBar
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "platform/mac/touchbar/mac_touchbar_common.h"
|
||||
|
||||
#include "base/platform/mac/base_utilities_mac.h"
|
||||
|
||||
#import <AppKit/NSTextField.h>
|
||||
|
||||
namespace TouchBar {
|
||||
|
||||
int WidthFromString(NSString *s) {
|
||||
return (int)ceil(
|
||||
[[NSTextField labelWithString:s] frame].size.width) * 1.2;
|
||||
}
|
||||
|
||||
NSImage *CreateNSImageFromStyleIcon(const style::icon &icon, int size) {
|
||||
auto instance = icon.instance(QColor(255, 255, 255, 255), 100);
|
||||
instance.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
NSImage *image = Platform::Q2NSImage(instance);
|
||||
[image setSize:NSMakeSize(size, size)];
|
||||
return image;
|
||||
}
|
||||
|
||||
} // namespace TouchBar
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Media {
|
||||
namespace Player {
|
||||
struct TrackState;
|
||||
} // namespace Player
|
||||
} // namespace Media
|
||||
|
||||
@class NSButton;
|
||||
@class NSCustomTouchBarItem;
|
||||
@class NSImage;
|
||||
@class NSSliderTouchBarItem;
|
||||
|
||||
namespace TouchBar {
|
||||
|
||||
[[nodiscard]] API_AVAILABLE(macos(10.12.2))
|
||||
NSButton *CreateTouchBarButton(
|
||||
NSImage *image,
|
||||
rpl::lifetime &lifetime,
|
||||
Fn<void()> callback);
|
||||
|
||||
[[nodiscard]] API_AVAILABLE(macos(10.12.2))
|
||||
NSButton *CreateTouchBarButton(
|
||||
const style::icon &icon,
|
||||
rpl::lifetime &lifetime,
|
||||
Fn<void()> callback);
|
||||
|
||||
[[nodiscard]] API_AVAILABLE(macos(10.12.2))
|
||||
NSButton *CreateTouchBarButtonWithTwoStates(
|
||||
NSImage *icon1,
|
||||
NSImage *icon2,
|
||||
rpl::lifetime &lifetime,
|
||||
Fn<void(bool)> callback,
|
||||
bool firstState,
|
||||
rpl::producer<bool> stateChanged = rpl::never<bool>());
|
||||
|
||||
[[nodiscard]] API_AVAILABLE(macos(10.12.2))
|
||||
NSButton *CreateTouchBarButtonWithTwoStates(
|
||||
const style::icon &icon1,
|
||||
const style::icon &icon2,
|
||||
rpl::lifetime &lifetime,
|
||||
Fn<void(bool)> callback,
|
||||
bool firstState,
|
||||
rpl::producer<bool> stateChanged = rpl::never<bool>());
|
||||
|
||||
[[nodiscard]] API_AVAILABLE(macos(10.12.2))
|
||||
NSSliderTouchBarItem *CreateTouchBarSlider(
|
||||
NSString *itemId,
|
||||
rpl::lifetime &lifetime,
|
||||
Fn<void(bool, double, double)> callback,
|
||||
rpl::producer<Media::Player::TrackState> stateChanged);
|
||||
|
||||
[[nodiscard]] API_AVAILABLE(macos(10.12.2))
|
||||
NSCustomTouchBarItem *CreateTouchBarTrackPosition(
|
||||
NSString *itemId,
|
||||
rpl::producer<Media::Player::TrackState> stateChanged);
|
||||
|
||||
} // namespace TouchBar
|
||||
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "platform/mac/touchbar/mac_touchbar_controls.h"
|
||||
|
||||
#include "base/platform/mac/base_utilities_mac.h" // Q2NSString()
|
||||
#include "core/sandbox.h" // Sandbox::customEnterFromEventLoop()
|
||||
#include "ui/text/format_values.h" // Ui::FormatDurationText()
|
||||
#include "media/audio/media_audio.h"
|
||||
#include "platform/mac/touchbar/mac_touchbar_common.h"
|
||||
|
||||
#import <AppKit/NSButton.h>
|
||||
#import <AppKit/NSCustomTouchBarItem.h>
|
||||
#import <AppKit/NSImage.h>
|
||||
#import <AppKit/NSImageView.h>
|
||||
#import <AppKit/NSSlider.h>
|
||||
#import <AppKit/NSSliderTouchBarItem.h>
|
||||
|
||||
using namespace TouchBar;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kPadding = 7;
|
||||
|
||||
inline NSImage *Icon(const style::icon &icon) {
|
||||
return CreateNSImageFromStyleIcon(icon, kCircleDiameter / 2);
|
||||
}
|
||||
|
||||
inline NSDictionary *Attributes() {
|
||||
return @{
|
||||
NSFontAttributeName: [NSFont systemFontOfSize:14],
|
||||
NSParagraphStyleAttributeName:
|
||||
[NSMutableParagraphStyle defaultParagraphStyle],
|
||||
NSForegroundColorAttributeName: [NSColor whiteColor]
|
||||
};
|
||||
}
|
||||
|
||||
inline NSString *FormatTime(TimeId time) {
|
||||
return Platform::Q2NSString(Ui::FormatDurationText(time));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
#pragma mark - TrackPosition
|
||||
|
||||
@interface TrackPosition : NSImageView
|
||||
@end // @interface TrackPosition
|
||||
|
||||
@implementation TrackPosition {
|
||||
NSMutableString *_text;
|
||||
|
||||
double _width;
|
||||
double _height;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
}
|
||||
|
||||
- (id)init:(rpl::producer< Media::Player::TrackState>)trackState {
|
||||
self = [super init];
|
||||
const auto textLength = _lifetime.make_state<rpl::variable<int>>(0);
|
||||
_width = _height = 0;
|
||||
_text = [[NSMutableString alloc] initWithCapacity:13];
|
||||
|
||||
rpl::combine(
|
||||
rpl::duplicate(
|
||||
trackState
|
||||
) | rpl::map([](const auto &state) {
|
||||
return state.position / 1000;
|
||||
}) | rpl::distinct_until_changed(),
|
||||
std::move(
|
||||
trackState
|
||||
) | rpl::map([](const auto &state) {
|
||||
return state.length / 1000;
|
||||
}) | rpl::distinct_until_changed()
|
||||
) | rpl::on_next([=](int position, int length) {
|
||||
[_text setString:[NSString stringWithFormat:@"%@ / %@",
|
||||
FormatTime(position),
|
||||
FormatTime(length)]];
|
||||
*textLength = _text.length;
|
||||
|
||||
[self display];
|
||||
}, _lifetime);
|
||||
|
||||
textLength->changes(
|
||||
) | rpl::on_next([=] {
|
||||
const auto size = [_text sizeWithAttributes:Attributes()];
|
||||
_width = size.width + kPadding * 2;
|
||||
_height = size.height;
|
||||
|
||||
if (self.image) {
|
||||
[self.image release];
|
||||
}
|
||||
self.image = [[NSImage alloc] initWithSize:NSMakeSize(
|
||||
_width,
|
||||
kCircleDiameter)];
|
||||
}, _lifetime);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)drawRect:(NSRect)dirtyRect {
|
||||
if (!(_text && _text.length && _width && _height)) {
|
||||
return;
|
||||
}
|
||||
const auto size = [_text sizeWithAttributes:Attributes()];
|
||||
const auto rect = CGRectMake(
|
||||
(_width - size.width) / 2,
|
||||
-(kCircleDiameter - _height) / 2,
|
||||
_width,
|
||||
kCircleDiameter);
|
||||
[_text drawInRect:rect withAttributes:Attributes()];
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
if (self.image) {
|
||||
[self.image release];
|
||||
}
|
||||
if (_text) {
|
||||
[_text release];
|
||||
}
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@end // @implementation TrackPosition
|
||||
|
||||
namespace TouchBar {
|
||||
|
||||
NSButton *CreateTouchBarButton(
|
||||
// const style::icon &icon,
|
||||
NSImage *image,
|
||||
rpl::lifetime &lifetime,
|
||||
Fn<void()> callback) {
|
||||
id block = [^{
|
||||
Core::Sandbox::Instance().customEnterFromEventLoop(callback);
|
||||
} copy];
|
||||
|
||||
NSButton* button = [NSButton
|
||||
buttonWithImage:image
|
||||
target:block
|
||||
action:@selector(invoke)];
|
||||
lifetime.add([=] {
|
||||
[block release];
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
NSButton *CreateTouchBarButton(
|
||||
const style::icon &icon,
|
||||
rpl::lifetime &lifetime,
|
||||
Fn<void()> callback) {
|
||||
return CreateTouchBarButton(Icon(icon), lifetime, std::move(callback));
|
||||
}
|
||||
|
||||
NSButton *CreateTouchBarButtonWithTwoStates(
|
||||
NSImage *icon1,
|
||||
NSImage *icon2,
|
||||
rpl::lifetime &lifetime,
|
||||
Fn<void(bool)> callback,
|
||||
bool firstState,
|
||||
rpl::producer<bool> stateChanged) {
|
||||
NSButton* button = [NSButton
|
||||
buttonWithImage:(firstState ? icon2 : icon1)
|
||||
target:nil
|
||||
action:nil];
|
||||
|
||||
const auto isFirstState = lifetime.make_state<bool>(firstState);
|
||||
id block = [^{
|
||||
const auto state = *isFirstState;
|
||||
button.image = state ? icon1 : icon2;
|
||||
*isFirstState = !state;
|
||||
Core::Sandbox::Instance().customEnterFromEventLoop([=] {
|
||||
callback(state);
|
||||
});
|
||||
} copy];
|
||||
|
||||
button.target = block;
|
||||
button.action = @selector(invoke);
|
||||
|
||||
std::move(
|
||||
stateChanged
|
||||
) | rpl::on_next([=](bool isChangedToFirstState) {
|
||||
button.image = isChangedToFirstState ? icon1 : icon2;
|
||||
}, lifetime);
|
||||
|
||||
lifetime.add([=] {
|
||||
[block release];
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
NSButton *CreateTouchBarButtonWithTwoStates(
|
||||
const style::icon &icon1,
|
||||
const style::icon &icon2,
|
||||
rpl::lifetime &lifetime,
|
||||
Fn<void(bool)> callback,
|
||||
bool firstState,
|
||||
rpl::producer<bool> stateChanged) {
|
||||
return CreateTouchBarButtonWithTwoStates(
|
||||
Icon(icon1),
|
||||
Icon(icon2),
|
||||
lifetime,
|
||||
std::move(callback),
|
||||
firstState,
|
||||
std::move(stateChanged));
|
||||
}
|
||||
|
||||
NSSliderTouchBarItem *CreateTouchBarSlider(
|
||||
NSString *itemId,
|
||||
rpl::lifetime &lifetime,
|
||||
Fn<void(bool, double, double)> callback,
|
||||
rpl::producer<Media::Player::TrackState> stateChanged) {
|
||||
const auto lastDurationMs = lifetime.make_state<crl::time>(0);
|
||||
|
||||
auto *seekBar = [[NSSliderTouchBarItem alloc] initWithIdentifier:itemId];
|
||||
seekBar.slider.minValue = 0.0f;
|
||||
seekBar.slider.maxValue = 1.0f;
|
||||
seekBar.customizationLabel = @"Seek Bar";
|
||||
|
||||
id block = [^{
|
||||
// https://stackoverflow.com/a/45891017
|
||||
auto *event = [[NSApplication sharedApplication] currentEvent];
|
||||
const auto touchUp = [event
|
||||
touchesMatchingPhase:NSTouchPhaseEnded
|
||||
inView:nil].count > 0;
|
||||
Core::Sandbox::Instance().customEnterFromEventLoop([=] {
|
||||
callback(touchUp, seekBar.slider.doubleValue, *lastDurationMs);
|
||||
});
|
||||
} copy];
|
||||
|
||||
std::move(
|
||||
stateChanged
|
||||
) | rpl::on_next([=](const Media::Player::TrackState &state) {
|
||||
const auto stop = Media::Player::IsStoppedOrStopping(state.state);
|
||||
const auto duration = double(stop ? 0 : state.length);
|
||||
auto slider = seekBar.slider;
|
||||
if (duration <= 0) {
|
||||
slider.enabled = false;
|
||||
slider.doubleValue = 0;
|
||||
} else {
|
||||
slider.enabled = true;
|
||||
if (!slider.highlighted) {
|
||||
const auto pos = stop
|
||||
? 0
|
||||
: std::max(state.position, int64(0));
|
||||
slider.doubleValue = (pos / duration) * slider.maxValue;
|
||||
*lastDurationMs = duration;
|
||||
}
|
||||
}
|
||||
}, lifetime);
|
||||
|
||||
seekBar.target = block;
|
||||
seekBar.action = @selector(invoke);
|
||||
lifetime.add([=] {
|
||||
[block release];
|
||||
});
|
||||
|
||||
return seekBar;
|
||||
}
|
||||
|
||||
NSCustomTouchBarItem *CreateTouchBarTrackPosition(
|
||||
NSString *itemId,
|
||||
rpl::producer<Media::Player::TrackState> stateChanged) {
|
||||
auto *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:itemId];
|
||||
auto *trackPosition = [[[TrackPosition alloc]
|
||||
init:std::move(stateChanged)] autorelease];
|
||||
|
||||
item.view = trackPosition;
|
||||
item.customizationLabel = @"Track Position";
|
||||
return item;
|
||||
}
|
||||
|
||||
} // namespace TouchBar
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#import <AppKit/NSTouchBar.h>
|
||||
|
||||
namespace Window {
|
||||
class Controller;
|
||||
} // namespace Window
|
||||
|
||||
namespace TouchBar::Main {
|
||||
|
||||
const auto kPinnedPanelItemIdentifier = @"pinnedPanel";
|
||||
const auto kPopoverInputItemIdentifier = @"popoverInput";
|
||||
const auto kPopoverPickerItemIdentifier = @"pickerButtons";
|
||||
|
||||
} // namespace TouchBar::Main
|
||||
|
||||
API_AVAILABLE(macos(10.12.2))
|
||||
@interface TouchBarMain : NSTouchBar
|
||||
- (id)init:(not_null<Window::Controller*>)controller
|
||||
touchBarSwitches:(rpl::producer<>)touchBarSwitches;
|
||||
@end
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "platform/mac/touchbar/mac_touchbar_main.h"
|
||||
|
||||
#include "platform/mac/touchbar/items/mac_formatter_item.h"
|
||||
#include "platform/mac/touchbar/items/mac_pinned_chats_item.h"
|
||||
#include "platform/mac/touchbar/items/mac_scrubber_item.h"
|
||||
#include "platform/mac/touchbar/mac_touchbar_common.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "window/window_session_controller.h"
|
||||
|
||||
#import <AppKit/NSCustomTouchBarItem.h>
|
||||
|
||||
using namespace TouchBar::Main;
|
||||
|
||||
#pragma mark - TouchBarMain
|
||||
|
||||
@interface TouchBarMain()
|
||||
@end // @interface TouchBarMain
|
||||
|
||||
@implementation TouchBarMain
|
||||
|
||||
- (id)init:(not_null<Window::Controller*>)controller
|
||||
touchBarSwitches:(rpl::producer<>)touchBarSwitches {
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
auto *pin = [[[NSCustomTouchBarItem alloc]
|
||||
initWithIdentifier:kPinnedPanelItemIdentifier] autorelease];
|
||||
pin.view = [[[PinnedDialogsPanel alloc]
|
||||
init:(&controller->sessionController()->session())
|
||||
destroyEvent:std::move(touchBarSwitches)] autorelease];
|
||||
|
||||
auto *sticker = [[[StickerEmojiPopover alloc]
|
||||
init:controller
|
||||
identifier:kPopoverPickerItemIdentifier] autorelease];
|
||||
|
||||
auto *format = [[[TextFormatPopover alloc]
|
||||
init:kPopoverInputItemIdentifier] autorelease];
|
||||
|
||||
self.templateItems = [NSSet setWithArray:@[pin, sticker, format]];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end // @implementation TouchBarMain
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#import <AppKit/NSTouchBar.h>
|
||||
|
||||
namespace Main {
|
||||
class Domain;
|
||||
} // namespace Main
|
||||
|
||||
namespace Window {
|
||||
class Controller;
|
||||
} // namespace Window
|
||||
|
||||
namespace Ui {
|
||||
struct MarkdownEnabledState;
|
||||
} // namespace Ui
|
||||
|
||||
API_AVAILABLE(macos(10.12.2))
|
||||
@interface RootTouchBar : NSTouchBar<NSTouchBarDelegate>
|
||||
- (id)init:(rpl::producer<Ui::MarkdownEnabledState>)markdownState
|
||||
controller:(not_null<Window::Controller*>)controller
|
||||
domain:(not_null<Main::Domain*>)domain;
|
||||
@end
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "platform/mac/touchbar/mac_touchbar_manager.h"
|
||||
|
||||
#include "apiwrap.h" // ApiWrap::updateStickers()
|
||||
#include "core/application.h"
|
||||
#include "data/data_chat_participant_status.h" // Data::CanSendAnyOf.
|
||||
#include "data/data_forum_topic.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/stickers/data_stickers.h" // Stickers::setsRef()
|
||||
#include "main/main_domain.h"
|
||||
#include "main/main_session.h"
|
||||
#include "media/audio/media_audio_capture.h"
|
||||
#include "media/player/media_player_instance.h"
|
||||
#include "platform/mac/touchbar/mac_touchbar_audio.h"
|
||||
#include "platform/mac/touchbar/mac_touchbar_common.h"
|
||||
#include "platform/mac/touchbar/mac_touchbar_main.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "window/window_session_controller.h"
|
||||
|
||||
#import <AppKit/NSGroupTouchBarItem.h>
|
||||
|
||||
using namespace TouchBar::Main;
|
||||
|
||||
namespace {
|
||||
|
||||
const auto kMainItemIdentifier = @"touchbarMain";
|
||||
const auto kAudioItemIdentifier = @"touchbarAudio";
|
||||
|
||||
} // namespace
|
||||
|
||||
@interface GroupTouchBarItem : NSGroupTouchBarItem
|
||||
- (rpl::lifetime &)lifetime;
|
||||
@end // @interface GroupTouchBarItem
|
||||
|
||||
@implementation GroupTouchBarItem {
|
||||
rpl::lifetime _lifetime;
|
||||
}
|
||||
|
||||
- (rpl::lifetime &)lifetime {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
@end // GroupTouchBarItem
|
||||
|
||||
#pragma mark - RootTouchBar
|
||||
|
||||
@interface RootTouchBar()
|
||||
@end // @interface RootTouchBar
|
||||
|
||||
@implementation RootTouchBar {
|
||||
Main::Session *_session;
|
||||
Window::Controller *_controller;
|
||||
|
||||
rpl::variable<Ui::MarkdownEnabledState> _markdownState;
|
||||
rpl::event_stream<> _touchBarSwitches;
|
||||
rpl::lifetime _lifetime;
|
||||
}
|
||||
|
||||
- (id)init:(rpl::producer<Ui::MarkdownEnabledState>)markdownState
|
||||
controller:(not_null<Window::Controller*>)controller
|
||||
domain:(not_null<Main::Domain*>)domain {
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
self.delegate = self;
|
||||
TouchBar::CustomEnterToCocoaEventLoop([=] {
|
||||
self.defaultItemIdentifiers = @[];
|
||||
});
|
||||
_controller = controller;
|
||||
_markdownState = std::move(markdownState);
|
||||
|
||||
auto sessionChanges = domain->activeSessionChanges(
|
||||
) | rpl::map([=](Main::Session *session) {
|
||||
if (session && session->data().stickers().setsRef().empty()) {
|
||||
session->api().updateStickers();
|
||||
}
|
||||
return session;
|
||||
});
|
||||
|
||||
const auto type = AudioMsgId::Type::Song;
|
||||
auto audioPlayer = rpl::merge(
|
||||
Media::Player::instance()->stops(type) | rpl::map_to(false),
|
||||
Media::Player::instance()->startsPlay(type) | rpl::map_to(true)
|
||||
);
|
||||
|
||||
auto voiceRecording = ::Media::Capture::instance()->startedChanges();
|
||||
|
||||
rpl::combine(
|
||||
std::move(sessionChanges),
|
||||
rpl::single(false) | rpl::then(Core::App().passcodeLockChanges()),
|
||||
rpl::single(false) | rpl::then(std::move(audioPlayer)),
|
||||
rpl::single(false) | rpl::then(std::move(voiceRecording))
|
||||
) | rpl::on_next([=](
|
||||
Main::Session *session,
|
||||
bool lock,
|
||||
bool audio,
|
||||
bool recording) {
|
||||
TouchBar::CustomEnterToCocoaEventLoop([=] {
|
||||
_touchBarSwitches.fire({});
|
||||
if (!audio) {
|
||||
self.defaultItemIdentifiers = @[];
|
||||
}
|
||||
self.defaultItemIdentifiers = (lock || recording)
|
||||
? @[]
|
||||
: audio
|
||||
? @[kAudioItemIdentifier]
|
||||
: session
|
||||
? @[kMainItemIdentifier]
|
||||
: @[];
|
||||
});
|
||||
}, _lifetime);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSTouchBarItem*)touchBar:(NSTouchBar*)touchBar
|
||||
makeItemForIdentifier:(NSTouchBarItemIdentifier)itemId {
|
||||
if (!touchBar || !_controller->sessionController()) {
|
||||
return nil;
|
||||
}
|
||||
const auto isEqual = [&](NSString *string) {
|
||||
return [itemId isEqualToString:string];
|
||||
};
|
||||
|
||||
if (isEqual(kMainItemIdentifier)) {
|
||||
auto *item = [[GroupTouchBarItem alloc] initWithIdentifier:itemId];
|
||||
item.groupTouchBar
|
||||
= [[[TouchBarMain alloc]
|
||||
init:_controller
|
||||
touchBarSwitches:_touchBarSwitches.events()] autorelease];
|
||||
rpl::combine(
|
||||
_markdownState.value(),
|
||||
_controller->sessionController()->activeChatValue(
|
||||
) | rpl::map([](Dialogs::Key k) {
|
||||
const auto topic = k.topic();
|
||||
const auto peer = k.peer();
|
||||
const auto rights = ChatRestriction::SendStickers
|
||||
| ChatRestriction::SendOther;
|
||||
return topic
|
||||
? Data::CanSendAnyOf(topic, rights)
|
||||
: (peer && Data::CanSendAnyOf(peer, rights));
|
||||
}) | rpl::distinct_until_changed()
|
||||
) | rpl::on_next([=](
|
||||
Ui::MarkdownEnabledState state,
|
||||
bool hasActiveChat) {
|
||||
item.groupTouchBar.defaultItemIdentifiers = @[
|
||||
kPinnedPanelItemIdentifier,
|
||||
(!state.disabled()
|
||||
? kPopoverInputItemIdentifier
|
||||
: hasActiveChat
|
||||
? kPopoverPickerItemIdentifier
|
||||
: @"")];
|
||||
}, [item lifetime]);
|
||||
|
||||
return [item autorelease];
|
||||
} else if (isEqual(kAudioItemIdentifier)) {
|
||||
auto *item = [[GroupTouchBarItem alloc] initWithIdentifier:itemId];
|
||||
auto *touchBar = [[[TouchBarAudioPlayer alloc] init]
|
||||
autorelease];
|
||||
item.groupTouchBar = touchBar;
|
||||
[touchBar closeRequests] | rpl::on_next([=] {
|
||||
Media::Player::instance()->stopAndClose();
|
||||
}, [item lifetime]);
|
||||
return [item autorelease];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end // @implementation RootTouchBar
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "media/view/media_view_playback_controls.h"
|
||||
#include "media/view/media_view_overlay_widget.h"
|
||||
|
||||
namespace TouchBar {
|
||||
|
||||
void SetupMediaViewTouchBar(
|
||||
WId winId,
|
||||
not_null<Media::View::PlaybackControls::Delegate*> controlsDelegate,
|
||||
rpl::producer<Media::Player::TrackState> trackState,
|
||||
rpl::producer<Media::View::OverlayWidget::TouchBarItemType> display,
|
||||
rpl::producer<bool> fullscreenToggled);
|
||||
|
||||
} // namespace TouchBar
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "platform/mac/touchbar/mac_touchbar_media_view.h"
|
||||
|
||||
#include "media/audio/media_audio.h"
|
||||
#include "platform/mac/touchbar/mac_touchbar_common.h"
|
||||
#include "platform/mac/touchbar/mac_touchbar_controls.h"
|
||||
#include "styles/style_media_player.h"
|
||||
#include "styles/style_media_view.h"
|
||||
|
||||
#import <AppKit/NSButton.h>
|
||||
#import <AppKit/NSCustomTouchBarItem.h>
|
||||
#import <AppKit/NSTouchBar.h>
|
||||
|
||||
using namespace TouchBar;
|
||||
using Delegate = Media::View::PlaybackControls::Delegate;
|
||||
using ItemType = Media::View::OverlayWidget::TouchBarItemType;
|
||||
|
||||
namespace {
|
||||
|
||||
inline NSTouchBarItemIdentifier Format(NSString *s) {
|
||||
return [NSString stringWithFormat:@"button.%@", s];
|
||||
}
|
||||
|
||||
const auto kPlayItemIdentifier = Format(@"playPause");
|
||||
const auto kRotateItemIdentifier = Format(@"rotate");
|
||||
const auto kFullscreenItemIdentifier = Format(@"fullscreen");
|
||||
const auto kPipItemIdentifier = Format(@"pip");
|
||||
const auto kTrackItemIdentifier = @"trackPosition";
|
||||
const auto kSeekItemIdentifier = @"seekBar";
|
||||
|
||||
}
|
||||
|
||||
#pragma mark - MediaViewTouchBar
|
||||
|
||||
@interface MediaViewTouchBar : NSTouchBar
|
||||
- (id)init:(not_null<Delegate*>)controlsDelegate
|
||||
trackState:(rpl::producer<Media::Player::TrackState>)trackState
|
||||
display:(rpl::producer<ItemType>)display
|
||||
fullscreenToggled:(rpl::producer<bool>)fullscreenToggled;
|
||||
@end
|
||||
|
||||
@implementation MediaViewTouchBar {
|
||||
rpl::lifetime _lifetime;
|
||||
}
|
||||
|
||||
- (id)init:(not_null<Delegate*>)controlsDelegate
|
||||
trackState:(rpl::producer<Media::Player::TrackState>)trackState
|
||||
display:(rpl::producer<ItemType>)display
|
||||
fullscreenToggled:(rpl::producer<bool>)fullscreenToggled {
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
const auto allocate = [](NSTouchBarItemIdentifier i) {
|
||||
return [[NSCustomTouchBarItem alloc] initWithIdentifier:i];
|
||||
};
|
||||
|
||||
auto *playPause = allocate(kPlayItemIdentifier);
|
||||
{
|
||||
auto *button = CreateTouchBarButtonWithTwoStates(
|
||||
st::touchBarIconPlayerPause,
|
||||
st::touchBarIconPlayerPlay,
|
||||
_lifetime,
|
||||
[=](bool value) {
|
||||
value
|
||||
? controlsDelegate->playbackControlsPlay()
|
||||
: controlsDelegate->playbackControlsPause();
|
||||
},
|
||||
false,
|
||||
rpl::duplicate(
|
||||
trackState
|
||||
) | rpl::map([](const auto &state) {
|
||||
return (state.state == Media::Player::State::Playing);
|
||||
}) | rpl::distinct_until_changed());
|
||||
playPause.view = button;
|
||||
playPause.customizationLabel = @"Play/Pause";
|
||||
}
|
||||
|
||||
auto *rotate = allocate(kRotateItemIdentifier);
|
||||
{
|
||||
auto *button = CreateTouchBarButton(
|
||||
[NSImage imageNamed:NSImageNameTouchBarRotateLeftTemplate],
|
||||
_lifetime,
|
||||
[=] { controlsDelegate->playbackControlsRotate(); });
|
||||
rotate.view = button;
|
||||
rotate.customizationLabel = @"Rotate";
|
||||
}
|
||||
|
||||
auto *fullscreen = allocate(kFullscreenItemIdentifier);
|
||||
{
|
||||
auto *button = CreateTouchBarButtonWithTwoStates(
|
||||
[NSImage imageNamed:NSImageNameTouchBarExitFullScreenTemplate],
|
||||
[NSImage imageNamed:NSImageNameTouchBarEnterFullScreenTemplate],
|
||||
_lifetime,
|
||||
[=](bool value) {
|
||||
value
|
||||
? controlsDelegate->playbackControlsFromFullScreen()
|
||||
: controlsDelegate->playbackControlsToFullScreen();
|
||||
},
|
||||
true,
|
||||
std::move(fullscreenToggled));
|
||||
fullscreen.view = button;
|
||||
fullscreen.customizationLabel = @"Fullscreen";
|
||||
}
|
||||
|
||||
auto *pip = allocate(kPipItemIdentifier);
|
||||
{
|
||||
auto *button = TouchBar::CreateTouchBarButton(
|
||||
CreateNSImageFromStyleIcon(
|
||||
st::mediaviewPipButton.icon,
|
||||
kCircleDiameter / 4 * 3),
|
||||
_lifetime,
|
||||
[=] { controlsDelegate->playbackControlsToPictureInPicture(); });
|
||||
pip.view = button;
|
||||
pip.customizationLabel = @"Picture-in-Picture";
|
||||
}
|
||||
|
||||
auto *trackPosition = CreateTouchBarTrackPosition(
|
||||
kTrackItemIdentifier,
|
||||
rpl::duplicate(trackState));
|
||||
|
||||
auto *seekBar = TouchBar::CreateTouchBarSlider(
|
||||
kSeekItemIdentifier,
|
||||
_lifetime,
|
||||
[=](bool touchUp, double value, double duration) {
|
||||
const auto progress = value * duration;
|
||||
touchUp
|
||||
? controlsDelegate->playbackControlsSeekFinished(progress)
|
||||
: controlsDelegate->playbackControlsSeekProgress(progress);
|
||||
},
|
||||
std::move(trackState));
|
||||
|
||||
self.templateItems = [NSSet setWithArray:@[
|
||||
playPause,
|
||||
rotate,
|
||||
fullscreen,
|
||||
pip,
|
||||
seekBar,
|
||||
trackPosition]];
|
||||
|
||||
const auto items = [](ItemType type) {
|
||||
switch (type) {
|
||||
case ItemType::Photo: return @[kRotateItemIdentifier];
|
||||
case ItemType::Video: return @[
|
||||
kRotateItemIdentifier,
|
||||
kFullscreenItemIdentifier,
|
||||
kPipItemIdentifier,
|
||||
kPlayItemIdentifier,
|
||||
kSeekItemIdentifier,
|
||||
kTrackItemIdentifier];
|
||||
default: return @[];
|
||||
};
|
||||
};
|
||||
|
||||
std::move(
|
||||
display
|
||||
) | rpl::distinct_until_changed(
|
||||
) | rpl::on_next([=](ItemType type) {
|
||||
TouchBar::CustomEnterToCocoaEventLoop([=] {
|
||||
self.defaultItemIdentifiers = items(type);
|
||||
});
|
||||
}, _lifetime);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end // @implementation MediaViewTouchBar
|
||||
|
||||
namespace TouchBar {
|
||||
|
||||
void SetupMediaViewTouchBar(
|
||||
WId winId,
|
||||
not_null<Delegate*> controlsDelegate,
|
||||
rpl::producer<Media::Player::TrackState> trackState,
|
||||
rpl::producer<ItemType> display,
|
||||
rpl::producer<bool> fullscreenToggled) {
|
||||
auto *window = [reinterpret_cast<NSView*>(winId) window];
|
||||
CustomEnterToCocoaEventLoop([=] {
|
||||
[window setTouchBar:[[[MediaViewTouchBar alloc]
|
||||
init:std::move(controlsDelegate)
|
||||
trackState:std::move(trackState)
|
||||
display:std::move(display)
|
||||
fullscreenToggled:std::move(fullscreenToggled)
|
||||
] autorelease]];
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace TouchBar
|
||||
62
Telegram/SourceFiles/platform/mac/tray_mac.h
Normal file
62
Telegram/SourceFiles/platform/mac/tray_mac.h
Normal 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
|
||||
422
Telegram/SourceFiles/platform/mac/tray_mac.mm
Normal file
422
Telegram/SourceFiles/platform/mac/tray_mac.mm
Normal 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
|
||||
297
Telegram/SourceFiles/platform/mac/webauthn_mac.mm
Normal file
297
Telegram/SourceFiles/platform/mac/webauthn_mac.mm
Normal 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
|
||||
179
Telegram/SourceFiles/platform/mac/window_title_mac.mm
Normal file
179
Telegram/SourceFiles/platform/mac/window_title_mac.mm
Normal 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
|
||||
@@ -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
|
||||
43
Telegram/SourceFiles/platform/platform_file_bookmark.h
Normal file
43
Telegram/SourceFiles/platform/platform_file_bookmark.h
Normal 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
|
||||
52
Telegram/SourceFiles/platform/platform_file_utilities.h
Normal file
52
Telegram/SourceFiles/platform/platform_file_utilities.h
Normal 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
|
||||
43
Telegram/SourceFiles/platform/platform_integration.cpp
Normal file
43
Telegram/SourceFiles/platform/platform_integration.cpp
Normal 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
|
||||
23
Telegram/SourceFiles/platform/platform_integration.h
Normal file
23
Telegram/SourceFiles/platform/platform_integration.h
Normal 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
|
||||
30
Telegram/SourceFiles/platform/platform_launcher.h
Normal file
30
Telegram/SourceFiles/platform/platform_launcher.h
Normal 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
|
||||
26
Telegram/SourceFiles/platform/platform_main_window.h
Normal file
26
Telegram/SourceFiles/platform/platform_main_window.h
Normal 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
|
||||
@@ -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
|
||||
259
Telegram/SourceFiles/platform/platform_overlay_widget.cpp
Normal file
259
Telegram/SourceFiles/platform/platform_overlay_widget.cpp
Normal 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
|
||||
104
Telegram/SourceFiles/platform/platform_overlay_widget.h
Normal file
104
Telegram/SourceFiles/platform/platform_overlay_widget.h
Normal 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
|
||||
77
Telegram/SourceFiles/platform/platform_specific.h
Normal file
77
Telegram/SourceFiles/platform/platform_specific.h
Normal 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
|
||||
41
Telegram/SourceFiles/platform/platform_text_recognition.h
Normal file
41
Telegram/SourceFiles/platform/platform_text_recognition.h
Normal 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
|
||||
26
Telegram/SourceFiles/platform/platform_tray.h
Normal file
26
Telegram/SourceFiles/platform/platform_tray.h
Normal 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
|
||||
50
Telegram/SourceFiles/platform/platform_webauthn.h
Normal file
50
Telegram/SourceFiles/platform/platform_webauthn.h
Normal 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
|
||||
40
Telegram/SourceFiles/platform/platform_window_title.h
Normal file
40
Telegram/SourceFiles/platform/platform_window_title.h
Normal 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
|
||||
@@ -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
|
||||
10
Telegram/SourceFiles/platform/win/current_geo_location_win.h
Normal file
10
Telegram/SourceFiles/platform/win/current_geo_location_win.h
Normal 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"
|
||||
473
Telegram/SourceFiles/platform/win/file_utilities_win.cpp
Normal file
473
Telegram/SourceFiles/platform/win/file_utilities_win.cpp
Normal 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
|
||||
24
Telegram/SourceFiles/platform/win/file_utilities_win.h
Normal file
24
Telegram/SourceFiles/platform/win/file_utilities_win.h
Normal 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
|
||||
189
Telegram/SourceFiles/platform/win/integration_win.cpp
Normal file
189
Telegram/SourceFiles/platform/win/integration_win.cpp
Normal 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
|
||||
51
Telegram/SourceFiles/platform/win/integration_win.h
Normal file
51
Telegram/SourceFiles/platform/win/integration_win.h
Normal 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
|
||||
134
Telegram/SourceFiles/platform/win/launcher_win.cpp
Normal file
134
Telegram/SourceFiles/platform/win/launcher_win.cpp
Normal 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
|
||||
32
Telegram/SourceFiles/platform/win/launcher_win.h
Normal file
32
Telegram/SourceFiles/platform/win/launcher_win.h
Normal 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
|
||||
849
Telegram/SourceFiles/platform/win/main_window_win.cpp
Normal file
849
Telegram/SourceFiles/platform/win/main_window_win.cpp
Normal 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
|
||||
113
Telegram/SourceFiles/platform/win/main_window_win.h
Normal file
113
Telegram/SourceFiles/platform/win/main_window_win.h
Normal 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
|
||||
978
Telegram/SourceFiles/platform/win/notifications_manager_win.cpp
Normal file
978
Telegram/SourceFiles/platform/win/notifications_manager_win.cpp
Normal 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('&', "&").toStdWString();
|
||||
const auto fastReply = LR"(
|
||||
<input id="fastReply" type="text" placeHolderContent=""/>
|
||||
<action
|
||||
content="Send"
|
||||
arguments="action=reply&)" + wid + LR"("
|
||||
activationType="background"
|
||||
imageUri=""
|
||||
hint-inputId="fastReply"/>
|
||||
)";
|
||||
const auto markAsRead = LR"(
|
||||
<action
|
||||
content=""
|
||||
arguments="action=mark&)" + wid + LR"("
|
||||
activationType="background"/>
|
||||
)";
|
||||
const auto actions = (options.hideReplyButton ? L"" : fastReply)
|
||||
+ (options.hideMarkAsRead ? L"" : markAsRead);
|
||||
return LR"(
|
||||
<toast launch="action=open&)" + 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 ¬ification);
|
||||
[[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 ¬ification) {
|
||||
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
|
||||
@@ -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
|
||||
20
Telegram/SourceFiles/platform/win/overlay_widget_win.h
Normal file
20
Telegram/SourceFiles/platform/win/overlay_widget_win.h
Normal 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
|
||||
728
Telegram/SourceFiles/platform/win/specific_win.cpp
Normal file
728
Telegram/SourceFiles/platform/win/specific_win.cpp
Normal 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"
|
||||
71
Telegram/SourceFiles/platform/win/specific_win.h
Normal file
71
Telegram/SourceFiles/platform/win/specific_win.h
Normal 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() {
|
||||
}
|
||||
24
Telegram/SourceFiles/platform/win/text_recognition_win.h
Normal file
24
Telegram/SourceFiles/platform/win/text_recognition_win.h
Normal 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
|
||||
448
Telegram/SourceFiles/platform/win/tray_win.cpp
Normal file
448
Telegram/SourceFiles/platform/win/tray_win.cpp
Normal 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
|
||||
79
Telegram/SourceFiles/platform/win/tray_win.h
Normal file
79
Telegram/SourceFiles/platform/win/tray_win.h
Normal 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
|
||||
297
Telegram/SourceFiles/platform/win/webauthn_win.cpp
Normal file
297
Telegram/SourceFiles/platform/win/webauthn_win.cpp
Normal 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 ¶m : 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
|
||||
507
Telegram/SourceFiles/platform/win/windows_app_user_model_id.cpp
Normal file
507
Telegram/SourceFiles/platform/win/windows_app_user_model_id.cpp
Normal 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
Reference in New Issue
Block a user