Loupe/src/window.rs

398 lines
14 KiB
Rust
Raw Normal View History

// 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
use crate::deps::*;
use adw::subclass::prelude::*;
use glib::clone;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::CompositeTemplate;
use ashpd::desktop::open_uri;
use ashpd::WindowIdentifier;
use gtk_macros::spawn;
use crate::config;
use crate::util;
use crate::widgets::{LpImageView, LpPropertiesView};
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)]
#[template(resource = "/org/gnome/Loupe/gtk/window.ui")]
pub struct LpWindow {
// Template children are used with the
// TemplateChild<T> wrapper, where T is the
// object type of the template child.
#[template_child]
pub flap: TemplateChild<adw::Flap>,
#[template_child]
pub headerbar: TemplateChild<gtk::HeaderBar>,
#[template_child]
pub menu_button: TemplateChild<gtk::MenuButton>,
#[template_child]
pub toast_overlay: TemplateChild<adw::ToastOverlay>,
#[template_child]
pub stack: TemplateChild<gtk::Stack>,
#[template_child]
pub status_page: TemplateChild<adw::StatusPage>,
#[template_child]
pub image_view: TemplateChild<LpImageView>,
#[template_child]
pub properties_view: TemplateChild<LpPropertiesView>,
#[template_child]
pub drop_target: TemplateChild<gtk::DropTarget>,
}
2021-06-27 15:28:35 -07: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) {
// bind_template() is a function generated by the
// CompositeTemplate macro to bind all children at once.
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
// Set up actions
klass.install_action("win.toggle-fullscreen", None, move |win, _, _| {
win.toggle_fullscreen(!win.is_fullscreened());
});
klass.install_action("win.open", None, move |win, _, _| {
win.pick_file();
});
klass.install_action("win.open-with", None, move |win, _, _| {
win.open_with();
});
klass.install_action("win.set-wallpaper", None, move |win, _, _| {
win.set_wallpaper();
});
klass.install_action("win.print", None, move |win, _, _| {
win.print();
});
2022-01-08 14:02:16 +01:00
klass.install_action("win.copy", None, move |win, _, _| {
win.copy();
});
klass.install_action("win.show-toast", Some("(si)"), 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));
}
});
}
2021-06-27 15:28:35 -07:00
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for LpWindow {
fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj);
if config::PROFILE == ".Devel" {
obj.add_css_class("devel");
}
obj.set_actions_enabled(false);
self.status_page
.set_icon_name(Some(&format!("{}-symbolic", config::APP_ID)));
// Set help overlay
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();
obj.set_help_overlay(Some(&help_overlay));
self.drop_target.set_types(&[gdk::FileList::static_type()]);
// For callbacks, you will want to reference the GTK docs on
// the relevant signal to see which parameters you need.
// In this case, we need only need the GValue,
// so we name it `value` then use `_` for the other spots.
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()`.
let list: gdk::FileList = value.get().unwrap();
// 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,
vec![
&gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
&gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
],
)
.expect("Could not query file info");
if info
.content_type()
.map(|t| t.to_string())
.filter(|t| t.starts_with("image/"))
.is_some() {
obj.set_image_from_file(&file, false);
} else {
obj.show_toast(
format!("\"{}\" is not a valid image.", info.display_name()),
adw::ToastPriority::High,
);
}
true
}),
);
}
}
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;
}
#[gtk::template_callbacks]
impl LpWindow {
pub fn new<A: IsA<gtk::Application>>(app: &A) -> Self {
glib::Object::new(&[("application", app)]).expect("Failed to create LpWindow")
}
fn toggle_fullscreen(&self, fullscreen: bool) {
let imp = self.imp();
if fullscreen {
imp.headerbar.add_css_class("osd");
imp.properties_view.add_css_class("osd")
} else {
imp.headerbar.remove_css_class("osd");
imp.properties_view.remove_css_class("osd")
}
self.set_fullscreened(fullscreen);
}
fn pick_file(&self) {
let chooser = gtk::FileChooserNative::new(
Some("Open Image"),
Some(self),
gtk::FileChooserAction::Open,
None,
None,
);
chooser.set_modal(true);
chooser.set_transient_for(Some(self));
let filter = gtk::FileFilter::new();
filter.set_property("name", &String::from("Supported image files"));
filter.add_mime_type("image/*");
chooser.add_filter(&filter);
chooser.connect_response(
clone!(@weak self as win, @strong chooser => move |_, resp| {
if resp == gtk::ResponseType::Accept {
2021-06-27 15:28:35 -07:00
if let Some(file) = chooser.file() {
win.set_image_from_file(&file, true);
}
}
}),
);
chooser.show();
}
fn open_with(&self) {
let imp = self.imp();
if let Some(file) = imp
.image_view
.active_file()
.and_then(|f| f.peek_path())
.and_then(|p| std::fs::File::open(p).ok())
{
spawn!(clone!(@weak self as win => async move {
if let Err(e) =
open_uri::open_file(&WindowIdentifier::from_native(&win).await, &file, true, true).await
{
log::error!("Could not open image in external program: {}", e);
}
}));
} else {
log::error!("Could not load a path for the current image.")
}
}
fn set_wallpaper(&self) {
let imp = self.imp();
if let Err(e) = imp.image_view.set_wallpaper() {
log::error!("Failed to set wallpaper: {}", e);
}
}
fn print(&self) {
let imp = self.imp();
if let Err(e) = imp.image_view.print() {
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);
} else {
self.show_toast("Image copied to clipboard", adw::ToastPriority::High);
2022-01-08 14:02:16 +01:00
}
}
fn show_toast(&self, text: impl AsRef<str>, priority: adw::ToastPriority) {
let imp = self.imp();
let toast = adw::Toast::new(text.as_ref());
toast.set_priority(priority);
imp.toast_overlay.add_toast(&toast);
}
widgets: Rework image_view with AdwCarousel, add ImagePage Previously we loaded images synchronously and manually changed `LpImage`. This could cause freezes when loading large images and SVGs. We also had no proper feedback for when a corrupted or unsupported image was loaded. This commit introduces a major rework to how we handle images and the gallery. It introduces a swipeable gallery that loads files asynchronously via a new widget: LpImagePage. LpImagePage owns the file and information related to the file, and on creation it spawns a future that either loads an error page when it can't load something (file deleted, corrupted, or unsupported) or the actual image via LpImage. LpImagePage now holds the right-click popover and the associated gestures. The gestures needed to be moved to LpImagePage in order to rework LpImageView's overlay - instead of having an overlay child that's a box with a few buttons, we now have the buttons as individual overlay children. This could cause them to overlap each other at certain sizes, but it's required in order to allow input to reach the carousel below. Right now this comes with a few regressions: * Occasionally input gets blocked across the applications. It's hard to tell if this is caused by swipes or right clicking. This input issue can cause a crash - no Rust backtrace, so a debugger is needed. * The "current" page doesn't seem to be properly loaded by the time `set_image_from_file ()` finishes, so we need to rework resizing from image dimensions * We need to re-implement loading different directories and files within directories There also seems to be an issue on touchscreens, swiping between images on a touchscreen causes responsiveness issues not present when swiping on a touchpad. My guess is that it's related to the long press gesture.
2022-01-19 23:11:38 -08:00
pub fn set_image_from_file(&self, file: &gio::File, _resize: bool) {
let imp = self.imp();
2021-06-27 15:28:35 -07:00
log::debug!("Loading file: {}", file.uri().to_string());
match imp.image_view.set_image_from_file(file) {
widgets: Rework image_view with AdwCarousel, add ImagePage Previously we loaded images synchronously and manually changed `LpImage`. This could cause freezes when loading large images and SVGs. We also had no proper feedback for when a corrupted or unsupported image was loaded. This commit introduces a major rework to how we handle images and the gallery. It introduces a swipeable gallery that loads files asynchronously via a new widget: LpImagePage. LpImagePage owns the file and information related to the file, and on creation it spawns a future that either loads an error page when it can't load something (file deleted, corrupted, or unsupported) or the actual image via LpImage. LpImagePage now holds the right-click popover and the associated gestures. The gestures needed to be moved to LpImagePage in order to rework LpImageView's overlay - instead of having an overlay child that's a box with a few buttons, we now have the buttons as individual overlay children. This could cause them to overlap each other at certain sizes, but it's required in order to allow input to reach the carousel below. Right now this comes with a few regressions: * Occasionally input gets blocked across the applications. It's hard to tell if this is caused by swipes or right clicking. This input issue can cause a crash - no Rust backtrace, so a debugger is needed. * The "current" page doesn't seem to be properly loaded by the time `set_image_from_file ()` finishes, so we need to rework resizing from image dimensions * We need to re-implement loading different directories and files within directories There also seems to be an issue on touchscreens, swiping between images on a touchscreen causes responsiveness issues not present when swiping on a touchpad. My guess is that it's related to the long press gesture.
2022-01-19 23:11:38 -08:00
Ok((/*width, height*/)) => {
// if resize {
// self.resize_from_dimensions(width, height);
// }
imp.stack.set_visible_child(&*imp.image_view);
imp.image_view.grab_focus();
self.set_actions_enabled(true)
}
Err(e) => log::error!("Could not load file: {}", e.to_string()),
}
}
pub fn set_actions_enabled(&self, enabled: bool) {
self.action_set_enabled("win.open-with", enabled);
self.action_set_enabled("win.set-wallpaper", enabled);
self.action_set_enabled("win.toggle-fullscreen", enabled);
self.action_set_enabled("win.print", enabled);
2022-01-08 14:02:16 +01:00
self.action_set_enabled("win.copy", enabled);
}
// 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) {
let imp = self.imp();
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();
// Ensure the window surface exists
2021-06-27 15:28:35 -07:00
if !self.is_realized() {
self.realize();
}
2021-06-27 15:28:35 -07:00
let display = gdk::Display::default().unwrap();
let monitor = display.monitor_at_surface(&self.native().unwrap().surface());
2021-06-27 15:28:35 -07:00
let monitor_geometry = monitor.geometry();
let monitor_width = monitor_geometry.width();
let monitor_height = monitor_geometry.height();
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);
}
// 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]
fn window_title(&self, file: Option<gio::File>) -> String {
file.and_then(|f| util::get_file_display_name(&f)) // If the file exists, get display name
.unwrap_or_else(|| String::from("Image Viewer")) // Return that or the default if there's nothing
}
// 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
}
}
}