add ability to reorder roles

- some other small tweaks
This commit is contained in:
ouwou
2021-03-12 01:55:46 -05:00
parent f35dea89f9
commit ae38add931
10 changed files with 313 additions and 19 deletions

141
components/draglistbox.cpp Normal file
View File

@@ -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;
}

View File

@@ -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
};

View File

@@ -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%);
}

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -1,5 +1,4 @@
#include "httpclient.hpp"
#include "httpclient.hpp"
//#define USE_LOCAL_PROXY
HTTPClient::HTTPClient(std::string api_base)

View File

@@ -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;
}

View File

@@ -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);
};

View File

@@ -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();
}
};

View File

@@ -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;