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

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

"""
Module implementing the log viewer widget and the log widget.
"""

from qt import *

import UI.PixmapCache

class LogViewWidget(QWidget):
    """
    Base class for widgets to be displayed by LogView.
    
    Subclasses may emit the signal sizeChanged(LogViewWidget)
    to tell the container that a resizing may be necessary.
    """
    def __init__(self, parent=None, name=None):
        """
        Constructor
        
        @param parent parent widget (QWidget)
        @param name name of this widget (string or QString)
        """
        QWidget.__init__(self, parent, name)
        
    def preferredBackgroundColor(self):
        """
        Private method setting the background colour.
        """
        return self.colorGroup().dark()
    
class LogWidget(LogViewWidget):
    """
    A class for displaying logging messages.
    
    LogWidget is a simple class to be plugged in a LogView. 
    It displays lines of text without interpretation of richtext or html tags. 
    Neither are special characters like newlines or tabs interpreted.
    The widget can either store all the strings sent to it (the default) or 
    can limit the number of lines to store. In this case the oldest lines 
    are discarded when new lines arrive.
    """
    def __init__(self, parent=None, name=None):
        """
        Constructor
        
        @param parent parent widget (QWidget)
        @param name name of this widget (string or QString)
        """
        LogViewWidget.__init__(self, parent, name)
        self.__listLines = QStringList()
        self.__maxLines = -1
        self.__maxLen = 0
        self.setPaletteBackgroundColor(self.colorGroup().base())
        self.setPaletteForegroundColor(self.colorGroup().text())
        
    def preferredBackgroundColor(self):
        """
        Reimplemented to return colorgroup().base().
        
        @return preferred background colour (QColor)
        """
        return self.colorGroup().base()
        
    def paintEvent(self, evt):
        """
        Reimplemented for custom painting.
        
        @param evt the paint event object (QPaintEvent)
        """
        fm = self.fontMetrics()
        height = fm.height()
        x = 2
        y = height
        p = QPainter(self)
        
        for line in self.__listLines:
            p.drawText(x, y, line)
            y += height
            
    def handleSetMaxLines(self, val):
        """
        Sets the maximum number of lines to be shown. 
        
        @param val maximum number of lines to be displayed
                If val is <= 0 then there will be no limit. If the maximum number 
                of lines is appended, the oldest are discarded.
        """
        self.__maxLines = val
        
    def append(self, text):
        """
        Public method to append text to the messages. 
        
        When the LogWidget is already scrolled to the bottom, it will 
        further scroll down to display the newly added line. If the 
        scrolling position is not at the end, this position is not changed.
        
        @param text text to be appended (string or QString)
        """
        fm = self.fontMetrics()
        self.__listLines.append(text)
        self.__maxLen = max(self.__maxLen, fm.boundingRect(text).width()+2)
        if self.__maxLines > 0 and self.__listLines.size() > self.__maxLines:
            self.__listLines = self.__listLines[-self.__maxLen:]
        self.setMinimumSize(self.__maxLen, self.__listLines.count()*fm.height() + 2)
        self.emit(PYSIGNAL('sizeChanged'), (self,))
        self.update()
        qApp.processEvents(10)
        
    def clear(self):
        """
        Public method to delete all strings from the internal buffer and clears the display.
        """
        self.__listLines.clear()
        fm = self.fontMetrics()
        self.setMinimumSize(QSize(min(200, self.__maxLen*fm.maxWidth()),
                                min(200, self.__listLines.count()*fm.height() + 2)))
        self.resize(1, 1)
        self.emit(PYSIGNAL('sizeChanged'), (self,))
        self.update()
        
    def copy(self):
        """
        Public method to copy all strings from the internal buffer to the clipboard.
        """
        qApp.clipboard().setText(self.__listLines.join("\n"), QClipboard.Clipboard)
        
class LogViewViewport(QWidget):
    """
    Internal class representing the viewport of the scrollview.
    """
    def __init__(self, parent=None, name=None):
        """
        Constructor
        
        @param parent parent widget (QWidget)
        @param name name of this widget (string or QString)
        """
        QWidget.__init__(self, parent, name)
        self.__child = LogWidget(self)
        self.__scrollOffsetX = 0
        self.__scrollOffsetY = 0
        self.setPaletteBackgroundColor(self.__child.preferredBackgroundColor())

    def getChild(self):
        return self.__child

    def getWidgetSize(self):
        """
        Public method to get the size of the child widget.
        
        @return the size of the child widget (QSize)
        """
        s = QSize()
        if self.__child is not None:
            s = self.__child.size()
        return s
        
    def setScrollOffsetP(self, point):
        """
        Sets the scrolling offset for the child widget.
        
        @param point scrolling offset (QPoint)
        """
        self.setScrollOffset(point.x(), point.y())
        
    def setScrollOffset(self, x, y):
        """
        Sets the scrolling offset for the child widget.
        
        @param x x-offset (int)
        @param y y-offset (int)
        """
        self.__scrollOffsetX = x
        self.__scrollOffsetY = y
        if self.__child is not None:
            self.__child.move(QPoint(-1*x, -1*y))
        
    def getWidget(self):
        """
        Public method returning the child widget.
        
        @return child widget
        """
        return self.__child
        
