00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033 from __future__ import print_function
00034
00035 from argparse import ArgumentParser, SUPPRESS
00036 import os
00037 import platform
00038 import signal
00039 import sys
00040
00041
00042 class Main(object):
00043
00044 main_filename = None
00045
00046 def __init__(self, qtgui_path, invoked_filename=None, settings_filename=None):
00047 self._qtgui_path = qtgui_path
00048 if invoked_filename is None:
00049 invoked_filename = os.path.abspath(__file__)
00050 Main.main_filename = invoked_filename
00051 if settings_filename is None:
00052 settings_filename = 'qt_gui'
00053 self._settings_filename = settings_filename
00054
00055 self.plugin_providers = []
00056 self._options = None
00057
00058
00059 self._dbus_available = False
00060 try:
00061
00062 from dbus.mainloop.glib import DBusGMainLoop
00063 DBusGMainLoop(set_as_default=True)
00064 import dbus
00065 try:
00066
00067 dbus.SessionBus()
00068 self._dbus_available = True
00069 except dbus.exceptions.DBusException:
00070 pass
00071 except ImportError:
00072 pass
00073
00074 def add_arguments(self, parser, standalone=False, plugin_argument_provider=None):
00075 common_group = parser.add_argument_group('Options for GUI instance')
00076 common_group.add_argument('-b', '--qt-binding', dest='qt_binding', type=str, metavar='BINDING',
00077 help='choose Qt bindings to be used [pyqt|pyside]')
00078 common_group.add_argument('--clear-config', dest='clear_config', default=False, action='store_true',
00079 help='clear the configuration (including all perspectives and plugin settings)')
00080 common_group.add_argument('-d', '--disable-init-threads', dest='disable_init_threads', default=False, action='store_true',
00081 help='do not set Qt.AA_X11InitThreads')
00082 if not standalone:
00083 common_group.add_argument('-f', '--freeze-layout', dest='freeze_layout', action='store_true',
00084 help='freeze the layout of the GUI (prevent rearranging widgets, disable undock/redock)')
00085 common_group.add_argument('--force-discover', dest='force_discover', default=False, action='store_true',
00086 help='force a rediscover of plugins')
00087 common_group.add_argument('-h', '--help', action='help',
00088 help='show this help message and exit')
00089 if not standalone:
00090 common_group.add_argument('-l', '--lock-perspective', dest='lock_perspective', action='store_true',
00091 help='lock the GUI to the used perspective (hide menu bar and close buttons of plugins)')
00092 common_group.add_argument('-m', '--multi-process', dest='multi_process', default=False, action='store_true',
00093 help='use separate processes for each plugin instance (currently only supported under X11)')
00094 common_group.add_argument('-p', '--perspective', dest='perspective', type=str, metavar='PERSPECTIVE',
00095 help='start with this named perspective')
00096 common_group.add_argument('--perspective-file', dest='perspective_file', type=str, metavar='PERSPECTIVE_FILE',
00097 help='start with a perspective loaded from a file')
00098 common_group.add_argument('--reload-import', dest='reload_import', default=False, action='store_true',
00099 help='reload every imported module')
00100 if not standalone:
00101 common_group.add_argument('-s', '--standalone', dest='standalone_plugin', type=str, metavar='PLUGIN',
00102 help='start only this plugin (implies -l). To pass arguments to the plugin use --args')
00103 common_group.add_argument('-t', '--on-top', dest='on_top', default=False, action='store_true',
00104 help='set window mode to always on top')
00105 common_group.add_argument('-v', '--verbose', dest='verbose', default=False, action='store_true',
00106 help='output qDebug messages')
00107
00108 if not standalone:
00109 common_group.add_argument('--args', dest='plugin_args', nargs='*', type=str,
00110 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.')
00111
00112 group = parser.add_argument_group('Options to query information without starting a GUI instance',
00113 'These options can be used to query information about valid arguments for various options.')
00114 group.add_argument('--list-perspectives', dest='list_perspectives', action='store_true',
00115 help='list available perspectives')
00116 group.add_argument('--list-plugins', dest='list_plugins', action='store_true',
00117 help='list available plugins')
00118 parser.add_argument_group(group)
00119
00120 group = parser.add_argument_group('Options to operate on a running GUI instance',
00121 'These options can be used to perform actions on a running GUI instance.')
00122 group.add_argument('--command-pid', dest='command_pid', type=int, metavar='PID',
00123 help='pid of the GUI instance to operate on, defaults to oldest running GUI instance')
00124 group.add_argument('--command-start-plugin', dest='command_start_plugin', type=str, metavar='PLUGIN',
00125 help='start plugin')
00126 group.add_argument('--command-switch-perspective', dest='command_switch_perspective', type=str, metavar='PERSPECTIVE',
00127 help='switch perspective')
00128 if not self._dbus_available:
00129 group.description = 'These options are not available since DBus is available!'
00130 for o in group._group_actions:
00131 o.help = SUPPRESS
00132 parser.add_argument_group(group)
00133
00134 group = parser.add_argument_group('Special options for embedding widgets from separate processes',
00135 'These options should never be used on the CLI but only from the GUI code itself.')
00136 group.add_argument('--embed-plugin', dest='embed_plugin', type=str, metavar='PLUGIN',
00137 help='embed a plugin into an already running GUI instance (requires all other --embed-* options)')
00138 group.add_argument('--embed-plugin-serial', dest='embed_plugin_serial', type=int, metavar='SERIAL',
00139 help='serial number of plugin to be embedded (requires all other --embed-* options)')
00140 group.add_argument('--embed-plugin-address', dest='embed_plugin_address', type=str, metavar='ADDRESS',
00141 help='dbus server address of the GUI instance to embed plugin into (requires all other --embed-* options)')
00142 for o in group._group_actions:
00143 o.help = SUPPRESS
00144 parser.add_argument_group(group)
00145
00146 if plugin_argument_provider:
00147 plugin_argument_provider(parser)
00148
00149 return common_group
00150
00151 def _add_plugin_providers(self):
00152 pass
00153
00154 def _add_reload_paths(self, reload_importer):
00155 reload_importer.add_reload_path(os.path.join(os.path.dirname(__file__), *('..',) * 4))
00156
00157 def _check_icon_theme_compliance(self):
00158 from python_qt_binding.QtGui import QIcon
00159
00160 if QIcon.themeName() == '' or \
00161 QIcon.fromTheme('document-save').isNull() or \
00162 QIcon.fromTheme('document-open').isNull() or \
00163 QIcon.fromTheme('edit-cut').isNull() or \
00164 QIcon.fromTheme('object-flip-horizontal').isNull():
00165 if 'darwin' in platform.platform().lower() and '/usr/local/share/icons' not in QIcon.themeSearchPaths():
00166 QIcon.setThemeSearchPaths(QIcon.themeSearchPaths() + ['/usr/local/share/icons'])
00167 original_theme = QIcon.themeName()
00168 QIcon.setThemeName('Tango')
00169 if QIcon.fromTheme('document-save').isNull():
00170 QIcon.setThemeName(original_theme)
00171
00172 def create_application(self, argv):
00173 from python_qt_binding.QtCore import Qt
00174 from python_qt_binding.QtGui import QApplication
00175 if not self._options.disable_init_threads:
00176 QApplication.setAttribute(Qt.AA_X11InitThreads, True)
00177 app = QApplication(argv)
00178 app.setAttribute(Qt.AA_DontShowIconsInMenus, False)
00179 return app
00180
00181 def main(self, argv=None, standalone=None, plugin_argument_provider=None, plugin_manager_settings_prefix=''):
00182 if argv is None:
00183 argv = sys.argv
00184
00185
00186 arguments = argv[1:]
00187
00188
00189 if not standalone:
00190 plugin_args = []
00191 if '--args' in arguments:
00192 index = arguments.index('--args')
00193 plugin_args = arguments[index + 1:]
00194 arguments = arguments[0:index + 1]
00195
00196 parser = ArgumentParser(os.path.basename(Main.main_filename), add_help=False)
00197 self.add_arguments(parser, standalone=bool(standalone), plugin_argument_provider=plugin_argument_provider)
00198 self._options = parser.parse_args(arguments)
00199
00200 if standalone:
00201
00202 parser = ArgumentParser(os.path.basename(Main.main_filename), add_help=False)
00203 self.add_arguments(parser, standalone=bool(standalone))
00204 self._options, plugin_args = parser.parse_known_args(arguments)
00205 self._options.plugin_args = plugin_args
00206
00207
00208 if standalone:
00209 self._options.freeze_layout = False
00210 self._options.lock_perspective = False
00211 self._options.multi_process = False
00212 self._options.perspective = None
00213 self._options.perspective_file = None
00214 self._options.standalone_plugin = standalone
00215 self._options.list_perspectives = False
00216 self._options.list_plugins = False
00217 self._options.command_pid = None
00218 self._options.command_start_plugin = None
00219 self._options.command_switch_perspective = None
00220 self._options.embed_plugin = None
00221 self._options.embed_plugin_serial = None
00222 self._options.embed_plugin_address = None
00223
00224
00225 try:
00226 if self._options.plugin_args and not self._options.standalone_plugin and not self._options.command_start_plugin and not self._options.embed_plugin:
00227 raise RuntimeError('Option --args can only be used together with either --standalone, --command-start-plugin or --embed-plugin option')
00228
00229 if self._options.freeze_layout and not self._options.lock_perspective:
00230 raise RuntimeError('Option --freeze_layout can only be used together with the --lock_perspective option')
00231
00232 list_options = (self._options.list_perspectives, self._options.list_plugins)
00233 list_options_set = [opt for opt in list_options if opt is not False]
00234 if len(list_options_set) > 1:
00235 raise RuntimeError('Only one --list-* option can be used at a time')
00236
00237 command_options = (self._options.command_start_plugin, self._options.command_switch_perspective)
00238 command_options_set = [opt for opt in command_options if opt is not None]
00239 if len(command_options_set) > 0 and not self._dbus_available:
00240 raise RuntimeError('Without DBus support the --command-* options are not available')
00241 if len(command_options_set) > 1:
00242 raise RuntimeError('Only one --command-* option can be used at a time (except --command-pid which is optional)')
00243 if len(command_options_set) == 0 and self._options.command_pid is not None:
00244 raise RuntimeError('Option --command_pid can only be used together with an other --command-* option')
00245
00246 embed_options = (self._options.embed_plugin, self._options.embed_plugin_serial, self._options.embed_plugin_address)
00247 embed_options_set = [opt for opt in embed_options if opt is not None]
00248 if len(command_options_set) > 0 and not self._dbus_available:
00249 raise RuntimeError('Without DBus support the --embed-* options are not available')
00250 if len(embed_options_set) > 0 and len(embed_options_set) < len(embed_options):
00251 raise RuntimeError('Missing option(s) - all \'--embed-*\' options must be set')
00252
00253 if len(embed_options_set) > 0 and self._options.clear_config:
00254 raise RuntimeError('Option --clear-config can only be used without any --embed-* option')
00255
00256 groups = (list_options_set, command_options_set, embed_options_set)
00257 groups_set = [opt for opt in groups if len(opt) > 0]
00258 if len(groups_set) > 1:
00259 raise RuntimeError('Options from different groups (--list, --command, --embed) can not be used together')
00260
00261 perspective_options = (self._options.perspective, self._options.perspective_file)
00262 perspective_options_set = [opt for opt in perspective_options if opt is not None]
00263 if len(perspective_options_set) > 1:
00264 raise RuntimeError('Only one --perspective-* option can be used at a time')
00265
00266 if self._options.perspective_file is not None and not os.path.isfile(self._options.perspective_file):
00267 raise RuntimeError('Option --perspective-file must reference existing file')
00268
00269 except RuntimeError as e:
00270 print(str(e))
00271
00272
00273 return 1
00274
00275
00276 if self._options.standalone_plugin is not None:
00277 self._options.lock_perspective = True
00278
00279
00280 from .application_context import ApplicationContext
00281 context = ApplicationContext()
00282 context.qtgui_path = self._qtgui_path
00283 context.options = self._options
00284
00285 if self._dbus_available:
00286 from dbus import DBusException, Interface, SessionBus
00287
00288
00289 if self._dbus_available:
00290 context.provide_app_dbus_interfaces = len(groups_set) == 0
00291 context.dbus_base_bus_name = 'org.ros.qt_gui'
00292 if context.provide_app_dbus_interfaces:
00293 context.dbus_unique_bus_name = context.dbus_base_bus_name + '.pid%d' % os.getpid()
00294
00295
00296 from .application_dbus_interface import ApplicationDBusInterface
00297 _dbus_server = ApplicationDBusInterface(context.dbus_base_bus_name)
00298
00299
00300 if len(command_options_set) > 0 or len(embed_options_set) > 0:
00301 host_pid = None
00302 if self._options.command_pid is not None:
00303 host_pid = self._options.command_pid
00304 else:
00305 try:
00306 remote_object = SessionBus().get_object(context.dbus_base_bus_name, '/Application')
00307 except DBusException:
00308 pass
00309 else:
00310 remote_interface = Interface(remote_object, context.dbus_base_bus_name + '.Application')
00311 host_pid = remote_interface.get_pid()
00312 if host_pid is not None:
00313 context.dbus_host_bus_name = context.dbus_base_bus_name + '.pid%d' % host_pid
00314
00315
00316 if len(command_options_set) > 0:
00317 if self._options.command_start_plugin is not None:
00318 try:
00319 remote_object = SessionBus().get_object(context.dbus_host_bus_name, '/PluginManager')
00320 except DBusException:
00321 (rc, msg) = (1, 'unable to communicate with GUI instance "%s"' % context.dbus_host_bus_name)
00322 else:
00323 remote_interface = Interface(remote_object, context.dbus_base_bus_name + '.PluginManager')
00324 (rc, msg) = remote_interface.start_plugin(self._options.command_start_plugin, ' '.join(self._options.plugin_args))
00325 if rc == 0:
00326 print('qt_gui_main() started plugin "%s" in GUI "%s"' % (msg, context.dbus_host_bus_name))
00327 else:
00328 print('qt_gui_main() could not start plugin "%s" in GUI "%s": %s' % (self._options.command_start_plugin, context.dbus_host_bus_name, msg))
00329 return rc
00330 elif self._options.command_switch_perspective is not None:
00331 remote_object = SessionBus().get_object(context.dbus_host_bus_name, '/PerspectiveManager')
00332 remote_interface = Interface(remote_object, context.dbus_base_bus_name + '.PerspectiveManager')
00333 remote_interface.switch_perspective(self._options.command_switch_perspective)
00334 print('qt_gui_main() switched to perspective "%s" in GUI "%s"' % (self._options.command_switch_perspective, context.dbus_host_bus_name))
00335 return 0
00336 raise RuntimeError('Unknown command not handled')
00337
00338
00339 setattr(sys, 'SELECT_QT_BINDING', self._options.qt_binding)
00340 from python_qt_binding import QT_BINDING
00341
00342 from python_qt_binding.QtCore import qDebug, qInstallMsgHandler, QSettings, Qt, QtCriticalMsg, QtDebugMsg, QtFatalMsg, QTimer, QtWarningMsg
00343 from python_qt_binding.QtGui import QAction, QIcon, QMenuBar
00344
00345 from .about_handler import AboutHandler
00346 from .composite_plugin_provider import CompositePluginProvider
00347 from .container_manager import ContainerManager
00348 from .help_provider import HelpProvider
00349 from .icon_loader import get_icon
00350 from .main_window import MainWindow
00351 from .minimized_dock_widgets_toolbar import MinimizedDockWidgetsToolbar
00352 from .perspective_manager import PerspectiveManager
00353 from .plugin_manager import PluginManager
00354
00355 def message_handler(type_, msg):
00356 colored_output = 'TERM' in os.environ and 'ANSI_COLORS_DISABLED' not in os.environ
00357 cyan_color = '\033[36m' if colored_output else ''
00358 red_color = '\033[31m' if colored_output else ''
00359 reset_color = '\033[0m' if colored_output else ''
00360 if type_ == QtDebugMsg and self._options.verbose:
00361 print(msg, file=sys.stderr)
00362 elif type_ == QtWarningMsg:
00363 print(cyan_color + msg + reset_color, file=sys.stderr)
00364 elif type_ == QtCriticalMsg:
00365 print(red_color + msg + reset_color, file=sys.stderr)
00366 elif type_ == QtFatalMsg:
00367 print(red_color + msg + reset_color, file=sys.stderr)
00368 sys.exit(1)
00369 qInstallMsgHandler(message_handler)
00370
00371 app = self.create_application(argv)
00372
00373 self._check_icon_theme_compliance()
00374
00375 settings = QSettings(QSettings.IniFormat, QSettings.UserScope, 'ros.org', self._settings_filename)
00376 if len(embed_options_set) == 0:
00377 if self._options.clear_config:
00378 settings.clear()
00379
00380 main_window = MainWindow()
00381 if self._options.on_top:
00382 main_window.setWindowFlags(Qt.WindowStaysOnTopHint)
00383
00384 main_window.statusBar()
00385
00386 def sigint_handler(*args):
00387 qDebug('\nsigint_handler()')
00388 main_window.close()
00389 signal.signal(signal.SIGINT, sigint_handler)
00390
00391 timer = QTimer()
00392 timer.start(500)
00393 timer.timeout.connect(lambda: None)
00394
00395
00396 menu_bar = QMenuBar()
00397 if 'darwin' in platform.platform().lower():
00398 menu_bar.setNativeMenuBar(True)
00399 else:
00400 menu_bar.setNativeMenuBar(False)
00401 if not self._options.lock_perspective:
00402 main_window.setMenuBar(menu_bar)
00403
00404 file_menu = menu_bar.addMenu(menu_bar.tr('&File'))
00405 action = QAction(file_menu.tr('&Quit'), file_menu)
00406 action.setIcon(QIcon.fromTheme('application-exit'))
00407 action.triggered.connect(main_window.close)
00408 file_menu.addAction(action)
00409
00410 else:
00411 app.setQuitOnLastWindowClosed(False)
00412
00413 main_window = None
00414 menu_bar = None
00415
00416 self._add_plugin_providers()
00417
00418
00419 plugin_provider = CompositePluginProvider(self.plugin_providers)
00420 plugin_manager = PluginManager(plugin_provider, settings, context, settings_prefix=plugin_manager_settings_prefix)
00421
00422 if self._options.list_plugins:
00423
00424 print('\n'.join(sorted(plugin_manager.get_plugins().values())))
00425 return 0
00426
00427 help_provider = HelpProvider()
00428 plugin_manager.plugin_help_signal.connect(help_provider.plugin_help_request)
00429
00430
00431 if main_window is not None:
00432 perspective_manager = PerspectiveManager(settings, context)
00433
00434 if self._options.list_perspectives:
00435
00436 print('\n'.join(sorted(perspective_manager.perspectives)))
00437 return 0
00438 else:
00439 perspective_manager = None
00440
00441 if main_window is not None:
00442 container_manager = ContainerManager(main_window, plugin_manager)
00443 plugin_manager.set_main_window(main_window, menu_bar, container_manager)
00444
00445 if not self._options.freeze_layout:
00446 minimized_dock_widgets_toolbar = MinimizedDockWidgetsToolbar(container_manager, main_window)
00447 main_window.addToolBar(Qt.BottomToolBarArea, minimized_dock_widgets_toolbar)
00448 plugin_manager.set_minimized_dock_widgets_toolbar(minimized_dock_widgets_toolbar)
00449
00450 if menu_bar is not None:
00451 perspective_menu = menu_bar.addMenu(menu_bar.tr('P&erspectives'))
00452 perspective_manager.set_menu(perspective_menu)
00453
00454
00455 if perspective_manager is not None and main_window is not None:
00456
00457 perspective_manager.perspective_changed_signal.connect(main_window.perspective_changed)
00458
00459 perspective_manager.save_settings_signal.connect(main_window.save_settings)
00460 perspective_manager.restore_settings_signal.connect(main_window.restore_settings)
00461 perspective_manager.restore_settings_without_plugin_changes_signal.connect(main_window.restore_settings)
00462
00463 if perspective_manager is not None and plugin_manager is not None:
00464 perspective_manager.save_settings_signal.connect(plugin_manager.save_settings)
00465 plugin_manager.save_settings_completed_signal.connect(perspective_manager.save_settings_completed)
00466 perspective_manager.restore_settings_signal.connect(plugin_manager.restore_settings)
00467 perspective_manager.restore_settings_without_plugin_changes_signal.connect(plugin_manager.restore_settings_without_plugins)
00468
00469 if plugin_manager is not None and main_window is not None:
00470
00471 plugin_manager.plugins_about_to_change_signal.connect(main_window.save_setup)
00472
00473 plugin_manager.plugins_changed_signal.connect(main_window.restore_state)
00474
00475 main_window.save_settings_before_close_signal.connect(plugin_manager.close_application)
00476
00477 plugin_manager.close_application_signal.connect(main_window.close, type=Qt.QueuedConnection)
00478
00479 if main_window is not None and menu_bar is not None:
00480 about_handler = AboutHandler(context.qtgui_path, main_window)
00481 help_menu = menu_bar.addMenu(menu_bar.tr('&Help'))
00482 action = QAction(file_menu.tr('&About'), help_menu)
00483 action.setIcon(QIcon.fromTheme('help-about'))
00484 action.triggered.connect(about_handler.show)
00485 help_menu.addAction(action)
00486
00487
00488 if main_window is not None:
00489 main_window.resize(600, 450)
00490 main_window.move(100, 100)
00491
00492
00493 src_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..'))
00494 if src_path not in sys.path:
00495 sys.path.append(src_path)
00496
00497
00498 plugin = None
00499 plugin_serial = None
00500 if self._options.embed_plugin is not None:
00501 plugin = self._options.embed_plugin
00502 plugin_serial = self._options.embed_plugin_serial
00503 elif self._options.standalone_plugin is not None:
00504 plugin = self._options.standalone_plugin
00505 plugin_serial = 0
00506 if plugin is not None:
00507 plugins = plugin_manager.find_plugins_by_name(plugin)
00508 if len(plugins) == 0:
00509 print('qt_gui_main() found no plugin matching "%s"' % plugin)
00510 return 1
00511 elif len(plugins) > 1:
00512 print('qt_gui_main() found multiple plugins matching "%s"\n%s' % (plugin, '\n'.join(plugins.values())))
00513 return 1
00514 plugin = plugins.keys()[0]
00515
00516 qDebug('QtBindingHelper using %s' % QT_BINDING)
00517
00518 plugin_manager.discover()
00519
00520 if self._options.reload_import:
00521 qDebug('ReloadImporter() automatically reload all subsequent imports')
00522 from .reload_importer import ReloadImporter
00523 _reload_importer = ReloadImporter()
00524 self._add_reload_paths(_reload_importer)
00525 _reload_importer.enable()
00526
00527
00528 if perspective_manager is not None:
00529 if plugin:
00530 perspective_manager.set_perspective(plugin, hide_and_without_plugin_changes=True)
00531 elif self._options.perspective_file:
00532 perspective_manager.import_perspective_from_file(self._options.perspective_file, perspective_manager.HIDDEN_PREFIX + '__cli_perspective_from_file')
00533 else:
00534 perspective_manager.set_perspective(self._options.perspective)
00535
00536
00537 if plugin:
00538 plugin_manager.load_plugin(plugin, plugin_serial, self._options.plugin_args)
00539 running = plugin_manager.is_plugin_running(plugin, plugin_serial)
00540 if not running:
00541 return 1
00542 if self._options.standalone_plugin:
00543
00544 plugin_descriptor = plugin_manager.get_plugin_descriptor(plugin)
00545 action_attributes = plugin_descriptor.action_attributes()
00546 if 'icon' in action_attributes and action_attributes['icon'] is not None:
00547 base_path = plugin_descriptor.attributes().get('plugin_path')
00548 try:
00549 icon = get_icon(action_attributes['icon'], action_attributes.get('icontype', None), base_path)
00550 except UserWarning:
00551 pass
00552 else:
00553 app.setWindowIcon(icon)
00554
00555 if main_window is not None:
00556 main_window.show()
00557 if sys.platform == 'darwin':
00558 main_window.raise_()
00559
00560 return app.exec_()
00561
00562
00563 if __name__ == '__main__':
00564 main = Main()
00565 sys.exit(main.main())