diff options
Diffstat (limited to 'src/components')
-rw-r--r-- | src/components/channels.cpp | 210 | ||||
-rw-r--r-- | src/components/channels.hpp | 33 | ||||
-rw-r--r-- | src/components/channelscellrenderer.cpp | 100 | ||||
-rw-r--r-- | src/components/channelscellrenderer.hpp | 30 | ||||
-rw-r--r-- | src/components/statusindicator.hpp | 4 | ||||
-rw-r--r-- | src/components/voiceinfobox.cpp | 99 | ||||
-rw-r--r-- | src/components/voiceinfobox.hpp | 20 | ||||
-rw-r--r-- | src/components/volumemeter.cpp | 125 | ||||
-rw-r--r-- | src/components/volumemeter.hpp | 31 |
9 files changed, 628 insertions, 24 deletions
diff --git a/src/components/channels.cpp b/src/components/channels.cpp index b691ab5..0da3de9 100644 --- a/src/components/channels.cpp +++ b/src/components/channels.cpp @@ -19,8 +19,16 @@ ChannelList::ChannelList() , m_menu_channel_open_tab("Open in New _Tab", true) , m_menu_dm_open_tab("Open in New _Tab", true) #endif +#ifdef WITH_VOICE + , m_menu_voice_channel_join("_Join", true) + , m_menu_voice_channel_disconnect("_Disconnect", true) +#endif , m_menu_dm_copy_id("_Copy ID", true) , m_menu_dm_close("") // changes depending on if group or not +#ifdef WITH_VOICE + , m_menu_dm_join_voice("Join _Voice", true) + , m_menu_dm_disconnect_voice("_Disconnect Voice", true) +#endif , m_menu_thread_copy_id("_Copy ID", true) , m_menu_thread_leave("_Leave", true) , m_menu_thread_archive("_Archive", true) @@ -34,7 +42,11 @@ ChannelList::ChannelList() const auto type = row[m_columns.m_type]; // text channels should not be allowed to be collapsed // maybe they should be but it seems a little difficult to handle expansion to permit this +#ifdef WITH_VOICE + if (type != RenderType::TextChannel && type != RenderType::VoiceChannel) { +#else if (type != RenderType::TextChannel) { +#endif if (row[m_columns.m_expanded]) { m_view.collapse_row(path); row[m_columns.m_expanded] = false; @@ -44,7 +56,11 @@ ChannelList::ChannelList() } } +#ifdef WITH_VOICE + if (type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread || type == RenderType::VoiceChannel) { +#else if (type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread) { +#endif const auto id = static_cast<Snowflake>(row[m_columns.m_id]); m_signal_action_channel_item_select.emit(id); Abaddon::Get().GetDiscordClient().MarkChannelAsRead(id, [](...) {}); @@ -160,6 +176,21 @@ ChannelList::ChannelList() m_menu_channel.append(m_menu_channel_copy_id); m_menu_channel.show_all(); +#ifdef WITH_VOICE + m_menu_voice_channel_join.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_join_voice_channel.emit(id); + }); + + m_menu_voice_channel_disconnect.signal_activate().connect([this]() { + m_signal_action_disconnect_voice.emit(); + }); + + m_menu_voice_channel.append(m_menu_voice_channel_join); + m_menu_voice_channel.append(m_menu_voice_channel_disconnect); + m_menu_voice_channel.show_all(); +#endif + m_menu_dm_copy_id.signal_activate().connect([this] { Gtk::Clipboard::get()->set_text(std::to_string((*m_model->get_iter(m_path_for_menu))[m_columns.m_id])); }); @@ -191,6 +222,17 @@ ChannelList::ChannelList() #endif m_menu_dm.append(m_menu_dm_toggle_mute); m_menu_dm.append(m_menu_dm_close); +#ifdef WITH_VOICE + m_menu_dm_join_voice.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_join_voice_channel.emit(id); + }); + m_menu_dm_disconnect_voice.signal_activate().connect([this]() { + m_signal_action_disconnect_voice.emit(); + }); + m_menu_dm.append(m_menu_dm_join_voice); + m_menu_dm.append(m_menu_dm_disconnect_voice); +#endif m_menu_dm.append(m_menu_dm_copy_id); m_menu_dm.show_all(); @@ -244,6 +286,11 @@ ChannelList::ChannelList() discord.signal_channel_unmuted().connect(sigc::mem_fun(*this, &ChannelList::OnChannelUnmute)); discord.signal_guild_muted().connect(sigc::mem_fun(*this, &ChannelList::OnGuildMute)); discord.signal_guild_unmuted().connect(sigc::mem_fun(*this, &ChannelList::OnGuildUnmute)); + +#if WITH_VOICE + discord.signal_voice_user_connect().connect(sigc::mem_fun(*this, &ChannelList::OnVoiceUserConnect)); + discord.signal_voice_user_disconnect().connect(sigc::mem_fun(*this, &ChannelList::OnVoiceUserDisconnect)); +#endif } void ChannelList::UsePanedHack(Gtk::Paned &paned) { @@ -332,13 +379,13 @@ void ChannelList::UpdateRemoveGuild(Snowflake id) { } void ChannelList::UpdateRemoveChannel(Snowflake id) { - auto iter = GetIteratorForChannelFromID(id); + auto iter = GetIteratorForRowFromID(id); if (!iter) return; m_model->erase(iter); } void ChannelList::UpdateChannel(Snowflake id) { - auto iter = GetIteratorForChannelFromID(id); + auto iter = GetIteratorForRowFromID(id); auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); if (!iter || !channel.has_value()) return; if (channel->Type == ChannelType::GUILD_CATEGORY) return UpdateChannelCategory(*channel); @@ -353,7 +400,7 @@ void ChannelList::UpdateChannel(Snowflake id) { // check if the parent has changed Gtk::TreeModel::iterator new_parent; if (channel->ParentID.has_value()) - new_parent = GetIteratorForChannelFromID(*channel->ParentID); + new_parent = GetIteratorForRowFromID(*channel->ParentID); else if (channel->GuildID.has_value()) new_parent = GetIteratorForGuildFromID(*channel->GuildID); @@ -370,7 +417,7 @@ void ChannelList::UpdateCreateChannel(const ChannelData &channel) { bool orphan; if (channel.ParentID.has_value()) { orphan = false; - auto iter = GetIteratorForChannelFromID(*channel.ParentID); + auto iter = GetIteratorForRowFromID(*channel.ParentID); channel_row = *m_model->append(iter->children()); } else { orphan = true; @@ -412,10 +459,10 @@ void ChannelList::UpdateGuild(Snowflake id) { } void ChannelList::OnThreadJoined(Snowflake id) { - if (GetIteratorForChannelFromID(id)) return; + if (GetIteratorForRowFromID(id)) return; const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); if (!channel.has_value()) return; - const auto parent = GetIteratorForChannelFromID(*channel->ParentID); + const auto parent = GetIteratorForRowFromID(*channel->ParentID); if (parent) CreateThreadRow(parent->children(), *channel); } @@ -430,7 +477,7 @@ void ChannelList::OnThreadDelete(const ThreadDeleteData &data) { // todo probably make the row stick around if its selected until the selection changes void ChannelList::OnThreadUpdate(const ThreadUpdateData &data) { - auto iter = GetIteratorForChannelFromID(data.Thread.ID); + auto iter = GetIteratorForRowFromID(data.Thread.ID); if (iter) (*iter)[m_columns.m_name] = "- " + Glib::Markup::escape_text(*data.Thread.Name); @@ -459,7 +506,7 @@ void ChannelList::OnThreadListSync(const ThreadListSyncData &data) { // delete all threads not present in the synced data for (auto thread_id : threads) { if (std::find_if(data.Threads.begin(), data.Threads.end(), [thread_id](const auto &x) { return x.ID == thread_id; }) == data.Threads.end()) { - auto iter = GetIteratorForChannelFromID(thread_id); + auto iter = GetIteratorForRowFromID(thread_id); m_model->erase(iter); } } @@ -467,25 +514,49 @@ void ChannelList::OnThreadListSync(const ThreadListSyncData &data) { // delete all archived threads for (auto thread : data.Threads) { if (thread.ThreadMetadata->IsArchived) { - if (auto iter = GetIteratorForChannelFromID(thread.ID)) + if (auto iter = GetIteratorForRowFromID(thread.ID)) m_model->erase(iter); } } } +#ifdef WITH_VOICE +void ChannelList::OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id) { + auto parent_iter = GetIteratorForRowFromIDOfType(channel_id, RenderType::VoiceChannel); + if (!parent_iter) return; + const auto user = Abaddon::Get().GetDiscordClient().GetUser(user_id); + if (!user.has_value()) return; + + auto user_row = *m_model->append(parent_iter->children()); + user_row[m_columns.m_type] = RenderType::VoiceParticipant; + user_row[m_columns.m_id] = user_id; + if (user.has_value()) { + user_row[m_columns.m_name] = user->GetEscapedName(); + } else { + user_row[m_columns.m_name] = "<i>Unknown</i>"; + } +} + +void ChannelList::OnVoiceUserDisconnect(Snowflake user_id, Snowflake channel_id) { + if (auto iter = GetIteratorForRowFromIDOfType(user_id, RenderType::VoiceParticipant)) { + m_model->erase(iter); + } +} +#endif + void ChannelList::DeleteThreadRow(Snowflake id) { - auto iter = GetIteratorForChannelFromID(id); + auto iter = GetIteratorForRowFromID(id); if (iter) m_model->erase(iter); } void ChannelList::OnChannelMute(Snowflake id) { - if (auto iter = GetIteratorForChannelFromID(id)) + if (auto iter = GetIteratorForRowFromID(id)) m_model->row_changed(m_model->get_path(iter), iter); } void ChannelList::OnChannelUnmute(Snowflake id) { - if (auto iter = GetIteratorForChannelFromID(id)) + if (auto iter = GetIteratorForRowFromID(id)) m_model->row_changed(m_model->get_path(iter), iter); } @@ -516,7 +587,7 @@ void ChannelList::SetActiveChannel(Snowflake id, bool expand_to) { m_temporary_thread_row = {}; } - const auto channel_iter = GetIteratorForChannelFromID(id); + const auto channel_iter = GetIteratorForRowFromID(id); if (channel_iter) { if (expand_to) { m_view.expand_to_path(m_model->get_path(channel_iter)); @@ -526,7 +597,7 @@ void ChannelList::SetActiveChannel(Snowflake id, bool expand_to) { m_view.get_selection()->unselect_all(); const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(id); if (!channel.has_value() || !channel->IsThread()) return; - auto parent_iter = GetIteratorForChannelFromID(*channel->ParentID); + auto parent_iter = GetIteratorForRowFromID(*channel->ParentID); if (!parent_iter) return; m_temporary_thread_row = CreateThreadRow(parent_iter->children(), *channel); m_view.get_selection()->select(m_temporary_thread_row); @@ -662,7 +733,11 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild, const Gtk for (const auto &channel_ : *guild.Channels) { const auto channel = discord.GetChannel(channel_.ID); if (!channel.has_value()) continue; +#ifdef WITH_VOICE + if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS || channel->Type == ChannelType::GUILD_VOICE) { +#else if (channel->Type == ChannelType::GUILD_TEXT || channel->Type == ChannelType::GUILD_NEWS) { +#endif if (channel->ParentID.has_value()) categories[*channel->ParentID].push_back(*channel); else @@ -688,9 +763,31 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild, const Gtk m_tmp_row_map[thread.ID] = CreateThreadRow(row.children(), thread); }; + auto add_voice_participants = [this, &discord](const ChannelData &channel, const Gtk::TreeNodeChildren &root) { + for (auto user_id : discord.GetUsersInVoiceChannel(channel.ID)) { + const auto user = discord.GetUser(user_id); + + auto user_row = *m_model->append(root); + user_row[m_columns.m_type] = RenderType::VoiceParticipant; + user_row[m_columns.m_id] = user_id; + if (user.has_value()) { + user_row[m_columns.m_name] = user->GetEscapedName(); + } else { + user_row[m_columns.m_name] = "<i>Unknown</i>"; + } + } + }; + for (const auto &channel : orphan_channels) { auto channel_row = *m_model->append(guild_row.children()); - channel_row[m_columns.m_type] = RenderType::TextChannel; + if (IsTextChannel(channel.Type)) + channel_row[m_columns.m_type] = RenderType::TextChannel; +#ifdef WITH_VOICE + else { + channel_row[m_columns.m_type] = RenderType::VoiceChannel; + add_voice_participants(channel, channel_row->children()); + } +#endif channel_row[m_columns.m_id] = channel.ID; channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name); channel_row[m_columns.m_sort] = *channel.Position + OrphanChannelSortOffset; @@ -713,7 +810,14 @@ Gtk::TreeModel::iterator ChannelList::AddGuild(const GuildData &guild, const Gtk for (const auto &channel : channels) { auto channel_row = *m_model->append(cat_row.children()); - channel_row[m_columns.m_type] = RenderType::TextChannel; + if (IsTextChannel(channel.Type)) + channel_row[m_columns.m_type] = RenderType::TextChannel; +#ifdef WITH_VOICE + else { + channel_row[m_columns.m_type] = RenderType::VoiceChannel; + add_voice_participants(channel, channel_row->children()); + } +#endif channel_row[m_columns.m_id] = channel.ID; channel_row[m_columns.m_name] = "#" + Glib::Markup::escape_text(*channel.Name); channel_row[m_columns.m_sort] = *channel.Position; @@ -753,7 +857,7 @@ Gtk::TreeModel::iterator ChannelList::CreateThreadRow(const Gtk::TreeNodeChildre } void ChannelList::UpdateChannelCategory(const ChannelData &channel) { - auto iter = GetIteratorForChannelFromID(channel.ID); + auto iter = GetIteratorForRowFromID(channel.ID); if (!iter) return; (*iter)[m_columns.m_sort] = *channel.Position; @@ -791,7 +895,7 @@ Gtk::TreeModel::iterator ChannelList::GetIteratorForGuildFromID(Snowflake id) { return {}; } -Gtk::TreeModel::iterator ChannelList::GetIteratorForChannelFromID(Snowflake id) { +Gtk::TreeModel::iterator ChannelList::GetIteratorForRowFromID(Snowflake id) { std::queue<Gtk::TreeModel::iterator> queue; for (const auto &child : m_model->children()) for (const auto &child2 : child.children()) @@ -808,6 +912,23 @@ Gtk::TreeModel::iterator ChannelList::GetIteratorForChannelFromID(Snowflake id) return {}; } +Gtk::TreeModel::iterator ChannelList::GetIteratorForRowFromIDOfType(Snowflake id, RenderType type) { + std::queue<Gtk::TreeModel::iterator> queue; + for (const auto &child : m_model->children()) + for (const auto &child2 : child.children()) + queue.push(child2); + + while (!queue.empty()) { + auto item = queue.front(); + if ((*item)[m_columns.m_type] == type && (*item)[m_columns.m_id] == id) return item; + for (const auto &child : item->children()) + queue.push(child); + queue.pop(); + } + + return {}; +} + bool ChannelList::IsTextChannel(ChannelType type) { return type == ChannelType::GUILD_TEXT || type == ChannelType::GUILD_NEWS; } @@ -838,7 +959,11 @@ bool ChannelList::SelectionFunc(const Glib::RefPtr<Gtk::TreeModel> &model, const m_last_selected = m_model->get_path(row); auto type = (*m_model->get_iter(path))[m_columns.m_type]; +#ifdef WITH_VOICE + return type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread || type == RenderType::VoiceChannel; +#else return type == RenderType::TextChannel || type == RenderType::DM || type == RenderType::Thread; +#endif } void ChannelList::AddPrivateChannels() { @@ -914,7 +1039,7 @@ void ChannelList::UpdateCreateDMChannel(const ChannelData &dm) { void ChannelList::OnMessageAck(const MessageAckData &data) { // trick renderer into redrawing m_model->row_changed(Gtk::TreeModel::Path("0"), m_model->get_iter("0")); // 0 is always path for dm header - auto iter = GetIteratorForChannelFromID(data.ChannelID); + auto iter = GetIteratorForRowFromID(data.ChannelID); if (iter) m_model->row_changed(m_model->get_path(iter), iter); auto channel = Abaddon::Get().GetDiscordClient().GetChannel(data.ChannelID); if (channel.has_value() && channel->GuildID.has_value()) { @@ -924,7 +1049,7 @@ void ChannelList::OnMessageAck(const MessageAckData &data) { } void ChannelList::OnMessageCreate(const Message &msg) { - auto iter = GetIteratorForChannelFromID(msg.ChannelID); + auto iter = GetIteratorForRowFromID(msg.ChannelID); if (iter) m_model->row_changed(m_model->get_path(iter), iter); // redraw const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(msg.ChannelID); if (!channel.has_value()) return; @@ -954,6 +1079,12 @@ bool ChannelList::OnButtonPressEvent(GdkEventButton *ev) { OnChannelSubmenuPopup(); m_menu_channel.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev)); break; +#ifdef WITH_VOICE + case RenderType::VoiceChannel: + OnVoiceChannelSubmenuPopup(); + m_menu_voice_channel.popup_at_pointer(reinterpret_cast<GdkEvent *>(ev)); + break; +#endif case RenderType::DM: { OnDMSubmenuPopup(); const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(static_cast<Snowflake>(row[m_columns.m_id])); @@ -1046,14 +1177,41 @@ void ChannelList::OnChannelSubmenuPopup() { m_menu_channel_toggle_mute.set_label("Mute"); } +#ifdef WITH_VOICE +void ChannelList::OnVoiceChannelSubmenuPopup() { + const auto iter = m_model->get_iter(m_path_for_menu); + if (!iter) return; + const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]); + auto &discord = Abaddon::Get().GetDiscordClient(); + if (discord.IsVoiceConnected() || discord.IsVoiceConnecting()) { + m_menu_voice_channel_join.set_sensitive(false); + m_menu_voice_channel_disconnect.set_sensitive(discord.GetVoiceChannelID() == id); + } else { + m_menu_voice_channel_join.set_sensitive(true); + m_menu_voice_channel_disconnect.set_sensitive(false); + } +} +#endif + void ChannelList::OnDMSubmenuPopup() { auto iter = m_model->get_iter(m_path_for_menu); if (!iter) return; const auto id = static_cast<Snowflake>((*iter)[m_columns.m_id]); - if (Abaddon::Get().GetDiscordClient().IsChannelMuted(id)) + auto &discord = Abaddon::Get().GetDiscordClient(); + if (discord.IsChannelMuted(id)) m_menu_dm_toggle_mute.set_label("Unmute"); else m_menu_dm_toggle_mute.set_label("Mute"); + +#ifdef WITH_VOICE + if (discord.IsVoiceConnected() || discord.IsVoiceConnecting()) { + m_menu_dm_join_voice.set_sensitive(false); + m_menu_dm_disconnect_voice.set_sensitive(discord.GetVoiceChannelID() == id); + } else { + m_menu_dm_join_voice.set_sensitive(true); + m_menu_dm_disconnect_voice.set_sensitive(false); + } +#endif } void ChannelList::OnThreadSubmenuPopup() { @@ -1096,6 +1254,16 @@ ChannelList::type_signal_action_open_new_tab ChannelList::signal_action_open_new } #endif +#ifdef WITH_VOICE +ChannelList::type_signal_action_join_voice_channel ChannelList::signal_action_join_voice_channel() { + return m_signal_action_join_voice_channel; +} + +ChannelList::type_signal_action_disconnect_voice ChannelList::signal_action_disconnect_voice() { + return m_signal_action_disconnect_voice; +} +#endif + ChannelList::ModelColumns::ModelColumns() { add(m_type); add(m_id); diff --git a/src/components/channels.hpp b/src/components/channels.hpp index da006dc..36c7766 100644 --- a/src/components/channels.hpp +++ b/src/components/channels.hpp @@ -52,6 +52,9 @@ protected: void OnThreadUpdate(const ThreadUpdateData &data); void OnThreadListSync(const ThreadListSyncData &data); + void OnVoiceUserConnect(Snowflake user_id, Snowflake channel_id); + void OnVoiceUserDisconnect(Snowflake user_id, Snowflake channel_id); + Gtk::TreeView m_view; class ModelColumns : public Gtk::TreeModel::ColumnRecord { @@ -87,7 +90,8 @@ protected: // separation necessary because a channel and guild can share the same id Gtk::TreeModel::iterator GetIteratorForTopLevelFromID(Snowflake id); Gtk::TreeModel::iterator GetIteratorForGuildFromID(Snowflake id); - Gtk::TreeModel::iterator GetIteratorForChannelFromID(Snowflake id); + Gtk::TreeModel::iterator GetIteratorForRowFromID(Snowflake id); + Gtk::TreeModel::iterator GetIteratorForRowFromIDOfType(Snowflake id, RenderType type); bool IsTextChannel(ChannelType type); @@ -132,10 +136,20 @@ protected: Gtk::MenuItem m_menu_channel_open_tab; #endif +#ifdef WITH_VOICE + Gtk::Menu m_menu_voice_channel; + Gtk::MenuItem m_menu_voice_channel_join; + Gtk::MenuItem m_menu_voice_channel_disconnect; +#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_VOICE + Gtk::MenuItem m_menu_dm_join_voice; + Gtk::MenuItem m_menu_dm_disconnect_voice; +#endif #ifdef WITH_LIBHANDY Gtk::MenuItem m_menu_dm_open_tab; @@ -155,6 +169,10 @@ protected: void OnDMSubmenuPopup(); void OnThreadSubmenuPopup(); +#ifdef WITH_VOICE + void OnVoiceChannelSubmenuPopup(); +#endif + bool m_updating_listing = false; Snowflake m_active_channel; @@ -174,6 +192,14 @@ public: type_signal_action_open_new_tab signal_action_open_new_tab(); #endif +#ifdef WITH_VOICE + using type_signal_action_join_voice_channel = sigc::signal<void, Snowflake>; + using type_signal_action_disconnect_voice = sigc::signal<void>; + + type_signal_action_join_voice_channel signal_action_join_voice_channel(); + type_signal_action_disconnect_voice signal_action_disconnect_voice(); +#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(); @@ -186,4 +212,9 @@ private: #ifdef WITH_LIBHANDY type_signal_action_open_new_tab m_signal_action_open_new_tab; #endif + +#ifdef WITH_VOICE + type_signal_action_join_voice_channel m_signal_action_join_voice_channel; + type_signal_action_disconnect_voice m_signal_action_disconnect_voice; +#endif }; diff --git a/src/components/channelscellrenderer.cpp b/src/components/channelscellrenderer.cpp index 1fa4a1a..fb82682 100644 --- a/src/components/channelscellrenderer.cpp +++ b/src/components/channelscellrenderer.cpp @@ -70,6 +70,12 @@ void CellRendererChannels::get_preferred_width_vfunc(Gtk::Widget &widget, int &m return get_preferred_width_vfunc_channel(widget, minimum_width, natural_width); case RenderType::Thread: return get_preferred_width_vfunc_thread(widget, minimum_width, natural_width); +#ifdef WITH_VOICE + case RenderType::VoiceChannel: + return get_preferred_width_vfunc_voice_channel(widget, minimum_width, natural_width); + case RenderType::VoiceParticipant: + return get_preferred_width_vfunc_voice_participant(widget, minimum_width, natural_width); +#endif case RenderType::DMHeader: return get_preferred_width_vfunc_dmheader(widget, minimum_width, natural_width); case RenderType::DM: @@ -89,6 +95,12 @@ void CellRendererChannels::get_preferred_width_for_height_vfunc(Gtk::Widget &wid return get_preferred_width_for_height_vfunc_channel(widget, height, minimum_width, natural_width); case RenderType::Thread: return get_preferred_width_for_height_vfunc_thread(widget, height, minimum_width, natural_width); +#ifdef WITH_VOICE + case RenderType::VoiceChannel: + return get_preferred_width_for_height_vfunc_voice_channel(widget, height, minimum_width, natural_width); + case RenderType::VoiceParticipant: + return get_preferred_width_for_height_vfunc_voice_participant(widget, height, minimum_width, natural_width); +#endif case RenderType::DMHeader: return get_preferred_width_for_height_vfunc_dmheader(widget, height, minimum_width, natural_width); case RenderType::DM: @@ -108,6 +120,12 @@ void CellRendererChannels::get_preferred_height_vfunc(Gtk::Widget &widget, int & return get_preferred_height_vfunc_channel(widget, minimum_height, natural_height); case RenderType::Thread: return get_preferred_height_vfunc_thread(widget, minimum_height, natural_height); +#ifdef WITH_VOICE + case RenderType::VoiceChannel: + return get_preferred_height_vfunc_voice_channel(widget, minimum_height, natural_height); + case RenderType::VoiceParticipant: + return get_preferred_height_vfunc_voice_participant(widget, minimum_height, natural_height); +#endif case RenderType::DMHeader: return get_preferred_height_vfunc_dmheader(widget, minimum_height, natural_height); case RenderType::DM: @@ -127,6 +145,12 @@ void CellRendererChannels::get_preferred_height_for_width_vfunc(Gtk::Widget &wid return get_preferred_height_for_width_vfunc_channel(widget, width, minimum_height, natural_height); case RenderType::Thread: return get_preferred_height_for_width_vfunc_thread(widget, width, minimum_height, natural_height); +#ifdef WITH_VOICE + case RenderType::VoiceChannel: + return get_preferred_height_for_width_vfunc_voice_channel(widget, width, minimum_height, natural_height); + case RenderType::VoiceParticipant: + return get_preferred_height_for_width_vfunc_voice_participant(widget, width, minimum_height, natural_height); +#endif case RenderType::DMHeader: return get_preferred_height_for_width_vfunc_dmheader(widget, width, minimum_height, natural_height); case RenderType::DM: @@ -146,6 +170,12 @@ void CellRendererChannels::render_vfunc(const Cairo::RefPtr<Cairo::Context> &cr, return render_vfunc_channel(cr, widget, background_area, cell_area, flags); case RenderType::Thread: return render_vfunc_thread(cr, widget, background_area, cell_area, flags); +#ifdef WITH_VOICE + case RenderType::VoiceChannel: + return render_vfunc_voice_channel(cr, widget, background_area, cell_area, flags); + case RenderType::VoiceParticipant: + return render_vfunc_voice_participant(cr, widget, background_area, cell_area, flags); +#endif case RenderType::DMHeader: return render_vfunc_dmheader(cr, widget, background_area, cell_area, flags); case RenderType::DM: @@ -575,6 +605,76 @@ void CellRendererChannels::render_vfunc_thread(const Cairo::RefPtr<Cairo::Contex } } +#ifdef WITH_VOICE + +// voice channel + +void CellRendererChannels::get_preferred_width_vfunc_voice_channel(Gtk::Widget &widget, int &minimum_width, int &natural_width) const { + m_renderer_text.get_preferred_width(widget, minimum_width, natural_width); +} + +void CellRendererChannels::get_preferred_width_for_height_vfunc_voice_channel(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const { + m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width); +} + +void CellRendererChannels::get_preferred_height_vfunc_voice_channel(Gtk::Widget &widget, int &minimum_height, int &natural_height) const { + m_renderer_text.get_preferred_height(widget, minimum_height, natural_height); +} + +void CellRendererChannels::get_preferred_height_for_width_vfunc_voice_channel(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const { + m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height); +} + +void CellRendererChannels::render_vfunc_voice_channel(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) { + Gtk::Requisition minimum_size, natural_size; + m_renderer_text.get_preferred_size(widget, minimum_size, natural_size); + + const int text_x = background_area.get_x() + 21; + const int text_y = background_area.get_y() + background_area.get_height() / 2 - natural_size.height / 2; + const int text_w = natural_size.width; + const int text_h = natural_size.height; + + Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h); + m_renderer_text.property_foreground_rgba() = Gdk::RGBA("#0f0"); + m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); + m_renderer_text.property_foreground_set() = false; +} + +// voice participant + +void CellRendererChannels::get_preferred_width_vfunc_voice_participant(Gtk::Widget &widget, int &minimum_width, int &natural_width) const { + m_renderer_text.get_preferred_width(widget, minimum_width, natural_width); +} + +void CellRendererChannels::get_preferred_width_for_height_vfunc_voice_participant(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const { + m_renderer_text.get_preferred_width_for_height(widget, height, minimum_width, natural_width); +} + +void CellRendererChannels::get_preferred_height_vfunc_voice_participant(Gtk::Widget &widget, int &minimum_height, int &natural_height) const { + m_renderer_text.get_preferred_height(widget, minimum_height, natural_height); +} + +void CellRendererChannels::get_preferred_height_for_width_vfunc_voice_participant(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const { + m_renderer_text.get_preferred_height_for_width(widget, width, minimum_height, natural_height); +} + +void CellRendererChannels::render_vfunc_voice_participant(const Cairo::RefPtr<Cairo::Context> &cr, Gtk::Widget &widget, const Gdk::Rectangle &background_area, const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags) { + Gtk::Requisition minimum_size, natural_size; + m_renderer_text.get_preferred_size(widget, minimum_size, natural_size); + + const int text_x = background_area.get_x() + 27; + const int text_y = background_area.get_y() + background_area.get_height() / 2 - natural_size.height / 2; + const int text_w = natural_size.width; + const int text_h = natural_size.height; + + Gdk::Rectangle text_cell_area(text_x, text_y, text_w, text_h); + m_renderer_text.property_foreground_rgba() = Gdk::RGBA("#f00"); + m_renderer_text.render(cr, widget, background_area, text_cell_area, flags); + m_renderer_text.property_foreground_set() = false; +} + +#endif + // dm header void CellRendererChannels::get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const { diff --git a/src/components/channelscellrenderer.hpp b/src/components/channelscellrenderer.hpp index 00c537b..f6859dc 100644 --- a/src/components/channelscellrenderer.hpp +++ b/src/components/channelscellrenderer.hpp @@ -11,6 +11,12 @@ enum class RenderType : uint8_t { TextChannel, Thread, +// TODO: maybe enable anyways but without ability to join if no voice support +#ifdef WITH_VOICE + VoiceChannel, + VoiceParticipant, +#endif + DMHeader, DM, }; @@ -95,6 +101,30 @@ protected: const Gdk::Rectangle &cell_area, Gtk::CellRendererState flags); +#ifdef WITH_VOICE + // voice channel + void get_preferred_width_vfunc_voice_channel(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; + void get_preferred_width_for_height_vfunc_voice_channel(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; + void get_preferred_height_vfunc_voice_channel(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; + void get_preferred_height_for_width_vfunc_voice_channel(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; + void render_vfunc_voice_channel(const Cairo::RefPtr<Cairo::Context> &cr, + Gtk::Widget &widget, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags); + + // voice channel + void get_preferred_width_vfunc_voice_participant(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; + void get_preferred_width_for_height_vfunc_voice_participant(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; + void get_preferred_height_vfunc_voice_participant(Gtk::Widget &widget, int &minimum_height, int &natural_height) const; + void get_preferred_height_for_width_vfunc_voice_participant(Gtk::Widget &widget, int width, int &minimum_height, int &natural_height) const; + void render_vfunc_voice_participant(const Cairo::RefPtr<Cairo::Context> &cr, + Gtk::Widget &widget, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags); +#endif + // dm header void get_preferred_width_vfunc_dmheader(Gtk::Widget &widget, int &minimum_width, int &natural_width) const; void get_preferred_width_for_height_vfunc_dmheader(Gtk::Widget &widget, int height, int &minimum_width, int &natural_width) const; diff --git a/src/components/statusindicator.hpp b/src/components/statusindicator.hpp index 1fa8b9b..edd64ea 100644 --- a/src/components/statusindicator.hpp +++ b/src/components/statusindicator.hpp @@ -10,9 +10,9 @@ public: protected: Gtk::SizeRequestMode get_request_mode_vfunc() const override; void get_preferred_width_vfunc(int &minimum_width, int &natural_width) const override; - void get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const override; - void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override; void get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const override; + void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override; + void get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const override; void on_size_allocate(Gtk::Allocation &allocation) override; void on_map() override; void on_unmap() override; diff --git a/src/components/voiceinfobox.cpp b/src/components/voiceinfobox.cpp new file mode 100644 index 0000000..456c5f0 --- /dev/null +++ b/src/components/voiceinfobox.cpp @@ -0,0 +1,99 @@ +#include "voiceinfobox.hpp" +#include "abaddon.hpp" +#include "util.hpp" + +VoiceInfoBox::VoiceInfoBox() + : Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) + , m_left(Gtk::ORIENTATION_VERTICAL) { + m_disconnect_ev.signal_button_press_event().connect([this](GdkEventButton *ev) -> bool { + if (ev->type == GDK_BUTTON_PRESS && ev->button == GDK_BUTTON_PRIMARY) { + spdlog::get("discord")->debug("Request disconnect from info box"); + Abaddon::Get().GetDiscordClient().DisconnectFromVoice(); + return true; + } + + return false; + }); + + AddPointerCursor(m_disconnect_ev); + + get_style_context()->add_class("voice-info"); + m_status.get_style_context()->add_class("voice-info-status"); + m_location.get_style_context()->add_class("voice-info-location"); + m_disconnect_img.get_style_context()->add_class("voice-info-disconnect-image"); + + m_status.set_label("You shouldn't see me"); + m_location.set_label("You shouldn't see me"); + + Abaddon::Get().GetDiscordClient().signal_voice_requested_connect().connect([this](Snowflake channel_id) { + show(); + + if (const auto channel = Abaddon::Get().GetDiscordClient().GetChannel(channel_id); channel.has_value() && channel->Name.has_value()) { + if (channel->GuildID.has_value()) { + if (const auto guild = Abaddon::Get().GetDiscordClient().GetGuild(*channel->GuildID); guild.has_value()) { + m_location.set_label(*channel->Name + " / " + guild->Name); + return; + } + } + + m_location.set_label(*channel->Name); + return; + } + + m_location.set_label("Unknown"); + }); + + Abaddon::Get().GetDiscordClient().signal_voice_requested_disconnect().connect([this]() { + hide(); + }); + + Abaddon::Get().GetDiscordClient().signal_voice_client_state_update().connect([this](DiscordVoiceClient::State state) { + Glib::ustring label; + switch (state) { + case DiscordVoiceClient::State::ConnectingToWebsocket: + label = "Connecting"; + break; + case DiscordVoiceClient::State::EstablishingConnection: + label = "Establishing connection"; + break; + case DiscordVoiceClient::State::Connected: + label = "Connected"; + break; + case DiscordVoiceClient::State::DisconnectedByServer: + case DiscordVoiceClient::State::DisconnectedByClient: + label = "Disconnected"; + break; + default: + label = "Unknown"; + break; + } + m_status.set_label(label); + }); + + AddPointerCursor(m_status_ev); + m_status_ev.signal_button_press_event().connect([this](GdkEventButton *ev) -> bool { + if (ev->type == GDK_BUTTON_PRESS && ev->button == GDK_BUTTON_PRIMARY) { + Abaddon::Get().ShowVoiceWindow(); + return true; + } + return false; + }); + + m_status.set_ellipsize(Pango::ELLIPSIZE_END); + m_location.set_ellipsize(Pango::ELLIPSIZE_END); + + m_disconnect_ev.add(m_disconnect_img); + m_disconnect_img.property_icon_name() = "call-stop-symbolic"; + m_disconnect_img.property_icon_size() = 5; + m_disconnect_img.set_hexpand(true); + m_disconnect_img.set_halign(Gtk::ALIGN_END); + + m_status_ev.add(m_status); + m_left.add(m_status_ev); + m_left.add(m_location); + + add(m_left); + add(m_disconnect_ev); + + show_all_children(); +} diff --git a/src/components/voiceinfobox.hpp b/src/components/voiceinfobox.hpp new file mode 100644 index 0000000..0117c0d --- /dev/null +++ b/src/components/voiceinfobox.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include <gtkmm/box.h> +#include <gtkmm/eventbox.h> +#include <gtkmm/image.h> +#include <gtkmm/label.h> + +class VoiceInfoBox : public Gtk::Box { +public: + VoiceInfoBox(); + +private: + Gtk::Box m_left; + Gtk::EventBox m_status_ev; + Gtk::Label m_status; + Gtk::Label m_location; + + Gtk::EventBox m_disconnect_ev; + Gtk::Image m_disconnect_img; +}; diff --git a/src/components/volumemeter.cpp b/src/components/volumemeter.cpp new file mode 100644 index 0000000..380af55 --- /dev/null +++ b/src/components/volumemeter.cpp @@ -0,0 +1,125 @@ +#include "volumemeter.hpp" +#include <cstring> + +VolumeMeter::VolumeMeter() + : Glib::ObjectBase("volumemeter") + , Gtk::Widget() { + set_has_window(true); +} + +void VolumeMeter::SetVolume(double fraction) { + m_fraction = fraction; + queue_draw(); +} + +void VolumeMeter::SetTick(double fraction) { + m_tick = fraction; + queue_draw(); +} + +void VolumeMeter::SetShowTick(bool show) { + m_show_tick = show; +} + +Gtk::SizeRequestMode VolumeMeter::get_request_mode_vfunc() const { + return Gtk::Widget::get_request_mode_vfunc(); +} + +void VolumeMeter::get_preferred_width_vfunc(int &minimum_width, int &natural_width) const { + const int width = get_allocated_width(); + minimum_width = natural_width = width; +} + +void VolumeMeter::get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const { + get_preferred_width_vfunc(minimum_width, natural_width); +} + +void VolumeMeter::get_preferred_height_vfunc(int &minimum_height, int &natural_height) const { + // blehhh :PPP + const int height = get_allocated_height(); + minimum_height = natural_height = 4; +} + +void VolumeMeter::get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const { + get_preferred_height_vfunc(minimum_height, natural_height); +} + +void VolumeMeter::on_size_allocate(Gtk::Allocation &allocation) { + set_allocation(allocation); + + if (m_window) + m_window->move_resize(allocation.get_x(), allocation.get_y(), allocation.get_width(), allocation.get_height()); +} + +void VolumeMeter::on_map() { + Gtk::Widget::on_map(); +} + +void VolumeMeter::on_unmap() { + Gtk::Widget::on_unmap(); +} + +void VolumeMeter::on_realize() { + set_realized(true); + + if (!m_window) { + GdkWindowAttr attributes; + std::memset(&attributes, 0, sizeof(attributes)); + + auto allocation = get_allocation(); + + attributes.x = allocation.get_x(); + attributes.y = allocation.get_y(); + attributes.width = allocation.get_width(); + attributes.height = allocation.get_height(); + + attributes.event_mask = get_events() | Gdk::EXPOSURE_MASK; + attributes.window_type = GDK_WINDOW_CHILD; + attributes.wclass = GDK_INPUT_OUTPUT; + + m_window = Gdk::Window::create(get_parent_window(), &attributes, GDK_WA_X | GDK_WA_Y); + set_window(m_window); + + m_window->set_user_data(gobj()); + } +} + +void VolumeMeter::on_unrealize() { + m_window.reset(); + + Gtk::Widget::on_unrealize(); +} + +bool VolumeMeter::on_draw(const Cairo::RefPtr<Cairo::Context> &cr) { + const auto allocation = get_allocation(); + const auto width = allocation.get_width(); + const auto height = allocation.get_height(); + + const double LOW_MAX = 0.7 * width; + const double MID_MAX = 0.85 * width; + const double desired_width = width * m_fraction; + + const double draw_low = std::min(desired_width, LOW_MAX); + const double draw_mid = std::min(desired_width, MID_MAX); + const double draw_hi = desired_width; + + cr->set_source_rgb(1.0, 0.0, 0.0); + cr->rectangle(0.0, 0.0, draw_hi, height); + cr->fill(); + cr->set_source_rgb(1.0, 0.5, 0.0); + cr->rectangle(0.0, 0.0, draw_mid, height); + cr->fill(); + cr->set_source_rgb(.0, 1.0, 0.0); + cr->rectangle(0.0, 0.0, draw_low, height); + cr->fill(); + + if (m_show_tick) { + const double tick_base = width * m_tick; + + cr->set_source_rgb(0.8, 0.8, 0.8); + cr->rectangle(tick_base, 0, 4, height); + cr->fill(); + } + + return true; +} diff --git a/src/components/volumemeter.hpp b/src/components/volumemeter.hpp new file mode 100644 index 0000000..83e0861 --- /dev/null +++ b/src/components/volumemeter.hpp @@ -0,0 +1,31 @@ +#pragma once +#include <gtkmm/widget.h> + +class VolumeMeter : public Gtk::Widget { +public: + VolumeMeter(); + + void SetVolume(double fraction); + void SetTick(double fraction); + void SetShowTick(bool show); + +protected: + Gtk::SizeRequestMode get_request_mode_vfunc() const override; + void get_preferred_width_vfunc(int &minimum_width, int &natural_width) const override; + void get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const override; + void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override; + void get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const override; + void on_size_allocate(Gtk::Allocation &allocation) override; + void on_map() override; + void on_unmap() override; + void on_realize() override; + void on_unrealize() override; + bool on_draw(const Cairo::RefPtr<Cairo::Context> &cr) override; + +private: + Glib::RefPtr<Gdk::Window> m_window; + + double m_fraction = 0.0; + double m_tick = 0.0; + bool m_show_tick = false; +}; |