client/securedrop_client/gui/base/misc.py (104 lines of code) (raw):
"""
Generic custom widgets.
Copyright (C) 2018 The Freedom of the Press Foundation.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
from PyQt5.QtCore import QSize, Qt
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QHBoxLayout, QLabel, QPushButton, QWidget
from securedrop_client.resources import load_icon, load_svg
class SvgToggleButton(QPushButton):
"""
A toggle button used to display the contents of Scalable Vector Graphics (SVG) files provided
for an on and off state.
Parameters
----------
on: str
The name of the SVG file to add to the button for on state.
off: str
The name of the SVG file to add to the button for off state.
svg_size: QSize, optional
The display size of the SVG, defaults to filling the entire size of the widget.
"""
def __init__(self, on: str, off: str, svg_size: QSize | None = None):
super().__init__()
# Set layout
layout = QHBoxLayout(self)
self.setLayout(layout)
# Remove margins and spacing
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# Add SVG icon and set its size
self._icon = load_icon(normal=on, normal_off=off)
self.setIcon(self._icon)
self.setIconSize(svg_size) if svg_size else self.setIconSize(QSize())
# Make this a toggle button
self.setCheckable(True)
def set_icon(self, on: str, off: str) -> None:
self._icon = load_icon(normal=on, normal_off=off)
self.setIcon(self._icon)
class SvgPushButton(QPushButton):
"""
A widget used to display the contents of Scalable Vector Graphics (SVG) files provided for
associated user action modes, see https://doc.qt.io/qt-5/qicon.html#Mode-enum.
Parameters
----------
normal: str
The name of the SVG file to add to the button for QIcon.Normal mode.
disabled: str, optional
The name of the SVG file to add to the button for QIcon.Disabled mode.
active: str, optional
The name of the SVG file to add to the button for QIcon.Active mode.
selected: str, optional
The name of the SVG file to add to the button for QIcon.Selected mode.
svg_size: QSize, optional
The display size of the SVG, defaults to filling the entire size of the widget.
"""
def __init__(
self,
normal: str,
disabled: str | None = None,
active: str | None = None,
selected: str | None = None,
svg_size: QSize | None = None,
) -> None:
super().__init__()
# Set layout
layout = QHBoxLayout(self)
self.setLayout(layout)
# Remove margins and spacing
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# Add SVG icon and set its size
self._icon: QIcon = load_icon(
normal=normal,
disabled=disabled,
active=active,
selected=selected,
disabled_off=disabled,
)
self.setIcon(self._icon)
self.setIconSize(svg_size) if svg_size else self.setIconSize(QSize())
class SvgLabel(QLabel):
"""
A widget used to display the contents of a Scalable Vector Graphics (SVG) file.
Parameters
----------
filename: str
The name of the SVG file to add to the label.
svg_size: QSize, optional
The display size of the SVG, defaults to filling the entire size of the widget.
"""
def __init__(self, filename: str, svg_size: QSize | None = None) -> None:
super().__init__()
# Remove margins and spacing
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
self.setLayout(layout)
# Add SVG and set its size
self.svg = load_svg(filename)
self.svg.setFixedSize(svg_size) if svg_size else self.svg.setFixedSize(QSize())
layout.addWidget(self.svg)
def update_image(self, filename: str, svg_size: QSize | None = None) -> None:
self.svg = load_svg(filename)
self.svg.setFixedSize(svg_size) if svg_size else self.svg.setFixedSize(QSize())
child = self.layout().takeAt(0)
if child and child.widget():
child.widget().deleteLater()
self.layout().addWidget(self.svg)
class SecureQLabel(QLabel):
MAX_PREVIEW_LENGTH = 200
def __init__(
self,
text: str = "",
parent: QWidget | None = None,
flags: Qt.WindowFlags | Qt.WindowType = Qt.WindowFlags(),
wordwrap: bool = True,
max_length: int = 0,
with_tooltip: bool = False,
):
super().__init__(parent, flags)
self.wordwrap = wordwrap
self.max_length = max_length
self.setWordWrap(wordwrap) # If True, wraps text at default of 70 characters
self.with_tooltip = with_tooltip
self.setText(text)
self.elided = self.text() != text
def setText(self, text: str) -> None:
text = text.strip()
self.setTextFormat(Qt.PlainText)
self.preview_text = text[: self.MAX_PREVIEW_LENGTH]
elided_text = self.get_elided_text(text)
self.elided = elided_text != text
if self.elided and self.with_tooltip:
tooltip_label = SecureQLabel(text)
self.setToolTip(tooltip_label.text())
super().setText(elided_text)
def refresh_preview_text(self) -> None:
self.setText(self.preview_text)
def get_elided_text(self, full_text: str) -> str:
if not self.max_length:
return full_text
# Only allow one line of elided text
if "\n" in full_text:
full_text = full_text.split("\n", 1)[0]
fm = self.fontMetrics()
px_width = fm.horizontalAdvance(full_text)
if px_width > self.max_length:
elided_text = ""
for c in full_text:
if fm.horizontalAdvance(elided_text) > self.max_length:
return elided_text[:-3] + "…"
elided_text = elided_text + c
return full_text
def is_elided(self) -> bool:
return self.elided