diff --git a/python/console/console.py b/python/console/console.py index 0ee5451f188..24d1ac1c939 100644 --- a/python/console/console.py +++ b/python/console/console.py @@ -110,7 +110,6 @@ class PythonConsole(QgsDockWidget): super().__init__(parent) self.setObjectName("PythonConsole") self.setWindowTitle(QCoreApplication.translate("PythonConsole", "Python Console")) - # self.setAllowedAreas(Qt.BottomDockWidgetArea) self.console = PythonConsoleWidget(self) QgsGui.instance().optionsChanged.connect(self.console.updateSettings) @@ -167,13 +166,9 @@ class PythonConsoleWidget(QWidget): self.splitter.addWidget(self.shellOutWidget) self.splitter.addWidget(self.shell) - # self.splitterEditor.addWidget(self.tabEditorWidget) - self.splitterObj = QSplitter(self.splitterEditor) self.splitterObj.setHandleWidth(3) self.splitterObj.setOrientation(Qt.Horizontal) - # self.splitterObj.setSizes([0, 0]) - # self.splitterObj.setStretchFactor(0, 1) self.widgetEditor = QWidget(self.splitterObj) self.widgetFind = QWidget(self) @@ -185,10 +180,6 @@ class PythonConsoleWidget(QWidget): self.listClassMethod.setColumnHidden(1, True) self.listClassMethod.setAlternatingRowColors(True) - # self.splitterEditor.addWidget(self.widgetEditor) - # self.splitterObj.addWidget(self.listClassMethod) - # self.splitterObj.addWidget(self.widgetEditor) - # Hide side editor on start up self.splitterObj.hide() self.listClassMethod.hide() @@ -286,26 +277,18 @@ class PythonConsoleWidget(QWidget): self.runScriptEditorButton.setIconVisibleInMenu(True) self.runScriptEditorButton.setToolTip(runScriptEditorBt) self.runScriptEditorButton.setText(runScriptEditorBt) - # Action Run Script (subprocess) - commentEditorBt = QCoreApplication.translate("PythonConsole", "Comment") - self.commentEditorButton = QAction(self) - self.commentEditorButton.setCheckable(False) - self.commentEditorButton.setEnabled(True) - self.commentEditorButton.setIcon(QgsApplication.getThemeIcon("console/iconCommentEditorConsole.svg")) - self.commentEditorButton.setMenuRole(QAction.PreferencesRole) - self.commentEditorButton.setIconVisibleInMenu(True) - self.commentEditorButton.setToolTip(commentEditorBt) - self.commentEditorButton.setText(commentEditorBt) - # Action Run Script (subprocess) - uncommentEditorBt = QCoreApplication.translate("PythonConsole", "Uncomment") - self.uncommentEditorButton = QAction(self) - self.uncommentEditorButton.setCheckable(False) - self.uncommentEditorButton.setEnabled(True) - self.uncommentEditorButton.setIcon(QgsApplication.getThemeIcon("console/iconUncommentEditorConsole.svg")) - self.uncommentEditorButton.setMenuRole(QAction.PreferencesRole) - self.uncommentEditorButton.setIconVisibleInMenu(True) - self.uncommentEditorButton.setToolTip(uncommentEditorBt) - self.uncommentEditorButton.setText(uncommentEditorBt) + + # Action Toggle comment + toggleText = QCoreApplication.translate("PythonConsole", "Toggle Comment") + self.toggleCommentEditorButton = QAction(self) + self.toggleCommentEditorButton.setCheckable(False) + self.toggleCommentEditorButton.setEnabled(True) + self.toggleCommentEditorButton.setIcon(QgsApplication.getThemeIcon("console/iconCommentEditorConsole.svg")) + self.toggleCommentEditorButton.setMenuRole(QAction.PreferencesRole) + self.toggleCommentEditorButton.setIconVisibleInMenu(True) + self.toggleCommentEditorButton.setToolTip(toggleText + " Ctrl+:") + self.toggleCommentEditorButton.setText(toggleText) + # Action for Object browser objList = QCoreApplication.translate("PythonConsole", "Object Inspector…") self.objectListButton = QAction(self) @@ -433,8 +416,7 @@ class PythonConsoleWidget(QWidget): self.toolBarEditor.addSeparator() self.toolBarEditor.addAction(self.findTextButton) self.toolBarEditor.addSeparator() - self.toolBarEditor.addAction(self.commentEditorButton) - self.toolBarEditor.addAction(self.uncommentEditorButton) + self.toolBarEditor.addAction(self.toggleCommentEditorButton) self.toolBarEditor.addSeparator() self.toolBarEditor.addAction(self.objectListButton) @@ -527,8 +509,7 @@ class PythonConsoleWidget(QWidget): self.findTextButton.triggered.connect(self._toggleFind) self.objectListButton.toggled.connect(self.toggleObjectListWidget) - self.commentEditorButton.triggered.connect(self.commentCode) - self.uncommentEditorButton.triggered.connect(self.uncommentCode) + self.toggleCommentEditorButton.triggered.connect(self.toggleComment) self.runScriptEditorButton.triggered.connect(self.runScriptEditor) self.cutEditorButton.triggered.connect(self.cutEditor) self.copyEditorButton.triggered.connect(self.copyEditor) @@ -657,11 +638,8 @@ class PythonConsoleWidget(QWidget): def runScriptEditor(self): self.tabEditorWidget.currentWidget().newEditor.runScriptCode() - def commentCode(self): - self.tabEditorWidget.currentWidget().newEditor.commentEditorCode(True) - - def uncommentCode(self): - self.tabEditorWidget.currentWidget().newEditor.commentEditorCode(False) + def toggleComment(self): + self.tabEditorWidget.currentWidget().newEditor.toggleComment() def openScriptFileExtEditor(self): tabWidget = self.tabEditorWidget.currentWidget() diff --git a/python/console/console_editor.py b/python/console/console_editor.py index 96d0982512b..b23137ce0f5 100644 --- a/python/console/console_editor.py +++ b/python/console/console_editor.py @@ -122,12 +122,6 @@ class Editor(QgsCodeEditorPython): self.syntaxCheckScut = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_4), self) self.syntaxCheckScut.setContext(Qt.WidgetShortcut) self.syntaxCheckScut.activated.connect(self.syntaxCheck) - self.commentScut = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_3), self) - self.commentScut.setContext(Qt.WidgetShortcut) - self.commentScut.activated.connect(self.parent.pc.commentCode) - self.uncommentScut = QShortcut(QKeySequence(Qt.SHIFT + Qt.CTRL + Qt.Key_3), self) - self.uncommentScut.setContext(Qt.WidgetShortcut) - self.uncommentScut.activated.connect(self.parent.pc.uncommentCode) self.modificationChanged.connect(self.parent.modified) self.modificationAttempted.connect(self.fileReadOnly) @@ -178,11 +172,8 @@ class Editor(QgsCodeEditorPython): self.selectAll, QKeySequence.SelectAll) menu.addSeparator() menu.addAction(QgsApplication.getThemeIcon("console/iconCommentEditorConsole.svg"), - QCoreApplication.translate("PythonConsole", "Comment"), - self.parent.pc.commentCode, 'Ctrl+3') - menu.addAction(QgsApplication.getThemeIcon("console/iconUncommentEditorConsole.svg"), - QCoreApplication.translate("PythonConsole", "Uncomment"), - self.parent.pc.uncommentCode, 'Shift+Ctrl+3') + QCoreApplication.translate("PythonConsole", "Toggle Comment"), + self.toggleComment, 'Ctrl+:') menu.addSeparator() gist_menu = QMenu(self) gist_menu.setTitle(QCoreApplication.translate("PythonConsole", "Share on GitHub")) @@ -338,31 +329,6 @@ class Editor(QgsCodeEditorPython): else: self.openFindWidget() - def commentEditorCode(self, commentCheck): - self.beginUndoAction() - if self.hasSelectedText(): - startLine, _, endLine, _ = self.getSelection() - for line in range(startLine, endLine + 1): - if commentCheck: - self.insertAt('#', line, 0) - else: - if not self.text(line).strip().startswith('#'): - continue - self.setSelection(line, self.indentation(line), - line, self.indentation(line) + 1) - self.removeSelectedText() - else: - line, pos = self.getCursorPosition() - if commentCheck: - self.insertAt('#', line, 0) - else: - if not self.text(line).strip().startswith('#'): - return - self.setSelection(line, self.indentation(line), - line, self.indentation(line) + 1) - self.removeSelectedText() - self.endUndoAction() - def createTempFile(self): import tempfile fd, path = tempfile.mkstemp() @@ -547,7 +513,7 @@ class Editor(QgsCodeEditorPython): if re.match(ptrn, txt): self.insert(' import') self.setCursorPosition(line, pos + 7) - QsciScintilla.keyPressEvent(self, e) + QgsCodeEditorPython.keyPressEvent(self, e) def focusInEvent(self, e): pathfile = self.parent.path @@ -560,7 +526,6 @@ class Editor(QgsCodeEditorPython): if pathfile and self.lastModified != QFileInfo(pathfile).lastModified(): self.beginUndoAction() self.selectAll() - # fileReplaced = self.selectedText() self.removeSelectedText() file = open(pathfile, "r") fileLines = file.readlines() @@ -658,7 +623,6 @@ class EditorTab(QWidget): if overwrite: try: permis = os.stat(path).st_mode - # self.newEditor.lastModified = QFileInfo(path).lastModified() os.chmod(path, permis) except: raise diff --git a/python/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in b/python/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in index 173ff5160f1..8729c3b6e06 100644 --- a/python/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in +++ b/python/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in @@ -62,6 +62,13 @@ Loads a ``script`` file. Searches the selected text in the official PyQGIS online documentation. .. versionadded:: 3.16 +%End + + void toggleComment(); +%Docstring +Toggle comment for the selected text. + +.. versionadded:: 3.30 %End protected: @@ -69,6 +76,8 @@ Searches the selected text in the official PyQGIS online documentation. virtual void initializeLexer(); + virtual void keyPressEvent( QKeyEvent *event ); + protected slots: void autoComplete(); diff --git a/python/plugins/processing/script/ScriptEditorDialog.py b/python/plugins/processing/script/ScriptEditorDialog.py index 7626f0240ee..e67fd729bcb 100644 --- a/python/plugins/processing/script/ScriptEditorDialog.py +++ b/python/plugins/processing/script/ScriptEditorDialog.py @@ -92,6 +92,8 @@ class ScriptEditorDialog(BASE, WIDGET): QgsApplication.getThemeIcon('/mActionIncreaseFont.svg')) self.actionDecreaseFontSize.setIcon( QgsApplication.getThemeIcon('/mActionDecreaseFont.svg')) + self.actionToggleComment.setIcon( + QgsApplication.getThemeIcon('console/iconCommentEditorConsole.svg')) # Connect signals and slots self.actionOpenScript.triggered.connect(self.openScript) @@ -106,6 +108,7 @@ class ScriptEditorDialog(BASE, WIDGET): self.actionFindReplace.toggled.connect(self.toggleSearchBox) self.actionIncreaseFontSize.triggered.connect(self.editor.zoomIn) self.actionDecreaseFontSize.triggered.connect(self.editor.zoomOut) + self.actionToggleComment.triggered.connect(self.editor.toggleComment) self.editor.textChanged.connect(lambda: self.setHasChanged(True)) self.leFindText.returnPressed.connect(self.find) diff --git a/python/plugins/processing/ui/DlgScriptEditor.ui b/python/plugins/processing/ui/DlgScriptEditor.ui index 653705d5b51..a349e51835b 100644 --- a/python/plugins/processing/ui/DlgScriptEditor.ui +++ b/python/plugins/processing/ui/DlgScriptEditor.ui @@ -6,7 +6,7 @@ 0 0 - 721 + 600 578 @@ -120,6 +120,8 @@ + + @@ -240,11 +242,16 @@ true + + Find && &Replace + Ctrl+F + + - Find && &Replace + Toggle Comment diff --git a/src/app/maptools/qgsmaptoolshaperegularpolygonabstract.cpp b/src/app/maptools/qgsmaptoolshaperegularpolygonabstract.cpp index 814d3b558ef..8e3ad267359 100644 --- a/src/app/maptools/qgsmaptoolshaperegularpolygonabstract.cpp +++ b/src/app/maptools/qgsmaptoolshaperegularpolygonabstract.cpp @@ -21,7 +21,7 @@ #include "qgisapp.h" #include "qgsmaptoolcapture.h" -QgsMapToolShapeRegularPolygonAbstract::QgsMapToolShapeRegularPolygonAbstract(const QString &id, QgsMapToolCapture *parentTool ) +QgsMapToolShapeRegularPolygonAbstract::QgsMapToolShapeRegularPolygonAbstract( const QString &id, QgsMapToolCapture *parentTool ) : QgsMapToolShapeAbstract( id, parentTool ) { } diff --git a/src/app/maptools/qgsmaptoolshaperegularpolygonabstract.h b/src/app/maptools/qgsmaptoolshaperegularpolygonabstract.h index 1cc9b0b2a18..09dd6faef39 100644 --- a/src/app/maptools/qgsmaptoolshaperegularpolygonabstract.h +++ b/src/app/maptools/qgsmaptoolshaperegularpolygonabstract.h @@ -28,7 +28,7 @@ class APP_EXPORT QgsMapToolShapeRegularPolygonAbstract: public QgsMapToolShapeAb Q_OBJECT public: - QgsMapToolShapeRegularPolygonAbstract(const QString &id, QgsMapToolCapture *parentTool); + QgsMapToolShapeRegularPolygonAbstract( const QString &id, QgsMapToolCapture *parentTool ); void clean() override; diff --git a/src/gui/codeeditors/qgscodeeditorpython.cpp b/src/gui/codeeditors/qgscodeeditorpython.cpp index 6dabd3499ac..0c5e0ebe34e 100644 --- a/src/gui/codeeditors/qgscodeeditorpython.cpp +++ b/src/gui/codeeditors/qgscodeeditorpython.cpp @@ -29,6 +29,7 @@ #include #include #include +#include QgsCodeEditorPython::QgsCodeEditorPython( QWidget *parent, const QList &filenames, Mode mode ) : QgsCodeEditor( parent, @@ -185,6 +186,19 @@ void QgsCodeEditorPython::initializeLexer() runPostLexerConfigurationTasks(); } +void QgsCodeEditorPython::keyPressEvent( QKeyEvent *event ) +{ + const bool ctrlModifier = event->modifiers() & Qt::ControlModifier; + + if ( ctrlModifier && event->key() == Qt::Key_Colon ) + { + event->accept(); + toggleComment(); + return; + } + return QgsCodeEditor::keyPressEvent( event ); +} + void QgsCodeEditorPython::autoComplete() { switch ( autoCompletionSource() ) @@ -242,6 +256,94 @@ void QgsCodeEditorPython::searchSelectedTextInPyQGISDocs() QDesktopServices::openUrl( QUrl( QStringLiteral( "https://qgis.org/pyqgis/%1/search.html?q=%2" ).arg( version, text ) ) ); } +void QgsCodeEditorPython::toggleComment() +{ + if ( isReadOnly() ) + { + return; + } + + beginUndoAction(); + int startLine, startPos, endLine, endPos; + if ( hasSelectedText() ) + { + getSelection( &startLine, &startPos, &endLine, &endPos ); + } + else + { + getCursorPosition( &startLine, &startPos ); + endLine = startLine; + endPos = startPos; + } + + // Check comment state and minimum indentation for each selected line + bool allEmpty = true; + bool allCommented = true; + int minIndentation = -1; + for ( int line = startLine; line <= endLine; line++ ) + { + const QString stripped = text( line ).trimmed(); + if ( !stripped.isEmpty() ) + { + allEmpty = false; + if ( !stripped.startsWith( '#' ) ) + { + allCommented = false; + } + if ( minIndentation == -1 || minIndentation > indentation( line ) ) + { + minIndentation = indentation( line ); + } + } + } + + // Special case, only empty lines + if ( allEmpty ) + { + return; + } + + // Selection shift to keep the same selected text after a # is added/removed + int delta = 0; + + for ( int line = startLine; line <= endLine; line++ ) + { + const QString stripped = text( line ).trimmed(); + + // Empty line + if ( stripped.isEmpty() ) + { + continue; + } + + if ( !allCommented ) + { + insertAt( QStringLiteral( "# " ), line, minIndentation ); + delta = -2; + } + else + { + if ( !stripped.startsWith( '#' ) ) + { + continue; + } + if ( stripped.startsWith( QLatin1String( "# " ) ) ) + { + delta = 2; + } + else + { + delta = 1; + } + setSelection( line, indentation( line ), line, indentation( line ) + delta ); + removeSelectedText(); + } + } + + endUndoAction(); + setSelection( startLine, startPos - delta, endLine, endPos - delta ); +} + ///@cond PRIVATE // // QgsQsciLexerPython @@ -256,9 +358,9 @@ const char *QgsQsciLexerPython::keywords( int set ) const { if ( set == 1 ) { - return "True False and as assert break class continue def del elif else except exec " + return "True False and as assert break class continue def del elif else except " "finally for from global if import in is lambda None not or pass " - "print raise return try while with yield"; + "raise return try while with yield async await nonlocal"; } return QsciLexerPython::keywords( set ); diff --git a/src/gui/codeeditors/qgscodeeditorpython.h b/src/gui/codeeditors/qgscodeeditorpython.h index 1089aae306b..0f3f5459d36 100644 --- a/src/gui/codeeditors/qgscodeeditorpython.h +++ b/src/gui/codeeditors/qgscodeeditorpython.h @@ -84,10 +84,19 @@ class GUI_EXPORT QgsCodeEditorPython : public QgsCodeEditor */ void searchSelectedTextInPyQGISDocs(); + /** + * Toggle comment for the selected text. + * + * \since QGIS 3.30 + */ + void toggleComment(); + protected: void initializeLexer() override; + virtual void keyPressEvent( QKeyEvent *event ) override; + protected slots: /**