Skip to content

Commit

Permalink
Don't allow struts to reserve entire monitors
Browse files Browse the repository at this point in the history
(Assume they're trying to reserve an internal edge instead, since this is
how Unity does it)

Closes #45
  • Loading branch information
Stephan Sokolow authored and Stephan Sokolow committed Jan 27, 2020
1 parent 60dadf2 commit a3ad59e
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 30 deletions.
84 changes: 67 additions & 17 deletions quicktile/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import math, sys
from collections import namedtuple
from enum import Enum
from enum import Enum, IntEnum, unique
from itertools import chain, combinations

import gi
Expand All @@ -37,6 +37,21 @@
# TODO: Re-add log.debug() calls in strategic places


@unique
class Edge(IntEnum):
"""Constants used by :meth:`StrutPartial.as_rects` to communicate
information :class:`UsableRegion` needs to properly handle panel
reservations on interior edges.
The values of the enum's members correspond to the tuple indexes in
StrutPartial.
"""
LEFT = 1
RIGHT = 2
TOP = 3
BOTTOM = 4


class Gravity(Enum): # pylint: disable=too-few-public-methods
"""Gravity definitions used by :class:`Rectangle`"""
TOP_LEFT = (0.0, 0.0)
Expand Down Expand Up @@ -250,7 +265,8 @@ def __new__(cls, left=0, right=0, top=0, bottom=0, # pylint: disable=R0913
left_start_y, left_end_y, right_start_y, right_end_y,
top_start_x, top_end_x, bottom_start_x, bottom_end_x)

def as_rects(self, desktop_rect: 'Rectangle') -> 'List[Rectangle]':
def as_rects(self, desktop_rect: 'Rectangle'
) -> 'List[Tuple[Edge, Rectangle]]':
"""Resolve self into absolute coordinates relative to ``desktop_rect``
Note that struts are relative to the bounding box of the whole desktop,
Expand All @@ -264,30 +280,30 @@ def as_rects(self, desktop_rect: 'Rectangle') -> 'List[Rectangle]':
"""
return [x for x in (
# Left
Rectangle(
(Edge.LEFT, Rectangle(
x=desktop_rect.x,
y=self.left_start_y,
width=self.left,
y2=self.left_end_y).intersect(desktop_rect),
y2=self.left_end_y).intersect(desktop_rect)),
# Right
Rectangle(
(Edge.RIGHT, Rectangle(
x=desktop_rect.x2,
y=self.right_start_y,
width=-self.right,
y2=self.right_end_y).intersect(desktop_rect),
y2=self.right_end_y).intersect(desktop_rect)),
# Top
Rectangle(
(Edge.TOP, Rectangle(
x=self.top_start_x,
y=desktop_rect.y,
x2=self.top_end_x,
height=self.top).intersect(desktop_rect),
height=self.top).intersect(desktop_rect)),
# Bottom
Rectangle(
(Edge.BOTTOM, Rectangle(
x=self.bottom_start_x,
y=desktop_rect.y2,
x2=self.bottom_end_x,
height=-self.bottom).intersect(desktop_rect),
) if bool(x)]
height=-self.bottom).intersect(desktop_rect)),
) if bool(x[1])]

# Keep _StrutPartial from showing up in automated documentation
del _StrutPartial
Expand Down Expand Up @@ -792,9 +808,48 @@ def _update(self):
strut_rects = [] # type: List[Rectangle]
for strut in self._struts:
# TODO: Test for off-by-one bugs
strut_rects.extend(strut.as_rects(desktop_rect))
# TODO: Think of a more efficient way to do this
for strut_pair in strut.as_rects(desktop_rect):
strut_rects.append(self._trim_strut(strut_pair))
self._strut_rects = strut_rects

def _trim_strut(self, strut: Tuple[Edge, Rectangle]) -> Rectangle:
"""Trim a strut rectangle to just the monitor it applies to"""
edge, strut_rect = strut

for monitor in self._monitors:
overlap = monitor.intersect(strut_rect)
if not bool(overlap):
continue

# Gotta do this manually unless I decide to add support for
# subtract taking a directional hint so it doesn't chop off the
# wrong end.
if overlap.width == monitor.width:
if edge == Edge.LEFT:
strut_rect = Rectangle(x=monitor.x2, y=strut_rect.y,
width=strut_rect.x2 - monitor.x2,
height=strut_rect.height
)
elif edge == Edge.RIGHT:
strut_rect = Rectangle(x=strut_rect.x, y=strut_rect.y,
width=monitor.x - strut_rect.x,
height=strut_rect.height
)
if overlap.height == monitor.height:
if edge == Edge.TOP:
strut_rect = Rectangle(x=strut_rect.x, y=monitor.y2,
width=strut_rect.width,
height=strut_rect.y2 - monitor.y2,
)
elif edge == Edge.BOTTOM:
strut_rect = Rectangle(x=strut_rect.x, y=strut_rect.y,
height=monitor.y - strut_rect.y,
width=strut_rect.width
)

