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
36 except:
37 from python_qt_binding.QtWidgets import QApplication, QInputDialog, QLineEdit
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, utf8
45 from .detailed_msg_box import MessageBox
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 PROFILE = 5
59 RECENT_PROFILE = 6
60 RECENT_FILE = 10
61 LAUNCH_FILE = 11
62 CFG_FILE = 12
63 FOLDER = 20
64 PACKAGE = 21
65 STACK = 22
66
67 - def __init__(self, name, path, lid, parent=None):
93
94
95
96
99
100 - def data(self, role):
101 '''
102 The view asks us for all sorts of information about our data...
103 @param role: the art of the data
104 @type role: U{QtCore.Qt.DisplayRole<https://srinikom.github.io/pyside-docs/PySide/QtCore/Qt.html>}
105 @see: U{http://www.pyside.org/docs/pyside-1.0.1/PySide/QtCore/Qt.html}
106 '''
107 if role == Qt.DisplayRole:
108
109 if self.id == LaunchItem.RECENT_FILE or self.id == LaunchItem.RECENT_PROFILE:
110 return "%s [%s]" % (self.name, self.package_name)
111 else:
112 return "%s" % self.name
113 elif role == Qt.ToolTipRole:
114
115 result = "%s" % self.path
116 if self.id == LaunchItem.RECENT_FILE or self.id == LaunchItem.RECENT_PROFILE:
117 result = "%s\nPress 'Delete' to remove the entry from the history list" % self.path
118 return result
119
120
121
122
123
124
125 elif role == Qt.EditRole:
126 return "%s" % self.name
127 else:
128
129 return QStandardItem.data(self, role)
130
131 - def setData(self, value, role=Qt.EditRole):
132 if role == Qt.EditRole:
133
134 if self.name != value and self.id in [self.RECENT_FILE, self.LAUNCH_FILE, self.RECENT_PROFILE, self.PROFILE, self.CFG_FILE, self.FOLDER]:
135 new_path = os.path.join(os.path.dirname(self.path), value)
136 if not os.path.exists(new_path):
137 os.rename(self.path, new_path)
138 if self.name != value and self.id in [self.RECENT_FILE, self.RECENT_PROFILE]:
139
140 nm.settings().launch_history_add(new_path, replace=self.path)
141 self.name = value
142 self.path = new_path
143 else:
144 MessageBox.warning(self, "Path already exists",
145 "`%s` already exists!" % value, "Complete path: %s" % new_path)
146 return QStandardItem.setData(self, value, role)
147
148 @classmethod
150 '''
151 Creates the list of the items . This list is used for the
152 visualization of topic data as a table row.
153 @param name: the topic name
154 @type name: C{str}
155 @param root: The parent QStandardItem
156 @type root: U{QtGui.QStandardItem<https://srinikom.github.io/pyside-docs/PySide/QtGui/QStandardItem.html>}
157 @return: the list for the representation as a row
158 @rtype: C{[L{LaunchItem} or U{QtGui.QStandardItem<https://srinikom.github.io/pyside-docs/PySide/QtGui/QStandardItem.html>}, ...]}
159 '''
160 items = []
161 item = LaunchItem(name, path, item_id, parent=root)
162 items.append(item)
163 return items
164
166 '''
167 @return: C{True} if it is a launch file
168 @rtype: C{boolean}
169 '''
170 return self.path is not None and os.path.isfile(self.path) and self.path.endswith('.launch')
171
173 '''
174 @return: C{True} if it is a config file
175 @rtype: C{boolean}
176 '''
177 return self.id == self.CFG_FILE
178
180 '''
181 @return: C{True} if it is a node_manager profile file
182 @rtype: C{boolean}
183 '''
184 return self.path is not None and os.path.isfile(self.path) and self.path.endswith('.nmprofile')
185
188 '''
189 The model to manage the list with launch files.
190 '''
191 header = [('Name', -1)]
192 '''@ivar: the list with columns C{[(name, width), ...]}'''
193
195 '''
196 Creates a new list model.
197 '''
198 QStandardItemModel.__init__(self)
199 self.setColumnCount(len(LaunchListModel.header))
200 self.setHorizontalHeaderLabels([label for label, _width in LaunchListModel.header])
201 self.pyqt_workaround = dict()
202 self.items = []
203 self.DIR_CACHE = {}
204 self.currentPath = None
205 self.root_paths = [os.path.normpath(p) for p in os.getenv("ROS_PACKAGE_PATH").split(':')]
206 self._setNewList(self._moveUp(None))
207 self.__packages = {}
208 self._fill_packages_thread = PackagesThread()
209
214
217
218
219
220
221
223 '''
224 @param index: parent of the list
225 @type index: U{QtCore.QModelIndex<https://srinikom.github.io/pyside-docs/PySide/QtCore/QModelIndex.html>}
226 @return: Flag or the requested item
227 @rtype: U{QtCore.Qt.ItemFlag<https://srinikom.github.io/pyside-docs/PySide/QtCore/Qt.html>}
228 @see: U{http://www.pyside.org/docs/pyside-1.0.1/PySide/QtCore/Qt.html}
229 '''
230 if not index.isValid():
231 return Qt.NoItemFlags
232 try:
233 item = self.itemFromIndex(index)
234 result = Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled
235 if item.id in [LaunchItem.RECENT_FILE, LaunchItem.RECENT_PROFILE, LaunchItem.LAUNCH_FILE, LaunchItem.CFG_FILE, LaunchItem.FOLDER, LaunchItem.PROFILE]:
236 result = result | Qt.ItemIsEditable | Qt.ItemIsDragEnabled
237 return result
238 except:
239 return Qt.ItemIsEnabled | Qt.ItemIsSelectable
240
241
242
243
244
246 return ['text/plain']
247
249 mimeData = QMimeData()
250 text = ''
251 for index in indexes:
252 if index.isValid():
253 item = self.itemFromIndex(index)
254 prev = '%s\n' % text if text else ''
255 text = '%sfile://%s' % (prev, item.path)
256 mimeData.setData('text/plain', text.encode('utf-8'))
257 return mimeData
258
259
260
261
262
264 '''
265 Reloads the cached packag list.
266 '''
267 if not self._fill_packages_thread.isAlive():
268 self._fill_packages_thread = PackagesThread()
269 self._fill_packages_thread.packages.connect(self._fill_packages)
270 self._fill_packages_thread.start()
271
273 '''
274 Reloads the current path.
275 '''
276
277 try:
278 from .common import PACKAGE_CACHE
279 PACKAGE_CACHE.clear()
280 self.DIR_CACHE = {}
281 except:
282 import traceback
283 print traceback.format_exc(2)
284 try:
285 if self.currentPath is None:
286 self._setNewList(self._moveUp(self.currentPath))
287 else:
288 self._setNewList(self._moveDown(self.currentPath))
289 except:
290 self._setNewList(self._moveUp(None))
291
293 '''
294 Returns for the given item and path the file path if this is a file. Otherwise the
295 folder will be expanded and None will be returned.
296 @param path_item: the list item
297 @type path_item: C{str}
298 @param path: the real path of the item
299 @type path: C{str}
300 @return: path of the launch file or None
301 @rtype: C{str}
302 @raise Exception if no path to given item was found
303 '''
304 if path_item == '..':
305 goto_path = os.path.dirname(path)
306 key_mod = QApplication.keyboardModifiers()
307 if key_mod & Qt.ControlModifier:
308 goto_path = None
309 root_path, items = self._moveUp(goto_path)
310 elif os.path.isfile(path):
311 return path
312 elif item_id == LaunchItem.RECENT_FILE or item_id == LaunchItem.LAUNCH_FILE:
313 raise Exception("Invalid file path: %s", path)
314 else:
315 key_mod = QApplication.keyboardModifiers()
316 onestep = False
317 if key_mod & Qt.ControlModifier:
318 onestep = True
319 root_path, items = self._moveDown(path, onestep)
320 self._setNewList((root_path, items))
321 return None
322
324 '''
325 Shows the new path in the launch configuration view. Only if the new path
326 is in ros package paths
327 @param path: new path
328 @type path: C{str}
329 '''
330
331 self._setNewList(self._moveDown(path))
332
334 if show:
335
336 try:
337 items = []
338 for package, path in self.__packages.items():
339 items.append((package, path, LaunchItem.PACKAGE))
340 self._setNewList((self.currentPath if self.currentPath else '', items))
341 except:
342 import traceback
343 print traceback.format_exc(2)
344 else:
345 self._setNewList(self._moveUp(self.currentPath))
346
348 '''
349 Copy the file or folder to new position...
350 '''
351 if QApplication.clipboard().mimeData().hasText() and self.currentPath:
352 text = QApplication.clipboard().mimeData().text()
353 if text.startswith('file://'):
354 path = text.replace('file://', '')
355 basename = os.path.basename(text)
356 ok = True
357 if os.path.exists(os.path.join(self.currentPath, basename)):
358 basename, ok = QInputDialog.getText(None, 'File exists', 'New name (or override):', QLineEdit.Normal, basename)
359 if ok and basename:
360 if os.path.isdir(path):
361 shutil.copytree(path, os.path.join(self.currentPath, basename))
362 elif os.path.isfile(path):
363 shutil.copy2(path, os.path.join(self.currentPath, basename))
364 self.reloadCurrentPath()
365
367 '''
368 Copy the selected path to the clipboard
369 '''
370 mimeData = QMimeData()
371 text = ''
372 for index in indexes:
373 if index.isValid():
374 item = self.itemFromIndex(index)
375 prev = '%s\n' % text if text else ''
376 text = '%sfile://%s' % (prev, item.path)
377 mimeData.setData('text/plain', text.encode('utf-8'))
378 QApplication.clipboard().setMimeData(mimeData)
379
380
381
382
383
385 '''
386 Sets the list to the given path and insert the items. If the root path is not
387 None the additional item '..' to go up will be inserted. The items parameter
388 is a tupel with three values (the displayed name, the path of the item, the id
389 of the item).
390 @see: L{LaunchListModel._addPathToList()}
391 @param root_path: the root directory
392 @type root_path: C{str}
393 @param items: the list with characterized items
394 @type items: C{[(item, path, id)]}
395 '''
396 root = self.invisibleRootItem()
397 while root.rowCount():
398 root.removeRow(0)
399 self.pyqt_workaround.clear()
400
401 if root_path is not None:
402 self._addPathToList('..', root_path, LaunchItem.NOTHING)
403 for item_name, item_path, item_id in items:
404 self._addPathToList(item_name, item_path, item_id)
405 self.currentPath = root_path
406
408 '''
409 Test whether the given path is in ROS_PACKAGE_PATH.
410 @return: C{True}, if the path is in the ROS_PACKAGE_PATH
411 @rtype: C{boolean}
412 '''
413
414 for p in self.root_paths:
415 if path.startswith(p):
416 return True
417 return False
418
420 '''
421 Inserts the given item in the list model.
422 @param item: the displayed name
423 @type item: C{str}
424 @param path: the path of the item
425 @type path: C{str}
426 @param path_id: the id of the item, which represents whether it is a file, package or stack.
427 @type path_id: C{constants of LaunchListModel}
428 '''
429 root = self.invisibleRootItem()
430 if item is None or path is None or path_id == LaunchItem.NOT_FOUND:
431 return False
432 if (path_id != LaunchItem.NOT_FOUND):
433
434 try:
435 for i in range(root.rowCount()):
436 launchItem = root.child(i)
437 launch_file_cmp = (path_id in [LaunchItem.RECENT_FILE, LaunchItem.LAUNCH_FILE, LaunchItem.RECENT_PROFILE, LaunchItem.PROFILE] and item < launchItem.name)
438 launch_id_cmp = (launchItem.id > path_id and launchItem.id > LaunchItem.LAUNCH_FILE)
439 launch_name_cmp = (launchItem.id == path_id and item < launchItem.name)
440 if launch_file_cmp or launch_id_cmp or launch_name_cmp:
441 new_item_row = LaunchItem.getItemList(item, path, path_id, root)
442 root.insertRow(i, new_item_row)
443 self.pyqt_workaround[item] = new_item_row[0]
444 return True
445 new_item_row = LaunchItem.getItemList(item, path, path_id, root)
446 root.appendRow(new_item_row)
447 self.pyqt_workaround[item] = new_item_row[0]
448 return True
449 except:
450 import traceback
451 print traceback.format_exc(2)
452 return False
453
500
502 '''
503 Moves recursively down in the path tree and searches for a launch file. If
504 one is found True will be returned.
505 @return: C{True} if the path contains a launch file.
506 @rtype: C{boolean}
507 '''
508 fileList = os.listdir(path)
509 for cfile in fileList:
510 _, file_extension = os.path.splitext(cfile)
511 if os.path.isfile(os.path.join(path, cfile)) and (cfile.endswith('.launch')) or (file_extension in nm.settings().launch_view_file_ext):
512 return True
513 for cfile in fileList:
514 if os.path.isdir(os.path.join(path, cfile)):
515 if self._containsLaunches(os.path.join(path, cfile)):
516 return True
517 return False
518
520 '''
521 Moves recursively down in the path tree until the current path contains a
522 launch file.
523 @return: tupel of (root_path, items)
524 @rtype: C{tupel of (root_path, items)}
525 @see: L{LaunchListModel._setNewList()}
526 '''
527 result_list = []
528 dirlist = os.listdir(path)
529 for cfile in dirlist:
530 item = os.path.normpath(''.join([path, '/', cfile]))
531 pathItem = os.path.basename(item)
532 if pathItem == 'src':
533 pathItem = '%s (src)' % os.path.basename(os.path.dirname(item))
534 pathId = self._identifyPath(item)
535 if (pathId != LaunchItem.NOT_FOUND):
536 result_list.append((pathItem, item, pathId))
537 if len(result_list) == 1 and not os.path.isfile(result_list[0][1]):
538 if not onestep:
539 return self._moveDown(result_list[0][1])
540 return path, result_list
541
543 '''
544 Moves recursively up in the path tree until the current path contains a
545 launch file or the root path defined by ROS_PACKAGES_PATH is reached.
546 @return: tupel of (root_path, items)
547 @rtype: C{tupel of (root_path, items)}
548 @see: L{LaunchListModel._setNewList()}
549 '''
550 result_list = []
551 if path is None or not self._is_in_ros_packages(path):
552 dirlist = self._getRootItems()
553 path = None
554 else:
555 dirlist = os.listdir(path)
556 for dfile in dirlist:
557 item = os.path.normpath(os.path.join(path, dfile)) if path is not None else dfile
558 pathItem = os.path.basename(item)
559 if pathItem == 'src':
560 pathItem = '%s (src)' % os.path.basename(os.path.dirname(item))
561 pathId = self._identifyPath(item)
562 if (pathId != LaunchItem.NOT_FOUND):
563 result_list.append((pathItem, item, pathId))
564 if path is not None and len(result_list) == 1 and not os.path.isfile(result_list[0][1]):
565 return self._moveUp(os.path.dirname(path))
566 else:
567 self.currentPath = None
568 return path, result_list
569