init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

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

View File

@@ -0,0 +1,40 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
class QString;
namespace base::Platform {
#ifdef Q_OS_MAC
std::optional<uint64> SetCustomAppIcon(QImage image);
std::optional<uint64> SetCustomAppIcon(const QString &path);
std::optional<uint64> CurrentCustomAppIconDigest();
bool ClearCustomAppIcon();
#else // Q_OS_MAC
inline std::optional<uint64> SetCustomAppIcon(QImage image) {
return std::nullopt;
}
inline std::optional<uint64> SetCustomAppIcon(const QString &path) {
return std::nullopt;
}
inline std::optional<uint64> CurrentCustomAppIconDigest() {
return std::nullopt;
}
inline bool ClearCustomAppIcon() {
return false;
}
#endif // Q_OS_MAC
} // namespace base::Platform

View File

@@ -0,0 +1,8 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/base_platform_file_utilities.h"

View File

@@ -0,0 +1,26 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
class QString;
class QFile;
namespace base::Platform {
void ShowInFolder(const QString &filepath);
[[nodiscard]] QString FileNameFromUserString(QString name);
bool DeleteDirectory(QString path);
void RemoveQuarantine(const QString &path);
[[nodiscard]] QString CurrentExecutablePath(int argc, char *argv[]);
[[nodiscard]] QString BundledResourcesPath();
bool RenameWithOverwrite(const QString &from, const QString &to);
void FlushFileData(QFile &file);
} // namespace base::Platform

View File

@@ -0,0 +1,25 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/global_shortcuts_generic.h"
class QKeyEvent;
namespace base::Platform::GlobalShortcuts {
[[nodiscard]] bool Available();
[[nodiscard]] bool Allowed();
void Start(Fn<void(GlobalShortcutKeyGeneric descriptor, bool down)> process);
void Stop();
[[nodiscard]] QString KeyName(GlobalShortcutKeyGeneric descriptor);
[[nodiscard]] bool IsToggleFullScreenKey(not_null<QKeyEvent*> e);
} // namespace base::Platform::GlobalShortcuts

View File

@@ -0,0 +1,14 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
namespace base::Platform {
void Haptic();
[[nodiscard]] bool IsSwipeBackEnabled();
} // namespace base::Platform

View File

@@ -0,0 +1,63 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/base_platform_info.h"
#include "base/algorithm.h"
namespace base::Platform {
namespace {
constexpr auto kMaxDeviceModelLength = 15;
constexpr auto kMaxGoodDeviceModelLength = 32;
} // namespace
bool IsDeviceModelOk(const QString &model) {
return !model.isEmpty() && (model.size() <= kMaxDeviceModelLength);
}
QString SimplifyDeviceModel(QString model) {
return CleanAndSimplify(model.replace(QChar('_'), QString()));
}
QString SimplifyGoodDeviceModel(QString model, std::vector<QString> remove) {
const auto limit = kMaxGoodDeviceModelLength;
auto result = QString();
for (const auto &word : model.split(QChar(' '))) {
if (ranges::contains(remove, word.toLower())) {
continue;
} else if (result.isEmpty()) {
result = word;
} else if (result.size() + word.size() + 1 > limit) {
return result;
} else {
result += ' ' + word;
}
}
return result;
}
QString ProductNameToDeviceModel(const QString &productName) {
if (productName.startsWith("HP ")) {
// Some special cases for good strings, like HP laptops.
return SimplifyGoodDeviceModel(
productName,
{ "notebook", "desktop", "mobile", "workstation", "pc" });
} else if (IsDeviceModelOk(productName)) {
return productName;
}
return {};
}
QString FinalizeDeviceModel(QString model) {
using namespace ::Platform;
model = std::move(model).trimmed();
return !model.isEmpty() ? model : IsMac() ? u"Mac"_q : u"Desktop"_q;
}
} // namespace base::Platform

View File

@@ -0,0 +1,84 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include <vector>
class QJsonObject;
class QString;
class QDate;
namespace base::Platform {
[[nodiscard]] bool IsDeviceModelOk(const QString &model);
[[nodiscard]] QString SimplifyDeviceModel(QString model);
[[nodiscard]] QString SimplifyGoodDeviceModel(
QString model,
std::vector<QString> remove);
[[nodiscard]] QString ProductNameToDeviceModel(const QString &productName);
[[nodiscard]] QString FinalizeDeviceModel(QString model);
} // namespace base::Platform
namespace Platform {
enum class OutdateReason {
IsOld,
Is32Bit,
};
[[nodiscard]] QString DeviceModelPretty();
[[nodiscard]] QString SystemVersionPretty();
[[nodiscard]] QString SystemCountry();
[[nodiscard]] QString SystemLanguage();
[[nodiscard]] QDate WhenSystemBecomesOutdated();
[[nodiscard]] OutdateReason WhySystemBecomesOutdated();
[[nodiscard]] int AutoUpdateVersion();
[[nodiscard]] QString AutoUpdateKey();
[[nodiscard]] constexpr bool IsWindows();
[[nodiscard]] constexpr bool IsWindows32Bit();
[[nodiscard]] constexpr bool IsWindows64Bit();
[[nodiscard]] constexpr bool IsWindowsARM64();
[[nodiscard]] constexpr bool IsWindowsStoreBuild();
[[nodiscard]] bool IsWindows7OrGreater();
[[nodiscard]] bool IsWindows8OrGreater();
[[nodiscard]] bool IsWindows8Point1OrGreater();
[[nodiscard]] bool IsWindows10OrGreater();
[[nodiscard]] bool IsWindows11OrGreater();
[[nodiscard]] constexpr bool IsMac();
[[nodiscard]] constexpr bool IsMacStoreBuild();
[[nodiscard]] bool IsMac10_12OrGreater();
[[nodiscard]] bool IsMac10_13OrGreater();
[[nodiscard]] bool IsMac10_14OrGreater();
[[nodiscard]] bool IsMac10_15OrGreater();
[[nodiscard]] bool IsMac11_0OrGreater();
[[nodiscard]] bool IsMac26_0OrGreater();
[[nodiscard]] constexpr bool IsLinux();
[[nodiscard]] bool IsX11();
[[nodiscard]] bool IsWayland();
[[nodiscard]] bool IsXwayland();
[[nodiscard]] QString GetLibcName();
[[nodiscard]] QString GetLibcVersion();
[[nodiscard]] QString GetWindowManager();
void Start(QJsonObject settings);
void Finish();
} // namespace Platform
#ifdef Q_OS_WIN
#include "base/platform/win/base_info_win.h"
#elif defined Q_OS_MAC // Q_OS_WIN
#include "base/platform/mac/base_info_mac.h"
#else // Q_OS_WIN || Q_OS_MAC
#include "base/platform/linux/base_info_linux.h"
#endif // else for Q_OS_WIN || Q_OS_MAC

View File

@@ -0,0 +1,16 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
namespace base::Platform {
[[nodiscard]] std::optional<crl::time> LastUserInputTime();
[[nodiscard]] inline bool LastUserInputTimeSupported() {
return LastUserInputTime().has_value();
}
} // namespace base::Platform

View File

@@ -0,0 +1,13 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
namespace base::Platform {
bool SwitchKeyboardLayoutToEnglish();
} // namespace base::Platform

View File

@@ -0,0 +1,20 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
namespace base::Platform {
class NetworkReachability {
public:
virtual ~NetworkReachability() = default;
static std::unique_ptr<NetworkReachability> Create();
[[nodiscard]] virtual rpl::producer<bool> availableValue() const = 0;
};
} // namespace base::Platform

View File

@@ -0,0 +1,31 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/power_save_blocker.h"
namespace base::Platform {
inline constexpr int kPowerSaveBlockTypeCount = static_cast<int>(
PowerSaveBlockType::kCount);
[[nodiscard]] inline constexpr int PowerSaveBlockTypeIndex(
PowerSaveBlockType type) {
Expects(static_cast<int>(type) >= 0);
Expects(type < PowerSaveBlockType::kCount);
return static_cast<int>(type);
}
// window may be null.
void BlockPowerSave(
PowerSaveBlockType type,
const QString &description,
QPointer<QWindow> window);
void UnblockPowerSave(PowerSaveBlockType type, QPointer<QWindow> window);
} // namespace base::Platform

View File

@@ -0,0 +1,18 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/basic_types.h"
#include <QtGui/QWindow>
namespace base::Platform {
void ActivateProcessWindow(int64 pid, WId windowId);
void ActivateThisProcessWindow(WId windowId);
} // namespace base::Platform

View File

@@ -0,0 +1,82 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
namespace base::Platform {
class SystemMediaControls {
public:
enum class Command {
PlayPause,
Play,
Pause,
Next,
Previous,
Stop,
Quit,
Raise,
LoopNone,
LoopTrack,
LoopPlaylist,
Shuffle,
None,
};
enum class PlaybackStatus {
Playing,
Paused,
Stopped,
};
enum class LoopStatus {
None,
Track,
Playlist,
};
SystemMediaControls();
~SystemMediaControls();
bool init();
[[nodiscard]] bool seekingSupported() const;
[[nodiscard]] bool volumeSupported() const;
void setApplicationName(const QString &name);
void setEnabled(bool enabled);
void setIsNextEnabled(bool value);
void setIsPreviousEnabled(bool value);
void setIsPlayPauseEnabled(bool value);
void setIsStopEnabled(bool value);
void setPlaybackStatus(PlaybackStatus status);
void setLoopStatus(LoopStatus status);
void setShuffle(bool value);
void setTitle(const QString &title);
void setArtist(const QString &artist);
void setThumbnail(const QImage &thumbnail);
void setDuration(int duration);
void setPosition(int position);
void setVolume(float64 volume);
void clearThumbnail();
void clearMetadata();
void updateDisplay();
[[nodiscard]] rpl::producer<Command> commandRequests() const;
[[nodiscard]] rpl::producer<float64> seekRequests() const;
[[nodiscard]] rpl::producer<float64> volumeChangeRequests() const;
[[nodiscard]] rpl::producer<> updatePositionRequests() const;
static bool Supported();
private:
struct Private;
const std::unique_ptr<Private> _private;
};
} // namespace base::Platform

View File

@@ -0,0 +1,26 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
namespace base::Platform {
struct UrlSchemeDescriptor {
QString executable; // Full path.
QString arguments; // Additional arguments.
QString protocol; // 'myprotocol'
QString protocolName; // "My Protocol Link"
QString shortAppName; // "myapp"
QString longAppName; // "MyApplication"
QString displayAppName; // "My Application"
QString displayAppDescription; // "My Nice Application"
};
[[nodiscard]] bool CheckUrlScheme(const UrlSchemeDescriptor &descriptor);
void RegisterUrlScheme(const UrlSchemeDescriptor &descriptor);
void UnregisterUrlScheme(const UrlSchemeDescriptor &descriptor);
} // namespace base::Platform

View File

@@ -0,0 +1,109 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/linux/base_battery_saving_linux.h"
#include "base/battery_saving.h"
#include "base/integration.h"
#include <QtCore/QDir>
#include <gio/gio.h>
#include <dlfcn.h>
extern "C" {
typedef struct _GPowerProfileMonitor GPowerProfileMonitor;
} // extern "C"
namespace base::Platform {
namespace {
class BatterySaving final : public AbstractBatterySaving {
public:
BatterySaving(Fn<void()> changedCallback);
~BatterySaving();
std::optional<bool> enabled() const override;
private:
GPowerProfileMonitor *_monitor = nullptr;
gulong _handlerId = 0;
Fn<void()> _changedCallback;
};
BatterySaving::BatterySaving(Fn<void()> changedCallback)
: _changedCallback(std::move(changedCallback)) {
// Detect battery
if (QDir(u"/sys/class/power_supply"_q).isEmpty()) {
return;
}
// glib 2.70+, we keep glib 2.56+ compatibility
static const auto dup_default = [] {
// reset dlerror after dlsym call
const auto guard = gsl::finally([] { dlerror(); });
return reinterpret_cast<GPowerProfileMonitor*(*)()>(
dlsym(RTLD_DEFAULT, "g_power_profile_monitor_dup_default"));
}();
if (!dup_default) {
return;
}
_monitor = dup_default();
if (_changedCallback) {
_handlerId = g_signal_connect_swapped(
_monitor,
"notify::power-saver-enabled",
G_CALLBACK(+[](BatterySaving *instance) {
Integration::Instance().enterFromEventLoop([&] {
instance->_changedCallback();
});
}), this);
}
}
BatterySaving::~BatterySaving() {
if (_monitor) {
if (_handlerId) {
g_signal_handler_disconnect(_monitor, _handlerId);
}
g_object_unref(_monitor);
}
}
std::optional<bool> BatterySaving::enabled() const {
if (!_monitor) {
return std::nullopt;
}
// glib 2.70+, we keep glib 2.40+ compatibility
static const auto get_power_saver_enabled = [] {
// reset dlerror after dlsym call
const auto guard = gsl::finally([] { dlerror(); });
return reinterpret_cast<gboolean(*)(GPowerProfileMonitor*)>(
dlsym(
RTLD_DEFAULT,
"g_power_profile_monitor_get_power_saver_enabled"));
}();
if (!get_power_saver_enabled) {
return std::nullopt;
}
return get_power_saver_enabled(_monitor);
}
} // namespace
std::unique_ptr<AbstractBatterySaving> CreateBatterySaving(
Fn<void()> changedCallback) {
return std::make_unique<BatterySaving>(std::move(changedCallback));
}
} // namespace base::Platform

View File

@@ -0,0 +1,8 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once

View File

@@ -0,0 +1,222 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/linux/base_file_utilities_linux.h"
#include "base/platform/base_platform_file_utilities.h"
#include "base/platform/linux/base_linux_xdp_utilities.h"
#include "base/platform/linux/base_linux_xdg_activation_token.h"
#include "base/algorithm.h"
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
#include <QtCore/QStandardPaths>
#include <QtGui/QDesktopServices>
#include <xdpopenuri/xdpopenuri.hpp>
#include <xdgfilemanager1/xdgfilemanager1.hpp>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>
#include <stdio.h>
#include <fcntl.h>
namespace base::Platform {
namespace {
using namespace gi::repository;
namespace GObject = gi::repository::GObject;
void PortalShowInFolder(const QString &filepath, Fn<void()> fail) {
XdpOpenURI::OpenURIProxy::new_for_bus(
Gio::BusType::SESSION_,
Gio::DBusProxyFlags::NONE_,
XDP::kService,
XDP::kObjectPath,
[=](GObject::Object, Gio::AsyncResult res) {
auto interface = XdpOpenURI::OpenURI(
XdpOpenURI::OpenURIProxy::new_for_bus_finish(res, nullptr));
if (!interface) {
fail();
return;
}
const auto fd = open(
QFile::encodeName(filepath).constData(),
O_RDONLY | O_CLOEXEC);
if (fd == -1) {
fail();
return;
}
RunWithXdgActivationToken([=](
const QString &activationToken) mutable {
interface.call_open_directory(
XDP::ParentWindowID(),
GLib::Variant::new_handle(0),
GLib::Variant::new_array({
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("activation_token"),
GLib::Variant::new_variant(
GLib::Variant::new_string(
activationToken.toStdString()))),
}),
Gio::UnixFDList::new_from_array(&fd, 1),
{},
[=](GObject::Object, Gio::AsyncResult res) mutable {
if (!interface.call_open_directory_finish(res)) {
fail();
}
});
});
});
}
void DBusShowInFolder(const QString &filepath, Fn<void()> fail) {
XdgFileManager1::FileManager1Proxy::new_for_bus(
Gio::BusType::SESSION_,
Gio::DBusProxyFlags::NONE_,
"org.freedesktop.FileManager1",
"/org/freedesktop/FileManager1",
[=](GObject::Object, Gio::AsyncResult res) {
auto interface = XdgFileManager1::FileManager1(
XdgFileManager1::FileManager1Proxy::new_for_bus_finish(
res,
nullptr));
if (!interface) {
fail();
return;
}
RunWithXdgActivationToken([=](const QString &startupId) mutable {
const auto callbackWrap = gi::unwrap(
Gio::AsyncReadyCallback(
[=](GObject::Object, Gio::AsyncResult res) mutable {
if (!interface.call_show_items_finish(res)) {
fail();
}
}
),
gi::scope_async);
xdg_file_manager1_file_manager1_call_show_items(
interface.gobj_(),
(std::array<const char*, 2>{
GLib::filename_to_uri(
filepath.toStdString(),
nullptr
).c_str(),
nullptr,
}).data(),
startupId.toStdString().c_str(),
nullptr,
&callbackWrap->wrapper,
callbackWrap);
});
});
}
} // namespace
void ShowInFolder(const QString &filepath) {
DBusShowInFolder(filepath, [=] {
PortalShowInFolder(filepath, [=] {
QDesktopServices::openUrl(
QUrl::fromLocalFile(QFileInfo(filepath).absolutePath()));
});
});
}
QString CurrentExecutablePath(int argc, char *argv[]) {
const auto exeLink = QFileInfo(u"/proc/%1/exe"_q.arg(getpid()));
if (exeLink.exists() && exeLink.isSymLink()) {
return exeLink.canonicalFilePath();
}
// Fallback to the first command line argument.
if (argc) {
const auto argv0 = QFile::decodeName(argv[0]);
if (!argv0.isEmpty() && !argv0.contains(QLatin1Char('/'))) {
const auto argv0InPath = QStandardPaths::findExecutable(argv0);
if (!argv0InPath.isEmpty()) {
return argv0InPath;
}
}
return QFileInfo(argv0).absoluteFilePath();
}
return QString();
}
void RemoveQuarantine(const QString &path) {
}
QString BundledResourcesPath() {
Unexpected("BundledResourcesPath not implemented.");
}
QString FileNameFromUserString(QString name) {
return name;
}
// From http://stackoverflow.com/questions/2256945/removing-a-non-empty-directory-programmatically-in-c-or-c
bool DeleteDirectory(QString path) {
if (path.endsWith('/')) {
path.chop(1);
}
const auto pathRaw = QFile::encodeName(path);
const auto d = opendir(pathRaw.constData());
if (!d) {
return false;
}
while (struct dirent *p = readdir(d)) {
// Skip the names "." and ".." as we don't want to recurse on them.
if (!strcmp(p->d_name, ".") || !strcmp(p->d_name, "..")) {
continue;
}
const auto fname = path + '/' + p->d_name;
const auto encoded = QFile::encodeName(fname);
struct stat statbuf;
if (!stat(encoded.constData(), &statbuf)) {
if (S_ISDIR(statbuf.st_mode)) {
if (!DeleteDirectory(fname)) {
closedir(d);
return false;
}
} else {
if (unlink(encoded.constData())) {
closedir(d);
return false;
}
}
}
}
closedir(d);
return !rmdir(pathRaw.constData());
}
bool RenameWithOverwrite(const QString &from, const QString &to) {
const auto fromPath = QFile::encodeName(from);
const auto toPath = QFile::encodeName(to);
return (rename(fromPath.constData(), toPath.constData()) == 0);
}
void FlushFileData(QFile &file) {
file.flush();
if (const auto descriptor = file.handle()) {
fsync(descriptor);
}
}
} // namespace base::Platform

View File

@@ -0,0 +1,7 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once

View File