return strut_rect

def clip_to_usable_region(self, rect: Rectangle) -> Optional[Rectangle]:
"""Given a rectangle, return a copy that has been shrunk to fit inside
the usable region of the monitor.
Expand All @@ -818,14 +873,9 @@ def clip_to_usable_region(self, rect: Rectangle) -> Optional[Rectangle]:
if not monitor:
return None

print("---")
print(rect, 'V', monitor)

rect = rect.intersect(monitor)
for panel in self._strut_rects:
print('=', rect, '-', panel)
rect = rect.subtract(panel)
print('=', rect, bool(rect))

# Apparently MyPy can't see through custom __bool__ implementations
return rect or None # type: ignore
Expand Down
30 changes: 17 additions & 13 deletions test_quicktile.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import logging, unittest

# from quicktile import commands
from quicktile.util import (clamp_idx, euclidean_dist, powerset, Gravity,
from quicktile.util import (clamp_idx, euclidean_dist, powerset, Edge, Gravity,
Rectangle, StrutPartial, UsableRegion, XInitError)

# Ensure code coverage counts modules not yet imported by tests.
Expand Down Expand Up @@ -170,37 +170,41 @@ def test_as_rects(self):
print("Desktop Rectangle: ", dtop_rect, " | Strut: ", strut)
self.assertEqual(strut.as_rects(dtop_rect), [x for x in (
# Left
Rectangle(x=dtop_rect.x, y=strut.left_start_y,
(Edge.LEFT, Rectangle(x=dtop_rect.x, y=strut.left_start_y,
width=strut.left, y2=strut.left_end_y
).intersect(dtop_rect),
).intersect(dtop_rect)),
# Right
Rectangle(x=dtop_rect.x2, y=strut.right_start_y,
(Edge.RIGHT, Rectangle(
x=dtop_rect.x2, y=strut.right_start_y,
width=-strut.right, y2=strut.right_end_y
).intersect(dtop_rect),
).intersect(dtop_rect)),
# Top
Rectangle(x=strut.top_start_x, y=dtop_rect.y,
(Edge.TOP, Rectangle(x=strut.top_start_x, y=dtop_rect.y,
x2=strut.top_end_x, height=strut.top
).intersect(dtop_rect),
).intersect(dtop_rect)),
# Bottom
Rectangle(x=strut.bottom_start_x, y=dtop_rect.y2,
(Edge.BOTTOM, Rectangle(
x=strut.bottom_start_x, y=dtop_rect.y2,
x2=strut.bottom_end_x, height=-strut.bottom
).intersect(dtop_rect)) if x])
).intersect(dtop_rect))) if x[1]])

def test_as_rects_pruning(self):
"""StrutPartial: as_rects doesn't return empty rects"""
dtop_rect = Rectangle(1, 2, 30, 40)
self.assertEqual(
StrutPartial(5, 0, 0, 0).as_rects(dtop_rect),
[Rectangle(x=1, y=2, width=5, height=40)])
[(Edge.LEFT, Rectangle(x=1, y=2, width=5, height=40))])
self.assertEqual(
StrutPartial(0, 6, 0, 0).as_rects(dtop_rect),
[Rectangle(x=dtop_rect.x2 - 6, y=2, width=6, height=40)])
[(Edge.RIGHT, Rectangle(
x=dtop_rect.x2 - 6, y=2, width=6, height=40))])
self.assertEqual(
StrutPartial(0, 0, 7, 0).as_rects(dtop_rect),
[Rectangle(x=1, y=2, width=30, height=7)])
[(Edge.TOP, Rectangle(x=1, y=2, width=30, height=7))])
self.assertEqual(
StrutPartial(0, 0, 0, 8).as_rects(dtop_rect),
[Rectangle(x=1, y=dtop_rect.y2 - 8, width=30, height=8)])
[(Edge.BOTTOM, Rectangle(
x=1, y=dtop_rect.y2 - 8, width=30, height=8))])


class TestRectangle(unittest.TestCase): # pylint: disable=R0904
Expand Down

0 comments on commit a3ad59e

Please sign in to comment.