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-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();
|
|
|
|
|
image.set_rotation(0.);
|
|
|
|
|
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
|
|
|
}
|