00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033 import rospy
00034 import rosbag
00035 import time
00036 import threading
00037
00038
00039 from python_qt_binding.QtCore import Qt, QTimer, qWarning, Signal
00040 from python_qt_binding.QtGui import QGraphicsScene, QMessageBox
00041
00042 import bag_helper
00043
00044 from .timeline_frame import TimelineFrame
00045 from .message_listener_thread import MessageListenerThread
00046 from .message_loader_thread import MessageLoaderThread
00047 from .player import Player
00048 from .recorder import Recorder
00049 from .timeline_menu import TimelinePopupMenu
00050
00051
00052 class BagTimeline(QGraphicsScene):
00053 """
00054 BagTimeline contains bag files, all information required to display the bag data visualization on the screen
00055 Also handles events
00056 """
00057 status_bar_changed_signal = Signal()
00058
00059 def __init__(self, context):
00060 """
00061 :param context: plugin context hook to enable adding rqt_bag plugin widgets as ROS_GUI snapin panes, ''PluginContext''
00062 """
00063 super(BagTimeline, self).__init__()
00064 self._bags = []
00065 self._bag_lock = threading.RLock()
00066
00067 self.background_task = None
00068 self.background_task_cancel = False
00069
00070
00071 self._playhead_lock = threading.RLock()
00072 self._max_play_speed = 1024.0
00073 self._min_play_speed = 1.0 / 1024.0
00074 self._play_speed = 0.0
00075 self._play_all = False
00076 self._playhead_positions_cvs = {}
00077 self._playhead_positions = {}
00078 self._message_loaders = {}
00079 self._messages_cvs = {}
00080 self._messages = {}
00081 self._message_listener_threads = {}
00082 self._player = False
00083 self._recorder = None
00084 self.last_frame = None
00085 self.last_playhead = None
00086 self.desired_playhead = None
00087 self.wrap = True
00088 self.stick_to_end = False
00089 self._play_timer = QTimer()
00090 self._play_timer.timeout.connect(self.on_idle)
00091 self._play_timer.start(3)
00092
00093
00094 self._context = context
00095 self.popups = set()
00096 self._views = []
00097 self._listeners = {}
00098
00099
00100 self._timeline_frame = TimelineFrame()
00101 self._timeline_frame.setPos(0, 0)
00102 self.addItem(self._timeline_frame)
00103
00104 self.background_progress = 0
00105
00106 def get_context(self):
00107 """
00108 :returns: the ROS_GUI context, 'PluginContext'
00109 """
00110 return self._context
00111
00112 def handle_close(self):
00113 """
00114 Cleans up the timeline, bag and any threads
00115 """
00116 for topic in self._get_topics():
00117 self.stop_publishing(topic)
00118 self._message_loaders[topic].stop()
00119 if self._player:
00120 self._player.stop()
00121 if self._recorder:
00122 self._recorder.stop()
00123 if self.background_task is not None:
00124 self.background_task_cancel = True
00125 self._timeline_frame.handle_close()
00126 for bag in self._bags:
00127 bag.close()
00128 for _, frame in self._views:
00129 self._context.remove_widget(frame)
00130
00131
00132 def add_bag(self, bag):
00133 """
00134 creates an indexing thread for each new topic in the bag
00135 fixes the boarders and notifies the indexing thread to index the new items bags
00136 :param bag: ros bag file, ''rosbag.bag''
00137 """
00138 self._bags.append(bag)
00139
00140 bag_topics = bag_helper.get_topics(bag)
00141
00142 new_topics = set(bag_topics) - set(self._timeline_frame.topics)
00143
00144 for topic in new_topics:
00145 self._playhead_positions_cvs[topic] = threading.Condition()
00146 self._messages_cvs[topic] = threading.Condition()
00147 self._message_loaders[topic] = MessageLoaderThread(self, topic)
00148
00149 self._timeline_frame._start_stamp = self._get_start_stamp()
00150 self._timeline_frame._end_stamp = self._get_end_stamp()
00151 self._timeline_frame.topics = self._get_topics()
00152 self._timeline_frame._topics_by_datatype = self._get_topics_by_datatype()
00153
00154 if self._timeline_frame._stamp_left is None:
00155 self._timeline_frame.reset_timeline()
00156
00157
00158 with self._timeline_frame.index_cache_cv:
00159 for topic in bag_topics:
00160 self._timeline_frame.invalidated_caches.add(topic)
00161 if topic in self._timeline_frame.index_cache:
00162 del self._timeline_frame.index_cache[topic]
00163
00164 self._timeline_frame.index_cache_cv.notify()
00165
00166
00167 def _get_start_stamp(self):
00168 """
00169 :return: first stamp in the bags, ''rospy.Time''
00170 """
00171 with self._bag_lock:
00172 start_stamp = None
00173 for bag in self._bags:
00174 bag_start_stamp = bag_helper.get_start_stamp(bag)
00175 if bag_start_stamp is not None and (start_stamp is None or bag_start_stamp < start_stamp):
00176 start_stamp = bag_start_stamp
00177 return start_stamp
00178
00179 def _get_end_stamp(self):
00180 """
00181 :return: last stamp in the bags, ''rospy.Time''
00182 """
00183 with self._bag_lock:
00184 end_stamp = None
00185 for bag in self._bags:
00186 bag_end_stamp = bag_helper.get_end_stamp(bag)
00187 if bag_end_stamp is not None and (end_stamp is None or bag_end_stamp > end_stamp):
00188 end_stamp = bag_end_stamp
00189 return end_stamp
00190
00191 def _get_topics(self):
00192 """
00193 :return: sorted list of topic names, ''list(str)''
00194 """
00195 with self._bag_lock:
00196 topics = set()
00197 for bag in self._bags:
00198 for topic in bag_helper.get_topics(bag):
00199 topics.add(topic)
00200 return sorted(topics)
00201
00202 def _get_topics_by_datatype(self):
00203 """
00204 :return: dict of list of topics for each datatype, ''dict(datatype:list(topic))''
00205 """
00206 with self._bag_lock:
00207 topics_by_datatype = {}
00208 for bag in self._bags:
00209 for datatype, topics in bag_helper.get_topics_by_datatype(bag).items():
00210 topics_by_datatype.setdefault(datatype, []).extend(topics)
00211 return topics_by_datatype
00212
00213 def get_datatype(self, topic):
00214 """
00215 :return: datatype associated with a topic, ''str''
00216 :raises: if there are multiple datatypes assigned to a single topic, ''Exception''
00217 """
00218 with self._bag_lock:
00219 datatype = None
00220 for bag in self._bags:
00221 bag_datatype = bag_helper.get_datatype(bag, topic)
00222 if datatype and bag_datatype and (bag_datatype != datatype):
00223 raise Exception('topic %s has multiple datatypes: %s and %s' % (topic, datatype, bag_datatype))
00224 datatype = bag_datatype
00225 return datatype
00226
00227 def get_entries(self, topics, start_stamp, end_stamp):
00228 """
00229 generator function for bag entries
00230 :param topics: list of topics to query, ''list(str)''
00231 :param start_stamp: stamp to start at, ''rospy.Time''
00232 :param end_stamp: stamp to end at, ''rospy,Time''
00233 :returns: entries the bag file, ''msg''
00234 """
00235 with self._bag_lock:
00236 from rosbag import bag
00237 bag_entries = []
00238 for b in self._bags:
00239 bag_start_time = bag_helper.get_start_stamp(b)
00240 if bag_start_time is not None and bag_start_time > end_stamp:
00241 continue
00242
00243 bag_end_time = bag_helper.get_end_stamp(b)
00244 if bag_end_time is not None and bag_end_time < start_stamp:
00245 continue
00246
00247 connections = list(b._get_connections(topics))
00248 bag_entries.append(b._get_entries(connections, start_stamp, end_stamp))
00249
00250 for entry, _ in bag._mergesort(bag_entries, key=lambda entry: entry.time):
00251 yield entry
00252
00253 def get_entries_with_bags(self, topic, start_stamp, end_stamp):
00254 """
00255 generator function for bag entries
00256 :param topics: list of topics to query, ''list(str)''
00257 :param start_stamp: stamp to start at, ''rospy.Time''
00258 :param end_stamp: stamp to end at, ''rospy,Time''
00259 :returns: tuple of (bag, entry) for the entries in the bag file, ''(rosbag.bag, msg)''
00260 """
00261 with self._bag_lock:
00262 from rosbag import bag
00263
00264 bag_entries = []
00265 bag_by_iter = {}
00266 for b in self._bags:
00267 bag_start_time = bag_helper.get_start_stamp(b)
00268 if bag_start_time is not None and bag_start_time > end_stamp:
00269 continue
00270
00271 bag_end_time = bag_helper.get_end_stamp(b)
00272 if bag_end_time is not None and bag_end_time < start_stamp:
00273 continue
00274
00275 connections = list(b._get_connections(topic))
00276 it = iter(b._get_entries(connections, start_stamp, end_stamp))
00277 bag_by_iter[it] = b
00278 bag_entries.append(it)
00279
00280 for entry, it in bag._mergesort(bag_entries, key=lambda entry: entry.time):
00281 yield bag_by_iter[it], entry
00282
00283 def get_entry(self, t, topic):
00284 """
00285 Access a bag entry
00286 :param t: time, ''rospy.Time''
00287 :param topic: the topic to be accessed, ''str''
00288 :return: tuple of (bag, entry) corisponding to time t and topic, ''(rosbag.bag, msg)''
00289 """
00290 with self._bag_lock:
00291 entry_bag, entry = None, None
00292 for bag in self._bags:
00293 bag_entry = bag._get_entry(t, bag._get_connections(topic))
00294 if bag_entry and (not entry or bag_entry.time > entry.time):
00295 entry_bag, entry = bag, bag_entry
00296
00297 return entry_bag, entry
00298
00299 def get_entry_after(self, t):
00300 """
00301 Access a bag entry
00302 :param t: time, ''rospy.Time''
00303 :return: tuple of (bag, entry) corisponding to time t, ''(rosbag.bag, msg)''
00304 """
00305 with self._bag_lock:
00306 entry_bag, entry = None, None
00307 for bag in self._bags:
00308 bag_entry = bag._get_entry_after(t, bag._get_connections())
00309 if bag_entry and (not entry or bag_entry.time < entry.time):
00310 entry_bag, entry = bag, bag_entry
00311
00312 return entry_bag, entry
00313
00314 def get_next_message_time(self):
00315 """
00316 :return: time of the next message after the current playhead position,''rospy.Time''
00317 """
00318 if self._timeline_frame.playhead is None:
00319 return None
00320
00321 _, entry = self.get_entry_after(self._timeline_frame.playhead)
00322 if entry is None:
00323 return self._timeline_frame._start_stamp
00324
00325 return entry.time
00326
00327
00328
00329 def start_background_task(self, background_task):
00330 """
00331 Verify that a background task is not currently running before starting a new one
00332 :param background_task: name of the background task, ''str''
00333 """
00334 if self.background_task is not None:
00335 QMessageBox(QMessageBox.Warning, 'Exclamation', 'Background operation already running:\n\n%s' % self.background_task, QMessageBox.Ok).exec_()
00336 return False
00337
00338 self.background_task = background_task
00339 self.background_task_cancel = False
00340 return True
00341
00342 def stop_background_task(self):
00343 self.background_task = None
00344
00345 def copy_region_to_bag(self, filename):
00346 if len(self._bags) > 0:
00347 self._export_region(filename, self._timeline_frame.topics, self._timeline_frame.play_region[0], self._timeline_frame.play_region[1])
00348
00349 def _export_region(self, path, topics, start_stamp, end_stamp):
00350 """
00351 Starts a thread to save the current selection to a new bag file
00352 :param path: filesystem path to write to, ''str''
00353 :param topics: topics to write to the file, ''list(str)''
00354 :param start_stamp: start of area to save, ''rospy.Time''
00355 :param end_stamp: end of area to save, ''rospy.Time''
00356 """
00357 if not self.start_background_task('Copying messages to "%s"' % path):
00358 return
00359
00360 bag_entries = list(self.get_entries_with_bags(topics, start_stamp, end_stamp))
00361
00362 if self.background_task_cancel:
00363 return
00364
00365
00366 total_messages = len(bag_entries)
00367
00368
00369 if total_messages == 0:
00370 QMessageBox(QMessageBox.Warning, 'rqt_bag', 'No messages found', QMessageBox.Ok).exec_()
00371 return
00372
00373
00374 try:
00375 export_bag = rosbag.Bag(path, 'w')
00376 except Exception:
00377 QMessageBox(QMessageBox.Warning, 'rqt_bag', 'Error opening bag file [%s] for writing' % path, QMessageBox.Ok).exec_()
00378
00379
00380 self._export_thread = threading.Thread(target=self._run_export_region, args=(export_bag, topics, start_stamp, end_stamp, bag_entries))
00381 self._export_thread.start()
00382
00383 def _run_export_region(self, export_bag, topics, start_stamp, end_stamp, bag_entries):
00384 """
00385 Threaded function that saves the current selection to a new bag file
00386 :param export_bag: bagfile to write to, ''rosbag.bag''
00387 :param topics: topics to write to the file, ''list(str)''
00388 :param start_stamp: start of area to save, ''rospy.Time''
00389 :param end_stamp: end of area to save, ''rospy.Time''
00390 """
00391 total_messages = len(bag_entries)
00392 update_step = max(1, total_messages / 100)
00393 message_num = 1
00394 progress = 0
00395
00396 for bag, entry in bag_entries:
00397 if self.background_task_cancel:
00398 break
00399 try:
00400 topic, msg, t = self.read_message(bag, entry.position)
00401 export_bag.write(topic, msg, t)
00402 except Exception as ex:
00403 qWarning('Error exporting message at position %s: %s' % (str(entry.position), str(ex)))
00404 export_bag.close()
00405 self.stop_background_task()
00406 return
00407
00408 if message_num % update_step == 0 or message_num == total_messages:
00409 new_progress = int(100.0 * (float(message_num) / total_messages))
00410 if new_progress != progress:
00411 progress = new_progress
00412 if not self.background_task_cancel:
00413 self.background_progress = progress
00414 self.status_bar_changed_signal.emit()
00415
00416 message_num += 1
00417
00418
00419 try:
00420 self.background_progress = 0
00421 self.status_bar_changed_signal.emit()
00422 export_bag.close()
00423 except Exception as ex:
00424 QMessageBox(QMessageBox.Warning, 'rqt_bag', 'Error closing bag file [%s]: %s' % (export_bag.filename, str(ex)), QMessageBox.Ok).exec_()
00425 self.stop_background_task()
00426
00427 def read_message(self, bag, position):
00428 with self._bag_lock:
00429 return bag._read_message(position)
00430
00431
00432 def on_mouse_down(self, event):
00433 if event.buttons() == Qt.LeftButton:
00434 self._timeline_frame.on_left_down(event)
00435 elif event.buttons() == Qt.MidButton:
00436 self._timeline_frame.on_middle_down(event)
00437 elif event.buttons() == Qt.RightButton:
00438 TimelinePopupMenu(self, event)
00439
00440 def on_mouse_up(self, event):
00441 self._timeline_frame.on_mouse_up(event)
00442
00443 def on_mouse_move(self, event):
00444 self._timeline_frame.on_mouse_move(event)
00445
00446 def on_mousewheel(self, event):
00447 self._timeline_frame.on_mousewheel(event)
00448
00449
00450
00451 def zoom_in(self):
00452 self._timeline_frame.zoom_in()
00453
00454 def zoom_out(self):
00455 self._timeline_frame.zoom_out()
00456
00457 def reset_zoom(self):
00458 self._timeline_frame.reset_zoom()
00459
00460 def translate_timeline_left(self):
00461 self._timeline_frame.translate_timeline_left()
00462
00463 def translate_timeline_right(self):
00464 self._timeline_frame.translate_timeline_right()
00465
00466
00467 def is_publishing(self, topic):
00468 return self._player and self._player.is_publishing(topic)
00469
00470 def start_publishing(self, topic):
00471 if not self._player and not self._create_player():
00472 return False
00473
00474 self._player.start_publishing(topic)
00475 return True
00476
00477 def stop_publishing(self, topic):
00478 if not self._player:
00479 return False
00480
00481 self._player.stop_publishing(topic)
00482 return True
00483
00484 def _create_player(self):
00485 if not self._player:
00486 try:
00487 self._player = Player(self)
00488 except Exception as ex:
00489 qWarning('Error starting player; aborting publish: %s' % str(ex))
00490 return False
00491
00492 return True
00493
00494 def set_publishing_state(self, start_publishing):
00495 if start_publishing:
00496 for topic in self._timeline_frame.topics:
00497 if not self.start_publishing(topic):
00498 break
00499 else:
00500 for topic in self._timeline_frame.topics:
00501 self.stop_publishing(topic)
00502
00503
00504 def _get_play_all(self):
00505 return self._play_all
00506
00507 def _set_play_all(self, play_all):
00508 if play_all == self._play_all:
00509 return
00510
00511 self._play_all = not self._play_all
00512
00513 self.last_frame = None
00514 self.last_playhead = None
00515 self.desired_playhead = None
00516
00517 play_all = property(_get_play_all, _set_play_all)
00518
00519 def toggle_play_all(self):
00520 self.play_all = not self.play_all
00521
00522
00523 def on_idle(self):
00524 if self.play_speed != 0.0:
00525 self._step_playhead()
00526
00527 def _step_playhead(self):
00528 """
00529 moves the playhead to the next position based on the desired position
00530 """
00531
00532 if self._timeline_frame.playhead != self.last_playhead:
00533 self.last_frame = None
00534 self.last_playhead = None
00535 self.desired_playhead = None
00536
00537 if self._play_all:
00538 self.step_next_message()
00539 else:
00540 self.step_fixed()
00541
00542 def step_fixed(self):
00543 """
00544 Moves the playhead a fixed distance into the future based on the current play speed
00545 """
00546 if self.play_speed == 0.0 or not self._timeline_frame.playhead:
00547 self.last_frame = None
00548 self.last_playhead = None
00549 return
00550
00551 now = rospy.Time.from_sec(time.time())
00552 if self.last_frame:
00553
00554 if self.stick_to_end:
00555 new_playhead = self.end_stamp
00556 else:
00557 new_playhead = self._timeline_frame.playhead + rospy.Duration.from_sec((now - self.last_frame).to_sec() * self.play_speed)
00558
00559 start_stamp, end_stamp = self._timeline_frame.play_region
00560
00561 if new_playhead > end_stamp:
00562 if self.wrap:
00563 if self.play_speed > 0.0:
00564 new_playhead = start_stamp
00565 else:
00566 new_playhead = end_stamp
00567 else:
00568 new_playhead = end_stamp
00569
00570 if self.play_speed > 0.0:
00571 self.stick_to_end = True
00572
00573 elif new_playhead < start_stamp:
00574 if self.wrap:
00575 if self.play_speed < 0.0:
00576 new_playhead = end_stamp
00577 else:
00578 new_playhead = start_stamp
00579 else:
00580 new_playhead = start_stamp
00581
00582
00583 self._timeline_frame.playhead = new_playhead
00584
00585 self.last_frame = now
00586 self.last_playhead = self._timeline_frame.playhead
00587
00588 def step_next_message(self):
00589 """
00590 Move the playhead to the next message
00591 """
00592 if self.play_speed <= 0.0 or not self._timeline_frame.playhead:
00593 self.last_frame = None
00594 self.last_playhead = None
00595 return
00596
00597 if self.last_frame:
00598 if not self.desired_playhead:
00599 self.desired_playhead = self._timeline_frame.playhead
00600 else:
00601 delta = rospy.Time.from_sec(time.time()) - self.last_frame
00602 if delta > rospy.Duration.from_sec(0.1):
00603 delta = rospy.Duration.from_sec(0.1)
00604 self.desired_playhead += delta
00605
00606
00607 next_message_time = self.get_next_message_time()
00608
00609 if next_message_time < self.desired_playhead:
00610 self._timeline_frame.playhead = next_message_time
00611 else:
00612 self._timeline_frame.playhead = self.desired_playhead
00613
00614 self.last_frame = rospy.Time.from_sec(time.time())
00615 self.last_playhead = self._timeline_frame.playhead
00616
00617
00618 def record_bag(self, filename, all=True, topics=[], regex=False, limit=0):
00619 try:
00620 self._recorder = Recorder(filename, bag_lock=self._bag_lock, all=all, topics=topics, regex=regex, limit=limit)
00621 except Exception, ex:
00622 qWarning('Error opening bag for recording [%s]: %s' % (filename, str(ex)))
00623 return
00624
00625 self._recorder.add_listener(self._message_recorded)
00626
00627 self.add_bag(self._recorder.bag)
00628
00629 self._recorder.start()
00630
00631 self.wrap = False
00632 self._timeline_frame._index_cache_thread.period = 0.1
00633
00634 self.update()
00635
00636 def toggle_recording(self):
00637 if self._recorder:
00638 self._recorder.toggle_paused()
00639 self.update()
00640
00641 def _message_recorded(self, topic, msg, t):
00642 if self._timeline_frame._start_stamp is None:
00643 self._timeline_frame._start_stamp = t
00644 self._timeline_frame._end_stamp = t
00645 self._timeline_frame._playhead = t
00646 elif self._timeline_frame._end_stamp is None or t > self._timeline_frame._end_stamp:
00647 self._timeline_frame._end_stamp = t
00648
00649 if not self._timeline_frame.topics or topic not in self._timeline_frame.topics:
00650 self._timeline_frame.topics = self._get_topics()
00651 self._timeline_frame._topics_by_datatype = self._get_topics_by_datatype()
00652
00653 self._playhead_positions_cvs[topic] = threading.Condition()
00654 self._messages_cvs[topic] = threading.Condition()
00655 self._message_loaders[topic] = MessageLoaderThread(self, topic)
00656
00657 if self._timeline_frame._stamp_left is None:
00658 self.reset_zoom()
00659
00660
00661 with self._timeline_frame.index_cache_cv:
00662 self._timeline_frame.invalidated_caches.add(topic)
00663 self._timeline_frame.index_cache_cv.notify()
00664
00665 if topic in self._listeners:
00666 for listener in self._listeners[topic]:
00667 try:
00668 listener.timeline_changed()
00669 except Exception, ex:
00670 qWarning('Error calling timeline_changed on %s: %s' % (type(listener), str(ex)))
00671
00672
00673 def add_view(self, topic, view, frame):
00674 self._views.append((view, frame))
00675 self.add_listener(topic, view)
00676
00677 def remove_view(self, topic, view):
00678 self.remove_listener(topic, view)
00679 for entry in self._views:
00680 if entry[0] == view:
00681 self._views.remove(entry)
00682 self.update()
00683
00684 def has_listeners(self, topic):
00685 return topic in self._listeners
00686
00687 def add_listener(self, topic, listener):
00688 self._listeners.setdefault(topic, []).append(listener)
00689
00690 self._message_listener_threads[(topic, listener)] = MessageListenerThread(self, topic, listener)
00691
00692 self._message_loaders[topic].reset()
00693 with self._playhead_positions_cvs[topic]:
00694 self._playhead_positions_cvs[topic].notify_all()
00695
00696 self.update()
00697
00698 def remove_listener(self, topic, listener):
00699 topic_listeners = self._listeners.get(topic)
00700 if topic_listeners is not None and listener in topic_listeners:
00701 topic_listeners.remove(listener)
00702
00703 if len(topic_listeners) == 0:
00704 del self._listeners[topic]
00705
00706
00707 if (topic, listener) in self._message_listener_threads:
00708 self._message_listener_threads[(topic, listener)].stop()
00709 del self._message_listener_threads[(topic, listener)]
00710 self.update()
00711
00712
00713
00714
00715 def _get_play_speed(self):
00716 if self._timeline_frame._paused:
00717 return 0.0
00718 return self._play_speed
00719
00720 def _set_play_speed(self, play_speed):
00721 if play_speed == self._play_speed:
00722 return
00723
00724 if play_speed > 0.0:
00725 self._play_speed = min(self._max_play_speed, max(self._min_play_speed, play_speed))
00726 elif play_speed < 0.0:
00727 self._play_speed = max(-self._max_play_speed, min(-self._min_play_speed, play_speed))
00728 else:
00729 self._play_speed = play_speed
00730
00731 if self._play_speed < 1.0:
00732 self.stick_to_end = False
00733
00734 self.update()
00735 play_speed = property(_get_play_speed, _set_play_speed)
00736
00737 def toggle_play(self):
00738 if self._play_speed != 0.0:
00739 self.play_speed = 0.0
00740 else:
00741 self.play_speed = 1.0
00742
00743 def navigate_play(self):
00744 self.play_speed = 1.0
00745
00746 def navigate_stop(self):
00747 self.play_speed = 0.0
00748
00749 def navigate_rewind(self):
00750 if self._play_speed < 0.0:
00751 new_play_speed = self._play_speed * 2.0
00752 elif self._play_speed == 0.0:
00753 new_play_speed = -1.0
00754 else:
00755 new_play_speed = self._play_speed * 0.5
00756
00757 self.play_speed = new_play_speed
00758
00759 def navigate_fastforward(self):
00760 if self._play_speed > 0.0:
00761 new_play_speed = self._play_speed * 2.0
00762 elif self._play_speed == 0.0:
00763 new_play_speed = 2.0
00764 else:
00765 new_play_speed = self._play_speed * 0.5
00766
00767 self.play_speed = new_play_speed
00768
00769 def navigate_start(self):
00770 self._timeline_frame.playhead = self._timeline_frame.play_region[0]
00771
00772 def navigate_end(self):
00773 self._timeline_frame.playhead = self._timeline_frame.play_region[1]