window.lua

1
------------------
2
-- Window class --
3
------------------
4
 
5
-- Window class table
6
window = {}
7
 
8
-- Widget construction aliases
9
local function entry()    return widget{type="entry"}    end
10
local function eventbox() return widget{type="eventbox"} end
11
local function hbox()     return widget{type="hbox"}     end
12
local function label()    return widget{type="label"}    end
13
local function notebook() return widget{type="notebook"} end
14
local function vbox()     return widget{type="vbox"}     end
15
 
16
-- Build and pack window widgets
17
function window.build()
18
    -- Create a table for widgets and state variables for a window
19
    local w = {
20
        win    = widget{type="window"},
21
        ebox   = eventbox(),
22
        layout = vbox(),
23
        tabs   = notebook(),
24
        -- Tab bar widgets
25
        tbar = {
26
            layout = hbox(),
27
            ebox   = eventbox(),
28
            titles = { },
29
        },
30
        -- Status bar widgets
31
        sbar = {
32
            layout = hbox(),
33
            ebox   = eventbox(),
34
            -- Left aligned widgets
35
            l = {
36
                layout = hbox(),
37
                ebox   = eventbox(),
38
                uri    = label(),
39
                loaded = label(),
40
            },
41
            -- Fills space between the left and right aligned widgets
42
            sep = eventbox(),
43
            -- Right aligned widgets
44
            r = {
45
                layout = hbox(),
46
                ebox   = eventbox(),
47
                buf    = label(),
48
                ssl    = label(),
49
                tabi   = label(),
50
                scroll = label(),
51
            },
52
        },
53
        -- Input bar widgets
54
        ibar = {
55
            layout  = hbox(),
56
            ebox    = eventbox(),
57
            prompt  = label(),
58
            input   = entry(),
59
        },
60
    }
61
 
62
    -- Assemble window
63
    w.ebox:set_child(w.layout)
64
    w.win:set_child(w.ebox)
65
 
66
    -- Pack tab bar
67
    local t = w.tbar
68
    t.ebox:set_child(t.layout, false, false, 0)
69
    w.layout:pack_start(t.ebox, false, false, 0)
70
 
71
    -- Pack notebook
72
    w.layout:pack_start(w.tabs, true, true, 0)
73
 
74
    -- Pack left-aligned statusbar elements
75
    local l = w.sbar.l
76
    l.layout:pack_start(l.uri,    false, false, 0)
77
    l.layout:pack_start(l.loaded, false, false, 0)
78
    l.ebox:set_child(l.layout)
79
 
80
    -- Pack right-aligned statusbar elements
81
    local r = w.sbar.r
82
    r.layout:pack_start(r.buf,    false, false, 0)
83
    r.layout:pack_start(r.ssl,    false, false, 0)
84
    r.layout:pack_start(r.tabi,   false, false, 0)
85
    r.layout:pack_start(r.scroll, false, false, 0)
86
    r.ebox:set_child(r.layout)
87
 
88
    -- Pack status bar elements
89
    local s = w.sbar
90
    s.layout:pack_start(l.ebox,   false, false, 0)
91
    s.layout:pack_start(s.sep,    true,  true,  0)
92
    s.layout:pack_start(r.ebox,   false, false, 0)
93
    s.ebox:set_child(s.layout)
94
    w.layout:pack_start(s.ebox,   false, false, 0)
95
 
96
    -- Pack input bar
97
    local i = w.ibar
98
    i.layout:pack_start(i.prompt, false, false, 0)
99
    i.layout:pack_start(i.input,  true,  true,  0)
100
    i.ebox:set_child(i.layout)
101
    w.layout:pack_start(i.ebox,    false, false, 0)
102
 
103
    -- Other settings
104
    i.input.show_frame = false
105
    w.tabs.show_tabs = false
106
    l.loaded:hide()
107
    l.uri.selectable = true
108
    r.ssl:hide()
109
 
110
    return w
111
end
112
 
