def _prevnext_cb(elems): elem = _find_prevnext(prev, elems) word = 'prev' if prev else 'forward' if elem is None: message.error("No {} links found!".format(word)) return url = elem.resolve_url(baseurl) if url is None: message.error("No {} links found!".format(word)) return qtutils.ensure_valid(url) cur_tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) if window: new_window = mainwindow.MainWindow( private=cur_tabbed_browser.is_private) new_window.show() tabbed_browser = objreg.get('tabbed-browser', scope='window', window=new_window.win_id) tabbed_browser.tabopen(url, background=False) elif tab: cur_tabbed_browser.tabopen(url, background=background) else: browsertab.load_url(url)
def _load_url_prepare(self, url: QUrl, *, emit_before_load_started: bool = True) -> None: qtutils.ensure_valid(url) if emit_before_load_started: self.before_load_started.emit(url)
def sizeHint(self, option, index): """Override sizeHint of QStyledItemDelegate. Return the cell size based on the QTextDocument size, but might not work correctly yet. Args: option: const QStyleOptionViewItem & option index: const QModelIndex & index Return: A QSize with the recommended size. """ value = index.data(Qt.SizeHintRole) if value is not None: return value self._opt = QStyleOptionViewItem(option) self.initStyleOption(self._opt, index) self._style = self._opt.widget.style() self._get_textdoc(index) docsize = self._doc.size().toSize() size = self._style.sizeFromContents(QStyle.CT_ItemViewItem, self._opt, docsize, self._opt.widget) qtutils.ensure_valid(size) return size + QSize(10, 3)
def resolve(self, query, from_file=False): """Resolve a proxy via PAC. Args: query: QNetworkProxyQuery. from_file: Whether the proxy info is coming from a file. Return: A list of QNetworkProxy objects in order of preference. """ qtutils.ensure_valid(query.url()) if from_file: string_flags = QUrl.PrettyDecoded else: string_flags = QUrl.RemoveUserInfo if query.url().scheme() == 'https': string_flags |= QUrl.RemovePath | QUrl.RemoveQuery result = self._resolver.call([query.url().toString(string_flags), query.peerHostName()]) result_str = result.toString() if not result.isString(): err = "Got strange value from FindProxyForURL: '{}'" raise EvalProxyError(err.format(result_str)) return self._parse_proxy_string(result_str)
def _get_search_url(txt): """Get a search engine URL for a text. Args: txt: Text to search for. Return: The search URL as a QUrl. """ log.url.debug("Finding search engine for {!r}".format(txt)) engine, term = _parse_search_term(txt) assert term if engine is None: engine = 'DEFAULT' template = config.val.url.searchengines[engine] quoted_term = urllib.parse.quote(term, safe='') url = qurl_from_user_input(template.format(quoted_term)) if config.val.url.open_base_url and term in config.val.url.searchengines: url = qurl_from_user_input(config.val.url.searchengines[term]) url.setPath(None) url.setFragment(None) url.setQuery(None) qtutils.ensure_valid(url) return url
def resolve_url(self, baseurl: QUrl) -> typing.Optional[QUrl]: """Resolve the URL in the element's src/href attribute. Args: baseurl: The URL to base relative URLs on as QUrl. Return: A QUrl with the absolute URL, or None. """ if baseurl.isRelative(): raise ValueError("Need an absolute base URL!") for attr in ['href', 'src']: if attr in self: text = self[attr].strip() break else: return None url = QUrl(text) if not url.isValid(): return None if url.isRelative(): url = baseurl.resolved(url) qtutils.ensure_valid(url) return url
def requestStarted(self, job): """Handle a request for a glimpse: scheme. This method must be reimplemented by all custom URL scheme handlers. The request is asynchronous and does not need to be handled right away. Args: job: QWebEngineUrlRequestJob """ url = job.requestUrl() if url.scheme() in ['chrome-error', 'chrome-extension']: # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378 job.fail(QWebEngineUrlRequestJob.UrlInvalid) return if not self._check_initiator(job): return if job.requestMethod() != b'GET': job.fail(QWebEngineUrlRequestJob.RequestDenied) return assert url.scheme() == 'glimpse' log.misc.debug("Got request for {}".format(url.toDisplayString())) try: mimetype, data = glimpsescheme.data_for_url(url) except glimpsescheme.Error as e: errors = { glimpsescheme.NotFoundError: QWebEngineUrlRequestJob.UrlNotFound, glimpsescheme.UrlInvalidError: QWebEngineUrlRequestJob.UrlInvalid, glimpsescheme.RequestDeniedError: QWebEngineUrlRequestJob.RequestDenied, glimpsescheme.SchemeOSError: QWebEngineUrlRequestJob.UrlNotFound, glimpsescheme.Error: QWebEngineUrlRequestJob.RequestFailed, } exctype = type(e) log.misc.error("{} while handling glimpse://* URL".format( exctype.__name__)) job.fail(errors[exctype]) except glimpsescheme.Redirect as e: qtutils.ensure_valid(e.url) job.redirect(e.url) else: log.misc.debug("Returning {} data".format(mimetype)) # We can't just use the QBuffer constructor taking a QByteArray, # because that somehow segfaults... # https://www.riverbankcomputing.com/pipermail/pyqt/2016-September/038075.html buf = QBuffer(parent=self) buf.open(QIODevice.WriteOnly) buf.write(data) buf.seek(0) buf.close() job.reply(mimetype.encode('ascii'), buf)
def first_item(self): """Return the index of the first child (non-category) in the model.""" for row, cat in enumerate(self._categories): if cat.rowCount() > 0: parent = self.index(row, 0) index = self.index(0, 0, parent) qtutils.ensure_valid(index) return index return QModelIndex()
def last_item(self): """Return the index of the last child (non-category) in the model.""" for row, cat in reversed(list(enumerate(self._categories))): childcount = cat.rowCount() if childcount > 0: parent = self.index(row, 0) index = self.index(childcount - 1, 0, parent) qtutils.ensure_valid(index) return index return QModelIndex()
def paintEvent(self, e): """Override QLabel::paintEvent to draw elided text.""" if self._elidemode == Qt.ElideNone: super().paintEvent(e) else: e.accept() painter = QPainter(self) geom = self.geometry() qtutils.ensure_valid(geom) painter.drawText(0, 0, geom.width(), geom.height(), int(self.alignment()), self._elided_text)
def load_url(self, url, newtab): """Open a URL, used as a slot. Args: url: The URL to open as QUrl. newtab: True to open URL in a new tab, False otherwise. """ qtutils.ensure_valid(url) if newtab or self.widget.currentWidget() is None: self.tabopen(url, background=False) else: self.widget.currentWidget().load_url(url)
def handler(request, operation, current_url): """Scheme handler for glimpse:// URLs. Args: request: QNetworkRequest to answer to. operation: The HTTP operation being done. current_url: The page we're on currently. Return: A QNetworkReply. """ if operation != QNetworkAccessManager.GetOperation: return networkreply.ErrorNetworkReply( request, "Unsupported request type", QNetworkReply.ContentOperationNotPermittedError) url = request.url() if ((url.scheme(), url.host(), url.path()) == ('glimpse', 'settings', '/set')): if current_url != QUrl('glimpse://settings/'): log.webview.warning("Blocking malicious request from {} to {}" .format(current_url.toDisplayString(), url.toDisplayString())) return networkreply.ErrorNetworkReply( request, "Invalid glimpse://settings request", QNetworkReply.ContentAccessDenied) try: mimetype, data = glimpsescheme.data_for_url(url) except glimpsescheme.Error as e: errors = { glimpsescheme.NotFoundError: QNetworkReply.ContentNotFoundError, glimpsescheme.UrlInvalidError: QNetworkReply.ContentOperationNotPermittedError, glimpsescheme.RequestDeniedError: QNetworkReply.ContentAccessDenied, glimpsescheme.SchemeOSError: QNetworkReply.ContentNotFoundError, glimpsescheme.Error: QNetworkReply.InternalServerError, } exctype = type(e) log.misc.error("{} while handling glimpse://* URL".format( exctype.__name__)) return networkreply.ErrorNetworkReply(request, str(e), errors[exctype]) except glimpsescheme.Redirect as e: qtutils.ensure_valid(e.url) return networkreply.RedirectNetworkReply(e.url) return networkreply.FixedDataNetworkReply(request, data, mimetype)
def delete_url(self, url): """Remove all history entries with the given url. Args: url: URL string to delete. """ qurl = QUrl(url) qtutils.ensure_valid(qurl) self.delete('url', self._format_url(qurl)) self.completion.delete('url', self._format_completion_url(qurl)) if self._last_url == url: self._last_url = None self.url_cleared.emit(qurl)
def sizeHint(self): """Return sizeHint based on the view contents.""" idx = self.model().last_index() bottom = self.visualRect(idx).bottom() if bottom != -1: margins = self.contentsMargins() height = (bottom + margins.top() + margins.bottom() + 2 * self.spacing()) size = QSize(0, height) else: size = QSize(0, 0) qtutils.ensure_valid(size) return size
def _draw_text(self, index): """Draw the text of an ItemViewItem. This is the main part where we differ from the original implementation in Qt: We use a QTextDocument to draw text. Args: index: The QModelIndex of the item to draw. """ if not self._opt.text: return text_rect_ = self._style.subElementRect( self._style.SE_ItemViewItemText, self._opt, self._opt.widget) qtutils.ensure_valid(text_rect_) margin = self._style.pixelMetric(QStyle.PM_FocusFrameHMargin, self._opt, self._opt.widget) + 1 # remove width padding text_rect = text_rect_.adjusted(margin, 0, -margin, 0) qtutils.ensure_valid(text_rect) # move text upwards a bit if index.parent().isValid(): text_rect.adjust(0, -1, 0, -1) else: text_rect.adjust(0, -2, 0, -2) self._painter.save() state = self._opt.state if state & QStyle.State_Enabled and state & QStyle.State_Active: cg = QPalette.Normal elif state & QStyle.State_Enabled: cg = QPalette.Inactive else: cg = QPalette.Disabled if state & QStyle.State_Selected: self._painter.setPen(self._opt.palette.color( cg, QPalette.HighlightedText)) # This is a dirty fix for the text jumping by one pixel for # whatever reason. text_rect.adjust(0, -1, 0, 0) else: self._painter.setPen(self._opt.palette.color(cg, QPalette.Text)) if state & QStyle.State_Editing: self._painter.setPen(self._opt.palette.color(cg, QPalette.Text)) self._painter.drawRect(text_rect_.adjusted(0, 0, -1, -1)) self._painter.translate(text_rect.left(), text_rect.top()) self._get_textdoc(index) self._draw_textdoc(text_rect, index.column()) self._painter.restore()
def tab_url(self, idx): """Get the URL of the tab at the given index. Return: The tab URL as QUrl. """ tab = self.widget(idx) if tab is None: url = QUrl() else: url = tab.url() # It's possible for url to be invalid, but the caller will handle that. qtutils.ensure_valid(url) return url
def connect_log_slot(obj): """Helper function to connect all signals to a logging slot.""" metaobj = obj.metaObject() for i in range(metaobj.methodCount()): meta_method = metaobj.method(i) qtutils.ensure_valid(meta_method) if meta_method.methodType() == QMetaMethod.Signal: name = bytes(meta_method.name()).decode('ascii') if name != 'destroyed': signal = getattr(obj, name) try: signal.connect(functools.partial( log_slot, obj, signal)) except TypeError: # pragma: no cover pass
def _draw_icon(self, layouts, opt, p): """Draw the tab icon. Args: layouts: The layouts from _tab_layout. opt: QStyleOption p: QPainter """ qtutils.ensure_valid(layouts.icon) icon_mode = (QIcon.Normal if opt.state & QStyle.State_Enabled else QIcon.Disabled) icon_state = (QIcon.On if opt.state & QStyle.State_Selected else QIcon.Off) icon = opt.icon.pixmap(opt.iconSize, icon_mode, icon_state) self._style.drawItemPixmap(p, layouts.icon, Qt.AlignCenter, icon)
def _tab_layout(self, opt): """Compute the text/icon rect from the opt rect. This is based on Qt's QCommonStylePrivate::tabLayout (qtbase/src/widgets/styles/qcommonstyle.cpp) as we can't use the private implementation. Args: opt: QStyleOptionTab Return: A Layout object with two QRects. """ padding = config.cache['tabs.padding'] indicator_padding = config.cache['tabs.indicator.padding'] text_rect = QRect(opt.rect) if not text_rect.isValid(): # This happens sometimes according to crash reports, but no idea # why... return None text_rect.adjust(padding.left, padding.top, -padding.right, -padding.bottom) indicator_width = config.cache['tabs.indicator.width'] if indicator_width == 0: indicator_rect = QRect() else: indicator_rect = QRect(opt.rect) qtutils.ensure_valid(indicator_rect) indicator_rect.adjust(padding.left + indicator_padding.left, padding.top + indicator_padding.top, 0, -(padding.bottom + indicator_padding.bottom)) indicator_rect.setWidth(indicator_width) text_rect.adjust(indicator_width + indicator_padding.left + indicator_padding.right, 0, 0, 0) icon_rect = self._get_icon_rect(opt, text_rect) if icon_rect.isValid(): text_rect.adjust( icon_rect.width() + TabBarStyle.ICON_PADDING, 0, 0, 0) text_rect = self._style.visualRect(opt.direction, opt.rect, text_rect) return Layouts(text=text_rect, icon=icon_rect, indicator=indicator_rect)
def get_by_qurl(self, url): """Look up a quickmark by QUrl, returning its name. Takes O(n) time, where n is the number of quickmarks. Use a name instead where possible. """ qtutils.ensure_valid(url) urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) try: index = list(self.marks.values()).index(urlstr) key = list(self.marks.keys())[index] except ValueError: raise DoesNotExistError( "Quickmark for '{}' not found!".format(urlstr)) return key
def test_ensure_valid(obj, raising, exc_reason, exc_str): """Test ensure_valid. Args: obj: The object to test with. raising: Whether QtValueError is expected to be raised. exc_reason: The expected .reason attribute of the exception. exc_str: The expected string of the exception. """ if raising: with pytest.raises(qtutils.QtValueError) as excinfo: qtutils.ensure_valid(obj) assert excinfo.value.reason == exc_reason assert str(excinfo.value) == exc_str else: qtutils.ensure_valid(obj)
def incdec_number(url, incdec, count=1, segments=None): """Find a number in the url and increment or decrement it. Args: url: The current url incdec: Either 'increment' or 'decrement' count: The number to increment or decrement by segments: A set of URL segments to search. Valid segments are: 'host', 'port', 'path', 'query', 'anchor'. Default: {'path', 'query'} Return: The new url with the number incremented/decremented. Raises IncDecError if the url contains no number. """ if not url.isValid(): raise InvalidUrlError(url) if segments is None: segments = {'path', 'query'} valid_segments = {'host', 'port', 'path', 'query', 'anchor'} if segments - valid_segments: extra_elements = segments - valid_segments raise IncDecError("Invalid segments: {}".format( ', '.join(extra_elements)), url) # Make a copy of the QUrl so we don't modify the original url = QUrl(url) # We're searching the last number so we walk the url segments backwards for segment, getter, setter in reversed(_URL_SEGMENTS): if segment not in segments: continue # Get the last number in a string not preceded by regex '%' or '%.' match = re.fullmatch(r'(.*\D|^)(?<!%)(?<!%.)(0*)(\d+)(.*)', getter(url)) if not match: continue setter(url, _get_incdec_value(match, incdec, url, count)) qtutils.ensure_valid(url) return url raise IncDecError("No number found in URL!", url)
def delete_cur_item(self, index): """Delete the row at the given index.""" qtutils.ensure_valid(index) parent = index.parent() cat = self._cat_from_idx(parent) assert cat, "CompletionView sent invalid index for deletion" if not cat.delete_func: raise cmdutils.CommandError("Cannot delete this item.") data = [ cat.data(cat.index(index.row(), i)) for i in range(cat.columnCount()) ] cat.delete_func(data) self.beginRemoveRows(parent, index.row(), index.row()) cat.removeRow(index.row(), QModelIndex()) self.endRemoveRows()
def tabSizeHint(self, index: int) -> QSize: """Override tabSizeHint to customize qb's tab size. https://wiki.python.org/moin/PyQt/Customising%20tab%20bars Args: index: The index of the tab. Return: A QSize. """ if self.count() == 0: # This happens on startup on macOS. # We return it directly rather than setting `size' because we don't # want to ensure it's valid in this special case. return QSize() height = self._minimum_tab_height() if self.vertical: confwidth = str(config.cache['tabs.width']) if confwidth.endswith('%'): main_window = objreg.get('main-window', scope='window', window=self._win_id) perc = int(confwidth.rstrip('%')) width = main_window.width() * perc / 100 else: width = int(confwidth) size = QSize(width, height) else: if config.cache['tabs.pinned.shrink'] and self._tab_pinned(index): # Give pinned tabs the minimum size they need to display their # titles, let Qt handle scaling it down if we get too small. width = self.minimumTabSizeHint(index, ellipsis=False).width() else: # Request as much space as possible so we fill the tabbar, let # Qt shrink us down. If for some reason (tests, bugs) # self.width() gives 0, use a sane min of 10 px width = max(self.width(), 10) max_width = config.cache['tabs.max_width'] if max_width > 0: width = min(max_width, width) size = QSize(width, height) qtutils.ensure_valid(size) return size
def _on_data_changed(self, idx, *, webengine): """Called when a downloader's data changed. Args: start: The first changed index as int. end: The last changed index as int, or -1 for all indices. webengine: If given, the QtNetwork download length is added to the index. """ if idx == -1: start_index = self.index(0, 0) end_index = self.last_index() else: if webengine: idx += len(self._qtnetwork_manager.downloads) start_index = self.index(idx, 0) end_index = self.index(idx, 0) qtutils.ensure_valid(start_index) qtutils.ensure_valid(end_index) self.dataChanged.emit(start_index, end_index)
def _draw_focus_rect(self): """Draw the focus rectangle of an ItemViewItem.""" state = self._opt.state if not state & QStyle.State_HasFocus: return o = self._opt o.rect = self._style.subElementRect( self._style.SE_ItemViewItemFocusRect, self._opt, self._opt.widget) o.state |= QStyle.State_KeyboardFocusChange | QStyle.State_Item qtutils.ensure_valid(o.rect) if state & QStyle.State_Enabled: cg = QPalette.Normal else: cg = QPalette.Disabled if state & QStyle.State_Selected: role = QPalette.Highlight else: role = QPalette.Window o.backgroundColor = self._opt.palette.color(cg, role) self._style.drawPrimitive(QStyle.PE_FrameFocusRect, o, self._painter, self._opt.widget)
def _save_tab(self, tab, active): """Get a dict with data for a single tab. Args: tab: The WebView to save. active: Whether the tab is currently active. """ data = {'history': []} if active: data['active'] = True for idx, item in enumerate(tab.history): qtutils.ensure_valid(item) item_data = self._save_tab_item(tab, idx, item) if item.url().scheme() == 'glimpse' and item.url().host() == 'back': # don't add glimpse://back to the session file if item_data.get('active', False) and data['history']: # mark entry before glimpse://back as active data['history'][-1]['active'] = True else: data['history'].append(item_data) return data
def update_for_url(self, url: QUrl) -> typing.Set[str]: """Update settings customized for the given tab. Return: A set of settings which actually changed. """ qtutils.ensure_valid(url) changed_settings = set() for values in config.instance: if not values.opt.supports_pattern: continue value = values.get_for_url(url, fallback=False) changed = self._update_setting(values.opt.name, value) if changed: log.config.debug("Changed for {}: {} = {}".format( url.toDisplayString(), values.opt.name, value)) changed_settings.add(values.opt.name) return changed_settings
def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True, force_search=False): """Get a QUrl based on a user input which is URL or search term. Args: urlstr: URL to load as a string. cwd: The current working directory, or None. relative: Whether to resolve relative files. do_search: Whether to perform a search on non-URLs. force_search: Whether to force a search even if the content can be interpreted as a URL or a path. Return: A target QUrl to a search page or the original URL. """ urlstr = urlstr.strip() path = get_path_if_valid(urlstr, cwd=cwd, relative=relative, check_exists=True) if not force_search and path is not None: url = QUrl.fromLocalFile(path) elif force_search or (do_search and not is_url(urlstr)): # probably a search term log.url.debug("URL is a fuzzy search term") try: url = _get_search_url(urlstr) except ValueError: # invalid search engine url = qurl_from_user_input(urlstr) else: # probably an address log.url.debug("URL is a fuzzy address") url = qurl_from_user_input(urlstr) log.url.debug("Converting fuzzy term {!r} to URL -> {}".format( urlstr, url.toDisplayString())) if do_search and config.val.url.auto_search != 'never' and urlstr: qtutils.ensure_valid(url) else: if not url.isValid(): raise InvalidUrlError(url) return url
def lessThan(self, lindex, rindex): """Custom sorting implementation. Prefers all items which start with self._pattern. Other than that, uses normal Python string sorting. Args: lindex: The QModelIndex of the left item (*left* < right) rindex: The QModelIndex of the right item (left < *right*) Return: True if left < right, else False """ qtutils.ensure_valid(lindex) qtutils.ensure_valid(rindex) left = self.srcmodel.data(lindex) right = self.srcmodel.data(rindex) if left is None or right is None: # pragma: no cover log.completion.warning("Got unexpected None value, " "left={!r} right={!r} " "lindex={!r} rindex={!r}" .format(left, right, lindex, rindex)) return False leftstart = left.startswith(self._pattern) rightstart = right.startswith(self._pattern) if leftstart and not rightstart: return True elif rightstart and not leftstart: return False elif self._sort: return left < right else: return False