Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
288 changes: 91 additions & 197 deletions src/WorkspaceManager.vala
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
//
// Copyright (C) 2014 Tom Beckmann, Rico Tzschichholz
//
// This program 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.
//
// This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
//
/*
* Copyright 2014 Tom Beckmann, Rico Tzschichholz
* Copyright 2025 elementary, Inc. (https://elementary.io)
* SPDX-License-Identifier: GPL-3.0-or-later
*/

public class Gala.WorkspaceManager : Object {
public static void init (WindowManager wm) requires (instance == null) {
Expand All @@ -28,47 +17,27 @@ public class Gala.WorkspaceManager : Object {

public WindowManager wm { get; construct; }

private Gee.LinkedList<Meta.Workspace> workspaces_marked_removed;
private int remove_freeze_count = 0;
private uint check_workspaces_id = 0;

private WorkspaceManager (WindowManager wm) {
Object (wm: wm);
}

construct {
workspaces_marked_removed = new Gee.LinkedList<Meta.Workspace> ();
unowned Meta.Display display = wm.get_display ();
unowned Meta.WorkspaceManager manager = display.get_workspace_manager ();

// There are some empty workspace at startup
cleanup ();
unowned var display = wm.get_display ();
unowned var manager = display.get_workspace_manager ();

manager.override_workspace_layout (Meta.DisplayCorner.TOPLEFT, false, 1, -1);

for (var i = 0; i < manager.get_n_workspaces (); i++) {
workspace_added (manager, i);
}

manager.workspace_switched.connect_after (workspace_switched);
manager.workspace_switched.connect_after (queue_check_workspaces);
manager.workspace_added.connect (workspace_added);
manager.workspace_removed.connect_after (workspace_removed);
display.window_entered_monitor.connect (window_entered_monitor);
display.window_left_monitor.connect (window_left_monitor);

// make sure the last workspace has no windows on it
if (Utils.get_n_windows (manager.get_workspace_by_index (manager.get_n_workspaces () - 1)) > 0) {
append_workspace ();
}
}

~WorkspaceManager () {
unowned Meta.Display display = wm.get_display ();
unowned Meta.WorkspaceManager manager = display.get_workspace_manager ();
manager.workspace_added.disconnect (workspace_added);
manager.workspace_switched.disconnect (workspace_switched);
manager.workspace_removed.disconnect (workspace_removed);
display.window_entered_monitor.disconnect (window_entered_monitor);
display.window_left_monitor.disconnect (window_left_monitor);
}

private void workspace_added (Meta.WorkspaceManager manager, int index) {
Expand All @@ -77,203 +46,128 @@ public class Gala.WorkspaceManager : Object {
return;
}

workspace.window_added.connect (queue_window_added);
workspace.window_removed.connect (window_removed);
workspace.window_added.connect (queue_check_workspaces);
workspace.window_removed.connect (queue_check_workspaces);
}

private void workspace_removed (Meta.WorkspaceManager manager, int index) {
List<Meta.Workspace> existing_workspaces = null;
for (int i = 0; i < manager.get_n_workspaces (); i++) {
existing_workspaces.append (manager.get_workspace_by_index (i));
}

var it = workspaces_marked_removed.iterator ();
while (it.next ()) {
var workspace = it.@get ();

if (existing_workspaces.index (workspace) < 0) {
it.remove ();
}
private void window_entered_monitor (Meta.Display display, int monitor, Meta.Window window) {
if (Meta.Prefs.get_workspaces_only_on_primary () && monitor == display.get_primary_monitor ()) {
queue_check_workspaces ();
}
}

private void workspace_switched (Meta.WorkspaceManager manager, int from, int to, Meta.MotionDirection direction) {
// remove empty workspaces after we switched away from them
maybe_remove_workspace (manager.get_workspace_by_index (from), null);
private void window_left_monitor (Meta.Display display, int monitor, Meta.Window window) {
if (Meta.Prefs.get_workspaces_only_on_primary () && monitor == display.get_primary_monitor ()) {
queue_check_workspaces ();
}
}

private void queue_window_added (Meta.Workspace? workspace, Meta.Window window) {
// We get this call very early so we have to queue an idle for ShellClients
// that might not have checked the window/got a protocol call yet
Idle.add (() => window_added (workspace, window));
/**
* Temporarily disables removing workspaces when they are empty
*/
public void freeze_remove () {
GLib.AtomicInt.inc (ref remove_freeze_count);
}

private bool window_added (Meta.Workspace? workspace, Meta.Window window) {
if (workspace == null || window.on_all_workspaces) {
return Source.REMOVE;
}

unowned Meta.WorkspaceManager manager = workspace.get_display ().get_workspace_manager ();
int last_workspace = manager.get_n_workspaces () - 1;

if ((window.window_type == Meta.WindowType.NORMAL
|| window.window_type == Meta.WindowType.DIALOG
|| window.window_type == Meta.WindowType.MODAL_DIALOG)
&& workspace.index () == last_workspace
) {
append_workspace ();
/**
* Undo the effect of freeze_remove()
*/
public void thaw_remove () {
if (GLib.AtomicInt.dec_and_test (ref remove_freeze_count)) {
queue_check_workspaces ();
}

return Source.REMOVE;
assert (remove_freeze_count >= 0);
}

private void window_removed (Meta.Workspace? workspace, Meta.Window window) {
if (workspace == null || window.on_all_workspaces) {
return;
private void queue_check_workspaces () {
if (check_workspaces_id == 0) {
var laters = wm.get_display ().get_compositor ().get_laters ();
check_workspaces_id = laters.add (BEFORE_REDRAW, check_workspaces);
}
}

if (window.window_type != Meta.WindowType.NORMAL
&& window.window_type != Meta.WindowType.DIALOG
&& window.window_type != Meta.WindowType.MODAL_DIALOG
) {
return;
}
private bool check_workspaces () {
unowned var display = wm.get_display ();
unowned var manager = display.get_workspace_manager ();

// has already been removed
if (workspace.index () < 0) {
return;
if (remove_freeze_count > 0) {
return Source.CONTINUE;
}

maybe_remove_workspace (workspace, window);
}
bool[] empty_workspaces = new bool[manager.n_workspaces];

private void window_entered_monitor (Meta.Display display, int monitor, Meta.Window window) {
if (Meta.Prefs.get_workspaces_only_on_primary () && monitor == display.get_primary_monitor ()) {
queue_window_added (window.get_workspace (), window);
for (int i = 0; i < empty_workspaces.length; i++) {
empty_workspaces[i] = true;
}
}

private void window_left_monitor (Meta.Display display, int monitor, Meta.Window window) {
if (Meta.Prefs.get_workspaces_only_on_primary () && monitor == display.get_primary_monitor ()) {
window_removed (window.get_workspace (), window);
unowned var active_startup_sequences = display.get_startup_notification ().get_sequences ();
foreach (var startup_sequence in active_startup_sequences) {
var index = startup_sequence.get_workspace ();
if (index >= 0 && index < empty_workspaces.length) {
empty_workspaces[index] = false;
}
}
}

private void append_workspace () {
unowned Meta.Display display = wm.get_display ();
unowned Meta.WorkspaceManager manager = display.get_workspace_manager ();
unowned var window_actors = display.get_window_actors ();
foreach (var actor in window_actors) {
var win = actor.meta_window;

manager.append_new_workspace (false, display.get_current_time ());
}
if (win == null || win.on_all_workspaces || win.get_workspace () == null) {
continue;
}

private void maybe_remove_workspace (Meta.Workspace workspace, Meta.Window? window) {
unowned var manager = workspace.get_display ().get_workspace_manager ();
var is_active_workspace = workspace == manager.get_active_workspace ();
var last_workspace_index = manager.get_n_workspaces () - 1 - workspaces_marked_removed.size;

// remove it right away if it was the active workspace and it's not the very last
// or we are in modal-mode
if ((!is_active_workspace || wm.is_modal ())
&& remove_freeze_count < 1
&& Utils.get_n_windows (workspace, true, window) == 0
&& workspace.index () != last_workspace_index
) {
queue_remove_workspace (workspace);
} else if (is_active_workspace // if window is the second last and empty, make it the last workspace
&& remove_freeze_count < 1
&& Utils.get_n_windows (workspace, true, window) == 0
&& workspace.index () == last_workspace_index - 1
) {
queue_remove_workspace (manager.get_workspace_by_index (last_workspace_index));
empty_workspaces[win.get_workspace ().index ()] = false;
}
}

private void queue_remove_workspace (Meta.Workspace workspace) {
// workspace has already been removed
if (workspace in workspaces_marked_removed) {
return;
// If we don't have an empty workspace at the end, add one
if (!empty_workspaces[empty_workspaces.length - 1]) {
manager.append_new_workspace (false, display.get_current_time ());
empty_workspaces += true;
}

workspaces_marked_removed.add (workspace);

// We might be here because of a signal emition from the ws machinery (e.g. workspace.window_removed).
// Often the function emitting the signal doesn't take a ref on the ws so if we remove it right
// away it will be freed. But because the function often accesses it after the singal emition this leads
// to warnings and in some cases a crash.
Idle.add (() => remove_workspace (workspace));
}

/**
* Make sure we switch to a different workspace and remove the given one
*
* @param workspace The workspace to remove
*/
private bool remove_workspace (Meta.Workspace workspace) {
unowned Meta.Display display = workspace.get_display ();
unowned Meta.WorkspaceManager manager = display.get_workspace_manager ();
var time = display.get_current_time ();
unowned Meta.Workspace active_workspace = manager.get_active_workspace ();

if (workspace == active_workspace) {
Meta.Workspace? next = null;

next = workspace.get_neighbor (Meta.MotionDirection.LEFT);
// if it's the first one we may have another one to the right
if (next == workspace || next == null) {
next = workspace.get_neighbor (Meta.MotionDirection.RIGHT);
}
var last_index = empty_workspaces.length - 1;

if (next != null) {
next.activate (time);
int last_empty_index = 0;
for (int i = last_index; i >= 0; i--) {
if (!empty_workspaces[i]) {
last_empty_index = i + 1;
break;
}
}

workspace.window_added.disconnect (queue_window_added);
workspace.window_removed.disconnect (window_removed);

manager.remove_workspace (workspace, time);

return Source.REMOVE;
}
if (!wm.is_modal ()) {
var active_index = manager.get_active_workspace_index ();
empty_workspaces[active_index] = false;
}

/**
* Temporarily disables removing workspaces when they are empty
*/
public void freeze_remove () {
GLib.AtomicInt.inc (ref remove_freeze_count);
}
// Delete empty workspaces except for the last one; do it from the end
// to avoid index changes
for (int i = last_index; i >= 0; i--) {
if (!empty_workspaces[i] || i == last_empty_index) {
continue;
}

/**
* Undo the effect of freeze_remove()
*/
public void thaw_remove () {
if (GLib.AtomicInt.dec_and_test (ref remove_freeze_count)) {
cleanup ();
}
var workspace = manager.get_workspace_by_index (i);

assert (remove_freeze_count >= 0);
}
if (workspace == manager.get_active_workspace ()) {
Meta.Workspace? next = null;

/**
* If workspaces are dynamic, checks if there are empty workspaces that should
* be removed. Particularly useful in conjunction with freeze/thaw_remove to
* cleanup after an operation that required stable workspace/window indices
*/
private void cleanup () {
unowned Meta.WorkspaceManager manager = wm.get_display ().get_workspace_manager ();
next = workspace.get_neighbor (LEFT);
// If it's the first one we may have another one to the right
if (next == workspace || next == null) {
next = workspace.get_neighbor (RIGHT);
}

bool remove_last = false;
foreach (var workspace in manager.get_workspaces ().copy ()) {
if (Utils.get_n_windows (workspace, true) != 0) {
remove_last = false;
continue;
if (next != null) {
next.activate (display.get_current_time ());
}
}

if (workspace.active) {
remove_last = true;
} else if (workspace.index () != manager.n_workspaces - 1 || remove_last) {
remove_workspace (workspace);
}
manager.remove_workspace (workspace, display.get_current_time ());
}

check_workspaces_id = 0;
return Source.REMOVE;
}
}