Merge branch 'rcorre-marks'

This commit is contained in:
Florian Bruhin 2016-04-21 22:55:44 +02:00
commit 39e8ac5159
15 changed files with 325 additions and 10 deletions

View File

@ -23,6 +23,10 @@ Added
- New `:edit-url` command to edit the URL in an external editor.
- New `network -> custom-headers` setting to send custom headers with every request.
- New `{url:pretty}` commandline replacement which gets replaced by the decoded URL.
- New marks to remember a scroll position:
- New `:jump-mark` command to jump to a mark, bound to `'`
- New `:set-mark` command to set a mark, bound to ```(backtick)
- The `'` mark gets set when moving away (hinting link with anchor, searching, etc.) so you can move back with `''`
Changed
~~~~~~~

View File

@ -158,6 +158,7 @@ Contributors, sorted by the number of commits in descending order:
* Joel Torstensson
* Patric Schmitz
* Claude
* Ryan Roden-Corrent
* meles5
* Tarcisio Fedrizzi
* Artur Shaik
@ -183,7 +184,6 @@ Contributors, sorted by the number of commits in descending order:
* Zach-Button
* Halfwit
* rikn00
* Ryan Roden-Corrent
* Michael Ilsaas
* Martin Zimmermann
* Brian Jackson

View File

@ -29,6 +29,7 @@
|<<home,home>>|Open main startpage in current tab.
|<<inspector,inspector>>|Toggle the web inspector.
|<<jseval,jseval>>|Evaluate a JavaScript string.
|<<jump-mark,jump-mark>>|Jump to the mark named by `key`.
|<<later,later>>|Execute a command after some time.
|<<navigate,navigate>>|Open typical prev/next links or navigate using the URL path.
|<<open,open>>|Open a URL in the current/[count]th tab.
@ -50,6 +51,7 @@
|<<session-save,session-save>>|Save a session.
|<<set,set>>|Set an option.
|<<set-cmd-text,set-cmd-text>>|Preset the statusbar to some text.
|<<set-mark,set-mark>>|Set a mark at the current scroll position in the current tab.
|<<spawn,spawn>>|Spawn a command in a shell.
|<<stop,stop>>|Stop loading in the current/[count]th tab.
|<<tab-clone,tab-clone>>|Duplicate the current tab.
@ -377,6 +379,15 @@ Evaluate a JavaScript string.
* 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.
[[jump-mark]]
=== jump-mark
Syntax: +:jump-mark 'key'+
Jump to the mark named by `key`.
==== positional arguments
* +'key'+: mark identifier; capital indicates a global mark
[[later]]
=== later
Syntax: +:later 'ms' 'command'+
@ -649,6 +660,15 @@ Preset the statusbar to some text.
* 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.
[[set-mark]]
=== set-mark
Syntax: +:set-mark 'key'+
Set a mark at the current scroll position in the current tab.
==== positional arguments
* +'key'+: mark identifier; capital indicates a global mark
[[spawn]]
=== spawn
Syntax: +:spawn [*--userscript*] [*--verbose*] [*--detach*] 'cmdline'+
@ -1244,7 +1264,7 @@ Scroll the current tab by 'count * dx/dy' pixels.
==== positional arguments
* +'dx'+: How much to scroll in x-direction.
* +'dy'+: How much to scroll in x-direction.
* +'dy'+: How much to scroll in y-direction.
==== count
multiplier

View File

