11

I run a MATLAB script in workspace 1. This generates several plots. In the mean time I switch to workspace 2 and work there. My problem is that the plots are popping up in workspace 2.

Is it possible to lock the software on a specific workspace, so while MATLAB generates the plots in workspace 1, I can work in workspace 2 without the disruption of the popping up plots?

Mokus
  • 4,502
  • Unity, GNOME Shell or something else? – A.B. Aug 27 '15 at 10:53
  • I add the tags, It is Ubuntu 14.04 with Unity – Mokus Aug 27 '15 at 10:56
  • To what class do the plot windows belong? (could you check with the command xprop WM_CLASS, and then click on the window?) Please also add the WM_CLASS of Matlab. – Jacob Vlijm Aug 27 '15 at 11:48
  • Matlab and the plots has the same WM_CLASS: WM_CLASS(STRING) = "sun-awt-X11-XFramePeer", "MATLAB R2015a - academic use" – Mokus Aug 27 '15 at 12:08
  • Interesting question. It is very well possible to automatically move new windows, belonging to an application to their "home" workspace in a split second, but that would imply that possibly you would very shortly see the window appear, and disappear immediately. To prevent the window to appear at all on the "wrong" workspace would require changing the code of the application, which is quite difficult. Would the first option be acceptable to you? It would take a small background script, possibly to be toggled on/off with a key shortcut. Let me know. – Jacob Vlijm Aug 27 '15 at 12:26
  • Will the first solution take away the focus of the cursor? – Mokus Aug 27 '15 at 12:41
  • It should not, but if it does, I won't post it :) – Jacob Vlijm Aug 27 '15 at 12:43
  • 2
    I will post later today, if not someone posts another brilliant solution in the meantime. – Jacob Vlijm Aug 27 '15 at 13:00
  • How about "docking" figures into the figures windows in Matlab? – Another approach would be to create all the figure windows at once, and then only later plot into them. – A. Donda Aug 27 '15 at 16:50
  • Going well, but might be tomorrow before it is "answer-ready". – Jacob Vlijm Aug 27 '15 at 18:54
  • 1
    Hi OHLÁLÁ, I actually got it working quite well, all additional windows of the application are instantly moved to the app's initial workspace, but.... indeed the current window on the current workspace loses focus nevertheless. Still figuring on a solution. Would you still try the solution? – Jacob Vlijm Aug 27 '15 at 22:24
  • Hi OHLÁLÁ, please let me know if the script is useful to you. The processor usage could be reduced to practically none with some tweaks, the focus issue is solved. Will probably continue editing tomorrow. – Jacob Vlijm Aug 28 '15 at 21:28
  • Thanks for the script. Right now I don't have access to my desktop PC, but monday I will try it out. – Mokus Aug 28 '15 at 21:30
  • I rewrote the script. It is pretty fresh and the test only runs for an hour now. Possibly we'll need to fix something still, but it works pretty well so far. – Jacob Vlijm Aug 30 '15 at 12:23

2 Answers2

8

IMPORTANT EDIT

Below a rewritten version of the script from the first answer (below). The differences:

  • The script now is extremely low on resources (like it should be with background scripts). The actions are now arranged to act if (and only if) they are needed. The loop does practically nothing but check for new windows to appear.
  • Bot the WM_CLASS and the targeted workspace are now arguments to run the script. Only use either the first or the second (identifying) part of the WM_CLASS (see further below: how to use)
  • The script now keeps focus on the currently active window (actually re- focusses in a split second)
  • When the script starts, it shows a notification (example gedit):

    enter image description here

The script

#!/usr/bin/env python3
import subprocess
import sys
import time
import math

app_class = sys.argv[1]
ws_lock = [int(n)-1 for n in sys.argv[2].split(",")]

def check_wlist():
    # get the current list of windows
    try:
        raw_list = [
            l.split() for l in subprocess.check_output(
                ["wmctrl", "-lG"]
                ).decode("utf-8").splitlines()
            ]
        ids = [l[0] for l in raw_list]
        return (raw_list, ids)
    except subprocess.CalledProcessError:
        pass

