imageView: Hide controls after mouse inactivity
Closes #25 Part-of: <https://gitlab.gnome.org/Incubator/loupe/-/merge_requests/189>
This commit is contained in:
parent
7b6ee24651
commit
916514604c
4 changed files with 180 additions and 10 deletions
|
|
@ -45,6 +45,9 @@
|
||||||
</style>
|
</style>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkEventControllerMotion" id="controls_box_start_events" />
|
||||||
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<child type="overlay">
|
<child type="overlay">
|
||||||
|
|
@ -96,6 +99,9 @@
|
||||||
</style>
|
</style>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkEventControllerMotion" id="controls_box_end_events" />
|
||||||
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
|
|
|
||||||
|
|
@ -203,5 +203,12 @@
|
||||||
</style>
|
</style>
|
||||||
</object>
|
</object>
|
||||||
</property>
|
</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkEventControllerMotion" id="motion_controller">
|
||||||
|
<signal name="motion" handler="on_motion_cb" swapped="yes"/>
|
||||||
|
<signal name="enter" handler="on_motion_cb" swapped="yes"/>
|
||||||
|
<signal name="leave" handler="on_motion_cb" swapped="yes"/>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
</template>
|
</template>
|
||||||
</interface>
|
</interface>
|
||||||
|
|
|
||||||
|
|
@ -65,9 +65,14 @@ mod imp {
|
||||||
/// overlayed controls
|
/// overlayed controls
|
||||||
#[template_child]
|
#[template_child]
|
||||||
pub(super) controls_box_start: TemplateChild<gtk::Widget>,
|
pub(super) controls_box_start: TemplateChild<gtk::Widget>,
|
||||||
|
#[template_child]
|
||||||
|
pub(super) controls_box_start_events: TemplateChild<gtk::EventControllerMotion>,
|
||||||
/// overlayed controls
|
/// overlayed controls
|
||||||
#[template_child]
|
#[template_child]
|
||||||
pub(super) controls_box_end: TemplateChild<gtk::Widget>,
|
pub(super) controls_box_end: TemplateChild<gtk::Widget>,
|
||||||
|
#[template_child]
|
||||||
|
pub(super) controls_box_end_events: TemplateChild<gtk::EventControllerMotion>,
|
||||||
|
|
||||||
#[template_child]
|
#[template_child]
|
||||||
pub sliding_view: TemplateChild<LpSlidingView>,
|
pub sliding_view: TemplateChild<LpSlidingView>,
|
||||||
|
|
||||||
|
|
@ -413,6 +418,22 @@ impl LpImageView {
|
||||||
self.imp().sliding_view.clone()
|
self.imp().sliding_view.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn controls_box_start(&self) -> gtk::Widget {
|
||||||
|
self.imp().controls_box_start.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn controls_box_start_events(&self) -> gtk::EventControllerMotion {
|
||||||
|
self.imp().controls_box_start_events.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn controls_box_end(&self) -> gtk::Widget {
|
||||||
|
self.imp().controls_box_end.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn controls_box_end_events(&self) -> gtk::EventControllerMotion {
|
||||||
|
self.imp().controls_box_end_events.clone()
|
||||||
|
}
|
||||||
|
|
||||||
fn model(&self) -> LpFileModel {
|
fn model(&self) -> LpFileModel {
|
||||||
self.imp().model.borrow().clone()
|
self.imp().model.borrow().clone()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
156
src/window.rs
156
src/window.rs
|
|
@ -28,12 +28,22 @@ use adw::subclass::prelude::*;
|
||||||
use glib::clone;
|
use glib::clone;
|
||||||
use gtk::CompositeTemplate;
|
use gtk::CompositeTemplate;
|
||||||
|
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
|
use std::cell::RefCell;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::util::{self, Direction, Position};
|
use crate::util::{self, Direction, Position};
|
||||||
use crate::widgets::{LpImage, LpImageView, LpPropertiesView};
|
use crate::widgets::{LpImage, LpImageView, LpPropertiesView};
|
||||||
|
|
||||||
|
/// Animation duration for showing overlay buttons in milliseconds
|
||||||
|
const SHOW_CONTROLS_ANIMATION_DURATION: u32 = 200;
|
||||||
|
/// Animation duration for hiding overlay buttons in milliseconds
|
||||||
|
const HIDE_CONTROLS_ANIMATION_DURATION: u32 = 1000;
|
||||||
|
/// Time of inactivity after which controls will be hidden in seconds
|
||||||
|
const HIDE_CONTROLS_IDLE_TIMEOUT: u32 = 3;
|
||||||
|
|
||||||
mod imp {
|
mod imp {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|
@ -77,6 +87,12 @@ mod imp {
|
||||||
pub(super) properties_view: TemplateChild<LpPropertiesView>,
|
pub(super) properties_view: TemplateChild<LpPropertiesView>,
|
||||||
#[template_child]
|
#[template_child]
|
||||||
pub(super) drop_target: TemplateChild<gtk::DropTarget>,
|
pub(super) drop_target: TemplateChild<gtk::DropTarget>,
|
||||||
|
#[template_child]
|
||||||
|
pub(super) motion_controller: TemplateChild<gtk::EventControllerMotion>,
|
||||||
|
|
||||||
|
pub(super) show_controls_animation: OnceCell<adw::TimedAnimation>,
|
||||||
|
pub(super) hide_controls_animation: OnceCell<adw::TimedAnimation>,
|
||||||
|
pub(super) hide_controls_timeout: RefCell<Option<glib::SourceId>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
|
|
@ -304,6 +320,12 @@ mod imp {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Make widgets visible when the focus moves
|
||||||
|
obj.connect_move_focus(|obj, _| {
|
||||||
|
obj.show_controls();
|
||||||
|
obj.queue_hide_controls();
|
||||||
|
});
|
||||||
|
|
||||||
self.status_page
|
self.status_page
|
||||||
.set_icon_name(Some(&format!("{}-symbolic", config::APP_ID)));
|
.set_icon_name(Some(&format!("{}-symbolic", config::APP_ID)));
|
||||||
|
|
||||||
|
|
@ -367,16 +389,6 @@ impl LpWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_fullscreen(&self, fullscreen: bool) {
|
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);
|
self.set_fullscreened(fullscreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -590,6 +602,8 @@ impl LpWindow {
|
||||||
imp.stack.set_visible_child(&*imp.image_view);
|
imp.stack.set_visible_child(&*imp.image_view);
|
||||||
self.set_actions_enabled(true);
|
self.set_actions_enabled(true);
|
||||||
imp.image_view.grab_focus();
|
imp.image_view.grab_focus();
|
||||||
|
|
||||||
|
self.queue_hide_controls();
|
||||||
} else {
|
} else {
|
||||||
imp.stack.set_visible_child(&*imp.status_page);
|
imp.stack.set_visible_child(&*imp.status_page);
|
||||||
self.set_actions_enabled(false);
|
self.set_actions_enabled(false);
|
||||||
|
|
@ -650,6 +664,103 @@ impl LpWindow {
|
||||||
self.action_set_enabled("win.zoom-in", can_zoom_in);
|
self.action_set_enabled("win.zoom-in", can_zoom_in);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_control_opacity(&self, opacity: f64) {
|
||||||
|
self.image_view().controls_box_start().set_opacity(opacity);
|
||||||
|
self.image_view().controls_box_end().set_opacity(opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn controls_opacity(&self) -> f64 {
|
||||||
|
self.image_view().controls_box_start().opacity()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Animation to show controls
|
||||||
|
fn show_controls_animation(&self) -> &adw::TimedAnimation {
|
||||||
|
self.imp().show_controls_animation.get_or_init(|| {
|
||||||
|
let target = adw::CallbackAnimationTarget::new(glib::clone!(
|
||||||
|
@weak self as obj => move |opacity| obj.set_control_opacity(opacity)
|
||||||
|
));
|
||||||
|
|
||||||
|
adw::TimedAnimation::builder()
|
||||||
|
.duration(SHOW_CONTROLS_ANIMATION_DURATION)
|
||||||
|
.widget(&self.image_view())
|
||||||
|
.target(&target)
|
||||||
|
.value_to(1.)
|
||||||
|
.build()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Animation to hide controls
|
||||||
|
fn hide_controls_animation(&self) -> &adw::TimedAnimation {
|
||||||
|
self.imp().hide_controls_animation.get_or_init(|| {
|
||||||
|
let target = adw::CallbackAnimationTarget::new(glib::clone!(
|
||||||
|
@weak self as obj => move |opacity| obj.set_control_opacity(opacity)
|
||||||
|
));
|
||||||
|
|
||||||
|
adw::TimedAnimation::builder()
|
||||||
|
.duration(HIDE_CONTROLS_ANIMATION_DURATION)
|
||||||
|
.widget(&self.image_view())
|
||||||
|
.target(&target)
|
||||||
|
.value_to(0.)
|
||||||
|
.build()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Queue a fade animation to play after `HIDE_CONTROLS_IDLE_TIMEOUT` seconds of inactivity
|
||||||
|
fn queue_hide_controls(&self) {
|
||||||
|
self.dequeue_hide_controls();
|
||||||
|
|
||||||
|
let new_timeout = glib::timeout_add_seconds_local_once(
|
||||||
|
HIDE_CONTROLS_IDLE_TIMEOUT,
|
||||||
|
glib::clone!(@weak self as win => move || {
|
||||||
|
win.imp().hide_controls_timeout.take();
|
||||||
|
win.hide_controls();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
self.imp().hide_controls_timeout.replace(Some(new_timeout));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dequeue_hide_controls(&self) {
|
||||||
|
if let Some(current_timeout) = self.imp().hide_controls_timeout.take() {
|
||||||
|
current_timeout.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_controls_visible(&self) -> bool {
|
||||||
|
if self.hide_controls_animation().state() == adw::AnimationState::Playing {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.image_view().controls_box_start().opacity() == 1.
|
||||||
|
|| self.show_controls_animation().state() == adw::AnimationState::Playing
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start animation to show controls
|
||||||
|
fn show_controls(&self) {
|
||||||
|
if !self.is_controls_visible() {
|
||||||
|
self.hide_controls_animation().pause();
|
||||||
|
self.show_controls_animation()
|
||||||
|
.set_value_from(self.controls_opacity());
|
||||||
|
self.show_controls_animation().play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start animation to hide controls
|
||||||
|
fn hide_controls(&self) {
|
||||||
|
if self.is_controls_visible() {
|
||||||
|
self.show_controls_animation().pause();
|
||||||
|
self.hide_controls_animation()
|
||||||
|
.set_value_from(self.controls_opacity());
|
||||||
|
self.hide_controls_animation().play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether or not the window is showing images
|
||||||
|
fn is_showing_image(&self) -> bool {
|
||||||
|
let imp = self.imp();
|
||||||
|
imp.stack.visible_child().as_ref() == Some(imp.image_view.upcast_ref())
|
||||||
|
}
|
||||||
|
|
||||||
// In the LpWindow UI file we define a `gtk::Expression`s
|
// In the LpWindow UI file we define a `gtk::Expression`s
|
||||||
// that is a closure. This closure takes the current `gio::File`
|
// that is a closure. This closure takes the current `gio::File`
|
||||||
// and processes it to return a window title.
|
// and processes it to return a window title.
|
||||||
|
|
@ -683,4 +794,29 @@ impl LpWindow {
|
||||||
adw::FlapFoldPolicy::Always
|
adw::FlapFoldPolicy::Always
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[template_callback]
|
||||||
|
fn on_motion_cb(&self) {
|
||||||
|
let controller = &self.imp().motion_controller;
|
||||||
|
if controller.current_event_time() == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.show_controls();
|
||||||
|
// Only queue animation if not over controls and there is an image shown
|
||||||
|
if !self
|
||||||
|
.image_view()
|
||||||
|
.controls_box_start_events()
|
||||||
|
.contains_pointer()
|
||||||
|
&& !self
|
||||||
|
.image_view()
|
||||||
|
.controls_box_end_events()
|
||||||
|
.contains_pointer()
|
||||||
|
&& self.is_showing_image()
|
||||||
|
{
|
||||||
|
self.queue_hide_controls();
|
||||||
|
} else {
|
||||||
|
self.dequeue_hide_controls();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue