diff --git a/src/file_model.rs b/src/file_model.rs new file mode 100644 index 0000000..d1e94af --- /dev/null +++ b/src/file_model.rs @@ -0,0 +1,139 @@ +// file_model.rs +// +// Copyright 2022 Christopher Davis +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: GPL-3.0-or-later + +use crate::deps::*; +use crate::util; + +use gio::prelude::*; +use gio::subclass::prelude::*; + +use once_cell::sync::OnceCell; + +use std::cell::RefCell; + +mod imp { + use super::*; + + #[derive(Debug, Default)] + pub struct LpFileModel { + pub(super) inner: RefCell>, + pub(super) directory: OnceCell, + } + + #[glib::object_subclass] + impl ObjectSubclass for LpFileModel { + const NAME: &'static str = "LpFileModel"; + type Type = super::LpFileModel; + type Interfaces = (gio::ListModel,); + } + + impl ObjectImpl for LpFileModel {} + + impl ListModelImpl for LpFileModel { + fn item_type(&self, _list_model: &Self::Type) -> glib::Type { + gio::File::static_type() + } + + fn n_items(&self, _list_model: &Self::Type) -> u32 { + self.inner.borrow().len() as u32 + } + + fn item(&self, _list_model: &Self::Type, position: u32) -> Option { + self.inner + .borrow() + .get(position as usize) + .map(|f| f.clone().upcast()) + } + } +} + +glib::wrapper! { + pub struct LpFileModel(ObjectSubclass) @implements gio::ListModel; +} + +impl LpFileModel { + pub fn from_directory(directory: &gio::File) -> Self { + let model = glib::Object::new::(&[]).expect("Could not create LpFileModel"); + + { + // Here we use a nested scope so that the mutable borrow only lasts as long as we need it + let mut vec = model.imp().inner.borrow_mut(); + + let enumerator = directory + .enumerate_children( + &format!( + "{},{}", + *gio::FILE_ATTRIBUTE_STANDARD_NAME, + *gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE + ), + gio::FileQueryInfoFlags::NONE, + gio::Cancellable::NONE, + ) + .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()) { + if content_type.starts_with("image/") { + let name = info.name(); + log::debug!("{:?} is an image, adding to the list", name); + vec.push(directory.resolve_relative_path(&name)); + } + } + } + }); + + // Then sort by name. + vec.sort_by(|file_a, file_b| { + let name_a = util::get_file_display_name(file_a).unwrap_or_default(); + let name_b = util::get_file_display_name(file_b).unwrap_or_default(); + + util::utf8_collate_key_for_filename(&name_a) + .cmp(&util::utf8_collate_key_for_filename(&name_b)) + }); + + model.imp().directory.set(directory.clone()).unwrap(); + } + + model + } + + pub fn previous(&self, current: &gio::File) -> Option { + let imp = self.imp(); + + let vec = imp.inner.borrow(); + let index = vec.iter().position(|f| f.equal(current))?; + + vec.get(index.checked_sub(1)?).cloned() + } + + pub fn next(&self, current: &gio::File) -> Option { + let imp = self.imp(); + + let vec = imp.inner.borrow(); + let index = vec.iter().position(|f| f.equal(current))?; + + vec.get(index + 1).cloned() + } + + pub fn directory(&self) -> Option { + self.imp().directory.get().cloned() + } +} diff --git a/src/main.rs b/src/main.rs index a9c56b8..99f3bdd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,7 @@ use gtk::gio::{self, prelude::*}; mod application; mod config; +mod file_model; mod thumbnail; mod util; mod widgets; diff --git a/src/meson.build b/src/meson.build index a6eb1fb..4942195 100644 --- a/src/meson.build +++ b/src/meson.build @@ -18,6 +18,7 @@ rust_sources = files( 'widgets/mod.rs', 'application.rs', 'config.rs', + 'file_model.rs', 'main.rs', 'util.rs', 'thumbnail.rs', diff --git a/src/widgets/image_view.rs b/src/widgets/image_view.rs index 4ba4cd3..bfdef0f 100644 --- a/src/widgets/image_view.rs +++ b/src/widgets/image_view.rs @@ -29,8 +29,9 @@ use anyhow::Context; use ashpd::desktop::wallpaper; use ashpd::WindowIdentifier; use once_cell::sync::Lazy; -use std::cell::{Cell, RefCell}; +use std::cell::RefCell; +use crate::file_model::LpFileModel; use crate::thumbnail::Thumbnail; use crate::util; use crate::widgets::LpImage; @@ -56,13 +57,9 @@ mod imp { pub menu_model: RefCell>, pub popover_menu_model: RefCell>, - pub directory: RefCell>, - pub filename: RefCell>, + pub model: RefCell>, + pub current_file: RefCell>, pub uri: RefCell>, - // Path of filenames - pub directory_pictures: RefCell>, - // Cell allows for interior mutability of primitive types - pub index: Cell, } #[glib::object_subclass] @@ -112,9 +109,9 @@ mod imp { PROPERTIES.as_ref() } - fn property(&self, _obj: &Self::Type, _id: usize, psec: &glib::ParamSpec) -> glib::Value { + fn property(&self, obj: &Self::Type, _id: usize, psec: &glib::ParamSpec) -> glib::Value { match psec.name() { - "filename" => self.filename.borrow().to_value(), + "filename" => obj.filename().to_value(), "controls" => self.controls.to_value(), _ => unimplemented!(), } @@ -181,11 +178,10 @@ impl LpImageView { } } + imp.current_file.replace(Some(file.clone())); self.set_parent_from_file(file); - imp.picture.set_file(file); - *imp.uri.borrow_mut() = Some(file.uri().to_string()); + self.update_image(file); self.notify("filename"); - self.update_action_state(); let width = imp.picture.image_width(); let height = imp.picture.image_height(); @@ -196,101 +192,71 @@ impl LpImageView { fn set_parent_from_file(&self, file: &gio::File) { let imp = self.imp(); + let mut model = imp.model.borrow_mut(); - 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::Cancellable::NONE, - ) - .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); + if let Some(ref parent) = file.parent() { + if let Some(ref m) = *model { + if m.directory().map_or(false, |f| !f.equal(parent)) { + *model = Some(LpFileModel::from_directory(parent)); + log::debug!("new model created"); + } + } else { + *model = Some(LpFileModel::from_directory(parent)); + log::debug!("new model created"); } - - *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 update_image(&self) { + fn update_image(&self, file: &gio::File) { let imp = self.imp(); - 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(&file); - *imp.uri.borrow_mut() = Some(file.uri().to_string()); - *imp.filename.borrow_mut() = util::get_file_display_name(&file); - self.notify("filename"); - self.update_action_state(); + imp.picture.set_file(file); + imp.uri.replace(Some(file.uri().to_string())); + self.update_action_state(file); } pub fn next(&self) { - let imp = self.imp(); - // TODO: Replace with `Cell::update()` once stabilized - imp.index.set(imp.index.get() + 1); - self.update_image(); + { + let imp = self.imp(); + let b = imp.model.borrow(); + let model = b.as_ref().unwrap(); + let mut current_file = imp.current_file.borrow_mut(); + + if let Some(file) = current_file.as_mut() { + *file = model.next(file).unwrap(); + self.update_image(file); + } + } + + self.notify("filename"); } pub fn previous(&self) { - let imp = self.imp(); - // TODO: Replace with `Cell::update()` once stabilized - imp.index.set(imp.index.get() - 1); - self.update_image(); + { + let imp = self.imp(); + let b = imp.model.borrow(); + let model = b.as_ref().unwrap(); + let mut current_file = imp.current_file.borrow_mut(); + + if let Some(file) = current_file.as_mut() { + *file = model.previous(file).unwrap(); + self.update_image(file); + } + } + + self.notify("filename"); } - pub fn update_action_state(&self) { + pub fn update_action_state(&self, file: &gio::File) { let imp = self.imp(); - 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); + let b = imp.model.borrow(); + let model = b.as_ref().unwrap(); + + let next_enabled = model.next(file).is_some(); + let prev_enabled = model.previous(file).is_some(); + + self.action_set_enabled("iv.next", next_enabled); + self.action_set_enabled("iv.previous", prev_enabled); } pub fn set_wallpaper(&self) -> anyhow::Result<()> { @@ -330,11 +296,10 @@ impl LpImageView { let imp = self.imp(); let operation = gtk::PrintOperation::new(); - let path = &format!( - "{}/{}", - imp.directory.borrow().as_ref().unwrap(), - &imp.directory_pictures.borrow()[imp.index.get()] - ); + let path = (imp.current_file.borrow().as_ref()) + .context("No file")? + .peek_path() + .context("No path")?; let pb = gdk_pixbuf::Pixbuf::from_file(path)?; let setup = gtk::PageSetup::default(); @@ -387,7 +352,9 @@ impl LpImageView { pub fn filename(&self) -> Option { let imp = self.imp(); - imp.filename.borrow().to_owned() + let b = imp.current_file.borrow(); + let file = b.as_ref()?; + util::get_file_display_name(file) } pub fn show_popover_at(&self, x: f64, y: f64) {