Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't silently close QGIS with unsaved changes in the console script #39191

Merged
merged 3 commits into from Oct 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
51 changes: 49 additions & 2 deletions python/console/console.py
Expand Up @@ -23,14 +23,23 @@
from qgis.PyQt.QtCore import Qt, QTimer, QCoreApplication, QSize, QByteArray, QFileInfo, QUrl, QDir
from qgis.PyQt.QtWidgets import QToolBar, QToolButton, QWidget, QSplitter, QTreeWidget, QAction, QFileDialog, QCheckBox, QSizePolicy, QMenu, QGridLayout, QApplication, QShortcut
from qgis.PyQt.QtGui import QDesktopServices, QKeySequence
from qgis.PyQt.QtWidgets import QVBoxLayout
from qgis.PyQt.QtWidgets import (
QVBoxLayout,
QMessageBox
)
from qgis.utils import iface
from .console_sci import ShellScintilla
from .console_output import ShellOutputScintilla
from .console_editor import EditorTabWidget
from .console_settings import ConsoleOptionsFactory
from qgis.core import Qgis, QgsApplication, QgsSettings
from qgis.gui import QgsFilterLineEdit, QgsHelp, QgsDockWidget, QgsGui
from qgis.gui import (
QgsFilterLineEdit,
QgsHelp,
QgsDockWidget,
QgsGui,
QgsApplicationExitBlockerInterface
)
from functools import partial

import sys
Expand Down Expand Up @@ -80,6 +89,16 @@ def init_options_widget():
iface.registerOptionsWidgetFactory(_options_factory)


class ConsoleExitBlocker(QgsApplicationExitBlockerInterface):

def __init__(self, console):
super().__init__()
self.console = console

def allowExit(self):
return self.console.allowExit()


class PythonConsole(QgsDockWidget):

def __init__(self, parent=None):
Expand Down Expand Up @@ -543,6 +562,34 @@ def __init__(self, parent=None):
self.findScut.setContext(Qt.WidgetWithChildrenShortcut)
self.findScut.activated.connect(self._closeFind)

if iface is not None:
self.exit_blocker = ConsoleExitBlocker(self)
iface.registerApplicationExitBlocker(self.exit_blocker)

def allowExit(self):
tab_count = self.tabEditorWidget.count()
for i in range(tab_count):
# iterate backwards through tabs, as we may be closing some as we go
tab_index = tab_count - i - 1
tab_widget = self.tabEditorWidget.widget(tab_index)
if tab_widget.newEditor.isModified():
ret = QMessageBox.question(self, self.tr("Save {}").format(self.tabEditorWidget.tabText(tab_index)),
self.tr("There are unsaved changes in this script. Do you want to keep those?"),
QMessageBox.Save | QMessageBox.Cancel | QMessageBox.Discard, QMessageBox.Cancel)
if ret == QMessageBox.Save:
tab_widget.save()
if tab_widget.newEditor.isModified():
# save failed, treat as cancel
return False
elif ret == QMessageBox.Discard:
pass
else:
return False

self.tabEditorWidget.removeTab(tab_index)

return True

def _toggleFind(self):
self.tabEditorWidget.currentWidget().newEditor.toggleFindWidget()

Expand Down
24 changes: 24 additions & 0 deletions python/gui/auto_generated/qgisinterface.sip.in
Expand Up @@ -1234,6 +1234,30 @@ Unregister a previously registered tool factory from the development/debugging t
.. seealso:: :py:func:`registerDevToolWidgetFactory`

.. versionadded:: 3.14
%End

virtual void registerApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker ) = 0;
%Docstring
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.

.. note::

Ownership of ``blocker`` is not transferred, and the blocker must
be unregistered when plugin is unloaded.

.. seealso:: :py:func:`unregisterApplicationExitBlocker`

.. versionadded:: 3.16
%End

virtual void unregisterApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker ) = 0;
%Docstring
Unregister a previously registered application exit ``blocker``.

.. seealso:: :py:func:`registerApplicationExitBlocker`

.. versionadded:: 3.16
%End

virtual void registerCustomDropHandler( QgsCustomDropHandler *handler ) = 0;
Expand Down
@@ -0,0 +1,77 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/gui/qgsapplicationexitblockerinterface.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/



