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__()
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:
111 self.table_view.horizontalHeader().setSortIndicatorShown(logical_index != 0)
112 self.table_view.horizontalHeader().sortIndicatorChanged.connect(update_sort_indicator)
114 self.table_view.horizontalHeader().setContextMenuPolicy(Qt.CustomContextMenu)
117 self.add_exclude_button.setIcon(QIcon.fromTheme(
'list-add'))
118 self.add_highlight_button.setIcon(QIcon.fromTheme(
'list-add'))
119 self.pause_button.setIcon(QIcon.fromTheme(
'media-playback-pause'))
120 if not self.pause_button.icon().isNull():
121 self.pause_button.setText(
'')
122 self.record_button.setIcon(QIcon.fromTheme(
'media-record'))
123 if not self.record_button.icon().isNull():
124 self.record_button.setText(
'')
125 self.load_button.setIcon(QIcon.fromTheme(
'document-open'))
126 if not self.load_button.icon().isNull():
127 self.load_button.setText(
'')
128 self.save_button.setIcon(QIcon.fromTheme(
'document-save'))
129 if not self.save_button.icon().isNull():
130 self.save_button.setText(
'')
131 self.clear_button.setIcon(QIcon.fromTheme(
'edit-clear'))
132 if not self.clear_button.icon().isNull():
133 self.clear_button.setText(
'')
134 self.highlight_exclude_button.setIcon(QIcon.fromTheme(
'format-text-strikethrough'))
147 self.highlight_exclude_button.clicked[bool].connect(
158 'message',
'severity',
'node',
'time',
'topic',
'location',
'custom']
161 self.tr(
'...containing'),
165 self.tr(
'...with severities'),
168 self.
_model.get_severity_dict),
170 self.tr(
'...from node'),
173 self.
_model.get_unique_nodes),
175 self.tr(
'...from time range'),
180 self.tr(
'...from topic'),
183 self.
_model.get_unique_topics),
185 self.tr(
'...from location'),
192 [self.
_model.get_severity_dict,
193 self.
_model.get_unique_nodes,
194 self.
_model.get_unique_topics])}
206 self.table_splitter.setSizes([1, 0])
208 self.table_splitter.setSizes([1, 1])
209 self.exclude_table.resizeColumnsToContents()
210 self.highlight_table.resizeColumnsToContents()
214 :param start_time: number of seconds before now to start, ''int'' (optional) 215 :param end_time: number of seconds before now to end, ''int'' (optional) 216 :returns: summary of message numbers within time 218 current_time = time.mktime(datetime.datetime.now().timetuple())
219 if start_time_offset
is None:
220 start_time = current_time - 240
222 start_time = current_time - start_time_offset
223 if end_time_offset
is not None:
224 end_time = current_time - end_time_offset
228 message_subset = self.
_model.get_message_between(start_time, end_time)
230 class Message_Summary(object):
231 __slots__ =
'fatal',
'error',
'warn',
'info',
'debug' 239 for message
in messages:
240 if message.severity == Message.DEBUG:
242 elif message.severity == Message.INFO:
244 elif message.severity == Message.WARN:
246 elif message.severity == Message.ERROR:
248 elif message.severity == Message.FATAL:
251 assert False,
"Unknown severity type '%s'" % str(message.severity)
253 return Message_Summary(message_subset)
257 :returns: the range of time of messages in the current table selection (min, max), 261 indexes = self.table_view.selectionModel().selectedIndexes()
264 rowlist = [self.
_proxy_model.mapToSource(current).row()
for current
in indexes]
265 rowlist = sorted(list(set(rowlist)))
267 mintime, maxtime = self.
_model.get_time_range(rowlist)
268 return (mintime, maxtime)
273 Deletes any highlight filters which have a checked delete button 276 if item[1].delete_button.isChecked():
278 self.highlight_table.removeCellWidget(index, 0)
279 self.highlight_table.removeRow(index)
280 item[0].filter_changed_signal.disconnect(
287 Deletes any exclude filters which have a checked delete button 290 if item[1].delete_button.isChecked():
292 self.exclude_table.removeCellWidget(index, 0)
293 self.exclude_table.removeRow(index)
294 item[0].filter_changed_signal.disconnect(
301 :param filter_index: if false then this function shows a QMenu to allow the user to choose 302 a type of message filter. ''bool'' 304 :param filter_index: the index of the filter to be added, ''int'' 305 :return: if a filter was added then the index is returned, ''int'' 307 :return: if no filter was added then None is returned, ''NoneType'' 309 if filter_index
is False:
311 filter_select_menu = QMenu()
316 if index
in [
'message',
'location']
or \
320 action = filter_select_menu.exec_(QCursor.pos())
326 if filter_index == -1:
344 newfilter.filter_changed_signal.connect(self.
_proxy_model.handle_highlight_filters_changed)
350 self.highlight_table.insertRow(index)
352 self.highlight_table.resizeColumnsToContents()
353 self.highlight_table.resizeRowsToContents()
354 newfilter.filter_changed_signal.emit()
359 :param filter_index: if false then this function shows a QMenu to allow the user to choose a 360 type of message filter. ''bool'' 362 :param filter_index: the index of the filter to be added, ''int'' 363 :return: if a filter was added then the index is returned, ''int'' 365 :return: if no filter was added then None is returned, ''NoneType'' 367 if filter_index
is False:
369 filter_select_menu = QMenu()
373 if index
in [
'message',
'location']
or \
377 action = filter_select_menu.exec_(QCursor.pos())
383 if filter_index == -1:
401 newfilter.filter_changed_signal.connect(self.
_proxy_model.handle_exclude_filters_changed)
406 self.exclude_table.insertRow(index)
408 self.exclude_table.resizeColumnsToContents()
409 self.exclude_table.resizeRowsToContents()
410 newfilter.filter_changed_signal.emit()
415 Modifies the relevant filters (based on selectiontype) to remove (exclude=True) 416 or highlight (exclude=False) the selection from the dataset in the tableview. 417 :param selection: the actual selection, ''str'' 418 :param selectiontype: the type of selection, ''str'' 419 :param exclude: If True process as an exclude filter, False process as an highlight filter, 425 self.tr(
'Severity'): 1,
426 self.tr(
'Message'): 0}
428 col = types[selectiontype]
431 "Bad Column name in ConsoleWidget._process_highlight_exclude_filter()")
434 unique_messages = set()
435 selected_indexes = self.table_view.selectionModel().selectedIndexes()
436 colcount = len(MessageDataModel.columns) + 1
437 num_selected = len(selected_indexes) // colcount
438 for index
in range(num_selected):
439 unique_messages.add(selected_indexes[(index * colcount) + 1].data())
440 unique_messages = list(unique_messages)
441 for message
in unique_messages:
445 QWidget, QRegExp(
'.*FilterWidget.*'))[0]
449 QWidget, QRegExp(
'.*FilterWidget.*'))[0]
450 filter_widget.set_regex(
False)
451 filter_widget.set_text(message)
461 if type(item[0]) == self.
filter_factory[selectiontype.lower()][1]:
470 if type(item[0]) == self.
filter_factory[selectiontype.lower()][1]:
475 QWidget, QRegExp(
'.*FilterWidget.*'))[0]
476 filter_widget.select_item(selection)
479 QWidget, QRegExp(
'.*FilterWidget.*'))[0]
480 filter_widget.select_item(selection)
484 Dynamically builds the rightclick menu based on the unique column data 485 from the passed in datamodel and then launches it modally 486 :param event: the mouse event object, ''QMouseEvent'' 489 for severity, label
in Message.SEVERITY_LABELS.items():
490 if severity
in self.
_model.get_unique_severities():
491 severities[severity] = label
492 nodes = sorted(self.
_model.get_unique_nodes())
493 topics = sorted(self.
_model.get_unique_topics())
497 menutext.append([self.tr(
'Exclude'), [[self.tr(
'Severity'), severities],
498 [self.tr(
'Node'), nodes],
499 [self.tr(
'Topic'), topics],
500 [self.tr(
'Selected Message(s)')]]])
501 menutext.append([self.tr(
'Highlight'), [[self.tr(
'Severity'), severities],
502 [self.tr(
'Node'), nodes],
503 [self.tr(
'Topic'), topics],
504 [self.tr(
'Selected Message(s)')]]])
505 menutext.append([self.tr(
'Copy Selected')])
506 menutext.append([self.tr(
'Browse Selected')])
511 for item
in menutext:
513 submenus.append(QMenu(item[0], menu))
514 for subitem
in item[1]:
516 subsubmenus.append(QMenu(subitem[0], submenus[-1]))
517 if isinstance(subitem[1], dict):
518 for key
in sorted(subitem[1].keys()):
519 action = subsubmenus[-1].addAction(subitem[1][key])
522 for subsubitem
in subitem[1]:
523 subsubmenus[-1].addAction(subsubitem)
524 submenus[-1].addMenu(subsubmenus[-1])
526 submenus[-1].addAction(subitem[0])
527 menu.addMenu(submenus[-1])
529 menu.addAction(item[0])
530 action = menu.exec_(event.globalPos())
532 if action
is None or action == 0:
534 elif action.text() == self.tr(
'Browse Selected'):
536 elif action.text() == self.tr(
'Copy Selected'):
538 for current
in self.table_view.selectionModel().selectedIndexes():
539 rowlist.append(self.
_proxy_model.mapToSource(current).row())
540 copytext = self.
_model.get_selected_text(rowlist)
541 if copytext
is not None:
542 clipboard = QApplication.clipboard()
543 clipboard.setText(copytext)
544 elif action.text() == self.tr(
'Selected Message(s)'):
545 if action.parentWidget().title() == self.tr(
'Highlight'):
547 elif action.parentWidget().title() == self.tr(
'Exclude'):
550 raise RuntimeError(
"Menu format corruption in ConsoleWidget._rightclick_menu()")
554 roottitle = action.parentWidget().parentWidget().title()
556 raise RuntimeError(
"Menu format corruption in ConsoleWidget._rightclick_menu()")
558 if roottitle == self.tr(
'Highlight'):
560 action.text(), action.parentWidget().title(),
False)
561 elif roottitle == self.tr(
'Exclude'):
563 action.text(), action.parentWidget().title(),
True)
566 "Unknown Root Action %s selected in ConsoleWidget._rightclick_menu()" %
571 Sets the message display label to the current value 574 tip = self.tr(
'Displaying %d messages') % (self.
_model.rowCount())
576 tip = self.tr(
'Displaying %d of %d messages') % (
578 self.messages_label.setText(tip)
586 for current
in self.table_view.selectionModel().selectedIndexes():
587 rowlist.append(self.
_proxy_model.mapToSource(current).row())
588 browsetext = self.
_model.get_selected_text(rowlist)
589 if browsetext
is not None:
594 self.
_model.remove_rows([])
598 filename = QFileDialog.getOpenFileName(
599 self, self.tr(
'Load from File'),
'.',
600 self.tr(
'rqt_console message file {.csv} (*.csv)'))
601 if filename[0] !=
'':
603 with open(filename[0],
'r') as h: 604 lines = h.read().splitlines() 610 columns = lines[0].split(
';')
617 for line
in lines[1:]:
623 has_prefix =
not last_wrapped
624 has_suffix = last_wrapped
627 has_prefix = line[0] ==
'"' 630 has_suffix = line[-1] ==
'"' 635 if not has_prefix
and not last_wrapped:
638 if last_wrapped
and has_prefix:
648 last_wrapped =
not has_suffix
654 data = row.split(
'";"')
656 msg.set_stamp_format(
'hh:mm:ss.ZZZ (yyyy-MM-dd)')
657 for i, column
in enumerate(columns):
659 if column ==
'message':
660 msg.message = value.replace(
'\\"',
'"')
661 elif column ==
'severity':
662 msg.severity = int(value)
663 if msg.severity
not in Message.SEVERITY_LABELS:
664 skipped.append(
'Unknown severity value: %s' % value)
667 elif column ==
'stamp':
668 parts = value.split(
'.')
670 skipped.append(
'Unknown timestamp format: %s' % value)
673 msg.stamp = (int(parts[0]), int(parts[1]))
674 elif column ==
'topics':
675 msg.topics = value.split(
',')
676 elif column ==
'node':
678 elif column ==
'location':
681 skipped.append(
'Unknown column: %s' % column)
688 'Skipped %d rows since they do not appear to be in ' 689 'rqt_console message file format:\n- %s' %
690 (len(skipped),
'\n- '.join(skipped)))
693 self.
_model.insert_rows(messages)
700 qWarning(
'File does not appear to be a rqt_console message file: missing file header.')
704 filename = QFileDialog.getSaveFileName(
705 self,
'Save to File',
'.', self.tr(
'rqt_console msg file {.csv} (*.csv)'))
706 if filename[0] !=
'':
707 filename = filename[0]
708 if filename[-4:] !=
'.csv':
711 handle = open(filename,
'w')
716 handle.write(
';'.join(MessageDataModel.columns) +
'\n')
719 msg = self.
_model._messages[row]
721 data[
'message'] = msg.message.replace(
'"',
'\\"')
722 data[
'severity'] = str(msg.severity)
723 data[
'node'] = msg.node
724 data[
'stamp'] = str(msg.stamp[0]) +
'.' + str(msg.stamp[1]).zfill(9)
725 data[
'topics'] =
','.join(msg.topics)
726 data[
'location'] = msg.location
728 for column
in MessageDataModel.columns:
729 line.append(
'"%s"' % data[column])
730 handle.write(
';'.join(line) +
'\n')
731 except Exception
as e:
732 qWarning(
'File save failed: %s' % str(e))
740 self.pause_button.setVisible(
False)
741 self.record_button.setVisible(
True)
745 self.pause_button.setVisible(
True)
746 self.record_button.setVisible(
False)
749 self.table_view.resizeColumnsToContents()
753 hide = menu.addAction(
'Hide Column')
754 showall = menu.addAction(
'Show all columns')
757 if self.table_view.horizontalHeader().count() - self.table_view.horizontalHeader().hiddenSectionCount() == 1:
758 hide.setEnabled(
False)
760 ac = menu.exec_(self.table_view.horizontalHeader().mapToGlobal(pos))
762 column = self.table_view.horizontalHeader().logicalIndexAt(pos.x())
763 self.table_view.horizontalHeader().hideSection(column)
765 for i
in range(self.table_view.horizontalHeader().count()):
766 self.table_view.horizontalHeader().showSection(i)
770 for current
in self.table_view.selectionModel().selectedIndexes():
771 rowlist.append(self.
_proxy_model.mapToSource(current).row())
772 rowlist = list(set(rowlist))
773 return self.
_model.remove_rows(rowlist)
777 Handles the delete key. 778 The delete key removes the tableview's selected rows from the datamodel 780 if event.key() == Qt.Key_Delete
and len(self.
_model._messages) > 0:
781 delete = QMessageBox.Yes
782 if len(self.table_view.selectionModel().selectedIndexes()) == 0:
783 delete = QMessageBox.question(
784 self, self.tr(
'Message'),
785 self.tr(
"Are you sure you want to delete all messages?"),
786 QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
787 if delete == QMessageBox.Yes
and \
788 event.key() == Qt.Key_Delete
and \
789 event.modifiers() == Qt.NoModifier:
792 return old_keyPressEvent(self.table_view, event)
796 old_doubleclickevent=QTableView.mouseDoubleClickEvent):
798 if event.buttons() & Qt.LeftButton
and event.modifiers() == Qt.NoModifier:
801 return old_doubleclickevent(self.table_view, event)
804 if event.buttons() & Qt.RightButton
and event.modifiers() == Qt.NoModifier:
807 return old_pressEvent(self.table_view, event)
810 instance_settings.set_value(
'settings_exist',
True)
812 instance_settings.set_value(
'table_splitter', self.table_splitter.saveState())
813 instance_settings.set_value(
'filter_splitter', self.filter_splitter.saveState())
815 instance_settings.set_value(
'paused', self.
_paused)
816 instance_settings.set_value(
817 'show_highlighted_only', self.highlight_exclude_button.isChecked())
821 exclude_filters.append(item[2])
822 filter_settings = instance_settings.get_settings(
'exclude_filter_' + str(index))
824 instance_settings.set_value(
'exclude_filters', pack(exclude_filters))
826 highlight_filters = []
828 highlight_filters.append(item[2])
829 filter_settings = instance_settings.get_settings(
'highlight_filter_' + str(index))
831 instance_settings.set_value(
'highlight_filters', pack(highlight_filters))
832 instance_settings.set_value(
'message_limit', self.
_model.get_message_limit())
835 if instance_settings.contains(
'table_splitter'):
836 self.table_splitter.restoreState(instance_settings.value(
'table_splitter'))
837 if instance_settings.contains(
'filter_splitter'):
838 self.filter_splitter.restoreState(instance_settings.value(
'filter_splitter'))
840 self.filter_splitter.setSizes([1, 1])
842 paused = instance_settings.value(
'paused')
in [
True,
'true']
847 self.highlight_exclude_button.setChecked(
848 instance_settings.value(
'show_highlighted_only')
in [
True,
'true'])
849 self.
_proxy_model.set_show_highlighted_only(self.highlight_exclude_button.isChecked())
852 item[1].delete_button.setChecked(
True)
854 if instance_settings.contains(
'exclude_filters'):
855 exclude_filters = unpack(instance_settings.value(
'exclude_filters'))
856 if exclude_filters
is not None:
857 for index, item
in enumerate(exclude_filters):
859 filter_settings = instance_settings.get_settings(
'exclude_filter_' + str(index))
865 item[1].delete_button.setChecked(
True)
867 if instance_settings.contains(
'highlight_filters'):
868 highlight_filters = unpack(instance_settings.value(
'highlight_filters'))
869 if highlight_filters
is not None:
870 for index, item
in enumerate(highlight_filters):
872 filter_settings = instance_settings.get_settings(
873 'highlight_filter_' + str(index))
878 if instance_settings.contains(
'message_limit'):
879 self.
_model.set_message_limit(int(instance_settings.value(
'message_limit')))