2020-12-26 02:05:34 -08:00
|
|
|
// image_view.rs
|
|
|
|
|
//
|
|
|
|
|
// Copyright 2020 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
|
|
|
|
|
|
2021-12-21 16:09:48 -08:00
|
|
|
use crate::deps::*;
|
2023-01-23 01:10:15 +01:00
|
|
|
use crate::file_model::LpFileModel;
|
2022-08-05 18:54:04 -04:00
|
|
|
use crate::i18n::*;
|
2023-01-23 01:10:15 +01:00
|
|
|
use crate::thumbnail::Thumbnail;
|
2023-02-06 19:14:01 +01:00
|
|
|
use crate::util::{Direction, Position};
|
2023-01-23 01:10:15 +01:00
|
|
|
use crate::widgets::{LpImage, LpImagePage, LpSlidingView};
|
2021-12-21 16:09:48 -08:00
|
|
|
|
2022-11-16 20:14:39 +01:00
|
|
|
use adw::prelude::*;
|
2021-12-21 12:15:46 -08:00
|
|
|
use adw::subclass::prelude::*;
|
2023-01-23 01:10:15 +01:00
|
|
|
use anyhow::Context;
|
2021-08-17 20:37:04 -07:00
|
|
|
use ashpd::desktop::wallpaper;
|
2022-08-17 21:31:36 -03:00
|
|
|
use ashpd::desktop::ResponseError;
|
|
|
|
|
use ashpd::Error;
|
2021-08-17 20:37:04 -07:00
|
|
|
use ashpd::WindowIdentifier;
|
2023-01-23 01:10:15 +01:00
|
|
|
use glib::clone;
|
|
|
|
|
use gtk::CompositeTemplate;
|
2022-08-03 19:32:02 -04:00
|
|
|
use gtk_macros::spawn;
|
2023-01-23 01:10:15 +01:00
|
|
|
use once_cell::sync::Lazy;
|
2022-08-03 19:32:02 -04:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
use std::cell::{Cell, RefCell};
|
|
|
|
|
use std::path::{Path, PathBuf};
|
2022-01-19 23:11:38 -08:00
|
|
|
|
2022-06-29 18:25:45 -04:00
|
|
|
// The number of pages we want to buffer
|
|
|
|
|
// on either side of the current page.
|
2023-01-23 01:10:15 +01:00
|
|
|
const BUFFER: usize = 2;
|
2021-09-05 05:51:34 -07:00
|
|
|
|
2020-12-26 02:05:34 -08:00
|
|
|
mod imp {
|
2023-03-02 22:43:29 -05:00
|
|
|
use once_cell::sync::OnceCell;
|
|
|
|
|
|
2020-12-26 02:05:34 -08:00
|
|
|
use super::*;
|
|
|
|
|
|
2021-09-05 05:51:34 -07:00
|
|
|
#[derive(Debug, Default, CompositeTemplate)]
|
2023-01-23 01:10:15 +01:00
|
|
|
#[template(file = "../../data/gtk/image_view.ui")]
|
2021-12-21 10:55:02 -08:00
|
|
|
pub struct LpImageView {
|
2022-11-16 20:14:39 +01:00
|
|
|
/// Direct child of this Adw::Bin
|
|
|
|
|
#[template_child]
|
|
|
|
|
pub bin_child: TemplateChild<gtk::Widget>,
|
2022-12-08 02:00:50 +01:00
|
|
|
/// overlayed controls
|
|
|
|
|
#[template_child]
|
|
|
|
|
pub controls_box_start: TemplateChild<gtk::Widget>,
|
|
|
|
|
/// overlayed controls
|
|
|
|
|
#[template_child]
|
|
|
|
|
pub controls_box_end: TemplateChild<gtk::Widget>,
|
2020-12-26 02:05:34 -08:00
|
|
|
#[template_child]
|
2023-01-23 01:10:15 +01:00
|
|
|
pub sliding_view: TemplateChild<LpSlidingView>,
|
2022-11-16 20:14:39 +01:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
pub model: RefCell<LpFileModel>,
|
|
|
|
|
pub preserve_content: Cell<bool>,
|
2023-03-02 22:43:29 -05:00
|
|
|
|
|
|
|
|
pub current_image_signals: OnceCell<glib::SignalGroup>,
|
2020-12-26 02:05:34 -08:00
|
|
|
}
|
|
|
|
|
|
2021-06-27 15:28:35 -07:00
|
|
|
#[glib::object_subclass]
|
2021-12-21 10:55:02 -08:00
|
|
|
impl ObjectSubclass for LpImageView {
|
|
|
|
|
const NAME: &'static str = "LpImageView";
|
|
|
|
|
type Type = super::LpImageView;
|
2021-12-21 12:15:46 -08:00
|
|
|
type ParentType = adw::Bin;
|
2020-12-26 02:05:34 -08:00
|
|
|
|
|
|
|
|
fn class_init(klass: &mut Self::Class) {
|
2021-01-30 15:52:32 -08:00
|
|
|
Self::bind_template(klass);
|
2022-01-19 23:11:38 -08:00
|
|
|
Self::Type::bind_template_callbacks(klass);
|
2020-12-26 02:05:34 -08:00
|
|
|
}
|
2021-01-13 23:05:45 -08:00
|
|
|
|
2021-06-27 15:28:35 -07:00
|
|
|
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
2021-01-13 23:05:45 -08:00
|
|
|
obj.init_template();
|
|
|
|
|
}
|
2020-12-26 02:05:34 -08:00
|
|
|
}
|
|
|
|
|
|
2021-12-21 10:55:02 -08:00
|
|
|
impl ObjectImpl for LpImageView {
|
2021-01-30 15:52:32 -08:00
|
|
|
fn properties() -> &'static [glib::ParamSpec] {
|
|
|
|
|
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
2022-11-10 19:39:23 +01:00
|
|
|
vec![
|
2023-01-23 01:10:15 +01:00
|
|
|
glib::ParamSpecObject::builder::<LpImagePage>("current-page")
|
2022-11-10 19:39:23 +01:00
|
|
|
.read_only()
|
|
|
|
|
.build(),
|
2022-12-06 18:24:32 +01:00
|
|
|
glib::ParamSpecBoolean::builder("is-previous-available")
|
|
|
|
|
.read_only()
|
|
|
|
|
.build(),
|
|
|
|
|
glib::ParamSpecBoolean::builder("is-next-available")
|
|
|
|
|
.read_only()
|
|
|
|
|
.build(),
|
2022-11-10 19:39:23 +01:00
|
|
|
]
|
2021-01-30 15:52:32 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
PROPERTIES.as_ref()
|
2020-12-26 02:05:34 -08:00
|
|
|
}
|
|
|
|
|
|
2022-11-02 21:11:43 +01:00
|
|
|
fn property(&self, _id: usize, psec: &glib::ParamSpec) -> glib::Value {
|
2023-02-12 02:07:42 +01:00
|
|
|
let obj = self.obj();
|
2021-06-27 15:28:35 -07:00
|
|
|
match psec.name() {
|
2023-01-23 01:10:15 +01:00
|
|
|
"current-page" => obj.current_page().to_value(),
|
2022-12-06 18:24:32 +01:00
|
|
|
"is-previous-available" => obj.is_previous_available().to_value(),
|
|
|
|
|
"is-next-available" => obj.is_next_available().to_value(),
|
2020-12-26 02:05:34 -08:00
|
|
|
_ => unimplemented!(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-02 21:11:43 +01:00
|
|
|
fn constructed(&self) {
|
2023-02-12 02:07:42 +01:00
|
|
|
let obj = self.obj();
|
2022-11-02 21:11:43 +01:00
|
|
|
|
|
|
|
|
self.parent_constructed();
|
2021-01-30 15:52:32 -08:00
|
|
|
|
2022-11-16 20:14:39 +01:00
|
|
|
// Manually mange widget layout, see `WidgetImpl` for details
|
2023-02-12 02:07:42 +01:00
|
|
|
obj.set_layout_manager(None::<gtk::LayoutManager>);
|
2022-11-16 20:14:39 +01:00
|
|
|
|
2023-03-02 22:43:29 -05:00
|
|
|
let signal_group = glib::SignalGroup::new(LpImage::static_type());
|
|
|
|
|
obj.connect_notify_local(
|
|
|
|
|
Some("current-page"),
|
|
|
|
|
clone!(@weak signal_group => move |obj, _| {
|
|
|
|
|
signal_group.set_target(obj.current_image().as_ref());
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let view = &*obj;
|
|
|
|
|
signal_group.connect_closure(
|
|
|
|
|
"notify::path",
|
|
|
|
|
true,
|
|
|
|
|
glib::closure_local!(@watch view => move |_: LpImage, _: glib::ParamSpec| {
|
|
|
|
|
view.current_image_path_changed();
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
signal_group.connect_closure(
|
|
|
|
|
"notify::is-deleted",
|
|
|
|
|
true,
|
|
|
|
|
glib::closure_local!(@watch view => move |_: LpImage, _: glib::ParamSpec| {
|
|
|
|
|
view.current_image_path_changed();
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
self.current_image_signals.set(signal_group).unwrap();
|
2023-01-23 01:10:15 +01:00
|
|
|
|
2022-01-08 16:21:27 +01:00
|
|
|
let source = gtk::DragSource::new();
|
2022-04-20 13:03:19 -07:00
|
|
|
source.set_exclusive(true);
|
2022-01-08 16:21:27 +01:00
|
|
|
|
|
|
|
|
source.connect_prepare(
|
2022-11-10 19:39:23 +01:00
|
|
|
glib::clone!(@weak obj => @default-return None, move |gesture, _, _| {
|
|
|
|
|
let is_scrollable = obj
|
|
|
|
|
.current_page()
|
|
|
|
|
.map(|p| p.image().is_hscrollable() || p.image().is_vscrollable());
|
|
|
|
|
|
|
|
|
|
// do scrolling if scrollable
|
|
|
|
|
if is_scrollable == Some(true) {
|
|
|
|
|
gesture.set_state(gtk::EventSequenceState::Denied);
|
|
|
|
|
None
|
|
|
|
|
} else {
|
2022-11-28 18:27:40 +01:00
|
|
|
obj.current_page().and_then(|p| p.content_provider())
|
2022-11-10 19:39:23 +01:00
|
|
|
}
|
2022-01-08 16:21:27 +01:00
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
source.connect_drag_begin(glib::clone!(@weak obj => move |source, _| {
|
2022-01-19 23:11:38 -08:00
|
|
|
if let Some(texture) = obj.current_page().and_then(|p| p.texture()) {
|
2022-01-08 16:21:27 +01:00
|
|
|
let thumbnail = Thumbnail::new(&texture);
|
|
|
|
|
source.set_icon(Some(&thumbnail), 0, 0);
|
|
|
|
|
};
|
|
|
|
|
}));
|
|
|
|
|
|
2023-02-12 02:07:42 +01:00
|
|
|
obj.add_controller(source);
|
2021-01-30 15:52:32 -08:00
|
|
|
}
|
2020-12-26 02:05:34 -08:00
|
|
|
}
|
|
|
|
|
|
2022-11-16 20:14:39 +01:00
|
|
|
impl WidgetImpl for LpImageView {
|
2023-02-20 23:44:17 +01:00
|
|
|
/// The main target of this manual implementation is to provide a good
|
2022-11-16 20:14:39 +01:00
|
|
|
/// natural width that allows to have a neat size for newly opened `LpWindow`s.
|
|
|
|
|
/// The actual calculation of the image natural width happens in
|
|
|
|
|
/// the measure function of `LpImage`.
|
|
|
|
|
///
|
|
|
|
|
/// This manual implementation is necessary since AdwCarousel gives the
|
|
|
|
|
/// largest natural width of all of it's children. But we actually want the
|
|
|
|
|
/// one of the opened (current) image.
|
|
|
|
|
fn measure(&self, orientation: gtk::Orientation, for_size: i32) -> (i32, i32, i32, i32) {
|
|
|
|
|
// Measure child of AdwBin
|
2022-12-08 02:00:50 +01:00
|
|
|
let (child_min, child_natural, _, _) = self.bin_child.measure(orientation, for_size);
|
|
|
|
|
|
|
|
|
|
// measure size of controls overlays
|
|
|
|
|
let (overlay1_min, _, _, _) = self.controls_box_start.measure(orientation, for_size);
|
|
|
|
|
let (overlay2_min, _, _, _) = self.controls_box_end.measure(orientation, for_size);
|
|
|
|
|
|
|
|
|
|
// take as minimum whatever is larger
|
|
|
|
|
let min = i32::max(child_min, overlay1_min + overlay2_min);
|
2022-11-16 20:14:39 +01:00
|
|
|
|
2023-02-12 02:07:42 +01:00
|
|
|
if let Some(page) = self.obj().current_page() {
|
2022-11-16 20:14:39 +01:00
|
|
|
// measure `LpImage`
|
|
|
|
|
let (_, image_natural, _, _) = page.image().measure(orientation, for_size);
|
|
|
|
|
|
|
|
|
|
// Ensure that minimum size is not smaller than the one of the child.
|
|
|
|
|
// Also ensure that the natural width is not smaller than the minimal size.
|
|
|
|
|
// Both things are required by GTK.
|
2022-12-08 02:00:50 +01:00
|
|
|
(min, i32::max(min, image_natural), -1, -1)
|
2022-11-16 20:14:39 +01:00
|
|
|
} else {
|
2022-12-08 02:00:50 +01:00
|
|
|
// also include controls overlays when mostly passing through child measurements
|
|
|
|
|
(min, i32::max(child_natural, min), -1, -1)
|
2022-11-16 20:14:39 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// this is necessary because we do layout manually
|
|
|
|
|
fn size_allocate(&self, width: i32, height: i32, baseline: i32) {
|
|
|
|
|
self.bin_child.allocate(width, height, baseline, None);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-21 10:55:02 -08:00
|
|
|
impl BinImpl for LpImageView {}
|
2020-12-26 02:05:34 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
glib::wrapper! {
|
2021-12-21 10:55:02 -08:00
|
|
|
pub struct LpImageView(ObjectSubclass<imp::LpImageView>)
|
2021-12-21 12:15:46 -08:00
|
|
|
@extends gtk::Widget, adw::Bin,
|
2020-12-26 02:05:34 -08:00
|
|
|
@implements gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-19 23:11:38 -08:00
|
|
|
#[gtk::template_callbacks]
|
2021-12-21 10:55:02 -08:00
|
|
|
impl LpImageView {
|
2023-01-23 01:10:15 +01:00
|
|
|
pub fn set_image_from_path(&self, path: &Path) {
|
|
|
|
|
if let Err(err) = self.load_path(path) {
|
|
|
|
|
log::error!("Failed to load path: {err}");
|
|
|
|
|
self.activate_action("win.show-toast", Some(&(err.to_string(), 1).to_variant()))
|
|
|
|
|
.unwrap();
|
2021-09-05 05:51:34 -07:00
|
|
|
}
|
2021-03-02 16:39:03 -08:00
|
|
|
}
|
|
|
|
|
|
2022-01-19 23:11:38 -08:00
|
|
|
// 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`.
|
2023-01-23 01:10:15 +01:00
|
|
|
fn load_path(&self, path: &Path) -> anyhow::Result<()> {
|
|
|
|
|
let sliding_view = self.sliding_view();
|
|
|
|
|
let directory = path.parent().map(|x| x.to_path_buf());
|
2021-09-05 05:51:34 -07:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
let is_same_directory = directory == self.model().directory();
|
2022-11-16 20:14:39 +01:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
if is_same_directory {
|
|
|
|
|
log::debug!("Re-using old model and navigating to the current file");
|
|
|
|
|
self.navigate_to_path(path);
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
2022-11-26 00:02:56 +01:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
let model = LpFileModel::from_path(path);
|
|
|
|
|
self.set_model(model);
|
|
|
|
|
log::debug!("New model created");
|
2022-11-16 20:14:39 +01:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
let page = LpImagePage::from_path(path);
|
2022-11-26 00:02:56 +01:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
sliding_view.clear();
|
|
|
|
|
sliding_view.append(&page);
|
|
|
|
|
|
|
|
|
|
// List other files in directory
|
|
|
|
|
if let Some(directory) = directory {
|
|
|
|
|
let path = path.to_path_buf();
|
|
|
|
|
spawn!(glib::clone!(@weak self as obj, @strong path => async move {
|
|
|
|
|
if let Err(err) = obj.model().load_directory(directory).await {
|
|
|
|
|
log::warn!("Failed to load directory: {err}");
|
|
|
|
|
obj.activate_action("win.show-toast", Some(&(err.to_string(), 1).to_variant()))
|
|
|
|
|
.unwrap();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
obj.update_sliding_view(&path);
|
|
|
|
|
}));
|
|
|
|
|
}
|
2022-11-04 19:28:48 +01:00
|
|
|
|
|
|
|
|
Ok(())
|
2022-01-19 23:11:38 -08:00
|
|
|
}
|
2022-01-16 19:03:13 -08:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
/// Move forward or backwards
|
2023-02-06 19:14:01 +01:00
|
|
|
pub fn navigate(&self, direction: Direction) {
|
2023-01-23 01:10:15 +01:00
|
|
|
if let Some(current_path) = self.current_path() {
|
|
|
|
|
let new_path = match direction {
|
2023-02-06 19:14:01 +01:00
|
|
|
Direction::Forward => self.model().after(¤t_path),
|
|
|
|
|
Direction::Back => self.model().before(¤t_path),
|
2023-01-23 01:10:15 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Some(new_path) = new_path {
|
|
|
|
|
self.scroll_sliding_view(&new_path);
|
2022-01-19 23:11:38 -08:00
|
|
|
}
|
2023-01-23 01:10:15 +01:00
|
|
|
}
|
2021-09-05 05:51:34 -07:00
|
|
|
}
|
|
|
|
|
|
2023-02-06 19:14:01 +01:00
|
|
|
/// Jump to position
|
|
|
|
|
pub fn jump(&self, position: Position) {
|
|
|
|
|
let new_path = match position {
|
|
|
|
|
Position::First => self.model().first(),
|
|
|
|
|
Position::Last => self.model().last(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Some(new_path) = new_path {
|
|
|
|
|
self.navigate_to_path(&new_path);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
/// Used for drag and drop
|
|
|
|
|
fn navigate_to_path(&self, new_path: &Path) {
|
|
|
|
|
let sliding_view = self.sliding_view();
|
2022-12-06 18:24:32 +01:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
if Some(new_path.to_path_buf()) == self.current_path() {
|
2022-12-06 18:24:32 +01:00
|
|
|
return;
|
2023-01-23 01:10:15 +01:00
|
|
|
}
|
2022-06-28 22:54:26 -04:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
let Some(new_index) = self.model().index_of(new_path) else {
|
|
|
|
|
log::warn!("Could not navigate to file {new_path:?}");
|
2022-06-28 22:54:26 -04:00
|
|
|
return;
|
2023-01-23 01:10:15 +01:00
|
|
|
};
|
2022-06-28 22:54:26 -04:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
let current_index = self
|
|
|
|
|
.current_path()
|
|
|
|
|
.and_then(|x| self.model().index_of(&x))
|
|
|
|
|
.unwrap_or_default();
|
2022-06-28 22:54:26 -04:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
let page = if let Some(page) = sliding_view.get(new_path) {
|
|
|
|
|
page
|
2022-06-28 22:54:26 -04:00
|
|
|
} else {
|
2023-01-23 01:10:15 +01:00
|
|
|
let page = LpImagePage::from_path(new_path);
|
2022-06-28 22:54:26 -04:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
if new_index > current_index {
|
|
|
|
|
sliding_view.append(&page);
|
|
|
|
|
} else {
|
|
|
|
|
sliding_view.prepend(&page);
|
|
|
|
|
}
|
2022-12-06 18:24:32 +01:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
page
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
log::debug!("Scrolling to page for {new_path:?}");
|
|
|
|
|
self.imp().preserve_content.set(true);
|
|
|
|
|
sliding_view.scroll_to(&page);
|
2022-06-29 12:22:39 -04:00
|
|
|
}
|
|
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
/// Ensures the sliding view contains the correct images
|
|
|
|
|
fn update_sliding_view(&self, current_path: &Path) {
|
|
|
|
|
log::debug!("Updating sliding_view neighbors for current path {current_path:?}");
|
|
|
|
|
let sliding_view = self.sliding_view();
|
2022-06-28 22:54:26 -04:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
self.imp().preserve_content.set(false);
|
|
|
|
|
|
|
|
|
|
let existing = sliding_view.pages();
|
|
|
|
|
let target = self.model().files_around(current_path, BUFFER);
|
|
|
|
|
|
|
|
|
|
// remove old pages
|
|
|
|
|
for (path, page) in &existing {
|
|
|
|
|
if !target.contains(path) {
|
|
|
|
|
sliding_view.remove(page);
|
2022-06-28 22:54:26 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
// add missing pages or put in correct position
|
|
|
|
|
for (position, path) in target.iter().enumerate() {
|
|
|
|
|
if let Some(page) = existing.get(path) {
|
|
|
|
|
sliding_view.move_to(page, position);
|
|
|
|
|
} else {
|
|
|
|
|
sliding_view.insert(&LpImagePage::from_path(path), position);
|
2022-06-28 22:54:26 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-06 18:24:32 +01:00
|
|
|
self.notify("is-previous-available");
|
|
|
|
|
self.notify("is-next-available");
|
2022-06-29 12:22:39 -04:00
|
|
|
}
|
|
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
fn scroll_sliding_view(&self, path: &Path) {
|
|
|
|
|
let Some(current_page) = self.sliding_view().pages().remove(path) else {
|
2023-02-20 23:44:17 +01:00
|
|
|
log::error!("Current path not available in sliding_view for scrolling: {path:?}");
|
2023-01-23 01:10:15 +01:00
|
|
|
return;
|
|
|
|
|
};
|
2022-06-29 12:22:39 -04:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
self.sliding_view().scroll_to(¤t_page);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn sliding_view(&self) -> LpSlidingView {
|
|
|
|
|
self.imp().sliding_view.clone()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn model(&self) -> LpFileModel {
|
|
|
|
|
self.imp().model.borrow().clone()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn set_model(&self, model: LpFileModel) {
|
|
|
|
|
model.connect_changed(
|
|
|
|
|
glib::clone!(@weak self as obj => move || obj.model_content_changed_cb()),
|
|
|
|
|
);
|
|
|
|
|
self.imp().model.replace(model);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Handle files are added or removed from directory
|
|
|
|
|
fn model_content_changed_cb(&self) {
|
|
|
|
|
let Some(current_path) = self.current_path() else { return; };
|
2022-06-29 12:22:39 -04:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
// LpImage did not get the update yet
|
|
|
|
|
// Update will be handled by current_image_path_changed
|
|
|
|
|
if !self.model().contains(¤t_path) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.update_sliding_view(¤t_path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Handle current image being moved or deleted
|
|
|
|
|
///
|
|
|
|
|
/// This is handled separately since we want to delete animations and
|
|
|
|
|
/// want to still show the same image if renamed.
|
|
|
|
|
fn current_image_path_changed(&self) {
|
|
|
|
|
if let Some(image) = self.current_page().map(|x| x.image()) {
|
|
|
|
|
if image.is_deleted() {
|
|
|
|
|
self.sliding_view().scroll_to_neighbor();
|
2022-06-29 12:22:39 -04:00
|
|
|
}
|
2023-01-23 01:10:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(current_path) = self.current_path() {
|
2023-02-12 01:19:47 +01:00
|
|
|
if self.model().contains(¤t_path) && !self.imp().preserve_content.get() {
|
|
|
|
|
self.update_sliding_view(¤t_path);
|
2022-06-29 12:22:39 -04:00
|
|
|
}
|
|
|
|
|
}
|
2023-01-23 01:10:15 +01:00
|
|
|
}
|
2022-12-06 18:24:32 +01:00
|
|
|
|
2023-01-30 19:07:17 +01:00
|
|
|
pub fn current_image(&self) -> Option<LpImage> {
|
|
|
|
|
self.imp().sliding_view.current_page().map(|x| x.image())
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-02 23:14:14 -05:00
|
|
|
pub fn current_image_signals(&self) -> &glib::SignalGroup {
|
|
|
|
|
self.imp()
|
|
|
|
|
.current_image_signals
|
|
|
|
|
.get()
|
|
|
|
|
.expect("Signal group should be set up during construction")
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
pub fn current_page(&self) -> Option<LpImagePage> {
|
|
|
|
|
self.imp().sliding_view.current_page()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn current_path(&self) -> Option<PathBuf> {
|
|
|
|
|
self.imp().sliding_view.current_page().map(|x| x.path())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn current_file(&self) -> Option<gio::File> {
|
|
|
|
|
self.imp()
|
|
|
|
|
.sliding_view
|
|
|
|
|
.current_page()
|
|
|
|
|
.and_then(|x| x.image().file())
|
2022-06-28 22:54:26 -04:00
|
|
|
}
|
|
|
|
|
|
2022-12-06 18:24:32 +01:00
|
|
|
/// Returns `true` if there is an image before the current one
|
|
|
|
|
pub fn is_previous_available(&self) -> bool {
|
2023-01-23 01:10:15 +01:00
|
|
|
if let Some(path) = self.current_path() {
|
|
|
|
|
self.model().index_of(&path) != Some(0)
|
2022-12-06 18:24:32 +01:00
|
|
|
} else {
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-01-16 19:03:13 -08:00
|
|
|
|
2022-12-06 18:24:32 +01:00
|
|
|
/// Returns `true` if there is an image after the current one
|
|
|
|
|
pub fn is_next_available(&self) -> bool {
|
2023-01-23 01:10:15 +01:00
|
|
|
if let Some(path) = self.current_path() {
|
|
|
|
|
let model = self.model();
|
|
|
|
|
model.index_of(&path) != Some(model.n_files().saturating_sub(1))
|
2022-12-06 18:24:32 +01:00
|
|
|
} else {
|
|
|
|
|
false
|
|
|
|
|
}
|
2021-09-05 05:51:34 -07:00
|
|
|
}
|
|
|
|
|
|
2022-01-19 23:11:38 -08:00
|
|
|
#[template_callback]
|
2023-01-23 01:10:15 +01:00
|
|
|
fn page_changed_cb(&self) {
|
|
|
|
|
self.notify("current-page");
|
|
|
|
|
self.notify("is-previous-available");
|
|
|
|
|
self.notify("is-next-available");
|
2022-08-19 12:06:07 -04:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
let Some(new_page) = self.current_page() else {
|
|
|
|
|
log::debug!("Page changed but no current page");
|
|
|
|
|
return;
|
|
|
|
|
};
|
2022-06-28 18:52:52 -04:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
if !self.imp().preserve_content.get() {
|
|
|
|
|
self.update_sliding_view(&new_page.path());
|
2022-08-19 12:06:07 -04:00
|
|
|
}
|
2023-01-23 01:10:15 +01:00
|
|
|
}
|
2022-01-19 23:11:38 -08:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
#[template_callback]
|
2023-02-02 20:23:05 +01:00
|
|
|
/// Called when page change animation completed
|
2023-01-23 01:10:15 +01:00
|
|
|
fn target_page_reached_cb(&self) {
|
2023-02-02 20:23:05 +01:00
|
|
|
let current_page = self.current_page();
|
|
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
if self.imp().preserve_content.get() {
|
2023-02-02 20:23:05 +01:00
|
|
|
if let Some(new_page) = ¤t_page {
|
2023-01-23 01:10:15 +01:00
|
|
|
self.update_sliding_view(&new_page.path());
|
|
|
|
|
} else {
|
|
|
|
|
log::error!("No LpImagePage");
|
2022-01-19 23:11:38 -08:00
|
|
|
}
|
|
|
|
|
}
|
2023-02-02 20:23:05 +01:00
|
|
|
|
|
|
|
|
// Reset zoom and rotation of other pages
|
|
|
|
|
if let Some(new_page) = ¤t_page {
|
|
|
|
|
let mut other_pages = self.sliding_view().pages();
|
|
|
|
|
other_pages.remove(&new_page.path());
|
|
|
|
|
for (_, page) in other_pages {
|
|
|
|
|
let image = page.image();
|
decoder: Use image-rs, libheif-rs, librsvg
This adds proper support for a lot of image formats including SVGs.
It will be the basis for many features to come. The technical core
is an asynchronous renderer that can arrange textures in smaller tiles
which might be generated on demand.
Closes #34, #40, #41, #49
Basis for fixing the following issues #7, #27, #36, #43, #68, #71
2023-03-04 03:14:52 +01:00
|
|
|
image.reset_rotation();
|
2023-02-02 20:23:05 +01:00
|
|
|
image.set_best_fit(true);
|
|
|
|
|
image.zoom_best_fit();
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-01-19 23:11:38 -08:00
|
|
|
}
|
|
|
|
|
|
2022-07-10 19:39:03 -04:00
|
|
|
#[template_callback]
|
|
|
|
|
fn get_fullscreen_icon(&self, fullscreened: bool) -> &'static str {
|
|
|
|
|
if fullscreened {
|
|
|
|
|
"view-restore-symbolic"
|
|
|
|
|
} else {
|
|
|
|
|
"view-fullscreen-symbolic"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
pub fn zoom_out(&self) {
|
|
|
|
|
if let Some(current_page) = self.current_page() {
|
|
|
|
|
current_page.image().zoom_out();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn zoom_in(&self) {
|
|
|
|
|
if let Some(current_page) = self.current_page() {
|
|
|
|
|
current_page.image().zoom_in();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn zoom_to(&self, level: f64) {
|
|
|
|
|
if let Some(current_page) = self.current_page() {
|
|
|
|
|
current_page.image().zoom_to(level);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-05 00:57:18 +01:00
|
|
|
pub fn rotate_image(&self, angle: f64) {
|
2022-11-03 17:40:06 +01:00
|
|
|
if let Some(current_page) = self.current_page() {
|
2022-11-10 19:39:23 +01:00
|
|
|
current_page.image().rotate_by(angle);
|
2022-11-03 17:40:06 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-22 12:18:43 -05:00
|
|
|
pub async fn set_background(&self) -> anyhow::Result<()> {
|
2023-01-23 01:10:15 +01:00
|
|
|
let background = self
|
|
|
|
|
.current_path()
|
|
|
|
|
.and_then(|p| std::fs::File::open(p).ok())
|
|
|
|
|
.context(i18n("Failed to open new background image"))?;
|
2023-01-22 12:18:43 -05:00
|
|
|
let native = self.native().expect("View should have a GtkNative");
|
|
|
|
|
let id = WindowIdentifier::from_native(&native).await;
|
|
|
|
|
|
|
|
|
|
match wallpaper::WallpaperRequest::default()
|
|
|
|
|
.set_on(wallpaper::SetOn::Background)
|
|
|
|
|
.show_preview(true)
|
|
|
|
|
.identifier(id)
|
2023-01-23 01:10:15 +01:00
|
|
|
.build_file(&background)
|
2023-01-22 12:18:43 -05:00
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
// We use `1` here because we can't pass enums directly as GVariants,
|
|
|
|
|
// so we need to use the C int value of the enum.
|
|
|
|
|
// `TOAST_PRIORITY_NORMAL = 0`, and `TOAST_PRIORITY_HIGH = 1`
|
|
|
|
|
Ok(_) => self
|
|
|
|
|
.activate_action(
|
|
|
|
|
"win.show-toast",
|
|
|
|
|
Some(&(i18n("Set as background."), 1).to_variant()),
|
|
|
|
|
)
|
|
|
|
|
.unwrap(),
|
|
|
|
|
Err(err) => {
|
|
|
|
|
if !matches!(err, Error::Response(ResponseError::Cancelled)) {
|
|
|
|
|
self.activate_action(
|
2022-11-02 21:11:43 +01:00
|
|
|
"win.show-toast",
|
2023-01-22 12:18:43 -05:00
|
|
|
Some(&(i18n("Could not set background."), 1).to_variant()),
|
2022-11-02 21:11:43 +01:00
|
|
|
)
|
2023-01-22 12:18:43 -05:00
|
|
|
.unwrap();
|
2022-11-02 21:11:43 +01:00
|
|
|
}
|
2023-01-22 12:18:43 -05:00
|
|
|
}
|
|
|
|
|
};
|
2020-12-26 02:05:34 -08:00
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn print(&self) -> anyhow::Result<()> {
|
|
|
|
|
let operation = gtk::PrintOperation::new();
|
2023-01-23 01:10:15 +01:00
|
|
|
let path = self.current_path().context("No file")?;
|
2020-12-26 02:05:34 -08:00
|
|
|
let pb = gdk_pixbuf::Pixbuf::from_file(path)?;
|
|
|
|
|
|
|
|
|
|
let setup = gtk::PageSetup::default();
|
2023-02-12 02:07:42 +01:00
|
|
|
let page_size = gtk::PaperSize::new(Some(gtk::PAPER_NAME_A4));
|
2020-12-26 02:05:34 -08:00
|
|
|
setup.set_paper_size(&page_size);
|
|
|
|
|
operation.set_default_page_setup(Some(&setup));
|
|
|
|
|
|
|
|
|
|
let settings = gtk::PrintSettings::default();
|
|
|
|
|
operation.set_print_settings(Some(&settings));
|
|
|
|
|
|
|
|
|
|
operation.connect_begin_print(move |op, _| {
|
|
|
|
|
op.set_n_pages(1);
|
|
|
|
|
});
|
|
|
|
|
|
2021-09-28 22:59:37 -07:00
|
|
|
// FIXME: Rework all of this after reading https://cairographics.org/manual/cairo-Image-Surfaces.html
|
|
|
|
|
// Since I don't know cairo; See also eog-print.c
|
2021-03-02 15:09:44 -08:00
|
|
|
operation.connect_draw_page(clone!(@weak pb => move |_, ctx, _| {
|
2022-01-08 22:18:12 -08:00
|
|
|
let cr = ctx.cairo_context();
|
2020-12-26 02:05:34 -08:00
|
|
|
cr.set_source_pixbuf(&pb, 0.0, 0.0);
|
2021-09-28 22:59:37 -07:00
|
|
|
let _ = cr.paint();
|
2020-12-26 02:05:34 -08:00
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
log::debug!("Running print operation...");
|
2021-06-27 15:28:35 -07:00
|
|
|
let root = self.root().context("Could not get root for widget")?;
|
2020-12-26 02:05:34 -08:00
|
|
|
let window = root
|
|
|
|
|
.downcast_ref::<gtk::Window>()
|
|
|
|
|
.context("Could not downcast to GtkWindow")?;
|
|
|
|
|
operation.run(gtk::PrintOperationAction::PrintDialog, Some(window))?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-08 14:02:16 +01:00
|
|
|
pub fn copy(&self) -> anyhow::Result<()> {
|
|
|
|
|
let clipboard = self.clipboard();
|
|
|
|
|
|
2023-01-24 01:51:59 +01:00
|
|
|
let content_provider = self
|
|
|
|
|
.current_page()
|
|
|
|
|
.context("No current page")?
|
|
|
|
|
.content_provider();
|
|
|
|
|
clipboard.set_content(content_provider.as_ref())?;
|
2022-01-08 14:02:16 +01:00
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2020-12-26 02:05:34 -08:00
|
|
|
}
|