Unverified Commit 3baa744a authored by Erik Reider's avatar Erik Reider Committed by GitHub

Animate floating notifications + configurable max height (#561)

* Added initial AnimatedList and Item Widgets * Use the new list in the NotificationWindow class + massive list improvements This includes a more simplified way of calculating the item positions relative to the fade threshold * Added center animations * Fixed replacing notification not scrolling to the new position (top/bottom) * Simplified size_allocate y offset * Fixed very large notifications fading before reaching top with small list size * Disable card animation if GTK animations are disabled * Removed unneeded TODO about allocation order * Fixed input region including the fade padding
parent f55b433e
...@@ -11,12 +11,11 @@ template $SwayNotificationCenterNotificationWindow: Gtk.ApplicationWindow { ...@@ -11,12 +11,11 @@ template $SwayNotificationCenterNotificationWindow: Gtk.ApplicationWindow {
vscrollbar-policy: automatic; vscrollbar-policy: automatic;
has-frame: false; has-frame: false;
Gtk.Viewport viewport { $AnimatedList list {
vexpand: true; scroll_to_append: true;
transition_children: true;
$IterBox box { vexpand: true;
orientation: vertical;
}
} }
} }
} }
public enum AnimatedListDirection {
TOP_TO_BOTTOM, BOTTOM_TO_TOP;
}
private struct AnimationData {
public Gtk.Adjustment vadj;
public unowned AnimatedListItem item;
}
private struct WidgetHeights {
int min_height;
int nat_height;
}
private struct WidgetAlloc {
float y;
int height;
}
public class AnimatedList : Gtk.Widget, Gtk.Scrollable {
public const int SCROLL_ANIMATION_DURATION = 500;
public Gtk.Adjustment hadjustment { get; set construct; }
public Gtk.ScrollablePolicy hscroll_policy { get; set; }
public Gtk.Adjustment vadjustment { get; set construct; }
public Gtk.ScrollablePolicy vscroll_policy { get; set; }
public uint n_children { get; private set; }
public unowned List<AnimatedListItem> children {
get;
private construct set;
}
public unowned List<unowned AnimatedListItem> visible_children {
get;
private construct set;
}
/**
* Indicates if new / removed children should display an expand and shrink
* animation.
*/
public bool transition_children { get; construct set; }
/** Whether or not the list should display its items in a stack or not */
public bool use_card_animation { get; construct set; }
/** The direction that the items should flow in */
public AnimatedListDirection direction { get; construct set; }
/** Scroll to the latest item added to the list */
public bool scroll_to_append { get; construct set; }
/** The default item reveal animation type */
public AnimatedListItem.RevealAnimationType animation_reveal_type {
get;
construct set;
}
/** The default item animation type */
public AnimatedListItem.ChildAnimationType animation_child_type {
get;
construct set;
}
// Scroll bottom animation
Adw.CallbackAnimationTarget scroll_btm_target;
Adw.TimedAnimation scroll_btm_anim;
AnimationData ? scroll_btm_anim_data = null;
// Scroll top animation
Adw.CallbackAnimationTarget scroll_top_target;
Adw.TimedAnimation scroll_top_anim;
AnimationData ? scroll_top_anim_data = null;
// Adding an item to the top compensation
Adw.CallbackAnimationTarget scroll_comp_target;
Adw.TimedAnimation scroll_comp_anim;
AnimationData ? scroll_comp_anim_data = null;
// When true, the size_allocate method will scroll to the top/bottom
private bool set_initial_scroll_value = false;
private float fade_distance = 0.0f;
private unowned Gtk.Settings settings = Gtk.Settings.get_default ();
construct {
hadjustment = null;
children = new List<AnimatedListItem> ();
visible_children = new List<unowned AnimatedListItem> ();
notify["vadjustment"].connect (() => {
if (vadjustment != null) {
vadjustment.value_changed.connect (() => {
queue_allocate ();
});
}
});
map.connect (() => {
// Ensures that the initial scroll position gets set after
// GTK recalculates the layout
Idle.add_once (() => {
set_initial_scroll_value = true;
queue_allocate ();
});
});
set_overflow (Gtk.Overflow.HIDDEN);
scroll_btm_target = new Adw.CallbackAnimationTarget (scroll_bottom_value_cb);
scroll_btm_anim = new Adw.TimedAnimation (
this, 0.0, 1.0, SCROLL_ANIMATION_DURATION, scroll_btm_target);
scroll_btm_anim.set_easing (Adw.Easing.EASE_OUT_QUINT);
scroll_top_target = new Adw.CallbackAnimationTarget (scroll_top_value_cb);
scroll_top_anim = new Adw.TimedAnimation (
this, 0.0, 1.0, SCROLL_ANIMATION_DURATION, scroll_top_target);
scroll_top_anim.set_easing (Adw.Easing.EASE_OUT_QUINT);
scroll_comp_target = new Adw.CallbackAnimationTarget (scroll_comp_value_cb);
scroll_comp_anim = new Adw.TimedAnimation (
this, 0.0, 1.0, SCROLL_ANIMATION_DURATION, scroll_comp_target);
scroll_comp_anim.set_easing (Adw.Easing.EASE_OUT_QUINT);
}
public AnimatedList () {
Object (
css_name: "animatedlist",
accessible_role: Gtk.AccessibleRole.LIST,
transition_children: true,
use_card_animation: true,
direction: AnimatedListDirection.TOP_TO_BOTTOM,
scroll_to_append: false
);
}
public override void dispose () {
foreach (AnimatedListItem child in children) {
transition_children = false;
remove.begin (child, false);
}
}
public bool get_border (out Gtk.Border border) {
border = Gtk.Border ();
return false;
}
protected override Gtk.SizeRequestMode get_request_mode () {
foreach (unowned AnimatedListItem item in children) {
if (item.get_request_mode () != Gtk.SizeRequestMode.CONSTANT_SIZE) {
return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH;
}
}
return Gtk.SizeRequestMode.CONSTANT_SIZE;
}
protected override void compute_expand_internal (out bool hexpand_p,
out bool vexpand_p) {
hexpand_p = false;
vexpand_p = false;
foreach (unowned AnimatedListItem item in children) {
hexpand_p |= item.compute_expand (Gtk.Orientation.HORIZONTAL);
vexpand_p |= item.compute_expand (Gtk.Orientation.VERTICAL);
}
}
private float get_fade_distance (int height) {
switch (direction) {
case AnimatedListDirection.TOP_TO_BOTTOM:
return height - fade_distance;
case AnimatedListDirection.BOTTOM_TO_TOP:
return fade_distance;
}
return 0;
}
private void compute_height (int width,
int height,
out int total_height,
out WidgetAlloc[] child_heights) {
total_height = 0;
child_heights = new WidgetAlloc[n_children];
fade_distance = 0;
int num_vexpand_children = 0;
WidgetHeights measured_height = WidgetHeights ();
WidgetHeights[] heights = new WidgetHeights[n_children];
int total_min = 0;
int total_nat = 0;
int i = 0;
foreach (AnimatedListItem child in children) {
if (!child.should_layout ()) {
continue;
}
// Get the largest minimum height and use it for the fade distance
int nat_width;
// First, get the minimum width of our widget
child.measure (Gtk.Orientation.HORIZONTAL, -1,
null, out nat_width, null, null);
// Now use the natural width to retrieve the minimum and
// natural height to display.
int min_height, nat_height;
child.measure (Gtk.Orientation.VERTICAL, nat_width,
out min_height, out nat_height, null, null);
fade_distance = float.max (
fade_distance,
int.min (min_height, nat_height)
);
int min, nat;
child.measure (Gtk.Orientation.VERTICAL, width,
out min, out nat, null, null);
heights[i] = WidgetHeights () {
min_height = min,
nat_height = nat,
};
total_min += min;
total_nat += nat;
if (child.compute_expand (Gtk.Orientation.VERTICAL)) {
num_vexpand_children++;
}
i++;
}
bool allocate_nat = false;
int extra_height = 0;
if (height >= measured_height.nat_height) {
allocate_nat = true;
extra_height = height - measured_height.nat_height;
} else {
warn_if_reached ();
}
int y = 0;
i = 0;
foreach (AnimatedListItem child in children) {
WidgetHeights computed_height = heights[i];
WidgetAlloc child_allocation = WidgetAlloc () {
y = 0,
height = computed_height.min_height,
};
if (allocate_nat) {
child_allocation.height = computed_height.nat_height;
}
if (child.compute_expand (Gtk.Orientation.VERTICAL)) {
child_allocation.height += extra_height / num_vexpand_children;
}
child_allocation.y = y;
child_heights[i] = child_allocation;
total_height += child_allocation.height;
y += child_allocation.height;
i++;
}
const float LIMIT = 0.2f;
if (fade_distance == 0) {
fade_distance = height * LIMIT;
} else {
// Make sure that the fade distance isn't larger than the height
fade_distance = float.min (fade_distance, height * LIMIT);
}
// The padding to add to the bottom/top of the list to compensate
// for the fade, but only when the list is large enough to allow
// for scrolling.
if (should_card_animate () && vadjustment != null && total_height > height) {
total_height += (int) fade_distance;
}
}
protected override void size_allocate (int width,
int height,
int baseline) {
// Recalculate which children are visible, so clear the old list
while (!visible_children.is_empty ()) {
visible_children.delete_link (visible_children.nth (0));
}
warn_if_fail (visible_children.is_empty ());
// Save the already computed widget heights. We need the total height
// for calculating the reversed list animation, so two loops through the
// widgets is necessary...
int total_height;
WidgetAlloc[] heights;
compute_height (width, height, out total_height, out heights);
bool is_reversed = direction == AnimatedListDirection.BOTTOM_TO_TOP;
bool has_scroll = total_height > height;
float scroll_y = 0.0f;
if (vadjustment != null) {
scroll_y = (float) vadjustment.value;
// Set the initial scroll value to the top or bottom
// if the user hasn't scrolled yet.
if (has_scroll && set_initial_scroll_value) {
if (get_mapped ()) {
set_initial_scroll_value = false;
}
switch (direction) {
case AnimatedListDirection.TOP_TO_BOTTOM:
scroll_y = 0;
break;
case AnimatedListDirection.BOTTOM_TO_TOP:
scroll_y = total_height - height;
break;
}
}
}
total_height = int.max (height, total_height);
// Allocate the size and position of each item
uint index = 0;
foreach (AnimatedListItem child in children) {
if (!child.should_layout ()) {
index++;
continue;
}
WidgetAlloc child_allocation = heights[index];
int child_height = child_allocation.height;
float scale = 1.0f;
float x = 0;
float y = child_allocation.y - scroll_y;
if (is_reversed) {
y = total_height - child_height - child_allocation.y - scroll_y;
}
float opacity = 1.0f;
bool skip_child = true;
if (y < height && child_height + y > 0) {
skip_child = false;
// Deck of cards effect
if (should_card_animate () && has_scroll) {
float item_center = y + child_height * 0.5f;
// Compensate for very tall items being faded out before seeing the top
if (child_height > height) {
item_center = y + (is_reversed ? child_height : 0);
}
// The cut off where to start fade away
float local_fade_distance = get_fade_distance (height);
if ((!is_reversed && item_center > local_fade_distance)
|| (is_reversed && item_center < local_fade_distance)) {
// The distance from the fade edge
float dist = Math.fabsf (item_center - local_fade_distance);
// Hide when half way across the "circle".
// A little trigonometry never killed anybody :-)
float radius = fade_distance;
if (dist < radius) {
float angle = Math.atanf (dist / radius);
// The Y value within the circle (dot product)
float new_y = Math.sinf (angle) * radius;
// The ratio between the untransformed height and the dot product height
scale = 1.0f - (new_y / height * 2);
// Center the item in the X axis
x = (width - (width * scale)) * 0.5f;
// Calculate the new distance from the start of the fade
// NOTE: Reverse the direction of the animation
// depending on the list direction.
y += (float) (dist * Math.sin (angle) * (is_reversed ? 1 : -1));
opacity = 1.0f - (dist / radius * 2);
skip_child |= opacity <= 0.1;
} else {
skip_child = true;
}
}
}
}
// Only display visible items
if (!skip_child
&& y < height && child_height + y > 0) {
// Prepend the child so that the child can be rendered first
// (to maintain a reversed z-index)
visible_children.prepend (child);
}
Gsk.Transform transform = new Gsk.Transform ()
.translate (Graphene.Point ().init (x, y))
.scale (scale, scale);
child.allocate (width, child_height, baseline, transform);
child.set_opacity (opacity);
index++;
}
if (vadjustment != null) {
vadjustment.configure (scroll_y,
0, total_height,
height * 0.1,
height * 0.9,
height);
}
}
protected override void measure (Gtk.Orientation orientation,
int for_size,
out int minimum,
out int natural,
out int minimum_baseline,
out int natural_baseline) {
minimum = 0;
natural = 0;
minimum_baseline = -1;
natural_baseline = -1;
int min = 0, nat = 0;
int largest_min = 0, largest_nat = 0;
foreach (AnimatedListItem child in children) {
if (!child.should_layout ()) {
continue;
}
int child_min, child_nat;
child.measure (orientation, for_size,
out child_min, out child_nat, null, null);
min += child_min;
nat += child_nat;
largest_min = int.max (largest_min, child_min);
largest_nat = int.max (largest_min, child_nat);
}
switch (orientation) {
case Gtk.Orientation.HORIZONTAL:
minimum = largest_min;
natural = largest_nat;
break;
case Gtk.Orientation.VERTICAL:
minimum = min;
natural = nat;
break;
}
}
protected override void snapshot (Gtk.Snapshot snapshot) {
// Only render the visible items, backwards to retain a valid z-index
foreach (unowned AnimatedListItem child in visible_children) {
if (!child.should_layout ()) {
continue;
}
snapshot_child (child, snapshot);
}
}
private Gtk.Adjustment clone_adjustment (Gtk.Adjustment original) {
return new Gtk.Adjustment (
original.get_value (),
original.get_lower (),
original.get_upper (),
original.get_step_increment (),
original.get_page_increment (),
original.get_page_size ()
);
}
private void scroll_bottom_value_cb (double value) {
vadjustment.set_value (
scroll_btm_anim_data.vadj.upper - scroll_btm_anim_data.vadj.page_size
+ scroll_btm_anim_data.item.get_height ());
}
private void play_scroll_bottom_anim (AnimatedListItem item) {
scroll_btm_anim_data = AnimationData () {
item = item,
vadj = clone_adjustment (vadjustment),
};
scroll_top_anim.duration = item.animation_duration;
scroll_top_anim.easing = item.animation_easing;
scroll_btm_anim.value_from = 0.0;
scroll_btm_anim.value_to = 1.0;
scroll_btm_anim.play ();
}
private void scroll_top_value_cb (double value) {
vadjustment.set_value (
Adw.lerp (scroll_top_anim_data.vadj.value, 0, value));
}
private void play_scroll_top_anim (AnimatedListItem item) {
scroll_top_anim_data = AnimationData () {
item = item,
vadj = clone_adjustment (vadjustment),
};
scroll_top_anim.duration = item.animation_duration;
scroll_top_anim.easing = item.animation_easing;
scroll_top_anim.value_from = 0.0;
scroll_top_anim.value_to = 1.0;
scroll_top_anim.play ();
}
private void scroll_comp_value_cb (double value) {
vadjustment.set_value (
scroll_comp_anim_data.vadj.value
+ scroll_comp_anim_data.item.get_height ());
}
private void play_scroll_comp_anim (AnimatedListItem item) {
scroll_comp_anim_data = AnimationData () {
item = item,
vadj = clone_adjustment (vadjustment),
};
scroll_comp_anim.duration = item.animation_duration;
scroll_comp_anim.easing = Adw.Easing.LINEAR;
scroll_comp_anim.value_from = 0.0;
scroll_comp_anim.value_to = 1.0;
scroll_comp_anim.play ();
}
private AnimatedListItem get_list_item (Gtk.Widget widget) {
AnimatedListItem item;
if (widget is AnimatedListItem) {
item = widget as AnimatedListItem;
} else {
item = new AnimatedListItem ();
item.child = widget;
// Set the defaults
item.animation_reveal_type = animation_reveal_type;
item.animation_child_type = animation_child_type;
widget.unparent ();
widget.set_parent (item);
}
return item;
}
/**
* Inserts a widget last into the list depending on direction:
* TOP_TO_BOTTOM: Bottom
* BOTTOM_TO_TOP: Top
*/
public async AnimatedListItem ? prepend (Gtk.Widget widget) {
if (widget == null) {
warn_if_reached ();
return null;
}
AnimatedListItem item = get_list_item (widget);
item.unparent ();
item.insert_before (this, null); // append
children.append (item);
n_children++;
// Fixes the lack of auto-scrolling when scrolled at the bottom
// and a new item gets added
if (direction == AnimatedListDirection.TOP_TO_BOTTOM
&& vadjustment.value == vadjustment.upper - vadjustment.page_size) {
play_scroll_bottom_anim (item);
} else if (direction == AnimatedListDirection.BOTTOM_TO_TOP) {
// Compensate for the scrolling when adding an item to the top of the list
play_scroll_comp_anim (item);
}
yield item.added (transition_children);
return item;
}
/**
* Inserts a widget first into the list depending on direction:
* TOP_TO_BOTTOM: Top
* BOTTOM_TO_TOP: Bottom
*/
public async AnimatedListItem ? append (Gtk.Widget widget) {
if (widget == null) {
warn_if_reached ();
return null;
}
AnimatedListItem item = get_list_item (widget);
item.unparent ();
item.insert_after (this, null); // prepend
children.prepend (item);
n_children++;
// Fixes the lack of auto-scrolling when scrolled at the bottom
// and a new item gets added
if (direction == AnimatedListDirection.BOTTOM_TO_TOP
&& vadjustment.value == vadjustment.upper - vadjustment.page_size) {
play_scroll_bottom_anim (item);
} else if (!scroll_to_append && direction == AnimatedListDirection.TOP_TO_BOTTOM) {
// Compensate for the scrolling when adding an item to the top of the list
play_scroll_comp_anim (item);
} else if (scroll_to_append) {
// Scrolls to the item if enabled
switch (direction) {
case AnimatedListDirection.TOP_TO_BOTTOM:
play_scroll_top_anim (item);
break;
case AnimatedListDirection.BOTTOM_TO_TOP:
play_scroll_bottom_anim (item);
break;
}
}
yield item.added (transition_children);
return item;
}
private AnimatedListItem ? try_get_ancestor (Gtk.Widget widget) {
AnimatedListItem item;
if (widget is AnimatedListItem) {
item = widget as AnimatedListItem;
} else if (widget.parent is AnimatedListItem) {
item = widget.parent as AnimatedListItem;
} else {
unowned Gtk.Widget ? ancestor
= widget.get_ancestor (typeof (AnimatedListItem));
if (!(ancestor is AnimatedListItem)) {
warning ("Widget %p of type \"%s\" is not an ancestor of %s!",
widget, widget.get_type ().name (),
typeof (AnimatedListItem).name ());
return null;
}
item = ancestor as AnimatedListItem;
}
if (item.parent != this) {
warn_if_reached ();
return null;
}
return item;
}
public async bool remove (Gtk.Widget widget, bool transition) {
if (widget == null) {
warn_if_reached ();
return false;
}
AnimatedListItem ? item = try_get_ancestor (widget);
if (item == null) {
return false;
}
// Will unparent itself when done animating
bool result = yield item.removed (transition_children && transition);
children.remove (item);
n_children--;
item.destroy ();
queue_resize ();
return result;
}
public bool move_to_beginning (Gtk.Widget widget, bool scroll_to) {
if (widget == null) {
warn_if_reached ();
return false;
}
AnimatedListItem ? item = try_get_ancestor (widget);
if (item == null) {
warn_if_reached ();
return false;
}
// move to the beginning of the list
item.insert_after (this, null);
children.remove (item);
children.prepend (item);
queue_resize ();
if (scroll_to) {
scroll_to_top ();
}
return true;
}
private uint scroll_to_source_id = 0;
public void scroll_to_top () {
if (scroll_to_source_id > 0) {
Source.remove (scroll_to_source_id);
}
scroll_to_source_id = Idle.add_once (() => {
scroll_to_source_id = 0;
unowned AnimatedListItem ? item = get_first_item ();
return_if_fail (item != null);
switch (direction) {
case AnimatedListDirection.TOP_TO_BOTTOM:
play_scroll_top_anim (item);
break;
case AnimatedListDirection.BOTTOM_TO_TOP:
play_scroll_bottom_anim (item);
break;
}
});
}
public bool is_empty () {
return children.is_empty ();
}
public unowned AnimatedListItem ? get_first_item () {
if (children.is_empty ()) {
return null;
}
return children.first ().data;
}
public unowned AnimatedListItem ? get_last_item () {
if (children.is_empty ()) {
return null;
}
return children.last ().data;
}
private bool should_card_animate () {
bool value = this.use_card_animation;
if (settings != null) {
value &= settings.gtk_enable_animations;
}
return value;
}
}
public class AnimatedListItem : Gtk.Widget {
public const int DEFAULT_ANIMATION_DURATION = 350;
public enum RevealAnimationType {
NONE, SLIDE, SLIDE_WITH
}
public enum ChildAnimationType {
NONE, SLIDE_FROM_LEFT, SLIDE_FROM_RIGHT
}
protected unowned Gtk.Widget _child = null;
public unowned Gtk.Widget child {
get {
return _child;
}
set {
_child = value;
if (_child != null) {
_child.unparent ();
_child.set_parent (this);
}
}
}
public int animation_duration { get; construct set; }
public Adw.Easing animation_easing { get; construct set; }
public RevealAnimationType animation_reveal_type { get; construct set; }
public ChildAnimationType animation_child_type { get; construct set; }
public bool animation_child_fade { get; construct set; }
public bool destroying { get; private set; default = false; }
private Adw.CallbackAnimationTarget target;
private Adw.TimedAnimation animation;
private double animation_value = 0.0;
private ulong animation_done_cb_id = 0;
private unowned SourceFunc ? removed_cb = null;
private unowned SourceFunc ? added_cb = null;
public AnimatedListItem () {
Object (
css_name: "animatedlistitem",
accessible_role: Gtk.AccessibleRole.LIST_ITEM,
overflow: Gtk.Overflow.HIDDEN,
animation_duration: DEFAULT_ANIMATION_DURATION,
animation_easing: Adw.Easing.EASE_OUT_QUINT,
animation_reveal_type: RevealAnimationType.SLIDE,
animation_child_type: ChildAnimationType.SLIDE_FROM_RIGHT,
animation_child_fade: true
);
target = new Adw.CallbackAnimationTarget (animation_value_cb);
animation = new Adw.TimedAnimation (this, 0.0, 1.0,
animation_duration, target);
bind_property ("animation-easing",
animation, "easing",
BindingFlags.SYNC_CREATE, null, null);
}
public override Gtk.SizeRequestMode get_request_mode () {
return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH;
}
public override void size_allocate (int width,
int height,
int baseline) {
if (child == null || !child.should_layout ()) {
return;
}
if (animation_value >= 1) {
child.allocate (width, height, baseline, null);
} else if (animation_value < 0) {
return;
}
int child_width = width;
int child_height = height;
if (animation_value < 1.0) {
int min, nat;
child.measure (Gtk.Orientation.VERTICAL, width,
out min, out nat, null, null);
if (Math.ceil (nat * animation_value) == height) {
child_height = nat;
} else if (Math.ceil (min * animation_value) == height) {
child_height = min;
} else {
double d = Math.floor (height / animation_value);
child_height = int.min ((int) d, int.MAX);
}
}
Gsk.Transform transform = new Gsk.Transform ();
switch (animation_reveal_type) {
case RevealAnimationType.SLIDE_WITH:
transform = transform.translate_3d (
Graphene.Point3D ().init (0, height - child_height, 0)
);
break;
case RevealAnimationType.SLIDE:
case RevealAnimationType.NONE:
break;
}
switch (animation_child_type) {
case ChildAnimationType.SLIDE_FROM_RIGHT:
transform = transform.translate_3d (
Graphene.Point3D ()
.init (child_width * (float) (1 - animation_value), 0, 0)
);
break;
case ChildAnimationType.SLIDE_FROM_LEFT:
transform = transform.translate_3d (
Graphene.Point3D ()
.init (-child_width * (float) (1 - animation_value), 0, 0)
);
break;
case ChildAnimationType.NONE:
break;
}
child.allocate (child_width, child_height, -1, transform);
}
public override void measure (Gtk.Orientation orientation,
int for_size,
out int minimum,
out int natural,
out int minimum_baseline,
out int natural_baseline) {
minimum = 0;
natural = 0;
minimum_baseline = -1;
natural_baseline = -1;
if (child == null || !child.should_layout ()) {
return;
}
child.measure (orientation, for_size,
out minimum, out natural, null, null);
switch (orientation) {
case Gtk.Orientation.HORIZONTAL:
break;
case Gtk.Orientation.VERTICAL:;
minimum = (int) Math.ceil (minimum * animation_value);
natural = (int) Math.ceil (natural * animation_value);
break;
}
}
public override void snapshot (Gtk.Snapshot snapshot) {
if (!child.should_layout ()) {
return;
}
if (animation_child_fade) {
snapshot.push_opacity (animation_value);
}
snapshot_child (child, snapshot);
if (animation_child_fade) {
snapshot.pop ();
}
}
private void animation_value_cb (double value) {
this.animation_value = value;
queue_resize ();
}
private void animation_done_add () {
if (added_cb != null) {
added_cb ();
added_cb = null;
}
}
private void animation_done_remove () {
unparent ();
if (removed_cb != null) {
removed_cb ();
removed_cb = null;
}
}
private void animation_remove_done_cb () {
if (animation_done_cb_id != 0) {
animation.disconnect (animation_done_cb_id);
animation_done_cb_id = 0;
}
}
delegate void animation_done (Adw.Animation animation);
private void animation_add_done_cb (animation_done handler) {
animation_remove_done_cb ();
animation_done_cb_id = animation.done.connect ((a) => handler (a));
}
public async void added (bool transition) {
if (added_cb != null) {
// Already running animation
return;
}
animation_remove_done_cb ();
if (get_mapped () && transition) {
animation_add_done_cb (animation_done_add);
added_cb = added.callback;
animation.value_from
= animation.state == Adw.AnimationState.PLAYING
? animation_value : 0.0;
animation.value_from = animation.value;
animation.value_to = 1.0;
animation.play ();
yield;
} else {
animation_value = 1.0;
animation_done_add ();
}
}
public async bool removed (bool transition) {
if (removed_cb != null) {
// Already running animation
return false;
}
animation_remove_done_cb ();
set_can_focus (false);
set_can_target (false);
if (get_mapped () && transition) {
animation_add_done_cb (animation_done_remove);
removed_cb = removed.callback;
animation.value_from
= animation.state == Adw.AnimationState.PLAYING
? animation_value : 1.0;
animation.value_to = 0.0;
destroying = true;
animation.play ();
yield;
} else {
animation_value = 0.0;
animation_done_remove ();
}
return true;
}
}
...@@ -432,6 +432,8 @@ namespace SwayNotificationCenter { ...@@ -432,6 +432,8 @@ namespace SwayNotificationCenter {
* Notification window's width, in pixels. * Notification window's width, in pixels.
*/ */
public int notification_window_width { get; set; default = 500; } public int notification_window_width { get; set; default = 500; }
/** Max height of the notification in pixels */
public int notification_window_height { get; set; default = -1; }
/** Hides the control center after clearing all notifications */ /** Hides the control center after clearing all notifications */
public bool hide_on_clear { get; set; default = false; } public bool hide_on_clear { get; set; default = false; }
......
...@@ -133,6 +133,11 @@ ...@@ -133,6 +133,11 @@
"description": "Width of the notification in pixels", "description": "Width of the notification in pixels",
"default": 500 "default": 500
}, },
"notification-window-height": {
"type": "integer",
"description": "Max height of the notification in pixels. -1 to use the full amount of space given by the compositor.",
"default": -1
},
"fit-to-screen": { "fit-to-screen": {
"type": "boolean", "type": "boolean",
"description": "If the control center should expand to both edges of the screen", "description": "If the control center should expand to both edges of the screen",
......
...@@ -82,45 +82,27 @@ public class DismissibleWidget : Gtk.Widget, Adw.Swipeable { ...@@ -82,45 +82,27 @@ public class DismissibleWidget : Gtk.Widget, Adw.Swipeable {
minimum_baseline = -1; minimum_baseline = -1;
natural_baseline = -1; natural_baseline = -1;
int child_min, child_nat; if (child == null || !child.should_layout ()) {
if (!child.visible) {
return; return;
} }
child.measure (orientation, for_size, child.measure (orientation, for_size,
out child_min, out child_nat, null, null); out minimum, out natural, null, null);
minimum = int.max (minimum, child_min);
natural = int.max (natural, child_nat);
} }
protected override void size_allocate (int width, int height, int baseline) { protected override void size_allocate (int width, int height, int baseline) {
if (!child.visible) { if (child == null || !child.should_layout ()) {
return; return;
} }
int child_width, child_height; int child_width = width;
int min = 0, nat = 0; int child_height = height;
if (orientation == Gtk.Orientation.HORIZONTAL) {
child.measure (orientation,
height, out min, out nat, null, null);
} else {
child.measure (orientation,
width, out min, out nat, null, null);
}
child_width = width;
child_height = height;
double x = 0; double x = 0;
if (get_direction () == Gtk.TextDirection.RTL) { if (get_direction () == Gtk.TextDirection.RTL) {
x -= ((width * swipe_progress) - (width - child_width) / 2.0) x -= (width * swipe_progress);
+ (width * child_offset * 2);
} else { } else {
x -= - ((width * swipe_progress) - (width - child_width) / 2.0) x += (width * swipe_progress);
- (width * child_offset * 2);
} }
Gsk.Transform transform = new Gsk.Transform () Gsk.Transform transform = new Gsk.Transform ()
...@@ -130,15 +112,9 @@ public class DismissibleWidget : Gtk.Widget, Adw.Swipeable { ...@@ -130,15 +112,9 @@ public class DismissibleWidget : Gtk.Widget, Adw.Swipeable {
} }
protected override void snapshot (Gtk.Snapshot snapshot) { protected override void snapshot (Gtk.Snapshot snapshot) {
snapshot.save ();
snapshot.push_opacity (1 - swipe_progress.abs ()); snapshot.push_opacity (1 - swipe_progress.abs ());
snapshot_child (child, snapshot); snapshot_child (child, snapshot);
snapshot.pop (); snapshot.pop ();
snapshot.restore ();
} }
/* /*
...@@ -178,6 +154,9 @@ public class DismissibleWidget : Gtk.Widget, Adw.Swipeable { ...@@ -178,6 +154,9 @@ public class DismissibleWidget : Gtk.Widget, Adw.Swipeable {
animation.set_value_to (to); animation.set_value_to (to);
animation.set_initial_velocity (velocity); animation.set_initial_velocity (velocity);
// Disable user input if dismissed
set_can_target (to == 0);
animation.play (); animation.play ();
gesture_active = false; gesture_active = false;
......
...@@ -50,6 +50,8 @@ widget_sources = [ ...@@ -50,6 +50,8 @@ widget_sources = [
app_sources = [ app_sources = [
'main.vala', 'main.vala',
'animatedList/animatedList.vala',
'animatedList/animatedListItem.vala',
'orderedHashTable/orderedHashTable.vala', 'orderedHashTable/orderedHashTable.vala',
'iterHelpers/iterBox.vala', 'iterHelpers/iterBox.vala',
'iterHelpers/iterListBoxController.vala', 'iterHelpers/iterListBoxController.vala',
......
...@@ -32,12 +32,9 @@ namespace SwayNotificationCenter { ...@@ -32,12 +32,9 @@ namespace SwayNotificationCenter {
[GtkChild] [GtkChild]
unowned Gtk.ScrolledWindow scrolled_window; unowned Gtk.ScrolledWindow scrolled_window;
[GtkChild] [GtkChild]
unowned Gtk.Viewport viewport; unowned AnimatedList list;
[GtkChild]
unowned IterBox box;
private bool list_reverse = false; private Graphene.Rect scrolled_window_bounds = Graphene.Rect.zero ();
private uint scroll_to_source_id = 0;
Gee.HashSet<uint32> inline_reply_notifications = new Gee.HashSet<uint32> (); Gee.HashSet<uint32> inline_reply_notifications = new Gee.HashSet<uint32> ();
...@@ -59,13 +56,64 @@ namespace SwayNotificationCenter { ...@@ -59,13 +56,64 @@ namespace SwayNotificationCenter {
// -1 should set it to the content size unless it exceeds max_height // -1 should set it to the content size unless it exceeds max_height
scrolled_window.set_min_content_height (-1); scrolled_window.set_min_content_height (-1);
scrolled_window.set_max_content_height (MAX_HEIGHT); scrolled_window.set_max_content_height (
int.max (ConfigModel.instance.notification_window_height, -1));
scrolled_window.set_propagate_natural_height (true); scrolled_window.set_propagate_natural_height (true);
set_resizable (false); // TODO: Make option
list.use_card_animation = true;
// set_resizable (false);
default_width = ConfigModel.instance.notification_window_width; default_width = ConfigModel.instance.notification_window_width;
} }
protected override void size_allocate (int w, int h, int baseline) {
base.size_allocate (w, h, baseline);
// Set the input region to only be the size of the ScrolledWindow
Graphene.Rect bounds;
scrolled_window.compute_bounds (this, out bounds);
if (!bounds.equal (this.scrolled_window_bounds)) {
this.scrolled_window_bounds = bounds;
unowned Gdk.Surface ?surface = window.get_surface ();
if (surface == null) {
return;
}
Cairo.Region region = new Cairo.Region ();
foreach (AnimatedListItem item in list.visible_children) {
if (item.destroying) {
continue;
}
Graphene.Rect out_bounds;
item.compute_bounds (this, out out_bounds);
Cairo.RectangleInt item_rect = Cairo.RectangleInt () {
x = (int) out_bounds.get_x (),
y = (int) out_bounds.get_y (),
width = (int) out_bounds.get_width (),
height = (int) out_bounds.get_height (),
};
region.union_rectangle (item_rect);
}
// The input region should only cover each preview widget
Graphene.Rect scrollbar_bounds;
unowned Gtk.Widget scrollbar = scrolled_window.get_vscrollbar ();
if (scrollbar.should_layout ()) {
scrollbar.compute_bounds (this, out scrollbar_bounds);
Cairo.RectangleInt rect = Cairo.RectangleInt () {
x = (int) scrollbar_bounds.get_x (),
y = (int) scrollbar_bounds.get_y (),
width = (int) scrollbar_bounds.get_width (),
height = (int) scrollbar_bounds.get_height (),
};
region.union_rectangle (rect);
}
surface.set_input_region (region);
}
}
protected override void snapshot (Gtk.Snapshot snapshot) { protected override void snapshot (Gtk.Snapshot snapshot) {
// HACK: Fixes fully transparent windows not being mapped // HACK: Fixes fully transparent windows not being mapped
Gdk.RGBA color = Gdk.RGBA () { Gdk.RGBA color = Gdk.RGBA () {
...@@ -82,6 +130,8 @@ namespace SwayNotificationCenter { ...@@ -82,6 +130,8 @@ namespace SwayNotificationCenter {
if (swaync_daemon.use_layer_shell) { if (swaync_daemon.use_layer_shell) {
GtkLayerShell.set_layer (this, ConfigModel.instance.layer.to_layer ()); GtkLayerShell.set_layer (this, ConfigModel.instance.layer.to_layer ());
GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.BOTTOM, true);
GtkLayerShell.set_anchor (this, GtkLayerShell.Edge.TOP, true);
switch (ConfigModel.instance.positionX) { switch (ConfigModel.instance.positionX) {
case PositionX.LEFT: case PositionX.LEFT:
GtkLayerShell.set_anchor ( GtkLayerShell.set_anchor (
...@@ -105,26 +155,40 @@ namespace SwayNotificationCenter { ...@@ -105,26 +155,40 @@ namespace SwayNotificationCenter {
switch (ConfigModel.instance.positionY) { switch (ConfigModel.instance.positionY) {
default: default:
case PositionY.TOP: case PositionY.TOP:
GtkLayerShell.set_anchor ( scrolled_window.set_valign (Gtk.Align.START);
this, GtkLayerShell.Edge.BOTTOM, false);
GtkLayerShell.set_anchor (
this, GtkLayerShell.Edge.TOP, true);
break; break;
case PositionY.CENTER: case PositionY.CENTER:
GtkLayerShell.set_anchor ( scrolled_window.set_valign (Gtk.Align.CENTER);
this, GtkLayerShell.Edge.BOTTOM, false);
GtkLayerShell.set_anchor (
this, GtkLayerShell.Edge.TOP, false);
break; break;
case PositionY.BOTTOM: case PositionY.BOTTOM:
GtkLayerShell.set_anchor ( scrolled_window.set_valign (Gtk.Align.END);
this, GtkLayerShell.Edge.TOP, false);
GtkLayerShell.set_anchor (
this, GtkLayerShell.Edge.BOTTOM, true);
break; break;
} }
} }
list_reverse = ConfigModel.instance.positionY == PositionY.BOTTOM;
list.animation_reveal_type = AnimatedListItem.RevealAnimationType.SLIDE;
switch (ConfigModel.instance.positionX) {
case PositionX.LEFT:
list.animation_child_type = AnimatedListItem.ChildAnimationType.SLIDE_FROM_LEFT;
break;
case PositionX.CENTER:
list.animation_child_type = AnimatedListItem.ChildAnimationType.NONE;
break;
default:
case PositionX.RIGHT:
list.animation_child_type = AnimatedListItem.ChildAnimationType.SLIDE_FROM_RIGHT;
break;
}
switch (ConfigModel.instance.positionY) {
default:
case SwayNotificationCenter.PositionY.TOP:
case SwayNotificationCenter.PositionY.CENTER:
list.direction = AnimatedListDirection.TOP_TO_BOTTOM;
break;
case SwayNotificationCenter.PositionY.BOTTOM:
list.direction = AnimatedListDirection.BOTTOM_TO_TOP;
break;
}
} }
public void change_visibility (bool value) { public void change_visibility (bool value) {
...@@ -141,17 +205,22 @@ namespace SwayNotificationCenter { ...@@ -141,17 +205,22 @@ namespace SwayNotificationCenter {
public void close_all_notifications (remove_iter_func ? func = null) { public void close_all_notifications (remove_iter_func ? func = null) {
inline_reply_notifications.clear (); inline_reply_notifications.clear ();
if (!this.get_realized ()) return; if (!this.get_realized ()) return;
foreach (unowned Gtk.Widget child in box.get_children ()) { foreach (unowned AnimatedListItem item in list.children) {
Notification notification = (Notification) child; if (item.destroying) {
continue;
}
Notification notification = (Notification) item.child;
if (func == null || func (notification)) { if (func == null || func (notification)) {
remove_notification (notification, false); remove_notification (notification, false, false);
} }
} }
close (); close ();
} }
private void remove_notification (Notification ? noti, bool dismiss) { private void remove_notification (Notification ? noti,
bool dismiss,
bool transition) {
// Remove notification and its destruction timeout // Remove notification and its destruction timeout
if (noti != null) { if (noti != null) {
if (noti.has_inline_reply) { if (noti.has_inline_reply) {
...@@ -165,32 +234,18 @@ namespace SwayNotificationCenter { ...@@ -165,32 +234,18 @@ namespace SwayNotificationCenter {
} }
} }
noti.remove_noti_timeout (); noti.remove_noti_timeout ();
box.remove (noti); list.remove.begin (noti, transition, (obj, res) => {
} if (list.remove.end (res)
&& dismiss
if (dismiss && (!get_realized ()
&& (!get_realized () || !get_mapped ()
|| !get_mapped () || !(get_child () is Gtk.Widget)
|| !(get_child () is Gtk.Widget) || list.is_empty ())) {
|| box.length == 0)) { close ();
close (); return;
return; }
} });
}
/** Scroll to the latest notification */
private void scroll_to_latest_notification () {
if (scroll_to_source_id > 0) {
Source.remove (scroll_to_source_id);
} }
scroll_to_source_id = Idle.add_once (() => {
scroll_to_source_id = 0;
if (list_reverse) {
viewport.scroll_to (box.get_last_child (), null);
} else {
viewport.scroll_to (box.get_first_child (), null);
}
});
} }
public void add_notification (NotifyParams param) { public void add_notification (NotifyParams param) {
...@@ -212,41 +267,38 @@ namespace SwayNotificationCenter { ...@@ -212,41 +267,38 @@ namespace SwayNotificationCenter {
} }
} }
if (list_reverse) {
box.append (noti);
} else {
box.prepend (noti);
}
if (!this.get_mapped () || !this.get_realized ()) { if (!this.get_mapped () || !this.get_realized ()) {
this.set_anchor (); this.set_anchor ();
this.show (); this.show ();
} }
scroll_to_latest_notification (); list.append.begin (noti);
} }
public void close_notification (uint32 id, bool dismiss) { public void close_notification (uint32 id, bool dismiss) {
foreach (unowned Gtk.Widget child in box.get_children ()) { foreach (unowned AnimatedListItem item in list.children) {
var noti = (Notification) child; if (item.destroying) {
continue;
}
var noti = (Notification) item.child;
if (noti != null && noti.param.applied_id == id) { if (noti != null && noti.param.applied_id == id) {
remove_notification (noti, dismiss); remove_notification (noti, dismiss, true);
break; break;
} }
} }
} }
public void replace_notification (uint32 id, NotifyParams new_params) { public void replace_notification (uint32 id, NotifyParams new_params) {
foreach (unowned Gtk.Widget child in box.get_children ()) { foreach (unowned AnimatedListItem item in list.children) {
var noti = (Notification) child; if (item.destroying) {
continue;
}
var noti = (Notification) item.child;
if (noti != null && noti.param.applied_id == id) { if (noti != null && noti.param.applied_id == id) {
noti.replace_notification (new_params); noti.replace_notification (new_params);
// Position the notification in the beginning/end of the list // Position the notification in the beginning/end of the list
if (list_reverse) { // and scroll to the new item
box.reorder_child_after (noti, box.get_last_child ()); list.move_to_beginning (noti, true);
} else {
box.reorder_child_after (noti, null);
}
scroll_to_latest_notification ();
return; return;
} }
} }
...@@ -256,36 +308,22 @@ namespace SwayNotificationCenter { ...@@ -256,36 +308,22 @@ namespace SwayNotificationCenter {
} }
public uint32 ? get_latest_notification () { public uint32 ? get_latest_notification () {
if (box.length == 0) { unowned AnimatedListItem ? item = list.get_first_item ();
if (item == null || !(item.child is Notification)) {
return null; return null;
} }
unowned Gtk.Widget ? child = null; Notification noti = (Notification) item.child;
if (list_reverse) {
child = box.get_last_child ();
} else {
child = box.get_first_child ();
}
if (child == null || !(child is Notification)) return null;
Notification noti = (Notification) child;
return noti.param.applied_id; return noti.param.applied_id;
} }
public void latest_notification_action (uint32 action) { public void latest_notification_action (uint32 action) {
if (box.length == 0) { unowned AnimatedListItem ? item = list.get_first_item ();
if (item == null || !(item.child is Notification)) {
return; return;
} }
Gtk.Widget ? child = null; Notification noti = (Notification) item.child;
if (list_reverse) {
child = box.get_last_child ();
} else {
child = box.get_first_child ();
}
if (child == null || !(child is Notification)) return;
Notification noti = (Notification) child;
noti.click_alt_action (action); noti.click_alt_action (action);
noti.close_notification (); noti.close_notification ();
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment