File: mod_xinerama.lua

package info (click to toggle)
notion 4.0.2%2Bdfsg-5
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 4,676 kB
  • sloc: ansic: 47,508; sh: 2,096; makefile: 603; perl: 270
file content (432 lines) | stat: -rw-r--r-- 13,701 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
-- Notion xinerama module - lua setup
--
-- by Tomas Ebenlendr <ebik@ucw.cz>
--
-- This library is free software; you can redistribute it and/or
-- modify it under the terms of the GNU Lesser General Public
-- License as published by the Free Software Foundation; either
-- version 2.1 of the License,or (at your option) any later version.
--
-- This library is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-- Lesser General Public License for more details.
--
-- You should have received a copy of the GNU Lesser General Public
-- License along with this library; if not,write to the Free Software
-- Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
--

-- This is a slight abuse of the package.loaded variable perhaps, but
-- library-like packages should handle checking if they're loaded instead of
-- confusing the user with require/include differences.
if package.loaded["mod_xinerama"] then return end

if not notioncore.load_module("mod_xinerama") then
    return
end

local mod_xinerama=_G["mod_xinerama"]

assert(mod_xinerama)


local table_maxn = table.maxn or function(tbl)
   local c=0
   for k in pairs(tbl) do c=c+1 end
   return c
end
-- Helper functions {{{

local function max(one, other)
    if one == nil then return other end
    if other == nil then return one end

    return (one > other) and one or other
end

-- creates new table, converts {x,y,w,h} representation to {x,y,xmax,ymax}
local function to_max_representation(screen)
    return {
	x = screen.x,
	y = screen.y,
	xmax = screen.x + screen.w,
	ymax = screen.y + screen.h
    }
end

-- edits passed table, converts representation {x,y,xmax,ymax} to {x,y,w,h},
-- and sorts table of indices (entry screen.ids)
local function fix_representation(screen)
    screen.w = screen.xmax - screen.x
    screen.h = screen.ymax - screen.y
    screen.xmax = nil
    screen.ymax = nil
    table.sort(screen.ids)
end

local function fix_representations(screens)
    for _k, screen in pairs(screens) do
	fix_representation(screen)
    end
end

-- }}}

