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 .main_window import MainWindow
00346 from .minimized_dock_widgets_toolbar import MinimizedDockWidgetsToolbar
00347 from .perspective_manager import PerspectiveManager
00348 from .plugin_manager import PluginManager
00349
00350 def message_handler(type_, msg):
00351 colored_output = 'TERM' in os.environ and 'ANSI_COLORS_DISABLED' not in os.environ
00352 cyan_color = '\033[36m' if colored_output else ''
00353 red_color = '\033[31m' if colored_output else ''
00354 reset_color = '\033[0m' if colored_output else ''
00355 if type_ == QtDebugMsg and self._options.verbose:
00356 print(msg, file=sys.stderr)
00357 elif type_ == QtWarningMsg:
00358 print(cyan_color + msg + reset_color, file=sys.stderr)
00359 elif type_ == QtCriticalMsg:
00360 print(red_color + msg + reset_color, file=sys.stderr)
00361 elif type_ == QtFatalMsg:
00362 print(red_color + msg + reset_color, file=sys.stderr)
00363 sys.exit(1)
00364 qInstallMsgHandler(message_handler)
00365
00366 app = self.create_application(argv)
00367
00368 self._check_icon_theme_compliance()
00369
00370 settings = QSettings(QSettings.IniFormat, QSettings.UserScope, 'ros.org', self._settings_filename)
00371 if len(embed_options_set) == 0:
00372 if self._options.clear_config:
00373 settings.clear()
00374
00375 main_window = MainWindow()
00376 if self._options.on_top:
00377 main_window.setWindowFlags(Qt.WindowStaysOnTopHint)
00378
00379 main_window.statusBar()
00380
00381 def sigint_handler(*args):
00382 qDebug('\nsigint_handler()')
00383 main_window.close()
00384 signal.signal(signal.SIGINT, sigint_handler)
00385
00386 timer = QTimer()
00387 timer.start(500)
00388 timer.timeout.connect(lambda: None)
00389
00390
00391 menu_bar = QMenuBar()
00392 if 'darwin' in platform.platform().lower():
00393 menu_bar.setNativeMenuBar(True)
00394 else:
00395 menu_bar.setNativeMenuBar(False)
00396 if not self._options.lock_perspective:
00397 main_window.setMenuBar(menu_bar)
00398
00399 file_menu = menu_bar.addMenu(menu_bar.tr('&File'))
00400 action = QAction(file_menu.tr('&Quit'), file_menu)
00401 action.setIcon(QIcon.fromTheme('application-exit'))
00402 action.triggered.connect(main_window.close)
00403 file_menu.addAction(action)
00404
00405 else:
00406 app.setQuitOnLastWindowClosed(False)
00407
00408 main_window = None
00409 menu_bar = None
00410
00411 self._add_plugin_providers()
00412
00413
00414 plugin_provider = CompositePluginProvider(self.plugin_providers)
00415 plugin_manager = PluginManager(plugin_provider, settings, context, settings_prefix=plugin_manager_settings_prefix)
00416
00417 if self._options.list_plugins:
00418
00419 print('\n'.join(sorted(plugin_manager.get_plugins().values())))
00420 return 0
00421
00422 help_provider = HelpProvider()
00423 plugin_manager.plugin_help_signal.connect(help_provider.plugin_help_request)
00424
00425
00426 if main_window is not None:
00427 perspective_manager = PerspectiveManager(settings, context)
00428
00429 if self._options.list_perspectives:
00430
00431 print('\n'.join(sorted(perspective_manager.perspectives)))
00432 return 0
00433 else:
00434 perspective_manager = None
00435
00436 if main_window is not None:
00437 container_manager = ContainerManager(main_window, plugin_manager)
00438 plugin_manager.set_main_window(main_window, menu_bar, container_manager)
00439
00440 if not self._options.freeze_layout:
00441 minimized_dock_widgets_toolbar = MinimizedDockWidgetsToolbar(container_manager, main_window)
00442 main_window.addToolBar(Qt.BottomToolBarArea, minimized_dock_widgets_toolbar)
00443 plugin_manager.set_minimized_dock_widgets_toolbar(minimized_dock_widgets_toolbar)
00444
00445 if menu_bar is not None:
00446 perspective_menu = menu_bar.addMenu(menu_bar.tr('P&erspectives'))
00447 perspective_manager.set_menu(perspective_menu)
00448
00449
00450 if perspective_manager is not None and main_window is not None:
00451
00452 perspective_manager.perspective_changed_signal.connect(main_window.perspective_changed)
00453
00454 perspective_manager.save_settings_signal.connect(main_window.save_settings)
00455 perspective_manager.restore_settings_signal.connect(main_window.restore_settings)
00456 perspective_manager.restore_settings_without_plugin_changes_signal.connect(main_window.restore_settings)
00457
00458 if perspective_manager is not None and plugin_manager is not None:
00459 perspective_manager.save_settings_signal.connect(plugin_manager.save_settings)
00460 plugin_manager.save_settings_completed_signal.connect(perspective_manager.save_settings_completed)
00461 perspective_manager.restore_settings_signal.connect(plugin_manager.restore_settings)
00462 perspective_manager.restore_settings_without_plugin_changes_signal.connect(plugin_manager.restore_settings_without_plugins)
00463
00464 if plugin_manager is not None and main_window is not None:
00465
00466 plugin_manager.plugins_about_to_change_signal.connect(main_window.save_setup)
00467
00468 plugin_manager.plugins_changed_signal.connect(main_window.restore_state)
00469
00470 main_window.save_settings_before_close_signal.connect(plugin_manager.close_application)
00471
00472 plugin_manager.close_application_signal.connect(main_window.close, type=Qt.QueuedConnection)
00473
00474 if main_window is not None and menu_bar is not None:
00475 about_handler = AboutHandler(context.qtgui_path, main_window)
00476 help_menu = menu_bar.addMenu(menu_bar.tr('&Help'))
00477 action = QAction(file_menu.tr('&About'), help_menu)
00478 action.setIcon(QIcon.fromTheme('help-about'))
00479 action.triggered.connect(about_handler.show)
00480 help_menu.addAction(action)
00481
00482
00483 if main_window is not None:
00484 main_window.resize(600, 450)
00485 main_window.move(100, 100)
00486
00487
00488 src_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..'))
00489 if src_path not in sys.path:
00490 sys.path.append(src_path)
00491
00492
00493 plugin = None
00494 plugin_serial = None
00495 if self._options.embed_plugin is not None:
00496 plugin = self._options.embed_plugin
00497 plugin_serial = self._options.embed_plugin_serial
00498 elif self._options.standalone_plugin is not None:
00499 plugin = self._options.standalone_plugin
00500 plugin_serial = 0
00501 if plugin is not None:
00502 plugins = plugin_manager.find_plugins_by_name(plugin)
00503 if len(plugins) == 0:
00504 print('qt_gui_main() found no plugin matching "%s"' % plugin)
00505 return 1
00506 elif len(plugins) > 1:
00507 print('qt_gui_main() found multiple plugins matching "%s"\n%s' % (plugin, '\n'.join(plugins.values())))
00508 return 1
00509 plugin = plugins.keys()[0]
00510
00511 qDebug('QtBindingHelper using %s' % QT_BINDING)
00512
00513 plugin_manager.discover()
00514
00515 if self._options.reload_import:
00516 qDebug('ReloadImporter() automatically reload all subsequent imports')
00517 from .reload_importer import ReloadImporter
00518 _reload_importer = ReloadImporter()
00519 self._add_reload_paths(_reload_importer)
00520 _reload_importer.enable()
00521
00522
00523 if perspective_manager is not None:
00524 if plugin:
00525 perspective_manager.set_perspective(plugin, hide_and_without_plugin_changes=True)
00526 elif self._options.perspective_file:
00527 perspective_manager.import_perspective_from_file(self._options.perspective_file, perspective_manager.HIDDEN_PREFIX + '__cli_perspective_from_file')
00528 else:
00529 perspective_manager.set_perspective(self._options.perspective)
00530
00531
00532 if plugin:
00533 plugin_manager.load_plugin(plugin, plugin_serial, self._options.plugin_args)
00534 running = plugin_manager.is_plugin_running(plugin, plugin_serial)
00535 if not running:
00536 return 1
00537
00538 if main_window is not None:
00539 main_window.show()
00540 if sys.platform == 'darwin':
00541 main_window.raise_()
00542
00543 return app.exec_()
00544
00545
00546 if __name__ == '__main__':
00547 main = Main()
00548 sys.exit(main.main())