def get_wssize():
    # get workspace size
    resdata = subprocess.check_output(["xrandr"]).decode("utf-8").split()
    i = resdata.index("current")
    return [int(n) for n in [resdata[i+1], resdata[i+3].replace(",", "")]]

def get_current(ws_size):
    # vector of the current workspace to origin of the spanning desktop
    dt_data = subprocess.check_output(
        ["wmctrl", "-d"]
        ).decode("utf-8").split()
    curr = [int(n) for n in dt_data[5].split(",")]
    return (int(curr[0]/ws_size[0]), int(curr[1]/ws_size[1]))

def get_relativewinpos(ws_size, w_data):
    # vector to the application window, relative to the current workspace
    xpos = int(w_data[2]); ypos = int(w_data[3])
    xw = ws_size[0]; yw = ws_size[1]
    return (math.ceil((xpos-xw)/xw), math.ceil((ypos-yw)/yw))

def get_abswindowpos(ws_size, w_data):
    # vector from the origin to the current window's workspace (flipped y-axis)
    curr_pos = get_current(ws_size)
    w_pos = get_relativewinpos(ws_size, w_data)
    return (curr_pos[0]+w_pos[0], curr_pos[1]+w_pos[1])

def wm_class(w_id):
    # get the WM_CLASS of new windows
    return subprocess.check_output(
        ["xprop", "-id", w_id.strip(), "WM_CLASS"]
        ).decode("utf-8").split("=")[-1].strip()

ws_size = get_wssize()
wlist1 = []
subprocess.Popen(["notify-send", 'workspace lock is running for '+app_class])

while True:
    # check focussed window ('except' for errors during "wild" workspace change)
    try:
        focus = subprocess.check_output(
            ["xdotool", "getwindowfocus"]
            ).decode("utf-8")
    except subprocess.CalledProcessError:
        pass
    time.sleep(1)
    wdata = check_wlist() 
    if wdata !=  None:
        # compare existing window- ids, checking for new ones
        wlist2 = wdata[1]
        if wlist2 != wlist1:
            # if so, check the new window's class
            newlist = [[w, wm_class(w)] for w in wlist2 if not w in wlist1]
            valids = sum([[l for l in wdata[0] if l[0] == w[0]] \
                          for w in newlist if app_class in w[1]], [])
            # for matching windows, check if they need to be moved (check workspace)
            for w in valids:
                abspos = list(get_abswindowpos(ws_size, w))
                if not abspos == ws_lock:
                    current = get_current(ws_size)
                    move = (
                        (ws_lock[0]-current[0])*ws_size[0],
                            (ws_lock[1]-current[1])*ws_size[1]-56
                        )
                    new_w = "wmctrl -ir "+w[0]+" -e "+(",").join(
                        ["0", str(int(w[2])+move[0]),
                         str(int(w[2])+move[1]), w[4], w[5]]
                        )
                    subprocess.call(["/bin/bash", "-c", new_w])
                    # re- focus on the window that was focussed
                    if not app_class in wm_class(focus):
                        subprocess.Popen(["wmctrl", "-ia", focus])
        wlist1 = wlist2

How to use

  1. The script needs both wmctrl and xdotool:

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

  3. Of your specific application, find out the WM_CLASS: open your application, run in a terminal:

    xprop WM_CLASS and click on the window of the application
    

    The output will look like (in your case):

    WM_CLASS: WM_CLASS(STRING) = "sun-awt-X11-XFramePeer", "MATLAB R2015a - academic use"
    

    Either use the first or the second part in the command to run the script.

  4. The command to run the script then is:

    python3 /path/to/lock_towspace.py "sun-awt-X11-XFramePeer" 2,2
    

    In the command, the last section; 2,2 is the workspace where you want to lock the application to (without spaces: (!) column, row), in "human" format; the first column/row is 1,1

  5. Test the script by running it. While running, open your application and let it produce windows as usual. All windows should appear on the targeted workspace, as set in the command.

OUTDATED ANSWER:

(second) TEST VERSION

The script below locks a specific application to its initial workspace. If the script is started, it determines on which workspace the application resides. All additional windows the application produces will be moved to the same workspace in a split second.

The focus issue is solved by automatically re- focussing on the window that was focussed before the additional window was produced.

The script

#!/usr/bin/env python3
import subprocess
import time
import math

app_class = '"gedit", "Gedit"'

def get_wssize():
    # get workspace size
    resdata = subprocess.check_output(["xrandr"]).decode("utf-8").split()
    i = resdata.index("current")
    return [int(n) for n in [resdata[i+1], resdata[i+3].replace(",", "")]]

def get_current(ws_size):
    # get vector of the current workspace to the origin of the spanning desktop (flipped y-axis)
    dt_data = subprocess.check_output(["wmctrl", "-d"]).decode("utf-8").split(); curr = [int(n) for n in dt_data[5].split(",")]
    return (int(curr[0]/ws_size[0]), int(curr[1]/ws_size[1]))

def get_relativewinpos(ws_size, w_data):
    # vector to the application window, relative to the current workspace
    xw = ws_size[0]; yw = ws_size[1]
    return (math.ceil((w_data[1]-xw)/xw), math.ceil((w_data[2]-yw)/yw))

def get_abswindowpos(ws_size, w_data):
    curr_pos = get_current(ws_size)
    w_pos = get_relativewinpos(ws_size, w_data)
    return (curr_pos[0]+w_pos[0], curr_pos[1]+w_pos[1])

def wm_class(w_id):
    return subprocess.check_output(["xprop", "-id", w_id, "WM_CLASS"]).decode("utf-8").split("=")[-1].strip()

def filter_windows(app_class):
    # find windows (id, x_pos, y_pos) of app_class
    try:
        raw_list = [l.split() for l in subprocess.check_output(["wmctrl", "-lG"]).decode("utf-8").splitlines()]
        return [(l[0], int(l[2]), int(l[3]), l[4], l[5]) for l in raw_list if wm_class(l[0]) == app_class]
    except subprocess.CalledProcessError:
        pass

ws_size = get_wssize()
init_window = get_abswindowpos(ws_size, filter_windows(app_class)[0])
valid_windows1 = filter_windows(app_class)

while True:
    focus = subprocess.check_output(["xdotool", "getwindowfocus"]).decode("utf-8")
    time.sleep(1)
    valid_windows2 = filter_windows(app_class)
    if all([valid_windows2 != None, valid_windows2 != valid_windows1]):
        for t in [t for t in valid_windows2 if not t[0] in [w[0] for w in valid_windows1]]:
            absolute = get_abswindowpos(ws_size, t)
            if not absolute == init_window:
                current = get_current(ws_size)
                move = ((init_window[0]-current[0])*ws_size[0], (init_window[1]-current[1])*ws_size[1]-56)
                new_w = "wmctrl -ir "+t[0]+" -e "+(",").join(["0", str(t[1]+move[0]), str(t[2]+move[1]), t[3], t[4]])
                subprocess.call(["/bin/bash", "-c", new_w])
            focus = str(hex(int(focus)))
            z = 10-len(focus); focus = focus[:2]+z*"0"+focus[2:]
            if not wm_class(focus) == app_class:
                subprocess.Popen(["wmctrl", "-ia", focus])
        valid_windows1 = valid_windows2

How to use

  1. The script needs both wmctrland xdotool

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

  3. determine your application's `WM_CLASS' by opening the application, then open a terminal and run the command:

    xprop WM_CLASS
    

    Then click on your application's window. Copy the output, looking like "sun-awt-X11-XFramePeer", "MATLAB R2015a - academic use" in your case, and place it between single quotes in the head section of the script, as indicated.

  4. Run the script with the command:

    python3 /path/to/keep_workspace.py
    

