Add versions for PySide6, PyQt6 & PySide2.

Break down examples into module files to make easier to read. Use
full-definitions on Enums (PyQt6 compatible, better documenting).
Add fixes for Qt6 versions & some general bugfixes.
This commit is contained in:
Martin Fitzpatrick
2024-02-19 13:36:32 +01:00
parent 38118a64a4
commit b74592ea41
1448 changed files with 146610 additions and 27745 deletions

View File

@@ -0,0 +1 @@
from .colorbutton import ColorButton

View File

@@ -0,0 +1,58 @@
from PyQt5.QtCore import Qt
from PyQt5.QtCore import pyqtSignal as Signal
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import QColorDialog, QPushButton
class ColorButton(QPushButton):
"""
Custom Qt Widget to show a chosen color.
Left-clicking the button shows the color-chooser, while
right-clicking resets the color to the default color (None by default).
"""
colorChanged = Signal(object)
def __init__(self, *args, color=None, **kwargs):
super(ColorButton, self).__init__(*args, **kwargs)
self.setObjectName("ColorButton")
self._color = None
self._default = color
self.pressed.connect(self.onColorPicker)
# Set the initial/default state.
self.setColor(self._default)
def setColor(self, color):
if color != self._color:
self._color = color
self.colorChanged.emit(color)
if self._color:
self.setStyleSheet(f"#ColorButton {{background-color: {self._color};}}")
else:
self.setStyleSheet("")
def color(self):
return self._color
def onColorPicker(self):
"""
Show color-picker dialog to select color.
Qt will use the native dialog by default.
"""
dlg = QColorDialog(self)
if self._color:
dlg.setCurrentColor(QColor(self._color))
if dlg.exec_():
self.setColor(dlg.currentColor().name())
def mousePressEvent(self, e):
if e.button() == Qt.MouseButton.RightButton:
self.setColor(self._default)
return super().mousePressEvent(e)

View File

@@ -0,0 +1,23 @@
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
from colorbutton import ColorButton
class Window(QMainWindow):
def __init__(self):
super().__init__()
palette = ColorButton(color="red")
palette.colorChanged.connect(self.show_selected_color)
self.setCentralWidget(palette)
def show_selected_color(self, c):
print("Selected: {}".format(c))
app = QApplication(sys.argv)
w = Window()
w.show()
app.exec_()

View File

@@ -0,0 +1 @@
from .equalizer_bar import EqualizerBar

View File

@@ -0,0 +1,142 @@
from PyQt5.QtCore import QRect, QRectF, QSize, Qt, QTimer
from PyQt5.QtGui import QBrush, QColor, QPainter
from PyQt5.QtWidgets import QSizePolicy, QWidget
class EqualizerBar(QWidget):
def __init__(self, bars, steps):
super().__init__()
self.setSizePolicy(
QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding
)
if isinstance(steps, list):
# list of colours.
self.n_steps = len(steps)
self.steps = steps
elif isinstance(steps, int):
# int number of bars, defaults to red.
self.n_steps = steps
self.steps = ["red"] * steps
else:
raise TypeError("steps must be a list or int")
# Bar appearance.
self.n_bars = bars
self._x_solid_percent = 0.8
self._y_solid_percent = 0.8
self._background_color = QColor("black")
self._padding = 25 # n-pixel gap around edge.
# Bar behaviour
self._timer = None
self.setDecayFrequencyMs(100)
self._decay = 10
# Ranges
self._vmin = 0
self._vmax = 100
# Current values are stored in a list.
self._values = [0.0] * bars
def paintEvent(self, e):
painter = QPainter(self)
brush = QBrush()
brush.setColor(self._background_color)
brush.setStyle(Qt.BrushStyle.SolidPattern)
rect = QRect(0, 0, painter.device().width(), painter.device().height())
painter.fillRect(rect, brush)
# Define our canvas.
d_height = painter.device().height() - (self._padding * 2)
d_width = painter.device().width() - (self._padding * 2)
# Draw the bars.
step_y = d_height / self.n_steps
bar_height = step_y * self._y_solid_percent
bar_height_space = step_y * (1 - self._x_solid_percent) / 2
step_x = d_width / self.n_bars
bar_width = step_x * self._x_solid_percent
bar_width_space = step_x * (1 - self._y_solid_percent) / 2
for b in range(self.n_bars):
# Calculate the y-stop position for this bar, from the value in range.
pc = (self._values[b] - self._vmin) / (self._vmax - self._vmin)
n_steps_to_draw = int(pc * self.n_steps)
for n in range(n_steps_to_draw):
brush.setColor(QColor(self.steps[n]))
rect = QRectF(
self._padding + (step_x * b) + bar_width_space,
self._padding + d_height - ((1 + n) * step_y) + bar_height_space,
bar_width,
bar_height,
)
painter.fillRect(rect, brush)
painter.end()
def sizeHint(self):
return QSize(20, 120)
def _trigger_refresh(self):
self.update()
def setDecay(self, f):
self._decay = float(f)
def setDecayFrequencyMs(self, ms):
if self._timer:
self._timer.stop()
if ms:
self._timer = QTimer()
self._timer.setInterval(ms)
self._timer.timeout.connect(self._decay_beat)
self._timer.start()
def _decay_beat(self):
self._values = [max(0, v - self._decay) for v in self._values]
self.update() # Redraw new position.
def setValues(self, v):
self._values = v
self.update()
def values(self):
return self._values
def setRange(self, vmin, vmax):
assert float(vmin) < float(vmax)
self._vmin, self._vmax = float(vmin), float(vmax)
def setColor(self, color):
self.steps = [color] * self._bar.n_steps
self.update()
def setColors(self, colors):
self.n_steps = len(colors)
self.steps = colors
self.update()
def setBarPadding(self, i):
self._padding = int(i)
self.update()
def setBarSolidXPercent(self, f):
self._x_solid_percent = float(f)
self.update()
def setBarSolidYPercent(self, f):
self._y_solid_percent = float(f)
self.update()
def setBackgroundColor(self, color):
self._background_color = QColor(color)
self.update()

