# -*- coding: utf-8 -*-

# Copyright (c) 2004 - 2005 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing a subclass of QCanvasView for our diagrams.
"""

import sys

from qt import *
from qtcanvas import *

from KdeQt import KQFileDialog, KQMessageBox
from KdeQt.KQPrinter import KQPrinter

from UMLWidget import UMLWidget
from UMLCanvasSizeDialog import UMLCanvasSizeDialog
from ZoomDialog import ZoomDialog
import Preferences

class UMLCanvasView(QCanvasView):
    """
    Class implementing a specialized canvasview for our diagrams.
    """
    def __init__(self, canvas, parent=None, name=None, flags=0):
        """
        Constructor
        
        @param canvas canvas that is managed by the view (QCanvas)
        @param parent parent widget of the view (QWidget)
        @param name name of the view widget (QString or string)
        @param flags the window flags to be passed to the view widget
        """
        QCanvasView.__init__(self, canvas, parent, name, flags)
        
        self.selectedWidgets = []
        self.selectionRect = QRect(0, 0, 1, 1)
        self.oldPosX = 0
        self.oldPosY = 0
        self.zoom = 1.0
        
        self.dragging = 0
        self.rubberbanding = 0
        self.rubberband = QCanvasRectangle(5, 5, 0, 0, canvas)
        self.rubberband.setPen(QPen(Qt.DotLine))
        self.rubberband.hide()
        
        QWhatsThis.add(self, self.trUtf8("<b>Diagram Canvas</b>\n"
            "<p>This canvas is used to show the selected diagram. \n"
            "There are various actions available to manipulate the \n"
            "shown widgets.</p>\n"
            "<ul>\n"
            "<li>Clicking on a widget selects it.</li>\n"
            "<li>Shift-clicking adds a widget to the selection.</li>\n"
            "<li>Shift-clicking a selected widget deselects it.</li>\n"
            "<li>Clicking on an empty spot of the canvas resets the selection.</li>\n"
            "<li>Dragging the mouse over the canvas spans a rubberband to \n"
            "select multiple widgets.</li>\n"
            "<li>Shift-dragging the mouse over the canvas spans a rubberband \n"
            "to add multiple widgets to the selection</li>\n"
            "<li>Dragging the mouse over a selected widget moves the \n"
            "whole selection.</li>\n"
            "</ul>\n"
            "<p><b>Note</b>: Widgets are the rectangular widgets on the canvas. \n"
            "The associations are adjusted automatically to the widgets.</p>"
        ))
        
        self.menuIDs = {}
        self.menu = QPopupMenu(self)
        self.menu.insertItem(self.trUtf8("Save as PNG"), self.handleSaveImage)
        self.menu.insertItem(self.trUtf8("Print"), self.handlePrintDiagram)
        self.menu.insertSeparator()
        self.menu.insertItem(self.trUtf8("Zoom in"), self.handleZoomIn)
        self.menu.insertItem(self.trUtf8("Zoom out"), self.handleZoomOut)
        self.menu.insertItem(self.trUtf8("Zoom..."), self.handleZoom)
        self.menu.insertItem(self.trUtf8("Reset Zoom"), self.handleZoomReset)
        self.menu.insertSeparator()
        self.menu.insertItem(self.trUtf8("Increase width by 100 points"),
            self.handleIncWidth)
        self.menu.insertItem(self.trUtf8("Increase height by 100 points"),
            self.handleIncHeight)
        self.menuIDs["DecWidth"] = \
            self.menu.insertItem(self.trUtf8("Decrease width by 100 points"), 
            self.handleDecWidth)
        self.menuIDs["DecHeight"] = \
            self.menu.insertItem(self.trUtf8("Decrease height by 100 points"), 
            self.handleDecHeight)
        self.menu.insertItem(self.trUtf8("Set size"), self.handleSetSize)
        self.menu.insertSeparator()
        self.menu.insertItem(self.trUtf8("Re-Layout"), self.handleReLayout)
        self.connect(self.menu, SIGNAL("aboutToShow()"), 
            self.handleShowPopupMenu)
        
    def contentsMousePressEvent(self, evt):
        """
        Overriden method to handle mouse button presses.
        
        This method determines the widget over which the button
        press occurred.
        
        @param evt mouse event (QMouseEvent)
        """
        # we're only interested in pure and shifted button presses
        if evt.state() & Qt.ControlButton or evt.state() & Qt.AltButton:
            evt.ignore()
            return
        if evt.button() == Qt.LeftButton:
            pos = self.inverseWorldMatrix().map(evt.pos())
            l = self.canvas().collisions(pos)
            if len(l) == 1:
                if isinstance(l[0], UMLWidget):
                    w = l[0]
                    if w not in self.selectedWidgets:
                        if evt.state() & Qt.ShiftButton:
                            self.selectedWidgets.append(w)
                        else:
                            for sw in self.selectedWidgets:
                                sw.setSelected(0)
                            self.selectedWidgets = [w]
                        w.setSelected(1)
                    else:
                        if evt.state() & Qt.ShiftButton:
                            self.selectedWidgets.remove(w)
                            w.setSelected(0)
                    srl = self.canvas().width()
                    srr = 0
                    srt = self.canvas().height()
                    srb = 0
                    for w in self.selectedWidgets:
                        wx = int(w.x())
                        wy = int(w.y())
                        ww = w.width()
                        wh = w.height()
                        srl = min(wx, srl)
                        srt = min(wy, srt)
                        srr = max(wx + ww, srr)
                        srb = max(wy + wh, srb)
                    self.selectionRect.setRect(srl, srt, srr - srl, srb - srt)
                    self.oldPosX = pos.x()
                    self.oldPosY = pos.y()
            else:
                if not (evt.state() & Qt.ShiftButton) and self.selectedWidgets:
                    for w in self.selectedWidgets:
                        w.setSelected(0)
                    self.selectedWidgets = []
                    self.selectionRect.setRect(0, 0, 1, 1)
                self.rubberbanding = 1 # start rubberbanding
                self.rubberband.move(pos.x(), pos.y())
                self.rubberband.setSize(0, 0)
                self.rubberband.show()
                
            self.canvas().update()
            
    def contentsMouseReleaseEvent(self, evt):
        """
        Overriden method to handle mouse button releases.
        
        This method simply resets the tracked widget.
        
        @param evt mouse event (QMouseEvent)
        """
        if evt.button() == Qt.LeftButton:
            if self.dragging:
                self.dragging = 0
                return
                
            if self.rubberbanding:
                self.rubberbanding = 0
                l = self.rubberband.collisions(1)
                for w in l:
                    if isinstance(w, UMLWidget) and \
                       w not in self.selectedWidgets:
                        self.selectedWidgets.append(w)
                        w.setSelected(1)
                self.rubberband.hide()
                self.rubberband.move(5, 5)
                self.canvas().update()
                return
        
    def contentsMouseMoveEvent(self, evt):
        """
        Overriden method to handle mouse moves.
        
        This method moves the widget according to the mouse
        movements.
        
        @param evt mouse event (QMouseEvent)
        """
        pos = self.inverseWorldMatrix().map(evt.pos())
        if self.rubberbanding:
            self.rubberband.setSize(int(pos.x() - self.rubberband.x()),
                                    int(pos.y() - self.rubberband.y()))
            pos = self.worldMatrix().map(pos)
            self.canvas().update()
            self.ensureVisible(pos.x(), pos.y(), 10, 10)
        elif self.selectedWidgets:
            self.dragging = 1
            cw = self.canvas().width() - 10
            ch = self.canvas().height() - 10
            newX = pos.x()
            newY = pos.y()
            if newX < 10:
                newX = 10
            elif newX > cw:
                newX = cw
            if newY < 10:
                newY = 10
            elif newY > ch:
                newY = ch
            offsetX = newX - self.oldPosX
            offsetY = newY - self.oldPosY
            self.oldPosX = newX
            self.oldPosY = newY
            srl = self.selectionRect.left()
            srr = self.selectionRect.right()
            srt = self.selectionRect.top()
            srb = self.selectionRect.bottom()
            if srl + offsetX < 10:
                offsetX = srl -10
            elif srr + offsetX > cw:
                offsetX = cw - srr
            if srt + offsetY < 10:
                offsetY = srt - 10
            elif srb + offsetY > ch:
                offsetY = ch - srb
            self.selectionRect.moveBy(offsetX, offsetY)
            for w in self.selectedWidgets:
                w.moveBy(offsetX, offsetY)
            self.canvas().update()
            pos = QPoint(newX, newY)
            pos = self.worldMatrix().map(pos)
            self.ensureVisible(pos.x(), pos.y(), 10, 10)
            
    def contextMenuEvent(self, evt):
        """
        Overriden method to handle a context menu event.
        
        @param evt context menu event (QContextMenuEvent)
        """
        evt.accept()
        self.menu.popup(evt.globalPos())        
        
    def handleShowPopupMenu(self):
        """
        Slot to handle the popup menu about to show signal.
        
        It is used to disable/enable certain menu items according
        to various conditions.
        """
        if self.canvas().width() <= self.width():
            self.menu.setItemEnabled(self.menuIDs["DecWidth"], 0)
        else:
            self.menu.setItemEnabled(self.menuIDs["DecWidth"], 1)
        if self.canvas().height() <= self.height():
            self.menu.setItemEnabled(self.menuIDs["DecHeight"], 0)
        else:
            self.menu.setItemEnabled(self.menuIDs["DecHeight"], 1)
        
    def handleResize(self, isWidth, amount):
        """
        Private method ti resize the drawing canvas.
        
        @param isWidth flag indicating width is to be resized (boolean)
        @param amount size increment (integer)
        """
        width = self.canvas().width()
        height = self.canvas().height()
        if isWidth:
            width += amount
        else:
            height += amount
        if width < self.width():
            width = self.width()
        if height < self.height():
            height = self.height()
        self.canvas().resize(width, height)
        
    def handleIncWidth(self):
        """
        Private method to handle the increase width context menu entry.
        """
        self.handleResize(1, 100)
        
    def handleIncHeight(self):
        """
        Private method to handle the increase height context menu entry.
        """
        self.handleResize(0, 100)
        
    def handleDecWidth(self):
        """
        Private method to handle the decrease width context menu entry.
        """
        self.handleResize(1, -100)
        
    def handleDecHeight(self):
        """
        Private method to handle the decrease height context menu entry.
        """
        self.handleResize(0, -100)
        
    def handleSetSize(self):
        """
        Private method to handle the set size context menu entry.
        """
        cv = self.canvas()
        dlg = UMLCanvasSizeDialog(cv.width(), cv.height(),
                                  self.width(), self.height(), 
                                  self, None, 1)
        if dlg.exec_loop() == QDialog.Accepted:
            width, height = dlg.getData()
            cv.resize(width, height)
        
    def handleSaveImage(self):
        """
        Private method to handle the save context menu entry.
        """
        selectedFilter = QString('')
        fname = KQFileDialog.getSaveFileName(\
            None,
            self.trUtf8("Portable Network Graphics (*.png)"),
            None, None,
            self.trUtf8("Save Diagram"),
            selectedFilter, 1)
        if not fname.isEmpty():
            ext = QFileInfo(fname).extension()
            if ext.isEmpty():
                ex = selectedFilter.section('(*',1,1).section(')',0,0)
                if not ex.isEmpty():
                    fname.append(ex)
            if QFileInfo(fname).exists():
                abort = KQMessageBox.warning(self,
                    self.trUtf8("Save Diagram"),
                    self.trUtf8("<p>The file <b>%1</b> already exists.</p>")
                        .arg(fname),
                    self.trUtf8("&Overwrite"),
                    self.trUtf8("&Abort"), None, 1)
                if abort:
                    return
                    
            rect = self.getDiagramRect(10)
            px = QPixmap(rect.width(), rect.height())
            self.getDiagram(rect, px)
            success = px.save(fname, "PNG")
            if not success:
                KQMessageBox.critical(None,
                    self.trUtf8("Save Diagram"),
                    self.trUtf8("""<p>The file <b>%1</b> could not be saved.</p>""")
                        .arg(fname),
                    self.trUtf8("&OK"),
                    None,
                    None,
                    0, -1)
                    
    def handleReLayout(self):
        """
        Private method to handle the re-layout context menu entry.
        """
        cv = self.canvas()
        items = cv.allItems()
        while items:
            items[0].hide()
            del items[0]
        self.parent().relayout()
        cv.update()
        
    def handlePrintDiagram(self):
        """
        Private slot called to print the diagram.
        """
        printer = KQPrinter()
        printer.setFullPage(1)
        if Preferences.getPrinter("ColorMode"):
            printer.setColorMode(KQPrinter.Color)
        else:
            printer.setColorMode(KQPrinter.GrayScale)
        if Preferences.getPrinter("FirstPageFirst"):
            printer.setPageOrder(KQPrinter.FirstPageFirst)
        else:
            printer.setPageOrder(KQPrinter.LastPageFirst)
        printer.setPrinterName(Preferences.getPrinter("PrinterName"))
        if printer.setup(self):
            p = QPainter(printer)
            metrics = QPaintDeviceMetrics(p.device())
            offsetX = 0
            offsetY = 0
            widthX = 0
            heightY = 0
            font = QFont("times", 10)
            p.setFont(font)
            fm = p.fontMetrics()
            fontHeight = fm.lineSpacing()
            marginX = printer.margins().width()
            marginY = printer.margins().height()
            
            # double the margin on bottom of page
            if printer.orientation() == KQPrinter.Portrait:
                width = metrics.width() - marginX * 2
                height = metrics.height() - fontHeight - 4 - marginY * 3
            else:
                marginX *= 2
                width = metrics.width() - marginX * 2
                height = metrics.height() - fontHeight - 4 - marginY * 2
            
            rect = self.getDiagramRect(5)
            diagram = QPixmap(rect.width(), rect.height())
            self.getDiagram(rect, diagram)
            
            finishX = 0
            finishY = 0
            page = 0
            pageX = 0
            pageY = 1
            while not finishX or not finishY:
                if not finishX:
                    offsetX = pageX * width
                    pageX += 1
                elif not finishY:
                    offsetY = pageY * height
                    offsetX = 0
                    pageY += 1
                    finishX = 0
                    pageX = 1
                if (width + offsetX) > diagram.width():
                    finishX = 1
                    widthX = diagram.width() - offsetX
                else:
                    widthX = width
                if diagram.width() < width:
                    widthX = diagram.width()
                    finishX = 1
                    offsetX = 0
                if (height + offsetY) > diagram.height():
                    finishY = 1
                    heightY = diagram.height() - offsetY
                else:
                    heightY = height
                if diagram.height() < height:
                    finishY = 1
                    heightY = diagram.height()
                    offsetY = 0
                    
                p.drawPixmap(marginX, marginY, diagram,
                             offsetX, offsetY, widthX, heightY)
                # write a foot note
                s = QString(self.trUtf8("Diagram: %1, Page %2")
                    .arg(self.parent().getDiagramName()).arg(page + 1))
                tc = QColor(50, 50, 50)
                p.setPen(tc)
                p.drawRect(marginX, marginY, width, height)
                p.drawLine(marginX, marginY + height + 2, 
                           marginX + width, marginY + height + 2)
                p.setFont(font)
                p.drawText(marginX, marginY + height + 4, width,
                           fontHeight, Qt.AlignRight, s)
                if not finishX or not finishY:
                    printer.newPage()
                    page += 1
                
            p.end()
        
    def getDiagramRect(self, border=0):
        """
        Method to calculate the minimum rectangle fitting the diagram.
        
        @param border border width to include in the calculation (integer)
        @return the minimum rectangle (QRect)
        """
        startx = sys.maxint
        starty = sys.maxint
        endx = 0
        endy = 0
        items = self.canvas().allItems()
        for itm in items:
            if isinstance(itm, UMLWidget):
                itmEndX = int(itm.x()) + itm.width()
                itmEndY = int(itm.y()) + itm.height()
                itmStartX = int(itm.x())
                itmStartY = int(itm.y())
                if startx >= itmStartX:
                    startx = itmStartX
                if starty >= itmStartY:
                    starty = itmStartY
                if endx <= itmEndX:
                    endx = itmEndX
                if endy <= itmEndY:
                    endy = itmEndY
        if border:
            startx -= border
            starty -= border
            endx += border
            endy += border
            
        return QRect(startx, starty, endx - startx, endy - starty)
        
    def getDiagram(self, rect, diagram):
        """
        Method to retrieve the diagram from the canvas fitting it in the minimum rectangle.
        
        @param rect minimum rectangle fitting the diagram (QRect)
        @param diagram pixmap to receive the diagram (QPixmap)
        """
        # step 1: deselect all widgets
        if self.selectedWidgets:
            for w in self.selectedWidgets:
                w.setSelected(0)
            self.canvas().update()
            
        # step 2: grab the diagram
        pixmap = QPixmap(rect.x() + rect.width(), rect.y() + rect.height())
        painter = QPainter()
        painter.begin(pixmap)
        self.canvas().drawArea(QRect(0, 0, pixmap.width(), pixmap.height()), painter)
        painter.end()
        bitBlt(diagram, QPoint(0, 0), pixmap, rect)
        
        # step 3: reselect the widgets
        if self.selectedWidgets:
            for w in self.selectedWidgets:
                w.setSelected(1)
            self.canvas().update()
        
    def doZoom(self):
        """
        Private method to perform the zooming.
        """
        wm = QWMatrix()
        wm.scale(self.zoom, self.zoom)
        self.setWorldMatrix(wm)
        
    def handleZoomIn(self):
        """
        Private method to handle the zoom in context menu entry.
        """
        self.zoom *= 2.0
        self.doZoom()
        
    def handleZoomOut(self):
        """
        Private method to handle the zoom out context menu entry.
        """
        self.zoom /= 2.0
        self.doZoom()
    
    def handleZoomReset(self):
        """
        Private method to handle the reset zoom context menu entry.
        """
        self.zoom = 1.0
        self.doZoom()
        
    def handleZoom(self):
        """
        Private method to handle the zoom context menu action.
        """
        dlg = ZoomDialog(self.zoom, self, None, 1)
        if dlg.exec_loop() == QDialog.Accepted:
            self.zoom = dlg.getZoomSize()
            self.doZoom()
