IconEditor/IconEditorGrid.py

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

eric ide

mercurial