View File

@@ -0,0 +1,51 @@
import random
import sys
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QApplication, QMainWindow
from equalizer_bar import EqualizerBar
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.equalizer = EqualizerBar(
5,
[
"#0C0786",
"#40039C",
"#6A00A7",
"#8F0DA3",
"#B02A8F",
"#CA4678",
"#E06461",
"#F1824C",
"#FCA635",
"#FCCC25",
"#EFF821",
],
)
self.equalizer.setBarSolidYPercent(0.4)
# self.equalizer.setBarSolidXPercent(0.4)
self.setCentralWidget(self.equalizer)
self._timer = QTimer()
self._timer.setInterval(100)
self._timer.timeout.connect(self.update_values)
self._timer.start()
def update_values(self):
self.equalizer.setValues(
[
min(100, v + random.randint(0, 50) if random.randint(0, 5) > 2 else v)
for v in self.equalizer.values()
]
)
app = QApplication(sys.argv)
w = Window()
w.show()
app.exec_()

View File

@@ -0,0 +1 @@
from .gradient import Gradient

View File

@@ -0,0 +1,190 @@
from PyQt5.QtCore import QRect, QRectF, QSize, Qt, pyqtSignal
from PyQt5.QtGui import QColor, QLinearGradient, QPainter, QPen
from PyQt5.QtWidgets import QColorDialog, QSizePolicy, QWidget
class Gradient(QWidget):
gradientChanged = pyqtSignal()
def __init__(self, gradient=None):
super().__init__()
self.setSizePolicy(
QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding
)
if gradient:
self._gradient = gradient
else:
self._gradient = [
(0.0, "#000000"),
(1.0, "#ffffff"),
]
# Stop point handle sizes.
self._handle_w = 10
self._handle_h = 10
self._drag_position = None
def paintEvent(self, e):
painter = QPainter(self)
width = painter.device().width()
height = painter.device().height()
# Draw the linear horizontal gradient.
gradient = QLinearGradient(0, 0, width, 0)
for stop, color in self._gradient:
gradient.setColorAt(stop, QColor(color))
rect = QRect(0, 0, width, height)
painter.fillRect(rect, gradient)
pen = QPen()
y = painter.device().height() / 2
# Draw the stop handles.
for stop, _ in self._gradient:
pen.setColor(QColor("white"))
painter.setPen(pen)
painter.drawLine(
int(stop * width),
int(y - self._handle_h),
int(stop * width),
int(y + self._handle_h),
)
pen.setColor(QColor("red"))
painter.setPen(pen)
rect = QRectF(
stop * width - self._handle_w / 2,
y - self._handle_h / 2,
self._handle_w,
self._handle_h,
)
painter.drawRect(rect)
painter.end()
def sizeHint(self):
return QSize(200, 50)
def _sort_gradient(self):
self._gradient = sorted(self._gradient, key=lambda g: g[0])
def _constrain_gradient(self):
self._gradient = [
# Ensure values within valid range.
(max(0.0, min(1.0, stop)), color)
for stop, color in self._gradient
]
def setGradient(self, gradient):
assert all([0.0 <= stop <= 1.0 for stop, _ in gradient])
self._gradient = gradient
self._constrain_gradient()
self._sort_gradient()
self.gradientChanged.emit()
def gradient(self):
return self._gradient
@property
def _end_stops(self):
return [0, len(self._gradient) - 1]
def addStop(self, stop, color=None):
# Stop is a value 0...1, find the point to insert this stop
# in the list.
assert 0.0 <= stop <= 1.0
for n, g in enumerate(self._gradient):
if g[0] > stop:
# Insert before this entry, with specified or next color.
self._gradient.insert(n, (stop, color or g[1]))
break
self._constrain_gradient()
self.gradientChanged.emit()
self.update()
def removeStopAtPosition(self, n):
if n not in self._end_stops:
del self._gradient[n]
self.gradientChanged.emit()
self.update()
def setColorAtPosition(self, n, color):
if n < len(self._gradient):
stop, _ = self._gradient[n]
self._gradient[n] = stop, color
self.gradientChanged.emit()
self.update()
def chooseColorAtPosition(self, n, current_color=None):
dlg = QColorDialog(self)
if current_color:
dlg.setCurrentColor(QColor(current_color))
if dlg.exec_():
self.setColorAtPosition(n, dlg.currentColor().name())
def _find_stop_handle_for_event(self, e, to_exclude=None):
width = self.width()
height = self.height()
midpoint = height / 2
# Are we inside a stop point? First check y.
if e.y() >= midpoint - self._handle_h and e.y() <= midpoint + self._handle_h:
for n, (stop, color) in enumerate(self._gradient):
if to_exclude and n in to_exclude:
# Allow us to skip the extreme ends of the gradient.
continue
if (
e.x() >= stop * width - self._handle_w
and e.x() <= stop * width + self._handle_w
):
return n
def mousePressEvent(self, e):
# We're in this stop point.
if e.button() == Qt.MouseButton.RightButton:
n = self._find_stop_handle_for_event(e)
if n is not None:
_, color = self._gradient[n]
self.chooseColorAtPosition(n, color)
elif e.button() == Qt.MouseButton.LeftButton:
n = self._find_stop_handle_for_event(e, to_exclude=self._end_stops)
if n is not None:
# Activate drag mode.
self._drag_position = n
def mouseReleaseEvent(self, e):
self._drag_position = None
self._sort_gradient()
def mouseMoveEvent(self, e):
# If drag active, move the stop.
if self._drag_position:
stop = e.x() / self.width()
_, color = self._gradient[self._drag_position]
self._gradient[self._drag_position] = stop, color
self._constrain_gradient()
self.update()
def mouseDoubleClickEvent(self, e):
# Calculate the position of the click relative 0..1 to the width.
n = self._find_stop_handle_for_event(e)
if n:
self._sort_gradient() # Ensure ordered.
# Delete existing, if not at the ends.
if n > 0 and n < len(self._gradient) - 1:
self.removeStopAtPosition(n)
else:
stop = e.x() / self.width()
self.addStop(stop)

