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::*;
|
2022-08-05 18:54:04 -04:00
|
|
|
use crate::i18n::*;
|
2021-12-21 16:09:48 -08:00
|
|
|
|
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 gtk_macros::spawn;
|
2020-12-26 02:05:34 -08:00
|
|
|
|
2022-11-16 20:14:39 +01:00
|
|
|
use std::cell::RefCell;
|
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};
|
2022-11-10 19:39:23 +01:00
|
|
|
use crate::widgets::{LpImage, LpImagePage, 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]
|
2021-12-21 12:15:46 -08:00
|
|
|
pub flap: TemplateChild<adw::Flap>,
|
2021-09-24 01:14:56 -07:00
|
|
|
#[template_child]
|
2020-12-26 02:05:34 -08:00
|
|
|
pub headerbar: TemplateChild<gtk::HeaderBar>,
|
|
|
|
|
#[template_child]
|
2022-11-23 19:12:19 +01:00
|
|
|
pub properties_button: TemplateChild<gtk::ToggleButton>,
|
|
|
|
|
#[template_child]
|
2021-09-24 01:14:56 -07:00
|
|
|
pub menu_button: TemplateChild<gtk::MenuButton>,
|
|
|
|
|
#[template_child]
|
2021-12-28 16:38:36 -08:00
|
|
|
pub toast_overlay: TemplateChild<adw::ToastOverlay>,
|
|
|
|
|
#[template_child]
|
2020-12-26 02:05:34 -08:00
|
|
|
pub stack: TemplateChild<gtk::Stack>,
|
|
|
|
|
#[template_child]
|
2021-12-21 12:15:46 -08:00
|
|
|
pub status_page: TemplateChild<adw::StatusPage>,
|
2020-12-26 02:05:34 -08:00
|
|
|
#[template_child]
|
2021-12-21 10:55:02 -08:00
|
|
|
pub image_view: TemplateChild<LpImageView>,
|
2021-12-27 15:03:31 -08:00
|
|
|
#[template_child]
|
2022-07-26 11:31:11 -04:00
|
|
|
pub properties_view: TemplateChild<LpPropertiesView>,
|
|
|
|
|
#[template_child]
|
2021-12-27 15:03:31 -08:00
|
|
|
pub drop_target: TemplateChild<gtk::DropTarget>,
|
2022-11-16 20:14:39 +01:00
|
|
|
|
|
|
|
|
pub watch_image_size: RefCell<Option<gtk::ExpressionWatch>>,
|
|
|
|
|
pub watch_image_error: RefCell<Option<gtk::ExpressionWatch>>,
|
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) {
|
|
|
|
|
let obj = self.instance();
|
|
|
|
|
|
|
|
|
|
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);
|
2022-11-10 19:39:23 +01:00
|
|
|
self.image_view
|
2023-01-23 01:10:15 +01:00
|
|
|
.property_expression("current-page")
|
2022-11-10 19:39:23 +01:00
|
|
|
.chain_property::<LpImagePage>("image")
|
|
|
|
|
.chain_property::<LpImage>("best-fit")
|
|
|
|
|
.watch(
|
|
|
|
|
glib::Object::NONE,
|
2022-11-23 13:51:27 -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.
|
2022-11-10 19:39:23 +01:00
|
|
|
glib::clone!(@weak obj => move || {
|
|
|
|
|
let enabled = obj
|
|
|
|
|
.imp()
|
|
|
|
|
.image_view
|
|
|
|
|
.current_page()
|
|
|
|
|
.map(|page| !page.image().is_best_fit())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
obj.action_set_enabled("win.zoom-out", enabled);
|
|
|
|
|
}),
|
|
|
|
|
);
|
2020-12-26 02:05:34 -08:00
|
|
|
|
2022-11-28 03:23:06 +01:00
|
|
|
// disable zoom-in if at maximum zoom level
|
|
|
|
|
self.image_view
|
2023-01-23 01:10:15 +01:00
|
|
|
.property_expression("current-page")
|
2022-11-28 03:23:06 +01:00
|
|
|
.chain_property::<LpImagePage>("image")
|
|
|
|
|
.chain_property::<LpImage>("is-max-zoom")
|
|
|
|
|
.watch(
|
|
|
|
|
glib::Object::NONE,
|
|
|
|
|
glib::clone!(@weak obj => move || {
|
|
|
|
|
let enabled = obj
|
|
|
|
|
.imp()
|
|
|
|
|
.image_view
|
|
|
|
|
.current_page()
|
|
|
|
|
.map(|page| !page.image().is_max_zoom())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
obj.action_set_enabled("win.zoom-in", enabled);
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
self.image_view.connect_notify_local(
|
|
|
|
|
Some("current-page"),
|
|
|
|
|
glib::clone!(@weak obj => move |_, _| {
|
|
|
|
|
obj.images_available();
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
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![
|
|
|
|
|
&gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
|
|
|
|
|
&gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
.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-01-23 01:10:15 +01:00
|
|
|
obj.set_image_from_path(&file.path().unwrap());
|
2021-12-27 15:03:31 -08:00
|
|
|
} else {
|
2021-12-28 16:38:36 -08:00
|
|
|
obj.show_toast(
|
2022-11-04 20:48:32 +01:00
|
|
|
i18n_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()
|
|
|
|
|
.title(&i18n("Open Image"))
|
|
|
|
|
.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-01-23 01:10:15 +01:00
|
|
|
self.set_image_from_path(&file.path().unwrap());
|
|
|
|
|
} else {
|
|
|
|
|
// TODO: ui error
|
|
|
|
|
log::error!("Failed to choose file");
|
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-01-23 01:10:15 +01:00
|
|
|
if let Some(ref file) = imp.image_view.current_path().map(gio::File::for_path) {
|
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 {
|
2022-08-05 18:54:04 -04:00
|
|
|
self.show_toast(i18n("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();
|
|
|
|
|
let (Some(file), Some(path)) = (image_view.current_file(), image_view.current_path())
|
|
|
|
|
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()
|
|
|
|
|
.title(&i18n("Image moved to trash"))
|
|
|
|
|
.button_label(&i18n("Undo"))
|
|
|
|
|
.build();
|
|
|
|
|
toast.connect_button_clicked(glib::clone!(@weak self as win => move |_| {
|
|
|
|
|
let path = path.clone();
|
|
|
|
|
spawn!(async move {
|
|
|
|
|
let result = crate::util::untrash(&path).await;
|
|
|
|
|
match result {
|
|
|
|
|
Ok(()) => win.image_view().set_image_from_path(&path),
|
|
|
|
|
Err(err) => {
|
|
|
|
|
log::error!("Failed to untrash {path:?}: {err}");
|
|
|
|
|
win.show_toast(
|
|
|
|
|
i18n("Failed to restore image from trash"),
|
|
|
|
|
adw::ToastPriority::High,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}));
|
|
|
|
|
self.imp().toast_overlay.add_toast(&toast);
|
|
|
|
|
}
|
|
|
|
|
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(
|
|
|
|
|
i18n("Failed to move image to trash"),
|
|
|
|
|
adw::ToastPriority::Normal,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Permanently delete image
|
|
|
|
|
///
|
|
|
|
|
/// Fallback for when trash no available
|
|
|
|
|
async fn delete(&self, path: &Path) {
|
|
|
|
|
let dialog = adw::MessageDialog::builder()
|
|
|
|
|
.modal(true)
|
|
|
|
|
.transient_for(self)
|
|
|
|
|
.heading(&i18n("Permanently Delete Image?"))
|
|
|
|
|
.body(&i18n_f(
|
|
|
|
|
"The image “{}” can only be deleted permanently.",
|
|
|
|
|
&[&PathBuf::from(&path.file_name().unwrap_or_default())
|
|
|
|
|
.display()
|
|
|
|
|
.to_string()],
|
|
|
|
|
))
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
dialog.add_responses(&[("cancel", &i18n("Cancel")), ("delete", &i18n("Delete"))]);
|
|
|
|
|
dialog.set_response_appearance("delete", adw::ResponseAppearance::Destructive);
|
|
|
|
|
|
|
|
|
|
if "delete" == dialog.run_future().await {
|
|
|
|
|
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}");
|
|
|
|
|
self.show_toast(i18n("Failed to delete image"), adw::ToastPriority::Normal);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
imp.toast_overlay.add_toast(&toast);
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
pub fn set_image_from_path(&self, path: &Path) {
|
2021-12-29 14:02:48 -08:00
|
|
|
let imp = self.imp();
|
2020-12-26 02:05:34 -08:00
|
|
|
|
2023-01-23 01:10:15 +01:00
|
|
|
log::debug!("Loading file: {path:?}");
|
|
|
|
|
imp.image_view.set_image_from_path(path);
|
|
|
|
|
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-01-31 21:56:44 +01:00
|
|
|
/// Handles change in availablilty of images
|
|
|
|
|
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() {
|
|
|
|
|
self.disconnect_present_watches();
|
|
|
|
|
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.disconnect_present_watches();
|
|
|
|
|
self.present();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn image_error(&self) {
|
|
|
|
|
if self.is_visible() {
|
|
|
|
|
self.disconnect_present_watches();
|
|
|
|
|
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 {
|
|
|
|
|
if page.error().is_some() {
|
|
|
|
|
log::debug!("Showin window because loading image failed");
|
|
|
|
|
self.disconnect_present_watches();
|
|
|
|
|
self.present();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn disconnect_present_watches(&self) {
|
|
|
|
|
if let Some(watch) = self.imp().watch_image_size.take() {
|
|
|
|
|
watch.unwatch();
|
|
|
|
|
}
|
|
|
|
|
if let Some(watch) = self.imp().watch_image_error.take() {
|
|
|
|
|
watch.unwatch();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-02 16:39:03 -08:00
|
|
|
// Adapted from https://gitlab.gnome.org/GNOME/eog/-/blob/master/src/eog-window.c:eog_window_obtain_desired_size
|
|
|
|
|
pub fn resize_from_dimensions(&self, img_width: i32, img_height: i32) {
|
2021-12-29 14:02:48 -08:00
|
|
|
let imp = self.imp();
|
2021-03-02 16:39:03 -08:00
|
|
|
let mut final_width = img_width;
|
|
|
|
|
let mut final_height = img_height;
|
|
|
|
|
|
2021-06-27 15:28:35 -07:00
|
|
|
let header_height = imp.headerbar.height();
|
2021-03-02 16:39:03 -08:00
|
|
|
|
|
|
|
|
// Ensure the window surface exists
|
2021-06-27 15:28:35 -07:00
|
|
|
if !self.is_realized() {
|
2022-11-02 21:11:43 +01:00
|
|
|
WidgetExt::realize(self);
|
2021-03-02 16:39:03 -08:00
|
|
|
}
|
|
|
|
|
|
2021-06-27 15:28:35 -07:00
|
|
|
let display = gdk::Display::default().unwrap();
|
2022-01-08 22:18:12 -08:00
|
|
|
let monitor = display.monitor_at_surface(&self.native().unwrap().surface());
|
2021-06-27 15:28:35 -07:00
|
|
|
let monitor_geometry = monitor.geometry();
|
2021-03-02 16:39:03 -08:00
|
|
|
|
2021-12-22 18:17:38 -08:00
|
|
|
let monitor_width = monitor_geometry.width();
|
|
|
|
|
let monitor_height = monitor_geometry.height();
|
2021-03-02 16:39:03 -08:00
|
|
|
|
|
|
|
|
if img_width > monitor_width || img_height + header_height > monitor_height {
|
|
|
|
|
let width_factor = (monitor_width as f32 * 0.85) / img_width as f32;
|
|
|
|
|
let height_factor =
|
|
|
|
|
(monitor_height as f32 * 0.85 - header_height as f32) / img_height as f32;
|
|
|
|
|
let factor = width_factor.min(height_factor);
|
|
|
|
|
|
|
|
|
|
final_width = (final_width as f32 * factor).round() as i32;
|
|
|
|
|
final_height = (final_height as f32 * factor).round() as i32;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.set_default_size(final_width, final_height);
|
|
|
|
|
log::debug!("Window resized to {} x {}", final_width, final_height);
|
|
|
|
|
}
|
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-01-23 01:10:15 +01:00
|
|
|
fn window_title(&self, path: glib::variant::Variant) -> String {
|
|
|
|
|
// ensure that templates are initialized
|
|
|
|
|
if path.as_maybe().is_none() {
|
|
|
|
|
i18n("Loupe")
|
|
|
|
|
} else {
|
|
|
|
|
self.imp()
|
|
|
|
|
.image_view
|
|
|
|
|
.current_file()
|
|
|
|
|
.and_then(|f| util::get_file_display_name(&f)) // If the file exists, get display name
|
|
|
|
|
.unwrap_or_else(|| i18n("Loupe")) // Return that or the default if there's nothing
|
|
|
|
|
}
|
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
|
|
|
}
|