@ -472,6 +472,9 @@ class CommandDispatcher:
bg: Open in a background tab.
window: Open in a new window.
"""
# save the pre-jump position in the special ' mark
self.set_mark("'")
cmdutils.check_exclusive((tab, bg, window), 'tbw')
widget = self._current_widget()
frame = widget.page().currentFrame()
@ -500,7 +503,7 @@ class CommandDispatcher:
Args:
dx: How much to scroll in x-direction.
dy: How much to scroll in x-direction.
dy: How much to scroll in y-direction.
count: multiplier
"""
dx *= count
@ -582,6 +585,9 @@ class CommandDispatcher:
horizontal: Scroll horizontally instead of vertically.
count: Percentage to scroll.
"""
# save the pre-jump position in the special ' mark
self.set_mark("'")
if perc is None and count is None:
perc = 100
elif perc is None:
@ -1414,6 +1420,7 @@ class CommandDispatcher:
text: The text to search for.
reverse: Reverse search direction.
"""
self.set_mark("'")
view = self._current_widget()
self._clear_search(view, text)
flags = 0
@ -1444,6 +1451,7 @@ class CommandDispatcher:
Args:
count: How many elements to ignore.
"""
self.set_mark("'")
view = self._current_widget()
self._clear_search(view, self._tabbed_browser.search_text)
@ -1464,6 +1472,7 @@ class CommandDispatcher:
Args:
count: How many elements to ignore.
"""
self.set_mark("'")
view = self._current_widget()
self._clear_search(view, self._tabbed_browser.search_text)
@ -1882,3 +1891,21 @@ class CommandDispatcher:
self.openurl, bg=bg, tab=tab, window=window, count=count))
ed.edit(url or self._current_url().toString())
@cmdutils.register(instance='command-dispatcher', scope='window')
def set_mark(self, key):
"""Set a mark at the current scroll position in the current tab.
Args:
key: mark identifier; capital indicates a global mark
"""
self._tabbed_browser.set_mark(key)
@cmdutils.register(instance='command-dispatcher', scope='window')
def jump_mark(self, key):
"""Jump to the mark named by `key`.
Args:
key: mark identifier; capital indicates a global mark
"""
self._tabbed_browser.jump_mark(key)

View File

@ -466,6 +466,13 @@ class HintManager(QObject):
QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton,
Qt.NoButton, modifiers),
]
if context.target in [Target.normal, Target.current]:
# Set the pre-jump mark ', so we can jump back here after following
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
tabbed_browser.set_mark("'")
if context.target == Target.current:
elem.remove_blank_target()
for evt in events:

View File