View File

@@ -0,0 +1,20 @@
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
from gradient import Gradient
class Window(QMainWindow):
def __init__(self):
super().__init__()
gradient = Gradient()
gradient.setGradient([(0, "black"), (1, "green"), (0.5, "red")])
self.setCentralWidget(gradient)
app = QApplication(sys.argv)
w = Window()
w.show()
app.exec_()

View File

@@ -0,0 +1 @@
from .paint import Paint

View File

@@ -0,0 +1,21 @@
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
from paint import PaintWidget
class Window(QMainWindow):
def __init__(self):
super().__init__()
paint = PaintWidget(300, 300)
paint.setPenWidth(5)
paint.setPenColor("#EB5160")
self.setCentralWidget(paint)
app = QApplication(sys.argv)
w = Window()
w.show()
app.exec_()

View File

@@ -0,0 +1,95 @@
from PyQt5.QtCore import QPoint, Qt
from PyQt5.QtGui import QBrush, QColor, QPainter, QPen, QPixmap
from PyQt5.QtWidgets import QLabel
class PaintWidget(QLabel):
def __init__(self, width, height, background="white", *args, **kwargs):
super().__init__(*args, **kwargs)
pixmap = QPixmap(width, height)
self.setPixmap(pixmap)
# Fill the canvas with the initial color.
painter = QPainter(self.pixmap())
brush = QBrush()
brush.setColor(QColor(background))
brush.setStyle(Qt.BrushStyle.SolidPattern)
painter.fillRect(0, 0, pixmap.width(), pixmap.height(), brush)
painter.end()
self.last_x, self.last_y = None, None
self._pen_color = QColor("#000000")
self._pen_width = 4
def setPenColor(self, c):
self._pen_color = QColor(c)
def setPenWidth(self, w):
self._pen_width = int(w)
def mouseMoveEvent(self, e):
if self.last_x is None: # First event.
self.last_x = e.x()
self.last_y = e.y()
return #  Ignore the first time.
painter = QPainter(self.pixmap())
p = painter.pen()
p.setWidth(self._pen_width)
p.setColor(self._pen_color)
painter.setPen(p)
painter.drawLine(self.last_x, self.last_y, e.x(), e.y())
painter.end()
self.update()
# Update the origin for next time.
self.last_x = e.x()
self.last_y = e.y()
def mousePressEvent(self, e):
if e.button() == Qt.MouseButton.RightButton:
self._flood_fill_from_event(e)
def mouseReleaseEvent(self, e):
self.last_x = None
self.last_y = None
def _flood_fill_from_event(self, e):
image = self.pixmap().toImage()
w, h = image.width(), image.height()
x, y = e.x(), e.y()
# Get our target color from origin.
target_color = image.pixel(x, y)
have_seen = set()
queue = [(x, y)]
def get_cardinal_points(have_seen, center_pos):
points = []
cx, cy = center_pos
for x, y in [(1, 0), (0, 1), (-1, 0), (0, -1)]:
xx, yy = cx + x, cy + y
if (
xx >= 0
and xx < w
and yy >= 0
and yy < h
and (xx, yy) not in have_seen
):
points.append((xx, yy))
have_seen.add((xx, yy))
return points
# Now perform the search and fill.
painter = QPainter(self.pixmap())
painter.setPen(QPen(self._pen_color))
while queue:
x, y = queue.pop()
if image.pixel(x, y) == target_color:
painter.drawPoint(QPoint(x, y))
queue.extend(get_cardinal_points(have_seen, (x, y)))
self.update()

