diff --git a/Cargo.lock b/Cargo.lock index abbaeea716dbeb905e2b3bf2cf7cd44278f08cbf..e0cc8b619343bcfe5a851e2e2b34925891696220 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -863,6 +863,7 @@ version = "0.1.0" dependencies = [ "anyhow", "ashpd", + "futures-channel", "gettext-rs", "gtk-macros", "gtk4", diff --git a/Cargo.toml b/Cargo.toml index 0babf778c9265a23130f1bde3ca651681ad86cb2..f0a26a2d9bb6dcc5791c87129fc4f54fa0996c25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ gtk-macros = "0.3.0" log = "0.4.11" once_cell = "1.9.0" pretty_env_logger = "0.4.0" +futures-channel = "0.3.19" [dependencies.ashpd] version = "0.2.2" diff --git a/data/gtk/image_page.ui b/data/gtk/image_page.ui new file mode 100644 index 0000000000000000000000000000000000000000..815b2494860d6f37def7f31d9b8bbc2c5938e49a --- /dev/null +++ b/data/gtk/image_page.ui @@ -0,0 +1,76 @@ + + + + +
+ + _Open With… + win.open-with + + + + _Print… + win.print + + + _Copy + win.copy + + + + _Set as Wallpaper + win.set-wallpaper + +
+
+
diff --git a/data/gtk/image_view.ui b/data/gtk/image_view.ui index 7dcb506aed1c1352f51c3990d3c5df9766e71960..88af8a38f36d11f00ec08b1e64010e0e511d0237 100644 --- a/data/gtk/image_view.ui +++ b/data/gtk/image_view.ui @@ -8,61 +8,42 @@ True - - center + + True + False + False + + + + + + start center + 18 + 18 + go-previous-symbolic + iv.previous + Previous Image + - - True - - - start - center - 18 - 18 - go-previous-symbolic - iv.previous - Previous Image - - - - - - True - end - center - 18 - 18 - go-next-symbolic - iv.next - Next Image - - - - - - 3 - - - - - True - - - - - False - image_menu - - + + True + end + center + 18 + 18 + go-next-symbolic + iv.next + Next Image + @@ -87,32 +68,4 @@ - -
- - _Open With… - win.open-with - - - - _Print… - win.print - - - _Copy - win.copy - - - - _Set as Wallpaper - win.set-wallpaper - -
-
diff --git a/data/gtk/window.ui b/data/gtk/window.ui index 7feb7f5aa358376bf319bd38d03401bed4d46c45..bf803d3f7ad80499f634fd75c7507714bf12b4f7 100644 --- a/data/gtk/window.ui +++ b/data/gtk/window.ui @@ -16,11 +16,6 @@ - - - image_view - - open-menu-symbolic diff --git a/data/loupe.gresource.xml b/data/loupe.gresource.xml index f4ab741ae62141f7aa033e560b57e0b5330fde6c..e321c41b0568a265a8cbf05e10109d047b1c3b23 100644 --- a/data/loupe.gresource.xml +++ b/data/loupe.gresource.xml @@ -3,6 +3,7 @@ gtk/help_overlay.ui gtk/window.ui + gtk/image_page.ui gtk/image_view.ui style.css diff --git a/src/file_model.rs b/src/file_model.rs new file mode 100644 index 0000000000000000000000000000000000000000..aa739cdacce8e089ebbebbe345c76afb084471f1 --- /dev/null +++ b/src/file_model.rs @@ -0,0 +1,132 @@ +// 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 directory(&self) -> Option { + self.imp().directory.get().cloned() + } + + pub fn file(&self, index: u32) -> Option { + let vec = self.imp().inner.borrow(); + vec.get(index as usize).cloned() + } + + pub fn index_of(&self, file: &gio::File) -> Option { + let imp = self.imp(); + let vec = imp.inner.borrow(); + vec.iter().position(|f| f.equal(file)).map(|p| p as u32) + } +} diff --git a/src/main.rs b/src/main.rs index a9c56b86e8284253230aa04f6116ad77095f47b7..99f3bddb1feb7a91f5a282fa790d58225f609717 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 a6eb1fb44cdfa59f68572eae698ce81c865d017a..0299cdab0f005f8823c8a518844800292f78a409 100644 --- a/src/meson.build +++ b/src/meson.build @@ -13,11 +13,13 @@ run_command( ) rust_sources = files( - 'widgets/image_view.rs', 'widgets/image.rs', + 'widgets/image_page.rs', + 'widgets/image_view.rs', 'widgets/mod.rs', 'application.rs', 'config.rs', + 'file_model.rs', 'main.rs', 'util.rs', 'thumbnail.rs', diff --git a/src/widgets/image.rs b/src/widgets/image.rs index 8b9a520a4fc661df2e902b0b1c6d8c555176a962..b663cb25f99d0bd515414ce1f21a9b3042a514c0 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -82,6 +82,12 @@ mod imp { obj.set_hexpand(true); obj.set_vexpand(true); } + + fn dispose(&self, obj: &Self::Type) { + while let Some(child) = obj.first_child() { + child.unparent(); + } + } } impl WidgetImpl for LpImage { @@ -184,7 +190,6 @@ impl LpImage { self.queue_draw(); self.queue_resize(); - imp.image_height.get(); } pub fn image_width(&self) -> i32 { @@ -197,12 +202,21 @@ impl LpImage { imp.image_height.get() } + pub fn set_texture_with_file(&self, texture: gdk::Texture, source_file: &gio::File) { + let imp = self.imp(); + imp.texture.replace(Some(texture)); + imp.file.replace(Some(source_file.clone())); + + self.queue_draw(); + self.queue_resize(); + } + pub fn texture(&self) -> Option { let imp = self.imp(); imp.texture.borrow().clone() } - pub fn content_provider(&self) -> anyhow::Result { + pub fn content_provider(&self) -> gdk::ContentProvider { let imp = self.imp(); let mut contents = vec![]; @@ -217,6 +231,6 @@ impl LpImage { contents.push(content); } - Ok(gdk::ContentProvider::new_union(contents.as_slice())) + gdk::ContentProvider::new_union(contents.as_slice()) } } diff --git a/src/widgets/image_page.rs b/src/widgets/image_page.rs new file mode 100644 index 0000000000000000000000000000000000000000..ef04ba759933f1aac1b87b90d0656a588dd35837 --- /dev/null +++ b/src/widgets/image_page.rs @@ -0,0 +1,160 @@ +// image_page.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 adw::subclass::prelude::*; +use glib::clone; +use gtk::prelude::*; +use gtk::subclass::prelude::*; +use gtk::CompositeTemplate; + +use once_cell::sync::OnceCell; + +use crate::widgets::LpImage; + +mod imp { + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/org/gnome/Loupe/gtk/image_page.ui")] + pub struct LpImagePage { + #[template_child] + pub(super) stack: TemplateChild, + #[template_child] + pub(super) spinner: TemplateChild, + #[template_child] + pub(super) error_page: TemplateChild, + #[template_child] + pub(super) image: TemplateChild, + #[template_child] + pub(super) popover: TemplateChild, + #[template_child] + pub(super) click_gesture: TemplateChild, + #[template_child] + pub(super) press_gesture: TemplateChild, + + pub(super) file: OnceCell, + } + + #[glib::object_subclass] + impl ObjectSubclass for LpImagePage { + const NAME: &'static str = "LpImagePage"; + type Type = super::LpImagePage; + type ParentType = adw::Bin; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for LpImagePage { + fn constructed(&self, obj: &Self::Type) { + self.parent_constructed(obj); + + self.click_gesture + .connect_pressed(clone!(@weak obj => move |gesture, _, x, y| { + obj.show_popover_at(x, y); + gesture.set_state(gtk::EventSequenceState::Claimed); + })); + + self.press_gesture + .connect_pressed(clone!(@weak obj => move |gesture, x, y| { + log::debug!("Long press triggered"); + obj.show_popover_at(x, y); + gesture.set_state(gtk::EventSequenceState::Claimed); + })); + } + } + + impl WidgetImpl for LpImagePage {} + impl BinImpl for LpImagePage {} +} + +glib::wrapper! { + pub struct LpImagePage(ObjectSubclass) + @extends gtk::Widget, adw::Bin, + @implements gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable; +} + +impl LpImagePage { + pub fn from_file(file: &gio::File) -> Self { + let obj = glib::Object::new::(&[]).unwrap(); + obj.imp().file.set(file.clone()).unwrap(); + + // This doesn't work properly for items not explicitly selected + // via the file chooser portal. I'm not sure how to make this work. + gtk::RecentManager::default().add_item(&file.uri()); + + let ctx = glib::MainContext::default(); + ctx.spawn_local(clone!(@weak obj, @weak file => async move { + let imp = obj.imp(); + match load_texture_from_file(&file).await { + Ok(texture) => { + imp.image.set_texture_with_file(texture, &file); + imp.stack.set_visible_child(&*imp.image); + imp.spinner.set_spinning(false); + }, + Err(e) => { + imp.stack.set_visible_child(&*imp.error_page); + imp.spinner.set_spinning(false); + log::error!("Could not load image: {e}"); + } + } + })); + + obj + } + + pub fn file(&self) -> Option { + self.imp().file.get().cloned() + } + + pub fn texture(&self) -> Option { + self.imp().image.texture() + } + + pub fn content_provider(&self) -> gdk::ContentProvider { + self.imp().image.content_provider() + } + + pub fn show_popover_at(&self, x: f64, y: f64) { + let imp = self.imp(); + + let rect = gdk::Rectangle::new(x as i32, y as i32, 0, 0); + + imp.popover.set_pointing_to(Some(&rect)); + imp.popover.popup(); + } +} + +async fn load_texture_from_file(file: &gio::File) -> Result { + let (sender, receiver) = futures_channel::oneshot::channel(); + + std::thread::spawn(clone!(@weak file => move || { + let result = gdk::Texture::from_file(&file); + sender.send(result).unwrap() + })); + + receiver.await.unwrap() +} diff --git a/src/widgets/image_view.rs b/src/widgets/image_view.rs index 4ba4cd37607bf56ad405544e5dafa5ebb95b208e..cf377fa408cb5030b86c1b9f3ae9195a6446dd01 100644 --- a/src/widgets/image_view.rs +++ b/src/widgets/image_view.rs @@ -25,15 +25,20 @@ use gtk::prelude::*; use gtk::subclass::prelude::*; use gtk::CompositeTemplate; -use anyhow::Context; +use anyhow::{bail, Context}; use ashpd::desktop::wallpaper; use ashpd::WindowIdentifier; use once_cell::sync::Lazy; use std::cell::{Cell, RefCell}; +use crate::file_model::LpFileModel; use crate::thumbnail::Thumbnail; use crate::util; -use crate::widgets::LpImage; +use crate::widgets::LpImagePage; + +// The number of pages we want to buffer +// on either side of the current page. +const BUFFER: u32 = 2; mod imp { use super::*; @@ -42,27 +47,11 @@ mod imp { #[template(resource = "/org/gnome/Loupe/gtk/image_view.ui")] pub struct LpImageView { #[template_child] - pub picture: TemplateChild, - #[template_child] - pub controls: TemplateChild, - #[template_child] - pub popover: TemplateChild, - #[template_child] - pub click_gesture: TemplateChild, - #[template_child] - pub press_gesture: TemplateChild, + pub carousel: TemplateChild, - // RefCell allows for interior mutability of non-primitive types. - pub menu_model: RefCell>, - pub popover_menu_model: RefCell>, - - pub directory: RefCell>, + pub model: RefCell>, pub filename: RefCell>, - pub uri: RefCell>, - // Path of filenames - pub directory_pictures: RefCell>, - // Cell allows for interior mutability of primitive types - pub index: Cell, + pub prev_index: Cell, } #[glib::object_subclass] @@ -73,13 +62,14 @@ mod imp { fn class_init(klass: &mut Self::Class) { Self::bind_template(klass); + Self::Type::bind_template_callbacks(klass); klass.install_action("iv.next", None, move |image_view, _, _| { - image_view.next(); + image_view.navigate(adw::NavigationDirection::Forward); }); klass.install_action("iv.previous", None, move |image_view, _, _| { - image_view.previous(); + image_view.navigate(adw::NavigationDirection::Back); }); } @@ -91,31 +81,21 @@ mod imp { impl ObjectImpl for LpImageView { fn properties() -> &'static [glib::ParamSpec] { static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecString::new( - "filename", - "Filename", - "The filename of the current file", - None, - glib::ParamFlags::READABLE, - ), - glib::ParamSpecObject::new( - "controls", - "Controls", - "The controls for the image view", - gtk::Box::static_type(), - glib::ParamFlags::READABLE, - ), - ] + vec![glib::ParamSpecString::new( + "filename", + "Filename", + "The filename of the current file", + None, + glib::ParamFlags::READABLE, + )] }); 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(), - "controls" => self.controls.to_value(), + "filename" => obj.filename().to_value(), _ => unimplemented!(), } } @@ -123,33 +103,17 @@ mod imp { fn constructed(&self, obj: &Self::Type) { self.parent_constructed(obj); - self.click_gesture - .connect_pressed(clone!(@weak obj => move |_, _, x, y| { - obj.show_popover_at(x, y); - })); - - self.press_gesture - .connect_pressed(clone!(@weak obj => move |_, x, y| { - obj.show_popover_at(x, y); - })); - let source = gtk::DragSource::new(); + source.set_exclusive(true); source.connect_prepare( glib::clone!(@weak obj => @default-return None, move |_, _, _| { - match obj.imp().picture.content_provider() { - Ok(content) => Some(content), - Err(e) => { - log::error!("Could not get content provider: {:?}", e); - None - } - } + obj.current_page().map(|p| p.content_provider()) }), ); source.connect_drag_begin(glib::clone!(@weak obj => move |source, _| { - let imp = obj.imp(); - if let Some(texture) = imp.picture.texture() { + if let Some(texture) = obj.current_page().and_then(|p| p.texture()) { let thumbnail = Thumbnail::new(&texture); source.set_icon(Some(&thumbnail), 0, 0); }; @@ -169,128 +133,226 @@ glib::wrapper! { @implements gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable; } +#[gtk::template_callbacks] impl LpImageView { - pub fn set_image_from_file(&self, file: &gio::File) -> anyhow::Result<(i32, i32)> { - let imp = self.imp(); - - if let Some(current_file) = imp.picture.file() { - if current_file.path().as_deref() == file.path().as_deref() { - return Err(anyhow::Error::msg( - "Image is the same as the previous image; Doing nothing.", - )); + pub fn set_image_from_file(&self, file: &gio::File) -> anyhow::Result<()> { + if let Some(current_file) = self.current_page().and_then(|p| p.file()) { + if current_file.equal(file) { + bail!("Image is the same as the previous image; Doing nothing."); } } - self.set_parent_from_file(file); - imp.picture.set_file(file); - *imp.uri.borrow_mut() = Some(file.uri().to_string()); + self.build_model_from_file(file); self.notify("filename"); - self.update_action_state(); - let width = imp.picture.image_width(); - let height = imp.picture.image_height(); + // TODO: rework width stuff + // let width = imp.picture.image_width(); + // let height = imp.picture.image_height(); - log::debug!("Image dimensions: {} x {}", width, height); - Ok((width, height)) + // log::debug!("Image dimensions: {} x {}", width, height); + // Ok((width, height)) + Ok(()) } - fn set_parent_from_file(&self, file: &gio::File) { + // Builds an `LpFileModel`, which is an implementation of `gio::ListModel` + // that holds a `gio::File` for each child within the same directory as the + // file we pass. This model will update with changes to the directory, + // and in turn we'll update our `adw::Carousel`. + fn build_model_from_file(&self, file: &gio::File) { let imp = self.imp(); - - 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); - } - } + let carousel = &imp.carousel; + + { + // Here we use a nested scope so that the mutable borrow only lasts as long as we need it + let mut model = imp.model.borrow_mut(); + + if let Some(ref parent) = file.parent() { + if let Some(ref m) = *model { + if m.directory().map_or(false, |f| !f.equal(parent)) { + // Clear the carousel before creating the new model + self.clear_carousel(false); + *model = Some(LpFileModel::from_directory(parent)); + log::debug!("new model created"); + } else { + log::debug!("Re-using old model and navigating to the current file"); + self.navigate_to_file(m, file); + return; } - }); + } else { + *model = Some(LpFileModel::from_directory(parent)); + log::debug!("new model created"); + } + } + } - // 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)) - }); + if let Some(model) = imp.model.borrow().as_ref() { + let index = model.index_of(file).unwrap(); + log::debug!("Currently at file {index} in the directory"); + imp.filename.replace(util::get_file_display_name(file)); + carousel.append(&LpImagePage::from_file(file)); + self.fill_carousel(model, index); + self.update_action_state(model, index); + } + } - log::debug!("Sorted files: {:?}", directory_vec); + pub fn navigate(&self, direction: adw::NavigationDirection) { + let carousel = &self.imp().carousel; + let pos = carousel.position().round() as u32; + match direction { + adw::NavigationDirection::Forward => { + if pos < carousel.n_pages() - 1 { + carousel.scroll_to(&carousel.nth_page(pos + 1), true); + } + } + adw::NavigationDirection::Back => { + if pos > 0 { + carousel.scroll_to(&carousel.nth_page(pos - 1), true) + } } + _ => unimplemented!("Navigation direction should only be back or forward."), + }; + } - *imp.filename.borrow_mut() = util::get_file_display_name(file); + fn navigate_to_file(&self, model: &LpFileModel, file: &gio::File) { + let imp = self.imp(); + let carousel = imp.carousel.get(); + let current_index = self + .current_page() + .and_then(|p| p.file()) + .and_then(|f| model.index_of(&f)) + .unwrap_or_default(); + let new_index = model.index_of(file).unwrap_or_default(); + + // Code style note: I generally don't do early returns like this + // in my rust code, but here we do it to avoid code duplication. + if new_index == current_index { + return; + } - imp.index.set( - directory_vec - .iter() - .position(|f| Some(f) == imp.filename.borrow().as_ref()) - .unwrap(), - ); + let guard = carousel.freeze_notify(); + let page = LpImagePage::from_file(file); - log::debug!("Current index is {}", imp.index.get()); + if new_index > current_index { + carousel.append(&page); + } else { + carousel.prepend(&page); } + + carousel.scroll_to(&page, true); + + // Clear everything on either side, then refill + self.clear_carousel(true); + self.fill_carousel(model, new_index); + self.update_action_state(model, new_index); + + drop(guard); } - fn update_image(&self) { + // Fills the carousel with items on either side of the given `index` of `model` + fn fill_carousel(&self, model: &LpFileModel, index: u32) { let imp = self.imp(); + let carousel = imp.carousel.get(); - let path = &format!( - "{}/{}", - imp.directory.borrow().as_ref().unwrap(), - &imp.directory_pictures.borrow()[imp.index.get()] - ); + for i in 1..=BUFFER { + if let Some(ref file) = model.file(index + i) { + carousel.append(&LpImagePage::from_file(file)) + } + } - 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(); + for i in 1..=BUFFER { + if let Some(ref file) = index.checked_sub(i).and_then(|i| model.file(i)) { + carousel.prepend(&LpImagePage::from_file(file)) + } + } + + imp.prev_index.set(index); } - 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(); + // Clear the carousel, optionally preserving the current position + // as a point to refill from + fn clear_carousel(&self, preserve_current_page: bool) { + let carousel = self.imp().carousel.get(); + + if preserve_current_page { + // Remove everything before the current page + for _ in 0..(carousel.position() as u32) { + carousel.remove(&carousel.nth_page(0)); + } + + // Then everything after + while carousel.n_pages() > 1 { + carousel.remove(&carousel.nth_page(carousel.n_pages() - 1)); + } + } else { + while carousel.n_pages() > 0 { + carousel.remove(&carousel.nth_page(0)); + } + } } - 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(); + pub fn update_action_state(&self, model: &LpFileModel, index: u32) { + let next_enabled = model.item(index + 1).is_some(); + let prev_enabled = index.checked_sub(1).and_then(|i| model.item(i)).is_some(); + + self.action_set_enabled("iv.next", next_enabled); + self.action_set_enabled("iv.previous", prev_enabled); } - pub fn update_action_state(&self) { + #[template_callback] + fn page_changed_cb(&self, _index: u32, carousel: &adw::Carousel) { 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 current = self.current_page().and_then(|p| p.file()).unwrap(); + + let model_index = model.index_of(¤t).unwrap(); + let prev_index = imp.prev_index.get(); + + if model_index != prev_index { + imp.filename.replace(util::get_file_display_name(¤t)); + self.notify("filename"); + self.update_action_state(model, model_index); + + // We've moved forward + if let Some(diff) = model_index.checked_sub(prev_index) { + for i in 0..diff { + if prev_index + .checked_sub(BUFFER) + .and_then(|r| r.checked_sub(i)) + .is_some() + { + carousel.remove(&carousel.nth_page(0)); + } + + let s = prev_index + BUFFER + i + 1; + if s <= model.n_items() { + if let Some(ref file) = model.file(s) { + carousel.append(&LpImagePage::from_file(file)); + } + } + } + } + + // We've moved backward + if let Some(diff) = prev_index.checked_sub(model_index) { + for i in 0..diff { + let s = prev_index + BUFFER + i + 1; + if s <= model.n_items() { + carousel.remove(&carousel.nth_page(carousel.n_pages() - 1)); + } + + if let Some(ref file) = prev_index + .checked_sub(BUFFER) + .and_then(|d| d.checked_sub(i + 1)) + .and_then(|d| model.file(d)) + { + carousel.prepend(&LpImagePage::from_file(file)); + } + } + } + + imp.prev_index.set(model_index); + } } pub fn set_wallpaper(&self) -> anyhow::Result<()> { @@ -327,14 +389,13 @@ impl LpImageView { } pub fn print(&self) -> anyhow::Result<()> { - 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 = self + .current_page() + .and_then(|p| p.file()) + .context("No file")? + .peek_path() + .context("No path")?; let pb = gdk_pixbuf::Pixbuf::from_file(path)?; let setup = gtk::PageSetup::default(); @@ -369,9 +430,8 @@ impl LpImageView { pub fn copy(&self) -> anyhow::Result<()> { let clipboard = self.clipboard(); - let imp = self.imp(); - if let Some(texture) = imp.picture.texture() { + if let Some(texture) = self.current_page().context("No current page")?.texture() { clipboard.set_texture(&texture); } else { anyhow::bail!("No Image displayed."); @@ -380,22 +440,23 @@ impl LpImageView { Ok(()) } - pub fn uri(&self) -> Option { - let imp = self.imp(); - imp.uri.borrow().to_owned() + pub fn current_page(&self) -> Option { + let carousel = &self.imp().carousel; + let pos = carousel.position().round() as u32; + if carousel.n_pages() > 0 { + carousel.nth_page(pos).downcast().ok() + } else { + None + } } - pub fn filename(&self) -> Option { - let imp = self.imp(); - imp.filename.borrow().to_owned() + pub fn uri(&self) -> Option { + let page = self.current_page().expect("No page"); + let file = page.file().expect("No file"); + Some(file.uri().to_string()) } - pub fn show_popover_at(&self, x: f64, y: f64) { - let imp = self.imp(); - - let rect = gdk::Rectangle::new(x as i32, y as i32, 0, 0); - - imp.popover.set_pointing_to(Some(&rect)); - imp.popover.popup(); + pub fn filename(&self) -> Option { + self.imp().filename.borrow().clone() } } diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index a20abfe50b27059d0a182cc5ae74496dec03ef0b..076921a916930c12a60b5b48f8e059706fae31de 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -18,7 +18,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later mod image; +mod image_page; mod image_view; pub use image::LpImage; +pub use image_page::LpImagePage; pub use image_view::LpImageView; diff --git a/src/window.rs b/src/window.rs index b5b7c88f83493fef423daa7c9781c50708768700..762e99fd6bf31406ef4de18fa32a82538870ad87 100644 --- a/src/window.rs +++ b/src/window.rs @@ -274,10 +274,10 @@ impl LpWindow { if let Some(file) = imp .image_view - .uri() - .map(|u| u.trim_start_matches("file://").to_string()) - .map(|u| std::fs::File::open(u).ok()) - .flatten() + .current_page() + .and_then(|p| p.file()) + .and_then(|f| f.peek_path()) + .and_then(|p| std::fs::File::open(p).ok()) { ctx.spawn_local(clone!(@weak self as win => async move { if let Err(e) = @@ -287,7 +287,7 @@ impl LpWindow { } })); } else { - log::error!("No URI for current image.") + log::error!("Could not load a path for the current image.") } } @@ -324,15 +324,15 @@ impl LpWindow { imp.toast_overlay.add_toast(&toast); } - pub fn set_image_from_file(&self, file: &gio::File, resize: bool) { + pub fn set_image_from_file(&self, file: &gio::File, _resize: bool) { let imp = self.imp(); log::debug!("Loading file: {}", file.uri().to_string()); match imp.image_view.set_image_from_file(file) { - Ok((width, height)) => { - if resize { - self.resize_from_dimensions(width, height); - } + Ok((/*width, height*/)) => { + // if resize { + // self.resize_from_dimensions(width, height); + // } imp.stack.set_visible_child(&*imp.image_view); imp.image_view.grab_focus();