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

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


"""
Module implementing the helpbrowser using QWebView.
"""

import os

from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4.QtWebKit import QWebView, QWebPage, QWebSettings
from PyQt4.QtNetwork import QNetworkProxy, QNetworkAccessManager, QNetworkReply, \
    QNetworkRequest

from KdeQt import KQMessageBox

import Preferences
import Utilities

from DownloadDialog import DownloadDialog

class HelpBrowser(QWebView):
    """
    Class implementing the helpbrowser widget.
    
    This is a subclass of the Qt QWebView to implement an
    interface compatible with the QTextBrowser based variant.
    
    @signal sourceChanged(const QUrl &) emitted after the current URL has changed
    @signal forwardAvailable(bool) emitted after the current URL has changed
    @signal backwardAvailable(bool) emitted after the current URL has changed
    @signal highlighted(const QString&) emitted, when the mouse hovers over a link
    """
    def __init__(self, parent = None, name = QString("")):
        """
        Constructor
        
        @param parent parent widget of this window (QWidget)
        @param name name of this window (string or QString)
        """
        QWebView.__init__(self, parent)
        self.setObjectName(name)
        self.setWhatsThis(self.trUtf8(
                """<b>Help Window</b>"""
                """<p>This window displays the selected help information.</p>"""
        ))
        
        self.mw = parent
        self.lastURL = QString()
        self.ctrlPressed = False
        self.__homeUrl = None
        self.__downloadWindows = []
        
        iconDatabasePath = os.path.join(Utilities.getConfigDir(), "favicons")
        if not os.path.exists(iconDatabasePath):
            os.makedirs(iconDatabasePath)
        QWebSettings.setIconDatabasePath(iconDatabasePath)
        
        self.setWebSettings()
        
        self.page().setNetworkAccessManager(self.mw.networkAccessManager())
        self.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks)
        self.connect(self, SIGNAL('linkClicked(const QUrl &)'), self.setSource)
        self.connect(self, SIGNAL('iconChanged()'), self.__iconChanged)
        
        self.connect(self, SIGNAL('urlChanged(const QUrl &)'), self.__urlChanged)
        self.connect(self, SIGNAL('statusBarMessage(const QString &)'), 
            self.__statusBarMessage)
        self.connect(self.page(), 
            SIGNAL('linkHovered(const QString &, const QString &, const QString &)'), 
            self.__linkHovered)
        
        self.connect(self, SIGNAL('loadStarted()'), self.__loadStarted)
        self.connect(self, SIGNAL('loadProgress(int)'), self.__loadProgress)
        self.connect(self, SIGNAL('loadFinished(bool)'), self.__loadFinished)
        
        self.page().setForwardUnsupportedContent(True)
        self.connect(self.page(), SIGNAL('unsupportedContent(QNetworkReply *)'), 
            self.__unsupportedContent)
        
        self.connect(self.page(), SIGNAL('downloadRequested(const QNetworkRequest &)'), 
            self.__downloadRequested)
    
    def setSource(self, name):
        """
        Public method used to set the source to be displayed.
        
        This method is overridden from QTextBrowser.
        
        @param name filename to be shown (QUrl)
        """
        if name is None or not name.isValid():
            return
        
        if self.ctrlPressed:
            # open in a new window
            self.mw.newTab(name)
            self.ctrlPressed = False
            return
        
        if name.scheme().isEmpty() or \
           name.scheme().length() == 1 or \
           name.scheme() == "file":
            # name is a local file
            if not name.scheme().isEmpty() and \
               name.scheme().length() == 1:
                    # it is a local path on win32 os
                    name = QUrl.fromLocalFile(name.toString())
            
            if not QFileInfo(name.toLocalFile()).exists():
                QMessageBox.critical(None,
                    self.trUtf8("Helpviewer"),
                    self.trUtf8("""<p>The file <b>%1</b> does not exist.</p>""")\
                        .arg(name.toLocalFile()),
                    QMessageBox.StandardButtons(\
                        QMessageBox.Ok))
                return

            if name.toLocalFile().endsWith(".pdf") or \
               name.toLocalFile().endsWith(".PDF") or \
               name.toLocalFile().endsWith(".chm") or \
               name.toLocalFile().endsWith(".CHM"):
                started = QDesktopServices.openUrl(name)
                if not started:
                    KQMessageBox.critical(self,
                        self.trUtf8("Helpviewer"),
                        self.trUtf8("""<p>Could not start a viewer"""
                        """ for file <b>%1</b>.</p>""").arg(name.path()))
                return
        else:
            if name.toString().endsWith(".pdf") or \
               name.toString().endsWith(".PDF") or \
               name.toString().endsWith(".chm") or \
               name.toString().endsWith(".CHM"):
                started = QDesktopServices.openUrl(name)
                if not started:
                    KQMessageBox.critical(self,
                        self.trUtf8("Helpviewer"),
                        self.trUtf8("""<p>Could not start a viewer"""
                        """ for file <b>%1</b>.</p>""").arg(name.path()))
                return
        
        self.setUrl(name)
        if self.__homeUrl is None:
            self.__homeUrl = name
    
    def source(self):
        """
        Public method to return the URL of the loaded page.
        
        @return URL loaded in the help browser (QUrl)
        """
        return self.url()
    
    def documentTitle(self):
        """
        Public method to return the title of the loaded page.
        
        @return title (QString)
        """
        return self.title()
    
    def backward(self):
        """
        Public slot to move backwards in history.
        """
        self.triggerPageAction(QWebPage.Back)
        self.__urlChanged(self.history().currentItem().url())
    
    def forward(self):
        """
        Public slot to move forward in history.
        """
        self.triggerPageAction(QWebPage.Forward)
        self.__urlChanged(self.history().currentItem().url())
    
    def home(self):
        """
        Public slot to move to the first page loaded.
        """
        self.setSource(self.__homeUrl)
        self.__urlChanged(self.history().currentItem().url())
    
    def reload(self):
        """
        Public slot to reload the current page.
        """
        self.triggerPageAction(QWebPage.Reload)
    
    def copy(self):
        """
        Public slot to copy the selected text.
        """
        self.triggerPageAction(QWebPage.Copy)
    
    def isForwardAvailable(self):
        """
        Public method to determine, if a forward move in history is possible.
        
        @return flag indicating move forward is possible (boolean)
        """
        return self.history().canGoForward()
    
    def isBackwardAvailable(self):
        """
        Public method to determine, if a backwards move in history is possible.
        
        @return flag indicating move backwards is possible (boolean)
        """
        return self.history().canGoBack()
    
    def zoomIn(self, range = 1):
        """
        Public slot to zoom into the page.
        
        @param range zoom factor (integer)
        """
        self.setTextSizeMultiplier(self.textSizeMultiplier() + range * 0.1)
    
    def zoomOut(self, range = 1):
        """
        Public slot to zoom out of the page.
        
        @param range zoom factor (integer)
        """
        self.setTextSizeMultiplier(self.textSizeMultiplier() - range * 0.1)
    
    def hasSelection(self):
        """
        Public method to determine, if there is some text selected.
        
        @return flag indicating text has been selected (boolean)
        """
        return not self.selectedText().isEmpty()
    
    def findNextPrev(self, txt, case, word, backwards):
        """
        Public slot to find the next occurrence of a text.
        
        @param txt text to search for (QString)
        @param case flag indicating a case sensitive search (boolean)
        @param word flag indicating a word based search (boolean) (ignored)
        @param backwards flag indicating a backwards search (boolean)
        """
        findFlags = QWebPage.FindFlags()
        if case:
            findFlags |= QWebPage.FindCaseSensitively
        if backwards:
            findFlags |= QWebPage.FindBackward
        
        return self.findText(txt, findFlags)
    
    def supportsSearchWholeWord(self):
        """
        Public method to determine, if a word based search is supported.
        
        @return flag indicating support for word based search (boolean)
        """
        return False
    
    def contextMenuEvent(self, evt):
        """
        Protected method called to create a context menu.
        
        This method is overridden from QWebView.
        
        @param evt reference to the context menu event object (QContextMenuEvent)
        """
        pos = evt.pos()
        menu = QMenu(self)
        
        self.lastURL = self.page().mainFrame().hitTestContent(evt.pos()).linkUrl()
        if not self.lastURL.isEmpty():
            self.lastURL = self.source().resolved(QUrl(self.lastURL)).toString()
            if self.lastURL.at(0) == '#':
                src = self.source().toString()
                hsh = src.indexOf('#')
                self.lastURL = (hsh >= 0 and src.left(hsh) or src) + self.lastURL
            
            menu.addAction(self.trUtf8("Open Link in New Tab\tCtrl+LMB"),
                self.__openLinkInNewTab)
        
        menu.addAction(self.mw.newTabAct)
        menu.addAction(self.mw.newAct)
        menu.addSeparator()
        menu.addAction(self.mw.backAct)
        menu.addAction(self.mw.forwardAct)
        menu.addAction(self.mw.homeAct)
        menu.addSeparator()
        menu.addAction(self.mw.zoomInAct)
        menu.addAction(self.mw.zoomOutAct)
        menu.addSeparator()
        menu.addAction(self.mw.copyAct)
        menu.addAction(self.mw.findAct)
        menu.addSeparator()
        menu.addAction(self.trUtf8("Web Inspector..."), self.__webInspector)
        
        menu.exec_(evt.globalPos())
    
    def __openLinkInNewTab(self):
        """
        Private method called by the context menu to open a link in a new window.
        """
        if self.lastURL.isEmpty():
            return
            
        oldCtrlPressed = self.ctrlPressed
        self.ctrlPressed = True
        self.setSource(QUrl(self.lastURL))
        self.ctrlPressed = oldCtrlPressed
    
    def __webInspector(self):
        """
        Private slot to show the web inspector window.
        """
        self.triggerPageAction(QWebPage.InspectElement)
    
    def keyPressEvent(self, evt):
        """
        Protected method called by a key press.
        
        This method is overridden from QTextBrowser.
        
        @param evt the key event (QKeyEvent)
        """
        self.ctrlPressed = (evt.key() == Qt.Key_Control)
        QWebView.keyPressEvent(self, evt)
    
    def keyReleaseEvent(self, evt):
        """
        Protected method called by a key release.
        
        This method is overridden from QTextBrowser.
        
        @param evt the key event (QKeyEvent)
        """
        self.ctrlPressed = False
        QWebView.keyReleaseEvent(self, evt)
    
    def clearHistory(self):
        """
        Public slot to clear the history.
        """
        self.history().clear()
        self.__urlChanged(self.history().currentItem().url())
    
    ############################################################################
    ## Signal converters below
    ############################################################################
    
    def __urlChanged(self, url):
        """
        Private slot to handle the urlChanged signal.
        
        @param url the new url (QUrl)
        """
        self.emit(SIGNAL('sourceChanged(const QUrl &)'), url)
        
        self.emit(SIGNAL('forwardAvailable(bool)'), self.isForwardAvailable())
        self.emit(SIGNAL('backwardAvailable(bool)'), self.isBackwardAvailable())
    
    def __statusBarMessage(self, text):
        """
        Private slot to handle the statusBarMessage signal.
        
        @param text text to be shown in the status bar (QString)
        """
        self.mw.statusBar().showMessage(text)
    
    def __linkHovered(self, link,  title, textContent):
        """
        Private slot to handle the linkHovered signal.
        
        @param link the URL of the link (QString)
        @param title the link title (QString)
        @param textContent text content of the link (QString)
        """
        self.emit(SIGNAL('highlighted(const QString&)'), link)
    
    ############################################################################
    ## Signal handlers below
    ############################################################################
    
    def __loadStarted(self):
        """
        Private method to handle the loadStarted signal.
        """
        self.mw.progressBar().show()
    
    def __loadProgress(self, progress):
        """
        Private method to handle the loadProgress signal.
        
        @param progress progress value (integer)
        """
        self.mw.progressBar().setValue(progress)
    
    def __loadFinished(self, ok):
        """
        Private method to handle the loadFinished signal.
        
        @param ok flag indicating the result (boolean)
        """
        self.mw.progressBar().hide()
        
        self.__iconChanged()
    
    def __unsupportedContent(self, reply):
        """
        Private slot to handle the unsupportedContent signal.
        
        @param reply reference to the reply object (QNetworkReply)
        """
        if reply.error() == QNetworkReply.NoError:
            if reply.url().isEmpty():
                return
            header = reply.header(QNetworkRequest.ContentLengthHeader)
            size, ok = header.toInt()
            if ok and size == 0:
                return
            
            dlg = DownloadDialog(reply, False, self.page())
            self.connect(dlg, SIGNAL("done()"), self.__downloadDone)
            self.__downloadWindows.append(dlg)
            dlg.show()
        else:
            self.setHtml(self.trUtf8("<html><head>"
                "<title>Help Window</title>"
                "</head>"
                "<body>"
                "<p>The requested URL <b>%1</b> could not be loaded.</p>"
                "<p>Reason: %2</p>"
                "</body>"
                "</html>").arg(reply.url().toString()).arg(reply.errorString()))
    
    def __downloadDone(self):
        """
        Private slot to handle the done signal of the download dialogs.
        """
        dlg = self.sender()
        if dlg in self.__downloadWindows:
            self.__downloadWindows.remove(dlg)
    
    def __downloadRequested(self, request):
        """
        Private slot to handle a download request.
        
        @param request reference to the request object (QNetworkRequest)
        """
        if request.url().isEmpty():
            return
        mgr = self.page().networkAccessManager()
        self.__unsupportedContent(mgr.get(request))
    
    def __iconChanged(self):
        """
        Private slot to handle the icon change.
        """
        self.mw.iconChanged(self.icon())
    
    ############################################################################
    ## Miscellaneous methods below
    ############################################################################
    
    def createWindow(self, windowType):
        """
        Protected method called, when a new window should be created.
        
        @param windowType type of the requested window (QWebPage.WebWindowType)
        """
        self.mw.newTab()
        return self.mw.currentBrowser()
    
    def preferencesChanged(self):
        """
        Public method to indicate a change of the settings.
        """
        self.setWebSettings()
        
        self.reload()

    def setWebSettings(self):
        standardFont = Preferences.getHelp("StandardFont")
        fixedFont = Preferences.getHelp("FixedFont")

        settings = self.page().settings()
        settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, True)
        
        settings.setFontFamily(QWebSettings.StandardFont, standardFont.family())
        settings.setFontSize(QWebSettings.DefaultFontSize, standardFont.pointSize())
        settings.setFontFamily(QWebSettings.FixedFont, fixedFont.family())
        settings.setFontSize(QWebSettings.DefaultFixedFontSize, fixedFont.pointSize())
        
        styleSheet = Preferences.getHelp("UserStyleSheet")
        if not styleSheet.isEmpty():
            settings.setUserStyleSheetUrl(QUrl(styleSheet))
        
        settings.setAttribute(QWebSettings.AutoLoadImages, 
            Preferences.getHelp("AutoLoadImages"))
        settings.setAttribute(QWebSettings.JavaEnabled, 
            Preferences.getHelp("JavaEnabled"))
        settings.setAttribute(QWebSettings.JavascriptEnabled, 
            Preferences.getHelp("JavaScriptEnabled"))
        settings.setAttribute(QWebSettings.JavascriptCanOpenWindows, 
            Preferences.getHelp("JavaScriptCanOpenWindows"))
        settings.setAttribute(QWebSettings.JavascriptCanAccessClipboard, 
            Preferences.getHelp("JavaScriptCanAccessClipboard"))
