5

I only want to execute a certain action with xdotool if the window is not visible to the user, this includes minimized windows, but also windows which are 100% covered by other windows (if no transparency is used at least). Ignoring the transparency issue is there a simple way to do this?

xdotool has a --onlyvisible option, but that only includes minimized windows, not covered windows. Of course there is the option of cycling through all visible windows, getting their window geometries and calculating how much of the window of interest they cover, but I do hope there is an easier and faster solution than doing this in bash.

Here is a nice illustration of the problem, but there it is only about listing windows, also it is for Max OS X. This question only has an answer which hints, but not shows how to do it by listing all visible windows with their respective z-order and calculating the visible area manually.

5

For completeness sake here is the naive / brute-force solution which I hoped is already implemented in some other utility. The fullyobscured notify-event in @Gilles link in the comment sounds very promising, but I wasn't sure how to get it to work, and this solution was also quite fun to implement.

The script simply calculates the coverage area of all overlapping window subtracting double counted areas and checks if it is as large as the window area. Because it correctly includes the frame borders the code looks a bit more complex than it could. It returns exit code 0 if fully covered and 1 if not. It takes a window ID as an argument. E.g. call it with if xcovered 0x1a00003; then echo 'covered!'; fi

Not counting the comments, debug comments and error-checking it could be only 40 lines long, surely even less. I actually wanted to use bc instead of python, but I couldn't find an easy way to transfer a bash array to a bc array.

#!/bin/bash
# Name: xcovered.sh
# Find out if C is completely or only partially covered by A or B
#  +-----------+
#  |   +===+   |
#  |   | +-------+
#  | C | |   B   |
#  |   | +-------+
#  +---| A |---+
#      +---+
# @return 0 if window ist not visible, 1 if visible
# Note: Only tested with three windows like in sketch above, but
#       it should also work for an arbitrary amount of overlapping windwows
wid=$1
if ! xwininfo -id $wid -stats | 'grep' -q 'IsViewable'; then return 0; fi

