-
-
Notifications
You must be signed in to change notification settings - Fork 22
/
extension.js
1314 lines (1084 loc) · 57.5 KB
/
extension.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//////////////////////////////////////////////////////////////////////////////////////////
// ,-. ,--. ,-. , , ,---. ,-. ;-. ,-. . . ,-. ,--. //
// | \ | ( ` | / | / \ | ) / | | | ) | //
// | | |- `-. |< | | | |-' | | | |-< |- //
// | / | . ) | \ | \ / | \ | | | ) | //
// `-' `--' `-' ' ` ' `-' ' `-' `--` `-' `--' //
//////////////////////////////////////////////////////////////////////////////////////////
// SPDX-FileCopyrightText: Simon Schneegans <[email protected]>
// SPDX-License-Identifier: GPL-3.0-or-later
'use strict';
import Gio from 'gi://Gio';
import Meta from 'gi://Meta';
import Clutter from 'gi://Clutter';
import Graphene from 'gi://Graphene';
import GObject from 'gi://GObject';
import Shell from 'gi://Shell';
import St from 'gi://St';
import * as Util from 'resource:///org/gnome/shell/misc/util.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as Config from 'resource:///org/gnome/shell/misc/config.js';
import {PressureBarrier} from 'resource:///org/gnome/shell/ui/layout.js';
import {WorkspacesView, FitMode} from 'resource:///org/gnome/shell/ui/workspacesView.js';
import {SwipeTracker} from 'resource:///org/gnome/shell/ui/swipeTracker.js';
import {WorkspaceAnimationController} from 'resource:///org/gnome/shell/ui/workspaceAnimation.js';
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
import * as utils from './src/utils.js';
import {DragGesture} from './src/DragGesture.js';
import {Skybox} from './src/Skybox.js';
//////////////////////////////////////////////////////////////////////////////////////////
// This extensions tweaks the positioning of workspaces in overview mode and while //
// switching workspaces in desktop mode to make them look like cube faces. //
//////////////////////////////////////////////////////////////////////////////////////////
const [GS_VERSION] = Config.PACKAGE_VERSION.split('.').map(s => Number(s));
// Maximum degrees the cube can be rotated up and down.
const MAX_VERTICAL_ROTATION = 50;
// Spacing to the screen sides of the vertically rotated cube.
const PADDING_V_ROTATION = 0.2;
export default class DesktopCube extends Extension {
_lastWorkspaceWidth = 0;
// ------------------------------------------------------------------------ public stuff
// This function could be called after the extension is enabled, which could be done
// from GNOME Tweaks, when you log in or when the screen is unlocked.
enable() {
// Store a reference to the settings object.
this._settings = this.getSettings();
// We will monkey-patch these methods. Let's store the original ones.
this._origEndGesture = SwipeTracker.prototype._endGesture;
this._origUpdateWorkspacesState = WorkspacesView.prototype._updateWorkspacesState;
this._origGetSpacing = WorkspacesView.prototype._getSpacing;
this._origUpdateVisibility = WorkspacesView.prototype._updateVisibility;
this._origPrepSwitch = WorkspaceAnimationController.prototype._prepareWorkspaceSwitch;
this._origFinalSwitch = WorkspaceAnimationController.prototype._finishWorkspaceSwitch;
// We will use extensionThis to refer to the extension inside the patched methods.
const extensionThis = this;
// -----------------------------------------------------------------------------------
// ------------------------------- cubify the overview -------------------------------
// -----------------------------------------------------------------------------------
// Normally, all workspaces outside the current field-of-view are hidden. We want to
// show all workspaces, so we patch this method. The original code is about here:
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/workspacesView.js#L420
WorkspacesView.prototype._updateVisibility = function() {
this._workspaces.forEach((w) => {
w.show();
});
};
// Usually, workspaces are placed next to each other separated by a few pixels (this
// separation is usually computed by the method below). To create the desktop cube, we
// have to position all workspaces on top of each other and rotate the around a pivot
// point in the center of the cube.
// The original arrangement of the workspaces is implemented in WorkspacesView's
// vfunc_allocate() which cannot be monkey-patched. As a workaround, we return a
// negative spacing in the method below...
// The original code is about here:
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/workspacesView.js#L219
WorkspacesView.prototype._getSpacing = function(box, fitMode, vertical) {
// We use the "normal" workspace spacing in desktop and app-grid mode.
const origValue =
extensionThis._origGetSpacing.apply(this, [box, fitMode, vertical]);
if (fitMode == FitMode.ALL) {
return origValue;
}
// Compute the negative spacing required to arrange workspaces on top of each other.
const overlapValue = -this._workspaces[0].get_preferred_width(box.get_size()[1])[1];
// Blend between the negative overlap-spacing and the "normal" spacing value.
const cubeMode = extensionThis._getCubeMode(this);
return Util.lerp(origValue, overlapValue, cubeMode);
};
// This is the main method which is called whenever the workspaces need to be
// repositioned.
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/workspacesView.js#L255
WorkspacesView.prototype._updateWorkspacesState = function() {
// Use the original method if we have just one workspace.
const faceCount = this._workspaces.length;
if (faceCount <= 1) {
extensionThis._origUpdateWorkspacesState.apply(this);
return;
}
// Here's a minor hack to improve the performance: During the transitions to / from
// the app drawer, this._updateWorkspacesState is called twice a frame. Once from
// the notify handler of this._fitModeAdjustment and thereafter once from the notify
// handler of this._overviewAdjustment. As this seems not so useful (and degrades
// performance a lot), we skip the first call. I am not aware of any cases where
// this._fitModeAdjustment is changed without any of the over adjustments to change
// as well...
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/workspacesView.js#L109
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/workspacesView.js#L45
if ((new Error()).stack.includes('fitModeNotify')) {
return;
}
// Compute blending state from and to the overview, from and to the app grid, and
// from and to the desktop mode. We will use cubeMode to fold and unfold the
// cube, overviewMode to add some depth between windows and backgrounds, and
// appDrawerMode to attenuate the scaling effect of the active workspace.
const appDrawerMode = extensionThis._getAppDrawerMode(this);
const overviewMode = extensionThis._getOverviewMode(this);
const cubeMode = extensionThis._getCubeMode(this);
// First we need the width of a single workspace. Simply calling
// this._workspaces[0]._background.width does not work in all cases, as this method
// seems to be called also when the background actor is not on the stage. As a hacky
// workaround, we store the last valid workspace width we got and use that value if
// we cannot get a new one...
let workspaceWidth = extensionThis._lastWorkspaceWidth;
const bg = this._workspaces[0]._background;
if (bg.get_stage() && bg.allocation.get_width() > 0) {
workspaceWidth = bg.allocation.get_width();
// Add gaps between workspaces in overview mode.
workspaceWidth +=
overviewMode * 2 * extensionThis._settings.get_int('workpace-separation');
extensionThis._lastWorkspaceWidth = workspaceWidth;
}
// That's the angle between consecutive workspaces.
const faceAngle = extensionThis._getFaceAngle(faceCount);
// That's the z-distance from the cube faces to the rotation pivot.
const centerDepth = extensionThis._getCenterDist(workspaceWidth, faceAngle);
// Apply vertical rotation if required. This comes from the pitch value of the
// modified SwipeTracker created by _addOverviewDragGesture() further below.
this.pivot_point_z = -centerDepth;
this.set_pivot_point(0.5, 0.5);
this.rotation_angle_x = extensionThis._pitch.value * MAX_VERTICAL_ROTATION;
// During rotations, the cube is scaled down and the windows are "exploded". If we
// are directly facing a cube side, the strengths of both effects are approaching
// zero. The strengths of both effects are small during horizontal rotations to make
// workspace-switching not so obtrusive. However, during vertical rotations, the
// effects are stronger.
const [depthOffset, explode] = extensionThis._getExplodeFactors(
this._scrollAdjustment.value, extensionThis._pitch.value, centerDepth,
this._monitorIndex);
// Now loop through all workspace and compute the individual rotations.
this._workspaces.forEach((w, index) => {
// First update the corner radii. Corners are only rounded in overview.
w.stateAdjustment.value = overviewMode;
// Now update the rotation of the cube face. The rotation center is -centerDepth
// units behind the front face.
w.pivot_point_z = -centerDepth;
// Make cube smaller during rotations.
w.translation_z = -depthOffset;
// The rotation angle is transitioned proportional to cubeMode^1.5. This slows
// down the rotation a bit closer to the desktop and to the app drawer.
w.rotation_angle_y =
Math.pow(cubeMode, 1.5) * (-this._scrollAdjustment.value + index) * faceAngle;
// Distance to being the active workspace in [-1...0...1].
const dist = Math.clamp(index - this._scrollAdjustment.value, -1, 1);
// This moves next and previous workspaces a bit to the left and right. This
// ensures that we can actually see them if we look at the cube from the front.
// The value is set to zero if we have five or more workspaces.
if (faceCount <= 4) {
w.translation_x =
dist * overviewMode * extensionThis._settings.get_int('horizontal-stretch');
} else {
w.translation_x = 0;
}
// Update opacity only in overview mode.
const opacityA = extensionThis._settings.get_int('active-workpace-opacity');
const opacityB = extensionThis._settings.get_int('inactive-workpace-opacity');
const opacity = Util.lerp(opacityA, opacityB, Math.abs(dist));
w._background.set_opacity(Util.lerp(255, opacity, overviewMode));
// Update workspace scale only in app grid mode. The 0.94 is supposed to be the
// same value as the WORKSPACE_INACTIVE_SCALE defined here:
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/workspacesView.js#L21
// As this is defined as 'const', we cannot access it here. But the exact value
// also not really matters...
const scale = Util.lerp(1, 0.94, Math.abs(dist) * appDrawerMode);
w.set_scale(scale, scale);
// Now we add some depth separation between the window clones. If the explode
// factor becomes too small, the depth sorting becomes non-deterministic.
if (explode > 0.001) {
const sortedActors = w._container.layout_manager._sortedWindows;
// Distribute the window clones translation_z values between zero and
// explode.
sortedActors.forEach((windowActor, j) => {
windowActor.translation_z = explode * (j + 1) / sortedActors.length;
});
// Now sort the window clones according to the orthogonal distance of the actor
// planes to the camera. This ensures proper depth sorting among the window
// clones.
if (sortedActors.length > 1) {
extensionThis._depthSortWindowActors(w._container.get_children(),
this._monitorIndex);
}
}
// Now we sort the children of the workspace (e.g. the background actor
// and the container for the window clones) by their orthogonal distance to the
// virtual camera. We add a tiny translation to the window-clone container to
// allow for proper sorting.
w._container.translation_z = 1;
extensionThis._depthSortWindowActors(w.get_children(), this._monitorIndex);
});
// The depth-sorting of cube faces is quite simple, we sort them by increasing
// rotation angle so that they are drawn back-to-front.
extensionThis._depthSortCubeFaces(this._workspaces);
};
// -----------------------------------------------------------------------------------
// --------------------- cubify workspace-switch in desktop mode ---------------------
// -----------------------------------------------------------------------------------
// This override rotates the workspaces during the transition to look like cube
// faces. The original movement of the workspaces is implemented in the setter of
// the progress property. We do not touch this, as keeping track of this progress
// is rather important. Instead, we listen to progress changes and tweak the
// transformation accordingly.
// This lambda is called in two places (further down), once for updates of the
// progress property, once for updates during gesture swipes. The latter does not
// trigger notify signals of the former for some reason...
const updateMonitorGroup = (group) => {
// First, we prevent any horizontal movement by countering the translation. We
// cannot simply set the x property to zero as this is used to track the
// progress.
group._container.translation_x = -group._container.x;
// That's the desired angle between consecutive workspaces.
const faceAngle = extensionThis._getFaceAngle(group._workspaceGroups.length);
// That's the z-distance from the cube faces to the rotation pivot.
const centerDepth =
extensionThis._getCenterDist(group._workspaceGroups[0].width, faceAngle);
// Apply vertical rotation if required. This comes from the pitch value of the
// modified SwipeTracker created by _addDesktopDragGesture() further below.
group._container.pivot_point_z = -centerDepth;
group._container.set_pivot_point(0.5, 0.5);
group._container.rotation_angle_x =
extensionThis._pitch.value * MAX_VERTICAL_ROTATION;
// During rotations, the cube is scaled down and the windows are "exploded". If we
// are directly facing a cube side, the strengths of both effects are approaching
// zero. The strengths of both effects are small during horizontal rotations to make
// workspace-switching not so obtrusive. However, during vertical rotations, the
// effects are stronger.
const [depthOffset, explode] = extensionThis._getExplodeFactors(
group.progress, extensionThis._pitch.value, centerDepth, group._monitor.index);
// Rotate the individual faces.
group._workspaceGroups.forEach((child, i) => {
child.set_pivot_point_z(-centerDepth);
child.set_pivot_point(0.5, 0.5);
child.rotation_angle_y = (i - group.progress) * faceAngle;
child.translation_z = -depthOffset;
child.clip_to_allocation = false;
// Counter the horizontal movement.
child.translation_x = -child.x;
// Make cube transparent during vertical rotations.
child._background.opacity = 255 * (1.0 - Math.abs(extensionThis._pitch.value));
// Now we add some depth separation between the window clones. We get the stacking
// order from the global window list. If the explode factor becomes too small, the
// depth sorting becomes non-deterministic.
if (explode > 0.001) {
const windowActors = global.get_window_actors().filter(w => {
return child._shouldShowWindow(w.meta_window);
});
// Distribute the window clones translation_z values between zero and
// explode.
windowActors.forEach((windowActor, j) => {
const record = child._windowRecords.find(r => r.windowActor === windowActor);
if (record) {
record.clone.translation_z = explode * (j + 1) / windowActors.length;
}
});
// Now sort the window clones and the background actor according to the
// orthogonal distance of the actor planes to the camera. This ensures proper
// depth sorting.
extensionThis._depthSortWindowActors(child.get_children(),
group._monitor.index);
}
});
// The depth-sorting of cube faces is quite simple, we sort them by increasing
// rotation angle.
extensionThis._depthSortCubeFaces(group._workspaceGroups);
// Update horizontal rotation of the background panorama during workspace switches.
if (this._skybox) {
this._skybox.yaw =
2 * Math.PI * group.progress / global.workspaceManager.get_n_workspaces();
}
};
// Whenever a workspace-switch is about to happen, we tweak the MonitorGroup class a
// bit to arrange the workspaces in a cube-like fashion. We have to adjust to parts of
// the code as the automatic transitions (e.g. when switching with key combinations)
// are handled differently than the gesture based switches.
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/workspaceAnimation.js#L299
WorkspaceAnimationController.prototype._prepareWorkspaceSwitch = function() {
// Here, we call the original method without any arguments. Usually, GNOME Shell
// "skips" workspaces when switching to a workspace which is more than one workspace
// to the left or the right. This behavior is not desirable for thr cube, as it
// messes with your spatial memory. If no workspaceIndices are given to this method,
// all workspaces will be shown during the workspace switch.
extensionThis._origPrepSwitch.apply(this, []);
// Now tweak the monitor groups.
this._switchData.monitors.forEach(m => {
// Call the method above whenever the transition progress changes.
m.connect('notify::progress', () => updateMonitorGroup(m));
// Call the method above whenever a gesture is active.
const orig = m.updateSwipeForMonitor;
m.updateSwipeForMonitor = function(progress, baseMonitorGroup) {
orig.apply(m, [progress, baseMonitorGroup]);
updateMonitorGroup(m);
};
});
// Make sure that the background panorama is drawn above the window group during a
// workspace switch.
if (extensionThis._skybox) {
extensionThis._skybox.get_parent().remove_child(extensionThis._skybox);
Main.uiGroup.insert_child_above(extensionThis._skybox, global.window_group);
// If the workspaces are only on the primary monitor, the skybox would cover all
// other non-rotating screens. Therefore, we temporarily limit its size to the
// primary monitor's size.
if (Meta.prefs_get_workspaces_only_on_primary()) {
const monitor =
global.display.get_monitor_geometry(global.display.get_primary_monitor());
extensionThis._skybox.width = monitor.width;
extensionThis._skybox.height = monitor.height;
extensionThis._skybox.x = monitor.x;
extensionThis._skybox.y = monitor.y;
}
}
};
// Re-attach the background panorama to the stage once the workspace switch is done.
WorkspaceAnimationController.prototype._finishWorkspaceSwitch = function(...params) {
extensionThis._origFinalSwitch.apply(this, params);
// Make sure that the skybox covers the entire stage again.
if (extensionThis._skybox) {
extensionThis._skybox.get_parent().remove_child(extensionThis._skybox);
global.stage.insert_child_below(extensionThis._skybox, null);
if (Meta.prefs_get_workspaces_only_on_primary()) {
extensionThis._skybox.width = global.stage.width;
extensionThis._skybox.height = global.stage.height;
extensionThis._skybox.x = global.stage.x;
extensionThis._skybox.y = global.stage.y;
}
}
};
// -----------------------------------------------------------------------------------
// ------------------------- enable cube rotation by dragging ------------------------
// -----------------------------------------------------------------------------------
// Usually, in GNOME Shell 40+, workspaces are move horizontally. We tweaked this to
// look like a horizontal rotation above. To store the current vertical rotation, we
// use the adjustment below.
this._pitch = new St.Adjustment({actor: global.stage, lower: -1, upper: 1});
// The overview's SwipeTracker will control the _overviewAdjustment of the
// WorkspacesDisplay. However, only horizontal swipes will update this adjustment. If
// only our pitch adjustment is changed (e.g. the user moved the mouse only
// vertically), the _overviewAdjustment will not change and therefore the workspaces
// will not been redrawn. Here we force redrawing by notifying changes if the pitch
// value changes.
this._pitch.connect('notify::value', () => {
if (Main.actionMode == Shell.ActionMode.OVERVIEW) {
Main.overview._overview._controls._workspacesDisplay._overviewAdjustment.notify(
'value');
}
});
// 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 an
// additional gesture to these two SwipeTracker instances tracking single-click drags.
// First, we fix an issue which leads to very quick workspace switches when the
// SwipeTracker are used with mouse clicks. When the mouse button is released, no
// event is added to the history. This means that the velocity is always calculated
// relative to the last received mouse movement. Even if he mouse pointer was
// stationary for some time, high velocities will be computed.
SwipeTracker.prototype._endGesture = function(time, distance, isTouchpad) {
// Add a final time step to the history.
this._history.append(time, 0);
// Then call the original method.
extensionThis._origEndGesture.apply(this, [time, distance, isTouchpad]);
};
// Add single-click drag gesture to the desktop.
if (this._settings.get_boolean('enable-desktop-dragging')) {
this._addDesktopDragGesture();
}
this._settings.connect('changed::enable-desktop-dragging', () => {
if (this._settings.get_boolean('enable-desktop-dragging')) {
this._addDesktopDragGesture();
} else {
this._removeDesktopDragGesture();
}
});
// Add single-click drag gesture to the panel.
if (this._settings.get_boolean('enable-panel-dragging')) {
this._addPanelDragGesture();
}
this._settings.connect('changed::enable-panel-dragging', () => {
if (this._settings.get_boolean('enable-panel-dragging')) {
this._addPanelDragGesture();
} else {
this._removePanelDragGesture();
}
});
// Add single-click drag gesture to the overview.
if (this._settings.get_boolean('enable-overview-dragging')) {
this._addOverviewDragGesture();
}
this._settings.connect('changed::enable-overview-dragging', () => {
if (this._settings.get_boolean('enable-overview-dragging')) {
this._addOverviewDragGesture();
} else {
this._removeOverviewDragGesture();
}
});
// -----------------------------------------------------------------------------------
// ---------------------------------- add the skybox ---------------------------------
// -----------------------------------------------------------------------------------
// This is called whenever the skybox texture setting is changed.
const updateSkybox = () => {
// First, delete the existing skybox.
if (this._skybox) {
this._skybox.destroy();
delete this._skybox;
}
const file = this._settings.get_string('background-panorama');
// Then, load a new one (if any).
if (file != '') {
try {
this._skybox = new Skybox(file);
// We add the skybox below everything.
global.stage.insert_child_below(this._skybox, null);
// Make sure that the skybox covers the entire stage.
global.stage.bind_property('width', this._skybox, 'width',
GObject.BindingFlags.SYNC_CREATE);
global.stage.bind_property('height', this._skybox, 'height',
GObject.BindingFlags.SYNC_CREATE);
} catch (error) {
utils.debug('Failed to set skybox: ' + error);
}
}
};
// Update the skybox whenever the corresponding setting is changed.
this._settings.connect('changed::background-panorama', updateSkybox);
updateSkybox();
// Update vertical rotation of the background panorama.
this._pitch.connect('notify::value', () => {
if (this._skybox) {
this._skybox.pitch = (this._pitch.value * MAX_VERTICAL_ROTATION) * Math.PI / 180;
}
});
// Update horizontal rotation of the background panorama during workspace switches in
// the overview.
Main.overview._overview.controls._workspaceAdjustment.connect('notify::value', () => {
if (this._skybox) {
this._skybox.yaw = 2 * Math.PI *
Main.overview._overview.controls._workspaceAdjustment.value /
global.workspaceManager.get_n_workspaces();
}
});
// -----------------------------------------------------------------------------------
// ----------------------- enable edge-drag workspace-switches -----------------------
// -----------------------------------------------------------------------------------
// We add two Meta.Barriers, one at each side of the stage. If the pointer hits one of
// these with enough pressure while dragging a window, we initiate a workspace-switch.
// The last parameter (0) is actually supposed to be a bitwise combination of
// Shell.ActionModes. The pressure barrier will only trigger, if Main.actionMode
// equals one of the given action modes. This works well for Shell.ActionMode.NORMAL
// and Shell.ActionMode.OVERVIEW, however it does not work for Shell.ActionMode.NONE
// (which actually equals zero). However, when we want the barriers to also trigger in
// Shell.ActionMode.NONE, as this is the mode during a drag-operation in the overview.
// Therefore, we modify the _onBarrierHit method of the pressure barrier to completely
// ignore this parameter. Instead, we check for the correct action mode in the trigger
// handler.
this._pressureBarrier =
new PressureBarrier(this._settings.get_int('edge-switch-pressure'), 1000, 0);
// Update pressure threshold when the corresponding settings key changes.
this._settings.connect('changed::edge-switch-pressure', () => {
this._pressureBarrier._threshold = this._settings.get_int('edge-switch-pressure');
});
// This is an exact copy of the original _onBarrierHit, with only one line disabled to
// ignore the given ActionMode.
// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/layout.js#L1411
this._pressureBarrier._onBarrierHit = function(barrier, event) {
barrier._isHit = true;
// If we've triggered the barrier, wait until the pointer has the
// left the barrier hitbox until we trigger it again.
if (this._isTriggered) return;
if (this._eventFilter && this._eventFilter(event)) return;
// Throw out all events not in the proper keybinding mode
// if (!(this._actionMode & Main.actionMode)) return;
let slide = this._getDistanceAlongBarrier(barrier, event);
let distance = this._getDistanceAcrossBarrier(barrier, event);
if (distance >= this._threshold) {
this._trigger();
return;
}
// Throw out events where the cursor is move more
// along the axis of the barrier than moving with
// the barrier.
if (slide > distance) return;
this._lastTime = event.time;
this._trimBarrierEvents();
distance = Math.min(15, distance);
this._barrierEvents.push([event.time, distance]);
this._currentPressure += distance;
if (this._currentPressure >= this._threshold) this._trigger();
};
// Now we add the left and right barrier to the pressure barrier.
const createBarriers = () => {
if (this._leftBarrier) {
this._pressureBarrier.removeBarrier(this._leftBarrier);
this._leftBarrier.destroy();
}
if (this._rightBarrier) {
this._pressureBarrier.removeBarrier(this._rightBarrier);
this._rightBarrier.destroy();
}
// Since GNOME 46, the display property is not required anymore.
if (GS_VERSION <= 45) {
this._leftBarrier = new Meta.Barrier({
display: global.display,
x1: 0,
x2: 0,
y1: 1,
y2: global.stage.height,
directions: Meta.BarrierDirection.POSITIVE_X,
});
this._rightBarrier = new Meta.Barrier({
display: global.display,
x1: global.stage.width,
x2: global.stage.width,
y1: 1,
y2: global.stage.height,
directions: Meta.BarrierDirection.NEGATIVE_X,
});
} else {
this._leftBarrier = new Meta.Barrier({
backend: global.backend,
x1: 0,
x2: 0,
y1: 1,
y2: global.stage.height,
directions: Meta.BarrierDirection.POSITIVE_X,
});
this._rightBarrier = new Meta.Barrier({
backend: global.backend,
x1: global.stage.width,
x2: global.stage.width,
y1: 1,
y2: global.stage.height,
directions: Meta.BarrierDirection.NEGATIVE_X,
});
}
this._pressureBarrier.addBarrier(this._leftBarrier);
this._pressureBarrier.addBarrier(this._rightBarrier);
};
// Re-create the barriers whenever the stage's allocation is changed.
this._stageAllocationID = global.stage.connect('notify::allocation', createBarriers);
createBarriers();
// When the pressure barrier is triggered, the corresponding setting is enabled, and a
// window is currently dragged, we move the dragged window to the adjacent workspace
// and activate it as well.
this._pressureBarrier.connect('trigger', () => {
const direction =
this._leftBarrier._isHit ? Meta.MotionDirection.LEFT : Meta.MotionDirection.RIGHT;
const newWorkspace =
global.workspace_manager.get_active_workspace().get_neighbor(direction);
if (Main.actionMode == Shell.ActionMode.NORMAL && this._draggedWindow &&
this._settings.get_boolean('enable-desktop-edge-switch')) {
Main.wm.actionMoveWindow(this._draggedWindow, newWorkspace);
} else if (Main.actionMode == Shell.ActionMode.NONE && Main.overview.visible &&
this._settings.get_boolean('enable-overview-edge-switch')) {
newWorkspace.activate(global.get_current_time());
}
});
// Keep a reference to the currently dragged window.
global.display.connect('grab-op-begin', (d, win, op) => {
if (op == Meta.GrabOp.MOVING) {
this._draggedWindow = win;
}
});
// Release the reference to the currently dragged window.
global.display.connect('grab-op-end', (d, win, op) => {
if (op == Meta.GrabOp.MOVING) {
this._draggedWindow = null;
}
});
// -----------------------------------------------------------------------------------
// ------------------- fix perspective of multi-monitor setups -----------------------
// -----------------------------------------------------------------------------------
// Usually, GNOME Shell uses one central perspective for all monitors combined. This
// results in a somewhat sheared appearance of the cube on multi-monitor setups where
// the primary monitor is not in the middle (or cubes are shown on multiple monitors).
// With the code below, we modify the projection and view matrices for each monitor so
// that each monitor uses its own central perspective. This seems to be possible on
// Wayland only. On X11, there's only one set of projection and view matrices for all
// monitors combined, so we tweak them so that the projection center is in the middle
// of the primary monitor. So it will at least only look bad on X11 if the cube is
// shown on all monitors...
const updateMonitorPerspective = () => {
// Disable the perspective fixes first...
this._disablePerspectiveCorrection();
// Store this so we do not have to get it too often.
this._enablePerMonitorPerspective =
this._settings.get_boolean('per-monitor-perspective') &&
global.display.get_n_monitors() > 1;
// ... and then enable them if required.
if (this._enablePerMonitorPerspective) {
this._enablePerspectiveCorrection();
}
};
this._settings.connect('changed::per-monitor-perspective', updateMonitorPerspective);
this._monitorsChangedID = global.backend.get_monitor_manager().connect(
'monitors-changed', updateMonitorPerspective);
updateMonitorPerspective();
}
// This function could be called after the extension is uninstalled, disabled in GNOME
// Tweaks, when you log out or when the screen locks.
disable() {
// Restore the original behavior.
SwipeTracker.prototype._endGesture = this._origEndGesture;
WorkspacesView.prototype._updateWorkspacesState = this._origUpdateWorkspacesState;
WorkspacesView.prototype._getSpacing = this._origGetSpacing;
WorkspacesView.prototype._updateVisibility = this._origUpdateVisibility;
WorkspaceAnimationController.prototype._prepareWorkspaceSwitch = this._origPrepSwitch;
WorkspaceAnimationController.prototype._finishWorkspaceSwitch = this._origFinalSwitch;
// Remove all drag-to-rotate gestures.
this._removeDesktopDragGesture();
this._removePanelDragGesture();
this._removeOverviewDragGesture();
// Clean up skybox.
if (this._skybox) {
this._skybox.destroy();
this._skybox = null;
}
// Clean up the edge-workspace-switching.
global.stage.disconnect(this._stageAllocationID);
this._pressureBarrier.destroy();
this._leftBarrier.destroy();
this._rightBarrier.destroy();
this._pressureBarrier = null;
this._leftBarrier = null;
this._rightBarrier = null;
// Clean up perspective correction.
this._disablePerspectiveCorrection();
global.backend.get_monitor_manager().disconnect(this._monitorsChangedID);
// Make sure that the settings object is freed.
this._settings = null;
}
// ----------------------------------------------------------------------- private stuff
// Calls inhibit_culling on the given actor and recursively on all mapped children.
_inhibitCulling(actor) {
if (actor.mapped) {
actor.inhibit_culling();
actor._culling_inhibited_by_desktop_cube = true;
actor.get_children().forEach(c => this._inhibitCulling(c));
}
};
// Calls uninhibit_culling on the given actor and recursively on all children. It will
// only call uninhibit_culling() on those actors which were inhibited before.
_uninhibitCulling(actor) {
if (actor._culling_inhibited_by_desktop_cube) {
delete actor._culling_inhibited_by_desktop_cube;
actor.uninhibit_culling();
actor.get_children().forEach(c => this._uninhibitCulling(c));
}
};
// Returns a value between [0...1] blending between overview (0) and app grid mode (1).
_getAppDrawerMode(workspacesView) {
return workspacesView._fitModeAdjustment.value;
}
// Returns a value between [0...1] blending between desktop / app drawer mode (0) and
// overview mode (1).
_getOverviewMode(workspacesView) {
return workspacesView._overviewAdjustment.value -
2 * this._getAppDrawerMode(workspacesView);
}
// Returns a value between [0...1]. If it's 0, the cube should be unfolded, if it's 1,
// the cube should be drawn like, well, a cube :).
_getCubeMode(workspacesView) {
return 1 - this._getAppDrawerMode(workspacesView)
}
// Returns the angle between consecutive workspaces.
_getFaceAngle(faceCount) {
// With this setting, our "cube" only covers 180°, if there are only two workspaces,
// it covers 90°. This prevents the affordance that it could be possible to switch
// from the last ot the first workspace.
if (this._settings.get_boolean('last-first-gap')) {
return (faceCount == 2 ? 90 : 180) / (faceCount - 1);
}
// Else the "cube" covers 360°.
return 360.0 / faceCount;
}
// Returns the z-distance from the cube faces to the rotation pivot.
_getCenterDist(workspaceWidth, faceAngle) {
let centerDepth = workspaceWidth / 2;
if (faceAngle < 180) {
centerDepth /= Math.tan(faceAngle * 0.5 * Math.PI / 180);
}
return centerDepth;
}
// This sorts the given list of children actors (which are supposed to be attached to
// the same parent) by increasing absolute rotation-y angle. This is used for
// depth-sorting, as cube faces which are less rotated, are in front of others.
_depthSortCubeFaces(actors) {
// First create a copy of the actors list and sort it by decreasing rotation angle.
const copy = actors.slice();
copy.sort((a, b) => {
return Math.abs(b.rotation_angle_y) - Math.abs(a.rotation_angle_y);
});
// Then sort the children actors accordingly.
const parent = actors[0].get_parent();
for (let i = 0; i < copy.length; i++) {
parent.set_child_at_index(copy[i], -1);
}
}
// This sorts the given list of children actors (which are supposed to be attached to
// the same parent) by increasing orthogonal distance to the camera. To do this, the
// camera position is projected onto the plane defined by the actor and the absolute
// distance from the camera to its projected position is computed. This is used for
// depth-sorting a list of parallel actors.
_depthSortWindowActors(actors, monitorIndex) {
// Sanity check.
if (actors.length <= 1) {
return;
}
// First, compute distance of virtual camera to the front workspace plane.
const camera = new Graphene.Point3D({
x: global.stage.width / 2,
y: global.stage.height / 2,
z: global.stage.height /
(2 * Math.tan(global.stage.perspective.fovy / 2 * Math.PI / 180))
});
// All actors are expected to share the same parent.
const parent = actors[0].get_parent();
// If the perspective is corrected for multi-monitor setups, the virtual camera is not
// in the middle of the stage but rather in front of each monitor.
if (this._enablePerMonitorPerspective) {
let monitor;
if (Meta.is_wayland_compositor()) {
// On Wayland, each monitor should have its own StageView. Therefore, the virtual
// camera has been positioned in front of each monitor separately.
monitor = global.display.get_monitor_geometry(monitorIndex);
} else {
// On X11, there's only one StageView. We move the virtual camera so that it is in
// front of the primary monitor.
monitor =
global.display.get_monitor_geometry(global.display.get_primary_monitor());
}
camera.x = monitor.x + monitor.width / 2;
camera.y = monitor.y + monitor.height / 2;
}
// Create a list of the orthogonal distances to the camera for each actor.
const distances = actors.map((a, i) => {
// A point on the actor plane.
const onActor = a.apply_relative_transform_to_point(
null, new Graphene.Point3D({x: 0, y: 0, z: 0}));
// A point one unit above the actor plane.
const aboveActor = a.apply_relative_transform_to_point(
null, new Graphene.Point3D({x: 0, y: 0, z: 1000}));
// The normal vector on the actor plane.
const normal = new Graphene.Point3D({
x: aboveActor.x - onActor.x,
y: aboveActor.y - onActor.y,
z: aboveActor.z - onActor.z,
});
const length =
Math.sqrt(normal.x * normal.x + normal.y * normal.y + normal.z * normal.z);
normal.x /= length;
normal.y /= length;
normal.z /= length;
onActor.x -= camera.x;
onActor.y -= camera.y;
onActor.z -= camera.z;
// Return the length of the projected vector.
return {
index: i,
distance: onActor.x * normal.x + onActor.y * normal.y + onActor.z * normal.z
};
});
// Sort by decreasing distance.
distances.sort((a, b) => {
return Math.abs(b.distance) - Math.abs(a.distance);
});
// Then use this to create a sorted list of actors.
const copy = distances.map(e => {
return actors[e.index];
});
// Finally, sort the children actors accordingly.
for (let i = 0; i < copy.length; i++) {
parent.set_child_at_index(copy[i], -1);
}
}
// During rotations, the cube is scaled down and the windows are "exploded". If we
// are directly facing a cube side, the strengths of both effects are approaching
// zero. The strengths of both effects are small during horizontal rotations to make
// workspace-switching not so obtrusive. However, during vertical rotations, the
// effects are stronger.
// This method returns two values:
// result[0]: A translation value by which the cube should be moved backwards.
// result[1]: A translation value by which windows may be moved away from the cube.
_getExplodeFactors(hRotation, vRotation, centerDepth, monitorIndex) {
// These are zero if we are facing a workspace and one if we look directly at an
// edge between adjacent workspaces or if the cube is rotated vertically
// respectively.
const hFactor = 1.0 - 2.0 * Math.abs(hRotation % 1 - 0.5);
const vFactor = Math.abs(vRotation);
// For horizontal rotations, we want to scale the cube (or rather move it backwards)
// a tiny bit to reveal a bit of parallax. However, if we have many cube sides, this
// looks weird, so we reduce the effect there. We use the offset which would make
// the cube's corners stay behind the original workspace faces during he rotation.
const monitor = global.display.get_monitor_geometry(monitorIndex);
const cornerDist =
Math.sqrt(Math.pow(centerDepth, 2) + Math.pow(monitor.width / 2, 2));
const hDepthOffset =
this._settings.get_double('window-parallax') * (cornerDist - centerDepth);
// The explode factor is set to the hDepthOffset value to make the front-most
// window stay at a constant depth.
const hExplode = hDepthOffset;
// For vertical rotations, we move the cube backwards to reveal everything. The
// maximum explode width is set to half of the workspace size.
const vExplode = this._settings.get_boolean('do-explode') ?
Math.max(monitor.width, monitor.height) / 2 :
0;
const diameter = 2 * (vExplode + centerDepth);
const camDist =
monitor.height / (2 * Math.tan(global.stage.perspective.fovy / 2 * Math.PI / 180));
const vDepthOffset =
(1 + PADDING_V_ROTATION) * diameter * camDist / monitor.width - centerDepth;