qutebrowser/qutebrowser/widgets/statusbar/bar.py

427 lines
16 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-05-13 07:10:50 +02: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/>.
"""The main statusbar widget."""
2014-08-26 19:10:14 +02:00
import collections
import datetime
2014-05-16 15:33:36 +02:00
from PyQt5.QtCore import pyqtSignal, pyqtSlot, pyqtProperty, Qt
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.keyinput import modeman
from qutebrowser.config import config, style
2014-08-26 20:15:41 +02:00
from qutebrowser.utils import usertypes, log
2014-08-26 19:10:14 +02:00
from qutebrowser.widgets.statusbar import (command, progress, keystring,
percentage, url, prompt)
from qutebrowser.widgets.statusbar import text as textwidget
2014-08-26 19:10:14 +02:00
PreviousWidget = usertypes.enum('PreviousWidget', 'none', 'prompt', 'command')
2014-05-13 07:10:50 +02:00
class StatusBar(QWidget):
"""The statusbar at the bottom of the mainwindow.
Attributes:
cmd: The Command widget in the statusbar.
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.
_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.
2014-05-16 16:43:14 +02:00
_last_text_time: The timestamp where a message was last displayed.
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-05-13 07:10:50 +02:00
Class attributes:
_error: If there currently is an error, accessed through the error
property.
For some reason we need to have this as class attribute so
pyqtProperty works correctly.
2014-05-26 16:59:11 +02:00
_prompt_active: If we're currently in prompt-mode, accessed through the
prompt_active property.
For some reason we need to have this as class attribute
so pyqtProperty works correctly.
2014-06-23 16:43:59 +02:00
_insert_active: If we're currently in insert mode, accessed through the
insert_active property.
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')
_error = False
2014-05-26 16:59:11 +02:00
_prompt_active = False
2014-06-23 16:43:59 +02:00
_insert_active = False
2014-05-13 07:10:50 +02:00
STYLESHEET = """
2014-05-26 16:59:11 +02:00
QWidget#StatusBar {{
2014-05-13 07:10:50 +02:00
{color[statusbar.bg]}
}}
2014-06-23 16:43:59 +02:00
QWidget#StatusBar[insert_active="true"] {{
{color[statusbar.bg.insert]}
}}
QWidget#StatusBar[prompt_active="true"] {{
{color[statusbar.bg.prompt]}
}}
QWidget#StatusBar[error="true"] {{
{color[statusbar.bg.error]}
}}
2014-05-13 07:10:50 +02:00
QWidget {{
{color[statusbar.fg]}
{font[statusbar]}
}}
"""
def __init__(self, parent=None):
super().__init__(parent)
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)
self._option = None
2014-05-16 16:43:14 +02:00
self._last_text_time = None
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-08-26 19:10:14 +02:00
self.cmd = command.Command()
2014-05-13 07:10:50 +02:00
self._stack.addWidget(self.cmd)
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-18 00:36:29 +02:00
self._text_pop_timer.setInterval(config.get('ui', 'message-timeout'))
2014-05-16 15:33:36 +02:00
self._text_pop_timer.timeout.connect(self._pop_text)
2014-05-13 07:10:50 +02:00
2014-08-26 19:10:14 +02:00
self.prompt = prompt.Prompt()
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-05-13 07:10:50 +02:00
self.cmd.show_cmd.connect(self._show_cmd_widget)
self.cmd.hide_cmd.connect(self._hide_cmd_widget)
self._hide_cmd_widget()
self.prompt.show_prompt.connect(self._show_prompt_widget)
2014-05-19 17:01:05 +02:00
self.prompt.hide_prompt.connect(self._hide_prompt_widget)
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)
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)
def __repr__(self):
return '<{}>'.format(self.__class__.__name__)
2014-05-13 07:10:50 +02:00
@pyqtProperty(bool)
def error(self):
"""Getter for self.error, so it can be used as Qt property."""
# pylint: disable=method-hidden
return self._error
@error.setter
def error(self, val):
"""Setter for self.error, so it can be used as Qt property.
Re-set the stylesheet after setting the value, so everything gets
updated by Qt properly.
"""
if self._error == val:
# 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
2014-08-26 20:15:41 +02:00
log.statusbar.debug("Setting error to {}".format(val))
2014-05-13 07:10:50 +02:00
self._error = val
2014-08-26 19:10:14 +02:00
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
if val:
# 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
# pylint: disable=method-hidden
return self._prompt_active
@prompt_active.setter
def prompt_active(self, val):
2014-06-23 16:43:59 +02:00
"""Setter for self.prompt_active, so it can be used as Qt property.
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."""
# pylint: disable=method-hidden
return self._insert_active
@insert_active.setter
def insert_active(self, val):
"""Setter for self.insert_active, so it can be used as Qt property.
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 insert_active to {}".format(val))
2014-06-23 16:43:59 +02:00
self._insert_active = val
2014-08-26 19:10:14 +02:00
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
2014-06-23 16:43:59 +02:00
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:
error, text = self._text_queue.popleft()
except IndexError:
self.error = False
self.txt.temptext = ''
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.command)
elif self._previous_widget == PreviousWidget.none:
pass
else:
raise AssertionError("Unknown _previous_widget!")
2014-05-16 15:33:36 +02:00
return
2014-08-26 20:15:41 +02:00
log.statusbar.debug("Displaying {} message: {}".format(
2014-05-16 15:33:36 +02:00
'error' if error else 'text', text))
2014-08-26 20:15:41 +02:00
log.statusbar.debug("Remaining: {}".format(self._text_queue))
2014-05-16 15:33:36 +02:00
self.error = error
self.txt.temptext = text
2014-05-13 07:10:50 +02:00
def _show_cmd_widget(self):
"""Show command widget instead of temporary text."""
self.error = False
self._previous_widget = PreviousWidget.prompt
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-05-13 07:10:50 +02:00
self._stack.setCurrentWidget(self.cmd)
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)
2014-05-19 17:01:05 +02:00
def _show_prompt_widget(self):
"""Show prompt widget instead of temporary text."""
self.error = False
2014-05-26 16:59:11 +02:00
self.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)
def _hide_prompt_widget(self):
"""Show temporary text instead of prompt widget."""
2014-05-26 16:59:11 +02:00
self.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)
2014-06-26 07:58:00 +02:00
def _disp_text(self, text, error, 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.
error: Whether it's an error message (True) or normal text (False)
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
"""
2014-06-22 23:33:43 +02:00
# FIXME probably using a QTime here would be easier.
2014-08-26 20:15:41 +02:00
log.statusbar.debug("Displaying text: {} (error={})".format(
text, error))
2014-08-26 19:10:14 +02:00
now = datetime.datetime.now()
2014-05-17 23:45:31 +02:00
mindelta = config.get('ui', 'message-timeout')
2014-05-16 16:43:14 +02:00
delta = (None if self._last_text_time is None
else now - self._last_text_time)
self._last_text_time = now
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.total_seconds() *
1000.0 > 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")
2014-05-16 16:43:14 +02:00
self.error = error
self.txt.temptext = text
self._text_pop_timer.start()
2014-05-16 16:50:53 +02:00
elif self._text_queue and self._text_queue[-1] == (error, text):
# 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
# immediately, temporarely 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.error = error
self.txt.temptext = 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")
2014-05-16 16:43:14 +02:00
self._text_queue.append((error, 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
"""
2014-06-26 07:58:00 +02:00
self._disp_text(text, True, 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
"""
2014-06-26 07:58:00 +02:00
self._disp_text(text, False, 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.normaltext = 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."""
if mode in modeman.instance().passthrough:
2014-07-28 22:40:58 +02:00
self.txt.normaltext = "-- {} MODE --".format(mode.name.upper())
2014-08-26 19:10:14 +02:00
if mode == usertypes.KeyMode.insert:
2014-06-23 16:43:59 +02:00
self.insert_active = True
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_left(self, mode):
"""Clear marked mode."""
if mode in modeman.instance().passthrough:
self.txt.normaltext = ""
2014-08-26 19:10:14 +02:00
if mode == usertypes.KeyMode.insert:
2014-06-23 16:43:59 +02:00
self.insert_active = False
2014-05-13 07:10:50 +02:00
2014-05-16 15:33:36 +02:00
@pyqtSlot(str, str)
def on_config_changed(self, section, option):
"""Update message timeout when config changed."""
2014-05-17 23:45:31 +02:00
if section == 'ui' and option == 'message-timeout':
2014-05-18 00:36:29 +02:00
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.
Emit:
resized: Always emitted.
"""
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.
Emit:
moved: Always emitted.
"""
super().moveEvent(e)
self.moved.emit(e.pos())