# get all stacked window ids after and including the given wid
wids=($(xprop -root | 'sed' -nE "/_NET_CLIENT_LIST_STACKING\(WINDOW\)/{ s|.*($wid)|\1|; s|,||g; p }"))
if [ ${#wids} -eq 0 ]; then
    echo -e "\e[31mCouldn't find specified window id $wid in _NET_CLIENT_LIST_STACKING(WINDOW)"'!'"\e[0m"
    return 2
fi
if [ ${#wids} -eq 1 ]; then return 0; fi

# Gather geometry of all windows in higher zorder / possibly lying on top
coords=(); frames=()
for owid in ${wids[@]}; do
    #xwininfo -id $owid | grep xwininfo
    if xwininfo -id $owid -stats | 'grep' -q 'IsViewable'; then
        # _NET_WM_ICON_GEOMETRY doesn't exist for xfce4-panel, thereby making this more difficult
        #coords=$(xprop -id $owid _NET_WM_ICON_GEOMETRY)
        #frames=$(xprop -id $owid _NET_FRAME_EXTENTS)
        x=($(xwininfo -id $owid -stats -wm | sed -nE '
            s|^[ \t]*Absolute upper-left X:[ \t]*([0-9]+).*|\1|Ip;
            s|^[ \t]*Absolute upper-left Y:[ \t]*([0-9]+).*|\1|Ip;
            s|^[ \t]*Width:[ \t]*([0-9]+).*|\1|Ip;
            s|^[ \t]*Height:[ \t]*([0-9]+).*|\1|Ip;
            /Frame extents:/I{ s|^[ \t}Frame Extents:[ \t]*||I; s|,||g; p; };
        ' | sed ':a; N; $!b a; s/\n/ /g '))
        if [ ! ${#x[@]} -eq 8 ]; then
            echo -e "\e[31mSomething went wrong when parsing the output of 'xwininfo -id $owid -stats -wm':\e[0m"
            xwininfo -id $owid -stats -wm
            exit 1
        fi
        # apply the frame width to the coordinates and window width
        # 0:x 1:y 2:w 3:h, border widths 4:left 5:right 6:top 7:bottom
        coords+=( "${x[0]}-${x[4]}, ${x[1]}-${x[6]}, ${x[2]}+${x[4]}+${x[5]}, ${x[3]}+${x[6]}+${x[7]}" )
    fi
done

IFS=','; python - <<EOF #| python
# Calculates the area of the union of all overlapping areas. If that area
# is equal to the window of interest area / size, then the window is covered.
# Note that the calcualted area can't be larger than that!
#   1
# D---C      => overlap given by H and B
# | H-|---G    x-overlap: max(0, xleft2-xright1)
# A---B   |         -> '1' and '2' is not known, that's why for left and right
#   |  2  |            use min, each
#   E-----F         -> max(0, min(xright1,xright2) - max(xleft1,xleft2) )
#                      Note that because of xleft<xright this can only
#                      result in xright1-xleft2 or xright2-xleft1
# All cases: 1 |     +--+ |   +--+ | +--+   | +--+      |
#            2 | +--+     | +--+   |   +--+ |      +--+ |
#      overlap |    0     |    2   |    2   |     0     |
def overlap( x1,y1,w1,h1, x2,y2,w2,h2, x3=0,y3=0,w3=65535,h3=65535 ):
    return max( 0, min(x1+w1,x2+w2,x3+w3) - max(x1,x2,x3) ) * \
           max( 0, min(y1+h1,y2+h2,y3+h3) - max(y1,y2,y3) )
x=[ ${coords[*]} ]
area=0
# Calculate overlap with window in question
# 0:x 1:y 2:w 3:h, border widths 0:left 1:right 2:top 3:bottom
for i in range( 4,len(x),4 ):
    area += overlap( *( x[0:4]+x[i:i+4] ) )

# subtract double counted areas i.e. areas overlapping to the window
# of interest and two other windows on top ... This is n**2
for i in range( 4,len(x),4 ):
    for j in range( i+4,len(x),4 ):
        area -= overlap( *( x[0:4]+x[i:i+4]+x[j:j+4] ) )

print "area =",area
print "woi  =",x[2]*x[3]
# exit code 0: if not fully covered, 1: if fully covered
exit( area < x[2]*x[3] )
EOF
exit $?
| improve this answer | |
  • 2
    Beware that this doesn't take non-rectangular windows into account. Those are admittedly rare. Does this correctly report windows visible only on another desktop or entirely outside the screen area as invisible? – Gilles 'SO- stop being evil' May 6 '16 at 0:50
  • 1
    I used your code to make it a Python module. Maybe you like to check it out: github.com/ikem-krueger/sleep-walker/blob/develop/src/usr/lib/… – Ikem Krueger Sep 15 '17 at 23:44
  • @user82110 Oh that's cool, thanks for the link! – mxmlnkn Sep 16 '17 at 22:18
  • 1
    Note that the "covered area" calculation here is not correct when there are more than three windows on top of the target window. See my solution here. – user12173 Sep 8 '19 at 3:21
2

The answer given by @mxmlnkn is a great start, but unfortunately the methodology for calculating the covered area is not correct for when 3 or more windows are on top of the target window.

To see why, imagine that there are 3 windows (which we'll call X, Y, and Z) on top of a target window T); further suppose that all these windows have the same coordinates. Then the given solution would first add |X ∩ T|+|Y ∩ T|+|Z ∩ T|=3*windowArea, and then subtract |(X U Y) ∩ T| + |(X U Z) ∩ T| + |(Y U Z) ∩ T| =3*windowArea, resulting in a net area calculation of 0. The error here is we are actually "double counting" the attempt to compensate for "double counting". To correct this, we add |X ∩ Y ∩ Z ∩ T|. This notion is formalized with the Principle of Inclusion-Exclusion (see here).

The "covered area" can be defined as (forgive the haphazard mathematical notation, unix.stackexchange.com does not allow LaTeX)

(A_1 U A_2 U ... U A_n) ∩ B

where A_1, A_2, ..., A_n are the windows that lie on top of the target window, and B is the target window.

We can use the Principle of Inclusion-Exclusion to expand (A_1 U A_2 U ... U A_n). We can then distribute the intersection with B across this result.

Concretely, this results in the following algorithm (C++):

bool windowIsVisible(Display *display, Window window, float threshold) {
  // Indicates whether a window is fully covered
  if (!windowIsViewable(display, window)) {
    return false;
  }

  auto rootWindow = DefaultRootWindow(display);
  auto coords = getWindowCoords(display, rootWindow, window);

  if (coords.size() <= 1) {
    return true;
  }

  float area = (coords[0][2]-coords[0][0]) * (coords[0][3]-coords[0][1]);
  float coveredArea = 0;

  auto selector = std::vector<bool>(coords.size()-1);
  for (int i = 0; i < selector.size(); i++) {
    std::fill(selector.begin(), selector.begin()+i+1, true);
    std::fill(selector.begin()+i+1, selector.end(), false);

    auto selectedWindows = std::vector<std::vector<int>>(i);
    do {
      selectedWindows.clear();
      for (int j = 0; j < selector.size(); j++) {
        if (selector[j]) selectedWindows.push_back(coords[j+1]);
      }
      selectedWindows.push_back(coords[0]);
      coveredArea += pow(-1, i)*calculateWindowOverlap(selectedWindows);
    } while (std::prev_permutation(selector.begin(), selector.end()));
  }

  float tol = 1e-4;
  return (1 - ((float)coveredArea)/((float)area) + tol) >= threshold;
}

int calculateWindowOverlap(std::vector<std::vector<int>> windowCoords) {
  if (windowCoords.size() == 0) {
    return 0;
  }

  std::vector<int> intersect = windowCoords[0];
  for (int i = 1; i < windowCoords.size(); i++) {
    intersect[0] = std::max(intersect[0], windowCoords[i][0]);
    intersect[1] = std::max(intersect[1], windowCoords[i][1]);
    intersect[2] = std::min(intersect[2], windowCoords[i][2]);
    intersect[3] = std::min(intersect[3], windowCoords[i][3]);
  }
  return std::max(0, intersect[2]-intersect[0]) *
    std::max(0, intersect[3]-intersect[1]);
}

std::vector<std::vector<int>> getWindowCoords(Display *display,
  Window queryWindow, Window targetWindow,
  bool *reachedTargetPtr = nullptr, int absX = 0, int absY = 0) {
  // Gather geometry of all windows in higher zorder
  std::vector<std::vector<int>> coords = {};

  bool reachedTarget = false;
  if (!reachedTargetPtr) {
    reachedTargetPtr = &reachedTarget;
  }

  Window rWindow;
  Window parentWindow;
  Window *childrenWindows;
  unsigned int numChildren;
  XQueryTree(display, queryWindow, &rWindow, &parentWindow,
    &childrenWindows, &numChildren);

  for (int i = 0; i < numChildren; i++) {
    if (childrenWindows[i] == targetWindow) {
      *reachedTargetPtr = true;
    }

    XWindowAttributes windowAttributes;
    XGetWindowAttributes(display, childrenWindows[i], &windowAttributes);
    if (*reachedTargetPtr && windowAttributes.map_state == IsViewable &&
      windowAttributes.c_class != InputOnly) {
      coords.push_back(std::vector<int> {
        windowAttributes.x + absX,
        windowAttributes.y + absY,
        windowAttributes.x + absX + windowAttributes.width,
        windowAttributes.y + absY + windowAttributes.height });
    }

    if (childrenWindows[i] != targetWindow) {
      auto childCoords = getWindowCoords(display, childrenWindows[i],
        targetWindow, reachedTargetPtr, absX + windowAttributes.x,
        absY + windowAttributes.y);

      coords.reserve(coords.size() + childCoords.size());
      coords.insert(coords.end(), childCoords.begin(), childCoords.end());
    }
  }

  return coords;
}

Basically, for k=1,2,...,n, we find all combinations of n choose k. We then calculate the area of the intersections of these windows, along with the target window, and add/subtract that result from the running area (in accordance with the (-1)^(k-1) term from the Principle of Inclusion-Exclusion.

I have implemented this in a simple tool I made here. Also, this is essentially an extension of the Rectangle Area II problem from leetcode. There are some more efficient ways to do this (check the solutions section), but I personally found that the mathematically intuitive way achieved adequate performance.

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.