qutebrowser/qutebrowser/widgets/_tabbedbrowser.py

558 lines
19 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
2014-05-12 21:23:16 +02:00
from PyQt5.QtGui import QClipboard
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
2014-05-12 21:23:16 +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.curcommand import CurCommandDispatcher
2014-05-14 18:00:40 +02:00
from qutebrowser.commands.exceptions import CommandError
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:
_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_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.
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)
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-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 = []
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-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)
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))
tab.loadStarted.connect(self._filter.create(self.cur_load_started))
tab.statusBarMessage.connect(
self._filter.create(self.cur_statusbar_message))
2014-05-16 14:20:37 +02:00
tab.scroll_perc_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(self.on_url_text_changed)
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.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)
tab.page().mainFrame().loadStarted.connect(partial(
self.on_load_started, tab))
2014-04-22 10:34:43 +02:00
2014-05-15 00:02:40 +02:00
def _close_tab(self, tab_or_idx):
"""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():
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')
@pyqtSlot(int)
def on_tab_close_requested(self, idx):
"""Close a tab via an index."""
self._close_tab(idx)
2014-05-09 11:20:17 +02:00
def _tab_move_absolute(self, idx):
"""Get an index for moving a tab absolutely.
Args:
idx: The index to get, as passed as count.
"""
if idx is None:
return 0
elif idx == 0:
return self.count() - 1
else:
return idx - 1
def _tab_move_relative(self, direction, delta):
"""Get an index for moving a tab relatively.
Args:
direction: + or - for relative moving, None for absolute.
delta: Delta to the current tab.
"""
if delta is None:
raise ValueError
if direction == '-':
return self.currentIndex() - delta
elif direction == '+':
return self.currentIndex() + delta
@pyqtSlot(str, 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.
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)
self.addTab(tab, "")
if url is not None:
url = urlutils.qurl(url)
tab.openurl(url)
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():
cmdutils.check_overflow(count + 1, 'int')
2014-02-18 18:32:07 +01:00
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
2014-05-13 21:25:16 +02:00
for tab in self.widgets:
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-05-16 23:01:40 +02:00
@cmdutils.register(instance='mainwindow.tabs', name='tab-close')
def tabclose(self, count=None):
"""Close the current/[count]th tab.
2014-05-16 23:01:40 +02:00
Command handler for :tab-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-05-15 00:02:40 +02:00
self._close_tab(tab)
2014-05-09 11:24:33 +02:00
@cmdutils.register(instance='mainwindow.tabs')
2014-05-16 23:01:40 +02:00
def tab_only(self):
2014-05-09 11:24:33 +02:00
"""Close all tabs except for the current one."""
2014-05-13 21:25:16 +02:00
for tab in self.widgets:
if tab is self.currentWidget():
2014-05-09 11:24:33 +02:00
continue
2014-05-13 21:25:16 +02:00
self._close_tab(tab)
2014-05-09 11:24:33 +02:00
2014-05-16 23:01:40 +02:00
@cmdutils.register(instance='mainwindow.tabs', split=False)
def open_tab(self, url):
"""Open a new tab with a given url."""
self.tabopen(url, background=False)
2013-12-15 21:40:15 +01:00
2014-05-16 23:01:40 +02:00
@cmdutils.register(instance='mainwindow.tabs', split=False)
def open_tab_bg(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-05-16 23:01:40 +02:00
def open_tab_cur(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-05-16 23:01:40 +02:00
message.set_cmd_text(':open-tab ' + 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-05-16 23:01:40 +02:00
def open_cur(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)
2014-05-16 23:01:40 +02:00
def open_tab_bg_cur(self):
"""Set the statusbar to :tabopen-bg and the current URL."""
url = urlutils.urlstring(self.currentWidget().url())
2014-05-16 23:01:40 +02:00
message.set_cmd_text(':open-tab-bg ' + 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):
"""Re-open a closed tab (optionally skipping [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:
2014-05-14 18:00:40 +02:00
raise CommandError("Nothing to undo!")
2014-01-17 23:17:24 +01:00
2014-05-16 23:01:40 +02:00
@cmdutils.register(instance='mainwindow.tabs', name='tab-prev')
def switch_prev(self, count=1):
"""Switch to the previous tab, or skip [count] tabs.
2014-01-29 15:30:19 +01:00
2014-05-16 23:01:40 +02:00
Command handler for :tab-prev.
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-05-14 18:00:40 +02:00
raise CommandError("First tab")
2014-05-16 23:01:40 +02:00
@cmdutils.register(instance='mainwindow.tabs', name='tab-next')
def switch_next(self, count=1):
2014-03-03 06:09:23 +01:00
"""Switch to the next tab, or skip [count] tabs.
2014-05-16 23:01:40 +02:00
Command handler for :tab-next.
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-05-14 18:00:40 +02:00
raise CommandError("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:
2014-05-14 18:00:40 +02:00
raise CommandError("Clipboard is empty.")
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')
2014-05-16 23:01:40 +02:00
def paste_tab(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-16 23:01:40 +02:00
def tab_focus(self, index=None, count=None):
"""Select the tab given as argument/[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 19:12:08 +02:00
try:
idx = cmdutils.arg_or_count(index, count, default=1,
countzero=self.count())
except ValueError as e:
2014-05-14 18:00:40 +02:00
raise CommandError(e)
cmdutils.check_overflow(idx + 1, 'int')
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-14 18:00:40 +02:00
raise CommandError("There's no tab with index {}!".format(idx))
2014-05-03 00:32:43 +02:00
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.
"""
if direction is None:
new_idx = self._tab_move_absolute(count)
2014-05-09 11:45:20 +02:00
elif direction in '+-':
try:
new_idx = self._tab_move_relative(direction, count)
except ValueError:
2014-05-14 18:00:40 +02:00
raise CommandError("Count must be given for relative moving!")
2014-05-09 11:45:20 +02:00
else:
2014-05-14 18:00:40 +02:00
raise CommandError("Invalid direction '{}'!".format(direction))
2014-05-09 11:45:20 +02:00
if not 0 <= new_idx < self.count():
2014-05-14 18:00:40 +02:00
raise CommandError("Can't move tab to position {}!".format(
new_idx))
2014-05-09 11:45:20 +02:00
tab = self.currentWidget()
cur_idx = self.currentIndex()
2014-05-09 11:45:20 +02:00
icon = self.tabIcon(cur_idx)
label = self.tabText(cur_idx)
cmdutils.check_overflow(cur_idx, 'int')
2014-05-15 00:03:54 +02:00
cmdutils.check_overflow(new_idx, 'int')
2014-05-09 11:45:20 +02:00
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:
2014-05-14 18:00:40 +02:00
raise CommandError("Last focused tab vanished!")
2014-05-09 11:57:58 +02:00
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-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
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
@pyqtSlot(str)
def on_url_text_changed(self, url):
2014-05-08 21:04:27 +02:00
"""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, url)
2014-05-08 21:04:27 +02:00
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.
"""
2014-05-12 23:03:55 +02:00
if not config.get('tabbar', 'show-favicons'):
return
2014-05-04 01:28:34 +02:00
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 when focus changed."""
2014-05-09 11:57:58 +02:00
tab = self.widget(idx)
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())