def __init__(self, tabwidget, parent=None): QDockWidget.__init__(self, "Find", parent) self.setObjectName('SearchFrame') self.setFeatures(QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable) self._dockwidget = QFrame(self) self.vbox_layout = QVBoxLayout(self._dockwidget) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().setSpacing(1) # frame with two rows for find and replace find_replace_frame = QFrame(self) find_replace_vbox_layout = QVBoxLayout(find_replace_frame) find_replace_vbox_layout.setContentsMargins(0, 0, 0, 0) find_replace_vbox_layout.setSpacing(1) # find_replace_vbox_layout.addSpacerItem(QSpacerItem(1, 1, QSizePolicy.Expanding, QSizePolicy.Expanding)) # create frame with find row find_frame = self._create_find_frame() find_replace_vbox_layout.addWidget(find_frame) rplc_frame = self._create_replace_frame() find_replace_vbox_layout.addWidget(rplc_frame) # frame for find&replace and search results self.vbox_layout.addWidget(find_replace_frame) self.vbox_layout.addWidget(self._create_found_frame()) # self.vbox_layout.addStretch(2024) self.setWidget(self._dockwidget) # intern search parameters self._tabwidget = tabwidget self.current_search_text = '' self.search_results = [] self.search_results_fileset = set() self._search_result_index = -1 self._search_recursive = False self._search_thread = None
def __init__(self, context): super(VelocityControl, self).__init__(context) # Give QObjects reasonable names self.setObjectName('VelocityControl') # Process standalone plugin command-line arguments from argparse import ArgumentParser parser = ArgumentParser() # Add argument(s) to the parser. parser.add_argument("-q", "--quiet", action="store_true", dest="quiet", help="Put plugin in silent mode") args, unknowns = parser.parse_known_args(context.argv()) # Create QWidget self._widget = QWidget() # Get path to UI file which is a sibling of this file # in this example the .ui and .py file are in the same folder #ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'MyPlugin.ui') # Extend the widget with all attributes and children from UI file #loadUi(ui_file, self._widget) # Give QObjects reasonable names self._widget.setObjectName('VelocityControlUi') vLayout = QVBoxLayout(self._widget) vLayout.setContentsMargins(0, 0, 0, 0) vLayout.setSpacing(0) self._widget.setLayout(vLayout) self._widget.layout().setSpacing(0) self._widget.setWindowTitle("VelocityControl"); # Show _widget.windowTitle on left-top of each plugin (when # it's set in _widget). This is useful when you open multiple # plugins at once. Also if you open multiple instances of your # plugin at once, these lines add number to make it easy to # tell from pane to pane. if context.serial_number() > 1: self._widget.setWindowTitle(self._widget.windowTitle() + (' (%d)' % context.serial_number())) # Add widget to the user interface context.add_widget(self._widget) self.context = context self._topic_list = '' self._topic_commands = {} self._jointgroups = {} self.signal_topic.connect( self.signal_callback_list ) rospy.on_shutdown(self.on_ros_shutdown) #handle the ROS shutdown commands
def __init__(self, caller, jointstate, parent=None, stored_topic=None): super(JointStateGroup, self).__init__(parent) ti = TopicInfo() # create a new group ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'iop_rqt_velocity_joint_group.ui') loadUi(ui_file, self) group_layout = QVBoxLayout(self.frame) group_layout.setContentsMargins(0, 0, 0, 0) group_layout.setSpacing(0) self._cmd_type = Float64MultiArray if len(jointstate.name) == 1: self._cmd_type = Float64 ti.fill_subscribed_topics(self.comboBox_cmdTopic, self._cmd_type._type, stored_topic) for joint_name in jointstate.name: joint_frame = QFrame() ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'iop_rqt_velocity_joint.ui') loadUi(ui_file, joint_frame) joint_frame.setObjectName(joint_name) joint_frame.labelJointName.setText(joint_name) joint_frame.lineEditCurrentValue.setText('0.0') joint_frame.doubleSpinBoxNewValue.setValue(0.0) joint_frame.layout().setContentsMargins(0, 0, 0, 0) joint_frame.layout().setSpacing(0) group_layout.addWidget(joint_frame) # self._widget.layout().insertWidget(self._widget.layout().count() - 1, group_frame) # self._jointgroups[caller] = group_frame self.setObjectName(caller) self._caller = caller self._jointstate = jointstate self._topic_command = self.comboBox_cmdTopic.currentText() self.pushButtonSend.clicked.connect(self.on_clicked_send) self.pushButtonSendZero.clicked.connect(self.on_clicked_send_zero) self.comboBox_cmdTopic.activated.connect(self.on_activated_topic) self._cmd_publisher = None if self._cmd_type and self._topic_command: self._cmd_publisher = rospy.Publisher(self._topic_command, self._cmd_type, queue_size=1)
def __init__(self, context, node=None): """ This class is intended to be called by rqt plugin framework class. Currently (12/12/2012) the whole widget is splitted into 2 panes: one on left allows you to choose the node(s) you work on. Right side pane lets you work with the parameters associated with the node(s) you select on the left. (12/27/2012) Despite the pkg name is changed to rqt_reconfigure to reflect the available functionality, file & class names remain 'param', expecting all the parameters will become handle-able. """ super(ParamWidget, self).__init__() self.setObjectName(self._TITLE_PLUGIN) self.setWindowTitle(self._TITLE_PLUGIN) rp = rospkg.RosPack() #TODO: .ui file needs to replace the GUI components declaration # below. For unknown reason, referring to another .ui files # from a .ui that is used in this class failed. So for now, # I decided not use .ui in this class. # If someone can tackle this I'd appreciate. _hlayout_top = QHBoxLayout(self) self._splitter = QSplitter(self) _hlayout_top.addWidget(self._splitter) _vlayout_nodesel_widget = QWidget() _vlayout_nodesel_side = QVBoxLayout() _hlayout_filter_widget = QWidget(self) _hlayout_filter = QHBoxLayout() self._text_filter = TextFilter() self.filter_lineedit = TextFilterWidget(self._text_filter, rp) self.filterkey_label = QLabel("&Filter key:") self.filterkey_label.setBuddy(self.filter_lineedit) _hlayout_filter.addWidget(self.filterkey_label) _hlayout_filter.addWidget(self.filter_lineedit) _hlayout_filter_widget.setLayout(_hlayout_filter) self._nodesel_widget = NodeSelectorWidget(self, rp, self.sig_sysmsg) _vlayout_nodesel_side.addWidget(_hlayout_filter_widget) _vlayout_nodesel_side.addWidget(self._nodesel_widget) _vlayout_nodesel_side.setSpacing(1) _vlayout_nodesel_widget.setLayout(_vlayout_nodesel_side) reconf_widget = ParameditWidget(rp) self._splitter.insertWidget(0, _vlayout_nodesel_widget) self._splitter.insertWidget(1, reconf_widget) # 1st column, _vlayout_nodesel_widget, to minimize width. # 2nd col to keep the possible max width. self._splitter.setStretchFactor(0, 0) self._splitter.setStretchFactor(1, 1) # Signal from paramedit widget to node selector widget. reconf_widget.sig_node_disabled_selected.connect( self._nodesel_widget.node_deselected) # Pass name of node to editor widget self._nodesel_widget.sig_node_selected.connect( reconf_widget.show_reconf) if not node: title = self._TITLE_PLUGIN else: title = self._TITLE_PLUGIN + ' %s' % node self.setObjectName(title) #Connect filter signal-slots. self._text_filter.filter_changed_signal.connect( self._filter_key_changed)
class Editor(QMainWindow): ''' Creates a dialog to edit a launch file. ''' finished_signal = Signal(list) ''' finished_signal has as parameter the filenames of the initialization and is emitted, if this dialog was closed. ''' def __init__(self, filenames, search_text='', parent=None): ''' @param filenames: a list with filenames. The last one will be activated. @type filenames: C{[str, ...]} @param search_text: if not empty, searches in new document for first occurrence of the given text @type search_text: C{str} (Default: C{Empty String}) ''' QMainWindow.__init__(self, parent) self.setObjectName(' - '.join(['Editor', str(filenames)])) self.setAttribute(Qt.WA_DeleteOnClose, True) self.setWindowFlags(Qt.Window) self.mIcon = QIcon(":/icons/crystal_clear_edit_launch.png") self._error_icon = QIcon(":/icons/crystal_clear_warning.png") self._empty_icon = QIcon() self.setWindowIcon(self.mIcon) window_title = "ROSLaunch Editor" if filenames: window_title = self.__getTabName(filenames[0]) self.setWindowTitle(window_title) self.init_filenames = list(filenames) self._search_thread = None # list with all open files self.files = [] # create tabs for files self.main_widget = QWidget(self) self.verticalLayout = QVBoxLayout(self.main_widget) self.verticalLayout.setContentsMargins(0, 0, 0, 0) self.verticalLayout.setSpacing(1) self.verticalLayout.setObjectName("verticalLayout") self.tabWidget = EditorTabWidget(self) self.tabWidget.setTabPosition(QTabWidget.North) self.tabWidget.setDocumentMode(True) self.tabWidget.setTabsClosable(True) self.tabWidget.setMovable(False) self.tabWidget.setObjectName("tabWidget") self.tabWidget.tabCloseRequested.connect(self.on_close_tab) self.verticalLayout.addWidget(self.tabWidget) self.buttons = self._create_buttons() self.verticalLayout.addWidget(self.buttons) self.setCentralWidget(self.main_widget) self.find_dialog = TextSearchFrame(self.tabWidget, self) self.find_dialog.search_result_signal.connect(self.on_search_result) self.find_dialog.replace_signal.connect(self.on_replace) self.addDockWidget(Qt.RightDockWidgetArea, self.find_dialog) # open the files for f in filenames: if f: self.on_load_request(os.path.normpath(f), search_text) self.readSettings() self.find_dialog.setVisible(False) # def __del__(self): # print "******** destroy", self.objectName() def _create_buttons(self): # create the buttons line self.buttons = QWidget(self) self.horizontalLayout = QHBoxLayout(self.buttons) self.horizontalLayout.setContentsMargins(4, 0, 4, 0) self.horizontalLayout.setObjectName("horizontalLayout") # add the search button self.searchButton = QPushButton(self) self.searchButton.setObjectName("searchButton") # self.searchButton.clicked.connect(self.on_shortcut_find) self.searchButton.toggled.connect(self.on_toggled_find) self.searchButton.setText(self._translate("&Find")) self.searchButton.setToolTip('Open a search dialog (Ctrl+F)') self.searchButton.setFlat(True) self.searchButton.setCheckable(True) self.horizontalLayout.addWidget(self.searchButton) # add the replace button self.replaceButton = QPushButton(self) self.replaceButton.setObjectName("replaceButton") # self.replaceButton.clicked.connect(self.on_shortcut_replace) self.replaceButton.toggled.connect(self.on_toggled_replace) self.replaceButton.setText(self._translate("&Replace")) self.replaceButton.setToolTip('Open a search&replace dialog (Ctrl+R)') self.replaceButton.setFlat(True) self.replaceButton.setCheckable(True) self.horizontalLayout.addWidget(self.replaceButton) # add the goto button self.gotoButton = QPushButton(self) self.gotoButton.setObjectName("gotoButton") self.gotoButton.clicked.connect(self.on_shortcut_goto) self.gotoButton.setText(self._translate("&Goto line")) self.gotoButton.setShortcut("Ctrl+G") self.gotoButton.setToolTip('Open a goto dialog (Ctrl+G)') self.gotoButton.setFlat(True) self.horizontalLayout.addWidget(self.gotoButton) # add a tag button self.tagButton = self._create_tag_button(self) self.horizontalLayout.addWidget(self.tagButton) # add spacer spacerItem = QSpacerItem(515, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem) # add line number label self.pos_label = QLabel() self.horizontalLayout.addWidget(self.pos_label) # add spacer spacerItem = QSpacerItem(515, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem) # add save button self.saveButton = QPushButton(self) self.saveButton.setObjectName("saveButton") self.saveButton.clicked.connect(self.on_saveButton_clicked) self.saveButton.setText(self._translate("&Save")) self.saveButton.setShortcut("Ctrl+S") self.saveButton.setToolTip('Save the changes to the file (Ctrl+S)') self.saveButton.setFlat(True) self.horizontalLayout.addWidget(self.saveButton) return self.buttons def keyPressEvent(self, event): ''' Enable the shortcats for search and replace ''' if event.key() == Qt.Key_Escape: self.reject() elif event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_F: if self.tabWidget.currentWidget().hasFocus(): if not self.searchButton.isChecked(): self.searchButton.setChecked(True) else: self.on_toggled_find(True) else: self.searchButton.setChecked(not self.searchButton.isChecked()) elif event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_R: if self.tabWidget.currentWidget().hasFocus(): if not self.replaceButton.isChecked(): self.replaceButton.setChecked(True) else: self.on_toggled_replace(True) else: self.replaceButton.setChecked(not self.replaceButton.isChecked()) else: event.accept() QMainWindow.keyPressEvent(self, event) def _translate(self, text): if hasattr(QApplication, "UnicodeUTF8"): return QApplication.translate("Editor", text, None, QApplication.UnicodeUTF8) else: return QApplication.translate("Editor", text, None) def readSettings(self): if nm.settings().store_geometry: settings = nm.settings().qsettings(nm.settings().CFG_GUI_FILE) settings.beginGroup("editor") maximized = settings.value("maximized", 'false') == 'true' if maximized: self.showMaximized() else: self.resize(settings.value("size", QSize(800, 640))) self.move(settings.value("pos", QPoint(0, 0))) try: self.restoreState(settings.value("window_state")) except: import traceback print traceback.format_exc() settings.endGroup() def storeSetting(self): if nm.settings().store_geometry: settings = nm.settings().qsettings(nm.settings().CFG_GUI_FILE) settings.beginGroup("editor") settings.setValue("size", self.size()) settings.setValue("pos", self.pos()) settings.setValue("maximized", self.isMaximized()) settings.setValue("window_state", self.saveState()) settings.endGroup() def on_load_request(self, filename, search_text=''): ''' Loads a file in a new tab or focus the tab, if the file is already open. @param filename: the path to file @type filename: C{str} @param search_text: if not empty, searches in new document for first occurrence of the given text @type search_text: C{str} (Default: C{Empty String}) ''' if not filename: return self.tabWidget.setUpdatesEnabled(False) try: if filename not in self.files: tab_name = self.__getTabName(filename) editor = TextEdit(filename, self.tabWidget) linenumber_editor = LineNumberWidget(editor) tab_index = self.tabWidget.addTab(linenumber_editor, tab_name) self.files.append(filename) editor.setCurrentPath(os.path.basename(filename)) editor.load_request_signal.connect(self.on_load_request) editor.document().modificationChanged.connect(self.on_editor_modificationChanged) editor.cursorPositionChanged.connect(self.on_editor_positionChanged) editor.setFocus(Qt.OtherFocusReason) editor.textChanged.connect(self.on_text_changed) self.tabWidget.setCurrentIndex(tab_index) # self.find_dialog.set_search_path(filename) else: for i in range(self.tabWidget.count()): if self.tabWidget.widget(i).filename == filename: self.tabWidget.setCurrentIndex(i) break except: import traceback rospy.logwarn("Error while open %s: %s", filename, traceback.format_exc(1)) self.tabWidget.setUpdatesEnabled(True) if search_text: try: self._search_thread.stop() self._search_thread = None except: pass self._search_thread = TextSearchThread(search_text, filename, path_text=self.tabWidget.widget(0).document().toPlainText(), recursive=True) self._search_thread.search_result_signal.connect(self.on_search_result_on_open) self._search_thread.start() def on_text_changed(self): if self.tabWidget.currentWidget().hasFocus(): self.find_dialog.file_changed(self.tabWidget.currentWidget().filename) def on_close_tab(self, tab_index): ''' Signal handling to close single tabs. @param tab_index: tab index to close @type tab_index: C{int} ''' try: doremove = True w = self.tabWidget.widget(tab_index) if w.document().isModified(): name = self.__getTabName(w.filename) result = QMessageBox.question(self, "Unsaved Changes", '\n\n'.join(["Save the file before closing?", name]), QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) if result == QMessageBox.Yes: self.tabWidget.currentWidget().save() elif result == QMessageBox.No: pass else: doremove = False if doremove: # remove the indexed files if w.filename in self.files: self.files.remove(w.filename) # close tab self.tabWidget.removeTab(tab_index) # close editor, if no tabs are open if not self.tabWidget.count(): self.close() except: import traceback rospy.logwarn("Error while close tab %s: %s", str(tab_index), traceback.format_exc(1)) def reject(self): if self.find_dialog.isVisible(): self.searchButton.setChecked(not self.searchButton.isChecked()) else: self.close() def closeEvent(self, event): ''' Test the open files for changes and save this if needed. ''' changed = [] # get the names of all changed files for i in range(self.tabWidget.count()): w = self.tabWidget.widget(i) if w.document().isModified(): changed.append(self.__getTabName(w.filename)) if changed: # ask the user for save changes if self.isHidden(): buttons = QMessageBox.Yes | QMessageBox.No else: buttons = QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel result = QMessageBox.question(self, "Unsaved Changes", '\n\n'.join(["Save the file before closing?", '\n'.join(changed)]), buttons) if result == QMessageBox.Yes: for i in range(self.tabWidget.count()): w = self.tabWidget.widget(i).save() event.accept() elif result == QMessageBox.No: event.accept() else: event.ignore() else: event.accept() if event.isAccepted(): self.storeSetting() self.finished_signal.emit(self.init_filenames) def on_editor_modificationChanged(self, value=None): ''' If the content was changed, a '*' will be shown in the tab name. ''' tab_name = self.__getTabName(self.tabWidget.currentWidget().filename) if (self.tabWidget.currentWidget().document().isModified()) or not QFileInfo(self.tabWidget.currentWidget().filename).exists(): tab_name = ''.join(['*', tab_name]) self.tabWidget.setTabText(self.tabWidget.currentIndex(), tab_name) def on_editor_positionChanged(self): ''' Shows the number of the line and column in a label. ''' cursor = self.tabWidget.currentWidget().textCursor() self.pos_label.setText(':%s:%s' % (cursor.blockNumber() + 1, cursor.columnNumber())) def __getTabName(self, lfile): base = os.path.basename(lfile).replace('.launch', '') (package, _) = package_name(os.path.dirname(lfile)) return '%s [%s]' % (base, package) ############################################################################## # HANDLER for buttons ############################################################################## def on_saveButton_clicked(self): ''' Saves the current document. This method is called if the C{save button} was clicked. ''' saved, errors, msg = self.tabWidget.currentWidget().save(True) if errors: QMessageBox.critical(self, "Error", msg) self.tabWidget.setTabIcon(self.tabWidget.currentIndex(), self._error_icon) self.tabWidget.setTabToolTip(self.tabWidget.currentIndex(), msg) elif saved: self.tabWidget.setTabIcon(self.tabWidget.currentIndex(), self._empty_icon) self.tabWidget.setTabToolTip(self.tabWidget.currentIndex(), '') def on_shortcut_find(self): pass def on_toggled_find(self, value): ''' Shows the search frame ''' if value: self.find_dialog.enable() else: self.replaceButton.setChecked(False) self.find_dialog.setVisible(False) self.tabWidget.currentWidget().setFocus() def on_toggled_replace(self, value): ''' Shows the replace lineedit in the search frame ''' if value: self.searchButton.setChecked(True) self.find_dialog.set_replace_visible(value) def on_shortcut_goto(self): ''' Opens a C{goto} dialog. ''' value = 1 ok = False try: value, ok = QInputDialog.getInt(self, "Goto", self.tr("Line number:"), QLineEdit.Normal, minValue=1, step=1) except: value, ok = QInputDialog.getInt(self, "Goto", self.tr("Line number:"), QLineEdit.Normal, min=1, step=1) if ok: if value > self.tabWidget.currentWidget().document().blockCount(): value = self.tabWidget.currentWidget().document().blockCount() curpos = self.tabWidget.currentWidget().textCursor().blockNumber() + 1 while curpos != value: mov = QTextCursor.NextBlock if curpos < value else QTextCursor.PreviousBlock self.tabWidget.currentWidget().moveCursor(mov) curpos = self.tabWidget.currentWidget().textCursor().blockNumber() + 1 self.tabWidget.currentWidget().setFocus(Qt.ActiveWindowFocusReason) ############################################################################## # SLOTS for search dialog ############################################################################## def on_search_result(self, search_text, found, path, index): ''' A slot to handle a found text. It goes to the position in the text and select the searched text. On new file it will be open. :param search_text: the searched text :type search_text: str :param found: the text was found or not :type found: bool :param path: the path of the file the text was found :type path: str :param index: the position in the text :type index: int ''' if found: if self.tabWidget.currentWidget().filename != path: focus_widget = QApplication.focusWidget() self.on_load_request(path) focus_widget.setFocus() cursor = self.tabWidget.currentWidget().textCursor() cursor.setPosition(index, QTextCursor.MoveAnchor) cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, len(search_text)) self.tabWidget.currentWidget().setTextCursor(cursor) def on_search_result_on_open(self, search_text, found, path, index): ''' Like on_search_result, but skips the text in comments. ''' if found: if self.tabWidget.currentWidget().filename != path: focus_widget = QApplication.focusWidget() self.on_load_request(path) focus_widget.setFocus() comment_start = self.tabWidget.currentWidget().document().find('<!--', index, QTextDocument.FindBackward) if not comment_start.isNull(): comment_end = self.tabWidget.currentWidget().document().find('-->', comment_start) if not comment_end.isNull() and comment_end.position() > index + len(search_text): # commented -> retrun return self.on_search_result(search_text, found, path, index) def on_replace(self, search_text, path, index, replaced_text): ''' A slot to handle a text replacement of the TextSearchFrame. :param search_text: the searched text :type search_text: str :param path: the path of the file the text was found :type path: str :param index: the position in the text :type index: int :param replaced_text: the new text :type replaced_text: str ''' cursor = self.tabWidget.currentWidget().textCursor() if cursor.selectedText() == search_text: cursor.insertText(replaced_text) ############################################################################## # LAUNCH TAG insertion ############################################################################## def _create_tag_button(self, parent=None): btn = QPushButton(parent) btn.setObjectName("tagButton") btn.setText(self._translate("Add &tag")) btn.setShortcut("Ctrl+T") btn.setToolTip('Adds a ROS launch tag to launch file (Ctrl+T)') btn.setMenu(self._create_tag_menu(btn)) btn.setFlat(True) return btn def _create_tag_menu(self, parent=None): # creates a tag menu tag_menu = QMenu("ROS Tags", parent) # group tag add_group_tag_action = QAction("<group>", self, statusTip="", triggered=self._on_add_group_tag) add_group_tag_action.setShortcuts(QKeySequence("Ctrl+Shift+g")) tag_menu.addAction(add_group_tag_action) # node tag add_node_tag_action = QAction("<node>", self, statusTip="", triggered=self._on_add_node_tag) add_node_tag_action.setShortcuts(QKeySequence("Ctrl+Shift+n")) tag_menu.addAction(add_node_tag_action) # node tag with all attributes add_node_tag_all_action = QAction("<node all>", self, statusTip="", triggered=self._on_add_node_tag_all) tag_menu.addAction(add_node_tag_all_action) # include tag with all attributes add_include_tag_all_action = QAction("<include>", self, statusTip="", triggered=self._on_add_include_tag_all) add_include_tag_all_action.setShortcuts(QKeySequence("Ctrl+Shift+i")) tag_menu.addAction(add_include_tag_all_action) # remap add_remap_tag_action = QAction("<remap>", self, statusTip="", triggered=self._on_add_remap_tag) add_remap_tag_action.setShortcuts(QKeySequence("Ctrl+Shift+r")) tag_menu.addAction(add_remap_tag_action) # env tag add_env_tag_action = QAction("<env>", self, statusTip="", triggered=self._on_add_env_tag) tag_menu.addAction(add_env_tag_action) # param tag add_param_tag_action = QAction("<param>", self, statusTip="", triggered=self._on_add_param_tag) add_param_tag_action.setShortcuts(QKeySequence("Ctrl+Shift+p")) tag_menu.addAction(add_param_tag_action) # param capability group tag add_param_cap_group_tag_action = QAction("<param capability group>", self, statusTip="", triggered=self._on_add_param_cap_group_tag) add_param_cap_group_tag_action.setShortcuts(QKeySequence("Ctrl+Alt+p")) tag_menu.addAction(add_param_cap_group_tag_action) # param tag with all attributes add_param_tag_all_action = QAction("<param all>", self, statusTip="", triggered=self._on_add_param_tag_all) tag_menu.addAction(add_param_tag_all_action) # rosparam tag with all attributes add_rosparam_tag_all_action = QAction("<rosparam>", self, statusTip="", triggered=self._on_add_rosparam_tag_all) tag_menu.addAction(add_rosparam_tag_all_action) # arg tag with default definition add_arg_tag_default_action = QAction("<arg default>", self, statusTip="", triggered=self._on_add_arg_tag_default) add_arg_tag_default_action.setShortcuts(QKeySequence("Ctrl+Shift+a")) tag_menu.addAction(add_arg_tag_default_action) # arg tag with value definition add_arg_tag_value_action = QAction("<arg value>", self, statusTip="", triggered=self._on_add_arg_tag_value) add_arg_tag_value_action.setShortcuts(QKeySequence("Ctrl+Alt+a")) tag_menu.addAction(add_arg_tag_value_action) # test tag add_test_tag_action = QAction("<test>", self, statusTip="", triggered=self._on_add_test_tag) add_test_tag_action.setShortcuts(QKeySequence("Ctrl+Alt+t")) tag_menu.addAction(add_test_tag_action) # test tag with all attributes add_test_tag_all_action = QAction("<test all>", self, statusTip="", triggered=self._on_add_test_tag_all) tag_menu.addAction(add_test_tag_all_action) return tag_menu def _insert_text(self, text): cursor = self.tabWidget.currentWidget().textCursor() if not cursor.isNull(): col = cursor.columnNumber() spaces = ''.join([' ' for _ in range(col)]) cursor.insertText(text.replace('\n', '\n%s' % spaces)) self.tabWidget.currentWidget().setFocus(Qt.OtherFocusReason) def _on_add_group_tag(self): self._insert_text('<group ns="namespace" clear_params="true|false">\n' '</group>') def _on_add_node_tag(self): dia = PackageDialog() if dia.exec_(): self._insert_text('<node name="%s" pkg="%s" type="%s">\n' '</node>' % (dia.binary, dia.package, dia.binary)) def _on_add_node_tag_all(self): dia = PackageDialog() if dia.exec_(): self._insert_text('<node name="%s" pkg="%s" type="%s"\n' ' args="arg1" machine="machine-name"\n' ' respawn="true" required="true"\n' ' ns="foo" clear_params="true|false"\n' ' output="log|screen" cwd="ROS_HOME|node"\n' ' launch-prefix="prefix arguments">\n' '</node>' % (dia.binary, dia.package, dia.binary)) def _on_add_include_tag_all(self): self._insert_text('<include file="$(find pkg-name)/path/filename.xml"\n' ' ns="foo" clear_params="true|false">\n' '</include>') def _on_add_remap_tag(self): self._insert_text('<remap from="original" to="new"/>') def _on_add_env_tag(self): self._insert_text('<env name="variable" value="value"/>') def _on_add_param_tag(self): self._insert_text('<param name="ns_name" value="value" />') def _on_add_param_cap_group_tag(self): self._insert_text('<param name="capability_group" value="demo" />') def _on_add_param_tag_all(self): self._insert_text('<param name="ns_name" value="value"\n' ' type="str|int|double|bool"\n' ' textfile="$(find pkg-name)/path/file.txt"\n' ' binfile="$(find pkg-name)/path/file"\n' ' command="$(find pkg-name)/exe \'$(find pkg-name)/arg.txt\'">\n' '</param>') def _on_add_rosparam_tag_all(self): self._insert_text('<rosparam param="param-name"\n' ' file="$(find pkg-name)/path/foo.yaml"\n' ' command="load|dump|delete"\n' ' ns="namespace">\n' '</rosparam>') def _on_add_arg_tag_default(self): self._insert_text('<arg name="foo" default="1" />') def _on_add_arg_tag_value(self): self._insert_text('<arg name="foo" value="bar" />') def _on_add_test_tag(self): dia = PackageDialog() if dia.exec_(): self._insert_text('<test name="%s" pkg="%s" type="%s" test-name="test_%s">\n' '</test>' % (dia.binary, dia.package, dia.binary, dia.binary)) def _on_add_test_tag_all(self): dia = PackageDialog() if dia.exec_(): self._insert_text('<test name="%s" pkg="%s" type="%s" test-name="test_%s">\n' ' args="arg1" time-limit="60.0"\n' ' ns="foo" clear_params="true|false"\n' ' cwd="ROS_HOME|node" retry="0"\n' ' launch-prefix="prefix arguments">\n' '</test>' % (dia.binary, dia.package, dia.binary, dia.binary))
class TextSearchFrame(QDockWidget): ''' A frame to find text in the Editor. ''' search_result_signal = Signal(str, bool, str, int) ''' @ivar: A signal emitted after search_threaded was started. (search text, found or not, file, position in text) for each result a signal will be emitted. ''' replace_signal = Signal(str, str, int, str) ''' @ivar: A signal emitted to replace string at given position. (search text, file, position in text, replaced by text) ''' def __init__(self, tabwidget, parent=None): QDockWidget.__init__(self, "Find", parent) self.setObjectName('SearchFrame') self.setFeatures(QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable) self._dockwidget = QFrame(self) self.vbox_layout = QVBoxLayout(self._dockwidget) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().setSpacing(1) # frame with two rows for find and replace find_replace_frame = QFrame(self) find_replace_vbox_layout = QVBoxLayout(find_replace_frame) find_replace_vbox_layout.setContentsMargins(0, 0, 0, 0) find_replace_vbox_layout.setSpacing(1) find_replace_vbox_layout.addSpacerItem( QSpacerItem(1, 1, QSizePolicy.Expanding, QSizePolicy.Expanding)) # create frame with find row find_frame = self._create_find_frame() find_replace_vbox_layout.addWidget(find_frame) rplc_frame = self._create_replace_frame() find_replace_vbox_layout.addWidget(rplc_frame) # frame for find&replace and search results self.vbox_layout.addWidget(find_replace_frame) self.vbox_layout.addWidget(self._create_found_frame()) self.vbox_layout.addStretch(2024) self.setWidget(self._dockwidget) # intern search parameters self._tabwidget = tabwidget self.current_search_text = '' self.search_results = [] self.search_results_fileset = set() self._search_result_index = -1 self._search_recursive = False self._search_thread = None def _create_find_frame(self): find_frame = QFrame(self) find_hbox_layout = QHBoxLayout(find_frame) find_hbox_layout.setContentsMargins(0, 0, 0, 0) find_hbox_layout.setSpacing(1) self.search_field = EnchancedLineEdit(find_frame) self.search_field.setPlaceholderText('search text') self.search_field.textChanged.connect(self.on_search_text_changed) self.search_field.returnPressed.connect(self.on_search) find_hbox_layout.addWidget(self.search_field) self.search_result_label = QLabel(find_frame) self.search_result_label.setText(' ') find_hbox_layout.addWidget(self.search_result_label) self.find_button_back = QPushButton("<") self.find_button_back.setFixedWidth(44) self.find_button_back.clicked.connect(self.on_search_back) find_hbox_layout.addWidget(self.find_button_back) self.find_button = QPushButton(">") self.find_button.setDefault(True) # self.find_button.setFlat(True) self.find_button.setFixedWidth(44) self.find_button.clicked.connect(self.on_search) find_hbox_layout.addWidget(self.find_button) return find_frame def _create_replace_frame(self): # create frame with replace row self.rplc_frame = rplc_frame = QFrame(self) rplc_hbox_layout = QHBoxLayout(rplc_frame) rplc_hbox_layout.setContentsMargins(0, 0, 0, 0) rplc_hbox_layout.setSpacing(1) self.replace_field = EnchancedLineEdit(rplc_frame) self.replace_field.setPlaceholderText('replace text') self.replace_field.returnPressed.connect(self.on_replace) rplc_hbox_layout.addWidget(self.replace_field) self.replace_result_label = QLabel(rplc_frame) self.replace_result_label.setText(' ') rplc_hbox_layout.addWidget(self.replace_result_label) self.replace_button = replace_button = QPushButton("> &Replace >") replace_button.setFixedWidth(90) replace_button.clicked.connect(self.on_replace_click) rplc_hbox_layout.addWidget(replace_button) rplc_frame.setVisible(False) return rplc_frame def _create_found_frame(self): self.found_files_frame = ff_frame = QGroupBox("recursive search") ff_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.found_files_vbox_layout = QVBoxLayout(ff_frame) self.found_files_vbox_layout.setSpacing(0) self.found_files_vbox_layout.setContentsMargins(0, 0, 0, 0) self.found_files_list = QListWidget(ff_frame) self.found_files_list.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.found_files_list.setFrameStyle(QFrame.StyledPanel) self.found_files_list.itemActivated.connect(self.on_itemActivated) self.found_files_list.setStyleSheet("QListWidget {" "background-color:transparent;" "}" "QListWidget::item {" "background-color:transparent;" "}" "QListWidget::item:selected {" "background-color: darkgray;" "}") self.found_files_vbox_layout.addWidget(self.found_files_list) ff_frame.setCheckable(True) ff_frame.setChecked(False) ff_frame.setFlat(True) self.found_files_list.setVisible(False) return self.found_files_frame def keyPressEvent(self, event): ''' Enable the shortcats for search and replace ''' self.parent().keyPressEvent(event) def on_search(self): ''' Initiate the new search or request a next search result. ''' if self.current_search_text != self.search_field.text( ) or self._search_recursive != self.found_files_frame.isChecked(): # clear current search results self._reset() self.current_search_text = self.search_field.text() if self.current_search_text: path_text = {} self._wait_for_result = True for i in range(self._tabwidget.count()): path_text[self._tabwidget.widget( i).filename] = self._tabwidget.widget( i).document().toPlainText() self._search_recursive = self.found_files_frame.isChecked() self._search_thread = TextSearchThread( self.current_search_text, self._tabwidget.currentWidget().filename, path_text=path_text, recursive=self._search_recursive) self._search_thread.search_result_signal.connect( self.on_search_result) self._search_thread.warning_signal.connect( self.on_warning_result) self._search_thread.start() elif self.search_results: self._check_position() if self.search_results: if self._search_result_index + 1 >= len(self.search_results): self._search_result_index = -1 self._search_result_index += 1 self.search_result_signal.emit( *self.search_results[self._search_result_index]) self.replace_button.setEnabled(True) self._update_label() def on_search_back(self): ''' Slot to handle the search back function. ''' self._check_position(False) if self.search_results: self._search_result_index -= 1 if self._search_result_index < 0: self._search_result_index = len(self.search_results) - 1 self._update_label() self.search_result_signal.emit( *self.search_results[self._search_result_index]) self.replace_button.setEnabled(True) def _check_position(self, forward=True): try: # if the position of the textCursor was changed by the user, move the search index cur_pos = self._tabwidget.currentWidget().textCursor().position() st, _f, pa, idx = self.search_results[self._search_result_index] sear_pos = idx + len(st) if cur_pos != sear_pos: first_idx = self._get_current_index_for_current_file() if first_idx != -1: st, _f, pa, idx = self.search_results[first_idx] sear_pos = idx + len(st) while cur_pos > sear_pos and self._tabwidget.currentWidget( ).filename == pa: first_idx += 1 st, _f, pa, idx = self.search_results[first_idx] sear_pos = idx + len(st) self._search_result_index = first_idx if forward: self._search_result_index -= 1 else: self._reset(True) except: pass def _get_current_index_for_current_file(self): for index in range(len(self.search_results)): _st, _f, pa, _idx = self.search_results[index] if self._tabwidget.currentWidget().filename == pa: return index return -1 def on_search_result(self, search_text, found, path, index): ''' Slot to handle the signals for search result. This signals are forwarded used search_result_signal. ''' if found and search_text == self.current_search_text: self.search_results_fileset.add(path) item = (search_text, found, path, index) if item not in self.search_results: self.search_results.append((search_text, found, path, index)) if self._wait_for_result: self._search_result_index += 1 if index >= self._tabwidget.currentWidget().textCursor( ).position() or self._tabwidget.currentWidget( ).filename != path: self._wait_for_result = False self.search_result_signal.emit( *self.search_results[self._search_result_index]) self.replace_button.setEnabled(True) if self.search_results: if len(self.search_results_fileset) > 1: for item in self.search_results_fileset: pkg, path = package_name(os.path.dirname(item)) itemstr = '%s [%s]' % (os.path.basename(item), pkg) if not self.found_files_list.findItems( itemstr, Qt.MatchExactly): list_item = QListWidgetItem(itemstr) list_item.setToolTip(item) self.found_files_list.addItem(list_item) self.found_files_frame.setVisible(True) self.found_files_list.setVisible( len(self.search_results_fileset) > 0) self._update_label() def on_warning_result(self, text): rospy.logwarn(text) def on_replace_click(self): self.on_replace() self.on_search() def on_replace(self): ''' Emits the replace signal, but only if currently selected text is equal to the searched one. ''' if self.search_results: try: search_text, _found, path, index = self.search_results[ self._search_result_index] cursor = self._tabwidget.currentWidget().textCursor() if cursor.selectedText() == search_text: rptxt = self.replace_field.text() for rindex in range(self._search_result_index + 1, len(self.search_results)): st, _f, pa, idx = self.search_results[rindex] if path == pa: self.search_results.pop(rindex) self.search_results.insert( rindex, (st, _f, pa, idx + len(rptxt) - len(st))) else: break self._remove_search_result(self._search_result_index) self.replace_signal.emit(search_text, path, index, rptxt) else: self.replace_button.setEnabled(False) except: import traceback print traceback.format_exc() pass def on_itemActivated(self, item): ''' Go to the results for the selected file entry in the list. ''' item_path = item.toolTip() new_search_index = -1 tmp_index = -1 search_index = -1 tmp_search_text = '' for search_text, found, path, index in self.search_results: new_search_index += 1 if item_path == path: if tmp_index == -1: tmp_index = new_search_index tmp_search_text = search_text search_index = index if new_search_index > self._search_result_index: self.search_result_signal.emit(search_text, found, path, index) self._search_result_index = new_search_index self._update_label() return if tmp_index != -1: self.search_result_signal.emit(tmp_search_text, True, item_path, search_index) self._search_result_index = tmp_index self._update_label() def on_search_text_changed(self, _text): ''' Clear search result if the text was changed. ''' self._reset() def _update_label(self, clear_label=False): ''' Updates the status label for search results. The info is created from search result lists. ''' msg = ' ' if self.search_results: count_files = len(self.search_results_fileset) msg = '%d/%d' % (self._search_result_index + 1, len(self.search_results)) if count_files > 1: msg = '%s(%d)' % (msg, count_files) if self._search_thread is not None and self._search_thread.is_alive(): msg = 'searching..%s' % msg elif not msg.strip() and self.current_search_text: msg = '0 found' self.current_search_text = '' if clear_label: msg = ' ' self.search_result_label.setText(msg) self.find_button_back.setEnabled(len(self.search_results)) def file_changed(self, path): ''' Clears search results if for changed file are some search results are available :param path: changed file path :type path: str ''' if path in self.search_results_fileset: self._reset() def set_replace_visible(self, value): self.rplc_frame.setVisible(value) self.raise_() self.activateWindow() if value: self.replace_field.setFocus() self.replace_field.selectAll() self.setWindowTitle("Find / Replace") else: self.setWindowTitle("Find") self.search_field.setFocus() def is_replace_visible(self): return self.rplc_frame.isVisible() def _reset(self, force_new_search=False): # clear current search results if self._search_thread is not None: self._search_thread.search_result_signal.disconnect() self._search_thread.stop() self._search_thread = None self.current_search_text = '' self.search_results = [] self.search_results_fileset = set() self.found_files_list.clear() self.found_files_list.setVisible(False) self._update_label(True) self._search_result_index = -1 self.find_button_back.setEnabled(False) if force_new_search: self.on_search() def enable(self): self.setVisible(True) # self.show() self.raise_() self.activateWindow() self.search_field.setFocus() self.search_field.selectAll() def _remove_search_result(self, index): try: self.search_results.pop(index) # create new set with files contain the search text new_path_set = set(path for _st, _fd, path, _idx in self.search_results) # remove the file from the list widget for pp in self.search_results_fileset - new_path_set: for wi_idx in range(self.found_files_list.count()): # we have to compare each tooltip of the item if pp == self.found_files_list.item(wi_idx).toolTip(): self.found_files_list.takeItem(wi_idx) break self.search_results_fileset = new_path_set self.found_files_list.setVisible( len(self.search_results_fileset) > 0) except: import traceback print traceback.format_exc()
class Editor(QMainWindow): ''' Creates a dialog to edit a launch file. ''' finished_signal = Signal(list) ''' finished_signal has as parameter the filenames of the initialization and is emitted, if this dialog was closed. ''' def __init__(self, filenames, search_text='', parent=None): ''' @param filenames: a list with filenames. The last one will be activated. @type filenames: C{[str, ...]} @param search_text: if not empty, searches in new document for first occurrence of the given text @type search_text: C{str} (Default: C{Empty String}) ''' QMainWindow.__init__(self, parent) self.setObjectName(' - '.join(['Editor', str(filenames)])) self.setAttribute(Qt.WA_DeleteOnClose, True) self.setWindowFlags(Qt.Window) self.mIcon = QIcon(":/icons/crystal_clear_edit_launch.png") self._error_icon = QIcon(":/icons/crystal_clear_warning.png") self._empty_icon = QIcon() self.setWindowIcon(self.mIcon) window_title = "ROSLaunch Editor" if filenames: window_title = self.__getTabName(filenames[0]) self.setWindowTitle(window_title) self.init_filenames = list(filenames) self._search_thread = None # list with all open files self.files = [] # create tabs for files self.main_widget = QWidget(self) self.verticalLayout = QVBoxLayout(self.main_widget) self.verticalLayout.setContentsMargins(0, 0, 0, 0) self.verticalLayout.setSpacing(1) self.verticalLayout.setObjectName("verticalLayout") self.tabWidget = EditorTabWidget(self) self.tabWidget.setTabPosition(QTabWidget.North) self.tabWidget.setDocumentMode(True) self.tabWidget.setTabsClosable(True) self.tabWidget.setMovable(False) self.tabWidget.setObjectName("tabWidget") self.tabWidget.tabCloseRequested.connect(self.on_close_tab) self.verticalLayout.addWidget(self.tabWidget) self.buttons = self._create_buttons() self.verticalLayout.addWidget(self.buttons) self.setCentralWidget(self.main_widget) self.find_dialog = TextSearchFrame(self.tabWidget, self) self.find_dialog.search_result_signal.connect(self.on_search_result) self.find_dialog.replace_signal.connect(self.on_replace) self.addDockWidget(Qt.RightDockWidgetArea, self.find_dialog) # open the files for f in filenames: if f: self.on_load_request(os.path.normpath(f), search_text) self.readSettings() self.find_dialog.setVisible(False) # def __del__(self): # print "******** destroy", self.objectName() def _create_buttons(self): # create the buttons line self.buttons = QWidget(self) self.horizontalLayout = QHBoxLayout(self.buttons) self.horizontalLayout.setContentsMargins(4, 0, 4, 0) self.horizontalLayout.setObjectName("horizontalLayout") # add the search button self.searchButton = QPushButton(self) self.searchButton.setObjectName("searchButton") # self.searchButton.clicked.connect(self.on_shortcut_find) self.searchButton.toggled.connect(self.on_toggled_find) self.searchButton.setText(self._translate("&Find")) self.searchButton.setToolTip('Open a search dialog (Ctrl+F)') self.searchButton.setFlat(True) self.searchButton.setCheckable(True) self.horizontalLayout.addWidget(self.searchButton) # add the replace button self.replaceButton = QPushButton(self) self.replaceButton.setObjectName("replaceButton") # self.replaceButton.clicked.connect(self.on_shortcut_replace) self.replaceButton.toggled.connect(self.on_toggled_replace) self.replaceButton.setText(self._translate("&Replace")) self.replaceButton.setToolTip('Open a search&replace dialog (Ctrl+R)') self.replaceButton.setFlat(True) self.replaceButton.setCheckable(True) self.horizontalLayout.addWidget(self.replaceButton) # add the goto button self.gotoButton = QPushButton(self) self.gotoButton.setObjectName("gotoButton") self.gotoButton.clicked.connect(self.on_shortcut_goto) self.gotoButton.setText(self._translate("&Goto line")) self.gotoButton.setShortcut("Ctrl+G") self.gotoButton.setToolTip('Open a goto dialog (Ctrl+G)') self.gotoButton.setFlat(True) self.horizontalLayout.addWidget(self.gotoButton) # add a tag button self.tagButton = self._create_tag_button(self) self.horizontalLayout.addWidget(self.tagButton) # add spacer spacerItem = QSpacerItem(515, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem) # add line number label self.pos_label = QLabel() self.horizontalLayout.addWidget(self.pos_label) # add spacer spacerItem = QSpacerItem(515, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem) # add save button self.saveButton = QPushButton(self) self.saveButton.setObjectName("saveButton") self.saveButton.clicked.connect(self.on_saveButton_clicked) self.saveButton.setText(self._translate("&Save")) self.saveButton.setShortcut("Ctrl+S") self.saveButton.setToolTip('Save the changes to the file (Ctrl+S)') self.saveButton.setFlat(True) self.horizontalLayout.addWidget(self.saveButton) return self.buttons def keyPressEvent(self, event): ''' Enable the shortcats for search and replace ''' if event.key() == Qt.Key_Escape: self.reject() elif event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_F: if self.tabWidget.currentWidget().hasFocus(): if not self.searchButton.isChecked(): self.searchButton.setChecked(True) else: self.on_toggled_find(True) else: self.searchButton.setChecked(not self.searchButton.isChecked()) elif event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_R: if self.tabWidget.currentWidget().hasFocus(): if not self.replaceButton.isChecked(): self.replaceButton.setChecked(True) else: self.on_toggled_replace(True) else: self.replaceButton.setChecked(not self.replaceButton.isChecked()) else: event.accept() QMainWindow.keyPressEvent(self, event) def _translate(self, text): if hasattr(QApplication, "UnicodeUTF8"): return QApplication.translate("Editor", text, None, QApplication.UnicodeUTF8) else: return QApplication.translate("Editor", text, None) def readSettings(self): if nm.settings().store_geometry: settings = nm.settings().qsettings(nm.settings().CFG_GUI_FILE) settings.beginGroup("editor") maximized = settings.value("maximized", 'false') == 'true' if maximized: self.showMaximized() else: self.resize(settings.value("size", QSize(800, 640))) self.move(settings.value("pos", QPoint(0, 0))) try: self.restoreState(settings.value("window_state")) except: import traceback print traceback.format_exc() settings.endGroup() def storeSetting(self): if nm.settings().store_geometry: settings = nm.settings().qsettings(nm.settings().CFG_GUI_FILE) settings.beginGroup("editor") settings.setValue("size", self.size()) settings.setValue("pos", self.pos()) settings.setValue("maximized", self.isMaximized()) settings.setValue("window_state", self.saveState()) settings.endGroup() def on_load_request(self, filename, search_text=''): ''' Loads a file in a new tab or focus the tab, if the file is already open. @param filename: the path to file @type filename: C{str} @param search_text: if not empty, searches in new document for first occurrence of the given text @type search_text: C{str} (Default: C{Empty String}) ''' if not filename: return self.tabWidget.setUpdatesEnabled(False) try: if filename not in self.files: tab_name = self.__getTabName(filename) editor = TextEdit(filename, self.tabWidget) linenumber_editor = LineNumberWidget(editor) tab_index = self.tabWidget.addTab(linenumber_editor, tab_name) self.files.append(filename) editor.setCurrentPath(os.path.basename(filename)) editor.load_request_signal.connect(self.on_load_request) editor.document().modificationChanged.connect(self.on_editor_modificationChanged) editor.cursorPositionChanged.connect(self.on_editor_positionChanged) editor.setFocus(Qt.OtherFocusReason) # editor.textChanged.connect(self.on_text_changed) editor.undoAvailable.connect(self.on_text_changed) self.tabWidget.setCurrentIndex(tab_index) # self.find_dialog.set_search_path(filename) else: for i in range(self.tabWidget.count()): if self.tabWidget.widget(i).filename == filename: self.tabWidget.setCurrentIndex(i) break except: import traceback rospy.logwarn("Error while open %s: %s", filename, traceback.format_exc(1)) self.tabWidget.setUpdatesEnabled(True) if search_text: try: self._search_thread.stop() self._search_thread = None except: pass self._search_thread = TextSearchThread(search_text, filename, path_text=self.tabWidget.widget(0).document().toPlainText(), recursive=True) self._search_thread.search_result_signal.connect(self.on_search_result_on_open) self._search_thread.start() def on_text_changed(self, value=""): if self.tabWidget.currentWidget().hasFocus(): self.find_dialog.file_changed(self.tabWidget.currentWidget().filename) def on_close_tab(self, tab_index): ''' Signal handling to close single tabs. @param tab_index: tab index to close @type tab_index: C{int} ''' try: doremove = True w = self.tabWidget.widget(tab_index) if w.document().isModified(): name = self.__getTabName(w.filename) result = QMessageBox.question(self, "Unsaved Changes", '\n\n'.join(["Save the file before closing?", name]), QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) if result == QMessageBox.Yes: self.tabWidget.currentWidget().save() elif result == QMessageBox.No: pass else: doremove = False if doremove: # remove the indexed files if w.filename in self.files: self.files.remove(w.filename) # close tab self.tabWidget.removeTab(tab_index) # close editor, if no tabs are open if not self.tabWidget.count(): self.close() except: import traceback rospy.logwarn("Error while close tab %s: %s", str(tab_index), traceback.format_exc(1)) def reject(self): if self.find_dialog.isVisible(): self.searchButton.setChecked(not self.searchButton.isChecked()) else: self.close() def closeEvent(self, event): ''' Test the open files for changes and save this if needed. ''' changed = [] # get the names of all changed files for i in range(self.tabWidget.count()): w = self.tabWidget.widget(i) if w.document().isModified(): changed.append(self.__getTabName(w.filename)) if changed: # ask the user for save changes if self.isHidden(): buttons = QMessageBox.Yes | QMessageBox.No else: buttons = QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel result = QMessageBox.question(self, "Unsaved Changes", '\n\n'.join(["Save the file before closing?", '\n'.join(changed)]), buttons) if result == QMessageBox.Yes: for i in range(self.tabWidget.count()): w = self.tabWidget.widget(i).save() event.accept() elif result == QMessageBox.No: event.accept() else: event.ignore() else: event.accept() if event.isAccepted(): self.storeSetting() self.finished_signal.emit(self.init_filenames) def on_editor_modificationChanged(self, value=None): ''' If the content was changed, a '*' will be shown in the tab name. ''' tab_name = self.__getTabName(self.tabWidget.currentWidget().filename) if (self.tabWidget.currentWidget().document().isModified()) or not QFileInfo(self.tabWidget.currentWidget().filename).exists(): tab_name = ''.join(['*', tab_name]) self.tabWidget.setTabText(self.tabWidget.currentIndex(), tab_name) def on_editor_positionChanged(self): ''' Shows the number of the line and column in a label. ''' cursor = self.tabWidget.currentWidget().textCursor() self.pos_label.setText(':%s:%s #%s' % (cursor.blockNumber() + 1, cursor.columnNumber(), cursor.position())) def __getTabName(self, lfile): base = os.path.basename(lfile).replace('.launch', '') (package, _) = package_name(os.path.dirname(lfile)) return '%s [%s]' % (base, package) ############################################################################## # HANDLER for buttons ############################################################################## def on_saveButton_clicked(self): ''' Saves the current document. This method is called if the C{save button} was clicked. ''' saved, errors, msg = self.tabWidget.currentWidget().save(True) if errors: QMessageBox.critical(self, "Error", msg) self.tabWidget.setTabIcon(self.tabWidget.currentIndex(), self._error_icon) self.tabWidget.setTabToolTip(self.tabWidget.currentIndex(), msg) elif saved: self.tabWidget.setTabIcon(self.tabWidget.currentIndex(), self._empty_icon) self.tabWidget.setTabToolTip(self.tabWidget.currentIndex(), '') def on_shortcut_find(self): pass def on_toggled_find(self, value): ''' Shows the search frame ''' if value: self.find_dialog.enable() else: self.replaceButton.setChecked(False) self.find_dialog.setVisible(False) self.tabWidget.currentWidget().setFocus() def on_toggled_replace(self, value): ''' Shows the replace lineedit in the search frame ''' if value: self.searchButton.setChecked(True) self.find_dialog.set_replace_visible(value) def on_shortcut_goto(self): ''' Opens a C{goto} dialog. ''' value = 1 ok = False try: value, ok = QInputDialog.getInt(self, "Goto", self.tr("Line number:"), QLineEdit.Normal, minValue=1, step=1) except: value, ok = QInputDialog.getInt(self, "Goto", self.tr("Line number:"), QLineEdit.Normal, min=1, step=1) if ok: if value > self.tabWidget.currentWidget().document().blockCount(): value = self.tabWidget.currentWidget().document().blockCount() curpos = self.tabWidget.currentWidget().textCursor().blockNumber() + 1 while curpos != value: mov = QTextCursor.NextBlock if curpos < value else QTextCursor.PreviousBlock self.tabWidget.currentWidget().moveCursor(mov) curpos = self.tabWidget.currentWidget().textCursor().blockNumber() + 1 self.tabWidget.currentWidget().setFocus(Qt.ActiveWindowFocusReason) ############################################################################## # SLOTS for search dialog ############################################################################## def on_search_result(self, search_text, found, path, index): ''' A slot to handle a found text. It goes to the position in the text and select the searched text. On new file it will be open. :param search_text: the searched text :type search_text: str :param found: the text was found or not :type found: bool :param path: the path of the file the text was found :type path: str :param index: the position in the text :type index: int ''' if found: if self.tabWidget.currentWidget().filename != path: focus_widget = QApplication.focusWidget() self.on_load_request(path) focus_widget.setFocus() cursor = self.tabWidget.currentWidget().textCursor() cursor.setPosition(index, QTextCursor.MoveAnchor) cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, len(search_text)) self.tabWidget.currentWidget().setTextCursor(cursor) def on_search_result_on_open(self, search_text, found, path, index): ''' Like on_search_result, but skips the text in comments. ''' if found: if self.tabWidget.currentWidget().filename != path: focus_widget = QApplication.focusWidget() self.on_load_request(path) focus_widget.setFocus() comment_start = self.tabWidget.currentWidget().document().find('<!--', index, QTextDocument.FindBackward) if not comment_start.isNull(): comment_end = self.tabWidget.currentWidget().document().find('-->', comment_start) if not comment_end.isNull() and comment_end.position() > index + len(search_text): # commented -> retrun return self.on_search_result(search_text, found, path, index) def on_replace(self, search_text, path, index, replaced_text): ''' A slot to handle a text replacement of the TextSearchFrame. :param search_text: the searched text :type search_text: str :param path: the path of the file the text was found :type path: str :param index: the position in the text :type index: int :param replaced_text: the new text :type replaced_text: str ''' cursor = self.tabWidget.currentWidget().textCursor() if cursor.selectedText() == search_text: cursor.insertText(replaced_text) ############################################################################## # LAUNCH TAG insertion ############################################################################## def _create_tag_button(self, parent=None): btn = QPushButton(parent) btn.setObjectName("tagButton") btn.setText(self._translate("Add &tag")) btn.setShortcut("Ctrl+T") btn.setToolTip('Adds a ROS launch tag to launch file (Ctrl+T)') btn.setMenu(self._create_tag_menu(btn)) btn.setFlat(True) return btn def _create_tag_menu(self, parent=None): # creates a tag menu tag_menu = QMenu("ROS Tags", parent) # group tag add_group_tag_action = QAction("<group>", self, statusTip="", triggered=self._on_add_group_tag) add_group_tag_action.setShortcuts(QKeySequence("Ctrl+Shift+g")) tag_menu.addAction(add_group_tag_action) # node tag add_node_tag_action = QAction("<node>", self, statusTip="", triggered=self._on_add_node_tag) add_node_tag_action.setShortcuts(QKeySequence("Ctrl+Shift+n")) tag_menu.addAction(add_node_tag_action) # node tag with all attributes add_node_tag_all_action = QAction("<node all>", self, statusTip="", triggered=self._on_add_node_tag_all) tag_menu.addAction(add_node_tag_all_action) # include tag with all attributes add_include_tag_all_action = QAction("<include>", self, statusTip="", triggered=self._on_add_include_tag_all) add_include_tag_all_action.setShortcuts(QKeySequence("Ctrl+Shift+i")) tag_menu.addAction(add_include_tag_all_action) # remap add_remap_tag_action = QAction("<remap>", self, statusTip="", triggered=self._on_add_remap_tag) add_remap_tag_action.setShortcuts(QKeySequence("Ctrl+Shift+r")) tag_menu.addAction(add_remap_tag_action) # env tag add_env_tag_action = QAction("<env>", self, statusTip="", triggered=self._on_add_env_tag) tag_menu.addAction(add_env_tag_action) # param tag add_param_tag_action = QAction("<param>", self, statusTip="", triggered=self._on_add_param_tag) add_param_tag_action.setShortcuts(QKeySequence("Ctrl+Shift+p")) tag_menu.addAction(add_param_tag_action) # param capability group tag add_param_cap_group_tag_action = QAction("<param capability group>", self, statusTip="", triggered=self._on_add_param_cap_group_tag) add_param_cap_group_tag_action.setShortcuts(QKeySequence("Ctrl+Alt+p")) tag_menu.addAction(add_param_cap_group_tag_action) # param tag with all attributes add_param_tag_all_action = QAction("<param all>", self, statusTip="", triggered=self._on_add_param_tag_all) tag_menu.addAction(add_param_tag_all_action) # rosparam tag with all attributes add_rosparam_tag_all_action = QAction("<rosparam>", self, statusTip="", triggered=self._on_add_rosparam_tag_all) tag_menu.addAction(add_rosparam_tag_all_action) # arg tag with default definition add_arg_tag_default_action = QAction("<arg default>", self, statusTip="", triggered=self._on_add_arg_tag_default) add_arg_tag_default_action.setShortcuts(QKeySequence("Ctrl+Shift+a")) tag_menu.addAction(add_arg_tag_default_action) # arg tag with value definition add_arg_tag_value_action = QAction("<arg value>", self, statusTip="", triggered=self._on_add_arg_tag_value) add_arg_tag_value_action.setShortcuts(QKeySequence("Ctrl+Alt+a")) tag_menu.addAction(add_arg_tag_value_action) # test tag add_test_tag_action = QAction("<test>", self, statusTip="", triggered=self._on_add_test_tag) add_test_tag_action.setShortcuts(QKeySequence("Ctrl+Alt+t")) tag_menu.addAction(add_test_tag_action) # test tag with all attributes add_test_tag_all_action = QAction("<test all>", self, statusTip="", triggered=self._on_add_test_tag_all) tag_menu.addAction(add_test_tag_all_action) return tag_menu def _insert_text(self, text): cursor = self.tabWidget.currentWidget().textCursor() if not cursor.isNull(): col = cursor.columnNumber() spaces = ''.join([' ' for _ in range(col)]) cursor.insertText(text.replace('\n', '\n%s' % spaces)) self.tabWidget.currentWidget().setFocus(Qt.OtherFocusReason) def _on_add_group_tag(self): self._insert_text('<group ns="namespace" clear_params="true|false">\n' '</group>') def _on_add_node_tag(self): dia = PackageDialog() if dia.exec_(): self._insert_text('<node name="%s" pkg="%s" type="%s">\n' '</node>' % (dia.binary, dia.package, dia.binary)) def _on_add_node_tag_all(self): dia = PackageDialog() if dia.exec_(): self._insert_text('<node name="%s" pkg="%s" type="%s"\n' ' args="arg1" machine="machine-name"\n' ' respawn="true" required="true"\n' ' ns="foo" clear_params="true|false"\n' ' output="log|screen" cwd="ROS_HOME|node"\n' ' launch-prefix="prefix arguments">\n' '</node>' % (dia.binary, dia.package, dia.binary)) def _on_add_include_tag_all(self): self._insert_text('<include file="$(find pkg-name)/path/filename.xml"\n' ' ns="foo" clear_params="true|false">\n' '</include>') def _on_add_remap_tag(self): self._insert_text('<remap from="original" to="new"/>') def _on_add_env_tag(self): self._insert_text('<env name="variable" value="value"/>') def _on_add_param_tag(self): self._insert_text('<param name="ns_name" value="value" />') def _on_add_param_cap_group_tag(self): self._insert_text('<param name="capability_group" value="demo" />') def _on_add_param_tag_all(self): self._insert_text('<param name="ns_name" value="value"\n' ' type="str|int|double|bool"\n' ' textfile="$(find pkg-name)/path/file.txt"\n' ' binfile="$(find pkg-name)/path/file"\n' ' command="$(find pkg-name)/exe \'$(find pkg-name)/arg.txt\'">\n' '</param>') def _on_add_rosparam_tag_all(self): self._insert_text('<rosparam param="param-name"\n' ' file="$(find pkg-name)/path/foo.yaml"\n' ' command="load|dump|delete"\n' ' ns="namespace">\n' '</rosparam>') def _on_add_arg_tag_default(self): self._insert_text('<arg name="foo" default="1" />') def _on_add_arg_tag_value(self): self._insert_text('<arg name="foo" value="bar" />') def _on_add_test_tag(self): dia = PackageDialog() if dia.exec_(): self._insert_text('<test name="%s" pkg="%s" type="%s" test-name="test_%s">\n' '</test>' % (dia.binary, dia.package, dia.binary, dia.binary)) def _on_add_test_tag_all(self): dia = PackageDialog() if dia.exec_(): self._insert_text('<test name="%s" pkg="%s" type="%s" test-name="test_%s">\n' ' args="arg1" time-limit="60.0"\n' ' ns="foo" clear_params="true|false"\n' ' cwd="ROS_HOME|node" retry="0"\n' ' launch-prefix="prefix arguments">\n' '</test>' % (dia.binary, dia.package, dia.binary, dia.binary))
class Editor(QMainWindow): ''' Creates a dialog to edit a launch file. ''' finished_signal = Signal(list) ''' finished_signal has as parameter the filenames of the initialization and is emitted, if this dialog was closed. ''' def __init__(self, filenames, search_text='', master_name='', parent=None): ''' :param filenames: a list with filenames. The last one will be activated. :type filenames: [str] :param str search_text: if not empty, searches in new document for first occurrence of the given text ''' QMainWindow.__init__(self, parent) self.setObjectName('Editor - %s' % utf8(filenames)) self.setAttribute(Qt.WA_DeleteOnClose, True) self.setWindowFlags(Qt.Window) self.mIcon = nm.settings().icon('crystal_clear_edit_launch.png') self._error_icon = nm.settings().icon('warning.png') self._info_icon = nm.settings().icon('info.png') self._empty_icon = QIcon() self.setWindowIcon(self.mIcon) window_title = "ROSLaunch Editor" if filenames: window_title = self.__getTabName(filenames[0]) self.setWindowTitle('%s @%s' % (window_title, master_name)) self.init_filenames = filenames self._search_node_count = 0 self._search_thread = None self._last_search_request = None # list with all open files self.files = [] # create tabs for files self.main_widget = QWidget(self) self.main_widget.setObjectName("editorMain") self.verticalLayout = QVBoxLayout(self.main_widget) self.verticalLayout.setContentsMargins(0, 0, 0, 0) self.verticalLayout.setSpacing(1) self.verticalLayout.setObjectName("verticalLayout") self.tabWidget = EditorTabWidget(self) self.tabWidget.setTabPosition(QTabWidget.North) self.tabWidget.setDocumentMode(True) self.tabWidget.setTabsClosable(True) self.tabWidget.setMovable(False) self.tabWidget.setObjectName("tabWidget") self.tabWidget.tabCloseRequested.connect(self.on_close_tab) self.tabWidget.currentChanged.connect(self.on_tab_changed) self.verticalLayout.addWidget(self.tabWidget) self.log_dock = self._create_log_bar() self.addDockWidget(Qt.BottomDockWidgetArea, self.log_dock) # self.verticalLayout.addWidget(self.log_bar) self.buttons = self._create_buttons() self.verticalLayout.addWidget(self.buttons) self.setCentralWidget(self.main_widget) self.find_dialog = TextSearchFrame(self.tabWidget, self) self.find_dialog.found_signal.connect(self.on_search_result) self.find_dialog.replace_signal.connect(self.on_replace) self.addDockWidget(Qt.RightDockWidgetArea, self.find_dialog) self.graph_view = GraphViewWidget(self.tabWidget, self) self.graph_view.load_signal.connect(self.on_graph_load_file) self.graph_view.goto_signal.connect(self.on_graph_goto) self.graph_view.search_signal.connect(self.on_load_request) self.graph_view.finished_signal.connect(self.on_graph_finished) self.graph_view.info_signal.connect(self.on_graph_info) self.addDockWidget(Qt.RightDockWidgetArea, self.graph_view) self.readSettings() self.find_dialog.setVisible(False) self.graph_view.setVisible(False) nm.nmd().file.changed_file.connect(self.on_changed_file) nm.nmd().file.packages_available.connect(self._on_new_packages) # open the files for f in filenames: if f: self.on_load_request(f, search_text, only_launch=True) self.log_dock.setVisible(False) try: pal = self.tabWidget.palette() self._default_color = pal.color(QPalette.Window) color = QColor.fromRgb(nm.settings().host_color(master_name, self._default_color.rgb())) bg_style_launch_dock = "QWidget#editorMain { background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 %s, stop: 0.7 %s);}" % (color.name(), self._default_color.name()) self.setStyleSheet('%s' % (bg_style_launch_dock)) except Exception as _: pass # import traceback # print(traceback.format_exc()) # def __del__(self): # print "******** destroy", self.objectName() def _create_buttons(self): # create the buttons line self.buttons = QWidget(self) self.horizontalLayout = QHBoxLayout(self.buttons) self.horizontalLayout.setContentsMargins(3, 0, 3, 0) self.horizontalLayout.setObjectName("horizontalLayout") # add open upper launchfile button self.upperButton = QPushButton(self) self.upperButton.setObjectName("upperButton") self.upperButton.clicked.connect(self.on_upperButton_clicked) self.upperButton.setIcon(nm.settings().icon('up.png')) self.upperButton.setShortcut("Ctrl+U") self.upperButton.setToolTip('Open the file which include the current file (Ctrl+U)') self.upperButton.setFlat(True) self.horizontalLayout.addWidget(self.upperButton) # add the goto button self.gotoButton = QPushButton(self) self.gotoButton.setObjectName("gotoButton") self.gotoButton.clicked.connect(self.on_shortcut_goto) self.gotoButton.setText(self._translate("&Goto line")) self.gotoButton.setShortcut("Ctrl+G") self.gotoButton.setToolTip('Open a goto dialog (Ctrl+G)') self.gotoButton.setFlat(True) self.horizontalLayout.addWidget(self.gotoButton) # add a tag button self.tagButton = self._create_tag_button(self) self.horizontalLayout.addWidget(self.tagButton) # add save button self.saveButton = QPushButton(self) self.saveButton.setObjectName("saveButton") self.saveButton.setIcon(QIcon.fromTheme("document-save")) self.saveButton.clicked.connect(self.on_saveButton_clicked) self.saveButton.setText(self._translate("&Save")) self.saveButton.setShortcut("Ctrl+S") self.saveButton.setToolTip('Save the changes to the file (Ctrl+S)') self.saveButton.setFlat(True) self.horizontalLayout.addWidget(self.saveButton) # add spacer spacerItem = QSpacerItem(515, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem) # add line number label self.pos_label = QLabel() self.horizontalLayout.addWidget(self.pos_label) # add spacer spacerItem = QSpacerItem(515, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem) # add show log button self.show_log_button = QPushButton("Log>>", self) self.show_log_button.setObjectName("show_log_button") self.show_log_button.clicked.connect(self.on_toggled_log) self.show_log_button.setFlat(True) self.show_log_button.setCheckable(True) self.horizontalLayout.addWidget(self.show_log_button) # add graph button self.graphButton = QPushButton(self) self.graphButton.setObjectName("graphButton") self.graphButton.toggled.connect(self.on_toggled_graph) self.graphButton.setText("Includ&e Graph >>") self.graphButton.setCheckable(True) self.graphButton.setShortcut("Ctrl+E") self.graphButton.setToolTip('Shows include and include from files (Ctrl+E)') self.graphButton.setFlat(True) self.horizontalLayout.addWidget(self.graphButton) # add the search button self.searchButton = QPushButton(self) self.searchButton.setObjectName("searchButton") # self.searchButton.clicked.connect(self.on_shortcut_find) self.searchButton.toggled.connect(self.on_toggled_find) self.searchButton.setText(self._translate("&Find >>")) self.searchButton.setToolTip('Open a search dialog (Ctrl+F)') self.searchButton.setFlat(True) self.searchButton.setCheckable(True) self.horizontalLayout.addWidget(self.searchButton) # add the replace button self.replaceButton = QPushButton(self) self.replaceButton.setObjectName("replaceButton") # self.replaceButton.clicked.connect(self.on_shortcut_replace) self.replaceButton.toggled.connect(self.on_toggled_replace) self.replaceButton.setText(self._translate("&Replace >>")) self.replaceButton.setToolTip('Open a search&replace dialog (Ctrl+R)') self.replaceButton.setFlat(True) self.replaceButton.setCheckable(True) self.horizontalLayout.addWidget(self.replaceButton) return self.buttons def _create_log_bar(self): self.log_dock = QDockWidget(self) self.log_dock.setObjectName('LogFrame') self.log_dock.setFeatures(QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable) self.log_bar = QWidget(self) self.horizontal_layout_log_bar = QHBoxLayout(self.log_bar) self.horizontal_layout_log_bar.setContentsMargins(2, 0, 2, 0) self.horizontal_layout_log_bar.setObjectName("horizontal_layout_log_bar") # add info label self._log_warning_count = 0 self.log_browser = QTextEdit() self.log_browser.setObjectName("log_browser") self.log_browser.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.log_browser.setLineWrapMode(QTextEdit.NoWrap) # self.log_browser.setMaximumHeight(120) color = QColor(255, 255, 235) bg_style = "QTextEdit#log_browser { background-color: %s;}" % color.name() self.log_bar.setStyleSheet("%s" % (bg_style)) self.horizontal_layout_log_bar.addWidget(self.log_browser) # add hide button self.clear_log_button = QPushButton("clear", self) self.clear_log_button.setObjectName("clear_log_button") self.clear_log_button.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Maximum) self.clear_log_button.clicked.connect(self.on_clear_log_button_clicked) self.clear_log_button.setFlat(True) self.horizontal_layout_log_bar.addWidget(self.clear_log_button) self.log_dock.setWidget(self.log_bar) return self.log_dock def keyPressEvent(self, event): ''' Enable the shortcats for search and replace ''' if event.key() == Qt.Key_Escape: self.reject() elif event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_F: if self.tabWidget.currentWidget().hasFocus(): if not self.searchButton.isChecked(): self.searchButton.setChecked(True) else: self.on_toggled_find(True) else: self.searchButton.setChecked(not self.searchButton.isChecked()) elif event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_R: if self.tabWidget.currentWidget().hasFocus(): if not self.replaceButton.isChecked(): self.replaceButton.setChecked(True) else: self.on_toggled_replace(True) else: self.replaceButton.setChecked(not self.replaceButton.isChecked()) elif event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_E: if self.tabWidget.currentWidget().hasFocus(): if not self.graphButton.isChecked(): self.graphButton.setChecked(True) else: self.on_toggled_graph(True) else: self.graphButton.setChecked(not self.graphButton.isChecked()) elif event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_W: self.on_close_tab(self.tabWidget.currentIndex()) elif event.modifiers() in [Qt.ControlModifier, Qt.AltModifier] and event.key() == Qt.Key_Up: self.on_upperButton_clicked() elif event.modifiers() in [Qt.ControlModifier, Qt.AltModifier] and event.key() == Qt.Key_Down: self.on_downButton_clicked() else: event.accept() QMainWindow.keyPressEvent(self, event) def _translate(self, text): if hasattr(QApplication, "UnicodeUTF8"): return QApplication.translate("Editor", text, None, QApplication.UnicodeUTF8) else: return QApplication.translate("Editor", text, None) def readSettings(self): if nm.settings().store_geometry: settings = nm.settings().qsettings(nm.settings().CFG_GUI_FILE) settings.beginGroup("editor") maximized = settings.value("maximized", 'false') == 'true' if maximized: self.showMaximized() else: self.resize(settings.value("size", QSize(800, 640))) self.move(settings.value("pos", QPoint(0, 0))) try: self.restoreState(settings.value("window_state")) except Exception: import traceback print(traceback.format_exc()) settings.endGroup() def storeSetting(self): if nm.settings().store_geometry: settings = nm.settings().qsettings(nm.settings().CFG_GUI_FILE) settings.beginGroup("editor") settings.setValue("size", self.size()) settings.setValue("pos", self.pos()) settings.setValue("maximized", self.isMaximized()) settings.setValue("window_state", self.saveState()) settings.endGroup() def on_load_request(self, filename, search_text='', insert_index=-1, goto_line=-1, only_launch=False, count_results=0): ''' Loads a file in a new tab or focus the tab, if the file is already open. :param str filename: the path to file :param str search_text: if not empty, searches in new document for first occurrence of the given text ''' if not filename: return self.tabWidget.setUpdatesEnabled(False) try: if filename not in self.files: tab_name = self.__getTabName(filename) editor = TextEdit(filename, parent=self) linenumber_editor = LineNumberWidget(editor) tab_index = 0 if insert_index > -1: tab_index = self.tabWidget.insertTab(insert_index, linenumber_editor, tab_name) else: tab_index = self.tabWidget.addTab(linenumber_editor, tab_name) self.files.append(filename) editor.setCurrentPath(os.path.basename(filename)) editor.load_request_signal.connect(self.on_load_request) editor.document().modificationChanged.connect(self.on_editor_modificationChanged) editor.cursorPositionChanged.connect(self.on_editor_positionChanged) editor.setFocus(Qt.OtherFocusReason) # editor.textChanged.connect(self.on_text_changed) editor.undoAvailable.connect(self.on_text_changed) self.tabWidget.setCurrentIndex(tab_index) # self.find_dialog.set_search_path(filename) else: for i in range(self.tabWidget.count()): if self.tabWidget.widget(i).filename == filename: self.tabWidget.setCurrentIndex(i) break self.tabWidget.setUpdatesEnabled(True) if search_text: if only_launch: self.find_dialog.found_files_list.clear() try: self._search_thread.stop() self._search_thread = None except Exception: pass # TODO: put all text of all tabs into path_text rospy.logdebug("serach for '%s'" % search_text) self._search_node_count = 0 self._search_thread = TextSearchThread(search_text, filename, recursive=True, only_launch=only_launch, count_results=count_results) self._search_thread.search_result_signal.connect(self.on_search_result_on_open) self._search_thread.warning_signal.connect(self.on_search_result_warning) self._last_search_request = (filename, search_text, insert_index, goto_line, only_launch) if not self.graph_view.is_loading(): self.on_graph_info("search thread: start search for '%s'" % self._search_thread._search_text) self._search_thread.start() if goto_line != -1: self._goto(goto_line, True) self.upperButton.setEnabled(self.tabWidget.count() > 1) except Exception as err: self.tabWidget.setUpdatesEnabled(True) import traceback msg = "Error while open %s: %s" % (filename, traceback.format_exc()) rospy.logwarn(msg) MessageBox.critical(self, "Error", utf8(err), msg) if self.tabWidget.count() == 0: self.close() def on_graph_load_file(self, path, insert_after=True): insert_index = self.tabWidget.currentIndex() + 1 if not insert_after and insert_index > 1: insert_index = self.tabWidget.currentIndex() self.on_load_request(path, insert_index=insert_index) def on_graph_goto(self, path, linenr): if path == self.tabWidget.currentWidget().filename: if linenr != -1: self._goto(linenr, True) def on_graph_finished(self): self.on_graph_info("build tree: finished", False) if self.graph_view.has_warnings: self.graphButton.setIcon(self._info_icon) else: self.graphButton.setIcon(self._empty_icon) if self._search_thread: try: self._search_thread.find_args_not_set = True self._search_thread.start() self.on_graph_info("search thread: start search for '%s'" % self._search_thread._search_text) except Exception: pass def on_graph_info(self, msg, warning=False): text_color = "#000000" if warning: self._log_warning_count += 1 if self._log_warning_count == 1: self.show_log_button.setIcon(self._error_icon) text_color = "#FE9A2E" text = ('<pre style="padding:10px;"><dt><font color="%s">' '%s</font></dt></pre>' % (text_color, msg)) self.log_browser.append(text) def on_text_changed(self, value=""): if self.tabWidget.currentWidget().hasFocus(): self.find_dialog.file_changed(self.tabWidget.currentWidget().filename) self._last_search_request = None def on_tab_changed(self, index): if index > -1: self.graph_view.set_file(self.tabWidget.widget(index).filename, self.tabWidget.widget(0).filename) self._last_search_request = None def on_close_tab(self, tab_index): ''' Signal handling to close single tabs. :param int tab_index: tab index to close ''' try: doremove = True w = self.tabWidget.widget(tab_index) if w.document().isModified(): name = self.__getTabName(w.filename) result = MessageBox.question(self, "Unsaved Changes", '\n\n'.join(["Save the file before closing?", name])) if result == MessageBox.Yes: self.tabWidget.currentWidget().save() elif result == MessageBox.No: pass elif rospy.is_shutdown(): doremove = False if doremove: # remove the indexed files if w.filename in self.files: self.files.remove(w.filename) # close tab self.tabWidget.removeTab(tab_index) # close editor, if no tabs are open if not self.tabWidget.count(): self.close() self._last_search_request = None except Exception: import traceback rospy.logwarn("Error while close tab %s: %s", str(tab_index), traceback.format_exc(1)) self.upperButton.setEnabled(self.tabWidget.count() > 1) def reject(self): if self.find_dialog.isVisible(): self.searchButton.setChecked(not self.searchButton.isChecked()) else: self.close() def on_changed_file(self, grpc_path, mtime): if grpc_path in self.files: for i in range(self.tabWidget.count()): if self.tabWidget.widget(i).filename == grpc_path: self.tabWidget.widget(i).file_changed(mtime) break if self._last_search_request is not None: self.on_load_request(*self._last_search_request) def closeEvent(self, event): ''' Test the open files for changes and save this if needed. ''' changed = [] # get the names of all changed files for i in range(self.tabWidget.count()): w = self.tabWidget.widget(i) if w.document().isModified(): changed.append(self.__getTabName(w.filename)) if changed: # ask the user for save changes if self.isHidden(): buttons = MessageBox.Yes | MessageBox.No else: buttons = MessageBox.Yes | MessageBox.No | MessageBox.Cancel result = MessageBox.question(self, "Unsaved Changes", '\n\n'.join(["Save the file before closing?", '\n'.join(changed)]), buttons=buttons) if result == MessageBox.Yes: for i in range(self.tabWidget.count()): w = self.tabWidget.widget(i).save() self.graph_view.clear_cache() event.accept() elif result == MessageBox.No: event.accept() elif rospy.is_shutdown(): event.ignore() else: event.accept() if event.isAccepted(): self.storeSetting() nm.nmd().file.changed_file.disconnect(self.on_changed_file) nm.nmd().file.packages_available.connect(self._on_new_packages) self.finished_signal.emit(self.init_filenames) def on_editor_modificationChanged(self, value=None): ''' If the content was changed, a '*' will be shown in the tab name. ''' tab_name = self.__getTabName(self.tabWidget.currentWidget().filename) if (self.tabWidget.currentWidget().document().isModified()): tab_name = '*%s' % tab_name self.tabWidget.setTabText(self.tabWidget.currentIndex(), tab_name) def on_editor_positionChanged(self): ''' Shows the number of the line and column in a label. ''' cursor = self.tabWidget.currentWidget().textCursor() self.pos_label.setText(':%s:%s #%s' % (cursor.blockNumber() + 1, cursor.columnNumber(), cursor.position())) def __getTabName(self, lfile): base = os.path.basename(lfile).replace('.launch', '') (package, _) = package_name(os.path.dirname(lfile)) return '%s [%s]' % (base, package) def _on_new_packages(self, url): try: if nmdurl.nmduri_from_path(url) == nmdurl.nmduri_from_path(self.tabWidget.currentWidget().filename): rospy.logdebug("packages updated, rebuild graph") if self.graph_view.has_none_packages: self.graph_view.clear_cache() except Exception: import traceback print(traceback.format_exc()) ############################################################################## # HANDLER for buttons ############################################################################## def on_clear_log_button_clicked(self): self._log_warning_count = 0 self.show_log_button.setIcon(self._empty_icon) self.log_browser.clear() self.log_dock.setVisible(False) self.show_log_button.setChecked(False) self.tabWidget.currentWidget().setFocus() def on_upperButton_clicked(self): ''' Opens the file which include the current open file ''' if self.tabWidget.currentIndex() != 0: self.graph_view.find_parent_file() def on_downButton_clicked(self): ''' Select editor right from current. ''' if self.tabWidget.currentIndex() < self.tabWidget.count(): self.tabWidget.setCurrentIndex(self.tabWidget.currentIndex() + 1) def on_saveButton_clicked(self): ''' Saves the current document. This method is called if the C{save button} was clicked. ''' saved, errors, msg = self.tabWidget.currentWidget().save() if errors: if msg: rospy.logwarn(msg) MessageBox.critical(self, "Error", "Error while save file: %s" % os.path.basename(self.tabWidget.currentWidget().filename), detailed_text=msg) self.tabWidget.setTabIcon(self.tabWidget.currentIndex(), self._error_icon) self.tabWidget.setTabToolTip(self.tabWidget.currentIndex(), msg) self.on_graph_info("saved failed %s: %s" % (self.tabWidget.currentWidget().filename, msg), True) elif saved: self.on_graph_info("saved %s" % self.tabWidget.currentWidget().filename) self.tabWidget.setTabIcon(self.tabWidget.currentIndex(), self._empty_icon) self.tabWidget.setTabToolTip(self.tabWidget.currentIndex(), '') self.graph_view.clear_cache() def on_shortcut_find(self): pass def on_toggled_log(self, value): ''' Shows the log bar ''' if value: self.log_dock.setVisible(True) else: self.log_dock.setVisible(False) self.tabWidget.currentWidget().setFocus() def on_toggled_graph(self, value): ''' Shows the search frame ''' if value: self.graph_view.enable() else: # self.replaceButton.setChecked(False) self.graph_view.setVisible(False) self.tabWidget.currentWidget().setFocus() def on_toggled_find(self, value, only_results=False): ''' Shows the search frame ''' if value: self.find_dialog.enable() self.find_dialog.find_frame.setVisible(not only_results) self.find_dialog.recursive_search_box.setVisible(not only_results) if not only_results: # clear results if not search text exists and we show not only search results if not self.find_dialog.search_field.text(): self.find_dialog.found_files_list.clear() else: self.replaceButton.setChecked(False) self.find_dialog.setVisible(False) self.tabWidget.currentWidget().setFocus() def on_toggled_replace(self, value): ''' Shows the replace lineedit in the search frame ''' if value: self.searchButton.setChecked(True) self.find_dialog.set_replace_visible(value) def on_shortcut_goto(self): ''' Opens a C{goto} dialog. ''' value = 1 ok = False try: value, ok = QInputDialog.getInt(self, "Goto", self.tr("Line number:"), QLineEdit.Normal, minValue=1, step=1) except Exception: value, ok = QInputDialog.getInt(self, "Goto", self.tr("Line number:"), QLineEdit.Normal, min=1, step=1) if ok: self._goto(value) self.tabWidget.currentWidget().setFocus(Qt.ActiveWindowFocusReason) def _goto(self, linenr, select_line=True): if linenr > self.tabWidget.currentWidget().document().blockCount(): linenr = self.tabWidget.currentWidget().document().blockCount() curpos = self.tabWidget.currentWidget().textCursor().blockNumber() + 1 while curpos != linenr: mov = QTextCursor.NextBlock if curpos < linenr else QTextCursor.PreviousBlock self.tabWidget.currentWidget().moveCursor(mov) curpos = self.tabWidget.currentWidget().textCursor().blockNumber() + 1 self.tabWidget.currentWidget().moveCursor(QTextCursor.EndOfBlock) self.tabWidget.currentWidget().moveCursor(QTextCursor.StartOfBlock, QTextCursor.KeepAnchor) ############################################################################## # SLOTS for search dialog ############################################################################## def on_search_result(self, search_text, found, path, startpos, endpos, linenr=-1, line_text=''): ''' A slot to handle a found text. It goes to the position in the text and select the searched text. On new file it will be open. :param search_text: the searched text :type search_text: str :param found: the text was found or not :type found: bool :param path: the path of the file the text was found :type path: str :param startpos: the position in the text :type startpos: int :param endpos: the end position in the text :type endpos: int ''' if found: if self.tabWidget.currentWidget().filename != path: focus_widget = QApplication.focusWidget() self.on_load_request(path) focus_widget.setFocus() self.tabWidget.currentWidget().select(startpos, endpos, False) def on_search_result_on_open(self, search_text, found, path, startpos, endpos, linenr, line_text): ''' Like on_search_result, but skips the text in comments. ''' if found: self._search_node_count += 1 if self._search_node_count > 1: self.on_toggled_find(True, only_results=True) self.find_dialog.current_search_text = search_text self.find_dialog.on_search_result(search_text, found, path, startpos, endpos, linenr, line_text) self.on_graph_info("search thread: found %s in '%s:%d'" % (search_text, path, linenr)) if self.tabWidget.currentWidget().filename != path: focus_widget = QApplication.focusWidget() self.on_load_request(path) if focus_widget is not None: focus_widget.setFocus() self.tabWidget.currentWidget().select(startpos, endpos, True) # self.on_search_result(search_text, found, path, startpos, endpos, linenr, line_text) def on_search_result_warning(self, msg): self.on_graph_info("search thread: %s" % (msg), True) def on_replace(self, search_text, path, index, replaced_text): ''' A slot to handle a text replacement of the TextSearchFrame. :param search_text: the searched text :type search_text: str :param path: the path of the file the text was found :type path: str :param index: the position in the text :type index: int :param replaced_text: the new text :type replaced_text: str ''' cursor = self.tabWidget.currentWidget().textCursor() if cursor.selectedText() == search_text: cursor.insertText(replaced_text) ############################################################################## # LAUNCH TAG insertion ############################################################################## def _show_custom_parameter_dialog(self): methods = {'nm/associations': self._on_add_cp_associations, 'capability_group': self._on_add_cp_capability_group, 'nm/kill_on_stop': self._on_add_cp_kill_on_stop, 'autostart/delay': self._on_add_cp_as_delay, 'autostart/exclude': self._on_add_cp_as_exclude, 'autostart/required_publisher': self._on_add_cp_as_req_publisher, 'respawn/max': self._on_add_cp_r_max, 'respawn/min_runtime': self._on_add_cp_r_min_runtime, 'respawn/delay': self._on_add_cp_r_delay, } res = SelectDialog.getValue('Insert custom parameter', "Select parameter to insert:", sorted(methods.keys()), exclusive=True, parent=self, select_if_single=False, store_geometry='insert_param') tags2insert = res[0] for tag in tags2insert: methods[tag]() def _create_tag_button(self, parent=None): btn = QPushButton(parent) btn.setObjectName("tagButton") btn.setText(self._translate("Add &tag")) # btn.setShortcut("Ctrl+T") btn.setToolTip('Adds a ROS launch tag to launch file') btn.setMenu(self._create_tag_menu(btn)) btn.setFlat(True) return btn def _create_tag_menu(self, parent=None): # creates a tag menu tag_menu = QMenu("ROS Tags", parent) # group tag add_group_tag_action = QAction("<group>", self, statusTip="", triggered=self._on_add_group_tag) add_group_tag_action.setShortcuts(QKeySequence("Ctrl+Shift+g")) tag_menu.addAction(add_group_tag_action) # node tag add_node_tag_action = QAction("<node>", self, statusTip="", triggered=self._on_add_node_tag) add_node_tag_action.setShortcuts(QKeySequence("Ctrl+Shift+n")) tag_menu.addAction(add_node_tag_action) # node tag with all attributes add_node_tag_all_action = QAction("<node all>", self, statusTip="", triggered=self._on_add_node_tag_all) tag_menu.addAction(add_node_tag_all_action) # include tag with all attributes add_include_tag_all_action = QAction("<include>", self, statusTip="", triggered=self._on_add_include_tag_all) add_include_tag_all_action.setShortcuts(QKeySequence("Ctrl+Shift+i")) tag_menu.addAction(add_include_tag_all_action) # remap add_remap_tag_action = QAction("<remap>", self, statusTip="", triggered=self._on_add_remap_tag) add_remap_tag_action.setShortcuts(QKeySequence("Ctrl+Shift+r")) tag_menu.addAction(add_remap_tag_action) # env tag add_env_tag_action = QAction("<env>", self, statusTip="", triggered=self._on_add_env_tag) tag_menu.addAction(add_env_tag_action) # param tag add_param_clipboard_tag_action = QAction("<param value>", self, statusTip="add value from clipboard", triggered=self._on_add_param_clipboard_tag) add_param_clipboard_tag_action.setShortcuts(QKeySequence("Ctrl+Shift+p")) tag_menu.addAction(add_param_clipboard_tag_action) add_param_tag_action = QAction("<param>", self, statusTip="", triggered=self._on_add_param_tag) add_param_tag_action.setShortcuts(QKeySequence("Ctrl+Shift+Alt+p")) tag_menu.addAction(add_param_tag_action) # param tag with all attributes add_param_tag_all_action = QAction("<param all>", self, statusTip="", triggered=self._on_add_param_tag_all) tag_menu.addAction(add_param_tag_all_action) # rosparam tag with all attributes add_rosparam_tag_all_action = QAction("<rosparam>", self, statusTip="", triggered=self._on_add_rosparam_tag_all) tag_menu.addAction(add_rosparam_tag_all_action) # arg tag with default definition add_arg_tag_default_action = QAction("<arg default>", self, statusTip="", triggered=self._on_add_arg_tag_default) add_arg_tag_default_action.setShortcuts(QKeySequence("Ctrl+Shift+a")) tag_menu.addAction(add_arg_tag_default_action) # arg tag with value definition add_arg_tag_value_action = QAction("<arg value>", self, statusTip="", triggered=self._on_add_arg_tag_value) add_arg_tag_value_action.setShortcuts(QKeySequence("Ctrl+Alt+a")) tag_menu.addAction(add_arg_tag_value_action) # test tag add_test_tag_action = QAction("<test>", self, statusTip="", triggered=self._on_add_test_tag) add_test_tag_action.setShortcuts(QKeySequence("Ctrl+Alt+t")) tag_menu.addAction(add_test_tag_action) # test tag with all attributes add_test_tag_all_action = QAction("<test all>", self, statusTip="", triggered=self._on_add_test_tag_all) tag_menu.addAction(add_test_tag_all_action) sub_cp_menu = QMenu("Custom parameters", parent) show_cp_dialog_action = QAction("Show Dialog", self, statusTip="", triggered=self._show_custom_parameter_dialog) show_cp_dialog_action.setShortcuts(QKeySequence("Ctrl+Shift+d")) sub_cp_menu.addAction(show_cp_dialog_action) add_cp_associations_action = QAction("nm/associations", self, statusTip="", triggered=self._on_add_cp_associations) add_cp_associations_action.setShortcuts(QKeySequence("Ctrl+Alt+a")) sub_cp_menu.addAction(add_cp_associations_action) sub_cp_as_menu = QMenu("Autostart", parent) add_cp_as_delay_action = QAction("delay", self, statusTip="", triggered=self._on_add_cp_as_delay) sub_cp_as_menu.addAction(add_cp_as_delay_action) add_cp_as_exclude_action = QAction("exclude", self, statusTip="", triggered=self._on_add_cp_as_exclude) sub_cp_as_menu.addAction(add_cp_as_exclude_action) add_cp_as_req_publisher_action = QAction("required publisher", self, statusTip="", triggered=self._on_add_cp_as_req_publisher) sub_cp_as_menu.addAction(add_cp_as_req_publisher_action) sub_cp_menu.addMenu(sub_cp_as_menu) sub_cp_r_menu = QMenu("Respawn", parent) add_cp_r_max_action = QAction("max", self, statusTip="", triggered=self._on_add_cp_r_max) sub_cp_r_menu.addAction(add_cp_r_max_action) add_cp_r_min_runtime_action = QAction("min_runtime", self, statusTip="", triggered=self._on_add_cp_r_min_runtime) sub_cp_r_menu.addAction(add_cp_r_min_runtime_action) add_cp_r_delay_action = QAction("delay", self, statusTip="", triggered=self._on_add_cp_r_delay) sub_cp_r_menu.addAction(add_cp_r_delay_action) sub_cp_menu.addMenu(sub_cp_r_menu) add_cp_capability_group_action = QAction("capability_group", self, statusTip="", triggered=self._on_add_cp_capability_group) add_cp_capability_group_action.setShortcuts(QKeySequence("Ctrl+Alt+p")) sub_cp_menu.addAction(add_cp_capability_group_action) add_cp_kill_on_stop_action = QAction("nm/kill_on_stop", self, statusTip="True or time to wait in ms", triggered=self._on_add_cp_kill_on_stop) add_cp_kill_on_stop_action.setShortcuts(QKeySequence("Ctrl+Shift+k")) sub_cp_menu.addAction(add_cp_kill_on_stop_action) tag_menu.addMenu(sub_cp_menu) return tag_menu def _insert_text(self, text, cursor_pose=None, selection_len=None): if self.tabWidget.currentWidget().isReadOnly(): return cursor = self.tabWidget.currentWidget().textCursor() if not cursor.isNull(): cursor.beginEditBlock() col = cursor.columnNumber() spaces = ''.join([' ' for _ in range(col)]) curr_cursor_pos = cursor.position() cursor.insertText(text.replace('\n', '\n%s' % spaces)) if cursor_pose is not None: cursor.setPosition(curr_cursor_pos + cursor_pose, QTextCursor.MoveAnchor) if selection_len is not None: cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor, selection_len) cursor.endEditBlock() self.tabWidget.currentWidget().setTextCursor(cursor) self.tabWidget.currentWidget().setFocus(Qt.OtherFocusReason) def _on_add_group_tag(self): self._insert_text('<group ns="namespace" clear_params="true|false">\n' '</group>', 11, 9) def _get_package_dialog(self): muri = masteruri_from_ros() if self.init_filenames: muri = nmdurl.masteruri(self.init_filenames[0]) return PackageDialog(muri) def _on_add_node_tag(self): dia = self._get_package_dialog() if dia.exec_(): self._insert_text('<node name="%s" pkg="%s" type="%s">\n' '</node>' % (dia.binary, dia.package, dia.binary)) def _on_add_node_tag_all(self): dia = self._get_package_dialog() if dia.exec_(): self._insert_text('<node name="%s" pkg="%s" type="%s"\n' ' args="arg1" machine="machine_name"\n' ' respawn="true" required="true"\n' ' ns="foo" clear_params="true|false"\n' ' output="log|screen" cwd="ROS_HOME|node"\n' ' launch-prefix="prefix arguments">\n' '</node>' % (dia.binary, dia.package, dia.binary)) def _on_add_include_tag_all(self): self._insert_text('<include file="$(find pkg-name)/path/filename.xml"\n' ' ns="foo" clear_params="true|false">\n' '</include>', 22, 27) def _on_add_remap_tag(self): self._insert_text('<remap from="original" to="new"/>', 13, 8) def _on_add_env_tag(self): self._insert_text('<env name="variable" value="value"/>', 11, 8) def _on_add_param_clipboard_tag(self): lines = QApplication.clipboard().mimeData().text().splitlines() name = "" if len(lines) == 1: name = lines[0] self._insert_text('<param name="%s" value="value" />' % name, 22 + len(name), 5) def _on_add_param_tag(self): self._insert_text('<param name="name" value="value" />', 13, 4) def _on_add_param_tag_all(self): self._insert_text('<param name="name" value="value"\n' ' type="str|int|double|bool"\n' ' textfile="$(find pkg-name)/path/file.txt"\n' ' binfile="$(find pkg-name)/path/file"\n' ' command="$(find pkg-name)/exe \'$(find pkg-name)/arg.txt\'">\n' '</param>', 13, 4) def _on_add_rosparam_tag_all(self): self._insert_text('<rosparam param="name"\n' ' file="$(find pkg-name)/path/foo.yaml"\n' ' command="load|dump|delete"\n' ' ns="namespace"\n' ' subst_value="true|false">\n' '</rosparam>', 17, 4) def _on_add_arg_tag_default(self): self._insert_text('<arg name="foo" default="1" />', 11, 3) def _on_add_arg_tag_value(self): self._insert_text('<arg name="foo" value="bar" />', 11, 3) def _on_add_test_tag(self): dia = self._get_package_dialog() if dia.exec_(): self._insert_text('<test name="%s" pkg="%s" type="%s" test-name="test_%s">\n' '</test>' % (dia.binary, dia.package, dia.binary, dia.binary)) def _on_add_test_tag_all(self): dia = self._get_package_dialog() if dia.exec_(): self._insert_text('<test name="%s" pkg="%s" type="%s" test-name="test_%s">\n' ' args="arg1" time-limit="60.0"\n' ' ns="foo" clear_params="true|false"\n' ' cwd="ROS_HOME|node" retry="0"\n' ' launch-prefix="prefix arguments">\n' '</test>' % (dia.binary, dia.package, dia.binary, dia.binary)) def _on_add_cp_capability_group(self): self._insert_text('<param name="capability_group" value="demo" />', 38, 4) def _on_add_cp_kill_on_stop(self): self._insert_text('<param name="nm/kill_on_stop" value="100" hint="[ms]" />', 34, 3) def _on_add_cp_associations(self): self._insert_text('<param name="nm/associations" value="node1,node2" hint="list of nodes" />', 34, 11) def _on_add_cp_as_delay(self): self._insert_text('<param name="autostart/delay" value="1" hint="[seconds]" />', 37, 1) def _on_add_cp_as_exclude(self): self._insert_text('<param name="autostart/exclude" value="True" />', 39, 4) def _on_add_cp_as_req_publisher(self): self._insert_text('<param name="autostart/required/publisher" value="topic" />', 50, 5) def _on_add_cp_r_max(self): self._insert_text('<param name="respawn/max" value="10" />', 33, 2) def _on_add_cp_r_min_runtime(self): self._insert_text('<param name="respawn/min_runtime" value="10" hint="[seconds]" />', 41, 2) def _on_add_cp_r_delay(self): self._insert_text('<param name="respawn/delay" value="5" hint="[seconds]" />', 31, 2)
def __init__(self, context, node=None): """ This class is intended to be called by rqt plugin framework class. Currently (12/12/2012) the whole widget is splitted into 2 panes: one on left allows you to choose the node(s) you work on. Right side pane lets you work with the parameters associated with the node(s) you select on the left. (12/27/2012) Despite the pkg name is changed to rqt_reconfigure to reflect the available functionality, file & class names remain 'param', expecting all the parameters will become handle-able. """ super(ParamWidget, self).__init__() self.setObjectName(self._TITLE_PLUGIN) self.setWindowTitle(self._TITLE_PLUGIN) rp = rospkg.RosPack() #TODO: .ui file needs to replace the GUI components declaration # below. For unknown reason, referring to another .ui files # from a .ui that is used in this class failed. So for now, # I decided not use .ui in this class. # If someone can tackle this I'd appreciate. _hlayout_top = QHBoxLayout(self) _hlayout_top.setContentsMargins(QMargins(0, 0, 0, 0)) self._splitter = QSplitter(self) _hlayout_top.addWidget(self._splitter) _vlayout_nodesel_widget = QWidget() _vlayout_nodesel_side = QVBoxLayout() _hlayout_filter_widget = QWidget(self) _hlayout_filter = QHBoxLayout() self._text_filter = TextFilter() self.filter_lineedit = TextFilterWidget(self._text_filter, rp) self.filterkey_label = QLabel("&Filter key:") self.filterkey_label.setBuddy(self.filter_lineedit) _hlayout_filter.addWidget(self.filterkey_label) _hlayout_filter.addWidget(self.filter_lineedit) _hlayout_filter_widget.setLayout(_hlayout_filter) self._nodesel_widget = NodeSelectorWidget(self, rp, self.sig_sysmsg) _vlayout_nodesel_side.addWidget(_hlayout_filter_widget) _vlayout_nodesel_side.addWidget(self._nodesel_widget) _vlayout_nodesel_side.setSpacing(1) _vlayout_nodesel_widget.setLayout(_vlayout_nodesel_side) reconf_widget = ParameditWidget(rp) self._splitter.insertWidget(0, _vlayout_nodesel_widget) self._splitter.insertWidget(1, reconf_widget) # 1st column, _vlayout_nodesel_widget, to minimize width. # 2nd col to keep the possible max width. self._splitter.setStretchFactor(0, 0) self._splitter.setStretchFactor(1, 1) # Signal from paramedit widget to node selector widget. reconf_widget.sig_node_disabled_selected.connect( self._nodesel_widget.node_deselected) # Pass name of node to editor widget self._nodesel_widget.sig_node_selected.connect( reconf_widget.show_reconf) if not node: title = self._TITLE_PLUGIN else: title = self._TITLE_PLUGIN + ' %s' % node self.setObjectName(title) #Connect filter signal-slots. self._text_filter.filter_changed_signal.connect( self._filter_key_changed) # Open any clients indicated from command line self.sig_selected.connect(self._nodesel_widget.node_selected) for rn in [rospy.resolve_name(c) for c in context.argv()]: if rn in self._nodesel_widget.get_paramitems(): self.sig_selected.emit(rn) else: rospy.logwarn('Could not find a dynamic reconfigure client named \'%s\'', str(rn))