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 import os
34 from python_qt_binding import QtGui
35 from python_qt_binding import QtCore
36
37 import roslib
38 import rospy
39 from xml_highlighter import XmlHighlighter
40 from yaml_highlighter import YamlHighlighter
41 from detailed_msg_box import WarningMessageBox
42
43 import node_manager_fkie as nm
44 from master_discovery_fkie.common import resolve_url
45 from common import package_name
46 from run_dialog import PackageDialog
47
49 '''
50 The XML editor to handle the included files. If an included file in the opened
51 launch file is detected, this can be open by STRG+(mouse click) in a new
52 editor.
53 '''
54
55 load_request_signal = QtCore.Signal(str)
56 ''' @ivar: A signal for request to open a configuration file'''
57
58 SUBSTITUTION_ARGS = ['env', 'optenv', 'find', 'anon', 'arg']
59 CONTEXT_FILE_EXT = ['.launch', '.test', '.xml']
60 YAML_VALIDATION_FILES = ['.yaml', '.iface', '.sync']
61
62 - def __init__(self, filename, parent=None):
63 self.parent = parent
64 QtGui.QTextEdit.__init__(self, parent)
65 self.setObjectName(' - '.join(['Editor', filename]))
66 font = QtGui.QFont()
67 font.setFamily("Fixed".decode("utf-8"))
68 font.setPointSize(12)
69 self.setFont(font)
70 self.setLineWrapMode(QtGui.QTextEdit.NoWrap)
71 self.setTabStopWidth(25)
72 self.setAcceptRichText(False)
73 self.setCursorWidth(2)
74 self.setFontFamily("courier new")
75 self.setProperty("backgroundVisible", True)
76 self.regexp_list = [QtCore.QRegExp("\\binclude\\b"), QtCore.QRegExp("\\btextfile\\b"),
77 QtCore.QRegExp("\\bfile\\b"), QtCore.QRegExp("\\bvalue=.*pkg:\/\/\\b"),
78 QtCore.QRegExp("\\bvalue=.*package:\/\/\\b"),
79 QtCore.QRegExp("\\bvalue=.*\$\(find\\b")]
80 self.filename = filename
81 self.file_info = None
82 if self.filename:
83 f = QtCore.QFile(filename);
84 if f.open(QtCore.QIODevice.ReadOnly | QtCore.QIODevice.Text):
85 self.file_info = QtCore.QFileInfo(filename)
86 self.setText(unicode(f.readAll(), "utf-8"))
87
88 self.path = '.'
89
90 self.setAcceptDrops(True)
91 if filename.endswith('.launch'):
92 self.hl = XmlHighlighter(self.document())
93 else:
94 self.hl = YamlHighlighter(self.document())
95
96
97
98
99 - def save(self, force=False):
100 '''
101 Saves changes to the file.
102 :return: saved, errors, msg
103 :rtype: bool, bool, str
104 '''
105 if force or self.document().isModified() or not QtCore.QFileInfo(self.filename).exists():
106 f = QtCore.QFile(self.filename)
107 if f.open(QtCore.QIODevice.WriteOnly | QtCore.QIODevice.Text):
108 f.write(self.toPlainText().encode('utf-8'))
109 self.document().setModified(False)
110 self.file_info = QtCore.QFileInfo(self.filename)
111
112 ext = os.path.splitext(self.filename)
113
114 if ext[1] in self.CONTEXT_FILE_EXT:
115 imported = False
116 try:
117 from lxml import etree
118 imported = True
119 parser = etree.XMLParser()
120 etree.fromstring(self.toPlainText().encode('utf-8'), parser)
121 except Exception as e:
122 if imported:
123 self.markLine(e.position[0])
124 return True, True, "%s"%e
125
126 elif ext[1] in self.YAML_VALIDATION_FILES:
127 try:
128 import yaml
129 yaml.load(self.toPlainText().encode('utf-8'))
130 except yaml.MarkedYAMLError as e:
131 return True, True, "%s"%e
132 return True, False, ''
133 else:
134 return False, True, "Cannot write XML file"
135 return False, False, ''
136
138 try:
139 cursor = self.textCursor()
140 cursor.setPosition(0, QtGui.QTextCursor.MoveAnchor)
141 while (cursor.block().blockNumber()+1 < no):
142 cursor.movePosition(QtGui.QTextCursor.NextBlock, QtGui.QTextCursor.MoveAnchor)
143 cursor.movePosition(QtGui.QTextCursor.EndOfBlock, QtGui.QTextCursor.KeepAnchor)
144 self.setTextCursor(cursor)
145 except:
146 pass
147
149 '''
150 Sets the current working path. This path is to open the included files, which
151 contains the relative path.
152 @param path: the path of the current opened file (without the file)
153 @type path: C{str}
154 '''
155 self.path = path
156
158 '''
159 Tries to determine the path of the included file. The statement of
160 C{$(find 'package')} will be resolved.
161 @param path: the sting which contains the included path
162 @type path: C{str}
163 @return: if no leading C{os.sep} is detected, the path setted by L{setCurrentPath()}
164 will be prepend. C{$(find 'package')} will be resolved. Otherwise the parameter
165 itself will be returned
166 @rtype: C{str}
167 '''
168 path = path.strip()
169 index = path.find('$')
170 if index > -1:
171 startIndex = path.find('(', index)
172 if startIndex > -1:
173 endIndex = path.find(')', startIndex+1)
174 script = path[startIndex+1:endIndex].split()
175 if len(script) == 2 and (script[0] == 'find'):
176 try:
177 pkg = roslib.packages.get_pkg_dir(script[1])
178 return os.path.normpath(''.join([pkg, '/', path[endIndex+1:]]))
179 except Exception as e:
180 rospy.logwarn(str(e))
181 else:
182 try:
183 return resolve_url(path)
184 except ValueError, e:
185 if len(path) > 0 and path[0] != '/':
186 return os.path.normpath(''.join([self.path, '/', path]))
187
188
189 return os.path.normpath(path)
190
192 '''
193 Searches in the given text for key indicates the including of a file and
194 return their index.
195 @param text: text to find
196 @type text: C{str}
197 @return: the index of the including key or -1
198 @rtype: C{int}
199 '''
200 for pattern in self.regexp_list:
201 index = pattern.indexIn(text)
202 if index > -1:
203 return index
204 return -1
205
207 '''
208 Returns all included files in the document.
209 '''
210 result = []
211 b = self.document().begin()
212 while b != self.document().end():
213 text = b.text()
214 index = self.index(text)
215 if index > -1:
216 startIndex = text.find('"', index)
217 if startIndex > -1:
218 endIndex = text.find('"', startIndex+1)
219 fileName = text[startIndex+1:endIndex]
220 if len(fileName) > 0:
221 try:
222 path = self.interpretPath(fileName)
223 f = QtCore.QFile(path)
224 ext = os.path.splitext(path)
225 if f.exists() and ext[1] in nm.settings().SEARCH_IN_EXT:
226 result.append(path)
227 except:
228 import traceback
229 print traceback.format_exc(1)
230 b = b.next()
231 return result
232
233 - def fileWithText(self, search_text):
234 '''
235 Searches for given text in this document and all included files.
236 @param search_text: text to find
237 @type search_text: C{str}
238 @return: the list with all files contain the text
239 @rtype: C{[str, ...]}
240 '''
241 result = []
242 start_pos = QtGui.QTextCursor()
243 search_result = self.document().find(search_text, start_pos.position()+1)
244 if not search_result.isNull():
245 result.append(self.filename)
246 inc_files = self.includedFiles()
247 for f in inc_files:
248 editor = Editor(f, None)
249 result[len(result):] = editor.fileWithText(search_text)
250 return result
251
253
254 try:
255 if self.filename and self.file_info:
256 if self.file_info.lastModified() != QtCore.QFileInfo(self.filename).lastModified():
257 self.file_info = QtCore.QFileInfo(self.filename)
258 result = QtGui.QMessageBox.question(self, "File changed", "File was changed, reload?", QtGui.QMessageBox.Yes | QtGui.QMessageBox.No)
259 if result == QtGui.QMessageBox.Yes:
260 f = QtCore.QFile(self.filename);
261 if f.open(QtCore.QIODevice.ReadOnly | QtCore.QIODevice.Text):
262 self.setText(unicode(f.readAll(), "utf-8"))
263 self.document().setModified(False)
264 self.textChanged.emit()
265 else:
266 QtGui.QMessageBox.critical(self, "Error", "Cannot open launch file%s"%self.filename)
267 except:
268 pass
269 QtGui.QTextEdit.focusInEvent(self, event)
270
272 '''
273 Opens the new editor, if the user clicked on the included file and sets the
274 default cursor.
275 '''
276 if event.modifiers() == QtCore.Qt.ControlModifier or event.modifiers() == QtCore.Qt.ShiftModifier:
277 cursor = self.cursorForPosition(event.pos())
278 index = self.index(cursor.block().text())
279 if index > -1:
280 startIndex = cursor.block().text().find('"', index)
281 if startIndex > -1:
282 endIndex = cursor.block().text().find('"', startIndex+1)
283 fileName = cursor.block().text()[startIndex+1:endIndex]
284 if len(fileName) > 0:
285 try:
286 qf = QtCore.QFile(self.interpretPath(fileName))
287 if not qf.exists():
288
289 result = QtGui.QMessageBox.question(self, "File not found", '\n\n'.join(["Create a new file?", qf.fileName()]), QtGui.QMessageBox.Yes | QtGui.QMessageBox.No)
290 if result == QtGui.QMessageBox.Yes:
291 d = os.path.dirname(qf.fileName())
292 if not os.path.exists(d):
293 os.makedirs(d)
294 with open(qf.fileName(),'w') as f:
295 if qf.fileName().endswith('.launch'):
296 f.write('<launch>\n\n</launch>')
297 self.load_request_signal.emit(qf.fileName())
298 else:
299 self.load_request_signal.emit(qf.fileName())
300 except Exception, e:
301 WarningMessageBox(QtGui.QMessageBox.Warning, "File not found %s"%fileName, str(e)).exec_()
302 QtGui.QTextEdit.mouseReleaseEvent(self, event)
303
305 '''
306 Sets the X{QtCore.Qt.PointingHandCursor} if the control key is pressed and
307 the mouse is over the included file.
308 '''
309 if event.modifiers() == QtCore.Qt.ControlModifier or event.modifiers() == QtCore.Qt.ShiftModifier:
310 cursor = self.cursorForPosition(event.pos())
311 index = self.index(cursor.block().text())
312 if index > -1:
313 self.viewport().setCursor(QtCore.Qt.PointingHandCursor)
314 else:
315 self.viewport().setCursor(QtCore.Qt.IBeamCursor)
316 else:
317 self.viewport().setCursor(QtCore.Qt.IBeamCursor)
318 QtGui.QTextEdit.mouseMoveEvent(self, event)
319
321 '''
322 Enable the mouse tracking by X{setMouseTracking()} if the control key is pressed.
323 '''
324 if event.key() == QtCore.Qt.Key_Control or event.key() == QtCore.Qt.Key_Shift:
325 self.setMouseTracking(True)
326 if event.modifiers() == QtCore.Qt.ControlModifier and event.key() == QtCore.Qt.Key_7:
327 self.commentText()
328 elif event.modifiers() == QtCore.Qt.AltModifier and event.key() == QtCore.Qt.Key_Space:
329 ext = os.path.splitext(self.filename)
330 if ext[1] in self.CONTEXT_FILE_EXT:
331 menu = self._create_context_substitution_menu()
332 if menu is None:
333 menu = self._create_context_tag_menu()
334 if menu:
335 menu.exec_(self.mapToGlobal(self.cursorRect().bottomRight()))
336 elif event.key() != QtCore.Qt.Key_Escape:
337
338 if event.key() == QtCore.Qt.Key_Tab:
339 self.shiftText()
340 else:
341 QtGui.QTextEdit.keyPressEvent(self, event)
342 else:
343 event.accept()
344 QtGui.QTextEdit.keyPressEvent(self, event)
345
347 '''
348 Disable the mouse tracking by X{setMouseTracking()} if the control key is
349 released and set the cursor back to X{QtCore.Qt.IBeamCursor}.
350 '''
351 if event.key() == QtCore.Qt.Key_Control or event.key() == QtCore.Qt.Key_Shift:
352 self.setMouseTracking(False)
353 self.viewport().setCursor(QtCore.Qt.IBeamCursor)
354 else:
355 event.accept()
356 QtGui.QTextEdit.keyReleaseEvent(self, event)
357
359 cursor = self.textCursor()
360 if not cursor.isNull():
361 cursor.beginEditBlock()
362 start = cursor.selectionStart()
363 end = cursor.selectionEnd()
364 cursor.setPosition(start)
365 block_start = cursor.blockNumber()
366 cursor.setPosition(end)
367 block_end = cursor.blockNumber()
368 if block_end-block_start > 0 and end-cursor.block().position() <= 0:
369
370 block_end -= 1
371 cursor.setPosition(start, QtGui.QTextCursor.MoveAnchor)
372 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
373 start = cursor.position()
374 while (cursor.block().blockNumber() < block_end+1):
375 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
376 ext = os.path.splitext(self.filename)
377
378 if ext[1] in self.CONTEXT_FILE_EXT:
379 if cursor.block().length() < 4:
380 cursor.movePosition(QtGui.QTextCursor.NextBlock)
381 continue
382 cursor.movePosition(QtGui.QTextCursor.NextCharacter, QtGui.QTextCursor.KeepAnchor, 4)
383
384 if cursor.selectedText() == '<!--':
385 cursor.insertText('')
386 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
387 cursor.movePosition(QtGui.QTextCursor.PreviousCharacter, QtGui.QTextCursor.KeepAnchor, 3)
388 if cursor.selectedText() == '-->':
389 cursor.insertText('')
390 else:
391 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
392 cursor.movePosition(QtGui.QTextCursor.EndOfLine, QtGui.QTextCursor.KeepAnchor)
393
394 if cursor.selectedText().find('<!--') < 0 and cursor.selectedText().find('-->') < 0:
395 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
396 cursor.insertText('<!--')
397 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
398 cursor.insertText('-->')
399 else:
400 if cursor.block().length() < 2:
401 cursor.movePosition(QtGui.QTextCursor.NextBlock)
402 continue
403 cursor.movePosition(QtGui.QTextCursor.NextCharacter, QtGui.QTextCursor.KeepAnchor, 2)
404
405 if cursor.selectedText() == '# ':
406 cursor.insertText('')
407 else:
408 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
409 cursor.insertText('# ')
410 cursor.movePosition(QtGui.QTextCursor.NextBlock)
411
412 cursor.endEditBlock()
413 cursor.setPosition(start, QtGui.QTextCursor.MoveAnchor)
414 cursor.movePosition(QtGui.QTextCursor.StartOfBlock, QtGui.QTextCursor.MoveAnchor)
415 while (cursor.block().blockNumber() < block_end):
416 cursor.movePosition(QtGui.QTextCursor.NextBlock, QtGui.QTextCursor.KeepAnchor)
417 cursor.movePosition(QtGui.QTextCursor.EndOfBlock, QtGui.QTextCursor.KeepAnchor)
418
419 self.setTextCursor(cursor)
420
421 - def shiftText(self):
422 '''
423 Increase (Decrease) indentation using Tab (Ctrl+Tab).
424 '''
425 cursor = self.textCursor()
426 if not cursor.isNull():
427 key_mod = QtGui.QApplication.keyboardModifiers()
428
429 cursor.beginEditBlock()
430 start = cursor.selectionStart()
431 end = cursor.selectionEnd()
432 cursor.setPosition(start)
433 block_start = cursor.blockNumber()
434 cursor.setPosition(end)
435 block_end = cursor.blockNumber()
436 if block_end-block_start == 0:
437
438 if key_mod & QtCore.Qt.ControlModifier or key_mod & QtCore.Qt.ShiftModifier:
439 for _ in range(2):
440 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
441 cursor.movePosition(QtGui.QTextCursor.NextCharacter, QtGui.QTextCursor.KeepAnchor, 1)
442 if cursor.selectedText() == ' ':
443 cursor.insertText('')
444 elif cursor.selectedText() == "\t":
445 cursor.insertText('')
446 break
447 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
448 else:
449
450 cursor.movePosition(QtGui.QTextCursor.NextCharacter, QtGui.QTextCursor.KeepAnchor, end-start)
451 cursor.insertText(' ')
452 else:
453
454 if key_mod & QtCore.Qt.ControlModifier or key_mod & QtCore.Qt.ShiftModifier:
455 removed = 0
456 for i in reversed(range(start, end)):
457 cursor.setPosition(i)
458 if cursor.atBlockStart():
459 cursor.movePosition(QtGui.QTextCursor.NextCharacter, QtGui.QTextCursor.KeepAnchor, 2)
460 if cursor.selectedText() == ' ':
461 cursor.insertText('')
462 removed += 2
463 else:
464 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
465 cursor.movePosition(QtGui.QTextCursor.NextCharacter, QtGui.QTextCursor.KeepAnchor, 1)
466 if cursor.selectedText() == ' ':
467 cursor.insertText('')
468 removed += 1
469 elif cursor.selectedText() == "\t":
470 cursor.insertText('')
471 removed += 1
472 cursor.setPosition(start)
473 cursor.movePosition(QtGui.QTextCursor.NextCharacter, QtGui.QTextCursor.KeepAnchor, end-start-removed)
474 else:
475
476 inserted = 0
477 for i in reversed(range(start, end)):
478 cursor.setPosition(i)
479 if cursor.atBlockStart():
480 cursor.insertText(' ')
481 inserted += 2
482 cursor.setPosition(start)
483 cursor.movePosition(QtGui.QTextCursor.NextCharacter, QtGui.QTextCursor.KeepAnchor, end-start+inserted)
484 self.setTextCursor(cursor)
485 cursor.endEditBlock()
486
487
488
489
490
492 if e.mimeData().hasFormat('text/plain'):
493 e.accept()
494 else:
495 e.ignore()
496
499
501 cursor = self.cursorForPosition(e.pos())
502 if not cursor.isNull():
503 text = e.mimeData().text()
504
505 if text.startswith('file://'):
506 text = text[7:]
507 if os.path.exists(text) and os.path.isfile(text):
508
509 (package, path) = package_name(os.path.dirname(text))
510 if text.endswith('.launch'):
511 if package:
512 cursor.insertText('<include file="$(find %s)%s" />'%(package, text.replace(path, '')))
513 else:
514 cursor.insertText('<include file="%s" />'%text)
515 else:
516 if package:
517 cursor.insertText('<rosparam file="$(find %s)%s" command="load" />'%(package, text.replace(path, '')))
518 else:
519 cursor.insertText('<rosparam file="%s" command="load" />'%text)
520 else:
521 cursor.insertText(e.mimeData().text())
522 e.accept()
523
524
525
526
527
529 parent_tag, inblock, attrs = self._get_parent_tag()
530 if not parent_tag:
531 return None
532 menu = QtGui.QMenu(self)
533 menu.triggered.connect(self._context_activated)
534 text = self.toPlainText()
535 pos = self.textCursor().position() - 1
536 try:
537 if not inblock:
538
539 attributes = sorted(list((set(XmlHighlighter.LAUNCH_ATTR[parent_tag]) - set(attrs))))
540 for attr in attributes:
541 action = menu.addAction(attr.rstrip('='))
542 action.setData('%s"'%attr if text[pos] == ' ' else ' %s"'%attr)
543 else:
544
545 tags = sorted(XmlHighlighter.LAUNCH_CHILDS[parent_tag])
546 if not tags:
547 return None
548 for tag in tags:
549 data = '<%s></%s>'%(tag, tag) if XmlHighlighter.LAUNCH_CHILDS[tag] else '<%s/>'%tag
550 if text[pos] == '<':
551 data = data[1:]
552 action = menu.addAction(tag)
553 action.setData(data)
554 except:
555
556
557 return None
558 return menu
559
561 text = self.toPlainText()
562 pos = self.textCursor().position() - 1
563 try:
564 if text[pos] == '$' or (text[pos] == '(' and text[pos-1] == '$'):
565 menu = QtGui.QMenu(self)
566 menu.triggered.connect(self._context_activated)
567 for arg in self.SUBSTITUTION_ARGS:
568 action = menu.addAction("%s"%arg)
569 action.setData("(%s"%arg if text[pos] == '$' else "%s"%arg)
570 return menu
571 except:
572 pass
573 return None
574
576 text = self.toPlainText()
577 pos = self.textCursor().position() - 1
578
579 try:
580 if not (text[pos] in [' ', '<', '>', '"', '\n'] or text[pos+1] in [' ', '<','"', '\n']):
581 return '', False, []
582 except:
583 pass
584 instr = (text[:pos+1].count('"') % 2)
585
586 if instr:
587 return '', False, []
588
589 closed_tags = []
590 current_attr = []
591 tag_reading = True
592 tag = ''
593 attr_reading = False
594 attr = ''
595 closed_gts = False
596
597 i = pos
598 while i >= 0:
599 if text[i] == '"':
600 instr = not instr
601 elif not instr:
602
603 if text[i] == '=':
604 attr_reading = True
605 elif text[i] in ['<', '/', '>']:
606 if text[i] == '>':
607 closed_gts = True
608 tag = ''
609 elif text[i] == '/' and closed_gts:
610 closed_gts = False
611 closed_tags.append(tag if tag else '/')
612 tag = ''
613 elif text[i] == '<':
614 if closed_tags and (tag == closed_tags[-1] or closed_tags[-1] == '/'):
615 closed_tags.pop()
616 current_attr = []
617 tag = ''
618 elif tag:
619 return tag[::-1], closed_gts, current_attr
620
621 elif text[i] == ' ':
622 if attr_reading and attr:
623 current_attr.append("%s="%attr[::-1])
624 attr_reading = False
625 attr = ''
626 tag = ''
627 tag_reading = True
628 else:
629 if tag_reading or closed_gts:
630 tag += text[i]
631 if attr_reading:
632 attr += text[i]
633 i -= 1
634 return '', False, []
635
636 - def _context_activated(self, arg):
637 cursor = self.textCursor()
638 if not cursor.isNull():
639 cursor.insertText(arg.data())
640
641
643 '''
644 A dialog to find text in the Editor.
645 '''
646
648 QtGui.QDialog.__init__(self, parent)
649 self.setObjectName('FindDialog')
650 self.setWindowTitle('Search')
651 self.verticalLayout = QtGui.QVBoxLayout(self)
652 self.verticalLayout.setObjectName("verticalLayout")
653
654 self.content = QtGui.QWidget(self)
655 self.contentLayout = QtGui.QFormLayout(self.content)
656
657
658 self.contentLayout.setContentsMargins(0, 0, 0, 0)
659 self.verticalLayout.addWidget(self.content)
660
661 label = QtGui.QLabel("Find:", self.content)
662 self.search_field = QtGui.QLineEdit(self.content)
663 self.contentLayout.addRow(label, self.search_field)
664 replace_label = QtGui.QLabel("Replace:", self.content)
665 self.replace_field = QtGui.QLineEdit(self.content)
666 self.contentLayout.addRow(replace_label, self.replace_field)
667 self.recursive = QtGui.QCheckBox("recursive search")
668 self.contentLayout.addRow(self.recursive)
669 self.result_label = QtGui.QLabel("")
670 self.verticalLayout.addWidget(self.result_label)
671 self.found_files = QtGui.QListWidget()
672 self.found_files.setVisible(False)
673 self.found_files.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
674 self.verticalLayout.addWidget(self.found_files)
675
676 self.buttonBox = QtGui.QDialogButtonBox(self)
677 self.find_button = QtGui.QPushButton(self.tr("&Find"))
678 self.find_button.setDefault(True)
679 self.buttonBox.addButton(self.find_button, QtGui.QDialogButtonBox.ActionRole)
680 self.replace_button = QtGui.QPushButton(self.tr("&Replace/Find"))
681 self.buttonBox.addButton(self.replace_button, QtGui.QDialogButtonBox.ActionRole)
682 self.buttonBox.addButton(QtGui.QDialogButtonBox.Close)
683 self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
684 self.buttonBox.setObjectName("buttonBox")
685 self.verticalLayout.addWidget(self.buttonBox)
686
687
688 QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL("rejected()"), self.reject)
689 QtCore.QMetaObject.connectSlotsByName(self)
690
691 self.search_text = ''
692 self.search_pos = QtGui.QTextCursor()
693
694
695
696
697
699 '''
700 Creates a dialog to edit a launch file.
701 '''
702 finished_signal = QtCore.Signal(list)
703 '''
704 finished_signal has as parameter the filenames of the initialization and is emitted, if this
705 dialog was closed.
706 '''
707
708 - def __init__(self, filenames, search_text='', parent=None):
709 '''
710 @param filenames: a list with filenames. The last one will be activated.
711 @type filenames: C{[str, ...]}
712 @param search_text: if not empty, searches in new document for first occurrence of the given text
713 @type search_text: C{str} (Default: C{Empty String})
714 '''
715 QtGui.QDialog.__init__(self, parent)
716 self.setObjectName(' - '.join(['xmlEditor', str(filenames)]))
717 self.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
718 self.setWindowFlags(QtCore.Qt.Window)
719 self.mIcon = QtGui.QIcon(":/icons/crystal_clear_edit_launch.png")
720 self._error_icon = QtGui.QIcon(":/icons/crystal_clear_warning.png")
721 self._empty_icon = QtGui.QIcon()
722 self.setWindowIcon(self.mIcon)
723 self.setWindowTitle("ROSLaunch Editor");
724
725 self.init_filenames = list(filenames)
726
727 self.files = []
728 '''@ivar: list with all open files '''
729
730
731 self.verticalLayout = QtGui.QVBoxLayout(self)
732 self.verticalLayout.setContentsMargins(0, 0, 0, 0)
733 self.verticalLayout.setObjectName("verticalLayout")
734 self.tabWidget = QtGui.QTabWidget(self)
735 self.tabWidget.setTabPosition(QtGui.QTabWidget.North)
736 self.tabWidget.setDocumentMode(True)
737 self.tabWidget.setTabsClosable(True)
738 self.tabWidget.setMovable(False)
739 self.tabWidget.setObjectName("tabWidget")
740 self.tabWidget.tabCloseRequested.connect(self.on_close_tab)
741 self.verticalLayout.addWidget(self.tabWidget)
742
743
744 self.buttons = QtGui.QWidget(self)
745 self.horizontalLayout = QtGui.QHBoxLayout(self.buttons)
746 self.horizontalLayout.setContentsMargins(4, 0, 4, 0)
747 self.horizontalLayout.setObjectName("horizontalLayout")
748
749 self.searchButton = QtGui.QPushButton(self)
750 self.searchButton.setObjectName("searchButton")
751 self.searchButton.clicked.connect(self.on_shortcut_find)
752 self.searchButton.setText(QtGui.QApplication.translate("XmlEditor", "Search", None, QtGui.QApplication.UnicodeUTF8))
753 self.searchButton.setShortcut(QtGui.QApplication.translate("XmlEditor", "Ctrl+F", None, QtGui.QApplication.UnicodeUTF8))
754 self.searchButton.setToolTip('Open a search dialog (Ctrl+F)')
755 self.horizontalLayout.addWidget(self.searchButton)
756
757 self.gotoButton = QtGui.QPushButton(self)
758 self.gotoButton.setObjectName("gotoButton")
759 self.gotoButton.clicked.connect(self.on_shortcut_goto)
760 self.gotoButton.setText(QtGui.QApplication.translate("XmlEditor", "Goto line", None, QtGui.QApplication.UnicodeUTF8))
761 self.gotoButton.setShortcut(QtGui.QApplication.translate("XmlEditor", "Ctrl+L", None, QtGui.QApplication.UnicodeUTF8))
762 self.gotoButton.setToolTip('Open a goto dialog (Ctrl+L)')
763 self.horizontalLayout.addWidget(self.gotoButton)
764
765 self.tagButton = self._create_tag_button(self)
766 self.horizontalLayout.addWidget(self.tagButton)
767
768
769 spacerItem = QtGui.QSpacerItem(515, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
770 self.horizontalLayout.addItem(spacerItem)
771
772 self.pos_label = QtGui.QLabel()
773 self.horizontalLayout.addWidget(self.pos_label)
774
775 spacerItem = QtGui.QSpacerItem(515, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
776 self.horizontalLayout.addItem(spacerItem)
777
778 self.saveButton = QtGui.QPushButton(self)
779 self.saveButton.setObjectName("saveButton")
780 self.saveButton.clicked.connect(self.on_saveButton_clicked)
781 self.saveButton.setText(QtGui.QApplication.translate("XmlEditor", "Save", None, QtGui.QApplication.UnicodeUTF8))
782 self.saveButton.setShortcut(QtGui.QApplication.translate("XmlEditor", "Ctrl+S", None, QtGui.QApplication.UnicodeUTF8))
783 self.saveButton.setToolTip('Save the changes to the file (Ctrl+S)')
784 self.horizontalLayout.addWidget(self.saveButton)
785 self.verticalLayout.addWidget(self.buttons)
786
787
788 self.find_dialog = FindDialog(self)
789 self.find_dialog.buttonBox.clicked.connect(self.on_find_dialog_clicked)
790 self.find_dialog.found_files.itemActivated.connect(self.find_dialog_itemActivated)
791
792
793
794
795
796 for f in filenames:
797 if f:
798 self.on_load_request(os.path.normpath(f), search_text)
799
800 self.readSettings()
801
802
803
804
816
825
827 '''
828 Loads a file in a new tab or focus the tab, if the file is already open.
829 @param filename: the path to file
830 @type filename: C{str}
831 @param search_text: if not empty, searches in new document for first occurrence of the given text
832 @type search_text: C{str} (Default: C{Empty String})
833 '''
834 if not filename:
835 return
836
837 self.tabWidget.setUpdatesEnabled(False)
838 try:
839 if not filename in self.files:
840 tab_name = self.__getTabName(filename)
841 editor = Editor(filename, self.tabWidget)
842 tab_index = self.tabWidget.addTab(editor, tab_name)
843 self.files.append(filename)
844 editor.setCurrentPath(os.path.basename(filename))
845 editor.load_request_signal.connect(self.on_load_request)
846 editor.textChanged.connect(self.on_editor_textChanged)
847 editor.cursorPositionChanged.connect(self.on_editor_positionChanged)
848 editor.setFocus(QtCore.Qt.OtherFocusReason)
849 self.tabWidget.setCurrentIndex(tab_index)
850 else:
851 for i in range(self.tabWidget.count()):
852 if self.tabWidget.widget(i).filename == filename:
853 self.tabWidget.setCurrentIndex(i)
854 break
855 except:
856 import traceback
857 rospy.logwarn("Error while open %s: %s", filename, traceback.format_exc(1))
858 self.tabWidget.setUpdatesEnabled(True)
859 if search_text:
860 if self.find(search_text, False):
861 if not self.find_dialog.search_pos.isNull():
862 self.tabWidget.currentWidget().moveCursor(QtGui.QTextCursor.StartOfLine)
863
865 '''
866 Signal handling to close single tabs.
867 @param tab_index: tab index to close
868 @type tab_index: C{int}
869 '''
870 try:
871 doremove = True
872 w = self.tabWidget.widget(tab_index)
873 if w.document().isModified():
874 name = self.__getTabName(w.filename)
875 result = QtGui.QMessageBox.question(self, "Unsaved Changes", '\n\n'.join(["Save the file before closing?", name]), QtGui.QMessageBox.Yes | QtGui.QMessageBox.No | QtGui.QMessageBox.Cancel)
876 if result == QtGui.QMessageBox.Yes:
877 self.tabWidget.currentWidget().save()
878 elif result == QtGui.QMessageBox.No:
879 pass
880 else:
881 doremove = False
882 if doremove:
883
884 if w.filename in self.files:
885 self.files.remove(w.filename)
886
887 self.tabWidget.removeTab(tab_index)
888
889 if not self.tabWidget.count():
890 self.close()
891 except:
892 import traceback
893 rospy.logwarn("Error while close tab %s: %s", str(tab_index), traceback.format_exc(1))
894
895
896
897
900
902 '''
903 Test the open files for changes and save this if needed.
904 '''
905 changed = []
906
907 for i in range(self.tabWidget.count()):
908 w = self.tabWidget.widget(i)
909 if w.document().isModified():
910 changed.append(self.__getTabName(w.filename))
911 if changed:
912
913 if self.isHidden():
914 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
915 else:
916 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No | QtGui.QMessageBox.Cancel
917 result = QtGui.QMessageBox.question(self, "Unsaved Changes", '\n\n'.join(["Save the file before closing?", '\n'.join(changed)]), buttons)
918 if result == QtGui.QMessageBox.Yes:
919 for i in range(self.tabWidget.count()):
920 w = self.tabWidget.widget(i).save()
921 event.accept()
922 elif result == QtGui.QMessageBox.No:
923 event.accept()
924 else:
925 event.ignore()
926 else:
927 event.accept()
928 if event.isAccepted():
929 self.storeSetting()
930 self.finished_signal.emit(self.init_filenames)
931
946
948 '''
949 If the content was changed, a '*' will be shown in the tab name.
950 '''
951 tab_name = self.__getTabName(self.tabWidget.currentWidget().filename)
952 if (self.tabWidget.currentWidget().document().isModified()) or not QtCore.QFileInfo(self.tabWidget.currentWidget().filename).exists():
953 tab_name = ''.join(['*', tab_name])
954 self.tabWidget.setTabText(self.tabWidget.currentIndex(), tab_name)
955
957 '''
958 Shows the number of the line and column in a label.
959 '''
960 cursor = self.tabWidget.currentWidget().textCursor()
961 self.pos_label.setText('%s:%s'%(cursor.blockNumber()+1, cursor.columnNumber()))
962
964 '''
965 Opens a find dialog.
966 '''
967 self.find_dialog.show()
968 self.find_dialog.raise_()
969 self.find_dialog.activateWindow()
970
972 '''
973 Opens a C{goto} dialog.
974 '''
975 value = 1
976 ok = False
977 try:
978 value, ok = QtGui.QInputDialog.getInt(self, "Goto",
979 self.tr("Line number:"), QtGui.QLineEdit.Normal,
980 minValue=1, step=1)
981 except:
982 value, ok = QtGui.QInputDialog.getInt(self, "Goto",
983 self.tr("Line number:"), QtGui.QLineEdit.Normal,
984 min=1, step=1)
985 if ok:
986 if value > self.tabWidget.currentWidget().document().blockCount():
987 value = self.tabWidget.currentWidget().document().blockCount()
988 curpos = self.tabWidget.currentWidget().textCursor().blockNumber()+1
989 while curpos != value:
990 mov = QtGui.QTextCursor.NextBlock if curpos < value else QtGui.QTextCursor.PreviousBlock
991 self.tabWidget.currentWidget().moveCursor(mov)
992 curpos = self.tabWidget.currentWidget().textCursor().blockNumber()+1
993 self.tabWidget.currentWidget().setFocus(QtCore.Qt.ActiveWindowFocusReason)
994
996 base = os.path.basename(lfile).replace('.launch', '')
997 (package, _) = package_name(os.path.dirname(lfile))
998 return ''.join([str(base), ' [', str(package),']'])
999
1001 '''
1002 Method to handle the button actions of the C{find dialog}.
1003 '''
1004 if button == self.find_dialog.find_button:
1005 self.find(self.find_dialog.search_field.text(), self.find_dialog.recursive.isChecked())
1006 elif button == self.find_dialog.replace_button:
1007 self.find_dialog.recursive.setChecked(False)
1008 cursor = self.tabWidget.currentWidget().textCursor()
1009 if self.find_dialog.search_field.text() and cursor.selectedText() == self.find_dialog.search_field.text():
1010 cursor.insertText(self.find_dialog.replace_field.text())
1011 currentLine = str(cursor.blockNumber()+1)
1012 self.find_dialog.result_label.setText(''.join(["'", self.find_dialog.search_text, "'", ' replaced at line: ', currentLine, ' by ', "'", self.find_dialog.replace_field.text(),"'"]))
1013 self.tabWidget.currentWidget().setTextCursor(cursor)
1014 self.find(self.find_dialog.search_field.text(), self.find_dialog.recursive.isChecked())
1015
1016 - def find(self, search_text, recursive):
1017 '''
1018 Searches for text in the current text editor. If `recursive` is C{True},
1019 the included files will be searched.
1020 @param search_text: text to find
1021 @type search_text: C{str}
1022 @param recursive: search in included files if this is C{True}
1023 @type recursive: C{bool}
1024 '''
1025 found = False
1026 if self.find_dialog.search_text != search_text:
1027 self.find_dialog.search_pos = QtGui.QTextCursor()
1028 self.find_dialog.found_files.clear()
1029 self.find_dialog.found_files.setVisible(False)
1030 self.find_dialog.result_label.setText(''.join(["'", search_text, "'", ' not found!']))
1031 self.find_dialog.search_text = search_text
1032 if search_text:
1033 if recursive:
1034 files = self.tabWidget.currentWidget().fileWithText(search_text)
1035 items = list(set(files))
1036 self.find_dialog.result_label.setText(''.join(["'", search_text, "'", ' found in ', str(len(items)), ' files:']))
1037 self.find_dialog.found_files.clear()
1038 self.find_dialog.found_files.addItems(items)
1039 self.find_dialog.found_files.setVisible(True)
1040
1041 found = True
1042 else:
1043 tmp_pos = self.find_dialog.search_pos
1044 self.find_dialog.search_pos = self.tabWidget.currentWidget().document().find(search_text, self.find_dialog.search_pos.position()+1)
1045 if self.find_dialog.search_pos.isNull() and not tmp_pos.isNull():
1046 self.find_dialog.search_pos = self.tabWidget.currentWidget().document().find(search_text, self.find_dialog.search_pos.position()+1)
1047
1048 if not self.find_dialog.search_pos.isNull():
1049 self.tabWidget.currentWidget().setTextCursor(self.find_dialog.search_pos)
1050 currentTabName = self.tabWidget.tabText(self.tabWidget.currentIndex())
1051 currentLine = str(self.tabWidget.currentWidget().textCursor().blockNumber()+1)
1052 self.find_dialog.result_label.setText(''.join(["'", search_text, "'", ' found at line: ', currentLine, ' in ', "'", currentTabName,"'"]))
1053 found = True
1054 else:
1055 self.find_dialog.result_label.setText(''.join(["'", search_text, "'", ' not found!']))
1056 return found
1057
1059 '''
1060 On recursive search all files contained the search text are listed. If one of
1061 this file is activated, it will be open in a new tab and the cursor moved to
1062 the C{search text} position.
1063 @param item: The activated item of the C{QListWidget}
1064 @type item: L{PySide.QtGui.QListWidgetItem}
1065 '''
1066 self.find_dialog.recursive.setChecked(False)
1067 self.on_load_request(item.text(), self.find_dialog.search_text)
1068 self.find(self.find_dialog.search_text, False)
1069 currentTabName = self.tabWidget.tabText(self.tabWidget.currentIndex())
1070 currentLine = str(self.tabWidget.currentWidget().textCursor().blockNumber()+1)
1071 self.find_dialog.result_label.setText(''.join(["'", self.find_dialog.search_text, "'", ' found at line: ', currentLine, ' in ', "'", currentTabName,"'"]))
1072
1073
1074
1075
1076
1132
1133 - def _insert_text(self, text):
1134 cursor = self.tabWidget.currentWidget().textCursor()
1135 if not cursor.isNull():
1136 col = cursor.columnNumber()
1137 spaces = ''.join([' ' for _ in range(col)])
1138 cursor.insertText(text.replace('\n','\n%s'%spaces))
1139 self.tabWidget.currentWidget().setFocus(QtCore.Qt.OtherFocusReason)
1140
1142 self._insert_text('<group ns="namespace" clear_params="true|false">\n'
1143 '</group>')
1144
1146 dia = PackageDialog()
1147 if dia.exec_():
1148 self._insert_text('<node name="%s" pkg="%s" type="%s">\n'
1149 '</node>'%(dia.binary, dia.package, dia.binary))
1150
1152 dia = PackageDialog()
1153 if dia.exec_():
1154 self._insert_text('<node name="%s" pkg="%s" type="%s"\n'
1155 ' args="arg1" machine="machine-name"\n'
1156 ' respawn="true" required="true"\n'
1157 ' ns="foo" clear_params="true|false"\n'
1158 ' output="log|screen" cwd="ROS_HOME|node"\n'
1159 ' launch-prefix="prefix arguments">\n'
1160 '</node>'%(dia.binary, dia.package, dia.binary))
1161
1163 self._insert_text('<include file="$(find pkg-name)/path/filename.xml"\n'
1164 ' ns="foo" clear_params="true|false">\n'
1165 '</include>')
1166
1168 self._insert_text('<remap from="original" to="new"/>')
1169
1171 self._insert_text('<env name="variable" value="value"/>')
1172
1174 self._insert_text('<param name="namespace/name" value="value" />')
1175
1177 self._insert_text('<param name="capability_group" value="demo" />')
1178
1180 self._insert_text('<param name="namespace/name" value="value"\n'
1181 ' type="str|int|double|bool"\n'
1182 ' textfile="$(find pkg-name)/path/file.txt"\n'
1183 ' binfile="$(find pkg-name)/path/file"\n'
1184 ' command="$(find pkg-name)/exe \'$(find pkg-name)/arg.txt\'">\n'
1185 '</param>')
1186
1188 self._insert_text('<rosparam param="param-name"\n'
1189 ' file="$(find pkg-name)/path/foo.yaml"\n'
1190 ' command="load|dump|delete"\n'
1191 ' ns="namespace">\n'
1192 '</rosparam>')
1193
1195 self._insert_text('<arg name="foo" default="1" />')
1196
1198 self._insert_text('<arg name="foo" value="bar" />')
1199
1201 dia = PackageDialog()
1202 if dia.exec_():
1203 self._insert_text('<test name="%s" pkg="%s" type="%s" test-name="test_%s">\n'
1204 '</test>'%(dia.binary, dia.package, dia.binary, dia.binary))
1205
1207 dia = PackageDialog()
1208 if dia.exec_():
1209 self._insert_text('<test name="%s" pkg="%s" type="%s" test-name="test_%s">\n'
1210 ' args="arg1" time-limit="60.0"\n'
1211 ' ns="foo" clear_params="true|false"\n'
1212 ' cwd="ROS_HOME|node" retry="0"\n'
1213 ' launch-prefix="prefix arguments">\n'
1214 '</test>'%(dia.binary, dia.package, dia.binary, dia.binary))
1215