View File

@@ -0,0 +1 @@
from .palette import PaletteGrid, PaletteHorizontal, PaletteVertical

View File

@@ -0,0 +1,25 @@
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
from palette import PaletteGrid, PaletteHorizontal, PaletteVertical
class Window(QMainWindow):
def __init__(self):
super().__init__()
# PaletteGrid or PaletteHorizontal, or PaletteVertical
palette = PaletteGrid("17undertones")
palette.selected.connect(self.show_selected_color)
self.setCentralWidget(palette)
self.show()
def show_selected_color(self, c):
print("Selected: {}".format(c))
app = QApplication(sys.argv)
w = Window()
w.show()
app.exec_()

View File

@@ -0,0 +1,123 @@
from PyQt5.QtCore import QSize, pyqtSignal
from PyQt5.QtWidgets import QGridLayout, QHBoxLayout, QPushButton, QVBoxLayout, QWidget
PALETTES = {
# bokeh paired 12
"paired12": [
"#000000",
"#a6cee3",
"#1f78b4",
"#b2df8a",
"#33a02c",
"#fb9a99",
"#e31a1c",
"#fdbf6f",
"#ff7f00",
"#cab2d6",
"#6a3d9a",
"#ffff99",
"#b15928",
"#ffffff",
],
# d3 category 10
"category10": [
"#000000",
"#1f77b4",
"#ff7f0e",
"#2ca02c",
"#d62728",
"#9467bd",
"#8c564b",
"#e377c2",
"#7f7f7f",
"#bcbd22",
"#17becf",
"#ffffff",
],
# 17 undertones https://lospec.com/palette-list/17undertones
"17undertones": [
"#000000",
"#141923",
"#414168",
"#3a7fa7",
"#35e3e3",
"#8fd970",
"#5ebb49",
"#458352",
"#dcd37b",
"#fffee5",
"#ffd035",
"#cc9245",
"#a15c3e",
"#a42f3b",
"#f45b7a",
"#c24998",
"#81588d",
"#bcb0c2",
"#ffffff",
],
}
class _PaletteButton(QPushButton):
def __init__(self, color):
super().__init__()
self.setFixedSize(QSize(24, 24))
self.color = color
self.setStyleSheet("background-color: %s;" % color)
class _PaletteBase(QWidget):
selected = pyqtSignal(object)
def _emit_color(self, color):
self.selected.emit(color)
class _PaletteLinearBase(_PaletteBase):
def __init__(self, colors, *args, **kwargs):
super().__init__(*args, **kwargs)
if isinstance(colors, str):
if colors in PALETTES:
colors = PALETTES[colors]
palette = self.layoutvh()
for c in colors:
b = _PaletteButton(c)
b.pressed.connect(lambda c=c: self._emit_color(c))
palette.addWidget(b)
self.setLayout(palette)
class PaletteHorizontal(_PaletteLinearBase):
layoutvh = QHBoxLayout
class PaletteVertical(_PaletteLinearBase):
layoutvh = QVBoxLayout
class PaletteGrid(_PaletteBase):
def __init__(self, colors, n_columns=5, *args, **kwargs):
super().__init__(*args, **kwargs)
if isinstance(colors, str):
if colors in PALETTES:
colors = PALETTES[colors]
palette = QGridLayout()
row, col = 0, 0
for c in colors:
b = _PaletteButton(c)
b.pressed.connect(lambda c=c: self._emit_color(c))
palette.addWidget(b, row, col)
col += 1
if col == n_columns:
col = 0
row += 1
self.setLayout(palette)

View File

