Merge remote-tracking branch 'upstream/master' into test_short_dict

This commit is contained in:
Julian Weigt 2016-08-11 17:51:45 +02:00
commit 060a3998c6
31 changed files with 561 additions and 198 deletions

View File

@ -30,6 +30,8 @@ Added
(to report bugs which are difficult to reproduce).
- New `hide-unmatched-rapid-hints` option to not hide hint unmatched hint labels
in rapid mode.
- New `{clipboard}` and `{primary}` replacements for the commandline which
replace the `:paste` command.
Changed
~~~~~~~
@ -58,11 +60,19 @@ Changed
- With `new-instance-open-target` set to a tab option, the tab is now opened in
the most recently focused (instead of the last opened) window. This can be
configured with the new `new-instance-open-target.window` setting.
It can also be set to `last-visible` to show the pages in the most recently
visible window.
- Word hints now are more clever about getting the element text from some elements.
- Completions for `:help` and `:bind` now also show hidden commands
- The `:buffer` completion now also filters using the first column (id).
- `:undo` has been improved to reopen tabs at the position they were closed.
Deprecated
~~~~~~~~~~
- The `:paste` command got deprecated as `:open` with `{clipboard}` and
`{primary}` can be used instead.
Removed
~~~~~~~
@ -79,6 +89,7 @@ Fixed
- `:bind` can now be used to bind to an alias (binding by editing `keys.conf`
already worked before)
- The command completion now updates correctly when changing aliases
- `:undo` now doesn't undo tabs "closed" by `:tab-detach` anymore.
v0.8.3 (unreleased)
-------------------

View File

@ -145,12 +145,12 @@ Contributors, sorted by the number of commits in descending order:
* Antoni Boucher
* Lamar Pavel
* Bruno Oliveira
* Jan Verbeek
* Alexander Cogneau
* Marshall Lochbaum
* Jakub Klinkovský
* Felix Van der Jeugt
* Martin Tournoij
* Jan Verbeek
* Raphael Pierzina
* Joel Torstensson
* Patric Schmitz
@ -192,6 +192,7 @@ Contributors, sorted by the number of commits in descending order:
* Nick Ginther
* Michał Góral
* Michael Ilsaas
* Michael Hoang
* Martin Zimmermann
* Fritz Reichwald
* Brian Jackson
@ -213,7 +214,6 @@ Contributors, sorted by the number of commits in descending order:
* adam
* Samir Benmendil
* Regina Hug
* Michael Hoang
* Mathias Fussenegger
* Marcelo Santos
* Jean-Louis Fuchs

View File

@ -38,7 +38,6 @@
|<<messages,messages>>|Show a log of past messages.
|<<navigate,navigate>>|Open typical prev/next links or navigate using the URL path.
|<<open,open>>|Open a URL in the current/[count]th tab.
|<<paste,paste>>|Open a page from the clipboard.
|<<print,print>>|Print the current/[count]th tab.
|<<quickmark-add,quickmark-add>>|Add a new quickmark.
|<<quickmark-del,quickmark-del>>|Delete a quickmark.
@ -487,6 +486,8 @@ Syntax: +:open [*--implicit*] [*--bg*] [*--tab*] [*--window*] ['url']+
Open a URL in the current/[count]th tab.
If the URL contains newlines, each line gets opened in its own tab.
==== positional arguments
* +'url'+: The URL to open.
@ -503,20 +504,6 @@ The tab index to open the URL in.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
[[paste]]
=== paste
Syntax: +:paste [*--sel*] [*--tab*] [*--bg*] [*--window*]+
Open a page from the clipboard.
If the pasted text contains newlines, each line gets opened in its own tab.
==== optional arguments
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
* +*-t*+, +*--tab*+: Open in a new tab.
* +*-b*+, +*--bg*+: Open in a background tab.
* +*-w*+, +*--window*+: Open in new window.
[[print]]
=== print
Syntax: +:print [*--preview*] [*--pdf* 'file']+

View File

@ -456,6 +456,7 @@ Valid values:
* +last-opened+: Open new tabs in the last opened window.
* +last-focused+: Open new tabs in the most recently focused window.
* +last-visible+: Open new tabs in the most recently visible window.
Default: +pass:[last-focused]+

View File

@ -350,6 +350,9 @@ def on_focus_changed(_old, new):
window = new.window()
if isinstance(window, mainwindow.MainWindow):
objreg.register('last-focused-main-window', window, update=True)
# A focused window must also be visible, and in this case we should
# consider it as the most recently looked-at window
objreg.register('last-visible-main-window', window, update=True)
def open_desktopservices_url(url):

View File

