Merge branch 'caret-line'
This commit is contained in:
commit
7a7d74dba1
|
|
@ -40,6 +40,7 @@ Added
|
|||
debugging.
|
||||
- New `colors.contextmenu.disabled.{fg,bg}` settings to customize colors for
|
||||
disabled items in the context menu.
|
||||
- New line selection mode (`:toggle-selection --line`), bound to `Shift-V` in caret mode.
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
|
|
|||
|
|
@ -1896,8 +1896,13 @@ This acts like readline's yank.
|
|||
|
||||
[[toggle-selection]]
|
||||
=== toggle-selection
|
||||
Syntax: +:toggle-selection [*--line*]+
|
||||
|
||||
Toggle caret selection mode.
|
||||
|
||||
==== optional arguments
|
||||
* +*-l*+, +*--line*+: Enables line-selection.
|
||||
|
||||
|
||||
== Debugging commands
|
||||
These commands are mainly intended for debugging. They are hidden if qutebrowser was started without the `--debug`-flag.
|
||||
|
|
|
|||
|
|
@ -446,6 +446,7 @@ Default:
|
|||
* +pass:[J]+: +pass:[scroll down]+
|
||||
* +pass:[K]+: +pass:[scroll up]+
|
||||
* +pass:[L]+: +pass:[scroll right]+
|
||||
* +pass:[V]+: +pass:[toggle-selection --line]+
|
||||
* +pass:[Y]+: +pass:[yank selection -s]+
|
||||
* +pass:[[]+: +pass:[move-to-start-of-prev-block]+
|
||||
* +pass:[]]+: +pass:[move-to-start-of-next-block]+
|
||||
|
|
|
|||
|
|
@ -427,13 +427,24 @@ class AbstractZoom(QObject):
|
|||
self._set_factor_internal(self._zoom_factor)
|
||||
|
||||
|
||||
class SelectionState(enum.Enum):
|
||||
|
||||
"""Possible states of selection in caret mode.
|
||||
|
||||
NOTE: Names need to line up with SelectionState in caret.js!
|
||||
"""
|
||||
|
||||
none = 1
|
||||
normal = 2
|
||||
line = 3
|
||||
|
||||
|
||||
class AbstractCaret(QObject):
|
||||
|
||||
"""Attribute ``caret`` of AbstractTab for caret browsing."""
|
||||
|
||||
#: Signal emitted when the selection was toggled.
|
||||
#: (argument - whether the selection is now active)
|
||||
selection_toggled = pyqtSignal(bool)
|
||||
selection_toggled = pyqtSignal(SelectionState)
|
||||
#: Emitted when a ``follow_selection`` action is done.
|
||||
follow_selected_done = pyqtSignal()
|
||||
|
||||
|
|
@ -442,7 +453,6 @@ class AbstractCaret(QObject):
|
|||
parent: QWidget = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._widget = typing.cast(QWidget, None)
|
||||
self.selection_enabled = False
|
||||
self._mode_manager = mode_manager
|
||||
mode_manager.entered.connect(self._on_mode_entered)
|
||||
mode_manager.left.connect(self._on_mode_left)
|
||||
|
|
@ -499,7 +509,7 @@ class AbstractCaret(QObject):
|
|||
def move_to_end_of_document(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def toggle_selection(self) -> None:
|
||||
def toggle_selection(self, line: bool = False) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def drop_selection(self) -> None:
|
||||
|
|
@ -825,6 +835,15 @@ class AbstractTabPrivate:
|
|||
def shutdown(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def run_js_sync(self, code: str) -> None:
|
||||
"""Run javascript sync.
|
||||
|
||||
Result will be returned when running JS is complete.
|
||||
This is only implemented for QtWebKit.
|
||||
For QtWebEngine, always raises UnsupportedOperationError.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AbstractTab(QWidget):
|
||||
|
||||
|
|
|
|||
|
|
@ -395,7 +395,10 @@ class WebEngineCaret(browsertab.AbstractCaret):
|
|||
if enabled is None:
|
||||
log.webview.debug("Ignoring selection status None")
|
||||
return
|
||||
self.selection_toggled.emit(enabled)
|
||||
if enabled:
|
||||
self.selection_toggled.emit(browsertab.SelectionState.normal)
|
||||
else:
|
||||
self.selection_toggled.emit(browsertab.SelectionState.none)
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def _on_mode_left(self, mode):
|
||||
|
|
@ -450,8 +453,9 @@ class WebEngineCaret(browsertab.AbstractCaret):
|
|||
def move_to_end_of_document(self):
|
||||
self._js_call('moveToEndOfDocument')
|
||||
|
||||
def toggle_selection(self):
|
||||
self._js_call('toggleSelection', callback=self.selection_toggled.emit)
|
||||
def toggle_selection(self, line=False):
|
||||
self._js_call('toggleSelection', line,
|
||||
callback=self._toggle_sel_translate)
|
||||
|
||||
def drop_selection(self):
|
||||
self._js_call('dropSelection')
|
||||
|
|
@ -526,6 +530,10 @@ class WebEngineCaret(browsertab.AbstractCaret):
|
|||
code = javascript.assemble('caret', command, *args)
|
||||
self._tab.run_js_async(code, callback)
|
||||
|
||||
def _toggle_sel_translate(self, state_str):
|
||||
state = browsertab.SelectionState[state_str]
|
||||
self.selection_toggled.emit(state)
|
||||
|
||||
|
||||
class WebEngineScroller(browsertab.AbstractScroller):
|
||||
|
||||
|
|
@ -1261,6 +1269,9 @@ class WebEngineTabPrivate(browsertab.AbstractTabPrivate):
|
|||
self._tab.action.exit_fullscreen()
|
||||
self._widget.shutdown()
|
||||
|
||||
def run_js_sync(self, code):
|
||||
raise browsertab.UnsupportedOperationError
|
||||
|
||||
|
||||
class WebEngineTab(browsertab.AbstractTab):
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import xml.etree.ElementTree
|
|||
|
||||
from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QPoint, QTimer, QSizeF, QSize
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QWidget
|
||||
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
from PyQt5.QtWidgets import QWidget
|
||||
|
|
@ -36,6 +37,7 @@ from qutebrowser.browser.webkit import (webview, tabhistory, webkitelem,
|
|||
from qutebrowser.utils import qtutils, usertypes, utils, log, debug
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.qt import sip
|
||||
from qutebrowser.keyinput import modeman
|
||||
|
||||
|
||||
class WebKitAction(browsertab.AbstractAction):
|
||||
|
|
@ -184,14 +186,18 @@ class WebKitCaret(browsertab.AbstractCaret):
|
|||
parent: QWidget = None) -> None:
|
||||
super().__init__(mode_manager, parent)
|
||||
self._tab = tab
|
||||
self._selection_state = browsertab.SelectionState.none
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def _on_mode_entered(self, mode):
|
||||
if mode != usertypes.KeyMode.caret:
|
||||
return
|
||||
|
||||
self.selection_enabled = self._widget.hasSelection()
|
||||
self.selection_toggled.emit(self.selection_enabled)
|
||||
if self._widget.hasSelection():
|
||||
self._selection_state = browsertab.SelectionState.normal
|
||||
else:
|
||||
self._selection_state = browsertab.SelectionState.none
|
||||
self.selection_toggled.emit(self._selection_state)
|
||||
settings = self._widget.settings()
|
||||
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
|
||||
|
||||
|
|
@ -206,7 +212,7 @@ class WebKitCaret(browsertab.AbstractCaret):
|
|||
#
|
||||
# Note: We can't use hasSelection() here, as that's always
|
||||
# true in caret mode.
|
||||
if not self.selection_enabled:
|
||||
if self._selection_state is browsertab.SelectionState.none:
|
||||
self._widget.page().currentFrame().evaluateJavaScript(
|
||||
utils.read_file('javascript/position_caret.js'))
|
||||
|
||||
|
|
@ -214,151 +220,189 @@ class WebKitCaret(browsertab.AbstractCaret):
|
|||
def _on_mode_left(self, _mode):
|
||||
settings = self._widget.settings()
|
||||
if settings.testAttribute(QWebSettings.CaretBrowsingEnabled):
|
||||
if self.selection_enabled and self._widget.hasSelection():
|
||||
if (self._selection_state is not browsertab.SelectionState.none and
|
||||
self._widget.hasSelection()):
|
||||
# Remove selection if it exists
|
||||
self._widget.triggerPageAction(QWebPage.MoveToNextChar)
|
||||
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False)
|
||||
self.selection_enabled = False
|
||||
self._selection_state = browsertab.SelectionState.none
|
||||
|
||||
def move_to_next_line(self, count=1):
|
||||
if not self.selection_enabled:
|
||||
act = QWebPage.MoveToNextLine
|
||||
else:
|
||||
if self._selection_state is not browsertab.SelectionState.none:
|
||||
act = QWebPage.SelectNextLine
|
||||
else:
|
||||
act = QWebPage.MoveToNextLine
|
||||
for _ in range(count):
|
||||
self._widget.triggerPageAction(act)
|
||||
if self._selection_state is browsertab.SelectionState.line:
|
||||
self._select_line_to_end()
|
||||
|
||||
def move_to_prev_line(self, count=1):
|
||||
if not self.selection_enabled:
|
||||
act = QWebPage.MoveToPreviousLine
|
||||
else:
|
||||
if self._selection_state is not browsertab.SelectionState.none:
|
||||
act = QWebPage.SelectPreviousLine
|
||||
else:
|
||||
act = QWebPage.MoveToPreviousLine
|
||||
for _ in range(count):
|
||||
self._widget.triggerPageAction(act)
|
||||
if self._selection_state is browsertab.SelectionState.line:
|
||||
self._select_line_to_start()
|
||||
|
||||
def move_to_next_char(self, count=1):
|
||||
if not self.selection_enabled:
|
||||
act = QWebPage.MoveToNextChar
|
||||
else:
|
||||
if self._selection_state is browsertab.SelectionState.normal:
|
||||
act = QWebPage.SelectNextChar
|
||||
elif self._selection_state is browsertab.SelectionState.line:
|
||||
return
|
||||
else:
|
||||
act = QWebPage.MoveToNextChar
|
||||
for _ in range(count):
|
||||
self._widget.triggerPageAction(act)
|
||||
|
||||
def move_to_prev_char(self, count=1):
|
||||
if not self.selection_enabled:
|
||||
act = QWebPage.MoveToPreviousChar
|
||||
else:
|
||||
if self._selection_state is browsertab.SelectionState.normal:
|
||||
act = QWebPage.SelectPreviousChar
|
||||
elif self._selection_state is browsertab.SelectionState.line:
|
||||
return
|
||||
else:
|
||||
act = QWebPage.MoveToPreviousChar
|
||||
for _ in range(count):
|
||||
self._widget.triggerPageAction(act)
|
||||
|
||||
def move_to_end_of_word(self, count=1):
|
||||
if not self.selection_enabled:
|
||||
act = [QWebPage.MoveToNextWord]
|
||||
if utils.is_windows: # pragma: no cover
|
||||
act.append(QWebPage.MoveToPreviousChar)
|
||||
else:
|
||||
if self._selection_state is browsertab.SelectionState.normal:
|
||||
act = [QWebPage.SelectNextWord]
|
||||
if utils.is_windows: # pragma: no cover
|
||||
act.append(QWebPage.SelectPreviousChar)
|
||||
elif self._selection_state is browsertab.SelectionState.line:
|
||||
return
|
||||
else:
|
||||
act = [QWebPage.MoveToNextWord]
|
||||
if utils.is_windows: # pragma: no cover
|
||||
act.append(QWebPage.MoveToPreviousChar)
|
||||
for _ in range(count):
|
||||
for a in act:
|
||||
self._widget.triggerPageAction(a)
|
||||
|
||||
def move_to_next_word(self, count=1):
|
||||
if not self.selection_enabled:
|
||||
act = [QWebPage.MoveToNextWord]
|
||||
if not utils.is_windows: # pragma: no branch
|
||||
act.append(QWebPage.MoveToNextChar)
|
||||
else:
|
||||
if self._selection_state is browsertab.SelectionState.normal:
|
||||
act = [QWebPage.SelectNextWord]
|
||||
if not utils.is_windows: # pragma: no branch
|
||||
act.append(QWebPage.SelectNextChar)
|
||||
elif self._selection_state is browsertab.SelectionState.line:
|
||||
return
|
||||
else:
|
||||
act = [QWebPage.MoveToNextWord]
|
||||
if not utils.is_windows: # pragma: no branch
|
||||
act.append(QWebPage.MoveToNextChar)
|
||||
for _ in range(count):
|
||||
for a in act:
|
||||
self._widget.triggerPageAction(a)
|
||||
|
||||
def move_to_prev_word(self, count=1):
|
||||
if not self.selection_enabled:
|
||||
act = QWebPage.MoveToPreviousWord
|
||||
else:
|
||||
if self._selection_state is browsertab.SelectionState.normal:
|
||||
act = QWebPage.SelectPreviousWord
|
||||
elif self._selection_state is browsertab.SelectionState.line:
|
||||
return
|
||||
else:
|
||||
act = QWebPage.MoveToPreviousWord
|
||||
for _ in range(count):
|
||||
self._widget.triggerPageAction(act)
|
||||
|
||||
def move_to_start_of_line(self):
|
||||
if not self.selection_enabled:
|
||||
act = QWebPage.MoveToStartOfLine
|
||||
else:
|
||||
if self._selection_state is browsertab.SelectionState.normal:
|
||||
act = QWebPage.SelectStartOfLine
|
||||
elif self._selection_state is browsertab.SelectionState.line:
|
||||
return
|
||||
else:
|
||||
act = QWebPage.MoveToStartOfLine
|
||||
self._widget.triggerPageAction(act)
|
||||
|
||||
def move_to_end_of_line(self):
|
||||
if not self.selection_enabled:
|
||||
act = QWebPage.MoveToEndOfLine
|
||||
else:
|
||||
if self._selection_state is browsertab.SelectionState.normal:
|
||||
act = QWebPage.SelectEndOfLine
|
||||
elif self._selection_state is browsertab.SelectionState.line:
|
||||
return
|
||||
else:
|
||||
act = QWebPage.MoveToEndOfLine
|
||||
self._widget.triggerPageAction(act)
|
||||
|
||||
def move_to_start_of_next_block(self, count=1):
|
||||
if not self.selection_enabled:
|
||||
act = [QWebPage.MoveToNextLine,
|
||||
QWebPage.MoveToStartOfBlock]
|
||||
else:
|
||||
if self._selection_state is not browsertab.SelectionState.none:
|
||||
act = [QWebPage.SelectNextLine,
|
||||
QWebPage.SelectStartOfBlock]
|
||||
else:
|
||||
act = [QWebPage.MoveToNextLine,
|
||||
QWebPage.MoveToStartOfBlock]
|
||||
for _ in range(count):
|
||||
for a in act:
|
||||
self._widget.triggerPageAction(a)
|
||||
if self._selection_state is browsertab.SelectionState.line:
|
||||
self._select_line_to_end()
|
||||
|
||||
def move_to_start_of_prev_block(self, count=1):
|
||||
if not self.selection_enabled:
|
||||
act = [QWebPage.MoveToPreviousLine,
|
||||
QWebPage.MoveToStartOfBlock]
|
||||
else:
|
||||
if self._selection_state is not browsertab.SelectionState.none:
|
||||
act = [QWebPage.SelectPreviousLine,
|
||||
QWebPage.SelectStartOfBlock]
|
||||
else:
|
||||
act = [QWebPage.MoveToPreviousLine,
|
||||
QWebPage.MoveToStartOfBlock]
|
||||
for _ in range(count):
|
||||
for a in act:
|
||||
self._widget.triggerPageAction(a)
|
||||
if self._selection_state is browsertab.SelectionState.line:
|
||||
self._select_line_to_start()
|
||||
|
||||
def move_to_end_of_next_block(self, count=1):
|
||||
if not self.selection_enabled:
|
||||
act = [QWebPage.MoveToNextLine,
|
||||
QWebPage.MoveToEndOfBlock]
|
||||
else:
|
||||
if self._selection_state is not browsertab.SelectionState.none:
|
||||
act = [QWebPage.SelectNextLine,
|
||||
QWebPage.SelectEndOfBlock]
|
||||
else:
|
||||
act = [QWebPage.MoveToNextLine,
|
||||
QWebPage.MoveToEndOfBlock]
|
||||
for _ in range(count):
|
||||
for a in act:
|
||||
self._widget.triggerPageAction(a)
|
||||
if self._selection_state is browsertab.SelectionState.line:
|
||||
self._select_line_to_end()
|
||||
|
||||
def move_to_end_of_prev_block(self, count=1):
|
||||
if not self.selection_enabled:
|
||||
act = [QWebPage.MoveToPreviousLine, QWebPage.MoveToEndOfBlock]
|
||||
else:
|
||||
if self._selection_state is not browsertab.SelectionState.none:
|
||||
act = [QWebPage.SelectPreviousLine, QWebPage.SelectEndOfBlock]
|
||||
else:
|
||||
act = [QWebPage.MoveToPreviousLine, QWebPage.MoveToEndOfBlock]
|
||||
for _ in range(count):
|
||||
for a in act:
|
||||
self._widget.triggerPageAction(a)
|
||||
if self._selection_state is browsertab.SelectionState.line:
|
||||
self._select_line_to_start()
|
||||
|
||||
def move_to_start_of_document(self):
|
||||
if not self.selection_enabled:
|
||||
act = QWebPage.MoveToStartOfDocument
|
||||
else:
|
||||
if self._selection_state is not browsertab.SelectionState.none:
|
||||
act = QWebPage.SelectStartOfDocument
|
||||
else:
|
||||
act = QWebPage.MoveToStartOfDocument
|
||||
self._widget.triggerPageAction(act)
|
||||
if self._selection_state is browsertab.SelectionState.line:
|
||||
self._select_line()
|
||||
|
||||
def move_to_end_of_document(self):
|
||||
if not self.selection_enabled:
|
||||
act = QWebPage.MoveToEndOfDocument
|
||||
else:
|
||||
if self._selection_state is not browsertab.SelectionState.none:
|
||||
act = QWebPage.SelectEndOfDocument
|
||||
else:
|
||||
act = QWebPage.MoveToEndOfDocument
|
||||
self._widget.triggerPageAction(act)
|
||||
|
||||
def toggle_selection(self):
|
||||
self.selection_enabled = not self.selection_enabled
|
||||
self.selection_toggled.emit(self.selection_enabled)
|
||||
def toggle_selection(self, line=False):
|
||||
if line:
|
||||
self._selection_state = browsertab.SelectionState.line
|
||||
self._select_line()
|
||||
self.reverse_selection()
|
||||
self._select_line()
|
||||
self.reverse_selection()
|
||||
elif self._selection_state is not browsertab.SelectionState.normal:
|
||||
self._selection_state = browsertab.SelectionState.normal
|
||||
else:
|
||||
self._selection_state = browsertab.SelectionState.none
|
||||
self.selection_toggled.emit(self._selection_state)
|
||||
|
||||
def drop_selection(self):
|
||||
self._widget.triggerPageAction(QWebPage.MoveToNextChar)
|
||||
|
|
@ -375,6 +419,32 @@ class WebKitCaret(browsertab.AbstractCaret):
|
|||
);
|
||||
}""")
|
||||
|
||||
def _select_line(self):
|
||||
self._widget.triggerPageAction(QWebPage.SelectStartOfLine)
|
||||
self.reverse_selection()
|
||||
self._widget.triggerPageAction(QWebPage.SelectEndOfLine)
|
||||
self.reverse_selection()
|
||||
|
||||
def _select_line_to_end(self):
|
||||
# direction of selection (if anchor is to the left or right
|
||||
# of focus) has to be checked before moving selection
|
||||
# to the end of line
|
||||
if self._js_selection_left_to_right():
|
||||
self._widget.triggerPageAction(QWebPage.SelectEndOfLine)
|
||||
|
||||
def _select_line_to_start(self):
|
||||
if not self._js_selection_left_to_right():
|
||||
self._widget.triggerPageAction(QWebPage.SelectStartOfLine)
|
||||
|
||||
def _js_selection_left_to_right(self):
|
||||
"""Return True iff the selection's direction is left to right."""
|
||||
return self._tab.private_api.run_js_sync("""
|
||||
var sel = window.getSelection();
|
||||
var position = sel.anchorNode.compareDocumentPosition(sel.focusNode);
|
||||
(!position && sel.anchorOffset < sel.focusOffset ||
|
||||
position === Node.DOCUMENT_POSITION_FOLLOWING);
|
||||
""")
|
||||
|
||||
def _follow_selected(self, *, tab=False):
|
||||
if QWebSettings.globalSettings().testAttribute(
|
||||
QWebSettings.JavascriptEnabled):
|
||||
|
|
@ -710,6 +780,11 @@ class WebKitTabPrivate(browsertab.AbstractTabPrivate):
|
|||
def shutdown(self):
|
||||
self._widget.shutdown()
|
||||
|
||||
def run_js_sync(self, code):
|
||||
document_element = self._widget.page().mainFrame().documentElement()
|
||||
result = document_element.evaluateJavaScript(code)
|
||||
return result
|
||||
|
||||
|
||||
class WebKitTab(browsertab.AbstractTab):
|
||||
|
||||
|
|
@ -771,8 +846,7 @@ class WebKitTab(browsertab.AbstractTab):
|
|||
def run_js_async(self, code, callback=None, *, world=None):
|
||||
if world is not None and world != usertypes.JsWorld.jseval:
|
||||
log.webview.warning("Ignoring world ID {}".format(world))
|
||||
document_element = self._widget.page().mainFrame().documentElement()
|
||||
result = document_element.evaluateJavaScript(code)
|
||||
result = self.private_api.run_js_sync(code)
|
||||
if callback is not None:
|
||||
callback(result)
|
||||
|
||||
|
|
|
|||
|
|
@ -185,9 +185,13 @@ def move_to_end_of_document(tab: apitypes.Tab) -> None:
|
|||
|
||||
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
def toggle_selection(tab: apitypes.Tab) -> None:
|
||||
"""Toggle caret selection mode."""
|
||||
tab.caret.toggle_selection()
|
||||
def toggle_selection(tab: apitypes.Tab, line: bool = False) -> None:
|
||||
"""Toggle caret selection mode.
|
||||
|
||||
Args:
|
||||
line: Enables line-selection.
|
||||
"""
|
||||
tab.caret.toggle_selection(line)
|
||||
|
||||
|
||||
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
|
||||
|
|
|
|||
|
|
@ -3078,6 +3078,7 @@ bindings.default:
|
|||
<Escape>: leave-mode
|
||||
caret:
|
||||
v: toggle-selection
|
||||
V: toggle-selection --line
|
||||
<Space>: toggle-selection
|
||||
<Ctrl-Space>: drop-selection
|
||||
c: enter-mode normal
|
||||
|
|
|
|||
|
|
@ -705,6 +705,18 @@ window._qutebrowser.caret = (function() {
|
|||
*/
|
||||
CaretBrowsing.isCaretVisible = false;
|
||||
|
||||
/**
|
||||
* Selection modes.
|
||||
* NOTE: Values need to line up with SelectionState in browsertab.py!
|
||||
*
|
||||
* @type {enum}
|
||||
*/
|
||||
CaretBrowsing.SelectionState = {
|
||||
"NONE": "none",
|
||||
"NORMAL": "normal",
|
||||
"LINE": "line",
|
||||
};
|
||||
|
||||
/**
|
||||
* The actual caret element, an absolute-positioned flashing line.
|
||||
* @type {Element}
|
||||
|
|
@ -887,7 +899,11 @@ window._qutebrowser.caret = (function() {
|
|||
CaretBrowsing.injectCaretStyles();
|
||||
CaretBrowsing.toggle();
|
||||
CaretBrowsing.initiated = true;
|
||||
CaretBrowsing.selectionEnabled = selectionRange > 0;
|
||||
if (selectionRange > 0) {
|
||||
CaretBrowsing.selectionState = CaretBrowsing.SelectionState.NORMAL;
|
||||
} else {
|
||||
CaretBrowsing.selectionState = CaretBrowsing.SelectionState.NONE;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -1145,16 +1161,45 @@ window._qutebrowser.caret = (function() {
|
|||
}
|
||||
};
|
||||
|
||||
CaretBrowsing.reverseSelection = () => {
|
||||
const sel = window.getSelection();
|
||||
sel.setBaseAndExtent(
|
||||
sel.extentNode, sel.extentOffset, sel.baseNode,
|
||||
sel.baseOffset
|
||||
);
|
||||
};
|
||||
|
||||
CaretBrowsing.selectLine = function() {
|
||||
const sel = window.getSelection();
|
||||
sel.modify("extend", "right", "lineboundary");
|
||||
CaretBrowsing.reverseSelection();
|
||||
sel.modify("extend", "left", "lineboundary");
|
||||
CaretBrowsing.reverseSelection();
|
||||
};
|
||||
|
||||
CaretBrowsing.updateLineSelection = function(direction, granularity) {
|
||||
if (granularity !== "character" && granularity !== "word") {
|
||||
window.
|
||||
getSelection().
|
||||
modify("extend", direction, granularity);
|
||||
CaretBrowsing.selectLine();
|
||||
}
|
||||
};
|
||||
|
||||
CaretBrowsing.move = function(direction, granularity, count = 1) {
|
||||
let action = "move";
|
||||
if (CaretBrowsing.selectionEnabled) {
|
||||
if (CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NONE) {
|
||||
action = "extend";
|
||||
}
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
window.
|
||||
getSelection().
|
||||
modify(action, direction, granularity);
|
||||
if (CaretBrowsing.selectionState === CaretBrowsing.SelectionState.LINE) {
|
||||
CaretBrowsing.updateLineSelection(direction, granularity);
|
||||
} else {
|
||||
window.
|
||||
getSelection().
|
||||
modify(action, direction, granularity);
|
||||
}
|
||||
}
|
||||
|
||||
if (CaretBrowsing.isWindows &&
|
||||
|
|
@ -1174,7 +1219,7 @@ window._qutebrowser.caret = (function() {
|
|||
|
||||
CaretBrowsing.moveToBlock = function(paragraph, boundary, count = 1) {
|
||||
let action = "move";
|
||||
if (CaretBrowsing.selectionEnabled) {
|
||||
if (CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NONE) {
|
||||
action = "extend";
|
||||
}
|
||||
for (let i = 0; i < count; i++) {
|
||||
|
|
@ -1185,6 +1230,10 @@ window._qutebrowser.caret = (function() {
|
|||
window.
|
||||
getSelection().
|
||||
modify(action, boundary, "paragraphboundary");
|
||||
|
||||
if (CaretBrowsing.selectionState === CaretBrowsing.SelectionState.LINE) {
|
||||
CaretBrowsing.selectLine();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1294,14 +1343,14 @@ window._qutebrowser.caret = (function() {
|
|||
funcs.setInitialCursor = () => {
|
||||
if (!CaretBrowsing.initiated) {
|
||||
CaretBrowsing.setInitialCursor();
|
||||
return CaretBrowsing.selectionEnabled;
|
||||
return CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NONE;
|
||||
}
|
||||
|
||||
if (window.getSelection().toString().length === 0) {
|
||||
positionCaret();
|
||||
}
|
||||
CaretBrowsing.toggle();
|
||||
return CaretBrowsing.selectionEnabled;
|
||||
return CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NONE;
|
||||
};
|
||||
|
||||
funcs.setFlags = (flags) => {
|
||||
|
|
@ -1399,17 +1448,22 @@ window._qutebrowser.caret = (function() {
|
|||
|
||||
funcs.getSelection = () => window.getSelection().toString();
|
||||
|
||||
funcs.toggleSelection = () => {
|
||||
CaretBrowsing.selectionEnabled = !CaretBrowsing.selectionEnabled;
|
||||
return CaretBrowsing.selectionEnabled;
|
||||
funcs.toggleSelection = (line) => {
|
||||
if (line) {
|
||||
CaretBrowsing.selectionState =
|
||||
CaretBrowsing.SelectionState.LINE;
|
||||
CaretBrowsing.selectLine();
|
||||
CaretBrowsing.finishMove();
|
||||
} else if (CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NORMAL) {
|
||||
CaretBrowsing.selectionState = CaretBrowsing.SelectionState.NORMAL;
|
||||
} else {
|
||||
CaretBrowsing.selectionState = CaretBrowsing.SelectionState.NONE;
|
||||
}
|
||||
return CaretBrowsing.selectionState;
|
||||
};
|
||||
|
||||
funcs.reverseSelection = () => {
|
||||
const sel = window.getSelection();
|
||||
sel.setBaseAndExtent(
|
||||
sel.extentNode, sel.extentOffset, sel.baseNode,
|
||||
sel.baseOffset
|
||||
);
|
||||
CaretBrowsing.reverseSelection();
|
||||
};
|
||||
|
||||
return funcs;
|
||||
|
|
|
|||
|
|
@ -373,13 +373,17 @@ class StatusBar(QWidget):
|
|||
self.maybe_hide()
|
||||
assert tab.is_private == self._color_flags.private
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def on_caret_selection_toggled(self, selection):
|
||||
@pyqtSlot(browsertab.SelectionState)
|
||||
def on_caret_selection_toggled(self, selection_state):
|
||||
"""Update the statusbar when entering/leaving caret selection mode."""
|
||||
log.statusbar.debug("Setting caret selection {}".format(selection))
|
||||
if selection:
|
||||
log.statusbar.debug("Setting caret selection {}"
|
||||
.format(selection_state))
|
||||
if selection_state is browsertab.SelectionState.normal:
|
||||
self._set_mode_text("caret selection")
|
||||
self._color_flags.caret = ColorFlags.CaretMode.selection
|
||||
elif selection_state is browsertab.SelectionState.line:
|
||||
self._set_mode_text("caret line selection")
|
||||
self._color_flags.caret = ColorFlags.CaretMode.selection
|
||||
else:
|
||||
self._set_mode_text("caret")
|
||||
self._color_flags.caret = ColorFlags.CaretMode.on
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ class TabbedBrowser(QWidget):
|
|||
cur_scroll_perc_changed = pyqtSignal(int, int)
|
||||
cur_load_status_changed = pyqtSignal(usertypes.LoadStatus)
|
||||
cur_fullscreen_requested = pyqtSignal(bool)
|
||||
cur_caret_selection_toggled = pyqtSignal(bool)
|
||||
cur_caret_selection_toggled = pyqtSignal(browsertab.SelectionState)
|
||||
close_window = pyqtSignal()
|
||||
resized = pyqtSignal('QRect')
|
||||
current_tab_changed = pyqtSignal(browsertab.AbstractTab)
|
||||
|
|
|
|||
|
|
@ -45,11 +45,12 @@ import helpers.stubs as stubsmod
|
|||
from qutebrowser.config import (config, configdata, configtypes, configexc,
|
||||
configfiles, configcache, stylesheet)
|
||||
from qutebrowser.api import config as configapi
|
||||
from qutebrowser.utils import objreg, standarddir, utils, usertypes
|
||||
from qutebrowser.utils import objreg, standarddir, utils, usertypes, qtutils
|
||||
from qutebrowser.browser import greasemonkey, history, qutescheme
|
||||
from qutebrowser.browser.webkit import cookies, cache
|
||||
from qutebrowser.misc import savemanager, sql, objects, sessions
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.qt import sip
|
||||
|
||||
|
||||
_qute_scheme_handler = None
|
||||
|
|
@ -64,14 +65,17 @@ class WidgetContainer(QWidget):
|
|||
self._qtbot = qtbot
|
||||
self.vbox = QVBoxLayout(self)
|
||||
qtbot.add_widget(self)
|
||||
self._widget = None
|
||||
|
||||
def set_widget(self, widget):
|
||||
self.vbox.addWidget(widget)
|
||||
widget.container = self
|
||||
self._widget = widget
|
||||
|
||||
def expose(self):
|
||||
with self._qtbot.waitExposed(self):
|
||||
self.show()
|
||||
self._widget.setFocus()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -204,19 +208,23 @@ def web_tab_setup(qtbot, tab_registry, session_manager_stub,
|
|||
|
||||
@pytest.fixture
|
||||
def webkit_tab(web_tab_setup, qtbot, cookiejar_and_cache, mode_manager,
|
||||
widget_container, webpage):
|
||||
widget_container, download_stub, webpage):
|
||||
webkittab = pytest.importorskip('qutebrowser.browser.webkit.webkittab')
|
||||
|
||||
tab = webkittab.WebKitTab(win_id=0, mode_manager=mode_manager,
|
||||
private=False)
|
||||
widget_container.set_widget(tab)
|
||||
|
||||
return tab
|
||||
yield tab
|
||||
|
||||
# Make sure the tab shuts itself down properly
|
||||
tab.private_api.shutdown()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def webengine_tab(web_tab_setup, qtbot, redirect_webengine_data,
|
||||
tabbed_browser_stubs, mode_manager, widget_container):
|
||||
tabbed_browser_stubs, mode_manager, widget_container,
|
||||
monkeypatch):
|
||||
tabwidget = tabbed_browser_stubs[0].widget
|
||||
tabwidget.current_index = 0
|
||||
tabwidget.index_of = 0
|
||||
|
|
@ -227,11 +235,25 @@ def webengine_tab(web_tab_setup, qtbot, redirect_webengine_data,
|
|||
tab = webenginetab.WebEngineTab(win_id=0, mode_manager=mode_manager,
|
||||
private=False)
|
||||
widget_container.set_widget(tab)
|
||||
|
||||
yield tab
|
||||
|
||||
# If a page is still loading here, _on_load_finished could get called
|
||||
# during teardown when session_manager_stub is already deleted.
|
||||
tab.stop()
|
||||
|
||||
# Make sure the tab shuts itself down properly
|
||||
tab.private_api.shutdown()
|
||||
|
||||
# If we wait for the GC to clean things up, there's a segfault inside
|
||||
# QtWebEngine sometimes (e.g. if we only run
|
||||
# tests/unit/browser/test_caret.py).
|
||||
# However, with Qt < 5.12, doing this here will lead to an immediate
|
||||
# segfault...
|
||||
monkeypatch.undo() # version_check could be patched
|
||||
if qtutils.version_check('5.12'):
|
||||
sip.delete(tab._widget)
|
||||
|
||||
|
||||
@pytest.fixture(params=['webkit', 'webengine'])
|
||||
def web_tab(request):
|
||||
|
|
|
|||
|
|
@ -615,6 +615,10 @@ class FakeDownloadManager:
|
|||
self.downloads.append(download_item)
|
||||
return download_item
|
||||
|
||||
def has_downloads_with_nam(self, _nam):
|
||||
"""Needed during WebView.shutdown()."""
|
||||
return False
|
||||
|
||||
|
||||
class FakeHistoryProgress:
|
||||
|
||||
|
|
|
|||
|
|
@ -24,15 +24,19 @@ import textwrap
|
|||
import pytest
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.utils import utils, qtutils, usertypes
|
||||
from qutebrowser.utils import usertypes
|
||||
from qutebrowser.browser import browsertab
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def caret(web_tab, qtbot, mode_manager):
|
||||
with qtbot.wait_signal(web_tab.load_finished):
|
||||
web_tab.container.expose()
|
||||
|
||||
with qtbot.wait_signal(web_tab.load_finished, timeout=10000):
|
||||
web_tab.load_url(QUrl('qute://testdata/data/caret.html'))
|
||||
|
||||
mode_manager.enter(usertypes.KeyMode.caret)
|
||||
with qtbot.wait_signal(web_tab.caret.selection_toggled):
|
||||
mode_manager.enter(usertypes.KeyMode.caret)
|
||||
|
||||
return web_tab.caret
|
||||
|
||||
|
|
@ -61,15 +65,21 @@ class Selection:
|
|||
selection = selection.strip()
|
||||
assert selection == expected
|
||||
return
|
||||
elif not selection and not expected:
|
||||
return
|
||||
|
||||
self._qtbot.wait(50)
|
||||
|
||||
assert False, 'Failed to get selection!'
|
||||
|
||||
def check_multiline(self, expected, *, strip=False):
|
||||
self.check(textwrap.dedent(expected).strip(), strip=strip)
|
||||
|
||||
def toggle(self):
|
||||
with self._qtbot.wait_signal(self._caret.selection_toggled):
|
||||
self._caret.toggle_selection()
|
||||
def toggle(self, *, line=False):
|
||||
"""Toggle the selection and return the new selection state."""
|
||||
with self._qtbot.wait_signal(self._caret.selection_toggled) as blocker:
|
||||
self._caret.toggle_selection(line=line)
|
||||
return blocker.args[0]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -77,6 +87,18 @@ def selection(qtbot, caret):
|
|||
return Selection(qtbot, caret)
|
||||
|
||||
|
||||
def test_toggle(caret, selection, qtbot):
|
||||
"""Make sure calling toggleSelection produces the correct callback values.
|
||||
|
||||
This also makes sure that the SelectionState enum in JS lines up with the
|
||||
Python browsertab.SelectionState enum.
|
||||
"""
|
||||
assert selection.toggle() == browsertab.SelectionState.normal
|
||||
assert selection.toggle(line=True) == browsertab.SelectionState.line
|
||||
assert selection.toggle() == browsertab.SelectionState.normal
|
||||
assert selection.toggle() == browsertab.SelectionState.none
|
||||
|
||||
|
||||
class TestDocument:
|
||||
|
||||
def test_selecting_entire_document(self, caret, selection):
|
||||
|
|
@ -287,17 +309,6 @@ def test_drop_selection(caret, selection):
|
|||
|
||||
class TestSearch:
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def expose(self, web_tab):
|
||||
"""Expose the web view if needed.
|
||||
|
||||
With QtWebEngine 5.13 on macOS/Windows, searching fails (callback
|
||||
called with False) when the view isn't exposed.
|
||||
"""
|
||||
if qtutils.version_check('5.13') and not utils.is_linux:
|
||||
web_tab.container.expose()
|
||||
web_tab.show()
|
||||
|
||||
# https://bugreports.qt.io/browse/QTBUG-60673
|
||||
|
||||
@pytest.mark.qtbug60673
|
||||
|
|
@ -340,15 +351,6 @@ class TestFollowSelected:
|
|||
def toggle_js(self, request, config_stub):
|
||||
config_stub.val.content.javascript.enabled = request.param
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def expose(self, web_tab):
|
||||
"""Expose the web view if needed.
|
||||
|
||||
On QtWebKit, or Qt < 5.11 and > 5.12 on QtWebEngine, we need to
|
||||
show the tab for selections to work properly.
|
||||
"""
|
||||
web_tab.container.expose()
|
||||
|
||||
def test_follow_selected_without_a_selection(self, qtbot, caret, selection, web_tab,
|
||||
mode_manager):
|
||||
caret.move_to_next_word() # Move cursor away from the link
|
||||
|
|
@ -405,3 +407,93 @@ class TestReverse:
|
|||
caret.reverse_selection()
|
||||
caret.move_to_start_of_line()
|
||||
selection.check("one two three")
|
||||
|
||||
|
||||
class TestLineSelection:
|
||||
|
||||
def test_toggle(self, caret, selection):
|
||||
selection.toggle(line=True)
|
||||
selection.check("one two three")
|
||||
|
||||
def test_toggle_untoggle(self, caret, selection):
|
||||
selection.toggle()
|
||||
selection.check("")
|
||||
selection.toggle(line=True)
|
||||
selection.check("one two three")
|
||||
selection.toggle()
|
||||
selection.check("one two three")
|
||||
|
||||
def test_from_center(self, caret, selection):
|
||||
caret.move_to_next_char(4)
|
||||
selection.toggle(line=True)
|
||||
selection.check("one two three")
|
||||
|
||||
def test_more_lines(self, caret, selection):
|
||||
selection.toggle(line=True)
|
||||
caret.move_to_next_line(2)
|
||||
selection.check_multiline("""
|
||||
one two three
|
||||
eins zwei drei
|
||||
|
||||
four five six
|
||||
""", strip=True)
|
||||
|
||||
def test_not_selecting_char(self, caret, selection):
|
||||
selection.toggle(line=True)
|
||||
caret.move_to_next_char()
|
||||
selection.check("one two three")
|
||||
caret.move_to_prev_char()
|
||||
selection.check("one two three")
|
||||
|
||||
def test_selecting_prev_next_word(self, caret, selection):
|
||||
selection.toggle(line=True)
|
||||
caret.move_to_next_word()
|
||||
selection.check("one two three")
|
||||
caret.move_to_prev_word()
|
||||
selection.check("one two three")
|
||||
|
||||
def test_selecting_end_word(self, caret, selection):
|
||||
selection.toggle(line=True)
|
||||
caret.move_to_end_of_word()
|
||||
selection.check("one two three")
|
||||
|
||||
def test_selecting_prev_next_line(self, caret, selection):
|
||||
selection.toggle(line=True)
|
||||
caret.move_to_next_line()
|
||||
selection.check_multiline("""
|
||||
one two three
|
||||
eins zwei drei
|
||||
""", strip=True)
|
||||
caret.move_to_prev_line()
|
||||
selection.check("one two three")
|
||||
|
||||
def test_not_selecting_start_end_line(self, caret, selection):
|
||||
selection.toggle(line=True)
|
||||
caret.move_to_end_of_line()
|
||||
selection.check("one two three")
|
||||
caret.move_to_start_of_line()
|
||||
selection.check("one two three")
|
||||
|
||||
def test_selecting_block(self, caret, selection):
|
||||
selection.toggle(line=True)
|
||||
caret.move_to_end_of_next_block()
|
||||
selection.check_multiline("""
|
||||
one two three
|
||||
eins zwei drei
|
||||
""", strip=True)
|
||||
|
||||
@pytest.mark.not_mac(
|
||||
reason='https://github.com/qutebrowser/qutebrowser/issues/5459')
|
||||
def test_selecting_start_end_document(self, caret, selection):
|
||||
selection.toggle(line=True)
|
||||
caret.move_to_end_of_document()
|
||||
selection.check_multiline("""
|
||||
one two three
|
||||
eins zwei drei
|
||||
|
||||
four five six
|
||||
vier fünf sechs
|
||||
""", strip=True)
|
||||
|
||||
caret.move_to_start_of_document()
|
||||
selection.check("one two three")
|
||||
|
|
|
|||
Loading…
Reference in New Issue