sidebar: Enable changing room category with drag-and-drop

Part of #757
This commit is contained in:
Kévin Commaille 2022-01-18 15:37:09 +01:00 committed by Julian Sparber
parent 411f26b441
commit ec3f7cfeae
15 changed files with 823 additions and 164 deletions

22
Cargo.lock generated
View File

@ -999,6 +999,7 @@ dependencies = [
"matrix-sdk",
"mime",
"mime_guess",
"num_enum",
"once_cell",
"qrcode",
"rand 0.8.4",
@ -2603,6 +2604,27 @@ dependencies = [
"libc",
]
[[package]]
name = "num_enum"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "720d3ea1055e4e4574c0c0b0f8c3fd4f24c4cdaf465948206dea090b57b526ad"
dependencies = [
"num_enum_derive",
]
[[package]]
name = "num_enum_derive"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d992b768490d7fe0d8586d9b5745f6c49f557da6d81dc982b1d167ad4edbb21"
dependencies = [
"proc-macro-crate 1.1.0",
"proc-macro2 1.0.30",
"quote 1.0.10",
"syn 1.0.80",
]
[[package]]
name = "objc"
version = "0.2.7"

View File

@ -29,13 +29,18 @@ futures = "0.3"
rand = "0.8"
indexmap = "1.6.2"
qrcode = "0.12.0"
ashpd = {git = "https://github.com/bilelmoussaoui/ashpd", rev="66d4dc0020181a7174451150ecc711344082b5ce", features=["feature_gtk4", "feature_pipewire", "log"]}
gst = {version = "0.17", package = "gstreamer"}
gst_base = {version = "0.17", package = "gstreamer-base"}
gst_video = {version = "0.17", package = "gstreamer-video"}
image = {version = "0.23", default-features = false, features=["png"]}
ashpd = { git = "https://github.com/bilelmoussaoui/ashpd", rev = "66d4dc0020181a7174451150ecc711344082b5ce", features = [
"feature_gtk4",
"feature_pipewire",
"log",
] }
gst = { version = "0.17", package = "gstreamer" }
gst_base = { version = "0.17", package = "gstreamer-base" }
gst_video = { version = "0.17", package = "gstreamer-video" }
image = { version = "0.23", default-features = false, features = ["png"] }
regex = "1.5.4"
mime_guess = "2.0.3"
num_enum = "0.5.6"
[dependencies.sourceview]
package = "sourceview5"
@ -52,4 +57,10 @@ version = "0.1.0-alpha-6"
[dependencies.matrix-sdk]
git = "https://github.com/jsparber/matrix-rust-sdk.git"
branch = "messages-api"
features = ["socks", "encryption", "sled_cryptostore", "sled_state_store", "markdown"]
features = [
"socks",
"encryption",
"sled_cryptostore",
"sled_state_store",
"markdown",
]

View File

@ -135,43 +135,80 @@ headerbar.flat {
/* Sidebar List */
.sidebar-list row {
padding-left: 10px;
padding-right: 10px;
margin: 0;
padding: 0;
border-radius: 0;
}
.sidebar-list .category {
margin-top: 4px;
.sidebar-list row:focus-visible:focus-within {
outline: 0;
}
.sidebar-list row:selected {
background: none;
}
.sidebar-list row > * {
margin: 0 12px;
padding: 12px 6px;
border-radius: 6px;
transition-property: outline, outline-width, outline-offset, outline-color;
transition-duration: 300ms;
animation-timing-function: ease-in-out;
outline: 0 solid transparent;
outline-offset: 2px;
}
.sidebar-list row:focus-visible:focus-within > * {
outline-color: alpha(@accent_color, 0.5);
outline-width: 2px;
outline-offset: -2px;
}
.sidebar-list:not(.drop-mode) row:hover > * {
background-color: alpha(currentColor, 0.07);
}
.sidebar-list row:active > * {
background-color: alpha(currentColor, 0.16);
}
.sidebar-list row:selected > * {
background-color: alpha(currentColor, 0.1);
}
.sidebar-list:not(.drop-mode) row:selected:hover > *,
.sidebar-list row:selected.has-open-popup > * {
background-color: alpha(currentColor, 0.13);
}
.sidebar-list row:selected:active > * {
background-color: alpha(currentColor, 0.19);
}
.sidebar-list row.entry {
font-weight: bold;
}
.sidebar-list row.category {
margin-top: 6px;
font-size: 0.8em;
font-weight: bold;
}
.sidebar-list .entry {
margin-top: 4px;
font-weight: bold;
}
.sidebar-list .category image.arrow {
.sidebar-list row.category image.arrow {
transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.sidebar-list .category .category-row:not(:checked) image.arrow:dir(ltr) {
.sidebar-list row.category .category-row:not(:checked) image.arrow:dir(ltr) {
transform: rotate(-0.25turn);
}
.sidebar-list .category .category-row:not(:checked) image.arrow:dir(rtl) {
.sidebar-list row.category .category-row:not(:checked) image.arrow:dir(rtl) {
transform: rotate(0.25turn);
}
.sidebar-list .room {
padding-top: 4px;
padding-bottom: 4px;
}
.sidebar-list .room .bold {
font-weight: bold;
}
.sidebar-list .room .notification_count {
.sidebar-list row.room .notification_count {
font-weight: bold;
font-size: 0.8em;
border-radius: 10px;
@ -179,10 +216,44 @@ headerbar.flat {
padding: 2px 5px;
}
.sidebar-list .room .highlight {
.sidebar-list row.room .highlight {
color: @accent_fg_color;
background-color: @accent_bg_color;
}
.sidebar-list sidebar-row.drag {
color: @accent_fg_color;
background-color: @accent_bg_color;
opacity: 0.6;
}
.sidebar-list sidebar-row.drop-disabled > * {
opacity: 0.6;
}
.sidebar-list sidebar-row.drop-empty {
color: @accent_color;
}
.sidebar-list sidebar-row.forget {
color: @error_color;
background: none;
}
.sidebar-list row.drop-active {
background-color: alpha(@accent_color, 0.1);
}
.sidebar-list row.category.drop-active,
.sidebar-list row.drop-active sidebar-row.forget {
color: @accent_color;
}
.sidebar-list row.drop-active .dim-label,
.sidebar-list row.drop-active sidebar-row.drop-empty .dim-label {
opacity: 1;
}
/* Content */
.room-history {
background: @theme_base_color;

View File

@ -12,11 +12,7 @@
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="ellipsize">end</property>
<binding name="label">
<lookup name="display-name">
<lookup name="category">SidebarCategoryRow</lookup>
</lookup>
</binding>
<property name="label" bind-source="SidebarCategoryRow" bind-property="label" bind-flags="sync-create"/>
<style>
<class name="dim-label"/>
</style>

View File

@ -36,7 +36,7 @@ use matrix_sdk::{
member::MembershipState, message::RoomMessageEventContent,
name::RoomNameEventContent, topic::RoomTopicEventContent,
},
tag::TagName,
tag::{TagInfo, TagName},
AnyRoomAccountDataEvent, AnyStateEventContent, AnyStrippedStateEvent,
AnySyncMessageEvent, AnySyncRoomEvent, AnySyncStateEvent, EventType, SyncMessageEvent,
Unsigned,
@ -284,7 +284,10 @@ mod imp {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![Signal::builder("order-changed", &[], <()>::static_type().into()).build()]
vec![
Signal::builder("order-changed", &[], <()>::static_type().into()).build(),
Signal::builder("room-forgotten", &[], <()>::static_type().into()).build(),
]
});
SIGNALS.as_ref()
}
@ -366,6 +369,56 @@ impl Room {
self.load_category();
}
/// Forget a room that is left.
pub fn forget(&self) {
if self.category() != RoomType::Left {
warn!("Cannot forget a room that is not left");
return;
}
let matrix_room = self.matrix_room();
let handle = spawn_tokio!(async move {
match matrix_room {
MatrixRoom::Left(room) => room.forget().await,
_ => unimplemented!(),
}
});
spawn!(
glib::PRIORITY_DEFAULT_IDLE,
clone!(@weak self as obj => async move {
match handle.await.unwrap() {
Ok(_) => {
obj.emit_by_name("room-forgotten", &[]).unwrap();
}
Err(error) => {
error!("Couldnt forget the room: {}", error);
let error = Error::new(
clone!(@weak obj => @default-return None, move |_| {
let error_message = gettext(
"Failed to forget <widget>."
);
let room_pill = Pill::new();
room_pill.set_room(Some(obj));
let label = LabelWithWidgets::new(&error_message, vec![room_pill]);
Some(label.upcast())
}),
);
if let Some(window) = obj.session().parent_window() {
window.append_error(&error);
}
// Load the previous category
obj.load_category();
},
};
})
);
}
pub fn category(&self) -> RoomType {
let priv_ = imp::Room::from_instance(self);
priv_.category.get()
@ -386,7 +439,9 @@ impl Room {
/// Set the category of this room.
///
/// This makes the necessary to propagate the category to the homeserver.
/// Note: Rooms can't be moved to the invite category and they can't be moved once they are upgraded
///
/// Note: Rooms can't be moved to the invite category and they can't be
/// moved once they are upgraded.
pub fn set_category(&self, category: RoomType) {
let matrix_room = self.matrix_room();
let previous_category = self.category();
@ -395,78 +450,93 @@ impl Room {
return;
}
if category == RoomType::Invited {
warn!("Rooms cant be moved to the invite Category");
return;
}
if self.category() == RoomType::Outdated {
if previous_category == RoomType::Outdated {
warn!("Can't set the category of an upgraded room");
return;
}
// Outdated rooms don't need to propagate anything to the server
if category == RoomType::Outdated {
self.set_category_internal(category);
return;
match category {
RoomType::Invited => {
warn!("Rooms cant be moved to the invite Category");
return;
}
RoomType::Outdated => {
// Outdated rooms don't need to propagate anything to the server
self.set_category_internal(category);
return;
}
_ => {}
}
let handle = spawn_tokio!(async move {
match matrix_room {
MatrixRoom::Invited(room) => {
match category {
RoomType::Invited => Ok(()),
RoomType::Favorite => {
room.accept_invitation().await
// TODO: set favorite tag
}
RoomType::Normal => room.accept_invitation().await,
RoomType::LowPriority => {
room.accept_invitation().await
// TODO: set low priority tag
}
RoomType::Left => room.reject_invitation().await,
RoomType::Outdated => unimplemented!(),
MatrixRoom::Invited(room) => match category {
RoomType::Invited => Ok(()),
RoomType::Favorite => {
room.accept_invitation().await
// TODO: set favorite tag
}
}
MatrixRoom::Joined(room) => {
match category {
RoomType::Invited => Ok(()),
RoomType::Favorite => {
// TODO: set favorite tag
Ok(())
}
RoomType::Normal => {
// TODO: remove tags
Ok(())
}
RoomType::LowPriority => {
// TODO: set low priority tag
Ok(())
}
RoomType::Left => room.leave().await,
RoomType::Outdated => unimplemented!(),
RoomType::Normal => {
room.accept_invitation().await
// TODO: remove tags
}
}
MatrixRoom::Left(room) => {
match category {
RoomType::Invited => Ok(()),
RoomType::Favorite => {
room.join().await
// TODO: set favorite tag
}
RoomType::Normal => {
room.join().await
// TODO: remove tags
}
RoomType::LowPriority => {
room.join().await
// TODO: set low priority tag
}
RoomType::Left => Ok(()),
RoomType::Outdated => unimplemented!(),
RoomType::LowPriority => {
room.accept_invitation().await
// TODO: set low priority tag
}
}
RoomType::Left => room.reject_invitation().await,
RoomType::Outdated => unimplemented!(),
},
MatrixRoom::Joined(room) => match category {
RoomType::Invited => Ok(()),
RoomType::Favorite => {
room.set_tag(TagName::Favorite.as_ref(), TagInfo::new())
.await?;
if previous_category == RoomType::LowPriority {
room.remove_tag(TagName::LowPriority.as_ref()).await?;
}
Ok(())
}
RoomType::Normal => {
match previous_category {
RoomType::Favorite => {
room.remove_tag(TagName::Favorite.as_ref()).await?;
}
RoomType::LowPriority => {
room.remove_tag(TagName::LowPriority.as_ref()).await?;
}
_ => {}
}
Ok(())
}
RoomType::LowPriority => {
room.set_tag(TagName::LowPriority.as_ref(), TagInfo::new())
.await?;
if previous_category == RoomType::Favorite {
room.remove_tag(TagName::Favorite.as_ref()).await?;
}
Ok(())
}
RoomType::Left => room.leave().await,
RoomType::Outdated => unimplemented!(),
},
MatrixRoom::Left(room) => match category {
RoomType::Invited => Ok(()),
RoomType::Favorite => {
room.join().await
// TODO: set favorite tag
}
RoomType::Normal => {
room.join().await
// TODO: remove tags
}
RoomType::LowPriority => {
room.join().await
// TODO: set low priority tag
}
RoomType::Left => Ok(()),
RoomType::Outdated => unimplemented!(),
},
}
});
@ -1033,6 +1103,16 @@ impl Room {
.unwrap()
}
/// Connect to the signal sent when a room was forgotten.
pub fn connect_room_forgotten<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("room-forgotten", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
f(&obj);
None
})
.unwrap()
}
pub fn predecessor(&self) -> Option<&RoomId> {
let priv_ = imp::Room::from_instance(self);
priv_.predecessor.get().map(std::ops::Deref::deref)

View File

@ -1,8 +1,12 @@
use crate::session::sidebar::CategoryType;
use std::convert::TryFrom;
use gtk::glib;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use crate::session::sidebar::CategoryType;
// TODO: do we also want the category `People` and a custom category support?
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::GEnum)]
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::GEnum, IntoPrimitive, TryFromPrimitive)]
#[repr(u32)]
#[genum(type_name = "RoomType")]
pub enum RoomType {
@ -14,6 +18,33 @@ pub enum RoomType {
Outdated = 5,
}
impl RoomType {
/// Check whether this `RoomType` can be changed to `category`.
pub fn can_change_to(&self, category: &RoomType) -> bool {
match self {
Self::Invited => {
matches!(
category,
Self::Favorite | Self::Normal | Self::LowPriority | Self::Left
)
}
Self::Favorite => {
matches!(category, Self::Normal | Self::LowPriority | Self::Left)
}
Self::Normal => {
matches!(category, Self::Favorite | Self::LowPriority | Self::Left)
}
Self::LowPriority => {
matches!(category, Self::Favorite | Self::Normal | Self::Left)
}
Self::Left => {
matches!(category, Self::Favorite | Self::Normal | Self::LowPriority)
}
Self::Outdated => false,
}
}
}
impl Default for RoomType {
fn default() -> Self {
RoomType::Normal
@ -25,3 +56,30 @@ impl ToString for RoomType {
CategoryType::from(self).to_string()
}
}
impl TryFrom<CategoryType> for RoomType {
type Error = &'static str;
fn try_from(category_type: CategoryType) -> Result<Self, Self::Error> {
Self::try_from(&category_type)
}
}
impl TryFrom<&CategoryType> for RoomType {
type Error = &'static str;
fn try_from(category_type: &CategoryType) -> Result<Self, Self::Error> {
match category_type {
CategoryType::None => Err("CategoryType::None cannot be a RoomType"),
CategoryType::Invited => Ok(Self::Invited),
CategoryType::Favorite => Ok(Self::Favorite),
CategoryType::Normal => Ok(Self::Normal),
CategoryType::LowPriority => Ok(Self::LowPriority),
CategoryType::Left => Ok(Self::Left),
CategoryType::Outdated => Ok(Self::Outdated),
CategoryType::VerificationRequest => {
Err("CategoryType::VerificationRequest cannot be a RoomType")
}
}
}
}

View File

@ -226,6 +226,9 @@ impl RoomList {
obj.items_changed(position as u32, 1, 1);
}
}));
room.connect_room_forgotten(clone!(@weak self as obj => move |room| {
obj.remove(room.room_id());
}));
}
self.items_changed(position as u32, 0, added as u32);

View File

@ -1,9 +1,10 @@
use adw::subclass::prelude::BinImpl;
use gettextrs::gettext;
use gtk::subclass::prelude::*;
use gtk::{self, prelude::*};
use gtk::{glib, CompositeTemplate};
use crate::session::sidebar::Category;
use crate::session::sidebar::{Category, CategoryType};
mod imp {
use super::*;
@ -13,8 +14,13 @@ mod imp {
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/FractalNext/sidebar-category-row.ui")]
pub struct CategoryRow {
/// The category of this row.
pub category: RefCell<Option<Category>>,
/// The expanded state of this row.
pub expanded: Cell<bool>,
/// The `CategoryType` to show a label for during a drag-and-drop
/// operation.
pub show_label_for_category: Cell<CategoryType>,
}
#[glib::object_subclass]
@ -51,6 +57,21 @@ mod imp {
true,
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
glib::ParamSpec::new_string(
"label",
"Label",
"The label to show for this row",
None,
glib::ParamFlags::READABLE,
),
glib::ParamSpec::new_enum(
"show-label-for-category",
"Show Label for Category",
"The CategoryType to show a label for",
CategoryType::static_type(),
CategoryType::None as i32,
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
]
});
@ -65,14 +86,9 @@ mod imp {
pspec: &glib::ParamSpec,
) {
match pspec.name() {
"category" => {
let category = value.get().unwrap();
obj.set_category(category);
}
"expanded" => {
let expanded = value.get().unwrap();
obj.set_expanded(expanded);
}
"category" => obj.set_category(value.get().unwrap()),
"expanded" => obj.set_expanded(value.get().unwrap()),
"show-label-for-category" => obj.set_show_label_for_category(value.get().unwrap()),
_ => unimplemented!(),
}
}
@ -81,6 +97,8 @@ mod imp {
match pspec.name() {
"category" => obj.category().to_value(),
"expanded" => obj.expanded().to_value(),
"label" => obj.label().to_value(),
"show-label-for-category" => obj.show_label_for_category().to_value(),
_ => unimplemented!(),
}
}
@ -97,7 +115,8 @@ glib::wrapper! {
impl CategoryRow {
pub fn new() -> Self {
glib::Object::new(&[]).expect("Failed to create CategoryRow")
glib::Object::new(&[("show-label-for-category", &CategoryType::None)])
.expect("Failed to create CategoryRow")
}
pub fn category(&self) -> Option<Category> {
@ -114,6 +133,7 @@ impl CategoryRow {
priv_.category.replace(category);
self.notify("category");
self.notify("label");
}
fn expanded(&self) -> bool {
@ -137,6 +157,66 @@ impl CategoryRow {
priv_.expanded.set(expanded);
self.notify("expanded");
}
pub fn label(&self) -> Option<String> {
let to_type = self.category()?.type_();
let from_type = self.show_label_for_category();
let label = match from_type {
CategoryType::Invited => match to_type {
CategoryType::Favorite => gettext("Join Room as Favorite"),
CategoryType::Normal => gettext("Join Room"),
CategoryType::LowPriority => gettext("Join Room as Low Priority"),
CategoryType::Left => gettext("Reject Invite"),
_ => to_type.to_string(),
},
CategoryType::Favorite => match to_type {
CategoryType::Normal => gettext("Unmark as Favorite"),
CategoryType::LowPriority => gettext("Mark as Low Priority"),
CategoryType::Left => gettext("Leave Room"),
_ => to_type.to_string(),
},
CategoryType::Normal => match to_type {
CategoryType::Favorite => gettext("Mark as Favorite"),
CategoryType::LowPriority => gettext("Mark as Low Priority"),
CategoryType::Left => gettext("Leave Room"),
_ => to_type.to_string(),
},
CategoryType::LowPriority => match to_type {
CategoryType::Favorite => gettext("Mark as Favorite"),
CategoryType::Normal => gettext("Unmark as Low Priority"),
CategoryType::Left => gettext("Leave Room"),
_ => to_type.to_string(),
},
CategoryType::Left => match to_type {
CategoryType::Favorite => gettext("Rejoin Room as Favorite"),
CategoryType::Normal => gettext("Rejoin Room"),
CategoryType::LowPriority => gettext("Rejoin Room as Low Priority"),
_ => to_type.to_string(),
},
_ => to_type.to_string(),
};
Some(label)
}
pub fn show_label_for_category(&self) -> CategoryType {
let priv_ = imp::CategoryRow::from_instance(self);
priv_.show_label_for_category.get()
}
pub fn set_show_label_for_category(&self, category: CategoryType) {
let priv_ = imp::CategoryRow::from_instance(self);
if category == self.show_label_for_category() {
return;
}
priv_.show_label_for_category.set(category);
self.notify("show-label-for-category");
self.notify("label");
}
}
impl Default for CategoryRow {

View File

@ -3,9 +3,10 @@ use gettextrs::gettext;
use gtk::glib;
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::GEnum)]
#[repr(u32)]
#[repr(i32)]
#[genum(type_name = "CategoryType")]
pub enum CategoryType {
None = -1,
VerificationRequest = 0,
Invited = 1,
Favorite = 2,
@ -24,6 +25,7 @@ impl Default for CategoryType {
impl ToString for CategoryType {
fn to_string(&self) -> String {
match self {
CategoryType::None => unimplemented!(),
CategoryType::VerificationRequest => gettext("Verifications"),
CategoryType::Invited => gettext("Invited"),
CategoryType::Favorite => gettext("Favorite"),

View File

@ -103,6 +103,7 @@ impl Entry {
pub fn icon_name(&self) -> Option<&str> {
match self.type_() {
EntryType::Explore => Some("explore-symbolic"),
EntryType::Forget => Some("user-trash-symbolic"),
}
}
}

View File

@ -6,6 +6,7 @@ use gtk::glib;
#[genum(type_name = "EntryType")]
pub enum EntryType {
Explore = 0,
Forget = 1,
}
impl Default for EntryType {
@ -18,6 +19,7 @@ impl ToString for EntryType {
fn to_string(&self) -> String {
match self {
EntryType::Explore => gettext("Explore"),
EntryType::Forget => gettext("Forget Room"),
}
}
}

View File

@ -1,6 +1,9 @@
use std::convert::TryFrom;
use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
use crate::session::{
room::RoomType,
room_list::RoomList,
sidebar::CategoryType,
sidebar::EntryType,
@ -17,10 +20,13 @@ mod imp {
#[derive(Debug, Default)]
pub struct ItemList {
pub list: OnceCell<[(glib::Object, Cell<bool>); 7]>,
pub list: OnceCell<[(glib::Object, Cell<bool>); 8]>,
pub room_list: OnceCell<RoomList>,
pub verification_list: OnceCell<VerificationList>,
pub show_all: Cell<bool>,
/// The `CategoryType` to show all compatible categories for.
///
/// Uses `RoomType::can_change_to` to find compatible categories.
pub show_all_for_category: Cell<CategoryType>,
}
#[glib::object_subclass]
@ -49,11 +55,12 @@ mod imp {
VerificationList::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
),
glib::ParamSpec::new_boolean(
"show-all",
"Show All",
"Whether all room categories should be shown",
false,
glib::ParamSpec::new_enum(
"show-all-for-category",
"Show All For Category",
"The `CategoryType` to show all compatible categories for",
CategoryType::static_type(),
CategoryType::None as i32,
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
]
@ -72,7 +79,7 @@ mod imp {
match pspec.name() {
"room-list" => obj.set_room_list(value.get().unwrap()),
"verification-list" => obj.set_verification_list(value.get().unwrap()),
"show-all" => obj.set_show_all(value.get().unwrap()),
"show-all-for-category" => obj.set_show_all_for_category(value.get().unwrap()),
_ => unimplemented!(),
}
}
@ -81,7 +88,7 @@ mod imp {
match pspec.name() {
"room-list" => obj.room_list().to_value(),
"verification-list" => obj.verification_list().to_value(),
"show-all" => obj.show_all().to_value(),
"show-all-for-category" => obj.show_all_for_category().to_value(),
_ => unimplemented!(),
}
}
@ -101,6 +108,7 @@ mod imp {
Category::new(CategoryType::Normal, room_list).upcast::<glib::Object>(),
Category::new(CategoryType::LowPriority, room_list).upcast::<glib::Object>(),
Category::new(CategoryType::Left, room_list).upcast::<glib::Object>(),
Entry::new(EntryType::Forget).upcast::<glib::Object>(),
];
for (index, item) in list.iter().enumerate() {
@ -108,7 +116,7 @@ mod imp {
category.connect_notify_local(
Some("empty"),
clone!(@weak obj => move |_, _| {
obj.update_category(index);
obj.update_item(index);
}),
);
}
@ -118,7 +126,9 @@ mod imp {
let visible = if let Some(category) = item.downcast_ref::<Category>() {
!category.is_empty()
} else {
true
item.downcast_ref::<Entry>()
.filter(|entry| entry.type_() == EntryType::Forget)
.is_none()
};
(item, Cell::new(visible))
});
@ -177,12 +187,30 @@ impl ItemList {
.expect("Failed to create ItemList")
}
fn update_category(&self, position: usize) {
fn update_item(&self, position: usize) {
let priv_ = imp::ItemList::from_instance(self);
let (item, old_visible) = priv_.list.get().unwrap().get(position).unwrap();
let category = item.downcast_ref::<Category>().unwrap();
let visible = !category.is_empty() || (self.show_all() && is_show_all_category(category));
let visible = if let Some(category) = item.downcast_ref::<Category>() {
!category.is_empty()
|| RoomType::try_from(self.show_all_for_category())
.ok()
.and_then(|room_type| {
RoomType::try_from(category.type_())
.ok()
.filter(|category| room_type.can_change_to(category))
})
.is_some()
} else if item
.downcast_ref::<Entry>()
.filter(|entry| entry.type_() == EntryType::Forget)
.is_some()
{
self.show_all_for_category() == CategoryType::Left
} else {
return;
};
if visible != old_visible.get() {
old_visible.set(visible);
let hidden_before_position = priv_
@ -201,32 +229,24 @@ impl ItemList {
}
}
// Whether all room categories are shown
// This doesn't include `CategoryType::Invite` since the user can't move rooms to it.
pub fn show_all(&self) -> bool {
pub fn show_all_for_category(&self) -> CategoryType {
let priv_ = imp::ItemList::from_instance(self);
priv_.show_all.get()
priv_.show_all_for_category.get()
}
// Set whether all room categories should be shown
// This doesn't include `CategoryType::Invite` since the user can't move rooms to it.
pub fn set_show_all(&self, show_all: bool) {
pub fn set_show_all_for_category(&self, category: CategoryType) {
let priv_ = imp::ItemList::from_instance(self);
if show_all == self.show_all() {
if category == self.show_all_for_category() {
return;
}
priv_.show_all.set(show_all);
for (index, (item, _)) in priv_.list.get().unwrap().iter().enumerate() {
if let Some(category) = item.downcast_ref::<Category>() {
if is_show_all_category(category) {
self.update_category(index);
}
}
priv_.show_all_for_category.set(category);
for i in 0..priv_.list.get().unwrap().len() {
self.update_item(i);
}
self.notify("show-all");
self.notify("show-all-for-category");
}
fn set_room_list(&self, room_list: RoomList) {
@ -249,15 +269,3 @@ impl ItemList {
priv_.verification_list.get().unwrap()
}
}
// Wheter this category should be shown when `show-all` is `true`
// This doesn't include `CategoryType::Invite` since the user can't move rooms to it.
fn is_show_all_category(category: &Category) -> bool {
matches!(
category.type_(),
CategoryType::Favorite
| CategoryType::Normal
| CategoryType::LowPriority
| CategoryType::Left
)
}

View File

@ -23,11 +23,11 @@ use self::row::Row;
use self::selection::Selection;
use self::verification_row::VerificationRow;
use adw::subclass::prelude::BinImpl;
use gtk::{gio, glib, prelude::*, subclass::prelude::*, CompositeTemplate, SelectionModel};
use adw::{prelude::*, subclass::prelude::*};
use gtk::{gio, glib, subclass::prelude::*, CompositeTemplate, SelectionModel};
use crate::components::Avatar;
use crate::session::room::Room;
use crate::session::room::{Room, RoomType};
use crate::session::verification::IdentityVerification;
use crate::session::Session;
use crate::session::User;
@ -37,7 +37,10 @@ mod imp {
use super::*;
use glib::subclass::InitializingObject;
use once_cell::sync::Lazy;
use std::cell::{Cell, RefCell};
use std::{
cell::{Cell, RefCell},
convert::TryFrom,
};
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/FractalNext/sidebar.ui")]
@ -55,6 +58,9 @@ mod imp {
#[template_child]
pub room_search: TemplateChild<gtk::SearchBar>,
pub user: RefCell<Option<User>>,
/// The type of the source that activated drop mode.
pub drop_source_type: Cell<Option<RoomType>>,
pub drop_binding: RefCell<Option<glib::Binding>>,
}
#[glib::object_subclass]
@ -68,6 +74,35 @@ mod imp {
Row::static_type();
Avatar::static_type();
Self::bind_template(klass);
klass.set_css_name("sidebar");
klass.install_action(
"sidebar.set-drop-source-type",
Some("u"),
move |obj, _, variant| {
obj.set_drop_source_type(
variant
.and_then(|variant| variant.get::<Option<u32>>().flatten())
.and_then(|u| RoomType::try_from(u).ok()),
);
},
);
klass.install_action("sidebar.update-drop-targets", None, move |obj, _, _| {
if obj.drop_source_type().is_some() {
obj.update_drop_targets();
}
});
klass.install_action(
"sidebar.set-active-drop-category",
Some("mu"),
move |obj, _, variant| {
obj.update_active_drop_targets(
variant
.and_then(|variant| variant.get::<Option<u32>>().flatten())
.and_then(|u| RoomType::try_from(u).ok()),
);
},
);
}
fn instance_init(obj: &InitializingObject<Self>) {
@ -98,7 +133,7 @@ mod imp {
"Item List",
"The list of items in the sidebar",
ItemList::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
glib::ParamFlags::WRITABLE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
glib::ParamSpec::new_object(
"selected-item",
@ -107,6 +142,14 @@ mod imp {
glib::Object::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
glib::ParamSpec::new_enum(
"drop-source-type",
"Drop Source Type",
"The type of the source that activated drop mode",
CategoryType::static_type(),
CategoryType::None as i32,
glib::ParamFlags::READABLE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
]
});
@ -144,6 +187,11 @@ mod imp {
"compact" => self.compact.get().to_value(),
"user" => obj.user().to_value(),
"selected-item" => obj.selected_item().to_value(),
"drop-source-type" => obj
.drop_source_type()
.map(CategoryType::from)
.unwrap_or(CategoryType::None)
.to_value(),
_ => unimplemented!(),
}
}
@ -151,7 +199,7 @@ mod imp {
fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj);
self.listview.get().connect_activate(move |listview, pos| {
self.listview.connect_activate(move |listview, pos| {
let model: Option<Selection> = listview.model().and_then(|o| o.downcast().ok());
let row: Option<gtk::TreeListRow> = model
.as_ref()
@ -200,6 +248,11 @@ impl Sidebar {
pub fn set_item_list(&self, item_list: Option<ItemList>) {
let priv_ = imp::Sidebar::from_instance(self);
if let Some(binding) = priv_.drop_binding.take() {
binding.unbind();
}
let item_list = match item_list {
Some(item_list) => item_list,
None => {
@ -208,6 +261,12 @@ impl Sidebar {
}
};
priv_.drop_binding.replace(
self.bind_property("drop-source-type", &item_list, "show-all-for-category")
.flags(glib::BindingFlags::SYNC_CREATE)
.build(),
);
let tree_model = gtk::TreeListModel::new(&item_list, false, true, |item| {
item.clone().downcast::<gio::ListModel>().ok()
});
@ -280,6 +339,119 @@ impl Sidebar {
.account_switcher
.set_logged_in_users(sessions_stack_pages, session_root);
}
pub fn drop_source_type(&self) -> Option<RoomType> {
let priv_ = imp::Sidebar::from_instance(self);
priv_.drop_source_type.get()
}
pub fn set_drop_source_type(&self, source_type: Option<RoomType>) {
let priv_ = imp::Sidebar::from_instance(self);
if self.drop_source_type() == source_type {
return;
}
priv_.drop_source_type.set(source_type);
if source_type.is_some() {
priv_.listview.add_css_class("drop-mode");
} else {
priv_.listview.remove_css_class("drop-mode");
}
self.notify("drop-source-type");
self.update_drop_targets();
}
/// Update the disabled or empty state of drop targets.
fn update_drop_targets(&self) {
let priv_ = imp::Sidebar::from_instance(self);
let mut child = priv_.listview.first_child();
while let Some(widget) = child {
if let Some(row) = widget
.first_child()
.and_then(|widget| widget.downcast::<Row>().ok())
{
if let Some(source_type) = self.drop_source_type() {
if row
.room_type()
.filter(|row_type| source_type.can_change_to(row_type))
.is_some()
{
row.remove_css_class("drop-disabled");
if row
.item()
.and_then(|object| object.downcast::<Category>().ok())
.filter(|category| category.is_empty())
.is_some()
{
row.add_css_class("drop-empty");
} else {
row.remove_css_class("drop-empty");
}
} else {
let is_forget_entry = row
.entry_type()
.filter(|entry_type| entry_type == &EntryType::Forget)
.is_some();
if is_forget_entry && source_type == RoomType::Left {
row.remove_css_class("drop-disabled");
} else {
row.add_css_class("drop-disabled");
row.remove_css_class("drop-empty");
}
}
} else {
// Clear style
row.remove_css_class("drop-disabled");
row.remove_css_class("drop-empty");
row.parent().unwrap().remove_css_class("drop-active");
};
if let Some(category_row) = row
.child()
.and_then(|child| child.downcast::<CategoryRow>().ok())
{
category_row.set_show_label_for_category(
self.drop_source_type()
.map(CategoryType::from)
.unwrap_or(CategoryType::None),
);
}
}
child = widget.next_sibling();
}
}
/// Update the active state of drop targets.
fn update_active_drop_targets(&self, target_type: Option<RoomType>) {
let priv_ = imp::Sidebar::from_instance(self);
let mut child = priv_.listview.first_child();
while let Some(widget) = child {
if let Some((row, row_type)) = widget
.first_child()
.and_then(|widget| widget.downcast::<Row>().ok())
.and_then(|row| {
let row_type = row.room_type()?;
Some((row, row_type))
})
{
if target_type
.filter(|target_type| target_type == &row_type)
.is_some()
{
row.parent().unwrap().add_css_class("drop-active");
} else {
row.parent().unwrap().remove_css_class("drop-active");
}
}
child = widget.next_sibling();
}
}
}
impl Default for Sidebar {

View File

@ -1,7 +1,7 @@
use adw::subclass::prelude::BinImpl;
use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
use gtk::{gdk, glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
use crate::session::room::{HighlightFlags, Room};
use crate::session::room::{HighlightFlags, Room, RoomType};
mod imp {
use super::*;
@ -74,6 +74,27 @@ mod imp {
}
}
fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj);
// Allow to drag rooms
let drag = gtk::DragSource::builder()
.actions(gdk::DragAction::MOVE)
.build();
drag.connect_prepare(
clone!(@weak obj => @default-return None, move |drag, x, y| {
obj.drag_prepare(drag, x, y)
}),
);
drag.connect_drag_begin(clone!(@weak obj => move |_, _| {
obj.drag_begin();
}));
drag.connect_drag_end(clone!(@weak obj => move |_, _, _| {
obj.drag_end();
}));
obj.add_controller(&drag);
}
fn dispose(&self, _obj: &Self::Type) {
if let Some(room) = self.room.take() {
if let Some(id) = self.signal_handler.take() {
@ -116,6 +137,7 @@ impl RoomRow {
if let Some(binding) = priv_.binding.take() {
binding.unbind();
}
priv_.display_name.remove_css_class("dim-label");
}
if let Some(ref room) = room {
@ -138,6 +160,10 @@ impl RoomRow {
}),
)));
if room.category() == RoomType::Left {
priv_.display_name.add_css_class("dim-label");
}
self.set_highlight();
}
priv_.room.replace(room);
@ -168,6 +194,26 @@ impl RoomRow {
};
}
}
fn drag_prepare(&self, drag: &gtk::DragSource, x: f64, y: f64) -> Option<gdk::ContentProvider> {
let paintable = gtk::WidgetPaintable::new(Some(&self.parent().unwrap()));
// FIXME: The hotspot coordinates don't work.
// See https://gitlab.gnome.org/GNOME/gtk/-/issues/2341
drag.set_icon(Some(&paintable), x as i32, y as i32);
self.room()
.map(|room| gdk::ContentProvider::for_value(&room.to_value()))
}
fn drag_begin(&self) {
self.parent().unwrap().add_css_class("drag");
let category = Some(u32::from(self.room().unwrap().category()));
self.activate_action("sidebar.set-drop-source-type", Some(&category.to_variant()));
}
fn drag_end(&self) {
self.activate_action("sidebar.set-drop-source-type", None);
self.parent().unwrap().remove_css_class("drag");
}
}
impl Default for RoomRow {

View File

@ -1,12 +1,16 @@
use std::convert::TryFrom;
use adw::{prelude::*, subclass::prelude::*};
use gtk::{glib, subclass::prelude::*};
use gtk::{gdk, glib, glib::clone, subclass::prelude::*};
use crate::session::{
room::Room,
room::{Room, RoomType},
sidebar::{Category, CategoryRow, Entry, EntryRow, RoomRow, VerificationRow},
verification::IdentityVerification,
};
use super::EntryType;
mod imp {
use super::*;
use once_cell::sync::Lazy;
@ -23,6 +27,10 @@ mod imp {
const NAME: &'static str = "SidebarRow";
type Type = super::Row;
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
klass.set_css_name("sidebar-row");
}
}
impl ObjectImpl for Row {
@ -72,6 +80,28 @@ mod imp {
_ => unimplemented!(),
}
}
fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj);
// Set up drop controller
let drop = gtk::DropTarget::builder()
.actions(gdk::DragAction::MOVE)
.formats(&gdk::ContentFormats::for_type(Room::static_type()))
.build();
drop.connect_accept(clone!(@weak obj => @default-return false, move |_, drop| {
obj.drop_accept(drop)
}));
drop.connect_leave(clone!(@weak obj => move |_| {
obj.drop_leave();
}));
drop.connect_drop(
clone!(@weak obj => @default-return false, move |_, v, _, _| {
obj.drop_end(v)
}),
);
obj.add_controller(&drop);
}
}
impl WidgetImpl for Row {}
@ -162,6 +192,10 @@ impl Row {
child
};
if entry.type_() == EntryType::Forget {
self.add_css_class("forget");
}
child.set_entry(Some(entry.clone()));
if let Some(list_item) = self.parent() {
@ -186,11 +220,84 @@ impl Row {
} else {
panic!("Wrong row item: {:?}", item);
}
self.activate_action("sidebar.update-drop-targets", None);
}
self.notify("item");
self.notify("list-row");
}
/// Get the `RoomType` of this item.
///
/// If this is not a `Category` or one of its children, returns `None`.
pub fn room_type(&self) -> Option<RoomType> {
let item = self.item()?;
if let Some(room) = item.downcast_ref::<Room>() {
Some(room.category())
} else {
item.downcast_ref::<Category>()
.and_then(|category| RoomType::try_from(category.type_()).ok())
}
}
/// Get the `EntryType` of this item.
///
/// If this is not a `Entry`, returns `None`.
pub fn entry_type(&self) -> Option<EntryType> {
let item = self.item()?;
item.downcast_ref::<Entry>().map(|entry| entry.type_())
}
fn drop_accept(&self, drop: &gdk::Drop) -> bool {
let room = drop
.drag()
.and_then(|drag| drag.content())
.and_then(|content| content.value(Room::static_type()).ok())
.and_then(|value| value.get::<Room>().ok());
if let Some(room) = room {
if let Some(target_type) = self.room_type() {
if room.category().can_change_to(&target_type) {
self.activate_action(
"sidebar.set-active-drop-category",
Some(&Some(u32::from(target_type)).to_variant()),
);
return true;
}
} else if let Some(entry_type) = self.entry_type() {
if room.category() == RoomType::Left && entry_type == EntryType::Forget {
self.parent().unwrap().add_css_class("drop-active");
self.activate_action("sidebar.set-active-drop-category", None);
return true;
}
}
}
false
}
fn drop_leave(&self) {
self.parent().unwrap().remove_css_class("drop-active");
self.activate_action("sidebar.set-active-drop-category", None);
}
fn drop_end(&self, value: &glib::Value) -> bool {
let mut ret = false;
if let Ok(room) = value.get::<Room>() {
if let Some(target_type) = self.room_type() {
if room.category().can_change_to(&target_type) {
room.set_category(target_type);
ret = true;
}
} else if let Some(entry_type) = self.entry_type() {
if room.category() == RoomType::Left && entry_type == EntryType::Forget {
room.forget();
ret = true;
}
}
}
self.activate_action("sidebar.set-drop-source-type", None);
ret
}
}
impl Default for Row {