Merge branch 'tab-complete' of https://github.com/toofar/qutebrowser into toofar-tab-complete

This commit is contained in:
Florian Bruhin 2016-03-31 06:58:22 +02:00
commit c1cec53c0e
10 changed files with 281 additions and 7 deletions

View File

@ -170,11 +170,11 @@ Contributors, sorted by the number of commits in descending order:
* ZDarian * ZDarian
* Milan Svoboda * Milan Svoboda
* John ShaggyTwoDope Jenkins * John ShaggyTwoDope Jenkins
* Jimmy
* Peter Vilim * Peter Vilim
* Clayton Craft * Clayton Craft
* Oliver Caldwell * Oliver Caldwell
* Jonas Schürmann * Jonas Schürmann
* Jimmy
* Panagiotis Ktistakis * Panagiotis Ktistakis
* Jakub Klinkovský * Jakub Klinkovský
* skinnay * skinnay

View File

@ -11,6 +11,7 @@
|<<bookmark-add,bookmark-add>>|Save the current page as a bookmark. |<<bookmark-add,bookmark-add>>|Save the current page as a bookmark.
|<<bookmark-del,bookmark-del>>|Delete a bookmark. |<<bookmark-del,bookmark-del>>|Delete a bookmark.
|<<bookmark-load,bookmark-load>>|Load 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. |<<close,close>>|Close the current window.
|<<download,download>>|Download a given URL, or current page if no URL given. |<<download,download>>|Download a given URL, or current page if no URL given.
|<<download-cancel,download-cancel>>|Cancel the last/[count]th download. |<<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. * 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. * 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 === close
Close the current window. Close the current window.

View File

@ -35,7 +35,7 @@ from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QCursor, QWindow from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QCursor, QWindow
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, QUrl, from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, QUrl,
QObject, Qt, QEvent) QObject, Qt, QEvent, pyqtSignal)
try: try:
import hunter import hunter
except ImportError: except ImportError:
@ -742,6 +742,8 @@ class Application(QApplication):
_args: ArgumentParser instance. _args: ArgumentParser instance.
""" """
new_window = pyqtSignal(mainwindow.MainWindow)
def __init__(self, args): def __init__(self, args):
"""Constructor. """Constructor.

View File

@ -45,6 +45,7 @@ from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
objreg, utils) objreg, utils)
from qutebrowser.utils.usertypes import KeyMode from qutebrowser.utils.usertypes import KeyMode
from qutebrowser.misc import editor, guiprocess from qutebrowser.misc import editor, guiprocess
from qutebrowser.completion.models import instances, sortfilter
class CommandDispatcher: class CommandDispatcher:
@ -834,6 +835,60 @@ class CommandDispatcher:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
self._open(url, tab, bg, window) 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', @cmdutils.register(instance='command-dispatcher', scope='window',
count='count') count='count')
def tab_focus(self, index: {'type': (int, 'last')}=None, count=None): def tab_focus(self, index: {'type': (int, 'last')}=None, count=None):

View File

@ -59,6 +59,14 @@ def _init_url_completion():
_instances[usertypes.Completion.url] = model _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(): def _init_setting_completions():
"""Initialize setting completion models.""" """Initialize setting completion models."""
log.completion.debug("Initializing setting completion.") log.completion.debug("Initializing setting completion.")
@ -115,6 +123,7 @@ INITIALIZERS = {
usertypes.Completion.command: _init_command_completion, usertypes.Completion.command: _init_command_completion,
usertypes.Completion.helptopic: _init_helptopic_completion, usertypes.Completion.helptopic: _init_helptopic_completion,
usertypes.Completion.url: _init_url_completion, usertypes.Completion.url: _init_url_completion,
usertypes.Completion.tab: _init_tab_completion,
usertypes.Completion.section: _init_setting_completions, usertypes.Completion.section: _init_setting_completions,
usertypes.Completion.option: _init_setting_completions, usertypes.Completion.option: _init_setting_completions,
usertypes.Completion.value: _init_setting_completions, usertypes.Completion.value: _init_setting_completions,

View File

@ -19,6 +19,9 @@
"""Misc. CompletionModels.""" """Misc. CompletionModels."""
from PyQt5.QtCore import Qt, QTimer, pyqtSlot
from qutebrowser.browser import webview
from qutebrowser.config import config, configdata from qutebrowser.config import config, configdata
from qutebrowser.utils import objreg, log from qutebrowser.utils import objreg, log
from qutebrowser.commands import cmdutils from qutebrowser.commands import cmdutils
@ -138,3 +141,77 @@ class SessionCompletionModel(base.BaseCompletionModel):
self.new_item(cat, name) self.new_item(cat, name)
except OSError: except OSError:
log.completion.exception("Failed to list sessions!") 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))

