src/eric7/IconEditor/IconEditorGrid.py

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

eric ide

mercurial