430 lines
16 KiB
JavaScript
Executable File
430 lines
16 KiB
JavaScript
Executable File
/*
|
|
This file is part of CoverflowAltTab.
|
|
|
|
CoverflowAltTab 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.
|
|
|
|
CoverflowAltTab 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 CoverflowAltTab. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import Clutter from 'gi://Clutter';
|
|
import GObject from 'gi://GObject';
|
|
import Graphene from 'gi://Graphene';
|
|
import St from 'gi://St';
|
|
|
|
/**
|
|
* Direction and Placement properties values are set to be compatible with deprecated
|
|
* Clutter.Gravity.
|
|
*/
|
|
|
|
export class Direction {}
|
|
Direction.TO_RIGHT = 3;
|
|
Direction.TO_LEFT = 7;
|
|
|
|
export class Placement {}
|
|
Placement.TOP = 1;
|
|
Placement.TOP_RIGHT = 2;
|
|
Placement.RIGHT = 3;
|
|
Placement.BOTTOM_RIGHT = 4;
|
|
Placement.BOTTOM = 5;
|
|
Placement.BOTTOM_LEFT = 6;
|
|
Placement.LEFT = 7;
|
|
Placement.TOP_LEFT = 8;
|
|
Placement.CENTER = 9;
|
|
|
|
export const Preview = GObject.registerClass({
|
|
GTypeName: "Preview",
|
|
Properties: {
|
|
'remove_icon_opacity': GObject.ParamSpec.double(
|
|
`remove_icon_opacity`,
|
|
`Revmove Icon Opacity`,
|
|
`Icon opacity when `,
|
|
GObject.ParamFlags.READWRITE,
|
|
0.0, 1.0,
|
|
1.0,
|
|
)
|
|
}
|
|
}, class Preview extends Clutter.Clone {
|
|
_init(window, switcher, ...args) {
|
|
super._init(...args);
|
|
this.metaWin = window;
|
|
this.switcher = switcher;
|
|
this._icon = null;
|
|
this._highlight = null;
|
|
this._flash = null;
|
|
this._entered = false;
|
|
this._effectNames = ['blur', 'glitch', 'desaturate', 'tint']
|
|
this._effectCounts = {};
|
|
for (let effect_name of this._effectNames) {
|
|
this._effectCounts[effect_name] = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Make the preview above all other children layers in the given parent.
|
|
*
|
|
* @param {Object} parent The preview parent. If is not its real parent,then the
|
|
* behaviour is undefined.
|
|
* @return {void}
|
|
*/
|
|
make_top_layer(parent) {
|
|
if (this.raise_top) {
|
|
this.raise_top()
|
|
if (this._icon) this._icon.raise_top();
|
|
} else if (parent.set_child_above_sibling) {
|
|
parent.set_child_above_sibling(this, null);
|
|
if (this._icon) parent.set_child_above_sibling(this._icon, this);
|
|
} else {
|
|
// Don't throw anything here, it may cause unstabilities
|
|
logError("No method found for making preview the top layer");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Make the preview below all other children layers in the given parent.
|
|
*
|
|
* @param {Object} parent The preview parent. If is not its real parent,then the
|
|
* behaviour is undefined.
|
|
* @return {void}
|
|
*/
|
|
make_bottom_layer(parent) {
|
|
if (this.lower_bottom) {
|
|
if (this._icon) this._icon.lower_bottom();
|
|
this.lower_bottom()
|
|
} else if (parent.set_child_below_sibling) {
|
|
parent.set_child_below_sibling(this, null);
|
|
if (this._icon) parent.set_child_above_sibling(this._icon, this);
|
|
} else {
|
|
// Don't throw anything here, it may cause unstabilities
|
|
logError("No method found for making preview the bottom layer");
|
|
}
|
|
}
|
|
|
|
addEffect(effect_class, constructor_argument, name, parameter_name, from_param_value, param_value, duration) {
|
|
duration = 0.99 * 1000.0 * duration;
|
|
let effect_name = name + "-effect";
|
|
let add_transition_name = effect_name + "-add";
|
|
let remove_transition_name = effect_name + "-remove";
|
|
let property_transition_name = `@effects.${effect_name}.${parameter_name}`;
|
|
if (this.get_transition(remove_transition_name) !== null) {
|
|
this.remove_transition(remove_transition_name);
|
|
let transition = Clutter.PropertyTransition.new(property_transition_name);
|
|
transition.progress_mode = Clutter.AnimationMode.LINEAR;
|
|
transition.duration = duration;
|
|
transition.remove_on_complete = true;
|
|
transition.set_from(this.get_effect(effect_name)[parameter_name]);
|
|
transition.set_to(param_value);
|
|
this.get_effect(effect_name)[parameter_name] = 1.0;
|
|
this.add_transition(add_transition_name, transition);
|
|
transition.connect('new-frame', (timeline, msecs) => {
|
|
this.queue_redraw();
|
|
});
|
|
} else if (this._effectCounts[name] == 0) {
|
|
if (this.get_transition(add_transition_name) === null) {
|
|
let transition = Clutter.PropertyTransition.new(property_transition_name);
|
|
transition.progress_mode = Clutter.AnimationMode.LINEAR;
|
|
transition.duration = duration;
|
|
transition.remove_on_complete = true;
|
|
transition.set_to(param_value);
|
|
transition.set_from(from_param_value);
|
|
this._newFrameCount = 0;
|
|
this.add_effect_with_name(effect_name, new effect_class(constructor_argument));
|
|
this.add_transition(add_transition_name, transition);
|
|
this._effectCounts[name] = 1;
|
|
transition.connect('new-frame', (timeline, msecs) => {
|
|
this.queue_redraw();
|
|
});
|
|
}
|
|
} else {
|
|
this._effectCounts[name] += 1;
|
|
}
|
|
}
|
|
|
|
removeEffect(name, parameter_name, value, duration) {
|
|
duration = 0.99 * 1000.0 * duration;
|
|
let effect_name = name + "-effect";
|
|
let add_transition_name = effect_name + "-add";
|
|
let remove_transition_name = effect_name + "-remove";
|
|
let property_transition_name = `@effects.${effect_name}.${parameter_name}`;
|
|
if (this._effectCounts[name] > 0) {
|
|
if (this._effectCounts[name] == 1) {
|
|
this.remove_transition(add_transition_name);
|
|
if (this.get_transition(remove_transition_name) === null) {
|
|
let transition = Clutter.PropertyTransition.new(property_transition_name);
|
|
transition.progress_mode = Clutter.AnimationMode.LINEAR;
|
|
transition.duration = duration;
|
|
transition.remove_on_complete = true;
|
|
transition.set_from(this.get_effect(effect_name)[parameter_name]);
|
|
transition.set_to(value);
|
|
this.get_effect(effect_name)[parameter_name] = 1.0;
|
|
this.add_transition(remove_transition_name, transition);
|
|
transition.connect("completed", (trans) => {
|
|
this.remove_effect_by_name(effect_name);
|
|
this._effectCounts[name] = 0;
|
|
});
|
|
}
|
|
} else {
|
|
this._effectCounts[name] -= 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
_pulse_highlight() {
|
|
if (this._highlight == null) return;
|
|
this._highlight.ease({
|
|
opacity: 255,
|
|
duration: 2000,
|
|
mode: Clutter.AnimationMode.EASE_IN_OUT_QUINT,
|
|
onComplete: () => {
|
|
this._highlight.ease({
|
|
opacity: 80,
|
|
duration: 1400,
|
|
mode: Clutter.AnimationMode.EASE_IN_OUT_QUINT,
|
|
onComplete: () => {
|
|
this._pulse_highlight();
|
|
},
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
remove_highlight() {
|
|
if (this._highlight != null) {
|
|
this._highlight.ease({
|
|
opacity: 0,
|
|
duration: 300,
|
|
mode: Clutter.AnimationMode.EASE_IN_OUT_QUINT,
|
|
onComplete: () => {
|
|
if (this._highlight != null) {
|
|
this._highlight.destroy()
|
|
this._highlight = null;
|
|
}
|
|
},
|
|
});
|
|
}
|
|
if (this._flash != null) {
|
|
this._flash.destroy();
|
|
this._flash = null;
|
|
}
|
|
}
|
|
|
|
_getHighlightStyle(alpha) {
|
|
let bgcolor = this.switcher._getSwitcherBackgroundColor();
|
|
let style =`background-color: rgba(${bgcolor.red}, ${bgcolor.green}, ${bgcolor.blue}, ${alpha})`;
|
|
return style;
|
|
}
|
|
|
|
vfunc_enter_event(crossingEvent) {
|
|
if (this.switcher._animatingClosed || this._entered == true) {
|
|
return Clutter.EVENT_PROPAGATE;
|
|
}
|
|
this._entered = true;
|
|
if (this.switcher._settings.raise_mouse_over) {
|
|
this.make_top_layer(this.switcher.previewActor);
|
|
this.switcher._raiseIcons();
|
|
}
|
|
if (this.switcher._settings.highlight_mouse_over) {
|
|
let window_actor = this.metaWin.get_compositor_private();
|
|
if (this._highlight == null) {
|
|
this._highlight = new St.Bin({
|
|
opacity: 0,
|
|
width: this.width,
|
|
height: this.height,
|
|
x: 0,
|
|
y: 0,
|
|
reactive: false,
|
|
});
|
|
this._highlight.set_style(this._getHighlightStyle(0.3));
|
|
let constraint = Clutter.BindConstraint.new(window_actor, Clutter.BindCoordinate.SIZE, 0);
|
|
this._highlight.add_constraint(constraint);
|
|
window_actor.add_child(this._highlight);
|
|
|
|
}
|
|
if (this._flash == null) {
|
|
this._flash = new St.Bin({
|
|
width: 1,
|
|
height: 1,
|
|
opacity: 255,
|
|
reactive: false,
|
|
x: 0,
|
|
y: 0,
|
|
});
|
|
this._flash.set_style(this._getHighlightStyle(1));
|
|
let constraint = Clutter.BindConstraint.new(window_actor, Clutter.BindCoordinate.SIZE, 0);
|
|
this._flash.add_constraint(constraint);
|
|
window_actor.add_child(this._flash);
|
|
this._flash.ease({
|
|
opacity: 0,
|
|
duration: 500,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUINT,
|
|
onComplete: () => {
|
|
this._pulse_highlight();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
return Clutter.EVENT_PROPAGATE;
|
|
}
|
|
|
|
addIcon() {
|
|
let app = this.switcher._tracker.get_window_app(this.metaWin);
|
|
let icon_size = this.switcher._settings.overlay_icon_size;
|
|
this._icon = app ? app.create_icon_texture(Math.min(icon_size, this.width, this.height) / this.scale) : null;
|
|
|
|
if (this._icon == null) {
|
|
this._icon = new St.Icon({
|
|
icon_name: 'applications-other',
|
|
});
|
|
}
|
|
|
|
let constraint = Clutter.BindConstraint.new(this, Clutter.BindCoordinate.ALL, 0);
|
|
this._icon.add_constraint(constraint);
|
|
|
|
this.bind_property_full('opacity',
|
|
this._icon, 'opacity',
|
|
GObject.BindingFlags.SYNC_CREATE,
|
|
(bind, source) => {
|
|
/* So that the icon fades out 1) when the preview fades
|
|
out, such as in the timeline switcher, and
|
|
2) when the icon is being removed,
|
|
but also ensure the icon only goes as high as the setting
|
|
opacity, we take the minimum of those three as our opacity.
|
|
Seems there might be a better way, but I'm not sure.
|
|
*/
|
|
return [true, Math.min(source, 255 * this.remove_icon_opacity, 255 * this.switcher._settings.overlay_icon_opacity)];
|
|
},
|
|
null);
|
|
this.bind_property('rotation_angle_y', this._icon, 'rotation_angle_y',
|
|
GObject.BindingFlags.SYNC_CREATE);
|
|
this.bind_property('pivot_point', this._icon, 'pivot_point',
|
|
GObject.BindingFlags.SYNC_CREATE);
|
|
this.bind_property('translation_x', this._icon, 'translation_x',
|
|
GObject.BindingFlags.SYNC_CREATE);
|
|
this.bind_property('scale_x', this._icon, 'scale_x',
|
|
GObject.BindingFlags.SYNC_CREATE);
|
|
this.bind_property('scale_y', this._icon, 'scale_y',
|
|
GObject.BindingFlags.SYNC_CREATE);
|
|
this.bind_property('scale_z', this._icon, 'scale_z',
|
|
GObject.BindingFlags.SYNC_CREATE);
|
|
this.switcher.previewActor.add_child(this._icon);
|
|
|
|
if (this.switcher._settings.icon_has_shadow) {
|
|
this._icon.add_style_class_name("icon-dropshadow");
|
|
}
|
|
|
|
}
|
|
|
|
removeIcon(animation_time) {
|
|
if (this._icon != null) {
|
|
let transition = Clutter.PropertyTransition.new('remove_icon_opacity');
|
|
transition.duration = 1000.0 * animation_time;
|
|
this._icon.remove_icon_opacity_start = this._icon.opacity / 255.;
|
|
transition.set_from(this._icon.remove_icon_opacity_start);
|
|
transition.set_to(0);
|
|
transition.remove_on_complete = true;
|
|
transition.connect('new-frame', (timeline, msecs) => {
|
|
this._icon.opacity = 255 * this._icon.remove_icon_opacity_start * (1 -
|
|
timeline.get_progress());//(1 - msecs / transition.duration);
|
|
this._icon.queue_redraw();
|
|
})
|
|
transition.connect('completed', (timeline) => {
|
|
if (this._icon != null) {
|
|
this._icon.destroy()
|
|
this._icon = null;
|
|
}
|
|
});
|
|
this.add_transition('remove_icon_opacity_transition', transition);
|
|
}
|
|
}
|
|
|
|
vfunc_leave_event(crossingEvent) {
|
|
this.remove_highlight();
|
|
this._entered = false;
|
|
if (this.switcher._settings.raise_mouse_over && !this.switcher._animatingClosed) this.switcher._updatePreviews(true, 0);
|
|
return Clutter.EVENT_PROPAGATE;
|
|
}
|
|
|
|
/**
|
|
* Gets the pivot point relative to the preview.
|
|
*
|
|
* @param {Placement} placement
|
|
* @return {Graphene.Point}
|
|
*/
|
|
get_pivot_point_placement(placement) {
|
|
let xFraction = 0,
|
|
yFraction = 0;
|
|
|
|
// Set xFraction
|
|
switch (placement) {
|
|
case Placement.TOP_LEFT:
|
|
case Placement.LEFT:
|
|
case Placement.BOTTOM_LEFT:
|
|
xFraction = 0;
|
|
break;
|
|
|
|
case Placement.TOP:
|
|
case Placement.CENTER:
|
|
case Placement.BOTTOM:
|
|
xFraction = 0.5;
|
|
break;
|
|
|
|
case Placement.TOP_RIGHT:
|
|
case Placement.RIGHT:
|
|
case Placement.BOTTOM_RIGHT:
|
|
xFraction = 1;
|
|
break;
|
|
|
|
default:
|
|
throw new Error("Unknown placement given");
|
|
}
|
|
|
|
// Set yFraction
|
|
switch (placement) {
|
|
case Placement.TOP_LEFT:
|
|
case Placement.TOP:
|
|
case Placement.TOP_RIGHT:
|
|
yFraction = 0;
|
|
break;
|
|
|
|
case Placement.LEFT:
|
|
case Placement.CENTER:
|
|
case Placement.RIGHT:
|
|
yFraction = 0.5;
|
|
break;
|
|
|
|
case Placement.BOTTOM_LEFT:
|
|
case Placement.BOTTOM:
|
|
case Placement.BOTTOM_RIGHT:
|
|
yFraction = 1;
|
|
break;
|
|
|
|
default:
|
|
throw new Error("Unknown placement given");
|
|
}
|
|
return new Graphene.Point({ x: xFraction, y: yFraction });
|
|
}
|
|
|
|
/**
|
|
* Sets the pivot point placement, relative to the preview.
|
|
*
|
|
* @param {Placement} placement
|
|
* @return {void}
|
|
*/
|
|
set_pivot_point_placement(placement) {
|
|
let point = this.get_pivot_point_placement(placement);
|
|
this.set_pivot_point(point.x, point.y);
|
|
}
|
|
});
|
|
|
|
export function findUpperLeftFromCenter(sideSize, position) {
|
|
return position - sideSize / 2;
|
|
}
|