#!/usr/bin/python
# -*- coding: Utf-8 -*-

# Script grandement inspiré et copié du code du livre «Rapid Gui Programming with Python and Qt»
# de Mark Summerfield (livre que je vous recommande vivement)


from __future__ import division
import functools, random, string, locale, gettext
import sys, os
from PyQt4.QtCore import *
from PyQt4.QtGui import *

MAC = "qt_mac_set_native_menubar" in dir()

## A4 in points
#PageSize = (595, 842)

## US Letter in points
#PageSize = (612, 792)

#PageSize = (355, 500)

PointSize = 10

MagicNumber = 0x70616765

FORMAT = "JPEG (*.jpg *.JPG *.jpeg *.JPEG);;PNG (*.png *.PNG);;GIF (*.gif *.GIF)\
	;;BMP (*.bmp *.BMP);;SGI (*.sgi *.SGI);;TIF (*.tif *.TIF);;TARGA (*.tga *.TGA)\
	;;PNM (*.pnm *.PNM);;PPM (*.ppm *.PPM);;Tous les fichiers (*.*)"

class TextItemDlg(QDialog):

	def __init__(self, item=None, position=None, scene=None,
			parent=None):
		QDialog.__init__(self, parent)
		
		self.item = item
		self.position = position
		self.scene = scene
		
		self.editor = QTextEdit()
		self.editor.setAcceptRichText(False)
		self.editor.setTabChangesFocus(True)
		editorLabel = QLabel(_(u"&Texte:"))
		editorLabel.setBuddy(self.editor)
		self.fontComboBox = QFontComboBox()
		self.fontComboBox.setCurrentFont(QFont("Sans Serif", PointSize))
		fontLabel = QLabel(_(u"&Police:"))
		fontLabel.setBuddy(self.fontComboBox)
		self.fontSpinBox = QSpinBox()
		self.fontSpinBox.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
		self.fontSpinBox.setRange(6, 280)
		self.fontSpinBox.setValue(PointSize)
		fontSizeLabel = QLabel(_(u"&Taille:"))
		fontSizeLabel.setBuddy(self.fontSpinBox)
		self.fontComboColor = QComboBox()
		
		self.listComboColor=[	(_(u'défaut'),		QColor(QPalette.Text)),
					(_(u'noir'),		QColor('#000000')),
					(_(u'gris foncé'),	QColor('#808080')),
					(_(u'gris'),		QColor('#a0a0a4')),
					(_(u'gris clair'),	QColor('#c0c0c0')),
					(_(u'blanc'),		QColor('#ffffff')),
					(_(u'rouge'),		QColor('#ff0000')),
					(_(u'rouge foncé'),	QColor('#800000')),
					(_(u'vert'),		QColor('#00ff00')),
					(_(u'vert foncé'),	QColor('#008000')),
					(_(u'bleu'),		QColor('#0000ff')),
					(_(u'bleu foncé'),	QColor('#000080')),
					(_(u'cyan'),		QColor('#00ffff')),
					(_(u'cyan foncé'),	QColor('#008080')),
					(_(u'magenta'),		QColor('#ff00ff')),
					(_(u'magenta foncé'),	QColor('#800080')),
					(_(u'jaune'),		QColor('#ffff00')),
					(_(u'jaune foncé'),	QColor('#808000')),
						]
		
		font =QFont()
		fm = QFontMetricsF(font)
		hauteurPixmap = fm.height() - 2 # icones espacées de 2 pixels
		
		for i in self.listComboColor:
			pixmap = QPixmap(hauteurPixmap, hauteurPixmap)
			color = i[1]
			pixmap.fill(color)
			self.fontComboColor.addItem(QIcon(pixmap), i[0], QVariant(i[1]))
		fontColorLabel = QLabel(_(u"&Couleur:"))
		fontColorLabel.setBuddy(self.fontComboColor)
		self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok|
						QDialogButtonBox.Cancel)
		self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False)
		
		if self.item is not None:
			self.editor.setPlainText(self.item.toPlainText())
			self.fontComboBox.setCurrentFont(self.item.font())
			self.fontSpinBox.setValue(self.item.font().pointSize())
			for i, tColor in enumerate(self.listComboColor):
				if tColor[1]==self.item.defaultTextColor(): break
			self.fontComboColor.setCurrentIndex(i)
			self.editor.setTextColor(self.item.defaultTextColor())
		
		layout = QGridLayout()
		layout.addWidget(editorLabel, 0, 0)
		layout.addWidget(self.editor, 1, 0, 1, 9)
		layout.addWidget(fontLabel, 2, 0)
		layout.addWidget(self.fontComboBox, 2, 1, 1, 2)
		layout.addWidget(fontSizeLabel, 2, 3)
		layout.addWidget(self.fontSpinBox, 2, 4, 1, 2)
		layout.addWidget(fontColorLabel, 2, 6)
		layout.addWidget(self.fontComboColor, 2, 7, 1, 2)
		layout.addWidget(self.buttonBox, 3, 0, 1, 9)
		self.setLayout(layout)
	
		self.connect(self.fontComboBox,SIGNAL("currentFontChanged(QFont)"),self.updateUi)
		self.connect(self.fontSpinBox,SIGNAL("valueChanged(int)"),self.updateUi)
		self.connect(self.editor,SIGNAL("textChanged()"),self.updateUi)
		self.connect(self.fontComboColor, SIGNAL("activated(int)"),self.updateUi)
		self.connect(self.buttonBox, SIGNAL("accepted()"), self.accept)
		self.connect(self.buttonBox, SIGNAL("rejected()"), self.reject)
		txt1,txt2=_(u"Texte sur images"),_(u"texte")
		if self.item is None: txt3 = _(u"Ajouter")
		else:  txt3 = _(u"Editer")
		self.setWindowTitle("%s - %s %s" % (txt1, txt3, txt2))
		self.updateUi()


	def updateUi(self):
		font = self.fontComboBox.currentFont()
		font.setPointSize(self.fontSpinBox.value())
		self.editor.document().setDefaultFont(font)
		i = self.fontComboColor.currentIndex()
		styleSheet = "QTextEdit{color: %s}" %self.fontHexaColor()
		self.editor.setStyleSheet(styleSheet)
		self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(
			not self.editor.toPlainText().isEmpty())


	def accept(self):
		if self.item is None:
			self.item = TextItem("", self.position, self.scene)
		font = self.fontComboBox.currentFont()
		font.setPointSize(self.fontSpinBox.value())
		self.item.setFont(font)
		self.item.setHtml(self.editor.toHtml())
		self.item.setDefaultTextColor(self.fontColor())
		self.item.update()
		QDialog.accept(self)
	
	
	def fontColor(self):
		"Renvoie la couleur de la police en type QColor"
		i = self.fontComboColor.currentIndex()
		color = self.fontComboColor.itemData(i).toString()
		return QColor(color)
	
	
	def fontHexaColor(self):
		"Renvoie la couleur de la police en type hexadécimal"
		return self.fontColor().toRgb().name()