113
-- Table of functions to call on window creation. Normally used to add signal
114
-- handlers to the new windows widgets.
115
window.init_funcs = {
116
    -- Attach notebook widget signals
117
    notebook_signals = function (w)
118
        w.tabs:add_signal("page-added", function (nbook, view, idx)
119
            w:update_tab_count(idx)
120
            w:update_tab_labels()
121
        end)
122
        w.tabs:add_signal("switch-page", function (nbook, view, idx)
123
            w:set_mode()
124
            w:update_tab_count(idx)
125
            w:update_win_title(view)
126
            w:update_uri(view)
127
            w:update_progress(view)
128
            w:update_tab_labels(idx)
129
            w:update_buf()
130
            w:update_ssl(view)
131
        end)
132
    end,
133
 
134
    last_win_check = function (w)
135
        w.win:add_signal("destroy", function ()
136
            -- call the quit function if this was the last window left
137
            if #luakit.windows == 0 then luakit.quit() end
138
        end)
139
    end,
140
 
141
    key_press_match = function (w)
142
        w.win:add_signal("key-press", function (_, mods, key)
143
            -- Reset command line completion
144
            if w:get_mode() == "command" and key ~= "Tab" and w.compl_start then
145
                w:update_uri()
146
                w.compl_index = 0
147
            end
148
 
149
            if w:hit(mods, key) then
150
                return true
151
            end
152
        end)
153
    end,
154
 
155
    apply_window_theme = function (w)
156
        local theme = lousy.theme.get()
157
        local s, i  = w.sbar, w.ibar
158
 
159
        -- Set foregrounds
160
        for wi, v in pairs({
161
            [s.l.uri]    = theme.uri_sbar_fg,
162
            [s.l.loaded] = theme.loaded_sbar_fg,
163
            [s.r.buf]    = theme.buf_sbar_fg,
164
            [s.r.tabi]   = theme.tabi_sbar_fg,
165
            [s.r.scroll] = theme.scroll_sbar_fg,
166
            [i.prompt]   = theme.prompt_ibar_fg,
167
            [i.input]    = theme.input_ibar_fg,
168
        }) do wi.fg = v end
169
 
170
        -- Set backgrounds
171
        for wi, v in pairs({
172
            [s.l.ebox]   = theme.sbar_bg,
173
            [s.r.ebox]   = theme.sbar_bg,
174
            [s.sep]      = theme.sbar_bg,
175
            [s.ebox]     = theme.sbar_bg,
176
            [i.ebox]     = theme.ibar_bg,
177
            [i.input]    = theme.input_ibar_bg,
178
        }) do wi.bg = v end
179
 
180
        -- Set fonts
181
        for wi, v in pairs({
182
            [s.l.uri]    = theme.uri_sbar_font,
183
            [s.l.loaded] = theme.loaded_sbar_font,
184
            [s.r.buf]    = theme.buf_sbar_font,
185
            [s.r.ssl]    = theme.ssl_sbar_font,
186
            [s.r.tabi]   = theme.tabi_sbar_font,
187
            [s.r.scroll] = theme.scroll_sbar_font,
188
            [i.prompt]   = theme.prompt_ibar_font,
189
            [i.input]    = theme.input_ibar_font,
190
        }) do wi.font = v end
191
    end,
192
}
193
 
