summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Bruhin <git@the-compiler.org>2016-03-31 07:10:20 +0200
committerFlorian Bruhin <git@the-compiler.org>2016-03-31 07:10:20 +0200
commitda1338278a82ab1921b6f62dd367a2da79329a3a (patch)
tree88bef100161f67f18d5a85dc21facedec8979687
parent99263ae351fe3d44dd172ed4106631182bd15dd7 (diff)
parentd15a3c6de8bd6f95c3ca44bb3b2a2cbf6ed29f81 (diff)
downloadqutebrowser-da1338278a82ab1921b6f62dd367a2da79329a3a.tar.gz
qutebrowser-da1338278a82ab1921b6f62dd367a2da79329a3a.zip
Merge branch 'toofar-tab-complete'
-rw-r--r--README.asciidoc2
-rw-r--r--doc/help/commands.asciidoc13
-rw-r--r--misc/cheatsheet.svg28
-rw-r--r--qutebrowser/app.py4
-rw-r--r--qutebrowser/browser/commands.py55
-rw-r--r--qutebrowser/completion/models/instances.py9
-rw-r--r--qutebrowser/completion/models/miscmodels.py77
-rw-r--r--qutebrowser/config/configdata.py5
-rw-r--r--qutebrowser/mainwindow/mainwindow.py2
-rw-r--r--qutebrowser/mainwindow/tabbedbrowser.py11
-rw-r--r--qutebrowser/utils/usertypes.py2
-rw-r--r--tests/integration/features/tabs.feature113
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