FreeCAD

Форк
0
/
Help.py 
469 строк · 16.5 Кб
1
# -*- coding: utf-8 -*-
2

3
# ***************************************************************************
4
# *   Copyright (c) 2021 Yorik van Havre <yorik@uncreated.net>              *
5
# *                                                                         *
6
# *   This program is free software; you can redistribute it and/or modify  *
7
# *   it under the terms of the GNU Lesser General Public License (LGPL)    *
8
# *   as published by the Free Software Foundation; either version 2 of     *
9
# *   the License, or (at your option) any later version.                   *
10
# *   for detail see the LICENCE text file.                                 *
11
# *                                                                         *
12
# *   This program is distributed in the hope that it will be useful,       *
13
# *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
14
# *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
15
# *   GNU Library General Public License for more details.                  *
16
# *                                                                         *
17
# *   You should have received a copy of the GNU Library General Public     *
18
# *   License along with this program; if not, write to the Free Software   *
19
# *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  *
20
# *   USA                                                                   *
21
# *                                                                         *
22
# ***************************************************************************
23

24
"""
25
Provide tools to access the FreeCAD documentation.
26

27
The main usage is using the "show" function. It can retrieve an URL,
28
a local file (markdown or html), or find a page automatically from
29
the settings set under Preferences->General->Help.
30

31
It doesn't matter what you give, the system will recognize if the contents are
32
HTML or Markdown and render it appropriately.
33

34
Basic usage:
35

36
    import Help
37
    Help.show("Draft Line")
38
    Help.show("Draft_Line") # works with spaces or underscores
39
    Help.show("https://wiki.freecadweb.org/Draft_Line")
40
    Help.show("https://gitlab.com/freecad/FreeCAD-documentation/-/raw/main/wiki/Draft_Line.md")
41
    Help.show("/home/myUser/.FreeCAD/Documentation/Draft_Line.md")
42
    Help.show("http://myserver.com/myfolder/Draft_Line.html")
43

44
Preferences keys (in "User parameter:BaseApp/Preferences/Mod/Help"):
45

46
    optionBrowser/optionTab/optionDialog (bool): Where to open the help dialog
47
    optionOnline/optionOffline (bool): where to fetch the documentation from
48
    URL (string): online location
49
    Location (string): offline location
50
    Suffix (string): a suffix to add to the URL, ex: /fr
51
    StyleSheet (string): optional CSS stylesheet to style the output
52
"""
53

54
import os
55
import FreeCAD
56

57

58
translate = FreeCAD.Qt.translate
59
QT_TRANSLATE_NOOP = FreeCAD.Qt.QT_TRANSLATE_NOOP
60

61
# texts and icons
62
WIKI_URL = "https://wiki.freecad.org"
63
MD_RAW_URL = "https://raw.githubusercontent.com/FreeCAD/FreeCAD-documentation/main/wiki"
64
MD_RENDERED_URL = "https://github.com/FreeCAD/FreeCAD-documentation/blob/main/wiki"
65
MD_TRANSLATIONS_FOLDER = "translations"
66
ERRORTXT = translate(
67
    "Help",
68
    "Contents for this page could not be retrieved. Please check settings under menu Edit -> Preferences -> General -> Help",
69
)
70
LOCTXT = translate(
71
    "Help",
72
    "Help files location could not be determined. Please check settings under menu Edit -> Preferences -> General -> Help",
73
)
74
LOGTXT = translate(
75
    "Help",
76
    "PySide QtWebEngineWidgets module is not available. Help rendering is done with the Web module",
77
)
78
CONVERTTXT = translate(
79
    "Help",
80
    "There is no markdown renderer installed on your system, so this help page is rendered as is. Please install the markdown or pandoc python modules to improve the rendering of this page.",
81
)
82
PREFS = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Help")
83
ICON = ":/icons/help-browser.svg"
84

85

86
def show(page, view=None, conv=None):
87
    """
88
    show(page,view=None, conv=None):
89
    Opens a help viewer and shows the given help page.
90
    The given help page can be a URL pointing to a markdown or HTML file,
91
    a name page / command name, or a file path pointing to a markdown
92
    or HTML file. If view is given (an instance of openBrowserHTML.HelpPage or
93
    any other object with a 'setHtml()' method), the page will be
94
    rendered there, otherwise a new tab/widget will be created according to
95
    preferences settings. If conv is given (markdown, pandoc, github, builtin or
96
    none), the corresponding markdown conversion method is used. Otherwise, the
97
    module will use the best available.
98
    In non-GUI mode, this function simply outputs the markdown or HTML text.
99
    """
100

101
    page = underscore_page(page)
102
    location = get_location(page)
103
    FreeCAD.Console.PrintLog("Help: opening " + location + "\n")
