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:
Nerixyz
2026-08-09 10:32:03 +00:00
committed by GitHub
parent f154a120ca
commit 514ff0b42c
17 changed files with 571 additions and 151 deletions
+25
View File
@@ -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" {
+54
View File
@@ -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
+48
View File
@@ -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
+2
View File
@@ -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
+1
View File
@@ -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
+6
View File
@@ -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();
+102
View File
@@ -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
+56
View File
@@ -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
+116 -140
View File
@@ -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;
+52
View File
@@ -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);
+57 -1
View File
@@ -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 = ...
*/
+8
View File
@@ -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;
+23 -10
View File
@@ -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();
}
+2
View File
@@ -223,6 +223,8 @@ public:
Scrollbar *scrollbar();
Split *findParentSplit() const;
using ChannelViewID = std::size_t;
///
/// \brief Get the ID of this ChannelView