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:
parent
1841789525
commit
84208d2887
7 changed files with 191 additions and 28 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
23
src/util.rs
Normal 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())
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue