class NetworkDiscoveryDialog(QDialog, threading.Thread): TIMEOUT = 0.1 display_clear_signal = Signal() display_append_signal = Signal(str) status_text_signal = Signal(str) network_join_request = Signal(int) def __init__(self, default_mcast_group, default_port, networks_count, parent=None): ''' Creates an input dialog. @param default_port: the default discovery port @type default_port: C{int} @param networks_count: the count of discovering ports @type networks_count: C{int} ''' QDialog.__init__(self, parent=parent) threading.Thread.__init__(self) self.default_port = default_port self.setObjectName('NetworkDiscoveryDialog') self.setAttribute(Qt.WA_DeleteOnClose, True) self.setWindowFlags(Qt.Window) self.setWindowTitle('Network Discovery') self.resize(728, 512) self.verticalLayout = QVBoxLayout(self) self.verticalLayout.setObjectName("verticalLayout") self.verticalLayout.setContentsMargins(1, 1, 1, 1) self.display = QTextBrowser(self) self.display.setReadOnly(True) self.verticalLayout.addWidget(self.display) self.display_clear_signal.connect(self.display.clear) self.display_append_signal.connect(self.display.append) self.display.anchorClicked.connect(self.on_anchorClicked) self.status_label = QLabel('0 messages', self) self.verticalLayout.addWidget(self.status_label) self.status_text_signal.connect(self.status_label.setText) self._msg_counts = dict() self._networks_count = networks_count self._running = True self._received_msgs = 0 self._discovered = dict() self._hosts = dict() # resolution for hostname and address self.mutex = threading.RLock() self.sockets = [] with self.mutex: try: for p in range(networks_count): msock = DiscoverSocket(default_port + p, default_mcast_group) self.sockets.append(msock) msock.settimeout(self.TIMEOUT) except Exception as e: self.display.setText(utf8(e)) self.setDaemon(True) self.start() def on_heartbeat_received(self, msg, address, is_multicast): force_update = False with self.mutex: try: hostname = self._hosts[address[0]] except Exception: self.status_text_signal.emit("resolve %s" % address[0]) hostname = nm.nameres().hostname(utf8(address[0]), resolve=True) self._hosts[address[0]] = hostname try: (_version, _msg_tuple) = Discoverer.msg2masterState(msg, address) index = address[1] - self.default_port if index not in self._discovered: self._discovered[index] = dict() self._discovered[index][address] = (hostname, time.time()) if hostname not in self._msg_counts: self._msg_counts[hostname] = 0 self._msg_counts[hostname] += 1 self._received_msgs += 1 force_update = True except Exception: print(traceback.format_exc(1)) if force_update: self._updateDisplay() def run(self): self.parent().masterlist_service.refresh(self.parent().getMasteruri(), False) while (not rospy.is_shutdown()) and self._running: with self.mutex: for msock in self.sockets: received = True while received: try: recv_item = msock.receive_queue.get(False) self._received_msgs += 1 self.on_heartbeat_received( recv_item.msg, recv_item.sender_addr, (recv_item.via == QueueReceiveItem.MULTICAST)) except queue.Empty: received = False status_text = 'received messages: %d' % (self._received_msgs) self.status_text_signal.emit(status_text) # self.parent().masterlist_service.refresh(self.parent().getMasteruri(), False) time.sleep(3) def closeEvent(self, event): self.stop() QDialog.closeEvent(self, event) def stop(self): self._running = False with self.mutex: for p in range(len(self.sockets)): try: self.sockets[p].close() except Exception: pass def _updateDisplay(self): self.display_clear_signal.emit() text = '<div style="font-family:Fixedsys,Courier,monospace; padding:10px;">\n' for index, addr_dict in self._discovered.items(): text = ''.join([ text, 'Network <b>', utf8(index), '</b>: <a href="', utf8(index), '">join</a><dl>' ]) for addr, (hostname, ts) in addr_dict.items(): text = ''.join([ text, '<dt>', self._getTsStr(ts), ' <b><u>', utf8(hostname), '</u></b> ', utf8(addr), ', received messages: ', str(self._msg_counts[hostname]), '</dt>\n' ]) text = ''.join([text, '</dl><br>']) text = ''.join([text, '</div>']) self.display_append_signal.emit(text) def _getTsStr(self, timestamp): dt = datetime.fromtimestamp(timestamp) diff = time.time() - timestamp diff_dt = datetime.fromtimestamp(diff) before = '0 sec' if (diff < 60): before = diff_dt.strftime('%S sec') elif (diff < 3600): before = diff_dt.strftime('%M:%S min') elif (diff < 86400): before = diff_dt.strftime('%H:%M:%S std') else: before = diff_dt.strftime('%d Day(s) %H:%M:%S') return ''.join([dt.strftime('%H:%M:%S'), ' (', before, ')']) def on_anchorClicked(self, url): self._updateDisplay() try: self.network_join_request.emit(int(url.toString())) except Exception: print(traceback.format_exc(1))
class SelectDialog(QDialog): ''' This dialog creates an input mask for a string list and return selected entries. ''' def __init__(self, items=list(), buttons=QDialogButtonBox.Cancel | QDialogButtonBox.Ok, exclusive=False, preselect_all=False, title='', description='', icon='', parent=None, select_if_single=True, checkitem1='', checkitem2='', closein=0): ''' Creates an input dialog. @param items: a list with strings @type items: C{list()} ''' QDialog.__init__(self, parent=parent) self.setObjectName(' - '.join(['SelectDialog', utf8(items)])) self.verticalLayout = QVBoxLayout(self) self.verticalLayout.setObjectName("verticalLayout") self.verticalLayout.setContentsMargins(1, 1, 1, 1) # add filter row self.filter_frame = QFrame(self) filterLayout = QHBoxLayout(self.filter_frame) filterLayout.setContentsMargins(1, 1, 1, 1) label = QLabel("Filter:", self.filter_frame) self.filter_field = EnchancedLineEdit(self.filter_frame) filterLayout.addWidget(label) filterLayout.addWidget(self.filter_field) self.filter_field.textChanged.connect(self._on_filter_changed) self.verticalLayout.addWidget(self.filter_frame) if description: self.description_frame = QFrame(self) descriptionLayout = QHBoxLayout(self.description_frame) # descriptionLayout.setContentsMargins(1, 1, 1, 1) if icon: self.icon_label = QLabel(self.description_frame) self.icon_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.icon_label.setPixmap( QPixmap(icon).scaled(30, 30, Qt.KeepAspectRatio)) descriptionLayout.addWidget(self.icon_label) self.description_label = QLabel(self.description_frame) self.description_label.setWordWrap(True) self.description_label.setText(description) descriptionLayout.addWidget(self.description_label) self.verticalLayout.addWidget(self.description_frame) # create area for the parameter self.content = MainBox(self) if items: self.scroll_area = QScrollArea(self) self.scroll_area.setFocusPolicy(Qt.NoFocus) self.scroll_area.setObjectName("scroll_area") self.scroll_area.setWidgetResizable(True) self.scroll_area.setWidget(self.content) self.verticalLayout.addWidget(self.scroll_area) self.checkitem1 = checkitem1 self.checkitem1_result = False self.checkitem2 = checkitem2 self.checkitem2_result = False # add select all option if not exclusive and items: self._ignore_next_toggle = False self.select_all_checkbox = QCheckBox('all entries') self.select_all_checkbox.setTristate(True) self.select_all_checkbox.stateChanged.connect( self._on_select_all_checkbox_stateChanged) self.verticalLayout.addWidget(self.select_all_checkbox) self.content.toggled.connect(self._on_main_toggle) if self.checkitem1: self.checkitem1_checkbox = QCheckBox(self.checkitem1) self.checkitem1_checkbox.stateChanged.connect( self._on_select_checkitem1_checkbox_stateChanged) self.verticalLayout.addWidget(self.checkitem1_checkbox) if self.checkitem2: self.checkitem2_checkbox = QCheckBox(self.checkitem2) self.checkitem2_checkbox.stateChanged.connect( self._on_select_checkitem2_checkbox_stateChanged) self.verticalLayout.addWidget(self.checkitem2_checkbox) if not items: spacerItem = QSpacerItem(1, 1, QSizePolicy.Expanding, QSizePolicy.Expanding) self.verticalLayout.addItem(spacerItem) self._close_timer = None self._closein = closein - 1 if closein > 0: self.closein_label = QLabel("OK in %d sec..." % closein) self.closein_label.setAlignment(Qt.AlignRight) self.verticalLayout.addWidget(self.closein_label) self._close_timer = threading.Timer(1.0, self._on_close_timer) self._close_timer.start() # create buttons self.buttonBox = QDialogButtonBox(self) self.buttonBox.setObjectName("buttonBox") self.buttonBox.setOrientation(Qt.Horizontal) self.buttonBox.setStandardButtons(buttons) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) self.verticalLayout.addWidget(self.buttonBox) # set the input fields if items: self.content.createFieldsFromValues(items, exclusive) if (select_if_single and len(items) == 1) or preselect_all: self.select_all_checkbox.setCheckState(Qt.Checked) if not items or len(items) < 7: self.filter_frame.setVisible(False) # print '=============== create', self.objectName() # # def __del__(self): # print "************ destroy", self.objectName() def _on_main_toggle(self, state): self.cancel_autoclose() self._ignore_next_toggle = state != self.select_all_checkbox.checkState( ) self.select_all_checkbox.setCheckState(state) def _on_select_all_checkbox_stateChanged(self, state): self.cancel_autoclose() if not self._ignore_next_toggle: self.content.setState(state) self._ignore_next_toggle = False def _on_select_checkitem1_checkbox_stateChanged(self, state): self.cancel_autoclose() if state == Qt.Checked: self.checkitem1_result = True elif state == Qt.Unchecked: self.checkitem1_result = False def _on_select_checkitem2_checkbox_stateChanged(self, state): self.cancel_autoclose() if state == Qt.Checked: self.checkitem2_result = True elif state == Qt.Unchecked: self.checkitem2_result = False def _on_filter_changed(self): self.content.filter(self.filter_field.text()) def _on_close_timer(self): self.closein_label.setText("OK in %d sec..." % self._closein) if self._closein == 0: self.buttonBox.accepted.emit() return self._closein -= 1 self._close_timer = threading.Timer(1.0, self._on_close_timer) self._close_timer.start() def cancel_autoclose(self): if self._close_timer is not None: self._close_timer.cancel() self.closein_label.setVisible(False) def getSelected(self): return self.content.getSelected() @staticmethod def getValue(title, description='', items=list(), exclusive=False, preselect_all=False, icon='', parent=None, select_if_single=True, checkitem1='', checkitem2='', closein=0): selectDia = SelectDialog(items, exclusive=exclusive, preselect_all=preselect_all, description=description, icon=icon, parent=parent, select_if_single=select_if_single, checkitem1=checkitem1, checkitem2=checkitem2, closein=closein) selectDia.setWindowTitle(title) selectDia.resize(480, 256) if selectDia.exec_(): if selectDia.checkitem2: return selectDia.getSelected( ), True, selectDia.checkitem1_result, selectDia.checkitem2_result if selectDia.checkitem1: return selectDia.getSelected( ), True, selectDia.checkitem1_result return selectDia.getSelected(), True if selectDia.checkitem2: return list(), False, False, False if selectDia.checkitem1: return list(), False, False return list(), False # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% # %%%%%%%%%%%%%%%%%% close handling %%%%%%%%%%%%%%%%%%%%% # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% def accept(self): self.cancel_autoclose() self.setResult(QDialog.Accepted) self.hide() def reject(self): self.cancel_autoclose() self.setResult(QDialog.Rejected) self.hide() def hideEvent(self, event): self.close() def closeEvent(self, event): ''' Test the open files for changes and save this if needed. ''' self.cancel_autoclose() self.setAttribute(Qt.WA_DeleteOnClose, True) QDialog.closeEvent(self, event)
class MessageBox(QDialog): NoIcon = 0 Information = 1 Warning = 2 Critical = 3 Question = 4 NoButton = 0 Ok = 1 # An "OK" button defined with the AcceptRole . Open = 2 # A "Open" button defined with the AcceptRole . Save = 4 # A "Save" button defined with the AcceptRole . Cancel = 8 # A "Cancel" button defined with the RejectRole . Close = 16 # A "Close" button defined with the RejectRole . Discard = 32 # A "Discard" or "Don't Save" button, depending on the platform, defined with the DestructiveRole . Apply = 64 # An "Apply" button defined with the ApplyRole . Reset = 128 # A "Reset" button defined with the ResetRole . RestoreDefaults = 256 # A "Restore Defaults" button defined with the ResetRole . Help = 512 # A "Help" button defined with the HelpRole . SaveAll = 1024 # A "Save All" button defined with the AcceptRole . Yes = 2048 # A "Yes" button defined with the YesRole . YesToAll = 4096 # A "Yes to All" button defined with the YesRole . No = 8192 # A "No" button defined with the NoRole . NoToAll = 16384 # A "No to All" button defined with the NoRole . Abort = 32768 # An "Abort" button defined with the RejectRole . Retry = 65536 # A "Retry" button defined with the AcceptRole . Ignore = 131072 # An "Ignore" button defined with the AcceptRole . Avoid = 262144 # An "'Don't show again'" button defined with the HelpRole, returns a default AcceptButton . def __init__(self, icon, title, text, detailed_text="", buttons=Cancel | Ok, parent=None): QDialog.__init__(self, parent=parent) self.setWindowFlags(self.windowFlags() & ~Qt.WindowTitleHint) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint & ~Qt.WindowMinimizeButtonHint) self.setObjectName('MessageBox') self._use_checkbox = True self.text = text self.verticalLayout = QVBoxLayout(self) self.verticalLayout.setObjectName("verticalLayout") self.verticalLayout.setContentsMargins(1, 1, 1, 1) self.horizontalLayout = QHBoxLayout() self.horizontalLayout.setObjectName("horizontalLayout") self.horizontalLayout.setContentsMargins(1, 1, 1, 1) # create icon pixmap = None if icon == self.NoIcon: pass elif icon == self.Question: pixmap = QPixmap( QImage(":icons/question.png").scaled(56, 56, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)) elif icon == self.Information: pixmap = QPixmap( QImage(":icons/info.png").scaled(56, 56, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)) elif icon == self.Warning: pixmap = QPixmap( QImage(":icons/warning.png").scaled(56, 56, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)) elif icon == self.Critical: pixmap = QPixmap( QImage(":icons/critical.png").scaled(56, 56, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)) spacerItem = QSpacerItem(10, 60, QSizePolicy.Minimum, QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem) self.icon_label = QLabel() if pixmap is not None: self.icon_label.setPixmap(pixmap) self.icon_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.horizontalLayout.addWidget(self.icon_label) spacerItem = QSpacerItem(10, 60, QSizePolicy.Minimum, QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem) # add message self.message_label = QLabel(text) self.message_label.setWordWrap(True) self.message_label.setScaledContents(True) self.message_label.setOpenExternalLinks(True) self.horizontalLayout.addWidget(self.message_label) self.verticalLayout.addLayout(self.horizontalLayout) # create buttons self.buttonBox = QDialogButtonBox(self) self.buttonBox.setObjectName("buttonBox") self.buttonBox.setOrientation(Qt.Horizontal) self._accept_button = None self._reject_button = None self._buttons = buttons self._create_buttons(buttons) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) self.verticalLayout.addWidget(self.buttonBox) if detailed_text: self.btn_show_details = QPushButton(self.tr('Details...')) self.btn_show_details.setCheckable(True) self.btn_show_details.setChecked(True) self.btn_show_details.toggled.connect(self.on_toggled_details) self.buttonBox.addButton(self.btn_show_details, QDialogButtonBox.ActionRole) # create area for detailed text self.textEdit = textEdit = QTextEdit(self) textEdit.setObjectName("textEdit") textEdit.setReadOnly(True) textEdit.setText(detailed_text) # textEdit.setVisible(False) self.verticalLayout.addWidget(self.textEdit) self.resize(480, self.verticalLayout.totalSizeHint().height()) buttons_in_box = self.buttonBox.buttons() if buttons_in_box: self.buttonBox.buttons()[0].setFocus() def setAcceptButton(self, button): ''' Sets the button with given ID to accept button if more then one button with AcceptRole was added to this dialog. Adds the buttton to the box if is not already in. :param int button: button id ''' if not button & self._buttons: self._create_buttons(button) self._accept_button = button def setRejectButton(self, button): ''' Sets the button with given ID to reject button if more then one button with RejectRole was added to this dialog. Adds the buttton to the box if is not already in. :param int button: button id ''' if not button & self._buttons: self._create_buttons(button) self._reject_button = button def on_toggled_details(self, checked): if checked: self.verticalLayout.addWidget(self.textEdit) else: self.verticalLayout.removeWidget(self.textEdit) self.textEdit.setVisible(checked) if not self.isMaximized(): self.setMinimumSize(self.verticalLayout.totalMinimumSize()) self.resize(self._current_size.width(), self.verticalLayout.totalSizeHint().height()) @staticmethod def about(parent, title, text, detailed_text='', buttons=Close): box = MessageBox(MessageBox.Information, title, text, detailed_text=detailed_text, buttons=buttons, parent=parent) if MessageBox.Yes & buttons: box.setAcceptButton(MessageBox.Yes) if MessageBox.Cancel & buttons: box.setRejectButton(MessageBox.Cancel) elif MessageBox.No & buttons: box.setRejectButton(MessageBox.No) return box.exec_() @staticmethod def information(parent, title, text, detailed_text='', buttons=Close): box = MessageBox(MessageBox.Information, title, text, detailed_text=detailed_text, buttons=buttons, parent=parent) if MessageBox.Yes & buttons: box.setAcceptButton(MessageBox.Yes) if MessageBox.Cancel & buttons: box.setRejectButton(MessageBox.Cancel) elif MessageBox.No & buttons: box.setRejectButton(MessageBox.No) return box.exec_() @staticmethod def question(parent, title, text, detailed_text='', buttons=Yes | No | Cancel): box = MessageBox(MessageBox.Question, title, text, detailed_text=detailed_text, buttons=buttons, parent=parent) if MessageBox.Yes & buttons: box.setAcceptButton(MessageBox.Yes) if MessageBox.Cancel & buttons: box.setRejectButton(MessageBox.Cancel) elif MessageBox.No & buttons: box.setRejectButton(MessageBox.No) return box.exec_() @staticmethod def warning(parent, title, text, detailed_text='', buttons=Ok): box = MessageBox(MessageBox.Warning, title, text, detailed_text=detailed_text, buttons=buttons, parent=parent) if MessageBox.Yes & buttons: box.setAcceptButton(MessageBox.Yes) if MessageBox.Cancel & buttons: box.setRejectButton(MessageBox.Cancel) elif MessageBox.No & buttons: box.setRejectButton(MessageBox.No) return box.exec_() @staticmethod def critical(parent, title, text, detailed_text='', buttons=Ok): box = MessageBox(MessageBox.Critical, title, text, detailed_text=detailed_text, buttons=buttons, parent=parent) if MessageBox.Yes & buttons: box.setAcceptButton(MessageBox.Yes) if MessageBox.Cancel & buttons: box.setRejectButton(MessageBox.Cancel) elif MessageBox.No & buttons: box.setRejectButton(MessageBox.No) return box.exec_() def resizeEvent(self, event): if not self.isMaximized(): self._current_size = event.size() # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% # %%%%%%%%%%%%%%%%%% close handling %%%%%%%%%%%%%%%%%%%%% # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% def exec_(self): if self.text in IGNORED_ERRORS: self.accept() return self.result() return QDialog.exec_(self) def accept(self): if self.result() == 0: if self._accept_button is not None: self.setResult(self._accept_button) else: self.setResult(1) self.accepted.emit() if self.isModal(): self.hide() def reject(self): if self.result() == 0: if self._reject_button is not None: self.setResult(self._reject_button) self.rejected.emit() self.hide() def hideEvent(self, event): # event.ignore() self.close() def closeEvent(self, event): self.setAttribute(Qt.WA_DeleteOnClose, True) QDialog.closeEvent(self, event) # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% # %%%%%%%%%%%%%%%%%% create buttons %%%%%%%%%%%%%%%%%%%%% # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% def _create_buttons(self, buttons): if MessageBox.Ok & buttons: self._accept_button = MessageBox.Ok bt = QPushButton(self.tr("&ok")) bt.clicked.connect(self._on_ok_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.AcceptRole) if MessageBox.Open & buttons: self._accept_button = MessageBox.Open bt = QPushButton(self.tr("&Open")) bt.clicked.connect(self._on_open_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.AcceptRole) if MessageBox.Save & buttons: self._accept_button = MessageBox.Save bt = QPushButton(self.tr("&Save")) bt.clicked.connect(self._on_save_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.AcceptRole) if MessageBox.Cancel & buttons: self._reject_button = MessageBox.Cancel bt = QPushButton(self.tr("&Cancel")) bt.clicked.connect(self._on_cancel_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.RejectRole) if MessageBox.Close & buttons: self._reject_button = MessageBox.Close bt = QPushButton(self.tr("&Close")) bt.clicked.connect(self._on_close_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.RejectRole) if MessageBox.Discard & buttons: bt = QPushButton(self.tr("&Discard")) bt.clicked.connect(self._on_discard_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.DestructiveRole) if MessageBox.Apply & buttons: bt = QPushButton(self.tr("&Apply")) bt.clicked.connect(self._on_apply_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.ApplyRole) if MessageBox.Reset & buttons: bt = QPushButton(self.tr("&Reset")) bt.clicked.connect(self._on_reset_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.ResetRole) if MessageBox.RestoreDefaults & buttons: bt = QPushButton(self.tr("&RestoreDefaults")) bt.clicked.connect(self._on_restore_defaults_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.ResetRole) if MessageBox.Help & buttons: bt = QPushButton(self.tr("&Help")) bt.clicked.connect(self._on_help_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.HelpRole) if MessageBox.SaveAll & buttons: self._accept_button = MessageBox.SaveAll bt = QPushButton(self.tr("&SaveAll")) bt.clicked.connect(self._on_saveall_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.AcceptRole) if MessageBox.Yes & buttons: bt = QPushButton(self.tr("&Yes")) bt.clicked.connect(self._on_yes_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.YesRole) if MessageBox.YesToAll & buttons: bt = QPushButton(self.tr("&YesToAll")) bt.clicked.connect(self._on_yestoall_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.YesRole) if MessageBox.No & buttons: bt = QPushButton(self.tr("&No")) bt.clicked.connect(self._on_no_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.NoRole) if MessageBox.NoToAll & buttons: bt = QPushButton(self.tr("&NoToAll")) bt.clicked.connect(self._on_notoall_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.NoRole) if MessageBox.Abort & buttons: self._reject_button = MessageBox.Abort bt = QPushButton(self.tr("&Abort")) bt.clicked.connect(self._on_abort_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.RejectRole) if MessageBox.Retry & buttons: self._accept_button = MessageBox.Retry bt = QPushButton(self.tr("&Retry")) bt.clicked.connect(self._on_retry_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.AcceptRole) if MessageBox.Ignore & buttons: self._accept_button = MessageBox.Ignore bt = QPushButton(self.tr("&Ignore")) bt.clicked.connect(self._on_ignore_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.AcceptRole) if MessageBox.Avoid & buttons: if self._use_checkbox: checkbox = QCheckBox("&Don't show again", self) checkbox.stateChanged.connect(self._check_ignore) self.buttonBox.addButton(checkbox, QDialogButtonBox.HelpRole) else: bt = QPushButton(self.tr("&Don't show again")) bt.setMaximumHeight(24) bt.clicked.connect(self._add_to_ignore) self.buttonBox.addButton(bt, QDialogButtonBox.HelpRole) def _on_ok_clicked(self): self.done(MessageBox.Ok) def _on_open_clicked(self): self.done(MessageBox.Open) def _on_save_clicked(self): self.done(MessageBox.Save) def _on_cancel_clicked(self): self.done(MessageBox.Cancel) def _on_close_clicked(self): self.done(MessageBox.Close) def _on_discard_clicked(self): self.done(MessageBox.Discard) def _on_apply_clicked(self): self.done(MessageBox.Apply) def _on_reset_clicked(self): self.done(MessageBox.Reset) def _on_restore_defaults_clicked(self): self.done(MessageBox.RestoreDefaults) def _on_help_clicked(self): self.done(MessageBox.Help) def _on_saveall_clicked(self): self.done(MessageBox.SaveAll) def _on_yes_clicked(self): self.done(MessageBox.Yes) def _on_yestoall_clicked(self): self.done(MessageBox.YesToAll) def _on_no_clicked(self): self.done(MessageBox.No) def _on_notoall_clicked(self): self.done(MessageBox.NoToAll) def _on_abort_clicked(self): self.done(MessageBox.Abort) def _on_retry_clicked(self): self.done(MessageBox.Retry) def _on_ignore_clicked(self): self.done(MessageBox.Ignore) def _add_to_ignore(self): IGNORED_ERRORS.append(self.text) self.accept() def _check_ignore(self, state): if state: IGNORED_ERRORS.append(self.text) else: try: IGNORED_ERRORS.remove(self.text) except Exception: pass
class EchoDialog(QDialog): MESSAGE_LINE_LIMIT = 128 MESSAGE_HZ_LIMIT = 10 MAX_DISPLAY_MSGS = 25 STATISTIC_QUEUE_LEN = 5000 ''' This dialog shows the output of a topic. ''' finished_signal = Signal(str) ''' finished_signal has as parameter the name of the topic and is emitted, if this dialog was closed. ''' msg_signal = Signal(object, bool) ''' msg_signal is a signal, which is emitted, if a new message was received. ''' text_hz_signal = Signal(str) text_signal = Signal(str) ''' text_signal is a signal, which is emitted, if a new text to display was received. ''' text_error_signal = Signal(str) ''' text_error_signal is a signal, which is emitted, if a new error text to display was received. ''' request_pw = Signal(object) def __init__(self, topic, msg_type, show_only_rate=False, masteruri=None, use_ssh=False, parent=None): ''' Creates an input dialog. @param topic: the name of the topic @type topic: C{str} @param msg_type: the type of the topic @type msg_type: C{str} @raise Exception: if no topic class was found for the given type ''' QDialog.__init__(self, parent=parent) self._masteruri = masteruri masteruri_str = '' if masteruri is None else '[%s]' % masteruri self.setObjectName(' - '.join(['EchoDialog', topic, masteruri_str])) self.setAttribute(Qt.WA_DeleteOnClose, True) self.setWindowFlags(Qt.Window) self.setWindowTitle('%s %s %s' % ('Echo --- ' if not show_only_rate else 'Hz --- ', topic, masteruri_str)) self.resize(728, 512) self.verticalLayout = QVBoxLayout(self) self.verticalLayout.setObjectName("verticalLayout") self.verticalLayout.setContentsMargins(1, 1, 1, 1) self.mIcon = QIcon(":/icons/crystal_clear_prop_run_echo.png") self.setWindowIcon(self.mIcon) self.topic = topic self.show_only_rate = show_only_rate self.lock = threading.RLock() self.last_printed_count = 0 self.msg_t0 = -1. self.msg_tn = 0 self.times = [] self.message_count = 0 self._rate_message = '' self._scrapped_msgs = 0 self._scrapped_msgs_sl = 0 self._last_received_ts = 0 self.receiving_hz = self.MESSAGE_HZ_LIMIT self.line_limit = self.MESSAGE_LINE_LIMIT self.field_filter_fn = None options = QWidget(self) if not show_only_rate: hLayout = QHBoxLayout(options) hLayout.setContentsMargins(1, 1, 1, 1) self.no_str_checkbox = no_str_checkbox = QCheckBox('Hide strings') no_str_checkbox.toggled.connect(self.on_no_str_checkbox_toggled) hLayout.addWidget(no_str_checkbox) self.no_arr_checkbox = no_arr_checkbox = QCheckBox('Hide arrays') no_arr_checkbox.toggled.connect(self.on_no_arr_checkbox_toggled) hLayout.addWidget(no_arr_checkbox) self.combobox_reduce_ch = QComboBox(self) self.combobox_reduce_ch.addItems( [str(self.MESSAGE_LINE_LIMIT), '0', '80', '256', '1024']) self.combobox_reduce_ch.activated[str].connect( self.combobox_reduce_ch_activated) self.combobox_reduce_ch.setEditable(True) self.combobox_reduce_ch.setToolTip( "Set maximum line width. 0 disables the limit.") hLayout.addWidget(self.combobox_reduce_ch) # reduce_ch_label = QLabel('ch', self) # hLayout.addWidget(reduce_ch_label) # add spacer spacerItem = QSpacerItem(515, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) hLayout.addItem(spacerItem) # add combobox for displaying frequency of messages self.combobox_displ_hz = QComboBox(self) self.combobox_displ_hz.addItems([ str(self.MESSAGE_HZ_LIMIT), '0', '0.1', '1', '50', '100', '1000' ]) self.combobox_displ_hz.activated[str].connect( self.on_combobox_hz_activated) self.combobox_displ_hz.setEditable(True) hLayout.addWidget(self.combobox_displ_hz) displ_hz_label = QLabel('Hz', self) hLayout.addWidget(displ_hz_label) # add combobox for count of displayed messages self.combobox_msgs_count = QComboBox(self) self.combobox_msgs_count.addItems( [str(self.MAX_DISPLAY_MSGS), '0', '50', '100']) self.combobox_msgs_count.activated[str].connect( self.on_combobox_count_activated) self.combobox_msgs_count.setEditable(True) self.combobox_msgs_count.setToolTip( "Set maximum displayed message count. 0 disables the limit.") hLayout.addWidget(self.combobox_msgs_count) displ_count_label = QLabel('#', self) hLayout.addWidget(displ_count_label) # add topic control button for unsubscribe and subscribe self.topic_control_button = QToolButton(self) self.topic_control_button.setText('stop') self.topic_control_button.setIcon( QIcon(':/icons/deleket_deviantart_stop.png')) self.topic_control_button.clicked.connect( self.on_topic_control_btn_clicked) hLayout.addWidget(self.topic_control_button) # add clear button clearButton = QToolButton(self) clearButton.setText('clear') clearButton.clicked.connect(self.on_clear_btn_clicked) hLayout.addWidget(clearButton) self.verticalLayout.addWidget(options) self.display = QTextBrowser(self) self.display.setReadOnly(True) self.verticalLayout.addWidget(self.display) self.display.document().setMaximumBlockCount(500) self.max_displayed_msgs = self.MAX_DISPLAY_MSGS self._blocks_in_msg = None self.display.setOpenLinks(False) self.display.anchorClicked.connect(self._on_display_anchorClicked) self.status_label = QLabel('0 messages', self) self.verticalLayout.addWidget(self.status_label) # subscribe to the topic errmsg = '' try: self.__msg_class = message.get_message_class(msg_type) if not self.__msg_class: errmsg = "Cannot load message class for [%s]. Did you build messages?" % msg_type # raise Exception("Cannot load message class for [%s]. Did you build messages?"%msg_type) except Exception as e: self.__msg_class = None errmsg = "Cannot load message class for [%s]. Did you build messagest?\nError: %s" % ( msg_type, e) # raise Exception("Cannot load message class for [%s]. Did you build messagest?\nError: %s"%(msg_type, e)) # variables for Subscriber self.msg_signal.connect(self._append_message) self.sub = None # vairables for SSH connection self.ssh_output_file = None self.ssh_error_file = None self.ssh_input_file = None self.text_signal.connect(self._append_text) self.text_hz_signal.connect(self._append_text_hz) self._current_msg = '' self._current_errmsg = '' self.text_error_signal.connect(self._append_error_text) # decide, which connection to open if use_ssh: self.__msg_class = None self._on_display_anchorClicked(QUrl(self._masteruri)) elif self.__msg_class is None: errtxt = '<pre style="color:red; font-family:Fixedsys,Courier,monospace; padding:10px;">\n%s</pre>' % ( errmsg) self.display.setText('<a href="%s">open using SSH</a>' % (masteruri)) self.display.append(errtxt) else: self.sub = rospy.Subscriber(self.topic, self.__msg_class, self._msg_handle) self.print_hz_timer = QTimer() self.print_hz_timer.timeout.connect(self._on_calc_hz) self.print_hz_timer.start(1000) # print "======== create", self.objectName() # # def __del__(self): # print "******* destroy", self.objectName() # def hideEvent(self, event): # self.close() def closeEvent(self, event): if self.sub is not None: self.sub.unregister() del self.sub try: self.ssh_output_file.close() self.ssh_error_file.close() # send Ctrl+C to remote process self.ssh_input_file.write('%s\n' % chr(3)) self.ssh_input_file.close() except: pass self.finished_signal.emit(self.topic) if self.parent() is None: QApplication.quit() # else: # self.setParent(None) def create_field_filter(self, echo_nostr, echo_noarr): def field_filter(val): try: # fields = val.__slots__ # field_types = val._slot_types for f, t in zip(val.__slots__, val._slot_types): if echo_noarr and '[' in t: continue elif echo_nostr and 'string' in t: continue yield f except: pass return field_filter def on_no_str_checkbox_toggled(self, state): self.field_filter_fn = self.create_field_filter( state, self.no_arr_checkbox.isChecked()) def on_no_arr_checkbox_toggled(self, state): self.field_filter_fn = self.create_field_filter( self.no_str_checkbox.isChecked(), state) def combobox_reduce_ch_activated(self, ch_txt): try: self.line_limit = int(ch_txt) except ValueError: try: self.line_limit = float(ch_txt) except ValueError: self.combobox_reduce_ch.setEditText(str(self.line_limit)) def on_combobox_hz_activated(self, hz_txt): try: self.receiving_hz = int(hz_txt) except ValueError: try: self.receiving_hz = float(hz_txt) except ValueError: self.combobox_displ_hz.setEditText(str(self.receiving_hz)) def on_combobox_count_activated(self, count_txt): try: self.max_displayed_msgs = int(count_txt) self._blocks_in_msg = None except ValueError: self.combobox_msgs_count.setEditText(str(self.max_displayed_msgs)) def on_clear_btn_clicked(self): self.display.clear() with self.lock: self.message_count = 0 self._scrapped_msgs = 0 del self.times[:] def on_topic_control_btn_clicked(self): try: if self.sub is None and self.ssh_output_file is None: if self.__msg_class: self.sub = rospy.Subscriber(self.topic, self.__msg_class, self._msg_handle) else: self._on_display_anchorClicked(QUrl(self._masteruri)) self.topic_control_button.setText('stop') self.topic_control_button.setIcon( QIcon(':/icons/deleket_deviantart_stop.png')) else: if self.sub is not None: self.sub.unregister() self.sub = None elif self.ssh_output_file is not None: self.ssh_output_file.close() self.ssh_error_file.close() self.ssh_output_file = None self.topic_control_button.setText('play') self.topic_control_button.setIcon( QIcon(':/icons/deleket_deviantart_play.png')) self.no_str_checkbox.setEnabled(True) self.no_arr_checkbox.setEnabled(True) except Exception as e: rospy.logwarn('Error while stop/play echo for topic %s: %s' % (self.topic, e)) def _msg_handle(self, data): self.msg_signal.emit(data, (data._connection_header['latching'] != '0')) def _append_message(self, msg, latched): ''' Adds a label to the dialog's layout and shows the given text. @param msg: the text to add to the dialog @type msg: message object ''' current_time = time.time() self._count_messages(current_time) # skip messages, if they are received often then MESSAGE_HZ_LIMIT if self._last_received_ts != 0 and self.receiving_hz != 0: if not latched and current_time - self._last_received_ts < 1.0 / self.receiving_hz: self._scrapped_msgs += 1 self._scrapped_msgs_sl += 1 return self._last_received_ts = current_time if not self.show_only_rate: # convert message to string and reduce line width to current limit msg = message.strify_message(msg, field_filter=self.field_filter_fn) if isinstance(msg, tuple): msg = msg[0] msg = self._trim_width(msg) msg = msg.replace('<', '<').replace('>', '>') # create a notification about scrapped messages if self._scrapped_msgs_sl > 0: txt = '<pre style="color:red; font-family:Fixedsys,Courier,monospace; padding:10px;">scrapped %s message because of Hz-settings</pre>' % self._scrapped_msgs_sl self.display.append(txt) self._scrapped_msgs_sl = 0 txt = '<pre style="background-color:#FFFCCC; font-family:Fixedsys,Courier; padding:10px;">---------- %s --------------------\n%s</pre>' % ( datetime.now().strftime("%d.%m.%Y %H:%M:%S.%f"), msg) # set the count of the displayed messages on receiving the first message self._update_max_msg_count(txt) self.display.append(txt) self._print_status() def _count_messages(self, ts=time.time()): ''' Counts the received messages. Call this method only on receive message. ''' current_time = ts with self.lock: # time reset if self.msg_t0 < 0 or self.msg_t0 > current_time: self.msg_t0 = current_time self.msg_tn = current_time self.times = [] else: self.times.append(current_time - self.msg_tn) self.msg_tn = current_time # keep only statistics for the last 5000 messages so as not to run out of memory if len(self.times) > self.STATISTIC_QUEUE_LEN: self.times.pop(0) self.message_count += 1 def _trim_width(self, msg): ''' reduce line width to current limit :param msg: the message :type msg: str :return: trimmed message ''' result = msg if self.line_limit != 0: a = '' for l in msg.splitlines(): a = a + (l if len(l) <= self.line_limit else l[0:self.line_limit - 3] + '...') + '\n' result = a return result def _update_max_msg_count(self, txt): ''' set the count of the displayed messages on receiving the first message :param txt: text of the message, which will be added to the document :type txt: str ''' if self._blocks_in_msg is None: td = QTextDocument(txt) self._blocks_in_msg = td.blockCount() self.display.document().setMaximumBlockCount( self._blocks_in_msg * self.max_displayed_msgs) def _on_calc_hz(self): if rospy.is_shutdown(): self.close() return if self.message_count == self.last_printed_count: return with self.lock: # the code from ROS rostopic n = len(self.times) if n < 2: return mean = sum(self.times) / n rate = 1. / mean if mean > 0. else 0 # std dev std_dev = math.sqrt(sum((x - mean)**2 for x in self.times) / n) # min and max max_delta = max(self.times) min_delta = min(self.times) self.last_printed_count = self.message_count self._rate_message = "average rate: %.3f\tmin: %.3fs max: %.3fs std dev: %.5fs window: %s" % ( rate, min_delta, max_delta, std_dev, n + 1) if self._scrapped_msgs > 0: self._rate_message += " --- scrapped msgs: %s" % self._scrapped_msgs self._print_status() if self.show_only_rate: self.display.append(self._rate_message) def _print_status(self): self.status_label.setText('%s messages %s' % (self.message_count, self._rate_message)) def _append_text(self, text): ''' Append echo text received through the SSH. ''' with self.lock: self._current_msg += text if self._current_msg.find('---') != -1: messages = self._current_msg.split('---') for m in messages[:-1]: current_time = time.time() self._count_messages(current_time) # limit the displayed text width m = self._trim_width(m) txt = '<pre style="background-color:#FFFCCC; font-family:Fixedsys,Courier; padding:10px;">---------- %s --------------------\n%s</pre>' % ( datetime.now().strftime("%d.%m.%Y %H:%M:%S.%f"), m) # set the count of the displayed messages on receiving the first message self._update_max_msg_count(txt) self.display.append(txt) self._current_msg = messages[-1] self._print_status() def _append_error_text(self, text): ''' Append error text received through the SSH. ''' with self.lock: self._current_errmsg += text if self._current_errmsg.find('\n') != -1: messages = self._current_errmsg.split('\n') for m in messages[:-1]: txt = '<pre style="color:red; font-family:Fixedsys,Courier,monospace; padding:10px;">%s</pre>' % m self.display.append(txt) self._current_errmsg = messages[-1] def _append_text_hz(self, text): ''' Append text received through the SSH for hz view. ''' with self.lock: self._current_msg += text if self._current_msg.find('\n') != -1: messages = self._current_msg.split('\n') for m in messages[:-1]: txt = '<div style="font-family:Fixedsys,Courier;">%s</div>' % ( m) self.display.append(txt) self._current_msg = messages[-1] def _on_display_anchorClicked(self, url, user=None, pw=None): try: ok = False if self.show_only_rate: self.ssh_input_file, self.ssh_output_file, self.ssh_error_file, ok = nm.ssh( ).ssh_exec(url.host(), ['rostopic hz %s' % (self.topic)], user, pw, auto_pw_request=True, get_pty=True) self.status_label.setText('connected to %s over SSH' % url.host()) else: self.combobox_displ_hz.setEnabled(False) nostr = '--nostr' if self.no_str_checkbox.isChecked() else '' noarr = '--noarr' if self.no_arr_checkbox.isChecked() else '' self.ssh_input_file, self.ssh_output_file, self.ssh_error_file, ok = nm.ssh( ).ssh_exec( url.host(), ['rostopic echo %s %s %s' % (nostr, noarr, self.topic)], user, pw, auto_pw_request=True, get_pty=True) if ok: self.display.clear() target = self._read_output_hz if self.show_only_rate else self._read_output thread = threading.Thread(target=target, args=((self.ssh_output_file, ))) thread.setDaemon(True) thread.start() thread = threading.Thread(target=self._read_error, args=((self.ssh_error_file, ))) thread.setDaemon(True) thread.start() elif self.ssh_output_file: self.ssh_output_file.close() self.ssh_error_file.close() except Exception as e: self._append_error_text('%s\n' % e) # import traceback # print traceback.format_exc() def _read_output_hz(self, output_file): try: while not output_file.closed: text = output_file.read(1) if text: self.text_hz_signal.emit(text) except: pass # import traceback # print traceback.format_exc() def _read_output(self, output_file): while not output_file.closed: text = output_file.read(1) if text: self.text_signal.emit(text) def _read_error(self, error_file): try: while not error_file.closed: text = error_file.read(1) if text: self.text_error_signal.emit(text) except: pass
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 SyncDialog(QDialog): ''' A dialog to set the sync options. ''' def __init__(self, parent=None): QDialog.__init__(self, parent) # self.host = host self.setWindowIcon(QIcon(":/icons/irondevil_sync.png")) self.setWindowTitle('Sync') self.verticalLayout = QVBoxLayout(self) self.verticalLayout.setObjectName("verticalLayout") self.resize(350, 190) self.toolButton_SyncAll = QToolButton(self) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(2) sizePolicy.setHeightForWidth(self.toolButton_SyncAll.sizePolicy().hasHeightForWidth()) self.toolButton_SyncAll.setSizePolicy(sizePolicy) self.toolButton_SyncAll.setObjectName("toolButton_SyncAll") self.verticalLayout.addWidget(self.toolButton_SyncAll) self.toolButton_SyncAll.setText(self._translate("Sync All")) self.toolButton_SyncAll.clicked.connect(self._on_sync_all_clicked) # self.toolButton_SyncAllAnyMsg = QToolButton(self) # sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # sizePolicy.setHorizontalStretch(0) # sizePolicy.setVerticalStretch(1) # sizePolicy.setHeightForWidth(self.toolButton_SyncAllAnyMsg.sizePolicy().hasHeightForWidth()) # self.toolButton_SyncAllAnyMsg.setSizePolicy(sizePolicy) # self.toolButton_SyncAllAnyMsg.setObjectName("toolButton_SyncAllAnyMsg") # self.verticalLayout.addWidget(self.toolButton_SyncAllAnyMsg) # self.toolButton_SyncAllAnyMsg.setText(self._translate("Sync all (+AnyMsg)")) # self.toolButton_SyncAllAnyMsg.clicked.connect(self._on_sync_all_anymsg_clicked) self.toolButton_SyncTopicOnDemand = QToolButton(self) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.toolButton_SyncTopicOnDemand.sizePolicy().hasHeightForWidth()) self.toolButton_SyncTopicOnDemand.setSizePolicy(sizePolicy) self.toolButton_SyncTopicOnDemand.setObjectName("toolButton_SyncTopicOnDemand") self.verticalLayout.addWidget(self.toolButton_SyncTopicOnDemand) self.toolButton_SyncTopicOnDemand.setText(self._translate("Sync only topics on demand")) self.toolButton_SyncTopicOnDemand.clicked.connect(self._on_sync_topics_on_demand_clicked) self.toolButton_SelectInterface = QToolButton(self) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.toolButton_SelectInterface.sizePolicy().hasHeightForWidth()) self.toolButton_SelectInterface.setSizePolicy(sizePolicy) self.toolButton_SelectInterface.setObjectName("toolButton_SelectInterface") self.verticalLayout.addWidget(self.toolButton_SelectInterface) self.toolButton_SelectInterface.setText(self._translate("Select an interface")) self.toolButton_SelectInterface.clicked.connect(self._on_select_interface_clicked) self.interface_field = QComboBox(self) self.interface_field.setInsertPolicy(QComboBox.InsertAlphabetically) self.interface_field.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)) self.interface_field.setEditable(True) self.interface_field.setVisible(False) self.interface_field.setObjectName("interface_field") self.verticalLayout.addWidget(self.interface_field) self.interface_field.currentIndexChanged[str].connect(self._on_interface_selected) self.toolButton_EditInterface = QToolButton(self) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.toolButton_EditInterface.sizePolicy().hasHeightForWidth()) self.toolButton_EditInterface.setSizePolicy(sizePolicy) self.toolButton_EditInterface.setObjectName("toolButton_EditInterface") self.verticalLayout.addWidget(self.toolButton_EditInterface) self.toolButton_EditInterface.setText(self._translate("Edit selected interface")) self.toolButton_EditInterface.clicked.connect(self._on_edit_interface_clicked) self.toolButton_EditInterface.setVisible(False) self.toolButton_CreateInterface = QToolButton(self) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.toolButton_CreateInterface.sizePolicy().hasHeightForWidth()) self.toolButton_CreateInterface.setSizePolicy(sizePolicy) self.toolButton_CreateInterface.setObjectName("toolButton_CreateInterface") self.verticalLayout.addWidget(self.toolButton_CreateInterface) self.toolButton_CreateInterface.setText(self._translate("Create an interface")) self.toolButton_CreateInterface.clicked.connect(self._on_create_interface_clicked) self.toolButton_CreateInterface.setVisible(False) self.textedit = TextEdit('', self) self.hl = SyncHighlighter(self.textedit.document()) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.textedit.setSizePolicy(sizePolicy) self.textedit.setObjectName("syncedit") self.verticalLayout.addWidget(self.textedit) self.textedit.setVisible(False) self._fill_interface_thread = None self._interfaces_files = None self._sync_args = [] self._interface_filename = None self.buttonBox = QDialogButtonBox(self) self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel) self.buttonBox.setOrientation(Qt.Horizontal) self.buttonBox.setObjectName("buttonBox") self.verticalLayout.addWidget(self.buttonBox) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) self._new_iface = True def _translate(self, text): if hasattr(QApplication, "UnicodeUTF8"): return QApplication.translate("Form", text, None, QApplication.UnicodeUTF8) else: return QApplication.translate("Form", text, None) @property def sync_args(self): return self._sync_args @property def interface_filename(self): return self._interface_filename def _on_sync_all_clicked(self): self.setResult(QDialog.Accepted) self._sync_args = [] self._sync_args.append(''.join(['_interface_url:=', "'.'"])) self._sync_args.append(''.join(['_sync_topics_on_demand:=', 'False'])) self._sync_args.append(''.join(['_ignore_hosts:=', '[]'])) self._sync_args.append(''.join(['_sync_hosts:=', '[]'])) self._sync_args.append(''.join(['_ignore_nodes:=', '[]'])) self._sync_args.append(''.join(['_sync_nodes:=', '[]'])) self._sync_args.append(''.join(['_ignore_topics:=', '[]'])) self._sync_args.append(''.join(['_ignore_publishers:=', '[]'])) self._sync_args.append(''.join(['_ignore_subscribers:=', '[]'])) self._sync_args.append(''.join(['_sync_topics:=', '[]'])) self._sync_args.append(''.join(['_ignore_services:=', '[]'])) self._sync_args.append(''.join(['_sync_services:=', '[]'])) self._sync_args.append(''.join(['_sync_remote_nodes:=', 'False'])) self._interface_filename = None self.accept() # def _on_sync_all_anymsg_clicked(self): # self._sync_args = [] # self._sync_args.append(''.join(['_interface_url:=', "'.'"])) # self._sync_args.append(''.join(['_sync_topics_on_demand:=', 'True'])) # self._sync_args.append(''.join(['_ignore_hosts:=', '[]'])) # self._sync_args.append(''.join(['_sync_hosts:=', '[]'])) # self._sync_args.append(''.join(['_ignore_nodes:=', '[]'])) # self._sync_args.append(''.join(['_sync_nodes:=', '[]'])) # self._sync_args.append(''.join(['_ignore_topics:=', '[]'])) # self._sync_args.append(''.join(['_sync_topics:=', '[/*]'])) # self._sync_args.append(''.join(['_ignore_services:=', '[]'])) # self._sync_args.append(''.join(['_sync_services:=', '[]'])) # self._sync_args.append(''.join(['_sync_remote_nodes:=', 'False'])) # self._interface_filename = None # self.accept() def _on_sync_topics_on_demand_clicked(self): self._sync_args = [] self._sync_args.append(''.join(['_interface_url:=', "'.'"])) self._sync_args.append(''.join(['_sync_topics_on_demand:=', 'True'])) self._sync_args.append(''.join(['_ignore_hosts:=', '[]'])) self._sync_args.append(''.join(['_sync_hosts:=', '[]'])) self._sync_args.append(''.join(['_ignore_nodes:=', '[]'])) self._sync_args.append(''.join(['_sync_nodes:=', '[]'])) self._sync_args.append(''.join(['_ignore_topics:=', '[]'])) self._sync_args.append(''.join(['_ignore_publishers:=', '[]'])) self._sync_args.append(''.join(['_ignore_subscribers:=', '[]'])) self._sync_args.append(''.join(['_sync_topics:=', '[/only_on_demand]'])) self._sync_args.append(''.join(['_ignore_services:=', '[/*]'])) self._sync_args.append(''.join(['_sync_services:=', '[]'])) self._sync_args.append(''.join(['_sync_remote_nodes:=', 'False'])) self._interface_filename = None self.accept() def _on_select_interface_clicked(self): self.toolButton_SyncAll.setVisible(False) # self.toolButton_SyncAllAnyMsg.setVisible(False) self.toolButton_SyncTopicOnDemand.setVisible(False) self.toolButton_SelectInterface.setVisible(False) self.interface_field.setVisible(True) self.toolButton_CreateInterface.setVisible(True) self.toolButton_EditInterface.setVisible(True) self.toolButton_EditInterface.setEnabled(False) self.textedit.setVisible(False) # # fill the interfaces if self._interfaces_files is None: self.interface_field.addItems(['interface searching...']) self.interface_field.setCurrentIndex(0) self._fill_interface_thread = InterfacesThread() self._fill_interface_thread.interfaces.connect(self._fill_interfaces) self._fill_interface_thread.start() else: self.toolButton_EditInterface.setEnabled(self.interface_field.currentText() in self._interfaces_files) self.buttonBox.clear() self.buttonBox.setStandardButtons(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.interface_field.setFocus(Qt.TabFocusReason) self.resize(350, 80) def _fill_interfaces(self, interfaces_files): self._interfaces_files = interfaces_files self.interface_field.clear() self.interface_field.clearEditText() self.interface_field.addItems(self._interfaces_files.keys()) def _on_interface_selected(self, interface): if self._interfaces_files and interface in self._interfaces_files: self._sync_args = [] self._sync_args.append(''.join(['_interface_url:=', interface])) self.toolButton_EditInterface.setEnabled(True) else: self.toolButton_EditInterface.setEnabled(False) def accept(self): if self.textedit.isVisible(): try: tmp_file = os.path.join(nm.screen().LOG_PATH, 'tmp_sync_interface.sync') with open(tmp_file, 'w+') as f: f.write(self.textedit.toPlainText()) from master_discovery_fkie.common import read_interface read_interface(tmp_file) if not self._new_iface and self.interface_field.currentText() in self._interfaces_files: fileName = self._interfaces_files[self.interface_field.currentText()] else: fileName, _ = QFileDialog.getSaveFileName(self, 'Save sync interface', '/home', "Sync Files (*.sync)") if fileName: with open(fileName, 'w+') as f: self._interface_filename = fileName f.write(self.textedit.toPlainText()) if self._new_iface: self.interface_field.clear() self._interfaces_files = None self._on_select_interface_clicked() # QDialog.accept(self) # self.resetView() except Exception as e: MessageBox.warning(self, "Create sync interface", "Error while create interface", utf8(e)) elif self.interface_field.isVisible(): interface = self.interface_field.currentText() if self._interfaces_files and interface in self._interfaces_files: self._interface_filename = self._interfaces_files[interface] self._sync_args = [] self._sync_args.append(''.join(['_interface_url:=', interface])) QDialog.accept(self) self.resetView() else: QDialog.accept(self) self.resetView() def reject(self): if self.textedit.isVisible(): self._on_select_interface_clicked() else: QDialog.reject(self) self.resetView() def _on_create_interface_clicked(self): self._new_iface = True self.interface_field.setVisible(False) self.toolButton_CreateInterface.setVisible(False) self.toolButton_EditInterface.setVisible(False) self.textedit.setVisible(True) self.textedit.setText("# The ignore_* lists will be processed first.\n" "# For ignore/sync nodes, topics or services\n" "# use follow declaration:\n" "#{param name}: \n" "# - {ros name}\n" "# or for selected hosts:\n" "# - {host name}:\n" "# - {ros name}\n\n" "# you can use follow wildcard: '*', but not as a first character\n" "ignore_hosts:\n" "sync_hosts:\n\n" "ignore_nodes:\n" "sync_nodes:\n\n" "ignore_topics:\n" "ignore_publishers:\n" "ignore_subscribers:\n" "sync_topics:\n\n" "ignore_services:\n" " - /*get_loggers\n" " - /*set_logger_level\n" "sync_services:\n\n" "# If sync_topics_on_demand is True the local subscribed and published topics\n" "# are synchronized with remote even if they are not in the sync_* list.\n" "sync_topics_on_demand: False\n\n" "# The nodes which are running not at the same host as the ROS master are not\n" "# synchronized by default. Use sync_remote_nodes to sync these nodes also.\n" "sync_remote_nodes: False\n\n" ) self.resize(350, 300) def _on_edit_interface_clicked(self): self._new_iface = False self.interface_field.setVisible(False) self.toolButton_CreateInterface.setVisible(False) self.toolButton_EditInterface.setVisible(False) self.textedit.setVisible(True) if self.interface_field.currentText() in self._interfaces_files: try: with open(self._interfaces_files[self.interface_field.currentText()], 'rw') as f: iface = f.read() self.textedit.setText(iface) except Exception as e: MessageBox.warning(self, "Edit sync interface", "Error while open interface", utf8(e)) self.resize(350, 300) def resetView(self): self.toolButton_SyncAll.setVisible(True) # self.toolButton_SyncAllAnyMsg.setVisible(True) self.toolButton_SyncTopicOnDemand.setVisible(True) self.toolButton_SelectInterface.setVisible(True) self.interface_field.setVisible(False) self.toolButton_CreateInterface.setVisible(False) self.toolButton_EditInterface.setVisible(False) self.textedit.setVisible(False) self.buttonBox.clear() self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel) self.resize(350, 160)
class SelectDialog(QDialog): ''' This dialog creates an input mask for a string list and return selected entries. ''' def __init__(self, items=list(), buttons=QDialogButtonBox.Cancel | QDialogButtonBox.Ok, exclusive=False, preselect_all=False, title='', description='', icon='', parent=None, select_if_single=True, checkitem1='', checkitem2=''): ''' Creates an input dialog. @param items: a list with strings @type items: C{list()} ''' QDialog.__init__(self, parent=parent) self.setObjectName(' - '.join(['SelectDialog', str(items)])) self.verticalLayout = QVBoxLayout(self) self.verticalLayout.setObjectName("verticalLayout") self.verticalLayout.setContentsMargins(1, 1, 1, 1) # add filter row self.filter_frame = QFrame(self) filterLayout = QHBoxLayout(self.filter_frame) filterLayout.setContentsMargins(1, 1, 1, 1) label = QLabel("Filter:", self.filter_frame) self.filter_field = EnchancedLineEdit(self.filter_frame) filterLayout.addWidget(label) filterLayout.addWidget(self.filter_field) self.filter_field.textChanged.connect(self._on_filter_changed) self.verticalLayout.addWidget(self.filter_frame) if description: self.description_frame = QFrame(self) descriptionLayout = QHBoxLayout(self.description_frame) # descriptionLayout.setContentsMargins(1, 1, 1, 1) if icon: self.icon_label = QLabel(self.description_frame) self.icon_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.icon_label.setPixmap(QPixmap(icon).scaled(30, 30, Qt.KeepAspectRatio)) descriptionLayout.addWidget(self.icon_label) self.description_label = QLabel(self.description_frame) self.description_label.setWordWrap(True) self.description_label.setText(description) descriptionLayout.addWidget(self.description_label) self.verticalLayout.addWidget(self.description_frame) # create area for the parameter self.content = MainBox(self) if items: self.scroll_area = QScrollArea(self) self.scroll_area.setFocusPolicy(Qt.NoFocus) self.scroll_area.setObjectName("scroll_area") self.scroll_area.setWidgetResizable(True) self.scroll_area.setWidget(self.content) self.verticalLayout.addWidget(self.scroll_area) self.checkitem1 = checkitem1 self.checkitem1_result = False self.checkitem2 = checkitem2 self.checkitem2_result = False # add select all option if not exclusive and items: self._ignore_next_toggle = False self.select_all_checkbox = QCheckBox('all entries') self.select_all_checkbox.setTristate(True) self.select_all_checkbox.stateChanged.connect(self._on_select_all_checkbox_stateChanged) self.verticalLayout.addWidget(self.select_all_checkbox) self.content.toggled.connect(self._on_main_toggle) if self.checkitem1: self.checkitem1_checkbox = QCheckBox(self.checkitem1) self.checkitem1_checkbox.stateChanged.connect(self._on_select_checkitem1_checkbox_stateChanged) self.verticalLayout.addWidget(self.checkitem1_checkbox) if self.checkitem2: self.checkitem2_checkbox = QCheckBox(self.checkitem2) self.checkitem2_checkbox.stateChanged.connect(self._on_select_checkitem2_checkbox_stateChanged) self.verticalLayout.addWidget(self.checkitem2_checkbox) if not items: spacerItem = QSpacerItem(1, 1, QSizePolicy.Expanding, QSizePolicy.Expanding) self.verticalLayout.addItem(spacerItem) # create buttons self.buttonBox = QDialogButtonBox(self) self.buttonBox.setObjectName("buttonBox") self.buttonBox.setOrientation(Qt.Horizontal) self.buttonBox.setStandardButtons(buttons) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) self.verticalLayout.addWidget(self.buttonBox) # set the input fields if items: self.content.createFieldsFromValues(items, exclusive) if (select_if_single and len(items) == 1) or preselect_all: self.select_all_checkbox.setCheckState(Qt.Checked) if not items or len(items) < 7: self.filter_frame.setVisible(False) # print '=============== create', self.objectName() # # def __del__(self): # print "************ destroy", self.objectName() def _on_main_toggle(self, state): self._ignore_next_toggle = state != self.select_all_checkbox.checkState() self.select_all_checkbox.setCheckState(state) def _on_select_all_checkbox_stateChanged(self, state): if not self._ignore_next_toggle: self.content.setState(state) self._ignore_next_toggle = False def _on_select_checkitem1_checkbox_stateChanged(self, state): if state == Qt.Checked: self.checkitem1_result = True elif state == Qt.Unchecked: self.checkitem1_result = False def _on_select_checkitem2_checkbox_stateChanged(self, state): if state == Qt.Checked: self.checkitem2_result = True elif state == Qt.Unchecked: self.checkitem2_result = False def _on_filter_changed(self): self.content.filter(self.filter_field.text()) def getSelected(self): return self.content.getSelected() @staticmethod def getValue(title, description='', items=list(), exclusive=False, preselect_all=False, icon='', parent=None, select_if_single=True, checkitem1='', checkitem2=''): selectDia = SelectDialog(items, exclusive=exclusive, preselect_all=preselect_all, description=description, icon=icon, parent=parent, select_if_single=select_if_single, checkitem1=checkitem1, checkitem2=checkitem2) selectDia.setWindowTitle(title) selectDia.resize(480, 256) if selectDia.exec_(): if selectDia.checkitem2: return selectDia.getSelected(), True, selectDia.checkitem1_result, selectDia.checkitem2_result if selectDia.checkitem1: return selectDia.getSelected(), True, selectDia.checkitem1_result return selectDia.getSelected(), True if selectDia.checkitem2: return list(), False, False, False if selectDia.checkitem1: return list(), False, False return list(), False # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% # %%%%%%%%%%%%%%%%%% close handling %%%%%%%%%%%%%%%%%%%%% # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% def accept(self): self.setResult(QDialog.Accepted) self.hide() def reject(self): self.setResult(QDialog.Rejected) self.hide() def hideEvent(self, event): self.close() def closeEvent(self, event): ''' Test the open files for changes and save this if needed. ''' self.setAttribute(Qt.WA_DeleteOnClose, True) QDialog.closeEvent(self, event)
class PackageDialog(QDialog): def __init__(self, masteruri, parent=None): QDialog.__init__(self, parent) self.setWindowTitle('Select Binary') self.verticalLayout = QVBoxLayout(self) self.verticalLayout.setObjectName("verticalLayout") self.content = QWidget() self.contentLayout = QFormLayout(self.content) self.contentLayout.setVerticalSpacing(0) self.verticalLayout.addWidget(self.content) self.packages = None self.masteruri = "ROS_MASTER_URI" if masteruri is None else masteruri package_label = QLabel("Package:", self.content) self.package_field = QComboBox(self.content) self.package_field.setInsertPolicy(QComboBox.InsertAlphabetically) self.package_field.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)) self.package_field.setEditable(True) self.contentLayout.addRow(package_label, self.package_field) binary_label = QLabel("Binary:", self.content) self.binary_field = QComboBox(self.content) self.binary_field.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)) self.binary_field.setEditable(True) self.contentLayout.addRow(binary_label, self.binary_field) self.buttonBox = QDialogButtonBox(self) self.buttonBox.setStandardButtons(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.buttonBox.setOrientation(Qt.Horizontal) self.buttonBox.setObjectName("buttonBox") self.verticalLayout.addWidget(self.buttonBox) self.package_field.setFocus(Qt.TabFocusReason) self.package = '' self.binary = '' self._request_bin_thread = None if self.packages is None: self.package_field.addItems(['packages searching...']) self.package_field.setCurrentIndex(0) # fill the input fields self.packages = { name: path for path, name in nm.nmd().file.get_packages( nmdurl.nmduri(masteruri)).items() } packages = self.packages.keys() packages.sort() self.package_field.clear() self.package_field.clearEditText() self.package_field.addItems(packages) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) QMetaObject.connectSlotsByName(self) self.package_field.activated[str].connect(self.on_package_selected) if hasattr(self.package_field, "textChanged"): # qt compatibility self.package_field.textChanged.connect(self.on_package_selected) self.binary_field.textChanged.connect(self.on_binary_selected) else: self.package_field.editTextChanged.connect( self.on_package_selected) self.binary_field.editTextChanged.connect(self.on_binary_selected) def on_package_selected(self, package): getnew = False if self._request_bin_thread is None: getnew = True else: if self._request_bin_thread.pkgname != package: self._request_bin_thread.cancel() getnew = True if self._request_bin_thread is not None and self._request_bin_thread.pkgname == package: # use already got data self._request_bin_thread.reemit() elif getnew: self.binary_field.clear() if self.packages and package in self.packages: self.binary_field.setEnabled(True) self._request_bin_thread = RequestBinariesThread( package, nmdurl.nmduri(self.masteruri)) self._request_bin_thread.binaries_signal.connect( self._on_new_binaries) self._request_bin_thread.start() def _on_new_binaries(self, pkgname, binaries): # update the binaries binaries = [os.path.basename(item) for item in binaries.keys()] binaries = list(set(binaries)) binaries.sort() self.binary_field.addItems(binaries) self.package = pkgname self.binary = self.binary_field.currentText() def on_binary_selected(self, binary): self.binary = binary
class SyncDialog(QDialog): ''' A dialog to set the sync options. ''' def __init__(self, parent=None): QDialog.__init__(self, parent) # self.host = host self.setWindowIcon(QIcon(":/icons/irondevil_sync.png")) self.setWindowTitle('Sync') self.verticalLayout = QVBoxLayout(self) self.verticalLayout.setObjectName("verticalLayout") self.resize(350, 190) self.toolButton_SyncAll = QToolButton(self) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(2) sizePolicy.setHeightForWidth( self.toolButton_SyncAll.sizePolicy().hasHeightForWidth()) self.toolButton_SyncAll.setSizePolicy(sizePolicy) self.toolButton_SyncAll.setObjectName("toolButton_SyncAll") self.verticalLayout.addWidget(self.toolButton_SyncAll) self.toolButton_SyncAll.setText(self._translate("Sync All")) self.toolButton_SyncAll.clicked.connect(self._on_sync_all_clicked) # self.toolButton_SyncAllAnyMsg = QToolButton(self) # sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # sizePolicy.setHorizontalStretch(0) # sizePolicy.setVerticalStretch(1) # sizePolicy.setHeightForWidth(self.toolButton_SyncAllAnyMsg.sizePolicy().hasHeightForWidth()) # self.toolButton_SyncAllAnyMsg.setSizePolicy(sizePolicy) # self.toolButton_SyncAllAnyMsg.setObjectName("toolButton_SyncAllAnyMsg") # self.verticalLayout.addWidget(self.toolButton_SyncAllAnyMsg) # self.toolButton_SyncAllAnyMsg.setText(self._translate("Sync all (+AnyMsg)")) # self.toolButton_SyncAllAnyMsg.clicked.connect(self._on_sync_all_anymsg_clicked) self.toolButton_SyncTopicOnDemand = QToolButton(self) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth( self.toolButton_SyncTopicOnDemand.sizePolicy().hasHeightForWidth()) self.toolButton_SyncTopicOnDemand.setSizePolicy(sizePolicy) self.toolButton_SyncTopicOnDemand.setObjectName( "toolButton_SyncTopicOnDemand") self.verticalLayout.addWidget(self.toolButton_SyncTopicOnDemand) self.toolButton_SyncTopicOnDemand.setText( self._translate("Sync only topics on demand")) self.toolButton_SyncTopicOnDemand.clicked.connect( self._on_sync_topics_on_demand_clicked) self.toolButton_SelectInterface = QToolButton(self) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth( self.toolButton_SelectInterface.sizePolicy().hasHeightForWidth()) self.toolButton_SelectInterface.setSizePolicy(sizePolicy) self.toolButton_SelectInterface.setObjectName( "toolButton_SelectInterface") self.verticalLayout.addWidget(self.toolButton_SelectInterface) self.toolButton_SelectInterface.setText( self._translate("Select an interface")) self.toolButton_SelectInterface.clicked.connect( self._on_select_interface_clicked) self.interface_field = QComboBox(self) self.interface_field.setInsertPolicy(QComboBox.InsertAlphabetically) self.interface_field.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)) self.interface_field.setEditable(True) self.interface_field.setVisible(False) self.interface_field.setObjectName("interface_field") self.verticalLayout.addWidget(self.interface_field) self.interface_field.currentIndexChanged[str].connect( self._on_interface_selected) self.toolButton_EditInterface = QToolButton(self) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth( self.toolButton_EditInterface.sizePolicy().hasHeightForWidth()) self.toolButton_EditInterface.setSizePolicy(sizePolicy) self.toolButton_EditInterface.setObjectName("toolButton_EditInterface") self.verticalLayout.addWidget(self.toolButton_EditInterface) self.toolButton_EditInterface.setText( self._translate("Edit selected interface")) self.toolButton_EditInterface.clicked.connect( self._on_edit_interface_clicked) self.toolButton_EditInterface.setVisible(False) self.toolButton_CreateInterface = QToolButton(self) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth( self.toolButton_CreateInterface.sizePolicy().hasHeightForWidth()) self.toolButton_CreateInterface.setSizePolicy(sizePolicy) self.toolButton_CreateInterface.setObjectName( "toolButton_CreateInterface") self.verticalLayout.addWidget(self.toolButton_CreateInterface) self.toolButton_CreateInterface.setText( self._translate("Create an interface")) self.toolButton_CreateInterface.clicked.connect( self._on_create_interface_clicked) self.toolButton_CreateInterface.setVisible(False) self.textedit = TextEdit('', self) self.hl = SyncHighlighter(self.textedit.document()) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.textedit.setSizePolicy(sizePolicy) self.textedit.setObjectName("syncedit") self.verticalLayout.addWidget(self.textedit) self.textedit.setVisible(False) self._fill_interface_thread = None self._interfaces_files = None self._sync_args = [] self._interface_filename = None self.buttonBox = QDialogButtonBox(self) self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel) self.buttonBox.setOrientation(Qt.Horizontal) self.buttonBox.setObjectName("buttonBox") self.verticalLayout.addWidget(self.buttonBox) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) self._new_iface = True def _translate(self, text): if hasattr(QApplication, "UnicodeUTF8"): return QApplication.translate("Form", text, None, QApplication.UnicodeUTF8) else: return QApplication.translate("Form", text, None) @property def sync_args(self): return self._sync_args @property def interface_filename(self): return self._interface_filename def _on_sync_all_clicked(self): self.setResult(QDialog.Accepted) self._sync_args = [] self._sync_args.append(''.join(['_interface_url:=', "'.'"])) self._sync_args.append(''.join(['_sync_topics_on_demand:=', 'False'])) self._sync_args.append(''.join(['_ignore_hosts:=', '[]'])) self._sync_args.append(''.join(['_sync_hosts:=', '[]'])) self._sync_args.append(''.join(['_ignore_nodes:=', '[]'])) self._sync_args.append(''.join(['_sync_nodes:=', '[]'])) self._sync_args.append(''.join(['_ignore_topics:=', '[]'])) self._sync_args.append(''.join(['_ignore_publishers:=', '[]'])) self._sync_args.append(''.join(['_ignore_subscribers:=', '[]'])) self._sync_args.append(''.join(['_sync_topics:=', '[]'])) self._sync_args.append(''.join(['_ignore_services:=', '[]'])) self._sync_args.append(''.join(['_sync_services:=', '[]'])) self._sync_args.append(''.join(['_sync_remote_nodes:=', 'False'])) self._interface_filename = None self.accept() # def _on_sync_all_anymsg_clicked(self): # self._sync_args = [] # self._sync_args.append(''.join(['_interface_url:=', "'.'"])) # self._sync_args.append(''.join(['_sync_topics_on_demand:=', 'True'])) # self._sync_args.append(''.join(['_ignore_hosts:=', '[]'])) # self._sync_args.append(''.join(['_sync_hosts:=', '[]'])) # self._sync_args.append(''.join(['_ignore_nodes:=', '[]'])) # self._sync_args.append(''.join(['_sync_nodes:=', '[]'])) # self._sync_args.append(''.join(['_ignore_topics:=', '[]'])) # self._sync_args.append(''.join(['_sync_topics:=', '[/*]'])) # self._sync_args.append(''.join(['_ignore_services:=', '[]'])) # self._sync_args.append(''.join(['_sync_services:=', '[]'])) # self._sync_args.append(''.join(['_sync_remote_nodes:=', 'False'])) # self._interface_filename = None # self.accept() def _on_sync_topics_on_demand_clicked(self): self._sync_args = [] self._sync_args.append(''.join(['_interface_url:=', "'.'"])) self._sync_args.append(''.join(['_sync_topics_on_demand:=', 'True'])) self._sync_args.append(''.join(['_ignore_hosts:=', '[]'])) self._sync_args.append(''.join(['_sync_hosts:=', '[]'])) self._sync_args.append(''.join(['_ignore_nodes:=', '[]'])) self._sync_args.append(''.join(['_sync_nodes:=', '[]'])) self._sync_args.append(''.join(['_ignore_topics:=', '[]'])) self._sync_args.append(''.join(['_ignore_publishers:=', '[]'])) self._sync_args.append(''.join(['_ignore_subscribers:=', '[]'])) self._sync_args.append(''.join(['_sync_topics:=', '[/only_on_demand]'])) self._sync_args.append(''.join(['_ignore_services:=', '[/*]'])) self._sync_args.append(''.join(['_sync_services:=', '[]'])) self._sync_args.append(''.join(['_sync_remote_nodes:=', 'False'])) self._interface_filename = None self.accept() def _on_select_interface_clicked(self): self.toolButton_SyncAll.setVisible(False) # self.toolButton_SyncAllAnyMsg.setVisible(False) self.toolButton_SyncTopicOnDemand.setVisible(False) self.toolButton_SelectInterface.setVisible(False) self.interface_field.setVisible(True) self.toolButton_CreateInterface.setVisible(True) self.toolButton_EditInterface.setVisible(True) self.toolButton_EditInterface.setEnabled(False) self.textedit.setVisible(False) # # fill the interfaces if self._interfaces_files is None: self.interface_field.addItems(['interface searching...']) self.interface_field.setCurrentIndex(0) self._fill_interface_thread = InterfacesThread() self._fill_interface_thread.interfaces.connect( self._fill_interfaces) self._fill_interface_thread.start() else: self.toolButton_EditInterface.setEnabled( self.interface_field.currentText() in self._interfaces_files) self.buttonBox.clear() self.buttonBox.setStandardButtons(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.interface_field.setFocus(Qt.TabFocusReason) self.resize(350, 80) def _fill_interfaces(self, interfaces_files): self._interfaces_files = interfaces_files self.interface_field.clear() self.interface_field.clearEditText() self.interface_field.addItems(self._interfaces_files.keys()) def _on_interface_selected(self, interface): if self._interfaces_files and interface in self._interfaces_files: self._sync_args = [] self._sync_args.append(''.join(['_interface_url:=', interface])) self.toolButton_EditInterface.setEnabled(True) else: self.toolButton_EditInterface.setEnabled(False) def accept(self): if self.textedit.isVisible(): try: tmp_file = os.path.join(screen.LOG_PATH, 'tmp_sync_interface.sync') with open(tmp_file, 'w+') as f: f.write(self.textedit.toPlainText()) from fkie_master_discovery.common import read_interface read_interface(tmp_file) if not self._new_iface and self.interface_field.currentText( ) in self._interfaces_files: fileName = self._interfaces_files[ self.interface_field.currentText()] else: fileName, _ = QFileDialog.getSaveFileName( self, 'Save sync interface', '/home', "Sync Files (*.sync)") if fileName: with open(fileName, 'w+') as f: self._interface_filename = fileName f.write(self.textedit.toPlainText()) if self._new_iface: self.interface_field.clear() self._interfaces_files = None self._on_select_interface_clicked() # QDialog.accept(self) # self.resetView() except Exception as e: MessageBox.warning(self, "Create sync interface", "Error while create interface", utf8(e)) elif self.interface_field.isVisible(): interface = self.interface_field.currentText() if self._interfaces_files and interface in self._interfaces_files: self._interface_filename = self._interfaces_files[interface] self._sync_args = [] self._sync_args.append(''.join(['_interface_url:=', interface])) QDialog.accept(self) self.resetView() else: QDialog.accept(self) self.resetView() def reject(self): if self.textedit.isVisible(): self._on_select_interface_clicked() else: QDialog.reject(self) self.resetView() def _on_create_interface_clicked(self): self._new_iface = True self.interface_field.setVisible(False) self.toolButton_CreateInterface.setVisible(False) self.toolButton_EditInterface.setVisible(False) self.textedit.setVisible(True) self.textedit.setText( "# The ignore_* lists will be processed first.\n" "# For ignore/sync nodes, topics or services\n" "# use follow declaration:\n" "#{param name}: \n" "# - {ros name}\n" "# or for selected hosts:\n" "# - {host name}:\n" "# - {ros name}\n\n" "# you can use follow wildcard: '*', but not as a first character\n" "ignore_hosts:\n" "sync_hosts:\n\n" "ignore_nodes:\n" "sync_nodes:\n\n" "ignore_topics:\n" "ignore_publishers:\n" "ignore_subscribers:\n" "sync_topics:\n\n" "ignore_services:\n" " - /*get_loggers\n" " - /*set_logger_level\n" "sync_services:\n\n" "# If sync_topics_on_demand is True the local subscribed and published topics\n" "# are synchronized with remote even if they are not in the sync_* list.\n" "sync_topics_on_demand: False\n\n" "# The nodes which are running not at the same host as the ROS master are not\n" "# synchronized by default. Use sync_remote_nodes to sync these nodes also.\n" "sync_remote_nodes: False\n\n") self.resize(350, 300) def _on_edit_interface_clicked(self): self._new_iface = False self.interface_field.setVisible(False) self.toolButton_CreateInterface.setVisible(False) self.toolButton_EditInterface.setVisible(False) self.textedit.setVisible(True) if self.interface_field.currentText() in self._interfaces_files: try: with open( self._interfaces_files[ self.interface_field.currentText()], 'rw') as f: iface = f.read() self.textedit.setText(iface) except Exception as e: MessageBox.warning(self, "Edit sync interface", "Error while open interface", utf8(e)) self.resize(350, 300) def resetView(self): self.toolButton_SyncAll.setVisible(True) # self.toolButton_SyncAllAnyMsg.setVisible(True) self.toolButton_SyncTopicOnDemand.setVisible(True) self.toolButton_SelectInterface.setVisible(True) self.interface_field.setVisible(False) self.toolButton_CreateInterface.setVisible(False) self.toolButton_EditInterface.setVisible(False) self.textedit.setVisible(False) self.buttonBox.clear() self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel) self.resize(350, 160)
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 PackageDialog(QDialog): def __init__(self, parent=None): QDialog.__init__(self, parent) self.setWindowTitle('Select Binary') self.verticalLayout = QVBoxLayout(self) self.verticalLayout.setObjectName("verticalLayout") self.content = QWidget() self.contentLayout = QFormLayout(self.content) self.contentLayout.setVerticalSpacing(0) self.verticalLayout.addWidget(self.content) self.packages = None package_label = QLabel("Package:", self.content) self.package_field = QComboBox(self.content) self.package_field.setInsertPolicy(QComboBox.InsertAlphabetically) self.package_field.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)) self.package_field.setEditable(True) self.contentLayout.addRow(package_label, self.package_field) binary_label = QLabel("Binary:", self.content) self.binary_field = QComboBox(self.content) # self.binary_field.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.binary_field.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)) self.binary_field.setEditable(True) self.contentLayout.addRow(binary_label, self.binary_field) self.buttonBox = QDialogButtonBox(self) self.buttonBox.setStandardButtons(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.buttonBox.setOrientation(Qt.Horizontal) self.buttonBox.setObjectName("buttonBox") self.verticalLayout.addWidget(self.buttonBox) self.package_field.setFocus(Qt.TabFocusReason) self.package = '' self.binary = '' if self.packages is None: self.package_field.addItems(['packages searching...']) self.package_field.setCurrentIndex(0) self._fill_packages_thread = PackagesThread() self._fill_packages_thread.packages.connect(self._fill_packages) self._fill_packages_thread.start() self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) QMetaObject.connectSlotsByName(self) self.package_field.activated[str].connect(self.on_package_selected) if hasattr(self.package_field, "textChanged"): # qt compatibility self.package_field.textChanged.connect(self.on_package_selected) self.binary_field.textChanged.connect(self.on_binary_selected) else: self.package_field.editTextChanged.connect(self.on_package_selected) self.binary_field.editTextChanged.connect(self.on_binary_selected) def _fill_packages(self, packages): # fill the input fields self.packages = packages packages = packages.keys() packages.sort() self.package_field.clear() self.package_field.clearEditText() self.package_field.addItems(packages) def _getBinaries(self, path): result = {} if os.path.isdir(path): fileList = os.listdir(path) for f in fileList: if f and f[0] != '.' and f not in ['build'] and not f.endswith('.cfg') and not f.endswith('.so'): ret = self._getBinaries(os.path.join(path, f)) result = dict(ret.items() + result.items()) elif os.path.isfile(path) and os.access(path, os.X_OK): # create a selection for binaries return {os.path.basename(path): path} return result def on_package_selected(self, package): self.binary_field.clear() if self.packages and package in self.packages: self.binary_field.setEnabled(True) path = self.packages[package] binaries = self._getBinaries(path).keys() try: # find binaries in catkin workspace from catkin.find_in_workspaces import find_in_workspaces as catkin_find search_paths = catkin_find(search_dirs=['libexec', 'share'], project=package, first_matching_workspace_only=True) for p in search_paths: binaries += self._getBinaries(p).keys() except: pass binaries = list(set(binaries)) binaries.sort() self.binary_field.addItems(binaries) self.package = package self.binary = self.binary_field.currentText() def on_binary_selected(self, binary): self.binary = binary
class NetworkDiscoveryDialog(QDialog, threading.Thread): TIMEOUT = 0.1 display_clear_signal = Signal() display_append_signal = Signal(str) status_text_signal = Signal(str) network_join_request = Signal(int) def __init__(self, default_mcast_group, default_port, networks_count, parent=None): ''' Creates an input dialog. @param default_port: the default discovery port @type default_port: C{int} @param networks_count: the count of discovering ports @type networks_count: C{int} ''' QDialog.__init__(self, parent=parent) threading.Thread.__init__(self) self.default_port = default_port self.setObjectName('NetworkDiscoveryDialog') self.setAttribute(Qt.WA_DeleteOnClose, True) self.setWindowFlags(Qt.Window) self.setWindowTitle('Network Discovery') self.resize(728, 512) self.verticalLayout = QVBoxLayout(self) self.verticalLayout.setObjectName("verticalLayout") self.verticalLayout.setContentsMargins(1, 1, 1, 1) self.display = QTextBrowser(self) self.display.setReadOnly(True) self.verticalLayout.addWidget(self.display) self.display_clear_signal.connect(self.display.clear) self.display_append_signal.connect(self.display.append) self.display.anchorClicked.connect(self.on_anchorClicked) self.status_label = QLabel('0 messages', self) self.verticalLayout.addWidget(self.status_label) self.status_text_signal.connect(self.status_label.setText) self._networks_count = networks_count self._running = True self._received_msgs = 0 self._discovered = dict() self._hosts = dict() # resolution for hostname and address self.mutex = threading.RLock() self.sockets = [] with self.mutex: for p in range(networks_count): msock = DiscoverSocket(default_port + p, default_mcast_group) self.sockets.append(msock) msock.settimeout(self.TIMEOUT) msock.set_message_callback(self.on_heartbeat_received) self.setDaemon(True) self.start() def on_heartbeat_received(self, msg, address, is_multicast): force_update = False with self.mutex: try: hostname = self._hosts[address[0]] except: self.status_text_signal.emit("resolve %s" % address[0]) hostname = nm.nameres().hostname(str(address[0]), resolve=True) self._hosts[address[0]] = hostname try: (_version, _msg_tuple) = Discoverer.msg2masterState(msg, address) index = address[1] - self.default_port if index not in self._discovered: self._discovered[index] = dict() self._discovered[index][address] = (hostname, time.time()) self._received_msgs += 1 force_update = True except: print traceback.format_exc(1) if force_update: self._updateDisplay() def run(self): self.parent().masterlist_service.refresh(self.parent().getMasteruri(), False) while (not rospy.is_shutdown()) and self._running: with self.mutex: status_text = 'received messages: %d' % (self._received_msgs) self.status_text_signal.emit(status_text) # self.parent().masterlist_service.refresh(self.parent().getMasteruri(), False) time.sleep(3) def closeEvent(self, event): self.stop() QDialog.closeEvent(self, event) def stop(self): self._running = False with self.mutex: for p in range(len(self.sockets)): try: self.sockets[p].close() except: pass def _updateDisplay(self): self.display_clear_signal.emit() text = '<div style="font-family:Fixedsys,Courier,monospace; padding:10px;">\n' for index, addr_dict in self._discovered.items(): text = ''.join([text, 'Network <b>', str(index), '</b>: <a href="', str(index), '">join</a><dl>']) for addr, (hostname, ts) in addr_dict.items(): text = ''.join([text, '<dt>', self._getTsStr(ts), ' <b><u>', str(hostname), '</u></b> ', str(addr), '</dt>\n']) text = ''.join([text, '</dl><br>']) text = ''.join([text, '</div>']) self.display_append_signal.emit(text) def _getTsStr(self, timestamp): dt = datetime.fromtimestamp(timestamp) diff = time.time() - timestamp diff_dt = datetime.fromtimestamp(diff) before = '0 sec' if (diff < 60): before = diff_dt.strftime('%S sec') elif (diff < 3600): before = diff_dt.strftime('%M:%S min') elif (diff < 86400): before = diff_dt.strftime('%H:%M:%S std') else: before = diff_dt.strftime('%d Day(s) %H:%M:%S') return ''.join([dt.strftime('%H:%M:%S'), ' (', before, ')']) def on_anchorClicked(self, url): self._updateDisplay() try: self.network_join_request.emit(int(url.toString())) except: print traceback.format_exc(1)
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)
class MessageBox(QDialog): NoIcon = 0 Information = 1 Warning = 2 Critical = 3 Question = 4 NoButton = 0 Ok = 1 # An "OK" button defined with the AcceptRole . Open = 2 # A "Open" button defined with the AcceptRole . Save = 4 # A "Save" button defined with the AcceptRole . Cancel = 8 # A "Cancel" button defined with the RejectRole . Close = 16 # A "Close" button defined with the RejectRole . Discard = 32 # A "Discard" or "Don't Save" button, depending on the platform, defined with the DestructiveRole . Apply = 64 # An "Apply" button defined with the ApplyRole . Reset = 128 # A "Reset" button defined with the ResetRole . RestoreDefaults = 256 # A "Restore Defaults" button defined with the ResetRole . Help = 512 # A "Help" button defined with the HelpRole . SaveAll = 1024 # A "Save All" button defined with the AcceptRole . Yes = 2048 # A "Yes" button defined with the YesRole . YesToAll = 4096 # A "Yes to All" button defined with the YesRole . No = 8192 # A "No" button defined with the NoRole . NoToAll = 16384 # A "No to All" button defined with the NoRole . Abort = 32768 # An "Abort" button defined with the RejectRole . Retry = 65536 # A "Retry" button defined with the AcceptRole . Ignore = 131072 # An "Ignore" button defined with the AcceptRole . Avoid = 262144 # An "'Don't show again'" button defined with the HelpRole, returns a default AcceptButton . def __init__(self, icon, title, text, detailed_text="", buttons=Cancel | Ok, parent=None): QDialog.__init__(self, parent=parent) self.setWindowFlags(self.windowFlags() & ~Qt.WindowTitleHint) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint & ~Qt.WindowMinimizeButtonHint) self.setObjectName('MessageBox') self._use_checkbox = True self.text = text self.verticalLayout = QVBoxLayout(self) self.verticalLayout.setObjectName("verticalLayout") self.verticalLayout.setContentsMargins(1, 1, 1, 1) self.horizontalLayout = QHBoxLayout() self.horizontalLayout.setObjectName("horizontalLayout") self.horizontalLayout.setContentsMargins(1, 1, 1, 1) # create icon pixmap = None if icon == self.NoIcon: pass elif icon == self.Question: pixmap = QApplication.style().standardPixmap(QStyle.SP_MessageBoxQuestion) elif icon == self.Information: pixmap = QApplication.style().standardPixmap(QStyle.SP_MessageBoxInformation) elif icon == self.Warning: pixmap = QPixmap(":icons/crystal_clear_warning_56.png") elif icon == self.Critical: pixmap = QApplication.style().standardPixmap(QStyle.SP_MessageBoxCritical) spacerItem = QSpacerItem(10, 60, QSizePolicy.Minimum, QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem) self.icon_label = QLabel() if pixmap is not None: self.icon_label.setPixmap(pixmap) self.icon_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.horizontalLayout.addWidget(self.icon_label) spacerItem = QSpacerItem(10, 60, QSizePolicy.Minimum, QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem) # add message self.message_label = QLabel(text) self.message_label.setWordWrap(True) self.message_label.setScaledContents(True) self.message_label.setOpenExternalLinks(True) self.horizontalLayout.addWidget(self.message_label) self.verticalLayout.addLayout(self.horizontalLayout) # create buttons self.buttonBox = QDialogButtonBox(self) self.buttonBox.setObjectName("buttonBox") self.buttonBox.setOrientation(Qt.Horizontal) self._accept_button = None self._reject_button = None self._buttons = buttons self._create_buttons(buttons) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) self.verticalLayout.addWidget(self.buttonBox) if detailed_text: self.btn_show_details = QPushButton(self.tr('Details...')) self.btn_show_details.setCheckable(True) self.btn_show_details.setChecked(True) self.btn_show_details.toggled.connect(self.on_toggled_details) self.buttonBox.addButton(self.btn_show_details, QDialogButtonBox.ActionRole) # create area for detailed text self.textEdit = textEdit = QTextEdit(self) textEdit.setObjectName("textEdit") textEdit.setReadOnly(True) textEdit.setText(detailed_text) # textEdit.setVisible(False) self.verticalLayout.addWidget(self.textEdit) self.resize(480, self.verticalLayout.totalSizeHint().height()) buttons_in_box = self.buttonBox.buttons() if buttons_in_box: self.buttonBox.buttons()[0].setFocus() def setAcceptButton(self, button): ''' Sets the button with given ID to accept button if more then one button with AcceptRole was added to this dialog. Adds the buttton to the box if is not already in. :param button: int ''' if not button & self._buttons: self._create_buttons(button) self._accept_button = button def setRejectButton(self, button): ''' Sets the button with given ID to reject button if more then one button with RejectRole was added to this dialog. Adds the buttton to the box if is not already in. :param button: int ''' if not button & self._buttons: self._create_buttons(button) self._reject_button = button def on_toggled_details(self, checked): if checked: self.verticalLayout.addWidget(self.textEdit) else: self.verticalLayout.removeWidget(self.textEdit) self.textEdit.setVisible(checked) if not self.isMaximized(): self.setMinimumSize(self.verticalLayout.totalMinimumSize()) self.resize(self._current_size.width(), self.verticalLayout.totalSizeHint().height()) @staticmethod def about(parent, title, text, detailed_text='', buttons=Close): box = MessageBox(MessageBox.Information, title, text, detailed_text=detailed_text, buttons=buttons, parent=parent) if MessageBox.Yes & buttons: box.setAcceptButton(MessageBox.Yes) if MessageBox.Cancel & buttons: box.setRejectButton(MessageBox.Cancel) elif MessageBox.No & buttons: box.setRejectButton(MessageBox.No) return box.exec_() @staticmethod def information(parent, title, text, detailed_text='', buttons=Close): box = MessageBox(MessageBox.Information, title, text, detailed_text=detailed_text, buttons=buttons, parent=parent) if MessageBox.Yes & buttons: box.setAcceptButton(MessageBox.Yes) if MessageBox.Cancel & buttons: box.setRejectButton(MessageBox.Cancel) elif MessageBox.No & buttons: box.setRejectButton(MessageBox.No) return box.exec_() @staticmethod def question(parent, title, text, detailed_text='', buttons=Yes | No | Cancel): box = MessageBox(MessageBox.Question, title, text, detailed_text=detailed_text, buttons=buttons, parent=parent) if MessageBox.Yes & buttons: box.setAcceptButton(MessageBox.Yes) if MessageBox.Cancel & buttons: box.setRejectButton(MessageBox.Cancel) elif MessageBox.No & buttons: box.setRejectButton(MessageBox.No) return box.exec_() @staticmethod def warning(parent, title, text, detailed_text='', buttons=Ok): box = MessageBox(MessageBox.Warning, title, text, detailed_text=detailed_text, buttons=buttons, parent=parent) if MessageBox.Yes & buttons: box.setAcceptButton(MessageBox.Yes) if MessageBox.Cancel & buttons: box.setRejectButton(MessageBox.Cancel) elif MessageBox.No & buttons: box.setRejectButton(MessageBox.No) return box.exec_() @staticmethod def critical(parent, title, text, detailed_text='', buttons=Ok): box = MessageBox(MessageBox.Critical, title, text, detailed_text=detailed_text, buttons=buttons, parent=parent) if MessageBox.Yes & buttons: box.setAcceptButton(MessageBox.Yes) if MessageBox.Cancel & buttons: box.setRejectButton(MessageBox.Cancel) elif MessageBox.No & buttons: box.setRejectButton(MessageBox.No) return box.exec_() def resizeEvent(self, event): if not self.isMaximized(): self._current_size = event.size() # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% # %%%%%%%%%%%%%%%%%% close handling %%%%%%%%%%%%%%%%%%%%% # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% def exec_(self): if self.text in IGNORED_ERRORS: self.accept() return self.result() return QDialog.exec_(self) def accept(self): if self.result() == 0: if self._accept_button is not None: self.setResult(self._accept_button) else: self.setResult(1) self.accepted.emit() if self.isModal(): self.hide() def reject(self): if self.result() == 0: if self._reject_button is not None: self.setResult(self._reject_button) self.rejected.emit() self.hide() def hideEvent(self, event): # event.ignore() self.close() def closeEvent(self, event): self.setAttribute(Qt.WA_DeleteOnClose, True) QDialog.closeEvent(self, event) # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% # %%%%%%%%%%%%%%%%%% create buttons %%%%%%%%%%%%%%%%%%%%% # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% def _create_buttons(self, buttons): if MessageBox.Ok & buttons: self._accept_button = MessageBox.Ok bt = QPushButton(self.tr("&ok")) bt.clicked.connect(self._on_ok_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.AcceptRole) if MessageBox.Open & buttons: self._accept_button = MessageBox.Open bt = QPushButton(self.tr("&Open")) bt.clicked.connect(self._on_open_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.AcceptRole) if MessageBox.Save & buttons: self._accept_button = MessageBox.Save bt = QPushButton(self.tr("&Save")) bt.clicked.connect(self._on_save_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.AcceptRole) if MessageBox.Cancel & buttons: self._reject_button = MessageBox.Cancel bt = QPushButton(self.tr("&Cancel")) bt.clicked.connect(self._on_cancel_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.RejectRole) if MessageBox.Close & buttons: self._reject_button = MessageBox.Close bt = QPushButton(self.tr("&Close")) bt.clicked.connect(self._on_close_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.RejectRole) if MessageBox.Discard & buttons: bt = QPushButton(self.tr("&Discard")) bt.clicked.connect(self._on_discard_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.DestructiveRole) if MessageBox.Apply & buttons: bt = QPushButton(self.tr("&Apply")) bt.clicked.connect(self._on_apply_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.ApplyRole) if MessageBox.Reset & buttons: bt = QPushButton(self.tr("&Reset")) bt.clicked.connect(self._on_reset_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.ResetRole) if MessageBox.RestoreDefaults & buttons: bt = QPushButton(self.tr("&RestoreDefaults")) bt.clicked.connect(self._on_restore_defaults_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.ResetRole) if MessageBox.Help & buttons: bt = QPushButton(self.tr("&Help")) bt.clicked.connect(self._on_help_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.HelpRole) if MessageBox.SaveAll & buttons: self._accept_button = MessageBox.SaveAll bt = QPushButton(self.tr("&SaveAll")) bt.clicked.connect(self._on_saveall_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.AcceptRole) if MessageBox.Yes & buttons: bt = QPushButton(self.tr("&Yes")) bt.clicked.connect(self._on_yes_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.YesRole) if MessageBox.YesToAll & buttons: bt = QPushButton(self.tr("&YesToAll")) bt.clicked.connect(self._on_yestoall_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.YesRole) if MessageBox.No & buttons: bt = QPushButton(self.tr("&No")) bt.clicked.connect(self._on_no_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.NoRole) if MessageBox.NoToAll & buttons: bt = QPushButton(self.tr("&NoToAll")) bt.clicked.connect(self._on_notoall_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.NoRole) if MessageBox.Abort & buttons: self._reject_button = MessageBox.Abort bt = QPushButton(self.tr("&Abort")) bt.clicked.connect(self._on_abort_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.RejectRole) if MessageBox.Retry & buttons: self._accept_button = MessageBox.Retry bt = QPushButton(self.tr("&Retry")) bt.clicked.connect(self._on_retry_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.AcceptRole) if MessageBox.Ignore & buttons: self._accept_button = MessageBox.Ignore bt = QPushButton(self.tr("&Ignore")) bt.clicked.connect(self._on_ignore_clicked) self.buttonBox.addButton(bt, QDialogButtonBox.AcceptRole) if MessageBox.Avoid & buttons: if self._use_checkbox: checkbox = QCheckBox("&Don't show again", self) checkbox.stateChanged.connect(self._check_ignore) self.buttonBox.addButton(checkbox, QDialogButtonBox.HelpRole) else: bt = QPushButton(self.tr("&Don't show again")) bt.setMaximumHeight(24) bt.clicked.connect(self._add_to_ignore) self.buttonBox.addButton(bt, QDialogButtonBox.HelpRole) def _on_ok_clicked(self): self.done(MessageBox.Ok) def _on_open_clicked(self): self.done(MessageBox.Open) def _on_save_clicked(self): self.done(MessageBox.Save) def _on_cancel_clicked(self): self.done(MessageBox.Cancel) def _on_close_clicked(self): self.done(MessageBox.Close) def _on_discard_clicked(self): self.done(MessageBox.Discard) def _on_apply_clicked(self): self.done(MessageBox.Apply) def _on_reset_clicked(self): self.done(MessageBox.Reset) def _on_restore_defaults_clicked(self): self.done(MessageBox.RestoreDefaults) def _on_help_clicked(self): self.done(MessageBox.Help) def _on_saveall_clicked(self): self.done(MessageBox.SaveAll) def _on_yes_clicked(self): self.done(MessageBox.Yes) def _on_yestoall_clicked(self): self.done(MessageBox.YesToAll) def _on_no_clicked(self): self.done(MessageBox.No) def _on_notoall_clicked(self): self.done(MessageBox.NoToAll) def _on_abort_clicked(self): self.done(MessageBox.Abort) def _on_retry_clicked(self): self.done(MessageBox.Retry) def _on_ignore_clicked(self): self.done(MessageBox.Ignore) def _add_to_ignore(self): IGNORED_ERRORS.append(self.text) self.accept() def _check_ignore(self, state): if state: IGNORED_ERRORS.append(self.text) else: try: IGNORED_ERRORS.remove(self.text) except Exception: pass
class EchoDialog(QDialog): MESSAGE_LINE_LIMIT = 128 MESSAGE_HZ_LIMIT = 10 MAX_DISPLAY_MSGS = 25 STATISTIC_QUEUE_LEN = 5000 """ This dialog shows the output of a topic. """ finished_signal = Signal(str) """ finished_signal has as parameter the name of the topic and is emitted, if this dialog was closed. """ msg_signal = Signal(object, bool) """ msg_signal is a signal, which is emitted, if a new message was received. """ text_hz_signal = Signal(str) text_signal = Signal(str) """ text_signal is a signal, which is emitted, if a new text to display was received. """ text_error_signal = Signal(str) """ text_error_signal is a signal, which is emitted, if a new error text to display was received. """ request_pw = Signal(object) def __init__(self, topic, msg_type, show_only_rate=False, masteruri=None, use_ssh=False, parent=None): """ Creates an input dialog. @param topic: the name of the topic @type topic: C{str} @param msg_type: the type of the topic @type msg_type: C{str} @raise Exception: if no topic class was found for the given type """ QDialog.__init__(self, parent=parent) self._masteruri = masteruri masteruri_str = "" if masteruri is None else "[%s]" % masteruri self.setObjectName(" - ".join(["EchoDialog", topic, masteruri_str])) self.setAttribute(Qt.WA_DeleteOnClose, True) self.setWindowFlags(Qt.Window) self.setWindowTitle("%s %s %s" % ("Echo --- " if not show_only_rate else "Hz --- ", topic, masteruri_str)) self.resize(728, 512) self.verticalLayout = QVBoxLayout(self) self.verticalLayout.setObjectName("verticalLayout") self.verticalLayout.setContentsMargins(1, 1, 1, 1) self.mIcon = QIcon(":/icons/crystal_clear_prop_run_echo.png") self.setWindowIcon(self.mIcon) self.topic = topic self.show_only_rate = show_only_rate self.lock = threading.RLock() self.last_printed_count = 0 self.msg_t0 = -1.0 self.msg_tn = 0 self.times = [] self.message_count = 0 self._rate_message = "" self._scrapped_msgs = 0 self._scrapped_msgs_sl = 0 self._last_received_ts = 0 self.receiving_hz = self.MESSAGE_HZ_LIMIT self.line_limit = self.MESSAGE_LINE_LIMIT self.field_filter_fn = None options = QWidget(self) if not show_only_rate: hLayout = QHBoxLayout(options) hLayout.setContentsMargins(1, 1, 1, 1) self.no_str_checkbox = no_str_checkbox = QCheckBox("Hide strings") no_str_checkbox.toggled.connect(self.on_no_str_checkbox_toggled) hLayout.addWidget(no_str_checkbox) self.no_arr_checkbox = no_arr_checkbox = QCheckBox("Hide arrays") no_arr_checkbox.toggled.connect(self.on_no_arr_checkbox_toggled) hLayout.addWidget(no_arr_checkbox) self.combobox_reduce_ch = QComboBox(self) self.combobox_reduce_ch.addItems([str(self.MESSAGE_LINE_LIMIT), "0", "80", "256", "1024"]) self.combobox_reduce_ch.activated[str].connect(self.combobox_reduce_ch_activated) self.combobox_reduce_ch.setEditable(True) self.combobox_reduce_ch.setToolTip("Set maximum line width. 0 disables the limit.") hLayout.addWidget(self.combobox_reduce_ch) # reduce_ch_label = QLabel('ch', self) # hLayout.addWidget(reduce_ch_label) # add spacer spacerItem = QSpacerItem(515, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) hLayout.addItem(spacerItem) # add combobox for displaying frequency of messages self.combobox_displ_hz = QComboBox(self) self.combobox_displ_hz.addItems([str(self.MESSAGE_HZ_LIMIT), "0", "0.1", "1", "50", "100", "1000"]) self.combobox_displ_hz.activated[str].connect(self.on_combobox_hz_activated) self.combobox_displ_hz.setEditable(True) hLayout.addWidget(self.combobox_displ_hz) displ_hz_label = QLabel("Hz", self) hLayout.addWidget(displ_hz_label) # add combobox for count of displayed messages self.combobox_msgs_count = QComboBox(self) self.combobox_msgs_count.addItems([str(self.MAX_DISPLAY_MSGS), "0", "50", "100"]) self.combobox_msgs_count.activated[str].connect(self.on_combobox_count_activated) self.combobox_msgs_count.setEditable(True) self.combobox_msgs_count.setToolTip("Set maximum displayed message count. 0 disables the limit.") hLayout.addWidget(self.combobox_msgs_count) displ_count_label = QLabel("#", self) hLayout.addWidget(displ_count_label) # add topic control button for unsubscribe and subscribe self.topic_control_button = QToolButton(self) self.topic_control_button.setText("stop") self.topic_control_button.setIcon(QIcon(":/icons/deleket_deviantart_stop.png")) self.topic_control_button.clicked.connect(self.on_topic_control_btn_clicked) hLayout.addWidget(self.topic_control_button) # add clear button clearButton = QToolButton(self) clearButton.setText("clear") clearButton.clicked.connect(self.on_clear_btn_clicked) hLayout.addWidget(clearButton) self.verticalLayout.addWidget(options) self.display = QTextBrowser(self) self.display.setReadOnly(True) self.verticalLayout.addWidget(self.display) self.display.document().setMaximumBlockCount(500) self.max_displayed_msgs = self.MAX_DISPLAY_MSGS self._blocks_in_msg = None self.display.setOpenLinks(False) self.display.anchorClicked.connect(self._on_display_anchorClicked) self.status_label = QLabel("0 messages", self) self.verticalLayout.addWidget(self.status_label) # subscribe to the topic errmsg = "" try: self.__msg_class = message.get_message_class(msg_type) if not self.__msg_class: errmsg = "Cannot load message class for [%s]. Did you build messages?" % msg_type # raise Exception("Cannot load message class for [%s]. Did you build messages?"%msg_type) except Exception as e: self.__msg_class = None errmsg = "Cannot load message class for [%s]. Did you build messagest?\nError: %s" % (msg_type, e) # raise Exception("Cannot load message class for [%s]. Did you build messagest?\nError: %s"%(msg_type, e)) # variables for Subscriber self.msg_signal.connect(self._append_message) self.sub = None # vairables for SSH connection self.ssh_output_file = None self.ssh_error_file = None self.ssh_input_file = None self.text_signal.connect(self._append_text) self.text_hz_signal.connect(self._append_text_hz) self._current_msg = "" self._current_errmsg = "" self.text_error_signal.connect(self._append_error_text) # decide, which connection to open if use_ssh: self.__msg_class = None self._on_display_anchorClicked(QUrl(self._masteruri)) elif self.__msg_class is None: errtxt = '<pre style="color:red; font-family:Fixedsys,Courier,monospace; padding:10px;">\n%s</pre>' % ( errmsg ) self.display.setText('<a href="%s">open using SSH</a>' % (masteruri)) self.display.append(errtxt) else: self.sub = rospy.Subscriber(self.topic, self.__msg_class, self._msg_handle) self.print_hz_timer = QTimer() self.print_hz_timer.timeout.connect(self._on_calc_hz) self.print_hz_timer.start(1000) # print "======== create", self.objectName() # # def __del__(self): # print "******* destroy", self.objectName() # def hideEvent(self, event): # self.close() def closeEvent(self, event): if self.sub is not None: self.sub.unregister() del self.sub try: self.ssh_output_file.close() self.ssh_error_file.close() # send Ctrl+C to remote process self.ssh_input_file.write("%s\n" % chr(3)) self.ssh_input_file.close() except: pass self.finished_signal.emit(self.topic) if self.parent() is None: QApplication.quit() # else: # self.setParent(None) def create_field_filter(self, echo_nostr, echo_noarr): def field_filter(val): try: # fields = val.__slots__ # field_types = val._slot_types for f, t in zip(val.__slots__, val._slot_types): if echo_noarr and "[" in t: continue elif echo_nostr and "string" in t: continue yield f except: pass return field_filter def on_no_str_checkbox_toggled(self, state): self.field_filter_fn = self.create_field_filter(state, self.no_arr_checkbox.isChecked()) def on_no_arr_checkbox_toggled(self, state): self.field_filter_fn = self.create_field_filter(self.no_str_checkbox.isChecked(), state) def combobox_reduce_ch_activated(self, ch_txt): try: self.line_limit = int(ch_txt) except ValueError: try: self.line_limit = float(ch_txt) except ValueError: self.combobox_reduce_ch.setEditText(str(self.line_limit)) def on_combobox_hz_activated(self, hz_txt): try: self.receiving_hz = int(hz_txt) except ValueError: try: self.receiving_hz = float(hz_txt) except ValueError: self.combobox_displ_hz.setEditText(str(self.receiving_hz)) def on_combobox_count_activated(self, count_txt): try: self.max_displayed_msgs = int(count_txt) self._blocks_in_msg = None except ValueError: self.combobox_msgs_count.setEditText(str(self.max_displayed_msgs)) def on_clear_btn_clicked(self): self.display.clear() with self.lock: self.message_count = 0 self._scrapped_msgs = 0 del self.times[:] def on_topic_control_btn_clicked(self): try: if self.sub is None and self.ssh_output_file is None: if self.__msg_class: self.sub = rospy.Subscriber(self.topic, self.__msg_class, self._msg_handle) else: self._on_display_anchorClicked(QUrl(self._masteruri)) self.topic_control_button.setText("stop") self.topic_control_button.setIcon(QIcon(":/icons/deleket_deviantart_stop.png")) else: if self.sub is not None: self.sub.unregister() self.sub = None elif self.ssh_output_file is not None: self.ssh_output_file.close() self.ssh_error_file.close() self.ssh_output_file = None self.topic_control_button.setText("play") self.topic_control_button.setIcon(QIcon(":/icons/deleket_deviantart_play.png")) self.no_str_checkbox.setEnabled(True) self.no_arr_checkbox.setEnabled(True) except Exception as e: rospy.logwarn("Error while stop/play echo for topic %s: %s" % (self.topic, e)) def _msg_handle(self, data): self.msg_signal.emit(data, (data._connection_header["latching"] != "0")) def _append_message(self, msg, latched): """ Adds a label to the dialog's layout and shows the given text. @param msg: the text to add to the dialog @type msg: message object """ current_time = time.time() self._count_messages(current_time) # skip messages, if they are received often then MESSAGE_HZ_LIMIT if self._last_received_ts != 0 and self.receiving_hz != 0: if not latched and current_time - self._last_received_ts < 1.0 / self.receiving_hz: self._scrapped_msgs += 1 self._scrapped_msgs_sl += 1 return self._last_received_ts = current_time if not self.show_only_rate: # convert message to string and reduce line width to current limit msg = message.strify_message(msg, field_filter=self.field_filter_fn) if isinstance(msg, tuple): msg = msg[0] msg = self._trim_width(msg) msg = msg.replace("<", "<").replace(">", ">") # create a notification about scrapped messages if self._scrapped_msgs_sl > 0: txt = ( '<pre style="color:red; font-family:Fixedsys,Courier,monospace; padding:10px;">scrapped %s message because of Hz-settings</pre>' % self._scrapped_msgs_sl ) self.display.append(txt) self._scrapped_msgs_sl = 0 txt = ( '<pre style="background-color:#FFFCCC; font-family:Fixedsys,Courier; padding:10px;">---------- %s --------------------\n%s</pre>' % (datetime.now().strftime("%d.%m.%Y %H:%M:%S.%f"), msg) ) # set the count of the displayed messages on receiving the first message self._update_max_msg_count(txt) self.display.append(txt) self._print_status() def _count_messages(self, ts=time.time()): """ Counts the received messages. Call this method only on receive message. """ current_time = ts with self.lock: # time reset if self.msg_t0 < 0 or self.msg_t0 > current_time: self.msg_t0 = current_time self.msg_tn = current_time self.times = [] else: self.times.append(current_time - self.msg_tn) self.msg_tn = current_time # keep only statistics for the last 5000 messages so as not to run out of memory if len(self.times) > self.STATISTIC_QUEUE_LEN: self.times.pop(0) self.message_count += 1 def _trim_width(self, msg): """ reduce line width to current limit :param msg: the message :type msg: str :return: trimmed message """ result = msg if self.line_limit != 0: a = "" for l in msg.splitlines(): a = a + (l if len(l) <= self.line_limit else l[0 : self.line_limit - 3] + "...") + "\n" result = a return result def _update_max_msg_count(self, txt): """ set the count of the displayed messages on receiving the first message :param txt: text of the message, which will be added to the document :type txt: str """ if self._blocks_in_msg is None: td = QTextDocument(txt) self._blocks_in_msg = td.blockCount() self.display.document().setMaximumBlockCount(self._blocks_in_msg * self.max_displayed_msgs) def _on_calc_hz(self): if rospy.is_shutdown(): self.close() return if self.message_count == self.last_printed_count: return with self.lock: # the code from ROS rostopic n = len(self.times) if n < 2: return mean = sum(self.times) / n rate = 1.0 / mean if mean > 0.0 else 0 # std dev std_dev = math.sqrt(sum((x - mean) ** 2 for x in self.times) / n) # min and max max_delta = max(self.times) min_delta = min(self.times) self.last_printed_count = self.message_count self._rate_message = "average rate: %.3f\tmin: %.3fs max: %.3fs std dev: %.5fs window: %s" % ( rate, min_delta, max_delta, std_dev, n + 1, ) if self._scrapped_msgs > 0: self._rate_message += " --- scrapped msgs: %s" % self._scrapped_msgs self._print_status() if self.show_only_rate: self.display.append(self._rate_message) def _print_status(self): self.status_label.setText("%s messages %s" % (self.message_count, self._rate_message)) def _append_text(self, text): """ Append echo text received through the SSH. """ with self.lock: self._current_msg += text if self._current_msg.find("---") != -1: messages = self._current_msg.split("---") for m in messages[:-1]: current_time = time.time() self._count_messages(current_time) # limit the displayed text width m = self._trim_width(m) txt = ( '<pre style="background-color:#FFFCCC; font-family:Fixedsys,Courier; padding:10px;">---------- %s --------------------\n%s</pre>' % (datetime.now().strftime("%d.%m.%Y %H:%M:%S.%f"), m) ) # set the count of the displayed messages on receiving the first message self._update_max_msg_count(txt) self.display.append(txt) self._current_msg = messages[-1] self._print_status() def _append_error_text(self, text): """ Append error text received through the SSH. """ with self.lock: self._current_errmsg += text if self._current_errmsg.find("\n") != -1: messages = self._current_errmsg.split("\n") for m in messages[:-1]: txt = '<pre style="color:red; font-family:Fixedsys,Courier,monospace; padding:10px;">%s</pre>' % m self.display.append(txt) self._current_errmsg = messages[-1] def _append_text_hz(self, text): """ Append text received through the SSH for hz view. """ with self.lock: self._current_msg += text if self._current_msg.find("\n") != -1: messages = self._current_msg.split("\n") for m in messages[:-1]: txt = '<div style="font-family:Fixedsys,Courier;">%s</div>' % (m) self.display.append(txt) self._current_msg = messages[-1] def _on_display_anchorClicked(self, url, user=None, pw=None): try: ok = False if self.show_only_rate: self.ssh_input_file, self.ssh_output_file, self.ssh_error_file, ok = nm.ssh().ssh_exec( url.host(), ["rostopic hz %s" % (self.topic)], user, pw, auto_pw_request=True, get_pty=True ) self.status_label.setText("connected to %s over SSH" % url.host()) else: self.combobox_displ_hz.setEnabled(False) nostr = "--nostr" if self.no_str_checkbox.isChecked() else "" noarr = "--noarr" if self.no_arr_checkbox.isChecked() else "" self.ssh_input_file, self.ssh_output_file, self.ssh_error_file, ok = nm.ssh().ssh_exec( url.host(), ["rostopic echo %s %s %s" % (nostr, noarr, self.topic)], user, pw, auto_pw_request=True, get_pty=True, ) if ok: self.display.clear() target = self._read_output_hz if self.show_only_rate else self._read_output thread = threading.Thread(target=target, args=((self.ssh_output_file,))) thread.setDaemon(True) thread.start() thread = threading.Thread(target=self._read_error, args=((self.ssh_error_file,))) thread.setDaemon(True) thread.start() elif self.ssh_output_file: self.ssh_output_file.close() self.ssh_error_file.close() except Exception as e: self._append_error_text("%s\n" % e) # import traceback # print traceback.format_exc() def _read_output_hz(self, output_file): try: while not output_file.closed: text = output_file.read(1) if text: self.text_hz_signal.emit(text) except: pass # import traceback # print traceback.format_exc() def _read_output(self, output_file): while not output_file.closed: text = output_file.read(1) if text: self.text_signal.emit(text) def _read_error(self, error_file): try: while not error_file.closed: text = error_file.read(1) if text: self.text_error_signal.emit(text) except: pass