@ -220,19 +220,6 @@ class AbstractZoom(QObject):
default_zoom = config.get('ui', 'default-zoom')
self._set_factor_internal(float(default_zoom) / 100)
@pyqtSlot(QPoint)
def _on_mouse_wheel_zoom(self, delta):
"""Handle zooming via mousewheel requested by the web view."""
divider = config.get('input', 'mouse-zoom-divider')
factor = self.factor() + delta.y() / divider
if factor < 0:
return
perc = int(100 * factor)
message.info(self._win_id, "Zoom level: {}%".format(perc))
self._neighborlist.fuzzyval = perc
self._set_factor_internal(factor)
self._default_zoom_changed = True
class AbstractCaret(QObject):
@ -485,9 +472,6 @@ class AbstractTab(QWidget):
self._mouse_event_filter = mouse.MouseEventFilter(self, parent=self)
self.backend = None
def _event_filter_target(self):
raise NotImplementedError
def _set_widget(self, widget):
# pylint: disable=protected-access
self._widget = widget
@ -498,9 +482,10 @@ class AbstractTab(QWidget):
self.zoom._widget = widget
self.search._widget = widget
self.printing._widget = widget
widget.mouse_wheel_zoom.connect(self.zoom._on_mouse_wheel_zoom)
event_filter_target = self._event_filter_target()
event_filter_target.installEventFilter(self._mouse_event_filter)
self._install_event_filter()
def _install_event_filter(self):
raise NotImplementedError
def _set_load_status(self, val):
"""Setter for load_status."""

View File

