Martin Fitzpatrick b74592ea41 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.
2024-02-19 13:36:32 +01:00

275 lines
9.2 KiB
Python

import random
import sys
import types
import constants
import resources_rc
from canvas import Canvas
from MainWindow import Ui_MainWindow
from PyQt5.QtCore import QPoint, QRect, Qt, QTimer
from PyQt5.QtGui import (
QFont,
QIcon,
QImage,
QPixmap,
QTransform,
)
from PyQt5.QtWidgets import (
QApplication,
QButtonGroup,
QColorDialog,
QComboBox,
QFileDialog,
QFontComboBox,
QLabel,
QMainWindow,
QSlider,
)
# Not actually required, the import triggers this already.
resources_rc.qInitResources()
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
# Replace canvas placeholder from QtDesigner.
self.horizontalLayout.removeWidget(self.canvas)
self.canvas = Canvas()
self.canvas.initialize()
# We need to enable mouse tracking to follow the mouse without the button pressed.
self.canvas.setMouseTracking(True)
# Enable focus to capture key inputs.
self.canvas.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
self.horizontalLayout.addWidget(self.canvas)
# Setup the mode buttons
mode_group = QButtonGroup(self)
mode_group.setExclusive(True)
for mode in constants.MODES:
btn = getattr(self, "%sButton" % mode)
btn.pressed.connect(lambda mode=mode: self.canvas.set_mode(mode))
mode_group.addButton(btn)
# Setup the color selection buttons.
self.primaryButton.pressed.connect(
lambda: self.choose_color(self.set_primary_color)
)
self.secondaryButton.pressed.connect(
lambda: self.choose_color(self.set_secondary_color)
)
# Initialize button colours.
for n, hex in enumerate(constants.COLORS, 1):
btn = getattr(self, "colorButton_%d" % n)
btn.setStyleSheet("QPushButton { background-color: %s; }" % hex)
btn.hex = hex # For use in the event below
def patch_mousePressEvent(self_, e):
if e.button() == Qt.MouseButton.LeftButton:
self.set_primary_color(self_.hex)
elif e.button() == Qt.MouseButton.RightButton:
self.set_secondary_color(self_.hex)
btn.mousePressEvent = types.MethodType(patch_mousePressEvent, btn)
# Setup up action signals
self.actionCopy.triggered.connect(self.copy_to_clipboard)
# Initialize animation timer.
self.timer = QTimer()
self.timer.timeout.connect(self.canvas.on_timer)
self.timer.setInterval(100)
self.timer.start()
# Setup to agree with Canvas.
self.set_primary_color("#000000")
self.set_secondary_color("#ffffff")
# Signals for canvas-initiated color changes (dropper).
self.canvas.primary_color_updated.connect(self.set_primary_color)
self.canvas.secondary_color_updated.connect(self.set_secondary_color)
# Setup the stamp state.
self.current_stamp_n = -1
self.next_stamp()
self.stampnextButton.pressed.connect(self.next_stamp)
# Menu options
self.actionNewImage.triggered.connect(self.canvas.initialize)
self.actionOpenImage.triggered.connect(self.open_file)
self.actionSaveImage.triggered.connect(self.save_file)
self.actionClearImage.triggered.connect(self.canvas.reset)
self.actionInvertColors.triggered.connect(self.invert)
self.actionFlipHorizontal.triggered.connect(self.flip_horizontal)
self.actionFlipVertical.triggered.connect(self.flip_vertical)
# Setup the drawing toolbar.
self.fontselect = QFontComboBox()
self.fontToolbar.addWidget(self.fontselect)
self.fontselect.currentFontChanged.connect(
lambda f: self.canvas.set_config("font", f)
)
self.fontselect.setCurrentFont(QFont("Times"))
self.fontsize = QComboBox()
self.fontsize.addItems([str(s) for s in constants.FONT_SIZES])
self.fontsize.currentTextChanged.connect(
lambda f: self.canvas.set_config("fontsize", int(f))
)
# Connect to the signal producing the text of the current selection. Convert the string to float
# and set as the pointsize. We could also use the index + retrieve from FONT_SIZES.
self.fontToolbar.addWidget(self.fontsize)
self.fontToolbar.addAction(self.actionBold)
self.actionBold.triggered.connect(lambda s: self.canvas.set_config("bold", s))
self.fontToolbar.addAction(self.actionItalic)
self.actionItalic.triggered.connect(
lambda s: self.canvas.set_config("italic", s)
)
self.fontToolbar.addAction(self.actionUnderline)
self.actionUnderline.triggered.connect(
lambda s: self.canvas.set_config("underline", s)
)
sizeicon = QLabel()
sizeicon.setPixmap(QPixmap(":/icons/border-weight.png"))
self.drawingToolbar.addWidget(sizeicon)
self.sizeselect = QSlider()
self.sizeselect.setRange(1, 20)
self.sizeselect.setOrientation(Qt.Orientation.Horizontal)
self.sizeselect.valueChanged.connect(
lambda s: self.canvas.set_config("size", s)
)
self.drawingToolbar.addWidget(self.sizeselect)
self.actionFillShapes.triggered.connect(
lambda s: self.canvas.set_config("fill", s)
)
self.drawingToolbar.addAction(self.actionFillShapes)
self.actionFillShapes.setChecked(True)
self.show()
def choose_color(self, callback):
dlg = QColorDialog()
if dlg.exec():
callback(dlg.selectedColor().name())
def set_primary_color(self, hex):
self.canvas.set_primary_color(hex)
self.primaryButton.setStyleSheet("QPushButton { background-color: %s; }" % hex)
def set_secondary_color(self, hex):
self.canvas.set_secondary_color(hex)
self.secondaryButton.setStyleSheet(
"QPushButton { background-color: %s; }" % hex
)
def next_stamp(self):
self.current_stamp_n += 1
if self.current_stamp_n >= len(constants.STAMPS):
self.current_stamp_n = 0
pixmap = QPixmap(constants.STAMPS[self.current_stamp_n])
self.stampnextButton.setIcon(QIcon(pixmap))
self.canvas.current_stamp = pixmap
def copy_to_clipboard(self):
clipboard = QApplication.clipboard()
if self.canvas.mode == "selectrect" and self.canvas.locked:
clipboard.setPixmap(self.canvas.selectrect_copy())
elif self.canvas.mode == "selectpoly" and self.canvas.locked:
clipboard.setPixmap(self.canvas.selectpoly_copy())
else:
clipboard.setPixmap(self.canvas.pixmap())
def open_file(self):
"""
Open image file for editing, scaling the smaller dimension and cropping the remainder.
:return:
"""
path, _ = QFileDialog.getOpenFileName(
self,
"Open file",
"",
"PNG image files (*.png); JPEG image files (*jpg); All files (*.*)",
)
if path:
pixmap = QPixmap()
pixmap.load(path)
# We need to crop down to the size of our canvas. Get the size of the loaded image.
iw = pixmap.width()
ih = pixmap.height()
# Get the size of the space we're filling.
cw, ch = constants.CANVAS_DIMENSIONS
if iw / cw < ih / ch: # The height is relatively bigger than the width.
pixmap = pixmap.scaledToWidth(cw)
hoff = (pixmap.height() - ch) // 2
pixmap = pixmap.copy(
QRect(
QPoint(0, hoff),
QPoint(cw, pixmap.height() - hoff),
)
)
elif iw / cw > ih / ch: # The height is relatively bigger than the width.
pixmap = pixmap.scaledToHeight(ch)
woff = (pixmap.width() - cw) // 2
pixmap = pixmap.copy(
QRect(
QPoint(woff, 0),
QPoint(pixmap.width() - woff, ch),
)
)
self.canvas.setPixmap(pixmap)
def save_file(self):
"""
Save active canvas to image file.
:return:
"""
path, _ = QFileDialog.getSaveFileName(
self, "Save file", "", "PNG Image file (*.png)"
)
if path:
pixmap = self.canvas.pixmap()
pixmap.save(path, "PNG")
def invert(self):
img = QImage(self.canvas.pixmap())
img.invertPixels()
pixmap = QPixmap()
pixmap.convertFromImage(img)
self.canvas.setPixmap(pixmap)
def flip_horizontal(self):
pixmap = self.canvas.pixmap()
self.canvas.setPixmap(pixmap.transformed(QTransform().scale(-1, 1)))
def flip_vertical(self):
pixmap = self.canvas.pixmap()
self.canvas.setPixmap(pixmap.transformed(QTransform().scale(1, -1)))
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setWindowIcon(QIcon(":/icons/piecasso.ico"))
window = MainWindow()
app.exec_()