Browse Source

sidebar: Add Explore entry

merge-requests/1327/merge
Julian Sparber 5 years ago
parent
commit
bd13ae4ef2
  1. 151
      data/resources/icons/scalable/status/explore-symbolic.svg
  2. 2
      data/resources/resources.gresource.xml
  3. 5
      data/resources/style.css
  4. 2
      data/resources/ui/session.ui
  5. 36
      data/resources/ui/sidebar-entry-row.ui
  6. 2
      po/POTFILES.in
  7. 61
      src/session/content/content.rs
  8. 27
      src/session/content/content_type.rs
  9. 2
      src/session/content/mod.rs
  10. 43
      src/session/mod.rs
  11. 109
      src/session/sidebar/entry.rs
  12. 101
      src/session/sidebar/entry_row.rs
  13. 24
      src/session/sidebar/item_list.rs
  14. 4
      src/session/sidebar/mod.rs
  15. 17
      src/session/sidebar/row.rs
  16. 152
      src/session/sidebar/selection.rs
  17. 44
      src/session/sidebar/sidebar.rs

151
data/resources/icons/scalable/status/explore-symbolic.svg

@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<filter id="a" height="100%" width="100%" x="0%" y="0%">
<feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
</filter>
<mask id="b">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
</g>
</mask>
<clipPath id="c">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="d">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="e">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="f">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="g">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="h">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="i">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="j">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="k">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="l">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="m">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="n">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
</g>
</mask>
<clipPath id="o">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="p">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
</g>
</mask>
<clipPath id="q">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="r">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="s">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="t">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
</g>
</mask>
<clipPath id="u">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="v">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
</g>
</mask>
<clipPath id="w">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="x">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="y">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<mask id="z">
<g filter="url(#a)">
<path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
</g>
</mask>
<clipPath id="A">
<path d="m 0 0 h 1024 v 800 h -1024 z"/>
</clipPath>
<g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -600 -80)">
<path d="m 562.460938 212.058594 h 10.449218 c -1.183594 0.492187 -1.296875 2.460937 0 3 h -10.449218 z m 0 0" fill="#2e3436"/>
</g>
<path d="m 8 1 c -0.207031 0 -0.390625 0.125 -0.46875 0.320312 l -1.726562 4.488282 l -4.484376 1.722656 c -0.425781 0.167969 -0.425781 0.769531 0 0.9375 l 4.484376 1.722656 l 1.726562 4.488282 c 0.167969 0.425781 0.769531 0.425781 0.9375 0 l 1.726562 -4.488282 l 4.484376 -1.722656 c 0.425781 -0.167969 0.425781 -0.769531 0 -0.9375 l -4.484376 -1.722656 l -1.726562 -4.488282 c -0.074219 -0.191406 -0.261719 -0.320312 -0.46875 -0.320312 z m 0 6 c 0.550781 0 1 0.449219 1 1 s -0.449219 1 -1 1 s -1 -0.449219 -1 -1 s 0.449219 -1 1 -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
<path d="m 3.480469 3 c -0.363281 0.011719 -0.589844 0.398438 -0.429688 0.722656 l 0.871094 1.738282 l 1.109375 -0.429688 l 0.429688 -1.109375 l -1.738282 -0.871094 c -0.074218 -0.035156 -0.15625 -0.054687 -0.242187 -0.050781 z m 9.039062 0 c -0.085937 -0.003906 -0.167969 0.015625 -0.242187 0.050781 l -1.738282 0.871094 l 0.429688 1.109375 l 1.109375 0.429688 l 0.871094 -1.738282 c 0.160156 -0.324218 -0.066407 -0.710937 -0.429688 -0.722656 z m -8.597656 7.539062 l -0.871094 1.738282 c -0.210937 0.429687 0.242188 0.882812 0.671875 0.671875 l 1.738282 -0.871094 l -0.429688 -1.109375 z m 8.15625 0 l -1.109375 0.429688 l -0.429688 1.109375 l 1.738282 0.871094 c 0.429687 0.210937 0.882812 -0.242188 0.671875 -0.671875 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
<g clip-path="url(#e)" mask="url(#d)" transform="matrix(1 0 0 1 -600 -80)">
<path d="m 16 632 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#g)" mask="url(#f)" transform="matrix(1 0 0 1 -600 -80)">
<path d="m 17 631 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -600 -80)">
<path d="m 18 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#k)" mask="url(#j)" transform="matrix(1 0 0 1 -600 -80)">
<path d="m 16 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#m)" mask="url(#l)" transform="matrix(1 0 0 1 -600 -80)">
<path d="m 17 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#o)" mask="url(#n)" transform="matrix(1 0 0 1 -600 -80)">
<path d="m 19 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
</g>
<g clip-path="url(#q)" mask="url(#p)" transform="matrix(1 0 0 1 -600 -80)">
<path d="m 136 660 v 7 h 7 v -7 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#s)" mask="url(#r)" transform="matrix(1 0 0 1 -600 -80)">
<path d="m 199 642 h 3 v 12 h -3 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#u)" mask="url(#t)" transform="matrix(1 0 0 1 -600 -80)">
<path d="m 209.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#w)" mask="url(#v)" transform="matrix(1 0 0 1 -600 -80)">
<path d="m 206.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#y)" mask="url(#x)" transform="matrix(1 0 0 1 -600 -80)">
<path d="m 229.5 143.160156 c -0.546875 0 -1 0.457032 -1 1 c 0 0.546875 0.453125 1 1 1 s 1 -0.453125 1 -1 c 0 -0.542968 -0.453125 -1 -1 -1 z m 0 0" fill="#2e3436"/>
</g>
<g clip-path="url(#A)" mask="url(#z)" transform="matrix(1 0 0 1 -600 -80)">
<path d="m 226.453125 143.160156 c -0.519531 0 -0.953125 0.433594 -0.953125 0.953125 v 0.09375 c 0 0.519531 0.433594 0.953125 0.953125 0.953125 h 0.09375 c 0.519531 0 0.953125 -0.433594 0.953125 -0.953125 v -0.09375 c 0 -0.519531 -0.433594 -0.953125 -0.953125 -0.953125 z m 0 0" fill="#2e3436"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.3 KiB