-- Contained screens {{{

-- true if [from1, to1] contains [from2, to2]
local function contains(from1, to1, from2, to2)
    return (from1 <= from2) and (to1 >= to2)
end

-- true if scr1 contains scr2
local function screen_contains(scr1, scr2)
    local x_in = contains(scr1.x, scr1.xmax, scr2.x, scr2.xmax)
    local y_in = contains(scr1.y, scr1.ymax, scr2.y, scr2.ymax)
    return x_in and y_in
end

--DOC
-- Filters out fully contained screens. I.e. it merges two screens
-- if one is fully contained in the other screen (this contains the
-- case that both screens are of the same geometry).
-- The output screens also contain field ids containing the numbers
-- of merged screens. The order of the screens is defined by the
-- first screen in the merged set. (I.e., having big B, and big C,
-- showing different parts of desktop and small A (primary) showing
-- part of C, then the order will be C,B and not B,C as someone
-- may expect.
--
-- Example input format: \{\{x=0,y=0,w=1024,h=768\},\{x=0,y=0,w=1280,h=1024\}\}
function mod_xinerama.merge_contained_screens(screens)
    local ret = {}
    for newnum, _newscreen in ipairs(screens) do
	newscreen = to_max_representation(_newscreen)
	local merged = false
	for prevnum, prevscreen in pairs(ret) do
	    if screen_contains(prevscreen, newscreen) then
		table.insert(prevscreen.ids,newnum)
		merged = true
	    elseif screen_contains(newscreen, prevscreen) then
		prevscreen.x = newscreen.x
		prevscreen.y = newscreen.y
		prevscreen.xmax = newscreen.xmax
		prevscreen.ymax = newscreen.ymax
		table.insert(prevscreen.ids,newnum)
		merged = true
	    end
	    if merged then break end
	end
	if not merged then
	    newscreen.ids = { newnum }
	    table.insert(ret, newscreen)
	end
    end
    fix_representations(ret)
    return ret
end

-- }}}

--- {{{ Overlapping screens

-- true if [from1, to1] overlaps [from2, to2]
local function overlaps (from1, to1, from2, to2)
    return (from1 < to2) and (from2 < to1)
end

-- true if scr1 overlaps scr2
local function screen_overlaps(scr1, scr2)
    local x_in = overlaps(scr1.x, scr1.xmax, scr2.x, scr2.xmax)
    local y_in = overlaps(scr1.y, scr1.ymax, scr2.y, scr2.ymax)
    return x_in and y_in
end

--DOC
-- Merges overlapping screens. I.e. it finds set of smallest rectangles,
-- such that these rectangles do not overlap and such that they contain
-- all screens.
--
-- Example input format: \{\{x=0,y=0,w=1024,h=768\},\{x=0,y=0,w=1280,h=1024\}\}
function mod_xinerama.merge_overlapping_screens(screens)
    local ret = {}
    for _newnum, _newscreen in ipairs(screens) do
	local newscreen = to_max_representation(_newscreen)
	newscreen.ids = { _newnum }
	local overlaps = true
	local pos
	while overlaps do
	    overlaps = false
	    for prevpos, prevscreen in pairs(ret) do
		if screen_overlaps(prevscreen, newscreen) then
		    -- stabilise ordering
		    if (not pos) or (prevpos < pos) then pos = prevpos end
		    -- merge with the previous screen
		    newscreen.x = math.min(newscreen.x, prevscreen.x)
		    newscreen.y = math.min(newscreen.y, prevscreen.y)
		    newscreen.xmax = math.max(newscreen.xmax, prevscreen.xmax)
		    newscreen.ymax = math.max(newscreen.ymax, prevscreen.ymax)
		    -- merge the indices
		    for _k, _v in ipairs(prevscreen.ids) do
			table.insert(newscreen.ids, _v)
		    end
		
		    -- delete the merged previous screen
		    table.remove(ret, prevpos)

		    -- restart from beginning
		    overlaps = true
		    break
		end
	    end
	end
	if not pos then pos = table_maxn(ret)+1 end
	table.insert(ret, pos, newscreen)
    end
    fix_representations(ret)
    return ret
end

--DOC
-- Merges overlapping screens. I.e. it merges two screens
-- if they overlap. It merges two screens if and only if there
-- is a path between them using only overlapping screens.
-- one is fully contained in the other screen (this contains the
-- case that both screens are of the same geometry).
-- The output screens also contain field ids containing the numbers
-- of merged screens. The order of the screens is defined by the
-- first screen in the merged set. (I.e., having big B, and big C,
-- showing different parts of desktop and small A (primary) showing
-- part of C, then the order will be C,B and not B,C as someone
-- may expect.
--
-- This function may output overlapping regions, AB and C on the example:
--         *-------*
-- *-----* |   C   |
-- |     | *-------*
-- |  A  |
-- |   +-+-------*
-- *---+-*       |
--     |    B    |
--     |         |
--     +---------*
-- Notion's WScreen implementation will (partially) hide C when AB is focused.
-- Thus this algorithm is not what you want by default.
--
-- Example input format: \{\{x=0,y=0,w=1024,h=768\},\{x=0,y=0,w=1280,h=1024\}\}
-- See test_xinerama.lua for example input/output
function mod_xinerama.merge_overlapping_screens_alternative(screens)
    -- Group overlapping screens into sets for merging.
    --         *-------*
    -- *-----* |   C   |
    -- |     | *-------*
    -- |  A  |
    -- |   +-+-------*
    -- *---+-*       |
    --     |    B    |
    --     |         |
    --     +---------*
    --
    -- Our algorithm merges A with B, but it does not
    -- merge C to 'AB'. This is due to we only identify
    -- overlapping screen sets in first phase.
    --
    --
    -- *-----*     *-----*
    -- | A +-+-----+-+ B |
    -- *---+-+  C  +-+---+
    --     +---------+
    --
    -- Our algoritm merges all three screens even if
    -- it first decides that A and B does not overlap,
    -- and then takes screen C.
    --
    local screensets = {}
    for _newnum, _newscreen in ipairs(screens) do
	newscreen = to_max_representation(_newscreen)
	newscreen.id = _newnum

	--Find all screensets to merge with:
	--if there is a screen in a screenset that overlaps
	--with current screen, then we mark the set in 'mergekeys'
	local mergekeys = {}
	-- We use ipairs here, because we rely on the order.
	for setkey, screenset in ipairs(screensets) do
	    -- find any screen of 'screenset' that overlaps 'newscreen'
	    for _k, prevscreen in pairs(screenset) do
		if screen_overlaps(newscreen, prevscreen) then
		    -- Found. 'setkey' contains indices of overlapping
		    -- 'screenset's, sorted decreasingly
		    table.insert(mergekeys, 1, setkey)
		    break
		end
	    end
	end

	-- Here we merge all marked screensets to one new screenset.
	-- We also delete the merged screensets from the 'screensets' table.
	local mergedset = {newscreen}
	local pos
	-- we use ipairs here, because we rely on the order.
	for _k, setkey in ipairs(mergekeys) do
	    -- copy contents of 'screensets[setkey]' to 'mergedset'
	    for _k2, prevscreen in pairs(screensets[setkey]) do
		table.insert(mergedset, prevscreen)
	    end
	    -- remove 'screensets[setkey]'
	    table.remove(screensets, setkey)
	    pos = setkey
	end

	-- pos keeps index of first set that we merged in this loop,
	-- we want to insert the product of this merge to pos.
	if not pos then pos = table_maxn(screensets)+1 end
	table.insert(screensets, pos, mergedset)
    end

    -- Now we have the screenset that contains the screens to be merged
    local ret = {}
    for _k, screenset in ipairs(screensets) do
	local newscreen = {
	    x = screenset[1].x,
	    xmax = screenset[1].xmax,
	    y = screenset[1].y,
	    ymax = screenset[1].ymax,
	    ids = {}
	}
	for _k2, screen in pairs(screenset) do
	    newscreen.x = math.min(newscreen.x, screen.x)
	    newscreen.y = math.min(newscreen.y, screen.y)
	    newscreen.xmax = math.max(newscreen.xmax, screen.xmax)
	    newscreen.ymax = math.max(newscreen.ymax, screen.ymax)
	    table.insert(newscreen.ids, screen.id)
	end
	table.insert(ret, newscreen)
    end
    fix_representations(ret)

    return ret
end

-- }}}

--- {{{ Setup notion's screens */

function mod_xinerama.close_invisible_screens(max_visible_screen_id)
    local invisible_screen_id = max_visible_screen_id + 1
    local invisible_screen = notioncore.find_screen_id(invisible_screen_id)
    while invisible_screen do
        -- note that this may not close the screen when it is still populated by
        -- child windows that cannot be 'rescued'
        invisible_screen:rqclose();

        invisible_screen_id = invisible_screen_id + 1
        invisible_screen = notioncore.find_screen_id(invisible_screen_id)
    end

end

-- find any screens with 0 workspaces and populate them with an empty one
function mod_xinerama.populate_empty_screens()
   local screen_id = 0;
   local screen = notioncore.find_screen_id(screen_id)
   while (screen ~= nil) do
       if screen:mx_count() == 0 then
           notioncore.create_ws(screen)
       end

       screen_id = screen_id + 1
       screen = notioncore.find_screen_id(screen_id)
   end
end

-- This should be made 'smarter', but at least let's make sure workspaces don't
-- end up on invisible screens
function mod_xinerama.rearrange_workspaces(max_visible_screen_id)
   function move_to_first_screen(workspace)
       notioncore.find_screen_id(0):attach(workspace)
   end

   function rearrange_workspaces_s(screen)
       if screen:id() > max_visible_screen_id then
           for i = 0, screen:mx_count() do
               move_to_first_screen(screen:mx_nth(i))
           end
       end
   end

   local screen_id = 0;
   local screen = notioncore.find_screen_id(screen_id)
   while (screen ~= nil) do
       rearrange_workspaces_s(screen);

       screen_id = screen_id + 1
       screen = notioncore.find_screen_id(screen_id)
   end
   mod_xinerama.populate_empty_screens()
end

function mod_xinerama.find_max_screen_id(screens)
    local max_screen_id = 0

    for screen_index, screen in ipairs(screens) do
        local screen_id = screen_index - 1
        max_screen_id = max(max_screen_id, screen_id)
    end

    return max_screen_id;
end

--DOC
-- Perform the setup of notion screens.
--
-- The first call sets up the screens of notion, subsequent calls update the
-- current screens
--
-- Returns true on success, false on failure
--
-- Example input: {{x=0,y=0,w=1024,h=768},{x=1024,y=0,w=1280,h=1024}}
function mod_xinerama.setup_screens(screens)
    -- Update screen dimensions or create new screens
    for screen_index, screen in ipairs(screens) do
        local screen_id = screen_index - 1
        local existing_screen = notioncore.find_screen_id(screen_id)

        if existing_screen ~= nil then
            mod_xinerama.update_screen(existing_screen, screen)
        else
            mod_xinerama.setup_new_screen(screen_id, screen)
            if package.loaded["mod_sp"] then
                mod_sp.create_scratchpad(notioncore.find_screen_id(screen_id))
            end
        end
    end
end

-- }}}

-- Mark ourselves loaded.
package.loaded["mod_xinerama"]=true

-- Load configuration file
dopath('cfg_xinerama', true)

--DOC
-- Queries Xinerama for the screen dimensions and updates notion screens
-- accordingly
function mod_xinerama.refresh()
    local screens = mod_xinerama.query_screens()
    if screens then
        local merged_screens = mod_xinerama.merge_overlapping_screens(screens)
        mod_xinerama.setup_screens(merged_screens)

        -- when the number of screens is lower than last time this function was
        -- called, ask 'superfluous' to close
        mod_xinerama.close_invisible_screens(mod_xinerama.find_max_screen_id(screens))
    end
    notioncore.screens_updated(notioncore.rootwin());
end

-- At this point any workspaces from a saved session haven't been added yet
mod_xinerama.refresh()