@ -1442,6 +1442,8 @@ KEY_DATA = collections.OrderedDict([
('search-prev', ['N']),
('enter-mode insert', ['i']),
('enter-mode caret', ['v']),
('enter-mode set_mark', ['`']),
('enter-mode jump_mark', ["'"]),
('yank', ['yy']),
('yank -s', ['yY']),
('yank -t', ['yt']),

View File

@ -67,9 +67,13 @@ class BaseKeyParser(QObject):
Signals:
keystring_updated: Emitted when the keystring is updated.
arg: New keystring.
request_leave: Emitted to request leaving a mode.
arg 0: Mode to leave.
arg 1: Reason for leaving.
"""
keystring_updated = pyqtSignal(str)
request_leave = pyqtSignal(usertypes.KeyMode, str)
do_log = True
passthrough = False

View File

@ -21,13 +21,13 @@
import functools
from PyQt5.QtCore import pyqtSignal, Qt, QObject, QEvent
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QObject, QEvent
from PyQt5.QtWidgets import QApplication
from qutebrowser.keyinput import modeparsers, keyparser
from qutebrowser.config import config
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import usertypes, log, objreg, utils
from qutebrowser.utils import usertypes, log, objreg, utils, usertypes
class KeyEvent:
@ -78,6 +78,8 @@ def init(win_id, parent):
warn=False),
KM.yesno: modeparsers.PromptKeyParser(win_id, modeman),
KM.caret: modeparsers.CaretKeyParser(win_id, modeman),
KM.set_mark: modeparsers.MarkKeyParser(win_id, KM.set_mark, modeman),
KM.jump_mark: modeparsers.MarkKeyParser(win_id, KM.jump_mark, modeman),
}
objreg.register('keyparsers', keyparsers, scope='window', window=win_id)
modeman.destroyed.connect(
@ -223,6 +225,7 @@ class ModeManager(QObject):
assert isinstance(mode, usertypes.KeyMode)
assert parser is not None
self._parsers[mode] = parser
parser.request_leave.connect(self.leave)
def enter(self, mode, reason=None, only_if_normal=False):
"""Enter a new mode.
@ -268,11 +271,12 @@ class ModeManager(QObject):
raise cmdexc.CommandError("Mode {} does not exist!".format(mode))
self.enter(m, 'command')
@pyqtSlot(usertypes.KeyMode, str)
def leave(self, mode, reason=None):
"""Leave a key mode.
Args:
mode: The name of the mode to leave.
mode: The mode to leave as a usertypes.KeyMode member.
reason: Why the mode was left.
"""
if self.mode != mode:

View File

@ -230,3 +230,55 @@ class CaretKeyParser(keyparser.CommandKeyParser):
super().__init__(win_id, parent, supports_count=True,
supports_chains=True)
self.read_config('caret')
class MarkKeyParser(keyparser.BaseKeyParser):
"""KeyParser for set_mark and jump_mark mode.
Attributes:
_mode: Either KeyMode.set_mark or KeyMode.jump_mark.
"""
def __init__(self, win_id, mode, parent=None):
super().__init__(win_id, parent, supports_count=False,
supports_chains=False)
self._mode = mode
def handle(self, e):
"""Override handle to always match the next key and create a mark.
Args:
e: the KeyPressEvent from Qt.
Return:
True if event has been handled, False otherwise.
"""
if utils.keyevent_to_string(e) is None:
# this is a modifier key, let it pass and keep going
return False
key = e.text()
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
if self._mode == usertypes.KeyMode.set_mark:
tabbed_browser.set_mark(key)
elif self._mode == usertypes.KeyMode.jump_mark:
tabbed_browser.jump_mark(key)
else:
raise ValueError("{} is not a valid mark mode".format(self._mode))
self.request_leave.emit(self._mode, "valid mark key")
return True
@pyqtSlot(str)
def on_keyconfig_changed(self, mode):
"""MarkKeyParser has no config section (no bindable keys)."""
pass
def execute(self, cmdstr, _keytype, count=None):
"""Should never be called on MarkKeyParser."""
assert False

View File

@ -30,7 +30,8 @@ from qutebrowser.config import config
from qutebrowser.keyinput import modeman
from qutebrowser.mainwindow import tabwidget
from qutebrowser.browser import signalfilter, webview
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg, urlutils
from qutebrowser.utils import (log, usertypes, utils, qtutils, objreg,
urlutils, message)
UndoEntry = collections.namedtuple('UndoEntry', ['url', 'history'])
@ -64,6 +65,8 @@ class TabbedBrowser(tabwidget.TabWidget):
_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.
_local_marks: Jump markers local to each page
_global_marks: Jump markers used across all pages
Signals:
cur_progress: Progress of the current tab changed (loadProgress).
@ -114,6 +117,8 @@ class TabbedBrowser(tabwidget.TabWidget):
self._now_focused = None
self.search_text = None
self.search_flags = 0
self._local_marks = {}
self._global_marks = {}
objreg.get('config').changed.connect(self.update_favicons)
objreg.get('config').changed.connect(self.update_window_title)
objreg.get('config').changed.connect(self.update_tab_titles)
@ -637,3 +642,53 @@ class TabbedBrowser(tabwidget.TabWidget):
self._now_focused.wheelEvent(e)
else:
e.ignore()
def set_mark(self, key):
"""Set a mark at the current scroll position in the current tab.
Args:
key: mark identifier; capital indicates a global mark
"""
# strip the fragment as it may interfere with scrolling
url = self.current_url().adjusted(QUrl.RemoveFragment)
point = self.currentWidget().page().currentFrame().scrollPosition()
if key.isupper():
self._global_marks[key] = point, url
else:
if url not in self._local_marks:
self._local_marks[url] = {}
self._local_marks[url][key] = point
def jump_mark(self, key):
"""Jump to the mark named by `key`.
Args:
key: mark identifier; capital indicates a global mark
"""
# consider urls that differ only in fragment to be identical
urlkey = self.current_url().adjusted(QUrl.RemoveFragment)
frame = self.currentWidget().page().currentFrame()
if key.isupper() and key in self._global_marks:
point, url = self._global_marks[key]
@pyqtSlot(bool)
def callback(ok):
if ok:
self.cur_load_finished.disconnect(callback)
frame.setScrollPosition(point)
self.openurl(url, newtab=False)
self.cur_load_finished.connect(callback)
elif urlkey in self._local_marks and key in self._local_marks[urlkey]:
point = self._local_marks[urlkey][key]
# save the pre-jump position in the special ' mark
# this has to happen after we read the mark, otherwise jump_mark
# "'" would just jump to the current position every time
self.set_mark("'")
frame.setScrollPosition(point)
else:
message.error(self._win_id, "Mark {} is not set".format(key))

View File

@ -231,7 +231,8 @@ ClickTarget = enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window'])
# Key input modes
KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
'insert', 'passthrough', 'caret'])
'insert', 'passthrough', 'caret', 'set_mark',
'jump_mark'])
# Available command completions

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Marks I</title>
</head>
<body>
<h1 id="top">Top</h1>
<a href="#top">Top</a>
<a href="#bottom">Bottom</a>
<div style="height: 3000px; width: 3000px;">Holy Grail</div>
<div style="height: 3000px; width: 3000px;">Waldo</div>
<div style="height: 3000px; width: 3000px;">Holy Grail</div>
<h1 id="bottom">Bottom</h1>
</body>
</html>

