def onAppStarted(self): """ The module has been loaded """ self.tree = None self.cfgWin = None self.scrolled = Gtk.ScrolledWindow() self.treeState = prefs.get(__name__, 'saved-states', None) self.scrolled.set_shadow_type(Gtk.ShadowType.IN) self.scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) self.scrolled.show() left_vbox = prefs.getWidgetsTree().get_object('vbox3') left_vbox.pack_start(self.scrolled, True, True, 0) self.static_paths = ['/', consts.dirBaseUsr] self.displaying_results = False self.populate_tree() music_paths = self.get_music_paths_from_tree() modules.postMsg(consts.MSG_EVT_MUSIC_PATHS_CHANGED, {'paths': music_paths}) self.tree.connect('drag-begin', self.onDragBegin)
def onAppStarted(self): """ Real initialization function, called when this module has been loaded """ self.currTrackLength = 0 # Widgets wTree = prefs.getWidgetsTree() self.btnPlay = wTree.get_object('btn-play') self.btnNext = wTree.get_object('btn-next') self.btnPrev = wTree.get_object('btn-previous') self.sclSeek = wTree.get_object('scl-position') # GTK handlers self.btnNext.connect( 'clicked', lambda widget: modules.postMsg(consts.MSG_CMD_NEXT)) self.btnPrev.connect( 'clicked', lambda widget: modules.postMsg(consts.MSG_CMD_PREVIOUS)) self.btnPlay.connect( 'clicked', lambda widget: modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE)) self.sclSeek.connect('change-value', self.onSeekChangingValue) self.sclSeek.connect('value-changed', self.onSeekValueChanged) self.sclSeek.hide() # Add preferences button. preferences_img = Gtk.Image.new_from_icon_name( 'preferences-system', Gtk.IconSize.SMALL_TOOLBAR) preferences_button = Gtk.ToolButton.new(preferences_img, None) toolbar_hbox = wTree.get_object('hbox4') toolbar_hbox.pack_end(preferences_button, False, False, 0) # Move button to the right. toolbar_hbox.reorder_child(preferences_button, 0) preferences_button.connect('clicked', lambda item: modules.showPreferences()) preferences_button.show_all()
def remove(self, iter=None): """ Remove the given track, or the selection if iter is None """ hadMark = self.tree.hasMark() iters = [iter] if iter else list(self.tree.iterSelectedRows()) prev_iter = self.tree.get_prev_iter_or_parent(iters[0]) # reverse list, so that we remove children before their fathers for iter in reversed(iters): track = self.tree.getTrack(iter) if track: self.playtime -= track.getLength() self.tree.removeRow(iter) self.tree.selection.unselect_all() if hadMark and not self.tree.hasMark(): modules.postMsg(consts.MSG_CMD_STOP) # Select new track when old selected is deleted if prev_iter: self.tree.select(prev_iter) else: first_iter = self.tree.get_first_iter() if first_iter: self.tree.select(first_iter) self.onListModified()
def onSearch(self, query): self.should_stop = False self.found_something = False # Transform whitespace-separated query into OR-regex. regex = re.compile('|'.join(tools.get_pattern(word) for word in query.split()), re.IGNORECASE) for dir in self.get_search_paths() + [consts.dirBaseUsr]: # Check if search has been aborted during filtering if self.should_stop: break # Only search in home folder if we haven't found anything yet. if dir == consts.dirBaseUsr and self.found_something: break results = self.search_dir(dir, query) # Check if search has been aborted during searching if results is None or self.should_stop: break dirs, files = self.filter_results(results, dir, regex) if not self.should_stop and (dirs or files): self.found_something = True modules.postMsg(consts.MSG_EVT_SEARCH_APPEND, {'results': (dirs, files), 'query': query}) modules.postMsg(consts.MSG_EVT_SEARCH_END)
def __onTrackEnded(self, error): """ Called to signal eos and errors """ self.nextURI = None if error: modules.postMsg(consts.MSG_EVT_TRACK_ENDED_ERROR) else: modules.postMsg(consts.MSG_EVT_TRACK_ENDED_OK)
def on_searchbox_activate(self, _entry): self.stop_searches() query = self.searchbox.get_text().strip() if len(query) < MIN_CHARS: msg = 'Search term has to have at least %d characters' % MIN_CHARS logging.info(msg) return query = self.searchbox.get_text() logging.info('Query: %s' % query) modules.postMsg(consts.MSG_EVT_SEARCH_START, {'query': query})
def onStop(self): """ Stop playing """ self.__stopUpdateTimer() self.player.stop() self.nextURI = None if self.playbackTimer is not None: GObject.source_remove(self.playbackTimer) modules.postMsg(consts.MSG_EVT_STOPPED)
def updateTimerHandler(self): """ Regularly called during playback (can be paused) """ if self.player.isPlaying(): position = self.player.getPosition() remaining = self.player.getDuration() - position modules.postMsg(consts.MSG_EVT_TRACK_POSITION, {'seconds': int(position // 1000000000)}) if remaining < 5000000000 and self.nextURI is None: modules.postMsg(consts.MSG_EVT_NEED_BUFFER) return True
def play(self, path=None): """ Replace/extend the tracklist If 'path' is None, use the current selection """ if path is None: track_paths = [ row[ROW_FULLPATH] for row in self.tree.getSelectedRows() ] else: track_paths = [self.tree.getRow(path)[ROW_FULLPATH]] modules.postMsg(consts.MSG_EVT_LOAD_TRACKS, {'paths': track_paths})
def onListModified(self): """ Some rows have been added/removed/moved """ # Getting the trackdir takes virtually no time, so we can do it on every # paylist change tracks = self.getTrackDir() self.playtime = tracks.get_playtime() modules.postMsg( consts.MSG_EVT_NEW_TRACKLIST, {'tracks': tracks, 'playtime': self.playtime}) if self.tree.hasMark(): modules.postMsg( consts.MSG_EVT_TRACK_MOVED, {'hasPrevious': self.__hasPreviousTrack(), 'hasNext': self.__hasNextTrack()})
def onTogglePause(self): """ Switch between play/pause """ if self.player.isPaused(): if self.queuedSeek is not None: self.player.seek(self.queuedSeek * 1000000000) self.queuedSeek = None self.player.play() modules.postMsg(consts.MSG_EVT_UNPAUSED) elif self.player.isPlaying(): if self.playbackTimer is not None: GObject.source_remove(self.playbackTimer) self.player.pause() modules.postMsg(consts.MSG_EVT_PAUSED)
def onScaleValueChanged(self, scale, idx): """ The user has moved one of the scales """ # Add a 'custom' entry to the presets if needed if self.preset is not None: self.preset = None prefs.set(__name__, 'preset', self.preset) self.combo.handler_block_by_func(self.onPresetChanged) self.comboStore.insert(0, (False, _('Custom'), None)) self.comboStore.insert(1, (True, '', None)) self.combo.set_active(0) self.combo.handler_unblock_by_func(self.onPresetChanged) self.lvls[idx] = scale.get_value() prefs.set(__name__, 'levels', self.lvls) modules.postMsg(consts.MSG_CMD_SET_EQZ_LVLS, {'lvls': self.lvls})
def onDragBegin(self, tree, context): """ A drag'n'drop operation has begun. Pass the paths to the track tree to let it decide whether to collapse directories. """ paths = [row[ROW_FULLPATH] for row in tree.getSelectedRows()] modules.postMsg(consts.MSG_CMD_FILE_EXPLORER_DRAG_BEGIN, {'paths': paths}) # Preload the tracks to speedup their addition to the playlist. import threading crawler = threading.Thread(target=media.preloadTracks, args=(paths, )) crawler.start()
def onShowPopupMenu(self, tree, button, time, path): # Keep reference after method exits. self.popup_menu = Gtk.Menu() # Remove remove = Gtk.MenuItem.new_with_label(_('Remove')) self.popup_menu.append(remove) if path is None: remove.set_sensitive(False) else: remove.connect('activate', lambda item: self.remove()) # Clear clear = Gtk.MenuItem.new_with_label(_('Clear Playlist')) self.popup_menu.append(clear) # Save to m3u export_m3u = Gtk.MenuItem.new_with_label(_('Export playlist to file')) self.popup_menu.append(export_m3u) # Save to dir export_dir = Gtk.MenuItem.new_with_label(_('Export playlist to directory')) self.popup_menu.append(export_dir) if len(tree.store) == 0: clear.set_sensitive(False) export_m3u.set_sensitive(False) export_dir.set_sensitive(False) else: clear.connect('activate', lambda item: modules.postMsg(consts.MSG_CMD_TRACKLIST_CLR)) export_m3u.connect('activate', lambda item: self.export_playlist_to_m3u()) export_dir.connect('activate', lambda item: self.export_playlist_to_dir()) self.popup_menu.show_all() self.popup_menu.popup(None, None, None, None, button, time)
def onMediaKey(self, appName, action): """ A media key has been pressed """ if action == 'Stop': modules.postMsg(consts.MSG_CMD_STOP) elif action == 'Next': modules.postMsg(consts.MSG_CMD_NEXT) elif action == 'Previous': modules.postMsg(consts.MSG_CMD_PREVIOUS) elif action in ['Play', 'Pause']: modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE)
def set(self, tracks, playNow): """ Replace the tracklist, clear it if tracks is None """ self.playtime = 0 if type(tracks) == list: trackdir = media.TrackDir(None, flat=True) trackdir.tracks = tracks tracks = trackdir if self.tree.hasMark() and ((not playNow) or (tracks is None) or tracks.empty()): modules.postMsg(consts.MSG_CMD_STOP) self.tree.clear() if tracks is not None and not tracks.empty(): self.insert(tracks, playNow=playNow) self.tree.collapse_all() self.onListModified()
def onModUnloaded(self): """ The module has been unloaded """ if self.currTrack is not None: modules.postMsg( consts.MSG_CMD_SET_COVER, { 'track': self.currTrack, 'pathThumbnail': None, 'pathFullSize': None }) # Delete covers that have been generated by this module for covers in self.coverMap.values(): if os.path.exists(covers[CVR_THUMB]): os.remove(covers[CVR_THUMB]) if os.path.exists(covers[CVR_FULL]): os.remove(covers[CVR_FULL]) self.coverMap = None # Delete blacklist self.coverBlacklist = None
def onTrackEnded(self, withError): """ The current track has ended, jump to the next one if any """ current_iter = self.tree.getMark() # If an error occurred with the current track, flag it as such if withError and current_iter: self.tree.setItem(current_iter, ROW_ICO, icons.errorMenuIcon()) # Find the next 'playable' track (not already flagged) next = self.__getNextTrackIter() if next: send_play_msg = True if current_iter: track_name = self.tree.getTrack(current_iter).getURI() send_play_msg = (track_name != self.bufferedTrack) self.jumpTo(next, sendPlayMsg=send_play_msg, forced=False) self.bufferedTrack = None return self.bufferedTrack = None modules.postMsg(consts.MSG_CMD_STOP)
def jumpTo(self, iter, sendPlayMsg=True, forced=True): """ Jump to the track located at the given iter """ if not iter: return mark = self.tree.getMark() if mark: self.set_track_playing(mark, False) self.tree.setMark(iter) self.tree.scroll(iter) # Check track track = self.tree.getTrack(iter) if not track: # Row may be a directory self.jumpTo(self.__getNextTrackIter()) return self.set_track_playing(iter, True) self.paused = False if sendPlayMsg: modules.postMsg(consts.MSG_CMD_PLAY, {'uri': track.getURI(), 'forced': forced}) modules.postMsg(consts.MSG_EVT_NEW_TRACK, {'track': track}) modules.postMsg(consts.MSG_EVT_TRACK_MOVED, {'hasPrevious': self.__hasPreviousTrack(), 'hasNext': self.__hasNextTrack()})
def on_add_dir(self, widget): parent = prefs.getWidgetsTree().get_object('win-main') path = fileChooser.openDirectory(parent, _('Choose a directory')) if path is None: return if os.path.isdir(path): if path in self.static_paths: errorMsgBox( None, _('Invalid Folder'), _('You cannot add your root or home folder to the music directories' )) return self.add_dir(path) music_paths = self.get_music_paths_from_tree() modules.postMsg(consts.MSG_EVT_MUSIC_PATHS_CHANGED, {'paths': music_paths}) self.set_info_text() else: errorMsgBox( None, _('This path does not exist'), '"%s"\n' % path + _('Please select an existing directory.'))
def onKeyboard(self, list, event): """ Keyboard shortcuts """ keyname = Gdk.keyval_name(event.keyval) if keyname == 'Delete': self.remove() elif keyname == 'Return': self.jumpTo(self.tree.getFirstSelectedRow()) elif keyname == 'space': modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE) elif keyname == 'Escape': modules.postMsg(consts.MSG_CMD_STOP) elif keyname == 'Left': modules.postMsg(consts.MSG_CMD_STEP, {'seconds': -5}) elif keyname == 'Right': modules.postMsg(consts.MSG_CMD_STEP, {'seconds': 5})
def timerFunc(self): """ Move a bit the scales to their target value """ isFinished = True # Move the scales a bit for i in range(10): currLvl = self.scales[i].get_value() targetLvl = self.targetLvls[i] difference = targetLvl - currLvl if abs(difference) <= 0.25: newLvl = targetLvl else: newLvl = currLvl + (difference / 8.0) isFinished = False self.lvls[i] = newLvl self.scales[i].set_value(newLvl) # Set the equalizer to the new levels modules.postMsg(consts.MSG_CMD_SET_EQZ_LVLS, {'lvls': self.lvls}) if isFinished: self.timer = None prefs.set(__name__, 'levels', self.lvls) # Make sure labels are up to date (sometimes they aren't when we're done with the animation) # Also unblock the handlers for i in range(10): self.scales[i].queue_draw() self.scales[i].handler_unblock_by_func( self.onScaleValueChanged) return False return True
def onSkipTrack(self, _notification, _action): """ The user wants to skip the current track """ if self.hasNext: modules.postMsg(consts.MSG_CMD_NEXT) else: modules.postMsg(consts.MSG_CMD_STOP)
def onAppStarted(self): """ The application has started """ self.modInit() modules.postMsg(consts.MSG_CMD_ENABLE_EQZ) modules.postMsg(consts.MSG_CMD_SET_EQZ_LVLS, {'lvls': self.lvls})
def onLoadTracks(self, paths): modules.postMsg(consts.MSG_CMD_TRACKLIST_ADD, { 'tracks': media.getTracks(paths), 'playNow': True })
def onAppStarted(self): """ This is the real initialization function, called when the module has been loaded """ wTree = tools.prefs.getWidgetsTree() self.playtime = 0 self.bufferedTrack = None # Retrieve widgets self.window = wTree.get_object('win-main') columns = (('', [(Gtk.CellRendererPixbuf(), GdkPixbuf.Pixbuf), (Gtk.CellRendererText(), GObject.TYPE_STRING)], True), (None, [(None, GObject.TYPE_PYOBJECT)], False), ) self.tree = TrackTreeView(columns, use_markup=True) self.tree.enableDNDReordering() target = Gtk.TargetEntry.new(*DND_INTERNAL_TARGET) targets = Gtk.TargetList.new([target]) self.tree.setDNDSources(targets) wTree.get_object('scrolled-tracklist').add(self.tree) # GTK handlers self.tree.connect('exttreeview-button-pressed', self.onMouseButton) self.tree.connect('tracktreeview-dnd', self.onDND) self.tree.connect('key-press-event', self.onKeyboard) self.tree.get_model().connect('row-deleted', self.onRowDeleted) _options, args = prefs.getCmdLine() self.savedPlaylist = os.path.join(consts.dirCfg, 'saved-playlist') self.paused = False # Populate the playlist with the saved playlist dump = None if os.path.exists(self.savedPlaylist): try: dump = pickleLoad(self.savedPlaylist) except (EOFError, ImportError, IOError): msg = '[%s] Unable to restore playlist from %s\n\n%s' log.logger.error(msg % (MOD_INFO[modules.MODINFO_NAME], self.savedPlaylist, traceback.format_exc())) if dump: self.restoreTreeDump(dump) log.logger.info('[%s] Restored playlist' % MOD_INFO[modules.MODINFO_NAME]) self.tree.collapse_all() self.select_last_played_track() self.onListModified() commands, args = tools.separate_commands_and_tracks(args) # Add commandline tracks to the playlist if args: log.logger.info('[%s] Filling playlist with files given on command line' % MOD_INFO[modules.MODINFO_NAME]) tracks = media.getTracks([os.path.abspath(arg) for arg in args]) playNow = 'stop' not in commands and 'pause' not in commands modules.postMsg(consts.MSG_CMD_TRACKLIST_ADD, {'tracks': tracks, 'playNow': playNow}) elif 'play' in commands: modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE) # Automatically save the content at regular intervals GObject.timeout_add_seconds(SAVE_INTERVAL, self.save_track_tree)
def onBufferingNeeded(self): """ The current track is close to its end, so we try to buffer the next one to avoid gaps """ where = self.__getNextTrackIter() if where: self.bufferedTrack = self.tree.getItem(where, ROW_TRK).getURI() modules.postMsg(consts.MSG_CMD_BUFFER, {'uri': self.bufferedTrack})
def onSeekValueChanged(self, range): """ The user has moved the seek slider """ modules.postMsg(consts.MSG_CMD_SEEK, {'seconds': int(range.get_value())})
def on_searchbox_changed(self, _entry): if self.searchbox.get_text().strip() == '': self.stop_searches() modules.postMsg(consts.MSG_EVT_SEARCH_RESET, {})
def on_remove_dir(self, widget, path): self.tree.removeRow(path) music_paths = self.get_music_paths_from_tree() modules.postMsg(consts.MSG_EVT_MUSIC_PATHS_CHANGED, {'paths': music_paths}) self.set_info_text()