Source code for schrodinger.utils.qapplication

"""
Functions for managing the global QApplication instance.

Typical usage:

if __name__ == '__main__':
    af2.start_application(MyPanel.panel)

(Note that the start_application function is available from the af2 namespace)

"""

import enum
import sys
import traceback

from schrodinger.infra import qt_message_handler
from schrodinger.Qt import QtCore
from schrodinger.Qt import IS_PYQT6
from schrodinger.utils.sysinfo import is_display_present

_APP_MODE = None
_APPLICATION = None

ApplicationMode = enum.Enum('RunMode', ['Standalone', 'Maestro', 'Canvas'])


[docs]class CantCreateQApplicationError(RuntimeError):
[docs] def __init__(self, msg="", *args, **kwargs): msg = "Could not create a QtWidgets application. " + msg super().__init__(msg, *args, **kwargs)
[docs]def require_application(func=None, create=False, use_qtcore_app=False): """ Use this decorator on functions that require a QApplication to run. When the decorated function is called, this will check whether a QApplication exists. If it does not, a RuntimeError will be raised unless create=True, which will cause a QApplication to be created. :param func: the function to decorate :param create: whether to create a QApplication if one does not exist :type create: bool :param use_qtcore_app: Whether to create the application using the QtCore module instead of the QtWidgets module. :type use_qtcore_app: bool """ if func is None: # func is None if decorator is called to specify options, i.e. # @require_application(create=True) # def foo(self): # pass return lambda func: require_application( func, create=create, use_qtcore_app=use_qtcore_app) def modified_func(*args, **kwargs): app = get_application(create=create, use_qtcore_app=use_qtcore_app) if app is None: raise RuntimeError(f'{func} requires a QApplication.') return func(*args, **kwargs) return modified_func
[docs]def get_application(create=True, use_qtcore_app=False): """ Gets the global QApplication instance. By default, creates one if none exists. :param create: Whether to create a new application if none exists. :type create: bool :param use_qtcore_app: Whether to create the application using the QtCore module instead of the QtWidgets module. :raises CantCreateQApplicationError: if use_qtcore_app is False but a QCoreApplication already exists. In the opposite case (use_qtcore_app is True but a QApplication already exists), no traceback is thrown (because QApplication is a substitutable subclass of QCoreApplication) :raises CantCreateQApplicationError: if use_qtcore_app is False and importing QtWidgets fails (e.g. due to missing graphics libraries) :return: the application :rtype: QApplication or None """ global _APPLICATION if _APPLICATION is None: _APPLICATION = QtCore.QCoreApplication.instance() if not _APPLICATION and create: # Ignore any command-line arguments since we don't expect that # calling code really intends to customize QApplication program_name = sys.argv[0] sysargs = [program_name] if use_qtcore_app: _APPLICATION = QtCore.QCoreApplication(sysargs) else: # Import here so we don't introduce a ui dependency try: from schrodinger.Qt import QtWidgets from schrodinger.ui.qt import style as qtstyle except ImportError: raise CantCreateQApplicationError( "Could not import QtWidgets.") if not is_display_present(): # If there's no display, use the offscreen QPA platform sysargs += ['-platform', 'offscreen'] _APPLICATION = QtWidgets.QApplication(sysargs) if not IS_PYQT6: # Windows: Hide the '?' button on QDialog # AA_DisableWindowContextHelpButton has been removed from # Qt 6 as it is now the default behavior _APPLICATION.setAttribute( QtCore.Qt.AA_DisableWindowContextHelpButton) qtstyle.apply_styles() qt_message_handler.install_handler() if use_qtcore_app is False and is_core_application(): raise CantCreateQApplicationError( "A QtCore application already exists in this process. A QtWidgets " "application must be created earlier in the code.") return _APPLICATION
[docs]def is_core_application(): """ Return whether the application instance is a QtCore application (as opposed to a QtWidgets application). :return: Whether there is a QtCore application or None if there's no application :rtype: bool or NoneType """ app = QtCore.QCoreApplication.instance() if app is None: return None return not app.inherits('QApplication')
[docs]def start_application(main_callable, use_qtcore_app=False): """ Begins the application's event loop using the ``exec`` method. The main callable is called via timer from within the event loop. This function is meant to be used when running in standalone scripts as follows: if __name__ == '__main__': application.start_application(main) Using this function to launch standalone scripts/panels more closely mimics the way Python is run under Maestro. For example, a panel may be presented with MyPanel.show() without having to call application.exec(). This is generally intended for use with GUI scripts. :param main_callable: the function/method to be run in the event loop, commonly a module level main function or MyPanelClass.panel :type main_callable: callable :param use_qtcore_app: Whether to create the application using the QtCore module instead of the QtWidgets module. Set to True for scripts that are non-GUI and/or need to run on machines that have no display. :type use_qtcore_app: bool """ application = get_application(use_qtcore_app=use_qtcore_app) QtCore.QTimer.singleShot(0, lambda: _main_wrapper(main_callable)) application.exec()
[docs]def run_application(main_callable, use_qtcore_app=True): """ This function is the same as start_application with two important differences: 1) Creates a QCoreApplication by default 2) Quits the application as soon as the main_callable function returns and does a system exit with that return value This is generally intended for use by non-GUI, commandline scripts. """ application = get_application(use_qtcore_app=use_qtcore_app) QtCore.QTimer.singleShot( 0, lambda: _main_wrapper(main_callable, quit_on_return=True)) application.exec()
def _main_wrapper(main_callable, quit_on_return=False): """ Wraps the main callable in an exception handler, so that unhandled exceptions properly quit the application and display the traceback. Optionally allows the application to automatically quit when the main callable returns. :param quit_on_return: whether to quit the application when the main_callable returns (and system exit with the return value) :type quit_on_return: bool """ # break an import cycle on cmdline from schrodinger.utils import cmdline try: rc = cmdline.main_wrapper(main_callable, exit_on_return=False) except: # Unhandled exceptions in main should quit the QApplication quit_application() raise if quit_on_return: quit_application() sys.exit(rc) def _truncate_traceback(tb, match_txt): """ Remove the top layers of a traceback for all layers that have `match_txt` in the function name. """ tuple_tb = traceback.extract_tb(tb) for entry in tuple_tb: filename, lineno, func_name, text = entry if match_txt in func_name: tb = tb.tb_next else: break return tb
[docs]def process_events(flags=None): """ Calls processEvents on the main event loop. Requires an application. :param flags: passed to processEvents. See QCoreApplication.processEvents for documentation :raises RuntimeError: if there is no application """ app = QtCore.QCoreApplication.instance() if app is None: raise RuntimeError('Must construct an application to process events.') if flags is None: app.processEvents() else: app.processEvents(flags)
[docs]def quit_application(): """ Quits the application. """ # This function deliberately does not use `get_application` or # `process_events` to keep it as vanilla as possible application = QtCore.QCoreApplication.instance() if application is not None: application.processEvents() application.exit()