mirror of
https://github.com/Chatterino/chatterino2.git
synced 2026-08-24 02:24:18 -05:00
feat(plugins): Add signal for context menus (#6961)
This PR adds the ability for plugins to add actions to context menus of
ChannelViews. They can add actions, submenus and separators.
I added the signal to the `WindowManager` to allow plugins to listen to
these events in any channel.
Here's an example plugin:
```lua
c2.windows:on_channelview_context_menu_requested(function(args)
args.menu:add_action("Testing", function()
args.channel:add_system_message("Clicked!")
end)
args.menu:insert_action(1, "First!", function()
local msg = "text='" .. args.message.message_text .. "'"
if args.message_element then
msg = msg .. " element=" .. args.message_element.type
end
if args.split then
msg = msg .. " split={channel=" .. args.split.channel:get_name() .. "}"
end
args.channel:add_system_message(msg)
end)
local sub = args.menu:add_menu("Submenu")
sub:add_action("An action", function() end)
sub:add_separator()
sub:add_action("Second one", function() end)
end)
```
Reviewed-by: Mm2PL <mm2pl+gh@kotmisia.pl>
Reviewed-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
Vendored
+25
@@ -655,11 +655,23 @@ declare namespace c2 {
|
||||
type: WindowType;
|
||||
}
|
||||
|
||||
interface ChannelViewContextMenuRequestedArgs {
|
||||
split?: Split;
|
||||
message: Message;
|
||||
message_element: MessageElement;
|
||||
channel?: Channel;
|
||||
menu: Menu;
|
||||
}
|
||||
|
||||
class WindowManager {
|
||||
main_window: Window;
|
||||
last_selected_window: Window;
|
||||
|
||||
all(): Window[];
|
||||
|
||||
on_channelview_context_menu_requested(
|
||||
cb: (args: ChannelViewContextMenuRequestedArgs) => void
|
||||
): ConnectionHandle;
|
||||
}
|
||||
|
||||
var windows: WindowManager;
|
||||
@@ -682,6 +694,19 @@ declare namespace c2 {
|
||||
to_local(): DateTime;
|
||||
to_utc(): DateTime;
|
||||
}
|
||||
|
||||
class Menu {
|
||||
add_action(text: string, cb: () => void): void;
|
||||
insert_action(
|
||||
before: string | number,
|
||||
text: string,
|
||||
cb: () => void
|
||||
): void;
|
||||
add_menu(text: string): Menu;
|
||||
insert_menu(before: string | number, text: string): Menu;
|
||||
add_separator(): void;
|
||||
insert_separator(before: string | number): void;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "chatterino.json" {
|
||||
|
||||
@@ -537,6 +537,48 @@ c2.ImageSet = {}
|
||||
function c2.ImageSet.new(image1, image2, image3) end
|
||||
-- End src/controllers/plugins/api/Images.hpp
|
||||
|
||||
-- Begin src/controllers/plugins/api/Menu.hpp
|
||||
|
||||
|
||||
---A generic menu used for context menus.
|
||||
---@class c2.Menu
|
||||
c2.Menu = {}
|
||||
|
||||
---Appends a new action to the menu.
|
||||
---@param text string
|
||||
---@param cb fun()
|
||||
function c2.Menu:add_action(text, cb) end
|
||||
|
||||
---Inserts an action named `text` before `before`. If `before` is not found,
|
||||
---the action is inserted at the end. `before` can either be a name or a
|
||||
---one-based index.
|
||||
---@param before string|integer A name or index of an action.
|
||||
---@param text string
|
||||
---@param cb fun()
|
||||
function c2.Menu:insert_action(before, text, cb) end
|
||||
|
||||
---Appends a new Menu with `title` to the menu.
|
||||
---@param title string
|
||||
---@return c2.Menu
|
||||
function c2.Menu:add_menu(title) end
|
||||
|
||||
---Inserts a new Menu named `title` before `before`. If `before` is not found,
|
||||
---the menu is inserted at the end. `before` can either be a name or a one-based
|
||||
---index.
|
||||
---@param before string|integer A name or index of an action.
|
||||
---@param title string
|
||||
function c2.Menu:insert_menu(before, title) end
|
||||
|
||||
---Appends a new separator.
|
||||
function c2.Menu:add_separator() end
|
||||
|
||||
---Inserts a new separator before `before`. If `before` is not found,
|
||||
---the separator is inserted at the end. `before` can either be a name or a
|
||||
---one-based index.
|
||||
---@param before string|integer A name or index of an action.
|
||||
function c2.Menu:insert_separator(before) end
|
||||
-- End src/controllers/plugins/api/Menu.hpp
|
||||
|
||||
-- Begin src/controllers/plugins/api/Message.hpp
|
||||
|
||||
|
||||
@@ -1006,6 +1048,18 @@ c2.WindowManager = {}
|
||||
---@return c2.Window[] windows
|
||||
function c2.WindowManager:all() end
|
||||
|
||||
---@class ChannelViewContextMenuRequestedArgs
|
||||
---@field split? c2.Split The split holding the channel view. This is `nil` if the view is not inside a split.
|
||||
---@field message c2.Message The clicked message.
|
||||
---@field message_element? MessageElement The clicked message element.
|
||||
---@field channel? c2.Channel The channel shown in the view. Note that this might be a virtual channel (e.g. in a search popup or usercard).
|
||||
---@field menu c2.Menu The context menu. Add your actions here.
|
||||
|
||||
---Registers an event handler for context menus in ChannelViews.
|
||||
---@param cb fun(args: ChannelViewContextMenuRequestedArgs)
|
||||
---@return c2.ConnectionHandle
|
||||
function c2.WindowManager:on_channelview_context_menu_requested(cb) end
|
||||
|
||||
---@type c2.WindowManager
|
||||
c2.windows = ...
|
||||
-- End src/controllers/plugins/api/WindowManager.hpp
|
||||
|
||||
@@ -977,6 +977,21 @@ GraphViz output:
|
||||
|
||||
Get all open windows.
|
||||
|
||||
##### `WindowManager:on_channelview_context_menu_requested(cb)`
|
||||
|
||||
Registers an event handler for context menus in ChannelViews.
|
||||
|
||||
When a context menu is requested, `cb` is passed a table with the following
|
||||
fields:
|
||||
|
||||
- `split?` ([`Split`](#split)) The split holding the channel view. This is `nil`
|
||||
if the view is not inside a split.
|
||||
- `message` ([`Message`](#message)) The clicked message.
|
||||
- `message_element?` (`MessageElement`) The clicked message element.
|
||||
- `channel?` ([`Channel`](#channel)) The channel shown in the view. Note that
|
||||
this might be a virtual channel (e.g. in a search popup or usercard).
|
||||
- `menu` ([`Menu`](#qmenu)) The context menu. Add your actions here.
|
||||
|
||||
#### `c2.windows`
|
||||
|
||||
The global [`WindowManager`](#windowmanager).
|
||||
@@ -1046,6 +1061,39 @@ a local time but `1970-01-01T00:00:00Z` is not.
|
||||
|
||||
Returns a copy of this datetime converted to UTC.
|
||||
|
||||
#### `Menu`
|
||||
|
||||
A generic menu used for context menus.
|
||||
|
||||
##### `Menu:add_action(text, cb)`
|
||||
|
||||
Appends a new action to the menu.
|
||||
|
||||
##### `Menu:insert_action(before, text, cb)`
|
||||
|
||||
Inserts an action named `text` before `before`. If `before` is not found, the
|
||||
action is inserted at the end. `before` can either be a name or a one-based
|
||||
index.
|
||||
|
||||
##### `Menu:add_menu(title)`
|
||||
|
||||
Appends a new Menu with `title` to the menu. Returns the new menu.
|
||||
|
||||
##### `Menu:insert_menu(before, text)`
|
||||
|
||||
Inserts a new Menu named `title` before `before`. If `before` is not found,
|
||||
the menu is inserted at the end. `before` can either be a name or a one-based
|
||||
index. Returns the new menu.
|
||||
|
||||
##### `Menu:add_separator()`
|
||||
|
||||
Appends a new separator.
|
||||
|
||||
##### `Menu:insert_separator(before)`
|
||||
|
||||
Inserts a new separator before `before`. If `before` is not found, the separator
|
||||
is inserted at the end. `before` can either be a name or a one-based index.
|
||||
|
||||
### Input/Output API
|
||||
|
||||
These functions are wrappers for Lua's I/O library. Functions on file pointer
|
||||
|
||||
@@ -281,6 +281,8 @@ set(SOURCE_FILES
|
||||
controllers/plugins/api/JSONParse.hpp
|
||||
controllers/plugins/api/JSONStringify.cpp
|
||||
controllers/plugins/api/JSONStringify.hpp
|
||||
controllers/plugins/api/Menu.cpp
|
||||
controllers/plugins/api/Menu.hpp
|
||||
controllers/plugins/api/Message.cpp
|
||||
controllers/plugins/api/Message.hpp
|
||||
controllers/plugins/api/WebSocket.cpp
|
||||
|
||||
@@ -99,6 +99,7 @@ sol::table toTable(lua_State *L, const CompletionEvent &ev);
|
||||
* @includefile controllers/plugins/api/HTTPResponse.hpp
|
||||
* @includefile controllers/plugins/api/HTTPRequest.hpp
|
||||
* @includefile controllers/plugins/api/Images.hpp
|
||||
* @includefile controllers/plugins/api/Menu.hpp
|
||||
* @includefile controllers/plugins/api/Message.hpp
|
||||
* @includefile controllers/plugins/api/WebSocket.hpp
|
||||
* @includefile controllers/plugins/api/WindowManager.hpp
|
||||
|
||||
@@ -83,6 +83,12 @@ Plugin::~Plugin()
|
||||
"This must be empty or destructor of sol::protected_function would "
|
||||
"explode malloc structures later");
|
||||
}
|
||||
|
||||
lua::PluginWeakRef Plugin::weakRef() const
|
||||
{
|
||||
return this->selfRef_.weak();
|
||||
}
|
||||
|
||||
int Plugin::addTimeout(QTimer *timer)
|
||||
{
|
||||
this->activeTimeouts.push_back(timer);
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
# include "controllers/plugins/api/Images.hpp"
|
||||
# include "controllers/plugins/api/IOWrapper.hpp"
|
||||
# include "controllers/plugins/api/JSON.hpp"
|
||||
# include "controllers/plugins/api/Menu.hpp"
|
||||
# include "controllers/plugins/api/Message.hpp"
|
||||
# include "controllers/plugins/api/WebSocket.hpp"
|
||||
# include "controllers/plugins/api/WindowManager.hpp"
|
||||
@@ -259,6 +260,7 @@ void PluginController::initSol(sol::state_view &lua, Plugin *plugin)
|
||||
lua::api::createAccounts(c2);
|
||||
lua::api::windowmanager::createUserTypes(c2);
|
||||
lua::api::datetime::createUserTypes(c2);
|
||||
lua::api::menu::createUserType(c2);
|
||||
c2["ChannelType"] = lua::createEnumTable<Channel::Type>(lua);
|
||||
c2["HTTPMethod"] = lua::createEnumTable<NetworkRequestType>(lua);
|
||||
c2["EventType"] = lua::createEnumTable<lua::api::EventType>(lua);
|
||||
|
||||
@@ -53,6 +53,11 @@ struct SignalCallback {
|
||||
}
|
||||
}
|
||||
|
||||
PluginWeakRef owner() const
|
||||
{
|
||||
return this->pluginRef;
|
||||
}
|
||||
|
||||
void operator()(auto &&...args) const
|
||||
{
|
||||
assertInGuiThread();
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to Chatterino <https://chatterino.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "controllers/plugins/api/Menu.hpp"
|
||||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
|
||||
# include "controllers/plugins/Plugin.hpp"
|
||||
# include "controllers/plugins/SignalCallback.hpp"
|
||||
# include "controllers/plugins/SolTypes.hpp"
|
||||
|
||||
# include <QMenu>
|
||||
|
||||
namespace {
|
||||
|
||||
QAction *findAction(const QMenu &menu, const QString &name)
|
||||
{
|
||||
const auto actions = menu.actions();
|
||||
for (QAction *action : actions) // NOLINT(misc-const-correctness)
|
||||
{
|
||||
if (action->text() == name)
|
||||
{
|
||||
return action;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QAction *findAction(const QMenu &menu, int n)
|
||||
{
|
||||
n -= 1; // Take one-based indices
|
||||
|
||||
const auto actions = menu.actions();
|
||||
if (n < 0 || n >= actions.size())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return menu.actions().at(n);
|
||||
}
|
||||
|
||||
QAction *findAction(const QMenu &menu, const std::variant<QString, int> &spec)
|
||||
{
|
||||
return std::visit(
|
||||
[&](auto &&it) {
|
||||
return findAction(menu, it);
|
||||
},
|
||||
spec);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino::lua::api::menu {
|
||||
|
||||
void createUserType(sol::table &c2)
|
||||
{
|
||||
c2.new_usertype<QMenu>(
|
||||
"Menu", sol::no_constructor, //
|
||||
|
||||
"add_action",
|
||||
[](QMenu &menu, const QString &name, ThisPluginState state,
|
||||
sol::main_protected_function cb) {
|
||||
// NOLINTNEXTLINE(clazy-connect-3arg-lambda)
|
||||
menu.addAction(
|
||||
name, SignalCallback(state.plugin()->weakRef(), std::move(cb)));
|
||||
},
|
||||
"insert_action",
|
||||
[](QMenu &menu, const std::variant<QString, int> &before,
|
||||
const QString &name, ThisPluginState state,
|
||||
sol::main_protected_function cb) {
|
||||
auto *act = new QAction(name, &menu);
|
||||
QObject::connect(
|
||||
act, &QAction::triggered,
|
||||
SignalCallback(state.plugin()->weakRef(), std::move(cb)));
|
||||
menu.insertAction(findAction(menu, before), act);
|
||||
},
|
||||
|
||||
"add_menu",
|
||||
[](QMenu &menu, const QString &title) {
|
||||
return QPointer(menu.addMenu(title));
|
||||
},
|
||||
"insert_menu",
|
||||
[](QMenu &menu, const std::variant<QString, int> &before,
|
||||
const QString &title) {
|
||||
return QPointer(menu.insertMenu(findAction(menu, before),
|
||||
new QMenu(title, &menu)));
|
||||
},
|
||||
|
||||
"add_separator",
|
||||
[](QMenu &menu) {
|
||||
menu.addSeparator();
|
||||
},
|
||||
"insert_separator",
|
||||
[](QMenu &menu, const std::variant<QString, int> &before) {
|
||||
menu.insertSeparator(findAction(menu, before));
|
||||
} //
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace chatterino::lua::api::menu
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,56 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to Chatterino <https://chatterino.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#pragma once
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include <sol/forward.hpp>
|
||||
|
||||
namespace chatterino::lua::api::menu {
|
||||
|
||||
/* @lua-fragment
|
||||
---A generic menu used for context menus.
|
||||
---@class c2.Menu
|
||||
c2.Menu = {}
|
||||
|
||||
---Appends a new action to the menu.
|
||||
---@param text string
|
||||
---@param cb fun()
|
||||
function c2.Menu:add_action(text, cb) end
|
||||
|
||||
---Inserts an action named `text` before `before`. If `before` is not found,
|
||||
---the action is inserted at the end. `before` can either be a name or a
|
||||
---one-based index.
|
||||
---@param before string|integer A name or index of an action.
|
||||
---@param text string
|
||||
---@param cb fun()
|
||||
function c2.Menu:insert_action(before, text, cb) end
|
||||
|
||||
---Appends a new Menu with `title` to the menu.
|
||||
---@param title string
|
||||
---@return c2.Menu
|
||||
function c2.Menu:add_menu(title) end
|
||||
|
||||
---Inserts a new Menu named `title` before `before`. If `before` is not found,
|
||||
---the menu is inserted at the end. `before` can either be a name or a one-based
|
||||
---index.
|
||||
---@param before string|integer A name or index of an action.
|
||||
---@param title string
|
||||
function c2.Menu:insert_menu(before, title) end
|
||||
|
||||
---Appends a new separator.
|
||||
function c2.Menu:add_separator() end
|
||||
|
||||
---Inserts a new separator before `before`. If `before` is not found,
|
||||
---the separator is inserted at the end. `before` can either be a name or a
|
||||
---one-based index.
|
||||
---@param before string|integer A name or index of an action.
|
||||
function c2.Menu:insert_separator(before) end
|
||||
*/
|
||||
|
||||
/// Creates the c2.Menu user type
|
||||
void createUserType(sol::table &c2);
|
||||
|
||||
} // namespace chatterino::lua::api::menu
|
||||
|
||||
#endif
|
||||
@@ -275,158 +275,134 @@ std::shared_ptr<Message> messageFromTable(const sol::table &tbl);
|
||||
|
||||
namespace chatterino::lua::api::message {
|
||||
|
||||
struct ElementRef {
|
||||
ElementRef() = default;
|
||||
ElementRef(std::shared_ptr<Message> msg, size_t index)
|
||||
: msg(std::move(msg))
|
||||
, index(index)
|
||||
MessageElement *ElementRef::element() const
|
||||
{
|
||||
if (!this->msg || this->index >= this->msg->elements.size())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
checkWritable(this->msg.get());
|
||||
return this->msg->elements[this->index].get();
|
||||
}
|
||||
|
||||
MessageElement *element() const
|
||||
const MessageElement *ElementRef::constElement() const
|
||||
{
|
||||
if (!this->msg || this->index >= this->msg->elements.size())
|
||||
{
|
||||
if (!this->msg || this->index >= this->msg->elements.size())
|
||||
return nullptr;
|
||||
}
|
||||
return this->msg->elements[this->index].get();
|
||||
}
|
||||
|
||||
MessageElement &ElementRef::ref() const
|
||||
{
|
||||
auto *el = this->element();
|
||||
if (!el)
|
||||
{
|
||||
throw std::runtime_error("Element does not exist or expired");
|
||||
}
|
||||
return *el;
|
||||
}
|
||||
|
||||
const MessageElement &ElementRef::cref() const
|
||||
{
|
||||
const auto *el = this->constElement();
|
||||
if (!el)
|
||||
{
|
||||
throw std::runtime_error("Element does not exist or expired");
|
||||
}
|
||||
return *el;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
sol::optional<T &> ElementRef::as() const
|
||||
{
|
||||
// using ref() to error if the reference is invalid
|
||||
auto *el = dynamic_cast<T *>(&this->ref());
|
||||
if (!el)
|
||||
{
|
||||
return sol::nullopt;
|
||||
}
|
||||
return *el;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
sol::optional<const T &> ElementRef::asConst() const
|
||||
{
|
||||
// using cref() to error if the reference is invalid
|
||||
const auto *el = dynamic_cast<const T *>(&this->cref());
|
||||
if (!el)
|
||||
{
|
||||
return sol::nullopt;
|
||||
}
|
||||
return *el;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool ElementRef::is() const
|
||||
{
|
||||
return dynamic_cast<const T *>(&this->cref()) != nullptr;
|
||||
}
|
||||
|
||||
/// Visit this element by dynamic casting
|
||||
template <typename... T>
|
||||
auto ElementRef::visit(auto &&...cb) const
|
||||
{
|
||||
static_assert(sizeof...(T) == sizeof...(cb) && sizeof...(T) > 0);
|
||||
|
||||
// infer the returned type inside the optional
|
||||
using Cb0 = std::tuple_element_t<0, std::tuple<decltype(cb)...>>;
|
||||
using T0 = std::tuple_element_t<0, std::tuple<T...>>;
|
||||
using TReturn = std::invoke_result_t<Cb0, T0 &>;
|
||||
|
||||
return this->visitOne<TReturn, T...>(std::forward<decltype(cb)>(cb)...);
|
||||
}
|
||||
|
||||
bool ElementRef::operator==(const ElementRef &rhs) const
|
||||
{
|
||||
return this->msg.get() == rhs.msg.get() && this->index == rhs.index;
|
||||
}
|
||||
|
||||
template <bool Const>
|
||||
decltype(auto) ElementRef::maybeConstElement() const
|
||||
{
|
||||
if constexpr (Const)
|
||||
{
|
||||
return this->constElement();
|
||||
}
|
||||
else
|
||||
{
|
||||
return this->element();
|
||||
}
|
||||
}
|
||||
|
||||
template <typename TReturn, typename T, typename... Rest>
|
||||
auto ElementRef::visitOne(auto &&cb, auto &&...rest) const
|
||||
-> std::conditional_t<std::is_void_v<TReturn>, void, sol::optional<TReturn>>
|
||||
{
|
||||
auto *el = dynamic_cast<T *>(this->maybeConstElement<std::is_const_v<T>>());
|
||||
if (!el)
|
||||
{
|
||||
if constexpr (sizeof...(rest) == 0)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
checkWritable(this->msg.get());
|
||||
return this->msg->elements[this->index].get();
|
||||
}
|
||||
|
||||
const MessageElement *constElement() const
|
||||
{
|
||||
if (!this->msg || this->index >= this->msg->elements.size())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return this->msg->elements[this->index].get();
|
||||
}
|
||||
|
||||
MessageElement &ref() const
|
||||
{
|
||||
auto *el = this->element();
|
||||
if (!el)
|
||||
{
|
||||
throw std::runtime_error("Element does not exist or expired");
|
||||
}
|
||||
return *el;
|
||||
}
|
||||
|
||||
const MessageElement &cref() const
|
||||
{
|
||||
const auto *el = this->constElement();
|
||||
if (!el)
|
||||
{
|
||||
throw std::runtime_error("Element does not exist or expired");
|
||||
}
|
||||
return *el;
|
||||
}
|
||||
|
||||
/// Cast this element to `T`. Otherwise nullopt is returned.
|
||||
/// Use `.map()` to access the content.
|
||||
template <typename T>
|
||||
sol::optional<T &> as() const
|
||||
{
|
||||
// using ref() to error if the reference is invalid
|
||||
auto *el = dynamic_cast<T *>(&this->ref());
|
||||
if (!el)
|
||||
{
|
||||
return sol::nullopt;
|
||||
}
|
||||
return *el;
|
||||
}
|
||||
|
||||
/// Cast this element to `const T`. Otherwise nullopt is returned.
|
||||
/// Use `.map()` to access the content.
|
||||
template <typename T>
|
||||
sol::optional<const T &> asConst() const
|
||||
{
|
||||
// using cref() to error if the reference is invalid
|
||||
const auto *el = dynamic_cast<const T *>(&this->cref());
|
||||
if (!el)
|
||||
{
|
||||
return sol::nullopt;
|
||||
}
|
||||
return *el;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool is() const
|
||||
{
|
||||
return dynamic_cast<const T *>(&this->cref()) != nullptr;
|
||||
}
|
||||
|
||||
/// Visit this element by dynamic casting
|
||||
template <typename... T>
|
||||
auto visit(auto &&...cb) const
|
||||
{
|
||||
static_assert(sizeof...(T) == sizeof...(cb) && sizeof...(T) > 0);
|
||||
|
||||
// infer the returned type inside the optional
|
||||
using Cb0 = std::tuple_element_t<0, std::tuple<decltype(cb)...>>;
|
||||
using T0 = std::tuple_element_t<0, std::tuple<T...>>;
|
||||
using TReturn = std::invoke_result_t<Cb0, T0 &>;
|
||||
|
||||
return this->visitOne<TReturn, T...>(std::forward<decltype(cb)>(cb)...);
|
||||
}
|
||||
|
||||
bool operator==(const ElementRef &rhs) const
|
||||
{
|
||||
return this->msg.get() == rhs.msg.get() && this->index == rhs.index;
|
||||
}
|
||||
|
||||
std::shared_ptr<Message> msg;
|
||||
size_t index = 0;
|
||||
|
||||
private:
|
||||
template <bool Const>
|
||||
decltype(auto) maybeConstElement() const
|
||||
{
|
||||
if constexpr (Const)
|
||||
{
|
||||
return this->constElement();
|
||||
}
|
||||
else
|
||||
{
|
||||
return this->element();
|
||||
}
|
||||
}
|
||||
|
||||
/// Run one callback
|
||||
///
|
||||
/// This is called recursively.
|
||||
/// If the callback returns something, we return an `optional<T>` otherwise
|
||||
/// we return `void`.
|
||||
template <typename TReturn, typename T, typename... Rest>
|
||||
auto visitOne(auto &&cb, auto &&...rest) const
|
||||
-> std::conditional_t<std::is_void_v<TReturn>, void,
|
||||
sol::optional<TReturn>>
|
||||
{
|
||||
auto *el =
|
||||
dynamic_cast<T *>(this->maybeConstElement<std::is_const_v<T>>());
|
||||
if (!el)
|
||||
{
|
||||
if constexpr (sizeof...(rest) == 0)
|
||||
if constexpr (std::is_void_v<
|
||||
std::invoke_result_t<decltype(cb), T &>>)
|
||||
{
|
||||
if constexpr (std::is_void_v<
|
||||
std::invoke_result_t<decltype(cb), T &>>)
|
||||
{
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
return sol::nullopt;
|
||||
}
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
return this->visitOne<TReturn, Rest...>(
|
||||
std::forward<decltype(rest)>(rest)...);
|
||||
return sol::nullopt;
|
||||
}
|
||||
}
|
||||
return std::invoke(cb, *el);
|
||||
else
|
||||
{
|
||||
return this->visitOne<TReturn, Rest...>(
|
||||
std::forward<decltype(rest)>(rest)...);
|
||||
}
|
||||
}
|
||||
};
|
||||
return std::invoke(cb, *el);
|
||||
}
|
||||
|
||||
struct ElementIterator {
|
||||
using difference_type = std::ptrdiff_t;
|
||||
|
||||
@@ -245,6 +245,58 @@ enum class ExposedLinkType : std::uint8_t {
|
||||
* @includefile common/enums/MessageContext.hpp
|
||||
*/
|
||||
|
||||
struct ElementRef {
|
||||
ElementRef() = default;
|
||||
ElementRef(std::shared_ptr<Message> msg, size_t index)
|
||||
: msg(std::move(msg))
|
||||
, index(index)
|
||||
{
|
||||
}
|
||||
|
||||
MessageElement *element() const;
|
||||
|
||||
const MessageElement *constElement() const;
|
||||
|
||||
MessageElement &ref() const;
|
||||
const MessageElement &cref() const;
|
||||
|
||||
/// Cast this element to `T`. Otherwise nullopt is returned.
|
||||
/// Use `.map()` to access the content.
|
||||
template <typename T>
|
||||
sol::optional<T &> as() const;
|
||||
|
||||
/// Cast this element to `const T`. Otherwise nullopt is returned.
|
||||
/// Use `.map()` to access the content.
|
||||
template <typename T>
|
||||
sol::optional<const T &> asConst() const;
|
||||
|
||||
template <typename T>
|
||||
bool is() const;
|
||||
|
||||
/// Visit this element by dynamic casting
|
||||
template <typename... T>
|
||||
auto visit(auto &&...cb) const;
|
||||
|
||||
bool operator==(const ElementRef &rhs) const;
|
||||
|
||||
std::shared_ptr<Message> msg;
|
||||
size_t index = 0;
|
||||
|
||||
private:
|
||||
template <bool Const>
|
||||
decltype(auto) maybeConstElement() const;
|
||||
|
||||
/// Run one callback
|
||||
///
|
||||
/// This is called recursively.
|
||||
/// If the callback returns something, we return an `optional<T>` otherwise
|
||||
/// we return `void`.
|
||||
template <typename TReturn, typename T, typename... Rest>
|
||||
auto visitOne(auto &&cb, auto &&...rest) const
|
||||
-> std::conditional_t<std::is_void_v<TReturn>, void,
|
||||
sol::optional<TReturn>>;
|
||||
};
|
||||
|
||||
/// Creates the c2.Message user type
|
||||
void createUserType(sol::table &c2);
|
||||
|
||||
|
||||
@@ -7,9 +7,15 @@
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
|
||||
# include "controllers/plugins/api/ChannelRef.hpp"
|
||||
# include "controllers/plugins/api/Message.hpp"
|
||||
# include "controllers/plugins/Plugin.hpp"
|
||||
# include "controllers/plugins/SignalCallback.hpp"
|
||||
# include "controllers/plugins/SolTypes.hpp" // IWYU pragma: keep
|
||||
# include "messages/layouts/MessageLayout.hpp"
|
||||
# include "messages/layouts/MessageLayoutElement.hpp"
|
||||
# include "singletons/WindowManager.hpp"
|
||||
# include "util/WeakPtrHelpers.hpp"
|
||||
# include "widgets/helper/ChannelView.hpp"
|
||||
# include "widgets/Notebook.hpp"
|
||||
# include "widgets/splits/Split.hpp"
|
||||
# include "widgets/Window.hpp"
|
||||
@@ -178,8 +184,58 @@ void createUserTypes(sol::table &c2)
|
||||
sol::readonly_property([](const WindowManager &self) {
|
||||
return QPointer(self.getLastSelectedWindow());
|
||||
}),
|
||||
"all", [](const WindowManager &self, sol::this_state state) {
|
||||
"all",
|
||||
[](const WindowManager &self, sol::this_state state) {
|
||||
return qPointerWrapped(self.windows(), state);
|
||||
},
|
||||
"on_channelview_context_menu_requested",
|
||||
[](WindowManager &self, ThisPluginState state,
|
||||
sol::main_protected_function cb) {
|
||||
return state.plugin()->connections.managedConnect(
|
||||
self.channelViewContextMenuRequested,
|
||||
[cb = SignalCallback(state.plugin()->weakRef(), std::move(cb))](
|
||||
const ChannelView &view, const MessageLayout &layout,
|
||||
const MessageLayoutElement *element, QMenu &menu) {
|
||||
auto pluginRef = cb.owner().strong();
|
||||
if (!pluginRef)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::optional<message::ElementRef> ref;
|
||||
|
||||
auto msg = std::const_pointer_cast<Message>(
|
||||
layout.getMessagePtr());
|
||||
if (element)
|
||||
{
|
||||
// Find the index of `el` in the message to create an
|
||||
// `ElementRef`.
|
||||
auto *el = &element->getCreator();
|
||||
for (size_t i = 0; i < msg->elements.size(); ++i)
|
||||
{
|
||||
if (msg->elements[i].get() == el)
|
||||
{
|
||||
ref.emplace(msg, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<ChannelRef> maybeChan;
|
||||
if (auto underlyingChan = view.underlyingChannel())
|
||||
{
|
||||
maybeChan.emplace(std::move(underlyingChan));
|
||||
}
|
||||
|
||||
auto tbl = pluginRef.plugin()->state().create_table_with(
|
||||
"split", QPointer(view.findParentSplit()), //
|
||||
"channel", std::move(maybeChan), //
|
||||
"menu", QPointer(&menu), //
|
||||
"message", std::move(msg), //
|
||||
"message_element", std::move(ref) //
|
||||
);
|
||||
cb(std::move(tbl));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,18 @@ c2.WindowManager = {}
|
||||
---@return c2.Window[] windows
|
||||
function c2.WindowManager:all() end
|
||||
|
||||
---@class ChannelViewContextMenuRequestedArgs
|
||||
---@field split? c2.Split The split holding the channel view. This is `nil` if the view is not inside a split.
|
||||
---@field message c2.Message The clicked message.
|
||||
---@field message_element? MessageElement The clicked message element.
|
||||
---@field channel? c2.Channel The channel shown in the view. Note that this might be a virtual channel (e.g. in a search popup or usercard).
|
||||
---@field menu c2.Menu The context menu. Add your actions here.
|
||||
|
||||
---Registers an event handler for context menus in ChannelViews.
|
||||
---@param cb fun(args: ChannelViewContextMenuRequestedArgs)
|
||||
---@return c2.ConnectionHandle
|
||||
function c2.WindowManager:on_channelview_context_menu_requested(cb) end
|
||||
|
||||
---@type c2.WindowManager
|
||||
c2.windows = ...
|
||||
*/
|
||||
|
||||
@@ -37,6 +37,8 @@ class Channel;
|
||||
using ChannelPtr = std::shared_ptr<Channel>;
|
||||
struct Message;
|
||||
using MessagePtr = std::shared_ptr<const Message>;
|
||||
class MessageLayout;
|
||||
class MessageLayoutElement;
|
||||
class WindowLayout;
|
||||
class Theme;
|
||||
class Fonts;
|
||||
@@ -170,6 +172,12 @@ public:
|
||||
pajlada::Signals::Signal<SplitContainer *> selectSplitContainer;
|
||||
pajlada::Signals::Signal<const MessagePtr &> scrollToMessageSignal;
|
||||
|
||||
/// This is invoked when a context menu for a message is requested in any
|
||||
/// ChannelView. It's primarily used by plugins to add items.
|
||||
pajlada::Signals::Signal<const ChannelView &, const MessageLayout &,
|
||||
const MessageLayoutElement *, QMenu &>
|
||||
channelViewContextMenuRequested;
|
||||
|
||||
private:
|
||||
// Load window layout from the window-layout.json file
|
||||
WindowLayout loadWindowLayoutFromFile() const;
|
||||
|
||||
@@ -464,6 +464,23 @@ Scrollbar *ChannelView::scrollbar()
|
||||
return this->scrollBar_;
|
||||
}
|
||||
|
||||
Split *ChannelView::findParentSplit() const
|
||||
{
|
||||
auto *split = dynamic_cast<Split *>(this->parentWidget());
|
||||
|
||||
if (split)
|
||||
{
|
||||
return split;
|
||||
}
|
||||
|
||||
auto *searchPopup = dynamic_cast<SearchPopup *>(this->parentWidget());
|
||||
if (!searchPopup)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return dynamic_cast<Split *>(searchPopup->parentWidget());
|
||||
}
|
||||
|
||||
bool ChannelView::pausable() const
|
||||
{
|
||||
return this->pausable_;
|
||||
@@ -1408,16 +1425,7 @@ MessageElementFlags ChannelView::getFlags() const
|
||||
|
||||
MessageElementFlags flags = app->getWindows()->getWordFlags();
|
||||
|
||||
auto *split = dynamic_cast<Split *>(this->parentWidget());
|
||||
|
||||
if (split == nullptr)
|
||||
{
|
||||
auto *searchPopup = dynamic_cast<SearchPopup *>(this->parentWidget());
|
||||
if (searchPopup != nullptr)
|
||||
{
|
||||
split = dynamic_cast<Split *>(searchPopup->parentWidget());
|
||||
}
|
||||
}
|
||||
auto *split = this->findParentSplit();
|
||||
|
||||
if (split != nullptr)
|
||||
{
|
||||
@@ -2631,6 +2639,11 @@ void ChannelView::addContextMenuItems(
|
||||
|
||||
this->messageMenuCreated.invoke(menu, hoveredElement);
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
getApp()->getWindows()->channelViewContextMenuRequested.invoke(
|
||||
*this, *layout, hoveredElement, *menu);
|
||||
|
||||
menu->popup(QCursor::pos());
|
||||
menu->raise();
|
||||
}
|
||||
|
||||
@@ -223,6 +223,8 @@ public:
|
||||
|
||||
Scrollbar *scrollbar();
|
||||
|
||||
Split *findParentSplit() const;
|
||||
|
||||
using ChannelViewID = std::size_t;
|
||||
///
|
||||
/// \brief Get the ID of this ChannelView
|
||||
|
||||
Reference in New Issue
Block a user