class LogView(QFrame):
    """
    Class providing a stack of tabbed scrollable widgets.
    
    Like QTabWidget the LogView provides a stack of widgets. Different from
    the normal QTabWidget the tabs are aligned at the bottom with QTabBar::RoundedBelow
    style. The contents of the different widgets can be scrolled by scrollbars that are
    on the right and bottom side. The widgets themselves must be derived from
    LogViewWidget.

    LogView is not derived from QTabWidget as this would prevent the implentation
    of the horizontal scrollbar at the side of the tabbar. The interface is similar to
    QTabWidget to make it easy to use.
    """
    def __init__(self, parent=None, name=None, flags=0):
        """
        Constructor
        
        @param parent parent widget (QWidget)
        @param name name of this widget (string or QString)
        @param flags window flags
        """
        QFrame.__init__(self, parent, name, flags)
        self.__splitter = None
        self.__scrollbarH = None
        self.__scrollbarV = None
        self.__tabbar = None
        self.__widgetStack = None
        self.__viewports = []
        
        self.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken)
        
        # create a grid layout for the components
        layout = QGridLayout(self, 2, 2)
        layout.setRowStretch(0, 1)
        layout.setRowStretch(1, 0)
        layout.setColStretch(0, 1)
        layout.setColStretch(1, 0)
        layout.setMargin(1)
        
        # create a splitter for the tabbar and the horizontal scrollbar
        self.__splitter = QSplitter(self, "LogViewSplitter")
        self.__splitter.setOpaqueResize()
        layout.addWidget(self.__splitter, 1, 0)
        
        # add the tabbar
        self.__tabbar = QTabBar(self.__splitter, "LogViewTabbar")
        self.__tabbar.setShape(QTabBar.RoundedBelow)
        
        # add the horizontal scrollbar
        self.__scrollbarH = QScrollBar(Qt.Horizontal, self.__splitter,
            "LogViewScrollbarH")
        
        # add the vertical scrollbar
        self.__scrollbarV = QScrollBar(Qt.Vertical, self, "logViewScrollbarV")
        layout.addWidget(self.__scrollbarV, 0, 1)
        
        # add the widgetstack
        self.__widgetStack = QWidgetStack(self, "LogViewWidgetStack")
        layout.addWidget(self.__widgetStack, 0, 0)
        
        # initially set the splitter for the tabbar and the horizontal
        # scrollbar to 50:50
        width = self.size().width() / 2
        self.__splitter.setSizes([width, width])
        
        # set up the connections
        self.connect(self.__tabbar, SIGNAL('selected(int)'), 
            self.handleTabbarSelected)
        self.connect(self.__scrollbarH, SIGNAL('valueChanged(int)'),
            self.handleScrollbarValueChanged)
        self.connect(self.__scrollbarV, SIGNAL('valueChanged(int)'),
            self.handleScrollbarValueChanged)
        
        # create the context menu
        self.menu = QPopupMenu(self)
        self.menu.insertItem(self.trUtf8('Clear'), self.handleClearWidget)
        self.menu.insertItem(self.trUtf8('Copy'), self.handleCopyWidget)
        
        self.setIcon(UI.PixmapCache.getPixmap("eric.png"))
        
    def wheelEvent(self, evt):
        """
        Private method to override the wheel event.
        
        @param evt wheel event (QWheelEvent)
        """
        self.setFocus()
        
        if evt.orientation() == Qt.Horizontal or evt.state() & Qt.ShiftButton:
            QApplication.sendEvent(self.__scrollbarH, evt)
        elif evt.orientation() == Qt.Vertical:
            QApplication.sendEvent(self.__scrollbarV, evt)
            
    def setupScrollBars(self):
        """
        Calculates the sizes of the scrollbars for the top widget.
        """
        viewport = self.getActualViewport()
        
        if viewport is None:
            return
            
        # calculate scrollbar sizes
        sizeViewport = viewport.size()
        sizeWidget = viewport.getWidgetSize()
        
        # horizontal
        sizeActual = sizeWidget.width()
        sizeVisible = sizeViewport.width()
        delta = sizeActual - sizeVisible
        if delta < 0:
            delta = 0
        self.__scrollbarH.setMinValue(0)
        self.__scrollbarH.setMaxValue(delta)
        self.__scrollbarH.setLineStep(max(sizeVisible/10, 1))
        self.__scrollbarH.setPageStep(sizeVisible)
        if delta == 0:
            self.__scrollbarH.setValue(0)
            
        # vertical
        atBottom = self.__scrollbarV.value() == self.__scrollbarV.maxValue()
        sizeActual = sizeWidget.height()
        sizeVisible = sizeViewport.height()
        delta = sizeActual - sizeVisible
        if delta < 0:
            delta = 0
        self.__scrollbarV.setMinValue(0)
        self.__scrollbarV.setMaxValue(delta)
        self.__scrollbarV.setLineStep(max(sizeVisible/10, 1))
        self.__scrollbarV.setPageStep(sizeVisible)
        if delta == 0:
            self.__scrollbarV.setValue(0)
        if atBottom:
            self.__scrollbarV.setValue(self.__scrollbarV.maxValue())
        
    def resizeEvent(self, evt):
        """
        Reimplemented to set the scrollbar sizes.
        
        @param evt resize event (QResizeEvent)
        """
        QFrame.resizeEvent(self, evt)
        self.setupScrollBars()
        
    def contextMenuEvent(self, evt):
        """
        Reimplemented for custom context menu.
        
        @param evt context menu event (QContextMenuEvent)
        """
        evt.accept()
        self.menu.popup(evt.globalPos())
        
    def getActualViewport(self):
        """
        Gets the actual viewport or None if there is none.
        
        @return reference to the actual viewport
        """
        widget = self.__widgetStack.visibleWidget()
        if widget is None:
            return None
            
        return widget
        
    def handleTabbarSelected(self, id):
        """
        Called when the selection in the tabbar changes. 
        
        The corresponding widget is activated.
        
        @param id the id of the selected tab (int)
        """
        self.__widgetStack.raiseWidget(id)
        self.setupScrollBars()
        
    def handleScrollbarValueChanged(self, value):
        """
        Called when the value of a scrollbar changes.
        
        @param value value of the scrollbar (int) (ignored)
        """
        viewport = self.getActualViewport()
        if viewport is None:
            return
            
        h = self.__scrollbarH.value()
        v = self.__scrollbarV.value()
        
        viewport.setScrollOffset(h, v)
        
    def addTab(self, labelOrTabOrIconset, label = None):
        """
        Adds a tab.
        
        @param labelOrTabOrIconset label, tab or icon to be shown
                (QString, QTab or QIconSet)
        @param label label to be displayed next to an icon (QString)
        @return a tuple of the tab id and the child managed by this tab
            (integer, LogWidget)
        """
        if isinstance(labelOrTabOrIconset, QTab):
            tab = labelOrTabOrIconset
        elif isinstance(labelOrTabOrIconset, QIconSet):
            if label is None:
                raise TypeError, "addTab() takes 4 arguments (3 given)"
            tab = QTab(labelOrTabOrIconset, label)
        else:
            tab = QTab(labelOrTabOrIconset)
        
        viewport = LogViewViewport(self, "viewport")
        id = self.__tabbar.addTab(tab)
        self.__widgetStack.addWidget(viewport, id)
        child = viewport.getChild()
        
        self.connect(child, PYSIGNAL('sizeChanged'), 
            self.handleWidgetSizeChanged)
            
        self.__viewports.append(viewport)
        
        return self.__tabbar.indexOf(id), child
        
    def setCurrentPage(self, pos):
        """
        Set the page at position pos as the current page.
        """
        tab = self.__tabbar.tabAt(pos)
        if tab is None:
            return
            
        if self.__tabbar.currentTab() != tab.identifier():
            self.__tabbar.setCurrentTab(pos)
        else:
            self.handleTabbarSelected(tab.identifier())
        
    def handleWidgetSizeChanged(self, widget):
        """
        Recalculates the scrollbars if necessary.
        """
        viewport = self.getActualViewport()
        if viewport is None:
            return
            
        if widget == viewport.getWidget():
            self.setupScrollBars()

    def currentPage(self):
        """
        Public method to retrieve the current page.
        
        @return the current page (QWidget)
        """
        return self.__widgetStack.visibleWidget()

    def nextTab(self):
        """
        Public slot used to show the next tab.
        """
        ind = self.__tabbar.indexOf(self.__tabbar.currentTab()) + 1
        if ind == self.__tabbar.count():
            ind = 0
            
        self.setCurrentPage(ind)
        self.currentPage().setFocus()

    def prevTab(self):
        """
        Public slot used to show the previous tab.
        """
        ind = self.__tabbar.indexOf(self.__tabbar.currentTab()) - 1
        if ind == -1:
            ind = self.__tabbar.count() - 1
            
        self.setCurrentPage(ind)
        self.currentPage().setFocus()

    ################################################################
    ## Menu handling slots below
    ################################################################
    
    def handleClearWidget(self):
        """
        Private slot to handle the clear popup menu action.
        """
        viewport = self.getActualViewport()
        if viewport is None:
            return
            
        viewport.getWidget().clear()

    def handleCopyWidget(self):
        """
        Private slot to handle the copy popup menu action.
        """
        viewport = self.getActualViewport()
        if viewport is None:
            return
            
        viewport.getWidget().copy()
