This commit is contained in:
2024-07-08 22:46:35 +02:00
parent 02f44c49d2
commit 27254d817a
56249 changed files with 808097 additions and 1 deletions

View File

@@ -0,0 +1,294 @@
//////////////////////////////////////////////////////////////////////////////////////////
// ,-. ,--. ,-. , , ,---. ,-. ;-. ,-. . . ,-. ,--. //
// | \ | ( ` | / | / \ | ) / | | | ) | //
// | | |- `-. |< | | | |-' | | | |-< |- //
// | / | . ) | \ | \ / | \ | | | ) | //
// `-' `--' `-' ' ` ' `-' ' `-' `--` `-' `--' //
//////////////////////////////////////////////////////////////////////////////////////////
// SPDX-FileCopyrightText: Simon Schneegans <code@simonschneegans.de>
// SPDX-License-Identifier: GPL-3.0-or-later
'use strict';
import Meta from 'gi://Meta';
import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import Shell from 'gi://Shell';
import * as Util from 'resource:///org/gnome/shell/misc/util.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import {ControlsState} from 'resource:///org/gnome/shell/ui/overviewControls.js';
import {Workspace} from 'resource:///org/gnome/shell/ui/workspace.js';
//////////////////////////////////////////////////////////////////////////////////////////
// In GNOME Shell, SwipeTrackers are used all over the place to capture swipe gestures. //
// There's one for entering the overview, one for switching workspaces in desktop mode, //
// one for switching workspaces in overview mode, one for horizontal scrolling in the //
// app drawer, and many more. The ones used for workspace-switching usually do not //
// respond to single-click dragging but only to multi-touch gestures. We want to be //
// able to rotate the cube with the left mouse button, so we add the gesture defined //
// below to these two SwipeTracker instances (this is done by the _addDragGesture() of //
// the extension class). The gesture is loosely based on the gesture defined here: //
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/swipeTracker.js#L213 //
// It behaves the same in the regard that it reports update events for horizontal //
// movements. However, it stores vertical movements as well and makes this accessible //
// via the "pitch" property. This is then used for vertical rotations of the cube. //
//////////////////////////////////////////////////////////////////////////////////////////
const State = {
INACTIVE: 0, // The state will change to PENDING as soon as there is a mouse click.
PENDING: 1, // There was a click, but not enough movement to trigger the gesture.
ACTIVE: 2 // The gesture has been triggered and is in progress.
};
// clang-format off
export var DragGesture =
GObject.registerClass({
Properties: {
'distance': GObject.ParamSpec.double(
'distance', 'distance', 'distance', GObject.ParamFlags.READWRITE, 0, Infinity, 0),
'pitch': GObject.ParamSpec.double(
'pitch', 'pitch', 'pitch', GObject.ParamFlags.READWRITE, 0, 1, 0),
'sensitivity': GObject.ParamSpec.double(
'sensitivity', 'sensitivity', 'sensitivity', GObject.ParamFlags.READWRITE, 1, 10, 1),
},
Signals: {
'begin': {param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE]},
'update': {param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE]},
'end': {param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE]},
},
},
class DragGesture extends GObject.Object {
// clang-format on
_init(actor, mode) {
super._init();
this._actor = actor;
this._state = State.INACTIVE;
this._mode = mode;
// We listen to the 'captured-event' to be able to intercept some other actions. The
// main problem is the long-press action of the desktop background actor. This
// swallows any click events preventing us from dragging the desktop background.
// By connecting to 'captured-event', we have to extra-careful to propagate any
// event we are not interested in.
this._actorConnection1 = actor.connect('captured-event', (a, e) => {
return this._handleEvent(e);
});
// Once the input is grabbed, events are delivered directly to the actor, so we have
// also to connect to the normal "event" signal.
this._actorConnection2 = actor.connect('event', (a, e) => {
if (this._lastGrab) {
return this._handleEvent(e);
}
return Clutter.EVENT_PROPAGATE;
});
}
// Disconnects from the actor.
destroy() {
this._actor.disconnect(this._actorConnection1);
this._actor.disconnect(this._actorConnection2);
}
// This is called on every captured event.
_handleEvent(event) {
// Abort if the gesture is not meant for the current action mode (e.g. either
// Shell.ActionMode.OVERVIEW or Shell.ActionMode.NORMAL).
if (this._mode != Main.actionMode) {
return Clutter.EVENT_PROPAGATE;
}
// In the overview, we only want to switch workspaces by dragging when in
// window-picker state.
if (Main.actionMode == Shell.ActionMode.OVERVIEW) {
if (Main.overview._overview.controls._stateAdjustment.value !=
ControlsState.WINDOW_PICKER) {
return Clutter.EVENT_PROPAGATE;
}
}
// Ignore touch events on X11. On X11, we get emulated pointer events.
if (!Meta.is_wayland_compositor() &&
(event.type() == Clutter.EventType.TOUCH_BEGIN ||
event.type() == Clutter.EventType.TOUCH_UPDATE ||
event.type() == Clutter.EventType.TOUCH_END)) {
return Clutter.EVENT_PROPAGATE;
}
// When a mouse button is pressed or a touch event starts, we store the
// corresponding position. The gesture is maybe triggered later, if the pointer was
// moved a little.
if (event.type() == Clutter.EventType.BUTTON_PRESS ||
event.type() == Clutter.EventType.TOUCH_BEGIN) {
const source = global.stage.get_event_actor(event);
if (source) {
// Here's a minor hack: In the overview, there are some draggable things like
// window previews which "compete" with this gesture. Sometimes, the cube is
// dragged, sometimes the window previews. So we make sure that we do only start
// the gesture for events which originate from the given actor or from a
// workspace's background.
if (Main.actionMode != Shell.ActionMode.OVERVIEW || source == this._actor ||
source.get_parent() instanceof Workspace) {
this._clickPos = event.get_coords();
this._state = State.PENDING;
}
}
return Clutter.EVENT_PROPAGATE;
}
// Abort the pending state if the pointer leaves the actor.
if (event.type() == Clutter.EventType.LEAVE && this._state == State.PENDING) {
this._cancel();
return Clutter.EVENT_PROPAGATE;
}
// As soon as the pointer is moved a bit, the drag action becomes active.
if (this._eventIsMotion(event)) {
// If the mouse button is not pressed, we are not interested in the event.
if (this._state != State.INACTIVE && event.type() == Clutter.EventType.MOTION &&
(event.get_state() & Clutter.ModifierType.BUTTON1_MASK) == 0) {
this._cancel();
return Clutter.EVENT_PROPAGATE;
}
const currentPos = event.get_coords();
// If we are in the pending state, the gesture may be triggered as soon as the
// pointer is moved enough.
if (this._state == State.PENDING) {
const threshold = Clutter.Settings.get_default().dnd_drag_threshold;
if (Math.abs(currentPos[0] - this._clickPos[0]) > threshold ||
Math.abs(currentPos[1] - this._clickPos[1]) > threshold) {
// When starting a drag in desktop mode, we grab the input so that we can move
// the pointer across windows without loosing the input events.
if (Main.actionMode == Shell.ActionMode.NORMAL) {
const sequence = event.type() == Clutter.EventType.TOUCH_UPDATE ?
event.get_event_sequence() :
null;
if (!this._grab(event.get_device(), sequence)) {
return Clutter.EVENT_PROPAGATE;
}
}
this._state = State.ACTIVE;
[this._lastX, this._startY] = currentPos;
this.pitch = 0;
this.emit('begin', event.get_time(), currentPos[0], currentPos[1]);
}
// Even if the gesture started, we propagate the event so that any other
// gestures may wait for long-presses are canceled properly.
return Clutter.EVENT_PROPAGATE;
}
// In the active state, we report updates on each movement.
if (this._state == State.ACTIVE) {
// Compute the horizontal movement relative to the last call.
let deltaX = currentPos[0] - this._lastX;
this._lastX = currentPos[0];
// Compute the accumulated pitch relative to the screen height.
this.pitch = (this._startY - currentPos[1]) / global.screen_height;
// Increase sensitivity.
deltaX *= this.sensitivity;
// Increase horizontal movement if the cube is rotated vertically.
deltaX *= Util.lerp(1.0, global.workspaceManager.get_n_workspaces(),
Math.abs(this.pitch));
this.emit('update', event.get_time(), -deltaX, this.distance);
return Clutter.EVENT_STOP;
}
return Clutter.EVENT_PROPAGATE;
}
// As soon as the mouse button is released or the touch event ends, we quit the
// gesture.
if (this._eventIsRelease(event)) {
// If the gesture was active, report an end event.
if (this._state == State.ACTIVE) {
this._cancel();
this.emit('end', event.get_time(), this.distance);
return Clutter.EVENT_STOP;
}
// If the gesture was in pending state, set it to inactive again.
this._cancel();
return Clutter.EVENT_PROPAGATE;
}
return Clutter.EVENT_PROPAGATE;
}
// This aborts any ongoing grab and resets the current state to inactive.
_cancel() {
if (this._lastGrab) {
this._ungrab();
}
this._state = State.INACTIVE;
}
// Makes sure that all events from the pointing device we received last input from is
// passed to the given actor. This is used to ensure that we do not "loose" the touch
// buttons will dragging them around.
_grab(device, sequence) {
this._lastGrab = global.stage.grab(this._actor);
return this._lastGrab != null;
}
// Releases a grab created with the method above.
_ungrab() {
this._lastGrab.dismiss();
this._lastGrab = null;
}
// This is borrowed from here:
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/dnd.js#L214
_eventIsRelease(event) {
if (event.type() == Clutter.EventType.BUTTON_RELEASE) {
const buttonMask = Clutter.ModifierType.BUTTON1_MASK |
Clutter.ModifierType.BUTTON2_MASK | Clutter.ModifierType.BUTTON3_MASK;
// We only obey the last button release from the device, other buttons may get
// pressed / released during the drag.
return (event.get_state() & buttonMask) == 0;
} else if (event.type() == Clutter.EventType.TOUCH_END) {
// For touch, we only obey the pointer emulating sequence.
return global.display.is_pointer_emulating_sequence(event.get_event_sequence());
}
return false;
}
// This is borrowed from here:
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/dnd.js#L259
_eventIsMotion(event) {
return event.type() == Clutter.EventType.MOTION ||
(event.type() == Clutter.EventType.TOUCH_UPDATE &&
global.display.is_pointer_emulating_sequence(event.get_event_sequence()));
}
});

View File

@@ -0,0 +1,101 @@
//////////////////////////////////////////////////////////////////////////////////////////
// ,-. ,--. ,-. , , ,---. ,-. ;-. ,-. . . ,-. ,--. //
// | \ | ( ` | / | / \ | ) / | | | ) | //
// | | |- `-. |< | | | |-' | | | |-< |- //
// | / | . ) | \ | \ / | \ | | | ) | //
// `-' `--' `-' ' ` ' `-' ' `-' `--` `-' `--' //
//////////////////////////////////////////////////////////////////////////////////////////
// SPDX-FileCopyrightText: Simon Schneegans <code@simonschneegans.de>
// SPDX-License-Identifier: GPL-3.0-or-later
'use strict';
import Gio from 'gi://Gio';
import Gtk from 'gi://Gtk';
import GObject from 'gi://GObject';
import * as utils from './utils.js';
const _ = await utils.importGettext();
//////////////////////////////////////////////////////////////////////////////////////////
// This is based on a similar class from the Fly-Pie extension (MIT License). //
// https://github.com/Schneegans/Fly-Pie/blob/main/src/prefs/ImageChooserButton.js //
// We only need file chooser buttons for images, so the content. //
//////////////////////////////////////////////////////////////////////////////////////////
export function registerImageChooserButton() {
if (GObject.type_from_name('DesktopCubeImageChooserButton') == null) {
GObject.registerClass(
{
GTypeName: 'DesktopCubeImageChooserButton',
Template: `resource:///ui/imageChooserButton.ui`,
InternalChildren: ['button', 'label'],
Properties: {
'file': GObject.ParamSpec.string('file', 'file', 'file',
GObject.ParamFlags.READWRITE, ''),
},
},
class DesktopCubeImageChooserButton extends Gtk.Box { // --------------------------
_init(params = {}) {
super._init(params);
this._dialog = new Gtk.Dialog({use_header_bar: true, modal: true, title: ''});
this._dialog.add_button(_('Select File'), Gtk.ResponseType.OK);
this._dialog.add_button(_('Cancel'), Gtk.ResponseType.CANCEL);
this._dialog.set_default_response(Gtk.ResponseType.OK);
const fileFilter = new Gtk.FileFilter();
fileFilter.add_mime_type('image/*');
this._fileChooser = new Gtk.FileChooserWidget({
action: Gtk.FileChooserAction.OPEN,
hexpand: true,
vexpand: true,
height_request: 500,
filter: fileFilter
});
this._dialog.get_content_area().append(this._fileChooser);
this._dialog.connect('response', (dialog, id) => {
if (id == Gtk.ResponseType.OK) {
this.file = this._fileChooser.get_file().get_path();
}
dialog.hide();
});
this._button.connect('clicked', (button) => {
this._dialog.set_transient_for(button.get_root());
this._dialog.show();
if (this._file != null) {
this._fileChooser.set_file(this._file);
}
});
}
// Returns the currently selected file.
get file() {
return this._file.get_path();
}
// This makes the file chooser dialog preselect the given file.
set file(value) {
this._file = Gio.File.new_for_path(value);
if (this._file.query_exists(null)) {
this._label.label = this._file.get_basename();
} else {
this._label.label = _('(None)');
this._file = null;
}
this.notify('file');
}
});
}
}

View File

@@ -0,0 +1,182 @@
//////////////////////////////////////////////////////////////////////////////////////////
// ,-. ,--. ,-. , , ,---. ,-. ;-. ,-. . . ,-. ,--. //
// | \ | ( ` | / | / \ | ) / | | | ) | //
// | | |- `-. |< | | | |-' | | | |-< |- //
// | / | . ) | \ | \ / | \ | | | ) | //
// `-' `--' `-' ' ` ' `-' ' `-' `--` `-' `--' //
//////////////////////////////////////////////////////////////////////////////////////////
// SPDX-FileCopyrightText: Simon Schneegans <code@simonschneegans.de>
// SPDX-License-Identifier: GPL-3.0-or-later
'use strict';
import Gio from 'gi://Gio';
import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import Shell from 'gi://Shell';
import GdkPixbuf from 'gi://GdkPixbuf';
import Cogl from 'gi://Cogl';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as utils from './utils.js';
//////////////////////////////////////////////////////////////////////////////////////////
// This file contains two classes, the Skybox (which is an actor) and the SkyboxEffect, //
// which is a Shell.GLSLEffect which is applied to the Skybox actor. //
// //
// SkyboxEffect loads a given texture and interprets it as a 360° panorama in //
// equirectangular projection. It uses the current perspective of the stage to draw a //
// perspectively correct portion of this panorama. It has an additional pitch and yaw //
// parameter controlling the horizontal and vertical rotation of the camera. //
//////////////////////////////////////////////////////////////////////////////////////////
// clang-format off
var SkyboxEffect = GObject.registerClass({
Properties: {
'yaw': GObject.ParamSpec.double('yaw', 'yaw', 'yaw', GObject.ParamFlags.READWRITE,
-2 * Math.PI, 2 * Math.PI, 0),
'pitch': GObject.ParamSpec.double('pitch', 'pitch', 'pitch', GObject.ParamFlags.READWRITE,
-0.5 * Math.PI, 0.5 * Math.PI, 0),
},
}, class SkyboxEffect extends Shell.GLSLEffect {
// clang-format on
_init(file) {
super._init();
this._texture = null;
// Attempt to load the texture.
this._loadTexture(file)
.then(texture => {
this._texture = texture;
this.queue_repaint();
})
.catch(error => {
utils.debug(error);
});
// Redraw if either the pitch or the yaw changes.
this.connect('notify::yaw', () => {this.queue_repaint()});
this.connect('notify::pitch', () => {this.queue_repaint()});
};
// This is called once to setup the Cogl.Pipeline.
vfunc_build_pipeline() {
// In the vertex shader, we compute the view space position of the actor's corners.
this.add_glsl_snippet(Shell.SnippetHook.VERTEX, 'varying vec4 vsPos;',
'vsPos = cogl_modelview_matrix * cogl_position_in;', false);
const fragmentDeclares = `
varying vec4 vsPos;
uniform sampler2D uTexture;
uniform float uPitch;
uniform float uYaw;
mat3 getPitch() {
float s = sin(uPitch);
float c = cos(uPitch);
return mat3(1.0, 0.0, 0.0, 0.0, c, s, 0.0, -s, c);
}
mat3 getYaw() {
float s = sin(uYaw);
float c = cos(uYaw);
return mat3(c, 0.0, -s, 0.0, 1.0, 0.0, s, 0.0, c);
}
`;
// The fragment shader uses the interpolated viewspace position to compute a view
// ray. This ray is rotated according to the pitch and yaw values.
const fragmentCode = `
// Rotate the view ray.
vec3 view = getYaw() * getPitch() * normalize(vsPos.xyz);
// Compute equirectangular projection.
const float pi = 3.14159265359;
float x = 0.5 + 0.5 * atan(view.x, -view.z) / pi;
float y = acos(view.y) / pi;
cogl_color_out = texture2D(uTexture, vec2(x, y));
`;
this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, fragmentDeclares, fragmentCode,
false);
}
// For each draw call, we have to set some uniform values.
vfunc_paint_target(node, paintContext) {
if (this._texture) {
this.get_pipeline().set_layer_texture(0, this._texture.get_texture());
this.set_uniform_float(this.get_uniform_location('uTexture'), 1, [0]);
this.set_uniform_float(this.get_uniform_location('uPitch'), 1, [this.pitch]);
this.set_uniform_float(this.get_uniform_location('uYaw'), 1, [this.yaw]);
super.vfunc_paint_target(node, paintContext);
}
}
// Load a texture asynchronously.
async _loadTexture(file) {
return new Promise((resolve, reject) => {
const stream = Gio.File.new_for_path(file).read(null);
GdkPixbuf.Pixbuf.new_from_stream_async(stream, null, (obj, result) => {
const FORMATS = [
Cogl.PixelFormat.G_8,
Cogl.PixelFormat.RG_88,
Cogl.PixelFormat.RGB_888,
Cogl.PixelFormat.RGBA_8888,
];
try {
const pixbuf = GdkPixbuf.Pixbuf.new_from_stream_finish(result);
const texture = new Clutter.Image();
texture.set_data(pixbuf.get_pixels(), FORMATS[pixbuf.get_n_channels() - 1],
pixbuf.get_width(), pixbuf.get_height(),
pixbuf.get_rowstride());
resolve(texture);
} catch (error) {
reject(error);
}
});
});
}
});
//////////////////////////////////////////////////////////////////////////////////////////
// The Skybox is a simple actor which applies the above effect to itself. It also has //
// the pitch and yaw properties - these are directly forwarded to the effect. //
//////////////////////////////////////////////////////////////////////////////////////////
// clang-format off
export var Skybox = GObject.registerClass({
Properties: {
'yaw': GObject.ParamSpec.double('yaw', 'yaw', 'yaw', GObject.ParamFlags.READWRITE,
-2 * Math.PI, 2 * Math.PI, 0),
'pitch': GObject.ParamSpec.double('pitch', 'pitch', 'pitch', GObject.ParamFlags.READWRITE,
-0.5 * Math.PI, 0.5 * Math.PI, 0),
}
}, class Skybox extends Clutter.Actor {
// clang-format on
_init(file) {
super._init();
// Apply the effect.
this._effect = new SkyboxEffect(file);
this.add_effect(this._effect);
// Make sure that the overview background is transparent.
Main.uiGroup.add_style_class_name('desktop-cube-panorama-enabled');
// Forward the yaw and pitch values.
this.bind_property('yaw', this._effect, 'yaw', GObject.BindingFlags.NONE);
this.bind_property('pitch', this._effect, 'pitch', GObject.BindingFlags.NONE);
// Revert to the original overview background appearance.
this.connect('destroy', () => {
Main.uiGroup.remove_style_class_name('desktop-cube-panorama-enabled');
});
}
});

