widgets: Rework image_view with AdwCarousel, add ImagePage

Previously we loaded images synchronously and manually
changed `LpImage`. This could cause freezes when loading
large images and SVGs. We also had no proper feedback for
when a corrupted or unsupported image was loaded.

This commit introduces a major rework to how we handle
images and the gallery. It introduces a swipeable gallery
that loads files asynchronously via a new widget: LpImagePage.

LpImagePage owns the file and information related to the file,
and on creation it spawns a future that either loads an error
page when it can't load something (file deleted, corrupted, or
unsupported) or the actual image via LpImage. LpImagePage
now holds the right-click popover and the associated gestures.

The gestures needed to be moved to LpImagePage in order to
rework LpImageView's overlay - instead of having an overlay
child that's a box with a few buttons, we now have the buttons
as individual overlay children. This could cause them
to overlap each other at certain sizes, but it's required
in order to allow input to reach the carousel below.

Right now this comes with a few regressions:

* Occasionally input gets blocked across the applications. It's
hard to tell if this is caused by swipes or right clicking. This
input issue can cause a crash - no Rust backtrace, so a debugger
is needed.

* The "current" page doesn't seem to be properly loaded by the
time `set_image_from_file ()` finishes, so we need to rework
resizing from image dimensions

* We need to re-implement loading different directories and
files within directories

There also seems to be an issue on touchscreens, swiping between
images on a touchscreen causes responsiveness issues not present
when swiping on a touchpad. My guess is that it's related to
the long press gesture.
This commit is contained in:
Christopher Davis 2022-01-19 23:11:38 -08:00
parent 3bb48155a0
commit 2aa480eee5
13 changed files with 459 additions and 227 deletions

1
Cargo.lock generated
View file

@ -863,6 +863,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"ashpd", "ashpd",
"futures-channel",
"gettext-rs", "gettext-rs",
"gtk-macros", "gtk-macros",
"gtk4", "gtk4",

View file

@ -9,6 +9,7 @@ gtk-macros = "0.3.0"
log = "0.4.11" log = "0.4.11"
once_cell = "1.9.0" once_cell = "1.9.0"
pretty_env_logger = "0.4.0" pretty_env_logger = "0.4.0"
futures-channel = "0.3.19"
[dependencies.ashpd] [dependencies.ashpd]
version = "0.2.2" version = "0.2.2"

76
data/gtk/image_page.ui Normal file
View file

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="LpImagePage" parent="AdwBin">
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="child">
<object class="GtkStack" id="stack">
<property name="transition-type">crossfade</property>
<child>
<object class="GtkSpinner" id="spinner">
<property name="halign">center</property>
<property name="valign">center</property>
<property name="spinning">True</property>
</object>
</child>
<child>
<object class="AdwStatusPage" id="error_page">
<property name="icon-name">image-missing-symbolic</property>
<property name="title" translatable="yes">Could not Load Image</property>
<property name="description" translatable="yes">This image could be corrupted or may use an unsupported file format.</property>
</object>
</child>
<child>
<object class="LpImage" id="image">
<property name="halign">center</property>
<property name="valign">center</property>
<child>
<object class="GtkGestureClick" id="click_gesture">
<property name="button">3</property>
</object>
</child>
<child>
<object class="GtkGestureLongPress" id="press_gesture">
<property name="touch-only">True</property>
</object>
</child>
<child>
<object class="GtkPopoverMenu" id="popover">
<property name="has-arrow">False</property>
<property name="menu-model">image_menu</property>
</object>
</child>
</object>
</child>
</object>
</property>
</template>
<menu id="image_menu">
<section>
<item>
<attribute name="label" translatable="yes">_Open With…</attribute>
<attribute name="action">win.open-with</attribute>
</item>
<!--<item>
<attribute name="label" translatable="yes">_Display On…</attribute>
<attribute name="action">win.display-on</attribute>
</item>-->
<item>
<attribute name="label" translatable="yes">_Print…</attribute>
<attribute name="action">win.print</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Copy</attribute>
<attribute name="action">win.copy</attribute>
</item>
<!--<item>
<attribute name="label" translatable="yes">_Rotate</attribute>
<attribute name="action">win.rotate-image</attribute>
</item>-->
<item>
<attribute name="label" translatable="yes">_Set as Wallpaper</attribute>
<attribute name="action">win.set-wallpaper</attribute>
</item>
</section>
</menu>
</interface>

