Package pyforms :: Package gui :: Package Controls :: Package ControlEventTimeline :: Module TimelineWidget
[hide private]
[frames] | no frames]

Source Code for Module pyforms.gui.Controls.ControlEventTimeline.TimelineWidget

  1  #!/usr/bin/python 
  2  # -*- coding: utf-8 -*- 
  3   
  4  """ pyforms.gui.Controls.ControlEventTimeline.TimelineWidget 
  5   
  6  """ 
  7   
  8  from PyQt4 import QtGui, QtCore 
  9  from pyforms.gui.Controls.ControlEventTimeline.TimelinePointer import TimelinePointer 
 10  from pyforms.gui.Controls.ControlEventTimeline.TimelineDelta import TimelineDelta 
 11  from pyforms.gui.Controls.ControlEventTimeline.TimelineChart import TimelineChart 
 12   
 13   
 14  __author__ = ["Ricardo Ribeiro", "Hugo Cachitas"] 
 15  __credits__ = ["Ricardo Ribeiro", "Hugo Cachitas"] 
 16  __license__ = "MIT" 
 17  __version__ = "0.0" 
 18  __maintainer__ = "Ricardo Ribeiro" 
 19  __email__ = "ricardojvr@gmail.com" 
 20  __status__ = "Development" 
21 22 23 -class TimelineWidget(QtGui.QWidget):
24 """ 25 Timeline widget definition to be used in the ControlEventTimeline 26 """ 27 28 _defautcolor = QtGui.QColor(100, 100, 255) 29
30 - def __init__(self, *args, **kwargs):
31 super(TimelineWidget, self).__init__(*args, **kwargs) 32 33 # self.setFocusPolicy(QtCore.Qt.StrongFocus) 34 # self.grabKeyboard() 35 self.setMouseTracking(True) 36 self.setMinimumWidth(300000) 37 # self.setMinimumHeight(30) 38 39 # Timeline background color 40 palette = self.palette() 41 palette.setColor(self.backgroundRole(), QtCore.Qt.white) 42 self.setPalette(palette) 43 44 self._chartsColors = [ 45 QtGui.QColor(255, 0, 0), QtGui.QColor(0, 100, 0), 46 QtGui.QColor(0, 0, 255), QtGui.QColor(100, 100, 0), 47 QtGui.QColor(100, 0, 100), QtGui.QColor(0, 100, 100) 48 ] 49 self._charts = [] 50 self._data = [] 51 52 self._scale = 1.0 53 self._lastMouseY = None 54 55 self._moving = False 56 self._resizingBegin = False 57 self._resizingEnd = False 58 self._creating_event = False 59 self._creating_event_start = None 60 self._creating_event_end = None 61 self._n_tracks = 1 62 63 self._selected = None 64 self._selected_track = 0 65 self._pointer = TimelinePointer(0, self) 66 67 # collects all info on the current timeline state 68 self._tracks_info = {} 69 70 # Video playback controls 71 self._video_playing = False 72 self._video_fps = None 73 self._video_fps_min = None 74 self._video_fps_max = None 75 self._video_fps_inc = None 76 77 # TESTING 78 # self._data = [ 79 # TimelineDelta(29, 50, 'Teste', top=20, parent=self), TimelineDelta(70, 120, 'Teste', top=20, parent=self), 80 # TimelineDelta(29, 50, 'Ricardo', top=54, parent=self), TimelineDelta(70, 120, 'Ricardo', top=54, parent=self) 81 # ] 82 83 self._update_tracks_info()
84 85 ########################################################################## 86 #### HELPERS/FUNCTIONS ################################################### 87 ########################################################################## 88
89 - def x2frame(self, x): return int(x / self._scale)
90
91 - def frame2x(self, frame): return int(frame * self._scale)
92
93 - def removeSelected(self):
94 if self._selected != None and not self._selected.lock: 95 self._data.remove(self._selected) 96 self._selected = None 97 self._update_tracks_info() 98 self.repaint()
99
100 - def lockSelected(self):
101 if self._selected != None: 102 self._selected.lock = not self._selected.lock 103 self._update_tracks_info() 104 self.repaint()
105
106 - def selectDelta(self, x, y):
107 # Check if the timeline pointer was selected 108 if y <= 20: 109 if self._pointer.collide(x, y): 110 return self._pointer 111 else: 112 return None 113 # Check if the timeline periods were selected 114 for col in self._data: 115 if col.collide(x, y): 116 return col 117 return None
118
119 - def __drawTrackLines(self, painter, start, end):
120 # Draw only from pixel start to end 121 painter.setPen(QtCore.Qt.DashLine) 122 painter.setOpacity(0.3) 123 # Draw horizontal lines 124 for track in range(0, self.numberoftracks + 1): 125 y = (track * 34) + 18 126 painter.drawLine(start, y, end, y) 127 128 # Draw vertical lines 129 for x in range(start - (start % 100), end, 100): 130 painter.drawLine(x, 20, x, self.height()) 131 string = "%d" % (x / self._scale) 132 boundtext = painter.boundingRect(QtCore.QRectF(), string) 133 painter.drawText(x - boundtext.width() / 2, 15, string) 134 painter.setOpacity(1.0)
135
136 - def __drawTrackLabels(self, painter):
137 painter.setPen(QtCore.Qt.black) 138 painter.setOpacity(0.5) 139 140 for key, value in self._tracks_info.items(): 141 text = value[0] 142 x0 = self.visibleRegion().boundingRect().x() 143 xmax = self.visibleRegion().boundingRect().width() 144 text_length = painter.fontMetrics().width(text) 145 x = 10 146 y = (key * 34) + 30 147 painter.drawText(x, y, text) 148 # self.update() 149 painter.setOpacity(1.0)
150
151 - def importchart_csv(self, csvfileobject):
152 chart = TimelineChart( 153 self, csvfileobject, color=self._chartsColors[len(self._charts)]) 154 self._charts.append(chart)
155
156 - def import_csv(self, csvfileobject):
157 """ 158 Extracts info from a file object and stores it in memory in 159 order to display it on the timeline. 160 161 Refer to the `export` method to learn about input file format 162 and structure. 163 """ 164 # Clear previously stored info 165 self._tracks_info.clear() 166 del self._data[:] 167 168 # File flow controls 169 get_track = True 170 get_event = False 171 for row in csvfileobject: 172 # Import events files 173 if row[0] == "": 174 # sep line has been found, get ready to get a track line next 175 get_track = True 176 get_event = False 177 continue 178 179 # Get track info from the row 180 if get_track: 181 track_id = int(row[0]) 182 track_label = row[5] 183 track_color = QtGui.QColor(row[4]) 184 self._tracks_info[track_id] = [track_label, track_color, []] 185 get_track = False 186 get_event = True 187 continue 188 # Get event info from the row 189 if get_event: 190 event = TimelineDelta(0, parent=self) 191 event.properties = row + [track_id] 192 # add to `_data` for compatibility reasons 193 self._data.append(event) 194 self._tracks_info[track_id][2].append(event)
195
196 - def export_csv(self, csvfileobject):
197 """ 198 Processes all timeline data in an arranged format to be written 199 in a CSV file. 200 201 202 Current file structure: 203 ======================= 204 205 --- CSV FILE BEGIN --- 206 Track info line 207 Event info line 208 Event info line 209 ... 210 Empty line 211 Track info line 212 Event info line 213 Event info line 214 ... 215 Empty line 216 Track info line 217 Event info line 218 Event info line 219 ... 220 Empty line 221 --- CSV FILE END --- 222 223 224 Track info line format: 225 ======================= 226 227 | Id | Total # of events in this track | | | Color | Label | 228 229 230 Event info line format: 231 ======================= 232 233 | Lock status | Begin frame | End frame | Comment | Color | | 234 """ 235 236 # Transform the info in the dict to a nested list to be written 237 outputstream = [] 238 for key, value in self._tracks_info.items(): 239 track_id = key 240 track_label = value[0] 241 track_color = value[1].name() 242 track_events = value[2] 243 track_events_total = len(value[2]) 244 245 row_track_info = [track_id, 246 track_events_total, 247 "", 248 "", 249 track_color, 250 track_label] 251 252 outputstream.append(row_track_info) 253 254 for event in track_events: 255 row_event = event.properties[:5] # TODO access individually 256 outputstream.append(row_event) 257 258 row_sep = ["" for item in range(len(row_track_info))] 259 outputstream.append(row_sep) 260 261 # Write outputstream generated above to CSV file object 262 for row in outputstream: 263 csvfileobject.writerow(row)
264 265 # FIXME To be replaced by the functions above 266 # def exportcsv(self, csvspan): 267 # self._update_tracks_info() 268 # self._data = sorted(self._data, key = lambda x : (x._begin, x._end) ) 269 # for period in self._data: 270 # csvspan.writerow(period.row) 271 272 # def importcsv(self, csvspan): 273 # for row in csvspan: 274 # time = TimelineDelta(0, parent= self) 275 # time.row = row 276 # self._data.append(time) 277 # self._update_tracks_info() 278
279 - def cleanCharts(self):
280 self._charts = [] 281 self.repaint()
282
283 - def clean(self):
284 self._data = [] 285 self._charts = [] 286 self._selected = None 287 self._n_tracks = 1 288 self._update_tracks_info() 289 self.repaint()
290
291 - def cleanLine(self):
292 if self._selected is not None: 293 track = self._selected.track 294 list2delete = [] 295 for x in self._data: 296 if x.track == track: 297 list2delete.append(x) 298 for x in list2delete: 299 self._data.remove(x) 300 self._selected = None 301 self._update_tracks_info() 302 else: 303 QtGui.QMessageBox.about( 304 self, "Error", "You must select a timebar first") 305 return
306
307 - def addPeriod(self, value, track=0, color=None):
308 """Adds an annotated interval.""" 309 begin, end, title = value 310 period = TimelineDelta(begin, end, title=title, parent=self) 311 period.track = track 312 if color is not None: 313 period.color = color 314 else: 315 period.color = self._tracks_info[track][1] 316 self._data.append(period)
317
318 - def _update_tracks_info(self):
319 """ 320 This method updates the dict `_tracks_info` which contains all 321 info stored in the timeline. 322 323 Currently, this method is called is the following situations: 324 - Class initialization 325 - Event added by double clicking the timeline 326 - Everything is cleaned 327 - A line is cleaned 328 - After an event is removed/locked/unlocked 329 #TODO- After editing a timeline line entry 330 #TODO- After editing an event (TimelineDelta object) 331 - After importing from a file 332 - Before exporting to a file 333 334 This dictionary stores info in the format shown below: 335 {line_numer : [ line_label, line_color, [data] ] } 336 """ 337 338 d = self._tracks_info 339 340 # Initialize a dict entry if a new line gets filled 341 for i in range(self._n_tracks): 342 if i not in d.keys(): 343 self._defaulttracklabel = "Right click to attribute a behavior" 344 d[i] = [self._defaulttracklabel, self.color, []] 345 346 # Gets data events track number and stores them accordingly 347 # in the dict. 348 for event in self._data: 349 tracknumber = event.track 350 if event not in d[tracknumber][2]: 351 d[tracknumber][2].append(event) 352 353 # When an event is deleted, it must disappear also from the dict. 354 # We need to look for events in the dict that disappeared from 355 # the data list. 356 for key, value in d.items(): 357 value[2] = [event for event in value[2] if event in self._data] 358 359 # self.__print_tracks_info() 360 self.repaint()
361
362 - def __print_tracks_info(self):
363 """Prints tracks info on screen.""" 364 d = self._tracks_info 365 if d: # returns True if dict is not empty 366 print("{0:=>79}".format(" TIMELINE INFO")) 367 print("Number of active tracks:", self._n_tracks) 368 print("Total number of events created:", len(self._data)) 369 print("Tracks:") 370 print("{0:>8} {1:^50} {2:^7} {3:>11}".format("ID", 371 "Label", 372 "Color", 373 "# of events")) 374 print("{0:-<79}".format(" ")) 375 for key, value in d.items(): 376 if self._selected_track == key: 377 sel = ">" 378 else: 379 sel = "" 380 print("{0:>4}{1:>4}{2:^50}{3:7}{4:^11}".format(sel, 381 key, 382 value[0], 383 value[1].name(), 384 len(value[2]))) 385 print() 386 else: 387 print("Nothing here.")
388 389 ########################################################################## 390 #### EVENTS ############################################################## 391 ########################################################################## 392
393 - def paintEvent(self, e):
394 super(TimelineWidget, self).paintEvent(e) 395 396 painter = QtGui.QPainter() 397 painter.begin(self) 398 painter.setRenderHint(QtGui.QPainter.Antialiasing) 399 painter.setFont(QtGui.QFont('Decorative', 8)) 400 401 start = self._scroll.horizontalScrollBar().sliderPosition() 402 end = start + self.parent().width() + 50 403 404 #Draw graphs ########################################################## 405 if len(self._charts) > 0: 406 painter.setPen(QtCore.Qt.black) 407 middle = self.height() // 2 408 painter.setOpacity(0.1) 409 painter.drawLine(start, middle, end, middle) 410 411 for chart in self._charts: 412 chart.draw(painter, start, end, 0, self.height()) 413 #End draw graph ####################################################### 414 415 self.__drawTrackLines(painter, start, end) 416 # Draw track labels 417 self.__drawTrackLabels(painter) 418 419 # Draw delta bars 420 for time in self._data: 421 painter.setBrush(time.color) 422 time.draw(painter) 423 424 # Draw the selected element 425 if self._selected != None: 426 painter.setBrush(QtGui.QColor(255, 0, 0)) 427 self._selected.draw(painter, showvalues=True) 428 429 # Draw the time pointer 430 self._pointer.draw(painter) 431 432 painter.end()
433
434 - def mouseDoubleClickEvent(self, event):
435 if self._selected is not None and self._selected != self._pointer and self._selected.collide(event.x(), event.y()): 436 self._selected.showEditWindow() 437 elif event.y() > 20: 438 top = (event.y() - 20) // 34 439 y = top * 34 + 20 440 x = event.x() / self._scale 441 # time = TimelineDelta(x, x + 50 / self._scale, title='', top=y, parent=self) 442 time = TimelineDelta(x, x + 10, title='', top=y, parent=self) 443 self._data.append(time) 444 self._selected = time 445 self._selected_track = self._selected.track 446 self._update_tracks_info() 447 self.repaint()
448
449 - def keyReleaseEvent(self, event):
450 super(TimelineWidget, self).keyReleaseEvent(event) 451 452 # Control video playback using the space bar to Play/Pause 453 if event.key() == QtCore.Qt.Key_Space: 454 self._video_playing = not self._video_playing 455 self.playVideoEvent() 456 self.repaint() 457 458 # Increase video frame rate (FPS) by 5 459 if event.key() == QtCore.Qt.Key_Plus: 460 if self._video_fps_min <= (self.fps + self._video_fps_inc) <= self._video_fps_max: 461 self.fps += self._video_fps_inc 462 else: 463 pass 464 self.fpsChangeEvent() 465 self.repaint() 466 467 # Decrease video frame rate (FPS) by 5 468 if event.key() == QtCore.Qt.Key_Minus: 469 if self._video_fps_min <= (self.fps - self._video_fps_inc) <= self._video_fps_max: 470 self.fps -= self._video_fps_inc 471 else: 472 pass 473 self.fpsChangeEvent() 474 self.repaint() 475 476 # Jumps 20 seconds forward 477 if event.key() == QtCore.Qt.Key_C: 478 self._pointer.position += 10 * self.fps 479 self.__checkPositionIsVisible(self.position) 480 self.repaint() 481 482 # Jumps 20 seconds backwards 483 if event.key() == QtCore.Qt.Key_Z: 484 self._pointer.position -= 10 * self.fps 485 self.__checkPositionIsVisible(self.position) 486 self.repaint() 487 488 # Jumps 1 frame forward 489 if event.key() == QtCore.Qt.Key_D: 490 self._pointer.position += 1 491 self.__checkPositionIsVisible(self.position) 492 self.repaint() 493 494 # Jumps 1 frame backwards 495 if event.key() == QtCore.Qt.Key_A: 496 self._pointer.position -= 1 497 self.__checkPositionIsVisible(self.position) 498 self.repaint() 499 500 if self._selected is not None: 501 modifier = int(event.modifiers()) 502 503 # Move the event (or the pointer) left 504 if modifier == QtCore.Qt.ControlModifier and event.key() == QtCore.Qt.Key_Left: 505 self._selected.move(-1, 0) 506 self.repaint() 507 508 # Move the event (or the pointer) right 509 if modifier == QtCore.Qt.ControlModifier and event.key() == QtCore.Qt.Key_Right: 510 self._selected.move(1, 0) 511 self.repaint() 512 513 if self._selected != self._pointer: 514 # Delete the selected event 515 if event.key() == QtCore.Qt.Key_Delete: 516 self.removeSelected() 517 518 # Lock or unlock an event 519 if event.key() == QtCore.Qt.Key_L: 520 self.lockSelected() 521 522 # Move the event up 523 if modifier == QtCore.Qt.ControlModifier and event.key() == QtCore.Qt.Key_Up: 524 self._selected.move(0, self._selected._top - 34) 525 self.repaint() 526 527 # Move the event down 528 if modifier == QtCore.Qt.ControlModifier and event.key() == QtCore.Qt.Key_Down: 529 self._selected.move(0, self._selected._top + 34) 530 self.repaint() 531 532 # Move the event end left 533 if modifier == int(QtCore.Qt.ShiftModifier | QtCore.Qt.ControlModifier) and event.key() == QtCore.Qt.Key_Left: 534 self._selected.moveEnd(-1) 535 self.repaint() 536 537 # Move the event end right 538 if modifier == int(QtCore.Qt.ShiftModifier | QtCore.Qt.ControlModifier) and event.key() == QtCore.Qt.Key_Right: 539 self._selected.moveEnd(1) 540 self.repaint() 541 542 # Move the event begin left 543 if modifier == QtCore.Qt.ShiftModifier and event.key() == QtCore.Qt.Key_Left: 544 self._selected.moveBegin(-1) 545 self.repaint() 546 547 # Move the event begin right 548 if modifier == QtCore.Qt.ShiftModifier and event.key() == QtCore.Qt.Key_Right: 549 self._selected.moveBegin(1) 550 self.repaint() 551 552 else: 553 # Keybinds to create an event at current frame 554 if event.key() == QtCore.Qt.Key_S and not self._creating_event: 555 # Start 556 self._creating_event_start = self._pointer.frame 557 self._creating_event = True 558 559 # TODO Add some indicator that an event is being recorded, like 560 # using the track selector circle to become red 561 562 return 563 564 if event.key() == QtCore.Qt.Key_S and self._creating_event: 565 # End, must be followed right after Start key and have no 566 # effect otherwise 567 self._creating_event_end = self._pointer.frame 568 569 start = self._creating_event_start 570 end = self._creating_event_end 571 comment = "" 572 573 if end > start: 574 self.addPeriod((start, end, comment), self._selected_track) 575 self._update_tracks_info() 576 self.repaint() 577 self._creating_event = False 578 else: 579 print("Event auto creation aborted.") 580 self._creating_event = False
581
582 - def mousePressEvent(self, event):
583 # Select the track 584 track_clicked = (event.y() - 20) // 34 585 if track_clicked in self._tracks_info.keys(): 586 self._selected_track = track_clicked 587 else: 588 pass 589 590 # Select the period bar 591 self._selected = self.selectDelta(event.x(), event.y()) 592 self._moving = False 593 self._resizingBegin = False 594 self._resizingEnd = False 595 596 if self._selected is not None: 597 # Select the action 598 if event.buttons() == QtCore.Qt.LeftButton: 599 if self._selected.canSlideEnd(event.x(), event.y()): 600 self._resizingEnd = True 601 elif self._selected.canSlideBegin(event.x(), event.y()): 602 self._resizingBegin = True 603 elif self._selected.collide(event.x(), event.y()): 604 self._moving = True 605 if event.y() <= 20 and not self._moving: 606 self._pointer.position = event.x() 607 608 self._update_tracks_info() 609 self.repaint()
610
611 - def mouseMoveEvent(self, event):
612 super(TimelineWidget, self).mouseMoveEvent(event) 613 614 # Do nothing if no event bar is selected 615 if self._selected is None: 616 return 617 618 # Set cursors 619 if self._selected.canSlideBegin(event.x(), event.y()) or self._selected.canSlideEnd(event.x(), event.y()): 620 self.setCursor(QtGui.QCursor(QtCore.Qt.SizeHorCursor)) 621 elif self._selected.collide(event.x(), event.y()): 622 self.setCursor(QtGui.QCursor(QtCore.Qt.SizeAllCursor)) 623 else: 624 self.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) 625 626 if self._selected is None: 627 return 628 629 if event.buttons() == QtCore.Qt.LeftButton: 630 if self._lastMouseY is not None: 631 diff = event.x() - self._lastMouseY 632 if diff != 0: 633 if self._moving: 634 self._selected.move(diff, event.y()) 635 elif self._resizingBegin: 636 self._selected.moveBegin(diff) 637 elif self._resizingEnd: 638 self._selected.moveEnd(diff) 639 self.repaint() 640 self._lastMouseY = event.x()
641
642 - def mouseReleaseEvent(self, event):
643 self._lastMouseY = None
644
645 - def trackInPosition(self, x, y):
646 return (y - 30) // 34
647
648 - def playVideoEvent(self):
649 pass
650
651 - def fpsChangeEvent(self):
652 pass
653 654 ########################################################################## 655 #### PROPERTIES ########################################################## 656 ########################################################################## 657
658 - def __checkPositionIsVisible(self, value):
659 playerPos = self.frame2x(value) 660 scrollLimit = self._scroll.horizontalScrollBar( 661 ).sliderPosition() + self.parent().width() - 50 662 if playerPos > scrollLimit: 663 newPos = playerPos - self.parent().width() + 50 664 self._scroll.horizontalScrollBar().setSliderPosition(newPos) 665 if playerPos < self._scroll.horizontalScrollBar().sliderPosition(): 666 self._scroll.horizontalScrollBar().setSliderPosition(playerPos)
667 668 @property
669 - def scroll(self): return self._scroll.horizontalScrollBar()
670 671 @property
672 - def position(self): return self._pointer._position
673 674 @position.setter
675 - def position(self, value):
676 self._pointer._position = value 677 ####################################################################### 678 # Check if the player position is inside the scroll 679 # if is not in, update the scroll position 680 self.__checkPositionIsVisible(value) 681 ####################################################################### 682 self.repaint()
683 684 @property
685 - def scale(self): return self._scale
686 687 @scale.setter
688 - def scale(self, value):
689 self._scale = value 690 self.repaint()
691 692 @property
693 - def color(self): return self._defautcolor
694 695 @color.setter
696 - def color(self, value): self._defautcolor = value
697 698 @property
699 - def numberoftracks(self): return self._n_tracks
700 701 @numberoftracks.setter
702 - def numberoftracks(self, value):
703 self._n_tracks = value 704 y = value * 34 + 20 705 if y + 40 > self.height(): 706 self.setMinimumHeight(y + 40)
707 708 # Video playback properties 709 @property
710 - def isPlaying(self): return self._video_playing
711 712 @property
713 - def fps(self): return self._video_fps
714 715 @fps.setter
716 - def fps(self, value): self._video_fps = value
717