diff options
Diffstat (limited to 'panorama.js')
-rw-r--r-- | panorama.js | 629 |
1 files changed, 629 insertions, 0 deletions
diff --git a/panorama.js b/panorama.js new file mode 100644 index 0000000..3f1c715 --- /dev/null +++ b/panorama.js @@ -0,0 +1,629 @@ +/** + * Use at your OWN RISK. + */ +let INFO = <> +<plugin name="panorama" version="0.1" + href="https://github.com/vimpr/vimperator-plugins/blob/master/panorama.js" + summary="Add supports for Panorama" + lang="en-US" + xmlns="http://vimperator.org/namespaces/liberator"> + <author email="teramako@gmail.com">teramako</author> + <license>MPL 1.1/GPL 2.0/LGPL 2.1</license> + <project name="Vimperator" minVersion="3.0pre"/> + <p> + For adding Panorama supports. + This plugin makes the default feature to not switch to the other group suddenly. + And add some mappings and commands for Parnorama. + </p> + <p> + Use at your OWN RISK. + This pluin overwrite many mappings, some commands, and some completions. + </p> + <h3 tag="panorama-new">New Mappings and Commands</h3> + <item> + <tags>g@</tags> + <spec><oa>count</oa>g@</spec> + <description> + <p>Switch to AppTab.</p> + <p>If the current tab is already AppTab, switch to the next AppTab.</p> + </description> + </item> + <item> + <tags><![CDATA[<C-S-n>]]></tags> + <spec><oa>count</oa><C-S-n></spec> + <description> + <p>Switch to next group.</p> + <p>Caution: cannot switch to empty group.</p> + </description> + </item> + <item> + <tags><![CDATA[<C-S-p>]]></tags> + <spec><oa>count</oa><C-S-p></spec> + <description> + <p>Switch to previous group.</p> + </description> + </item> + <item> + <tags>:mkgroup :mkg</tags> + <spec>:mkg<oa>roup</oa><oa>!</oa> <oa>GroupName</oa></spec> + <description> + <p>Create new tab group named <a>GroupName</a>.</p> + <p>If specified <a>!</a>, move the current tab to the group.</p> + </description> + </item> + <item> + <tags>:stash :stashtogroup</tags> + <spec>:stash<oa>togroup</oa><oa>!</oa> <a>GroupName</a></spec> + <description> + <p>Stash the current tab to <a>GroupName</a>.</p> + <p>Caution: connnot stash AppTab (pinned tab)</p> + </description> + </item> + <item> + <tags>:switchgroup :swg</tags> + <spec>:switchgroup <a>GroupName</a></spec> + <spec>:swg <a>GroupName</a></spec> + <spec>:<oa>count</oa>switchgroup</spec> + <spec>:<oa>count</oa>swg</spec> + <description> + <p>Switch group to <a>GroupName</a></p> + </description> + </item> +</plugin> +</>; + +/** + * @method selectVisible {{{ + * 現在表示されているタブでの絶対/相対位置によるタブ選択を行う + * (tabs.select() だと全タブが対象となる) + * @param {String} spec + * @param {Boolean} wrap + */ +function selectVisible (spec, wrap) { + if (spec === void(0) || spec === "") + return; + + let tabs = gBrowser.visibleTabs; + let index; + if (typeof spec === "number" || /^\d+$/.test(spec)) { + index = parseInt(spec, 10); + } else if (spec === "$") { + index = tabs.length - 1; + } else if (/^[+-]\d+$/.test(spec)) { + index = tabs.indexOf(gBrowser.mCurrentTab) + parseInt(spec, 10); + } else { + return; + } + let length = tabs.length; + if (index > length - 1) + index = wrap ? index % length : length - 1; + else if (index < 0) + index = wrap ? index % length + length : 0; + + gBrowser.mTabContainer.selectedItem = tabs[index]; +} // }}} + +/** + * @method switchTo {{{ + * tabs.switchTo 相当の関数 + * @param {String} buffer + * @param {Boolean} allowNonUnique + * @param {Number} count + * @param {Boolean} reverse + */ +function switchTo (buffer, allowNonUnique, count, reverse) { + if (buffer == "") + return null; + if (buffer != null) { + tabs._lastBufferSwitchArgs = buffer; + tabs._lastBufferSwitchSpecial = allowNonUnique; + } else { + buffer = this._lastBufferSwitchArgs; + if (allowNonUnique === void(0) || allowNonUnique === null) + allowNonUnique = tabs._lastBufferSwitchSpecial; + } + + if (buffer == "#") { + tabs.selectAlternateTab(); + return; + } + + let tab = searchTab(buffer); + if (tab) { + tabs.select(tab._tPos, false); + return; + } + + if (!count || count < 1) + count = 1; + reverse = !!reverse; + + m = []; + let lowerBuffer = buffer.toLowerCase(); + let first = tabs.index() + (reverse ? 0 : 1); + let length = config.tabbrowser.browsers.length; + for (let [i, ] in tabs.browsers) { + let index = (i + first) % length; + let browser = config.tabbrowser.browsers[index]; + let url, title; + if ("__SS_restoreState" in browser) { + let entry = browser.__SS_data.entries[0]; + url = entry.url; + title = entry.title; + } else { + url = browser.contentDocument.location.href; + title = browser.contentDocument.title; + } + title = title.toLowerCase(); + if (url == buffer) { + tabs.select(index, false); + return; + } + if (url.indexOf(buffer) >= 0 || title.indexOf(lowerBuffer) >= 0) + m.push(index); + } + if (m.length == 0) + liberator.echoerr("E94: No matching buffer for " + buffer); + else if (m.length > 1 && !allowNonUnique) + liberator.echoerr("E93: More than one match for " + buffer); + else { + if (reverse) { + index = m.length - count; + while (index < 0) + index + m.length; + } else { + index = (count - 1) % m.length; + } + tabs.select(m[index], false); + } +} // }}} + + +/** + * @method searchTab {{{ + * @param {String} buffer + * - "{Number}:" + * - "{GroupName} {Number}:" + * @return {Element|null} + */ +function searchTab (buffer) { + if (buffer == "#") { + if (tabs.alternate != null && tabs.getTab() != tabs.alternate) + return tabs.alternate; + return null; + } + let m = buffer.match(/^(\d+):?/); + if (m) + return tabs.getTab(parseInt(m[1], 10) -1); + m = buffer.match(/^(.+?)\s+(\d+):?/); + if (m) { + let [, groupName, tabNum] = m; + tabNum = parseInt(tabNum, 10); + let group = getGroupByName(groupName)[0]; + if (!group) + return null; + let tabItem = group.getChild(tabNum -1); + if (!tabItem) + return null; + return tabItem.tab; + } + return null; +} // }}} + +let TV = window.TabView; +/** + * @type {Window} TabView._window {{{ + */ +this.__defineGetter__("tabView", function() { + if (TV && TV._window && TV._window.GroupItems) { + delete this.tabView; + this.tabView = TV._window; + return TV._window; + } else { + let wating = true; + TV._initFrame(function(){ wating = false; }) + while (wating) + liberator.threadYield(false, true); + return this.tabView; + } +}); // }}} + +/** + * @type {Array} Array of AppTabs + */ +this.__defineGetter__("appTabs", function() gBrowser.visibleTabs.filter(function(t) t.pinned)); + +/** + * @method createGroup {{{ + * @param {String} name GroupName + * @return {GroupItem} + */ +function createGroup (name) { + let pageBounds = tabView.Items.getPageBounds(); + pageBounds.inset(20, 20); + let box = new tabView.Rect(pageBounds); + box.width = 50; + box.height = 50; + let group = new tabView.GroupItem([], { bounds: box, title: name }); + if (name && group.$title.hasClass("defaultName")) + group.$title.removeClass("defaultName"); + return group; +} // }}} + +/** + * @param {String|Number} name GroupName or GroupId + * @return {GroupItem[]} + */ +function getGroupByName (name) { + if (typeof name === "number") + return tabView.GroupItems.groupItems.filter(function(g) g.id == name); + return tabView.GroupItems.groupItems.filter(function(g) g.getTitle() == name); +} +/** + * @param {Element} tab + * @param {GroupItem|Number} group GroupItem object or group id + */ +function tabMoveToGroup (tab, group) { + let id = (typeof group == "object") ? group.id : group; + tabView.GroupItems.moveTabToGroupItem(tab, id); +} + +/** + * @method switchToGroup {{{ + * @param {String|Number} spec + * @param {Boolean} wrap + */ +function switchToGroup (spec, wrap) { + const GI = tabView.GroupItems + let current = GI.getActiveGroupItem() || GI.getActiveOrphanTab(); + let groupsAndOrphans = GI.groupItems.concat(GI.getOrphanedTabs()); + let offset = 1, relative = false, index; + if (typeof spec === "number") + index = parseInt(spec, 10); + else if (/^[+-]\d+$/.test(spec)) { + let buf = parseInt(spec, 10); + index = groupsAndOrphans.indexOf(current) + buf; + offset = buf >= 0 ? 1 : -1; + relative = true; + } else if (spec != "") { + if (/^\d+$/.test(spec)) + spec = parseInt(spec, 10); + let targetGroup = getGroupByName(spec)[0]; + if (targetGroup) + index = groupsAndOrphans.indexOf(targetGroup); + else { + liberator.echoerr("No such group: " + spec); + return; + } + } else { + return; + } + let length = groupsAndOrphans.length; + let apps = appTabs; + function groupSwitch (index, wrap) { + if (index > length - 1) + index = wrap ? index % length : length - 1; + else if (index < 0) + index = wrap ? index % length + length : 0; + + let target = groupsAndOrphans[index], + group = null; + if (target instanceof tabView.GroupItem) { + group = target; + target = target.getActiveTab() || target.getChild(0); + } + + if (target) { + gBrowser.mTabContainer.selectedItem = target.tab; + } else if (group && apps.length != 0) { + GI.setActiveGroupItem(group); + tabView.UI.goToTab(tabs.getTab(0)); + } else if (relative) { + groupSwitch(index + offset, true); + } else { + liberator.echoerr("Cannot switch to " + spec); + return; + } + } + groupSwitch(index, wrap); +} // }}} + +// ============================================================================ +// Mappings {{{ +// ============================================================================ + +/** + * {count}g@ select {count} of AppTab + * g@ select AppTab, + * if already selected, select the next AppTab + */ +mappings.add([modes.NORMAL], ["g@"], + "Go to AppTab", + function (count) { + let apps = appTabs; + let i = 0; + if (count != null) + i = count; + else { + let currentTab = tabs.getTab(); + if (currentTab.pinned) + i = apps.indexOf(currentTab) + 1; + i %= apps.length; + } + if (apps[i]) + selectVisible(i); + }, { count: true }); + +/** + * Switch to next group + */ +mappings.add([modes.NORMAL], ["<C-S-n>"], + "switch to next group", + function (count) { switchToGroup("+" + (count || 1), true); }, + { count: true }); + +/** + * Switch to previous group + */ +mappings.add([modes.NORMAL], ["<C-S-p>"], + "switch to previous group", + function (count) { switchToGroup("-" + (count || 1), true); }, + { count: true }); + +// overwrite 'g0", 'g^' +mappings.getDefault(modes.NORMAL, "g0").action = function (count) { selectVisible(0); }; +// overwrite 'g$' +mappings.getDefault(modes.NORMAL, "g$").action = function (count) { selectVisible("$"); }; +// overwrite 'gt' +mappings.getDefault(modes.NORMAL, "gt").action = function (count) { + if (count != null) + selectVisible(count - 1, false); + else + selectVisible("+1", true); +}; +// overwrite 'C-n', 'C-Tab', 'C-PageDown' +mappings.getDefault(modes.NORMAL, "<C-n>").action = function (count) { + selectVisible("+" + (count || 1), true); +}; +// overwrite 'gT' +mappings.getDefault(modes.NORMAL, "gT").action = function (count) { + selectVisible("-" + (count || 1), true); +} +// overwrite 'b' +mappings.getDefault(modes.NORMAL, "b").action = function (count) { + if (count != null) + selectVisible(count - 1); + else + commandline.open("", "buffer! ", modes.EX); +} + +// }}} + +// ============================================================================ +// Command {{{ +// ============================================================================ +/** + * overwrite :ls :buffers + */ +let (cmd = commands.get("buffers")) { + cmd.action = function (args) { + completion.listCompleter("buffer", args.literalArg, null, args.bang); + }; + cmd.bang = true; +} + +/** + * overwrite :buffer + */ +let (cmd = commands.get("buffer")) { + cmd.action = function (args) { + let arg = args.literalArg; + if (arg && args.count > 0) + switchTo(arg, args.bang); + else if (args.count > 0) + switchTo(args.count.toString(), args.bang); + else + switchTo(arg, args.bang); + }; + cmd.completer = function (context) completion.buffer(context, true); +} + +commands.addUserCommand(["mkg[roup]"], "create Group", + function (args) { + let groupName = args.literalArg; + let group = createGroup(groupName); + if (args.bang) { + let currentTab = tabs.getTab(); + if (!currentTab.pinned) + TV.moveTabTo(currentTab, group.id); + } + }, { + argCount: "1", + bang: true, + literal: 0, + }, true); + +commands.addUserCommand(["switchgruop", "swg"], "Switch Group", + function (args) { + if (args.count > 0) { + switchToGroup("+" + args.count, true); + } else { + switchToGroup(args.literalArg); + } + }, { + argCount: "?", + count: true, + literal: 0, + completer: function (context) completion.tabgroup(context, true), + }, true); + +commands.addUserCommand(["stash[togroup]"], "Stash the current tab to other group", + function (args) { + let currentTab = tabs.getTab(); + if (currentTab.pinned) { + liberator.echoerr("Connot stash an AppTab"); + return; + } + let groupName = args.literalArg; + let group = getGroupByName(groupName)[0]; + if (!group) { + if (args.bang) { + group = createGroup(groupName); + } else { + liberator.echoerr("No such group: " + groupName.quote() + ". if want create, add \"!\""); + return; + } + } + TV.moveTabTo(currentTab, group.id); + } ,{ + argCount: "1", + bang: true, + literal: 0, + completer: function (context) completion.tabgroup(context, true), + }, true); + +// }}} + +// ============================================================================ +// Completion {{{ +// ============================================================================ +completion.tabgroup = function TabGroupCompleter (context, excludeActiveGroup) { + const GI = tabView.GroupItems; + let groupItems = GI.groupItems; + if (excludeActiveGroup) { + let activeGroup = GI.getActiveGroupItem(); + groupItems = groupItems.filter(function(group) group.id != activeGroup.id); + } + context.title = ["TabGroup"]; + context.completions = groupItems.map(function(group) { + let title = group.getTitle(); + let desc = [ + "Title:", title || "(Untitled)", + "TabNum:", group.getChildren().length, + ].join(" "); + if (!title) + title = group.id; + return [title, desc]; + }); +}; + +/** + * overwite completion.buffer + */ +(function(TV, gBrowser) { + const UNTITLE_LABEL = "(Untitled)"; + + function getIndicator (tab) { + if (tab == gBrowser.mCurrentTab) + return "%"; + else if (tab == tabs.alternate) + return "#"; + return " "; + } + function getURLFromTab (tab) { + if ("__SS_restoreState" in tab.linkedBrowser && "__SS_data" in tab.linkedBrowser) + return tab.linkedBrowser.__SS_data.entries[0].url; + return tab.linkedBrowser.contentDocument.location.href; + } + function generateVisibleTabs () { + for (let [i, tab] in Iterator(gBrowser.visibleTabs)) { + let indicator = getIndicator(tab), + label = tab.label || UNTITLE_LABEL, + url = getURLFromTab(tab), + index = (tab._tPos + 1) + ": "; + let item = { + text: [ index + label, index + url], + url: template.highlightURL(url), + indicator: indicator + (tab.pinned ? "@" : ""), + icon: tab.image || DEFAULT_FAVICON + }; + if (!tab.pinned && tab.tabItem && tab.tabItem.parent) { + let groupName = tab.tabItem.parent.getTitle(); + if (groupName) { + let prefix = groupName + " " + (i + 1) + ": "; + item.text.push(prefix + label); + item.text.push(prefix + url); + } + } + yield item; + } + } + function generateGroupList (group, groupName) { + for (let [i, tabItem] in Iterator(group.getChildren())) { + let indicator = getIndicator(tabItem.tab); + let index = (tabItem.tab._tPos + 1) + ": ", + label = tabItem.tab.label || UNTITLE_LABEL, + url = getURLFromTab(tabItem.tab); + let item = { + text: [index + label, index + url], + url: template.highlightURL(url), + indicator: indicator, + icon: tabItem.tab.image || DEFAULT_FAVICON + }; + if (groupName) { + index = groupName + " " + (i + 1) + ": "; + item.text.push(index + label); + item.text.push(index + url); + } + yield item; + } + } + function generateOrphanedList (tabItems) { + for (let [i, tabItem] in Iterator(tabItems)) { + let indicator = getIndicator(tabItem.tab), + index = (tabItem.tab._tPos + 1) + ": "; + label = tabItem.tab.label || UNTITLE_LABEL, + url = getURLFromTab(tabItem.tab); + yield { + text: [index + label, index + url], + url: template.highlightURL(url), + indicator: indicator, + icon: tabItem.tab.image || DEFAULT_FAVICON + }; + } + } + + completion.buffer = function bufferCompletion (context, all) { + context.anchored = false; + context.keys = { text: "text", description: "url", icon: "icon" }; + context.compare = CompletionContext.Sort.number; + let process = context.process[0]; + context.process = [ + function (item, text) <> + <span highlight="Indicator" style="display: inline-block; width: 2em; text-align: center">{item.item.indicator}</span> + { process.call(this, item, text) } + </> + ]; + context.title = ["Buffers"]; + context.completions = [item for (item in generateVisibleTabs())]; + if (!all) + return; + let self = this; + TV._initFrame(function() { + let groups = TV._window.GroupItems; + let activeGroup = groups.getActiveGroupItem(); + let activeGroupId = activeGroup === null ? null : activeGroup.id; + for (let [i, group] in Iterator(groups.groupItems)) { + if (group.id != activeGroupId) { + let groupName = group.getTitle(); + context.fork("GROUP_" + group.id, 0, self, function (context) { + context.title = [groupName || UNTITLE_LABEL]; + context.completions = [item for (item in generateGroupList(group, groupName))]; + }); + } + } + let orphanedTabs = [tabItem for ([, tabItem] in Iterator(groups.getOrphanedTabs())) if (tabItem.tab.hidden)]; + if (orphanedTabs.length == 0) + return; + context.fork("__ORPHANED__", 0, self, function (context) { + context.title = ["Orphaned"]; + context.completions = [item for (item in generateOrphanedList(orphanedTabs))]; + }); + }); + }; + +})(window.TabView, window.gBrowser); + +// }}} + +// vim: sw=2 ts=2 et fdm=marker: |