1
|
|
|
"""Layout calculation code""" |
2
|
|
|
|
3
|
|
|
__author__ = "Stephan Sokolow (deitarion/SSokolow)" |
4
|
|
|
__license__ = "GNU GPL 2.0 or later" |
5
|
|
|
|
6
|
|
|
import math |
7
|
|
|
from heapq import heappop, heappush |
8
|
|
|
|
9
|
|
|
# Allow MyPy to work without depending on the `typing` package |
10
|
|
|
# (And silence complaints from only using the imported types in comments) |
11
|
|
|
MYPY = False |
12
|
|
|
if MYPY: |
13
|
|
|
# pylint: disable=unused-import |
14
|
|
|
from typing import (Any, Dict, Iterable, Iterator, List, Optional, # NOQA |
15
|
|
|
Sequence, Sized, Tuple, Union) |
16
|
|
|
|
17
|
|
|
# pylint: disable=import-error, no-name-in-module |
18
|
|
|
from gtk.gdk import Rectangle # NOQA |
19
|
|
|
from .util import GeomTuple, PercentRect # NOQA |
20
|
|
|
|
21
|
|
|
Geom = Union[Rectangle, GeomTuple] # pylint: disable=invalid-name |
22
|
|
|
del MYPY |
23
|
|
|
|
24
|
|
|
def check_tolerance(distance, monitor_geom, tolerance=0.1): |
25
|
|
|
"""Check whether a distance is within tolerance, adjusted for window size. |
26
|
|
|
|
27
|
|
|
@param distance: An integer value representing a distance in pixels. |
28
|
|
|
@param monitor_geom: An (x, y, w, h) tuple representing the monitor |
29
|
|
|
geometry in pixels. |
30
|
|
|
@param tolerance: A value between 0.0 and 1.0, inclusive, which represents |
31
|
|
|
a percentage of the monitor size. |
32
|
|
|
""" |
33
|
|
|
|
34
|
|
|
# Take the euclidean distance of the monitor rectangle and convert |
35
|
|
|
# `distance` into a percentage of it, then test against `tolerance`. |
36
|
|
|
return float(distance) / math.hypot(*tuple(monitor_geom)[2:4]) < tolerance |
37
|
|
|
|
38
|
|
|
def closest_geom_match(needle, haystack): |
39
|
|
|
# type: (Geom, Sequence[Geom]) -> Tuple[int, int] |
40
|
|
|
"""Find the geometry in C{haystack} that most closely matches C{needle}. |
41
|
|
|
|
42
|
|
|
@return: A tuple of the euclidean distance and index in C{haystack} for the |
43
|
|
|
best match. |
44
|
|
|
""" |
45
|
|
|
# Calculate euclidean distances between the window's current geometry |
46
|
|
|
# and all presets and store them in a min heap. |
47
|
|
|
euclid_distance = [] # type: List[Tuple[int, int]] |
48
|
|
|
for haystack_pos, haystack_val in enumerate(haystack): |
49
|
|
|
distance = sum([(needle_i - haystack_i) ** 2 for (needle_i, haystack_i) |
50
|
|
|
in zip(tuple(needle), tuple(haystack_val))]) ** 0.5 |
51
|
|
|
heappush(euclid_distance, (distance, haystack_pos)) |
52
|
|
|
|
53
|
|
|
# to the next configuration. Otherwise, use the first configuration. |
54
|
|
|
closest_distance, closest_idx = heappop(euclid_distance) |
55
|
|
|
return closest_distance, closest_idx |
56
|
|
|
|
57
|
|
|
def resolve_fractional_geom(geom_tuple, monitor_geom, win_geom=None): |
58
|
|
|
# type: (Optional[Geom], Geom, Optional[Geom]) -> Geom |
59
|
|
|
"""Resolve proportional (eg. 0.5) and preserved (None) coordinates. |
60
|
|
|
|
61
|
|
|
@param geom_tuple: An (x, y, w, h) tuple with monitor-relative values in |
62
|
|
|
the range from 0.0 to 1.0, inclusive. |
63
|
|
|
|
64
|
|
|
If C{None}, then the value of C{win_geom} will be used. |
65
|
|
|
@param monitor_geom: An (x, y, w, h) tuple defining the bounding box of the |
66
|
|
|
monitor (or other desired region) within the desktop. |
67
|
|
|
@param win_geom: An (x, y, w, h) tuple defining the current shape of the |
68
|
|
|
window, in absolute desktop pixel coordinates. |
69
|
|
|
""" |
70
|
|
|
monitor_geom = tuple(monitor_geom) |
71
|
|
|
|
72
|
|
|
if geom_tuple is None: |
73
|
|
|
return win_geom |
74
|
|
|
else: |
75
|
|
|
# Multiply x and w by monitor.w, y and h by monitor.h |
76
|
|
|
return tuple(int(i * j) for i, j in |
77
|
|
|
zip(geom_tuple, monitor_geom[2:4] + monitor_geom[2:4])) |
78
|
|
|
|
79
|
|
|
class GravityLayout(object): # pylint: disable=too-few-public-methods |
80
|
|
|
"""Helper for translating top-left relative dimensions to other corners. |
81
|
|
|
|
82
|
|
|
Used to generate L{commands.cycle_dimensions} presets. |
83
|
|
|
|
84
|
|
|
Expects to operate on decimal percentage values. (0 <= x <= 1) |
85
|
|
|
""" |
86
|
|
|
#: Possible window alignments relative to the monitor/desktop. |
87
|
|
|
#: @todo 1.0.0: Normalize these to X11 or CSS terminology for 1.0 |
88
|
|
|
#: (API-breaking change) |
89
|
|
|
GRAVITIES = { |
90
|
|
|
'top-left': (0.0, 0.0), |
91
|
|
|
'top': (0.5, 0.0), |
92
|
|
|
'top-right': (1.0, 0.0), |
93
|
|
|
'left': (0.0, 0.5), |
94
|
|
|
'middle': (0.5, 0.5), |
95
|
|
|
'right': (1.0, 0.5), |
96
|
|
|
'bottom-left': (0.0, 1.0), |
97
|
|
|
'bottom': (0.5, 1.0), |
98
|
|
|
'bottom-right': (1.0, 1.0), |
99
|
|
|
} # type: Dict[str, Tuple[float, float]] |
100
|
|
|
|
101
|
|
|
def __init__(self, margin_x=0, margin_y=0): # type: (int, int) -> None |
102
|
|
|
""" |
103
|
|
|
@param margin_x: Horizontal margin to apply when calculating window |
104
|
|
|
positions, as decimal percentage of screen width. |
105
|
|
|
@param margin_y: Vertical margin to apply when calculating window |
106
|
|
|
positions, as decimal percentage of screen height. |
107
|
|
|
""" |
108
|
|
|
self.margin_x = margin_x |
109
|
|
|
self.margin_y = margin_y |
110
|
|
|
|
111
|
|
|
# pylint: disable=too-many-arguments |
112
|
|
|
def __call__(self, |
|
|
|
|
113
|
|
|
width, # type: float |
114
|
|
|
height, # type: float |
115
|
|
|
gravity='top-left', # type: str |
116
|
|
|
x=None, # type: Optional[float] |
117
|
|
|
y=None # type: Optional[float] |
118
|
|
|
): # type: (...) -> Tuple[float, float, float, float] |
119
|
|
|
"""Return an C{(x, y, w, h)} tuple relative to C{gravity}. |
120
|
|
|
|
121
|
|
|
This function takes and returns percentages, represented as decimals |
122
|
|
|
in the range 0 <= x <= 1, which can be multiplied by width and height |
123
|
|
|
values in actual units to produce actual window geometry. |
124
|
|
|
|
125
|
|
|
It can be used in two ways: |
126
|
|
|
|
127
|
|
|
1. If called B{without} C{x} and C{y} values, it will compute a |
128
|
|
|
geometry tuple which will align a window C{w} wide and C{h} tall |
129
|
|
|
according to C{geometry}. |
130
|
|
|
|
131
|
|
|
2. If called B{with} C{x} and C{y} values, it will translate a |
132
|
|
|
geometry tuple which is relative to the top-left corner so that it is |
133
|
|
|
instead relative to another corner. |
134
|
|
|
|
135
|
|
|
@param width: Desired width |
136
|
|
|
@param height: Desired height |
137
|
|
|
@param gravity: Desired window alignment from L{GRAVITIES} |
138
|
|
|
@param x: Desired horizontal position if not the same as C{gravity} |
139
|
|
|
@param y: Desired vertical position if not the same as C{gravity} |
140
|
|
|
|
141
|
|
|
@returns: C{(x, y, w, h)} |
142
|
|
|
|
143
|
|
|
@note: All parameters except C{gravity} are decimal values in the range |
144
|
|
|
C{0 <= x <= 1}. |
145
|
|
|
""" |
146
|
|
|
|
147
|
|
|
x = x or self.GRAVITIES[gravity][0] |
148
|
|
|
y = y or self.GRAVITIES[gravity][1] |
149
|
|
|
offset_x = width * self.GRAVITIES[gravity][0] |
150
|
|
|
offset_y = height * self.GRAVITIES[gravity][1] |
151
|
|
|
|
152
|
|
|
return (round(x - offset_x + self.margin_x, 3), |
153
|
|
|
round(y - offset_y + self.margin_y, 3), |
154
|
|
|
round(width - (self.margin_x * 2), 3), |
155
|
|
|
round(height - (self.margin_y * 2), 3)) |
156
|
|
|
|
157
|
|
|
def make_winsplit_positions(columns): |
158
|
|
|
# type: (int) -> Dict[str, List[PercentRect]] |
159
|
|
|
"""Generate the classic WinSplit Revolution tiling presets |
160
|
|
|
|
161
|
|
|
@todo: Figure out how best to put this in the config file. |
162
|
|
|
""" |
163
|
|
|
|
164
|
|
|
# TODO: Plumb GravityLayout.__init__'s arguments into the config file |
165
|
|
|
gvlay = GravityLayout() |
166
|
|
|
col_width = 1.0 / columns |
167
|
|
|
cycle_steps = tuple(round(col_width * x, 3) |
168
|
|
|
for x in range(1, columns)) |
169
|
|
|
|
170
|
|
|
middle_steps = (1.0,) + cycle_steps |
171
|
|
|
edge_steps = (0.5,) + cycle_steps |
172
|
|
|
|
173
|
|
|
positions = { |
174
|
|
|
'middle': [gvlay(width, 1, 'middle') for width in middle_steps], |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
for grav in ('top', 'bottom'): |
178
|
|
|
positions[grav] = [gvlay(width, 0.5, grav) for width in middle_steps] |
179
|
|
|
for grav in ('left', 'right'): |
180
|
|
|
positions[grav] = [gvlay(width, 1, grav) for width in edge_steps] |
181
|
|
|
for grav in ('top-left', 'top-right', 'bottom-left', 'bottom-right'): |
182
|
|
|
positions[grav] = [gvlay(width, 0.5, grav) for width in edge_steps] |
183
|
|
|
|
184
|
|
|
return positions |
185
|
|
|
|
This check looks for invalid names for a range of different identifiers.
You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.
If your project includes a Pylint configuration file, the settings contained in that file take precedence.
To find out more about Pylint, please refer to their site.