View File

@ -187,6 +187,8 @@ class MainWindow(QWidget):
#self.tabWidget.setCurrentIndex(0) #self.tabWidget.setCurrentIndex(0)
#QtCore.QMetaObject.connectSlotsByName(MainWindow) #QtCore.QMetaObject.connectSlotsByName(MainWindow)
objreg.get("app").new_window.emit(self)
def __repr__(self): def __repr__(self):
return utils.get_repr(self) return utils.get_repr(self)

View File

@ -63,7 +63,7 @@ class TabbedBrowser(tabwidget.TabWidget):
tabbar -> new-tab-position set to 'left'. tabbar -> new-tab-position set to 'left'.
_tab_insert_idx_right: Same as above, for 'right'. _tab_insert_idx_right: Same as above, for 'right'.
_undo_stack: List of UndoEntry namedtuples of closed tabs. _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: Signals:
cur_progress: Progress of the current tab changed (loadProgress). cur_progress: Progress of the current tab changed (loadProgress).
@ -82,6 +82,7 @@ class TabbedBrowser(tabwidget.TabWidget):
widget can adjust its size to it. widget can adjust its size to it.
arg: The new size. arg: The new size.
current_tab_changed: The current tab changed to the emitted WebView. 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) cur_progress = pyqtSignal(int)
@ -96,13 +97,14 @@ class TabbedBrowser(tabwidget.TabWidget):
resized = pyqtSignal('QRect') resized = pyqtSignal('QRect')
got_cmd = pyqtSignal(str) got_cmd = pyqtSignal(str)
current_tab_changed = pyqtSignal(webview.WebView) current_tab_changed = pyqtSignal(webview.WebView)
new_tab = pyqtSignal(webview.WebView, int)
def __init__(self, win_id, parent=None): def __init__(self, win_id, parent=None):
super().__init__(win_id, parent) super().__init__(win_id, parent)
self._win_id = win_id self._win_id = win_id
self._tab_insert_idx_left = 0 self._tab_insert_idx_left = 0
self._tab_insert_idx_right = -1 self._tab_insert_idx_right = -1
self._shutting_down = False self.shutting_down = False
self.tabCloseRequested.connect(self.on_tab_close_requested) self.tabCloseRequested.connect(self.on_tab_close_requested)
self.currentChanged.connect(self.on_current_changed) self.currentChanged.connect(self.on_current_changed)
self.cur_load_started.connect(self.on_cur_load_started) self.cur_load_started.connect(self.on_cur_load_started)
@ -234,7 +236,7 @@ class TabbedBrowser(tabwidget.TabWidget):
def shutdown(self): def shutdown(self):
"""Try to shut down all tabs cleanly.""" """Try to shut down all tabs cleanly."""
self._shutting_down = True self.shutting_down = True
for tab in self.widgets(): for tab in self.widgets():
self._remove_tab(tab) self._remove_tab(tab)
@ -398,6 +400,7 @@ class TabbedBrowser(tabwidget.TabWidget):
if not background: if not background:
self.setCurrentWidget(tab) self.setCurrentWidget(tab)
tab.show() tab.show()
self.new_tab.emit(tab, idx)
return tab return tab
def _get_new_tab_idx(self, explicit): def _get_new_tab_idx(self, explicit):
@ -546,7 +549,7 @@ class TabbedBrowser(tabwidget.TabWidget):
@pyqtSlot(int) @pyqtSlot(int)
def on_current_changed(self, idx): def on_current_changed(self, idx):
"""Set last-focused-tab and leave hinting mode when focus changed.""" """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 # closing the last tab (before quitting) or shutting down
return return
tab = self.widget(idx) tab = self.widget(idx)

View File

@ -237,7 +237,7 @@ KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
# Available command completions # Available command completions
Completion = enum('Completion', ['command', 'section', 'option', 'value', Completion = enum('Completion', ['command', 'section', 'option', 'value',
'helptopic', 'quickmark_by_name', '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. # Exit statuses for errors. Needs to be an int for sys.exit.

View File

@ -709,3 +709,116 @@ Feature: Tab management
- data/hints/link.html - data/hints/link.html
- about:blank - about:blank
- data/hello.txt (active) - 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