]> git.decadent.org.uk Git - ion3.git/blob - mod_panews/mod_panews.lua
[svn-inject] Installing original source of ion3
[ion3.git] / mod_panews / mod_panews.lua
1 --
2 -- ion/mod_panews/mod_panews.lua
3 -- 
4 -- Copyright (c) Tuomo Valkonen 2004-2006.
5 -- 
6 -- Ion is free software; you can redistribute it and/or modify it under
7 -- the terms of the GNU Lesser General Public License as published by
8 -- the Free Software Foundation; either version 2.1 of the License, or
9 -- (at your option) any later version.
10 --
11
12
13 -- This is a slight abuse of the package.loaded variable perhaps, but
14 -- library-like packages should handle checking if they're loaded instead of
15 -- confusing the user with require/include differences.
16 if package.loaded["templates"] then return end
17
18 if not ioncore.load_module("mod_panews") then
19     return
20 end
21
22 assert(not _G["mod_panews"]);
23 local mod_panews={}
24 _G["mod_panews"]=mod_panews
25
26 local private={}
27 local settings={}
28
29 -- Settings {{{
30
31 -- Classes:
32 --  (T)erminal
33 --  (V)iewer
34 --  (M)isc
35 settings.valid_classifications={["V"]=true, ["T"]=true, ["M"]=true,}
36
37 -- Xterm, rxvt, aterm, etc. all have "XTerm" as class part of WM_CLASS
38 settings.terminal_emulators={["XTerm"]=true,} 
39
40 -- Pixel scale factor from 1280x1024/75dpi
41 settings.scalef=1.0
42
43 --settings.b_ratio=(1+math.sqrt(5))/2
44 --settings.s_ratio=1
45 settings.b_ratio=3
46 settings.s_ratio=2
47
48 settings.b_ratio2=7
49 settings.s_ratio2=1
50
51 settings.templates={}
52
53 settings.templates["default"]={
54     type="WSplitFloat",
55     dir="horizontal",
56     tls=settings.b_ratio,
57     brs=settings.s_ratio,
58     tls_brs_incl_handles=true,
59     tl={
60         type="WSplitPane",
61         contents={
62             type="WSplitFloat",
63             dir="vertical",
64             tls=settings.b_ratio2,
65             brs=settings.s_ratio2,
66             tls_brs_incl_handles=true,
67             tl={
68                 type="WSplitPane",
69                 marker="V:single",
70             },
71             br={
72                 type="WSplitPane",
73                 marker="M:right",
74             },
75         },
76     },
77     br={
78         type="WSplitPane",
79         marker="T:up",
80     },
81 }
82
83
84 settings.templates["alternative1"]={
85     type="WSplitFloat",
86     dir="horizontal",
87     tls=settings.s_ratio2,
88     brs=settings.b_ratio2,
89     tls_brs_incl_handles=true,
90     tl={
91         type="WSplitPane",
92         marker="M:down",
93     },
94     br={
95         type="WSplitFloat",
96         dir="vertical",
97         tls=settings.b_ratio,
98         brs=settings.s_ratio,
99         tls_brs_incl_handles=true,
100         tl={
101             type="WSplitPane",
102             marker="V:single",
103         },
104         br={
105             type="WSplitPane",
106             marker="T:right",
107         },
108     },
109 }
110
111
112 settings.templates["alternative2"]={
113     type="WSplitFloat",
114     dir="vertical",
115     tls=settings.b_ratio,
116     brs=settings.s_ratio,
117     tls_brs_incl_handles=true,
118     tl={
119         type="WSplitFloat",
120         dir="horizontal",
121         tls=settings.s_ratio2,
122         brs=settings.b_ratio2,
123         tls_brs_incl_handles=true,
124         tl={
125             type="WSplitPane",
126             marker="M:down",
127         },
128         br={
129             type="WSplitPane",
130             marker="V:single",
131         },
132     },
133     br={
134         type="WSplitPane",
135         marker="T:right",
136     },
137 }
138
139
140 settings.template=settings.templates["default"]
141
142 settings.shrink_minimum=32
143
144 --DOC
145 -- Set some module parameters. Currently \var{s} may contain the following
146 -- fields:
147 -- \begin{tabularx}{\linewidth}{lX}
148 --  \tabhead{Field & Description}
149 --  \var{template} & layout template for newly created \type{WPaneWS} 
150 --                   workspaces. This can be either a table or one of the 
151 --                   predefined layouts 'default', 'alternative1', and
152 --                   'alternative2'. \\
153 --  \var{scalef}   & Scale factor for classification heuristics to work 
154 --                   with different screen resolutions. The default is 1.0 
155 --                   and is designed for 1280x1024 at 75dpi. \\
156 --  \var{valid_classifications} & A table with valid window classifications
157 --                                as valid keys. \\
158 -- \end{tabularx}
159 function mod_panews.set(s)
160     if s.template then
161         local ok=false
162         if type(s.template)=="string" then
163             if settings.templates[s.template] then
164                 settings.template=settings.templates[s.template]
165                 ok=true
166             end
167         elseif type(s.template)=="table" then
168             settings.template=s.template
169             ok=true
170         end
171         if not ok then
172             ioncore.warn_traced("Invalid template.")
173         end
174     end
175     if s.scalef then
176         if type(s.scalef)~="number" or s.scalef<=0 then
177             ioncore.warn_traced('Invalid scale factor')
178         else
179             settings.scalef=s.scalef
180         end
181     end
182     
183     if type(s.valid_classifications)=="table" then
184         settings.valid_classifications=s.valid_classifications
185     end
186 end
187
188
189 --DOC
190 -- Get some module settings. See \fnref{mod_panews.set} for documentation
191 -- on the contents of the returned table.
192 function mod_panews.get()
193     return table.copy(settings, true)
194 end
195
196                         
197 -- }}}
198
199
200 -- Helper code {{{
201
202 local function sfind(s, p)
203     local function drop2(a, b, ...)
204         return unpack(arg)
205     end
206     return drop2(string.find(s, p))
207 end
208
209
210 function private.div_length(w, r1, r2)
211     local a=math.ceil(w*r1/(r1+r2))
212     return a, w-a
213 end
214
215 function private.split3(d, ls, cs, rs, lo, co, ro)
216     return {
217         tls = ls+cs,
218         brs = rs,
219         dir = d,
220         tl = {
221             tls = ls,
222             brs = cs,
223             dir = d,
224             tl = (lo or {}),
225             br = (co or {}),
226         },
227         br = (ro or {}),
228     }
229 end
230
231 function private.center3(d, ts, cs, lo, co, ro)
232     local sc=math.min(ts, cs)
233     local sl=math.floor((ts-sc)/2)
234     local sr=ts-sc-sl
235     local r=private.split3(d, sl, sc, sr, lo, co, ro)
236     return r
237 end
238
239 function private.split2(d, ts, ls, rs, lo, ro)
240     if ls and rs then
241         assert(ls+rs==ts)
242     elseif not ls then
243         ls=ts-rs
244     else
245         rs=ts-ls
246     end
247     assert(rs>=0 and ls>=0)
248     return {
249         type="WSplitSplit",
250         dir=d,
251         tls=math.floor(ls),
252         brs=math.ceil(rs),
253         tl=lo,
254         br=ro,
255     }
256 end
257
258 -- }}}
259
260
261 -- Classification {{{
262
263 function private.classify(ws, reg)
264     if obj_is(reg, "WClientWin") then
265         -- Check if there's a winprop override
266         local wp=ioncore.getwinprop(reg)
267         if wp and wp.panews_classification then
268             if settings.valid_classifications[wp.panews_classification] then
269                 return wp.panews_classification
270             end
271         end
272         
273         -- Handle known terminal emulators.
274         local id=reg:get_ident()
275         if settings.terminal_emulators[id.class] then
276             return "T"
277         end
278     end
279     
280     -- Try size heuristics.
281     local cg=reg:geom()
282
283     if cg.w<3/8*(1280*settings.scalef) then
284         return "M"
285     end
286     
287     if cg.h>4/8*(960*settings.scalef) then
288         return "V"
289     else
290         return "T"
291     end
292 end
293
294 -- }}}
295
296
297 -- Placement code {{{
298
299 local max_penalty=5
300 local just_some_pixel_count=16
301
302 function private.fitlevel_(min1, s1, us1, min2, s2, us2)
303     local p1, p2=4, 0.5
304     
305     if us2>=min2 then
306         p2=0
307     end
308     
309     if us1>=math.max(s1/2, min1) then
310         p1=0
311     elseif us1>=min1+just_some_pixel_count then
312         p1=1
313     elseif us1>=min1 then
314         p1=2
315     elseif us1>0 then
316         p1=3
317     end
318     
319     return p1+p2
320 end
321
322 function private.fitlevel(frame, node, horiz)
323     local fg=frame:geom()
324     local fsh=frame:size_hints()
325     local sg=node:geom()
326     if not horiz then
327         return private.fitlevel_(fsh.min_h, fg.h, sg.h, fsh.min_w, fg.w, sg.w)
328     else
329         return private.fitlevel_(fsh.min_w, fg.w, sg.w, fsh.min_h, fg.h, sg.h)
330     end
331 end
332
333
334 function private.use_unused(p, n, d, forcelevel)
335     local f=private.fitlevel(p.frame, n, (d=="left" or d=="right"))
336     if f>forcelevel then
337         return false, f
338     end
339
340     if d=="single" then
341         p.res_node=n
342         p.res_config={reg=p.frame}
343         return true, f
344     end
345
346     -- TODO: Check fit
347     local sg=n:geom()
348     local fg=p.frame:geom()
349     
350     if d=="up" or d=="down" then
351         p.res_node=n
352         local fh=math.min(fg.h, sg.h)
353         if d=="up" then
354             p.res_config=private.split2("vertical", sg.h, nil, fh,
355                                   {}, {reg=p.frame})
356         else
357             p.res_config=private.split2("vertical", sg.h, fh, nil,
358                                   {reg=p.frame}, {})
359         end
360         return true, f
361     elseif d=="left" or d=="right" then
362         p.res_node=n
363         local fw=math.min(fg.w, sg.w)
364         if d=="left" then
365             p.res_config=private.split2("horizontal", sg.w, nil, fw,
366                                   {}, {reg=p.frame})
367         else
368             p.res_config=private.split2("horizontal", sg.w, fw, nil,
369                                   {reg=p.frame}, {})
370         end
371         return true, f
372     end
373     
374     return false, f
375 end
376
377
378 function private.scan_pane(p, node, d)
379     local function do_scan_active(n, uf)
380         local t=obj_typename(n)
381         if t=="WSplitRegion" then
382             local f=private.fitlevel(p.frame, n, (d=="left" or d=="right"))
383             if f<uf then
384                 p.res_node=n
385                 return true
386             end
387         elseif t=="WSplitSplit" or t=="WSplitFloat" then
388             local a, b=n:tl(), n:br()
389             if b==n:current() then
390                 a, b=b, a
391             end
392             return (do_scan_active(a, uf) or do_scan_active(b, uf))
393         end
394         return false
395     end
396
397     local function do_scan_unused(n, forcelevel)
398         local t=obj_typename(n)
399         if t=="WSplitSplit" or t=="WSplitFloat" then
400             local sd=n:dir()
401             local a, b
402             if (d=="up" and sd=="vertical") or (d=="left" and sd=="horizontal") then
403                 a, b=n:tl(), n:br()
404             elseif (d=="down" and sd=="vertical") or (d=="right" and sd=="horizontal") then
405                 a, b=n:br(), n:tl()
406             else
407                 a, b=n:current(), (n:current()==n:tl() and n:br() or n:tl())
408             end
409
410             local ok, f, f2
411             ok, f=do_scan_unused(a, forcelevel)
412             if not ok then
413                 ok, f2=do_scan_unused(b, forcelevel)
414             end
415             return ok, math.min(f, f2 or max_penalty)
416         elseif t=="WSplitUnused" then
417             -- Found it
418             return private.use_unused(p, n, d, forcelevel)
419         end
420         return false, max_penalty
421     end
422     
423     local ok, fitlevel=do_scan_unused(node, 0)
424     if not ok then
425         ok=do_scan_active(node, fitlevel)
426         if not ok then
427             ok=do_scan_unused(node, 3)
428         end
429     end
430     return ok
431 end
432     
433
434 function private.make_placement(p)
435     if p.specifier then
436         local n=p.specifier
437         local pcls, pdir
438         
439         while n and not obj_is(n, "WSplitPane") do
440             n=n:parent()
441         end
442         
443         if n then
444             pcls, pdir=sfind((n:marker() or ""), "(.*):(.*)")
445         end
446             
447         return private.use_unused(p, p.specifier, (pdir or "single"), 2)
448     end
449     
450     local cls=private.classify(p.ws, p.reg)
451     
452     local function do_scan_cls(n)
453         local t=obj_typename(n)
454         if t=="WSplitPane" then
455             local m=n:marker()
456             if m then
457                 local pcls, pdir=sfind(m, "(.*):(.*)")
458                 if pcls and pcls==cls then
459                     return private.scan_pane(p, n:contents(), pdir)
460                 end
461             else
462                 return do_scan_cls(n:contents())
463             end
464         elseif t=="WSplitSplit" or t=="WSplitFloat" then
465             return (do_scan_cls(n:tl()) or do_scan_cls(n:br()))
466         end
467     end
468     
469     local function do_scan_fallback(n)
470         local t=obj_typename(n)
471         if t=="WSplitUnused" then
472             p.res_node=n
473             p.res_config={reg=p.frame}
474             return true
475         elseif t=="WSplitRegion" then
476             p.res_node=n
477             return true
478         elseif t=="WSplitPane" then
479             return do_scan_fallback(n:contents())
480         elseif t=="WSplitSplit" or t=="WSplitFloat" then
481             return (do_scan_fallback(n:tl()) or do_scan_fallback(n:br()))
482         end
483     end
484     
485     local node=p.ws:split_tree()
486     return (do_scan_cls(node) or do_scan_fallback(node))
487 end
488
489 -- }}}
490
491
492 -- Layout initialisation {{{
493
494
495 function private.calc_sizes(tmpl, sw, sh)
496     tmpl._w, tmpl._h=sw, sh
497
498     if tmpl.type=="WSplitSplit" or tmpl.type=="WSplitFloat" then
499         local tmps, tlw, tlh, brw, brh
500         -- Calculate pixel sizes of things
501         if tmpl.dir=="vertical" then
502             tmps=sh
503         else
504             tmps=sw
505         end
506         
507         tmpl.tls, tmpl.brs=private.div_length(tmps, tmpl.tls, tmpl.brs)
508     
509         if tmpl.dir=="vertical" then
510             tlw, brw=sw, sw
511             tlh, brh=tmpl.tls, tmpl.brs
512         else
513             tlw, brw=tmpl.tls, tmpl.brs
514             tlh, brh=sh, sh
515         end
516     
517         private.calc_sizes(tmpl.tl, tlw, tlh)
518         private.calc_sizes(tmpl.br, brw, brh)
519     end
520 end
521
522
523 function private.init_layout(p)
524     p.layout=table.copy(settings.template, true) -- deep copy template
525     local wg=p.ws:geom()
526     private.calc_sizes(p.layout, wg.w, wg.h)
527     return true
528 end
529
530 -- }}}
531
532
533 -- Initialisation {{{
534
535 function private.setup_hooks()
536     local function hookto(hkname, fn)
537         local hk=ioncore.get_hook(hkname)
538         if not hk then
539             error("No hook "..hkname)
540         end
541         if not hk:add(fn) then
542             error("Unable to hook to "..hkname)
543         end
544     end
545
546     hookto("panews_init_layout_alt", private.init_layout)
547     hookto("panews_make_placement_alt", private.make_placement)
548 end
549
550
551 private.setup_hooks()
552
553 -- }}}
554
555
556 -- Mark ourselves loaded.
557 package.loaded["templates"]=true
558
559
560 -- Load configuration file
561 dopath('cfg_panews', false)
562