1# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: 2 3# Copyright 2016-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org> 4# 5# This file is part of qutebrowser. 6# 7# qutebrowser is free software: you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation, either version 3 of the License, or 10# (at your option) any later version. 11# 12# qutebrowser is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. 19 20"""Base class for a wrapper over QWebView/QWebEngineView.""" 21 22import enum 23import itertools 24import functools 25import dataclasses 26from typing import (cast, TYPE_CHECKING, Any, Callable, Iterable, List, Optional, 27 Sequence, Set, Type, Union) 28 29from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt, 30 QEvent, QPoint, QRect) 31from PyQt5.QtGui import QKeyEvent, QIcon, QPixmap 32from PyQt5.QtWidgets import QWidget, QApplication, QDialog 33from PyQt5.QtPrintSupport import QPrintDialog, QPrinter 34from PyQt5.QtNetwork import QNetworkAccessManager 35 36if TYPE_CHECKING: 37 from PyQt5.QtWebKit import QWebHistory, QWebHistoryItem 38 from PyQt5.QtWebKitWidgets import QWebPage 39 from PyQt5.QtWebEngineWidgets import ( 40 QWebEngineHistory, QWebEngineHistoryItem, QWebEnginePage) 41 42from qutebrowser.keyinput import modeman 43from qutebrowser.config import config 44from qutebrowser.utils import (utils, objreg, usertypes, log, qtutils, 45 urlutils, message, jinja) 46from qutebrowser.misc import miscwidgets, objects, sessions 47from qutebrowser.browser import eventfilter, inspector 48from qutebrowser.qt import sip 49 50if TYPE_CHECKING: 51 from qutebrowser.browser import webelem 52 from qutebrowser.browser.inspector import AbstractWebInspector 53 54 55tab_id_gen = itertools.count(0) 56 57 58def create(win_id: int, 59 private: bool, 60 parent: QWidget = None) -> 'AbstractTab': 61 """Get a QtWebKit/QtWebEngine tab object. 62 63 Args: 64 win_id: The window ID where the tab will be shown. 65 private: Whether the tab is a private/off the record tab. 66 parent: The Qt parent to set. 67 """ 68 # Importing modules here so we don't depend on QtWebEngine without the 69 # argument and to avoid circular imports. 70 mode_manager = modeman.instance(win_id) 71 if objects.backend == usertypes.Backend.QtWebEngine: 72 from qutebrowser.browser.webengine import webenginetab 73 tab_class: Type[AbstractTab] = webenginetab.WebEngineTab 74 elif objects.backend == usertypes.Backend.QtWebKit: 75 from qutebrowser.browser.webkit import webkittab 76 tab_class = webkittab.WebKitTab 77 else: 78 raise utils.Unreachable(objects.backend) 79 return tab_class(win_id=win_id, mode_manager=mode_manager, private=private, 80 parent=parent) 81 82 83class WebTabError(Exception): 84 85 """Base class for various errors.""" 86 87 88class UnsupportedOperationError(WebTabError): 89 90 """Raised when an operation is not supported with the given backend.""" 91 92 93class TerminationStatus(enum.Enum): 94 95 """How a QtWebEngine renderer process terminated. 96 97 Also see QWebEnginePage::RenderProcessTerminationStatus 98 """ 99 100 #: Unknown render process status value gotten from Qt. 101 unknown = -1 102 #: The render process terminated normally. 103 normal = 0 104 #: The render process terminated with with a non-zero exit status. 105 abnormal = 1 106 #: The render process crashed, for example because of a segmentation fault. 107 crashed = 2 108 #: The render process was killed, for example by SIGKILL or task manager kill. 109 killed = 3 110 111 112@dataclasses.dataclass 113class TabData: 114 115 """A simple namespace with a fixed set of attributes. 116 117 Attributes: 118 keep_icon: Whether the (e.g. cloned) icon should not be cleared on page 119 load. 120 inspector: The QWebInspector used for this webview. 121 viewing_source: Set if we're currently showing a source view. 122 Only used when sources are shown via pygments. 123 open_target: Where to open the next link. 124 Only used for QtWebKit. 125 override_target: Override for open_target for fake clicks (like hints). 126 Only used for QtWebKit. 127 pinned: Flag to pin the tab. 128 fullscreen: Whether the tab has a video shown fullscreen currently. 129 netrc_used: Whether netrc authentication was performed. 130 input_mode: current input mode for the tab. 131 splitter: InspectorSplitter used to show inspector inside the tab. 132 """ 133 134 keep_icon: bool = False 135 viewing_source: bool = False 136 inspector: Optional['AbstractWebInspector'] = None 137 open_target: usertypes.ClickTarget = usertypes.ClickTarget.normal 138 override_target: Optional[usertypes.ClickTarget] = None 139 pinned: bool = False 140 fullscreen: bool = False 141 netrc_used: bool = False 142 input_mode: usertypes.KeyMode = usertypes.KeyMode.normal 143 last_navigation: Optional[usertypes.NavigationRequest] = None 144 splitter: Optional[miscwidgets.InspectorSplitter] = None 145 146 def should_show_icon(self) -> bool: 147 return (config.val.tabs.favicons.show == 'always' or 148 config.val.tabs.favicons.show == 'pinned' and self.pinned) 149 150 151class AbstractAction: 152 153 """Attribute ``action`` of AbstractTab for Qt WebActions.""" 154 155 action_class: Type[Union['QWebPage', 'QWebEnginePage']] 156 action_base: Type[Union['QWebPage.WebAction', 'QWebEnginePage.WebAction']] 157 158 def __init__(self, tab: 'AbstractTab') -> None: 159 self._widget = cast(QWidget, None) 160 self._tab = tab 161 162 def exit_fullscreen(self) -> None: 163 """Exit the fullscreen mode.""" 164 raise NotImplementedError 165 166 def save_page(self) -> None: 167 """Save the current page.""" 168 raise NotImplementedError 169 170 def run_string(self, name: str) -> None: 171 """Run a webaction based on its name.""" 172 member = getattr(self.action_class, name, None) 173 if not isinstance(member, self.action_base): 174 raise WebTabError("{} is not a valid web action!".format(name)) 175 self._widget.triggerPageAction(member) 176 177 def show_source(self, pygments: bool = False) -> None: 178 """Show the source of the current page in a new tab.""" 179 raise NotImplementedError 180 181 def _show_html_source(self, html: str) -> None: 182 """Show the given HTML as source page.""" 183 tb = objreg.get('tabbed-browser', scope='window', window=self._tab.win_id) 184 new_tab = tb.tabopen(background=False, related=True) 185 new_tab.set_html(html, self._tab.url()) 186 new_tab.data.viewing_source = True 187 188 def _show_source_fallback(self, source: str) -> None: 189 """Show source with pygments unavailable.""" 190 html = jinja.render( 191 'pre.html', 192 title='Source', 193 content=source, 194 preamble="Note: The optional Pygments dependency wasn't found - " 195 "showing unhighlighted source.", 196 ) 197 self._show_html_source(html) 198 199 def _show_source_pygments(self) -> None: 200 201 def show_source_cb(source: str) -> None: 202 """Show source as soon as it's ready.""" 203 try: 204 import pygments 205 import pygments.lexers 206 import pygments.formatters 207 except ImportError: 208 # Pygments is an optional dependency 209 self._show_source_fallback(source) 210 return 211 212 try: 213 lexer = pygments.lexers.HtmlLexer() 214 formatter = pygments.formatters.HtmlFormatter( 215 full=True, linenos='table') 216 except AttributeError: 217 # Remaining namespace package from Pygments 218 self._show_source_fallback(source) 219 return 220 221 html = pygments.highlight(source, lexer, formatter) 222 self._show_html_source(html) 223 224 self._tab.dump_async(show_source_cb) 225 226 227class AbstractPrinting: 228 229 """Attribute ``printing`` of AbstractTab for printing the page.""" 230 231 def __init__(self, tab: 'AbstractTab') -> None: 232 self._widget = cast(QWidget, None) 233 self._tab = tab 234 235 def check_pdf_support(self) -> None: 236 """Check whether writing to PDFs is supported. 237 238 If it's not supported (by the current Qt version), a WebTabError is 239 raised. 240 """ 241 raise NotImplementedError 242 243 def check_preview_support(self) -> None: 244 """Check whether showing a print preview is supported. 245 246 If it's not supported (by the current Qt version), a WebTabError is 247 raised. 248 """ 249 raise NotImplementedError 250 251 def to_pdf(self, filename: str) -> bool: 252 """Print the tab to a PDF with the given filename.""" 253 raise NotImplementedError 254 255 def to_printer(self, printer: QPrinter, 256 callback: Callable[[bool], None] = None) -> None: 257 """Print the tab. 258 259 Args: 260 printer: The QPrinter to print to. 261 callback: Called with a boolean 262 (True if printing succeeded, False otherwise) 263 """ 264 raise NotImplementedError 265 266 def show_dialog(self) -> None: 267 """Print with a QPrintDialog.""" 268 def print_callback(ok: bool) -> None: 269 """Called when printing finished.""" 270 if not ok: 271 message.error("Printing failed!") 272 diag.deleteLater() 273 274 def do_print() -> None: 275 """Called when the dialog was closed.""" 276 self.to_printer(diag.printer(), print_callback) 277 278 diag = QPrintDialog(self._tab) 279 if utils.is_mac: 280 # For some reason we get a segfault when using open() on macOS 281 ret = diag.exec() 282 if ret == QDialog.Accepted: 283 do_print() 284 else: 285 diag.open(do_print) 286 287 288class AbstractSearch(QObject): 289 290 """Attribute ``search`` of AbstractTab for doing searches. 291 292 Attributes: 293 text: The last thing this view was searched for. 294 search_displayed: Whether we're currently displaying search results in 295 this view. 296 _flags: The flags of the last search (needs to be set by subclasses). 297 _widget: The underlying WebView widget. 298 """ 299 300 #: Signal emitted when a search was finished 301 #: (True if the text was found, False otherwise) 302 finished = pyqtSignal(bool) 303 #: Signal emitted when an existing search was cleared. 304 cleared = pyqtSignal() 305 306 _Callback = Callable[[bool], None] 307 308 def __init__(self, tab: 'AbstractTab', parent: QWidget = None): 309 super().__init__(parent) 310 self._tab = tab 311 self._widget = cast(QWidget, None) 312 self.text: Optional[str] = None 313 self.search_displayed = False 314 315 def _is_case_sensitive(self, ignore_case: usertypes.IgnoreCase) -> bool: 316 """Check if case-sensitivity should be used. 317 318 This assumes self.text is already set properly. 319 320 Arguments: 321 ignore_case: The ignore_case value from the config. 322 """ 323 assert self.text is not None 324 mapping = { 325 usertypes.IgnoreCase.smart: not self.text.islower(), 326 usertypes.IgnoreCase.never: True, 327 usertypes.IgnoreCase.always: False, 328 } 329 return mapping[ignore_case] 330 331 def search(self, text: str, *, 332 ignore_case: usertypes.IgnoreCase = usertypes.IgnoreCase.never, 333 reverse: bool = False, 334 wrap: bool = True, 335 result_cb: _Callback = None) -> None: 336 """Find the given text on the page. 337 338 Args: 339 text: The text to search for. 340 ignore_case: Search case-insensitively. 341 reverse: Reverse search direction. 342 wrap: Allow wrapping at the top or bottom of the page. 343 result_cb: Called with a bool indicating whether a match was found. 344 """ 345 raise NotImplementedError 346 347 def clear(self) -> None: 348 """Clear the current search.""" 349 raise NotImplementedError 350 351 def prev_result(self, *, result_cb: _Callback = None) -> None: 352 """Go to the previous result of the current search. 353 354 Args: 355 result_cb: Called with a bool indicating whether a match was found. 356 """ 357 raise NotImplementedError 358 359 def next_result(self, *, result_cb: _Callback = None) -> None: 360 """Go to the next result of the current search. 361 362 Args: 363 result_cb: Called with a bool indicating whether a match was found. 364 """ 365 raise NotImplementedError 366 367 368class AbstractZoom(QObject): 369 370 """Attribute ``zoom`` of AbstractTab for controlling zoom.""" 371 372 def __init__(self, tab: 'AbstractTab', parent: QWidget = None) -> None: 373 super().__init__(parent) 374 self._tab = tab 375 self._widget = cast(QWidget, None) 376 # Whether zoom was changed from the default. 377 self._default_zoom_changed = False 378 self._init_neighborlist() 379 config.instance.changed.connect(self._on_config_changed) 380 self._zoom_factor = float(config.val.zoom.default) / 100 381 382 @pyqtSlot(str) 383 def _on_config_changed(self, option: str) -> None: 384 if option in ['zoom.levels', 'zoom.default']: 385 if not self._default_zoom_changed: 386 factor = float(config.val.zoom.default) / 100 387 self.set_factor(factor) 388 self._init_neighborlist() 389 390 def _init_neighborlist(self) -> None: 391 """Initialize self._neighborlist. 392 393 It is a NeighborList with the zoom levels.""" 394 levels = config.val.zoom.levels 395 self._neighborlist: usertypes.NeighborList[float] = usertypes.NeighborList( 396 levels, mode=usertypes.NeighborList.Modes.edge) 397 self._neighborlist.fuzzyval = config.val.zoom.default 398 399 def apply_offset(self, offset: int) -> float: 400 """Increase/Decrease the zoom level by the given offset. 401 402 Args: 403 offset: The offset in the zoom level list. 404 405 Return: 406 The new zoom level. 407 """ 408 level = self._neighborlist.getitem(offset) 409 self.set_factor(float(level) / 100, fuzzyval=False) 410 return level 411 412 def _set_factor_internal(self, factor: float) -> None: 413 raise NotImplementedError 414 415 def set_factor(self, factor: float, *, fuzzyval: bool = True) -> None: 416 """Zoom to a given zoom factor. 417 418 Args: 419 factor: The zoom factor as float. 420 fuzzyval: Whether to set the NeighborLists fuzzyval. 421 """ 422 if fuzzyval: 423 self._neighborlist.fuzzyval = int(factor * 100) 424 if factor < 0: 425 raise ValueError("Can't zoom to factor {}!".format(factor)) 426 427 default_zoom_factor = float(config.val.zoom.default) / 100 428 self._default_zoom_changed = (factor != default_zoom_factor) 429 430 self._zoom_factor = factor 431 self._set_factor_internal(factor) 432 433 def factor(self) -> float: 434 return self._zoom_factor 435 436 def apply_default(self) -> None: 437 self._set_factor_internal(float(config.val.zoom.default) / 100) 438 439 def reapply(self) -> None: 440 self._set_factor_internal(self._zoom_factor) 441 442 443class SelectionState(enum.Enum): 444 445 """Possible states of selection in caret mode. 446 447 NOTE: Names need to line up with SelectionState in caret.js! 448 """ 449 450 none = enum.auto() 451 normal = enum.auto() 452 line = enum.auto() 453 454 455class AbstractCaret(QObject): 456 457 """Attribute ``caret`` of AbstractTab for caret browsing.""" 458 459 #: Signal emitted when the selection was toggled. 460 selection_toggled = pyqtSignal(SelectionState) 461 #: Emitted when a ``follow_selection`` action is done. 462 follow_selected_done = pyqtSignal() 463 464 def __init__(self, 465 tab: 'AbstractTab', 466 mode_manager: modeman.ModeManager, 467 parent: QWidget = None) -> None: 468 super().__init__(parent) 469 self._widget = cast(QWidget, None) 470 self._mode_manager = mode_manager 471 mode_manager.entered.connect(self._on_mode_entered) 472 mode_manager.left.connect(self._on_mode_left) 473 self._tab = tab 474 475 def _on_mode_entered(self, mode: usertypes.KeyMode) -> None: 476 raise NotImplementedError 477 478 def _on_mode_left(self, mode: usertypes.KeyMode) -> None: 479 raise NotImplementedError 480 481 def move_to_next_line(self, count: int = 1) -> None: 482 raise NotImplementedError 483 484 def move_to_prev_line(self, count: int = 1) -> None: 485 raise NotImplementedError 486 487 def move_to_next_char(self, count: int = 1) -> None: 488 raise NotImplementedError 489 490 def move_to_prev_char(self, count: int = 1) -> None: 491 raise NotImplementedError 492 493 def move_to_end_of_word(self, count: int = 1) -> None: 494 raise NotImplementedError 495 496 def move_to_next_word(self, count: int = 1) -> None: 497 raise NotImplementedError 498 499 def move_to_prev_word(self, count: int = 1) -> None: 500 raise NotImplementedError 501 502 def move_to_start_of_line(self) -> None: 503 raise NotImplementedError 504 505 def move_to_end_of_line(self) -> None: 506 raise NotImplementedError 507 508 def move_to_start_of_next_block(self, count: int = 1) -> None: 509 raise NotImplementedError 510 511 def move_to_start_of_prev_block(self, count: int = 1) -> None: 512 raise NotImplementedError 513 514 def move_to_end_of_next_block(self, count: int = 1) -> None: 515 raise NotImplementedError 516 517 def move_to_end_of_prev_block(self, count: int = 1) -> None: 518 raise NotImplementedError 519 520 def move_to_start_of_document(self) -> None: 521 raise NotImplementedError 522 523 def move_to_end_of_document(self) -> None: 524 raise NotImplementedError 525 526 def toggle_selection(self, line: bool = False) -> None: 527 raise NotImplementedError 528 529 def drop_selection(self) -> None: 530 raise NotImplementedError 531 532 def selection(self, callback: Callable[[str], None]) -> None: 533 raise NotImplementedError 534 535 def reverse_selection(self) -> None: 536 raise NotImplementedError 537 538 def _follow_enter(self, tab: bool) -> None: 539 """Follow a link by faking an enter press.""" 540 if tab: 541 self._tab.fake_key_press(Qt.Key_Enter, modifier=Qt.ControlModifier) 542 else: 543 self._tab.fake_key_press(Qt.Key_Enter) 544 545 def follow_selected(self, *, tab: bool = False) -> None: 546 raise NotImplementedError 547 548 549class AbstractScroller(QObject): 550 551 """Attribute ``scroller`` of AbstractTab to manage scroll position.""" 552 553 #: Signal emitted when the scroll position changed (int, int) 554 perc_changed = pyqtSignal(int, int) 555 #: Signal emitted before the user requested a jump. 556 #: Used to set the special ' mark so the user can return. 557 before_jump_requested = pyqtSignal() 558 559 def __init__(self, tab: 'AbstractTab', parent: QWidget = None): 560 super().__init__(parent) 561 self._tab = tab 562 self._widget = cast(QWidget, None) 563 if 'log-scroll-pos' in objects.debug_flags: 564 self.perc_changed.connect(self._log_scroll_pos_change) 565 566 @pyqtSlot() 567 def _log_scroll_pos_change(self) -> None: 568 log.webview.vdebug( # type: ignore[attr-defined] 569 "Scroll position changed to {}".format(self.pos_px())) 570 571 def _init_widget(self, widget: QWidget) -> None: 572 self._widget = widget 573 574 def pos_px(self) -> int: 575 raise NotImplementedError 576 577 def pos_perc(self) -> int: 578 raise NotImplementedError 579 580 def to_perc(self, x: int = None, y: int = None) -> None: 581 raise NotImplementedError 582 583 def to_point(self, point: QPoint) -> None: 584 raise NotImplementedError 585 586 def to_anchor(self, name: str) -> None: 587 raise NotImplementedError 588 589 def delta(self, x: int = 0, y: int = 0) -> None: 590 raise NotImplementedError 591 592 def delta_page(self, x: float = 0, y: float = 0) -> None: 593 raise NotImplementedError 594 595 def up(self, count: int = 1) -> None: 596 raise NotImplementedError 597 598 def down(self, count: int = 1) -> None: 599 raise NotImplementedError 600 601 def left(self, count: int = 1) -> None: 602 raise NotImplementedError 603 604 def right(self, count: int = 1) -> None: 605 raise NotImplementedError 606 607 def top(self) -> None: 608 raise NotImplementedError 609 610 def bottom(self) -> None: 611 raise NotImplementedError 612 613 def page_up(self, count: int = 1) -> None: 614 raise NotImplementedError 615 616 def page_down(self, count: int = 1) -> None: 617 raise NotImplementedError 618 619 def at_top(self) -> bool: 620 raise NotImplementedError 621 622 def at_bottom(self) -> bool: 623 raise NotImplementedError 624 625 626class AbstractHistoryPrivate: 627 628 """Private API related to the history.""" 629 630 def serialize(self) -> bytes: 631 """Serialize into an opaque format understood by self.deserialize.""" 632 raise NotImplementedError 633 634 def deserialize(self, data: bytes) -> None: 635 """Deserialize from a format produced by self.serialize.""" 636 raise NotImplementedError 637 638 def load_items(self, items: Sequence[sessions.TabHistoryItem]) -> None: 639 """Deserialize from a list of TabHistoryItems.""" 640 raise NotImplementedError 641 642 643class AbstractHistory: 644 645 """The history attribute of a AbstractTab.""" 646 647 def __init__(self, tab: 'AbstractTab') -> None: 648 self._tab = tab 649 self._history = cast(Union['QWebHistory', 'QWebEngineHistory'], None) 650 self.private_api = AbstractHistoryPrivate() 651 652 def __len__(self) -> int: 653 raise NotImplementedError 654 655 def __iter__(self) -> Iterable[Union['QWebHistoryItem', 'QWebEngineHistoryItem']]: 656 raise NotImplementedError 657 658 def _check_count(self, count: int) -> None: 659 """Check whether the count is positive.""" 660 if count < 0: 661 raise WebTabError("count needs to be positive!") 662 663 def current_idx(self) -> int: 664 raise NotImplementedError 665 666 def back(self, count: int = 1) -> None: 667 """Go back in the tab's history.""" 668 self._check_count(count) 669 idx = self.current_idx() - count 670 if idx >= 0: 671 self._go_to_item(self._item_at(idx)) 672 else: 673 self._go_to_item(self._item_at(0)) 674 raise WebTabError("At beginning of history.") 675 676 def forward(self, count: int = 1) -> None: 677 """Go forward in the tab's history.""" 678 self._check_count(count) 679 idx = self.current_idx() + count 680 if idx < len(self): 681 self._go_to_item(self._item_at(idx)) 682 else: 683 self._go_to_item(self._item_at(len(self) - 1)) 684 raise WebTabError("At end of history.") 685 686 def can_go_back(self) -> bool: 687 raise NotImplementedError 688 689 def can_go_forward(self) -> bool: 690 raise NotImplementedError 691 692 def _item_at(self, i: int) -> Any: 693 raise NotImplementedError 694 695 def _go_to_item(self, item: Any) -> None: 696 raise NotImplementedError 697 698 def back_items(self) -> List[Any]: 699 raise NotImplementedError 700 701 def forward_items(self) -> List[Any]: 702 raise NotImplementedError 703 704 705class AbstractElements: 706 707 """Finding and handling of elements on the page.""" 708 709 _MultiCallback = Callable[[Sequence['webelem.AbstractWebElement']], None] 710 _SingleCallback = Callable[[Optional['webelem.AbstractWebElement']], None] 711 _ErrorCallback = Callable[[Exception], None] 712 713 def __init__(self, tab: 'AbstractTab') -> None: 714 self._widget = cast(QWidget, None) 715 self._tab = tab 716 717 def find_css(self, selector: str, 718 callback: _MultiCallback, 719 error_cb: _ErrorCallback, *, 720 only_visible: bool = False) -> None: 721 """Find all HTML elements matching a given selector async. 722 723 If there's an error, the callback is called with a webelem.Error 724 instance. 725 726 Args: 727 callback: The callback to be called when the search finished. 728 error_cb: The callback to be called when an error occurred. 729 selector: The CSS selector to search for. 730 only_visible: Only show elements which are visible on screen. 731 """ 732 raise NotImplementedError 733 734 def find_id(self, elem_id: str, callback: _SingleCallback) -> None: 735 """Find the HTML element with the given ID async. 736 737 Args: 738 callback: The callback to be called when the search finished. 739 Called with a WebEngineElement or None. 740 elem_id: The ID to search for. 741 """ 742 raise NotImplementedError 743 744 def find_focused(self, callback: _SingleCallback) -> None: 745 """Find the focused element on the page async. 746 747 Args: 748 callback: The callback to be called when the search finished. 749 Called with a WebEngineElement or None. 750 """ 751 raise NotImplementedError 752 753 def find_at_pos(self, pos: QPoint, callback: _SingleCallback) -> None: 754 """Find the element at the given position async. 755 756 This is also called "hit test" elsewhere. 757 758 Args: 759 pos: The QPoint to get the element for. 760 callback: The callback to be called when the search finished. 761 Called with a WebEngineElement or None. 762 """ 763 raise NotImplementedError 764 765 766class AbstractAudio(QObject): 767 768 """Handling of audio/muting for this tab.""" 769 770 muted_changed = pyqtSignal(bool) 771 recently_audible_changed = pyqtSignal(bool) 772 773 def __init__(self, tab: 'AbstractTab', parent: QWidget = None) -> None: 774 super().__init__(parent) 775 self._widget = cast(QWidget, None) 776 self._tab = tab 777 778 def set_muted(self, muted: bool, override: bool = False) -> None: 779 """Set this tab as muted or not. 780 781 Arguments: 782 override: If set to True, muting/unmuting was done manually and 783 overrides future automatic mute/unmute changes based on 784 the URL. 785 """ 786 raise NotImplementedError 787 788 def is_muted(self) -> bool: 789 raise NotImplementedError 790 791 def is_recently_audible(self) -> bool: 792 """Whether this tab has had audio playing recently.""" 793 raise NotImplementedError 794 795 796class AbstractTabPrivate: 797 798 """Tab-related methods which are only needed in the core. 799 800 Those methods are not part of the API which is exposed to extensions, and 801 should ideally be removed at some point in the future. 802 """ 803 804 def __init__(self, mode_manager: modeman.ModeManager, 805 tab: 'AbstractTab') -> None: 806 self._widget = cast(QWidget, None) 807 self._tab = tab 808 self._mode_manager = mode_manager 809 810 def event_target(self) -> QWidget: 811 """Return the widget events should be sent to.""" 812 raise NotImplementedError 813 814 def handle_auto_insert_mode(self, ok: bool) -> None: 815 """Handle `input.insert_mode.auto_load` after loading finished.""" 816 if not ok or not config.cache['input.insert_mode.auto_load']: 817 return 818 819 cur_mode = self._mode_manager.mode 820 if cur_mode == usertypes.KeyMode.insert: 821 return 822 823 def _auto_insert_mode_cb( 824 elem: Optional['webelem.AbstractWebElement'] 825 ) -> None: 826 """Called from JS after finding the focused element.""" 827 if elem is None: 828 log.webview.debug("No focused element!") 829 return 830 if elem.is_editable(): 831 modeman.enter(self._tab.win_id, usertypes.KeyMode.insert, 832 'load finished', only_if_normal=True) 833 834 self._tab.elements.find_focused(_auto_insert_mode_cb) 835 836 def clear_ssl_errors(self) -> None: 837 raise NotImplementedError 838 839 def networkaccessmanager(self) -> Optional[QNetworkAccessManager]: 840 """Get the QNetworkAccessManager for this tab. 841 842 This is only implemented for QtWebKit. 843 For QtWebEngine, always returns None. 844 """ 845 raise NotImplementedError 846 847 def shutdown(self) -> None: 848 raise NotImplementedError 849 850 def run_js_sync(self, code: str) -> None: 851 """Run javascript sync. 852 853 Result will be returned when running JS is complete. 854 This is only implemented for QtWebKit. 855 For QtWebEngine, always raises UnsupportedOperationError. 856 """ 857 raise NotImplementedError 858 859 def _recreate_inspector(self) -> None: 860 """Recreate the inspector when detached to a window. 861 862 This is needed to circumvent a QtWebEngine bug (which wasn't 863 investigated further) which sometimes results in the window not 864 appearing anymore. 865 """ 866 self._tab.data.inspector = None 867 self.toggle_inspector(inspector.Position.window) 868 869 def toggle_inspector(self, position: inspector.Position) -> None: 870 """Show/hide (and if needed, create) the web inspector for this tab.""" 871 tabdata = self._tab.data 872 if tabdata.inspector is None: 873 assert tabdata.splitter is not None 874 tabdata.inspector = self._init_inspector( 875 splitter=tabdata.splitter, 876 win_id=self._tab.win_id) 877 self._tab.shutting_down.connect(tabdata.inspector.shutdown) 878 tabdata.inspector.recreate.connect(self._recreate_inspector) 879 tabdata.inspector.inspect(self._widget.page()) 880 tabdata.inspector.set_position(position) 881 882 def _init_inspector(self, splitter: 'miscwidgets.InspectorSplitter', 883 win_id: int, 884 parent: QWidget = None) -> 'AbstractWebInspector': 885 """Get a WebKitInspector/WebEngineInspector. 886 887 Args: 888 splitter: InspectorSplitter where the inspector can be placed. 889 win_id: The window ID this inspector is associated with. 890 parent: The Qt parent to set. 891 """ 892 raise NotImplementedError 893 894 895class AbstractTab(QWidget): 896 897 """An adapter for QWebView/QWebEngineView representing a single tab.""" 898 899 #: Signal emitted when a website requests to close this tab. 900 window_close_requested = pyqtSignal() 901 #: Signal emitted when a link is hovered (the hover text) 902 link_hovered = pyqtSignal(str) 903 #: Signal emitted when a page started loading 904 load_started = pyqtSignal() 905 #: Signal emitted when a page is loading (progress percentage) 906 load_progress = pyqtSignal(int) 907 #: Signal emitted when a page finished loading (success as bool) 908 load_finished = pyqtSignal(bool) 909 #: Signal emitted when a page's favicon changed (icon as QIcon) 910 icon_changed = pyqtSignal(QIcon) 911 #: Signal emitted when a page's title changed (new title as str) 912 title_changed = pyqtSignal(str) 913 #: Signal emitted when this tab was pinned/unpinned (new pinned state as bool) 914 pinned_changed = pyqtSignal(bool) 915 #: Signal emitted when a new tab should be opened (url as QUrl) 916 new_tab_requested = pyqtSignal(QUrl) 917 #: Signal emitted when a page's URL changed (url as QUrl) 918 url_changed = pyqtSignal(QUrl) 919 #: Signal emitted when a tab's content size changed 920 #: (new size as QSizeF) 921 contents_size_changed = pyqtSignal(QSizeF) 922 #: Signal emitted when a page requested full-screen (bool) 923 fullscreen_requested = pyqtSignal(bool) 924 #: Signal emitted before load starts (URL as QUrl) 925 before_load_started = pyqtSignal(QUrl) 926 927 # Signal emitted when a page's load status changed 928 # (argument: usertypes.LoadStatus) 929 load_status_changed = pyqtSignal(usertypes.LoadStatus) 930 # Signal emitted before shutting down 931 shutting_down = pyqtSignal() 932 # Signal emitted when a history item should be added 933 history_item_triggered = pyqtSignal(QUrl, QUrl, str) 934 # Signal emitted when the underlying renderer process terminated. 935 # arg 0: A TerminationStatus member. 936 # arg 1: The exit code. 937 renderer_process_terminated = pyqtSignal(TerminationStatus, int) 938 939 # Hosts for which a certificate error happened. Shared between all tabs. 940 # 941 # Note that we remember hosts here, without scheme/port: 942 # QtWebEngine/Chromium also only remembers hostnames, and certificates are 943 # for a given hostname anyways. 944 _insecure_hosts: Set[str] = set() 945 946 def __init__(self, *, win_id: int, 947 mode_manager: 'modeman.ModeManager', 948 private: bool, 949 parent: QWidget = None) -> None: 950 utils.unused(mode_manager) # needed for mypy 951 self.is_private = private 952 self.win_id = win_id 953 self.tab_id = next(tab_id_gen) 954 super().__init__(parent) 955 956 self.registry = objreg.ObjectRegistry() 957 tab_registry = objreg.get('tab-registry', scope='window', 958 window=win_id) 959 tab_registry[self.tab_id] = self 960 objreg.register('tab', self, registry=self.registry) 961 962 self.data = TabData() 963 self._layout = miscwidgets.WrapperLayout(self) 964 self._widget = cast(QWidget, None) 965 self._progress = 0 966 self._load_status = usertypes.LoadStatus.none 967 self._tab_event_filter = eventfilter.TabEventFilter( 968 self, parent=self) 969 self.backend: Optional[usertypes.Backend] = None 970 971 # If true, this tab has been requested to be removed (or is removed). 972 self.pending_removal = False 973 self.shutting_down.connect(functools.partial( 974 setattr, self, 'pending_removal', True)) 975 976 self.before_load_started.connect(self._on_before_load_started) 977 978 def _set_widget(self, widget: QWidget) -> None: 979 # pylint: disable=protected-access 980 self._widget = widget 981 self.data.splitter = miscwidgets.InspectorSplitter( 982 win_id=self.win_id, main_webview=widget) 983 self._layout.wrap(self, self.data.splitter) 984 self.history._history = widget.history() 985 self.history.private_api._history = widget.history() 986 self.scroller._init_widget(widget) 987 self.caret._widget = widget 988 self.zoom._widget = widget 989 self.search._widget = widget 990 self.printing._widget = widget 991 self.action._widget = widget 992 self.elements._widget = widget 993 self.audio._widget = widget 994 self.private_api._widget = widget 995 self.settings._settings = widget.settings() 996 997 self._install_event_filter() 998 self.zoom.apply_default() 999 1000 def _install_event_filter(self) -> None: 1001 raise NotImplementedError 1002 1003 def _set_load_status(self, val: usertypes.LoadStatus) -> None: 1004 """Setter for load_status.""" 1005 if not isinstance(val, usertypes.LoadStatus): 1006 raise TypeError("Type {} is no LoadStatus member!".format(val)) 1007 log.webview.debug("load status for {}: {}".format(repr(self), val)) 1008 self._load_status = val 1009 self.load_status_changed.emit(val) 1010 1011 def send_event(self, evt: QEvent) -> None: 1012 """Send the given event to the underlying widget. 1013 1014 The event will be sent via QApplication.postEvent. 1015 Note that a posted event must not be re-used in any way! 1016 """ 1017 # This only gives us some mild protection against re-using events, but 1018 # it's certainly better than a segfault. 1019 if getattr(evt, 'posted', False): 1020 raise utils.Unreachable("Can't re-use an event which was already " 1021 "posted!") 1022 1023 recipient = self.private_api.event_target() 1024 if recipient is None: 1025 # https://github.com/qutebrowser/qutebrowser/issues/3888 1026 log.webview.warning("Unable to find event target!") 1027 return 1028 1029 evt.posted = True # type: ignore[attr-defined] 1030 QApplication.postEvent(recipient, evt) 1031 1032 def navigation_blocked(self) -> bool: 1033 """Test if navigation is allowed on the current tab.""" 1034 return self.data.pinned and config.val.tabs.pinned.frozen 1035 1036 @pyqtSlot(QUrl) 1037 def _on_before_load_started(self, url: QUrl) -> None: 1038 """Adjust the title if we are going to visit a URL soon.""" 1039 qtutils.ensure_valid(url) 1040 url_string = url.toDisplayString() 1041 log.webview.debug("Going to start loading: {}".format(url_string)) 1042 self.title_changed.emit(url_string) 1043 1044 @pyqtSlot(QUrl) 1045 def _on_url_changed(self, url: QUrl) -> None: 1046 """Update title when URL has changed and no title is available.""" 1047 if url.isValid() and not self.title(): 1048 self.title_changed.emit(url.toDisplayString()) 1049 self.url_changed.emit(url) 1050 1051 @pyqtSlot() 1052 def _on_load_started(self) -> None: 1053 self._progress = 0 1054 self.data.viewing_source = False 1055 self._set_load_status(usertypes.LoadStatus.loading) 1056 self.load_started.emit() 1057 1058 @pyqtSlot(usertypes.NavigationRequest) 1059 def _on_navigation_request( 1060 self, 1061 navigation: usertypes.NavigationRequest 1062 ) -> None: 1063 """Handle common acceptNavigationRequest code.""" 1064 url = utils.elide(navigation.url.toDisplayString(), 100) 1065 log.webview.debug("navigation request: url {}, type {}, is_main_frame " 1066 "{}".format(url, 1067 navigation.navigation_type, 1068 navigation.is_main_frame)) 1069 1070 if navigation.is_main_frame: 1071 self.data.last_navigation = navigation 1072 1073 if not navigation.url.isValid(): 1074 if navigation.navigation_type == navigation.Type.link_clicked: 1075 msg = urlutils.get_errstring(navigation.url, 1076 "Invalid link clicked") 1077 message.error(msg) 1078 self.data.open_target = usertypes.ClickTarget.normal 1079 1080 log.webview.debug("Ignoring invalid URL {} in " 1081 "acceptNavigationRequest: {}".format( 1082 navigation.url.toDisplayString(), 1083 navigation.url.errorString())) 1084 navigation.accepted = False 1085 1086 @pyqtSlot(bool) 1087 def _on_load_finished(self, ok: bool) -> None: 1088 assert self._widget is not None 1089 if sip.isdeleted(self._widget): 1090 # https://github.com/qutebrowser/qutebrowser/issues/3498 1091 return 1092 1093 if sessions.session_manager is not None: 1094 sessions.session_manager.save_autosave() 1095 1096 self.load_finished.emit(ok) 1097 1098 if not self.title(): 1099 self.title_changed.emit(self.url().toDisplayString()) 1100 1101 self.zoom.reapply() 1102 1103 def _update_load_status(self, ok: bool) -> None: 1104 """Update the load status after a page finished loading. 1105 1106 Needs to be called by subclasses to trigger a load status update, e.g. 1107 as a response to a loadFinished signal. 1108 """ 1109 url = self.url() 1110 is_https = url.scheme() == 'https' 1111 1112 if not ok: 1113 loadstatus = usertypes.LoadStatus.error 1114 elif is_https and url.host() in self._insecure_hosts: 1115 loadstatus = usertypes.LoadStatus.warn 1116 elif is_https: 1117 loadstatus = usertypes.LoadStatus.success_https 1118 else: 1119 loadstatus = usertypes.LoadStatus.success 1120 1121 self._set_load_status(loadstatus) 1122 1123 @pyqtSlot() 1124 def _on_history_trigger(self) -> None: 1125 """Emit history_item_triggered based on backend-specific signal.""" 1126 raise NotImplementedError 1127 1128 @pyqtSlot(int) 1129 def _on_load_progress(self, perc: int) -> None: 1130 self._progress = perc 1131 self.load_progress.emit(perc) 1132 1133 def url(self, *, requested: bool = False) -> QUrl: 1134 raise NotImplementedError 1135 1136 def progress(self) -> int: 1137 return self._progress 1138 1139 def load_status(self) -> usertypes.LoadStatus: 1140 return self._load_status 1141 1142 def _load_url_prepare(self, url: QUrl) -> None: 1143 qtutils.ensure_valid(url) 1144 self.before_load_started.emit(url) 1145 1146 def load_url(self, url: QUrl) -> None: 1147 raise NotImplementedError 1148 1149 def reload(self, *, force: bool = False) -> None: 1150 raise NotImplementedError 1151 1152 def stop(self) -> None: 1153 raise NotImplementedError 1154 1155 def fake_key_press(self, 1156 key: Qt.Key, 1157 modifier: Qt.KeyboardModifier = Qt.NoModifier) -> None: 1158 """Send a fake key event to this tab.""" 1159 press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0) 1160 release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier, 1161 0, 0, 0) 1162 self.send_event(press_evt) 1163 self.send_event(release_evt) 1164 1165 def dump_async(self, 1166 callback: Callable[[str], None], *, 1167 plain: bool = False) -> None: 1168 """Dump the current page's html asynchronously. 1169 1170 The given callback will be called with the result when dumping is 1171 complete. 1172 """ 1173 raise NotImplementedError 1174 1175 def run_js_async( 1176 self, 1177 code: str, 1178 callback: Callable[[Any], None] = None, *, 1179 world: Union[usertypes.JsWorld, int] = None 1180 ) -> None: 1181 """Run javascript async. 1182 1183 The given callback will be called with the result when running JS is 1184 complete. 1185 1186 Args: 1187 code: The javascript code to run. 1188 callback: The callback to call with the result, or None. 1189 world: A world ID (int or usertypes.JsWorld member) to run the JS 1190 in the main world or in another isolated world. 1191 """ 1192 raise NotImplementedError 1193 1194 def title(self) -> str: 1195 raise NotImplementedError 1196 1197 def icon(self) -> None: 1198 raise NotImplementedError 1199 1200 def set_html(self, html: str, base_url: QUrl = QUrl()) -> None: 1201 raise NotImplementedError 1202 1203 def set_pinned(self, pinned: bool) -> None: 1204 self.data.pinned = pinned 1205 self.pinned_changed.emit(pinned) 1206 1207 def renderer_process_pid(self) -> Optional[int]: 1208 """Get the PID of the underlying renderer process. 1209 1210 Returns None if the PID can't be determined or if getting the PID isn't 1211 supported. 1212 """ 1213 raise NotImplementedError 1214 1215 def grab_pixmap(self, rect: QRect = None) -> Optional[QPixmap]: 1216 """Grab a QPixmap of the displayed page. 1217 1218 Returns None if we got a null pixmap from Qt. 1219 """ 1220 if rect is None: 1221 pic = self._widget.grab() 1222 else: 1223 qtutils.ensure_valid(rect) 1224 pic = self._widget.grab(rect) 1225 1226 if pic.isNull(): 1227 return None 1228 1229 return pic 1230 1231 def __repr__(self) -> str: 1232 try: 1233 qurl = self.url() 1234 url = qurl.toDisplayString( 1235 QUrl.EncodeUnicode) # type: ignore[arg-type] 1236 except (AttributeError, RuntimeError) as exc: 1237 url = '<{}>'.format(exc.__class__.__name__) 1238 else: 1239 url = utils.elide(url, 100) 1240 return utils.get_repr(self, tab_id=self.tab_id, url=url) 1241 1242 def is_deleted(self) -> bool: 1243 assert self._widget is not None 1244 return sip.isdeleted(self._widget) 1245