class TextItem(QGraphicsTextItem):

	def __init__(self, text, position, scene,
			font=QFont("Sans Serif", PointSize), matrix=QMatrix(), color=QColor(0,0,0)):
		QGraphicsTextItem.__init__(self, text)
		self.setFlags(QGraphicsItem.ItemIsSelectable|
				QGraphicsItem.ItemIsMovable)
		self.setFont(font)
		self.setPos(position)
		self.setMatrix(matrix)
		self.setDefaultTextColor(color)
		scene.clearSelection()
		scene.addItem(self)
		self.setSelected(True)
		print "adresse classe", self


	def parentWidget(self):
		return self.scene().views()[0]
		
	def hexaColor(self):
		return self.defaultTextColor().toRgb().name()
	
	def mouseDoubleClickEvent(self, event):
		dialog = TextItemDlg(self, self.parentWidget())
		dialog.exec_()


class BoxItem(QGraphicsItem):

	def __init__(self, position, scene, style=Qt.SolidLine,
			rect=None, matrix=QMatrix()):
		QGraphicsItem.__init__(self)
		self.setFlags(QGraphicsItem.ItemIsSelectable|
				QGraphicsItem.ItemIsMovable|
				QGraphicsItem.ItemIsFocusable)
		if rect is None:
			rect = QRectF(-10 * PointSize, -PointSize,
					20 * PointSize, 2 * PointSize)
		self.rect = rect
		self.style = style
		self.setPos(position)
		self.setMatrix(matrix)
		scene.clearSelection()
		scene.addItem(self)
		self.setSelected(True)
		self.setFocus()


	def parentWidget(self):
		return self.scene().views()[0]


	def boundingRect(self):
		return self.rect.adjusted(-2, -2, 2, 2)


	def paint(self, painter, option, widget):
		pen = QPen(self.style)
		pen.setColor(Qt.black)
		pen.setWidth(1)
		if option.state & QStyle.State_Selected:
			pen.setColor(Qt.blue)
		painter.setPen(pen)
		painter.drawRect(self.rect)


	def contextMenuEvent(self, event):
		wrapped = [] # indispensable mais je ne sais pas pourquoi
		print "contextMenuEvent_contextMenuEvent_contextMenuEvent"
		menu = QMenu(self.parentWidget())
		for text, param in (
			(_(u"Contour &solide"), Qt.SolidLine),
			(_(u"Contour en &pointillets"), Qt.DashLine),
			(_(u"Contour en p&oints"), Qt.DotLine),
			(_(u"Contour en po&intillets et points"), Qt.DashDotLine),
			(_(u"Contour en pointillets et poin&ts (x2)"), Qt.DashDotDotLine)):
			wrapper = functools.partial(self.setStyle, param)
			wrapped.append(wrapper)
			menu.addAction(text, wrapper)
		menu.exec_(event.screenPos())


	def setStyle(self, style):
		self.style = style
		self.update()


	def keyPressEvent(self, event):
		factor = PointSize / 4
		changed = False
		if event.modifiers() & Qt.ShiftModifier:
			if event.key() == Qt.Key_Left:
				self.rect.setRight(self.rect.right() - factor)
				changed = True
			elif event.key() == Qt.Key_Right:
				self.rect.setRight(self.rect.right() + factor)
				changed = True
			elif event.key() == Qt.Key_Up:
				self.rect.setBottom(self.rect.bottom() - factor)
				changed = True
			elif event.key() == Qt.Key_Down:
				self.rect.setBottom(self.rect.bottom() + factor)
				changed = True
		if changed:
			self.update()
		else:
			QGraphicsItem.keyPressEvent(self, event)


