mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
chore(clangd): new clang-format rules + actually applies to all cpp/h files
This commit is contained in:
@@ -3,3 +3,10 @@ IndentWidth: 2
|
||||
ColumnLimit: 120
|
||||
PointerAlignment: Left
|
||||
NamespaceIndentation: All
|
||||
SortIncludes: CaseSensitive
|
||||
IncludeBlocks: Regroup
|
||||
IncludeCategories:
|
||||
- Regex: '^".*"'
|
||||
Priority: 1
|
||||
- Regex: '^<.*>'
|
||||
Priority: 2
|
||||
|
||||
@@ -25,8 +25,8 @@ run m=mode:
|
||||
./build-{{m}}/noctalia
|
||||
|
||||
format:
|
||||
clang-format -i src/**/*.cpp src/**/*.h
|
||||
find src \( -name '*.cpp' -o -name '*.h' \) | xargs grep -rlP '\s+$' | xargs -r sed -i 's/[[:space:]]*$//'
|
||||
find src \( -name '*.cpp' -o -name '*.h' \) -print0 | xargs -0 clang-format -i
|
||||
find src \( -name '*.cpp' -o -name '*.h' \) -print0 | xargs -0 grep -ZlP '\s+$' | xargs -0 -r sed -i 's/[[:space:]]*$//'
|
||||
|
||||
clean m=mode:
|
||||
rm -rf build-{{m}}
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include <cerrno>
|
||||
#include <poll.h>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <wayland-client-core.h>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
#include "auth/pam_authenticator.h"
|
||||
|
||||
#include <pwd.h>
|
||||
#include <security/pam_appl.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <pwd.h>
|
||||
#include <security/pam_appl.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
#include "compositors/ext_workspace_backend.h"
|
||||
|
||||
#include "core/log.h"
|
||||
#include "ext-workspace-v1-client-protocol.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "ext-workspace-v1-client-protocol.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("workspace_ext");
|
||||
|
||||
@@ -3,35 +3,35 @@
|
||||
#include "core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <charconv>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <string_view>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("workspace_hyprland");
|
||||
constexpr Logger kLog("workspace_hyprland");
|
||||
|
||||
[[nodiscard]] std::optional<std::size_t> parseLeadingNumber(const std::string& value) {
|
||||
if (value.empty() || !std::isdigit(static_cast<unsigned char>(value.front()))) {
|
||||
return std::nullopt;
|
||||
}
|
||||
[[nodiscard]] std::optional<std::size_t> parseLeadingNumber(const std::string& value) {
|
||||
if (value.empty() || !std::isdigit(static_cast<unsigned char>(value.front()))) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::size_t parsed = 0;
|
||||
std::size_t index = 0;
|
||||
while (index < value.size() && std::isdigit(static_cast<unsigned char>(value[index]))) {
|
||||
parsed = (parsed * 10) + static_cast<std::size_t>(value[index] - '0');
|
||||
++index;
|
||||
std::size_t parsed = 0;
|
||||
std::size_t index = 0;
|
||||
while (index < value.size() && std::isdigit(static_cast<unsigned char>(value[index]))) {
|
||||
parsed = (parsed * 10) + static_cast<std::size_t>(value[index] - '0');
|
||||
++index;
|
||||
}
|
||||
return parsed > 0 ? std::optional<std::size_t>(parsed) : std::nullopt;
|
||||
}
|
||||
return parsed > 0 ? std::optional<std::size_t>(parsed) : std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -589,8 +589,7 @@ void HyprlandWorkspaceBackend::handleEvent(std::string_view line) {
|
||||
}
|
||||
m_workspaces.erase(std::remove_if(m_workspaces.begin(), m_workspaces.end(),
|
||||
[&](const WorkspaceState& ws) {
|
||||
return (id.has_value() && ws.id == *id) ||
|
||||
(!name.empty() && ws.name == name);
|
||||
return (id.has_value() && ws.id == *id) || (!name.empty() && ws.name == name);
|
||||
}),
|
||||
m_workspaces.end());
|
||||
|
||||
@@ -840,8 +839,8 @@ std::string HyprlandWorkspaceBackend::quoteCommandArg(const std::string& value)
|
||||
}
|
||||
|
||||
Workspace HyprlandWorkspaceBackend::toWorkspace(const WorkspaceState& state) {
|
||||
const std::uint32_t coord = state.id >= 0 ? static_cast<std::uint32_t>(state.id - 1)
|
||||
: static_cast<std::uint32_t>(state.ordinal);
|
||||
const std::uint32_t coord =
|
||||
state.id >= 0 ? static_cast<std::uint32_t>(state.id - 1) : static_cast<std::uint32_t>(state.ordinal);
|
||||
return Workspace{
|
||||
.id = !state.name.empty() ? state.name : std::to_string(state.id),
|
||||
.name = state.name,
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
#include "compositors/workspace_backend.h"
|
||||
|
||||
#include <json.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <json.hpp>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
#include "compositors/mango/mango_keyboard_backend.h"
|
||||
|
||||
#include "core/process.h"
|
||||
#include "dwl-ipc-unstable-v2-client-protocol.h"
|
||||
#include "wayland-client-core.h"
|
||||
#include "wayland-client-protocol.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "dwl-ipc-unstable-v2-client-protocol.h"
|
||||
#include "wayland-client-core.h"
|
||||
#include "wayland-client-protocol.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kSyncTtl = std::chrono::milliseconds(75);
|
||||
@@ -111,9 +110,7 @@ MangoKeyboardBackend::MangoKeyboardBackend(std::string_view compositorHint) {
|
||||
|
||||
MangoKeyboardBackend::~MangoKeyboardBackend() { cleanup(); }
|
||||
|
||||
bool MangoKeyboardBackend::isAvailable() const noexcept {
|
||||
return syncState() && preferredOutputState() != nullptr;
|
||||
}
|
||||
bool MangoKeyboardBackend::isAvailable() const noexcept { return syncState() && preferredOutputState() != nullptr; }
|
||||
|
||||
bool MangoKeyboardBackend::cycleLayout() const {
|
||||
if (!m_enabled) {
|
||||
@@ -179,7 +176,8 @@ void MangoKeyboardBackend::ensureConnected() const {
|
||||
}
|
||||
|
||||
m_registry = wl_display_get_registry(m_display);
|
||||
if (m_registry == nullptr || wl_registry_add_listener(m_registry, &kRegistryListener, const_cast<MangoKeyboardBackend*>(this)) != 0) {
|
||||
if (m_registry == nullptr ||
|
||||
wl_registry_add_listener(m_registry, &kRegistryListener, const_cast<MangoKeyboardBackend*>(this)) != 0) {
|
||||
const_cast<MangoKeyboardBackend*>(this)->cleanup();
|
||||
m_failed = true;
|
||||
return;
|
||||
@@ -262,12 +260,14 @@ void MangoKeyboardBackend::onRegistryGlobal(std::uint32_t name, const char* inte
|
||||
return;
|
||||
}
|
||||
|
||||
if (std::string_view(interfaceName) != zdwl_ipc_manager_v2_interface.name || m_registry == nullptr || m_manager != nullptr) {
|
||||
if (std::string_view(interfaceName) != zdwl_ipc_manager_v2_interface.name || m_registry == nullptr ||
|
||||
m_manager != nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto bindVersion = std::min<std::uint32_t>(version, 2);
|
||||
m_manager = static_cast<zdwl_ipc_manager_v2*>(wl_registry_bind(m_registry, name, &zdwl_ipc_manager_v2_interface, bindVersion));
|
||||
m_manager = static_cast<zdwl_ipc_manager_v2*>(
|
||||
wl_registry_bind(m_registry, name, &zdwl_ipc_manager_v2_interface, bindVersion));
|
||||
if (m_manager == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -349,9 +349,8 @@ void MangoKeyboardBackend::bindOutput(std::uint32_t name) {
|
||||
}
|
||||
|
||||
m_outputsByName.emplace(name, output);
|
||||
m_outputs.try_emplace(output,
|
||||
OutputState{.pending = {}, .output = output, .handle = nullptr, .active = false,
|
||||
.keyboardLayout = {}});
|
||||
m_outputs.try_emplace(
|
||||
output, OutputState{.pending = {}, .output = output, .handle = nullptr, .active = false, .keyboardLayout = {}});
|
||||
bindOutputHandle(output);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
#include "compositors/mango/mango_workspace_backend.h"
|
||||
|
||||
#include "core/log.h"
|
||||
#include "dwl-ipc-unstable-v2-client-protocol.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <string>
|
||||
|
||||
#include "dwl-ipc-unstable-v2-client-protocol.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("workspace_mango");
|
||||
@@ -230,8 +229,7 @@ void MangoWorkspaceBackend::onOutputTag(zdwl_ipc_output_v2* handle, std::uint32_
|
||||
tagInfo.occupied = clients > 0;
|
||||
kLog.debug("tag event output={} protocol_tag={} active={} urgent={} occupied={} total_tags={} snapshot={}",
|
||||
static_cast<const void*>(output->second.output), tag + 1, tagInfo.active ? "yes" : "no",
|
||||
tagInfo.urgent ? "yes" : "no", tagInfo.occupied ? "yes" : "no", m_tagCount,
|
||||
summarizeTags(output->second));
|
||||
tagInfo.urgent ? "yes" : "no", tagInfo.occupied ? "yes" : "no", m_tagCount, summarizeTags(output->second));
|
||||
}
|
||||
|
||||
void MangoWorkspaceBackend::onOutputFrame(zdwl_ipc_output_v2* handle) {
|
||||
|
||||
@@ -14,103 +14,103 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("niri_output");
|
||||
constexpr Logger kLog("niri_output");
|
||||
|
||||
[[nodiscard]] bool containsToken(std::string_view haystack, std::string_view needle) {
|
||||
if (haystack.empty() || needle.empty()) {
|
||||
return false;
|
||||
}
|
||||
std::string lhs(haystack);
|
||||
std::string rhs(needle);
|
||||
std::ranges::transform(lhs, lhs.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
std::ranges::transform(rhs, rhs.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return lhs.find(rhs) != std::string::npos;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string trim(std::string value) {
|
||||
auto notSpace = [](unsigned char c) { return !std::isspace(c); };
|
||||
const auto begin = std::find_if(value.begin(), value.end(), notSpace);
|
||||
if (begin == value.end()) {
|
||||
return {};
|
||||
}
|
||||
const auto end = std::find_if(value.rbegin(), value.rend(), notSpace).base();
|
||||
return std::string(begin, end);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string readCommand(const char* cmd) {
|
||||
FILE* pipe = ::popen(cmd, "r");
|
||||
if (pipe == nullptr) {
|
||||
return {};
|
||||
[[nodiscard]] bool containsToken(std::string_view haystack, std::string_view needle) {
|
||||
if (haystack.empty() || needle.empty()) {
|
||||
return false;
|
||||
}
|
||||
std::string lhs(haystack);
|
||||
std::string rhs(needle);
|
||||
std::ranges::transform(lhs, lhs.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
std::ranges::transform(rhs, rhs.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return lhs.find(rhs) != std::string::npos;
|
||||
}
|
||||
|
||||
std::array<char, 512> buffer{};
|
||||
std::string output;
|
||||
while (std::fgets(buffer.data(), static_cast<int>(buffer.size()), pipe) != nullptr) {
|
||||
output += buffer.data();
|
||||
}
|
||||
::pclose(pipe);
|
||||
return trim(output);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<std::string> parseFocusedOutputName(std::string_view payload) {
|
||||
if (payload.empty()) {
|
||||
return std::nullopt;
|
||||
[[nodiscard]] std::string trim(std::string value) {
|
||||
auto notSpace = [](unsigned char c) { return !std::isspace(c); };
|
||||
const auto begin = std::find_if(value.begin(), value.end(), notSpace);
|
||||
if (begin == value.end()) {
|
||||
return {};
|
||||
}
|
||||
const auto end = std::find_if(value.rbegin(), value.rend(), notSpace).base();
|
||||
return std::string(begin, end);
|
||||
}
|
||||
|
||||
try {
|
||||
const auto json = nlohmann::json::parse(payload);
|
||||
|
||||
if (json.is_string()) {
|
||||
const auto value = trim(json.get<std::string>());
|
||||
return value.empty() ? std::nullopt : std::optional<std::string>{value};
|
||||
[[nodiscard]] std::string readCommand(const char* cmd) {
|
||||
FILE* pipe = ::popen(cmd, "r");
|
||||
if (pipe == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (json.is_object()) {
|
||||
if (auto it = json.find("name"); it != json.end() && it->is_string()) {
|
||||
const auto value = trim(it->get<std::string>());
|
||||
if (!value.empty()) {
|
||||
return value;
|
||||
}
|
||||
std::array<char, 512> buffer{};
|
||||
std::string output;
|
||||
while (std::fgets(buffer.data(), static_cast<int>(buffer.size()), pipe) != nullptr) {
|
||||
output += buffer.data();
|
||||
}
|
||||
::pclose(pipe);
|
||||
return trim(output);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<std::string> parseFocusedOutputName(std::string_view payload) {
|
||||
if (payload.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
try {
|
||||
const auto json = nlohmann::json::parse(payload);
|
||||
|
||||
if (json.is_string()) {
|
||||
const auto value = trim(json.get<std::string>());
|
||||
return value.empty() ? std::nullopt : std::optional<std::string>{value};
|
||||
}
|
||||
if (auto it = json.find("output"); it != json.end()) {
|
||||
if (it->is_string()) {
|
||||
|
||||
if (json.is_object()) {
|
||||
if (auto it = json.find("name"); it != json.end() && it->is_string()) {
|
||||
const auto value = trim(it->get<std::string>());
|
||||
if (!value.empty()) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
if (it->is_object()) {
|
||||
if (auto nameIt = it->find("name"); nameIt != it->end() && nameIt->is_string()) {
|
||||
const auto value = trim(nameIt->get<std::string>());
|
||||
if (auto it = json.find("output"); it != json.end()) {
|
||||
if (it->is_string()) {
|
||||
const auto value = trim(it->get<std::string>());
|
||||
if (!value.empty()) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
if (it->is_object()) {
|
||||
if (auto nameIt = it->find("name"); nameIt != it->end() && nameIt->is_string()) {
|
||||
const auto value = trim(nameIt->get<std::string>());
|
||||
if (!value.empty()) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (json.is_array()) {
|
||||
for (const auto& item : json) {
|
||||
if (!item.is_object()) {
|
||||
continue;
|
||||
}
|
||||
if (auto it = item.find("name"); it != item.end() && it->is_string()) {
|
||||
const auto value = trim(it->get<std::string>());
|
||||
if (!value.empty()) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (const nlohmann::json::exception&) {
|
||||
const auto value = trim(std::string(payload));
|
||||
return value.empty() ? std::nullopt : std::optional<std::string>{value};
|
||||
}
|
||||
|
||||
if (json.is_array()) {
|
||||
for (const auto& item : json) {
|
||||
if (!item.is_object()) {
|
||||
continue;
|
||||
}
|
||||
if (auto it = item.find("name"); it != item.end() && it->is_string()) {
|
||||
const auto value = trim(it->get<std::string>());
|
||||
if (!value.empty()) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (const nlohmann::json::exception&) {
|
||||
const auto value = trim(std::string(payload));
|
||||
return value.empty() ? std::nullopt : std::optional<std::string>{value};
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NiriOutputBackend::NiriOutputBackend(std::string_view compositorHint) {
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <cerrno>
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <charconv>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <json.hpp>
|
||||
@@ -16,73 +16,73 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("niri_workspace");
|
||||
constexpr auto kReconnectDelay = std::chrono::seconds(2);
|
||||
constexpr std::string_view kEventStreamRequest = "\"EventStream\"\n";
|
||||
constexpr Logger kLog("niri_workspace");
|
||||
constexpr auto kReconnectDelay = std::chrono::seconds(2);
|
||||
constexpr std::string_view kEventStreamRequest = "\"EventStream\"\n";
|
||||
|
||||
[[nodiscard]] bool containsToken(std::string_view haystack, std::string_view needle) {
|
||||
if (haystack.empty() || needle.empty()) {
|
||||
return false;
|
||||
}
|
||||
std::string lhs(haystack);
|
||||
std::string rhs(needle);
|
||||
std::ranges::transform(lhs, lhs.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
std::ranges::transform(rhs, rhs.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return lhs.find(rhs) != std::string::npos;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<std::uint64_t> jsonUnsigned(const nlohmann::json& json) {
|
||||
if (json.is_number_unsigned()) {
|
||||
return json.get<std::uint64_t>();
|
||||
}
|
||||
if (json.is_number_integer()) {
|
||||
const auto value = json.get<std::int64_t>();
|
||||
if (value >= 0) {
|
||||
return static_cast<std::uint64_t>(value);
|
||||
[[nodiscard]] bool containsToken(std::string_view haystack, std::string_view needle) {
|
||||
if (haystack.empty() || needle.empty()) {
|
||||
return false;
|
||||
}
|
||||
std::string lhs(haystack);
|
||||
std::string rhs(needle);
|
||||
std::ranges::transform(lhs, lhs.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
std::ranges::transform(rhs, rhs.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return lhs.find(rhs) != std::string::npos;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<std::uint64_t> jsonOptionalUnsigned(const nlohmann::json& json, const char* key) {
|
||||
const auto it = json.find(key);
|
||||
if (it == json.end() || it->is_null()) {
|
||||
[[nodiscard]] std::optional<std::uint64_t> jsonUnsigned(const nlohmann::json& json) {
|
||||
if (json.is_number_unsigned()) {
|
||||
return json.get<std::uint64_t>();
|
||||
}
|
||||
if (json.is_number_integer()) {
|
||||
const auto value = json.get<std::int64_t>();
|
||||
if (value >= 0) {
|
||||
return static_cast<std::uint64_t>(value);
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
return jsonUnsigned(*it);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string jsonOptionalString(const nlohmann::json& json, const char* key) {
|
||||
const auto it = json.find(key);
|
||||
if (it == json.end() || !it->is_string()) {
|
||||
return {};
|
||||
}
|
||||
return it->get<std::string>();
|
||||
}
|
||||
|
||||
[[nodiscard]] const nlohmann::json* arrayPayload(const nlohmann::json& payload, const char* key) {
|
||||
if (payload.is_array()) {
|
||||
return &payload;
|
||||
}
|
||||
const auto it = payload.find(key);
|
||||
if (payload.is_object() && it != payload.end() && it->is_array()) {
|
||||
return &(*it);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
[[nodiscard]] const nlohmann::json* objectPayload(const nlohmann::json& payload, const char* key) {
|
||||
if (payload.is_object()) {
|
||||
const auto it = payload.find(key);
|
||||
if (it != payload.end() && it->is_object()) {
|
||||
return &(*it);
|
||||
[[nodiscard]] std::optional<std::uint64_t> jsonOptionalUnsigned(const nlohmann::json& json, const char* key) {
|
||||
const auto it = json.find(key);
|
||||
if (it == json.end() || it->is_null()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (payload.contains("id")) {
|
||||
return jsonUnsigned(*it);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string jsonOptionalString(const nlohmann::json& json, const char* key) {
|
||||
const auto it = json.find(key);
|
||||
if (it == json.end() || !it->is_string()) {
|
||||
return {};
|
||||
}
|
||||
return it->get<std::string>();
|
||||
}
|
||||
|
||||
[[nodiscard]] const nlohmann::json* arrayPayload(const nlohmann::json& payload, const char* key) {
|
||||
if (payload.is_array()) {
|
||||
return &payload;
|
||||
}
|
||||
const auto it = payload.find(key);
|
||||
if (payload.is_object() && it != payload.end() && it->is_array()) {
|
||||
return &(*it);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
[[nodiscard]] const nlohmann::json* objectPayload(const nlohmann::json& payload, const char* key) {
|
||||
if (payload.is_object()) {
|
||||
const auto it = payload.find(key);
|
||||
if (it != payload.end() && it->is_object()) {
|
||||
return &(*it);
|
||||
}
|
||||
if (payload.contains("id")) {
|
||||
return &payload;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -111,9 +111,9 @@ int NiriWorkspaceMonitor::pollTimeoutMs() const noexcept {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(m_nextReconnectAt -
|
||||
std::chrono::steady_clock::now())
|
||||
.count();
|
||||
const auto remaining =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(m_nextReconnectAt - std::chrono::steady_clock::now())
|
||||
.count();
|
||||
return static_cast<int>(std::max<std::int64_t>(0, remaining));
|
||||
}
|
||||
|
||||
@@ -189,13 +189,11 @@ void NiriWorkspaceMonitor::apply(std::vector<Workspace>& workspaces, const std::
|
||||
matches[i] = pickCandidate([&](const WorkspaceState& candidate) { return candidate.id == *parsedId; });
|
||||
}
|
||||
if (matches[i] == nullptr && !workspaces[i].name.empty()) {
|
||||
matches[i] =
|
||||
pickCandidate([&](const WorkspaceState& candidate) { return candidate.name == workspaces[i].name; });
|
||||
matches[i] = pickCandidate([&](const WorkspaceState& candidate) { return candidate.name == workspaces[i].name; });
|
||||
}
|
||||
if (matches[i] == nullptr && parsedIndex.has_value()) {
|
||||
matches[i] = pickCandidate([&](const WorkspaceState& candidate) {
|
||||
return static_cast<std::size_t>(candidate.idx) == *parsedIndex;
|
||||
});
|
||||
matches[i] = pickCandidate(
|
||||
[&](const WorkspaceState& candidate) { return static_cast<std::size_t>(candidate.idx) == *parsedIndex; });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +291,9 @@ void NiriWorkspaceMonitor::closeSocket(bool scheduleReconnectFlag) {
|
||||
}
|
||||
}
|
||||
|
||||
void NiriWorkspaceMonitor::scheduleReconnect() { m_nextReconnectAt = std::chrono::steady_clock::now() + kReconnectDelay; }
|
||||
void NiriWorkspaceMonitor::scheduleReconnect() {
|
||||
m_nextReconnectAt = std::chrono::steady_clock::now() + kReconnectDelay;
|
||||
}
|
||||
|
||||
void NiriWorkspaceMonitor::readSocket() {
|
||||
std::array<char, 4096> buffer{};
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
#include "compositors/workspace_backend.h"
|
||||
|
||||
#include <json.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <json.hpp>
|
||||
#include <optional>
|
||||
#include <poll.h>
|
||||
#include <string>
|
||||
@@ -60,8 +59,7 @@ private:
|
||||
[[nodiscard]] bool handleWindowOpenedOrChanged(const nlohmann::json& payload);
|
||||
[[nodiscard]] bool handleWindowClosed(const nlohmann::json& payload);
|
||||
[[nodiscard]] static std::optional<WorkspaceState> parseWorkspace(const nlohmann::json& json);
|
||||
[[nodiscard]] static std::optional<std::pair<std::uint64_t, WindowState>>
|
||||
parseWindow(const nlohmann::json& json);
|
||||
[[nodiscard]] static std::optional<std::pair<std::uint64_t, WindowState>> parseWindow(const nlohmann::json& json);
|
||||
[[nodiscard]] static std::optional<std::uint64_t> parseUnsigned(const std::string& value);
|
||||
[[nodiscard]] static std::optional<std::size_t> parseLeadingNumber(const std::string& value);
|
||||
void recomputeOccupancy();
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/toml.h"
|
||||
#include "ui/palette.h"
|
||||
#include "ui/style.h"
|
||||
|
||||
#include "core/toml.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "core/ui_phase.h"
|
||||
|
||||
#include "core/log.h"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
@@ -13,17 +13,17 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("bluetooth");
|
||||
constexpr Logger kLog("bluetooth");
|
||||
|
||||
const sdbus::ServiceName k_bluezBusName{"org.bluez"};
|
||||
const sdbus::ObjectPath k_bluezRoot{"/org/bluez"};
|
||||
const sdbus::ObjectPath k_agentObjectPath{"/org/noctalia/BluetoothAgent"};
|
||||
constexpr auto k_agentInterface = "org.bluez.Agent1";
|
||||
constexpr auto k_agentManagerInterface = "org.bluez.AgentManager1";
|
||||
constexpr auto k_capability = "KeyboardDisplay";
|
||||
const sdbus::ServiceName k_bluezBusName{"org.bluez"};
|
||||
const sdbus::ObjectPath k_bluezRoot{"/org/bluez"};
|
||||
const sdbus::ObjectPath k_agentObjectPath{"/org/noctalia/BluetoothAgent"};
|
||||
constexpr auto k_agentInterface = "org.bluez.Agent1";
|
||||
constexpr auto k_agentManagerInterface = "org.bluez.AgentManager1";
|
||||
constexpr auto k_capability = "KeyboardDisplay";
|
||||
|
||||
const sdbus::Error::Name k_errRejected{"org.bluez.Error.Rejected"};
|
||||
const sdbus::Error::Name k_errCanceled{"org.bluez.Error.Canceled"};
|
||||
const sdbus::Error::Name k_errRejected{"org.bluez.Error.Rejected"};
|
||||
const sdbus::Error::Name k_errCanceled{"org.bluez.Error.Canceled"};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -182,48 +182,45 @@ BluetoothAgent::BluetoothAgent(SystemBus& bus) : m_impl(std::make_unique<Impl>(b
|
||||
m_impl->object = sdbus::createObject(bus.connection(), k_agentObjectPath);
|
||||
|
||||
m_impl->object
|
||||
->addVTable(
|
||||
sdbus::registerMethod("Release").implementedAs([]() {}),
|
||||
sdbus::registerMethod("RequestPinCode")
|
||||
.withInputParamNames("device")
|
||||
.withOutputParamNames("pincode")
|
||||
.implementedAs([this](sdbus::Result<std::string>&& result, sdbus::ObjectPath device) {
|
||||
m_impl->onRequestPinCode(std::move(result), std::move(device));
|
||||
}),
|
||||
sdbus::registerMethod("DisplayPinCode")
|
||||
.withInputParamNames("device", "pincode")
|
||||
.implementedAs(
|
||||
[this](sdbus::Result<>&& result, sdbus::ObjectPath device, std::string pincode) {
|
||||
m_impl->onDisplayPinCode(std::move(result), std::move(device), std::move(pincode));
|
||||
}),
|
||||
sdbus::registerMethod("RequestPasskey")
|
||||
.withInputParamNames("device")
|
||||
.withOutputParamNames("passkey")
|
||||
.implementedAs([this](sdbus::Result<std::uint32_t>&& result, sdbus::ObjectPath device) {
|
||||
m_impl->onRequestPasskey(std::move(result), std::move(device));
|
||||
}),
|
||||
sdbus::registerMethod("DisplayPasskey")
|
||||
.withInputParamNames("device", "passkey", "entered")
|
||||
.implementedAs([this](sdbus::ObjectPath device, std::uint32_t passkey, std::uint16_t entered) {
|
||||
m_impl->onDisplayPasskey(std::move(device), passkey, entered);
|
||||
}),
|
||||
sdbus::registerMethod("RequestConfirmation")
|
||||
.withInputParamNames("device", "passkey")
|
||||
.implementedAs(
|
||||
[this](sdbus::Result<>&& result, sdbus::ObjectPath device, std::uint32_t passkey) {
|
||||
m_impl->onRequestConfirmation(std::move(result), std::move(device), passkey);
|
||||
}),
|
||||
sdbus::registerMethod("RequestAuthorization")
|
||||
.withInputParamNames("device")
|
||||
.implementedAs([this](sdbus::Result<>&& result, sdbus::ObjectPath device) {
|
||||
m_impl->onRequestAuthorization(std::move(result), std::move(device));
|
||||
}),
|
||||
sdbus::registerMethod("AuthorizeService")
|
||||
.withInputParamNames("device", "uuid")
|
||||
.implementedAs([this](sdbus::Result<>&& result, sdbus::ObjectPath device, std::string uuid) {
|
||||
m_impl->onAuthorizeService(std::move(result), std::move(device), std::move(uuid));
|
||||
}),
|
||||
sdbus::registerMethod("Cancel").implementedAs([this]() { m_impl->onCancel(); }))
|
||||
->addVTable(sdbus::registerMethod("Release").implementedAs([]() {}),
|
||||
sdbus::registerMethod("RequestPinCode")
|
||||
.withInputParamNames("device")
|
||||
.withOutputParamNames("pincode")
|
||||
.implementedAs([this](sdbus::Result<std::string>&& result, sdbus::ObjectPath device) {
|
||||
m_impl->onRequestPinCode(std::move(result), std::move(device));
|
||||
}),
|
||||
sdbus::registerMethod("DisplayPinCode")
|
||||
.withInputParamNames("device", "pincode")
|
||||
.implementedAs([this](sdbus::Result<>&& result, sdbus::ObjectPath device, std::string pincode) {
|
||||
m_impl->onDisplayPinCode(std::move(result), std::move(device), std::move(pincode));
|
||||
}),
|
||||
sdbus::registerMethod("RequestPasskey")
|
||||
.withInputParamNames("device")
|
||||
.withOutputParamNames("passkey")
|
||||
.implementedAs([this](sdbus::Result<std::uint32_t>&& result, sdbus::ObjectPath device) {
|
||||
m_impl->onRequestPasskey(std::move(result), std::move(device));
|
||||
}),
|
||||
sdbus::registerMethod("DisplayPasskey")
|
||||
.withInputParamNames("device", "passkey", "entered")
|
||||
.implementedAs([this](sdbus::ObjectPath device, std::uint32_t passkey, std::uint16_t entered) {
|
||||
m_impl->onDisplayPasskey(std::move(device), passkey, entered);
|
||||
}),
|
||||
sdbus::registerMethod("RequestConfirmation")
|
||||
.withInputParamNames("device", "passkey")
|
||||
.implementedAs([this](sdbus::Result<>&& result, sdbus::ObjectPath device, std::uint32_t passkey) {
|
||||
m_impl->onRequestConfirmation(std::move(result), std::move(device), passkey);
|
||||
}),
|
||||
sdbus::registerMethod("RequestAuthorization")
|
||||
.withInputParamNames("device")
|
||||
.implementedAs([this](sdbus::Result<>&& result, sdbus::ObjectPath device) {
|
||||
m_impl->onRequestAuthorization(std::move(result), std::move(device));
|
||||
}),
|
||||
sdbus::registerMethod("AuthorizeService")
|
||||
.withInputParamNames("device", "uuid")
|
||||
.implementedAs([this](sdbus::Result<>&& result, sdbus::ObjectPath device, std::string uuid) {
|
||||
m_impl->onAuthorizeService(std::move(result), std::move(device), std::move(uuid));
|
||||
}),
|
||||
sdbus::registerMethod("Cancel").implementedAs([this]() { m_impl->onCancel(); }))
|
||||
.forInterface(k_agentInterface);
|
||||
|
||||
try {
|
||||
@@ -247,9 +244,7 @@ BluetoothAgent::~BluetoothAgent() {
|
||||
m_impl->cancelPending();
|
||||
try {
|
||||
auto agentManager = sdbus::createProxy(m_impl->bus.connection(), k_bluezBusName, k_bluezRoot);
|
||||
agentManager->callMethod("UnregisterAgent")
|
||||
.onInterface(k_agentManagerInterface)
|
||||
.withArguments(k_agentObjectPath);
|
||||
agentManager->callMethod("UnregisterAgent").onInterface(k_agentManagerInterface).withArguments(k_agentObjectPath);
|
||||
} catch (const sdbus::Error& e) {
|
||||
kLog.debug("BlueZ agent unregister failed: {}", e.what());
|
||||
}
|
||||
@@ -299,9 +294,7 @@ void BluetoothAgent::cancelPending() {
|
||||
}
|
||||
}
|
||||
|
||||
bool BluetoothAgent::hasPendingRequest() const noexcept {
|
||||
return m_impl != nullptr && m_impl->hasPending();
|
||||
}
|
||||
bool BluetoothAgent::hasPendingRequest() const noexcept { return m_impl != nullptr && m_impl->hasPending(); }
|
||||
|
||||
BluetoothPairingRequest BluetoothAgent::pendingRequest() const {
|
||||
return m_impl != nullptr ? m_impl->pending : BluetoothPairingRequest{};
|
||||
|
||||
@@ -47,9 +47,9 @@ public:
|
||||
void setRequestCallback(RequestCallback callback);
|
||||
|
||||
// Reply paths — safe no-ops if nothing is pending.
|
||||
void acceptConfirm(); // Confirm / Authorize / AuthorizeService / DisplayPinCode / DisplayPasskey
|
||||
void acceptConfirm(); // Confirm / Authorize / AuthorizeService / DisplayPinCode / DisplayPasskey
|
||||
void rejectConfirm();
|
||||
void submitPin(const std::string& pin); // PinCode kind
|
||||
void submitPin(const std::string& pin); // PinCode kind
|
||||
void submitPasskey(std::uint32_t passkey); // Passkey kind
|
||||
void cancelPending();
|
||||
|
||||
|
||||
@@ -15,211 +15,210 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("bluetooth");
|
||||
constexpr Logger kLog("bluetooth");
|
||||
|
||||
const sdbus::ServiceName k_bluezBusName{"org.bluez"};
|
||||
const sdbus::ObjectPath k_rootPath{"/"};
|
||||
constexpr auto k_adapterInterface = "org.bluez.Adapter1";
|
||||
constexpr auto k_deviceInterface = "org.bluez.Device1";
|
||||
constexpr auto k_batteryInterface = "org.bluez.Battery1";
|
||||
constexpr auto k_objectManagerInterface = "org.freedesktop.DBus.ObjectManager";
|
||||
constexpr auto k_propertiesInterface = "org.freedesktop.DBus.Properties";
|
||||
const sdbus::ServiceName k_bluezBusName{"org.bluez"};
|
||||
const sdbus::ObjectPath k_rootPath{"/"};
|
||||
constexpr auto k_adapterInterface = "org.bluez.Adapter1";
|
||||
constexpr auto k_deviceInterface = "org.bluez.Device1";
|
||||
constexpr auto k_batteryInterface = "org.bluez.Battery1";
|
||||
constexpr auto k_objectManagerInterface = "org.freedesktop.DBus.ObjectManager";
|
||||
constexpr auto k_propertiesInterface = "org.freedesktop.DBus.Properties";
|
||||
|
||||
using InterfaceProps = std::map<std::string, sdbus::Variant>;
|
||||
using ObjectInterfaces = std::map<std::string, InterfaceProps>;
|
||||
using ManagedObjects = std::map<sdbus::ObjectPath, ObjectInterfaces>;
|
||||
using InterfaceProps = std::map<std::string, sdbus::Variant>;
|
||||
using ObjectInterfaces = std::map<std::string, InterfaceProps>;
|
||||
using ManagedObjects = std::map<sdbus::ObjectPath, ObjectInterfaces>;
|
||||
|
||||
template <typename T>
|
||||
std::optional<T> variantGet(const sdbus::Variant& value) {
|
||||
try {
|
||||
return value.get<T>();
|
||||
} catch (const sdbus::Error&) {
|
||||
return std::nullopt;
|
||||
template <typename T> std::optional<T> variantGet(const sdbus::Variant& value) {
|
||||
try {
|
||||
return value.get<T>();
|
||||
} catch (const sdbus::Error&) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BluetoothDeviceKind classifyIcon(std::string_view icon) {
|
||||
// See https://specifications.freedesktop.org/icon-naming-spec/latest/
|
||||
// BlueZ uses names like "audio-headset", "input-mouse", "phone", "computer".
|
||||
if (icon == "audio-headset") {
|
||||
return BluetoothDeviceKind::Headset;
|
||||
}
|
||||
if (icon == "audio-headphones") {
|
||||
return BluetoothDeviceKind::Headphones;
|
||||
}
|
||||
if (icon == "audio-card" || icon == "audio-speakers") {
|
||||
return BluetoothDeviceKind::Speaker;
|
||||
}
|
||||
if (icon == "input-mouse") {
|
||||
return BluetoothDeviceKind::Mouse;
|
||||
}
|
||||
if (icon == "input-keyboard") {
|
||||
return BluetoothDeviceKind::Keyboard;
|
||||
}
|
||||
if (icon == "input-gaming") {
|
||||
return BluetoothDeviceKind::Gamepad;
|
||||
}
|
||||
if (icon == "phone") {
|
||||
return BluetoothDeviceKind::Phone;
|
||||
}
|
||||
if (icon == "computer") {
|
||||
return BluetoothDeviceKind::Computer;
|
||||
}
|
||||
if (icon == "video-display") {
|
||||
return BluetoothDeviceKind::Tv;
|
||||
}
|
||||
return BluetoothDeviceKind::Unknown;
|
||||
}
|
||||
|
||||
BluetoothDeviceKind classifyClass(std::uint32_t cod) {
|
||||
// Bluetooth Class of Device: bits 8-12 are Major Device Class.
|
||||
const std::uint32_t major = (cod >> 8) & 0x1FU;
|
||||
const std::uint32_t minor = (cod >> 2) & 0x3FU;
|
||||
switch (major) {
|
||||
case 0x01: // Computer
|
||||
return BluetoothDeviceKind::Computer;
|
||||
case 0x02: // Phone
|
||||
return BluetoothDeviceKind::Phone;
|
||||
case 0x04: // Audio/Video
|
||||
switch (minor) {
|
||||
case 0x01: // Wearable headset
|
||||
case 0x02: // Hands-free
|
||||
BluetoothDeviceKind classifyIcon(std::string_view icon) {
|
||||
// See https://specifications.freedesktop.org/icon-naming-spec/latest/
|
||||
// BlueZ uses names like "audio-headset", "input-mouse", "phone", "computer".
|
||||
if (icon == "audio-headset") {
|
||||
return BluetoothDeviceKind::Headset;
|
||||
case 0x06:
|
||||
return BluetoothDeviceKind::Headphones;
|
||||
case 0x05: // Loudspeaker
|
||||
case 0x07: // Portable audio
|
||||
return BluetoothDeviceKind::Speaker;
|
||||
case 0x04:
|
||||
return BluetoothDeviceKind::Microphone;
|
||||
case 0x0A: // Video display/loudspeaker
|
||||
case 0x0B: // Video display/conferencing
|
||||
return BluetoothDeviceKind::Tv;
|
||||
default:
|
||||
}
|
||||
if (icon == "audio-headphones") {
|
||||
return BluetoothDeviceKind::Headphones;
|
||||
}
|
||||
case 0x05: // Peripheral
|
||||
switch (minor & 0x0FU) {
|
||||
case 0x01:
|
||||
return BluetoothDeviceKind::Keyboard;
|
||||
case 0x02:
|
||||
if (icon == "audio-card" || icon == "audio-speakers") {
|
||||
return BluetoothDeviceKind::Speaker;
|
||||
}
|
||||
if (icon == "input-mouse") {
|
||||
return BluetoothDeviceKind::Mouse;
|
||||
}
|
||||
if (icon == "input-keyboard") {
|
||||
return BluetoothDeviceKind::Keyboard;
|
||||
}
|
||||
if (icon == "input-gaming") {
|
||||
return BluetoothDeviceKind::Gamepad;
|
||||
}
|
||||
if (icon == "phone") {
|
||||
return BluetoothDeviceKind::Phone;
|
||||
}
|
||||
if (icon == "computer") {
|
||||
return BluetoothDeviceKind::Computer;
|
||||
}
|
||||
if (icon == "video-display") {
|
||||
return BluetoothDeviceKind::Tv;
|
||||
}
|
||||
return BluetoothDeviceKind::Unknown;
|
||||
}
|
||||
|
||||
BluetoothDeviceKind classifyClass(std::uint32_t cod) {
|
||||
// Bluetooth Class of Device: bits 8-12 are Major Device Class.
|
||||
const std::uint32_t major = (cod >> 8) & 0x1FU;
|
||||
const std::uint32_t minor = (cod >> 2) & 0x3FU;
|
||||
switch (major) {
|
||||
case 0x01: // Computer
|
||||
return BluetoothDeviceKind::Computer;
|
||||
case 0x02: // Phone
|
||||
return BluetoothDeviceKind::Phone;
|
||||
case 0x04: // Audio/Video
|
||||
switch (minor) {
|
||||
case 0x01: // Wearable headset
|
||||
case 0x02: // Hands-free
|
||||
return BluetoothDeviceKind::Headset;
|
||||
case 0x06:
|
||||
return BluetoothDeviceKind::Headphones;
|
||||
case 0x05: // Loudspeaker
|
||||
case 0x07: // Portable audio
|
||||
return BluetoothDeviceKind::Speaker;
|
||||
case 0x04:
|
||||
return BluetoothDeviceKind::Microphone;
|
||||
case 0x0A: // Video display/loudspeaker
|
||||
case 0x0B: // Video display/conferencing
|
||||
return BluetoothDeviceKind::Tv;
|
||||
default:
|
||||
return BluetoothDeviceKind::Headphones;
|
||||
}
|
||||
case 0x05: // Peripheral
|
||||
switch (minor & 0x0FU) {
|
||||
case 0x01:
|
||||
return BluetoothDeviceKind::Keyboard;
|
||||
case 0x02:
|
||||
return BluetoothDeviceKind::Mouse;
|
||||
default:
|
||||
return BluetoothDeviceKind::Unknown;
|
||||
}
|
||||
case 0x07: // Wearable
|
||||
return BluetoothDeviceKind::Watch;
|
||||
case 0x08: // Toy / Gamepad
|
||||
return BluetoothDeviceKind::Gamepad;
|
||||
default:
|
||||
return BluetoothDeviceKind::Unknown;
|
||||
}
|
||||
case 0x07: // Wearable
|
||||
return BluetoothDeviceKind::Watch;
|
||||
case 0x08: // Toy / Gamepad
|
||||
return BluetoothDeviceKind::Gamepad;
|
||||
default:
|
||||
return BluetoothDeviceKind::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
void readAdapterProps(const InterfaceProps& props, BluetoothState& out) {
|
||||
out.adapterPresent = true;
|
||||
if (auto it = props.find("Powered"); it != props.end()) {
|
||||
if (auto v = variantGet<bool>(it->second)) {
|
||||
out.powered = *v;
|
||||
void readAdapterProps(const InterfaceProps& props, BluetoothState& out) {
|
||||
out.adapterPresent = true;
|
||||
if (auto it = props.find("Powered"); it != props.end()) {
|
||||
if (auto v = variantGet<bool>(it->second)) {
|
||||
out.powered = *v;
|
||||
}
|
||||
}
|
||||
if (auto it = props.find("Discoverable"); it != props.end()) {
|
||||
if (auto v = variantGet<bool>(it->second)) {
|
||||
out.discoverable = *v;
|
||||
}
|
||||
}
|
||||
if (auto it = props.find("Pairable"); it != props.end()) {
|
||||
if (auto v = variantGet<bool>(it->second)) {
|
||||
out.pairable = *v;
|
||||
}
|
||||
}
|
||||
if (auto it = props.find("Discovering"); it != props.end()) {
|
||||
if (auto v = variantGet<bool>(it->second)) {
|
||||
out.discovering = *v;
|
||||
}
|
||||
}
|
||||
if (auto it = props.find("Alias"); it != props.end()) {
|
||||
if (auto v = variantGet<std::string>(it->second)) {
|
||||
out.adapterName = std::move(*v);
|
||||
}
|
||||
} else if (auto nameIt = props.find("Name"); nameIt != props.end()) {
|
||||
if (auto v = variantGet<std::string>(nameIt->second)) {
|
||||
out.adapterName = std::move(*v);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (auto it = props.find("Discoverable"); it != props.end()) {
|
||||
if (auto v = variantGet<bool>(it->second)) {
|
||||
out.discoverable = *v;
|
||||
}
|
||||
}
|
||||
if (auto it = props.find("Pairable"); it != props.end()) {
|
||||
if (auto v = variantGet<bool>(it->second)) {
|
||||
out.pairable = *v;
|
||||
}
|
||||
}
|
||||
if (auto it = props.find("Discovering"); it != props.end()) {
|
||||
if (auto v = variantGet<bool>(it->second)) {
|
||||
out.discovering = *v;
|
||||
}
|
||||
}
|
||||
if (auto it = props.find("Alias"); it != props.end()) {
|
||||
if (auto v = variantGet<std::string>(it->second)) {
|
||||
out.adapterName = std::move(*v);
|
||||
}
|
||||
} else if (auto nameIt = props.find("Name"); nameIt != props.end()) {
|
||||
if (auto v = variantGet<std::string>(nameIt->second)) {
|
||||
out.adapterName = std::move(*v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mergeDeviceProps(const InterfaceProps& props, BluetoothDeviceInfo& out) {
|
||||
if (auto it = props.find("Address"); it != props.end()) {
|
||||
if (auto v = variantGet<std::string>(it->second)) {
|
||||
out.address = std::move(*v);
|
||||
void mergeDeviceProps(const InterfaceProps& props, BluetoothDeviceInfo& out) {
|
||||
if (auto it = props.find("Address"); it != props.end()) {
|
||||
if (auto v = variantGet<std::string>(it->second)) {
|
||||
out.address = std::move(*v);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (auto it = props.find("Alias"); it != props.end()) {
|
||||
if (auto v = variantGet<std::string>(it->second)) {
|
||||
out.alias = std::move(*v);
|
||||
}
|
||||
}
|
||||
if (out.alias.empty()) {
|
||||
if (auto it = props.find("Name"); it != props.end()) {
|
||||
if (auto it = props.find("Alias"); it != props.end()) {
|
||||
if (auto v = variantGet<std::string>(it->second)) {
|
||||
out.alias = std::move(*v);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (auto it = props.find("Paired"); it != props.end()) {
|
||||
if (auto v = variantGet<bool>(it->second)) {
|
||||
out.paired = *v;
|
||||
}
|
||||
}
|
||||
if (auto it = props.find("Trusted"); it != props.end()) {
|
||||
if (auto v = variantGet<bool>(it->second)) {
|
||||
out.trusted = *v;
|
||||
}
|
||||
}
|
||||
if (auto it = props.find("Connected"); it != props.end()) {
|
||||
if (auto v = variantGet<bool>(it->second)) {
|
||||
out.connected = *v;
|
||||
if (out.connected) {
|
||||
out.connecting = false;
|
||||
if (out.alias.empty()) {
|
||||
if (auto it = props.find("Name"); it != props.end()) {
|
||||
if (auto v = variantGet<std::string>(it->second)) {
|
||||
out.alias = std::move(*v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (auto it = props.find("RSSI"); it != props.end()) {
|
||||
if (auto v = variantGet<std::int16_t>(it->second)) {
|
||||
out.rssi = *v;
|
||||
out.hasRssi = true;
|
||||
}
|
||||
}
|
||||
BluetoothDeviceKind kindFromIcon = BluetoothDeviceKind::Unknown;
|
||||
if (auto it = props.find("Icon"); it != props.end()) {
|
||||
if (auto v = variantGet<std::string>(it->second)) {
|
||||
kindFromIcon = classifyIcon(*v);
|
||||
}
|
||||
}
|
||||
if (kindFromIcon != BluetoothDeviceKind::Unknown) {
|
||||
out.kind = kindFromIcon;
|
||||
} else if (auto it = props.find("Class"); it != props.end()) {
|
||||
if (auto v = variantGet<std::uint32_t>(it->second)) {
|
||||
const auto kindFromCod = classifyClass(*v);
|
||||
if (kindFromCod != BluetoothDeviceKind::Unknown) {
|
||||
out.kind = kindFromCod;
|
||||
if (auto it = props.find("Paired"); it != props.end()) {
|
||||
if (auto v = variantGet<bool>(it->second)) {
|
||||
out.paired = *v;
|
||||
}
|
||||
}
|
||||
if (auto it = props.find("Trusted"); it != props.end()) {
|
||||
if (auto v = variantGet<bool>(it->second)) {
|
||||
out.trusted = *v;
|
||||
}
|
||||
}
|
||||
if (auto it = props.find("Connected"); it != props.end()) {
|
||||
if (auto v = variantGet<bool>(it->second)) {
|
||||
out.connected = *v;
|
||||
if (out.connected) {
|
||||
out.connecting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (auto it = props.find("RSSI"); it != props.end()) {
|
||||
if (auto v = variantGet<std::int16_t>(it->second)) {
|
||||
out.rssi = *v;
|
||||
out.hasRssi = true;
|
||||
}
|
||||
}
|
||||
BluetoothDeviceKind kindFromIcon = BluetoothDeviceKind::Unknown;
|
||||
if (auto it = props.find("Icon"); it != props.end()) {
|
||||
if (auto v = variantGet<std::string>(it->second)) {
|
||||
kindFromIcon = classifyIcon(*v);
|
||||
}
|
||||
}
|
||||
if (kindFromIcon != BluetoothDeviceKind::Unknown) {
|
||||
out.kind = kindFromIcon;
|
||||
} else if (auto it = props.find("Class"); it != props.end()) {
|
||||
if (auto v = variantGet<std::uint32_t>(it->second)) {
|
||||
const auto kindFromCod = classifyClass(*v);
|
||||
if (kindFromCod != BluetoothDeviceKind::Unknown) {
|
||||
out.kind = kindFromCod;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (out.alias.empty()) {
|
||||
out.alias = out.address.empty() ? std::string("Unknown device") : out.address;
|
||||
}
|
||||
}
|
||||
if (out.alias.empty()) {
|
||||
out.alias = out.address.empty() ? std::string("Unknown device") : out.address;
|
||||
}
|
||||
}
|
||||
|
||||
void mergeBatteryProps(const InterfaceProps& props, BluetoothDeviceInfo& out) {
|
||||
if (auto it = props.find("Percentage"); it != props.end()) {
|
||||
if (auto v = variantGet<std::uint8_t>(it->second)) {
|
||||
out.hasBattery = true;
|
||||
out.batteryPercent = *v;
|
||||
void mergeBatteryProps(const InterfaceProps& props, BluetoothDeviceInfo& out) {
|
||||
if (auto it = props.find("Percentage"); it != props.end()) {
|
||||
if (auto v = variantGet<std::uint8_t>(it->second)) {
|
||||
out.hasBattery = true;
|
||||
out.batteryPercent = *v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -613,8 +612,8 @@ void BluetoothService::forget(const std::string& devicePath) {
|
||||
}
|
||||
|
||||
BluetoothDeviceInfo* BluetoothService::findDevice(const std::string& path) {
|
||||
auto it = std::find_if(m_devices.begin(), m_devices.end(),
|
||||
[&](const BluetoothDeviceInfo& d) { return d.path == path; });
|
||||
auto it =
|
||||
std::find_if(m_devices.begin(), m_devices.end(), [&](const BluetoothDeviceInfo& d) { return d.path == path; });
|
||||
return it == m_devices.end() ? nullptr : &*it;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
class SystemBus;
|
||||
|
||||
namespace sdbus {
|
||||
class IProxy;
|
||||
class IProxy;
|
||||
}
|
||||
|
||||
enum class BluetoothDeviceKind : std::uint8_t {
|
||||
|
||||
+143
-144
@@ -30,177 +30,176 @@ std::string joinedArtists(const std::vector<std::string>& artists) {
|
||||
|
||||
namespace {
|
||||
|
||||
static constexpr auto k_dbus_interface = "org.freedesktop.DBus";
|
||||
static constexpr auto k_properties_interface = "org.freedesktop.DBus.Properties";
|
||||
static constexpr auto k_mpris_root_interface = "org.mpris.MediaPlayer2";
|
||||
static constexpr auto k_mpris_player_interface = "org.mpris.MediaPlayer2.Player";
|
||||
static constexpr auto k_noctalia_mpris_interface = "dev.noctalia.Mpris";
|
||||
static constexpr auto k_properties_debounce_window = std::chrono::milliseconds{120};
|
||||
static constexpr auto k_metadata_stabilize_window = std::chrono::milliseconds{900};
|
||||
static const sdbus::ServiceName k_dbus_name{"org.freedesktop.DBus"};
|
||||
static const sdbus::ObjectPath k_dbus_path{"/org/freedesktop/DBus"};
|
||||
static const sdbus::ObjectPath k_mpris_path{"/org/mpris/MediaPlayer2"};
|
||||
static const sdbus::ServiceName k_noctalia_mpris_bus_name{"dev.noctalia.Mpris"};
|
||||
static const sdbus::ObjectPath k_noctalia_mpris_object_path{"/dev/noctalia/Mpris"};
|
||||
static constexpr auto k_dbus_interface = "org.freedesktop.DBus";
|
||||
static constexpr auto k_properties_interface = "org.freedesktop.DBus.Properties";
|
||||
static constexpr auto k_mpris_root_interface = "org.mpris.MediaPlayer2";
|
||||
static constexpr auto k_mpris_player_interface = "org.mpris.MediaPlayer2.Player";
|
||||
static constexpr auto k_noctalia_mpris_interface = "dev.noctalia.Mpris";
|
||||
static constexpr auto k_properties_debounce_window = std::chrono::milliseconds{120};
|
||||
static constexpr auto k_metadata_stabilize_window = std::chrono::milliseconds{900};
|
||||
static const sdbus::ServiceName k_dbus_name{"org.freedesktop.DBus"};
|
||||
static const sdbus::ObjectPath k_dbus_path{"/org/freedesktop/DBus"};
|
||||
static const sdbus::ObjectPath k_mpris_path{"/org/mpris/MediaPlayer2"};
|
||||
static const sdbus::ServiceName k_noctalia_mpris_bus_name{"dev.noctalia.Mpris"};
|
||||
static const sdbus::ObjectPath k_noctalia_mpris_object_path{"/dev/noctalia/Mpris"};
|
||||
|
||||
bool is_mpris_bus_name(std::string_view name) { return name.starts_with("org.mpris.MediaPlayer2."); }
|
||||
bool is_mpris_bus_name(std::string_view name) { return name.starts_with("org.mpris.MediaPlayer2."); }
|
||||
|
||||
bool is_valid_loop_status(std::string_view loop_status) {
|
||||
return loop_status == "None" || loop_status == "Track" || loop_status == "Playlist";
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T get_property_or(sdbus::IProxy& proxy, std::string_view interface_name, std::string_view property_name, T fallback) {
|
||||
try {
|
||||
const sdbus::Variant value = proxy.getProperty(property_name).onInterface(interface_name);
|
||||
return value.get<T>();
|
||||
} catch (const sdbus::Error&) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
std::map<std::string, sdbus::Variant> get_metadata_or(sdbus::IProxy& proxy) {
|
||||
return get_property_or(proxy, k_mpris_player_interface, "Metadata", std::map<std::string, sdbus::Variant>{});
|
||||
}
|
||||
|
||||
std::string get_string_from_variant(const std::map<std::string, sdbus::Variant>& values, std::string_view key) {
|
||||
const auto it = values.find(std::string{key});
|
||||
if (it == values.end()) {
|
||||
return {};
|
||||
bool is_valid_loop_status(std::string_view loop_status) {
|
||||
return loop_status == "None" || loop_status == "Track" || loop_status == "Playlist";
|
||||
}
|
||||
|
||||
try {
|
||||
return it->second.get<std::string>();
|
||||
} catch (const sdbus::Error&) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
std::string get_object_path_from_variant(const std::map<std::string, sdbus::Variant>& values, std::string_view key) {
|
||||
const auto it = values.find(std::string{key});
|
||||
if (it == values.end()) {
|
||||
return {};
|
||||
template <typename T>
|
||||
T get_property_or(sdbus::IProxy& proxy, std::string_view interface_name, std::string_view property_name, T fallback) {
|
||||
try {
|
||||
const sdbus::Variant value = proxy.getProperty(property_name).onInterface(interface_name);
|
||||
return value.get<T>();
|
||||
} catch (const sdbus::Error&) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return it->second.get<sdbus::ObjectPath>();
|
||||
} catch (const sdbus::Error&) {
|
||||
std::map<std::string, sdbus::Variant> get_metadata_or(sdbus::IProxy& proxy) {
|
||||
return get_property_or(proxy, k_mpris_player_interface, "Metadata", std::map<std::string, sdbus::Variant>{});
|
||||
}
|
||||
|
||||
std::string get_string_from_variant(const std::map<std::string, sdbus::Variant>& values, std::string_view key) {
|
||||
const auto it = values.find(std::string{key});
|
||||
if (it == values.end()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
return it->second.get<std::string>();
|
||||
} catch (const sdbus::Error&) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> get_string_array_from_variant(const std::map<std::string, sdbus::Variant>& values,
|
||||
std::string_view key) {
|
||||
const auto it = values.find(std::string{key});
|
||||
if (it == values.end()) {
|
||||
return {};
|
||||
}
|
||||
std::string get_object_path_from_variant(const std::map<std::string, sdbus::Variant>& values, std::string_view key) {
|
||||
const auto it = values.find(std::string{key});
|
||||
if (it == values.end()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
return it->second.get<std::vector<std::string>>();
|
||||
} catch (const sdbus::Error&) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
int64_t get_int64_from_variant(const std::map<std::string, sdbus::Variant>& values, std::string_view key) {
|
||||
const auto it = values.find(std::string{key});
|
||||
if (it == values.end()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
return it->second.get<int64_t>();
|
||||
} catch (const sdbus::Error&) {
|
||||
try {
|
||||
const auto value = it->second.get<uint64_t>();
|
||||
return value > static_cast<uint64_t>(std::numeric_limits<int64_t>::max())
|
||||
? std::numeric_limits<int64_t>::max()
|
||||
: static_cast<int64_t>(value);
|
||||
return it->second.get<sdbus::ObjectPath>();
|
||||
} catch (const sdbus::Error&) {
|
||||
try {
|
||||
return static_cast<int64_t>(it->second.get<int32_t>());
|
||||
return it->second.get<std::string>();
|
||||
} catch (const sdbus::Error&) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> get_string_array_from_variant(const std::map<std::string, sdbus::Variant>& values,
|
||||
std::string_view key) {
|
||||
const auto it = values.find(std::string{key});
|
||||
if (it == values.end()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
return it->second.get<std::vector<std::string>>();
|
||||
} catch (const sdbus::Error&) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
int64_t get_int64_from_variant(const std::map<std::string, sdbus::Variant>& values, std::string_view key) {
|
||||
const auto it = values.find(std::string{key});
|
||||
if (it == values.end()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
return it->second.get<int64_t>();
|
||||
} catch (const sdbus::Error&) {
|
||||
try {
|
||||
const auto value = it->second.get<uint64_t>();
|
||||
return value > static_cast<uint64_t>(std::numeric_limits<int64_t>::max()) ? std::numeric_limits<int64_t>::max()
|
||||
: static_cast<int64_t>(value);
|
||||
} catch (const sdbus::Error&) {
|
||||
try {
|
||||
return static_cast<int64_t>(it->second.get<uint32_t>());
|
||||
return static_cast<int64_t>(it->second.get<int32_t>());
|
||||
} catch (const sdbus::Error&) {
|
||||
return 0;
|
||||
try {
|
||||
return static_cast<int64_t>(it->second.get<uint32_t>());
|
||||
} catch (const sdbus::Error&) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[[maybe_unused]] std::string primary_artist(const std::vector<std::string>& artists) {
|
||||
if (artists.empty()) {
|
||||
return {};
|
||||
}
|
||||
return artists.front();
|
||||
}
|
||||
|
||||
[[maybe_unused]] std::string joinKeys(const std::map<std::string, sdbus::Variant>& values) {
|
||||
std::string out;
|
||||
bool first = true;
|
||||
for (const auto& [key, _] : values) {
|
||||
if (!first) {
|
||||
out += ", ";
|
||||
[[maybe_unused]] std::string primary_artist(const std::vector<std::string>& artists) {
|
||||
if (artists.empty()) {
|
||||
return {};
|
||||
}
|
||||
first = false;
|
||||
out += key;
|
||||
return artists.front();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
[[maybe_unused]] std::string joinStrings(const std::vector<std::string>& values) {
|
||||
std::string out;
|
||||
bool first = true;
|
||||
for (const auto& value : values) {
|
||||
if (!first) {
|
||||
out += ", ";
|
||||
[[maybe_unused]] std::string joinKeys(const std::map<std::string, sdbus::Variant>& values) {
|
||||
std::string out;
|
||||
bool first = true;
|
||||
for (const auto& [key, _] : values) {
|
||||
if (!first) {
|
||||
out += ", ";
|
||||
}
|
||||
first = false;
|
||||
out += key;
|
||||
}
|
||||
first = false;
|
||||
out += value;
|
||||
return out;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool hasStrongNowPlayingMetadata(const MprisPlayerInfo& info) {
|
||||
// Track IDs/source URLs can exist during transient "loading" states where the
|
||||
// user-visible metadata is still placeholder-only (e.g. app identity + logo).
|
||||
// Treat metadata as strong only when actual now-playing fields are present.
|
||||
return !info.title.empty() || !info.artists.empty() || !info.album.empty();
|
||||
}
|
||||
[[maybe_unused]] std::string joinStrings(const std::vector<std::string>& values) {
|
||||
std::string out;
|
||||
bool first = true;
|
||||
for (const auto& value : values) {
|
||||
if (!first) {
|
||||
out += ", ";
|
||||
}
|
||||
first = false;
|
||||
out += value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::map<std::string, sdbus::Variant> to_dbus_player(const MprisPlayerInfo& info) {
|
||||
std::map<std::string, sdbus::Variant> player;
|
||||
player["bus_name"] = sdbus::Variant(info.busName);
|
||||
player["identity"] = sdbus::Variant(info.identity);
|
||||
player["desktop_entry"] = sdbus::Variant(info.desktopEntry);
|
||||
player["playback_status"] = sdbus::Variant(info.playbackStatus);
|
||||
player["track_id"] = sdbus::Variant(info.trackId);
|
||||
player["title"] = sdbus::Variant(info.title);
|
||||
player["artists"] = sdbus::Variant(info.artists);
|
||||
player["album"] = sdbus::Variant(info.album);
|
||||
player["source_url"] = sdbus::Variant(info.sourceUrl);
|
||||
player["art_url"] = sdbus::Variant(info.artUrl);
|
||||
player["loop_status"] = sdbus::Variant(info.loopStatus);
|
||||
player["shuffle"] = sdbus::Variant(info.shuffle);
|
||||
player["volume"] = sdbus::Variant(info.volume);
|
||||
player["position_us"] = sdbus::Variant(info.positionUs);
|
||||
player["length_us"] = sdbus::Variant(info.lengthUs);
|
||||
player["can_play"] = sdbus::Variant(info.canPlay);
|
||||
player["can_pause"] = sdbus::Variant(info.canPause);
|
||||
player["can_go_next"] = sdbus::Variant(info.canGoNext);
|
||||
player["can_go_previous"] = sdbus::Variant(info.canGoPrevious);
|
||||
player["can_seek"] = sdbus::Variant(info.canSeek);
|
||||
return player;
|
||||
}
|
||||
bool hasStrongNowPlayingMetadata(const MprisPlayerInfo& info) {
|
||||
// Track IDs/source URLs can exist during transient "loading" states where the
|
||||
// user-visible metadata is still placeholder-only (e.g. app identity + logo).
|
||||
// Treat metadata as strong only when actual now-playing fields are present.
|
||||
return !info.title.empty() || !info.artists.empty() || !info.album.empty();
|
||||
}
|
||||
|
||||
constexpr Logger kLog("mpris");
|
||||
std::map<std::string, sdbus::Variant> to_dbus_player(const MprisPlayerInfo& info) {
|
||||
std::map<std::string, sdbus::Variant> player;
|
||||
player["bus_name"] = sdbus::Variant(info.busName);
|
||||
player["identity"] = sdbus::Variant(info.identity);
|
||||
player["desktop_entry"] = sdbus::Variant(info.desktopEntry);
|
||||
player["playback_status"] = sdbus::Variant(info.playbackStatus);
|
||||
player["track_id"] = sdbus::Variant(info.trackId);
|
||||
player["title"] = sdbus::Variant(info.title);
|
||||
player["artists"] = sdbus::Variant(info.artists);
|
||||
player["album"] = sdbus::Variant(info.album);
|
||||
player["source_url"] = sdbus::Variant(info.sourceUrl);
|
||||
player["art_url"] = sdbus::Variant(info.artUrl);
|
||||
player["loop_status"] = sdbus::Variant(info.loopStatus);
|
||||
player["shuffle"] = sdbus::Variant(info.shuffle);
|
||||
player["volume"] = sdbus::Variant(info.volume);
|
||||
player["position_us"] = sdbus::Variant(info.positionUs);
|
||||
player["length_us"] = sdbus::Variant(info.lengthUs);
|
||||
player["can_play"] = sdbus::Variant(info.canPlay);
|
||||
player["can_pause"] = sdbus::Variant(info.canPause);
|
||||
player["can_go_next"] = sdbus::Variant(info.canGoNext);
|
||||
player["can_go_previous"] = sdbus::Variant(info.canGoPrevious);
|
||||
player["can_seek"] = sdbus::Variant(info.canSeek);
|
||||
return player;
|
||||
}
|
||||
|
||||
constexpr Logger kLog("mpris");
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -802,8 +801,8 @@ void MprisService::syncSignals(const std::optional<MprisPlayerInfo>& previousAct
|
||||
m_lastEmittedActivePlayer = current_active_name;
|
||||
}
|
||||
|
||||
if (previousActive.has_value() && current_active.has_value() &&
|
||||
previousActive->busName == current_active->busName && previousActive->title != current_active->title) {
|
||||
if (previousActive.has_value() && current_active.has_value() && previousActive->busName == current_active->busName &&
|
||||
previousActive->title != current_active->title) {
|
||||
emitTrackChanged(*current_active);
|
||||
}
|
||||
}
|
||||
@@ -923,9 +922,9 @@ void MprisService::addOrRefreshPlayer(const std::string& busName) {
|
||||
const MprisPlayerInfo info = readPlayerInfo(*proxyIt->second, busName);
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
// kLog.debug(
|
||||
// "queried player name={} identity=\"{}\" status=\"{}\" title=\"{}\" artist=\"{}\" track_id=\"{}\" art_url=\"{}\"",
|
||||
// info.busName, info.identity, info.playbackStatus, info.title, primary_artist(info.artists), info.trackId,
|
||||
// info.artUrl);
|
||||
// "queried player name={} identity=\"{}\" status=\"{}\" title=\"{}\" artist=\"{}\" track_id=\"{}\"
|
||||
// art_url=\"{}\"", info.busName, info.identity, info.playbackStatus, info.title, primary_artist(info.artists),
|
||||
// info.trackId, info.artUrl);
|
||||
if (info.artUrl.empty()) {
|
||||
const auto metadata = get_metadata_or(*proxyIt->second);
|
||||
// kLog.debug("queried player missing art url name={} metadata_keys=[{}]", info.busName, joinKeys(metadata));
|
||||
@@ -1070,7 +1069,7 @@ std::optional<std::string> MprisService::chooseActivePlayer() const {
|
||||
}
|
||||
}
|
||||
if (mostRecentPlaying.has_value()) {
|
||||
//kLog.debug("choose active player source=recent_playing name={}", *mostRecentPlaying);
|
||||
// kLog.debug("choose active player source=recent_playing name={}", *mostRecentPlaying);
|
||||
return mostRecentPlaying;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
#include <vector>
|
||||
|
||||
namespace sdbus {
|
||||
class IObject;
|
||||
class IProxy;
|
||||
class IObject;
|
||||
class IProxy;
|
||||
} // namespace sdbus
|
||||
|
||||
class SessionBus;
|
||||
|
||||
@@ -17,41 +17,41 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("network");
|
||||
constexpr Logger kLog("network");
|
||||
|
||||
using SecretsDict = std::map<std::string, std::map<std::string, sdbus::Variant>>;
|
||||
using SecretsDict = std::map<std::string, std::map<std::string, sdbus::Variant>>;
|
||||
|
||||
const sdbus::ServiceName k_agentBusName{"org.noctalia.SecretAgent"};
|
||||
const sdbus::ObjectPath k_agentObjectPath{"/org/freedesktop/NetworkManager/SecretAgent"};
|
||||
constexpr auto k_agentInterface = "org.freedesktop.NetworkManager.SecretAgent";
|
||||
constexpr auto k_agentIdentifier = "org.noctalia.SecretAgent";
|
||||
const sdbus::ServiceName k_agentBusName{"org.noctalia.SecretAgent"};
|
||||
const sdbus::ObjectPath k_agentObjectPath{"/org/freedesktop/NetworkManager/SecretAgent"};
|
||||
constexpr auto k_agentInterface = "org.freedesktop.NetworkManager.SecretAgent";
|
||||
constexpr auto k_agentIdentifier = "org.noctalia.SecretAgent";
|
||||
|
||||
const sdbus::ServiceName k_nmBusName{"org.freedesktop.NetworkManager"};
|
||||
const sdbus::ObjectPath k_agentManagerObjectPath{"/org/freedesktop/NetworkManager/AgentManager"};
|
||||
constexpr auto k_agentManagerInterface = "org.freedesktop.NetworkManager.AgentManager";
|
||||
const sdbus::ServiceName k_nmBusName{"org.freedesktop.NetworkManager"};
|
||||
const sdbus::ObjectPath k_agentManagerObjectPath{"/org/freedesktop/NetworkManager/AgentManager"};
|
||||
constexpr auto k_agentManagerInterface = "org.freedesktop.NetworkManager.AgentManager";
|
||||
|
||||
constexpr std::uint32_t k_nmSecretAgentGetSecretsFlagAllowInteraction = 0x1;
|
||||
constexpr std::uint32_t k_nmSecretAgentGetSecretsFlagAllowInteraction = 0x1;
|
||||
|
||||
constexpr auto k_wirelessSettingName = "802-11-wireless";
|
||||
constexpr auto k_wirelessSecuritySettingName = "802-11-wireless-security";
|
||||
constexpr auto k_pskKey = "psk";
|
||||
constexpr auto k_wirelessSettingName = "802-11-wireless";
|
||||
constexpr auto k_wirelessSecuritySettingName = "802-11-wireless-security";
|
||||
constexpr auto k_pskKey = "psk";
|
||||
|
||||
std::string extractSsid(const SecretsDict& connection) {
|
||||
auto wifiIt = connection.find(k_wirelessSettingName);
|
||||
if (wifiIt == connection.end()) {
|
||||
return {};
|
||||
std::string extractSsid(const SecretsDict& connection) {
|
||||
auto wifiIt = connection.find(k_wirelessSettingName);
|
||||
if (wifiIt == connection.end()) {
|
||||
return {};
|
||||
}
|
||||
auto ssidIt = wifiIt->second.find("ssid");
|
||||
if (ssidIt == wifiIt->second.end()) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const auto bytes = ssidIt->second.get<std::vector<std::uint8_t>>();
|
||||
return std::string(bytes.begin(), bytes.end());
|
||||
} catch (const sdbus::Error&) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
auto ssidIt = wifiIt->second.find("ssid");
|
||||
if (ssidIt == wifiIt->second.end()) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const auto bytes = ssidIt->second.get<std::vector<std::uint8_t>>();
|
||||
return std::string(bytes.begin(), bytes.end());
|
||||
} catch (const sdbus::Error&) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -154,7 +154,9 @@ NetworkSecretAgent::NetworkSecretAgent(SystemBus& bus) : m_impl(std::make_unique
|
||||
// Register with NM's agent manager.
|
||||
try {
|
||||
auto agentManager = sdbus::createProxy(bus.connection(), k_nmBusName, k_agentManagerObjectPath);
|
||||
agentManager->callMethod("Register").onInterface(k_agentManagerInterface).withArguments(std::string(k_agentIdentifier));
|
||||
agentManager->callMethod("Register")
|
||||
.onInterface(k_agentManagerInterface)
|
||||
.withArguments(std::string(k_agentIdentifier));
|
||||
kLog.info("registered NetworkManager secret agent as {}", k_agentIdentifier);
|
||||
} catch (const sdbus::Error& e) {
|
||||
kLog.warn("secret agent registration failed: {}", e.what());
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
class SystemBus;
|
||||
|
||||
namespace sdbus {
|
||||
class IObject;
|
||||
class IObject;
|
||||
}
|
||||
|
||||
// NetworkManager secret agent. Registers with org.freedesktop.NetworkManager.AgentManager
|
||||
|
||||
@@ -13,88 +13,87 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("network");
|
||||
constexpr Logger kLog("network");
|
||||
|
||||
const sdbus::ServiceName k_nmBusName{"org.freedesktop.NetworkManager"};
|
||||
const sdbus::ObjectPath k_nmObjectPath{"/org/freedesktop/NetworkManager"};
|
||||
constexpr auto k_nmInterface = "org.freedesktop.NetworkManager";
|
||||
constexpr auto k_nmDeviceInterface = "org.freedesktop.NetworkManager.Device";
|
||||
constexpr auto k_nmDeviceWirelessInterface = "org.freedesktop.NetworkManager.Device.Wireless";
|
||||
constexpr auto k_nmSettingsInterface = "org.freedesktop.NetworkManager.Settings";
|
||||
const sdbus::ObjectPath k_nmSettingsObjectPath{"/org/freedesktop/NetworkManager/Settings"};
|
||||
constexpr auto k_nmSettingsConnectionInterface = "org.freedesktop.NetworkManager.Settings.Connection";
|
||||
const sdbus::ServiceName k_nmBusName{"org.freedesktop.NetworkManager"};
|
||||
const sdbus::ObjectPath k_nmObjectPath{"/org/freedesktop/NetworkManager"};
|
||||
constexpr auto k_nmInterface = "org.freedesktop.NetworkManager";
|
||||
constexpr auto k_nmDeviceInterface = "org.freedesktop.NetworkManager.Device";
|
||||
constexpr auto k_nmDeviceWirelessInterface = "org.freedesktop.NetworkManager.Device.Wireless";
|
||||
constexpr auto k_nmSettingsInterface = "org.freedesktop.NetworkManager.Settings";
|
||||
const sdbus::ObjectPath k_nmSettingsObjectPath{"/org/freedesktop/NetworkManager/Settings"};
|
||||
constexpr auto k_nmSettingsConnectionInterface = "org.freedesktop.NetworkManager.Settings.Connection";
|
||||
|
||||
// NM80211ApSecurityFlags bits we care about.
|
||||
constexpr std::uint32_t k_nm80211ApSecNone = 0x0;
|
||||
constexpr auto k_nmActiveConnectionInterface = "org.freedesktop.NetworkManager.Connection.Active";
|
||||
constexpr auto k_nmAccessPointInterface = "org.freedesktop.NetworkManager.AccessPoint";
|
||||
constexpr auto k_nmIp4ConfigInterface = "org.freedesktop.NetworkManager.IP4Config";
|
||||
constexpr auto k_propertiesInterface = "org.freedesktop.DBus.Properties";
|
||||
// NM80211ApSecurityFlags bits we care about.
|
||||
constexpr std::uint32_t k_nm80211ApSecNone = 0x0;
|
||||
constexpr auto k_nmActiveConnectionInterface = "org.freedesktop.NetworkManager.Connection.Active";
|
||||
constexpr auto k_nmAccessPointInterface = "org.freedesktop.NetworkManager.AccessPoint";
|
||||
constexpr auto k_nmIp4ConfigInterface = "org.freedesktop.NetworkManager.IP4Config";
|
||||
constexpr auto k_propertiesInterface = "org.freedesktop.DBus.Properties";
|
||||
|
||||
// NMDeviceType values from NetworkManager D-Bus API.
|
||||
constexpr std::uint32_t k_nmDeviceTypeWifi = 2;
|
||||
// NMDeviceType values from NetworkManager D-Bus API.
|
||||
constexpr std::uint32_t k_nmDeviceTypeWifi = 2;
|
||||
|
||||
// NMActiveConnectionState
|
||||
constexpr std::uint32_t k_nmActiveConnectionStateActivated = 2;
|
||||
// NMActiveConnectionState
|
||||
constexpr std::uint32_t k_nmActiveConnectionStateActivated = 2;
|
||||
|
||||
template <typename T>
|
||||
T getPropertyOr(sdbus::IProxy& proxy, std::string_view interfaceName, std::string_view propertyName, T fallback) {
|
||||
try {
|
||||
const sdbus::Variant value = proxy.getProperty(propertyName).onInterface(std::string(interfaceName));
|
||||
return value.get<T>();
|
||||
} catch (const sdbus::Error&) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
std::string ipv4FromUint(std::uint32_t addrLe) {
|
||||
// NM stores IPv4 addresses as native-byte-order uint32 in network order bytes.
|
||||
// I.e. the bytes a.b.c.d are laid out in memory low->high as a,b,c,d.
|
||||
std::array<std::uint8_t, 4> bytes{};
|
||||
bytes[0] = static_cast<std::uint8_t>(addrLe & 0xffU);
|
||||
bytes[1] = static_cast<std::uint8_t>((addrLe >> 8) & 0xffU);
|
||||
bytes[2] = static_cast<std::uint8_t>((addrLe >> 16) & 0xffU);
|
||||
bytes[3] = static_cast<std::uint8_t>((addrLe >> 24) & 0xffU);
|
||||
char buf[32];
|
||||
std::snprintf(buf, sizeof(buf), "%u.%u.%u.%u", bytes[0], bytes[1], bytes[2], bytes[3]);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
std::string firstIpv4FromConfig(sdbus::IConnection& conn, const std::string& ip4ConfigPath) {
|
||||
if (ip4ConfigPath.empty() || ip4ConfigPath == "/") {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
auto proxy =
|
||||
sdbus::createProxy(conn, k_nmBusName, sdbus::ObjectPath{ip4ConfigPath});
|
||||
// Prefer "AddressData" (vector<dict<string,variant>>) since "Addresses" is deprecated.
|
||||
template <typename T>
|
||||
T getPropertyOr(sdbus::IProxy& proxy, std::string_view interfaceName, std::string_view propertyName, T fallback) {
|
||||
try {
|
||||
const sdbus::Variant value = proxy->getProperty("AddressData").onInterface(k_nmIp4ConfigInterface);
|
||||
const auto data = value.get<std::vector<std::map<std::string, sdbus::Variant>>>();
|
||||
for (const auto& entry : data) {
|
||||
auto it = entry.find("address");
|
||||
if (it != entry.end()) {
|
||||
try {
|
||||
return it->second.get<std::string>();
|
||||
} catch (const sdbus::Error&) {
|
||||
const sdbus::Variant value = proxy.getProperty(propertyName).onInterface(std::string(interfaceName));
|
||||
return value.get<T>();
|
||||
} catch (const sdbus::Error&) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
std::string ipv4FromUint(std::uint32_t addrLe) {
|
||||
// NM stores IPv4 addresses as native-byte-order uint32 in network order bytes.
|
||||
// I.e. the bytes a.b.c.d are laid out in memory low->high as a,b,c,d.
|
||||
std::array<std::uint8_t, 4> bytes{};
|
||||
bytes[0] = static_cast<std::uint8_t>(addrLe & 0xffU);
|
||||
bytes[1] = static_cast<std::uint8_t>((addrLe >> 8) & 0xffU);
|
||||
bytes[2] = static_cast<std::uint8_t>((addrLe >> 16) & 0xffU);
|
||||
bytes[3] = static_cast<std::uint8_t>((addrLe >> 24) & 0xffU);
|
||||
char buf[32];
|
||||
std::snprintf(buf, sizeof(buf), "%u.%u.%u.%u", bytes[0], bytes[1], bytes[2], bytes[3]);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
std::string firstIpv4FromConfig(sdbus::IConnection& conn, const std::string& ip4ConfigPath) {
|
||||
if (ip4ConfigPath.empty() || ip4ConfigPath == "/") {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
auto proxy = sdbus::createProxy(conn, k_nmBusName, sdbus::ObjectPath{ip4ConfigPath});
|
||||
// Prefer "AddressData" (vector<dict<string,variant>>) since "Addresses" is deprecated.
|
||||
try {
|
||||
const sdbus::Variant value = proxy->getProperty("AddressData").onInterface(k_nmIp4ConfigInterface);
|
||||
const auto data = value.get<std::vector<std::map<std::string, sdbus::Variant>>>();
|
||||
for (const auto& entry : data) {
|
||||
auto it = entry.find("address");
|
||||
if (it != entry.end()) {
|
||||
try {
|
||||
return it->second.get<std::string>();
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
// Fallback: legacy Addresses (vector<vector<uint32>> — addr, prefix, gateway).
|
||||
try {
|
||||
const sdbus::Variant value = proxy->getProperty("Addresses").onInterface(k_nmIp4ConfigInterface);
|
||||
const auto data = value.get<std::vector<std::vector<std::uint32_t>>>();
|
||||
if (!data.empty() && !data.front().empty()) {
|
||||
return ipv4FromUint(data.front().front());
|
||||
}
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
// Fallback: legacy Addresses (vector<vector<uint32>> — addr, prefix, gateway).
|
||||
try {
|
||||
const sdbus::Variant value = proxy->getProperty("Addresses").onInterface(k_nmIp4ConfigInterface);
|
||||
const auto data = value.get<std::vector<std::vector<std::uint32_t>>>();
|
||||
if (!data.empty() && !data.front().empty()) {
|
||||
return ipv4FromUint(data.front().front());
|
||||
}
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
} catch (const sdbus::Error&) {
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -137,8 +136,8 @@ NetworkService::NetworkService(SystemBus& bus) : m_bus(bus) {
|
||||
if (deviceType != k_nmDeviceTypeWifi) {
|
||||
continue;
|
||||
}
|
||||
const auto lastScan = getPropertyOr<std::int64_t>(*device, k_nmDeviceWirelessInterface, "LastScan",
|
||||
std::int64_t{0});
|
||||
const auto lastScan =
|
||||
getPropertyOr<std::int64_t>(*device, k_nmDeviceWirelessInterface, "LastScan", std::int64_t{0});
|
||||
if (lastScan > baseline) {
|
||||
baseline = lastScan;
|
||||
}
|
||||
@@ -442,12 +441,9 @@ void NetworkService::refreshAccessPoints() {
|
||||
info.ssid.assign(ssidBytes.begin(), ssidBytes.end());
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
info.strength =
|
||||
getPropertyOr<std::uint8_t>(*ap, k_nmAccessPointInterface, "Strength", std::uint8_t{0});
|
||||
const auto wpaFlags =
|
||||
getPropertyOr<std::uint32_t>(*ap, k_nmAccessPointInterface, "WpaFlags", 0U);
|
||||
const auto rsnFlags =
|
||||
getPropertyOr<std::uint32_t>(*ap, k_nmAccessPointInterface, "RsnFlags", 0U);
|
||||
info.strength = getPropertyOr<std::uint8_t>(*ap, k_nmAccessPointInterface, "Strength", std::uint8_t{0});
|
||||
const auto wpaFlags = getPropertyOr<std::uint32_t>(*ap, k_nmAccessPointInterface, "WpaFlags", 0U);
|
||||
const auto rsnFlags = getPropertyOr<std::uint32_t>(*ap, k_nmAccessPointInterface, "RsnFlags", 0U);
|
||||
info.secured = (wpaFlags != k_nm80211ApSecNone) || (rsnFlags != k_nm80211ApSecNone);
|
||||
if (info.ssid.empty()) {
|
||||
continue; // skip hidden networks for now
|
||||
@@ -531,7 +527,8 @@ void NetworkService::rebindActiveConnection() {
|
||||
std::string newDevicePath;
|
||||
if (m_activeConnection != nullptr) {
|
||||
try {
|
||||
const sdbus::Variant value = m_activeConnection->getProperty("Devices").onInterface(k_nmActiveConnectionInterface);
|
||||
const sdbus::Variant value =
|
||||
m_activeConnection->getProperty("Devices").onInterface(k_nmActiveConnectionInterface);
|
||||
const auto devices = value.get<std::vector<sdbus::ObjectPath>>();
|
||||
if (!devices.empty()) {
|
||||
newDevicePath = devices.front();
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
class SystemBus;
|
||||
|
||||
namespace sdbus {
|
||||
class IProxy;
|
||||
class IProxy;
|
||||
}
|
||||
|
||||
struct AccessPointInfo {
|
||||
|
||||
@@ -17,9 +17,8 @@ static const sdbus::ObjectPath k_object_path{"/org/freedesktop/Notifications"};
|
||||
static constexpr auto k_interface = "org.freedesktop.Notifications";
|
||||
|
||||
NotificationService::NotificationService(SessionBus& bus, NotificationManager& manager) : m_manager(manager) {
|
||||
m_manager.setActionInvokeCallback([this](uint32_t id, const std::string& actionKey) {
|
||||
emitActionInvoked(id, actionKey);
|
||||
});
|
||||
m_manager.setActionInvokeCallback(
|
||||
[this](uint32_t id, const std::string& actionKey) { emitActionInvoked(id, actionKey); });
|
||||
|
||||
bus.connection().requestName(k_bus_name);
|
||||
m_object = sdbus::createObject(bus.connection(), k_object_path);
|
||||
@@ -75,98 +74,96 @@ static constexpr int32_t k_min_timeout = -1;
|
||||
|
||||
namespace {
|
||||
|
||||
std::string clamp_str(std::string_view s) {
|
||||
const auto len = std::min(s.size(), k_max_string_len);
|
||||
return std::string{s.substr(0, len)};
|
||||
}
|
||||
|
||||
bool isBlankText(std::string_view text) {
|
||||
return text.empty() ||
|
||||
std::all_of(text.begin(), text.end(),
|
||||
[](unsigned char ch) { return std::isspace(ch) != 0; });
|
||||
}
|
||||
|
||||
std::vector<std::string> sanitize_actions(const std::vector<std::string>& actions) {
|
||||
static constexpr std::string_view kFallbackActionLabel = "Action";
|
||||
|
||||
std::vector<std::string> sanitized;
|
||||
sanitized.reserve(actions.size() - (actions.size() % 2));
|
||||
|
||||
for (size_t i = 0; i + 1 < actions.size(); i += 2) {
|
||||
std::string actionKey = clamp_str(actions[i]);
|
||||
std::string label = clamp_str(actions[i + 1]);
|
||||
|
||||
if (actionKey.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isBlankText(label)) {
|
||||
label = kFallbackActionLabel;
|
||||
}
|
||||
|
||||
sanitized.push_back(std::move(actionKey));
|
||||
sanitized.push_back(std::move(label));
|
||||
std::string clamp_str(std::string_view s) {
|
||||
const auto len = std::min(s.size(), k_max_string_len);
|
||||
return std::string{s.substr(0, len)};
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
using NotificationImageDataStruct =
|
||||
sdbus::Struct<std::int32_t, std::int32_t, std::int32_t, bool, std::int32_t, std::int32_t,
|
||||
std::vector<std::uint8_t>>;
|
||||
|
||||
std::optional<NotificationImageData> decode_image_data_variant(const sdbus::Variant& value) {
|
||||
try {
|
||||
const auto data = value.get<NotificationImageDataStruct>();
|
||||
NotificationImageData out;
|
||||
out.width = std::get<0>(data);
|
||||
out.height = std::get<1>(data);
|
||||
out.rowStride = std::get<2>(data);
|
||||
out.hasAlpha = std::get<3>(data);
|
||||
out.bitsPerSample = std::get<4>(data);
|
||||
out.channels = std::get<5>(data);
|
||||
out.data = std::get<6>(data);
|
||||
return out;
|
||||
} catch (const sdbus::Error&) {
|
||||
bool isBlankText(std::string_view text) {
|
||||
return text.empty() ||
|
||||
std::all_of(text.begin(), text.end(), [](unsigned char ch) { return std::isspace(ch) != 0; });
|
||||
}
|
||||
|
||||
try {
|
||||
const auto data = value.get<std::tuple<std::int32_t, std::int32_t, std::int32_t, bool, std::int32_t,
|
||||
std::int32_t, std::vector<std::uint8_t>>>();
|
||||
NotificationImageData out;
|
||||
out.width = std::get<0>(data);
|
||||
out.height = std::get<1>(data);
|
||||
out.rowStride = std::get<2>(data);
|
||||
out.hasAlpha = std::get<3>(data);
|
||||
out.bitsPerSample = std::get<4>(data);
|
||||
out.channels = std::get<5>(data);
|
||||
out.data = std::get<6>(data);
|
||||
return out;
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
std::vector<std::string> sanitize_actions(const std::vector<std::string>& actions) {
|
||||
static constexpr std::string_view kFallbackActionLabel = "Action";
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
std::vector<std::string> sanitized;
|
||||
sanitized.reserve(actions.size() - (actions.size() % 2));
|
||||
|
||||
std::optional<NotificationImageData> decode_image_hint(const std::map<std::string, sdbus::Variant>& hints,
|
||||
std::string* outSourceKey = nullptr) {
|
||||
for (const char* key : {"image-data", "image_data", "icon_data"}) {
|
||||
const auto it = hints.find(key);
|
||||
if (it == hints.end()) {
|
||||
continue;
|
||||
}
|
||||
for (size_t i = 0; i + 1 < actions.size(); i += 2) {
|
||||
std::string actionKey = clamp_str(actions[i]);
|
||||
std::string label = clamp_str(actions[i + 1]);
|
||||
|
||||
auto decoded = decode_image_data_variant(it->second);
|
||||
if (decoded.has_value()) {
|
||||
if (outSourceKey != nullptr) {
|
||||
*outSourceKey = key;
|
||||
if (actionKey.empty()) {
|
||||
continue;
|
||||
}
|
||||
return decoded;
|
||||
|
||||
if (isBlankText(label)) {
|
||||
label = kFallbackActionLabel;
|
||||
}
|
||||
|
||||
sanitized.push_back(std::move(actionKey));
|
||||
sanitized.push_back(std::move(label));
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
using NotificationImageDataStruct = sdbus::Struct<std::int32_t, std::int32_t, std::int32_t, bool, std::int32_t,
|
||||
std::int32_t, std::vector<std::uint8_t>>;
|
||||
|
||||
std::optional<NotificationImageData> decode_image_data_variant(const sdbus::Variant& value) {
|
||||
try {
|
||||
const auto data = value.get<NotificationImageDataStruct>();
|
||||
NotificationImageData out;
|
||||
out.width = std::get<0>(data);
|
||||
out.height = std::get<1>(data);
|
||||
out.rowStride = std::get<2>(data);
|
||||
out.hasAlpha = std::get<3>(data);
|
||||
out.bitsPerSample = std::get<4>(data);
|
||||
out.channels = std::get<5>(data);
|
||||
out.data = std::get<6>(data);
|
||||
return out;
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
|
||||
try {
|
||||
const auto data = value.get<std::tuple<std::int32_t, std::int32_t, std::int32_t, bool, std::int32_t, std::int32_t,
|
||||
std::vector<std::uint8_t>>>();
|
||||
NotificationImageData out;
|
||||
out.width = std::get<0>(data);
|
||||
out.height = std::get<1>(data);
|
||||
out.rowStride = std::get<2>(data);
|
||||
out.hasAlpha = std::get<3>(data);
|
||||
out.bitsPerSample = std::get<4>(data);
|
||||
out.channels = std::get<5>(data);
|
||||
out.data = std::get<6>(data);
|
||||
return out;
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<NotificationImageData> decode_image_hint(const std::map<std::string, sdbus::Variant>& hints,
|
||||
std::string* outSourceKey = nullptr) {
|
||||
for (const char* key : {"image-data", "image_data", "icon_data"}) {
|
||||
const auto it = hints.find(key);
|
||||
if (it == hints.end()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto decoded = decode_image_data_variant(it->second);
|
||||
if (decoded.has_value()) {
|
||||
if (outSourceKey != nullptr) {
|
||||
*outSourceKey = key;
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
+277
-279
@@ -18,8 +18,8 @@
|
||||
#include <sdbus-c++/MethodResult.h>
|
||||
#include <sdbus-c++/Types.h>
|
||||
#include <sdbus-c++/VTableItems.h>
|
||||
#include <string_view>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <tuple>
|
||||
@@ -28,249 +28,250 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("polkit");
|
||||
constexpr Logger kLog("polkit");
|
||||
|
||||
const sdbus::ServiceName k_authorityBusName{"org.freedesktop.PolicyKit1"};
|
||||
const sdbus::ObjectPath k_authorityObjectPath{"/org/freedesktop/PolicyKit1/Authority"};
|
||||
constexpr auto k_authorityInterface = "org.freedesktop.PolicyKit1.Authority";
|
||||
const sdbus::ServiceName k_logindBusName{"org.freedesktop.login1"};
|
||||
const sdbus::ObjectPath k_logindObjectPath{"/org/freedesktop/login1"};
|
||||
constexpr auto k_logindManagerInterface = "org.freedesktop.login1.Manager";
|
||||
constexpr auto k_logindSessionInterface = "org.freedesktop.login1.Session";
|
||||
const sdbus::ServiceName k_authorityBusName{"org.freedesktop.PolicyKit1"};
|
||||
const sdbus::ObjectPath k_authorityObjectPath{"/org/freedesktop/PolicyKit1/Authority"};
|
||||
constexpr auto k_authorityInterface = "org.freedesktop.PolicyKit1.Authority";
|
||||
const sdbus::ServiceName k_logindBusName{"org.freedesktop.login1"};
|
||||
const sdbus::ObjectPath k_logindObjectPath{"/org/freedesktop/login1"};
|
||||
constexpr auto k_logindManagerInterface = "org.freedesktop.login1.Manager";
|
||||
constexpr auto k_logindSessionInterface = "org.freedesktop.login1.Session";
|
||||
|
||||
const sdbus::ObjectPath k_agentObjectPath{"/org/noctalia/PolkitAuthenticationAgent"};
|
||||
constexpr auto k_agentInterface = "org.freedesktop.PolicyKit1.AuthenticationAgent";
|
||||
const sdbus::ObjectPath k_agentObjectPath{"/org/noctalia/PolkitAuthenticationAgent"};
|
||||
constexpr auto k_agentInterface = "org.freedesktop.PolicyKit1.AuthenticationAgent";
|
||||
|
||||
using Subject = sdbus::Struct<std::string, std::map<std::string, sdbus::Variant>>;
|
||||
using Identity = sdbus::Struct<std::string, std::map<std::string, sdbus::Variant>>;
|
||||
using IdentityDetails = std::map<std::string, sdbus::Variant>;
|
||||
using Subject = sdbus::Struct<std::string, std::map<std::string, sdbus::Variant>>;
|
||||
using Identity = sdbus::Struct<std::string, std::map<std::string, sdbus::Variant>>;
|
||||
using IdentityDetails = std::map<std::string, sdbus::Variant>;
|
||||
|
||||
std::string localeFromEnvironment() {
|
||||
if (const char* lang = std::getenv("LANG"); lang != nullptr && lang[0] != '\0') {
|
||||
return lang;
|
||||
}
|
||||
return "C";
|
||||
}
|
||||
|
||||
std::optional<std::string> usernameFromUid(std::uint32_t uid) {
|
||||
passwd pwd{};
|
||||
passwd* result = nullptr;
|
||||
std::array<char, 4096> buffer{};
|
||||
const int rc = getpwuid_r(static_cast<uid_t>(uid), &pwd, buffer.data(), buffer.size(), &result);
|
||||
if (rc != 0 || result == nullptr || result->pw_name == nullptr || result->pw_name[0] == '\0') {
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::string(result->pw_name);
|
||||
}
|
||||
|
||||
std::optional<std::string> identityUsername(const Identity& identity) {
|
||||
const std::string& kind = std::get<0>(identity);
|
||||
const IdentityDetails& details = std::get<1>(identity);
|
||||
if (kind != "unix-user") {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto nameIt = details.find("name");
|
||||
if (nameIt != details.end()) {
|
||||
try {
|
||||
const std::string name = nameIt->second.get<std::string>();
|
||||
if (!name.empty()) {
|
||||
return name;
|
||||
}
|
||||
} catch (const sdbus::Error&) {
|
||||
std::string localeFromEnvironment() {
|
||||
if (const char* lang = std::getenv("LANG"); lang != nullptr && lang[0] != '\0') {
|
||||
return lang;
|
||||
}
|
||||
return "C";
|
||||
}
|
||||
const auto uidIt = details.find("uid");
|
||||
if (uidIt != details.end()) {
|
||||
try {
|
||||
return usernameFromUid(uidIt->second.get<std::uint32_t>());
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool writeAll(int fd, std::string_view payload) {
|
||||
const char* cursor = payload.data();
|
||||
std::size_t remaining = payload.size();
|
||||
while (remaining > 0) {
|
||||
const ssize_t written = ::write(fd, cursor, remaining);
|
||||
if (written < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
cursor += static_cast<std::size_t>(written);
|
||||
remaining -= static_cast<std::size_t>(written);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<std::string> readLineNonBlocking(int fd, std::string& buffer) {
|
||||
char chunk[256];
|
||||
while (true) {
|
||||
const ssize_t n = ::read(fd, chunk, sizeof(chunk));
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
break;
|
||||
}
|
||||
std::optional<std::string> usernameFromUid(std::uint32_t uid) {
|
||||
passwd pwd{};
|
||||
passwd* result = nullptr;
|
||||
std::array<char, 4096> buffer{};
|
||||
const int rc = getpwuid_r(static_cast<uid_t>(uid), &pwd, buffer.data(), buffer.size(), &result);
|
||||
if (rc != 0 || result == nullptr || result->pw_name == nullptr || result->pw_name[0] == '\0') {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (n == 0) {
|
||||
if (buffer.empty()) {
|
||||
return std::string(result->pw_name);
|
||||
}
|
||||
|
||||
std::optional<std::string> identityUsername(const Identity& identity) {
|
||||
const std::string& kind = std::get<0>(identity);
|
||||
const IdentityDetails& details = std::get<1>(identity);
|
||||
if (kind != "unix-user") {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto nameIt = details.find("name");
|
||||
if (nameIt != details.end()) {
|
||||
try {
|
||||
const std::string name = nameIt->second.get<std::string>();
|
||||
if (!name.empty()) {
|
||||
return name;
|
||||
}
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
}
|
||||
const auto uidIt = details.find("uid");
|
||||
if (uidIt != details.end()) {
|
||||
try {
|
||||
return usernameFromUid(uidIt->second.get<std::uint32_t>());
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool writeAll(int fd, std::string_view payload) {
|
||||
const char* cursor = payload.data();
|
||||
std::size_t remaining = payload.size();
|
||||
while (remaining > 0) {
|
||||
const ssize_t written = ::write(fd, cursor, remaining);
|
||||
if (written < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
cursor += static_cast<std::size_t>(written);
|
||||
remaining -= static_cast<std::size_t>(written);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<std::string> readLineNonBlocking(int fd, std::string& buffer) {
|
||||
char chunk[256];
|
||||
while (true) {
|
||||
const ssize_t n = ::read(fd, chunk, sizeof(chunk));
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
break;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
std::string out = std::move(buffer);
|
||||
buffer.clear();
|
||||
return out;
|
||||
}
|
||||
buffer.append(chunk, static_cast<std::size_t>(n));
|
||||
const std::size_t newlinePos = buffer.find('\n');
|
||||
if (newlinePos != std::string::npos) {
|
||||
std::string line = buffer.substr(0, newlinePos);
|
||||
buffer.erase(0, newlinePos + 1);
|
||||
return line;
|
||||
}
|
||||
}
|
||||
return std::string{};
|
||||
}
|
||||
|
||||
bool setFdNonBlocking(int fd) {
|
||||
const int flags = ::fcntl(fd, F_GETFL, 0);
|
||||
if (flags < 0) {
|
||||
return false;
|
||||
}
|
||||
return ::fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0;
|
||||
}
|
||||
|
||||
std::optional<std::string> helperPath() {
|
||||
constexpr std::array<const char*, 2> candidates = {
|
||||
"/usr/lib/polkit-1/polkit-agent-helper-1",
|
||||
"/usr/libexec/polkit-agent-helper-1",
|
||||
};
|
||||
for (const char* path : candidates) {
|
||||
if (::access(path, X_OK) == 0) {
|
||||
return std::string(path);
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string extractPromptText(const std::string& line, const std::string& token) {
|
||||
if (line.size() <= token.size()) {
|
||||
return "Authentication required";
|
||||
}
|
||||
std::string prompt = line.substr(token.size());
|
||||
if (!prompt.empty() && prompt.front() == ' ') {
|
||||
prompt.erase(prompt.begin());
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
|
||||
std::optional<std::string> sessionIdFromLogind(SystemBus& bus) {
|
||||
try {
|
||||
auto manager = sdbus::createProxy(bus.connection(), k_logindBusName, k_logindObjectPath);
|
||||
sdbus::ObjectPath sessionPath;
|
||||
manager->callMethod("GetSessionByPID")
|
||||
.onInterface(k_logindManagerInterface)
|
||||
.withArguments(static_cast<std::uint32_t>(::getpid()))
|
||||
.storeResultsTo(sessionPath);
|
||||
|
||||
auto session = sdbus::createProxy(bus.connection(), k_logindBusName, sessionPath);
|
||||
const sdbus::Variant idVar = session->getProperty("Id").onInterface(k_logindSessionInterface);
|
||||
const std::string sessionId = idVar.get<std::string>();
|
||||
if (!sessionId.empty()) {
|
||||
return sessionId;
|
||||
}
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Subject makeSessionSubject(SystemBus& bus) {
|
||||
if (const auto sessionId = sessionIdFromLogind(bus); sessionId.has_value()) {
|
||||
std::map<std::string, sdbus::Variant> sessionDetails;
|
||||
sessionDetails.emplace("session-id", sdbus::Variant{*sessionId});
|
||||
return Subject{"unix-session", std::move(sessionDetails)};
|
||||
}
|
||||
|
||||
if (const char* sessionId = std::getenv("XDG_SESSION_ID"); sessionId != nullptr && sessionId[0] != '\0') {
|
||||
std::map<std::string, sdbus::Variant> sessionDetails;
|
||||
sessionDetails.emplace("session-id", sdbus::Variant{std::string(sessionId)});
|
||||
return Subject{"unix-session", std::move(sessionDetails)};
|
||||
}
|
||||
|
||||
// Fallback when the session id is unavailable: register for this process.
|
||||
std::map<std::string, sdbus::Variant> details;
|
||||
const pid_t pid = ::getpid();
|
||||
std::ifstream statFile("/proc/self/stat");
|
||||
if (statFile.is_open()) {
|
||||
std::string line;
|
||||
std::getline(statFile, line);
|
||||
const std::size_t rightParen = line.rfind(')');
|
||||
if (rightParen != std::string::npos && rightParen + 2 < line.size()) {
|
||||
// /proc/<pid>/stat field 22 is process start time (clock ticks since boot).
|
||||
std::istringstream rest(line.substr(rightParen + 2));
|
||||
std::string field;
|
||||
std::uint64_t startTime = 0;
|
||||
for (int fieldIndex = 3; fieldIndex <= 22; ++fieldIndex) {
|
||||
if (!(rest >> field)) {
|
||||
break;
|
||||
}
|
||||
if (fieldIndex == 22) {
|
||||
try {
|
||||
startTime = static_cast<std::uint64_t>(std::stoull(field));
|
||||
} catch (const std::exception&) {
|
||||
startTime = 0;
|
||||
}
|
||||
break;
|
||||
if (n == 0) {
|
||||
if (buffer.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::string out = std::move(buffer);
|
||||
buffer.clear();
|
||||
return out;
|
||||
}
|
||||
buffer.append(chunk, static_cast<std::size_t>(n));
|
||||
const std::size_t newlinePos = buffer.find('\n');
|
||||
if (newlinePos != std::string::npos) {
|
||||
std::string line = buffer.substr(0, newlinePos);
|
||||
buffer.erase(0, newlinePos + 1);
|
||||
return line;
|
||||
}
|
||||
details.emplace("pid", sdbus::Variant{static_cast<std::uint32_t>(pid)});
|
||||
details.emplace("start-time", sdbus::Variant{startTime});
|
||||
return Subject{"unix-process", std::move(details)};
|
||||
}
|
||||
return std::string{};
|
||||
}
|
||||
// Fallback when /proc parsing fails.
|
||||
details.emplace("pid", sdbus::Variant{static_cast<std::uint32_t>(pid)});
|
||||
details.emplace("start-time", sdbus::Variant{static_cast<std::uint64_t>(0)});
|
||||
return Subject{"unix-process", std::move(details)};
|
||||
}
|
||||
|
||||
PolkitIdentity toPolkitIdentity(const Identity& wireIdentity) {
|
||||
PolkitIdentity identity;
|
||||
identity.kind = std::get<0>(wireIdentity);
|
||||
const auto& details = std::get<1>(wireIdentity);
|
||||
bool setFdNonBlocking(int fd) {
|
||||
const int flags = ::fcntl(fd, F_GETFL, 0);
|
||||
if (flags < 0) {
|
||||
return false;
|
||||
}
|
||||
return ::fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0;
|
||||
}
|
||||
|
||||
const auto uidIt = details.find("uid");
|
||||
if (uidIt != details.end()) {
|
||||
std::optional<std::string> helperPath() {
|
||||
constexpr std::array<const char*, 2> candidates = {
|
||||
"/usr/lib/polkit-1/polkit-agent-helper-1",
|
||||
"/usr/libexec/polkit-agent-helper-1",
|
||||
};
|
||||
for (const char* path : candidates) {
|
||||
if (::access(path, X_OK) == 0) {
|
||||
return std::string(path);
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string extractPromptText(const std::string& line, const std::string& token) {
|
||||
if (line.size() <= token.size()) {
|
||||
return "Authentication required";
|
||||
}
|
||||
std::string prompt = line.substr(token.size());
|
||||
if (!prompt.empty() && prompt.front() == ' ') {
|
||||
prompt.erase(prompt.begin());
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
|
||||
std::optional<std::string> sessionIdFromLogind(SystemBus& bus) {
|
||||
try {
|
||||
identity.uid = uidIt->second.get<std::uint32_t>();
|
||||
auto manager = sdbus::createProxy(bus.connection(), k_logindBusName, k_logindObjectPath);
|
||||
sdbus::ObjectPath sessionPath;
|
||||
manager->callMethod("GetSessionByPID")
|
||||
.onInterface(k_logindManagerInterface)
|
||||
.withArguments(static_cast<std::uint32_t>(::getpid()))
|
||||
.storeResultsTo(sessionPath);
|
||||
|
||||
auto session = sdbus::createProxy(bus.connection(), k_logindBusName, sessionPath);
|
||||
const sdbus::Variant idVar = session->getProperty("Id").onInterface(k_logindSessionInterface);
|
||||
const std::string sessionId = idVar.get<std::string>();
|
||||
if (!sessionId.empty()) {
|
||||
return sessionId;
|
||||
}
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto userNameIt = details.find("name");
|
||||
if (userNameIt != details.end()) {
|
||||
try {
|
||||
identity.userName = userNameIt->second.get<std::string>();
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
}
|
||||
return identity;
|
||||
}
|
||||
|
||||
std::optional<std::size_t> firstSupportedIdentityIndex(const std::vector<Identity>& identities, std::string& outUser) {
|
||||
for (std::size_t i = 0; i < identities.size(); ++i) {
|
||||
if (auto resolved = identityUsername(identities[i]); resolved.has_value()) {
|
||||
outUser = *resolved;
|
||||
return i;
|
||||
Subject makeSessionSubject(SystemBus& bus) {
|
||||
if (const auto sessionId = sessionIdFromLogind(bus); sessionId.has_value()) {
|
||||
std::map<std::string, sdbus::Variant> sessionDetails;
|
||||
sessionDetails.emplace("session-id", sdbus::Variant{*sessionId});
|
||||
return Subject{"unix-session", std::move(sessionDetails)};
|
||||
}
|
||||
|
||||
if (const char* sessionId = std::getenv("XDG_SESSION_ID"); sessionId != nullptr && sessionId[0] != '\0') {
|
||||
std::map<std::string, sdbus::Variant> sessionDetails;
|
||||
sessionDetails.emplace("session-id", sdbus::Variant{std::string(sessionId)});
|
||||
return Subject{"unix-session", std::move(sessionDetails)};
|
||||
}
|
||||
|
||||
// Fallback when the session id is unavailable: register for this process.
|
||||
std::map<std::string, sdbus::Variant> details;
|
||||
const pid_t pid = ::getpid();
|
||||
std::ifstream statFile("/proc/self/stat");
|
||||
if (statFile.is_open()) {
|
||||
std::string line;
|
||||
std::getline(statFile, line);
|
||||
const std::size_t rightParen = line.rfind(')');
|
||||
if (rightParen != std::string::npos && rightParen + 2 < line.size()) {
|
||||
// /proc/<pid>/stat field 22 is process start time (clock ticks since boot).
|
||||
std::istringstream rest(line.substr(rightParen + 2));
|
||||
std::string field;
|
||||
std::uint64_t startTime = 0;
|
||||
for (int fieldIndex = 3; fieldIndex <= 22; ++fieldIndex) {
|
||||
if (!(rest >> field)) {
|
||||
break;
|
||||
}
|
||||
if (fieldIndex == 22) {
|
||||
try {
|
||||
startTime = static_cast<std::uint64_t>(std::stoull(field));
|
||||
} catch (const std::exception&) {
|
||||
startTime = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
details.emplace("pid", sdbus::Variant{static_cast<std::uint32_t>(pid)});
|
||||
details.emplace("start-time", sdbus::Variant{startTime});
|
||||
return Subject{"unix-process", std::move(details)};
|
||||
}
|
||||
}
|
||||
// Fallback when /proc parsing fails.
|
||||
details.emplace("pid", sdbus::Variant{static_cast<std::uint32_t>(pid)});
|
||||
details.emplace("start-time", sdbus::Variant{static_cast<std::uint64_t>(0)});
|
||||
return Subject{"unix-process", std::move(details)};
|
||||
}
|
||||
|
||||
PolkitIdentity toPolkitIdentity(const Identity& wireIdentity) {
|
||||
PolkitIdentity identity;
|
||||
identity.kind = std::get<0>(wireIdentity);
|
||||
const auto& details = std::get<1>(wireIdentity);
|
||||
|
||||
const auto uidIt = details.find("uid");
|
||||
if (uidIt != details.end()) {
|
||||
try {
|
||||
identity.uid = uidIt->second.get<std::uint32_t>();
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
}
|
||||
const auto userNameIt = details.find("name");
|
||||
if (userNameIt != details.end()) {
|
||||
try {
|
||||
identity.userName = userNameIt->second.get<std::string>();
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
}
|
||||
return identity;
|
||||
}
|
||||
|
||||
std::optional<std::size_t> firstSupportedIdentityIndex(const std::vector<Identity>& identities,
|
||||
std::string& outUser) {
|
||||
for (std::size_t i = 0; i < identities.size(); ++i) {
|
||||
if (auto resolved = identityUsername(identities[i]); resolved.has_value()) {
|
||||
outUser = *resolved;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -486,63 +487,62 @@ PolkitAgent::PolkitAgent(SystemBus& bus) : m_impl(std::make_unique<Impl>(bus)) {
|
||||
m_impl->object = sdbus::createObject(bus.connection(), k_agentObjectPath);
|
||||
|
||||
m_impl->object
|
||||
->addVTable(
|
||||
sdbus::registerMethod("BeginAuthentication")
|
||||
.withInputParamNames("action_id", "message", "icon_name", "details", "cookie", "identities")
|
||||
.implementedAs([this](sdbus::Result<>&& result, std::string actionId, std::string message,
|
||||
std::string iconName,
|
||||
std::map<std::string, std::string> /*details*/, std::string cookie,
|
||||
std::vector<Identity> identities) {
|
||||
if (m_impl == nullptr) {
|
||||
result.returnResults();
|
||||
return;
|
||||
}
|
||||
if (m_impl->pendingBeginResult.has_value()) {
|
||||
// Defensive: finish any previous deferred call before replacing state.
|
||||
m_impl->finishBeginAuthenticationCall();
|
||||
}
|
||||
if (m_impl->pending.has_value()) {
|
||||
m_impl->stopHelper();
|
||||
m_impl->clearConversationState();
|
||||
}
|
||||
m_impl->pendingBeginResult = std::move(result);
|
||||
PolkitRequest request;
|
||||
request.actionId = std::move(actionId);
|
||||
request.message = std::move(message);
|
||||
request.iconName = std::move(iconName);
|
||||
request.cookie = std::move(cookie);
|
||||
request.identities.reserve(identities.size());
|
||||
for (const auto& identity : identities) {
|
||||
request.identities.push_back(toPolkitIdentity(identity));
|
||||
}
|
||||
m_impl->pending = request;
|
||||
m_impl->pendingIdentitiesWire = std::move(identities);
|
||||
->addVTable(sdbus::registerMethod("BeginAuthentication")
|
||||
.withInputParamNames("action_id", "message", "icon_name", "details", "cookie", "identities")
|
||||
.implementedAs([this](sdbus::Result<>&& result, std::string actionId, std::string message,
|
||||
std::string iconName, std::map<std::string, std::string> /*details*/,
|
||||
std::string cookie, std::vector<Identity> identities) {
|
||||
if (m_impl == nullptr) {
|
||||
result.returnResults();
|
||||
return;
|
||||
}
|
||||
if (m_impl->pendingBeginResult.has_value()) {
|
||||
// Defensive: finish any previous deferred call before replacing state.
|
||||
m_impl->finishBeginAuthenticationCall();
|
||||
}
|
||||
if (m_impl->pending.has_value()) {
|
||||
m_impl->stopHelper();
|
||||
m_impl->clearConversationState();
|
||||
}
|
||||
m_impl->pendingBeginResult = std::move(result);
|
||||
PolkitRequest request;
|
||||
request.actionId = std::move(actionId);
|
||||
request.message = std::move(message);
|
||||
request.iconName = std::move(iconName);
|
||||
request.cookie = std::move(cookie);
|
||||
request.identities.reserve(identities.size());
|
||||
for (const auto& identity : identities) {
|
||||
request.identities.push_back(toPolkitIdentity(identity));
|
||||
}
|
||||
m_impl->pending = request;
|
||||
m_impl->pendingIdentitiesWire = std::move(identities);
|
||||
|
||||
std::string userName;
|
||||
const auto selectedIdentityIndex =
|
||||
firstSupportedIdentityIndex(m_impl->pendingIdentitiesWire, userName);
|
||||
if (!selectedIdentityIndex.has_value()) {
|
||||
kLog.warn("polkit request \"{}\" has no unix-user identity", request.actionId);
|
||||
m_impl->clearPending();
|
||||
return;
|
||||
}
|
||||
m_impl->activeIdentityIndex = *selectedIdentityIndex;
|
||||
m_impl->activeUser = userName;
|
||||
if (!m_impl->startHelperSession()) {
|
||||
kLog.warn("polkit helper startup failed for action \"{}\"", request.actionId);
|
||||
m_impl->clearPending();
|
||||
return;
|
||||
}
|
||||
}),
|
||||
sdbus::registerMethod("CancelAuthentication").withInputParamNames("cookie").implementedAs(
|
||||
[this](const std::string& cookie) {
|
||||
if (m_impl == nullptr || !m_impl->pending.has_value()) {
|
||||
return;
|
||||
}
|
||||
if (m_impl->pending->cookie == cookie) {
|
||||
m_impl->clearPending();
|
||||
}
|
||||
}))
|
||||
std::string userName;
|
||||
const auto selectedIdentityIndex =
|
||||
firstSupportedIdentityIndex(m_impl->pendingIdentitiesWire, userName);
|
||||
if (!selectedIdentityIndex.has_value()) {
|
||||
kLog.warn("polkit request \"{}\" has no unix-user identity", request.actionId);
|
||||
m_impl->clearPending();
|
||||
return;
|
||||
}
|
||||
m_impl->activeIdentityIndex = *selectedIdentityIndex;
|
||||
m_impl->activeUser = userName;
|
||||
if (!m_impl->startHelperSession()) {
|
||||
kLog.warn("polkit helper startup failed for action \"{}\"", request.actionId);
|
||||
m_impl->clearPending();
|
||||
return;
|
||||
}
|
||||
}),
|
||||
sdbus::registerMethod("CancelAuthentication")
|
||||
.withInputParamNames("cookie")
|
||||
.implementedAs([this](const std::string& cookie) {
|
||||
if (m_impl == nullptr || !m_impl->pending.has_value()) {
|
||||
return;
|
||||
}
|
||||
if (m_impl->pending->cookie == cookie) {
|
||||
m_impl->clearPending();
|
||||
}
|
||||
}))
|
||||
.forInterface(k_agentInterface);
|
||||
|
||||
try {
|
||||
@@ -654,6 +654,4 @@ std::string PolkitAgent::supplementaryMessage() const {
|
||||
return m_impl != nullptr ? m_impl->supplementaryMessage : std::string{};
|
||||
}
|
||||
|
||||
bool PolkitAgent::supplementaryIsError() const noexcept {
|
||||
return m_impl != nullptr && m_impl->supplementaryError;
|
||||
}
|
||||
bool PolkitAgent::supplementaryIsError() const noexcept { return m_impl != nullptr && m_impl->supplementaryError; }
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <poll.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <poll.h>
|
||||
|
||||
class SystemBus;
|
||||
|
||||
|
||||
@@ -23,49 +23,48 @@ std::string profileLabel(std::string_view profile) {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("power");
|
||||
constexpr Logger kLog("power");
|
||||
|
||||
static const sdbus::ServiceName k_powerProfilesBusName{"org.freedesktop.UPower.PowerProfiles"};
|
||||
static const sdbus::ObjectPath k_powerProfilesObjectPath{"/org/freedesktop/UPower/PowerProfiles"};
|
||||
static constexpr auto k_powerProfilesInterface = "org.freedesktop.UPower.PowerProfiles";
|
||||
static constexpr auto k_propertiesInterface = "org.freedesktop.DBus.Properties";
|
||||
static const sdbus::ServiceName k_powerProfilesBusName{"org.freedesktop.UPower.PowerProfiles"};
|
||||
static const sdbus::ObjectPath k_powerProfilesObjectPath{"/org/freedesktop/UPower/PowerProfiles"};
|
||||
static constexpr auto k_powerProfilesInterface = "org.freedesktop.UPower.PowerProfiles";
|
||||
static constexpr auto k_propertiesInterface = "org.freedesktop.DBus.Properties";
|
||||
|
||||
template <typename T>
|
||||
T getPropertyOr(sdbus::IProxy& proxy, std::string_view propertyName, T fallback) {
|
||||
try {
|
||||
const sdbus::Variant value = proxy.getProperty(propertyName).onInterface(k_powerProfilesInterface);
|
||||
return value.get<T>();
|
||||
} catch (const sdbus::Error&) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> decodeProfiles(const sdbus::Variant& value) {
|
||||
std::vector<std::string> profiles;
|
||||
|
||||
try {
|
||||
const auto profileMaps = value.get<std::vector<std::map<std::string, sdbus::Variant>>>();
|
||||
profiles.reserve(profileMaps.size());
|
||||
for (const auto& profileMap : profileMaps) {
|
||||
auto it = profileMap.find("Profile");
|
||||
if (it == profileMap.end()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const std::string profile = it->second.get<std::string>();
|
||||
if (!profile.empty()) {
|
||||
profiles.push_back(profile);
|
||||
}
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
template <typename T> T getPropertyOr(sdbus::IProxy& proxy, std::string_view propertyName, T fallback) {
|
||||
try {
|
||||
const sdbus::Variant value = proxy.getProperty(propertyName).onInterface(k_powerProfilesInterface);
|
||||
return value.get<T>();
|
||||
} catch (const sdbus::Error&) {
|
||||
return fallback;
|
||||
}
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
|
||||
std::ranges::sort(profiles);
|
||||
profiles.erase(std::unique(profiles.begin(), profiles.end()), profiles.end());
|
||||
return profiles;
|
||||
}
|
||||
std::vector<std::string> decodeProfiles(const sdbus::Variant& value) {
|
||||
std::vector<std::string> profiles;
|
||||
|
||||
try {
|
||||
const auto profileMaps = value.get<std::vector<std::map<std::string, sdbus::Variant>>>();
|
||||
profiles.reserve(profileMaps.size());
|
||||
for (const auto& profileMap : profileMaps) {
|
||||
auto it = profileMap.find("Profile");
|
||||
if (it == profileMap.end()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const std::string profile = it->second.get<std::string>();
|
||||
if (!profile.empty()) {
|
||||
profiles.push_back(profile);
|
||||
}
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
}
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
|
||||
std::ranges::sort(profiles);
|
||||
profiles.erase(std::unique(profiles.begin(), profiles.end()), profiles.end());
|
||||
return profiles;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
class SystemBus;
|
||||
|
||||
namespace sdbus {
|
||||
class IProxy;
|
||||
class IProxy;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string profileLabel(std::string_view profile);
|
||||
|
||||
+274
-282
@@ -10,239 +10,240 @@
|
||||
|
||||
namespace {
|
||||
|
||||
static const sdbus::ServiceName k_watcher_bus_name{"org.kde.StatusNotifierWatcher"};
|
||||
static const sdbus::ObjectPath k_watcher_object_path{"/StatusNotifierWatcher"};
|
||||
static constexpr auto k_watcher_interface = "org.kde.StatusNotifierWatcher";
|
||||
static const sdbus::ServiceName k_watcher_bus_name{"org.kde.StatusNotifierWatcher"};
|
||||
static const sdbus::ObjectPath k_watcher_object_path{"/StatusNotifierWatcher"};
|
||||
static constexpr auto k_watcher_interface = "org.kde.StatusNotifierWatcher";
|
||||
|
||||
static const sdbus::ServiceName k_dbus_name{"org.freedesktop.DBus"};
|
||||
static const sdbus::ObjectPath k_dbus_path{"/org/freedesktop/DBus"};
|
||||
static constexpr auto k_dbus_interface = "org.freedesktop.DBus";
|
||||
static constexpr auto k_item_interface = "org.kde.StatusNotifierItem";
|
||||
static constexpr auto k_menu_interface = "com.canonical.dbusmenu";
|
||||
static constexpr auto k_default_item_path = "/StatusNotifierItem";
|
||||
static const sdbus::ServiceName k_dbus_name{"org.freedesktop.DBus"};
|
||||
static const sdbus::ObjectPath k_dbus_path{"/org/freedesktop/DBus"};
|
||||
static constexpr auto k_dbus_interface = "org.freedesktop.DBus";
|
||||
static constexpr auto k_item_interface = "org.kde.StatusNotifierItem";
|
||||
static constexpr auto k_menu_interface = "com.canonical.dbusmenu";
|
||||
static constexpr auto k_default_item_path = "/StatusNotifierItem";
|
||||
|
||||
bool starts_with_slash(std::string_view value) { return !value.empty() && value.front() == '/'; }
|
||||
bool starts_with_slash(std::string_view value) { return !value.empty() && value.front() == '/'; }
|
||||
|
||||
bool looks_like_dbus_name(std::string_view value) {
|
||||
return !value.empty() && value != "__path_only__";
|
||||
}
|
||||
bool looks_like_dbus_name(std::string_view value) { return !value.empty() && value != "__path_only__"; }
|
||||
|
||||
std::string lower_copy(std::string_view value) {
|
||||
std::string out(value);
|
||||
std::ranges::transform(out, out.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return out;
|
||||
}
|
||||
std::string lower_copy(std::string_view value) {
|
||||
std::string out(value);
|
||||
std::ranges::transform(out, out.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<std::string> path_name_hints(std::string_view objectPath) {
|
||||
std::vector<std::string> hints;
|
||||
if (objectPath.empty()) {
|
||||
return hints;
|
||||
}
|
||||
|
||||
auto push = [&hints](std::string value) {
|
||||
if (value.empty()) {
|
||||
return;
|
||||
}
|
||||
value = lower_copy(value);
|
||||
if (std::ranges::find(hints, value) == hints.end()) {
|
||||
hints.push_back(std::move(value));
|
||||
}
|
||||
};
|
||||
|
||||
std::string tail(objectPath);
|
||||
if (const auto slash = tail.find_last_of('/'); slash != std::string::npos && slash + 1 < tail.size()) {
|
||||
tail = tail.substr(slash + 1);
|
||||
}
|
||||
|
||||
push(tail);
|
||||
|
||||
std::string dashed = tail;
|
||||
std::replace(dashed.begin(), dashed.end(), '_', '-');
|
||||
push(dashed);
|
||||
|
||||
std::string underscored = tail;
|
||||
std::replace(underscored.begin(), underscored.end(), '-', '_');
|
||||
push(underscored);
|
||||
|
||||
for (const auto& suffix : {"_client", "-client", ".desktop"}) {
|
||||
for (const auto& candidate : std::vector<std::string>{tail, dashed, underscored}) {
|
||||
if (candidate.size() > std::char_traits<char>::length(suffix) && candidate.ends_with(suffix)) {
|
||||
push(candidate.substr(0, candidate.size() - std::char_traits<char>::length(suffix)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> path_name_hints(std::string_view objectPath) {
|
||||
std::vector<std::string> hints;
|
||||
if (objectPath.empty()) {
|
||||
return hints;
|
||||
}
|
||||
|
||||
auto push = [&hints](std::string value) {
|
||||
if (value.empty()) {
|
||||
return;
|
||||
}
|
||||
value = lower_copy(value);
|
||||
if (std::ranges::find(hints, value) == hints.end()) {
|
||||
hints.push_back(std::move(value));
|
||||
}
|
||||
};
|
||||
|
||||
std::string tail(objectPath);
|
||||
if (const auto slash = tail.find_last_of('/'); slash != std::string::npos && slash + 1 < tail.size()) {
|
||||
tail = tail.substr(slash + 1);
|
||||
}
|
||||
|
||||
push(tail);
|
||||
|
||||
std::string dashed = tail;
|
||||
std::replace(dashed.begin(), dashed.end(), '_', '-');
|
||||
push(dashed);
|
||||
|
||||
std::string underscored = tail;
|
||||
std::replace(underscored.begin(), underscored.end(), '-', '_');
|
||||
push(underscored);
|
||||
|
||||
for (const auto& suffix : {"_client", "-client", ".desktop"}) {
|
||||
for (const auto& candidate : std::vector<std::string>{tail, dashed, underscored}) {
|
||||
if (candidate.size() > std::char_traits<char>::length(suffix) && candidate.ends_with(suffix)) {
|
||||
push(candidate.substr(0, candidate.size() - std::char_traits<char>::length(suffix)));
|
||||
std::string stripMnemonicUnderscores(std::string label) {
|
||||
std::string out;
|
||||
out.reserve(label.size());
|
||||
for (std::size_t i = 0; i < label.size(); ++i) {
|
||||
if (label[i] == '_') {
|
||||
if (i + 1 < label.size() && label[i + 1] == '_') {
|
||||
out.push_back('_');
|
||||
++i;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hints;
|
||||
}
|
||||
|
||||
std::string stripMnemonicUnderscores(std::string label) {
|
||||
std::string out;
|
||||
out.reserve(label.size());
|
||||
for (std::size_t i = 0; i < label.size(); ++i) {
|
||||
if (label[i] == '_') {
|
||||
if (i + 1 < label.size() && label[i + 1] == '_') {
|
||||
out.push_back('_');
|
||||
++i;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
out.push_back(label[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T get_property_or(sdbus::IProxy& proxy, std::string_view property_name, T fallback) {
|
||||
try {
|
||||
const sdbus::Variant value = proxy.getProperty(property_name).onInterface(k_item_interface);
|
||||
return value.get<T>();
|
||||
} catch (const sdbus::Error&) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
std::string get_item_property_string_or(sdbus::IProxy& proxy, std::string_view propertyName, std::string fallback) {
|
||||
try {
|
||||
const sdbus::Variant value = proxy.getProperty(propertyName).onInterface(k_item_interface);
|
||||
try {
|
||||
return value.get<std::string>();
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
try {
|
||||
return value.get<sdbus::ObjectPath>();
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
using IconPixmapTuple = std::tuple<std::int32_t, std::int32_t, std::vector<std::uint8_t>>;
|
||||
using IconPixmapStruct = sdbus::Struct<std::int32_t, std::int32_t, std::vector<std::uint8_t>>;
|
||||
using DbusMenuLayout = sdbus::Struct<std::int32_t, std::map<std::string, sdbus::Variant>, std::vector<sdbus::Variant>>;
|
||||
|
||||
TrayMenuEntry decodeMenuEntry(const DbusMenuLayout& entryLayout) {
|
||||
TrayMenuEntry out;
|
||||
out.id = std::get<0>(entryLayout);
|
||||
const auto& props = std::get<1>(entryLayout);
|
||||
|
||||
if (const auto it = props.find("label"); it != props.end()) {
|
||||
try {
|
||||
out.label = stripMnemonicUnderscores(it->second.get<std::string>());
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
}
|
||||
if (const auto it = props.find("enabled"); it != props.end()) {
|
||||
try {
|
||||
out.enabled = it->second.get<bool>();
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
}
|
||||
if (const auto it = props.find("visible"); it != props.end()) {
|
||||
try {
|
||||
out.visible = it->second.get<bool>();
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
}
|
||||
if (const auto it = props.find("type"); it != props.end()) {
|
||||
try {
|
||||
out.separator = (it->second.get<std::string>() == "separator");
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
}
|
||||
if (const auto it = props.find("children-display"); it != props.end()) {
|
||||
try {
|
||||
out.hasSubmenu = (it->second.get<std::string>() == "submenu");
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<IconPixmapTuple> iconPixmapsFromVariant(const sdbus::Variant& value) {
|
||||
try {
|
||||
return value.get<std::vector<IconPixmapTuple>>();
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
|
||||
try {
|
||||
const auto structs = value.get<std::vector<IconPixmapStruct>>();
|
||||
std::vector<IconPixmapTuple> out;
|
||||
out.reserve(structs.size());
|
||||
for (const auto& entry : structs) {
|
||||
out.emplace_back(std::get<0>(entry), std::get<1>(entry), std::get<2>(entry));
|
||||
out.push_back(label[i]);
|
||||
}
|
||||
return out;
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<IconPixmapTuple> get_icon_pixmaps_or(sdbus::IProxy& proxy, std::string_view property_name,
|
||||
const std::vector<IconPixmapTuple>& fallback) {
|
||||
try {
|
||||
const sdbus::Variant value = proxy.getProperty(property_name).onInterface(k_item_interface);
|
||||
const auto decoded = iconPixmapsFromVariant(value);
|
||||
if (!decoded.empty()) {
|
||||
return decoded;
|
||||
template <typename T> T get_property_or(sdbus::IProxy& proxy, std::string_view property_name, T fallback) {
|
||||
try {
|
||||
const sdbus::Variant value = proxy.getProperty(property_name).onInterface(k_item_interface);
|
||||
return value.get<T>();
|
||||
} catch (const sdbus::Error&) {
|
||||
return fallback;
|
||||
}
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
|
||||
try {
|
||||
std::map<std::string, sdbus::Variant> all;
|
||||
proxy.callMethod("GetAll").onInterface("org.freedesktop.DBus.Properties").withArguments(k_item_interface).storeResultsTo(all);
|
||||
const auto it = all.find(std::string(property_name));
|
||||
if (it != all.end()) {
|
||||
const auto decoded = iconPixmapsFromVariant(it->second);
|
||||
std::string get_item_property_string_or(sdbus::IProxy& proxy, std::string_view propertyName, std::string fallback) {
|
||||
try {
|
||||
const sdbus::Variant value = proxy.getProperty(propertyName).onInterface(k_item_interface);
|
||||
try {
|
||||
return value.get<std::string>();
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
try {
|
||||
return value.get<sdbus::ObjectPath>();
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
using IconPixmapTuple = std::tuple<std::int32_t, std::int32_t, std::vector<std::uint8_t>>;
|
||||
using IconPixmapStruct = sdbus::Struct<std::int32_t, std::int32_t, std::vector<std::uint8_t>>;
|
||||
using DbusMenuLayout =
|
||||
sdbus::Struct<std::int32_t, std::map<std::string, sdbus::Variant>, std::vector<sdbus::Variant>>;
|
||||
|
||||
TrayMenuEntry decodeMenuEntry(const DbusMenuLayout& entryLayout) {
|
||||
TrayMenuEntry out;
|
||||
out.id = std::get<0>(entryLayout);
|
||||
const auto& props = std::get<1>(entryLayout);
|
||||
|
||||
if (const auto it = props.find("label"); it != props.end()) {
|
||||
try {
|
||||
out.label = stripMnemonicUnderscores(it->second.get<std::string>());
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
}
|
||||
if (const auto it = props.find("enabled"); it != props.end()) {
|
||||
try {
|
||||
out.enabled = it->second.get<bool>();
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
}
|
||||
if (const auto it = props.find("visible"); it != props.end()) {
|
||||
try {
|
||||
out.visible = it->second.get<bool>();
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
}
|
||||
if (const auto it = props.find("type"); it != props.end()) {
|
||||
try {
|
||||
out.separator = (it->second.get<std::string>() == "separator");
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
}
|
||||
if (const auto it = props.find("children-display"); it != props.end()) {
|
||||
try {
|
||||
out.hasSubmenu = (it->second.get<std::string>() == "submenu");
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<IconPixmapTuple> iconPixmapsFromVariant(const sdbus::Variant& value) {
|
||||
try {
|
||||
return value.get<std::vector<IconPixmapTuple>>();
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
|
||||
try {
|
||||
const auto structs = value.get<std::vector<IconPixmapStruct>>();
|
||||
std::vector<IconPixmapTuple> out;
|
||||
out.reserve(structs.size());
|
||||
for (const auto& entry : structs) {
|
||||
out.emplace_back(std::get<0>(entry), std::get<1>(entry), std::get<2>(entry));
|
||||
}
|
||||
return out;
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<IconPixmapTuple> get_icon_pixmaps_or(sdbus::IProxy& proxy, std::string_view property_name,
|
||||
const std::vector<IconPixmapTuple>& fallback) {
|
||||
try {
|
||||
const sdbus::Variant value = proxy.getProperty(property_name).onInterface(k_item_interface);
|
||||
const auto decoded = iconPixmapsFromVariant(value);
|
||||
if (!decoded.empty()) {
|
||||
return decoded;
|
||||
}
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
} catch (const sdbus::Error&) {
|
||||
|
||||
try {
|
||||
std::map<std::string, sdbus::Variant> all;
|
||||
proxy.callMethod("GetAll")
|
||||
.onInterface("org.freedesktop.DBus.Properties")
|
||||
.withArguments(k_item_interface)
|
||||
.storeResultsTo(all);
|
||||
const auto it = all.find(std::string(property_name));
|
||||
if (it != all.end()) {
|
||||
const auto decoded = iconPixmapsFromVariant(it->second);
|
||||
if (!decoded.empty()) {
|
||||
return decoded;
|
||||
}
|
||||
}
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
bool pickBestPixmap(const std::vector<IconPixmapTuple>& pixmaps, std::vector<std::uint8_t>& outArgb,
|
||||
std::int32_t& outW, std::int32_t& outH) {
|
||||
std::size_t bestIndex = static_cast<std::size_t>(-1);
|
||||
std::int64_t bestArea = -1;
|
||||
|
||||
bool pickBestPixmap(const std::vector<IconPixmapTuple>& pixmaps, std::vector<std::uint8_t>& outArgb, std::int32_t& outW,
|
||||
std::int32_t& outH) {
|
||||
std::size_t bestIndex = static_cast<std::size_t>(-1);
|
||||
std::int64_t bestArea = -1;
|
||||
for (std::size_t i = 0; i < pixmaps.size(); ++i) {
|
||||
const auto& [w, h, data] = pixmaps[i];
|
||||
if (w <= 0 || h <= 0 || data.empty()) {
|
||||
continue;
|
||||
}
|
||||
if (static_cast<std::size_t>(w * h * 4) > data.size()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < pixmaps.size(); ++i) {
|
||||
const auto& [w, h, data] = pixmaps[i];
|
||||
if (w <= 0 || h <= 0 || data.empty()) {
|
||||
continue;
|
||||
}
|
||||
if (static_cast<std::size_t>(w * h * 4) > data.size()) {
|
||||
continue;
|
||||
const std::int64_t area = static_cast<std::int64_t>(w) * static_cast<std::int64_t>(h);
|
||||
if (area > bestArea) {
|
||||
bestArea = area;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
const std::int64_t area = static_cast<std::int64_t>(w) * static_cast<std::int64_t>(h);
|
||||
if (area > bestArea) {
|
||||
bestArea = area;
|
||||
bestIndex = i;
|
||||
if (bestIndex == static_cast<std::size_t>(-1)) {
|
||||
outArgb.clear();
|
||||
outW = 0;
|
||||
outH = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& [w, h, data] = pixmaps[bestIndex];
|
||||
outW = w;
|
||||
outH = h;
|
||||
outArgb = data;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (bestIndex == static_cast<std::size_t>(-1)) {
|
||||
outArgb.clear();
|
||||
outW = 0;
|
||||
outH = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& [w, h, data] = pixmaps[bestIndex];
|
||||
outW = w;
|
||||
outH = h;
|
||||
outArgb = data;
|
||||
return true;
|
||||
}
|
||||
|
||||
constexpr Logger kLog("tray");
|
||||
constexpr Logger kLog("tray");
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -329,34 +330,34 @@ std::vector<TrayItemInfo> TrayService::items() const {
|
||||
|
||||
namespace {
|
||||
|
||||
// Recursively decode a DbusMenuLayout into the cache. Each layout node contributes
|
||||
// a `std::vector<TrayMenuEntry>` keyed by its id into entriesByParent. Invisible
|
||||
// entries are skipped from display but we still recurse so their own children
|
||||
// (if any) are reachable from the cache.
|
||||
void ingestLayoutNode(const DbusMenuLayout& node,
|
||||
std::unordered_map<std::int32_t, std::vector<TrayMenuEntry>>& entriesByParent) {
|
||||
const auto nodeId = std::get<0>(node);
|
||||
const auto& children = std::get<2>(node);
|
||||
// Recursively decode a DbusMenuLayout into the cache. Each layout node contributes
|
||||
// a `std::vector<TrayMenuEntry>` keyed by its id into entriesByParent. Invisible
|
||||
// entries are skipped from display but we still recurse so their own children
|
||||
// (if any) are reachable from the cache.
|
||||
void ingestLayoutNode(const DbusMenuLayout& node,
|
||||
std::unordered_map<std::int32_t, std::vector<TrayMenuEntry>>& entriesByParent) {
|
||||
const auto nodeId = std::get<0>(node);
|
||||
const auto& children = std::get<2>(node);
|
||||
|
||||
std::vector<TrayMenuEntry> entries;
|
||||
entries.reserve(children.size());
|
||||
for (const auto& childValue : children) {
|
||||
try {
|
||||
const auto child = childValue.get<DbusMenuLayout>();
|
||||
auto entry = decodeMenuEntry(child);
|
||||
ingestLayoutNode(child, entriesByParent);
|
||||
if (entry.id <= 0 || !entry.visible) {
|
||||
continue;
|
||||
std::vector<TrayMenuEntry> entries;
|
||||
entries.reserve(children.size());
|
||||
for (const auto& childValue : children) {
|
||||
try {
|
||||
const auto child = childValue.get<DbusMenuLayout>();
|
||||
auto entry = decodeMenuEntry(child);
|
||||
ingestLayoutNode(child, entriesByParent);
|
||||
if (entry.id <= 0 || !entry.visible) {
|
||||
continue;
|
||||
}
|
||||
if (entry.label.empty() && !entry.separator) {
|
||||
continue;
|
||||
}
|
||||
entries.push_back(std::move(entry));
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
if (entry.label.empty() && !entry.separator) {
|
||||
continue;
|
||||
}
|
||||
entries.push_back(std::move(entry));
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
entriesByParent[nodeId] = std::move(entries);
|
||||
}
|
||||
entriesByParent[nodeId] = std::move(entries);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -416,8 +417,8 @@ std::vector<TrayMenuEntry> TrayService::menuEntries(const std::string& itemId) {
|
||||
return {};
|
||||
}
|
||||
if (itemIt->second.busName.empty() || itemIt->second.menuObjectPath.empty()) {
|
||||
kLog.warn("menuEntries: missing bus/menu path id={} bus='{}' menu='{}'",
|
||||
itemId, itemIt->second.busName, itemIt->second.menuObjectPath);
|
||||
kLog.warn("menuEntries: missing bus/menu path id={} bus='{}' menu='{}'", itemId, itemIt->second.busName,
|
||||
itemIt->second.menuObjectPath);
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -469,8 +470,7 @@ std::vector<TrayMenuEntry> TrayService::menuEntriesForParent(const std::string&
|
||||
return {};
|
||||
}
|
||||
|
||||
void TrayService::ensureMenuCache(const std::string& itemId, const std::string& busName,
|
||||
const std::string& menuPath) {
|
||||
void TrayService::ensureMenuCache(const std::string& itemId, const std::string& busName, const std::string& menuPath) {
|
||||
if (busName.empty() || menuPath.empty()) {
|
||||
return;
|
||||
}
|
||||
@@ -480,8 +480,7 @@ void TrayService::ensureMenuCache(const std::string& itemId, const std::string&
|
||||
}
|
||||
|
||||
try {
|
||||
auto proxy = sdbus::createProxy(m_bus.connection(), sdbus::ServiceName{busName},
|
||||
sdbus::ObjectPath{menuPath});
|
||||
auto proxy = sdbus::createProxy(m_bus.connection(), sdbus::ServiceName{busName}, sdbus::ObjectPath{menuPath});
|
||||
|
||||
// LayoutUpdated(rev, parent): server is telling us the subtree rooted at
|
||||
// `parent` changed. Invalidate aggressively — menus are small so re-fetch
|
||||
@@ -531,17 +530,14 @@ void TrayService::sendMenuEvent(const std::string& itemId, std::int32_t entryId,
|
||||
return;
|
||||
}
|
||||
const auto timestamp = static_cast<std::uint32_t>(
|
||||
std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count());
|
||||
std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count());
|
||||
try {
|
||||
it->second.proxy->callMethod("Event")
|
||||
.onInterface(k_menu_interface)
|
||||
.withTimeout(std::chrono::milliseconds(500))
|
||||
.withArguments(entryId, eventName, sdbus::Variant{std::int32_t{0}}, timestamp);
|
||||
} catch (const sdbus::Error& e) {
|
||||
kLog.debug("dbusmenu Event failed id={} entryId={} event={} err={}",
|
||||
itemId, entryId, eventName, e.what());
|
||||
kLog.debug("dbusmenu Event failed id={} entryId={} event={} err={}", itemId, entryId, eventName, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,9 +555,7 @@ bool TrayService::activateMenuEntry(const std::string& itemId, std::int32_t entr
|
||||
return false;
|
||||
}
|
||||
const auto timestamp = static_cast<std::uint32_t>(
|
||||
std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count());
|
||||
std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count());
|
||||
try {
|
||||
it->second.proxy->callMethod("Event")
|
||||
.onInterface(k_menu_interface)
|
||||
@@ -619,8 +613,7 @@ bool TrayService::openContextMenu(const std::string& itemId, std::int32_t x, std
|
||||
}
|
||||
}
|
||||
|
||||
void TrayService::onRegisterStatusNotifierItem(const std::string& serviceOrPath,
|
||||
const std::string& senderBusName) {
|
||||
void TrayService::onRegisterStatusNotifierItem(const std::string& serviceOrPath, const std::string& senderBusName) {
|
||||
kLog.info("RegisterStatusNotifierItem: service/path='{}' sender='{}'", serviceOrPath, senderBusName);
|
||||
if (serviceOrPath.empty()) {
|
||||
kLog.warn("register item ignored: empty service/path");
|
||||
@@ -699,24 +692,24 @@ void TrayService::registerOrRefreshItem(const std::string& busName, const std::s
|
||||
if (inserted) {
|
||||
kLog.info("tray item registered id={} bus='{}' path='{}'", itemId, busName, objectPath);
|
||||
m_items.emplace(itemId, TrayItemInfo{
|
||||
.id = itemId,
|
||||
.busName = busName,
|
||||
.objectPath = objectPath,
|
||||
.iconName = {},
|
||||
.iconThemePath = {},
|
||||
.attentionIconName = {},
|
||||
.menuObjectPath = {},
|
||||
.itemName = {},
|
||||
.title = {},
|
||||
.status = {},
|
||||
.iconArgb32 = {},
|
||||
.iconWidth = 0,
|
||||
.iconHeight = 0,
|
||||
.attentionArgb32 = {},
|
||||
.attentionWidth = 0,
|
||||
.attentionHeight = 0,
|
||||
.needsAttention = false,
|
||||
});
|
||||
.id = itemId,
|
||||
.busName = busName,
|
||||
.objectPath = objectPath,
|
||||
.iconName = {},
|
||||
.iconThemePath = {},
|
||||
.attentionIconName = {},
|
||||
.menuObjectPath = {},
|
||||
.itemName = {},
|
||||
.title = {},
|
||||
.status = {},
|
||||
.iconArgb32 = {},
|
||||
.iconWidth = 0,
|
||||
.iconHeight = 0,
|
||||
.attentionArgb32 = {},
|
||||
.attentionWidth = 0,
|
||||
.attentionHeight = 0,
|
||||
.needsAttention = false,
|
||||
});
|
||||
|
||||
if (looks_like_dbus_name(busName)) {
|
||||
auto [proxyIt, _] = m_itemProxies.emplace(
|
||||
@@ -739,8 +732,7 @@ void TrayService::registerOrRefreshItem(const std::string& busName, const std::s
|
||||
kLog.debug("item registered: {}", itemId);
|
||||
m_watcherObject->emitSignal("StatusNotifierItemRegistered").onInterface(k_watcher_interface).withArguments(itemId);
|
||||
m_watcherObject->emitPropertiesChangedSignal(
|
||||
k_watcher_interface,
|
||||
std::vector<sdbus::PropertyName>{sdbus::PropertyName{"RegisteredStatusNotifierItems"}});
|
||||
k_watcher_interface, std::vector<sdbus::PropertyName>{sdbus::PropertyName{"RegisteredStatusNotifierItems"}});
|
||||
}
|
||||
|
||||
if (looks_like_dbus_name(busName)) {
|
||||
@@ -784,9 +776,9 @@ bool TrayService::ensureItemProxy(const std::string& itemId) {
|
||||
|
||||
auto& item = m_items[itemId];
|
||||
item.busName = candidate;
|
||||
auto [proxyIt, _] = m_itemProxies.emplace(
|
||||
itemId, sdbus::createProxy(m_bus.connection(), sdbus::ServiceName{candidate},
|
||||
sdbus::ObjectPath{item.objectPath}));
|
||||
auto [proxyIt, _] =
|
||||
m_itemProxies.emplace(itemId, sdbus::createProxy(m_bus.connection(), sdbus::ServiceName{candidate},
|
||||
sdbus::ObjectPath{item.objectPath}));
|
||||
|
||||
proxyIt->second->uponSignal("NewIcon").onInterface(k_item_interface).call([this, itemId]() {
|
||||
refreshItemMetadata(itemId);
|
||||
@@ -841,13 +833,13 @@ void TrayService::refreshItemMetadata(const std::string& itemId) {
|
||||
auto next = cur;
|
||||
// Use the existing value as the fallback so a transient D-Bus failure doesn't
|
||||
// wipe out data that was successfully fetched earlier (e.g. menuObjectPath).
|
||||
next.iconName = get_item_property_string_or(*proxyIt->second, "IconName", cur.iconName);
|
||||
next.iconThemePath = get_item_property_string_or(*proxyIt->second, "IconThemePath", cur.iconThemePath);
|
||||
next.attentionIconName= get_item_property_string_or(*proxyIt->second, "AttentionIconName",cur.attentionIconName);
|
||||
next.menuObjectPath = get_item_property_string_or(*proxyIt->second, "Menu", cur.menuObjectPath);
|
||||
next.itemName = get_item_property_string_or(*proxyIt->second, "Id", cur.itemName);
|
||||
next.title = get_item_property_string_or(*proxyIt->second, "Title", cur.title);
|
||||
next.status = get_item_property_string_or(*proxyIt->second, "Status", cur.status);
|
||||
next.iconName = get_item_property_string_or(*proxyIt->second, "IconName", cur.iconName);
|
||||
next.iconThemePath = get_item_property_string_or(*proxyIt->second, "IconThemePath", cur.iconThemePath);
|
||||
next.attentionIconName = get_item_property_string_or(*proxyIt->second, "AttentionIconName", cur.attentionIconName);
|
||||
next.menuObjectPath = get_item_property_string_or(*proxyIt->second, "Menu", cur.menuObjectPath);
|
||||
next.itemName = get_item_property_string_or(*proxyIt->second, "Id", cur.itemName);
|
||||
next.title = get_item_property_string_or(*proxyIt->second, "Title", cur.title);
|
||||
next.status = get_item_property_string_or(*proxyIt->second, "Status", cur.status);
|
||||
next.needsAttention = (next.status == "NeedsAttention");
|
||||
|
||||
const auto iconPixmaps = get_icon_pixmaps_or(*proxyIt->second, "IconPixmap", {});
|
||||
@@ -856,12 +848,11 @@ void TrayService::refreshItemMetadata(const std::string& itemId) {
|
||||
const auto attentionPixmaps = get_icon_pixmaps_or(*proxyIt->second, "AttentionIconPixmap", {});
|
||||
pickBestPixmap(attentionPixmaps, next.attentionArgb32, next.attentionWidth, next.attentionHeight);
|
||||
|
||||
kLog.debug(
|
||||
"item metadata id={} itemName='{}' status={} iconName='{}' attentionIconName='{}' menu='{}' "
|
||||
"iconThemePath='{}' iconPixmap={}x{} (bytes={}) attentionPixmap={}x{} (bytes={})",
|
||||
itemId, next.itemName, next.status, next.iconName, next.attentionIconName, next.menuObjectPath,
|
||||
next.iconThemePath, next.iconWidth, next.iconHeight, next.iconArgb32.size(), next.attentionWidth,
|
||||
next.attentionHeight, next.attentionArgb32.size());
|
||||
kLog.debug("item metadata id={} itemName='{}' status={} iconName='{}' attentionIconName='{}' menu='{}' "
|
||||
"iconThemePath='{}' iconPixmap={}x{} (bytes={}) attentionPixmap={}x{} (bytes={})",
|
||||
itemId, next.itemName, next.status, next.iconName, next.attentionIconName, next.menuObjectPath,
|
||||
next.iconThemePath, next.iconWidth, next.iconHeight, next.iconArgb32.size(), next.attentionWidth,
|
||||
next.attentionHeight, next.attentionArgb32.size());
|
||||
|
||||
if (next == itemIt->second) {
|
||||
// Menu path unchanged — make sure the cache/subscription exists (may not have
|
||||
@@ -897,8 +888,9 @@ void TrayService::removeItemsForBusName(const std::string& busName) {
|
||||
m_itemProxies.erase(itemId);
|
||||
m_menuCache.erase(itemId);
|
||||
kLog.debug("item unregistered: {}", itemId);
|
||||
m_watcherObject->emitSignal("StatusNotifierItemUnregistered").onInterface(k_watcher_interface).withArguments(
|
||||
itemId);
|
||||
m_watcherObject->emitSignal("StatusNotifierItemUnregistered")
|
||||
.onInterface(k_watcher_interface)
|
||||
.withArguments(itemId);
|
||||
}
|
||||
m_watcherObject->emitPropertiesChangedSignal(
|
||||
k_watcher_interface, std::vector<sdbus::PropertyName>{sdbus::PropertyName{"RegisteredStatusNotifierItems"}});
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <sdbus-c++/sdbus-c++.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <sdbus-c++/sdbus-c++.h>
|
||||
|
||||
class SessionBus;
|
||||
|
||||
struct TrayItemInfo {
|
||||
|
||||
@@ -11,22 +11,22 @@
|
||||
|
||||
namespace {
|
||||
|
||||
static const sdbus::ServiceName k_upowerBusName{"org.freedesktop.UPower"};
|
||||
static const sdbus::ObjectPath k_upowerObjectPath{"/org/freedesktop/UPower"};
|
||||
static constexpr auto k_upowerInterface = "org.freedesktop.UPower";
|
||||
static constexpr auto k_deviceInterface = "org.freedesktop.UPower.Device";
|
||||
static constexpr auto k_propertiesInterface = "org.freedesktop.DBus.Properties";
|
||||
static const sdbus::ServiceName k_upowerBusName{"org.freedesktop.UPower"};
|
||||
static const sdbus::ObjectPath k_upowerObjectPath{"/org/freedesktop/UPower"};
|
||||
static constexpr auto k_upowerInterface = "org.freedesktop.UPower";
|
||||
static constexpr auto k_deviceInterface = "org.freedesktop.UPower.Device";
|
||||
static constexpr auto k_propertiesInterface = "org.freedesktop.DBus.Properties";
|
||||
|
||||
// UPower device types
|
||||
constexpr std::uint32_t k_deviceTypeBattery = 2;
|
||||
// UPower device types
|
||||
constexpr std::uint32_t k_deviceTypeBattery = 2;
|
||||
|
||||
// UPower battery states
|
||||
constexpr std::uint32_t k_stateCharging = 1;
|
||||
constexpr std::uint32_t k_stateDischarging = 2;
|
||||
constexpr std::uint32_t k_stateEmpty = 3;
|
||||
constexpr std::uint32_t k_stateFullyCharged = 4;
|
||||
constexpr std::uint32_t k_statePendingCharge = 5;
|
||||
constexpr std::uint32_t k_statePendingDischarge = 6;
|
||||
// UPower battery states
|
||||
constexpr std::uint32_t k_stateCharging = 1;
|
||||
constexpr std::uint32_t k_stateDischarging = 2;
|
||||
constexpr std::uint32_t k_stateEmpty = 3;
|
||||
constexpr std::uint32_t k_stateFullyCharged = 4;
|
||||
constexpr std::uint32_t k_statePendingCharge = 5;
|
||||
constexpr std::uint32_t k_statePendingDischarge = 6;
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -52,36 +52,36 @@ std::string batteryStateLabel(BatteryState state) {
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename T>
|
||||
T getPropertyOr(sdbus::IProxy& proxy, std::string_view iface, std::string_view propertyName, T fallback) {
|
||||
try {
|
||||
const sdbus::Variant value = proxy.getProperty(propertyName).onInterface(iface);
|
||||
return value.get<T>();
|
||||
} catch (const sdbus::Error&) {
|
||||
return fallback;
|
||||
template <typename T>
|
||||
T getPropertyOr(sdbus::IProxy& proxy, std::string_view iface, std::string_view propertyName, T fallback) {
|
||||
try {
|
||||
const sdbus::Variant value = proxy.getProperty(propertyName).onInterface(iface);
|
||||
return value.get<T>();
|
||||
} catch (const sdbus::Error&) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BatteryState decodeBatteryState(std::uint32_t raw) {
|
||||
switch (raw) {
|
||||
case k_stateCharging:
|
||||
return BatteryState::Charging;
|
||||
case k_stateDischarging:
|
||||
return BatteryState::Discharging;
|
||||
case k_stateEmpty:
|
||||
return BatteryState::Empty;
|
||||
case k_stateFullyCharged:
|
||||
return BatteryState::FullyCharged;
|
||||
case k_statePendingCharge:
|
||||
return BatteryState::PendingCharge;
|
||||
case k_statePendingDischarge:
|
||||
return BatteryState::PendingDischarge;
|
||||
default:
|
||||
return BatteryState::Unknown;
|
||||
BatteryState decodeBatteryState(std::uint32_t raw) {
|
||||
switch (raw) {
|
||||
case k_stateCharging:
|
||||
return BatteryState::Charging;
|
||||
case k_stateDischarging:
|
||||
return BatteryState::Discharging;
|
||||
case k_stateEmpty:
|
||||
return BatteryState::Empty;
|
||||
case k_stateFullyCharged:
|
||||
return BatteryState::FullyCharged;
|
||||
case k_statePendingCharge:
|
||||
return BatteryState::PendingCharge;
|
||||
case k_statePendingDischarge:
|
||||
return BatteryState::PendingDischarge;
|
||||
default:
|
||||
return BatteryState::Unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constexpr Logger kLog("upower");
|
||||
constexpr Logger kLog("upower");
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
class SystemBus;
|
||||
|
||||
namespace sdbus {
|
||||
class IProxy;
|
||||
class ObjectPath;
|
||||
class IProxy;
|
||||
class ObjectPath;
|
||||
} // namespace sdbus
|
||||
|
||||
enum class BatteryState : std::uint8_t {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#include "idle/idle_inhibitor.h"
|
||||
|
||||
#include "core/log.h"
|
||||
#include "idle-inhibit-unstable-v1-client-protocol.h"
|
||||
#include "ipc/ipc_service.h"
|
||||
#include "wayland/layer_surface.h"
|
||||
#include "wayland/wayland_connection.h"
|
||||
|
||||
#include "idle-inhibit-unstable-v1-client-protocol.h"
|
||||
#include <wayland-client.h>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#include "idle/idle_manager.h"
|
||||
|
||||
#include "core/log.h"
|
||||
#include "wayland/wayland_connection.h"
|
||||
|
||||
#include "ext-idle-notify-v1-client-protocol.h"
|
||||
#include "wayland/wayland_connection.h"
|
||||
|
||||
namespace {
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
#include "launcher/usage_tracker.h"
|
||||
|
||||
#include <json.hpp>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <json.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
|
||||
+6
-3
@@ -54,13 +54,16 @@ namespace {
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
if (argc >= 2) {
|
||||
if (std::strcmp(argv[1], "theme") == 0) return noctalia::theme::runCli(argc, argv);
|
||||
if (std::strcmp(argv[1], "msg") == 0) return noctalia::ipc::runCli(argc, argv);
|
||||
if (std::strcmp(argv[1], "theme") == 0)
|
||||
return noctalia::theme::runCli(argc, argv);
|
||||
if (std::strcmp(argv[1], "msg") == 0)
|
||||
return noctalia::ipc::runCli(argc, argv);
|
||||
}
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
const int rc = runTopLevelFlag(argv[i]);
|
||||
if (rc >= 0) return rc;
|
||||
if (rc >= 0)
|
||||
return rc;
|
||||
}
|
||||
|
||||
return runShell();
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
#include "ipc/ipc_arg_parse.h"
|
||||
#include "ipc/ipc_service.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <pipewire/extensions/metadata.h>
|
||||
#include <pipewire/pipewire.h>
|
||||
#include <spa/param/audio/format-utils.h>
|
||||
@@ -13,12 +18,6 @@
|
||||
#include <spa/pod/builder.h>
|
||||
#include <spa/pod/parser.h>
|
||||
#include <spa/utils/result.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
#include "core/log.h"
|
||||
#include "pipewire/pipewire_service.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <numbers>
|
||||
#include <pipewire/core.h>
|
||||
#include <pipewire/keys.h>
|
||||
#include <pipewire/properties.h>
|
||||
@@ -11,11 +15,6 @@
|
||||
#include <spa/param/audio/raw.h>
|
||||
#include <spa/param/format-utils.h>
|
||||
#include <spa/pod/pod.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <numbers>
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -119,4 +119,3 @@ constexpr Color lerpColor(const Color& a, const Color& b, float t) {
|
||||
.a = a.a + (b.a - a.a) * t,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
|
||||
#include <webp/decode.h>
|
||||
|
||||
#define WUFFS_IMPLEMENTATION
|
||||
@@ -12,46 +11,43 @@
|
||||
|
||||
namespace {
|
||||
|
||||
class RgbaDecodeCallbacks final : public wuffs_aux::DecodeImageCallbacks {
|
||||
public:
|
||||
wuffs_base__pixel_format SelectPixfmt(
|
||||
const wuffs_base__image_config& imageConfig) override {
|
||||
(void)imageConfig;
|
||||
return wuffs_base__make_pixel_format(WUFFS_BASE__PIXEL_FORMAT__RGBA_NONPREMUL);
|
||||
}
|
||||
};
|
||||
class RgbaDecodeCallbacks final : public wuffs_aux::DecodeImageCallbacks {
|
||||
public:
|
||||
wuffs_base__pixel_format SelectPixfmt(const wuffs_base__image_config& imageConfig) override {
|
||||
(void)imageConfig;
|
||||
return wuffs_base__make_pixel_format(WUFFS_BASE__PIXEL_FORMAT__RGBA_NONPREMUL);
|
||||
}
|
||||
};
|
||||
|
||||
// Returns true if the buffer starts with the RIFF....WEBP signature.
|
||||
bool isWebP(const std::uint8_t* data, std::size_t size) {
|
||||
return size >= 12 &&
|
||||
data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'F' &&
|
||||
data[8] == 'W' && data[9] == 'E' && data[10] == 'B' && data[11] == 'P';
|
||||
}
|
||||
|
||||
std::optional<DecodedRasterImage> decodeWebP(
|
||||
const std::uint8_t* data, std::size_t size, std::string* errorMessage) {
|
||||
int width = 0, height = 0;
|
||||
std::uint8_t* rgba = WebPDecodeRGBA(data, size, &width, &height);
|
||||
if (rgba == nullptr) {
|
||||
if (errorMessage != nullptr)
|
||||
*errorMessage = "libwebp: failed to decode WebP image";
|
||||
return std::nullopt;
|
||||
// Returns true if the buffer starts with the RIFF....WEBP signature.
|
||||
bool isWebP(const std::uint8_t* data, std::size_t size) {
|
||||
return size >= 12 && data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'F' && data[8] == 'W' &&
|
||||
data[9] == 'E' && data[10] == 'B' && data[11] == 'P';
|
||||
}
|
||||
|
||||
DecodedRasterImage decoded;
|
||||
decoded.width = width;
|
||||
decoded.height = height;
|
||||
std::size_t bytes = static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4;
|
||||
decoded.pixels.resize(bytes);
|
||||
std::memcpy(decoded.pixels.data(), rgba, bytes);
|
||||
WebPFree(rgba);
|
||||
return decoded;
|
||||
}
|
||||
std::optional<DecodedRasterImage> decodeWebP(const std::uint8_t* data, std::size_t size, std::string* errorMessage) {
|
||||
int width = 0, height = 0;
|
||||
std::uint8_t* rgba = WebPDecodeRGBA(data, size, &width, &height);
|
||||
if (rgba == nullptr) {
|
||||
if (errorMessage != nullptr)
|
||||
*errorMessage = "libwebp: failed to decode WebP image";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
DecodedRasterImage decoded;
|
||||
decoded.width = width;
|
||||
decoded.height = height;
|
||||
std::size_t bytes = static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4;
|
||||
decoded.pixels.resize(bytes);
|
||||
std::memcpy(decoded.pixels.data(), rgba, bytes);
|
||||
WebPFree(rgba);
|
||||
return decoded;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<DecodedRasterImage>
|
||||
decodeRasterImage(const std::uint8_t* data, std::size_t size, std::string* errorMessage) {
|
||||
std::optional<DecodedRasterImage> decodeRasterImage(const std::uint8_t* data, std::size_t size,
|
||||
std::string* errorMessage) {
|
||||
if ((data == nullptr) || (size == 0)) {
|
||||
if (errorMessage != nullptr) {
|
||||
*errorMessage = "empty image buffer";
|
||||
|
||||
@@ -12,6 +12,5 @@ struct DecodedRasterImage {
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::optional<DecodedRasterImage>
|
||||
decodeRasterImage(const std::uint8_t* data, std::size_t size, std::string* errorMessage = nullptr);
|
||||
|
||||
[[nodiscard]] std::optional<DecodedRasterImage> decodeRasterImage(const std::uint8_t* data, std::size_t size,
|
||||
std::string* errorMessage = nullptr);
|
||||
|
||||
+5
-10
@@ -53,8 +53,7 @@ struct Mat3 {
|
||||
}
|
||||
|
||||
[[nodiscard]] float determinant() const {
|
||||
return m[0] * (m[4] * m[8] - m[7] * m[5]) - m[3] * (m[1] * m[8] - m[7] * m[2]) +
|
||||
m[6] * (m[1] * m[5] - m[4] * m[2]);
|
||||
return m[0] * (m[4] * m[8] - m[7] * m[5]) - m[3] * (m[1] * m[8] - m[7] * m[2]) + m[6] * (m[1] * m[5] - m[4] * m[2]);
|
||||
}
|
||||
|
||||
[[nodiscard]] Mat3 inverse() const {
|
||||
@@ -66,14 +65,10 @@ struct Mat3 {
|
||||
const float invDet = 1.0f / det;
|
||||
Mat3 out;
|
||||
out.m = {
|
||||
(m[4] * m[8] - m[7] * m[5]) * invDet,
|
||||
(m[7] * m[2] - m[1] * m[8]) * invDet,
|
||||
(m[1] * m[5] - m[4] * m[2]) * invDet,
|
||||
(m[6] * m[5] - m[3] * m[8]) * invDet,
|
||||
(m[0] * m[8] - m[6] * m[2]) * invDet,
|
||||
(m[3] * m[2] - m[0] * m[5]) * invDet,
|
||||
(m[3] * m[7] - m[6] * m[4]) * invDet,
|
||||
(m[6] * m[1] - m[0] * m[7]) * invDet,
|
||||
(m[4] * m[8] - m[7] * m[5]) * invDet, (m[7] * m[2] - m[1] * m[8]) * invDet,
|
||||
(m[1] * m[5] - m[4] * m[2]) * invDet, (m[6] * m[5] - m[3] * m[8]) * invDet,
|
||||
(m[0] * m[8] - m[6] * m[2]) * invDet, (m[3] * m[2] - m[0] * m[5]) * invDet,
|
||||
(m[3] * m[7] - m[6] * m[4]) * invDet, (m[6] * m[1] - m[0] * m[7]) * invDet,
|
||||
(m[0] * m[4] - m[3] * m[1]) * invDet,
|
||||
};
|
||||
return out;
|
||||
|
||||
@@ -6,29 +6,29 @@
|
||||
|
||||
namespace {
|
||||
|
||||
GLuint compileShader(GLenum type, const char* source) {
|
||||
const GLuint shader = glCreateShader(type);
|
||||
if (shader == 0) {
|
||||
throw std::runtime_error("glCreateShader failed");
|
||||
GLuint compileShader(GLenum type, const char* source) {
|
||||
const GLuint shader = glCreateShader(type);
|
||||
if (shader == 0) {
|
||||
throw std::runtime_error("glCreateShader failed");
|
||||
}
|
||||
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
GLint logLength = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLength);
|
||||
std::string log(static_cast<std::size_t>(std::max(logLength, 1)), '\0');
|
||||
glGetShaderInfoLog(shader, logLength, nullptr, log.data());
|
||||
glDeleteShader(shader);
|
||||
throw std::runtime_error("shader compilation failed: " + log);
|
||||
}
|
||||
|
||||
return shader;
|
||||
}
|
||||
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_FALSE) {
|
||||
GLint logLength = 0;
|
||||
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLength);
|
||||
std::string log(static_cast<std::size_t>(std::max(logLength, 1)), '\0');
|
||||
glGetShaderInfoLog(shader, logLength, nullptr, log.data());
|
||||
glDeleteShader(shader);
|
||||
throw std::runtime_error("shader compilation failed: " + log);
|
||||
}
|
||||
|
||||
return shader;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ShaderProgram::~ShaderProgram() { destroy(); }
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include "render/gl_shared_context.h"
|
||||
|
||||
namespace {
|
||||
constexpr Logger kLog("texcache");
|
||||
constexpr Logger kLog("texcache");
|
||||
} // namespace
|
||||
|
||||
SharedTextureCache::~SharedTextureCache() {
|
||||
|
||||
@@ -21,29 +21,29 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("texture");
|
||||
constexpr Logger kLog("texture");
|
||||
|
||||
bool endsWith(const std::string& str, const std::string& suffix) {
|
||||
if (suffix.size() > str.size()) {
|
||||
return false;
|
||||
bool endsWith(const std::string& str, const std::string& suffix) {
|
||||
if (suffix.size() > str.size()) {
|
||||
return false;
|
||||
}
|
||||
return std::equal(suffix.rbegin(), suffix.rend(), str.rbegin());
|
||||
}
|
||||
return std::equal(suffix.rbegin(), suffix.rend(), str.rbegin());
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> readFile(const std::string& path) {
|
||||
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
||||
if (!file) {
|
||||
return {};
|
||||
std::vector<std::uint8_t> readFile(const std::string& path) {
|
||||
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
||||
if (!file) {
|
||||
return {};
|
||||
}
|
||||
const auto size = file.tellg();
|
||||
if (size <= 0) {
|
||||
return {};
|
||||
}
|
||||
std::vector<std::uint8_t> data(static_cast<std::size_t>(size));
|
||||
file.seekg(0);
|
||||
file.read(reinterpret_cast<char*>(data.data()), size);
|
||||
return data;
|
||||
}
|
||||
const auto size = file.tellg();
|
||||
if (size <= 0) {
|
||||
return {};
|
||||
}
|
||||
std::vector<std::uint8_t> data(static_cast<std::size_t>(size));
|
||||
file.seekg(0);
|
||||
file.read(reinterpret_cast<char*>(data.data()), size);
|
||||
return data;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -118,13 +118,12 @@ TextureHandle TextureManager::loadFromFile(const std::string& path, int targetSi
|
||||
return decodeEncodedRaster(fileData.data(), fileData.size(), &path, mipmap);
|
||||
}
|
||||
|
||||
TextureHandle TextureManager::loadFromEncodedBytes(const std::uint8_t* data, std::size_t size,
|
||||
bool mipmap) {
|
||||
TextureHandle TextureManager::loadFromEncodedBytes(const std::uint8_t* data, std::size_t size, bool mipmap) {
|
||||
return decodeEncodedRaster(data, size, nullptr, mipmap);
|
||||
}
|
||||
|
||||
TextureHandle TextureManager::loadFromRaw(const std::uint8_t* data, std::size_t size, int width,
|
||||
int height, int stride, PixmapFormat format, bool mipmap) {
|
||||
TextureHandle TextureManager::loadFromRaw(const std::uint8_t* data, std::size_t size, int width, int height, int stride,
|
||||
PixmapFormat format, bool mipmap) {
|
||||
if (data == nullptr || size == 0 || width <= 0 || height <= 0) {
|
||||
return {};
|
||||
}
|
||||
@@ -141,8 +140,8 @@ TextureHandle TextureManager::loadFromRaw(const std::uint8_t* data, std::size_t
|
||||
|
||||
const std::size_t requiredSize = (heightSize - 1U) * actualStride + minStride;
|
||||
if (size < requiredSize) {
|
||||
kLog.warn("raw pixmap buffer too small: width={} height={} stride={} have={} need={}", width,
|
||||
height, stride, size, requiredSize);
|
||||
kLog.warn("raw pixmap buffer too small: width={} height={} stride={} have={} need={}", width, height, stride, size,
|
||||
requiredSize);
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -159,19 +158,34 @@ TextureHandle TextureManager::loadFromRaw(const std::uint8_t* data, std::size_t
|
||||
|
||||
switch (format) {
|
||||
case PixmapFormat::RGBA:
|
||||
d[0] = s[0]; d[1] = s[1]; d[2] = s[2]; d[3] = s[3];
|
||||
d[0] = s[0];
|
||||
d[1] = s[1];
|
||||
d[2] = s[2];
|
||||
d[3] = s[3];
|
||||
break;
|
||||
case PixmapFormat::BGRA:
|
||||
d[0] = s[2]; d[1] = s[1]; d[2] = s[0]; d[3] = s[3];
|
||||
d[0] = s[2];
|
||||
d[1] = s[1];
|
||||
d[2] = s[0];
|
||||
d[3] = s[3];
|
||||
break;
|
||||
case PixmapFormat::ARGB:
|
||||
d[0] = s[1]; d[1] = s[2]; d[2] = s[3]; d[3] = s[0];
|
||||
d[0] = s[1];
|
||||
d[1] = s[2];
|
||||
d[2] = s[3];
|
||||
d[3] = s[0];
|
||||
break;
|
||||
case PixmapFormat::RGB:
|
||||
d[0] = s[0]; d[1] = s[1]; d[2] = s[2]; d[3] = 255;
|
||||
d[0] = s[0];
|
||||
d[1] = s[1];
|
||||
d[2] = s[2];
|
||||
d[3] = 255;
|
||||
break;
|
||||
case PixmapFormat::BGR:
|
||||
d[0] = s[2]; d[1] = s[1]; d[2] = s[0]; d[3] = 255;
|
||||
d[0] = s[2];
|
||||
d[1] = s[1];
|
||||
d[2] = s[0];
|
||||
d[3] = 255;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -195,8 +209,7 @@ void TextureManager::cleanup() {
|
||||
}
|
||||
}
|
||||
|
||||
TextureHandle TextureManager::uploadRgba(const std::uint8_t* data, int width, int height,
|
||||
bool mipmap) {
|
||||
TextureHandle TextureManager::uploadRgba(const std::uint8_t* data, int width, int height, bool mipmap) {
|
||||
GLuint tex = 0;
|
||||
glGenTextures(1, &tex);
|
||||
glBindTexture(GL_TEXTURE_2D, tex);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <GLES2/gl2.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -28,19 +27,16 @@ public:
|
||||
TextureManager(const TextureManager&) = delete;
|
||||
TextureManager& operator=(const TextureManager&) = delete;
|
||||
|
||||
[[nodiscard]] TextureHandle loadFromFile(const std::string& path, int targetSize = 0,
|
||||
bool mipmap = false);
|
||||
[[nodiscard]] TextureHandle loadFromEncodedBytes(const std::uint8_t* data, std::size_t size,
|
||||
bool mipmap = false);
|
||||
[[nodiscard]] TextureHandle loadFromRaw(const std::uint8_t* data, std::size_t size, int width,
|
||||
int height, int stride, PixmapFormat format,
|
||||
bool mipmap = false);
|
||||
[[nodiscard]] TextureHandle loadFromFile(const std::string& path, int targetSize = 0, bool mipmap = false);
|
||||
[[nodiscard]] TextureHandle loadFromEncodedBytes(const std::uint8_t* data, std::size_t size, bool mipmap = false);
|
||||
[[nodiscard]] TextureHandle loadFromRaw(const std::uint8_t* data, std::size_t size, int width, int height, int stride,
|
||||
PixmapFormat format, bool mipmap = false);
|
||||
void unload(TextureHandle& handle);
|
||||
void cleanup();
|
||||
|
||||
private:
|
||||
TextureHandle decodeEncodedRaster(const std::uint8_t* data, std::size_t size,
|
||||
const std::string* debugPath = nullptr, bool mipmap = false);
|
||||
TextureHandle decodeEncodedRaster(const std::uint8_t* data, std::size_t size, const std::string* debugPath = nullptr,
|
||||
bool mipmap = false);
|
||||
TextureHandle uploadRgba(const std::uint8_t* data, int width, int height, bool mipmap = false);
|
||||
std::vector<GLuint> m_textures;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kVertexShader[] = R"(
|
||||
constexpr char kVertexShader[] = R"(
|
||||
precision highp float;
|
||||
attribute vec2 a_position;
|
||||
varying vec2 v_texcoord;
|
||||
@@ -16,9 +16,9 @@ void main() {
|
||||
}
|
||||
)";
|
||||
|
||||
// Separable Gaussian blur with a fixed 81-tap loop (-40..40).
|
||||
// u_radius controls how many taps are active; taps beyond u_radius are skipped.
|
||||
constexpr char kFragmentShader[] = R"(
|
||||
// Separable Gaussian blur with a fixed 81-tap loop (-40..40).
|
||||
// u_radius controls how many taps are active; taps beyond u_radius are skipped.
|
||||
constexpr char kFragmentShader[] = R"(
|
||||
precision highp float;
|
||||
uniform sampler2D u_texture;
|
||||
uniform vec2 u_texelSize;
|
||||
@@ -52,20 +52,18 @@ void BlurProgram::ensureInitialized() {
|
||||
m_program.create(kVertexShader, kFragmentShader);
|
||||
|
||||
const auto id = m_program.id();
|
||||
m_posLoc = glGetAttribLocation(id, "a_position");
|
||||
m_texLoc = glGetUniformLocation(id, "u_texture");
|
||||
m_texelSzLoc = glGetUniformLocation(id, "u_texelSize");
|
||||
m_posLoc = glGetAttribLocation(id, "a_position");
|
||||
m_texLoc = glGetUniformLocation(id, "u_texture");
|
||||
m_texelSzLoc = glGetUniformLocation(id, "u_texelSize");
|
||||
m_directionLoc = glGetUniformLocation(id, "u_direction");
|
||||
m_radiusLoc = glGetUniformLocation(id, "u_radius");
|
||||
m_radiusLoc = glGetUniformLocation(id, "u_radius");
|
||||
|
||||
if (m_posLoc < 0 || m_texLoc < 0 || m_texelSzLoc < 0 || m_directionLoc < 0 || m_radiusLoc < 0) {
|
||||
throw std::runtime_error("failed to query blur shader locations");
|
||||
}
|
||||
}
|
||||
|
||||
void BlurProgram::destroy() {
|
||||
m_program.destroy();
|
||||
}
|
||||
void BlurProgram::destroy() { m_program.destroy(); }
|
||||
|
||||
void BlurProgram::draw(GLuint srcTex, std::uint32_t width, std::uint32_t height, float dirX, float dirY,
|
||||
float radius) const {
|
||||
|
||||
@@ -17,9 +17,9 @@ public:
|
||||
|
||||
private:
|
||||
ShaderProgram m_program;
|
||||
GLint m_posLoc = -1;
|
||||
GLint m_texLoc = -1;
|
||||
GLint m_texelSzLoc = -1;
|
||||
GLint m_posLoc = -1;
|
||||
GLint m_texLoc = -1;
|
||||
GLint m_texelSzLoc = -1;
|
||||
GLint m_directionLoc = -1;
|
||||
GLint m_radiusLoc = -1;
|
||||
GLint m_radiusLoc = -1;
|
||||
};
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
namespace {
|
||||
|
||||
// Positions a unit quad, applies a pixel-space transform, converts to NDC.
|
||||
constexpr char kVertexShaderSource[] = R"(
|
||||
// Positions a unit quad, applies a pixel-space transform, converts to NDC.
|
||||
constexpr char kVertexShaderSource[] = R"(
|
||||
precision highp float;
|
||||
|
||||
attribute vec2 a_position;
|
||||
@@ -29,13 +29,13 @@ void main() {
|
||||
}
|
||||
)";
|
||||
|
||||
// Fragment shader: two modes selected by u_tint_mode.
|
||||
// mode 0 (RGBA): texture stores a premultiplied RGBA glyph; scale by opacity.
|
||||
// mode 1 (tint): texture stores an alpha coverage mask; the output is
|
||||
// premul(tint) * coverage * opacity.
|
||||
// Both paths output premultiplied to match the pipeline-wide
|
||||
// GL_ONE / GL_ONE_MINUS_SRC_ALPHA blend mode.
|
||||
constexpr char kFragmentShaderSource[] = R"(
|
||||
// Fragment shader: two modes selected by u_tint_mode.
|
||||
// mode 0 (RGBA): texture stores a premultiplied RGBA glyph; scale by opacity.
|
||||
// mode 1 (tint): texture stores an alpha coverage mask; the output is
|
||||
// premul(tint) * coverage * opacity.
|
||||
// Both paths output premultiplied to match the pipeline-wide
|
||||
// GL_ONE / GL_ONE_MINUS_SRC_ALPHA blend mode.
|
||||
constexpr char kFragmentShaderSource[] = R"(
|
||||
precision highp float;
|
||||
|
||||
uniform sampler2D u_texture;
|
||||
@@ -63,15 +63,15 @@ void GlyphProgram::ensureInitialized() {
|
||||
}
|
||||
|
||||
m_program.create(kVertexShaderSource, kFragmentShaderSource);
|
||||
m_positionLocation = glGetAttribLocation(m_program.id(), "a_position");
|
||||
m_texCoordLocation = glGetAttribLocation(m_program.id(), "a_texcoord");
|
||||
m_positionLocation = glGetAttribLocation(m_program.id(), "a_position");
|
||||
m_texCoordLocation = glGetAttribLocation(m_program.id(), "a_texcoord");
|
||||
m_surfaceSizeLocation = glGetUniformLocation(m_program.id(), "u_surface_size");
|
||||
m_rectLocation = glGetUniformLocation(m_program.id(), "u_size");
|
||||
m_opacityLocation = glGetUniformLocation(m_program.id(), "u_opacity");
|
||||
m_samplerLocation = glGetUniformLocation(m_program.id(), "u_texture");
|
||||
m_rectLocation = glGetUniformLocation(m_program.id(), "u_size");
|
||||
m_opacityLocation = glGetUniformLocation(m_program.id(), "u_opacity");
|
||||
m_samplerLocation = glGetUniformLocation(m_program.id(), "u_texture");
|
||||
m_transformLocation = glGetUniformLocation(m_program.id(), "u_transform");
|
||||
m_tintLocation = glGetUniformLocation(m_program.id(), "u_tint");
|
||||
m_tintModeLocation = glGetUniformLocation(m_program.id(), "u_tint_mode");
|
||||
m_tintLocation = glGetUniformLocation(m_program.id(), "u_tint");
|
||||
m_tintModeLocation = glGetUniformLocation(m_program.id(), "u_tint_mode");
|
||||
|
||||
if (m_positionLocation < 0 || m_texCoordLocation < 0 || m_surfaceSizeLocation < 0 || m_rectLocation < 0 ||
|
||||
m_opacityLocation < 0 || m_samplerLocation < 0 || m_transformLocation < 0 || m_tintLocation < 0 ||
|
||||
@@ -82,20 +82,19 @@ void GlyphProgram::ensureInitialized() {
|
||||
|
||||
void GlyphProgram::destroy() {
|
||||
m_program.destroy();
|
||||
m_positionLocation = -1;
|
||||
m_texCoordLocation = -1;
|
||||
m_positionLocation = -1;
|
||||
m_texCoordLocation = -1;
|
||||
m_surfaceSizeLocation = -1;
|
||||
m_rectLocation = -1;
|
||||
m_opacityLocation = -1;
|
||||
m_samplerLocation = -1;
|
||||
m_rectLocation = -1;
|
||||
m_opacityLocation = -1;
|
||||
m_samplerLocation = -1;
|
||||
m_transformLocation = -1;
|
||||
m_tintLocation = -1;
|
||||
m_tintModeLocation = -1;
|
||||
m_tintLocation = -1;
|
||||
m_tintModeLocation = -1;
|
||||
}
|
||||
|
||||
void GlyphProgram::bindCommon(GLuint texture, float surfaceWidth, float surfaceHeight, float width, float height,
|
||||
float u0, float v0, float u1, float v1, float opacity,
|
||||
const Mat3& transform) const {
|
||||
float u0, float v0, float u1, float v1, float opacity, const Mat3& transform) const {
|
||||
const std::array<GLfloat, 12> positions = {
|
||||
0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f,
|
||||
};
|
||||
@@ -123,9 +122,8 @@ void GlyphProgram::bindCommon(GLuint texture, float surfaceWidth, float surfaceH
|
||||
glDisableVertexAttribArray(texAttr);
|
||||
}
|
||||
|
||||
void GlyphProgram::draw(GLuint texture, float surfaceWidth, float surfaceHeight, float width, float height,
|
||||
float u0, float v0, float u1, float v1, float opacity,
|
||||
const Mat3& transform) const {
|
||||
void GlyphProgram::draw(GLuint texture, float surfaceWidth, float surfaceHeight, float width, float height, float u0,
|
||||
float v0, float u1, float v1, float opacity, const Mat3& transform) const {
|
||||
if (!m_program.isValid() || texture == 0 || width <= 0.0f || height <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
@@ -136,8 +134,8 @@ void GlyphProgram::draw(GLuint texture, float surfaceWidth, float surfaceHeight,
|
||||
}
|
||||
|
||||
void GlyphProgram::drawTinted(GLuint texture, float surfaceWidth, float surfaceHeight, float width, float height,
|
||||
float u0, float v0, float u1, float v1, float opacity, const Color& tint,
|
||||
const Mat3& transform) const {
|
||||
float u0, float v0, float u1, float v1, float opacity, const Color& tint,
|
||||
const Mat3& transform) const {
|
||||
if (!m_program.isValid() || texture == 0 || width <= 0.0f || height <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kVertexShaderSource[] = R"(
|
||||
constexpr char kVertexShaderSource[] = R"(
|
||||
precision highp float;
|
||||
|
||||
attribute vec2 a_position;
|
||||
@@ -53,7 +53,7 @@ void main() {
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr char kFragmentShaderSource[] = R"(
|
||||
constexpr char kFragmentShaderSource[] = R"(
|
||||
precision highp float;
|
||||
|
||||
uniform sampler2D u_texture;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kVertexShaderSource[] = R"(
|
||||
constexpr char kVertexShaderSource[] = R"(
|
||||
precision highp float;
|
||||
|
||||
attribute vec2 a_position;
|
||||
@@ -25,7 +25,7 @@ void main() {
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr char kFragmentShaderSource[] = R"(
|
||||
constexpr char kFragmentShaderSource[] = R"(
|
||||
precision highp float;
|
||||
|
||||
uniform vec4 u_start_color;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kVertexShaderSource[] = R"(
|
||||
constexpr char kVertexShaderSource[] = R"(
|
||||
precision highp float;
|
||||
|
||||
attribute vec2 a_position;
|
||||
@@ -29,7 +29,7 @@ void main() {
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr char kFragmentShaderSource[] = R"(
|
||||
constexpr char kFragmentShaderSource[] = R"(
|
||||
precision highp float;
|
||||
|
||||
uniform vec2 u_rect_size;
|
||||
@@ -168,7 +168,7 @@ void RectProgram::destroy() {
|
||||
}
|
||||
|
||||
void RectProgram::draw(float surfaceWidth, float surfaceHeight, float width, float height,
|
||||
const RoundedRectStyle& style, const Mat3& transform) const {
|
||||
const RoundedRectStyle& style, const Mat3& transform) const {
|
||||
if (!m_program.isValid() || width <= 0.0f || height <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@ struct RoundedRectStyle {
|
||||
|
||||
constexpr bool operator==(const RoundedRectStyle& lhs, const RoundedRectStyle& rhs) noexcept {
|
||||
return lhs.fill == rhs.fill && lhs.fillEnd == rhs.fillEnd && lhs.border == rhs.border &&
|
||||
lhs.fillMode == rhs.fillMode && lhs.gradientDirection == rhs.gradientDirection &&
|
||||
lhs.radius == rhs.radius && lhs.softness == rhs.softness && lhs.borderWidth == rhs.borderWidth;
|
||||
lhs.fillMode == rhs.fillMode && lhs.gradientDirection == rhs.gradientDirection && lhs.radius == rhs.radius &&
|
||||
lhs.softness == rhs.softness && lhs.borderWidth == rhs.borderWidth;
|
||||
}
|
||||
|
||||
class RectProgram {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kVertexShaderSource[] = R"(
|
||||
constexpr char kVertexShaderSource[] = R"(
|
||||
precision highp float;
|
||||
|
||||
attribute vec2 a_position;
|
||||
@@ -29,7 +29,7 @@ void main() {
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr char kFragmentShaderSource[] = R"(
|
||||
constexpr char kFragmentShaderSource[] = R"(
|
||||
precision highp float;
|
||||
|
||||
uniform vec2 u_rect_size;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kVertexShader[] = R"(
|
||||
constexpr char kVertexShader[] = R"(
|
||||
precision highp float;
|
||||
attribute vec2 a_position;
|
||||
varying vec2 v_texcoord;
|
||||
@@ -16,9 +16,9 @@ void main() {
|
||||
}
|
||||
)";
|
||||
|
||||
// Common GLSL functions shared by all transition fragment shaders.
|
||||
// Included at the top of each fragment source via string concatenation.
|
||||
constexpr char kCommonFunctions[] = R"(
|
||||
// Common GLSL functions shared by all transition fragment shaders.
|
||||
// Included at the top of each fragment source via string concatenation.
|
||||
constexpr char kCommonFunctions[] = R"(
|
||||
precision highp float;
|
||||
|
||||
uniform sampler2D u_source1;
|
||||
@@ -89,7 +89,7 @@ vec4 sampleWithFillMode(sampler2D tex, vec2 uv, float imgWidth, float imgHeight)
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr char kFadeFragment[] = R"(
|
||||
constexpr char kFadeFragment[] = R"(
|
||||
void main() {
|
||||
vec2 uv = v_texcoord;
|
||||
vec4 color1 = sampleWithFillMode(u_source1, uv, u_imageWidth1, u_imageHeight1);
|
||||
@@ -98,7 +98,7 @@ void main() {
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr char kWipeFragment[] = R"(
|
||||
constexpr char kWipeFragment[] = R"(
|
||||
uniform float u_direction;
|
||||
uniform float u_smoothness;
|
||||
|
||||
@@ -135,7 +135,7 @@ void main() {
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr char kDiscFragment[] = R"(
|
||||
constexpr char kDiscFragment[] = R"(
|
||||
uniform float u_smoothness;
|
||||
uniform float u_centerX;
|
||||
uniform float u_centerY;
|
||||
@@ -166,7 +166,7 @@ void main() {
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr char kStripesFragment[] = R"(
|
||||
constexpr char kStripesFragment[] = R"(
|
||||
uniform float u_smoothness;
|
||||
uniform float u_aspectRatio;
|
||||
uniform float u_stripeCount;
|
||||
@@ -216,7 +216,7 @@ void main() {
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr char kZoomFragment[] = R"(
|
||||
constexpr char kZoomFragment[] = R"(
|
||||
void main() {
|
||||
vec2 uv = v_texcoord;
|
||||
|
||||
@@ -237,7 +237,7 @@ void main() {
|
||||
}
|
||||
)";
|
||||
|
||||
constexpr char kHoneycombFragment[] = R"(
|
||||
constexpr char kHoneycombFragment[] = R"(
|
||||
uniform float u_cellSize;
|
||||
uniform float u_centerX;
|
||||
uniform float u_centerY;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "render/core/shader_program.h"
|
||||
|
||||
#include <GLES2/gl2.h>
|
||||
|
||||
#include <array>
|
||||
|
||||
struct TransitionParams {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "render/render_context.h"
|
||||
|
||||
#include "core/log.h"
|
||||
#include "core/ui_phase.h"
|
||||
#include "render/gl_shared_context.h"
|
||||
@@ -9,17 +10,15 @@
|
||||
#include "render/scene/rect_node.h"
|
||||
#include "render/scene/spinner_node.h"
|
||||
#include "render/scene/text_node.h"
|
||||
|
||||
#include "ui/style.h"
|
||||
|
||||
#include <GLES2/gl2.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
#include <GLES2/gl2.h>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("render");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "render/render_target.h"
|
||||
|
||||
#include "render/render_context.h"
|
||||
|
||||
#include <wayland-egl.h>
|
||||
|
||||
@@ -21,8 +21,8 @@ public:
|
||||
void pointerMotion(float x, float y, std::uint32_t serial);
|
||||
// Returns true if the event was consumed by a scene widget
|
||||
bool pointerButton(float x, float y, std::uint32_t button, bool pressed);
|
||||
bool pointerAxis(float x, float y, std::uint32_t axis, std::uint32_t axisSource, double value,
|
||||
std::int32_t discrete, std::int32_t value120, float lines);
|
||||
bool pointerAxis(float x, float y, std::uint32_t axis, std::uint32_t axisSource, double value, std::int32_t discrete,
|
||||
std::int32_t value120, float lines);
|
||||
|
||||
// Dispatch keyboard events to the focused area
|
||||
void keyEvent(std::uint32_t sym, std::uint32_t utf32, std::uint32_t modifiers, bool pressed, bool preedit = false);
|
||||
|
||||
+26
-26
@@ -1,6 +1,6 @@
|
||||
#include "core/ui_phase.h"
|
||||
#include "render/scene/node.h"
|
||||
|
||||
#include "core/ui_phase.h"
|
||||
#include "render/animation/animation_manager.h"
|
||||
#include "render/core/mat3.h"
|
||||
|
||||
@@ -9,32 +9,32 @@
|
||||
|
||||
namespace {
|
||||
|
||||
Mat3 localTransform(const Node* node) {
|
||||
const float cx = node->width() * 0.5f;
|
||||
const float cy = node->height() * 0.5f;
|
||||
return Mat3::translation(node->x(), node->y()) * Mat3::translation(cx, cy) * Mat3::rotation(node->rotation()) *
|
||||
Mat3::scale(node->scale(), node->scale()) * Mat3::translation(-cx, -cy);
|
||||
}
|
||||
|
||||
Mat3 computeWorldTransform(const Node* node) {
|
||||
Mat3 world = Mat3::identity();
|
||||
for (const Node* current = node; current != nullptr; current = current->parent()) {
|
||||
world = localTransform(current) * world;
|
||||
}
|
||||
return world;
|
||||
}
|
||||
|
||||
bool pointInsideNode(const Node* node, float sceneX, float sceneY, float& localX, float& localY) {
|
||||
if (node == nullptr) {
|
||||
return false;
|
||||
Mat3 localTransform(const Node* node) {
|
||||
const float cx = node->width() * 0.5f;
|
||||
const float cy = node->height() * 0.5f;
|
||||
return Mat3::translation(node->x(), node->y()) * Mat3::translation(cx, cy) * Mat3::rotation(node->rotation()) *
|
||||
Mat3::scale(node->scale(), node->scale()) * Mat3::translation(-cx, -cy);
|
||||
}
|
||||
|
||||
const Mat3 inverse = computeWorldTransform(node).inverse();
|
||||
const Vec2 local = inverse.transformPoint(sceneX, sceneY);
|
||||
localX = local.x;
|
||||
localY = local.y;
|
||||
return localX >= 0.0f && localX < node->width() && localY >= 0.0f && localY < node->height();
|
||||
}
|
||||
Mat3 computeWorldTransform(const Node* node) {
|
||||
Mat3 world = Mat3::identity();
|
||||
for (const Node* current = node; current != nullptr; current = current->parent()) {
|
||||
world = localTransform(current) * world;
|
||||
}
|
||||
return world;
|
||||
}
|
||||
|
||||
bool pointInsideNode(const Node* node, float sceneX, float sceneY, float& localX, float& localY) {
|
||||
if (node == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Mat3 inverse = computeWorldTransform(node).inverse();
|
||||
const Vec2 local = inverse.transformPoint(sceneX, sceneY);
|
||||
localX = local.x;
|
||||
localY = local.y;
|
||||
return localX >= 0.0f && localX < node->width() && localY >= 0.0f && localY < node->height();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -307,7 +307,7 @@ bool Node::mapFromScene(const Node* node, float sceneX, float sceneY, float& out
|
||||
}
|
||||
|
||||
void Node::transformedBounds(const Node* node, const Mat3& world, float& outLeft, float& outTop, float& outRight,
|
||||
float& outBottom) {
|
||||
float& outBottom) {
|
||||
const Vec2 corners[] = {
|
||||
world.transformPoint(0.0f, 0.0f),
|
||||
world.transformPoint(node->width(), 0.0f),
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
#include "core/log.h"
|
||||
#include "render/programs/glyph_program.h"
|
||||
|
||||
#include <cairo.h>
|
||||
#include <cairo-ft.h>
|
||||
|
||||
#include <cairo.h>
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H
|
||||
|
||||
@@ -17,22 +16,20 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("text");
|
||||
constexpr Logger kLog("text");
|
||||
|
||||
constexpr std::uint32_t kSizeQuant = 64;
|
||||
constexpr std::uint32_t kScaleQuant = 64;
|
||||
constexpr std::uint32_t kSizeQuant = 64;
|
||||
constexpr std::uint32_t kScaleQuant = 64;
|
||||
|
||||
inline std::uint32_t quantizeSize(float v) {
|
||||
return static_cast<std::uint32_t>(std::max(0.0f, v) * static_cast<float>(kSizeQuant) + 0.5f);
|
||||
}
|
||||
inline std::uint32_t quantizeSize(float v) {
|
||||
return static_cast<std::uint32_t>(std::max(0.0f, v) * static_cast<float>(kSizeQuant) + 0.5f);
|
||||
}
|
||||
|
||||
inline std::uint16_t quantizeScale(float v) {
|
||||
return static_cast<std::uint16_t>(std::max(0.0f, v) * static_cast<float>(kScaleQuant) + 0.5f);
|
||||
}
|
||||
inline std::uint16_t quantizeScale(float v) {
|
||||
return static_cast<std::uint16_t>(std::max(0.0f, v) * static_cast<float>(kScaleQuant) + 0.5f);
|
||||
}
|
||||
|
||||
void hashCombine(std::size_t& seed, std::size_t v) {
|
||||
seed ^= v + 0x9E3779B97F4A7C15ULL + (seed << 12) + (seed >> 4);
|
||||
}
|
||||
void hashCombine(std::size_t& seed, std::size_t v) { seed ^= v + 0x9E3779B97F4A7C15ULL + (seed << 12) + (seed >> 4); }
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -100,9 +97,7 @@ void CairoGlyphRenderer::setContentScale(float scale) {
|
||||
}
|
||||
}
|
||||
|
||||
void CairoGlyphRenderer::touch(CacheMap::iterator it) {
|
||||
m_lru.splice(m_lru.begin(), m_lru, it->second.lruIt);
|
||||
}
|
||||
void CairoGlyphRenderer::touch(CacheMap::iterator it) { m_lru.splice(m_lru.begin(), m_lru, it->second.lruIt); }
|
||||
|
||||
void CairoGlyphRenderer::evict(CacheMap::iterator it) {
|
||||
if (it->second.texture != 0) {
|
||||
@@ -152,7 +147,7 @@ CairoGlyphRenderer::TextMetrics CairoGlyphRenderer::measureGlyph(char32_t codepo
|
||||
out.width = static_cast<float>(m.horiAdvance) / 64.0f * invScale;
|
||||
out.left = 0.0f;
|
||||
out.right = w;
|
||||
out.top = -bearingY; // top of ink = -bearingY relative to baseline
|
||||
out.top = -bearingY; // top of ink = -bearingY relative to baseline
|
||||
out.bottom = height - bearingY;
|
||||
return out;
|
||||
}
|
||||
@@ -284,8 +279,8 @@ CairoGlyphRenderer::CacheEntry* CairoGlyphRenderer::lookupOrRasterize(char32_t c
|
||||
return &ins->second;
|
||||
}
|
||||
|
||||
void CairoGlyphRenderer::drawGlyph(float surfaceWidth, float surfaceHeight, float x, float baselineY, char32_t codepoint,
|
||||
float fontSize, const Color& color, const Mat3& transform) {
|
||||
void CairoGlyphRenderer::drawGlyph(float surfaceWidth, float surfaceHeight, float x, float baselineY,
|
||||
char32_t codepoint, float fontSize, const Color& color, const Mat3& transform) {
|
||||
if (m_face == nullptr || m_program == nullptr || codepoint == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "render/core/mat3.h"
|
||||
|
||||
#include <GLES2/gl2.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <list>
|
||||
#include <string>
|
||||
|
||||
@@ -3,94 +3,95 @@
|
||||
#include "core/log.h"
|
||||
#include "render/programs/glyph_program.h"
|
||||
|
||||
#include <cairo.h>
|
||||
#include <fontconfig/fontconfig.h>
|
||||
#include <pango/pango.h>
|
||||
#include <pango/pangocairo.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cairo.h>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <fontconfig/fontconfig.h>
|
||||
#include <functional>
|
||||
#include <pango/pango.h>
|
||||
#include <pango/pangocairo.h>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("text");
|
||||
constexpr Logger kLog("text");
|
||||
|
||||
constexpr std::uint32_t kSizeQuant = 64;
|
||||
constexpr std::uint32_t kScaleQuant = 64;
|
||||
constexpr std::uint32_t kSizeQuant = 64;
|
||||
constexpr std::uint32_t kScaleQuant = 64;
|
||||
|
||||
inline std::uint32_t quantizeSize(float v) {
|
||||
return static_cast<std::uint32_t>(std::max(0.0f, v) * static_cast<float>(kSizeQuant) + 0.5f);
|
||||
}
|
||||
inline std::uint32_t quantizeSize(float v) {
|
||||
return static_cast<std::uint32_t>(std::max(0.0f, v) * static_cast<float>(kSizeQuant) + 0.5f);
|
||||
}
|
||||
|
||||
inline std::uint16_t quantizeScale(float v) {
|
||||
return static_cast<std::uint16_t>(std::max(0.0f, v) * static_cast<float>(kScaleQuant) + 0.5f);
|
||||
}
|
||||
inline std::uint16_t quantizeScale(float v) {
|
||||
return static_cast<std::uint16_t>(std::max(0.0f, v) * static_cast<float>(kScaleQuant) + 0.5f);
|
||||
}
|
||||
|
||||
void hashCombine(std::size_t& seed, std::size_t v) {
|
||||
seed ^= v + 0x9E3779B97F4A7C15ULL + (seed << 12) + (seed >> 4);
|
||||
}
|
||||
void hashCombine(std::size_t& seed, std::size_t v) { seed ^= v + 0x9E3779B97F4A7C15ULL + (seed << 12) + (seed >> 4); }
|
||||
|
||||
// Pack rgb into the top 24 bits; alpha is always forced to 0xFF so that
|
||||
// opacity animations on a mixed-content string (the RGBA emoji path) reuse
|
||||
// the same cache entry — the caller's alpha is applied at draw time via
|
||||
// u_opacity instead of being baked into the raster.
|
||||
std::uint32_t packColorRgb(const Color& c) {
|
||||
const auto clamp8 = [](float v) -> std::uint32_t {
|
||||
const float s = std::clamp(v, 0.0f, 1.0f);
|
||||
return static_cast<std::uint32_t>(s * 255.0f + 0.5f);
|
||||
};
|
||||
return (clamp8(c.r) << 24) | (clamp8(c.g) << 16) | (clamp8(c.b) << 8) | 0xFFu;
|
||||
}
|
||||
// Pack rgb into the top 24 bits; alpha is always forced to 0xFF so that
|
||||
// opacity animations on a mixed-content string (the RGBA emoji path) reuse
|
||||
// the same cache entry — the caller's alpha is applied at draw time via
|
||||
// u_opacity instead of being baked into the raster.
|
||||
std::uint32_t packColorRgb(const Color& c) {
|
||||
const auto clamp8 = [](float v) -> std::uint32_t {
|
||||
const float s = std::clamp(v, 0.0f, 1.0f);
|
||||
return static_cast<std::uint32_t>(s * 255.0f + 0.5f);
|
||||
};
|
||||
return (clamp8(c.r) << 24) | (clamp8(c.g) << 16) | (clamp8(c.b) << 8) | 0xFFu;
|
||||
}
|
||||
|
||||
// Swap BGRA<->RGBA in place on a premultiplied ARGB32 Cairo surface buffer.
|
||||
void swizzleBgraToRgba(unsigned char* data, int width, int height, int stride) {
|
||||
for (int y = 0; y < height; ++y) {
|
||||
unsigned char* row = data + y * stride;
|
||||
for (int x = 0; x < width; ++x) {
|
||||
unsigned char* p = row + x * 4;
|
||||
std::swap(p[0], p[2]); // B <-> R; G and A unchanged
|
||||
// Swap BGRA<->RGBA in place on a premultiplied ARGB32 Cairo surface buffer.
|
||||
void swizzleBgraToRgba(unsigned char* data, int width, int height, int stride) {
|
||||
for (int y = 0; y < height; ++y) {
|
||||
unsigned char* row = data + y * stride;
|
||||
for (int x = 0; x < width; ++x) {
|
||||
unsigned char* p = row + x * 4;
|
||||
std::swap(p[0], p[2]); // B <-> R; G and A unchanged
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scan UTF-8 text for codepoints that are likely to resolve to a COLR/bitmap
|
||||
// color glyph. We can't ask Pango cheaply whether a shaped run used a color
|
||||
// font, so we approximate: if the text contains codepoints in the common
|
||||
// emoji / symbol / dingbat ranges, rasterize as RGBA so the color layers are
|
||||
// preserved. Otherwise we can use A8 coverage + shader tint, which lets one
|
||||
// cache entry serve all colors for the same text.
|
||||
bool containsColorGlyph(std::string_view text) {
|
||||
const auto* s = reinterpret_cast<const unsigned char*>(text.data());
|
||||
const std::size_t n = text.size();
|
||||
std::size_t i = 0;
|
||||
while (i < n) {
|
||||
unsigned char b = s[i];
|
||||
char32_t cp = 0;
|
||||
int len = 1;
|
||||
if (b < 0x80) {
|
||||
cp = b;
|
||||
} else if ((b & 0xE0) == 0xC0 && i + 1 < n) {
|
||||
cp = static_cast<char32_t>((b & 0x1F) << 6 | (s[i + 1] & 0x3F));
|
||||
len = 2;
|
||||
} else if ((b & 0xF0) == 0xE0 && i + 2 < n) {
|
||||
cp = static_cast<char32_t>((b & 0x0F) << 12 | (s[i + 1] & 0x3F) << 6 | (s[i + 2] & 0x3F));
|
||||
len = 3;
|
||||
} else if ((b & 0xF8) == 0xF0 && i + 3 < n) {
|
||||
cp = static_cast<char32_t>((b & 0x07) << 18 | (s[i + 1] & 0x3F) << 12 | (s[i + 2] & 0x3F) << 6 | (s[i + 3] & 0x3F));
|
||||
len = 4;
|
||||
} else {
|
||||
return true; // malformed — be safe
|
||||
// Scan UTF-8 text for codepoints that are likely to resolve to a COLR/bitmap
|
||||
// color glyph. We can't ask Pango cheaply whether a shaped run used a color
|
||||
// font, so we approximate: if the text contains codepoints in the common
|
||||
// emoji / symbol / dingbat ranges, rasterize as RGBA so the color layers are
|
||||
// preserved. Otherwise we can use A8 coverage + shader tint, which lets one
|
||||
// cache entry serve all colors for the same text.
|
||||
bool containsColorGlyph(std::string_view text) {
|
||||
const auto* s = reinterpret_cast<const unsigned char*>(text.data());
|
||||
const std::size_t n = text.size();
|
||||
std::size_t i = 0;
|
||||
while (i < n) {
|
||||
unsigned char b = s[i];
|
||||
char32_t cp = 0;
|
||||
int len = 1;
|
||||
if (b < 0x80) {
|
||||
cp = b;
|
||||
} else if ((b & 0xE0) == 0xC0 && i + 1 < n) {
|
||||
cp = static_cast<char32_t>((b & 0x1F) << 6 | (s[i + 1] & 0x3F));
|
||||
len = 2;
|
||||
} else if ((b & 0xF0) == 0xE0 && i + 2 < n) {
|
||||
cp = static_cast<char32_t>((b & 0x0F) << 12 | (s[i + 1] & 0x3F) << 6 | (s[i + 2] & 0x3F));
|
||||
len = 3;
|
||||
} else if ((b & 0xF8) == 0xF0 && i + 3 < n) {
|
||||
cp = static_cast<char32_t>((b & 0x07) << 18 | (s[i + 1] & 0x3F) << 12 | (s[i + 2] & 0x3F) << 6 |
|
||||
(s[i + 3] & 0x3F));
|
||||
len = 4;
|
||||
} else {
|
||||
return true; // malformed — be safe
|
||||
}
|
||||
i += static_cast<std::size_t>(len);
|
||||
if (cp >= 0x2600 && cp <= 0x27BF)
|
||||
return true; // misc symbols + dingbats
|
||||
if (cp >= 0x1F000 && cp <= 0x1FFFF)
|
||||
return true; // emoji planes
|
||||
if (cp >= 0x1F900 && cp <= 0x1F9FF)
|
||||
return true; // supplemental symbols
|
||||
}
|
||||
i += static_cast<std::size_t>(len);
|
||||
if (cp >= 0x2600 && cp <= 0x27BF) return true; // misc symbols + dingbats
|
||||
if (cp >= 0x1F000 && cp <= 0x1FFFF) return true; // emoji planes
|
||||
if (cp >= 0x1F900 && cp <= 0x1F9FF) return true; // supplemental symbols
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -391,7 +392,7 @@ void CairoTextRenderer::rasterizeLayout(PangoLayout* layout, const Color& color,
|
||||
struct LineSlot {
|
||||
PangoLayoutLine* line = nullptr;
|
||||
int xLeftPx = 0; // alignment offset of this line within the layout (pixels)
|
||||
int yTopPx = 0; // top of line in full-layout raster pixels
|
||||
int yTopPx = 0; // top of line in full-layout raster pixels
|
||||
int baselinePx = 0; // baseline of line in full-layout raster pixels
|
||||
};
|
||||
struct TilePlan {
|
||||
@@ -631,8 +632,8 @@ void CairoTextRenderer::draw(float surfaceWidth, float surfaceHeight, float x, f
|
||||
return;
|
||||
}
|
||||
if (entry->tiles.size() > 1) {
|
||||
kLog.warn("draw tiles={} pxW={} pxH={} baseXY=({}, {})", entry->tiles.size(), entry->pixelWidth,
|
||||
entry->pixelHeight, x, baselineY);
|
||||
kLog.warn("draw tiles={} pxW={} pxH={} baseXY=({}, {})", entry->tiles.size(), entry->pixelWidth, entry->pixelHeight,
|
||||
x, baselineY);
|
||||
}
|
||||
|
||||
const float invScale = 1.0f / m_contentScale;
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "render/core/renderer.h"
|
||||
|
||||
#include <GLES2/gl2.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <list>
|
||||
#include <string>
|
||||
@@ -34,8 +33,8 @@ public:
|
||||
float width = 0.0f;
|
||||
float left = 0.0f;
|
||||
float right = 0.0f;
|
||||
float top = 0.0f; // negative — above baseline
|
||||
float bottom = 0.0f; // positive — below baseline
|
||||
float top = 0.0f; // negative — above baseline
|
||||
float bottom = 0.0f; // positive — below baseline
|
||||
float inkTop = 0.0f; // negative — visible ink above baseline
|
||||
float inkBottom = 0.0f; // positive — visible ink below baseline
|
||||
};
|
||||
@@ -113,12 +112,12 @@ private:
|
||||
|
||||
struct CacheEntry {
|
||||
std::vector<Tile> tiles;
|
||||
int pixelWidth = 0; // total raster surface pixel width
|
||||
int pixelHeight = 0; // total raster surface pixel height (sum of tiles)
|
||||
float baselinePx = 0; // baseline from top of full layout, in raster pixels
|
||||
TextMetrics metrics; // logical metrics in logical (unscaled) pixels
|
||||
int pixelWidth = 0; // total raster surface pixel width
|
||||
int pixelHeight = 0; // total raster surface pixel height (sum of tiles)
|
||||
float baselinePx = 0; // baseline from top of full layout, in raster pixels
|
||||
TextMetrics metrics; // logical metrics in logical (unscaled) pixels
|
||||
std::size_t bytes = 0;
|
||||
bool tinted = false; // true: GL_ALPHA coverage, tint in shader; false: premul RGBA
|
||||
bool tinted = false; // true: GL_ALPHA coverage, tint in shader; false: premul RGBA
|
||||
LruList::iterator lruIt;
|
||||
};
|
||||
|
||||
@@ -147,7 +146,7 @@ private:
|
||||
bool m_fontConfigInitialized = false;
|
||||
std::string m_fontFamily = "sans-serif";
|
||||
|
||||
PangoFontMap* m_fontMap = nullptr; // owned
|
||||
PangoFontMap* m_fontMap = nullptr; // owned
|
||||
PangoContext* m_pangoContext = nullptr; // owned
|
||||
GlyphProgram* m_program = nullptr;
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
#include "render/gl_shared_context.h"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include <GLES2/gl2.h>
|
||||
#include <stdexcept>
|
||||
#include <wayland-egl.h>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "scripting/luau_host.h"
|
||||
|
||||
#include "core/log.h"
|
||||
|
||||
#include "lua.h"
|
||||
#include "luacode.h"
|
||||
#include "lualib.h"
|
||||
|
||||
+230
-233
@@ -1,12 +1,12 @@
|
||||
#include "shell/bar/bar.h"
|
||||
|
||||
#include "config/config_service.h"
|
||||
#include "core/ui_phase.h"
|
||||
#include "core/log.h"
|
||||
#include "ipc/ipc_service.h"
|
||||
#include "core/ui_phase.h"
|
||||
#include "dbus/power/power_profiles_service.h"
|
||||
#include "dbus/tray/tray_service.h"
|
||||
#include "dbus/upower/upower_service.h"
|
||||
#include "ipc/ipc_service.h"
|
||||
#include "render/render_context.h"
|
||||
#include "render/scene/rect_node.h"
|
||||
#include "shell/widget/widget.h"
|
||||
@@ -22,227 +22,222 @@
|
||||
#include "wayland/wayland_connection.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <wayland-client-core.h>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kCircularCapsuleNarrowWidthEpsilon = 1.0f;
|
||||
constexpr float kCircularCapsuleNarrowWidthEpsilon = 1.0f;
|
||||
|
||||
std::uint32_t positionToAnchor(const std::string& position) {
|
||||
if (position == "bottom") {
|
||||
return LayerShellAnchor::Bottom | LayerShellAnchor::Left | LayerShellAnchor::Right;
|
||||
}
|
||||
if (position == "left") {
|
||||
return LayerShellAnchor::Top | LayerShellAnchor::Bottom | LayerShellAnchor::Left;
|
||||
}
|
||||
if (position == "right") {
|
||||
return LayerShellAnchor::Top | LayerShellAnchor::Bottom | LayerShellAnchor::Right;
|
||||
}
|
||||
// Default: top
|
||||
return LayerShellAnchor::Top | LayerShellAnchor::Left | LayerShellAnchor::Right;
|
||||
}
|
||||
|
||||
constexpr Logger kLog("bar");
|
||||
|
||||
ThemeColor withOpacity(const ThemeColor& color, float opacity) {
|
||||
ThemeColor out = color;
|
||||
out.alpha = std::clamp(out.alpha * std::clamp(opacity, 0.0f, 1.0f), 0.0f, 1.0f);
|
||||
return out;
|
||||
}
|
||||
|
||||
struct ShadowBleed {
|
||||
std::int32_t left = 0, right = 0, up = 0, down = 0;
|
||||
};
|
||||
|
||||
ShadowBleed computeShadowBleed(const BarConfig& cfg) {
|
||||
if (cfg.shadowBlur <= 0)
|
||||
return {};
|
||||
return {
|
||||
cfg.shadowBlur + std::max(0, -cfg.shadowOffsetX),
|
||||
cfg.shadowBlur + std::max(0, cfg.shadowOffsetX),
|
||||
cfg.shadowBlur + std::max(0, -cfg.shadowOffsetY),
|
||||
cfg.shadowBlur + std::max(0, cfg.shadowOffsetY),
|
||||
};
|
||||
}
|
||||
|
||||
void layoutBarSections(BarInstance& instance, Renderer& renderer, float barAreaW, float barAreaH, float padding,
|
||||
bool isVertical) {
|
||||
const float slotCross = isVertical ? barAreaW : barAreaH;
|
||||
|
||||
auto layoutWidgets = [&](std::vector<std::unique_ptr<Widget>>& widgets) {
|
||||
for (auto& widget : widgets) {
|
||||
widget->layout(renderer, barAreaW, barAreaH);
|
||||
std::uint32_t positionToAnchor(const std::string& position) {
|
||||
if (position == "bottom") {
|
||||
return LayerShellAnchor::Bottom | LayerShellAnchor::Left | LayerShellAnchor::Right;
|
||||
}
|
||||
};
|
||||
layoutWidgets(instance.startWidgets);
|
||||
layoutWidgets(instance.centerWidgets);
|
||||
layoutWidgets(instance.endWidgets);
|
||||
if (position == "left") {
|
||||
return LayerShellAnchor::Top | LayerShellAnchor::Bottom | LayerShellAnchor::Left;
|
||||
}
|
||||
if (position == "right") {
|
||||
return LayerShellAnchor::Top | LayerShellAnchor::Bottom | LayerShellAnchor::Right;
|
||||
}
|
||||
// Default: top
|
||||
return LayerShellAnchor::Top | LayerShellAnchor::Left | LayerShellAnchor::Right;
|
||||
}
|
||||
|
||||
auto finalizeCapsules = [isVertical, slotCross, &renderer](std::vector<std::unique_ptr<Widget>>& widgets) {
|
||||
for (auto& w : widgets) {
|
||||
if (w == nullptr || !w->barCapsuleSpec().enabled) {
|
||||
continue;
|
||||
constexpr Logger kLog("bar");
|
||||
|
||||
ThemeColor withOpacity(const ThemeColor& color, float opacity) {
|
||||
ThemeColor out = color;
|
||||
out.alpha = std::clamp(out.alpha * std::clamp(opacity, 0.0f, 1.0f), 0.0f, 1.0f);
|
||||
return out;
|
||||
}
|
||||
|
||||
struct ShadowBleed {
|
||||
std::int32_t left = 0, right = 0, up = 0, down = 0;
|
||||
};
|
||||
|
||||
ShadowBleed computeShadowBleed(const BarConfig& cfg) {
|
||||
if (cfg.shadowBlur <= 0)
|
||||
return {};
|
||||
return {
|
||||
cfg.shadowBlur + std::max(0, -cfg.shadowOffsetX),
|
||||
cfg.shadowBlur + std::max(0, cfg.shadowOffsetX),
|
||||
cfg.shadowBlur + std::max(0, -cfg.shadowOffsetY),
|
||||
cfg.shadowBlur + std::max(0, cfg.shadowOffsetY),
|
||||
};
|
||||
}
|
||||
|
||||
void layoutBarSections(BarInstance& instance, Renderer& renderer, float barAreaW, float barAreaH, float padding,
|
||||
bool isVertical) {
|
||||
const float slotCross = isVertical ? barAreaW : barAreaH;
|
||||
|
||||
auto layoutWidgets = [&](std::vector<std::unique_ptr<Widget>>& widgets) {
|
||||
for (auto& widget : widgets) {
|
||||
widget->layout(renderer, barAreaW, barAreaH);
|
||||
}
|
||||
Node* shell = w->barCapsuleShell();
|
||||
Box* bg = w->barCapsuleBox();
|
||||
Node* inner = w->root();
|
||||
if (shell == nullptr || bg == nullptr || inner == nullptr) {
|
||||
continue;
|
||||
}
|
||||
// Keep the capsule shell visibility in sync with the inner root so that
|
||||
// hidden widgets don't occupy phantom space in the flex layout.
|
||||
shell->setVisible(inner->visible());
|
||||
const float scale = w->contentScale();
|
||||
const float iw = inner->width();
|
||||
const float ih = inner->height();
|
||||
if (!w->shouldShowBarCapsule()) {
|
||||
shell->setSize(iw, ih);
|
||||
inner->setPosition(0.0f, 0.0f);
|
||||
bg->setVisible(false);
|
||||
bg->setPosition(0.0f, 0.0f);
|
||||
bg->setSize(iw, ih);
|
||||
continue;
|
||||
}
|
||||
// Uniform capsule body extent on the cross axis — matches the reference "A"
|
||||
// height used by Glyph/Label so all capsules share the same cross size
|
||||
// regardless of each widget's content height.
|
||||
const auto refMetrics = renderer.measureText("A", Style::fontSizeBody * scale);
|
||||
const float bodyExtent = std::round(refMetrics.bottom - refMetrics.top);
|
||||
const float pad = w->barCapsuleSpec().padding * scale;
|
||||
const float padMain = pad;
|
||||
const float padCross = std::min(pad, Style::spaceXs * scale);
|
||||
float shellMain = (isVertical ? ih : iw) + 2.0f * padMain;
|
||||
float shellCross = bodyExtent + 2.0f * padCross;
|
||||
if (isVertical) {
|
||||
shellCross = std::min(shellCross, slotCross);
|
||||
}
|
||||
float shellW = isVertical ? shellCross : shellMain;
|
||||
float shellH = isVertical ? shellMain : shellCross;
|
||||
float innerX = std::round((shellW - iw) * 0.5f);
|
||||
float innerY = std::round((shellH - ih) * 0.5f);
|
||||
// Glyph-only widgets have content close to bodyExtent on both axes — round
|
||||
// them into a circular capsule. Multi-line / wide content (e.g. stacked
|
||||
// vertical clock) must NOT be squared, or the capsule collapses on the
|
||||
// main axis.
|
||||
const float iconThreshold = bodyExtent + (kCircularCapsuleNarrowWidthEpsilon * scale);
|
||||
const bool iconSized = iw <= iconThreshold && ih <= iconThreshold;
|
||||
if (iconSized) {
|
||||
float side = std::max(shellW, shellH);
|
||||
if (isVertical) {
|
||||
side = std::min(side, slotCross);
|
||||
};
|
||||
layoutWidgets(instance.startWidgets);
|
||||
layoutWidgets(instance.centerWidgets);
|
||||
layoutWidgets(instance.endWidgets);
|
||||
|
||||
auto finalizeCapsules = [isVertical, slotCross, &renderer](std::vector<std::unique_ptr<Widget>>& widgets) {
|
||||
for (auto& w : widgets) {
|
||||
if (w == nullptr || !w->barCapsuleSpec().enabled) {
|
||||
continue;
|
||||
}
|
||||
shellW = side;
|
||||
shellH = side;
|
||||
innerX = std::round((shellW - iw) * 0.5f);
|
||||
innerY = std::round((shellH - ih) * 0.5f);
|
||||
Node* shell = w->barCapsuleShell();
|
||||
Box* bg = w->barCapsuleBox();
|
||||
Node* inner = w->root();
|
||||
if (shell == nullptr || bg == nullptr || inner == nullptr) {
|
||||
continue;
|
||||
}
|
||||
// Keep the capsule shell visibility in sync with the inner root so that
|
||||
// hidden widgets don't occupy phantom space in the flex layout.
|
||||
shell->setVisible(inner->visible());
|
||||
const float scale = w->contentScale();
|
||||
const float iw = inner->width();
|
||||
const float ih = inner->height();
|
||||
if (!w->shouldShowBarCapsule()) {
|
||||
shell->setSize(iw, ih);
|
||||
inner->setPosition(0.0f, 0.0f);
|
||||
bg->setVisible(false);
|
||||
bg->setPosition(0.0f, 0.0f);
|
||||
bg->setSize(iw, ih);
|
||||
continue;
|
||||
}
|
||||
// Uniform capsule body extent on the cross axis — matches the reference "A"
|
||||
// height used by Glyph/Label so all capsules share the same cross size
|
||||
// regardless of each widget's content height.
|
||||
const auto refMetrics = renderer.measureText("A", Style::fontSizeBody * scale);
|
||||
const float bodyExtent = std::round(refMetrics.bottom - refMetrics.top);
|
||||
const float pad = w->barCapsuleSpec().padding * scale;
|
||||
const float padMain = pad;
|
||||
const float padCross = std::min(pad, Style::spaceXs * scale);
|
||||
float shellMain = (isVertical ? ih : iw) + 2.0f * padMain;
|
||||
float shellCross = bodyExtent + 2.0f * padCross;
|
||||
if (isVertical) {
|
||||
shellCross = std::min(shellCross, slotCross);
|
||||
}
|
||||
float shellW = isVertical ? shellCross : shellMain;
|
||||
float shellH = isVertical ? shellMain : shellCross;
|
||||
float innerX = std::round((shellW - iw) * 0.5f);
|
||||
float innerY = std::round((shellH - ih) * 0.5f);
|
||||
// Glyph-only widgets have content close to bodyExtent on both axes — round
|
||||
// them into a circular capsule. Multi-line / wide content (e.g. stacked
|
||||
// vertical clock) must NOT be squared, or the capsule collapses on the
|
||||
// main axis.
|
||||
const float iconThreshold = bodyExtent + (kCircularCapsuleNarrowWidthEpsilon * scale);
|
||||
const bool iconSized = iw <= iconThreshold && ih <= iconThreshold;
|
||||
if (iconSized) {
|
||||
float side = std::max(shellW, shellH);
|
||||
if (isVertical) {
|
||||
side = std::min(side, slotCross);
|
||||
}
|
||||
shellW = side;
|
||||
shellH = side;
|
||||
innerX = std::round((shellW - iw) * 0.5f);
|
||||
innerY = std::round((shellH - ih) * 0.5f);
|
||||
}
|
||||
shell->setSize(shellW, shellH);
|
||||
bg->setVisible(true);
|
||||
bg->setPosition(0.0f, 0.0f);
|
||||
bg->setSize(shellW, shellH);
|
||||
inner->setPosition(innerX, innerY);
|
||||
bg->setRadius(std::min(shellW, shellH) * 0.5f);
|
||||
}
|
||||
};
|
||||
finalizeCapsules(instance.startWidgets);
|
||||
finalizeCapsules(instance.centerWidgets);
|
||||
finalizeCapsules(instance.endWidgets);
|
||||
|
||||
const float contentMainStart = padding;
|
||||
const float contentMainEnd = std::max(contentMainStart, (isVertical ? barAreaH : barAreaW) - padding);
|
||||
const float contentMainSpan = std::max(0.0f, contentMainEnd - contentMainStart);
|
||||
|
||||
auto configureSlot = [&](Node* slot, float mainOffset, float mainSize) {
|
||||
slot->setClipChildren(true);
|
||||
if (isVertical) {
|
||||
slot->setPosition(0.0f, mainOffset);
|
||||
slot->setSize(slotCross, mainSize);
|
||||
} else {
|
||||
slot->setPosition(mainOffset, 0.0f);
|
||||
slot->setSize(mainSize, slotCross);
|
||||
}
|
||||
};
|
||||
|
||||
auto configureSection = [&](Flex* section, FlexJustify justify) {
|
||||
section->setJustify(justify);
|
||||
section->layout(renderer);
|
||||
};
|
||||
|
||||
configureSection(instance.startSection, FlexJustify::Start);
|
||||
configureSection(instance.centerSection, FlexJustify::Center);
|
||||
configureSection(instance.endSection, FlexJustify::End);
|
||||
|
||||
// Anchor mode: if a center widget is flagged as the anchor, pin its center to the
|
||||
// bar midline so surrounding siblings growing/shrinking cannot drift it sideways.
|
||||
const Node* anchorNode = nullptr;
|
||||
for (const auto& widget : instance.centerWidgets) {
|
||||
if (widget != nullptr && widget->isAnchor() && widget->layoutBoundsNode() != nullptr) {
|
||||
anchorNode = widget->layoutBoundsNode();
|
||||
break;
|
||||
}
|
||||
shell->setSize(shellW, shellH);
|
||||
bg->setVisible(true);
|
||||
bg->setPosition(0.0f, 0.0f);
|
||||
bg->setSize(shellW, shellH);
|
||||
inner->setPosition(innerX, innerY);
|
||||
bg->setRadius(std::min(shellW, shellH) * 0.5f);
|
||||
}
|
||||
};
|
||||
finalizeCapsules(instance.startWidgets);
|
||||
finalizeCapsules(instance.centerWidgets);
|
||||
finalizeCapsules(instance.endWidgets);
|
||||
|
||||
const float contentMainStart = padding;
|
||||
const float contentMainEnd = std::max(contentMainStart, (isVertical ? barAreaH : barAreaW) - padding);
|
||||
const float contentMainSpan = std::max(0.0f, contentMainEnd - contentMainStart);
|
||||
const float barMidline = contentMainStart + contentMainSpan * 0.5f;
|
||||
const float centerNaturalMain = isVertical ? instance.centerSection->height() : instance.centerSection->width();
|
||||
|
||||
auto configureSlot = [&](Node* slot, float mainOffset, float mainSize) {
|
||||
slot->setClipChildren(true);
|
||||
if (isVertical) {
|
||||
slot->setPosition(0.0f, mainOffset);
|
||||
slot->setSize(slotCross, mainSize);
|
||||
float centerSlotStart;
|
||||
float centerSlotMain;
|
||||
float centerSectionOffset; // offset of section origin within its slot along main axis
|
||||
if (anchorNode != nullptr) {
|
||||
const float anchorOffsetInSection = isVertical ? anchorNode->y() : anchorNode->x();
|
||||
const float anchorSpan = isVertical ? anchorNode->height() : anchorNode->width();
|
||||
const float anchorCenterInSection = anchorOffsetInSection + anchorSpan * 0.5f;
|
||||
// Place the section so that the anchor's center sits at barMidline.
|
||||
float desiredSectionStart = barMidline - anchorCenterInSection;
|
||||
// Clamp so the section stays within the content area.
|
||||
const float maxStart = contentMainEnd - centerNaturalMain;
|
||||
desiredSectionStart = std::clamp(desiredSectionStart, contentMainStart, std::max(contentMainStart, maxStart));
|
||||
centerSlotStart = desiredSectionStart;
|
||||
centerSlotMain = std::min(centerNaturalMain, contentMainEnd - centerSlotStart);
|
||||
centerSectionOffset = 0.0f;
|
||||
} else {
|
||||
slot->setPosition(mainOffset, 0.0f);
|
||||
slot->setSize(mainSize, slotCross);
|
||||
centerSlotMain = std::min(contentMainSpan, centerNaturalMain);
|
||||
centerSlotStart = contentMainStart + std::max(0.0f, (contentMainSpan - centerSlotMain) * 0.5f);
|
||||
centerSectionOffset = (centerSlotMain - centerNaturalMain) * 0.5f;
|
||||
}
|
||||
};
|
||||
const float centerSlotEnd = centerSlotStart + centerSlotMain;
|
||||
const float startSlotMain = std::max(0.0f, centerSlotStart - contentMainStart);
|
||||
const float endSlotMain = std::max(0.0f, contentMainEnd - centerSlotEnd);
|
||||
|
||||
auto configureSection = [&](Flex* section, FlexJustify justify) {
|
||||
section->setJustify(justify);
|
||||
section->layout(renderer);
|
||||
};
|
||||
configureSlot(instance.startSlot, contentMainStart, startSlotMain);
|
||||
configureSlot(instance.centerSlot, centerSlotStart, centerSlotMain);
|
||||
configureSlot(instance.endSlot, centerSlotEnd, endSlotMain);
|
||||
|
||||
configureSection(instance.startSection, FlexJustify::Start);
|
||||
configureSection(instance.centerSection, FlexJustify::Center);
|
||||
configureSection(instance.endSection, FlexJustify::End);
|
||||
|
||||
// Anchor mode: if a center widget is flagged as the anchor, pin its center to the
|
||||
// bar midline so surrounding siblings growing/shrinking cannot drift it sideways.
|
||||
const Node* anchorNode = nullptr;
|
||||
for (const auto& widget : instance.centerWidgets) {
|
||||
if (widget != nullptr && widget->isAnchor() && widget->layoutBoundsNode() != nullptr) {
|
||||
anchorNode = widget->layoutBoundsNode();
|
||||
break;
|
||||
if (isVertical) {
|
||||
instance.startSection->setPosition((slotCross - instance.startSection->width()) * 0.5f, 0.0f);
|
||||
instance.centerSection->setPosition((slotCross - instance.centerSection->width()) * 0.5f, centerSectionOffset);
|
||||
instance.endSection->setPosition((slotCross - instance.endSection->width()) * 0.5f,
|
||||
endSlotMain - instance.endSection->height());
|
||||
} else {
|
||||
instance.startSection->setPosition(0.0f, (slotCross - instance.startSection->height()) * 0.5f);
|
||||
instance.centerSection->setPosition(centerSectionOffset, (slotCross - instance.centerSection->height()) * 0.5f);
|
||||
instance.endSection->setPosition(endSlotMain - instance.endSection->width(),
|
||||
(slotCross - instance.endSection->height()) * 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
const float barMidline = contentMainStart + contentMainSpan * 0.5f;
|
||||
const float centerNaturalMain = isVertical ? instance.centerSection->height() : instance.centerSection->width();
|
||||
|
||||
float centerSlotStart;
|
||||
float centerSlotMain;
|
||||
float centerSectionOffset; // offset of section origin within its slot along main axis
|
||||
if (anchorNode != nullptr) {
|
||||
const float anchorOffsetInSection =
|
||||
isVertical ? anchorNode->y() : anchorNode->x();
|
||||
const float anchorSpan =
|
||||
isVertical ? anchorNode->height() : anchorNode->width();
|
||||
const float anchorCenterInSection = anchorOffsetInSection + anchorSpan * 0.5f;
|
||||
// Place the section so that the anchor's center sits at barMidline.
|
||||
float desiredSectionStart = barMidline - anchorCenterInSection;
|
||||
// Clamp so the section stays within the content area.
|
||||
const float maxStart = contentMainEnd - centerNaturalMain;
|
||||
desiredSectionStart = std::clamp(desiredSectionStart, contentMainStart, std::max(contentMainStart, maxStart));
|
||||
centerSlotStart = desiredSectionStart;
|
||||
centerSlotMain = std::min(centerNaturalMain, contentMainEnd - centerSlotStart);
|
||||
centerSectionOffset = 0.0f;
|
||||
} else {
|
||||
centerSlotMain = std::min(contentMainSpan, centerNaturalMain);
|
||||
centerSlotStart = contentMainStart + std::max(0.0f, (contentMainSpan - centerSlotMain) * 0.5f);
|
||||
centerSectionOffset = (centerSlotMain - centerNaturalMain) * 0.5f;
|
||||
}
|
||||
const float centerSlotEnd = centerSlotStart + centerSlotMain;
|
||||
const float startSlotMain = std::max(0.0f, centerSlotStart - contentMainStart);
|
||||
const float endSlotMain = std::max(0.0f, contentMainEnd - centerSlotEnd);
|
||||
|
||||
configureSlot(instance.startSlot, contentMainStart, startSlotMain);
|
||||
configureSlot(instance.centerSlot, centerSlotStart, centerSlotMain);
|
||||
configureSlot(instance.endSlot, centerSlotEnd, endSlotMain);
|
||||
|
||||
if (isVertical) {
|
||||
instance.startSection->setPosition((slotCross - instance.startSection->width()) * 0.5f, 0.0f);
|
||||
instance.centerSection->setPosition((slotCross - instance.centerSection->width()) * 0.5f,
|
||||
centerSectionOffset);
|
||||
instance.endSection->setPosition((slotCross - instance.endSection->width()) * 0.5f,
|
||||
endSlotMain - instance.endSection->height());
|
||||
} else {
|
||||
instance.startSection->setPosition(0.0f, (slotCross - instance.startSection->height()) * 0.5f);
|
||||
instance.centerSection->setPosition(centerSectionOffset,
|
||||
(slotCross - instance.centerSection->height()) * 0.5f);
|
||||
instance.endSection->setPosition(endSlotMain - instance.endSection->width(),
|
||||
(slotCross - instance.endSection->height()) * 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
void tickWidgets(std::vector<std::unique_ptr<Widget>>& widgets, float deltaMs) {
|
||||
for (auto& widget : widgets) {
|
||||
if (widget != nullptr && widget->needsFrameTick()) {
|
||||
widget->onFrameTick(deltaMs);
|
||||
void tickWidgets(std::vector<std::unique_ptr<Widget>>& widgets, float deltaMs) {
|
||||
for (auto& widget : widgets) {
|
||||
if (widget != nullptr && widget->needsFrameTick()) {
|
||||
widget->onFrameTick(deltaMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool widgetsNeedFrameTick(const std::vector<std::unique_ptr<Widget>>& widgets) {
|
||||
return std::any_of(widgets.begin(), widgets.end(),
|
||||
[](const auto& widget) { return widget != nullptr && widget->needsFrameTick(); });
|
||||
}
|
||||
bool widgetsNeedFrameTick(const std::vector<std::unique_ptr<Widget>>& widgets) {
|
||||
return std::any_of(widgets.begin(), widgets.end(),
|
||||
[](const auto& widget) { return widget != nullptr && widget->needsFrameTick(); });
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -252,10 +247,10 @@ bool Bar::initialize(WaylandConnection& wayland, ConfigService* config, TimeServ
|
||||
NotificationManager* notifications, TrayService* tray, PipeWireService* audio,
|
||||
UPowerService* upower, SystemMonitorService* sysmon, PowerProfilesService* powerProfiles,
|
||||
NetworkService* network, IdleInhibitor* idleInhibitor, MprisService* mpris,
|
||||
PipeWireSpectrum* audioSpectrum,
|
||||
HttpClient* httpClient, WeatherService* weatherService, RenderContext* renderContext,
|
||||
NightLightManager* nightLight, noctalia::theme::ThemeService* themeService,
|
||||
BluetoothService* bluetooth, BrightnessService* brightness) {
|
||||
PipeWireSpectrum* audioSpectrum, HttpClient* httpClient, WeatherService* weatherService,
|
||||
RenderContext* renderContext, NightLightManager* nightLight,
|
||||
noctalia::theme::ThemeService* themeService, BluetoothService* bluetooth,
|
||||
BrightnessService* brightness) {
|
||||
m_wayland = &wayland;
|
||||
m_config = config;
|
||||
m_time = timeService;
|
||||
@@ -458,42 +453,42 @@ void Bar::createInstance(const WaylandOutput& output, const BarConfig& barConfig
|
||||
std::int32_t exclusiveZone = 0;
|
||||
|
||||
if (!vertical) {
|
||||
mLeft = std::max(0, mH - sb.left);
|
||||
mLeft = std::max(0, mH - sb.left);
|
||||
mRight = std::max(0, mH - sb.right);
|
||||
if (isBottom) {
|
||||
mBottom = std::max(0, mV - sb.down);
|
||||
surfH = static_cast<std::uint32_t>(sb.up + barConfig.thickness + std::min(mV, sb.down));
|
||||
mBottom = std::max(0, mV - sb.down);
|
||||
surfH = static_cast<std::uint32_t>(sb.up + barConfig.thickness + std::min(mV, sb.down));
|
||||
exclusiveZone = barConfig.thickness + std::min(mV, sb.down);
|
||||
} else {
|
||||
mTop = std::max(0, mV - sb.up);
|
||||
surfH = static_cast<std::uint32_t>(std::min(mV, sb.up) + barConfig.thickness + sb.down);
|
||||
mTop = std::max(0, mV - sb.up);
|
||||
surfH = static_cast<std::uint32_t>(std::min(mV, sb.up) + barConfig.thickness + sb.down);
|
||||
exclusiveZone = std::min(mV, sb.up) + barConfig.thickness;
|
||||
}
|
||||
} else {
|
||||
mTop = std::max(0, mV - sb.up);
|
||||
mTop = std::max(0, mV - sb.up);
|
||||
mBottom = std::max(0, mV - sb.down);
|
||||
if (isRight) {
|
||||
mRight = std::max(0, mH - sb.right);
|
||||
surfW = static_cast<std::uint32_t>(sb.left + barConfig.thickness + std::min(mH, sb.right));
|
||||
mRight = std::max(0, mH - sb.right);
|
||||
surfW = static_cast<std::uint32_t>(sb.left + barConfig.thickness + std::min(mH, sb.right));
|
||||
exclusiveZone = barConfig.thickness + std::min(mH, sb.right);
|
||||
} else {
|
||||
mLeft = std::max(0, mH - sb.left);
|
||||
surfW = static_cast<std::uint32_t>(std::min(mH, sb.left) + barConfig.thickness + sb.right);
|
||||
mLeft = std::max(0, mH - sb.left);
|
||||
surfW = static_cast<std::uint32_t>(std::min(mH, sb.left) + barConfig.thickness + sb.right);
|
||||
exclusiveZone = std::min(mH, sb.left) + barConfig.thickness;
|
||||
}
|
||||
}
|
||||
|
||||
auto surfaceConfig = LayerSurfaceConfig{
|
||||
.nameSpace = "noctalia-" + barConfig.name,
|
||||
.layer = LayerShellLayer::Top,
|
||||
.anchor = anchor,
|
||||
.width = surfW,
|
||||
.height = surfH,
|
||||
.nameSpace = "noctalia-" + barConfig.name,
|
||||
.layer = LayerShellLayer::Top,
|
||||
.anchor = anchor,
|
||||
.width = surfW,
|
||||
.height = surfH,
|
||||
.exclusiveZone = exclusiveZone,
|
||||
.marginTop = mTop,
|
||||
.marginRight = mRight,
|
||||
.marginBottom = mBottom,
|
||||
.marginLeft = mLeft,
|
||||
.marginTop = mTop,
|
||||
.marginRight = mRight,
|
||||
.marginBottom = mBottom,
|
||||
.marginLeft = mLeft,
|
||||
.defaultHeight = surfH,
|
||||
};
|
||||
|
||||
@@ -556,7 +551,9 @@ void Bar::populateWidgets(BarInstance& instance) {
|
||||
|
||||
void Bar::tickWidgets(std::vector<std::unique_ptr<Widget>>& widgets, float deltaMs) { ::tickWidgets(widgets, deltaMs); }
|
||||
|
||||
bool Bar::widgetsNeedFrameTick(const std::vector<std::unique_ptr<Widget>>& widgets) { return ::widgetsNeedFrameTick(widgets); }
|
||||
bool Bar::widgetsNeedFrameTick(const std::vector<std::unique_ptr<Widget>>& widgets) {
|
||||
return ::widgetsNeedFrameTick(widgets);
|
||||
}
|
||||
|
||||
bool Bar::instanceNeedsFrameTick(const BarInstance& instance) {
|
||||
return widgetsNeedFrameTick(instance.startWidgets) || widgetsNeedFrameTick(instance.centerWidgets) ||
|
||||
@@ -602,10 +599,10 @@ void Bar::buildScene(BarInstance& instance, std::uint32_t width, std::uint32_t h
|
||||
|
||||
// Shadow bleed in each direction (matches createInstance geometry).
|
||||
const auto sbi = computeShadowBleed(instance.barConfig);
|
||||
const float bleedLeft = static_cast<float>(sbi.left);
|
||||
const float bleedLeft = static_cast<float>(sbi.left);
|
||||
const float bleedRight = static_cast<float>(sbi.right);
|
||||
const float bleedUp = static_cast<float>(sbi.up);
|
||||
const float bleedDown = static_cast<float>(sbi.down);
|
||||
const float bleedUp = static_cast<float>(sbi.up);
|
||||
const float bleedDown = static_cast<float>(sbi.down);
|
||||
|
||||
// The bar's visual area within the tight surface.
|
||||
// compositor margins absorbed the outer gap; only the shadow bleed (capped at the gap)
|
||||
@@ -714,9 +711,9 @@ void Bar::buildScene(BarInstance& instance, std::uint32_t width, std::uint32_t h
|
||||
|
||||
// Fade-in animation
|
||||
instance.sceneRoot->setOpacity(0.0f);
|
||||
instance.animations.animate(0.0f, 1.0f, Style::animSlow, Easing::EaseOutCubic,
|
||||
[root = instance.sceneRoot.get()](float v) { root->setOpacity(v); }, {},
|
||||
instance.sceneRoot.get());
|
||||
instance.animations.animate(
|
||||
0.0f, 1.0f, Style::animSlow, Easing::EaseOutCubic,
|
||||
[root = instance.sceneRoot.get()](float v) { root->setOpacity(v); }, {}, instance.sceneRoot.get());
|
||||
|
||||
instance.surface->setSceneRoot(instance.sceneRoot.get());
|
||||
}
|
||||
@@ -804,10 +801,10 @@ void Bar::updateWidgets(BarInstance& instance) {
|
||||
const float marginV = static_cast<float>(instance.barConfig.marginV);
|
||||
const bool isVertical = (instance.barConfig.position == "left" || instance.barConfig.position == "right");
|
||||
const auto sbi = computeShadowBleed(instance.barConfig);
|
||||
const float bleedLeft = static_cast<float>(sbi.left);
|
||||
const float bleedLeft = static_cast<float>(sbi.left);
|
||||
const float bleedRight = static_cast<float>(sbi.right);
|
||||
const float bleedUp = static_cast<float>(sbi.up);
|
||||
const float bleedDown = static_cast<float>(sbi.down);
|
||||
const float bleedUp = static_cast<float>(sbi.up);
|
||||
const float bleedDown = static_cast<float>(sbi.down);
|
||||
float barAreaW, barAreaH;
|
||||
if (isVertical) {
|
||||
const float barAreaY = std::min(marginV, bleedUp);
|
||||
|
||||
+6
-7
@@ -38,13 +38,12 @@ public:
|
||||
Bar();
|
||||
|
||||
bool initialize(WaylandConnection& wayland, ConfigService* config, TimeService* timeService,
|
||||
NotificationManager* notifications, TrayService* tray, PipeWireService* audio,
|
||||
UPowerService* upower, SystemMonitorService* sysmon, PowerProfilesService* powerProfiles,
|
||||
NetworkService* network, IdleInhibitor* idleInhibitor, MprisService* mpris,
|
||||
PipeWireSpectrum* audioSpectrum, HttpClient* httpClient, WeatherService* weatherService,
|
||||
RenderContext* renderContext, NightLightManager* nightLight,
|
||||
noctalia::theme::ThemeService* themeService, BluetoothService* bluetooth,
|
||||
BrightnessService* brightness);
|
||||
NotificationManager* notifications, TrayService* tray, PipeWireService* audio, UPowerService* upower,
|
||||
SystemMonitorService* sysmon, PowerProfilesService* powerProfiles, NetworkService* network,
|
||||
IdleInhibitor* idleInhibitor, MprisService* mpris, PipeWireSpectrum* audioSpectrum,
|
||||
HttpClient* httpClient, WeatherService* weatherService, RenderContext* renderContext,
|
||||
NightLightManager* nightLight, noctalia::theme::ThemeService* themeService,
|
||||
BluetoothService* bluetooth, BrightnessService* brightness);
|
||||
void reload();
|
||||
void closeAllInstances();
|
||||
void show();
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
#include "render/scene/input_dispatcher.h"
|
||||
#include "render/scene/node.h"
|
||||
#include "shell/widget/widget.h"
|
||||
#include "wayland/layer_surface.h"
|
||||
|
||||
#include "ui/signal.h"
|
||||
#include "wayland/layer_surface.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "render/scene/input_area.h"
|
||||
#include "shell/control_center/tab.h"
|
||||
#include "shell/panel/panel_manager.h"
|
||||
#include "time/time_service.h"
|
||||
#include "ui/controls/button.h"
|
||||
#include "ui/controls/flex.h"
|
||||
#include "ui/controls/glyph.h"
|
||||
@@ -16,7 +17,6 @@
|
||||
#include "ui/controls/scroll_view.h"
|
||||
#include "ui/palette.h"
|
||||
#include "ui/style.h"
|
||||
#include "time/time_service.h"
|
||||
#include "wayland/clipboard_service.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -30,72 +30,71 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kSidebarWidth = 272.0f;
|
||||
constexpr float kRowHeight = 46.0f;
|
||||
constexpr float kPreviewImageHeight = 280.0f;
|
||||
constexpr float kListGlyphSize = 24.0f;
|
||||
constexpr auto kPreviewPayloadDebounceInterval = std::chrono::milliseconds(75);
|
||||
constexpr auto kFilterDebounceInterval = std::chrono::milliseconds(120);
|
||||
constexpr float kSidebarWidth = 272.0f;
|
||||
constexpr float kRowHeight = 46.0f;
|
||||
constexpr float kPreviewImageHeight = 280.0f;
|
||||
constexpr float kListGlyphSize = 24.0f;
|
||||
constexpr auto kPreviewPayloadDebounceInterval = std::chrono::milliseconds(75);
|
||||
constexpr auto kFilterDebounceInterval = std::chrono::milliseconds(120);
|
||||
|
||||
std::string collapseWhitespace(std::string_view text) {
|
||||
std::string out;
|
||||
out.reserve(text.size());
|
||||
std::string collapseWhitespace(std::string_view text) {
|
||||
std::string out;
|
||||
out.reserve(text.size());
|
||||
|
||||
bool lastWasSpace = true;
|
||||
for (char ch : text) {
|
||||
const bool isWhitespace = (ch == ' ' || ch == '\n' || ch == '\t' || ch == '\r');
|
||||
if (isWhitespace) {
|
||||
if (!lastWasSpace) {
|
||||
out.push_back(' ');
|
||||
bool lastWasSpace = true;
|
||||
for (char ch : text) {
|
||||
const bool isWhitespace = (ch == ' ' || ch == '\n' || ch == '\t' || ch == '\r');
|
||||
if (isWhitespace) {
|
||||
if (!lastWasSpace) {
|
||||
out.push_back(' ');
|
||||
}
|
||||
lastWasSpace = true;
|
||||
continue;
|
||||
}
|
||||
lastWasSpace = true;
|
||||
continue;
|
||||
out.push_back(ch);
|
||||
lastWasSpace = false;
|
||||
}
|
||||
out.push_back(ch);
|
||||
lastWasSpace = false;
|
||||
|
||||
if (!out.empty() && out.back() == ' ') {
|
||||
out.pop_back();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
if (!out.empty() && out.back() == ' ') {
|
||||
out.pop_back();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
std::string formatBytes(std::size_t bytes) {
|
||||
const char* units[] = {"B", "KB", "MB", "GB"};
|
||||
double value = static_cast<double>(bytes);
|
||||
std::size_t unitIndex = 0;
|
||||
while (value >= 1024.0 && unitIndex + 1 < std::size(units)) {
|
||||
value /= 1024.0;
|
||||
++unitIndex;
|
||||
}
|
||||
|
||||
std::string formatBytes(std::size_t bytes) {
|
||||
const char* units[] = {"B", "KB", "MB", "GB"};
|
||||
double value = static_cast<double>(bytes);
|
||||
std::size_t unitIndex = 0;
|
||||
while (value >= 1024.0 && unitIndex + 1 < std::size(units)) {
|
||||
value /= 1024.0;
|
||||
++unitIndex;
|
||||
char buffer[32];
|
||||
if (unitIndex == 0) {
|
||||
std::snprintf(buffer, sizeof(buffer), "%zu %s", bytes, units[unitIndex]);
|
||||
} else {
|
||||
std::snprintf(buffer, sizeof(buffer), "%.1f %s", value, units[unitIndex]);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
char buffer[32];
|
||||
if (unitIndex == 0) {
|
||||
std::snprintf(buffer, sizeof(buffer), "%zu %s", bytes, units[unitIndex]);
|
||||
} else {
|
||||
std::snprintf(buffer, sizeof(buffer), "%.1f %s", value, units[unitIndex]);
|
||||
std::string entryTitle(const ClipboardEntry& entry) {
|
||||
if (!entry.textPreview.empty()) {
|
||||
return entry.textPreview;
|
||||
}
|
||||
if (entry.isImage()) {
|
||||
return i18n::tr("clipboard.entry-image");
|
||||
}
|
||||
return entry.dataMimeType.empty() ? i18n::tr("clipboard.entry-title") : entry.dataMimeType;
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
std::string entryTitle(const ClipboardEntry& entry) {
|
||||
if (!entry.textPreview.empty()) {
|
||||
return entry.textPreview;
|
||||
std::string previewTitle(const ClipboardEntry& entry) {
|
||||
if (entry.isImage()) {
|
||||
return i18n::tr("clipboard.preview-image-title");
|
||||
}
|
||||
return i18n::tr("clipboard.preview-text-title");
|
||||
}
|
||||
if (entry.isImage()) {
|
||||
return i18n::tr("clipboard.entry-image");
|
||||
}
|
||||
return entry.dataMimeType.empty() ? i18n::tr("clipboard.entry-title") : entry.dataMimeType;
|
||||
}
|
||||
|
||||
|
||||
std::string previewTitle(const ClipboardEntry& entry) {
|
||||
if (entry.isImage()) {
|
||||
return i18n::tr("clipboard.preview-image-title");
|
||||
}
|
||||
return i18n::tr("clipboard.preview-text-title");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -170,9 +169,8 @@ void ClipboardPanel::create() {
|
||||
filterInput->setHorizontalPadding(Style::spaceMd * scale);
|
||||
filterInput->setOnChange([this](const std::string& text) { onFilterChanged(text); });
|
||||
filterInput->setOnSubmit([this](const std::string& /*text*/) { activateSelected(); });
|
||||
filterInput->setOnKeyEvent([this](std::uint32_t sym, std::uint32_t modifiers) {
|
||||
return handleKeyEvent(sym, modifiers);
|
||||
});
|
||||
filterInput->setOnKeyEvent(
|
||||
[this](std::uint32_t sym, std::uint32_t modifiers) { return handleKeyEvent(sym, modifiers); });
|
||||
m_filterInput = filterInput.get();
|
||||
sidebar->addChild(std::move(filterInput));
|
||||
|
||||
@@ -287,8 +285,7 @@ void ClipboardPanel::doLayout(Renderer& renderer, float width, float height) {
|
||||
}
|
||||
|
||||
const float previewScrollH = m_previewScrollView->height();
|
||||
if (m_lastPreviewWidth != m_previewScrollView->contentViewportWidth() ||
|
||||
m_lastPreviewHeight != previewScrollH) {
|
||||
if (m_lastPreviewWidth != m_previewScrollView->contentViewportWidth() || m_lastPreviewHeight != previewScrollH) {
|
||||
rebuildPreview(renderer, m_previewScrollView->contentViewportWidth(), previewScrollH);
|
||||
relayoutNeeded = true;
|
||||
}
|
||||
@@ -451,8 +448,7 @@ void ClipboardPanel::rebuildList(Renderer& renderer, float width) {
|
||||
row->setDirection(FlexDirection::Horizontal);
|
||||
row->setAlign(FlexAlign::Center);
|
||||
row->setGap(Style::spaceMd);
|
||||
row->setPadding(Style::spaceXs, Style::spaceSm,
|
||||
Style::spaceXs, Style::spaceSm);
|
||||
row->setPadding(Style::spaceXs, Style::spaceSm, Style::spaceXs, Style::spaceSm);
|
||||
row->setSize(width, 0.0f);
|
||||
row->setFillParentMainAxis(true);
|
||||
row->setMinHeight(kRowHeight);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "shell/panel/panel.h"
|
||||
#include "core/timer_manager.h"
|
||||
#include "shell/panel/panel.h"
|
||||
#include "wayland/clipboard_service.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
@@ -8,64 +8,64 @@
|
||||
|
||||
namespace clipboard_paste {
|
||||
|
||||
namespace {
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("clipboard");
|
||||
constexpr Logger kLog("clipboard");
|
||||
|
||||
std::optional<VirtualPasteShortcut> virtualPasteShortcutFor(ClipboardAutoPasteMode mode, bool isImage) {
|
||||
switch (mode) {
|
||||
case ClipboardAutoPasteMode::Off:
|
||||
std::optional<VirtualPasteShortcut> virtualPasteShortcutFor(ClipboardAutoPasteMode mode, bool isImage) {
|
||||
switch (mode) {
|
||||
case ClipboardAutoPasteMode::Off:
|
||||
return std::nullopt;
|
||||
case ClipboardAutoPasteMode::Auto:
|
||||
return isImage ? VirtualPasteShortcut::CtrlV : VirtualPasteShortcut::CtrlShiftV;
|
||||
case ClipboardAutoPasteMode::CtrlV:
|
||||
return VirtualPasteShortcut::CtrlV;
|
||||
case ClipboardAutoPasteMode::CtrlShiftV:
|
||||
return VirtualPasteShortcut::CtrlShiftV;
|
||||
case ClipboardAutoPasteMode::ShiftInsert:
|
||||
return VirtualPasteShortcut::ShiftInsert;
|
||||
}
|
||||
return std::nullopt;
|
||||
case ClipboardAutoPasteMode::Auto:
|
||||
return isImage ? VirtualPasteShortcut::CtrlV : VirtualPasteShortcut::CtrlShiftV;
|
||||
case ClipboardAutoPasteMode::CtrlV:
|
||||
return VirtualPasteShortcut::CtrlV;
|
||||
case ClipboardAutoPasteMode::CtrlShiftV:
|
||||
return VirtualPasteShortcut::CtrlShiftV;
|
||||
case ClipboardAutoPasteMode::ShiftInsert:
|
||||
return VirtualPasteShortcut::ShiftInsert;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<std::string> wtypeArgsFor(ClipboardAutoPasteMode mode, bool isImage) {
|
||||
switch (mode) {
|
||||
case ClipboardAutoPasteMode::Off:
|
||||
std::vector<std::string> wtypeArgsFor(ClipboardAutoPasteMode mode, bool isImage) {
|
||||
switch (mode) {
|
||||
case ClipboardAutoPasteMode::Off:
|
||||
return {};
|
||||
case ClipboardAutoPasteMode::Auto:
|
||||
return isImage ? std::vector<std::string>{"wtype", "-M", "ctrl", "-k", "v", "-m", "ctrl"}
|
||||
: std::vector<std::string>{"wtype", "-M", "ctrl", "-M", "shift", "-k",
|
||||
"v", "-m", "shift", "-m", "ctrl"};
|
||||
case ClipboardAutoPasteMode::CtrlV:
|
||||
return {"wtype", "-M", "ctrl", "-k", "v", "-m", "ctrl"};
|
||||
case ClipboardAutoPasteMode::CtrlShiftV:
|
||||
return {"wtype", "-M", "ctrl", "-M", "shift", "-k", "v", "-m", "shift", "-m", "ctrl"};
|
||||
case ClipboardAutoPasteMode::ShiftInsert:
|
||||
return {"wtype", "-M", "shift", "-k", "Insert", "-m", "shift"};
|
||||
}
|
||||
return {};
|
||||
case ClipboardAutoPasteMode::Auto:
|
||||
return isImage ? std::vector<std::string>{"wtype", "-M", "ctrl", "-k", "v", "-m", "ctrl"}
|
||||
: std::vector<std::string>{"wtype", "-M", "ctrl", "-M", "shift", "-k",
|
||||
"v", "-m", "shift", "-m", "ctrl"};
|
||||
case ClipboardAutoPasteMode::CtrlV:
|
||||
return {"wtype", "-M", "ctrl", "-k", "v", "-m", "ctrl"};
|
||||
case ClipboardAutoPasteMode::CtrlShiftV:
|
||||
return {"wtype", "-M", "ctrl", "-M", "shift", "-k", "v", "-m", "shift", "-m", "ctrl"};
|
||||
case ClipboardAutoPasteMode::ShiftInsert:
|
||||
return {"wtype", "-M", "shift", "-k", "Insert", "-m", "shift"};
|
||||
}
|
||||
return {};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool pasteEntry(const ClipboardEntry& entry, ClipboardAutoPasteMode mode, VirtualKeyboardService& virtualKeyboard) {
|
||||
if (mode == ClipboardAutoPasteMode::Off) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool isImage = entry.isImage();
|
||||
const auto shortcut = virtualPasteShortcutFor(mode, isImage);
|
||||
if (shortcut.has_value() && virtualKeyboard.sendPasteShortcut(*shortcut)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto args = wtypeArgsFor(mode, isImage);
|
||||
if (!args.empty() && process::launchDetached(args)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
kLog.warn("clipboard auto-paste failed: native virtual keyboard unavailable and wtype launch failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool pasteEntry(const ClipboardEntry& entry, ClipboardAutoPasteMode mode, VirtualKeyboardService& virtualKeyboard) {
|
||||
if (mode == ClipboardAutoPasteMode::Off) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool isImage = entry.isImage();
|
||||
const auto shortcut = virtualPasteShortcutFor(mode, isImage);
|
||||
if (shortcut.has_value() && virtualKeyboard.sendPasteShortcut(*shortcut)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto args = wtypeArgsFor(mode, isImage);
|
||||
if (!args.empty() && process::launchDetached(args)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
kLog.warn("clipboard auto-paste failed: native virtual keyboard unavailable and wtype launch failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace clipboard_paste
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "shell/control_center/audio_tab.h"
|
||||
|
||||
#include "core/ui_phase.h"
|
||||
#include "config/config_service.h"
|
||||
#include "core/ui_phase.h"
|
||||
#include "pipewire/pipewire_service.h"
|
||||
#include "render/core/renderer.h"
|
||||
#include "render/scene/input_area.h"
|
||||
@@ -25,194 +25,194 @@ using namespace control_center;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kDevicesColumnGrow = 3.0f;
|
||||
constexpr float kValueLabelWidth = Style::controlHeightLg + Style::spaceLg;
|
||||
constexpr float kVolumeSyncEpsilon = 0.005f; // 0.5%
|
||||
constexpr auto kVolumeCommitInterval = std::chrono::milliseconds(16);
|
||||
constexpr auto kVolumeStateHoldoff = std::chrono::milliseconds(180);
|
||||
constexpr float kDevicesColumnGrow = 3.0f;
|
||||
constexpr float kValueLabelWidth = Style::controlHeightLg + Style::spaceLg;
|
||||
constexpr float kVolumeSyncEpsilon = 0.005f; // 0.5%
|
||||
constexpr auto kVolumeCommitInterval = std::chrono::milliseconds(16);
|
||||
constexpr auto kVolumeStateHoldoff = std::chrono::milliseconds(180);
|
||||
|
||||
class AudioDeviceRow : public Flex {
|
||||
public:
|
||||
explicit AudioDeviceRow(std::function<void()> onSelect) : m_onSelect(std::move(onSelect)) {
|
||||
setDirection(FlexDirection::Horizontal);
|
||||
setAlign(FlexAlign::Center);
|
||||
setGap(Style::spaceSm);
|
||||
setPadding(Style::spaceSm, Style::spaceMd);
|
||||
setMinHeight(Style::controlHeightLg);
|
||||
setRadius(Style::radiusMd);
|
||||
setBackground(roleColor(ColorRole::Surface));
|
||||
setBorderWidth(0.0f);
|
||||
class AudioDeviceRow : public Flex {
|
||||
public:
|
||||
explicit AudioDeviceRow(std::function<void()> onSelect) : m_onSelect(std::move(onSelect)) {
|
||||
setDirection(FlexDirection::Horizontal);
|
||||
setAlign(FlexAlign::Center);
|
||||
setGap(Style::spaceSm);
|
||||
setPadding(Style::spaceSm, Style::spaceMd);
|
||||
setMinHeight(Style::controlHeightLg);
|
||||
setRadius(Style::radiusMd);
|
||||
setBackground(roleColor(ColorRole::Surface));
|
||||
setBorderWidth(0.0f);
|
||||
|
||||
auto radio = std::make_unique<RadioButton>();
|
||||
radio->setOnChange([this](bool /*checked*/) {
|
||||
if (m_onSelect) {
|
||||
m_onSelect();
|
||||
}
|
||||
});
|
||||
m_radio = static_cast<RadioButton*>(addChild(std::move(radio)));
|
||||
auto radio = std::make_unique<RadioButton>();
|
||||
radio->setOnChange([this](bool /*checked*/) {
|
||||
if (m_onSelect) {
|
||||
m_onSelect();
|
||||
}
|
||||
});
|
||||
m_radio = static_cast<RadioButton*>(addChild(std::move(radio)));
|
||||
|
||||
auto title = std::make_unique<Label>();
|
||||
title->setBold(true);
|
||||
title->setFontSize(Style::fontSizeBody);
|
||||
title->setColor(roleColor(ColorRole::OnSurface));
|
||||
title->setFlexGrow(1.0f);
|
||||
m_title = title.get();
|
||||
addChild(std::move(title));
|
||||
auto title = std::make_unique<Label>();
|
||||
title->setBold(true);
|
||||
title->setFontSize(Style::fontSizeBody);
|
||||
title->setColor(roleColor(ColorRole::OnSurface));
|
||||
title->setFlexGrow(1.0f);
|
||||
m_title = title.get();
|
||||
addChild(std::move(title));
|
||||
|
||||
m_detail = nullptr; // Remove detail label (subtext)
|
||||
m_detail = nullptr; // Remove detail label (subtext)
|
||||
|
||||
auto area = std::make_unique<InputArea>();
|
||||
area->setPropagateEvents(true);
|
||||
area->setOnEnter([this](const InputArea::PointerData& /*data*/) { applyState(); });
|
||||
area->setOnLeave([this]() { applyState(); });
|
||||
area->setOnPress([this](const InputArea::PointerData& /*data*/) { applyState(); });
|
||||
area->setOnClick([this](const InputArea::PointerData& /*data*/) {
|
||||
if (m_onSelect) {
|
||||
m_onSelect();
|
||||
}
|
||||
});
|
||||
m_inputArea = static_cast<InputArea*>(addChild(std::move(area)));
|
||||
auto area = std::make_unique<InputArea>();
|
||||
area->setPropagateEvents(true);
|
||||
area->setOnEnter([this](const InputArea::PointerData& /*data*/) { applyState(); });
|
||||
area->setOnLeave([this]() { applyState(); });
|
||||
area->setOnPress([this](const InputArea::PointerData& /*data*/) { applyState(); });
|
||||
area->setOnClick([this](const InputArea::PointerData& /*data*/) {
|
||||
if (m_onSelect) {
|
||||
m_onSelect();
|
||||
}
|
||||
});
|
||||
m_inputArea = static_cast<InputArea*>(addChild(std::move(area)));
|
||||
|
||||
applyState();
|
||||
m_paletteConn = paletteChanged().connect([this] { applyState(); });
|
||||
}
|
||||
|
||||
void setDevice(const AudioNode& node) {
|
||||
m_radio->setChecked(node.isDefault);
|
||||
const std::string title = !node.description.empty() ? node.description : node.name;
|
||||
|
||||
if (m_title != nullptr) {
|
||||
m_title->setText(title);
|
||||
}
|
||||
}
|
||||
|
||||
void doLayout(Renderer& renderer) override {
|
||||
if (m_radio == nullptr || m_title == nullptr || m_inputArea == nullptr) {
|
||||
return;
|
||||
applyState();
|
||||
m_paletteConn = paletteChanged().connect([this] { applyState(); });
|
||||
}
|
||||
|
||||
m_radio->layout(renderer);
|
||||
void setDevice(const AudioNode& node) {
|
||||
m_radio->setChecked(node.isDefault);
|
||||
const std::string title = !node.description.empty() ? node.description : node.name;
|
||||
|
||||
const float textMaxWidth =
|
||||
std::max(0.0f, width() - paddingLeft() - paddingRight() - gap() - m_radio->width());
|
||||
m_title->setMaxWidth(textMaxWidth);
|
||||
|
||||
m_inputArea->setVisible(false);
|
||||
Flex::doLayout(renderer);
|
||||
m_inputArea->setVisible(true);
|
||||
m_inputArea->setPosition(0.0f, 0.0f);
|
||||
m_inputArea->setSize(width(), height());
|
||||
|
||||
applyState();
|
||||
}
|
||||
|
||||
private:
|
||||
void applyState() {
|
||||
if (pressed()) {
|
||||
setBackground(roleColor(ColorRole::Primary));
|
||||
setBorderColor(roleColor(ColorRole::Primary));
|
||||
setBorderWidth(Style::borderWidth);
|
||||
if (m_title != nullptr) {
|
||||
m_title->setColor(roleColor(ColorRole::OnPrimary));
|
||||
m_title->setText(title);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setBackground(roleColor(ColorRole::Surface));
|
||||
setBorderColor(roleColor(hovered() ? ColorRole::Primary : ColorRole::Surface));
|
||||
setBorderWidth(hovered() ? Style::borderWidth : 0.0f);
|
||||
if (m_title != nullptr) {
|
||||
m_title->setColor(roleColor(ColorRole::OnSurface));
|
||||
void doLayout(Renderer& renderer) override {
|
||||
if (m_radio == nullptr || m_title == nullptr || m_inputArea == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_radio->layout(renderer);
|
||||
|
||||
const float textMaxWidth = std::max(0.0f, width() - paddingLeft() - paddingRight() - gap() - m_radio->width());
|
||||
m_title->setMaxWidth(textMaxWidth);
|
||||
|
||||
m_inputArea->setVisible(false);
|
||||
Flex::doLayout(renderer);
|
||||
m_inputArea->setVisible(true);
|
||||
m_inputArea->setPosition(0.0f, 0.0f);
|
||||
m_inputArea->setSize(width(), height());
|
||||
|
||||
applyState();
|
||||
}
|
||||
|
||||
private:
|
||||
void applyState() {
|
||||
if (pressed()) {
|
||||
setBackground(roleColor(ColorRole::Primary));
|
||||
setBorderColor(roleColor(ColorRole::Primary));
|
||||
setBorderWidth(Style::borderWidth);
|
||||
if (m_title != nullptr) {
|
||||
m_title->setColor(roleColor(ColorRole::OnPrimary));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setBackground(roleColor(ColorRole::Surface));
|
||||
setBorderColor(roleColor(hovered() ? ColorRole::Primary : ColorRole::Surface));
|
||||
setBorderWidth(hovered() ? Style::borderWidth : 0.0f);
|
||||
if (m_title != nullptr) {
|
||||
m_title->setColor(roleColor(ColorRole::OnSurface));
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] bool hovered() const noexcept { return m_inputArea != nullptr && m_inputArea->hovered(); }
|
||||
[[nodiscard]] bool pressed() const noexcept { return m_inputArea != nullptr && m_inputArea->pressed(); }
|
||||
|
||||
std::function<void()> m_onSelect;
|
||||
RadioButton* m_radio = nullptr;
|
||||
Label* m_title = nullptr;
|
||||
Label* m_detail = nullptr;
|
||||
InputArea* m_inputArea = nullptr;
|
||||
Signal<>::ScopedConnection m_paletteConn;
|
||||
};
|
||||
|
||||
std::vector<AudioNode> sortedDevices(const std::vector<AudioNode>& devices) {
|
||||
std::vector<AudioNode> sorted = devices;
|
||||
std::ranges::sort(sorted, [](const AudioNode& a, const AudioNode& b) {
|
||||
const std::string& left = !a.description.empty() ? a.description : a.name;
|
||||
const std::string& right = !b.description.empty() ? b.description : b.name;
|
||||
if (left != right) {
|
||||
return left < right;
|
||||
}
|
||||
return a.id < b.id;
|
||||
});
|
||||
return sorted;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool hovered() const noexcept { return m_inputArea != nullptr && m_inputArea->hovered(); }
|
||||
[[nodiscard]] bool pressed() const noexcept { return m_inputArea != nullptr && m_inputArea->pressed(); }
|
||||
|
||||
std::function<void()> m_onSelect;
|
||||
RadioButton* m_radio = nullptr;
|
||||
Label* m_title = nullptr;
|
||||
Label* m_detail = nullptr;
|
||||
InputArea* m_inputArea = nullptr;
|
||||
Signal<>::ScopedConnection m_paletteConn;
|
||||
};
|
||||
|
||||
std::vector<AudioNode> sortedDevices(const std::vector<AudioNode>& devices) {
|
||||
std::vector<AudioNode> sorted = devices;
|
||||
std::ranges::sort(sorted, [](const AudioNode& a, const AudioNode& b) {
|
||||
const std::string& left = !a.description.empty() ? a.description : a.name;
|
||||
const std::string& right = !b.description.empty() ? b.description : b.name;
|
||||
if (left != right) {
|
||||
return left < right;
|
||||
}
|
||||
return a.id < b.id;
|
||||
});
|
||||
return sorted;
|
||||
}
|
||||
|
||||
void addSubtitle(Flex& parent, const std::string& text, float scale) {
|
||||
auto label = std::make_unique<Label>();
|
||||
label->setText(text);
|
||||
label->setCaptionStyle();
|
||||
label->setFontSize(Style::fontSizeCaption * scale);
|
||||
label->setColor(roleColor(ColorRole::OnSurfaceVariant));
|
||||
parent.addChild(std::move(label));
|
||||
}
|
||||
|
||||
void addEmptyState(Flex& parent, const std::string& title, const std::string& body, float scale) {
|
||||
auto card = std::make_unique<Flex>();
|
||||
card->setDirection(FlexDirection::Vertical);
|
||||
card->setAlign(FlexAlign::Start);
|
||||
card->setGap(Style::spaceXs * scale);
|
||||
card->setPadding(Style::spaceMd * scale);
|
||||
card->setRadius(Style::radiusMd * scale);
|
||||
card->setBackground(roleColor(ColorRole::Surface));
|
||||
card->setBorderWidth(0.0f);
|
||||
|
||||
auto titleLabel = std::make_unique<Label>();
|
||||
titleLabel->setText(title);
|
||||
titleLabel->setBold(true);
|
||||
titleLabel->setFontSize(Style::fontSizeBody * scale);
|
||||
titleLabel->setColor(roleColor(ColorRole::OnSurface));
|
||||
card->addChild(std::move(titleLabel));
|
||||
|
||||
auto bodyLabel = std::make_unique<Label>();
|
||||
bodyLabel->setText(body);
|
||||
bodyLabel->setCaptionStyle();
|
||||
bodyLabel->setFontSize(Style::fontSizeCaption * scale);
|
||||
bodyLabel->setColor(roleColor(ColorRole::OnSurfaceVariant));
|
||||
card->addChild(std::move(bodyLabel));
|
||||
|
||||
parent.addChild(std::move(card));
|
||||
}
|
||||
|
||||
std::string deviceListKey(const std::vector<AudioNode>& devices) {
|
||||
std::string key;
|
||||
for (const auto& device : devices) {
|
||||
key += std::to_string(device.id);
|
||||
key.push_back(':');
|
||||
key += device.isDefault ? '1' : '0';
|
||||
key.push_back(':');
|
||||
key += device.name;
|
||||
key.push_back(':');
|
||||
key += device.description;
|
||||
key.push_back('\n');
|
||||
void addSubtitle(Flex& parent, const std::string& text, float scale) {
|
||||
auto label = std::make_unique<Label>();
|
||||
label->setText(text);
|
||||
label->setCaptionStyle();
|
||||
label->setFontSize(Style::fontSizeCaption * scale);
|
||||
label->setColor(roleColor(ColorRole::OnSurfaceVariant));
|
||||
parent.addChild(std::move(label));
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
std::string widestPercentLabel(float sliderMaxValue) {
|
||||
const std::size_t digits =
|
||||
std::to_string(static_cast<int>(std::round(std::max(0.0f, sliderMaxValue) * 100.0f))).size();
|
||||
return std::string(std::max<std::size_t>(1, digits), '8') + "%";
|
||||
}
|
||||
void addEmptyState(Flex& parent, const std::string& title, const std::string& body, float scale) {
|
||||
auto card = std::make_unique<Flex>();
|
||||
card->setDirection(FlexDirection::Vertical);
|
||||
card->setAlign(FlexAlign::Start);
|
||||
card->setGap(Style::spaceXs * scale);
|
||||
card->setPadding(Style::spaceMd * scale);
|
||||
card->setRadius(Style::radiusMd * scale);
|
||||
card->setBackground(roleColor(ColorRole::Surface));
|
||||
card->setBorderWidth(0.0f);
|
||||
|
||||
auto titleLabel = std::make_unique<Label>();
|
||||
titleLabel->setText(title);
|
||||
titleLabel->setBold(true);
|
||||
titleLabel->setFontSize(Style::fontSizeBody * scale);
|
||||
titleLabel->setColor(roleColor(ColorRole::OnSurface));
|
||||
card->addChild(std::move(titleLabel));
|
||||
|
||||
auto bodyLabel = std::make_unique<Label>();
|
||||
bodyLabel->setText(body);
|
||||
bodyLabel->setCaptionStyle();
|
||||
bodyLabel->setFontSize(Style::fontSizeCaption * scale);
|
||||
bodyLabel->setColor(roleColor(ColorRole::OnSurfaceVariant));
|
||||
card->addChild(std::move(bodyLabel));
|
||||
|
||||
parent.addChild(std::move(card));
|
||||
}
|
||||
|
||||
std::string deviceListKey(const std::vector<AudioNode>& devices) {
|
||||
std::string key;
|
||||
for (const auto& device : devices) {
|
||||
key += std::to_string(device.id);
|
||||
key.push_back(':');
|
||||
key += device.isDefault ? '1' : '0';
|
||||
key.push_back(':');
|
||||
key += device.name;
|
||||
key.push_back(':');
|
||||
key += device.description;
|
||||
key.push_back('\n');
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
std::string widestPercentLabel(float sliderMaxValue) {
|
||||
const std::size_t digits =
|
||||
std::to_string(static_cast<int>(std::round(std::max(0.0f, sliderMaxValue) * 100.0f))).size();
|
||||
return std::string(std::max<std::size_t>(1, digits), '8') + "%";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AudioTab::AudioTab(PipeWireService* audio, ConfigService* config) : m_audio(audio), m_config(config) {}
|
||||
|
||||
bool AudioTab::dragging() const noexcept {
|
||||
return (m_outputSlider != nullptr && m_outputSlider->dragging()) || (m_inputSlider != nullptr && m_inputSlider->dragging());
|
||||
return (m_outputSlider != nullptr && m_outputSlider->dragging()) ||
|
||||
(m_inputSlider != nullptr && m_inputSlider->dragging());
|
||||
}
|
||||
|
||||
std::unique_ptr<Flex> AudioTab::create() {
|
||||
@@ -406,12 +406,12 @@ void AudioTab::doLayout(Renderer& renderer, float contentWidth, float bodyHeight
|
||||
m_rootLayout->layout(renderer);
|
||||
|
||||
if (m_outputDeviceLabel != nullptr && m_outputVolumeCard != nullptr) {
|
||||
m_outputDeviceLabel->setMaxWidth(
|
||||
std::max(0.0f, m_outputVolumeCard->width() - m_outputVolumeCard->paddingLeft() - m_outputVolumeCard->paddingRight()));
|
||||
m_outputDeviceLabel->setMaxWidth(std::max(0.0f, m_outputVolumeCard->width() - m_outputVolumeCard->paddingLeft() -
|
||||
m_outputVolumeCard->paddingRight()));
|
||||
}
|
||||
if (m_inputDeviceLabel != nullptr && m_inputVolumeCard != nullptr) {
|
||||
m_inputDeviceLabel->setMaxWidth(
|
||||
std::max(0.0f, m_inputVolumeCard->width() - m_inputVolumeCard->paddingLeft() - m_inputVolumeCard->paddingRight()));
|
||||
m_inputDeviceLabel->setMaxWidth(std::max(0.0f, m_inputVolumeCard->width() - m_inputVolumeCard->paddingLeft() -
|
||||
m_inputVolumeCard->paddingRight()));
|
||||
}
|
||||
m_rootLayout->layout(renderer);
|
||||
|
||||
@@ -441,12 +441,12 @@ void AudioTab::doUpdate(Renderer& renderer) {
|
||||
const bool inputDragging = m_inputSlider != nullptr && m_inputSlider->dragging();
|
||||
|
||||
if (m_outputDeviceLabel != nullptr) {
|
||||
m_outputDeviceLabel->setText(
|
||||
sink != nullptr ? (!sink->description.empty() ? sink->description : sink->name) : "No output device selected");
|
||||
m_outputDeviceLabel->setText(sink != nullptr ? (!sink->description.empty() ? sink->description : sink->name)
|
||||
: "No output device selected");
|
||||
}
|
||||
if (m_inputDeviceLabel != nullptr) {
|
||||
m_inputDeviceLabel->setText(
|
||||
source != nullptr ? (!source->description.empty() ? source->description : source->name) : "No input device selected");
|
||||
m_inputDeviceLabel->setText(source != nullptr ? (!source->description.empty() ? source->description : source->name)
|
||||
: "No input device selected");
|
||||
}
|
||||
|
||||
const float sinkVolume = sink != nullptr ? sink->volume : 0.0f;
|
||||
@@ -454,17 +454,14 @@ void AudioTab::doUpdate(Renderer& renderer) {
|
||||
const bool showPendingSink = sink != nullptr && m_pendingSinkVolume >= 0.0f && m_pendingSinkId == sink->id;
|
||||
const bool showPendingSource = source != nullptr && m_pendingSourceVolume >= 0.0f && m_pendingSourceId == source->id;
|
||||
const bool holdSinkState = outputDragging && sink != nullptr && m_lastSentSinkVolume >= 0.0f &&
|
||||
now < m_ignoreSinkStateUntil &&
|
||||
std::abs(sink->volume - m_lastSentSinkVolume) > 0.02f;
|
||||
now < m_ignoreSinkStateUntil && std::abs(sink->volume - m_lastSentSinkVolume) > 0.02f;
|
||||
const bool holdSourceState = inputDragging && source != nullptr && m_lastSentSourceVolume >= 0.0f &&
|
||||
now < m_ignoreSourceStateUntil &&
|
||||
std::abs(source->volume - m_lastSentSourceVolume) > 0.02f;
|
||||
const float displayedSinkVolume =
|
||||
std::clamp(showPendingSink ? m_pendingSinkVolume : (holdSinkState ? m_lastSentSinkVolume : sinkVolume), 0.0f,
|
||||
sliderMax);
|
||||
const float displayedSinkVolume = std::clamp(
|
||||
showPendingSink ? m_pendingSinkVolume : (holdSinkState ? m_lastSentSinkVolume : sinkVolume), 0.0f, sliderMax);
|
||||
const float displayedSourceVolume =
|
||||
std::clamp(showPendingSource ? m_pendingSourceVolume
|
||||
: (holdSourceState ? m_lastSentSourceVolume : sourceVolume),
|
||||
std::clamp(showPendingSource ? m_pendingSourceVolume : (holdSourceState ? m_lastSentSourceVolume : sourceVolume),
|
||||
0.0f, sliderMax);
|
||||
|
||||
if (m_outputSlider != nullptr) {
|
||||
@@ -683,9 +680,8 @@ void AudioTab::flushPendingVolumes(bool force) {
|
||||
if (shouldSendSink && !force && outputDragging) {
|
||||
const auto nextSendAt = m_lastSinkCommitAt + kVolumeCommitInterval;
|
||||
if (now < nextSendAt) {
|
||||
m_sinkVolumeDebounceTimer.start(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(nextSendAt - now),
|
||||
[this]() { flushPendingVolumes(); });
|
||||
m_sinkVolumeDebounceTimer.start(std::chrono::duration_cast<std::chrono::milliseconds>(nextSendAt - now),
|
||||
[this]() { flushPendingVolumes(); });
|
||||
shouldSendSink = false;
|
||||
}
|
||||
}
|
||||
@@ -713,9 +709,8 @@ void AudioTab::flushPendingVolumes(bool force) {
|
||||
if (shouldSendSource && !force && inputDragging) {
|
||||
const auto nextSendAt = m_lastSourceCommitAt + kVolumeCommitInterval;
|
||||
if (now < nextSendAt) {
|
||||
m_sourceVolumeDebounceTimer.start(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(nextSendAt - now),
|
||||
[this]() { flushPendingVolumes(); });
|
||||
m_sourceVolumeDebounceTimer.start(std::chrono::duration_cast<std::chrono::milliseconds>(nextSendAt - now),
|
||||
[this]() { flushPendingVolumes(); });
|
||||
shouldSendSource = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
#include "core/timer_manager.h"
|
||||
#include "shell/control_center/tab.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
||||
@@ -24,167 +24,167 @@ using namespace control_center;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kRowMinHeight = Style::controlHeightLg;
|
||||
constexpr float kRowMinHeight = Style::controlHeightLg;
|
||||
|
||||
const char* glyphFor(BluetoothDeviceKind kind) {
|
||||
switch (kind) {
|
||||
case BluetoothDeviceKind::Headset:
|
||||
return "bluetooth-device-headset";
|
||||
case BluetoothDeviceKind::Headphones:
|
||||
return "bluetooth-device-headphones";
|
||||
case BluetoothDeviceKind::Earbuds:
|
||||
return "bluetooth-device-earbuds";
|
||||
case BluetoothDeviceKind::Speaker:
|
||||
return "bluetooth-device-speaker";
|
||||
case BluetoothDeviceKind::Microphone:
|
||||
return "bluetooth-device-microphone";
|
||||
case BluetoothDeviceKind::Mouse:
|
||||
return "bluetooth-device-mouse";
|
||||
case BluetoothDeviceKind::Keyboard:
|
||||
return "bluetooth-device-keyboard";
|
||||
case BluetoothDeviceKind::Phone:
|
||||
return "bluetooth-device-phone";
|
||||
case BluetoothDeviceKind::Computer:
|
||||
return "settings-display";
|
||||
case BluetoothDeviceKind::Gamepad:
|
||||
return "bluetooth-device-gamepad";
|
||||
case BluetoothDeviceKind::Watch:
|
||||
return "bluetooth-device-watch";
|
||||
case BluetoothDeviceKind::Tv:
|
||||
return "bluetooth-device-tv";
|
||||
case BluetoothDeviceKind::Unknown:
|
||||
default:
|
||||
return "bluetooth-device-generic";
|
||||
}
|
||||
}
|
||||
|
||||
enum class DeviceBucket : std::uint8_t {
|
||||
Connected,
|
||||
Paired,
|
||||
Available,
|
||||
};
|
||||
|
||||
DeviceBucket bucketFor(const BluetoothDeviceInfo& d) {
|
||||
if (d.connected) {
|
||||
return DeviceBucket::Connected;
|
||||
}
|
||||
if (d.paired) {
|
||||
return DeviceBucket::Paired;
|
||||
}
|
||||
return DeviceBucket::Available;
|
||||
}
|
||||
|
||||
class BluetoothDeviceRow : public Flex {
|
||||
public:
|
||||
BluetoothDeviceRow(BluetoothDeviceInfo device, BluetoothService* service, float scale)
|
||||
: m_device(std::move(device)), m_service(service) {
|
||||
setDirection(FlexDirection::Horizontal);
|
||||
setAlign(FlexAlign::Center);
|
||||
setGap(Style::spaceSm * scale);
|
||||
setPadding(Style::spaceSm * scale, Style::spaceMd * scale);
|
||||
setMinHeight(kRowMinHeight * scale);
|
||||
setRadius(Style::radiusMd * scale);
|
||||
setBackground(roleColor(ColorRole::Surface));
|
||||
setBorderWidth(0.0f);
|
||||
|
||||
auto icon = std::make_unique<Glyph>();
|
||||
icon->setGlyph(glyphFor(m_device.kind));
|
||||
icon->setGlyphSize(Style::fontSizeBody * scale);
|
||||
icon->setColor(roleColor(ColorRole::OnSurface));
|
||||
addChild(std::move(icon));
|
||||
|
||||
auto alias = std::make_unique<Label>();
|
||||
alias->setText(m_device.alias);
|
||||
alias->setBold(m_device.connected);
|
||||
alias->setFontSize(Style::fontSizeBody * scale);
|
||||
alias->setColor(roleColor(ColorRole::OnSurface));
|
||||
alias->setFlexGrow(1.0f);
|
||||
m_title = alias.get();
|
||||
addChild(std::move(alias));
|
||||
|
||||
if (m_device.hasBattery) {
|
||||
auto battery = std::make_unique<Label>();
|
||||
battery->setText(std::to_string(static_cast<int>(m_device.batteryPercent)) + "%");
|
||||
battery->setCaptionStyle();
|
||||
battery->setFontSize(Style::fontSizeCaption * scale);
|
||||
battery->setColor(roleColor(ColorRole::OnSurfaceVariant));
|
||||
addChild(std::move(battery));
|
||||
} else if (m_device.hasRssi && bucketFor(m_device) == DeviceBucket::Available) {
|
||||
auto rssi = std::make_unique<Label>();
|
||||
rssi->setText(std::to_string(static_cast<int>(m_device.rssi)) + " dBm");
|
||||
rssi->setCaptionStyle();
|
||||
rssi->setFontSize(Style::fontSizeCaption * scale);
|
||||
rssi->setColor(roleColor(ColorRole::OnSurfaceVariant));
|
||||
addChild(std::move(rssi));
|
||||
const char* glyphFor(BluetoothDeviceKind kind) {
|
||||
switch (kind) {
|
||||
case BluetoothDeviceKind::Headset:
|
||||
return "bluetooth-device-headset";
|
||||
case BluetoothDeviceKind::Headphones:
|
||||
return "bluetooth-device-headphones";
|
||||
case BluetoothDeviceKind::Earbuds:
|
||||
return "bluetooth-device-earbuds";
|
||||
case BluetoothDeviceKind::Speaker:
|
||||
return "bluetooth-device-speaker";
|
||||
case BluetoothDeviceKind::Microphone:
|
||||
return "bluetooth-device-microphone";
|
||||
case BluetoothDeviceKind::Mouse:
|
||||
return "bluetooth-device-mouse";
|
||||
case BluetoothDeviceKind::Keyboard:
|
||||
return "bluetooth-device-keyboard";
|
||||
case BluetoothDeviceKind::Phone:
|
||||
return "bluetooth-device-phone";
|
||||
case BluetoothDeviceKind::Computer:
|
||||
return "settings-display";
|
||||
case BluetoothDeviceKind::Gamepad:
|
||||
return "bluetooth-device-gamepad";
|
||||
case BluetoothDeviceKind::Watch:
|
||||
return "bluetooth-device-watch";
|
||||
case BluetoothDeviceKind::Tv:
|
||||
return "bluetooth-device-tv";
|
||||
case BluetoothDeviceKind::Unknown:
|
||||
default:
|
||||
return "bluetooth-device-generic";
|
||||
}
|
||||
}
|
||||
|
||||
if (m_device.paired) {
|
||||
auto trust = std::make_unique<Toggle>();
|
||||
trust->setToggleSize(ToggleSize::Small);
|
||||
trust->setScale(scale);
|
||||
trust->setChecked(m_device.trusted);
|
||||
trust->setOnChange([this](bool checked) {
|
||||
if (m_service != nullptr) {
|
||||
m_service->setTrusted(m_device.path, checked);
|
||||
}
|
||||
});
|
||||
addChild(std::move(trust));
|
||||
}
|
||||
enum class DeviceBucket : std::uint8_t {
|
||||
Connected,
|
||||
Paired,
|
||||
Available,
|
||||
};
|
||||
|
||||
auto primary = std::make_unique<Button>();
|
||||
primary->setVariant(ButtonVariant::Default);
|
||||
switch (bucketFor(m_device)) {
|
||||
case DeviceBucket::Connected:
|
||||
primary->setText("Disconnect");
|
||||
primary->setVariant(ButtonVariant::Outline);
|
||||
break;
|
||||
case DeviceBucket::Paired:
|
||||
primary->setText(m_device.connecting ? "Connecting…" : "Connect");
|
||||
break;
|
||||
case DeviceBucket::Available:
|
||||
primary->setText(m_device.connecting ? "Pairing…" : "Pair");
|
||||
break;
|
||||
DeviceBucket bucketFor(const BluetoothDeviceInfo& d) {
|
||||
if (d.connected) {
|
||||
return DeviceBucket::Connected;
|
||||
}
|
||||
primary->setOnClick([this]() {
|
||||
if (m_service == nullptr) {
|
||||
return;
|
||||
if (d.paired) {
|
||||
return DeviceBucket::Paired;
|
||||
}
|
||||
return DeviceBucket::Available;
|
||||
}
|
||||
|
||||
class BluetoothDeviceRow : public Flex {
|
||||
public:
|
||||
BluetoothDeviceRow(BluetoothDeviceInfo device, BluetoothService* service, float scale)
|
||||
: m_device(std::move(device)), m_service(service) {
|
||||
setDirection(FlexDirection::Horizontal);
|
||||
setAlign(FlexAlign::Center);
|
||||
setGap(Style::spaceSm * scale);
|
||||
setPadding(Style::spaceSm * scale, Style::spaceMd * scale);
|
||||
setMinHeight(kRowMinHeight * scale);
|
||||
setRadius(Style::radiusMd * scale);
|
||||
setBackground(roleColor(ColorRole::Surface));
|
||||
setBorderWidth(0.0f);
|
||||
|
||||
auto icon = std::make_unique<Glyph>();
|
||||
icon->setGlyph(glyphFor(m_device.kind));
|
||||
icon->setGlyphSize(Style::fontSizeBody * scale);
|
||||
icon->setColor(roleColor(ColorRole::OnSurface));
|
||||
addChild(std::move(icon));
|
||||
|
||||
auto alias = std::make_unique<Label>();
|
||||
alias->setText(m_device.alias);
|
||||
alias->setBold(m_device.connected);
|
||||
alias->setFontSize(Style::fontSizeBody * scale);
|
||||
alias->setColor(roleColor(ColorRole::OnSurface));
|
||||
alias->setFlexGrow(1.0f);
|
||||
m_title = alias.get();
|
||||
addChild(std::move(alias));
|
||||
|
||||
if (m_device.hasBattery) {
|
||||
auto battery = std::make_unique<Label>();
|
||||
battery->setText(std::to_string(static_cast<int>(m_device.batteryPercent)) + "%");
|
||||
battery->setCaptionStyle();
|
||||
battery->setFontSize(Style::fontSizeCaption * scale);
|
||||
battery->setColor(roleColor(ColorRole::OnSurfaceVariant));
|
||||
addChild(std::move(battery));
|
||||
} else if (m_device.hasRssi && bucketFor(m_device) == DeviceBucket::Available) {
|
||||
auto rssi = std::make_unique<Label>();
|
||||
rssi->setText(std::to_string(static_cast<int>(m_device.rssi)) + " dBm");
|
||||
rssi->setCaptionStyle();
|
||||
rssi->setFontSize(Style::fontSizeCaption * scale);
|
||||
rssi->setColor(roleColor(ColorRole::OnSurfaceVariant));
|
||||
addChild(std::move(rssi));
|
||||
}
|
||||
|
||||
if (m_device.paired) {
|
||||
auto trust = std::make_unique<Toggle>();
|
||||
trust->setToggleSize(ToggleSize::Small);
|
||||
trust->setScale(scale);
|
||||
trust->setChecked(m_device.trusted);
|
||||
trust->setOnChange([this](bool checked) {
|
||||
if (m_service != nullptr) {
|
||||
m_service->setTrusted(m_device.path, checked);
|
||||
}
|
||||
});
|
||||
addChild(std::move(trust));
|
||||
}
|
||||
|
||||
auto primary = std::make_unique<Button>();
|
||||
primary->setVariant(ButtonVariant::Default);
|
||||
switch (bucketFor(m_device)) {
|
||||
case DeviceBucket::Connected:
|
||||
m_service->disconnectDevice(m_device.path);
|
||||
primary->setText("Disconnect");
|
||||
primary->setVariant(ButtonVariant::Outline);
|
||||
break;
|
||||
case DeviceBucket::Paired:
|
||||
m_service->connect(m_device.path);
|
||||
primary->setText(m_device.connecting ? "Connecting…" : "Connect");
|
||||
break;
|
||||
case DeviceBucket::Available:
|
||||
m_service->pair(m_device.path);
|
||||
primary->setText(m_device.connecting ? "Pairing…" : "Pair");
|
||||
break;
|
||||
}
|
||||
PanelManager::instance().refresh();
|
||||
});
|
||||
m_primaryButton = static_cast<Button*>(addChild(std::move(primary)));
|
||||
|
||||
if (m_device.paired) {
|
||||
auto forget = std::make_unique<Button>();
|
||||
forget->setVariant(ButtonVariant::Ghost);
|
||||
forget->setText("Forget");
|
||||
forget->setOnClick([this]() {
|
||||
if (m_service != nullptr) {
|
||||
m_service->forget(m_device.path);
|
||||
primary->setOnClick([this]() {
|
||||
if (m_service == nullptr) {
|
||||
return;
|
||||
}
|
||||
switch (bucketFor(m_device)) {
|
||||
case DeviceBucket::Connected:
|
||||
m_service->disconnectDevice(m_device.path);
|
||||
break;
|
||||
case DeviceBucket::Paired:
|
||||
m_service->connect(m_device.path);
|
||||
break;
|
||||
case DeviceBucket::Available:
|
||||
m_service->pair(m_device.path);
|
||||
break;
|
||||
}
|
||||
PanelManager::instance().refresh();
|
||||
});
|
||||
m_forgetButton = static_cast<Button*>(addChild(std::move(forget)));
|
||||
}
|
||||
}
|
||||
m_primaryButton = static_cast<Button*>(addChild(std::move(primary)));
|
||||
|
||||
private:
|
||||
BluetoothDeviceInfo m_device;
|
||||
BluetoothService* m_service = nullptr;
|
||||
Label* m_title = nullptr;
|
||||
Button* m_primaryButton = nullptr;
|
||||
Button* m_forgetButton = nullptr;
|
||||
};
|
||||
if (m_device.paired) {
|
||||
auto forget = std::make_unique<Button>();
|
||||
forget->setVariant(ButtonVariant::Ghost);
|
||||
forget->setText("Forget");
|
||||
forget->setOnClick([this]() {
|
||||
if (m_service != nullptr) {
|
||||
m_service->forget(m_device.path);
|
||||
}
|
||||
PanelManager::instance().refresh();
|
||||
});
|
||||
m_forgetButton = static_cast<Button*>(addChild(std::move(forget)));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
BluetoothDeviceInfo m_device;
|
||||
BluetoothService* m_service = nullptr;
|
||||
Label* m_title = nullptr;
|
||||
Button* m_primaryButton = nullptr;
|
||||
Button* m_forgetButton = nullptr;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -502,8 +502,7 @@ void BluetoothTab::syncPairingCard() {
|
||||
if (m_pairingTitle != nullptr) {
|
||||
m_pairingTitle->setText("Pair " + alias);
|
||||
}
|
||||
const bool needsInput =
|
||||
req.kind == BluetoothPairingKind::PinCode || req.kind == BluetoothPairingKind::Passkey;
|
||||
const bool needsInput = req.kind == BluetoothPairingKind::PinCode || req.kind == BluetoothPairingKind::Passkey;
|
||||
const bool showsCode = req.kind == BluetoothPairingKind::Confirm ||
|
||||
req.kind == BluetoothPairingKind::DisplayPasskey ||
|
||||
req.kind == BluetoothPairingKind::DisplayPinCode;
|
||||
@@ -634,7 +633,7 @@ void BluetoothTab::rebuildDeviceList(Renderer& renderer) {
|
||||
if (devices.empty()) {
|
||||
auto empty = std::make_unique<Label>();
|
||||
empty->setText(m_service->state().powered ? "No devices found. Start scanning to discover nearby Bluetooth devices."
|
||||
: "Bluetooth is off");
|
||||
: "Bluetooth is off");
|
||||
empty->setCaptionStyle();
|
||||
empty->setFontSize(Style::fontSizeCaption);
|
||||
empty->setColor(roleColor(ColorRole::OnSurfaceVariant));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "core/ui_phase.h"
|
||||
#include "shell/control_center/calendar_tab.h"
|
||||
|
||||
#include "core/ui_phase.h"
|
||||
#include "render/core/renderer.h"
|
||||
#include "shell/control_center/tab.h"
|
||||
#include "shell/panel/panel_manager.h"
|
||||
@@ -20,73 +20,73 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kCalendarGridGap = Style::spaceSm;
|
||||
constexpr float kCalendarNavButtonSize = Style::controlHeight;
|
||||
constexpr float kCalendarWeekdayRowHeight = Style::controlHeight;
|
||||
constexpr float kCalendarHeaderHeight = Style::controlHeightLg;
|
||||
constexpr float kCalendarCellSizeMin = Style::controlHeightSm + Style::spaceXs;
|
||||
constexpr float kCalendarCellSizeMax = Style::controlHeightLg + Style::spaceLg;
|
||||
constexpr float kCalendarLayoutEpsilon = 0.5f;
|
||||
constexpr float kCalendarGridGap = Style::spaceSm;
|
||||
constexpr float kCalendarNavButtonSize = Style::controlHeight;
|
||||
constexpr float kCalendarWeekdayRowHeight = Style::controlHeight;
|
||||
constexpr float kCalendarHeaderHeight = Style::controlHeightLg;
|
||||
constexpr float kCalendarCellSizeMin = Style::controlHeightSm + Style::spaceXs;
|
||||
constexpr float kCalendarCellSizeMax = Style::controlHeightLg + Style::spaceLg;
|
||||
constexpr float kCalendarLayoutEpsilon = 0.5f;
|
||||
|
||||
std::string todayLabel() {
|
||||
std::time_t now = std::time(nullptr);
|
||||
std::tm local = *std::localtime(&now);
|
||||
char buf[64];
|
||||
if (std::strftime(buf, sizeof(buf), "Today · %A, %d %B %Y", &local) == 0) {
|
||||
return "Today";
|
||||
std::string todayLabel() {
|
||||
std::time_t now = std::time(nullptr);
|
||||
std::tm local = *std::localtime(&now);
|
||||
char buf[64];
|
||||
if (std::strftime(buf, sizeof(buf), "Today · %A, %d %B %Y", &local) == 0) {
|
||||
return "Today";
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
std::string monthName(int month) {
|
||||
static constexpr std::array<const char*, 12> kMonths = {"January", "February", "March", "April",
|
||||
"May", "June", "July", "August",
|
||||
"September", "October", "November", "December"};
|
||||
if (month < 0 || month >= static_cast<int>(kMonths.size())) {
|
||||
return "Calendar";
|
||||
std::string monthName(int month) {
|
||||
static constexpr std::array<const char*, 12> kMonths = {"January", "February", "March", "April",
|
||||
"May", "June", "July", "August",
|
||||
"September", "October", "November", "December"};
|
||||
if (month < 0 || month >= static_cast<int>(kMonths.size())) {
|
||||
return "Calendar";
|
||||
}
|
||||
return kMonths[static_cast<std::size_t>(month)];
|
||||
}
|
||||
return kMonths[static_cast<std::size_t>(month)];
|
||||
}
|
||||
|
||||
int daysInMonth(int yearValue, int monthValue) {
|
||||
static constexpr std::array<int, 12> kDays = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
|
||||
int result = kDays[static_cast<std::size_t>(monthValue)];
|
||||
if (monthValue == 1) {
|
||||
const bool leap = ((yearValue % 4 == 0 && yearValue % 100 != 0) || (yearValue % 400 == 0));
|
||||
result = leap ? 29 : 28;
|
||||
int daysInMonth(int yearValue, int monthValue) {
|
||||
static constexpr std::array<int, 12> kDays = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
|
||||
int result = kDays[static_cast<std::size_t>(monthValue)];
|
||||
if (monthValue == 1) {
|
||||
const bool leap = ((yearValue % 4 == 0 && yearValue % 100 != 0) || (yearValue % 400 == 0));
|
||||
result = leap ? 29 : 28;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
struct CalendarBuildState {
|
||||
int currentYear = 0;
|
||||
int currentMonth = 0;
|
||||
int today = 0;
|
||||
int displayYear = 0;
|
||||
int displayMonth = 0;
|
||||
int displayWeekday = 0;
|
||||
bool isCurrentMonth = false;
|
||||
};
|
||||
struct CalendarBuildState {
|
||||
int currentYear = 0;
|
||||
int currentMonth = 0;
|
||||
int today = 0;
|
||||
int displayYear = 0;
|
||||
int displayMonth = 0;
|
||||
int displayWeekday = 0;
|
||||
bool isCurrentMonth = false;
|
||||
};
|
||||
|
||||
CalendarBuildState currentCalendarState(int monthOffset) {
|
||||
std::time_t now = std::time(nullptr);
|
||||
std::tm local = *std::localtime(&now);
|
||||
CalendarBuildState currentCalendarState(int monthOffset) {
|
||||
std::time_t now = std::time(nullptr);
|
||||
std::tm local = *std::localtime(&now);
|
||||
|
||||
CalendarBuildState state;
|
||||
state.currentYear = local.tm_year + 1900;
|
||||
state.currentMonth = local.tm_mon;
|
||||
state.today = local.tm_mday;
|
||||
CalendarBuildState state;
|
||||
state.currentYear = local.tm_year + 1900;
|
||||
state.currentMonth = local.tm_mon;
|
||||
state.today = local.tm_mday;
|
||||
|
||||
std::tm display = local;
|
||||
display.tm_mday = 1;
|
||||
display.tm_mon += monthOffset;
|
||||
std::mktime(&display);
|
||||
state.displayYear = display.tm_year + 1900;
|
||||
state.displayMonth = display.tm_mon;
|
||||
state.displayWeekday = display.tm_wday;
|
||||
state.isCurrentMonth = state.displayYear == state.currentYear && state.displayMonth == state.currentMonth;
|
||||
return state;
|
||||
}
|
||||
std::tm display = local;
|
||||
display.tm_mday = 1;
|
||||
display.tm_mon += monthOffset;
|
||||
std::mktime(&display);
|
||||
state.displayYear = display.tm_year + 1900;
|
||||
state.displayMonth = display.tm_mon;
|
||||
state.displayWeekday = display.tm_wday;
|
||||
state.isCurrentMonth = state.displayYear == state.currentYear && state.displayMonth == state.currentMonth;
|
||||
return state;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -225,8 +225,8 @@ void CalendarTab::doLayout(Renderer& renderer, float contentWidth, float bodyHei
|
||||
const bool sizeChanged = std::abs(innerWidth - m_lastInnerWidth) >= kCalendarLayoutEpsilon ||
|
||||
std::abs(innerHeight - m_lastInnerHeight) >= kCalendarLayoutEpsilon;
|
||||
const bool displayChanged = state.displayYear != m_lastDisplayYear || state.displayMonth != m_lastDisplayMonth;
|
||||
const bool todayChanged = state.currentYear != m_lastCurrentYear || state.currentMonth != m_lastCurrentMonth ||
|
||||
state.today != m_lastToday;
|
||||
const bool todayChanged =
|
||||
state.currentYear != m_lastCurrentYear || state.currentMonth != m_lastCurrentMonth || state.today != m_lastToday;
|
||||
if (!sizeChanged && !displayChanged && !todayChanged) {
|
||||
return;
|
||||
}
|
||||
@@ -359,7 +359,7 @@ void CalendarTab::rebuild() {
|
||||
cell->setMinHeight(dayCellHeight);
|
||||
cell->setRadius(Style::radiusLg * scale);
|
||||
cell->setFontSize(dayCellHeight > (Style::controlHeightLg + Style::spaceXs) * scale ? Style::fontSizeTitle * scale
|
||||
: Style::fontSizeBody * scale);
|
||||
: Style::fontSizeBody * scale);
|
||||
cell->setText("");
|
||||
|
||||
if (index < firstWeekdayMonBased) {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
#include "shell/control_center/audio_tab.h"
|
||||
#include "shell/control_center/bluetooth_tab.h"
|
||||
#include "shell/control_center/display_tab.h"
|
||||
#include "shell/control_center/calendar_tab.h"
|
||||
#include "shell/control_center/display_tab.h"
|
||||
#include "shell/control_center/media_tab.h"
|
||||
#include "shell/control_center/network_tab.h"
|
||||
#include "shell/control_center/notifications_tab.h"
|
||||
@@ -38,11 +38,12 @@ class WeatherService;
|
||||
class ControlCenterPanel : public Panel {
|
||||
public:
|
||||
ControlCenterPanel(NotificationManager* notifications, PipeWireService* audio, MprisService* mpris,
|
||||
ConfigService* config = nullptr, HttpClient* httpClient = nullptr, WeatherService* weather = nullptr,
|
||||
PipeWireSpectrum* spectrum = nullptr, UPowerService* upower = nullptr,
|
||||
PowerProfilesService* powerProfiles = nullptr, NetworkService* network = nullptr,
|
||||
NetworkSecretAgent* networkSecrets = nullptr, BluetoothService* bluetooth = nullptr,
|
||||
BluetoothAgent* bluetoothAgent = nullptr, BrightnessService* brightness = nullptr);
|
||||
ConfigService* config = nullptr, HttpClient* httpClient = nullptr,
|
||||
WeatherService* weather = nullptr, PipeWireSpectrum* spectrum = nullptr,
|
||||
UPowerService* upower = nullptr, PowerProfilesService* powerProfiles = nullptr,
|
||||
NetworkService* network = nullptr, NetworkSecretAgent* networkSecrets = nullptr,
|
||||
BluetoothService* bluetooth = nullptr, BluetoothAgent* bluetoothAgent = nullptr,
|
||||
BrightnessService* brightness = nullptr);
|
||||
|
||||
void create() override;
|
||||
void onFrameTick(float deltaMs) override;
|
||||
|
||||
@@ -18,18 +18,18 @@ using namespace control_center;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kBrightnessSyncEpsilon = 0.005f;
|
||||
constexpr auto kBrightnessCommitInterval = std::chrono::milliseconds(16);
|
||||
constexpr auto kBrightnessStateHoldoff = std::chrono::milliseconds(180);
|
||||
constexpr float kBrightnessSyncEpsilon = 0.005f;
|
||||
constexpr auto kBrightnessCommitInterval = std::chrono::milliseconds(16);
|
||||
constexpr auto kBrightnessStateHoldoff = std::chrono::milliseconds(180);
|
||||
|
||||
std::string buildDisplayListKey(const std::vector<BrightnessDisplay>& displays) {
|
||||
std::string key;
|
||||
for (const auto& d : displays) {
|
||||
key += d.id;
|
||||
key += ';';
|
||||
std::string buildDisplayListKey(const std::vector<BrightnessDisplay>& displays) {
|
||||
std::string key;
|
||||
for (const auto& d : displays) {
|
||||
key += d.id;
|
||||
key += ';';
|
||||
}
|
||||
return key;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -126,8 +126,8 @@ void DisplayTab::doUpdate(Renderer& renderer) {
|
||||
const bool holdState = isDragging && m_lastSentBrightness >= 0.0f && now < m_ignoreStateUntil &&
|
||||
std::abs(display->brightness - m_lastSentBrightness) > 0.02f;
|
||||
|
||||
const float displayedBrightness =
|
||||
std::clamp(isPending ? m_pendingBrightness : (holdState ? m_lastSentBrightness : display->brightness), 0.0f, 1.0f);
|
||||
const float displayedBrightness = std::clamp(
|
||||
isPending ? m_pendingBrightness : (holdState ? m_lastSentBrightness : display->brightness), 0.0f, 1.0f);
|
||||
|
||||
card.slider->setEnabled(true);
|
||||
if (!isDragging && std::abs(displayedBrightness - card.lastBrightness) >= kBrightnessSyncEpsilon) {
|
||||
@@ -286,9 +286,7 @@ void DisplayTab::queueBrightness(const std::string& displayId, float value) {
|
||||
}
|
||||
|
||||
if (!m_debounceTimer.active()) {
|
||||
m_debounceTimer.start(kBrightnessCommitInterval, [this]() {
|
||||
flushPendingBrightness();
|
||||
});
|
||||
m_debounceTimer.start(kBrightnessCommitInterval, [this]() { flushPendingBrightness(); });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,156 +31,156 @@ using namespace control_center;
|
||||
|
||||
namespace {
|
||||
|
||||
const Logger kLog{"media_tab"};
|
||||
const Logger kLog{"media_tab"};
|
||||
|
||||
constexpr float kArtworkSize = Style::controlHeightLg * 6;
|
||||
constexpr float kMediaNowCardMinHeight = Style::controlHeightLg * 11 + Style::spaceSm * 2;
|
||||
constexpr float kMediaControlsHeight = Style::controlHeightLg + Style::spaceXs;
|
||||
constexpr float kMediaPlayPauseHeight = Style::controlHeightLg + Style::spaceSm;
|
||||
constexpr float kMediaArtworkMinHeight = Style::controlHeightLg * 4;
|
||||
constexpr auto kNoActivePlayerGrace = std::chrono::milliseconds(2000);
|
||||
constexpr float kArtworkSize = Style::controlHeightLg * 6;
|
||||
constexpr float kMediaNowCardMinHeight = Style::controlHeightLg * 11 + Style::spaceSm * 2;
|
||||
constexpr float kMediaControlsHeight = Style::controlHeightLg + Style::spaceXs;
|
||||
constexpr float kMediaPlayPauseHeight = Style::controlHeightLg + Style::spaceSm;
|
||||
constexpr float kMediaArtworkMinHeight = Style::controlHeightLg * 4;
|
||||
constexpr auto kNoActivePlayerGrace = std::chrono::milliseconds(2000);
|
||||
|
||||
bool isRemoteArtUrl(std::string_view artUrl) {
|
||||
return artUrl.starts_with("https://") || artUrl.starts_with("http://");
|
||||
}
|
||||
|
||||
std::string extractQueryParam(std::string_view url, std::string_view key) {
|
||||
const auto queryPos = url.find('?');
|
||||
if (queryPos == std::string_view::npos) {
|
||||
return {};
|
||||
bool isRemoteArtUrl(std::string_view artUrl) {
|
||||
return artUrl.starts_with("https://") || artUrl.starts_with("http://");
|
||||
}
|
||||
|
||||
std::string_view query = url.substr(queryPos + 1);
|
||||
while (!query.empty()) {
|
||||
const auto ampPos = query.find('&');
|
||||
const std::string_view pair = query.substr(0, ampPos);
|
||||
const auto eqPos = pair.find('=');
|
||||
const std::string_view pairKey = pair.substr(0, eqPos);
|
||||
if (pairKey == key) {
|
||||
return eqPos == std::string_view::npos ? std::string{} : std::string(pair.substr(eqPos + 1));
|
||||
std::string extractQueryParam(std::string_view url, std::string_view key) {
|
||||
const auto queryPos = url.find('?');
|
||||
if (queryPos == std::string_view::npos) {
|
||||
return {};
|
||||
}
|
||||
if (ampPos == std::string_view::npos) {
|
||||
break;
|
||||
|
||||
std::string_view query = url.substr(queryPos + 1);
|
||||
while (!query.empty()) {
|
||||
const auto ampPos = query.find('&');
|
||||
const std::string_view pair = query.substr(0, ampPos);
|
||||
const auto eqPos = pair.find('=');
|
||||
const std::string_view pairKey = pair.substr(0, eqPos);
|
||||
if (pairKey == key) {
|
||||
return eqPos == std::string_view::npos ? std::string{} : std::string(pair.substr(eqPos + 1));
|
||||
}
|
||||
if (ampPos == std::string_view::npos) {
|
||||
break;
|
||||
}
|
||||
query.remove_prefix(ampPos + 1);
|
||||
}
|
||||
query.remove_prefix(ampPos + 1);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string deriveYouTubeThumbnailUrl(std::string_view sourceUrl) {
|
||||
if (sourceUrl.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string videoId;
|
||||
if (sourceUrl.find("youtube.com/watch") != std::string_view::npos) {
|
||||
videoId = extractQueryParam(sourceUrl, "v");
|
||||
} else if (sourceUrl.find("youtu.be/") != std::string_view::npos) {
|
||||
const auto marker = sourceUrl.find("youtu.be/");
|
||||
const auto start = marker + std::string_view("youtu.be/").size();
|
||||
const auto end = sourceUrl.find_first_of("?#&/", start);
|
||||
videoId =
|
||||
std::string(sourceUrl.substr(start, end == std::string_view::npos ? sourceUrl.size() - start : end - start));
|
||||
} else if (sourceUrl.find("youtube.com/shorts/") != std::string_view::npos) {
|
||||
const auto marker = sourceUrl.find("youtube.com/shorts/");
|
||||
const auto start = marker + std::string_view("youtube.com/shorts/").size();
|
||||
const auto end = sourceUrl.find_first_of("?#&/", start);
|
||||
videoId =
|
||||
std::string(sourceUrl.substr(start, end == std::string_view::npos ? sourceUrl.size() - start : end - start));
|
||||
std::string deriveYouTubeThumbnailUrl(std::string_view sourceUrl) {
|
||||
if (sourceUrl.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string videoId;
|
||||
if (sourceUrl.find("youtube.com/watch") != std::string_view::npos) {
|
||||
videoId = extractQueryParam(sourceUrl, "v");
|
||||
} else if (sourceUrl.find("youtu.be/") != std::string_view::npos) {
|
||||
const auto marker = sourceUrl.find("youtu.be/");
|
||||
const auto start = marker + std::string_view("youtu.be/").size();
|
||||
const auto end = sourceUrl.find_first_of("?#&/", start);
|
||||
videoId =
|
||||
std::string(sourceUrl.substr(start, end == std::string_view::npos ? sourceUrl.size() - start : end - start));
|
||||
} else if (sourceUrl.find("youtube.com/shorts/") != std::string_view::npos) {
|
||||
const auto marker = sourceUrl.find("youtube.com/shorts/");
|
||||
const auto start = marker + std::string_view("youtube.com/shorts/").size();
|
||||
const auto end = sourceUrl.find_first_of("?#&/", start);
|
||||
videoId =
|
||||
std::string(sourceUrl.substr(start, end == std::string_view::npos ? sourceUrl.size() - start : end - start));
|
||||
}
|
||||
|
||||
if (videoId.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return std::format("https://i.ytimg.com/vi/{}/hqdefault.jpg", videoId);
|
||||
}
|
||||
|
||||
if (videoId.empty()) {
|
||||
return {};
|
||||
std::string effectiveArtUrl(const MprisPlayerInfo& player) {
|
||||
if (!player.artUrl.empty()) {
|
||||
return player.artUrl;
|
||||
}
|
||||
return deriveYouTubeThumbnailUrl(player.sourceUrl);
|
||||
}
|
||||
|
||||
return std::format("https://i.ytimg.com/vi/{}/hqdefault.jpg", videoId);
|
||||
}
|
||||
|
||||
std::string effectiveArtUrl(const MprisPlayerInfo& player) {
|
||||
if (!player.artUrl.empty()) {
|
||||
return player.artUrl;
|
||||
int hexValue(char ch) {
|
||||
if (ch >= '0' && ch <= '9') {
|
||||
return ch - '0';
|
||||
}
|
||||
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||
if (ch >= 'a' && ch <= 'f') {
|
||||
return 10 + (ch - 'a');
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return deriveYouTubeThumbnailUrl(player.sourceUrl);
|
||||
}
|
||||
|
||||
int hexValue(char ch) {
|
||||
if (ch >= '0' && ch <= '9') {
|
||||
return ch - '0';
|
||||
std::string decodeUriComponent(std::string_view text) {
|
||||
std::string decoded;
|
||||
decoded.reserve(text.size());
|
||||
|
||||
for (std::size_t i = 0; i < text.size(); ++i) {
|
||||
if (text[i] == '%' && i + 2 < text.size()) {
|
||||
const int hi = hexValue(text[i + 1]);
|
||||
const int lo = hexValue(text[i + 2]);
|
||||
if (hi >= 0 && lo >= 0) {
|
||||
decoded.push_back(static_cast<char>((hi << 4) | lo));
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
decoded.push_back(text[i]);
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||
if (ch >= 'a' && ch <= 'f') {
|
||||
return 10 + (ch - 'a');
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::string decodeUriComponent(std::string_view text) {
|
||||
std::string decoded;
|
||||
decoded.reserve(text.size());
|
||||
|
||||
for (std::size_t i = 0; i < text.size(); ++i) {
|
||||
if (text[i] == '%' && i + 2 < text.size()) {
|
||||
const int hi = hexValue(text[i + 1]);
|
||||
const int lo = hexValue(text[i + 2]);
|
||||
if (hi >= 0 && lo >= 0) {
|
||||
decoded.push_back(static_cast<char>((hi << 4) | lo));
|
||||
i += 2;
|
||||
continue;
|
||||
std::string normalizeArtPath(std::string_view artUrl) {
|
||||
if (artUrl.empty()) {
|
||||
return {};
|
||||
}
|
||||
if (isRemoteArtUrl(artUrl)) {
|
||||
return {};
|
||||
}
|
||||
std::string path(artUrl);
|
||||
constexpr std::string_view prefix = "file://";
|
||||
if (path.starts_with(prefix)) {
|
||||
path.erase(0, prefix.size());
|
||||
if (path.starts_with("localhost/")) {
|
||||
path.erase(0, std::string_view("localhost").size());
|
||||
} else if (!path.empty() && path.front() != '/') {
|
||||
const auto firstSlash = path.find('/');
|
||||
path = firstSlash == std::string::npos ? std::string{} : path.substr(firstSlash);
|
||||
}
|
||||
}
|
||||
decoded.push_back(text[i]);
|
||||
return decodeUriComponent(path);
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
std::filesystem::path artCachePath(std::string_view artUrl) {
|
||||
const std::filesystem::path cacheDir = std::filesystem::path("/tmp") / "noctalia-media-art";
|
||||
const std::size_t hash = std::hash<std::string_view>{}(artUrl);
|
||||
return cacheDir / (std::to_string(hash) + ".img");
|
||||
}
|
||||
|
||||
std::string normalizeArtPath(std::string_view artUrl) {
|
||||
if (artUrl.empty()) {
|
||||
return {};
|
||||
}
|
||||
if (isRemoteArtUrl(artUrl)) {
|
||||
return {};
|
||||
}
|
||||
std::string path(artUrl);
|
||||
constexpr std::string_view prefix = "file://";
|
||||
if (path.starts_with(prefix)) {
|
||||
path.erase(0, prefix.size());
|
||||
if (path.starts_with("localhost/")) {
|
||||
path.erase(0, std::string_view("localhost").size());
|
||||
} else if (!path.empty() && path.front() != '/') {
|
||||
const auto firstSlash = path.find('/');
|
||||
path = firstSlash == std::string::npos ? std::string{} : path.substr(firstSlash);
|
||||
std::string joinArtists(const std::vector<std::string>& artists) {
|
||||
if (artists.empty()) {
|
||||
return {};
|
||||
}
|
||||
std::string joined = artists.front();
|
||||
for (std::size_t i = 1; i < artists.size(); ++i) {
|
||||
joined += ", ";
|
||||
joined += artists[i];
|
||||
}
|
||||
return joined;
|
||||
}
|
||||
return decodeUriComponent(path);
|
||||
}
|
||||
|
||||
std::filesystem::path artCachePath(std::string_view artUrl) {
|
||||
const std::filesystem::path cacheDir = std::filesystem::path("/tmp") / "noctalia-media-art";
|
||||
const std::size_t hash = std::hash<std::string_view>{}(artUrl);
|
||||
return cacheDir / (std::to_string(hash) + ".img");
|
||||
}
|
||||
|
||||
std::string joinArtists(const std::vector<std::string>& artists) {
|
||||
if (artists.empty()) {
|
||||
return {};
|
||||
std::string playPauseGlyph(const std::string& playbackStatus) {
|
||||
return playbackStatus == "Playing" ? "media-pause" : "media-play";
|
||||
}
|
||||
std::string joined = artists.front();
|
||||
for (std::size_t i = 1; i < artists.size(); ++i) {
|
||||
joined += ", ";
|
||||
joined += artists[i];
|
||||
}
|
||||
return joined;
|
||||
}
|
||||
|
||||
std::string playPauseGlyph(const std::string& playbackStatus) {
|
||||
return playbackStatus == "Playing" ? "media-pause" : "media-play";
|
||||
}
|
||||
std::string repeatGlyph(const std::string& loopStatus) { return loopStatus == "Track" ? "repeat-once" : "repeat"; }
|
||||
|
||||
std::string repeatGlyph(const std::string& loopStatus) { return loopStatus == "Track" ? "repeat-once" : "repeat"; }
|
||||
|
||||
ButtonVariant toggleVariant(bool active) { return active ? ButtonVariant::Accent : ButtonVariant::Ghost; }
|
||||
ButtonVariant toggleVariant(bool active) { return active ? ButtonVariant::Accent : ButtonVariant::Ghost; }
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -607,8 +607,9 @@ void MediaTab::doLayout(Renderer& renderer, float contentWidth, float bodyHeight
|
||||
}
|
||||
|
||||
if (m_playerMenu != nullptr && m_nowCard != nullptr) {
|
||||
const float menuWidth = std::clamp(Style::controlHeightLg * 6.0f * scale, Style::controlHeightLg * 4.2f * scale,
|
||||
std::max(1.0f, m_nowCard->width() - (m_nowCard->paddingLeft() + m_nowCard->paddingRight())));
|
||||
const float menuWidth =
|
||||
std::clamp(Style::controlHeightLg * 6.0f * scale, Style::controlHeightLg * 4.2f * scale,
|
||||
std::max(1.0f, m_nowCard->width() - (m_nowCard->paddingLeft() + m_nowCard->paddingRight())));
|
||||
m_playerMenu->setMenuWidth(menuWidth);
|
||||
m_playerMenu->setVisible(m_playerMenuOpen);
|
||||
if (m_playerMenuOpen) {
|
||||
@@ -621,8 +622,8 @@ void MediaTab::doLayout(Renderer& renderer, float contentWidth, float bodyHeight
|
||||
Node::absolutePosition(m_rootLayout, rootAbsX, rootAbsY);
|
||||
const float localNowX = nowAbsX - rootAbsX;
|
||||
const float localNowY = nowAbsY - rootAbsY;
|
||||
const float x = localNowX + std::max(m_nowCard->paddingLeft(),
|
||||
m_nowCard->width() - m_nowCard->paddingRight() - menuWidth);
|
||||
const float x =
|
||||
localNowX + std::max(m_nowCard->paddingLeft(), m_nowCard->width() - m_nowCard->paddingRight() - menuWidth);
|
||||
const float y = localNowY + m_nowCard->paddingTop() + Style::controlHeightSm * scale + Style::spaceXs * scale;
|
||||
m_playerMenu->setPosition(x, y);
|
||||
m_playerMenu->layout(renderer);
|
||||
@@ -630,10 +631,10 @@ void MediaTab::doLayout(Renderer& renderer, float contentWidth, float bodyHeight
|
||||
}
|
||||
|
||||
if (m_visualizerBody != nullptr && m_visualizerSpectrum != nullptr) {
|
||||
const float bodyWidth =
|
||||
std::max(0.0f, m_visualizerBody->width() - (m_visualizerBody->paddingLeft() + m_visualizerBody->paddingRight()));
|
||||
const float bodyHeightAvail =
|
||||
std::max(0.0f, m_visualizerBody->height() - (m_visualizerBody->paddingTop() + m_visualizerBody->paddingBottom()));
|
||||
const float bodyWidth = std::max(0.0f, m_visualizerBody->width() -
|
||||
(m_visualizerBody->paddingLeft() + m_visualizerBody->paddingRight()));
|
||||
const float bodyHeightAvail = std::max(
|
||||
0.0f, m_visualizerBody->height() - (m_visualizerBody->paddingTop() + m_visualizerBody->paddingBottom()));
|
||||
const float spectrumWidth = std::max(1.0f, bodyWidth);
|
||||
const float spectrumHeight = std::max(1.0f, bodyHeightAvail);
|
||||
m_visualizerSpectrum->setSize(spectrumWidth, spectrumHeight);
|
||||
@@ -759,8 +760,7 @@ void MediaTab::refresh(Renderer& renderer) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!active.has_value() && m_lastActiveSnapshot.has_value() &&
|
||||
now - m_lastActiveSeenAt <= kNoActivePlayerGrace) {
|
||||
if (!active.has_value() && m_lastActiveSnapshot.has_value() && now - m_lastActiveSeenAt <= kNoActivePlayerGrace) {
|
||||
// Keep last player briefly to hide transient MPRIS discovery gaps.
|
||||
active = m_lastActiveSnapshot;
|
||||
}
|
||||
@@ -796,8 +796,8 @@ void MediaTab::refresh(Renderer& renderer) {
|
||||
}
|
||||
}
|
||||
|
||||
if (m_trackTitle == nullptr || m_trackArtist == nullptr || m_progressSlider == nullptr || m_playPauseButton == nullptr ||
|
||||
m_repeatButton == nullptr || m_shuffleButton == nullptr) {
|
||||
if (m_trackTitle == nullptr || m_trackArtist == nullptr || m_progressSlider == nullptr ||
|
||||
m_playPauseButton == nullptr || m_repeatButton == nullptr || m_shuffleButton == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -854,8 +854,7 @@ void MediaTab::refresh(Renderer& renderer) {
|
||||
std::error_code ec;
|
||||
if (std::filesystem::exists(cached, ec) && std::filesystem::file_size(cached, ec) > 0) {
|
||||
artPath = cached.string();
|
||||
} else if (m_httpClient != nullptr &&
|
||||
m_pendingArtDownloads.find(resolvedArtUrl) == m_pendingArtDownloads.end()) {
|
||||
} else if (m_httpClient != nullptr && m_pendingArtDownloads.find(resolvedArtUrl) == m_pendingArtDownloads.end()) {
|
||||
std::filesystem::create_directories(cached.parent_path(), ec);
|
||||
m_pendingArtDownloads.insert(resolvedArtUrl);
|
||||
m_httpClient->download(resolvedArtUrl, cached, [this, url = resolvedArtUrl](bool success) {
|
||||
|
||||
@@ -23,169 +23,169 @@ using namespace control_center;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kRowMinHeight = Style::controlHeightLg;
|
||||
constexpr float kRowMinHeight = Style::controlHeightLg;
|
||||
|
||||
std::string currentTitle(const NetworkState& s) {
|
||||
if (s.kind == NetworkConnectivity::Wireless && s.connected && !s.ssid.empty()) {
|
||||
return s.ssid;
|
||||
}
|
||||
if (s.kind == NetworkConnectivity::Wired && s.connected) {
|
||||
return s.interfaceName.empty() ? std::string("Wired connection") : s.interfaceName;
|
||||
}
|
||||
return "Not connected";
|
||||
}
|
||||
|
||||
std::string currentDetail(const NetworkState& s) {
|
||||
if (!s.connected) {
|
||||
return s.wirelessEnabled ? "Wi-Fi is on" : "Wi-Fi is off";
|
||||
}
|
||||
std::string out;
|
||||
if (!s.interfaceName.empty()) {
|
||||
out = s.interfaceName;
|
||||
}
|
||||
if (!s.ipv4.empty()) {
|
||||
if (!out.empty()) {
|
||||
out += " • ";
|
||||
std::string currentTitle(const NetworkState& s) {
|
||||
if (s.kind == NetworkConnectivity::Wireless && s.connected && !s.ssid.empty()) {
|
||||
return s.ssid;
|
||||
}
|
||||
out += s.ipv4;
|
||||
}
|
||||
if (s.kind == NetworkConnectivity::Wireless && s.signalStrength > 0) {
|
||||
if (!out.empty()) {
|
||||
out += " • ";
|
||||
if (s.kind == NetworkConnectivity::Wired && s.connected) {
|
||||
return s.interfaceName.empty() ? std::string("Wired connection") : s.interfaceName;
|
||||
}
|
||||
out += std::to_string(static_cast<int>(s.signalStrength)) + "%";
|
||||
return "Not connected";
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
class AccessPointRow : public Flex {
|
||||
public:
|
||||
AccessPointRow(AccessPointInfo ap, bool saved, std::function<void(const AccessPointInfo&)> onActivate,
|
||||
std::function<void(const AccessPointInfo&)> onForget)
|
||||
: m_ap(std::move(ap)), m_onActivate(std::move(onActivate)), m_onForget(std::move(onForget)) {
|
||||
setDirection(FlexDirection::Horizontal);
|
||||
setAlign(FlexAlign::Center);
|
||||
setGap(Style::spaceSm);
|
||||
setPadding(Style::spaceSm, Style::spaceMd);
|
||||
setMinHeight(kRowMinHeight);
|
||||
setRadius(Style::radiusMd);
|
||||
setBackground(roleColor(ColorRole::Surface));
|
||||
setBorderWidth(0.0f);
|
||||
|
||||
auto signalGlyph = std::make_unique<Glyph>();
|
||||
signalGlyph->setGlyph(NetworkTab_rowGlyph(m_ap));
|
||||
signalGlyph->setGlyphSize(Style::fontSizeBody);
|
||||
signalGlyph->setColor(roleColor(ColorRole::OnSurface));
|
||||
addChild(std::move(signalGlyph));
|
||||
|
||||
auto ssid = std::make_unique<Label>();
|
||||
ssid->setText(m_ap.ssid);
|
||||
ssid->setBold(m_ap.active);
|
||||
ssid->setFontSize(Style::fontSizeBody);
|
||||
ssid->setColor(roleColor(ColorRole::OnSurface));
|
||||
ssid->setFlexGrow(1.0f);
|
||||
m_title = ssid.get();
|
||||
addChild(std::move(ssid));
|
||||
|
||||
if (m_ap.secured) {
|
||||
auto lock = std::make_unique<Glyph>();
|
||||
lock->setGlyph("lock");
|
||||
lock->setGlyphSize(Style::fontSizeCaption);
|
||||
lock->setColor(roleColor(ColorRole::OnSurfaceVariant));
|
||||
addChild(std::move(lock));
|
||||
std::string currentDetail(const NetworkState& s) {
|
||||
if (!s.connected) {
|
||||
return s.wirelessEnabled ? "Wi-Fi is on" : "Wi-Fi is off";
|
||||
}
|
||||
std::string out;
|
||||
if (!s.interfaceName.empty()) {
|
||||
out = s.interfaceName;
|
||||
}
|
||||
if (!s.ipv4.empty()) {
|
||||
if (!out.empty()) {
|
||||
out += " • ";
|
||||
}
|
||||
out += s.ipv4;
|
||||
}
|
||||
if (s.kind == NetworkConnectivity::Wireless && s.signalStrength > 0) {
|
||||
if (!out.empty()) {
|
||||
out += " • ";
|
||||
}
|
||||
out += std::to_string(static_cast<int>(s.signalStrength)) + "%";
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
auto strength = std::make_unique<Label>();
|
||||
strength->setText(std::to_string(static_cast<int>(m_ap.strength)) + "%");
|
||||
strength->setCaptionStyle();
|
||||
strength->setFontSize(Style::fontSizeCaption);
|
||||
strength->setColor(roleColor(ColorRole::OnSurfaceVariant));
|
||||
addChild(std::move(strength));
|
||||
class AccessPointRow : public Flex {
|
||||
public:
|
||||
AccessPointRow(AccessPointInfo ap, bool saved, std::function<void(const AccessPointInfo&)> onActivate,
|
||||
std::function<void(const AccessPointInfo&)> onForget)
|
||||
: m_ap(std::move(ap)), m_onActivate(std::move(onActivate)), m_onForget(std::move(onForget)) {
|
||||
setDirection(FlexDirection::Horizontal);
|
||||
setAlign(FlexAlign::Center);
|
||||
setGap(Style::spaceSm);
|
||||
setPadding(Style::spaceSm, Style::spaceMd);
|
||||
setMinHeight(kRowMinHeight);
|
||||
setRadius(Style::radiusMd);
|
||||
setBackground(roleColor(ColorRole::Surface));
|
||||
setBorderWidth(0.0f);
|
||||
|
||||
if (saved) {
|
||||
auto forget = std::make_unique<Button>();
|
||||
forget->setVariant(ButtonVariant::Ghost);
|
||||
forget->setText("Forget");
|
||||
forget->setOnClick([this]() {
|
||||
if (m_onForget) {
|
||||
m_onForget(m_ap);
|
||||
auto signalGlyph = std::make_unique<Glyph>();
|
||||
signalGlyph->setGlyph(NetworkTab_rowGlyph(m_ap));
|
||||
signalGlyph->setGlyphSize(Style::fontSizeBody);
|
||||
signalGlyph->setColor(roleColor(ColorRole::OnSurface));
|
||||
addChild(std::move(signalGlyph));
|
||||
|
||||
auto ssid = std::make_unique<Label>();
|
||||
ssid->setText(m_ap.ssid);
|
||||
ssid->setBold(m_ap.active);
|
||||
ssid->setFontSize(Style::fontSizeBody);
|
||||
ssid->setColor(roleColor(ColorRole::OnSurface));
|
||||
ssid->setFlexGrow(1.0f);
|
||||
m_title = ssid.get();
|
||||
addChild(std::move(ssid));
|
||||
|
||||
if (m_ap.secured) {
|
||||
auto lock = std::make_unique<Glyph>();
|
||||
lock->setGlyph("lock");
|
||||
lock->setGlyphSize(Style::fontSizeCaption);
|
||||
lock->setColor(roleColor(ColorRole::OnSurfaceVariant));
|
||||
addChild(std::move(lock));
|
||||
}
|
||||
|
||||
auto strength = std::make_unique<Label>();
|
||||
strength->setText(std::to_string(static_cast<int>(m_ap.strength)) + "%");
|
||||
strength->setCaptionStyle();
|
||||
strength->setFontSize(Style::fontSizeCaption);
|
||||
strength->setColor(roleColor(ColorRole::OnSurfaceVariant));
|
||||
addChild(std::move(strength));
|
||||
|
||||
if (saved) {
|
||||
auto forget = std::make_unique<Button>();
|
||||
forget->setVariant(ButtonVariant::Ghost);
|
||||
forget->setText("Forget");
|
||||
forget->setOnClick([this]() {
|
||||
if (m_onForget) {
|
||||
m_onForget(m_ap);
|
||||
}
|
||||
});
|
||||
m_forgetButton = static_cast<Button*>(addChild(std::move(forget)));
|
||||
}
|
||||
|
||||
auto area = std::make_unique<InputArea>();
|
||||
area->setPropagateEvents(true);
|
||||
area->setOnEnter([this](const InputArea::PointerData& /*data*/) { applyState(); });
|
||||
area->setOnLeave([this]() { applyState(); });
|
||||
area->setOnPress([this](const InputArea::PointerData& /*data*/) { applyState(); });
|
||||
area->setOnClick([this](const InputArea::PointerData& /*data*/) {
|
||||
if (m_onActivate) {
|
||||
m_onActivate(m_ap);
|
||||
}
|
||||
});
|
||||
m_forgetButton = static_cast<Button*>(addChild(std::move(forget)));
|
||||
m_inputArea = static_cast<InputArea*>(addChild(std::move(area)));
|
||||
|
||||
applyState();
|
||||
m_paletteConn = paletteChanged().connect([this] { applyState(); });
|
||||
}
|
||||
|
||||
auto area = std::make_unique<InputArea>();
|
||||
area->setPropagateEvents(true);
|
||||
area->setOnEnter([this](const InputArea::PointerData& /*data*/) { applyState(); });
|
||||
area->setOnLeave([this]() { applyState(); });
|
||||
area->setOnPress([this](const InputArea::PointerData& /*data*/) { applyState(); });
|
||||
area->setOnClick([this](const InputArea::PointerData& /*data*/) {
|
||||
if (m_onActivate) {
|
||||
m_onActivate(m_ap);
|
||||
void doLayout(Renderer& renderer) override {
|
||||
if (m_inputArea == nullptr) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
m_inputArea = static_cast<InputArea*>(addChild(std::move(area)));
|
||||
|
||||
applyState();
|
||||
m_paletteConn = paletteChanged().connect([this] { applyState(); });
|
||||
}
|
||||
|
||||
void doLayout(Renderer& renderer) override {
|
||||
if (m_inputArea == nullptr) {
|
||||
return;
|
||||
m_inputArea->setVisible(false);
|
||||
Flex::doLayout(renderer);
|
||||
m_inputArea->setVisible(true);
|
||||
m_inputArea->setPosition(0.0f, 0.0f);
|
||||
m_inputArea->setSize(width(), height());
|
||||
// Carve out the forget button so its own InputArea gets clicks first.
|
||||
if (m_forgetButton != nullptr) {
|
||||
const float areaWidth = std::max(0.0f, m_forgetButton->x() - gap());
|
||||
m_inputArea->setSize(areaWidth, height());
|
||||
}
|
||||
applyState();
|
||||
}
|
||||
m_inputArea->setVisible(false);
|
||||
Flex::doLayout(renderer);
|
||||
m_inputArea->setVisible(true);
|
||||
m_inputArea->setPosition(0.0f, 0.0f);
|
||||
m_inputArea->setSize(width(), height());
|
||||
// Carve out the forget button so its own InputArea gets clicks first.
|
||||
if (m_forgetButton != nullptr) {
|
||||
const float areaWidth = std::max(0.0f, m_forgetButton->x() - gap());
|
||||
m_inputArea->setSize(areaWidth, height());
|
||||
}
|
||||
applyState();
|
||||
}
|
||||
|
||||
static const char* NetworkTab_rowGlyph(const AccessPointInfo& ap) {
|
||||
if (ap.strength >= 67) {
|
||||
return "wifi-2";
|
||||
static const char* NetworkTab_rowGlyph(const AccessPointInfo& ap) {
|
||||
if (ap.strength >= 67) {
|
||||
return "wifi-2";
|
||||
}
|
||||
if (ap.strength >= 34) {
|
||||
return "wifi-1";
|
||||
}
|
||||
return "wifi-0";
|
||||
}
|
||||
if (ap.strength >= 34) {
|
||||
return "wifi-1";
|
||||
}
|
||||
return "wifi-0";
|
||||
}
|
||||
|
||||
private:
|
||||
void applyState() {
|
||||
const bool hov = m_inputArea != nullptr && m_inputArea->hovered();
|
||||
const bool pressed = m_inputArea != nullptr && m_inputArea->pressed();
|
||||
if (pressed) {
|
||||
setBackground(roleColor(ColorRole::Primary));
|
||||
setBorderColor(roleColor(ColorRole::Primary));
|
||||
setBorderWidth(Style::borderWidth);
|
||||
private:
|
||||
void applyState() {
|
||||
const bool hov = m_inputArea != nullptr && m_inputArea->hovered();
|
||||
const bool pressed = m_inputArea != nullptr && m_inputArea->pressed();
|
||||
if (pressed) {
|
||||
setBackground(roleColor(ColorRole::Primary));
|
||||
setBorderColor(roleColor(ColorRole::Primary));
|
||||
setBorderWidth(Style::borderWidth);
|
||||
if (m_title != nullptr) {
|
||||
m_title->setColor(roleColor(ColorRole::OnPrimary));
|
||||
}
|
||||
return;
|
||||
}
|
||||
setBackground(roleColor(m_ap.active ? ColorRole::Secondary : ColorRole::Surface, m_ap.active ? 0.25f : 1.0f));
|
||||
setBorderColor(roleColor(hov ? ColorRole::Primary : ColorRole::Surface));
|
||||
setBorderWidth(hov ? Style::borderWidth : 0.0f);
|
||||
if (m_title != nullptr) {
|
||||
m_title->setColor(roleColor(ColorRole::OnPrimary));
|
||||
m_title->setColor(roleColor(ColorRole::OnSurface));
|
||||
}
|
||||
return;
|
||||
}
|
||||
setBackground(roleColor(m_ap.active ? ColorRole::Secondary : ColorRole::Surface, m_ap.active ? 0.25f : 1.0f));
|
||||
setBorderColor(roleColor(hov ? ColorRole::Primary : ColorRole::Surface));
|
||||
setBorderWidth(hov ? Style::borderWidth : 0.0f);
|
||||
if (m_title != nullptr) {
|
||||
m_title->setColor(roleColor(ColorRole::OnSurface));
|
||||
}
|
||||
}
|
||||
|
||||
AccessPointInfo m_ap;
|
||||
std::function<void(const AccessPointInfo&)> m_onActivate;
|
||||
std::function<void(const AccessPointInfo&)> m_onForget;
|
||||
Label* m_title = nullptr;
|
||||
InputArea* m_inputArea = nullptr;
|
||||
Button* m_forgetButton = nullptr;
|
||||
Signal<>::ScopedConnection m_paletteConn;
|
||||
};
|
||||
AccessPointInfo m_ap;
|
||||
std::function<void(const AccessPointInfo&)> m_onActivate;
|
||||
std::function<void(const AccessPointInfo&)> m_onForget;
|
||||
Label* m_title = nullptr;
|
||||
InputArea* m_inputArea = nullptr;
|
||||
Button* m_forgetButton = nullptr;
|
||||
Signal<>::ScopedConnection m_paletteConn;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "core/ui_phase.h"
|
||||
#include "shell/control_center/notifications_tab.h"
|
||||
|
||||
#include "core/ui_phase.h"
|
||||
#include "notification/notification_manager.h"
|
||||
#include "render/core/renderer.h"
|
||||
#include "shell/panel/panel_manager.h"
|
||||
@@ -18,58 +18,58 @@ using namespace control_center;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kNotificationListRightPadding = Style::spaceSm;
|
||||
constexpr float kNotificationActionButtonSize = Style::controlHeightSm;
|
||||
constexpr int kSummaryMaxLines = 2;
|
||||
constexpr int kBodyMaxLines = 3;
|
||||
constexpr int kExpandedMaxLines = 500;
|
||||
constexpr float kNotificationListRightPadding = Style::spaceSm;
|
||||
constexpr float kNotificationActionButtonSize = Style::controlHeightSm;
|
||||
constexpr int kSummaryMaxLines = 2;
|
||||
constexpr int kBodyMaxLines = 3;
|
||||
constexpr int kExpandedMaxLines = 500;
|
||||
|
||||
std::string statusText(const NotificationHistoryEntry& entry) {
|
||||
if (entry.active) {
|
||||
return "Active";
|
||||
}
|
||||
if (!entry.closeReason.has_value()) {
|
||||
std::string statusText(const NotificationHistoryEntry& entry) {
|
||||
if (entry.active) {
|
||||
return "Active";
|
||||
}
|
||||
if (!entry.closeReason.has_value()) {
|
||||
return "Closed";
|
||||
}
|
||||
switch (*entry.closeReason) {
|
||||
case CloseReason::Expired:
|
||||
return "Expired";
|
||||
case CloseReason::Dismissed:
|
||||
return "Dismissed";
|
||||
case CloseReason::ClosedByCall:
|
||||
return "Closed";
|
||||
}
|
||||
return "Closed";
|
||||
}
|
||||
switch (*entry.closeReason) {
|
||||
case CloseReason::Expired: return "Expired";
|
||||
case CloseReason::Dismissed: return "Dismissed";
|
||||
case CloseReason::ClosedByCall: return "Closed";
|
||||
}
|
||||
return "Closed";
|
||||
}
|
||||
|
||||
ColorRole statusColorRole(const NotificationHistoryEntry& entry) {
|
||||
if (entry.active) {
|
||||
return ColorRole::Primary;
|
||||
}
|
||||
if (entry.closeReason == CloseReason::Dismissed) {
|
||||
return ColorRole::Secondary;
|
||||
}
|
||||
return ColorRole::OnSurfaceVariant;
|
||||
}
|
||||
|
||||
void applyNotificationCardStyle(Flex& card, float scale) {
|
||||
applyOutlinedCard(card, scale);
|
||||
}
|
||||
|
||||
bool canExpandText(Renderer& renderer, std::string_view text, float fontSize, bool bold, float maxWidth,
|
||||
int collapsedMaxLines) {
|
||||
if (text.empty()) {
|
||||
return false;
|
||||
ColorRole statusColorRole(const NotificationHistoryEntry& entry) {
|
||||
if (entry.active) {
|
||||
return ColorRole::Primary;
|
||||
}
|
||||
if (entry.closeReason == CloseReason::Dismissed) {
|
||||
return ColorRole::Secondary;
|
||||
}
|
||||
return ColorRole::OnSurfaceVariant;
|
||||
}
|
||||
|
||||
const auto collapsed = renderer.measureText(text, fontSize, bold, maxWidth, collapsedMaxLines);
|
||||
const auto expanded = renderer.measureText(text, fontSize, bold, maxWidth, kExpandedMaxLines);
|
||||
const float collapsedHeight = collapsed.bottom - collapsed.top;
|
||||
const float expandedHeight = expanded.bottom - expanded.top;
|
||||
return expandedHeight > collapsedHeight + 0.5f;
|
||||
}
|
||||
void applyNotificationCardStyle(Flex& card, float scale) { applyOutlinedCard(card, scale); }
|
||||
|
||||
bool canExpandText(Renderer& renderer, std::string_view text, float fontSize, bool bold, float maxWidth,
|
||||
int collapsedMaxLines) {
|
||||
if (text.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto collapsed = renderer.measureText(text, fontSize, bold, maxWidth, collapsedMaxLines);
|
||||
const auto expanded = renderer.measureText(text, fontSize, bold, maxWidth, kExpandedMaxLines);
|
||||
const float collapsedHeight = collapsed.bottom - collapsed.top;
|
||||
const float expandedHeight = expanded.bottom - expanded.top;
|
||||
return expandedHeight > collapsedHeight + 0.5f;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NotificationsTab::NotificationsTab(NotificationManager* notifications)
|
||||
: m_notifications(notifications) {}
|
||||
NotificationsTab::NotificationsTab(NotificationManager* notifications) : m_notifications(notifications) {}
|
||||
|
||||
std::unique_ptr<Flex> NotificationsTab::create() {
|
||||
const float scale = contentScale();
|
||||
@@ -318,9 +318,8 @@ void NotificationsTab::rebuild(Renderer& renderer, float width) {
|
||||
dismiss->setMinHeight(actionButtonSize);
|
||||
dismiss->setPadding(Style::spaceXs * scale);
|
||||
dismiss->setRadius(Style::radiusMd * scale);
|
||||
dismiss->setOnClick([this, id = it->notification.id, active = it->active]() {
|
||||
removeNotificationEntry(id, active);
|
||||
});
|
||||
dismiss->setOnClick(
|
||||
[this, id = it->notification.id, active = it->active]() { removeNotificationEntry(id, active); });
|
||||
headerActions->addChild(std::move(dismiss));
|
||||
header->addChild(std::move(headerActions));
|
||||
card->addChild(std::move(header));
|
||||
|
||||
@@ -24,10 +24,10 @@ using namespace control_center;
|
||||
|
||||
namespace {
|
||||
|
||||
void styleOverviewCard(Flex& card, float scale) {
|
||||
applyOutlinedCard(card, scale);
|
||||
card.setGap(Style::spaceSm * scale);
|
||||
}
|
||||
void styleOverviewCard(Flex& card, float scale) {
|
||||
applyOutlinedCard(card, scale);
|
||||
card.setGap(Style::spaceSm * scale);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -7,35 +7,35 @@
|
||||
|
||||
namespace control_center {
|
||||
|
||||
void applyOutlinedCard(Flex& card, float scale) {
|
||||
card.setDirection(FlexDirection::Vertical);
|
||||
card.setAlign(FlexAlign::Stretch);
|
||||
card.setGap(Style::spaceSm * scale);
|
||||
card.setPadding((Style::spaceSm + Style::spaceXs) * scale, Style::spaceMd * scale);
|
||||
card.setRadius(Style::radiusXl * scale);
|
||||
card.setBackground(roleColor(ColorRole::Surface, 0.75f));
|
||||
card.setBorderWidth(Style::borderWidth);
|
||||
card.setBorderColor(roleColor(ColorRole::Outline));
|
||||
}
|
||||
void applyOutlinedCard(Flex& card, float scale) {
|
||||
card.setDirection(FlexDirection::Vertical);
|
||||
card.setAlign(FlexAlign::Stretch);
|
||||
card.setGap(Style::spaceSm * scale);
|
||||
card.setPadding((Style::spaceSm + Style::spaceXs) * scale, Style::spaceMd * scale);
|
||||
card.setRadius(Style::radiusXl * scale);
|
||||
card.setBackground(roleColor(ColorRole::Surface, 0.75f));
|
||||
card.setBorderWidth(Style::borderWidth);
|
||||
card.setBorderColor(roleColor(ColorRole::Outline));
|
||||
}
|
||||
|
||||
Label* addTitle(Flex& parent, const std::string& text, float scale) {
|
||||
auto label = std::make_unique<Label>();
|
||||
label->setText(text);
|
||||
label->setBold(true);
|
||||
label->setFontSize(Style::fontSizeTitle * scale);
|
||||
label->setColor(roleColor(ColorRole::OnSurface));
|
||||
auto* ptr = label.get();
|
||||
parent.addChild(std::move(label));
|
||||
return ptr;
|
||||
}
|
||||
Label* addTitle(Flex& parent, const std::string& text, float scale) {
|
||||
auto label = std::make_unique<Label>();
|
||||
label->setText(text);
|
||||
label->setBold(true);
|
||||
label->setFontSize(Style::fontSizeTitle * scale);
|
||||
label->setColor(roleColor(ColorRole::OnSurface));
|
||||
auto* ptr = label.get();
|
||||
parent.addChild(std::move(label));
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void addBody(Flex& parent, const std::string& text, float scale) {
|
||||
auto label = std::make_unique<Label>();
|
||||
label->setText(text);
|
||||
label->setFontSize(Style::fontSizeBody * scale);
|
||||
label->setColor(roleColor(ColorRole::OnSurfaceVariant));
|
||||
parent.addChild(std::move(label));
|
||||
}
|
||||
void addBody(Flex& parent, const std::string& text, float scale) {
|
||||
auto label = std::make_unique<Label>();
|
||||
label->setText(text);
|
||||
label->setFontSize(Style::fontSizeBody * scale);
|
||||
label->setColor(roleColor(ColorRole::OnSurfaceVariant));
|
||||
parent.addChild(std::move(label));
|
||||
}
|
||||
|
||||
} // namespace control_center
|
||||
|
||||
|
||||
@@ -285,8 +285,7 @@ void WeatherTab::doLayout(Renderer& renderer, float contentWidth, float bodyHeig
|
||||
if (m_currentCard != nullptr && m_currentGlyph != nullptr) {
|
||||
const float cardInnerWidth =
|
||||
std::max(0.0f, m_currentCard->width() - (m_currentCard->paddingLeft() + m_currentCard->paddingRight()));
|
||||
currentTextWidth =
|
||||
std::max(1.0f, cardInnerWidth - m_currentGlyph->width() - m_currentCard->gap());
|
||||
currentTextWidth = std::max(1.0f, cardInnerWidth - m_currentGlyph->width() - m_currentCard->gap());
|
||||
}
|
||||
if (m_currentTempLabel != nullptr) {
|
||||
m_currentTempLabel->setMaxWidth(currentTextWidth);
|
||||
@@ -313,7 +312,8 @@ void WeatherTab::doLayout(Renderer& renderer, float contentWidth, float bodyHeig
|
||||
if (m_statusLabel != nullptr) {
|
||||
m_statusLabel->setMaxWidth(leftColumnWidth);
|
||||
}
|
||||
for (auto* label : {m_windLabel, m_sunriseLabel, m_sunsetLabel, m_timezoneLabel, m_longitudeLabel, m_elevationLabel}) {
|
||||
for (auto* label :
|
||||
{m_windLabel, m_sunriseLabel, m_sunsetLabel, m_timezoneLabel, m_longitudeLabel, m_elevationLabel}) {
|
||||
if (label != nullptr) {
|
||||
label->setMaxWidth(leftColumnWidth);
|
||||
}
|
||||
@@ -339,7 +339,8 @@ void WeatherTab::doLayout(Renderer& renderer, float contentWidth, float bodyHeig
|
||||
if (m_currentGlyph != nullptr && m_currentText != nullptr && m_currentCard != nullptr) {
|
||||
const float cardInnerHeight =
|
||||
std::max(0.0f, m_currentCard->height() - (m_currentCard->paddingTop() + m_currentCard->paddingBottom()));
|
||||
const float desiredGlyph = std::max(Style::controlHeightLg * 1.8f * scale, std::min(m_currentText->height(), cardInnerHeight));
|
||||
const float desiredGlyph =
|
||||
std::max(Style::controlHeightLg * 1.8f * scale, std::min(m_currentText->height(), cardInnerHeight));
|
||||
m_currentGlyph->setGlyphSize(desiredGlyph);
|
||||
}
|
||||
|
||||
@@ -351,8 +352,8 @@ void WeatherTab::doLayout(Renderer& renderer, float contentWidth, float bodyHeig
|
||||
}
|
||||
}
|
||||
if (visibleRows > 0) {
|
||||
const float detailsInnerHeight = std::max(
|
||||
0.0f, detailsTargetHeight - (m_detailsCard->paddingTop() + m_detailsCard->paddingBottom()));
|
||||
const float detailsInnerHeight =
|
||||
std::max(0.0f, detailsTargetHeight - (m_detailsCard->paddingTop() + m_detailsCard->paddingBottom()));
|
||||
const float gapsTotal = m_detailsCard->gap() * static_cast<float>(visibleRows - 1);
|
||||
const float rowHeight =
|
||||
std::max(Style::controlHeightSm * scale, (detailsInnerHeight - gapsTotal) / static_cast<float>(visibleRows));
|
||||
@@ -376,8 +377,8 @@ void WeatherTab::doLayout(Renderer& renderer, float contentWidth, float bodyHeig
|
||||
const float forecastInnerHeight = std::max(
|
||||
0.0f, m_forecastColumn->height() - (m_forecastColumn->paddingTop() + m_forecastColumn->paddingBottom()));
|
||||
const float gapsTotal = m_forecastColumn->gap() * static_cast<float>(visibleForecastDays - 1);
|
||||
const float rowHeight = std::max(Style::controlHeightLg * scale, (forecastInnerHeight - gapsTotal) /
|
||||
static_cast<float>(visibleForecastDays));
|
||||
const float rowHeight = std::max(Style::controlHeightLg * scale,
|
||||
(forecastInnerHeight - gapsTotal) / static_cast<float>(visibleForecastDays));
|
||||
|
||||
for (std::size_t i = 0; i < kDayCount; ++i) {
|
||||
if (m_dayCards[i] == nullptr) {
|
||||
@@ -566,11 +567,11 @@ void WeatherTab::sync(Renderer& renderer) {
|
||||
m_weather->displayTemperatureUnit()));
|
||||
if (m_currentHiLoLabel != nullptr) {
|
||||
if (!snapshot.forecastDays.empty()) {
|
||||
m_currentHiLoLabel->setText(
|
||||
std::format("H/L {} / {}{}", static_cast<int>(std::lround(m_weather->displayTemperature(
|
||||
snapshot.forecastDays.front().temperatureMaxC))),
|
||||
static_cast<int>(std::lround(m_weather->displayTemperature(snapshot.forecastDays.front().temperatureMinC))),
|
||||
m_weather->displayTemperatureUnit()));
|
||||
m_currentHiLoLabel->setText(std::format(
|
||||
"H/L {} / {}{}",
|
||||
static_cast<int>(std::lround(m_weather->displayTemperature(snapshot.forecastDays.front().temperatureMaxC))),
|
||||
static_cast<int>(std::lround(m_weather->displayTemperature(snapshot.forecastDays.front().temperatureMinC))),
|
||||
m_weather->displayTemperatureUnit()));
|
||||
} else {
|
||||
m_currentHiLoLabel->setText("H/L -- / --");
|
||||
}
|
||||
|
||||
@@ -7,49 +7,49 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("desktop");
|
||||
constexpr Logger kLog("desktop");
|
||||
|
||||
std::string getStringSetting(const std::unordered_map<std::string, WidgetSettingValue>& settings, const std::string& key,
|
||||
const std::string& fallback = {}) {
|
||||
const auto it = settings.find(key);
|
||||
if (it == settings.end()) {
|
||||
std::string getStringSetting(const std::unordered_map<std::string, WidgetSettingValue>& settings,
|
||||
const std::string& key, const std::string& fallback = {}) {
|
||||
const auto it = settings.find(key);
|
||||
if (it == settings.end()) {
|
||||
return fallback;
|
||||
}
|
||||
if (const auto* value = std::get_if<std::string>(&it->second)) {
|
||||
return *value;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
if (const auto* value = std::get_if<std::string>(&it->second)) {
|
||||
return *value;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
float getFloatSetting(const std::unordered_map<std::string, WidgetSettingValue>& settings, const std::string& key,
|
||||
float fallback) {
|
||||
const auto it = settings.find(key);
|
||||
if (it == settings.end()) {
|
||||
float getFloatSetting(const std::unordered_map<std::string, WidgetSettingValue>& settings, const std::string& key,
|
||||
float fallback) {
|
||||
const auto it = settings.find(key);
|
||||
if (it == settings.end()) {
|
||||
return fallback;
|
||||
}
|
||||
if (const auto* value = std::get_if<double>(&it->second)) {
|
||||
return static_cast<float>(*value);
|
||||
}
|
||||
if (const auto* value = std::get_if<std::int64_t>(&it->second)) {
|
||||
return static_cast<float>(*value);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
if (const auto* value = std::get_if<double>(&it->second)) {
|
||||
return static_cast<float>(*value);
|
||||
}
|
||||
if (const auto* value = std::get_if<std::int64_t>(&it->second)) {
|
||||
return static_cast<float>(*value);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
int getIntSetting(const std::unordered_map<std::string, WidgetSettingValue>& settings, const std::string& key,
|
||||
int fallback) {
|
||||
const auto it = settings.find(key);
|
||||
if (it == settings.end()) {
|
||||
int getIntSetting(const std::unordered_map<std::string, WidgetSettingValue>& settings, const std::string& key,
|
||||
int fallback) {
|
||||
const auto it = settings.find(key);
|
||||
if (it == settings.end()) {
|
||||
return fallback;
|
||||
}
|
||||
if (const auto* value = std::get_if<std::int64_t>(&it->second)) {
|
||||
return static_cast<int>(*value);
|
||||
}
|
||||
if (const auto* value = std::get_if<double>(&it->second)) {
|
||||
return static_cast<int>(*value);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
if (const auto* value = std::get_if<std::int64_t>(&it->second)) {
|
||||
return static_cast<int>(*value);
|
||||
}
|
||||
if (const auto* value = std::get_if<double>(&it->second)) {
|
||||
return static_cast<int>(*value);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -65,7 +65,8 @@ DesktopWidgetFactory::create(const std::string& type,
|
||||
kLog.warn("desktop widget factory: clock requires TimeService");
|
||||
return nullptr;
|
||||
}
|
||||
auto widget = std::make_unique<DesktopClockWidget>(*m_timeService, getStringSetting(settings, "format", "{:%H:%M}"));
|
||||
auto widget =
|
||||
std::make_unique<DesktopClockWidget>(*m_timeService, getStringSetting(settings, "format", "{:%H:%M}"));
|
||||
widget->setContentScale(contentScale);
|
||||
return widget;
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ class DesktopWidgetFactory {
|
||||
public:
|
||||
DesktopWidgetFactory(TimeService* timeService, PipeWireSpectrum* pipewireSpectrum);
|
||||
|
||||
[[nodiscard]] std::unique_ptr<DesktopWidget> create(const std::string& type,
|
||||
const std::unordered_map<std::string, WidgetSettingValue>& settings,
|
||||
float contentScale = 1.0f) const;
|
||||
[[nodiscard]] std::unique_ptr<DesktopWidget>
|
||||
create(const std::string& type, const std::unordered_map<std::string, WidgetSettingValue>& settings,
|
||||
float contentScale = 1.0f) const;
|
||||
|
||||
private:
|
||||
TimeService* m_timeService = nullptr;
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
#include "shell/desktop/widget_transform.h"
|
||||
#include "wayland/wayland_connection.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <charconv>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
@@ -18,170 +18,170 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("desktop");
|
||||
constexpr std::string_view kDesktopWidgetIdPrefix = "desktop-widget-";
|
||||
constexpr Logger kLog("desktop");
|
||||
constexpr std::string_view kDesktopWidgetIdPrefix = "desktop-widget-";
|
||||
|
||||
std::string stateDir() {
|
||||
const char* xdg = std::getenv("XDG_STATE_HOME");
|
||||
if (xdg != nullptr && xdg[0] != '\0') {
|
||||
return std::string(xdg) + "/noctalia";
|
||||
}
|
||||
const char* home = std::getenv("HOME");
|
||||
if (home != nullptr && home[0] != '\0') {
|
||||
return std::string(home) + "/.local/state/noctalia";
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string outputKey(const WaylandOutput& output) {
|
||||
if (!output.connectorName.empty()) {
|
||||
return output.connectorName;
|
||||
}
|
||||
return std::to_string(output.name);
|
||||
}
|
||||
|
||||
const WaylandOutput* resolveEffectiveOutput(const WaylandConnection& wayland, const std::string& requestedOutput) {
|
||||
const auto& outputs = wayland.outputs();
|
||||
const WaylandOutput* primary = nullptr;
|
||||
for (const auto& output : outputs) {
|
||||
if (!output.done || output.output == nullptr) {
|
||||
continue;
|
||||
std::string stateDir() {
|
||||
const char* xdg = std::getenv("XDG_STATE_HOME");
|
||||
if (xdg != nullptr && xdg[0] != '\0') {
|
||||
return std::string(xdg) + "/noctalia";
|
||||
}
|
||||
if (primary == nullptr) {
|
||||
primary = &output;
|
||||
const char* home = std::getenv("HOME");
|
||||
if (home != nullptr && home[0] != '\0') {
|
||||
return std::string(home) + "/.local/state/noctalia";
|
||||
}
|
||||
if (!requestedOutput.empty() && outputKey(output) == requestedOutput) {
|
||||
return &output;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string outputKey(const WaylandOutput& output) {
|
||||
if (!output.connectorName.empty()) {
|
||||
return output.connectorName;
|
||||
}
|
||||
return std::to_string(output.name);
|
||||
}
|
||||
return primary;
|
||||
}
|
||||
|
||||
float outputLogicalWidth(const WaylandOutput& output) {
|
||||
if (output.logicalWidth > 0) {
|
||||
return static_cast<float>(output.logicalWidth);
|
||||
}
|
||||
return static_cast<float>(std::max(1, output.width / std::max(1, output.scale)));
|
||||
}
|
||||
|
||||
float outputLogicalHeight(const WaylandOutput& output) {
|
||||
if (output.logicalHeight > 0) {
|
||||
return static_cast<float>(output.logicalHeight);
|
||||
}
|
||||
return static_cast<float>(std::max(1, output.height / std::max(1, output.scale)));
|
||||
}
|
||||
|
||||
bool formatShowsSeconds(const DesktopWidgetState& state) {
|
||||
if (state.type != "clock") {
|
||||
return false;
|
||||
}
|
||||
const auto it = state.settings.find("format");
|
||||
if (it == state.settings.end()) {
|
||||
return false;
|
||||
}
|
||||
const auto* format = std::get_if<std::string>(&it->second);
|
||||
if (format == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return format->find("%S") != std::string::npos || format->find("%T") != std::string::npos ||
|
||||
format->find("%X") != std::string::npos;
|
||||
}
|
||||
|
||||
float estimateIntrinsicWidth(const DesktopWidgetState& state) {
|
||||
if (state.type == "clock") {
|
||||
return formatShowsSeconds(state) ? 270.0f : 210.0f;
|
||||
}
|
||||
if (state.type == "audio_visualizer") {
|
||||
const auto it = state.settings.find("width");
|
||||
if (it != state.settings.end()) {
|
||||
if (const auto* value = std::get_if<double>(&it->second)) {
|
||||
return static_cast<float>(*value);
|
||||
const WaylandOutput* resolveEffectiveOutput(const WaylandConnection& wayland, const std::string& requestedOutput) {
|
||||
const auto& outputs = wayland.outputs();
|
||||
const WaylandOutput* primary = nullptr;
|
||||
for (const auto& output : outputs) {
|
||||
if (!output.done || output.output == nullptr) {
|
||||
continue;
|
||||
}
|
||||
if (const auto* value = std::get_if<std::int64_t>(&it->second)) {
|
||||
return static_cast<float>(*value);
|
||||
if (primary == nullptr) {
|
||||
primary = &output;
|
||||
}
|
||||
if (!requestedOutput.empty() && outputKey(output) == requestedOutput) {
|
||||
return &output;
|
||||
}
|
||||
}
|
||||
return 240.0f;
|
||||
return primary;
|
||||
}
|
||||
return 210.0f;
|
||||
}
|
||||
|
||||
float estimateIntrinsicHeight(const DesktopWidgetState& state) {
|
||||
if (state.type == "audio_visualizer") {
|
||||
const auto it = state.settings.find("height");
|
||||
if (it != state.settings.end()) {
|
||||
if (const auto* value = std::get_if<double>(&it->second)) {
|
||||
return static_cast<float>(*value);
|
||||
}
|
||||
if (const auto* value = std::get_if<std::int64_t>(&it->second)) {
|
||||
return static_cast<float>(*value);
|
||||
}
|
||||
float outputLogicalWidth(const WaylandOutput& output) {
|
||||
if (output.logicalWidth > 0) {
|
||||
return static_cast<float>(output.logicalWidth);
|
||||
}
|
||||
return 96.0f;
|
||||
return static_cast<float>(std::max(1, output.width / std::max(1, output.scale)));
|
||||
}
|
||||
return 70.0f;
|
||||
}
|
||||
|
||||
void writeSetting(toml::table& table, const std::string& key, const WidgetSettingValue& value) {
|
||||
std::visit(
|
||||
[&](const auto& concrete) {
|
||||
using T = std::decay_t<decltype(concrete)>;
|
||||
if constexpr (std::is_same_v<T, std::vector<std::string>>) {
|
||||
toml::array array;
|
||||
for (const auto& item : concrete) {
|
||||
array.push_back(item);
|
||||
}
|
||||
table.insert_or_assign(key, std::move(array));
|
||||
} else {
|
||||
table.insert_or_assign(key, concrete);
|
||||
float outputLogicalHeight(const WaylandOutput& output) {
|
||||
if (output.logicalHeight > 0) {
|
||||
return static_cast<float>(output.logicalHeight);
|
||||
}
|
||||
return static_cast<float>(std::max(1, output.height / std::max(1, output.scale)));
|
||||
}
|
||||
|
||||
bool formatShowsSeconds(const DesktopWidgetState& state) {
|
||||
if (state.type != "clock") {
|
||||
return false;
|
||||
}
|
||||
const auto it = state.settings.find("format");
|
||||
if (it == state.settings.end()) {
|
||||
return false;
|
||||
}
|
||||
const auto* format = std::get_if<std::string>(&it->second);
|
||||
if (format == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return format->find("%S") != std::string::npos || format->find("%T") != std::string::npos ||
|
||||
format->find("%X") != std::string::npos;
|
||||
}
|
||||
|
||||
float estimateIntrinsicWidth(const DesktopWidgetState& state) {
|
||||
if (state.type == "clock") {
|
||||
return formatShowsSeconds(state) ? 270.0f : 210.0f;
|
||||
}
|
||||
if (state.type == "audio_visualizer") {
|
||||
const auto it = state.settings.find("width");
|
||||
if (it != state.settings.end()) {
|
||||
if (const auto* value = std::get_if<double>(&it->second)) {
|
||||
return static_cast<float>(*value);
|
||||
}
|
||||
if (const auto* value = std::get_if<std::int64_t>(&it->second)) {
|
||||
return static_cast<float>(*value);
|
||||
}
|
||||
},
|
||||
value);
|
||||
}
|
||||
|
||||
std::optional<WidgetSettingValue> readSetting(const toml::node& node) {
|
||||
if (const auto* stringValue = node.as_string()) {
|
||||
return WidgetSettingValue{stringValue->get()};
|
||||
}
|
||||
if (const auto* intValue = node.as_integer()) {
|
||||
return WidgetSettingValue{intValue->get()};
|
||||
}
|
||||
if (const auto* floatValue = node.as_floating_point()) {
|
||||
return WidgetSettingValue{floatValue->get()};
|
||||
}
|
||||
if (const auto* boolValue = node.as_boolean()) {
|
||||
return WidgetSettingValue{boolValue->get()};
|
||||
}
|
||||
if (const auto* arrayValue = node.as_array()) {
|
||||
std::vector<std::string> strings;
|
||||
for (const auto& item : *arrayValue) {
|
||||
if (auto value = item.value<std::string>()) {
|
||||
strings.push_back(*value);
|
||||
}
|
||||
return 240.0f;
|
||||
}
|
||||
return WidgetSettingValue{std::move(strings)};
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool parseDesktopWidgetCounter(std::string_view id, std::uint64_t& value) {
|
||||
if (!id.starts_with(kDesktopWidgetIdPrefix)) {
|
||||
return false;
|
||||
return 210.0f;
|
||||
}
|
||||
|
||||
const std::string_view suffix = id.substr(kDesktopWidgetIdPrefix.size());
|
||||
if (suffix.empty()) {
|
||||
return false;
|
||||
float estimateIntrinsicHeight(const DesktopWidgetState& state) {
|
||||
if (state.type == "audio_visualizer") {
|
||||
const auto it = state.settings.find("height");
|
||||
if (it != state.settings.end()) {
|
||||
if (const auto* value = std::get_if<double>(&it->second)) {
|
||||
return static_cast<float>(*value);
|
||||
}
|
||||
if (const auto* value = std::get_if<std::int64_t>(&it->second)) {
|
||||
return static_cast<float>(*value);
|
||||
}
|
||||
}
|
||||
return 96.0f;
|
||||
}
|
||||
return 70.0f;
|
||||
}
|
||||
|
||||
value = 0;
|
||||
const auto* begin = suffix.data();
|
||||
const auto* end = suffix.data() + suffix.size();
|
||||
const auto [ptr, ec] = std::from_chars(begin, end, value, 16);
|
||||
return ec == std::errc{} && ptr == end;
|
||||
}
|
||||
void writeSetting(toml::table& table, const std::string& key, const WidgetSettingValue& value) {
|
||||
std::visit(
|
||||
[&](const auto& concrete) {
|
||||
using T = std::decay_t<decltype(concrete)>;
|
||||
if constexpr (std::is_same_v<T, std::vector<std::string>>) {
|
||||
toml::array array;
|
||||
for (const auto& item : concrete) {
|
||||
array.push_back(item);
|
||||
}
|
||||
table.insert_or_assign(key, std::move(array));
|
||||
} else {
|
||||
table.insert_or_assign(key, concrete);
|
||||
}
|
||||
},
|
||||
value);
|
||||
}
|
||||
|
||||
std::string makeDesktopWidgetId(std::uint64_t counter) { return std::format("desktop-widget-{:016x}", counter); }
|
||||
std::optional<WidgetSettingValue> readSetting(const toml::node& node) {
|
||||
if (const auto* stringValue = node.as_string()) {
|
||||
return WidgetSettingValue{stringValue->get()};
|
||||
}
|
||||
if (const auto* intValue = node.as_integer()) {
|
||||
return WidgetSettingValue{intValue->get()};
|
||||
}
|
||||
if (const auto* floatValue = node.as_floating_point()) {
|
||||
return WidgetSettingValue{floatValue->get()};
|
||||
}
|
||||
if (const auto* boolValue = node.as_boolean()) {
|
||||
return WidgetSettingValue{boolValue->get()};
|
||||
}
|
||||
if (const auto* arrayValue = node.as_array()) {
|
||||
std::vector<std::string> strings;
|
||||
for (const auto& item : *arrayValue) {
|
||||
if (auto value = item.value<std::string>()) {
|
||||
strings.push_back(*value);
|
||||
}
|
||||
}
|
||||
return WidgetSettingValue{std::move(strings)};
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool parseDesktopWidgetCounter(std::string_view id, std::uint64_t& value) {
|
||||
if (!id.starts_with(kDesktopWidgetIdPrefix)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string_view suffix = id.substr(kDesktopWidgetIdPrefix.size());
|
||||
if (suffix.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
value = 0;
|
||||
const auto* begin = suffix.data();
|
||||
const auto* end = suffix.data() + suffix.size();
|
||||
const auto [ptr, ec] = std::from_chars(begin, end, value, 16);
|
||||
return ec == std::errc{} && ptr == end;
|
||||
}
|
||||
|
||||
std::string makeDesktopWidgetId(std::uint64_t counter) { return std::format("desktop-widget-{:016x}", counter); }
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -207,7 +207,6 @@ void DesktopWidgetsController::initialize(WaylandConnection& wayland, ConfigServ
|
||||
if (m_config != nullptr) {
|
||||
m_config->addReloadCallback([this]() { applyVisibility(); });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void DesktopWidgetsController::registerIpc(IpcService& ipc) {
|
||||
|
||||
@@ -33,118 +33,118 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("desktop");
|
||||
constexpr float kToolbarY = 68.0f;
|
||||
constexpr float kSelectionStroke = 2.0f;
|
||||
constexpr float kRotatePadding = 14.0f;
|
||||
constexpr float kHandleSize = 14.0f;
|
||||
constexpr float kMinScale = 0.2f;
|
||||
constexpr float kMaxScale = 8.0f;
|
||||
constexpr float kRotationSnap = static_cast<float>(M_PI) / 12.0f;
|
||||
constexpr std::array<const char*, 2> kWidgetTypeLabels{"Clock", "Audio Visualizer"};
|
||||
constexpr std::string_view kDesktopWidgetIdPrefix = "desktop-widget-";
|
||||
constexpr Logger kLog("desktop");
|
||||
constexpr float kToolbarY = 68.0f;
|
||||
constexpr float kSelectionStroke = 2.0f;
|
||||
constexpr float kRotatePadding = 14.0f;
|
||||
constexpr float kHandleSize = 14.0f;
|
||||
constexpr float kMinScale = 0.2f;
|
||||
constexpr float kMaxScale = 8.0f;
|
||||
constexpr float kRotationSnap = static_cast<float>(M_PI) / 12.0f;
|
||||
constexpr std::array<const char*, 2> kWidgetTypeLabels{"Clock", "Audio Visualizer"};
|
||||
constexpr std::string_view kDesktopWidgetIdPrefix = "desktop-widget-";
|
||||
|
||||
std::string outputKey(const WaylandOutput& output) {
|
||||
if (!output.connectorName.empty()) {
|
||||
return output.connectorName;
|
||||
}
|
||||
return std::to_string(output.name);
|
||||
}
|
||||
|
||||
const WaylandOutput* resolveEffectiveOutput(const WaylandConnection& wayland, const std::string& requestedOutput) {
|
||||
const auto& outputs = wayland.outputs();
|
||||
const WaylandOutput* primary = nullptr;
|
||||
for (const auto& output : outputs) {
|
||||
if (!output.done || output.output == nullptr) {
|
||||
continue;
|
||||
std::string outputKey(const WaylandOutput& output) {
|
||||
if (!output.connectorName.empty()) {
|
||||
return output.connectorName;
|
||||
}
|
||||
if (primary == nullptr) {
|
||||
primary = &output;
|
||||
return std::to_string(output.name);
|
||||
}
|
||||
|
||||
const WaylandOutput* resolveEffectiveOutput(const WaylandConnection& wayland, const std::string& requestedOutput) {
|
||||
const auto& outputs = wayland.outputs();
|
||||
const WaylandOutput* primary = nullptr;
|
||||
for (const auto& output : outputs) {
|
||||
if (!output.done || output.output == nullptr) {
|
||||
continue;
|
||||
}
|
||||
if (primary == nullptr) {
|
||||
primary = &output;
|
||||
}
|
||||
if (!requestedOutput.empty() && outputKey(output) == requestedOutput) {
|
||||
return &output;
|
||||
}
|
||||
}
|
||||
if (!requestedOutput.empty() && outputKey(output) == requestedOutput) {
|
||||
return &output;
|
||||
return primary;
|
||||
}
|
||||
|
||||
float snapToGrid(float value, std::int32_t cellSize) {
|
||||
if (cellSize <= 0) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return primary;
|
||||
}
|
||||
|
||||
float snapToGrid(float value, std::int32_t cellSize) {
|
||||
if (cellSize <= 0) {
|
||||
return value;
|
||||
}
|
||||
return std::round(value / static_cast<float>(cellSize)) * static_cast<float>(cellSize);
|
||||
}
|
||||
|
||||
float normalizeAngle(float radians) {
|
||||
while (radians > static_cast<float>(M_PI)) {
|
||||
radians -= static_cast<float>(M_PI * 2.0);
|
||||
}
|
||||
while (radians < -static_cast<float>(M_PI)) {
|
||||
radians += static_cast<float>(M_PI * 2.0);
|
||||
}
|
||||
return radians;
|
||||
}
|
||||
|
||||
float outputLogicalWidth(const WaylandOutput& output) {
|
||||
if (output.logicalWidth > 0) {
|
||||
return static_cast<float>(output.logicalWidth);
|
||||
}
|
||||
return static_cast<float>(std::max(1, output.width / std::max(1, output.scale)));
|
||||
}
|
||||
|
||||
float outputLogicalHeight(const WaylandOutput& output) {
|
||||
if (output.logicalHeight > 0) {
|
||||
return static_cast<float>(output.logicalHeight);
|
||||
}
|
||||
return static_cast<float>(std::max(1, output.height / std::max(1, output.scale)));
|
||||
}
|
||||
|
||||
bool formatShowsSeconds(const DesktopWidgetState& state) {
|
||||
if (state.type != "clock") {
|
||||
return false;
|
||||
}
|
||||
const auto it = state.settings.find("format");
|
||||
if (it == state.settings.end()) {
|
||||
return false;
|
||||
}
|
||||
const auto* format = std::get_if<std::string>(&it->second);
|
||||
if (format == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return format->find("%S") != std::string::npos || format->find("%T") != std::string::npos ||
|
||||
format->find("%X") != std::string::npos;
|
||||
}
|
||||
|
||||
bool parseDesktopWidgetCounter(std::string_view id, std::uint64_t& value) {
|
||||
if (!id.starts_with(kDesktopWidgetIdPrefix)) {
|
||||
return false;
|
||||
return std::round(value / static_cast<float>(cellSize)) * static_cast<float>(cellSize);
|
||||
}
|
||||
|
||||
const std::string_view suffix = id.substr(kDesktopWidgetIdPrefix.size());
|
||||
if (suffix.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
value = 0;
|
||||
const auto* begin = suffix.data();
|
||||
const auto* end = suffix.data() + suffix.size();
|
||||
const auto [ptr, ec] = std::from_chars(begin, end, value, 16);
|
||||
return ec == std::errc{} && ptr == end;
|
||||
}
|
||||
|
||||
std::string nextDesktopWidgetId(const DesktopWidgetsSnapshot& snapshot) {
|
||||
std::uint64_t maxCounter = 0;
|
||||
for (const auto& widget : snapshot.widgets) {
|
||||
std::uint64_t counter = 0;
|
||||
if (parseDesktopWidgetCounter(widget.id, counter)) {
|
||||
maxCounter = std::max(maxCounter, counter);
|
||||
float normalizeAngle(float radians) {
|
||||
while (radians > static_cast<float>(M_PI)) {
|
||||
radians -= static_cast<float>(M_PI * 2.0);
|
||||
}
|
||||
while (radians < -static_cast<float>(M_PI)) {
|
||||
radians += static_cast<float>(M_PI * 2.0);
|
||||
}
|
||||
return radians;
|
||||
}
|
||||
|
||||
const std::uint64_t nextCounter =
|
||||
maxCounter == std::numeric_limits<std::uint64_t>::max() ? maxCounter : (maxCounter + 1);
|
||||
return std::format("desktop-widget-{:016x}", nextCounter);
|
||||
}
|
||||
float outputLogicalWidth(const WaylandOutput& output) {
|
||||
if (output.logicalWidth > 0) {
|
||||
return static_cast<float>(output.logicalWidth);
|
||||
}
|
||||
return static_cast<float>(std::max(1, output.width / std::max(1, output.scale)));
|
||||
}
|
||||
|
||||
float outputLogicalHeight(const WaylandOutput& output) {
|
||||
if (output.logicalHeight > 0) {
|
||||
return static_cast<float>(output.logicalHeight);
|
||||
}
|
||||
return static_cast<float>(std::max(1, output.height / std::max(1, output.scale)));
|
||||
}
|
||||
|
||||
bool formatShowsSeconds(const DesktopWidgetState& state) {
|
||||
if (state.type != "clock") {
|
||||
return false;
|
||||
}
|
||||
const auto it = state.settings.find("format");
|
||||
if (it == state.settings.end()) {
|
||||
return false;
|
||||
}
|
||||
const auto* format = std::get_if<std::string>(&it->second);
|
||||
if (format == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return format->find("%S") != std::string::npos || format->find("%T") != std::string::npos ||
|
||||
format->find("%X") != std::string::npos;
|
||||
}
|
||||
|
||||
bool parseDesktopWidgetCounter(std::string_view id, std::uint64_t& value) {
|
||||
if (!id.starts_with(kDesktopWidgetIdPrefix)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string_view suffix = id.substr(kDesktopWidgetIdPrefix.size());
|
||||
if (suffix.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
value = 0;
|
||||
const auto* begin = suffix.data();
|
||||
const auto* end = suffix.data() + suffix.size();
|
||||
const auto [ptr, ec] = std::from_chars(begin, end, value, 16);
|
||||
return ec == std::errc{} && ptr == end;
|
||||
}
|
||||
|
||||
std::string nextDesktopWidgetId(const DesktopWidgetsSnapshot& snapshot) {
|
||||
std::uint64_t maxCounter = 0;
|
||||
for (const auto& widget : snapshot.widgets) {
|
||||
std::uint64_t counter = 0;
|
||||
if (parseDesktopWidgetCounter(widget.id, counter)) {
|
||||
maxCounter = std::max(maxCounter, counter);
|
||||
}
|
||||
}
|
||||
|
||||
const std::uint64_t nextCounter =
|
||||
maxCounter == std::numeric_limits<std::uint64_t>::max() ? maxCounter : (maxCounter + 1);
|
||||
return std::format("desktop-widget-{:016x}", nextCounter);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -410,8 +410,8 @@ void DesktopWidgetsEditor::rebuildScene(OverlaySurface& surface) {
|
||||
view.transformNode->setRotation(widgetState.rotationRad);
|
||||
view.transformNode->setScale(widgetState.scale);
|
||||
view.transformNode->setZIndex(4);
|
||||
view.bodyArea->setOnPress([this, id = widgetState.id, w = view.intrinsicWidth, h = view.intrinsicHeight](
|
||||
const InputArea::PointerData& data) {
|
||||
view.bodyArea->setOnPress([this, id = widgetState.id, w = view.intrinsicWidth,
|
||||
h = view.intrinsicHeight](const InputArea::PointerData& data) {
|
||||
if (data.button != BTN_LEFT) {
|
||||
return;
|
||||
}
|
||||
@@ -595,12 +595,13 @@ void DesktopWidgetsEditor::updateSelectionVisuals(OverlaySurface& surface) {
|
||||
return;
|
||||
}
|
||||
|
||||
const WidgetTransformBounds bounds = computeWidgetTransformBounds(
|
||||
state->cx, state->cy, selectedIt->second.intrinsicWidth, selectedIt->second.intrinsicHeight, state->scale,
|
||||
state->rotationRad);
|
||||
const WidgetTransformBounds bounds =
|
||||
computeWidgetTransformBounds(state->cx, state->cy, selectedIt->second.intrinsicWidth,
|
||||
selectedIt->second.intrinsicHeight, state->scale, state->rotationRad);
|
||||
|
||||
surface.rotationRing->setPosition(bounds.left - kRotatePadding, bounds.top - kRotatePadding);
|
||||
surface.rotationRing->setFrameSize(bounds.aabbWidth + kRotatePadding * 2.0f, bounds.aabbHeight + kRotatePadding * 2.0f);
|
||||
surface.rotationRing->setFrameSize(bounds.aabbWidth + kRotatePadding * 2.0f,
|
||||
bounds.aabbHeight + kRotatePadding * 2.0f);
|
||||
|
||||
surface.rotateArea->setPosition(bounds.left - kRotatePadding, bounds.top - kRotatePadding);
|
||||
surface.rotateArea->setFrameSize(bounds.aabbWidth + kRotatePadding * 2.0f, bounds.aabbHeight + kRotatePadding * 2.0f);
|
||||
@@ -641,10 +642,10 @@ void DesktopWidgetsEditor::addWidget(const std::string& outputName, const std::s
|
||||
float centerX = 320.0f;
|
||||
float centerY = 240.0f;
|
||||
if (const WaylandOutput* output = resolveEffectiveOutput(*m_wayland, outputName); output != nullptr) {
|
||||
const int logicalWidth = output->logicalWidth > 0 ? output->logicalWidth
|
||||
: output->width / std::max(1, output->scale);
|
||||
const int logicalHeight = output->logicalHeight > 0 ? output->logicalHeight
|
||||
: output->height / std::max(1, output->scale);
|
||||
const int logicalWidth =
|
||||
output->logicalWidth > 0 ? output->logicalWidth : output->width / std::max(1, output->scale);
|
||||
const int logicalHeight =
|
||||
output->logicalHeight > 0 ? output->logicalHeight : output->height / std::max(1, output->scale);
|
||||
centerX = static_cast<float>(std::max(1, logicalWidth)) * 0.5f;
|
||||
centerY = static_cast<float>(std::max(1, logicalHeight)) * 0.5f;
|
||||
}
|
||||
@@ -719,8 +720,8 @@ void DesktopWidgetsEditor::updateDrag() {
|
||||
state->cy = snapToGrid(state->cy, m_snapshot.grid.cellSize);
|
||||
}
|
||||
} else if (m_drag.mode == DragMode::Rotate) {
|
||||
const float startAngle = std::atan2(m_drag.startSceneY - m_drag.initialState.cy,
|
||||
m_drag.startSceneX - m_drag.initialState.cx);
|
||||
const float startAngle =
|
||||
std::atan2(m_drag.startSceneY - m_drag.initialState.cy, m_drag.startSceneX - m_drag.initialState.cx);
|
||||
const float currentAngle =
|
||||
std::atan2(m_currentEventSceneY - m_drag.initialState.cy, m_currentEventSceneX - m_drag.initialState.cx);
|
||||
float rotation = normalizeAngle(m_drag.initialState.rotationRad + (currentAngle - startAngle));
|
||||
@@ -732,9 +733,9 @@ void DesktopWidgetsEditor::updateDrag() {
|
||||
if (shouldSnap()) {
|
||||
const float snappedCornerX = snapToGrid(m_currentEventSceneX, m_snapshot.grid.cellSize);
|
||||
const float snappedCornerY = snapToGrid(m_currentEventSceneY, m_snapshot.grid.cellSize);
|
||||
const WidgetTransformBounds baseBounds = computeWidgetTransformBounds(
|
||||
m_drag.initialState.cx, m_drag.initialState.cy, m_drag.intrinsicWidth, m_drag.intrinsicHeight, 1.0f,
|
||||
m_drag.initialState.rotationRad);
|
||||
const WidgetTransformBounds baseBounds =
|
||||
computeWidgetTransformBounds(m_drag.initialState.cx, m_drag.initialState.cy, m_drag.intrinsicWidth,
|
||||
m_drag.intrinsicHeight, 1.0f, m_drag.initialState.rotationRad);
|
||||
const float scaleX =
|
||||
std::max(0.0f, (snappedCornerX - m_drag.initialState.cx) * 2.0f / std::max(1.0f, baseBounds.aabbWidth));
|
||||
const float scaleY =
|
||||
@@ -757,10 +758,9 @@ void DesktopWidgetsEditor::updateDrag() {
|
||||
|
||||
if (m_wayland != nullptr) {
|
||||
if (const WaylandOutput* output = resolveEffectiveOutput(*m_wayland, state->outputName); output != nullptr) {
|
||||
const WidgetTransformClampResult clamped =
|
||||
clampWidgetCenterToOutput(state->cx, state->cy, m_drag.intrinsicWidth, m_drag.intrinsicHeight, state->scale,
|
||||
state->rotationRad, outputLogicalWidth(*output), outputLogicalHeight(*output),
|
||||
kDesktopWidgetMinVisibleFraction);
|
||||
const WidgetTransformClampResult clamped = clampWidgetCenterToOutput(
|
||||
state->cx, state->cy, m_drag.intrinsicWidth, m_drag.intrinsicHeight, state->scale, state->rotationRad,
|
||||
outputLogicalWidth(*output), outputLogicalHeight(*output), kDesktopWidgetMinVisibleFraction);
|
||||
state->cx = clamped.cx;
|
||||
state->cy = clamped.cy;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "render/scene/node.h"
|
||||
#include "render/animation/animation_manager.h"
|
||||
#include "render/scene/input_dispatcher.h"
|
||||
#include "render/scene/node.h"
|
||||
#include "shell/desktop/desktop_widget_factory.h"
|
||||
#include "shell/desktop/desktop_widgets_controller.h"
|
||||
#include "wayland/layer_surface.h"
|
||||
@@ -100,7 +100,8 @@ private:
|
||||
void addWidget(const std::string& outputName, const std::string& type);
|
||||
void removeSelectedWidget();
|
||||
void requestExit();
|
||||
void startDrag(DragMode mode, const std::string& widgetId, float intrinsicWidth, float intrinsicHeight, bool rebuildOnFinish);
|
||||
void startDrag(DragMode mode, const std::string& widgetId, float intrinsicWidth, float intrinsicHeight,
|
||||
bool rebuildOnFinish);
|
||||
void updateDrag();
|
||||
void finishDrag();
|
||||
[[nodiscard]] OverlaySurface* findSurface(wl_surface* surface);
|
||||
|
||||
@@ -16,54 +16,54 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Logger kLog("desktop");
|
||||
constexpr Logger kLog("desktop");
|
||||
|
||||
std::string outputKey(const WaylandOutput& output) {
|
||||
if (!output.connectorName.empty()) {
|
||||
return output.connectorName;
|
||||
}
|
||||
return std::to_string(output.name);
|
||||
}
|
||||
|
||||
float outputLogicalWidth(const WaylandOutput& output) {
|
||||
if (output.logicalWidth > 0) {
|
||||
return static_cast<float>(output.logicalWidth);
|
||||
}
|
||||
return static_cast<float>(std::max(1, output.width / std::max(1, output.scale)));
|
||||
}
|
||||
|
||||
float outputLogicalHeight(const WaylandOutput& output) {
|
||||
if (output.logicalHeight > 0) {
|
||||
return static_cast<float>(output.logicalHeight);
|
||||
}
|
||||
return static_cast<float>(std::max(1, output.height / std::max(1, output.scale)));
|
||||
}
|
||||
|
||||
const WaylandOutput* resolveEffectiveOutput(const WaylandConnection& wayland, const std::string& requestedOutput) {
|
||||
const auto& outputs = wayland.outputs();
|
||||
const WaylandOutput* primary = nullptr;
|
||||
for (const auto& output : outputs) {
|
||||
if (!output.done || output.output == nullptr) {
|
||||
continue;
|
||||
}
|
||||
if (primary == nullptr) {
|
||||
primary = &output;
|
||||
}
|
||||
if (!requestedOutput.empty() && outputKey(output) == requestedOutput) {
|
||||
return &output;
|
||||
std::string outputKey(const WaylandOutput& output) {
|
||||
if (!output.connectorName.empty()) {
|
||||
return output.connectorName;
|
||||
}
|
||||
return std::to_string(output.name);
|
||||
}
|
||||
return primary;
|
||||
}
|
||||
|
||||
DesktopWidgetState* findStateById(DesktopWidgetsSnapshot& snapshot, const std::string& id) {
|
||||
for (auto& widget : snapshot.widgets) {
|
||||
if (widget.id == id) {
|
||||
return &widget;
|
||||
float outputLogicalWidth(const WaylandOutput& output) {
|
||||
if (output.logicalWidth > 0) {
|
||||
return static_cast<float>(output.logicalWidth);
|
||||
}
|
||||
return static_cast<float>(std::max(1, output.width / std::max(1, output.scale)));
|
||||
}
|
||||
|
||||
float outputLogicalHeight(const WaylandOutput& output) {
|
||||
if (output.logicalHeight > 0) {
|
||||
return static_cast<float>(output.logicalHeight);
|
||||
}
|
||||
return static_cast<float>(std::max(1, output.height / std::max(1, output.scale)));
|
||||
}
|
||||
|
||||
const WaylandOutput* resolveEffectiveOutput(const WaylandConnection& wayland, const std::string& requestedOutput) {
|
||||
const auto& outputs = wayland.outputs();
|
||||
const WaylandOutput* primary = nullptr;
|
||||
for (const auto& output : outputs) {
|
||||
if (!output.done || output.output == nullptr) {
|
||||
continue;
|
||||
}
|
||||
if (primary == nullptr) {
|
||||
primary = &output;
|
||||
}
|
||||
if (!requestedOutput.empty() && outputKey(output) == requestedOutput) {
|
||||
return &output;
|
||||
}
|
||||
}
|
||||
return primary;
|
||||
}
|
||||
|
||||
DesktopWidgetState* findStateById(DesktopWidgetsSnapshot& snapshot, const std::string& id) {
|
||||
for (auto& widget : snapshot.widgets) {
|
||||
if (widget.id == id) {
|
||||
return &widget;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -148,9 +148,8 @@ void DesktopWidgetsHost::syncInstances() {
|
||||
return;
|
||||
}
|
||||
|
||||
std::erase_if(m_instances, [this](const auto& instance) {
|
||||
return findStateById(m_snapshot, instance->state.id) == nullptr;
|
||||
});
|
||||
std::erase_if(m_instances,
|
||||
[this](const auto& instance) { return findStateById(m_snapshot, instance->state.id) == nullptr; });
|
||||
|
||||
for (const auto& state : m_snapshot.widgets) {
|
||||
const WaylandOutput* output = resolveEffectiveOutput(*m_wayland, state.outputName);
|
||||
@@ -165,9 +164,9 @@ void DesktopWidgetsHost::syncInstances() {
|
||||
}
|
||||
|
||||
const std::string effectiveOutputName = outputKey(*output);
|
||||
const bool widgetDefinitionChanged =
|
||||
existing->state.type != state.type || existing->state.settings != state.settings ||
|
||||
existing->effectiveOutputName != effectiveOutputName;
|
||||
const bool widgetDefinitionChanged = existing->state.type != state.type ||
|
||||
existing->state.settings != state.settings ||
|
||||
existing->effectiveOutputName != effectiveOutputName;
|
||||
|
||||
if (widgetDefinitionChanged) {
|
||||
std::erase_if(m_instances, [&state](const auto& instance) { return instance->state.id == state.id; });
|
||||
@@ -189,8 +188,8 @@ void DesktopWidgetsHost::createInstance(const DesktopWidgetState& state, const W
|
||||
return;
|
||||
}
|
||||
|
||||
auto widget = m_factory->create(state.type, state.settings,
|
||||
m_config != nullptr ? m_config->config().shell.uiScale : 1.0f);
|
||||
auto widget =
|
||||
m_factory->create(state.type, state.settings, m_config != nullptr ? m_config->config().shell.uiScale : 1.0f);
|
||||
if (widget == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -240,8 +239,9 @@ void DesktopWidgetsHost::createInstance(const DesktopWidgetState& state, const W
|
||||
|
||||
instance->surface->setConfigureCallback(
|
||||
[rawInstance](std::uint32_t /*width*/, std::uint32_t /*height*/) { rawInstance->surface->requestLayout(); });
|
||||
instance->surface->setPrepareFrameCallback(
|
||||
[this, rawInstance](bool needsUpdate, bool needsLayout) { prepareFrame(*rawInstance, needsUpdate, needsLayout); });
|
||||
instance->surface->setPrepareFrameCallback([this, rawInstance](bool needsUpdate, bool needsLayout) {
|
||||
prepareFrame(*rawInstance, needsUpdate, needsLayout);
|
||||
});
|
||||
instance->surface->setFrameTickCallback([this, rawInstance](float deltaMs) {
|
||||
if (rawInstance->widget == nullptr || m_renderContext == nullptr) {
|
||||
return;
|
||||
@@ -292,7 +292,8 @@ void DesktopWidgetsHost::prepareFrame(DesktopWidgetInstance& instance, bool need
|
||||
}
|
||||
|
||||
if (m_wayland != nullptr) {
|
||||
if (const WaylandOutput* output = resolveEffectiveOutput(*m_wayland, instance.state.outputName); output != nullptr) {
|
||||
if (const WaylandOutput* output = resolveEffectiveOutput(*m_wayland, instance.state.outputName);
|
||||
output != nullptr) {
|
||||
const WidgetTransformClampResult clamped = clampWidgetCenterToOutput(
|
||||
instance.state.cx, instance.state.cy, instance.intrinsicWidth, instance.intrinsicHeight, instance.state.scale,
|
||||
instance.state.rotationRad, outputLogicalWidth(*output), outputLogicalHeight(*output),
|
||||
@@ -302,9 +303,9 @@ void DesktopWidgetsHost::prepareFrame(DesktopWidgetInstance& instance, bool need
|
||||
}
|
||||
}
|
||||
|
||||
const WidgetTransformSurfaceGeometry geometry = computeWidgetSurfaceGeometry(
|
||||
instance.state.cx, instance.state.cy, instance.intrinsicWidth, instance.intrinsicHeight, instance.state.scale,
|
||||
instance.state.rotationRad);
|
||||
const WidgetTransformSurfaceGeometry geometry =
|
||||
computeWidgetSurfaceGeometry(instance.state.cx, instance.state.cy, instance.intrinsicWidth,
|
||||
instance.intrinsicHeight, instance.state.scale, instance.state.rotationRad);
|
||||
|
||||
if (instance.surface->width() != geometry.surfaceWidth || instance.surface->height() != geometry.surfaceHeight) {
|
||||
instance.surface->requestSize(geometry.surfaceWidth, geometry.surfaceHeight);
|
||||
@@ -317,9 +318,9 @@ void DesktopWidgetsHost::prepareFrame(DesktopWidgetInstance& instance, bool need
|
||||
}
|
||||
if (instance.transformNode != nullptr) {
|
||||
instance.transformNode->setFrameSize(instance.intrinsicWidth, instance.intrinsicHeight);
|
||||
instance.transformNode->setPosition((static_cast<float>(instance.surface->width()) - instance.intrinsicWidth) * 0.5f,
|
||||
(static_cast<float>(instance.surface->height()) - instance.intrinsicHeight) *
|
||||
0.5f);
|
||||
instance.transformNode->setPosition(
|
||||
(static_cast<float>(instance.surface->width()) - instance.intrinsicWidth) * 0.5f,
|
||||
(static_cast<float>(instance.surface->height()) - instance.intrinsicHeight) * 0.5f);
|
||||
instance.transformNode->setRotation(instance.state.rotationRad);
|
||||
instance.transformNode->setScale(instance.state.scale);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "render/scene/node.h"
|
||||
#include "render/animation/animation_manager.h"
|
||||
#include "render/scene/node.h"
|
||||
#include "shell/desktop/desktop_widget_factory.h"
|
||||
#include "shell/desktop/desktop_widgets_controller.h"
|
||||
#include "wayland/layer_surface.h"
|
||||
|
||||
@@ -51,10 +51,8 @@ inline WidgetTransformSurfaceGeometry computeWidgetSurfaceGeometry(float cx, flo
|
||||
WidgetTransformSurfaceGeometry geometry;
|
||||
geometry.surfaceWidth = std::max<std::uint32_t>(1, static_cast<std::uint32_t>(std::ceil(bounds.aabbWidth)));
|
||||
geometry.surfaceHeight = std::max<std::uint32_t>(1, static_cast<std::uint32_t>(std::ceil(bounds.aabbHeight)));
|
||||
geometry.marginLeft =
|
||||
static_cast<std::int32_t>(std::lround(cx - static_cast<float>(geometry.surfaceWidth) * 0.5f));
|
||||
geometry.marginTop =
|
||||
static_cast<std::int32_t>(std::lround(cy - static_cast<float>(geometry.surfaceHeight) * 0.5f));
|
||||
geometry.marginLeft = static_cast<std::int32_t>(std::lround(cx - static_cast<float>(geometry.surfaceWidth) * 0.5f));
|
||||
geometry.marginTop = static_cast<std::int32_t>(std::lround(cy - static_cast<float>(geometry.surfaceHeight) * 0.5f));
|
||||
return geometry;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
|
||||
DesktopAudioVisualizerWidget::DesktopAudioVisualizerWidget(PipeWireSpectrum* spectrum, float width, float height,
|
||||
int bands)
|
||||
: m_spectrum(spectrum), m_width(std::max(1.0f, width)), m_height(std::max(1.0f, height)), m_bands(std::max(1, bands)) {}
|
||||
: m_spectrum(spectrum), m_width(std::max(1.0f, width)), m_height(std::max(1.0f, height)),
|
||||
m_bands(std::max(1, bands)) {}
|
||||
|
||||
DesktopAudioVisualizerWidget::~DesktopAudioVisualizerWidget() {
|
||||
if (m_spectrum != nullptr && m_listenerId != 0) {
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
|
||||
namespace {
|
||||
|
||||
bool formatShowsSeconds(const std::string& format) {
|
||||
return format.find("%S") != std::string::npos || format.find("%T") != std::string::npos ||
|
||||
format.find("%X") != std::string::npos;
|
||||
}
|
||||
bool formatShowsSeconds(const std::string& format) {
|
||||
return format.find("%S") != std::string::npos || format.find("%T") != std::string::npos ||
|
||||
format.find("%X") != std::string::npos;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
+348
-342
File diff suppressed because it is too large
Load Diff
@@ -109,20 +109,18 @@ private:
|
||||
void launchEntry(const DesktopEntry& entry);
|
||||
void launchAction(const DesktopAction& action);
|
||||
void handleItemClick(DockInstance& instance, DockItemView& item);
|
||||
void openWindowPicker(DockInstance& instance, DockItemView& item,
|
||||
std::vector<ToplevelInfo> windows);
|
||||
void openWindowPicker(DockInstance& instance, DockItemView& item, std::vector<ToplevelInfo> windows);
|
||||
void closeWindowPicker();
|
||||
void openItemMenu(DockInstance& instance, DockItemView& item);
|
||||
void closeItemMenu();
|
||||
void startHideFadeOut(DockInstance& inst);
|
||||
|
||||
[[nodiscard]] bool matchesActiveApp(const DockItemView& item, std::string_view activeAppIdLower) const;
|
||||
[[nodiscard]] bool matchesRunningApp(const DockItemView& item,
|
||||
const std::vector<std::string>& runningLower) const;
|
||||
[[nodiscard]] bool matchesRunningApp(const DockItemView& item, const std::vector<std::string>& runningLower) const;
|
||||
|
||||
// Geometry helpers
|
||||
[[nodiscard]] std::int32_t dockContentSize(std::size_t itemCount) const; // item row length (main axis)
|
||||
[[nodiscard]] std::int32_t dockThickness() const; // cross-axis (includes icon, cell padding, dock padding)
|
||||
[[nodiscard]] std::int32_t dockThickness() const; // cross-axis (includes icon, cell padding, dock padding)
|
||||
[[nodiscard]] bool isVertical() const;
|
||||
|
||||
// Generic popup (window picker and item context menu).
|
||||
@@ -151,6 +149,6 @@ private:
|
||||
std::unordered_map<wl_surface*, DockInstance*> m_surfaceMap;
|
||||
DockInstance* m_hoveredInstance = nullptr;
|
||||
DockInstance* m_popupOwnerInstance = nullptr; // instance that owns the current open popup
|
||||
std::unique_ptr<DockPopup> m_windowMenu; // left-click multi-window picker
|
||||
std::unique_ptr<DockPopup> m_itemMenu; // right-click context menu
|
||||
std::unique_ptr<DockPopup> m_windowMenu; // left-click multi-window picker
|
||||
std::unique_ptr<DockPopup> m_itemMenu; // right-click context menu
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "shell/launcher/launcher_panel.h"
|
||||
|
||||
#include "core/deferred_call.h"
|
||||
#include "config/config_service.h"
|
||||
#include "core/deferred_call.h"
|
||||
#include "core/ui_phase.h"
|
||||
#include "render/core/renderer.h"
|
||||
#include "render/scene/input_area.h"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user