eric6/IconEditor/IconEditorGrid.py

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

eric ide

mercurial