qutebrowser/qutebrowser/widgets/statusbar.py
2014-02-13 18:57:19 +01:00

500 lines
15 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/>.
"""Widgets needed in the qutebrowser statusbar."""
import logging
from PyQt5.QtCore import pyqtSignal, Qt, pyqtProperty
from PyQt5.QtWidgets import (QLineEdit, QShortcut, QHBoxLayout, QWidget,
QSizePolicy, QProgressBar, QLabel, QStyle,
QStyleOption)
from PyQt5.QtGui import QValidator, QKeySequence, QPainter
import qutebrowser.utils.config as config
import qutebrowser.commands.keys as keys
from qutebrowser.utils.url import urlstring
class StatusBar(QWidget):
"""The statusbar at the bottom of the mainwindow."""
cmd = None
txt = None
keystring = None
percentage = None
url = None
prog = None
resized = pyqtSignal('QRect')
moved = pyqtSignal('QPoint')
_error = False
_stylesheet = """
QWidget#StatusBar[error="false"] {{
{color[statusbar.bg]}
}}
QWidget#StatusBar[error="true"] {{
{color[statusbar.bg.error]}
}}
QWidget {{
{color[statusbar.fg]}
{font[statusbar]}
}}
"""
# TODO: the statusbar should be a bit smaller
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName(self.__class__.__name__)
self.setStyleSheet(config.get_stylesheet(self._stylesheet))
self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed)
hbox = QHBoxLayout(self)
hbox.setContentsMargins(0, 0, 0, 0)
hbox.setSpacing(5)
self.cmd = Command(self)
hbox.addWidget(self.cmd)
self.txt = Text(self)
hbox.addWidget(self.txt)
hbox.addStretch()
self.keystring = KeyString(self)
hbox.addWidget(self.keystring)
self.url = Url(self)
hbox.addWidget(self.url)
self.percentage = Percentage(self)
hbox.addWidget(self.percentage)
self.prog = Progress(self)
hbox.addWidget(self.prog)
@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-sets the stylesheet after setting the value, so everything gets
updated by Qt properly.
"""
self._error = val
self.setStyleSheet(config.get_stylesheet(self._stylesheet))
def paintEvent(self, e):
"""Override QWIidget.paintEvent to handle stylesheets."""
# pylint: disable=unused-argument
option = QStyleOption()
option.initFrom(self)
painter = QPainter(self)
self.style().drawPrimitive(QStyle.PE_Widget, option, painter, self)
def disp_error(self, text):
"""Displaysan error in the statusbar."""
self.error = True
self.txt.set_error(text)
def clear_error(self):
"""Clear a displayed error from the status bar."""
self.error = False
self.txt.clear_error()
def resizeEvent(self, e):
"""Extend resizeEvent of QWidget to emit a resized signal afterwards.
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.
e -- The QMoveEvent.
"""
super().moveEvent(e)
self.moved.emit(e.pos())
class Command(QLineEdit):
"""The commandline part of the statusbar."""
# Emitted when a command is triggered by the user
got_cmd = pyqtSignal(str)
# Emitted for searches triggered by the user
got_search = pyqtSignal(str)
got_search_rev = pyqtSignal(str)
statusbar = None # The status bar object
esc_pressed = pyqtSignal() # Emitted when escape is pressed
tab_pressed = pyqtSignal(bool) # Emitted when tab is pressed (arg: shift)
hide_completion = pyqtSignal() # Hide completion window
history = [] # The command history, with newer commands at the bottom
_tmphist = []
_histpos = None
# FIXME won't the tab key switch to the next widget?
# See [0] for a possible fix.
# [0] http://www.saltycrane.com/blog/2008/01/how-to-capture-tab-key-press-event-with/ # noqa # pylint: disable=line-too-long
def __init__(self, statusbar):
super().__init__(statusbar)
# FIXME
self.statusbar = statusbar
self.setStyleSheet("""
QLineEdit {
border: 0px;
padding-left: 1px;
background-color: transparent;
}
""")
self.setValidator(CommandValidator())
self.returnPressed.connect(self.process_cmdline)
self.textEdited.connect(self._histbrowse_stop)
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Ignored)
for (key, handler) in [
(Qt.Key_Escape, self.esc_pressed),
(Qt.Key_Up, self.key_up_handler),
(Qt.Key_Down, self.key_down_handler),
(Qt.Key_Tab | Qt.SHIFT, lambda: self.tab_pressed.emit(True)),
(Qt.Key_Tab, lambda: self.tab_pressed.emit(False))
]:
sc = QShortcut(self)
sc.setKey(QKeySequence(key))
sc.setContext(Qt.WidgetWithChildrenShortcut)
sc.activated.connect(handler)
def process_cmdline(self):
"""Handle the command in the status bar."""
signals = {
':': self.got_cmd,
'/': self.got_search,
'?': self.got_search_rev,
}
self._histbrowse_stop()
text = self.text()
if not self.history or text != self.history[-1]:
self.history.append(text)
self.setText('')
if text[0] in signals:
signals[text[0]].emit(text.lstrip(text[0]))
def set_cmd(self, text):
"""Preset the statusbar to some text."""
self.setText(text)
self.setFocus()
def append_cmd(self, text):
"""Append text to the commandline."""
# FIXME do the right thing here
self.setText(':' + text)
self.setFocus()
def focusOutEvent(self, e):
"""Clear the statusbar text if it's explicitely unfocused."""
if e.reason() in [Qt.MouseFocusReason, Qt.TabFocusReason,
Qt.BacktabFocusReason, Qt.OtherFocusReason]:
self.setText('')
self._histbrowse_stop()
self.hide_completion.emit()
super().focusOutEvent(e)
def focusInEvent(self, e):
"""Clear error message when the statusbar is focused."""
self.statusbar.clear_error()
super().focusInEvent(e)
def _histbrowse_start(self):
"""Start browsing to the history.
Called when the user presses the up/down key and wasn't browsing the
history already.
"""
pre = self.text().strip()
logging.debug('Preset text: "{}"'.format(pre))
if pre:
self._tmphist = [e for e in self.history if e.startswith(pre)]
else:
self._tmphist = self.history
self._histpos = len(self._tmphist) - 1
def _histbrowse_stop(self):
"""Stop browsing the history."""
self._histpos = None
def key_up_handler(self):
"""Handle Up presses (go back in history)."""
logging.debug("history up [pre]: pos {}".format(self._histpos))
if self._histpos is None:
self._histbrowse_start()
elif self._histpos <= 0:
return
else:
self._histpos -= 1
if not self._tmphist:
return
logging.debug("history up: {} / len {} / pos {}".format(
self._tmphist, len(self._tmphist), self._histpos))
self.set_cmd(self._tmphist[self._histpos])
def key_down_handler(self):
"""Handle Down presses (go forward in history)."""
logging.debug("history up [pre]: pos {}".format(self._histpos,
self._tmphist, len(self._tmphist), self._histpos))
if (self._histpos is None or
self._histpos >= len(self._tmphist) - 1 or
not self._tmphist):
return
self._histpos += 1
logging.debug("history up: {} / len {} / pos {}".format(
self._tmphist, len(self._tmphist), self._histpos))
self.set_cmd(self._tmphist[self._histpos])
class CommandValidator(QValidator):
"""Validator to prevent the : from getting deleted."""
def validate(self, string, pos):
"""Override QValidator::validate.
string -- The string to validate.
pos -- The current curser position.
Return a tuple (status, string, pos) as a QValidator should.
"""
if any(string.startswith(c) for c in keys.startchars):
return (QValidator.Acceptable, string, pos)
else:
return (QValidator.Invalid, string, pos)
class Progress(QProgressBar):
"""The progress bar part of the status bar."""
statusbar = None
# FIXME for some reason, margin-left is not shown
_stylesheet = """
QProgressBar {{
border-radius: 0px;
border: 2px solid transparent;
margin-left: 1px;
background-color: transparent;
}}
QProgressBar::chunk {{
{color[statusbar.progress.bg]}
}}
"""
def __init__(self, statusbar):
super().__init__(statusbar)
self.statusbar = statusbar
self.setStyleSheet(config.get_stylesheet(self._stylesheet))
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Ignored)
self.setTextVisible(False)
self.hide()
def on_load_started(self):
"""Clear old error and show progress, used as slot to loadStarted."""
self.setValue(0)
self.show()
class TextBase(QLabel):
"""A text in the statusbar.
Unlike QLabel, the text will get elided.
Eliding is loosly based on
http://gedgedev.blogspot.ch/2010/12/elided-labels-in-qt.html
"""
elidemode = None
_elided_text = None
def __init__(self, bar, elidemode=Qt.ElideRight):
super().__init__(bar)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum)
self.elidemode = elidemode
def setText(self, txt):
"""Extend QLabel::setText to update the elided text afterwards."""
super().setText(txt)
self._update_elided_text(self.geometry().width())
def resizeEvent(self, e):
"""Extend QLabel::resizeEvent to update the elided text afterwards."""
super().resizeEvent(e)
self._update_elided_text(e.size().width())
def _update_elided_text(self, width):
"""Update the elided text when necessary.
width -- The maximal width the text should take.
"""
self._elided_text = self.fontMetrics().elidedText(
self.text(), self.elidemode, width, Qt.TextShowMnemonic)
def paintEvent(self, e):
"""Override QLabel::paintEvent to draw elided text."""
if self.elidemode == Qt.ElideNone:
super().paintEvent(e)
else:
painter = QPainter(self)
geom = self.geometry()
painter.drawText(0, 0, geom.width(), geom.height(),
self.alignment(), self._elided_text)
class Text(TextBase):
"""Text displayed in the statusbar."""
old_text = ''
def set_error(self, text):
"""Display an error message and save current text in old_text."""
self.old_text = self.text()
self.setText(text)
def clear_error(self):
"""Clear a displayed error message."""
self.setText(self.old_text)
class KeyString(TextBase):
"""Keychain string displayed in the statusbar."""
pass
class Percentage(TextBase):
"""Reading percentage displayed in the statusbar."""
def set_perc(self, x, y):
"""Setter to be used as a Qt slot."""
# pylint: disable=unused-argument
if y == 0:
self.setText('[top]')
elif y == 100:
self.setText('[bot]')
else:
self.setText('[{:2}%]'.format(y))
class Url(TextBase):
"""URL displayed in the statusbar."""
_old_url = None
_old_urltype = None
_urltype = None # 'normal', 'ok', 'error', 'warn, 'hover'
_stylesheet = """
QLabel#Url[urltype="normal"] {{
{color[statusbar.url.fg]}
}}
QLabel#Url[urltype="success"] {{
{color[statusbar.url.fg.success]}
}}
QLabel#Url[urltype="error"] {{
{color[statusbar.url.fg.error]}
}}
QLabel#Url[urltype="warn"] {{
{color[statusbar.url.fg.warn]}
}}
QLabel#Url[urltype="hover"] {{
{color[statusbar.url.fg.hover]}
}}
"""
def __init__(self, bar, elidemode=Qt.ElideMiddle):
"""Override TextBase::__init__ to elide in the middle by default."""
super().__init__(bar, elidemode)
self.setObjectName(self.__class__.__name__)
self.setStyleSheet(config.get_stylesheet(self._stylesheet))
@pyqtProperty(str)
def urltype(self):
"""Getter for self.urltype, so it can be used as Qt property."""
# pylint: disable=method-hidden
return self._urltype
@urltype.setter
def urltype(self, val):
"""Setter for self.urltype, so it can be used as Qt property."""
self._urltype = val
self.setStyleSheet(config.get_stylesheet(self._stylesheet))
def on_loading_finished(self, ok):
"""Slot for cur_loading_finished. Colors the URL according to ok."""
# FIXME: set color to warn if there was an SSL error
self.urltype = 'success' if ok else 'error'
def set_url(self, s):
"""Setter to be used as a Qt slot."""
self.setText(urlstring(s))
self.urltype = 'normal'
def set_hover_url(self, link, title, text):
"""Setter to be used as a Qt slot.
Saves old shown URL in self._old_url and restores it later if a link is
"un-hovered" when it gets called with empty parameters.
"""
# pylint: disable=unused-argument
if link:
if self._old_url is None:
self._old_url = self.text()
if self._old_urltype is None:
self._old_urltype = self._urltype
self.urltype = 'hover'
self.setText(link)
else:
self.setText(self._old_url)
self.urltype = self._old_urltype
self._old_url = None
self._old_urltype = None