1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2009 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing a thread class populating and updating the QtHelp
8documentation database.
9"""
10
11import os
12
13from PyQt5.QtCore import (
14    pyqtSignal, QThread, Qt, QMutex, QDateTime, QDir, QLibraryInfo, QFileInfo
15)
16from PyQt5.QtHelp import QHelpEngineCore
17
18from eric6config import getConfig
19
20from Globals import qVersionTuple
21
22
23class HelpDocsInstaller(QThread):
24    """
25    Class implementing the worker thread populating and updating the QtHelp
26    documentation database.
27
28    @signal errorMessage(str) emitted, if an error occurred during
29        the installation of the documentation
30    @signal docsInstalled(bool) emitted after the installation has finished
31    """
32    errorMessage = pyqtSignal(str)
33    docsInstalled = pyqtSignal(bool)
34
35    def __init__(self, collection):
36        """
37        Constructor
38
39        @param collection full pathname of the collection file (string)
40        """
41        super().__init__()
42
43        self.__abort = False
44        self.__collection = collection
45        self.__mutex = QMutex()
46
47    def stop(self):
48        """
49        Public slot to stop the installation procedure.
50        """
51        if not self.isRunning():
52            return
53
54        self.__mutex.lock()
55        self.__abort = True
56        self.__mutex.unlock()
57        self.wait()
58
59    def installDocs(self):
60        """
61        Public method to start the installation procedure.
62        """
63        self.start(QThread.Priority.LowPriority)
64
65    def run(self):
66        """
67        Public method executed by the thread.
68        """
69        engine = QHelpEngineCore(self.__collection)
70        changes = False
71
72        qt5Docs = [
73            "activeqt", "qdoc", "qmake", "qt3d", "qt3drenderer",
74            "qtandroidextras", "qtassistant", "qtbluetooth", "qtcanvas3d",
75            "qtcharts", "qtconcurrent", "qtcore", "qtdatavisualization",
76            "qtdbus", "qtdesigner", "qtdistancefieldgenerator", "qtdoc",
77            "qtenginio", "qtenginiooverview", "qtenginoqml", "qtgamepad",
78            "qtgraphicaleffects", "qtgui", "qthelp", "qtimageformats",
79            "qtlabscalendar", "qtlabsplatform", "qtlabscontrols", "qtlinguist",
80            "qtlocation", "qtlottieanimation", "qtmaxextras", "qtmultimedia",
81            "qtmultimediawidgets", "qtnetwork", "qtnetworkauth", "qtnfc",
82            "qtopengl", "qtplatformheaders", "qtpositioning", "qtprintsupport",
83            "qtpurchasing", "qtqml", "qtqmltest", "qtquick", "qtquickcontrols",
84            "qtquickcontrols1", "qtquickdialogs", "qtquickextras",
85            "qtquicklayouts", "qtremoteobjects", "qtscript", "qtscripttools",
86            "qtscxml", "qtsensors", "qtserialbus", "qtserialport", "qtspeech",
87            "qtsql", "qtsvg", "qttest", "qttestlib", "qtuitools",
88            "qtvirtualkeyboard", "qtwaylandcompositor", "qtwebchannel",
89            "qtwebengine", "qtwebenginewidgets", "qtwebkit",
90            "qtwebkitexamples", "qtwebsockets", "qtwebview", "qtwidgets",
91            "qtwinextras", "qtx11extras", "qtxml", "qtxmlpatterns"]
92        for qtDocs, version in [(qt5Docs, 5)]:
93            for doc in qtDocs:
94                changes |= self.__installQtDoc(doc, version, engine)
95                self.__mutex.lock()
96                if self.__abort:
97                    engine = None
98                    self.__mutex.unlock()
99                    return
100                self.__mutex.unlock()
101
102        changes |= self.__installEric6Doc(engine)
103        engine = None
104        del engine
105        self.docsInstalled.emit(changes)
106
107    def __installQtDoc(self, name, version, engine):
108        """
109        Private method to install/update a Qt help document.
110
111        @param name name of the Qt help document (string)
112        @param version Qt version of the help documens (integer)
113        @param engine reference to the help engine (QHelpEngineCore)
114        @return flag indicating success (boolean)
115        """
116        versionKey = "qt_version_{0}@@{1}".format(version, name)
117        info = engine.customValue(versionKey, "")
118        lst = info.split('|')
119
120        dt = QDateTime()
121        if len(lst) and lst[0]:
122            dt = QDateTime.fromString(lst[0], Qt.DateFormat.ISODate)
123
124        qchFile = ""
125        if len(lst) == 2:
126            qchFile = lst[1]
127
128        if version == 4:
129            docsPath = QDir(
130                QLibraryInfo.location(
131                    QLibraryInfo.LibraryLocation.DocumentationPath) +
132                QDir.separator() + "qch")
133        elif version == 5:
134            docsPath = QLibraryInfo.location(
135                QLibraryInfo.LibraryLocation.DocumentationPath)
136            if (
137                not os.path.isdir(docsPath) or
138                len(QDir(docsPath).entryList(["*.qch"])) == 0
139            ):
140                docsPathList = QDir.fromNativeSeparators(docsPath).split("/")
141                docsPath = os.sep.join(
142                    docsPathList[:-3] +
143                    ["Docs", "Qt-{0}.{1}".format(*qVersionTuple())])
144            docsPath = QDir(docsPath)
145        else:
146            # unsupported Qt version
147            return False
148
149        files = docsPath.entryList(["*.qch"])
150        if not files:
151            engine.setCustomValue(
152                versionKey,
153                QDateTime().toString(Qt.DateFormat.ISODate) + '|')
154            return False
155
156        for f in files:
157            if f.startswith(name + "."):
158                fi = QFileInfo(docsPath.absolutePath() + QDir.separator() + f)
159                namespace = QHelpEngineCore.namespaceName(
160                    fi.absoluteFilePath())
161                if not namespace:
162                    continue
163
164                if (
165                    dt.isValid() and
166                    namespace in engine.registeredDocumentations() and
167                    (fi.lastModified().toString(Qt.DateFormat.ISODate) ==
168                     dt.toString(Qt.DateFormat.ISODate)) and
169                    qchFile == fi.absoluteFilePath()
170                ):
171                    return False
172
173                if namespace in engine.registeredDocumentations():
174                    engine.unregisterDocumentation(namespace)
175
176                if not engine.registerDocumentation(fi.absoluteFilePath()):
177                    self.errorMessage.emit(
178                        self.tr(
179                            """<p>The file <b>{0}</b> could not be"""
180                            """ registered. <br/>Reason: {1}</p>""")
181                        .format(fi.absoluteFilePath, engine.error())
182                    )
183                    return False
184
185                engine.setCustomValue(
186                    versionKey,
187                    fi.lastModified().toString(Qt.DateFormat.ISODate) + '|' +
188                    fi.absoluteFilePath())
189                return True
190
191        return False
192
193    def __installEric6Doc(self, engine):
194        """
195        Private method to install/update the eric help documentation.
196
197        @param engine reference to the help engine (QHelpEngineCore)
198        @return flag indicating success (boolean)
199        """
200        versionKey = "eric6_ide"
201        info = engine.customValue(versionKey, "")
202        lst = info.split('|')
203
204        dt = QDateTime()
205        if len(lst) and lst[0]:
206            dt = QDateTime.fromString(lst[0], Qt.DateFormat.ISODate)
207
208        qchFile = ""
209        if len(lst) == 2:
210            qchFile = lst[1]
211
212        docsPath = QDir(getConfig("ericDocDir") + QDir.separator() + "Help")
213
214        files = docsPath.entryList(["*.qch"])
215        if not files:
216            engine.setCustomValue(
217                versionKey, QDateTime().toString(Qt.DateFormat.ISODate) + '|')
218            return False
219
220        for f in files:
221            if f == "source.qch":
222                fi = QFileInfo(docsPath.absolutePath() + QDir.separator() + f)
223                namespace = QHelpEngineCore.namespaceName(
224                    fi.absoluteFilePath())
225                if not namespace:
226                    continue
227
228                if (
229                    dt.isValid() and
230                    namespace in engine.registeredDocumentations() and
231                    (fi.lastModified().toString(Qt.DateFormat.ISODate) ==
232                     dt.toString(Qt.DateFormat.ISODate)) and
233                    qchFile == fi.absoluteFilePath()
234                ):
235                    return False
236
237                if namespace in engine.registeredDocumentations():
238                    engine.unregisterDocumentation(namespace)
239
240                if not engine.registerDocumentation(fi.absoluteFilePath()):
241                    self.errorMessage.emit(
242                        self.tr(
243                            """<p>The file <b>{0}</b> could not be"""
244                            """ registered. <br/>Reason: {1}</p>""")
245                        .format(fi.absoluteFilePath, engine.error())
246                    )
247                    return False
248
249                engine.setCustomValue(
250                    versionKey,
251                    fi.lastModified().toString(Qt.DateFormat.ISODate) + '|' +
252                    fi.absoluteFilePath())
253                return True
254
255        return False
256