104
    if not location:
105
        FreeCAD.Console.PrintError(LOCTXT + "\n")
106
        return
107
    md = get_contents(location)
108
    html = convert(md, conv)
109
    baseurl = get_uri(location)
110
    pagename = os.path.basename(page.replace("_", " ").replace(".md", ""))
111
    title = translate("Help", "Help") + ": " + pagename
112
    if FreeCAD.GuiUp:
113
        if PREFS.GetBool("optionBrowser", False):  # desktop web browser
114
            show_browser(location)
115
        elif PREFS.GetBool("optionDialog", False):  # floating dock window
116
            show_dialog(html, baseurl, title, view)
117
        else:  # MDI tab - default
118
            show_tab(html, baseurl, title, view)
119
    else:
120
        # console mode, we just print the output
121
        print(md)
122

123

124
def underscore_page(page):
125
    """change spaces by underscores in the given page name"""
126

127
    if "/" in page:
128
        page = page.split("/")
129
        page[-1] = page[-1].replace(" ", "_")
130
        page = "/".join(page)
131
    else:
132
        page.replace(" ", "_")
133
    return page
134

135

136
def get_uri(location):
137
    """returns a valid URI from a disk or network location"""
138

139
    baseurl = os.path.dirname(location) + "/"
140
    if baseurl.startswith("/"):  # unix path
141
        baseurl = "file://" + baseurl
142
    if baseurl[0].isupper() and (baseurl[1] == ":"):  # windows path
143
        baseurl = baseurl.replace("\\", "/")
144
        baseurl = "file:///" + baseurl
145
    return baseurl
146

147

148
def get_location(page):
149
    """retrieves the location (online or offline) of a given page"""
150

151
    location = ""
152
    if page.startswith("http"):
153
        return page
154
    if page.startswith("file://"):
155
        return page[7:]
156
    # offline location
157
    if os.path.exists(page):
158
        return page
159
    page = page.replace(".md", "")
160
    page = page.replace(" ", "_")
161
    page = page.replace("wiki/", "")
162
    page = page.split("#")[0]
163
    suffix = PREFS.GetString("Suffix", "")
164
    if suffix:
165
        if not suffix.startswith("/"):
166
            suffix = "/" + suffix
167
    if PREFS.GetBool("optionWiki", True):  # default
168
        location = WIKI_URL + "/" + page + suffix
169
    elif PREFS.GetBool("optionMarkdown", False):
170
        if PREFS.GetBool("optionBrowser", False):
171
            location = MD_RENDERED_URL
172
        else:
173
            location = MD_RAW_URL
174
        if suffix:
175
            location += "/" + MD_TRANSLATIONS_FOLDER + suffix
176
        location += "/" + page + ".md"
177
    elif PREFS.GetBool("optionGithub", False):
178
        location = MD_RENDERED_URL
179
        if suffix:
180
            location += "/" + MD_TRANSLATIONS_FOLDER + suffix
181
        location += "/" + page + ".md"
182
    elif PREFS.GetBool("optionCustom", False):
183
        location = PREFS.GetString("Location", "")
184
        if not location:
185
            location = os.path.join(
186
                FreeCAD.getUserAppDataDir(),
187
                "Mod",
188
                "offline-documentation",
189
                "FreeCAD-documentation-main",
190
                "wiki",
191
            )
192
        location = os.path.join(location, page + ".md")
193
    return location
194

195

196
def show_browser(url):
197
    """opens the desktop browser with the given URL"""
198

199
    from PySide import QtCore, QtGui
200

201
    try:
202
        ret = QtGui.QDesktopServices.openUrl(QtCore.QUrl(url))
203
        if not ret:
204
            # some users reported problems with the above
205
            import webbrowser
206

207
            webbrowser.open_new(url)
208
    except:
209
        import webbrowser
210

211
        webbrowser.open_new(url)
212

213

214
def show_dialog(html, baseurl, title, view=None):
215
    """opens a dock dialog with the given html"""
216

217
    from PySide import QtCore
218

219
    if get_qtwebwidgets(html, baseurl, title):
220
        if view:  # reusing existing view
221
            view.setHtml(html, baseUrl=QtCore.QUrl(baseurl))
222
            view.parent().parent().setWindowTitle(title)
223
        else:
224
            openBrowserHTML(html, baseurl, title, ICON, dialog=True)
225

226

227
def show_tab(html, baseurl, title, view=None):
228
    """opens a MDI tab with the given html"""
229

230
    from PySide import QtCore
231

232
    if get_qtwebwidgets(html, baseurl, title):
233
        if view:  # reusing existing view
234
            view.setHtml(html, baseUrl=QtCore.QUrl(baseurl))
