2021-12-13 18:19:23 -08:00
|
|
|
//
|
|
|
|
|
// Copyright 2021 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::i18n::i18n;
|
|
|
|
|
use crate::image_metadata::{ImageMetadata, LpImageMetadata};
|
|
|
|
|
use crate::util;
|
2021-12-21 16:09:48 -08:00
|
|
|
|
2022-11-03 17:40:06 +01:00
|
|
|
use adw::{prelude::*, subclass::prelude::*};
|
2022-11-16 20:14:39 +01:00
|
|
|
use anyhow::Context;
|
2023-01-23 01:10:15 +01:00
|
|
|
use gtk_macros::spawn;
|
2021-12-13 18:19:23 -08:00
|
|
|
use once_cell::sync::Lazy;
|
2022-11-10 19:39:23 +01:00
|
|
|
use once_cell::unsync::OnceCell;
|
2021-12-13 18:19:23 -08:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
use std::cell::{Cell, RefCell};
|
|
|
|
|
use std::path::{Path, PathBuf};
|
2022-11-03 17:40:06 +01:00
|
|
|
|
2023-02-06 18:40:32 +01:00
|
|
|
/// Milliseconds
|
2022-11-28 03:23:06 +01:00
|
|
|
const ZOOM_ANIMATION_DURATION: u32 = 200;
|
2023-02-06 18:40:32 +01:00
|
|
|
/// Milliseconds
|
2022-11-28 03:23:06 +01:00
|
|
|
const ROTATION_ANIMATION_DURATION: u32 = 200;
|
2022-11-10 19:39:23 +01:00
|
|
|
|
2023-02-06 18:40:32 +01:00
|
|
|
/// Relative to current zoom level
|
2022-11-28 03:23:06 +01:00
|
|
|
const ZOOM_FACTOR_BUTTON: f64 = 1.5;
|
2023-02-06 18:40:32 +01:00
|
|
|
/// Relative to current zoom level
|
2022-11-28 03:23:06 +01:00
|
|
|
const ZOOM_FACTOR_WHEEL: f64 = 1.3;
|
2023-02-06 18:40:32 +01:00
|
|
|
/// Relative to current zoom level
|
|
|
|
|
const ZOOM_FACTOR_WHEEL_HI_RES: f64 = 0.001;
|
2022-11-28 03:23:06 +01:00
|
|
|
|
2023-02-06 18:40:32 +01:00
|
|
|
/// Relative to best-fit level
|
|
|
|
|
const ZOOM_FACTOR_DOUBLE_TAP: f64 = 2.5;
|
|
|
|
|
|
2023-02-07 21:56:00 +01:00
|
|
|
/// Relative to best-fit and `MAX_ZOOM_LEVEL`
|
|
|
|
|
const ZOOM_FACTOR_MAX_RUBBERBAND: f64 = 2.;
|
|
|
|
|
/// Smaller values make the band feel stiffer
|
|
|
|
|
const RUBBERBANDING_EXPONENT: f64 = 0.4;
|
|
|
|
|
|
2023-02-06 18:40:32 +01:00
|
|
|
/// Max zoom level 2000%
|
2022-11-28 03:23:06 +01:00
|
|
|
const MAX_ZOOM_LEVEL: f64 = 20.0;
|
2022-11-10 19:39:23 +01:00
|
|
|
|
2021-12-13 18:19:23 -08:00
|
|
|
mod imp {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Default)]
|
2021-12-21 10:55:02 -08:00
|
|
|
pub struct LpImage {
|
2021-12-13 18:19:23 -08:00
|
|
|
pub file: RefCell<Option<gio::File>>,
|
2023-01-23 01:10:15 +01:00
|
|
|
pub path: RefCell<Option<PathBuf>>,
|
|
|
|
|
pub is_deleted: Cell<bool>,
|
|
|
|
|
/// Track changes to this image
|
|
|
|
|
pub file_monitor: RefCell<Option<gio::FileMonitor>>,
|
2021-12-13 18:19:23 -08:00
|
|
|
pub texture: RefCell<Option<gdk::Texture>>,
|
2022-11-16 20:14:39 +01:00
|
|
|
/// Set when image is completely loaded and ready for displaying
|
|
|
|
|
pub is_loaded: Cell<bool>,
|
|
|
|
|
|
2022-11-03 17:40:06 +01:00
|
|
|
/// Rotation final value (can differ from `rotation` during animation)
|
|
|
|
|
pub rotation_target: Cell<f64>,
|
|
|
|
|
/// Rotated presentation of original image in degrees clockwise
|
|
|
|
|
pub rotation: Cell<f64>,
|
|
|
|
|
// Animates the `rotation` property
|
2022-11-10 19:39:23 +01:00
|
|
|
pub rotation_animation: OnceCell<adw::TimedAnimation>,
|
2022-11-03 17:40:06 +01:00
|
|
|
/// Mirrored presentation of original image
|
|
|
|
|
pub mirrored: Cell<bool>,
|
2022-11-10 19:39:23 +01:00
|
|
|
|
|
|
|
|
/// Displayed zoom level
|
|
|
|
|
pub zoom: Cell<f64>,
|
|
|
|
|
pub zoom_animation: OnceCell<adw::TimedAnimation>,
|
|
|
|
|
/// Targeted zoom level, might differ from `zoom` when animation is running
|
|
|
|
|
pub zoom_target: Cell<f64>,
|
|
|
|
|
/// Current animation is transitioning from having horizontal scrollbars
|
|
|
|
|
/// to not having them or vice versa.
|
|
|
|
|
pub zoom_hscrollbar_transition: Cell<bool>,
|
|
|
|
|
/// Same but vor vertical
|
|
|
|
|
pub zoom_vscrollbar_transition: Cell<bool>,
|
|
|
|
|
|
|
|
|
|
/// Always fit image into window, causes `zoom` to change automatically
|
|
|
|
|
pub best_fit: Cell<bool>,
|
2022-11-28 03:23:06 +01:00
|
|
|
/// Max zoom level is reached, stored to only send signals on change
|
|
|
|
|
pub max_zoom: Cell<bool>,
|
2022-11-10 19:39:23 +01:00
|
|
|
|
|
|
|
|
/// Horizontal scrolling
|
|
|
|
|
pub hadjustment: RefCell<Option<gtk::Adjustment>>,
|
|
|
|
|
/// Vertical scrolling
|
|
|
|
|
pub vadjustment: RefCell<Option<gtk::Adjustment>>,
|
|
|
|
|
|
2022-11-03 17:40:06 +01:00
|
|
|
/// Currently EXIF data
|
2022-11-06 01:08:43 +01:00
|
|
|
pub image_metadata: RefCell<LpImageMetadata>,
|
2022-11-16 20:14:39 +01:00
|
|
|
/// This will often be available earlier than the complete image
|
|
|
|
|
pub image_original_size: Cell<(i32, i32)>,
|
2022-11-10 19:39:23 +01:00
|
|
|
|
|
|
|
|
/// Current pointer position
|
|
|
|
|
pub pointer_position: Cell<Option<(f64, f64)>>,
|
|
|
|
|
|
|
|
|
|
/// Position of fingers during zoom gesture
|
|
|
|
|
///
|
|
|
|
|
/// Required for calculating delta when moving window on touchscreen.
|
|
|
|
|
/// On touchpad this is only the initial value used as the zoom target.
|
|
|
|
|
pub zoom_gesture_center: Cell<Option<(f64, f64)>>,
|
|
|
|
|
/// Required for calculating delta while moving window around
|
|
|
|
|
pub last_drag_value: Cell<Option<(f64, f64)>>,
|
|
|
|
|
|
|
|
|
|
widget_dimensions: Cell<(i32, i32)>,
|
2021-12-13 18:19:23 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[glib::object_subclass]
|
2021-12-21 10:55:02 -08:00
|
|
|
impl ObjectSubclass for LpImage {
|
|
|
|
|
const NAME: &'static str = "LpImage";
|
2021-12-13 18:19:23 -08:00
|
|
|
type ParentType = gtk::Widget;
|
2021-12-21 10:55:02 -08:00
|
|
|
type Type = super::LpImage;
|
2022-11-10 19:39:23 +01:00
|
|
|
type Interfaces = (gtk::Scrollable,);
|
2021-12-13 18:19:23 -08:00
|
|
|
}
|
|
|
|
|
|
2021-12-21 10:55:02 -08:00
|
|
|
impl ObjectImpl for LpImage {
|
2021-12-13 18:19:23 -08:00
|
|
|
fn properties() -> &'static [glib::ParamSpec] {
|
|
|
|
|
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
2022-11-03 17:40:06 +01:00
|
|
|
vec![
|
|
|
|
|
glib::ParamSpecObject::builder::<gio::File>("file")
|
|
|
|
|
.read_only()
|
|
|
|
|
.build(),
|
2023-01-23 01:10:15 +01:00
|
|
|
glib::ParamSpecVariant::builder("path", glib::VariantTy::BYTE_STRING)
|
|
|
|
|
.read_only()
|
|
|
|
|
.build(),
|
|
|
|
|
glib::ParamSpecBoolean::builder("is-deleted")
|
|
|
|
|
.read_only()
|
|
|
|
|
.build(),
|
2022-11-16 20:14:39 +01:00
|
|
|
glib::ParamSpecBoolean::builder("is-loaded")
|
|
|
|
|
.read_only()
|
|
|
|
|
.build(),
|
2022-11-06 01:08:43 +01:00
|
|
|
glib::ParamSpecObject::builder::<LpImageMetadata>("metadata")
|
|
|
|
|
.read_only()
|
|
|
|
|
.build(),
|
2022-11-10 19:39:23 +01:00
|
|
|
glib::ParamSpecDouble::builder("rotation")
|
|
|
|
|
.explicit_notify()
|
|
|
|
|
.build(),
|
|
|
|
|
glib::ParamSpecBoolean::builder("mirrored")
|
|
|
|
|
.explicit_notify()
|
|
|
|
|
.build(),
|
|
|
|
|
glib::ParamSpecDouble::builder("zoom")
|
|
|
|
|
.explicit_notify()
|
|
|
|
|
.build(),
|
|
|
|
|
glib::ParamSpecBoolean::builder("best-fit")
|
|
|
|
|
.explicit_notify()
|
|
|
|
|
.build(),
|
2022-11-28 03:23:06 +01:00
|
|
|
glib::ParamSpecBoolean::builder("is-max-zoom")
|
|
|
|
|
.read_only()
|
|
|
|
|
.build(),
|
2022-11-16 20:14:39 +01:00
|
|
|
glib::ParamSpecVariant::builder("image-size", glib::VariantTy::TUPLE)
|
|
|
|
|
.read_only()
|
|
|
|
|
.build(),
|
2022-11-10 19:39:23 +01:00
|
|
|
glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("hadjustment"),
|
|
|
|
|
glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("vadjustment"),
|
|
|
|
|
glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("hscroll-policy"),
|
|
|
|
|
glib::ParamSpecOverride::for_interface::<gtk::Scrollable>("vscroll-policy"),
|
2022-11-03 17:40:06 +01:00
|
|
|
]
|
2021-12-13 18:19:23 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
PROPERTIES.as_ref()
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-02 21:11:43 +01:00
|
|
|
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
2023-02-12 02:07:42 +01:00
|
|
|
let obj = self.obj();
|
2021-12-13 18:19:23 -08:00
|
|
|
match pspec.name() {
|
|
|
|
|
"file" => obj.file().to_value(),
|
2023-01-23 01:10:15 +01:00
|
|
|
"path" => obj.path().to_variant().to_value(),
|
|
|
|
|
"is-deleted" => obj.is_deleted().to_value(),
|
2022-11-16 20:14:39 +01:00
|
|
|
"is-loaded" => obj.is_loaded().to_value(),
|
2022-11-06 01:08:43 +01:00
|
|
|
"metadata" => obj.metadata().to_value(),
|
2022-11-03 17:40:06 +01:00
|
|
|
"rotation" => obj.rotation().to_value(),
|
|
|
|
|
"mirrored" => obj.mirrored().to_value(),
|
2022-11-10 19:39:23 +01:00
|
|
|
"zoom" => obj.zoom().to_value(),
|
|
|
|
|
"best-fit" => obj.is_best_fit().to_value(),
|
2022-11-28 03:23:06 +01:00
|
|
|
"is-max-zoom" => obj.is_max_zoom().to_value(),
|
2022-11-16 20:14:39 +01:00
|
|
|
"image-size" => obj.image_size().to_variant().to_value(),
|
2022-11-10 19:39:23 +01:00
|
|
|
// don't use getter functions here sicne they can return a fake adjustment
|
|
|
|
|
"hadjustment" => self.hadjustment.borrow().to_value(),
|
|
|
|
|
"vadjustment" => self.vadjustment.borrow().to_value(),
|
|
|
|
|
"hscroll-policy" | "vscroll-policy" => gtk::ScrollablePolicy::Minimum.to_value(),
|
|
|
|
|
name => unimplemented!("property {}", name),
|
2021-12-13 18:19:23 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-02 21:11:43 +01:00
|
|
|
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
|
2023-02-12 02:07:42 +01:00
|
|
|
let obj = self.obj();
|
2021-12-13 18:19:23 -08:00
|
|
|
match pspec.name() {
|
2022-11-03 17:40:06 +01:00
|
|
|
"rotation" => obj.set_rotation(value.get().unwrap()),
|
|
|
|
|
"mirrored" => obj.set_mirrored(value.get().unwrap()),
|
2022-11-10 19:39:23 +01:00
|
|
|
"zoom" => obj.set_zoom(value.get().unwrap()),
|
|
|
|
|
"best-fit" => obj.set_best_fit(value.get().unwrap()),
|
|
|
|
|
"hadjustment" => obj.set_hadjustment(value.get().unwrap()),
|
|
|
|
|
"vadjustment" => obj.set_vadjustment(value.get().unwrap()),
|
|
|
|
|
"hscroll-policy" | "vscroll-policy" => (),
|
2021-12-13 18:19:23 -08:00
|
|
|
_ => unimplemented!(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-02 21:11:43 +01:00
|
|
|
fn constructed(&self) {
|
2022-11-10 19:39:23 +01:00
|
|
|
self.parent_constructed();
|
|
|
|
|
|
2023-02-12 02:07:42 +01:00
|
|
|
let obj = self.obj();
|
2021-12-13 18:19:23 -08:00
|
|
|
obj.set_hexpand(true);
|
|
|
|
|
obj.set_vexpand(true);
|
2022-11-10 19:39:23 +01:00
|
|
|
obj.set_overflow(gtk::Overflow::Hidden);
|
2022-11-03 17:40:06 +01:00
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
self.zoom.set(1.);
|
|
|
|
|
self.zoom_target.set(1.);
|
|
|
|
|
self.best_fit.set(true);
|
|
|
|
|
|
|
|
|
|
self.connect_controllers();
|
|
|
|
|
self.connect_gestures();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn dispose(&self) {
|
2023-02-12 02:07:42 +01:00
|
|
|
let obj = self.obj();
|
2022-11-10 19:39:23 +01:00
|
|
|
|
|
|
|
|
// remove target fron zoom animation because it's property of this object
|
|
|
|
|
obj.rotation_animation()
|
|
|
|
|
.set_target(&adw::CallbackAnimationTarget::new(|_| {}));
|
|
|
|
|
obj.zoom_animation()
|
|
|
|
|
.set_target(&adw::CallbackAnimationTarget::new(|_| {}));
|
|
|
|
|
|
|
|
|
|
while let Some(child) = obj.first_child() {
|
|
|
|
|
child.unparent();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl LpImage {
|
|
|
|
|
fn connect_controllers(&self) {
|
2023-02-12 02:07:42 +01:00
|
|
|
let obj = self.obj();
|
2022-11-10 19:39:23 +01:00
|
|
|
|
|
|
|
|
// Needed vor having the current cursor position available
|
|
|
|
|
let motion_controller = gtk::EventControllerMotion::new();
|
|
|
|
|
motion_controller.connect_enter(glib::clone!(@weak obj => move |_, x, y| {
|
|
|
|
|
obj.imp().pointer_position.set(Some((x, y)));
|
|
|
|
|
}));
|
|
|
|
|
motion_controller.connect_motion(glib::clone!(@weak obj => move |_, x, y| {
|
|
|
|
|
obj.imp().pointer_position.set(Some((x, y)));
|
|
|
|
|
}));
|
|
|
|
|
motion_controller.connect_leave(glib::clone!(@weak obj => move |_| {
|
|
|
|
|
obj.imp().pointer_position.set(None);
|
|
|
|
|
}));
|
2023-02-12 02:07:42 +01:00
|
|
|
obj.add_controller(motion_controller);
|
2022-11-10 19:39:23 +01:00
|
|
|
|
|
|
|
|
// Zoom via scroll wheels etc
|
|
|
|
|
let scroll_controller =
|
|
|
|
|
gtk::EventControllerScroll::new(gtk::EventControllerScrollFlags::VERTICAL);
|
|
|
|
|
|
|
|
|
|
scroll_controller.connect_scroll(glib::clone!(@weak obj => @default-return gtk::Inhibit(false), move |event, _, y| {
|
|
|
|
|
// use Ctrl key as modifier for vertical scrolling
|
|
|
|
|
if event.current_event_state() == gdk::ModifierType::CONTROL_MASK
|
|
|
|
|
|| event.current_event_state() == gdk::ModifierType::SHIFT_MASK
|
|
|
|
|
{
|
|
|
|
|
// propagate event to scrolled window
|
|
|
|
|
return gtk::Inhibit(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// touchpads do zoom via gestures only
|
|
|
|
|
if event.current_event_device().map(|x| x.source())
|
|
|
|
|
== Some(gdk::InputSource::Touchpad)
|
|
|
|
|
{
|
|
|
|
|
// propagate event to scrolled window
|
|
|
|
|
return gtk::Inhibit(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let (zoom_factor, animated) = match event.unit() {
|
|
|
|
|
gdk::ScrollUnit::Wheel => (y.abs() * ZOOM_FACTOR_WHEEL, true),
|
|
|
|
|
// TODO: completely untested
|
|
|
|
|
gdk::ScrollUnit::Surface => (y.abs() * ZOOM_FACTOR_WHEEL_HI_RES + 1.0, false),
|
|
|
|
|
unknown_unit => {
|
|
|
|
|
log::warn!("Ignoring unknown scroll unit: {unknown_unit}");
|
|
|
|
|
(1., false)
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let zoom = if y > 0. {
|
|
|
|
|
obj.imp().zoom_target.get() / zoom_factor
|
|
|
|
|
} else {
|
|
|
|
|
obj.imp().zoom_target.get() * zoom_factor
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if zoom > obj.zoom_level_best_fit() {
|
|
|
|
|
obj.set_best_fit(false);
|
|
|
|
|
if animated {
|
|
|
|
|
obj.zoom_to(zoom);
|
|
|
|
|
} else {
|
|
|
|
|
obj.set_zoom(zoom);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
obj.set_best_fit(true);
|
|
|
|
|
if animated {
|
|
|
|
|
obj.zoom_to(obj.zoom_level_best_fit());
|
|
|
|
|
} else {
|
|
|
|
|
obj.set_zoom(obj.zoom_level_best_fit());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// do not propagate event to scrolled window
|
|
|
|
|
gtk::Inhibit(true)
|
|
|
|
|
}));
|
|
|
|
|
|
2023-02-12 02:07:42 +01:00
|
|
|
obj.add_controller(scroll_controller);
|
2022-11-10 19:39:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn connect_gestures(&self) {
|
2023-02-12 02:07:42 +01:00
|
|
|
let obj = self.obj();
|
2022-11-10 19:39:23 +01:00
|
|
|
|
2023-02-06 18:40:32 +01:00
|
|
|
// Double click for fullscreen (mouse/touchpad) or zoom (touch screen)
|
|
|
|
|
let left_click_gesture = gtk::GestureClick::builder().button(1).build();
|
2023-02-12 02:07:42 +01:00
|
|
|
obj.add_controller(left_click_gesture.clone());
|
2023-02-06 18:40:32 +01:00
|
|
|
left_click_gesture.connect_pressed(
|
|
|
|
|
glib::clone!(@weak obj => move |gesture, n_press, x, y| {
|
|
|
|
|
// only handle double clicks
|
|
|
|
|
if n_press != 2 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if gesture.device().map(|x| x.source()) == Some(gdk::InputSource::Touchscreen) {
|
|
|
|
|
// zoom
|
|
|
|
|
obj.imp().pointer_position.set(Some((x, y)));
|
|
|
|
|
if obj.is_best_fit() {
|
|
|
|
|
// zoom in
|
|
|
|
|
obj.zoom_to(ZOOM_FACTOR_DOUBLE_TAP * obj.zoom_level_best_fit());
|
|
|
|
|
} else {
|
|
|
|
|
// zoom back out
|
|
|
|
|
obj.zoom_best_fit();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// fullscreen
|
|
|
|
|
obj.activate_action("win.toggle-fullscreen", None).unwrap();
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
// Drag for moving image around
|
|
|
|
|
let drag_gesture = gtk::GestureDrag::new();
|
2023-02-12 02:07:42 +01:00
|
|
|
obj.add_controller(drag_gesture.clone());
|
2022-11-10 19:39:23 +01:00
|
|
|
|
|
|
|
|
drag_gesture.connect_drag_begin(glib::clone!(@weak obj => move |gesture, _, _| {
|
|
|
|
|
if obj.is_hscrollable() || obj.is_vscrollable() {
|
|
|
|
|
obj.set_cursor(gdk::Cursor::from_name("grabbing", None).as_ref());
|
|
|
|
|
obj.imp().last_drag_value.set(Some((0., 0.)));
|
|
|
|
|
} else {
|
|
|
|
|
// let drag and drop handle the events when not scrollable
|
|
|
|
|
gesture.set_state(gtk::EventSequenceState::Denied);
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
drag_gesture.connect_drag_update(glib::clone!(@weak obj => move |_, x1, y1| {
|
|
|
|
|
if let Some((x0, y0)) = obj.imp().last_drag_value.get() {
|
|
|
|
|
let hadjustment = obj.hadjustment();
|
|
|
|
|
let vadjustment = obj.vadjustment();
|
|
|
|
|
|
|
|
|
|
hadjustment.set_value(hadjustment.value() - x1 + x0);
|
|
|
|
|
vadjustment.set_value(vadjustment.value() - y1 + y0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
obj.imp().last_drag_value.set(Some((x1, y1)));
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
drag_gesture.connect_drag_end(glib::clone!(@weak obj => move |_, _, _| {
|
|
|
|
|
obj.set_cursor(None);
|
|
|
|
|
obj.imp().last_drag_value.set(None);
|
|
|
|
|
}));
|
2022-11-10 20:03:12 +01:00
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
// Rotate
|
2022-11-10 20:03:12 +01:00
|
|
|
let rotation_gesture = gtk::GestureRotate::new();
|
2023-02-12 02:07:42 +01:00
|
|
|
obj.add_controller(rotation_gesture.clone());
|
2022-11-10 20:03:12 +01:00
|
|
|
|
2023-02-07 23:14:55 +01:00
|
|
|
rotation_gesture.connect_angle_changed(
|
|
|
|
|
glib::clone!(@weak obj => move |gesture, _, _| {
|
|
|
|
|
let angle = gesture.angle_delta();
|
|
|
|
|
// offset for rotate gesture to take effect
|
|
|
|
|
if angle.abs().to_degrees() > 20. {
|
|
|
|
|
obj.set_rotation(obj.imp().rotation_target.get() + angle.to_degrees());
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
);
|
2022-11-10 20:03:12 +01:00
|
|
|
|
|
|
|
|
rotation_gesture.connect_end(glib::clone!(@weak obj => move |_, _| {
|
2022-11-10 19:39:23 +01:00
|
|
|
log::debug!("Rotate gesture ended");
|
|
|
|
|
|
2022-11-10 20:03:12 +01:00
|
|
|
let angle = (obj.rotation() / 90.).round() * 90. - obj.imp().rotation_target.get();
|
2022-11-10 19:39:23 +01:00
|
|
|
obj.rotate_by(angle);
|
2022-11-10 20:03:12 +01:00
|
|
|
}));
|
2022-01-19 23:11:38 -08:00
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
// Zoom
|
|
|
|
|
let zoom_gesture = gtk::GestureZoom::new();
|
2023-02-12 02:07:42 +01:00
|
|
|
obj.add_controller(zoom_gesture.clone());
|
2022-11-05 00:57:18 +01:00
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
zoom_gesture.connect_begin(glib::clone!(@weak obj => move |gesture, _| {
|
|
|
|
|
obj.imp()
|
|
|
|
|
.zoom_gesture_center
|
|
|
|
|
.set(gesture.bounding_box_center());
|
|
|
|
|
}));
|
2022-11-05 00:57:18 +01:00
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
zoom_gesture.connect_scale_changed(glib::clone!(@weak obj => move |gesture, scale| {
|
|
|
|
|
let hadjustment = obj.hadjustment();
|
|
|
|
|
let vadjustment = obj.vadjustment();
|
|
|
|
|
let zoom = obj.imp().zoom_target.get() * scale;
|
|
|
|
|
|
|
|
|
|
// move image with fingers on touchscreens
|
|
|
|
|
if gesture.device().map(|x| x.source()) == Some(gdk::InputSource::Touchscreen) {
|
|
|
|
|
if let p1 @ Some((x1, y1)) = gesture.bounding_box_center() {
|
|
|
|
|
if let Some((x0, y0)) = obj.imp().zoom_gesture_center.get() {
|
2023-02-12 01:00:38 +01:00
|
|
|
hadjustment.set_value(hadjustment.value() + x0 - x1);
|
|
|
|
|
vadjustment.set_value(vadjustment.value() + y0 - y1);
|
2022-11-10 19:39:23 +01:00
|
|
|
} else {
|
|
|
|
|
log::warn!("Zoom bounding box center: No previous value");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
obj.imp().zoom_gesture_center.set(p1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
obj.set_zoom_aiming(zoom, obj.imp().zoom_gesture_center.get());
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
zoom_gesture.connect_end(glib::clone!(@weak obj => move |_, _| {
|
|
|
|
|
log::debug!("Zoom gesture ended");
|
|
|
|
|
|
|
|
|
|
let rotation_target = (obj.rotation() / 90.).round() * 90.;
|
|
|
|
|
if obj.zoom() < obj.zoom_level_best_fit_for_rotation(rotation_target) {
|
|
|
|
|
obj.zoom_to(obj.zoom_level_best_fit_for_rotation(rotation_target));
|
|
|
|
|
} else {
|
2022-11-28 03:23:06 +01:00
|
|
|
// rubberband if over highest zoom level and sets `zoom_target`
|
|
|
|
|
obj.zoom_to(obj.zoom());
|
2022-11-10 19:39:23 +01:00
|
|
|
};
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
zoom_gesture.group_with(&rotation_gesture);
|
2022-01-19 23:11:38 -08:00
|
|
|
}
|
2021-12-13 18:19:23 -08:00
|
|
|
}
|
|
|
|
|
|
2021-12-21 10:55:02 -08:00
|
|
|
impl WidgetImpl for LpImage {
|
2022-11-10 19:39:23 +01:00
|
|
|
// called when the widget size might have changed
|
|
|
|
|
fn size_allocate(&self, width: i32, height: i32, _baseline: i32) {
|
2023-02-12 02:07:42 +01:00
|
|
|
let widget = self.obj();
|
2022-11-10 19:39:23 +01:00
|
|
|
|
|
|
|
|
// ensure there is an actual size change
|
|
|
|
|
if self.widget_dimensions.get() != (width, height) {
|
|
|
|
|
self.widget_dimensions.set((width, height));
|
|
|
|
|
// calculate new zoom value for best fit
|
|
|
|
|
if widget.is_best_fit() {
|
|
|
|
|
let best_fit_level = widget.zoom_level_best_fit();
|
|
|
|
|
self.zoom.set(best_fit_level);
|
|
|
|
|
self.zoom_target.set(best_fit_level);
|
2023-02-12 02:07:42 +01:00
|
|
|
self.obj().zoom_animation().pause();
|
2022-11-10 19:39:23 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
widget.configure_adjustments();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// called when the widget content should be re-rendered
|
2022-11-02 21:11:43 +01:00
|
|
|
fn snapshot(&self, snapshot: >k::Snapshot) {
|
2021-12-13 18:19:23 -08:00
|
|
|
if let Some(texture) = self.texture.borrow().as_ref() {
|
2023-02-12 02:07:42 +01:00
|
|
|
let widget = self.obj();
|
2022-11-03 17:40:06 +01:00
|
|
|
let widget_width = widget.width() as f64;
|
|
|
|
|
let widget_height = widget.height() as f64;
|
2021-12-13 18:19:23 -08:00
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
// make sure the scrollbars are correct
|
|
|
|
|
widget.configure_adjustments();
|
2022-11-03 17:40:06 +01:00
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
snapshot.save();
|
2022-11-03 17:40:06 +01:00
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
// apply the scrolling position to the image
|
|
|
|
|
if let Some(adj) = self.hadjustment.borrow().as_ref() {
|
|
|
|
|
let x = -(adj.value() - (adj.upper() - widget_width) / 2.);
|
|
|
|
|
snapshot.translate(&graphene::Point::new(x as f32, 0.));
|
|
|
|
|
}
|
|
|
|
|
if let Some(adj) = self.vadjustment.borrow().as_ref() {
|
|
|
|
|
let y = -(adj.value() - (adj.upper() - widget_height) / 2.);
|
|
|
|
|
snapshot.translate(&graphene::Point::new(0., y as f32));
|
|
|
|
|
}
|
2022-11-03 17:40:06 +01:00
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
// Center image in coordinates.
|
|
|
|
|
// Needed for rotating around the center of the image, and
|
|
|
|
|
// mirroring the image does not put it to a completely different position.
|
|
|
|
|
snapshot.translate(&graphene::Point::new(
|
|
|
|
|
(widget_width / 2.) as f32,
|
|
|
|
|
(widget_height / 2.) as f32,
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
// vertical centering in widget when no scrolling
|
|
|
|
|
let x = f64::max((widget_width - widget.image_displayed_width()) / 2.0, 0.);
|
|
|
|
|
let y = f64::max((widget_height - widget.image_displayed_height()) / 2.0, 0.);
|
2023-01-30 01:53:13 +01:00
|
|
|
// round to pixel values to not have a half pixel offset to physical pixels
|
|
|
|
|
// the offset would leading to a blury output
|
|
|
|
|
snapshot.translate(&graphene::Point::new(
|
|
|
|
|
widget.round(x) as f32,
|
|
|
|
|
widget.round(y) as f32,
|
|
|
|
|
));
|
2022-11-10 19:39:23 +01:00
|
|
|
|
|
|
|
|
// apply the transformations from properties
|
|
|
|
|
snapshot.rotate(widget.rotation() as f32);
|
|
|
|
|
if widget.mirrored() {
|
|
|
|
|
snapshot.scale(-1., 1.);
|
2021-12-13 18:19:23 -08:00
|
|
|
}
|
2022-11-10 19:39:23 +01:00
|
|
|
snapshot.scale(widget.zoom() as f32, widget.zoom() as f32);
|
|
|
|
|
|
|
|
|
|
// scale to actual pixel size
|
|
|
|
|
// this is needed since usually the texture would just fill the widget
|
|
|
|
|
snapshot.scale(
|
|
|
|
|
(texture.width() as f64 / widget_width) as f32,
|
|
|
|
|
(texture.height() as f64 / widget_height) as f32,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// move back to original position
|
|
|
|
|
snapshot.translate(&graphene::Point::new(
|
|
|
|
|
(-widget_width / 2.) as f32,
|
|
|
|
|
(-widget_height / 2.) as f32,
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
texture.snapshot(snapshot, widget_width, widget_height);
|
|
|
|
|
snapshot.restore();
|
2021-12-13 18:19:23 -08:00
|
|
|
}
|
|
|
|
|
}
|
2022-11-16 20:14:39 +01:00
|
|
|
|
|
|
|
|
fn measure(&self, orientation: gtk::Orientation, _for_size: i32) -> (i32, i32, i32, i32) {
|
2023-02-12 02:07:42 +01:00
|
|
|
let (image_width, image_height) = self.obj().image_size();
|
2022-11-16 20:14:39 +01:00
|
|
|
|
|
|
|
|
if image_width > 0 && image_height > 0 {
|
|
|
|
|
if let Some(display) = gdk::Display::default() {
|
2023-02-12 02:07:42 +01:00
|
|
|
if let Some(native) = self.obj().native() {
|
|
|
|
|
let hidpi_scale = self.obj().scale_factor() as f64;
|
2022-11-16 20:14:39 +01:00
|
|
|
|
|
|
|
|
// get monitor information
|
2023-02-12 02:07:42 +01:00
|
|
|
// TODO: Get rid of unwrap
|
|
|
|
|
let monitor = display.monitor_at_surface(&native.surface()).unwrap();
|
2022-11-16 20:14:39 +01:00
|
|
|
let monitor_geometry = monitor.geometry();
|
|
|
|
|
// TODO: Per documentation those dimensions should not be physical pixels.
|
|
|
|
|
// But on Wayland they are physiscal pixels and on X11 not.
|
|
|
|
|
// Taking the version that works on Wayland for now.
|
|
|
|
|
// <https://gitlab.gnome.org/GNOME/gtk/-/issues/5391>
|
|
|
|
|
let monitor_width = monitor_geometry.width() as f64 - 40.;
|
|
|
|
|
let monitor_height = monitor_geometry.height() as f64 - 60.;
|
|
|
|
|
|
|
|
|
|
// areas
|
|
|
|
|
let monitor_area = monitor_width * monitor_height;
|
|
|
|
|
let image_area = image_width as f64 * image_height as f64;
|
|
|
|
|
|
|
|
|
|
let ocuppy_area_factor = if monitor_area < 1024. * 768. {
|
|
|
|
|
// for small monitors occupy 80% of the area
|
|
|
|
|
0.8
|
|
|
|
|
} else {
|
|
|
|
|
// for large monitors occupy 30% of the area
|
|
|
|
|
0.3
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// factor for width and height that will achive the desired area occupation
|
|
|
|
|
// derived from:
|
|
|
|
|
// monitor_area * ocuppy_area_factor ==
|
|
|
|
|
// (image_width * size_scale) * (image_height * size_scale)
|
|
|
|
|
let size_scale = f64::sqrt(monitor_area / image_area * ocuppy_area_factor);
|
|
|
|
|
// ensure that we never increase image size
|
|
|
|
|
let target_scale = f64::min(1.0, size_scale);
|
|
|
|
|
let mut nat_width = image_width as f64 * target_scale;
|
|
|
|
|
let mut nat_height = image_height as f64 * target_scale;
|
|
|
|
|
|
|
|
|
|
// scale down if targeted occupation does not fit in one direction
|
|
|
|
|
if nat_width > monitor_width {
|
|
|
|
|
nat_width = monitor_width;
|
|
|
|
|
nat_height = nat_height * monitor_width / nat_width;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// same for other direction
|
|
|
|
|
if nat_height > monitor_height {
|
|
|
|
|
nat_height = monitor_height;
|
|
|
|
|
nat_width = nat_width * monitor_height / nat_height;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let size = match orientation {
|
|
|
|
|
gtk::Orientation::Horizontal => (nat_width / hidpi_scale).round(),
|
|
|
|
|
gtk::Orientation::Vertical => (nat_height / hidpi_scale).round(),
|
|
|
|
|
_ => unreachable!(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (0, size as i32, -1, -1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// fallback if monitor size or image size is not known:
|
|
|
|
|
// use original image size and hope for the best
|
|
|
|
|
// TODO: We could use a small default monitor size estimate instead
|
|
|
|
|
let size = match orientation {
|
|
|
|
|
gtk::Orientation::Horizontal => image_width,
|
|
|
|
|
gtk::Orientation::Vertical => image_height,
|
|
|
|
|
_ => unreachable!(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
(0, size, -1, -1)
|
|
|
|
|
}
|
2021-12-13 18:19:23 -08:00
|
|
|
}
|
2022-11-10 19:39:23 +01:00
|
|
|
|
|
|
|
|
impl ScrollableImpl for LpImage {}
|
2021-12-13 18:19:23 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
glib::wrapper! {
|
2022-11-10 19:39:23 +01:00
|
|
|
pub struct LpImage(ObjectSubclass<imp::LpImage>)
|
|
|
|
|
@extends gtk::Widget,
|
|
|
|
|
@implements gtk::Scrollable;
|
2021-12-13 18:19:23 -08:00
|
|
|
}
|
|
|
|
|
|
2021-12-21 10:55:02 -08:00
|
|
|
impl LpImage {
|
2023-01-23 01:10:15 +01:00
|
|
|
pub async fn load(&self, path: &Path) -> anyhow::Result<()> {
|
2021-12-29 14:02:48 -08:00
|
|
|
let imp = self.imp();
|
2023-01-23 01:10:15 +01:00
|
|
|
let path = path.to_path_buf();
|
|
|
|
|
let file = gio::File::for_path(&path);
|
|
|
|
|
self.set_file(&file);
|
2021-12-13 18:19:23 -08:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
if !path.is_file() {
|
|
|
|
|
return Err(anyhow::Error::msg(i18n("File does not exist")));
|
|
|
|
|
}
|
2022-11-03 17:40:06 +01:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
log::debug!("Loading file {path:?}");
|
2022-11-16 20:14:39 +01:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
log::debug!("Loading EXIF");
|
|
|
|
|
let metadata = util::spawn(
|
|
|
|
|
"image-load-exif",
|
|
|
|
|
glib::clone!(@strong path => move || ImageMetadata::load(&path)),
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
|
|
|
|
let orientation = metadata.orientation();
|
2022-11-16 20:14:39 +01:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
imp.image_metadata.replace(LpImageMetadata::from(metadata));
|
|
|
|
|
imp.rotation_target.set(-orientation.rotation);
|
|
|
|
|
imp.rotation.set(-orientation.rotation);
|
|
|
|
|
imp.mirrored.set(orientation.mirrored);
|
2022-11-16 20:14:39 +01:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
self.notify("metadata");
|
2022-11-06 01:08:43 +01:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
log::debug!("Loading image size");
|
|
|
|
|
let (_, x, y) = gdk_pixbuf::Pixbuf::file_info_future(path.clone())
|
|
|
|
|
.await?
|
|
|
|
|
.context(i18n("Failed to load image size"))?;
|
|
|
|
|
imp.image_original_size.set((x, y));
|
|
|
|
|
self.notify("image-size");
|
2022-11-16 20:14:39 +01:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
log::debug!("Loading complete image");
|
|
|
|
|
let texture = util::spawn(
|
2022-11-16 20:14:39 +01:00
|
|
|
"image-load-image",
|
2023-01-22 13:52:50 -05:00
|
|
|
glib::clone!(@weak file => @default-return Err(anyhow::Error::msg(i18n("File does not exist"))), move || {
|
2022-11-16 20:14:39 +01:00
|
|
|
log::debug!("Creating texture");
|
2023-01-22 13:52:50 -05:00
|
|
|
let texture = gdk::Texture::from_file(&file)?;
|
|
|
|
|
Ok::<_,anyhow::Error>(texture)
|
2022-11-16 20:14:39 +01:00
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
.await?
|
2023-01-22 13:52:50 -05:00
|
|
|
.context(i18n("Failed to load image"))?;
|
2022-11-16 20:14:39 +01:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
imp.texture.replace(Some(texture));
|
2022-11-16 20:14:39 +01:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
self.configure_adjustments();
|
2022-11-16 20:14:39 +01:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
self.queue_draw();
|
|
|
|
|
self.queue_allocate();
|
|
|
|
|
self.queue_resize();
|
2022-11-03 17:40:06 +01:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
log::debug!("Image loaded and scheduled for drawing");
|
|
|
|
|
imp.is_loaded.set(true);
|
|
|
|
|
self.notify("is-loaded");
|
2021-12-13 18:19:23 -08:00
|
|
|
|
2022-11-16 20:14:39 +01:00
|
|
|
Ok(())
|
|
|
|
|
}
|
2022-11-10 19:39:23 +01:00
|
|
|
|
2022-11-16 20:14:39 +01:00
|
|
|
pub fn is_loaded(&self) -> bool {
|
|
|
|
|
self.imp().is_loaded.get()
|
2022-11-10 19:39:23 +01:00
|
|
|
}
|
|
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
pub fn is_deleted(&self) -> bool {
|
|
|
|
|
self.imp().is_deleted.get()
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
/// Zoom level that makes the image fit in widget
|
|
|
|
|
///
|
|
|
|
|
/// During image rotation the image does not actually fit into widget.
|
|
|
|
|
/// Instead the level is interpolated between zoom levels
|
|
|
|
|
fn zoom_level_best_fit(&self) -> f64 {
|
|
|
|
|
self.zoom_level_best_fit_for_rotation(self.rotation())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Same, but not for current rotation target
|
|
|
|
|
///
|
|
|
|
|
/// Used for calculating the required zoom level after rotation
|
|
|
|
|
fn zoom_level_best_fit_for_rotation(&self, rotation: f64) -> f64 {
|
|
|
|
|
if let Some(texture) = self.texture() {
|
|
|
|
|
let rotated = rotation.to_radians().sin().abs();
|
|
|
|
|
let texture_aspect_ratio = texture.width() as f64 / texture.height() as f64;
|
|
|
|
|
let widget_aspect_ratio = self.width() as f64 / self.height() as f64;
|
|
|
|
|
|
|
|
|
|
let default_zoom = if texture_aspect_ratio > widget_aspect_ratio {
|
|
|
|
|
(self.width() as f64 / texture.width() as f64).min(1.)
|
|
|
|
|
} else {
|
|
|
|
|
(self.height() as f64 / texture.height() as f64).min(1.)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let rotated_zoom = if 1. / texture_aspect_ratio > widget_aspect_ratio {
|
|
|
|
|
(self.width() as f64 / texture.height() as f64).min(1.)
|
|
|
|
|
} else {
|
|
|
|
|
(self.height() as f64 / texture.width() as f64).min(1.)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
rotated * rotated_zoom + (1. - rotated) * default_zoom
|
|
|
|
|
} else {
|
|
|
|
|
1.
|
|
|
|
|
}
|
2021-12-13 18:19:23 -08:00
|
|
|
}
|
|
|
|
|
|
2022-11-04 20:12:44 +01:00
|
|
|
pub fn file(&self) -> Option<gio::File> {
|
2023-01-23 01:10:15 +01:00
|
|
|
self.imp().file.borrow().clone()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn path(&self) -> Option<PathBuf> {
|
|
|
|
|
self.imp().path.borrow().clone()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) fn set_file(&self, file: &gio::File) {
|
2022-11-04 20:12:44 +01:00
|
|
|
let imp = self.imp();
|
|
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
imp.file.replace(Some(file.clone()));
|
|
|
|
|
imp.path.replace(file.path());
|
|
|
|
|
self.notify("path");
|
|
|
|
|
|
|
|
|
|
let monitor = file.monitor_file(gio::FileMonitorFlags::WATCH_MOVES, gio::Cancellable::NONE);
|
|
|
|
|
if let Ok(m) = &monitor {
|
|
|
|
|
m.connect_changed(
|
|
|
|
|
glib::clone!(@weak self as obj => move |_, file_a, file_b, event| {
|
|
|
|
|
obj.file_changed(event, file_a, file_b);
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
imp.file_monitor.replace(monitor.ok());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// File changed on drive
|
|
|
|
|
fn file_changed(
|
|
|
|
|
&self,
|
|
|
|
|
event: gio::FileMonitorEvent,
|
|
|
|
|
file_a: &gio::File,
|
|
|
|
|
file_b: Option<&gio::File>,
|
|
|
|
|
) {
|
|
|
|
|
match event {
|
|
|
|
|
gio::FileMonitorEvent::Renamed => {
|
|
|
|
|
if let Some(file) = file_b {
|
|
|
|
|
log::debug!("Moved to {:?}", file.path());
|
|
|
|
|
self.set_file(file);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
gio::FileMonitorEvent::ChangesDoneHint => {
|
|
|
|
|
let obj = self.clone();
|
|
|
|
|
let file = file_a.clone();
|
|
|
|
|
// TODO: error handling is missing
|
|
|
|
|
spawn! {async move { let _ = obj.load(&file.path().unwrap()).await; }};
|
|
|
|
|
}
|
|
|
|
|
gio::FileMonitorEvent::Deleted
|
|
|
|
|
| gio::FileMonitorEvent::MovedOut
|
|
|
|
|
| gio::FileMonitorEvent::Unmounted => {
|
|
|
|
|
self.imp().is_deleted.set(true);
|
|
|
|
|
self.notify("is-deleted");
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
2022-11-04 20:12:44 +01:00
|
|
|
}
|
|
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
/// Texture that contains the original image
|
2022-11-03 17:40:06 +01:00
|
|
|
pub fn texture(&self) -> Option<gdk::Texture> {
|
2021-12-29 14:02:48 -08:00
|
|
|
let imp = self.imp();
|
2022-11-03 17:40:06 +01:00
|
|
|
imp.texture.borrow().clone()
|
2021-12-13 18:19:23 -08:00
|
|
|
}
|
|
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
fn mirrored(&self) -> bool {
|
|
|
|
|
self.imp().mirrored.get()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn set_mirrored(&self, mirrored: bool) {
|
|
|
|
|
if mirrored == self.mirrored() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.imp().mirrored.set(mirrored);
|
|
|
|
|
self.notify("mirrored");
|
|
|
|
|
self.queue_draw();
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-03 17:40:06 +01:00
|
|
|
pub fn rotation(&self) -> f64 {
|
|
|
|
|
self.imp().rotation.get()
|
2021-12-13 18:19:23 -08:00
|
|
|
}
|
2022-01-08 14:02:16 +01:00
|
|
|
|
2022-11-03 17:40:06 +01:00
|
|
|
pub fn set_rotation(&self, rotation: f64) {
|
2022-11-10 19:39:23 +01:00
|
|
|
if rotation == self.rotation() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-03 17:40:06 +01:00
|
|
|
self.imp().rotation.set(rotation);
|
2022-11-10 19:39:23 +01:00
|
|
|
self.notify("rotation");
|
2022-01-19 23:11:38 -08:00
|
|
|
self.queue_draw();
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
pub fn rotate_by(&self, angle: f64) {
|
|
|
|
|
log::debug!("Rotate by {} degrees", angle);
|
|
|
|
|
let target = &self.imp().rotation_target;
|
|
|
|
|
target.set(target.get() + angle);
|
|
|
|
|
|
|
|
|
|
let animation = self.rotation_animation();
|
|
|
|
|
|
|
|
|
|
animation.set_value_from(self.rotation());
|
|
|
|
|
animation.set_value_to(target.get());
|
|
|
|
|
animation.play();
|
|
|
|
|
|
|
|
|
|
if self.is_best_fit() {
|
|
|
|
|
let animation = self.zoom_animation();
|
|
|
|
|
|
|
|
|
|
animation.set_value_from(self.zoom());
|
|
|
|
|
animation.set_value_to(self.zoom_level_best_fit_for_rotation(target.get()));
|
|
|
|
|
animation.play();
|
|
|
|
|
}
|
2022-11-03 17:40:06 +01:00
|
|
|
}
|
|
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
fn rotation_animation(&self) -> &adw::TimedAnimation {
|
|
|
|
|
self.imp().rotation_animation.get_or_init(|| {
|
|
|
|
|
adw::TimedAnimation::builder()
|
|
|
|
|
.duration(ROTATION_ANIMATION_DURATION)
|
|
|
|
|
.widget(self)
|
|
|
|
|
.target(&adw::PropertyAnimationTarget::new(self, "rotation"))
|
|
|
|
|
.build()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn is_best_fit(&self) -> bool {
|
|
|
|
|
self.imp().best_fit.get()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn set_best_fit(&self, best_fit: bool) {
|
|
|
|
|
if best_fit == self.is_best_fit() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.imp().best_fit.set(best_fit);
|
|
|
|
|
self.notify("best-fit");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Current zoom level
|
|
|
|
|
pub fn zoom(&self) -> f64 {
|
|
|
|
|
self.imp().zoom.get()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Set zoom level aiming for cursor position or center if not available
|
|
|
|
|
///
|
|
|
|
|
/// Aiming means that the scrollbars are adjust such that the same point
|
|
|
|
|
/// of the image remains under the cursor after changing the zoom level.
|
|
|
|
|
fn set_zoom(&self, zoom: f64) {
|
|
|
|
|
self.set_zoom_aiming(zoom, self.imp().pointer_position.get())
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-28 03:23:06 +01:00
|
|
|
pub fn is_max_zoom(&self) -> bool {
|
|
|
|
|
self.imp().max_zoom.get()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn set_max_zoom(&self, value: bool) {
|
|
|
|
|
if self.is_max_zoom() == value {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.imp().max_zoom.set(value);
|
|
|
|
|
self.notify("is-max-zoom");
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
/// Set zoom level aiming for given position or center if not available
|
2023-02-07 21:56:00 +01:00
|
|
|
fn set_zoom_aiming(&self, mut zoom: f64, aiming: Option<(f64, f64)>) {
|
|
|
|
|
// allow some deviantion from max value for rubberbanding
|
|
|
|
|
if zoom > MAX_ZOOM_LEVEL {
|
|
|
|
|
let max_deviation = MAX_ZOOM_LEVEL * ZOOM_FACTOR_MAX_RUBBERBAND;
|
|
|
|
|
let deviation = zoom / MAX_ZOOM_LEVEL;
|
|
|
|
|
zoom = f64::min(
|
|
|
|
|
MAX_ZOOM_LEVEL * deviation.powf(RUBBERBANDING_EXPONENT),
|
|
|
|
|
max_deviation,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if zoom < self.zoom_level_best_fit() {
|
|
|
|
|
let minimum = self.zoom_level_best_fit();
|
|
|
|
|
let max_deviation = minimum / ZOOM_FACTOR_MAX_RUBBERBAND;
|
|
|
|
|
let deviation = zoom / minimum;
|
|
|
|
|
zoom = f64::max(
|
|
|
|
|
minimum * deviation.powf(RUBBERBANDING_EXPONENT),
|
|
|
|
|
max_deviation,
|
|
|
|
|
);
|
|
|
|
|
dbg!(zoom);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if zoom == self.zoom() {
|
2022-11-10 19:39:23 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let zoom_ratio = self.imp().zoom.get() / zoom;
|
|
|
|
|
|
|
|
|
|
self.imp().zoom.set(zoom);
|
|
|
|
|
|
|
|
|
|
self.configure_adjustments();
|
|
|
|
|
|
|
|
|
|
let center_x = self.widget_width() / 2.;
|
|
|
|
|
let center_y = self.widget_height() / 2.;
|
|
|
|
|
|
|
|
|
|
let (x, y) = aiming.unwrap_or((center_x, center_y));
|
|
|
|
|
|
|
|
|
|
if self.imp().zoom_hscrollbar_transition.get() {
|
|
|
|
|
if zoom_ratio < 1. {
|
|
|
|
|
self.hadjustment()
|
|
|
|
|
.set_value(self.max_hadjustment_value() / 2.);
|
|
|
|
|
} else {
|
|
|
|
|
// move towards center
|
|
|
|
|
self.hadjustment()
|
|
|
|
|
.set_value(self.hadjustment_corrected_for_zoom(zoom_ratio, center_x));
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
self.hadjustment()
|
|
|
|
|
.set_value(self.hadjustment_corrected_for_zoom(zoom_ratio, x));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.imp().zoom_vscrollbar_transition.get() {
|
|
|
|
|
if zoom_ratio < 1. {
|
|
|
|
|
self.vadjustment()
|
|
|
|
|
.set_value(self.max_vadjustment_value() / 2.);
|
|
|
|
|
} else {
|
|
|
|
|
// move towards center
|
|
|
|
|
self.vadjustment()
|
|
|
|
|
.set_value(self.vadjustment_corrected_for_zoom(zoom_ratio, center_y));
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
self.vadjustment()
|
|
|
|
|
.set_value(self.vadjustment_corrected_for_zoom(zoom_ratio, y));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.notify("zoom");
|
2022-11-03 17:40:06 +01:00
|
|
|
self.queue_draw();
|
2022-01-08 14:02:16 +01:00
|
|
|
}
|
2022-01-08 16:21:27 +01:00
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
/// Animation that makes larger zoom steps (from buttons etc) look smooth
|
|
|
|
|
fn zoom_animation(&self) -> &adw::TimedAnimation {
|
|
|
|
|
self.imp().zoom_animation.get_or_init(|| {
|
|
|
|
|
let animation = adw::TimedAnimation::builder()
|
|
|
|
|
.duration(ZOOM_ANIMATION_DURATION)
|
|
|
|
|
.widget(self)
|
|
|
|
|
.target(&adw::PropertyAnimationTarget::new(self, "zoom"))
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
animation.connect_done(glib::clone!(@weak self as obj => move |_| {
|
|
|
|
|
obj.imp().zoom_hscrollbar_transition.set(false);
|
|
|
|
|
obj.imp().zoom_vscrollbar_transition.set(false);
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
animation
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Required scrollbar change to keep aiming
|
|
|
|
|
///
|
|
|
|
|
/// When zooming by a ratio of `zoom_delta` and wanting to keep position `x`
|
|
|
|
|
/// in the image at the same place in the widget, the returned value is
|
|
|
|
|
/// the correct value for hadjustment to achive that.
|
|
|
|
|
pub fn hadjustment_corrected_for_zoom(&self, zoom_delta: f64, x: f64) -> f64 {
|
|
|
|
|
let adj = self.hadjustment();
|
|
|
|
|
// Width of bars to the left and right of the image
|
|
|
|
|
let border = if self.widget_width() > self.image_displayed_width() {
|
|
|
|
|
(self.widget_width() - self.image_displayed_width()) / 2.
|
|
|
|
|
} else {
|
|
|
|
|
0.
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
f64::max((x + adj.value() - border) / zoom_delta - x, 0.)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Same but for vertical adjustment
|
|
|
|
|
pub fn vadjustment_corrected_for_zoom(&self, zoom_delta: f64, y: f64) -> f64 {
|
|
|
|
|
let adj = self.vadjustment();
|
|
|
|
|
// Width of bars to the top and bottom of the image
|
|
|
|
|
let border = if self.widget_height() > self.image_displayed_height() {
|
|
|
|
|
(self.widget_height() - self.image_displayed_height()) / 2.
|
|
|
|
|
} else {
|
|
|
|
|
0.
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
f64::max((y + adj.value() - border) / zoom_delta - y, 0.)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Zoom in a step with animation
|
|
|
|
|
///
|
|
|
|
|
/// Used by buttons
|
|
|
|
|
pub fn zoom_in(&self) {
|
2022-11-29 00:04:13 +01:00
|
|
|
let zoom = self.imp().zoom_target.get() * ZOOM_FACTOR_BUTTON;
|
2022-11-10 19:39:23 +01:00
|
|
|
|
|
|
|
|
self.zoom_to(zoom);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Zoom out a step with animation
|
|
|
|
|
///
|
|
|
|
|
/// Used by buttons
|
|
|
|
|
pub fn zoom_out(&self) {
|
2022-11-29 00:04:13 +01:00
|
|
|
let zoom = self.imp().zoom_target.get() / ZOOM_FACTOR_BUTTON;
|
2022-11-10 19:39:23 +01:00
|
|
|
|
|
|
|
|
self.zoom_to(zoom);
|
2022-11-23 19:12:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Zoom to best fit
|
|
|
|
|
///
|
|
|
|
|
/// Used by shortcut
|
|
|
|
|
pub fn zoom_best_fit(&self) {
|
|
|
|
|
self.zoom_to(self.zoom_level_best_fit());
|
2022-11-10 19:39:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Zoom to specific level with animation
|
2022-11-28 03:23:06 +01:00
|
|
|
pub fn zoom_to(&self, mut zoom: f64) {
|
|
|
|
|
if zoom >= MAX_ZOOM_LEVEL {
|
|
|
|
|
zoom = MAX_ZOOM_LEVEL;
|
|
|
|
|
self.set_max_zoom(true);
|
|
|
|
|
} else {
|
|
|
|
|
self.set_max_zoom(false);
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-28 23:43:56 +01:00
|
|
|
// If image is only 1/4 of a zoom step away from best-fit, also
|
|
|
|
|
// activate best-fit. This avoids bugs with floating point precision
|
|
|
|
|
// and removes akward minimal zoom steps.
|
|
|
|
|
let extended_best_fit_threshold =
|
|
|
|
|
self.zoom_level_best_fit() * (1. + (ZOOM_FACTOR_BUTTON - 1.) / 4.);
|
|
|
|
|
|
|
|
|
|
if zoom <= extended_best_fit_threshold {
|
|
|
|
|
zoom = self.zoom_level_best_fit();
|
|
|
|
|
self.set_best_fit(true);
|
|
|
|
|
} else {
|
|
|
|
|
self.set_best_fit(false);
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
log::debug!("Zoom to {zoom:.3}");
|
|
|
|
|
|
2022-11-28 03:23:06 +01:00
|
|
|
self.imp().zoom_target.set(zoom);
|
|
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
// abort if already at correct zoom level
|
2022-11-28 03:23:06 +01:00
|
|
|
if zoom == self.zoom() {
|
2022-11-10 19:39:23 +01:00
|
|
|
log::debug!("Already at correct zoom level");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(texture) = self.texture() {
|
|
|
|
|
let current_hborder = self.widget_width() - self.image_displayed_width();
|
|
|
|
|
let target_hborder = self.widget_width() - texture.width() as f64 * zoom;
|
|
|
|
|
|
|
|
|
|
self.imp()
|
|
|
|
|
.zoom_hscrollbar_transition
|
|
|
|
|
.set(current_hborder.signum() != target_hborder.signum() && current_hborder != 0.);
|
|
|
|
|
|
|
|
|
|
let current_vborder = self.widget_height() - self.image_displayed_height();
|
|
|
|
|
let target_vborder = self.widget_height() - texture.height() as f64 * zoom;
|
|
|
|
|
|
|
|
|
|
self.imp()
|
|
|
|
|
.zoom_hscrollbar_transition
|
|
|
|
|
.set(current_vborder.signum() != target_vborder.signum() && current_vborder != 0.);
|
|
|
|
|
|
|
|
|
|
let animation = self.zoom_animation();
|
|
|
|
|
|
|
|
|
|
animation.set_value_from(self.zoom());
|
|
|
|
|
animation.set_value_to(zoom);
|
|
|
|
|
animation.play();
|
|
|
|
|
} else {
|
|
|
|
|
log::error!("No texture to zoom");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-16 20:14:39 +01:00
|
|
|
/// Image size of original image with EXIF rotation applied
|
|
|
|
|
pub fn image_size(&self) -> (i32, i32) {
|
|
|
|
|
let orientation = self.imp().image_metadata.borrow().orientation();
|
|
|
|
|
if orientation.rotation.abs() == 90. {
|
|
|
|
|
let (x, y) = self.imp().image_original_size.get();
|
|
|
|
|
(y, x)
|
|
|
|
|
} else {
|
|
|
|
|
self.imp().image_original_size.get()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
/// Image width with current zoom factor and rotation
|
|
|
|
|
///
|
|
|
|
|
/// During rotation it is an interpolated size that does not
|
|
|
|
|
/// represent the actual size. The size returned might well be
|
|
|
|
|
/// larger than what can actually be displayed within the widget.
|
|
|
|
|
pub fn image_displayed_width(&self) -> f64 {
|
|
|
|
|
if let Some(texture) = self.texture() {
|
|
|
|
|
let rotated = self.rotation().to_radians().sin().abs();
|
|
|
|
|
|
|
|
|
|
((1. - rotated) * texture.width() as f64 + rotated * texture.height() as f64)
|
|
|
|
|
* self.zoom()
|
|
|
|
|
} else {
|
|
|
|
|
0.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn image_displayed_height(&self) -> f64 {
|
|
|
|
|
if let Some(texture) = self.texture() {
|
|
|
|
|
let rotated = self.rotation().to_radians().sin().abs();
|
|
|
|
|
|
|
|
|
|
((1. - rotated) * texture.height() as f64 + rotated * texture.width() as f64)
|
|
|
|
|
* self.zoom()
|
|
|
|
|
} else {
|
|
|
|
|
0.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-30 19:07:17 +01:00
|
|
|
/// Stepwise scrolls inside an image when zoomed in
|
|
|
|
|
pub fn pan(&self, direction: >k::PanDirection) {
|
|
|
|
|
let sign = match direction {
|
|
|
|
|
gtk::PanDirection::Left | gtk::PanDirection::Up => -1.,
|
|
|
|
|
gtk::PanDirection::Right | gtk::PanDirection::Down => 1.,
|
|
|
|
|
_ => {
|
|
|
|
|
log::error!("Unkown pan direction {direction:?}");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let (adjustment, max) = match direction {
|
|
|
|
|
gtk::PanDirection::Left | gtk::PanDirection::Right => {
|
|
|
|
|
(self.hadjustment(), self.max_hadjustment_value())
|
|
|
|
|
}
|
|
|
|
|
gtk::PanDirection::Up | gtk::PanDirection::Down => {
|
|
|
|
|
(self.vadjustment(), self.max_vadjustment_value())
|
|
|
|
|
}
|
|
|
|
|
_ => {
|
|
|
|
|
log::error!("Unkown pan direction {direction:?}");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let value = (adjustment.value() + sign * adjustment.step_increment()).clamp(0., max);
|
|
|
|
|
|
|
|
|
|
adjustment.set_value(value);
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
fn hadjustment(&self) -> gtk::Adjustment {
|
|
|
|
|
if let Some(adj) = self.imp().hadjustment.borrow().as_ref() {
|
|
|
|
|
adj.clone()
|
|
|
|
|
} else {
|
2022-11-16 20:14:39 +01:00
|
|
|
log::trace!("Hadjustment not set yet: Using fake object");
|
2022-11-10 19:39:23 +01:00
|
|
|
gtk::Adjustment::default()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn set_hadjustment(&self, adjustment: Option<gtk::Adjustment>) {
|
|
|
|
|
if let Some(adj) = &adjustment {
|
|
|
|
|
adj.connect_value_changed(glib::clone!(@weak self as obj => move |_| {
|
|
|
|
|
obj.queue_draw();
|
|
|
|
|
}));
|
|
|
|
|
// TODO: needed?
|
|
|
|
|
self.queue_allocate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.imp().hadjustment.replace(adjustment);
|
|
|
|
|
self.configure_adjustments();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn vadjustment(&self) -> gtk::Adjustment {
|
|
|
|
|
if let Some(adj) = self.imp().vadjustment.borrow().as_ref() {
|
|
|
|
|
adj.clone()
|
|
|
|
|
} else {
|
2022-11-16 20:14:39 +01:00
|
|
|
log::trace!("Vadjustment not set yet: Using fake object");
|
2022-11-10 19:39:23 +01:00
|
|
|
gtk::Adjustment::default()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn set_vadjustment(&self, adjustment: Option<gtk::Adjustment>) {
|
|
|
|
|
if let Some(adj) = &adjustment {
|
|
|
|
|
adj.connect_value_changed(glib::clone!(@weak self as obj => move |_| {
|
|
|
|
|
obj.queue_draw();
|
|
|
|
|
}));
|
|
|
|
|
self.queue_allocate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.imp().vadjustment.replace(adjustment);
|
|
|
|
|
self.configure_adjustments();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Configure scrollbars for current situation
|
|
|
|
|
fn configure_adjustments(&self) {
|
|
|
|
|
let hadjustment = self.hadjustment();
|
2023-01-30 01:46:58 +01:00
|
|
|
// round to application pixels to avoid tiny rounding errors from zoom
|
|
|
|
|
let content_width = self.round(self.image_displayed_width());
|
2022-11-10 19:39:23 +01:00
|
|
|
let widget_width = self.widget_width();
|
|
|
|
|
|
|
|
|
|
hadjustment.configure(
|
|
|
|
|
hadjustment.value().clamp(0., self.max_hadjustment_value()),
|
2023-01-30 01:46:58 +01:00
|
|
|
// lower
|
2022-11-10 19:39:23 +01:00
|
|
|
0.,
|
2023-01-30 01:46:58 +01:00
|
|
|
// upper
|
2022-11-10 19:39:23 +01:00
|
|
|
content_width,
|
2023-01-30 19:07:17 +01:00
|
|
|
// arrow button and shortcut step
|
2022-11-10 19:39:23 +01:00
|
|
|
widget_width * 0.1,
|
|
|
|
|
// page up/down step
|
|
|
|
|
widget_width * 0.9,
|
2023-01-30 01:46:58 +01:00
|
|
|
// page size
|
|
|
|
|
f64::min(widget_width, content_width),
|
2022-11-10 19:39:23 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let vadjustment = self.vadjustment();
|
2023-01-30 01:46:58 +01:00
|
|
|
// round to application pixels to avoid tiny rounding errors from zoom
|
|
|
|
|
let content_height = self.round(self.image_displayed_height());
|
2022-11-10 19:39:23 +01:00
|
|
|
let widget_height = self.widget_height();
|
|
|
|
|
|
|
|
|
|
vadjustment.configure(
|
|
|
|
|
vadjustment.value().clamp(0., self.max_vadjustment_value()),
|
2023-01-30 01:46:58 +01:00
|
|
|
// lower
|
2022-11-10 19:39:23 +01:00
|
|
|
0.,
|
2023-01-30 01:46:58 +01:00
|
|
|
// upper
|
2022-11-10 19:39:23 +01:00
|
|
|
content_height,
|
2023-01-30 19:07:17 +01:00
|
|
|
// arrow button and shortcut step
|
2022-11-10 19:39:23 +01:00
|
|
|
widget_height * 0.1,
|
|
|
|
|
// page up/down step
|
|
|
|
|
widget_height * 0.9,
|
2023-01-30 01:46:58 +01:00
|
|
|
// page_size
|
|
|
|
|
f64::min(widget_height, content_height),
|
2022-11-10 19:39:23 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn max_hadjustment_value(&self) -> f64 {
|
|
|
|
|
f64::max(self.image_displayed_width() - self.widget_width(), 0.)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn max_vadjustment_value(&self) -> f64 {
|
|
|
|
|
f64::max(self.image_displayed_height() - self.widget_height(), 0.)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn is_hscrollable(&self) -> bool {
|
|
|
|
|
self.max_hadjustment_value() != 0.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn is_vscrollable(&self) -> bool {
|
|
|
|
|
self.max_vadjustment_value() != 0.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn widget_height(&self) -> f64 {
|
|
|
|
|
self.height() as f64
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn widget_width(&self) -> f64 {
|
|
|
|
|
self.width() as f64
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-06 01:08:43 +01:00
|
|
|
pub fn metadata(&self) -> LpImageMetadata {
|
|
|
|
|
self.imp().image_metadata.borrow().clone()
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
/// Drag and drop content provider
|
2022-11-28 18:27:40 +01:00
|
|
|
pub fn content_provider(&self) -> Option<gdk::ContentProvider> {
|
|
|
|
|
let file = self.file()?;
|
|
|
|
|
let list = gdk::FileList::from_array(&[file]);
|
|
|
|
|
Some(gdk::ContentProvider::for_value(&list.to_value()))
|
2022-01-08 16:21:27 +01:00
|
|
|
}
|
2023-01-30 01:46:58 +01:00
|
|
|
|
|
|
|
|
/// Returns scaling aware rounded application pixel
|
|
|
|
|
///
|
|
|
|
|
/// One physical pixel is 0.5 application pixels
|
|
|
|
|
pub fn round(&self, number: f64) -> f64 {
|
|
|
|
|
let scale = self.scale_factor() as f64;
|
|
|
|
|
(number * scale).round() / scale
|
|
|
|
|
}
|
2021-12-13 18:19:23 -08:00
|
|
|
}
|