2
data/resources/resources.gresource.xml

@ -17,6 +17,7 @@
<file compressed="true" preprocess="xml-stripblanks" alias="sidebar.ui">ui/sidebar.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="sidebar-item.ui">ui/sidebar-item.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="sidebar-category-row.ui">ui/sidebar-category-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="sidebar-entry-row.ui">ui/sidebar-entry-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="sidebar-room-row.ui">ui/sidebar-room-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="window.ui">ui/window.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="context-menu-bin.ui">ui/context-menu-bin.ui</file>
@ -27,6 +28,7 @@
<file compressed="true">style.css</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/send-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/status/welcome.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/status/explore-symbolic.svg</file>
</gresource>
</gresources>

5
data/resources/style.css

@ -46,6 +46,11 @@ headerbar.flat {
font-weight: bold;
}
.sidebar .entry {
margin-top: 4px;
font-weight: bold;
}
.sidebar .category image.arrow {
transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

2
data/resources/ui/session.ui

@ -51,12 +51,14 @@
<property name="compact" bind-source="content" bind-property="folded" bind-flags="sync-create"/>
<property name="room-list" bind-source="Session" bind-property="room-list" bind-flags="sync-create"/>
<property name="selected-room" bind-source="Session" bind-property="selected-room" bind-flags="sync-create | bidirectional"/>
<property name="selected-type" bind-source="Session" bind-property="selected-content-type" bind-flags="sync-create | bidirectional"/>
</object>
</child>
<child>
<object class="Content">
<property name="compact" bind-source="content" bind-property="folded" bind-flags="sync-create"/>
<property name="room" bind-source="Session" bind-property="selected-room" bind-flags="sync-create | bidirectional"/>
<property name="content-type" bind-source="Session" bind-property="selected-content-type" bind-flags="sync-create | bidirectional"/>
<property name="error-list">error_list</property>
</object>
</child>

36
data/resources/ui/sidebar-entry-row.ui

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="SidebarEntryRow" parent="AdwBin">
<style>
<class name="entry-row"/>
</style>
<child>
<object class="GtkBox">
<property name="spacing">12</property>
<child>
<object class="GtkImage">
<property name="icon-name">explore2-symbolic</property>
<binding name="icon-name">
<lookup name="icon-name" type="Entry">
<lookup name="entry">SidebarEntryRow</lookup>
</lookup>
</binding>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="ellipsize">end</property>
<binding name="label">
<lookup name="display-name" type="Entry">
<lookup name="entry">SidebarEntryRow</lookup>
</lookup>
</binding>
</object>
</child>
</object>
</child>
</template>
</interface>

2
po/POTFILES.in

@ -22,6 +22,7 @@ data/resources/ui/in-app-notification.ui
data/resources/ui/session.ui
data/resources/ui/shortcuts.ui
data/resources/ui/sidebar-category-row.ui
data/resources/ui/sidebar-entry-row.ui
data/resources/ui/sidebar-item.ui
data/resources/ui/sidebar-room-row.ui
data/resources/ui/sidebar.ui
@ -64,6 +65,7 @@ src/session/room/mod.rs
src/session/room/room.rs
src/session/room/timeline.rs
src/session/sidebar/category_row.rs
src/session/sidebar/entry.rs
src/session/sidebar/mod.rs
src/session/sidebar/room_row.rs
src/session/sidebar/row.rs

61
src/session/content/content.rs

@ -1,4 +1,5 @@
use crate::session::{
content::ContentType,
content::Invite,
content::RoomHistory,
room::{Room, RoomType},
@ -16,6 +17,7 @@ mod imp {
pub struct Content {
pub compact: Cell<bool>,
pub room: RefCell<Option<Room>>,
pub content_type: Cell<ContentType>,
pub error_list: RefCell<Option<gio::ListStore>>,
pub category_handler: RefCell<Option<SignalHandlerId>>,
#[template_child]
@ -39,7 +41,7 @@ mod imp {
klass.set_accessible_role(gtk::AccessibleRole::Group);
klass.install_action("content.go-back", None, move |widget, _, _| {
widget.set_room(None);
widget.set_content_type(ContentType::None);
});
}
@ -74,6 +76,14 @@ mod imp {
gio::ListStore::static_type(),
glib::ParamFlags::READWRITE,
),
glib::ParamSpec::new_enum(
"content-type",
"Content Type",
"The type of content currently displayed",
ContentType::static_type(),
ContentType::default() as i32,
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
]
});
@ -99,6 +109,7 @@ mod imp {
"error-list" => {
self.error_list.replace(value.get().unwrap());
}
"content-type" => obj.set_content_type(value.get().unwrap()),
_ => unimplemented!(),
}
}
@ -108,6 +119,7 @@ mod imp {
"compact" => self.compact.get().to_value(),
"room" => obj.room().to_value(),
"error-list" => self.error_list.borrow().to_value(),
"content-type" => obj.content_type().to_value(),
_ => unimplemented!(),
}
}
@ -127,6 +139,24 @@ impl Content {
glib::Object::new(&[]).expect("Failed to create Content")
}
pub fn content_type(&self) -> ContentType {
let priv_ = imp::Content::from_instance(self);
priv_.content_type.get()
}
pub fn set_content_type(&self, content_type: ContentType) {
let priv_ = imp::Content::from_instance(self);
if self.content_type() == content_type {
return;
}
priv_.content_type.set(content_type);
self.set_visible_child();
self.notify("content-type");
}
pub fn set_room(&self, room: Option<Room>) {
let priv_ = imp::Content::from_instance(self);
@ -143,17 +173,16 @@ impl Content {
if let Some(ref room) = room {
let handler_id = room.connect_notify_local(
Some("category"),
clone!(@weak self as obj => move |room, _| {
obj.set_visible_child(room);
clone!(@weak self as obj => move |_, _| {
obj.set_visible_child();
}),
);
self.set_visible_child(&room);
priv_.category_handler.replace(Some(handler_id));
}
priv_.room.replace(room);
self.set_visible_child();
self.notify("room");
}
@ -162,13 +191,25 @@ impl Content {
priv_.room.borrow().clone()
}
fn set_visible_child(&self, room: &Room) {
fn set_visible_child(&self) {
let priv_ = imp::Content::from_instance(self);
if room.category() == RoomType::Invited {
priv_.stack.set_visible_child(&*priv_.invite);
} else {
priv_.stack.set_visible_child(&*priv_.room_history);
match self.content_type() {
ContentType::None => {
//TODO: display an empty state
}
ContentType::Room => {
if let Some(room) = &*priv_.room.borrow() {
if room.category() == RoomType::Invited {
priv_.stack.set_visible_child(&*priv_.invite);
} else {
priv_.stack.set_visible_child(&*priv_.room_history);
}
}
}
ContentType::Explore => {
todo!("Display explore");
}
}
}
}

27
src/session/content/content_type.rs

@ -0,0 +1,27 @@
use gettextrs::gettext;
use gtk::glib;
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::GEnum)]
#[repr(u32)]
#[genum(type_name = "ContentType")]
pub enum ContentType {
None = 0,
Explore = 1,
Room = 2,
}
impl Default for ContentType {
fn default() -> Self {
ContentType::None
}
}
impl ToString for ContentType {
fn to_string(&self) -> String {
match self {
ContentType::None => gettext("No selection"),
ContentType::Explore => gettext("Explore"),
ContentType::Room => gettext("Room"),
}
}
}

2
src/session/content/mod.rs

@ -1,4 +1,5 @@
mod content;
mod content_type;
mod divider_row;
mod invite;
mod item_row;
@ -8,6 +9,7 @@ mod room_history;
mod state_row;
pub use self::content::Content;
pub use self::content_type::ContentType;
use self::divider_row::DividerRow;
use self::invite::Invite;
use self::item_row::ItemRow;

43
src/session/mod.rs

@ -20,6 +20,7 @@ use crate::utils::do_async;
use crate::Error;
use crate::RUNTIME;
use crate::session::content::ContentType;
use adw;
use adw::subclass::prelude::BinImpl;
use gtk::subclass::prelude::*;
@ -41,7 +42,7 @@ mod imp {
use super::*;
use glib::subclass::{InitializingObject, Signal};
use once_cell::sync::{Lazy, OnceCell};
use std::cell::RefCell;
use std::cell::{Cell, RefCell};
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/FractalNext/session.ui")]
@ -58,6 +59,7 @@ mod imp {
pub room_list: OnceCell<RoomList>,
pub user: OnceCell<User>,
pub selected_room: RefCell<Option<Room>>,
pub selected_content_type: Cell<ContentType>,
pub is_ready: OnceCell<bool>,
}
@ -98,6 +100,14 @@ mod imp {
Room::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
glib::ParamSpec::new_enum(
"selected-content-type",
"Selected Content Type",
"The current content type selected",
ContentType::static_type(),
ContentType::default() as i32,
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
glib::ParamSpec::new_object(
"user",
"User",
@ -123,6 +133,7 @@ mod imp {
let selected_room = value.get().unwrap();
obj.set_selected_room(selected_room);
}
"selected-content-type" => obj.set_selected_content_type(value.get().unwrap()),
_ => unimplemented!(),
}
}
@ -132,6 +143,7 @@ mod imp {
"room-list" => obj.room_list().to_value(),
"selected-room" => obj.selected_room().to_value(),
"user" => obj.user().to_value(),
"selected-content-type" => obj.selected_content_type().to_value(),
_ => unimplemented!(),
}
}
@ -157,6 +169,29 @@ impl Session {
glib::Object::new(&[]).expect("Failed to create Session")
}
pub fn selected_content_type(&self) -> ContentType {
let priv_ = imp::Session::from_instance(self);
priv_.selected_content_type.get()
}
pub fn set_selected_content_type(&self, selected_type: ContentType) {
let priv_ = imp::Session::from_instance(self);
if self.selected_content_type() == selected_type {
return;
}
if selected_type == ContentType::None {
priv_.content.navigate(adw::NavigationDirection::Back);
} else {
priv_.content.navigate(adw::NavigationDirection::Forward);
}
priv_.selected_content_type.set(selected_type);
self.notify("selected-content-type");
}
pub fn selected_room(&self) -> Option<Room> {
let priv_ = imp::Session::from_instance(self);
priv_.selected_room.borrow().clone()
@ -169,12 +204,6 @@ impl Session {
return;
}
if selected_room.is_some() {
priv_.content.navigate(adw::NavigationDirection::Forward);
} else {
priv_.content.navigate(adw::NavigationDirection::Back);
}
priv_.selected_room.replace(selected_room);
self.notify("selected-room");

109
src/session/sidebar/entry.rs

@ -0,0 +1,109 @@
use gtk::{glib, prelude::*, subclass::prelude::*};
use crate::session::content::ContentType;
mod imp {
use std::cell::{Cell, RefCell};
use super::*;
#[derive(Debug, Default)]
pub struct Entry {
pub type_: Cell<ContentType>,
pub display_name: RefCell<Option<String>>,
pub icon_name: RefCell<Option<String>>,
}
#[glib::object_subclass]
impl ObjectSubclass for Entry {
const NAME: &'static str = "Entry";
type Type = super::Entry;
type ParentType = glib::Object;
}
impl ObjectImpl for Entry {
fn properties() -> &'static [glib::ParamSpec] {
use once_cell::sync::Lazy;
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpec::new_enum(
"type",
"Type",
"The type of this category",
ContentType::static_type(),
ContentType::default() as i32,
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
),
glib::ParamSpec::new_string(
"display-name",
"Display Name",
"The display name of this Entry",
None,
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
glib::ParamSpec::new_string(
"icon-name",
"Icon Name",
"The icon name used for this Entry",
None,
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
]
});
PROPERTIES.as_ref()
}
fn set_property(
&self,
_obj: &Self::Type,
_id: usize,
value: &glib::Value,
pspec: &glib::ParamSpec,
) {
match pspec.name() {
"type" => {
self.type_.set(value.get().unwrap());
}
"display-name" => {
let _ = self.display_name.replace(value.get().unwrap());
}
"icon-name" => {
let _ = self.icon_name.replace(value.get().unwrap());
}
_ => unimplemented!(),
}
}
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"type" => obj.type_().to_value(),
"display-name" => obj.type_().to_string().to_value(),
"icon-name" => obj.icon_name().to_value(),
_ => unimplemented!(),
}
}
}
}
glib::wrapper! {
pub struct Entry(ObjectSubclass<imp::Entry>);
}
impl Entry {
pub fn new(type_: ContentType) -> Self {
glib::Object::new(&[("type", &type_)]).expect("Failed to create Entry")
}
pub fn type_(&self) -> ContentType {
let priv_ = imp::Entry::from_instance(self);
priv_.type_.get()
}
pub fn icon_name(&self) -> Option<&str> {
match self.type_() {
ContentType::Explore => Some("explore-symbolic"),
_ => None,
}
}
}

