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, utf8
40 from node_manager_fkie.run_dialog import PackageDialog
41 from node_manager_fkie.launch_config import LaunchConfig
42 import node_manager_fkie as nm
43
44 from .line_number_widget import LineNumberWidget
45 from .graph_view import GraphViewWidget
46 from .text_edit import TextEdit
47 from .text_search_frame import TextSearchFrame
48 from .text_search_thread import TextSearchThread
49 from node_manager_fkie.detailed_msg_box import MessageBox
50
51 try:
52 from python_qt_binding.QtGui import QApplication, QAction, QLineEdit, QWidget, QMainWindow
53 from python_qt_binding.QtGui import QDialog, QInputDialog, QLabel, QMenu, QPushButton, QTabWidget
54 from python_qt_binding.QtGui import QHBoxLayout, QVBoxLayout, QSpacerItem, QSplitter, QSizePolicy
55 except:
56 from python_qt_binding.QtWidgets import QApplication, QAction, QLineEdit, QWidget, QMainWindow
57 from python_qt_binding.QtWidgets import QDialog, QInputDialog, QLabel, QMenu, QPushButton, QTabWidget
58 from python_qt_binding.QtWidgets import QHBoxLayout, QVBoxLayout, QSpacerItem, QSplitter, QSizePolicy
59
60
86
87
89 '''
90 Creates a dialog to edit a launch file.
91 '''
92 finished_signal = Signal(list)
93 '''
94 finished_signal has as parameter the filenames of the initialization and is emitted, if this
95 dialog was closed.
96 '''
97
98 - def __init__(self, filenames, search_text='', parent=None):
99 '''
100 @param filenames: a list with filenames. The last one will be activated.
101 @type filenames: C{[str, ...]}
102 @param search_text: if not empty, searches in new document for first occurrence of the given text
103 @type search_text: C{str} (Default: C{Empty String})
104 '''
105 QMainWindow.__init__(self, parent)
106 self.setObjectName('Editor - %s' % utf8(filenames))
107 self.setAttribute(Qt.WA_DeleteOnClose, True)
108 self.setWindowFlags(Qt.Window)
109 self.mIcon = QIcon(":/icons/crystal_clear_edit_launch.png")
110 self._error_icon = QIcon(":/icons/crystal_clear_warning.png")
111 self._empty_icon = QIcon()
112 self.setWindowIcon(self.mIcon)
113 window_title = "ROSLaunch Editor"
114 if filenames:
115 window_title = self.__getTabName(filenames[0])
116 self.setWindowTitle(window_title)
117 self.init_filenames = list(filenames)
118 self._search_thread = None
119
120 self.files = []
121
122 self.main_widget = QWidget(self)
123 self.verticalLayout = QVBoxLayout(self.main_widget)
124 self.verticalLayout.setContentsMargins(0, 0, 0, 0)
125 self.verticalLayout.setSpacing(1)
126 self.verticalLayout.setObjectName("verticalLayout")
127
128 self.tabWidget = EditorTabWidget(self)
129 self.tabWidget.setTabPosition(QTabWidget.North)
130 self.tabWidget.setDocumentMode(True)
131 self.tabWidget.setTabsClosable(True)
132 self.tabWidget.setMovable(False)
133 self.tabWidget.setObjectName("tabWidget")
134 self.tabWidget.tabCloseRequested.connect(self.on_close_tab)
135 self.tabWidget.currentChanged.connect(self.on_tab_changed)
136
137 self.verticalLayout.addWidget(self.tabWidget)
138 self.buttons = self._create_buttons()
139 self.verticalLayout.addWidget(self.buttons)
140 self.setCentralWidget(self.main_widget)
141
142 self.find_dialog = TextSearchFrame(self.tabWidget, self)
143 self.find_dialog.search_result_signal.connect(self.on_search_result)
144 self.find_dialog.replace_signal.connect(self.on_replace)
145 self.addDockWidget(Qt.RightDockWidgetArea, self.find_dialog)
146
147 self.graph_view = GraphViewWidget(self.tabWidget, self)
148 self.graph_view.load_signal.connect(self.on_graph_load_file)
149 self.graph_view.goto_signal.connect(self.on_graph_goto)
150 self.addDockWidget(Qt.RightDockWidgetArea, self.graph_view)
151
152 for f in filenames:
153 if f:
154 self.on_load_request(os.path.normpath(f), search_text)
155 self.readSettings()
156 self.find_dialog.setVisible(False)
157 self.graph_view.setVisible(False)
158
159
160
161
241
243 '''
244 Enable the shortcats for search and replace
245 '''
246 if event.key() == Qt.Key_Escape:
247 self.reject()
248 elif event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_F:
249 if self.tabWidget.currentWidget().hasFocus():
250 if not self.searchButton.isChecked():
251 self.searchButton.setChecked(True)
252 else:
253 self.on_toggled_find(True)
254 else:
255 self.searchButton.setChecked(not self.searchButton.isChecked())
256 elif event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_R:
257 if self.tabWidget.currentWidget().hasFocus():
258 if not self.replaceButton.isChecked():
259 self.replaceButton.setChecked(True)
260 else:
261 self.on_toggled_replace(True)
262 else:
263 self.replaceButton.setChecked(not self.replaceButton.isChecked())
264 elif event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_E:
265 if self.tabWidget.currentWidget().hasFocus():
266 if not self.graphButton.isChecked():
267 self.graphButton.setChecked(True)
268 else:
269 self.on_toggled_graph(True)
270 else:
271 self.graphButton.setChecked(not self.graphButton.isChecked())
272 elif event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_W:
273 self.on_close_tab(self.tabWidget.currentIndex())
274 else:
275 event.accept()
276 QMainWindow.keyPressEvent(self, event)
277
279 if hasattr(QApplication, "UnicodeUTF8"):
280 return QApplication.translate("Editor", text, None, QApplication.UnicodeUTF8)
281 else:
282 return QApplication.translate("Editor", text, None)
283
300
310
311 - def on_load_request(self, filename, search_text='', insert_index=-1, goto_line=-1):
312 '''
313 Loads a file in a new tab or focus the tab, if the file is already open.
314 @param filename: the path to file
315 @type filename: C{str}
316 @param search_text: if not empty, searches in new document for first occurrence of the given text
317 @type search_text: C{str} (Default: C{Empty String})
318 '''
319 if not filename:
320 return
321 self.tabWidget.setUpdatesEnabled(False)
322 try:
323 if filename not in self.files:
324 tab_name = self.__getTabName(filename)
325 editor = TextEdit(filename, self.tabWidget)
326 linenumber_editor = LineNumberWidget(editor)
327 tab_index = 0
328 if insert_index > -1:
329 tab_index = self.tabWidget.insertTab(insert_index, linenumber_editor, tab_name)
330 else:
331 tab_index = self.tabWidget.addTab(linenumber_editor, tab_name)
332 self.files.append(filename)
333 editor.setCurrentPath(os.path.basename(filename))
334 editor.load_request_signal.connect(self.on_load_request)
335 editor.document().modificationChanged.connect(self.on_editor_modificationChanged)
336 editor.cursorPositionChanged.connect(self.on_editor_positionChanged)
337 editor.setFocus(Qt.OtherFocusReason)
338
339 editor.undoAvailable.connect(self.on_text_changed)
340 self.tabWidget.setCurrentIndex(tab_index)
341
342 else:
343 for i in range(self.tabWidget.count()):
344 if self.tabWidget.widget(i).filename == filename:
345 self.tabWidget.setCurrentIndex(i)
346 break
347 except Exception:
348 import traceback
349 rospy.logwarn("Error while open %s: %s", filename, traceback.format_exc(1))
350 self.tabWidget.setUpdatesEnabled(True)
351 if search_text:
352 try:
353 self._search_thread.stop()
354 self._search_thread = None
355 except Exception:
356 pass
357 self._search_thread = TextSearchThread(search_text, filename, path_text=self.tabWidget.widget(0).document().toPlainText(), recursive=True)
358 self._search_thread.search_result_signal.connect(self.on_search_result_on_open)
359 self._search_thread.start()
360 if goto_line != -1:
361 self._goto(goto_line, True)
362 self.upperButton.setEnabled(self.tabWidget.count() > 1)
363
365 insert_index = self.tabWidget.currentIndex() + 1
366 if not insert_after:
367 insert_index = self.tabWidget.currentIndex()
368 self.on_load_request(path, insert_index=insert_index)
369
371 if path == self.tabWidget.currentWidget().filename:
372 if linenr != -1:
373 self._goto(linenr, True)
374
375 - def on_text_changed(self, value=""):
376 if self.tabWidget.currentWidget().hasFocus():
377 self.find_dialog.file_changed(self.tabWidget.currentWidget().filename)
378
382
384 '''
385 Signal handling to close single tabs.
386 @param tab_index: tab index to close
387 @type tab_index: C{int}
388 '''
389 try:
390 doremove = True
391 w = self.tabWidget.widget(tab_index)
392 if w.document().isModified():
393 name = self.__getTabName(w.filename)
394 result = MessageBox.question(self, "Unsaved Changes", '\n\n'.join(["Save the file before closing?", name]))
395 if result == MessageBox.Yes:
396 self.tabWidget.currentWidget().save()
397 elif result == MessageBox.No:
398 pass
399 else:
400 doremove = False
401 if doremove:
402
403 if w.filename in self.files:
404 self.files.remove(w.filename)
405
406 self.tabWidget.removeTab(tab_index)
407
408 if not self.tabWidget.count():
409 self.close()
410 except Exception:
411 import traceback
412 rospy.logwarn("Error while close tab %s: %s", str(tab_index), traceback.format_exc(1))
413 self.upperButton.setEnabled(self.tabWidget.count() > 1)
414
416 if self.find_dialog.isVisible():
417 self.searchButton.setChecked(not self.searchButton.isChecked())
418 else:
419 self.close()
420
452
454 '''
455 If the content was changed, a '*' will be shown in the tab name.
456 '''
457 tab_name = self.__getTabName(self.tabWidget.currentWidget().filename)
458 if (self.tabWidget.currentWidget().document().isModified()) or not QFileInfo(self.tabWidget.currentWidget().filename).exists():
459 tab_name = ''.join(['*', tab_name])
460 self.tabWidget.setTabText(self.tabWidget.currentIndex(), tab_name)
461
463 '''
464 Shows the number of the line and column in a label.
465 '''
466 cursor = self.tabWidget.currentWidget().textCursor()
467 self.pos_label.setText(':%s:%s #%s' % (cursor.blockNumber() + 1, cursor.columnNumber(), cursor.position()))
468
470 base = os.path.basename(lfile).replace('.launch', '')
471 (package, _) = package_name(os.path.dirname(lfile))
472 return '%s [%s]' % (base, package)
473
474
475
476
477
484
499
502
513
515 '''
516 Shows the search frame
517 '''
518 if value:
519 self.find_dialog.enable()
520 else:
521 self.replaceButton.setChecked(False)
522 self.find_dialog.setVisible(False)
523 self.tabWidget.currentWidget().setFocus()
524
532
534 '''
535 Opens a C{goto} dialog.
536 '''
537 value = 1
538 ok = False
539 try:
540 value, ok = QInputDialog.getInt(self, "Goto", self.tr("Line number:"),
541 QLineEdit.Normal, minValue=1, step=1)
542 except Exception:
543 value, ok = QInputDialog.getInt(self, "Goto", self.tr("Line number:"),
544 QLineEdit.Normal, min=1, step=1)
545 if ok:
546 self._goto(value)
547 self.tabWidget.currentWidget().setFocus(Qt.ActiveWindowFocusReason)
548
549 - def _goto(self, linenr, select_line=True):
550 if linenr > self.tabWidget.currentWidget().document().blockCount():
551 linenr = self.tabWidget.currentWidget().document().blockCount()
552 curpos = self.tabWidget.currentWidget().textCursor().blockNumber() + 1
553 while curpos != linenr:
554 mov = QTextCursor.NextBlock if curpos < linenr else QTextCursor.PreviousBlock
555 self.tabWidget.currentWidget().moveCursor(mov)
556 curpos = self.tabWidget.currentWidget().textCursor().blockNumber() + 1
557 self.tabWidget.currentWidget().moveCursor(QTextCursor.EndOfBlock)
558 self.tabWidget.currentWidget().moveCursor(QTextCursor.StartOfBlock, QTextCursor.KeepAnchor)
559
560
561
562
563
565 '''
566 A slot to handle a found text. It goes to the position in the text and select
567 the searched text. On new file it will be open.
568 :param search_text: the searched text
569 :type search_text: str
570 :param found: the text was found or not
571 :type found: bool
572 :param path: the path of the file the text was found
573 :type path: str
574 :param index: the position in the text
575 :type index: int
576 '''
577 if found:
578 if self.tabWidget.currentWidget().filename != path:
579 focus_widget = QApplication.focusWidget()
580 self.on_load_request(path)
581 focus_widget.setFocus()
582 cursor = self.tabWidget.currentWidget().textCursor()
583 cursor.setPosition(index, QTextCursor.MoveAnchor)
584 cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, len(search_text))
585 self.tabWidget.currentWidget().setTextCursor(cursor)
586 cursor_y = self.tabWidget.currentWidget().cursorRect().top()
587 vbar = self.tabWidget.currentWidget().verticalScrollBar()
588 vbar.setValue(vbar.value() + cursor_y * 0.8)
589
591 '''
592 Like on_search_result, but skips the text in comments.
593 '''
594 if found:
595 if self.tabWidget.currentWidget().filename != path:
596 focus_widget = QApplication.focusWidget()
597 self.on_load_request(path)
598 focus_widget.setFocus()
599 comment_start = self.tabWidget.currentWidget().document().find('<!--', index, QTextDocument.FindBackward)
600 if not comment_start.isNull():
601 comment_end = self.tabWidget.currentWidget().document().find('-->', comment_start)
602 if not comment_end.isNull() and comment_end.position() > index + len(search_text):
603
604 return
605 self.on_search_result(search_text, found, path, index)
606
607 - def on_replace(self, search_text, path, index, replaced_text):
608 '''
609 A slot to handle a text replacement of the TextSearchFrame.
610 :param search_text: the searched text
611 :type search_text: str
612 :param path: the path of the file the text was found
613 :type path: str
614 :param index: the position in the text
615 :type index: int
616 :param replaced_text: the new text
617 :type replaced_text: str
618 '''
619 cursor = self.tabWidget.currentWidget().textCursor()
620 if cursor.selectedText() == search_text:
621 cursor.insertText(replaced_text)
622
623
624
625
626
636
638
639 tag_menu = QMenu("ROS Tags", parent)
640
641 add_group_tag_action = QAction("<group>", self, statusTip="", triggered=self._on_add_group_tag)
642 add_group_tag_action.setShortcuts(QKeySequence("Ctrl+Shift+g"))
643 tag_menu.addAction(add_group_tag_action)
644
645 add_node_tag_action = QAction("<node>", self, statusTip="", triggered=self._on_add_node_tag)
646 add_node_tag_action.setShortcuts(QKeySequence("Ctrl+Shift+n"))
647 tag_menu.addAction(add_node_tag_action)
648
649 add_node_tag_all_action = QAction("<node all>", self, statusTip="", triggered=self._on_add_node_tag_all)
650 tag_menu.addAction(add_node_tag_all_action)
651
652 add_include_tag_all_action = QAction("<include>", self, statusTip="", triggered=self._on_add_include_tag_all)
653 add_include_tag_all_action.setShortcuts(QKeySequence("Ctrl+Shift+i"))
654 tag_menu.addAction(add_include_tag_all_action)
655
656 add_remap_tag_action = QAction("<remap>", self, statusTip="", triggered=self._on_add_remap_tag)
657 add_remap_tag_action.setShortcuts(QKeySequence("Ctrl+Shift+r"))
658 tag_menu.addAction(add_remap_tag_action)
659
660 add_env_tag_action = QAction("<env>", self, statusTip="", triggered=self._on_add_env_tag)
661 tag_menu.addAction(add_env_tag_action)
662
663 add_param_tag_action = QAction("<param>", self, statusTip="", triggered=self._on_add_param_tag)
664 add_param_tag_action.setShortcuts(QKeySequence("Ctrl+Shift+p"))
665 tag_menu.addAction(add_param_tag_action)
666
667 add_param_cap_group_tag_action = QAction("<param capability group>", self, statusTip="", triggered=self._on_add_param_cap_group_tag)
668 add_param_cap_group_tag_action.setShortcuts(QKeySequence("Ctrl+Alt+p"))
669 tag_menu.addAction(add_param_cap_group_tag_action)
670
671 add_param_tag_all_action = QAction("<param all>", self, statusTip="", triggered=self._on_add_param_tag_all)
672 tag_menu.addAction(add_param_tag_all_action)
673
674 add_rosparam_tag_all_action = QAction("<rosparam>", self, statusTip="", triggered=self._on_add_rosparam_tag_all)
675 tag_menu.addAction(add_rosparam_tag_all_action)
676
677 add_arg_tag_default_action = QAction("<arg default>", self, statusTip="", triggered=self._on_add_arg_tag_default)
678 add_arg_tag_default_action.setShortcuts(QKeySequence("Ctrl+Shift+a"))
679 tag_menu.addAction(add_arg_tag_default_action)
680
681 add_arg_tag_value_action = QAction("<arg value>", self, statusTip="", triggered=self._on_add_arg_tag_value)
682 add_arg_tag_value_action.setShortcuts(QKeySequence("Ctrl+Alt+a"))
683 tag_menu.addAction(add_arg_tag_value_action)
684
685
686 add_test_tag_action = QAction("<test>", self, statusTip="", triggered=self._on_add_test_tag)
687 add_test_tag_action.setShortcuts(QKeySequence("Ctrl+Alt+t"))
688 tag_menu.addAction(add_test_tag_action)
689
690 add_test_tag_all_action = QAction("<test all>", self, statusTip="", triggered=self._on_add_test_tag_all)
691 tag_menu.addAction(add_test_tag_all_action)
692 return tag_menu
693
694 - def _insert_text(self, text):
695 cursor = self.tabWidget.currentWidget().textCursor()
696 if not cursor.isNull():
697 col = cursor.columnNumber()
698 spaces = ''.join([' ' for _ in range(col)])
699 cursor.insertText(text.replace('\n', '\n%s' % spaces))
700 self.tabWidget.currentWidget().setFocus(Qt.OtherFocusReason)
701
703 self._insert_text('<group ns="namespace" clear_params="true|false">\n'
704 '</group>')
705
707 dia = PackageDialog()
708 if dia.exec_():
709 self._insert_text('<node name="%s" pkg="%s" type="%s">\n'
710 '</node>' % (dia.binary, dia.package, dia.binary))
711
713 dia = PackageDialog()
714 if dia.exec_():
715 self._insert_text('<node name="%s" pkg="%s" type="%s"\n'
716 ' args="arg1" machine="machine_name"\n'
717 ' respawn="true" required="true"\n'
718 ' ns="foo" clear_params="true|false"\n'
719 ' output="log|screen" cwd="ROS_HOME|node"\n'
720 ' launch-prefix="prefix arguments">\n'
721 '</node>' % (dia.binary, dia.package, dia.binary))
722
724 self._insert_text('<include file="$(find pkg-name)/path/filename.xml"\n'
725 ' ns="foo" clear_params="true|false">\n'
726 '</include>')
727
729 self._insert_text('<remap from="original" to="new"/>')
730
732 self._insert_text('<env name="variable" value="value"/>')
733
735 self._insert_text('<param name="ns_name" value="value" />')
736
738 self._insert_text('<param name="capability_group" value="demo" />')
739
741 self._insert_text('<param name="ns_name" value="value"\n'
742 ' type="str|int|double|bool"\n'
743 ' textfile="$(find pkg-name)/path/file.txt"\n'
744 ' binfile="$(find pkg-name)/path/file"\n'
745 ' command="$(find pkg-name)/exe \'$(find pkg-name)/arg.txt\'">\n'
746 '</param>')
747
749 self._insert_text('<rosparam param="param-name"\n'
750 ' file="$(find pkg-name)/path/foo.yaml"\n'
751 ' command="load|dump|delete"\n'
752 ' ns="namespace">\n'
753 '</rosparam>')
754
756 self._insert_text('<arg name="foo" default="1" />')
757
759 self._insert_text('<arg name="foo" value="bar" />')
760
762 dia = PackageDialog()
763 if dia.exec_():
764 self._insert_text('<test name="%s" pkg="%s" type="%s" test-name="test_%s">\n'
765 '</test>' % (dia.binary, dia.package, dia.binary, dia.binary))
766
768 dia = PackageDialog()
769 if dia.exec_():
770 self._insert_text('<test name="%s" pkg="%s" type="%s" test-name="test_%s">\n'
771 ' args="arg1" time-limit="60.0"\n'
772 ' ns="foo" clear_params="true|false"\n'
773 ' cwd="ROS_HOME|node" retry="0"\n'
774 ' launch-prefix="prefix arguments">\n'
775 '</test>' % (dia.binary, dia.package, dia.binary, dia.binary))
776