eric7/IconEditor/IconEditorGrid.py

branch
eric7
changeset 8312
800c432b34c8
parent 8269
87f521f359d5
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2009 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the icon editor grid.
8 """
9
10 import os
11
12 from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QPoint, QRect, QSize
13 from PyQt5.QtGui import (
14 QImage, QColor, QPixmap, qRgba, QPainter, QCursor, QBrush, qGray, qAlpha
15 )
16 from PyQt5.QtWidgets import (
17 QUndoCommand, QWidget, QSizePolicy, QUndoStack, QApplication, QDialog
18 )
19
20 from E5Gui import E5MessageBox
21
22
23 class IconEditCommand(QUndoCommand):
24 """
25 Class implementing an undo command for the icon editor.
26 """
27 def __init__(self, grid, text, oldImage, parent=None):
28 """
29 Constructor
30
31 @param grid reference to the icon editor grid (IconEditorGrid)
32 @param text text for the undo command (string)
33 @param oldImage copy of the icon before the changes were applied
34 (QImage)
35 @param parent reference to the parent command (QUndoCommand)
36 """
37 super().__init__(text, parent)
38
39 self.__grid = grid
40 self.__imageBefore = QImage(oldImage)
41 self.__imageAfter = None
42
43 def setAfterImage(self, image):
44 """
45 Public method to set the image after the changes were applied.
46
47 @param image copy of the icon after the changes were applied (QImage)
48 """
49 self.__imageAfter = QImage(image)
50
51 def undo(self):
52 """
53 Public method to perform the undo.
54 """
55 self.__grid.setIconImage(self.__imageBefore, undoRedo=True)
56
57 def redo(self):
58 """
59 Public method to perform the redo.
60 """
61 if self.__imageAfter:
62 self.__grid.setIconImage(self.__imageAfter, undoRedo=True)
63
64
65 class IconEditorGrid(QWidget):
66 """
67 Class implementing the icon editor grid.
68
69 @signal canRedoChanged(bool) emitted after the redo status has changed
70 @signal canUndoChanged(bool) emitted after the undo status has changed
71 @signal clipboardImageAvailable(bool) emitted to signal the availability
72 of an image to be pasted
73 @signal colorChanged(QColor) emitted after the drawing color was changed
74 @signal imageChanged(bool) emitted after the image was modified
75 @signal positionChanged(int, int) emitted after the cursor poition was
76 changed
77 @signal previewChanged(QPixmap) emitted to signal a new preview pixmap
78 @signal selectionAvailable(bool) emitted to signal a change of the
79 selection
80 @signal sizeChanged(int, int) emitted after the size has been changed
81 @signal zoomChanged(int) emitted to signal a change of the zoom value
82 """
83 canRedoChanged = pyqtSignal(bool)
84 canUndoChanged = pyqtSignal(bool)
85 clipboardImageAvailable = pyqtSignal(bool)
86 colorChanged = pyqtSignal(QColor)
87 imageChanged = pyqtSignal(bool)
88 positionChanged = pyqtSignal(int, int)
89 previewChanged = pyqtSignal(QPixmap)
90 selectionAvailable = pyqtSignal(bool)
91 sizeChanged = pyqtSignal(int, int)
92 zoomChanged = pyqtSignal(int)
93
94 # convert to Enum
95 Pencil = 1
96 Rubber = 2
97 Line = 3
98 Rectangle = 4
99 FilledRectangle = 5
100 Circle = 6
101 FilledCircle = 7
102 Ellipse = 8
103 FilledEllipse = 9
104 Fill = 10
105 ColorPicker = 11
106
107 # convert to Enum
108 RectangleSelection = 20
109 CircleSelection = 21
110
111 MarkColor = QColor(255, 255, 255, 255)
112 NoMarkColor = QColor(0, 0, 0, 0)
113
114 ZoomMinimum = 100
115 ZoomMaximum = 10000
116 ZoomStep = 100
117 ZoomDefault = 1200
118 ZoomPercent = True
119
120 def __init__(self, parent=None):
121 """
122 Constructor
123
124 @param parent reference to the parent widget (QWidget)
125 """
126 super().__init__(parent)
127
128 self.setAttribute(Qt.WidgetAttribute.WA_StaticContents)
129 self.setSizePolicy(QSizePolicy.Policy.Minimum,
130 QSizePolicy.Policy.Minimum)
131
132 self.__curColor = Qt.GlobalColor.black
133 self.__zoom = 12
134 self.__curTool = self.Pencil
135 self.__startPos = QPoint()
136 self.__endPos = QPoint()
137 self.__dirty = False
138 self.__selecting = False
139 self.__selRect = QRect()
140 self.__isPasting = False
141 self.__clipboardSize = QSize()
142 self.__pasteRect = QRect()
143
144 self.__undoStack = QUndoStack(self)
145 self.__currentUndoCmd = None
146
147 self.__image = QImage(32, 32, QImage.Format.Format_ARGB32)
148 self.__image.fill(qRgba(0, 0, 0, 0))
149 self.__markImage = QImage(self.__image)
150 self.__markImage.fill(self.NoMarkColor.rgba())
151
152 self.__compositingMode = (
153 QPainter.CompositionMode.CompositionMode_SourceOver
154 )
155 self.__lastPos = (-1, -1)
156
157 self.__gridEnabled = True
158 self.__selectionAvailable = False
159
160 self.__initCursors()
161 self.__initUndoTexts()
162
163 self.setMouseTracking(True)
164
165 self.__undoStack.canRedoChanged.connect(self.canRedoChanged)
166 self.__undoStack.canUndoChanged.connect(self.canUndoChanged)
167 self.__undoStack.cleanChanged.connect(self.__cleanChanged)
168
169 self.imageChanged.connect(self.__updatePreviewPixmap)
170 QApplication.clipboard().dataChanged.connect(self.__checkClipboard)
171
172 self.__checkClipboard()
173
174 def __initCursors(self):
175 """
176 Private method to initialize the various cursors.
177 """
178 cursorsPath = os.path.join(os.path.dirname(__file__), "cursors")
179
180 self.__normalCursor = QCursor(Qt.CursorShape.ArrowCursor)
181
182 pix = QPixmap(os.path.join(cursorsPath, "colorpicker-cursor.xpm"))
183 mask = pix.createHeuristicMask()
184 pix.setMask(mask)
185 self.__colorPickerCursor = QCursor(pix, 1, 21)
186
187 pix = QPixmap(os.path.join(cursorsPath, "paintbrush-cursor.xpm"))
188 mask = pix.createHeuristicMask()
189 pix.setMask(mask)
190 self.__paintCursor = QCursor(pix, 0, 19)
191
192 pix = QPixmap(os.path.join(cursorsPath, "fill-cursor.xpm"))
193 mask = pix.createHeuristicMask()
194 pix.setMask(mask)
195 self.__fillCursor = QCursor(pix, 3, 20)
196
197 pix = QPixmap(os.path.join(cursorsPath, "aim-cursor.xpm"))
198 mask = pix.createHeuristicMask()
199 pix.setMask(mask)
200 self.__aimCursor = QCursor(pix, 10, 10)
201
202 pix = QPixmap(os.path.join(cursorsPath, "eraser-cursor.xpm"))
203 mask = pix.createHeuristicMask()
204 pix.setMask(mask)
205 self.__rubberCursor = QCursor(pix, 1, 16)
206
207 def __initUndoTexts(self):
208 """
209 Private method to initialize texts to be associated with undo commands
210 for the various drawing tools.
211 """
212 self.__undoTexts = {
213 self.Pencil: self.tr("Set Pixel"),
214 self.Rubber: self.tr("Erase Pixel"),
215 self.Line: self.tr("Draw Line"),
216 self.Rectangle: self.tr("Draw Rectangle"),
217 self.FilledRectangle: self.tr("Draw Filled Rectangle"),
218 self.Circle: self.tr("Draw Circle"),
219 self.FilledCircle: self.tr("Draw Filled Circle"),
220 self.Ellipse: self.tr("Draw Ellipse"),
221 self.FilledEllipse: self.tr("Draw Filled Ellipse"),
222 self.Fill: self.tr("Fill Region"),
223 }
224
225 def isDirty(self):
226 """
227 Public method to check the dirty status.
228
229 @return flag indicating a modified status (boolean)
230 """
231 return self.__dirty
232
233 def setDirty(self, dirty, setCleanState=False):
234 """
235 Public slot to set the dirty flag.
236
237 @param dirty flag indicating the new modification status (boolean)
238 @param setCleanState flag indicating to set the undo stack to clean
239 (boolean)
240 """
241 self.__dirty = dirty
242 self.imageChanged.emit(dirty)
243
244 if not dirty and setCleanState:
245 self.__undoStack.setClean()
246
247 def sizeHint(self):
248 """
249 Public method to report the size hint.
250
251 @return size hint (QSize)
252 """
253 size = self.__zoom * self.__image.size()
254 if self.__zoom >= 3 and self.__gridEnabled:
255 size += QSize(1, 1)
256 return size
257
258 def setPenColor(self, newColor):
259 """
260 Public method to set the drawing color.
261
262 @param newColor reference to the new color (QColor)
263 """
264 self.__curColor = QColor(newColor)
265 self.colorChanged.emit(QColor(newColor))
266
267 def penColor(self):
268 """
269 Public method to get the current drawing color.
270
271 @return current drawing color (QColor)
272 """
273 return QColor(self.__curColor)
274
275 def setCompositingMode(self, mode):
276 """
277 Public method to set the compositing mode.
278
279 @param mode compositing mode to set (QPainter.CompositionMode)
280 """
281 self.__compositingMode = mode
282
283 def compositingMode(self):
284 """
285 Public method to get the compositing mode.
286
287 @return compositing mode (QPainter.CompositionMode)
288 """
289 return self.__compositingMode
290
291 def setTool(self, tool):
292 """
293 Public method to set the current drawing tool.
294
295 @param tool drawing tool to be used
296 (IconEditorGrid.Pencil ... IconEditorGrid.CircleSelection)
297 """
298 self.__curTool = tool
299 self.__lastPos = (-1, -1)
300
301 if self.__curTool in [self.RectangleSelection, self.CircleSelection]:
302 self.__selecting = True
303 else:
304 self.__selecting = False
305
306 if self.__curTool in [self.RectangleSelection, self.CircleSelection,
307 self.Line, self.Rectangle, self.FilledRectangle,
308 self.Circle, self.FilledCircle,
309 self.Ellipse, self.FilledEllipse]:
310 self.setCursor(self.__aimCursor)
311 elif self.__curTool == self.Fill:
312 self.setCursor(self.__fillCursor)
313 elif self.__curTool == self.ColorPicker:
314 self.setCursor(self.__colorPickerCursor)
315 elif self.__curTool == self.Pencil:
316 self.setCursor(self.__paintCursor)
317 elif self.__curTool == self.Rubber:
318 self.setCursor(self.__rubberCursor)
319 else:
320 self.setCursor(self.__normalCursor)
321
322 def tool(self):
323 """
324 Public method to get the current drawing tool.
325
326 @return current drawing tool
327 (IconEditorGrid.Pencil ... IconEditorGrid.CircleSelection)
328 """
329 return self.__curTool
330
331 def setIconImage(self, newImage, undoRedo=False, clearUndo=False):
332 """
333 Public method to set a new icon image.
334
335 @param newImage reference to the new image (QImage)
336 @param undoRedo flag indicating an undo or redo operation (boolean)
337 @param clearUndo flag indicating to clear the undo stack (boolean)
338 """
339 if newImage != self.__image:
340 self.__image = newImage.convertToFormat(
341 QImage.Format.Format_ARGB32)
342 self.update()
343 self.updateGeometry()
344 self.resize(self.sizeHint())
345
346 self.__markImage = QImage(self.__image)
347 self.__markImage.fill(self.NoMarkColor.rgba())
348
349 if undoRedo:
350 self.setDirty(not self.__undoStack.isClean())
351 else:
352 self.setDirty(False)
353
354 if clearUndo:
355 self.__undoStack.clear()
356
357 self.sizeChanged.emit(*self.iconSize())
358
359 def iconImage(self):
360 """
361 Public method to get a copy of the icon image.
362
363 @return copy of the icon image (QImage)
364 """
365 return QImage(self.__image)
366
367 def iconSize(self):
368 """
369 Public method to get the size of the icon.
370
371 @return width and height of the image as a tuple (integer, integer)
372 """
373 return self.__image.width(), self.__image.height()
374
375 def setZoomFactor(self, newZoom):
376 """
377 Public method to set the zoom factor in percent.
378
379 @param newZoom zoom factor (integer >= 100)
380 """
381 newZoom = max(100, newZoom) # must not be less than 100
382 if newZoom != self.__zoom:
383 self.__zoom = newZoom // 100
384 self.update()
385 self.updateGeometry()
386 self.resize(self.sizeHint())
387 self.zoomChanged.emit(int(self.__zoom * 100))
388
389 def zoomFactor(self):
390 """
391 Public method to get the current zoom factor in percent.
392
393 @return zoom factor (integer)
394 """
395 return self.__zoom * 100
396
397 def setGridEnabled(self, enable):
398 """
399 Public method to enable the display of grid lines.
400
401 @param enable enabled status of the grid lines (boolean)
402 """
403 if enable != self.__gridEnabled:
404 self.__gridEnabled = enable
405 self.update()
406
407 def isGridEnabled(self):
408 """
409 Public method to get the grid lines status.
410
411 @return enabled status of the grid lines (boolean)
412 """
413 return self.__gridEnabled
414
415 def paintEvent(self, evt):
416 """
417 Protected method called to repaint some of the widget.
418
419 @param evt reference to the paint event object (QPaintEvent)
420 """
421 painter = QPainter(self)
422
423 if self.__zoom >= 3 and self.__gridEnabled:
424 painter.setPen(self.palette().windowText().color())
425 i = 0
426 while i <= self.__image.width():
427 painter.drawLine(
428 self.__zoom * i, 0,
429 self.__zoom * i, self.__zoom * self.__image.height())
430 i += 1
431 j = 0
432 while j <= self.__image.height():
433 painter.drawLine(
434 0, self.__zoom * j,
435 self.__zoom * self.__image.width(), self.__zoom * j)
436 j += 1
437
438 col = QColor("#aaa")
439 painter.setPen(Qt.PenStyle.DashLine)
440 for i in range(0, self.__image.width()):
441 for j in range(0, self.__image.height()):
442 rect = self.__pixelRect(i, j)
443 if evt.region().intersects(rect):
444 color = QColor.fromRgba(self.__image.pixel(i, j))
445 painter.fillRect(rect, QBrush(Qt.GlobalColor.white))
446 painter.fillRect(QRect(rect.topLeft(), rect.center()), col)
447 painter.fillRect(QRect(rect.center(), rect.bottomRight()),
448 col)
449 painter.fillRect(rect, QBrush(color))
450
451 if self.__isMarked(i, j):
452 painter.drawRect(rect.adjusted(0, 0, -1, -1))
453
454 painter.end()
455
456 def __pixelRect(self, i, j):
457 """
458 Private method to determine the rectangle for a given pixel coordinate.
459
460 @param i x-coordinate of the pixel in the image (integer)
461 @param j y-coordinate of the pixel in the image (integer)
462 @return rectangle for the given pixel coordinates (QRect)
463 """
464 if self.__zoom >= 3 and self.__gridEnabled:
465 return QRect(self.__zoom * i + 1, self.__zoom * j + 1,
466 self.__zoom - 1, self.__zoom - 1)
467 else:
468 return QRect(self.__zoom * i, self.__zoom * j,
469 self.__zoom, self.__zoom)
470
471 def mousePressEvent(self, evt):
472 """
473 Protected method to handle mouse button press events.
474
475 @param evt reference to the mouse event object (QMouseEvent)
476 """
477 if evt.button() == Qt.MouseButton.LeftButton:
478 if self.__isPasting:
479 self.__isPasting = False
480 self.editPaste(True)
481 self.__markImage.fill(self.NoMarkColor.rgba())
482 self.update(self.__pasteRect)
483 self.__pasteRect = QRect()
484 return
485
486 if self.__curTool == self.Pencil:
487 cmd = IconEditCommand(self, self.__undoTexts[self.__curTool],
488 self.__image)
489 self.__setImagePixel(evt.pos(), True)
490 self.setDirty(True)
491 self.__undoStack.push(cmd)
492 self.__currentUndoCmd = cmd
493 elif self.__curTool == self.Rubber:
494 cmd = IconEditCommand(self, self.__undoTexts[self.__curTool],
495 self.__image)
496 self.__setImagePixel(evt.pos(), False)
497 self.setDirty(True)
498 self.__undoStack.push(cmd)
499 self.__currentUndoCmd = cmd
500 elif self.__curTool == self.Fill:
501 i, j = self.__imageCoordinates(evt.pos())
502 col = QColor()
503 col.setRgba(self.__image.pixel(i, j))
504 cmd = IconEditCommand(self, self.__undoTexts[self.__curTool],
505 self.__image)
506 self.__drawFlood(i, j, col)
507 self.setDirty(True)
508 self.__undoStack.push(cmd)
509 cmd.setAfterImage(self.__image)
510 elif self.__curTool == self.ColorPicker:
511 i, j = self.__imageCoordinates(evt.pos())
512 col = QColor()
513 col.setRgba(self.__image.pixel(i, j))
514 self.setPenColor(col)
515 else:
516 self.__unMark()
517 self.__startPos = evt.pos()
518 self.__endPos = evt.pos()
519
520 def mouseMoveEvent(self, evt):
521 """
522 Protected method to handle mouse move events.
523
524 @param evt reference to the mouse event object (QMouseEvent)
525 """
526 self.positionChanged.emit(*self.__imageCoordinates(evt.pos()))
527
528 if (
529 self.__isPasting and
530 not (evt.buttons() & Qt.MouseButton.LeftButton)
531 ):
532 self.__drawPasteRect(evt.pos())
533 return
534
535 if evt.buttons() & Qt.MouseButton.LeftButton:
536 if self.__curTool == self.Pencil:
537 self.__setImagePixel(evt.pos(), True)
538 self.setDirty(True)
539 elif self.__curTool == self.Rubber:
540 self.__setImagePixel(evt.pos(), False)
541 self.setDirty(True)
542 elif self.__curTool in [self.Fill, self.ColorPicker]:
543 pass # do nothing
544 else:
545 self.__drawTool(evt.pos(), True)
546
547 def mouseReleaseEvent(self, evt):
548 """
549 Protected method to handle mouse button release events.
550
551 @param evt reference to the mouse event object (QMouseEvent)
552 """
553 if evt.button() == Qt.MouseButton.LeftButton:
554 if (
555 self.__curTool in [self.Pencil, self.Rubber] and
556 self.__currentUndoCmd
557 ):
558 self.__currentUndoCmd.setAfterImage(self.__image)
559 self.__currentUndoCmd = None
560
561 if self.__curTool not in [self.Pencil, self.Rubber,
562 self.Fill, self.ColorPicker,
563 self.RectangleSelection,
564 self.CircleSelection]:
565 cmd = IconEditCommand(self, self.__undoTexts[self.__curTool],
566 self.__image)
567 if self.__drawTool(evt.pos(), False):
568 self.__undoStack.push(cmd)
569 cmd.setAfterImage(self.__image)
570 self.setDirty(True)
571
572 def __setImagePixel(self, pos, opaque):
573 """
574 Private slot to set or erase a pixel.
575
576 @param pos position of the pixel in the widget (QPoint)
577 @param opaque flag indicating a set operation (boolean)
578 """
579 i, j = self.__imageCoordinates(pos)
580
581 if self.__image.rect().contains(i, j) and (i, j) != self.__lastPos:
582 if opaque:
583 painter = QPainter(self.__image)
584 painter.setPen(self.penColor())
585 painter.setCompositionMode(self.__compositingMode)
586 painter.drawPoint(i, j)
587 else:
588 self.__image.setPixel(i, j, qRgba(0, 0, 0, 0))
589 self.__lastPos = (i, j)
590
591 self.update(self.__pixelRect(i, j))
592
593 def __imageCoordinates(self, pos):
594 """
595 Private method to convert from widget to image coordinates.
596
597 @param pos widget coordinate (QPoint)
598 @return tuple with the image coordinates (tuple of two integers)
599 """
600 i = pos.x() // self.__zoom
601 j = pos.y() // self.__zoom
602 return i, j
603
604 def __drawPasteRect(self, pos):
605 """
606 Private slot to draw a rectangle for signaling a paste operation.
607
608 @param pos widget position of the paste rectangle (QPoint)
609 """
610 self.__markImage.fill(self.NoMarkColor.rgba())
611 if self.__pasteRect.isValid():
612 self.__updateImageRect(
613 self.__pasteRect.topLeft(),
614 self.__pasteRect.bottomRight() + QPoint(1, 1))
615
616 x, y = self.__imageCoordinates(pos)
617 isize = self.__image.size()
618 sx = (
619 self.__clipboardSize.width()
620 if x + self.__clipboardSize.width() <= isize.width() else
621 isize.width() - x
622 )
623 sy = (
624 self.__clipboardSize.height()
625 if y + self.__clipboardSize.height() <= isize.height() else
626 isize.height() - y
627 )
628
629 self.__pasteRect = QRect(QPoint(x, y), QSize(sx - 1, sy - 1))
630
631 painter = QPainter(self.__markImage)
632 painter.setPen(self.MarkColor)
633 painter.drawRect(self.__pasteRect)
634 painter.end()
635
636 self.__updateImageRect(self.__pasteRect.topLeft(),
637 self.__pasteRect.bottomRight() + QPoint(1, 1))
638
639 def __drawTool(self, pos, mark):
640 """
641 Private method to perform a draw operation depending of the current
642 tool.
643
644 @param pos widget coordinate to perform the draw operation at (QPoint)
645 @param mark flag indicating a mark operation (boolean)
646 @return flag indicating a successful draw (boolean)
647 """
648 self.__unMark()
649
650 if mark:
651 self.__endPos = QPoint(pos)
652 drawColor = self.MarkColor
653 img = self.__markImage
654 else:
655 drawColor = self.penColor()
656 img = self.__image
657
658 start = QPoint(*self.__imageCoordinates(self.__startPos))
659 end = QPoint(*self.__imageCoordinates(pos))
660
661 painter = QPainter(img)
662 painter.setPen(drawColor)
663 painter.setCompositionMode(self.__compositingMode)
664
665 if self.__curTool == self.Line:
666 painter.drawLine(start, end)
667
668 elif self.__curTool in [self.Rectangle, self.FilledRectangle,
669 self.RectangleSelection]:
670 left = min(start.x(), end.x())
671 top = min(start.y(), end.y())
672 right = max(start.x(), end.x())
673 bottom = max(start.y(), end.y())
674 if self.__curTool == self.RectangleSelection:
675 painter.setBrush(QBrush(drawColor))
676 if self.__curTool == self.FilledRectangle:
677 for y in range(top, bottom + 1):
678 painter.drawLine(left, y, right, y)
679 else:
680 painter.drawRect(left, top, right - left, bottom - top)
681 if self.__selecting:
682 self.__selRect = QRect(
683 left, top, right - left + 1, bottom - top + 1)
684 self.__selectionAvailable = True
685 self.selectionAvailable.emit(True)
686
687 elif self.__curTool in [self.Circle, self.FilledCircle,
688 self.CircleSelection]:
689 deltaX = abs(start.x() - end.x())
690 deltaY = abs(start.y() - end.y())
691 r = max(deltaX, deltaY)
692 if self.__curTool in [self.FilledCircle, self.CircleSelection]:
693 painter.setBrush(QBrush(drawColor))
694 painter.drawEllipse(start, r, r)
695 if self.__selecting:
696 self.__selRect = QRect(start.x() - r, start.y() - r,
697 2 * r + 1, 2 * r + 1)
698 self.__selectionAvailable = True
699 self.selectionAvailable.emit(True)
700
701 elif self.__curTool in [self.Ellipse, self.FilledEllipse]:
702 r1 = abs(start.x() - end.x())
703 r2 = abs(start.y() - end.y())
704 if r1 == 0 or r2 == 0:
705 return False
706 if self.__curTool == self.FilledEllipse:
707 painter.setBrush(QBrush(drawColor))
708 painter.drawEllipse(start, r1, r2)
709
710 painter.end()
711
712 if self.__curTool in [self.Circle, self.FilledCircle,
713 self.Ellipse, self.FilledEllipse]:
714 self.update()
715 else:
716 self.__updateRect(self.__startPos, pos)
717
718 return True
719
720 def __drawFlood(self, i, j, oldColor, doUpdate=True):
721 """
722 Private method to perform a flood fill operation.
723
724 @param i x-value in image coordinates (integer)
725 @param j y-value in image coordinates (integer)
726 @param oldColor reference to the color at position i, j (QColor)
727 @param doUpdate flag indicating an update is requested (boolean)
728 (used for speed optimizations)
729 """
730 if (
731 not self.__image.rect().contains(i, j) or
732 self.__image.pixel(i, j) != oldColor.rgba() or
733 self.__image.pixel(i, j) == self.penColor().rgba()
734 ):
735 return
736
737 self.__image.setPixel(i, j, self.penColor().rgba())
738
739 self.__drawFlood(i, j - 1, oldColor, False)
740 self.__drawFlood(i, j + 1, oldColor, False)
741 self.__drawFlood(i - 1, j, oldColor, False)
742 self.__drawFlood(i + 1, j, oldColor, False)
743
744 if doUpdate:
745 self.update()
746
747 def __updateRect(self, pos1, pos2):
748 """
749 Private slot to update parts of the widget.
750
751 @param pos1 top, left position for the update in widget coordinates
752 (QPoint)
753 @param pos2 bottom, right position for the update in widget
754 coordinates (QPoint)
755 """
756 self.__updateImageRect(QPoint(*self.__imageCoordinates(pos1)),
757 QPoint(*self.__imageCoordinates(pos2)))
758
759 def __updateImageRect(self, ipos1, ipos2):
760 """
761 Private slot to update parts of the widget.
762
763 @param ipos1 top, left position for the update in image coordinates
764 (QPoint)
765 @param ipos2 bottom, right position for the update in image
766 coordinates (QPoint)
767 """
768 r1 = self.__pixelRect(ipos1.x(), ipos1.y())
769 r2 = self.__pixelRect(ipos2.x(), ipos2.y())
770
771 left = min(r1.x(), r2.x())
772 top = min(r1.y(), r2.y())
773 right = max(r1.x() + r1.width(), r2.x() + r2.width())
774 bottom = max(r1.y() + r1.height(), r2.y() + r2.height())
775 self.update(left, top, right - left + 1, bottom - top + 1)
776
777 def __unMark(self):
778 """
779 Private slot to remove the mark indicator.
780 """
781 self.__markImage.fill(self.NoMarkColor.rgba())
782 if self.__curTool in [self.Circle, self.FilledCircle,
783 self.Ellipse, self.FilledEllipse,
784 self.CircleSelection]:
785 self.update()
786 else:
787 self.__updateRect(self.__startPos, self.__endPos)
788
789 if self.__selecting:
790 self.__selRect = QRect()
791 self.__selectionAvailable = False
792 self.selectionAvailable.emit(False)
793
794 def __isMarked(self, i, j):
795 """
796 Private method to check, if a pixel is marked.
797
798 @param i x-value in image coordinates (integer)
799 @param j y-value in image coordinates (integer)
800 @return flag indicating a marked pixel (boolean)
801 """
802 return self.__markImage.pixel(i, j) == self.MarkColor.rgba()
803
804 def __updatePreviewPixmap(self):
805 """
806 Private slot to generate and signal an updated preview pixmap.
807 """
808 p = QPixmap.fromImage(self.__image)
809 self.previewChanged.emit(p)
810
811 def previewPixmap(self):
812 """
813 Public method to generate a preview pixmap.
814
815 @return preview pixmap (QPixmap)
816 """
817 p = QPixmap.fromImage(self.__image)
818 return p
819
820 def __checkClipboard(self):
821 """
822 Private slot to check, if the clipboard contains a valid image, and
823 signal the result.
824 """
825 ok = self.__clipboardImage()[1]
826 self.__clipboardImageAvailable = ok
827 self.clipboardImageAvailable.emit(ok)
828
829 def canPaste(self):
830 """
831 Public slot to check the availability of the paste operation.
832
833 @return flag indicating availability of paste (boolean)
834 """
835 return self.__clipboardImageAvailable
836
837 def __clipboardImage(self):
838 """
839 Private method to get an image from the clipboard.
840
841 @return tuple with the image (QImage) and a flag indicating a
842 valid image (boolean)
843 """
844 img = QApplication.clipboard().image()
845 ok = not img.isNull()
846 if ok:
847 img = img.convertToFormat(QImage.Format.Format_ARGB32)
848
849 return img, ok
850
851 def __getSelectionImage(self, cut):
852 """
853 Private method to get an image from the selection.
854
855 @param cut flag indicating to cut the selection (boolean)
856 @return image of the selection (QImage)
857 """
858 if cut:
859 cmd = IconEditCommand(self, self.tr("Cut Selection"),
860 self.__image)
861
862 img = QImage(self.__selRect.size(), QImage.Format.Format_ARGB32)
863 img.fill(qRgba(0, 0, 0, 0))
864 for i in range(0, self.__selRect.width()):
865 for j in range(0, self.__selRect.height()):
866 if (
867 self.__image.rect().contains(
868 self.__selRect.x() + i, self.__selRect.y() + j) and
869 self.__isMarked(self.__selRect.x() + i,
870 self.__selRect.y() + j)
871 ):
872 img.setPixel(i, j, self.__image.pixel(
873 self.__selRect.x() + i, self.__selRect.y() + j))
874 if cut:
875 self.__image.setPixel(self.__selRect.x() + i,
876 self.__selRect.y() + j,
877 qRgba(0, 0, 0, 0))
878
879 if cut:
880 self.__undoStack.push(cmd)
881 cmd.setAfterImage(self.__image)
882
883 self.__unMark()
884
885 if cut:
886 self.update(self.__selRect)
887
888 return img
889
890 def editCopy(self):
891 """
892 Public slot to copy the selection.
893 """
894 if self.__selRect.isValid():
895 img = self.__getSelectionImage(False)
896 QApplication.clipboard().setImage(img)
897
898 def editCut(self):
899 """
900 Public slot to cut the selection.
901 """
902 if self.__selRect.isValid():
903 img = self.__getSelectionImage(True)
904 QApplication.clipboard().setImage(img)
905
906 @pyqtSlot()
907 def editPaste(self, pasting=False):
908 """
909 Public slot to paste an image from the clipboard.
910
911 @param pasting flag indicating part two of the paste operation
912 (boolean)
913 """
914 img, ok = self.__clipboardImage()
915 if ok:
916 if (
917 img.width() > self.__image.width() or
918 img.height() > self.__image.height()
919 ):
920 res = E5MessageBox.yesNo(
921 self,
922 self.tr("Paste"),
923 self.tr(
924 """<p>The clipboard image is larger than the"""
925 """ current image.<br/>Paste as new image?</p>"""))
926 if res:
927 self.editPasteAsNew()
928 return
929 elif not pasting:
930 self.__isPasting = True
931 self.__clipboardSize = img.size()
932 else:
933 cmd = IconEditCommand(self, self.tr("Paste Clipboard"),
934 self.__image)
935 self.__markImage.fill(self.NoMarkColor.rgba())
936 painter = QPainter(self.__image)
937 painter.setPen(self.penColor())
938 painter.setCompositionMode(self.__compositingMode)
939 painter.drawImage(
940 self.__pasteRect.x(), self.__pasteRect.y(), img, 0, 0,
941 self.__pasteRect.width() + 1,
942 self.__pasteRect.height() + 1)
943
944 self.__undoStack.push(cmd)
945 cmd.setAfterImage(self.__image)
946
947 self.__updateImageRect(
948 self.__pasteRect.topLeft(),
949 self.__pasteRect.bottomRight() + QPoint(1, 1))
950 else:
951 E5MessageBox.warning(
952 self,
953 self.tr("Pasting Image"),
954 self.tr("""Invalid image data in clipboard."""))
955
956 def editPasteAsNew(self):
957 """
958 Public slot to paste the clipboard as a new image.
959 """
960 img, ok = self.__clipboardImage()
961 if ok:
962 cmd = IconEditCommand(
963 self, self.tr("Paste Clipboard as New Image"),
964 self.__image)
965 self.setIconImage(img)
966 self.setDirty(True)
967 self.__undoStack.push(cmd)
968 cmd.setAfterImage(self.__image)
969
970 def editSelectAll(self):
971 """
972 Public slot to select the complete image.
973 """
974 self.__unMark()
975
976 self.__startPos = QPoint(0, 0)
977 self.__endPos = QPoint(self.rect().bottomRight())
978 self.__markImage.fill(self.MarkColor.rgba())
979 self.__selRect = self.__image.rect()
980 self.__selectionAvailable = True
981 self.selectionAvailable.emit(True)
982
983 self.update()
984
985 def editClear(self):
986 """
987 Public slot to clear the image.
988 """
989 self.__unMark()
990
991 cmd = IconEditCommand(self, self.tr("Clear Image"), self.__image)
992 self.__image.fill(qRgba(0, 0, 0, 0))
993 self.update()
994 self.setDirty(True)
995 self.__undoStack.push(cmd)
996 cmd.setAfterImage(self.__image)
997
998 def editResize(self):
999 """
1000 Public slot to resize the image.
1001 """
1002 from .IconSizeDialog import IconSizeDialog
1003 dlg = IconSizeDialog(self.__image.width(), self.__image.height())
1004 res = dlg.exec()
1005 if res == QDialog.DialogCode.Accepted:
1006 newWidth, newHeight = dlg.getData()
1007 if (
1008 newWidth != self.__image.width() or
1009 newHeight != self.__image.height()
1010 ):
1011 cmd = IconEditCommand(self, self.tr("Resize Image"),
1012 self.__image)
1013 img = self.__image.scaled(
1014 newWidth, newHeight, Qt.AspectRatioMode.IgnoreAspectRatio,
1015 Qt.TransformationMode.SmoothTransformation)
1016 self.setIconImage(img)
1017 self.setDirty(True)
1018 self.__undoStack.push(cmd)
1019 cmd.setAfterImage(self.__image)
1020
1021 def editNew(self):
1022 """
1023 Public slot to generate a new, empty image.
1024 """
1025 from .IconSizeDialog import IconSizeDialog
1026 dlg = IconSizeDialog(self.__image.width(), self.__image.height())
1027 res = dlg.exec()
1028 if res == QDialog.DialogCode.Accepted:
1029 width, height = dlg.getData()
1030 img = QImage(width, height, QImage.Format.Format_ARGB32)
1031 img.fill(qRgba(0, 0, 0, 0))
1032 self.setIconImage(img)
1033
1034 def grayScale(self):
1035 """
1036 Public slot to convert the image to gray preserving transparency.
1037 """
1038 cmd = IconEditCommand(self, self.tr("Convert to Grayscale"),
1039 self.__image)
1040 for x in range(self.__image.width()):
1041 for y in range(self.__image.height()):
1042 col = self.__image.pixel(x, y)
1043 if col != qRgba(0, 0, 0, 0):
1044 gray = qGray(col)
1045 self.__image.setPixel(
1046 x, y, qRgba(gray, gray, gray, qAlpha(col)))
1047 self.update()
1048 self.setDirty(True)
1049 self.__undoStack.push(cmd)
1050 cmd.setAfterImage(self.__image)
1051
1052 def editUndo(self):
1053 """
1054 Public slot to perform an undo operation.
1055 """
1056 if self.__undoStack.canUndo():
1057 self.__undoStack.undo()
1058
1059 def editRedo(self):
1060 """
1061 Public slot to perform a redo operation.
1062 """
1063 if self.__undoStack.canRedo():
1064 self.__undoStack.redo()
1065
1066 def canUndo(self):
1067 """
1068 Public method to return the undo status.
1069
1070 @return flag indicating the availability of undo (boolean)
1071 """
1072 return self.__undoStack.canUndo()
1073
1074 def canRedo(self):
1075 """
1076 Public method to return the redo status.
1077
1078 @return flag indicating the availability of redo (boolean)
1079 """
1080 return self.__undoStack.canRedo()
1081
1082 def __cleanChanged(self, clean):
1083 """
1084 Private slot to handle the undo stack clean state change.
1085
1086 @param clean flag indicating the clean state (boolean)
1087 """
1088 self.setDirty(not clean)
1089
1090 def shutdown(self):
1091 """
1092 Public slot to perform some shutdown actions.
1093 """
1094 self.__undoStack.canRedoChanged.disconnect(self.canRedoChanged)
1095 self.__undoStack.canUndoChanged.disconnect(self.canUndoChanged)
1096 self.__undoStack.cleanChanged.disconnect(self.__cleanChanged)
1097
1098 def isSelectionAvailable(self):
1099 """
1100 Public method to check the availability of a selection.
1101
1102 @return flag indicating the availability of a selection (boolean)
1103 """
1104 return self.__selectionAvailable

eric ide

mercurial