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/gui/auto_additions/qgscodeeditor.py b/python/PyQt6/gui/auto_additions/qgscodeeditor.py index 9ca000f68875..0449f760090a 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 whent the F1 key is pressed while hovering over a 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_generated/codeeditors/qgscodeeditor.sip.in b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditor.sip.in index 30eccb100b18..605ec9b40620 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( QString word ); +%Docstring +Emitted whent the F1 key is pressed while hovering over a 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..1130559bd339 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 +Searches 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..a3ec357aeca3 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( const QString &api = QStringLiteral( "pyqgis" ), bool embedded = true, const QString &object = QString(), const QString &module = QString() ) = 0; +%Docstring +Show a page of the API documentation + +:param api: "pyqgis" or "qgis" or "qt" or "pyqgis-search" +:param embedded: If ``True``, the documentation will be opened in the embedded devtools webview. Otherwise, use system web browser +:param object: object to show in the documentation +:param module: used only if api = "pyqgis" + +.. versionadded:: 3.42 %End virtual void registerApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker ) = 0; 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..1c6068112c8c 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,40 @@ def __parse_object(object=None): module = match[1] obj = match[2] return 'qt', module, obj +""", + r""" +def _help(object=None, api="pyqgis", embedded=True, 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. + ''' + + if not object: + return iface.showApiDocumentation(api, embedded=embedded) + + if isinstance(object, str): + try: + object = eval(object) + except (SyntaxError, NameError): + if embedded and not force_search: + return iface.showApiDocumentation(api, embedded=True) + else: + return iface.showApiDocumentation("pyqgis-search", object=object, embedded=False) + + obj_info = __parse_object(object) + if not obj_info: + if force_search or isinstance(object, str) and not embedded: + return iface.showApiDocumentation("pyqgis-search", object=object, embedded=False) + else: + return iface.showApiDocumentation(api, embedded=embedded) + + obj_type, module, class_name = obj_info + if obj_type == "qt": + api = "qt" + + iface.showApiDocumentation(api, embedded=embedded, object=class_name, module=module) + """, r""" def _api(object=None): @@ -109,18 +145,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") """, r""" def _pyqgis(object=None): @@ -129,18 +154,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="pyqgis") """ ] @@ -224,9 +238,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("pyqgis") elif cmd == '_api': - webbrowser.open("https://qgis.org/api/{}".format('' if version == 'master' else version)) + self.shell.showApi("qgis") elif cmd == '_cookbook': webbrowser.open( "https://docs.qgis.org/{}/en/docs/pyqgis_developer_cookbook/".format( @@ -458,3 +472,28 @@ 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): + + pythonSettingsTreeNode = QgsSettingsTree.node("gui").childNode("code-editor").childNode("python") + + embedded = pythonSettingsTreeNode.childSetting('context-help-embedded').value() + + self._interpreter.execCommandImpl(f'_help({repr(text)}, api="pyqgis", embedded={embedded}, force_search={force_search})', show_input=False) + + def showApi(self, api): + pythonSettingsTreeNode = QgsSettingsTree.node("gui").childNode("code-editor").childNode("python") + embedded = pythonSettingsTreeNode.childSetting('context-help-embedded').value() + self._interpreter.execCommandImpl(f'_help(api="{api}", embedded={embedded})', 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..39ecc7e14b74 100644 --- a/python/console/console_settings.py +++ b/python/console/console_settings.py @@ -211,14 +211,16 @@ 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-embedded').setValue(self.contextHelpBrowser.currentIndex() == 0) + codeEditorTreeNode.childSetting('context-help-hover').setValue(self.contextHelpHover.isChecked()) def restoreSettings(self): settings = QgsSettings() @@ -244,7 +246,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()) @@ -252,6 +255,8 @@ def restoreSettings(self): self.autopep8Level.setValue(pythonSettingsTreeNode.childSetting("autopep8-level").value()) self.blackNormalizeQuotes.setChecked(pythonSettingsTreeNode.childSetting("black-normalize-quotes").value()) self.maxLineLength.setValue(pythonSettingsTreeNode.childSetting("max-line-length").value()) + self.contextHelpBrowser.setCurrentIndex(0 if pythonSettingsTreeNode.childSetting('context-help-embedded').value() else 1) + self.contextHelpHover.setChecked(codeEditorTreeNode.childSetting('context-help-hover').value()) if settings.value("pythonConsole/autoCompleteSource") == 'fromDoc': self.autoCompFromDoc.setChecked(True) diff --git a/python/console/console_settings.ui b/python/console/console_settings.ui index 70c26cbc6715..c8e1c3c81e23 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,35 @@ true - + 2 - + - Automatic parentheses insertion + Open in - - - - Automatically surround selection when typing quotes or brackets - - - - - - - Automatic insertion of the 'import' string on 'from xxx' - + + + + + Embedded Webview (developer tools) + + + + + Default system web browser + + - - - - - - - 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 +655,6 @@ groupBoxAutoCompletion - scrollArea autoCompThreshold autoCompFromDoc autoCompFromAPI @@ -616,6 +662,7 @@ autoCloseBracket autoSurround autoInsertImport + contextHelpBrowser formatOnSave sortImports maxLineLength @@ -631,6 +678,8 @@ groupBoxPreparedAPI compileAPIs lineEdit + scrollArea + externalEditor diff --git a/python/gui/auto_additions/qgscodeeditor.py b/python/gui/auto_additions/qgscodeeditor.py index 56a526d62071..d6374e103a41 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 whent the F1 key is pressed while hovering over a 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_generated/codeeditors/qgscodeeditor.sip.in b/python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in index 2fe8d0f9c15c..47847693be0e 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( QString word ); +%Docstring +Emitted whent the F1 key is pressed while hovering over a 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..1130559bd339 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 +Searches 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..a3ec357aeca3 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( const QString &api = QStringLiteral( "pyqgis" ), bool embedded = true, const QString &object = QString(), const QString &module = QString() ) = 0; +%Docstring +Show a page of the API documentation + +:param api: "pyqgis" or "qgis" or "qt" or "pyqgis-search" +:param embedded: If ``True``, the documentation will be opened in the embedded devtools webview. Otherwise, use system web browser +:param object: object to show in the documentation +:param module: used only if api = "pyqgis" + +.. versionadded:: 3.42 %End virtual void registerApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker ) = 0; diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index dab19c127f3a..ed6ddcc8533d 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..0fe86ddb81a2 --- /dev/null +++ b/src/app/devtools/documentation/qgsdocumentationpanelwidget.cpp @@ -0,0 +1,42 @@ +/*************************************************************************** + 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 "qgswebview.h" +#include "qgisapp.h" +#include + +// +// QgsDocumentationPanelWidget +// + +QgsDocumentationPanelWidget::QgsDocumentationPanelWidget( QWidget *parent ) + : QgsDevToolWidget( parent ) +{ + setupUi( this ); + + connect( mPyQgisHomeButton, &QToolButton::clicked, this, [] {QgisApp::instance()->showApiDocumentation( QStringLiteral( "pyqgis" ), true );} ); + connect( mQtHomeButton, &QToolButton::clicked, this, [] {QgisApp::instance()->showApiDocumentation( QStringLiteral( "qt" ), true );} ); + 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..e0288928a229 --- /dev/null +++ b/src/app/devtools/documentation/qgsdocumentationpanelwidget.h @@ -0,0 +1,45 @@ +/*************************************************************************** + 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 "ui_qgsdocumentationpanelbase.h" + +/** + * \ingroup app + * \class QgsDocumentationPanelWidget + * \brief A panel widget showing profiled startup times for debugging. + * + * \since QGIS 3.14 + */ +class QgsDocumentationPanelWidget : public QgsDevToolWidget, private Ui::QgsDocumentationPanelBase +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsDocumentationPanelWidget. + */ + QgsDocumentationPanelWidget( QWidget *parent ); + + + void showUrl( const QUrl &url ); + +}; + + +#endif // QGSDOCUMENTATIONPANELWIDGET_H diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 128f26ec4df9..a05839079c88 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -12963,25 +12963,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", false ); } void QgisApp::pyQgisApiDocumentation() { - QgsSettings settings; - QString PyQgisApiUrl = settings.value( QStringLiteral( "qgis/PyQgisApiUrl" ), - QStringLiteral( "https://qgis.org/pyqgis/" ) ).toString(); - openURL( PyQgisApiUrl, false ); + showApiDocumentation( "pyqgis", false ); } void QgisApp::reportaBug() @@ -13125,6 +13112,96 @@ void QgisApp::unregisterDevToolFactory( QgsDevToolWidgetFactory *factory ) mDevToolFactories.removeAll( factory ); } + +void QgisApp::showApiDocumentation( const QString &api, bool embedded, const QString &object, const QString &module ) +{ + bool useQgisDocDirectory = false; + QString baseUrl; + QString version; + + if ( api == "qt" ) + { + version = QString( qVersion() ).split( '.' ).mid( 0, 2 ).join( '.' ); + baseUrl = QString( "https://doc.qt.io/qt-%1/" ).arg( version ); + } + else if ( api.contains( "qgis" ) ) + { + if ( Qgis::version().toLower().contains( QStringLiteral( "master" ) ) ) + { + version = QStringLiteral( "master" ); + } + else + { + version = QString( Qgis::version() ).split( '.' ).mid( 0, 2 ).join( '.' ); + } + + if ( api.contains( "pyqgis" ) ) + { + 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(); + } + } + } + else + { + messageBar()->pushWarning( tr( "Unknown API" ), api ); + return; + } + + QString url; + if ( object.isEmpty() ) + { + url = baseUrl == "api/" ? baseUrl + "index.html" : baseUrl; + } + else + { + if ( api == QStringLiteral( "pyqgis" ) ) + { + url = baseUrl + QString( "%1/%2.html" ).arg( module, object ); + } + else if ( api == QStringLiteral( "pyqgis-search" ) ) + { + url = baseUrl + QString( "search.html?q=%2" ).arg( object ); + } + else if ( api == QStringLiteral( "qgis" ) ) + { + url = baseUrl + QString( "class%1.html" ).arg( object ); + } + else // Qt + { + url = baseUrl + QString( "%1.html" ).arg( object.toLower() ); + } + } + + if ( embedded ) + { + if ( useQgisDocDirectory ) + { + url = "file://" + QgsApplication::pkgDataPath() + "/doc/" + url; + } + mDevToolsDock->show(); + mDevToolsWidget->showUrl( QUrl( url ) ); + } + else + { + openURL( url, useQgisDocDirectory ); + } +} + void QgisApp::registerApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker ) { mApplicationExitBlockers << blocker; diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h index 7514bb96ccde..38aa74ca3698 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( const QString &api, bool embedded, 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 2e9a1d4d925a..5699c60067a7 100644 --- a/src/app/qgisappinterface.cpp +++ b/src/app/qgisappinterface.cpp @@ -640,6 +640,12 @@ void QgisAppInterface::unregisterDevToolWidgetFactory( QgsDevToolWidgetFactory * qgis->unregisterDevToolFactory( factory ); } +void QgisAppInterface::showApiDocumentation( const QString &api, bool embedded, const QString &object, const QString &module ) +{ + qgis->showApiDocumentation( api, embedded, object, module ); +} + + void QgisAppInterface::registerApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker ) { qgis->registerApplicationExitBlocker( blocker ); diff --git a/src/app/qgisappinterface.h b/src/app/qgisappinterface.h index d1494ed9fbce..8237d5e9c595 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( const QString &api, bool embedded, 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 2199cdf3e378..35181cc2340c 100644 --- a/src/app/qgsdevtoolspanelwidget.cpp +++ b/src/app/qgsdevtoolspanelwidget.cpp @@ -18,6 +18,7 @@ #include "qgsdevtoolwidget.h" #include "qgspanelwidgetstack.h" #include "qgssettingsentryimpl.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" ) ); @@ -30,6 +31,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 ); @@ -43,6 +49,21 @@ QgsDevToolsPanelWidget::QgsDevToolsPanelWidget( const QListaddWidget( widget ); + + QListWidgetItem *item = new QListWidgetItem( widget->windowIcon(), QString() ); + item->setToolTip( widget->windowTitle() ); + item->setData( Qt::UserRole, widget->windowTitle() ); + mOptionsListWidget->addItem( item ); + if ( mOptionsListWidget->count() == 1 ) + { + setCurrentTool( 0 ); + } +} + + void QgsDevToolsPanelWidget::addToolFactory( QgsDevToolWidgetFactory *factory ) { if ( QgsDevToolWidget *toolWidget = factory->createWidget( this ) ) @@ -103,3 +124,9 @@ void QgsDevToolsPanelWidget::setCurrentTool( int row ) whileBlocking( mOptionsListWidget )->setCurrentRow( row ); mStackedWidget->setCurrentIndex( row ); } + +void QgsDevToolsPanelWidget::showUrl( const QUrl &url ) +{ + whileBlocking( mOptionsListWidget )->setCurrentRow( 0 ); + mDocumentationPanel->showUrl( url ); +} diff --git a/src/app/qgsdevtoolspanelwidget.h b/src/app/qgsdevtoolspanelwidget.h index 13e46402817f..d35f4677bcf0 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,15 @@ 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 showUrl( const QUrl &url ); + private slots: void setCurrentTool( int row ); @@ -45,6 +49,7 @@ class APP_EXPORT QgsDevToolsPanelWidget : public QWidget, private Ui::QgsDevTool private: QMap< QgsDevToolWidgetFactory *, int> mFactoryPages; + QgsDocumentationPanelWidget *mDocumentationPanel; }; #endif // QGSDEVTOOLSPANELWIDGET_H 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/codeeditors/qgscodeeditor.cpp b/src/gui/codeeditors/qgscodeeditor.cpp index b37baa753e7a..b4407949d46a 100644 --- a/src/gui/codeeditors/qgscodeeditor.cpp +++ b/src/gui/codeeditors/qgscodeeditor.cpp @@ -23,6 +23,7 @@ #include "qgscodeeditorhistorydialog.h" #include "qgsstringutils.h" #include "qgsfontutils.h" +#include "qgssettingsentryimpl.h" #include #include @@ -37,6 +38,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" ) }, @@ -192,6 +199,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..7337ba5705e3 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 whent the F1 key is pressed while hovering over a word + * + * \since QGIS 3.42 + */ + void helpRequested( QString word ); + protected: /** diff --git a/src/gui/codeeditors/qgscodeeditorpython.cpp b/src/gui/codeeditors/qgscodeeditorpython.cpp index 7f3ac0b19eb2..bb7789f4787d 100644 --- a/src/gui/codeeditors/qgscodeeditorpython.cpp +++ b/src/gui/codeeditors/qgscodeeditorpython.cpp @@ -51,6 +51,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 QgsSettingsEntryBool *QgsCodeEditorPython::settingContextHelpEmbedded = new QgsSettingsEntryBool( QStringLiteral( "context-help-embedded" ), sTreePythonCodeEditor, true, QStringLiteral( "Whether the context help should be displayed in an embedded webview in the devtools panel" ) ); ///@endcond PRIVATE @@ -72,6 +73,8 @@ QgsCodeEditorPython::QgsCodeEditorPython( QWidget *parent, const QList QgsCodeEditorPython::initializeLexer(); + connect( this, &QgsCodeEditorPython::helpRequested, this, &QgsCodeEditorPython::showApiDocumentation ); + updateCapabilities(); } @@ -516,12 +519,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 ); @@ -706,13 +721,25 @@ 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( '.' ); + 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..fc125410c6c2 100644 --- a/src/gui/codeeditors/qgscodeeditorpython.h +++ b/src/gui/codeeditors/qgscodeeditorpython.h @@ -62,6 +62,7 @@ class GUI_EXPORT QgsCodeEditorPython : public QgsCodeEditor static const QgsSettingsEntryInteger *settingAutopep8Level; static const QgsSettingsEntryBool *settingBlackNormalizeQuotes; static const QgsSettingsEntryString *settingExternalPythonEditorCommand; + static const QgsSettingsEntryBool *settingContextHelpEmbedded; ///@endcond PRIVATE #endif @@ -129,6 +130,13 @@ class GUI_EXPORT QgsCodeEditorPython : public QgsCodeEditor */ void searchSelectedTextInPyQGISDocs(); + /** + * Searches 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..c2603b627d4f 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 "pyqgis" or "qgis" or "qt" or "pyqgis-search" + * \param embedded If TRUE, the documentation will be opened in the embedded devtools webview. Otherwise, use system web browser + * \param object object to show in the documentation + * \param module used only if api = "pyqgis" + * \since QGIS 3.42 + */ + virtual void showApiDocumentation( const QString &api = QStringLiteral( "pyqgis" ), bool embedded = true, 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/ui/qgsdocumentationpanelbase.ui b/src/ui/qgsdocumentationpanelbase.ui new file mode 100644 index 000000000000..288d4dee34d1 --- /dev/null +++ b/src/ui/qgsdocumentationpanelbase.ui @@ -0,0 +1,119 @@ + + + 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 + + + + + + + + Qt API Documentation + + + + :/images/themes/default/mIconQt.svg:/images/themes/default/mIconQt.svg + + + + 24 + 24 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Open in Web Browser + + + + :/images/themes/default/mIconWms.svg:/images/themes/default/mIconWms.svg + + + + 24 + 24 + + + + + + + + + + + 0 + 0 + + + + + + + + + QgsPanelWidget + QWidget +
qgspanelwidget.h
+ 1 +
+ + QgsWebView + QWidget +
qgswebview.h
+ 1 +
+
+ + + + +