class QgsApplicationExitBlockerInterface
{
%Docstring
An interface that may be implemented to allow plugins or scripts to temporarily block
the QGIS application from exiting.

This interface allows plugins to implement custom logic to determine whether it is safe
for the application to exit, e.g. by checking whether the plugin or script has any
unsaved changes which should be saved or discarded before allowing QGIS to exit.

QgsApplicationExitBlockerInterface are registered via the iface object:

Example
-------

.. code-block:: python

class MyPluginExitBlocker(QgsApplicationExitBlockerInterface):

def allowExit(self):
if self.has_unsaved_changes():
# show a warning prompt
# ...
# prevent QGIS application from exiting
return False

# allow QGIS application to exit
return True

my_blocker = MyPluginExitBlocker()
iface.registerApplicationExitBlocker(my_blocker)

.. versionadded:: 3.16
%End

%TypeHeaderCode
#include "qgsapplicationexitblockerinterface.h"
%End
public:

virtual ~QgsApplicationExitBlockerInterface();

virtual bool allowExit() = 0;
%Docstring
Called whenever the QGIS application has been asked to exit by a user.

The subclass can use this method to implement custom logic handling whether it is safe
for the application to exit, e.g. by checking whether the plugin or script has any unsaved
changes which should be saved or discarded before allowing QGIS to exit.

The implementation should return ``True`` if it is safe for QGIS to exit, or ``False`` if it
wishes to prevent the application from exiting.

.. note::

It is safe to use GUI widgets in implementations of this function, including message
boxes or custom dialogs with event loops.
%End
};

/************************************************************************
* This file has been generated automatically from *
* *
* src/gui/qgsapplicationexitblockerinterface.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
1 change: 1 addition & 0 deletions python/gui/gui_auto.sip
Expand Up @@ -7,6 +7,7 @@
%Include auto_generated/qgsadvanceddigitizingfloater.sip
%Include auto_generated/qgsaggregatetoolbutton.sip
%Include auto_generated/qgsalignmentcombobox.sip
%Include auto_generated/qgsapplicationexitblockerinterface.sip
%Include auto_generated/qgsattributedialog.sip
%Include auto_generated/qgsattributeeditorcontext.sip
%Include auto_generated/qgsattributeform.sip
Expand Down
23 changes: 22 additions & 1 deletion src/app/qgisapp.cpp
Expand Up @@ -181,6 +181,7 @@ Q_GUI_EXPORT extern int qt_defaultDpiX();
#include "qgsauthsslerrorsdialog.h"
#endif
#include "qgsappscreenshots.h"
#include "qgsapplicationexitblockerinterface.h"
#include "qgsbookmarks.h"
#include "qgsbookmarkeditordialog.h"
#include "qgsbrowserdockwidget.h"
Expand Down Expand Up @@ -6388,7 +6389,7 @@ void QgisApp::fileExit()
}

QgsCanvasRefreshBlocker refreshBlocker;
if ( checkUnsavedLayerEdits() && checkMemoryLayers() && saveDirty() )
if ( checkUnsavedLayerEdits() && checkMemoryLayers() && checkExitBlockers() && saveDirty() )
{
closeProject();
userProfileManager()->setDefaultFromActive();
Expand Down Expand Up @@ -12686,6 +12687,16 @@ void QgisApp::unregisterDevToolFactory( QgsDevToolWidgetFactory *factory )
mDevToolFactories.removeAll( factory );
}

void QgisApp::registerApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker )
{
mApplicationExitBlockers << blocker;
}

void QgisApp::unregisterApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker )
{
mApplicationExitBlockers.removeAll( blocker );
}

QgsMapLayer *QgisApp::activeLayer()
{
return mLayerTreeView ? mLayerTreeView->currentLayer() : nullptr;
Expand Down Expand Up @@ -13237,6 +13248,16 @@ bool QgisApp::checkMemoryLayers()
return close;
}

bool QgisApp::checkExitBlockers()
{
for ( QgsApplicationExitBlockerInterface *blocker : qgis::as_const( mApplicationExitBlockers ) )
{
if ( !blocker->allowExit() )
return false;
}
return true;
}

bool QgisApp::checkTasksDependOnProject()
{
QSet< QString > activeTaskDescriptions;
Expand Down
26 changes: 26 additions & 0 deletions src/app/qgisapp.h
Expand Up @@ -111,6 +111,7 @@ class Qgs3DMapCanvasDockWidget;
class QgsHandleBadLayersHandler;
class QgsNetworkAccessManager;
class QgsGpsConnection;
class QgsApplicationExitBlockerInterface;

class QDomDocument;
class QNetworkReply;
Expand Down Expand Up @@ -746,6 +747,23 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow
//! Unregister a previously registered dev tool factory
void unregisterDevToolFactory( QgsDevToolWidgetFactory *factory );

/**
* 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.
*
* \note Ownership of \a blocker is not transferred, and the blocker must
* be unregistered when plugin is unloaded.
*
* \see unregisterApplicationExitBlocker()
*/
void registerApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker );

