Merge branch 'tab-complete' of https://github.com/toofar/qutebrowser into toofar-tab-complete
This commit is contained in:
commit
c1cec53c0e
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user