35 from python_qt_binding
import loadUi
36 from python_qt_binding.QtGui
import QCursor, QIcon
37 from python_qt_binding.QtWidgets
import (QApplication, QFileDialog, QHeaderView,
38 QMenu, QMessageBox, QTableView, QWidget)
39 from python_qt_binding.QtCore
import QRegExp, Qt, qWarning
46 from .filters.custom_filter
import CustomFilter
47 from .filters.location_filter
import LocationFilter
48 from .filters.message_filter
import MessageFilter
49 from .filters.node_filter
import NodeFilter
50 from .filters.severity_filter
import SeverityFilter
51 from .filters.time_filter
import TimeFilter
52 from .filters.topic_filter
import TopicFilter
54 from .filters.custom_filter_widget
import CustomFilterWidget
55 from .filters.filter_wrapper_widget
import FilterWrapperWidget
56 from .filters.list_filter_widget
import ListFilterWidget
57 from .filters.text_filter_widget
import TextFilterWidget
58 from .filters.time_filter_widget
import TimeFilterWidget
60 from .message
import Message
61 from .message_data_model
import MessageDataModel
63 from .text_browse_dialog
import TextBrowseDialog
69 Primary widget for the rqt_console plugin. 72 def __init__(self, proxy_model, rospack, minimal=False):
74 :param proxymodel: the proxy model to display in the widget,''QSortFilterProxyModel'' 75 :param minimal: if true the load, save and column buttons will be hidden as well as the 76 filter splitter, ''bool'' 78 super(ConsoleWidget, self).
__init__()
80 self.
_model = self._proxy_model.sourceModel()
88 ui_file = os.path.join(
89 self._rospack.get_path(
'rqt_console'),
'resource',
'console_widget.ui')
93 self.load_button.hide()
94 self.save_button.hide()
95 self.column_resize_button.hide()
96 self.setObjectName(
'ConsoleWidget')
97 self.table_view.setModel(proxy_model)
101 self.table_view.horizontalHeader().resizeSection(idx, width)
103 setSectionResizeMode = self.table_view.horizontalHeader().setSectionResizeMode
104 except AttributeError:
105 setSectionResizeMode = self.table_view.horizontalHeader().setResizeMode
106 setSectionResizeMode(1, QHeaderView.Stretch)
108 def update_sort_indicator(logical_index, order):
109 if logical_index == 0:
110 self._proxy_model.sort(-1)
111 self.table_view.horizontalHeader().setSortIndicatorShown(logical_index != 0)
112 self.table_view.horizontalHeader().sortIndicatorChanged.connect(update_sort_indicator)
114 self.add_exclude_button.setIcon(QIcon.fromTheme(
'list-add'))
115 self.add_highlight_button.setIcon(QIcon.fromTheme(
'list-add'))
116 self.pause_button.setIcon(QIcon.fromTheme(
'media-playback-pause'))
117 if not self.pause_button.icon().isNull():
118 self.pause_button.setText(
'')
119 self.record_button.setIcon(QIcon.fromTheme(
'media-record'))
120 if not self.record_button.icon().isNull():
121 self.record_button.setText(
'')
122 self.load_button.setIcon(QIcon.fromTheme(
'document-open'))
123 if not self.load_button.icon().isNull():
124 self.load_button.setText(
'')
125 self.save_button.setIcon(QIcon.fromTheme(
'document-save'))
126 if not self.save_button.icon().isNull():
127 self.save_button.setText(
'')
128 self.clear_button.setIcon(QIcon.fromTheme(
'edit-clear'))
129 if not self.clear_button.icon().isNull():
130 self.clear_button.setText(
'')
131 self.highlight_exclude_button.setIcon(QIcon.fromTheme(
'format-text-strikethrough'))
144 self.highlight_exclude_button.clicked[bool].connect(
145 self._proxy_model.set_show_highlighted_only)
155 'message',
'severity',
'node',
'time',
'topic',
'location',
'custom']
158 self.tr(
'...containing'),
162 self.tr(
'...with severities'),
165 self._model.get_severity_dict),
167 self.tr(
'...from node'),
170 self._model.get_unique_nodes),
172 self.tr(
'...from time range'),
177 self.tr(
'...from topic'),
180 self._model.get_unique_topics),
182 self.tr(
'...from location'),
189 [self._model.get_severity_dict,
190 self._model.get_unique_nodes,
191 self._model.get_unique_topics])}
203 self.table_splitter.setSizes([1, 0])
205 self.table_splitter.setSizes([1, 1])
206 self.exclude_table.resizeColumnsToContents()
207 self.highlight_table.resizeColumnsToContents()
211 :param start_time: number of seconds before now to start, ''int'' (optional) 212 :param end_time: number of seconds before now to end, ''int'' (optional) 213 :returns: summary of message numbers within time 215 current_time = time.mktime(datetime.datetime.now().timetuple())
216 if start_time_offset
is None:
217 start_time = current_time - 240
219 start_time = current_time - start_time_offset
220 if end_time_offset
is not None:
221 end_time = current_time - end_time_offset
225 message_subset = self._model.get_message_between(start_time, end_time)
227 class Message_Summary(object):
228 __slots__ =
'fatal',
'error',
'warn',
'info',
'debug' 236 for message
in messages:
237 if message.severity == Message.DEBUG:
239 elif message.severity == Message.INFO:
241 elif message.severity == Message.WARN:
243 elif message.severity == Message.ERROR:
245 elif message.severity == Message.FATAL:
248 assert False,
"Unknown severity type '%s'" % str(message.severity)
250 return Message_Summary(message_subset)
254 :returns: the range of time of messages in the current table selection (min, max), 258 indexes = self.table_view.selectionModel().selectedIndexes()
261 rowlist = [self._proxy_model.mapToSource(current).row()
for current
in indexes]
262 rowlist = sorted(list(set(rowlist)))
264 mintime, maxtime = self._model.get_time_range(rowlist)
265 return (mintime, maxtime)
270 Deletes any highlight filters which have a checked delete button 273 if item[1].delete_button.isChecked():
274 self._proxy_model.delete_highlight_filter(index)
275 self.highlight_table.removeCellWidget(index, 0)
276 self.highlight_table.removeRow(index)
277 item[0].filter_changed_signal.disconnect(
278 self._proxy_model.handle_highlight_filters_changed)
284 Deletes any exclude filters which have a checked delete button 287 if item[1].delete_button.isChecked():
288 self._proxy_model.delete_exclude_filter(index)
289 self.exclude_table.removeCellWidget(index, 0)
290 self.exclude_table.removeRow(index)
291 item[0].filter_changed_signal.disconnect(
292 self._proxy_model.handle_exclude_filters_changed)
298 :param filter_index: if false then this function shows a QMenu to allow the user to choose 299 a type of message filter. ''bool'' 301 :param filter_index: the index of the filter to be added, ''int'' 302 :return: if a filter was added then the index is returned, ''int'' 304 :return: if no filter was added then None is returned, ''NoneType'' 306 if filter_index
is False:
308 filter_select_menu = QMenu()
313 if index
in [
'message',
'location']
or \
317 action = filter_select_menu.exec_(QCursor.pos())
323 if filter_index == -1:
335 self._highlight_filters.append((
340 self._proxy_model.add_highlight_filter(newfilter)
341 newfilter.filter_changed_signal.connect(self._proxy_model.handle_highlight_filters_changed)
347 self.highlight_table.insertRow(index)
349 self.highlight_table.resizeColumnsToContents()
350 self.highlight_table.resizeRowsToContents()
351 newfilter.filter_changed_signal.emit()
356 :param filter_index: if false then this function shows a QMenu to allow the user to choose a 357 type of message filter. ''bool'' 359 :param filter_index: the index of the filter to be added, ''int'' 360 :return: if a filter was added then the index is returned, ''int'' 362 :return: if no filter was added then None is returned, ''NoneType'' 364 if filter_index
is False:
366 filter_select_menu = QMenu()
370 if index
in [
'message',
'location']
or \
374 action = filter_select_menu.exec_(QCursor.pos())
380 if filter_index == -1:
392 self._exclude_filters.append((
397 self._proxy_model.add_exclude_filter(newfilter)
398 newfilter.filter_changed_signal.connect(self._proxy_model.handle_exclude_filters_changed)
400 self._model.rowsInserted.connect(self.
_exclude_filters[index][1].repopulate)
403 self.exclude_table.insertRow(index)
405 self.exclude_table.resizeColumnsToContents()
406 self.exclude_table.resizeRowsToContents()
407 newfilter.filter_changed_signal.emit()
412 Modifies the relevant filters (based on selectiontype) to remove (exclude=True) 413 or highlight (exclude=False) the selection from the dataset in the tableview. 414 :param selection: the actual selection, ''str'' 415 :param selectiontype: the type of selection, ''str'' 416 :param exclude: If True process as an exclude filter, False process as an highlight filter, 422 self.tr(
'Severity'): 1,
423 self.tr(
'Message'): 0}
425 col = types[selectiontype]
428 "Bad Column name in ConsoleWidget._process_highlight_exclude_filter()")
431 unique_messages = set()
432 selected_indexes = self.table_view.selectionModel().selectedIndexes()
433 num_selected = len(selected_indexes) / 6
434 for index
in range(num_selected):
435 unique_messages.add(selected_indexes[num_selected * col + index].data())
436 unique_messages = list(unique_messages)
437 for message
in unique_messages:
438 message = message.replace(
'\\',
'\\\\')
439 message = message.replace(
'.',
'\\.')
443 QWidget, QRegExp(
'.*FilterWidget.*'))[0]
447 QWidget, QRegExp(
'.*FilterWidget.*'))[0]
448 filter_widget.set_regex(
True)
449 filter_widget.set_text(
'^' + message +
'$')
459 if type(item[0]) == self.
filter_factory[selectiontype.lower()][1]:
468 if type(item[0]) == self.
filter_factory[selectiontype.lower()][1]:
473 QWidget, QRegExp(
'.*FilterWidget.*'))[0]
474 filter_widget.select_item(selection)
477 QWidget, QRegExp(
'.*FilterWidget.*'))[0]
478 filter_widget.select_item(selection)
482 Dynamically builds the rightclick menu based on the unique column data 483 from the passed in datamodel and then launches it modally 484 :param event: the mouse event object, ''QMouseEvent'' 487 for severity, label
in Message.SEVERITY_LABELS.items():
488 if severity
in self._model.get_unique_severities():
489 severities[severity] = label
490 nodes = sorted(self._model.get_unique_nodes())
491 topics = sorted(self._model.get_unique_topics())
495 menutext.append([self.tr(
'Exclude'), [[self.tr(
'Severity'), severities],
496 [self.tr(
'Node'), nodes],
497 [self.tr(
'Topic'), topics],
498 [self.tr(
'Selected Message(s)')]]])
499 menutext.append([self.tr(
'Highlight'), [[self.tr(
'Severity'), severities],
500 [self.tr(
'Node'), nodes],
501 [self.tr(
'Topic'), topics],
502 [self.tr(
'Selected Message(s)')]]])
503 menutext.append([self.tr(
'Copy Selected')])
504 menutext.append([self.tr(
'Browse Selected')])
509 for item
in menutext:
511 submenus.append(QMenu(item[0], menu))
512 for subitem
in item[1]:
514 subsubmenus.append(QMenu(subitem[0], submenus[-1]))
515 if isinstance(subitem[1], dict):
516 for key
in sorted(subitem[1].keys()):
517 action = subsubmenus[-1].addAction(subitem[1][key])
520 for subsubitem
in subitem[1]:
521 subsubmenus[-1].addAction(subsubitem)
522 submenus[-1].addMenu(subsubmenus[-1])
524 submenus[-1].addAction(subitem[0])
525 menu.addMenu(submenus[-1])
527 menu.addAction(item[0])
528 action = menu.exec_(event.globalPos())
530 if action
is None or action == 0:
532 elif action.text() == self.tr(
'Browse Selected'):
534 elif action.text() == self.tr(
'Copy Selected'):
536 for current
in self.table_view.selectionModel().selectedIndexes():
537 rowlist.append(self._proxy_model.mapToSource(current).row())
538 copytext = self._model.get_selected_text(rowlist)
539 if copytext
is not None:
540 clipboard = QApplication.clipboard()
541 clipboard.setText(copytext)
542 elif action.text() == self.tr(
'Selected Message(s)'):
543 if action.parentWidget().title() == self.tr(
'Highlight'):
545 elif action.parentWidget().title() == self.tr(
'Exclude'):
548 raise RuntimeError(
"Menu format corruption in ConsoleWidget._rightclick_menu()")
552 roottitle = action.parentWidget().parentWidget().title()
554 raise RuntimeError(
"Menu format corruption in ConsoleWidget._rightclick_menu()")
556 if roottitle == self.tr(
'Highlight'):
558 action.text(), action.parentWidget().title(),
False)
559 elif roottitle == self.tr(
'Exclude'):
561 action.text(), action.parentWidget().title(),
True)
564 "Unknown Root Action %s selected in ConsoleWidget._rightclick_menu()" %
569 Sets the message display label to the current value 571 if self._model.rowCount() == self._proxy_model.rowCount():
572 tip = self.tr(
'Displaying %d messages') % (self._model.rowCount())
574 tip = self.tr(
'Displaying %d of %d messages') % (
575 self._proxy_model.rowCount(), self._model.rowCount())
576 self.messages_label.setText(tip)
584 for current
in self.table_view.selectionModel().selectedIndexes():
585 rowlist.append(self._proxy_model.mapToSource(current).row())
586 browsetext = self._model.get_selected_text(rowlist)
587 if browsetext
is not None:
588 self._browsers.append(TextBrowseDialog(browsetext, self.
_rospack))
592 self._model.remove_rows([])
596 filename = QFileDialog.getOpenFileName(
597 self, self.tr(
'Load from File'),
'.',
598 self.tr(
'rqt_console message file {.csv} (*.csv)'))
599 if filename[0] !=
'':
601 with open(filename[0],
'r') as h: 602 lines = h.read().splitlines() 608 columns = lines[0].split(
';')
615 for line
in lines[1:]:
621 has_prefix =
not last_wrapped
622 has_suffix = last_wrapped
625 has_prefix = line[0] ==
'"' 628 has_suffix = line[-1] ==
'"' 633 if not has_prefix
and not last_wrapped:
636 if last_wrapped
and has_prefix:
646 last_wrapped =
not has_suffix
652 data = row.split(
'";"')
654 msg.set_stamp_format(
'hh:mm:ss.ZZZ (yyyy-MM-dd)')
655 for i, column
in enumerate(columns):
657 if column ==
'message':
658 msg.message = value.replace(
'\\"',
'"')
659 elif column ==
'severity':
660 msg.severity = int(value)
661 if msg.severity
not in Message.SEVERITY_LABELS:
662 skipped.append(
'Unknown severity value: %s' % value)
665 elif column ==
'stamp':
666 parts = value.split(
'.')
668 skipped.append(
'Unknown timestamp format: %s' % value)
671 msg.stamp = (int(parts[0]), int(parts[1]))
672 elif column ==
'topics':
673 msg.topics = value.split(
',')
674 elif column ==
'node':
676 elif column ==
'location':
679 skipped.append(
'Unknown column: %s' % column)
686 'Skipped %d rows since they do not appear to be in ' 687 'rqt_console message file format:\n- %s' %
688 (len(skipped),
'\n- '.join(skipped)))
691 self._model.insert_rows(messages)
698 qWarning(
'File does not appear to be a rqt_console message file: missing file header.')
702 filename = QFileDialog.getSaveFileName(
703 self,
'Save to File',
'.', self.tr(
'rqt_console msg file {.csv} (*.csv)'))
704 if filename[0] !=
'':
705 filename = filename[0]
706 if filename[-4:] !=
'.csv':
709 handle = open(filename,
'w')
714 handle.write(
';'.join(MessageDataModel.columns) +
'\n')
715 for index
in range(self._proxy_model.rowCount()):
716 row = self._proxy_model.mapToSource(self._proxy_model.index(index, 0)).row()
717 msg = self._model._messages[row]
719 data[
'message'] = msg.message.replace(
'"',
'\\"')
720 data[
'severity'] = str(msg.severity)
721 data[
'node'] = msg.node
722 data[
'stamp'] = str(msg.stamp[0]) +
'.' + str(msg.stamp[1]).zfill(9)
723 data[
'topics'] =
','.join(msg.topics)
724 data[
'location'] = msg.location
726 for column
in MessageDataModel.columns:
727 line.append(
'"%s"' % data[column])
728 handle.write(
';'.join(line) +
'\n')
729 except Exception
as e:
730 qWarning(
'File save failed: %s' % str(e))
738 self.pause_button.setVisible(
False)
739 self.record_button.setVisible(
True)
743 self.pause_button.setVisible(
True)
744 self.record_button.setVisible(
False)
747 self.table_view.resizeColumnsToContents()
751 for current
in self.table_view.selectionModel().selectedIndexes():
752 rowlist.append(self._proxy_model.mapToSource(current).row())
753 rowlist = list(set(rowlist))
754 return self._model.remove_rows(rowlist)
758 Handles the delete key. 759 The delete key removes the tableview's selected rows from the datamodel 761 if event.key() == Qt.Key_Delete
and len(self._model._messages) > 0:
762 delete = QMessageBox.Yes
763 if len(self.table_view.selectionModel().selectedIndexes()) == 0:
764 delete = QMessageBox.question(
765 self, self.tr(
'Message'),
766 self.tr(
"Are you sure you want to delete all messages?"),
767 QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
768 if delete == QMessageBox.Yes
and \
769 event.key() == Qt.Key_Delete
and \
770 event.modifiers() == Qt.NoModifier:
773 return old_keyPressEvent(self.table_view, event)
777 old_doubleclickevent=QTableView.mouseDoubleClickEvent):
779 if event.buttons() & Qt.LeftButton
and event.modifiers() == Qt.NoModifier:
782 return old_doubleclickevent(self.table_view, event)
785 if event.buttons() & Qt.RightButton
and event.modifiers() == Qt.NoModifier:
788 return old_pressEvent(self.table_view, event)
791 instance_settings.set_value(
'settings_exist',
True)
793 instance_settings.set_value(
'table_splitter', self.table_splitter.saveState())
794 instance_settings.set_value(
'filter_splitter', self.filter_splitter.saveState())
796 instance_settings.set_value(
'paused', self.
_paused)
797 instance_settings.set_value(
798 'show_highlighted_only', self.highlight_exclude_button.isChecked())
802 exclude_filters.append(item[2])
803 filter_settings = instance_settings.get_settings(
'exclude_filter_' + str(index))
805 instance_settings.set_value(
'exclude_filters', pack(exclude_filters))
807 highlight_filters = []
809 highlight_filters.append(item[2])
810 filter_settings = instance_settings.get_settings(
'highlight_filter_' + str(index))
812 instance_settings.set_value(
'highlight_filters', pack(highlight_filters))
813 instance_settings.set_value(
'message_limit', self._model.get_message_limit())
816 if instance_settings.contains(
'table_splitter'):
817 self.table_splitter.restoreState(instance_settings.value(
'table_splitter'))
818 if instance_settings.contains(
'filter_splitter'):
819 self.filter_splitter.restoreState(instance_settings.value(
'filter_splitter'))
821 self.filter_splitter.setSizes([1, 1])
823 paused = instance_settings.value(
'paused')
in [
True,
'true']
828 self.highlight_exclude_button.setChecked(
829 instance_settings.value(
'show_highlighted_only')
in [
True,
'true'])
830 self._proxy_model.set_show_highlighted_only(self.highlight_exclude_button.isChecked())
833 item[1].delete_button.setChecked(
True)
835 if instance_settings.contains(
'exclude_filters'):
836 exclude_filters = unpack(instance_settings.value(
'exclude_filters'))
837 if exclude_filters
is not None:
838 for index, item
in enumerate(exclude_filters):
840 filter_settings = instance_settings.get_settings(
'exclude_filter_' + str(index))
846 item[1].delete_button.setChecked(
True)
848 if instance_settings.contains(
'highlight_filters'):
849 highlight_filters = unpack(instance_settings.value(
'highlight_filters'))
850 if highlight_filters
is not None:
851 for index, item
in enumerate(highlight_filters):
853 filter_settings = instance_settings.get_settings(
854 'highlight_filter_' + str(index))
859 if instance_settings.contains(
'message_limit'):
860 self._model.set_message_limit(int(instance_settings.value(
'message_limit')))