diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/abaddon.cpp | 59 | ||||
-rw-r--r-- | src/abaddon.hpp | 2 | ||||
-rw-r--r-- | src/components/channels.cpp | 33 | ||||
-rw-r--r-- | src/components/channels.hpp | 27 | ||||
-rw-r--r-- | src/components/channeltabswitcherhandy.cpp | 202 | ||||
-rw-r--r-- | src/components/channeltabswitcherhandy.hpp | 66 | ||||
-rw-r--r-- | src/components/chatwindow.cpp | 46 | ||||
-rw-r--r-- | src/components/chatwindow.hpp | 33 | ||||
-rw-r--r-- | src/discord/channel.cpp | 13 | ||||
-rw-r--r-- | src/discord/channel.hpp | 1 | ||||
-rw-r--r-- | src/state.cpp | 11 | ||||
-rw-r--r-- | src/state.hpp | 9 | ||||
-rw-r--r-- | src/windows/mainwindow.cpp | 42 | ||||
-rw-r--r-- | src/windows/mainwindow.hpp | 11 |
14 files changed, 530 insertions, 25 deletions
diff --git a/src/abaddon.cpp b/src/abaddon.cpp index a2d65e5..4ca1462 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 @@ -59,9 +63,48 @@ Abaddon &Abaddon::Get() { return instance; } +#ifdef WITH_LIBHANDY + #ifdef _WIN32 +constexpr static guint BUTTON_BACK = 4; +constexpr static guint BUTTON_FORWARD = 5; + #else +constexpr static guint BUTTON_BACK = 8; +constexpr static guint BUTTON_FORWARD = 9; + #endif + +static void HandleButtonEvents(GdkEvent *event, MainWindow *main_window) { + if (event->type != GDK_BUTTON_PRESS) return; + + auto *widget = gtk_get_event_widget(event); + if (widget == nullptr) return; + auto *window = gtk_widget_get_toplevel(widget); + if (static_cast<void *>(window) != static_cast<void *>(main_window->gobj())) return; // is this the right way??? + + switch (event->button.button) { + case BUTTON_BACK: + main_window->GoBack(); + break; + case BUTTON_FORWARD: + main_window->GoForward(); + break; + } +} + +static void MainEventHandler(GdkEvent *event, void *main_window) { + HandleButtonEvents(event, static_cast<MainWindow *>(main_window)); + gtk_main_do_event(event); +} +#endif + 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> §ion, const Glib::Error &error) { Gtk::MessageDialog dlg("css failed parsing (" + error.what() + ")", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); @@ -103,6 +146,10 @@ int Abaddon::StartGTK() { m_main_window->set_title(APP_TITLE); m_main_window->set_position(Gtk::WIN_POS_CENTER); +#ifdef WITH_LIBHANDY + gdk_event_handler_set(&MainEventHandler, m_main_window.get(), nullptr); +#endif + if (!m_settings.IsValid()) { Gtk::MessageDialog dlg(*m_main_window, "The settings file could not be opened!", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); dlg.set_position(Gtk::WIN_POS_CENTER); @@ -138,7 +185,7 @@ int Abaddon::StartGTK() { m_main_window->signal_action_view_pins().connect(sigc::mem_fun(*this, &Abaddon::ActionViewPins)); m_main_window->signal_action_view_threads().connect(sigc::mem_fun(*this, &Abaddon::ActionViewThreads)); - m_main_window->GetChannelList()->signal_action_channel_item_select().connect(sigc::mem_fun(*this, &Abaddon::ActionChannelOpened)); + m_main_window->GetChannelList()->signal_action_channel_item_select().connect(sigc::bind(sigc::mem_fun(*this, &Abaddon::ActionChannelOpened), true)); m_main_window->GetChannelList()->signal_action_guild_leave().connect(sigc::mem_fun(*this, &Abaddon::ActionLeaveGuild)); m_main_window->GetChannelList()->signal_action_guild_settings().connect(sigc::mem_fun(*this, &Abaddon::ActionGuildSettings)); @@ -414,6 +461,9 @@ void Abaddon::SaveState() { AbaddonApplicationState state; state.ActiveChannel = m_main_window->GetChatActiveChannel(); state.Expansion = m_main_window->GetChannelList()->GetExpansionState(); +#ifdef WITH_LIBHANDY + state.Tabs = m_main_window->GetChatWindow()->GetTabsState(); +#endif const auto path = GetStateCachePath(); if (!util::IsFolder(path)) { @@ -440,6 +490,9 @@ void Abaddon::LoadState() { try { AbaddonApplicationState state = nlohmann::json::parse(data.begin(), data.end()); m_main_window->GetChannelList()->UseExpansionState(state.Expansion); +#ifdef WITH_LIBHANDY + m_main_window->GetChatWindow()->UseTabsState(state.Tabs); +#endif ActionChannelOpened(state.ActiveChannel); } catch (const std::exception &e) { printf("failed to load application state: %s\n", e.what()); @@ -551,7 +604,7 @@ void Abaddon::ActionJoinGuildDialog() { } } -void Abaddon::ActionChannelOpened(Snowflake id) { +void Abaddon::ActionChannelOpened(Snowflake id, bool expand_to) { if (!id.IsValid() || id == m_main_window->GetChatActiveChannel()) return; m_main_window->GetChatWindow()->SetTopic(""); @@ -574,7 +627,7 @@ void Abaddon::ActionChannelOpened(Snowflake id) { display = "Empty group"; m_main_window->set_title(std::string(APP_TITLE) + " - " + display); } - m_main_window->UpdateChatActiveChannel(id); + m_main_window->UpdateChatActiveChannel(id, expand_to); if (m_channels_requested.find(id) == m_channels_requested.end()) { // dont fire requests we know will fail if (can_access) { diff --git a/src/abaddon.hpp b/src/abaddon.hpp index 3404633..3296c45 100644 --- a/src/abaddon.hpp +++ b/src/abaddon.hpp @@ -35,7 +35,7 @@ public: void ActionDisconnect(); void ActionSetToken(); void ActionJoinGuildDialog(); - void ActionChannelOpened(Snowflake id); + void ActionChannelOpened(Snowflake id, bool expand_to = true); void ActionChatInputSubmit(std::string msg, Snowflake channel, Snowflake referenced_message); void ActionChatLoadHistory(Snowflake id); void ActionChatEditMessage(Snowflake channel_id, Snowflake id); diff --git a/src/components/channels.cpp b/src/components/channels.cpp index 4a6b1bc..2b83eb0 100644 --- a/src/components/channels.cpp +++ b/src/components/channels.cpp @@ -17,6 +17,10 @@ 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) + , m_menu_dm_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 +147,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); @@ -170,6 +183,13 @@ ChannelList::ChannelList() else discord.MuteChannel(id, NOOP_CALLBACK); }); +#ifdef WITH_LIBHANDY + m_menu_dm_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_dm.append(m_menu_dm_open_tab); +#endif m_menu_dm.append(m_menu_dm_toggle_mute); m_menu_dm.append(m_menu_dm_close); m_menu_dm.append(m_menu_dm_copy_id); @@ -442,7 +462,7 @@ void ChannelList::OnGuildUnmute(Snowflake id) { // create a temporary channel row for non-joined threads // and delete them when the active channel switches off of them if still not joined -void ChannelList::SetActiveChannel(Snowflake id) { +void ChannelList::SetActiveChannel(Snowflake id, bool expand_to) { // mark channel as read when switching off if (m_active_channel.IsValid()) Abaddon::Get().GetDiscordClient().MarkChannelAsRead(m_active_channel, [](...) {}); @@ -459,11 +479,12 @@ void ChannelList::SetActiveChannel(Snowflake id) { const auto channel_iter = GetIteratorForChannelFromID(id); if (channel_iter) { - m_view.expand_to_path(m_model->get_path(channel_iter)); + if (expand_to) { + m_view.expand_to_path(m_model->get_path(channel_iter)); + } m_view.get_selection()->select(channel_iter); } else { m_view.get_selection()->unselect_all(); - // SetActiveChannel should probably just take the channel object const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); if (!channel.has_value() || !channel->IsThread()) return; auto parent_iter = GetIteratorForChannelFromID(*channel->ParentID); @@ -960,6 +981,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..53a68c9 100644 --- a/src/components/channels.hpp +++ b/src/components/channels.hpp @@ -19,7 +19,7 @@ public: ChannelList(); void UpdateListing(); - void SetActiveChannel(Snowflake id); + void SetActiveChannel(Snowflake id, bool expand_to); // channel list should be populated when this is called void UseExpansionState(const ExpansionStateRoot &state); @@ -121,11 +121,19 @@ 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; Gtk::MenuItem m_menu_dm_toggle_mute; +#ifdef WITH_LIBHANDY + Gtk::MenuItem m_menu_dm_open_tab; +#endif + Gtk::Menu m_menu_thread; Gtk::MenuItem m_menu_thread_copy_id; Gtk::MenuItem m_menu_thread_leave; @@ -149,16 +157,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..f7b0226 --- /dev/null +++ b/src/components/channeltabswitcherhandy.cpp @@ -0,0 +1,202 @@ +#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->GetDisplayName().c_str()); + hdy_tab_page_set_tooltip(page, nullptr); + + m_pages[id] = page; + m_pages_rev[page] = id; + + CheckUnread(id); + CheckPageIcon(page, *channel); + AppendPageHistory(page, id); +} + +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); + AppendPageHistory(page, id); + } +} + +TabsState ChannelTabSwitcherHandy::GetTabsState() { + TabsState state; + + const gint num_pages = hdy_tab_view_get_n_pages(m_tab_view); + for (gint i = 0; i < num_pages; i++) { + auto *page = hdy_tab_view_get_nth_page(m_tab_view, i); + if (page != nullptr) { + if (const auto it = m_pages_rev.find(page); it != m_pages_rev.end()) { + state.Channels.push_back(it->second); + } + } + } + + return state; +} + +void ChannelTabSwitcherHandy::UseTabsState(const TabsState &state) { + for (auto id : state.Channels) { + AddChannelTab(id); + } +} + +void ChannelTabSwitcherHandy::GoBackOnCurrent() { + AdvanceOnCurrent(-1); +} + +void ChannelTabSwitcherHandy::GoForwardOnCurrent() { + AdvanceOnCurrent(1); +} + +int ChannelTabSwitcherHandy::GetNumberOfTabs() const { + return hdy_tab_view_get_n_pages(m_tab_view); +} + +void ChannelTabSwitcherHandy::CheckUnread(Snowflake id) { + if (auto it = m_pages.find(id); it != m_pages.end()) { + auto &discord = Abaddon::Get().GetDiscordClient(); + const bool has_unreads = discord.GetUnreadStateForChannel(id) > -1; + const bool show_indicator = has_unreads && !discord.IsChannelMuted(id); + hdy_tab_page_set_needs_attention(it->second, show_indicator); + } +} + +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); +} + +void ChannelTabSwitcherHandy::AppendPageHistory(HdyTabPage *page, Snowflake channel) { + auto it = m_page_history.find(page); + if (it == m_page_history.end()) { + m_page_history[page] = PageHistory { { channel }, 0 }; + return; + } + + // drop everything beyond current position + it->second.Visited.resize(++it->second.CurrentVisitedIndex); + it->second.Visited.push_back(channel); +} + +void ChannelTabSwitcherHandy::AdvanceOnCurrent(size_t by) { + auto *current = hdy_tab_view_get_selected_page(m_tab_view); + if (current == nullptr) return; + auto history = m_page_history.find(current); + if (history == m_page_history.end()) return; + if (by + history->second.CurrentVisitedIndex < 0 || by + history->second.CurrentVisitedIndex >= history->second.Visited.size()) return; + + history->second.CurrentVisitedIndex += by; + const auto to_id = history->second.Visited.at(history->second.CurrentVisitedIndex); + + // temporarily point current index to the end so that it doesnt fuck up the history + // remove it immediately after cuz the emit will call ReplaceActiveTab + const auto real = history->second.CurrentVisitedIndex; + history->second.CurrentVisitedIndex = history->second.Visited.size() - 1; + m_signal_channel_switched_to.emit(to_id); + // iterator might not be valid + history = m_page_history.find(current); + if (history != m_page_history.end()) { + history->second.Visited.pop_back(); + } + history->second.CurrentVisitedIndex = real; +} + +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..561d463 --- /dev/null +++ b/src/components/channeltabswitcherhandy.hpp @@ -0,0 +1,66 @@ +#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" + #include "state.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); + TabsState GetTabsState(); + void UseTabsState(const TabsState &state); + + void GoBackOnCurrent(); + void GoForwardOnCurrent(); + + [[nodiscard]] int GetNumberOfTabs() const; + +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); + void AppendPageHistory(HdyTabPage *page, Snowflake channel); + void AdvanceOnCurrent(size_t by); + + 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; + + struct PageHistory { + std::vector<Snowflake> Visited; + size_t CurrentVisitedIndex; + }; + + std::unordered_map<HdyTabPage *, PageHistory> m_page_history; + + 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..8667488 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, false); + }); +#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); @@ -55,7 +65,7 @@ ChatWindow::ChatWindow() { m_completer.show(); m_chat->signal_action_channel_click().connect([this](Snowflake id) { - m_signal_action_channel_click.emit(id); + m_signal_action_channel_click.emit(id, true); }); m_chat->signal_action_chat_load_history().connect([this](Snowflake id) { m_signal_action_chat_load_history.emit(id); @@ -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,32 @@ void ChatWindow::SetTopic(const std::string &text) { m_topic.set_visible(text.length() > 0); } +#ifdef WITH_LIBHANDY +void ChatWindow::OpenNewTab(Snowflake id) { + // open if its the first tab (in which case it really isnt a tab but whatever) + if (m_tab_switcher->GetNumberOfTabs() == 0) { + m_signal_action_channel_click.emit(id, false); + } + m_tab_switcher->AddChannelTab(id); +} + +TabsState ChatWindow::GetTabsState() { + return m_tab_switcher->GetTabsState(); +} + +void ChatWindow::UseTabsState(const TabsState &state) { + m_tab_switcher->UseTabsState(state); +} + +void ChatWindow::GoBack() { + m_tab_switcher->GoBackOnCurrent(); +} + +void ChatWindow::GoForward() { + m_tab_switcher->GoForwardOnCurrent(); +} +#endif + Snowflake ChatWindow::GetActiveChannel() const { return m_active_channel; } diff --git a/src/components/chatwindow.hpp b/src/components/chatwindow.hpp index 0f40e88..1c0b7cc 100644 --- a/src/components/chatwindow.hpp +++ b/src/components/chatwindow.hpp @@ -4,6 +4,11 @@ #include <set> #include "discord/discord.hpp" #include "completer.hpp" +#include "state.hpp" + +#ifdef WITH_LIBHANDY +class ChannelTabSwitcherHandy; +#endif class ChatMessageHeader; class ChatMessageItemContainer; @@ -25,11 +30,19 @@ 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); + TabsState GetTabsState(); + void UseTabsState(const TabsState &state); + void GoBack(); + void GoForward(); +#endif + protected: bool m_is_replying = false; Snowflake m_replying_to; @@ -62,14 +75,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, bool>; + 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/state.cpp b/src/state.cpp index 043d181..bf4ab0f 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -24,9 +24,18 @@ void from_json(const nlohmann::json &j, ExpansionState &m) { j.at("c").get_to(m.Children); } +void to_json(nlohmann::json &j, const TabsState &m) { + j = m.Channels; +} + +void from_json(const nlohmann::json &j, TabsState &m) { + j.get_to(m.Channels); +} + void to_json(nlohmann::json &j, const AbaddonApplicationState &m) { j["active_channel"] = m.ActiveChannel; j["expansion"] = m.Expansion; + j["tabs"] = m.Tabs; } void from_json(const nlohmann::json &j, AbaddonApplicationState &m) { @@ -34,4 +43,6 @@ void from_json(const nlohmann::json &j, AbaddonApplicationState &m) { j.at("active_channel").get_to(m.ActiveChannel); if (j.contains("expansion")) j.at("expansion").get_to(m.Expansion); + if (j.contains("tabs")) + j.at("tabs").get_to(m.Tabs); } diff --git a/src/state.hpp b/src/state.hpp index 230808f..81c36d2 100644 --- a/src/state.hpp +++ b/src/state.hpp @@ -1,3 +1,4 @@ +#pragma once #include <vector> #include <nlohmann/json.hpp> #include "discord/snowflake.hpp" @@ -18,9 +19,17 @@ struct ExpansionState { friend void from_json(const nlohmann::json &j, ExpansionState &m); }; +struct TabsState { + std::vector<Snowflake> Channels; + + friend void to_json(nlohmann::json &j, const TabsState &m); + friend void from_json(const nlohmann::json &j, TabsState &m); +}; + struct AbaddonApplicationState { Snowflake ActiveChannel; ExpansionStateRoot Expansion; + TabsState Tabs; friend void to_json(nlohmann::json &j, const AbaddonApplicationState &m); friend void from_json(const nlohmann::json &j, AbaddonApplicationState &m); diff --git a/src/windows/mainwindow.cpp b/src/windows/mainwindow.cpp index edd485d..b77981c 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(); @@ -99,10 +105,10 @@ void MainWindow::UpdateChatWindowContents() { m_members.UpdateMemberList(); } -void MainWindow::UpdateChatActiveChannel(Snowflake id) { +void MainWindow::UpdateChatActiveChannel(Snowflake id, bool expand_to) { m_chat.SetActiveChannel(id); m_members.SetActiveChannel(id); - m_channel_list.SetActiveChannel(id); + m_channel_list.SetActiveChannel(id, expand_to); m_content_stack.set_visible_child("chat"); } @@ -151,6 +157,16 @@ void MainWindow::UpdateMenus() { OnViewSubmenuPopup(); } +#ifdef WITH_LIBHANDY +void MainWindow::GoBack() { + m_chat.GoBack(); +} + +void MainWindow::GoForward() { + m_chat.GoForward(); +} +#endif + void MainWindow::OnDiscordSubmenuPopup() { auto &discord = Abaddon::Get().GetDiscordClient(); auto channel_id = GetChatActiveChannel(); @@ -236,10 +252,20 @@ void MainWindow::SetupMenu() { m_menu_view_threads.set_label("Threads"); m_menu_view_mark_guild_as_read.set_label("Mark Server as Read"); m_menu_view_mark_guild_as_read.add_accelerator("activate", m_accels, GDK_KEY_Escape, Gdk::SHIFT_MASK, Gtk::ACCEL_VISIBLE); +#ifdef WITH_LIBHANDY + m_menu_view_go_back.set_label("Go Back"); + m_menu_view_go_forward.set_label("Go Forward"); + m_menu_view_go_back.add_accelerator("activate", m_accels, GDK_KEY_Left, Gdk::MOD1_MASK, Gtk::ACCEL_VISIBLE); + m_menu_view_go_forward.add_accelerator("activate", m_accels, GDK_KEY_Right, Gdk::MOD1_MASK, Gtk::ACCEL_VISIBLE); +#endif m_menu_view_sub.append(m_menu_view_friends); m_menu_view_sub.append(m_menu_view_pins); m_menu_view_sub.append(m_menu_view_threads); m_menu_view_sub.append(m_menu_view_mark_guild_as_read); +#ifdef WITH_LIBHANDY + m_menu_view_sub.append(m_menu_view_go_back); + m_menu_view_sub.append(m_menu_view_go_forward); +#endif m_menu_bar.append(m_menu_file); m_menu_bar.append(m_menu_discord); @@ -279,7 +305,7 @@ void MainWindow::SetupMenu() { }); m_menu_view_friends.signal_activate().connect([this] { - UpdateChatActiveChannel(Snowflake::Invalid); + UpdateChatActiveChannel(Snowflake::Invalid, true); m_members.UpdateMemberList(); m_content_stack.set_visible_child("friends"); }); @@ -300,6 +326,16 @@ void MainWindow::SetupMenu() { discord.MarkGuildAsRead(*channel->GuildID, NOOP_CALLBACK); } }); + +#ifdef WITH_LIBHANDY + m_menu_view_go_back.signal_activate().connect([this] { + GoBack(); + }); + + m_menu_view_go_forward.signal_activate().connect([this] { + GoForward(); + }); +#endif } MainWindow::type_signal_action_connect MainWindow::signal_action_connect() { diff --git a/src/windows/mainwindow.hpp b/src/windows/mainwindow.hpp index 0932af5..ce3a576 100644 --- a/src/windows/mainwindow.hpp +++ b/src/windows/mainwindow.hpp @@ -13,7 +13,7 @@ public: void UpdateMembers(); void UpdateChannelListing(); void UpdateChatWindowContents(); - void UpdateChatActiveChannel(Snowflake id); + void UpdateChatActiveChannel(Snowflake id, bool expand_to); Snowflake GetChatActiveChannel() const; void UpdateChatNewMessage(const Message &data); void UpdateChatMessageDeleted(Snowflake id, Snowflake channel_id); @@ -25,6 +25,11 @@ public: void UpdateChatReactionRemove(Snowflake id, const Glib::ustring ¶m); void UpdateMenus(); +#ifdef WITH_LIBHANDY + void GoBack(); + void GoForward(); +#endif + ChannelList *GetChannelList(); ChatWindow *GetChatWindow(); MemberList *GetMemberList(); @@ -68,6 +73,10 @@ private: Gtk::MenuItem m_menu_view_pins; Gtk::MenuItem m_menu_view_threads; Gtk::MenuItem m_menu_view_mark_guild_as_read; +#ifdef WITH_LIBHANDY + Gtk::MenuItem m_menu_view_go_back; + Gtk::MenuItem m_menu_view_go_forward; +#endif void OnViewSubmenuPopup(); public: |