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 QFileInfo, QPoint, QSize, Qt, Signal
34 from python_qt_binding.QtGui import QIcon, QKeySequence, QTextCursor, QTextDocument
35 import os
36
37 import rospy
38
39 from node_manager_fkie.common import package_name
40 from node_manager_fkie.run_dialog import PackageDialog
41 import node_manager_fkie as nm
42
43 from .line_number_widget import LineNumberWidget
44 from .text_edit import TextEdit
45 from .text_search_frame import TextSearchFrame
46 from .text_search_thread import TextSearchThread
47
48 try:
49 from python_qt_binding.QtGui import QApplication, QAction, QLineEdit, QMessageBox, QWidget, QMainWindow
50 from python_qt_binding.QtGui import QDialog, QInputDialog, QLabel, QMenu, QPushButton, QTabWidget
51 from python_qt_binding.QtGui import QHBoxLayout, QVBoxLayout, QSpacerItem, QSplitter, QSizePolicy
52 except:
53 from python_qt_binding.QtWidgets import QApplication, QAction, QLineEdit, QMessageBox, QWidget, QMainWindow
54 from python_qt_binding.QtWidgets import QDialog, QInputDialog, QLabel, QMenu, QPushButton, QTabWidget
55 from python_qt_binding.QtWidgets import QHBoxLayout, QVBoxLayout, QSpacerItem, QSplitter, QSizePolicy
56
57
83
84
86 '''
87 Creates a dialog to edit a launch file.
88 '''
89 finished_signal = Signal(list)
90 '''
91 finished_signal has as parameter the filenames of the initialization and is emitted, if this
92 dialog was closed.
93 '''
94
95 - def __init__(self, filenames, search_text='', parent=None):
96 '''
97 @param filenames: a list with filenames. The last one will be activated.
98 @type filenames: C{[str, ...]}
99 @param search_text: if not empty, searches in new document for first occurrence of the given text
100 @type search_text: C{str} (Default: C{Empty String})
101 '''
102 QMainWindow.__init__(self, parent)
103 self.setObjectName(' - '.join(['Editor', str(filenames)]))
104 self.setAttribute(Qt.WA_DeleteOnClose, True)
105 self.setWindowFlags(Qt.Window)
106 self.mIcon = QIcon(":/icons/crystal_clear_edit_launch.png")
107 self._error_icon = QIcon(":/icons/crystal_clear_warning.png")
108 self._empty_icon = QIcon()
109 self.setWindowIcon(self.mIcon)
110 window_title = "ROSLaunch Editor"
111 if filenames:
112 window_title = self.__getTabName(filenames[0])
113 self.setWindowTitle(window_title)
114 self.init_filenames = list(filenames)
115 self._search_thread = None
116
117 self.files = []
118
119 self.main_widget = QWidget(self)
120 self.verticalLayout = QVBoxLayout(self.main_widget)
121 self.verticalLayout.setContentsMargins(0, 0, 0, 0)
122 self.verticalLayout.setSpacing(1)
123 self.verticalLayout.setObjectName("verticalLayout")
124
125 self.tabWidget = EditorTabWidget(self)
126 self.tabWidget.setTabPosition(QTabWidget.North)
127 self.tabWidget.setDocumentMode(True)
128 self.tabWidget.setTabsClosable(True)
129 self.tabWidget.setMovable(False)
130 self.tabWidget.setObjectName("tabWidget")
131 self.tabWidget.tabCloseRequested.connect(self.on_close_tab)
132
133 self.verticalLayout.addWidget(self.tabWidget)
134 self.buttons = self._create_buttons()
135 self.verticalLayout.addWidget(self.buttons)
136 self.setCentralWidget(self.main_widget)
137
138 self.find_dialog = TextSearchFrame(self.tabWidget, self)
139 self.find_dialog.search_result_signal.connect(self.on_search_result)
140 self.find_dialog.replace_signal.connect(self.on_replace)
141 self.addDockWidget(Qt.RightDockWidgetArea, self.find_dialog)
142
143 for f in filenames:
144 if f:
145 self.on_load_request(os.path.normpath(f), search_text)
146 self.readSettings()
147 self.find_dialog.setVisible(False)
148
149
150
151
209
211 '''
212 Enable the shortcats for search and replace
213 '''
214 if event.key() == Qt.Key_Escape:
215 self.reject()
216 elif event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_F:
217 if self.tabWidget.currentWidget().hasFocus():
218 if not self.searchButton.isChecked():
219 self.searchButton.setChecked(True)
220 else:
221 self.on_toggled_find(True)
222 else:
223 self.searchButton.setChecked(not self.searchButton.isChecked())
224 elif event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_R:
225 if self.tabWidget.currentWidget().hasFocus():
226 if not self.replaceButton.isChecked():
227 self.replaceButton.setChecked(True)
228 else:
229 self.on_toggled_replace(True)
230 else:
231 self.replaceButton.setChecked(not self.replaceButton.isChecked())
232 else:
233 event.accept()
234 QMainWindow.keyPressEvent(self, event)
235
237 if hasattr(QApplication, "UnicodeUTF8"):
238 return QApplication.translate("Editor", text, None, QApplication.UnicodeUTF8)
239 else:
240 return QApplication.translate("Editor", text, None)
241
258
268
270 '''
271 Loads a file in a new tab or focus the tab, if the file is already open.
272 @param filename: the path to file
273 @type filename: C{str}
274 @param search_text: if not empty, searches in new document for first occurrence of the given text
275 @type search_text: C{str} (Default: C{Empty String})
276 '''
277 if not filename:
278 return
279 self.tabWidget.setUpdatesEnabled(False)
280 try:
281 if filename not in self.files:
282 tab_name = self.__getTabName(filename)
283 editor = TextEdit(filename, self.tabWidget)
284 linenumber_editor = LineNumberWidget(editor)
285 tab_index = self.tabWidget.addTab(linenumber_editor, tab_name)
286 self.files.append(filename)
287 editor.setCurrentPath(os.path.basename(filename))
288 editor.load_request_signal.connect(self.on_load_request)
289 editor.document().modificationChanged.connect(self.on_editor_modificationChanged)
290 editor.cursorPositionChanged.connect(self.on_editor_positionChanged)
291 editor.setFocus(Qt.OtherFocusReason)
292
293 editor.undoAvailable.connect(self.on_text_changed)
294 self.tabWidget.setCurrentIndex(tab_index)
295
296 else:
297 for i in range(self.tabWidget.count()):
298 if self.tabWidget.widget(i).filename == filename:
299 self.tabWidget.setCurrentIndex(i)
300 break
301 except:
302 import traceback
303 rospy.logwarn("Error while open %s: %s", filename, traceback.format_exc(1))
304 self.tabWidget.setUpdatesEnabled(True)
305 if search_text:
306 try:
307 self._search_thread.stop()
308 self._search_thread = None
309 except:
310 pass
311 self._search_thread = TextSearchThread(search_text, filename, path_text=self.tabWidget.widget(0).document().toPlainText(), recursive=True)
312 self._search_thread.search_result_signal.connect(self.on_search_result_on_open)
313 self._search_thread.start()
314
315 - def on_text_changed(self, value=""):
316 if self.tabWidget.currentWidget().hasFocus():
317 self.find_dialog.file_changed(self.tabWidget.currentWidget().filename)
318
320 '''
321 Signal handling to close single tabs.
322 @param tab_index: tab index to close
323 @type tab_index: C{int}
324 '''
325 try:
326 doremove = True
327 w = self.tabWidget.widget(tab_index)
328 if w.document().isModified():
329 name = self.__getTabName(w.filename)
330 result = QMessageBox.question(self, "Unsaved Changes", '\n\n'.join(["Save the file before closing?", name]), QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
331 if result == QMessageBox.Yes:
332 self.tabWidget.currentWidget().save()
333 elif result == QMessageBox.No:
334 pass
335 else:
336 doremove = False
337 if doremove:
338
339 if w.filename in self.files:
340 self.files.remove(w.filename)
341
342 self.tabWidget.removeTab(tab_index)
343
344 if not self.tabWidget.count():
345 self.close()
346 except:
347 import traceback
348 rospy.logwarn("Error while close tab %s: %s", str(tab_index), traceback.format_exc(1))
349
351 if self.find_dialog.isVisible():
352 self.searchButton.setChecked(not self.searchButton.isChecked())
353 else:
354 self.close()
355
357 '''
358 Test the open files for changes and save this if needed.
359 '''
360 changed = []
361
362 for i in range(self.tabWidget.count()):
363 w = self.tabWidget.widget(i)
364 if w.document().isModified():
365 changed.append(self.__getTabName(w.filename))
366 if changed:
367
368 if self.isHidden():
369 buttons = QMessageBox.Yes | QMessageBox.No
370 else:
371 buttons = QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
372 result = QMessageBox.question(self, "Unsaved Changes", '\n\n'.join(["Save the file before closing?", '\n'.join(changed)]), buttons)
373 if result == QMessageBox.Yes:
374 for i in range(self.tabWidget.count()):
375 w = self.tabWidget.widget(i).save()
376 event.accept()
377 elif result == QMessageBox.No:
378 event.accept()
379 else:
380 event.ignore()
381 else:
382 event.accept()
383 if event.isAccepted():
384 self.storeSetting()
385 self.finished_signal.emit(self.init_filenames)
386
388 '''
389 If the content was changed, a '*' will be shown in the tab name.
390 '''
391 tab_name = self.__getTabName(self.tabWidget.currentWidget().filename)
392 if (self.tabWidget.currentWidget().document().isModified()) or not QFileInfo(self.tabWidget.currentWidget().filename).exists():
393 tab_name = ''.join(['*', tab_name])
394 self.tabWidget.setTabText(self.tabWidget.currentIndex(), tab_name)
395
397 '''
398 Shows the number of the line and column in a label.
399 '''
400 cursor = self.tabWidget.currentWidget().textCursor()
401 self.pos_label.setText(':%s:%s #%s' % (cursor.blockNumber() + 1, cursor.columnNumber(), cursor.position()))
402
404 base = os.path.basename(lfile).replace('.launch', '')
405 (package, _) = package_name(os.path.dirname(lfile))
406 return '%s [%s]' % (base, package)
407
408
409
410
411
425
428
430 '''
431 Shows the search frame
432 '''
433 if value:
434 self.find_dialog.enable()
435 else:
436 self.replaceButton.setChecked(False)
437 self.find_dialog.setVisible(False)
438 self.tabWidget.currentWidget().setFocus()
439
447
449 '''
450 Opens a C{goto} dialog.
451 '''
452 value = 1
453 ok = False
454 try:
455 value, ok = QInputDialog.getInt(self, "Goto", self.tr("Line number:"),
456 QLineEdit.Normal, minValue=1, step=1)
457 except:
458 value, ok = QInputDialog.getInt(self, "Goto", self.tr("Line number:"),
459 QLineEdit.Normal, min=1, step=1)
460 if ok:
461 if value > self.tabWidget.currentWidget().document().blockCount():
462 value = self.tabWidget.currentWidget().document().blockCount()
463 curpos = self.tabWidget.currentWidget().textCursor().blockNumber() + 1
464 while curpos != value:
465 mov = QTextCursor.NextBlock if curpos < value else QTextCursor.PreviousBlock
466 self.tabWidget.currentWidget().moveCursor(mov)
467 curpos = self.tabWidget.currentWidget().textCursor().blockNumber() + 1
468 self.tabWidget.currentWidget().setFocus(Qt.ActiveWindowFocusReason)
469
470
471
472
473
475 '''
476 A slot to handle a found text. It goes to the position in the text and select
477 the searched text. On new file it will be open.
478 :param search_text: the searched text
479 :type search_text: str
480 :param found: the text was found or not
481 :type found: bool
482 :param path: the path of the file the text was found
483 :type path: str
484 :param index: the position in the text
485 :type index: int
486 '''
487 if found:
488 if self.tabWidget.currentWidget().filename != path:
489 focus_widget = QApplication.focusWidget()
490 self.on_load_request(path)
491 focus_widget.setFocus()
492 cursor = self.tabWidget.currentWidget().textCursor()
493 cursor.setPosition(index, QTextCursor.MoveAnchor)
494 cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, len(search_text))
495 self.tabWidget.currentWidget().setTextCursor(cursor)
496
498 '''
499 Like on_search_result, but skips the text in comments.
500 '''
501 if found:
502 if self.tabWidget.currentWidget().filename != path:
503 focus_widget = QApplication.focusWidget()
504 self.on_load_request(path)
505 focus_widget.setFocus()
506 comment_start = self.tabWidget.currentWidget().document().find('<!--', index, QTextDocument.FindBackward)
507 if not comment_start.isNull():
508 comment_end = self.tabWidget.currentWidget().document().find('-->', comment_start)
509 if not comment_end.isNull() and comment_end.position() > index + len(search_text):
510
511 return
512 self.on_search_result(search_text, found, path, index)
513
514 - def on_replace(self, search_text, path, index, replaced_text):
515 '''
516 A slot to handle a text replacement of the TextSearchFrame.
517 :param search_text: the searched text
518 :type search_text: str
519 :param path: the path of the file the text was found
520 :type path: str
521 :param index: the position in the text
522 :type index: int
523 :param replaced_text: the new text
524 :type replaced_text: str
525 '''
526 cursor = self.tabWidget.currentWidget().textCursor()
527 if cursor.selectedText() == search_text:
528 cursor.insertText(replaced_text)
529
530
531
532
533
543
545
546 tag_menu = QMenu("ROS Tags", parent)
547
548 add_group_tag_action = QAction("<group>", self, statusTip="", triggered=self._on_add_group_tag)
549 add_group_tag_action.setShortcuts(QKeySequence("Ctrl+Shift+g"))
550 tag_menu.addAction(add_group_tag_action)
551
552 add_node_tag_action = QAction("<node>", self, statusTip="", triggered=self._on_add_node_tag)
553 add_node_tag_action.setShortcuts(QKeySequence("Ctrl+Shift+n"))
554 tag_menu.addAction(add_node_tag_action)
555
556 add_node_tag_all_action = QAction("<node all>", self, statusTip="", triggered=self._on_add_node_tag_all)
557 tag_menu.addAction(add_node_tag_all_action)
558
559 add_include_tag_all_action = QAction("<include>", self, statusTip="", triggered=self._on_add_include_tag_all)
560 add_include_tag_all_action.setShortcuts(QKeySequence("Ctrl+Shift+i"))
561 tag_menu.addAction(add_include_tag_all_action)
562
563 add_remap_tag_action = QAction("<remap>", self, statusTip="", triggered=self._on_add_remap_tag)
564 add_remap_tag_action.setShortcuts(QKeySequence("Ctrl+Shift+r"))
565 tag_menu.addAction(add_remap_tag_action)
566
567 add_env_tag_action = QAction("<env>", self, statusTip="", triggered=self._on_add_env_tag)
568 tag_menu.addAction(add_env_tag_action)
569
570 add_param_tag_action = QAction("<param>", self, statusTip="", triggered=self._on_add_param_tag)
571 add_param_tag_action.setShortcuts(QKeySequence("Ctrl+Shift+p"))
572 tag_menu.addAction(add_param_tag_action)
573
574 add_param_cap_group_tag_action = QAction("<param capability group>", self, statusTip="", triggered=self._on_add_param_cap_group_tag)
575 add_param_cap_group_tag_action.setShortcuts(QKeySequence("Ctrl+Alt+p"))
576 tag_menu.addAction(add_param_cap_group_tag_action)
577
578 add_param_tag_all_action = QAction("<param all>", self, statusTip="", triggered=self._on_add_param_tag_all)
579 tag_menu.addAction(add_param_tag_all_action)
580
581 add_rosparam_tag_all_action = QAction("<rosparam>", self, statusTip="", triggered=self._on_add_rosparam_tag_all)
582 tag_menu.addAction(add_rosparam_tag_all_action)
583
584 add_arg_tag_default_action = QAction("<arg default>", self, statusTip="", triggered=self._on_add_arg_tag_default)
585 add_arg_tag_default_action.setShortcuts(QKeySequence("Ctrl+Shift+a"))
586 tag_menu.addAction(add_arg_tag_default_action)
587
588 add_arg_tag_value_action = QAction("<arg value>", self, statusTip="", triggered=self._on_add_arg_tag_value)
589 add_arg_tag_value_action.setShortcuts(QKeySequence("Ctrl+Alt+a"))
590 tag_menu.addAction(add_arg_tag_value_action)
591
592
593 add_test_tag_action = QAction("<test>", self, statusTip="", triggered=self._on_add_test_tag)
594 add_test_tag_action.setShortcuts(QKeySequence("Ctrl+Alt+t"))
595 tag_menu.addAction(add_test_tag_action)
596
597 add_test_tag_all_action = QAction("<test all>", self, statusTip="", triggered=self._on_add_test_tag_all)
598 tag_menu.addAction(add_test_tag_all_action)
599 return tag_menu
600
601 - def _insert_text(self, text):
602 cursor = self.tabWidget.currentWidget().textCursor()
603 if not cursor.isNull():
604 col = cursor.columnNumber()
605 spaces = ''.join([' ' for _ in range(col)])
606 cursor.insertText(text.replace('\n', '\n%s' % spaces))
607 self.tabWidget.currentWidget().setFocus(Qt.OtherFocusReason)
608
610 self._insert_text('<group ns="namespace" clear_params="true|false">\n'
611 '</group>')
612
614 dia = PackageDialog()
615 if dia.exec_():
616 self._insert_text('<node name="%s" pkg="%s" type="%s">\n'
617 '</node>' % (dia.binary, dia.package, dia.binary))
618
620 dia = PackageDialog()
621 if dia.exec_():
622 self._insert_text('<node name="%s" pkg="%s" type="%s"\n'
623 ' args="arg1" machine="machine-name"\n'
624 ' respawn="true" required="true"\n'
625 ' ns="foo" clear_params="true|false"\n'
626 ' output="log|screen" cwd="ROS_HOME|node"\n'
627 ' launch-prefix="prefix arguments">\n'
628 '</node>' % (dia.binary, dia.package, dia.binary))
629
631 self._insert_text('<include file="$(find pkg-name)/path/filename.xml"\n'
632 ' ns="foo" clear_params="true|false">\n'
633 '</include>')
634
636 self._insert_text('<remap from="original" to="new"/>')
637
639 self._insert_text('<env name="variable" value="value"/>')
640
642 self._insert_text('<param name="ns_name" value="value" />')
643
645 self._insert_text('<param name="capability_group" value="demo" />')
646
648 self._insert_text('<param name="ns_name" value="value"\n'
649 ' type="str|int|double|bool"\n'
650 ' textfile="$(find pkg-name)/path/file.txt"\n'
651 ' binfile="$(find pkg-name)/path/file"\n'
652 ' command="$(find pkg-name)/exe \'$(find pkg-name)/arg.txt\'">\n'
653 '</param>')
654
656 self._insert_text('<rosparam param="param-name"\n'
657 ' file="$(find pkg-name)/path/foo.yaml"\n'
658 ' command="load|dump|delete"\n'
659 ' ns="namespace">\n'
660 '</rosparam>')
661
663 self._insert_text('<arg name="foo" default="1" />')
664
666 self._insert_text('<arg name="foo" value="bar" />')
667
669 dia = PackageDialog()
670 if dia.exec_():
671 self._insert_text('<test name="%s" pkg="%s" type="%s" test-name="test_%s">\n'
672 '</test>' % (dia.binary, dia.package, dia.binary, dia.binary))
673
675 dia = PackageDialog()
676 if dia.exec_():
677 self._insert_text('<test name="%s" pkg="%s" type="%s" test-name="test_%s">\n'
678 ' args="arg1" time-limit="60.0"\n'
679 ' ns="foo" clear_params="true|false"\n'
680 ' cwd="ROS_HOME|node" retry="0"\n'
681 ' launch-prefix="prefix arguments">\n'
682 '</test>' % (dia.binary, dia.package, dia.binary, dia.binary))
683