image_view: Add directory navigation

This commit adds directory navigation to Image Viewer.
Currently it only sorts images by name, instead of following
directory sorting like the mockups.
This commit is contained in:
Christopher Davis 2021-09-05 05:51:34 -07:00
parent 1841789525
commit 84208d2887
7 changed files with 191 additions and 28 deletions

View file

@ -2,6 +2,7 @@
<interface> <interface>
<template class="IvImageView" parent="AdwBin"> <template class="IvImageView" parent="AdwBin">
<property name="css-name">imageview</property> <property name="css-name">imageview</property>
<property name="focusable">True</property>
<property name="child"> <property name="child">
<object class="GtkOverlay"> <object class="GtkOverlay">
<property name="hexpand">True</property> <property name="hexpand">True</property>
@ -90,6 +91,22 @@
</child> </child>
</object> </object>
</property> </property>
<child>
<object class="GtkShortcutController">
<child>
<object class="GtkShortcut">
<property name="trigger">Left</property>
<property name="action">action(iv.previous)</property>
</object>
</child>
<child>
<object class="GtkShortcut">
<property name="trigger">Right</property>
<property name="action">action(iv.next)</property>
</object>
</child>
</object>
</child>
<style> <style>
<class name="image-view"/> <class name="image-view"/>
</style> </style>

View file

@ -5,7 +5,7 @@
<property name="default-height">498</property> <property name="default-height">498</property>
<property name="width-request">360</property> <property name="width-request">360</property>
<property name="title">Image Viewer</property> <property name="title">Image Viewer</property>
<property name="child"> <property name="content">
<object class="GtkBox"> <object class="GtkBox">
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<child> <child>

View file

@ -22,6 +22,7 @@ use gio::prelude::*;
mod application; mod application;
mod config; mod config;
mod util;
mod widgets; mod widgets;
mod window; mod window;

View file

@ -18,6 +18,7 @@ rust_sources = files(
'application.rs', 'application.rs',
'config.rs', 'config.rs',
'main.rs', 'main.rs',
'util.rs',
'window.rs', 'window.rs',
) )

23
src/util.rs Normal file
View file

@ -0,0 +1,23 @@
use gio::prelude::*;
use glib::translate::*;
pub fn utf8_collate_key_for_filename(filename: &String) -> String {
unsafe {
from_glib_full(glib::ffi::g_utf8_collate_key_for_filename(
filename.to_glib_none().0,
filename.len() as isize,
))
}
}
pub fn get_file_display_name(file: &gio::File) -> Option<String> {
let info = file
.query_info(
*gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
gio::FileQueryInfoFlags::empty(),
gio::NONE_CANCELLABLE,
)
.ok()?;
Some(info.display_name().to_string())
}

View file

