------------------
-- Window class --
------------------
-- Window class table
window = {}
-- Widget construction aliases
local function entry() return widget{type="entry"} end
local function eventbox() return widget{type="eventbox"} end
local function hbox() return widget{type="hbox"} end
local function label() return widget{type="label"} end
local function notebook() return widget{type="notebook"} end
local function vbox() return widget{type="vbox"} end
-- Build and pack window widgets
function window.build()
-- Create a table for widgets and state variables for a window
local w = {
win = widget{type="window"},
ebox = eventbox(),
layout = vbox(),
tabs = notebook(),
-- Tab bar widgets
tbar = {
layout = hbox(),
ebox = eventbox(),
titles = { },
},
-- Status bar widgets
sbar = {
layout = hbox(),
ebox = eventbox(),
-- Left aligned widgets
l = {
layout = hbox(),
ebox = eventbox(),
uri = label(),
loaded = label(),
},
-- Fills space between the left and right aligned widgets
sep = eventbox(),
-- Right aligned widgets
r = {
layout = hbox(),
ebox = eventbox(),
buf = label(),
ssl = label(),
tabi = label(),
scroll = label(),
},
},
-- Input bar widgets
ibar = {
layout = hbox(),
ebox = eventbox(),
prompt = label(),
input = entry(),
},
}
-- Assemble window
w.ebox:set_child(w.layout)
w.win:set_child(w.ebox)
-- Pack tab bar
local t = w.tbar
t.ebox:set_child(t.layout, false, false, 0)
w.layout:pack_start(t.ebox, false, false, 0)
-- Pack notebook
w.layout:pack_start(w.tabs, true, true, 0)
-- Pack left-aligned statusbar elements
local l = w.sbar.l
l.layout:pack_start(l.uri, false, false, 0)
l.layout:pack_start(l.loaded, false, false, 0)
l.ebox:set_child(l.layout)
-- Pack right-aligned statusbar elements
local r = w.sbar.r
r.layout:pack_start(r.buf, false, false, 0)
r.layout:pack_start(r.ssl, false, false, 0)
r.layout:pack_start(r.tabi, false, false, 0)
r.layout:pack_start(r.scroll, false, false, 0)
r.ebox:set_child(r.layout)
-- Pack status bar elements
local s = w.sbar
s.layout:pack_start(l.ebox, false, false, 0)
s.layout:pack_start(s.sep, true, true, 0)
s.layout:pack_start(r.ebox, false, false, 0)
s.ebox:set_child(s.layout)
w.layout:pack_start(s.ebox, false, false, 0)
-- Pack input bar
local i = w.ibar
i.layout:pack_start(i.prompt, false, false, 0)
i.layout:pack_start(i.input, true, true, 0)
i.ebox:set_child(i.layout)
w.layout:pack_start(i.ebox, false, false, 0)
-- Other settings
i.input.show_frame = false
w.tabs.show_tabs = false
l.loaded:hide()
l.uri.selectable = true
r.ssl:hide()
return w
end
-- Table of functions to call on window creation. Normally used to add signal
-- handlers to the new windows widgets.
window.init_funcs = {
-- Attach notebook widget signals
notebook_signals = function (w)
w.tabs:add_signal("page-added", function (nbook, view, idx)
w:update_tab_count(idx)
w:update_tab_labels()
end)
w.tabs:add_signal("switch-page", function (nbook, view, idx)
w:set_mode()
w:update_tab_count(idx)
w:update_win_title(view)
w:update_uri(view)
w:update_progress(view)
w:update_tab_labels(idx)
w:update_buf()
w:update_ssl(view)
end)
end,
last_win_check = function (w)
w.win:add_signal("destroy", function ()
-- call the quit function if this was the last window left
if #luakit.windows == 0 then luakit.quit() end
end)
end,
key_press_match = function (w)
w.win:add_signal("key-press", function (_, mods, key)
-- Reset command line completion
if w:get_mode() == "command" and key ~= "Tab" and w.compl_start then
w:update_uri()
w.compl_index = 0
end
if w:hit(mods, key) then
return true
end
end)
end,
apply_window_theme = function (w)
local theme = lousy.theme.get()
local s, i = w.sbar, w.ibar
-- Set foregrounds
for wi, v in pairs({
[s.l.uri] = theme.uri_sbar_fg,
[s.l.loaded] = theme.loaded_sbar_fg,
[s.r.buf] = theme.buf_sbar_fg,
[s.r.tabi] = theme.tabi_sbar_fg,
[s.r.scroll] = theme.scroll_sbar_fg,
[i.prompt] = theme.prompt_ibar_fg,
[i.input] = theme.input_ibar_fg,
}) do wi.fg = v end
-- Set backgrounds
for wi, v in pairs({
[s.l.ebox] = theme.sbar_bg,
[s.r.ebox] = theme.sbar_bg,
[s.sep] = theme.sbar_bg,
[s.ebox] = theme.sbar_bg,
[i.ebox] = theme.ibar_bg,
[i.input] = theme.input_ibar_bg,
}) do wi.bg = v end
-- Set fonts
for wi, v in pairs({
[s.l.uri] = theme.uri_sbar_font,
[s.l.loaded] = theme.loaded_sbar_font,
[s.r.buf] = theme.buf_sbar_font,
[s.r.ssl] = theme.ssl_sbar_font,
[s.r.tabi] = theme.tabi_sbar_font,
[s.r.scroll] = theme.scroll_sbar_font,
[i.prompt] = theme.prompt_ibar_font,
[i.input] = theme.input_ibar_font,
}) do wi.font = v end
end,
}
-- Helper functions which operate on the window widgets or structure.
window.methods = {
-- Return the widget in the currently active tab
get_current = function (w) return w.tabs:atindex(w.tabs:current()) end,
-- Check if given widget is the widget in the currently active tab
is_current = function (w, wi) return w.tabs:indexof(wi) == w.tabs:current() end,
get_tab_title = function (w, view)
if not view then view = w:get_current() end
return view:get_prop("title") or view.uri or "(Untitled)"
end,
new_tab = function (w, uri)
local view = webview.new(w, uri)
w.tabs:append(view)
w:update_tab_count()
return view
end,
-- Wrapper around the bind plugin's hit method
hit = function (w, mods, key)
local caught, newbuf = lousy.bind.hit(w.binds or {}, mods, key, w.buffer, false, w)
if w.win then
w.buffer = newbuf
w:update_buf()
end
return caught
end,
-- Wrapper around the bind plugin's match_cmd method
match_cmd = function (w, buffer)
return lousy.bind.match_cmd(binds.commands, buffer, w)
end,
-- enter command or characters into command line
enter_cmd = function (w, cmd)
local i = w.ibar.input
w:set_mode("command")
i.text = cmd
i:set_position(-1)
end,
-- insert a string into the command line at the current cursor position
insert_cmd = function (w, str)
if not str then return end
local i = w.ibar.input
local text = i.text
local pos = i:get_position()
local left, right = string.sub(text, 1, pos), string.sub(text, pos+1)
i.text = left .. str .. right
i:set_position(pos + #str + 1)
end,
-- Command line completion of available commands
cmd_completion = function (w)
local i = w.ibar.input
local s = w.sbar.l.uri
local cmpl = {}
-- Get last completion (is reset on key press other than <Tab>)
if not w.compl_start or w.compl_index == 0 then
w.compl_start = "^" .. string.sub(i.text, 2)
w.compl_index = 1
end
-- Get suitable commands
for _, b in ipairs(binds.commands) do
for _, c in pairs(b.commands) do
if c and string.match(c, w.compl_start) then
table.insert(cmpl, c)
end
end
end
table.sort(cmpl)
if #cmpl > 0 then
local text = ""
for index, comp in pairs(cmpl) do
if index == w.compl_index then
i.text = ":" .. comp .. " "
i:set_position(-1)
end
if text ~= "" then
text = text .. " | "
end
text = text .. comp
end
-- cycle through all possible completions
if w.compl_index == #cmpl then
w.compl_index = 1
else
w.compl_index = w.compl_index + 1
end
s.text = lousy.util.escape(text)
end
end,
del_word = function (w)
local i = w.ibar.input
local text = i.text
local pos = i:get_position()
if text and #text > 1 and pos > 1 then
local left, right = string.sub(text, 2, pos), string.sub(text, pos+1)
if not string.find(left, "%s") then
left = ""
elseif string.find(left, "%w+%s*$") then
left = string.sub(left, 0, string.find(left, "%w+%s*$") - 1)
elseif string.find(left, "%W+%s*$") then
left = string.sub(left, 0, string.find(left, "%W+%s*$") - 1)
end
i.text = string.sub(text, 1, 1) .. left .. right
i:set_position(#left + 2)
end
end,
del_line = function (w)
local i = w.ibar.input
if i.text ~= ":" then
i.text = ":"
i:set_position(-1)
end
end,
-- Search history adding
srch_hist_add = function (w, srch)
if not w.srch_hist then w.srch_hist = {} end
-- Check overflow
local max_hist = globals.max_srch_history or 100
if #w.srch_hist > (max_hist + 5) then
while #w.srch_hist > max_hist do
table.remove(w.srch_hist, 1)
end
end
table.insert(w.srch_hist, srch)
end,
-- Search history traversing
srch_hist_prev = function (w)
if not w.srch_hist then w.srch_hist = {} end
if not w.srch_hist_cursor then
w.srch_hist_cursor = #w.srch_hist + 1
w.srch_hist_current = w.ibar.input.text
end
local c = w.srch_hist_cursor - 1
if w.srch_hist[c] then
w.srch_hist_cursor = c
w.ibar.input.text = w.srch_hist[c]
w.ibar.input:set_position(-1)
end
end,
srch_hist_next = function (w)
if not w.srch_hist then w.srch_hist = {} end
local c = (w.srch_hist_cursor or #w.srch_hist) + 1
if w.srch_hist[c] then
w.srch_hist_cursor = c
w.ibar.input.text = w.srch_hist[c]
w.ibar.input:set_position(-1)
elseif w.srch_hist_current then
w.srch_hist_cursor = nil
w.ibar.input.text = w.srch_hist_current
w.ibar.input:set_position(-1)
end
end,
-- Command history adding
cmd_hist_add = function (w, cmd)
if not w.cmd_hist then w.cmd_hist = {} end
-- Make sure history doesn't overflow
local max_hist = globals.max_cmd_hist or 100
if #w.cmd_hist > (max_hist + 5) then
while #w.cmd_hist > max_hist do
table.remove(w.cmd_hist, 1)
end
end
table.insert(w.cmd_hist, cmd)
end,
-- Command history traversing
cmd_hist_prev = function (w)
if not w.cmd_hist then w.cmd_hist = {} end
if not w.cmd_hist_cursor then
w.cmd_hist_cursor = #w.cmd_hist + 1
w.cmd_hist_current = w.ibar.input.text
end
local c = w.cmd_hist_cursor - 1
if w.cmd_hist[c] then
w.cmd_hist_cursor = c
w.ibar.input.text = w.cmd_hist[c]
w.ibar.input:set_position(-1)
end
end,
cmd_hist_next = function (w)
if not w.cmd_hist then w.cmd_hist = {} end
local c = (w.cmd_hist_cursor or #w.cmd_hist) + 1
if w.cmd_hist[c] then
w.cmd_hist_cursor = c
w.ibar.input.text = w.cmd_hist[c]
w.ibar.input:set_position(-1)
elseif w.cmd_hist_current then
w.cmd_hist_cursor = nil
w.ibar.input.text = w.cmd_hist_current
w.ibar.input:set_position(-1)
end
end,
-- GUI content update functions
update_tab_count = function (w, i, t)
w.sbar.r.tabi.text = string.format("[%d/%d]", i or w.tabs:current(), t or w.tabs:count())
end,
update_win_title = function (w, view)
if not view then view = w:get_current() end
local title = view:get_prop("title")
local uri = view.uri
if not title and not uri then
w.win.title = "luakit"
else
w.win.title = (title or "luakit") .. " - " .. (uri or "about:blank")
end
end,
update_uri = function (w, view, uri)
if not view then view = w:get_current() end
w.sbar.l.uri.text = lousy.util.escape((uri or (view and view.uri) or "about:blank"))
end,
update_progress = function (w, view, p)
if not view then view = w:get_current() end
if not p then p = view:get_prop("progress") end
local loaded = w.sbar.l.loaded
if not view:loading() or p == 1 then
loaded:hide()
else
loaded:show()
loaded.text = string.format("(%d%%)", p * 100)
end
end,
update_scroll = function (w, view)
if not view then view = w:get_current() end
local scroll = w.sbar.r.scroll
if view then
local val, max = view:get_scroll_vert()
if max == 0 then val = "All"
elseif val == 0 then val = "Top"
elseif val == max then val = "Bot"
else val = string.format("%2d%%", (val/max) * 100)
end
if scroll.text ~= val then scroll.text = val end
scroll:show()
else
scroll:hide()
end
end,
update_ssl = function (w, view)
if not view then view = w:get_current() end
local trusted = view:ssl_trusted()
local theme = lousy.theme.get()
local ssl = w.sbar.r.ssl
if trusted ~= nil and not w.checking_ssl then
ssl.fg = theme.notrust_fg
ssl.text = "(nocheck)"
ssl:show()
elseif trusted == true then
ssl.fg = theme.trust_fg
ssl.text = "(trust)"
ssl:show()
elseif trusted == false then
ssl.fg = theme.notrust_fg
ssl.text = "(notrust)"
ssl:show()
else
ssl:hide()
end
end,
update_buf = function (w)
local buf = w.sbar.r.buf
if w.buffer then
buf.text = lousy.util.escape(string.format(" %-3s", w.buffer))
buf:show()
else
buf:hide()
end
end,
update_binds = function (w, mode)
-- Generate the list of active key & buffer binds for this mode
w.binds = lousy.util.table.join(binds.mode_binds[mode], binds.mode_binds.all)
-- Clear & hide buffer
w.buffer = nil
w:update_buf()
end,
-- Tab label functions
-- TODO: Move these functions into a module (I.e. lousy.widget.tablist)
make_tab_label = function (w, pos)
local theme = lousy.theme.get()
local t = {
label = label(),
sep = eventbox(),
ebox = eventbox(),
layout = hbox(),
}
t.label.font = theme.tab_font
t.label:set_width(1)
t.layout:pack_start(t.label, true, true, 0)
t.layout:pack_start(t.sep, false, false, 0)
t.ebox:set_child(t.layout)
t.ebox:add_signal("button-release", function (e, m, b)
if b == 1 then
w.tabs:switch(pos)
return true
elseif b == 2 then
w:close_tab(w.tabs:atindex(pos))
return true
end
end)
return t
end,
destroy_tab_label = function (w, t)
if not t then t = table.remove(w.tbar.titles) end
-- Destroy widgets without their own windows first (I.e. labels)
for _, wi in ipairs{ t.label, t.sep, t.ebox, t.layout } do wi:destroy() end
end,
update_tab_labels = function (w, current)
local tb = w.tbar
local count, current = w.tabs:count(), current or w.tabs:current()
tb.ebox:hide()
-- Leave the tablist hidden if there is only one tab open
if count <= 1 then
return nil
end
if count ~= #tb.titles then
-- Grow the number of labels
while count > #tb.titles do
local t = w:make_tab_label(#tb.titles + 1)
tb.layout:pack_start(t.ebox, true, true, 0)
table.insert(tb.titles, t)
end
-- Prune number of labels
while count < #tb.titles do
w:destroy_tab_label()
end
end
if count ~= 0 then
for i = 1, count do
local view = w.tabs:atindex(i)
local t = tb.titles[i]
local title = " " ..i.. " "..w:get_tab_title(view)
t.label.text = lousy.util.escape(string.format("%-40s", title))
w:apply_tablabel_theme(t, i == current)
end
end
tb.ebox:show()
end,
-- Theme functions
apply_tablabel_theme = function (w, t, selected)
local theme = lousy.theme.get()
selected = (selected and "_selected") or ""
t.label.fg = theme[string.format("tab%s_fg", selected)]
t.ebox.bg = theme[string.format("tab%s_bg", selected)]
end,
close_win = function (w)
-- Close all tabs
while w.tabs:count() ~= 0 do
w:close_tab()
end
-- Recursively remove widgets from window
local children = lousy.util.recursive_remove(w.win)
-- Destroy all widgets
for i, c in ipairs(lousy.util.table.join(children, {w.win})) do
if c.hide then c:hide() end
c:destroy()
end
-- Clear window struct
w = setmetatable(w, {})
for k, _ in pairs(w) do w[k] = nil end
-- Quit if closed last window
if #luakit.windows == 0 then luakit.quit() end
end,
}
-- Ordered list of class index functions. Other classes (E.g. webview) are able
-- to add their own index functions to this list.
window.indexes = {
-- Find function in window.methods first
function (w, k) return window.methods[k] end
}
-- Create new window
function window.new(uris)
local w = window.build()
-- Set window metatable
setmetatable(w, {
__index = function (_, k)
-- Check widget structure first
local v = rawget(w, k)
if v then return v end
-- Call each window index function
for _, index in ipairs(window.indexes) do
v = index(w, k)
if v then return v end
end
end,
})
-- Call window init functions
for _, func in pairs(window.init_funcs) do
func(w)
end
-- Populate notebook with tabs
for _, uri in ipairs(uris or {}) do
w:new_tab(uri)
end
-- Make sure something is loaded
if w.tabs:count() == 0 then
w:new_tab(globals.homepage)
end
-- Set initial mode
w:set_mode()
return w
end
-- vim: et:sw=4:ts=8:sts=4:tw=80