1# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: 2 3# Copyright 2015-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 QtWebKit/QtWebEngine web inspector.""" 21 22import base64 23import binascii 24import enum 25from typing import cast, Optional 26 27from PyQt5.QtWidgets import QWidget 28from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent 29from PyQt5.QtGui import QCloseEvent 30 31from qutebrowser.browser import eventfilter 32from qutebrowser.config import configfiles 33from qutebrowser.utils import log, usertypes 34from qutebrowser.keyinput import modeman 35from qutebrowser.misc import miscwidgets 36 37 38class Position(enum.Enum): 39 40 """Where the inspector is shown.""" 41 42 right = enum.auto() 43 left = enum.auto() 44 top = enum.auto() 45 bottom = enum.auto() 46 window = enum.auto() 47 48 49class Error(Exception): 50 51 """Raised when the inspector could not be initialized.""" 52 53 54class _EventFilter(QObject): 55 56 """Event filter to enter insert mode when inspector was clicked. 57 58 We need to use this with a ChildEventFilter (rather than just overriding 59 mousePressEvent) for two reasons: 60 61 - For QtWebEngine, we need to listen for mouse events on its focusProxy(), 62 which can change when another page loads (which might be possible with an 63 inspector as well?) 64 65 - For QtWebKit, we need to listen for mouse events on the QWebView used by 66 the QWebInspector. 67 """ 68 69 clicked = pyqtSignal() 70 71 def eventFilter(self, _obj: QObject, event: QEvent) -> bool: 72 """Translate mouse presses to a clicked signal.""" 73 if event.type() == QEvent.MouseButtonPress: 74 self.clicked.emit() 75 return False 76 77 78class AbstractWebInspector(QWidget): 79 80 """Base class for QtWebKit/QtWebEngine inspectors. 81 82 Attributes: 83 _position: position of the inspector (right/left/top/bottom/window) 84 _splitter: InspectorSplitter where the inspector can be placed. 85 86 Signals: 87 recreate: Emitted when the inspector should be recreated. 88 """ 89 90 recreate = pyqtSignal() 91 92 def __init__(self, splitter: 'miscwidgets.InspectorSplitter', 93 win_id: int, 94 parent: QWidget = None) -> None: 95 super().__init__(parent) 96 self._widget = cast(QWidget, None) 97 self._layout = miscwidgets.WrapperLayout(self) 98 self._splitter = splitter 99 self._position: Optional[Position] = None 100 self._win_id = win_id 101 102 self._event_filter = _EventFilter(parent=self) 103 self._event_filter.clicked.connect(self._on_clicked) 104 self._child_event_filter = eventfilter.ChildEventFilter( 105 eventfilter=self._event_filter, 106 parent=self) 107 108 def _set_widget(self, widget: QWidget) -> None: 109 self._widget = widget 110 self._widget.setWindowTitle("Web Inspector") 111 self._widget.installEventFilter(self._child_event_filter) 112 self._layout.wrap(self, self._widget) 113 114 def _load_position(self) -> Position: 115 """Get the last position the inspector was in.""" 116 pos = configfiles.state['inspector'].get('position', 'right') 117 return Position[pos] 118 119 def _save_position(self, position: Position) -> None: 120 """Save the last position the inspector was in.""" 121 configfiles.state['inspector']['position'] = position.name 122 123 def _needs_recreate(self) -> bool: 124 """Whether the inspector needs recreation when detaching to a window. 125 126 This is done due to an unknown QtWebEngine bug which sometimes prevents 127 inspector windows from showing up. 128 129 Needs to be overridden by subclasses. 130 """ 131 return False 132 133 @pyqtSlot() 134 def _on_clicked(self) -> None: 135 """Enter insert mode if a docked inspector was clicked.""" 136 if self._position != Position.window: 137 modeman.enter(self._win_id, usertypes.KeyMode.insert, 138 reason='Inspector clicked', only_if_normal=True) 139 140 def set_position(self, position: Optional[Position]) -> None: 141 """Set the position of the inspector. 142 143 If the position is None, the last known position is used. 144 """ 145 if position is None: 146 position = self._load_position() 147 else: 148 self._save_position(position) 149 150 if position == self._position: 151 self.toggle() 152 return 153 154 if (position == Position.window and 155 self._position is not None and 156 self._needs_recreate()): 157 # Detaching to window 158 self.recreate.emit() 159 self.shutdown() 160 return 161 elif position == Position.window: 162 self.setParent(None) # type: ignore[call-overload] 163 self._load_state_geometry() 164 else: 165 self._splitter.set_inspector(self, position) 166 167 self._position = position 168 169 self._widget.show() 170 self.show() 171 172 def toggle(self) -> None: 173 """Toggle visibility of the inspector.""" 174 if self.isVisible(): 175 self.hide() 176 else: 177 self.show() 178 179 def _load_state_geometry(self) -> None: 180 """Load the geometry from the state file.""" 181 try: 182 data = configfiles.state['inspector']['window'] 183 geom = base64.b64decode(data, validate=True) 184 except KeyError: 185 # First start 186 pass 187 except binascii.Error: 188 log.misc.exception("Error while reading geometry") 189 else: 190 log.init.debug("Loading geometry from {!r}".format(geom)) 191 ok = self._widget.restoreGeometry(geom) 192 if not ok: 193 log.init.warning("Error while loading geometry.") 194 195 def closeEvent(self, _e: QCloseEvent) -> None: 196 """Save the geometry when closed.""" 197 data = self._widget.saveGeometry().data() 198 geom = base64.b64encode(data).decode('ASCII') 199 configfiles.state['inspector']['window'] = geom 200 201 def inspect(self, page: QWidget) -> None: 202 """Inspect the given QWeb(Engine)Page.""" 203 raise NotImplementedError 204 205 @pyqtSlot() 206 def shutdown(self) -> None: 207 """Clean up the inspector.""" 208 self.close() 209 self.deleteLater() 210