194
-- Helper functions which operate on the window widgets or structure.
195
window.methods = {
196
    -- Return the widget in the currently active tab
197
    get_current = function (w)       return w.tabs:atindex(w.tabs:current())       end,
198
    -- Check if given widget is the widget in the currently active tab
199
    is_current  = function (w, wi)   return w.tabs:indexof(wi) == w.tabs:current() end,
200
 
201
    get_tab_title = function (w, view)
202
        if not view then view = w:get_current() end
203
        return view:get_prop("title") or view.uri or "(Untitled)"
204
    end,
205
 
206
    new_tab = function (w, uri)
207
        local view = webview.new(w, uri)
208
        w.tabs:append(view)
209
        w:update_tab_count()
210
        return view
211
    end,
212
 
213
    -- Wrapper around the bind plugin's hit method
214
    hit = function (w, mods, key)
215
        local caught, newbuf = lousy.bind.hit(w.binds or {}, mods, key, w.buffer, false, w)
216
        if w.win then
217
            w.buffer = newbuf
218
            w:update_buf()
219
        end
220
        return caught
221
    end,
222
 
223
    -- Wrapper around the bind plugin's match_cmd method
224
    match_cmd = function (w, buffer)
225
        return lousy.bind.match_cmd(binds.commands, buffer, w)
226
    end,
227
 
228
    -- enter command or characters into command line
229
    enter_cmd = function (w, cmd)
230
        local i = w.ibar.input
231
        w:set_mode("command")
232
        i.text = cmd
233
        i:set_position(-1)
234
    end,
235
 
236
    -- insert a string into the command line at the current cursor position
237
    insert_cmd = function (w, str)
238
        if not str then return end
239
        local i = w.ibar.input
240
        local text = i.text
241
        local pos = i:get_position()
242
        local left, right = string.sub(text, 1, pos), string.sub(text, pos+1)
243
        i.text = left .. str .. right
244
        i:set_position(pos + #str + 1)
245
    end,
246
 
247
    -- Command line completion of available commands
248
    cmd_completion = function (w)
249
        local i = w.ibar.input
250
        local s = w.sbar.l.uri
251
        local cmpl = {}
252
 
253
        -- Get last completion (is reset on key press other than <Tab>)
254
        if not w.compl_start or w.compl_index == 0 then
255
            w.compl_start = "^" .. string.sub(i.text, 2)
256
            w.compl_index = 1
257
        end
258
 
259
        -- Get suitable commands
260
        for _, b in ipairs(binds.commands) do
261
            for _, c in pairs(b.commands) do
262
                if c and string.match(c, w.compl_start) then
263
                    table.insert(cmpl, c)
264
                end
265
            end
266
        end
267
 
268
        table.sort(cmpl)
269
 
270
        if #cmpl > 0 then
271
            local text = ""
272
            for index, comp in pairs(cmpl) do
273
                if index == w.compl_index then
274
                    i.text = ":" .. comp .. " "
275
                    i:set_position(-1)
276
                end
277
                if text ~= "" then
278
                    text = text .. " | "
279
                end
280
                text = text .. comp
281
            end
282
 
283
            -- cycle through all possible completions
284
            if w.compl_index == #cmpl then
285
                w.compl_index = 1
286
            else
287
                w.compl_index = w.compl_index + 1
288
            end
289
            s.text = lousy.util.escape(text)
290
        end
291
    end,
292
 
293
    del_word = function (w)
294
        local i = w.ibar.input
295
        local text = i.text
296
        local pos = i:get_position()
297
        if text and #text > 1 and pos > 1 then
298
            local left, right = string.sub(text, 2, pos), string.sub(text, pos+1)
299
            if not string.find(left, "%s") then
300
                left = ""
301
            elseif string.find(left, "%w+%s*$") then
302
                left = string.sub(left, 0, string.find(left, "%w+%s*$") - 1)
303
            elseif string.find(left, "%W+%s*$") then
304
                left = string.sub(left, 0, string.find(left, "%W+%s*$") - 1)
305
            end
306
            i.text =  string.sub(text, 1, 1) .. left .. right
307
            i:set_position(#left + 2)
308
        end
309
    end,
310
 
311
    del_line = function (w)
312
        local i = w.ibar.input
313
        if i.text ~= ":" then
314
            i.text = ":"
315
            i:set_position(-1)
316
        end
317
    end,
318
 
319
    -- Search history adding
320
    srch_hist_add = function (w, srch)
321
        if not w.srch_hist then w.srch_hist = {} end
322
        -- Check overflow
323
        local max_hist = globals.max_srch_history or 100
324
        if #w.srch_hist > (max_hist + 5) then
325
            while #w.srch_hist > max_hist do
326
                table.remove(w.srch_hist, 1)
327
            end
328
        end
329
        table.insert(w.srch_hist, srch)
330
    end,
331
 
332
    -- Search history traversing
333
    srch_hist_prev = function (w)
334
        if not w.srch_hist then w.srch_hist = {} end
335
        if not w.srch_hist_cursor then
336
            w.srch_hist_cursor = #w.srch_hist + 1
337
            w.srch_hist_current = w.ibar.input.text
338
        end
339
        local c = w.srch_hist_cursor - 1
340
        if w.srch_hist[c] then
341
            w.srch_hist_cursor = c
342
            w.ibar.input.text = w.srch_hist[c]
343
            w.ibar.input:set_position(-1)
344
        end
345
    end,
346
 
347
    srch_hist_next = function (w)
348
        if not w.srch_hist then w.srch_hist = {} end
349
        local c = (w.srch_hist_cursor or #w.srch_hist) + 1
350
        if w.srch_hist[c] then
351
            w.srch_hist_cursor = c
352
            w.ibar.input.text = w.srch_hist[c]
353
            w.ibar.input:set_position(-1)
354
        elseif w.srch_hist_current then
355
            w.srch_hist_cursor = nil
356
            w.ibar.input.text = w.srch_hist_current
357
            w.ibar.input:set_position(-1)
358
        end
359
    end,
360
 
361
    -- Command history adding
362
    cmd_hist_add = function (w, cmd)
363
        if not w.cmd_hist then w.cmd_hist = {} end
364
        -- Make sure history doesn't overflow
365
        local max_hist = globals.max_cmd_hist or 100
366
        if #w.cmd_hist > (max_hist + 5) then
367
            while #w.cmd_hist > max_hist do
368
                table.remove(w.cmd_hist, 1)
369
            end
370
        end
371
        table.insert(w.cmd_hist, cmd)
372
    end,
373
 
374
    -- Command history traversing
375
    cmd_hist_prev = function (w)
376
        if not w.cmd_hist then w.cmd_hist = {} end
377
        if not w.cmd_hist_cursor then
378
            w.cmd_hist_cursor = #w.cmd_hist + 1
379
            w.cmd_hist_current = w.ibar.input.text
380
        end
381
        local c = w.cmd_hist_cursor - 1
382
        if w.cmd_hist[c] then
383
            w.cmd_hist_cursor = c
384
            w.ibar.input.text = w.cmd_hist[c]
385
            w.ibar.input:set_position(-1)
386
        end
387
    end,
388
 
389
    cmd_hist_next = function (w)
390
        if not w.cmd_hist then w.cmd_hist = {} end
391
        local c = (w.cmd_hist_cursor or #w.cmd_hist) + 1
392
        if w.cmd_hist[c] then
393
            w.cmd_hist_cursor = c
394
            w.ibar.input.text = w.cmd_hist[c]
395
            w.ibar.input:set_position(-1)
396
        elseif w.cmd_hist_current then
397
            w.cmd_hist_cursor = nil
398
            w.ibar.input.text = w.cmd_hist_current
399
            w.ibar.input:set_position(-1)
400
        end
401
    end,
402
 
403
    -- GUI content update functions
404
    update_tab_count = function (w, i, t)
405
        w.sbar.r.tabi.text = string.format("[%d/%d]", i or w.tabs:current(), t or w.tabs:count())
406
    end,
407
 
408
    update_win_title = function (w, view)
409
        if not view then view = w:get_current() end
410
        local title = view:get_prop("title")
411
        local uri = view.uri
412
        if not title and not uri then
413
            w.win.title = "luakit"
414
        else
415
            w.win.title = (title or "luakit") .. " - " .. (uri or "about:blank")
416
        end
417
    end,
418
 
419
    update_uri = function (w, view, uri)
420
        if not view then view = w:get_current() end
421
        w.sbar.l.uri.text = lousy.util.escape((uri or (view and view.uri) or "about:blank"))
422
    end,
423
 
424
    update_progress = function (w, view, p)
425
        if not view then view = w:get_current() end
426
        if not p then p = view:get_prop("progress") end
427
        local loaded = w.sbar.l.loaded
428
        if not view:loading() or p == 1 then
429
            loaded:hide()
430
        else
431
            loaded:show()
432
            loaded.text = string.format("(%d%%)", p * 100)
433
        end
434
    end,
435
 
436
    update_scroll = function (w, view)
437
        if not view then view = w:get_current() end
438
        local scroll = w.sbar.r.scroll
439
        if view then
440
            local val, max = view:get_scroll_vert()
441
            if max == 0 then val = "All"
442
            elseif val == 0 then val = "Top"
443
            elseif val == max then val = "Bot"
444
            else val = string.format("%2d%%", (val/max) * 100)
445
            end
446
            if scroll.text ~= val then scroll.text = val end
447
            scroll:show()
448
        else
449
            scroll:hide()
450
        end
451
    end,
452
 
453
    update_ssl = function (w, view)
454
        if not view then view = w:get_current() end
455
        local trusted = view:ssl_trusted()
456
        local theme = lousy.theme.get()
457
        local ssl = w.sbar.r.ssl
458
        if trusted ~= nil and not w.checking_ssl then
459
            ssl.fg = theme.notrust_fg
460
            ssl.text = "(nocheck)"
461
            ssl:show()
462
        elseif trusted == true then
463
            ssl.fg = theme.trust_fg
464
            ssl.text = "(trust)"
465
            ssl:show()
466
        elseif trusted == false then
467
            ssl.fg = theme.notrust_fg
468
            ssl.text = "(notrust)"
469
            ssl:show()
470
        else
471
            ssl:hide()
472
        end
473
    end,
474
 
475
    update_buf = function (w)
476
        local buf = w.sbar.r.buf
477
        if w.buffer then
478
            buf.text = lousy.util.escape(string.format(" %-3s", w.buffer))
479
            buf:show()
480
        else
481
            buf:hide()
482
        end
483
    end,
484
 
485
    update_binds = function (w, mode)
486
        -- Generate the list of active key & buffer binds for this mode
487
        w.binds = lousy.util.table.join(binds.mode_binds[mode], binds.mode_binds.all)
488
        -- Clear & hide buffer
489
        w.buffer = nil
490
        w:update_buf()
491
    end,
492
 
493
    -- Tab label functions
494
    -- TODO: Move these functions into a module (I.e. lousy.widget.tablist)
495
    make_tab_label = function (w, pos)
496
        local theme = lousy.theme.get()
497
        local t = {
498
            label  = label(),
499
            sep    = eventbox(),
500
            ebox   = eventbox(),
501
            layout = hbox(),
502
        }
503
        t.label.font = theme.tab_font
504
        t.label:set_width(1)
505
        t.layout:pack_start(t.label, true,  true, 0)
506
        t.layout:pack_start(t.sep,   false,  false, 0)
507
        t.ebox:set_child(t.layout)
508
        t.ebox:add_signal("button-release", function (e, m, b)
509
            if b == 1 then
510
                w.tabs:switch(pos)
511
                return true
512
            elseif b == 2 then
513
                w:close_tab(w.tabs:atindex(pos))
514
                return true
515
            end
516
        end)
517
        return t
518
    end,
519
 
520
    destroy_tab_label = function (w, t)
521
        if not t then t = table.remove(w.tbar.titles) end
522
        -- Destroy widgets without their own windows first (I.e. labels)
523
        for _, wi in ipairs{ t.label, t.sep, t.ebox, t.layout } do wi:destroy() end
524
    end,
525
 
526
    update_tab_labels = function (w, current)
527
        local tb = w.tbar
528
        local count, current = w.tabs:count(), current or w.tabs:current()
529
        tb.ebox:hide()
530
 
531
        -- Leave the tablist hidden if there is only one tab open
532
        if count <= 1 then
533
            return nil
534
        end
535
 
536
        if count ~= #tb.titles then
537
            -- Grow the number of labels
538
            while count > #tb.titles do
539
                local t = w:make_tab_label(#tb.titles + 1)
540
                tb.layout:pack_start(t.ebox, true, true,  0)
541
                table.insert(tb.titles, t)
542
            end
543
            -- Prune number of labels
544
            while count < #tb.titles do
545
                w:destroy_tab_label()
546
            end
547
        end
548
 
549
        if count ~= 0 then
550
            for i = 1, count do
551
                local view = w.tabs:atindex(i)
552
                local t = tb.titles[i]
553
                local title = " " ..i.. " "..w:get_tab_title(view)
554
                t.label.text = lousy.util.escape(string.format("%-40s", title))
555
                w:apply_tablabel_theme(t, i == current)
556
            end
557
        end
558
        tb.ebox:show()
559
    end,
560
 
561
    -- Theme functions
562
    apply_tablabel_theme = function (w, t, selected)
563
        local theme = lousy.theme.get()
564
        selected = (selected and "_selected") or ""
565
        t.label.fg = theme[string.format("tab%s_fg", selected)]
566
        t.ebox.bg = theme[string.format("tab%s_bg", selected)]
567
    end,
568
 
569
    close_win = function (w)
570
        -- Close all tabs
571
        while w.tabs:count() ~= 0 do
572
            w:close_tab()
573
        end
574
 
575
        -- Recursively remove widgets from window
576
        local children = lousy.util.recursive_remove(w.win)
577
        -- Destroy all widgets
578
        for i, c in ipairs(lousy.util.table.join(children, {w.win})) do
579
            if c.hide then c:hide() end
580
            c:destroy()
581
        end
582
 
583
        -- Clear window struct
584
        w = setmetatable(w, {})
585
        for k, _ in pairs(w) do w[k] = nil end
586
 
587
        -- Quit if closed last window
588
        if #luakit.windows == 0 then luakit.quit() end
589
    end,
590
}
591
 
592
-- Ordered list of class index functions. Other classes (E.g. webview) are able
593
-- to add their own index functions to this list.
594
window.indexes = {
595
    -- Find function in window.methods first
596
    function (w, k) return window.methods[k] end
597
}
598
 
599
-- Create new window
600
function window.new(uris)
601
    local w = window.build()
602
 
603
    -- Set window metatable
604
    setmetatable(w, {
605
        __index = function (_, k)
606
            -- Check widget structure first
607
            local v = rawget(w, k)
608
            if v then return v end
609
            -- Call each window index function
610
            for _, index in ipairs(window.indexes) do
611
                v = index(w, k)
612
                if v then return v end
613
            end
614
        end,
615
    })
616
 
617
    -- Call window init functions
618
    for _, func in pairs(window.init_funcs) do
619
        func(w)
620
    end
621
 
622
    -- Populate notebook with tabs
623
    for _, uri in ipairs(uris or {}) do
624
        w:new_tab(uri)
625
    end
626
 
627
    -- Make sure something is loaded
628
    if w.tabs:count() == 0 then
629
        w:new_tab(globals.homepage)
630
    end
631
 
632
    -- Set initial mode
633
    w:set_mode()
634
 
635
    return w
636
end
637
 
638
-- vim: et:sw=4:ts=8:sts=4:tw=80