Separate window show ui and code to enable editing

This commit is contained in:
Sophie Herold 2024-09-14 00:48:46 +02:00
parent 0012d807e3
commit 9205c93fae
8 changed files with 527 additions and 382 deletions

View file

@ -28,6 +28,7 @@ mod properties_view;
mod shy_bin;
mod sliding_view;
mod window_content;
mod window_image;
pub use drag_overlay::LpDragOverlay;
pub use error_details::LpErrorDetails;
@ -40,3 +41,4 @@ pub use properties_view::LpPropertiesView;
pub use shy_bin::LpShyBin;
pub use sliding_view::LpSlidingView;
pub use window_content::LpWindowContent;
pub use window_image::LpWindowImage;

View file

@ -6,11 +6,13 @@
<lookup name="root">LpWindowContent</lookup>
</lookup>
</binding>
<!--
<binding name="is-showing-image">
<lookup name="is-showing-image" type="LpWindow">
<lookup name="root">LpWindowContent</lookup>
</lookup>
</binding>
-->
<property name="child">
<object class="AdwToolbarView" id="toolbar_view">
<child type="top">

View file

@ -35,8 +35,8 @@ use std::path::{Path, PathBuf};
use actions::*;
use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::{clone, Properties};
use gtk::{CompositeTemplate, Widget};
use adw::{glib, gtk};
use gtk::Widget;
use crate::application::LpApplication;
use crate::config;
@ -44,9 +44,7 @@ use crate::deps::*;
use crate::util::gettext::*;
use crate::util::Direction;
use crate::widgets::{LpDragOverlay, LpImage, LpImageView, LpPropertiesView, LpWindowContent};
/// Show window after X milliseconds even if image dimensions are not known yet
const SHOW_WINDOW_AFTER: u64 = 2000;
use crate::window::LpWindow;
/// Animation duration for showing overlay buttons in milliseconds
const SHOW_CONTROLS_ANIMATION_DURATION: u32 = 200;
@ -57,43 +55,30 @@ const HIDE_CONTROLS_IDLE_TIMEOUT: u64 = 3000;
mod imp {
use gio::glib::VariantTy;
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
#[properties(wrapper_type = super::LpWindowImage)]
#[template(file = "window_image.ui")]
pub struct LpWindowImage {
#[property(get, construct_only)]
window: OnceCell<LpWindow>,
// 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, Properties, CompositeTemplate)]
#[properties(wrapper_type = super::LpWindow)]
#[template(file = "window.ui")]
pub struct LpWindow {
#[template_child]
pub(super) window_content: TemplateChild<LpWindowContent>,
#[template_child]
pub(super) headerbar: TemplateChild<adw::HeaderBar>,
#[template_child]
pub(super) headerbar_events: TemplateChild<gtk::EventControllerMotion>,
#[template_child]
pub(super) properties_button: TemplateChild<gtk::ToggleButton>,
#[template_child]
pub(super) primary_menu: TemplateChild<gtk::MenuButton>,
#[template_child]
pub(super) fullscreen_button: TemplateChild<gtk::Button>,
#[template_child]
pub(super) toast_overlay: TemplateChild<adw::ToastOverlay>,
pub(super) properties_button: TemplateChild<gtk::ToggleButton>,
#[template_child]
pub(super) primary_menu: TemplateChild<gtk::MenuButton>,
#[template_child]
pub(super) stack: TemplateChild<gtk::Stack>,
#[template_child]
@ -129,10 +114,14 @@ mod imp {
}
#[glib::object_subclass]
impl ObjectSubclass for LpWindow {
const NAME: &'static str = "LpWindow";
type Type = super::LpWindow;
type ParentType = adw::ApplicationWindow;
impl ObjectSubclass for LpWindowImage {
const NAME: &'static str = "LpWindowImage";
type Type = super::LpWindowImage;
type ParentType = adw::Bin;
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
fn class_init(klass: &mut Self::Class) {
// bind_template() is a function generated by the
@ -159,45 +148,31 @@ mod imp {
ActionPartGlobal::init_actions_and_bindings(klass);
Action::init_actions_and_bindings(klass);
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));
}
},
);
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for LpWindow {
impl ObjectImpl for LpWindowImage {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
self.forward_click_gesture.connect_pressed(clone!(
self.forward_click_gesture.connect_pressed(glib::clone!(
#[weak]
obj,
move |_, _, _, _| {
obj.image_view().navigate(Direction::Forward, false);
}
));
self.backward_click_gesture.connect_pressed(clone!(
self.backward_click_gesture.connect_pressed(glib::clone!(
#[weak]
obj,
move |_, _, _, _| {
obj.image_view().navigate(Direction::Back, false);
}
));
self.properties_button.connect_toggled(clone!(
self.properties_button.connect_toggled(glib::clone!(
#[weak]
obj,
move |_| {
@ -205,25 +180,6 @@ mod imp {
}
));
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.on_current_page_changed();
obj.on_fullscreen_changed();
@ -233,16 +189,12 @@ mod imp {
move |_| obj.on_current_page_changed()
));
obj.connect_fullscreened_notify(glib::clone!(
obj.window().connect_fullscreened_notify(glib::clone!(
#[weak]
obj,
move |_| obj.on_fullscreen_changed()
));
obj.connect_map(|win| {
win.resize_default();
});
let gesture_click = gtk::GestureClick::new();
gesture_click.connect_pressed(glib::clone!(
#[weak]
@ -392,15 +344,23 @@ mod imp {
));
// Activate global shortcuts only if no dialog is open
obj.connect_visible_dialog_notify(|obj| obj.update_accel_status());
obj.connect_is_active_notify(|obj| obj.update_accel_status());
obj.window().connect_visible_dialog_notify(glib::clone!(
#[weak]
obj,
move |_| obj.update_accel_status()
));
obj.window().connect_is_active_notify(glib::clone!(
#[weak]
obj,
move |_| obj.update_accel_status()
));
self.status_page
.set_icon_name(Some(&format!("{}-symbolic", config::APP_ID)));
self.drop_target.set_types(&[gdk::FileList::static_type()]);
self.drop_target.connect_accept(clone!(
self.drop_target.connect_accept(glib::clone!(
#[weak]
obj,
#[upgrade_or]
@ -420,7 +380,7 @@ mod imp {
// 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!(
self.drop_target.connect_drop(glib::clone!(
#[weak]
obj,
#[upgrade_or]
@ -451,7 +411,7 @@ mod imp {
}
// Maybe one day this will actually work
obj.present();
obj.window().present();
true
}
@ -459,12 +419,10 @@ mod imp {
}
}
impl WidgetImpl for LpWindow {}
impl WindowImpl for LpWindow {}
impl ApplicationWindowImpl for LpWindow {}
impl AdwApplicationWindowImpl for LpWindow {}
impl WidgetImpl for LpWindowImage {}
impl BinImpl for LpWindowImage {}
impl LpWindow {
impl LpWindowImage {
fn is_showing_image(&self) -> bool {
self.image_view.try_get().map_or(false, |image_view| {
image_view
@ -476,20 +434,11 @@ mod imp {
}
glib::wrapper! {
pub struct LpWindow(ObjectSubclass<imp::LpWindow>)
@extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, adw::ApplicationWindow,
@implements gio::ActionMap, gio::ActionGroup, gtk::Native;
pub struct LpWindowImage(ObjectSubclass<imp::LpWindowImage>)
@extends gtk::Widget, adw::Bin;
}
impl LpWindow {
pub fn new<A: IsA<gtk::Application>>(app: &A) -> Self {
glib::Object::builder().property("application", app).build()
}
fn toggle_fullscreen(&self, fullscreen: bool) {
self.set_fullscreened(fullscreen);
}
impl LpWindowImage {
fn zoom_out_cursor(&self) {
if let Some(image) = self.imp().image_view.current_image() {
image.zoom_out_cursor();
@ -563,7 +512,7 @@ impl LpWindow {
.as_ref(),
);
if let Ok(selected) = chooser.open_multiple_future(Some(self)).await {
if let Ok(selected) = chooser.open_multiple_future(Some(&self.window())).await {
let images: Vec<_> = selected
.into_iter()
.filter_map(|files| {
@ -584,7 +533,7 @@ impl LpWindow {
if let Some(ref file) = imp.image_view.current_file() {
let launcher = gtk::FileLauncher::new(Some(file));
launcher.set_always_ask(true);
if let Err(e) = launcher.launch_future(Some(self)).await {
if let Err(e) = launcher.launch_future(Some(&self.window())).await {
if !e.matches(gtk::DialogError::Dismissed) {
log::error!("Could not open image in external program: {}", e);
}
@ -616,7 +565,7 @@ impl LpWindow {
/// Returns true if some text in metadata is currently selected
fn has_metadata_selected(&self) -> bool {
if let Some(focus_widget) = self.focus() {
if let Some(focus_widget) = self.window().focus() {
if focus_widget.is_ancestor(&*self.imp().properties_view) {
if let Ok(label) = focus_widget.downcast::<gtk::Label>() {
return label.selection_bounds().is_some();
@ -634,7 +583,7 @@ impl LpWindow {
if let Err(e) = imp.image_view.copy() {
log::error!("Failed to copy to clipboard: {}", e);
} else {
self.show_toast(
self.window().show_toast(
gettext("Image copied to clipboard"),
adw::ToastPriority::High,
);
@ -661,17 +610,17 @@ impl LpWindow {
.priority(adw::ToastPriority::High)
.build();
toast.connect_button_clicked(glib::clone!(
#[weak(rename_to = win)]
#[weak(rename_to = obj)]
self,
move |_| {
let path = path.clone();
glib::spawn_future_local(async move {
win.image_view()
obj.image_view()
.set_trash_restore(Some(gio::File::for_path(&path)));
let result = crate::util::untrash(&path).await;
if let Err(err) = result {
log::error!("Failed to untrash {path:?}: {err}");
win.show_toast(
obj.window().show_toast(
gettext("Failed to restore image from trash"),
adw::ToastPriority::High,
);
@ -679,14 +628,15 @@ impl LpWindow {
});
}
));
self.imp().toast_overlay.add_toast(toast);
todo!();
//self.imp().toast_overlay.add_toast(toast);
}
Err(err) => {
if Some(gio::IOErrorEnum::NotSupported) == err.kind::<gio::IOErrorEnum>() {
self.delete_future(&path).await;
} else {
log::error!("Failed to delete file {path:?}: {err}");
self.show_toast(
self.window().show_toast(
gettext("Failed to move image to trash"),
adw::ToastPriority::Normal,
);
@ -732,7 +682,7 @@ impl LpWindow {
if let Err(err) = result {
log::error!("Failed to delete file {path:?}: {err}");
self.show_toast(
self.window().show_toast(
gettext("Failed to delete image"),
adw::ToastPriority::Normal,
);
@ -748,15 +698,6 @@ impl LpWindow {
self.imp().headerbar.clone()
}
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);
}
/// Set status for actions that are related to the active image
fn update_image_actions_status(&self) {
// Actions that are enabled if the image is shown
@ -866,7 +807,7 @@ impl LpWindow {
imp.stack.set_visible_child(&*imp.status_page);
imp.status_page.grab_focus();
// Leave fullscreen since status page has no controls to leave it
self.set_fullscreened(false);
self.window().set_fullscreened(false);
}
imp.properties_button.set_sensitive(has_image);
@ -896,7 +837,7 @@ impl LpWindow {
.and_then(|x| x.metadata().file_name())
.unwrap_or_else(|| gettext("Image Viewer"));
self.set_title(Some(&title));
self.window().set_title(Some(&title));
}
pub fn image_size_available(&self) {
@ -913,41 +854,7 @@ impl LpWindow {
if image.is_some_and(|img| img.image_size_available()) {
log::debug!("Showing window because image size is ready");
self.present();
}
}
pub fn resize_default(&self) {
let imp = self.imp();
if self
.image_view()
.current_image()
.is_some_and(|img| img.image_size_available())
{
let shows_properties = imp.properties_button.is_active();
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);
self.window().present();
}
}
@ -962,19 +869,19 @@ impl LpWindow {
if current_page.is_some_and(|page| page.image().error().is_some()) {
log::debug!("Showing window because loading image failed");
self.present();
self.window().present();
}
}
fn on_fullscreen_changed(&self) {
let icon = if self.is_fullscreen() {
let icon = if self.window().is_fullscreen() {
"view-restore-symbolic"
} else {
"view-fullscreen-symbolic"
};
self.imp().fullscreen_button.set_icon_name(icon);
if !self.is_fullscreen() {
if !self.window().is_fullscreen() {
self.set_cursor(None);
self.show_controls();
}
@ -990,14 +897,14 @@ impl LpWindow {
}
fn update_accel_status(&self) {
let Some(application) = self.application() else {
let Some(application) = self.window().application() else {
log::error!("No application for window found");
return;
};
// Only change status if active window
if self.is_active() {
if self.visible_dialog().is_some() {
if self.window().is_active() {
if self.window().visible_dialog().is_some() {
// If AdwDialog is visible, remove global accels that are for the main window
ActionPartGlobal::remove_accels(&application);
} else {
@ -1007,8 +914,7 @@ impl LpWindow {
}
}
async fn show_about(&self) {
let about = crate::about::dialog().await;
about.present(Some(self));
pub fn properties_button(&self) -> gtk::ToggleButton {
self.imp().properties_button.clone()
}
}

View file

@ -56,25 +56,7 @@
</item>
</section>
</menu>
<template class="LpWindow" parent="AdwApplicationWindow">
<property name="default-width">600</property>
<property name="default-height">498</property>
<property name="width-request">360</property>
<property name="height-request">294</property>
<child>
<object class="AdwBreakpoint">
<condition>max-width: 590sp</condition>
<setter object="multi_layout_view" property="layout-name">bottom-properties</setter>
</object>
</child>
<child>
<object class="AdwBreakpoint">
<condition>max-width: 590sp and max-height: 550sp</condition>
<setter object="multi_layout_view" property="layout-name">overlay-properties</setter>
</object>
</child>
<property name="content">
<object class="AdwToastOverlay" id="toast_overlay">
<template class="LpWindowImage" parent="AdwBin">
<property name="child">
<object class="AdwMultiLayoutView" id="multi_layout_view">
<child>
@ -192,18 +174,26 @@
<!-- Layout slots -->
<child type="headerbar">
<object class="AdwHeaderBar" id="headerbar">
<property name="show-end-title-buttons" bind-source="LpWindow" bind-property="fullscreened" bind-flags="sync-create|invert-boolean" />
<property name="show-start-title-buttons" bind-source="LpWindow" bind-property="fullscreened" bind-flags="sync-create|invert-boolean" />
<!--
<property name="show-end-title-buttons" bind-source="window" bind-property="fullscreened" bind-flags="sync-create|invert-boolean" />
<property name="show-start-title-buttons" bind-source="window" bind-property="fullscreened" bind-flags="sync-create|invert-boolean" />
-->
<style>
<class name="fullscreen-osd" />
</style>
<child type="start">
<object class="GtkButton" id="fullscreen_button">
<property name="halign">end</property>
<property name="action-name">win.toggle-fullscreen</property>
<property name="tooltip-text" translatable="yes">Toggle Fullscreen</property>
</object>
</child>
<child type="start">
<object class="GtkButton" id="edit_button">
<property name="action-name">win.edit</property>
<property name="icon-name">document-edit-symbolic</property>
<property name="tooltip-text" translatable="yes">Edit Image</property>
</object>
</child>
<child type="end">
<object class="GtkMenuButton" id="primary_menu">
<property name="icon-name">open-menu-symbolic</property>
@ -302,7 +292,5 @@
</child>
</object>
</property>
</object>
</property>
</template>
</interface>

View file

@ -78,7 +78,9 @@ impl ActionPartGlobal {
}
}
pub fn init_actions_and_bindings(klass: &mut <super::imp::LpWindow as ObjectSubclass>::Class) {
pub fn init_actions_and_bindings(
klass: &mut <super::imp::LpWindowImage as ObjectSubclass>::Class,
) {
for action in Self::iter() {
match action {
ActionPartGlobal::ImageLeftInstant => {
@ -198,10 +200,14 @@ pub enum Action {
About,
#[strum(to_string = "win.reload")]
Reload,
#[strum(to_string = "win.edit")]
Edit,
}
impl Action {
pub fn init_actions_and_bindings(klass: &mut <super::imp::LpWindow as ObjectSubclass>::Class) {
pub fn init_actions_and_bindings(
klass: &mut <super::imp::LpWindowImage as ObjectSubclass>::Class,
) {
for action in Self::iter() {
match action {
// Navigation
@ -430,15 +436,16 @@ impl Action {
}
Action::ToggleFullscreen => {
klass.install_action(&action, None, move |win, _, _| {
klass.install_action(&action, None, move |obj, _, _| {
let win = obj.window();
win.toggle_fullscreen(!win.is_fullscreen());
});
klass.add_binding_action(gdk::Key::F11, gdk::ModifierType::empty(), &action);
}
Action::LeaveFullscreen => {
klass.install_action(&action, None, move |win, _, _| {
win.toggle_fullscreen(false);
klass.install_action(&action, None, move |obj, _, _| {
obj.window().toggle_fullscreen(false);
});
klass.add_binding_action(gdk::Key::Escape, gdk::ModifierType::empty(), &action);
}
@ -487,8 +494,8 @@ impl Action {
}
Action::About => {
klass.install_action_async(&action, None, |win, _, _| async move {
win.show_about().await
klass.install_action_async(&action, None, |obj, _, _| async move {
obj.window().show_about().await
});
}
@ -502,6 +509,13 @@ impl Action {
});
klass.add_binding_action(gdk::Key::F5, gdk::ModifierType::empty(), &action);
}
Action::Edit => {
klass.install_action_async(&action, None, move |obj, _, _| async move {
obj.window().show_edit();
});
klass.add_binding_action(gdk::Key::e, gdk::ModifierType::empty(), &action);
}
}
}
}

View file

@ -20,22 +20,24 @@
use super::*;
/// Manage visibility of overlay controls
impl LpWindow {
impl LpWindowImage {
/// Set control opacity
///
/// Also changes headerbar transparency if it flat and hides/shows cursor on
/// fullscreen
fn set_control_opacity(&self, opacity: f64, hiding: bool) {
let window = self.window();
self.image_view().controls_box_start().set_opacity(opacity);
self.image_view().controls_box_end().set_opacity(opacity);
if self.is_headerbar_flat() && self.is_fullscreen() {
if self.is_headerbar_flat() && window.is_fullscreen() {
self.headerbar().set_opacity(opacity);
} else {
self.headerbar().set_opacity(1.);
}
if self.is_fullscreen() && hiding && opacity < 0.9 {
if window.is_fullscreen() && hiding && opacity < 0.9 {
self.set_cursor(gdk::Cursor::from_name("none", None).as_ref());
} else {
self.set_cursor(None);

199
src/window.rs Normal file
View file

@ -0,0 +1,199 @@
// 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
use adw::prelude::*;
use adw::subclass::prelude::*;
use gtk::CompositeTemplate;
use crate::config;
use crate::deps::*;
use crate::widgets::LpImageView;
/// Show window after X milliseconds even if image dimensions are not known yet
const SHOW_WINDOW_AFTER: u64 = 2000;
mod imp {
use gio::glib::VariantTy;
use super::*;
use crate::widgets::LpWindowImage;
// 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(file = "window.ui")]
pub struct LpWindow {
#[template_child]
pub(super) toast_overlay: TemplateChild<adw::ToastOverlay>,
#[template_child]
pub(super) window_image: TemplateChild<LpWindowImage>,
}
#[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));
}
},
);
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
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();
if imp
.window_image
.image_view()
.current_image()
.is_some_and(|img| img.image_size_available())
{
let shows_properties = imp.window_image.properties_button().is_active();
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);
}
}
pub 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);
}
pub async fn show_about(&self) {
let about = crate::about::dialog().await;
about.present(Some(self));
}
pub fn show_edit(&self) {
todo!()
}
pub fn image_view(&self) -> LpImageView {
self.imp().window_image.image_view()
}
}

32
src/window.ui Normal file
View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="LpWindow" parent="AdwApplicationWindow">
<property name="default-width">600</property>
<property name="default-height">498</property>
<property name="width-request">360</property>
<property name="height-request">294</property>
<!--
<child>
<object class="AdwBreakpoint">
<condition>max-width: 590sp</condition>
<setter object="multi_layout_view" property="layout-name">bottom-properties</setter>
</object>
</child>
<child>
<object class="AdwBreakpoint">
<condition>max-width: 590sp and max-height: 550sp</condition>
<setter object="multi_layout_view" property="layout-name">overlay-properties</setter>
</object>
</child>
-->
<property name="content">
<object class="AdwToastOverlay" id="toast_overlay">
<property name="child">
<object class="LpWindowImage" id="window_image">
<property name="window">LpWindow</property>
</object>
</property>
</object>
</property>
</template>
</interface>