qutebrowser/qutebrowser/mainwindow/statusbar/bar.py

520 lines
20 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:
2015-01-03 15:51:31 +01:00
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
2014-05-13 07:10:50 +02: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/>.
"""The main statusbar widget."""
2014-08-26 19:10:14 +02:00
import collections
2014-05-16 15:33:36 +02:00
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, pyqtProperty, Qt, QTime, QSize,
QTimer)
2014-05-13 07:10:50 +02:00
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy
2014-08-26 19:10:14 +02:00
from qutebrowser.config import config, style
2014-09-26 15:48:24 +02:00
from qutebrowser.utils import usertypes, log, objreg, utils
from qutebrowser.mainwindow.statusbar import (command, progress, keystring,
2015-04-02 11:57:56 +02:00
percentage, url, prompt,
tabindex)
from qutebrowser.mainwindow.statusbar import text as textwidget
PreviousWidget = usertypes.enum('PreviousWidget', ['none', 'prompt',
'command'])
Severity = usertypes.enum('Severity', ['normal', 'warning', 'error'])
2015-05-13 22:44:37 +02:00
CaretMode = usertypes.enum('CaretMode', ['off', 'on', 'selection'])
2014-05-13 07:10:50 +02:00
class StatusBar(QWidget):
"""The statusbar at the bottom of the mainwindow.
Attributes:
txt: The Text widget in the statusbar.
keystring: The KeyString widget in the statusbar.
percentage: The Percentage widget in the statusbar.
2014-06-20 17:40:36 +02:00
url: The UrlText widget in the statusbar.
2014-05-13 07:10:50 +02:00
prog: The Progress widget in the statusbar.
2014-09-28 22:13:14 +02:00
cmd: The Command widget in the statusbar.
2014-05-13 07:10:50 +02:00
_hbox: The main QHBoxLayout.
_stack: The QStackedLayout with cmd/txt widgets.
2014-05-16 15:33:36 +02:00
_text_queue: A deque of (error, text) tuples to be displayed.
error: True if message is an error, False otherwise
_text_pop_timer: A Timer displaying the error messages.
_stopwatch: A QTime for the last displayed message.
2014-05-19 04:19:16 +02:00
_timer_was_active: Whether the _text_pop_timer was active before hiding
the command widget.
_previous_widget: A PreviousWidget member - the widget which was
displayed when an error interrupted it.
2014-09-28 22:13:14 +02:00
_win_id: The window ID the statusbar is associated with.
2014-05-13 07:10:50 +02:00
Class attributes:
_severity: The severity of the current message, a Severity member.
2014-05-13 07:10:50 +02:00
For some reason we need to have this as class attribute so
pyqtProperty works correctly.
2014-05-13 07:10:50 +02:00
_prompt_active: If we're currently in prompt-mode.
2014-05-26 16:59:11 +02:00
For some reason we need to have this as class attribute
so pyqtProperty works correctly.
_insert_active: If we're currently in insert mode.
2014-06-23 16:43:59 +02:00
For some reason we need to have this as class attribute
so pyqtProperty works correctly.
2015-05-13 22:44:37 +02:00
_caret_mode: The current caret mode (off/on/selection).
For some reason we need to have this as class attribute
so pyqtProperty works correctly.
2014-05-13 07:10:50 +02:00
Signals:
resized: Emitted when the statusbar has resized, so the completion
widget can adjust its size to it.
arg: The new size.
moved: Emitted when the statusbar has moved, so the completion widget
can move the the right position.
arg: The new position.
"""
resized = pyqtSignal('QRect')
moved = pyqtSignal('QPoint')
_severity = None
2014-05-26 16:59:11 +02:00
_prompt_active = False
2014-06-23 16:43:59 +02:00
_insert_active = False
2015-05-13 22:44:37 +02:00
_caret_mode = CaretMode.off
2014-05-13 07:10:50 +02:00
STYLESHEET = """
2014-08-28 17:47:40 +02:00
QWidget#StatusBar {
2014-08-28 17:54:11 +02:00
{{ color['statusbar.bg'] }}
2014-08-28 17:47:40 +02:00
}
QWidget#StatusBar[insert_active="true"] {
2014-08-28 17:54:11 +02:00
{{ color['statusbar.bg.insert'] }}
2014-08-28 17:47:40 +02:00
}
2015-05-13 22:44:37 +02:00
QWidget#StatusBar[caret_mode="on"] {
{{ color['statusbar.bg.caret'] }}
}
2015-05-13 22:44:37 +02:00
QWidget#StatusBar[caret_mode="selection"] {
{{ color['statusbar.bg.caret-selection'] }}
}
2014-08-28 17:47:40 +02:00
QWidget#StatusBar[prompt_active="true"] {
2014-08-28 17:54:11 +02:00
{{ color['statusbar.bg.prompt'] }}
2014-08-28 17:47:40 +02:00
}
QWidget#StatusBar[severity="error"] {
2014-08-28 17:54:11 +02:00
{{ color['statusbar.bg.error'] }}
2014-08-28 17:47:40 +02:00
}
QWidget#StatusBar[severity="warning"] {
{{ color['statusbar.bg.warning'] }}
}
QLabel, QLineEdit {
2014-08-28 17:54:11 +02:00
{{ color['statusbar.fg'] }}
{{ font['statusbar'] }}
2014-08-28 17:47:40 +02:00
}
2014-05-13 07:10:50 +02:00
"""
2014-09-28 22:13:14 +02:00
def __init__(self, win_id, parent=None):
2014-05-13 07:10:50 +02:00
super().__init__(parent)
2014-09-28 22:13:14 +02:00
objreg.register('statusbar', self, scope='window', window=win_id)
2014-05-13 07:10:50 +02:00
self.setObjectName(self.__class__.__name__)
self.setAttribute(Qt.WA_StyledBackground)
2014-08-26 19:10:14 +02:00
style.set_register_stylesheet(self)
2014-05-13 07:10:50 +02:00
self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed)
2014-09-28 22:13:14 +02:00
self._win_id = win_id
2014-05-13 07:10:50 +02:00
self._option = None
self._stopwatch = QTime()
2014-05-13 07:10:50 +02:00
self._hbox = QHBoxLayout(self)
self._hbox.setContentsMargins(0, 0, 0, 0)
self._hbox.setSpacing(5)
2014-06-19 14:13:44 +02:00
self._stack = QStackedLayout()
self._hbox.addLayout(self._stack)
2014-05-13 07:10:50 +02:00
self._stack.setContentsMargins(0, 0, 0, 0)
2014-09-28 22:13:14 +02:00
self.cmd = command.Command(win_id)
self._stack.addWidget(self.cmd)
2014-11-10 07:05:57 +01:00
objreg.register('status-command', self.cmd, scope='window',
window=win_id)
2014-05-13 07:10:50 +02:00
2014-08-26 19:10:14 +02:00
self.txt = textwidget.Text()
2014-05-13 07:10:50 +02:00
self._stack.addWidget(self.txt)
2014-05-19 04:19:16 +02:00
self._timer_was_active = False
2014-08-26 19:10:14 +02:00
self._text_queue = collections.deque()
self._text_pop_timer = usertypes.Timer(self, 'statusbar_text_pop')
2014-05-16 15:33:36 +02:00
self._text_pop_timer.timeout.connect(self._pop_text)
self.set_pop_timer_interval()
objreg.get('config').changed.connect(self.set_pop_timer_interval)
2014-05-13 07:10:50 +02:00
2014-09-28 22:13:14 +02:00
self.prompt = prompt.Prompt(win_id)
2014-05-19 17:01:05 +02:00
self._stack.addWidget(self.prompt)
self._previous_widget = PreviousWidget.none
2014-05-19 17:01:05 +02:00
2014-09-28 22:13:14 +02:00
self.cmd.show_cmd.connect(self._show_cmd_widget)
self.cmd.hide_cmd.connect(self._hide_cmd_widget)
2014-05-13 07:10:50 +02:00
self._hide_cmd_widget()
prompter = objreg.get('prompter', scope='window', window=self._win_id)
prompter.show_prompt.connect(self._show_prompt_widget)
prompter.hide_prompt.connect(self._hide_prompt_widget)
2014-05-19 17:01:05 +02:00
self._hide_prompt_widget()
2014-05-13 07:10:50 +02:00
2014-08-26 19:10:14 +02:00
self.keystring = keystring.KeyString()
2014-05-13 07:10:50 +02:00
self._hbox.addWidget(self.keystring)
2014-08-26 19:10:14 +02:00
self.url = url.UrlText()
2014-05-13 07:10:50 +02:00
self._hbox.addWidget(self.url)
2014-08-26 19:10:14 +02:00
self.percentage = percentage.Percentage()
2014-05-13 07:10:50 +02:00
self._hbox.addWidget(self.percentage)
self.tabindex = tabindex.TabIndex()
self._hbox.addWidget(self.tabindex)
2014-06-19 14:13:44 +02:00
# We add a parent to Progress here because it calls self.show() based
# on some signals, and if that happens before it's added to the layout,
# it will quickly blink up as independent window.
2014-08-26 19:10:14 +02:00
self.prog = progress.Progress(self)
2014-05-13 07:10:50 +02:00
self._hbox.addWidget(self.prog)
objreg.get('config').changed.connect(self.maybe_hide)
QTimer.singleShot(0, self.maybe_hide)
def __repr__(self):
2014-09-26 15:48:24 +02:00
return utils.get_repr(self)
@config.change_filter('ui', 'hide-statusbar')
def maybe_hide(self):
"""Hide the statusbar if it's configured to do so."""
hide = config.get('ui', 'hide-statusbar')
if hide:
self.hide()
else:
self.show()
@pyqtProperty(str)
def severity(self):
"""Getter for self.severity, so it can be used as Qt property.
Return:
The severity as a string (!)
"""
if self._severity is None:
return ""
else:
return self._severity.name
2014-05-13 07:10:50 +02:00
def _set_severity(self, severity):
"""Set the severity for the current message.
2014-05-13 07:10:50 +02:00
Re-set the stylesheet after setting the value, so everything gets
updated by Qt properly.
Args:
severity: A Severity member.
2014-05-13 07:10:50 +02:00
"""
if self._severity == severity:
# This gets called a lot (e.g. if the completion selection was
# changed), and setStyleSheet is relatively expensive, so we ignore
# this if there's nothing to change.
return
log.statusbar.debug("Setting severity to {}".format(severity))
self._severity = severity
2014-08-26 19:10:14 +02:00
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
if severity != Severity.normal:
# If we got an error while command/prompt was shown, raise the text
# widget.
self._stack.setCurrentWidget(self.txt)
2014-05-13 07:10:50 +02:00
2014-05-26 16:59:11 +02:00
@pyqtProperty(bool)
def prompt_active(self):
2014-06-23 16:43:59 +02:00
"""Getter for self.prompt_active, so it can be used as Qt property."""
2014-05-26 16:59:11 +02:00
return self._prompt_active
def _set_prompt_active(self, val):
"""Setter for self.prompt_active.
2014-05-26 16:59:11 +02:00
Re-set the stylesheet after setting the value, so everything gets
updated by Qt properly.
"""
2014-08-26 20:15:41 +02:00
log.statusbar.debug("Setting prompt_active to {}".format(val))
2014-05-26 16:59:11 +02:00
self._prompt_active = val
2014-08-26 19:10:14 +02:00
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
2014-05-26 16:59:11 +02:00
2014-06-23 16:43:59 +02:00
@pyqtProperty(bool)
def insert_active(self):
"""Getter for self.insert_active, so it can be used as Qt property."""
return self._insert_active
2015-05-13 22:44:37 +02:00
@pyqtProperty(str)
def caret_mode(self):
"""Getter for self._caret_mode, so it can be used as Qt property."""
return self._caret_mode.name
2015-05-13 21:52:42 +02:00
def set_mode_active(self, mode, val):
"""Setter for self.{insert,caret}_active.
2014-06-23 16:43:59 +02:00
Re-set the stylesheet after setting the value, so everything gets
updated by Qt properly.
"""
if mode == usertypes.KeyMode.insert:
log.statusbar.debug("Setting insert_active to {}".format(val))
self._insert_active = val
elif mode == usertypes.KeyMode.caret:
2015-05-11 20:32:27 +02:00
webview = objreg.get("tabbed-browser", scope="window",
window=self._win_id).currentWidget()
2015-05-13 22:44:37 +02:00
log.statusbar.debug("Setting caret_mode - val {}, selection "
"{}".format(val, webview.selection_enabled))
if val:
if webview.selection_enabled:
self._set_mode_text("{} selection".format(mode.name))
self._caret_mode = CaretMode.selection
else:
self._set_mode_text(mode.name)
2015-05-13 22:44:37 +02:00
self._caret_mode = CaretMode.on
else:
self._caret_mode = CaretMode.off
2014-08-26 19:10:14 +02:00
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
2014-06-23 16:43:59 +02:00
def _set_mode_text(self, mode):
"""Set the mode text."""
text = "-- {} MODE --".format(mode.upper())
self.txt.set_text(self.txt.Text.normal, text)
2014-05-16 15:33:36 +02:00
def _pop_text(self):
"""Display a text in the statusbar and pop it from _text_queue."""
try:
severity, text = self._text_queue.popleft()
2014-05-16 15:33:36 +02:00
except IndexError:
self._set_severity(Severity.normal)
self.txt.set_text(self.txt.Text.temp, '')
2014-05-16 15:33:36 +02:00
self._text_pop_timer.stop()
# If a previous widget was interrupted by an error, restore it.
if self._previous_widget == PreviousWidget.prompt:
self._stack.setCurrentWidget(self.prompt)
elif self._previous_widget == PreviousWidget.command:
self._stack.setCurrentWidget(self.cmd)
elif self._previous_widget == PreviousWidget.none:
self.maybe_hide()
else:
raise AssertionError("Unknown _previous_widget!")
2014-05-16 15:33:36 +02:00
return
self.show()
log.statusbar.debug("Displaying message: {} (severity {})".format(
text, severity))
2014-08-26 20:15:41 +02:00
log.statusbar.debug("Remaining: {}".format(self._text_queue))
self._set_severity(severity)
self.txt.set_text(self.txt.Text.temp, text)
2014-05-16 15:33:36 +02:00
2014-05-13 07:10:50 +02:00
def _show_cmd_widget(self):
"""Show command widget instead of temporary text."""
self._set_severity(Severity.normal)
self._previous_widget = PreviousWidget.command
2014-05-19 04:19:16 +02:00
if self._text_pop_timer.isActive():
self._timer_was_active = True
2014-05-16 15:33:36 +02:00
self._text_pop_timer.stop()
2014-09-28 22:13:14 +02:00
self._stack.setCurrentWidget(self.cmd)
self.show()
2014-05-13 07:10:50 +02:00
def _hide_cmd_widget(self):
"""Show temporary text instead of command widget."""
2014-08-26 20:15:41 +02:00
log.statusbar.debug("Hiding cmd widget, queue: {}".format(
self._text_queue))
self._previous_widget = PreviousWidget.none
2014-05-19 04:19:16 +02:00
if self._timer_was_active:
# Restart the text pop timer if it was active before hiding.
2014-05-16 15:33:36 +02:00
self._pop_text()
self._text_pop_timer.start()
2014-05-19 04:19:16 +02:00
self._timer_was_active = False
2014-05-13 07:10:50 +02:00
self._stack.setCurrentWidget(self.txt)
self.maybe_hide()
2014-05-13 07:10:50 +02:00
2014-05-19 17:01:05 +02:00
def _show_prompt_widget(self):
"""Show prompt widget instead of temporary text."""
if self._stack.currentWidget() is self.prompt:
return
self._set_severity(Severity.normal)
self._set_prompt_active(True)
self._previous_widget = PreviousWidget.prompt
2014-05-19 17:01:05 +02:00
if self._text_pop_timer.isActive():
self._timer_was_active = True
self._text_pop_timer.stop()
self._stack.setCurrentWidget(self.prompt)
self.show()
2014-05-19 17:01:05 +02:00
def _hide_prompt_widget(self):
"""Show temporary text instead of prompt widget."""
self._set_prompt_active(False)
self._previous_widget = PreviousWidget.none
2014-08-26 20:15:41 +02:00
log.statusbar.debug("Hiding prompt widget, queue: {}".format(
2014-05-21 17:29:09 +02:00
self._text_queue))
2014-05-19 17:01:05 +02:00
if self._timer_was_active:
# Restart the text pop timer if it was active before hiding.
self._pop_text()
self._text_pop_timer.start()
self._timer_was_active = False
self._stack.setCurrentWidget(self.txt)
self.maybe_hide()
2014-05-19 17:01:05 +02:00
def _disp_text(self, text, severity, immediately=False):
2014-05-16 17:21:43 +02:00
"""Inner logic for disp_error and disp_temp_text.
Args:
text: The message to display.
severity: The severity of the messages.
2014-06-26 07:58:00 +02:00
immediately: If set, message gets displayed immediately instead of
queued.
2014-05-16 17:21:43 +02:00
"""
log.statusbar.debug("Displaying text: {} (severity={})".format(
text, severity))
2014-05-17 23:45:31 +02:00
mindelta = config.get('ui', 'message-timeout')
if self._stopwatch.isNull():
delta = None
self._stopwatch.start()
else:
delta = self._stopwatch.restart()
2014-08-26 20:15:41 +02:00
log.statusbar.debug("queue: {} / delta: {}".format(
self._text_queue, delta))
if not self._text_queue and (delta is None or delta > mindelta):
2014-05-16 16:43:14 +02:00
# If the queue is empty and we didn't print messages for long
# enough, we can take the short route and display the message
# immediately. We then start the pop_timer only to restore the
# normal state in 2 seconds.
2014-08-26 20:15:41 +02:00
log.statusbar.debug("Displaying immediately")
self._set_severity(severity)
self.show()
self.txt.set_text(self.txt.Text.temp, text)
self._text_pop_timer.start()
elif self._text_queue and self._text_queue[-1] == (severity, text):
2014-05-16 16:50:53 +02:00
# If we get the same message multiple times in a row and we're
# still displaying it *anyways* we ignore the new one
2014-08-26 20:15:41 +02:00
log.statusbar.debug("ignoring")
2014-06-26 07:58:00 +02:00
elif immediately:
# This message is a reaction to a keypress and should be displayed
2014-09-22 20:44:07 +02:00
# immediately, temporarily interrupting the message queue.
# We display this immediately and restart the timer.to clear it and
# display the rest of the queue later.
2014-08-26 20:15:41 +02:00
log.statusbar.debug("Moving to beginning of queue")
self._set_severity(severity)
self.show()
self.txt.set_text(self.txt.Text.temp, text)
self._text_pop_timer.start()
2014-05-16 16:43:14 +02:00
else:
2014-05-16 16:50:53 +02:00
# There are still some messages to be displayed, so we queue this
# up.
2014-08-26 20:15:41 +02:00
log.statusbar.debug("queueing")
self._text_queue.append((severity, text))
self._text_pop_timer.start()
2014-05-16 16:43:14 +02:00
2014-05-16 17:21:43 +02:00
@pyqtSlot(str, bool)
2014-06-26 07:58:00 +02:00
def disp_error(self, text, immediately=False):
2014-05-16 17:21:43 +02:00
"""Display an error in the statusbar.
2014-05-13 07:10:50 +02:00
2014-05-16 17:21:43 +02:00
Args:
text: The message to display.
2014-06-26 07:58:00 +02:00
immediately: If set, message gets displayed immediately instead of
queued.
2014-05-16 17:21:43 +02:00
"""
self._disp_text(text, Severity.error, immediately)
@pyqtSlot(str, bool)
def disp_warning(self, text, immediately=False):
"""Display a warning in the statusbar.
Args:
text: The message to display.
immediately: If set, message gets displayed immediately instead of
queued.
"""
self._disp_text(text, Severity.warning, immediately)
2014-05-16 17:21:43 +02:00
@pyqtSlot(str, bool)
2014-06-26 07:58:00 +02:00
def disp_temp_text(self, text, immediately):
2014-05-16 17:21:43 +02:00
"""Display a temporary text in the statusbar.
Args:
text: The message to display.
2014-06-26 07:58:00 +02:00
immediately: If set, message gets displayed immediately instead of
queued.
2014-05-16 17:21:43 +02:00
"""
self._disp_text(text, Severity.normal, immediately)
2014-05-13 07:10:50 +02:00
2014-05-16 15:33:36 +02:00
@pyqtSlot(str)
def set_text(self, val):
"""Set a normal (persistent) text in the status bar."""
self.txt.set_text(self.txt.Text.normal, val)
2014-05-13 07:10:50 +02:00
2014-08-26 19:10:14 +02:00
@pyqtSlot(usertypes.KeyMode)
2014-05-13 07:10:50 +02:00
def on_mode_entered(self, mode):
"""Mark certain modes in the commandline."""
2014-09-28 22:13:14 +02:00
mode_manager = objreg.get('mode-manager', scope='window',
window=self._win_id)
if mode in mode_manager.passthrough:
self._set_mode_text(mode.name)
if mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret):
2015-05-13 21:52:42 +02:00
self.set_mode_active(mode, True)
2014-05-13 07:10:50 +02:00
@pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)
def on_mode_left(self, old_mode, new_mode):
2014-05-13 07:10:50 +02:00
"""Clear marked mode."""
2014-09-28 22:13:14 +02:00
mode_manager = objreg.get('mode-manager', scope='window',
window=self._win_id)
if old_mode in mode_manager.passthrough:
if new_mode in mode_manager.passthrough:
self._set_mode_text(new_mode.name)
else:
self.txt.set_text(self.txt.Text.normal, '')
if old_mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret):
2015-05-13 21:52:42 +02:00
self.set_mode_active(old_mode, False)
2014-05-13 07:10:50 +02:00
@config.change_filter('ui', 'message-timeout')
def set_pop_timer_interval(self):
2014-05-16 15:33:36 +02:00
"""Update message timeout when config changed."""
self._text_pop_timer.setInterval(config.get('ui', 'message-timeout'))
2014-05-16 15:33:36 +02:00
2014-05-13 07:10:50 +02: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())
def moveEvent(self, e):
"""Extend moveEvent of QWidget to emit a moved signal afterwards.
Args:
e: The QMoveEvent.
"""
super().moveEvent(e)
self.moved.emit(e.pos())
def minimumSizeHint(self):
"""Set the minimum height to the text height plus some padding."""
width = super().minimumSizeHint().width()
height = self.fontMetrics().height() + 3
return QSize(width, height)