qutebrowser/qutebrowser/widgets/statusbar.py

440 lines
13 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-02-10 15:01:05 +01:00
"""Widgets needed in the qutebrowser statusbar."""
2014-01-27 21:42:00 +01:00
import logging
from PyQt5.QtCore import pyqtSignal, Qt, pyqtProperty
2014-02-10 15:01:05 +01:00
from PyQt5.QtWidgets import (QLineEdit, QShortcut, QHBoxLayout, QWidget,
QSizePolicy, QProgressBar, QLabel, QStyle,
QStyleOption)
from PyQt5.QtGui import QValidator, QKeySequence, QPainter
2014-01-27 21:42:00 +01:00
2014-02-10 15:01:05 +01:00
import qutebrowser.utils.config as config
import qutebrowser.commands.keys as keys
2014-02-10 17:55:03 +01:00
from qutebrowser.utils.url import urlstring
2014-01-27 21:42:00 +01:00
2014-02-10 15:01:05 +01:00
class StatusBar(QWidget):
"""The statusbar at the bottom of the mainwindow."""
cmd = None
txt = None
keystring = None
percentage = None
url = None
2014-02-10 15:01:05 +01:00
prog = None
resized = pyqtSignal('QRect')
moved = pyqtSignal('QPoint')
_error = False
2014-02-10 15:01:05 +01:00
_stylesheet = """
QWidget#StatusBar[error="false"] {{
{color[statusbar.bg]}
}}
QWidget#StatusBar[error="true"] {{
{color[statusbar.bg.error]}
}}
QWidget {{
{color[statusbar.fg]}
2014-02-10 15:01:05 +01:00
{font[statusbar]}
}}
"""
# TODO: the statusbar should be a bit smaller
2014-02-12 22:46:06 +01:00
# FIXME In general, texts should be elided instead of cut off.
# See http://gedgedev.blogspot.ch/2010/12/elided-labels-in-qt.html
2014-02-12 20:51:50 +01:00
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName(self.__class__.__name__)
2014-02-10 15:01:05 +01:00
self.setStyleSheet(config.get_stylesheet(self._stylesheet))
2014-02-12 22:46:06 +01:00
self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed)
2014-02-10 15:01:05 +01:00
hbox = QHBoxLayout(self)
hbox.setContentsMargins(0, 0, 0, 0)
hbox.setSpacing(5)
2014-02-10 15:01:05 +01:00
self.cmd = Command(self)
hbox.addWidget(self.cmd)
2014-02-10 15:01:05 +01:00
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)
2014-02-10 15:01:05 +01:00
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)
2014-02-10 15:01:05 +01:00
def disp_error(self, text):
"""Displaysan error in the statusbar."""
self.error = True
self.txt.set_error(text)
2014-02-10 15:01:05 +01:00
def clear_error(self):
"""Clear a displayed error from the status bar."""
self.error = False
self.txt.clear_error()
2014-02-10 15:01:05 +01:00
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())
2014-01-27 21:42:00 +01:00
class Command(QLineEdit):
2014-02-07 20:21:50 +01:00
2014-01-29 15:30:19 +01:00
"""The commandline part of the statusbar."""
2014-02-07 20:21:50 +01:00
2014-01-28 23:04:02 +01:00
# Emitted when a command is triggered by the user
got_cmd = pyqtSignal(str)
2014-01-29 21:06:56 +01:00
# Emitted for searches triggered by the user
got_search = pyqtSignal(str)
got_search_rev = pyqtSignal(str)
2014-01-29 08:36:44 +01:00
statusbar = None # The status bar object
2014-01-28 23:04:02 +01:00
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
2014-01-27 21:42:00 +01:00
_tmphist = []
_histpos = None
# FIXME won't the tab key switch to the next widget?
2014-01-29 08:36:44 +01:00
# 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
2014-01-27 21:42:00 +01:00
2014-01-29 08:36:44 +01:00
def __init__(self, statusbar):
super().__init__(statusbar)
2014-01-27 21:42:00 +01:00
# FIXME
2014-01-29 08:36:44 +01:00
self.statusbar = statusbar
self.setStyleSheet("""
QLineEdit {
border: 0px;
padding-left: 1px;
background-color: transparent;
}
""")
2014-02-10 15:01:05 +01:00
self.setValidator(CommandValidator())
self.returnPressed.connect(self.process_cmdline)
2014-01-27 21:42:00 +01:00
self.textEdited.connect(self._histbrowse_stop)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored)
2014-01-27 21:42:00 +01:00
2014-01-29 15:30:19 +01:00
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))
]:
2014-01-27 21:42:00 +01:00
sc = QShortcut(self)
sc.setKey(QKeySequence(key))
sc.setContext(Qt.WidgetWithChildrenShortcut)
sc.activated.connect(handler)
def process_cmdline(self):
2014-01-29 15:30:19 +01:00
"""Handle the command in the status bar."""
2014-01-29 21:06:56 +01:00
signals = {
':': self.got_cmd,
'/': self.got_search,
'?': self.got_search_rev,
}
2014-01-27 21:42:00 +01:00
self._histbrowse_stop()
text = self.text()
2014-01-27 21:42:00 +01:00
if not self.history or text != self.history[-1]:
self.history.append(text)
self.setText('')
2014-01-29 21:06:56 +01:00
if text[0] in signals:
signals[text[0]].emit(text.lstrip(text[0]))
2014-01-27 21:42:00 +01:00
def set_cmd(self, text):
2014-01-29 15:30:19 +01:00
"""Preset the statusbar to some text."""
self.setText(text)
2014-01-27 21:42:00 +01:00
self.setFocus()
def append_cmd(self, text):
2014-01-29 15:30:19 +01:00
"""Append text to the commandline."""
2014-01-27 21:42:00 +01:00
# FIXME do the right thing here
self.setText(':' + text)
self.setFocus()
def focusOutEvent(self, e):
2014-01-29 15:30:19 +01:00
"""Clear the statusbar text if it's explicitely unfocused."""
2014-01-27 21:42:00 +01:00
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):
2014-01-29 15:30:19 +01:00
"""Clear error message when the statusbar is focused."""
2014-01-29 08:36:44 +01:00
self.statusbar.clear_error()
2014-01-27 21:42:00 +01:00
super().focusInEvent(e)
def _histbrowse_start(self):
2014-01-29 15:30:19 +01:00
"""Start browsing to the history.
Called when the user presses the up/down key and wasn't browsing the
history already.
2014-02-07 20:21:50 +01:00
"""
pre = self.text().strip()
2014-01-27 21:42:00 +01:00
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):
2014-01-29 15:30:19 +01:00
"""Stop browsing the history."""
2014-01-27 21:42:00 +01:00
self._histpos = None
def key_up_handler(self):
2014-01-29 15:30:19 +01:00
"""Handle Up presses (go back in history)."""
2014-01-27 21:42:00 +01:00
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):
2014-01-29 15:30:19 +01:00
"""Handle Down presses (go forward in history)."""
2014-01-27 21:42:00 +01:00
logging.debug("history up [pre]: pos {}".format(self._histpos,
2014-01-28 23:04:02 +01:00
self._tmphist, len(self._tmphist), self._histpos))
2014-01-27 21:42:00 +01:00
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])
2014-01-28 23:04:02 +01:00
2014-02-10 15:01:05 +01:00
class CommandValidator(QValidator):
2014-01-29 15:30:19 +01:00
2014-02-07 20:21:50 +01:00
"""Validator to prevent the : from getting deleted."""
2014-01-29 15:30:19 +01:00
2014-02-07 20:21:50 +01:00
def validate(self, string, pos):
"""Override QValidator::validate.
2014-01-29 15:30:19 +01:00
string -- The string to validate.
pos -- The current curser position.
2014-02-07 20:21:50 +01:00
Returns a tuple (status, string, pos) as a QValidator should.
2014-01-29 15:30:19 +01:00
"""
if any(string.startswith(c) for c in keys.startchars):
2014-01-27 21:42:00 +01:00
return (QValidator.Acceptable, string, pos)
else:
return (QValidator.Invalid, string, pos)
2014-02-10 15:01:05 +01:00
class Progress(QProgressBar):
"""The progress bar part of the status bar."""
statusbar = None
_error = False
# FIXME for some reason, margin-left is not shown
2014-02-10 15:01:05 +01:00
_stylesheet = """
QProgressBar {{
border-radius: 0px;
border: 2px solid transparent;
margin-left: 1px;
background-color: transparent;
}}
QProgressBar[error="false"]::chunk {{
{color[statusbar.progress.bg]}
2014-02-10 15:01:05 +01:00
}}
QProgressBar[error="true"]::chunk {{
{color[statusbar.progress.bg.error]}
2014-02-10 15:01:05 +01:00
}}
"""
def __init__(self, statusbar):
super().__init__(statusbar)
self.statusbar = statusbar
self.setStyleSheet(config.get_stylesheet(self._stylesheet))
2014-02-10 15:01:05 +01:00
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Ignored)
self.setTextVisible(False)
self.hide()
@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))
2014-02-10 15:01:05 +01:00
2014-02-12 17:13:31 +01:00
def on_load_started(self):
"""Clear old error and show progress, used as slot to loadStarted."""
self.setValue(0)
self.error = False
self.show()
2014-02-10 15:01:05 +01:00
def load_finished(self, ok):
"""Hide the progress bar or color it red, depending on ok.
Slot for the loadFinished signal of a QWebView.
"""
self.error = not ok
2014-02-10 15:01:05 +01:00
if ok:
self.hide()
class TextBase(QLabel):
2014-02-10 15:01:05 +01:00
"""A text in the statusbar."""
2014-02-10 15:01:05 +01:00
def __init__(self, bar):
super().__init__(bar)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum)
2014-02-10 15:01:05 +01:00
class Text(TextBase):
2014-02-10 17:55:03 +01:00
"""Text displayed in the statusbar."""
2014-02-10 15:01:05 +01:00
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
2014-02-10 15:01:05 +01:00
class Percentage(TextBase):
"""Reading percentage displayed in the statusbar."""
def set_perc(self, x, y):
2014-02-10 15:01:05 +01:00
"""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 = ''
2014-02-10 15:01:05 +01:00
2014-02-10 17:55:03 +01:00
def set_url(self, s):
"""Setter to be used as a Qt slot."""
self.setText(urlstring(s))
2014-02-10 17:55:03 +01:00
2014-02-11 13:27:48 +01:00
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:
self.old_url = self.text()
self.setText(link)
2014-02-11 13:27:48 +01:00
else:
self.setText(self.old_url)