mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
feat(launcher): add right click options
This commit is contained in:
@@ -59,6 +59,9 @@
|
|||||||
},
|
},
|
||||||
"launcher": {
|
"launcher": {
|
||||||
"search-placeholder": "Search applications...",
|
"search-placeholder": "Search applications...",
|
||||||
|
"context-menu": {
|
||||||
|
"open": "Open"
|
||||||
|
},
|
||||||
"empty": {
|
"empty": {
|
||||||
"type-to-search": "Type to search...",
|
"type-to-search": "Type to search...",
|
||||||
"no-results": "No results found"
|
"no-results": "No results found"
|
||||||
|
|||||||
@@ -336,14 +336,31 @@ bool AppProvider::activate(const LauncherResult& result) {
|
|||||||
refreshEntriesIfNeeded();
|
refreshEntriesIfNeeded();
|
||||||
|
|
||||||
for (const auto& entry : m_entries) {
|
for (const auto& entry : m_entries) {
|
||||||
if (entry.path == result.id) {
|
if (entry.path != result.id) {
|
||||||
std::string token;
|
continue;
|
||||||
if (m_wayland != nullptr && m_wayland->hasXdgActivation()) {
|
|
||||||
token = m_wayland->requestActivationToken(nullptr);
|
|
||||||
}
|
|
||||||
launchCommand(entry.exec, entry.terminal, token);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string execLine = entry.exec;
|
||||||
|
if (!result.desktopActionId.empty()) {
|
||||||
|
const DesktopAction* chosen = nullptr;
|
||||||
|
for (const auto& action : entry.actions) {
|
||||||
|
if (action.id == result.desktopActionId) {
|
||||||
|
chosen = &action;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (chosen == nullptr || chosen->exec.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
execLine = chosen->exec;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string token;
|
||||||
|
if (m_wayland != nullptr && m_wayland->hasXdgActivation()) {
|
||||||
|
token = m_wayland->requestActivationToken(nullptr);
|
||||||
|
}
|
||||||
|
launchCommand(execLine, entry.terminal, token);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ struct LauncherResult {
|
|||||||
std::string iconName;
|
std::string iconName;
|
||||||
std::string iconPath;
|
std::string iconPath;
|
||||||
std::string actionText;
|
std::string actionText;
|
||||||
|
// When launching an application via AppProvider, matches DesktopAction::id (primary Exec leaves this empty).
|
||||||
|
std::string desktopActionId;
|
||||||
double score = 0.0;
|
double score = 0.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,12 @@
|
|||||||
#include "i18n/i18n.h"
|
#include "i18n/i18n.h"
|
||||||
#include "render/core/async_texture_cache.h"
|
#include "render/core/async_texture_cache.h"
|
||||||
#include "render/core/renderer.h"
|
#include "render/core/renderer.h"
|
||||||
|
#include "render/render_context.h"
|
||||||
#include "render/scene/input_area.h"
|
#include "render/scene/input_area.h"
|
||||||
#include "render/scene/node.h"
|
#include "render/scene/node.h"
|
||||||
#include "shell/panel/panel_manager.h"
|
#include "shell/panel/panel_manager.h"
|
||||||
|
#include "system/desktop_entry.h"
|
||||||
|
#include "ui/controls/context_menu_popup.h"
|
||||||
#include "ui/controls/flex.h"
|
#include "ui/controls/flex.h"
|
||||||
#include "ui/controls/glyph.h"
|
#include "ui/controls/glyph.h"
|
||||||
#include "ui/controls/image.h"
|
#include "ui/controls/image.h"
|
||||||
@@ -19,9 +22,11 @@
|
|||||||
#include "ui/palette.h"
|
#include "ui/palette.h"
|
||||||
#include "ui/style.h"
|
#include "ui/style.h"
|
||||||
#include "util/fuzzy_match.h"
|
#include "util/fuzzy_match.h"
|
||||||
|
#include "wayland/wayland_connection.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <xkbcommon/xkbcommon-keysyms.h>
|
#include <xkbcommon/xkbcommon-keysyms.h>
|
||||||
@@ -230,12 +235,14 @@ namespace {
|
|||||||
class LauncherResultAdapter final : public VirtualGridAdapter {
|
class LauncherResultAdapter final : public VirtualGridAdapter {
|
||||||
public:
|
public:
|
||||||
using ActivateCallback = std::function<void(std::size_t)>;
|
using ActivateCallback = std::function<void(std::size_t)>;
|
||||||
|
using SecondaryActivateCallback = std::function<void(std::size_t, float, float)>;
|
||||||
|
|
||||||
LauncherResultAdapter(float scale, AsyncTextureCache* cache) : m_scale(scale), m_cache(cache) {}
|
LauncherResultAdapter(float scale, AsyncTextureCache* cache) : m_scale(scale), m_cache(cache) {}
|
||||||
|
|
||||||
void setResults(const std::vector<LauncherResult>* results) { m_results = results; }
|
void setResults(const std::vector<LauncherResult>* results) { m_results = results; }
|
||||||
void setRenderer(Renderer* renderer) { m_renderer = renderer; }
|
void setRenderer(Renderer* renderer) { m_renderer = renderer; }
|
||||||
void setOnActivate(ActivateCallback callback) { m_onActivate = std::move(callback); }
|
void setOnActivate(ActivateCallback callback) { m_onActivate = std::move(callback); }
|
||||||
|
void setOnSecondaryActivate(SecondaryActivateCallback callback) { m_onSecondaryActivate = std::move(callback); }
|
||||||
|
|
||||||
[[nodiscard]] std::size_t itemCount() const override { return m_results == nullptr ? 0u : m_results->size(); }
|
[[nodiscard]] std::size_t itemCount() const override { return m_results == nullptr ? 0u : m_results->size(); }
|
||||||
|
|
||||||
@@ -257,12 +264,19 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onSecondaryActivate(std::size_t index, float anchorX, float anchorY) override {
|
||||||
|
if (m_onSecondaryActivate) {
|
||||||
|
m_onSecondaryActivate(index, anchorX, anchorY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
float m_scale;
|
float m_scale;
|
||||||
AsyncTextureCache* m_cache = nullptr;
|
AsyncTextureCache* m_cache = nullptr;
|
||||||
Renderer* m_renderer = nullptr;
|
Renderer* m_renderer = nullptr;
|
||||||
const std::vector<LauncherResult>* m_results = nullptr;
|
const std::vector<LauncherResult>* m_results = nullptr;
|
||||||
ActivateCallback m_onActivate;
|
ActivateCallback m_onActivate;
|
||||||
|
SecondaryActivateCallback m_onSecondaryActivate;
|
||||||
};
|
};
|
||||||
|
|
||||||
LauncherPanel::LauncherPanel(ConfigService* config, AsyncTextureCache* asyncTextures)
|
LauncherPanel::LauncherPanel(ConfigService* config, AsyncTextureCache* asyncTextures)
|
||||||
@@ -308,6 +322,8 @@ void LauncherPanel::create() {
|
|||||||
m_adapter = std::make_unique<LauncherResultAdapter>(scale, m_asyncTextures);
|
m_adapter = std::make_unique<LauncherResultAdapter>(scale, m_asyncTextures);
|
||||||
m_adapter->setResults(&m_results);
|
m_adapter->setResults(&m_results);
|
||||||
m_adapter->setOnActivate([this](std::size_t index) { activateAt(index); });
|
m_adapter->setOnActivate([this](std::size_t index) { activateAt(index); });
|
||||||
|
m_adapter->setOnSecondaryActivate(
|
||||||
|
[this](std::size_t index, float ax, float ay) { openAppActionsMenu(index, ax, ay); });
|
||||||
|
|
||||||
auto grid = std::make_unique<VirtualGridView>();
|
auto grid = std::make_unique<VirtualGridView>();
|
||||||
grid->setColumns(1);
|
grid->setColumns(1);
|
||||||
@@ -370,6 +386,10 @@ void LauncherPanel::onOpen(std::string_view context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void LauncherPanel::onClose() {
|
void LauncherPanel::onClose() {
|
||||||
|
if (m_actionsMenu != nullptr && m_actionsMenu->isOpen()) {
|
||||||
|
m_actionsMenu->close();
|
||||||
|
}
|
||||||
|
|
||||||
if (m_asyncTextures != nullptr) {
|
if (m_asyncTextures != nullptr) {
|
||||||
DeferredCall::callLater([asyncTextures = m_asyncTextures]() { asyncTextures->trimUnused(0); });
|
DeferredCall::callLater([asyncTextures = m_asyncTextures]() { asyncTextures->trimUnused(0); });
|
||||||
}
|
}
|
||||||
@@ -530,6 +550,106 @@ void LauncherPanel::applyEmptyState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LauncherPanel::openAppActionsMenu(std::size_t index, float anchorX, float anchorY) {
|
||||||
|
if (index >= m_results.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const LauncherResult& base = m_results[index];
|
||||||
|
|
||||||
|
const DesktopEntry* match = nullptr;
|
||||||
|
for (const auto& e : desktopEntries()) {
|
||||||
|
if (e.path == base.id) {
|
||||||
|
match = &e;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (match == nullptr || match->actions.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WaylandConnection* wl = PanelManager::instance().wayland();
|
||||||
|
RenderContext* rc = PanelManager::instance().renderContext();
|
||||||
|
if (wl == nullptr || rc == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto parentCtx = PanelManager::instance().fallbackPopupParentContext();
|
||||||
|
if (!parentCtx.has_value()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_actionsMenu == nullptr) {
|
||||||
|
m_actionsMenu = std::make_unique<ContextMenuPopup>(*wl, *rc);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<DesktopAction> actionsCopy = match->actions;
|
||||||
|
|
||||||
|
std::vector<ContextMenuControlEntry> entries;
|
||||||
|
entries.reserve(actionsCopy.size() + 1);
|
||||||
|
entries.push_back(ContextMenuControlEntry{
|
||||||
|
.id = -1,
|
||||||
|
.label = i18n::tr("launcher.context-menu.open"),
|
||||||
|
.enabled = true,
|
||||||
|
.separator = false,
|
||||||
|
.hasSubmenu = false,
|
||||||
|
});
|
||||||
|
for (std::int32_t i = 0; i < static_cast<std::int32_t>(actionsCopy.size()); ++i) {
|
||||||
|
entries.push_back(ContextMenuControlEntry{
|
||||||
|
.id = i,
|
||||||
|
.label = actionsCopy[static_cast<std::size_t>(i)].name,
|
||||||
|
.enabled = true,
|
||||||
|
.separator = false,
|
||||||
|
.hasSubmenu = false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const float scale = contentScale();
|
||||||
|
constexpr float kMenuWidth = 240.0f;
|
||||||
|
const float menuWidth = kMenuWidth * scale;
|
||||||
|
|
||||||
|
PanelManager::instance().beginAttachedPopup(parentCtx->surface);
|
||||||
|
PanelManager::instance().setActivePopup(m_actionsMenu.get());
|
||||||
|
|
||||||
|
m_actionsMenu->setOnDismissed([parentSurface = parentCtx->surface]() {
|
||||||
|
PanelManager::instance().clearActivePopup();
|
||||||
|
PanelManager::instance().endAttachedPopup(parentSurface);
|
||||||
|
});
|
||||||
|
|
||||||
|
m_actionsMenu->setOnActivate(
|
||||||
|
[this, base, actionsCopy = std::move(actionsCopy)](const ContextMenuControlEntry& entry) {
|
||||||
|
LauncherResult result = base;
|
||||||
|
result.desktopActionId.clear();
|
||||||
|
if (entry.id >= 0 && entry.id < static_cast<std::int32_t>(actionsCopy.size())) {
|
||||||
|
result.desktopActionId = actionsCopy[static_cast<std::size_t>(entry.id)].id;
|
||||||
|
} else if (entry.id != -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& provider : m_providers) {
|
||||||
|
if (provider->name() != std::string_view(result.providerName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!provider->activate(result)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (provider->trackUsage()) {
|
||||||
|
m_usageTracker.record(provider->name(), result.id);
|
||||||
|
}
|
||||||
|
PanelManager::instance().closePanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const float inset = std::round(std::max(4.0f, Style::spaceXs * scale));
|
||||||
|
const std::int32_t ax = static_cast<std::int32_t>(std::round(anchorX - inset));
|
||||||
|
const std::int32_t ay = static_cast<std::int32_t>(std::round(anchorY - inset));
|
||||||
|
const std::int32_t aw = static_cast<std::int32_t>(std::round(inset * 2.0f));
|
||||||
|
const std::int32_t ah = static_cast<std::int32_t>(std::round(inset * 2.0f));
|
||||||
|
|
||||||
|
m_actionsMenu->open(std::move(entries), menuWidth, 12, ax, ay, std::max(1, aw), std::max(1, ah),
|
||||||
|
parentCtx->layerSurface, parentCtx->output);
|
||||||
|
}
|
||||||
|
|
||||||
void LauncherPanel::activateAt(std::size_t index) {
|
void LauncherPanel::activateAt(std::size_t index) {
|
||||||
if (index >= m_results.size()) {
|
if (index >= m_results.size()) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
class ContextMenuPopup;
|
||||||
class Flex;
|
class Flex;
|
||||||
class Glyph;
|
class Glyph;
|
||||||
class Image;
|
class Image;
|
||||||
@@ -53,6 +54,7 @@ private:
|
|||||||
void activateSelected();
|
void activateSelected();
|
||||||
bool handleKeyEvent(std::uint32_t sym, std::uint32_t modifiers);
|
bool handleKeyEvent(std::uint32_t sym, std::uint32_t modifiers);
|
||||||
void applyEmptyState();
|
void applyEmptyState();
|
||||||
|
void openAppActionsMenu(std::size_t index, float anchorX, float anchorY);
|
||||||
|
|
||||||
std::vector<std::unique_ptr<LauncherProvider>> m_providers;
|
std::vector<std::unique_ptr<LauncherProvider>> m_providers;
|
||||||
std::vector<LauncherResult> m_results;
|
std::vector<LauncherResult> m_results;
|
||||||
@@ -70,4 +72,5 @@ private:
|
|||||||
std::size_t m_selectedIndex = 0;
|
std::size_t m_selectedIndex = 0;
|
||||||
ConfigService* m_config = nullptr;
|
ConfigService* m_config = nullptr;
|
||||||
AsyncTextureCache* m_asyncTextures = nullptr;
|
AsyncTextureCache* m_asyncTextures = nullptr;
|
||||||
|
std::unique_ptr<ContextMenuPopup> m_actionsMenu;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -60,14 +60,19 @@ VirtualGridView::VirtualGridView() {
|
|||||||
|
|
||||||
auto inputArea = std::make_unique<InputArea>();
|
auto inputArea = std::make_unique<InputArea>();
|
||||||
inputArea->setZIndex(50);
|
inputArea->setZIndex(50);
|
||||||
|
inputArea->setAcceptedButtons(InputArea::buttonMask({BTN_LEFT, BTN_RIGHT}));
|
||||||
inputArea->setOnEnter([this](const InputArea::PointerData& data) { onPointerEnter(data.localX, data.localY); });
|
inputArea->setOnEnter([this](const InputArea::PointerData& data) { onPointerEnter(data.localX, data.localY); });
|
||||||
inputArea->setOnMotion([this](const InputArea::PointerData& data) { onPointerMotion(data.localX, data.localY); });
|
inputArea->setOnMotion([this](const InputArea::PointerData& data) { onPointerMotion(data.localX, data.localY); });
|
||||||
inputArea->setOnLeave([this]() { onPointerLeave(); });
|
inputArea->setOnLeave([this]() { onPointerLeave(); });
|
||||||
inputArea->setOnPress([this](const InputArea::PointerData& data) {
|
inputArea->setOnPress([this](const InputArea::PointerData& data) {
|
||||||
if (!data.pressed || data.button != BTN_LEFT) {
|
if (!data.pressed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onPointerPress(data.localX, data.localY);
|
if (data.button == BTN_LEFT) {
|
||||||
|
onPointerPress(data.localX, data.localY);
|
||||||
|
} else if (data.button == BTN_RIGHT) {
|
||||||
|
onSecondaryPointerPress(data.localX, data.localY);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
m_inputArea = static_cast<InputArea*>(m_canvas->addChild(std::move(inputArea)));
|
m_inputArea = static_cast<InputArea*>(m_canvas->addChild(std::move(inputArea)));
|
||||||
}
|
}
|
||||||
@@ -389,6 +394,22 @@ void VirtualGridView::onPointerPress(float localX, float localY) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VirtualGridView::onSecondaryPointerPress(float localX, float localY) {
|
||||||
|
const auto idx = indexAt(localX, localY);
|
||||||
|
if (!idx.has_value()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedIndex(idx);
|
||||||
|
if (m_adapter != nullptr) {
|
||||||
|
float wx = 0.0f;
|
||||||
|
float wy = 0.0f;
|
||||||
|
Node::absolutePosition(m_inputArea, wx, wy);
|
||||||
|
wx += localX;
|
||||||
|
wy += localY;
|
||||||
|
m_adapter->onSecondaryActivate(*idx, wx, wy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<std::size_t> VirtualGridView::indexAt(float localX, float localY) const noexcept {
|
std::optional<std::size_t> VirtualGridView::indexAt(float localX, float localY) const noexcept {
|
||||||
if (m_layoutColumns == 0 || m_cellWidth <= 0.0f || m_cellHeightResolved <= 0.0f || m_itemCount == 0) {
|
if (m_layoutColumns == 0 || m_cellWidth <= 0.0f || m_cellHeightResolved <= 0.0f || m_itemCount == 0) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ public:
|
|||||||
// Optional: respond to an activation gesture (click). The grid still
|
// Optional: respond to an activation gesture (click). The grid still
|
||||||
// updates its own selection state and fires onSelectionChanged.
|
// updates its own selection state and fires onSelectionChanged.
|
||||||
virtual void onActivate(std::size_t /*index*/) {}
|
virtual void onActivate(std::size_t /*index*/) {}
|
||||||
|
|
||||||
|
// Optional: secondary button press (e.g. context menu). Anchor coordinates are in the panel scene graph
|
||||||
|
// (surface-local).
|
||||||
|
virtual void onSecondaryActivate(std::size_t /*index*/, float /*anchorX*/, float /*anchorY*/) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
class VirtualGridView : public Flex {
|
class VirtualGridView : public Flex {
|
||||||
@@ -83,6 +87,7 @@ private:
|
|||||||
void onPointerMotion(float localX, float localY);
|
void onPointerMotion(float localX, float localY);
|
||||||
void onPointerLeave();
|
void onPointerLeave();
|
||||||
void onPointerPress(float localX, float localY);
|
void onPointerPress(float localX, float localY);
|
||||||
|
void onSecondaryPointerPress(float localX, float localY);
|
||||||
[[nodiscard]] std::optional<std::size_t> indexAt(float localX, float localY) const noexcept;
|
[[nodiscard]] std::optional<std::size_t> indexAt(float localX, float localY) const noexcept;
|
||||||
|
|
||||||
ScrollView* m_scroll = nullptr;
|
ScrollView* m_scroll = nullptr;
|
||||||
|
|||||||
Reference in New Issue
Block a user