console_widget.py
Go to the documentation of this file.
1 # Software License Agreement (BSD License)
2 #
3 # Copyright (c) 2012, Willow Garage, Inc.
4 # All rights reserved.
5 #
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions
8 # are met:
9 #
10 # * Redistributions of source code must retain the above copyright
11 # notice, this list of conditions and the following disclaimer.
12 # * Redistributions in binary form must reproduce the above
13 # copyright notice, this list of conditions and the following
14 # disclaimer in the documentation and/or other materials provided
15 # with the distribution.
16 # * Neither the name of Willow Garage, Inc. nor the names of its
17 # contributors may be used to endorse or promote products derived
18 # from this software without specific prior written permission.
19 #
20 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
23 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
24 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
25 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
26 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 # POSSIBILITY OF SUCH DAMAGE.
32 
33 import os
34 
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
40 
41 import time
42 import datetime
43 
44 from rqt_py_common.ini_helper import pack, unpack
45 
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
53 
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
59 
60 from .message import Message
61 from .message_data_model import MessageDataModel
62 
63 from .text_browse_dialog import TextBrowseDialog
64 
65 
66 class ConsoleWidget(QWidget):
67 
68  """
69  Primary widget for the rqt_console plugin.
70  """
71 
72  def __init__(self, proxy_model, rospack, minimal=False):
73  """
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''
77  """
78  super(ConsoleWidget, self).__init__()
79  self._proxy_model = proxy_model
80  self._model = self._proxy_model.sourceModel()
81  self._paused = False
82  self._rospack = rospack
83 
84  # These are lists of Tuples = (,)
85  self._exclude_filters = []
87 
88  ui_file = os.path.join(
89  self._rospack.get_path('rqt_console'), 'resource', 'console_widget.ui')
90  loadUi(ui_file, self)
91 
92  if minimal:
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)
98 
99  self._columnwidth = (60, 100, 70, 100, 100, 100, 100)
100  for idx, width in enumerate(self._columnwidth):
101  self.table_view.horizontalHeader().resizeSection(idx, width)
102  try:
103  setSectionResizeMode = self.table_view.horizontalHeader().setSectionResizeMode # Qt5
104  except AttributeError:
105  setSectionResizeMode = self.table_view.horizontalHeader().setResizeMode # Qt4
106  setSectionResizeMode(1, QHeaderView.Stretch)
107 
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)
113 
114  self.table_view.horizontalHeader().setContextMenuPolicy(Qt.CustomContextMenu)
115  self.table_view.horizontalHeader().customContextMenuRequested.connect(self._handle_column_right_click)
116 
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'))
135 
136  self.pause_button.clicked[bool].connect(self._handle_pause_clicked)
137  self.record_button.clicked[bool].connect(self._handle_record_clicked)
138  self.load_button.clicked[bool].connect(self._handle_load_clicked)
139  self.save_button.clicked[bool].connect(self._handle_save_clicked)
140  self.column_resize_button.clicked[bool].connect(self._handle_column_resize_clicked)
141  self.clear_button.clicked[bool].connect(self._handle_clear_button_clicked)
142 
143  self.table_view.mouseDoubleClickEvent = self._handle_mouse_double_click
144  self.table_view.mousePressEvent = self._handle_mouse_press
145  self.table_view.keyPressEvent = self._handle_custom_keypress
146 
147  self.highlight_exclude_button.clicked[bool].connect(
148  self._proxy_model.set_show_highlighted_only)
149 
150  self.add_highlight_button.clicked.connect(self._add_highlight_filter)
151  self.add_exclude_button.clicked.connect(self._add_exclude_filter)
152 
153  # Filter factory dictionary:
154  # index 0 is a label describing the widget, index 1 is the class that
155  # provides filtering logic index 2 is the widget that sets the data in the
156  # filter class, index 3 are the arguments for the widget class constructor
158  'message', 'severity', 'node', 'time', 'topic', 'location', 'custom']
159  self.filter_factory = {
160  'message': (
161  self.tr('...containing'),
162  MessageFilter,
163  TextFilterWidget),
164  'severity': (
165  self.tr('...with severities'),
166  SeverityFilter,
167  ListFilterWidget,
168  self._model.get_severity_dict),
169  'node': (
170  self.tr('...from node'),
171  NodeFilter,
172  ListFilterWidget,
173  self._model.get_unique_nodes),
174  'time': (
175  self.tr('...from time range'),
176  TimeFilter,
177  TimeFilterWidget,
179  'topic': (
180  self.tr('...from topic'),
181  TopicFilter,
182  ListFilterWidget,
183  self._model.get_unique_topics),
184  'location': (
185  self.tr('...from location'),
186  LocationFilter,
187  TextFilterWidget),
188  'custom': (
189  self.tr('Custom'),
190  CustomFilter,
191  CustomFilterWidget,
192  [self._model.get_severity_dict,
193  self._model.get_unique_nodes,
194  self._model.get_unique_topics])}
195 
196  self._model.rowsInserted.connect(self.update_status)
197  self._model.rowsRemoved.connect(self.update_status)
198  self._proxy_model.rowsInserted.connect(self.update_status)
199  self._proxy_model.rowsRemoved.connect(self.update_status)
200 
201  # list of TextBrowserDialogs to close when cleaning up
202  self._browsers = []
203 
204  # This defaults the filters panel to start by taking 50% of the available space
205  if minimal:
206  self.table_splitter.setSizes([1, 0])
207  else:
208  self.table_splitter.setSizes([1, 1])
209  self.exclude_table.resizeColumnsToContents()
210  self.highlight_table.resizeColumnsToContents()
211 
212  def get_message_summary(self, start_time_offset=None, end_time_offset=None):
213  """
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
217  """
218  current_time = time.mktime(datetime.datetime.now().timetuple())
219  if start_time_offset is None:
220  start_time = current_time - 240
221  else:
222  start_time = current_time - start_time_offset
223  if end_time_offset is not None:
224  end_time = current_time - end_time_offset
225  else:
226  end_time = None
227 
228  message_subset = self._model.get_message_between(start_time, end_time)
229 
230  class Message_Summary(object):
231  __slots__ = 'fatal', 'error', 'warn', 'info', 'debug'
232 
233  def __init__(self, messages):
234  self.fatal = 0
235  self.error = 0
236  self.warn = 0
237  self.info = 0
238  self.debug = 0
239  for message in messages:
240  if message.severity == Message.DEBUG:
241  self.debug += 1
242  elif message.severity == Message.INFO:
243  self.info += 1
244  elif message.severity == Message.WARN:
245  self.warn += 1
246  elif message.severity == Message.ERROR:
247  self.error += 1
248  elif message.severity == Message.FATAL:
249  self.fatal += 1
250  else:
251  assert False, "Unknown severity type '%s'" % str(message.severity)
252 
253  return Message_Summary(message_subset)
254 
256  """
257  :returns: the range of time of messages in the current table selection (min, max),
258  ''tuple(str,str)''
259  """
260  rowlist = []
261  indexes = self.table_view.selectionModel().selectedIndexes()
262 
263  if indexes:
264  rowlist = [self._proxy_model.mapToSource(current).row() for current in indexes]
265  rowlist = sorted(list(set(rowlist)))
266 
267  mintime, maxtime = self._model.get_time_range(rowlist)
268  return (mintime, maxtime)
269  return (-1, -1)
270 
272  """
273  Deletes any highlight filters which have a checked delete button
274  """
275  for index, item in enumerate(self._highlight_filters):
276  if item[1].delete_button.isChecked():
277  self._proxy_model.delete_highlight_filter(index)
278  self.highlight_table.removeCellWidget(index, 0)
279  self.highlight_table.removeRow(index)
280  item[0].filter_changed_signal.disconnect(
281  self._proxy_model.handle_highlight_filters_changed)
282  item[1].delete_button.clicked.disconnect(self._delete_highlight_filter)
283  del self._highlight_filters[index]
284 
286  """
287  Deletes any exclude filters which have a checked delete button
288  """
289  for index, item in enumerate(self._exclude_filters):
290  if item[1].delete_button.isChecked():
291  self._proxy_model.delete_exclude_filter(index)
292  self.exclude_table.removeCellWidget(index, 0)
293  self.exclude_table.removeRow(index)
294  item[0].filter_changed_signal.disconnect(
295  self._proxy_model.handle_exclude_filters_changed)
296  item[1].delete_button.clicked.disconnect(self._delete_exclude_filter)
297  del self._exclude_filters[index]
298 
299  def _add_highlight_filter(self, filter_index=False):
300  """
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''
303  OR
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''
306  OR
307  :return: if no filter was added then None is returned, ''NoneType''
308  """
309  if filter_index is False:
310  filter_index = -1
311  filter_select_menu = QMenu()
312  for index in self._filter_factory_order:
313  # flattens the _highlight filters list and only adds the item if it
314  # doesn't already exist
315 
316  if index in ['message', 'location'] or \
317  not self.filter_factory[index][1] in \
318  [type(item) for sublist in self._highlight_filters for item in sublist]:
319  filter_select_menu.addAction(self.filter_factory[index][0])
320  action = filter_select_menu.exec_(QCursor.pos())
321  if action is None:
322  return
323  for index in self._filter_factory_order:
324  if self.filter_factory[index][0] == action.text():
325  filter_index = index
326  if filter_index == -1:
327  return
328 
329  index = len(self._highlight_filters)
330  newfilter = self.filter_factory[filter_index][1]()
331  if len(self.filter_factory[filter_index]) >= 4:
332  newwidget = self.filter_factory[filter_index][2](
333  newfilter, self._rospack, self.filter_factory[filter_index][3])
334  else:
335  newwidget = self.filter_factory[filter_index][2](newfilter, self._rospack)
336 
337  # pack the new filter tuple onto the filter list
338  self._highlight_filters.append((
339  newfilter,
340  FilterWrapperWidget(
341  newwidget, self.filter_factory[filter_index][0]),
342  filter_index))
343  self._proxy_model.add_highlight_filter(newfilter)
344  newfilter.filter_changed_signal.connect(self._proxy_model.handle_highlight_filters_changed)
345  self._highlight_filters[index][1].delete_button.clicked.connect(
347  self._model.rowsInserted.connect(self._highlight_filters[index][1].repopulate)
348 
349  # place the widget in the proper location
350  self.highlight_table.insertRow(index)
351  self.highlight_table.setCellWidget(index, 0, self._highlight_filters[index][1])
352  self.highlight_table.resizeColumnsToContents()
353  self.highlight_table.resizeRowsToContents()
354  newfilter.filter_changed_signal.emit()
355  return index
356 
357  def _add_exclude_filter(self, filter_index=False):
358  """
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''
361  OR
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''
364  OR
365  :return: if no filter was added then None is returned, ''NoneType''
366  """
367  if filter_index is False:
368  filter_index = -1
369  filter_select_menu = QMenu()
370  for index in self._filter_factory_order:
371  # flattens the _exclude filters list and only adds the item if it doesn't
372  # already exist
373  if index in ['message', 'location'] or \
374  not self.filter_factory[index][1] in \
375  [type(item) for sublist in self._exclude_filters for item in sublist]:
376  filter_select_menu.addAction(self.filter_factory[index][0])
377  action = filter_select_menu.exec_(QCursor.pos())
378  if action is None:
379  return None
380  for index in self._filter_factory_order:
381  if self.filter_factory[index][0] == action.text():
382  filter_index = index
383  if filter_index == -1:
384  return None
385 
386  index = len(self._exclude_filters)
387  newfilter = self.filter_factory[filter_index][1]()
388  if len(self.filter_factory[filter_index]) >= 4:
389  newwidget = self.filter_factory[filter_index][2](
390  newfilter, self._rospack, self.filter_factory[filter_index][3])
391  else:
392  newwidget = self.filter_factory[filter_index][2](newfilter, self._rospack)
393 
394  # pack the new filter tuple onto the filter list
395  self._exclude_filters.append((
396  newfilter,
397  FilterWrapperWidget(
398  newwidget, self.filter_factory[filter_index][0]),
399  filter_index))
400  self._proxy_model.add_exclude_filter(newfilter)
401  newfilter.filter_changed_signal.connect(self._proxy_model.handle_exclude_filters_changed)
402  self._exclude_filters[index][1].delete_button.clicked.connect(self._delete_exclude_filter)
403  self._model.rowsInserted.connect(self._exclude_filters[index][1].repopulate)
404 
405  # place the widget in the proper location
406  self.exclude_table.insertRow(index)
407  self.exclude_table.setCellWidget(index, 0, self._exclude_filters[index][1])
408  self.exclude_table.resizeColumnsToContents()
409  self.exclude_table.resizeRowsToContents()
410  newfilter.filter_changed_signal.emit()
411  return index
412 
413  def _process_highlight_exclude_filter(self, selection, selectiontype, exclude=False):
414  """
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,
420  ''bool''
421  """
422  types = {
423  self.tr('Node'): 2,
424  self.tr('Topic'): 4,
425  self.tr('Severity'): 1,
426  self.tr('Message'): 0}
427  try:
428  col = types[selectiontype]
429  except:
430  raise RuntimeError(
431  "Bad Column name in ConsoleWidget._process_highlight_exclude_filter()")
432 
433  if col == 0:
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:
442  if exclude:
443  filter_index = self._add_exclude_filter(selectiontype.lower())
444  filter_widget = self._exclude_filters[filter_index][1].findChildren(
445  QWidget, QRegExp('.*FilterWidget.*'))[0]
446  else:
447  filter_index = self._add_highlight_filter(selectiontype.lower())
448  filter_widget = self._highlight_filters[filter_index][1].findChildren(
449  QWidget, QRegExp('.*FilterWidget.*'))[0]
450  filter_widget.set_regex(False)
451  filter_widget.set_text(message)
452 
453  else:
454  if exclude:
455  # Test if the filter we are adding already exists if it does use the existing filter
456  if self.filter_factory[selectiontype.lower()][1] not in \
457  [type(item) for sublist in self._exclude_filters for item in sublist]:
458  filter_index = self._add_exclude_filter(selectiontype.lower())
459  else:
460  for index, item in enumerate(self._exclude_filters):
461  if type(item[0]) == self.filter_factory[selectiontype.lower()][1]:
462  filter_index = index
463  else:
464  # Test if the filter we are adding already exists if it does use the existing filter
465  if self.filter_factory[selectiontype.lower()][1] not in \
466  [type(item) for sublist in self._highlight_filters for item in sublist]:
467  filter_index = self._add_highlight_filter(col)
468  else:
469  for index, item in enumerate(self._highlight_filters):
470  if type(item[0]) == self.filter_factory[selectiontype.lower()][1]:
471  filter_index = index
472 
473  if exclude:
474  filter_widget = self._exclude_filters[filter_index][1].findChildren(
475  QWidget, QRegExp('.*FilterWidget.*'))[0]
476  filter_widget.select_item(selection)
477  else:
478  filter_widget = self._highlight_filters[filter_index][1].findChildren(
479  QWidget, QRegExp('.*FilterWidget.*'))[0]
480  filter_widget.select_item(selection)
481 
482  def _rightclick_menu(self, event):
483  """
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''
487  """
488  severities = {}
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())
494 
495  # menutext entries turned into
496  menutext = []
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')])
507 
508  menu = QMenu()
509  submenus = []
510  subsubmenus = []
511  for item in menutext:
512  if len(item) > 1:
513  submenus.append(QMenu(item[0], menu))
514  for subitem in item[1]:
515  if len(subitem) > 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])
520  action.setData(key)
521  else:
522  for subsubitem in subitem[1]:
523  subsubmenus[-1].addAction(subsubitem)
524  submenus[-1].addMenu(subsubmenus[-1])
525  else:
526  submenus[-1].addAction(subitem[0])
527  menu.addMenu(submenus[-1])
528  else:
529  menu.addAction(item[0])
530  action = menu.exec_(event.globalPos())
531 
532  if action is None or action == 0:
533  return
534  elif action.text() == self.tr('Browse Selected'):
535  self._show_browsers()
536  elif action.text() == self.tr('Copy Selected'):
537  rowlist = []
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'):
546  self._process_highlight_exclude_filter(action.text(), 'Message', False)
547  elif action.parentWidget().title() == self.tr('Exclude'):
548  self._process_highlight_exclude_filter(action.text(), 'Message', True)
549  else:
550  raise RuntimeError("Menu format corruption in ConsoleWidget._rightclick_menu()")
551  else:
552  # This processes the dynamic list entries (severity, node and topic)
553  try:
554  roottitle = action.parentWidget().parentWidget().title()
555  except:
556  raise RuntimeError("Menu format corruption in ConsoleWidget._rightclick_menu()")
557 
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)
564  else:
565  raise RuntimeError(
566  "Unknown Root Action %s selected in ConsoleWidget._rightclick_menu()" %
567  roottitle)
568 
569  def update_status(self):
570  """
571  Sets the message display label to the current value
572  """
573  if self._model.rowCount() == self._proxy_model.rowCount():
574  tip = self.tr('Displaying %d messages') % (self._model.rowCount())
575  else:
576  tip = self.tr('Displaying %d of %d messages') % (
577  self._proxy_model.rowCount(), self._model.rowCount())
578  self.messages_label.setText(tip)
579 
581  for browser in self._browsers:
582  browser.close()
583 
584  def _show_browsers(self):
585  rowlist = []
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:
590  self._browsers.append(TextBrowseDialog(browsetext, self._rospack))
591  self._browsers[-1].show()
592 
593  def _handle_clear_button_clicked(self, checked):
594  self._model.remove_rows([])
595  Message._next_id = 1
596 
597  def _handle_load_clicked(self, checked):
598  filename = QFileDialog.getOpenFileName(
599  self, self.tr('Load from File'), '.',
600  self.tr('rqt_console message file {.csv} (*.csv)'))
601  if filename[0] != '':
602  try:
603  with open(filename[0], 'r') as h:
604  lines = h.read().splitlines()
605  except IOError as e:
606  qWarning(str(e))
607  return False
608 
609  # extract column header
610  columns = lines[0].split(';')
611  if len(lines) < 2:
612  return True
613 
614  # join wrapped lines
615  rows = []
616  last_wrapped = False
617  for line in lines[1:]:
618  # ignore empty lines
619  if not line:
620  continue
621  # check for quotes and remove them
622  if line == '"':
623  has_prefix = not last_wrapped
624  has_suffix = last_wrapped
625  line = ''
626  else:
627  has_prefix = line[0] == '"'
628  if has_prefix:
629  line = line[1:]
630  has_suffix = line[-1] == '"'
631  if has_suffix:
632  line = line[:-1]
633 
634  # ignore line without prefix if previous line was not wrapped
635  if not has_prefix and not last_wrapped:
636  continue
637  # remove wrapped line which is not continued on the next line
638  if last_wrapped and has_prefix:
639  rows.pop()
640 
641  # add/append lines
642  if last_wrapped:
643  rows[-1] += line
644  else:
645  # add line without quote prefix
646  rows.append(line)
647 
648  last_wrapped = not has_suffix
649 
650  # generate message for each row
651  messages = []
652  skipped = []
653  for row in rows:
654  data = row.split('";"')
655  msg = Message()
656  msg.set_stamp_format('hh:mm:ss.ZZZ (yyyy-MM-dd)')
657  for i, column in enumerate(columns):
658  value = data[i]
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)
665  msg = None
666  break
667  elif column == 'stamp':
668  parts = value.split('.')
669  if len(parts) != 2:
670  skipped.append('Unknown timestamp format: %s' % value)
671  msg = None
672  break
673  msg.stamp = (int(parts[0]), int(parts[1]))
674  elif column == 'topics':
675  msg.topics = value.split(',')
676  elif column == 'node':
677  msg.node = value
678  elif column == 'location':
679  msg.location = value
680  else:
681  skipped.append('Unknown column: %s' % column)
682  msg = None
683  break
684  if msg:
685  messages.append(msg)
686  if skipped:
687  qWarning(
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)))
691 
692  if messages:
693  self._model.insert_rows(messages)
694 
695  self._handle_pause_clicked()
696 
697  return True
698 
699  else:
700  qWarning('File does not appear to be a rqt_console message file: missing file header.')
701  return False
702 
703  def _handle_save_clicked(self, checked):
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':
709  filename += '.csv'
710  try:
711  handle = open(filename, 'w')
712  except IOError as e:
713  qWarning(str(e))
714  return
715  try:
716  handle.write(';'.join(MessageDataModel.columns) + '\n')
717  for index in range(self._proxy_model.rowCount()):
718  row = self._proxy_model.mapToSource(self._proxy_model.index(index, 0)).row()
719  msg = self._model._messages[row]
720  data = {}
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
727  line = []
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))
733  return False
734  finally:
735  handle.close()
736  return True
737 
739  self._paused = True
740  self.pause_button.setVisible(False)
741  self.record_button.setVisible(True)
742 
744  self._paused = False
745  self.pause_button.setVisible(True)
746  self.record_button.setVisible(False)
747 
749  self.table_view.resizeColumnsToContents()
750 
752  menu = QMenu(self)
753  hide = menu.addAction('Hide Column')
754  showall = menu.addAction('Show all columns')
755 
756  # Don't allow hiding the last column
757  if self.table_view.horizontalHeader().count() - self.table_view.horizontalHeader().hiddenSectionCount() == 1:
758  hide.setEnabled(False)
759 
760  ac = menu.exec_(self.table_view.horizontalHeader().mapToGlobal(pos))
761  if ac == hide:
762  column = self.table_view.horizontalHeader().logicalIndexAt(pos.x())
763  self.table_view.horizontalHeader().hideSection(column)
764  elif ac == showall:
765  for i in range(self.table_view.horizontalHeader().count()):
766  self.table_view.horizontalHeader().showSection(i)
767 
769  rowlist = []
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)
774 
775  def _handle_custom_keypress(self, event, old_keyPressEvent=QTableView.keyPressEvent):
776  """
777  Handles the delete key.
778  The delete key removes the tableview's selected rows from the datamodel
779  """
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:
790  if self._delete_selected_rows():
791  event.accept()
792  return old_keyPressEvent(self.table_view, event)
793 
795  self, event,
796  old_doubleclickevent=QTableView.mouseDoubleClickEvent):
797 
798  if event.buttons() & Qt.LeftButton and event.modifiers() == Qt.NoModifier:
799  self._show_browsers()
800  event.accept()
801  return old_doubleclickevent(self.table_view, event)
802 
803  def _handle_mouse_press(self, event, old_pressEvent=QTableView.mousePressEvent):
804  if event.buttons() & Qt.RightButton and event.modifiers() == Qt.NoModifier:
805  self._rightclick_menu(event)
806  event.accept()
807  return old_pressEvent(self.table_view, event)
808 
809  def save_settings(self, plugin_settings, instance_settings):
810  instance_settings.set_value('settings_exist', True)
811 
812  instance_settings.set_value('table_splitter', self.table_splitter.saveState())
813  instance_settings.set_value('filter_splitter', self.filter_splitter.saveState())
814 
815  instance_settings.set_value('paused', self._paused)
816  instance_settings.set_value(
817  'show_highlighted_only', self.highlight_exclude_button.isChecked())
818 
819  exclude_filters = []
820  for index, item in enumerate(self._exclude_filters):
821  exclude_filters.append(item[2])
822  filter_settings = instance_settings.get_settings('exclude_filter_' + str(index))
823  item[1].save_settings(filter_settings)
824  instance_settings.set_value('exclude_filters', pack(exclude_filters))
825 
826  highlight_filters = []
827  for index, item in enumerate(self._highlight_filters):
828  highlight_filters.append(item[2])
829  filter_settings = instance_settings.get_settings('highlight_filter_' + str(index))
830  item[1].save_settings(filter_settings)
831  instance_settings.set_value('highlight_filters', pack(highlight_filters))
832  instance_settings.set_value('message_limit', self._model.get_message_limit())
833 
834  def restore_settings(self, pluggin_settings, instance_settings):
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'))
839  else:
840  self.filter_splitter.setSizes([1, 1])
841 
842  paused = instance_settings.value('paused') in [True, 'true']
843  if paused:
844  self._handle_pause_clicked()
845  else:
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())
850 
851  for item in self._exclude_filters:
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):
858  self._add_exclude_filter(item)
859  filter_settings = instance_settings.get_settings('exclude_filter_' + str(index))
860  self._exclude_filters[-1][1].restore_settings(filter_settings)
861  else:
862  self._add_exclude_filter('severity')
863 
864  for item in self._highlight_filters:
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):
871  self._add_highlight_filter(item)
872  filter_settings = instance_settings.get_settings(
873  'highlight_filter_' + str(index))
874  self._highlight_filters[-1][1].restore_settings(filter_settings)
875  else:
876  self._add_highlight_filter('message')
877 
878  if instance_settings.contains('message_limit'):
879  self._model.set_message_limit(int(instance_settings.value('message_limit')))
def save_settings(self, plugin_settings, instance_settings)
def _handle_mouse_double_click(self, event, old_doubleclickevent=QTableView.mouseDoubleClickEvent)
def get_message_summary(self, start_time_offset=None, end_time_offset=None)
def restore_settings(self, pluggin_settings, instance_settings)
def _handle_custom_keypress(self, event, old_keyPressEvent=QTableView.keyPressEvent)
def _add_exclude_filter(self, filter_index=False)
def _handle_mouse_press(self, event, old_pressEvent=QTableView.mousePressEvent)
def _process_highlight_exclude_filter(self, selection, selectiontype, exclude=False)
def _add_highlight_filter(self, filter_index=False)
def __init__(self, proxy_model, rospack, minimal=False)


rqt_console
Author(s): Aaron Blasdel
autogenerated on Sun May 24 2020 03:23:49