Ticket #3714: 0001-Modularization-of-classes.patch
File 0001-Modularization-of-classes.patch, 91.2 KB (added by humitos, 11 years ago) |
---|
-
deleted file ControlToolbar.py
From a9d92381d222f8c5fff29c2b6d90f78820c27ea2 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann <humitos@gmail.com> Date: Mon, 14 Jan 2013 12:42:35 -0300 Subject: [PATCH Jukebox 1/2] Modularization of classes - Put each class in a different .py file - Rename jukeboxactivity.py to activity.py to make this more standard Signed-off-by: Manuel Kaufmann <humitos@gmail.com> --- ControlToolbar.py | 169 ---------- activity.py | 678 +++++++++++++++++++++++++++++++++++++++ activity/activity.info | 2 +- controls.py | 134 ++++++++ jukeboxactivity.py | 841 ------------------------------------------------- player.py | 182 +++++++++++ playlist.py | 137 ++++++++ viewtoolbar.py | 171 ++++++++++ widgets.py | 137 -------- 9 files changed, 1303 insertions(+), 1148 deletions(-) delete mode 100644 ControlToolbar.py create mode 100644 activity.py create mode 100644 controls.py delete mode 100644 jukeboxactivity.py create mode 100644 player.py create mode 100644 playlist.py create mode 100644 viewtoolbar.py delete mode 100644 widgets.py diff --git a/ControlToolbar.py b/ControlToolbar.py deleted file mode 100644 index 2205bde..0000000
+ - 1 # Copyright (C) 2007 Andy Wingo <wingo@pobox.com>2 # Copyright (C) 2007 Red Hat, Inc.3 # Copyright (C) 2008 Kushal Das <kushal@fedoraproject.org>4 # This program is free software; you can redistribute it and/or modify5 # it under the terms of the GNU General Public License as published by6 # the Free Software Foundation; either version 2 of the License, or7 # (at your option) any later version.8 #9 # This program is distributed in the hope that it will be useful,10 # but WITHOUT ANY WARRANTY; without even the implied warranty of11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12 # GNU General Public License for more details.13 #14 # You should have received a copy of the GNU General Public License15 # along with this program; if not, write to the Free Software16 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA17 18 import logging19 20 from gettext import gettext as _21 22 from gi.repository import GObject23 from gi.repository import Gtk24 25 from sugar3.graphics.toolbutton import ToolButton26 from sugar3.graphics.toggletoolbutton import ToggleToolButton27 28 29 class ViewToolbar(Gtk.Toolbar):30 __gtype_name__ = 'ViewToolbar'31 32 __gsignals__ = {33 'go-fullscreen': (GObject.SignalFlags.RUN_FIRST,34 None,35 ([])),36 'toggle-playlist': (GObject.SignalFlags.RUN_FIRST,37 None,38 ([]))39 }40 41 def __init__(self):42 GObject.GObject.__init__(self)43 44 self._show_playlist = ToggleToolButton('view-list')45 self._show_playlist.set_active(True)46 self._show_playlist.set_tooltip(_('Show Playlist'))47 self._show_playlist.connect('toggled', self._playlist_toggled_cb)48 self.insert(self._show_playlist, -1)49 self._show_playlist.show()50 51 self._fullscreen = ToolButton('view-fullscreen')52 self._fullscreen.set_tooltip(_('Fullscreen'))53 self._fullscreen.connect('clicked', self._fullscreen_cb)54 self.insert(self._fullscreen, -1)55 self._fullscreen.show()56 57 def _fullscreen_cb(self, button):58 self.emit('go-fullscreen')59 60 def _playlist_toggled_cb(self, button):61 self.emit('toggle-playlist')62 63 64 class Control(GObject.GObject):65 """Class to create the Control (play) toolbar"""66 67 def __init__(self, toolbar, jukebox):68 GObject.GObject.__init__(self)69 70 self.toolbar = toolbar71 self.jukebox = jukebox72 73 self.open_button = ToolButton('list-add')74 self.open_button.set_tooltip(_('Add track'))75 self.open_button.show()76 self.open_button.connect('clicked', jukebox.open_button_clicked_cb)77 self.toolbar.insert(self.open_button, -1)78 79 erase_playlist_entry_btn = ToolButton(icon_name='list-remove')80 erase_playlist_entry_btn.set_tooltip(_('Remove track'))81 erase_playlist_entry_btn.connect('clicked',82 jukebox._erase_playlist_entry_clicked_cb)83 self.toolbar.insert(erase_playlist_entry_btn, -1)84 85 spacer = Gtk.SeparatorToolItem()86 self.toolbar.insert(spacer, -1)87 spacer.show()88 89 self.prev_button = ToolButton('player_rew')90 self.prev_button.set_tooltip(_('Previous'))91 self.prev_button.show()92 self.prev_button.connect('clicked', self.prev_button_clicked_cb)93 self.toolbar.insert(self.prev_button, -1)94 95 self.pause_image = Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_PAUSE,96 Gtk.IconSize.BUTTON)97 self.pause_image.show()98 self.play_image = Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_PLAY,99 Gtk.IconSize.BUTTON)100 self.play_image.show()101 102 self.button = Gtk.ToolButton()103 self.button.set_icon_widget(self.play_image)104 self.button.set_property('can-default', True)105 self.button.show()106 self.button.connect('clicked', self._button_clicked_cb)107 108 self.toolbar.insert(self.button, -1)109 110 self.next_button = ToolButton('player_fwd')111 self.next_button.set_tooltip(_('Next'))112 self.next_button.show()113 self.next_button.connect('clicked', self.next_button_clicked_cb)114 self.toolbar.insert(self.next_button, -1)115 116 current_time = Gtk.ToolItem()117 self.current_time_label = Gtk.Label(label='')118 current_time.add(self.current_time_label)119 current_time.show()120 toolbar.insert(current_time, -1)121 122 self.adjustment = Gtk.Adjustment(0.0, 0.00, 100.0, 0.1, 1.0, 1.0)123 self.hscale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL,124 adjustment=self.adjustment)125 self.hscale.set_draw_value(False)126 # FIXME: this seems to be deprecated127 # self.hscale.set_update_policy(Gtk.UPDATE_CONTINUOUS)128 logging.debug("FIXME: AttributeError: 'Scale' object has no "129 "attribute 'set_update_policy'")130 self.hscale.connect('button-press-event',131 jukebox.scale_button_press_cb)132 self.hscale.connect('button-release-event',133 jukebox.scale_button_release_cb)134 135 self.scale_item = Gtk.ToolItem()136 self.scale_item.set_expand(True)137 self.scale_item.add(self.hscale)138 self.toolbar.insert(self.scale_item, -1)139 140 total_time = Gtk.ToolItem()141 self.total_time_label = Gtk.Label(label='')142 total_time.add(self.total_time_label)143 total_time.show()144 toolbar.insert(total_time, -1)145 146 def prev_button_clicked_cb(self, widget):147 self.jukebox.songchange('prev')148 149 def next_button_clicked_cb(self, widget):150 self.jukebox.songchange('next')151 152 def _button_clicked_cb(self, widget):153 self.jukebox.play_toggled()154 155 def set_button_play(self):156 self.button.set_icon_widget(self.play_image)157 158 def set_button_pause(self):159 self.button.set_icon_widget(self.pause_image)160 161 def set_disabled(self):162 self.button.set_sensitive(False)163 self.scale_item.set_sensitive(False)164 self.hscale.set_sensitive(False)165 166 def set_enabled(self):167 self.button.set_sensitive(True)168 self.scale_item.set_sensitive(True)169 self.hscale.set_sensitive(True) -
new file activity.py
diff --git a/activity.py b/activity.py new file mode 100644 index 0000000..14fcbe9
- + 1 # This program is free software; you can redistribute it and/or 2 # modify it under the terms of the GNU Lesser General Public 3 # License as published by the Free Software Foundation; either 4 # version 2.1 of the License, or (at your option) any later version. 5 # 6 # This library is distributed in the hope that it will be useful, 7 # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 9 # Lesser General Public License for more details. 10 # 11 # You should have received a copy of the GNU Lesser General Public 12 # License along with this library; if not, write to the Free Software 13 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 14 # USA 15 16 # Activity that plays media. 17 # Copyright (C) 2007 Andy Wingo <wingo@pobox.com> 18 # Copyright (C) 2007 Red Hat, Inc. 19 # Copyright (C) 2008-2010 Kushal Das <kushal@fedoraproject.org> 20 # Copyright (C) 2013 Manuel Kaufmann <humitos@gmail.com> 21 22 import sys 23 import logging 24 import tempfile 25 from gettext import gettext as _ 26 import os 27 28 from sugar3.activity import activity 29 from sugar3.graphics.objectchooser import ObjectChooser 30 from sugar3 import mime 31 from sugar3.datastore import datastore 32 33 from sugar3.graphics.toolbarbox import ToolbarBox 34 from sugar3.graphics.toolbarbox import ToolbarButton 35 from sugar3.activity.widgets import StopButton 36 from sugar3.activity.widgets import ActivityToolbarButton 37 from sugar3.graphics.alert import ErrorAlert 38 from sugar3.graphics.alert import Alert 39 40 import gi 41 gi.require_version('Gtk', '3.0') 42 gi.require_version('Gst', '1.0') 43 44 from gi.repository import GObject 45 from gi.repository import Gdk 46 from gi.repository import Gtk 47 from gi.repository import Gst 48 from gi.repository import Gio 49 50 import urllib 51 from viewtoolbar import ViewToolbar 52 from controls import Controls 53 from player import GstPlayer 54 55 from playlist import PlayList 56 57 PLAYLIST_WIDTH_PROP = 1.0 / 3 58 59 60 class JukeboxActivity(activity.Activity): 61 UPDATE_INTERVAL = 500 62 63 def __init__(self, handle): 64 activity.Activity.__init__(self, handle) 65 self._object_id = handle.object_id 66 self.set_title(_('Jukebox Activity')) 67 self.player = None 68 self.max_participants = 1 69 self._playlist_jobject = None 70 71 toolbar_box = ToolbarBox() 72 activity_button = ActivityToolbarButton(self) 73 activity_toolbar = activity_button.page 74 toolbar_box.toolbar.insert(activity_button, 0) 75 self.title_entry = activity_toolbar.title 76 77 # FIXME: I don't know what is the mission of this line 78 # activity_toolbar.stop.hide() 79 80 self.volume_monitor = Gio.VolumeMonitor.get() 81 self.volume_monitor.connect('mount-added', self._mount_added_cb) 82 self.volume_monitor.connect('mount-removed', self._mount_removed_cb) 83 84 _view_toolbar = ViewToolbar() 85 _view_toolbar.connect('go-fullscreen', 86 self.__go_fullscreen_cb) 87 _view_toolbar.connect('toggle-playlist', 88 self.__toggle_playlist_cb) 89 view_toolbar_button = ToolbarButton( 90 page=_view_toolbar, 91 icon_name='toolbar-view') 92 _view_toolbar.show() 93 toolbar_box.toolbar.insert(view_toolbar_button, -1) 94 view_toolbar_button.show() 95 96 self.control = Controls(toolbar_box.toolbar, self) 97 98 toolbar_box.toolbar.insert(StopButton(self), -1) 99 100 self.set_toolbar_box(toolbar_box) 101 toolbar_box.show_all() 102 103 self.connect("key_press_event", self._key_press_event_cb) 104 105 # We want to be notified when the activity gets the focus or 106 # loses it. When it is not active, we don't need to keep 107 # reproducing the video 108 self.connect("notify::active", self._notify_active_cb) 109 110 # FIXME: this is related with shared activity and it doesn't work 111 # if handle.uri: 112 # pass 113 # elif self._shared_activity: 114 # if self.get_shared(): 115 # pass 116 # else: 117 # # Wait for a successful join before trying to get the document 118 # self.connect("joined", self._joined_cb) 119 120 self.update_id = -1 121 self.changed_id = -1 122 self.seek_timeout_id = -1 123 self.player = None 124 self.uri = None 125 126 # {'url': 'file://.../media.ogg', 'title': 'My song', object_id: '..'} 127 self.playlist = [] 128 129 self.jobjectlist = [] 130 self.playpath = None 131 self.currentplaying = None 132 self.playflag = False 133 self._not_found_files = 0 134 135 # README: I changed this because I was getting an error when I 136 # tried to modify self.bin with something different than 137 # Gtk.Bin 138 139 # self.bin = Gtk.HBox() 140 # self.bin.show() 141 142 self.canvas = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 143 self._alert = None 144 145 self.playlist_widget = PlayList(self.play) 146 self.playlist_widget.update(self.playlist) 147 self.playlist_widget.show() 148 self.canvas.pack_start(self.playlist_widget, False, True, 0) 149 self._empty_widget = Gtk.Label(label="") 150 self._empty_widget.show() 151 self.videowidget = VideoWidget() 152 self.set_canvas(self.canvas) 153 self._init_view_area() 154 self.show_all() 155 self.canvas.connect('size-allocate', self.__size_allocate_cb) 156 157 #From ImageViewer Activity 158 self._want_document = True 159 if self._object_id is None: 160 self._show_object_picker = GObject.timeout_add(1000, \ 161 self._show_picker_cb) 162 163 if handle.uri: 164 self.uri = handle.uri 165 GObject.idle_add(self._start, self.uri, handle.title) 166 167 # Create the player just once 168 logging.debug('Instantiating GstPlayer') 169 self.player = GstPlayer(self.videowidget) 170 self.player.connect("eos", self._player_eos_cb) 171 self.player.connect("error", self._player_error_cb) 172 self.p_position = Gst.CLOCK_TIME_NONE 173 self.p_duration = Gst.CLOCK_TIME_NONE 174 175 def _notify_active_cb(self, widget, event): 176 """Sugar notify us that the activity is becoming active or inactive. 177 When we are inactive, we stop the player if it is reproducing 178 a video. 179 """ 180 if self.player.player.props.uri is not None: 181 if not self.player.is_playing() and self.props.active: 182 self.player.play() 183 if self.player.is_playing() and not self.props.active: 184 self.player.pause() 185 186 def _init_view_area(self): 187 """ 188 Use a notebook with two pages, one empty an another 189 with the videowidget 190 """ 191 self.view_area = Gtk.Notebook() 192 self.view_area.set_show_tabs(False) 193 self.view_area.append_page(self._empty_widget, None) 194 self.view_area.append_page(self.videowidget, None) 195 self.canvas.pack_end(self.view_area, expand=True, 196 fill=True, padding=0) 197 198 def _switch_canvas(self, show_video): 199 """Show or hide the video visualization in the canvas. 200 201 When hidden, the canvas is filled with an empty widget to 202 ensure redrawing. 203 204 """ 205 if show_video: 206 self.view_area.set_current_page(1) 207 else: 208 self.view_area.set_current_page(0) 209 self.canvas.queue_draw() 210 211 def __size_allocate_cb(self, widget, allocation): 212 canvas_size = self.canvas.get_allocation() 213 playlist_width = int(canvas_size.width * PLAYLIST_WIDTH_PROP) 214 self.playlist_widget.set_size_request(playlist_width, 0) 215 216 def open_button_clicked_cb(self, widget): 217 """ To open the dialog to select a new file""" 218 #self.player.seek(0L) 219 #self.player.stop() 220 #self.playlist = [] 221 #self.playpath = None 222 #self.currentplaying = None 223 #self.playflag = False 224 self._want_document = True 225 self._show_object_picker = GObject.timeout_add(1, self._show_picker_cb) 226 227 def _key_press_event_cb(self, widget, event): 228 keyname = Gdk.keyval_name(event.keyval) 229 logging.info("Keyname Press: %s, time: %s", keyname, event.time) 230 if self.title_entry.has_focus(): 231 return False 232 233 if keyname == "space": 234 self.play_toggled() 235 return True 236 237 def check_if_next_prev(self): 238 if self.currentplaying == 0: 239 self.control.prev_button.set_sensitive(False) 240 else: 241 self.control.prev_button.set_sensitive(True) 242 if self.currentplaying == len(self.playlist) - 1: 243 self.control.next_button.set_sensitive(False) 244 else: 245 self.control.next_button.set_sensitive(True) 246 247 def songchange(self, direction): 248 #if self.playflag: 249 # self.playflag = False 250 # return 251 self.player.seek(0L) 252 if direction == "prev" and self.currentplaying > 0: 253 self.play(self.currentplaying - 1) 254 logging.info("prev: " + self.playlist[self.currentplaying]['url']) 255 #self.playflag = True 256 elif direction == "next" and \ 257 self.currentplaying < len(self.playlist) - 1: 258 self.play(self.currentplaying + 1) 259 logging.info("next: " + self.playlist[self.currentplaying]['url']) 260 #self.playflag = True 261 else: 262 self.play_toggled() 263 self.player.stop() 264 self._switch_canvas(show_video=False) 265 self.player.set_uri(None) 266 self.check_if_next_prev() 267 268 def play(self, media_index): 269 self._switch_canvas(show_video=True) 270 self.currentplaying = media_index 271 url = self.playlist[self.currentplaying]['url'] 272 error = None 273 if url.startswith('journal://'): 274 try: 275 jobject = datastore.get(url[len("journal://"):]) 276 url = 'file://' + jobject.file_path 277 except: 278 path = url[len("journal://"):] 279 error = _('The file %s was not found') % path 280 281 self.check_if_next_prev() 282 283 if error is None: 284 self.player.set_uri(url) 285 self.player.play() 286 else: 287 self.control.set_disabled() 288 self._show_error_alert(error) 289 290 self.playlist_widget.set_cursor(self.currentplaying) 291 292 def _player_eos_cb(self, widget): 293 self.songchange('next') 294 295 def _show_error_alert(self, title, msg=None): 296 self._alert = ErrorAlert() 297 self._alert.props.title = title 298 if msg is not None: 299 self._alert.props.msg = msg 300 self.add_alert(self._alert) 301 self._alert.connect('response', self._alert_cancel_cb) 302 self._alert.show() 303 304 def _mount_added_cb(self, volume_monitor, device): 305 self.view_area.set_current_page(0) 306 self.remove_alert(self._alert) 307 self.playlist_widget.update(self.playlist) 308 309 def _mount_removed_cb(self, volume_monitor, device): 310 self.view_area.set_current_page(0) 311 self.remove_alert(self._alert) 312 self.playlist_widget.update(self.playlist) 313 314 def _show_missing_tracks_alert(self, nro): 315 self._alert = Alert() 316 title = _('%s tracks not found.') % nro 317 self._alert.props.title = title 318 self._alert.add_button(Gtk.ResponseType.APPLY, _('Details')) 319 self.add_alert(self._alert) 320 self._alert.connect('response', 321 self.__missing_tracks_alert_response_cb) 322 323 def __missing_tracks_alert_response_cb(self, alert, response_id): 324 vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 325 vbox.props.valign = Gtk.Align.CENTER 326 label = Gtk.Label(label='') 327 label.set_markup(_('<b>Missing tracks</b>')) 328 vbox.pack_start(label, False, False, 15) 329 330 for track in self.playlist_widget.get_missing_tracks(): 331 path = track['url'].replace('journal://', '')\ 332 .replace('file://', '') 333 label = Gtk.Label(label=path) 334 vbox.add(label) 335 336 _missing_tracks = Gtk.ScrolledWindow() 337 _missing_tracks.add_with_viewport(vbox) 338 _missing_tracks.show_all() 339 340 self.view_area.append_page(_missing_tracks, None) 341 342 self.view_area.set_current_page(2) 343 self.remove_alert(alert) 344 345 def _alert_cancel_cb(self, alert, response_id): 346 self.remove_alert(alert) 347 348 def _player_error_cb(self, widget, message, detail): 349 self.player.stop() 350 self.player.set_uri(None) 351 self.control.set_disabled() 352 353 file_path = self.playlist[self.currentplaying]['url']\ 354 .replace('journal://', 'file://') 355 mimetype = mime.get_for_file(file_path) 356 357 title = _('Error') 358 msg = _('This "%s" file can\'t be played') % mimetype 359 self._switch_canvas(False) 360 self._show_error_alert(title, msg) 361 362 def _joined_cb(self, activity): 363 logging.debug("someone joined") 364 pass 365 366 def _shared_cb(self, activity): 367 logging.debug("shared start") 368 pass 369 370 def _show_picker_cb(self): 371 #From ImageViewer Activity 372 if not self._want_document: 373 return 374 375 # README: some arguments are deprecated so I avoid them 376 377 # chooser = ObjectChooser(_('Choose document'), self, 378 # Gtk.DialogFlags.MODAL | 379 # Gtk.DialogFlags.DESTROY_WITH_PARENT, 380 # what_filter=mime.GENERIC_TYPE_AUDIO) 381 382 chooser = ObjectChooser(self, what_filter=mime.GENERIC_TYPE_AUDIO) 383 384 try: 385 result = chooser.run() 386 if result == Gtk.ResponseType.ACCEPT: 387 jobject = chooser.get_selected_object() 388 if jobject and jobject.file_path: 389 logging.error('Adding %s', jobject.file_path) 390 title = jobject.metadata.get('title', None) 391 self._load_file(jobject.file_path, title, 392 jobject.object_id) 393 finally: 394 #chooser.destroy() 395 #del chooser 396 pass 397 398 def read_file(self, file_path): 399 """Load a file from the datastore on activity start.""" 400 logging.debug('JukeBoxAtivity.read_file: %s', file_path) 401 title = self.metadata.get('title', None) 402 self._load_file(file_path, title, self._object_id) 403 404 def _load_file(self, file_path, title, object_id): 405 self.uri = os.path.abspath(file_path) 406 if os.path.islink(self.uri): 407 self.uri = os.path.realpath(self.uri) 408 mimetype = mime.get_for_file('file://' + file_path) 409 logging.error('read_file mime %s', mimetype) 410 if mimetype == 'audio/x-mpegurl': 411 # is a M3U playlist: 412 for uri in self._read_m3u_playlist(file_path): 413 if not self.playlist_widget.check_available_media(uri['url']): 414 self._not_found_files += 1 415 416 GObject.idle_add(self._start, uri['url'], uri['title'], 417 uri['object_id']) 418 else: 419 # is another media file: 420 GObject.idle_add(self._start, self.uri, title, object_id) 421 422 if self._not_found_files > 0: 423 self._show_missing_tracks_alert(self._not_found_files) 424 425 def _create_playlist_jobject(self): 426 """Create an object in the Journal to store the playlist. 427 428 This is needed if the activity was not started from a playlist 429 or from scratch. 430 431 """ 432 jobject = datastore.create() 433 jobject.metadata['mime_type'] = "audio/x-mpegurl" 434 jobject.metadata['title'] = _('Jukebox playlist') 435 436 temp_path = os.path.join(activity.get_activity_root(), 437 'instance') 438 if not os.path.exists(temp_path): 439 os.makedirs(temp_path) 440 441 jobject.file_path = tempfile.mkstemp(dir=temp_path)[1] 442 self._playlist_jobject = jobject 443 444 def write_file(self, file_path): 445 446 def write_playlist_to_file(file_path): 447 """Open the file at file_path and write the playlist. 448 449 It is saved in audio/x-mpegurl format. 450 451 """ 452 list_file = open(file_path, 'w') 453 for uri in self.playlist: 454 list_file.write('#EXTINF: %s\n' % uri['title']) 455 list_file.write('%s\n' % uri['url']) 456 list_file.close() 457 458 if not self.metadata['mime_type']: 459 self.metadata['mime_type'] = 'audio/x-mpegurl' 460 461 if self.metadata['mime_type'] == 'audio/x-mpegurl': 462 write_playlist_to_file(file_path) 463 464 else: 465 if self._playlist_jobject is None: 466 self._create_playlist_jobject() 467 468 # Add the playlist to the playlist jobject description. 469 # This is only done if the activity was not started from a 470 # playlist or from scratch: 471 description = '' 472 for uri in self.playlist: 473 description += '%s\n' % uri['title'] 474 self._playlist_jobject.metadata['description'] = description 475 476 write_playlist_to_file(self._playlist_jobject.file_path) 477 datastore.write(self._playlist_jobject) 478 479 def _read_m3u_playlist(self, file_path): 480 urls = [] 481 title = '' 482 for line in open(file_path).readlines(): 483 line = line.strip() 484 if line != '': 485 if line.startswith('#EXTINF:'): 486 # line with data 487 #EXTINF: title 488 title = line[len('#EXTINF:'):] 489 else: 490 uri = {} 491 uri['url'] = line.strip() 492 uri['title'] = title 493 if uri['url'].startswith('journal://'): 494 uri['object_id'] = uri['url'][len('journal://'):] 495 else: 496 uri['object_id'] = None 497 urls.append(uri) 498 title = '' 499 return urls 500 501 def _start(self, uri=None, title=None, object_id=None): 502 self._want_document = False 503 self.playpath = os.path.dirname(uri) 504 if not uri: 505 return False 506 507 if title is not None: 508 title = title.strip() 509 if object_id is not None: 510 self.playlist.append({'url': 'journal://' + object_id, 511 'title': title}) 512 else: 513 if uri.startswith("file://"): 514 self.playlist.append({'url': uri, 'title': title}) 515 else: 516 uri = "file://" + urllib.quote(os.path.abspath(uri)) 517 self.playlist.append({'url': uri, 'title': title}) 518 519 self.playlist_widget.update(self.playlist) 520 521 try: 522 if self.currentplaying is None: 523 logging.info("Playing: " + self.playlist[0]['url']) 524 url = self.playlist[0]['url'] 525 if url.startswith('journal://'): 526 jobject = datastore.get(url[len("journal://"):]) 527 url = 'file://' + jobject.file_path 528 529 self.player.set_uri(url) 530 self.player.play() 531 self.currentplaying = 0 532 self.play_toggled() 533 self.show_all() 534 else: 535 pass 536 #self.player.seek(0L) 537 #self.player.stop() 538 #self.currentplaying += 1 539 #self.player.set_uri(self.playlist[self.currentplaying]) 540 #self.play_toggled() 541 except: 542 pass 543 self.check_if_next_prev() 544 return False 545 546 def play_toggled(self): 547 self.control.set_enabled() 548 549 if self.player.is_playing(): 550 self.player.pause() 551 self.control.set_button_play() 552 else: 553 if self.player.error: 554 self.control.set_disabled() 555 else: 556 self.player.play() 557 if self.update_id == -1: 558 self.update_id = GObject.timeout_add(self.UPDATE_INTERVAL, 559 self.update_scale_cb) 560 self.control.set_button_pause() 561 562 def volume_changed_cb(self, widget, value): 563 if self.player: 564 self.player.player.set_property('volume', value) 565 566 def scale_button_press_cb(self, widget, event): 567 self.control.button.set_sensitive(False) 568 self.was_playing = self.player.is_playing() 569 if self.was_playing: 570 self.player.pause() 571 572 # don't timeout-update position during seek 573 if self.update_id != -1: 574 GObject.source_remove(self.update_id) 575 self.update_id = -1 576 577 # make sure we get changed notifies 578 if self.changed_id == -1: 579 self.changed_id = self.control.hscale.connect('value-changed', 580 self.scale_value_changed_cb) 581 582 def scale_value_changed_cb(self, scale): 583 # see seek.c:seek_cb 584 real = long(scale.get_value() * self.p_duration / 100) # in ns 585 self.player.seek(real) 586 # allow for a preroll 587 self.player.get_state(timeout=50 * Gst.MSECOND) # 50 ms 588 589 def scale_button_release_cb(self, widget, event): 590 # see seek.cstop_seek 591 widget.disconnect(self.changed_id) 592 self.changed_id = -1 593 594 self.control.button.set_sensitive(True) 595 if self.seek_timeout_id != -1: 596 GObject.source_remove(self.seek_timeout_id) 597 self.seek_timeout_id = -1 598 else: 599 if self.was_playing: 600 self.player.play() 601 602 if self.update_id != -1: 603 self.error('Had a previous update timeout id') 604 else: 605 self.update_id = GObject.timeout_add(self.UPDATE_INTERVAL, 606 self.update_scale_cb) 607 608 def update_scale_cb(self): 609 success, self.p_position, self.p_duration = \ 610 self.player.query_position() 611 612 if not success: 613 return True 614 615 if self.p_position != Gst.CLOCK_TIME_NONE: 616 value = self.p_position * 100.0 / self.p_duration 617 self.control.adjustment.set_value(value) 618 619 # Update the current time 620 seconds = self.p_position * 10 ** -9 621 time = '%2d:%02d' % (int(seconds / 60), int(seconds % 60)) 622 self.control.current_time_label.set_text(time) 623 624 # FIXME: this should be updated just once when the file starts 625 # the first time 626 if self.p_duration != Gst.CLOCK_TIME_NONE: 627 seconds = self.p_duration * 10 ** -9 628 time = '%2d:%02d' % (int(seconds / 60), int(seconds % 60)) 629 self.control.total_time_label.set_text(time) 630 631 return True 632 633 def _erase_playlist_entry_clicked_cb(self, widget): 634 self.playlist_widget.delete_selected_items() 635 636 def __go_fullscreen_cb(self, toolbar): 637 self.fullscreen() 638 639 def __toggle_playlist_cb(self, toolbar): 640 if self.playlist_widget.get_visible(): 641 self.playlist_widget.hide() 642 else: 643 self.playlist_widget.show_all() 644 self.canvas.queue_draw() 645 646 647 class VideoWidget(Gtk.DrawingArea): 648 def __init__(self): 649 GObject.GObject.__init__(self) 650 self.set_events(Gdk.EventMask.POINTER_MOTION_MASK | 651 Gdk.EventMask.POINTER_MOTION_HINT_MASK | 652 Gdk.EventMask.EXPOSURE_MASK | 653 Gdk.EventMask.KEY_PRESS_MASK | 654 Gdk.EventMask.KEY_RELEASE_MASK) 655 656 self.set_app_paintable(True) 657 self.set_double_buffered(False) 658 659 660 if __name__ == '__main__': 661 window = Gtk.Window() 662 view = VideoWidget() 663 664 #player.connect("eos", self._player_eos_cb) 665 #player.connect("error", self._player_error_cb) 666 view.show() 667 window.add(view) 668 669 def map_cb(widget): 670 player = GstPlayer(view) 671 player.set_uri(sys.argv[1]) 672 player.play() 673 674 window.connect('map', map_cb) 675 window.maximize() 676 window.show_all() 677 window.connect("destroy", Gtk.main_quit) 678 Gtk.main() -
activity/activity.info
diff --git a/activity/activity.info b/activity/activity.info index 76cfd1b..f66c422 100644
a b name = Jukebox 3 3 bundle_id = org.laptop.sugar.Jukebox 4 4 license = GPLv2+ 5 5 icon = activity-jukebox 6 exec = sugar-activity jukeboxactivity.JukeboxActivity6 exec = sugar-activity activity.JukeboxActivity 7 7 show_launcher = yes 8 8 activity_version = 29 9 9 mime_types = video/x-theora;audio/x-vorbis;audio/x-flac;audio/x-speex;application/x-ogm-video;application/x-ogm-audio;video/x-mng;audio/x-aiff;audio/x-wav;audio/x-m4a;video/mpeg4;video/mpeg-stream;video/mpeg;application/ogg;video/mpegts;video/mpeg2;video/mpeg1;audio/mpeg;audio/x-ac3;video/x-cdxa;audio/x-au;audio/mpegurl;audio/x-mpegurl;audio/x-vorbis+ogg;audio/x-scpls;audio/ogg;video/ogg;audio/x-flac+ogg;audio/x-speex+ogg;video/x-theora+ogg;video/x-ogm+ogg;video/x-flv;video/mp4;video/x-matroska;video/x-msvideo;video/quicktime, video/x-quicktime, image/mov, audio/aiff, audio/x-midi, video/avi -
new file controls.py
diff --git a/controls.py b/controls.py new file mode 100644 index 0000000..b84a55a
- + 1 # This program is free software; you can redistribute it and/or 2 # modify it under the terms of the GNU Lesser General Public 3 # License as published by the Free Software Foundation; either 4 # version 2.1 of the License, or (at your option) any later version. 5 # 6 # This library is distributed in the hope that it will be useful, 7 # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 9 # Lesser General Public License for more details. 10 # 11 # You should have received a copy of the GNU Lesser General Public 12 # License along with this library; if not, write to the Free Software 13 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 14 # USA 15 16 # Copyright (C) 2013 Manuel Kaufmann <humitos@gmail.com> 17 18 import logging 19 20 from gi.repository import Gtk 21 from gi.repository import GObject 22 23 from gettext import gettext as _ 24 25 from sugar3.graphics.toolbutton import ToolButton 26 27 28 class Controls(GObject.GObject): 29 """Class to create the Control (play, back, forward, 30 add, remove, etc) toolbar""" 31 32 def __init__(self, toolbar, jukebox): 33 GObject.GObject.__init__(self) 34 35 self.toolbar = toolbar 36 self.jukebox = jukebox 37 38 self.open_button = ToolButton('list-add') 39 self.open_button.set_tooltip(_('Add track')) 40 self.open_button.show() 41 self.open_button.connect('clicked', jukebox.open_button_clicked_cb) 42 self.toolbar.insert(self.open_button, -1) 43 44 erase_playlist_entry_btn = ToolButton(icon_name='list-remove') 45 erase_playlist_entry_btn.set_tooltip(_('Remove track')) 46 erase_playlist_entry_btn.connect('clicked', 47 jukebox._erase_playlist_entry_clicked_cb) 48 self.toolbar.insert(erase_playlist_entry_btn, -1) 49 50 spacer = Gtk.SeparatorToolItem() 51 self.toolbar.insert(spacer, -1) 52 spacer.show() 53 54 self.prev_button = ToolButton('player_rew') 55 self.prev_button.set_tooltip(_('Previous')) 56 self.prev_button.show() 57 self.prev_button.connect('clicked', self.prev_button_clicked_cb) 58 self.toolbar.insert(self.prev_button, -1) 59 60 self.pause_image = Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_PAUSE, 61 Gtk.IconSize.BUTTON) 62 self.pause_image.show() 63 self.play_image = Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_PLAY, 64 Gtk.IconSize.BUTTON) 65 self.play_image.show() 66 67 self.button = Gtk.ToolButton() 68 self.button.set_icon_widget(self.play_image) 69 self.button.set_property('can-default', True) 70 self.button.show() 71 self.button.connect('clicked', self._button_clicked_cb) 72 73 self.toolbar.insert(self.button, -1) 74 75 self.next_button = ToolButton('player_fwd') 76 self.next_button.set_tooltip(_('Next')) 77 self.next_button.show() 78 self.next_button.connect('clicked', self.next_button_clicked_cb) 79 self.toolbar.insert(self.next_button, -1) 80 81 current_time = Gtk.ToolItem() 82 self.current_time_label = Gtk.Label(label='') 83 current_time.add(self.current_time_label) 84 current_time.show() 85 toolbar.insert(current_time, -1) 86 87 self.adjustment = Gtk.Adjustment(0.0, 0.00, 100.0, 0.1, 1.0, 1.0) 88 self.hscale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL, 89 adjustment=self.adjustment) 90 self.hscale.set_draw_value(False) 91 # FIXME: this seems to be deprecated 92 # self.hscale.set_update_policy(Gtk.UPDATE_CONTINUOUS) 93 logging.debug("FIXME: AttributeError: 'Scale' object has no " 94 "attribute 'set_update_policy'") 95 self.hscale.connect('button-press-event', 96 jukebox.scale_button_press_cb) 97 self.hscale.connect('button-release-event', 98 jukebox.scale_button_release_cb) 99 100 self.scale_item = Gtk.ToolItem() 101 self.scale_item.set_expand(True) 102 self.scale_item.add(self.hscale) 103 self.toolbar.insert(self.scale_item, -1) 104 105 total_time = Gtk.ToolItem() 106 self.total_time_label = Gtk.Label(label='') 107 total_time.add(self.total_time_label) 108 total_time.show() 109 toolbar.insert(total_time, -1) 110 111 def prev_button_clicked_cb(self, widget): 112 self.jukebox.songchange('prev') 113 114 def next_button_clicked_cb(self, widget): 115 self.jukebox.songchange('next') 116 117 def _button_clicked_cb(self, widget): 118 self.jukebox.play_toggled() 119 120 def set_button_play(self): 121 self.button.set_icon_widget(self.play_image) 122 123 def set_button_pause(self): 124 self.button.set_icon_widget(self.pause_image) 125 126 def set_disabled(self): 127 self.button.set_sensitive(False) 128 self.scale_item.set_sensitive(False) 129 self.hscale.set_sensitive(False) 130 131 def set_enabled(self): 132 self.button.set_sensitive(True) 133 self.scale_item.set_sensitive(True) 134 self.hscale.set_sensitive(True) -
deleted file jukeboxactivity.py
diff --git a/jukeboxactivity.py b/jukeboxactivity.py deleted file mode 100644 index 1bc4d4a..0000000
+ - 1 """2 jukeboxactivity.py3 Activity that plays media.4 Copyright (C) 2007 Andy Wingo <wingo@pobox.com>5 Copyright (C) 2007 Red Hat, Inc.6 Copyright (C) 2008-2010 Kushal Das <kushal@fedoraproject.org>7 """8 9 # This program is free software; you can redistribute it and/or10 # modify it under the terms of the GNU Lesser General Public11 # License as published by the Free Software Foundation; either12 # version 2.1 of the License, or (at your option) any later version.13 #14 # This library is distributed in the hope that it will be useful,15 # but WITHOUT ANY WARRANTY; without even the implied warranty of16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU17 # Lesser General Public License for more details.18 #19 # You should have received a copy of the GNU Lesser General Public20 # License along with this library; if not, write to the Free Software21 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-130722 # USA23 24 import sys25 import logging26 import tempfile27 from gettext import gettext as _28 import os29 30 from sugar3.activity import activity31 from sugar3.graphics.objectchooser import ObjectChooser32 from sugar3 import mime33 from sugar3.datastore import datastore34 35 from sugar3.graphics.toolbarbox import ToolbarBox36 from sugar3.graphics.toolbarbox import ToolbarButton37 from sugar3.activity.widgets import StopButton38 from sugar3.activity.widgets import ActivityToolbarButton39 from sugar3.graphics.alert import ErrorAlert40 from sugar3.graphics.alert import Alert41 42 import gi43 gi.require_version('Gtk', '3.0')44 gi.require_version('Gst', '1.0')45 46 from gi.repository import GObject47 from gi.repository import Gdk48 from gi.repository import Gtk49 from gi.repository import Gst50 from gi.repository import Gio51 52 # Needed for window.get_xid(), xvimagesink.set_window_handle(),53 # respectively:54 from gi.repository import GdkX11, GstVideo55 56 # Avoid "Fatal Python error: GC object already tracked"57 # http://stackoverflow.com/questions/7496629/gstreamer-appsrc-causes-random-crashes58 GObject.threads_init()59 60 # Initialize GStreamer61 Gst.init(None)62 63 import urllib64 from ControlToolbar import Control, ViewToolbar65 from ConfigParser import ConfigParser66 cf = ConfigParser()67 68 from widgets import PlayListWidget69 70 PLAYLIST_WIDTH_PROP = 1.0 / 371 72 73 class JukeboxActivity(activity.Activity):74 UPDATE_INTERVAL = 50075 76 def __init__(self, handle):77 activity.Activity.__init__(self, handle)78 self._object_id = handle.object_id79 self.set_title(_('Jukebox Activity'))80 self.player = None81 self.max_participants = 182 self._playlist_jobject = None83 84 toolbar_box = ToolbarBox()85 activity_button = ActivityToolbarButton(self)86 activity_toolbar = activity_button.page87 toolbar_box.toolbar.insert(activity_button, 0)88 self.title_entry = activity_toolbar.title89 90 # FIXME: I don't know what is the mission of this line91 # activity_toolbar.stop.hide()92 93 self.volume_monitor = Gio.VolumeMonitor.get()94 self.volume_monitor.connect('mount-added', self._mount_added_cb)95 self.volume_monitor.connect('mount-removed', self._mount_removed_cb)96 97 _view_toolbar = ViewToolbar()98 _view_toolbar.connect('go-fullscreen',99 self.__go_fullscreen_cb)100 _view_toolbar.connect('toggle-playlist',101 self.__toggle_playlist_cb)102 view_toolbar_button = ToolbarButton(103 page=_view_toolbar,104 icon_name='toolbar-view')105 _view_toolbar.show()106 toolbar_box.toolbar.insert(view_toolbar_button, -1)107 view_toolbar_button.show()108 109 self.control = Control(toolbar_box.toolbar, self)110 111 toolbar_box.toolbar.insert(StopButton(self), -1)112 113 self.set_toolbar_box(toolbar_box)114 toolbar_box.show_all()115 116 self.connect("key_press_event", self._key_press_event_cb)117 118 # We want to be notified when the activity gets the focus or119 # loses it. When it is not active, we don't need to keep120 # reproducing the video121 self.connect("notify::active", self._notify_active_cb)122 123 # FIXME: this is related with shared activity and it doesn't work124 # if handle.uri:125 # pass126 # elif self._shared_activity:127 # if self.get_shared():128 # pass129 # else:130 # # Wait for a successful join before trying to get the document131 # self.connect("joined", self._joined_cb)132 133 self.update_id = -1134 self.changed_id = -1135 self.seek_timeout_id = -1136 self.player = None137 self.uri = None138 139 # {'url': 'file://.../media.ogg', 'title': 'My song', object_id: '..'}140 self.playlist = []141 142 self.jobjectlist = []143 self.playpath = None144 self.currentplaying = None145 self.playflag = False146 self._not_found_files = 0147 148 # README: I changed this because I was getting an error when I149 # tried to modify self.bin with something different than150 # Gtk.Bin151 152 # self.bin = Gtk.HBox()153 # self.bin.show()154 155 self.canvas = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)156 self._alert = None157 158 self.playlist_widget = PlayListWidget(self.play)159 self.playlist_widget.update(self.playlist)160 self.playlist_widget.show()161 self.canvas.pack_start(self.playlist_widget, False, True, 0)162 self._empty_widget = Gtk.Label(label="")163 self._empty_widget.show()164 self.videowidget = VideoWidget()165 self.set_canvas(self.canvas)166 self._init_view_area()167 self.show_all()168 self.canvas.connect('size-allocate', self.__size_allocate_cb)169 170 #From ImageViewer Activity171 self._want_document = True172 if self._object_id is None:173 self._show_object_picker = GObject.timeout_add(1000, \174 self._show_picker_cb)175 176 if handle.uri:177 self.uri = handle.uri178 GObject.idle_add(self._start, self.uri, handle.title)179 180 # Create the player just once181 logging.debug('Instantiating GstPlayer')182 self.player = GstPlayer(self.videowidget)183 self.player.connect("eos", self._player_eos_cb)184 self.player.connect("error", self._player_error_cb)185 self.p_position = Gst.CLOCK_TIME_NONE186 self.p_duration = Gst.CLOCK_TIME_NONE187 188 def _notify_active_cb(self, widget, event):189 """Sugar notify us that the activity is becoming active or inactive.190 When we are inactive, we stop the player if it is reproducing191 a video.192 """193 if self.player.player.props.uri is not None:194 if not self.player.is_playing() and self.props.active:195 self.player.play()196 if self.player.is_playing() and not self.props.active:197 self.player.pause()198 199 def _init_view_area(self):200 """201 Use a notebook with two pages, one empty an another202 with the videowidget203 """204 self.view_area = Gtk.Notebook()205 self.view_area.set_show_tabs(False)206 self.view_area.append_page(self._empty_widget, None)207 self.view_area.append_page(self.videowidget, None)208 self.canvas.pack_end(self.view_area, expand=True,209 fill=True, padding=0)210 211 def _switch_canvas(self, show_video):212 """Show or hide the video visualization in the canvas.213 214 When hidden, the canvas is filled with an empty widget to215 ensure redrawing.216 217 """218 if show_video:219 self.view_area.set_current_page(1)220 else:221 self.view_area.set_current_page(0)222 self.canvas.queue_draw()223 224 def __size_allocate_cb(self, widget, allocation):225 canvas_size = self.canvas.get_allocation()226 playlist_width = int(canvas_size.width * PLAYLIST_WIDTH_PROP)227 self.playlist_widget.set_size_request(playlist_width, 0)228 229 def open_button_clicked_cb(self, widget):230 """ To open the dialog to select a new file"""231 #self.player.seek(0L)232 #self.player.stop()233 #self.playlist = []234 #self.playpath = None235 #self.currentplaying = None236 #self.playflag = False237 self._want_document = True238 self._show_object_picker = GObject.timeout_add(1, self._show_picker_cb)239 240 def _key_press_event_cb(self, widget, event):241 keyname = Gdk.keyval_name(event.keyval)242 logging.info("Keyname Press: %s, time: %s", keyname, event.time)243 if self.title_entry.has_focus():244 return False245 246 if keyname == "space":247 self.play_toggled()248 return True249 250 def check_if_next_prev(self):251 if self.currentplaying == 0:252 self.control.prev_button.set_sensitive(False)253 else:254 self.control.prev_button.set_sensitive(True)255 if self.currentplaying == len(self.playlist) - 1:256 self.control.next_button.set_sensitive(False)257 else:258 self.control.next_button.set_sensitive(True)259 260 def songchange(self, direction):261 #if self.playflag:262 # self.playflag = False263 # return264 self.player.seek(0L)265 if direction == "prev" and self.currentplaying > 0:266 self.play(self.currentplaying - 1)267 logging.info("prev: " + self.playlist[self.currentplaying]['url'])268 #self.playflag = True269 elif direction == "next" and \270 self.currentplaying < len(self.playlist) - 1:271 self.play(self.currentplaying + 1)272 logging.info("next: " + self.playlist[self.currentplaying]['url'])273 #self.playflag = True274 else:275 self.play_toggled()276 self.player.stop()277 self._switch_canvas(show_video=False)278 self.player.set_uri(None)279 self.check_if_next_prev()280 281 def play(self, media_index):282 self._switch_canvas(show_video=True)283 self.currentplaying = media_index284 url = self.playlist[self.currentplaying]['url']285 error = None286 if url.startswith('journal://'):287 try:288 jobject = datastore.get(url[len("journal://"):])289 url = 'file://' + jobject.file_path290 except:291 path = url[len("journal://"):]292 error = _('The file %s was not found') % path293 294 self.check_if_next_prev()295 296 if error is None:297 self.player.set_uri(url)298 self.player.play()299 else:300 self.control.set_disabled()301 self._show_error_alert(error)302 303 self.playlist_widget.set_cursor(self.currentplaying)304 305 def _player_eos_cb(self, widget):306 self.songchange('next')307 308 def _show_error_alert(self, title, msg=None):309 self._alert = ErrorAlert()310 self._alert.props.title = title311 if msg is not None:312 self._alert.props.msg = msg313 self.add_alert(self._alert)314 self._alert.connect('response', self._alert_cancel_cb)315 self._alert.show()316 317 def _mount_added_cb(self, volume_monitor, device):318 self.view_area.set_current_page(0)319 self.remove_alert(self._alert)320 self.playlist_widget.update(self.playlist)321 322 def _mount_removed_cb(self, volume_monitor, device):323 self.view_area.set_current_page(0)324 self.remove_alert(self._alert)325 self.playlist_widget.update(self.playlist)326 327 def _show_missing_tracks_alert(self, nro):328 self._alert = Alert()329 title = _('%s tracks not found.') % nro330 self._alert.props.title = title331 self._alert.add_button(Gtk.ResponseType.APPLY, _('Details'))332 self.add_alert(self._alert)333 self._alert.connect('response',334 self.__missing_tracks_alert_response_cb)335 336 def __missing_tracks_alert_response_cb(self, alert, response_id):337 vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)338 vbox.props.valign = Gtk.Align.CENTER339 label = Gtk.Label(label='')340 label.set_markup(_('<b>Missing tracks</b>'))341 vbox.pack_start(label, False, False, 15)342 343 for track in self.playlist_widget.get_missing_tracks():344 path = track['url'].replace('journal://', '')\345 .replace('file://', '')346 label = Gtk.Label(label=path)347 vbox.add(label)348 349 _missing_tracks = Gtk.ScrolledWindow()350 _missing_tracks.add_with_viewport(vbox)351 _missing_tracks.show_all()352 353 self.view_area.append_page(_missing_tracks, None)354 355 self.view_area.set_current_page(2)356 self.remove_alert(alert)357 358 def _alert_cancel_cb(self, alert, response_id):359 self.remove_alert(alert)360 361 def _player_error_cb(self, widget, message, detail):362 self.player.stop()363 self.player.set_uri(None)364 self.control.set_disabled()365 366 file_path = self.playlist[self.currentplaying]['url']\367 .replace('journal://', 'file://')368 mimetype = mime.get_for_file(file_path)369 370 title = _('Error')371 msg = _('This "%s" file can\'t be played') % mimetype372 self._switch_canvas(False)373 self._show_error_alert(title, msg)374 375 def _joined_cb(self, activity):376 logging.debug("someone joined")377 pass378 379 def _shared_cb(self, activity):380 logging.debug("shared start")381 pass382 383 def _show_picker_cb(self):384 #From ImageViewer Activity385 if not self._want_document:386 return387 388 # README: some arguments are deprecated so I avoid them389 390 # chooser = ObjectChooser(_('Choose document'), self,391 # Gtk.DialogFlags.MODAL |392 # Gtk.DialogFlags.DESTROY_WITH_PARENT,393 # what_filter=mime.GENERIC_TYPE_AUDIO)394 395 chooser = ObjectChooser(self, what_filter=mime.GENERIC_TYPE_AUDIO)396 397 try:398 result = chooser.run()399 if result == Gtk.ResponseType.ACCEPT:400 jobject = chooser.get_selected_object()401 if jobject and jobject.file_path:402 logging.error('Adding %s', jobject.file_path)403 title = jobject.metadata.get('title', None)404 self._load_file(jobject.file_path, title,405 jobject.object_id)406 finally:407 #chooser.destroy()408 #del chooser409 pass410 411 def read_file(self, file_path):412 """Load a file from the datastore on activity start."""413 logging.debug('JukeBoxAtivity.read_file: %s', file_path)414 title = self.metadata.get('title', None)415 self._load_file(file_path, title, self._object_id)416 417 def _load_file(self, file_path, title, object_id):418 self.uri = os.path.abspath(file_path)419 if os.path.islink(self.uri):420 self.uri = os.path.realpath(self.uri)421 mimetype = mime.get_for_file('file://' + file_path)422 logging.error('read_file mime %s', mimetype)423 if mimetype == 'audio/x-mpegurl':424 # is a M3U playlist:425 for uri in self._read_m3u_playlist(file_path):426 if not self.playlist_widget.check_available_media(uri['url']):427 self._not_found_files += 1428 429 GObject.idle_add(self._start, uri['url'], uri['title'],430 uri['object_id'])431 else:432 # is another media file:433 GObject.idle_add(self._start, self.uri, title, object_id)434 435 if self._not_found_files > 0:436 self._show_missing_tracks_alert(self._not_found_files)437 438 def _create_playlist_jobject(self):439 """Create an object in the Journal to store the playlist.440 441 This is needed if the activity was not started from a playlist442 or from scratch.443 444 """445 jobject = datastore.create()446 jobject.metadata['mime_type'] = "audio/x-mpegurl"447 jobject.metadata['title'] = _('Jukebox playlist')448 449 temp_path = os.path.join(activity.get_activity_root(),450 'instance')451 if not os.path.exists(temp_path):452 os.makedirs(temp_path)453 454 jobject.file_path = tempfile.mkstemp(dir=temp_path)[1]455 self._playlist_jobject = jobject456 457 def write_file(self, file_path):458 459 def write_playlist_to_file(file_path):460 """Open the file at file_path and write the playlist.461 462 It is saved in audio/x-mpegurl format.463 464 """465 list_file = open(file_path, 'w')466 for uri in self.playlist:467 list_file.write('#EXTINF: %s\n' % uri['title'])468 list_file.write('%s\n' % uri['url'])469 list_file.close()470 471 if not self.metadata['mime_type']:472 self.metadata['mime_type'] = 'audio/x-mpegurl'473 474 if self.metadata['mime_type'] == 'audio/x-mpegurl':475 write_playlist_to_file(file_path)476 477 else:478 if self._playlist_jobject is None:479 self._create_playlist_jobject()480 481 # Add the playlist to the playlist jobject description.482 # This is only done if the activity was not started from a483 # playlist or from scratch:484 description = ''485 for uri in self.playlist:486 description += '%s\n' % uri['title']487 self._playlist_jobject.metadata['description'] = description488 489 write_playlist_to_file(self._playlist_jobject.file_path)490 datastore.write(self._playlist_jobject)491 492 def _read_m3u_playlist(self, file_path):493 urls = []494 title = ''495 for line in open(file_path).readlines():496 line = line.strip()497 if line != '':498 if line.startswith('#EXTINF:'):499 # line with data500 #EXTINF: title501 title = line[len('#EXTINF:'):]502 else:503 uri = {}504 uri['url'] = line.strip()505 uri['title'] = title506 if uri['url'].startswith('journal://'):507 uri['object_id'] = uri['url'][len('journal://'):]508 else:509 uri['object_id'] = None510 urls.append(uri)511 title = ''512 return urls513 514 def _start(self, uri=None, title=None, object_id=None):515 self._want_document = False516 self.playpath = os.path.dirname(uri)517 if not uri:518 return False519 520 if title is not None:521 title = title.strip()522 if object_id is not None:523 self.playlist.append({'url': 'journal://' + object_id,524 'title': title})525 else:526 if uri.startswith("file://"):527 self.playlist.append({'url': uri, 'title': title})528 else:529 uri = "file://" + urllib.quote(os.path.abspath(uri))530 self.playlist.append({'url': uri, 'title': title})531 532 self.playlist_widget.update(self.playlist)533 534 try:535 if self.currentplaying is None:536 logging.info("Playing: " + self.playlist[0]['url'])537 url = self.playlist[0]['url']538 if url.startswith('journal://'):539 jobject = datastore.get(url[len("journal://"):])540 url = 'file://' + jobject.file_path541 542 self.player.set_uri(url)543 self.player.play()544 self.currentplaying = 0545 self.play_toggled()546 self.show_all()547 else:548 pass549 #self.player.seek(0L)550 #self.player.stop()551 #self.currentplaying += 1552 #self.player.set_uri(self.playlist[self.currentplaying])553 #self.play_toggled()554 except:555 pass556 self.check_if_next_prev()557 return False558 559 def play_toggled(self):560 self.control.set_enabled()561 562 if self.player.is_playing():563 self.player.pause()564 self.control.set_button_play()565 else:566 if self.player.error:567 self.control.set_disabled()568 else:569 self.player.play()570 if self.update_id == -1:571 self.update_id = GObject.timeout_add(self.UPDATE_INTERVAL,572 self.update_scale_cb)573 self.control.set_button_pause()574 575 def volume_changed_cb(self, widget, value):576 if self.player:577 self.player.player.set_property('volume', value)578 579 def scale_button_press_cb(self, widget, event):580 self.control.button.set_sensitive(False)581 self.was_playing = self.player.is_playing()582 if self.was_playing:583 self.player.pause()584 585 # don't timeout-update position during seek586 if self.update_id != -1:587 GObject.source_remove(self.update_id)588 self.update_id = -1589 590 # make sure we get changed notifies591 if self.changed_id == -1:592 self.changed_id = self.control.hscale.connect('value-changed',593 self.scale_value_changed_cb)594 595 def scale_value_changed_cb(self, scale):596 # see seek.c:seek_cb597 real = long(scale.get_value() * self.p_duration / 100) # in ns598 self.player.seek(real)599 # allow for a preroll600 self.player.get_state(timeout=50 * Gst.MSECOND) # 50 ms601 602 def scale_button_release_cb(self, widget, event):603 # see seek.cstop_seek604 widget.disconnect(self.changed_id)605 self.changed_id = -1606 607 self.control.button.set_sensitive(True)608 if self.seek_timeout_id != -1:609 GObject.source_remove(self.seek_timeout_id)610 self.seek_timeout_id = -1611 else:612 if self.was_playing:613 self.player.play()614 615 if self.update_id != -1:616 self.error('Had a previous update timeout id')617 else:618 self.update_id = GObject.timeout_add(self.UPDATE_INTERVAL,619 self.update_scale_cb)620 621 def update_scale_cb(self):622 success, self.p_position, self.p_duration = \623 self.player.query_position()624 625 if not success:626 return True627 628 if self.p_position != Gst.CLOCK_TIME_NONE:629 value = self.p_position * 100.0 / self.p_duration630 self.control.adjustment.set_value(value)631 632 # Update the current time633 seconds = self.p_position * 10 ** -9634 time = '%2d:%02d' % (int(seconds / 60), int(seconds % 60))635 self.control.current_time_label.set_text(time)636 637 # FIXME: this should be updated just once when the file starts638 # the first time639 if self.p_duration != Gst.CLOCK_TIME_NONE:640 seconds = self.p_duration * 10 ** -9641 time = '%2d:%02d' % (int(seconds / 60), int(seconds % 60))642 self.control.total_time_label.set_text(time)643 644 return True645 646 def _erase_playlist_entry_clicked_cb(self, widget):647 self.playlist_widget.delete_selected_items()648 649 def __go_fullscreen_cb(self, toolbar):650 self.fullscreen()651 652 def __toggle_playlist_cb(self, toolbar):653 if self.playlist_widget.get_visible():654 self.playlist_widget.hide()655 else:656 self.playlist_widget.show_all()657 self.canvas.queue_draw()658 659 660 class GstPlayer(GObject.GObject):661 662 __gsignals__ = {663 'error': (GObject.SignalFlags.RUN_FIRST, None, [str, str]),664 'eos': (GObject.SignalFlags.RUN_FIRST, None, []),665 }666 667 def __init__(self, videowidget):668 GObject.GObject.__init__(self)669 670 self.playing = False671 self.error = False672 673 # Create GStreamer pipeline674 self.pipeline = Gst.Pipeline()675 # Create bus to get events from GStreamer pipeline676 self.bus = self.pipeline.get_bus()677 self.bus.add_signal_watch()678 679 self.bus.connect('message::eos', self.__on_eos_message)680 self.bus.connect('message::error', self.__on_error_message)681 682 # This is needed to make the video output in our DrawingArea683 self.bus.enable_sync_message_emission()684 self.bus.connect('sync-message::element', self.__on_sync_message)685 686 # Create GStreamer elements687 self.player = Gst.ElementFactory.make('playbin', None)688 self.pipeline.add(self.player)689 690 # Set the proper flags to render the vis-plugin691 GST_PLAY_FLAG_VIS = 1 << 3692 GST_PLAY_FLAG_TEXT = 1 << 2693 self.player.props.flags |= GST_PLAY_FLAG_VIS694 self.player.props.flags |= GST_PLAY_FLAG_TEXT695 696 r = Gst.Registry.get()697 l = [x for x in r.get_feature_list(Gst.ElementFactory)698 if (x.get_metadata('klass') == "Visualization")]699 if len(l):700 e = l.pop() # take latest plugin in the list701 vis_plug = Gst.ElementFactory.make(e.get_name(), e.get_name())702 self.player.set_property('vis-plugin', vis_plug)703 704 self.overlay = None705 videowidget.realize()706 self.videowidget = videowidget707 self.videowidget_xid = videowidget.get_window().get_xid()708 self._init_video_sink()709 710 def __on_error_message(self, bus, msg):711 self.stop()712 self.playing = False713 self.error = True714 err, debug = msg.parse_error()715 self.emit('error', err, debug)716 717 def __on_eos_message(self, bus, msg):718 logging.debug('SIGNAL: eos')719 self.playing = False720 self.emit('eos')721 722 def __on_sync_message(self, bus, msg):723 if msg.get_structure().get_name() == 'prepare-window-handle':724 msg.src.set_window_handle(self.videowidget_xid)725 726 def set_uri(self, uri):727 self.pipeline.set_state(Gst.State.READY)728 logging.debug('### Setting URI: %s', uri)729 self.player.set_property('uri', uri)730 731 def _init_video_sink(self):732 self.bin = Gst.Bin()733 videoscale = Gst.ElementFactory.make('videoscale', 'videoscale')734 self.bin.add(videoscale)735 pad = videoscale.get_static_pad("sink")736 ghostpad = Gst.GhostPad.new("sink", pad)737 self.bin.add_pad(ghostpad)738 videoscale.set_property("method", 0)739 740 textoverlay = Gst.ElementFactory.make('textoverlay', 'textoverlay')741 self.overlay = textoverlay742 self.bin.add(textoverlay)743 conv = Gst.ElementFactory.make("videoconvert", "conv")744 self.bin.add(conv)745 videosink = Gst.ElementFactory.make('autovideosink', 'autovideosink')746 self.bin.add(videosink)747 748 videoscale.link(textoverlay)749 textoverlay.link(conv)750 conv.link(videosink)751 752 self.player.set_property("video-sink", self.bin)753 754 def set_overlay(self, title, artist, album):755 text = "%s\n%s" % (title, artist)756 if album and len(album):757 text += "\n%s" % album758 self.overlay.set_property("text", text)759 self.overlay.set_property("font-desc", "sans bold 14")760 self.overlay.set_property("halignment", "right")761 self.overlay.set_property("valignment", "bottom")762 try:763 # Only in OLPC versions of gstreamer-plugins-base for now764 self.overlay.set_property("line-align", "left")765 except:766 pass767 768 def query_position(self):769 "Returns a (position, duration) tuple"770 771 p_success, position = self.player.query_position(Gst.Format.TIME)772 d_success, duration = self.player.query_duration(Gst.Format.TIME)773 774 return (p_success and d_success, position, duration)775 776 def seek(self, location):777 """778 @param location: time to seek to, in nanoseconds779 """780 781 logging.debug('Seek: %s ns', location)782 783 self.pipeline.seek_simple(Gst.Format.TIME,784 Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,785 location)786 787 def pause(self):788 logging.debug("pausing player")789 self.pipeline.set_state(Gst.State.PAUSED)790 self.playing = False791 792 def play(self):793 logging.debug("playing player")794 self.pipeline.set_state(Gst.State.PLAYING)795 self.playing = True796 self.error = False797 798 def stop(self):799 self.playing = False800 self.pipeline.set_state(Gst.State.NULL)801 logging.debug("stopped player")802 803 def get_state(self, timeout=1):804 return self.player.get_state(timeout=timeout)805 806 def is_playing(self):807 return self.playing808 809 810 class VideoWidget(Gtk.DrawingArea):811 def __init__(self):812 GObject.GObject.__init__(self)813 self.set_events(Gdk.EventMask.POINTER_MOTION_MASK |814 Gdk.EventMask.POINTER_MOTION_HINT_MASK |815 Gdk.EventMask.EXPOSURE_MASK |816 Gdk.EventMask.KEY_PRESS_MASK |817 Gdk.EventMask.KEY_RELEASE_MASK)818 819 self.set_app_paintable(True)820 self.set_double_buffered(False)821 822 823 if __name__ == '__main__':824 window = Gtk.Window()825 view = VideoWidget()826 827 #player.connect("eos", self._player_eos_cb)828 #player.connect("error", self._player_error_cb)829 view.show()830 window.add(view)831 832 def map_cb(widget):833 player = GstPlayer(view)834 player.set_uri(sys.argv[1])835 player.play()836 837 window.connect('map', map_cb)838 window.maximize()839 window.show_all()840 window.connect("destroy", Gtk.main_quit)841 Gtk.main() -
new file player.py
diff --git a/player.py b/player.py new file mode 100644 index 0000000..b5b03da
- + 1 # This program is free software; you can redistribute it and/or 2 # modify it under the terms of the GNU Lesser General Public 3 # License as published by the Free Software Foundation; either 4 # version 2.1 of the License, or (at your option) any later version. 5 # 6 # This library is distributed in the hope that it will be useful, 7 # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 9 # Lesser General Public License for more details. 10 # 11 # You should have received a copy of the GNU Lesser General Public 12 # License along with this library; if not, write to the Free Software 13 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 14 # USA 15 16 # Copyright (C) 2013 Manuel Kaufmann <humitos@gmail.com> 17 18 import logging 19 20 from gi.repository import Gst 21 from gi.repository import GObject 22 23 # Needed for window.get_xid(), xvimagesink.set_window_handle(), 24 # respectively: 25 from gi.repository import GdkX11, GstVideo 26 27 # Avoid "Fatal Python error: GC object already tracked" 28 # http://stackoverflow.com/questions/7496629/gstreamer-appsrc-causes-random-crashes 29 # GObject.threads_init() 30 31 # Initialize GStreamer 32 Gst.init(None) 33 34 35 class GstPlayer(GObject.GObject): 36 37 __gsignals__ = { 38 'error': (GObject.SignalFlags.RUN_FIRST, None, [str, str]), 39 'eos': (GObject.SignalFlags.RUN_FIRST, None, []), 40 } 41 42 def __init__(self, videowidget): 43 GObject.GObject.__init__(self) 44 45 self.playing = False 46 self.error = False 47 48 # Create GStreamer pipeline 49 self.pipeline = Gst.Pipeline() 50 # Create bus to get events from GStreamer pipeline 51 self.bus = self.pipeline.get_bus() 52 self.bus.add_signal_watch() 53 54 self.bus.connect('message::eos', self.__on_eos_message) 55 self.bus.connect('message::error', self.__on_error_message) 56 57 # This is needed to make the video output in our DrawingArea 58 self.bus.enable_sync_message_emission() 59 self.bus.connect('sync-message::element', self.__on_sync_message) 60 61 # Create GStreamer elements 62 self.player = Gst.ElementFactory.make('playbin', None) 63 self.pipeline.add(self.player) 64 65 # Set the proper flags to render the vis-plugin 66 GST_PLAY_FLAG_VIS = 1 << 3 67 GST_PLAY_FLAG_TEXT = 1 << 2 68 self.player.props.flags |= GST_PLAY_FLAG_VIS 69 self.player.props.flags |= GST_PLAY_FLAG_TEXT 70 71 r = Gst.Registry.get() 72 l = [x for x in r.get_feature_list(Gst.ElementFactory) 73 if (x.get_metadata('klass') == "Visualization")] 74 if len(l): 75 e = l.pop() # take latest plugin in the list 76 vis_plug = Gst.ElementFactory.make(e.get_name(), e.get_name()) 77 self.player.set_property('vis-plugin', vis_plug) 78 79 self.overlay = None 80 videowidget.realize() 81 self.videowidget = videowidget 82 self.videowidget_xid = videowidget.get_window().get_xid() 83 self._init_video_sink() 84 85 def __on_error_message(self, bus, msg): 86 self.stop() 87 self.playing = False 88 self.error = True 89 err, debug = msg.parse_error() 90 self.emit('error', err, debug) 91 92 def __on_eos_message(self, bus, msg): 93 logging.debug('SIGNAL: eos') 94 self.playing = False 95 self.emit('eos') 96 97 def __on_sync_message(self, bus, msg): 98 if msg.get_structure().get_name() == 'prepare-window-handle': 99 msg.src.set_window_handle(self.videowidget_xid) 100 101 def set_uri(self, uri): 102 self.pipeline.set_state(Gst.State.READY) 103 logging.debug('### Setting URI: %s', uri) 104 self.player.set_property('uri', uri) 105 106 def _init_video_sink(self): 107 self.bin = Gst.Bin() 108 videoscale = Gst.ElementFactory.make('videoscale', 'videoscale') 109 self.bin.add(videoscale) 110 pad = videoscale.get_static_pad("sink") 111 ghostpad = Gst.GhostPad.new("sink", pad) 112 self.bin.add_pad(ghostpad) 113 videoscale.set_property("method", 0) 114 115 textoverlay = Gst.ElementFactory.make('textoverlay', 'textoverlay') 116 self.overlay = textoverlay 117 self.bin.add(textoverlay) 118 conv = Gst.ElementFactory.make("videoconvert", "conv") 119 self.bin.add(conv) 120 videosink = Gst.ElementFactory.make('autovideosink', 'autovideosink') 121 self.bin.add(videosink) 122 123 videoscale.link(textoverlay) 124 textoverlay.link(conv) 125 conv.link(videosink) 126 127 self.player.set_property("video-sink", self.bin) 128 129 def set_overlay(self, title, artist, album): 130 text = "%s\n%s" % (title, artist) 131 if album and len(album): 132 text += "\n%s" % album 133 self.overlay.set_property("text", text) 134 self.overlay.set_property("font-desc", "sans bold 14") 135 self.overlay.set_property("halignment", "right") 136 self.overlay.set_property("valignment", "bottom") 137 try: 138 # Only in OLPC versions of gstreamer-plugins-base for now 139 self.overlay.set_property("line-align", "left") 140 except: 141 pass 142 143 def query_position(self): 144 "Returns a (position, duration) tuple" 145 146 p_success, position = self.player.query_position(Gst.Format.TIME) 147 d_success, duration = self.player.query_duration(Gst.Format.TIME) 148 149 return (p_success and d_success, position, duration) 150 151 def seek(self, location): 152 """ 153 @param location: time to seek to, in nanoseconds 154 """ 155 156 logging.debug('Seek: %s ns', location) 157 158 self.pipeline.seek_simple(Gst.Format.TIME, 159 Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, 160 location) 161 162 def pause(self): 163 logging.debug("pausing player") 164 self.pipeline.set_state(Gst.State.PAUSED) 165 self.playing = False 166 167 def play(self): 168 logging.debug("playing player") 169 self.pipeline.set_state(Gst.State.PLAYING) 170 self.playing = True 171 self.error = False 172 173 def stop(self): 174 self.playing = False 175 self.pipeline.set_state(Gst.State.NULL) 176 logging.debug("stopped player") 177 178 def get_state(self, timeout=1): 179 return self.player.get_state(timeout=timeout) 180 181 def is_playing(self): 182 return self.playing -
new file playlist.py
diff --git a/playlist.py b/playlist.py new file mode 100644 index 0000000..4563d47
- + 1 # This program is free software; you can redistribute it and/or 2 # modify it under the terms of the GNU Lesser General Public 3 # License as published by the Free Software Foundation; either 4 # version 2.1 of the License, or (at your option) any later version. 5 # 6 # This library is distributed in the hope that it will be useful, 7 # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 9 # Lesser General Public License for more details. 10 # 11 # You should have received a copy of the GNU Lesser General Public 12 # License along with this library; if not, write to the Free Software 13 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 14 # USA 15 16 import logging 17 import os 18 from gettext import gettext as _ 19 20 import gi 21 gi.require_version('Gtk', '3.0') 22 23 from gi.repository import GObject 24 from gi.repository import Gtk 25 from gi.repository import Pango 26 27 from sugar3.graphics.icon import CellRendererIcon 28 29 30 COLUMNS_NAME = ('index', 'media', 'available') 31 COLUMNS = dict((name, i) for i, name in enumerate(COLUMNS_NAME)) 32 33 34 class PlayList(Gtk.ScrolledWindow): 35 def __init__(self, play_callback): 36 self._playlist = None 37 self._play_callback = play_callback 38 39 GObject.GObject.__init__(self, hadjustment=None, 40 vadjustment=None) 41 self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) 42 self.listview = Gtk.TreeView() 43 self.treemodel = Gtk.ListStore(int, object, bool) 44 self.listview.set_model(self.treemodel) 45 selection = self.listview.get_selection() 46 selection.set_mode(Gtk.SelectionMode.SINGLE) 47 48 renderer_icon = CellRendererIcon(self.listview) 49 renderer_icon.props.icon_name = 'emblem-notification' 50 renderer_icon.props.width = 20 51 renderer_icon.props.height = 20 52 renderer_icon.props.size = 20 53 treecol_icon = Gtk.TreeViewColumn() 54 treecol_icon.pack_start(renderer_icon, False) 55 treecol_icon.set_cell_data_func(renderer_icon, self._set_icon) 56 self.listview.append_column(treecol_icon) 57 58 renderer_idx = Gtk.CellRendererText() 59 treecol_idx = Gtk.TreeViewColumn(_('No.')) 60 treecol_idx.pack_start(renderer_idx, True) 61 treecol_idx.set_cell_data_func(renderer_idx, self._set_number) 62 self.listview.append_column(treecol_idx) 63 64 renderer_title = Gtk.CellRendererText() 65 renderer_title.set_property('ellipsize', Pango.EllipsizeMode.END) 66 treecol_title = Gtk.TreeViewColumn(_('Play List')) 67 treecol_title.pack_start(renderer_title, True) 68 treecol_title.set_cell_data_func(renderer_title, self._set_title) 69 self.listview.append_column(treecol_title) 70 71 # we don't support search in the playlist for the moment: 72 self.listview.set_enable_search(False) 73 74 self.listview.connect('row-activated', self.__on_row_activated) 75 76 self.add(self.listview) 77 78 def __on_row_activated(self, treeview, path, col): 79 model = treeview.get_model() 80 81 treeiter = model.get_iter(path) 82 media_idx = model.get_value(treeiter, COLUMNS['index']) 83 self._play_callback(media_idx) 84 85 def _set_number(self, column, cell, model, it, data): 86 idx = model.get_value(it, COLUMNS['index']) 87 cell.set_property('text', idx + 1) 88 89 def _set_title(self, column, cell, model, it, data): 90 playlist_item = model.get_value(it, COLUMNS['media']) 91 available = model.get_value(it, COLUMNS['available']) 92 93 cell.set_property('text', playlist_item['title']) 94 sensitive = True 95 if not available: 96 sensitive = False 97 cell.set_property('sensitive', sensitive) 98 99 def _set_icon(self, column, cell, model, it, data): 100 available = model.get_value(it, COLUMNS['available']) 101 cell.set_property('visible', not available) 102 103 def update(self, playlist): 104 self.treemodel.clear() 105 self._playlist = playlist 106 pl = list(enumerate(playlist)) 107 for i, media in pl: 108 available = self.check_available_media(media['url']) 109 media['available'] = available 110 self.treemodel.append((i, media, available)) 111 #self.set_cursor(0) 112 113 def set_cursor(self, index): 114 self.listview.set_cursor((index,)) 115 116 def delete_selected_items(self): 117 selection = self.listview.get_selection() 118 sel_model, sel_rows = self.listview.get_selection().get_selected_rows() 119 for row in sel_rows: 120 index = sel_model.get_value(sel_model.get_iter(row), 0) 121 self._playlist.pop(index) 122 self.treemodel.remove(self.treemodel.get_iter(row)) 123 self.update(self._playlist) 124 125 def check_available_media(self, uri): 126 path = uri.replace('journal://', '').replace('file://', '') 127 if os.path.exists(path): 128 return True 129 else: 130 return False 131 132 def get_missing_tracks(self): 133 missing_tracks = [] 134 for track in self._playlist: 135 if not track['available']: 136 missing_tracks.append(track) 137 return missing_tracks -
new file viewtoolbar.py
diff --git a/viewtoolbar.py b/viewtoolbar.py new file mode 100644 index 0000000..f2d99d2
- + 1 # This program is free software; you can redistribute it and/or modify 2 # it under the terms of the GNU General Public License as published by 3 # the Free Software Foundation; either version 2 of the License, or 4 # (at your option) any later version. 5 # 6 # This program is distributed in the hope that it will be useful, 7 # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 # GNU General Public License for more details. 10 # 11 # You should have received a copy of the GNU General Public License 12 # along with this program; if not, write to the Free Software 13 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 14 15 # Copyright (C) 2007 Andy Wingo <wingo@pobox.com> 16 # Copyright (C) 2007 Red Hat, Inc. 17 # Copyright (C) 2008 Kushal Das <kushal@fedoraproject.org> 18 # Copyright (C) 2013 Manuel Kaufmann <humitos@gmail.com> 19 20 import logging 21 22 from gettext import gettext as _ 23 24 from gi.repository import GObject 25 from gi.repository import Gtk 26 27 from sugar3.graphics.toolbutton import ToolButton 28 from sugar3.graphics.toggletoolbutton import ToggleToolButton 29 30 31 class ViewToolbar(Gtk.Toolbar): 32 __gtype_name__ = 'ViewToolbar' 33 34 __gsignals__ = { 35 'go-fullscreen': (GObject.SignalFlags.RUN_FIRST, 36 None, 37 ([])), 38 'toggle-playlist': (GObject.SignalFlags.RUN_FIRST, 39 None, 40 ([])) 41 } 42 43 def __init__(self): 44 GObject.GObject.__init__(self) 45 46 self._show_playlist = ToggleToolButton('view-list') 47 self._show_playlist.set_active(True) 48 self._show_playlist.set_tooltip(_('Show Playlist')) 49 self._show_playlist.connect('toggled', self._playlist_toggled_cb) 50 self.insert(self._show_playlist, -1) 51 self._show_playlist.show() 52 53 self._fullscreen = ToolButton('view-fullscreen') 54 self._fullscreen.set_tooltip(_('Fullscreen')) 55 self._fullscreen.connect('clicked', self._fullscreen_cb) 56 self.insert(self._fullscreen, -1) 57 self._fullscreen.show() 58 59 def _fullscreen_cb(self, button): 60 self.emit('go-fullscreen') 61 62 def _playlist_toggled_cb(self, button): 63 self.emit('toggle-playlist') 64 65 66 class Control(GObject.GObject): 67 """Class to create the Control (play) toolbar""" 68 69 def __init__(self, toolbar, jukebox): 70 GObject.GObject.__init__(self) 71 72 self.toolbar = toolbar 73 self.jukebox = jukebox 74 75 self.open_button = ToolButton('list-add') 76 self.open_button.set_tooltip(_('Add track')) 77 self.open_button.show() 78 self.open_button.connect('clicked', jukebox.open_button_clicked_cb) 79 self.toolbar.insert(self.open_button, -1) 80 81 erase_playlist_entry_btn = ToolButton(icon_name='list-remove') 82 erase_playlist_entry_btn.set_tooltip(_('Remove track')) 83 erase_playlist_entry_btn.connect('clicked', 84 jukebox._erase_playlist_entry_clicked_cb) 85 self.toolbar.insert(erase_playlist_entry_btn, -1) 86 87 spacer = Gtk.SeparatorToolItem() 88 self.toolbar.insert(spacer, -1) 89 spacer.show() 90 91 self.prev_button = ToolButton('player_rew') 92 self.prev_button.set_tooltip(_('Previous')) 93 self.prev_button.show() 94 self.prev_button.connect('clicked', self.prev_button_clicked_cb) 95 self.toolbar.insert(self.prev_button, -1) 96 97 self.pause_image = Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_PAUSE, 98 Gtk.IconSize.BUTTON) 99 self.pause_image.show() 100 self.play_image = Gtk.Image.new_from_stock(Gtk.STOCK_MEDIA_PLAY, 101 Gtk.IconSize.BUTTON) 102 self.play_image.show() 103 104 self.button = Gtk.ToolButton() 105 self.button.set_icon_widget(self.play_image) 106 self.button.set_property('can-default', True) 107 self.button.show() 108 self.button.connect('clicked', self._button_clicked_cb) 109 110 self.toolbar.insert(self.button, -1) 111 112 self.next_button = ToolButton('player_fwd') 113 self.next_button.set_tooltip(_('Next')) 114 self.next_button.show() 115 self.next_button.connect('clicked', self.next_button_clicked_cb) 116 self.toolbar.insert(self.next_button, -1) 117 118 current_time = Gtk.ToolItem() 119 self.current_time_label = Gtk.Label(label='') 120 current_time.add(self.current_time_label) 121 current_time.show() 122 toolbar.insert(current_time, -1) 123 124 self.adjustment = Gtk.Adjustment(0.0, 0.00, 100.0, 0.1, 1.0, 1.0) 125 self.hscale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL, 126 adjustment=self.adjustment) 127 self.hscale.set_draw_value(False) 128 # FIXME: this seems to be deprecated 129 # self.hscale.set_update_policy(Gtk.UPDATE_CONTINUOUS) 130 logging.debug("FIXME: AttributeError: 'Scale' object has no " 131 "attribute 'set_update_policy'") 132 self.hscale.connect('button-press-event', 133 jukebox.scale_button_press_cb) 134 self.hscale.connect('button-release-event', 135 jukebox.scale_button_release_cb) 136 137 self.scale_item = Gtk.ToolItem() 138 self.scale_item.set_expand(True) 139 self.scale_item.add(self.hscale) 140 self.toolbar.insert(self.scale_item, -1) 141 142 total_time = Gtk.ToolItem() 143 self.total_time_label = Gtk.Label(label='') 144 total_time.add(self.total_time_label) 145 total_time.show() 146 toolbar.insert(total_time, -1) 147 148 def prev_button_clicked_cb(self, widget): 149 self.jukebox.songchange('prev') 150 151 def next_button_clicked_cb(self, widget): 152 self.jukebox.songchange('next') 153 154 def _button_clicked_cb(self, widget): 155 self.jukebox.play_toggled() 156 157 def set_button_play(self): 158 self.button.set_icon_widget(self.play_image) 159 160 def set_button_pause(self): 161 self.button.set_icon_widget(self.pause_image) 162 163 def set_disabled(self): 164 self.button.set_sensitive(False) 165 self.scale_item.set_sensitive(False) 166 self.hscale.set_sensitive(False) 167 168 def set_enabled(self): 169 self.button.set_sensitive(True) 170 self.scale_item.set_sensitive(True) 171 self.hscale.set_sensitive(True) -
deleted file widgets.py
diff --git a/widgets.py b/widgets.py deleted file mode 100644 index fbbd9c5..0000000
+ - 1 # This program is free software; you can redistribute it and/or2 # modify it under the terms of the GNU Lesser General Public3 # License as published by the Free Software Foundation; either4 # version 2.1 of the License, or (at your option) any later version.5 #6 # This library is distributed in the hope that it will be useful,7 # but WITHOUT ANY WARRANTY; without even the implied warranty of8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU9 # Lesser General Public License for more details.10 #11 # You should have received a copy of the GNU Lesser General Public12 # License along with this library; if not, write to the Free Software13 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-130714 # USA15 16 import logging17 import os18 from gettext import gettext as _19 20 import gi21 gi.require_version('Gtk', '3.0')22 23 from gi.repository import GObject24 from gi.repository import Gtk25 from gi.repository import Pango26 27 from sugar3.graphics.icon import CellRendererIcon28 29 30 COLUMNS_NAME = ('index', 'media', 'available')31 COLUMNS = dict((name, i) for i, name in enumerate(COLUMNS_NAME))32 33 34 class PlayListWidget(Gtk.ScrolledWindow):35 def __init__(self, play_callback):36 self._playlist = None37 self._play_callback = play_callback38 39 GObject.GObject.__init__(self, hadjustment=None,40 vadjustment=None)41 self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)42 self.listview = Gtk.TreeView()43 self.treemodel = Gtk.ListStore(int, object, bool)44 self.listview.set_model(self.treemodel)45 selection = self.listview.get_selection()46 selection.set_mode(Gtk.SelectionMode.SINGLE)47 48 renderer_icon = CellRendererIcon(self.listview)49 renderer_icon.props.icon_name = 'emblem-notification'50 renderer_icon.props.width = 2051 renderer_icon.props.height = 2052 renderer_icon.props.size = 2053 treecol_icon = Gtk.TreeViewColumn()54 treecol_icon.pack_start(renderer_icon, False)55 treecol_icon.set_cell_data_func(renderer_icon, self._set_icon)56 self.listview.append_column(treecol_icon)57 58 renderer_idx = Gtk.CellRendererText()59 treecol_idx = Gtk.TreeViewColumn(_('No.'))60 treecol_idx.pack_start(renderer_idx, True)61 treecol_idx.set_cell_data_func(renderer_idx, self._set_number)62 self.listview.append_column(treecol_idx)63 64 renderer_title = Gtk.CellRendererText()65 renderer_title.set_property('ellipsize', Pango.EllipsizeMode.END)66 treecol_title = Gtk.TreeViewColumn(_('Play List'))67 treecol_title.pack_start(renderer_title, True)68 treecol_title.set_cell_data_func(renderer_title, self._set_title)69 self.listview.append_column(treecol_title)70 71 # we don't support search in the playlist for the moment:72 self.listview.set_enable_search(False)73 74 self.listview.connect('row-activated', self.__on_row_activated)75 76 self.add(self.listview)77 78 def __on_row_activated(self, treeview, path, col):79 model = treeview.get_model()80 81 treeiter = model.get_iter(path)82 media_idx = model.get_value(treeiter, COLUMNS['index'])83 self._play_callback(media_idx)84 85 def _set_number(self, column, cell, model, it, data):86 idx = model.get_value(it, COLUMNS['index'])87 cell.set_property('text', idx + 1)88 89 def _set_title(self, column, cell, model, it, data):90 playlist_item = model.get_value(it, COLUMNS['media'])91 available = model.get_value(it, COLUMNS['available'])92 93 cell.set_property('text', playlist_item['title'])94 sensitive = True95 if not available:96 sensitive = False97 cell.set_property('sensitive', sensitive)98 99 def _set_icon(self, column, cell, model, it, data):100 available = model.get_value(it, COLUMNS['available'])101 cell.set_property('visible', not available)102 103 def update(self, playlist):104 self.treemodel.clear()105 self._playlist = playlist106 pl = list(enumerate(playlist))107 for i, media in pl:108 available = self.check_available_media(media['url'])109 media['available'] = available110 self.treemodel.append((i, media, available))111 #self.set_cursor(0)112 113 def set_cursor(self, index):114 self.listview.set_cursor((index,))115 116 def delete_selected_items(self):117 selection = self.listview.get_selection()118 sel_model, sel_rows = self.listview.get_selection().get_selected_rows()119 for row in sel_rows:120 index = sel_model.get_value(sel_model.get_iter(row), 0)121 self._playlist.pop(index)122 self.treemodel.remove(self.treemodel.get_iter(row))123 self.update(self._playlist)124 125 def check_available_media(self, uri):126 path = uri.replace('journal://', '').replace('file://', '')127 if os.path.exists(path):128 return True129 else:130 return False131 132 def get_missing_tracks(self):133 missing_tracks = []134 for track in self._playlist:135 if not track['available']:136 missing_tracks.append(track)137 return missing_tracks