summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/abaddon.cpp10
-rw-r--r--src/components/channels.cpp18
-rw-r--r--src/components/channels.hpp21
-rw-r--r--src/components/channeltabswitcherhandy.cpp128
-rw-r--r--src/components/channeltabswitcherhandy.hpp49
-rw-r--r--src/components/chatwindow.cpp24
-rw-r--r--src/components/chatwindow.hpp28
-rw-r--r--src/discord/channel.cpp13
-rw-r--r--src/discord/channel.hpp1
-rw-r--r--src/windows/mainwindow.cpp6
10 files changed, 286 insertions, 12 deletions
diff --git a/src/abaddon.cpp b/src/abaddon.cpp
index a2d65e5..3909590 100644
--- a/src/abaddon.cpp
+++ b/src/abaddon.cpp
@@ -17,6 +17,10 @@
#include "windows/pinnedwindow.hpp"
#include "windows/threadswindow.hpp"
+#ifdef WITH_LIBHANDY
+ #include <handy.h>
+#endif
+
#ifdef _WIN32
#pragma comment(lib, "crypt32.lib")
#endif
@@ -62,6 +66,12 @@ Abaddon &Abaddon::Get() {
int Abaddon::StartGTK() {
m_gtk_app = Gtk::Application::create("com.github.uowuo.abaddon");
+#ifdef WITH_LIBHANDY
+ m_gtk_app->signal_activate().connect([] {
+ hdy_init();
+ });
+#endif
+
m_css_provider = Gtk::CssProvider::create();
m_css_provider->signal_parsing_error().connect([](const Glib::RefPtr<const Gtk::CssSection> &section, const Glib::Error &error) {
Gtk::MessageDialog dlg("css failed parsing (" + error.what() + ")", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
diff --git a/src/components/channels.cpp b/src/components/channels.cpp
index 4a6b1bc..929eeb8 100644
--- a/src/components/channels.cpp
+++ b/src/components/channels.cpp
@@ -17,6 +17,9 @@ ChannelList::ChannelList()
, m_menu_category_copy_id("_Copy ID", true)
, m_menu_channel_copy_id("_Copy ID", true)
, m_menu_channel_mark_as_read("Mark as _Read", true)
+#ifdef WITH_LIBHANDY
+ , m_menu_channel_open_tab("Open in New _Tab", true)
+#endif
, m_menu_dm_copy_id("_Copy ID", true)
, m_menu_dm_close("") // changes depending on if group or not
, m_menu_thread_copy_id("_Copy ID", true)
@@ -143,6 +146,15 @@ ChannelList::ChannelList()
else
discord.MuteChannel(id, NOOP_CALLBACK);
});
+
+#ifdef WITH_LIBHANDY
+ m_menu_channel_open_tab.signal_activate().connect([this] {
+ const auto id = static_cast<Snowflake>((*m_model->get_iter(m_path_for_menu))[m_columns.m_id]);
+ m_signal_action_open_new_tab.emit(id);
+ });
+ m_menu_channel.append(m_menu_channel_open_tab);
+#endif
+
m_menu_channel.append(m_menu_channel_mark_as_read);
m_menu_channel.append(m_menu_channel_toggle_mute);
m_menu_channel.append(m_menu_channel_copy_id);
@@ -960,6 +972,12 @@ ChannelList::type_signal_action_guild_settings ChannelList::signal_action_guild_
return m_signal_action_guild_settings;
}
+#ifdef WITH_LIBHANDY
+ChannelList::type_signal_action_open_new_tab ChannelList::signal_action_open_new_tab() {
+ return m_signal_action_open_new_tab;
+}
+#endif
+
ChannelList::ModelColumns::ModelColumns() {
add(m_type);
add(m_id);
diff --git a/src/components/channels.hpp b/src/components/channels.hpp
index 044d0b5..e8032af 100644
--- a/src/components/channels.hpp
+++ b/src/components/channels.hpp
@@ -121,6 +121,10 @@ protected:
Gtk::MenuItem m_menu_channel_mark_as_read;
Gtk::MenuItem m_menu_channel_toggle_mute;
+#ifdef WITH_LIBHANDY
+ Gtk::MenuItem m_menu_channel_open_tab;
+#endif
+
Gtk::Menu m_menu_dm;
Gtk::MenuItem m_menu_dm_copy_id;
Gtk::MenuItem m_menu_dm_close;
@@ -149,16 +153,25 @@ protected:
std::unordered_map<Snowflake, Gtk::TreeModel::iterator> m_tmp_channel_map;
public:
- typedef sigc::signal<void, Snowflake> type_signal_action_channel_item_select;
- typedef sigc::signal<void, Snowflake> type_signal_action_guild_leave;
- typedef sigc::signal<void, Snowflake> type_signal_action_guild_settings;
+ using type_signal_action_channel_item_select = sigc::signal<void, Snowflake>;
+ using type_signal_action_guild_leave = sigc::signal<void, Snowflake>;
+ using type_signal_action_guild_settings = sigc::signal<void, Snowflake>;
+
+#ifdef WITH_LIBHANDY
+ using type_signal_action_open_new_tab = sigc::signal<void, Snowflake>;
+ type_signal_action_open_new_tab signal_action_open_new_tab();
+#endif
type_signal_action_channel_item_select signal_action_channel_item_select();
type_signal_action_guild_leave signal_action_guild_leave();
type_signal_action_guild_settings signal_action_guild_settings();
-protected:
+private:
type_signal_action_channel_item_select m_signal_action_channel_item_select;
type_signal_action_guild_leave m_signal_action_guild_leave;
type_signal_action_guild_settings m_signal_action_guild_settings;
+
+#ifdef WITH_LIBHANDY
+ type_signal_action_open_new_tab m_signal_action_open_new_tab;
+#endif
};
diff --git a/src/components/channeltabswitcherhandy.cpp b/src/components/channeltabswitcherhandy.cpp
new file mode 100644
index 0000000..e359896
--- /dev/null
+++ b/src/components/channeltabswitcherhandy.cpp
@@ -0,0 +1,128 @@
+#ifdef WITH_LIBHANDY
+
+ #include "channeltabswitcherhandy.hpp"
+ #include "abaddon.hpp"
+
+void selected_page_notify_cb(HdyTabView *view, GParamSpec *pspec, ChannelTabSwitcherHandy *switcher) {
+ auto *page = hdy_tab_view_get_selected_page(view);
+ if (auto it = switcher->m_pages_rev.find(page); it != switcher->m_pages_rev.end()) {
+ switcher->m_signal_channel_switched_to.emit(it->second);
+ }
+}
+
+gboolean close_page_cb(HdyTabView *view, HdyTabPage *page, ChannelTabSwitcherHandy *switcher) {
+ switcher->ClearPage(page);
+ hdy_tab_view_close_page_finish(view, page, true);
+ return GDK_EVENT_STOP;
+}
+
+ChannelTabSwitcherHandy::ChannelTabSwitcherHandy() {
+ m_tab_bar = hdy_tab_bar_new();
+ m_tab_bar_wrapped = Glib::wrap(GTK_WIDGET(m_tab_bar));
+ m_tab_view = hdy_tab_view_new();
+ m_tab_view_wrapped = Glib::wrap(GTK_WIDGET(m_tab_view));
+
+ g_signal_connect(m_tab_view, "notify::selected-page", G_CALLBACK(selected_page_notify_cb), this);
+ g_signal_connect(m_tab_view, "close-page", G_CALLBACK(close_page_cb), this);
+
+ hdy_tab_bar_set_view(m_tab_bar, m_tab_view);
+ add(*m_tab_bar_wrapped);
+ m_tab_bar_wrapped->show();
+
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ discord.signal_message_create().connect([this](const Message &data) {
+ CheckUnread(data.ChannelID);
+ });
+
+ discord.signal_message_ack().connect([this](const MessageAckData &data) {
+ CheckUnread(data.ChannelID);
+ });
+}
+
+void ChannelTabSwitcherHandy::AddChannelTab(Snowflake id) {
+ if (m_pages.find(id) != m_pages.end()) return;
+
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ const auto channel = discord.GetChannel(id);
+ if (!channel.has_value()) return;
+
+ auto *dummy = Gtk::make_managed<Gtk::Box>(); // minimal
+ auto *page = hdy_tab_view_append(m_tab_view, GTK_WIDGET(dummy->gobj()));
+
+ hdy_tab_page_set_title(page, ("#" + *channel->Name).c_str());
+ hdy_tab_page_set_tooltip(page, nullptr);
+
+ m_pages[id] = page;
+ m_pages_rev[page] = id;
+
+ CheckUnread(id);
+ CheckPageIcon(page, *channel);
+}
+
+void ChannelTabSwitcherHandy::ReplaceActiveTab(Snowflake id) {
+ auto *page = hdy_tab_view_get_selected_page(m_tab_view);
+ if (page == nullptr) {
+ AddChannelTab(id);
+ } else if (auto it = m_pages.find(id); it != m_pages.end()) {
+ hdy_tab_view_set_selected_page(m_tab_view, it->second);
+ } else {
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ const auto channel = discord.GetChannel(id);
+ if (!channel.has_value()) return;
+
+ hdy_tab_page_set_title(page, channel->GetDisplayName().c_str());
+
+ ClearPage(page);
+ m_pages[id] = page;
+ m_pages_rev[page] = id;
+
+ CheckUnread(id);
+ CheckPageIcon(page, *channel);
+ }
+}
+
+void ChannelTabSwitcherHandy::CheckUnread(Snowflake id) {
+ if (auto it = m_pages.find(id); it != m_pages.end()) {
+ hdy_tab_page_set_needs_attention(it->second, Abaddon::Get().GetDiscordClient().GetUnreadStateForChannel(id) > -1);
+ }
+}
+
+void ChannelTabSwitcherHandy::ClearPage(HdyTabPage *page) {
+ if (auto it = m_pages_rev.find(page); it != m_pages_rev.end()) {
+ m_pages.erase(it->second);
+ }
+ m_pages_rev.erase(page);
+ m_page_icons.erase(page);
+}
+
+void ChannelTabSwitcherHandy::OnPageIconLoad(HdyTabPage *page, const Glib::RefPtr<Gdk::Pixbuf> &pb) {
+ auto new_pb = pb->scale_simple(16, 16, Gdk::INTERP_BILINEAR);
+ m_page_icons[page] = new_pb;
+ hdy_tab_page_set_icon(page, G_ICON(new_pb->gobj()));
+}
+
+void ChannelTabSwitcherHandy::CheckPageIcon(HdyTabPage *page, const ChannelData &data) {
+ if (data.GuildID.has_value()) {
+ if (const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(*data.GuildID); guild.has_value() && guild->HasIcon()) {
+ auto *child_widget = hdy_tab_page_get_child(page);
+ if (child_widget == nullptr) return; // probably wont happen :---)
+ // i think this works???
+ auto *trackable = Glib::wrap(GTK_WIDGET(child_widget));
+
+ Abaddon::Get().GetImageManager().LoadFromURL(
+ guild->GetIconURL("png", "16"),
+ sigc::track_obj([this, page](const Glib::RefPtr<Gdk::Pixbuf> &pb) { OnPageIconLoad(page, pb); },
+ *trackable));
+ return;
+ }
+ return;
+ }
+
+ hdy_tab_page_set_icon(page, nullptr);
+}
+
+ChannelTabSwitcherHandy::type_signal_channel_switched_to ChannelTabSwitcherHandy::signal_channel_switched_to() {
+ return m_signal_channel_switched_to;
+}
+
+#endif
diff --git a/src/components/channeltabswitcherhandy.hpp b/src/components/channeltabswitcherhandy.hpp
new file mode 100644
index 0000000..6a2dbff
--- /dev/null
+++ b/src/components/channeltabswitcherhandy.hpp
@@ -0,0 +1,49 @@
+#pragma once
+// perhaps this should be conditionally included within cmakelists?
+#ifdef WITH_LIBHANDY
+ #include <gtkmm/box.h>
+ #include <unordered_map>
+ #include <handy.h>
+ #include "discord/snowflake.hpp"
+
+class ChannelData;
+
+// thin wrapper over c api
+// HdyTabBar + invisible HdyTabView since it needs one
+class ChannelTabSwitcherHandy : public Gtk::Box {
+public:
+ ChannelTabSwitcherHandy();
+
+ // no-op if already added
+ void AddChannelTab(Snowflake id);
+ // switches to existing tab if it exists
+ void ReplaceActiveTab(Snowflake id);
+
+private:
+ void CheckUnread(Snowflake id);
+ void ClearPage(HdyTabPage *page);
+ void OnPageIconLoad(HdyTabPage *page, const Glib::RefPtr<Gdk::Pixbuf> &pb);
+ void CheckPageIcon(HdyTabPage *page, const ChannelData &data);
+
+ HdyTabBar *m_tab_bar;
+ Gtk::Widget *m_tab_bar_wrapped;
+ HdyTabView *m_tab_view;
+ Gtk::Widget *m_tab_view_wrapped;
+
+ std::unordered_map<Snowflake, HdyTabPage *> m_pages;
+ std::unordered_map<HdyTabPage *, Snowflake> m_pages_rev;
+ // need to hold a reference to the pixbuf data
+ std::unordered_map<HdyTabPage *, Glib::RefPtr<Gdk::Pixbuf>> m_page_icons;
+
+ friend void selected_page_notify_cb(HdyTabView *, GParamSpec *, ChannelTabSwitcherHandy *);
+ friend gboolean close_page_cb(HdyTabView *, HdyTabPage *, ChannelTabSwitcherHandy *);
+
+public:
+ using type_signal_channel_switched_to = sigc::signal<void, Snowflake>;
+
+ type_signal_channel_switched_to signal_channel_switched_to();
+
+private:
+ type_signal_channel_switched_to m_signal_channel_switched_to;
+};
+#endif
diff --git a/src/components/chatwindow.cpp b/src/components/chatwindow.cpp
index 582343d..99ec8a0 100644
--- a/src/components/chatwindow.cpp
+++ b/src/components/chatwindow.cpp
@@ -4,6 +4,9 @@
#include "ratelimitindicator.hpp"
#include "chatinput.hpp"
#include "chatlist.hpp"
+#ifdef WITH_LIBHANDY
+ #include "channeltabswitcherhandy.hpp"
+#endif
ChatWindow::ChatWindow() {
Abaddon::Get().GetDiscordClient().signal_message_send_fail().connect(sigc::mem_fun(*this, &ChatWindow::OnMessageSendFail));
@@ -15,6 +18,13 @@ ChatWindow::ChatWindow() {
m_rate_limit_indicator = Gtk::manage(new RateLimitIndicator);
m_meta = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
+#ifdef WITH_LIBHANDY
+ m_tab_switcher = Gtk::make_managed<ChannelTabSwitcherHandy>();
+ m_tab_switcher->signal_channel_switched_to().connect([this](Snowflake id) {
+ m_signal_action_channel_click.emit(id);
+ });
+#endif
+
m_rate_limit_indicator->set_margin_end(5);
m_rate_limit_indicator->set_hexpand(true);
m_rate_limit_indicator->set_halign(Gtk::ALIGN_END);
@@ -88,6 +98,10 @@ ChatWindow::ChatWindow() {
m_meta->add(*m_input_indicator);
m_meta->add(*m_rate_limit_indicator);
// m_scroll->add(*m_list);
+#ifdef WITH_LIBHANDY
+ m_main->add(*m_tab_switcher);
+ m_tab_switcher->show();
+#endif
m_main->add(m_topic);
m_main->add(*m_chat);
m_main->add(m_completer);
@@ -115,6 +129,10 @@ void ChatWindow::SetActiveChannel(Snowflake id) {
m_rate_limit_indicator->SetActiveChannel(id);
if (m_is_replying)
StopReplying();
+
+#ifdef WITH_LIBHANDY
+ m_tab_switcher->ReplaceActiveTab(id);
+#endif
}
void ChatWindow::AddNewMessage(const Message &data) {
@@ -150,6 +168,12 @@ void ChatWindow::SetTopic(const std::string &text) {
m_topic.set_visible(text.length() > 0);
}
+#ifdef WITH_LIBHANDY
+void ChatWindow::OpenNewTab(Snowflake id) {
+ m_tab_switcher->AddChannelTab(id);
+}
+#endif
+
Snowflake ChatWindow::GetActiveChannel() const {
return m_active_channel;
}
diff --git a/src/components/chatwindow.hpp b/src/components/chatwindow.hpp
index 0f40e88..d77afec 100644
--- a/src/components/chatwindow.hpp
+++ b/src/components/chatwindow.hpp
@@ -5,6 +5,10 @@
#include "discord/discord.hpp"
#include "completer.hpp"
+#ifdef WITH_LIBHANDY
+class ChannelTabSwitcherHandy;
+#endif
+
class ChatMessageHeader;
class ChatMessageItemContainer;
class ChatInput;
@@ -25,11 +29,15 @@ public:
void DeleteMessage(Snowflake id); // add [deleted] indicator
void UpdateMessage(Snowflake id); // add [edited] indicator
void AddNewHistory(const std::vector<Message> &msgs); // prepend messages
- void InsertChatInput(const std::string& text);
+ void InsertChatInput(const std::string &text);
Snowflake GetOldestListedMessage(); // oldest message that is currently in the ListBox
void UpdateReactions(Snowflake id);
void SetTopic(const std::string &text);
+#ifdef WITH_LIBHANDY
+ void OpenNewTab(Snowflake id);
+#endif
+
protected:
bool m_is_replying = false;
Snowflake m_replying_to;
@@ -62,14 +70,18 @@ protected:
RateLimitIndicator *m_rate_limit_indicator;
Gtk::Box *m_meta;
+#ifdef WITH_LIBHANDY
+ ChannelTabSwitcherHandy *m_tab_switcher;
+#endif
+
public:
- typedef sigc::signal<void, Snowflake, Snowflake> type_signal_action_message_edit;
- typedef sigc::signal<void, std::string, Snowflake, Snowflake> type_signal_action_chat_submit;
- typedef sigc::signal<void, Snowflake> type_signal_action_chat_load_history;
- typedef sigc::signal<void, Snowflake> type_signal_action_channel_click;
- typedef sigc::signal<void, Snowflake> type_signal_action_insert_mention;
- typedef sigc::signal<void, Snowflake, Glib::ustring> type_signal_action_reaction_add;
- typedef sigc::signal<void, Snowflake, Glib::ustring> type_signal_action_reaction_remove;
+ using type_signal_action_message_edit = sigc::signal<void, Snowflake, Snowflake>;
+ using type_signal_action_chat_submit = sigc::signal<void, std::string, Snowflake, Snowflake>;
+ using type_signal_action_chat_load_history = sigc::signal<void, Snowflake>;
+ using type_signal_action_channel_click = sigc::signal<void, Snowflake>;
+ using type_signal_action_insert_mention = sigc::signal<void, Snowflake>;
+ using type_signal_action_reaction_add = sigc::signal<void, Snowflake, Glib::ustring>;
+ using type_signal_action_reaction_remove = sigc::signal<void, Snowflake, Glib::ustring>;
type_signal_action_message_edit signal_action_message_edit();
type_signal_action_chat_submit signal_action_chat_submit();
diff --git a/src/discord/channel.cpp b/src/discord/channel.cpp
index 0770581..6277341 100644
--- a/src/discord/channel.cpp
+++ b/src/discord/channel.cpp
@@ -92,6 +92,19 @@ std::string ChannelData::GetIconURL() const {
return "https://cdn.discordapp.com/channel-icons/" + std::to_string(ID) + "/" + *Icon + ".png";
}
+std::string ChannelData::GetDisplayName() const {
+ if (Name.has_value()) {
+ return "#" + *Name;
+ } else {
+ const auto recipients = GetDMRecipients();
+ if (Type == ChannelType::DM && !recipients.empty())
+ return recipients[0].Username;
+ else if (Type == ChannelType::GROUP_DM)
+ return std::to_string(recipients.size()) + " members";
+ }
+ return "Unknown";
+}
+
std::vector<Snowflake> ChannelData::GetChildIDs() const {
return Abaddon::Get().GetDiscordClient().GetChildChannelIDs(ID);
}
diff --git a/src/discord/channel.hpp b/src/discord/channel.hpp
index 8feeb92..77cf029 100644
--- a/src/discord/channel.hpp
+++ b/src/discord/channel.hpp
@@ -102,6 +102,7 @@ struct ChannelData {
[[nodiscard]] bool IsText() const noexcept;
[[nodiscard]] bool HasIcon() const noexcept;
[[nodiscard]] std::string GetIconURL() const;
+ [[nodiscard]] std::string GetDisplayName() const;
[[nodiscard]] std::vector<Snowflake> GetChildIDs() const;
[[nodiscard]] std::optional<PermissionOverwrite> GetOverwrite(Snowflake id) const;
[[nodiscard]] std::vector<UserData> GetDMRecipients() const;
diff --git a/src/windows/mainwindow.cpp b/src/windows/mainwindow.cpp
index 4436785..04fc72a 100644
--- a/src/windows/mainwindow.cpp
+++ b/src/windows/mainwindow.cpp
@@ -27,6 +27,12 @@ MainWindow::MainWindow()
chat->set_hexpand(true);
chat->show();
+#ifdef WITH_LIBHANDY
+ m_channel_list.signal_action_open_new_tab().connect([this](Snowflake id) {
+ m_chat.OpenNewTab(id);
+ });
+#endif
+
m_channel_list.set_vexpand(true);
m_channel_list.set_size_request(-1, -1);
m_channel_list.show();