@@ -0,0 +1,672 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/linux/base_global_shortcuts_linux.h"
#include "base/const_string.h"
#include "base/global_shortcuts_generic.h"
#include "base/platform/base_platform_info.h" // IsX11
#include "base/debug_log.h"
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
#include "base/platform/linux/base_linux_xcb_utilities.h" // CustomConnection, IsExtensionPresent
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
#include <QKeySequence>
#include <QSocketNotifier>
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
#include <xcb/record.h>
#include <xcb/xcb.h>
#include <xcb/xcb_keysyms.h> // xcb_key_symbols_*
#include <xcb/xcbext.h> // xcb_poll_for_reply
#include <xkbcommon/xkbcommon-keysyms.h>
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
class QKeyEvent;
namespace base::Platform::GlobalShortcuts {
namespace {
constexpr auto kShiftMouseButton = std::numeric_limits<uint64>::max() - 100;
Fn<void(GlobalShortcutKeyGeneric descriptor, bool down)> ProcessCallback;
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
using XcbReply = xcb_record_enable_context_reply_t;
bool IsKeypad(xcb_keysym_t keysym) {
return (xcb_is_keypad_key(keysym) || xcb_is_private_keypad_key(keysym));
}
bool SkipMouseButton(xcb_button_t b) {
return (b == 1) // Ignore the left button.
|| (b > 3 && b < 8); // Ignore the wheel.
}
class X11Manager final {
public:
X11Manager();
~X11Manager();
[[nodiscard]] bool available() const;
private:
void process(XcbReply *reply);
xcb_keysym_t computeKeysym(xcb_keycode_t detail, uint16_t state);
XCB::CustomConnection _connection;
std::unique_ptr<
xcb_key_symbols_t,
custom_delete<xcb_key_symbols_free>
> _keySymbols;
std::unique_ptr<QSocketNotifier> _notifier;
xcb_record_context_t _context = XCB_NONE;
xcb_record_enable_context_cookie_t _cookie = { XCB_NONE };
};
X11Manager::X11Manager()
: _keySymbols(xcb_key_symbols_alloc(_connection)) {
if (xcb_connection_has_error(_connection)) {
LOG((
"Global Shortcuts Manager: Error to open local display!"));
return;
}
if (!XCB::IsExtensionPresent(_connection, &xcb_record_id)) {
LOG(("Global Shortcuts Manager: "
"RECORD extension not supported on this X server!"));
return;
}
_context = xcb_generate_id(_connection);
const xcb_record_client_spec_t clientSpec[] = {
XCB_RECORD_CS_ALL_CLIENTS
};
const xcb_record_range_t recordRange[] = {
[] {
xcb_record_range_t rr;
memset(&rr, 0, sizeof(rr));
// XCB_KEY_PRESS = 2
// XCB_KEY_RELEASE = 3
// XCB_BUTTON_PRESS = 4
// XCB_BUTTON_RELEASE = 5
rr.device_events = { XCB_KEY_PRESS, XCB_BUTTON_RELEASE };
return rr;
}()
};
const auto createCookie = xcb_record_create_context_checked(
_connection,
_context,
0,
sizeof(clientSpec) / sizeof(clientSpec[0]),
sizeof(recordRange) / sizeof(recordRange[0]),
clientSpec,
recordRange);
if (const auto error = xcb_request_check(_connection, createCookie)) {
LOG((
"Global Shortcuts Manager: Could not create a record context!"));
_context = XCB_NONE;
free(error);
return;
}
_cookie = xcb_record_enable_context(_connection, _context);
xcb_flush(_connection);
_notifier = std::make_unique<QSocketNotifier>(
xcb_get_file_descriptor(_connection),
QSocketNotifier::Read);
QObject::connect(_notifier.get(), &QSocketNotifier::activated, [=] {
while (const auto event = xcb_poll_for_event(_connection)) {
free(event);
}
void *reply = nullptr;
xcb_generic_error_t *error = nullptr;
while (_cookie.sequence
&& xcb_poll_for_reply(
_connection,
_cookie.sequence,
&reply,
&error)) {
// The xcb_poll_for_reply method may set both reply and error
// to null if connection has error.
if (xcb_connection_has_error(_connection)) {
break;
}
if (error) {
free(error);
break;
}
if (!reply) {
continue;
}
process(reinterpret_cast<XcbReply*>(reply));
free(reply);
}
});
_notifier->setEnabled(true);
}
X11Manager::~X11Manager() {
if (_cookie.sequence) {
xcb_record_disable_context(_connection, _context);
_cookie = { XCB_NONE };
}
if (_context) {
xcb_record_free_context(_connection, _context);
_context = XCB_NONE;
}
}
void X11Manager::process(XcbReply *reply) {
if (!ProcessCallback) {
return;
}
// Seems like xcb_button_press_event_t and xcb_key_press_event_t structs
// are the same, so we can safely cast both of them
// to the xcb_key_press_event_t.
const auto events = reinterpret_cast<xcb_key_press_event_t*>(
xcb_record_enable_context_data(reply));
const auto countEvents = xcb_record_enable_context_data_length(reply) /
sizeof(xcb_key_press_event_t);
for (auto e = events; e < (events + countEvents); e++) {
const auto type = e->response_type;
const auto buttonPress = (type == XCB_BUTTON_PRESS);
const auto buttonRelease = (type == XCB_BUTTON_RELEASE);
const auto keyPress = (type == XCB_KEY_PRESS);
const auto keyRelease = (type == XCB_KEY_RELEASE);
const auto isButton = (buttonPress || buttonRelease);
if (!(keyPress || keyRelease || isButton)) {
continue;
}
const auto code = e->detail;
if (isButton && SkipMouseButton(code)) {
return;
}
const auto descriptor = isButton
? (kShiftMouseButton + code)
: GlobalShortcutKeyGeneric(computeKeysym(code, e->state));
ProcessCallback(descriptor, keyPress || buttonPress);
}
}
xcb_keysym_t X11Manager::computeKeysym(xcb_keycode_t detail, uint16_t state) {
// Perhaps XCB_MOD_MASK_1-5 are needed here.
const auto keySym1 = xcb_key_symbols_get_keysym(_keySymbols.get(), detail, 1);
if (IsKeypad(keySym1)) {
return keySym1;
}
if (keySym1 >= Qt::Key_A && keySym1 <= Qt::Key_Z) {
if (keySym1 != XCB_NO_SYMBOL) {
return keySym1;
}
}
return xcb_key_symbols_get_keysym(_keySymbols.get(), detail, 0);
}
bool X11Manager::available() const {
return _cookie.sequence;
}
std::unique_ptr<X11Manager> _x11Manager = nullptr;
void EnsureX11ShortcutManager() {
if (!_x11Manager) {
_x11Manager = std::make_unique<X11Manager>();
}
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
} // namespace
bool Available() {
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
if (::Platform::IsX11()) {
EnsureX11ShortcutManager();
return _x11Manager->available();
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
return false;
}
bool Allowed() {
return Available();
}
void Start(Fn<void(GlobalShortcutKeyGeneric descriptor, bool down)> process) {
ProcessCallback = std::move(process);
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
EnsureX11ShortcutManager();
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
}
void Stop() {
ProcessCallback = nullptr;
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
_x11Manager = nullptr;
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
}
QString KeyName(GlobalShortcutKeyGeneric descriptor) {
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
// Telegram/ThirdParty/fcitx-qt5/platforminputcontext/qtkey.cpp
static const auto KeyToString = flat_map<uint64, int>{
{ XKB_KEY_KP_Space, Qt::Key_Space },
{ XKB_KEY_KP_Tab, Qt::Key_Tab },
{ XKB_KEY_KP_Enter, Qt::Key_Enter },
{ XKB_KEY_KP_F1, Qt::Key_F1 },
{ XKB_KEY_KP_F2, Qt::Key_F2 },
{ XKB_KEY_KP_F3, Qt::Key_F3 },
{ XKB_KEY_KP_F4, Qt::Key_F4 },
{ XKB_KEY_KP_Home, Qt::Key_Home },
{ XKB_KEY_KP_Left, Qt::Key_Left },
{ XKB_KEY_KP_Up, Qt::Key_Up },
{ XKB_KEY_KP_Right, Qt::Key_Right },
{ XKB_KEY_KP_Down, Qt::Key_Down },
{ XKB_KEY_KP_Page_Up, Qt::Key_PageUp },
{ XKB_KEY_KP_Page_Down, Qt::Key_PageDown },
{ XKB_KEY_KP_End, Qt::Key_End },
{ XKB_KEY_KP_Begin, Qt::Key_Clear },
{ XKB_KEY_KP_Insert, Qt::Key_Insert },
{ XKB_KEY_KP_Delete, Qt::Key_Delete },
{ XKB_KEY_KP_Equal, Qt::Key_Equal },
{ XKB_KEY_KP_Multiply, Qt::Key_multiply },
{ XKB_KEY_KP_Add, Qt::Key_Plus },
{ XKB_KEY_KP_Separator, Qt::Key_Comma },
{ XKB_KEY_KP_Subtract, Qt::Key_Minus },
{ XKB_KEY_KP_Decimal, Qt::Key_Period },
{ XKB_KEY_KP_Divide, Qt::Key_Slash },
{ XKB_KEY_KP_0, Qt::Key_0 },
{ XKB_KEY_KP_1, Qt::Key_1 },
{ XKB_KEY_KP_2, Qt::Key_2 },
{ XKB_KEY_KP_3, Qt::Key_3 },
{ XKB_KEY_KP_4, Qt::Key_4 },
{ XKB_KEY_KP_5, Qt::Key_5 },
{ XKB_KEY_KP_6, Qt::Key_6 },
{ XKB_KEY_KP_7, Qt::Key_7 },
{ XKB_KEY_KP_8, Qt::Key_8 },
{ XKB_KEY_KP_9, Qt::Key_9 },
{ XKB_KEY_Escape, Qt::Key_Escape },
{ XKB_KEY_Tab, Qt::Key_Tab },
{ XKB_KEY_ISO_Left_Tab, Qt::Key_Tab },
{ XKB_KEY_BackSpace, Qt::Key_Backspace },
{ XKB_KEY_Return, Qt::Key_Return },
{ XKB_KEY_KP_Enter, Qt::Key_Enter },
{ XKB_KEY_Insert, Qt::Key_Insert },
{ XKB_KEY_Delete, Qt::Key_Delete },
{ XKB_KEY_Clear, Qt::Key_Delete },
{ XKB_KEY_Pause, Qt::Key_Pause },
{ XKB_KEY_Print, Qt::Key_Print },
{ XKB_KEY_Sys_Req, Qt::Key_SysReq },
{ XKB_KEY_SunSys_Req, Qt::Key_SysReq },
{ 0x1007ff00, Qt::Key_SysReq },
{ XKB_KEY_Home, Qt::Key_Home },
{ XKB_KEY_End, Qt::Key_End },
{ XKB_KEY_Left, Qt::Key_Left },
{ XKB_KEY_Up, Qt::Key_Up },
{ XKB_KEY_Right, Qt::Key_Right },
{ XKB_KEY_Down, Qt::Key_Down },
{ XKB_KEY_Page_Up, Qt::Key_PageUp },
{ XKB_KEY_Page_Down, Qt::Key_PageDown },
{ XKB_KEY_Shift_L, Qt::Key_Shift },
{ XKB_KEY_Shift_R, Qt::Key_Shift },
{ XKB_KEY_Shift_Lock, Qt::Key_Shift },
{ XKB_KEY_Control_L, Qt::Key_Control },
{ XKB_KEY_Control_R, Qt::Key_Control },
{ XKB_KEY_Meta_L, Qt::Key_Meta },
{ XKB_KEY_Meta_R, Qt::Key_Meta },
{ XKB_KEY_Alt_L, Qt::Key_Alt },
{ XKB_KEY_Alt_R, Qt::Key_Alt },
{ XKB_KEY_Caps_Lock, Qt::Key_CapsLock },
{ XKB_KEY_Num_Lock, Qt::Key_NumLock },
{ XKB_KEY_Scroll_Lock, Qt::Key_ScrollLock },
{ XKB_KEY_F1, Qt::Key_F1 },
{ XKB_KEY_F2, Qt::Key_F2 },
{ XKB_KEY_F3, Qt::Key_F3 },
{ XKB_KEY_F4, Qt::Key_F4 },
{ XKB_KEY_F5, Qt::Key_F5 },
{ XKB_KEY_F6, Qt::Key_F6 },
{ XKB_KEY_F7, Qt::Key_F7 },
{ XKB_KEY_F8, Qt::Key_F8 },
{ XKB_KEY_F9, Qt::Key_F9 },
{ XKB_KEY_F10, Qt::Key_F10 },
{ XKB_KEY_F11, Qt::Key_F11 },
{ XKB_KEY_F12, Qt::Key_F12 },
{ XKB_KEY_F13, Qt::Key_F13 },
{ XKB_KEY_F14, Qt::Key_F14 },
{ XKB_KEY_F15, Qt::Key_F15 },
{ XKB_KEY_F16, Qt::Key_F16 },
{ XKB_KEY_F17, Qt::Key_F17 },
{ XKB_KEY_F18, Qt::Key_F18 },
{ XKB_KEY_F19, Qt::Key_F19 },
{ XKB_KEY_F20, Qt::Key_F20 },
{ XKB_KEY_F21, Qt::Key_F21 },
{ XKB_KEY_F22, Qt::Key_F22 },
{ XKB_KEY_F23, Qt::Key_F23 },
{ XKB_KEY_F24, Qt::Key_F24 },
{ XKB_KEY_F25, Qt::Key_F25 },
{ XKB_KEY_F26, Qt::Key_F26 },
{ XKB_KEY_F27, Qt::Key_F27 },
{ XKB_KEY_F28, Qt::Key_F28 },
{ XKB_KEY_F29, Qt::Key_F29 },
{ XKB_KEY_F30, Qt::Key_F30 },
{ XKB_KEY_F31, Qt::Key_F31 },
{ XKB_KEY_F32, Qt::Key_F32 },
{ XKB_KEY_F33, Qt::Key_F33 },
{ XKB_KEY_F34, Qt::Key_F34 },
{ XKB_KEY_F35, Qt::Key_F35 },
{ XKB_KEY_Super_L, Qt::Key_Super_L },
{ XKB_KEY_Super_R, Qt::Key_Super_R },
{ XKB_KEY_Menu, Qt::Key_Menu },
{ XKB_KEY_Hyper_L, Qt::Key_Hyper_L },
{ XKB_KEY_Hyper_R, Qt::Key_Hyper_R },
{ XKB_KEY_Help, Qt::Key_Help },
{ XKB_KEY_ISO_Level3_Shift, Qt::Key_AltGr },
{ XKB_KEY_Multi_key, Qt::Key_Multi_key },
{ XKB_KEY_Codeinput, Qt::Key_Codeinput },
{ XKB_KEY_SingleCandidate, Qt::Key_SingleCandidate },
{ XKB_KEY_MultipleCandidate, Qt::Key_MultipleCandidate },
{ XKB_KEY_PreviousCandidate, Qt::Key_PreviousCandidate },
{ XKB_KEY_Mode_switch, Qt::Key_Mode_switch },
{ XKB_KEY_script_switch, Qt::Key_Mode_switch },
{ XKB_KEY_Kanji, Qt::Key_Kanji },
{ XKB_KEY_Muhenkan, Qt::Key_Muhenkan },
{ XKB_KEY_Henkan, Qt::Key_Henkan },
{ XKB_KEY_Romaji, Qt::Key_Romaji },
{ XKB_KEY_Hiragana, Qt::Key_Hiragana },
{ XKB_KEY_Katakana, Qt::Key_Katakana },
{ XKB_KEY_Hiragana_Katakana, Qt::Key_Hiragana_Katakana },
{ XKB_KEY_Zenkaku, Qt::Key_Zenkaku },
{ XKB_KEY_Hankaku, Qt::Key_Hankaku },
{ XKB_KEY_Zenkaku_Hankaku, Qt::Key_Zenkaku_Hankaku },
{ XKB_KEY_Touroku, Qt::Key_Touroku },
{ XKB_KEY_Massyo, Qt::Key_Massyo },
{ XKB_KEY_Kana_Lock, Qt::Key_Kana_Lock },
{ XKB_KEY_Kana_Shift, Qt::Key_Kana_Shift },
{ XKB_KEY_Eisu_Shift, Qt::Key_Eisu_Shift },
{ XKB_KEY_Eisu_toggle, Qt::Key_Eisu_toggle },
{ XKB_KEY_Kanji_Bangou, Qt::Key_Codeinput },
{ XKB_KEY_Zen_Koho, Qt::Key_MultipleCandidate },
{ XKB_KEY_Mae_Koho, Qt::Key_PreviousCandidate },
{ XKB_KEY_Hangul, Qt::Key_Hangul },
{ XKB_KEY_Hangul_Start, Qt::Key_Hangul_Start },
{ XKB_KEY_Hangul_End, Qt::Key_Hangul_End },
{ XKB_KEY_Hangul_Hanja, Qt::Key_Hangul_Hanja },
{ XKB_KEY_Hangul_Jamo, Qt::Key_Hangul_Jamo },
{ XKB_KEY_Hangul_Romaja, Qt::Key_Hangul_Romaja },
{ XKB_KEY_Hangul_Codeinput, Qt::Key_Codeinput },
{ XKB_KEY_Hangul_Jeonja, Qt::Key_Hangul_Jeonja },
{ XKB_KEY_Hangul_Banja, Qt::Key_Hangul_Banja },
{ XKB_KEY_Hangul_PreHanja, Qt::Key_Hangul_PreHanja },
{ XKB_KEY_Hangul_PostHanja, Qt::Key_Hangul_PostHanja },
{ XKB_KEY_Hangul_SingleCandidate, Qt::Key_SingleCandidate },
{ XKB_KEY_Hangul_MultipleCandidate, Qt::Key_MultipleCandidate },
{ XKB_KEY_Hangul_PreviousCandidate, Qt::Key_PreviousCandidate },
{ XKB_KEY_Hangul_Special, Qt::Key_Hangul_Special },
{ XKB_KEY_Hangul_switch, Qt::Key_Mode_switch },
{ XKB_KEY_dead_grave, Qt::Key_Dead_Grave },
{ XKB_KEY_dead_acute, Qt::Key_Dead_Acute },
{ XKB_KEY_dead_circumflex, Qt::Key_Dead_Circumflex },
{ XKB_KEY_dead_tilde, Qt::Key_Dead_Tilde },
{ XKB_KEY_dead_macron, Qt::Key_Dead_Macron },
{ XKB_KEY_dead_breve, Qt::Key_Dead_Breve },
{ XKB_KEY_dead_abovedot, Qt::Key_Dead_Abovedot },
{ XKB_KEY_dead_diaeresis, Qt::Key_Dead_Diaeresis },
{ XKB_KEY_dead_abovering, Qt::Key_Dead_Abovering },
{ XKB_KEY_dead_doubleacute, Qt::Key_Dead_Doubleacute },
{ XKB_KEY_dead_caron, Qt::Key_Dead_Caron },
{ XKB_KEY_dead_cedilla, Qt::Key_Dead_Cedilla },
{ XKB_KEY_dead_ogonek, Qt::Key_Dead_Ogonek },
{ XKB_KEY_dead_iota, Qt::Key_Dead_Iota },
{ XKB_KEY_dead_voiced_sound, Qt::Key_Dead_Voiced_Sound },
{ XKB_KEY_dead_semivoiced_sound, Qt::Key_Dead_Semivoiced_Sound },
{ XKB_KEY_dead_belowdot, Qt::Key_Dead_Belowdot },
{ XKB_KEY_dead_hook, Qt::Key_Dead_Hook },
{ XKB_KEY_dead_horn, Qt::Key_Dead_Horn },
{ XKB_KEY_XF86Back, Qt::Key_Back },
{ XKB_KEY_XF86Forward, Qt::Key_Forward },
{ XKB_KEY_XF86Stop, Qt::Key_Stop },
{ XKB_KEY_XF86Refresh, Qt::Key_Refresh },
{ XKB_KEY_XF86AudioLowerVolume, Qt::Key_VolumeDown },
{ XKB_KEY_XF86AudioMute, Qt::Key_VolumeMute },
{ XKB_KEY_XF86AudioRaiseVolume, Qt::Key_VolumeUp },
{ XKB_KEY_XF86AudioPlay, Qt::Key_MediaPlay },
{ XKB_KEY_XF86AudioStop, Qt::Key_MediaStop },
{ XKB_KEY_XF86AudioPrev, Qt::Key_MediaPrevious },
{ XKB_KEY_XF86AudioNext, Qt::Key_MediaNext },
{ XKB_KEY_XF86AudioRecord, Qt::Key_MediaRecord },
{ XKB_KEY_XF86AudioPause, Qt::Key_MediaPause },
{ XKB_KEY_XF86HomePage, Qt::Key_HomePage },
{ XKB_KEY_XF86Favorites, Qt::Key_Favorites },
{ XKB_KEY_XF86Search, Qt::Key_Search },
{ XKB_KEY_XF86Standby, Qt::Key_Standby },
{ XKB_KEY_XF86OpenURL, Qt::Key_OpenUrl },
{ XKB_KEY_XF86Mail, Qt::Key_LaunchMail },
{ XKB_KEY_XF86AudioMedia, Qt::Key_LaunchMedia },
{ XKB_KEY_XF86MyComputer, Qt::Key_Launch0 },
{ XKB_KEY_XF86Calculator, Qt::Key_Launch1 },
{ XKB_KEY_XF86Launch0, Qt::Key_Launch2 },
{ XKB_KEY_XF86Launch1, Qt::Key_Launch3 },
{ XKB_KEY_XF86Launch2, Qt::Key_Launch4 },
{ XKB_KEY_XF86Launch3, Qt::Key_Launch5 },
{ XKB_KEY_XF86Launch4, Qt::Key_Launch6 },
{ XKB_KEY_XF86Launch5, Qt::Key_Launch7 },
{ XKB_KEY_XF86Launch6, Qt::Key_Launch8 },
{ XKB_KEY_XF86Launch7, Qt::Key_Launch9 },
{ XKB_KEY_XF86Launch8, Qt::Key_LaunchA },
{ XKB_KEY_XF86Launch9, Qt::Key_LaunchB },
{ XKB_KEY_XF86LaunchA, Qt::Key_LaunchC },
{ XKB_KEY_XF86LaunchB, Qt::Key_LaunchD },
{ XKB_KEY_XF86LaunchC, Qt::Key_LaunchE },
{ XKB_KEY_XF86LaunchD, Qt::Key_LaunchF },
{ XKB_KEY_XF86MonBrightnessUp, Qt::Key_MonBrightnessUp },
{ XKB_KEY_XF86MonBrightnessDown, Qt::Key_MonBrightnessDown },
{ XKB_KEY_XF86KbdLightOnOff, Qt::Key_KeyboardLightOnOff },
{ XKB_KEY_XF86KbdBrightnessUp, Qt::Key_KeyboardBrightnessUp },
{ XKB_KEY_XF86PowerOff, Qt::Key_PowerOff },
{ XKB_KEY_XF86WakeUp, Qt::Key_WakeUp },
{ XKB_KEY_XF86Eject, Qt::Key_Eject },
{ XKB_KEY_XF86ScreenSaver, Qt::Key_ScreenSaver },
{ XKB_KEY_XF86WWW, Qt::Key_WWW },
{ XKB_KEY_XF86Memo, Qt::Key_Memo },
{ XKB_KEY_XF86LightBulb, Qt::Key_LightBulb },
{ XKB_KEY_XF86Shop, Qt::Key_Shop },
{ XKB_KEY_XF86History, Qt::Key_History },
{ XKB_KEY_XF86AddFavorite, Qt::Key_AddFavorite },
{ XKB_KEY_XF86HotLinks, Qt::Key_HotLinks },
{ XKB_KEY_XF86BrightnessAdjust, Qt::Key_BrightnessAdjust },
{ XKB_KEY_XF86Finance, Qt::Key_Finance },
{ XKB_KEY_XF86Community, Qt::Key_Community },
{ XKB_KEY_XF86AudioRewind, Qt::Key_AudioRewind },
{ XKB_KEY_XF86BackForward, Qt::Key_BackForward },
{ XKB_KEY_XF86ApplicationLeft, Qt::Key_ApplicationLeft },
{ XKB_KEY_XF86ApplicationRight, Qt::Key_ApplicationRight },
{ XKB_KEY_XF86Book, Qt::Key_Book },
{ XKB_KEY_XF86CD, Qt::Key_CD },
{ XKB_KEY_XF86Calculater, Qt::Key_Calculator },
{ XKB_KEY_XF86ToDoList, Qt::Key_ToDoList },
{ XKB_KEY_XF86ClearGrab, Qt::Key_ClearGrab },
{ XKB_KEY_XF86Close, Qt::Key_Close },
{ XKB_KEY_XF86Copy, Qt::Key_Copy },
{ XKB_KEY_XF86Cut, Qt::Key_Cut },
{ XKB_KEY_XF86Display, Qt::Key_Display },
{ XKB_KEY_XF86DOS, Qt::Key_DOS },
{ XKB_KEY_XF86Documents, Qt::Key_Documents },
{ XKB_KEY_XF86Excel, Qt::Key_Excel },
{ XKB_KEY_XF86Explorer, Qt::Key_Explorer },
{ XKB_KEY_XF86Game, Qt::Key_Game },
{ XKB_KEY_XF86Go, Qt::Key_Go },
{ XKB_KEY_XF86iTouch, Qt::Key_iTouch },
{ XKB_KEY_XF86LogOff, Qt::Key_LogOff },
{ XKB_KEY_XF86Market, Qt::Key_Market },
{ XKB_KEY_XF86Meeting, Qt::Key_Meeting },
{ XKB_KEY_XF86MenuKB, Qt::Key_MenuKB },
{ XKB_KEY_XF86MenuPB, Qt::Key_MenuPB },
{ XKB_KEY_XF86MySites, Qt::Key_MySites },
{ XKB_KEY_XF86News, Qt::Key_News },
{ XKB_KEY_XF86OfficeHome, Qt::Key_OfficeHome },
{ XKB_KEY_XF86Option, Qt::Key_Option },
{ XKB_KEY_XF86Paste, Qt::Key_Paste },
{ XKB_KEY_XF86Phone, Qt::Key_Phone },
{ XKB_KEY_XF86Calendar, Qt::Key_Calendar },
{ XKB_KEY_XF86Reply, Qt::Key_Reply },
{ XKB_KEY_XF86Reload, Qt::Key_Reload },
{ XKB_KEY_XF86RotateWindows, Qt::Key_RotateWindows },
{ XKB_KEY_XF86RotationPB, Qt::Key_RotationPB },
{ XKB_KEY_XF86RotationKB, Qt::Key_RotationKB },
{ XKB_KEY_XF86Save, Qt::Key_Save },
{ XKB_KEY_XF86Send, Qt::Key_Send },
{ XKB_KEY_XF86Spell, Qt::Key_Spell },
{ XKB_KEY_XF86SplitScreen, Qt::Key_SplitScreen },
{ XKB_KEY_XF86Support, Qt::Key_Support },
{ XKB_KEY_XF86TaskPane, Qt::Key_TaskPane },
{ XKB_KEY_XF86Terminal, Qt::Key_Terminal },
{ XKB_KEY_XF86Tools, Qt::Key_Tools },
{ XKB_KEY_XF86Travel, Qt::Key_Travel },
{ XKB_KEY_XF86Video, Qt::Key_Video },
{ XKB_KEY_XF86Word, Qt::Key_Word },
{ XKB_KEY_XF86Xfer, Qt::Key_Xfer },
{ XKB_KEY_XF86ZoomIn, Qt::Key_ZoomIn },
{ XKB_KEY_XF86ZoomOut, Qt::Key_ZoomOut },
{ XKB_KEY_XF86Away, Qt::Key_Away },
{ XKB_KEY_XF86Messenger, Qt::Key_Messenger },
{ XKB_KEY_XF86WebCam, Qt::Key_WebCam },
{ XKB_KEY_XF86MailForward, Qt::Key_MailForward },
{ XKB_KEY_XF86Pictures, Qt::Key_Pictures },
{ XKB_KEY_XF86Music, Qt::Key_Music },
{ XKB_KEY_XF86Battery, Qt::Key_Battery },
{ XKB_KEY_XF86Bluetooth, Qt::Key_Bluetooth },
{ XKB_KEY_XF86WLAN, Qt::Key_WLAN },
{ XKB_KEY_XF86UWB, Qt::Key_UWB },
{ XKB_KEY_XF86AudioForward, Qt::Key_AudioForward },
{ XKB_KEY_XF86AudioRepeat, Qt::Key_AudioRepeat },
{ XKB_KEY_XF86AudioRandomPlay, Qt::Key_AudioRandomPlay },
{ XKB_KEY_XF86Subtitle, Qt::Key_Subtitle },
{ XKB_KEY_XF86AudioCycleTrack, Qt::Key_AudioCycleTrack },
{ XKB_KEY_XF86Time, Qt::Key_Time },
{ XKB_KEY_XF86Hibernate, Qt::Key_Hibernate },
{ XKB_KEY_XF86View, Qt::Key_View },
{ XKB_KEY_XF86TopMenu, Qt::Key_TopMenu },
{ XKB_KEY_XF86PowerDown, Qt::Key_PowerDown },
{ XKB_KEY_XF86Suspend, Qt::Key_Suspend },
{ XKB_KEY_XF86ContrastAdjust, Qt::Key_ContrastAdjust },
{ XKB_KEY_XF86LaunchE, Qt::Key_LaunchG },
{ XKB_KEY_XF86LaunchF, Qt::Key_LaunchH },
{ XKB_KEY_XF86Select, Qt::Key_Select },
{ XKB_KEY_Cancel, Qt::Key_Cancel },
{ XKB_KEY_Execute, Qt::Key_Execute },
{ XKB_KEY_XF86Sleep, Qt::Key_Sleep },
};
// Mouse.
// Taken from QXcbConnection::translateMouseButton.
static const auto XcbButtonToQt = flat_map<uint64, Qt::MouseButton>{
// { 1, Qt::LeftButton }, // Ignore the left button.
{ 2, Qt::MiddleButton },
{ 3, Qt::RightButton },
// Button values 4-7 were already handled as Wheel events.
{ 8, Qt::BackButton },
{ 9, Qt::ForwardButton },
{ 10, Qt::ExtraButton3 },
{ 11, Qt::ExtraButton4 },
{ 12, Qt::ExtraButton5 },
{ 13, Qt::ExtraButton6 },
{ 14, Qt::ExtraButton7 },
{ 15, Qt::ExtraButton8 },
{ 16, Qt::ExtraButton9 },
{ 17, Qt::ExtraButton10 },
{ 18, Qt::ExtraButton11 },
{ 19, Qt::ExtraButton12 },
{ 20, Qt::ExtraButton13 },
{ 21, Qt::ExtraButton14 },
{ 22, Qt::ExtraButton15 },
{ 23, Qt::ExtraButton16 },
{ 24, Qt::ExtraButton17 },
{ 25, Qt::ExtraButton18 },
{ 26, Qt::ExtraButton19 },
{ 27, Qt::ExtraButton20 },
{ 28, Qt::ExtraButton21 },
{ 29, Qt::ExtraButton22 },
{ 30, Qt::ExtraButton23 },
{ 31, Qt::ExtraButton24 },
};
if (descriptor > kShiftMouseButton) {
const auto button = descriptor - kShiftMouseButton;
if (XcbButtonToQt.contains(button)) {
return QString("Mouse %1").arg(button);
}
}
//
// Modifiers.
static const auto ModifierToString = flat_map<uint64, const_string>{
{ XKB_KEY_Shift_L, "Shift" },
{ XKB_KEY_Shift_R, "Right Shift" },
{ XKB_KEY_Control_L, "Ctrl" },
{ XKB_KEY_Control_R, "Right Ctrl" },
{ XKB_KEY_Meta_L, "Meta" },
{ XKB_KEY_Meta_R, "Right Meta" },
{ XKB_KEY_Alt_L, "Alt" },
{ XKB_KEY_Alt_R, "Right Alt" },
{ XKB_KEY_Super_L, "Super" },
{ XKB_KEY_Super_R, "Right Super" },
};
const auto modIt = ModifierToString.find(descriptor);
if (modIt != end(ModifierToString)) {
return modIt->second.utf16();
}
//
const auto fromSequence = [](int k) {
return QKeySequence(k).toString(QKeySequence::NativeText);
};
// The conversion is not necessary,
// if the value in the range Qt::Key_Space - Qt::Key_QuoteLeft.
if (descriptor >= Qt::Key_Space && descriptor <= Qt::Key_QuoteLeft) {
return fromSequence(descriptor);
}
const auto prefix = IsKeypad(descriptor) ? "Num " : QString();
const auto keyIt = KeyToString.find(descriptor);
return (keyIt != end(KeyToString))
? prefix + fromSequence(keyIt->second)
: QString("\\x%1").arg(descriptor, 0, 16);
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
return {};
}
bool IsToggleFullScreenKey(not_null<QKeyEvent*> e) {
return false;
}
} // namespace base::Platform::GlobalShortcuts

View File

@@ -0,0 +1,9 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/global_shortcuts.h"

View File

@@ -0,0 +1,53 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/linux/base_haptic_linux.h"
#include <QtGui/QGuiApplication>
#include <sigxcpufeedback/sigxcpufeedback.hpp>
namespace base::Platform {
namespace {
using namespace gi::repository;
namespace GObject = gi::repository::GObject;
} // namespace
void Haptic() {
SigxcpuFeedback::HapticProxy::new_for_bus(
Gio::BusType::SESSION_,
Gio::DBusProxyFlags::NONE_,
"org.sigxcpu.Feedback",
"/org/sigxcpu/Feedback",
[=](GObject::Object, Gio::AsyncResult res) {
auto interface = SigxcpuFeedback::Haptic(
SigxcpuFeedback::HapticProxy::new_for_bus_finish(
res,
nullptr));
if (!interface) {
return;
}
interface.call_vibrate(
QGuiApplication::desktopFileName().toStdString(),
GLib::Variant::new_array({
GLib::Variant::new_tuple({
GLib::Variant::new_double(0.2),
GLib::Variant::new_uint32(5),
}),
}),
nullptr);
});
}
bool IsSwipeBackEnabled() {
return true;
}
} // namespace base::Platform

View File

@@ -0,0 +1,9 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/platform/base_platform_haptic.h"

View File

@@ -0,0 +1,386 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/linux/base_info_linux.h"
#include "base/algorithm.h"
#include "base/platform/linux/base_linux_library.h"
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
#include "base/platform/linux/base_linux_xcb_utilities.h"
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
#include <QtCore/QJsonObject>
#include <QtCore/QLocale>
#include <QtCore/QVersionNumber>
#include <QtCore/QDate>
#include <QtCore/QFile>
#include <QtCore/QProcess>
#include <QtGui/QGuiApplication>
#include <sys/utsname.h>
#ifdef __GLIBC__
#include <gnu/libc-version.h>
#endif // __GLIBC__
extern "C" {
struct wl_display;
} // extern "C"
namespace Platform {
namespace {
[[nodiscard]] QStringList GetDesktopEnvironment() {
const auto list = qEnvironmentVariable("XDG_CURRENT_DESKTOP").split(':');
return list | ranges::views::transform([](const auto &item) {
return item.simplified();
}) | ranges::to<QStringList>;
}
[[nodiscard]] QString ChassisTypeToString(uint type) {
switch (type) {
case 0x3: /* Desktop */
case 0x4: /* Low Profile Desktop */
case 0x6: /* Mini Tower */
case 0x7: /* Tower */
case 0xD: /* All in one (i.e. PC built into monitor) */
return "Desktop";
case 0x8: /* Portable */
case 0x9: /* Laptop */
case 0xA: /* Notebook */
case 0xE: /* Sub Notebook */
return "Laptop";
case 0xB: /* Hand Held */
return "Handset";
case 0x11: /* Main Server Chassis */
case 0x1C: /* Blade */
case 0x1D: /* Blade Enclosure */
return "Server";
case 0x1E: /* Tablet */
return "Tablet";
case 0x1F: /* Convertible */
case 0x20: /* Detachable */
return "Convertible";
default:
return "";
}
}
[[nodiscard]] bool IsGlibcLess228() {
static const auto result = [] {
const auto libcName = GetLibcName();
const auto libcVersion = GetLibcVersion();
return (libcName == qstr("glibc"))
&& !libcVersion.isEmpty()
&& (QVersionNumber::fromString(libcVersion)
< QVersionNumber(2, 28));
}();
return result;
}
} // namespace
QString DeviceModelPretty() {
using namespace base::Platform;
static const auto result = FinalizeDeviceModel([&] {
const auto value = [](const char *key) {
auto file = QFile(u"/sys/class/dmi/id/"_q + key);
return (file.open(QIODevice::ReadOnly | QIODevice::Text))
? SimplifyDeviceModel(QString(file.readAll()))
: QString();
};
const auto productName = value("product_name");
if (const auto model = ProductNameToDeviceModel(productName)
; !model.isEmpty()) {
return model;
}
const auto productFamily = value("product_family");
const auto boardName = value("board_name");
const auto familyName = SimplifyDeviceModel(
productFamily + ' ' + boardName);
if (IsDeviceModelOk(familyName)) {
return familyName;
} else if (IsDeviceModelOk(boardName)) {
return boardName;
} else if (IsDeviceModelOk(productFamily)) {
return productFamily;
}
const auto virtualization = []() -> QString {
QProcess process;
process.start("systemd-detect-virt", QStringList());
process.waitForFinished();
return process.readAll().simplified().toUpper();
}();
if (!virtualization.isEmpty() && virtualization != qstr("NONE")) {
return virtualization;
}
const auto chassisType = ChassisTypeToString(
value("chassis_type").toUInt());
if (!chassisType.isEmpty()) {
return chassisType;
}
return QString();
}());
return result;
}
QString SystemVersionPretty() {
static const auto result = [&] {
QStringList resultList{};
struct utsname u;
if (uname(&u) == 0) {
resultList << u.sysname;
#ifndef Q_OS_LINUX
resultList << u.release;
#endif // !Q_OS_LINUX
} else {
resultList << "Unknown";
}
if (const auto desktopEnvironment = GetDesktopEnvironment();
!desktopEnvironment.isEmpty()) {
resultList << desktopEnvironment;
} else if (const auto windowManager = GetWindowManager();
!windowManager.isEmpty()) {
resultList << windowManager;
}
if (IsWayland()) {
resultList << "Wayland";
} else if (IsXwayland()) {
resultList << "Xwayland";
} else if (IsX11()) {
resultList << "X11";
}
const auto libcName = GetLibcName();
const auto libcVersion = GetLibcVersion();
if (!libcVersion.isEmpty()) {
if (!libcName.isEmpty()) {
resultList << libcName;
} else {
resultList << "libc";
}
resultList << libcVersion;
}
return resultList.join(' ');
}();
return result;
}
QString SystemCountry() {
return QLocale::system().name().split('_').last();
}
QString SystemLanguage() {
const auto system = QLocale::system();
const auto languages = system.uiLanguages();
return languages.isEmpty()
? system.name().split('_').first()
: languages.front();
}
QDate WhenSystemBecomesOutdated() {
if (IsGlibcLess228()) {
return QDate(2023, 7, 1); // Older than CentOS 8.
}
return QDate();
}
int AutoUpdateVersion() {
if (IsGlibcLess228()) {
return 2;
}
return 4;
}
QString AutoUpdateKey() {
return "linux";
}
QString GetLibcName() {
#ifdef __GLIBC__
return "glibc";
#endif // __GLIBC__
return QString();
}
QString GetLibcVersion() {
#ifdef __GLIBC__
static const auto result = [&] {
const auto version = QString::fromLatin1(gnu_get_libc_version());
return QVersionNumber::fromString(version).isNull() ? QString() : version;
}();
return result;
#endif // __GLIBC__
return QString();
}
QString GetWindowManager() {
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
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 nameAtom = base::Platform::XCB::GetAtom(
connection,
"_NET_WM_NAME");
const auto utf8Atom = base::Platform::XCB::GetAtom(
connection,
"UTF8_STRING");
const auto supportingWindow = base::Platform::XCB::GetSupportingWMCheck(
connection,
root);
if (!nameAtom || !utf8Atom || !supportingWindow) {
return {};
}
const auto cookie = xcb_get_property(
connection,
false,
supportingWindow,
nameAtom,
utf8Atom,
0,
1024);
const auto reply = base::Platform::XCB::MakeReplyPointer(
xcb_get_property_reply(
connection,
cookie,
nullptr));
if (!reply) {
return {};
}
return (reply->format == 8 && reply->type == utf8Atom)
? QString::fromUtf8(
reinterpret_cast<const char*>(
xcb_get_property_value(reply.get())),
xcb_get_property_value_length(reply.get())).simplified()
: QString();
#else // !DESKTOP_APP_DISABLE_X11_INTEGRATION
return QString();
#endif // DESKTOP_APP_DISABLE_X11_INTEGRATION
}
bool IsX11() {
if (!qApp) {
static const auto result = []() -> bool {
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
const base::Platform::XCB::Connection connection;
return connection && !xcb_connection_has_error(connection);
#else // !DESKTOP_APP_DISABLE_X11_INTEGRATION
return qEnvironmentVariableIsSet("DISPLAY");
#endif // DESKTOP_APP_DISABLE_X11_INTEGRATION
}();
return result;
}
static const bool result =
#if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0)
#if defined QT_FEATURE_xcb && QT_CONFIG(xcb)
qApp->nativeInterface<QNativeInterface::QX11Application>()
#else // xcb
false
#endif // !xcb
#else // Qt >= 6.2.0
QGuiApplication::platformName() == "xcb"
#endif // Qt < 6.2.0
;
return result;
}
bool IsWayland() {
if (!qApp) {
static const auto result = []() -> bool {
struct wl_display *(*wl_display_connect)(const char *name);
void (*wl_display_disconnect)(struct wl_display *display);
if (const auto lib = base::Platform::LoadLibrary(
"libwayland-client.so.0",
RTLD_NODELETE); lib
&& LOAD_LIBRARY_SYMBOL(lib, wl_display_connect)
&& LOAD_LIBRARY_SYMBOL(lib, wl_display_disconnect)) {
const auto display = wl_display_connect(nullptr);
if (display) {
wl_display_disconnect(display);
}
return display;
}
return qEnvironmentVariableIsSet("WAYLAND_DISPLAY");
}();
return result;
}
static const bool result =
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
#if defined QT_FEATURE_wayland && QT_CONFIG(wayland)
qApp->nativeInterface<QNativeInterface::QWaylandApplication>()
#else // wayland
false
#endif // !wayland
#else // Qt >= 6.7.0
QGuiApplication::platformName().startsWith("wayland")
#endif // Qt < 6.7.0
;
return result;
}
bool IsXwayland() {
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
static const auto result = []() -> bool {
const base::Platform::XCB::Connection connection;
if (!connection || xcb_connection_has_error(connection)) {
return false;
}
constexpr auto kXWAYLAND = "XWAYLAND";
const auto reply = base::Platform::XCB::MakeReplyPointer(
xcb_query_extension_reply(
connection,
xcb_query_extension(
connection,
strlen(kXWAYLAND),
kXWAYLAND),
nullptr));
if (!reply) {
return false;
}
return reply->present;
}();
return result;
#else // !DESKTOP_APP_DISABLE_X11_INTEGRATION
return IsX11() && qEnvironmentVariableIsSet("WAYLAND_DISPLAY");
#endif // DESKTOP_APP_DISABLE_X11_INTEGRATION
}
void Start(QJsonObject options) {
}
void Finish() {
}
} // namespace Platform

View File

@@ -0,0 +1,40 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/platform/base_platform_info.h"
namespace Platform {
inline OutdateReason WhySystemBecomesOutdated() {
return OutdateReason::IsOld;
}
inline constexpr bool IsLinux() {
return true;
}
inline constexpr bool IsWindows() { return false; }
inline constexpr bool IsWindows32Bit() { return false; }
inline constexpr bool IsWindows64Bit() { return false; }
inline constexpr bool IsWindowsARM64() { return false; }
inline constexpr bool IsWindowsStoreBuild() { return false; }
inline bool IsWindows7OrGreater() { return false; }
inline bool IsWindows8OrGreater() { return false; }
inline bool IsWindows8Point1OrGreater() { return false; }
inline bool IsWindows10OrGreater() { return false; }
inline bool IsWindows11OrGreater() { return false; }
inline constexpr bool IsMac() { return false; }
inline constexpr bool IsMacStoreBuild() { return false; }
inline bool IsMac10_12OrGreater() { return false; }
inline bool IsMac10_13OrGreater() { return false; }
inline bool IsMac10_14OrGreater() { return false; }
inline bool IsMac10_15OrGreater() { return false; }
inline bool IsMac11_0OrGreater() { return false; }
inline bool IsMac26_0OrGreater() { return false; }
} // namespace Platform

View File

@@ -0,0 +1,104 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/linux/base_last_input_linux.h"
#include "base/debug_log.h"
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
#include "base/platform/linux/base_linux_xcb_utilities.h"
#include <xcb/screensaver.h>
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
#include <mutteridlemonitor/mutteridlemonitor.hpp>
namespace base::Platform {
namespace {
using namespace gi::repository;
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
std::optional<crl::time> XCBLastUserInputTime() {
const XCB::Connection connection;
if (!connection || xcb_connection_has_error(connection)) {
return std::nullopt;
}
if (!XCB::IsExtensionPresent(connection, &xcb_screensaver_id)) {
return std::nullopt;
}
const auto root = XCB::GetRootWindow(connection);
if (!root) {
return std::nullopt;
}
const auto cookie = xcb_screensaver_query_info(
connection,
root);
const auto reply = XCB::MakeReplyPointer(
xcb_screensaver_query_info_reply(
connection,
cookie,
nullptr));
if (!reply) {
return std::nullopt;
}
return (crl::now() - static_cast<crl::time>(reply->ms_since_user_input));
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
std::optional<crl::time> MutterDBusLastUserInputTime() {
static auto NotSupported = false;
if (NotSupported) {
return std::nullopt;
}
auto interface = MutterIdleMonitor::IdleMonitor(
MutterIdleMonitor::IdleMonitorProxy::new_for_bus_sync(
Gio::BusType::SESSION_,
Gio::DBusProxyFlags::DO_NOT_AUTO_START_AT_CONSTRUCTION_,
"org.gnome.Mutter.IdleMonitor",
"/org/gnome/Mutter/IdleMonitor/Core",
nullptr));
if (!interface) {
NotSupported = true;
return std::nullopt;
}
const auto result = interface.call_get_idletime_sync();
if (!result) {
NotSupported = true;
return std::nullopt;
}
return (crl::now() - static_cast<crl::time>(std::get<1>(*result)));
}
} // namespace
std::optional<crl::time> LastUserInputTime() {
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
const auto xcbResult = XCBLastUserInputTime();
if (xcbResult.has_value()) {
return xcbResult;
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
const auto mutterResult = MutterDBusLastUserInputTime();
if (mutterResult.has_value()) {
return mutterResult;
}
return std::nullopt;
}
} // namespace base::Platform

View File

@@ -0,0 +1,8 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once

View File

@@ -0,0 +1,15 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/linux/base_layout_switch_linux.h"
namespace base::Platform {
bool SwitchKeyboardLayoutToEnglish() {
return false;
}
} // namespace base::Platform

View File

@@ -0,0 +1,8 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once

View File

@@ -0,0 +1,175 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/linux/base_linux_allocation_tracer.h"
#include "base/debug_log.h"
#include <mutex>
#include <stdio.h>
#include <unistd.h>
void SetMallocLogger(void (*logger)(size_t, void *));
void SetVallocLogger(void (*logger)(size_t, void *));
void SetPVallocLogger(void (*logger)(size_t, void *));
void SetReallocLogger(void (*logger)(void *, size_t, void *));
void SetFreeLogger(void (*logger)(void *));
void SetMemAlignLogger(void (*logger)(size_t, size_t, void *));
void SetAlignedAllocLogger(void (*logger)(size_t, size_t, void *));
void SetPosixMemAlignLogger(void (*logger)(size_t, size_t, void *));
void SetCallocLogger(void (*logger)(size_t, size_t, void *));
namespace base::Platform {
namespace {
#ifdef DESKTOP_APP_USE_ALLOCATION_TRACER
constexpr auto kBufferSize = 1024 * 1024;
char *Buffer/* = nullptr*/;
FILE *File/* = 0*/;
char *Data/* = nullptr*/;
std::mutex Mutex;
void WriteBlock() {
if (Data > Buffer) {
fwrite(Buffer, Data - Buffer, 1, File);
fflush(File);
fdatasync(fileno(File));
Data = Buffer;
}
}
template <size_t Size>
void AppendEntry(char (&bytes)[Size]) {
std::unique_lock<std::mutex> lock(Mutex);
if (!File) {
return;
} else if (Data + Size > Buffer + kBufferSize) {
WriteBlock();
}
*reinterpret_cast<std::uint32_t*>(bytes + 1) = uint32_t(time(nullptr));
memcpy(Data, bytes, Size);
Data += Size;
}
void MallocLogger(size_t size, void *result) {
char entry[5 + sizeof(std::uint64_t) * 2];
entry[0] = 1;
*reinterpret_cast<std::uint64_t*>(entry + 5)
= static_cast<std::uint64_t>(size);
*reinterpret_cast<std::uint64_t*>(entry + 5 + sizeof(std::uint64_t))
= reinterpret_cast<std::uint64_t>(result);
AppendEntry(entry);
}
void VallocLogger(size_t size, void *result) {
MallocLogger(size, result);
}
void PVallocLogger(size_t size, void *result) {
MallocLogger(size, result);
}
void CallocLogger(size_t num, size_t size, void *result) {
MallocLogger(num * size, result);
}
void ReallocLogger(void *ptr, size_t size, void *result) {
if (!ptr) {
return MallocLogger(size, result);
}
char entry[5 + sizeof(std::uint64_t) * 3];
entry[0] = 2;
*reinterpret_cast<std::uint64_t*>(entry + 5)
= reinterpret_cast<std::uint64_t>(ptr);
*reinterpret_cast<std::uint64_t*>(entry + 5 + sizeof(std::uint64_t))
= static_cast<std::uint64_t>(size);
*reinterpret_cast<std::uint64_t*>(entry + 5 + sizeof(std::uint64_t) * 2)
= reinterpret_cast<std::uint64_t>(result);
AppendEntry(entry);
}
void MemAlignLogger(size_t alignment, size_t size, void *result) {
MallocLogger(size, result);
}
void AlignedAllocLogger(size_t alignment, size_t size, void *result) {
MallocLogger(size, result);
}
void PosixMemAlignLogger(size_t alignment, size_t size, void *result) {
MallocLogger(size, result);
}
void FreeLogger(void *ptr) {
if (ptr) {
char entry[5 + sizeof(std::uint64_t)];
entry[0] = 3;
*reinterpret_cast<std::uint64_t*>(entry + 5)
= reinterpret_cast<std::uint64_t>(ptr);
AppendEntry(entry);
}
}
void InstallLoggers() {
SetMallocLogger(MallocLogger);
SetVallocLogger(VallocLogger);
SetPVallocLogger(PVallocLogger);
SetCallocLogger(CallocLogger);
SetReallocLogger(ReallocLogger);
SetMemAlignLogger(MemAlignLogger);
SetAlignedAllocLogger(AlignedAllocLogger);
SetPosixMemAlignLogger(PosixMemAlignLogger);
SetFreeLogger(FreeLogger);
}
void RemoveLoggers() {
SetMallocLogger(nullptr);
SetVallocLogger(nullptr);
SetPVallocLogger(nullptr);
SetCallocLogger(nullptr);
SetReallocLogger(nullptr);
SetMemAlignLogger(nullptr);
SetAlignedAllocLogger(nullptr);
SetPosixMemAlignLogger(nullptr);
SetFreeLogger(nullptr);
}
#endif // DESKTOP_APP_USE_ALLOCATION_TRACER
} // namespace
void SetAllocationTracerPath(const QString &path) {
#ifdef DESKTOP_APP_USE_ALLOCATION_TRACER
Expects(!Buffer);
Data = Buffer = new char[kBufferSize];
if (!Buffer) {
return;
}
File = fopen(path.toStdString().c_str(), "wb");
if (!File) {
return;
}
InstallLoggers();
#endif // DESKTOP_APP_USE_ALLOCATION_TRACER
}
void FinishAllocationTracer() {
#ifdef DESKTOP_APP_USE_ALLOCATION_TRACER
if (File) {
RemoveLoggers();
std::unique_lock<std::mutex> lock(Mutex);
WriteBlock();
fclose(File);
File = nullptr;
}
#endif // DESKTOP_APP_USE_ALLOCATION_TRACER
}
} // namespace base::Platform

View File

@@ -0,0 +1,14 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
namespace base::Platform {
void SetAllocationTracerPath(const QString &path);
void FinishAllocationTracer();
} // namespace base::Platform

View File

@@ -0,0 +1,48 @@
/*
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 "base/platform/linux/base_linux_app_launch_context.h"
#include "base/platform/linux/base_linux_xdp_utilities.h"
#include "base/platform/linux/base_linux_xdg_activation_token.h"
#include <gio/gio.hpp>
namespace base::Platform {
namespace internal {
namespace {
using namespace gi::repository;
class AppLaunchContext : public Gio::impl::AppLaunchContextImpl {
public:
AppLaunchContext() : Gio::impl::AppLaunchContextImpl(this) {
if (const auto parentWindowId = XDP::ParentWindowID()
; !parentWindowId.empty()) {
setenv("PARENT_WINDOW_ID", parentWindowId);
}
}
gi::cstring get_startup_notify_id_(
Gio::AppInfo,
gi::Collection<GList, ::GFile*, gi::transfer_none_t>
) noexcept override {
if (const auto token = XdgActivationToken(); !token.isNull()) {
return token.toStdString();
}
return {};
}
};
} // namespace
} // namespace internal
gi::repository::Gio::AppLaunchContext AppLaunchContext() {
return *gi::make_ref<internal::AppLaunchContext>();
}
} // namespace base::Platform

View File

@@ -0,0 +1,18 @@
/*
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 gi::repository::Gio {
class AppLaunchContext;
} // namespace gi::repository::Gio
namespace base::Platform {
gi::repository::Gio::AppLaunchContext AppLaunchContext();
} // namespace base::Platform

View File

@@ -0,0 +1,152 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/linux/base_linux_dbus_utilities.h"
#include <xdgdbus/xdgdbus.hpp>
namespace base::Platform::DBus {
namespace {
using namespace gi::repository;
namespace GObject = gi::repository::GObject;
gi::result<XdgDBus::DBus> MakeInterface(const GDBusConnection *connection) {
return XdgDBus::DBusProxy::new_sync(
gi::wrap(connection, gi::transfer_none),
Gio::DBusProxyFlags::NONE_,
kService,
kObjectPath,
nullptr);
}
auto MakeUnexpected(GLib::Error &&error) {
return make_unexpected(std::make_unique<GLib::Error>(std::move(error)));
}
} // namespace
Result<bool> NameHasOwner(
const GDBusConnection *connection,
const std::string &name) {
auto interface = MakeInterface(connection);
if (!interface) {
return MakeUnexpected(std::move(interface.error()));
}
auto result = interface->call_name_has_owner_sync(name);
if (!result) {
return MakeUnexpected(std::move(result.error()));
}
return std::get<1>(*result);
}
Result<std::vector<std::string>> ListActivatableNames(
const GDBusConnection *connection) {
auto interface = MakeInterface(connection);
if (!interface) {
return MakeUnexpected(std::move(interface.error()));
}
auto result = interface->call_list_activatable_names_sync();
if (!result) {
return MakeUnexpected(std::move(result.error()));
}
return std::get<1>(*result) | ranges::to<std::vector<std::string>>;
}
Result<StartReply> StartServiceByName(
const GDBusConnection *connection,
const std::string &name) {
auto interface = MakeInterface(connection);
if (!interface) {
return MakeUnexpected(std::move(interface.error()));
}
auto result = interface->call_start_service_by_name_sync(name, 0);
if (!result) {
return MakeUnexpected(std::move(result.error()));
}
return StartReply(std::get<1>(*result));
}
void StartServiceByNameAsync(
const GDBusConnection *connection,
const std::string &name,
Fn<void(Fn<Result<StartReply>()>)> callback) {
auto interface = MakeInterface(connection);
if (!interface) {
const auto error = std::make_shared<GLib::Error>(
std::move(interface.error()));
callback([=]() -> Result<StartReply> {
return MakeUnexpected(std::move(*error));
});
return;
}
interface->call_start_service_by_name(name, 0, [
=,
interface = *interface
](GObject::Object source_object, Gio::AsyncResult res) mutable {
callback([=]() mutable -> Result<StartReply> {
auto result = interface.call_start_service_by_name_finish(res);
if (!result) {
return MakeUnexpected(std::move(result.error()));
}
return StartReply(std::get<1>(*result));
});
});
}
struct ServiceWatcher::Private {
XdgDBus::DBus interface;
};
ServiceWatcher::ServiceWatcher(
const GDBusConnection *connection,
const std::string &service,
Fn<void(
const std::string &,
const std::string &,
const std::string &)> callback)
: _private(std::make_unique<Private>()) {
XdgDBus::DBusProxy::new_(
gi::wrap(connection, gi::transfer_none),
Gio::DBusProxyFlags::NONE_,
kService,
kObjectPath,
nullptr,
crl::guard(this, [=](GObject::Object, Gio::AsyncResult res) {
_private->interface = XdgDBus::DBus(
XdgDBus::DBusProxy::new_finish(res, nullptr));
if (!_private->interface) {
return;
}
_private->interface.signal_name_owner_changed().connect([=](
XdgDBus::DBus,
std::string name,
std::string oldOwner,
std::string newOwner) {
if (name != service) {
return;
}
callback(name, oldOwner, newOwner);
});
}));
}
ServiceWatcher::~ServiceWatcher() = default;
} // namespace base::Platform::DBus

View File

@@ -0,0 +1,61 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/expected.h"
#include "base/weak_ptr.h"
typedef struct _GDBusConnection GDBusConnection;
namespace base::Platform::DBus {
inline constexpr auto kService = "org.freedesktop.DBus";
inline constexpr auto kObjectPath = "/org/freedesktop/DBus";
inline constexpr auto kInterface = kService;
template <typename T>
using Result = expected<T, std::unique_ptr<std::exception>>;
enum class StartReply {
Success,
AlreadyRunning,
};
Result<bool> NameHasOwner(
const GDBusConnection *connection,
const std::string &name);
Result<std::vector<std::string>> ListActivatableNames(
const GDBusConnection *connection);
Result<StartReply> StartServiceByName(
const GDBusConnection *connection,
const std::string &name);
void StartServiceByNameAsync(
const GDBusConnection *connection,
const std::string &name,
Fn<void(Fn<Result<StartReply>()>)> callback);
class ServiceWatcher : public has_weak_ptr {
public:
ServiceWatcher(
const GDBusConnection *connection,
const std::string &service,
Fn<void(
const std::string &,
const std::string &,
const std::string &)> callback);
~ServiceWatcher();
private:
struct Private;
std::unique_ptr<Private> _private;
};
} // namespace base::Platform::DBus

View File

@@ -0,0 +1,35 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/linux/base_linux_library.h"
#include "base/debug_log.h"
namespace base {
namespace Platform {
LibraryHandle LoadLibrary(const char *name, int flags) {
DEBUG_LOG(("Loading '%1'...").arg(name));
if (auto lib = LibraryHandle(dlopen(name, RTLD_LAZY | flags))) {
DEBUG_LOG(("Loaded '%1'!").arg(name));
return lib;
}
LOG(("Could not load '%1'! Error: %2").arg(name, dlerror()));
return nullptr;
}
void *LoadSymbolGeneric(const LibraryHandle &lib, const char *name) {
if (!lib) {
return nullptr;
} else if (const auto result = dlsym(lib.get(), name)) {
return result;
}
LOG(("Error: failed to load '%1' function: %2").arg(name, dlerror()));
return nullptr;
}
} // namespace Platform
} // namespace base

View File

@@ -0,0 +1,38 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/custom_delete.h"
#include <dlfcn.h>
#include <memory>
#define LOAD_LIBRARY_SYMBOL(lib, func) \
::base::Platform::LoadSymbol(lib, #func, func)
namespace base {
namespace Platform {
using LibraryHandle = std::unique_ptr<void, custom_delete<dlclose>>;
LibraryHandle LoadLibrary(const char *name, int flags = 0);
[[nodiscard]] void *LoadSymbolGeneric(
const LibraryHandle &lib,
const char *name);
template <typename Function>
inline bool LoadSymbol(
const LibraryHandle &lib,
const char *name,
Function &func) {
func = reinterpret_cast<Function>(LoadSymbolGeneric(lib, name));
return (func != nullptr);
}
} // namespace Platform
} // namespace base

View File

@@ -0,0 +1,485 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/linux/base_linux_xcb_utilities.h"
#include "base/qt/qt_common_adapters.h"
#include <QtCore/QAbstractEventDispatcher>
#include <QtCore/QAbstractNativeEventFilter>
#include <QtCore/QSocketNotifier>
#include <QtGui/QGuiApplication>
#if QT_VERSION < QT_VERSION_CHECK(6, 2, 0)
#include <qpa/qplatformnativeinterface.h>
#endif // Qt < 6.2.0
namespace base::Platform::XCB {
namespace {
class QtEventFilter : public QAbstractNativeEventFilter {
public:
QtEventFilter(Fn<void(xcb_generic_event_t*)> handler)
: _handler(handler) {
QCoreApplication::instance()->installNativeEventFilter(this);
}
private:
bool nativeEventFilter(
const QByteArray &eventType,
void *message,
native_event_filter_result *result) override {
_handler(reinterpret_cast<xcb_generic_event_t*>(message));
return false;
}
Fn<void(xcb_generic_event_t*)> _handler;
};
} // namespace
SharedConnection::SharedConnection()
: std::shared_ptr<CustomConnection>([] {
static std::weak_ptr<CustomConnection> Weak;
auto result = Weak.lock();
if (!result) {
Weak = result = std::make_shared<CustomConnection>();
}
return result;
}()) {}
xcb_connection_t *GetConnectionFromQt() {
#if defined QT_FEATURE_xcb && QT_CONFIG(xcb)
#if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0)
using namespace QNativeInterface;
const auto native = qApp->nativeInterface<QX11Application>();
#else // Qt >= 6.2.0
const auto native = QGuiApplication::platformNativeInterface();
#endif // Qt < 6.2.0
if (!native) {
return nullptr;
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0)
return native->connection();
#else // Qt >= 6.2.0
return reinterpret_cast<xcb_connection_t*>(
native->nativeResourceForIntegration(QByteArray("connection")));
#endif // Qt < 6.2.0
#else // xcb
return nullptr;
#endif // !xcb
}
rpl::lifetime InstallEventHandler(
xcb_connection_t *connection,
Fn<void(xcb_generic_event_t*)> handler) {
if (!connection || xcb_connection_has_error(connection)) {
return rpl::lifetime();
}
static base::flat_map<
xcb_connection_t*,
std::pair<
std::variant<
v::null_t,
std::unique_ptr<QSocketNotifier>,
std::unique_ptr<QtEventFilter>
>,
std::vector<std::unique_ptr<Fn<void(xcb_generic_event_t*)>>>
>
> EventHandlers;
auto it = EventHandlers.find(connection);
if (it == EventHandlers.cend()) {
it = EventHandlers.emplace(connection).first;
if (connection == GetConnectionFromQt()) {
it->second.first = std::make_unique<QtEventFilter>([=](
xcb_generic_event_t *event) {
const auto it = EventHandlers.find(connection);
for (const auto &handler : it->second.second) {
(*handler)(event);
}
});
} else {
it->second.first = std::make_unique<QSocketNotifier>(
xcb_get_file_descriptor(connection),
QSocketNotifier::Read);
auto &notifier = *v::get<std::unique_ptr<QSocketNotifier>>(
it->second.first);
QObject::connect(
QCoreApplication::eventDispatcher(),
&QAbstractEventDispatcher::aboutToBlock,
&notifier,
[=] {
const auto it = EventHandlers.find(connection);
EventPointer<xcb_generic_event_t> event;
while (!xcb_connection_has_error(connection)
&& (event = MakeEventPointer(
xcb_poll_for_event(connection)))) {
for (const auto &handler : it->second.second) {
(*handler)(event.get());
}
}
// Let handlers handle the error
if (xcb_connection_has_error(connection)) {
for (const auto &handler : it->second.second) {
(*handler)(nullptr);
}
it->second.first = v::null;
}
});
notifier.setEnabled(true);
}
}
const auto ptr = it->second.second.emplace_back(new Fn(handler)).get();
return rpl::lifetime([=] {
const auto it = EventHandlers.find(connection);
it->second.second.erase(
ranges::remove(
it->second.second,
ptr,
&decltype(it->second.second)::value_type::get),
it->second.second.end());
if (it->second.second.empty()) {
EventHandlers.remove(connection);
}
});
}
xcb_timestamp_t GetTimestamp(xcb_connection_t *connection) {
if (!connection || xcb_connection_has_error(connection)) {
return XCB_CURRENT_TIME;
}
const auto window = GetRootWindow(connection);
if (!window) {
return XCB_CURRENT_TIME;
}
const auto atom = GetAtom(connection, "_DESKTOP_APP_GET_TIMESTAMP");
if (!atom) {
return XCB_CURRENT_TIME;
}
const auto eventMask = ChangeWindowEventMask(
connection,
window,
XCB_EVENT_MASK_PROPERTY_CHANGE);
if (!eventMask) {
return XCB_CURRENT_TIME;
}
QEventLoop loop;
xcb_timestamp_t timestamp = XCB_CURRENT_TIME;
const auto lifetime = InstallEventHandler(
connection,
[&](xcb_generic_event_t *event) {
if (!event) {
loop.quit();
return;
}
const auto guard = gsl::finally([&] {
free(
xcb_get_input_focus_reply(
connection,
xcb_get_input_focus(connection),
nullptr));
});
if ((event->response_type & ~0x80) != XCB_PROPERTY_NOTIFY) {
return;
}
const auto pn = reinterpret_cast<xcb_property_notify_event_t*>(
event);
if (pn->window != window || pn->atom != atom) {
return;
}
timestamp = pn->time;
loop.quit();
});
if (!lifetime) {
return XCB_CURRENT_TIME;
}
const auto error = MakeErrorPointer(
xcb_request_check(
connection,
xcb_change_property_checked(
connection,
XCB_PROP_MODE_REPLACE,
window,
atom,
XCB_ATOM_INTEGER,
32,
0,
nullptr)));
if (error) {
return XCB_CURRENT_TIME;
}
loop.exec();
return timestamp;
}
xcb_window_t GetRootWindow(xcb_connection_t *connection) {
if (!connection || xcb_connection_has_error(connection)) {
return XCB_NONE;
}
const auto screen = xcb_setup_roots_iterator(
xcb_get_setup(connection)).data;
if (!screen) {
return XCB_NONE;
}
return screen->root;
}
xcb_atom_t GetAtom(xcb_connection_t *connection, const QString &name) {
if (!connection || xcb_connection_has_error(connection)) {
return XCB_NONE;
}
const auto cookie = xcb_intern_atom(
connection,
0,
name.size(),
name.toUtf8().constData());
const auto reply = MakeReplyPointer(xcb_intern_atom_reply(
connection,
cookie,
nullptr));
if (!reply) {
return XCB_NONE;
}
return reply->atom;
}
bool IsExtensionPresent(
xcb_connection_t *connection,
xcb_extension_t *ext) {
if (!connection || xcb_connection_has_error(connection)) {
return false;
}
const auto reply = xcb_get_extension_data(
connection,
ext);
if (!reply) {
return false;
}
return reply->present;
}
std::vector<xcb_atom_t> GetWMSupported(
xcb_connection_t *connection,
xcb_window_t root) {
auto netWmAtoms = std::vector<xcb_atom_t>{};
if (!connection || xcb_connection_has_error(connection)) {
return netWmAtoms;
}
const auto supportedAtom = GetAtom(connection, "_NET_SUPPORTED");
if (!supportedAtom) {
return netWmAtoms;
}
auto offset = 0;
auto remaining = 0;
do {
const auto cookie = xcb_get_property(
connection,
false,
root,
supportedAtom,
XCB_ATOM_ATOM,
offset,
1024);
const auto reply = MakeReplyPointer(xcb_get_property_reply(
connection,
cookie,
nullptr));
if (!reply) {
break;
}
remaining = 0;
if (reply->type == XCB_ATOM_ATOM && reply->format == 32) {
const auto len = xcb_get_property_value_length(reply.get())
/ sizeof(xcb_atom_t);
const auto atoms = reinterpret_cast<xcb_atom_t*>(
xcb_get_property_value(reply.get()));
const auto s = netWmAtoms.size();
netWmAtoms.resize(s + len);
memcpy(netWmAtoms.data() + s, atoms, len * sizeof(xcb_atom_t));
remaining = reply->bytes_after;
offset += len;
}
} while (remaining > 0);
return netWmAtoms;
}
xcb_window_t GetSupportingWMCheck(
xcb_connection_t *connection,
xcb_window_t root) {
if (!connection || xcb_connection_has_error(connection)) {
return XCB_NONE;
}
const auto supportingAtom = base::Platform::XCB::GetAtom(
connection,
"_NET_SUPPORTING_WM_CHECK");
if (!supportingAtom) {
return XCB_NONE;
}
const auto cookie = xcb_get_property(
connection,
false,
root,
supportingAtom,
XCB_ATOM_WINDOW,
0,
1024);
const auto reply = MakeReplyPointer(xcb_get_property_reply(
connection,
cookie,
nullptr));
if (!reply) {
return XCB_NONE;
}
return (reply->format == 32 && reply->type == XCB_ATOM_WINDOW)
? *reinterpret_cast<xcb_window_t*>(
xcb_get_property_value(reply.get()))
: XCB_NONE;
}
bool IsSupportedByWM(xcb_connection_t *connection, const QString &atomName) {
if (!connection || xcb_connection_has_error(connection)) {
return false;
}
const auto root = GetRootWindow(connection);
if (!root) {
return false;
}
const auto atom = GetAtom(connection, atomName);
if (!atom) {
return false;
}
return ranges::contains(GetWMSupported(connection, root), atom);
}
rpl::lifetime ChangeWindowEventMask(
xcb_connection_t *connection,
xcb_window_t window,
uint mask,
ChangeWindowEventMaskMode mode,
bool revert) {
using Mode = ChangeWindowEventMaskMode;
if (!connection || xcb_connection_has_error(connection)) {
return rpl::lifetime();
}
const auto windowAttribsCookie = xcb_get_window_attributes(
connection,
window);
const auto windowAttribs = MakeReplyPointer(
xcb_get_window_attributes_reply(
connection,
windowAttribsCookie,
nullptr));
const uint oldMask = windowAttribs ? windowAttribs->your_event_mask : 0;
if ((mode == Mode::Add) && (oldMask & mask)) {
return rpl::lifetime([] {});
} else if ((mode == Mode::Remove) && !(oldMask & mask)) {
return rpl::lifetime([] {});
} else if (oldMask == mask) {
return rpl::lifetime([] {});
}
const uint value[] = {
mode == Mode::Add
? oldMask | mask
: mode == Mode::Remove
? oldMask & ~mask
: mask
};
const auto error = MakeErrorPointer(
xcb_request_check(
connection,
xcb_change_window_attributes_checked(
connection,
window,
XCB_CW_EVENT_MASK,
value)));
if (error) {
return rpl::lifetime();
}
if (!revert) {
return rpl::lifetime([] {});
}
return rpl::lifetime([=] {
if (xcb_connection_has_error(connection)) {
return;
}
const uint value[] = {
oldMask
};
free(
xcb_request_check(
connection,
xcb_change_window_attributes_checked(
connection,
window,
XCB_CW_EVENT_MASK,
value)));
});
}
} // namespace base::Platform::XCB

View File

@@ -0,0 +1,145 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/custom_delete.h"
#include <xcb/xcb.h>
namespace base::Platform::XCB {
using ConnectionPointer = std::unique_ptr<
xcb_connection_t,
custom_delete<xcb_disconnect>
>;
class CustomConnection : public ConnectionPointer {
public:
CustomConnection()
: ConnectionPointer(xcb_connect(nullptr, nullptr)) {
}
[[nodiscard]] operator xcb_connection_t*() const {
return get();
}
};
class SharedConnection : public std::shared_ptr<CustomConnection> {
public:
SharedConnection();
[[nodiscard]] operator xcb_connection_t*() const {
return get() ? get()->get() : nullptr;
}
};
template <typename T>
using EventPointer = std::unique_ptr<T, custom_delete<free>>;
template <typename T>
[[nodiscard]] EventPointer<T> MakeEventPointer(T *event) {
return EventPointer<T>(event);
}
template <typename T>
using ReplyPointer = std::unique_ptr<T, custom_delete<free>>;
template <typename T>
[[nodiscard]] ReplyPointer<T> MakeReplyPointer(T *reply) {
return ReplyPointer<T>(reply);
}
template <typename T>
using ErrorPointer = std::unique_ptr<T, custom_delete<free>>;
template <typename T>
[[nodiscard]] ErrorPointer<T> MakeErrorPointer(T *error) {
return ErrorPointer<T>(error);
}
[[nodiscard]] xcb_connection_t *GetConnectionFromQt();
[[nodiscard]] rpl::lifetime InstallEventHandler(
xcb_connection_t *connection,
Fn<void(xcb_generic_event_t*)> handler);
[[nodiscard]] xcb_timestamp_t GetTimestamp(xcb_connection_t *connection);
[[nodiscard]] xcb_window_t GetRootWindow(xcb_connection_t *connection);
[[nodiscard]] xcb_atom_t GetAtom(
xcb_connection_t *connection,
const QString &name);
[[nodiscard]] bool IsExtensionPresent(
xcb_connection_t *connection,
xcb_extension_t *ext);
[[nodiscard]] std::vector<xcb_atom_t> GetWMSupported(
xcb_connection_t *connection,
xcb_window_t root);
[[nodiscard]] xcb_window_t GetSupportingWMCheck(
xcb_connection_t *connection,
xcb_window_t root);
[[nodiscard]] bool IsSupportedByWM(
xcb_connection_t *connection,
const QString &atomName);
enum class ChangeWindowEventMaskMode {
Add,
Remove,
Replace,
};
[[nodiscard]] rpl::lifetime ChangeWindowEventMask(
xcb_connection_t *connection,
xcb_window_t window,
uint mask,
ChangeWindowEventMaskMode mode = ChangeWindowEventMaskMode::Add,
bool revert = true);
class Connection {
public:
Connection()
: _qtConnection(GetConnectionFromQt())
, _sharedConnection(_qtConnection
? std::optional<SharedConnection>()
: std::optional<SharedConnection>(std::in_place)) {
}
[[nodiscard]] operator xcb_connection_t*() const {
return _sharedConnection
? static_cast<xcb_connection_t*>(*_sharedConnection)
: _qtConnection;
}
private:
xcb_connection_t * const _qtConnection = nullptr;
const std::optional<SharedConnection> _sharedConnection;
};
template <typename Object, auto constructor, auto destructor>
class ObjectWithConnection
: public std::unique_ptr<Object, custom_delete<destructor>> {
public:
ObjectWithConnection() {
if (_connection && !xcb_connection_has_error(_connection)) {
this->reset(constructor(_connection));
}
}
~ObjectWithConnection() {
this->reset(nullptr);
}
private:
const Connection _connection;
};
} // namespace base::Platform::XCB

View File

@@ -0,0 +1,65 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/linux/base_linux_xdg_activation_token.h"
#include "base/invoke_queued.h"
#include <QtGui/QGuiApplication>
#include <QtGui/QWindow>
#include <qpa/qplatformwindow_p.h>
namespace base::Platform {
void RunWithXdgActivationToken(Fn<void(QString)> callback) {
#if defined QT_FEATURE_wayland && QT_CONFIG(wayland)
const auto window = QGuiApplication::focusWindow();
if (!window) {
callback({});
return;
} else if (!window->isVisible()) {
InvokeQueued(qApp, [=] {
RunWithXdgActivationToken(callback);
});
return;
}
using namespace QNativeInterface;
using namespace QNativeInterface::Private;
const auto native = qApp->nativeInterface<QWaylandApplication>();
const auto nativeWindow = window->nativeInterface<QWaylandWindow>();
if (!native || !nativeWindow) {
callback({});
return;
}
QObject::connect(
nativeWindow,
&QWaylandWindow::xdgActivationTokenCreated,
nativeWindow,
callback,
Qt::SingleShotConnection);
nativeWindow->requestXdgActivationToken(native->lastInputSerial());
#else // wayland
callback({});
#endif // !wayland
}
QString XdgActivationToken() {
QString result;
QEventLoop loop;
InvokeQueued(qApp, [&] {
RunWithXdgActivationToken([&](QString token) {
result = token;
loop.quit();
});
});
loop.exec();
return result;
}
} // namespace base::Platform

View File

@@ -0,0 +1,14 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
namespace base::Platform {
void RunWithXdgActivationToken(Fn<void(QString)> callback);
QString XdgActivationToken();
} // namespace base::Platform

View File

@@ -0,0 +1,122 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/linux/base_linux_xdp_utilities.h"
#include "base/platform/base_platform_info.h"
#include <xdpsettings/xdpsettings.hpp>
#include <QtGui/QGuiApplication>
#include <QtGui/QWindow>
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
#include <qpa/qplatformintegration.h>
#include <private/qguiapplication_p.h>
#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
#include <private/qdesktopunixservices_p.h>
#else // Qt >= 6.9.0
#include <private/qgenericunixservices_p.h>
#endif // Qt < 6.9.0
#endif // Qt >= 6.5.0
#include <sstream>
namespace base::Platform::XDP {
namespace {
using namespace gi::repository;
namespace GObject = gi::repository::GObject;
} // namespace
std::string ParentWindowID() {
return ParentWindowID(QGuiApplication::focusWindow());
}
std::string ParentWindowID(QWindow *window) {
if (!window) {
return {};
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
#if QT_VERSION < QT_VERSION_CHECK(6, 9, 0)
using QDesktopUnixServices = QGenericUnixServices;
#endif // Qt < 6.9.0
if (const auto services = dynamic_cast<QDesktopUnixServices*>(
QGuiApplicationPrivate::platformIntegration()->services())) {
return services->portalWindowIdentifier(window).toStdString();
}
#endif // Qt >= 6.5.0
if (::Platform::IsX11()) {
std::stringstream result;
result << "x11:" << std::hex << window->winId();
return result.str();
}
return {};
}
gi::result<GLib::Variant> ReadSetting(
const std::string &group,
const std::string &key) {
auto interface = gi::result<XdpSettings::Settings>(
XdpSettings::SettingsProxy::new_for_bus_sync(
Gio::BusType::SESSION_,
Gio::DBusProxyFlags::NONE_,
kService,
kObjectPath));
if (!interface) {
return make_unexpected(std::move(interface.error()));
}
auto result = interface->call_read_one_sync(group, key);
if (!result) {
return make_unexpected(std::move(result.error()));
}
return std::get<1>(*result).get_variant();
}
class SettingWatcher::Private {
public:
XdpSettings::Settings interface;
};
SettingWatcher::SettingWatcher(
Fn<void(
const std::string &,
const std::string &,
GLib::Variant)> callback)
: _private(std::make_unique<Private>()) {
XdpSettings::SettingsProxy::new_for_bus(
Gio::BusType::SESSION_,
Gio::DBusProxyFlags::NONE_,
kService,
kObjectPath,
crl::guard(this, [=](GObject::Object, Gio::AsyncResult res) {
_private->interface = XdpSettings::Settings(
XdpSettings::SettingsProxy::new_for_bus_finish(res, nullptr));
if (!_private->interface) {
return;
}
_private->interface.signal_setting_changed().connect([=](
XdpSettings::Settings,
std::string group,
std::string key,
GLib::Variant value) {
callback(group, key, value.get_variant());
});
}));
}
SettingWatcher::~SettingWatcher() = default;
} // namespace base::Platform::XDP

View File

@@ -0,0 +1,158 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/expected.h"
#include "base/weak_ptr.h"
#include <glib/glib.hpp>
#if __has_include(<glibmm.h>)
#include <glibmm.h>
#endif // __has_include(<glibmm.h>)
class QWindow;
namespace base::Platform::XDP {
inline constexpr auto kService = "org.freedesktop.portal.Desktop";
inline constexpr auto kObjectPath = "/org/freedesktop/portal/desktop";
inline constexpr auto kRequestInterface = "org.freedesktop.portal.Request";
inline constexpr auto kSettingsInterface = "org.freedesktop.portal.Settings";
std::string ParentWindowID();
std::string ParentWindowID(QWindow *window);
gi::result<gi::repository::GLib::Variant> ReadSetting(
const std::string &group,
const std::string &key);
#if __has_include(<glibmm.h>)
template <typename T>
gi::result<T> ReadSetting(
const std::string &group,
const std::string &key) {
try {
auto value = ReadSetting(group, key);
if (value) {
return Glib::wrap(value->gobj_copy_()).get_dynamic<T>();
}
return make_unexpected(std::move(value.error()));
} catch (std::invalid_argument &e) {
return make_unexpected(
std::make_unique<std::invalid_argument>(std::move(e)));
} catch (std::bad_cast &e) {
return make_unexpected(std::make_unique<std::bad_cast>(std::move(e)));
} catch (...) {
return make_unexpected(std::make_unique<std::exception>());
}
}
#endif
class SettingWatcher : public has_weak_ptr {
public:
SettingWatcher(
Fn<void(
const std::string &,
const std::string &,
gi::repository::GLib::Variant)> callback);
SettingWatcher(
const std::string &group,
const std::string &key,
Fn<void(gi::repository::GLib::Variant)> callback)
: SettingWatcher([=](
const std::string &group2,
const std::string &key2,
gi::repository::GLib::Variant value) {
if (group == group2 && key == key2) {
callback(value);
}
}) {
}
SettingWatcher(
const std::string &group,
const std::string &key,
Fn<void()> callback)
: SettingWatcher(group, key, [=](gi::repository::GLib::Variant) {
callback();
}) {
}
#if __has_include(<glibmm.h>)
template <typename ...Args>
SettingWatcher(
Fn<void(
const std::string &,
const std::string &,
Args...)> callback)
: SettingWatcher([=](
const std::string &group,
const std::string &key,
gi::repository::GLib::Variant value) {
if constexpr (sizeof...(Args) == 0) {
callback(group, key);
return;
}
try {
using Tuple = std::tuple<std::decay_t<Args>...>;
if constexpr (sizeof...(Args) == 1) {
using Arg0 = std::tuple_element_t<0, Tuple>;
callback(
group,
key,
Glib::wrap(value.gobj_copy_()).get_dynamic<Arg0>());
} else {
std::apply(
callback,
std::tuple_cat(
std::forward_as_tuple(group, key),
Glib::wrap(value.gobj_copy_()).get_dynamic<Tuple>()));
}
} catch (...) {
}
}) {
}
template <typename Callback>
SettingWatcher(Callback callback)
: SettingWatcher(std::function(callback)) {
}
template <typename ...Args>
SettingWatcher(
const std::string &group,
const std::string &key,
Fn<void(Args...)> callback)
: SettingWatcher([=](
const std::string &group2,
const std::string &key2,
Args &&...value) {
if (group == group2 && key == key2) {
callback(std::forward<decltype(value)>(value)...);
}
}) {
}
template <typename Callback>
SettingWatcher(
const std::string &group,
const std::string &key,
Callback callback)
: SettingWatcher(group, key, std::function(callback)) {
}
#endif // __has_include(<glibmm.h>)
~SettingWatcher();
private:
class Private;
const std::unique_ptr<Private> _private;
};
} // namespace base::Platform::XDP

View File

@@ -0,0 +1,332 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/linux/base_linux_xsettings.h"
#include <QtCore/QByteArray>
#include <QtCore/QtEndian>
#include <QtGui/QColor>
#include <vector>
#include <algorithm>
namespace base::Platform::XCB {
/* Implementation of http://standards.freedesktop.org/xsettings-spec/xsettings-0.5.html */
class XSettings::PropertyValue {
public:
PropertyValue() {
}
void updateValue(
xcb_connection_t *connection,
const QByteArray &name,
const QVariant &value,
int last_change_serial) {
if (last_change_serial <= this->last_change_serial)
return;
this->value = value;
this->last_change_serial = last_change_serial;
for (const auto &callback : callback_links)
(*callback)(connection, name, value);
}
rpl::lifetime addCallback(PropertyChangeFunc func) {
const auto handle = Instance();
callback_links.push_back(std::make_unique<PropertyChangeFunc>(func));
const auto ptr = callback_links.back().get();
return rpl::lifetime([=] {
(void)handle;
callback_links.erase(
ranges::remove(
callback_links,
ptr,
&decltype(callback_links)::value_type::get),
callback_links.end());
});
}
QVariant value;
int last_change_serial = -1;
std::vector<std::unique_ptr<PropertyChangeFunc>> callback_links;
};
class XSettings::Private {
public:
Private() {
}
QByteArray getSettings() {
int offset = 0;
QByteArray settings;
const auto _xsettings_atom = GetAtom(
connection,
"_XSETTINGS_SETTINGS");
if (!_xsettings_atom)
return settings;
while (1) {
const auto cookie = xcb_get_property(
connection,
false,
x_settings_window,
_xsettings_atom,
_xsettings_atom,
offset/4,
8192);
const auto reply = MakeReplyPointer(xcb_get_property_reply(
connection,
cookie,
nullptr));
bool more = false;
if (!reply)
return settings;
const auto property_value_length = xcb_get_property_value_length(
reply.get());
settings.append(
static_cast<const char *>(xcb_get_property_value(
reply.get())),
property_value_length);
offset += property_value_length;
more = reply->bytes_after != 0;
if (!more)
break;
}
return settings;
}
static int round_to_nearest_multiple_of_4(int value) {
int remainder = value % 4;
if (!remainder)
return value;
return value + 4 - remainder;
}
void populateSettings(const QByteArray &xSettings) {
if (xSettings.length() < 12)
return;
char byteOrder = xSettings.at(0);
if (byteOrder != XCB_IMAGE_ORDER_LSB_FIRST
&& byteOrder != XCB_IMAGE_ORDER_MSB_FIRST) {
qWarning("ByteOrder byte %d not 0 or 1", byteOrder);
return;
}
#define ADJUST_BO(b, t, x) \
((b == XCB_IMAGE_ORDER_LSB_FIRST) ? \
qFromLittleEndian<t>(x) : \
qFromBigEndian<t>(x))
#define VALIDATE_LENGTH(x) \
if ((size_t)xSettings.length() < (offset + local_offset + 12 + x)) { \
qWarning("Length %d runs past end of data", x); \
return; \
}
uint number_of_settings = ADJUST_BO(
byteOrder,
quint32,
xSettings.mid(8,4).constData());
const char *data = xSettings.constData() + 12;
size_t offset = 0;
for (uint i = 0; i < number_of_settings; i++) {
int local_offset = 0;
VALIDATE_LENGTH(2);
Type type = static_cast<Type>(
*reinterpret_cast<const quint8 *>(data + offset));
local_offset += 2;
VALIDATE_LENGTH(2);
quint16 name_len = ADJUST_BO(
byteOrder,
quint16,
data + offset + local_offset);
local_offset += 2;
VALIDATE_LENGTH(name_len);
QByteArray name(data + offset + local_offset, name_len);
local_offset += round_to_nearest_multiple_of_4(name_len);
VALIDATE_LENGTH(4);
int last_change_serial = ADJUST_BO(
byteOrder,
qint32,
data + offset + local_offset);
Q_UNUSED(last_change_serial);
local_offset += 4;
QVariant value;
if (type == Type::String) {
VALIDATE_LENGTH(4);
int value_length = ADJUST_BO(
byteOrder,
qint32,
data + offset + local_offset);
local_offset+=4;
VALIDATE_LENGTH(value_length);
QByteArray value_string(
data + offset + local_offset,
value_length);
value.setValue(value_string);
local_offset += round_to_nearest_multiple_of_4(value_length);
} else if (type == Type::Integer) {
VALIDATE_LENGTH(4);
int value_length = ADJUST_BO(
byteOrder,
qint32,
data + offset + local_offset);
local_offset += 4;
value.setValue(value_length);
} else if (type == Type::Color) {
VALIDATE_LENGTH(2*4);
quint16 red = ADJUST_BO(
byteOrder,
quint16,
data + offset + local_offset);
local_offset += 2;
quint16 green = ADJUST_BO(
byteOrder,
quint16,
data + offset + local_offset);
local_offset += 2;
quint16 blue = ADJUST_BO(
byteOrder,
quint16,
data + offset + local_offset);
local_offset += 2;
quint16 alpha = ADJUST_BO(
byteOrder,
quint16,
data + offset + local_offset);
local_offset += 2;
QColor color_value(red,green,blue,alpha);
value.setValue(color_value);
}
offset += local_offset;
settings[name].updateValue(
connection,
name,
value,
last_change_serial);
}
}
const Connection connection;
xcb_window_t x_settings_window = XCB_NONE;
base::flat_map<QByteArray, PropertyValue> settings;
bool initialized = false;
rpl::lifetime lifetime;
};
XSettings::XSettings()
: _private(std::make_unique<Private>()) {
if (!_private->connection)
return;
if (xcb_connection_has_error(_private->connection))
return;
const auto selection_owner_atom = GetAtom(
_private->connection,
"_XSETTINGS_S0");
if (!selection_owner_atom)
return;
const auto selection_cookie = xcb_get_selection_owner(
_private->connection,
selection_owner_atom);
const auto selection_result = MakeReplyPointer(
xcb_get_selection_owner_reply(
_private->connection,
selection_cookie,
nullptr));
if (!selection_result)
return;
_private->x_settings_window = selection_result->owner;
if (!_private->x_settings_window)
return;
auto event_handler = InstallEventHandler(
_private->connection,
[=](xcb_generic_event_t *event) {
if (!event)
return;
const auto response_type = event->response_type & ~0x80;
if (response_type != XCB_PROPERTY_NOTIFY)
return;
const auto pn = reinterpret_cast<xcb_property_notify_event_t*>(
event);
if (pn->window != _private->x_settings_window)
return;
_private->populateSettings(_private->getSettings());
});
if (!event_handler)
return;
_private->lifetime.add(std::move(event_handler));
auto event_mask = ChangeWindowEventMask(
_private->connection,
_private->x_settings_window,
XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_PROPERTY_CHANGE);
if (!event_mask)
return;
_private->lifetime.add(std::move(event_mask));
_private->populateSettings(_private->getSettings());
_private->initialized = true;
}
XSettings::~XSettings() = default;
std::shared_ptr<XSettings> XSettings::Instance() {
static std::weak_ptr<XSettings> Weak;
auto result = Weak.lock();
if (!result) {
Weak = result = std::shared_ptr<XSettings>(
new XSettings,
[](XSettings *ptr) { delete ptr; });
}
return result;
}
bool XSettings::initialized() const {
return _private->initialized;
}
rpl::lifetime XSettings::registerCallbackForProperty(
const QByteArray &property,
PropertyChangeFunc func) {
return _private->settings[property].addCallback(func);
}
QVariant XSettings::setting(const QByteArray &property) const {
return _private->settings[property].value;
}
} // namespace base::Platform::XCB

View File

@@ -0,0 +1,45 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/platform/linux/base_linux_xcb_utilities.h"
namespace base::Platform::XCB {
class XSettings {
public:
[[nodiscard]] static std::shared_ptr<XSettings> Instance();
[[nodiscard]] bool initialized() const;
[[nodiscard]] QVariant setting(const QByteArray &property) const;
using PropertyChangeFunc = Fn<void(
xcb_connection_t *connection,
const QByteArray &name,
const QVariant &property)>;
[[nodiscard]] rpl::lifetime registerCallbackForProperty(
const QByteArray &property,
PropertyChangeFunc func);
private:
XSettings();
~XSettings();
enum class Type {
Integer,
String,
Color,
};
class PropertyValue;
class Private;
const std::unique_ptr<Private> _private;
};
} // namespace base::Platform::XCB

View File

@@ -0,0 +1,15 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/base_platform_network_reachability.h"
namespace base::Platform {
std::unique_ptr<NetworkReachability> NetworkReachability::Create() {
return nullptr;
}
} // namespace base::Platform

View File

@@ -0,0 +1,214 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/linux/base_power_save_blocker_linux.h"
#include "base/platform/base_platform_info.h"
#include "base/platform/linux/base_linux_xdp_utilities.h"
#include "base/timer_rpl.h"
#include "base/weak_ptr.h"
#include "base/random.h"
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
#include "base/platform/linux/base_linux_xcb_utilities.h"
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
#include <QtGui/QWindow>
#include <xdpinhibit/xdpinhibit.hpp>
#include <xdprequest/xdprequest.hpp>
namespace base::Platform {
namespace {
using namespace gi::repository;
namespace GObject = gi::repository::GObject;
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
constexpr auto kResetScreenSaverTimeout = 10 * crl::time(1000);
// Use the basic reset API
// due to https://gitlab.freedesktop.org/xorg/xserver/-/issues/363
void XCBPreventDisplaySleep(bool prevent) {
static rpl::lifetime lifetime;
if (!prevent) {
lifetime.destroy();
return;
} else if (lifetime) {
return;
}
timer_each(
kResetScreenSaverTimeout
) | rpl::map([connection = XCB::Connection()] {
return connection;
}) | rpl::filter([](xcb_connection_t *connection) {
return connection && !xcb_connection_has_error(connection);
}) | rpl::on_next([](xcb_connection_t *connection) {
free(
xcb_request_check(
connection,
xcb_force_screen_saver_checked(
connection,
XCB_SCREEN_SAVER_RESET)));
}, lifetime);
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
/* https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.Inhibit
* 1: Logout
* 2: User Switch
* 4: Suspend
* 8: Idle
*/
class PortalInhibit final : public has_weak_ptr {
public:
PortalInhibit(uint flags = 0, const QString &description = {}) {
XdpInhibit::InhibitProxy::new_for_bus(
Gio::BusType::SESSION_,
Gio::DBusProxyFlags::NONE_,
XDP::kService,
XDP::kObjectPath,
crl::guard(this, [=](GObject::Object, Gio::AsyncResult res) {
auto proxy = XdpInhibit::InhibitProxy::new_for_bus_finish(
res,
nullptr);
if (!proxy) {
return;
}
const auto handleToken = "desktop_app"
+ std::to_string(RandomValue<uint>());
auto uniqueName = std::string(
proxy.get_connection().get_unique_name());
uniqueName.erase(0, 1);
uniqueName.replace(uniqueName.find('.'), 1, 1, '_');
XdpRequest::RequestProxy::new_(
proxy.get_connection(),
Gio::DBusProxyFlags::NONE_,
XDP::kService,
XDP::kObjectPath
+ std::string("/request/")
+ uniqueName
+ '/'
+ handleToken,
nullptr,
crl::guard(this, [=](
GObject::Object,
Gio::AsyncResult res) mutable {
_request = XdpRequest::RequestProxy::new_finish(
res,
nullptr);
auto request = _request; // take a ref
const auto weak = make_weak(this);
const auto signalId = std::make_shared<ulong>();
*signalId = _request.signal_response().connect([=](
XdpRequest::Request,
guint,
GLib::Variant) mutable {
if (!weak) {
request.call_close(nullptr);
}
request.disconnect(*signalId);
});
XdpInhibit::Inhibit(proxy).call_inhibit(
XDP::ParentWindowID(),
flags,
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(
description.toStdString()))),
}),
nullptr);
}));
}));
}
~PortalInhibit() {
if (!_request) {
return;
}
_request.call_close(nullptr);
}
private:
XdpRequest::Request _request;
};
void PortalPreventDisplaySleep(
bool prevent,
const QString &description = {}) {
static std::optional<PortalInhibit> instance;
if (prevent && !instance) {
instance.emplace(8 /* Idle */, description);
} else if (!prevent && instance) {
instance.reset();
}
}
void PortalPreventAppSuspension(
bool prevent,
const QString &description = {}) {
static std::optional<PortalInhibit> instance;
if (prevent && !instance) {
instance.emplace(4 /* Suspend */, description);
} else if (!prevent && instance) {
instance.reset();
}
}
} // namespace
void BlockPowerSave(
PowerSaveBlockType type,
const QString &description,
QPointer<QWindow> window) {
crl::on_main([=] {
switch (type) {
case PowerSaveBlockType::PreventAppSuspension:
PortalPreventAppSuspension(true, description);
break;
case PowerSaveBlockType::PreventDisplaySleep:
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
XCBPreventDisplaySleep(true);
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
PortalPreventDisplaySleep(true, description);
break;
}
});
}
void UnblockPowerSave(PowerSaveBlockType type, QPointer<QWindow> window) {
crl::on_main([=] {
switch (type) {
case PowerSaveBlockType::PreventAppSuspension:
PortalPreventAppSuspension(false);
break;
case PowerSaveBlockType::PreventDisplaySleep:
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
XCBPreventDisplaySleep(false);
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
PortalPreventDisplaySleep(false);
break;
}
});
}
} // namespace base::Platform

