console_widget.py
Go to the documentation of this file.
00001 # Software License Agreement (BSD License)
00002 #
00003 # Copyright (c) 2012, Willow Garage, Inc.
00004 # All rights reserved.
00005 #
00006 # Redistribution and use in source and binary forms, with or without
00007 # modification, are permitted provided that the following conditions
00008 # are met:
00009 #
00010 #  * Redistributions of source code must retain the above copyright
00011 #    notice, this list of conditions and the following disclaimer.
00012 #  * Redistributions in binary form must reproduce the above
00013 #    copyright notice, this list of conditions and the following
00014 #    disclaimer in the documentation and/or other materials provided
00015 #    with the distribution.
00016 #  * Neither the name of Willow Garage, Inc. nor the names of its
00017 #    contributors may be used to endorse or promote products derived
00018 #    from this software without specific prior written permission.
00019 #
00020 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
00021 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
00022 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
00023 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
00024 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
00025 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
00026 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
00027 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
00028 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
00029 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
00030 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00031 # POSSIBILITY OF SUCH DAMAGE.
00032 
00033 import os
00034 
00035 from python_qt_binding import loadUi
00036 from python_qt_binding.QtGui import QCursor, QIcon
00037 from python_qt_binding.QtWidgets import (QApplication, QFileDialog, QHeaderView,
00038                                          QMenu, QMessageBox, QTableView, QWidget)
00039 from python_qt_binding.QtCore import QRegExp, Qt, qWarning
00040 
00041 import time
00042 import datetime
00043 
00044 from rqt_py_common.ini_helper import pack, unpack
00045 
00046 from .filters.custom_filter import CustomFilter
00047 from .filters.location_filter import LocationFilter
00048 from .filters.message_filter import MessageFilter
00049 from .filters.node_filter import NodeFilter
00050 from .filters.severity_filter import SeverityFilter
00051 from .filters.time_filter import TimeFilter
00052 from .filters.topic_filter import TopicFilter
00053 
00054 from .filters.custom_filter_widget import CustomFilterWidget
00055 from .filters.filter_wrapper_widget import FilterWrapperWidget
00056 from .filters.list_filter_widget import ListFilterWidget
00057 from .filters.text_filter_widget import TextFilterWidget
00058 from .filters.time_filter_widget import TimeFilterWidget
00059 
00060 from .message import Message
00061 from .message_data_model import MessageDataModel
00062 
00063 from .text_browse_dialog import TextBrowseDialog
00064 
00065 
00066 class ConsoleWidget(QWidget):
00067 
00068     """
00069     Primary widget for the rqt_console plugin.
00070     """
00071 
00072     def __init__(self, proxy_model, rospack, minimal=False):
00073         """
00074         :param proxymodel: the proxy model to display in the widget,''QSortFilterProxyModel''
00075         :param minimal: if true the load, save and column buttons will be hidden as well as the
00076                         filter splitter, ''bool''
00077         """
00078         super(ConsoleWidget, self).__init__()
00079         self._proxy_model = proxy_model
00080         self._model = self._proxy_model.sourceModel()
00081         self._paused = False
00082         self._rospack = rospack
00083 
00084         # These are lists of Tuples = (,)
00085         self._exclude_filters = []
00086         self._highlight_filters = []
00087 
00088         ui_file = os.path.join(
00089             self._rospack.get_path('rqt_console'), 'resource', 'console_widget.ui')
00090         loadUi(ui_file, self)
00091 
00092         if minimal:
00093             self.load_button.hide()
00094             self.save_button.hide()
00095             self.column_resize_button.hide()
00096         self.setObjectName('ConsoleWidget')
00097         self.table_view.setModel(proxy_model)
00098 
00099         self._columnwidth = (60, 100, 70, 100, 100, 100, 100)
00100         for idx, width in enumerate(self._columnwidth):
00101             self.table_view.horizontalHeader().resizeSection(idx, width)
00102         try:
00103             setSectionResizeMode = self.table_view.horizontalHeader().setSectionResizeMode  # Qt5
00104         except AttributeError:
00105             setSectionResizeMode = self.table_view.horizontalHeader().setResizeMode  # Qt4
00106         setSectionResizeMode(1, QHeaderView.Stretch)
00107 
00108         def update_sort_indicator(logical_index, order):
00109             if logical_index == 0:
00110                 self._proxy_model.sort(-1)
00111             self.table_view.horizontalHeader().setSortIndicatorShown(logical_index != 0)
00112         self.table_view.horizontalHeader().sortIndicatorChanged.connect(update_sort_indicator)
00113 
00114         self.add_exclude_button.setIcon(QIcon.fromTheme('list-add'))
00115         self.add_highlight_button.setIcon(QIcon.fromTheme('list-add'))
00116         self.pause_button.setIcon(QIcon.fromTheme('media-playback-pause'))
00117         if not self.pause_button.icon().isNull():
00118             self.pause_button.setText('')
00119         self.record_button.setIcon(QIcon.fromTheme('media-record'))
00120         if not self.record_button.icon().isNull():
00121             self.record_button.setText('')
00122         self.load_button.setIcon(QIcon.fromTheme('document-open'))
00123         if not self.load_button.icon().isNull():
00124             self.load_button.setText('')
00125         self.save_button.setIcon(QIcon.fromTheme('document-save'))
00126         if not self.save_button.icon().isNull():
00127             self.save_button.setText('')
00128         self.clear_button.setIcon(QIcon.fromTheme('edit-clear'))
00129         if not self.clear_button.icon().isNull():
00130             self.clear_button.setText('')
00131         self.highlight_exclude_button.setIcon(QIcon.fromTheme('format-text-strikethrough'))
00132 
00133         self.pause_button.clicked[bool].connect(self._handle_pause_clicked)
00134         self.record_button.clicked[bool].connect(self._handle_record_clicked)
00135         self.load_button.clicked[bool].connect(self._handle_load_clicked)
00136         self.save_button.clicked[bool].connect(self._handle_save_clicked)
00137         self.column_resize_button.clicked[bool].connect(self._handle_column_resize_clicked)
00138         self.clear_button.clicked[bool].connect(self._handle_clear_button_clicked)
00139 
00140         self.table_view.mouseDoubleClickEvent = self._handle_mouse_double_click
00141         self.table_view.mousePressEvent = self._handle_mouse_press
00142         self.table_view.keyPressEvent = self._handle_custom_keypress
00143 
00144         self.highlight_exclude_button.clicked[bool].connect(
00145             self._proxy_model.set_show_highlighted_only)
00146 
00147         self.add_highlight_button.clicked.connect(self._add_highlight_filter)
00148         self.add_exclude_button.clicked.connect(self._add_exclude_filter)
00149 
00150         # Filter factory dictionary:
00151         # index 0 is a label describing the widget, index 1 is the class that
00152         # provides filtering logic index 2 is the widget that sets the data in the
00153         # filter class, index 3 are the arguments for the widget class constructor
00154         self._filter_factory_order = [
00155             'message', 'severity', 'node', 'time', 'topic', 'location', 'custom']
00156         self.filter_factory = {
00157             'message': (
00158                 self.tr('...containing'),
00159                 MessageFilter,
00160                 TextFilterWidget),
00161             'severity': (
00162                 self.tr('...with severities'),
00163                 SeverityFilter,
00164                 ListFilterWidget,
00165                 self._model.get_severity_dict),
00166             'node': (
00167                 self.tr('...from node'),
00168                 NodeFilter,
00169                 ListFilterWidget,
00170                 self._model.get_unique_nodes),
00171             'time': (
00172                 self.tr('...from time range'),
00173                 TimeFilter,
00174                 TimeFilterWidget,
00175                 self.get_time_range_from_selection),
00176             'topic': (
00177                 self.tr('...from topic'),
00178                 TopicFilter,
00179                 ListFilterWidget,
00180                 self._model.get_unique_topics),
00181             'location': (
00182                 self.tr('...from location'),
00183                 LocationFilter,
00184                 TextFilterWidget),
00185             'custom': (
00186                 self.tr('Custom'),
00187                 CustomFilter,
00188                 CustomFilterWidget,
00189                 [self._model.get_severity_dict,
00190                  self._model.get_unique_nodes,
00191                  self._model.get_unique_topics])}
00192 
00193         self._model.rowsInserted.connect(self.update_status)
00194         self._model.rowsRemoved.connect(self.update_status)
00195         self._proxy_model.rowsInserted.connect(self.update_status)
00196         self._proxy_model.rowsRemoved.connect(self.update_status)
00197 
00198         # list of TextBrowserDialogs to close when cleaning up
00199         self._browsers = []
00200 
00201         # This defaults the filters panel to start by taking 50% of the available space
00202         if minimal:
00203             self.table_splitter.setSizes([1, 0])
00204         else:
00205             self.table_splitter.setSizes([1, 1])
00206         self.exclude_table.resizeColumnsToContents()
00207         self.highlight_table.resizeColumnsToContents()
00208 
00209     def get_message_summary(self, start_time_offset=None, end_time_offset=None):
00210         """
00211         :param start_time: number of seconds before now to start, ''int'' (optional)
00212         :param end_time: number of seconds before now to end, ''int'' (optional)
00213         :returns: summary of message numbers within time
00214         """
00215         current_time = time.mktime(datetime.datetime.now().timetuple())
00216         if start_time_offset is None:
00217             start_time = current_time - 240
00218         else:
00219             start_time = current_time - start_time_offset
00220         if end_time_offset is not None:
00221             end_time = current_time - end_time_offset
00222         else:
00223             end_time = None
00224 
00225         message_subset = self._model.get_message_between(start_time, end_time)
00226 
00227         class Message_Summary(object):
00228             __slots__ = 'fatal', 'error', 'warn', 'info', 'debug'
00229 
00230             def __init__(self, messages):
00231                 self.fatal = 0
00232                 self.error = 0
00233                 self.warn = 0
00234                 self.info = 0
00235                 self.debug = 0
00236                 for message in messages:
00237                     if message.severity == Message.DEBUG:
00238                         self.debug += 1
00239                     elif message.severity == Message.INFO:
00240                         self.info += 1
00241                     elif message.severity == Message.WARN:
00242                         self.warn += 1
00243                     elif message.severity == Message.ERROR:
00244                         self.error += 1
00245                     elif message.severity == Message.FATAL:
00246                         self.fatal += 1
00247                     else:
00248                         assert False, "Unknown severity type '%s'" % str(message.severity)
00249 
00250         return Message_Summary(message_subset)
00251 
00252     def get_time_range_from_selection(self):
00253         """
00254         :returns: the range of time of messages in the current table selection (min, max),
00255                   ''tuple(str,str)''
00256         """
00257         rowlist = []
00258         indexes = self.table_view.selectionModel().selectedIndexes()
00259 
00260         if indexes:
00261             rowlist = [self._proxy_model.mapToSource(current).row() for current in indexes]
00262             rowlist = sorted(list(set(rowlist)))
00263 
00264             mintime, maxtime = self._model.get_time_range(rowlist)
00265             return (mintime, maxtime)
00266         return (-1, -1)
00267 
00268     def _delete_highlight_filter(self):
00269         """
00270         Deletes any highlight filters which have a checked delete button
00271         """
00272         for index, item in enumerate(self._highlight_filters):
00273             if item[1].delete_button.isChecked():
00274                 self._proxy_model.delete_highlight_filter(index)
00275                 self.highlight_table.removeCellWidget(index, 0)
00276                 self.highlight_table.removeRow(index)
00277                 item[0].filter_changed_signal.disconnect(
00278                     self._proxy_model.handle_highlight_filters_changed)
00279                 item[1].delete_button.clicked.disconnect(self._delete_highlight_filter)
00280                 del self._highlight_filters[index]
00281 
00282     def _delete_exclude_filter(self):
00283         """
00284         Deletes any exclude filters which have a checked delete button
00285         """
00286         for index, item in enumerate(self._exclude_filters):
00287             if item[1].delete_button.isChecked():
00288                 self._proxy_model.delete_exclude_filter(index)
00289                 self.exclude_table.removeCellWidget(index, 0)
00290                 self.exclude_table.removeRow(index)
00291                 item[0].filter_changed_signal.disconnect(
00292                     self._proxy_model.handle_exclude_filters_changed)
00293                 item[1].delete_button.clicked.disconnect(self._delete_exclude_filter)
00294                 del self._exclude_filters[index]
00295 
00296     def _add_highlight_filter(self, filter_index=False):
00297         """
00298         :param filter_index: if false then this function shows a QMenu to allow the user to choose
00299                              a type of message filter. ''bool''
00300         OR
00301         :param filter_index: the index of the filter to be added, ''int''
00302         :return: if a filter was added then the index is returned, ''int''
00303         OR
00304         :return: if no filter was added then None is returned, ''NoneType''
00305         """
00306         if filter_index is False:
00307             filter_index = -1
00308             filter_select_menu = QMenu()
00309             for index in self._filter_factory_order:
00310                 # flattens the _highlight filters list and only adds the item if it
00311                 # doesn't already exist
00312 
00313                 if index in ['message', 'location'] or \
00314                         not self.filter_factory[index][1] in \
00315                             [type(item) for sublist in self._highlight_filters for item in sublist]:
00316                     filter_select_menu.addAction(self.filter_factory[index][0])
00317             action = filter_select_menu.exec_(QCursor.pos())
00318             if action is None:
00319                 return
00320             for index in self._filter_factory_order:
00321                 if self.filter_factory[index][0] == action.text():
00322                     filter_index = index
00323             if filter_index == -1:
00324                 return
00325 
00326         index = len(self._highlight_filters)
00327         newfilter = self.filter_factory[filter_index][1]()
00328         if len(self.filter_factory[filter_index]) >= 4:
00329             newwidget = self.filter_factory[filter_index][2](
00330                 newfilter, self._rospack, self.filter_factory[filter_index][3])
00331         else:
00332             newwidget = self.filter_factory[filter_index][2](newfilter, self._rospack)
00333 
00334         # pack the new filter tuple onto the filter list
00335         self._highlight_filters.append((
00336             newfilter,
00337             FilterWrapperWidget(
00338                 newwidget, self.filter_factory[filter_index][0]),
00339             filter_index))
00340         self._proxy_model.add_highlight_filter(newfilter)
00341         newfilter.filter_changed_signal.connect(self._proxy_model.handle_highlight_filters_changed)
00342         self._highlight_filters[index][1].delete_button.clicked.connect(
00343             self._delete_highlight_filter)
00344         self._model.rowsInserted.connect(self._highlight_filters[index][1].repopulate)
00345 
00346         # place the widget in the proper location
00347         self.highlight_table.insertRow(index)
00348         self.highlight_table.setCellWidget(index, 0, self._highlight_filters[index][1])
00349         self.highlight_table.resizeColumnsToContents()
00350         self.highlight_table.resizeRowsToContents()
00351         newfilter.filter_changed_signal.emit()
00352         return index
00353 
00354     def _add_exclude_filter(self, filter_index=False):
00355         """
00356         :param filter_index: if false then this function shows a QMenu to allow the user to choose a
00357                              type of message filter. ''bool''
00358         OR
00359         :param filter_index: the index of the filter to be added, ''int''
00360         :return: if a filter was added then the index is returned, ''int''
00361         OR
00362         :return: if no filter was added then None is returned, ''NoneType''
00363         """
00364         if filter_index is False:
00365             filter_index = -1
00366             filter_select_menu = QMenu()
00367             for index in self._filter_factory_order:
00368                 # flattens the _exclude filters list and only adds the item if it doesn't
00369                 # already exist
00370                 if index in ['message', 'location'] or \
00371                         not self.filter_factory[index][1] in \
00372                             [type(item) for sublist in self._exclude_filters for item in sublist]:
00373                     filter_select_menu.addAction(self.filter_factory[index][0])
00374             action = filter_select_menu.exec_(QCursor.pos())
00375             if action is None:
00376                 return None
00377             for index in self._filter_factory_order:
00378                 if self.filter_factory[index][0] == action.text():
00379                     filter_index = index
00380             if filter_index == -1:
00381                 return None
00382 
00383         index = len(self._exclude_filters)
00384         newfilter = self.filter_factory[filter_index][1]()
00385         if len(self.filter_factory[filter_index]) >= 4:
00386             newwidget = self.filter_factory[filter_index][2](
00387                 newfilter, self._rospack, self.filter_factory[filter_index][3])
00388         else:
00389             newwidget = self.filter_factory[filter_index][2](newfilter, self._rospack)
00390 
00391         # pack the new filter tuple onto the filter list
00392         self._exclude_filters.append((
00393             newfilter,
00394             FilterWrapperWidget(
00395                 newwidget, self.filter_factory[filter_index][0]),
00396             filter_index))
00397         self._proxy_model.add_exclude_filter(newfilter)
00398         newfilter.filter_changed_signal.connect(self._proxy_model.handle_exclude_filters_changed)
00399         self._exclude_filters[index][1].delete_button.clicked.connect(self._delete_exclude_filter)
00400         self._model.rowsInserted.connect(self._exclude_filters[index][1].repopulate)
00401 
00402         # place the widget in the proper location
00403         self.exclude_table.insertRow(index)
00404         self.exclude_table.setCellWidget(index, 0, self._exclude_filters[index][1])
00405         self.exclude_table.resizeColumnsToContents()
00406         self.exclude_table.resizeRowsToContents()
00407         newfilter.filter_changed_signal.emit()
00408         return index
00409 
00410     def _process_highlight_exclude_filter(self, selection, selectiontype, exclude=False):
00411         """
00412         Modifies the relevant filters (based on selectiontype) to remove (exclude=True)
00413         or highlight (exclude=False) the selection from the dataset in the tableview.
00414         :param selection: the actual selection, ''str''
00415         :param selectiontype: the type of selection, ''str''
00416         :param exclude: If True process as an exclude filter, False process as an highlight filter,
00417                         ''bool''
00418         """
00419         types = {
00420             self.tr('Node'): 2,
00421             self.tr('Topic'): 4,
00422             self.tr('Severity'): 1,
00423             self.tr('Message'): 0}
00424         try:
00425             col = types[selectiontype]
00426         except:
00427             raise RuntimeError(
00428                 "Bad Column name in ConsoleWidget._process_highlight_exclude_filter()")
00429 
00430         if col == 0:
00431             unique_messages = set()
00432             selected_indexes = self.table_view.selectionModel().selectedIndexes()
00433             num_selected = len(selected_indexes) / 6
00434             for index in range(num_selected):
00435                 unique_messages.add(selected_indexes[num_selected * col + index].data())
00436             unique_messages = list(unique_messages)
00437             for message in unique_messages:
00438                 message = message.replace('\\', '\\\\')
00439                 message = message.replace('.', '\\.')
00440                 if exclude:
00441                     filter_index = self._add_exclude_filter(selectiontype.lower())
00442                     filter_widget = self._exclude_filters[filter_index][1].findChildren(
00443                         QWidget, QRegExp('.*FilterWidget.*'))[0]
00444                 else:
00445                     filter_index = self._add_highlight_filter(col)
00446                     filter_widget = self._highlight_filters[filter_index][1].findChildren(
00447                         QWidget, QRegExp('.*FilterWidget.*'))[0]
00448                 filter_widget.set_regex(True)
00449                 filter_widget.set_text('^' + message + '$')
00450 
00451         else:
00452             if exclude:
00453                 # Test if the filter we are adding already exists if it does use the existing filter
00454                 if self.filter_factory[selectiontype.lower()][1] not in \
00455                         [type(item) for sublist in self._exclude_filters for item in sublist]:
00456                     filter_index = self._add_exclude_filter(selectiontype.lower())
00457                 else:
00458                     for index, item in enumerate(self._exclude_filters):
00459                         if type(item[0]) == self.filter_factory[selectiontype.lower()][1]:
00460                             filter_index = index
00461             else:
00462                 # Test if the filter we are adding already exists if it does use the existing filter
00463                 if self.filter_factory[selectiontype.lower()][1] not in \
00464                         [type(item) for sublist in self._highlight_filters for item in sublist]:
00465                     filter_index = self._add_highlight_filter(col)
00466                 else:
00467                     for index, item in enumerate(self._highlight_filters):
00468                         if type(item[0]) == self.filter_factory[selectiontype.lower()][1]:
00469                             filter_index = index
00470 
00471             if exclude:
00472                 filter_widget = self._exclude_filters[filter_index][1].findChildren(
00473                     QWidget, QRegExp('.*FilterWidget.*'))[0]
00474                 filter_widget.select_item(selection)
00475             else:
00476                 filter_widget = self._highlight_filters[filter_index][1].findChildren(
00477                     QWidget, QRegExp('.*FilterWidget.*'))[0]
00478                 filter_widget.select_item(selection)
00479 
00480     def _rightclick_menu(self, event):
00481         """
00482         Dynamically builds the rightclick menu based on the unique column data
00483         from the passed in datamodel and then launches it modally
00484         :param event: the mouse event object, ''QMouseEvent''
00485         """
00486         severities = {}
00487         for severity, label in Message.SEVERITY_LABELS.items():
00488             if severity in self._model.get_unique_severities():
00489                 severities[severity] = label
00490         nodes = sorted(self._model.get_unique_nodes())
00491         topics = sorted(self._model.get_unique_topics())
00492 
00493         # menutext entries turned into
00494         menutext = []
00495         menutext.append([self.tr('Exclude'), [[self.tr('Severity'), severities],
00496                                               [self.tr('Node'), nodes],
00497                                               [self.tr('Topic'), topics],
00498                                               [self.tr('Selected Message(s)')]]])
00499         menutext.append([self.tr('Highlight'), [[self.tr('Severity'), severities],
00500                                                 [self.tr('Node'), nodes],
00501                                                 [self.tr('Topic'), topics],
00502                                                 [self.tr('Selected Message(s)')]]])
00503         menutext.append([self.tr('Copy Selected')])
00504         menutext.append([self.tr('Browse Selected')])
00505 
00506         menu = QMenu()
00507         submenus = []
00508         subsubmenus = []
00509         for item in menutext:
00510             if len(item) > 1:
00511                 submenus.append(QMenu(item[0], menu))
00512                 for subitem in item[1]:
00513                     if len(subitem) > 1:
00514                         subsubmenus.append(QMenu(subitem[0], submenus[-1]))
00515                         if isinstance(subitem[1], dict):
00516                             for key in sorted(subitem[1].keys()):
00517                                 action = subsubmenus[-1].addAction(subitem[1][key])
00518                                 action.setData(key)
00519                         else:
00520                             for subsubitem in subitem[1]:
00521                                 subsubmenus[-1].addAction(subsubitem)
00522                         submenus[-1].addMenu(subsubmenus[-1])
00523                     else:
00524                         submenus[-1].addAction(subitem[0])
00525                 menu.addMenu(submenus[-1])
00526             else:
00527                 menu.addAction(item[0])
00528         action = menu.exec_(event.globalPos())
00529 
00530         if action is None or action == 0:
00531             return
00532         elif action.text() == self.tr('Browse Selected'):
00533             self._show_browsers()
00534         elif action.text() == self.tr('Copy Selected'):
00535             rowlist = []
00536             for current in self.table_view.selectionModel().selectedIndexes():
00537                 rowlist.append(self._proxy_model.mapToSource(current).row())
00538             copytext = self._model.get_selected_text(rowlist)
00539             if copytext is not None:
00540                 clipboard = QApplication.clipboard()
00541                 clipboard.setText(copytext)
00542         elif action.text() == self.tr('Selected Message(s)'):
00543             if action.parentWidget().title() == self.tr('Highlight'):
00544                 self._process_highlight_exclude_filter(action.text(), 'Message', False)
00545             elif action.parentWidget().title() == self.tr('Exclude'):
00546                 self._process_highlight_exclude_filter(action.text(), 'Message', True)
00547             else:
00548                 raise RuntimeError("Menu format corruption in ConsoleWidget._rightclick_menu()")
00549         else:
00550             # This processes the dynamic list entries (severity, node and topic)
00551             try:
00552                 roottitle = action.parentWidget().parentWidget().title()
00553             except:
00554                 raise RuntimeError("Menu format corruption in ConsoleWidget._rightclick_menu()")
00555 
00556             if roottitle == self.tr('Highlight'):
00557                 self._process_highlight_exclude_filter(
00558                     action.text(), action.parentWidget().title(), False)
00559             elif roottitle == self.tr('Exclude'):
00560                 self._process_highlight_exclude_filter(
00561                     action.text(), action.parentWidget().title(), True)
00562             else:
00563                 raise RuntimeError(
00564                     "Unknown Root Action %s selected in ConsoleWidget._rightclick_menu()" %
00565                     roottitle)
00566 
00567     def update_status(self):
00568         """
00569         Sets the message display label to the current value
00570         """
00571         if self._model.rowCount() == self._proxy_model.rowCount():
00572             tip = self.tr('Displaying %d messages') % (self._model.rowCount())
00573         else:
00574             tip = self.tr('Displaying %d of %d messages') % (
00575                 self._proxy_model.rowCount(), self._model.rowCount())
00576         self.messages_label.setText(tip)
00577 
00578     def cleanup_browsers_on_close(self):
00579         for browser in self._browsers:
00580             browser.close()
00581 
00582     def _show_browsers(self):
00583         rowlist = []
00584         for current in self.table_view.selectionModel().selectedIndexes():
00585             rowlist.append(self._proxy_model.mapToSource(current).row())
00586         browsetext = self._model.get_selected_text(rowlist)
00587         if browsetext is not None:
00588             self._browsers.append(TextBrowseDialog(browsetext, self._rospack))
00589             self._browsers[-1].show()
00590 
00591     def _handle_clear_button_clicked(self, checked):
00592         self._model.remove_rows([])
00593         Message._next_id = 1
00594 
00595     def _handle_load_clicked(self, checked):
00596         filename = QFileDialog.getOpenFileName(
00597             self, self.tr('Load from File'), '.',
00598             self.tr('rqt_console message file {.csv} (*.csv)'))
00599         if filename[0] != '':
00600             try:
00601                 with open(filename[0], 'r') as h:
00602                     lines = h.read().splitlines()
00603             except IOError as e:
00604                 qWarning(str(e))
00605                 return False
00606 
00607             # extract column header
00608             columns = lines[0].split(';')
00609             if len(lines) < 2:
00610                 return True
00611 
00612             # join wrapped lines
00613             rows = []
00614             last_wrapped = False
00615             for line in lines[1:]:
00616                 # ignore empty lines
00617                 if not line:
00618                     continue
00619                 # check for quotes and remove them
00620                 if line == '"':
00621                     has_prefix = not last_wrapped
00622                     has_suffix = last_wrapped
00623                     line = ''
00624                 else:
00625                     has_prefix = line[0] == '"'
00626                     if has_prefix:
00627                         line = line[1:]
00628                     has_suffix = line[-1] == '"'
00629                     if has_suffix:
00630                         line = line[:-1]
00631 
00632                 # ignore line without prefix if previous line was not wrapped
00633                 if not has_prefix and not last_wrapped:
00634                     continue
00635                 # remove wrapped line which is not continued on the next line
00636                 if last_wrapped and has_prefix:
00637                     rows.pop()
00638 
00639                 # add/append lines
00640                 if last_wrapped:
00641                     rows[-1] += line
00642                 else:
00643                     # add line without quote prefix
00644                     rows.append(line)
00645 
00646                 last_wrapped = not has_suffix
00647 
00648             # generate message for each row
00649             messages = []
00650             skipped = []
00651             for row in rows:
00652                 data = row.split('";"')
00653                 msg = Message()
00654                 msg.set_stamp_format('hh:mm:ss.ZZZ (yyyy-MM-dd)')
00655                 for i, column in enumerate(columns):
00656                     value = data[i]
00657                     if column == 'message':
00658                         msg.message = value.replace('\\"', '"')
00659                     elif column == 'severity':
00660                         msg.severity = int(value)
00661                         if msg.severity not in Message.SEVERITY_LABELS:
00662                             skipped.append('Unknown severity value: %s' % value)
00663                             msg = None
00664                             break
00665                     elif column == 'stamp':
00666                         parts = value.split('.')
00667                         if len(parts) != 2:
00668                             skipped.append('Unknown timestamp format: %s' % value)
00669                             msg = None
00670                             break
00671                         msg.stamp = (int(parts[0]), int(parts[1]))
00672                     elif column == 'topics':
00673                         msg.topics = value.split(',')
00674                     elif column == 'node':
00675                         msg.node = value
00676                     elif column == 'location':
00677                         msg.location = value
00678                     else:
00679                         skipped.append('Unknown column: %s' % column)
00680                         msg = None
00681                         break
00682                 if msg:
00683                     messages.append(msg)
00684             if skipped:
00685                 qWarning(
00686                     'Skipped %d rows since they do not appear to be in '
00687                     'rqt_console message file format:\n- %s' %
00688                     (len(skipped), '\n- '.join(skipped)))
00689 
00690             if messages:
00691                 self._model.insert_rows(messages)
00692 
00693                 self._handle_pause_clicked(True)
00694 
00695             return True
00696 
00697         else:
00698             qWarning('File does not appear to be a rqt_console message file: missing file header.')
00699             return False
00700 
00701     def _handle_save_clicked(self, checked):
00702         filename = QFileDialog.getSaveFileName(
00703             self, 'Save to File', '.', self.tr('rqt_console msg file {.csv} (*.csv)'))
00704         if filename[0] != '':
00705             filename = filename[0]
00706             if filename[-4:] != '.csv':
00707                 filename += '.csv'
00708             try:
00709                 handle = open(filename, 'w')
00710             except IOError as e:
00711                 qWarning(str(e))
00712                 return
00713             try:
00714                 handle.write(';'.join(MessageDataModel.columns) + '\n')
00715                 for index in range(self._proxy_model.rowCount()):
00716                     row = self._proxy_model.mapToSource(self._proxy_model.index(index, 0)).row()
00717                     msg = self._model._messages[row]
00718                     data = {}
00719                     data['message'] = msg.message.replace('"', '\\"')
00720                     data['severity'] = str(msg.severity)
00721                     data['node'] = msg.node
00722                     data['stamp'] = str(msg.stamp[0]) + '.' + str(msg.stamp[1]).zfill(9)
00723                     data['topics'] = ','.join(msg.topics)
00724                     data['location'] = msg.location
00725                     line = []
00726                     for column in MessageDataModel.columns:
00727                         line.append('"%s"' % data[column])
00728                     handle.write(';'.join(line) + '\n')
00729             except Exception as e:
00730                 qWarning('File save failed: %s' % str(e))
00731                 return False
00732             finally:
00733                 handle.close()
00734             return True
00735 
00736     def _handle_pause_clicked(self):
00737         self._paused = True
00738         self.pause_button.setVisible(False)
00739         self.record_button.setVisible(True)
00740 
00741     def _handle_record_clicked(self):
00742         self._paused = False
00743         self.pause_button.setVisible(True)
00744         self.record_button.setVisible(False)
00745 
00746     def _handle_column_resize_clicked(self):
00747         self.table_view.resizeColumnsToContents()
00748 
00749     def _delete_selected_rows(self):
00750         rowlist = []
00751         for current in self.table_view.selectionModel().selectedIndexes():
00752             rowlist.append(self._proxy_model.mapToSource(current).row())
00753         rowlist = list(set(rowlist))
00754         return self._model.remove_rows(rowlist)
00755 
00756     def _handle_custom_keypress(self, event, old_keyPressEvent=QTableView.keyPressEvent):
00757         """
00758         Handles the delete key.
00759         The delete key removes the tableview's selected rows from the datamodel
00760         """
00761         if event.key() == Qt.Key_Delete and len(self._model._messages) > 0:
00762             delete = QMessageBox.Yes
00763             if len(self.table_view.selectionModel().selectedIndexes()) == 0:
00764                 delete = QMessageBox.question(
00765                     self, self.tr('Message'),
00766                     self.tr("Are you sure you want to delete all messages?"),
00767                     QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
00768             if delete == QMessageBox.Yes and \
00769                     event.key() == Qt.Key_Delete and \
00770                     event.modifiers() == Qt.NoModifier:
00771                 if self._delete_selected_rows():
00772                     event.accept()
00773         return old_keyPressEvent(self.table_view, event)
00774 
00775     def _handle_mouse_double_click(
00776             self, event,
00777             old_doubleclickevent=QTableView.mouseDoubleClickEvent):
00778 
00779         if event.buttons() & Qt.LeftButton and event.modifiers() == Qt.NoModifier:
00780             self._show_browsers()
00781             event.accept()
00782         return old_doubleclickevent(self.table_view, event)
00783 
00784     def _handle_mouse_press(self, event, old_pressEvent=QTableView.mousePressEvent):
00785         if event.buttons() & Qt.RightButton and event.modifiers() == Qt.NoModifier:
00786             self._rightclick_menu(event)
00787             event.accept()
00788         return old_pressEvent(self.table_view, event)
00789 
00790     def save_settings(self, plugin_settings, instance_settings):
00791         instance_settings.set_value('settings_exist', True)
00792 
00793         instance_settings.set_value('table_splitter', self.table_splitter.saveState())
00794         instance_settings.set_value('filter_splitter', self.filter_splitter.saveState())
00795 
00796         instance_settings.set_value('paused', self._paused)
00797         instance_settings.set_value(
00798             'show_highlighted_only', self.highlight_exclude_button.isChecked())
00799 
00800         exclude_filters = []
00801         for index, item in enumerate(self._exclude_filters):
00802             exclude_filters.append(item[2])
00803             filter_settings = instance_settings.get_settings('exclude_filter_' + str(index))
00804             item[1].save_settings(filter_settings)
00805         instance_settings.set_value('exclude_filters', pack(exclude_filters))
00806 
00807         highlight_filters = []
00808         for index, item in enumerate(self._highlight_filters):
00809             highlight_filters.append(item[2])
00810             filter_settings = instance_settings.get_settings('highlight_filter_' + str(index))
00811             item[1].save_settings(filter_settings)
00812         instance_settings.set_value('highlight_filters', pack(highlight_filters))
00813         instance_settings.set_value('message_limit', self._model.get_message_limit())
00814 
00815     def restore_settings(self, pluggin_settings, instance_settings):
00816         if instance_settings.contains('table_splitter'):
00817             self.table_splitter.restoreState(instance_settings.value('table_splitter'))
00818         if instance_settings.contains('filter_splitter'):
00819             self.filter_splitter.restoreState(instance_settings.value('filter_splitter'))
00820         else:
00821             self.filter_splitter.setSizes([1, 1])
00822 
00823         paused = instance_settings.value('paused') in [True, 'true']
00824         if paused:
00825             self._handle_pause_clicked()
00826         else:
00827             self._handle_record_clicked()
00828         self.highlight_exclude_button.setChecked(
00829             instance_settings.value('show_highlighted_only') in [True, 'true'])
00830         self._proxy_model.set_show_highlighted_only(self.highlight_exclude_button.isChecked())
00831 
00832         for item in self._exclude_filters:
00833             item[1].delete_button.setChecked(True)
00834         self._delete_exclude_filter()
00835         if instance_settings.contains('exclude_filters'):
00836             exclude_filters = unpack(instance_settings.value('exclude_filters'))
00837             if exclude_filters is not None:
00838                 for index, item in enumerate(exclude_filters):
00839                     self._add_exclude_filter(item)
00840                     filter_settings = instance_settings.get_settings('exclude_filter_' + str(index))
00841                     self._exclude_filters[-1][1].restore_settings(filter_settings)
00842         else:
00843             self._add_exclude_filter('severity')
00844 
00845         for item in self._highlight_filters:
00846             item[1].delete_button.setChecked(True)
00847         self._delete_highlight_filter()
00848         if instance_settings.contains('highlight_filters'):
00849             highlight_filters = unpack(instance_settings.value('highlight_filters'))
00850             if highlight_filters is not None:
00851                 for index, item in enumerate(highlight_filters):
00852                     self._add_highlight_filter(item)
00853                     filter_settings = instance_settings.get_settings(
00854                         'highlight_filter_' + str(index))
00855                     self._highlight_filters[-1][1].restore_settings(filter_settings)
00856         else:
00857             self._add_highlight_filter('message')
00858 
00859         if instance_settings.contains('message_limit'):
00860             self._model.set_message_limit(int(instance_settings.value('message_limit')))


rqt_console
Author(s): Aaron Blasdel
autogenerated on Sat Jun 8 2019 20:58:08