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.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'))
132 
133  self.pause_button.clicked[bool].connect(self._handle_pause_clicked)
134  self.record_button.clicked[bool].connect(self._handle_record_clicked)
135  self.load_button.clicked[bool].connect(self._handle_load_clicked)
136  self.save_button.clicked[bool].connect(self._handle_save_clicked)
137  self.column_resize_button.clicked[bool].connect(self._handle_column_resize_clicked)
138  self.clear_button.clicked[bool].connect(self._handle_clear_button_clicked)
139 
140  self.table_view.mouseDoubleClickEvent = self._handle_mouse_double_click
141  self.table_view.mousePressEvent = self._handle_mouse_press
142  self.table_view.keyPressEvent = self._handle_custom_keypress
143 
144  self.highlight_exclude_button.clicked[bool].connect(
145  self._proxy_model.set_show_highlighted_only)
146 
147  self.add_highlight_button.clicked.connect(self._add_highlight_filter)
148  self.add_exclude_button.clicked.connect(self._add_exclude_filter)
149 
150  # Filter factory dictionary:
151  # index 0 is a label describing the widget, index 1 is the class that
152  # provides filtering logic index 2 is the widget that sets the data in the
153  # filter class, index 3 are the arguments for the widget class constructor
155  'message', 'severity', 'node', 'time', 'topic', 'location', 'custom']
156  self.filter_factory = {
157  'message': (
158  self.tr('...containing'),
159  MessageFilter,
160  TextFilterWidget),
161  'severity': (
162  self.tr('...with severities'),
163  SeverityFilter,
164  ListFilterWidget,
165  self._model.get_severity_dict),
166  'node': (
167  self.tr('...from node'),
168  NodeFilter,
169  ListFilterWidget,
170  self._model.get_unique_nodes),
171  'time': (
172  self.tr('...from time range'),
173  TimeFilter,
174  TimeFilterWidget,
176  'topic': (
177  self.tr('...from topic'),
178  TopicFilter,
179  ListFilterWidget,
180  self._model.get_unique_topics),
181  'location': (
182  self.tr('...from location'),
183  LocationFilter,
184  TextFilterWidget),
185  'custom': (
186  self.tr('Custom'),
187  CustomFilter,
188  CustomFilterWidget,
189  [self._model.get_severity_dict,
190  self._model.get_unique_nodes,
191  self._model.get_unique_topics])}
192 
193  self._model.rowsInserted.connect(self.update_status)
194  self._model.rowsRemoved.connect(self.update_status)
195  self._proxy_model.rowsInserted.connect(self.update_status)
196  self._proxy_model.rowsRemoved.connect(self.update_status)
197 
198  # list of TextBrowserDialogs to close when cleaning up
199  self._browsers = []
200 
201  # This defaults the filters panel to start by taking 50% of the available space
202  if minimal:
203  self.table_splitter.setSizes([1, 0])
204  else:
205  self.table_splitter.setSizes([1, 1])
206  self.exclude_table.resizeColumnsToContents()
207  self.highlight_table.resizeColumnsToContents()
208 
209  def get_message_summary(self, start_time_offset=None, end_time_offset=None):
210  """
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
214  """
215  current_time = time.mktime(datetime.datetime.now().timetuple())
216  if start_time_offset is None:
217  start_time = current_time - 240
218  else:
219  start_time = current_time - start_time_offset
220  if end_time_offset is not None:
221  end_time = current_time - end_time_offset
222  else:
223  end_time = None
224 
225  message_subset = self._model.get_message_between(start_time, end_time)
226 
227  class Message_Summary(object):
228  __slots__ = 'fatal', 'error', 'warn', 'info', 'debug'
229 
230  def __init__(self, messages):
231  self.fatal = 0
232  self.error = 0
233  self.warn = 0
234  self.info = 0
235  self.debug = 0
236  for message in messages:
237  if message.severity == Message.DEBUG:
238  self.debug += 1
239  elif message.severity == Message.INFO:
240  self.info += 1
241  elif message.severity == Message.WARN:
242  self.warn += 1
243  elif message.severity == Message.ERROR:
244  self.error += 1
245  elif message.severity == Message.FATAL:
246  self.fatal += 1
247  else:
248  assert False, "Unknown severity type '%s'" % str(message.severity)
249 
250  return Message_Summary(message_subset)
251 
253  """
254  :returns: the range of time of messages in the current table selection (min, max),
255  ''tuple(str,str)''
256  """
257  rowlist = []
258  indexes = self.table_view.selectionModel().selectedIndexes()
259 
260  if indexes:
261  rowlist = [self._proxy_model.mapToSource(current).row() for current in indexes]
262  rowlist = sorted(list(set(rowlist)))
263 
264  mintime, maxtime = self._model.get_time_range(rowlist)
265  return (mintime, maxtime)
266  return (-1, -1)
267 
269  """
270  Deletes any highlight filters which have a checked delete button
271  """
272  for index, item in enumerate(self._highlight_filters):
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)
279  item[1].delete_button.clicked.disconnect(self._delete_highlight_filter)
280  del self._highlight_filters[index]
281 
283  """
284  Deletes any exclude filters which have a checked delete button
285  """
286  for index, item in enumerate(self._exclude_filters):
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)
293  item[1].delete_button.clicked.disconnect(self._delete_exclude_filter)
294  del self._exclude_filters[index]
295 
296  def _add_highlight_filter(self, filter_index=False):
297  """
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''
300  OR
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''
303  OR
304  :return: if no filter was added then None is returned, ''NoneType''
305  """
306  if filter_index is False:
307  filter_index = -1
308  filter_select_menu = QMenu()
309  for index in self._filter_factory_order:
310  # flattens the _highlight filters list and only adds the item if it
311  # doesn't already exist
312 
313  if index in ['message', 'location'] or \
314  not self.filter_factory[index][1] in \
315  [type(item) for sublist in self._highlight_filters for item in sublist]:
316  filter_select_menu.addAction(self.filter_factory[index][0])
317  action = filter_select_menu.exec_(QCursor.pos())
318  if action is None:
319  return
320  for index in self._filter_factory_order:
321  if self.filter_factory[index][0] == action.text():
322  filter_index = index
323  if filter_index == -1:
324  return
325 
326  index = len(self._highlight_filters)
327  newfilter = self.filter_factory[filter_index][1]()
328  if len(self.filter_factory[filter_index]) >= 4:
329  newwidget = self.filter_factory[filter_index][2](
330  newfilter, self._rospack, self.filter_factory[filter_index][3])
331  else:
332  newwidget = self.filter_factory[filter_index][2](newfilter, self._rospack)
333 
334  # pack the new filter tuple onto the filter list
335  self._highlight_filters.append((
336  newfilter,
337  FilterWrapperWidget(
338  newwidget, self.filter_factory[filter_index][0]),
339  filter_index))
340  self._proxy_model.add_highlight_filter(newfilter)
341  newfilter.filter_changed_signal.connect(self._proxy_model.handle_highlight_filters_changed)
342  self._highlight_filters[index][1].delete_button.clicked.connect(
344  self._model.rowsInserted.connect(self._highlight_filters[index][1].repopulate)
345 
346  # place the widget in the proper location
347  self.highlight_table.insertRow(index)
348  self.highlight_table.setCellWidget(index, 0, self._highlight_filters[index][1])
349  self.highlight_table.resizeColumnsToContents()
350  self.highlight_table.resizeRowsToContents()
351  newfilter.filter_changed_signal.emit()
352  return index
353 
354  def _add_exclude_filter(self, filter_index=False):
355  """
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''
358  OR
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''
361  OR
362  :return: if no filter was added then None is returned, ''NoneType''
363  """
364  if filter_index is False:
365  filter_index = -1
366  filter_select_menu = QMenu()
367  for index in self._filter_factory_order:
368  # flattens the _exclude filters list and only adds the item if it doesn't
369  # already exist
370  if index in ['message', 'location'] or \
371  not self.filter_factory[index][1] in \
372  [type(item) for sublist in self._exclude_filters for item in sublist]:
373  filter_select_menu.addAction(self.filter_factory[index][0])
374  action = filter_select_menu.exec_(QCursor.pos())
375  if action is None:
376  return None
377  for index in self._filter_factory_order:
378  if self.filter_factory[index][0] == action.text():
379  filter_index = index
380  if filter_index == -1:
381  return None
382 
383  index = len(self._exclude_filters)
384  newfilter = self.filter_factory[filter_index][1]()
385  if len(self.filter_factory[filter_index]) >= 4:
386  newwidget = self.filter_factory[filter_index][2](
387  newfilter, self._rospack, self.filter_factory[filter_index][3])
388  else:
389  newwidget = self.filter_factory[filter_index][2](newfilter, self._rospack)
390 
391  # pack the new filter tuple onto the filter list
392  self._exclude_filters.append((
393  newfilter,
394  FilterWrapperWidget(
395  newwidget, self.filter_factory[filter_index][0]),
396  filter_index))
397  self._proxy_model.add_exclude_filter(newfilter)
398  newfilter.filter_changed_signal.connect(self._proxy_model.handle_exclude_filters_changed)
399  self._exclude_filters[index][1].delete_button.clicked.connect(self._delete_exclude_filter)
400  self._model.rowsInserted.connect(self._exclude_filters[index][1].repopulate)
401 
402  # place the widget in the proper location
403  self.exclude_table.insertRow(index)
404  self.exclude_table.setCellWidget(index, 0, self._exclude_filters[index][1])
405  self.exclude_table.resizeColumnsToContents()
406  self.exclude_table.resizeRowsToContents()
407  newfilter.filter_changed_signal.emit()
408  return index
409 
410  def _process_highlight_exclude_filter(self, selection, selectiontype, exclude=False):
411  """
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,
417  ''bool''
418  """
419  types = {
420  self.tr('Node'): 2,
421  self.tr('Topic'): 4,
422  self.tr('Severity'): 1,
423  self.tr('Message'): 0}
424  try:
425  col = types[selectiontype]
426  except:
427  raise RuntimeError(
428  "Bad Column name in ConsoleWidget._process_highlight_exclude_filter()")
429 
430  if col == 0:
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('.', '\\.')
440  if exclude:
441  filter_index = self._add_exclude_filter(selectiontype.lower())
442  filter_widget = self._exclude_filters[filter_index][1].findChildren(
443  QWidget, QRegExp('.*FilterWidget.*'))[0]
444  else:
445  filter_index = self._add_highlight_filter(col)
446  filter_widget = self._highlight_filters[filter_index][1].findChildren(
447  QWidget, QRegExp('.*FilterWidget.*'))[0]
448  filter_widget.set_regex(True)
449  filter_widget.set_text('^' + message + '$')
450 
451  else:
452  if exclude:
453  # Test if the filter we are adding already exists if it does use the existing filter
454  if self.filter_factory[selectiontype.lower()][1] not in \
455  [type(item) for sublist in self._exclude_filters for item in sublist]:
456  filter_index = self._add_exclude_filter(selectiontype.lower())
457  else:
458  for index, item in enumerate(self._exclude_filters):
459  if type(item[0]) == self.filter_factory[selectiontype.lower()][1]:
460  filter_index = index
461  else:
462  # Test if the filter we are adding already exists if it does use the existing filter
463  if self.filter_factory[selectiontype.lower()][1] not in \
464  [type(item) for sublist in self._highlight_filters for item in sublist]:
465  filter_index = self._add_highlight_filter(col)
466  else:
467  for index, item in enumerate(self._highlight_filters):
468  if type(item[0]) == self.filter_factory[selectiontype.lower()][1]:
469  filter_index = index
470 
471  if exclude:
472  filter_widget = self._exclude_filters[filter_index][1].findChildren(
473  QWidget, QRegExp('.*FilterWidget.*'))[0]
474  filter_widget.select_item(selection)
475  else:
476  filter_widget = self._highlight_filters[filter_index][1].findChildren(
477  QWidget, QRegExp('.*FilterWidget.*'))[0]
478  filter_widget.select_item(selection)
479 
480  def _rightclick_menu(self, event):
481  """
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''
485  """
486  severities = {}
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())
492 
493  # menutext entries turned into
494  menutext = []
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')])
505 
506  menu = QMenu()
507  submenus = []
508  subsubmenus = []
509  for item in menutext:
510  if len(item) > 1:
511  submenus.append(QMenu(item[0], menu))
512  for subitem in item[1]:
513  if len(subitem) > 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])
518  action.setData(key)
519  else:
520  for subsubitem in subitem[1]:
521  subsubmenus[-1].addAction(subsubitem)
522  submenus[-1].addMenu(subsubmenus[-1])
523  else:
524  submenus[-1].addAction(subitem[0])
525  menu.addMenu(submenus[-1])
526  else:
527  menu.addAction(item[0])
528  action = menu.exec_(event.globalPos())
529 
530  if action is None or action == 0:
531  return
532  elif action.text() == self.tr('Browse Selected'):
533  self._show_browsers()
534  elif action.text() == self.tr('Copy Selected'):
535  rowlist = []
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'):
544  self._process_highlight_exclude_filter(action.text(), 'Message', False)
545  elif action.parentWidget().title() == self.tr('Exclude'):
546  self._process_highlight_exclude_filter(action.text(), 'Message', True)
547  else:
548  raise RuntimeError("Menu format corruption in ConsoleWidget._rightclick_menu()")
549  else:
550  # This processes the dynamic list entries (severity, node and topic)
551  try:
552  roottitle = action.parentWidget().parentWidget().title()
553  except:
554  raise RuntimeError("Menu format corruption in ConsoleWidget._rightclick_menu()")
555 
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)
562  else:
563  raise RuntimeError(
564  "Unknown Root Action %s selected in ConsoleWidget._rightclick_menu()" %
565  roottitle)
566 
567  def update_status(self):
568  """
569  Sets the message display label to the current value
570  """
571  if self._model.rowCount() == self._proxy_model.rowCount():
572  tip = self.tr('Displaying %d messages') % (self._model.rowCount())
573  else:
574  tip = self.tr('Displaying %d of %d messages') % (
575  self._proxy_model.rowCount(), self._model.rowCount())
576  self.messages_label.setText(tip)
577 
579  for browser in self._browsers:
580  browser.close()
581 
582  def _show_browsers(self):
583  rowlist = []
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))
589  self._browsers[-1].show()
590 
591  def _handle_clear_button_clicked(self, checked):
592  self._model.remove_rows([])
593  Message._next_id = 1
594 
595  def _handle_load_clicked(self, checked):
596  filename = QFileDialog.getOpenFileName(
597  self, self.tr('Load from File'), '.',
598  self.tr('rqt_console message file {.csv} (*.csv)'))
599  if filename[0] != '':
600  try:
601  with open(filename[0], 'r') as h:
602  lines = h.read().splitlines()
603  except IOError as e:
604  qWarning(str(e))
605  return False
606 
607  # extract column header
608  columns = lines[0].split(';')
609  if len(lines) < 2:
610  return True
611 
612  # join wrapped lines
613  rows = []
614  last_wrapped = False
615  for line in lines[1:]:
616  # ignore empty lines
617  if not line:
618  continue
619  # check for quotes and remove them
620  if line == '"':
621  has_prefix = not last_wrapped
622  has_suffix = last_wrapped
623  line = ''
624  else:
625  has_prefix = line[0] == '"'
626  if has_prefix:
627  line = line[1:]
628  has_suffix = line[-1] == '"'
629  if has_suffix:
630  line = line[:-1]
631 
632  # ignore line without prefix if previous line was not wrapped
633  if not has_prefix and not last_wrapped:
634  continue
635  # remove wrapped line which is not continued on the next line
636  if last_wrapped and has_prefix:
637  rows.pop()
638 
639  # add/append lines
640  if last_wrapped:
641  rows[-1] += line
642  else:
643  # add line without quote prefix
644  rows.append(line)
645 
646  last_wrapped = not has_suffix
647 
648  # generate message for each row
649  messages = []
650  skipped = []
651  for row in rows:
652  data = row.split('";"')
653  msg = Message()
654  msg.set_stamp_format('hh:mm:ss.ZZZ (yyyy-MM-dd)')
655  for i, column in enumerate(columns):
656  value = data[i]
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)
663  msg = None
664  break
665  elif column == 'stamp':
666  parts = value.split('.')
667  if len(parts) != 2:
668  skipped.append('Unknown timestamp format: %s' % value)
669  msg = None
670  break
671  msg.stamp = (int(parts[0]), int(parts[1]))
672  elif column == 'topics':
673  msg.topics = value.split(',')
674  elif column == 'node':
675  msg.node = value
676  elif column == 'location':
677  msg.location = value
678  else:
679  skipped.append('Unknown column: %s' % column)
680  msg = None
681  break
682  if msg:
683  messages.append(msg)
684  if skipped:
685  qWarning(
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)))
689 
690  if messages:
691  self._model.insert_rows(messages)
692 
693  self._handle_pause_clicked(True)
694 
695  return True
696 
697  else:
698  qWarning('File does not appear to be a rqt_console message file: missing file header.')
699  return False
700 
701  def _handle_save_clicked(self, checked):
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':
707  filename += '.csv'
708  try:
709  handle = open(filename, 'w')
710  except IOError as e:
711  qWarning(str(e))
712  return
713  try:
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]
718  data = {}
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
725  line = []
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))
731  return False
732  finally:
733  handle.close()
734  return True
735 
737  self._paused = True
738  self.pause_button.setVisible(False)
739  self.record_button.setVisible(True)
740 
742  self._paused = False
743  self.pause_button.setVisible(True)
744  self.record_button.setVisible(False)
745 
747  self.table_view.resizeColumnsToContents()
748 
750  rowlist = []
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)
755 
756  def _handle_custom_keypress(self, event, old_keyPressEvent=QTableView.keyPressEvent):
757  """
758  Handles the delete key.
759  The delete key removes the tableview's selected rows from the datamodel
760  """
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:
771  if self._delete_selected_rows():
772  event.accept()
773  return old_keyPressEvent(self.table_view, event)
774 
776  self, event,
777  old_doubleclickevent=QTableView.mouseDoubleClickEvent):
778 
779  if event.buttons() & Qt.LeftButton and event.modifiers() == Qt.NoModifier:
780  self._show_browsers()
781  event.accept()
782  return old_doubleclickevent(self.table_view, event)
783 
784  def _handle_mouse_press(self, event, old_pressEvent=QTableView.mousePressEvent):
785  if event.buttons() & Qt.RightButton and event.modifiers() == Qt.NoModifier:
786  self._rightclick_menu(event)
787  event.accept()
788  return old_pressEvent(self.table_view, event)
789 
790  def save_settings(self, plugin_settings, instance_settings):
791  instance_settings.set_value('settings_exist', True)
792 
793  instance_settings.set_value('table_splitter', self.table_splitter.saveState())
794  instance_settings.set_value('filter_splitter', self.filter_splitter.saveState())
795 
796  instance_settings.set_value('paused', self._paused)
797  instance_settings.set_value(
798  'show_highlighted_only', self.highlight_exclude_button.isChecked())
799 
800  exclude_filters = []
801  for index, item in enumerate(self._exclude_filters):
802  exclude_filters.append(item[2])
803  filter_settings = instance_settings.get_settings('exclude_filter_' + str(index))
804  item[1].save_settings(filter_settings)
805  instance_settings.set_value('exclude_filters', pack(exclude_filters))
806 
807  highlight_filters = []
808  for index, item in enumerate(self._highlight_filters):
809  highlight_filters.append(item[2])
810  filter_settings = instance_settings.get_settings('highlight_filter_' + str(index))
811  item[1].save_settings(filter_settings)
812  instance_settings.set_value('highlight_filters', pack(highlight_filters))
813  instance_settings.set_value('message_limit', self._model.get_message_limit())
814 
815  def restore_settings(self, pluggin_settings, instance_settings):
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'))
820  else:
821  self.filter_splitter.setSizes([1, 1])
822 
823  paused = instance_settings.value('paused') in [True, 'true']
824  if paused:
825  self._handle_pause_clicked()
826  else:
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())
831 
832  for item in self._exclude_filters:
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):
839  self._add_exclude_filter(item)
840  filter_settings = instance_settings.get_settings('exclude_filter_' + str(index))
841  self._exclude_filters[-1][1].restore_settings(filter_settings)
842  else:
843  self._add_exclude_filter('severity')
844 
845  for item in self._highlight_filters:
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):
852  self._add_highlight_filter(item)
853  filter_settings = instance_settings.get_settings(
854  'highlight_filter_' + str(index))
855  self._highlight_filters[-1][1].restore_settings(filter_settings)
856  else:
857  self._add_highlight_filter('message')
858 
859  if instance_settings.contains('message_limit'):
860  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 Wed Jun 5 2019 21:05:12