2024-09-14 00:48:46 +02:00
|
|
|
// Copyright (c) 2024 Sophie Herold
|
|
|
|
|
//
|
|
|
|
|
// 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-12-16 21:27:20 +01:00
|
|
|
use std::ops::Deref;
|
|
|
|
|
|
2024-09-14 00:48:46 +02:00
|
|
|
use adw::prelude::*;
|
|
|
|
|
use adw::subclass::prelude::*;
|
2024-09-14 21:34:29 +02:00
|
|
|
use gio::glib::VariantTy;
|
2024-09-14 00:48:46 +02:00
|
|
|
use gtk::CompositeTemplate;
|
2024-12-16 21:27:20 +01:00
|
|
|
use strum::IntoEnumIterator;
|
2024-09-14 00:48:46 +02:00
|
|
|
|
|
|
|
|
use crate::config;
|
|
|
|
|
use crate::deps::*;
|
2024-12-18 01:29:14 +01:00
|
|
|
use crate::util::gettext::*;
|
2024-12-16 21:27:20 +01:00
|
|
|
use crate::util::Direction;
|
2024-09-15 21:06:23 +02:00
|
|
|
use crate::widgets::{LpEditWindow, LpImageView, LpImageWindow};
|
2024-09-14 00:48:46 +02:00
|
|
|
|
2024-12-18 01:29:14 +01:00
|
|
|
use super::error_details::ErrorType;
|
|
|
|
|
use super::LpErrorDetails;
|
|
|
|
|
|
2024-09-14 00:48:46 +02:00
|
|
|
/// Show window after X milliseconds even if image dimensions are not known yet
|
|
|
|
|
const SHOW_WINDOW_AFTER: u64 = 2000;
|
|
|
|
|
|
|
|
|
|
mod imp {
|
2024-12-14 00:38:04 +01:00
|
|
|
use std::cell::Cell;
|
|
|
|
|
|
2024-09-14 00:48:46 +02: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.
|
|
|
|
|
//
|
|
|
|
|
// 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.
|
2024-12-14 00:38:04 +01:00
|
|
|
#[derive(Default, Debug, CompositeTemplate, glib::Properties)]
|
2024-09-14 00:48:46 +02:00
|
|
|
#[template(file = "window.ui")]
|
2024-12-14 00:38:04 +01:00
|
|
|
#[properties(wrapper_type=super::LpWindow)]
|
2024-09-14 00:48:46 +02:00
|
|
|
pub struct LpWindow {
|
|
|
|
|
#[template_child]
|
|
|
|
|
pub(super) toast_overlay: TemplateChild<adw::ToastOverlay>,
|
2024-09-14 21:34:29 +02:00
|
|
|
#[template_child]
|
|
|
|
|
pub(super) stack: TemplateChild<adw::ViewStack>,
|
2024-09-14 00:48:46 +02:00
|
|
|
|
|
|
|
|
#[template_child]
|
2024-09-14 21:39:53 +02:00
|
|
|
pub(super) image_window: TemplateChild<LpImageWindow>,
|
2024-09-14 21:34:29 +02:00
|
|
|
#[template_child]
|
2024-09-15 21:06:23 +02:00
|
|
|
pub(super) edit_window_child: TemplateChild<adw::Bin>,
|
2024-12-14 00:38:04 +01:00
|
|
|
|
|
|
|
|
#[property(get, set)]
|
|
|
|
|
narrow_layout: Cell<bool>,
|
|
|
|
|
#[property(get, set)]
|
|
|
|
|
wide_layout: Cell<bool>,
|
2024-09-14 00:48:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[glib::object_subclass]
|
|
|
|
|
impl ObjectSubclass for LpWindow {
|
|
|
|
|
const NAME: &'static str = "LpWindow";
|
|
|
|
|
type Type = super::LpWindow;
|
|
|
|
|
type ParentType = adw::ApplicationWindow;
|
|
|
|
|
|
|
|
|
|
fn class_init(klass: &mut Self::Class) {
|
|
|
|
|
Self::bind_template(klass);
|
|
|
|
|
|
|
|
|
|
klass.add_binding_action(
|
|
|
|
|
gdk::Key::question,
|
|
|
|
|
gdk::ModifierType::CONTROL_MASK,
|
|
|
|
|
"win.show-help-overlay",
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
klass.install_action(
|
|
|
|
|
"win.show-toast",
|
|
|
|
|
Some(VariantTy::TUPLE),
|
|
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
2024-12-16 21:27:20 +01:00
|
|
|
|
2024-12-18 01:29:14 +01:00
|
|
|
WindowAction::init_actions_and_bindings(klass);
|
|
|
|
|
|
2024-12-16 21:27:20 +01:00
|
|
|
ActionPartGlobal::init_actions_and_bindings(klass);
|
2024-09-14 00:48:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
|
|
|
|
obj.init_template();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-14 00:38:04 +01:00
|
|
|
#[glib::derived_properties]
|
2024-09-14 00:48:46 +02:00
|
|
|
impl ObjectImpl for LpWindow {
|
|
|
|
|
fn constructed(&self) {
|
|
|
|
|
self.parent_constructed();
|
|
|
|
|
|
|
|
|
|
let obj = self.obj();
|
|
|
|
|
|
|
|
|
|
if config::APP_ID.ends_with(".Devel") {
|
|
|
|
|
obj.add_css_class("devel");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Limit effect of modal dialogs to this window
|
|
|
|
|
// and keeps the others usable
|
|
|
|
|
gtk::WindowGroup::new().add_window(&*obj);
|
|
|
|
|
|
|
|
|
|
glib::timeout_add_local_once(
|
|
|
|
|
std::time::Duration::from_millis(SHOW_WINDOW_AFTER),
|
|
|
|
|
glib::clone!(
|
|
|
|
|
#[weak]
|
|
|
|
|
obj,
|
|
|
|
|
move || if !obj.is_visible() {
|
|
|
|
|
obj.present()
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
obj.connect_map(|win| {
|
|
|
|
|
win.resize_default();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl WidgetImpl for LpWindow {}
|
|
|
|
|
impl WindowImpl for LpWindow {}
|
|
|
|
|
impl ApplicationWindowImpl for LpWindow {}
|
|
|
|
|
impl AdwApplicationWindowImpl for LpWindow {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
glib::wrapper! {
|
|
|
|
|
pub struct LpWindow(ObjectSubclass<imp::LpWindow>)
|
|
|
|
|
@extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, adw::ApplicationWindow,
|
|
|
|
|
@implements gio::ActionMap, gio::ActionGroup, gtk::Native;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl LpWindow {
|
|
|
|
|
pub fn new<A: IsA<gtk::Application>>(app: &A) -> Self {
|
|
|
|
|
glib::Object::builder().property("application", app).build()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn toggle_fullscreen(&self, fullscreen: bool) {
|
|
|
|
|
self.set_fullscreened(fullscreen);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn resize_default(&self) {
|
|
|
|
|
let imp = self.imp();
|
|
|
|
|
|
2024-09-15 21:06:23 +02:00
|
|
|
if self
|
2024-09-14 00:48:46 +02:00
|
|
|
.image_view()
|
|
|
|
|
.current_image()
|
|
|
|
|
.is_some_and(|img| img.image_size_available())
|
|
|
|
|
{
|
2024-09-14 21:39:53 +02:00
|
|
|
let shows_properties = imp.image_window.properties_button().is_active();
|
2024-09-14 00:48:46 +02:00
|
|
|
|
|
|
|
|
let (_, window_natural_width, _, _) = self.measure(gtk::Orientation::Horizontal, -1);
|
|
|
|
|
let (_, window_natural_height, _, _) = self.measure(gtk::Orientation::Vertical, -1);
|
|
|
|
|
|
|
|
|
|
// These have to be in sync with the "conditions" for the "overlay-properties"
|
|
|
|
|
// breakpoint
|
|
|
|
|
let min_width_for_overlay = adw::LengthUnit::Sp.to_px(590., None).ceil() as i32;
|
|
|
|
|
let min_height_for_overlay = adw::LengthUnit::Sp.to_px(550., None).ceil() as i32;
|
|
|
|
|
|
|
|
|
|
let (width, height) = if shows_properties
|
|
|
|
|
&& window_natural_width < min_width_for_overlay
|
|
|
|
|
&& window_natural_height < min_height_for_overlay
|
|
|
|
|
{
|
|
|
|
|
// Avoid overlaying bottom sheet being triggered for the image properties by
|
|
|
|
|
// using a window wide enough to allow for a sidebar
|
|
|
|
|
(min_width_for_overlay.saturating_add(1), -1)
|
|
|
|
|
} else {
|
|
|
|
|
// this lets the window determine the default size from LpImage's natural size
|
|
|
|
|
(-1, -1)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
self.set_default_size(width, height);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-18 01:29:14 +01:00
|
|
|
pub fn show_toast(&self, text: &str, priority: adw::ToastPriority) {
|
2024-09-14 00:48:46 +02:00
|
|
|
let imp = self.imp();
|
|
|
|
|
|
2024-12-18 01:29:14 +01:00
|
|
|
let toast = adw::Toast::new(text);
|
2024-09-14 00:48:46 +02:00
|
|
|
toast.set_priority(priority);
|
|
|
|
|
|
|
|
|
|
imp.toast_overlay.add_toast(toast);
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-18 01:29:14 +01:00
|
|
|
pub fn show_error(&self, stub: &str, details: &str, type_: ErrorType) {
|
|
|
|
|
let imp = self.imp();
|
|
|
|
|
|
|
|
|
|
let action = match type_ {
|
|
|
|
|
ErrorType::General => WindowAction::ShowError,
|
|
|
|
|
ErrorType::Loader => WindowAction::ShowLoaderError,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let toast = adw::Toast::builder()
|
|
|
|
|
.title(stub)
|
|
|
|
|
.priority(adw::ToastPriority::High)
|
|
|
|
|
.button_label(gettext("Show Details"))
|
|
|
|
|
.action_name(action.to_string())
|
|
|
|
|
.action_target(&details.to_variant())
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
imp.toast_overlay.add_toast(toast);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn show_error_details(&self, details: &str) {
|
|
|
|
|
LpErrorDetails::new(self, details, ErrorType::General);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn show_loader_error_details(&self, details: &str) {
|
|
|
|
|
LpErrorDetails::new(self, details, ErrorType::Loader);
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-14 00:48:46 +02:00
|
|
|
pub async fn show_about(&self) {
|
|
|
|
|
let about = crate::about::dialog().await;
|
|
|
|
|
about.present(Some(self));
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-17 22:13:35 +02:00
|
|
|
pub fn show_image(&self) {
|
|
|
|
|
self.imp()
|
|
|
|
|
.stack
|
|
|
|
|
.set_visible_child(&*self.imp().image_window);
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-14 00:48:46 +02:00
|
|
|
pub fn show_edit(&self) {
|
2024-09-15 21:06:23 +02:00
|
|
|
if let Some(image) = self.image_view().current_image() {
|
|
|
|
|
let edit_child = &*self.imp().edit_window_child;
|
|
|
|
|
|
2024-09-17 22:13:35 +02:00
|
|
|
edit_child.set_child(Some(&LpEditWindow::new(self.clone(), image)));
|
2024-09-15 21:06:23 +02:00
|
|
|
self.imp().stack.set_visible_child(edit_child);
|
|
|
|
|
} else {
|
|
|
|
|
log::error!("Can't open image editor since no current image exists");
|
|
|
|
|
}
|
2024-09-14 00:48:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn image_view(&self) -> LpImageView {
|
2024-09-14 21:39:53 +02:00
|
|
|
self.imp().image_window.image_view()
|
2024-09-14 00:48:46 +02:00
|
|
|
}
|
2024-12-16 21:27:20 +01:00
|
|
|
|
|
|
|
|
pub fn image_window(&self) -> LpImageWindow {
|
|
|
|
|
self.imp().image_window.clone()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Actions that need some global accels
|
|
|
|
|
///
|
|
|
|
|
/// The app level accels are needed for arrow key bindings. Because the arrow
|
|
|
|
|
/// keys are usually used for widget navigation, it's not possible to overwrite
|
|
|
|
|
/// them on key-binding level.
|
|
|
|
|
///
|
|
|
|
|
/// These have to be registered at window level to be callable from
|
|
|
|
|
/// GtkApplication.
|
|
|
|
|
#[derive(strum::Display, strum::AsRefStr, strum::EnumIter)]
|
|
|
|
|
pub enum ActionPartGlobal {
|
|
|
|
|
#[strum(to_string = "win.image-left-instant")]
|
|
|
|
|
ImageLeftInstant,
|
|
|
|
|
#[strum(to_string = "win.image-right-instant")]
|
|
|
|
|
ImageRightInstant,
|
|
|
|
|
// Pan
|
|
|
|
|
#[strum(to_string = "win.pan-up")]
|
|
|
|
|
PanUp,
|
|
|
|
|
#[strum(to_string = "win.pan-right")]
|
|
|
|
|
PanRight,
|
|
|
|
|
#[strum(to_string = "win.pan-down")]
|
|
|
|
|
PanDown,
|
|
|
|
|
#[strum(to_string = "win.pan-left")]
|
|
|
|
|
PanLeft,
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-18 01:29:14 +01:00
|
|
|
impl Deref for ActionPartGlobal {
|
|
|
|
|
type Target = str;
|
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
|
|
|
self.as_ref()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-16 21:27:20 +01:00
|
|
|
impl ActionPartGlobal {
|
|
|
|
|
pub fn add_accels(application: >k::Application) {
|
|
|
|
|
for action in Self::iter() {
|
|
|
|
|
match action {
|
|
|
|
|
Self::ImageRightInstant => application.set_accels_for_action(&action, &["Right"]),
|
|
|
|
|
Self::ImageLeftInstant => application.set_accels_for_action(&action, &["Left"]),
|
|
|
|
|
Self::PanUp => application.set_accels_for_action(&action, &["<Ctrl>Up"]),
|
|
|
|
|
Self::PanRight => application.set_accels_for_action(&action, &["<Ctrl>Right"]),
|
|
|
|
|
Self::PanDown => application.set_accels_for_action(&action, &["<Ctrl>Down"]),
|
|
|
|
|
Self::PanLeft => application.set_accels_for_action(&action, &["<Ctrl>Left"]),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn remove_accels(application: >k::Application) {
|
|
|
|
|
for action in Self::iter() {
|
|
|
|
|
application.set_accels_for_action(&action, &[]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn init_actions_and_bindings(klass: &mut <imp::LpWindow as ObjectSubclass>::Class) {
|
|
|
|
|
for action in Self::iter() {
|
|
|
|
|
match action {
|
|
|
|
|
ActionPartGlobal::ImageLeftInstant => {
|
|
|
|
|
klass.install_action(&action, None, move |win, _, _| {
|
|
|
|
|
if win.direction() == gtk::TextDirection::Rtl {
|
|
|
|
|
win.image_view().navigate(Direction::Forward, false);
|
|
|
|
|
} else {
|
|
|
|
|
win.image_view().navigate(Direction::Back, false);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
klass.add_binding_action(
|
|
|
|
|
gdk::Key::Page_Down,
|
|
|
|
|
gdk::ModifierType::empty(),
|
|
|
|
|
&action,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ActionPartGlobal::ImageRightInstant => {
|
|
|
|
|
klass.install_action(&action, None, move |win, _, _| {
|
|
|
|
|
if win.direction() == gtk::TextDirection::Rtl {
|
|
|
|
|
win.image_view().navigate(Direction::Back, false);
|
|
|
|
|
} else {
|
|
|
|
|
win.image_view().navigate(Direction::Forward, false);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
klass.add_binding_action(
|
|
|
|
|
gdk::Key::Page_Up,
|
|
|
|
|
gdk::ModifierType::empty(),
|
|
|
|
|
&action,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Pan
|
|
|
|
|
ActionPartGlobal::PanUp => {
|
|
|
|
|
klass.install_action(&action, None, move |win, _, _| {
|
|
|
|
|
win.image_window().pan(>k::PanDirection::Up);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
ActionPartGlobal::PanRight => {
|
|
|
|
|
klass.install_action(&action, None, move |win, _, _| {
|
|
|
|
|
win.image_window().pan(>k::PanDirection::Right);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
ActionPartGlobal::PanDown => {
|
|
|
|
|
klass.install_action(&action, None, move |win, _, _| {
|
|
|
|
|
win.image_window().pan(>k::PanDirection::Down);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
ActionPartGlobal::PanLeft => {
|
|
|
|
|
klass.install_action(&action, None, move |win, _, _| {
|
|
|
|
|
win.image_window().pan(>k::PanDirection::Left);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-18 01:29:14 +01:00
|
|
|
#[derive(strum::Display, strum::AsRefStr, strum::EnumIter)]
|
|
|
|
|
enum WindowAction {
|
|
|
|
|
#[strum(to_string = "win.show-error")]
|
|
|
|
|
ShowError,
|
|
|
|
|
#[strum(to_string = "win.show-loader-error")]
|
|
|
|
|
ShowLoaderError,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Deref for WindowAction {
|
2024-12-16 21:27:20 +01:00
|
|
|
type Target = str;
|
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
|
|
|
self.as_ref()
|
|
|
|
|
}
|
2024-09-14 00:48:46 +02:00
|
|
|
}
|
2024-12-18 01:29:14 +01:00
|
|
|
|
|
|
|
|
impl WindowAction {
|
|
|
|
|
pub fn init_actions_and_bindings(klass: &mut <imp::LpWindow as ObjectSubclass>::Class) {
|
|
|
|
|
for action in Self::iter() {
|
|
|
|
|
match action {
|
|
|
|
|
Self::ShowError => {
|
|
|
|
|
klass.install_action(
|
|
|
|
|
&action,
|
|
|
|
|
Some(VariantTy::STRING),
|
|
|
|
|
move |win, _, variant| {
|
|
|
|
|
if let Some(message) = variant.and_then(String::from_variant) {
|
|
|
|
|
win.show_error_details(&message);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
Self::ShowLoaderError => {
|
|
|
|
|
klass.install_action(
|
|
|
|
|
&action,
|
|
|
|
|
Some(VariantTy::STRING),
|
|
|
|
|
move |win, _, variant| {
|
|
|
|
|
if let Some(message) = variant.and_then(String::from_variant) {
|
|
|
|
|
win.show_loader_error_details(&message);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|