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