@@ -0,0 +1 @@
from .password import PasswordEdit

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 469.333 469.333" style="enable-background:new 0 0 469.333 469.333;" xml:space="preserve">
<g>
<g>
<g>
<path d="M234.667,170.667c-35.307,0-64,28.693-64,64s28.693,64,64,64s64-28.693,64-64S269.973,170.667,234.667,170.667z"/>
<path d="M234.667,74.667C128,74.667,36.907,141.013,0,234.667c36.907,93.653,128,160,234.667,160
c106.773,0,197.76-66.347,234.667-160C432.427,141.013,341.44,74.667,234.667,74.667z M234.667,341.333
c-58.88,0-106.667-47.787-106.667-106.667S175.787,128,234.667,128s106.667,47.787,106.667,106.667
S293.547,341.333,234.667,341.333z"/>
</g>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 469.44 469.44" style="enable-background:new 0 0 469.44 469.44;" xml:space="preserve">
<g>
<g>
<g>
<path d="M231.147,160.373l67.2,67.2l0.32-3.52c0-35.307-28.693-64-64-64L231.147,160.373z"/>
<path d="M234.667,117.387c58.88,0,106.667,47.787,106.667,106.667c0,13.76-2.773,26.88-7.573,38.933l62.4,62.4
c32.213-26.88,57.6-61.653,73.28-101.333c-37.013-93.653-128-160-234.773-160c-29.867,0-58.453,5.333-85.013,14.933l46.08,45.973
C207.787,120.267,220.907,117.387,234.667,117.387z"/>
<path d="M21.333,59.253l48.64,48.64l9.707,9.707C44.48,145.12,16.64,181.707,0,224.053c36.907,93.653,128,160,234.667,160
c33.067,0,64.64-6.4,93.547-18.027l9.067,9.067l62.187,62.293l27.2-27.093L48.533,32.053L21.333,59.253z M139.307,177.12
l32.96,32.96c-0.96,4.587-1.6,9.173-1.6,13.973c0,35.307,28.693,64,64,64c4.8,0,9.387-0.64,13.867-1.6l32.96,32.96
c-14.187,7.04-29.973,11.307-46.827,11.307C175.787,330.72,128,282.933,128,224.053C128,207.2,132.267,191.413,139.307,177.12z"
/>
</g>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
import sys
from password import PasswordEdit
from PyQt5.QtWidgets import QApplication, QMainWindow
class Window(QMainWindow):
def __init__(self):
super().__init__()
password = PasswordEdit()
self.setCentralWidget(password)
app = QApplication(sys.argv)
w = Window()
w.show()
app.exec_()

View File

@@ -0,0 +1,58 @@
import os
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QLineEdit
folder = os.path.dirname(__file__)
class PasswordEdit(QLineEdit):
"""
Password LineEdit with icons to show/hide password entries.
Based on this example https://kushaldas.in/posts/creating-password-input-widget-in-pyqt.html by Kushal Das.
"""
def __init__(
self,
show_visibility=True,
visible_icon=None,
hidden_icon=None,
icons_from_theme=False,
*args,
**kwargs,
):
super().__init__(*args, **kwargs)
if icons_from_theme:
self.visibleIcon = QIcon.fromTheme("view-visible")
self.hiddenIcon = QIcon.fromTheme("view-hidden")
else:
if visible_icon:
self.visibleIcon = visible_icon
else:
self.visibleIcon = QIcon(os.path.join(folder, "eye.svg"))
if hidden_icon:
self.hiddenIcon = hidden_icon
else:
self.hiddenIcon = QIcon(os.path.join(folder, "hidden.svg"))
self.setEchoMode(QLineEdit.EchoMode.Password)
if show_visibility:
# Add the password hide/shown toggle at the end of the edit box.
self.togglepasswordAction = self.addAction(
self.visibleIcon, QLineEdit.ActionPosition.TrailingPosition
)
self.togglepasswordAction.triggered.connect(self.on_toggle_password_Action)
self.password_shown = False
def on_toggle_password_Action(self):
if not self.password_shown:
self.setEchoMode(QLineEdit.EchoMode.Normal)
self.password_shown = True
self.togglepasswordAction.setIcon(self.hiddenIcon)
else:
self.setEchoMode(QLineEdit.EchoMode.Password)
self.password_shown = False
self.togglepasswordAction.setIcon(self.visibleIcon)

View File

@@ -0,0 +1 @@
from .power_bar import PowerBar

View File

@@ -0,0 +1,26 @@
import sys
from PyQt5.QtWidgets import QApplication
from power_bar import PowerBar
app = QApplication(sys.argv)
volume = PowerBar(
[
"#053061",
"#2166ac",
"#4393c3",
"#92c5de",
"#d1e5f0",
"#f7f7f7",
"#fddbc7",
"#f4a582",
"#d6604d",
"#b2182b",
"#67001f",
]
)
volume.setBarSolidPercent(0.8)
volume.setBarPadding(5)
volume.show()
app.exec_()

View File

