qutebrowser/qutebrowser/widgets/_tabbedbrowser.py
2014-05-06 14:08:40 +02:00

412 lines
14 KiB
Python

# 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/>.
"""The main tabbed browser widget."""
import logging
from functools import partial
from PyQt5.QtWidgets import QApplication, QSizePolicy
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSize
from PyQt5.QtGui import QClipboard, QIcon
import qutebrowser.utils.url as urlutils
import qutebrowser.utils.message as message
import qutebrowser.config.config as config
import qutebrowser.commands.utils as cmdutils
from qutebrowser.widgets._tabwidget import TabWidget
from qutebrowser.widgets.webview import WebView
from qutebrowser.browser.signalfilter import SignalFilter
from qutebrowser.browser.curcommand import CurCommandDispatcher
class TabbedBrowser(TabWidget):
"""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 added to a signal_cache of the tab, so it can be
emitted again if the current tab changes.
- the signal gets filtered with _filter_signals and self.cur_* gets
emitted if the signal occured in the current tab.
Attributes:
_url_stack: Stack of URLs of closed tabs.
_tabs: A list of open tabs.
_filter: A SignalFilter instance.
cur: A CurCommandDispatcher instance to dispatch commands to the
current tab.
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_changed: Current URL changed (urlChanged)
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 %.
hint_strings_updated: Hint strings were updated.
arg: A list of hint strings.
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.
"""
cur_progress = pyqtSignal(int)
cur_load_started = pyqtSignal()
cur_load_finished = pyqtSignal(bool)
cur_statusbar_message = pyqtSignal(str)
cur_url_changed = pyqtSignal('QUrl')
cur_link_hovered = pyqtSignal(str, str, str)
cur_scroll_perc_changed = pyqtSignal(int, int)
hint_strings_updated = pyqtSignal(list)
shutdown_complete = pyqtSignal()
quit = pyqtSignal()
resized = pyqtSignal('QRect')
def __init__(self, parent=None):
super().__init__(parent)
self.currentChanged.connect(lambda idx:
self.widget(idx).signal_cache.replay())
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self._tabs = []
self._url_stack = []
self._filter = SignalFilter(self)
self.cur = CurCommandDispatcher(self)
# FIXME adjust this to font size
self.setIconSize(QSize(12, 12))
def _cb_tab_shutdown(self, tab):
"""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.
"""
try:
self._tabs.remove(tab)
except ValueError:
logging.exception("tab {} could not be removed".format(tab))
logging.debug("Tabs after removing: {}".format(self._tabs))
if not self._tabs: # all tabs shut down
logging.debug("Tab shutdown complete.")
self.shutdown_complete.emit()
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.page().mainFrame().loadStarted.connect(partial(
self.on_load_started, tab))
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(
self._filter.create(self.cur_scroll_perc_changed))
tab.urlChanged.connect(self._filter.create(self.cur_url_changed))
# hintmanager
tab.hintmanager.hint_strings_updated.connect(self.hint_strings_updated)
tab.hintmanager.openurl.connect(self.cur.openurl_slot)
# misc
tab.titleChanged.connect(self.on_title_changed)
tab.open_tab.connect(self.tabopen)
tab.iconChanged.connect(self.on_icon_changed)
@pyqtSlot(str, bool)
def tabopen(self, url, background=False):
"""Open a new tab with a given url.
Inner logic for tabopen and backtabopen.
Also connect all the signals we need to _filter_signals.
Args:
url: The URL to open.
background: Whether to oepn the tab in the background.
"""
logging.debug("Opening {}".format(url))
url = urlutils.qurl(url)
tab = WebView(self)
self._connect_tab_signals(tab)
self._tabs.append(tab)
self.addTab(tab, urlutils.urlstring(url))
tab.show()
tab.openurl(url)
if not background:
self.setCurrentWidget(tab)
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():
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()
except TypeError:
pass
tabcount = self.count()
if tabcount == 0:
logging.debug("No tabs -> shutdown complete")
self.shutdown_complete.emit()
return
for tabidx in range(tabcount):
logging.debug("Shutting down tab {}/{}".format(tabidx, tabcount))
tab = self.widget(tabidx)
tab.shutdown(callback=partial(self._cb_tab_shutdown, tab))
@cmdutils.register(instance='mainwindow.tabs')
def tabclose(self, count=None):
"""Close the current/[count]th tab.
Command handler for :close.
Args:
count: The tab index to close, or None
Emit:
quit: If last tab was closed and last-close in config is set to
quit.
"""
idx = self.currentIndex() if count is None else count - 1
tab = self.cntwidget(count)
if tab is None:
return
last_close = config.get('tabbar', 'last-close')
if self.count() > 1:
url = tab.url()
if not url.isEmpty():
self._url_stack.append(url)
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')
@cmdutils.register(instance='mainwindow.tabs', split=False, name='tabopen')
def tabopen_cmd(self, url):
"""Open a new tab with a given url."""
self.tabopen(url, background=False)
@cmdutils.register(instance='mainwindow.tabs', split=False,
name='backtabopen')
def backtabopen_cmd(self, url):
"""Open a new tab in background."""
self.tabopen(url, background=True)
@cmdutils.register(instance='mainwindow.tabs', hide=True)
def tabopencur(self):
"""Set the statusbar to :tabopen and the current URL."""
url = urlutils.urlstring(self.currentWidget().url())
message.set_cmd_text(':tabopen ' + url)
@cmdutils.register(instance='mainwindow.tabs', hide=True)
def opencur(self):
"""Set the statusbar to :open and the current URL."""
url = urlutils.urlstring(self.currentWidget().url())
message.set_cmd_text(':open ' + url)
@cmdutils.register(instance='mainwindow.tabs', name='undo')
def undo_close(self):
"""Switch to the previous tab, or skip [count] tabs.
Command handler for :undo.
"""
if self._url_stack:
self.tabopen(self._url_stack.pop())
else:
message.error("Nothing to undo!")
@cmdutils.register(instance='mainwindow.tabs', name='tabprev')
def switch_prev(self, count=1):
"""Switch to the ([count]th) previous tab.
Command handler for :tabprev.
Args:
count: How many tabs to switch back.
"""
newidx = self.currentIndex() - count
if newidx >= 0:
self.setCurrentIndex(newidx)
elif config.get('tabbar', 'wrap'):
self.setCurrentIndex(newidx % self.count())
else:
message.error("First tab")
@cmdutils.register(instance='mainwindow.tabs', name='tabnext')
def switch_next(self, count=1):
"""Switch to the next tab, or skip [count] tabs.
Command handler for :tabnext.
Args:
count: How many tabs to switch forward.
"""
newidx = self.currentIndex() + count
if newidx < self.count():
self.setCurrentIndex(newidx)
elif config.get('tabbar', 'wrap'):
self.setCurrentIndex(newidx % self.count())
else:
message.error("Last tab")
@cmdutils.register(instance='mainwindow.tabs', nargs=(0, 1))
def paste(self, sel=False, tab=False):
"""Open a page from the clipboard.
Command handler for :paste.
Args:
sel: True to use primary selection, False to use clipboard
tab: True to open in a new tab.
"""
clip = QApplication.clipboard()
mode = QClipboard.Selection if sel else QClipboard.Clipboard
url = clip.text(mode)
if not url:
message.error("Clipboard is empty.")
return
logging.debug("Clipboard contained: '{}'".format(url))
if tab:
self.tabopen(url)
else:
self.cur.openurl(url)
@cmdutils.register(instance='mainwindow.tabs')
def tabpaste(self, sel=False):
"""Open a page from the clipboard in a new tab.
Command handler for :paste.
Args:
sel: True to use primary selection, False to use clipboard
"""
self.paste(sel, True)
@cmdutils.register(instance='mainwindow.tabs')
def focus_tab(self, idx=None, count=None):
"""Select the tab given as argument or in count.
Args:
idx: The tab index to focus, starting with 1.
"""
if ((idx is None and count is None) or
(idx is not None and count is not None)):
message.error("Either argument or count must be given!")
return
try:
i = int(idx) if idx is not None else count
except ValueError:
message.error("Invalid argument: {}".format(idx))
return
if 1 <= i <= self.count():
self.setCurrentIndex(i - 1)
else:
message.error("Index out of bounds!")
return
@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)
@pyqtSlot()
def on_load_started(self, tab):
"""Clear signal cache and icon when a tab started loading.
Args:
tab: The tab where the signal belongs to.
"""
tab.signal_cache.clear()
self.setTabIcon(self.indexOf(tab), QIcon())
@pyqtSlot(str)
def on_title_changed(self, text):
"""Set the title of a tab.
Slot for the titleChanged signal of any tab.
Args:
text: The text to set.
"""
logging.debug("title changed to '{}'".format(text))
if text:
self.setTabText(self.indexOf(self.sender()), text)
else:
logging.debug("ignoring title change")
@pyqtSlot()
def on_icon_changed(self):
"""Set the icon of a tab.
Slot for the iconChanged signal of any tab.
"""
tab = self.sender()
self.setTabIcon(self.indexOf(tab), tab.icon())
@pyqtSlot(str)
def on_mode_left(self, mode):
"""Give focus to tabs if command mode was left."""
if mode == "command":
self.setFocus()
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())