def __init__(self, masteruri, cfg, ns, nodes, parent=None):
        QFrame.__init__(self, parent)
        self._masteruri = masteruri
        self._nodes = {cfg: {ns: nodes}}
        frame_layout = QVBoxLayout(self)
        frame_layout.setContentsMargins(0, 0, 0, 0)
        # create frame for warning label
        self.warning_frame = warning_frame = QFrame(self)
        warning_layout = QHBoxLayout(warning_frame)
        warning_layout.setContentsMargins(0, 0, 0, 0)
        warning_layout.addItem(
            QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Expanding))
        self.warning_label = QLabel()
        icon = nm.settings().icon('crystal_clear_warning.png')
        self.warning_label.setPixmap(icon.pixmap(QSize(40, 40)))
        self.warning_label.setToolTip(
            'Multiple configuration for same node found!\nA first one will be selected for the start a node!'
        )
        warning_layout.addWidget(self.warning_label)
        warning_layout.addItem(
            QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Expanding))
        frame_layout.addItem(
            QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Expanding))
        frame_layout.addWidget(warning_frame)
        # create frame for start/stop buttons
        buttons_frame = QFrame()
        buttons_layout = QHBoxLayout(buttons_frame)
        buttons_layout.setContentsMargins(0, 0, 0, 0)
        buttons_layout.addItem(QSpacerItem(20, 20))
        self.on_button = QPushButton()
        self.on_button.setFlat(False)
        self.on_button.setText("On")
        self.on_button.clicked.connect(self.on_on_clicked)
        buttons_layout.addWidget(self.on_button)

        self.off_button = QPushButton()
        self.off_button.setFlat(True)
        self.off_button.setText("Off")
        self.off_button.clicked.connect(self.on_off_clicked)
        buttons_layout.addWidget(self.off_button)
        buttons_layout.addItem(QSpacerItem(20, 20))
        frame_layout.addWidget(buttons_frame)
        frame_layout.addItem(
            QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Expanding))
        self.warning_frame.setVisible(False)
    def __init__(self, masteruri, cfg, ns, nodes, parent=None):
        QFrame.__init__(self, parent)
        self._masteruri = masteruri
        self._nodes = {cfg: {ns: nodes}}
        frame_layout = QVBoxLayout(self)
        frame_layout.setContentsMargins(0, 0, 0, 0)
        # create frame for warning label
        self.warning_frame = warning_frame = QFrame(self)
        warning_layout = QHBoxLayout(warning_frame)
        warning_layout.setContentsMargins(0, 0, 0, 0)
        warning_layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Expanding))
        self.warning_label = QLabel()
        icon = QIcon(':/icons/crystal_clear_warning.png')
        self.warning_label.setPixmap(icon.pixmap(QSize(40, 40)))
        self.warning_label.setToolTip('Multiple configuration for same node found!\nA first one will be selected for the start a node!')
        warning_layout.addWidget(self.warning_label)
        warning_layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Expanding))
        frame_layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Expanding))
        frame_layout.addWidget(warning_frame)
        # create frame for start/stop buttons
        buttons_frame = QFrame()
        buttons_layout = QHBoxLayout(buttons_frame)
        buttons_layout.setContentsMargins(0, 0, 0, 0)
        buttons_layout.addItem(QSpacerItem(20, 20))
        self.on_button = QPushButton()
        self.on_button.setFlat(False)
        self.on_button.setText("On")
        self.on_button.clicked.connect(self.on_on_clicked)
        buttons_layout.addWidget(self.on_button)

        self.off_button = QPushButton()
        self.off_button.setFlat(True)
        self.off_button.setText("Off")
        self.off_button.clicked.connect(self.on_off_clicked)
        buttons_layout.addWidget(self.off_button)
        buttons_layout.addItem(QSpacerItem(20, 20))
        frame_layout.addWidget(buttons_frame)
        frame_layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Expanding))
        self.warning_frame.setVisible(False)
Esempio n. 3
0
    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)
Esempio n. 4
0
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
Esempio n. 5
0
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))
Esempio n. 6
0
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))
Esempio n. 7
0
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
Esempio n. 9
0
    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)