Merge branch 'waiting-dialog-includes-presentation-shutdown' into 'master'

Display the closing progress dialog during plugin shutdown

See merge request openlp/openlp!554
This commit is contained in:
Raoul Snyman 2023-01-19 17:45:08 +00:00
commit 2d2e84b4ce
3 changed files with 154 additions and 24 deletions

View File

@ -22,6 +22,7 @@
This is the main window, where all the action happens. This is the main window, where all the action happens.
""" """
import shutil import shutil
from contextlib import contextmanager
from datetime import datetime, date from datetime import datetime, date
from pathlib import Path from pathlib import Path
from tempfile import gettempdir from tempfile import gettempdir
@ -529,20 +530,32 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
self.ws_server = WebSocketServer() self.ws_server = WebSocketServer()
self.screen_updating_lock = Lock() self.screen_updating_lock = Lock()
@contextmanager
def _show_wait_dialog(self, title, message):
"""
Show a wait dialog, wait for some tasks to complete, and then close it.
"""
try:
# Display a progress dialog with a message
wait_dialog = QtWidgets.QProgressDialog(message, '', 0, 0, self)
wait_dialog.setWindowTitle(title)
for window_flag in [QtCore.Qt.WindowContextHelpButtonHint]:
wait_dialog.setWindowFlag(window_flag, False)
wait_dialog.setWindowModality(QtCore.Qt.WindowModal)
wait_dialog.setAutoClose(False)
wait_dialog.setCancelButton(None)
wait_dialog.show()
QtWidgets.QApplication.processEvents()
yield
finally:
# Finally close the message window
wait_dialog.close()
def _wait_for_threads(self): def _wait_for_threads(self):
""" """
Wait for the threads Wait for the threads
""" """
# Sometimes the threads haven't finished, let's wait for them # Sometimes the threads haven't finished, let's wait for them
wait_dialog = QtWidgets.QProgressDialog(translate('OpenLP.MainWindow', 'Waiting for some things to finish...'),
'', 0, 0, self)
wait_dialog.setWindowTitle(translate('OpenLP.MainWindow', 'Please Wait'))
for window_flag in [QtCore.Qt.WindowContextHelpButtonHint]:
wait_dialog.setWindowFlag(window_flag, False)
wait_dialog.setWindowModality(QtCore.Qt.WindowModal)
wait_dialog.setAutoClose(False)
wait_dialog.setCancelButton(None)
wait_dialog.show()
thread_names = list(self.application.worker_threads.keys()) thread_names = list(self.application.worker_threads.keys())
for thread_name in thread_names: for thread_name in thread_names:
if thread_name not in self.application.worker_threads.keys(): if thread_name not in self.application.worker_threads.keys():
@ -569,7 +582,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
except RuntimeError: except RuntimeError:
# Ignore the RuntimeError that is thrown when Qt has already deleted the C++ thread object # Ignore the RuntimeError that is thrown when Qt has already deleted the C++ thread object
pass pass
wait_dialog.close()
def bootstrap_post_set_up(self): def bootstrap_post_set_up(self):
""" """
@ -1085,10 +1097,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
else: else:
event.accept() event.accept()
if event.isAccepted(): if event.isAccepted():
# Wait for all the threads to complete with self._show_wait_dialog(translate('OpenLP.MainWindow', 'Please Wait'),
self._wait_for_threads() translate('OpenLP.MainWindow', 'Waiting for some things to finish...')):
# If we just did a settings import, close without saving changes. # Wait for all the threads to complete
self.clean_up(save_settings=not self.settings_imported) self._wait_for_threads()
# If we just did a settings import, close without saving changes.
self.clean_up(save_settings=not self.settings_imported)
def eventFilter(self, obj, event): def eventFilter(self, obj, event):
if event.type() == QtCore.QEvent.FileOpen: if event.type() == QtCore.QEvent.FileOpen:

View File

@ -78,14 +78,14 @@ def settings(qapp, registry):
# Needed on windows to make sure a Settings object is available during the tests # Needed on windows to make sure a Settings object is available during the tests
sets = Settings() sets = Settings()
sets.setValue('themes/global theme', 'my_theme') sets.setValue('themes/global theme', 'my_theme')
Registry().register('settings', sets) registry.register('settings', sets)
Registry().register('settings_thread', sets) registry.register('settings_thread', sets)
Registry().register('application', qapp) registry.register('application', qapp)
qapp.settings = sets qapp.settings = sets
yield sets yield sets
del sets del sets
Registry().remove('settings') registry.remove('settings')
Registry().remove('settings_thread') registry.remove('settings_thread')
os.close(fd) os.close(fd)
os.unlink(Settings().fileName()) os.unlink(Settings().fileName())
@ -95,12 +95,12 @@ def mock_settings(qapp, registry):
"""A Mock Settings() instance""" """A Mock Settings() instance"""
# Create and register a mock settings object to work with # Create and register a mock settings object to work with
mk_settings = MagicMock() mk_settings = MagicMock()
Registry().register('settings', mk_settings) registry.register('settings', mk_settings)
Registry().register('application', qapp) registry.register('application', qapp)
Registry().register('settings_thread', mk_settings) registry.register('settings_thread', mk_settings)
yield mk_settings yield mk_settings
Registry().remove('settings') registry.remove('settings')
Registry().remove('settings_thread') registry.remove('settings_thread')
del mk_settings del mk_settings

View File

@ -831,3 +831,119 @@ def test_update_recent_files_menu(mocked_create_action, mocked_add_actions, Mock
# THEN: There should be no errors # THEN: There should be no errors
assert mocked_create_action.call_count == 2 assert mocked_create_action.call_count == 2
@patch('openlp.core.ui.mainwindow.QtWidgets.QProgressDialog')
def test_show_wait_dialog(MockProcessDialog, main_window_reduced):
"""Test that the show wait dialog context manager works correctly"""
# GIVEN: A mocked out QProgressDialog and a minimal main window
mocked_wait_dialog = MagicMock()
MockProcessDialog.return_value = mocked_wait_dialog
# WHEN: Calling _show_wait_dialog()
with main_window_reduced._show_wait_dialog('Test', 'This is a test'):
pass
# THEN: The correct methods should have been called
MockProcessDialog.assert_called_once_with('This is a test', '', 0, 0, main_window_reduced)
mocked_wait_dialog.setWindowTitle.assert_called_once_with('Test')
mocked_wait_dialog.setWindowFlag.assert_called_once_with(QtCore.Qt.WindowContextHelpButtonHint, False)
mocked_wait_dialog.setWindowModality.assert_called_once_with(QtCore.Qt.WindowModal)
mocked_wait_dialog.setAutoClose.assert_called_once_with(False)
mocked_wait_dialog.setCancelButton.assert_called_once_with(None)
mocked_wait_dialog.show.assert_called_once_with()
mocked_wait_dialog.close.assert_called_once_with()
@patch('openlp.core.ui.mainwindow.QtWidgets.QApplication')
def test_wait_for_threads(MockApp, main_window_reduced):
"""Test that the wait_for_threads() method correctly stops the threads"""
# GIVEN: A mocked application, and a reduced main window
mocked_http_thread = MagicMock()
mocked_http_thread.isRunning.side_effect = [True, True, False, False]
mocked_http_worker = MagicMock()
mocked_http_worker.stop = MagicMock()
main_window_reduced.application.worker_threads = {
'http': {'thread': mocked_http_thread, 'worker': mocked_http_worker}
}
# WHEN: _wait_for_threads() is called
main_window_reduced._wait_for_threads()
# THEN: The correct methods should have been called
assert MockApp.processEvents.call_count == 2, 'processEvents() should have been called twice'
mocked_http_worker.stop.assert_called_once()
assert mocked_http_thread.isRunning.call_count == 4, 'isRunning() should have been called 4 times'
mocked_http_thread.wait.assert_called_once_with(100)
@patch('openlp.core.ui.mainwindow.QtWidgets.QApplication')
def test_wait_for_threads_no_threads(MockApp, main_window_reduced):
"""Test that the wait_for_threads() method exits early when there are no threads"""
# GIVEN: A mocked application, and a reduced main window
main_window_reduced.application.worker_threads = {}
# WHEN: _wait_for_threads() is called
main_window_reduced._wait_for_threads()
# THEN: The correct methods should have been called
assert MockApp.processEvents.call_count == 0, 'processEvents() should not have been called'
@patch('openlp.core.ui.mainwindow.QtWidgets.QApplication')
def test_wait_for_threads_disappearing_thread(MockApp, main_window_reduced):
"""Test that the wait_for_threads() method correctly ignores threads that resolve themselves"""
# GIVEN: A mocked application, and a reduced main window
main_window_reduced.application.worker_threads = MagicMock(**{'keys.side_effect': [['http'], []]})
# WHEN: _wait_for_threads() is called
main_window_reduced._wait_for_threads()
# THEN: The correct methods should have been called
assert MockApp.processEvents.call_count == 0, 'processEvents() should not have been called'
@patch('openlp.core.ui.mainwindow.QtWidgets.QApplication')
def test_wait_for_threads_stuck_thread(MockApp, main_window_reduced):
"""Test that the wait_for_threads() method correctly stops the threads"""
# GIVEN: A mocked application, and a reduced main window
mocked_http_thread = MagicMock()
mocked_http_thread.isRunning.return_value = True
mocked_http_worker = MagicMock()
mocked_http_worker.stop = MagicMock()
main_window_reduced.application.worker_threads = {
'http': {'thread': mocked_http_thread, 'worker': mocked_http_worker}
}
# WHEN: _wait_for_threads() is called
main_window_reduced._wait_for_threads()
# THEN: The correct methods should have been called
assert MockApp.processEvents.call_count == 51, 'processEvents() should have been called 51 times'
mocked_http_worker.stop.assert_called_once()
assert mocked_http_thread.isRunning.call_count == 53, 'isRunning() should have been called 53 times'
mocked_http_thread.wait.assert_called_with(100)
mocked_http_thread.terminate.assert_called_once()
@patch('openlp.core.ui.mainwindow.QtWidgets.QApplication')
def test_wait_for_threads_runtime_error(MockApp, main_window_reduced):
"""Test that the wait_for_threads() method handles a runtime error"""
# GIVEN: A mocked application, and a reduced main window
mocked_http_thread = MagicMock()
mocked_http_thread.isRunning.side_effect = RuntimeError
mocked_http_worker = MagicMock()
mocked_http_worker.stop = MagicMock()
main_window_reduced.application.worker_threads = {
'http': {'thread': mocked_http_thread, 'worker': mocked_http_worker}
}
# WHEN: _wait_for_threads() is called
main_window_reduced._wait_for_threads()
# THEN: The correct methods should have been called
assert MockApp.processEvents.call_count == 1, 'processEvents() should have been called once'
mocked_http_worker.stop.assert_called_once()
assert mocked_http_thread.isRunning.call_count == 1, 'isRunning() should have been called once'
mocked_http_thread.wait.assert_not_called()
mocked_http_thread.terminate.assert_not_called()