Adds new buffer command with completion.

`buffer` takes either a tab index or a string and focuses the specified
tab. The index can be of the form [0-9]+ which will switch to the
relevant tab in the current window or [0-9]+/[0-9]+ (that is
win_id/index) which will focus the specified window before switching
tabs. If a string is passed the list of open tabs across all windows is
sorted based on title and url (just like in the completion widget) and
the top result is selected.
This commit is contained in:
Jimmy 2016-02-18 19:01:30 +13:00
parent 4b4b3f2bc9
commit 1c10a1aecf
7 changed files with 154 additions and 6 deletions

View File

@ -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.

View File

@ -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:
@ -834,6 +835,60 @@ class CommandDispatcher:
raise cmdexc.CommandError(e)
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):

View File

@ -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,

View File

@ -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))

View File

@ -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)

View File

@ -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)

View File

@ -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.