Merge branch 'contrib_goto_focus'
[notion.git] / mod_xrandr / cfg_xrandr.lua
blobb92df3a90db37f9686112551eeda3b623fb6710c
1 -- For honest workspaces, the initial outputs information, which determines the
2 -- physical screen that a workspace wants to be on, is part of the C class
3 -- WGroupWS. For "full screen workspaces" and scratchpads, we only keep this
4 -- information in a temporary list.
5 InitialOutputs={}
7 function getInitialOutputs(ws)
8 if obj_is(ws, "WGroupCW") or is_scratchpad(ws) then
9 return InitialOutputs[ws:name()]
10 elseif obj_is(ws, "WGroupWS") then
11 return WGroupWS.get_initial_outputs(ws)
12 else
13 return nil
14 end
15 end
17 function setInitialOutputs(ws, outputs)
18 if obj_is(ws, "WGroupCW") or is_scratchpad(ws) then
19 InitialOutputs[ws:name()] = outputs
20 elseif obj_is(ws, "WGroupWS") then
21 WGroupWS.set_initial_outputs(ws, outputs)
22 end
23 end
25 function nilOrEmpty(t)
26 return not t or empty(t)
27 end
29 function mod_xrandr.workspace_added(ws)
30 if nilOrEmpty(getInitialOutputs(ws)) then
31 outputs = mod_xrandr.get_outputs(ws:screen_of(ws))
32 outputKeys = {}
33 for k,v in pairs(outputs) do
34 table.insert(outputKeys, k)
35 end
36 setInitialOutputs(ws, outputKeys)
37 end
38 return true
39 end
41 function for_all_workspaces_do(fn)
42 local workspaces={}
43 notioncore.region_i(function(scr)
44 scr:managed_i(function(ws)
45 table.insert(workspaces, ws)
46 return true
47 end)
48 return true
49 end, "WScreen")
50 for _,ws in ipairs(workspaces) do
51 fn(ws)
52 end
53 end
55 function mod_xrandr.workspaces_added()
56 for_all_workspaces_do(mod_xrandr.workspace_added)
57 end
59 function mod_xrandr.screenmanagedchanged(tab)
60 if tab.mode == 'add' then
61 mod_xrandr.workspace_added(tab.sub);
62 end
63 end
65 screen_managed_changed_hook = notioncore.get_hook('screen_managed_changed_hook')
66 if screen_managed_changed_hook then
67 screen_managed_changed_hook:add(mod_xrandr.screenmanagedchanged)
68 end
70 post_layout_setup_hook = notioncore.get_hook('ioncore_post_layout_setup_hook')
71 post_layout_setup_hook:add(mod_xrandr.workspaces_added)
73 function add_safe(t, key, value)
74 if t[key] == nil then
75 t[key] = {}
76 end
78 table.insert(t[key], value)
79 end
81 -- parameter: list of output names
82 -- returns: map from screen name to screen
83 function candidate_screens_for_output(max_screen_id, all_outputs, outputname)
84 local retval = {}
86 function addIfContainsOutput(screen)
87 local outputs_within_screen = mod_xrandr.get_outputs_within(all_outputs, screen)
88 if screen:id() <= max_screen_id and outputs_within_screen[outputname] ~= nil then
89 retval[screen:name()] = screen
90 end
91 return true
92 end
93 notioncore.region_i(addIfContainsOutput, "WScreen")
95 return retval
96 end
98 -- parameter: maximum screen id, list of all output names, list of output names for which we want the screens
99 -- returns: map from screen name to screen
100 function candidate_screens_for_outputs(max_screen_id, all_outputs, outputnames)
101 local result = {}
103 if outputnames == nil then return result end
105 for i,outputname in pairs(outputnames) do
106 local screens = candidate_screens_for_output(max_screen_id, all_outputs, outputname)
107 for k,screen in pairs(screens) do
108 result[k] = screen;
111 return result;
114 function firstValue(t)
115 local key, value = next(t)
116 return value
119 function firstKey(t)
120 local key, value = next(t)
121 return key
124 function empty(t)
125 return not next(t)
128 function singleton(t)
129 local first = next(t)
130 return first and not next(t, first)
133 function is_scratchpad(ws)
134 return package.loaded["mod_sp"] and mod_sp.is_scratchpad(ws)
137 function find_scratchpad(screen)
138 local sp
139 screen:managed_i(function(ws)
140 if is_scratchpad(ws) then
141 sp=ws
142 return false
143 else
144 return true
146 end)
147 return sp
150 function move_if_needed(workspace, screen_id)
151 local screen = notioncore.find_screen_id(screen_id)
153 if workspace:screen_of() ~= screen then
154 if is_scratchpad(workspace) then
155 -- Moving a scratchpad to another screen is not meaningful, so instead we move
156 -- its content
157 local content={}
158 workspace:bottom():managed_i(function(reg)
159 table.insert(content, reg)
160 return true
161 end)
162 local sp=find_scratchpad(screen)
163 for _,reg in ipairs(content) do
164 sp:bottom():attach(reg)
166 return
169 screen:attach(workspace)
173 -- Arrange the workspaces over the first number_of_screens screens
174 function mod_xrandr.rearrangeworkspaces(max_screen_id)
175 -- for each screen id, which workspaces should be on that screen
176 new_mapping = {}
177 -- workspaces that want to be on an output that's currently not on any screen
178 orphans = {}
179 -- workspaces that want to be on multiple available outputs
180 wanderers = {}
182 local all_outputs = mod_xrandr.get_all_outputs()
184 -- When moving a "full screen workspace" to another screen, we seem to lose
185 -- its placeholder and thereby the possibility to return it from full
186 -- screen later. Let's therefore try to close any full screen workspace
187 -- before rearranging.
188 full_screen_workspaces={}
189 for_all_workspaces_do(function(ws)
190 if obj_is(ws, "WGroupCW") then table.insert(full_screen_workspaces, ws)
192 return true
193 end)
194 for _,ws in ipairs(full_screen_workspaces) do
195 ws:set_fullscreen("false")
198 -- round one: divide workspaces in directly assignable,
199 -- orphans and wanderers
200 function roundone(workspace)
201 local screens = candidate_screens_for_outputs(max_screen_id, all_outputs, getInitialOutputs(workspace))
202 if nilOrEmpty(screens) then
203 table.insert(orphans, workspace)
204 elseif singleton(screens) then
205 add_safe(new_mapping, firstValue(screens):id(), workspace)
206 else
207 wanderers[workspace] = screens
209 return true
211 for_all_workspaces_do(roundone)
213 for workspace,screens in pairs(wanderers) do
214 -- TODO add to screen with least # of workspaces instead of just the
215 -- first one that applies
216 if screens[workspace:screen_of():name()] then
217 add_safe(new_mapping, workspace:screen_of():id(), workspace)
218 else
219 add_safe(new_mapping, firstValue(screens):id(), workspace)
222 for i,workspace in pairs(orphans) do
223 -- TODO add to screen with least # of workspaces instead of just the first one
224 add_safe(new_mapping, 0, workspace)
227 for screen_id,workspaces in pairs(new_mapping) do
228 -- move workspace to that
229 for i,workspace in pairs(workspaces) do
230 move_if_needed(workspace, screen_id)
235 -- refresh xinerama and rearrange workspaces on screen layout updates
236 function mod_xrandr.screenlayoutupdated()
237 notioncore.profiling_start('notion_xrandrrefresh.prof')
239 local screens = mod_xinerama.query_screens()
240 if screens then
241 local merged_screens = mod_xinerama.merge_overlapping_screens(screens)
242 mod_xinerama.setup_screens(merged_screens)
245 local max_screen_id = mod_xinerama.find_max_screen_id(screens);
246 mod_xrandr.rearrangeworkspaces(max_screen_id)
248 if screens then
249 mod_xinerama.close_invisible_screens(max_screen_id)
250 end
252 mod_xinerama.populate_empty_screens()
254 notioncore.screens_updated(notioncore.rootwin())
255 notioncore.profiling_stop()
258 randr_screen_change_notify_hook = notioncore.get_hook('randr_screen_change_notify')
260 if randr_screen_change_notify_hook then
261 randr_screen_change_notify_hook:add(mod_xrandr.screenlayoutupdated)