@@ -0,0 +1,158 @@
from PyQt5.QtCore import QRect, QRectF, QSize, Qt, pyqtSignal
from PyQt5.QtGui import QBrush, QColor, QPainter
from PyQt5.QtWidgets import QDial, QSizePolicy, QVBoxLayout, QWidget
class _Bar(QWidget):
clickedValue = pyqtSignal(int)
def __init__(self, steps):
super().__init__()
self.setSizePolicy(
QSizePolicy.Policy.MinimumExpanding,
QSizePolicy.Policy.MinimumExpanding,
)
if isinstance(steps, list):
# list of colours.
self.n_steps = len(steps)
self.steps = steps
elif isinstance(steps, int):
# int number of bars, defaults to red.
self.n_steps = steps
self.steps = ["red"] * steps
else:
raise TypeError("steps must be a list or int")
self._bar_solid_percent = 0.8
self._background_color = QColor("black")
self._padding = 4.0 # n-pixel gap around edge.
def paintEvent(self, e):
painter = QPainter(self)
brush = QBrush()
brush.setColor(self._background_color)
brush.setStyle(Qt.BrushStyle.SolidPattern)
rect = QRect(0, 0, painter.device().width(), painter.device().height())
painter.fillRect(rect, brush)
# Get current state.
parent = self.parent()
vmin, vmax = parent.minimum(), parent.maximum()
value = parent.value()
# Define our canvas.
d_height = painter.device().height() - (self._padding * 2)
d_width = painter.device().width() - (self._padding * 2)
# Draw the bars.
step_size = d_height / self.n_steps
bar_height = step_size * self._bar_solid_percent
bar_spacer = step_size * (1 - self._bar_solid_percent) / 2
# Calculate the y-stop position, from the value in range.
pc = (value - vmin) / (vmax - vmin)
n_steps_to_draw = int(pc * self.n_steps)
for n in range(n_steps_to_draw):
brush.setColor(QColor(self.steps[n]))
rect = QRectF(
self._padding,
self._padding + d_height - ((1 + n) * step_size) + bar_spacer,
d_width,
bar_height,
)
painter.fillRect(rect, brush)
painter.end()
def sizeHint(self):
return QSize(40, 120)
def _trigger_refresh(self):
self.update()
def _calculate_clicked_value(self, e):
parent = self.parent()
vmin, vmax = parent.minimum(), parent.maximum()
d_height = self.size().height() + (self._padding * 2)
step_size = d_height / self.n_steps
click_y = e.y() - self._padding - step_size / 2
pc = (d_height - click_y) / d_height
value = vmin + pc * (vmax - vmin)
self.clickedValue.emit(value)
def mouseMoveEvent(self, e):
self._calculate_clicked_value(e)
def mousePressEvent(self, e):
self._calculate_clicked_value(e)
class PowerBar(QWidget):
"""
Custom Qt Widget to show a power bar and dial.
Demonstrating compound and custom-drawn widget.
Left-clicking the button shows the color-chooser, while
right-clicking resets the color to None (no-color).
"""
def __init__(self, steps=5):
super().__init__()
layout = QVBoxLayout()
self._bar = _Bar(steps)
layout.addWidget(self._bar)
# Create the QDial widget and set up defaults.
# - we provide accessors on this class to override.
self._dial = QDial()
self._dial.setNotchesVisible(True)
self._dial.setWrapping(False)
self._dial.valueChanged.connect(self._bar._trigger_refresh)
# Take feedback from click events on the meter.
self._bar.clickedValue.connect(self._dial.setValue)
layout.addWidget(self._dial)
self.setLayout(layout)
def __getattr__(self, name):
if name in self.__dict__:
return self[name]
try:
return getattr(self._dial, name)
except AttributeError:
raise AttributeError(
"'{}' object has no attribute '{}'".format(
self.__class__.__name__, name
)
)
def setColor(self, color):
self._bar.steps = [color] * self._bar.n_steps
self._bar.update()
def setColors(self, colors):
self._bar.n_steps = len(colors)
self._bar.steps = colors
self._bar.update()
def setBarPadding(self, i):
self._bar._padding = int(i)
self._bar.update()
def setBarSolidPercent(self, f):
self._bar._bar_solid_percent = float(f)
self._bar.update()
def setBackgroundColor(self, color):
self._bar._background_color = QColor(color)
self._bar.update()

View File

@@ -0,0 +1 @@
from .rangeslider import RangeSlider

View File

@@ -0,0 +1,12 @@
import sys
from PyQt5.QtWidgets import QApplication
from range_slider import RangeSlider
app = QApplication(sys.argv)
slider = RangeSlider()
slider.valueChanged.connect(print)
slider.show()
app.exec_()

View File

