diff --git a/src/ewmh_m2m/__main__.py b/src/ewmh_m2m/__main__.py index ce8ab7e..5ff62ff 100755 --- a/src/ewmh_m2m/__main__.py +++ b/src/ewmh_m2m/__main__.py @@ -54,9 +54,9 @@ def main(): arg_parser.add_argument( "--direction", "-d", action="store", - type=Ordinal.get, - choices=list(Ordinal), - default=Ordinal.NEXT.name.capitalize(), + type=Ordinal.__getitem__, + choices=[o.name for o in sorted(Ordinal, key=lambda o: o.value)], + default=Ordinal.EAST_SOUTHEAST.name, help="Direction in which to move the window (default: %(default)s)") arg_parser.add_argument("--no-wrap", "-W", action="store_true", help="Do not go back if no screen found.") diff --git a/src/ewmh_m2m/geometry.py b/src/ewmh_m2m/geometry.py index 706fbf8..e37c33d 100644 --- a/src/ewmh_m2m/geometry.py +++ b/src/ewmh_m2m/geometry.py @@ -1,3 +1,8 @@ +import math +import typing +from ewmh_m2m.ordinal import Ordinal + + class Geometry: """Data class to manipulate rectangles defined as (x, y, w, h)""" def __init__(self, x: float = 0, y: float = 0, w: float = 0, h: float = 0): @@ -39,6 +44,26 @@ def vertically_overlap(self, other) -> bool: def overlap(self, other) -> bool: return self.horizontally_overlap(other) and self.vertically_overlap(other) + def directions_to(self, other: "Geometry") -> typing.Collection[Ordinal]: + result = set(Ordinal) + if self.horizontally_overlap(other): + result &= {o for o in Ordinal if abs(o.sin) < 0.5} + elif self.vertically_overlap(other): + result &= {o for o in Ordinal if abs(o.cos) < 0.5} + else: + result -= {o for o in Ordinal if o.value % 90 == 0} + + if other.x > self.x: + result &= {o for o in Ordinal if o.cos >= 0 } + if other.y < self.y: + result &= {o for o in Ordinal if o.sin >= 0 } + if other.x < self.x: + result &= {o for o in Ordinal if o.cos <= 0 } + if other.y > self.y: + result &= {o for o in Ordinal if o.sin <= 0 } + + return result + def __eq__(self, other): return list(self) == list(other) diff --git a/src/ewmh_m2m/ordinal.py b/src/ewmh_m2m/ordinal.py index fb352d9..9d5e94b 100644 --- a/src/ewmh_m2m/ordinal.py +++ b/src/ewmh_m2m/ordinal.py @@ -1,44 +1,32 @@ from enum import Enum +import math +_HEXADAN = 360 / 16 class Ordinal(Enum): - NORTH = 0 - EAST = 1 - SOUTH = 2 - WEST = 3 - NEXT = 4 - PREV = 5 + EAST = 0 * _HEXADAN + NORTH = 4 * _HEXADAN + WEST = 8 * _HEXADAN + SOUTH = 12 * _HEXADAN - @classmethod - def get(cls, v: str): - if v.upper()[0] == 'E': - return Ordinal.EAST - if v.upper()[0] == 'W': - return Ordinal.WEST - if v.upper()[0:2] == 'NO': - return Ordinal.NORTH - if v.upper()[0] == 'S': - return Ordinal.SOUTH - if v.upper()[0:2] == 'NE': - return Ordinal.NEXT - if v.upper()[0] == 'P': - return Ordinal.PREV - raise TypeError("No direction match with '{}'".format(v)) + NORTHEAST = 2 * _HEXADAN + NORTHWEST = 6 * _HEXADAN + SOUTHWEST = 10 * _HEXADAN + SOUTHEAST = 14 * _HEXADAN - def __str__(self): - return self.name + EAST_NORTHEAST = 1 * _HEXADAN + NORTH_NORTHEAST = 3 * _HEXADAN + NORTH_NORTHWEST = 5 * _HEXADAN + WEST_NORTHWEST = 7 * _HEXADAN + WEST_SOUTHWEST = 9 * _HEXADAN + SOUTH_SOUTHWEST = 11 * _HEXADAN + SOUTH_SOUTHEAST = 13 * _HEXADAN + EAST_SOUTHEAST = 15 * _HEXADAN + + def __init__(self, value) -> None: + self.sin = round(math.sin(math.radians(value)), 6) + self.cos = round(math.cos(math.radians(value)), 6) @property def opposite(self): - if self is Ordinal.NORTH: - return Ordinal.SOUTH - if self is Ordinal.SOUTH: - return Ordinal.NORTH - if self is Ordinal.EAST: - return Ordinal.WEST - if self is Ordinal.WEST: - return Ordinal.EAST - if self is Ordinal.NEXT: - return Ordinal.PREV - if self is Ordinal.PREV: - return Ordinal.NEXT + return Ordinal((self.value + 180) % 360) diff --git a/src/ewmh_m2m/screen.py b/src/ewmh_m2m/screen.py index 23f1102..6adf4da 100644 --- a/src/ewmh_m2m/screen.py +++ b/src/ewmh_m2m/screen.py @@ -1,3 +1,4 @@ +import math from typing import Set, Iterable, Dict, List, Optional import xpybutil.xinerama @@ -17,20 +18,18 @@ def get_sibling_screens(current: Geometry, screens: Iterable[Geometry]) -> Dict[ Each list is ordered from the nearest screen to the furthest one. """ - horizontal_screens = [g for g in screens if current.horizontally_overlap(g)] - vertical_screens = [g for g in screens if current.vertically_overlap(g)] - - screens_list = list(screens) - current_index = screens_list.index(current) - ordered_screens = screens_list[:current_index] + screens_list[current_index + 1:] if (current_index > 0) else screens_list[1:] - + directions = [ + (g, current.directions_to(g)) for g in screens if g != current + ] return { - Ordinal.SOUTH: sorted([g for g in vertical_screens if g.y > current.y], key=lambda g: g.y), - Ordinal.NORTH: sorted([g for g in vertical_screens if g.y < current.y], key=lambda g: -1 * g.y), - Ordinal.EAST: sorted([g for g in horizontal_screens if g.x > current.x], key=lambda g: g.x), - Ordinal.WEST: sorted([g for g in horizontal_screens if g.x < current.x], key=lambda g: -1 * g.x), - Ordinal.NEXT: ordered_screens, - Ordinal.PREV: ordered_screens.reverse() + o: sorted( + [g for g, dirs in directions if o in dirs], + key=lambda g: ( + math.hypot(g.x - current.x, g.y - current.y), + math.copysign(g.x, o.cos) if abs(o.cos) > abs(o.sin) else math.copysign(g.y, o.sin) + ) + ) + for o in Ordinal } @@ -43,4 +42,3 @@ def get_sibling_screen(siblings: Dict[Ordinal, List[Geometry]], if not no_wrap and siblings[direction.opposite]: return siblings[direction.opposite][-1] return None - diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 3b058b6..2ff9811 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1,4 +1,5 @@ from ewmh_m2m.geometry import Geometry +from ewmh_m2m.ordinal import Ordinal class TestGeometry: @@ -47,3 +48,15 @@ def test_overlap(self): assert g1.overlap(g2) assert g2.overlap(g1) + + def test_directions_aligned(self): + g1 = Geometry(0, 0, 1, 1) + g2 = Geometry(1, 0, 1, 1) + + assert g1.directions_to(g2) == {Ordinal.EAST, Ordinal.EAST_NORTHEAST, Ordinal.EAST_SOUTHEAST} + + def test_directions_not_aligned(self): + g1 = Geometry(0, 0, 1, 1) + g2 = Geometry(1, 1, 1, 1) + + assert g1.directions_to(g2) == {Ordinal.SOUTH_SOUTHEAST, Ordinal.SOUTHEAST, Ordinal.EAST_SOUTHEAST} diff --git a/tests/test_screen.py b/tests/test_screen.py index 9ff1104..56a1826 100644 --- a/tests/test_screen.py +++ b/tests/test_screen.py @@ -32,11 +32,8 @@ def test_siblings_horizontal(self): siblings = ewmh_m2m.screen.get_sibling_screens(current, screens) - assert siblings == { - Ordinal.SOUTH: [], Ordinal.NORTH: [], - Ordinal.EAST: [Geometry(30, 0, 10, 10), Geometry(40, 0, 10, 10)], - Ordinal.WEST: [Geometry(10, 0, 10, 10), Geometry(0, 0, 10, 10)] - } + assert siblings[Ordinal.EAST] == [Geometry(30, 0, 10, 10), Geometry(40, 0, 10, 10)] + assert siblings[Ordinal.WEST] == [Geometry(10, 0, 10, 10), Geometry(0, 0, 10, 10)] def test_siblings_vertical(self): current = Geometry(0, 20, 10, 10) @@ -50,11 +47,8 @@ def test_siblings_vertical(self): siblings = ewmh_m2m.screen.get_sibling_screens(current, screens) - assert siblings == { - Ordinal.EAST: [], Ordinal.WEST: [], - Ordinal.SOUTH: [Geometry(0, 30, 10, 10), Geometry(0, 40, 10, 10)], - Ordinal.NORTH: [Geometry(0, 10, 10, 10), Geometry(0, 0, 10, 10)] - } + assert siblings[Ordinal.SOUTH] == [Geometry(0, 30, 10, 10), Geometry(0, 40, 10, 10)] + assert siblings[Ordinal.NORTH] == [Geometry(0, 10, 10, 10), Geometry(0, 0, 10, 10)] def test_siblings(self): screens = sorted( @@ -64,12 +58,26 @@ def test_siblings(self): siblings = ewmh_m2m.screen.get_sibling_screens(current, screens) - assert siblings == { - Ordinal.NORTH: [Geometry(2, 1, 1, 1), Geometry(2, 0, 1, 1)], - Ordinal.EAST: [Geometry(3, 2, 1, 1), Geometry(4, 2, 1, 1)], - Ordinal.SOUTH: [Geometry(2, 3, 1, 1), Geometry(2, 4, 1, 1)], - Ordinal.WEST: [Geometry(1, 2, 1, 1), Geometry(0, 2, 1, 1)] - } + assert siblings[Ordinal.NORTH] == [Geometry(2, 1, 1, 1), Geometry(2, 0, 1, 1)] + assert siblings[Ordinal.EAST] == [Geometry(3, 2, 1, 1), Geometry(4, 2, 1, 1)] + assert siblings[Ordinal.SOUTH] == [Geometry(2, 3, 1, 1), Geometry(2, 4, 1, 1)] + assert siblings[Ordinal.WEST] == [Geometry(1, 2, 1, 1), Geometry(0, 2, 1, 1)] + + assert siblings[Ordinal.NORTHEAST] == [Geometry(3, 1, 1, 1), Geometry(3, 0, 1, 1), Geometry(4, 1, 1, 1), Geometry(4, 0, 1, 1)] + assert siblings[Ordinal.NORTHWEST] == [Geometry(1, 1, 1, 1), Geometry(1, 0, 1, 1), Geometry(0, 1, 1, 1), Geometry(0, 0, 1, 1)] + assert siblings[Ordinal.SOUTHWEST] == [Geometry(1, 3, 1, 1), Geometry(1, 4, 1, 1), Geometry(0, 3, 1, 1), Geometry(0, 4, 1, 1)] + assert siblings[Ordinal.SOUTHEAST] == [Geometry(3, 3, 1, 1), Geometry(3, 4, 1, 1), Geometry(4, 3, 1, 1), Geometry(4, 4, 1, 1)] + + assert set(siblings[Ordinal.EAST_NORTHEAST]) == set(siblings[Ordinal.EAST] + siblings[Ordinal.NORTHEAST]) + assert set(siblings[Ordinal.NORTH_NORTHEAST]) == set(siblings[Ordinal.NORTH] + siblings[Ordinal.NORTHEAST]) + assert set(siblings[Ordinal.NORTH_NORTHWEST]) == set(siblings[Ordinal.NORTH] + siblings[Ordinal.NORTHWEST]) + assert set(siblings[Ordinal.WEST_NORTHWEST]) == set(siblings[Ordinal.WEST] + siblings[Ordinal.NORTHWEST]) + assert set(siblings[Ordinal.WEST_SOUTHWEST]) == set(siblings[Ordinal.WEST] + siblings[Ordinal.SOUTHWEST]) + assert set(siblings[Ordinal.SOUTH_SOUTHWEST]) == set(siblings[Ordinal.SOUTH] + siblings[Ordinal.SOUTHWEST]) + assert set(siblings[Ordinal.SOUTH_SOUTHEAST]) == set(siblings[Ordinal.SOUTH] + siblings[Ordinal.SOUTHEAST]) + assert set(siblings[Ordinal.EAST_SOUTHEAST]) == set(siblings[Ordinal.EAST] + siblings[Ordinal.SOUTHEAST]) + + def test_siblings_gh_issue_14(self): """