diff options
author | Florian Bruhin <git@the-compiler.org> | 2016-03-31 07:10:20 +0200 |
---|---|---|
committer | Florian Bruhin <git@the-compiler.org> | 2016-03-31 07:10:20 +0200 |
commit | da1338278a82ab1921b6f62dd367a2da79329a3a (patch) | |
tree | 88bef100161f67f18d5a85dc21facedec8979687 | |
parent | 99263ae351fe3d44dd172ed4106631182bd15dd7 (diff) | |
parent | d15a3c6de8bd6f95c3ca44bb3b2a2cbf6ed29f81 (diff) | |
download | qutebrowser-da1338278a82ab1921b6f62dd367a2da79329a3a.tar.gz qutebrowser-da1338278a82ab1921b6f62dd367a2da79329a3a.zip |
Merge branch 'toofar-tab-complete'
-rw-r--r-- | README.asciidoc | 2 | ||||
-rw-r--r-- | doc/help/commands.asciidoc | 13 | ||||
-rw-r--r-- | misc/cheatsheet.svg | 28 | ||||
-rw-r--r-- | qutebrowser/app.py | 4 | ||||
-rw-r--r-- | qutebrowser/browser/commands.py | 55 | ||||
-rw-r--r-- | qutebrowser/completion/models/instances.py | 9 | ||||
-rw-r--r-- | qutebrowser/completion/models/miscmodels.py | 77 | ||||
-rw-r--r-- | qutebrowser/config/configdata.py | 5 | ||||
-rw-r--r-- | qutebrowser/mainwindow/mainwindow.py | 2 | ||||
-rw-r--r-- | qutebrowser/mainwindow/tabbedbrowser.py | 11 | ||||
-rw-r--r-- | qutebrowser/utils/usertypes.py | 2 | ||||
-rw-r--r-- | tests/integration/features/tabs.feature | 113 |
12 files changed, 296 insertions, 25 deletions
diff --git a/README.asciidoc b/README.asciidoc index d2ee9e02d..6cb17e248 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -170,11 +170,11 @@ Contributors, sorted by the number of commits in descending order: * ZDarian * Milan Svoboda * John ShaggyTwoDope Jenkins +* Jimmy * Peter Vilim * Clayton Craft * Oliver Caldwell * Jonas Schürmann -* Jimmy * Panagiotis Ktistakis * Jakub Klinkovský * skinnay diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index d1875c645..ae7b597d3 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -11,6 +11,7 @@ |<<bookmark-add,bookmark-add>>|Save the current page as a bookmark. |<<bookmark-del,bookmark-del>>|Delete a bookmark. |<<bookmark-load,bookmark-load>>|Load a bookmark. +|<<buffer,buffer>>|Select tab by index or url/title best match. |<<close,close>>|Close the current window. |<<download,download>>|Download a given URL, or current page if no URL given. |<<download-cancel,download-cancel>>|Cancel the last/[count]th download. @@ -142,6 +143,18 @@ Load a bookmark. * This command does not split arguments after the last argument and handles quotes literally. * With this command, +;;+ is interpreted literally instead of splitting off a second command. +[[buffer]] +=== buffer +Syntax: +:buffer 'index'+ + +Select tab by index or url/title best match. + +Focuses window if necessary. + +==== positional arguments +* +'index'+: The [win_id/]index of the tab to focus. Or a substring in which case the closest match will be focused. + + [[close]] === close Close the current window. diff --git a/misc/cheatsheet.svg b/misc/cheatsheet.svg index 13cf3101e..251a6ed0e 100644 --- a/misc/cheatsheet.svg +++ b/misc/cheatsheet.svg @@ -13,7 +13,7 @@ height="640" id="svg2" sodipodi:version="0.32" - inkscape:version="0.48.5 r10040" + inkscape:version="0.91 r13725" version="1.0" sodipodi:docname="cheatsheet.svg" inkscape:output_extension="org.inkscape.output.svg.inkscape" @@ -32,18 +32,18 @@ objecttolerance="10" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="0.8791156" - inkscape:cx="768.67127" - inkscape:cy="133.80749" + inkscape:zoom="1.7582312" + inkscape:cx="875.18895" + inkscape:cy="136.8726" inkscape:document-units="px" inkscape:current-layer="layer1" width="1024px" height="640px" showgrid="false" - inkscape:window-width="636" - inkscape:window-height="536" - inkscape:window-x="2560" - inkscape:window-y="0" + inkscape:window-width="1362" + inkscape:window-height="740" + inkscape:window-x="0" + inkscape:window-y="24" showguides="true" inkscape:guide-bbox="true" inkscape:window-maximized="0" @@ -3040,17 +3040,13 @@ style="font-weight:bold;-inkscape-font-specification:'Sans Bold';fill:#ff0000" id="flowSpan3852">(10)</flowSpan> misc. commands:</flowPara><flowPara style="font-size:10px;fill:#000000" - id="flowPara3725-0"><flowSpan - style="fill:#0000ff" - id="flowSpan5471">gm</flowSpan> - move tab</flowPara><flowPara + id="flowPara3725-0">gt - switch tabs by name</flowPara><flowPara style="font-size:10px;fill:#000000" - id="flowPara3854"><flowSpan + id="flowPara4052"><flowSpan style="fill:#0000ff" - id="flowSpan5473">gl</flowSpan> - move tab to left</flowPara><flowPara + id="flowSpan4054">gm/gl/lr</flowSpan> - move tab</flowPara><flowPara style="font-size:10px;fill:#000000" - id="flowPara3856"><flowSpan - style="fill:#0000ff" - id="flowSpan5475">gr</flowSpan> - move tab to right</flowPara><flowPara + id="flowPara4056"> (to index/left/right)</flowPara><flowPara style="font-size:10px;fill:#000000" id="flowPara3858">gC - clone tab</flowPara><flowPara style="font-size:10px;fill:#000000" diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 7ba630ddb..b22d3b8eb 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -35,7 +35,7 @@ from PyQt5.QtWidgets import QApplication from PyQt5.QtWebKit import QWebSettings from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QCursor, QWindow from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, QUrl, - QObject, Qt, QEvent) + QObject, Qt, QEvent, pyqtSignal) try: import hunter except ImportError: @@ -742,6 +742,8 @@ class Application(QApplication): _args: ArgumentParser instance. """ + new_window = pyqtSignal(mainwindow.MainWindow) + def __init__(self, args): """Constructor. diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 82a52cc3f..3cae6941c 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -45,6 +45,7 @@ from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, objreg, utils) from qutebrowser.utils.usertypes import KeyMode from qutebrowser.misc import editor, guiprocess +from qutebrowser.completion.models import instances, sortfilter class CommandDispatcher: @@ -835,6 +836,60 @@ class CommandDispatcher: self._open(url, tab, bg, window) @cmdutils.register(instance='command-dispatcher', scope='window', + completion=[usertypes.Completion.tab]) + def buffer(self, index): + """Select tab by index or url/title best match. + + Focuses window if necessary. + + Args: + index: The [win_id/]index of the tab to focus. Or a substring + in which case the closest match will be focused. + """ + index_parts = index.split('/', 1) + + try: + for part in index_parts: + int(part) + except ValueError: + model = instances.get(usertypes.Completion.tab) + sf = sortfilter.CompletionFilterModel(source=model) + sf.set_pattern(index) + if sf.count() > 0: + index = sf.data(sf.first_item()) + index_parts = index.split('/', 1) + else: + raise cmdexc.CommandError( + "No matching tab for: {}".format(index)) + + if len(index_parts) == 2: + win_id = int(index_parts[0]) + idx = int(index_parts[1]) + elif len(index_parts) == 1: + idx = int(index_parts[0]) + active_win = objreg.get('app').activeWindow() + if active_win is None: + # Not sure how you enter a command without an active window... + raise cmdexc.CommandError( + "No window specified and couldn't find active window!") + win_id = active_win.win_id + + if win_id not in objreg.window_registry: + raise cmdexc.CommandError( + "There's no window with id {}!".format(win_id)) + + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=win_id) + if not 0 < idx <= tabbed_browser.count(): + raise cmdexc.CommandError( + "There's no tab with index {}!".format(idx)) + + window = objreg.window_registry[win_id] + window.activateWindow() + window.raise_() + tabbed_browser.setCurrentIndex(idx-1) + + @cmdutils.register(instance='command-dispatcher', scope='window', count='count') def tab_focus(self, index: {'type': (int, 'last')}=None, count=None): """Select the tab given as argument/[count]. diff --git a/qutebrowser/completion/models/instances.py b/qutebrowser/completion/models/instances.py index 9d3cb2644..002a8a815 100644 --- a/qutebrowser/completion/models/instances.py +++ b/qutebrowser/completion/models/instances.py @@ -59,6 +59,14 @@ def _init_url_completion(): _instances[usertypes.Completion.url] = model +def _init_tab_completion(): + """Initialize the tab completion model.""" + log.completion.debug("Initializing tab completion.") + with debug.log_time(log.completion, 'tab completion init'): + model = miscmodels.TabCompletionModel() + _instances[usertypes.Completion.tab] = model + + def _init_setting_completions(): """Initialize setting completion models.""" log.completion.debug("Initializing setting completion.") @@ -115,6 +123,7 @@ INITIALIZERS = { usertypes.Completion.command: _init_command_completion, usertypes.Completion.helptopic: _init_helptopic_completion, usertypes.Completion.url: _init_url_completion, + usertypes.Completion.tab: _init_tab_completion, usertypes.Completion.section: _init_setting_completions, usertypes.Completion.option: _init_setting_completions, usertypes.Completion.value: _init_setting_completions, diff --git a/qutebrowser/completion/models/miscmodels.py b/qutebrowser/completion/models/miscmodels.py index e4cade0ab..400ea2568 100644 --- a/qutebrowser/completion/models/miscmodels.py +++ b/qutebrowser/completion/models/miscmodels.py @@ -19,6 +19,9 @@ """Misc. CompletionModels.""" +from PyQt5.QtCore import Qt, QTimer, pyqtSlot + +from qutebrowser.browser import webview from qutebrowser.config import config, configdata from qutebrowser.utils import objreg, log from qutebrowser.commands import cmdutils @@ -138,3 +141,77 @@ class SessionCompletionModel(base.BaseCompletionModel): self.new_item(cat, name) except OSError: log.completion.exception("Failed to list sessions!") + + +class TabCompletionModel(base.BaseCompletionModel): + + """A model to complete on open tabs across all windows. + + Used for switching the buffer command.""" + + # https://github.com/The-Compiler/qutebrowser/issues/545 + # pylint: disable=abstract-method + + #IDX_COLUMN = 0 + URL_COLUMN = 1 + TEXT_COLUMN = 2 + + COLUMN_WIDTHS = (6, 40, 54) + DUMB_SORT = Qt.DescendingOrder + + def __init__(self, parent=None): + super().__init__(parent) + + self.columns_to_filter = [self.URL_COLUMN, self.TEXT_COLUMN] + + for win_id in objreg.window_registry: + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=win_id) + for i in range(tabbed_browser.count()): + tab = tabbed_browser.widget(i) + tab.url_text_changed.connect(self.rebuild) + tab.shutting_down.connect(self.delayed_rebuild) + tabbed_browser.new_tab.connect(self.on_new_tab) + objreg.get("app").new_window.connect(self.on_new_window) + self.rebuild() + + # slot argument should be mainwindow.MainWindow but can't import + # that at module level because of import loops. + @pyqtSlot(object) + def on_new_window(self, window): + """Add hooks to new windows.""" + window.tabbed_browser.new_tab.connect(self.on_new_tab) + + @pyqtSlot(webview.WebView) + def on_new_tab(self, tab): + """Add hooks to new tabs.""" + tab.url_text_changed.connect(self.rebuild) + tab.shutting_down.connect(self.delayed_rebuild) + self.rebuild() + + @pyqtSlot() + def delayed_rebuild(self): + """Fire a rebuild indirectly so widgets get a chance to update.""" + QTimer.singleShot(0, self.rebuild) + + @pyqtSlot() + def rebuild(self): + """Rebuild completion model from current tabs. + + Very lazy method of keeping the model up to date. We could connect to + signals for new tab, tab url/title changed, tab close, tab moved and + make sure we handled background loads too ... but iterating over a + few/few dozen/few hundred tabs doesn't take very long at all. + """ + self.removeRows(0, self.rowCount()) + for win_id in objreg.window_registry: + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=win_id) + if tabbed_browser.shutting_down: + continue + c = self.new_category("{}".format(win_id)) + for i in range(tabbed_browser.count()): + tab = tabbed_browser.widget(i) + self.new_item(c, "{}/{}".format(win_id, i+1), + tab.url().toDisplayString(), + tabbed_browser.page_title(i)) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index fd44200ae..10dad1ce9 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1397,8 +1397,8 @@ KEY_DATA = collections.OrderedDict([ ('tab-move', ['gm']), ('tab-move -', ['gl']), ('tab-move +', ['gr']), - ('tab-focus', ['J', 'gt']), - ('tab-prev', ['K', 'gT']), + ('tab-focus', ['J']), + ('tab-prev', ['K']), ('tab-clone', ['gC']), ('reload', ['r']), ('reload -f', ['R']), @@ -1477,6 +1477,7 @@ KEY_DATA = collections.OrderedDict([ ('download-cancel', ['ad']), ('download-clear', ['cd']), ('view-source', ['gf']), + ('set-cmd-text -s :buffer', ['gt']), ('tab-focus last', ['<Ctrl-Tab>']), ('enter-mode passthrough', ['<Ctrl-V>']), ('quit', ['<Ctrl-Q>']), diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 062ff0d95..f7fe0b706 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -187,6 +187,8 @@ class MainWindow(QWidget): #self.tabWidget.setCurrentIndex(0) #QtCore.QMetaObject.connectSlotsByName(MainWindow) + objreg.get("app").new_window.emit(self) + def __repr__(self): return utils.get_repr(self) diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index ef79b3ad5..7c6c4e9c8 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -63,7 +63,7 @@ class TabbedBrowser(tabwidget.TabWidget): tabbar -> new-tab-position set to 'left'. _tab_insert_idx_right: Same as above, for 'right'. _undo_stack: List of UndoEntry namedtuples of closed tabs. - _shutting_down: Whether we're currently shutting down. + shutting_down: Whether we're currently shutting down. Signals: cur_progress: Progress of the current tab changed (loadProgress). @@ -82,6 +82,7 @@ class TabbedBrowser(tabwidget.TabWidget): widget can adjust its size to it. arg: The new size. current_tab_changed: The current tab changed to the emitted WebView. + new_tab: Emits the new WebView and its index when a new tab is opened. """ cur_progress = pyqtSignal(int) @@ -96,13 +97,14 @@ class TabbedBrowser(tabwidget.TabWidget): resized = pyqtSignal('QRect') got_cmd = pyqtSignal(str) current_tab_changed = pyqtSignal(webview.WebView) + new_tab = pyqtSignal(webview.WebView, int) def __init__(self, win_id, parent=None): super().__init__(win_id, parent) self._win_id = win_id self._tab_insert_idx_left = 0 self._tab_insert_idx_right = -1 - self._shutting_down = False + self.shutting_down = False self.tabCloseRequested.connect(self.on_tab_close_requested) self.currentChanged.connect(self.on_current_changed) self.cur_load_started.connect(self.on_cur_load_started) @@ -234,7 +236,7 @@ class TabbedBrowser(tabwidget.TabWidget): def shutdown(self): """Try to shut down all tabs cleanly.""" - self._shutting_down = True + self.shutting_down = True for tab in self.widgets(): self._remove_tab(tab) @@ -398,6 +400,7 @@ class TabbedBrowser(tabwidget.TabWidget): if not background: self.setCurrentWidget(tab) tab.show() + self.new_tab.emit(tab, idx) return tab def _get_new_tab_idx(self, explicit): @@ -546,7 +549,7 @@ class TabbedBrowser(tabwidget.TabWidget): @pyqtSlot(int) def on_current_changed(self, idx): """Set last-focused-tab and leave hinting mode when focus changed.""" - if idx == -1 or self._shutting_down: + if idx == -1 or self.shutting_down: # closing the last tab (before quitting) or shutting down return tab = self.widget(idx) diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 7ec0e57c7..5e29ee535 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -237,7 +237,7 @@ KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt', # Available command completions Completion = enum('Completion', ['command', 'section', 'option', 'value', 'helptopic', 'quickmark_by_name', - 'bookmark_by_url', 'url', 'sessions']) + 'bookmark_by_url', 'url', 'tab', 'sessions']) # Exit statuses for errors. Needs to be an int for sys.exit. diff --git a/tests/integration/features/tabs.feature b/tests/integration/features/tabs.feature index 8d898694b..630fa6279 100644 --- a/tests/integration/features/tabs.feature +++ b/tests/integration/features/tabs.feature @@ -709,3 +709,116 @@ Feature: Tab management - data/hints/link.html - about:blank - data/hello.txt (active) + + # :buffer + + Scenario: buffer without args + Given I have a fresh instance + When I run :buffer + Then the error "buffer: The following arguments are required: index" should be shown + + Scenario: buffer one window title present + When I open data/title.html + And I open data/search.html in a new tab + And I open data/scroll.html in a new tab + And I run :buffer "Searching text" + Then the following tabs should be open: + - data/title.html + - data/search.html (active) + - data/scroll.html + + Scenario: buffer one window title not present + When I run :buffer "invalid title" + Then the error "No matching tab for: invalid title" should be shown + + Scenario: buffer two window title present + When I open data/title.html + And I open data/search.html in a new tab + And I open data/scroll.html in a new tab + And I open data/caret.html in a new window + And I open data/paste_primary.html in a new tab + And I run :buffer "Scrolling" + Then the session should look like: + windows: + - active: true + tabs: + - history: + - url: about:blank + - url: http://localhost:*/data/title.html + - history: + - url: http://localhost:*/data/search.html + - active: true + history: + - url: http://localhost:*/data/scroll.html + - tabs: + - history: + - url: http://localhost:*/data/caret.html + - active: true + history: + - url: http://localhost:*/data/paste_primary.html + + Scenario: buffer one window index not present + When I open data/title.html + And I run :buffer "666" + Then the error "There's no tab with index 666!" should be shown + + Scenario: buffer one window win not present + When I open data/title.html + And I run :buffer "2/1" + Then the error "There's no window with id 2!" should be shown + + Scenario: buffer two window index present + Given I have a fresh instance + When I open data/title.html + And I open data/search.html in a new tab + And I open data/scroll.html in a new tab + And I run :open -w http://localhost:(port)/data/caret.html + And I open data/paste_primary.html in a new tab + And I wait until data/caret.html is loaded + And I run :buffer "0/2" + Then the session should look like: + windows: + - active: true + tabs: + - history: + - url: about:blank + - url: http://localhost:*/data/title.html + - active: true + history: + - url: http://localhost:*/data/search.html + - history: + - url: http://localhost:*/data/scroll.html + - tabs: + - history: + - url: http://localhost:*/data/caret.html + - active: true + history: + - url: http://localhost:*/data/paste_primary.html + + Scenario: buffer troubling args 01 + Given I have a fresh instance + When I open data/title.html + And I run :buffer "-1" + Then the error "There's no tab with index -1!" should be shown + + Scenario: buffer troubling args 02 + When I open data/title.html + And I run :buffer "/" + Then the following tabs should be open: + - data/title.html (active) + + Scenario: buffer troubling args 03 + When I open data/title.html + And I run :buffer "//" + Then the following tabs should be open: + - data/title.html (active) + + Scenario: buffer troubling args 04 + When I open data/title.html + And I run :buffer "0/x" + Then the error "No matching tab for: 0/x" should be shown + + Scenario: buffer troubling args 05 + When I open data/title.html + And I run :buffer "1/2/3" + Then the error "No matching tab for: 1/2/3" should be shown |