View File

@@ -0,0 +1,9 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/platform/base_platform_power_save_blocker.h"

View File

@@ -0,0 +1,110 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/linux/base_process_linux.h"
#include "base/platform/base_platform_info.h"
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
#include "base/platform/linux/base_linux_xcb_utilities.h"
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
#include <QtGui/QGuiApplication>
namespace base::Platform {
namespace {
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
void XCBActivateWindow(WId window) {
const XCB::Connection connection;
if (!connection || xcb_connection_has_error(connection)) {
return;
}
const auto root = XCB::GetRootWindow(connection);
if (!root) {
return;
}
const auto timestamp = XCB::GetTimestamp(connection);
if (!timestamp) {
return;
}
const auto activeWindowAtom = XCB::GetAtom(
connection,
"_NET_ACTIVE_WINDOW");
if (!activeWindowAtom) {
return;
}
const auto focusWindow = QGuiApplication::focusWindow();
// map the window first
free(
xcb_request_check(
connection,
xcb_map_window_checked(connection, window)));
// now raise (restack) the window
uint32_t values[] = { XCB_STACK_MODE_ABOVE };
free(
xcb_request_check(
connection,
xcb_configure_window_checked(
connection,
window,
XCB_CONFIG_WINDOW_STACK_MODE,
values)));
// and, finally, make it the active window
xcb_client_message_event_t xev;
xev.response_type = XCB_CLIENT_MESSAGE;
xev.format = 32;
xev.sequence = 0;
xev.window = window;
xev.type = activeWindowAtom;
xev.data.data32[0] = 1; // source: 1=application 2=pager
xev.data.data32[1] = timestamp; // timestamp
xev.data.data32[2] = focusWindow // currently active window
? focusWindow->winId()
: XCB_NONE;
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
} // namespace
void ActivateProcessWindow(int64 pid, WId windowId) {
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
if (::Platform::IsX11()) {
XCBActivateWindow(windowId);
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
}
void ActivateThisProcessWindow(WId windowId) {
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
if (::Platform::IsX11()) {
XCBActivateWindow(windowId);
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
}
} // namespace base::Platform

View File

@@ -0,0 +1,9 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/platform/base_platform_process.h"

View File

@@ -0,0 +1,485 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/base_platform_system_media_controls.h"
#include "base/integration.h"
#include "base/platform/base_platform_info.h" // IsWayland
#include <mpris/mpris.hpp>
#include <QtCore/QBuffer>
#include <QtGui/QGuiApplication>
#include <QtGui/QImage>
#include <ksandbox.h>
namespace base::Platform {
namespace {
using namespace gi::repository;
namespace GObject = gi::repository::GObject;
// QString to GLib::Variant.
inline auto Q2V(const QString &s) {
return GLib::Variant::new_string(s.toStdString());
}
std::string ConvertPlaybackStatus(
SystemMediaControls::PlaybackStatus status) {
using Status = SystemMediaControls::PlaybackStatus;
switch (status) {
case Status::Playing: return "Playing";
case Status::Paused: return "Paused";
case Status::Stopped: return "Stopped";
}
Unexpected("ConvertPlaybackStatus in SystemMediaControls");
}
std::string ConvertLoopStatus(SystemMediaControls::LoopStatus status) {
using Status = SystemMediaControls::LoopStatus;
switch (status) {
case Status::None: return "None";
case Status::Track: return "Track";
case Status::Playlist: return "Playlist";
}
Unexpected("ConvertLoopStatus in SystemMediaControls");
}
auto EventToCommand(const std::string &event) {
using Command = SystemMediaControls::Command;
if (event == "Pause") {
return Command::Pause;
} else if (event == "Play") {
return Command::Play;
} else if (event == "Stop") {
return Command::Stop;
} else if (event == "PlayPause") {
return Command::PlayPause;
} else if (event == "Next") {
return Command::Next;
} else if (event == "Previous") {
return Command::Previous;
} else if (event == "Quit") {
return Command::Quit;
} else if (event == "Raise") {
return Command::Raise;
}
Unexpected("EventToCommand in SystemMediaControls");
}
auto LoopStatusToCommand(const std::string &status) {
using Command = SystemMediaControls::Command;
if (status == "None") {
return Command::LoopNone;
} else if (status == "Track") {
return Command::LoopTrack;
} else if (status == "Playlist") {
return Command::LoopPlaylist;
}
return Command::None;
}
} // namespace
struct SystemMediaControls::Private : public Mpris::MediaPlayer2 {
public:
struct PlayerData {
int64 duration = 0;
bool inSetShuffle = false;
};
Private();
void init();
void deinit();
[[nodiscard]] bool dbusAvailable() {
return static_cast<bool>(_dbus.connection);
}
[[nodiscard]] Mpris::MediaPlayer2Player player() {
return *_player;
}
[[nodiscard]] PlayerData &playerData() {
return _playerData;
}
[[nodiscard]] rpl::producer<Command> commandRequests() const {
return _commandRequests.events();
}
[[nodiscard]] rpl::producer<int64> seekRequests() const {
return _seekRequests.events();
}
[[nodiscard]] rpl::producer<float64> volumeChangeRequests() const {
return _volumeChangeRequests.events();
}
[[nodiscard]] rpl::producer<> updatePositionRequests() const {
return _updatePositionRequests.events();
}
private:
class Player : public Mpris::impl::MediaPlayer2PlayerSkeletonImpl {
public:
struct DefinitionData {
GI_DEFINES_MEMBER(
Gio::impl::internal::DBusInterfaceSkeletonClassDef,
get_info,
true)
GI_DEFINES_MEMBER(
Gio::impl::internal::DBusInterfaceIfaceDef,
get_info,
false)
};
Player(not_null<Private*> parent)
: Mpris::impl::MediaPlayer2PlayerSkeletonImpl(this)
, _parent(parent)
, _position(this) {
}
private:
const not_null<Private*> _parent;
class Position : public gi::property<int64> {
public:
Position(not_null<Player*> parent)
: gi::property<int64>(parent, "position")
, _parent(parent) {
}
protected:
void get_property(GValue *value) override {
// prevent recursion via SystemMediaControls::setPosition
if (!_updating) {
_updating = true;
_parent->_parent->_updatePositionRequests.fire({});
_updating = false;
}
gi::property<int64>::get_property(value);
}
private:
const not_null<Player*> _parent;
bool _updating = false;
} _position;
};
gi::ref_ptr<Player> _player;
struct {
Gio::DBusConnection connection;
Gio::DBusObjectManagerServer objectManager;
uint ownId = 0;
} _dbus;
PlayerData _playerData;
rpl::event_stream<Command> _commandRequests;
rpl::event_stream<int64> _seekRequests;
rpl::event_stream<float64> _volumeChangeRequests;
rpl::event_stream<> _updatePositionRequests;
};
SystemMediaControls::Private::Private()
: Mpris::MediaPlayer2(Mpris::MediaPlayer2Skeleton::new_())
, _player(gi::make_ref<Player>(this))
, _dbus({
.connection = Gio::bus_get_sync(Gio::BusType::SESSION_, nullptr)
}) {
set_can_quit(true);
set_can_raise(!::Platform::IsWayland());
set_desktop_entry(QGuiApplication::desktopFileName().toStdString());
set_identity(QGuiApplication::desktopFileName().toStdString());
player().set_can_control(true);
player().set_can_seek(true);
player().set_maximum_rate(1.0);
player().set_minimum_rate(1.0);
player().set_playback_status("Stopped");
player().set_loop_status("None");
player().set_rate(1.0);
const auto executeCommand = [=](
GObject::Object,
Gio::DBusMethodInvocation invocation) {
base::Integration::Instance().enterFromEventLoop([&] {
_commandRequests.fire_copy(
EventToCommand(invocation.get_method_name()));
});
invocation.return_value();
return true;
};
signal_handle_quit().connect(executeCommand);
signal_handle_raise().connect(executeCommand);
player().signal_handle_next().connect(executeCommand);
player().signal_handle_pause().connect(executeCommand);
player().signal_handle_play().connect(executeCommand);
player().signal_handle_play_pause().connect(executeCommand);
player().signal_handle_previous().connect(executeCommand);
player().signal_handle_stop().connect(executeCommand);
player().signal_handle_seek().connect([=](
Mpris::MediaPlayer2Player,
Gio::DBusMethodInvocation invocation,
int64 offset) {
base::Integration::Instance().enterFromEventLoop([&] {
_seekRequests.fire_copy(
player().property_position().get() + offset);
});
player().complete_seek(invocation);
return true;
});
player().signal_handle_set_position().connect([=](
Mpris::MediaPlayer2Player,
Gio::DBusMethodInvocation invocation,
const std::string &trackId,
int64 position) {
base::Integration::Instance().enterFromEventLoop([&] {
_seekRequests.fire_copy(position);
});
player().complete_set_position(invocation);
return true;
});
player().property_loop_status().signal_notify().connect([=](
GObject::Object,
GObject::ParamSpec) {
base::Integration::Instance().enterFromEventLoop([&] {
_commandRequests.fire_copy(
LoopStatusToCommand(player().get_loop_status()));
});
});
player().property_shuffle().signal_notify().connect([=](
GObject::Object,
GObject::ParamSpec) {
if (playerData().inSetShuffle) {
return;
}
base::Integration::Instance().enterFromEventLoop([&] {
_commandRequests.fire_copy(Command::Shuffle);
});
});
player().property_volume().signal_notify().connect([=](
GObject::Object,
GObject::ParamSpec) {
base::Integration::Instance().enterFromEventLoop([&] {
_volumeChangeRequests.fire_copy(player().get_volume());
});
});
}
void SystemMediaControls::Private::init() {
if (!_dbus.connection || _dbus.ownId) {
return;
}
auto object = Mpris::ObjectSkeleton::new_("/org/mpris/MediaPlayer2");
object.set_media_player2(*this);
object.set_media_player2_player(player());
_dbus.objectManager = Gio::DBusObjectManagerServer::new_("/org/mpris");
_dbus.objectManager.export_(object);
_dbus.objectManager.set_connection(_dbus.connection);
_dbus.ownId = Gio::bus_own_name_on_connection(
_dbus.connection,
"org.mpris.MediaPlayer2." + (KSandbox::isFlatpak()
? qEnvironmentVariable("FLATPAK_ID").toStdString()
: KSandbox::isSnap()
? qEnvironmentVariable("SNAP_INSTANCE_NAME").toStdString()
: QCoreApplication::applicationName().toStdString()),
Gio::BusNameOwnerFlags::NONE_);
}
void SystemMediaControls::Private::deinit() {
if (_dbus.ownId) {
Gio::bus_unown_name(_dbus.ownId);
_dbus.ownId = 0;
}
_dbus.objectManager = {};
}
SystemMediaControls::SystemMediaControls()
: _private(std::make_unique<Private>()) {
}
SystemMediaControls::~SystemMediaControls() {
_private->deinit();
}
bool SystemMediaControls::init() {
clearMetadata();
return _private->dbusAvailable();
}
void SystemMediaControls::setApplicationName(const QString &name) {
_private->set_identity(name.toStdString());
}
void SystemMediaControls::setEnabled(bool enabled) {
if (enabled) {
_private->init();
} else {
_private->deinit();
}
}
void SystemMediaControls::setIsNextEnabled(bool value) {
_private->player().set_can_go_next(value);
}
void SystemMediaControls::setIsPreviousEnabled(bool value) {
_private->player().set_can_go_previous(value);
}
void SystemMediaControls::setIsPlayPauseEnabled(bool value) {
_private->player().set_can_play(value);
_private->player().set_can_pause(value);
}
void SystemMediaControls::setIsStopEnabled(bool value) {
}
void SystemMediaControls::setPlaybackStatus(PlaybackStatus status) {
_private->player().set_playback_status(ConvertPlaybackStatus(status));
}
void SystemMediaControls::setLoopStatus(LoopStatus status) {
// prevent property update -> rpl event -> property update recursion
const auto statusString = ConvertLoopStatus(status);
if (_private->player().get_loop_status() != statusString) {
_private->player().set_loop_status(ConvertLoopStatus(status));
}
}
void SystemMediaControls::setShuffle(bool value) {
// prevent property update -> rpl event -> property update recursion
_private->playerData().inSetShuffle = true;
if (_private->player().get_shuffle() != value) {
_private->player().set_shuffle(value);
}
_private->playerData().inSetShuffle = false;
}
void SystemMediaControls::setTitle(const QString &title) {
auto metadata = GLib::VariantDict::new_(
_private->player().get_metadata());
metadata.insert_value("xesam:title", Q2V(title));
_private->player().set_metadata(metadata.end());
}
void SystemMediaControls::setArtist(const QString &artist) {
auto metadata = GLib::VariantDict::new_(
_private->player().get_metadata());
metadata.insert_value(
"xesam:artist",
GLib::Variant::new_array({ Q2V(artist) }));
_private->player().set_metadata(metadata.end());
}
void SystemMediaControls::setThumbnail(const QImage &thumbnail) {
QByteArray thumbnailData;
QBuffer thumbnailBuffer(&thumbnailData);
thumbnail.save(&thumbnailBuffer, "JPG", 87);
auto metadata = GLib::VariantDict::new_(
_private->player().get_metadata());
metadata.insert_value(
"mpris:artUrl",
Q2V("data:image/jpeg;base64," + thumbnailData.toBase64()));
_private->player().set_metadata(metadata.end());
}
void SystemMediaControls::setDuration(int duration) {
_private->playerData().duration = duration * 1000;
auto metadata = GLib::VariantDict::new_(
_private->player().get_metadata());
metadata.insert_value(
"mpris:length",
GLib::Variant::new_int64(_private->playerData().duration));
_private->player().set_metadata(metadata.end());
}
void SystemMediaControls::setPosition(int position) {
// get_position reads the value directly from variable
// in the generated C code and doesn't account for the
// property override. It's possible to override get_position
// but the C++ bindings don't let do this due to
// https://gitlab.gnome.org/GNOME/gobject-introspection/-/issues/183
//
// Once this is fixed, Player should be able to override get_position
// and it will be possible to use (get|set)_position here
// like other functions do with properties.
auto prop = _private->player().property_position();
const auto was = prop.get();
prop.set(position * 1000);
const auto playerPosition = prop.get();
const auto positionDifference = was - playerPosition;
if (positionDifference > 1000000 || positionDifference < -1000000) {
_private->player().emit_seeked(playerPosition);
}
}
void SystemMediaControls::setVolume(float64 volume) {
// prevent property update -> rpl event -> property update recursion
if (_private->player().get_volume() != volume) {
_private->player().set_volume(volume);
}
}
void SystemMediaControls::clearThumbnail() {
auto metadata = GLib::VariantDict::new_(
_private->player().get_metadata());
metadata.remove("mpris:artUrl");
_private->player().set_metadata(metadata.end());
}
void SystemMediaControls::clearMetadata() {
_private->player().set_metadata(GLib::Variant::new_array({
GLib::Variant::new_dict_entry(
GLib::Variant::new_string("mpris:trackid"),
// fake path
GLib::Variant::new_variant(
GLib::Variant::new_object_path("/org/desktop_app/track/0"))),
}));
}
void SystemMediaControls::updateDisplay() {
}
auto SystemMediaControls::commandRequests() const
-> rpl::producer<SystemMediaControls::Command> {
return _private->commandRequests();
}
rpl::producer<float64> SystemMediaControls::seekRequests() const {
return _private->seekRequests(
) | rpl::map([=](int64 position) {
return float64(position) / _private->playerData().duration;
});
}
rpl::producer<float64> SystemMediaControls::volumeChangeRequests() const {
return _private->volumeChangeRequests();
}
rpl::producer<> SystemMediaControls::updatePositionRequests() const {
return _private->updatePositionRequests();
}
bool SystemMediaControls::seekingSupported() const {
return true;
}
bool SystemMediaControls::volumeSupported() const {
return true;
}
bool SystemMediaControls::Supported() {
return true;
}
} // namespace base::Platform

View File

@@ -0,0 +1,23 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/linux/base_system_unlock_linux.h"
namespace base {
rpl::producer<SystemUnlockAvailability> SystemUnlockStatus(
bool lookupDetails) {
return rpl::single(SystemUnlockAvailability{ .known = true });
}
void SuggestSystemUnlock(
not_null<QWidget*> parent,
const QString &text,
Fn<void(SystemUnlockResult)> done) {
done(SystemUnlockResult::Cancelled);
}
} // namespace base

View File

@@ -0,0 +1,9 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/system_unlock.h"

View File

@@ -0,0 +1,169 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/linux/base_url_scheme_linux.h"
#include "base/integration.h"
#include "base/debug_log.h"
#include <QtGui/QGuiApplication>
#include <kshell.h>
#include <ksandbox.h>
#include <gio/gio.hpp>
#include <snapcraft/snapcraft.hpp>
#if __has_include(<giounix/giounix.hpp>)
#include <giounix/giounix.hpp>
#else // __has_include(<giounix/giounix.hpp>)
#define GioUnix Gio
#endif // !__has_include(<giounix/giounix.hpp>)
namespace base::Platform {
namespace {
using namespace gi::repository;
namespace GObject = gi::repository::GObject;
void SnapDefaultHandler(const QString &protocol) {
Snapcraft::SettingsProxy::new_for_bus(
Gio::BusType::SESSION_,
Gio::DBusProxyFlags::NONE_,
"io.snapcraft.Settings",
"/io/snapcraft/Settings",
[=](GObject::Object, Gio::AsyncResult res) {
auto interface = Snapcraft::Settings(
Snapcraft::SettingsProxy::new_for_bus_finish(res, nullptr));
if (!interface) {
return;
}
interface.call_get_sub(
"default-url-scheme-handler",
protocol.toStdString(),
[=](GObject::Object, Gio::AsyncResult res) mutable {
const auto currentHandler = [&]()
-> std::optional<std::string> {
if (auto result = interface.call_get_sub_finish(
res)) {
return std::get<1>(*result).opt_();
}
return std::nullopt;
}();
if (!currentHandler) {
return;
}
const auto &integration = Integration::Instance();
const auto expectedHandler = integration.executableName()
+ u".desktop"_q;
if (currentHandler->c_str() == expectedHandler) {
return;
}
interface.call_set_sub(
"default-url-scheme-handler",
protocol.toStdString(),
expectedHandler.toStdString(),
nullptr);
});
});
}
} // namespace
bool CheckUrlScheme(const UrlSchemeDescriptor &descriptor) {
const auto handlerType = "x-scheme-handler/"
+ descriptor.protocol.toStdString();
const auto neededCommandline = KShell::joinArgs(QStringList{
descriptor.executable,
} + KShell::splitArgs(descriptor.arguments) + QStringList{
"--",
}).toStdString();
auto currentAppInfo = Gio::AppInfo::get_default_for_type(
handlerType,
true);
if (currentAppInfo) {
return currentAppInfo.get_commandline() == neededCommandline + " %u"
|| currentAppInfo.get_commandline() == neededCommandline + " %U";
}
return false;
}
void RegisterUrlScheme(const UrlSchemeDescriptor &descriptor) {
if (KSandbox::isSnap()) {
SnapDefaultHandler(descriptor.protocol);
return;
}
if (CheckUrlScheme(descriptor)) {
return;
}
UnregisterUrlScheme(descriptor);
const auto handlerType = "x-scheme-handler/"
+ descriptor.protocol.toStdString();
const auto commandlineForCreator = KShell::joinArgs(QStringList{
descriptor.executable,
} + KShell::splitArgs(descriptor.arguments) + QStringList{
"--",
}).toStdString();
const auto appId = QGuiApplication::desktopFileName().toStdString();
if (!appId.empty()) {
Gio::AppInfo appInfo = GioUnix::DesktopAppInfo::new_(appId + ".desktop");
if (appInfo) {
if (appInfo.get_commandline() == commandlineForCreator + " %u"
|| appInfo.get_commandline() == commandlineForCreator + " %U") {
appInfo.set_as_default_for_type(handlerType);
return;
}
}
}
auto newAppInfo = Gio::AppInfo::create_from_commandline(
commandlineForCreator,
descriptor.displayAppName.toStdString(),
Gio::AppInfoCreateFlags::SUPPORTS_URIS_
| Gio::AppInfoCreateFlags::SUPPORTS_STARTUP_NOTIFICATION_,
nullptr);
if (newAppInfo) {
newAppInfo.set_as_default_for_type(handlerType);
}
}
void UnregisterUrlScheme(const UrlSchemeDescriptor &descriptor) {
const auto handlerType = "x-scheme-handler/"
+ descriptor.protocol.toStdString();
const auto neededCommandline = KShell::joinArgs(QStringList{
descriptor.executable,
} + KShell::splitArgs(descriptor.arguments) + QStringList{
"--",
"%u",
}).toStdString();
auto registeredAppInfos = Gio::AppInfo::get_recommended_for_type(
handlerType);
for (auto &appInfo : registeredAppInfos) {
if (appInfo.get_commandline() == neededCommandline
&& !std::string(appInfo.get_id()).compare(0, 8, "userapp-")) {
appInfo.delete_();
}
}
}
} // namespace base::Platform

View File

@@ -0,0 +1,9 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/platform/base_platform_url_scheme.h"

View File

@@ -0,0 +1,33 @@
<node>
<interface name='io.snapcraft.Settings'>
<method name='Check'>
<arg type='s' name='setting' direction='in'/>
<arg type='s' name='check' direction='in'/>
<arg type='s' name='result' direction='out'/>
</method>
<method name='CheckSub'>
<arg type='s' name='setting' direction='in'/>
<arg type='s' name='subproperty' direction='in'/>
<arg type='s' name='check' direction='in'/>
<arg type='s' name='result' direction='out'/>
</method>
<method name='Get'>
<arg type='s' name='setting' direction='in'/>
<arg type='s' name='result' direction='out'/>
</method>
<method name='GetSub'>
<arg type='s' name='setting' direction='in'/>
<arg type='s' name='subproperty' direction='in'/>
<arg type='s' name='result' direction='out'/>
</method>
<method name='Set'>
<arg type='s' name='setting' direction='in'/>
<arg type='s' name='value' direction='in'/>
</method>
<method name='SetSub'>
<arg type='s' name='setting' direction='in'/>
<arg type='s' name='subproperty' direction='in'/>
<arg type='s' name='value' direction='in'/>
</method>
</interface>
</node>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0"?>
<node>
<interface name='org.mpris.MediaPlayer2'>
<method name='Raise'/>
<method name='Quit'/>
<property name='CanQuit' type='b' access='read'/>
<property name='CanRaise' type='b' access='read'/>
<property name='HasTrackList' type='b' access='read'/>
<property name='Identity' type='s' access='read'/>
<property name='DesktopEntry' type='s' access='read'/>
<property name='SupportedUriSchemes' type='as' access='read'/>
<property name='SupportedMimeTypes' type='as' access='read'/>
<property name='Fullscreen' type='b' access='read'/>
<property name='CanSetFullscreen' type='b' access='read'/>
</interface>
<interface name='org.mpris.MediaPlayer2.Player'>
<method name='Next'/>
<method name='Previous'/>
<method name='Pause'/>
<method name='PlayPause'/>
<method name='Stop'/>
<method name='Play'/>
<method name='Seek'>
<arg direction='in' name='Offset' type='x'/>
</method>
<method name='SetPosition'>
<arg direction='in' name='TrackId' type='o'/>
<arg direction='in' name='Position' type='x'/>
</method>
<method name='OpenUri'>
<arg direction='in' name='Uri' type='s'/>
</method>
<signal name='Seeked'>
<arg name='Position' type='x'/>
</signal>
<property name='PlaybackStatus' type='s' access='read'/>
<property name='LoopStatus' type='s' access='readwrite'/>
<property name='Rate' type='d' access='read'/>
<property name='Shuffle' type='b' access='readwrite'/>
<property name='Metadata' type='a{sv}' access='read'>
<annotation
name="org.qtproject.QtDBus.QtTypeName"
value="QVariantMap"/>
</property>
<property name='Volume' type='d' access='readwrite'/>
<property name='Position' type='x' access='read'/>
<property name='MinimumRate' type='d' access='read'/>
<property name='MaximumRate' type='d' access='read'/>
<property name='CanGoNext' type='b' access='read'/>
<property name='CanGoPrevious' type='b' access='read'/>
<property name='CanPlay' type='b' access='read'/>
<property name='CanPause' type='b' access='read'/>
<property name='CanSeek' type='b' access='read'/>
<property name='CanControl' type='b' access='read'/>
</interface>
</node>

View File

@@ -0,0 +1,144 @@
<!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.DBus">
<method name="Hello">
<arg direction="out" type="s"/>
</method>
<method name="RequestName">
<arg direction="in" type="s"/>
<arg direction="in" type="u"/>
<arg direction="out" type="u"/>
</method>
<method name="ReleaseName">
<arg direction="in" type="s"/>
<arg direction="out" type="u"/>
</method>
<method name="StartServiceByName">
<arg direction="in" type="s"/>
<arg direction="in" type="u"/>
<arg direction="out" type="u"/>
</method>
<method name="UpdateActivationEnvironment">
<arg direction="in" type="a{ss}"/>
</method>
<method name="NameHasOwner">
<arg direction="in" type="s"/>
<arg direction="out" type="b"/>
</method>
<method name="ListNames">
<arg direction="out" type="as"/>
</method>
<method name="ListActivatableNames">
<arg direction="out" type="as"/>
</method>
<method name="AddMatch">
<arg direction="in" type="s"/>
</method>
<method name="RemoveMatch">
<arg direction="in" type="s"/>
</method>
<method name="GetNameOwner">
<arg direction="in" type="s"/>
<arg direction="out" type="s"/>
</method>
<method name="ListQueuedOwners">
<arg direction="in" type="s"/>
<arg direction="out" type="as"/>
</method>
<method name="GetConnectionUnixUser">
<arg direction="in" type="s"/>
<arg direction="out" type="u"/>
</method>
<method name="GetConnectionUnixProcessID">
<arg direction="in" type="s"/>
<arg direction="out" type="u"/>
</method>
<method name="GetAdtAuditSessionData">
<arg direction="in" type="s"/>
<arg direction="out" type="ay"/>
</method>
<method name="GetConnectionSELinuxSecurityContext">
<arg direction="in" type="s"/>
<arg direction="out" type="ay"/>
</method>
<method name="ReloadConfig">
</method>
<method name="GetId">
<arg direction="out" type="s"/>
</method>
<method name="GetConnectionCredentials">
<arg direction="in" type="s"/>
<arg direction="out" type="a{sv}"/>
</method>
<property name="Features" type="as" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
</property>
<property name="Interfaces" type="as" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
</property>
<signal name="NameOwnerChanged">
<arg type="s"/>
<arg type="s"/>
<arg type="s"/>
</signal>
<signal name="NameLost">
<arg type="s"/>
</signal>
<signal name="NameAcquired">
<arg type="s"/>
</signal>
</interface>
<interface name="org.freedesktop.DBus.Properties">
<method name="Get">
<arg direction="in" type="s"/>
<arg direction="in" type="s"/>
<arg direction="out" type="v"/>
</method>
<method name="GetAll">
<arg direction="in" type="s"/>
<arg direction="out" type="a{sv}"/>
</method>
<method name="Set">
<arg direction="in" type="s"/>
<arg direction="in" type="s"/>
<arg direction="in" type="v"/>
</method>
<signal name="PropertiesChanged">
<arg type="s" name="interface_name"/>
<arg type="a{sv}" name="changed_properties"/>
<arg type="as" name="invalidated_properties"/>
</signal>
</interface>
<interface name="org.freedesktop.DBus.Introspectable">
<method name="Introspect">
<arg direction="out" type="s"/>
</method>
</interface>
<interface name="org.freedesktop.DBus.Monitoring">
<method name="BecomeMonitor">
<arg direction="in" type="as"/>
<arg direction="in" type="u"/>
</method>
</interface>
<interface name="org.freedesktop.DBus.Peer">
<method name="GetMachineId">
<arg direction="out" type="s"/>
</method>
<method name="Ping">
</method>
</interface>
<interface name="org.freedesktop.DBus.Debug.Stats">
<method name="GetStats">
<arg direction="out" type="a{sv}"/>
</method>
<method name="GetConnectionStats">
<arg direction="in" type="s"/>
<arg direction="out" type="a{sv}"/>
</method>
<method name="GetAllMatchRules">
<arg direction="out" type="a{sas}"/>
</method>
</interface>
</node>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0"?>
<node>
<interface name='org.freedesktop.FileManager1'>
<method name='ShowFolders'>
<arg type='as' name='URIs' direction='in'/>
<arg type='s' name='StartupId' direction='in'/>
</method>
<method name='ShowItems'>
<arg type='as' name='URIs' direction='in'/>
<arg type='s' name='StartupId' direction='in'/>
</method>
<method name='ShowItemProperties'>
<arg type='as' name='URIs' direction='in'/>
<arg type='s' name='StartupId' direction='in'/>
</method>
</interface>
</node>

View File

@@ -0,0 +1,35 @@
<!DOCTYPE node PUBLIC
'-//freedesktop//DTD D-BUS Object Introspection 1.0//EN'
'http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd'>
<node>
<!--
org.gnome.Mutter.IdleMonitor:
@short_description: idle monitor interface
This interface is used by gnome-desktop to implement
user activity monitoring.
-->
<interface name="org.gnome.Mutter.IdleMonitor">
<method name="GetIdletime">
<arg name="idletime" direction="out" type="t"/>
</method>
<method name="AddIdleWatch">
<arg name="interval" direction="in" type="t" />
<arg name="id" direction="out" type="u" />
</method>
<method name="AddUserActiveWatch">
<arg name="id" direction="out" type="u" />
</method>
<method name="RemoveWatch">
<arg name="id" direction="in" type="u" />
</method>
<signal name="WatchFired">
<arg name="id" direction="out" type="u" />
</signal>
</interface>
</node>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" ?>
<node>
<!-- org.sigxcpu.Feedback.Haptic
@short_description: Interface to make a device vibrate
This D-Bus interface is used to make a device's haptic motor
vibrate. This is can be useful e.g. for games.
To provider user feedback the event based interface should be
preferred.
-->
<interface name="org.sigxcpu.Feedback.Haptic">
<!--
Vibrate:
@app_id: The application id usually in "reverse DNS" format
@pattern: The vibration pattern.
@success: Whether vibration was triggered
Triggers the given vibration pattern on the haptic device. The
pattern is a sequence of relative amplitude and duration pairs.
The amplitude must be between 0.0 and 1.0.
-->
<method name="Vibrate">
<arg direction="in" name="app_id" type="s"/>
<arg direction="in" name="pattern" type="a(du)"/>
<arg direction="out" name="success" type="b"/>
</method>
</interface>
</node>

View File

@@ -0,0 +1,8 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once

View File

@@ -0,0 +1,202 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_battery_saving_mac.h"
#include "base/battery_saving.h"
#include "base/integration.h"
#include <Cocoa/Cocoa.h>
#include <IOKit/ps/IOPSKeys.h>
#include <IOKit/ps/IOPowerSources.h>
@interface LowPowerModeObserver : NSObject {
}
- (id) initWithCallback:(Fn<void()>)callback;
- (void) powerStateChanged:(NSNotification*)aNotification;
@end // @interface LowPowerModeObserver
@implementation LowPowerModeObserver {
Fn<void()> _callback;
}
- (id) initWithCallback:(Fn<void()>)callback {
if (self = [super init]) {
_callback = std::move(callback);
}
return self;
}
- (void) powerStateChanged:(NSNotification*)aNotification {
_callback();
}
@end // @implementation LowPowerModeObserver
namespace base::Platform {
namespace {
class BatterySaving final : public AbstractBatterySaving {
public:
BatterySaving(Fn<void()> changedCallback);
~BatterySaving();
std::optional<bool> enabled() const override;
private:
static void RunLoopCallback(void *callback) {
const auto observer = static_cast<LowPowerModeObserver*>(callback);
[observer powerStateChanged:nil];
}
LowPowerModeObserver *_observer = nil;
CFRunLoopSourceRef _runLoopSource = nullptr;
};
struct BatteryState {
bool has = false;
bool draining = false;
bool low = false;
};
[[nodiscard]] BatteryState DetectBatteryState() {
CFTypeRef info = IOPSCopyPowerSourcesInfo();
if (!info) {
return {};
}
const auto infoGuard = gsl::finally([&] { CFRelease(info); });
CFArrayRef list = IOPSCopyPowerSourcesList(info);
if (!list) {
return {};
}
const auto listGuard = gsl::finally([&] { CFRelease(list); });
CFIndex count = CFArrayGetCount(list);
auto result = BatteryState();
for (CFIndex i = 0; i < count; ++i) {
const auto description = CFDictionaryRef(
IOPSGetPowerSourceDescription(info, CFArrayGetValueAtIndex(list, i)));
if (!description) {
continue;
}
const auto type = CFStringRef(CFDictionaryGetValue(description, CFSTR(kIOPSTransportTypeKey)));
if (!type) {
continue;
}
const auto isInternal = (CFStringCompare(type, CFSTR(kIOPSInternalType), 0) == kCFCompareEqualTo);
if (!isInternal) {
continue;
}
const auto isPresent = CFBooleanRef(CFDictionaryGetValue(description, CFSTR(kIOPSIsPresentKey)));
if (!isPresent || !CFBooleanGetValue(isPresent)) {
continue;
}
const auto state = CFStringRef(CFDictionaryGetValue(description, CFSTR(kIOPSPowerSourceStateKey)));
if (!state) {
continue;
}
const auto isDraining = (CFStringCompare(state, CFSTR(kIOPSBatteryPowerValue), 0) == kCFCompareEqualTo);
result.has = true;
result.draining = isDraining;
const auto lowWarnLevel = CFNumberRef(CFDictionaryGetValue(description, CFSTR(kIOPSLowWarnLevelKey)));
const auto nowCapacity = CFNumberRef(CFDictionaryGetValue(description, CFSTR(kIOPSCurrentCapacityKey)));
const auto maxCapacity = CFNumberRef(CFDictionaryGetValue(description, CFSTR(kIOPSMaxCapacityKey)));
if (!lowWarnLevel || !nowCapacity || !maxCapacity) {
continue;
}
auto lowWarnLevelValue = int64_t();
auto nowCapacityValue = int64_t();
auto maxCapacityValue = int64_t();
if (!CFNumberGetValue(lowWarnLevel, kCFNumberSInt64Type, &lowWarnLevelValue)
|| !CFNumberGetValue(nowCapacity, kCFNumberSInt64Type, &nowCapacityValue)
|| !CFNumberGetValue(maxCapacity, kCFNumberSInt64Type, &maxCapacityValue)
|| (lowWarnLevelValue <= 0 || lowWarnLevelValue >= 100)
|| (nowCapacityValue < 0 || nowCapacityValue > maxCapacityValue || !maxCapacityValue)) {
continue;
}
result.low = (nowCapacityValue / double(maxCapacityValue) <= lowWarnLevelValue / 100.);
}
return result;
}
BatterySaving::BatterySaving(Fn<void()> changedCallback) {
if (!DetectBatteryState().has) {
return;
}
auto wrapped = [copy = std::move(changedCallback)] {
Integration::Instance().enterFromEventLoop(copy);
};
_observer = [[LowPowerModeObserver alloc] initWithCallback:std::move(wrapped)];
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
if (@available(macOS 12.0, *)) {
[center
addObserver: _observer
selector: @selector(powerStateChanged:)
name: NSProcessInfoPowerStateDidChangeNotification
object: nil];
}
[center
addObserver: _observer
selector: @selector(powerStateChanged:)
name: NSProcessInfoThermalStateDidChangeNotification
object: nil];
_runLoopSource = IOPSNotificationCreateRunLoopSource(
RunLoopCallback,
static_cast<void*>(_observer));
CFRunLoopAddSource(
CFRunLoopGetCurrent(),
_runLoopSource,
kCFRunLoopDefaultMode);
}
BatterySaving::~BatterySaving() {
if (_runLoopSource) {
CFRunLoopRemoveSource(
CFRunLoopGetCurrent(),
_runLoopSource,
kCFRunLoopDefaultMode);
CFRelease(_runLoopSource);
}
if (_observer) {
[_observer release];
}
}
std::optional<bool> BatterySaving::enabled() const {
if (!_observer) {
return std::nullopt;
}
NSProcessInfo *info = [NSProcessInfo processInfo];
if (@available(macOS 12.0, *)) {
if ([info isLowPowerModeEnabled]) {
return true;
}
}
const auto state = DetectBatteryState();
if (!state.has || !state.draining) {
return false;
} else if ([info thermalState] == NSProcessInfoThermalStateCritical) {
return true;
}
return state.low;
}
} // namespace
std::unique_ptr<AbstractBatterySaving> CreateBatterySaving(
Fn<void()> changedCallback) {
return std::make_unique<BatterySaving>(std::move(changedCallback));
}
} // namespace base::Platform

View File

@@ -0,0 +1,14 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
namespace Platform::ConfirmQuit {
[[nodiscard]] bool RunModal(QString text);
[[nodiscard]] QString QuitKeysString();
} // namespace Platform::ConfirmQuit

View File

@@ -0,0 +1,462 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_confirm_quit.h"
#include "base/platform/mac/base_utilities_mac.h"
// Thanks Chromium: chrome/browser/ui/cocoa/confirm_quit*
#import <Cocoa/Cocoa.h>
#import <QuartzCore/QuartzCore.h>
namespace {
// How long the user must hold down Cmd+Q to confirm the quit.
constexpr auto kShowDuration = crl::time(1500);
// Duration of the window fade out animation.
constexpr auto kWindowFadeOutDuration = crl::time(200);
// For metrics recording only: How long the user must hold the keys to
// differentitate kDoubleTap from kTapHold.
constexpr auto kDoubleTapTimeDelta = crl::time(320);
// Leeway between the |targetDate| and the current time that will confirm a
// quit.
constexpr auto kTimeDeltaFuzzFactor = crl::time(1000);
} // namespace
@class ConfirmQuitFrameView;
// The ConfirmQuitPanelController manages the black HUD window that tells users
// to "Hold Cmd+Q to Quit".
@interface ConfirmQuitPanelController : NSWindowController<NSWindowDelegate> {
@private
// The content view of the window that this controller manages.
ConfirmQuitFrameView* _contentView; // Weak, owned by the window.
NSString *_message;
}
// Returns a singleton instance of the Controller. This will create one if it
// does not currently exist.
+ (ConfirmQuitPanelController*)sharedControllerWithMessage:(NSString*)message;
// Runs a modal loop that brings up the panel and handles the logic for if and
// when to terminate. Returns YES if the quit should continue.
- (BOOL)runModalLoopForApplication:(NSApplication*)app;
// Shows the window.
- (void)showWindow:(id)sender;
// If the user did not confirm quit, send this message to give the user
// instructions on how to quit.
- (void)dismissPanel;
@end
// The content view of the window that draws a custom frame.
@interface ConfirmQuitFrameView : NSView {
@private
NSTextField* _message; // Weak, owned by the view hierarchy.
}
- (void)setMessageText:(NSString*)text;
@end
@implementation ConfirmQuitFrameView
- (instancetype)initWithFrame:(NSRect)frameRect {
if ((self = [super initWithFrame:frameRect])) {
// The frame will be fixed up when |-setMessageText:| is called.
_message = [[NSTextField alloc] initWithFrame:NSZeroRect];
[_message setEditable:NO];
[_message setSelectable:NO];
[_message setBezeled:NO];
[_message setDrawsBackground:NO];
[_message setFont:[NSFont boldSystemFontOfSize:24]];
[_message setTextColor:[NSColor whiteColor]];
[self addSubview:_message];
[_message release];
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect {
const CGFloat kCornerRadius = 9.0;
NSBezierPath* path = [NSBezierPath
bezierPathWithRoundedRect:[self bounds]
xRadius:kCornerRadius
yRadius:kCornerRadius];
NSColor* fillColor = [NSColor colorWithCalibratedWhite:0.2 alpha:0.75];
[fillColor set];
[path fill];
}
- (void)setMessageText:(NSString*)text {
const CGFloat kHorizontalPadding = 30; // In view coordinates.
// Style the string.
NSMutableAttributedString *attrString
= [[NSMutableAttributedString alloc] initWithString:text];
NSShadow *textShadow = [[NSShadow alloc] init];
const auto guard = gsl::finally([&] {
[textShadow release];
[attrString release];
});
[textShadow
setShadowColor:[NSColor
colorWithCalibratedWhite:0
alpha:0.6]];
[textShadow setShadowOffset:NSMakeSize(0, -1)];
[textShadow setShadowBlurRadius:1.0];
[attrString addAttribute:NSShadowAttributeName
value:textShadow
range:NSMakeRange(0, [text length])];
[_message setAttributedStringValue:attrString];
// Fixup the frame of the string.
[_message sizeToFit];
NSRect messageFrame = [_message frame];
NSRect frameInViewSpace
= [_message convertRect:[[self window] frame] fromView:nil];
if (NSWidth(messageFrame) > NSWidth(frameInViewSpace)) {
frameInViewSpace.size.width = NSWidth(messageFrame) + kHorizontalPadding;
}
messageFrame.origin.x = NSWidth(frameInViewSpace) / 2 - NSMidX(messageFrame);
messageFrame.origin.y = NSHeight(frameInViewSpace) / 2 - NSMidY(messageFrame);
[[self window]
setFrame:[_message convertRect:frameInViewSpace toView:nil]
display:YES];
[_message setFrame:messageFrame];
}
@end
// Animation ///////////////////////////////////////////////////////////////////
// This animation will run through all the windows of the passed-in
// NSApplication and will fade their alpha value to 0.0. When the animation is
// complete, this will release itself.
@interface FadeAllWindowsAnimation : NSAnimation<NSAnimationDelegate> {
@private
NSApplication* _application;
}
- (instancetype)initWithApplication:(NSApplication*)app
animationDuration:(NSTimeInterval)duration;
@end
@implementation FadeAllWindowsAnimation
- (instancetype)initWithApplication:(NSApplication*)app
animationDuration:(NSTimeInterval)duration {
if ((self = [super initWithDuration:duration
animationCurve:NSAnimationLinear])) {
_application = app;
[self setDelegate:self];
}
return self;
}
- (void)setCurrentProgress:(NSAnimationProgress)progress {
for (NSWindow* window in [_application windows]) {
[window setAlphaValue:1.0 - progress];
}
}
- (void)animationDidStop:(NSAnimation*)anim {
[self autorelease];
}
@end
// Private Interface ///////////////////////////////////////////////////////////
@interface ConfirmQuitPanelController (Private) <CAAnimationDelegate>
- (void)animateFadeOut;
- (NSEvent*)pumpEventQueueForKeyUp:(NSApplication*)app untilDate:(NSDate*)date;
- (void)hideAllWindowsForApplication:(NSApplication*)app
withDuration:(NSTimeInterval)duration;
- (void)sendAccessibilityAnnouncement;
@end
ConfirmQuitPanelController* g_confirmQuitPanelController = nil;
////////////////////////////////////////////////////////////////////////////////
@implementation ConfirmQuitPanelController
+ (ConfirmQuitPanelController*)sharedControllerWithMessage:(NSString*)message {
if (!g_confirmQuitPanelController) {
g_confirmQuitPanelController =
[[ConfirmQuitPanelController alloc] initWithMessage:message];
}
return [[g_confirmQuitPanelController retain] autorelease];
}
- (instancetype)initWithMessage:(NSString*)message {
const NSRect kWindowFrame = NSMakeRect(0, 0, 350, 70);
NSWindow *window
= [[NSWindow alloc]
initWithContentRect:kWindowFrame
styleMask:NSWindowStyleMaskBorderless
backing:NSBackingStoreBuffered
defer:NO];
const auto guard = gsl::finally([&] { [window release]; });
if ((self = [super initWithWindow:window])) {
[window setDelegate:self];
[window setBackgroundColor:[NSColor clearColor]];
[window setOpaque:NO];
[window setHasShadow:NO];
// Create the content view. Take the frame from the existing content view.
NSRect frame = [[window contentView] frame];
_contentView = [[ConfirmQuitFrameView alloc] initWithFrame:frame];
[window setContentView:_contentView];
// Set the proper string.
_message = [message retain];
[_contentView setMessageText:_message];
[_contentView release];
}
return self;
}
- (BOOL)runModalLoopForApplication:(NSApplication*)app {
ConfirmQuitPanelController *keepAlive = [self retain];
const auto guard = gsl::finally([&] { [keepAlive release]; });
// If this is the second of two such attempts to quit within a certain time
// interval, then just quit.
// Time of last quit attempt, if any.
static auto lastQuitAttempt = crl::time();
const auto timeNow = crl::now();
if (lastQuitAttempt && (timeNow - lastQuitAttempt) < kTimeDeltaFuzzFactor) {
// The panel tells users to Hold Cmd+Q. However, we also want to have a
// double-tap shortcut that allows for a quick quit path. For the users who
// tap Cmd+Q and then hold it with the window still open, this double-tap
// logic will run and cause the quit to get committed. If the key
// combination held down, the system will start sending the Cmd+Q event to
// the next key application, and so on. This is bad, so instead we hide all
// the windows (without animation) to look like we've "quit" and then wait
// for the KeyUp event to commit the quit.
[self hideAllWindowsForApplication:app withDuration:0];
NSEvent* nextEvent = [self
pumpEventQueueForKeyUp:app
untilDate:[NSDate distantFuture]];
[app discardEventsMatchingMask:NSEventMaskAny beforeEvent:nextEvent];
return YES;
} else {
lastQuitAttempt = timeNow;
}
// Show the info panel that explains what the user must to do confirm quit.
[self showWindow:self];
// Explicitly announce the hold-to-quit message. For an ordinary modal dialog
// VoiceOver would announce it and read its message, but VoiceOver does not do
// this for windows whose styleMask is NSWindowStyleMaskBorderless, so do it
// manually here. Without this screenreader users have no way to know why
// their quit hotkey seems not to work.
[self sendAccessibilityAnnouncement];
// Spin a nested run loop until the |targetDate| is reached or a KeyUp event
// is sent.
const auto targetDate = crl::now() + kShowDuration;
BOOL willQuit = NO;
NSEvent* nextEvent = nil;
do {
// Dequeue events until a key up is received. To avoid busy waiting, figure
// out the amount of time that the thread can sleep before taking further
// action.
NSDate* waitDate = [NSDate
dateWithTimeIntervalSinceNow:(kShowDuration - kTimeDeltaFuzzFactor) / 1000.];
nextEvent = [self pumpEventQueueForKeyUp:app untilDate:waitDate];
// Wait for the time expiry to happen. Once past the hold threshold,
// commit to quitting and hide all the open windows.
if (!willQuit) {
const auto now = crl::now();
const auto difference = (targetDate - now);
if (difference < kTimeDeltaFuzzFactor) {
willQuit = YES;
// At this point, the quit has been confirmed and windows should all
// fade out to convince the user to release the key combo to finalize
// the quit.
[self
hideAllWindowsForApplication:app
withDuration:kWindowFadeOutDuration / 1000.];
}
}
} while (!nextEvent);
// The user has released the key combo. Discard any events (i.e. the
// repeated KeyDown Cmd+Q).
[app discardEventsMatchingMask:NSEventMaskAny beforeEvent:nextEvent];
if (willQuit) {
// The user held down the combination long enough that quitting should
// happen.
return YES;
} else {
// Slowly fade the confirm window out in case the user doesn't
// understand what they have to do to quit.
[self dismissPanel];
return NO;
}
// Default case: terminate.
return YES;
}
- (void)windowWillClose:(NSNotification*)notif {
// Release all animations because CAAnimation retains its delegate (self),
// which will cause a retain cycle. Break it!
[[self window] setAnimations:@{}];
g_confirmQuitPanelController = nil;
[self autorelease];
}
- (void)showWindow:(id)sender {
// If a panel that is fading out is going to be reused here, make sure it
// does not get released when the animation finishes.
ConfirmQuitPanelController *keepAlive = [self retain];
const auto guard = gsl::finally([&] { [keepAlive release]; });
[[self window] setAnimations:@{}];
[[self window] center];
[[self window] setAlphaValue:1.0];
[super showWindow:sender];
}
- (void)dismissPanel {
[self
performSelector:@selector(animateFadeOut)
withObject:nil
afterDelay:1.0];
}
- (void)animateFadeOut {
NSWindow* window = [self window];
CAAnimation *animation
= [[window animationForKey:@"alphaValue"] copy];
const auto guard = gsl::finally([&] { [animation release]; });
[animation setDelegate:self];
[animation setDuration:0.2];
NSMutableDictionary* dictionary
= [NSMutableDictionary dictionaryWithDictionary:[window animations]];
dictionary[@"alphaValue"] = animation;
[window setAnimations:dictionary];
[[window animator] setAlphaValue:0.0];
}
- (void)animationDidStart:(CAAnimation*)theAnimation {
// CAAnimationDelegate method added on OSX 10.12.
}
- (void)animationDidStop:(CAAnimation*)theAnimation finished:(BOOL)finished {
[self close];
}
// Runs a nested loop that pumps the event queue until the next KeyUp event.
- (NSEvent*)pumpEventQueueForKeyUp:(NSApplication*)app untilDate:(NSDate*)date {
return [app
nextEventMatchingMask:NSEventMaskKeyUp
untilDate:date
inMode:NSEventTrackingRunLoopMode
dequeue:YES];
}
// Iterates through the list of open windows and hides them all.
- (void)hideAllWindowsForApplication:(NSApplication*)app
withDuration:(NSTimeInterval)duration {
FadeAllWindowsAnimation* animation =
[[FadeAllWindowsAnimation alloc] initWithApplication:app
animationDuration:duration];
// Releases itself when the animation stops.
[animation startAnimation];
}
- (void)sendAccessibilityAnnouncement {
NSAccessibilityPostNotificationWithUserInfo(
[NSApp mainWindow], NSAccessibilityAnnouncementRequestedNotification, @{
NSAccessibilityAnnouncementKey : _message,
NSAccessibilityPriorityKey : @(NSAccessibilityPriorityHigh),
});
}
@end
namespace Platform::ConfirmQuit {
namespace {
// This returns the NSMenuItem that quits the application.
[[nodiscard]] NSMenuItem *QuitMenuItem() {
NSMenu* mainMenu = [NSApp mainMenu];
// Get the application menu (i.e. Chromium).
NSMenu* appMenu = [[mainMenu itemAtIndex:0] submenu];
for (NSMenuItem* item in [appMenu itemArray]) {
// Find the Quit item.
if ([item action] == @selector(terminate:)) {
return item;
}
}
// Default to Cmd+Q.
NSMenuItem* item = [[[NSMenuItem alloc]
initWithTitle:@""
action:@selector(terminate:)
keyEquivalent:@"q"] autorelease];
item.keyEquivalentModifierMask = NSEventModifierFlagCommand;
return item;
}
[[nodiscard]] QString KeyCombinationForMenuItem(NSMenuItem *item) {
auto result = QString();
NSUInteger modifiers = item.keyEquivalentModifierMask;
if (modifiers & NSEventModifierFlagCommand) {
result.append(QChar(0x2318));
}
if (modifiers & NSEventModifierFlagControl) {
result.append(QChar(0x2303));
}
if (modifiers & NSEventModifierFlagOption) {
result.append(QChar(0x2325));
}
if (modifiers & NSEventModifierFlagShift) {
result.append(QChar(0x21E7));
}
result.append(NS2QString([item.keyEquivalent uppercaseString]));
return result;
}
// This looks at the Main Menu and determines what the user has set as the
// key combination for quit. It then gets the modifiers and builds a string
// to display them.
[[nodiscard]] QString KeyCommandString() {
return KeyCombinationForMenuItem(QuitMenuItem());
}
} // namespace
bool RunModal(QString text) {
return [[ConfirmQuitPanelController sharedControllerWithMessage:Q2NSString(text)]
runModalLoopForApplication:NSApp];
}
QString QuitKeysString() {
return KeyCommandString();
}
} // namespace Platform::ConfirmQuit

View File

@@ -0,0 +1,9 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/platform/base_platform_custom_app_icon.h"

View File

@@ -0,0 +1,387 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_custom_app_icon_mac.h"
#include "base/debug_log.h"
#include "base/platform/mac/base_utilities_mac.h"
#include <QtGui/QImage>
#include <QtCore/QDir>
#include <QtCore/QFile>
#include <QtCore/QTemporaryFile>
#include <sys/xattr.h>
#include <xxhash.h>
namespace base::Platform {
namespace {
using namespace ::Platform;
constexpr auto kFinderInfo = "com.apple.FinderInfo";
constexpr auto kResourceFork = "com.apple.ResourceFork";
// We want to write [8], so just in case.
constexpr auto kFinderInfoMinSize = 16;
// Usually.
constexpr auto kFinderInfoSize = 32;
// Just in case.
constexpr auto kFinderInfoMaxSize = 256;
// Limit custom icons to 10 MB.
constexpr auto kResourceForkMaxSize = 10 * 1024 * 1024;
[[nodiscard]] QString BundlePath() {
@autoreleasepool {
NSString *path = @"";
@try {
path = [[NSBundle mainBundle] bundlePath];
if (!path) {
Unexpected("Could not get bundled path!");
}
return QFile::decodeName([path fileSystemRepresentation]);
}
@catch (NSException *exception) {
Unexpected("Exception in resource registering.");
}
}
}
int Launch(const QString &command, const QStringList &arguments) {
@autoreleasepool {
@try {
NSMutableArray *list = [NSMutableArray arrayWithCapacity:arguments.size()];
for (const auto &argument : arguments) {
[list addObject:Q2NSString(argument)];
}
NSTask *task = [[NSTask alloc] init];
task.launchPath = Q2NSString(command);
task.arguments = list;
[task launch];
[task waitUntilExit];
return [task terminationStatus];
}
@catch (NSException *exception) {
return -888;
}
@finally {
}
}
}
[[nodiscard]] std::optional<std::string> ReadCustomIconAttribute(const QString &bundle) {
const auto native = QFile::encodeName(bundle);
auto info = std::array<char, kFinderInfoMaxSize>();
const auto result = getxattr(
native.data(),
kFinderInfo,
info.data(),
info.size(),
0, // position
XATTR_NOFOLLOW);
const auto error = (result < 0) ? errno : 0;
if (result < 0) {
if (error == ENOATTR) {
return std::string();
} else {
LOG(("Icon Error: Could not get %1 xattr, error: %2."
).arg(kFinderInfo
).arg(error));
return std::nullopt;
}
} else if (result < kFinderInfoMinSize) {
LOG(("Icon Error: Bad existing %1 xattr size: %2."
).arg(kFinderInfo
).arg(error));
return std::nullopt;
}
return std::string(info.data(), result);
}
[[nodiscard]] bool WriteCustomIconAttribute(
const QString &bundle,
const std::string &value) {
const auto native = QFile::encodeName(bundle);
const auto result = setxattr(
native.data(),
kFinderInfo,
value.data(),
value.size(),
0, // position
XATTR_NOFOLLOW);
if (result != 0) {
LOG(("Icon Error: Could not set %1 xattr, error: %2."
).arg(kFinderInfo
).arg(errno));
return false;
}
return true;
}
[[nodiscard]] bool DeleteCustomIconAttribute(const QString &bundle) {
const auto native = QFile::encodeName(bundle);
const auto result = removexattr(
native.data(),
kFinderInfo,
XATTR_NOFOLLOW);
if (result != 0) {
LOG(("Icon Error: Could not remove %1 xattr, error: %2."
).arg(kFinderInfo
).arg(errno));
return false;
}
return true;
}
[[nodiscard]] bool EnableCustomIcon(const QString &bundle) {
auto info = ReadCustomIconAttribute(bundle);
if (!info.has_value()) {
return false;
} else if (info->empty()) {
*info = std::string(kFinderInfoSize, char(0));
}
if ((*info)[8] & 0x04) {
(*info)[8] &= ~0x04;
if (!WriteCustomIconAttribute(bundle, *info)) {
return false;
}
}
(*info)[8] |= 4;
return WriteCustomIconAttribute(bundle, *info);
}
[[nodiscard]] bool DisableCustomIcon(const QString &bundle) {
auto info = ReadCustomIconAttribute(bundle);
if (!info.has_value()) {
return false;
} else if (info->empty()) {
return true;
}
return DeleteCustomIconAttribute(bundle);
}
[[nodiscard]] bool RefreshDock() {
Launch("/bin/bash", { "-c", "rm /var/folders/*/*/*/com.apple.dock.iconcache" });
const auto killall = Launch("/usr/bin/killall", { "Dock" });
if (killall != 0) {
LOG(("Icon Error: Failed to run `killall Dock`, result: %1.").arg(killall));
return false;
}
return true;
}
[[nodiscard]] QString TempPath(const QString &extension) {
auto file = QTemporaryFile(
QDir::tempPath() + "/custom_icon_XXXXXX." + extension);
file.setAutoRemove(false);
const auto result = file.open() ? file.fileName() : QString();
if (result.isEmpty()) {
LOG(("Icon Error: Could not obtain a temporary file name."));
}
return result;
}
[[nodiscard]] std::optional<std::string> ReadResourceFork(
const QString &path) {
const auto native = QFile::encodeName(path);
auto buffer = std::string(kResourceForkMaxSize + 1, char(0));
const auto result = getxattr(
native.data(),
kResourceFork,
buffer.data(),
buffer.size(),
0, // position
XATTR_NOFOLLOW);
const auto error = (result < 0) ? errno : 0;
if (result < 0) {
if (error == ENOATTR) {
return std::string();
} else {
LOG(("Icon Error: Could not get %1 xattr, error: %2."
).arg(kResourceFork
).arg(error));
return std::nullopt;
}
} else if (result > kResourceForkMaxSize) {
LOG(("Icon Error: Got too large %1 xattr, size: %2."
).arg(kResourceFork
).arg(result));
return std::nullopt;
}
buffer.resize(result);
return buffer;
}
[[nodiscard]] bool WriteResourceFork(
const QString &path,
const std::string &data) {
const auto native = QFile::encodeName(path);
const auto result = setxattr(
native.data(),
kResourceFork,
data.data(),
data.size(),
0, // position
XATTR_NOFOLLOW);
if (result != 0) {
LOG(("Icon Error: Could not set %1 xattr, error: %2."
).arg(kResourceFork
).arg(errno));
return false;
}
return true;
}
[[nodiscard]] uint64 Digest(const std::string &data) {
return XXH64(data.data(), data.size(), 0);
}
[[nodiscard]] std::optional<uint64> SetPreparedIcon(const QString &path) {
const auto sips = Launch("/usr/bin/sips", {
"-i",
path
});
if (sips != 0) {
LOG(("Icon Error: Failed to run `sips -i \"%1\"`, result: %2."
).arg(path
).arg(sips));
return std::nullopt;
}
const auto bundle = BundlePath();
const auto icon = bundle + "/Icon\r";
const auto touch = Launch("/usr/bin/touch", { icon });
if (touch != 0) {
LOG(("Icon Error: Failed to run `touch \"%1\"`, result: %2."
).arg(icon
).arg(touch));
return std::nullopt;
}
#if 0 // Faster, but without a digest.
const auto from = path + "/..namedfork/rsrc";
const auto to = icon + "/..namedfork/rsrc";
const auto cp = Launch("/bin/cp", { from, to });
if (cp != 0) {
LOG(("Icon Error: Failed to run `cp \"%1\" \"%2\"`, result: %3."
).arg(from
).arg(to
).arg(cp));
return false;
}
#endif
auto rsrc = ReadResourceFork(path);
if (!rsrc) {
return false;
} else if (rsrc->empty()) {
LOG(("Icon Error: Empty resource fork after sips in \"%1\".").arg(path));
return false;
} else if (!WriteResourceFork(icon, *rsrc) || !EnableCustomIcon(bundle)) {
return std::nullopt;
}
return RefreshDock()
? std::make_optional(Digest(*rsrc))
: std::nullopt;
}
} // namespace
std::optional<uint64> SetCustomAppIcon(QImage image) {
if (image.isNull()) {
LOG(("Icon Error: Null image received."));
return std::nullopt;
}
if (image.format() != QImage::Format_ARGB32_Premultiplied
&& image.format() != QImage::Format_ARGB32
&& image.format() != QImage::Format_RGB32) {
image = std::move(image).convertToFormat(QImage::Format_ARGB32);
if (image.isNull()) {
LOG(("Icon Error: Failed to convert image to ARGB32."));
return std::nullopt;
}
}
const auto temp = TempPath("icns");
if (temp.isEmpty()) {
return std::nullopt;
}
const auto guard = gsl::finally([&] { QFile::remove(temp); });
if (!image.save(temp, "PNG")) {
LOG(("Icon Error: Failed to save image to \"%1\".").arg(temp));
return std::nullopt;
}
return SetPreparedIcon(temp);
}
std::optional<uint64> SetCustomAppIcon(const QString &path) {
const auto icns = path.endsWith(".icns", Qt::CaseInsensitive);
if (!icns) {
auto image = QImage(path);
if (image.isNull()) {
LOG(("Icon Error: Failed to read image from \"%1\".").arg(path));
return std::nullopt;
}
return SetCustomAppIcon(std::move(image));
}
const auto temp = TempPath("icns");
if (temp.isEmpty()) {
return std::nullopt;
}
const auto guard = gsl::finally([&] { QFile::remove(temp); });
QFile::remove(temp);
if (!QFile(path).copy(temp)) {
LOG(("Icon Error: Failed to copy icon from \"%1\" to \"%2\"."
).arg(path
).arg(temp));
return std::nullopt;
}
return SetPreparedIcon(temp);
}
std::optional<uint64> CurrentCustomAppIconDigest() {
const auto bundle = BundlePath();
const auto icon = bundle + "/Icon\r";
const auto attr = ReadCustomIconAttribute(bundle);
if (!attr) {
return std::nullopt;
} else if (attr->empty()) {
return 0;
}
const auto value = ReadResourceFork(icon);
if (!value) {
return std::nullopt;
} else if (value->empty()) {
return 0;
}
return Digest(*value);
}
bool ClearCustomAppIcon() {
const auto bundle = BundlePath();
const auto icon = bundle + "/Icon\r";
Launch("/bin/rm", { icon });
auto info = ReadCustomIconAttribute(bundle);
if (!info.has_value()) {
return false;
} else if (info->empty()) {
return true;
} else if (!DeleteCustomIconAttribute(bundle)) {
return false;
}
return RefreshDock();
}
} // namespace base::Platform

View File

@@ -0,0 +1,7 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once

View File

@@ -0,0 +1,93 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_file_utilities_mac.h"
#include "base/platform/mac/base_utilities_mac.h"
#include <QtCore/QFileInfo>
#include <sys/xattr.h>
#include <stdio.h>
#include <unistd.h>
namespace base::Platform {
using namespace ::Platform;
void ShowInFolder(const QString &filepath) {
const auto folder = QFileInfo(filepath).absolutePath();
@autoreleasepool {
[[NSWorkspace sharedWorkspace] selectFile:Q2NSString(filepath) inFileViewerRootedAtPath:Q2NSString(folder)];
}
}
void RemoveQuarantine(const QString &path) {
const auto kQuarantineAttribute = "com.apple.quarantine";
const auto local = QFile::encodeName(path);
removexattr(local.data(), kQuarantineAttribute, 0);
}
QString BundledResourcesPath() {
@autoreleasepool {
NSString *path = @"";
@try {
path = [[NSBundle mainBundle] bundlePath];
if (!path) {
Unexpected("Could not get bundled path!");
}
path = [path stringByAppendingString:@"/Contents/Resources"];
return QFile::decodeName([path fileSystemRepresentation]);
}
@catch (NSException *exception) {
Unexpected("Exception in resource registering.");
}
}
}
QString FileNameFromUserString(QString name) {
return name;
}
bool DeleteDirectory(QString path) {
if (path.endsWith('/')) {
path.chop(1);
}
BOOL result = NO;
@autoreleasepool {
result = [[NSFileManager defaultManager] removeItemAtPath:Q2NSString(path) error:nil];
}
return (result != NO);
}
QString CurrentExecutablePath(int argc, char *argv[]) {
return NS2QString([[NSBundle mainBundle] bundlePath]);
}
bool RenameWithOverwrite(const QString &from, const QString &to) {
const auto fromPath = QFile::encodeName(from);
const auto toPath = QFile::encodeName(to);
return (rename(fromPath.constData(), toPath.constData()) == 0);
}
void FlushFileData(QFile &file) {
file.flush();
if (const auto descriptor = file.handle()) {
fsync(descriptor);
}
}
} // namespace base::Platform

View File

@@ -0,0 +1,9 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/platform/base_platform_global_shortcuts.h"

View File

@@ -0,0 +1,345 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_global_shortcuts_mac.h"
#include "base/platform/mac/base_utilities_mac.h"
#include "base/invoke_queued.h"
#include "base/const_string.h"
#include <QtGui/QtEvents>
#include <Carbon/Carbon.h>
#import <Foundation/Foundation.h>
#import <IOKit/hidsystem/IOHIDLib.h>
namespace base::Platform::GlobalShortcuts {
namespace {
constexpr auto kShiftMouseButton = std::numeric_limits<uint64>::max() - 100;
CFMachPortRef EventPort = nullptr;
CFRunLoopSourceRef EventPortSource = nullptr;
CFRunLoopRef ThreadRunLoop = nullptr;
std::thread Thread;
Fn<void(GlobalShortcutKeyGeneric descriptor, bool down)> ProcessCallback;
struct EventData {
GlobalShortcutKeyGeneric descriptor = 0;
bool down = false;
};
using MaybeEventData = std::optional<EventData>;
MaybeEventData ProcessKeyEvent(CGEventType type, CGEventRef event) {
if (CGEventGetIntegerValueField(event, kCGKeyboardEventAutorepeat)) {
return std::nullopt;
}
const auto keycode = CGEventGetIntegerValueField(
event,
kCGKeyboardEventKeycode);
if (keycode == 0xB3) {
// Some KeyDown+KeyUp sent when quickly pressing and releasing Fn.
return std::nullopt;
}
const auto flags = CGEventGetFlags(event);
const auto maybeDown = [&]() -> std::optional<bool> {
if (type == kCGEventKeyDown) {
return true;
} else if (type == kCGEventKeyUp) {
return false;
} else if (type != kCGEventFlagsChanged) {
return std::nullopt;
}
switch (keycode) {
case kVK_CapsLock:
return (flags & kCGEventFlagMaskAlphaShift) != 0;
case kVK_Shift:
case kVK_RightShift:
return (flags & kCGEventFlagMaskShift) != 0;
case kVK_Control:
case kVK_RightControl:
return (flags & kCGEventFlagMaskControl) != 0;
case kVK_Option:
case kVK_RightOption:
return (flags & kCGEventFlagMaskAlternate) != 0;
case kVK_Command:
case kVK_RightCommand:
return (flags & kCGEventFlagMaskCommand) != 0;
case kVK_Function:
return (flags & kCGEventFlagMaskSecondaryFn) != 0;
default:
return std::nullopt;
}
}();
if (!maybeDown) {
return std::nullopt;
}
const auto descriptor = GlobalShortcutKeyGeneric(keycode);
const auto down = *maybeDown;
return EventData{ descriptor, down };
}
MaybeEventData ProcessMouseEvent(CGEventType type, CGEventRef event) {
const auto button = CGEventGetIntegerValueField(
event,
kCGMouseEventButtonNumber);
if (!button) {
return std::nullopt;
}
const auto code = GlobalShortcutKeyGeneric(kShiftMouseButton
+ button
// Increase the value by 1, because the right button = 1.
+ 1);
const auto down = (type == kCGEventOtherMouseDown)
|| (type == kCGEventRightMouseDown);
return EventData{ code, down };
}
CGEventRef EventTapCallback(
CGEventTapProxy,
CGEventType type,
CGEventRef event,
void*) {
const auto isKey = (type == kCGEventKeyDown)
|| (type == kCGEventKeyUp)
|| (type == kCGEventFlagsChanged);
const auto maybeData = isKey
? ProcessKeyEvent(type, event)
: ProcessMouseEvent(type, event);
if (maybeData) {
ProcessCallback(maybeData->descriptor, maybeData->down);
}
return event;
}
} // namespace
bool Available() {
return true;
}
bool Allowed() {
if (@available(macOS 10.15, *)) {
// Input Monitoring is required on macOS 10.15 an later.
// Even if user grants access, restart is required.
static const auto result = IOHIDCheckAccess(
kIOHIDRequestTypeListenEvent);
return (result == kIOHIDAccessTypeGranted);
} else if (@available(macOS 10.14, *)) {
// Accessibility is required on macOS 10.14.
NSDictionary *const options=
@{(__bridge NSString *)kAXTrustedCheckOptionPrompt: @FALSE};
return AXIsProcessTrustedWithOptions(
(__bridge CFDictionaryRef)options);
}
return true;
}
void Start(Fn<void(GlobalShortcutKeyGeneric descriptor, bool down)> process) {
Expects(!EventPort);
Expects(!EventPortSource);
ProcessCallback = std::move(process);
EventPort = CGEventTapCreate(
kCGHIDEventTap,
kCGHeadInsertEventTap,
kCGEventTapOptionListenOnly,
(CGEventMaskBit(kCGEventKeyDown)
| CGEventMaskBit(kCGEventKeyUp)
| CGEventMaskBit(kCGEventOtherMouseDown)
| CGEventMaskBit(kCGEventOtherMouseUp)
| CGEventMaskBit(kCGEventRightMouseDown)
| CGEventMaskBit(kCGEventRightMouseUp)
| CGEventMaskBit(kCGEventFlagsChanged)),
EventTapCallback,
nullptr);
if (!EventPort) {
ProcessCallback = nullptr;
return;
}
EventPortSource = CFMachPortCreateRunLoopSource(
kCFAllocatorDefault,
EventPort,
0);
if (!EventPortSource) {
CFMachPortInvalidate(EventPort);
CFRelease(EventPort);
EventPort = nullptr;
ProcessCallback = nullptr;
return;
}
Thread = std::thread([] {
ThreadRunLoop = CFRunLoopGetCurrent();
CFRunLoopAddSource(
ThreadRunLoop,
EventPortSource,
kCFRunLoopCommonModes);
CGEventTapEnable(EventPort, true);
CFRunLoopRun();
});
}
void Stop() {
if (!EventPort) {
return;
}
CFRunLoopStop(ThreadRunLoop);
Thread.join();
CFMachPortInvalidate(EventPort);
CFRelease(EventPort);
EventPort = nullptr;
CFRelease(EventPortSource);
EventPortSource = nullptr;
ProcessCallback = nullptr;
}
QString KeyName(GlobalShortcutKeyGeneric descriptor) {
static const auto KeyToString = flat_map<uint64, const_string>{
{ kVK_Return, "\xE2\x8F\x8E" },
{ kVK_Tab, "\xE2\x87\xA5" },
{ kVK_Space, "\xE2\x90\xA3" },
{ kVK_Delete, "\xE2\x8C\xAB" },
{ kVK_Escape, "\xE2\x8E\x8B" },
{ kVK_Command, "\xE2\x8C\x98" },
{ kVK_Shift, "\xE2\x87\xA7" },
{ kVK_CapsLock, "Caps Lock" },
{ kVK_Option, "\xE2\x8C\xA5" },
{ kVK_Control, "\xE2\x8C\x83" },
{ kVK_RightCommand, "Right \xE2\x8C\x98" },
{ kVK_RightShift, "Right \xE2\x87\xA7" },
{ kVK_RightOption, "Right \xE2\x8C\xA5" },
{ kVK_RightControl, "Right \xE2\x8C\x83" },
{ kVK_Function, "Fn" },
{ kVK_F17, "F17" },
{ kVK_VolumeUp, "Volume Up" },
{ kVK_VolumeDown, "Volume Down" },
{ kVK_Mute, "Mute" },
{ kVK_F18, "F18" },
{ kVK_F19, "F19" },
{ kVK_F20, "F20" },
{ kVK_F5, "F5" },
{ kVK_F6, "F6" },
{ kVK_F7, "F7" },
{ kVK_F3, "F3" },
{ kVK_F8, "F8" },
{ kVK_F9, "F9" },
{ kVK_F11, "F11" },
{ kVK_F13, "F13" },
{ kVK_F16, "F16" },
{ kVK_F14, "F14" },
{ kVK_F10, "F10" },
{ kVK_F12, "F12" },
{ kVK_F15, "F15" },
{ kVK_Help, "Help" },
{ kVK_Home, "\xE2\x86\x96" },
{ kVK_PageUp, "Page Up" },
{ kVK_ForwardDelete, "\xe2\x8c\xa6" },
{ kVK_F4, "F4" },
{ kVK_End, "\xE2\x86\x98" },
{ kVK_F2, "F2" },
{ kVK_PageDown, "Page Down" },
{ kVK_F1, "F1" },
{ kVK_LeftArrow, "\xE2\x86\x90" },
{ kVK_RightArrow, "\xE2\x86\x92" },
{ kVK_DownArrow, "\xE2\x86\x93" },
{ kVK_UpArrow, "\xE2\x86\x91" },
{ kVK_ANSI_A, "A" },
{ kVK_ANSI_S, "S" },
{ kVK_ANSI_D, "D" },
{ kVK_ANSI_F, "F" },
{ kVK_ANSI_H, "H" },
{ kVK_ANSI_G, "G" },
{ kVK_ANSI_Z, "Z" },
{ kVK_ANSI_X, "X" },
{ kVK_ANSI_C, "C" },
{ kVK_ANSI_V, "V" },
{ kVK_ANSI_B, "B" },
{ kVK_ANSI_Q, "Q" },
{ kVK_ANSI_W, "W" },
{ kVK_ANSI_E, "E" },
{ kVK_ANSI_R, "R" },
{ kVK_ANSI_Y, "Y" },
{ kVK_ANSI_T, "T" },
{ kVK_ANSI_1, "1" },
{ kVK_ANSI_2, "2" },
{ kVK_ANSI_3, "3" },
{ kVK_ANSI_4, "4" },
{ kVK_ANSI_6, "6" },
{ kVK_ANSI_5, "5" },
{ kVK_ANSI_Equal, "=" },
{ kVK_ANSI_9, "9" },
{ kVK_ANSI_7, "7" },
{ kVK_ANSI_Minus, "-" },
{ kVK_ANSI_8, "8" },
{ kVK_ANSI_0, "0" },
{ kVK_ANSI_RightBracket, "]" },
{ kVK_ANSI_O, "O" },
{ kVK_ANSI_U, "U" },
{ kVK_ANSI_LeftBracket, "[" },
{ kVK_ANSI_I, "I" },
{ kVK_ANSI_P, "P" },
{ kVK_ANSI_L, "L" },
{ kVK_ANSI_J, "J" },
{ kVK_ANSI_Quote, "'" },
{ kVK_ANSI_K, "K" },
{ kVK_ANSI_Semicolon, "/" },
{ kVK_ANSI_Backslash, "\\" },
{ kVK_ANSI_Comma, "," },
{ kVK_ANSI_Slash, "/" },
{ kVK_ANSI_N, "N" },
{ kVK_ANSI_M, "M" },
{ kVK_ANSI_Period, "." },
{ kVK_ANSI_Grave, "`" },
{ kVK_ANSI_KeypadDecimal, "Num ." },
{ kVK_ANSI_KeypadMultiply, "Num *" },
{ kVK_ANSI_KeypadPlus, "Num +" },
{ kVK_ANSI_KeypadClear, "Num Clear" },
{ kVK_ANSI_KeypadDivide, "Num /" },
{ kVK_ANSI_KeypadEnter, "Num Enter" },
{ kVK_ANSI_KeypadMinus, "Num -" },
{ kVK_ANSI_KeypadEquals, "Num =" },
{ kVK_ANSI_Keypad0, "Num 0" },
{ kVK_ANSI_Keypad1, "Num 1" },
{ kVK_ANSI_Keypad2, "Num 2" },
{ kVK_ANSI_Keypad3, "Num 3" },
{ kVK_ANSI_Keypad4, "Num 4" },
{ kVK_ANSI_Keypad5, "Num 5" },
{ kVK_ANSI_Keypad6, "Num 6" },
{ kVK_ANSI_Keypad7, "Num 7" },
{ kVK_ANSI_Keypad8, "Num 8" },
{ kVK_ANSI_Keypad9, "Num 9" },
};
if (descriptor > kShiftMouseButton) {
return QString("Mouse %1").arg(descriptor - kShiftMouseButton);
}
const auto i = KeyToString.find(descriptor);
return (i != end(KeyToString))
? i->second.utf16()
: QString("\\x%1").arg(descriptor, 0, 16);
}
bool IsToggleFullScreenKey(not_null<QKeyEvent*> e) {
const auto mods = e->nativeModifiers()
& NSEventModifierFlagDeviceIndependentFlagsMask;
return (mods == NSEventModifierFlagFunction) // Fn
&& (e->nativeVirtualKey() == 3); // F
}
} // namespace base::Platform::GlobalShortcuts

View File

@@ -0,0 +1,9 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/platform/base_platform_haptic.h"

View File

@@ -0,0 +1,33 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_haptic_mac.h"
#include "base/integration.h"
#include <Cocoa/Cocoa.h>
namespace base::Platform {
void Haptic() {
Integration::Instance().enterFromEventLoop([=] {
[[NSHapticFeedbackManager defaultPerformer]
performFeedbackPattern:NSHapticFeedbackPatternGeneric
performanceTime:NSHapticFeedbackPerformanceTimeDrawCompleted];
});
}
bool IsSwipeBackEnabled() {
static const auto cached = [] {
const auto defaults = [NSUserDefaults standardUserDefaults];
NSNumber *setting = [defaults
objectForKey:@"AppleEnableSwipeNavigateWithScrolls"];
return setting ? [setting boolValue] : true;
}();
return cached;
}
} // namespace base::Platform

View File

@@ -0,0 +1,51 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/platform/base_platform_info.h"
namespace Platform {
inline OutdateReason WhySystemBecomesOutdated() {
return OutdateReason::IsOld;
}
inline constexpr bool IsMac() {
return true;
}
inline constexpr bool IsMacStoreBuild() {
#ifdef OS_MAC_STORE
return true;
#else // OS_MAC_STORE
return false;
#endif // OS_MAC_STORE
}
inline constexpr bool IsWindows() { return false; }
inline constexpr bool IsWindows32Bit() { return false; }
inline constexpr bool IsWindows64Bit() { return false; }
inline constexpr bool IsWindowsARM64() { return false; }
inline constexpr bool IsWindowsStoreBuild() { return false; }
inline bool IsWindows7OrGreater() { return false; }
inline bool IsWindows8OrGreater() { return false; }
inline bool IsWindows8Point1OrGreater() { return false; }
inline bool IsWindows10OrGreater() { return false; }
inline bool IsWindows11OrGreater() { return false; }
inline constexpr bool IsLinux() { return false; }
inline bool IsX11() { return false; }
inline bool IsWayland() { return false; }
inline bool IsXwayland() { return false; }
inline QString GetLibcName() { return QString(); }
inline QString GetLibcVersion() { return QString(); }
inline QString GetWindowManager() { return QString(); }
void OpenInputMonitoringPrivacySettings();
void OpenDesktopCapturePrivacySettings();
void OpenAccessibilityPrivacySettings();
} // namespace Platform

View File

@@ -0,0 +1,292 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_info_mac.h"
#include "base/timer.h"
#include "base/algorithm.h"
#include "base/platform/base_platform_info.h"
#include "base/platform/mac/base_utilities_mac.h"
#include <QtCore/QDate>
#include <QtCore/QProcess>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonValue>
#include <QtCore/QOperatingSystemVersion>
#include <sys/sysctl.h>
#include <Cocoa/Cocoa.h>
#import <IOKit/hidsystem/IOHIDLib.h>
@interface WakeUpObserver : NSObject {
}
- (void) receiveWakeNote:(NSNotification*)note;
@end // @interface WakeUpObserver
@implementation WakeUpObserver {
}
- (void) receiveWakeNote:(NSNotification*)aNotification {
base::CheckLocalTime();
}
@end // @implementation WakeUpObserver
namespace Platform {
namespace {
WakeUpObserver *GlobalWakeUpObserver = nil;
QString FromIdentifier(const QString &model) {
if (model.isEmpty() || model.toLower().indexOf("mac") < 0) {
return QString();
}
QStringList words;
QString word;
for (const QChar &ch : model) {
if (!ch.isLetter()) {
continue;
}
if (ch.isUpper()) {
if (!word.isEmpty()) {
words.push_back(word);
word = QString();
}
}
word.append(ch);
}
if (!word.isEmpty()) {
words.push_back(word);
}
QString result;
for (const QString &word : words) {
if (!result.isEmpty()
&& word != "Mac"
&& word != "Book") {
result.append(' ');
}
result.append(word);
}
return result;
}
[[nodiscard]] int MajorVersion() {
static const auto current = QOperatingSystemVersion::current();
return current.majorVersion();
}
[[nodiscard]] int MinorVersion() {
static const auto current = QOperatingSystemVersion::current();
return current.minorVersion();
}
[[nodiscard]] int PatchVersion() {
static const auto current = QOperatingSystemVersion::current();
return current.microVersion();
}
template <int Major, int Minor>
bool IsMacThatOrGreater() {
static const auto result = (MajorVersion() >= Major)
&& ((MajorVersion() > Major) || (MinorVersion() >= Minor));
return result;
}
template <int Minor>
[[nodiscard]] bool IsMac10ThatOrGreater() {
return IsMacThatOrGreater<10, Minor>();
}
[[nodiscard]] NSURL *PrivacySettingsUrl(const QString &section) {
NSString *url = Q2NSString(
"x-apple.systempreferences:com.apple.preference.security?" + section
);
return [NSURL URLWithString:url];
}
[[nodiscard]] bool RunningThroughRosetta() {
auto result = int(0);
auto size = sizeof(result);
sysctlbyname("sysctl.proc_translated", &result, &size, nullptr, 0);
return (result == 1);
}
[[nodiscard]] QString DeviceFromSystemProfiler() {
// Starting with MacBook M2 the hw.model returns simply Mac[digits],[digits].
// So we try reading "system_profiler" output.
auto process = QProcess();
process.start(
"system_profiler",
{ "-json", "SPHardwareDataType", "-detailLevel", "mini" });
process.waitForFinished();
auto error = QJsonParseError{ 0, QJsonParseError::NoError };
const auto document = QJsonDocument::fromJson(process.readAll(), &error);
if (error.error != QJsonParseError::NoError || !document.isObject()) {
return {};
}
const auto fields = document.object()["SPHardwareDataType"].toArray()[0].toObject();
const auto result = fields["machine_name"].toString();
if (result.isEmpty()) {
return {};
}
const auto chip = fields["chip_type"].toString();
return chip.startsWith("Apple ") ? (result + chip.mid(5)) : result;
}
} // namespace
QString DeviceModelPretty() {
using namespace base::Platform;
static const auto result = FinalizeDeviceModel([&] {
const auto fromSystemProfiler = DeviceFromSystemProfiler();
if (!fromSystemProfiler.isEmpty()) {
return fromSystemProfiler;
}
size_t length = 0;
sysctlbyname("hw.model", nullptr, &length, nullptr, 0);
if (length > 0) {
QByteArray bytes(length, Qt::Uninitialized);
sysctlbyname("hw.model", bytes.data(), &length, nullptr, 0);
const auto parsed = base::CleanAndSimplify(
FromIdentifier(QString::fromUtf8(bytes)));
if (!parsed.isEmpty()) {
return parsed;
}
}
return QString();
}());
return result;
}
QString SystemVersionPretty() {
const auto major = MajorVersion();
const auto minor = MinorVersion();
const auto patch = PatchVersion();
const auto addAsPatch = (patch > 0) ? u".%1"_q.arg(patch) : QString();
if (major < 10) {
return "OS X";
} else if (major == 10 && minor < 12) {
return QString("OS X 10.%1").arg(minor) + addAsPatch;
}
return QString("macOS %1.%2").arg(major).arg(minor) + addAsPatch;
}
QString SystemCountry() {
NSLocale *currentLocale = [NSLocale currentLocale]; // get the current locale.
NSString *countryCode = [currentLocale objectForKey:NSLocaleCountryCode];
return countryCode ? NS2QString(countryCode) : QString();
}
QString SystemLanguage() {
if (auto currentLocale = [NSLocale currentLocale]) { // get the current locale.
if (NSString *collator = [currentLocale objectForKey:NSLocaleCollatorIdentifier]) {
return NS2QString(collator);
}
if (NSString *identifier = [currentLocale objectForKey:NSLocaleIdentifier]) {
return NS2QString(identifier);
}
if (NSString *language = [currentLocale objectForKey:NSLocaleLanguageCode]) {
return NS2QString(language);
}
}
return QString();
}
QDate WhenSystemBecomesOutdated() {
if (!IsMac10_13OrGreater()) {
return QDate(2023, 7, 1);
}
return QDate();
}
int AutoUpdateVersion() {
if (!IsMac10_13OrGreater()) {
return 2;
}
return 4;
}
QString AutoUpdateKey() {
if (QSysInfo::currentCpuArchitecture().startsWith("arm")
|| RunningThroughRosetta()) {
return "armac";
} else {
return "mac";
}
}
bool IsMac10_12OrGreater() {
return IsMac10ThatOrGreater<12>();
}
bool IsMac10_13OrGreater() {
return IsMac10ThatOrGreater<13>();
}
bool IsMac10_14OrGreater() {
return IsMac10ThatOrGreater<14>();
}
bool IsMac10_15OrGreater() {
return IsMac10ThatOrGreater<15>();
}
bool IsMac11_0OrGreater() {
return IsMacThatOrGreater<11, 0>();
}
bool IsMac26_0OrGreater() {
return IsMacThatOrGreater<26, 0>();
}
void Start(QJsonObject settings) {
Expects(GlobalWakeUpObserver == nil);
GlobalWakeUpObserver = [[WakeUpObserver alloc] init];
NSNotificationCenter *center = [[NSWorkspace sharedWorkspace] notificationCenter];
Assert(center != nil);
[center
addObserver: GlobalWakeUpObserver
selector: @selector(receiveWakeNote:)
name: NSWorkspaceDidWakeNotification
object: nil];
Ensures(GlobalWakeUpObserver != nil);
}
void Finish() {
Expects(GlobalWakeUpObserver != nil);
[GlobalWakeUpObserver release];
GlobalWakeUpObserver = nil;
}
void OpenInputMonitoringPrivacySettings() {
if (@available(macOS 10.15, *)) {
IOHIDRequestAccess(kIOHIDRequestTypeListenEvent);
}
[[NSWorkspace sharedWorkspace] openURL:PrivacySettingsUrl("Privacy_ListenEvent")];
}
void OpenDesktopCapturePrivacySettings() {
if (@available(macOS 11.0, *)) {
CGRequestScreenCaptureAccess();
}
[[NSWorkspace sharedWorkspace] openURL:PrivacySettingsUrl("Privacy_ScreenCapture")];
}
void OpenAccessibilityPrivacySettings() {
NSDictionary *const options=@{(__bridge NSString *)kAXTrustedCheckOptionPrompt: @TRUE};
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
}
} // namespace Platform

View File

@@ -0,0 +1,8 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once

View File

@@ -0,0 +1,68 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_last_input_mac.h"
#include <IOKit/IOKitLib.h>
#include <CoreFoundation/CoreFoundation.h>
namespace base::Platform {
// Taken from https://github.com/trueinteractions/tint/issues/53.
std::optional<crl::time> LastUserInputTime() {
CFMutableDictionaryRef properties = 0;
CFTypeRef obj;
mach_port_t masterPort;
io_iterator_t iter;
io_registry_entry_t curObj;
IOMasterPort(MACH_PORT_NULL, &masterPort);
/* Get IOHIDSystem */
IOServiceGetMatchingServices(masterPort, IOServiceMatching("IOHIDSystem"), &iter);
if (iter == 0) {
return std::nullopt;
} else {
curObj = IOIteratorNext(iter);
}
if (IORegistryEntryCreateCFProperties(curObj, &properties, kCFAllocatorDefault, 0) == KERN_SUCCESS && properties != NULL) {
obj = CFDictionaryGetValue(properties, CFSTR("HIDIdleTime"));
CFRetain(obj);
} else {
return std::nullopt;
}
uint64 err = ~0L, idleTime = err;
if (obj) {
CFTypeID type = CFGetTypeID(obj);
if (type == CFDataGetTypeID()) {
CFDataGetBytes((CFDataRef) obj, CFRangeMake(0, sizeof(idleTime)), (UInt8*)&idleTime);
} else if (type == CFNumberGetTypeID()) {
CFNumberGetValue((CFNumberRef)obj, kCFNumberSInt64Type, &idleTime);
} else {
// error
}
CFRelease(obj);
if (idleTime != err) {
idleTime /= 1000000; // return as ms
}
} else {
// error
}
CFRelease((CFTypeRef)properties);
IOObjectRelease(curObj);
IOObjectRelease(iter);
if (idleTime == err) {
return std::nullopt;
}
return (crl::now() - static_cast<crl::time>(idleTime));
}
} // namespace base::Platform

View File

@@ -0,0 +1,8 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once

View File

@@ -0,0 +1,66 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_layout_switch_mac.h"
#include <Carbon/Carbon.h>
#include <Foundation/Foundation.h>
namespace base::Platform {
bool SwitchKeyboardLayoutToEnglish() {
auto result = false;
@autoreleasepool {
const auto kEnglish = base::flat_map<NSString*, int>{
{ @"com.apple.keylayout.USExtended", 0 },
{ @"com.apple.keylayout.ABC", 0 },
{ @"com.apple.keylayout.Australian", 1 },
{ @"com.apple.keylayout.British", 2 },
{ @"com.apple.keylayout.British-PC", 3 },
{ @"com.apple.keylayout.Canadian", 1 },
{ @"com.apple.keylayout.Colemak", 1 },
{ @"com.apple.keylayout.Dvorak", 0 },
{ @"com.apple.keylayout.Dvorak-Left", 0 },
{ @"com.apple.keylayout.DVORAK-QWERTYCMD", 0 },
{ @"com.apple.keylayout.Dvorak-Right", 0 },
{ @"com.apple.keylayout.Irish", 1 },
{ @"com.apple.keylayout.USInternational-PC", 4 },
{ @"com.apple.keylayout.US", 5 },
};
auto selectedLayout = (NSObject*)NULL;
auto selectedLevel = 0;
const auto offer = [&](NSObject *layout, int level) {
if (level > selectedLevel) {
selectedLayout = layout;
selectedLevel = level;
}
};
NSArray *list = [(NSArray *)TISCreateInputSourceList(NULL,NO) autorelease];
for (NSObject *layout in list) {
NSString *layoutId = (NSString*)TISGetInputSourceProperty(
(TISInputSourceRef)layout,
kTISPropertyInputSourceID);
for (const auto &[checkId, checkLevel] : kEnglish) {
if ([layoutId isEqualToString:checkId]) {
offer(layout, checkLevel);
}
}
}
if (selectedLayout != nullptr) {
TISSelectInputSource(TISInputSourceRef(selectedLayout));
result = true;
}
}
return result;
}
} // namespace base::Platform

View File

@@ -0,0 +1,15 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/base_platform_network_reachability.h"
namespace base::Platform {
std::unique_ptr<NetworkReachability> NetworkReachability::Create() {
return nullptr;
}
} // namespace base::Platform

View File

@@ -0,0 +1,9 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/platform/base_platform_power_save_blocker.h"

View File

@@ -0,0 +1,99 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_power_save_blocker_mac.h"
#include "base/debug_log.h"
#include <crl/crl_object_on_thread.h>
// Thanks Chromium: services/device/wake_lock/power_save_blocker
#include <IOKit/pwr_mgt/IOPMLib.h>
namespace base::Platform {
namespace {
// Power management cannot be done on the UI thread. IOPMAssertionCreate does a
// synchronous MIG call to configd, so if it is called on the main thread the UI
// is at the mercy of another process. See http://crbug.com/79559 and
// http://www.opensource.apple.com/source/IOKitUser/IOKitUser-514.16.31/pwr_mgt.subproj/IOPMLibPrivate.c .
class BlockManager final {
public:
explicit BlockManager(crl::weak_on_thread<BlockManager> weak);
void block(PowerSaveBlockType type, const QString &description);
void unblock(PowerSaveBlockType type);
private:
crl::weak_on_thread<BlockManager> _weak;
IOPMAssertionID _assertions[kPowerSaveBlockTypeCount] = {};
};
BlockManager::BlockManager(crl::weak_on_thread<BlockManager> weak)
: _weak(weak) {
}
void BlockManager::block(PowerSaveBlockType type, const QString &description) {
const auto level = CFStringRef([&] {
// See QA1340 <http://developer.apple.com/library/mac/#qa/qa1340/> for more
// details.
switch (type) {
case PowerSaveBlockType::PreventAppSuspension:
return kIOPMAssertionTypeNoIdleSleep;
case PowerSaveBlockType::PreventDisplaySleep:
return kIOPMAssertionTypeNoDisplaySleep;
}
Unexpected("Type in BlockManager::block.");
}());
const auto reason = description.toCFString();
IOReturn result = IOPMAssertionCreateWithName(
level,
kIOPMAssertionLevelOn,
reason,
&_assertions[PowerSaveBlockTypeIndex(type)]);
CFRelease(reason);
if (result != kIOReturnSuccess) {
LOG(("System Error: IOPMAssertionCreate: %1").arg(result));
}
}
void BlockManager::unblock(PowerSaveBlockType type) {
const auto index = PowerSaveBlockTypeIndex(type);
if (_assertions[index] != kIOPMNullAssertionID) {
IOReturn result = IOPMAssertionRelease(_assertions[index]);
_assertions[index] = kIOPMNullAssertionID;
if (result != kIOReturnSuccess) {
LOG(("System Error: IOPMAssertionRelease: %1").arg(result));
}
}
}
[[nodiscard]] crl::object_on_thread<BlockManager> &Manager() {
static auto result = crl::object_on_thread<BlockManager>();
return result;
}
} // namespace
void BlockPowerSave(
PowerSaveBlockType type,
const QString &description,
QPointer<QWindow> window) {
Manager().with([=](BlockManager &instance) {
instance.block(type, description);
});
}
void UnblockPowerSave(PowerSaveBlockType type, QPointer<QWindow> window) {
Manager().with([=](BlockManager &instance) {
instance.unblock(type);
});
}
} // namespace base::Platform

View File

@@ -0,0 +1,9 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/platform/base_platform_process.h"

View File

@@ -0,0 +1,23 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_process_mac.h"
#include <Cocoa/Cocoa.h>
namespace base::Platform {
void ActivateProcessWindow(int64 pid, WId windowId) {
}
void ActivateThisProcessWindow(WId windowId) {
[NSApp activateIgnoringOtherApps:YES];
if (const auto view = reinterpret_cast<NSView*>(windowId)) {
[[view window] makeKeyAndOrderFront:NSApp];
}
}
} // namespace base::Platform

View File

@@ -0,0 +1,379 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/base_platform_system_media_controls.h"
#include "base/integration.h"
#include "base/platform/mac/base_utilities_mac.h"
#import <MediaPlayer/MediaPlayer.h>
#include <QtGui/QImage>
#include <QtWidgets/QWidget>
namespace {
using Command = base::Platform::SystemMediaControls::Command;
using ::Platform::Q2NSString;
using ::Platform::Q2NSImage;
inline auto CommandCenter() {
return [MPRemoteCommandCenter sharedCommandCenter];
}
MPNowPlayingPlaybackState ConvertPlaybackStatus(
base::Platform::SystemMediaControls::PlaybackStatus status) {
using Status = base::Platform::SystemMediaControls::PlaybackStatus;
switch (status) {
case Status::Playing: return MPNowPlayingPlaybackStatePlaying;
case Status::Paused: return MPNowPlayingPlaybackStatePaused;
case Status::Stopped: return MPNowPlayingPlaybackStateStopped;
}
Unexpected("ConvertPlaybackStatus in SystemMediaControls");
}
auto EventToCommand(MPRemoteCommandEvent *event) {
const auto commandCenter = CommandCenter();
const auto command = event.command;
if (command == commandCenter.pauseCommand) {
return Command::Pause;
} else if (command == commandCenter.playCommand) {
return Command::Play;
} else if (command == commandCenter.stopCommand) {
return Command::Stop;
} else if (command == commandCenter.togglePlayPauseCommand) {
return Command::PlayPause;
} else if (command == commandCenter.nextTrackCommand) {
return Command::Next;
} else if (command == commandCenter.previousTrackCommand) {
return Command::Previous;
}
return Command::None;
}
struct RemoteCommand {
const MPRemoteCommand *command;
bool lastEnabled = false;
};
} // namespace
#pragma mark - CommandHandler
@interface CommandHandler : NSObject {
}
@end // @interface CommandHandler
@implementation CommandHandler {
rpl::event_stream<Command> _commandRequests;
rpl::event_stream<int> _seekRequests;
std::vector<RemoteCommand> _commands;
}
- (id)init {
self = [super init];
const auto center = CommandCenter();
_commands = {
{ .command = center.pauseCommand },
{ .command = center.playCommand },
{ .command = center.stopCommand },
{ .command = center.togglePlayPauseCommand },
{ .command = center.nextTrackCommand },
{ .command = center.previousTrackCommand },
{ .command = center.changeRepeatModeCommand },
{ .command = center.changeShuffleModeCommand },
{ .command = center.changePlaybackRateCommand },
{ .command = center.seekBackwardCommand },
{ .command = center.seekForwardCommand },
{ .command = center.skipBackwardCommand },
{ .command = center.skipForwardCommand },
{ .command = center.changePlaybackPositionCommand },
{ .command = center.ratingCommand },
{ .command = center.likeCommand },
{ .command = center.dislikeCommand },
{ .command = center.bookmarkCommand },
{ .command = center.enableLanguageOptionCommand },
{ .command = center.disableLanguageOptionCommand },
};
[self initCommands];
return self;
}
- (void)initCommands {
for (const auto &c : _commands) {
c.command.enabled = c.lastEnabled;
}
const auto center = CommandCenter();
const auto selector = @selector(onCommand:);
[center.pauseCommand addTarget:self action:selector];
[center.playCommand addTarget:self action:selector];
[center.stopCommand addTarget:self action:selector];
[center.togglePlayPauseCommand addTarget:self action:selector];
[center.nextTrackCommand addTarget:self action:selector];
[center.previousTrackCommand addTarget:self action:selector];
[center.changePlaybackPositionCommand
addTarget:self
action:@selector(onSeek:)];
}
- (void)clearCommands {
const auto center = CommandCenter();
for (auto &c : _commands) {
c.lastEnabled = c.command.enabled;
c.command.enabled = false;
}
[center.pauseCommand removeTarget:self];
[center.playCommand removeTarget:self];
[center.stopCommand removeTarget:self];
[center.togglePlayPauseCommand removeTarget:self];
[center.nextTrackCommand removeTarget:self];
[center.previousTrackCommand removeTarget:self];
[center.changePlaybackPositionCommand removeTarget:self];
}
- (rpl::producer<Command>)commandRequests {
return _commandRequests.events();
}
- (rpl::producer<int>)seekRequests {
return _seekRequests.events();
}
- (MPRemoteCommandHandlerStatus)onCommand:(MPRemoteCommandEvent*)event {
base::Integration::Instance().enterFromEventLoop([&] {
self->_commandRequests.fire_copy(EventToCommand(event));
});
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus)onSeek:(
MPChangePlaybackPositionCommandEvent*)event {
base::Integration::Instance().enterFromEventLoop([&] {
self->_seekRequests.fire(event.positionTime * 1000);
});
return MPRemoteCommandHandlerStatusSuccess;
}
- (void)dealloc {
[self clearCommands];
[super dealloc];
}
@end // @@implementation CommandHandler
namespace base::Platform {
struct SystemMediaControls::Private {
Private(
not_null<NSMutableDictionary*> info,
not_null<CommandHandler*> commandHandler)
: info(info)
, commandHandler(commandHandler) {
}
[[nodiscard]] float64 duration() const {
return ((NSNumber*)[info
objectForKey:MPMediaItemPropertyPlaybackDuration]).doubleValue;
}
const not_null<NSMutableDictionary*> info;
const not_null<CommandHandler*> commandHandler;
bool enabled = false;
};
SystemMediaControls::SystemMediaControls()
: _private(std::make_unique<Private>(
[[NSMutableDictionary alloc] init],
[[CommandHandler alloc] init])) {
}
SystemMediaControls::~SystemMediaControls() {
setEnabled(false);
[_private->info release];
[_private->commandHandler release];
}
bool SystemMediaControls::init() {
clearMetadata();
updateDisplay();
return true;
}
void SystemMediaControls::setApplicationName(const QString &name) {
}
void SystemMediaControls::setEnabled(bool enabled) {
if (_private->enabled == enabled) {
return;
}
_private->enabled = enabled;
if (enabled) {
[_private->commandHandler initCommands];
} else {
[_private->commandHandler clearCommands];
}
updateDisplay();
}
void SystemMediaControls::setIsNextEnabled(bool value) {
CommandCenter().nextTrackCommand.enabled = value;
}
void SystemMediaControls::setIsPreviousEnabled(bool value) {
CommandCenter().previousTrackCommand.enabled = value;
}
void SystemMediaControls::setIsPlayPauseEnabled(bool value) {
CommandCenter().togglePlayPauseCommand.enabled = value;
}
void SystemMediaControls::setIsStopEnabled(bool value) {
CommandCenter().stopCommand.enabled = value;
}
void SystemMediaControls::setPlaybackStatus(
SystemMediaControls::PlaybackStatus status) {
[MPNowPlayingInfoCenter defaultCenter].playbackState =
ConvertPlaybackStatus(status);
}
void SystemMediaControls::setLoopStatus(LoopStatus status) {
}
void SystemMediaControls::setShuffle(bool value) {
}
void SystemMediaControls::setTitle(const QString &title) {
[_private->info
setObject:Q2NSString(title)
forKey:MPMediaItemPropertyTitle];
}
void SystemMediaControls::setArtist(const QString &artist) {
[_private->info
setObject:Q2NSString(artist)
forKey:MPMediaItemPropertyArtist];
}
void SystemMediaControls::setThumbnail(const QImage &thumbnail) {
if (thumbnail.isNull()) {
return;
}
if (@available(macOS 10.13.2, *)) {
const auto copy = thumbnail;
[_private->info
setObject:[[[MPMediaItemArtwork alloc]
initWithBoundsSize:CGSizeMake(copy.width(), copy.height())
requestHandler:^NSImage *(CGSize size) {
return Q2NSImage(copy.scaled(
int(size.width),
int(size.height)));
}] autorelease]
forKey:MPMediaItemPropertyArtwork];
updateDisplay();
}
}
void SystemMediaControls::setDuration(int duration) {
[_private->info
setObject:[NSNumber numberWithDouble:(duration / 1000.)]
forKey:MPMediaItemPropertyPlaybackDuration];
}
void SystemMediaControls::setPosition(int position) {
[_private->info
setObject:[NSNumber numberWithDouble:(position / 1000.)]
forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
}
void SystemMediaControls::setVolume(float64 volume) {
}
void SystemMediaControls::clearThumbnail() {
if (@available(macOS 10.13.2, *)) {
[_private->info removeObjectForKey:MPMediaItemPropertyArtwork];
updateDisplay();
}
}
void SystemMediaControls::clearMetadata() {
const auto zeroNumber = [NSNumber numberWithInt:0];
const auto oneNumber = [NSNumber numberWithInt:1];
const auto &info = _private->info;
[info
setObject:zeroNumber
forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
[info setObject:zeroNumber forKey:MPMediaItemPropertyPlaybackDuration];
[info setObject:oneNumber forKey:MPNowPlayingInfoPropertyPlaybackRate];
[info
setObject:oneNumber
forKey:MPNowPlayingInfoPropertyDefaultPlaybackRate];
[info setObject:@"" forKey:MPMediaItemPropertyTitle];
[info setObject:@"" forKey:MPMediaItemPropertyArtist];
[info
setObject:@(MPNowPlayingInfoMediaTypeAudio)
forKey:MPNowPlayingInfoPropertyMediaType];
}
void SystemMediaControls::updateDisplay() {
[[MPNowPlayingInfoCenter defaultCenter]
performSelectorOnMainThread:@selector(setNowPlayingInfo:)
withObject:((_private->enabled && _private->duration())
? _private->info
: nil)
waitUntilDone:false];
}
auto SystemMediaControls::commandRequests() const
-> rpl::producer<SystemMediaControls::Command> {
return [_private->commandHandler commandRequests];
}
rpl::producer<float64> SystemMediaControls::seekRequests() const {
return (
[_private->commandHandler seekRequests]
) | rpl::map([=](int position) {
return float64(position) / (_private->duration() * 1000);
});
}
rpl::producer<float64> SystemMediaControls::volumeChangeRequests() const {
return rpl::never<float64>();
}
rpl::producer<> SystemMediaControls::updatePositionRequests() const {
return rpl::never<>();
}
bool SystemMediaControls::seekingSupported() const {
return true;
}
bool SystemMediaControls::volumeSupported() const {
return false;
}
bool SystemMediaControls::Supported() {
if (@available(macOS 10.12.2, *)) {
return true;
}
return false;
}
} // namespace base::Platform

View File

@@ -0,0 +1,9 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/system_unlock.h"

View File

@@ -0,0 +1,87 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_system_unlock_mac.h"
#include "base/platform/mac/base_utilities_mac.h"
#include <Foundation/Foundation.h>
#include <LocalAuthentication/LocalAuthentication.h>
namespace base {
namespace {
[[nodiscard]] bool Available(LAContext *context, LAPolicy policy) {
NSError *error = nil;
return [context canEvaluatePolicy:policy error:&error];
}
[[nodiscard]] SystemUnlockAvailability Available(bool lookupDetails) {
LAContext *context = [[LAContext alloc] init];
auto result = SystemUnlockAvailability{
.known = true,
.available = Available(
context,
LAPolicyDeviceOwnerAuthentication),
.withBiometrics = lookupDetails && Available(
context,
LAPolicyDeviceOwnerAuthenticationWithBiometrics),
};
if (lookupDetails) {
if (@available(macOS 10.15, *)) {
result.withCompanion = Available(
context,
LAPolicyDeviceOwnerAuthenticationWithWatch);
}
}
[context release];
return result;
}
} // namespace
rpl::producer<SystemUnlockAvailability> SystemUnlockStatus(
bool lookupDetails) {
static auto result = rpl::variable<SystemUnlockAvailability>();
auto refreshed = Available(lookupDetails);
if (!lookupDetails) {
const auto now = result.current();
refreshed.withBiometrics = now.available && now.withBiometrics;
refreshed.withCompanion = now.available && now.withCompanion;
}
result = refreshed;
return result.value();
}
void SuggestSystemUnlock(
not_null<QWidget*> parent,
const QString &text,
Fn<void(SystemUnlockResult)> done) {
LAContext *context = [[LAContext alloc] init];
if (Available(context, LAPolicyDeviceOwnerAuthentication)) {
[context
evaluatePolicy:LAPolicyDeviceOwnerAuthentication
localizedReason:Platform::Q2NSString(text)
reply:^(BOOL success, NSError *error) {
const auto code = int(error.code);
if (success) {
done(SystemUnlockResult::Success);
} else if (error.code == LAErrorTouchIDLockout) {
done(SystemUnlockResult::FloodError);
} else {
done(SystemUnlockResult::Cancelled);
}
}];
} else {
done(SystemUnlockResult::Cancelled);
}
}
} // namespace base

View File

@@ -0,0 +1,9 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/platform/base_platform_url_scheme.h"

View File

@@ -0,0 +1,39 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_url_scheme_mac.h"
#include <Carbon/Carbon.h>
#include <CoreFoundation/CoreFoundation.h>
namespace base::Platform {
bool CheckUrlScheme(const UrlSchemeDescriptor &descriptor) {
const auto name = descriptor.protocol.toStdString();
const auto str = CFStringCreateWithCString(nullptr, name.c_str(), kCFStringEncodingASCII);
const auto current = LSCopyDefaultHandlerForURLScheme(str);
const auto result = CFStringCompare(
current,
(CFStringRef)[[NSBundle mainBundle] bundleIdentifier],
kCFCompareCaseInsensitive);
CFRelease(str);
return (result == kCFCompareEqualTo);
}
void RegisterUrlScheme(const UrlSchemeDescriptor &descriptor) {
const auto name = descriptor.protocol.toStdString();
const auto str = CFStringCreateWithCString(nullptr, name.c_str(), kCFStringEncodingASCII);
LSSetDefaultHandlerForURLScheme(
str,
(CFStringRef)[[NSBundle mainBundle] bundleIdentifier]);
CFRelease(str);
}
void UnregisterUrlScheme(const UrlSchemeDescriptor &descriptor) {
// TODO
}
} // namespace base::Platform

View File

@@ -0,0 +1,58 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include <QtGui/QImage>
#include <Cocoa/Cocoa.h>
namespace Platform {
inline NSString *Q2NSString(const QString &str) {
return [NSString stringWithUTF8String:str.toUtf8().constData()];
}
inline NSString *Q2NSString(QStringView str) {
return [NSString stringWithUTF8String:str.toUtf8().constData()];
}
inline QString NS2QString(NSString *str) {
return QString::fromUtf8([str cStringUsingEncoding:NSUTF8StringEncoding]);
}
template <int Size>
inline QString MakeFromLetters(const uint32 (&letters)[Size]) {
QString result;
result.reserve(Size);
for (int32 i = 0; i < Size; ++i) {
auto code = letters[i];
auto salt1 = (code >> 8) & 0xFFU;
auto salt2 = (code >> 24) & 0xFFU;
auto part1 = ((code & 0xFFU) ^ (salt1 ^ salt2)) & 0xFFU;
auto part2 = (((code >> 16) & 0xFFU) ^ (salt1 ^ ~salt2)) & 0xFFU;
result.push_back(QChar((part2 << 8) | part1));
}
return result;
}
inline NSImage *Q2NSImage(const QImage &image) {
if (image.isNull()) {
return nil;
}
CGImageRef cgImage = image.toCGImage();
if (!cgImage) {
return nil;
}
auto nsImage = [[NSImage alloc] initWithSize:NSZeroSize];
auto *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:cgImage];
imageRep.size = (image.size() / image.devicePixelRatioF()).toCGSize();
[nsImage addRepresentation:[imageRep autorelease]];
CFRelease(cgImage);
return [nsImage autorelease];
}
} // namespace Platform

View File

@@ -0,0 +1,7 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/mac/base_utilities_mac.h"

View File

@@ -0,0 +1,107 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/win/base_battery_saving_win.h"
#include "base/battery_saving.h"
#include "base/integration.h"
#include <QtCore/QAbstractNativeEventFilter>
#include <QtCore/QCoreApplication>
#include <QtGui/QWindow>
#include <QtWidgets/QWidget>
#include <windows.h>
namespace base::Platform {
namespace {
class BatterySaving final
: public AbstractBatterySaving
, public QAbstractNativeEventFilter {
public:
BatterySaving(Fn<void()> changedCallback);
~BatterySaving();
std::optional<bool> enabled() const override;
private:
bool nativeEventFilter(
const QByteArray &eventType,
void *message,
native_event_filter_result *result) override;
QWidget _fake;
HWND _hwnd = nullptr;
HPOWERNOTIFY _notify = nullptr;
Fn<void()> _changedCallback;
};
BatterySaving::BatterySaving(Fn<void()> changedCallback)
: _changedCallback(std::move(changedCallback)) {
if (!_changedCallback) {
return;
}
_fake.hide();
_fake.createWinId();
const auto window = _fake.windowHandle();
if (!window) {
return;
}
_hwnd = reinterpret_cast<HWND>(window->winId());
if (!_hwnd) {
return;
}
_notify = RegisterPowerSettingNotification(
_hwnd,
&GUID_POWER_SAVING_STATUS,
DEVICE_NOTIFY_WINDOW_HANDLE);
if (!_notify) {
return;
}
qApp->installNativeEventFilter(this);
}
BatterySaving::~BatterySaving() {
if (_notify) {
qApp->removeNativeEventFilter(this);
UnregisterPowerSettingNotification(_notify);
}
}
std::optional<bool> BatterySaving::enabled() const {
if (_changedCallback && !_notify) {
return std::nullopt;
}
auto status = SYSTEM_POWER_STATUS();
if (!GetSystemPowerStatus(&status) || (status.BatteryFlag & 128)) {
return std::nullopt;
}
return (status.SystemStatusFlag == 1);
}
bool BatterySaving::nativeEventFilter(
const QByteArray &eventType,
void *message,
native_event_filter_result *result) {
Expects(_hwnd != nullptr);
const auto msg = static_cast<MSG*>(message);
if (msg->hwnd == _hwnd && msg->message == WM_POWERBROADCAST) {
Integration::Instance().enterFromEventLoop(_changedCallback);
}
return false;
}
} // namespace
std::unique_ptr<AbstractBatterySaving> CreateBatterySaving(
Fn<void()> changedCallback) {
return std::make_unique<BatterySaving>(std::move(changedCallback));
}
} // namespace base::Platform

View File

@@ -0,0 +1,7 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once

View File

@@ -0,0 +1,247 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/win/base_file_utilities_win.h"
#include "base/platform/win/base_windows_safe_library.h"
#include "base/platform/win/base_windows_shlobj_h.h"
#include "base/algorithm.h"
#include <QtCore/QString>
#include <QtCore/QDir>
#include <array>
#include <string>
#include <shellapi.h>
#include <Shlwapi.h>
#include <RestartManager.h>
#include <io.h>
#define LOAD_SYMBOL(lib, name) ::base::Platform::LoadMethod(lib, #name, name)
namespace base::Platform {
namespace {
// RSTRTMGR.DLL
DWORD(__stdcall *RmStartSession)(
_Out_ DWORD *pSessionHandle,
_Reserved_ DWORD dwSessionFlags,
_Out_writes_(CCH_RM_SESSION_KEY + 1) WCHAR strSessionKey[]);
DWORD(__stdcall *RmRegisterResources)(
_In_ DWORD dwSessionHandle,
_In_ UINT nFiles,
_In_reads_opt_(nFiles) LPCWSTR rgsFileNames[],
_In_ UINT nApplications,
_In_reads_opt_(nApplications) RM_UNIQUE_PROCESS rgApplications[],
_In_ UINT nServices,
_In_reads_opt_(nServices) LPCWSTR rgsServiceNames[]);
DWORD(__stdcall *RmGetList)(
_In_ DWORD dwSessionHandle,
_Out_ UINT *pnProcInfoNeeded,
_Inout_ UINT *pnProcInfo,
_Inout_updates_opt_(*pnProcInfo) RM_PROCESS_INFO rgAffectedApps[],
_Out_ LPDWORD lpdwRebootReasons);
DWORD(__stdcall *RmShutdown)(
_In_ DWORD dwSessionHandle,
_In_ ULONG lActionFlags,
_In_opt_ RM_WRITE_STATUS_CALLBACK fnStatus);
DWORD(__stdcall *RmEndSession)(
_In_ DWORD dwSessionHandle);
} // namespace
void ShowInFolder(const QString &filepath) {
auto nativePath = QDir::toNativeSeparators(filepath);
const auto path = nativePath.toStdWString();
if (const auto pidl = ILCreateFromPathW(path.c_str())) {
SHOpenFolderAndSelectItems(pidl, 0, nullptr, 0);
ILFree(pidl);
return;
}
const auto pathEscaped = nativePath.replace('"', QString("\"\""));
const auto command = ("/select," + pathEscaped).toStdWString();
ShellExecute(
0,
0,
L"explorer",
command.c_str(),
0,
SW_SHOWNORMAL);
}
QString FileNameFromUserString(QString name) {
const auto kBadExtensions = { qstr(".lnk"), qstr(".scf") };
const auto kMaskExtension = qstr(".download");
for (const auto extension : kBadExtensions) {
if (name.endsWith(extension, Qt::CaseInsensitive)) {
name += kMaskExtension;
}
}
static const auto BadNames = {
qstr("CON"),
qstr("PRN"),
qstr("AUX"),
qstr("NUL"),
qstr("COM1"),
qstr("COM2"),
qstr("COM3"),
qstr("COM4"),
qstr("COM5"),
qstr("COM6"),
qstr("COM7"),
qstr("COM8"),
qstr("COM9"),
qstr("LPT1"),
qstr("LPT2"),
qstr("LPT3"),
qstr("LPT4"),
qstr("LPT5"),
qstr("LPT6"),
qstr("LPT7"),
qstr("LPT8"),
qstr("LPT9")
};
for (const auto bad : BadNames) {
if (name.startsWith(bad, Qt::CaseInsensitive)) {
if (name.size() == bad.size() || name[bad.size()] == '.') {
name = '_' + name;
break;
}
}
}
return name;
}
bool DeleteDirectory(QString path) {
if (path.endsWith('/')) {
path.chop(1);
}
const auto wide = QDir::toNativeSeparators(path).toStdWString()
+ wchar_t(0)
+ wchar_t(0);
SHFILEOPSTRUCT file_op = {
NULL,
FO_DELETE,
wide.data(),
L"",
FOF_NOCONFIRMATION |
FOF_NOERRORUI |
FOF_SILENT,
false,
0,
L""
};
return (SHFileOperation(&file_op) == 0);
}
void RemoveQuarantine(const QString &path) {
}
QString BundledResourcesPath() {
Unexpected("BundledResourcesPath not implemented.");
}
QString CurrentExecutablePath(int argc, char *argv[]) {
auto result = std::array<WCHAR, MAX_PATH + 1>{ 0 };
const auto count = GetModuleFileName(
nullptr,
result.data(),
MAX_PATH + 1);
if (count < MAX_PATH + 1) {
const auto info = QFileInfo(QDir::fromNativeSeparators(
QString::fromWCharArray(result.data(), count)));
return info.absoluteFilePath();
}
// Fallback to the first command line argument.
auto argsCount = 0;
if (const auto args = CommandLineToArgvW(GetCommandLine(), &argsCount)) {
auto info = QFileInfo(QDir::fromNativeSeparators(
QString::fromWCharArray(args[0])));
LocalFree(args);
return info.absoluteFilePath();
}
return QString();
}
bool CloseProcesses(const QString &filename) {
static const auto loaded = [&] {
const auto LibRstrtMgr = SafeLoadLibrary(L"rstrtmgr.dll");
return LOAD_SYMBOL(LibRstrtMgr, RmStartSession)
&& LOAD_SYMBOL(LibRstrtMgr, RmRegisterResources)
&& LOAD_SYMBOL(LibRstrtMgr, RmGetList)
&& LOAD_SYMBOL(LibRstrtMgr, RmShutdown)
&& LOAD_SYMBOL(LibRstrtMgr, RmEndSession);
}();
if (!loaded) {
return false;
}
auto session = DWORD();
auto sessionKey = std::wstring(CCH_RM_SESSION_KEY + 1, wchar_t(0));
auto error = RmStartSession(&session, 0, sessionKey.data());
if (error != ERROR_SUCCESS) {
return false;
}
const auto guard = gsl::finally([&] { RmEndSession(session); });
const auto path = QDir::toNativeSeparators(filename).toStdWString();
auto nullterm = path.c_str();
error = RmRegisterResources(
session,
1,
&nullterm,
0,
nullptr,
0,
nullptr);
if (error != ERROR_SUCCESS) {
return false;
}
auto processInfoNeeded = UINT(0);
auto processInfoCount = UINT(0);
auto reason = DWORD();
error = RmGetList(
session,
&processInfoNeeded,
&processInfoCount,
nullptr,
&reason);
if (error != ERROR_SUCCESS && error != ERROR_MORE_DATA) {
return false;
} else if (processInfoNeeded <= 0) {
return true;
}
error = RmShutdown(session, RmForceShutdown, NULL);
if (error != ERROR_SUCCESS) {
return false;
}
return true;
}
bool RenameWithOverwrite(const QString &from, const QString &to) {
const auto fromPath = QDir::toNativeSeparators(from).toStdWString();
const auto toPath = QDir::toNativeSeparators(to).toStdWString();
return MoveFileEx(
fromPath.c_str(),
toPath.c_str(),
MOVEFILE_REPLACE_EXISTING);
}
void FlushFileData(QFile &file) {
file.flush();
if (const auto descriptor = file.handle()) {
if (const auto handle = HANDLE(_get_osfhandle(descriptor))) {
FlushFileBuffers(handle);
}
}
}
} // namespace base::Platform

View File

@@ -0,0 +1,13 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
namespace base::Platform {
bool CloseProcesses(const QString &filename);
} // namespace base::Platform

View File

@@ -0,0 +1,224 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/win/base_global_shortcuts_win.h"
#include <windows.h>
namespace base::Platform::GlobalShortcuts {
namespace {
constexpr auto kShiftMouseButton = std::numeric_limits<uint64>::max() - 100;
HHOOK GlobalHookKeyboard = nullptr;
HHOOK GlobalHookMouse = nullptr;
HANDLE ThreadHandle = nullptr;
HANDLE ThreadEvent = nullptr;
DWORD ThreadId = 0;
Fn<void(GlobalShortcutKeyGeneric descriptor, bool down)> ProcessCallback;
[[nodiscard]] GlobalShortcutKeyGeneric MakeDescriptor(
uint32 virtualKeyCode,
uint32 lParam) {
return GlobalShortcutKeyGeneric(
(uint64(virtualKeyCode) << 32) | uint64(lParam));
}
[[nodiscard]] GlobalShortcutKeyGeneric MakeMouseDescriptor(uint8 button) {
Expects(button > 0 && button < 100);
return kShiftMouseButton + button;
}
[[nodiscard]] uint32 GetVirtualKeyCode(GlobalShortcutKeyGeneric descriptor) {
return uint32(uint64(descriptor) >> 32);
}
[[nodiscard]] uint32 GetLParam(GlobalShortcutKeyGeneric descriptor) {
return uint32(uint64(descriptor) & 0xFFFFFFFFULL);
}
void ProcessHookedKeyboardEvent(WPARAM wParam, LPARAM lParam) {
const auto press = (PKBDLLHOOKSTRUCT)lParam;
const auto repeatCount = uint32(0);
const auto extendedBit = ((press->flags & LLKHF_EXTENDED) != 0);
//const auto contextBit = ((press->flags & LLKHF_ALTDOWN) != 0);
//const auto transitionState = ((press->flags & LLKHF_UP) != 0);
const auto lParamForEvent = (repeatCount & 0x0000FFFFU)
| ((uint32(press->scanCode) & 0xFFU) << 16)
| (extendedBit ? (uint32(KF_EXTENDED) << 16) : 0);
//| (contextBit ? (uint32(KF_ALTDOWN) << 16) : 0); // Alt pressed.
//| (transitionState ? (uint32(KF_UP) << 16) : 0); // Is pressed.
const auto descriptor = MakeDescriptor(press->vkCode, lParamForEvent);
const auto down = (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN);
ProcessCallback(descriptor, down);
}
void ProcessHookedMouseEvent(WPARAM wParam, LPARAM lParam) {
if (wParam != WM_RBUTTONDOWN
&& wParam != WM_RBUTTONUP
&& wParam != WM_MBUTTONDOWN
&& wParam != WM_MBUTTONUP
&& wParam != WM_XBUTTONDOWN
&& wParam != WM_XBUTTONUP) {
return;
}
const auto button = [&] {
if (wParam == WM_RBUTTONDOWN || wParam == WM_RBUTTONUP) {
return 2;
} else if (wParam == WM_MBUTTONDOWN || wParam == WM_MBUTTONUP) {
return 3;
}
const auto press = (PMSLLHOOKSTRUCT)lParam;
const auto xbutton = ((press->mouseData >> 16) & 0xFFU);
return (xbutton >= 0x01 && xbutton <= 0x18) ? int(xbutton + 3) : 0;
}();
if (!button) {
return;
}
const auto descriptor = MakeMouseDescriptor(button);
const auto down = (wParam == WM_RBUTTONDOWN)
|| (wParam == WM_MBUTTONDOWN)
|| (wParam == WM_XBUTTONDOWN);
ProcessCallback(descriptor, down);
}
LRESULT CALLBACK LowLevelKeyboardProc(
_In_ int nCode,
_In_ WPARAM wParam,
_In_ LPARAM lParam) {
if (nCode == HC_ACTION) {
ProcessHookedKeyboardEvent(wParam, lParam);
}
return CallNextHookEx(GlobalHookKeyboard, nCode, wParam, lParam);
}
LRESULT CALLBACK LowLevelMouseProc(
_In_ int nCode,
_In_ WPARAM wParam,
_In_ LPARAM lParam) {
if (nCode == HC_ACTION) {
ProcessHookedMouseEvent(wParam, lParam);
}
return CallNextHookEx(GlobalHookMouse, nCode, wParam, lParam);
}
DWORD WINAPI RunThread(LPVOID) {
auto message = MSG();
// Force message loop creation.
PeekMessage(&message, nullptr, WM_USER, WM_USER, PM_NOREMOVE);
SetEvent(ThreadEvent);
const auto guard = gsl::finally([&] {
if (GlobalHookKeyboard) {
UnhookWindowsHookEx(GlobalHookKeyboard);
GlobalHookKeyboard = nullptr;
}
if (GlobalHookMouse) {
UnhookWindowsHookEx(GlobalHookMouse);
GlobalHookMouse = nullptr;
}
});
GlobalHookKeyboard = SetWindowsHookEx(
WH_KEYBOARD_LL,
LowLevelKeyboardProc,
nullptr,
0);
GlobalHookMouse = SetWindowsHookEx(
WH_MOUSE_LL,
LowLevelMouseProc,
nullptr,
0);
if (!GlobalHookKeyboard || !GlobalHookMouse) {
return -1;
}
while (GetMessage(&message, nullptr, 0, 0)) {
if (message.message == WM_QUIT) {
break;
}
TranslateMessage(&message);
DispatchMessage(&message);
}
return 0;
}
} // namespace
bool Available() {
return true;
}
bool Allowed() {
return true;
}
void Start(Fn<void(GlobalShortcutKeyGeneric descriptor, bool down)> process) {
Expects(!ThreadHandle);
Expects(!ThreadId);
ProcessCallback = std::move(process);
ThreadEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
if (!ThreadEvent) {
ProcessCallback = nullptr;
return;
}
ThreadHandle = CreateThread(
nullptr,
0,
&RunThread,
nullptr,
0,
&ThreadId);
if (!ThreadHandle) {
CloseHandle(ThreadEvent);
ThreadEvent = nullptr;
ThreadId = 0;
ProcessCallback = nullptr;
}
}
void Stop() {
if (!ThreadHandle) {
return;
}
WaitForSingleObject(ThreadEvent, INFINITE);
PostThreadMessage(ThreadId, WM_QUIT, 0, 0);
WaitForSingleObject(ThreadHandle, INFINITE);
CloseHandle(ThreadHandle);
CloseHandle(ThreadEvent);
ThreadHandle = ThreadEvent = nullptr;
ThreadId = 0;
ProcessCallback = nullptr;
}
QString KeyName(GlobalShortcutKeyGeneric descriptor) {
if (descriptor > kShiftMouseButton) {
return QString("Mouse %1").arg(descriptor - kShiftMouseButton);
}
constexpr auto kLimit = 1024;
WCHAR buffer[kLimit + 1] = { 0 };
// Remove 25 bit, we want to differentiate between left and right Ctrl-s.
auto lParam = LONG(GetLParam(descriptor) & ~(1U << 25));
return GetKeyNameText(lParam, buffer, kLimit)
? QString::fromWCharArray(buffer)
: (GetVirtualKeyCode(descriptor) == VK_RSHIFT)
? QString("Right Shift")
: QString("\\x%1").arg(GetVirtualKeyCode(descriptor), 0, 16);
}
bool IsToggleFullScreenKey(not_null<QKeyEvent*> e) {
return false;
}
} // namespace base::Platform::GlobalShortcuts

View File

@@ -0,0 +1,9 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:z
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/platform/base_platform_global_shortcuts.h"

View File

@@ -0,0 +1,18 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/win/base_haptic_win.h"
namespace base::Platform {
void Haptic() {
}
bool IsSwipeBackEnabled() {
return true;
}
} // namespace base::Platform

View File

@@ -0,0 +1,9 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/platform/base_platform_haptic.h"

View File

@@ -0,0 +1,371 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "base/platform/win/base_info_win.h"
#include "base/algorithm.h"
#include "base/platform/win/base_windows_safe_library.h"
#include <QtCore/QOperatingSystemVersion>
#include <QtCore/QJsonObject>
#include <QtCore/QDate>
#include <QtCore/QSettings>
#include <windows.h>
#include <VersionHelpers.h>
namespace Platform {
namespace {
decltype(&::IsWow64Process2) IsWow64Process2;
bool IsWow64Process2Supported() {
static const auto Result = [&] {
#define LOAD_SYMBOL(lib, name) base::Platform::LoadMethod(lib, #name, name)
const auto lib = base::Platform::SafeLoadLibrary(L"Kernel32.dll");
return LOAD_SYMBOL(lib, IsWow64Process2);
#undef LOAD_SYMBOL
}();
return Result;
}
QString GetLangCodeById(unsigned int lngId) {
const auto primary = (lngId & 0xFFU);
switch (primary) {
case 0x36: return u"af"_q;
case 0x1C: return u"sq"_q;
case 0x5E: return u"am"_q;
case 0x01: return u"ar"_q;
case 0x2B: return u"hy"_q;
case 0x4D: return u"as"_q;
case 0x2C: return u"az"_q;
case 0x45: return u"bn"_q;
case 0x6D: return u"ba"_q;
case 0x2D: return u"eu"_q;
case 0x23: return u"be"_q;
case 0x1A:
return (lngId == LANG_CROATIAN)
? u"hr"_q
: (lngId == LANG_BOSNIAN_NEUTRAL || lngId == LANG_BOSNIAN)
? u"bs"_q
: u"sr"_q;
case 0x7E: return u"br"_q;
case 0x02: return u"bg"_q;
case 0x92: return u"ku"_q;
case 0x03: return u"ca"_q;
case 0x04: return u"zh"_q;
case 0x83: return u"co"_q;
case 0x05: return u"cs"_q;
case 0x06: return u"da"_q;
case 0x65: return u"dv"_q;
case 0x13: return u"nl"_q;
case 0x09: return u"en"_q;
case 0x25: return u"et"_q;
case 0x38: return u"fo"_q;
case 0x0B: return u"fi"_q;
case 0x0c: return u"fr"_q;
case 0x62: return u"fy"_q;
case 0x56: return u"gl"_q;
case 0x37: return u"ka"_q;
case 0x07: return u"de"_q;
case 0x08: return u"el"_q;
case 0x6F: return u"kl"_q;
case 0x47: return u"gu"_q;
case 0x68: return u"ha"_q;
case 0x0D: return u"he"_q;
case 0x39: return u"hi"_q;
case 0x0E: return u"hu"_q;
case 0x0F: return u"is"_q;
case 0x70: return u"ig"_q;
case 0x21: return u"id"_q;
case 0x5D: return u"iu"_q;
case 0x3C: return u"ga"_q;
case 0x34: return u"xh"_q;
case 0x35: return u"zu"_q;
case 0x10: return u"it"_q;
case 0x11: return u"ja"_q;
case 0x4B: return u"kn"_q;
case 0x3F: return u"kk"_q;
case 0x53: return u"kh"_q;
case 0x87: return u"rw"_q;
case 0x12: return u"ko"_q;
case 0x40: return u"ky"_q;
case 0x54: return u"lo"_q;
case 0x26: return u"lv"_q;
case 0x27: return u"lt"_q;
case 0x6E: return u"lb"_q;
case 0x2F: return u"mk"_q;
case 0x3E: return u"ms"_q;
case 0x4C: return u"ml"_q;
case 0x3A: return u"mt"_q;
case 0x81: return u"mi"_q;
case 0x4E: return u"mr"_q;
case 0x50: return u"mn"_q;
case 0x61: return u"ne"_q;
case 0x14: return u"no"_q;
case 0x82: return u"oc"_q;
case 0x48: return u"or"_q;
case 0x63: return u"ps"_q;
case 0x29: return u"fa"_q;
case 0x15: return u"pl"_q;
case 0x16: return u"pt"_q;
case 0x67: return u"ff"_q;
case 0x46: return u"pa"_q;
case 0x18: return u"ro"_q;
case 0x17: return u"rm"_q;
case 0x19: return u"ru"_q;
case 0x3B: return u"se"_q;
case 0x4F: return u"sa"_q;
case 0x32: return u"tn"_q;
case 0x59: return u"sd"_q;
case 0x5B: return u"si"_q;
case 0x1B: return u"sk"_q;
case 0x24: return u"sl"_q;
case 0x0A: return u"es"_q;
case 0x41: return u"sw"_q;
case 0x1D: return u"sv"_q;
case 0x28: return u"tg"_q;
case 0x49: return u"ta"_q;
case 0x44: return u"tt"_q;
case 0x4A: return u"te"_q;
case 0x1E: return u"th"_q;
case 0x51: return u"bo"_q;
case 0x73: return u"ti"_q;
case 0x1F: return u"tr"_q;
case 0x42: return u"tk"_q;
case 0x22: return u"uk"_q;
case 0x20: return u"ur"_q;
case 0x80: return u"ug"_q;
case 0x43: return u"uz"_q;
case 0x2A: return u"vi"_q;
case 0x52: return u"cy"_q;
case 0x88: return u"wo"_q;
case 0x78: return u"ii"_q;
case 0x6A: return u"yo"_q;
}
return QString();
}
} // namespace
QString DeviceModelPretty() {
using namespace base::Platform;
static const auto result = FinalizeDeviceModel([&] {
const auto bios = QSettings(
"HKEY_LOCAL_MACHINE\\HARDWARE\\DESCRIPTION\\System\\BIOS",
QSettings::NativeFormat);
const auto value = [&](const char *key) {
return SimplifyDeviceModel(bios.value(key).toString());
};
const auto systemProductName = value("SystemProductName");
if (const auto model = ProductNameToDeviceModel(systemProductName)
; !model.isEmpty()) {
return model;
}
const auto systemFamily = value("SystemFamily");
const auto baseBoardProduct = value("BaseBoardProduct");
const auto familyBoard = SimplifyDeviceModel(
systemFamily + ' ' + baseBoardProduct);
if (IsDeviceModelOk(familyBoard)) {
return familyBoard;
} else if (IsDeviceModelOk(baseBoardProduct)) {
return baseBoardProduct;
} else if (IsDeviceModelOk(systemFamily)) {
return systemFamily;
}
return QString();
}());
return result;
}
QString SystemVersionPretty() {
static const auto result = [&] {
QStringList resultList;
if (IsWindows11OrGreater()) {
resultList << "Windows 11";
} else if (IsWindows10OrGreater()) {
resultList << "Windows 10";
} else if (IsWindows8Point1OrGreater()) {
resultList << "Windows 8.1";
} else if (IsWindows8OrGreater()) {
resultList << "Windows 8";
} else if (IsWindows7OrGreater()) {
resultList << "Windows 7";
} else {
resultList << QSysInfo::prettyProductName();
}
USHORT processMachine, nativeMachine{};
if (IsWow64Process2Supported()) {
IsWow64Process2(
GetCurrentProcess(),
&processMachine,
&nativeMachine);
} else {
BOOL isWow64{};
IsWow64Process(GetCurrentProcess(), &isWow64);
if (isWow64) {
nativeMachine = IMAGE_FILE_MACHINE_AMD64;
}
}
switch (nativeMachine) {
case IMAGE_FILE_MACHINE_AMD64: {
resultList << "x64";
break;
}
case IMAGE_FILE_MACHINE_ARM64: {
resultList << "arm64";
break;
}
}
return resultList.join(' ');
}();
return result;
}
QString SystemCountry() {
auto key = HKEY();
const auto result = RegOpenKeyEx(
HKEY_CURRENT_USER,
L"Control Panel\\International\\Geo",
0,
KEY_READ,
&key);
if (result == ERROR_SUCCESS) {
constexpr auto kBufSize = 4;
auto checkType = DWORD();
auto checkSize = DWORD(kBufSize * 2);
auto checkStr = std::array<WCHAR, kBufSize>{ 0 };
const auto result = RegQueryValueEx(
key,
L"Name",
0,
&checkType,
reinterpret_cast<BYTE*>(checkStr.data()),
&checkSize);
if (result == ERROR_SUCCESS && checkSize == 6) { // 2 wchars + null
return QString::fromWCharArray(checkStr.data());
}
}
int chCount = GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SISO3166CTRYNAME, 0, 0);
if (chCount && chCount < 128) {
WCHAR wstrCountry[128];
int len = GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SISO3166CTRYNAME, wstrCountry, chCount);
if (len) {
return QString::fromStdWString(std::wstring(wstrCountry));
}
}
return QString();
}
QString SystemLanguage() {
constexpr auto kMaxLanguageLength = 128;
auto uiLanguageId = GetUserDefaultUILanguage();
auto uiLanguageLength = GetLocaleInfo(uiLanguageId, LOCALE_SNAME, nullptr, 0);
if (uiLanguageLength > 0 && uiLanguageLength < kMaxLanguageLength) {
WCHAR uiLanguageWideString[kMaxLanguageLength] = { 0 };
uiLanguageLength = GetLocaleInfo(uiLanguageId, LOCALE_SNAME, uiLanguageWideString, uiLanguageLength);
if (uiLanguageLength <= 0) {
return QString();
}
return QString::fromWCharArray(uiLanguageWideString);
}
auto uiLanguageCodeLength = GetLocaleInfo(uiLanguageId, LOCALE_ILANGUAGE, nullptr, 0);
if (uiLanguageCodeLength > 0 && uiLanguageCodeLength < kMaxLanguageLength) {
WCHAR uiLanguageCodeWideString[kMaxLanguageLength] = { 0 };
uiLanguageCodeLength = GetLocaleInfo(uiLanguageId, LOCALE_ILANGUAGE, uiLanguageCodeWideString, uiLanguageCodeLength);
if (uiLanguageCodeLength <= 0) {
return QString();
}
auto languageCode = 0U;
for (auto i = 0; i != uiLanguageCodeLength; ++i) {
auto ch = uiLanguageCodeWideString[i];
if (!ch) {
break;
}
languageCode *= 0x10U;
if (ch >= WCHAR('0') && ch <= WCHAR('9')) {
languageCode += static_cast<unsigned>(int(ch) - int(WCHAR('0')));
} else if (ch >= WCHAR('A') && ch <= WCHAR('F')) {
languageCode += static_cast<unsigned>(0x0A + int(ch) - int(WCHAR('A')));
} else {
return QString();
}
}
return GetLangCodeById(languageCode);
}
return QString();
}
QDate WhenSystemBecomesOutdated() {
return QDate();
}
int AutoUpdateVersion() {
return 4;
}
QString AutoUpdateKey() {
if (IsWindowsARM64()) {
return "winarm";
} else if (IsWindows64Bit()) {
return "win64";
} else {
return "win";
}
}
bool IsWindows7OrGreater() {
static const auto result = ::IsWindows7OrGreater();
return result;
}
bool IsWindows8OrGreater() {
static const auto result = ::IsWindows8OrGreater();
return result;
}
bool IsWindows8Point1OrGreater() {
static const auto result = ::IsWindows8Point1OrGreater();
return result;
}
bool IsWindows10OrGreater() {
static const auto result = ::IsWindows10OrGreater();
return result;
}
bool IsWindows11OrGreater() {
static const auto result = [&] {
if (!IsWindows10OrGreater()) {
return false;
}
const auto version = QOperatingSystemVersion::current();
return (version.majorVersion() > 10)
|| (version.microVersion() >= 22000);
}();
return result;
}
void Start(QJsonObject settings) {
SetDllDirectory(L"");
}
void Finish() {
}
} // namespace Platform

View File

@@ -0,0 +1,69 @@
// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#pragma once
#include "base/platform/base_platform_info.h"
namespace Platform {
inline OutdateReason WhySystemBecomesOutdated() {
return OutdateReason::IsOld;
}
inline constexpr bool IsWindows() {
return true;
}
inline constexpr bool IsWindows32Bit() {
#ifdef Q_PROCESSOR_X86_32
return true;
#else // Q_PROCESSOR_X86_32
return false;
#endif // Q_PROCESSOR_X86_32
}
inline constexpr bool IsWindows64Bit() {
#ifdef Q_PROCESSOR_X86_64
return true;
#else // Q_PROCESSOR_X86_64
return false;
#endif // Q_PROCESSOR_X86_64
}
inline constexpr bool IsWindowsARM64() {
#ifdef Q_PROCESSOR_ARM_64
return true;
#else // Q_PROCESSOR_ARM_64
return false;
#endif // Q_PROCESSOR_ARM_64
}
inline constexpr bool IsWindowsStoreBuild() {
#ifdef OS_WIN_STORE
return true;
#else // OS_WIN_STORE
return false;
#endif // OS_WIN_STORE
}
inline constexpr bool IsMac() { return false; }
inline constexpr bool IsMacStoreBuild() { return false; }
inline bool IsMac10_12OrGreater() { return false; }
inline bool IsMac10_13OrGreater() { return false; }
inline bool IsMac10_14OrGreater() { return false; }
inline bool IsMac10_15OrGreater() { return false; }
inline bool IsMac11_0OrGreater() { return false; }
inline bool IsMac26_0OrGreater() { return false; }
inline constexpr bool IsLinux() { return false; }
inline bool IsX11() { return false; }
inline bool IsWayland() { return false; }
inline bool IsXwayland() { return false; }
inline QString GetLibcName() { return QString(); }
inline QString GetLibcVersion() { return QString(); }
inline QString GetWindowManager() { return QString(); }
} // namespace Platform

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