101
src/session/sidebar/entry_row.rs

@ -0,0 +1,101 @@
use adw;
use adw::subclass::prelude::BinImpl;
use gtk::subclass::prelude::*;
use gtk::{self, prelude::*};
use gtk::{glib, CompositeTemplate};
use crate::session::sidebar::Entry;
mod imp {
use super::*;
use glib::subclass::InitializingObject;
use std::cell::RefCell;
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/FractalNext/sidebar-entry-row.ui")]
pub struct EntryRow {
pub entry: RefCell<Option<Entry>>,
}
#[glib::object_subclass]
impl ObjectSubclass for EntryRow {
const NAME: &'static str = "SidebarEntryRow";
type Type = super::EntryRow;
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for EntryRow {
fn properties() -> &'static [glib::ParamSpec] {
use once_cell::sync::Lazy;
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpec::new_object(
"entry",
"Entry",
"The entry of this row",
Entry::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
)]
});
PROPERTIES.as_ref()
}
fn set_property(
&self,
obj: &Self::Type,
_id: usize,
value: &glib::Value,
pspec: &glib::ParamSpec,
) {
match pspec.name() {
"entry" => obj.set_entry(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"entry" => obj.entry().to_value(),
_ => unimplemented!(),
}
}
}
impl WidgetImpl for EntryRow {}
impl BinImpl for EntryRow {}
}
glib::wrapper! {
pub struct EntryRow(ObjectSubclass<imp::EntryRow>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
impl EntryRow {
pub fn new() -> Self {
glib::Object::new(&[]).expect("Failed to create EntryRow")
}
pub fn entry(&self) -> Option<Entry> {
let priv_ = imp::EntryRow::from_instance(&self);
priv_.entry.borrow().clone()
}
pub fn set_entry(&self, entry: Option<Entry>) {
let priv_ = imp::EntryRow::from_instance(&self);
if self.entry() == entry {
return;
}
priv_.entry.replace(entry);
self.notify("entry");
}
}

24
src/session/sidebar/item_list.rs

@ -1,6 +1,11 @@
use gtk::{gio, glib, prelude::*, subclass::prelude::*};
use crate::session::{room::RoomType, room_list::RoomList, sidebar::Category};
use crate::session::{
content::ContentType,
room::RoomType,
room_list::RoomList,
sidebar::{Category, Entry},
};
mod imp {
use once_cell::unsync::OnceCell;
@ -9,7 +14,7 @@ mod imp {
#[derive(Debug, Default)]
pub struct ItemList {
pub list: OnceCell<[Category; 5]>,
pub list: OnceCell<[glib::Object; 6]>,
}
#[glib::object_subclass]
@ -24,7 +29,7 @@ mod imp {
impl ListModelImpl for ItemList {
fn item_type(&self, _list_model: &Self::Type) -> glib::Type {
Category::static_type()
glib::Object::static_type()
}
fn n_items(&self, _list_model: &Self::Type) -> u32 {
self.list.get().map(|l| l.len()).unwrap_or(0) as u32
@ -61,14 +66,15 @@ impl ItemList {
priv_
.list
.set([
Category::new(RoomType::Invited, room_list),
Category::new(RoomType::Favorite, room_list),
Category::new(RoomType::Normal, room_list),
Category::new(RoomType::LowPriority, room_list),
Category::new(RoomType::Left, room_list),
Entry::new(ContentType::Explore).upcast::<glib::Object>(),
Category::new(RoomType::Invited, room_list).upcast::<glib::Object>(),
Category::new(RoomType::Favorite, room_list).upcast::<glib::Object>(),
Category::new(RoomType::Normal, room_list).upcast::<glib::Object>(),
Category::new(RoomType::LowPriority, room_list).upcast::<glib::Object>(),
Category::new(RoomType::Left, room_list).upcast::<glib::Object>(),
])
.unwrap();
self.items_changed(0, 0, 5);
self.items_changed(0, 0, 6);
}
}

4
src/session/sidebar/mod.rs

@ -1,5 +1,7 @@
mod category;
mod category_row;
mod entry;
mod entry_row;
mod item_list;
mod room_row;
mod row;
@ -8,6 +10,8 @@ mod sidebar;
pub use self::category::Category;
use self::category_row::CategoryRow;
pub use self::entry::Entry;
use self::entry_row::EntryRow;
pub use self::item_list::ItemList;
use self::room_row::RoomRow;
use self::row::Row;

17
src/session/sidebar/row.rs

@ -3,7 +3,7 @@ use gtk::{glib, prelude::*, subclass::prelude::*};
use crate::session::{
room::Room,
sidebar::{Category, CategoryRow, RoomRow},
sidebar::{Category, CategoryRow, Entry, EntryRow, RoomRow},
};
mod imp {
@ -151,6 +151,21 @@ impl Row {
if let Some(list_item) = self.parent() {
list_item.set_css_classes(&["room"]);
}
} else if let Some(entry) = item.downcast_ref::<Entry>() {
let child = if let Some(Ok(child)) = self.child().map(|w| w.downcast::<EntryRow>())
{
child
} else {
let child = EntryRow::new();
self.set_child(Some(&child));
child
};
child.set_entry(Some(entry.clone()));
if let Some(list_item) = self.parent() {
list_item.set_css_classes(&["entry"]);
}
} else {
panic!("Wrong row item: {:?}", item);
}

152
src/session/sidebar/selection.rs

@ -1,6 +1,6 @@
use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
use crate::session::room::Room;
use crate::session::{content::ContentType, room::Room, sidebar::Entry};
mod imp {
use super::*;
@ -11,7 +11,7 @@ mod imp {
pub struct Selection {
pub model: RefCell<Option<gio::ListModel>>,
pub selected: Cell<u32>,
pub selected_room: RefCell<Option<Room>>,
pub selected_item: RefCell<Option<glib::Object>>,
pub signal_handler: RefCell<Option<glib::SignalHandlerId>>,
}
@ -51,10 +51,18 @@ mod imp {
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
glib::ParamSpec::new_object(
"selected-room",
"Selected Room",
"The selected room",
Room::static_type(),
"selected-item",
"Selected Item",
"The selected item",
glib::Object::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
glib::ParamSpec::new_enum(
"selected-type",
"Selected Type",
"The currently selected content type",
ContentType::static_type(),
ContentType::default() as i32,
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
]
@ -79,10 +87,8 @@ mod imp {
let selected = value.get().unwrap();
obj.set_selected(selected);
}
"selected-room" => {
let selected_room = value.get().unwrap();
obj.set_selected_room(selected_room);
}
"selected-item" => obj.set_selected_item(value.get().unwrap()),
"selected-type" => obj.set_selected_type(value.get().unwrap()),
_ => unimplemented!(),
}
}
@ -91,7 +97,8 @@ mod imp {
match pspec.name() {
"model" => obj.model().to_value(),
"selected" => obj.selected().to_value(),
"selected-room" => obj.selected_room().to_value(),
"selected-item" => obj.selected_item().to_value(),
"selected-type" => obj.selected_type().to_value(),
_ => unimplemented!(),
}
}
@ -157,9 +164,77 @@ impl Selection {
priv_.selected.get()
}
pub fn selected_room(&self) -> Option<Room> {
pub fn selected_item(&self) -> Option<glib::Object> {
let priv_ = imp::Selection::from_instance(self);
priv_.selected_room.borrow().clone()
priv_.selected_item.borrow().clone()
}
pub fn selected_type(&self) -> ContentType {
if let Some(item) = self.selected_item() {
if item.is::<Room>() {
return ContentType::Room;
} else if let Ok(entry) = item.downcast::<Entry>() {
return entry.type_();
}
}
ContentType::None
}
pub fn set_selected_type(&self, selected_type: ContentType) {
let priv_ = imp::Selection::from_instance(self);
if self.selected_type() == selected_type {
return;
}
match selected_type {
ContentType::None => self.set_selected_item(None),
ContentType::Room => {
if self
.selected_item()
.and_then(|item| item.downcast::<Room>().ok())
.is_none()
{
if let Some(model) = &*priv_.model.borrow() {
for i in 0..model.n_items() {
if let Some(room) = model
.item(i)
.and_then(|item| item.downcast::<gtk::TreeListRow>().ok())
.and_then(|i| i.item())
.and_then(|o| o.downcast::<Room>().ok())
{
self.set_selected_item(Some(room.upcast()));
break;
}
}
}
}
}
ContentType::Explore => {
if !self
.selected_item()
.and_then(|item| item.downcast::<Entry>().ok())
.map_or(false, |entry| entry.type_() == selected_type)
{
if let Some(model) = &*priv_.model.borrow() {
for i in 0..model.n_items() {
if let Some(entry) = model
.item(i)
.and_then(|item| item.downcast::<gtk::TreeListRow>().ok())
.and_then(|i| i.item())
.and_then(|o| o.downcast::<Entry>().ok())
{
if entry.type_() == selected_type {
self.set_selected_item(Some(entry.upcast()));
break;
}
}
}
}
}
}
};
}
pub fn set_model<P: IsA<gio::ListModel>>(&self, model: Option<&P>) {
@ -202,9 +277,10 @@ impl Selection {
priv_.selected.replace(gtk::INVALID_LIST_POSITION);
self.notify("selected");
}
if self.selected_room().is_some() {
priv_.selected_room.replace(None);
self.notify("selected-room");
if self.selected_item().is_some() {
priv_.selected_item.replace(None);
self.notify("selected-type");
self.notify("selected-item");
}
self.items_changed(0, n_items_before, 0);
@ -221,13 +297,13 @@ impl Selection {
return;
}
let selected_room = self
let selected_item = self
.model()
.and_then(|m| m.item(position))
.and_then(|o| o.downcast::<gtk::TreeListRow>().ok())
.and_then(|r| r.item())
.and_then(|o| o.downcast::<Room>().ok());
let selected = if selected_room.is_none() {
.and_then(|r| r.item());
let selected = if selected_item.is_none() {
gtk::INVALID_LIST_POSITION
} else {
position
@ -238,7 +314,7 @@ impl Selection {
}
priv_.selected.replace(selected);
priv_.selected_room.replace(selected_room);
priv_.selected_item.replace(selected_item);
if old_selected == gtk::INVALID_LIST_POSITION {
self.selection_changed(selected, 1);
@ -251,14 +327,15 @@ impl Selection {
}
self.notify("selected");
self.notify("selected-room");
self.notify("selected-item");
self.notify("selected-type");
}
pub fn set_selected_room(&self, room: Option<Room>) {
fn set_selected_item(&self, item: Option<glib::Object>) {
let priv_ = imp::Selection::from_instance(self);
let selected_room = self.selected_room();
if selected_room == room {
let selected_item = self.selected_item();
if selected_item == item {
return;
}
@ -266,15 +343,14 @@ impl Selection {
let mut selected = gtk::INVALID_LIST_POSITION;
if room.is_some() {
if item.is_some() {
if let Some(model) = self.model() {
for i in 0..model.n_items() {
let r = model
let current_item = model
.item(i)
.and_then(|o| o.downcast::<gtk::TreeListRow>().ok())
.and_then(|r| r.item())
.and_then(|o| o.downcast::<Room>().ok());
if r == room {
.and_then(|r| r.item());
if current_item == item {
selected = i;
break;
}
@ -282,7 +358,7 @@ impl Selection {
}
}
priv_.selected_room.replace(room);
priv_.selected_item.replace(item);
if old_selected != selected {
priv_.selected.replace(selected);
@ -299,7 +375,8 @@ impl Selection {
self.notify("selected");
}
self.notify("selected-room");
self.notify("selected-item");
self.notify("selected-type");
}
fn items_changed_cb(&self, model: &gio::ListModel, position: u32, removed: u32, added: u32) {
@ -308,9 +385,9 @@ impl Selection {
let _guard = self.freeze_notify();
let selected = self.selected();
let selected_room = self.selected_room();
let selected_item = self.selected_item();
if selected_room.is_none() || selected < position {
if selected_item.is_none() || selected < position {
// unchanged
} else if selected != gtk::INVALID_LIST_POSITION && selected >= position + removed {
priv_.selected.replace(selected + added - removed);
@ -322,12 +399,11 @@ impl Selection {
priv_.selected.replace(gtk::INVALID_LIST_POSITION);
self.notify("selected");
} else {
let room = model
let item = model
.item(position + i)
.and_then(|o| o.downcast::<gtk::TreeListRow>().ok())
.and_then(|r| r.item())
.and_then(|o| o.downcast::<Room>().ok());
if room == selected_room {
.and_then(|r| r.item());
if item == selected_item {
// the item moved
if selected != position + i {
priv_.selected.replace(position + i);

44
src/session/sidebar/sidebar.rs

@ -2,8 +2,9 @@ use adw::subclass::prelude::BinImpl;
use gtk::{gio, glib, prelude::*, subclass::prelude::*, CompositeTemplate};
use crate::session::{
content::ContentType,
room::Room,
sidebar::{Category, ItemList, RoomRow, Row, Selection},
sidebar::{Category, Entry, ItemList, RoomRow, Row, Selection},
RoomList,
};
@ -18,6 +19,7 @@ mod imp {
pub struct Sidebar {
pub compact: Cell<bool>,
pub selected_room: RefCell<Option<Room>>,
pub selected_type: Cell<ContentType>,
#[template_child]
pub headerbar: TemplateChild<adw::HeaderBar>,
#[template_child]
@ -68,6 +70,14 @@ mod imp {
Room::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
glib::ParamSpec::new_enum(
"selected-type",
"Selected",
"The type of item that is selected",
ContentType::static_type(),
ContentType::default() as i32,
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
]
});
@ -94,6 +104,7 @@ mod imp {
let selected_room = value.get().unwrap();
obj.set_selected_room(selected_room);
}
"selected-type" => obj.set_selected_type(value.get().unwrap()),
_ => unimplemented!(),
}
}
@ -102,6 +113,7 @@ mod imp {
match pspec.name() {
"compact" => self.compact.get().to_value(),
"selected-room" => obj.selected_room().to_value(),
"selected-type" => obj.selected_type().to_value(),
_ => unimplemented!(),
}
}
@ -126,6 +138,12 @@ mod imp {
row.set_expanded(!row.is_expanded());
} else if row.item().and_then(|o| o.downcast::<Room>().ok()).is_some() {
model.set_selected(pos);
} else if row
.item()
.and_then(|o| o.downcast::<Entry>().ok())
.is_some()
{
model.set_selected(pos);
}
}
}
@ -147,6 +165,23 @@ impl Sidebar {
glib::Object::new(&[]).expect("Failed to create Sidebar")
}
pub fn selected_type(&self) -> ContentType {
let priv_ = imp::Sidebar::from_instance(self);
priv_.selected_type.get()
}
fn set_selected_type(&self, selected_type: ContentType) {
let priv_ = imp::Sidebar::from_instance(self);
if self.selected_type() == selected_type {
return;
}
priv_.selected_type.set(selected_type);
self.notify("selected-type");
}
pub fn selected_room(&self) -> Option<Room> {
let priv_ = imp::Sidebar::from_instance(self);
priv_.selected_room.borrow().clone()
@ -188,7 +223,11 @@ impl Sidebar {
.build();
let selection = Selection::new(Some(&filter_model));
self.bind_property("selected-room", &selection, "selected-room")
self.bind_property("selected-room", &selection, "selected-item")
.flags(glib::BindingFlags::SYNC_CREATE | glib::BindingFlags::BIDIRECTIONAL)
.build();
self.bind_property("selected-type", &selection, "selected-type")
.flags(glib::BindingFlags::SYNC_CREATE | glib::BindingFlags::BIDIRECTIONAL)
.build();
@ -206,7 +245,6 @@ impl Sidebar {
}
priv_.selected_room.replace(selected_room);
self.notify("selected-room");
}
}

Loading…
Cancel
Save