general: Use a custom ListModel for file tracking
This is more robust than manually keeping a vector and using indexing.
This commit is contained in:
parent
ca3c02a5c2
commit
3bb48155a0
4 changed files with 205 additions and 97 deletions
139
src/file_model.rs
Normal file
139
src/file_model.rs
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
// file_model.rs
|
||||||
|
//
|
||||||
|
// Copyright 2022 Christopher Davis <christopherdavis@gnome.org>
|
||||||
|
//
|
||||||
|
// 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 <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
// 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<Vec<gio::File>>,
|
||||||
|
pub(super) directory: OnceCell<gio::File>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<glib::Object> {
|
||||||
|
self.inner
|
||||||
|
.borrow()
|
||||||
|
.get(position as usize)
|
||||||
|
.map(|f| f.clone().upcast())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct LpFileModel(ObjectSubclass<imp::LpFileModel>) @implements gio::ListModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LpFileModel {
|
||||||
|
pub fn from_directory(directory: &gio::File) -> Self {
|
||||||
|
let model = glib::Object::new::<Self>(&[]).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<gio::File> {
|
||||||
|
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<gio::File> {
|
||||||
|
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<gio::File> {
|
||||||
|
self.imp().directory.get().cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,7 @@ use gtk::gio::{self, prelude::*};
|
||||||
|
|
||||||
mod application;
|
mod application;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod file_model;
|
||||||
mod thumbnail;
|
mod thumbnail;
|
||||||
mod util;
|
mod util;
|
||||||
mod widgets;
|
mod widgets;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ rust_sources = files(
|
||||||
'widgets/mod.rs',
|
'widgets/mod.rs',
|
||||||
'application.rs',
|
'application.rs',
|
||||||
'config.rs',
|
'config.rs',
|
||||||
|
'file_model.rs',
|
||||||
'main.rs',
|
'main.rs',
|
||||||
'util.rs',
|
'util.rs',
|
||||||
'thumbnail.rs',
|
'thumbnail.rs',
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,9 @@ use anyhow::Context;
|
||||||
use ashpd::desktop::wallpaper;
|
use ashpd::desktop::wallpaper;
|
||||||
use ashpd::WindowIdentifier;
|
use ashpd::WindowIdentifier;
|
||||||
use once_cell::sync::Lazy;
|
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::thumbnail::Thumbnail;
|
||||||
use crate::util;
|
use crate::util;
|
||||||
use crate::widgets::LpImage;
|
use crate::widgets::LpImage;
|
||||||
|
|
@ -56,13 +57,9 @@ mod imp {
|
||||||
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 model: RefCell<Option<LpFileModel>>,
|
||||||
pub filename: RefCell<Option<String>>,
|
pub current_file: RefCell<Option<gio::File>>,
|
||||||
pub uri: RefCell<Option<String>>,
|
pub uri: RefCell<Option<String>>,
|
||||||
// Path of filenames
|
|
||||||
pub directory_pictures: RefCell<Vec<String>>,
|
|
||||||
// Cell<T> allows for interior mutability of primitive types
|
|
||||||
pub index: Cell<usize>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
|
|
@ -112,9 +109,9 @@ mod imp {
|
||||||
PROPERTIES.as_ref()
|
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() {
|
match psec.name() {
|
||||||
"filename" => self.filename.borrow().to_value(),
|
"filename" => obj.filename().to_value(),
|
||||||
"controls" => self.controls.to_value(),
|
"controls" => self.controls.to_value(),
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
}
|
}
|
||||||
|
|
@ -181,11 +178,10 @@ impl LpImageView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
imp.current_file.replace(Some(file.clone()));
|
||||||
self.set_parent_from_file(file);
|
self.set_parent_from_file(file);
|
||||||
imp.picture.set_file(file);
|
self.update_image(file);
|
||||||
*imp.uri.borrow_mut() = Some(file.uri().to_string());
|
|
||||||
self.notify("filename");
|
self.notify("filename");
|
||||||
self.update_action_state();
|
|
||||||
|
|
||||||
let width = imp.picture.image_width();
|
let width = imp.picture.image_width();
|
||||||
let height = imp.picture.image_height();
|
let height = imp.picture.image_height();
|
||||||
|
|
@ -196,101 +192,71 @@ impl LpImageView {
|
||||||
|
|
||||||
fn set_parent_from_file(&self, file: &gio::File) {
|
fn set_parent_from_file(&self, file: &gio::File) {
|
||||||
let imp = self.imp();
|
let imp = self.imp();
|
||||||
|
let mut model = imp.model.borrow_mut();
|
||||||
|
|
||||||
if let Some(parent) = file.parent() {
|
if let Some(ref parent) = file.parent() {
|
||||||
let parent_path = parent.path().map(|p| p.to_str().unwrap().to_string());
|
if let Some(ref m) = *model {
|
||||||
let mut directory_vec = imp.directory_pictures.borrow_mut();
|
if m.directory().map_or(false, |f| !f.equal(parent)) {
|
||||||
|
*model = Some(LpFileModel::from_directory(parent));
|
||||||
if parent_path.as_deref() != imp.directory.borrow().as_deref() {
|
log::debug!("new model created");
|
||||||
*imp.directory.borrow_mut() = parent_path;
|
}
|
||||||
directory_vec.clear();
|
} else {
|
||||||
|
*model = Some(LpFileModel::from_directory(parent));
|
||||||
let enumerator = parent
|
log::debug!("new model created");
|
||||||
.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.
|
fn update_image(&self, file: &gio::File) {
|
||||||
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 update_image(&self) {
|
|
||||||
let imp = self.imp();
|
let imp = self.imp();
|
||||||
|
|
||||||
let path = &format!(
|
imp.picture.set_file(file);
|
||||||
"{}/{}",
|
imp.uri.replace(Some(file.uri().to_string()));
|
||||||
imp.directory.borrow().as_ref().unwrap(),
|
self.update_action_state(file);
|
||||||
&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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next(&self) {
|
pub fn next(&self) {
|
||||||
|
{
|
||||||
let imp = self.imp();
|
let imp = self.imp();
|
||||||
// TODO: Replace with `Cell::update()` once stabilized
|
let b = imp.model.borrow();
|
||||||
imp.index.set(imp.index.get() + 1);
|
let model = b.as_ref().unwrap();
|
||||||
self.update_image();
|
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) {
|
pub fn previous(&self) {
|
||||||
|
{
|
||||||
let imp = self.imp();
|
let imp = self.imp();
|
||||||
// TODO: Replace with `Cell::update()` once stabilized
|
let b = imp.model.borrow();
|
||||||
imp.index.set(imp.index.get() - 1);
|
let model = b.as_ref().unwrap();
|
||||||
self.update_image();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_action_state(&self) {
|
self.notify("filename");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_action_state(&self, file: &gio::File) {
|
||||||
let imp = self.imp();
|
let imp = self.imp();
|
||||||
let index = imp.index.get();
|
let b = imp.model.borrow();
|
||||||
self.action_set_enabled("iv.next", index < imp.directory_pictures.borrow().len() - 1);
|
let model = b.as_ref().unwrap();
|
||||||
self.action_set_enabled("iv.previous", index > 0);
|
|
||||||
|
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<()> {
|
pub fn set_wallpaper(&self) -> anyhow::Result<()> {
|
||||||
|
|
@ -330,11 +296,10 @@ impl LpImageView {
|
||||||
let imp = self.imp();
|
let imp = self.imp();
|
||||||
|
|
||||||
let operation = gtk::PrintOperation::new();
|
let operation = gtk::PrintOperation::new();
|
||||||
let path = &format!(
|
let path = (imp.current_file.borrow().as_ref())
|
||||||
"{}/{}",
|
.context("No file")?
|
||||||
imp.directory.borrow().as_ref().unwrap(),
|
.peek_path()
|
||||||
&imp.directory_pictures.borrow()[imp.index.get()]
|
.context("No path")?;
|
||||||
);
|
|
||||||
let pb = gdk_pixbuf::Pixbuf::from_file(path)?;
|
let pb = gdk_pixbuf::Pixbuf::from_file(path)?;
|
||||||
|
|
||||||
let setup = gtk::PageSetup::default();
|
let setup = gtk::PageSetup::default();
|
||||||
|
|
@ -387,7 +352,9 @@ impl LpImageView {
|
||||||
|
|
||||||
pub fn filename(&self) -> Option<String> {
|
pub fn filename(&self) -> Option<String> {
|
||||||
let imp = self.imp();
|
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) {
|
pub fn show_popover_at(&self, x: f64, y: f64) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue