summaryrefslogtreecommitdiffstats
path: root/js/ui/environment.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/ui/environment.js')
-rw-r--r--js/ui/environment.js470
1 files changed, 470 insertions, 0 deletions
diff --git a/js/ui/environment.js b/js/ui/environment.js
new file mode 100644
index 0000000..8c790da
--- /dev/null
+++ b/js/ui/environment.js
@@ -0,0 +1,470 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported init */
+
+const Config = imports.misc.config;
+
+imports.gi.versions.AccountsService = '1.0';
+imports.gi.versions.Atk = '1.0';
+imports.gi.versions.Atspi = '2.0';
+imports.gi.versions.Clutter = Config.LIBMUTTER_API_VERSION;
+imports.gi.versions.Cogl = Config.LIBMUTTER_API_VERSION;
+imports.gi.versions.Gcr = '4';
+imports.gi.versions.Gdk = '3.0';
+imports.gi.versions.Gdm = '1.0';
+imports.gi.versions.Geoclue = '2.0';
+imports.gi.versions.Gio = '2.0';
+imports.gi.versions.GDesktopEnums = '3.0';
+imports.gi.versions.GdkPixbuf = '2.0';
+imports.gi.versions.GnomeBluetooth = '3.0';
+imports.gi.versions.GnomeDesktop = '3.0';
+imports.gi.versions.Graphene = '1.0';
+imports.gi.versions.Gtk = '3.0';
+imports.gi.versions.GWeather = '4.0';
+imports.gi.versions.IBus = '1.0';
+imports.gi.versions.Malcontent = '0';
+imports.gi.versions.NM = '1.0';
+imports.gi.versions.NMA = '1.0';
+imports.gi.versions.Pango = '1.0';
+imports.gi.versions.Polkit = '1.0';
+imports.gi.versions.PolkitAgent = '1.0';
+imports.gi.versions.Rsvg = '2.0';
+imports.gi.versions.Soup = '3.0';
+imports.gi.versions.TelepathyGLib = '0.12';
+imports.gi.versions.TelepathyLogger = '0.2';
+imports.gi.versions.UPowerGlib = '1.0';
+
+try {
+ if (Config.HAVE_SOUP2)
+ throw new Error('Soup3 support not enabled');
+ const Soup_ = imports.gi.Soup;
+} catch (e) {
+ imports.gi.versions.Soup = '2.4';
+ const { Soup } = imports.gi;
+ _injectSoup3Compat(Soup);
+}
+
+const { Clutter, Gio, GLib, GObject, Meta, Polkit, Shell, St } = imports.gi;
+const Gettext = imports.gettext;
+const System = imports.system;
+const SignalTracker = imports.misc.signalTracker;
+
+Gio._promisify(Gio.DataInputStream.prototype, 'fill_async');
+Gio._promisify(Gio.DataInputStream.prototype, 'read_line_async');
+Gio._promisify(Gio.DBus, 'get');
+Gio._promisify(Gio.DBusConnection.prototype, 'call');
+Gio._promisify(Gio.DBusProxy, 'new');
+Gio._promisify(Gio.DBusProxy.prototype, 'init_async');
+Gio._promisify(Gio.DBusProxy.prototype, 'call_with_unix_fd_list');
+Gio._promisify(Polkit.Permission, 'new');
+
+let _localTimeZone = null;
+
+// We can't import shell JS modules yet, because they may have
+// variable initializations, etc, that depend on init() already having
+// been run.
+
+
+// "monkey patch" in some varargs ClutterContainer methods; we need
+// to do this per-container class since there is no representation
+// of interfaces in Javascript
+function _patchContainerClass(containerClass) {
+ // This one is a straightforward mapping of the C method
+ containerClass.prototype.child_set = function (actor, props) {
+ let meta = this.get_child_meta(actor);
+ for (let prop in props)
+ meta[prop] = props[prop];
+ };
+
+ // clutter_container_add() actually is a an add-many-actors
+ // method. We conveniently, but somewhat dubiously, take the
+ // this opportunity to make it do something more useful.
+ containerClass.prototype.add = function (actor, props) {
+ this.add_actor(actor);
+ if (props)
+ this.child_set(actor, props);
+ };
+}
+
+function _patchLayoutClass(layoutClass, styleProps) {
+ if (styleProps) {
+ layoutClass.prototype.hookup_style = function (container) {
+ container.connect('style-changed', () => {
+ let node = container.get_theme_node();
+ for (let prop in styleProps) {
+ let [found, length] = node.lookup_length(styleProps[prop], false);
+ if (found)
+ this[prop] = length;
+ }
+ });
+ };
+ }
+}
+
+/**
+ * Mimick the Soup 3 APIs we use when falling back to Soup 2.4
+ *
+ * @param {object} Soup 2.4 namespace
+ * @returns {void}
+ */
+function _injectSoup3Compat(Soup) {
+ Soup.StatusCode = Soup.KnownStatusCode;
+
+ Soup.Message.new_from_encoded_form =
+ function (method, uri, form) {
+ const soupUri = new Soup.URI(uri);
+ soupUri.set_query(form);
+ return Soup.Message.new_from_uri(method, soupUri);
+ };
+ Soup.Message.prototype.set_request_body_from_bytes =
+ function (contentType, bytes) {
+ this.set_request(
+ contentType,
+ Soup.MemoryUse.COPY,
+ new TextDecoder().decode(bytes.get_data()));
+ };
+
+ Soup.Session.prototype.send_and_read_async =
+ function (message, prio, cancellable, callback) {
+ this.queue_message(message, () => callback(this, message));
+ };
+ Soup.Session.prototype.send_and_read_finish =
+ function (message) {
+ if (message.status_code !== Soup.KnownStatusCode.OK)
+ return null;
+
+ return message.response_body.flatten().get_as_bytes();
+ };
+}
+
+function _makeEaseCallback(params, cleanup) {
+ let onComplete = params.onComplete;
+ delete params.onComplete;
+
+ let onStopped = params.onStopped;
+ delete params.onStopped;
+
+ return isFinished => {
+ cleanup();
+
+ if (onStopped)
+ onStopped(isFinished);
+ if (onComplete && isFinished)
+ onComplete();
+ };
+}
+
+function _getPropertyTarget(actor, propName) {
+ if (!propName.startsWith('@'))
+ return [actor, propName];
+
+ let [type, name, prop] = propName.split('.');
+ switch (type) {
+ case '@layout':
+ return [actor.layout_manager, name];
+ case '@actions':
+ return [actor.get_action(name), prop];
+ case '@constraints':
+ return [actor.get_constraint(name), prop];
+ case '@content':
+ return [actor.content, name];
+ case '@effects':
+ return [actor.get_effect(name), prop];
+ }
+
+ throw new Error(`Invalid property name ${propName}`);
+}
+
+function _easeActor(actor, params) {
+ actor.save_easing_state();
+
+ if (params.duration != undefined)
+ actor.set_easing_duration(params.duration);
+ delete params.duration;
+
+ if (params.delay != undefined)
+ actor.set_easing_delay(params.delay);
+ delete params.delay;
+
+ let repeatCount = 0;
+ if (params.repeatCount != undefined)
+ repeatCount = params.repeatCount;
+ delete params.repeatCount;
+
+ let autoReverse = false;
+ if (params.autoReverse != undefined)
+ autoReverse = params.autoReverse;
+ delete params.autoReverse;
+
+ // repeatCount doesn't include the initial iteration
+ const numIterations = repeatCount + 1;
+ // whether the transition should finish where it started
+ const isReversed = autoReverse && numIterations % 2 === 0;
+
+ if (params.mode != undefined)
+ actor.set_easing_mode(params.mode);
+ delete params.mode;
+
+ const prepare = () => {
+ Meta.disable_unredirect_for_display(global.display);
+ global.begin_work();
+ };
+ const cleanup = () => {
+ Meta.enable_unredirect_for_display(global.display);
+ global.end_work();
+ };
+ let callback = _makeEaseCallback(params, cleanup);
+
+ // cancel overwritten transitions
+ let animatedProps = Object.keys(params).map(p => p.replace('_', '-', 'g'));
+ animatedProps.forEach(p => actor.remove_transition(p));
+
+ if (actor.get_easing_duration() > 0 || !isReversed)
+ actor.set(params);
+ actor.restore_easing_state();
+
+ const transitions = animatedProps
+ .map(p => actor.get_transition(p))
+ .filter(t => t !== null);
+
+ transitions.forEach(t => t.set({ repeatCount, autoReverse }));
+
+ const [transition] = transitions;
+
+ if (transition && transition.delay)
+ transition.connect('started', () => prepare());
+ else
+ prepare();
+
+ if (transition)
+ transition.connect('stopped', (t, finished) => callback(finished));
+ else
+ callback(true);
+}
+
+function _easeActorProperty(actor, propName, target, params) {
+ // Avoid pointless difference with ease()
+ if (params.mode)
+ params.progress_mode = params.mode;
+ delete params.mode;
+
+ if (params.duration)
+ params.duration = adjustAnimationTime(params.duration);
+ let duration = Math.floor(params.duration || 0);
+
+ let repeatCount = 0;
+ if (params.repeatCount != undefined)
+ repeatCount = params.repeatCount;
+ delete params.repeatCount;
+
+ let autoReverse = false;
+ if (params.autoReverse != undefined)
+ autoReverse = params.autoReverse;
+ delete params.autoReverse;
+
+ // repeatCount doesn't include the initial iteration
+ const numIterations = repeatCount + 1;
+ // whether the transition should finish where it started
+ const isReversed = autoReverse && numIterations % 2 === 0;
+
+ // Copy Clutter's behavior for implicit animations, see
+ // should_skip_implicit_transition()
+ if (actor instanceof Clutter.Actor && !actor.mapped)
+ duration = 0;
+
+ const prepare = () => {
+ Meta.disable_unredirect_for_display(global.display);
+ global.begin_work();
+ };
+ const cleanup = () => {
+ Meta.enable_unredirect_for_display(global.display);
+ global.end_work();
+ };
+ let callback = _makeEaseCallback(params, cleanup);
+
+ // cancel overwritten transition
+ actor.remove_transition(propName);
+
+ if (duration == 0) {
+ let [obj, prop] = _getPropertyTarget(actor, propName);
+
+ if (!isReversed)
+ obj[prop] = target;
+
+ prepare();
+ callback(true);
+
+ return;
+ }
+
+ let pspec = actor.find_property(propName);
+ let transition = new Clutter.PropertyTransition(Object.assign({
+ property_name: propName,
+ interval: new Clutter.Interval({ value_type: pspec.value_type }),
+ remove_on_complete: true,
+ repeat_count: repeatCount,
+ auto_reverse: autoReverse,
+ }, params));
+ actor.add_transition(propName, transition);
+
+ transition.set_to(target);
+
+ if (transition.delay)
+ transition.connect('started', () => prepare());
+ else
+ prepare();
+
+ transition.connect('stopped', (t, finished) => callback(finished));
+}
+
+function init() {
+ // Add some bindings to the global JS namespace
+ globalThis.global = Shell.Global.get();
+
+ globalThis._ = Gettext.gettext;
+ globalThis.C_ = Gettext.pgettext;
+ globalThis.ngettext = Gettext.ngettext;
+ globalThis.N_ = s => s;
+
+ GObject.gtypeNameBasedOnJSPath = true;
+
+ GObject.Object.prototype.connectObject = function (...args) {
+ SignalTracker.connectObject(this, ...args);
+ };
+ GObject.Object.prototype.connect_object = function (...args) {
+ SignalTracker.connectObject(this, ...args);
+ };
+ GObject.Object.prototype.disconnectObject = function (...args) {
+ SignalTracker.disconnectObject(this, ...args);
+ };
+ GObject.Object.prototype.disconnect_object = function (...args) {
+ SignalTracker.disconnectObject(this, ...args);
+ };
+
+ SignalTracker.registerDestroyableType(Clutter.Actor);
+
+ // Miscellaneous monkeypatching
+ _patchContainerClass(St.BoxLayout);
+
+ _patchLayoutClass(Clutter.GridLayout, {
+ row_spacing: 'spacing-rows',
+ column_spacing: 'spacing-columns',
+ });
+ _patchLayoutClass(Clutter.BoxLayout, { spacing: 'spacing' });
+
+ let origSetEasingDuration = Clutter.Actor.prototype.set_easing_duration;
+ Clutter.Actor.prototype.set_easing_duration = function (msecs) {
+ origSetEasingDuration.call(this, adjustAnimationTime(msecs));
+ };
+ let origSetEasingDelay = Clutter.Actor.prototype.set_easing_delay;
+ Clutter.Actor.prototype.set_easing_delay = function (msecs) {
+ origSetEasingDelay.call(this, adjustAnimationTime(msecs));
+ };
+
+ Clutter.Actor.prototype.ease = function (props) {
+ _easeActor(this, props);
+ };
+ Clutter.Actor.prototype.ease_property = function (propName, target, params) {
+ _easeActorProperty(this, propName, target, params);
+ };
+ St.Adjustment.prototype.ease = function (target, params) {
+ // we're not an actor of course, but we implement the same
+ // transition API as Clutter.Actor, so this works anyway
+ _easeActorProperty(this, 'value', target, params);
+ };
+
+ Clutter.Actor.prototype[Symbol.iterator] = function* () {
+ for (let c = this.get_first_child(); c; c = c.get_next_sibling())
+ yield c;
+ };
+
+ Clutter.Actor.prototype.toString = function () {
+ return St.describe_actor(this);
+ };
+ // Deprecation warning for former JS classes turned into an actor subclass
+ Object.defineProperty(Clutter.Actor.prototype, 'actor', {
+ get() {
+ let klass = this.constructor.name;
+ let { stack } = new Error();
+ log(`Usage of object.actor is deprecated for ${klass}\n${stack}`);
+ return this;
+ },
+ });
+
+ Gio.File.prototype.touch_async = function (callback) {
+ Shell.util_touch_file_async(this, callback);
+ };
+ Gio.File.prototype.touch_finish = function (result) {
+ return Shell.util_touch_file_finish(this, result);
+ };
+
+ St.set_slow_down_factor = function (factor) {
+ let { stack } = new Error();
+ log(`St.set_slow_down_factor() is deprecated, use St.Settings.slow_down_factor\n${stack}`);
+ St.Settings.get().slow_down_factor = factor;
+ };
+
+ let origToString = Object.prototype.toString;
+ Object.prototype.toString = function () {
+ let base = origToString.call(this);
+ try {
+ if ('actor' in this && this.actor instanceof Clutter.Actor)
+ return base.replace(/\]$/, ` delegate for ${this.actor.toString().substring(1)}`);
+ else
+ return base;
+ } catch (e) {
+ return base;
+ }
+ };
+
+ // Override to clear our own timezone cache as well
+ const origClearDateCaches = System.clearDateCaches;
+ System.clearDateCaches = function () {
+ _localTimeZone = null;
+ origClearDateCaches();
+ };
+
+ // Work around https://bugzilla.mozilla.org/show_bug.cgi?id=508783
+ Date.prototype.toLocaleFormat = function (format) {
+ if (_localTimeZone === null)
+ _localTimeZone = GLib.TimeZone.new_local();
+
+ let dt = GLib.DateTime.new(_localTimeZone,
+ this.getFullYear(),
+ this.getMonth() + 1,
+ this.getDate(),
+ this.getHours(),
+ this.getMinutes(),
+ this.getSeconds());
+ return dt?.format(format) ?? '';
+ };
+
+ let slowdownEnv = GLib.getenv('GNOME_SHELL_SLOWDOWN_FACTOR');
+ if (slowdownEnv) {
+ let factor = parseFloat(slowdownEnv);
+ if (!isNaN(factor) && factor > 0.0)
+ St.Settings.get().slow_down_factor = factor;
+ }
+
+ // OK, now things are initialized enough that we can import shell JS
+ const Format = imports.format;
+
+ String.prototype.format = Format.format;
+
+ Math.clamp = function (x, lower, upper) {
+ return Math.min(Math.max(x, lower), upper);
+ };
+}
+
+// adjustAnimationTime:
+// @msecs: time in milliseconds
+//
+// Adjust @msecs to account for St's enable-animations
+// and slow-down-factor settings
+function adjustAnimationTime(msecs) {
+ let settings = St.Settings.get();
+
+ if (!settings.enable_animations)
+ return Math.min(msecs, 1);
+ return settings.slow_down_factor * msecs;
+}
+