qutebrowser/qutebrowser/widgets/_tabbedbrowser.py

521 lines
18 KiB
Python
Raw Normal View History

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
import logging
2014-04-17 09:44:26 +02:00
from functools import partial
from PyQt5.QtWidgets import QApplication, QSizePolicy
2014-05-04 01:28:34 +02:00
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSize
from PyQt5.QtGui import QClipboard, QIcon
2014-02-07 17:20:55 +01:00
import qutebrowser.utils.url as urlutils
2014-04-21 20:29:58 +02:00
import qutebrowser.utils.message as message
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
from qutebrowser.widgets._tabwidget import TabWidget
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.curcommand import CurCommandDispatcher
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 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.
2014-02-07 20:21:50 +01:00
2014-02-18 16:38:13 +01:00
Attributes:
_url_stack: Stack of URLs of closed tabs.
_tabs: A list of open tabs.
2014-04-17 09:44:26 +02:00
_filter: A SignalFilter instance.
cur: A CurCommandDispatcher instance to dispatch commands to the
current tab.
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_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.
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.
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_changed = pyqtSignal('QUrl')
cur_link_hovered = pyqtSignal(str, str, str)
2014-01-21 08:37:21 +01:00
cur_scroll_perc_changed = pyqtSignal(int, int)
hint_strings_updated = pyqtSignal(list)
2014-02-18 16:38:13 +01:00
shutdown_complete = pyqtSignal()
quit = pyqtSignal()
resized = pyqtSignal('QRect')
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-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 = []
self._url_stack = []
2014-04-17 09:44:26 +02:00
self._filter = SignalFilter(self)
self.cur = CurCommandDispatcher(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
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)
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()
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))
2014-05-04 01:28:34 +02:00
tab.page().mainFrame().loadStarted.connect(partial(
self.on_load_started, tab))
2014-04-22 10:34:43 +02:00
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))
2014-05-08 21:04:27 +02:00
tab.urlChanged.connect(self.on_url_changed)
2014-04-22 10:34:43 +02:00
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)
2014-04-22 10:34:43 +02:00
# misc
2014-04-22 10:45:07 +02:00
tab.titleChanged.connect(self.on_title_changed)
2014-05-04 01:28:34 +02:00
tab.iconChanged.connect(self.on_icon_changed)
2014-04-22 10:34:43 +02:00
2014-05-09 11:20:17 +02:00
def _close_tab(self, tab):
"""Close the given tab.
Args:
tab: The QTabWidget to close.
"""
idx = self.indexOf(tab)
if idx == -1:
raise ValueError("tab is not contained in TabbedWidget!")
url = tab.url()
if not url.isEmpty():
self._url_stack.append(url)
self.removeTab(idx)
tab.shutdown(callback=partial(self._cb_tab_shutdown, tab))
@pyqtSlot(str, bool)
def tabopen(self, url=None, background=None):
"""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 open the tab in the background.
if None, the background-tabs setting decides.
Return:
The opened WebView instance.
"""
logging.debug("Creating new tab with url {}".format(url))
tab = WebView(self)
self._connect_tab_signals(tab)
self._tabs.append(tab)
if url is not None:
url = urlutils.qurl(url)
2014-05-08 21:04:27 +02:00
self.addTab(tab, "")
tab.openurl(url)
else:
self.addTab(tab, "")
if background is None:
background = config.get('general', 'background-tabs')
if not background:
self.setCurrentWidget(tab)
tab.show()
return tab
def cntwidget(self, count=None):
2014-02-18 18:32:07 +01:00
"""Return a widget based on a count/idx.
2014-02-19 10:58:32 +01:00
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.
2014-02-18 18:32:07 +01:00
"""
if count is None:
return self.currentWidget()
elif 1 <= count <= self.count():
return self.widget(count - 1)
else:
return None
def shutdown(self):
2014-02-19 10:58:32 +01:00
"""Try to shut down all tabs cleanly.
Emit:
shutdown_complete if the shutdown completed successfully.
"""
2014-02-18 18:32:07 +01:00
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)
2014-04-17 09:44:26 +02:00
tab.shutdown(callback=partial(self._cb_tab_shutdown, tab))
2014-02-18 18:32:07 +01:00
2014-03-03 18:47:42 +01:00
@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:
2014-04-27 21:21:14 +02:00
quit: If last tab was closed and last-close in config is set to
quit.
"""
tab = self.cntwidget(count)
if tab is None:
return
2014-04-27 21:21:14 +02:00
last_close = config.get('tabbar', 'last-close')
if self.count() > 1:
2014-05-09 11:20:17 +02:00
self._close_tab(tab)
elif last_close == 'quit':
self.quit.emit()
elif last_close == 'blank':
tab.openurl('about:blank')
2014-05-09 11:24:33 +02:00
@cmdutils.register(instance='mainwindow.tabs')
def only(self):
"""Close all tabs except for the current one."""
for i in range(self.count() - 1):
if i == self.currentIndex():
continue
self._close_tab(self.widget(i))
@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)
2013-12-15 21:40:15 +01:00
@cmdutils.register(instance='mainwindow.tabs', split=False,
name='backtabopen')
def backtabopen_cmd(self, url):
2014-04-21 23:33:19 +02:00
"""Open a new tab in background."""
self.tabopen(url, background=True)
2014-04-21 23:33:19 +02:00
2014-03-03 18:47:42 +01:00
@cmdutils.register(instance='mainwindow.tabs', hide=True)
2014-02-06 13:34:49 +01:00
def tabopencur(self):
2014-04-24 21:28:24 +02:00
"""Set the statusbar to :tabopen and the current URL."""
2014-02-10 18:49:25 +01:00
url = urlutils.urlstring(self.currentWidget().url())
2014-04-24 21:28:24 +02:00
message.set_cmd_text(':tabopen ' + url)
2014-02-06 13:34:49 +01:00
2014-03-03 18:47:42 +01:00
@cmdutils.register(instance='mainwindow.tabs', hide=True)
2014-02-06 13:34:49 +01:00
def opencur(self):
2014-04-24 21:28:24 +02:00
"""Set the statusbar to :open and the current URL."""
2014-02-10 18:49:25 +01:00
url = urlutils.urlstring(self.currentWidget().url())
2014-04-24 21:28:24 +02:00
message.set_cmd_text(':open ' + url)
2014-02-06 13:34:49 +01:00
@cmdutils.register(instance='mainwindow.tabs', hide=True)
def backtabopencur(self):
"""Set the statusbar to :backtabopen and the current URL."""
url = urlutils.urlstring(self.currentWidget().url())
message.set_cmd_text(':backtabopen ' + url)
2014-03-03 18:47:42 +01:00
@cmdutils.register(instance='mainwindow.tabs', name='undo')
2014-01-17 23:17:24 +01:00
def undo_close(self):
2014-03-03 06:09:23 +01:00
"""Switch to the previous tab, or skip [count] tabs.
2014-01-29 15:30:19 +01:00
Command handler for :undo.
"""
if self._url_stack:
self.tabopen(self._url_stack.pop())
else:
message.error("Nothing to undo!")
2014-01-17 23:17:24 +01:00
2014-03-03 18:47:42 +01:00
@cmdutils.register(instance='mainwindow.tabs', name='tabprev')
def switch_prev(self, count=1):
"""Switch to the ([count]th) previous tab.
2014-01-29 15:30:19 +01:00
Command handler for :tabprev.
2014-02-07 20:21:50 +01:00
2014-02-19 10:58:32 +01:00
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:
2014-04-23 06:17:29 +02:00
message.error("First tab")
2014-03-03 18:47:42 +01:00
@cmdutils.register(instance='mainwindow.tabs', name='tabnext')
def switch_next(self, count=1):
2014-03-03 06:09:23 +01:00
"""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:
2014-04-23 06:17:29 +02:00
message.error("Last tab")
2014-04-21 21:05:39 +02:00
@cmdutils.register(instance='mainwindow.tabs', nargs=(0, 1))
2014-04-21 20:29:58 +02:00
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
2014-04-21 20:29:58 +02:00
tab: True to open in a new tab.
"""
clip = QApplication.clipboard()
mode = QClipboard.Selection if sel else QClipboard.Clipboard
url = clip.text(mode)
2014-04-21 20:29:58 +02:00
if not url:
message.error("Clipboard is empty.")
return
logging.debug("Clipboard contained: '{}'".format(url))
2014-04-21 20:29:58 +02:00
if tab:
self.tabopen(url)
else:
self.cur.openurl(url)
2014-03-03 18:47:42 +01:00
@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
"""
2014-04-21 20:29:58 +02:00
self.paste(sel, True)
2014-05-03 00:32:43 +02:00
@cmdutils.register(instance='mainwindow.tabs')
2014-05-08 09:03:48 +02:00
def focus_tab(self, index=None, count=None):
2014-05-05 10:09:19 +02:00
"""Select the tab given as argument or in count.
2014-05-04 01:33:01 +02:00
Args:
2014-05-08 09:03:48 +02:00
index: The tab index to focus, starting with 1.
2014-05-04 01:33:01 +02:00
"""
2014-05-09 11:29:10 +02:00
if index is not None and count is not None:
2014-05-03 00:32:43 +02:00
message.error("Either argument or count must be given!")
return
2014-05-09 11:29:10 +02:00
elif index is not None:
try:
idx = int(index)
except ValueError:
message.error("Argument ({}) needs to be a number!".format(
index))
return
elif count is not None:
if count == 0:
idx = self.count()
else:
idx = count
else:
idx = 1
2014-05-08 09:03:48 +02:00
if 1 <= idx <= self.count():
self.setCurrentIndex(idx - 1)
2014-05-03 00:32:43 +02:00
else:
2014-05-08 09:03:48 +02:00
message.error("There's no tab with index {}!".format(idx))
2014-05-03 00:32:43 +02:00
return
2014-05-09 11:45:20 +02:00
@cmdutils.register(instance='mainwindow.tabs')
def tab_move(self, direction=None, count=None):
"""Move the current tab.
Args:
direction: + or - for relative moving, None for absolute.
count: If moving absolutely: New position (or first).
If moving relatively: Offset.
"""
cur_idx = self.currentIndex()
if direction is None:
# absolute move
if count is None:
new_idx = 0
elif count == 0:
new_idx = self.count() - 1
else:
new_idx = count - 1
elif direction in '+-':
# relative move
if count is None:
message.error("Count must be given for relative moving!")
return
if direction == '-':
new_idx = cur_idx - count
elif direction == '+':
new_idx = cur_idx + count
else:
message.error("Invalid direction '{}'!".format(direction))
return
if not 0 <= new_idx < self.count():
message.error("Can't move tab to position {}!".format(new_idx))
return
tab = self.currentWidget()
icon = self.tabIcon(cur_idx)
label = self.tabText(cur_idx)
self.removeTab(cur_idx)
self.insertTab(new_idx, tab, icon, label)
self.setCurrentIndex(new_idx)
2014-05-09 11:57:58 +02:00
@cmdutils.register(instance='mainwindow.tabs')
def tab_focus_last(self):
2014-05-09 15:30:27 +02:00
"""Select the tab which was last focused."""
2014-05-09 11:57:58 +02:00
idx = self.indexOf(self.last_focused)
if idx == -1:
message.error("Last focused tab vanished!")
return
self.setCurrentIndex(idx)
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-04 01:28:34 +02:00
@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())
2014-04-22 10:45:07 +02:00
@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.
"""
2014-04-25 16:53:23 +02:00
logging.debug("title changed to '{}'".format(text))
2014-04-22 10:45:07 +02:00
if text:
self.setTabText(self.indexOf(self.sender()), text)
else:
2014-04-25 16:53:23 +02:00
logging.debug("ignoring title change")
2014-04-22 10:45:07 +02:00
2014-05-08 21:04:27 +02:00
@pyqtSlot('QUrl')
def on_url_changed(self, url):
"""Set the new URL as title if there's no title yet."""
idx = self.indexOf(self.sender())
if not self.tabText(idx):
self.setTabText(idx, urlutils.urlstring(url))
2014-05-04 01:28:34 +02:00
@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())
2014-04-25 07:50:21 +02:00
@pyqtSlot(str)
def on_mode_left(self, mode):
2014-04-25 11:21:00 +02:00
"""Give focus to tabs if command mode was left."""
2014-04-25 07:50:21 +02:00
if mode == "command":
self.setFocus()
2014-05-09 11:57:58 +02:00
@pyqtSlot(int)
def on_current_changed(self, idx):
"""Set last_focused and replay signal cache if focus changed."""
tab = self.widget(idx)
tab.signal_cache.replay()
self.last_focused = self.now_focused
self.now_focused = tab
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())