@ -32,11 +32,13 @@ use ashpd::WindowIdentifier;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::cell::{Cell, RefCell}; use std::cell::{Cell, RefCell};
use crate::util;
mod imp { mod imp {
use super::*; use super::*;
use glib::subclass; use glib::subclass;
#[derive(Debug, CompositeTemplate)] #[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/ImageViewer/gtk/image_view.ui")] #[template(resource = "/org/gnome/ImageViewer/gtk/image_view.ui")]
pub struct IvImageView { pub struct IvImageView {
#[template_child] #[template_child]
@ -52,12 +54,17 @@ mod imp {
#[template_child] #[template_child]
pub press_gesture: TemplateChild<gtk::GestureLongPress>, pub press_gesture: TemplateChild<gtk::GestureLongPress>,
pub connection: zbus::Connection, // Cell<T> allows for interior mutability of primitive types
// Cell<T> allows for interior mutability of primitive types.
pub header_visible: Cell<bool>, pub header_visible: Cell<bool>,
// RefCell<T> does the same for non-primitive types. // RefCell<T> does the same for non-primitive types.
pub menu_model: RefCell<Option<gio::MenuModel>>, pub menu_model: RefCell<Option<gio::MenuModel>>,
pub popover_menu_model: RefCell<Option<gio::MenuModel>>, pub popover_menu_model: RefCell<Option<gio::MenuModel>>,
pub directory: RefCell<Option<String>>,
pub filename: RefCell<Option<String>>,
// Path of filenames
pub directory_pictures: RefCell<Vec<String>>,
pub index: Cell<usize>,
} }
#[glib::object_subclass] #[glib::object_subclass]
@ -66,26 +73,16 @@ mod imp {
type Type = super::IvImageView; type Type = super::IvImageView;
type ParentType = libadwaita::Bin; type ParentType = libadwaita::Bin;
fn new() -> Self {
let connection =
zbus::Connection::new_session().expect("Could not create zbus session");
Self {
headerbar: TemplateChild::default(),
menu_button: TemplateChild::default(),
picture: TemplateChild::default(),
popover: TemplateChild::default(),
click_gesture: TemplateChild::default(),
press_gesture: TemplateChild::default(),
connection,
header_visible: Cell::new(false),
menu_model: RefCell::default(),
popover_menu_model: RefCell::default(),
}
}
fn class_init(klass: &mut Self::Class) { fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass); Self::bind_template(klass);
klass.install_action("iv.next", None, move |image_view, _, _| {
image_view.next();
});
klass.install_action("iv.previous", None, move |image_view, _, _| {
image_view.previous();
});
} }
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) { fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
@ -118,6 +115,13 @@ mod imp {
gio::MenuModel::static_type(), gio::MenuModel::static_type(),
glib::ParamFlags::READWRITE, glib::ParamFlags::READWRITE,
), ),
glib::ParamSpec::new_string(
"filename",
"Filename",
"The filename of the current file",
None,
glib::ParamFlags::READABLE,
),
] ]
}); });
@ -149,6 +153,7 @@ mod imp {
"header-visible" => self.header_visible.get().to_value(), "header-visible" => self.header_visible.get().to_value(),
"primary-menu-model" => self.menu_model.borrow().to_value(), "primary-menu-model" => self.menu_model.borrow().to_value(),
"popover-menu-model" => self.popover_menu_model.borrow().to_value(), "popover-menu-model" => self.popover_menu_model.borrow().to_value(),
"filename" => self.filename.borrow().to_value(),
_ => unimplemented!(), _ => unimplemented!(),
} }
} }
@ -213,13 +218,83 @@ impl IvImageView {
pub fn set_image_from_file(&self, file: &gio::File) { pub fn set_image_from_file(&self, file: &gio::File) {
let imp = imp::IvImageView::from_instance(&self); let imp = imp::IvImageView::from_instance(&self);
if let Some(current_file) = imp.picture.file() {
if current_file.path().as_deref() == file.path().as_deref() {
return;
}
}
self.set_parent_from_file(file);
imp.picture.set_file(Some(file)); imp.picture.set_file(Some(file));
self.notify("filename");
self.update_action_state();
if let Err(e) = self.load_dimensions_from_file(file) { if let Err(e) = self.load_dimensions_from_file(file) {
log::error!("Could not load image dimensions: {}", e); log::error!("Could not load image dimensions: {}", e);
}; };
} }
fn set_parent_from_file(&self, file: &gio::File) {
let imp = imp::IvImageView::from_instance(&self);
if let Some(parent) = file.parent() {
let parent_path = parent.path().map(|p| p.to_str().unwrap().to_string());
let mut directory_vec = imp.directory_pictures.borrow_mut();
if parent_path.as_deref() != imp.directory.borrow().as_deref() {
*imp.directory.borrow_mut() = parent_path;
directory_vec.clear();
let enumerator = parent
.enumerate_children(
&format!(
"{},{}",
*gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
*gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE
),
gio::FileQueryInfoFlags::NONE,
gio::NONE_CANCELLABLE,
)
.unwrap();
// Filter out non-images; For now we support "all" image types.
enumerator.for_each(|info| {
if let Ok(info) = info {
if let Some(content_type) = info.content_type().map(|t| t.to_string()) {
let filename = info.display_name().to_string();
log::debug!("Filename: {}", filename);
log::debug!("Mimetype: {}", content_type);
if content_type.starts_with("image/") {
log::debug!("{} is an image, adding to the list", filename);
directory_vec.push(filename);
}
}
}
});
// Then sort by name.
directory_vec.sort_by(|name_a, name_b| {
util::utf8_collate_key_for_filename(name_a)
.cmp(&util::utf8_collate_key_for_filename(name_b))
});
log::debug!("Sorted files: {:?}", directory_vec);
}
*imp.filename.borrow_mut() = util::get_file_display_name(file);
imp.index.set(
directory_vec
.iter()
.position(|f| Some(f) == imp.filename.borrow().as_ref())
.unwrap(),
);
log::debug!("Current index is {}", imp.index.get());
}
}
fn load_dimensions_from_file(&self, file: &gio::File) -> anyhow::Result<()> { fn load_dimensions_from_file(&self, file: &gio::File) -> anyhow::Result<()> {
let pb = file.path().context("No path for current file")?; let pb = file.path().context("No path for current file")?;
let (_, width, height) = let (_, width, height) =
@ -231,6 +306,42 @@ impl IvImageView {
Ok(()) Ok(())
} }
fn update_image(&self) {
let imp = imp::IvImageView::from_instance(&self);
let path = &format!(
"{}/{}",
imp.directory.borrow().as_ref().unwrap(),
&imp.directory_pictures.borrow()[imp.index.get()]
);
let file = gio::File::for_path(path);
imp.picture.set_file(Some(&file));
*imp.filename.borrow_mut() = util::get_file_display_name(&file);
self.notify("filename");
self.update_action_state();
}
pub fn next(&self) {
let imp = imp::IvImageView::from_instance(&self);
// TODO: Replace with `Cell::update()` once stabilized
imp.index.set(imp.index.get() + 1);
self.update_image();
}
pub fn previous(&self) {
let imp = imp::IvImageView::from_instance(&self);
// TODO: Replace with `Cell::update()` once stabilized
imp.index.set(imp.index.get() - 1);
self.update_image();
}
pub fn update_action_state(&self) {
let imp = imp::IvImageView::from_instance(&self);
let index = imp.index.get();
self.action_set_enabled("iv.next", index < imp.directory_pictures.borrow().len() - 1);
self.action_set_enabled("iv.previous", index > 0);
}
pub fn set_wallpaper(&self) -> anyhow::Result<()> { pub fn set_wallpaper(&self) -> anyhow::Result<()> {
let wallpaper = self.uri().context("No URI for current file")?; let wallpaper = self.uri().context("No URI for current file")?;
let ctx = glib::MainContext::default(); let ctx = glib::MainContext::default();
@ -283,6 +394,11 @@ impl IvImageView {
Some(file.uri().to_string()) Some(file.uri().to_string())
} }
pub fn filename(&self) -> Option<String> {
let imp = imp::IvImageView::from_instance(&self);
imp.filename.borrow().to_owned()
}
pub fn show_popover_at(&self, x: f64, y: f64) { pub fn show_popover_at(&self, x: f64, y: f64) {
let imp = imp::IvImageView::from_instance(&self); let imp = imp::IvImageView::from_instance(&self);

View file

@ -112,6 +112,16 @@ mod imp {
obj.resize_from_dimensions(width, height); obj.resize_from_dimensions(width, height);
}), }),
); );
// Property bindings didn't seem to work here
self.image_view.connect_notify_local(
Some("filename"),
clone!(@weak obj => move |iv, _| {
if let Some(filename) = iv.filename() {
obj.set_title(Some(&filename));
}
}),
);
} }
} }
@ -244,12 +254,7 @@ impl IvWindow {
imp.image_view.set_image_from_file(file); imp.image_view.set_image_from_file(file);
imp.stack.set_visible_child(&*imp.image_view); imp.stack.set_visible_child(&*imp.image_view);
imp.image_view.grab_focus();
if let Some(name) = file.basename() {
self.set_title(Some(
name.file_name().expect("Missing name").to_str().unwrap(),
));
}
self.set_actions_enabled(true); self.set_actions_enabled(true);
} }