1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 from python_qt_binding.QtCore import QMimeData, Qt
34 try:
35 from python_qt_binding.QtGui import QApplication, QInputDialog, QLineEdit, QMessageBox
36 except:
37 from python_qt_binding.QtWidgets import QApplication, QInputDialog, QLineEdit, QMessageBox
38 from python_qt_binding.QtGui import QIcon, QStandardItem, QStandardItemModel
39 import os
40 import shutil
41
42 import node_manager_fkie as nm
43
44 from .common import is_package, package_name
45 from .detailed_msg_box import WarningMessageBox
46 from .packages_thread import PackagesThread
50 '''
51 The launch item stored in the launch model.
52 '''
53
54 ITEM_TYPE = QStandardItem.UserType + 40
55
56 NOT_FOUND = -1
57 NOTHING = 0
58 RECENT_FILE = 1
59 LAUNCH_FILE = 2
60 CFG_FILE = 3
61 FOLDER = 10
62 PACKAGE = 11
63 STACK = 12
64
65 - def __init__(self, name, path, lid, parent=None):
66 '''
67 Initialize the topic item.
68 @param name: the topic name
69 @type name: C{str}
70 '''
71 QStandardItem.__init__(self, name)
72 self.parent_item = parent
73 self.name = name
74 self.path = path
75 self.package_name = package_name(os.path.dirname(self.path))[0]
76 self.id = lid
77 if self.id == LaunchItem.FOLDER:
78 self.setIcon(QIcon(":/icons/crystal_clear_folder.png"))
79 elif self.id == LaunchItem.PACKAGE:
80 self.setIcon(QIcon(":/icons/crystal_clear_package.png"))
81 elif self.id == LaunchItem.LAUNCH_FILE:
82 self.setIcon(QIcon(":/icons/crystal_clear_launch_file.png"))
83 elif self.id == LaunchItem.RECENT_FILE:
84 self.setIcon(QIcon(":/icons/crystal_clear_launch_file_recent.png"))
85 elif self.id == LaunchItem.STACK:
86 self.setIcon(QIcon(":/icons/crystal_clear_stack.png"))
87
88
89
90
93
94 - def data(self, role):
95 '''
96 The view asks us for all sorts of information about our data...
97 @param role: the art of the data
98 @type role: U{QtCore.Qt.DisplayRole<https://srinikom.github.io/pyside-docs/PySide/QtCore/Qt.html>}
99 @see: U{http://www.pyside.org/docs/pyside-1.0.1/PySide/QtCore/Qt.html}
100 '''
101 if role == Qt.DisplayRole:
102
103 if self.id == LaunchItem.RECENT_FILE:
104 return "%s [%s]" % (self.name, self.package_name)
105 else:
106 return "%s" % self.name
107 elif role == Qt.ToolTipRole:
108
109 result = "%s" % self.path
110 if self.id == LaunchItem.RECENT_FILE:
111 result = "%s\nPress 'Delete' to remove the entry from the history list" % self.path
112 return result
113
114
115
116
117
118
119 elif role == Qt.EditRole:
120 return "%s" % self.name
121 else:
122
123 return QStandardItem.data(self, role)
124
125 - def setData(self, value, role=Qt.EditRole):
126 if role == Qt.EditRole:
127
128 if self.name != value and self.id in [self.RECENT_FILE, self.LAUNCH_FILE, self.CFG_FILE, self.FOLDER]:
129 new_path = os.path.join(os.path.dirname(self.path), value)
130 if not os.path.exists(new_path):
131 os.rename(self.path, new_path)
132 self.name = value
133 self.path = new_path
134 else:
135 WarningMessageBox(QMessageBox.Warning, "Path already exists",
136 "`%s` already exists!" % value, "Complete path: %s" % new_path).exec_()
137 return QStandardItem.setData(self, value, role)
138
139 @classmethod
141 '''
142 Creates the list of the items . This list is used for the
143 visualization of topic data as a table row.
144 @param name: the topic name
145 @type name: C{str}
146 @param root: The parent QStandardItem
147 @type root: U{QtGui.QStandardItem<https://srinikom.github.io/pyside-docs/PySide/QtGui/QStandardItem.html>}
148 @return: the list for the representation as a row
149 @rtype: C{[L{LaunchItem} or U{QtGui.QStandardItem<https://srinikom.github.io/pyside-docs/PySide/QtGui/QStandardItem.html>}, ...]}
150 '''
151 items = []
152 item = LaunchItem(name, path, item_id, parent=root)
153 items.append(item)
154 return items
155
157 '''
158 @return: C{True} if it is a launch file
159 @rtype: C{boolean}
160 '''
161 return self.path is not None and os.path.isfile(self.path) and self.path.endswith('.launch')
162
164 '''
165 @return: C{True} if it is a config file
166 @rtype: C{boolean}
167 '''
168 return self.id == self.CFG_FILE
169
172 '''
173 The model to manage the list with launch files.
174 '''
175 header = [('Name', -1)]
176 '''@ivar: the list with columns C{[(name, width), ...]}'''
177
179 '''
180 Creates a new list model.
181 '''
182 QStandardItemModel.__init__(self)
183 self.setColumnCount(len(LaunchListModel.header))
184 self.setHorizontalHeaderLabels([label for label, _width in LaunchListModel.header])
185 self.pyqt_workaround = dict()
186 self.items = []
187 self.DIR_CACHE = {}
188 self.currentPath = None
189 self.load_history = self._getLoadHistory()
190 self.root_paths = [os.path.normpath(p) for p in os.getenv("ROS_PACKAGE_PATH").split(':')]
191 self._setNewList(self._moveUp(None))
192 self.__packages = {}
193 self._fill_packages_thread = PackagesThread()
194
196 result = list(self.load_history)
197 result.extend(self.root_paths)
198 return result
199
202
203
204
205
206
208 '''
209 @param index: parent of the list
210 @type index: U{QtCore.QModelIndex<https://srinikom.github.io/pyside-docs/PySide/QtCore/QModelIndex.html>}
211 @return: Flag or the requested item
212 @rtype: U{QtCore.Qt.ItemFlag<https://srinikom.github.io/pyside-docs/PySide/QtCore/Qt.html>}
213 @see: U{http://www.pyside.org/docs/pyside-1.0.1/PySide/QtCore/Qt.html}
214 '''
215 if not index.isValid():
216 return Qt.NoItemFlags
217 try:
218 item = self.itemFromIndex(index)
219 result = Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled
220 if item.id in [LaunchItem.RECENT_FILE, LaunchItem.LAUNCH_FILE, LaunchItem.CFG_FILE, LaunchItem.FOLDER]:
221 result = result | Qt.ItemIsEditable | Qt.ItemIsDragEnabled
222 return result
223 except:
224 return Qt.ItemIsEnabled | Qt.ItemIsSelectable
225
226
227
228
229
231 return ['text/plain']
232
234 mimeData = QMimeData()
235 text = ''
236 for index in indexes:
237 if index.isValid():
238 item = self.itemFromIndex(index)
239 prev = '%s\n' % text if text else ''
240 text = '%sfile://%s' % (prev, item.path)
241 mimeData.setData('text/plain', text)
242 return mimeData
243
244
245
246
247
249 '''
250 Reloads the cached packag list.
251 '''
252 if not self._fill_packages_thread.isAlive():
253 self._fill_packages_thread = PackagesThread()
254 self._fill_packages_thread.packages.connect(self._fill_packages)
255 self._fill_packages_thread.start()
256
258 '''
259 Reloads the current path.
260 '''
261
262 try:
263 from .common import PACKAGE_CACHE
264 PACKAGE_CACHE.clear()
265 self.DIR_CACHE = {}
266 except:
267 import traceback
268 print traceback.format_exc(2)
269 try:
270 if self.currentPath is None:
271 self._setNewList(self._moveUp(self.currentPath))
272 else:
273 self._setNewList(self._moveDown(self.currentPath))
274 except:
275 self._setNewList(self._moveUp(None))
276
278 '''
279 Returns for the given item and path the file path if this is a file. Otherwise the
280 folder will be expanded and None will be returned.
281 @param path_item: the list item
282 @type path_item: C{str}
283 @param path: the real path of the item
284 @type path: C{str}
285 @return: path of the launch file or None
286 @rtype: C{str}
287 @raise Exception if no path to given item was found
288 '''
289 if path_item == '..':
290 goto_path = os.path.dirname(path)
291 key_mod = QApplication.keyboardModifiers()
292 if key_mod & Qt.ControlModifier:
293 goto_path = None
294 root_path, items = self._moveUp(goto_path)
295 elif os.path.isfile(path):
296 return path
297 elif item_id == LaunchItem.RECENT_FILE or item_id == LaunchItem.LAUNCH_FILE:
298 raise Exception("Invalid file path: %s", path)
299 else:
300 key_mod = QApplication.keyboardModifiers()
301 onestep = False
302 if key_mod & Qt.ControlModifier:
303 onestep = True
304 root_path, items = self._moveDown(path, onestep)
305 self._setNewList((root_path, items))
306 return None
307
309 '''
310 Shows the new path in the launch configuration view. Only if the new path
311 is in ros package paths
312 @param path: new path
313 @type path: C{str}
314 '''
315
316 self._setNewList(self._moveDown(path))
317
318 - def add2LoadHistory(self, launch_file):
319 try:
320 self.load_history.remove(launch_file)
321 except:
322 pass
323 self.load_history.append(launch_file)
324 try:
325 while len(self.load_history) > nm.settings().launch_history_length:
326 self.load_history.pop(0)
327 except:
328 pass
329 self._storeLoadHistory(self.load_history)
330
331
332 - def removeFromLoadHistory(self, launch_file):
333 try:
334 self.load_history.remove(launch_file)
335 except:
336 pass
337 self._storeLoadHistory(self.load_history)
338
339
341 if show:
342
343 try:
344 items = []
345 for package, path in self.__packages.items():
346 items.append((package, path, LaunchItem.PACKAGE))
347 self._setNewList((self.currentPath if self.currentPath else '', items))
348 except:
349 import traceback
350 print traceback.format_exc(2)
351 else:
352 self._setNewList(self._moveUp(self.currentPath))
353
355 '''
356 Copy the file or folder to new position...
357 '''
358 if QApplication.clipboard().mimeData().hasText() and self.currentPath:
359 text = QApplication.clipboard().mimeData().text()
360 if text.startswith('file://'):
361 path = text.replace('file://', '')
362 basename = os.path.basename(text)
363 ok = True
364 if os.path.exists(os.path.join(self.currentPath, basename)):
365 basename, ok = QInputDialog.getText(None, 'File exists', 'New name (or override):', QLineEdit.Normal, basename)
366 if ok and basename:
367 if os.path.isdir(path):
368 shutil.copytree(path, os.path.join(self.currentPath, basename))
369 elif os.path.isfile(path):
370 shutil.copy2(path, os.path.join(self.currentPath, basename))
371 self.reloadCurrentPath()
372
374 '''
375 Copy the selected path to the clipboard
376 '''
377 mimeData = QMimeData()
378 text = ''
379 for index in indexes:
380 if index.isValid():
381 item = self.itemFromIndex(index)
382 prev = '%s\n' % text if text else ''
383 text = '%sfile://%s' % (prev, item.path)
384 mimeData.setData('text/plain', text)
385 QApplication.clipboard().setMimeData(mimeData)
386
387
388
389
390
392 '''
393 Sets the list to the given path and insert the items. If the root path is not
394 None the additional item '..' to go up will be inserted. The items parameter
395 is a tupel with three values (the displayed name, the path of the item, the id
396 of the item).
397 @see: L{LaunchListModel._addPathToList()}
398 @param root_path: the root directory
399 @type root_path: C{str}
400 @param items: the list with characterized items
401 @type items: C{[(item, path, id)]}
402 '''
403 root = self.invisibleRootItem()
404 while root.rowCount():
405 root.removeRow(0)
406 self.pyqt_workaround.clear()
407
408 if root_path is not None:
409 self._addPathToList('..', root_path, LaunchItem.NOTHING)
410 for item_name, item_path, item_id in items:
411 self._addPathToList(item_name, item_path, item_id)
412 self.currentPath = root_path
413
415 '''
416 Test whether the given path is in ROS_PACKAGE_PATH.
417 @return: C{True}, if the path is in the ROS_PACKAGE_PATH
418 @rtype: C{boolean}
419 '''
420
421 for p in self.root_paths:
422 if path.startswith(p):
423 return True
424 return False
425
427 '''
428 Inserts the given item in the list model.
429 @param item: the displayed name
430 @type item: C{str}
431 @param path: the path of the item
432 @type path: C{str}
433 @param path_id: the id of the item, which represents whether it is a file, package or stack.
434 @type path_id: C{constants of LaunchListModel}
435 '''
436 root = self.invisibleRootItem()
437 if item is None or path is None or path_id == LaunchItem.NOT_FOUND:
438 return False
439 if (path_id != LaunchItem.NOT_FOUND):
440
441 try:
442 for i in range(root.rowCount()):
443 launchItem = root.child(i)
444 launch_file_cmp = (path_id in [LaunchItem.RECENT_FILE, LaunchItem.LAUNCH_FILE] and item < launchItem.name)
445 launch_id_cmp = (launchItem.id > path_id and launchItem.id > LaunchItem.LAUNCH_FILE)
446 launch_name_cmp = (launchItem.id == path_id and item < launchItem.name)
447 if launch_file_cmp or launch_id_cmp or launch_name_cmp:
448 new_item_row = LaunchItem.getItemList(item, path, path_id, root)
449 root.insertRow(i, new_item_row)
450 self.pyqt_workaround[item] = new_item_row[0]
451 return True
452 new_item_row = LaunchItem.getItemList(item, path, path_id, root)
453 root.appendRow(new_item_row)
454 self.pyqt_workaround[item] = new_item_row[0]
455 return True
456 except:
457 import traceback
458 print traceback.format_exc(2)
459 return False
460
498
500 '''
501 Moves recursively down in the path tree and searches for a launch file. If
502 one is found True will be returned.
503 @return: C{True} if the path contains a launch file.
504 @rtype: C{boolean}
505 '''
506 fileList = os.listdir(path)
507 for cfile in fileList:
508 _, file_extension = os.path.splitext(cfile)
509 if os.path.isfile(os.path.join(path, cfile)) and (cfile.endswith('.launch')) or (file_extension in nm.settings().launch_view_file_ext):
510 return True
511 for cfile in fileList:
512 if os.path.isdir(os.path.join(path, cfile)):
513 if self._containsLaunches(os.path.join(path, cfile)):
514 return True
515 return False
516
518 '''
519 Moves recursively down in the path tree until the current path contains a
520 launch file.
521 @return: tupel of (root_path, items)
522 @rtype: C{tupel of (root_path, items)}
523 @see: L{LaunchListModel._setNewList()}
524 '''
525 result_list = []
526 dirlist = os.listdir(path)
527 for cfile in dirlist:
528 item = os.path.normpath(''.join([path, '/', cfile]))
529 pathItem = os.path.basename(item)
530 if pathItem == 'src':
531 pathItem = '%s (src)' % os.path.basename(os.path.dirname(item))
532 pathId = self._identifyPath(item)
533 if (pathId != LaunchItem.NOT_FOUND):
534 result_list.append((pathItem, item, pathId))
535 if len(result_list) == 1 and not os.path.isfile(result_list[0][1]):
536 if not onestep:
537 return self._moveDown(result_list[0][1])
538 return path, result_list
539
541 '''
542 Moves recursively up in the path tree until the current path contains a
543 launch file or the root path defined by ROS_PACKAGES_PATH is reached.
544 @return: tupel of (root_path, items)
545 @rtype: C{tupel of (root_path, items)}
546 @see: L{LaunchListModel._setNewList()}
547 '''
548 result_list = []
549 if path is None or not self._is_in_ros_packages(path):
550 dirlist = self._getRootItems()
551 path = None
552 else:
553 dirlist = os.listdir(path)
554 for dfile in dirlist:
555 item = os.path.normpath(os.path.join(path, dfile)) if path is not None else dfile
556 pathItem = os.path.basename(item)
557 if pathItem == 'src':
558 pathItem = '%s (src)' % os.path.basename(os.path.dirname(item))
559 pathId = self._identifyPath(item)
560 if (pathId != LaunchItem.NOT_FOUND):
561 result_list.append((pathItem, item, pathId))
562 if path is not None and len(result_list) == 1 and not os.path.isfile(result_list[0][1]):
563 return self._moveUp(os.path.dirname(path))
564 else:
565 self.currentPath = None
566 return path, result_list
567
568 - def _getLoadHistory(self):
569 '''
570 Read the history of the recently loaded files from the file stored in ROS_HOME path.
571 @return: the list with file names
572 @rtype: C{[str]}
573 '''
574 result = list()
575 historyFile = nm.settings().qsettings(nm.settings().LAUNCH_HISTORY_FILE)
576 size = historyFile.beginReadArray("launch_history")
577 for i in range(size):
578 historyFile.setArrayIndex(i)
579 if i >= nm.settings().launch_history_length:
580 break
581 launch_file = historyFile.value("file")
582 if os.path.isfile(launch_file):
583 result.append(launch_file)
584 historyFile.endArray()
585 return result
586
587 - def _storeLoadHistory(self, files):
588 '''
589 Saves the list of recently loaded files to history. The existing history will be replaced!
590 @param files: the list with filenames
591 @type files: C{[str]}
592 '''
593 historyFile = nm.settings().qsettings(nm.settings().LAUNCH_HISTORY_FILE)
594 historyFile.beginWriteArray("launch_history")
595 for i, launch_file in enumerate(files):
596 historyFile.setArrayIndex(i)
597 historyFile.setValue("file", launch_file)
598 historyFile.endArray()
599