/**
* Unregister a previously registered application exit \a blocker.
* \see registerApplicationExitBlocker()
*/
void unregisterApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker );

//! Register a new custom drop handler.
void registerCustomDropHandler( QgsCustomDropHandler *handler );

Expand Down Expand Up @@ -2094,6 +2112,12 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow
*/
bool checkMemoryLayers();

/**
* Checks whether any registered application exit blockers should prevent
* the application from exiting.
*/
bool checkExitBlockers();

//! Checks for running tasks dependent on the open project
bool checkTasksDependOnProject();

Expand Down Expand Up @@ -2569,6 +2593,8 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow

QList<QgsDevToolWidgetFactory * > mDevToolFactories;

QList<QgsApplicationExitBlockerInterface * > mApplicationExitBlockers;

QVector<QPointer<QgsCustomDropHandler>> mCustomDropHandlers;
QVector<QPointer<QgsCustomProjectOpenHandler>> mCustomProjectOpenHandlers;
QVector<QPointer<QgsLayoutCustomDropHandler>> mCustomLayoutDropHandlers;
Expand Down
10 changes: 10 additions & 0 deletions src/app/qgisappinterface.cpp
Expand Up @@ -571,6 +571,16 @@ void QgisAppInterface::unregisterDevToolWidgetFactory( QgsDevToolWidgetFactory *
qgis->unregisterDevToolFactory( factory );
}

void QgisAppInterface::registerApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker )
{
qgis->registerApplicationExitBlocker( blocker );
}

void QgisAppInterface::unregisterApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker )
{
qgis->unregisterApplicationExitBlocker( blocker );
}

void QgisAppInterface::registerCustomDropHandler( QgsCustomDropHandler *handler )
{
qgis->registerCustomDropHandler( handler );
Expand Down
2 changes: 2 additions & 0 deletions src/app/qgisappinterface.h
Expand Up @@ -150,6 +150,8 @@ class APP_EXPORT QgisAppInterface : public QgisInterface
void unregisterProjectPropertiesWidgetFactory( QgsOptionsWidgetFactory *factory ) override;
void registerDevToolWidgetFactory( QgsDevToolWidgetFactory *factory ) override;
void unregisterDevToolWidgetFactory( QgsDevToolWidgetFactory *factory ) override;
void registerApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker ) override;
void unregisterApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker ) override;
void registerCustomDropHandler( QgsCustomDropHandler *handler ) override;
void unregisterCustomDropHandler( QgsCustomDropHandler *handler ) override;
void registerCustomProjectOpenHandler( QgsCustomProjectOpenHandler *handler ) override;
Expand Down
1 change: 1 addition & 0 deletions src/gui/CMakeLists.txt
Expand Up @@ -592,6 +592,7 @@ SET(QGIS_GUI_HDRS
qgsadvanceddigitizingfloater.h
qgsaggregatetoolbutton.h
qgsalignmentcombobox.h
qgsapplicationexitblockerinterface.h
qgsattributedialog.h
qgsattributeeditorcontext.h
qgsattributeform.h
Expand Down
20 changes: 20 additions & 0 deletions src/gui/qgisinterface.h
Expand Up @@ -65,6 +65,7 @@ class QgsMeshLayer;
class QgsBrowserGuiModel;
class QgsDevToolWidgetFactory;
class QgsGpsConnection;
class QgsApplicationExitBlockerInterface;


/**
Expand Down Expand Up @@ -1025,6 +1026,25 @@ class GUI_EXPORT QgisInterface : public QObject
*/
virtual void unregisterDevToolWidgetFactory( QgsDevToolWidgetFactory *factory ) = 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.
*
* \note Ownership of \a blocker is not transferred, and the blocker must
* be unregistered when plugin is unloaded.
*
* \see unregisterApplicationExitBlocker()
* \since QGIS 3.16
*/
virtual void registerApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker ) = 0;

/**
* Unregister a previously registered application exit \a blocker.
* \see registerApplicationExitBlocker()
* \since QGIS 3.16
*/
virtual void unregisterApplicationExitBlocker( QgsApplicationExitBlockerInterface *blocker ) = 0;

/**
* Register a new custom drop \a handler.
* \note Ownership of \a handler is not transferred, and the handler must
Expand Down