diff --git a/images/images.qrc b/images/images.qrc index 196d6eb94a83..8a9d117d34e0 100644 --- a/images/images.qrc +++ b/images/images.qrc @@ -1013,6 +1013,7 @@ themes/default/propertyicons/notes.svg themes/default/stacked-diagram.svg themes/default/mIconStac.svg + themes/default/mIconQt.svg qgis_tips/symbol_levels.png diff --git a/images/themes/default/mIconQt.svg b/images/themes/default/mIconQt.svg new file mode 100644 index 000000000000..e88e37c56a29 --- /dev/null +++ b/images/themes/default/mIconQt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py index 9f09110d5044..cecc1b4ab3e0 100644 --- a/python/PyQt6/core/auto_additions/qgis.py +++ b/python/PyQt6/core/auto_additions/qgis.py @@ -10883,6 +10883,36 @@ """ # -- Qgis.ColorModel.baseClass = Qgis +# monkey patching scoped based enum +Qgis.DocumentationApi.PyQgis.__doc__ = "PyQgis API documentation" +Qgis.DocumentationApi.PyQgisSearch.__doc__ = "Search in PyQgis API documentation" +Qgis.DocumentationApi.CppQgis.__doc__ = "C++ QGIS API documentation" +Qgis.DocumentationApi.Qt.__doc__ = "Qt API documentation" +Qgis.DocumentationApi.__doc__ = """Documentation API + +.. versionadded:: 3.42 + +* ``PyQgis``: PyQgis API documentation +* ``PyQgisSearch``: Search in PyQgis API documentation +* ``CppQgis``: C++ QGIS API documentation +* ``Qt``: Qt API documentation + +""" +# -- +Qgis.DocumentationApi.baseClass = Qgis +# monkey patching scoped based enum +Qgis.DocumentationBrowser.DeveloperToolsPanel.__doc__ = "Embedded webview in the DevTools panel" +Qgis.DocumentationBrowser.SystemWebBrowser.__doc__ = "Default system web browser" +Qgis.DocumentationBrowser.__doc__ = """Documentation API browser + +.. versionadded:: 3.42 + +* ``DeveloperToolsPanel``: Embedded webview in the DevTools panel +* ``SystemWebBrowser``: Default system web browser + +""" +# -- +Qgis.DocumentationBrowser.baseClass = Qgis try: Qgis.__attribute_docs__ = {'QGIS_DEV_VERSION': 'The development version', 'DEFAULT_SEARCH_RADIUS_MM': 'Identify search radius in mm', 'DEFAULT_MAPTOPIXEL_THRESHOLD': 'Default threshold between map coordinates and device coordinates for map2pixel simplification', 'DEFAULT_HIGHLIGHT_COLOR': 'Default highlight color. The transparency is expected to only be applied to polygon\nfill. Lines and outlines are rendered opaque.', 'DEFAULT_HIGHLIGHT_BUFFER_MM': 'Default highlight buffer in mm.', 'DEFAULT_HIGHLIGHT_MIN_WIDTH_MM': 'Default highlight line/stroke minimum width in mm.', 'SCALE_PRECISION': 'Fudge factor used to compare two scales. The code is often going from scale to scale\ndenominator. So it looses precision and, when a limit is inclusive, can lead to errors.\nTo avoid that, use this factor instead of using <= or >=.\n\n.. deprecated:: 3.40\n\n No longer used by QGIS and will be removed in QGIS 4.0.', 'DEFAULT_Z_COORDINATE': 'Default Z coordinate value.\nThis value have to be assigned to the Z coordinate for the vertex.', 'DEFAULT_M_COORDINATE': 'Default M coordinate value.\nThis value have to be assigned to the M coordinate for the vertex.\n\n.. versionadded:: 3.20', 'UI_SCALE_FACTOR': 'UI scaling factor. This should be applied to all widget sizes obtained from font metrics,\nto account for differences in the default font sizes across different platforms.', 'DEFAULT_SNAP_TOLERANCE': 'Default snapping distance tolerance.', 'DEFAULT_SNAP_UNITS': 'Default snapping distance units.'} Qgis.version = staticmethod(Qgis.version) diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in index de6703eaf11d..72554d931866 100644 --- a/python/PyQt6/core/auto_generated/qgis.sip.in +++ b/python/PyQt6/core/auto_generated/qgis.sip.in @@ -3173,6 +3173,20 @@ The development version Cmyk, }; + enum class DocumentationApi /BaseType=IntEnum/ + { + PyQgis, + PyQgisSearch, + CppQgis, + Qt, + }; + + enum class DocumentationBrowser /BaseType=IntEnum/ + { + DeveloperToolsPanel, + SystemWebBrowser, + }; + static const double DEFAULT_SEARCH_RADIUS_MM; static const float DEFAULT_MAPTOPIXEL_THRESHOLD; diff --git a/python/PyQt6/gui/auto_additions/qgscodeeditor.py b/python/PyQt6/gui/auto_additions/qgscodeeditor.py index 9ca000f68875..668fbcdafcd1 100644 --- a/python/PyQt6/gui/auto_additions/qgscodeeditor.py +++ b/python/PyQt6/gui/auto_additions/qgscodeeditor.py @@ -57,13 +57,14 @@ QgsCodeEditor.Flags.baseClass = QgsCodeEditor Flags = QgsCodeEditor # dirty hack since SIP seems to introduce the flags in module try: - QgsCodeEditor.__attribute_docs__ = {'SEARCH_RESULT_INDICATOR': 'Indicator index for search results', 'sessionHistoryCleared': 'Emitted when the history of commands run in the current session is cleared.\n\n.. versionadded:: 3.30\n', 'persistentHistoryCleared': 'Emitted when the persistent history of commands run in the editor is cleared.\n\n.. versionadded:: 3.30\n'} + QgsCodeEditor.__attribute_docs__ = {'SEARCH_RESULT_INDICATOR': 'Indicator index for search results', 'sessionHistoryCleared': 'Emitted when the history of commands run in the current session is cleared.\n\n.. versionadded:: 3.30\n', 'persistentHistoryCleared': 'Emitted when the persistent history of commands run in the editor is cleared.\n\n.. versionadded:: 3.30\n', 'helpRequested': 'Emitted when documentation was requested for the specified ``word``.\n\n.. versionadded:: 3.42\n'} QgsCodeEditor.languageToString = staticmethod(QgsCodeEditor.languageToString) QgsCodeEditor.defaultColor = staticmethod(QgsCodeEditor.defaultColor) QgsCodeEditor.color = staticmethod(QgsCodeEditor.color) QgsCodeEditor.setColor = staticmethod(QgsCodeEditor.setColor) QgsCodeEditor.getMonospaceFont = staticmethod(QgsCodeEditor.getMonospaceFont) QgsCodeEditor.isFixedPitch = staticmethod(QgsCodeEditor.isFixedPitch) + QgsCodeEditor.__signal_arguments__ = {'helpRequested': ['word: str']} QgsCodeEditor.__group__ = ['codeeditors'] except NameError: pass diff --git a/python/PyQt6/gui/auto_additions/qgsgui.py b/python/PyQt6/gui/auto_additions/qgsgui.py index a6c8829b3eb9..510f82f0a465 100644 --- a/python/PyQt6/gui/auto_additions/qgsgui.py +++ b/python/PyQt6/gui/auto_additions/qgsgui.py @@ -48,5 +48,6 @@ def _force_int(v): return int(v.value) if isinstance(v, Enum) else v QgsGui.higFlags = staticmethod(QgsGui.higFlags) QgsGui.sampleColor = staticmethod(QgsGui.sampleColor) QgsGui.findScreenAt = staticmethod(QgsGui.findScreenAt) + QgsGui.hasWebEngine = staticmethod(QgsGui.hasWebEngine) except NameError: pass diff --git a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in index 30eccb100b18..aa184913a5e0 100644 --- a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in +++ b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in @@ -519,6 +519,14 @@ Emitted when the persistent history of commands run in the editor is cleared. .. versionadded:: 3.30 %End + + void helpRequested( const QString &word ); +%Docstring +Emitted when documentation was requested for the specified ``word``. + +.. versionadded:: 3.42 +%End + protected: static bool isFixedPitch( const QFont &font ); diff --git a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in index 6ff2e3befbbb..e42a32690ce3 100644 --- a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in +++ b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in @@ -94,6 +94,13 @@ Updates the editor capabilities. Searches the selected text in the official PyQGIS online documentation. .. versionadded:: 3.16 +%End + + virtual void showApiDocumentation( const QString &item ); +%Docstring +Displays the given text in the official APIs (PyQGIS, C++ QGIS or Qt) documentation. + +.. versionadded:: 3.42 %End virtual void toggleComment(); diff --git a/python/PyQt6/gui/auto_generated/qgisinterface.sip.in b/python/PyQt6/gui/auto_generated/qgisinterface.sip.in index d78806ba761b..04d4e289c185 100644 --- a/python/PyQt6/gui/auto_generated/qgisinterface.sip.in +++ b/python/PyQt6/gui/auto_generated/qgisinterface.sip.in @@ -1490,6 +1490,18 @@ Unregister a previously registered tool factory from the development/debugging t .. seealso:: :py:func:`registerDevToolWidgetFactory` .. versionadded:: 3.14 +%End + + virtual void showApiDocumentation( Qgis::DocumentationApi api = Qgis::DocumentationApi::PyQgis, Qgis::DocumentationBrowser browser = Qgis::DocumentationBrowser::DeveloperToolsPanel, const QString &object = QString(), const QString &module = QString() ) = 0; +%Docstring +Show a page of the API documentation + +:param api: Which API to display +:param browser: Web browser used to display the API documentation +:param object: object to show in the documentation +:param module: used only if api = :py:class:`Qgis`.DocumentationApi.PyQgis + +.. versionadded:: 3.42 %End virtual void registerApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker ) = 0; diff --git a/python/PyQt6/gui/auto_generated/qgsgui.sip.in b/python/PyQt6/gui/auto_generated/qgsgui.sip.in index d18c77a26b6c..26210306af85 100644 --- a/python/PyQt6/gui/auto_generated/qgsgui.sip.in +++ b/python/PyQt6/gui/auto_generated/qgsgui.sip.in @@ -251,6 +251,13 @@ Returns the screen at the given global ``point`` (pixel). + static bool hasWebEngine(); +%Docstring +Checks whether QWebEngineView is available to display HTML content. + +.. versionadded:: 3.42 +%End + signals: diff --git a/python/console/console_editor.py b/python/console/console_editor.py index edf83b352ec8..b1929e1478ed 100644 --- a/python/console/console_editor.py +++ b/python/console/console_editor.py @@ -123,6 +123,9 @@ def __init__(self, self.modificationChanged.connect(self.editor_tab.modified) self.modificationAttempted.connect(self.fileReadOnly) + def showApiDocumentation(self, text): + self.console_widget.shell.showApiDocumentation(text) + def set_code_editor_widget(self, widget: QgsCodeEditorWidget): self.code_editor_widget = widget self.code_editor_widget.loadedExternalChanges.connect( @@ -154,11 +157,15 @@ def contextMenuEvent(self, e): runSelected.setShortcut('Ctrl+E') # spellok menu.addAction(runSelected) # spellok - pyQGISHelpAction = QAction(QgsApplication.getThemeIcon("console/iconHelpConsole.svg"), - QCoreApplication.translate("PythonConsole", "Search Selection in PyQGIS Documentation"), - menu) - pyQGISHelpAction.triggered.connect(self.searchSelectedTextInPyQGISDocs) - menu.addAction(pyQGISHelpAction) + word = self.selectedText() or self.wordAtPoint(e.pos()) + if word: + context_help_action = QAction( + QgsApplication.getThemeIcon("mActionHelpContents.svg"), + QCoreApplication.translate("PythonConsole", "Context Help"), + menu) + context_help_action.triggered.connect(partial(self.console_widget.shell.showApiDocumentation, word, force_search=True)) + context_help_action.setShortcut('F1') + menu.addAction(context_help_action) start_action = QAction(QgsApplication.getThemeIcon("mActionStart.svg"), QCoreApplication.translate("PythonConsole", "Run Script"), @@ -246,7 +253,6 @@ def contextMenuEvent(self, e): self.console_widget.openSettings) syntaxCheckAction.setEnabled(False) pasteAction.setEnabled(False) - pyQGISHelpAction.setEnabled(False) cutAction.setEnabled(False) runSelected.setEnabled(False) # spellok copyAction.setEnabled(False) @@ -258,7 +264,6 @@ def contextMenuEvent(self, e): runSelected.setEnabled(True) # spellok copyAction.setEnabled(True) cutAction.setEnabled(True) - pyQGISHelpAction.setEnabled(True) if not self.text() == '': selectAllAction.setEnabled(True) syntaxCheckAction.setEnabled(True) diff --git a/python/console/console_output.py b/python/console/console_output.py index 61ea81b599d5..b1544c687e65 100644 --- a/python/console/console_output.py +++ b/python/console/console_output.py @@ -20,6 +20,7 @@ from __future__ import annotations import sys +from functools import partial from typing import TYPE_CHECKING from qgis.PyQt import sip @@ -239,11 +240,15 @@ def contextMenuEvent(self, e): clearAction.triggered.connect(self.clearConsole) menu.addAction(clearAction) - pyQGISHelpAction = QAction(QgsApplication.getThemeIcon("console/iconHelpConsole.svg"), - QCoreApplication.translate("PythonConsole", "Search Selection in PyQGIS Documentation"), - menu) - pyQGISHelpAction.triggered.connect(self.searchSelectedTextInPyQGISDocs) - menu.addAction(pyQGISHelpAction) + word = self.selectedText() or self.wordAtPoint(e.pos()) + if word: + context_help_action = QAction( + QgsApplication.getThemeIcon("mActionHelpContents.svg"), + QCoreApplication.translate("PythonConsole", "Context Help"), + menu) + context_help_action.triggered.connect(partial(self.shell_editor.showApiDocumentation, word, force_search=True)) + context_help_action.setShortcut('F1') + menu.addAction(context_help_action) menu.addSeparator() copyAction = QAction( @@ -271,13 +276,11 @@ def contextMenuEvent(self, e): runAction.setEnabled(False) clearAction.setEnabled(False) copyAction.setEnabled(False) - pyQGISHelpAction.setEnabled(False) selectAllAction.setEnabled(False) showEditorAction.setEnabled(True) if self.hasSelectedText(): runAction.setEnabled(True) copyAction.setEnabled(True) - pyQGISHelpAction.setEnabled(True) if not self.text(3) == '': selectAllAction.setEnabled(True) clearAction.setEnabled(True) @@ -311,17 +314,8 @@ def enteredSelected(self): self.shell_editor.insertFromDropPaste(cmd) self.shell_editor.entered() - def keyPressEvent(self, e): - # empty text indicates possible shortcut key sequence so stay in output - txt = e.text() - if len(txt) and txt >= " ": - self.shell_editor.append(txt) - self.shell_editor.moveCursorToEnd() - self.shell_editor.setFocus() - e.ignore() - else: - # possible shortcut key sequence, accept it - e.accept() - def widgetMessageBar(self, text: str): self.infoBar.pushMessage(text, Qgis.MessageLevel.Info) + + def showApiDocumentation(self, text): + self.shell_editor.showApiDocumentation(text) diff --git a/python/console/console_sci.py b/python/console/console_sci.py index ba0688c8d9df..5b0d9d8d270b 100644 --- a/python/console/console_sci.py +++ b/python/console/console_sci.py @@ -24,6 +24,7 @@ import re import sys import traceback +from functools import partial from typing import ( Optional, TYPE_CHECKING @@ -33,12 +34,13 @@ from qgis.PyQt.Qsci import QsciScintilla from qgis.PyQt.QtCore import Qt, QCoreApplication -from qgis.PyQt.QtGui import QKeySequence, QFontMetrics, QClipboard -from qgis.PyQt.QtWidgets import QShortcut, QApplication +from qgis.PyQt.QtGui import QKeySequence, QFontMetrics, QClipboard, QCursor +from qgis.PyQt.QtWidgets import QShortcut, QApplication, QAction from qgis.core import ( QgsApplication, Qgis, - QgsProcessingUtils + QgsProcessingUtils, + QgsSettingsTree, ) from qgis.gui import ( QgsCodeEditorPython, @@ -101,6 +103,52 @@ def __parse_object(object=None): module = match[1] obj = match[2] return 'qt', module, obj +""", + r""" +def _help(object=None, api=Qgis.DocumentationApi.PyQgis, force_search=False): + ''' + Link to the C++ or PyQGIS API documentation for the given object. + If no object is given, the main PyQGIS API page is opened. + If the object is not part of the QGIS API but is a Qt object the Qt documentation is opened. + ''' + + pythonSettingsTreeNode = QgsSettingsTree.node("gui").childNode("code-editor").childNode("python") + browserName = pythonSettingsTreeNode.childSetting('context-help-browser').valueAsVariant() + try: + browser = Qgis.DocumentationBrowser[browserName] + except KeyError: + browser = Qgis.DocumentationBrowser.DeveloperToolsPanel + + if not object: + return iface.showApiDocumentation(api, browser=browser) + + def search_or_home(object_str): + if not object_str: + return iface.showApiDocumentation(api, browser=browser) + if browser == Qgis.DocumentationBrowser.DeveloperToolsPanel and not QgsGui.hasWebEngine(): + if force_search: + return iface.showApiDocumentation(Qgis.DocumentationApi.PyQgisSearch, object=object, browser=Qgis.DocumentationBrowser.SystemWebBrowser) + else: + return iface.showApiDocumentation(api, browser=browser) + else: + return iface.showApiDocumentation(Qgis.DocumentationApi.PyQgisSearch, object=object, browser=browser) + + if isinstance(object, str): + try: + object = eval(object) + except (SyntaxError, NameError): + return search_or_home(object) + + obj_info = __parse_object(object) + if not obj_info: + return search_or_home(object if isinstance(object, str) else None) + + obj_type, module, class_name = obj_info + if obj_type == "qt": + api = Qgis.DocumentationApi.Qt + + iface.showApiDocumentation(api, browser=browser, object=class_name, module=module) + """, r""" def _api(object=None): @@ -109,18 +157,7 @@ def _api(object=None): If no object is given, the main API page is opened. If the object is not part of the QGIS API but is a Qt object the Qt documentation is opened. ''' - import webbrowser - api = __parse_object(object) - - version = '' if 'master' in Qgis.QGIS_VERSION.lower() else re.findall(r'^\d.[0-9]*', Qgis.QGIS_VERSION)[0] - - if not api: - webbrowser.open(f"https://qgis.org/api/{version}") - elif api[0] == 'qgis': - webbrowser.open(f"https://api.qgis.org/api/{version}/class{api[2]}.html") - elif api[0] == 'qt': - qtversion = '.'.join(qVersion().split(".")[:2]) - webbrowser.open(f"https://doc.qt.io/qt-{qtversion}/{api[2].lower()}.html") + return _help(object, api=Qgis.DocumentationApi.CppQgis) """, r""" def _pyqgis(object=None): @@ -129,18 +166,7 @@ def _pyqgis(object=None): If no object is given, the main PyQGIS API page is opened. If the object is not part of the QGIS API but is a Qt object the Qt documentation is opened. ''' - import webbrowser - api = __parse_object(object) - - version = 'master' if 'master' in Qgis.QGIS_VERSION.lower() else re.findall(r'^\d.[0-9]*', Qgis.QGIS_VERSION)[0] - - if not api: - webbrowser.open(f"https://qgis.org/pyqgis/{version}") - elif api[0] == 'qgis': - webbrowser.open(f"https://qgis.org/pyqgis/{version}/{api[1]}/{api[2]}.html") - elif api[0] == 'qt': - qtversion = '.'.join(qVersion().split(".")[:2]) - webbrowser.open(f"https://doc.qt.io/qt-{qtversion}/{api[2].lower()}.html") + return _help(object, api=Qgis.DocumentationApi.PyQgis) """ ] @@ -224,9 +250,9 @@ def execCommandImpl(self, cmd, show_input=True): if cmd == "?": self.shell.console_widget.shell_output.insertHelp() elif cmd == '_pyqgis': - webbrowser.open("https://qgis.org/pyqgis/{}".format(version)) + self.shell.showApi(Qgis.DocumentationApi.PyQgis) elif cmd == '_api': - webbrowser.open("https://qgis.org/api/{}".format('' if version == 'master' else version)) + self.shell.showApi(Qgis.DocumentationApi.CppQgis) elif cmd == '_cookbook': webbrowser.open( "https://docs.qgis.org/{}/en/docs/pyqgis_developer_cookbook/".format( @@ -458,3 +484,21 @@ def runFile(self, filename, override_file_name: Optional[str] = None): self._interpreter.execCommandImpl("del __file__", False) self._interpreter.execCommandImpl("sys.path.remove({0})".format( QgsProcessingUtils.stringToPythonLiteral(dirname)), False) + + def showApiDocumentation(self, text, force_search=False): + self._interpreter.execCommandImpl(f'_help({repr(text)}, api=Qgis.DocumentationApi.PyQgis, force_search={force_search})', show_input=False) + + def showApi(self, api: Qgis.DocumentationApi): + self._interpreter.execCommandImpl(f'_help(api=Qgis.DocumentationApi.{api.name})', show_input=False) + + def populateContextMenu(self, menu): + + word = self.selectedText() or self.wordAtPoint(self.mapFromGlobal(QCursor.pos())) + if word: + context_help_action = QAction( + QgsApplication.getThemeIcon("mActionHelpContents.svg"), + QCoreApplication.translate("PythonConsole", "Context Help"), + menu) + context_help_action.triggered.connect(partial(self.showApiDocumentation, word, force_search=True)) + context_help_action.setShortcut('F1') + menu.addAction(context_help_action) diff --git a/python/console/console_settings.py b/python/console/console_settings.py index a34e0adc259d..dafa29931eee 100644 --- a/python/console/console_settings.py +++ b/python/console/console_settings.py @@ -25,7 +25,7 @@ from qgis.PyQt.QtWidgets import QWidget, QFileDialog, QMessageBox, QTableWidgetItem, QHBoxLayout from qgis.PyQt.QtGui import QIcon, QDesktopServices -from qgis.core import QgsSettings, QgsApplication, QgsSettingsTree +from qgis.core import QgsSettings, QgsApplication, QgsSettingsTree, Qgis from qgis.gui import QgsOptionsPageWidget, QgsOptionsWidgetFactory from .console_compile_apis import PrepareAPIDialog @@ -76,6 +76,10 @@ def __init__(self, parent): self.parent = parent self.setupUi(self) + # Populate the documentation Browser combobox + self.contextHelpBrowser.addItem(QCoreApplication.translate("PythonConsole", "Embedded Webview (developer tools)"), Qgis.DocumentationBrowser.DeveloperToolsPanel) + self.contextHelpBrowser.addItem(QCoreApplication.translate("PythonConsole", "Default System Web Browser"), Qgis.DocumentationBrowser.SystemWebBrowser) + self.autopep8Level.setClearValue(1) self.maxLineLength.setClearValue(80) @@ -211,14 +215,17 @@ def saveSettings(self): settings.setValue("pythonConsole/formatOnSave", self.formatOnSave.isChecked()) - pythonSettingsTreeNode = QgsSettingsTree.node("gui").childNode("code-editor").childNode("python") + codeEditorTreeNode = QgsSettingsTree.node("gui").childNode("code-editor") + pythonSettingsTreeNode = codeEditorTreeNode.childNode("python") pythonSettingsTreeNode.childSetting("sort-imports").setValue(self.sortImports.isChecked()) pythonSettingsTreeNode.childSetting("formatter").setValue(self.formatter.currentText()) pythonSettingsTreeNode.childSetting("autopep8-level").setValue(self.autopep8Level.value()) pythonSettingsTreeNode.childSetting("black-normalize-quotes").setValue(self.blackNormalizeQuotes.isChecked()) pythonSettingsTreeNode.childSetting("max-line-length").setValue(self.maxLineLength.value()) - pythonSettingsTreeNode.childSetting('external-editor').setValue( - self.externalEditor.text()) + pythonSettingsTreeNode.childSetting('external-editor').setValue(self.externalEditor.text()) + pythonSettingsTreeNode.childSetting('context-help-browser').setVariantValue(self.contextHelpBrowser.currentData().name) + + codeEditorTreeNode.childSetting('context-help-hover').setValue(self.contextHelpHover.isChecked()) def restoreSettings(self): settings = QgsSettings() @@ -244,7 +251,8 @@ def restoreSettings(self): self.autoSurround.setChecked(settings.value("pythonConsole/autoSurround", True, type=bool)) self.autoInsertImport.setChecked(settings.value("pythonConsole/autoInsertImport", False, type=bool)) - pythonSettingsTreeNode = QgsSettingsTree.node("gui").childNode("code-editor").childNode("python") + codeEditorTreeNode = QgsSettingsTree.node("gui").childNode("code-editor") + pythonSettingsTreeNode = codeEditorTreeNode.childNode("python") self.formatOnSave.setChecked(settings.value("pythonConsole/formatOnSave", False, type=bool)) self.sortImports.setChecked(pythonSettingsTreeNode.childSetting("sort-imports").value()) @@ -253,6 +261,15 @@ def restoreSettings(self): self.blackNormalizeQuotes.setChecked(pythonSettingsTreeNode.childSetting("black-normalize-quotes").value()) self.maxLineLength.setValue(pythonSettingsTreeNode.childSetting("max-line-length").value()) + browserName = pythonSettingsTreeNode.childSetting('context-help-browser').valueAsVariant() + try: + browser = Qgis.DocumentationBrowser[browserName] + except KeyError: + browser = Qgis.DocumentationBrowser.DeveloperToolsPanel + + self.contextHelpBrowser.setCurrentIndex(self.contextHelpBrowser.findData(browser)) + self.contextHelpHover.setChecked(codeEditorTreeNode.childSetting('context-help-hover').value()) + if settings.value("pythonConsole/autoCompleteSource") == 'fromDoc': self.autoCompFromDoc.setChecked(True) elif settings.value("pythonConsole/autoCompleteSource") == 'fromAPI': diff --git a/python/console/console_settings.ui b/python/console/console_settings.ui index 70c26cbc6715..a95715b34490 100644 --- a/python/console/console_settings.ui +++ b/python/console/console_settings.ui @@ -6,8 +6,8 @@ 0 0 - 809 - 974 + 753 + 556 @@ -24,6 +24,7 @@ + 50 false @@ -52,9 +53,9 @@ 0 - -115 - 795 - 1089 + 0 + 739 + 1240 @@ -70,8 +71,95 @@ 0 - - + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Run and Debug + + + true + + + true + + + + 2 + + + + + Auto-save script before running + + + + + + + Enable Object Inspector (switching between tabs may be slow) + + + false + + + + + + + + + + Typing + + + false + + + true + + + + 2 + + + + + Automatic parentheses insertion + + + + + + + Automatically surround selection when typing quotes or brackets + + + + + + + Automatic insertion of the 'import' string on 'from xxx' + + + + + + + + Formatting @@ -176,6 +264,35 @@ + + + + External Editor + + + + + + <html><head/><body><p>Command to launch an external Python code editor. If empty, the default system editor will be used.</p><p>Use the token <span style=" font-style:italic;">&lt;file&gt;</span> to insert the filename, <span style=" font-style:italic;">&lt;line&gt;</span> to insert line number, and <span style=" font-style:italic;">&lt;col&gt;</span> to insert the column number. For example:<br/><span style=" font-family:'Noto Sans Mono';">kate -l &lt;line&gt; -c &lt;col&gt; &quot;&lt;file&gt;&quot;</span></p></body></html> + + + true + + + Qt::TextBrowserInteraction + + + + + + + Default + + + + + + @@ -264,7 +381,7 @@ - + APIs @@ -466,58 +583,10 @@ - - - - Run and Debug - - - true - - - true - - - - 2 - - - - - Auto-save script before running - - - - - - - Enable Object Inspector (switching between tabs may be slow) - - - false - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - + + - Typing + Contextual Help (F1) false @@ -525,57 +594,24 @@ true - + 2 - + - Automatic parentheses insertion + Open in - - - - Automatically surround selection when typing quotes or brackets - - - - - - - Automatic insertion of the 'import' string on 'from xxx' - - + + - - - - - - - External Editor - - - - + + - <html><head/><body><p>Command to launch an external Python code editor. If empty, the default system editor will be used.</p><p>Use the token <span style=" font-style:italic;">&lt;file&gt;</span> to insert the filename, <span style=" font-style:italic;">&lt;line&gt;</span> to insert line number, and <span style=" font-style:italic;">&lt;col&gt;</span> to insert the column number. For example:<br/><span style=" font-family:'Noto Sans Mono';">kate -l &lt;line&gt; -c &lt;col&gt; &quot;&lt;file&gt;&quot;</span></p></body></html> - - - true - - - Qt::TextBrowserInteraction - - - - - - - Default + F1 works on hovered words @@ -608,7 +644,6 @@ groupBoxAutoCompletion - scrollArea autoCompThreshold autoCompFromDoc autoCompFromAPI @@ -616,6 +651,7 @@ autoCloseBracket autoSurround autoInsertImport + contextHelpBrowser formatOnSave sortImports maxLineLength @@ -631,6 +667,8 @@ groupBoxPreparedAPI compileAPIs lineEdit + scrollArea + externalEditor diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index 6a2d3f4d36a2..99bbecf3f9e1 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -10794,6 +10794,36 @@ """ # -- Qgis.ColorModel.baseClass = Qgis +# monkey patching scoped based enum +Qgis.DocumentationApi.PyQgis.__doc__ = "PyQgis API documentation" +Qgis.DocumentationApi.PyQgisSearch.__doc__ = "Search in PyQgis API documentation" +Qgis.DocumentationApi.CppQgis.__doc__ = "C++ QGIS API documentation" +Qgis.DocumentationApi.Qt.__doc__ = "Qt API documentation" +Qgis.DocumentationApi.__doc__ = """Documentation API + +.. versionadded:: 3.42 + +* ``PyQgis``: PyQgis API documentation +* ``PyQgisSearch``: Search in PyQgis API documentation +* ``CppQgis``: C++ QGIS API documentation +* ``Qt``: Qt API documentation + +""" +# -- +Qgis.DocumentationApi.baseClass = Qgis +# monkey patching scoped based enum +Qgis.DocumentationBrowser.DeveloperToolsPanel.__doc__ = "Embedded webview in the DevTools panel" +Qgis.DocumentationBrowser.SystemWebBrowser.__doc__ = "Default system web browser" +Qgis.DocumentationBrowser.__doc__ = """Documentation API browser + +.. versionadded:: 3.42 + +* ``DeveloperToolsPanel``: Embedded webview in the DevTools panel +* ``SystemWebBrowser``: Default system web browser + +""" +# -- +Qgis.DocumentationBrowser.baseClass = Qgis from enum import Enum diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index fc9c1c4223fd..b6687de410d2 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -3173,6 +3173,20 @@ The development version Cmyk, }; + enum class DocumentationApi + { + PyQgis, + PyQgisSearch, + CppQgis, + Qt, + }; + + enum class DocumentationBrowser + { + DeveloperToolsPanel, + SystemWebBrowser, + }; + static const double DEFAULT_SEARCH_RADIUS_MM; static const float DEFAULT_MAPTOPIXEL_THRESHOLD; diff --git a/python/gui/auto_additions/qgscodeeditor.py b/python/gui/auto_additions/qgscodeeditor.py index 56a526d62071..7c4121878534 100644 --- a/python/gui/auto_additions/qgscodeeditor.py +++ b/python/gui/auto_additions/qgscodeeditor.py @@ -56,13 +56,14 @@ QgsCodeEditor.Flags.baseClass = QgsCodeEditor Flags = QgsCodeEditor # dirty hack since SIP seems to introduce the flags in module try: - QgsCodeEditor.__attribute_docs__ = {'SEARCH_RESULT_INDICATOR': 'Indicator index for search results', 'sessionHistoryCleared': 'Emitted when the history of commands run in the current session is cleared.\n\n.. versionadded:: 3.30\n', 'persistentHistoryCleared': 'Emitted when the persistent history of commands run in the editor is cleared.\n\n.. versionadded:: 3.30\n'} + QgsCodeEditor.__attribute_docs__ = {'SEARCH_RESULT_INDICATOR': 'Indicator index for search results', 'sessionHistoryCleared': 'Emitted when the history of commands run in the current session is cleared.\n\n.. versionadded:: 3.30\n', 'persistentHistoryCleared': 'Emitted when the persistent history of commands run in the editor is cleared.\n\n.. versionadded:: 3.30\n', 'helpRequested': 'Emitted when documentation was requested for the specified ``word``.\n\n.. versionadded:: 3.42\n'} QgsCodeEditor.languageToString = staticmethod(QgsCodeEditor.languageToString) QgsCodeEditor.defaultColor = staticmethod(QgsCodeEditor.defaultColor) QgsCodeEditor.color = staticmethod(QgsCodeEditor.color) QgsCodeEditor.setColor = staticmethod(QgsCodeEditor.setColor) QgsCodeEditor.getMonospaceFont = staticmethod(QgsCodeEditor.getMonospaceFont) QgsCodeEditor.isFixedPitch = staticmethod(QgsCodeEditor.isFixedPitch) + QgsCodeEditor.__signal_arguments__ = {'helpRequested': ['word: str']} QgsCodeEditor.__group__ = ['codeeditors'] except NameError: pass diff --git a/python/gui/auto_additions/qgsgui.py b/python/gui/auto_additions/qgsgui.py index 62086f05bb2d..23b0a1136f99 100644 --- a/python/gui/auto_additions/qgsgui.py +++ b/python/gui/auto_additions/qgsgui.py @@ -33,5 +33,6 @@ QgsGui.higFlags = staticmethod(QgsGui.higFlags) QgsGui.sampleColor = staticmethod(QgsGui.sampleColor) QgsGui.findScreenAt = staticmethod(QgsGui.findScreenAt) + QgsGui.hasWebEngine = staticmethod(QgsGui.hasWebEngine) except NameError: pass diff --git a/python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in b/python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in index 2fe8d0f9c15c..f7312489bddd 100644 --- a/python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in +++ b/python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in @@ -519,6 +519,14 @@ Emitted when the persistent history of commands run in the editor is cleared. .. versionadded:: 3.30 %End + + void helpRequested( const QString &word ); +%Docstring +Emitted when documentation was requested for the specified ``word``. + +.. versionadded:: 3.42 +%End + protected: static bool isFixedPitch( const QFont &font ); diff --git a/python/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in b/python/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in index 6ff2e3befbbb..e42a32690ce3 100644 --- a/python/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in +++ b/python/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in @@ -94,6 +94,13 @@ Updates the editor capabilities. Searches the selected text in the official PyQGIS online documentation. .. versionadded:: 3.16 +%End + + virtual void showApiDocumentation( const QString &item ); +%Docstring +Displays the given text in the official APIs (PyQGIS, C++ QGIS or Qt) documentation. + +.. versionadded:: 3.42 %End virtual void toggleComment(); diff --git a/python/gui/auto_generated/qgisinterface.sip.in b/python/gui/auto_generated/qgisinterface.sip.in index d78806ba761b..04d4e289c185 100644 --- a/python/gui/auto_generated/qgisinterface.sip.in +++ b/python/gui/auto_generated/qgisinterface.sip.in @@ -1490,6 +1490,18 @@ Unregister a previously registered tool factory from the development/debugging t .. seealso:: :py:func:`registerDevToolWidgetFactory` .. versionadded:: 3.14 +%End + + virtual void showApiDocumentation( Qgis::DocumentationApi api = Qgis::DocumentationApi::PyQgis, Qgis::DocumentationBrowser browser = Qgis::DocumentationBrowser::DeveloperToolsPanel, const QString &object = QString(), const QString &module = QString() ) = 0; +%Docstring +Show a page of the API documentation + +:param api: Which API to display +:param browser: Web browser used to display the API documentation +:param object: object to show in the documentation +:param module: used only if api = :py:class:`Qgis`.DocumentationApi.PyQgis + +.. versionadded:: 3.42 %End virtual void registerApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker ) = 0; diff --git a/python/gui/auto_generated/qgsgui.sip.in b/python/gui/auto_generated/qgsgui.sip.in index c63fc6258cfc..f454cd348c20 100644 --- a/python/gui/auto_generated/qgsgui.sip.in +++ b/python/gui/auto_generated/qgsgui.sip.in @@ -251,6 +251,13 @@ Returns the screen at the given global ``point`` (pixel). + static bool hasWebEngine(); +%Docstring +Checks whether QWebEngineView is available to display HTML content. + +.. versionadded:: 3.42 +%End + signals: diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 0124ddae0253..10105ca8fd6c 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -174,6 +174,7 @@ set(QGIS_APP_SRCS devtools/networklogger/qgsnetworkloggerwidgetfactory.cpp devtools/profiler/qgsprofilerpanelwidget.cpp devtools/profiler/qgsprofilerwidgetfactory.cpp + devtools/documentation/qgsdocumentationpanelwidget.cpp devtools/querylogger/qgsappquerylogger.cpp devtools/querylogger/qgsdatabasequeryloggernode.cpp devtools/querylogger/qgsqueryloggerpanelwidget.cpp diff --git a/src/app/devtools/documentation/qgsdocumentationpanelwidget.cpp b/src/app/devtools/documentation/qgsdocumentationpanelwidget.cpp new file mode 100644 index 000000000000..a638dade0e7d --- /dev/null +++ b/src/app/devtools/documentation/qgsdocumentationpanelwidget.cpp @@ -0,0 +1,56 @@ +/*************************************************************************** + qgsdocumentationpanelwidget.cpp + ------------------------- + begin : October 2024 + copyright : (C) 2024 by Yoann Quenach de Quivillic + email : yoann dot quenach at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsdocumentationpanelwidget.h" +#include "moc_qgsdocumentationpanelwidget.cpp" +#include "qgisapp.h" +#include + +#ifdef HAVE_WEBENGINE +#include +#else +#include "qgswebview.h" +#endif + + +// +// QgsDocumentationPanelWidget +// + +QgsDocumentationPanelWidget::QgsDocumentationPanelWidget( QWidget *parent ) + : QgsDevToolWidget( parent ) +{ + setupUi( this ); +#ifdef HAVE_WEBENGINE + mWebView = new QWebEngineView( this ); +#else + mWebView = new QgsWebView( this ); +#endif + + mWebViewContainer->layout()->addWidget( mWebView ); + + connect( mPyQgisHomeButton, &QToolButton::clicked, this, [] {QgisApp::instance()->showApiDocumentation( Qgis::DocumentationApi::PyQgis, Qgis::DocumentationBrowser::DeveloperToolsPanel );} ); + connect( mQtHomeButton, &QToolButton::clicked, this, [] {QgisApp::instance()->showApiDocumentation( Qgis::DocumentationApi::Qt, Qgis::DocumentationBrowser::DeveloperToolsPanel );} ); + connect( mOpenUrlButton, &QToolButton::clicked, this, [this] {QgisApp::instance()->openURL( mWebView->url().toString(), false );} ); + +} + +void QgsDocumentationPanelWidget::showUrl( const QUrl &url ) +{ + if ( mWebView->url() != url ) + { + mWebView->load( url ); + } +} diff --git a/src/app/devtools/documentation/qgsdocumentationpanelwidget.h b/src/app/devtools/documentation/qgsdocumentationpanelwidget.h new file mode 100644 index 000000000000..75038ad8cb64 --- /dev/null +++ b/src/app/devtools/documentation/qgsdocumentationpanelwidget.h @@ -0,0 +1,61 @@ +/*************************************************************************** + qgsdocumentationpanelwidget.h + ------------------------- + begin : October 2024 + copyright : (C) 2024 by Yoann Quenach de Quivillic + email : yoann dot quenach at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#ifndef QGSDOCUMENTATIONPANELWIDGET_H +#define QGSDOCUMENTATIONPANELWIDGET_H + +#include "qgsdevtoolwidget.h" +#include "qgsconfig.h" +#include "ui_qgsdocumentationpanelbase.h" + +#ifdef HAVE_WEBENGINE +class QWebEngineView; +#else +class QgsWebView; +#endif + +/** + * \ingroup app + * \class QgsDocumentationPanelWidget + * \brief A panel widget showing the API documentation. + * + * \since QGIS 3.42 + */ +class QgsDocumentationPanelWidget : public QgsDevToolWidget, private Ui::QgsDocumentationPanelBase +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsDocumentationPanelWidget. + */ + QgsDocumentationPanelWidget( QWidget *parent ); + + + void showUrl( const QUrl &url ); + + private: + +#ifdef HAVE_WEBENGINE + QWebEngineView *mWebView = nullptr; +#else + QgsWebView *mWebView = nullptr; +#endif + + +}; + + +#endif // QGSDOCUMENTATIONPANELWIDGET_H diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index a4a5d942c682..51ecbabd1137 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -12964,25 +12964,12 @@ void QgisApp::helpContents() void QgisApp::apiDocumentation() { - if ( QFileInfo::exists( QgsApplication::pkgDataPath() + "/doc/api/index.html" ) ) - { - openURL( QStringLiteral( "api/index.html" ) ); - } - else - { - QgsSettings settings; - QString QgisApiUrl = settings.value( QStringLiteral( "qgis/QgisApiUrl" ), - QStringLiteral( "https://qgis.org/api/" ) ).toString(); - openURL( QgisApiUrl, false ); - } + showApiDocumentation( Qgis::DocumentationApi::CppQgis, Qgis::DocumentationBrowser::SystemWebBrowser ); } void QgisApp::pyQgisApiDocumentation() { - QgsSettings settings; - QString PyQgisApiUrl = settings.value( QStringLiteral( "qgis/PyQgisApiUrl" ), - QStringLiteral( "https://qgis.org/pyqgis/" ) ).toString(); - openURL( PyQgisApiUrl, false ); + showApiDocumentation( Qgis::DocumentationApi::PyQgis, Qgis::DocumentationBrowser::SystemWebBrowser ); } void QgisApp::reportaBug() @@ -13126,6 +13113,12 @@ void QgisApp::unregisterDevToolFactory( QgsDevToolWidgetFactory *factory ) mDevToolFactories.removeAll( factory ); } + +void QgisApp::showApiDocumentation( Qgis::DocumentationApi api, Qgis::DocumentationBrowser browser, const QString &object, const QString &module ) +{ + mDevToolsWidget->showApiDocumentation( api, browser, object, module ); +} + void QgisApp::registerApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker ) { mApplicationExitBlockers << blocker; diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h index 7514bb96ccde..34b8e29d21b7 100644 --- a/src/app/qgisapp.h +++ b/src/app/qgisapp.h @@ -798,6 +798,9 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow //! Unregister a previously registered dev tool factory void unregisterDevToolFactory( QgsDevToolWidgetFactory *factory ); + //! Show a page of the API documentation + void showApiDocumentation( Qgis::DocumentationApi api, Qgis::DocumentationBrowser browser, const QString &object = QString(), const QString &module = QString() ); + /** * Register a new application exit blocker, which can be used to prevent the QGIS application * from exiting while a plugin or script has unsaved changes. @@ -1415,6 +1418,9 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow */ void activateDeactivateLayerRelatedActions( QgsMapLayer *layer ); + //! Open a url in the users configured browser + void openURL( QString url, bool useQgisDocDirectory = true ); + protected: void showEvent( QShowEvent *event ) override; @@ -1778,8 +1784,6 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow void supportProviders(); //! Open the QGIS homepage in users browser void helpQgisHomePage(); - //! Open a url in the users configured browser - void openURL( QString url, bool useQgisDocDirectory = true ); //! Check qgis version against the qgis version server void checkQgisVersion(); //!Invoke the custom projection dialog @@ -2737,6 +2741,7 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow QgsScopedDevToolWidgetFactory mStartupProfilerWidgetFactory; QgsAppQueryLogger *mQueryLogger = nullptr; QgsScopedDevToolWidgetFactory mQueryLoggerWidgetFactory; + QgsScopedDevToolWidgetFactory mDocumentationWidgetFactory; std::vector< QgsScopedOptionsWidgetFactory > mOptionWidgetFactories; diff --git a/src/app/qgisappinterface.cpp b/src/app/qgisappinterface.cpp index 62fb66c1fc39..ca870fc2040e 100644 --- a/src/app/qgisappinterface.cpp +++ b/src/app/qgisappinterface.cpp @@ -641,6 +641,12 @@ void QgisAppInterface::unregisterDevToolWidgetFactory( QgsDevToolWidgetFactory * qgis->unregisterDevToolFactory( factory ); } +void QgisAppInterface::showApiDocumentation( Qgis::DocumentationApi api, Qgis::DocumentationBrowser browser, const QString &object, const QString &module ) +{ + qgis->showApiDocumentation( api, browser, object, module ); +} + + void QgisAppInterface::registerApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker ) { qgis->registerApplicationExitBlocker( blocker ); diff --git a/src/app/qgisappinterface.h b/src/app/qgisappinterface.h index d1494ed9fbce..b67407b5d6d9 100644 --- a/src/app/qgisappinterface.h +++ b/src/app/qgisappinterface.h @@ -157,6 +157,7 @@ class APP_EXPORT QgisAppInterface : public QgisInterface void unregisterProjectPropertiesWidgetFactory( QgsOptionsWidgetFactory *factory ) override; void registerDevToolWidgetFactory( QgsDevToolWidgetFactory *factory ) override; void unregisterDevToolWidgetFactory( QgsDevToolWidgetFactory *factory ) override; + void showApiDocumentation( Qgis::DocumentationApi api, Qgis::DocumentationBrowser browser, const QString &object, const QString &module ) override; void registerApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker ) override; void unregisterApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker ) override; void registerMapToolHandler( QgsAbstractMapToolHandler *handler ) override; diff --git a/src/app/qgsdevtoolspanelwidget.cpp b/src/app/qgsdevtoolspanelwidget.cpp index 427e82299a10..202b33777b71 100644 --- a/src/app/qgsdevtoolspanelwidget.cpp +++ b/src/app/qgsdevtoolspanelwidget.cpp @@ -19,6 +19,9 @@ #include "qgsdevtoolwidget.h" #include "qgspanelwidgetstack.h" #include "qgssettingsentryimpl.h" +#include "qgsapplication.h" +#include "qgsdockwidget.h" +#include "devtools/documentation/qgsdocumentationpanelwidget.h" const QgsSettingsEntryString *QgsDevToolsPanelWidget::settingLastActiveTab = new QgsSettingsEntryString( QStringLiteral( "last-active-tab" ), QgsDevToolsPanelWidget::sTreeDevTools, QString(), QStringLiteral( "Last visible tab in developer tools panel" ) ); @@ -31,6 +34,11 @@ QgsDevToolsPanelWidget::QgsDevToolsPanelWidget( const QListsetIconSize( QgisApp::instance()->iconSize( false ) ); mOptionsListWidget->setMaximumWidth( static_cast< int >( mOptionsListWidget->iconSize().width() * 1.18 ) ); + + // Add embedded documentation + mDocumentationPanel = new QgsDocumentationPanelWidget( this ); + addToolWidget( mDocumentationPanel ) ; + for ( QgsDevToolWidgetFactory *factory : factories ) addToolFactory( factory ); @@ -44,6 +52,21 @@ QgsDevToolsPanelWidget::QgsDevToolsPanelWidget( const QListaddWidget( widget ); + + QListWidgetItem *item = new QListWidgetItem( widget->windowIcon(), QString() ); + item->setToolTip( widget->windowTitle() ); + item->setData( Qt::UserRole, widget->objectName() ); + mOptionsListWidget->addItem( item ); + if ( mOptionsListWidget->count() == 1 ) + { + setCurrentTool( 0 ); + } +} + + void QgsDevToolsPanelWidget::addToolFactory( QgsDevToolWidgetFactory *factory ) { if ( QgsDevToolWidget *toolWidget = factory->createWidget( this ) ) @@ -104,3 +127,107 @@ void QgsDevToolsPanelWidget::setCurrentTool( int row ) whileBlocking( mOptionsListWidget )->setCurrentRow( row ); mStackedWidget->setCurrentIndex( row ); } + +void QgsDevToolsPanelWidget::showApiDocumentation( + Qgis::DocumentationApi api, Qgis::DocumentationBrowser browser, const QString &object, const QString &module +) +{ + bool useQgisDocDirectory = false; + QString baseUrl; + QString version; + + if ( api == Qgis::DocumentationApi::Qt ) + { + version = QString( qVersion() ).split( '.' ).mid( 0, 2 ).join( '.' ); + baseUrl = QString( "https://doc.qt.io/qt-%1/" ).arg( version ); + } + else + { + if ( Qgis::version().toLower().contains( QStringLiteral( "master" ) ) ) + { + version = QStringLiteral( "master" ); + } + else + { + version = QString( Qgis::version() ).split( '.' ).mid( 0, 2 ).join( '.' ); + } + + if ( api == Qgis::DocumentationApi::PyQgis || api == Qgis::DocumentationApi::PyQgisSearch ) + { + QgsSettings settings; + baseUrl = settings.value( QStringLiteral( "qgis/PyQgisApiUrl" ), + QString( "https://qgis.org/pyqgis/%1/" ).arg( version ) ).toString(); + } + else + { + if ( QFileInfo::exists( QgsApplication::pkgDataPath() + "/doc/api/index.html" ) ) + { + useQgisDocDirectory = true; + baseUrl = "api/"; + } + else + { + QgsSettings settings; + baseUrl = settings.value( QStringLiteral( "qgis/QgisApiUrl" ), + QString( "https://qgis.org/api/%1/" ).arg( version ) ).toString(); + } + } + } + + + QString url; + if ( object.isEmpty() ) + { + url = baseUrl == "api/" ? baseUrl + "index.html" : baseUrl; + } + else + { + switch ( api ) + { + case Qgis::DocumentationApi::PyQgis: + url = baseUrl + QString( "%1/%2.html" ).arg( module, object ); + break; + case Qgis::DocumentationApi::PyQgisSearch: + url = baseUrl + QString( "search.html?q=%2" ).arg( object ); + break; + case Qgis::DocumentationApi::CppQgis: + url = baseUrl + QString( "class%1.html" ).arg( object ); + break; + case Qgis::DocumentationApi::Qt: + url = baseUrl + QString( "%1.html" ).arg( object.toLower() ); + break; + } + } +#ifndef HAVE_WEBENGINE + // QWebView does not support the search function from the PyQGIS documentation homepage + if ( api == Qgis::DocumentationApi::PyQgisSearch ) + { + browser = Qgis::DocumentationBrowser::SystemWebBrowser; + } +#endif + + switch ( browser ) + { + case Qgis::DocumentationBrowser::SystemWebBrowser: + QgisApp::instance()->openURL( url, useQgisDocDirectory ); + break; + case Qgis::DocumentationBrowser::DeveloperToolsPanel: + if ( useQgisDocDirectory ) + { + url = "file://" + QgsApplication::pkgDataPath() + "/doc/" + url; + } + if ( QgsDockWidget *dock = QgisApp::instance()->findChild< QgsDockWidget * >( "DevTools" ) ) + { + dock->setUserVisible( true ); + } + showUrl( QUrl( url ) ); + break; + } + +} + +void QgsDevToolsPanelWidget::showUrl( const QUrl &url ) +{ + setActiveTab( mDocumentationPanel->objectName() ); + mDocumentationPanel->showUrl( url ); +} diff --git a/src/app/qgsdevtoolspanelwidget.h b/src/app/qgsdevtoolspanelwidget.h index 13e46402817f..4b482fd45433 100644 --- a/src/app/qgsdevtoolspanelwidget.h +++ b/src/app/qgsdevtoolspanelwidget.h @@ -20,7 +20,8 @@ #include "qgssettingstree.h" class QgsDevToolWidgetFactory; - +class QgsDevToolWidget; +class QgsDocumentationPanelWidget; class APP_EXPORT QgsDevToolsPanelWidget : public QWidget, private Ui::QgsDevToolsWidgetBase { Q_OBJECT @@ -32,12 +33,22 @@ class APP_EXPORT QgsDevToolsPanelWidget : public QWidget, private Ui::QgsDevTool QgsDevToolsPanelWidget( const QList &factories, QWidget *parent = nullptr ); ~QgsDevToolsPanelWidget() override; + void addToolWidget( QgsDevToolWidget *widget ); void addToolFactory( QgsDevToolWidgetFactory *factory ); void removeToolFactory( QgsDevToolWidgetFactory *factory ); void setActiveTab( const QString &title ); + void showApiDocumentation( + Qgis::DocumentationApi api = Qgis::DocumentationApi::PyQgis, + Qgis::DocumentationBrowser browser = Qgis::DocumentationBrowser::DeveloperToolsPanel, + const QString &object = QString(), + const QString &module = QString() + ); + + void showUrl( const QUrl &url ); + private slots: void setCurrentTool( int row ); @@ -45,6 +56,7 @@ class APP_EXPORT QgsDevToolsPanelWidget : public QWidget, private Ui::QgsDevTool private: QMap< QgsDevToolWidgetFactory *, int> mFactoryPages; + QgsDocumentationPanelWidget *mDocumentationPanel = nullptr; }; #endif // QGSDEVTOOLSPANELWIDGET_H diff --git a/src/core/qgis.h b/src/core/qgis.h index 70f7e59b7905..63eb997fda78 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -5579,6 +5579,32 @@ class CORE_EXPORT Qgis }; Q_ENUM( ColorModel ) + /** + * Documentation API + * + * \since QGIS 3.42 + */ + enum class DocumentationApi : int + { + PyQgis, //!< PyQgis API documentation + PyQgisSearch, //!< Search in PyQgis API documentation + CppQgis, //!< C++ QGIS API documentation + Qt, //!< Qt API documentation + }; + Q_ENUM( DocumentationApi ) + + /** + * Documentation API browser + * + * \since QGIS 3.42 + */ + enum class DocumentationBrowser : int + { + DeveloperToolsPanel, //!< Embedded webview in the DevTools panel + SystemWebBrowser, //!< Default system web browser + }; + Q_ENUM( DocumentationBrowser ) + /** * Identify search radius in mm */ diff --git a/src/core/qgswebview.h b/src/core/qgswebview.h index ce98e823e891..cdfdc5454e31 100644 --- a/src/core/qgswebview.h +++ b/src/core/qgswebview.h @@ -93,6 +93,11 @@ class CORE_EXPORT QgsWebView : public QTextBrowser setSource( url ); } + QUrl url() const + { + return source(); + } + QWebPage *page() const { return mPage; diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 2c6d41bb70fc..9e9aa88fa51e 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -1824,6 +1824,13 @@ if (BUILD_WITH_QT6) ) endif() +if (WITH_QTWEBENGINE) + find_package(${QT_VERSION_BASE} COMPONENTS WebEngineWidgets REQUIRED) + target_link_libraries(qgis_gui + ${QT_VERSION_BASE}::WebEngineWidgets + ) +endif() + if (WITH_QTGAMEPAD) target_link_libraries(qgis_gui ${QT_VERSION_BASE}::Gamepad diff --git a/src/gui/codeeditors/qgscodeeditor.cpp b/src/gui/codeeditors/qgscodeeditor.cpp index 076524549360..bd83094f8250 100644 --- a/src/gui/codeeditors/qgscodeeditor.cpp +++ b/src/gui/codeeditors/qgscodeeditor.cpp @@ -24,6 +24,7 @@ #include "qgscodeeditorhistorydialog.h" #include "qgsstringutils.h" #include "qgsfontutils.h" +#include "qgssettingsentryimpl.h" #include #include @@ -38,6 +39,12 @@ #include #include "Qsci/qscilexer.h" +///@cond PRIVATE +const QgsSettingsEntryBool *QgsCodeEditor::settingContextHelpHover = new QgsSettingsEntryBool( QStringLiteral( "context-help-hover" ), sTreeCodeEditor, false, QStringLiteral( "Whether the context help should works on hovered words" ) ); +///@endcond PRIVATE + + + QMap< QgsCodeEditorColorScheme::ColorRole, QString > QgsCodeEditor::sColorRoleToSettingsKey { {QgsCodeEditorColorScheme::ColorRole::Default, QStringLiteral( "defaultFontColor" ) }, @@ -193,6 +200,30 @@ void QgsCodeEditor::keyPressEvent( QKeyEvent *event ) return; } + if ( event->key() == Qt::Key_F1 ) + { + + // Check if some text is selected + QString text = selectedText(); + + // Check if mouse is hovering over a word + if ( text.isEmpty() && settingContextHelpHover->value() ) + { + text = wordAtPoint( mapFromGlobal( QCursor::pos() ) ); + } + + // Otherwise, check if there is a word at the current text cursor position + if ( text.isEmpty() ) + { + int line, index; + getCursorPosition( &line, &index ); + text = wordAtLineIndex( line, index ); + } + emit helpRequested( text ) ; + return; + } + + if ( mMode == QgsCodeEditor::Mode::CommandInput ) { switch ( event->key() ) diff --git a/src/gui/codeeditors/qgscodeeditor.h b/src/gui/codeeditors/qgscodeeditor.h index 94ebd4153fe8..21008cbd85d6 100644 --- a/src/gui/codeeditors/qgscodeeditor.h +++ b/src/gui/codeeditors/qgscodeeditor.h @@ -33,6 +33,7 @@ class QgsFilterLineEdit; class QToolButton; class QCheckBox; +class QgsSettingsEntryBool; SIP_IF_MODULE( HAVE_QSCI_SIP ) @@ -107,6 +108,7 @@ class GUI_EXPORT QgsCodeEditor : public QsciScintilla #ifndef SIP_RUN static inline QgsSettingsTreeNode *sTreeCodeEditor = QgsSettingsTree::sTreeGui->createChildNode( QStringLiteral( "code-editor" ) ); + static const QgsSettingsEntryBool *settingContextHelpHover; #endif /** @@ -551,6 +553,14 @@ class GUI_EXPORT QgsCodeEditor : public QsciScintilla */ void persistentHistoryCleared(); + + /** + * Emitted when documentation was requested for the specified \a word. + * + * \since QGIS 3.42 + */ + void helpRequested( const QString &word ); + protected: /** diff --git a/src/gui/codeeditors/qgscodeeditorpython.cpp b/src/gui/codeeditors/qgscodeeditorpython.cpp index 65c167c566e6..078026aeb287 100644 --- a/src/gui/codeeditors/qgscodeeditorpython.cpp +++ b/src/gui/codeeditors/qgscodeeditorpython.cpp @@ -22,6 +22,7 @@ #include "qgspythonrunner.h" #include "qgsprocessingutils.h" #include "qgssettingsentryimpl.h" +#include "qgssettingsentryenumflag.h" #include "qgssettings.h" #include #include @@ -52,6 +53,7 @@ const QgsSettingsEntryBool *QgsCodeEditorPython::settingSortImports = new QgsSet const QgsSettingsEntryInteger *QgsCodeEditorPython::settingAutopep8Level = new QgsSettingsEntryInteger( QStringLiteral( "autopep8-level" ), sTreePythonCodeEditor, 1, QStringLiteral( "Autopep8 aggressive level" ) ); const QgsSettingsEntryBool *QgsCodeEditorPython::settingBlackNormalizeQuotes = new QgsSettingsEntryBool( QStringLiteral( "black-normalize-quotes" ), sTreePythonCodeEditor, true, QStringLiteral( "Whether quotes should be normalized when auto-formatting code using black" ) ); const QgsSettingsEntryString *QgsCodeEditorPython::settingExternalPythonEditorCommand = new QgsSettingsEntryString( QStringLiteral( "external-editor" ), sTreePythonCodeEditor, QString(), QStringLiteral( "Command to launch an external Python code editor. Use the token to insert the filename, to insert line number, and to insert the column number." ) ); +const QgsSettingsEntryEnumFlag< Qgis::DocumentationBrowser > *QgsCodeEditorPython::settingContextHelpBrowser = new QgsSettingsEntryEnumFlag< Qgis::DocumentationBrowser >( QStringLiteral( "context-help-browser" ), sTreePythonCodeEditor, Qgis::DocumentationBrowser::DeveloperToolsPanel, QStringLiteral( "Web browser used to display the api documentation" ) ); ///@endcond PRIVATE @@ -73,6 +75,8 @@ QgsCodeEditorPython::QgsCodeEditorPython( QWidget *parent, const QList QgsCodeEditorPython::initializeLexer(); + connect( this, &QgsCodeEditorPython::helpRequested, this, &QgsCodeEditorPython::showApiDocumentation ); + updateCapabilities(); } @@ -517,12 +521,24 @@ void QgsCodeEditorPython::populateContextMenu( QMenu *menu ) { QgsCodeEditor::populateContextMenu( menu ); + QString text = selectedText(); + if ( text.isEmpty() ) + { + text = wordAtPoint( mapFromGlobal( QCursor::pos() ) ); + } + if ( text.isEmpty() ) + { + return; + } + QAction *pyQgisHelpAction = new QAction( QgsApplication::getThemeIcon( QStringLiteral( "console/iconHelpConsole.svg" ) ), tr( "Search Selection in PyQGIS Documentation" ), menu ); + pyQgisHelpAction->setEnabled( hasSelectedText() ); - connect( pyQgisHelpAction, &QAction::triggered, this, &QgsCodeEditorPython::searchSelectedTextInPyQGISDocs ); + pyQgisHelpAction->setShortcut( QStringLiteral( "F1" ) ); + connect( pyQgisHelpAction, &QAction::triggered, this, [text, this] {showApiDocumentation( text );} ); menu->addSeparator(); menu->addAction( pyQgisHelpAction ); @@ -707,13 +723,32 @@ bool QgsCodeEditorPython::checkSyntax() void QgsCodeEditorPython::searchSelectedTextInPyQGISDocs() { - if ( !hasSelectedText() ) - return; + showApiDocumentation( selectedText() ); +} - QString text = selectedText(); - text = text.replace( QLatin1String( ">>> " ), QString() ).replace( QLatin1String( "... " ), QString() ).trimmed(); // removing prompts - const QString version = QString( Qgis::version() ).split( '.' ).mid( 0, 2 ).join( '.' ); - QDesktopServices::openUrl( QUrl( QStringLiteral( "https://qgis.org/pyqgis/%1/search.html?q=%2" ).arg( version, text ) ) ); +void QgsCodeEditorPython::showApiDocumentation( const QString &text ) +{ + QString searchText = text; + searchText = searchText.replace( QLatin1String( ">>> " ), QString() ).replace( QLatin1String( "... " ), QString() ).trimmed(); // removing prompts + + QRegularExpression qtExpression( "^Q[A-Z][a-zA-Z]" ); + + if ( qtExpression.match( searchText ).hasMatch() ) + { + const QString qtVersion = QString( qVersion() ).split( '.' ).mid( 0, 2 ).join( '.' ); + QString baseUrl = QString( "https://doc.qt.io/qt-%1" ).arg( qtVersion ); + QDesktopServices::openUrl( QUrl( QStringLiteral( "%1/%2.html" ).arg( baseUrl, searchText.toLower() ) ) ); + return; + } + const QString qgisVersion = QString( Qgis::version() ).split( '.' ).mid( 0, 2 ).join( '.' ); + if ( searchText.isEmpty() ) + { + QDesktopServices::openUrl( QUrl( QStringLiteral( "https://qgis.org/pyqgis/%1/" ).arg( qgisVersion ) ) ); + } + else + { + QDesktopServices::openUrl( QUrl( QStringLiteral( "https://qgis.org/pyqgis/%1/search.html?q=%2" ).arg( qgisVersion, searchText ) ) ); + } } void QgsCodeEditorPython::toggleComment() diff --git a/src/gui/codeeditors/qgscodeeditorpython.h b/src/gui/codeeditors/qgscodeeditorpython.h index 6e7813422982..f0c9efc6bc9e 100644 --- a/src/gui/codeeditors/qgscodeeditorpython.h +++ b/src/gui/codeeditors/qgscodeeditorpython.h @@ -23,6 +23,7 @@ class QgsSettingsEntryInteger; class QgsSettingsEntryBool; +template class QgsSettingsEntryEnumFlag; SIP_IF_MODULE( HAVE_QSCI_SIP ) @@ -62,6 +63,7 @@ class GUI_EXPORT QgsCodeEditorPython : public QgsCodeEditor static const QgsSettingsEntryInteger *settingAutopep8Level; static const QgsSettingsEntryBool *settingBlackNormalizeQuotes; static const QgsSettingsEntryString *settingExternalPythonEditorCommand; + static const QgsSettingsEntryEnumFlag< Qgis::DocumentationBrowser > *settingContextHelpBrowser; ///@endcond PRIVATE #endif @@ -129,6 +131,13 @@ class GUI_EXPORT QgsCodeEditorPython : public QgsCodeEditor */ void searchSelectedTextInPyQGISDocs(); + /** + * Displays the given text in the official APIs (PyQGIS, C++ QGIS or Qt) documentation. + * + * \since QGIS 3.42 + */ + virtual void showApiDocumentation( const QString &item ); + /** * Toggle comment for the selected text. * diff --git a/src/gui/qgisinterface.h b/src/gui/qgisinterface.h index 8563f6f94d5b..4c454e6c3ba2 100644 --- a/src/gui/qgisinterface.h +++ b/src/gui/qgisinterface.h @@ -1280,6 +1280,16 @@ class GUI_EXPORT QgisInterface : public QObject */ virtual void unregisterDevToolWidgetFactory( QgsDevToolWidgetFactory *factory ) = 0; + /** + * Show a page of the API documentation + * \param api Which API to display + * \param browser Web browser used to display the API documentation + * \param object object to show in the documentation + * \param module used only if api = Qgis::DocumentationApi::PyQgis + * \since QGIS 3.42 + */ + virtual void showApiDocumentation( Qgis::DocumentationApi api = Qgis::DocumentationApi::PyQgis, Qgis::DocumentationBrowser browser = Qgis::DocumentationBrowser::DeveloperToolsPanel, const QString &object = QString(), const QString &module = QString() ) = 0; + /** * Register a new application exit blocker, which can be used to prevent the QGIS application * from exiting while a plugin or script has unsaved changes. diff --git a/src/gui/qgsgui.cpp b/src/gui/qgsgui.cpp index 315d3b374aaa..e78c9cedef5d 100644 --- a/src/gui/qgsgui.cpp +++ b/src/gui/qgsgui.cpp @@ -527,6 +527,15 @@ void QgsGui::initCalloutWidgets() } ); } +bool QgsGui::hasWebEngine() +{ +#ifdef HAVE_WEBENGINE + return true; +#else + return false; +#endif +} + ///@cond PRIVATE void QgsGui::emitOptionsChanged() { diff --git a/src/gui/qgsgui.h b/src/gui/qgsgui.h index 50d0a85eef2f..f8a9402e72d7 100644 --- a/src/gui/qgsgui.h +++ b/src/gui/qgsgui.h @@ -318,6 +318,13 @@ class GUI_EXPORT QgsGui : public QObject */ static void initCalloutWidgets() SIP_SKIP; + /** + * Checks whether QWebEngineView is available to display HTML content. + * + * \since QGIS 3.42 + */ + static bool hasWebEngine(); + ///@cond PRIVATE void emitOptionsChanged() SIP_SKIP; ///@endcond diff --git a/src/ui/qgsdocumentationpanelbase.ui b/src/ui/qgsdocumentationpanelbase.ui new file mode 100644 index 000000000000..37f35dd0abb3 --- /dev/null +++ b/src/ui/qgsdocumentationpanelbase.ui @@ -0,0 +1,136 @@ + + + QgsDocumentationPanelBase + + + + 0 + 0 + 428 + 538 + + + + API Documentation + + + + :/images/themes/default/mActionHelpContents.svg:/images/themes/default/mActionHelpContents.svg + + + + + + + + PyQGIS API Documentation + + + + :/images/icons/qgis_icon.svg:/images/icons/qgis_icon.svg + + + + 24 + 24 + + + + true + + + + + + + Qt API Documentation + + + + :/images/themes/default/mIconQt.svg:/images/themes/default/mIconQt.svg + + + + 24 + 24 + + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Open in Web Browser + + + + :/images/themes/default/mIconWms.svg:/images/themes/default/mIconWms.svg + + + + 24 + 24 + + + + true + + + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + QgsPanelWidget + QWidget +
qgspanelwidget.h
+ 1 +
+
+ + + + +