class GraphicsView(QGraphicsView):

	def __init__(self, parent=None):
		QGraphicsView.__init__(self, parent)
		self.setDragMode(QGraphicsView.RubberBandDrag)
		self.setRenderHint(QPainter.Antialiasing)
		self.setRenderHint(QPainter.TextAntialiasing)
	
	def wheelEvent(self, event):
		factor = 1.41 ** (-event.delta() / 240.0)
		self.scale(factor, factor)


class Image_Divers_TxtSurImg(QWidget):
	# -----------------------------------
	# Cadre accueillant les widgets de :
	# Image >> Divers >> Texte sur images
	# -----------------------------------
	
	ZOOM_MOINS,ZOOM_PLUS,ZOOM_REEL = range(3)
	
	def __init__(self, parent):
        	QWidget.__init__(self)
		
		self.filename = QString() # sert pour le chemin des boites de dialogue
		self.listFileNames = QStringList()
		self.copiedItem = QByteArray()
		self.pasteOffset = 5
		self.prevPoint = QPoint()
		self.addOffset = 5
		self.borders = []
	
		self.view = GraphicsView()
		self.scene = QGraphicsScene(self)
		self.view.setScene(self.scene)
		
		# Image de fond
		self.background = QGraphicsPixmapItem()
		
		buttonLayout = QVBoxLayout()
		# Liste qui servira à activer/désactiver les boutons
		self.buttonsList = []
		self.zoomPlus = functools.partial(self.zoom, Image_Divers_TxtSurImg.ZOOM_PLUS)
		self.zoomMoins = functools.partial(self.zoom, Image_Divers_TxtSurImg.ZOOM_MOINS)
		self.zoomReel = functools.partial(self.zoom, Image_Divers_TxtSurImg.ZOOM_REEL)
		for text, slot in (
			(_(u"C&harger Images..."), self.openImg),
			(_(u"Ajouter &Texte"), self.addText),
			#(_(u"Ajouter &Boite"), self.addBox), # prochainement...
			(_(u"Ajouter Ima&ge"), self.addPixmap),
			(_(u"C&opier"), self.copy),
			(_(u"Cou&per"), self.cut),
			(_(u"Co&ller"), self.paste),
			(_(u"&Effacer..."), self.delete),
			#(_(u"Pivo&ter"), self.rotate), # prochainement...
			(_(u"Zoom &avant"), self.zoomPlus),
			(_(u"Zoom a&rrière"), self.zoomMoins),
			(_(u"Zoom &100%"), self.zoomReel),
			):
			button = QPushButton(text)
			if not text == "C&harger Images...":
				button.setEnabled(False)
				self.buttonsList.append(button)
			if not MAC:
				button.setFocusPolicy(Qt.NoFocus)
			self.connect(button, SIGNAL("clicked()"), slot)
			buttonLayout.addWidget(button)
			if text == "C&harger Images...":
				button.setToolTip(_(u"charge les images de fond"))
				buttonLayout.addStretch(0)
			if text == "&Effacer...": # provisoire...
				buttonLayout.addStretch(0)
			#if text == "Pivo&ter": # prochainement...
				#buttonLayout.addStretch(0)
		buttonLayout.addStretch()
		
		vbox = QVBoxLayout()
		hbox = QHBoxLayout()
		hbox.addWidget(self.view, 1)
		hbox.addLayout(buttonLayout)
		vbox.addLayout(hbox)
		
		#-----------------
		# widgets du bas
		#-----------------
		
		# Boutons
		boutAide=QPushButton(_(u" Aide"))
		boutAide.setIcon(QIcon("Icones/icone_aide_128.png"))
		self.connect(boutAide, SIGNAL("clicked()"), self.afficherAide)
		
		self.boutAppliquer=QPushButton(_(u"Appliquer"))
		self.boutAppliquer.setIcon(QIcon("Icones/icone_appliquer_128.png"))
		
		# Bouton inactif au départ
		self.boutAppliquer.setEnabled(False)
		
		self.connect(self.boutAppliquer, SIGNAL("clicked()"), self.appliquer)
		
		# Ligne de séparation juste au dessus des boutons
		ligne = QFrame()
		ligne.setFrameShape(QFrame.HLine)
		ligne.setFrameShadow(QFrame.Sunken)
		vbox.addWidget(ligne)
		
		hbox=QHBoxLayout()
		hbox.addWidget(boutAide)
		hbox.addStretch()
		hbox.addWidget(self.boutAppliquer)
		vbox.addLayout(hbox)
		
		self.setLayout(vbox)
	
	
	def position(self, pixmap=None):
		print "point", QCursor.pos().x(), QCursor.pos().y() ###
		point = self.mapFromGlobal(QCursor.pos())
		print "pointbis", point.x(), point.y() ###
		if not self.view.geometry().contains(point) or pixmap:
			print "111"
			coord = random.randint(36, 144)
			point = QPoint(coord, coord)
		else:
			print "112"
			if point == self.prevPoint:
				print "113"
				point += QPoint(self.addOffset, self.addOffset)
				self.addOffset += 5
			else:
				print "114"
				self.addOffset = 5
				self.prevPoint = point
		print "pointter", point.x(), point.y(), '\n' ###
		# la position du point haut-gauche du rectangle encadrant le texte
		print "pointquater", self.view.mapToScene(point).x(), self.view.mapToScene(point).y() ###
		return self.view.mapToScene(point)


	def addText(self):
		dialog = TextItemDlg(position=self.position(), scene=self.scene, parent=self)
		dialog.exec_()
		self.voirCacherBoutonAppliquer()
	
	
	def addBox(self):
		BoxItem(self.position(), self.scene)
		self.voirCacherBoutonAppliquer()
	
	
	def addPixmap(self):
		formats = ["*.%s" % unicode(format).lower() \
			for format in QImageReader.supportedImageFormats()]
		path = QFileInfo(self.filename).path() \
			if not self.filename.isEmpty() else QDir.homePath()
		txt = _(u"Fichiers Image")
		fname = QFileDialog.getOpenFileName(self, "Ouvrir des images",
			path, "%s (%s);;%s" %(txt, " ".join(formats), FORMAT))
		if fname.isEmpty():
			return
		self.filename = fname
		self.createPixmapItem(QPixmap(fname), self.position(pixmap=True))
		self.voirCacherBoutonAppliquer()
	
	
	def createPixmapItem(self, pixmap, position, matrix=QMatrix()):
		item = QGraphicsPixmapItem(pixmap)
		item.setFlags(QGraphicsItem.ItemIsSelectable|
			QGraphicsItem.ItemIsMovable)
		item.setPos(position)
		item.setMatrix(matrix)
		self.scene.clearSelection()
		self.scene.addItem(item)
		item.setSelected(True)
	
	
	def selectedItem(self):
		items = self.scene.selectedItems()
		if len(items) == 1:
			return items[0]
		return None
	
	
	def copy(self):
		item = self.selectedItem()
		if item is None:
			return
		self.copiedItem.clear()
		self.pasteOffset = 5
		stream = QDataStream(self.copiedItem, QIODevice.WriteOnly)
		self.writeItemToStream(stream, item)
	
	
	def cut(self):
		item = self.selectedItem()
		if item is None:
			return
		self.copy()
		self.scene.removeItem(item)
		del item
		self.voirCacherBoutonAppliquer()
	
	
	def paste(self):
		if self.copiedItem.isEmpty():
			return
		stream = QDataStream(self.copiedItem, QIODevice.ReadOnly)
		self.readItemFromStream(stream, self.pasteOffset)
		self.pasteOffset += 5
		self.voirCacherBoutonAppliquer()
	
	
	def rotate(self):
		for item in self.scene.selectedItems():
			item.rotate(30)
	
	
	def delete(self):
		items = self.scene.selectedItems()
		print "items sélectionnés", items ###
		txt1,txt2=_(u"Effacer"),_(u"élément")
		if len(items) and QMessageBox.question(self,
			_(u"Texte sur images - Effacer"),
			"%s %d %s%s?" % (txt1,len(items),
			txt2,"s" if len(items) != 1 else ""),
			QMessageBox.Yes|QMessageBox.No) == QMessageBox.Yes:
			while items:
				item = items.pop()
				print "item?", item
				self.scene.removeItem(item)
				del item
		self.voirCacherBoutonAppliquer()
	
	
	def openImg(self):
		"Chargement des images qui vont accueillir les motifs (textes, images)"
		formats = ["*.%s" % unicode(format).lower() \
			for format in QImageReader.supportedImageFormats()]
		path = QFileInfo(self.filename).path() \
			if not self.filename.isEmpty() else QDir.homePath()
		txt = _(u"Fichiers Image")
		fname = QFileDialog.getOpenFileNames(self, "Ouvrir des images",
			path, "%s (%s);;%s" %(txt, " ".join(formats), FORMAT))
		
		if fname.isEmpty():
			return
		self.filename = fname[0]
		self.listFileNames = fname
		
		# Sélection et affichage de la plus petite image pour être sûr de ne
		# pas écrire d'item dans le vide
		aire = 10e20
		plusPetiteImg = fname[0]
		for image in fname:
			img = QImage(image)
			aireProv = img.width() * img.height()
			if aireProv < aire:
				aire = aireProv
				plusPetiteImg = image
		
		pixmap = QPixmap(plusPetiteImg)
		
		# Suppression de la précédente image si elle existait
		if not self.background.pixmap().isNull():
			self.scene.removeItem(self.background)
			del self.background
		
		# Ajout de la nouvelle image
		self.scene.setSceneRect(0, 0, pixmap.width(), pixmap.height())
		self.background = self.scene.addPixmap(pixmap)
		
		for button in self.buttonsList:
			button.setEnabled(True)
		self.voirCacherBoutonAppliquer()
	
	
	def readItemFromStream(self, stream, offset=0):
		type = QString()
		position = QPointF()
		matrix = QMatrix()
		color = QColor()
		stream >> type >> position >> matrix
		if offset:
			position += QPointF(offset, offset)
		if type == "Text":
			text = QString()
			font = QFont()
			stream >> text >> font >> color
			TextItem(text, position, self.scene, font, matrix, color)
		elif type == "Box":
			rect = QRectF()
			stream >> rect
			style = Qt.PenStyle(stream.readInt16())
			BoxItem(position, self.scene, style, rect, matrix)
		elif type == "Pixmap":
			pixmap = QPixmap()
			stream >> pixmap
			self.createPixmapItem(pixmap, position, matrix)
	
	
	def writeItemToStream(self, stream, item):
		if isinstance(item, QGraphicsTextItem):
			stream << QString("Text") << item.pos() << item.matrix() \
			<< item.toPlainText() << item.font() << item.defaultTextColor()
		elif isinstance(item, QGraphicsPixmapItem):
			stream << QString("Pixmap") << item.pos() \
			<< item.matrix() << item.pixmap()
		elif isinstance(item, BoxItem):
			stream << QString("Box") << item.pos() << item.matrix() \
			<< item.rect
			stream.writeInt16(item.style)
	
	
	def appliquer(self):
		"Conversion des images: ajout de textes, images et boites"
		
		path = QFileInfo(self.filename).path() \
			if not self.filename.isEmpty() else QDir.homePath()
		
		fname = QFileDialog.getSaveFileName(self,"Sauver les images", path)
		if fname.isEmpty(): return
		
		imgLen = len(self.listFileNames)
		
		self.filename = fname
		
		painter = QPainter()
		
		progress=QProgressDialog(_(u"Conversion en cours..."), _(u"Arrêter"), 0, 100)
		progress.setWindowTitle(_(u'EnKoDeur-Mixeur. Fenêtre de progression'))
		progress.show()
		progress.setValue(0)
		
		for index, img in enumerate(self.listFileNames):
			imgObj = QImage(img)
			painter.begin(imgObj)
			painter.setRenderHint(QPainter.TextAntialiasing)
			for item in self.scene.items():
				if isinstance(item,  QGraphicsTextItem):
					painter.setFont(item.font())
					painter.setPen(item.defaultTextColor())
					text = item.toPlainText().split('\n')
					fm = QFontMetricsF(item.font())
					h = fm.height()
					x, y = item.pos().x(), item.pos().y()
					# en considérant que la hauteur marge de la boite = 1/4 de
					# la hauteur d'une police et que le tracé d'une ligne se
					# fait à partir du pixel inférieur gauche du 1er caractère
					y += 3/4*h
					for ligne in text:
						painter.drawText(x, y, ligne)
						y += h
				elif isinstance(item, QGraphicsPixmapItem) and item!=self.background:
					painter.drawImage(item.pos(), item.pixmap().toImage())
				elif isinstance(item, BoxItem):
					print "PAS FAIT!"
			painter.end()
			path = QFileInfo(fname).absolutePath()
			baseName = QFileInfo(fname).completeBaseName()
			suffix = QFileInfo(img).suffix()
			sauvPath = path+'/'+baseName+'_'+string.zfill((index+1), 6)+"."+suffix
			imgObj.save(sauvPath)
			progress.setValue(int(100*(index+1)/imgLen))
		
		# Boite de dialogue d'information
		QMessageBox.information(self, _(u"Appliquer - Items sur images"),
			_(u"Les items ont été écrits sur les images avec succès!"))
	
	
	def zoom(self, val):
		if val==Image_Divers_TxtSurImg.ZOOM_PLUS:
			self.view.scale(1.2, 1.2)
		elif val==Image_Divers_TxtSurImg.ZOOM_MOINS:
			self.view.scale(0.8, 0.8)
		elif val==Image_Divers_TxtSurImg.ZOOM_REEL:
			self.view.resetTransform()
	
	
	def voirCacherBoutonAppliquer(self):
		# Rappel: la bordure est aussi un item -> soustraction de celui-ci
		enable = not self.listFileNames.isEmpty() and len(self.scene.items()) - 1
		self.boutAppliquer.setEnabled(enable)
	
	
	def afficherAide(self):
		"Boîte de dialogue de l'aide"
		messageAide=QMessageBox(self)
		messageAide.setText(_(u"Blablablabla ... blabla."))
		messageAide.setWindowTitle(_(u"Aide"))
		messageAide.setIcon(QMessageBox.Information)
		messageAide.exec_()