View file

@ -8,61 +8,42 @@
<object class="GtkOverlay"> <object class="GtkOverlay">
<property name="hexpand">True</property> <property name="hexpand">True</property>
<child> <child>
<object class="LpImage" id="picture"> <object class="AdwCarousel" id="carousel">
<property name="halign">center</property> <property name="interactive">True</property>
<property name="valign">center</property> <property name="allow-mouse-drag">False</property>
<property name="allow-scroll-wheel">False</property>
<signal name="page-changed" handler="page_changed_cb" swapped="yes"/>
</object> </object>
</child> </child>
<child type="overlay"> <child type="overlay">
<object class="GtkBox" id="controls"> <object class="GtkButton" id="prev_button">
<property name="vexpand">True</property> <property name="halign">start</property>
<child> <property name="valign">center</property>
<object class="GtkButton" id="prev_button"> <property name="margin-start">18</property>
<property name="halign">start</property> <property name="margin-end">18</property>
<property name="valign">center</property> <property name="icon-name">go-previous-symbolic</property>
<property name="margin-start">18</property> <property name="action-name">iv.previous</property>
<property name="margin-end">18</property> <property name="tooltip-text" translatable="yes">Previous Image</property>
<property name="icon-name">go-previous-symbolic</property> <style>
<property name="action-name">iv.previous</property> <class name="osd"/>
<property name="tooltip-text" translatable="yes">Previous Image</property> <class name="circular"/>
<style> </style>
<class name="osd"/> </object>
<class name="circular"/> </child>
</style> <child type="overlay">
</object> <object class="GtkButton" id="next_button">
</child> <property name="hexpand">True</property>
<child> <property name="halign">end</property>
<object class="GtkButton" id="next_button"> <property name="valign">center</property>
<property name="hexpand">True</property> <property name="margin-start">18</property>
<property name="halign">end</property> <property name="margin-end">18</property>
<property name="valign">center</property> <property name="icon-name">go-next-symbolic</property>
<property name="margin-start">18</property> <property name="action-name">iv.next</property>
<property name="margin-end">18</property> <property name="tooltip-text" translatable="yes">Next Image</property>
<property name="icon-name">go-next-symbolic</property> <style>
<property name="action-name">iv.next</property> <class name="osd"/>
<property name="tooltip-text" translatable="yes">Next Image</property> <class name="circular"/>
<style> </style>
<class name="osd"/>
<class name="circular"/>
</style>
</object>
</child>
<child>
<object class="GtkGestureClick" id="click_gesture">
<property name="button">3</property>
</object>
</child>
<child>
<object class="GtkGestureLongPress" id="press_gesture">
<property name="touch-only">True</property>
</object>
</child>
<child>
<object class="GtkPopoverMenu" id="popover">
<property name="has-arrow">False</property>
<property name="menu-model">image_menu</property>
</object>
</child>
</object> </object>
</child> </child>
</object> </object>
@ -87,32 +68,4 @@
<class name="image-view"/> <class name="image-view"/>
</style> </style>
</template> </template>
<menu id="image_menu">
<section>
<item>
<attribute name="label" translatable="yes">_Open With…</attribute>
<attribute name="action">win.open-with</attribute>
</item>
<!--<item>
<attribute name="label" translatable="yes">_Display On…</attribute>
<attribute name="action">win.display-on</attribute>
</item>-->
<item>
<attribute name="label" translatable="yes">_Print…</attribute>
<attribute name="action">win.print</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Copy</attribute>
<attribute name="action">win.copy</attribute>
</item>
<!--<item>
<attribute name="label" translatable="yes">_Rotate</attribute>
<attribute name="action">win.rotate-image</attribute>
</item>-->
<item>
<attribute name="label" translatable="yes">_Set as Wallpaper</attribute>
<attribute name="action">win.set-wallpaper</attribute>
</item>
</section>
</menu>
</interface> </interface>

View file

