2020-12-26 02:05:34 -08:00
|
|
|
// window.rs
|
|
|
|
|
//
|
|
|
|
|
// Copyright 2020 Christopher Davis <christopherdavis@gnome.org>
|
|
|
|
|
//
|
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
|
// it under the terms of the GNU General Public License as published by
|
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
// (at your option) any later version.
|
|
|
|
|
//
|
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
// GNU General Public License for more details.
|
|
|
|
|
//
|
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
|
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
//
|
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
|
2021-12-21 16:09:48 -08:00
|
|
|
use crate::deps::*;
|
2023-04-11 00:02:28 +02:00
|
|
|
use crate::util::gettext::*;
|
2021-12-21 16:09:48 -08:00
|
|
|
|
2023-04-06 21:14:27 +02:00
|
|
|
use crate::util::spawn;
|
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;
|
|
|
|
|
use gtk::CompositeTemplate;
|
|
|
|
|
|
2023-01-31 21:56:44 +01:00
|
|
|
use std::path::{Path, PathBuf};
|
2022-11-16 20:14:39 +01:00
|
|
|
|
2020-12-26 02:05:34 -08:00
|
|
|
use crate::config;
|
2023-02-06 19:14:01 +01:00
|
|
|
use crate::util::{self, Direction, Position};
|
2023-03-02 23:14:14 -05:00
|
|
|
use crate::widgets::{LpImage, LpImageView, LpPropertiesView};
|
2020-12-26 02:05:34 -08:00
|
|
|
|
|
|
|
|
mod imp {
|
|
|
|
|
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-01-23 01:10:15 +01:00
|
|
|
#[template(file = "../data/gtk/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-03-14 18:56:35 +01:00
|
|
|
pub(super) flap: TemplateChild<adw::Flap>,
|
2021-09-24 01:14:56 -07:00
|
|
|
#[template_child]
|
2023-03-14 18:56:35 +01:00
|
|
|
pub(super) headerbar: TemplateChild<gtk::HeaderBar>,
|
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-03-14 18:56:35 +01:00
|
|
|
pub(super) menu_button: TemplateChild<gtk::MenuButton>,
|
2021-09-24 01:14:56 -07:00
|
|
|
#[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>,
|
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>,
|
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);
|
2022-07-26 12:07:08 -04:00
|
|
|
Self::Type::bind_template_callbacks(klass);
|
2021-09-22 21:33:47 -07:00
|
|
|
|
|
|
|
|
// Set up actions
|
|
|
|
|
klass.install_action("win.toggle-fullscreen", None, move |win, _, _| {
|
2021-09-24 01:14:56 -07:00
|
|
|
win.toggle_fullscreen(!win.is_fullscreened());
|
2021-09-22 21:33:47 -07:00
|
|
|
});
|
|
|
|
|
|
2022-12-06 18:24:32 +01:00
|
|
|
klass.install_action("win.next", None, move |win, _, _| {
|
2023-02-06 19:14:01 +01:00
|
|
|
win.imp().image_view.navigate(Direction::Forward);
|
2022-12-06 18:24:32 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
klass.install_action("win.previous", None, move |win, _, _| {
|
2023-02-06 19:14:01 +01:00
|
|
|
win.imp().image_view.navigate(Direction::Back);
|
|
|
|
|
});
|
|
|
|
|
|
2023-02-06 21:07:23 +01:00
|
|
|
klass.install_action("win.image-right", None, move |win, _, _| {
|
|
|
|
|
if win.direction() == gtk::TextDirection::Rtl {
|
|
|
|
|
win.imp().image_view.navigate(Direction::Back);
|
|
|
|
|
} else {
|
|
|
|
|
win.imp().image_view.navigate(Direction::Forward);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
klass.install_action("win.image-left", None, move |win, _, _| {
|
|
|
|
|
if win.direction() == gtk::TextDirection::Rtl {
|
|
|
|
|
win.imp().image_view.navigate(Direction::Forward);
|
|
|
|
|
} else {
|
|
|
|
|
win.imp().image_view.navigate(Direction::Back);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2023-02-06 19:14:01 +01:00
|
|
|
klass.install_action("win.first", None, move |win, _, _| {
|
|
|
|
|
win.imp().image_view.jump(Position::First);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
klass.install_action("win.last", None, move |win, _, _| {
|
|
|
|
|
win.imp().image_view.jump(Position::Last);
|
2022-12-06 18:24:32 +01:00
|
|
|
});
|
|
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
klass.install_action("win.zoom-out", None, move |win, _, _| {
|
|
|
|
|
win.zoom_out();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
klass.install_action("win.zoom-in", None, move |win, _, _| {
|
|
|
|
|
win.zoom_in();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
klass.install_action("win.zoom-to", Some("d"), move |win, _, level| {
|
|
|
|
|
win.zoom_to(level.unwrap().get().unwrap());
|
|
|
|
|
});
|
|
|
|
|
|
2022-11-23 19:12:19 +01:00
|
|
|
klass.install_action("win.zoom-best-fit", None, move |win, _, _| {
|
|
|
|
|
win.zoom_best_fit();
|
|
|
|
|
});
|
|
|
|
|
|
2023-01-30 19:07:17 +01:00
|
|
|
klass.install_action("win.pan-up", None, move |win, _, _| {
|
|
|
|
|
win.pan(>k::PanDirection::Up);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
klass.install_action("win.pan-down", None, move |win, _, _| {
|
|
|
|
|
win.pan(>k::PanDirection::Down);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
klass.install_action("win.pan-left", None, move |win, _, _| {
|
|
|
|
|
win.pan(>k::PanDirection::Left);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
klass.install_action("win.pan-right", None, move |win, _, _| {
|
|
|
|
|
win.pan(>k::PanDirection::Right);
|
|
|
|
|
});
|
|
|
|
|
|
2022-11-23 19:12:19 +01:00
|
|
|
klass.install_action("win.leave-fullscreen", None, move |win, _, _| {
|
|
|
|
|
win.toggle_fullscreen(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
klass.install_action("win.toggle-properties", None, move |win, _, _| {
|
|
|
|
|
win.imp()
|
|
|
|
|
.properties_button
|
|
|
|
|
.set_active(!win.imp().properties_button.is_active());
|
|
|
|
|
});
|
|
|
|
|
|
2022-12-12 11:25:12 -05:00
|
|
|
klass.install_action_async("win.open", None, |win, _, _| async move {
|
|
|
|
|
win.pick_file().await;
|
2021-09-22 21:33:47 -07:00
|
|
|
});
|
|
|
|
|
|
2023-01-15 19:18:16 -05:00
|
|
|
klass.install_action_async("win.open-with", None, |win, _, _| async move {
|
|
|
|
|
win.open_with().await;
|
2021-09-22 21:33:47 -07:00
|
|
|
});
|
|
|
|
|
|
2022-11-05 00:57:18 +01:00
|
|
|
klass.install_action("win.rotate", Some("d"), move |win, _, angle| {
|
|
|
|
|
win.rotate_image(angle.unwrap().get().unwrap());
|
2022-11-03 17:40:06 +01:00
|
|
|
});
|
|
|
|
|
|
2023-01-22 12:18:43 -05:00
|
|
|
klass.install_action_async("win.set-background", None, |win, _, _| async move {
|
|
|
|
|
win.set_background().await;
|
2021-09-22 21:33:47 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
klass.install_action("win.print", None, move |win, _, _| {
|
|
|
|
|
win.print();
|
|
|
|
|
});
|
2021-12-28 16:38:36 -08:00
|
|
|
|
2022-01-08 14:02:16 +01:00
|
|
|
klass.install_action("win.copy", None, move |win, _, _| {
|
|
|
|
|
win.copy();
|
|
|
|
|
});
|
|
|
|
|
|
2023-01-31 21:56:44 +01:00
|
|
|
klass.install_action_async("win.trash", None, |win, _, _| async move {
|
|
|
|
|
win.trash().await;
|
|
|
|
|
});
|
|
|
|
|
|
2021-12-28 16:38:36 -08:00
|
|
|
klass.install_action("win.show-toast", Some("(si)"), move |win, _, var| {
|
2022-06-29 20:09:45 -04:00
|
|
|
if let Some((ref toast, i)) = var.and_then(|v| v.get::<(String, i32)>()) {
|
2021-12-28 16:38:36 -08:00
|
|
|
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) {
|
2023-02-12 02:07:42 +01:00
|
|
|
let obj = self.obj();
|
2022-11-02 21:11:43 +01:00
|
|
|
|
|
|
|
|
self.parent_constructed();
|
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);
|
|
|
|
|
|
2021-09-22 21:33:47 -07:00
|
|
|
obj.set_actions_enabled(false);
|
2023-03-02 23:14:14 -05:00
|
|
|
|
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.
|
2023-03-04 11:47:07 -05:00
|
|
|
current_image_signals.connect_bind_local(glib::clone!(@weak obj => move |_, _| {
|
2023-03-02 23:14:14 -05:00
|
|
|
obj.on_zoom_status_changed()
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
let win = &*obj;
|
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.
|
|
|
|
|
glib::closure_local!(@watch win => move |_: &LpImage, _: &glib::ParamSpec| {
|
|
|
|
|
win.on_zoom_status_changed();
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
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,
|
|
|
|
|
glib::closure_local!(@watch win => move |_: &LpImage, _: &glib::ParamSpec| {
|
|
|
|
|
win.on_zoom_status_changed();
|
|
|
|
|
}),
|
|
|
|
|
);
|
2022-11-28 03:23:06 +01:00
|
|
|
|
2023-03-04 11:47:07 -05:00
|
|
|
current_image_signals.connect_closure(
|
|
|
|
|
"notify::image-size",
|
|
|
|
|
false,
|
|
|
|
|
glib::closure_local!(@watch win => move |_: &LpImage, _: &glib::ParamSpec| {
|
|
|
|
|
win.image_size_ready();
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
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,
|
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
|
|
|
glib::closure_local!(@watch win => move |_: &LpImage, _: &glib::ParamSpec| {
|
2023-03-04 11:47:07 -05:00
|
|
|
win.image_error();
|
2023-01-23 01:10:15 +01: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
|
|
|
self.image_view.connect_notify_local(
|
|
|
|
|
Some("current-page"),
|
|
|
|
|
glib::clone!(@weak obj => move |_, _| {
|
|
|
|
|
obj.images_available();
|
|
|
|
|
}),
|
|
|
|
|
);
|
2023-03-04 11:47:07 -05:00
|
|
|
|
2022-12-06 18:24:32 +01:00
|
|
|
// action win.previous status
|
|
|
|
|
self.image_view.connect_notify_local(
|
|
|
|
|
Some("is-previous-available"),
|
|
|
|
|
glib::clone!(@weak obj => move |_, _| {
|
|
|
|
|
obj.action_set_enabled(
|
|
|
|
|
"win.previous",
|
|
|
|
|
obj.imp().image_view.is_previous_available(),
|
|
|
|
|
);
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// action win.next status
|
|
|
|
|
self.image_view.connect_notify_local(
|
|
|
|
|
Some("is-next-available"),
|
|
|
|
|
glib::clone!(@weak obj => move |_, _| {
|
|
|
|
|
obj.action_set_enabled(
|
|
|
|
|
"win.next",
|
|
|
|
|
obj.imp().image_view.is_next_available(),
|
|
|
|
|
);
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
2020-12-26 02:05:34 -08:00
|
|
|
self.status_page
|
|
|
|
|
.set_icon_name(Some(&format!("{}-symbolic", config::APP_ID)));
|
|
|
|
|
|
|
|
|
|
// Set help overlay
|
2021-09-28 17:44:26 -07:00
|
|
|
let builder = gtk::Builder::from_resource("/org/gnome/Loupe/gtk/help_overlay.ui");
|
2021-06-27 15:28:35 -07:00
|
|
|
let help_overlay = builder.object("help_overlay").unwrap();
|
2020-12-26 02:05:34 -08:00
|
|
|
obj.set_help_overlay(Some(&help_overlay));
|
|
|
|
|
|
2022-07-26 12:07:08 -04:00
|
|
|
self.drop_target.set_types(&[gdk::FileList::static_type()]);
|
|
|
|
|
|
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.
|
2021-12-27 15:03:31 -08:00
|
|
|
self.drop_target.connect_drop(
|
|
|
|
|
clone!(@weak obj => @default-return 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.
|
|
|
|
|
//
|
|
|
|
|
// 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()`.
|
2022-11-04 19:45:28 +01:00
|
|
|
let list: gdk::FileList = match value.get() {
|
|
|
|
|
Ok(list) => list,
|
|
|
|
|
Err(err) => {
|
|
|
|
|
log::error!("Issue with drop value: {err}");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
};
|
2021-12-27 15:03:31 -08:00
|
|
|
|
|
|
|
|
// TODO: Handle this like EOG and make a "directory" out of the given files
|
|
|
|
|
let file = list.files().get(0).unwrap().clone();
|
|
|
|
|
let info = util::query_attributes(
|
|
|
|
|
&file,
|
2021-12-28 16:38:36 -08:00
|
|
|
vec![
|
2023-02-12 02:07:42 +01:00
|
|
|
gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
|
|
|
|
|
gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
|
2021-12-28 16:38:36 -08:00
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
.expect("Could not query file info");
|
2021-12-27 15:03:31 -08:00
|
|
|
|
|
|
|
|
if info
|
|
|
|
|
.content_type()
|
|
|
|
|
.map(|t| t.to_string())
|
|
|
|
|
.filter(|t| t.starts_with("image/"))
|
|
|
|
|
.is_some() {
|
2023-04-12 01:11:59 +02:00
|
|
|
obj.set_image_from_file(&file);
|
2021-12-27 15:03:31 -08:00
|
|
|
} else {
|
2021-12-28 16:38:36 -08:00
|
|
|
obj.show_toast(
|
2023-04-11 00:02:28 +02:00
|
|
|
gettext_f("“{}” is not a valid image.", &[&info.display_name()]),
|
2021-12-28 16:38:36 -08:00
|
|
|
adw::ToastPriority::High,
|
|
|
|
|
);
|
2021-12-27 15:03:31 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
true
|
|
|
|
|
}),
|
|
|
|
|
);
|
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
|
|
|
}
|
|
|
|
|
|
2022-07-26 12:07:08 -04:00
|
|
|
#[gtk::template_callbacks]
|
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) {
|
2021-12-29 14:02:48 -08:00
|
|
|
let imp = self.imp();
|
2021-09-24 01:14:56 -07:00
|
|
|
|
|
|
|
|
if fullscreen {
|
|
|
|
|
imp.headerbar.add_css_class("osd");
|
2022-07-26 11:31:11 -04:00
|
|
|
imp.properties_view.add_css_class("osd")
|
2021-09-24 01:14:56 -07:00
|
|
|
} else {
|
|
|
|
|
imp.headerbar.remove_css_class("osd");
|
2022-07-26 11:31:11 -04:00
|
|
|
imp.properties_view.remove_css_class("osd")
|
2021-09-24 01:14:56 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.set_fullscreened(fullscreen);
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-10 19:39:23 +01:00
|
|
|
fn zoom_out(&self) {
|
|
|
|
|
self.imp().image_view.zoom_out();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn zoom_in(&self) {
|
|
|
|
|
self.imp().image_view.zoom_in();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn zoom_to(&self, level: f64) {
|
|
|
|
|
self.imp().image_view.zoom_to(level);
|
|
|
|
|
}
|
|
|
|
|
|
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) {
|
|
|
|
|
let filter_store = gio::ListStore::new(gtk::FileFilter::static_type());
|
2020-12-26 02:05:34 -08:00
|
|
|
|
|
|
|
|
let filter = gtk::FileFilter::new();
|
2021-12-22 18:17:38 -08:00
|
|
|
filter.set_property("name", &String::from("Supported image files"));
|
2020-12-26 02:05:34 -08:00
|
|
|
filter.add_mime_type("image/*");
|
2022-12-12 11:25:12 -05:00
|
|
|
filter_store.append(&filter);
|
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"))
|
2022-12-12 11:25:12 -05:00
|
|
|
.filters(&filter_store)
|
|
|
|
|
.modal(true)
|
|
|
|
|
.build();
|
2020-12-26 02:05:34 -08:00
|
|
|
|
2023-01-14 14:23:54 -05:00
|
|
|
if let Ok(file) = chooser.open_future(Some(self)).await {
|
2023-04-12 01:11:59 +02:00
|
|
|
self.set_image_from_file(&file);
|
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));
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-08 14:02:16 +01:00
|
|
|
fn copy(&self) {
|
|
|
|
|
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-04-12 01:11:59 +02:00
|
|
|
let (Some(file), Some(path)) = (image_view.current_file(), image_view.current_file().and_then(|x| x.path()))
|
2023-01-31 21:56:44 +01:00
|
|
|
else { log::error!("No file to trash"); return; };
|
|
|
|
|
|
|
|
|
|
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-01-31 21:56:44 +01:00
|
|
|
.build();
|
|
|
|
|
toast.connect_button_clicked(glib::clone!(@weak self as win => move |_| {
|
|
|
|
|
let path = path.clone();
|
2023-04-06 21:14:27 +02:00
|
|
|
spawn(async move {
|
2023-01-31 21:56:44 +01:00
|
|
|
let result = crate::util::untrash(&path).await;
|
|
|
|
|
match result {
|
2023-04-12 01:11:59 +02:00
|
|
|
Ok(()) => win.image_view().set_image_from_file(&gio::File::for_path(&path)),
|
2023-01-31 21:56:44 +01:00
|
|
|
Err(err) => {
|
|
|
|
|
log::error!("Failed to untrash {path:?}: {err}");
|
|
|
|
|
win.show_toast(
|
2023-04-11 00:02:28 +02:00
|
|
|
gettext("Failed to restore image from trash"),
|
2023-01-31 21:56:44 +01:00
|
|
|
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>() {
|
|
|
|
|
self.delete(&path).await;
|
|
|
|
|
} 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,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Permanently delete image
|
|
|
|
|
///
|
2023-03-09 04:13:48 +01:00
|
|
|
/// Fallback for when trash not available
|
2023-01-31 21:56:44 +01:00
|
|
|
async fn delete(&self, path: &Path) {
|
|
|
|
|
let dialog = adw::MessageDialog::builder()
|
|
|
|
|
.modal(true)
|
|
|
|
|
.transient_for(self)
|
2023-04-11 00:02:28 +02:00
|
|
|
.heading(gettext("Permanently Delete Image?"))
|
|
|
|
|
.body(gettext_f(
|
2023-01-31 21:56:44 +01:00
|
|
|
"The image “{}” can only be deleted permanently.",
|
|
|
|
|
&[&PathBuf::from(&path.file_name().unwrap_or_default())
|
|
|
|
|
.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);
|
|
|
|
|
|
2023-02-12 02:07:42 +01:00
|
|
|
if "delete" == dialog.choose_future().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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn image_view(&self) -> LpImageView {
|
|
|
|
|
self.imp().image_view.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
|
|
|
}
|
|
|
|
|
|
2023-04-12 01:11:59 +02:00
|
|
|
pub fn set_image_from_file(&self, file: &gio::File) {
|
2021-12-29 14:02:48 -08:00
|
|
|
let imp = self.imp();
|
2020-12-26 02:05:34 -08:00
|
|
|
|
2023-04-12 01:11:59 +02:00
|
|
|
log::debug!("Loading file: {}", file.uri());
|
|
|
|
|
imp.image_view.set_image_from_file(file);
|
2023-01-23 01:10:15 +01:00
|
|
|
self.set_actions_enabled(true);
|
2020-12-26 02:05:34 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn set_actions_enabled(&self, enabled: bool) {
|
2021-09-22 21:33:47 -07:00
|
|
|
self.action_set_enabled("win.open-with", enabled);
|
2022-08-17 21:31:36 -03:00
|
|
|
self.action_set_enabled("win.set-background", enabled);
|
2021-09-22 21:33:47 -07:00
|
|
|
self.action_set_enabled("win.toggle-fullscreen", enabled);
|
|
|
|
|
self.action_set_enabled("win.print", enabled);
|
2022-11-05 00:57:18 +01:00
|
|
|
self.action_set_enabled("win.rotate", enabled);
|
2022-01-08 14:02:16 +01:00
|
|
|
self.action_set_enabled("win.copy", enabled);
|
2023-01-31 21:56:44 +01:00
|
|
|
self.action_set_enabled("win.trash", enabled);
|
2022-11-23 19:12:19 +01:00
|
|
|
self.action_set_enabled("win.zoom-best-fit", enabled);
|
|
|
|
|
self.action_set_enabled("win.zoom-to", enabled);
|
|
|
|
|
self.action_set_enabled("win.toggle-properties", enabled);
|
2020-12-26 02:05:34 -08:00
|
|
|
}
|
2021-03-02 16:39:03 -08:00
|
|
|
|
2023-02-20 23:44:17 +01:00
|
|
|
/// Handles change in availability of images
|
2023-01-31 21:56:44 +01:00
|
|
|
fn images_available(&self) {
|
2023-01-23 01:10:15 +01:00
|
|
|
let imp = self.imp();
|
|
|
|
|
|
|
|
|
|
if imp.image_view.current_page().is_some() {
|
|
|
|
|
imp.stack.set_visible_child(&*imp.image_view);
|
|
|
|
|
imp.image_view.grab_focus();
|
|
|
|
|
} else {
|
|
|
|
|
imp.stack.set_visible_child(&*imp.status_page);
|
|
|
|
|
imp.status_page.grab_focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-16 20:14:39 +01:00
|
|
|
pub fn image_size_ready(&self) {
|
|
|
|
|
// 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());
|
|
|
|
|
|
|
|
|
|
if let Some(image) = image {
|
|
|
|
|
if image.image_size() > (0, 0) {
|
|
|
|
|
log::debug!("Showing window because image size is ready");
|
|
|
|
|
// this let's the window determine the default size from LpImage's natural size
|
|
|
|
|
self.set_default_size(-1, -1);
|
|
|
|
|
self.present();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
if let Some(page) = current_page {
|
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
|
|
|
if page.image().error().is_some() {
|
2023-02-20 23:44:17 +01:00
|
|
|
log::debug!("Showing window because loading image failed");
|
2022-11-16 20:14:39 +01:00
|
|
|
self.present();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
self.action_set_enabled("win.zoom-out", can_zoom_out);
|
|
|
|
|
self.action_set_enabled("win.zoom-in", can_zoom_in);
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-26 12:07:08 -04:00
|
|
|
// In the LpWindow UI file we define a `gtk::Expression`s
|
|
|
|
|
// that is a closure. This closure takes the current `gio::File`
|
|
|
|
|
// and processes it to return a window title.
|
|
|
|
|
//
|
|
|
|
|
// In this function we chain `Option`s with `and_then()` in order
|
|
|
|
|
// to handle optional results with a fallback, without needing to
|
|
|
|
|
// have multiple `match` or `if let` branches, and without needing
|
|
|
|
|
// to unwrap.
|
|
|
|
|
#[template_callback]
|
2023-04-12 04:13:30 +02:00
|
|
|
fn window_title(&self, file: Option<&gio::File>) -> String {
|
2023-01-23 01:10:15 +01:00
|
|
|
// ensure that templates are initialized
|
2023-04-12 04:13:30 +02:00
|
|
|
if file.is_none() {
|
2023-04-11 00:02:28 +02:00
|
|
|
gettext("Loupe")
|
2023-01-23 01:10:15 +01:00
|
|
|
} else {
|
|
|
|
|
self.imp()
|
|
|
|
|
.image_view
|
|
|
|
|
.current_file()
|
|
|
|
|
.and_then(|f| util::get_file_display_name(&f)) // If the file exists, get display name
|
2023-04-11 00:02:28 +02:00
|
|
|
.unwrap_or_else(|| gettext("Loupe")) // Return that or the default if there's nothing
|
2023-01-23 01:10:15 +01:00
|
|
|
}
|
2022-07-26 12:07:08 -04:00
|
|
|
}
|
2022-07-26 11:31:11 -04:00
|
|
|
|
|
|
|
|
// We also have a closure that returns `adw::FlapFoldPolicy`.
|
|
|
|
|
// if we aren't fullscreened, or if the properties are revealed,
|
|
|
|
|
// we unfold the main flap. Otherwise we're always folded.
|
|
|
|
|
#[template_callback]
|
|
|
|
|
fn fold_policy(&self, fullscreened: bool, properties_revealed: bool) -> adw::FlapFoldPolicy {
|
|
|
|
|
if !fullscreened || properties_revealed {
|
|
|
|
|
adw::FlapFoldPolicy::Never
|
|
|
|
|
} else {
|
|
|
|
|
adw::FlapFoldPolicy::Always
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-26 02:05:34 -08:00
|
|
|
}
|