Initial Commit
The initial commit of gnome-image-viewer. Things are in an MVP state, with image viewing and opening from the desktop functional. We also can open images externally, print them, and set them as backgrounds.
This commit is contained in:
commit
f692ab5626
38 changed files with 3807 additions and 0 deletions
208
src/application.rs
Normal file
208
src/application.rs
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
// application.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_macros::*;
|
||||
|
||||
use crate::config;
|
||||
use crate::window::IvWindow;
|
||||
|
||||
mod imp {
|
||||
use super::*;
|
||||
|
||||
// The basic struct that holds our
|
||||
// state and widgets
|
||||
#[derive(Debug)]
|
||||
pub struct IvApplication {
|
||||
pub gtk_settings: gtk::Settings,
|
||||
}
|
||||
|
||||
// Sets up the basics for the GObject
|
||||
impl ObjectSubclass for IvApplication {
|
||||
const NAME: &'static str = "IvApplication";
|
||||
type Type = super::IvApplication;
|
||||
type ParentType = gtk::Application;
|
||||
type Instance = glib::subclass::simple::InstanceStruct<Self>;
|
||||
type Class = glib::subclass::simple::ClassStruct<Self>;
|
||||
|
||||
// This macro implements some boilerplate code
|
||||
// for the object setup, e.g. get_type()
|
||||
glib::object_subclass!();
|
||||
|
||||
// Initialize with default values
|
||||
fn new() -> Self {
|
||||
let gtk_settings =
|
||||
gtk::Settings::get_default().expect("Could not get default GTK settings");
|
||||
|
||||
Self { gtk_settings }
|
||||
}
|
||||
}
|
||||
|
||||
// Overrides GObject vfuncs
|
||||
impl ObjectImpl for IvApplication {
|
||||
fn constructed(&self, obj: &Self::Type) {
|
||||
self.parent_constructed(obj);
|
||||
|
||||
// Set up the CSS and force dark theme
|
||||
let display = gdk::Display::get_default().unwrap();
|
||||
let provider = gtk::CssProvider::new();
|
||||
provider.load_from_resource("/org/gnome/ImageViewer/image-viewer.css");
|
||||
gtk::StyleContext::add_provider_for_display(&display, &provider, 600);
|
||||
self.gtk_settings
|
||||
.set_property_gtk_application_prefer_dark_theme(true);
|
||||
|
||||
// Set up the actions
|
||||
obj.setup_actions();
|
||||
}
|
||||
}
|
||||
|
||||
// Overrides GApplication vfuncs
|
||||
impl ApplicationImpl for IvApplication {
|
||||
fn activate(&self, application: &Self::Type) {
|
||||
let window = IvWindow::new(application);
|
||||
|
||||
application.add_window(&window);
|
||||
window.present();
|
||||
}
|
||||
|
||||
// Handles opening files from the command line or other applications
|
||||
fn open(&self, application: &Self::Type, files: &[gio::File], _hint: &str) {
|
||||
for file in files {
|
||||
let win = IvWindow::new(application);
|
||||
win.set_image_from_file(file);
|
||||
win.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is empty, but we still need to provide an
|
||||
// empty implementation for each type we subclass.
|
||||
impl GtkApplicationImpl for IvApplication {}
|
||||
}
|
||||
|
||||
// Creates a wrapper struct that inherits the functions
|
||||
// from objects listed as @extends or interfaces it @implements.
|
||||
// This is what allows us to do e.g. application.quit() on
|
||||
// IvApplication without casting.
|
||||
glib::wrapper! {
|
||||
pub struct IvApplication(ObjectSubclass<imp::IvApplication>)
|
||||
@extends gio::Application, gtk::Application,
|
||||
@implements gio::ActionGroup, gio::ActionMap;
|
||||
}
|
||||
|
||||
// This is where the member functions of IvApplication go.
|
||||
impl IvApplication {
|
||||
pub fn new() -> Self {
|
||||
glib::Object::new(&[
|
||||
("application-id", &config::APP_ID.to_string()),
|
||||
("flags", &gio::ApplicationFlags::HANDLES_OPEN),
|
||||
])
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn setup_actions(&self) {
|
||||
// action! is a macro from gtk_macros
|
||||
// that creates a GSimpleAction with a callback.
|
||||
// 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.
|
||||
action!(
|
||||
self,
|
||||
"about",
|
||||
clone!(@weak self as app => move |_, _| {
|
||||
app.show_about();
|
||||
})
|
||||
);
|
||||
|
||||
action!(
|
||||
self,
|
||||
"help",
|
||||
clone!(@weak self as app => move |_, _| {
|
||||
app.show_help();
|
||||
})
|
||||
);
|
||||
|
||||
action!(
|
||||
self,
|
||||
"quit",
|
||||
clone!(@weak self as app => move |_, _| {
|
||||
app.quit();
|
||||
})
|
||||
);
|
||||
|
||||
action!(
|
||||
self,
|
||||
"new-window",
|
||||
clone!(@weak self as app => move |_, _| {
|
||||
let win = IvWindow::new(&app);
|
||||
win.show();
|
||||
})
|
||||
);
|
||||
|
||||
self.set_accels_for_action("app.help", &["F1"]);
|
||||
self.set_accels_for_action("app.quit", &["<Primary>Q"]);
|
||||
self.set_accels_for_action("app.new-window", &["<Primary>N"]);
|
||||
self.set_accels_for_action("win.open", &["<Primary>O"]);
|
||||
self.set_accels_for_action("win.print", &["<Primary>P"]);
|
||||
self.set_accels_for_action("win.show-help-overlay", &["<Primary>F1"]);
|
||||
self.set_accels_for_action("win.toggle-fullscreen", &["F11"]);
|
||||
self.set_accels_for_action("window.close", &["<Primary>W"]);
|
||||
}
|
||||
|
||||
pub fn show_about(&self) {
|
||||
// Builders are a pattern that allow you to create
|
||||
// an object and set all relevant properties very
|
||||
// easily in a way that's idiomatic to Rust.
|
||||
let dialog = gtk::AboutDialogBuilder::new()
|
||||
.authors(vec![String::from(
|
||||
"Christopher Davis <christopherdavis@gnome.org>",
|
||||
)])
|
||||
.artists(vec![
|
||||
String::from("Allan Day"),
|
||||
String::from("Jakub Steiner"),
|
||||
String::from("Tobias Bernard"),
|
||||
])
|
||||
.copyright("© 2020 The GNOME Project")
|
||||
.license_type(gtk::License::Gpl30)
|
||||
.program_name("Image Viewer")
|
||||
.logo_icon_name(config::APP_ID)
|
||||
.version(config::VERSION)
|
||||
.build();
|
||||
|
||||
if let Some(window) = self.get_active_window() {
|
||||
dialog.set_modal(true);
|
||||
dialog.set_transient_for(Some(&window));
|
||||
}
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
pub fn show_help(&self) {
|
||||
gtk::show_uri(gtk::NONE_WINDOW, "help:gnome-image-viewer", 0);
|
||||
}
|
||||
}
|
||||
5
src/config.rs.in
Normal file
5
src/config.rs.in
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub const APP_ID: &str = "@APP_ID@";
|
||||
pub const PROFILE: &str = "@PROFILE@";
|
||||
pub const PKGDATADIR: &str = "@pkgdatadir@";
|
||||
pub const VERSION: &str = "@VERSION@";
|
||||
pub const LOCALEDIR: &str = "@localedir@";
|
||||
47
src/main.rs
Normal file
47
src/main.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// main.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 gettextrs::*;
|
||||
use gio::prelude::*;
|
||||
|
||||
mod application;
|
||||
mod config;
|
||||
mod widgets;
|
||||
mod window;
|
||||
|
||||
use application::IvApplication;
|
||||
|
||||
fn main() {
|
||||
gtk::init().expect("Failed to initialize GTK.");
|
||||
libhandy::init();
|
||||
widgets::init();
|
||||
pretty_env_logger::init();
|
||||
|
||||
setlocale(LocaleCategory::LcAll, "");
|
||||
bindtextdomain("gnome-image-viewer", config::LOCALEDIR);
|
||||
textdomain("gnome-image-viewer");
|
||||
|
||||
let res = gio::Resource::load(config::PKGDATADIR.to_owned() + "/gnome-image-viewer.gresource")
|
||||
.expect("Could not load resources");
|
||||
gio::resources_register(&res);
|
||||
|
||||
let app = IvApplication::new();
|
||||
let ret = app.run(&std::env::args().collect::<Vec<_>>());
|
||||
std::process::exit(ret);
|
||||
}
|
||||
43
src/meson.build
Normal file
43
src/meson.build
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
configure_file(
|
||||
input: 'config.rs.in',
|
||||
output: 'config.rs',
|
||||
configuration: conf
|
||||
)
|
||||
|
||||
# Copy the config.rs output to the source directory.
|
||||
run_command(
|
||||
'cp',
|
||||
join_paths(meson.build_root(), 'src', 'config.rs'),
|
||||
join_paths(meson.source_root(), 'src', 'config.rs'),
|
||||
check: true
|
||||
)
|
||||
|
||||
rust_sources = files(
|
||||
'widgets/image_view.rs',
|
||||
'widgets/mod.rs',
|
||||
'application.rs',
|
||||
'config.rs',
|
||||
'main.rs',
|
||||
'window.rs',
|
||||
)
|
||||
|
||||
sources = [cargo_sources, rust_sources]
|
||||
|
||||
cargo_script = find_program(join_paths(meson.source_root(), 'build-aux/cargo.sh'))
|
||||
cargo_release = custom_target(
|
||||
'cargo-build',
|
||||
build_by_default: true,
|
||||
input: sources,
|
||||
output: meson.project_name(),
|
||||
console: true,
|
||||
install: true,
|
||||
install_dir: get_option('bindir'),
|
||||
command: [
|
||||
cargo_script,
|
||||
meson.build_root(),
|
||||
meson.source_root(),
|
||||
'@OUTPUT@',
|
||||
get_option('buildtype'),
|
||||
meson.project_name(),
|
||||
]
|
||||
)
|
||||
291
src/widgets/image_view.rs
Normal file
291
src/widgets/image_view.rs
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
// image_view.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 gdk::prelude::*;
|
||||
use glib::clone;
|
||||
use glib::subclass::prelude::*;
|
||||
use gtk::prelude::*;
|
||||
use gtk::subclass::prelude::*;
|
||||
use gtk::subclass::widget::WidgetImplExt;
|
||||
use gtk::CompositeTemplate;
|
||||
|
||||
use anyhow::Context;
|
||||
use ashpd::desktop::wallpaper::{SetOn, WallpaperOptions, WallpaperProxy};
|
||||
use ashpd::{BasicResponse as Basic, RequestProxy, Response, WindowIdentifier};
|
||||
use std::cell::{Cell, RefCell};
|
||||
|
||||
mod imp {
|
||||
use super::*;
|
||||
use glib::subclass;
|
||||
|
||||
static PROPERTIES: [subclass::Property; 3] = [
|
||||
subclass::Property("header-visible", |header_visible| {
|
||||
glib::ParamSpec::boolean(
|
||||
header_visible,
|
||||
"Header visible",
|
||||
"Whether or not the headerbar is visible",
|
||||
false,
|
||||
glib::ParamFlags::READWRITE,
|
||||
)
|
||||
}),
|
||||
subclass::Property("primary-menu-model", |primary_menu_model| {
|
||||
glib::ParamSpec::object(
|
||||
primary_menu_model,
|
||||
"Primary Menu Model",
|
||||
"The menu model for the menu button",
|
||||
gio::MenuModel::static_type(),
|
||||
glib::ParamFlags::READWRITE,
|
||||
)
|
||||
}),
|
||||
subclass::Property("popover-menu-model", |popover_menu_model| {
|
||||
glib::ParamSpec::object(
|
||||
popover_menu_model,
|
||||
"Popover Menu Model",
|
||||
"The menu model for the menu button",
|
||||
gio::MenuModel::static_type(),
|
||||
glib::ParamFlags::READWRITE,
|
||||
)
|
||||
}),
|
||||
];
|
||||
|
||||
#[derive(Debug, CompositeTemplate)]
|
||||
pub struct IvImageView {
|
||||
#[template_child]
|
||||
pub headerbar: TemplateChild<gtk::HeaderBar>,
|
||||
#[template_child]
|
||||
pub menu_button: TemplateChild<gtk::MenuButton>,
|
||||
#[template_child]
|
||||
pub picture: TemplateChild<gtk::Picture>,
|
||||
#[template_child]
|
||||
pub popover: TemplateChild<gtk::PopoverMenu>,
|
||||
#[template_child]
|
||||
pub click_gesture: TemplateChild<gtk::GestureClick>,
|
||||
#[template_child]
|
||||
pub press_gesture: TemplateChild<gtk::GestureLongPress>,
|
||||
|
||||
pub connection: zbus::Connection,
|
||||
// Cell<T> allows for interior mutability of primitive types.
|
||||
pub header_visible: Cell<bool>,
|
||||
// RefCell<T> does the same for non-primitive types.
|
||||
pub menu_model: RefCell<Option<gio::MenuModel>>,
|
||||
pub popover_menu_model: RefCell<Option<gio::MenuModel>>,
|
||||
}
|
||||
|
||||
impl ObjectSubclass for IvImageView {
|
||||
const NAME: &'static str = "IvImageView";
|
||||
type Type = super::IvImageView;
|
||||
type ParentType = gtk::Widget;
|
||||
type Instance = subclass::simple::InstanceStruct<Self>;
|
||||
type Class = subclass::simple::ClassStruct<Self>;
|
||||
|
||||
glib::object_subclass!();
|
||||
|
||||
fn new() -> Self {
|
||||
let connection =
|
||||
zbus::Connection::new_session().expect("Could not create zbus session");
|
||||
|
||||
Self {
|
||||
headerbar: TemplateChild::default(),
|
||||
menu_button: TemplateChild::default(),
|
||||
picture: TemplateChild::default(),
|
||||
popover: TemplateChild::default(),
|
||||
click_gesture: TemplateChild::default(),
|
||||
press_gesture: TemplateChild::default(),
|
||||
connection,
|
||||
header_visible: Cell::new(false),
|
||||
menu_model: RefCell::default(),
|
||||
popover_menu_model: RefCell::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.install_properties(&PROPERTIES);
|
||||
klass.set_template_from_resource("/org/gnome/ImageViewer/gtk/image_view.ui");
|
||||
Self::bind_template_children(klass);
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for IvImageView {
|
||||
fn constructed(&self, obj: &Self::Type) {
|
||||
obj.init_template();
|
||||
self.parent_constructed(obj);
|
||||
|
||||
obj.bind_property("header-visible", &self.headerbar.get(), "visible")
|
||||
.flags(glib::BindingFlags::BIDIRECTIONAL)
|
||||
.flags(glib::BindingFlags::SYNC_CREATE)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
self.click_gesture
|
||||
.get()
|
||||
.connect_pressed(clone!(@weak obj => move |_, _, x, y| {
|
||||
obj.show_popover_at(x, y);
|
||||
}));
|
||||
|
||||
self.press_gesture
|
||||
.get()
|
||||
.connect_pressed(clone!(@weak obj => move |_, x, y| {
|
||||
obj.show_popover_at(x, y);
|
||||
}));
|
||||
}
|
||||
|
||||
fn get_property(&self, _obj: &Self::Type, id: usize) -> glib::Value {
|
||||
let prop = &PROPERTIES[id];
|
||||
|
||||
match *prop {
|
||||
subclass::Property("header-visible", ..) => self.header_visible.get().to_value(),
|
||||
subclass::Property("primary-menu-model", ..) => self.menu_model.borrow().to_value(),
|
||||
subclass::Property("popover-menu-model", ..) => {
|
||||
self.popover_menu_model.borrow().to_value()
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_property(&self, _obj: &Self::Type, id: usize, value: &glib::Value) {
|
||||
let prop = &PROPERTIES[id];
|
||||
|
||||
match *prop {
|
||||
subclass::Property("header-visible", ..) => {
|
||||
self.header_visible
|
||||
.set(value.get().unwrap().unwrap_or_default());
|
||||
}
|
||||
subclass::Property("primary-menu-model", ..) => {
|
||||
let model: Option<gio::MenuModel> = value.get().unwrap();
|
||||
self.menu_button.get().set_menu_model(model.as_ref());
|
||||
*self.menu_model.borrow_mut() = model;
|
||||
}
|
||||
subclass::Property("popover-menu-model", ..) => {
|
||||
let model: Option<gio::MenuModel> = value.get().unwrap();
|
||||
self.popover.get().set_menu_model(model.as_ref());
|
||||
*self.popover_menu_model.borrow_mut() = model;
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn dispose(&self, obj: &Self::Type) {
|
||||
log::debug!("Disposing {}", Self::NAME);
|
||||
if let Some(child) = obj.get_first_child() {
|
||||
while let Some(sibling) = child.get_next_sibling() {
|
||||
sibling.unparent();
|
||||
}
|
||||
child.unparent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for IvImageView {
|
||||
fn size_allocate(&self, widget: &Self::Type, width: i32, height: i32, baseline: i32) {
|
||||
self.parent_size_allocate(widget, width, height, baseline);
|
||||
self.popover.get().present();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct IvImageView(ObjectSubclass<imp::IvImageView>)
|
||||
@extends gtk::Widget,
|
||||
@implements gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
|
||||
}
|
||||
|
||||
impl IvImageView {
|
||||
pub fn set_image_from_file(&self, file: &gio::File) {
|
||||
let imp = imp::IvImageView::from_instance(&self);
|
||||
|
||||
imp.picture.get().set_file(Some(file));
|
||||
}
|
||||
|
||||
pub fn set_wallpaper(&self) -> anyhow::Result<()> {
|
||||
let imp = imp::IvImageView::from_instance(&self);
|
||||
|
||||
let wallpaper = self.uri().context("No URI for current file")?;
|
||||
let wallpaper_proxy = WallpaperProxy::new(&imp.connection)?;
|
||||
|
||||
let request_handle = wallpaper_proxy.set_wallpaper_uri(
|
||||
WindowIdentifier::default(),
|
||||
&wallpaper,
|
||||
WallpaperOptions::default().set_on(SetOn::Background),
|
||||
)?;
|
||||
|
||||
let request = RequestProxy::new(&imp.connection, &request_handle)?;
|
||||
request.on_response(|response: Response<Basic>| {
|
||||
log::debug!("Response is OK: {}", response.is_ok());
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn print(&self) -> anyhow::Result<()> {
|
||||
let imp = imp::IvImageView::from_instance(&self);
|
||||
|
||||
let file = imp.picture.get().get_file().context("No file to print")?;
|
||||
let operation = gtk::PrintOperation::new();
|
||||
let path = file.get_path().context("No path for current file")?;
|
||||
let pb = gdk_pixbuf::Pixbuf::from_file(path)?;
|
||||
|
||||
let setup = gtk::PageSetup::default();
|
||||
let page_size = gtk::PaperSize::new(Some(>k::PAPER_NAME_A4));
|
||||
setup.set_paper_size(&page_size);
|
||||
operation.set_default_page_setup(Some(&setup));
|
||||
|
||||
let settings = gtk::PrintSettings::default();
|
||||
operation.set_print_settings(Some(&settings));
|
||||
|
||||
operation.connect_begin_print(move |op, _| {
|
||||
op.set_n_pages(1);
|
||||
});
|
||||
|
||||
operation.connect_draw_page(clone!(@weak pb => move |op, ctx, _| {
|
||||
let cr = ctx.get_cairo_context().expect("No cairo context for print context");
|
||||
cr.set_source_pixbuf(&pb, 0.0, 0.0);
|
||||
cr.paint();
|
||||
}));
|
||||
|
||||
log::debug!("Running print operation...");
|
||||
let root = self.get_root().context("Could not get root for widget")?;
|
||||
let window = root
|
||||
.downcast_ref::<gtk::Window>()
|
||||
.context("Could not downcast to GtkWindow")?;
|
||||
operation.run(gtk::PrintOperationAction::PrintDialog, Some(window))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn uri(&self) -> Option<String> {
|
||||
let imp = imp::IvImageView::from_instance(&self);
|
||||
let file = imp.picture.get().get_file()?;
|
||||
Some(file.get_uri().to_string())
|
||||
}
|
||||
|
||||
pub fn show_popover_at(&self, x: f64, y: f64) {
|
||||
let imp = imp::IvImageView::from_instance(&self);
|
||||
let popover = imp.popover.get();
|
||||
|
||||
let rect = gdk::Rectangle {
|
||||
x: x as i32,
|
||||
y: y as i32,
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
|
||||
popover.set_pointing_to(&rect);
|
||||
popover.popup();
|
||||
}
|
||||
}
|
||||
32
src/widgets/mod.rs
Normal file
32
src/widgets/mod.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// mod.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 glib::prelude::*;
|
||||
|
||||
mod image_view;
|
||||
|
||||
pub use image_view::IvImageView;
|
||||
|
||||
pub fn init() {
|
||||
// For each widget we use in a template,
|
||||
// we need to ensure the type is set up.
|
||||
// This is equivalent to doing g_type_ensure ()
|
||||
// in C.
|
||||
IvImageView::static_type();
|
||||
}
|
||||
296
src/window.rs
Normal file
296
src/window.rs
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
// 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 libhandy::subclass::prelude as hdy;
|
||||
|
||||
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.
|
||||
#[derive(Debug, CompositeTemplate)]
|
||||
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<libhandy::StatusPage>,
|
||||
#[template_child]
|
||||
pub image_view: TemplateChild<IvImageView>,
|
||||
#[template_child]
|
||||
pub open_gesture: TemplateChild<gtk::GestureClick>,
|
||||
}
|
||||
|
||||
impl ObjectSubclass for IvWindow {
|
||||
const NAME: &'static str = "IvWindow";
|
||||
type Type = super::IvWindow;
|
||||
type ParentType = libhandy::ApplicationWindow;
|
||||
type Instance = glib::subclass::simple::InstanceStruct<Self>;
|
||||
type Class = glib::subclass::simple::ClassStruct<Self>;
|
||||
|
||||
glib::object_subclass!();
|
||||
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
// For the initial value, use this function.
|
||||
headerbar: TemplateChild::default(),
|
||||
stack: TemplateChild::default(),
|
||||
status_page: TemplateChild::default(),
|
||||
image_view: TemplateChild::default(),
|
||||
open_gesture: TemplateChild::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.set_template_from_resource("/org/gnome/ImageViewer/gtk/window.ui");
|
||||
// bind_template_children() is a function generated by the
|
||||
// CompositeTemplate macro to bind all children at once.
|
||||
Self::bind_template_children(klass);
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for IvWindow {
|
||||
fn constructed(&self, obj: &Self::Type) {
|
||||
obj.init_template();
|
||||
self.parent_constructed(obj);
|
||||
|
||||
if config::PROFILE == ".Devel" {
|
||||
obj.add_css_class("devel");
|
||||
}
|
||||
|
||||
obj.setup_actions();
|
||||
|
||||
self.status_page
|
||||
.get() // .get() is needed for now to use template children.
|
||||
.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.get_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
|
||||
.get()
|
||||
.connect_released(clone!(@weak obj => move |_, _, _, _| {
|
||||
obj.pick_file();
|
||||
}));
|
||||
|
||||
obj.bind_property("fullscreened", &self.image_view.get(), "header-visible")
|
||||
.flags(glib::BindingFlags::SYNC_CREATE | glib::BindingFlags::BIDIRECTIONAL)
|
||||
.build()
|
||||
.unwrap();
|
||||
obj.bind_property("fullscreened", &self.headerbar.get(), "visible")
|
||||
.flags(
|
||||
glib::BindingFlags::SYNC_CREATE
|
||||
| glib::BindingFlags::BIDIRECTIONAL
|
||||
| glib::BindingFlags::INVERT_BOOLEAN,
|
||||
)
|
||||
.build()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for IvWindow {}
|
||||
impl WindowImpl for IvWindow {}
|
||||
impl ApplicationWindowImpl for IvWindow {}
|
||||
impl hdy::ApplicationWindowImpl for IvWindow {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct IvWindow(ObjectSubclass<imp::IvWindow>)
|
||||
@extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, libhandy::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_property_fullscreened(!win.get_property_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.get_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.get().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.get().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.get().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);
|
||||
|
||||
imp.image_view.get().set_image_from_file(file);
|
||||
imp.stack.get().set_visible_child(&imp.image_view.get());
|
||||
|
||||
log::debug!("Loading file: {}", file.get_uri().to_string());
|
||||
|
||||
if let Some(name) = file.get_basename() {
|
||||
self.set_title(Some(
|
||||
name.file_name().expect("Missing name").to_str().unwrap(),
|
||||
));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue