main.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 
00003 # Copyright (c) 2011, Dirk Thomas, Dorian Scholz, TU Darmstadt
00004 # All rights reserved.
00005 #
00006 # Redistribution and use in source and binary forms, with or without
00007 # modification, are permitted provided that the following conditions
00008 # are met:
00009 #
00010 #   * Redistributions of source code must retain the above copyright
00011 #     notice, this list of conditions and the following disclaimer.
00012 #   * Redistributions in binary form must reproduce the above
00013 #     copyright notice, this list of conditions and the following
00014 #     disclaimer in the documentation and/or other materials provided
00015 #     with the distribution.
00016 #   * Neither the name of the TU Darmstadt nor the names of its
00017 #     contributors may be used to endorse or promote products derived
00018 #     from this software without specific prior written permission.
00019 #
00020 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
00021 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
00022 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
00023 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
00024 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
00025 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
00026 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
00027 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
00028 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
00029 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
00030 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00031 # POSSIBILITY OF SUCH DAMAGE.
00032 
00033 from __future__ import print_function
00034 
00035 import os
00036 import signal
00037 import sys
00038 from argparse import ArgumentParser, SUPPRESS
00039 
00040 
00041 class Main(object):
00042 
00043     main_filename = None
00044 
00045     def __init__(self, invoked_filename=None, settings_filename=None):
00046         if invoked_filename is None:
00047             invoked_filename = os.path.abspath(__file__)
00048         Main.main_filename = invoked_filename
00049         if settings_filename is None:
00050             settings_filename = 'qt_gui'
00051         self._settings_filename = settings_filename
00052         self.plugin_providers = []
00053         self._dbus_available = False
00054         self._options = None
00055 
00056     def _add_arguments(self, parser):
00057         parser.add_argument('-b', '--qt-binding', dest='qt_binding', type=str, metavar='BINDING',
00058                           help='choose Qt bindings to be used [pyqt|pyside]')
00059         parser.add_argument('--clear-config', dest='clear_config', default=False, action='store_true',
00060                           help='clear the configuration (including all perspectives and plugin settings)')
00061         parser.add_argument('-l', '--lock-perspective', dest='lock_perspective', action='store_true',
00062                           help='lock the GUI to the used perspective (hide menu bar and close buttons of plugins)')
00063         parser.add_argument('-m', '--multi-process', dest='multi_process', default=False, action='store_true',
00064                           help='use separate processes for each plugin instance (currently only supported under X11)')
00065         parser.add_argument('-p', '--perspective', dest='perspective', type=str, metavar='PERSPECTIVE',
00066                           help='start with this perspective')
00067         parser.add_argument('--reload-import', dest='reload_import', default=False, action='store_true',
00068                           help='reload every imported module')
00069         parser.add_argument('-s', '--standalone', dest='standalone_plugin', type=str, metavar='PLUGIN',
00070                           help='start only this plugin (implies -l). To pass arguments to the plugin use --args')
00071         parser.add_argument('-v', '--verbose', dest='verbose', default=False, action='store_true',
00072                           help='output qDebug messages')
00073 
00074         parser.add_argument('--args', dest='args', nargs='*', type=str,
00075                           help='arbitrary arguments which are passes to the plugin (only with -s, --command-start-plugin or --embed-plugin). It must be the last option since it collects all following options.')
00076 
00077         group = parser.add_argument_group('Options to query information without starting a GUI instance',
00078                             'These options can be used to query information about valid arguments for various options.')
00079         group.add_argument('--list-perspectives', dest='list_perspectives', action='store_true',
00080                          help='list available perspectives')
00081         group.add_argument('--list-plugins', dest='list_plugins', action='store_true',
00082                          help='list available plugins')
00083         parser.add_argument_group(group)
00084 
00085         group = parser.add_argument_group('Options to operate on a running GUI instance',
00086                             'These options can be used to perform actions on a running GUI instance.')
00087         group.add_argument('--command-pid', dest='command_pid', type=int, metavar='PID',
00088                          help='pid of the GUI instance to operate on, defaults to oldest running GUI instance')
00089         group.add_argument('--command-start-plugin', dest='command_start_plugin', type=str, metavar='PLUGIN',
00090                          help='start plugin')
00091         group.add_argument('--command-switch-perspective', dest='command_switch_perspective', type=str, metavar='PERSPECTIVE',
00092                          help='switch perspective')
00093         if not self._dbus_available:
00094             group.description = 'These options are not available since the DBus module is not found!'
00095             for o in group._group_actions:
00096                 o.help = SUPPRESS
00097         parser.add_argument_group(group)
00098 
00099         group = parser.add_argument_group('Special options for embedding widgets from separate processes',
00100                             'These options should never be used on the CLI but only from the GUI code itself.')
00101         group.add_argument('--embed-plugin', dest='embed_plugin', type=str, metavar='PLUGIN',
00102                          help='embed a plugin into an already running GUI instance (requires all other --embed-* options)')
00103         group.add_argument('--embed-plugin-serial', dest='embed_plugin_serial', type=int, metavar='SERIAL',
00104                          help='serial number of plugin to be embedded (requires all other --embed-* options)')
00105         group.add_argument('--embed-plugin-address', dest='embed_plugin_address', type=str, metavar='ADDRESS',
00106                          help='dbus server address of the GUI instance to embed plugin into (requires all other --embed-* options)')
00107         for o in group._group_actions:
00108             o.help = SUPPRESS
00109         parser.add_argument_group(group)
00110 
00111     def _add_plugin_providers(self):
00112         pass
00113 
00114     def _caching_hook(self):
00115         pass
00116 
00117     def _add_reload_paths(self, reload_importer):
00118         reload_importer.add_reload_path(os.path.join(os.path.dirname(__file__), *('..',) * 4))
00119 
00120     def __check_icon_theme_compliance(self):
00121         from python_qt_binding.QtGui import QIcon
00122         # TODO find a better way to verify Theme standard compliance
00123         if QIcon.themeName() == '' or \
00124            QIcon.fromTheme('document-save').isNull() or \
00125            QIcon.fromTheme('document-open').isNull() or \
00126            QIcon.fromTheme('edit-cut').isNull() or \
00127            QIcon.fromTheme('object-flip-horizontal').isNull():
00128             original_theme = QIcon.themeName()
00129             QIcon.setThemeName('Tango')
00130             if QIcon.fromTheme('document-save').isNull():
00131                 QIcon.setThemeName(original_theme)
00132 
00133     def main(self, argv=None, standalone=None):
00134         # check if DBus is available
00135         try:
00136             import dbus
00137             del dbus
00138             self._dbus_available = True
00139         except ImportError:
00140             pass
00141 
00142         if argv is None:
00143             argv = sys.argv
00144 
00145         # extract --args and everything behind manually since argparse can not handle that
00146         arguments = argv[1:]
00147         args = []
00148         if '--args' in arguments:
00149             index = arguments.index('--args')
00150             args = arguments[index + 1:]
00151             arguments = arguments[0:index + 1]
00152 
00153         if standalone:
00154             arguments += ['-s', standalone]
00155 
00156         parser = ArgumentParser('usage: %prog [options]')
00157         self._add_arguments(parser)
00158         self._options = parser.parse_args(arguments)
00159         self._options.args = args
00160 
00161         # check option dependencies
00162         try:
00163             if self._options.args and not self._options.standalone_plugin and not self._options.command_start_plugin and not self._options.embed_plugin:
00164                 raise RuntimeError('Option --args can only be used together with either --standalone, --command-start-plugin or --embed-plugin option')
00165 
00166             list_options = (self._options.list_perspectives, self._options.list_plugins)
00167             list_options_set = [opt for opt in list_options if opt is not False]
00168             if len(list_options_set) > 1:
00169                 raise RuntimeError('Only one --list-* option can be used at a time')
00170 
00171             command_options = (self._options.command_start_plugin, self._options.command_switch_perspective)
00172             command_options_set = [opt for opt in command_options if opt is not None]
00173             if len(command_options_set) > 0 and not self._dbus_available:
00174                 raise RuntimeError('Without DBus support the --command-* options are not available')
00175             if len(command_options_set) > 1:
00176                 raise RuntimeError('Only one --command-* option can be used at a time (except --command-pid which is optional)')
00177             if len(command_options_set) == 0 and self._options.command_pid is not None:
00178                 raise RuntimeError('Option --command_pid can only be used together with an other --command-* option')
00179 
00180             embed_options = (self._options.embed_plugin, self._options.embed_plugin_serial, self._options.embed_plugin_address)
00181             embed_options_set = [opt for opt in embed_options if opt is not None]
00182             if len(command_options_set) > 0 and not self._dbus_available:
00183                 raise RuntimeError('Without DBus support the --embed-* options are not available')
00184             if len(embed_options_set) > 0 and len(embed_options_set) < len(embed_options):
00185                 raise RuntimeError('Missing option(s) - all \'--embed-*\' options must be set')
00186 
00187             if len(embed_options_set) > 0 and self._options.clear_config:
00188                 raise RuntimeError('Option --clear-config can only be used without any --embed-* option')
00189 
00190             groups = (list_options_set, command_options_set, embed_options_set)
00191             groups_set = [opt for opt in groups if len(opt) > 0]
00192             if len(groups_set) > 1:
00193                 raise RuntimeError('Options from different groups (--list, --command, --embed) can not be used together')
00194 
00195         except RuntimeError as e:
00196             print(str(e))
00197             #parser.parse_args(['--help'])
00198             # calling --help will exit
00199             return 1
00200 
00201         # set implicit option dependencies
00202         if self._options.standalone_plugin is not None:
00203             self._options.lock_perspective = True
00204 
00205         # use qt/glib mainloop integration to get dbus mainloop working
00206         if self._dbus_available:
00207             from dbus.mainloop.glib import DBusGMainLoop
00208             from dbus import DBusException, Interface, SessionBus
00209             DBusGMainLoop(set_as_default=True)
00210 
00211         # create application context containing various relevant information
00212         from .application_context import ApplicationContext
00213         context = ApplicationContext()
00214         context.options = self._options
00215 
00216         # non-special applications provide various dbus interfaces
00217         if self._dbus_available:
00218             context.provide_app_dbus_interfaces = len(groups_set) == 0
00219             context.dbus_base_bus_name = 'org.ros.qt_gui'
00220             if context.provide_app_dbus_interfaces:
00221                 context.dbus_unique_bus_name = context.dbus_base_bus_name + '.pid%d' % os.getpid()
00222 
00223                 # provide pid of application via dbus
00224                 from .application_dbus_interface import ApplicationDBusInterface
00225                 _dbus_server = ApplicationDBusInterface(context.dbus_base_bus_name)
00226 
00227         # determine host bus name, either based on pid given on command line or via dbus application interface if any other instance is available
00228         if len(command_options_set) > 0 or len(embed_options_set) > 0:
00229             host_pid = None
00230             if self._options.command_pid is not None:
00231                 host_pid = self._options.command_pid
00232             else:
00233                 try:
00234                     remote_object = SessionBus().get_object(context.dbus_base_bus_name, '/Application')
00235                 except DBusException:
00236                     pass
00237                 else:
00238                     remote_interface = Interface(remote_object, context.dbus_base_bus_name + '.Application')
00239                     host_pid = remote_interface.get_pid()
00240             if host_pid is not None:
00241                 context.dbus_host_bus_name = context.dbus_base_bus_name + '.pid%d' % host_pid
00242 
00243         # execute command on host application instance
00244         if len(command_options_set) > 0:
00245             if self._options.command_start_plugin is not None:
00246                 try:
00247                     remote_object = SessionBus().get_object(context.dbus_host_bus_name, '/PluginManager')
00248                 except DBusException:
00249                     (rc, msg) = (1, 'unable to communicate with GUI instance "%s"' % context.dbus_host_bus_name)
00250                 else:
00251                     remote_interface = Interface(remote_object, context.dbus_base_bus_name + '.PluginManager')
00252                     (rc, msg) = remote_interface.start_plugin(self._options.command_start_plugin, ' '.join(self._options.args))
00253                 if rc == 0:
00254                     print('qt_gui_main() started plugin "%s" in GUI "%s"' % (msg, context.dbus_host_bus_name))
00255                 else:
00256                     print('qt_gui_main() could not start plugin "%s" in GUI "%s": %s' % (self._options.command_start_plugin, context.dbus_host_bus_name, msg))
00257                 return rc
00258             elif self._options.command_switch_perspective is not None:
00259                 remote_object = SessionBus().get_object(context.dbus_host_bus_name, '/PerspectiveManager')
00260                 remote_interface = Interface(remote_object, context.dbus_base_bus_name + '.PerspectiveManager')
00261                 remote_interface.switch_perspective(self._options.command_switch_perspective)
00262                 print('qt_gui_main() switched to perspective "%s" in GUI "%s"' % (self._options.command_switch_perspective, context.dbus_host_bus_name))
00263                 return 0
00264             raise RuntimeError('Unknown command not handled')
00265 
00266         # choose selected or default qt binding
00267         setattr(sys, 'SELECT_QT_BINDING', self._options.qt_binding)
00268         from python_qt_binding import QT_BINDING
00269 
00270         from python_qt_binding.QtCore import qDebug, qInstallMsgHandler, QSettings, Qt, QtCriticalMsg, QtDebugMsg, QtFatalMsg, QTimer, QtWarningMsg
00271         from python_qt_binding.QtGui import QAction, QApplication, QIcon, QMenuBar
00272 
00273         from .about_handler import AboutHandler
00274         from .composite_plugin_provider import CompositePluginProvider
00275         from .help_provider import HelpProvider
00276         from .main_window import MainWindow
00277         from .perspective_manager import PerspectiveManager
00278         from .plugin_manager import PluginManager
00279 
00280         def message_handler(type_, msg):
00281             colored_output = 'TERM' in os.environ and 'ANSI_COLORS_DISABLED' not in os.environ
00282             cyan_color = '\033[36m' if colored_output else ''
00283             red_color = '\033[31m' if colored_output else ''
00284             reset_color = '\033[0m' if colored_output else ''
00285             if type_ == QtDebugMsg and self._options.verbose:
00286                 print(msg, file=sys.stderr)
00287             elif type_ == QtWarningMsg:
00288                 print(cyan_color + msg + reset_color, file=sys.stderr)
00289             elif type_ == QtCriticalMsg:
00290                 print(red_color + msg + reset_color, file=sys.stderr)
00291             elif type_ == QtFatalMsg:
00292                 print(red_color + msg + reset_color, file=sys.stderr)
00293                 sys.exit(1)
00294         qInstallMsgHandler(message_handler)
00295 
00296         app = QApplication(argv)
00297         app.setAttribute(Qt.AA_DontShowIconsInMenus, False)
00298 
00299         self.__check_icon_theme_compliance()
00300 
00301         if len(embed_options_set) == 0:
00302             settings = QSettings(QSettings.IniFormat, QSettings.UserScope, 'ros.org', self._settings_filename)
00303             if self._options.clear_config:
00304                 settings.clear()
00305 
00306             main_window = MainWindow()
00307             main_window.setDockNestingEnabled(True)
00308             main_window.statusBar()
00309 
00310             def sigint_handler(*args):
00311                 qDebug('\nsigint_handler()')
00312                 main_window.close()
00313             signal.signal(signal.SIGINT, sigint_handler)
00314             # the timer enables triggering the sigint_handler
00315             timer = QTimer()
00316             timer.start(500)
00317             timer.timeout.connect(lambda: None)
00318 
00319             # create own menu bar to share one menu bar on Mac
00320             menu_bar = QMenuBar()
00321             menu_bar.setNativeMenuBar(False)
00322             if not self._options.lock_perspective:
00323                 main_window.setMenuBar(menu_bar)
00324 
00325             file_menu = menu_bar.addMenu(menu_bar.tr('File'))
00326             action = QAction(file_menu.tr('Quit'), file_menu)
00327             action.setIcon(QIcon.fromTheme('application-exit'))
00328             action.triggered.connect(main_window.close)
00329             file_menu.addAction(action)
00330 
00331         else:
00332             app.setQuitOnLastWindowClosed(False)
00333 
00334             settings = None
00335             main_window = None
00336             menu_bar = None
00337 
00338         self._add_plugin_providers()
00339 
00340         # setup plugin manager
00341         plugin_provider = CompositePluginProvider(self.plugin_providers)
00342         plugin_manager = PluginManager(plugin_provider, context)
00343 
00344         if self._options.list_plugins:
00345             # output available plugins
00346             print('\n'.join(sorted(plugin_manager.get_plugins().values())))
00347             return 0
00348 
00349         help_provider = HelpProvider()
00350         plugin_manager.plugin_help_signal.connect(help_provider.plugin_help_request)
00351 
00352         # setup perspective manager
00353         if settings is not None:
00354             perspective_manager = PerspectiveManager(settings, context)
00355 
00356             if self._options.list_perspectives:
00357                 # output available perspectives
00358                 print('\n'.join(sorted(perspective_manager.perspectives)))
00359                 return 0
00360         else:
00361             perspective_manager = None
00362 
00363         if main_window is not None:
00364             plugin_manager.set_main_window(main_window, menu_bar)
00365 
00366         if settings is not None and menu_bar is not None:
00367             perspective_menu = menu_bar.addMenu(menu_bar.tr('Perspectives'))
00368             perspective_manager.set_menu(perspective_menu)
00369 
00370         # connect various signals and slots
00371         if perspective_manager is not None and main_window is not None:
00372             # signal changed perspective to update window title
00373             perspective_manager.perspective_changed_signal.connect(main_window.perspective_changed)
00374             # signal new settings due to changed perspective
00375             perspective_manager.save_settings_signal.connect(main_window.save_settings)
00376             perspective_manager.restore_settings_signal.connect(main_window.restore_settings)
00377             perspective_manager.restore_settings_without_plugin_changes_signal.connect(main_window.restore_settings)
00378 
00379         if perspective_manager is not None and plugin_manager is not None:
00380             perspective_manager.save_settings_signal.connect(plugin_manager.save_settings)
00381             plugin_manager.save_settings_completed_signal.connect(perspective_manager.save_settings_completed)
00382             perspective_manager.restore_settings_signal.connect(plugin_manager.restore_settings)
00383             perspective_manager.restore_settings_without_plugin_changes_signal.connect(plugin_manager.restore_settings_without_plugins)
00384 
00385         if plugin_manager is not None and main_window is not None:
00386             # signal before changing plugins to save window state
00387             plugin_manager.plugins_about_to_change_signal.connect(main_window.save_setup)
00388             # signal changed plugins to restore window state
00389             plugin_manager.plugins_changed_signal.connect(main_window.restore_state)
00390             # signal save settings to store plugin setup on close
00391             main_window.save_settings_before_close_signal.connect(plugin_manager.close_application)
00392             # signal save and shutdown called for all plugins, trigger closing main window again
00393             plugin_manager.close_application_signal.connect(main_window.close, type=Qt.QueuedConnection)
00394 
00395         if main_window is not None and menu_bar is not None:
00396             about_handler = AboutHandler(main_window)
00397             help_menu = menu_bar.addMenu(menu_bar.tr('Help'))
00398             action = QAction(file_menu.tr('About'), help_menu)
00399             action.setIcon(QIcon.fromTheme('help-about'))
00400             action.triggered.connect(about_handler.show)
00401             help_menu.addAction(action)
00402 
00403         # set initial size - only used without saved configuration
00404         if main_window is not None:
00405             main_window.resize(600, 450)
00406             main_window.move(100, 100)
00407 
00408         # ensure that qt_gui/src is in sys.path
00409         src_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..'))
00410         if src_path not in sys.path:
00411             sys.path.append(src_path)
00412 
00413         # load specific plugin
00414         plugin = None
00415         plugin_serial = None
00416         if self._options.embed_plugin is not None:
00417             plugin = self._options.embed_plugin
00418             plugin_serial = self._options.embed_plugin_serial
00419         elif self._options.standalone_plugin is not None:
00420             plugin = self._options.standalone_plugin
00421             plugin_serial = 0
00422         if plugin is not None:
00423             plugins = plugin_manager.find_plugins_by_name(plugin)
00424             if len(plugins) == 0:
00425                 print('qt_gui_main() found no plugin matching "%s"' % plugin)
00426                 return 1
00427             elif len(plugins) > 1:
00428                 print('qt_gui_main() found multiple plugins matching "%s"\n%s' % (plugin, '\n'.join(plugins.values())))
00429                 return 1
00430             plugin = plugins.keys()[0]
00431 
00432         qDebug('QtBindingHelper using %s' % QT_BINDING)
00433 
00434         plugin_manager.discover()
00435 
00436         self._caching_hook()
00437 
00438         if self._options.reload_import:
00439             qDebug('ReloadImporter() automatically reload all subsequent imports')
00440             from .reload_importer import ReloadImporter
00441             _reload_importer = ReloadImporter()
00442             self._add_reload_paths(_reload_importer)
00443             _reload_importer.enable()
00444 
00445         # switch perspective
00446         if perspective_manager is not None:
00447             if not plugin:
00448                 perspective_manager.set_perspective(self._options.perspective)
00449             else:
00450                 perspective_manager.set_perspective(plugin, hide_and_without_plugin_changes=True)
00451 
00452         # load specific plugin
00453         if plugin:
00454             plugin_manager.load_plugin(plugin, plugin_serial, self._options.args)
00455 
00456         if main_window is not None:
00457             main_window.show()
00458 
00459         return app.exec_()
00460 
00461 
00462 if __name__ == '__main__':
00463     main = Main()
00464     sys.exit(main.main())


qt_gui
Author(s): Dirk Thomas
autogenerated on Fri Jan 3 2014 11:44:00