2023-04-19 20:02:01 +02:00
|
|
|
// Copyright (c) 2020-2023 Christopher Davis
|
2024-01-01 22:41:56 +01:00
|
|
|
// Copyright (c) 2022-2024 Sophie Herold
|
2023-04-19 20:02:01 +02:00
|
|
|
// Copyright (c) 2022 Elton A Rodrigues
|
|
|
|
|
// Copyright (c) 2022 Maximiliano Sandoval R
|
2023-12-06 23:45:02 +01:00
|
|
|
// Copyright (c) 2023 Matteo Nardi
|
2023-11-29 21:15:36 +01:00
|
|
|
// Copyright (c) 2023 Julian Hofer
|
2023-07-16 22:47:10 +02:00
|
|
|
// Copyright (c) 2023 FineFindus
|
|
|
|
|
// Copyright (c) 2023 qwel
|
|
|
|
|
// Copyright (c) 2023 Huan Thieu Nguyen
|
2024-04-03 01:41:21 +02:00
|
|
|
// Copyright (c) 2024 James Frost
|
2020-12-26 02:05:34 -08:00
|
|
|
//
|
|
|
|
|
// 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
|
|
|
|
|
|
2024-05-09 22:51:05 +02:00
|
|
|
mod actions;
|
2023-12-22 22:51:17 +01:00
|
|
|
mod controls;
|
|
|
|
|
|
2023-12-06 23:45:02 +01:00
|
|
|
use std::cell::{Cell, OnceCell, RefCell};
|
|
|
|
|
use std::path::{Path, PathBuf};
|
2021-12-21 16:09:48 -08:00
|
|
|
|
2024-05-09 22:51:05 +02:00
|
|
|
use actions::*;
|
2023-01-31 21:56:44 +01:00
|
|
|
use adw::prelude::*;
|
2021-12-21 12:15:46 -08:00
|
|
|
use adw::subclass::prelude::*;
|
2020-12-26 02:05:34 -08:00
|
|
|
use glib::clone;
|
2023-12-19 15:25:15 +01:00
|
|
|
use gtk::{CompositeTemplate, Widget};
|
2020-12-26 02:05:34 -08:00
|
|
|
|
2023-12-05 17:24:53 +01:00
|
|
|
use crate::application::LpApplication;
|
2020-12-26 02:05:34 -08:00
|
|
|
use crate::config;
|
2023-12-06 23:45:02 +01:00
|
|
|
use crate::deps::*;
|
|
|
|
|
use crate::util::gettext::*;
|
2024-05-09 22:51:05 +02:00
|
|
|
use crate::util::Direction;
|
2023-12-02 22:07:33 +01:00
|
|
|
use crate::widgets::{LpDragOverlay, LpImage, LpImageView, LpPropertiesView};
|
2020-12-26 02:05:34 -08:00
|
|
|
|
2023-04-27 21:12:47 +02:00
|
|
|
/// Show window after X milliseconds even if image dimensions are not known yet
|
|
|
|
|
const SHOW_WINDOW_AFTER: u64 = 2000;
|
|
|
|
|
|
2023-04-20 02:32:41 +02:00
|
|
|
/// Animation duration for showing overlay buttons in milliseconds
|
|
|
|
|
const SHOW_CONTROLS_ANIMATION_DURATION: u32 = 200;
|
|
|
|
|
/// Animation duration for hiding overlay buttons in milliseconds
|
|
|
|
|
const HIDE_CONTROLS_ANIMATION_DURATION: u32 = 1000;
|
2023-04-27 21:12:47 +02:00
|
|
|
/// Time of inactivity after which controls will be hidden in milliseconds
|
|
|
|
|
const HIDE_CONTROLS_IDLE_TIMEOUT: u64 = 3000;
|
2023-04-20 02:32:41 +02:00
|
|
|
|
2020-12-26 02:05:34 -08:00
|
|
|
mod imp {
|
2024-02-05 16:27:05 +01:00
|
|
|
use gio::glib::VariantTy;
|
|
|
|
|
|
2020-12-26 02:05:34 -08:00
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
// To use composite templates, you need
|
|
|
|
|
// to use derive macro. Derive macros generate
|
|
|
|
|
// code to e.g. implement a trait on something.
|
|
|
|
|
// In this case, code is generated for Debug output
|
|
|
|
|
// and to handle binding the template children.
|
|
|
|
|
//
|
|
|
|
|
// For this derive macro, you need to have
|
|
|
|
|
// `use gtk::CompositeTemplate` in your code.
|
2021-06-27 15:28:35 -07:00
|
|
|
//
|
|
|
|
|
// Because all of our member fields implement the
|
|
|
|
|
// `Default` trait, we can use `#[derive(Default)]`.
|
|
|
|
|
// If some member fields did not implement default,
|
|
|
|
|
// we'd need to have a `new()` function in the
|
|
|
|
|
// `impl ObjectSubclass for $TYPE` section.
|
|
|
|
|
#[derive(Default, Debug, CompositeTemplate)]
|
2023-11-28 21:50:51 +01:00
|
|
|
#[template(file = "window.ui")]
|
2021-12-21 10:55:02 -08:00
|
|
|
pub struct LpWindow {
|
2020-12-26 02:05:34 -08:00
|
|
|
// Template children are used with the
|
|
|
|
|
// TemplateChild<T> wrapper, where T is the
|
|
|
|
|
// object type of the template child.
|
|
|
|
|
#[template_child]
|
2023-04-26 21:17:03 +02:00
|
|
|
pub(super) toolbar_view: TemplateChild<adw::ToolbarView>,
|
|
|
|
|
#[template_child]
|
|
|
|
|
pub(super) headerbar: TemplateChild<adw::HeaderBar>,
|
|
|
|
|
#[template_child]
|
|
|
|
|
pub(super) headerbar_events: TemplateChild<gtk::EventControllerMotion>,
|
2020-12-26 02:05:34 -08:00
|
|
|
#[template_child]
|
2023-03-14 18:56:35 +01:00
|
|
|
pub(super) properties_button: TemplateChild<gtk::ToggleButton>,
|
2022-11-23 19:12:19 +01:00
|
|
|
#[template_child]
|
2023-12-02 22:07:33 +01:00
|
|
|
pub(super) primary_menu: TemplateChild<gtk::MenuButton>,
|
|
|
|
|
#[template_child]
|
2023-03-14 18:56:35 +01:00
|
|
|
pub(super) toast_overlay: TemplateChild<adw::ToastOverlay>,
|
2021-12-28 16:38:36 -08:00
|
|
|
#[template_child]
|
2023-03-14 18:56:35 +01:00
|
|
|
pub(super) stack: TemplateChild<gtk::Stack>,
|
2020-12-26 02:05:34 -08:00
|
|
|
#[template_child]
|
2023-03-14 18:56:35 +01:00
|
|
|
pub(super) status_page: TemplateChild<adw::StatusPage>,
|
2020-12-26 02:05:34 -08:00
|
|
|
#[template_child]
|
2023-03-14 18:56:35 +01:00
|
|
|
pub(super) image_view: TemplateChild<LpImageView>,
|
2021-12-27 15:03:31 -08:00
|
|
|
#[template_child]
|
2023-03-14 18:56:35 +01:00
|
|
|
pub(super) properties_view: TemplateChild<LpPropertiesView>,
|
2023-06-10 23:43:09 +02:00
|
|
|
|
|
|
|
|
#[template_child]
|
2023-06-25 21:13:56 +02:00
|
|
|
pub(super) drag_overlay: TemplateChild<LpDragOverlay>,
|
2022-07-26 11:31:11 -04:00
|
|
|
#[template_child]
|
2023-03-14 18:56:35 +01:00
|
|
|
pub(super) drop_target: TemplateChild<gtk::DropTarget>,
|
2023-05-21 16:17:48 +00:00
|
|
|
|
|
|
|
|
#[template_child]
|
|
|
|
|
pub(super) forward_click_gesture: TemplateChild<gtk::GestureClick>,
|
|
|
|
|
#[template_child]
|
|
|
|
|
pub(super) backward_click_gesture: TemplateChild<gtk::GestureClick>,
|
|
|
|
|
|
2023-04-26 21:17:03 +02:00
|
|
|
/// Motion controller for complete window
|
2023-05-01 18:06:38 +02:00
|
|
|
pub(super) motion_controller: gtk::EventControllerMotion,
|
|
|
|
|
pub(super) pointer_position: Cell<(f64, f64)>,
|
2023-04-20 02:32:41 +02:00
|
|
|
|
|
|
|
|
pub(super) show_controls_animation: OnceCell<adw::TimedAnimation>,
|
|
|
|
|
pub(super) hide_controls_animation: OnceCell<adw::TimedAnimation>,
|
|
|
|
|
pub(super) hide_controls_timeout: RefCell<Option<glib::SourceId>>,
|
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 LpWindow {
|
|
|
|
|
const NAME: &'static str = "LpWindow";
|
|
|
|
|
type Type = super::LpWindow;
|
2021-12-21 12:15:46 -08:00
|
|
|
type ParentType = adw::ApplicationWindow;
|
2020-12-26 02:05:34 -08:00
|
|
|
|
|
|
|
|
fn class_init(klass: &mut Self::Class) {
|
2021-01-30 15:52:32 -08:00
|
|
|
// bind_template() is a function generated by the
|
2020-12-26 02:05:34 -08:00
|
|
|
// CompositeTemplate macro to bind all children at once.
|
2021-01-30 15:52:32 -08:00
|
|
|
Self::bind_template(klass);
|
2021-09-22 21:33:47 -07:00
|
|
|
|
2024-02-05 16:27:05 +01:00
|
|
|
klass.add_binding(gdk::Key::c, gdk::ModifierType::CONTROL_MASK, |window| {
|
|
|
|
|
if window.has_metadata_selected() {
|
|
|
|
|
// Pass on to normal copy handler to copy selected metadata
|
|
|
|
|
glib::Propagation::Proceed
|
|
|
|
|
} else {
|
|
|
|
|
window.copy_image();
|
|
|
|
|
glib::Propagation::Stop
|
|
|
|
|
}
|
|
|
|
|
});
|
2023-11-30 01:20:13 +01:00
|
|
|
|
2024-05-09 22:51:05 +02:00
|
|
|
klass.add_binding_action(
|
|
|
|
|
gdk::Key::question,
|
|
|
|
|
gdk::ModifierType::CONTROL_MASK,
|
|
|
|
|
"win.show-help-overlay",
|
2024-02-05 16:27:05 +01:00
|
|
|
);
|
2022-11-10 19:39:23 +01:00
|
|
|
|
2024-05-09 22:51:05 +02:00
|
|
|
// Set up actions
|
2022-01-08 14:02:16 +01:00
|
|
|
|
2024-05-09 22:51:05 +02:00
|
|
|
ActionPartGlobal::init_actions_and_bindings(klass);
|
|
|
|
|
Action::init_actions_and_bindings(klass);
|
2024-02-09 21:19:35 +01:00
|
|
|
|
2024-02-05 16:27:05 +01:00
|
|
|
klass.install_action(
|
|
|
|
|
"win.show-toast",
|
2024-02-06 13:11:20 +01:00
|
|
|
Some(VariantTy::TUPLE),
|
2024-02-05 16:27:05 +01:00
|
|
|
move |win, _, var| {
|
|
|
|
|
if let Some((ref toast, i)) = var.and_then(|v| v.get::<(String, i32)>()) {
|
|
|
|
|
win.show_toast(toast, adw::ToastPriority::__Unknown(i));
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
2020-12-26 02:05:34 -08:00
|
|
|
}
|
2021-09-22 21:33:47 -07: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 LpWindow {
|
2022-11-02 21:11:43 +01:00
|
|
|
fn constructed(&self) {
|
|
|
|
|
self.parent_constructed();
|
2020-12-26 02:05:34 -08:00
|
|
|
|
2023-12-02 22:07:33 +01:00
|
|
|
let obj = self.obj();
|
|
|
|
|
|
2024-06-18 00:37:38 +02:00
|
|
|
self.forward_click_gesture.connect_pressed(clone!(
|
|
|
|
|
#[weak]
|
|
|
|
|
obj,
|
|
|
|
|
move |_, _, _, _| {
|
2024-03-28 21:21:22 +00:00
|
|
|
obj.image_view().navigate(Direction::Forward, false);
|
2024-06-18 00:37:38 +02:00
|
|
|
}
|
|
|
|
|
));
|
|
|
|
|
self.backward_click_gesture.connect_pressed(clone!(
|
|
|
|
|
#[weak]
|
|
|
|
|
obj,
|
|
|
|
|
move |_, _, _, _| {
|
2024-03-28 21:21:22 +00:00
|
|
|
obj.image_view().navigate(Direction::Back, false);
|
2024-06-18 00:37:38 +02:00
|
|
|
}
|
|
|
|
|
));
|
|
|
|
|
self.properties_button.connect_toggled(clone!(
|
|
|
|
|
#[weak]
|
|
|
|
|
obj,
|
|
|
|
|
move |_| {
|
2023-12-05 17:24:53 +01:00
|
|
|
obj.on_properties_button_toggled();
|
2024-06-18 00:37:38 +02:00
|
|
|
}
|
|
|
|
|
));
|
2023-05-21 16:17:48 +00:00
|
|
|
|
2020-12-26 02:05:34 -08:00
|
|
|
if config::PROFILE == ".Devel" {
|
|
|
|
|
obj.add_css_class("devel");
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-31 21:56:44 +01:00
|
|
|
// Limit effect of modal dialogs to this window
|
|
|
|
|
// and keeps the others usable
|
|
|
|
|
gtk::WindowGroup::new().add_window(&*obj);
|
|
|
|
|
|
2023-04-27 21:12:47 +02:00
|
|
|
glib::timeout_add_local_once(
|
|
|
|
|
std::time::Duration::from_millis(SHOW_WINDOW_AFTER),
|
2024-06-18 00:37:38 +02:00
|
|
|
glib::clone!(
|
|
|
|
|
#[weak]
|
|
|
|
|
obj,
|
|
|
|
|
move || if !obj.is_visible() {
|
|
|
|
|
obj.present()
|
|
|
|
|
}
|
|
|
|
|
),
|
2023-04-27 21:12:47 +02:00
|
|
|
);
|
|
|
|
|
|
2023-11-30 22:20:57 +01:00
|
|
|
obj.on_current_page_changed();
|
2023-12-04 18:22:47 +01:00
|
|
|
obj.on_fullscreen_changed();
|
2023-11-30 22:20:57 +01:00
|
|
|
|
2024-06-18 00:37:38 +02:00
|
|
|
self.image_view.connect_current_page_notify(glib::clone!(
|
|
|
|
|
#[weak]
|
|
|
|
|
obj,
|
|
|
|
|
move |_| obj.on_current_page_changed()
|
|
|
|
|
));
|
2023-03-02 23:14:14 -05:00
|
|
|
|
2024-06-18 00:37:38 +02:00
|
|
|
obj.connect_fullscreened_notify(glib::clone!(
|
|
|
|
|
#[weak]
|
|
|
|
|
obj,
|
|
|
|
|
move |_| obj.on_fullscreen_changed()
|
|
|
|
|
));
|
2023-12-02 22:07:33 +01:00
|
|
|
|
2024-01-19 16:03:24 +01:00
|
|
|
obj.connect_map(|win| {
|
|
|
|
|
win.resize_default();
|
|
|
|
|
});
|
|
|
|
|
|
2024-06-18 00:37:38 +02:00
|
|
|
self.properties_button.connect_active_notify(glib::clone!(
|
|
|
|
|
#[weak]
|
|
|
|
|
obj,
|
|
|
|
|
move |_| obj.update_headerbar_style()
|
|
|
|
|
));
|
2023-12-02 22:07:33 +01:00
|
|
|
|
2023-05-01 17:44:45 +02:00
|
|
|
let gesture_click = gtk::GestureClick::new();
|
2024-06-18 00:37:38 +02:00
|
|
|
gesture_click.connect_pressed(glib::clone!(
|
|
|
|
|
#[weak]
|
|
|
|
|
obj,
|
|
|
|
|
move |_, _, _, _| obj.on_click()
|
|
|
|
|
));
|
2023-05-01 17:44:45 +02:00
|
|
|
obj.add_controller(gesture_click);
|
|
|
|
|
|
2024-06-18 00:37:38 +02:00
|
|
|
self.motion_controller.connect_motion(glib::clone!(
|
|
|
|
|
#[weak]
|
|
|
|
|
obj,
|
|
|
|
|
move |_, x, y| obj.on_motion((x, y))
|
|
|
|
|
));
|
|
|
|
|
self.motion_controller.connect_enter(glib::clone!(
|
|
|
|
|
#[weak]
|
|
|
|
|
obj,
|
|
|
|
|
move |_, x, y| obj.on_motion((x, y))
|
|
|
|
|
));
|
2023-05-01 18:06:38 +02:00
|
|
|
obj.add_controller(self.motion_controller.clone());
|
|
|
|
|
|
2023-03-04 11:47:07 -05:00
|
|
|
let current_image_signals = self.image_view.current_image_signals();
|
2023-03-02 23:14:14 -05:00
|
|
|
// clone! is a macro from glib-rs that allows
|
|
|
|
|
// you to easily handle references in callbacks
|
|
|
|
|
// without refcycles or leaks.
|
|
|
|
|
//
|
|
|
|
|
// When you don't want the callback to keep the
|
|
|
|
|
// Object alive, pass as @weak. Otherwise, pass
|
|
|
|
|
// as @strong. Most of the time you will want
|
|
|
|
|
// to use @weak.
|
2024-06-18 00:37:38 +02:00
|
|
|
current_image_signals.connect_bind_local(glib::clone!(
|
|
|
|
|
#[weak]
|
|
|
|
|
obj,
|
|
|
|
|
move |_, _| obj.on_zoom_status_changed()
|
|
|
|
|
));
|
2023-03-02 23:14:14 -05:00
|
|
|
|
2023-12-04 12:52:40 +01:00
|
|
|
current_image_signals.connect_local(
|
|
|
|
|
"metadata-changed",
|
|
|
|
|
true,
|
2024-06-18 00:37:38 +02:00
|
|
|
glib::clone!(
|
|
|
|
|
#[weak]
|
|
|
|
|
obj,
|
|
|
|
|
#[upgrade_or_else]
|
|
|
|
|
|| None,
|
|
|
|
|
move |_| {
|
|
|
|
|
obj.update_title();
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
),
|
2023-12-04 12:52:40 +01:00
|
|
|
);
|
|
|
|
|
|
2023-03-04 11:47:07 -05:00
|
|
|
current_image_signals.connect_closure(
|
2023-03-02 23:14:14 -05:00
|
|
|
"notify::best-fit",
|
|
|
|
|
false,
|
|
|
|
|
// `closure_local!` is similar to `clone`, but you use `@watch` instead of clone.
|
|
|
|
|
// `@watch` means that this signal will be disconnected when the watched object
|
|
|
|
|
// is dropped.
|
2024-06-18 00:37:38 +02:00
|
|
|
glib::closure_local!(
|
|
|
|
|
#[watch]
|
|
|
|
|
obj,
|
|
|
|
|
move |_: &LpImage, _: &glib::ParamSpec| {
|
|
|
|
|
obj.on_zoom_status_changed();
|
|
|
|
|
}
|
|
|
|
|
),
|
2023-03-02 23:14:14 -05:00
|
|
|
);
|
|
|
|
|
|
2023-03-04 11:47:07 -05:00
|
|
|
current_image_signals.connect_closure(
|
2023-03-02 23:14:14 -05:00
|
|
|
"notify::is-max-zoom",
|
|
|
|
|
false,
|
2024-06-18 00:37:38 +02:00
|
|
|
glib::closure_local!(
|
|
|
|
|
#[watch]
|
|
|
|
|
obj,
|
|
|
|
|
move |_: &LpImage, _: &glib::ParamSpec| {
|
|
|
|
|
obj.on_zoom_status_changed();
|
|
|
|
|
}
|
|
|
|
|
),
|
2023-03-02 23:14:14 -05:00
|
|
|
);
|
2022-11-28 03:23:06 +01:00
|
|
|
|
2023-03-04 11:47:07 -05:00
|
|
|
current_image_signals.connect_closure(
|
2023-11-29 01:49:14 +01:00
|
|
|
"notify::image-size-available",
|
2023-03-04 11:47:07 -05:00
|
|
|
false,
|
2024-06-18 00:37:38 +02:00
|
|
|
glib::closure_local!(
|
|
|
|
|
#[watch]
|
|
|
|
|
obj,
|
|
|
|
|
move |_: &LpImage, _: &glib::ParamSpec| {
|
|
|
|
|
obj.image_size_available();
|
|
|
|
|
}
|
|
|
|
|
),
|
2023-03-04 11:47:07 -05:00
|
|
|
);
|
|
|
|
|
|
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
|
|
|
current_image_signals.connect_closure(
|
2023-03-04 11:47:07 -05:00
|
|
|
"notify::error",
|
|
|
|
|
false,
|
2024-06-18 00:37:38 +02:00
|
|
|
glib::closure_local!(
|
|
|
|
|
#[watch]
|
|
|
|
|
obj,
|
|
|
|
|
move |_: &LpImage, _: &glib::ParamSpec| {
|
|
|
|
|
obj.image_error();
|
|
|
|
|
}
|
|
|
|
|
),
|
2023-01-23 01:10:15 +01:00
|
|
|
);
|
|
|
|
|
|
2024-06-18 00:37:38 +02:00
|
|
|
self.toolbar_view.connect_top_bar_style_notify(glib::clone!(
|
|
|
|
|
#[weak]
|
|
|
|
|
obj,
|
|
|
|
|
move |_| obj.on_top_bar_style_notify()
|
|
|
|
|
));
|
2023-04-26 21:17:03 +02:00
|
|
|
|
2022-12-06 18:24:32 +01:00
|
|
|
// action win.previous status
|
2024-06-18 00:37:38 +02:00
|
|
|
self.image_view
|
|
|
|
|
.connect_is_previous_available_notify(glib::clone!(
|
|
|
|
|
#[weak]
|
|
|
|
|
obj,
|
|
|
|
|
move |_| {
|
|
|
|
|
obj.action_set_enabled(
|
|
|
|
|
"win.previous",
|
|
|
|
|
obj.imp().image_view.is_previous_available(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
));
|
2022-12-06 18:24:32 +01:00
|
|
|
|
|
|
|
|
// action win.next status
|
2023-11-30 22:23:05 +01:00
|
|
|
self.image_view
|
2024-06-18 00:37:38 +02:00
|
|
|
.connect_is_next_available_notify(glib::clone!(
|
|
|
|
|
#[weak]
|
|
|
|
|
obj,
|
|
|
|
|
move |_| {
|
|
|
|
|
obj.action_set_enabled(
|
|
|
|
|
"win.next",
|
|
|
|
|
obj.imp().image_view.is_next_available(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
));
|
2022-12-06 18:24:32 +01:00
|
|
|
|
2023-04-20 02:32:41 +02:00
|
|
|
// Make widgets visible when the focus moves
|
|
|
|
|
obj.connect_move_focus(|obj, _| {
|
|
|
|
|
obj.show_controls();
|
2023-11-30 22:27:21 +01:00
|
|
|
obj.schedule_hide_controls();
|
2023-04-20 02:32:41 +02:00
|
|
|
});
|
|
|
|
|
|
2024-05-09 22:51:05 +02:00
|
|
|
// Activate global shortcuts only if no dialog is open
|
|
|
|
|
obj.connect_visible_dialog_notify(|obj| obj.update_accel_status());
|
|
|
|
|
obj.connect_is_active_notify(|obj| obj.update_accel_status());
|
|
|
|
|
|
2020-12-26 02:05:34 -08:00
|
|
|
self.status_page
|
|
|
|
|
.set_icon_name(Some(&format!("{}-symbolic", config::APP_ID)));
|
|
|
|
|
|
2022-07-26 12:07:08 -04:00
|
|
|
self.drop_target.set_types(&[gdk::FileList::static_type()]);
|
|
|
|
|
|
2024-06-18 00:37:38 +02:00
|
|
|
self.drop_target.connect_accept(clone!(
|
|
|
|
|
#[weak]
|
|
|
|
|
obj,
|
|
|
|
|
#[upgrade_or_else]
|
|
|
|
|
|| false,
|
|
|
|
|
move |_drop_target, drop| {
|
2023-09-05 23:07:18 +02:00
|
|
|
// Only accept drops from external sources or different windows
|
2024-06-18 00:37:38 +02:00
|
|
|
let different_source = drop.drag().is_none()
|
|
|
|
|
|| drop.drag() != obj.image_view().drag_source().drag();
|
2023-09-05 23:07:18 +02:00
|
|
|
// We have to do this manually since we are overwriting the default handler
|
|
|
|
|
let correct_format = drop.formats().contains_type(gdk::FileList::static_type());
|
|
|
|
|
|
|
|
|
|
different_source && correct_format
|
2024-06-18 00:37:38 +02:00
|
|
|
}
|
|
|
|
|
));
|
2023-06-10 23:43:09 +02:00
|
|
|
|
2020-12-26 02:05:34 -08:00
|
|
|
// For callbacks, you will want to reference the GTK docs on
|
|
|
|
|
// the relevant signal to see which parameters you need.
|
2022-07-26 12:07:08 -04:00
|
|
|
// In this case, we need only need the GValue,
|
|
|
|
|
// so we name it `value` then use `_` for the other spots.
|
2024-06-18 00:37:38 +02:00
|
|
|
self.drop_target.connect_drop(clone!(
|
|
|
|
|
#[weak]
|
|
|
|
|
obj,
|
|
|
|
|
#[upgrade_or_else]
|
|
|
|
|
|| false,
|
|
|
|
|
move |_, value, _, _| {
|
|
|
|
|
// Here we use a GValue, which is a dynamic object that can hold different
|
|
|
|
|
// types, e.g. strings, numbers, or in this case objects. In
|
|
|
|
|
// order to get the GdkFileList from the GValue, we need to
|
|
|
|
|
// use the `get()` method.
|
2021-12-27 15:03:31 -08:00
|
|
|
//
|
2024-06-18 00:37:38 +02:00
|
|
|
// We've added type annotations here, and written it as `let list: gdk::FileList
|
|
|
|
|
// = ...`, but you might also see places where type
|
|
|
|
|
// arguments are used. This line could have been written as
|
|
|
|
|
// `let list = value.get::<gdk::FileList>().unwrap()`.
|
2023-05-30 12:52:59 +00:00
|
|
|
let files = match value.get::<gdk::FileList>() {
|
2023-04-18 01:52:06 +02:00
|
|
|
Ok(list) => list.files(),
|
2022-11-04 19:45:28 +01:00
|
|
|
Err(err) => {
|
|
|
|
|
log::error!("Issue with drop value: {err}");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
};
|
2021-12-27 15:03:31 -08:00
|
|
|
|
2023-05-30 12:52:59 +00:00
|
|
|
if !files.is_empty() {
|
2023-04-18 01:52:06 +02:00
|
|
|
obj.image_view().set_images_from_files(files);
|
2021-12-27 15:03:31 -08:00
|
|
|
} else {
|
2023-04-18 01:52:06 +02:00
|
|
|
log::error!("Dropped FileList was empty");
|
|
|
|
|
return false;
|
2021-12-27 15:03:31 -08:00
|
|
|
}
|
|
|
|
|
|
2023-04-18 01:52:06 +02:00
|
|
|
// Maybe one day this will actually work
|
|
|
|
|
obj.present();
|
|
|
|
|
|
2021-12-27 15:03:31 -08:00
|
|
|
true
|
2024-06-18 00:37:38 +02:00
|
|
|
}
|
|
|
|
|
));
|
2020-12-26 02:05:34 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-21 10:55:02 -08:00
|
|
|
impl WidgetImpl for LpWindow {}
|
|
|
|
|
impl WindowImpl for LpWindow {}
|
|
|
|
|
impl ApplicationWindowImpl for LpWindow {}
|
|
|
|
|
impl AdwApplicationWindowImpl for LpWindow {}
|
2020-12-26 02:05:34 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
glib::wrapper! {
|
2021-12-21 10:55:02 -08:00
|
|
|
pub struct LpWindow(ObjectSubclass<imp::LpWindow>)
|
2021-12-21 12:15:46 -08:00
|
|
|
@extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, adw::ApplicationWindow,
|
2021-12-29 17:43:58 -08:00
|
|
|
@implements gio::ActionMap, gio::ActionGroup, gtk::Native;
|
2020-12-26 02:05:34 -08:00
|
|
|
}
|
|
|
|
|
|
2021-12-21 10:55:02 -08:00
|
|
|
impl LpWindow {
|
2020-12-26 02:05:34 -08:00
|
|
|
pub fn new<A: IsA<gtk::Application>>(app: &A) -> Self {
|
2022-11-24 12:26:04 -05:00
|
|
|
glib::Object::builder().property("application", app).build()
|
2020-12-26 02:05:34 -08:00
|
|
|
}
|
|
|
|
|
|
2021-09-24 01:14:56 -07:00
|
|
|
fn toggle_fullscreen(&self, fullscreen: bool) {
|
|
|
|
|
self.set_fullscreened(fullscreen);
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-04 14:33:23 +01:00
|
|
|
fn zoom_out_cursor(&self) {
|
|
|
|
|
if let Some(image) = self.imp().image_view.current_image() {
|
|
|
|
|
image.zoom_out_cursor();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn zoom_out_center(&self) {
|
|
|
|
|
if let Some(image) = self.imp().image_view.current_image() {
|
|
|
|
|
image.zoom_out_center();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn zoom_in_cursor(&self) {
|
|
|
|
|
if let Some(image) = self.imp().image_view.current_image() {
|
|
|
|
|
image.zoom_in_cursor();
|
|
|
|
|
}
|
2022-11-10 19:39:23 +01:00
|
|
|
}
|
|
|
|
|
|
2024-01-04 14:33:23 +01:00
|
|
|
fn zoom_in_center(&self) {
|
|
|
|
|
if let Some(image) = self.imp().image_view.current_image() {
|
|
|
|
|
image.zoom_in_center();
|
|
|
|
|
}
|
2022-11-10 19:39:23 +01:00
|
|
|
}
|
|
|
|
|
|
2023-04-13 04:52:14 +02:00
|
|
|
fn zoom_to_exact(&self, level: f64) {
|
|
|
|
|
if let Some(image) = self.imp().image_view.current_image() {
|
|
|
|
|
image.zoom_to_exact(level);
|
|
|
|
|
}
|
2022-11-10 19:39:23 +01:00
|
|
|
}
|
|
|
|
|
|
2022-11-23 19:12:19 +01:00
|
|
|
fn zoom_best_fit(&self) {
|
|
|
|
|
if let Some(page) = self.imp().image_view.current_page() {
|
|
|
|
|
page.image().zoom_best_fit();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-30 19:07:17 +01:00
|
|
|
fn pan(&self, direction: >k::PanDirection) {
|
|
|
|
|
if let Some(image) = self.imp().image_view.current_image() {
|
|
|
|
|
image.pan(direction);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-12 11:25:12 -05:00
|
|
|
async fn pick_file(&self) {
|
2023-08-02 14:27:23 +02:00
|
|
|
let filter_list = gio::ListStore::new::<gtk::FileFilter>();
|
2020-12-26 02:05:34 -08:00
|
|
|
|
2023-04-21 16:53:12 +02:00
|
|
|
let filter_supported_formats = gtk::FileFilter::new();
|
|
|
|
|
filter_supported_formats.set_name(Some(&gettext("Supported image formats")));
|
2024-06-14 17:46:33 +02:00
|
|
|
for mime_type in glycin::supported_loader_mime_types().await {
|
|
|
|
|
filter_supported_formats.add_mime_type(mime_type.as_str());
|
2023-06-20 20:58:01 +02:00
|
|
|
}
|
2023-04-21 16:53:12 +02:00
|
|
|
|
|
|
|
|
let filter_all_files = gtk::FileFilter::new();
|
|
|
|
|
filter_all_files.set_name(Some(&gettext("All files")));
|
|
|
|
|
filter_all_files.add_pattern("*");
|
|
|
|
|
|
|
|
|
|
filter_list.append(&filter_supported_formats);
|
|
|
|
|
filter_list.append(&filter_all_files);
|
2020-12-26 02:05:34 -08:00
|
|
|
|
2022-12-12 11:25:12 -05:00
|
|
|
let chooser = gtk::FileDialog::builder()
|
2023-04-11 00:02:28 +02:00
|
|
|
.title(gettext("Open Image"))
|
2023-04-21 16:53:12 +02:00
|
|
|
.filters(&filter_list)
|
|
|
|
|
.default_filter(&filter_supported_formats)
|
2022-12-12 11:25:12 -05:00
|
|
|
.modal(true)
|
|
|
|
|
.build();
|
2020-12-26 02:05:34 -08:00
|
|
|
|
2023-08-27 22:20:13 +02:00
|
|
|
chooser.set_initial_folder(
|
|
|
|
|
self.image_view()
|
|
|
|
|
.current_image()
|
|
|
|
|
.and_then(|x| x.file())
|
|
|
|
|
.and_then(|x| x.parent())
|
|
|
|
|
.as_ref(),
|
|
|
|
|
);
|
|
|
|
|
|
2023-05-21 23:41:22 +02:00
|
|
|
if let Ok(selected) = chooser.open_multiple_future(Some(self)).await {
|
|
|
|
|
let images: Vec<_> = selected
|
|
|
|
|
.into_iter()
|
|
|
|
|
.filter_map(|files| {
|
|
|
|
|
files
|
|
|
|
|
.ok()
|
|
|
|
|
.and_then(|object| object.downcast::<gio::File>().ok())
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
self.image_view().set_images_from_files(images);
|
2023-01-23 01:10:15 +01:00
|
|
|
} else {
|
2023-04-12 01:11:59 +02:00
|
|
|
log::debug!("File dialog canceled or file not readable");
|
2022-12-12 11:25:12 -05:00
|
|
|
}
|
2020-12-26 02:05:34 -08:00
|
|
|
}
|
|
|
|
|
|
2023-01-15 19:18:16 -05:00
|
|
|
async fn open_with(&self) {
|
2021-12-29 14:02:48 -08:00
|
|
|
let imp = self.imp();
|
2021-12-29 17:43:58 -08:00
|
|
|
|
2023-04-12 01:11:59 +02:00
|
|
|
if let Some(ref file) = imp.image_view.current_file() {
|
2023-01-15 19:18:16 -05:00
|
|
|
let launcher = gtk::FileLauncher::new(Some(file));
|
2023-08-15 17:18:57 +02:00
|
|
|
launcher.set_always_ask(true);
|
2023-01-15 19:18:16 -05:00
|
|
|
if let Err(e) = launcher.launch_future(Some(self)).await {
|
|
|
|
|
if !e.matches(gtk::DialogError::Dismissed) {
|
2021-12-29 17:43:58 -08:00
|
|
|
log::error!("Could not open image in external program: {}", e);
|
|
|
|
|
}
|
2023-01-15 19:18:16 -05:00
|
|
|
}
|
2020-12-26 02:05:34 -08:00
|
|
|
} else {
|
2022-02-05 13:26:33 -08:00
|
|
|
log::error!("Could not load a path for the current image.")
|
2020-12-26 02:05:34 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-05 00:57:18 +01:00
|
|
|
fn rotate_image(&self, angle: f64) {
|
|
|
|
|
self.imp().image_view.rotate_image(angle)
|
2022-11-03 17:40:06 +01:00
|
|
|
}
|
|
|
|
|
|
2023-01-22 12:18:43 -05:00
|
|
|
async fn set_background(&self) {
|
2021-12-29 14:02:48 -08:00
|
|
|
let imp = self.imp();
|
2020-12-26 02:05:34 -08:00
|
|
|
|
2023-01-22 12:18:43 -05:00
|
|
|
if let Err(e) = imp.image_view.set_background().await {
|
2022-08-17 21:31:36 -03:00
|
|
|
log::error!("Failed to set background: {}", e);
|
2020-12-26 02:05:34 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn print(&self) {
|
2021-12-29 14:02:48 -08:00
|
|
|
let imp = self.imp();
|
2020-12-26 02:05:34 -08:00
|
|
|
|
2021-01-13 23:16:02 -08:00
|
|
|
if let Err(e) = imp.image_view.print() {
|
2020-12-26 02:05:34 -08:00
|
|
|
log::error!("Failed to print file: {}", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-30 01:20:13 +01:00
|
|
|
/// Returns true if some text in metadata is currently selected
|
|
|
|
|
fn has_metadata_selected(&self) -> bool {
|
2024-06-14 17:46:33 +02:00
|
|
|
if let Some(focus_widget) = self.focus() {
|
2023-11-30 01:20:13 +01:00
|
|
|
if focus_widget.is_ancestor(&*self.imp().properties_view) {
|
|
|
|
|
if let Ok(label) = focus_widget.downcast::<gtk::Label>() {
|
|
|
|
|
return label.selection_bounds().is_some();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Copy image to clipboard or metadata text if selected
|
|
|
|
|
fn copy_image(&self) {
|
2022-01-08 14:02:16 +01:00
|
|
|
let imp = self.imp();
|
|
|
|
|
|
|
|
|
|
if let Err(e) = imp.image_view.copy() {
|
|
|
|
|
log::error!("Failed to copy to clipboard: {}", e);
|
2022-07-01 13:08:14 -04:00
|
|
|
} else {
|
2023-04-11 00:02:28 +02:00
|
|
|
self.show_toast(
|
|
|
|
|
gettext("Image copied to clipboard"),
|
|
|
|
|
adw::ToastPriority::High,
|
|
|
|
|
);
|
2022-01-08 14:02:16 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-31 21:56:44 +01:00
|
|
|
async fn trash(&self) {
|
|
|
|
|
let image_view = self.image_view();
|
2023-07-06 23:48:56 +02:00
|
|
|
let (Some(file), Some(path)) = (
|
|
|
|
|
image_view.current_file(),
|
|
|
|
|
image_view.current_file().and_then(|x| x.path()),
|
|
|
|
|
) else {
|
|
|
|
|
log::error!("No file to trash");
|
|
|
|
|
return;
|
|
|
|
|
};
|
2023-01-31 21:56:44 +01:00
|
|
|
|
|
|
|
|
let result = file.trash_future(glib::Priority::default()).await;
|
|
|
|
|
|
|
|
|
|
match result {
|
|
|
|
|
Ok(()) => {
|
|
|
|
|
let toast = adw::Toast::builder()
|
2023-04-11 00:02:28 +02:00
|
|
|
.title(gettext("Image moved to trash"))
|
|
|
|
|
.button_label(gettext("Undo"))
|
2023-06-20 23:37:30 +02:00
|
|
|
.priority(adw::ToastPriority::High)
|
2023-01-31 21:56:44 +01:00
|
|
|
.build();
|
2024-06-18 00:37:38 +02:00
|
|
|
toast.connect_button_clicked(glib::clone!(
|
|
|
|
|
#[weak(rename_to = win)]
|
|
|
|
|
self,
|
|
|
|
|
move |_| {
|
|
|
|
|
let path = path.clone();
|
|
|
|
|
glib::spawn_future_local(async move {
|
|
|
|
|
win.image_view()
|
|
|
|
|
.set_trash_restore(Some(gio::File::for_path(&path)));
|
|
|
|
|
let result = crate::util::untrash(&path).await;
|
|
|
|
|
if let Err(err) = result {
|
|
|
|
|
log::error!("Failed to untrash {path:?}: {err}");
|
|
|
|
|
win.show_toast(
|
|
|
|
|
gettext("Failed to restore image from trash"),
|
|
|
|
|
adw::ToastPriority::High,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
));
|
2023-02-12 02:07:42 +01:00
|
|
|
self.imp().toast_overlay.add_toast(toast);
|
2023-01-31 21:56:44 +01:00
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
if Some(gio::IOErrorEnum::NotSupported) == err.kind::<gio::IOErrorEnum>() {
|
2024-02-09 21:19:35 +01:00
|
|
|
self.delete_future(&path).await;
|
2023-01-31 21:56:44 +01:00
|
|
|
} else {
|
|
|
|
|
log::error!("Failed to delete file {path:?}: {err}");
|
|
|
|
|
self.show_toast(
|
2023-04-11 00:02:28 +02:00
|
|
|
gettext("Failed to move image to trash"),
|
2023-01-31 21:56:44 +01:00
|
|
|
adw::ToastPriority::Normal,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-09 21:19:35 +01:00
|
|
|
/// Delete action called
|
|
|
|
|
async fn delete(&self) {
|
|
|
|
|
let image_view = self.image_view();
|
|
|
|
|
let Some(path) = image_view.current_file().and_then(|x| x.path()) else {
|
|
|
|
|
log::error!("No file to delete");
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
self.delete_future(&path).await;
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-31 21:56:44 +01:00
|
|
|
/// Permanently delete image
|
|
|
|
|
///
|
2024-02-09 21:19:35 +01:00
|
|
|
/// Fallback for when trash not available or explicit call with shortcut
|
|
|
|
|
async fn delete_future(&self, path: &Path) {
|
2024-06-11 23:32:45 -07:00
|
|
|
let dialog = adw::AlertDialog::builder()
|
2023-04-11 00:02:28 +02:00
|
|
|
.heading(gettext("Permanently Delete Image?"))
|
|
|
|
|
.body(gettext_f(
|
2024-02-09 21:19:35 +01:00
|
|
|
"After deleting the image “{}” it will be permanently lost.",
|
2024-01-01 22:41:56 +01:00
|
|
|
[PathBuf::from(&path.file_name().unwrap_or_default())
|
2023-01-31 21:56:44 +01:00
|
|
|
.display()
|
|
|
|
|
.to_string()],
|
|
|
|
|
))
|
|
|
|
|
.build();
|
|
|
|
|
|
2023-04-11 00:02:28 +02:00
|
|
|
dialog.add_responses(&[
|
|
|
|
|
("cancel", &gettext("Cancel")),
|
|
|
|
|
("delete", &gettext("Delete")),
|
|
|
|
|
]);
|
2023-01-31 21:56:44 +01:00
|
|
|
dialog.set_response_appearance("delete", adw::ResponseAppearance::Destructive);
|
|
|
|
|
|
2024-06-11 23:32:45 -07:00
|
|
|
if "delete" == dialog.choose_future(self).await {
|
2023-01-31 21:56:44 +01:00
|
|
|
let file = gio::File::for_path(path);
|
|
|
|
|
let result = file.delete_future(glib::Priority::default()).await;
|
|
|
|
|
|
|
|
|
|
if let Err(err) = result {
|
|
|
|
|
log::error!("Failed to delete file {path:?}: {err}");
|
2023-04-11 00:02:28 +02:00
|
|
|
self.show_toast(
|
|
|
|
|
gettext("Failed to delete image"),
|
|
|
|
|
adw::ToastPriority::Normal,
|
|
|
|
|
);
|
2023-01-31 21:56:44 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-18 01:52:06 +02:00
|
|
|
pub fn image_view(&self) -> LpImageView {
|
2023-01-31 21:56:44 +01:00
|
|
|
self.imp().image_view.clone()
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-26 21:17:03 +02:00
|
|
|
pub fn headerbar(&self) -> adw::HeaderBar {
|
|
|
|
|
self.imp().headerbar.clone()
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-04 01:04:04 -04:00
|
|
|
fn show_toast(&self, text: impl AsRef<str>, priority: adw::ToastPriority) {
|
2021-12-29 14:02:48 -08:00
|
|
|
let imp = self.imp();
|
2021-12-28 16:38:36 -08:00
|
|
|
|
|
|
|
|
let toast = adw::Toast::new(text.as_ref());
|
|
|
|
|
toast.set_priority(priority);
|
|
|
|
|
|
2023-02-12 02:07:42 +01:00
|
|
|
imp.toast_overlay.add_toast(toast);
|
2021-12-28 16:38:36 -08:00
|
|
|
}
|
|
|
|
|
|
2020-12-26 02:05:34 -08:00
|
|
|
pub fn set_actions_enabled(&self, enabled: bool) {
|
2023-11-30 22:32:29 +01:00
|
|
|
const ACTIONS: &[&str] = &[
|
|
|
|
|
"win.open-with",
|
|
|
|
|
"win.set-background",
|
|
|
|
|
"win.toggle-fullscreen",
|
|
|
|
|
"win.print",
|
2024-05-09 22:51:05 +02:00
|
|
|
"win.rotate-cw",
|
|
|
|
|
"win.rotate-ccw",
|
2023-11-30 22:32:29 +01:00
|
|
|
"win.copy-image",
|
|
|
|
|
"win.zoom-best-fit",
|
|
|
|
|
"win.zoom-to-exact",
|
|
|
|
|
"win.toggle-properties",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for action in ACTIONS {
|
|
|
|
|
self.action_set_enabled(action, enabled);
|
|
|
|
|
}
|
2020-12-26 02:05:34 -08:00
|
|
|
}
|
2021-03-02 16:39:03 -08:00
|
|
|
|
2023-12-19 15:25:15 +01:00
|
|
|
/// Handles change in image and availability of images
|
2023-11-30 22:20:57 +01:00
|
|
|
fn on_current_page_changed(&self) {
|
2023-01-23 01:10:15 +01:00
|
|
|
let imp = self.imp();
|
2023-12-19 15:25:15 +01:00
|
|
|
let was_showing_image =
|
|
|
|
|
imp.stack.visible_child().as_ref() == Some(imp.image_view.upcast_ref::<Widget>());
|
2023-12-02 22:07:33 +01:00
|
|
|
let current_page = imp.image_view.current_page();
|
|
|
|
|
|
|
|
|
|
// HeaderBar style
|
|
|
|
|
self.update_headerbar_style();
|
2023-01-23 01:10:15 +01:00
|
|
|
|
2023-11-30 22:20:57 +01:00
|
|
|
// Window title
|
2023-12-04 12:52:40 +01:00
|
|
|
self.update_title();
|
2023-11-30 22:20:57 +01:00
|
|
|
|
2023-12-02 22:07:33 +01:00
|
|
|
// Properties view
|
|
|
|
|
let current_image = current_page.as_ref().map(|x| x.image());
|
|
|
|
|
imp.properties_view.set_image(current_image.as_ref());
|
|
|
|
|
|
|
|
|
|
let has_image = current_page.is_some();
|
2023-11-30 22:20:57 +01:00
|
|
|
|
|
|
|
|
self.set_actions_enabled(has_image);
|
2023-06-23 19:42:30 +00:00
|
|
|
self.action_set_enabled(
|
|
|
|
|
"win.trash",
|
|
|
|
|
imp.image_view
|
|
|
|
|
.current_file()
|
2023-08-27 22:05:46 +02:00
|
|
|
.is_some_and(|file| file.path().is_some()),
|
2023-06-23 19:42:30 +00:00
|
|
|
);
|
2023-04-20 21:40:55 +02:00
|
|
|
|
2023-11-30 22:20:57 +01:00
|
|
|
if has_image {
|
2023-12-19 15:25:15 +01:00
|
|
|
// Properties buttons was not sensitive before
|
2023-12-07 15:20:13 +01:00
|
|
|
if !imp.properties_button.is_sensitive() {
|
|
|
|
|
let settings = LpApplication::default().settings();
|
2023-12-19 15:25:15 +01:00
|
|
|
// Pickup config for setting it's state
|
2023-12-07 15:20:13 +01:00
|
|
|
imp.properties_button
|
|
|
|
|
.set_active(settings.boolean("show-properties"));
|
|
|
|
|
}
|
2023-12-19 15:25:15 +01:00
|
|
|
if !was_showing_image {
|
|
|
|
|
imp.stack.set_visible_child(&*imp.image_view);
|
|
|
|
|
imp.image_view.grab_focus();
|
|
|
|
|
self.show_controls();
|
|
|
|
|
self.schedule_hide_controls();
|
|
|
|
|
}
|
2023-01-23 01:10:15 +01:00
|
|
|
} else {
|
2023-12-05 17:24:53 +01:00
|
|
|
imp.properties_button.set_active(false);
|
2023-01-23 01:10:15 +01:00
|
|
|
imp.stack.set_visible_child(&*imp.status_page);
|
|
|
|
|
imp.status_page.grab_focus();
|
2023-04-27 20:48:59 +02:00
|
|
|
// Leave fullscreen since status page has no controls to leave it
|
|
|
|
|
self.set_fullscreened(false);
|
2023-01-23 01:10:15 +01:00
|
|
|
}
|
2023-12-07 15:20:13 +01:00
|
|
|
|
|
|
|
|
imp.properties_button.set_sensitive(has_image);
|
2023-01-23 01:10:15 +01:00
|
|
|
}
|
|
|
|
|
|
2023-12-06 23:45:02 +01:00
|
|
|
/// When the image-properties sidebar is displayed or hidden, we should
|
|
|
|
|
/// update the "show-properties" setting.
|
2023-12-05 17:24:53 +01:00
|
|
|
fn on_properties_button_toggled(&self) {
|
|
|
|
|
let imp = self.imp();
|
2023-12-06 23:45:02 +01:00
|
|
|
// When no image is shown, we skip this update as the sidebar should always be
|
|
|
|
|
// hidden. This can happen when deleting a picture.
|
2023-12-05 17:24:53 +01:00
|
|
|
if imp.image_view.current_page().is_none() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let settings = LpApplication::default().settings();
|
|
|
|
|
let result = settings.set_boolean("show-properties", imp.properties_button.is_active());
|
|
|
|
|
if let Err(err) = result {
|
|
|
|
|
log::warn!("Failed to save show-properties state, {}", err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-04 12:52:40 +01:00
|
|
|
pub fn update_title(&self) {
|
|
|
|
|
let title = self
|
|
|
|
|
.imp()
|
|
|
|
|
.image_view
|
|
|
|
|
.current_image()
|
|
|
|
|
.and_then(|x| x.metadata().file_name())
|
|
|
|
|
.unwrap_or_else(|| gettext("Image Viewer"));
|
|
|
|
|
|
|
|
|
|
self.set_title(Some(&title));
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-19 16:03:24 +01:00
|
|
|
pub fn image_size_available(&self) {
|
2022-11-16 20:14:39 +01:00
|
|
|
// if visible for whatever reason, don't do any resize
|
|
|
|
|
if self.is_visible() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let image = self
|
|
|
|
|
.imp()
|
|
|
|
|
.image_view
|
2023-01-23 01:10:15 +01:00
|
|
|
.current_page()
|
2022-11-16 20:14:39 +01:00
|
|
|
.map(|page| page.image());
|
|
|
|
|
|
2024-01-19 16:03:24 +01:00
|
|
|
if image.is_some_and(|img| img.image_size_available()) {
|
2023-06-26 11:59:17 +00:00
|
|
|
log::debug!("Showing window because image size is ready");
|
2024-01-19 16:03:24 +01:00
|
|
|
self.present();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn resize_default(&self) {
|
|
|
|
|
if self
|
|
|
|
|
.image_view()
|
|
|
|
|
.current_image()
|
|
|
|
|
.is_some_and(|img| img.image_size_available())
|
|
|
|
|
{
|
2023-06-26 11:59:17 +00:00
|
|
|
// this let's the window determine the default size from LpImage's natural size
|
|
|
|
|
self.set_default_size(-1, -1);
|
2022-11-16 20:14:39 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn image_error(&self) {
|
|
|
|
|
if self.is_visible() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
let current_page = self.imp().image_view.current_page();
|
2022-11-16 20:14:39 +01:00
|
|
|
|
2023-06-26 11:59:17 +00:00
|
|
|
if current_page.is_some_and(|page| page.image().error().is_some()) {
|
|
|
|
|
log::debug!("Showing window because loading image failed");
|
|
|
|
|
self.present();
|
2022-11-16 20:14:39 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-02 23:14:14 -05:00
|
|
|
fn on_zoom_status_changed(&self) {
|
|
|
|
|
let can_zoom_out = self
|
|
|
|
|
.image_view()
|
|
|
|
|
.current_image()
|
|
|
|
|
.map(|image| !image.is_best_fit())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
let can_zoom_in = self
|
|
|
|
|
.image_view()
|
|
|
|
|
.current_image()
|
|
|
|
|
.map(|image| !image.is_max_zoom())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
2024-01-04 14:33:23 +01:00
|
|
|
self.action_set_enabled("win.zoom-out-cursor", can_zoom_out);
|
|
|
|
|
self.action_set_enabled("win.zoom-out-center", can_zoom_out);
|
|
|
|
|
self.action_set_enabled("win.zoom-in-cursor", can_zoom_in);
|
|
|
|
|
self.action_set_enabled("win.zoom-in-center", can_zoom_in);
|
2023-03-02 23:14:14 -05:00
|
|
|
}
|
|
|
|
|
|
2023-12-02 22:07:33 +01:00
|
|
|
fn on_fullscreen_changed(&self) {
|
2023-12-04 18:22:47 +01:00
|
|
|
self.imp()
|
|
|
|
|
.image_view
|
2024-06-14 17:46:33 +02:00
|
|
|
.on_fullscreen_changed(self.is_fullscreen());
|
2023-12-04 18:22:47 +01:00
|
|
|
|
2023-12-02 22:07:33 +01:00
|
|
|
self.update_headerbar_style();
|
|
|
|
|
|
2024-06-14 17:46:33 +02:00
|
|
|
if !self.is_fullscreen() {
|
2023-04-22 01:35:38 +02:00
|
|
|
self.set_cursor(None);
|
|
|
|
|
self.show_controls();
|
2023-04-20 02:32:41 +02:00
|
|
|
}
|
2023-11-30 22:27:21 +01:00
|
|
|
self.schedule_hide_controls();
|
2023-04-20 02:32:41 +02:00
|
|
|
}
|
2023-04-26 21:17:03 +02:00
|
|
|
|
|
|
|
|
fn is_headerbar_flat(&self) -> bool {
|
|
|
|
|
matches!(
|
|
|
|
|
self.imp().toolbar_view.top_bar_style(),
|
|
|
|
|
adw::ToolbarStyle::Flat
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn on_top_bar_style_notify(&self) {
|
|
|
|
|
if self.is_headerbar_flat() {
|
|
|
|
|
// Bring headerbar opacity in sync with controls
|
|
|
|
|
self.headerbar().set_opacity(self.controls_opacity());
|
|
|
|
|
} else {
|
|
|
|
|
self.headerbar().set_opacity(1.);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-02 22:07:33 +01:00
|
|
|
fn is_content_extended_to_top(&self) -> bool {
|
2024-06-14 17:46:33 +02:00
|
|
|
self.is_fullscreen() && !self.imp().properties_button.is_active()
|
2023-04-26 21:17:03 +02:00
|
|
|
}
|
|
|
|
|
|
2023-12-02 22:07:33 +01:00
|
|
|
fn update_headerbar_style(&self) {
|
|
|
|
|
let imp = self.imp();
|
|
|
|
|
|
|
|
|
|
let extend = self.is_content_extended_to_top();
|
|
|
|
|
imp.toolbar_view.set_extend_content_to_top_edge(extend);
|
|
|
|
|
|
2023-08-31 15:50:28 +02:00
|
|
|
// Flat headerbar for empty state and fullscreen
|
2023-12-02 22:07:33 +01:00
|
|
|
let toolbar_style = if imp.image_view.current_image().is_none() || extend {
|
2023-04-26 21:17:03 +02:00
|
|
|
adw::ToolbarStyle::Flat
|
|
|
|
|
} else {
|
2023-07-30 21:13:35 +02:00
|
|
|
// Use the border variant of raised to avoid shadows over images
|
2023-12-02 22:07:33 +01:00
|
|
|
adw::ToolbarStyle::RaisedBorder
|
|
|
|
|
};
|
|
|
|
|
imp.toolbar_view.set_top_bar_style(toolbar_style);
|
2023-04-26 21:17:03 +02:00
|
|
|
}
|
2024-05-09 22:51:05 +02:00
|
|
|
|
|
|
|
|
fn update_accel_status(&self) {
|
|
|
|
|
let Some(application) = self.application() else {
|
|
|
|
|
log::error!("No application for window found");
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Only change status if active window
|
|
|
|
|
if self.is_active() {
|
|
|
|
|
if self.visible_dialog().is_some() {
|
|
|
|
|
// If AdwDialog is visible, remove global accels that are for the main window
|
|
|
|
|
ActionPartGlobal::remove_accels(&application);
|
|
|
|
|
} else {
|
|
|
|
|
// Add accels if viewing main window
|
|
|
|
|
ActionPartGlobal::add_accels(&application);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn show_about(&self) {
|
|
|
|
|
let about = crate::about::dialog().await;
|
2024-06-14 17:46:33 +02:00
|
|
|
about.present(Some(self));
|
2024-05-09 22:51:05 +02:00
|
|
|
}
|
2020-12-26 02:05:34 -08:00
|
|
|
}
|