View File

@ -0,0 +1,91 @@
Feature: Setting positional marks
Background:
Given I open data/marks.html
And I run :tab-only
## :set-mark, :jump-mark
Scenario: Setting and jumping to a local mark
When I run :scroll-px 5 10
And I run :set-mark a
And I run :scroll-px 0 20
And I run :jump-mark a
Then the page should be scrolled to 5 10
Scenario: Jumping back after jumping to a particular percentage
When I run :scroll-px 10 20
And I run :scroll-perc 100
And I run :jump-mark "'"
Then the page should be scrolled to 10 20
Scenario: Setting the same local mark on another page
When I run :scroll-px 5 10
And I run :set-mark a
And I open data/marks.html
And I run :scroll-px 0 20
And I run :set-mark a
And I run :jump-mark a
Then the page should be scrolled to 0 20
Scenario: Jumping to a local mark after returning to a page
When I run :scroll-px 5 10
And I run :set-mark a
And I open data/numbers/1.txt
And I run :set-mark a
And I open data/marks.html
And I run :jump-mark a
Then the page should be scrolled to 5 10
Scenario: Setting and jumping to a global mark
When I run :scroll-px 5 20
And I run :set-mark A
And I open data/numbers/1.txt
And I run :jump-mark A
Then data/marks.html should be loaded
And the page should be scrolled to 5 20
Scenario: Jumping to an unset mark
When I run :jump-mark b
Then the error "Mark b is not set" should be shown
Scenario: Jumping to a local mark that was set on another page
When I run :set-mark b
And I open data/numbers/1.txt
And I run :jump-mark b
Then the error "Mark b is not set" should be shown
Scenario: Jumping to a local mark after changing fragments
When I open data/marks.html#top
And I run :scroll 'top'
And I run :scroll-px 10 10
And I run :set-mark a
When I open data/marks.html#bottom
And I run :jump-mark a
Then the page should be scrolled to 10 10
Scenario: Jumping back after following a link
When I run :hint links normal
And I run :follow-hint s
And I run :jump-mark "'"
Then the page should be scrolled to 0 0
Scenario: Jumping back after searching
When I run :scroll-px 20 15
And I run :search Waldo
And I run :jump-mark "'"
Then the page should be scrolled to 20 15
Scenario: Jumping back after search-next
When I run :search Grail
And I run :search-next
And I run :jump-mark "'"
Then the page should be scrolled to 0 0
Scenario: Hovering a hint does not set the ' mark
When I run :scroll-px 30 20
And I run :scroll-perc 0
And I run :hint links hover
And I run :follow-hint s
And I run :jump-mark "'"
Then the page should be scrolled to 30 20

View File

@ -0,0 +1,29 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016 Ryan Roden-Corrent (rcorre) <ryan@rcorre.net>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
import pytest_bdd as bdd
bdd.scenarios('marks.feature')
@bdd.then(bdd.parsers.parse("the page should be scrolled to {x} {y}"))
def check_y(quteproc, x, y):
data = quteproc.get_session()
pos = data['windows'][0]['tabs'][0]['history'][-1]['scroll-pos']
assert int(x) == pos['x']
assert int(y) == pos['y']

View File

@ -22,14 +22,17 @@ import pytest
from qutebrowser.keyinput import modeman as modeman_module
from qutebrowser.utils import usertypes
from PyQt5.QtCore import Qt
from PyQt5.QtCore import Qt, QObject, pyqtSignal
class FakeKeyparser:
class FakeKeyparser(QObject):
"""A fake BaseKeyParser which doesn't handle anything."""
request_leave = pyqtSignal(usertypes.KeyMode, str)
def __init__(self):
super().__init__()
self.passthrough = False
def handle(self, evt):