@@ -0,0 +1,179 @@
from PyQt5.QtCore import QRect, QSize, Qt, pyqtSignal
from PyQt5.QtGui import QBrush, QMouseEvent, QPainter, QPaintEvent, QPalette
from PyQt5.QtWidgets import (
QApplication,
QSizePolicy,
QSlider,
QStyle,
QStyleOptionSlider,
QWidget,
)
class RangeSlider(QWidget):
valueChanged = pyqtSignal(int, int)
def __init__(self, parent=None):
super().__init__(parent)
self.first_position = 1
self.second_position = 8
self.opt = QStyleOptionSlider()
self.opt.minimum = 0
self.opt.maximum = 10
self.setTickPosition(QSlider.TickPosition.TicksAbove)
self.setTickInterval(1)
self.setSizePolicy(
QSizePolicy(
QSizePolicy.Policy.Expanding,
QSizePolicy.Policy.Fixed,
QSizePolicy.ControlType.Slider,
)
)
def setRangeLimit(self, minimum: int, maximum: int):
self.opt.minimum = minimum
self.opt.maximum = maximum
def setRange(self, start: int, end: int):
self.first_position = start
self.second_position = end
def getRange(self):
return (self.first_position, self.second_position)
def setTickPosition(self, position: QSlider.TickPosition):
self.opt.tickPosition = position
def setTickInterval(self, ti: int):
self.opt.tickInterval = ti
def paintEvent(self, event: QPaintEvent):
painter = QPainter(self)
# Draw rule
self.opt.initFrom(self)
self.opt.rect = self.rect()
self.opt.sliderPosition = 0
self.opt.subControls = (
QStyle.SubControl.SC_SliderGroove | QStyle.SubControl.SC_SliderTickmarks
)
# Draw GROOVE
self.style().drawComplexControl(
QStyle.ComplexControl.CC_Slider, self.opt, painter
)
# Draw INTERVAL
color = self.palette().color(QPalette.ColorRole.Highlight)
color.setAlpha(160)
painter.setBrush(QBrush(color))
painter.setPen(Qt.PenStyle.NoPen)
self.opt.sliderPosition = self.first_position
x_left_handle = (
self.style()
.subControlRect(
QStyle.ComplexControl.CC_Slider,
self.opt,
QStyle.SubControl.SC_SliderHandle,
)
.right()
)
self.opt.sliderPosition = self.second_position
x_right_handle = (
self.style()
.subControlRect(
QStyle.ComplexControl.CC_Slider,
self.opt,
QStyle.SubControl.SC_SliderHandle,
)
.left()
)
groove_rect = self.style().subControlRect(
QStyle.ComplexControl.CC_Slider, self.opt, QStyle.SubControl.SC_SliderGroove
)
selection = QRect(
x_left_handle,
groove_rect.y(),
x_right_handle - x_left_handle,
groove_rect.height(),
).adjusted(-1, 1, 1, -1)
painter.drawRect(selection)
# Draw first handle
self.opt.subControls = QStyle.SubControl.SC_SliderHandle
self.opt.sliderPosition = self.first_position
self.style().drawComplexControl(
QStyle.ComplexControl.CC_Slider, self.opt, painter
)
# Draw second handle
self.opt.sliderPosition = self.second_position
self.style().drawComplexControl(
QStyle.ComplexControl.CC_Slider, self.opt, painter
)
def mousePressEvent(self, event: QMouseEvent):
self.opt.sliderPosition = self.first_position
self._first_sc = self.style().hitTestComplexControl(
QStyle.ComplexControl.CC_Slider, self.opt, event.pos(), self
)
self.opt.sliderPosition = self.second_position
self._second_sc = self.style().hitTestComplexControl(
QStyle.ComplexControl.CC_Slider, self.opt, event.pos(), self
)
def mouseMoveEvent(self, event: QMouseEvent):
distance = self.opt.maximum - self.opt.minimum
pos = self.style().sliderValueFromPosition(
0, distance, event.pos().x(), self.rect().width()
)
if self._first_sc == QStyle.SubControl.SC_SliderHandle:
if pos <= self.second_position:
self.first_position = pos
self.valueChanged.emit(self.first_position, self.second_position)
self.update()
return
if self._second_sc == QStyle.SubControl.SC_SliderHandle:
if pos >= self.first_position:
self.second_position = pos
self.valueChanged.emit(self.first_position, self.second_position)
self.update()
def sizeHint(self):
"""override"""
SliderLength = 84
TickSpace = 5
w = SliderLength
h = self.style().pixelMetric(
QStyle.PixelMetric.PM_SliderThickness, self.opt, self
)
if (
self.opt.tickPosition & QSlider.TickPosition.TicksAbove
or self.opt.tickPosition & QSlider.TickPosition.TicksBelow
):
h += TickSpace
return (
self.style()
.sizeFromContents(
QStyle.ContentsType.CT_Slider, self.opt, QSize(w, h), self
)
.expandedTo(QApplication.globalStrut())
)

View File

@@ -0,0 +1 @@
from .toggle import Toggle, AnimatedToggle

View File

@@ -0,0 +1,27 @@
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
from toggle import AnimatedToggle, Toggle
class Window(QMainWindow):
def __init__(self):
super().__init__()
toggle_1 = Toggle()
toggle_2 = AnimatedToggle(
checked_color="#FFB000", pulse_checked_color="#44FFB000"
)
container = QWidget()
layout = QVBoxLayout()
layout.addWidget(toggle_1)
layout.addWidget(toggle_2)
container.setLayout(layout)
self.setCentralWidget(container)
app = QApplication([])
w = Window()
w.show()
app.exec_()

View File