@ -16,11 +16,6 @@
<property name="flap"> <property name="flap">
<object class="GtkHeaderBar" id="headerbar"> <object class="GtkHeaderBar" id="headerbar">
<property name="show_title_buttons" bind-source="LpWindow" bind-property="fullscreened" bind-flags="sync-create|invert-boolean"/> <property name="show_title_buttons" bind-source="LpWindow" bind-property="fullscreened" bind-flags="sync-create|invert-boolean"/>
<binding name="opacity">
<lookup type="GtkWidget" name="opacity">
<lookup name="controls">image_view</lookup>
</lookup>
</binding>
<child type="end"> <child type="end">
<object class="GtkMenuButton" id="menu_button"> <object class="GtkMenuButton" id="menu_button">
<property name="icon-name">open-menu-symbolic</property> <property name="icon-name">open-menu-symbolic</property>

View file

@ -3,6 +3,7 @@
<gresource prefix="/org/gnome/Loupe"> <gresource prefix="/org/gnome/Loupe">
<file compressed="true" preprocess="xml-stripblanks">gtk/help_overlay.ui</file> <file compressed="true" preprocess="xml-stripblanks">gtk/help_overlay.ui</file>
<file compressed="true" preprocess="xml-stripblanks">gtk/window.ui</file> <file compressed="true" preprocess="xml-stripblanks">gtk/window.ui</file>
<file compressed="true" preprocess="xml-stripblanks">gtk/image_page.ui</file>
<file compressed="true" preprocess="xml-stripblanks">gtk/image_view.ui</file> <file compressed="true" preprocess="xml-stripblanks">gtk/image_view.ui</file>
<file>style.css</file> <file>style.css</file>
</gresource> </gresource>

View file

@ -136,4 +136,10 @@ impl LpFileModel {
pub fn directory(&self) -> Option<gio::File> { pub fn directory(&self) -> Option<gio::File> {
self.imp().directory.get().cloned() self.imp().directory.get().cloned()
} }
pub fn index_of(&self, file: &gio::File) -> Option<usize> {
let imp = self.imp();
let vec = imp.inner.borrow();
vec.iter().position(|f| f.equal(file))
}
} }

View file

