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