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
|
||||
Reference in New Issue
Block a user