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>
<template class="IvImageView" parent="AdwBin">
<property name="css-name">imageview</property>
<property name="focusable">True</property>
<property name="child">
<object class="GtkOverlay">
<property name="hexpand">True</property>
@ -90,6 +91,22 @@
</child>
</object>
</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>
<class name="image-view"/>
</style>

View file

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

View file

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

View file

@ -18,6 +18,7 @@ rust_sources = files(
'application.rs',
'config.rs',
'main.rs',
'util.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 std::cell::{Cell, RefCell};
use crate::util;
mod imp {
use super::*;
use glib::subclass;
#[derive(Debug, CompositeTemplate)]
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/ImageViewer/gtk/image_view.ui")]
pub struct IvImageView {
#[template_child]
@ -52,12 +54,17 @@ mod imp {
#[template_child]
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>,
// RefCell<T> does the same for non-primitive types.
pub 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]
@ -66,26 +73,16 @@ mod imp {
type Type = super::IvImageView;
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) {
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>) {
@ -118,6 +115,13 @@ mod imp {
gio::MenuModel::static_type(),
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(),
"primary-menu-model" => self.menu_model.borrow().to_value(),
"popover-menu-model" => self.popover_menu_model.borrow().to_value(),
"filename" => self.filename.borrow().to_value(),
_ => unimplemented!(),
}
}
@ -213,13 +218,83 @@ impl IvImageView {
pub fn set_image_from_file(&self, file: &gio::File) {
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));
self.notify("filename");
self.update_action_state();
if let Err(e) = self.load_dimensions_from_file(file) {
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<()> {
let pb = file.path().context("No path for current file")?;
let (_, width, height) =
@ -231,6 +306,42 @@ impl IvImageView {
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<()> {
let wallpaper = self.uri().context("No URI for current file")?;
let ctx = glib::MainContext::default();
@ -283,6 +394,11 @@ impl IvImageView {
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) {
let imp = imp::IvImageView::from_instance(&self);

View file

@ -112,6 +112,16 @@ mod imp {
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.stack.set_visible_child(&*imp.image_view);
if let Some(name) = file.basename() {
self.set_title(Some(
name.file_name().expect("Missing name").to_str().unwrap(),
));
}
imp.image_view.grab_focus();
self.set_actions_enabled(true);
}