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