@ -13,8 +13,9 @@ run_command(
) )
rust_sources = files( rust_sources = files(
'widgets/image_view.rs',
'widgets/image.rs', 'widgets/image.rs',
'widgets/image_page.rs',
'widgets/image_view.rs',
'widgets/mod.rs', 'widgets/mod.rs',
'application.rs', 'application.rs',
'config.rs', 'config.rs',

View file

@ -82,6 +82,12 @@ mod imp {
obj.set_hexpand(true); obj.set_hexpand(true);
obj.set_vexpand(true); obj.set_vexpand(true);
} }
fn dispose(&self, obj: &Self::Type) {
while let Some(child) = obj.first_child() {
child.unparent();
}
}
} }
impl WidgetImpl for LpImage { impl WidgetImpl for LpImage {
@ -184,7 +190,6 @@ impl LpImage {
self.queue_draw(); self.queue_draw();
self.queue_resize(); self.queue_resize();
imp.image_height.get();
} }
pub fn image_width(&self) -> i32 { pub fn image_width(&self) -> i32 {
@ -197,12 +202,21 @@ impl LpImage {
imp.image_height.get() 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<gdk::Texture> { pub fn texture(&self) -> Option<gdk::Texture> {
let imp = self.imp(); let imp = self.imp();
imp.texture.borrow().clone() imp.texture.borrow().clone()
} }
pub fn content_provider(&self) -> anyhow::Result<gdk::ContentProvider> { pub fn content_provider(&self) -> gdk::ContentProvider {
let imp = self.imp(); let imp = self.imp();
let mut contents = vec![]; let mut contents = vec![];
@ -217,6 +231,6 @@ impl LpImage {
contents.push(content); contents.push(content);
} }
Ok(gdk::ContentProvider::new_union(contents.as_slice())) gdk::ContentProvider::new_union(contents.as_slice())
} }
} }

157
src/widgets/image_page.rs Normal file
View file

@ -0,0 +1,157 @@
// image_page.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 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<gtk::Stack>,
#[template_child]
pub(super) spinner: TemplateChild<gtk::Spinner>,
#[template_child]
pub(super) error_page: TemplateChild<adw::StatusPage>,
#[template_child]
pub(super) image: TemplateChild<LpImage>,
#[template_child]
pub(super) popover: TemplateChild<gtk::PopoverMenu>,
#[template_child]
pub(super) click_gesture: TemplateChild<gtk::GestureClick>,
#[template_child]
pub(super) press_gesture: TemplateChild<gtk::GestureLongPress>,
pub(super) file: OnceCell<gio::File>,
}
#[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<Self>) {
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 |_, _, 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);
}));
}
}
impl WidgetImpl for LpImagePage {}
impl BinImpl for LpImagePage {}
}
glib::wrapper! {
pub struct LpImagePage(ObjectSubclass<imp::LpImagePage>)
@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::<Self>(&[]).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<gio::File> {
self.imp().file.get().cloned()
}
pub fn texture(&self) -> Option<gdk::Texture> {
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<gdk::Texture, glib::Error> {
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()
}

View file

@ -25,7 +25,7 @@ use gtk::prelude::*;
use gtk::subclass::prelude::*; use gtk::subclass::prelude::*;
use gtk::CompositeTemplate; use gtk::CompositeTemplate;
use anyhow::Context; use anyhow::{bail, 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;
@ -34,7 +34,11 @@ use std::cell::RefCell;
use crate::file_model::LpFileModel; 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::LpImagePage;
// Maximum number of pages to load
// at any given time
const N_PAGES: u32 = 3;
mod imp { mod imp {
use super::*; use super::*;
@ -43,23 +47,10 @@ mod imp {
#[template(resource = "/org/gnome/Loupe/gtk/image_view.ui")] #[template(resource = "/org/gnome/Loupe/gtk/image_view.ui")]
pub struct LpImageView { pub struct LpImageView {
#[template_child] #[template_child]
pub picture: TemplateChild<LpImage>, pub carousel: TemplateChild<adw::Carousel>,
#[template_child]
pub controls: TemplateChild<gtk::Box>,
#[template_child]
pub popover: TemplateChild<gtk::PopoverMenu>,
#[template_child]
pub click_gesture: TemplateChild<gtk::GestureClick>,
#[template_child]
pub press_gesture: TemplateChild<gtk::GestureLongPress>,
// RefCell<T> allows for interior mutability of non-primitive types.
pub menu_model: RefCell<Option<gio::MenuModel>>,
pub popover_menu_model: RefCell<Option<gio::MenuModel>>,
pub model: RefCell<Option<LpFileModel>>, pub model: RefCell<Option<LpFileModel>>,
pub current_file: RefCell<Option<gio::File>>, pub filename: RefCell<Option<String>>,
pub uri: RefCell<Option<String>>,
} }
#[glib::object_subclass] #[glib::object_subclass]
@ -70,13 +61,14 @@ mod imp {
fn class_init(klass: &mut Self::Class) { fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass); Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
klass.install_action("iv.next", None, move |image_view, _, _| { 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, _, _| { klass.install_action("iv.previous", None, move |image_view, _, _| {
image_view.previous(); image_view.navigate(adw::NavigationDirection::Back);
}); });
} }
@ -88,22 +80,13 @@ mod imp {
impl ObjectImpl for LpImageView { impl ObjectImpl for LpImageView {
fn properties() -> &'static [glib::ParamSpec] { fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| { static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![ vec![glib::ParamSpecString::new(
glib::ParamSpecString::new( "filename",
"filename", "Filename",
"Filename", "The filename of the current file",
"The filename of the current file", None,
None, glib::ParamFlags::READABLE,
glib::ParamFlags::READABLE, )]
),
glib::ParamSpecObject::new(
"controls",
"Controls",
"The controls for the image view",
gtk::Box::static_type(),
glib::ParamFlags::READABLE,
),
]
}); });
PROPERTIES.as_ref() PROPERTIES.as_ref()
@ -112,7 +95,6 @@ mod imp {
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" => obj.filename().to_value(), "filename" => obj.filename().to_value(),
"controls" => self.controls.to_value(),
_ => unimplemented!(), _ => unimplemented!(),
} }
} }
@ -120,33 +102,16 @@ mod imp {
fn constructed(&self, obj: &Self::Type) { fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj); 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(); let source = gtk::DragSource::new();
source.connect_prepare( source.connect_prepare(
glib::clone!(@weak obj => @default-return None, move |_, _, _| { glib::clone!(@weak obj => @default-return None, move |_, _, _| {
match obj.imp().picture.content_provider() { obj.current_page().map(|p| p.content_provider())
Ok(content) => Some(content),
Err(e) => {
log::error!("Could not get content provider: {:?}", e);
None
}
}
}), }),
); );
source.connect_drag_begin(glib::clone!(@weak obj => move |source, _| { source.connect_drag_begin(glib::clone!(@weak obj => move |source, _| {
let imp = obj.imp(); if let Some(texture) = obj.current_page().and_then(|p| p.texture()) {
if let Some(texture) = imp.picture.texture() {
let thumbnail = Thumbnail::new(&texture); let thumbnail = Thumbnail::new(&texture);
source.set_icon(Some(&thumbnail), 0, 0); source.set_icon(Some(&thumbnail), 0, 0);
}; };
@ -166,85 +131,116 @@ glib::wrapper! {
@implements gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable; @implements gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
} }
#[gtk::template_callbacks]
impl LpImageView { impl LpImageView {
pub fn set_image_from_file(&self, file: &gio::File) -> anyhow::Result<(i32, i32)> { pub fn set_image_from_file(&self, file: &gio::File) -> anyhow::Result<()> {
let imp = self.imp(); if let Some(current_file) = self.current_page().and_then(|p| p.file()) {
if current_file.equal(file) {
if let Some(current_file) = imp.picture.file() { bail!("Image is the same as the previous image; Doing nothing.");
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.",
));
} }
} }
imp.current_file.replace(Some(file.clone())); self.build_model_from_file(file);
self.set_parent_from_file(file); self.update_action_state(file);
self.update_image(file);
self.notify("filename"); self.notify("filename");
let width = imp.picture.image_width(); // TODO: rework width stuff
let height = imp.picture.image_height(); // let width = imp.picture.image_width();
// let height = imp.picture.image_height();
log::debug!("Image dimensions: {} x {}", width, height); // log::debug!("Image dimensions: {} x {}", width, height);
Ok((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`.
//
// TODO: Properly loading a new model with the same view,
// & loading a file from the same directory
fn build_model_from_file(&self, file: &gio::File) {
let imp = self.imp(); let imp = self.imp();
let mut model = imp.model.borrow_mut(); let carousel = &imp.carousel;
if let Some(ref parent) = file.parent() { {
if let Some(ref m) = *model { // Here we use a nested scope so that the mutable borrow only lasts as long as we need it
if m.directory().map_or(false, |f| !f.equal(parent)) { 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)) {
*model = Some(LpFileModel::from_directory(parent));
log::debug!("new model created");
} else {
// Early return if the parent is equal to the current model's directory
return;
}
} else {
*model = Some(LpFileModel::from_directory(parent)); *model = Some(LpFileModel::from_directory(parent));
log::debug!("new model created"); log::debug!("new model created");
} }
}
}
if let Some(model) = imp.model.borrow().as_ref() {
let index = model.index_of(file).unwrap() as u32;
imp.filename.replace(util::get_file_display_name(file));
carousel.append(&LpImagePage::from_file(file));
// Here we need to check if we're at the start or end of our directory.
// If we are, we try to add two files to the other side. Otherwise,
// add one file on each end of the current file.
let iterations = if index == 0 || index == model.n_items() - 1 {
2
} else { } else {
*model = Some(LpFileModel::from_directory(parent)); 1
log::debug!("new model created"); };
let mut next_file = file.clone();
for i in 0..iterations {
log::debug!("Loop to find next files: {}", i);
if let Some(f) = model.next(&next_file) {
log::debug!("Adding next image URI: {}", f.uri());
carousel.append(&LpImagePage::from_file(&f));
next_file = f;
} else {
// Return early if there are no files in this direction;
break;
}
}
let mut prev_file = file.clone();
for i in 0..iterations {
log::debug!("Loop to find prior files: {}", i);
if let Some(f) = model.previous(&prev_file) {
log::debug!("Adding previous image URI: {}", f.uri());
carousel.prepend(&LpImagePage::from_file(&f));
prev_file = f;
} else {
break;
}
} }
} }
} }
fn update_image(&self, file: &gio::File) { pub fn navigate(&self, direction: adw::NavigationDirection) {
let imp = self.imp(); let carousel = &self.imp().carousel;
let pos = carousel.position().round() as u32;
imp.picture.set_file(file); match direction {
imp.uri.replace(Some(file.uri().to_string())); adw::NavigationDirection::Forward => {
self.update_action_state(file); if pos < carousel.n_pages() - 1 {
} carousel.scroll_to(&carousel.nth_page(pos + 1), true);
}
pub fn next(&self) {
{
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);
} }
} adw::NavigationDirection::Back => {
if pos > 0 {
self.notify("filename"); carousel.scroll_to(&carousel.nth_page(pos - 1), true)
} }
pub fn previous(&self) {
{
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);
} }
} _ => unimplemented!("Navigation direction should only be back or forward."),
};
self.notify("filename");
} }
pub fn update_action_state(&self, file: &gio::File) { pub fn update_action_state(&self, file: &gio::File) {
@ -259,6 +255,37 @@ impl LpImageView {
self.action_set_enabled("iv.previous", prev_enabled); self.action_set_enabled("iv.previous", prev_enabled);
} }
#[template_callback]
fn page_changed_cb(&self, index: u32, carousel: &adw::Carousel) {
let imp = self.imp();
let b = imp.model.borrow();
let model = b.as_ref().unwrap();
let current = self.current_page().and_then(|p| p.file()).unwrap();
imp.filename.replace(util::get_file_display_name(&current));
self.notify("filename");
self.update_action_state(&current);
// We've moved forward
if index + 1 == N_PAGES {
if let Some(ref next) = model.next(&current) {
log::debug!("Next URI: {}", next.uri());
// Remove the page at index 0, add a new page at the end
carousel.remove(&carousel.nth_page(0));
carousel.append(&LpImagePage::from_file(next));
}
}
// We've moved backward
if index == 0 {
if let Some(ref prev) = model.previous(&current) {
log::debug!("Previous URI: {}", prev.uri());
// Remove the page at the front, add a new page at the back
carousel.remove(&carousel.nth_page(carousel.n_pages() - 1));
carousel.prepend(&LpImagePage::from_file(prev));
}
}
}
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();
@ -293,10 +320,10 @@ impl LpImageView {
} }
pub fn print(&self) -> anyhow::Result<()> { pub fn print(&self) -> anyhow::Result<()> {
let imp = self.imp();
let operation = gtk::PrintOperation::new(); let operation = gtk::PrintOperation::new();
let path = (imp.current_file.borrow().as_ref()) let path = self
.current_page()
.and_then(|p| p.file())
.context("No file")? .context("No file")?
.peek_path() .peek_path()
.context("No path")?; .context("No path")?;
@ -334,9 +361,8 @@ impl LpImageView {
pub fn copy(&self) -> anyhow::Result<()> { pub fn copy(&self) -> anyhow::Result<()> {
let clipboard = self.clipboard(); 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); clipboard.set_texture(&texture);
} else { } else {
anyhow::bail!("No Image displayed."); anyhow::bail!("No Image displayed.");
@ -345,24 +371,23 @@ impl LpImageView {
Ok(()) Ok(())
} }
fn current_page(&self) -> Option<LpImagePage> {
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 uri(&self) -> Option<String> { pub fn uri(&self) -> Option<String> {
let imp = self.imp(); let page = self.current_page().expect("No page");
imp.uri.borrow().to_owned() let file = page.file().expect("No file");
Some(file.uri().to_string())
} }
pub fn filename(&self) -> Option<String> { pub fn filename(&self) -> Option<String> {
let imp = self.imp(); self.imp().filename.borrow().clone()
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) {
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();
} }
} }

View file

@ -18,7 +18,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
mod image; mod image;
mod image_page;
mod image_view; mod image_view;
pub use image::LpImage; pub use image::LpImage;
pub use image_page::LpImagePage;
pub use image_view::LpImageView; pub use image_view::LpImageView;

View file

@ -324,15 +324,15 @@ impl LpWindow {
imp.toast_overlay.add_toast(&toast); 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(); let imp = self.imp();
log::debug!("Loading file: {}", file.uri().to_string()); log::debug!("Loading file: {}", file.uri().to_string());
match imp.image_view.set_image_from_file(file) { match imp.image_view.set_image_from_file(file) {
Ok((width, height)) => { Ok((/*width, height*/)) => {
if resize { // if resize {
self.resize_from_dimensions(width, height); // self.resize_from_dimensions(width, height);
} // }
imp.stack.set_visible_child(&*imp.image_view); imp.stack.set_visible_child(&*imp.image_view);
imp.image_view.grab_focus(); imp.image_view.grab_focus();