00001
00002 import os
00003 import re
00004 import yaml
00005 import threading
00006 import itertools
00007
00008 import rospy
00009 import rospkg
00010 import roslaunch
00011
00012 from rqt_launchtree.launchtree_loader import LaunchtreeLoader
00013 from rqt_launchtree.launchtree_config import LaunchtreeConfig, LaunchtreeArg, LaunchtreeRemap, LaunchtreeParam, LaunchtreeRosparam
00014
00015 from python_qt_binding import loadUi
00016 from python_qt_binding.QtCore import Qt, Signal
00017 from python_qt_binding.QtGui import QFileDialog, QWidget, QIcon, QTreeWidgetItem, QColor
00018
00019 class LaunchtreeEntryItem(QTreeWidgetItem):
00020 _type_order = [dict, roslaunch.core.Node, LaunchtreeRosparam, roslaunch.core.Param, LaunchtreeRemap, LaunchtreeArg, object]
00021
00022 def __init__(self, *args, **kw ):
00023 super(LaunchtreeEntryItem, self).__init__(*args, **kw)
00024 self.inconsistent = False
00025 def __ge__(self, other):
00026 own_type_idx = map(lambda t: isinstance(self.instance, t), self._type_order).index(True)
00027 other_type_idx = map(lambda t: isinstance(other.instance, t), self._type_order).index(True)
00028 if own_type_idx != other_type_idx:
00029 return own_type_idx >= other_type_idx
00030 return self.text(0) >= other.text(0)
00031 def __lt__(self, other):
00032 return not self.__ge__(other)
00033
00034
00035 class LaunchtreeWidget(QWidget):
00036
00037 update_launch_view = Signal(object)
00038 display_load_error = Signal(str, str)
00039
00040 def __init__(self, context):
00041 super(LaunchtreeWidget, self).__init__()
00042
00043 self._rp = rospkg.RosPack()
00044 self._rp_package_list = self._rp.list()
00045 res_folder = os.path.join(self._rp.get_path('rqt_launchtree'), 'resource')
00046 ui_file = os.path.join(res_folder, 'launchtree_widget.ui')
00047 loadUi(ui_file, self)
00048
00049 self._block_load = True
00050
00051 self.editor = 'gedit'
00052
00053 self.setObjectName('LaunchtreeWidget')
00054 self.reload_button.setIcon(QIcon.fromTheme('view-refresh'))
00055
00056 self._properties_empty_ui = os.path.join(res_folder, 'properties_empty.ui')
00057 self._properties_param_ui = os.path.join(res_folder, 'properties_param.ui')
00058
00059 self._icon_include = QIcon(os.path.join(res_folder, 'img/include.png'))
00060 self._icon_node = QIcon(os.path.join(res_folder, 'img/node.png'))
00061 self._icon_param = QIcon(os.path.join(res_folder, 'img/param.png'))
00062 self._icon_arg = QIcon(os.path.join(res_folder, 'img/arg.png'))
00063 self._icon_remap = QIcon(os.path.join(res_folder, 'img/remap.png'))
00064 self._icon_rosparam = QIcon(os.path.join(res_folder, 'img/rosparam_load.png'))
00065 self._icon_default = QIcon(os.path.join(res_folder, 'img/default.png'))
00066 self._icon_warn = QIcon(os.path.join(res_folder, 'img/warn.png'))
00067 self._launch_separator = ' -- '
00068 self._highlight_color = QColor(255, 255, 150)
00069 self._neutral_color = QColor(255, 255, 255, 0)
00070
00071
00072 self.update_launch_view.connect(self._update_launch_view)
00073 self.display_load_error.connect(self._display_load_error)
00074 self.package_select.currentIndexChanged.connect(self.update_launchfiles)
00075 self.launchfile_select.currentIndexChanged.connect(lambda idx: self.load_launchfile())
00076 self.reload_button.clicked.connect(self.load_launchfile)
00077 self.open_button.clicked.connect(self._root_open_clicked)
00078 self.launch_view.currentItemChanged.connect(self.launch_entry_changed)
00079 self.filter_nodes.toggled.connect(lambda t: self._filter_launch_view())
00080 self.filter_params.toggled.connect(lambda t: self._filter_launch_view())
00081 self.filter_args.toggled.connect(lambda t: self._filter_launch_view())
00082 self.filter_remaps.toggled.connect(lambda t: self._filter_launch_view())
00083 self.filter_empty.toggled.connect(lambda t: self._filter_launch_view())
00084 self.search_input.textChanged.connect(lambda t: self._filter_launch_view(collapse=t==''))
00085 self.launch_open_button.clicked.connect(self._launch_open_clicked)
00086
00087 self.reset()
00088
00089
00090 def reset(self):
00091 self._launch_config = LaunchtreeConfig()
00092 self._package_list = list()
00093 self._load_thread = None
00094 self.properties_content.setCurrentIndex(0)
00095 self.main_view.setCurrentIndex(0)
00096
00097 self.update_package_list()
00098
00099
00100 def block_load(self, do_block):
00101 self._block_load = do_block
00102
00103 def load_launchfile(self):
00104 if self._block_load: return
00105 self.launch_view.clear()
00106 self.properties_content.setCurrentIndex(0)
00107 self.main_view.setCurrentIndex(0)
00108 filename = os.path.join(
00109 self._rp.get_path(self.package_select.currentText()),
00110 self.launchfile_select.currentText()
00111 )
00112 launchargs = roslaunch.substitution_args.resolve_args(self.args_input.text()).split(' ')
00113 if os.path.isfile(filename):
00114 self.progress_bar.setRange(0,0)
00115 self._load_thread = threading.Thread(target=self._load_launch_items, args=[filename, launchargs])
00116 self._load_thread.daemon = True
00117 self._load_thread.start()
00118
00119 def _load_launch_items(self, filename, launchargs):
00120 self._launch_config = LaunchtreeConfig()
00121 items = list()
00122 try:
00123 loader = LaunchtreeLoader()
00124 loader.load(filename, self._launch_config, verbose=False, argv=['','',''] + launchargs)
00125 items = self.display_config_tree(self._launch_config.tree)
00126 except Exception as e:
00127 error_msg = re.sub(r'(\[?(?:/\w+)+\.launch\]?)',
00128 lambda m: '[%s]'%self._filename_to_label(m.group(0)),
00129 str(e)
00130 )
00131 help_msg = ''
00132 if 'arg to be set' in str(e):
00133 help_msg = 'You can pass args to the root launch file by specifying them in the "args" input field, for example "arg_key:=arg_value".'
00134 self.display_load_error.emit(error_msg, help_msg)
00135 self.update_launch_view.emit(items)
00136
00137
00138 def display_config_tree(self, config_tree):
00139 items = list()
00140 for key, instance in config_tree.items():
00141 if key == '_root': continue
00142 i = LaunchtreeEntryItem()
00143 i.instance = instance
00144 if isinstance(i.instance, roslaunch.core.Param):
00145 i.inconsistent = i.instance.inconsistent
00146 if isinstance(instance, dict):
00147 childItems = self.display_config_tree(instance)
00148 i.inconsistent = any(c.inconsistent for c in childItems)
00149 i.addChildren(childItems)
00150 i.instance = instance.get('_root', instance)
00151 if isinstance(i.instance, dict):
00152 i.setText(0, self._filename_to_label(key.split(':')[0]))
00153 i.setIcon(0, self._icon_include if not i.inconsistent else self._icon_warn)
00154 else:
00155 i.setText(0, self._filename_to_label(key.split(':')[0]) if isinstance(i.instance, LaunchtreeRosparam) else
00156 key.split(':')[0])
00157 i.setIcon(0,
00158 self._icon_warn if i.inconsistent else
00159 self._icon_node if isinstance(i.instance, roslaunch.core.Node) else
00160 self._icon_param if isinstance(i.instance, roslaunch.core.Param) else
00161 self._icon_arg if isinstance(i.instance, LaunchtreeArg) else
00162 self._icon_remap if isinstance(i.instance, LaunchtreeRemap) else
00163 self._icon_rosparam if isinstance(i.instance, LaunchtreeRosparam) else
00164 self._icon_default)
00165 items.append(i)
00166 return items
00167
00168 def _display_load_error(self, error_msg, help_msg):
00169 self.error_label.setText(error_msg)
00170 self.help_label.setText(help_msg)
00171 self.main_view.setCurrentIndex(1)
00172
00173 def _update_launch_view(self, items):
00174 self.launch_view.clear()
00175 self.launch_view.addTopLevelItems(items)
00176 self.launch_view.sortItems(0, Qt.AscendingOrder)
00177 self._filter_launch_view()
00178 self.progress_bar.setRange(0,1)
00179 self.progress_bar.setValue(1)
00180 self._load_thread = None
00181
00182 def update_package_list(self):
00183 self._package_list = sorted(
00184 filter(lambda p: len(self._get_launch_files(self._rp.get_path(p)))>0,
00185 self._rp_package_list
00186 )
00187 )
00188 self.package_select.clear()
00189 self.package_select.addItems(self._package_list)
00190 self.package_select.setCurrentIndex(0)
00191
00192 def update_launchfiles(self, idx):
00193 package = self.package_select.itemText(idx)
00194 folder = self._rp.get_path(package)
00195 launchfiles = self._get_launch_files(folder)
00196 self.launchfile_select.clear()
00197 self.launchfile_select.addItems(launchfiles)
00198
00199 def _get_launch_files(self, path):
00200 return sorted(
00201 itertools.imap(lambda p: p.replace(path + '/', ''),
00202 itertools.ifilter(self._is_launch_file,
00203 itertools.chain.from_iterable(
00204 itertools.imap(lambda f:
00205 map(lambda n: os.path.join(f[0], n), f[2]),
00206 os.walk(path)
00207 )
00208 )
00209 )
00210 )
00211 )
00212
00213 def _is_launch_file(self, path):
00214 if not os.path.isfile(path): return False
00215 (root, ext) = os.path.splitext(path)
00216 if ext != '.launch': return False
00217 return True
00218
00219 def launch_entry_changed(self, current, previous):
00220
00221 if current is None:
00222 return
00223 data = current.instance
00224 if isinstance(data, dict) and data.has_key('_root'):
00225 data = data['_root']
00226 if isinstance(data, roslaunch.core.Param):
00227 self.properties_content.setCurrentIndex(1)
00228 self.param_name.setText(data.key.split('/')[-1] + ':')
00229 if isinstance(data.value, list):
00230 self.param_value_list.clear()
00231 self.param_value_list.addItems(list(str(v) for v in data.value))
00232 self.param_value_panel.setCurrentIndex(2)
00233 elif len(str(data.value)) < 100:
00234 self.param_value.setText(str(data.value))
00235 self.param_value_panel.setCurrentIndex(0)
00236 else:
00237 self.param_value_long.setPlainText(str(data.value))
00238 self.param_value_panel.setCurrentIndex(1)
00239 elif isinstance(data, roslaunch.core.Node):
00240 self.properties_content.setCurrentIndex(2)
00241 self.node_package.setText(data.package)
00242 self.node_type.setText(data.type)
00243 self.node_namespace.setText(str(data.namespace))
00244 self.node_args.setText(str(data.args))
00245 self.node_args.setEnabled(data.args != '')
00246 self.node_prefix.setText(str(data.launch_prefix) if data.launch_prefix is not None else '')
00247 self.node_prefix.setEnabled(data.launch_prefix is not None)
00248 self.node_machine.setText(str(data.machine_name) if data.machine_name is not None else '')
00249 self.node_machine.setEnabled(data.machine_name is not None)
00250 elif isinstance(data, LaunchtreeArg):
00251 self.properties_content.setCurrentIndex(4)
00252 self.arg_name.setText(data.name)
00253 self.arg_value.setText(str(data.value) if data.value is not None else '')
00254 self.arg_default.setText(str(data.default) if data.default is not None else '')
00255 self.arg_doc.setText(str(data.doc) if data.doc is not None else '')
00256 self.arg_value.setEnabled(data.value is not None)
00257 self.arg_default.setEnabled(not self.arg_value.isEnabled())
00258 elif isinstance(data, LaunchtreeRemap):
00259 self.properties_content.setCurrentIndex(5)
00260 self.remap_from.setText(data.from_topic)
00261 self.remap_to.setText(data.to_topic)
00262 elif isinstance(data, roslaunch.core.Machine):
00263 self.properties_content.setCurrentIndex(6)
00264 self.machine_address.setText(str(data.address))
00265 self.machine_port.setText(str(data.ssh_port))
00266 self.machine_user.setText(str(data.user) if data.user is not None else '')
00267 self.machine_user.setEnabled(data.user is not None)
00268 self.machine_loader.setText(str(data.env_loader) if data.env_loader is not None else '')
00269 self.machine_loader.setEnabled(data.env_loader is not None)
00270 elif isinstance(data, LaunchtreeRosparam):
00271 self.properties_content.setCurrentIndex(3)
00272 path_segments = self.launch_view.currentItem().text(0).split(self._launch_separator)
00273 if len(path_segments) == 2:
00274 (p, l) = path_segments
00275 (d, f) = os.path.split(l)
00276 else:
00277 p = None
00278 f = path_segments[0]
00279 self.file_package.setText(p if p is not None else '')
00280 self.file_package.setEnabled(p is not None)
00281 self.file_name.setText(f)
00282 elif isinstance(data, dict):
00283 self.properties_content.setCurrentIndex(3)
00284 (p, l) = self.launch_view.currentItem().text(0).split(self._launch_separator)
00285 (d, f) = os.path.split(l)
00286 self.file_package.setText(p)
00287 self.file_name.setText(f)
00288
00289
00290 else:
00291 self.properties_content.setCurrentIndex(0)
00292
00293 def _filter_launch_view(self, collapse=False):
00294 show_nodes = self.filter_nodes.isChecked()
00295 show_params = self.filter_params.isChecked()
00296 show_args = self.filter_args.isChecked()
00297 show_remaps = self.filter_remaps.isChecked()
00298 show_empty = self.filter_empty.isChecked()
00299 search_text = self.search_input.text()
00300 highlight = search_text != ''
00301 expand = not collapse and highlight
00302
00303 def filter_launch_entry(entry):
00304 show = False
00305
00306
00307 if isinstance(entry.instance, roslaunch.core.Param):
00308 show = show_params
00309
00310 elif isinstance(entry.instance, roslaunch.core.Node):
00311 show = show_nodes
00312
00313 elif isinstance(entry.instance, roslaunch.core.Machine):
00314 show = show_nodes
00315
00316 elif isinstance(entry.instance, LaunchtreeArg):
00317 show = show_args
00318
00319 elif isinstance(entry.instance, LaunchtreeRemap):
00320 show = show_remaps
00321
00322 show &= search_text in entry.text(0)
00323 if show:
00324 entry.setBackgroundColor(0, self._highlight_color if highlight else self._neutral_color)
00325
00326 if entry.childCount() > 0:
00327 not_empty = any(map(filter_launch_entry, map(entry.child, range(entry.childCount()))))
00328 show |= show_empty or not_empty
00329 entry.setExpanded(not collapse and (expand or entry.isExpanded()))
00330
00331 entry.setHidden(not show)
00332 return show
00333
00334 for idx in range(self.launch_view.topLevelItemCount()):
00335 filter_launch_entry(self.launch_view.topLevelItem(idx))
00336
00337
00338 def _launch_open_clicked(self):
00339 (p, l) = self.launch_view.currentItem().text(0).split(self._launch_separator)
00340 filename = os.path.join(self._rp.get_path(p), l)
00341 thread = threading.Thread(target=os.system, args=['%s %s' % (self.editor, filename)])
00342 thread.daemon = True
00343 thread.start()
00344
00345 def _root_open_clicked(self):
00346 filename = os.path.join(
00347 self._rp.get_path(self.package_select.currentText()),
00348 self.launchfile_select.currentText()
00349 )
00350 thread = threading.Thread(target=os.system, args=['%s %s' % (self.editor, filename)])
00351 thread.daemon = True
00352 thread.start()
00353
00354
00355 def shutdown(self):
00356 pass
00357
00358 def _filename_to_label(self, filename):
00359 tail = list()
00360 for d in reversed(filename.split('/')):
00361 if d in self._rp_package_list:
00362 return '%s%s%s' % (d, self._launch_separator, '/'.join(reversed(tail)))
00363 else:
00364 tail.append(d)
00365 return filename