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