summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--components/draglistbox.cpp141
-rw-r--r--components/draglistbox.hpp45
-rw-r--r--css/main.css8
-rw-r--r--discord/discord.cpp52
-rw-r--r--discord/discord.hpp3
-rw-r--r--discord/httpclient.cpp1
-rw-r--r--discord/objects.cpp9
-rw-r--r--discord/objects.hpp12
-rw-r--r--windows/guildsettings/rolespane.cpp58
-rw-r--r--windows/guildsettings/rolespane.hpp3
10 files changed, 313 insertions, 19 deletions
diff --git a/components/draglistbox.cpp b/components/draglistbox.cpp
new file mode 100644
index 0000000..492abc3
--- /dev/null
+++ b/components/draglistbox.cpp
@@ -0,0 +1,141 @@
+#include "draglistbox.hpp"
+
+DragListBox::DragListBox() {
+ drag_dest_set(m_entries, Gtk::DEST_DEFAULT_MOTION | Gtk::DEST_DEFAULT_DROP, Gdk::ACTION_MOVE);
+}
+
+void DragListBox::row_drag_begin(Gtk::Widget *widget, const Glib::RefPtr<Gdk::DragContext> &context) {
+ m_drag_row = dynamic_cast<Gtk::ListBoxRow *>(widget->get_ancestor(GTK_TYPE_LIST_BOX_ROW));
+
+ auto alloc = m_drag_row->get_allocation();
+ auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, alloc.get_width(), alloc.get_height());
+ auto cr = Cairo::Context::create(surface);
+
+ m_drag_row->get_style_context()->add_class("drag-icon");
+ gtk_widget_draw(reinterpret_cast<GtkWidget *>(m_drag_row->gobj()), cr->cobj());
+ m_drag_row->get_style_context()->remove_class("drag-icon");
+
+ int x, y;
+ widget->translate_coordinates(*m_drag_row, 0, 0, x, y);
+ surface->set_device_offset(-x, -y);
+ context->set_icon(surface);
+}
+
+bool DragListBox::on_drag_motion(const Glib::RefPtr<Gdk::DragContext> &context, gint x, gint y, guint time) {
+ if (y > m_hover_top || y < m_hover_bottom) {
+ auto *row = get_row_at_y(y);
+ if (row == nullptr) return true;
+ const bool old_top = m_top;
+ const auto alloc = row->get_allocation();
+
+ const int hover_row_y = alloc.get_y();
+ const int hover_row_height = alloc.get_height();
+ if (row != m_drag_row) {
+ if (y < hover_row_y + hover_row_height / 2) {
+ m_hover_top = hover_row_y;
+ m_hover_bottom = m_hover_top + hover_row_height / 2;
+ row->get_style_context()->add_class("drag-hover-top");
+ row->get_style_context()->remove_class("drag-hover-bottom");
+ m_top = true;
+ } else {
+ m_hover_top = hover_row_y + hover_row_height / 2;
+ m_hover_bottom = hover_row_y + hover_row_height;
+ row->get_style_context()->add_class("drag-hover-bottom");
+ row->get_style_context()->remove_class("drag-hover-top");
+ m_top = false;
+ }
+ }
+
+ if (m_hover_row != nullptr && m_hover_row != row) {
+ if (old_top)
+ m_hover_row->get_style_context()->remove_class("drag-hover-top");
+ else
+ m_hover_row->get_style_context()->remove_class("drag-hover-bottom");
+ }
+
+ m_hover_row = row;
+ }
+
+ check_scroll(y);
+ if (m_should_scroll && !m_scrolling) {
+ m_scrolling = true;
+ Glib::signal_timeout().connect(sigc::mem_fun(*this, &DragListBox::scroll), SCROLL_DELAY);
+ }
+
+ return true;
+}
+
+void DragListBox::on_drag_leave(const Glib::RefPtr<Gdk::DragContext> &context, guint time) {
+ m_should_scroll = false;
+}
+
+void DragListBox::check_scroll(gint y) {
+ if (!get_focus_vadjustment())
+ return;
+
+ const double vadjustment_min = get_focus_vadjustment()->get_value();
+ const double vadjustment_max = get_focus_vadjustment()->get_page_size() + vadjustment_min;
+ const double show_min = std::max(0, y - SCROLL_DISTANCE);
+ const double show_max = std::min(get_focus_vadjustment()->get_upper(), static_cast<double>(y) + SCROLL_DISTANCE);
+ if (vadjustment_min > show_min) {
+ m_should_scroll = true;
+ m_scroll_up = true;
+ } else if (vadjustment_max < show_max) {
+ m_should_scroll = true;
+ m_scroll_up = false;
+ } else {
+ m_should_scroll = false;
+ }
+}
+
+bool DragListBox::scroll() {
+ if (m_should_scroll) {
+ if (m_scroll_up) {
+ get_focus_vadjustment()->set_value(get_focus_vadjustment()->get_value() - SCROLL_STEP_SIZE);
+ } else {
+ get_focus_vadjustment()->set_value(get_focus_vadjustment()->get_value() + SCROLL_STEP_SIZE);
+ }
+ } else {
+ m_scrolling = false;
+ }
+ return m_should_scroll;
+}
+
+void DragListBox::on_drag_data_received(const Glib::RefPtr<Gdk::DragContext> &context, int x, int y, const Gtk::SelectionData &selection_data, guint info, guint time) {
+ int index = 0;
+ if (m_hover_row != nullptr) {
+ if (m_top) {
+ index = m_hover_row->get_index() - 1;
+ m_hover_row->get_style_context()->remove_class("drag-hover-top");
+ } else {
+ index = m_hover_row->get_index();
+ m_hover_row->get_style_context()->remove_class("drag-hover-bottom");
+ }
+
+ Gtk::Widget *handle = *reinterpret_cast<Gtk::Widget *const *>(selection_data.get_data());
+ auto *row = dynamic_cast<Gtk::ListBoxRow *>(handle->get_ancestor(GTK_TYPE_LIST_BOX_ROW));
+
+ if (row != nullptr && row != m_hover_row) {
+ if (row->get_index() > index)
+ index += 1;
+ if (m_signal_on_drop.emit(row, index)) return;
+ row->get_parent()->remove(*row);
+ insert(*row, index);
+ }
+ }
+
+ m_drag_row = nullptr;
+}
+
+void DragListBox::add_draggable(Gtk::ListBoxRow *widget) {
+ widget->drag_source_set(m_entries, Gdk::BUTTON1_MASK, Gdk::ACTION_MOVE);
+ widget->signal_drag_begin().connect(sigc::bind<0>(sigc::mem_fun(*this, &DragListBox::row_drag_begin), widget));
+ widget->signal_drag_data_get().connect([this, widget](const Glib::RefPtr<Gdk::DragContext> &context, Gtk::SelectionData &selection_data, guint info, guint time) {
+ selection_data.set("GTK_LIST_BOX_ROW", 32, reinterpret_cast<const guint8 *>(&widget), sizeof(&widget));
+ });
+ add(*widget);
+}
+
+DragListBox::type_signal_on_drop DragListBox::signal_on_drop() {
+ return m_signal_on_drop;
+}
diff --git a/components/draglistbox.hpp b/components/draglistbox.hpp
new file mode 100644
index 0000000..9f204be
--- /dev/null
+++ b/components/draglistbox.hpp
@@ -0,0 +1,45 @@
+#pragma once
+#include <gtkmm.h>
+
+class DragListBox : public Gtk::ListBox {
+public:
+ DragListBox();
+
+ void row_drag_begin(Gtk::Widget *widget, const Glib::RefPtr<Gdk::DragContext> &context);
+
+ bool on_drag_motion(const Glib::RefPtr<Gdk::DragContext> &context, gint x, gint y, guint time) override;
+
+ void on_drag_leave(const Glib::RefPtr<Gdk::DragContext> &context, guint time) override;
+
+ void check_scroll(gint y);
+
+ bool scroll();
+
+ void on_drag_data_received(const Glib::RefPtr<Gdk::DragContext> &context, int x, int y, const Gtk::SelectionData &selection_data, guint info, guint time) override;
+
+ void add_draggable(Gtk::ListBoxRow *widget);
+
+private:
+ Gtk::ListBoxRow *m_hover_row = nullptr;
+ Gtk::ListBoxRow *m_drag_row = nullptr;
+ bool m_top = false;
+ int m_hover_top = 0;
+ int m_hover_bottom = 0;
+ bool m_should_scroll = false;
+ bool m_scrolling = false;
+ bool m_scroll_up = false;
+
+ constexpr static int SCROLL_STEP_SIZE = 8;
+ constexpr static int SCROLL_DISTANCE = 30;
+ constexpr static int SCROLL_DELAY = 50;
+
+ const std::vector<Gtk::TargetEntry> m_entries = {
+ Gtk::TargetEntry("GTK_LIST_BOX_ROW", Gtk::TARGET_SAME_APP, 0),
+ };
+
+ using type_signal_on_drop = sigc::signal<bool, Gtk::ListBoxRow *, int /* new index */>;
+ type_signal_on_drop m_signal_on_drop;
+
+public:
+ type_signal_on_drop signal_on_drop(); // return true to prevent drop
+};
diff --git a/css/main.css b/css/main.css
index a2eae1e..0df912f 100644
--- a/css/main.css
+++ b/css/main.css
@@ -321,3 +321,11 @@
.app-window colorswatch {
box-shadow: 0 1px rgba(0, 0, 0, 0);
}
+
+.drag-hover-top {
+ background: linear-gradient(to bottom, rgba(255, 66, 66, 0.65) 0%, rgba(0, 0, 0, 0) 35%);
+}
+
+.drag-hover-bottom {
+ background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 65%, rgba(255, 66, 66, 0.65) 100%);
+}
diff --git a/discord/discord.cpp b/discord/discord.cpp
index ff4b8bf..4fa1a63 100644
--- a/discord/discord.cpp
+++ b/discord/discord.cpp
@@ -571,6 +571,58 @@ void DiscordClient::ModifyRoleColor(Snowflake guild_id, Snowflake role_id, Gdk::
ModifyRoleColor(guild_id, role_id, int_color, callback);
}
+void DiscordClient::ModifyRolePosition(Snowflake guild_id, Snowflake role_id, int position, sigc::slot<void(bool success)> callback) {
+ const auto roles = GetGuild(guild_id)->FetchRoles();
+ if (position > roles.size()) return;
+ // gay and makes you send every role in between new and old position
+ size_t index_from = -1, index_to = -1;
+ for (size_t i = 0; i < roles.size(); i++) {
+ const auto &role = roles[i];
+ if (role.ID == role_id)
+ index_from = i;
+ else if (role.Position == position)
+ index_to = i;
+ if (index_from != -1 && index_to != -1) break;
+ }
+
+ if (index_from == -1 || index_to == -1) return;
+
+ int dir;
+ size_t range_from, range_to;
+ if (index_to > index_from) {
+ dir = 1;
+ range_from = index_from + 1;
+ range_to = index_to + 1;
+ } else {
+ dir = -1;
+ range_from = index_to;
+ range_to = index_from;
+ }
+
+ ModifyGuildRolePositionsObject obj;
+
+ obj.Positions.push_back({ roles[index_from].ID, position });
+ for (size_t i = range_from; i < range_to; i++)
+ obj.Positions.push_back({ roles[i].ID, roles[i].Position + dir });
+
+ m_http.MakePATCH("/guilds/" + std::to_string(guild_id) + "/roles", nlohmann::json(obj).dump(), [this, callback](const http::response_type &response) {
+ callback(CheckCode(response));
+ });
+}
+
+bool DiscordClient::CanModifyRole(Snowflake guild_id, Snowflake role_id, Snowflake user_id) const {
+ const auto guild = *GetGuild(guild_id);
+ if (guild.OwnerID == user_id) return true;
+ const auto role = *GetRole(role_id);
+ const auto has_modify = HasGuildPermission(user_id, guild_id, Permission::MANAGE_CHANNELS);
+ const auto highest = GetMemberHighestRole(guild_id, user_id);
+ return has_modify && highest.has_value() && highest->Position > role.Position;
+}
+
+bool DiscordClient::CanModifyRole(Snowflake guild_id, Snowflake role_id) const {
+ return CanModifyRole(guild_id, role_id, GetUserData().ID);
+}
+
std::vector<BanData> DiscordClient::GetBansInGuild(Snowflake guild_id) {
return m_store.GetBans(guild_id);
}
diff --git a/discord/discord.hpp b/discord/discord.hpp
index 0ee70c7..7a729e7 100644
--- a/discord/discord.hpp
+++ b/discord/discord.hpp
@@ -127,7 +127,10 @@ public:
void ModifyRoleName(Snowflake guild_id, Snowflake role_id, const Glib::ustring &name, sigc::slot<void(bool success)> callback);
void ModifyRoleColor(Snowflake guild_id, Snowflake role_id, uint32_t color, sigc::slot<void(bool success)> callback);
void ModifyRoleColor(Snowflake guild_id, Snowflake role_id, Gdk::RGBA color, sigc::slot<void(bool success)> callback);
+ void ModifyRolePosition(Snowflake guild_id, Snowflake role_id, int position, sigc::slot<void(bool success)> callback);
+ bool CanModifyRole(Snowflake guild_id, Snowflake role_id) const;
+ bool CanModifyRole(Snowflake guild_id, Snowflake role_id, Snowflake user_id) const;
// real client doesn't seem to use the single role endpoints so neither do we
template<typename Iter>
diff --git a/discord/httpclient.cpp b/discord/httpclient.cpp
index fa43810..7297643 100644
--- a/discord/httpclient.cpp
+++ b/discord/httpclient.cpp
@@ -1,5 +1,4 @@
#include "httpclient.hpp"
-#include "httpclient.hpp"
//#define USE_LOCAL_PROXY
HTTPClient::HTTPClient(std::string api_base)
diff --git a/discord/objects.cpp b/discord/objects.cpp
index 0c75bf3..6eb6ca6 100644
--- a/discord/objects.cpp
+++ b/discord/objects.cpp
@@ -376,3 +376,12 @@ void to_json(nlohmann::json &j, const ModifyGuildRoleObject &m) {
if (m.Permissions.has_value())
j["permissions"] = std::to_string(static_cast<uint64_t>(*m.Permissions));
}
+
+void to_json(nlohmann::json &j, const ModifyGuildRolePositionsObject::PositionParam &m) {
+ j["id"] = m.ID;
+ JS_IF("position", m.Position);
+}
+
+void to_json(nlohmann::json &j, const ModifyGuildRolePositionsObject &m) {
+ j = m.Positions;
+}
diff --git a/discord/objects.hpp b/discord/objects.hpp
index 713e4bd..e10ed75 100644
--- a/discord/objects.hpp
+++ b/discord/objects.hpp
@@ -526,3 +526,15 @@ struct ModifyGuildRoleObject {
friend void to_json(nlohmann::json &j, const ModifyGuildRoleObject &m);
};
+
+struct ModifyGuildRolePositionsObject {
+ struct PositionParam {
+ Snowflake ID;
+ std::optional<int> Position; // no idea why this can be optional
+
+ friend void to_json(nlohmann::json &j, const PositionParam &m);
+ };
+ std::vector<PositionParam> Positions;
+
+ friend void to_json(nlohmann::json &j, const ModifyGuildRolePositionsObject &m);
+};
diff --git a/windows/guildsettings/rolespane.cpp b/windows/guildsettings/rolespane.cpp
index 311e775..007be23 100644
--- a/windows/guildsettings/rolespane.cpp
+++ b/windows/guildsettings/rolespane.cpp
@@ -28,23 +28,13 @@ void GuildSettingsRolesPane::OnRoleSelect(Snowflake role_id) {
auto &discord = Abaddon::Get().GetDiscordClient();
const auto role = *discord.GetRole(role_id);
m_roles_perms.SetRole(role);
-
- const auto self_id = discord.GetUserData().ID;
-
- const auto is_owner = discord.GetGuild(GuildID)->OwnerID == self_id;
- if (is_owner)
- m_roles_perms.set_sensitive(true);
- else {
- const bool can_modify = discord.HasGuildPermission(self_id, GuildID, Permission::MANAGE_ROLES);
- if (!can_modify) {
- m_roles_perms.set_sensitive(false);
- } else {
- const auto highest = discord.GetMemberHighestRole(GuildID, self_id);
- m_roles_perms.set_sensitive(highest.has_value() && highest->Position > role.Position);
- }
- }
+ m_roles_perms.set_sensitive(discord.CanModifyRole(GuildID, role_id));
}
+static std::vector<Gtk::TargetEntry> g_target_entries = {
+ Gtk::TargetEntry("GTK_LIST_ROLES_ROW", Gtk::TARGET_SAME_APP, 0)
+};
+
GuildSettingsRolesPaneRoles::GuildSettingsRolesPaneRoles(Snowflake guild_id)
: Gtk::Box(Gtk::ORIENTATION_VERTICAL)
, GuildID(guild_id) {
@@ -61,19 +51,47 @@ GuildSettingsRolesPaneRoles::GuildSettingsRolesPaneRoles(Snowflake guild_id)
m_signal_role_select.emit(selected->RoleID);
});
+ m_list.set_focus_vadjustment(m_list_scroll.get_vadjustment());
+ m_list.signal_on_drop().connect([this](Gtk::ListBoxRow *row_, int new_index) -> bool {
+ if (auto *row = dynamic_cast<GuildSettingsRolesPaneRolesListItem *>(row_)) {
+ auto &discord = Abaddon::Get().GetDiscordClient();
+ const auto num_rows = m_list.get_children().size();
+ const auto new_pos = num_rows - new_index - 1;
+ if (row->RoleID == GuildID) return true; // moving role @everyone
+ if (new_index == num_rows) return true; // trying to move row below @everyone
+ // make sure it wont modify a neighbor role u dont have perms to modify
+ if (!discord.CanModifyRole(GuildID, row->RoleID)) return false;
+ const auto cb = [this](bool success) {
+ if (!success) {
+ Gtk::MessageDialog dlg("Failed to set role position", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
+ dlg.set_position(Gtk::WIN_POS_CENTER_ON_PARENT);
+ dlg.run();
+ }
+ };
+ discord.ModifyRolePosition(GuildID, row->RoleID, new_pos, sigc::track_obj(cb, *this));
+ return true;
+ }
+ return false;
+ });
+
auto &discord = Abaddon::Get().GetDiscordClient();
discord.signal_role_create().connect(sigc::mem_fun(*this, &GuildSettingsRolesPaneRoles::OnRoleCreate));
discord.signal_role_delete().connect(sigc::mem_fun(*this, &GuildSettingsRolesPaneRoles::OnRoleDelete));
const auto guild = *discord.GetGuild(GuildID);
const auto roles = guild.FetchRoles();
+ const bool can_modify = discord.HasGuildPermission(discord.GetUserData().ID, GuildID, Permission::MANAGE_ROLES);
for (const auto &role : roles) {
auto *row = Gtk::manage(new GuildSettingsRolesPaneRolesListItem(guild, role));
+ row->drag_source_set(g_target_entries, Gdk::BUTTON1_MASK, Gdk::ACTION_MOVE);
row->set_margin_start(5);
row->set_halign(Gtk::ALIGN_FILL);
row->show();
m_rows[role.ID] = row;
- m_list.add(*row);
+ if (can_modify)
+ m_list.add_draggable(row);
+ else
+ m_list.add(*row);
}
m_list.set_sort_func([this](Gtk::ListBoxRow *rowa_, Gtk::ListBoxRow *rowb_) -> int {
@@ -108,12 +126,16 @@ GuildSettingsRolesPaneRoles::GuildSettingsRolesPaneRoles(Snowflake guild_id)
void GuildSettingsRolesPaneRoles::OnRoleCreate(Snowflake guild_id, Snowflake role_id) {
if (guild_id != GuildID) return;
auto &discord = Abaddon::Get().GetDiscordClient();
+ const bool can_modify = discord.HasGuildPermission(discord.GetUserData().ID, guild_id, Permission::MANAGE_ROLES);
const auto guild = *discord.GetGuild(guild_id);
const auto role = *discord.GetRole(role_id);
auto *row = Gtk::manage(new GuildSettingsRolesPaneRolesListItem(guild, role));
row->show();
m_rows[role_id] = row;
- m_list.add(*row);
+ if (can_modify)
+ m_list.add_draggable(row);
+ else
+ m_list.add(*row);
}
void GuildSettingsRolesPaneRoles::OnRoleDelete(Snowflake guild_id, Snowflake role_id) {
@@ -205,6 +227,7 @@ GuildSettingsRolesPaneInfo::GuildSettingsRolesPaneInfo(Snowflake guild_id)
if (!success) {
m_color_button.set_rgba(IntToRGBA(discord.GetRole(RoleID)->Color));
Gtk::MessageDialog dlg("Failed to set role color", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
+ dlg.set_position(Gtk::WIN_POS_CENTER_ON_PARENT);
dlg.run();
}
};
@@ -364,6 +387,7 @@ void GuildSettingsRolesPaneInfo::UpdateRoleName() {
if (!success) {
m_role_name.set_text(discord.GetRole(RoleID)->Name);
Gtk::MessageDialog dlg("Failed to set role name", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);
+ dlg.set_position(Gtk::WIN_POS_CENTER_ON_PARENT);
dlg.run();
}
};
diff --git a/windows/guildsettings/rolespane.hpp b/windows/guildsettings/rolespane.hpp
index f2ff988..9aa2ff8 100644
--- a/windows/guildsettings/rolespane.hpp
+++ b/windows/guildsettings/rolespane.hpp
@@ -2,6 +2,7 @@
#include <gtkmm.h>
#include <unordered_map>
#include "../../discord/guild.hpp"
+#include "../../components/draglistbox.hpp"
class GuildSettingsRolesPaneRolesListItem : public Gtk::ListBoxRow {
public:
@@ -32,7 +33,7 @@ private:
Gtk::Entry m_search;
Gtk::ScrolledWindow m_list_scroll;
- Gtk::ListBox m_list;
+ DragListBox m_list;
typedef sigc::signal<void, Snowflake /* role_id */> type_signal_role_select;
type_signal_role_select m_signal_role_select;