@ -236,6 +236,8 @@ class CommandDispatcher:
bg=False, tab=False, window=False, count=None):
"""Open a URL in the current/[count]th tab.
If the URL contains newlines, each line gets opened in its own tab.
Args:
url: The URL to open.
bg: Open in a new background tab.
@ -247,35 +249,73 @@ class CommandDispatcher:
"""
if url is None:
if tab or bg or window:
url = config.get('general', 'default-page')
urls = [config.get('general', 'default-page')]
else:
raise cmdexc.CommandError("No URL given, but -t/-b/-w is not "
"set!")
else:
try:
url = objreg.get('quickmark-manager').get(url)
except urlmarks.Error:
try:
url = urlutils.fuzzy_url(url)
except urlutils.InvalidUrlError as e:
# We don't use cmdexc.CommandError here as this can be
# called async from edit_url
message.error(self._win_id, str(e))
return
if tab or bg or window:
self._open(url, tab, bg, window, not implicit)
else:
curtab = self._cntwidget(count)
if curtab is None:
if count is None:
# We want to open a URL in the current tab, but none exists
# yet.
self._tabbed_browser.tabopen(url)
else:
# Explicit count with a tab that doesn't exist.
return
urls = self._parse_url_input(url)
for i, cur_url in enumerate(urls):
if not window and i > 0:
tab = False
bg = True
if tab or bg or window:
self._open(cur_url, tab, bg, window, not implicit)
else:
curtab.openurl(url)
curtab = self._cntwidget(count)
if curtab is None:
if count is None:
# We want to open a URL in the current tab, but none
# exists yet.
self._tabbed_browser.tabopen(cur_url)
else:
# Explicit count with a tab that doesn't exist.
return
else:
curtab.openurl(cur_url)
def _parse_url(self, url, *, force_search=False):
"""Parse a URL or quickmark or search query.
Args:
url: The URL to parse.
force_search: Whether to force a search even if the content can be
interpreted as a URL or a path.
Return:
A URL that can be opened.
"""
try:
return objreg.get('quickmark-manager').get(url)
except urlmarks.Error:
try:
return urlutils.fuzzy_url(url, force_search=force_search)
except urlutils.InvalidUrlError as e:
# We don't use cmdexc.CommandError here as this can be
# called async from edit_url
message.error(self._win_id, str(e))
return None
def _parse_url_input(self, url):
"""Parse a URL or newline-separated list of URLs.
Args:
url: The URL or list to parse.
Return:
A list of URLs that can be opened.
"""
force_search = False
urllist = [u for u in url.split('\n') if u.strip()]
if (len(urllist) > 1 and not urlutils.is_url(urllist[0]) and
urlutils.get_path_if_valid(urllist[0], check_exists=True)
is None):
urllist = [url]
force_search = True
for cur_url in urllist:
parsed = self._parse_url(cur_url, force_search=force_search)
if parsed is not None:
yield parsed
@cmdutils.register(instance='command-dispatcher', name='reload',
scope='window')
@ -387,7 +427,7 @@ class CommandDispatcher:
url = self._current_url()
self._open(url, window=True)
cur_widget = self._current_widget()
self._tabbed_browser.close_tab(cur_widget)
self._tabbed_browser.close_tab(cur_widget, add_undo=False)
def _back_forward(self, tab, bg, window, count, forward):
"""Helper function for :back/:forward."""
@ -796,7 +836,8 @@ class CommandDispatcher:
else:
raise cmdexc.CommandError("Last tab")
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.register(instance='command-dispatcher', scope='window',
deprecated="Use :open {clipboard}")
def paste(self, sel=False, tab=False, bg=False, window=False):
"""Open a page from the clipboard.
@ -810,15 +851,12 @@ class CommandDispatcher:
window: Open in new window.
"""
force_search = False
if sel and utils.supports_selection():
target = "Primary selection"
else:
if not utils.supports_selection():
sel = False
target = "Clipboard"
text = utils.get_clipboard(selection=sel)
if not text.strip():
raise cmdexc.CommandError("{} is empty.".format(target))
log.misc.debug("{} contained: {!r}".format(target, text))
try:
text = utils.get_clipboard(selection=sel)
except utils.ClipboardError as e:
raise cmdexc.CommandError(e)
text_urls = [u for u in text.split('\n') if u.strip()]
if (len(text_urls) > 1 and not urlutils.is_url(text_urls[0]) and
urlutils.get_path_if_valid(
@ -1463,9 +1501,12 @@ class CommandDispatcher:
raise cmdexc.CommandError("Focused element is not editable!")
try:
sel = utils.get_clipboard(selection=True)
except utils.SelectionUnsupportedError:
sel = utils.get_clipboard()
try:
sel = utils.get_clipboard(selection=True)
except utils.SelectionUnsupportedError:
sel = utils.get_clipboard()
except utils.ClipboardEmptyError:
return
log.misc.debug("Pasting primary selection into element {}".format(
elem.debug_text()))

View File

@ -21,24 +21,63 @@
from qutebrowser.config import config
from qutebrowser.utils import message
from qutebrowser.utils import message, log
from PyQt5.QtCore import QObject, QEvent, Qt
class ChildEventFilter(QObject):
"""An event filter re-adding MouseEventFilter on ChildEvent.
This is needed because QtWebEngine likes to randomly change its
focusProxy...
FIXME:qtwebengine Add a test for this happening
Attributes:
_filter: The event filter to install.
_widget: The widget expected to send out childEvents.
"""
def __init__(self, eventfilter, widget, parent=None):
super().__init__(parent)
self._filter = eventfilter
assert widget is not None
self._widget = widget
def eventFilter(self, obj, event):
"""Act on ChildAdded events."""
if event.type() == QEvent.ChildAdded:
child = event.child()
log.mouse.debug("{} got new child {}, installing filter".format(
obj, child))
assert obj is self._widget
child.installEventFilter(self._filter)
return False
class MouseEventFilter(QObject):
"""Handle mouse events on a tab."""
"""Handle mouse events on a tab.
Attributes:
_tab: The browsertab object this filter is installed on.
_handlers: A dict of handler functions for the handled events.
_ignore_wheel_event: Whether to ignore the next wheelEvent.
"""
def __init__(self, tab, parent=None):
super().__init__(parent)
self._tab = tab
self._handlers = {
QEvent.MouseButtonPress: self._handle_mouse_press,
QEvent.Wheel: self._handle_wheel,
}
self._ignore_wheel_event = False
def _handle_mouse_press(self, e):
def _handle_mouse_press(self, _obj, e):
"""Handle pressing of a mouse button."""
is_rocker_gesture = (config.get('input', 'rocker-gestures') and
e.buttons() == Qt.LeftButton | Qt.RightButton)
@ -46,6 +85,29 @@ class MouseEventFilter(QObject):
if e.button() in [Qt.XButton1, Qt.XButton2] or is_rocker_gesture:
self._mousepress_backforward(e)
return True
self._ignore_wheel_event = True
return False
def _handle_wheel(self, _obj, e):
"""Zoom on Ctrl-Mousewheel.
Args:
e: The QWheelEvent.
"""
if self._ignore_wheel_event:
# See https://github.com/The-Compiler/qutebrowser/issues/395
self._ignore_wheel_event = False
return True
if e.modifiers() & Qt.ControlModifier:
divider = config.get('input', 'mouse-zoom-divider')
factor = self._tab.zoom.factor() + (e.angleDelta().y() / divider)
if factor < 0:
return False
perc = int(100 * factor)
message.info(self._tab.win_id, "Zoom level: {}%".format(perc))
self._tab.zoom.set_factor(factor)
return False
def _mousepress_backforward(self, e):
@ -69,9 +131,9 @@ class MouseEventFilter(QObject):
message.error(self._tab.win_id, "At end of history.",
immediately=True)
def eventFilter(self, _obj, event):
def eventFilter(self, obj, event):
"""Filter events going to a QWeb(Engine)View."""
evtype = event.type()
if evtype not in self._handlers:
return False
return self._handlers[evtype](event)
return self._handlers[evtype](obj, event)

View File

@ -31,7 +31,7 @@ from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript
# pylint: enable=no-name-in-module,import-error,useless-suppression
from qutebrowser.browser import browsertab
from qutebrowser.browser import browsertab, mouse
from qutebrowser.browser.webengine import webview, webengineelem
from qutebrowser.utils import usertypes, qtutils, log, javascript, utils
@ -335,6 +335,7 @@ class WebEngineTab(browsertab.AbstractTab):
self.backend = usertypes.Backend.QtWebEngine
# init js stuff
self._init_js()
self._child_event_filter = None
def _init_js(self):
js_code = '\n'.join([
@ -359,8 +360,12 @@ class WebEngineTab(browsertab.AbstractTab):
# FIXME:qtwebengine what about runsOnSubFrames?
page.scripts().insert(script)
def _event_filter_target(self):
return self._widget.focusProxy()
def _install_event_filter(self):
self._widget.focusProxy().installEventFilter(self._mouse_event_filter)
self._child_event_filter = mouse.ChildEventFilter(
eventfilter=self._mouse_event_filter, widget=self._widget,
parent=self)
self._widget.installEventFilter(self._child_event_filter)
def openurl(self, url):
self._openurl_prepare(url)

View File

@ -33,24 +33,10 @@ class WebEngineView(QWebEngineView):
"""Custom QWebEngineView subclass with qutebrowser-specific features."""
mouse_wheel_zoom = pyqtSignal(QPoint)
def __init__(self, parent=None):
super().__init__(parent)
self.setPage(WebEnginePage(self))
def wheelEvent(self, e):
"""Zoom on Ctrl-Mousewheel.
Args:
e: The QWheelEvent.
"""
if e.modifiers() & Qt.ControlModifier:
e.accept()
self.mouse_wheel_zoom.emit(e.angleDelta())
else:
super().wheelEvent(e)
class WebEnginePage(QWebEnginePage):

View File

@ -510,8 +510,8 @@ class WebKitTab(browsertab.AbstractTab):
self.zoom.set_default()
self.backend = usertypes.Backend.QtWebKit
def _event_filter_target(self):
return self._widget
def _install_event_filter(self):
self._widget.installEventFilter(self._mouse_event_filter)
def openurl(self, url):
self._openurl_prepare(url)

View File

@ -47,22 +47,16 @@ class WebView(QWebView):
_old_scroll_pos: The old scroll position.
_check_insertmode: If True, in mouseReleaseEvent we should check if we
need to enter/leave insert mode.
_ignore_wheel_event: Ignore the next wheel event.
See https://github.com/The-Compiler/qutebrowser/issues/395
Signals:
scroll_pos_changed: Scroll percentage of current tab changed.
arg 1: x-position in %.
arg 2: y-position in %.
mouse_wheel_zoom: Emitted when the page should be zoomed because the
mousewheel was used with ctrl.
arg 1: The angle delta of the wheel event (QPoint)
shutting_down: Emitted when the view is shutting down.
"""
scroll_pos_changed = pyqtSignal(int, int)
shutting_down = pyqtSignal()
mouse_wheel_zoom = pyqtSignal(QPoint)
def __init__(self, win_id, tab_id, tab, parent=None):
super().__init__(parent)
@ -75,7 +69,6 @@ class WebView(QWebView):
self._check_insertmode = False
self.scroll_pos = (-1, -1)
self._old_scroll_pos = (-1, -1)
self._ignore_wheel_event = False
self._set_bg_color()
self._tab_id = tab_id
@ -388,7 +381,6 @@ class WebView(QWebView):
"""
self._mousepress_insertmode(e)
self._mousepress_opentarget(e)
self._ignore_wheel_event = True
super().mousePressEvent(e)
def mouseReleaseEvent(self, e):
@ -404,19 +396,3 @@ class WebView(QWebView):
self.shutting_down.connect(menu.close)
modeman.instance(self.win_id).entered.connect(menu.close)
menu.exec_(e.globalPos())
def wheelEvent(self, e):
"""Zoom on Ctrl-Mousewheel.
Args:
e: The QWheelEvent.
"""
if self._ignore_wheel_event:
self._ignore_wheel_event = False
# See https://github.com/The-Compiler/qutebrowser/issues/395
return
if e.modifiers() & Qt.ControlModifier:
e.accept()
self.mouse_wheel_zoom.emit(e.angleDelta())
else:
super().wheelEvent(e)

View File

@ -26,7 +26,7 @@ from PyQt5.QtCore import pyqtSlot, QUrl, QObject
from qutebrowser.config import config, configexc
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import message, objreg, qtutils
from qutebrowser.utils import message, objreg, qtutils, utils
from qutebrowser.misc import split
@ -49,21 +49,29 @@ def _current_url(tabbed_browser):
def replace_variables(win_id, arglist):
"""Utility function to replace variables like {url} in a list of args."""
variables = {
'{url}': lambda: _current_url(tabbed_browser).toString(
QUrl.FullyEncoded | QUrl.RemovePassword),
'{url:pretty}': lambda: _current_url(tabbed_browser).toString(
QUrl.RemovePassword),
'{clipboard}': utils.get_clipboard,
'{primary}': lambda: utils.get_clipboard(selection=True),
}
values = {}
args = []
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
if any('{url}' in arg for arg in arglist):
url = _current_url(tabbed_browser).toString(QUrl.FullyEncoded |
QUrl.RemovePassword)
if any('{url:pretty}' in arg for arg in arglist):
pretty_url = _current_url(tabbed_browser).toString(QUrl.RemovePassword)
for arg in arglist:
if '{url}' in arg:
args.append(arg.replace('{url}', url))
elif '{url:pretty}' in arg:
args.append(arg.replace('{url:pretty}', pretty_url))
else:
try:
for arg in arglist:
for var, func in variables.items():
if var in arg:
if var not in values:
values[var] = func()
arg = arg.replace(var, values[var])
args.append(arg)
except utils.ClipboardError as e:
raise cmdexc.CommandError(e)
return args

View File

@ -233,7 +233,9 @@ def data(readonly=False):
('last-opened', "Open new tabs in the last opened "
"window."),
('last-focused', "Open new tabs in the most recently "
"focused window.")
"focused window."),
('last-visible', "Open new tabs in the most recently "
"visible window.")
)), 'last-focused'),
"Which window to choose when opening links as new tabs."),
@ -1521,12 +1523,12 @@ KEY_DATA = collections.OrderedDict([
('yank domain -s', ['yD']),
('yank pretty-url', ['yp']),
('yank pretty-url -s', ['yP']),
('paste', ['pp']),
('paste -s', ['pP']),
('paste -t', ['Pp']),
('paste -ts', ['PP']),
('paste -w', ['wp']),
('paste -ws', ['wP']),
('open {clipboard}', ['pp']),
('open {primary}', ['pP']),
('open -t {clipboard}', ['Pp']),
('open -t {primary}', ['PP']),
('open -w {clipboard}', ['wp']),
('open -w {primary}', ['wP']),
('quickmark-save', ['m']),
('set-cmd-text -s :quickmark-load', ['b']),
('set-cmd-text -s :quickmark-load -t', ['B']),
@ -1696,6 +1698,11 @@ CHANGED_KEY_COMMANDS = [
(re.compile(r'^yank-selected -p'), r'yank selection -s'),
(re.compile(r'^yank-selected'), r'yank selection'),
(re.compile(r'^paste$'), r'open {clipboard}'),
(re.compile(r'^paste -([twb])$'), r'open -\1 {clipboard}'),
(re.compile(r'^paste -([twb])s$'), r'open -\1 {primary}'),
(re.compile(r'^paste -s([twb])$'), r'open -\1 {primary}'),
(re.compile(r'^completion-item-next'), r'completion-item-focus next'),
(re.compile(r'^completion-item-prev'), r'completion-item-focus prev'),
]

View File

@ -28,26 +28,41 @@ window._qutebrowser.scroll = (function() {
var y_px = window.scrollY;
if (x !== undefined) {
x_px = (elem.scrollWidth - elem.clientWidth) / 100 * x;
x_px = (elem.scrollWidth - window.innerWidth) / 100 * x;
}
if (y !== undefined) {
y_px = (elem.scrollHeight - elem.clientHeight) / 100 * y;
y_px = (elem.scrollHeight - window.innerHeight) / 100 * y;
}
/*
console.log(JSON.stringify({
"x": x,
"window.scrollX": window.scrollX,
"window.innerWidth": window.innerWidth,
"elem.scrollWidth": elem.scrollWidth,
"x_px": x_px,
"y": y,
"window.scrollY": window.scrollY,
"window.innerHeight": window.innerHeight,
"elem.scrollHeight": elem.scrollHeight,
"y_px": y_px,
}));
*/
window.scroll(x_px, y_px);
};
funcs.delta_page = function(x, y) {
var dx = document.documentElement.clientWidth * x;
var dy = document.documentElement.clientHeight * y;
var dx = window.innerWidth * x;
var dy = window.innerHeight * y;
window.scrollBy(dx, dy);
};
funcs.pos = function() {
var elem = document.documentElement;
var dx = elem.scrollWidth - elem.clientWidth;
var dy = elem.scrollHeight - elem.clientHeight;
var dx = elem.scrollWidth - window.innerWidth;
var dy = elem.scrollHeight - window.innerHeight;
var perc_x, perc_y;
if (dx === 0) {

View File

@ -74,6 +74,8 @@ def get_window(via_ipc, force_window=False, force_tab=False,
window = objreg.last_focused_window()
elif win_mode == 'last-opened':
window = objreg.last_window()
elif win_mode == 'last-visible':
window = objreg.last_visible_window()
except objreg.NoWindow:
# There is no window left, so we open a new one
window = MainWindow()
@ -458,8 +460,23 @@ class MainWindow(QWidget):
self._downloadview.updateGeometry()
self.tabbed_browser.tabBar().refresh()
def showEvent(self, e):
"""Extend showEvent to register us as the last-visible-main-window.
Args:
e: The QShowEvent
"""
super().showEvent(e)
objreg.register('last-visible-main-window', self, update=True)
def _do_close(self):
"""Helper function for closeEvent."""
last_visible = objreg.get('last-visible-main-window')
if self is last_visible:
try:
objreg.delete('last-visible-main-window')
except KeyError:
pass
objreg.get('session-manager').save_last_window_session()
self._save_geometry()
log.destroy.debug("Closing window {}".format(self.win_id))

View File

@ -217,11 +217,12 @@ class TabbedBrowser(tabwidget.TabWidget):
for tab in self.widgets():
self._remove_tab(tab)
def close_tab(self, tab):
def close_tab(self, tab, *, add_undo=True):
"""Close a tab.
Args:
tab: The QWebView to be closed.
add_undo: Whether the tab close can be undone.
"""
last_close = config.get('tabs', 'last-close')
count = self.count()
@ -229,7 +230,7 @@ class TabbedBrowser(tabwidget.TabWidget):
if last_close == 'ignore' and count == 1:
return
self._remove_tab(tab)
self._remove_tab(tab, add_undo=add_undo)
if count == 1: # We just closed the last tab above.
if last_close == 'close':
@ -243,11 +244,12 @@ class TabbedBrowser(tabwidget.TabWidget):
url = config.get('general', 'default-page')
self.openurl(url, newtab=True)
def _remove_tab(self, tab):
def _remove_tab(self, tab, *, add_undo=True):
"""Remove a tab from the tab list and delete it properly.
Args:
tab: The QWebView to be closed.
add_undo: Whether the tab close can be undone.
"""
idx = self.indexOf(tab)
if idx == -1:
@ -261,8 +263,9 @@ class TabbedBrowser(tabwidget.TabWidget):
window=self._win_id)
if tab.url().isValid():
history_data = tab.history.serialize()
entry = UndoEntry(tab.url(), history_data, idx)
self._undo_stack.append(entry)
if add_undo:
entry = UndoEntry(tab.url(), history_data, idx)
self._undo_stack.append(entry)
elif tab.url().isEmpty():
# There are some good reasons why a URL could be empty
# (target="_blank" with a download, see [1]), so we silently ignore

View File

@ -47,7 +47,7 @@ class MinimalLineEditMixin:
if e.key() == Qt.Key_Insert and e.modifiers() == Qt.ShiftModifier:
try:
text = utils.get_clipboard(selection=True)
except utils.SelectionUnsupportedError:
except utils.ClipboardError:
pass
else:
e.accept()

View File

@ -285,6 +285,14 @@ def dump_objects():
return lines
def last_visible_window():
"""Get the last visible window, or the last focused window if none."""
try:
return get('last-visible-main-window')
except KeyError:
return last_focused_window()
def last_focused_window():
"""Get the last focused window, or the last window if none."""
try:

View File

@ -43,11 +43,21 @@ fake_clipboard = None
log_clipboard = False
class SelectionUnsupportedError(Exception):
class ClipboardError(Exception):
"""Raised if the clipboard contents are unavailable for some reason."""
class SelectionUnsupportedError(ClipboardError):
"""Raised if [gs]et_clipboard is used and selection=True is unsupported."""
class ClipboardEmptyError(ClipboardError):
"""Raised if get_clipboard is used and the clipboard is empty."""
def elide(text, length):
"""Elide text so it uses a maximum of length chars."""
if length < 1:
@ -810,6 +820,11 @@ def get_clipboard(selection=False):
mode = QClipboard.Selection if selection else QClipboard.Clipboard
data = QApplication.clipboard().text(mode=mode)
target = "Primary selection" if selection else "Clipboard"
if not data.strip():
raise ClipboardEmptyError("{} is empty.".format(target))
log.misc.debug("{} contained: {!r}".format(target, data))
return data

View File

@ -327,11 +327,13 @@ def generate_commands(filename):
for name, cmd in cmdutils.cmd_dict.items():
if name in cmdutils.aliases:
continue
if cmd.deprecated:
continue
if cmd.hide:
hidden_cmds.append((name, cmd))
elif cmd.debug:
debug_cmds.append((name, cmd))
elif not cmd.deprecated:
else:
normal_cmds.append((name, cmd))
normal_cmds.sort()
hidden_cmds.sort()

View File

@ -6,6 +6,6 @@
<title>Scrolling inside an iframe</title>
</head>
<body>
<iframe style="margin: 50px;" src="/data/scroll.html"></iframe>
<iframe style="margin: 50px;" src="/data/scroll/simple.html"></iframe>
</body>
</html>

View File

@ -0,0 +1,214 @@
<!-- This is the same as scroll.html but without <!DOCTYPE html> -->
<html>
<head>
<meta charset="utf-8">
<title>Scrolling without doctype</title>
</head>
<body>
<pre>
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
This is a very long line so this page can be scrolled horizontally. Did you think this line would end here already? Nah, it does not. But now it will. Or will it? I think it's not long enough yet. But depending on your screen size, this is not even enough yet, so I'm typing some more gibberish here. Hopefully that helps. I'm glad if it did, I'm always happy to help. Really, you're welcome. Okay, okay, can I stop now?
</pre>
<a href="/data/hello2.txt">next</a> link to test the --top-navigate argument for :scroll-page.
<a href="/data/hello3.txt">prev</a> link to test the --bottom-navigate argument for :scroll-page.
</body>
</html>

View File

@ -497,7 +497,7 @@ def should_quit(qtbot, quteproc):
def _get_scroll_values(quteproc):
data = quteproc.get_session()
pos = data['windows'][0]['tabs'][0]['history'][0]['scroll-pos']
pos = data['windows'][0]['tabs'][0]['history'][-1]['scroll-pos']
return (pos['x'], pos['y'])

View File

@ -488,7 +488,7 @@ Feature: Various utility commands.
Then the page should contain the plaintext "Local storage status: not working"
Scenario: :repeat-command
Given I open data/scroll.html
Given I open data/scroll/simple.html
And I run :tab-only
When I run :scroll down
And I run :repeat-command
@ -496,7 +496,7 @@ Feature: Various utility commands.
Then the page should be scrolled vertically
Scenario: :repeat-command with count
Given I open data/scroll.html
Given I open data/scroll/simple.html
And I run :tab-only
When I run :scroll down with count 3
And I run :scroll up
@ -504,7 +504,7 @@ Feature: Various utility commands.
Then the page should not be scrolled
Scenario: :repeat-command with not-normal command inbetween
Given I open data/scroll.html
Given I open data/scroll/simple.html
And I run :tab-only
When I run :scroll down with count 3
And I run :scroll up
@ -524,3 +524,16 @@ Feature: Various utility commands.
Then the following tabs should be open:
- data/hints/link_blank.html
- data/hello.txt (active)
## Variables
Scenario: {url} as part of an argument
When I open data/hello.txt
And I run :message-info foo{url}
Then the message "foohttp://localhost:*/hello.txt" should be shown
Scenario: Multiple variables in an argument
When I open data/hello.txt
And I put "foo" into the clipboard
And I run :message-info {clipboard}bar{url}
Then the message "foobarhttp://localhost:*/hello.txt" should be shown

View File

@ -2,7 +2,7 @@ Feature: Scrolling
Tests the various scroll commands.
Background:
Given I open data/scroll.html
Given I open data/scroll/simple.html
And I run :tab-only
## :scroll-px
@ -193,6 +193,12 @@ Feature: Scrolling
Scenario: :scroll-perc with count and argument
When I run :scroll-perc 0 with count 50
Then the page should be scrolled vertically
# https://github.com/The-Compiler/qutebrowser/issues/1821
Scenario: :scroll-perc without doctype
When I open data/scroll/no_doctype.html
And I run :scroll-perc 100
Then the page should be scrolled vertically
## :scroll-page

View File

@ -863,13 +863,13 @@ Feature: Tab management
Scenario: :buffer with a matching title
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/scroll/simple.html in a new tab
And I run :buffer "Searching text"
And I wait for "Current tab changed, focusing <qutebrowser.browser.* tab_id=* url='http://localhost:*/data/search.html'>" in the log
Then the following tabs should be open:
- data/title.html
- data/search.html (active)
- data/scroll.html
- data/scroll/simple.html
Scenario: :buffer with no matching title
When I run :buffer "invalid title"
@ -878,11 +878,11 @@ Feature: Tab management
Scenario: :buffer with matching title and two windows
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/scroll/simple.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"
And I wait for "Focus object changed: <qutebrowser.browser.* tab_id=* url='http://localhost:*/data/scroll.html'>" in the log
And I wait for "Focus object changed: <qutebrowser.browser.* tab_id=* url='http://localhost:*/data/scroll/simple.html'>" in the log
Then the session should look like:
windows:
- active: true
@ -894,7 +894,7 @@ Feature: Tab management
- url: http://localhost:*/data/search.html
- active: true
history:
- url: http://localhost:*/data/scroll.html
- url: http://localhost:*/data/scroll/simple.html
- tabs:
- history:
- url: http://localhost:*/data/caret.html
@ -916,7 +916,7 @@ Feature: Tab management
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 open data/scroll/simple.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
@ -933,7 +933,7 @@ Feature: Tab management
history:
- url: http://localhost:*/data/search.html
- history:
- url: http://localhost:*/data/scroll.html
- url: http://localhost:*/data/scroll/simple.html
- tabs:
- history:
- url: http://localhost:*/data/caret.html

View File

@ -1,6 +1,6 @@
Feature: Yanking and pasting.
:yank and :paste can be used to copy/paste the URL or title from/to the
clipboard and primary selection.
:yank, {clipboard} and {primary} can be used to copy/paste the URL or title
from/to the clipboard and primary selection.
Background:
Given I run :tab-only
@ -45,11 +45,11 @@ Feature: Yanking and pasting.
Then the message "Yanked URL to clipboard: http://localhost:(port)/data/title with spaces.html" should be shown
And the clipboard should contain "http://localhost:(port)/data/title with spaces.html"
#### :paste
#### {clipboard} and {primary}
Scenario: Pasting a URL
When I put "http://localhost:(port)/data/hello.txt" into the clipboard
And I run :paste
And I run :open {clipboard}
And I wait until data/hello.txt is loaded
Then the requests should be:
data/hello.txt
@ -57,32 +57,32 @@ Feature: Yanking and pasting.
Scenario: Pasting a URL from primary selection
When selection is supported
And I put "http://localhost:(port)/data/hello2.txt" into the primary selection
And I run :paste --sel
And I run :open {primary}
And I wait until data/hello2.txt is loaded
Then the requests should be:
data/hello2.txt
Scenario: Pasting with empty clipboard
When I put "" into the clipboard
And I run :paste
And I run :open {clipboard} (invalid command)
Then the error "Clipboard is empty." should be shown
Scenario: Pasting with empty selection
When selection is supported
And I put "" into the primary selection
And I run :paste --sel
And I run :open {primary} (invalid command)
Then the error "Primary selection is empty." should be shown
Scenario: Pasting with a space in clipboard
When I put " " into the clipboard
And I run :paste
And I run :open {clipboard} (invalid command)
Then the error "Clipboard is empty." should be shown
Scenario: Pasting in a new tab
Given I open about:blank
When I run :tab-only
And I put "http://localhost:(port)/data/hello.txt" into the clipboard
And I run :paste -t
And I run :open -t {clipboard}
And I wait until data/hello.txt is loaded
Then the following tabs should be open:
- about:blank
@ -92,7 +92,7 @@ Feature: Yanking and pasting.
Given I open about:blank
When I run :tab-only
And I put "http://localhost:(port)/data/hello.txt" into the clipboard
And I run :paste -b
And I run :open -b {clipboard}
And I wait until data/hello.txt is loaded
Then the following tabs should be open:
- about:blank (active)
@ -101,7 +101,7 @@ Feature: Yanking and pasting.
Scenario: Pasting in a new window
Given I have a fresh instance
When I put "http://localhost:(port)/data/hello.txt" into the clipboard
And I run :paste -w
And I run :open -w {clipboard}
And I wait until data/hello.txt is loaded
Then the session should look like:
windows:
@ -119,7 +119,7 @@ Feature: Yanking and pasting.
Scenario: Pasting an invalid URL
When I set general -> auto-search to false
And I put "foo bar" into the clipboard
And I run :paste
And I run :open {clipboard}
Then the error "Invalid URL" should be shown
Scenario: Pasting multiple urls in a new tab
@ -128,7 +128,7 @@ Feature: Yanking and pasting.
http://localhost:(port)/data/hello.txt
http://localhost:(port)/data/hello2.txt
http://localhost:(port)/data/hello3.txt
And I run :paste -t
And I run :open -t {clipboard}
And I wait until data/hello.txt is loaded
And I wait until data/hello2.txt is loaded
And I wait until data/hello3.txt is loaded
@ -145,7 +145,7 @@ Feature: Yanking and pasting.
this url:
http://qutebrowser.org
should not open
And I run :paste -t
And I run :open -t {clipboard}
And I wait until data/hello.txt?q=this%20url%3A%0Ahttp%3A//qutebrowser.org%0Ashould%20not%20open is loaded
Then the following tabs should be open:
- about:blank
@ -159,7 +159,7 @@ Feature: Yanking and pasting.
text:
should open
as search
And I run :paste -t
And I run :open -t {clipboard}
And I wait until data/hello.txt?q=text%3A%0Ashould%20open%0Aas%20search is loaded
Then the following tabs should be open:
- about:blank
@ -172,7 +172,7 @@ Feature: Yanking and pasting.
http://localhost:(port)/data/hello.txt
http://localhost:(port)/data/hello2.txt
http://localhost:(port)/data/hello3.txt
And I run :paste -b
And I run :open -b {clipboard}
And I wait until data/hello.txt is loaded
And I wait until data/hello2.txt is loaded
And I wait until data/hello3.txt is loaded
@ -188,7 +188,7 @@ Feature: Yanking and pasting.
http://localhost:(port)/data/hello.txt
http://localhost:(port)/data/hello2.txt
http://localhost:(port)/data/hello3.txt
And I run :paste -w
And I run :open -w {clipboard}
And I wait until data/hello.txt is loaded
And I wait until data/hello2.txt is loaded
And I wait until data/hello3.txt is loaded
@ -218,13 +218,13 @@ Feature: Yanking and pasting.
Scenario: Pasting multiple urls with an empty one
When I open about:blank
And I put "http://localhost:(port)/data/hello.txt\n\nhttp://localhost:(port)/data/hello2.txt" into the clipboard
And I run :paste -t
And I run :open -t {clipboard}
Then no crash should happen
Scenario: Pasting multiple urls with an almost empty one
When I open about:blank
And I put "http://localhost:(port)/data/hello.txt\n \nhttp://localhost:(port)/data/hello2.txt" into the clipboard
And I run :paste -t
And I run :open -t {clipboard}
Then no crash should happen
#### :paste-primary

View File

@ -19,7 +19,7 @@
import pytest
from PyQt5.QtCore import PYQT_VERSION, pyqtSignal, QPoint
from PyQt5.QtCore import PYQT_VERSION, pyqtSignal
from qutebrowser.browser import browsertab
from qutebrowser.keyinput import modeman
@ -29,22 +29,16 @@ pytestmark = pytest.mark.usefixtures('redirect_xdg_data')
try:
from PyQt5.QtWebKitWidgets import QWebView
class WebView(QWebView):
mouse_wheel_zoom = pyqtSignal(QPoint)
except ImportError:
WebView = None
QWebView = None
try:
from PyQt5.QtWebEngineWidgets import QWebEngineView
class WebEngineView(QWebEngineView):
mouse_wheel_zoom = pyqtSignal(QPoint)
except ImportError:
WebEngineView = None
QWebEngineView = None
@pytest.fixture(params=[WebView, WebEngineView])
@pytest.fixture(params=[QWebView, QWebEngineView])
def view(qtbot, config_stub, request):
config_stub.data = {
'input': {
@ -107,8 +101,8 @@ class Tab(browsertab.AbstractTab):
self.search = browsertab.AbstractSearch(parent=self)
self.printing = browsertab.AbstractPrinting()
def _event_filter_target(self):
return self._widget
def _install_event_filter(self):
pass
@pytest.mark.skipif(PYQT_VERSION < 0x050600,

View File

@ -298,6 +298,10 @@ class TestKeyConfigParser:
('yank -ds', 'yank domain -s'),
('yank -p', 'yank pretty-url'),
('yank -ps', 'yank pretty-url -s'),
('paste', 'open {clipboard}'),
('paste -t', 'open -t {clipboard}'),
('paste -ws', 'open -w {primary}'),
]
)
def test_migrations(self, old, new_expected):