View File

@@ -0,0 +1,42 @@
//////////////////////////////////////////////////////////////////////////////////////////
// ,-. ,--. ,-. , , ,---. ,-. ;-. ,-. . . ,-. ,--. //
// | \ | ( ` | / | / \ | ) / | | | ) | //
// | | |- `-. |< | | | |-' | | | |-< |- //
// | / | . ) | \ | \ / | \ | | | ) | //
// `-' `--' `-' ' ` ' `-' ' `-' `--` `-' `--' //
//////////////////////////////////////////////////////////////////////////////////////////
// SPDX-FileCopyrightText: Simon Schneegans <code@simonschneegans.de>
// SPDX-License-Identifier: GPL-3.0-or-later
'use strict';
// This method can be used to write a message to GNOME Shell's log. This is enhances
// the standard log() functionality by prepending the extension's name and the location
// where the message was logged. As the extensions name is part of the location, you
// can more effectively watch the log output of GNOME Shell:
// journalctl -f -o cat | grep -E 'desktop-cube|'
// This method is based on a similar script from the Fly-Pie GNOME Shell extension which
// os published under the MIT License (https://github.com/Schneegans/Fly-Pie).
export function debug(message) {
const stack = new Error().stack.split('\n');
// Remove debug() function call from stack.
stack.shift();
// Find the index of the extension directory (e.g. desktop-cube@schneegans.github.com)
// in the stack entry. We do not want to print the entire absolute file path.
const extensionRoot = stack[0].indexOf('desktop-cube@schneegans.github.com');
console.log('[' + stack[0].slice(extensionRoot) + '] ' + message);
}
// This method can be used to import gettext. This is done differently in the
// GNOME Shell process and in the preferences process.
export async function importGettext() {
if (typeof global === 'undefined') {
return (await import('resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'))
.gettext;
}
return (await import('resource:///org/gnome/shell/extensions/extension.js')).gettext;
}