377 lines
13 KiB
Rust
377 lines
13 KiB
Rust
use adw::{prelude::*, subclass::prelude::*};
|
|
use gettextrs::gettext;
|
|
use gtk::{accessible::Relation, gdk, glib, glib::clone};
|
|
|
|
use super::{CategoryRow, IconItemRow, RoomRow, Sidebar, VerificationRow};
|
|
use crate::{
|
|
session::model::{
|
|
Category, CategoryType, IconItem, IdentityVerification, ItemType, Room, RoomType,
|
|
SidebarItem,
|
|
},
|
|
spawn, toast,
|
|
utils::{message_dialog, BoundObjectWeakRef},
|
|
};
|
|
|
|
mod imp {
|
|
use std::{cell::RefCell, marker::PhantomData};
|
|
|
|
use super::*;
|
|
|
|
#[derive(Debug, Default, glib::Properties)]
|
|
#[properties(wrapper_type = super::Row)]
|
|
pub struct Row {
|
|
/// The ancestor sidebar of this row.
|
|
#[property(get, set = Self::set_sidebar, construct_only)]
|
|
pub sidebar: BoundObjectWeakRef<Sidebar>,
|
|
/// The list row to track for expander state.
|
|
#[property(get, set = Self::set_list_row, explicit_notify, nullable)]
|
|
pub list_row: RefCell<Option<gtk::TreeListRow>>,
|
|
/// The sidebar item of this row.
|
|
#[property(get = Self::item)]
|
|
pub item: PhantomData<Option<SidebarItem>>,
|
|
pub bindings: RefCell<Vec<glib::Binding>>,
|
|
}
|
|
|
|
#[glib::object_subclass]
|
|
impl ObjectSubclass for Row {
|
|
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");
|
|
klass.set_accessible_role(gtk::AccessibleRole::ListItem);
|
|
}
|
|
}
|
|
|
|
#[glib::derived_properties]
|
|
impl ObjectImpl for Row {
|
|
fn constructed(&self) {
|
|
self.parent_constructed();
|
|
let obj = self.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 {}
|
|
impl BinImpl for Row {}
|
|
|
|
impl Row {
|
|
/// Set the ancestor sidebar of this row.
|
|
fn set_sidebar(&self, sidebar: Sidebar) {
|
|
let obj = self.obj();
|
|
|
|
let drop_source_type_handler =
|
|
sidebar.connect_drop_source_category_type_notify(clone!(@weak obj => move |_| {
|
|
obj.update_for_drop_source_type();
|
|
}));
|
|
|
|
let drop_active_target_type_handler = sidebar
|
|
.connect_drop_active_target_category_type_notify(clone!(@weak obj => move |_| {
|
|
obj.update_for_drop_active_target_type();
|
|
}));
|
|
|
|
self.sidebar.set(
|
|
&sidebar,
|
|
vec![drop_source_type_handler, drop_active_target_type_handler],
|
|
);
|
|
}
|
|
|
|
/// Set the list row to track for expander state.
|
|
fn set_list_row(&self, list_row: Option<gtk::TreeListRow>) {
|
|
if self.list_row.borrow().clone() == list_row {
|
|
return;
|
|
}
|
|
let obj = self.obj();
|
|
|
|
for binding in self.bindings.take() {
|
|
binding.unbind();
|
|
}
|
|
|
|
self.list_row.replace(list_row.clone());
|
|
|
|
let mut bindings = vec![];
|
|
if let Some((row, item)) = list_row.zip(self.item()) {
|
|
if let Some(category) = item.downcast_ref::<Category>() {
|
|
let child = if let Some(child) = obj.child().and_downcast::<CategoryRow>() {
|
|
child
|
|
} else {
|
|
let child = CategoryRow::new();
|
|
obj.set_child(Some(&child));
|
|
obj.update_relation(&[Relation::LabelledBy(&[child.labelled_by()])]);
|
|
child
|
|
};
|
|
child.set_category(Some(category.clone()));
|
|
|
|
bindings.push(
|
|
row.bind_property("expanded", &child, "expanded")
|
|
.sync_create()
|
|
.build(),
|
|
);
|
|
} else if let Some(room) = item.downcast_ref::<Room>() {
|
|
let child = if let Some(child) = obj.child().and_downcast::<RoomRow>() {
|
|
child
|
|
} else {
|
|
let child = RoomRow::new();
|
|
obj.set_child(Some(&child));
|
|
child
|
|
};
|
|
|
|
child.set_room(Some(room.clone()));
|
|
} else if let Some(icon_item) = item.downcast_ref::<IconItem>() {
|
|
let child = if let Some(child) = obj.child().and_downcast::<IconItemRow>() {
|
|
child
|
|
} else {
|
|
let child = IconItemRow::new();
|
|
obj.set_child(Some(&child));
|
|
child
|
|
};
|
|
|
|
child.set_icon_item(Some(icon_item.clone()));
|
|
} else if let Some(verification) = item.downcast_ref::<IdentityVerification>() {
|
|
let child = if let Some(child) = obj.child().and_downcast::<VerificationRow>() {
|
|
child
|
|
} else {
|
|
let child = VerificationRow::new();
|
|
obj.set_child(Some(&child));
|
|
child
|
|
};
|
|
|
|
child.set_identity_verification(Some(verification.clone()));
|
|
} else {
|
|
panic!("Wrong row item: {item:?}");
|
|
}
|
|
|
|
obj.update_for_drop_source_type();
|
|
}
|
|
|
|
self.bindings.replace(bindings);
|
|
|
|
obj.notify_item();
|
|
obj.notify_list_row();
|
|
}
|
|
|
|
/// The sidebar item of this row.
|
|
fn item(&self) -> Option<SidebarItem> {
|
|
self.list_row
|
|
.borrow()
|
|
.as_ref()
|
|
.and_then(|r| r.item())
|
|
.and_downcast()
|
|
}
|
|
}
|
|
}
|
|
|
|
glib::wrapper! {
|
|
/// A row of the sidebar.
|
|
pub struct Row(ObjectSubclass<imp::Row>)
|
|
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
|
|
}
|
|
|
|
impl Row {
|
|
pub fn new(sidebar: &Sidebar) -> Self {
|
|
glib::Object::builder()
|
|
.property("sidebar", sidebar)
|
|
.property("focusable", true)
|
|
.build()
|
|
}
|
|
|
|
/// 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.r#type()).ok())
|
|
}
|
|
}
|
|
|
|
/// Get the [`ItemType`] of this item.
|
|
///
|
|
/// If this is not an [`IconItem`], returns `None`.
|
|
pub fn item_type(&self) -> Option<ItemType> {
|
|
self.item()
|
|
.and_downcast_ref::<IconItem>()
|
|
.map(|i| i.r#type())
|
|
}
|
|
|
|
/// Handle the drag-n-drop hovering this row.
|
|
fn drop_accept(&self, drop: &gdk::Drop) -> bool {
|
|
let Some(sidebar) = self.sidebar() else {
|
|
return false;
|
|
};
|
|
|
|
let room = drop
|
|
.drag()
|
|
.map(|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) {
|
|
sidebar.set_drop_active_target_type(Some(target_type));
|
|
return true;
|
|
}
|
|
} else if let Some(item_type) = self.item_type() {
|
|
if room.category() == RoomType::Left && item_type == ItemType::Forget {
|
|
self.add_css_class("drop-active");
|
|
sidebar.set_drop_active_target_type(None);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Handle the drag-n-drop leaving this row.
|
|
fn drop_leave(&self) {
|
|
self.remove_css_class("drop-active");
|
|
if let Some(sidebar) = self.sidebar() {
|
|
sidebar.set_drop_active_target_type(None);
|
|
}
|
|
}
|
|
|
|
/// Handle the drop on this row.
|
|
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) {
|
|
spawn!(clone!(@weak self as obj, @weak room => async move {
|
|
obj.set_room_category(&room, target_type).await;
|
|
}));
|
|
ret = true;
|
|
}
|
|
} else if let Some(item_type) = self.item_type() {
|
|
if room.category() == RoomType::Left && item_type == ItemType::Forget {
|
|
spawn!(clone!(@strong self as obj, @weak room => async move {
|
|
obj.forget_room(&room).await;
|
|
}));
|
|
ret = true;
|
|
}
|
|
}
|
|
}
|
|
if let Some(sidebar) = self.sidebar() {
|
|
sidebar.set_drop_source_type(None);
|
|
}
|
|
ret
|
|
}
|
|
|
|
/// Change the category of the given room room.
|
|
async fn set_room_category(&self, room: &Room, category: RoomType) {
|
|
let Some(window) = self.root().and_downcast::<gtk::Window>() else {
|
|
return;
|
|
};
|
|
|
|
if category == RoomType::Left && !message_dialog::confirm_leave_room(room, &window).await {
|
|
return;
|
|
}
|
|
|
|
let previous_category = room.category();
|
|
if room.set_category(category).await.is_err() {
|
|
toast!(
|
|
self,
|
|
gettext(
|
|
// Translators: Do NOT translate the content between '{' and '}', this is a variable name.
|
|
"Failed to move {room} from {previous_category} to {new_category}.",
|
|
),
|
|
@room,
|
|
previous_category = previous_category.to_string(),
|
|
new_category = category.to_string(),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Forget the given room.
|
|
async fn forget_room(&self, room: &Room) {
|
|
if room.forget().await.is_err() {
|
|
toast!(
|
|
self,
|
|
// Translators: Do NOT translate the content between '{' and '}', this is a variable name.
|
|
gettext("Failed to forget {room}."),
|
|
@room,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Update the disabled or empty state of this drop target.
|
|
fn update_for_drop_source_type(&self) {
|
|
let source_type = self.sidebar().and_then(|s| s.drop_source_type());
|
|
|
|
if let Some(source_type) = source_type {
|
|
if self
|
|
.room_type()
|
|
.is_some_and(|row_type| source_type.can_change_to(row_type))
|
|
{
|
|
self.remove_css_class("drop-disabled");
|
|
|
|
if self
|
|
.item()
|
|
.and_downcast::<Category>()
|
|
.is_some_and(|category| category.empty())
|
|
{
|
|
self.add_css_class("drop-empty");
|
|
} else {
|
|
self.remove_css_class("drop-empty");
|
|
}
|
|
} else {
|
|
let is_forget_item = self
|
|
.item_type()
|
|
.is_some_and(|item_type| item_type == ItemType::Forget);
|
|
if is_forget_item && source_type == RoomType::Left {
|
|
self.remove_css_class("drop-disabled");
|
|
} else {
|
|
self.add_css_class("drop-disabled");
|
|
self.remove_css_class("drop-empty");
|
|
}
|
|
}
|
|
} else {
|
|
// Clear style
|
|
self.remove_css_class("drop-disabled");
|
|
self.remove_css_class("drop-empty");
|
|
self.remove_css_class("drop-active");
|
|
};
|
|
|
|
if let Some(category_row) = self.child().and_downcast::<CategoryRow>() {
|
|
category_row.set_show_label_for_category(
|
|
source_type.map(CategoryType::from).unwrap_or_default(),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Update the active state of this drop target.
|
|
fn update_for_drop_active_target_type(&self) {
|
|
let Some(room_type) = self.room_type() else {
|
|
return;
|
|
};
|
|
let target_type = self.sidebar().and_then(|s| s.drop_active_target_type());
|
|
|
|
if target_type.is_some_and(|target_type| target_type == room_type) {
|
|
self.add_css_class("drop-active");
|
|
} else {
|
|
self.remove_css_class("drop-active");
|
|
}
|
|
}
|
|
}
|