Fix 'dist' target in Makefile
[notion/jeffpc.git] / mod_xinerama / mod_xinerama.lua
blobd8b768caf430f356f553c5ea32c3cc41e3f89882
1 -- Notion xinerama module - lua setup
2 --
3 -- by Tomas Ebenlendr <ebik@ucw.cz>
4 --
5 -- This library is free software; you can redistribute it and/or
6 -- modify it under the terms of the GNU Lesser General Public
7 -- License as published by the Free Software Foundation; either
8 -- version 2.1 of the License,or (at your option) any later version.
9 --
10 -- This library is distributed in the hope that it will be useful,
11 -- but WITHOUT ANY WARRANTY; without even the implied warranty of
12 -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 -- Lesser General Public License for more details.
15 -- You should have received a copy of the GNU Lesser General Public
16 -- License along with this library; if not,write to the Free Software
17 -- Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 -- This is a slight abuse of the package.loaded variable perhaps, but
21 -- library-like packages should handle checking if they're loaded instead of
22 -- confusing the user with require/include differences.
23 if package.loaded["mod_xinerama"] then return end
25 if not notioncore.load_module("mod_xinerama") then
26 return
27 end
29 local mod_xinerama=_G["mod_xinerama"]
31 assert(mod_xinerama)
34 -- Helper functions {{{
36 local function max(one, other)
37 if one == nil then return other end
38 if other == nil then return one end
40 return (one > other) and one or other
41 end
43 -- creates new table, converts {x,y,w,h} representation to {x,y,xmax,ymax}
44 local function to_max_representation(screen)
45 return {
46 x = screen.x,
47 y = screen.y,
48 xmax = screen.x + screen.w,
49 ymax = screen.y + screen.h
51 end
53 -- edits passed table, converts representation {x,y,xmax,ymax} to {x,y,w,h},
54 -- and sorts table of indices (entry screen.ids)
55 local function fix_representation(screen)
56 screen.w = screen.xmax - screen.x
57 screen.h = screen.ymax - screen.y
58 screen.xmax = nil
59 screen.ymax = nil
60 table.sort(screen.ids)
61 end
63 local function fix_representations(screens)
64 for _k, screen in pairs(screens) do
65 fix_representation(screen)
66 end
67 end
69 -- }}}
71 -- Contained screens {{{
73 -- true if [from1, to1] contains [from2, to2]
74 local function contains(from1, to1, from2, to2)
75 return (from1 <= from2) and (to1 >= to2)
76 end
78 -- true if scr1 contains scr2
79 local function screen_contains(scr1, scr2)
80 local x_in = contains(scr1.x, scr1.xmax, scr2.x, scr2.xmax)
81 local y_in = contains(scr1.y, scr1.ymax, scr2.y, scr2.ymax)
82 return x_in and y_in
83 end
85 --DOC
86 -- Filters out fully contained screens. I.e. it merges two screens
87 -- if one is fully contained in the other screen (this contains the
88 -- case that both screens are of the same geometry).
89 -- The output screens also contain field ids containing the numbers
90 -- of merged screens. The order of the screens is defined by the
91 -- first screen in the merged set. (I.e., having big B, and big C,
92 -- showing different parts of desktop and small A (primary) showing
93 -- part of C, then the order will be C,B and not B,C as someone
94 -- may expect.
96 -- Example input format: \{\{x=0,y=0,w=1024,h=768\},\{x=0,y=0,w=1280,h=1024\}\}
97 function mod_xinerama.merge_contained_screens(screens)
98 local ret = {}
99 for newnum, _newscreen in ipairs(screens) do
100 newscreen = to_max_representation(_newscreen)
101 local merged = false
102 for prevnum, prevscreen in pairs(ret) do
103 if screen_contains(prevscreen, newscreen) then
104 table.insert(prevscreen.ids,newnum)
105 merged = true
106 elseif screen_contains(newscreen, prevscreen) then
107 prevscreen.x = newscreen.x
108 prevscreen.y = newscreen.y
109 prevscreen.xmax = newscreen.xmax
110 prevscreen.ymax = newscreen.ymax
111 table.insert(prevscreen.ids,newnum)
112 merged = true
114 if merged then break end
116 if not merged then
117 newscreen.ids = { newnum }
118 table.insert(ret, newscreen)
121 fix_representations(ret)
122 return ret
125 -- }}}
127 --- {{{ Overlapping screens
129 -- true if [from1, to1] overlaps [from2, to2]
130 local function overlaps (from1, to1, from2, to2)
131 return (from1 < to2) and (from2 < to1)
134 -- true if scr1 overlaps scr2
135 local function screen_overlaps(scr1, scr2)
136 local x_in = overlaps(scr1.x, scr1.xmax, scr2.x, scr2.xmax)
137 local y_in = overlaps(scr1.y, scr1.ymax, scr2.y, scr2.ymax)
138 return x_in and y_in
141 --DOC
142 -- Merges overlapping screens. I.e. it finds set of smallest rectangles,
143 -- such that these rectangles do not overlap and such that they contain
144 -- all screens.
146 -- Example input format: \{\{x=0,y=0,w=1024,h=768\},\{x=0,y=0,w=1280,h=1024\}\}
147 function mod_xinerama.merge_overlapping_screens(screens)
148 local ret = {}
149 for _newnum, _newscreen in ipairs(screens) do
150 local newscreen = to_max_representation(_newscreen)
151 newscreen.ids = { _newnum }
152 local overlaps = true
153 local pos
154 while overlaps do
155 overlaps = false
156 for prevpos, prevscreen in pairs(ret) do
157 if screen_overlaps(prevscreen, newscreen) then
158 -- stabilise ordering
159 if (not pos) or (prevpos < pos) then pos = prevpos end
160 -- merge with the previous screen
161 newscreen.x = math.min(newscreen.x, prevscreen.x)
162 newscreen.y = math.min(newscreen.y, prevscreen.y)
163 newscreen.xmax = math.max(newscreen.xmax, prevscreen.xmax)
164 newscreen.ymax = math.max(newscreen.ymax, prevscreen.ymax)
165 -- merge the indices
166 for _k, _v in ipairs(prevscreen.ids) do
167 table.insert(newscreen.ids, _v)
170 -- delete the merged previous screen
171 table.remove(ret, prevpos)
173 -- restart from beginning
174 overlaps = true
175 break
179 if not pos then pos = table.maxn(ret)+1 end
180 table.insert(ret, pos, newscreen)
182 fix_representations(ret)
183 return ret
186 --DOC
187 -- Merges overlapping screens. I.e. it merges two screens
188 -- if they overlap. It merges two screens if and only if there
189 -- is a path between them using only overlapping screens.
190 -- one is fully contained in the other screen (this contains the
191 -- case that both screens are of the same geometry).
192 -- The output screens also contain field ids containing the numbers
193 -- of merged screens. The order of the screens is defined by the
194 -- first screen in the merged set. (I.e., having big B, and big C,
195 -- showing different parts of desktop and small A (primary) showing
196 -- part of C, then the order will be C,B and not B,C as someone
197 -- may expect.
199 -- This function may output overlapping regions, AB and C on the example:
200 -- *-------*
201 -- *-----* | C |
202 -- | | *-------*
203 -- | A |
204 -- | +-+-------*
205 -- *---+-* |
206 -- | B |
207 -- | |
208 -- +---------*
209 -- Notion's WScreen implementation will (partially) hide C when AB is focused.
210 -- Thus this algorithm is not what you want by default.
212 -- Example input format: \{\{x=0,y=0,w=1024,h=768\},\{x=0,y=0,w=1280,h=1024\}\}
213 -- See test_xinerama.lua for example input/output
214 function mod_xinerama.merge_overlapping_screens_alternative(screens)
215 -- Group overlapping screens into sets for merging.
216 -- *-------*
217 -- *-----* | C |
218 -- | | *-------*
219 -- | A |
220 -- | +-+-------*
221 -- *---+-* |
222 -- | B |
223 -- | |
224 -- +---------*
226 -- Our algorithm merges A with B, but it does not
227 -- merge C to 'AB'. This is due to we only identify
228 -- overlapping screen sets in first phase.
231 -- *-----* *-----*
232 -- | A +-+-----+-+ B |
233 -- *---+-+ C +-+---+
234 -- +---------+
236 -- Our algoritm merges all three screens even if
237 -- it first decides that A and B does not overlap,
238 -- and then takes screen C.
240 local screensets = {}
241 for _newnum, _newscreen in ipairs(screens) do
242 newscreen = to_max_representation(_newscreen)
243 newscreen.id = _newnum
245 --Find all screensets to merge with:
246 --if there is a screen in a screenset that overlaps
247 --with current screen, then we mark the set in 'mergekeys'
248 local mergekeys = {}
249 -- We use ipairs here, because we rely on the order.
250 for setkey, screenset in ipairs(screensets) do
251 -- find any screen of 'screenset' that overlaps 'newscreen'
252 for _k, prevscreen in pairs(screenset) do
253 if screen_overlaps(newscreen, prevscreen) then
254 -- Found. 'setkey' contains indices of overlapping
255 -- 'screenset's, sorted decreasingly
256 table.insert(mergekeys, 1, setkey)
257 break
262 -- Here we merge all marked screensets to one new screenset.
263 -- We also delete the merged screensets from the 'screensets' table.
264 local mergedset = {newscreen}
265 local pos
266 -- we use ipairs here, because we rely on the order.
267 for _k, setkey in ipairs(mergekeys) do
268 -- copy contents of 'screensets[setkey]' to 'mergedset'
269 for _k2, prevscreen in pairs(screensets[setkey]) do
270 table.insert(mergedset, prevscreen)
272 -- remove 'screensets[setkey]'
273 table.remove(screensets, setkey)
274 pos = setkey
277 -- pos keeps index of first set that we merged in this loop,
278 -- we want to insert the product of this merge to pos.
279 if not pos then pos = table.maxn(screensets)+1 end
280 table.insert(screensets, pos, mergedset)
283 -- Now we have the screenset that contains the screens to be merged
284 local ret = {}
285 for _k, screenset in ipairs(screensets) do
286 local newscreen = {
287 x = screenset[1].x,
288 xmax = screenset[1].xmax,
289 y = screenset[1].y,
290 ymax = screenset[1].ymax,
291 ids = {}
293 for _k2, screen in pairs(screenset) do
294 newscreen.x = math.min(newscreen.x, screen.x)
295 newscreen.y = math.min(newscreen.y, screen.y)
296 newscreen.xmax = math.max(newscreen.xmax, screen.xmax)
297 newscreen.ymax = math.max(newscreen.ymax, screen.ymax)
298 table.insert(newscreen.ids, screen.id)
300 table.insert(ret, newscreen)
302 fix_representations(ret)
304 return ret
307 -- }}}
309 --- {{{ Setup notion's screens */
311 function mod_xinerama.close_invisible_screens(max_visible_screen_id)
312 local invisible_screen_id = max_visible_screen_id + 1
313 local invisible_screen = notioncore.find_screen_id(invisible_screen_id)
314 while invisible_screen do
315 -- note that this may not close the screen when it is still populated by
316 -- child windows that cannot be 'rescued'
317 invisible_screen:rqclose();
319 invisible_screen_id = invisible_screen_id + 1
320 invisible_screen = notioncore.find_screen_id(invisible_screen_id)
325 -- find any screens with 0 workspaces and populate them with an empty one
326 function mod_xinerama.populate_empty_screens()
327 local screen_id = 0;
328 local screen = notioncore.find_screen_id(screen_id)
329 while (screen ~= nil) do
330 if screen:mx_count() == 0 then
331 notioncore.create_ws(screen)
334 screen_id = screen_id + 1
335 screen = notioncore.find_screen_id(screen_id)
339 -- This should be made 'smarter', but at least let's make sure workspaces don't
340 -- end up on invisible screens
341 function mod_xinerama.rearrange_workspaces(max_visible_screen_id)
342 function move_to_first_screen(workspace)
343 notioncore.find_screen_id(0):attach(workspace)
346 function rearrange_workspaces_s(screen)
347 if screen:id() > max_visible_screen_id then
348 for i = 0, screen:mx_count() do
349 move_to_first_screen(screen:mx_nth(i))
354 local screen_id = 0;
355 local screen = notioncore.find_screen_id(screen_id)
356 while (screen ~= nil) do
357 rearrange_workspaces_s(screen);
359 screen_id = screen_id + 1
360 screen = notioncore.find_screen_id(screen_id)
362 mod_xinerama.populate_empty_screens()
365 function mod_xinerama.find_max_screen_id(screens)
366 local max_screen_id = 0
368 for screen_index, screen in ipairs(screens) do
369 local screen_id = screen_index - 1
370 max_screen_id = max(max_screen_id, screen_id)
373 return max_screen_id;
376 --DOC
377 -- Perform the setup of notion screens.
379 -- The first call sets up the screens of notion, subsequent calls update the
380 -- current screens
382 -- Returns true on success, false on failure
384 -- Example input: {{x=0,y=0,w=1024,h=768},{x=1024,y=0,w=1280,h=1024}}
385 function mod_xinerama.setup_screens(screens)
386 -- Update screen dimensions or create new screens
387 for screen_index, screen in ipairs(screens) do
388 local screen_id = screen_index - 1
389 local existing_screen = notioncore.find_screen_id(screen_id)
391 if existing_screen ~= nil then
392 mod_xinerama.update_screen(existing_screen, screen)
393 else
394 mod_xinerama.setup_new_screen(screen_id, screen)
395 if package.loaded["mod_sp"] then
396 mod_sp.create_scratchpad(notioncore.find_screen_id(screen_id))
402 -- }}}
404 -- Mark ourselves loaded.
405 package.loaded["mod_xinerama"]=true
407 -- Load configuration file
408 dopath('cfg_xinerama', true)
410 --DOC
411 -- Queries Xinerama for the screen dimensions and updates notion screens
412 -- accordingly
413 function mod_xinerama.refresh()
414 local screens = mod_xinerama.query_screens()
415 if screens then
416 local merged_screens = mod_xinerama.merge_overlapping_screens(screens)
417 mod_xinerama.setup_screens(merged_screens)
419 -- when the number of screens is lower than last time this function was
420 -- called, ask 'superfluous' to close
421 mod_xinerama.close_invisible_screens(mod_xinerama.find_max_screen_id(screens))
422 end
423 notioncore.screens_updated(notioncore.rootwin());
426 -- At this point any workspaces from a saved session haven't been added yet
427 mod_xinerama.refresh()