Browse Source
The strings for the choices are too long, they could be ellipsized in a popover. So we use a new subpage instead, where the strings can be as long as they need to be. This removes ComboLoadingRow since it is now unused.fractal-13
11 changed files with 360 additions and 402 deletions
@ -1,47 +0,0 @@
|
||||
using Gtk 4.0; |
||||
using Adw 1; |
||||
|
||||
template $ComboLoadingRow: Adw.ActionRow { |
||||
selectable: false; |
||||
activatable: bind template.read-only inverted; |
||||
|
||||
styles [ |
||||
"combo", |
||||
"property", |
||||
] |
||||
|
||||
Gtk.Box arrow_box { |
||||
valign: center; |
||||
|
||||
$LoadingBin loading_bin { |
||||
child: Gtk.Image { |
||||
visible: bind template.read-only inverted; |
||||
icon-name: "pan-down-symbolic"; |
||||
accessible-role: presentation; |
||||
|
||||
styles [ |
||||
"dropdown-arrow", |
||||
] |
||||
}; |
||||
} |
||||
|
||||
Gtk.Popover popover { |
||||
notify::visible => $popover_visible() swapped; |
||||
|
||||
styles [ |
||||
"menu", |
||||
] |
||||
|
||||
child: Gtk.ScrolledWindow { |
||||
hscrollbar-policy: never; |
||||
max-content-height: 400; |
||||
propagate-natural-width: true; |
||||
propagate-natural-height: true; |
||||
|
||||
child: Gtk.ListBox list { |
||||
row-activated => $row_activated() swapped; |
||||
}; |
||||
}; |
||||
} |
||||
} |
||||
} |
||||
@ -1,278 +0,0 @@
|
||||
use adw::{prelude::*, subclass::prelude::*}; |
||||
use gtk::{CompositeTemplate, glib, glib::clone, pango}; |
||||
|
||||
use crate::{components::LoadingBin, utils::BoundObject}; |
||||
|
||||
mod imp { |
||||
use std::{ |
||||
cell::{Cell, RefCell}, |
||||
marker::PhantomData, |
||||
}; |
||||
|
||||
use glib::subclass::InitializingObject; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Debug, Default, CompositeTemplate, glib::Properties)] |
||||
#[template(resource = "/org/gnome/Fractal/ui/components/rows/combo_loading_row.ui")] |
||||
#[properties(wrapper_type = super::ComboLoadingRow)] |
||||
pub struct ComboLoadingRow { |
||||
#[template_child] |
||||
loading_bin: TemplateChild<LoadingBin>, |
||||
#[template_child] |
||||
popover: TemplateChild<gtk::Popover>, |
||||
#[template_child] |
||||
list: TemplateChild<gtk::ListBox>, |
||||
/// The string model to build the list.
|
||||
#[property(get, set = Self::set_string_model, explicit_notify, nullable)] |
||||
string_model: BoundObject<gtk::StringList>, |
||||
/// The position of the selected string.
|
||||
#[property(get, default = gtk::INVALID_LIST_POSITION)] |
||||
selected: Cell<u32>, |
||||
/// The selected string.
|
||||
#[property(get, set = Self::set_selected_string, explicit_notify, nullable)] |
||||
selected_string: RefCell<Option<String>>, |
||||
/// Whether the row is loading.
|
||||
#[property(get = Self::is_loading, set = Self::set_is_loading)] |
||||
is_loading: PhantomData<bool>, |
||||
/// Whether the row is read-only.
|
||||
#[property(get, set = Self::set_read_only, explicit_notify)] |
||||
read_only: Cell<bool>, |
||||
selected_handlers: RefCell<Vec<glib::SignalHandlerId>>, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for ComboLoadingRow { |
||||
const NAME: &'static str = "ComboLoadingRow"; |
||||
type Type = super::ComboLoadingRow; |
||||
type ParentType = adw::ActionRow; |
||||
|
||||
fn class_init(klass: &mut Self::Class) { |
||||
Self::bind_template(klass); |
||||
Self::bind_template_callbacks(klass); |
||||
|
||||
klass.set_accessible_role(gtk::AccessibleRole::ComboBox); |
||||
} |
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) { |
||||
obj.init_template(); |
||||
} |
||||
} |
||||
|
||||
#[glib::derived_properties] |
||||
impl ObjectImpl for ComboLoadingRow {} |
||||
|
||||
impl WidgetImpl for ComboLoadingRow {} |
||||
impl ListBoxRowImpl for ComboLoadingRow {} |
||||
impl PreferencesRowImpl for ComboLoadingRow {} |
||||
|
||||
impl ActionRowImpl for ComboLoadingRow { |
||||
fn activate(&self) { |
||||
if !self.is_loading() { |
||||
self.popover.popup(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[gtk::template_callbacks] |
||||
impl ComboLoadingRow { |
||||
/// Set the string model to build the list.
|
||||
fn set_string_model(&self, model: Option<gtk::StringList>) { |
||||
if self.string_model.obj() == model { |
||||
return; |
||||
} |
||||
let obj = self.obj(); |
||||
|
||||
for handler in self.selected_handlers.take() { |
||||
obj.disconnect(handler); |
||||
} |
||||
self.string_model.disconnect_signals(); |
||||
|
||||
self.list.bind_model( |
||||
model.as_ref(), |
||||
clone!( |
||||
#[weak] |
||||
obj, |
||||
#[upgrade_or_else] |
||||
|| { gtk::ListBoxRow::new().upcast() }, |
||||
move |item| { |
||||
let Some(item) = item.downcast_ref::<gtk::StringObject>() else { |
||||
return gtk::ListBoxRow::new().upcast(); |
||||
}; |
||||
|
||||
let string = item.string(); |
||||
let child = gtk::Box::new(gtk::Orientation::Horizontal, 6); |
||||
|
||||
let label = gtk::Label::builder() |
||||
.xalign(0.0) |
||||
.ellipsize(pango::EllipsizeMode::End) |
||||
.max_width_chars(40) |
||||
.valign(gtk::Align::Center) |
||||
.label(string) |
||||
.build(); |
||||
child.append(&label); |
||||
|
||||
let icon = gtk::Image::builder() |
||||
.accessible_role(gtk::AccessibleRole::Presentation) |
||||
.icon_name("object-select-symbolic") |
||||
.build(); |
||||
|
||||
let selected_handler = obj.connect_selected_string_notify(clone!( |
||||
#[weak] |
||||
label, |
||||
#[weak] |
||||
icon, |
||||
move |obj| { |
||||
let is_selected = |
||||
obj.selected_string().is_some_and(|s| s == label.label()); |
||||
let opacity = if is_selected { 1.0 } else { 0.0 }; |
||||
icon.set_opacity(opacity); |
||||
} |
||||
)); |
||||
obj.imp() |
||||
.selected_handlers |
||||
.borrow_mut() |
||||
.push(selected_handler); |
||||
|
||||
let is_selected = obj.selected_string().is_some_and(|s| s == label.label()); |
||||
let opacity = if is_selected { 1.0 } else { 0.0 }; |
||||
icon.set_opacity(opacity); |
||||
child.append(&icon); |
||||
|
||||
gtk::ListBoxRow::builder().child(&child).build().upcast() |
||||
} |
||||
), |
||||
); |
||||
|
||||
if let Some(model) = model { |
||||
let items_changed_handler = model.connect_items_changed(clone!( |
||||
#[weak(rename_to = imp)] |
||||
self, |
||||
move |_, _, _, _| { |
||||
imp.update_selected(); |
||||
} |
||||
)); |
||||
|
||||
self.string_model.set(model, vec![items_changed_handler]); |
||||
} |
||||
|
||||
self.update_selected(); |
||||
obj.notify_string_model(); |
||||
} |
||||
|
||||
/// Set whether the row is loading.
|
||||
fn set_selected_string(&self, string: Option<String>) { |
||||
if *self.selected_string.borrow() == string { |
||||
return; |
||||
} |
||||
let obj = self.obj(); |
||||
|
||||
obj.set_subtitle(string.as_deref().unwrap_or_default()); |
||||
self.selected_string.replace(string); |
||||
|
||||
self.update_selected(); |
||||
obj.notify_selected_string(); |
||||
} |
||||
|
||||
/// Update the position of the selected string.
|
||||
fn update_selected(&self) { |
||||
let mut selected = gtk::INVALID_LIST_POSITION; |
||||
|
||||
if let Some((string_model, selected_string)) = self |
||||
.string_model |
||||
.obj() |
||||
.zip(self.selected_string.borrow().clone()) |
||||
{ |
||||
for (pos, item) in string_model.iter::<glib::Object>().enumerate() { |
||||
let Some(item) = item.ok().and_downcast::<gtk::StringObject>() else { |
||||
// The iterator is broken.
|
||||
break; |
||||
}; |
||||
|
||||
if item.string() == selected_string { |
||||
selected = pos as u32; |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
if self.selected.get() == selected { |
||||
return; |
||||
} |
||||
|
||||
self.selected.set(selected); |
||||
self.obj().notify_selected(); |
||||
} |
||||
|
||||
/// Whether the row is loading.
|
||||
fn is_loading(&self) -> bool { |
||||
self.loading_bin.is_loading() |
||||
} |
||||
|
||||
/// Set whether the row is loading.
|
||||
fn set_is_loading(&self, loading: bool) { |
||||
if self.is_loading() == loading { |
||||
return; |
||||
} |
||||
|
||||
self.loading_bin.set_is_loading(loading); |
||||
self.obj().notify_is_loading(); |
||||
} |
||||
|
||||
/// Set whether the row is read-only.
|
||||
fn set_read_only(&self, read_only: bool) { |
||||
if self.read_only.get() == read_only { |
||||
return; |
||||
} |
||||
let obj = self.obj(); |
||||
|
||||
self.read_only.set(read_only); |
||||
|
||||
obj.update_property(&[gtk::accessible::Property::ReadOnly(read_only)]); |
||||
obj.notify_read_only(); |
||||
} |
||||
|
||||
/// A row was activated.
|
||||
#[template_callback] |
||||
fn row_activated(&self, row: >k::ListBoxRow) { |
||||
let Some(string) = row |
||||
.child() |
||||
.and_downcast::<gtk::Box>() |
||||
.and_then(|b| b.first_child()) |
||||
.and_downcast::<gtk::Label>() |
||||
.map(|l| l.label()) |
||||
else { |
||||
return; |
||||
}; |
||||
|
||||
self.popover.popdown(); |
||||
self.set_selected_string(Some(string.into())); |
||||
} |
||||
|
||||
/// The popover's visibility changed.
|
||||
#[template_callback] |
||||
fn popover_visible(&self) { |
||||
let obj = self.obj(); |
||||
let is_visible = self.popover.is_visible(); |
||||
|
||||
if is_visible { |
||||
obj.add_css_class("has-open-popup"); |
||||
} else { |
||||
obj.remove_css_class("has-open-popup"); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// An `AdwActionRow` behaving like a combo box, with a loading state.
|
||||
pub struct ComboLoadingRow(ObjectSubclass<imp::ComboLoadingRow>) |
||||
@extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, adw::ActionRow, |
||||
@implements gtk::Actionable, gtk::Accessible; |
||||
} |
||||
|
||||
impl ComboLoadingRow { |
||||
pub fn new() -> Self { |
||||
glib::Object::new() |
||||
} |
||||
} |
||||
@ -0,0 +1,79 @@
|
||||
using Gtk 4.0; |
||||
using Adw 1; |
||||
|
||||
template $RoomDetailsHistoryVisibilitySubpage: Adw.NavigationPage { |
||||
title: _("Who Can Read History"); |
||||
|
||||
styles [ |
||||
"form-page", |
||||
] |
||||
|
||||
child: Adw.ToolbarView { |
||||
[top] |
||||
Adw.HeaderBar { |
||||
show-back-button: false; |
||||
|
||||
[start] |
||||
Gtk.Button { |
||||
icon-name: "go-previous-symbolic"; |
||||
tooltip-text: _("Back"); |
||||
clicked => $go_back() swapped; |
||||
|
||||
styles [ |
||||
"back", |
||||
] |
||||
} |
||||
|
||||
[end] |
||||
$LoadingButton save_button { |
||||
sensitive: bind template.changed; |
||||
content-label: _("_Save"); |
||||
use-underline: true; |
||||
clicked => $save() swapped; |
||||
|
||||
styles [ |
||||
"suggested-action", |
||||
] |
||||
} |
||||
} |
||||
|
||||
content: Gtk.ScrolledWindow scrolled_window { |
||||
hscrollbar-policy: never; |
||||
propagate-natural-height: true; |
||||
|
||||
child: Adw.Clamp { |
||||
child: Gtk.ListBox { |
||||
valign: start; |
||||
|
||||
styles [ |
||||
"boxed-list", |
||||
] |
||||
|
||||
$CheckLoadingRow { |
||||
title: _("Anyone, even if they are not in the room"); |
||||
action-name: "history-visibility.set-value"; |
||||
action-target: "'world-readable'"; |
||||
} |
||||
|
||||
$CheckLoadingRow { |
||||
title: _("Members only, since this option was selected"); |
||||
action-name: "history-visibility.set-value"; |
||||
action-target: "'shared'"; |
||||
} |
||||
|
||||
$CheckLoadingRow { |
||||
title: _("Members only, since they were invited"); |
||||
action-name: "history-visibility.set-value"; |
||||
action-target: "'invited'"; |
||||
} |
||||
|
||||
$CheckLoadingRow { |
||||
title: _("Members only, since they joined the room"); |
||||
action-name: "history-visibility.set-value"; |
||||
action-target: "'joined'"; |
||||
} |
||||
}; |
||||
}; |
||||
}; |
||||
}; |
||||
} |
||||
@ -0,0 +1,241 @@
|
||||
use adw::{prelude::*, subclass::prelude::*}; |
||||
use gettextrs::gettext; |
||||
use gtk::{glib, glib::clone}; |
||||
use ruma::events::{ |
||||
StateEventType, |
||||
room::{history_visibility::RoomHistoryVisibilityEventContent, power_levels::PowerLevelAction}, |
||||
}; |
||||
use tracing::error; |
||||
|
||||
use crate::{ |
||||
components::{CheckLoadingRow, LoadingButton, UnsavedChangesResponse, unsaved_changes_dialog}, |
||||
session::model::{HistoryVisibilityValue, Room}, |
||||
spawn_tokio, toast, |
||||
utils::BoundObjectWeakRef, |
||||
}; |
||||
|
||||
mod imp { |
||||
use std::cell::{Cell, RefCell}; |
||||
|
||||
use glib::subclass::InitializingObject; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)] |
||||
#[template(
|
||||
resource = "/org/gnome/Fractal/ui/session/view/content/room_details/history_visibility_subpage.ui" |
||||
)] |
||||
#[properties(wrapper_type = super::HistoryVisibilitySubpage)] |
||||
pub struct HistoryVisibilitySubpage { |
||||
#[template_child] |
||||
save_button: TemplateChild<LoadingButton>, |
||||
/// The presented room.
|
||||
#[property(get, set = Self::set_room, explicit_notify, nullable)] |
||||
room: BoundObjectWeakRef<Room>, |
||||
/// The local value of the history visibility.
|
||||
#[property(get, set = Self::set_local_value, explicit_notify, builder(HistoryVisibilityValue::default()))] |
||||
local_value: Cell<HistoryVisibilityValue>, |
||||
/// Whether the history visibility was changed by the user.
|
||||
#[property(get)] |
||||
changed: Cell<bool>, |
||||
permissions_handler: RefCell<Option<glib::SignalHandlerId>>, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for HistoryVisibilitySubpage { |
||||
const NAME: &'static str = "RoomDetailsHistoryVisibilitySubpage"; |
||||
type Type = super::HistoryVisibilitySubpage; |
||||
type ParentType = adw::NavigationPage; |
||||
|
||||
fn class_init(klass: &mut Self::Class) { |
||||
CheckLoadingRow::ensure_type(); |
||||
|
||||
Self::bind_template(klass); |
||||
Self::bind_template_callbacks(klass); |
||||
|
||||
klass.install_property_action("history-visibility.set-value", "local-value"); |
||||
} |
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) { |
||||
obj.init_template(); |
||||
} |
||||
} |
||||
|
||||
#[glib::derived_properties] |
||||
impl ObjectImpl for HistoryVisibilitySubpage { |
||||
fn dispose(&self) { |
||||
self.disconnect_signals(); |
||||
} |
||||
} |
||||
|
||||
impl WidgetImpl for HistoryVisibilitySubpage {} |
||||
impl NavigationPageImpl for HistoryVisibilitySubpage {} |
||||
|
||||
#[gtk::template_callbacks] |
||||
impl HistoryVisibilitySubpage { |
||||
/// Set the presented room.
|
||||
fn set_room(&self, room: Option<&Room>) { |
||||
let Some(room) = room else { |
||||
// Just ignore when room is missing.
|
||||
return; |
||||
}; |
||||
|
||||
self.disconnect_signals(); |
||||
|
||||
let permissions_handler = room.permissions().connect_changed(clone!( |
||||
#[weak(rename_to = imp)] |
||||
self, |
||||
move |_| { |
||||
imp.update(); |
||||
} |
||||
)); |
||||
self.permissions_handler.replace(Some(permissions_handler)); |
||||
|
||||
let history_visibility_handler = room.connect_history_visibility_notify(clone!( |
||||
#[weak(rename_to = imp)] |
||||
self, |
||||
move |_| { |
||||
imp.update(); |
||||
} |
||||
)); |
||||
|
||||
self.room.set(room, vec![history_visibility_handler]); |
||||
|
||||
self.update(); |
||||
self.obj().notify_room(); |
||||
} |
||||
|
||||
/// Update the subpage.
|
||||
fn update(&self) { |
||||
let Some(room) = self.room.obj() else { |
||||
return; |
||||
}; |
||||
|
||||
self.set_local_value(room.history_visibility()); |
||||
|
||||
self.save_button.set_is_loading(false); |
||||
self.update_changed(); |
||||
} |
||||
|
||||
/// Set the local value of the history visibility.
|
||||
fn set_local_value(&self, value: HistoryVisibilityValue) { |
||||
if self.local_value.get() == value { |
||||
return; |
||||
} |
||||
|
||||
self.local_value.set(value); |
||||
|
||||
self.update_changed(); |
||||
self.obj().notify_local_value(); |
||||
} |
||||
|
||||
/// Whether we can change the history visibility.
|
||||
fn can_change(&self) -> bool { |
||||
let Some(room) = self.room.obj() else { |
||||
return false; |
||||
}; |
||||
|
||||
if room.history_visibility() == HistoryVisibilityValue::Unsupported { |
||||
return false; |
||||
} |
||||
|
||||
room.permissions() |
||||
.is_allowed_to(PowerLevelAction::SendState( |
||||
StateEventType::RoomHistoryVisibility, |
||||
)) |
||||
} |
||||
|
||||
/// Update whether the join rule was changed by the user.
|
||||
#[template_callback] |
||||
fn update_changed(&self) { |
||||
let Some(room) = self.room.obj() else { |
||||
return; |
||||
}; |
||||
|
||||
let changed = if self.can_change() { |
||||
let current_join_rule = room.history_visibility(); |
||||
let new_join_rule = self.local_value.get(); |
||||
|
||||
current_join_rule != new_join_rule |
||||
} else { |
||||
false |
||||
}; |
||||
|
||||
self.changed.set(changed); |
||||
self.obj().notify_changed(); |
||||
} |
||||
|
||||
/// Save the changes of this page.
|
||||
#[template_callback] |
||||
async fn save(&self) { |
||||
if !self.changed.get() { |
||||
// Nothing to do.
|
||||
return; |
||||
} |
||||
|
||||
let Some(room) = self.room.obj() else { |
||||
return; |
||||
}; |
||||
|
||||
self.save_button.set_is_loading(true); |
||||
|
||||
let content = RoomHistoryVisibilityEventContent::new(self.local_value.get().into()); |
||||
|
||||
let matrix_room = room.matrix_room().clone(); |
||||
let handle = spawn_tokio!(async move { matrix_room.send_state_event(content).await }); |
||||
|
||||
if let Err(error) = handle.await.unwrap() { |
||||
error!("Could not change room history visibility: {error}"); |
||||
toast!(self.obj(), gettext("Could not change who can read history")); |
||||
self.save_button.set_is_loading(false); |
||||
} |
||||
} |
||||
|
||||
/// Go back to the previous page in the room details.
|
||||
///
|
||||
/// If there are changes in the page, ask the user to confirm.
|
||||
#[template_callback] |
||||
async fn go_back(&self) { |
||||
let obj = self.obj(); |
||||
let mut reset_after = false; |
||||
|
||||
if self.changed.get() { |
||||
match unsaved_changes_dialog(&*obj).await { |
||||
UnsavedChangesResponse::Save => self.save().await, |
||||
UnsavedChangesResponse::Discard => reset_after = true, |
||||
UnsavedChangesResponse::Cancel => return, |
||||
} |
||||
} |
||||
|
||||
let _ = obj.activate_action("navigation.pop", None); |
||||
|
||||
if reset_after { |
||||
self.update(); |
||||
} |
||||
} |
||||
|
||||
/// Disconnect all the signal handlers.
|
||||
fn disconnect_signals(&self) { |
||||
if let Some(room) = self.room.obj() { |
||||
if let Some(handler) = self.permissions_handler.take() { |
||||
room.permissions().disconnect(handler); |
||||
} |
||||
} |
||||
|
||||
self.room.disconnect_signals(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// Subpage to select the history visibility of a room.
|
||||
pub struct HistoryVisibilitySubpage(ObjectSubclass<imp::HistoryVisibilitySubpage>) |
||||
@extends gtk::Widget, adw::NavigationPage, @implements gtk::Accessible; |
||||
} |
||||
|
||||
impl HistoryVisibilitySubpage { |
||||
/// Construct a new `HistoryVisibilitySubpage` for the given room.
|
||||
pub fn new(room: &Room) -> Self { |
||||
glib::Object::builder().property("room", room).build() |
||||
} |
||||
} |
||||
Loading…
Reference in new issue