If it works as you like, I'll add a toggle function. Although it works already for a few hours on my system, bu it might need some tweaking first however.

Notes

Although you should not notice it, the script does add some processor load to the system. On my elderly system I noticed an increase of 3-10%. If you like how it works, I will probably further tweak it to reduce the load.

The script, as it is, assumes the secundary windows are of the same class as the main window, like you indicated in a comment. With a (very) simple change, the secondary windows can be of another class however.

Explanation

Although probably not very interesting for an average reader, the script works by calculating in vectors. On startup, the script calculates:

  • the vector from the origin to the current workspace with the output of wmctrl -d
  • the vector to the application's window, relative to the current workspace, by the output of wmctrl -lG
  • From these two, the script calculates the absolute position of the application's window on the spanning desktop (all workspaces in one matrix)

From then on, the script looks for new windows of the same application, with the output of xprop WM_CLASS, looks up their position in the same way as above and moves them to the "original" workspace.

Since the newly created window "stole" the focus from the last used window the user was working on, the focus is subsequently set to the window that had focus before.

Jacob Vlijm
  • 83,767
  • This is very awsome. It might be a good idea to create a indicator where the user can lock different application into workspaces. Right now I had the problem with Matlab, but the same problem will occur with matplotlib – Mokus Aug 30 '15 at 13:44
  • @OHLÁLÁ as mentioned, I find the question very interesting and will keep working on it. What I have in mind is a file in which the user can set application and workspace -sets. If you run into possible bugs, please mention it! – Jacob Vlijm Aug 30 '15 at 13:46
  • What will be the behaviour when two Matlab is started on separate workspaces? – Mokus Aug 30 '15 at 13:58
  • @OHLÁLÁ then they both will be locked to the workspace you set in the command. Since their WM_CLASS is identical, the second one will be moved to the one you set in the command. – Jacob Vlijm Aug 30 '15 at 14:01
  • Are there other posibilities to identify an application, other than WM_CLASS? – Mokus Aug 30 '15 at 14:03
  • @OHLÁLÁ their pid, their process name. All will be in one I am afraid. I am not that familiar with Matlab (Not much more than I know what it does),. I understand you want to run two (or more) instances of Matlab and pin their plot windows to a specific workspace? Not sure if it can be identified easily, but with a working situation, could you post somewhere the output of pstree? Also, do the output windows have a specific name? It would be an easy shot if that is the case :). – Jacob Vlijm Aug 30 '15 at 14:08
  • Here is the output: http://paste.ofcode.org/36f9KQiGjGFDjArcS3NGzPu – Mokus Aug 30 '15 at 14:12
  • @OHLÁLÁ I am afraid we do not have other options to distinguish different instances of one application than: - (part of) window name, - window size, - order of window creation. In short: window properties. Is there any identifying property in that? – Jacob Vlijm Aug 30 '15 at 15:09
0

The issue, I believe, was resolved years ago, but I can't refrain from being a bit ironic. Jacob's answer is extremely helpful and generous; the python script is super useful and I really appreciate his work on the answer. But within the question it is a complete overkill, because the easiest solution is to tell Matlab that you want to create the plot without actual drawing. This is done by setting the value figure object's property 'Visible' to 'off' when creating the figure with a plot in Matlab… For example:

% this  simply opens a pop-up window with an empty figure
figure(); 
% then different stuff is done like plotting, setting axes, lines widths adjusting, setting colors, etc.

%%%%% % In this case the Matlab Editor won't show the figure in a window. All graphical job is done virtually

figure('Visible', 'off');

% alternatively, fig = figure(); fig.Visible = 'off';

% or by using a set() function set(fig, 'Visible', 'off');

The figures could be saved as images of the common type (i.e. jpeg). If saving is not needed, this Visible property could be set back to default fig.Visible = 'on'; when the code runs to the end and the figure will appear. But in case when a bunch of pictures needs to be produced in a batch, this is the only way to use your computer while the code is running.