2025-01-17 01:30:41 +01:00
|
|
|
// Copyright (c) 2024-2025 Sophie Herold
|
2024-09-14 21:34:29 +02: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
|
|
|
|
|
|
2025-10-21 22:47:39 +02:00
|
|
|
use std::cell::{Cell, OnceCell, RefCell};
|
2024-12-16 21:27:20 +01:00
|
|
|
use std::sync::Arc;
|
2024-09-15 21:06:23 +02:00
|
|
|
|
2024-09-14 21:34:29 +02:00
|
|
|
use adw::prelude::*;
|
|
|
|
|
use adw::subclass::prelude::*;
|
2024-10-18 15:51:34 +02:00
|
|
|
use glycin::Operations;
|
2024-09-14 21:34:29 +02:00
|
|
|
|
2024-12-18 17:12:17 +01:00
|
|
|
use super::LpImage;
|
2025-12-04 06:14:49 +01:00
|
|
|
use super::edit::LpEditCrop;
|
2024-09-15 21:06:23 +02:00
|
|
|
use crate::deps::*;
|
2025-12-04 06:14:49 +01:00
|
|
|
use crate::util::ErrorType;
|
2024-12-18 01:29:14 +01:00
|
|
|
use crate::util::gettext::*;
|
2024-12-18 17:12:17 +01:00
|
|
|
use crate::util::root::ParentWindow;
|
2024-09-15 21:06:23 +02:00
|
|
|
|
2024-09-17 22:13:35 +02:00
|
|
|
mod imp {
|
2024-09-14 21:34:29 +02:00
|
|
|
use super::*;
|
2024-09-15 21:06:23 +02:00
|
|
|
|
2024-09-14 21:34:29 +02:00
|
|
|
#[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
|
2024-09-15 21:06:23 +02:00
|
|
|
#[properties(wrapper_type = super::LpEditWindow)]
|
2024-09-14 21:39:53 +02:00
|
|
|
#[template(file = "edit_window.ui")]
|
2024-09-15 21:06:23 +02:00
|
|
|
pub struct LpEditWindow {
|
|
|
|
|
#[template_child]
|
|
|
|
|
toolbar_view: TemplateChild<adw::ToolbarView>,
|
2024-09-17 22:13:35 +02:00
|
|
|
#[template_child]
|
|
|
|
|
cancel: TemplateChild<gtk::Button>,
|
2025-01-15 21:45:13 +01:00
|
|
|
#[template_child]
|
2025-01-31 14:01:33 +01:00
|
|
|
pub(crate) save: TemplateChild<gtk::MenuButton>,
|
2025-01-22 20:39:31 +01:00
|
|
|
#[template_child]
|
|
|
|
|
saving_revealer: TemplateChild<gtk::Revealer>,
|
|
|
|
|
#[template_child]
|
|
|
|
|
saving_status: TemplateChild<gtk::Label>,
|
|
|
|
|
#[template_child]
|
|
|
|
|
saving_info: TemplateChild<gtk::Revealer>,
|
2025-01-31 13:49:20 +01:00
|
|
|
#[template_child]
|
|
|
|
|
shortcut_controller: TemplateChild<gtk::ShortcutController>,
|
2024-09-15 21:06:23 +02:00
|
|
|
|
|
|
|
|
#[property(get, construct_only)]
|
|
|
|
|
original_image: OnceCell<LpImage>,
|
2025-01-22 15:41:56 +01:00
|
|
|
#[property(get, construct_only)]
|
|
|
|
|
pub edit_crop: OnceCell<LpEditCrop>,
|
2024-10-18 15:51:34 +02:00
|
|
|
|
2025-10-21 22:47:39 +02:00
|
|
|
/// Whether the original file can be trashed
|
|
|
|
|
pub(super) can_trash: Cell<bool>,
|
|
|
|
|
|
2024-10-18 15:51:34 +02:00
|
|
|
pub(super) operations: RefCell<Option<Arc<Operations>>>,
|
2025-01-29 13:24:36 +01:00
|
|
|
save_cancellable: RefCell<Option<gio::Cancellable>>,
|
2024-09-14 21:34:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[glib::object_subclass]
|
2024-09-15 21:06:23 +02:00
|
|
|
impl ObjectSubclass for LpEditWindow {
|
|
|
|
|
const NAME: &'static str = "LpEditWindow";
|
|
|
|
|
type Type = super::LpEditWindow;
|
2024-09-14 21:34:29 +02:00
|
|
|
type ParentType = adw::Bin;
|
|
|
|
|
|
|
|
|
|
fn class_init(klass: &mut Self::Class) {
|
|
|
|
|
Self::bind_template(klass);
|
2025-01-14 00:12:53 +01:00
|
|
|
|
|
|
|
|
klass.install_action("edit.save-copy", None, |obj, _, _| {
|
|
|
|
|
glib::spawn_future_local(glib::clone!(
|
|
|
|
|
#[weak]
|
|
|
|
|
obj,
|
2025-01-15 21:45:13 +01:00
|
|
|
async move { obj.imp().save_copy().await }
|
|
|
|
|
));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
klass.install_action("edit.save-overwrite", None, |obj, _, _| {
|
|
|
|
|
glib::spawn_future_local(glib::clone!(
|
|
|
|
|
#[weak]
|
|
|
|
|
obj,
|
|
|
|
|
async move { obj.imp().save_overwrite().await }
|
2025-01-14 00:12:53 +01:00
|
|
|
));
|
|
|
|
|
});
|
2025-01-31 13:49:20 +01:00
|
|
|
|
|
|
|
|
klass.install_action("edit.cancel", None, move |obj, _, _| {
|
|
|
|
|
obj.imp().cancel();
|
|
|
|
|
});
|
2024-09-14 21:34:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
|
|
|
|
obj.init_template();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[glib::derived_properties]
|
2024-09-15 21:06:23 +02:00
|
|
|
impl ObjectImpl for LpEditWindow {
|
2024-09-14 21:34:29 +02:00
|
|
|
fn constructed(&self) {
|
|
|
|
|
self.parent_constructed();
|
2024-09-15 21:06:23 +02:00
|
|
|
|
|
|
|
|
let obj = self.obj();
|
|
|
|
|
|
2025-01-15 21:45:13 +01:00
|
|
|
glib::spawn_future_local(glib::clone!(
|
|
|
|
|
#[weak]
|
|
|
|
|
obj,
|
|
|
|
|
async move {
|
|
|
|
|
let can_trash = obj.original_image().can_trash().await;
|
2025-10-21 22:47:39 +02:00
|
|
|
obj.imp().can_trash.replace(can_trash);
|
2025-01-15 21:45:13 +01:00
|
|
|
obj.action_set_enabled("edit.save-overwrite", can_trash);
|
|
|
|
|
}
|
|
|
|
|
));
|
2025-01-31 13:49:20 +01:00
|
|
|
|
|
|
|
|
self.shortcut_controller.add_shortcut(gtk::Shortcut::new(
|
|
|
|
|
Some(gtk::KeyvalTrigger::new(
|
|
|
|
|
gdk::Key::S,
|
|
|
|
|
gdk::ModifierType::CONTROL_MASK,
|
|
|
|
|
)),
|
|
|
|
|
Some(gtk::NamedAction::new("edit.save-overwrite")),
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
self.shortcut_controller.add_shortcut(gtk::Shortcut::new(
|
|
|
|
|
Some(gtk::KeyvalTrigger::new(
|
|
|
|
|
gdk::Key::S,
|
|
|
|
|
gdk::ModifierType::CONTROL_MASK.union(gdk::ModifierType::SHIFT_MASK),
|
|
|
|
|
)),
|
|
|
|
|
Some(gtk::NamedAction::new("edit.save-copy")),
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
self.shortcut_controller.add_shortcut(gtk::Shortcut::new(
|
|
|
|
|
Some(gtk::KeyvalTrigger::new(
|
|
|
|
|
gdk::Key::Escape,
|
|
|
|
|
gdk::ModifierType::NO_MODIFIER_MASK,
|
|
|
|
|
)),
|
|
|
|
|
Some(gtk::NamedAction::new("edit.cancel")),
|
|
|
|
|
));
|
2024-09-14 21:34:29 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-22 15:41:56 +01:00
|
|
|
impl WidgetImpl for LpEditWindow {
|
|
|
|
|
fn root(&self) {
|
|
|
|
|
self.parent_root();
|
|
|
|
|
|
|
|
|
|
let obj = self.obj();
|
|
|
|
|
|
2025-01-29 00:20:03 +01:00
|
|
|
obj.edit_crop()
|
|
|
|
|
.selection()
|
|
|
|
|
.connect_cropped_notify(glib::clone!(
|
|
|
|
|
#[weak]
|
|
|
|
|
obj,
|
2025-01-31 14:01:33 +01:00
|
|
|
move |_| obj.imp().update_save_state(false)
|
2025-01-29 00:20:03 +01:00
|
|
|
));
|
2025-01-31 14:01:33 +01:00
|
|
|
|
|
|
|
|
self.update_save_state(false);
|
2025-01-22 15:41:56 +01:00
|
|
|
}
|
|
|
|
|
}
|
2024-09-15 21:06:23 +02:00
|
|
|
impl BinImpl for LpEditWindow {}
|
2024-10-18 15:51:34 +02:00
|
|
|
|
|
|
|
|
impl LpEditWindow {
|
2025-01-31 13:49:20 +01:00
|
|
|
fn cancel(&self) {
|
|
|
|
|
let obj = self.obj();
|
|
|
|
|
|
|
|
|
|
if let Some(cancellable) = obj.imp().save_cancellable.replace(None) {
|
|
|
|
|
cancellable.cancel();
|
|
|
|
|
}
|
2025-12-09 22:04:58 +01:00
|
|
|
obj.window_inspect(|w| w.show_image());
|
2025-01-31 13:49:20 +01:00
|
|
|
}
|
|
|
|
|
|
2025-01-15 21:45:13 +01:00
|
|
|
async fn save_copy(&self) {
|
2024-10-18 15:51:34 +02:00
|
|
|
let obj = self.obj();
|
2025-01-31 14:01:33 +01:00
|
|
|
self.save.popdown();
|
2024-10-18 15:51:34 +02:00
|
|
|
|
|
|
|
|
if let Some(current_file) = obj.original_image().file() {
|
2025-01-29 13:24:36 +01:00
|
|
|
let cancellable = gio::Cancellable::new();
|
|
|
|
|
if let Some(old_cancellable) =
|
|
|
|
|
self.save_cancellable.replace(Some(cancellable.clone()))
|
|
|
|
|
{
|
|
|
|
|
old_cancellable.cancel();
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-18 15:51:34 +02:00
|
|
|
let file_dialog = gtk::FileDialog::new();
|
|
|
|
|
|
2025-01-28 23:43:38 +01:00
|
|
|
let suggested_file = if let Some(path) = current_file.path() {
|
|
|
|
|
if let Some(basename) = path.file_stem() {
|
|
|
|
|
let mut suggested_file = None;
|
|
|
|
|
for i in 1..20 {
|
|
|
|
|
let mut new_path = path.clone();
|
|
|
|
|
let mut new_filename = basename.to_os_string();
|
|
|
|
|
|
|
|
|
|
if i == 1 {
|
|
|
|
|
// Translators: Filename suffix
|
|
|
|
|
new_filename.push(gettext(" (Edited)"));
|
|
|
|
|
} else {
|
|
|
|
|
// Translators: Filename suffix, {} is replaced by a number
|
|
|
|
|
new_filename.push(ngettext(" (Edited)", " (Edited {})", i));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(ext) = path.extension() {
|
|
|
|
|
new_filename.push(".");
|
|
|
|
|
new_filename.push(ext);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
new_path.set_file_name(new_filename);
|
|
|
|
|
|
|
|
|
|
let file = gio::File::for_path(&new_path);
|
2025-01-31 20:09:21 +01:00
|
|
|
let file_exists = file
|
|
|
|
|
.query_info_future(
|
2025-01-28 23:43:38 +01:00
|
|
|
gio::FILE_ATTRIBUTE_STANDARD_NAME,
|
|
|
|
|
gio::FileQueryInfoFlags::NONE,
|
|
|
|
|
glib::Priority::DEFAULT,
|
2025-01-31 20:09:21 +01:00
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.is_ok();
|
2025-01-28 23:43:38 +01:00
|
|
|
|
|
|
|
|
if !file_exists {
|
|
|
|
|
suggested_file = Some(file);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
suggested_file.unwrap_or_else(|| current_file.clone())
|
|
|
|
|
} else {
|
|
|
|
|
current_file.clone()
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
current_file.clone()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
file_dialog.set_initial_file(Some(&suggested_file));
|
2024-10-18 15:51:34 +02:00
|
|
|
|
2025-12-09 22:04:58 +01:00
|
|
|
match file_dialog.save_future(obj.try_window().as_ref()).await {
|
2024-10-18 15:51:34 +02:00
|
|
|
Err(err) => {
|
2025-12-18 23:15:46 +01:00
|
|
|
tracing::error!("{}", err);
|
2024-10-18 15:51:34 +02:00
|
|
|
}
|
|
|
|
|
Ok(new_file) => {
|
2025-01-29 13:24:36 +01:00
|
|
|
if self
|
|
|
|
|
.save(current_file, new_file.clone(), cancellable.clone())
|
|
|
|
|
.await
|
|
|
|
|
{
|
2025-12-09 22:04:58 +01:00
|
|
|
obj.window_inspect(|w| w.show_specific_image(new_file));
|
2025-01-17 12:35:43 +01:00
|
|
|
}
|
2025-01-15 21:45:13 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn save_overwrite(&self) {
|
|
|
|
|
let obj = self.obj();
|
2025-01-31 14:01:33 +01:00
|
|
|
self.save.popdown();
|
2025-01-15 21:45:13 +01:00
|
|
|
|
2026-01-29 19:04:26 +01:00
|
|
|
if let Some(original_file) = obj.original_image().file()
|
|
|
|
|
&& let Some(current_path) = original_file.path()
|
|
|
|
|
&& let Some(mut file_stem) = current_path.file_stem().map(|x| x.to_os_string())
|
|
|
|
|
{
|
|
|
|
|
let mut tmp_path = current_path.clone();
|
|
|
|
|
file_stem.push(".tmp");
|
|
|
|
|
if let Some(ext) = current_path.extension() {
|
|
|
|
|
file_stem.push(".");
|
|
|
|
|
file_stem.push(ext);
|
|
|
|
|
}
|
|
|
|
|
tmp_path.set_file_name(file_stem);
|
2025-01-29 13:24:36 +01:00
|
|
|
|
2026-01-29 19:04:26 +01:00
|
|
|
let tmp_file = gio::File::for_path(&tmp_path);
|
2025-01-29 13:24:36 +01:00
|
|
|
|
2026-01-29 19:04:26 +01:00
|
|
|
let cancellable = gio::Cancellable::new();
|
|
|
|
|
if let Some(old_cancellable) =
|
|
|
|
|
self.save_cancellable.replace(Some(cancellable.clone()))
|
|
|
|
|
{
|
|
|
|
|
old_cancellable.cancel();
|
|
|
|
|
}
|
2025-01-15 21:45:13 +01:00
|
|
|
|
2026-01-29 19:04:26 +01:00
|
|
|
let written = self
|
|
|
|
|
.save(original_file.clone(), tmp_file.clone(), cancellable.clone())
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
if written {
|
|
|
|
|
tracing::debug!("Moving image to trash '{}'", original_file.uri());
|
|
|
|
|
let trash_result = gio::CancellableFuture::new(
|
|
|
|
|
original_file.trash_future(glib::Priority::DEFAULT),
|
|
|
|
|
cancellable.clone(),
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
if trash_result.is_err() {
|
|
|
|
|
// Canceled
|
|
|
|
|
tracing::debug!("Trashing image canceled");
|
|
|
|
|
return;
|
|
|
|
|
} else if let Ok(Err(err)) = trash_result {
|
|
|
|
|
obj.window_show_error(
|
2025-01-15 21:45:13 +01:00
|
|
|
&gettext("Failed to save image."),
|
|
|
|
|
&format!("Failed to move image {current_path:?} to trash and therefore couldn't save image: {err}"),
|
|
|
|
|
ErrorType::General,
|
|
|
|
|
);
|
2026-01-29 19:04:26 +01:00
|
|
|
return;
|
|
|
|
|
}
|
2025-02-13 14:53:28 +01:00
|
|
|
|
2026-01-29 19:04:26 +01:00
|
|
|
tracing::debug!("Moving '{}' to '{}'", tmp_file.uri(), original_file.uri());
|
|
|
|
|
let move_result = gio::CancellableFuture::new(
|
|
|
|
|
tmp_file
|
|
|
|
|
.move_future(
|
|
|
|
|
&original_file,
|
|
|
|
|
gio::FileCopyFlags::NONE,
|
|
|
|
|
glib::Priority::DEFAULT,
|
2025-01-29 13:24:36 +01:00
|
|
|
)
|
2026-01-29 19:04:26 +01:00
|
|
|
.0,
|
|
|
|
|
cancellable.clone(),
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
if move_result.is_err() {
|
|
|
|
|
// Canceled
|
|
|
|
|
tracing::debug!("Moving image canceled");
|
|
|
|
|
return;
|
|
|
|
|
} else if let Ok(Err(err)) = move_result {
|
|
|
|
|
obj.window_show_error(
|
2025-01-15 21:45:13 +01:00
|
|
|
&gettext("Failed to save image."),
|
2024-12-18 01:29:14 +01:00
|
|
|
&format!(
|
2025-02-13 14:53:28 +01:00
|
|
|
"Failed to move image image to {current_path:?}: {err} move {:?} to {:?}", tmp_file.path().unwrap(), original_file.path().unwrap()
|
2024-12-18 01:29:14 +01:00
|
|
|
),
|
|
|
|
|
ErrorType::General,
|
2025-01-15 21:45:13 +01:00
|
|
|
);
|
2026-01-29 19:04:26 +01:00
|
|
|
return;
|
2025-01-15 21:45:13 +01:00
|
|
|
}
|
2026-01-29 19:04:26 +01:00
|
|
|
|
|
|
|
|
obj.window_inspect(|w| w.show_specific_image(original_file));
|
2025-01-15 21:45:13 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-17 12:35:43 +01:00
|
|
|
#[must_use]
|
|
|
|
|
/// Saves image with operations applies
|
|
|
|
|
///
|
|
|
|
|
/// Returns `true` if editing and saving was successful
|
2025-01-29 13:24:36 +01:00
|
|
|
async fn save(
|
|
|
|
|
&self,
|
|
|
|
|
current_file: gio::File,
|
|
|
|
|
new_file: gio::File,
|
|
|
|
|
cancellable: gio::Cancellable,
|
|
|
|
|
) -> bool {
|
2025-01-15 21:45:13 +01:00
|
|
|
let obj = self.obj();
|
2025-01-22 15:41:56 +01:00
|
|
|
|
|
|
|
|
obj.edit_crop().apply_crop();
|
|
|
|
|
|
2025-01-22 20:39:31 +01:00
|
|
|
self.set_saving_status(Some(gettext("Editing Image")));
|
2025-01-29 13:24:36 +01:00
|
|
|
let mut editor = glycin::Editor::new(current_file);
|
|
|
|
|
editor.cancellable(cancellable.clone());
|
|
|
|
|
|
2025-01-15 21:45:13 +01:00
|
|
|
if let Some(operations) = obj.operations() {
|
2025-12-18 23:15:46 +01:00
|
|
|
tracing::debug!("Computing edited image.");
|
2025-07-07 22:18:36 +02:00
|
|
|
let result = match editor.edit().await {
|
|
|
|
|
Ok(editable_image) => editable_image.apply_complete(&operations).await,
|
|
|
|
|
Err(err) => Err(err),
|
|
|
|
|
};
|
2025-01-15 21:45:13 +01:00
|
|
|
match result {
|
2025-02-13 22:38:13 +01:00
|
|
|
Err(err) if matches!(err.error(), glycin::Error::Canceled(_)) => {
|
2025-12-18 23:15:46 +01:00
|
|
|
tracing::debug!("Computing edited image canceled");
|
2025-02-13 22:38:13 +01:00
|
|
|
}
|
2025-01-15 21:45:13 +01:00
|
|
|
Err(err) => {
|
2025-12-18 23:15:46 +01:00
|
|
|
tracing::warn!("Failed to edit image: {err}");
|
2025-12-09 22:04:58 +01:00
|
|
|
obj.window_show_error(
|
2025-01-15 21:45:13 +01:00
|
|
|
&gettext("Failed to edit image."),
|
|
|
|
|
&format!("Failed to edit image:\n\n{err}\n\n{operations:#?}"),
|
2025-02-13 22:08:13 +01:00
|
|
|
ErrorType::Loader,
|
2025-01-15 21:45:13 +01:00
|
|
|
)
|
|
|
|
|
}
|
2025-07-07 22:18:36 +02:00
|
|
|
Ok(edit) => {
|
2025-12-18 23:15:46 +01:00
|
|
|
tracing::debug!("Saving edited image to '{}'", new_file.uri());
|
2025-07-07 22:18:36 +02:00
|
|
|
match edit.data().get() {
|
2025-01-15 21:45:13 +01:00
|
|
|
Err(err) => {
|
2025-12-09 22:04:58 +01:00
|
|
|
obj.window_show_error(
|
2025-01-15 21:45:13 +01:00
|
|
|
&gettext("Failed to Save Image"),
|
|
|
|
|
&format!("Failed to get binary data: {err}"),
|
|
|
|
|
ErrorType::General,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
Ok(data) => {
|
2025-01-22 20:39:31 +01:00
|
|
|
self.set_saving_status(Some(gettext("Saving Image")));
|
|
|
|
|
|
2025-02-13 14:53:28 +01:00
|
|
|
let save_result = gio::CancellableFuture::new(
|
|
|
|
|
new_file.replace_contents_future(
|
2025-01-15 21:45:13 +01:00
|
|
|
data,
|
|
|
|
|
None,
|
|
|
|
|
true,
|
|
|
|
|
gio::FileCreateFlags::NONE,
|
2025-02-13 14:53:28 +01:00
|
|
|
),
|
|
|
|
|
cancellable,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
if save_result.is_err() {
|
2025-12-18 23:15:46 +01:00
|
|
|
tracing::debug!("Saving image canceled");
|
2025-02-13 14:53:28 +01:00
|
|
|
} else if let Ok(Err(err)) = save_result {
|
2025-12-09 22:04:58 +01:00
|
|
|
obj.window_show_error(
|
2025-01-15 21:45:13 +01:00
|
|
|
&gettext("Failed to Save Image"),
|
|
|
|
|
&format!("Failed to write file:\n\n{err:?}"),
|
|
|
|
|
ErrorType::General,
|
|
|
|
|
);
|
|
|
|
|
} else {
|
2025-12-18 23:15:46 +01:00
|
|
|
tracing::debug!("Image saved");
|
2025-01-15 21:45:13 +01:00
|
|
|
return true;
|
2024-12-18 01:29:14 +01:00
|
|
|
}
|
|
|
|
|
}
|
2024-10-18 15:51:34 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-15 21:45:13 +01:00
|
|
|
|
2025-01-22 20:39:31 +01:00
|
|
|
self.set_saving_status(None);
|
2025-01-15 21:45:13 +01:00
|
|
|
false
|
2024-10-18 15:51:34 +02:00
|
|
|
}
|
2025-01-22 20:39:31 +01:00
|
|
|
|
|
|
|
|
fn set_saving_status(&self, message: Option<String>) {
|
|
|
|
|
let is_saving = message.is_some();
|
|
|
|
|
self.saving_status.set_label(&message.unwrap_or_default());
|
|
|
|
|
self.saving_revealer.set_reveal_child(is_saving);
|
2025-01-31 14:01:33 +01:00
|
|
|
self.update_save_state(is_saving);
|
2025-01-22 20:39:31 +01:00
|
|
|
|
|
|
|
|
if is_saving {
|
|
|
|
|
// Show text and spinner delayed
|
|
|
|
|
glib::timeout_add_local_once(
|
|
|
|
|
std::time::Duration::from_millis(
|
|
|
|
|
self.saving_revealer.transition_duration().into(),
|
|
|
|
|
),
|
|
|
|
|
glib::clone!(
|
|
|
|
|
#[weak(rename_to=imp)]
|
|
|
|
|
self,
|
|
|
|
|
move || {
|
|
|
|
|
imp.saving_info.set_reveal_child(true);
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
self.saving_info.set_reveal_child(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-31 14:01:33 +01:00
|
|
|
|
|
|
|
|
fn is_save_sensitive(&self) -> bool {
|
|
|
|
|
let obj = self.obj();
|
|
|
|
|
|
|
|
|
|
self.operations
|
|
|
|
|
.borrow()
|
|
|
|
|
.as_ref()
|
|
|
|
|
.is_some_and(|x| !x.operations().is_empty())
|
|
|
|
|
|| obj.edit_crop().selection().cropped()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn update_save_state(&self, force_disabled: bool) {
|
|
|
|
|
let obj = self.obj();
|
|
|
|
|
let enabled = self.is_save_sensitive() && !force_disabled;
|
|
|
|
|
|
|
|
|
|
self.save.set_sensitive(enabled);
|
2025-10-21 22:47:39 +02:00
|
|
|
obj.action_set_enabled("edit.save-overwrite", enabled & self.can_trash.get());
|
2025-01-31 14:01:33 +01:00
|
|
|
obj.action_set_enabled("edit.save-copy", enabled);
|
|
|
|
|
}
|
2024-10-18 15:51:34 +02:00
|
|
|
}
|
2024-09-14 21:34:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
glib::wrapper! {
|
2024-09-15 21:06:23 +02:00
|
|
|
pub struct LpEditWindow(ObjectSubclass<imp::LpEditWindow>)
|
2025-08-22 15:03:55 +02:00
|
|
|
@extends gtk::Widget, adw::Bin,
|
|
|
|
|
@implements gtk::Buildable, gtk::Accessible, gtk::ConstraintTarget;
|
2024-09-14 21:34:29 +02:00
|
|
|
}
|
|
|
|
|
|
2024-09-15 21:06:23 +02:00
|
|
|
impl LpEditWindow {
|
2024-12-21 01:16:24 +01:00
|
|
|
pub const REQUIRED_OPERATIONS: &[glycin::OperationId] = &[
|
|
|
|
|
glycin::OperationId::Clip,
|
|
|
|
|
glycin::OperationId::MirrorHorizontally,
|
|
|
|
|
glycin::OperationId::Rotate,
|
|
|
|
|
];
|
|
|
|
|
|
2024-12-18 17:12:17 +01:00
|
|
|
pub fn new(image: LpImage) -> Self {
|
2024-09-15 21:06:23 +02:00
|
|
|
glib::Object::builder()
|
2025-01-22 15:41:56 +01:00
|
|
|
.property("original_image", image.clone())
|
|
|
|
|
.property("edit_crop", LpEditCrop::new(image))
|
2024-09-15 21:06:23 +02:00
|
|
|
.build()
|
2024-09-14 21:34:29 +02:00
|
|
|
}
|
2024-10-18 15:51:34 +02:00
|
|
|
|
|
|
|
|
pub fn operations(&self) -> Option<Arc<glycin::Operations>> {
|
|
|
|
|
self.imp().operations.borrow().clone()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn set_operations(&self, operations: Option<Arc<glycin::Operations>>) {
|
|
|
|
|
let imp = self.imp();
|
2025-01-22 15:41:56 +01:00
|
|
|
imp.operations.replace(operations);
|
2025-01-31 14:01:33 +01:00
|
|
|
imp.update_save_state(false);
|
2024-10-18 15:51:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn add_operation(&self, operation: glycin::Operation) {
|
|
|
|
|
let imp = self.imp();
|
|
|
|
|
|
|
|
|
|
let mut operations = imp
|
|
|
|
|
.operations
|
|
|
|
|
.borrow()
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|x| x.operations().to_vec())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
operations.push(operation);
|
|
|
|
|
|
|
|
|
|
self.set_operations(Some(Arc::new(Operations::new(operations))));
|
|
|
|
|
}
|
2024-09-14 21:34:29 +02:00
|
|
|
}
|