235
            view.parent().parent().setWindowTitle(title)
236
        else:
237
            # the line below causes a crash with current Qt5 version
238
            # openBrowserHTML(html,baseurl,title,ICON)
239
            # so ATM we use the WebGui browser instead
240
            import WebGui
241

242
            WebGui.openBrowserHTML(html, baseurl, title, ICON)
243

244

245
def get_qtwebwidgets(html, baseurl, title):
246
    """opens a web module view if qtwebwidgets module is not available, and returns False"""
247

248
    try:
249
        from PySide import QtGui, QtWebEngineWidgets
250
    except:
251
        FreeCAD.Console.PrintLog(LOGTXT + "\n")
252
        import WebGui
253

254
        WebGui.openBrowserHTML(html, baseurl, title, ICON)
255
        return False
256
    else:
257
        return True
258

259

260
def get_contents(location):
261
    """retrieves text contents of a given page"""
262

263
    import urllib
264

265
    if location.startswith("http"):
266
        import urllib.request
267

268
        try:
269
            r = urllib.request.urlopen(location)
270
        except:
271
            return ERRORTXT
272
        contents = r.read().decode("utf8")
273
        return contents
274
    else:
275
        if os.path.exists(location):
276
            with open(location, mode="r", encoding="utf8") as f:
277
                contents = f.read()
278
            return contents
279
    return ERRORTXT
280

281

282
def convert(content, force=None):
283
    """converts the given markdown code to html. Force can be None (automatic)
284
    or markdown, pandoc, github or raw/builtin"""
285

286
    import urllib
287

288
    def convert_markdown(m):
289
        try:
290
            import markdown
291
            from markdown.extensions import codehilite
292

293
            return markdown.markdown(m, extensions=["codehilite"])
294
        except:
295
            return None
296

297
    def convert_pandoc(m):
298
        try:
299
            import pypandoc
300

301
            return pypandoc.convert_text(m, "html", format="md")
302
        except:
303
            return None
304

305
    def convert_github(m):
306
        try:
307
            import json
308
            import urllib.request
309

310
            data = {"text": m, "mode": "markdown"}
311
            bdata = json.dumps(data).encode("utf-8")
312
            return (
313
                urllib.request.urlopen("https://api.github.com/markdown", data=bdata)
314
                .read()
315
                .decode("utf8")
316
            )
317
        except:
318
            return None
319

320
    def convert_raw(m):
321
        # simple and dirty regex-based markdown to html
322

323
        import re
324

325
        f = re.DOTALL | re.MULTILINE
326
        m = re.sub(r"^##### (.*?)\n", r"<h5>\1</h5>\n", m, flags=f)  # ##### titles
327
        m = re.sub(r"^#### (.*?)\n", r"<h4>\1</h4>\n", m, flags=f)  # #### titles
328
        m = re.sub(r"^### (.*?)\n", r"<h3>\1</h3>\n", m, flags=f)  # ### titles
329
        m = re.sub(r"^## (.*?)\n", r"<h2>\1</h2>\n", m, flags=f)  # ## titles
330
        m = re.sub(r"^# (.*?)\n", r"<h1>\1</h1>\n", m, flags=f)  # # titles
331
        m = re.sub(r"!\[(.*?)\]\((.*?)\)", r'<img alt="\1" src="\2">', m, flags=f)  # images
332
        m = re.sub(r"\[(.*?)\]\((.*?)\)", r'<a href="\2">\1</a>', m, flags=f)  # links
333
        m = re.sub(r"\*\*(.*?)\*\*", r"<b>\1</b>", m)  # bold
334
        m = re.sub(r"\*(.*?)\*", r"<i>\1</i>", m)  # italic
335
        m = re.sub(r"\n\n", r"<br/>", m, flags=f)  # double new lines
336
        m += "\n<br/><hr/><small>" + CONVERTTXT + "</small>"
337
        return m
338

339
    if "<html" in content:
340
        # this is html already
341
        return content
342

343
    if force == "markdown":
344
        html = convert_markdown(content)
345
    elif force == "pandoc":
346
        html = convert_pandoc(content)
347
    elif force == "github":
348
        html = convert_github(content)
349
    elif force in ["raw", "builtin"]:
350
        html = convert_raw(content)
351
    elif force == "none":
352
        return content
353
    else:
354
        # auto mode
355
        html = convert_pandoc(content)
356
        if not html:
357
            html = convert_markdown(content)
358
            if not html:
359
                html = convert_raw(content)
360
    if not "<html" in html:
361
        html = (
362
            '<html>\n<head>\n<meta charset="utf-8"/>\n</head>\n<body>\n\n'
363
            + html
364
            + "</body>\n</html>"
365
        )
366
    # insert css
367
    css = None
