1-- returning true means we took responsibility for attaching to parent
2local swap_focus;
3
4local function save_wnd(wnd)
5	wnd.old = {
6		autocrop = wnd.autocrop,
7		shader = image_shader(wnd.canvas),
8		scalemode = wnd.scalemode,
9		showtbar = wnd.show_titlebar
10	};
11end
12
13local function restore_wnd(wnd)
14	if (not wnd.old) then
15		return;
16	end
17
18	if (wnd.old.shader > 0) then
19		image_shader(wnd.canvas, wnd.old.shader);
20	else
21		image_shader(wnd.canvas, "DEFAULT");
22	end
23
24	wnd.scalemode = wnd.old.scalemode;
25	wnd.autocrop = wnd.old.autocrop;
26	wnd.show_titlebar = wnd.old.showtbar;
27	wnd.displayhint_block_wh = false;
28	blend_image(wnd.canvas, wnd.canvas_opa and wnd.canvas_opa or 1.0);
29	wnd:set_title();
30end
31
32-- "center" means normal behavior and the active shader for the window
33local function center_imgcfg(wnd)
34	show_image(wnd.anchor);
35	if (wnd.space and not wnd.space.layouter) then
36		print("dangling layouter", debug.traceback());
37	end
38
39	if (wnd.space and not wnd.space.layouter.scaled) then
40		return;
41	end
42
43	wnd.displayhint_block_wh = true;
44	if (not wnd.old) then
45		save_wnd(wnd);
46	end
47
48	restore_wnd(wnd);
49end
50
51-- "side" means stretch, optional blend and a normal shader
52local function side_imgcfg(wnd, btime)
53	local vid = wnd.canvas;
54-- should the blend be animated, we add it as a timer so any unknown
55-- reset-image-transforms don't get in the way
56	if (btime) then
57		timer_add_periodic("layblend", 1, true,
58			function()
59				if (valid_vid(vid)) then
60					blend_image(vid, gconfig_get("autolay_sideopa"), btime);
61				end
62			end, true);
63	else
64		blend_image(wnd.canvas, gconfig_get("autolay_sideopa"));
65	end
66
67	if (wnd.space and not wnd.space.layouter.scaled) then
68		return;
69	end
70
71-- first cache original values so we can restore when reassigning
72	if (not wnd.old) then
73		save_wnd(wnd);
74	end
75	wnd.scalemode = "stretch";
76	wnd.autocrop = false;
77	wnd.displayhint_block_wh = true;
78
79	image_set_txcos_default(wnd.canvas, wnd.origo_ll);
80	if (gconfig_get("autolay_shader")) then
81		shader_setup(wnd.canvas, "simple", gconfig_get("autolay_sideshdr"));
82	end
83
84-- swap to side, maybe disable titlebar
85	wnd:set_titlebar(gconfig_get("autolay_sidetbar"));
86end
87
88local function sel_h(wnd, mouse)
89	if (mouse and not dispatch_locked()) then
90		if (gconfig_get("autolay_selswap")) then
91			swap_focus(wnd);
92		end
93	end
94end
95
96-- trigger if window baseclass changes
97local function reg_h(wnd)
98	if (not wnd.space) then
99		return;
100	end
101
102	save_wnd(wnd);
103	if (wnd.space.children[2] == wnd) then
104		center_imgcfg(wnd);
105	else
106		side_imgcfg(wnd);
107	end
108end
109
110local function center_setup(space)
111	local lin = space:linearize();
112-- nothing in focus region? then use selected window
113	local focus = space.children[2];
114	if (space.children[1]) then
115		if (space.children[1].center_focus) then
116			focus = space.children[1];
117		end
118	end
119
120	if (not focus) then
121		focus = space.selected;
122	end
123
124	if (focus) then
125		center_imgcfg(focus);
126	end
127
128-- nothing to do
129	if (#lin <= 1) then
130		return;
131	end
132	focus.center_focus = true;
133
134-- new "fair" window division of the remaining windows
135	local left = {};
136	local right = {};
137	local ctr = 1;
138	for i=1,#lin do
139		lin[i]:add_handler("register", reg_h);
140		lin[i]:add_handler("select", sel_h);
141		lin[i].vweight = 1.0;
142		if (lin[i] ~= focus) then
143			lin[i].center_focus = false;
144			side_imgcfg(lin[i]);
145
146			if (ctr % 2 == 1) then
147				table.insert(left, lin[i]);
148			else
149				table.insert(right, lin[i]);
150			end
151			ctr = ctr + 1;
152		end
153	end
154
155-- edge condition, ignore due to lost still left in structure can get us here
156	if (0 == #left and 0 == #right) then
157		return;
158	end
159
160	local ccount = right[1] and 3 or 2;
161	local mw = ccount * gconfig_get("autolay_centerw");
162	local rw = (ccount - mw) / (ccount - 1);
163-- set horizontal weight to be 10 80 10 or 10 [80 yet expand to 90]
164	space.children = {left[1], focus, right[1]};
165	focus.children = {};
166	focus.weight = mw;
167	focus.parent = space;
168
169	left[1].parent = space;
170	left[1].weight = rw;
171	left[1].children = {};
172
173	if (right[1]) then
174		right[1].weight = rw;
175		right[1].parent = space;
176		right[1].children = {};
177	end
178
179	for i=2,#left do
180		left[i].parent = left[i-1];
181		left[i].parent.children[1] = left[i];
182		left[i].children = {};
183	end
184
185	for i=2,#right do
186		right[i].parent = right[i-1];
187		right[i].parent.children[1] = right[i];
188		right[i].children = {};
189	end
190
191	space.children[2]:select();
192end
193
194-- all the edge cases to make sure center area is priority-focus and not
195-- running into multiple :select or :deselect calls on the same object
196-- breaking state
197local function center_focus(space)
198	local dst = space.children[2] and space.children[2] or space.children[1];
199	if (not dst) then
200		return;
201	end
202
203	space.layouter.cw = dst.max_w;
204	space.layouter.ch = dst.max_h;
205
206	if (space.selected == dst) then
207		return;
208	end
209
210	if (space.selected) then
211		space.selected:deselect();
212	end
213end
214
215-- find out how many levels of children a specific node has, needed to calc
216-- fair vertical weight when doing column- relayouting
217local function get_depth(node)
218	local depth = 0;
219	local last = node;
220
221	while node do
222		last = node;
223		depth = depth + 1;
224		node = node.children[1];
225	end
226
227	return depth, last;
228end
229
230-- return true? then we take responsibility for marking selected and insertion
231local function center_added(space, wnd)
232	if (not wnd.old) then
233		save_wnd(wnd);
234	end
235
236	if (#space.children ~= 3) then
237		table.insert(space.children, wnd);
238		wnd.parent = space;
239		center_setup(space);
240		space:resize();
241		center_focus(space);
242		return true;
243	end
244
245-- find the least used column
246	local ld = get_depth(space.children[1]);
247	local rd = get_depth(space.children[3]);
248	local dst;
249	local ind = ld < rd and 1 or 3;
250
251	local dst = space.children[ind];
252
253-- go to the deepest slot
254	while true do
255		if (not dst.children[1]) then
256			break;
257		else
258			dst = dst.children[1];
259		end
260	end
261
262-- insert there
263	dst.children[1] = wnd;
264	wnd.parent = dst;
265
266-- make sure sizes are fair
267	side_imgcfg(wnd);
268
269	hijack = true;
270	wnd:add_handler("register", reg_h);
271	wnd:add_handler("select", sel_h);
272	center_focus(space);
273	space:resize();
274	return true;
275end
276
277local function center_lost(space, wnd, destroy)
278-- priority is balance the first entry
279	if (#space.children ~= 3) then
280		center_setup(space);
281		space:resize();
282		center_focus(space);
283		return true;
284	end
285
286
287-- rebalance columns if the number of nodes in each gets unevenly distributed
288	local ld, ln = get_depth(space.children[1]);
289	local rd, rn = get_depth(space.children[3]);
290
291	if (ld > rd + 1) then
292		ln.parent.children[1] = nil;
293		rn.children[1] = ln;
294		ln.parent = rn;
295	elseif (rd > ld + 1) then
296		rn.parent.children[1] = nil;
297		ln.children[1] = rn;
298		rn.parent = ln;
299	end
300
301	if (not destroy) then
302		restore_wnd(wnd);
303		wnd:drop_handler("register", reg_h);
304		wnd:drop_handler("select", sel_h);
305	end
306
307	local mw = (3 - 3 * gconfig_get("autolay_centerw")) / 2;
308
309-- ugly edge condition, if we migrate or destroy a child to the first
310-- or last node, it can be promoted to first level with no event for us to
311-- latch on to, therefore force- reset the weights.
312	space.children[1].weight = mw;
313	space.children[3].weight = mw;
314
315	center_focus(space);
316	space:resize();
317	return true;
318end
319
320local function center_resize(space, lin, evblock, wnd, cb)
321
322-- this layouter can only deal with tiling mode
323	if (space.mode ~= "tile") then
324		if (lin) then
325			for i,v in ipairs(lin) do
326				restore_wnd(v);
327			end
328		end
329		return false;
330	end
331
332	if (evblock) then
333-- always forward the dimensions of the center space, block if this
334-- corresponds to last known "column" size as a catch all for some async. races
335		if (space.children[2]) then
336			local mw = space.children[2].max_w;
337			local mh = space.children[2].max_h;
338			local dw = space.children[2].width - space.children[2].effective_w;
339			local dh = space.children[2].height - space.children[2].effective_h;
340
341			if (not space.layouter.scaled or (not space.layouter.cw or
342				(mw == space.layouter.cw and mh == space.layouter.ch))) then
343				cb(mw, mh, mw - dw, mh - dh);
344			end
345		end
346		return true;
347	end
348	if (not active_display().selected and
349		active_display():active_space() == space) then
350		if (space.children[2]) then
351			space.children[2]:select();
352		elseif (space.children[1]) then
353			space.children[1]:select();
354		end
355	end
356-- just forward to the next layer
357end
358
359swap_focus = function(sel)
360	local sp = active_display():active_space();
361	local sw = active_display().selected;
362	local dw = sp.children[2];
363	if (not sp or #sp.children < 2 or not sw or sw.space ~= sp) then
364		return;
365	end
366
367	local cw = dw.max_w;
368	local ch = dw.max_h;
369	local cx = dw.x;
370	local cy = dw.y;
371	dw.center_focus = false;
372
373-- swap and maipulate allowed / perceived dimensions as the swap function
374-- does not do that, otherwise we risk one additional space- resize
375	if (sw ~= dw) then
376		local rw = sw.max_w;
377		local rh = sw.max_h;
378
379		sw.last = dw;
380		sw.center_focus = true;
381		sw.max_w = cw; sw.max_h = ch;
382		sw:swap(dw, false, true);
383		center_imgcfg(sw);
384		sw:resize(cw, ch);
385		dw.x = sw.x; dw.y = sw.y;
386		dw.max_w = rw; dw.max_h = rh;
387		sw.x = cx; sw.y = cy;
388		side_imgcfg(dw, gconfig_get("wnd_animation"));
389
390-- mask the event propagation if we're running in scaled- mode
391		dw:resize(rw, rh, false, true);
392		dw:reposition();
393
394-- "swap-in", use [last] reference for the window to swap
395	elseif (dw.last and dw.last.swap) then
396		local newc = dw.last;
397		newc.last = dw;
398		newc.center_focus = true;
399		dw.max_w = newc.max_w;
400		dw.max_h = newc.max_h;
401		newc.max_w = cw;
402		newc.max_h = ch;
403		dw:swap(newc, false, true);
404		center_imgcfg(newc);
405		newc:resize(cw, ch);
406		dw.x = newc.x; dw.y = newc.y;
407		newc.x = cx; newc.y = cy;
408		side_imgcfg(dw, gconfig_get("wnd_animation"));
409		dw:resize(dw.max_w, dw.max_h, false, true);
410	end
411
412	if (sel) then
413		sp.children[2]:select();
414	end
415
416	sp:resize(true);
417end
418
419local function center_free(space)
420	local lst = space:linearize();
421	for k,v in ipairs(lst) do
422		restore_wnd(v);
423		v.weight = 1.0;
424		v.vweight = 1.0;
425		v:drop_handler("register", reg_h);
426		v:drop_handler("select", sel_h);
427	end
428
429	space.layouter = nil;
430	space:resize();
431end
432
433-- book layouter is a horizontal/flat layout where a specific ration is
434-- designated to the 'main' area (children[1]) and the rest will be 'evenly'
435-- divided across the remaining area. Instad of scaling, we crop by
436-- manipulating texture coordinates. The switch operation works like turning a
437-- page. All pages are resized to fit the main area, so there are no resize
438-- cascades.
439local function book_resize(space, lin, evblock, wnd, cb)
440end
441
442local function book_added(space, wnd)
443end
444
445local function book_lost(space, wnd, destroy)
446end
447
448local function book_free(space)
449
450end
451
452-- ONLY REGISTRATION / MENU SETUP BOILERPLATE BELOW --
453
454local function copy(intbl)
455	local res = {};
456	for k,v in pairs(intbl) do res[k] = v; end
457	return res;
458end
459
460local book_layouter = {
461	resize = book_resize,
462	added = book_added,
463	lost = book_lost,
464	cleanup = book_free,
465	block_grow = true,
466	block_merge = true,
467	block_collapse = true,
468	block_swap = true
469};
470
471local centerscale_layouter = {
472	resize = center_resize,
473	added = center_added,
474	lost = center_lost,
475	cleanup = center_free,
476
477-- control all "normal" operations
478	block_grow = true,
479	block_merge = true,
480	block_collapse = true,
481	block_swap = true,
482	scaled = true,
483	block_rzevent = true
484};
485
486local center_layouter = {
487	resize = center_resize,
488	added = center_added,
489	lost = center_lost,
490	cleanup = center_free,
491
492-- control all "normal" operations
493	block_grow = true,
494	block_merge = true,
495	block_collapse = true,
496	block_swap = true,
497	scaled = false,
498	block_rzevent = false
499};
500
501local function set_layouter(space, layouter)
502	space.layouter = copy(layouter);
503	center_setup(space);
504	space:resize();
505	center_focus(space);
506end
507
508local layouters = {
509{
510	name = "center",
511	label = "Center Focus",
512	kind = "action",
513	handler = function()
514		set_layouter(active_display():active_space(), center_layouter);
515	end,
516},
517{
518	name = "center_scale",
519	label = "Center Focus (Force-Scale)",
520	kind = "action",
521	handler = function()
522		set_layouter(active_display():active_space(), centerscale_layouter);
523	end
524},
525{
526	name = "book",
527	label = "Book",
528	kind = "action",
529	eval = function()
530		return false;
531	end,
532	handler = function()
533		set_layouter(active_display():active_space(), book_layouter);
534	end
535},
536{
537	name = "none",
538	label = "Default",
539	kind = "action",
540	eval = function()
541		local d = active_display():active_space();
542		return d.layouter ~= nil;
543	end,
544	handler = function()
545		local space = active_display():active_space();
546		space.layouter.cleanup(space);
547	end
548}
549}
550
551-- recurse down the side columns and make their properties reflect side-cfg
552local function update_space(space)
553	for i,v in ipairs({1, 3}) do
554		local a = space.children[v];
555		while (a) do
556			side_imgcfg(a);
557			a = a.children[1];
558		end
559	end
560end
561
562-- add new config-db keys
563gconfig_register("autolay_sideopa", 0.5);
564gconfig_register("autolay_selswap", true);
565gconfig_register("autolay_centerw", 0.8);
566gconfig_register("autolay_sidetbar", true);
567gconfig_register("autolay_sideshdr", "noalpha");
568gconfig_register("autolay_shader", false);
569
570local function reconfig()
571	for tiler in all_tilers_iter() do
572-- for all workspaces that uses this layouter, rebuild the layout to
573-- reflect changes in weights, titlebar, etc.
574		for i=1, 10 do
575			if (tiler.spaces[i] and tiler.spaces[i].layouter and
576				tiler.spaces[i].layouter.scaled and
577				tiler.spaces[i].layouter.resize == center_resize) then
578				update_space(tiler.spaces[i]);
579			end
580		end
581	end
582end
583
584-- and triggers for config change
585gconfig_listen("autolay_sideshdr", "autolayshdr", reconfig);
586gconfig_listen("autolay_sidetbar", "autolaytb", reconfig);
587gconfig_listen("autolay_sideopa", "autolayh", reconfig);
588
589-- and menu entries
590local laycfg = {
591{
592	name = "sideopa",
593	label = "Side-Opacity(Scaled)",
594	kind = "value",
595	description = "Set the opacity that will be applied to side columns windows",
596	initial = function() return gconfig_get("autolay_sideopa"); end,
597	validator = gen_valid_float(0.0, 1.0),
598	handler = function(ctx, val)
599		gconfig_set("autolay_sideopa", tonumber(val));
600	end
601},
602{
603	name = "selswap",
604	label = "Mouse-SelectSwap",
605	kind = "value",
606	description = "Change swap-on-click behavior",
607	set = {LBL_YES, LBL_NO},
608	initial = function()
609		return gconfig_get("autolay_selswap") and LBL_YES or LBL_NO;
610	end,
611	handler = function(ctx, val)
612		gconfig_set("autolay_selswap", val == LBL_YES);
613	end
614},
615{
616	name = "centersz",
617	label = "Center Weight",
618	kind = "value",
619	description = "Control the display- relative center area allocation weight",
620	initial = function()
621		return gconfig_get("autolay_centerw");
622	end,
623	hint = "0.5 .. 0.9",
624	validator = gen_valid_float(0.5, 0.9),
625	handler = function(ctx, val)
626		gconfig_set("autolay_centerw", tonumber(val));
627	end
628},
629{
630	name = "sidetbar",
631	label = "Side Titlebar",
632	kind = "value",
633	description = "Control if the side columns windows should retain their titlebar or not",
634	set = {LBL_YES, LBL_NO},
635	initial = function()
636		return gconfig_get("autolay_sidetbar") and LBL_YES or LBL_NO;
637	end,
638	handler = function(ctx, val)
639		gconfig_set("autolay_sidetbar", val == LBL_YES);
640	end
641},
642{
643	name = "sideshader",
644	label = "Side Shader",
645	kind = "value",
646	description = "Control if the side columns will get special processing or not",
647	set = {LBL_YES, LBL_NO},
648	initial = function()
649		return gconfig_get("autolay_shader") and LBL_YES or LBL_NO;
650	end,
651	handler = function(ctx, val)
652		gconfig_set("autolay_shader", val == LBL_YES);
653	end
654},
655{
656	name = "sideshader_value",
657	label = "Side Shader-Select",
658	description = "Change the shader that will be used on entries in the side columns",
659	initial = function()
660		return gconfig_get("autolay_sideshdr");
661	end,
662	eval = function()
663		return gconfig_get("autolay_shader");
664	end,
665	kind = "value",
666	set = function() return shader_list({"effect", "simple"}); end,
667	handler = function(ctx, val)
668		local key, dom = shader_getkey(val, {"effect", "simple"});
669		if (key ~= nil) then
670			gconfig_set("autolay_sideshdr", key);
671		end
672	end
673}
674};
675
676menus_register("global", "settings/tools",
677{
678	name = "autolayouts",
679	label = "Auto Layouting",
680	submenu = true,
681	description = "Change the look and feel of the tiling autolayouter",
682	kind = "action",
683	handler = laycfg
684});
685
686menus_register("global", "tools",
687{
688	name = "autolayouts",
689	label = "Auto Layouting",
690	submenu = true,
691	description = "Heuristics for automatic management of tile workspaces",
692	kind = "action",
693	handler = layouters
694});
695
696menus_register("target", "window/swap",
697{
698	name = "swap",
699	label = "Swap(Focus)",
700	kind = "action",
701	description = "Swap the window with the focus area",
702	eval = function()
703		return active_display().selected and
704			active_display().selected.space.layouter;
705	end,
706	handler = function() swap_focus(); end
707});
708
709menus_register("target", "window/swap",
710{
711	name = "swap_sel",
712	label = "Swap-Select(Focus)",
713	kind = "action",
714	description = "Swap the window with the focus area, and select it",
715	eval = function()
716		return active_display().selected and
717			active_display().selected.space.layouter;
718	end,
719	handler = function()
720		swap_focus(true);
721	end
722});
723