qutebrowser/qutebrowser/widgets/tabbedbrowser.py

414 lines
15 KiB
Python
Raw Normal View History

2014-06-19 09:04:37 +02:00
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
2014-02-06 14:01:23 +01:00
# Copyright 2014 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# 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-04-17 09:44:26 +02:00
from functools import partial
2014-05-17 22:38:07 +02:00
from PyQt5.QtWidgets import QSizePolicy
2014-05-04 01:28:34 +02:00
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSize
2014-02-23 18:07:17 +01:00
import qutebrowser.config.config as config
2014-03-03 06:09:23 +01:00
import qutebrowser.commands.utils as cmdutils
2014-05-22 17:50:10 +02:00
import qutebrowser.keyinput.modeman as modeman
2014-05-23 16:11:55 +02:00
import qutebrowser.utils.log as log
2014-06-03 14:57:57 +02:00
from qutebrowser.widgets.tabwidget import TabWidget, EmptyTabIcon
2014-04-25 12:24:26 +02:00
from qutebrowser.widgets.webview import WebView
2014-04-17 09:44:26 +02:00
from qutebrowser.browser.signalfilter import SignalFilter
from qutebrowser.browser.commands import CommandDispatcher
2014-06-21 16:42:58 +02:00
from qutebrowser.utils.misc import qt_ensure_valid
2013-12-15 21:40:15 +01:00
2014-01-28 23:04:02 +01:00
2013-12-15 21:40:15 +01:00
class TabbedBrowser(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.
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:
_tabs: A list of open tabs.
2014-04-17 09:44:26 +02:00
_filter: A SignalFilter instance.
2014-05-17 22:38:07 +02:00
url_stack: Stack of URLs of closed tabs.
cmd: A TabCommandDispatcher instance.
2014-05-09 11:57:58 +02:00
last_focused: The tab which was focused last.
now_focused: The tab which is focused now.
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)
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 %.
cur_load_status_changed: Loading status of current tab changed.
hint_strings_updated: Hint strings were updated.
arg: A list of hint strings.
2014-02-18 16:38:13 +01:00
shutdown_complete: The shuttdown is completed.
quit: The last tab was closed, quit application.
resized: Emitted when the browser window has resized, so the completion
widget can adjust its size to it.
arg: The new size.
start_download: Emitted when any tab wants to start downloading
something.
current_tab_changed: The current tab changed to the emitted WebView.
2014-01-29 15:30:19 +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)
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)
cur_load_status_changed = pyqtSignal(str)
start_download = pyqtSignal('QNetworkReply*')
hint_strings_updated = pyqtSignal(list)
2014-02-18 16:38:13 +01:00
shutdown_complete = pyqtSignal()
quit = pyqtSignal()
resized = pyqtSignal('QRect')
2014-05-21 19:53:58 +02:00
got_cmd = pyqtSignal(str)
current_tab_changed = pyqtSignal(WebView)
2013-12-15 21:40:15 +01:00
2014-02-12 20:51:50 +01:00
def __init__(self, parent=None):
2013-12-15 21:40:15 +01:00
super().__init__(parent)
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-01-30 22:29:01 +01:00
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self._tabs = []
2014-05-17 22:38:07 +02:00
self.url_stack = []
2014-04-17 09:44:26 +02:00
self._filter = SignalFilter(self)
self.cmd = CommandDispatcher(self)
2014-05-09 11:57:58 +02:00
self.last_focused = None
self.now_focused = None
2014-05-04 01:28:34 +02:00
# FIXME adjust this to font size
self.setIconSize(QSize(12, 12))
2013-12-15 21:40:15 +01:00
def __repr__(self):
return '<{} with {} tabs>'.format(self.__class__.__name__,
self.count())
2014-05-13 21:25:16 +02:00
@property
def widgets(self):
"""Get a list of open tab widgets.
We don't implement this as generator so we can delete tabs while
iterating over the list."""
w = []
for i in range(self.count()):
w.append(self.widget(i))
return w
2014-02-18 18:32:07 +01:00
def _cb_tab_shutdown(self, tab):
2014-02-19 10:58:32 +01:00
"""Called after a tab has been shut down completely.
Args:
tab: The tab object which has been shut down.
Emit:
shutdown_complete: When the tab shutdown is done completely.
"""
2014-02-18 18:32:07 +01:00
try:
self._tabs.remove(tab)
2014-06-21 22:40:31 +02:00
except ValueError:
log.destroy.exception("tab {} could not be removed")
2014-05-23 16:11:55 +02:00
log.destroy.debug("Tabs after removing: {}".format(self._tabs))
2014-02-18 18:32:07 +01:00
if not self._tabs: # all tabs shut down
2014-05-23 16:11:55 +02:00
log.destroy.debug("Tab shutdown complete.")
2014-02-18 18:32:07 +01:00
self.shutdown_complete.emit()
2014-04-22 10:34:43 +02:00
def _connect_tab_signals(self, tab):
"""Set up the needed signals for tab."""
# filtered signals
tab.linkHovered.connect(self._filter.create(self.cur_link_hovered))
tab.loadProgress.connect(self._filter.create(self.cur_progress))
tab.loadFinished.connect(self._filter.create(self.cur_load_finished))
tab.loadStarted.connect(self._filter.create(self.cur_load_started))
tab.statusBarMessage.connect(
self._filter.create(self.cur_statusbar_message))
tab.scroll_pos_changed.connect(
2014-04-22 10:34:43 +02:00
self._filter.create(self.cur_scroll_perc_changed))
tab.url_text_changed.connect(
self._filter.create(self.cur_url_text_changed))
tab.url_text_changed.connect(partial(self.on_url_text_changed, tab))
tab.load_status_changed.connect(
self._filter.create(self.cur_load_status_changed))
# hintmanager
tab.hintmanager.hint_strings_updated.connect(self.hint_strings_updated)
tab.hintmanager.openurl.connect(self.openurl)
# downloads
tab.page().unsupportedContent.connect(self.start_download)
tab.page().start_download.connect(self.start_download)
2014-04-22 10:34:43 +02:00
# misc
tab.titleChanged.connect(partial(self.on_title_changed, tab))
tab.iconChanged.connect(partial(self.on_icon_changed, tab))
tab.page().mainFrame().loadStarted.connect(partial(
self.on_load_started, tab))
tab.page().windowCloseRequested.connect(partial(
self.on_window_close_requested, tab))
2014-04-22 10:34:43 +02:00
2014-05-17 23:15:42 +02:00
def cntwidget(self, count=None):
"""Return a widget based on a count/idx.
Args:
count: The tab index, or None.
Return:
The current widget if count is None.
The widget with the given tab ID if count is given.
None if no widget was found.
"""
if count is None:
return self.currentWidget()
elif 1 <= count <= self.count():
cmdutils.check_overflow(count + 1, 'int')
return self.widget(count - 1)
else:
return None
def shutdown(self):
"""Try to shut down all tabs cleanly.
Emit:
shutdown_complete if the shutdown completed successfully.
"""
try:
self.currentChanged.disconnect()
2014-06-20 23:26:19 +02:00
except TypeError as e:
2014-06-21 22:40:31 +02:00
log.destroy.debug("Error while shutting down tabs: {}: {}".format(
e.__class__.__name__, e))
2014-05-17 23:15:42 +02:00
tabcount = self.count()
if tabcount == 0:
2014-05-23 16:11:55 +02:00
log.destroy.debug("No tabs -> shutdown complete")
2014-05-17 23:15:42 +02:00
self.shutdown_complete.emit()
return
for tab in self.widgets:
tab.shutdown(callback=partial(self._cb_tab_shutdown, tab))
2014-05-17 22:38:07 +02:00
def close_tab(self, tab_or_idx):
2014-05-15 00:02:40 +02:00
"""Close a tab with either index or tab given.
2014-05-09 11:20:17 +02:00
Args:
2014-05-15 00:02:40 +02:00
tab_or_index: Either the QWebView to be closed or an index.
2014-05-09 11:20:17 +02:00
"""
2014-05-15 00:02:40 +02:00
try:
idx = int(tab_or_idx)
except TypeError:
tab = tab_or_idx
idx = self.indexOf(tab_or_idx)
if idx == -1:
raise ValueError("tab {} is not contained in "
"TabbedWidget!".format(tab))
else:
tab = self.widget(idx)
if tab is None:
raise ValueError("invalid index {}!".format(idx))
last_close = config.get('tabbar', 'last-close')
if self.count() > 1:
url = tab.url()
if not url.isEmpty():
2014-06-22 23:32:49 +02:00
qt_ensure_valid(url)
2014-05-17 22:38:07 +02:00
self.url_stack.append(url)
2014-05-15 00:02:40 +02:00
self.removeTab(idx)
tab.shutdown(callback=partial(self._cb_tab_shutdown, tab))
elif last_close == 'quit':
self.quit.emit()
elif last_close == 'blank':
tab.openurl('about:blank')
2014-05-17 22:38:07 +02:00
@pyqtSlot('QUrl', bool)
def openurl(self, url, newtab):
"""Open a URL, used as a slot.
Args:
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-06-21 16:42:58 +02:00
qt_ensure_valid(url)
2014-05-17 22:38:07 +02:00
if newtab:
self.tabopen(url, background=False)
else:
2014-05-17 22:38:07 +02:00
self.currentWidget().openurl(url)
2014-05-17 22:38:07 +02:00
@pyqtSlot(int)
def on_tab_close_requested(self, idx):
"""Close a tab via an index."""
self.close_tab(idx)
@pyqtSlot(WebView)
def on_window_close_requested(self, widget):
"""Close a tab with a widget given."""
self.close_tab(widget)
@pyqtSlot('QUrl', bool)
def tabopen(self, url=None, background=None):
"""Open a new tab with a given URL.
2014-05-16 23:01:40 +02:00
Inner logic for open-tab and open-tab-bg.
Also connect all the signals we need to _filter_signals.
Args:
url: The URL to open as QUrl or None for an empty tab.
background: Whether to open the tab in the background.
if None, the background-tabs setting decides.
Return:
The opened WebView instance.
"""
2014-06-21 16:42:58 +02:00
qt_ensure_valid(url)
2014-05-23 16:11:55 +02:00
log.webview.debug("Creating new tab with URL {}".format(url))
tab = WebView(self)
self._connect_tab_signals(tab)
self._tabs.append(tab)
self.addTab(tab, "")
if url is not None:
tab.openurl(url)
if background is None:
background = config.get('general', 'background-tabs')
if not background:
self.setCurrentWidget(tab)
tab.show()
2014-06-19 11:50:31 +02:00
tab.setFocus()
return tab
2014-05-17 22:38:07 +02:00
@pyqtSlot(str, int)
def search(self, text, flags):
"""Search for text in the current page.
Args:
2014-05-17 22:38:07 +02:00
text: The text to search for.
flags: The QWebPage::FindFlags.
"""
2014-05-17 23:46:06 +02:00
self.currentWidget().findText(text, flags)
2014-05-17 22:38:07 +02:00
@pyqtSlot(str)
def handle_hint_key(self, keystr):
"""Handle a new hint keypress."""
self.currentWidget().hintmanager.handle_partial_key(keystr)
2014-05-03 00:32:43 +02:00
2014-05-17 22:38:07 +02:00
@pyqtSlot(str)
def fire_hint(self, keystr):
"""Fire a completed hint."""
self.currentWidget().hintmanager.fire(keystr)
2014-05-09 11:45:20 +02:00
2014-05-17 22:38:07 +02:00
@pyqtSlot(str)
def filter_hints(self, filterstr):
"""Filter displayed hints."""
self.currentWidget().hintmanager.filter_hints(filterstr)
2014-05-09 11:45:20 +02:00
2014-04-22 10:45:07 +02:00
@pyqtSlot(str, str)
def on_config_changed(self, section, option):
"""Update tab config when config was changed."""
super().on_config_changed(section, option)
for tab in self._tabs:
tab.on_config_changed(section, option)
2014-05-12 23:03:55 +02:00
if (section, option) == ('tabbar', 'show-favicons'):
show = config.get('tabbar', 'show-favicons')
2014-05-13 21:25:16 +02:00
for i, tab in enumerate(self.widgets):
2014-05-12 23:03:55 +02:00
if show:
2014-05-13 21:25:16 +02:00
self.setTabIcon(i, tab.icon())
2014-05-12 23:03:55 +02:00
else:
self.setTabIcon(i, EmptyTabIcon())
2014-04-22 10:45:07 +02:00
2014-05-04 01:28:34 +02:00
@pyqtSlot()
def on_load_started(self, tab):
"""Clear icon when a tab started loading.
2014-05-04 01:28:34 +02:00
Args:
tab: The tab where the signal belongs to.
"""
2014-05-12 21:23:16 +02:00
self.setTabIcon(self.indexOf(tab), EmptyTabIcon())
2014-05-04 01:28:34 +02:00
@pyqtSlot(WebView, str)
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:
tab: The WebView where the title was changed.
2014-04-22 10:45:07 +02:00
text: The text to set.
"""
2014-05-23 16:11:55 +02:00
log.webview.debug("title changed to '{}'".format(text))
2014-04-22 10:45:07 +02:00
if text:
self.setTabText(self.indexOf(tab), text)
2014-04-22 10:45:07 +02:00
else:
2014-05-23 16:11:55 +02:00
log.webview.debug("ignoring title change")
2014-04-22 10:45:07 +02:00
@pyqtSlot(WebView, str)
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.
"""
idx = self.indexOf(tab)
2014-05-08 21:04:27 +02:00
if not self.tabText(idx):
self.setTabText(idx, url)
2014-05-08 21:04:27 +02:00
@pyqtSlot(WebView)
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.
Args:
tab: The WebView where the title was changed.
2014-05-04 01:28:34 +02:00
"""
2014-05-12 23:03:55 +02:00
if not config.get('tabbar', 'show-favicons'):
return
2014-05-04 01:28:34 +02:00
self.setTabIcon(self.indexOf(tab), tab.icon())
2014-04-25 07:50:21 +02:00
@pyqtSlot(str)
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-04-25 07:50:21 +02:00
if mode == "command":
2014-06-19 11:50:31 +02:00
self.currentWidget().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-05-22 17:50:10 +02:00
"""Set last_focused and leave hinting mode when focus changed."""
modeman.maybe_leave('hint', 'tab changed')
2014-05-09 11:57:58 +02:00
tab = self.widget(idx)
self.last_focused = self.now_focused
self.now_focused = tab
self.current_tab_changed.emit(tab)
2014-05-09 11:57:58 +02:00
def resizeEvent(self, e):
"""Extend resizeEvent of QWidget to emit a resized signal afterwards.
Args:
e: The QResizeEvent
Emit:
resize: Always emitted.
"""
super().resizeEvent(e)
self.resized.emit(self.geometry())