368
    cssfile = PREFS.GetString("StyleSheet", "")
369
    if not cssfile:
370
        cssfile = os.path.join(os.path.dirname(__file__), "default.css")
371
    if False:  # linked CSS file
372
        # below code doesn't work in FreeCAD apparently because it prohibits cross-URL stuff
373
        cssfile = urllib.parse.urljoin("file:", urllib.request.pathname2url(cssfile))
374
        css = '<link rel="stylesheet" type="text/css" href="' + cssfile + '"/>'
375
    else:
376
        if os.path.exists(cssfile):
377
            with open(cssfile) as cf:
378
                css = cf.read()
379
            if css:
380
                css = "<style>\n" + css + "\n</style>"
381
        else:
382
            print("Debug: Help: Unable to open css file:", cssfile)
383
    if css:
384
        html = html.replace("</head>", css + "\n</head>")
385
    return html
386

387

388
def add_preferences_page():
389
    """adds the Help preferences page to the UI"""
390

391
    import FreeCADGui
392

393
    page = os.path.join(os.path.dirname(__file__), "dlgPreferencesHelp.ui")
394
    FreeCADGui.addPreferencePage(page, QT_TRANSLATE_NOOP("QObject", "General"))
395

396

397
def add_language_path():
398
    """registers the Help translations to FreeCAD"""
399

400
    import FreeCADGui
401
    import Help_rc
402

403
    FreeCADGui.addLanguagePath(":/translations")
404

405

406
def openBrowserHTML(html, baseurl, title, icon, dialog=False):
407
    """creates a browser view and adds it as a FreeCAD MDI tab or dockable dialog"""
408

409
    import FreeCADGui
410
    from PySide import QtCore, QtGui, QtWidgets, QtWebEngineWidgets
411

412
    # turn an int into a qt dock area
413
    def getDockArea(area):
414
        if area == 1:
415
            return QtCore.Qt.LeftDockWidgetArea
416
        elif area == 4:
417
            return QtCore.Qt.TopDockWidgetArea
418
        elif area == 8:
419
            return QtCore.Qt.BottomDockWidgetArea
420
        else:
421
            return QtCore.Qt.RightDockWidgetArea
422

423
    # save dock widget size and location
424
    def onDockLocationChanged(area):
425
        PREFS.SetInt("dockWidgetArea", int(area))
426
        mw = FreeCADGui.getMainWindow()
427
        dock = mw.findChild(QtWidgets.QDockWidget, "HelpWidget")
428
        if dock:
429
            PREFS.SetBool("dockWidgetFloat", dock.isFloating())
430
            PREFS.SetInt("dockWidgetWidth", dock.width())
431
            PREFS.SetInt("dockWidgetHeight", dock.height())
432

433
    # a custom page that handles .md links
434
    class HelpPage(QtWebEngineWidgets.QWebEnginePage):
435
        def acceptNavigationRequest(self, url, _type, isMainFrame):
436
            if _type == QtWebEngineWidgets.QWebEnginePage.NavigationTypeLinkClicked:
437
                show(url.toString(), view=self)
438
            return super().acceptNavigationRequest(url, _type, isMainFrame)
439

440
    mw = FreeCADGui.getMainWindow()
441
    view = QtWebEngineWidgets.QWebEngineView()
442
    page = HelpPage(None, view)
443
    page.setHtml(html, baseUrl=QtCore.QUrl(baseurl))
444
    view.setPage(page)
445

446
    if dialog:
447
        area = PREFS.GetInt("dockWidgetArea", 2)
448
        floating = PREFS.GetBool("dockWidgetFloat", True)
449
        height = PREFS.GetBool("dockWidgetWidth", 200)
450
        width = PREFS.GetBool("dockWidgetHeight", 300)
451
        dock = mw.findChild(QtWidgets.QDockWidget, "HelpWidget")
452
        if not dock:
453
            dock = QtWidgets.QDockWidget()
454
            dock.setObjectName("HelpWidget")
455
            mw.addDockWidget(getDockArea(area), dock)
456
            dock.setFloating(floating)
457
            dock.setGeometry(dock.x(), dock.y(), width, height)
458
            dock.dockLocationChanged.connect(onDockLocationChanged)
459
        dock.setWidget(view)
460
        dock.setWindowTitle(title)
461
        dock.setWindowIcon(QtGui.QIcon(icon))
462
        dock.show()
463
    else:
464
        mdi = mw.findChild(QtWidgets.QMdiArea)
465
        sw = mdi.addSubWindow(view)
466
        sw.setWindowTitle(title)
467
        sw.setWindowIcon(QtGui.QIcon(icon))
468
        sw.show()
469
        mdi.setActiveSubWindow(sw)
470

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.