2014-06-19 09:04:37 +02:00
|
|
|
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
|
|
|
|
2015-01-03 15:51:31 +01:00
|
|
|
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
2014-02-06 14:01:23 +01:00
|
|
|
#
|
|
|
|
# This file is part of qutebrowser.
|
|
|
|
#
|
|
|
|
# qutebrowser is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# qutebrowser is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
2014-01-29 15:30:19 +01:00
|
|
|
|
2014-03-03 21:35:13 +01:00
|
|
|
"""The main tabbed browser widget."""
|
2014-02-17 12:23:52 +01:00
|
|
|
|
2014-08-26 19:10:14 +02:00
|
|
|
import functools
|
2014-09-27 22:56:50 +02:00
|
|
|
import collections
|
2014-01-20 15:58:49 +01:00
|
|
|
|
2014-05-17 22:38:07 +02:00
|
|
|
from PyQt5.QtWidgets import QSizePolicy
|
2014-09-03 10:47:27 +02:00
|
|
|
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSize, QTimer, QUrl
|
2014-07-14 18:01:02 +02:00
|
|
|
from PyQt5.QtGui import QIcon
|
2014-08-12 17:18:13 +02:00
|
|
|
from PyQt5.QtWebKitWidgets import QWebPage
|
2014-01-20 15:58:49 +01:00
|
|
|
|
2014-08-26 19:10:14 +02:00
|
|
|
from qutebrowser.config import config
|
|
|
|
from qutebrowser.keyinput import modeman
|
2014-12-13 17:28:50 +01:00
|
|
|
from qutebrowser.mainwindow import tabwidget
|
|
|
|
from qutebrowser.browser import signalfilter, commands, webview
|
2014-10-06 22:15:26 +02:00
|
|
|
from qutebrowser.utils import (log, message, usertypes, utils, qtutils, objreg,
|
|
|
|
urlutils)
|
2013-12-15 21:40:15 +01:00
|
|
|
|
2014-01-28 23:04:02 +01:00
|
|
|
|
2014-09-27 22:56:50 +02:00
|
|
|
UndoEntry = collections.namedtuple('UndoEntry', ['url', 'history'])
|
|
|
|
|
|
|
|
|
2015-03-03 23:21:23 +01:00
|
|
|
class TabDeletedError(Exception):
|
|
|
|
|
|
|
|
"""Exception raised when _tab_index is called for a deleted tab."""
|
|
|
|
|
|
|
|
|
2014-08-26 19:10:14 +02:00
|
|
|
class TabbedBrowser(tabwidget.TabWidget):
|
2014-02-07 20:21:50 +01:00
|
|
|
|
2014-01-29 15:30:19 +01:00
|
|
|
"""A TabWidget with QWebViews inside.
|
|
|
|
|
|
|
|
Provides methods to manage tabs, convenience methods to interact with the
|
|
|
|
current tab (cur_*) and filters signals to re-emit them when they occured
|
|
|
|
in the currently visible tab.
|
2014-02-05 14:01:16 +01:00
|
|
|
|
|
|
|
For all tab-specific signals (cur_*) emitted by a tab, this happens:
|
|
|
|
- the signal gets filtered with _filter_signals and self.cur_* gets
|
|
|
|
emitted if the signal occured in the current tab.
|
2014-02-07 20:21:50 +01:00
|
|
|
|
2014-02-18 16:38:13 +01:00
|
|
|
Attributes:
|
2014-09-28 22:13:14 +02:00
|
|
|
_win_id: The window ID this tabbedbrowser is associated with.
|
2014-04-17 09:44:26 +02:00
|
|
|
_filter: A SignalFilter instance.
|
2014-07-29 22:44:14 +02:00
|
|
|
_now_focused: The tab which is focused now.
|
2014-08-06 07:58:07 +02:00
|
|
|
_tab_insert_idx_left: Where to insert a new tab with
|
|
|
|
tabbar -> new-tab-position set to 'left'.
|
|
|
|
_tab_insert_idx_right: Same as above, for 'right'.
|
2014-09-27 22:56:50 +02:00
|
|
|
_undo_stack: List of UndoEntry namedtuples of closed tabs.
|
2014-02-18 16:38:13 +01:00
|
|
|
|
|
|
|
Signals:
|
|
|
|
cur_progress: Progress of the current tab changed (loadProgress).
|
|
|
|
cur_load_started: Current tab started loading (loadStarted)
|
|
|
|
cur_load_finished: Current tab finished loading (loadFinished)
|
|
|
|
cur_statusbar_message: Current tab got a statusbar message
|
|
|
|
(statusBarMessage)
|
2014-05-15 22:31:01 +02:00
|
|
|
cur_url_text_changed: Current URL text changed.
|
2014-02-18 16:38:13 +01:00
|
|
|
cur_link_hovered: Link hovered in current tab (linkHovered)
|
|
|
|
cur_scroll_perc_changed: Scroll percentage of current tab changed.
|
|
|
|
arg 1: x-position in %.
|
|
|
|
arg 2: y-position in %.
|
2014-05-16 07:11:39 +02:00
|
|
|
cur_load_status_changed: Loading status of current tab changed.
|
2014-10-07 17:09:24 +02:00
|
|
|
close_window: The last tab was closed, close this window.
|
2014-02-18 19:05:56 +01:00
|
|
|
resized: Emitted when the browser window has resized, so the completion
|
|
|
|
widget can adjust its size to it.
|
|
|
|
arg: The new size.
|
2014-06-16 22:49:22 +02:00
|
|
|
current_tab_changed: The current tab changed to the emitted WebView.
|
2014-01-29 15:30:19 +01:00
|
|
|
"""
|
2014-01-20 15:58:49 +01:00
|
|
|
|
2014-02-18 16:38:13 +01:00
|
|
|
cur_progress = pyqtSignal(int)
|
|
|
|
cur_load_started = pyqtSignal()
|
|
|
|
cur_load_finished = pyqtSignal(bool)
|
|
|
|
cur_statusbar_message = pyqtSignal(str)
|
2014-05-15 22:31:01 +02:00
|
|
|
cur_url_text_changed = pyqtSignal(str)
|
2014-02-18 16:38:13 +01:00
|
|
|
cur_link_hovered = pyqtSignal(str, str, str)
|
2014-01-21 08:37:21 +01:00
|
|
|
cur_scroll_perc_changed = pyqtSignal(int, int)
|
2014-05-16 07:11:39 +02:00
|
|
|
cur_load_status_changed = pyqtSignal(str)
|
2014-10-07 17:09:24 +02:00
|
|
|
close_window = pyqtSignal()
|
2014-02-18 19:05:56 +01:00
|
|
|
resized = pyqtSignal('QRect')
|
2014-05-21 19:53:58 +02:00
|
|
|
got_cmd = pyqtSignal(str)
|
2014-08-26 19:10:14 +02:00
|
|
|
current_tab_changed = pyqtSignal(webview.WebView)
|
2013-12-15 21:40:15 +01:00
|
|
|
|
2014-09-28 22:13:14 +02:00
|
|
|
def __init__(self, win_id, parent=None):
|
2014-09-28 23:23:02 +02:00
|
|
|
super().__init__(win_id, parent)
|
2014-09-28 22:13:14 +02:00
|
|
|
self._win_id = win_id
|
2014-08-06 07:58:07 +02:00
|
|
|
self._tab_insert_idx_left = 0
|
|
|
|
self._tab_insert_idx_right = -1
|
2014-05-15 00:02:40 +02:00
|
|
|
self.tabCloseRequested.connect(self.on_tab_close_requested)
|
2014-05-09 11:57:58 +02:00
|
|
|
self.currentChanged.connect(self.on_current_changed)
|
2014-09-25 19:08:15 +02:00
|
|
|
self.cur_load_started.connect(self.on_cur_load_started)
|
2014-01-30 22:29:01 +01:00
|
|
|
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
2014-09-27 22:56:50 +02:00
|
|
|
self._undo_stack = []
|
2014-09-28 22:13:14 +02:00
|
|
|
self._filter = signalfilter.SignalFilter(win_id, self)
|
|
|
|
dispatcher = commands.CommandDispatcher(win_id)
|
|
|
|
objreg.register('command-dispatcher', dispatcher, scope='window',
|
|
|
|
window=win_id)
|
2014-09-29 10:43:06 +02:00
|
|
|
self.destroyed.connect(
|
|
|
|
functools.partial(objreg.delete, 'command-dispatcher',
|
|
|
|
scope='window', window=win_id))
|
2014-07-29 22:44:14 +02:00
|
|
|
self._now_focused = None
|
2014-05-04 01:28:34 +02:00
|
|
|
# FIXME adjust this to font size
|
2014-10-01 22:23:27 +02:00
|
|
|
# https://github.com/The-Compiler/qutebrowser/issues/119
|
2014-05-04 01:28:34 +02:00
|
|
|
self.setIconSize(QSize(12, 12))
|
2014-10-18 19:50:10 +02:00
|
|
|
objreg.get('config').changed.connect(self.update_favicons)
|
2015-01-28 08:40:16 +01:00
|
|
|
objreg.get('config').changed.connect(self.update_window_title)
|
2015-02-13 17:30:36 +01:00
|
|
|
objreg.get('config').changed.connect(self.update_tab_titles)
|
2013-12-15 21:40:15 +01:00
|
|
|
|
2014-06-17 06:37:56 +02:00
|
|
|
def __repr__(self):
|
2014-09-26 15:48:24 +02:00
|
|
|
return utils.get_repr(self, count=self.count())
|
2014-06-17 06:37:56 +02:00
|
|
|
|
2015-03-03 23:21:23 +01:00
|
|
|
def _tab_index(self, tab):
|
|
|
|
"""Get the index of a given tab.
|
|
|
|
|
|
|
|
Raises TabDeletedError if the tab doesn't exist anymore.
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
idx = self.indexOf(tab)
|
|
|
|
except RuntimeError as e:
|
|
|
|
log.webview.debug("Got invalid tab {} ({})!".format(tab, e))
|
|
|
|
raise TabDeletedError(e)
|
|
|
|
if idx == -1:
|
|
|
|
log.webview.debug("Got invalid tab {} (index is -1)!".format(tab))
|
|
|
|
raise TabDeletedError("index is -1!")
|
|
|
|
return idx
|
|
|
|
|
2014-05-13 21:25:16 +02:00
|
|
|
def widgets(self):
|
|
|
|
"""Get a list of open tab widgets.
|
|
|
|
|
|
|
|
We don't implement this as generator so we can delete tabs while
|
2014-09-02 21:54:07 +02:00
|
|
|
iterating over the list.
|
|
|
|
"""
|
2014-05-13 21:25:16 +02:00
|
|
|
w = []
|
|
|
|
for i in range(self.count()):
|
|
|
|
w.append(self.widget(i))
|
|
|
|
return w
|
|
|
|
|
2015-01-28 08:40:16 +01:00
|
|
|
@config.change_filter('ui', 'window-title-format')
|
|
|
|
def update_window_title(self):
|
2015-01-25 23:25:08 +01:00
|
|
|
"""Change the window title to match the current tab."""
|
|
|
|
idx = self.currentIndex()
|
2015-02-13 17:30:36 +01:00
|
|
|
if idx == -1:
|
|
|
|
# (e.g. last tab removed)
|
|
|
|
log.webview.debug("Not updating window title because index is -1")
|
|
|
|
return
|
|
|
|
tabtitle = self.page_title(idx)
|
2015-01-25 23:25:08 +01:00
|
|
|
widget = self.widget(idx)
|
|
|
|
|
2015-01-28 08:40:16 +01:00
|
|
|
fields = {}
|
2015-01-25 23:25:08 +01:00
|
|
|
if widget.load_status == webview.LoadStatus.loading:
|
2015-01-28 08:40:16 +01:00
|
|
|
fields['perc'] = '[{}%] '.format(widget.progress)
|
2015-01-25 23:25:08 +01:00
|
|
|
else:
|
2015-01-28 08:40:16 +01:00
|
|
|
fields['perc'] = ''
|
|
|
|
fields['perc_raw'] = widget.progress
|
|
|
|
fields['title'] = tabtitle
|
|
|
|
fields['title_sep'] = ' - ' if tabtitle else ''
|
2015-02-13 17:30:36 +01:00
|
|
|
fields['id'] = self._win_id
|
2015-01-28 08:40:16 +01:00
|
|
|
fmt = config.get('ui', 'window-title-format')
|
|
|
|
self.window().setWindowTitle(fmt.format(**fields))
|
2014-09-25 17:39:56 +02:00
|
|
|
|
2014-04-22 10:34:43 +02:00
|
|
|
def _connect_tab_signals(self, tab):
|
|
|
|
"""Set up the needed signals for tab."""
|
2014-07-16 11:07:05 +02:00
|
|
|
page = tab.page()
|
|
|
|
frame = page.mainFrame()
|
2014-04-22 10:34:43 +02:00
|
|
|
# filtered signals
|
2014-07-16 11:07:05 +02:00
|
|
|
tab.linkHovered.connect(
|
|
|
|
self._filter.create(self.cur_link_hovered, tab))
|
|
|
|
tab.loadProgress.connect(
|
|
|
|
self._filter.create(self.cur_progress, tab))
|
|
|
|
frame.loadFinished.connect(
|
|
|
|
self._filter.create(self.cur_load_finished, tab))
|
|
|
|
frame.loadStarted.connect(
|
|
|
|
self._filter.create(self.cur_load_started, tab))
|
2014-04-22 10:34:43 +02:00
|
|
|
tab.statusBarMessage.connect(
|
2014-07-16 11:07:05 +02:00
|
|
|
self._filter.create(self.cur_statusbar_message, tab))
|
2014-05-19 09:50:56 +02:00
|
|
|
tab.scroll_pos_changed.connect(
|
2014-07-16 11:07:05 +02:00
|
|
|
self._filter.create(self.cur_scroll_perc_changed, tab))
|
2014-05-15 22:31:01 +02:00
|
|
|
tab.url_text_changed.connect(
|
2014-07-16 11:07:05 +02:00
|
|
|
self._filter.create(self.cur_url_text_changed, tab))
|
2014-05-16 07:11:39 +02:00
|
|
|
tab.load_status_changed.connect(
|
2014-07-16 11:07:05 +02:00
|
|
|
self._filter.create(self.cur_load_status_changed, tab))
|
2014-08-26 19:10:14 +02:00
|
|
|
tab.url_text_changed.connect(
|
|
|
|
functools.partial(self.on_url_text_changed, tab))
|
2014-04-22 10:34:43 +02:00
|
|
|
# misc
|
2014-08-26 19:10:14 +02:00
|
|
|
tab.titleChanged.connect(
|
|
|
|
functools.partial(self.on_title_changed, tab))
|
|
|
|
tab.iconChanged.connect(
|
|
|
|
functools.partial(self.on_icon_changed, tab))
|
|
|
|
tab.loadProgress.connect(
|
|
|
|
functools.partial(self.on_load_progress, tab))
|
|
|
|
frame.loadFinished.connect(
|
|
|
|
functools.partial(self.on_load_finished, tab))
|
|
|
|
frame.loadStarted.connect(
|
|
|
|
functools.partial(self.on_load_started, tab))
|
2014-07-16 11:07:05 +02:00
|
|
|
page.windowCloseRequested.connect(
|
2014-08-26 19:10:14 +02:00
|
|
|
functools.partial(self.on_window_close_requested, tab))
|
2014-04-22 10:34:43 +02:00
|
|
|
|
2014-07-28 01:40:25 +02:00
|
|
|
def current_url(self):
|
|
|
|
"""Get the URL of the current tab.
|
|
|
|
|
|
|
|
Intended to be used from command handlers.
|
|
|
|
|
|
|
|
Return:
|
|
|
|
The current URL as QUrl.
|
|
|
|
"""
|
2014-09-03 10:47:27 +02:00
|
|
|
widget = self.currentWidget()
|
|
|
|
if widget is None:
|
|
|
|
url = QUrl()
|
|
|
|
else:
|
|
|
|
url = widget.cur_url
|
2015-01-04 20:13:25 +01:00
|
|
|
# It's possible for url to be invalid, but the caller will handle that.
|
|
|
|
qtutils.ensure_valid(url)
|
2014-07-28 01:40:25 +02:00
|
|
|
return url
|
|
|
|
|
2014-05-17 23:15:42 +02:00
|
|
|
def shutdown(self):
|
2014-10-07 23:08:37 +02:00
|
|
|
"""Try to shut down all tabs cleanly."""
|
2014-05-17 23:15:42 +02:00
|
|
|
try:
|
|
|
|
self.currentChanged.disconnect()
|
2014-09-16 08:20:19 +02:00
|
|
|
except TypeError:
|
2015-03-05 21:46:56 +01:00
|
|
|
log.destroy.debug("Error while shutting down tabs")
|
2014-09-02 21:54:07 +02:00
|
|
|
for tab in self.widgets():
|
2014-07-31 20:40:21 +02:00
|
|
|
self._remove_tab(tab)
|
2014-05-17 23:15:42 +02:00
|
|
|
|
2014-07-31 19:31:24 +02:00
|
|
|
def close_tab(self, tab):
|
2014-10-07 20:56:27 +02:00
|
|
|
"""Close a tab.
|
2014-05-09 11:20:17 +02:00
|
|
|
|
|
|
|
Args:
|
2014-07-31 19:31:24 +02:00
|
|
|
tab: The QWebView to be closed.
|
2014-05-09 11:20:17 +02:00
|
|
|
"""
|
2014-08-06 08:10:32 +02:00
|
|
|
last_close = config.get('tabs', 'last-close')
|
2014-05-15 00:02:40 +02:00
|
|
|
if self.count() > 1:
|
2014-07-31 19:31:24 +02:00
|
|
|
self._remove_tab(tab)
|
2014-10-07 17:09:24 +02:00
|
|
|
elif last_close == 'close':
|
2014-07-31 19:31:24 +02:00
|
|
|
self._remove_tab(tab)
|
2014-10-07 17:09:24 +02:00
|
|
|
self.close_window.emit()
|
2014-05-15 00:02:40 +02:00
|
|
|
elif last_close == 'blank':
|
2014-10-27 20:46:35 +01:00
|
|
|
tab.openurl(QUrl('about:blank'))
|
2014-05-15 00:02:40 +02:00
|
|
|
|
2014-07-31 19:31:24 +02:00
|
|
|
def _remove_tab(self, tab):
|
|
|
|
"""Remove a tab from the tab list and delete it properly.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
tab: The QWebView to be closed.
|
|
|
|
"""
|
|
|
|
idx = self.indexOf(tab)
|
|
|
|
if idx == -1:
|
|
|
|
raise ValueError("tab {} is not contained in TabbedWidget!".format(
|
|
|
|
tab))
|
|
|
|
if tab is self._now_focused:
|
|
|
|
self._now_focused = None
|
2014-09-29 07:17:01 +02:00
|
|
|
if tab is objreg.get('last-focused-tab', None, scope='window',
|
|
|
|
window=self._win_id):
|
|
|
|
objreg.delete('last-focused-tab', scope='window',
|
|
|
|
window=self._win_id)
|
2014-10-03 16:58:30 +02:00
|
|
|
if tab.cur_url.isValid():
|
2014-09-27 22:56:50 +02:00
|
|
|
history_data = qtutils.serialize(tab.history())
|
|
|
|
entry = UndoEntry(tab.cur_url, history_data)
|
|
|
|
self._undo_stack.append(entry)
|
2014-10-08 21:22:43 +02:00
|
|
|
elif tab.cur_url.isEmpty():
|
|
|
|
# There are some good reasons why an URL could be empty
|
|
|
|
# (target="_blank" with a download, see [1]), so we silently ignore
|
|
|
|
# this.
|
|
|
|
# [1] https://github.com/The-Compiler/qutebrowser/issues/163
|
|
|
|
pass
|
2014-10-03 16:58:30 +02:00
|
|
|
else:
|
2014-10-08 21:22:43 +02:00
|
|
|
# We display a warnings for URLs which are not empty but invalid -
|
|
|
|
# but we don't return here because we want the tab to close either
|
|
|
|
# way.
|
2014-10-06 22:22:59 +02:00
|
|
|
urlutils.invalid_url_error(self._win_id, tab.cur_url, "saving tab")
|
2014-08-01 23:23:31 +02:00
|
|
|
tab.shutdown()
|
2014-07-31 19:31:24 +02:00
|
|
|
self.removeTab(idx)
|
2014-07-31 19:34:30 +02:00
|
|
|
tab.deleteLater()
|
2014-07-31 19:31:24 +02:00
|
|
|
|
2014-09-27 22:56:50 +02:00
|
|
|
def undo(self):
|
|
|
|
"""Undo removing of a tab."""
|
|
|
|
url, history_data = self._undo_stack.pop()
|
2014-12-21 13:06:24 +01:00
|
|
|
newtab = self.tabopen(url, background=False)
|
2014-09-27 22:56:50 +02:00
|
|
|
qtutils.deserialize(history_data, newtab.history())
|
|
|
|
|
2014-05-17 22:38:07 +02:00
|
|
|
@pyqtSlot('QUrl', bool)
|
|
|
|
def openurl(self, url, newtab):
|
|
|
|
"""Open a URL, used as a slot.
|
2014-05-09 15:52:03 +02:00
|
|
|
|
|
|
|
Args:
|
2014-06-20 16:33:01 +02:00
|
|
|
url: The URL to open as QUrl.
|
2014-05-17 22:38:07 +02:00
|
|
|
newtab: True to open URL in a new tab, False otherwise.
|
2014-05-09 15:52:03 +02:00
|
|
|
"""
|
2014-08-26 19:23:06 +02:00
|
|
|
qtutils.ensure_valid(url)
|
2014-05-17 22:38:07 +02:00
|
|
|
if newtab:
|
|
|
|
self.tabopen(url, background=False)
|
2014-05-09 15:52:03 +02:00
|
|
|
else:
|
2014-05-17 22:38:07 +02:00
|
|
|
self.currentWidget().openurl(url)
|
2014-05-09 15:52:03 +02:00
|
|
|
|
2014-05-17 22:38:07 +02:00
|
|
|
@pyqtSlot(int)
|
|
|
|
def on_tab_close_requested(self, idx):
|
|
|
|
"""Close a tab via an index."""
|
2014-08-01 14:27:57 +02:00
|
|
|
tab = self.widget(idx)
|
|
|
|
if tab is None:
|
|
|
|
log.webview.debug("Got invalid tab {} for index {}!".format(
|
|
|
|
tab, idx))
|
|
|
|
return
|
|
|
|
self.close_tab(tab)
|
2014-05-09 15:52:03 +02:00
|
|
|
|
2014-08-26 19:10:14 +02:00
|
|
|
@pyqtSlot(webview.WebView)
|
2014-06-10 16:50:14 +02:00
|
|
|
def on_window_close_requested(self, widget):
|
|
|
|
"""Close a tab with a widget given."""
|
|
|
|
self.close_tab(widget)
|
|
|
|
|
2014-06-20 16:33:01 +02:00
|
|
|
@pyqtSlot('QUrl', bool)
|
2014-08-06 07:58:07 +02:00
|
|
|
def tabopen(self, url=None, background=None, explicit=False):
|
2014-05-15 16:27:34 +02:00
|
|
|
"""Open a new tab with a given URL.
|
2014-05-02 10:11:47 +02:00
|
|
|
|
2014-05-16 23:01:40 +02:00
|
|
|
Inner logic for open-tab and open-tab-bg.
|
2014-05-02 10:11:47 +02:00
|
|
|
Also connect all the signals we need to _filter_signals.
|
|
|
|
|
|
|
|
Args:
|
2014-06-20 16:33:01 +02:00
|
|
|
url: The URL to open as QUrl or None for an empty tab.
|
2014-05-08 20:36:05 +02:00
|
|
|
background: Whether to open the tab in the background.
|
|
|
|
if None, the background-tabs setting decides.
|
2014-08-06 07:58:07 +02:00
|
|
|
explicit: Whether the tab was opened explicitely.
|
|
|
|
If this is set, the new position might be different. With
|
|
|
|
the default settings we handle it like Chromium does:
|
|
|
|
- Tabs from clicked links etc. are to the right of
|
|
|
|
the current.
|
|
|
|
- Explicitely opened tabs are at the very right.
|
2014-05-08 20:36:05 +02:00
|
|
|
|
|
|
|
Return:
|
|
|
|
The opened WebView instance.
|
2014-05-02 10:11:47 +02:00
|
|
|
"""
|
2014-06-23 07:48:45 +02:00
|
|
|
if url is not None:
|
2014-08-26 19:23:06 +02:00
|
|
|
qtutils.ensure_valid(url)
|
2014-05-23 16:11:55 +02:00
|
|
|
log.webview.debug("Creating new tab with URL {}".format(url))
|
2014-11-21 06:22:52 +01:00
|
|
|
if config.get('tabs', 'tabs-are-windows') and self.count() > 0:
|
2014-12-13 17:28:50 +01:00
|
|
|
from qutebrowser.mainwindow import mainwindow
|
2015-02-16 22:56:12 +01:00
|
|
|
window = mainwindow.MainWindow()
|
|
|
|
window.show()
|
2014-11-21 06:22:52 +01:00
|
|
|
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
2015-02-16 22:56:12 +01:00
|
|
|
window=window.win_id)
|
2014-11-21 06:22:52 +01:00
|
|
|
return tabbed_browser.tabopen(url, background, explicit)
|
2014-09-28 22:13:14 +02:00
|
|
|
tab = webview.WebView(self._win_id, self)
|
2014-05-02 10:11:47 +02:00
|
|
|
self._connect_tab_signals(tab)
|
2014-11-23 21:26:59 +01:00
|
|
|
idx = self._get_new_tab_idx(explicit)
|
|
|
|
self.insertTab(idx, tab, "")
|
|
|
|
if url is not None:
|
|
|
|
tab.openurl(url)
|
|
|
|
if background is None:
|
|
|
|
background = config.get('tabs', 'background-tabs')
|
|
|
|
if not background:
|
|
|
|
self.setCurrentWidget(tab)
|
|
|
|
tab.show()
|
|
|
|
return tab
|
|
|
|
|
|
|
|
def _get_new_tab_idx(self, explicit):
|
|
|
|
"""Get the index of a tab to insert.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
explicit: Whether the tab was opened explicitely.
|
|
|
|
|
|
|
|
Return:
|
|
|
|
The index of the new tab.
|
|
|
|
"""
|
2014-08-06 07:58:07 +02:00
|
|
|
if explicit:
|
2014-08-06 08:10:32 +02:00
|
|
|
pos = config.get('tabs', 'new-tab-position-explicit')
|
2014-08-06 07:58:07 +02:00
|
|
|
else:
|
2014-08-06 08:10:32 +02:00
|
|
|
pos = config.get('tabs', 'new-tab-position')
|
2014-08-06 07:58:07 +02:00
|
|
|
if pos == 'left':
|
|
|
|
idx = self._tab_insert_idx_left
|
2014-08-06 07:47:59 +02:00
|
|
|
# On first sight, we'd think we have to decrement
|
2014-08-06 07:58:07 +02:00
|
|
|
# self._tab_insert_idx_left here, as we want the next tab to be
|
|
|
|
# *before* the one we just opened. However, since we opened a tab
|
|
|
|
# *to the left* of the currently focused tab, indices will shift by
|
|
|
|
# 1 automatically.
|
|
|
|
elif pos == 'right':
|
|
|
|
idx = self._tab_insert_idx_right
|
|
|
|
self._tab_insert_idx_right += 1
|
|
|
|
elif pos == 'first':
|
2014-08-06 07:47:59 +02:00
|
|
|
idx = 0
|
2014-08-06 07:58:07 +02:00
|
|
|
elif pos == 'last':
|
2014-08-06 07:47:59 +02:00
|
|
|
idx = -1
|
|
|
|
else:
|
2014-08-06 07:58:07 +02:00
|
|
|
raise ValueError("Invalid new-tab-position '{}'.".format(pos))
|
2014-08-06 07:47:59 +02:00
|
|
|
log.webview.debug("new-tab-position {} -> opening new tab at {}, "
|
2014-08-06 07:58:07 +02:00
|
|
|
"next left: {} / right: {}".format(
|
|
|
|
pos, idx, self._tab_insert_idx_left,
|
|
|
|
self._tab_insert_idx_right))
|
2014-11-23 21:26:59 +01:00
|
|
|
return idx
|
2014-05-02 10:11:47 +02:00
|
|
|
|
2014-05-17 22:38:07 +02:00
|
|
|
@pyqtSlot(str, int)
|
|
|
|
def search(self, text, flags):
|
|
|
|
"""Search for text in the current page.
|
2014-02-21 22:00:41 +01:00
|
|
|
|
|
|
|
Args:
|
2014-05-17 22:38:07 +02:00
|
|
|
text: The text to search for.
|
|
|
|
flags: The QWebPage::FindFlags.
|
2014-02-21 22:00:41 +01:00
|
|
|
"""
|
2014-08-12 17:18:13 +02:00
|
|
|
log.webview.debug("Searching with text '{}' and flags "
|
|
|
|
"0x{:04x}.".format(text, int(flags)))
|
|
|
|
widget = self.currentWidget()
|
|
|
|
old_scroll_pos = widget.scroll_pos
|
|
|
|
found = widget.findText(text, flags)
|
|
|
|
if not found and not flags & QWebPage.HighlightAllOccurrences and text:
|
2014-09-28 22:13:14 +02:00
|
|
|
message.error(self._win_id, "Text '{}' not found on "
|
|
|
|
"page!".format(text), immediately=True)
|
2014-08-12 17:18:13 +02:00
|
|
|
else:
|
|
|
|
backward = int(flags) & QWebPage.FindBackward
|
2014-08-12 18:30:15 +02:00
|
|
|
|
2014-08-12 17:18:13 +02:00
|
|
|
def check_scroll_pos():
|
|
|
|
"""Check if the scroll position got smaller and show info."""
|
|
|
|
if not backward and widget.scroll_pos < old_scroll_pos:
|
2014-09-28 22:13:14 +02:00
|
|
|
message.info(self._win_id, "Search hit BOTTOM, continuing "
|
|
|
|
"at TOP", immediately=True)
|
2014-08-12 17:18:13 +02:00
|
|
|
elif backward and widget.scroll_pos > old_scroll_pos:
|
2014-09-28 22:13:14 +02:00
|
|
|
message.info(self._win_id, "Search hit TOP, continuing at "
|
|
|
|
"BOTTOM", immediately=True)
|
2014-08-12 17:18:13 +02:00
|
|
|
# We first want QWebPage to refresh.
|
|
|
|
QTimer.singleShot(0, check_scroll_pos)
|
2014-02-21 22:00:41 +01:00
|
|
|
|
2014-10-18 19:50:10 +02:00
|
|
|
@config.change_filter('tabs', 'show-favicons')
|
2014-09-28 11:27:52 +02:00
|
|
|
def update_favicons(self):
|
|
|
|
"""Update favicons when config was changed."""
|
|
|
|
show = config.get('tabs', 'show-favicons')
|
|
|
|
for i, tab in enumerate(self.widgets()):
|
|
|
|
if show:
|
|
|
|
self.setTabIcon(i, tab.icon())
|
|
|
|
else:
|
|
|
|
self.setTabIcon(i, QIcon())
|
2014-04-22 10:45:07 +02:00
|
|
|
|
2014-05-04 01:28:34 +02:00
|
|
|
@pyqtSlot()
|
|
|
|
def on_load_started(self, tab):
|
2015-01-25 23:25:08 +01:00
|
|
|
"""Clear icon and update title when a tab started loading.
|
2014-05-04 01:28:34 +02:00
|
|
|
|
|
|
|
Args:
|
|
|
|
tab: The tab where the signal belongs to.
|
|
|
|
"""
|
2014-07-31 20:40:21 +02:00
|
|
|
try:
|
2015-03-03 23:21:23 +01:00
|
|
|
idx = self._tab_index(tab)
|
|
|
|
except TabDeletedError:
|
2014-06-24 07:01:15 +02:00
|
|
|
# We can get signals for tabs we already deleted...
|
|
|
|
return
|
2015-02-13 17:30:36 +01:00
|
|
|
self.update_tab_title(idx)
|
2015-01-26 07:18:07 +01:00
|
|
|
if tab.keep_icon:
|
|
|
|
tab.keep_icon = False
|
|
|
|
else:
|
|
|
|
self.setTabIcon(idx, QIcon())
|
2015-01-25 23:25:08 +01:00
|
|
|
if idx == self.currentIndex():
|
2015-01-28 08:40:16 +01:00
|
|
|
self.update_window_title()
|
2014-05-04 01:28:34 +02:00
|
|
|
|
2014-07-04 17:32:17 +02:00
|
|
|
@pyqtSlot()
|
|
|
|
def on_cur_load_started(self):
|
|
|
|
"""Leave insert/hint mode when loading started."""
|
2014-09-28 22:13:14 +02:00
|
|
|
modeman.maybe_leave(self._win_id, usertypes.KeyMode.insert,
|
|
|
|
'load started')
|
|
|
|
modeman.maybe_leave(self._win_id, usertypes.KeyMode.hint,
|
|
|
|
'load started')
|
2014-07-04 17:32:17 +02:00
|
|
|
|
2014-08-26 19:10:14 +02:00
|
|
|
@pyqtSlot(webview.WebView, str)
|
2014-06-16 22:49:22 +02:00
|
|
|
def on_title_changed(self, tab, text):
|
2014-04-22 10:45:07 +02:00
|
|
|
"""Set the title of a tab.
|
|
|
|
|
|
|
|
Slot for the titleChanged signal of any tab.
|
|
|
|
|
|
|
|
Args:
|
2014-06-16 22:49:22 +02:00
|
|
|
tab: The WebView where the title was changed.
|
2014-04-22 10:45:07 +02:00
|
|
|
text: The text to set.
|
|
|
|
"""
|
2014-07-31 20:40:21 +02:00
|
|
|
if not text:
|
2014-09-25 17:36:40 +02:00
|
|
|
log.webview.debug("Ignoring title change to '{}'.".format(text))
|
2014-07-31 20:40:21 +02:00
|
|
|
return
|
|
|
|
try:
|
2015-03-03 23:21:23 +01:00
|
|
|
idx = self._tab_index(tab)
|
|
|
|
except TabDeletedError:
|
2014-07-31 20:40:21 +02:00
|
|
|
# We can get signals for tabs we already deleted...
|
|
|
|
return
|
2014-09-25 17:36:40 +02:00
|
|
|
log.webview.debug("Changing title for idx {} to '{}'".format(
|
|
|
|
idx, text))
|
2015-02-13 17:30:36 +01:00
|
|
|
self.set_page_title(idx, text)
|
2014-07-31 20:40:21 +02:00
|
|
|
if idx == self.currentIndex():
|
2015-01-28 08:40:16 +01:00
|
|
|
self.update_window_title()
|
2014-04-22 10:45:07 +02:00
|
|
|
|
2014-08-26 19:10:14 +02:00
|
|
|
@pyqtSlot(webview.WebView, str)
|
2014-06-16 22:49:22 +02:00
|
|
|
def on_url_text_changed(self, tab, url):
|
|
|
|
"""Set the new URL as title if there's no title yet.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
tab: The WebView where the title was changed.
|
|
|
|
url: The new URL.
|
|
|
|
"""
|
2014-07-31 20:40:21 +02:00
|
|
|
try:
|
2015-03-03 23:21:23 +01:00
|
|
|
idx = self._tab_index(tab)
|
|
|
|
except TabDeletedError:
|
2014-07-31 20:40:21 +02:00
|
|
|
# We can get signals for tabs we already deleted...
|
|
|
|
return
|
2015-02-13 17:30:36 +01:00
|
|
|
if not self.page_title(idx):
|
|
|
|
self.set_page_title(idx, url)
|
2014-05-08 21:04:27 +02:00
|
|
|
|
2014-08-26 19:10:14 +02:00
|
|
|
@pyqtSlot(webview.WebView)
|
2014-06-16 22:49:22 +02:00
|
|
|
def on_icon_changed(self, tab):
|
2014-05-04 01:28:34 +02:00
|
|
|
"""Set the icon of a tab.
|
|
|
|
|
|
|
|
Slot for the iconChanged signal of any tab.
|
2014-06-16 22:49:22 +02:00
|
|
|
|
|
|
|
Args:
|
|
|
|
tab: The WebView where the title was changed.
|
2014-05-04 01:28:34 +02:00
|
|
|
"""
|
2014-08-06 08:10:32 +02:00
|
|
|
if not config.get('tabs', 'show-favicons'):
|
2014-05-12 23:03:55 +02:00
|
|
|
return
|
2014-07-31 20:40:21 +02:00
|
|
|
try:
|
2015-03-03 23:21:23 +01:00
|
|
|
idx = self._tab_index(tab)
|
|
|
|
except TabDeletedError:
|
2014-07-31 20:40:21 +02:00
|
|
|
# We can get signals for tabs we already deleted...
|
|
|
|
return
|
2014-06-22 23:33:32 +02:00
|
|
|
self.setTabIcon(idx, tab.icon())
|
2014-05-04 01:28:34 +02:00
|
|
|
|
2014-08-26 19:10:14 +02:00
|
|
|
@pyqtSlot(usertypes.KeyMode)
|
2014-04-25 07:50:21 +02:00
|
|
|
def on_mode_left(self, mode):
|
2014-06-19 11:50:31 +02:00
|
|
|
"""Give focus to current tab if command mode was left."""
|
2014-12-28 00:41:50 +01:00
|
|
|
if mode in (usertypes.KeyMode.command, usertypes.KeyMode.prompt,
|
|
|
|
usertypes.KeyMode.yesno):
|
2014-10-14 17:50:50 +02:00
|
|
|
widget = self.currentWidget()
|
2014-12-28 00:41:50 +01:00
|
|
|
log.modes.debug("Left status-input mode, focusing {!r}".format(
|
|
|
|
widget))
|
2014-10-14 17:50:50 +02:00
|
|
|
if widget is None:
|
|
|
|
return
|
|
|
|
widget.setFocus()
|
2014-04-25 07:50:21 +02:00
|
|
|
|
2014-05-09 11:57:58 +02:00
|
|
|
@pyqtSlot(int)
|
|
|
|
def on_current_changed(self, idx):
|
2014-09-24 06:41:51 +02:00
|
|
|
"""Set last-focused-tab and leave hinting mode when focus changed."""
|
2014-10-05 19:47:12 +02:00
|
|
|
if idx == -1:
|
|
|
|
# closing the last tab (before quitting)
|
|
|
|
return
|
2014-05-09 11:57:58 +02:00
|
|
|
tab = self.widget(idx)
|
2014-10-28 07:44:42 +01:00
|
|
|
log.modes.debug("Current tab changed, focusing {!r}".format(tab))
|
2014-07-16 16:34:58 +02:00
|
|
|
tab.setFocus()
|
2014-11-11 21:42:53 +01:00
|
|
|
for mode in (usertypes.KeyMode.hint, usertypes.KeyMode.insert):
|
|
|
|
modeman.maybe_leave(self._win_id, mode, 'tab changed')
|
2014-09-24 06:41:51 +02:00
|
|
|
if self._now_focused is not None:
|
2014-09-29 07:17:01 +02:00
|
|
|
objreg.register('last-focused-tab', self._now_focused, update=True,
|
|
|
|
scope='window', window=self._win_id)
|
2014-07-29 22:44:14 +02:00
|
|
|
self._now_focused = tab
|
2014-06-16 22:49:22 +02:00
|
|
|
self.current_tab_changed.emit(tab)
|
2015-02-13 17:30:36 +01:00
|
|
|
QTimer.singleShot(0, self.update_window_title)
|
2014-08-06 07:58:07 +02:00
|
|
|
self._tab_insert_idx_left = self.currentIndex()
|
|
|
|
self._tab_insert_idx_right = self.currentIndex() + 1
|
2014-05-09 11:57:58 +02:00
|
|
|
|
2014-10-28 07:44:42 +01:00
|
|
|
@pyqtSlot()
|
|
|
|
def on_cmd_return_pressed(self):
|
|
|
|
"""Set focus when the commandline closes."""
|
|
|
|
log.modes.debug("Commandline closed, focusing {!r}".format(self))
|
|
|
|
|
2014-07-16 13:51:16 +02:00
|
|
|
def on_load_progress(self, tab, perc):
|
|
|
|
"""Adjust tab indicator on load progress."""
|
2014-07-31 20:40:21 +02:00
|
|
|
try:
|
2015-03-03 23:21:23 +01:00
|
|
|
idx = self._tab_index(tab)
|
|
|
|
except TabDeletedError:
|
2014-07-31 20:40:21 +02:00
|
|
|
# We can get signals for tabs we already deleted...
|
|
|
|
return
|
2014-11-26 21:16:27 +01:00
|
|
|
start = config.get('colors', 'tabs.indicator.start')
|
|
|
|
stop = config.get('colors', 'tabs.indicator.stop')
|
|
|
|
system = config.get('colors', 'tabs.indicator.system')
|
2014-07-16 13:51:16 +02:00
|
|
|
color = utils.interpolate_color(start, stop, perc, system)
|
2015-02-13 17:30:36 +01:00
|
|
|
self.set_tab_indicator_color(idx, color)
|
|
|
|
self.update_tab_title(idx)
|
2015-01-25 23:25:08 +01:00
|
|
|
if idx == self.currentIndex():
|
2015-01-28 08:40:16 +01:00
|
|
|
self.update_window_title()
|
2014-07-16 13:51:16 +02:00
|
|
|
|
2014-12-10 16:38:09 +01:00
|
|
|
def on_load_finished(self, tab):
|
|
|
|
"""Adjust tab indicator when loading finished.
|
|
|
|
|
|
|
|
We don't take loadFinished's ok argument here as it always seems to be
|
|
|
|
true when the QWebPage has an ErrorPageExtension implemented.
|
|
|
|
See https://github.com/The-Compiler/qutebrowser/issues/84
|
|
|
|
"""
|
2014-07-31 20:40:21 +02:00
|
|
|
try:
|
2015-03-03 23:21:23 +01:00
|
|
|
idx = self._tab_index(tab)
|
|
|
|
except TabDeletedError:
|
2014-07-31 20:40:21 +02:00
|
|
|
# We can get signals for tabs we already deleted...
|
|
|
|
return
|
2014-12-10 16:38:09 +01:00
|
|
|
if tab.page().error_occured:
|
|
|
|
color = config.get('colors', 'tabs.indicator.error')
|
|
|
|
else:
|
2014-11-26 21:16:27 +01:00
|
|
|
start = config.get('colors', 'tabs.indicator.start')
|
|
|
|
stop = config.get('colors', 'tabs.indicator.stop')
|
|
|
|
system = config.get('colors', 'tabs.indicator.system')
|
2014-07-16 13:51:16 +02:00
|
|
|
color = utils.interpolate_color(start, stop, 100, system)
|
2015-02-13 17:30:36 +01:00
|
|
|
self.set_tab_indicator_color(idx, color)
|
|
|
|
self.update_tab_title(idx)
|
2015-01-25 23:25:08 +01:00
|
|
|
if idx == self.currentIndex():
|
2015-01-28 08:40:16 +01:00
|
|
|
self.update_window_title()
|
2014-07-16 13:51:16 +02:00
|
|
|
|
2014-02-21 22:00:41 +01:00
|
|
|
def resizeEvent(self, e):
|
|
|
|
"""Extend resizeEvent of QWidget to emit a resized signal afterwards.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
e: The QResizeEvent
|
|
|
|
"""
|
|
|
|
super().resizeEvent(e)
|
|
|
|
self.resized.emit(self.geometry())
|