Wed, 01 Sep 2010 03:59:23 +0100
Initial set of changes to make 'insert' mode 'normal', eradicate 'command' mode and adjust key bindings appropriately (following common binding patterns from other browsers, and in places the nano text editor)
------------------ -- 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