This commit adds directory navigation to Image Viewer. Currently it only sorts images by name, instead of following directory sorting like the mockups.
322 lines
10 KiB
Rust
322 lines
10 KiB
Rust
// 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 gio::prelude::*;
|
|
use glib::clone;
|
|
use glib::subclass::prelude::*;
|
|
use gtk::prelude::*;
|
|
use gtk::subclass::prelude::*;
|
|
use gtk::CompositeTemplate;
|
|
use gtk_macros::*;
|
|
use libadwaita::subclass::prelude::*;
|
|
|
|
use crate::config;
|
|
use crate::widgets::IvImageView;
|
|
|
|
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.
|
|
//
|
|
// 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/ImageViewer/gtk/window.ui")]
|
|
pub struct IvWindow {
|
|
// Template children are used with the
|
|
// TemplateChild<T> wrapper, where T is the
|
|
// object type of the template child.
|
|
#[template_child]
|
|
pub headerbar: TemplateChild<gtk::HeaderBar>,
|
|
#[template_child]
|
|
pub stack: TemplateChild<gtk::Stack>,
|
|
#[template_child]
|
|
pub status_page: TemplateChild<libadwaita::StatusPage>,
|
|
#[template_child]
|
|
pub image_view: TemplateChild<IvImageView>,
|
|
#[template_child]
|
|
pub open_gesture: TemplateChild<gtk::GestureClick>,
|
|
}
|
|
|
|
#[glib::object_subclass]
|
|
impl ObjectSubclass for IvWindow {
|
|
const NAME: &'static str = "IvWindow";
|
|
type Type = super::IvWindow;
|
|
type ParentType = libadwaita::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);
|
|
}
|
|
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
|
obj.init_template();
|
|
}
|
|
}
|
|
|
|
impl ObjectImpl for IvWindow {
|
|
fn constructed(&self, obj: &Self::Type) {
|
|
self.parent_constructed(obj);
|
|
|
|
if config::PROFILE == ".Devel" {
|
|
obj.add_css_class("devel");
|
|
}
|
|
|
|
obj.setup_actions();
|
|
|
|
self.status_page
|
|
.set_icon_name(Some(&format!("{}-symbolic", config::APP_ID)));
|
|
|
|
// Set help overlay
|
|
let builder = gtk::Builder::from_resource("/org/gnome/ImageViewer/gtk/help_overlay.ui");
|
|
let help_overlay = builder.object("help_overlay").unwrap();
|
|
obj.set_help_overlay(Some(&help_overlay));
|
|
|
|
// 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 none to react to the gesture,
|
|
// so "_" is used in all 4 spots.
|
|
self.open_gesture
|
|
.connect_released(clone!(@weak obj => move |_, _, _, _| {
|
|
obj.pick_file();
|
|
}));
|
|
|
|
self.image_view.connect_dimensions_loaded(
|
|
clone!(@weak obj => move |_, width, height| {
|
|
obj.resize_from_dimensions(width, height);
|
|
}),
|
|
);
|
|
|
|
// Property bindings didn't seem to work here
|
|
self.image_view.connect_notify_local(
|
|
Some("filename"),
|
|
clone!(@weak obj => move |iv, _| {
|
|
if let Some(filename) = iv.filename() {
|
|
obj.set_title(Some(&filename));
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
impl WidgetImpl for IvWindow {}
|
|
impl WindowImpl for IvWindow {}
|
|
impl ApplicationWindowImpl for IvWindow {}
|
|
impl AdwApplicationWindowImpl for IvWindow {}
|
|
}
|
|
|
|
glib::wrapper! {
|
|
pub struct IvWindow(ObjectSubclass<imp::IvWindow>)
|
|
@extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, libadwaita::ApplicationWindow,
|
|
@implements gio::ActionMap, gio::ActionGroup;
|
|
}
|
|
|
|
impl IvWindow {
|
|
pub fn new<A: IsA<gtk::Application>>(app: &A) -> Self {
|
|
glib::Object::new(&[("application", app)]).expect("Failed to create IvWindow")
|
|
}
|
|
|
|
pub fn setup_actions(&self) {
|
|
action!(
|
|
self,
|
|
"toggle-fullscreen",
|
|
clone!(@weak self as win => move |_, _| {
|
|
win.set_fullscreened(!win.is_fullscreened());
|
|
})
|
|
);
|
|
|
|
action!(
|
|
self,
|
|
"open",
|
|
clone!(@weak self as win => move |_, _| {
|
|
win.pick_file();
|
|
})
|
|
);
|
|
|
|
action!(
|
|
self,
|
|
"open-with",
|
|
clone!(@weak self as win => move |_, _| {
|
|
win.open_with();
|
|
})
|
|
);
|
|
|
|
action!(
|
|
self,
|
|
"set-wallpaper",
|
|
clone!(@weak self as win => move |_, _| {
|
|
win.set_wallpaper();
|
|
})
|
|
);
|
|
|
|
action!(
|
|
self,
|
|
"print",
|
|
clone!(@weak self as win => move |_, _| {
|
|
win.print();
|
|
})
|
|
);
|
|
|
|
self.set_actions_enabled(false);
|
|
}
|
|
|
|
fn pick_file(&self) {
|
|
let chooser = gtk::FileChooserNative::new(
|
|
Some(&"Open Image".to_string()),
|
|
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"))
|
|
.unwrap();
|
|
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 {
|
|
if let Some(file) = chooser.file() {
|
|
win.set_image_from_file(&file);
|
|
}
|
|
}
|
|
}),
|
|
);
|
|
|
|
chooser.show();
|
|
}
|
|
|
|
fn open_with(&self) {
|
|
let imp = imp::IvWindow::from_instance(self);
|
|
|
|
if let Some(uri) = imp.image_view.uri() {
|
|
std::process::Command::new("xdg-open")
|
|
.arg(uri)
|
|
.output()
|
|
.unwrap();
|
|
} else {
|
|
log::error!("No URI for current image.")
|
|
}
|
|
}
|
|
|
|
fn set_wallpaper(&self) {
|
|
let imp = imp::IvWindow::from_instance(self);
|
|
|
|
if let Err(e) = imp.image_view.set_wallpaper() {
|
|
log::error!("Failed to set wallpaper: {}", e);
|
|
}
|
|
}
|
|
|
|
fn print(&self) {
|
|
let imp = imp::IvWindow::from_instance(self);
|
|
|
|
if let Err(e) = imp.image_view.print() {
|
|
log::error!("Failed to print file: {}", e);
|
|
}
|
|
}
|
|
|
|
pub fn set_image_from_file(&self, file: &gio::File) {
|
|
let imp = imp::IvWindow::from_instance(self);
|
|
|
|
log::debug!("Loading file: {}", file.uri().to_string());
|
|
|
|
imp.image_view.set_image_from_file(file);
|
|
imp.stack.set_visible_child(&*imp.image_view);
|
|
imp.image_view.grab_focus();
|
|
|
|
self.set_actions_enabled(true);
|
|
}
|
|
|
|
pub fn set_actions_enabled(&self, enabled: bool) {
|
|
// Here we need to downcast because lookup_action()` returns a plain
|
|
// GAction, not a GSimpleAction.
|
|
self.lookup_action("open-with")
|
|
.unwrap()
|
|
.downcast::<gio::SimpleAction>()
|
|
.unwrap()
|
|
.set_enabled(enabled);
|
|
self.lookup_action("set-wallpaper")
|
|
.unwrap()
|
|
.downcast::<gio::SimpleAction>()
|
|
.unwrap()
|
|
.set_enabled(enabled);
|
|
self.lookup_action("toggle-fullscreen")
|
|
.unwrap()
|
|
.downcast::<gio::SimpleAction>()
|
|
.unwrap()
|
|
.set_enabled(enabled);
|
|
self.lookup_action("print")
|
|
.unwrap()
|
|
.downcast::<gio::SimpleAction>()
|
|
.unwrap()
|
|
.set_enabled(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 = imp::IvWindow::from_instance(self);
|
|
let mut final_width = img_width;
|
|
let mut final_height = img_height;
|
|
|
|
let header_height = imp.headerbar.height();
|
|
|
|
// Ensure the window surface exists
|
|
if !self.is_realized() {
|
|
self.realize();
|
|
}
|
|
|
|
let display = gdk::Display::default().unwrap();
|
|
let monitor = display
|
|
.monitor_at_surface(&self.native().unwrap().surface().unwrap())
|
|
.unwrap();
|
|
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);
|
|
}
|
|
}
|