I was just wondering, is there any way of 'grouping' windows? I mean, is there a way of joining the edges of two or more windows together so that when one is moved, the other moves with it, acting as one large window? Or at least something similar where moving one window moves the other in the same way? I am running Ubuntu GNOME 15.10 with GNOME 3.18.


Solution 1:

Unfinished answer; looking for input

While at first sight it seems very well doable, using wmctrl, as always, reality is (much) more complicated than theory.

I am hesitating to post this as an answer, since it is only an experimental, conceptual answer, not a ready-to-use solution (yet) due to some bugs. I am posting it nevertheless in the hope to get some input on solving issues in the current version. The question is interesting enough for further development (IMO) to see if a smooth solution can be created.

Language: None

Although I wrote the script in Python, the language is irrelevant to the issues I am running into; mostly related to peculiarities in the use of wmctrl.

I could use xdotool to position windows, but since OP mentions moving a set of windows to another workspace, wmctrl has some advantages, especially on using Gnome, where workspaces are arranged different from Unity.

An example how it currently works, moving a group of windows

enter image description here

moving windows as a group

  • screen cast

The example in the screen cast above was made on Unity. It should however work similarly on Gnome (apart from the deviation, see further below about the script).

  • In the current setup, a group of windows can be created by adding the frontmost window to the group by calling the script below with the argument: a. In the screen cast, I added the command to a (Unity) launcher, on Gnome, it could be done with a shortcut key.
  • Subsequently, if the script is called with the argument r after one of the grouped windows is moved, the script moves all windows from the group likewise, restoring the windows, relative to each other:

How it is done

  • Run with the option a, the script further below adds the currently active window, its position and size (as in the corresponding line in the output of wmctrl -lG), to a file, wgroup_data.txt (in ~).
  • Run with the option r, the script reads the file, looks for the window that changed position, calculates the vector between the old- and the new position and moves the other windows accordingly.
  • If a window is closed, it is automatically removed from the group list by the script.

So far no problem.

Issues

However, if one of the windows does not fully fit inside the current workspace's borders, the script fails. Actually, the wmctrl command fails, since it also fails "outside" the script, run as a single command.

  • see when it fails

The script

#!/usr/bin/env python3
import subprocess
import os
import sys

arg = sys.argv[1]

# vertical deviation for Unity (use 0 for Gnome)
deviation = 28

fdata = os.environ["HOME"]+"/wgroup_data.txt"

def get_wmctrl():
    # try because of buggy wmctrl...
    try:
        return subprocess.check_output(["wmctrl", "-lG"]).decode("utf-8")
    except subprocess.CalledProcessError:
        pass

def remove_window(window):
    data = open(fdata).readlines()
    [data.remove(l) for l in data if l.startswith(window)]
    open(fdata, "wt").write(("").join(data))

def addwindow():
    relevant = get_wmctrl()
    frontmost = hex(int((subprocess.check_output(["xdotool", "getactivewindow"]).decode("utf-8").strip())))
    frontmost = frontmost[:2]+str((10-len(frontmost))*"0")+frontmost[2:]
    open(fdata, "+a").write([l+("\n") for l in get_wmctrl().splitlines() if frontmost in l][0])

    print(frontmost)

def rearrange():
    wlist = get_wmctrl()
    if wlist != None:
        group = [(l.strip(), l.split()) for l in open(fdata).read().splitlines() if not l in ("", "\n")]
        try: 
            changed = [w for w in group if (w[0] in wlist, w[1][0] in wlist) == (False, True)][0] #
            # only proceed if one of the grouped windows moved (give priority to a light loop if not):
            follow = []
            for w in group:
                if not w == changed:
                    test = (w[0] in wlist, w[1][0] in wlist)
                    if test == (True, True):
                        follow.append(w)
                    elif test == (False, False):
                        # remove closed window from list
                        remove_window(w[1][0])
            # only proceed if there are windows to move:
            if follow:
                # find match of the moved window (new coords)
                wlines = wlist.splitlines()
                match = [l.split() for l in wlines if changed[1][0] in l][0]
                # calculate the move vector
                x_move = int(match[2])-(int(changed[1][2])); y_move = int(match[3])-(int(changed[1][3]))
                for w in follow:
                    # should be changed to try?
                    w[1][2] = str(int(w[1][2]) + x_move); w[1][3] = str(int(w[1][3]) + y_move - deviation)
                    subprocess.Popen([
                        "wmctrl", "-ir", w[1][0], "-e",
                        (",").join([w[1][1], w[1][2], w[1][3], w[1][4], w[1][5]])
                        ])
                # update grouplist
                while True:
                    try:
                        newlines = sum([[l for l in get_wmctrl().splitlines() if w in l] for w in [match[0]]+[item[1][0] for item in follow]], [])
                        open(fdata, "wt").write(("\n").join(newlines))                 
                        break
                    except AttributeError:
                        pass
        except IndexError:
            print("nothing changed")

if arg == "a":
    addwindow()
elif arg == "r":
    rearrange()

How to use

  1. The script needs both wmctrl and xdotool

    sudo apt-get install xdotool wmctrl
    
  2. Copy the script into an empty file, save it as group_windows.py

  3. If you are on Gnome:
    In the head section of the script, change the line:

    deviation = 28
    

    into

    deviation = 0 
    
  4. Add two commands to different shortcuts:

    python3 /path/to/group_windows.py a
    

    to add windows to a group, and

    python3 /path/to/group_windows.py r
    

    to rearrange windows, as shown in the screen cast

  5. Test the script by adding some windows to a group, move them around and restore their relative position, as shown in the screen cast.

Further development

The issue could be solved, code- wise, by simply refusing to move the windows in case any of the windows is expected to get out of the current workspace. In that case even the just moved window should be returned to its initial position to keep the relative positions alive.

This would however need extensive calculating (nothing for the computer, but complicated to code), and it would be more elegant to make partial positioning outside the current workspace possible; it is no problem when a window is positioned "on or over the edge" manually.
Any suggestions on solving the issue is more than welcome.