@@ -0,0 +1,196 @@
from PyQt5.QtCore import (
QEasingCurve,
QPoint,
QPointF,
QPropertyAnimation,
QRectF,
QSequentialAnimationGroup,
QSize,
Qt,
pyqtProperty,
pyqtSlot,
)
from PyQt5.QtGui import QBrush, QColor, QPainter, QPaintEvent, QPen
from PyQt5.QtWidgets import QCheckBox
class Toggle(QCheckBox):
_transparent_pen = QPen(Qt.GlobalColor.transparent)
_light_grey_pen = QPen(Qt.GlobalColor.lightGray)
def __init__(
self,
parent=None,
bar_color=Qt.GlobalColor.gray,
checked_color="#00B0FF",
handle_color=Qt.GlobalColor.white,
):
super().__init__(parent)
# Save our properties on the object via self, so we can access them later
# in the paintEvent.
self._bar_brush = QBrush(bar_color)
self._bar_checked_brush = QBrush(QColor(checked_color).lighter())
self._handle_brush = QBrush(handle_color)
self._handle_checked_brush = QBrush(QColor(checked_color))
# Setup the rest of the widget.
self.setContentsMargins(8, 0, 8, 0)
self._handle_position = 0
self.stateChanged.connect(self.handle_state_change)
def sizeHint(self):
return QSize(58, 45)
def hitButton(self, pos: QPoint):
return self.contentsRect().contains(pos)
def paintEvent(self, e: QPaintEvent):
contRect = self.contentsRect()
handleRadius = round(0.24 * contRect.height())
p = QPainter(self)
p.setRenderHint(QPainter.RenderHint.Antialiasing)
p.setPen(self._transparent_pen)
barRect = QRectF(
0, 0, contRect.width() - handleRadius, 0.40 * contRect.height()
)
barRect.moveCenter(contRect.center())
rounding = barRect.height() / 2
# the handle will move along this line
trailLength = contRect.width() - 2 * handleRadius
xPos = contRect.x() + handleRadius + trailLength * self._handle_position
if self.isChecked():
p.setBrush(self._bar_checked_brush)
p.drawRoundedRect(barRect, rounding, rounding)
p.setBrush(self._handle_checked_brush)
else:
p.setBrush(self._bar_brush)
p.drawRoundedRect(barRect, rounding, rounding)
p.setPen(self._light_grey_pen)
p.setBrush(self._handle_brush)
p.drawEllipse(QPointF(xPos, barRect.center().y()), handleRadius, handleRadius)
p.end()
@pyqtSlot(int)
def handle_state_change(self, value):
self._handle_position = 1 if value else 0
@pyqtProperty(float)
def handle_position(self):
return self._handle_position
@handle_position.setter
def handle_position(self, pos):
"""change the property
we need to trigger QWidget.update() method, either by:
1- calling it here [ what we're doing ].
2- connecting the QPropertyAnimation.valueChanged() signal to it.
"""
self._handle_position = pos
self.update()
@pyqtProperty(float)
def pulse_radius(self):
return self._pulse_radius
@pulse_radius.setter
def pulse_radius(self, pos):
self._pulse_radius = pos
self.update()
class AnimatedToggle(Toggle):
_transparent_pen = QPen(Qt.GlobalColor.transparent)
_light_grey_pen = QPen(Qt.GlobalColor.lightGray)
def __init__(
self,
*args,
pulse_unchecked_color="#44999999",
pulse_checked_color="#4400B0EE",
**kwargs,
):
self._pulse_radius = 0
super().__init__(*args, **kwargs)
self.animation = QPropertyAnimation(self, b"handle_position", self)
self.animation.setEasingCurve(QEasingCurve.Type.InOutCubic)
self.animation.setDuration(200) # time in ms
self.pulse_anim = QPropertyAnimation(self, b"pulse_radius", self)
self.pulse_anim.setDuration(350) # time in ms
self.pulse_anim.setStartValue(10)
self.pulse_anim.setEndValue(20)
self.animations_group = QSequentialAnimationGroup()
self.animations_group.addAnimation(self.animation)
self.animations_group.addAnimation(self.pulse_anim)
self._pulse_unchecked_animation = QBrush(QColor(pulse_unchecked_color))
self._pulse_checked_animation = QBrush(QColor(pulse_checked_color))
@pyqtSlot(int)
def handle_state_change(self, value):
self.animations_group.stop()
if value:
self.animation.setEndValue(1)
else:
self.animation.setEndValue(0)
self.animations_group.start()
def paintEvent(self, e: QPaintEvent):
contRect = self.contentsRect()
handleRadius = round(0.24 * contRect.height())
p = QPainter(self)
p.setRenderHint(QPainter.RenderHint.Antialiasing)
p.setPen(self._transparent_pen)
barRect = QRectF(
0, 0, contRect.width() - handleRadius, 0.40 * contRect.height()
)
barRect.moveCenter(contRect.center())
rounding = barRect.height() / 2
# the handle will move along this line
trailLength = contRect.width() - 2 * handleRadius
xPos = contRect.x() + handleRadius + trailLength * self._handle_position
if self.pulse_anim.state() == QPropertyAnimation.State.Running:
p.setBrush(
self._pulse_checked_animation
if self.isChecked()
else self._pulse_unchecked_animation
)
p.drawEllipse(
QPointF(xPos, barRect.center().y()),
self._pulse_radius,
self._pulse_radius,
)
if self.isChecked():
p.setBrush(self._bar_checked_brush)
p.drawRoundedRect(barRect, rounding, rounding)
p.setBrush(self._handle_checked_brush)
else:
p.setBrush(self._bar_brush)
p.drawRoundedRect(barRect, rounding, rounding)
p.setPen(self._light_grey_pen)
p.setBrush(self._handle_brush)
p.drawEllipse(QPointF(xPos, barRect.center().y()), handleRadius, handleRadius)
p.end()