~ubuntu-branches/ubuntu/natty/miro/natty

« back to all changes in this revision

Viewing changes to lib/frontends/widgets/application.py

  • Committer: Bazaar Package Importer
  • Author(s): Bryce Harrington
  • Date: 2011-01-22 02:46:33 UTC
  • mfrom: (1.4.10 upstream) (1.7.5 experimental)
  • Revision ID: james.westby@ubuntu.com-20110122024633-kjme8u93y2il5nmf
Tags: 3.5.1-1ubuntu1
* Merge from debian.  Remaining ubuntu changes:
  - Use python 2.7 instead of python 2.6
  - Relax dependency on python-dbus to >= 0.83.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Miro - an RSS based video player application
 
2
# Copyright (C) 2005-2010 Participatory Culture Foundation
 
3
#
 
4
# This program is free software; you can redistribute it and/or modify
 
5
# it under the terms of the GNU General Public License as published by
 
6
# the Free Software Foundation; either version 2 of the License, or
 
7
# (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 of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU General Public License
 
15
# along with this program; if not, write to the Free Software
 
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
 
17
#
 
18
# In addition, as a special exception, the copyright holders give
 
19
# permission to link the code of portions of this program with the OpenSSL
 
20
# library.
 
21
#
 
22
# You must obey the GNU General Public License in all respects for all of
 
23
# the code used other than OpenSSL. If you modify file(s) with this
 
24
# exception, you may extend this exception to your version of the file(s),
 
25
# but you are not obligated to do so. If you do not wish to do so, delete
 
26
# this exception statement from your version. If you delete this exception
 
27
# statement from all source files in the program, then also delete it here.
 
28
 
 
29
"""``miro.frontends.widgets.application`` -- The application module
 
30
holds :class:`Application` and the portable code to handle the high
 
31
level running of the Miro application.
 
32
 
 
33
It also holds:
 
34
 
 
35
* :class:`InfoUpdater` -- tracks channel/item updates from the backend
 
36
  and sends the information to the frontend
 
37
* :class:`InfoUpdaterCallbackList` -- tracks the list of callbacks for
 
38
  info updater
 
39
* :class:`WidgetsMessageHandler` -- frontend message handler
 
40
* :class:`FrontendStatesStore` -- stores state of the frontend
 
41
"""
 
42
 
 
43
import os
 
44
import logging
 
45
import sys
 
46
import urllib
 
47
 
 
48
from miro import app
 
49
from miro import config
 
50
from miro import crashreport
 
51
from miro import prefs
 
52
from miro import feed
 
53
from miro import startup
 
54
from miro import signals
 
55
from miro import messages
 
56
from miro import eventloop
 
57
from miro import videoconversion
 
58
from miro.gtcache import gettext as _
 
59
from miro.gtcache import ngettext
 
60
from miro.frontends.widgets import dialogs
 
61
from miro.frontends.widgets import newsearchfeed
 
62
from miro.frontends.widgets import newfeed
 
63
from miro.frontends.widgets import newfolder
 
64
from miro.frontends.widgets import itemedit
 
65
from miro.frontends.widgets import addtoplaylistdialog
 
66
from miro.frontends.widgets import removefeeds
 
67
from miro.frontends.widgets import diagnostics
 
68
from miro.frontends.widgets import crashdialog
 
69
from miro.frontends.widgets import itemlist
 
70
from miro.frontends.widgets import itemlistcontroller
 
71
from miro.frontends.widgets import prefpanel
 
72
from miro.frontends.widgets import displays
 
73
from miro.frontends.widgets import menus
 
74
from miro.frontends.widgets import tablistmanager
 
75
from miro.frontends.widgets import playback
 
76
from miro.frontends.widgets import search
 
77
from miro.frontends.widgets import rundialog
 
78
from miro.frontends.widgets import watchedfolders
 
79
from miro.frontends.widgets import quitconfirmation
 
80
from miro.frontends.widgets import firsttimedialog
 
81
from miro.frontends.widgets.widgetconst import MAX_VOLUME
 
82
from miro.frontends.widgets.window import MiroWindow
 
83
from miro.plat.frontends.widgets.threads import call_on_ui_thread
 
84
from miro.plat.frontends.widgets.widgetset import Rect
 
85
from miro.plat.utils import FilenameType
 
86
from miro import fileutil
 
87
 
 
88
class Application:
 
89
    """This class holds the portable application code.  Each platform
 
90
    extends this class with a platform-specific version.
 
91
    """
 
92
    def __init__(self):
 
93
        app.widgetapp = self
 
94
        self.ignore_errors = False
 
95
        self.message_handler = WidgetsMessageHandler()
 
96
        self.default_guide_info = None
 
97
        self.window = None
 
98
        self.ui_initialized = False
 
99
        messages.FrontendMessage.install_handler(self.message_handler)
 
100
        app.info_updater = InfoUpdater()
 
101
        app.watched_folder_manager = watchedfolders.WatchedFolderManager()
 
102
        self.download_count = 0
 
103
        self.paused_count = 0
 
104
        self.unwatched_count = 0
 
105
 
 
106
    def exception_handler(self, type, value, traceback):
 
107
        report = crashreport.format_crash_report("in frontend thread",
 
108
            exc_info=(type, value, traceback), details=None)
 
109
        self.handle_crash_report(report)
 
110
 
 
111
    def startup(self):
 
112
        """Connects to signals, installs handlers, and calls :meth:`startup`
 
113
        from the :mod:`miro.startup` module.
 
114
        """
 
115
        self.connect_to_signals()
 
116
        startup.install_movies_directory_gone_handler(self.handle_movies_directory_gone)
 
117
        startup.install_first_time_handler(self.handle_first_time)
 
118
        startup.startup()
 
119
 
 
120
    def startup_ui(self):
 
121
        """Starts up the widget ui by sending a bunch of messages
 
122
        requesting data from the backend.  Also sets up managers,
 
123
        initializes the ui, and displays the :class:`MiroWindow`.
 
124
        """
 
125
        # Send a couple messages to the backend, when we get responses,
 
126
        # WidgetsMessageHandler() will call build_window()
 
127
        messages.TrackGuides().send_to_backend()
 
128
        messages.QuerySearchInfo().send_to_backend()
 
129
        messages.TrackWatchedFolders().send_to_backend()
 
130
        messages.QueryFrontendState().send_to_backend()
 
131
        messages.TrackChannels().send_to_backend()
 
132
 
 
133
        app.item_list_controller_manager = \
 
134
                itemlistcontroller.ItemListControllerManager()
 
135
        app.display_manager = displays.DisplayManager()
 
136
        app.menu_manager = menus.MenuStateManager()
 
137
        app.playback_manager = playback.PlaybackManager()
 
138
        app.search_manager = search.SearchManager()
 
139
        app.inline_search_memory = search.InlineSearchMemory()
 
140
        app.tab_list_manager = tablistmanager.TabListManager()
 
141
        self.ui_initialized = True
 
142
 
 
143
        self.window = MiroWindow(config.get(prefs.LONG_APP_NAME),
 
144
                                 self.get_main_window_dimensions())
 
145
        self.window.connect_weak('key-press', self.on_key_press)
 
146
        self._window_show_callback = self.window.connect_weak('show',
 
147
                self.on_window_show)
 
148
 
 
149
    def on_window_show(self, window):
 
150
        m = messages.FrontendStarted()
 
151
        # Use call_on_ui_thread to introduce a bit of a delay.  On GTK it uses
 
152
        # gobject.add_idle(), so it won't run until the GUI processing is
 
153
        # idle.  I'm (BDK) not sure what happens on Cocoa, but it's worth a
 
154
        # try there as well.
 
155
        call_on_ui_thread(m.send_to_backend)
 
156
        self.window.disconnect(self._window_show_callback)
 
157
        del self._window_show_callback
 
158
 
 
159
    def on_key_press(self, window, key, mods):
 
160
        if (app.playback_manager.is_playing and
 
161
                app.playback_manager.detached_window is None):
 
162
            return playback.handle_key_press(key, mods)
 
163
 
 
164
    def handle_movies_directory_gone(self, continue_callback):
 
165
        call_on_ui_thread(self._handle_movies_directory_gone, continue_callback)
 
166
 
 
167
    def _handle_movies_directory_gone(self, continue_callback):
 
168
        title = _("Movies directory gone")
 
169
        description = _(
 
170
            "%(shortappname)s can't use the primary video directory "
 
171
            "located at:\n"
 
172
            "\n"
 
173
            "%(moviedirectory)s\n"
 
174
            "\n"
 
175
            "This may be because it is located on an external drive that "
 
176
            "is not connected, is a directory that %(shortappname)s does "
 
177
            "not have write permission to, or there is something that is "
 
178
            "not a directory at that path.\n"
 
179
            "\n"
 
180
            "If you continue, the primary video directory will be reset "
 
181
            "to a location on this drive.  If you had videos downloaded "
 
182
            "this will cause %(shortappname)s to lose details about those "
 
183
            "videos.\n"
 
184
            "\n"
 
185
            "If you quit, then you can connect the drive or otherwise "
 
186
            "fix the problem and relaunch %(shortappname)s.",
 
187
            {"shortappname": config.get(prefs.SHORT_APP_NAME),
 
188
             "moviedirectory": config.get(prefs.MOVIES_DIRECTORY)}
 
189
        )
 
190
        ret = dialogs.show_choice_dialog(title, description,
 
191
                [dialogs.BUTTON_CONTINUE, dialogs.BUTTON_QUIT])
 
192
 
 
193
        if ret == dialogs.BUTTON_QUIT:
 
194
            self.do_quit()
 
195
            return
 
196
 
 
197
        continue_callback()
 
198
 
 
199
    def handle_first_time(self, continue_callback):
 
200
        call_on_ui_thread(lambda: self._handle_first_time(continue_callback))
 
201
 
 
202
    def _handle_first_time(self, continue_callback):
 
203
        startup.mark_first_time()
 
204
        firsttimedialog.FirstTimeDialog(continue_callback).run()
 
205
 
 
206
    def build_window(self):
 
207
        app.tab_list_manager.populate_tab_list()
 
208
        for info in self.message_handler.initial_guides:
 
209
            app.tab_list_manager.site_list.add(info)
 
210
        app.tab_list_manager.site_list.model_changed()
 
211
        app.tab_list_manager.handle_startup_selection()
 
212
        videobox = self.window.videobox
 
213
        videobox.volume_slider.set_value(config.get(prefs.VOLUME_LEVEL))
 
214
        videobox.volume_slider.connect('changed', self.on_volume_change)
 
215
        videobox.volume_slider.connect('released', self.on_volume_set)
 
216
        videobox.volume_muter.connect('clicked', self.on_volume_mute)
 
217
        videobox.controls.play.connect('clicked', self.on_play_clicked)
 
218
        videobox.controls.stop.connect('clicked', self.on_stop_clicked)
 
219
        videobox.controls.forward.connect('clicked', self.on_forward_clicked)
 
220
        videobox.controls.forward.connect('held-down', self.on_fast_forward)
 
221
        videobox.controls.forward.connect('released', self.on_stop_fast_playback)
 
222
        videobox.controls.previous.connect('clicked', self.on_previous_clicked)
 
223
        videobox.controls.previous.connect('held-down', self.on_fast_backward)
 
224
        videobox.controls.previous.connect('released', self.on_stop_fast_playback)
 
225
        videobox.controls.fullscreen.connect('clicked', self.on_fullscreen_clicked)
 
226
        self.window.show()
 
227
        messages.TrackPlaylists().send_to_backend()
 
228
        messages.TrackDownloadCount().send_to_backend()
 
229
        messages.TrackPausedCount().send_to_backend()
 
230
        messages.TrackNewVideoCount().send_to_backend()
 
231
        messages.TrackNewAudioCount().send_to_backend()
 
232
        messages.TrackUnwatchedCount().send_to_backend()
 
233
 
 
234
    def get_main_window_dimensions(self):
 
235
        """Override this to provide platform-specific Main Window dimensions.
 
236
 
 
237
        Must return a Rect.
 
238
        """
 
239
        return Rect(100, 300, 800, 600)
 
240
 
 
241
    def get_right_width(self):
 
242
        """Returns the width of the right side of the splitter.
 
243
        """
 
244
        return self.window.get_frame().get_width() - self.window.splitter.get_left_width()
 
245
 
 
246
    def on_volume_change(self, slider, volume):
 
247
        app.playback_manager.set_volume(volume)
 
248
 
 
249
    def on_volume_mute(self, button=None):
 
250
        slider = self.window.videobox.volume_slider
 
251
        if slider.get_value() == 0:
 
252
            value = getattr(self, "previous_volume_value", 0.75)
 
253
        else:
 
254
            self.previous_volume_value = slider.get_value()
 
255
            value = 0.0
 
256
 
 
257
        slider.set_value(value)
 
258
        self.on_volume_change(slider, value)
 
259
        self.on_volume_set(slider)
 
260
 
 
261
    def on_volume_set(self, slider):
 
262
        self.on_volume_value_set(slider.get_value())
 
263
    
 
264
    def on_volume_value_set(self, value):
 
265
        config.set(prefs.VOLUME_LEVEL, value)
 
266
        config.save()
 
267
 
 
268
    def on_play_clicked(self, button=None):
 
269
        if app.playback_manager.is_playing:
 
270
            app.playback_manager.play_pause()
 
271
        else:
 
272
            self.play_selection()
 
273
 
 
274
    def play_selection(self):
 
275
        app.item_list_controller_manager.play_selection()
 
276
 
 
277
    def on_stop_clicked(self, button=None):
 
278
        app.playback_manager.stop()
 
279
 
 
280
    def on_forward_clicked(self, button=None):
 
281
        app.playback_manager.play_next_item()
 
282
 
 
283
    def on_previous_clicked(self, button=None):
 
284
        app.playback_manager.play_prev_item(from_user=True)
 
285
 
 
286
    def on_skip_forward(self):
 
287
        app.playback_manager.skip_forward()
 
288
 
 
289
    def on_skip_backward(self):
 
290
        app.playback_manager.skip_backward()
 
291
 
 
292
    def on_fast_forward(self, button=None):
 
293
        app.playback_manager.fast_forward()
 
294
 
 
295
    def on_fast_backward(self, button=None):
 
296
        app.playback_manager.fast_backward()
 
297
 
 
298
    def on_stop_fast_playback(self, button):
 
299
        app.playback_manager.stop_fast_playback()
 
300
 
 
301
    def on_fullscreen_clicked(self, button=None):
 
302
        app.playback_manager.fullscreen()
 
303
 
 
304
    def on_toggle_detach_clicked(self, button=None):
 
305
        app.playback_manager.toggle_detached_mode()
 
306
 
 
307
    def up_volume(self):
 
308
        slider = self.window.videobox.volume_slider
 
309
        v = min(slider.get_value() + 0.05, MAX_VOLUME)
 
310
        slider.set_value(v)
 
311
        self.on_volume_change(slider, v)
 
312
        self.on_volume_set(slider)
 
313
 
 
314
    def down_volume(self):
 
315
        slider = self.window.videobox.volume_slider
 
316
        v = max(slider.get_value() - 0.05, 0.0)
 
317
        slider.set_value(v)
 
318
        self.on_volume_change(slider, v)
 
319
        self.on_volume_set(slider)
 
320
 
 
321
    def share_item(self, item):
 
322
        share_items = {"file_url": item.file_url,
 
323
                       "item_name": item.name.encode('utf-8')}
 
324
        if item.feed_url:
 
325
            share_items["feed_url"] = item.feed_url
 
326
        query_string = "&".join(["%s=%s" % (key, urllib.quote(val)) for key, val in share_items.items()])
 
327
        share_url = "%s/item/?%s" % (config.get(prefs.SHARE_URL), query_string)
 
328
        self.open_url(share_url)
 
329
 
 
330
    def share_feed(self):
 
331
        t, channel_infos = app.tab_list_manager.get_selection()
 
332
        if t in ('feed', 'audio-feed') and len(channel_infos) == 1:
 
333
            ci = channel_infos[0]
 
334
            share_items = {"feed_url": ci.base_href}
 
335
            query_string = "&".join(["%s=%s" % (key, urllib.quote(val)) for key, val in share_items.items()])
 
336
            share_url = "%s/feed/?%s" % (config.get(prefs.SHARE_URL), query_string)
 
337
            self.open_url(share_url)
 
338
 
 
339
    def delete_backup_databases(self):
 
340
        dbs = app.db.get_backup_databases()
 
341
        for mem in dbs:
 
342
            fileutil.remove(mem)
 
343
 
 
344
    def check_then_reveal_file(self, filename):
 
345
        if not os.path.exists(filename):
 
346
            basename = os.path.basename(filename)
 
347
            dialogs.show_message(
 
348
                _("Error Revealing File"),
 
349
                _("The file %(filename)s was deleted from outside %(appname)s.",
 
350
                  {"filename": basename,
 
351
                   "appname": config.get(prefs.SHORT_APP_NAME)}),
 
352
                dialogs.WARNING_MESSAGE)
 
353
        else:
 
354
            self.reveal_file(filename)
 
355
 
 
356
    def reveal_conversions_folder(self):
 
357
        self.reveal_file(videoconversion.get_conversions_folder())
 
358
 
 
359
    def open_video(self):
 
360
        title = _('Open Files...')
 
361
        filenames = dialogs.ask_for_open_pathname(title, select_multiple=True)
 
362
 
 
363
        if not filenames:
 
364
            return
 
365
 
 
366
        filenames_good = [mem for mem in filenames if os.path.isfile(mem)]
 
367
        if len(filenames_good) != len(filenames):
 
368
            filenames_bad = set(filenames) - set(filenames_good)
 
369
            if len(filenames_bad) == 1:
 
370
                filename = list(filenames_bad)[0]
 
371
                dialogs.show_message(_('Open Files - Error'),
 
372
                                     _('File %(filename)s does not exist.',
 
373
                                       {"filename": filename}),
 
374
                                     dialogs.WARNING_MESSAGE)
 
375
            else:
 
376
                dialogs.show_message(_('Open Files - Error'),
 
377
                                    _('The following files do not exist:') +
 
378
                                    '\n' + '\n'.join(filenames_bad),
 
379
                                     dialogs.WARNING_MESSAGE)
 
380
        else:
 
381
            if len(filenames_good) == 1:
 
382
                messages.OpenIndividualFile(filenames_good[0]).send_to_backend()
 
383
            else:
 
384
                messages.OpenIndividualFiles(filenames_good).send_to_backend()
 
385
 
 
386
    def ask_for_url(self, title, description, error_title, error_description):
 
387
        """Ask the user to enter a url in a TextEntry box.
 
388
 
 
389
        If the URL the user enters is invalid, she will be asked to re-enter
 
390
        it again.  This process repeats until the user enters a valid URL, or
 
391
        clicks Cancel.
 
392
 
 
393
        The initial text for the TextEntry will be the clipboard contents (if
 
394
        it is a valid URL).
 
395
        """
 
396
        text = app.widgetapp.get_clipboard_text()
 
397
        if text is not None and feed.validate_feed_url(text):
 
398
            text = feed.normalize_feed_url(text)
 
399
        else:
 
400
            text = ""
 
401
        while 1:
 
402
            text = dialogs.ask_for_string(title, description, initial_text=text)
 
403
            if text == None:
 
404
                return
 
405
 
 
406
            normalized_url = feed.normalize_feed_url(text)
 
407
            if feed.validate_feed_url(normalized_url):
 
408
                return normalized_url
 
409
 
 
410
            title = error_title
 
411
            description = error_description
 
412
 
 
413
    def new_download(self):
 
414
        url = self.ask_for_url( _('New Download'),
 
415
                _('Enter the URL of the item to download'),
 
416
                _('New Download - Invalid URL'),
 
417
                _('The address you entered is not a valid url.\nPlease check the URL and try again.\n\nEnter the URL of the item to download'))
 
418
        if url is not None:
 
419
            messages.DownloadURL(url).send_to_backend()
 
420
 
 
421
    def check_version(self):
 
422
        # this gets called by the backend, so it has to send a message to
 
423
        # the frontend to open a dialog
 
424
        def up_to_date_callback():
 
425
            messages.MessageToUser(_("%(appname)s is up to date",
 
426
                                     {'appname': config.get(prefs.SHORT_APP_NAME)}),
 
427
                                   _("%(appname)s is up to date!",
 
428
                                     {'appname': config.get(prefs.SHORT_APP_NAME)})).send_to_frontend()
 
429
 
 
430
        messages.CheckVersion(up_to_date_callback).send_to_backend()
 
431
 
 
432
    def preferences(self):
 
433
        prefpanel.show_window()
 
434
 
 
435
    def remove_items(self, selection=None):
 
436
        if not selection:
 
437
            selection = app.item_list_controller_manager.get_selection()
 
438
            selection = [s for s in selection if s.downloaded]
 
439
 
 
440
            if not selection:
 
441
                return
 
442
 
 
443
        external_count = len([s for s in selection if s.is_external])
 
444
        folder_count = len([s for s in selection if s.is_container_item])
 
445
        total_count = len(selection)
 
446
 
 
447
        if total_count == 1 and external_count == folder_count == 0:
 
448
            messages.DeleteVideo(selection[0].id).send_to_backend()
 
449
            return
 
450
 
 
451
        title = ngettext('Remove item', 'Remove items', total_count)
 
452
 
 
453
        if external_count > 0:
 
454
            description = ngettext(
 
455
                'One of these items was not downloaded from a feed. '
 
456
                'Would you like to delete it or just remove it from the Library?',
 
457
 
 
458
                'Some of these items were not downloaded from a feed. '
 
459
                'Would you like to delete them or just remove them from the Library?',
 
460
 
 
461
                external_count
 
462
            )
 
463
            if folder_count > 0:
 
464
                description += '\n\n' + ngettext(
 
465
                    'One of these items is a folder.  Delete/Removeing a '
 
466
                    "folder will delete/remove it's contents",
 
467
 
 
468
                    'Some of these items are folders.  Delete/Removeing a '
 
469
                    "folder will delete/remove it's contents",
 
470
 
 
471
                    folder_count
 
472
                )
 
473
            ret = dialogs.show_choice_dialog(title, description,
 
474
                                             [dialogs.BUTTON_REMOVE_ENTRY,
 
475
                                              dialogs.BUTTON_DELETE_FILE,
 
476
                                              dialogs.BUTTON_CANCEL])
 
477
 
 
478
        else:
 
479
            description = ngettext(
 
480
                'Are you sure you want to delete the item?',
 
481
                'Are you sure you want to delete all %(count)d items?',
 
482
                total_count,
 
483
                {"count": total_count}
 
484
            )
 
485
            if folder_count > 0:
 
486
                description += '\n\n' + ngettext(
 
487
                    'One of these items is a folder.  Deleting a '
 
488
                    "folder will delete it's contents",
 
489
 
 
490
                    'Some of these items are folders.  Deleting a '
 
491
                    "folder will delete it's contents",
 
492
 
 
493
                    folder_count
 
494
                )
 
495
            ret = dialogs.show_choice_dialog(title, description,
 
496
                                             [dialogs.BUTTON_DELETE,
 
497
                                              dialogs.BUTTON_CANCEL])
 
498
 
 
499
        if ret in (dialogs.BUTTON_OK, dialogs.BUTTON_DELETE_FILE,
 
500
                dialogs.BUTTON_DELETE):
 
501
            for mem in selection:
 
502
                messages.DeleteVideo(mem.id).send_to_backend()
 
503
 
 
504
        elif ret == dialogs.BUTTON_REMOVE_ENTRY:
 
505
            for mem in selection:
 
506
                if mem.is_external:
 
507
                    messages.RemoveVideoEntry(mem.id).send_to_backend()
 
508
                else:
 
509
                    messages.DeleteVideo(mem.id).send_to_backend()
 
510
 
 
511
    def edit_item(self):
 
512
        selection = app.item_list_controller_manager.get_selection()
 
513
        selection = [s for s in selection if s.downloaded]
 
514
 
 
515
        if not selection:
 
516
            return
 
517
 
 
518
        item_info = selection[0]
 
519
 
 
520
        change_dict = itemedit.run_dialog(item_info)
 
521
        if change_dict:
 
522
            messages.EditItem(item_info.id, change_dict).send_to_backend()
 
523
 
 
524
    def save_item(self):
 
525
        selection = app.item_list_controller_manager.get_selection()
 
526
        selection = [s for s in selection if s.downloaded]
 
527
 
 
528
        if not selection:
 
529
            return
 
530
 
 
531
        title = _('Save Item As...')
 
532
        filename = selection[0].video_path
 
533
        filename = os.path.basename(filename)
 
534
        filename = dialogs.ask_for_save_pathname(title, filename)
 
535
 
 
536
        if not filename:
 
537
            return
 
538
 
 
539
        messages.SaveItemAs(selection[0].id, filename).send_to_backend()
 
540
 
 
541
    def convert_items(self, converter_id):
 
542
        selection = app.item_list_controller_manager.get_selection()
 
543
        for item_info in selection:
 
544
            videoconversion.convert(converter_id, item_info)
 
545
 
 
546
    def copy_item_url(self):
 
547
        selection = app.item_list_controller_manager.get_selection()
 
548
        selection = [s for s in selection if s.downloaded]
 
549
 
 
550
        if not selection and app.playback_manager.is_playing:
 
551
            selection = [app.playback_manager.get_playing_item()]
 
552
 
 
553
        if not selection:
 
554
            return
 
555
 
 
556
        selection = selection[0]
 
557
        if selection.file_url:
 
558
            app.widgetapp.copy_text_to_clipboard(selection.file_url)
 
559
 
 
560
    def add_new_feed(self):
 
561
        url, section = newfeed.run_dialog()
 
562
        if url is not None:
 
563
            messages.NewFeed(url, section).send_to_backend()
 
564
 
 
565
    def add_new_search_feed(self):
 
566
        data = newsearchfeed.run_dialog()
 
567
 
 
568
        if not data:
 
569
            return
 
570
 
 
571
        if data[0] == "feed":
 
572
            messages.NewFeedSearchFeed(data[1], data[2], data[3]).send_to_backend()
 
573
        elif data[0] == "search_engine":
 
574
            messages.NewFeedSearchEngine(data[1], data[2], data[3]).send_to_backend()
 
575
        elif data[0] == "url":
 
576
            messages.NewFeedSearchURL(data[1], data[2], data[3]).send_to_backend()
 
577
 
 
578
    def add_new_feed_folder(self, add_selected=False, default_type='feed'):
 
579
        name, section = newfolder.run_dialog(default_type)
 
580
        if name is not None:
 
581
            if add_selected:
 
582
                t, infos = app.tab_list_manager.get_selection()
 
583
                child_ids = [info.id for info in infos]
 
584
            else:
 
585
                child_ids = None
 
586
            messages.NewFeedFolder(name, section, child_ids).send_to_backend()
 
587
 
 
588
    def add_new_guide(self):
 
589
        url = self.ask_for_url(_('Add Website'),
 
590
                _('Enter the URL of the website to add'),
 
591
                _('Add Website - Invalid URL'),
 
592
                _("The address you entered is not a valid url.\n"
 
593
                  "Please check the URL and try again.\n\n"
 
594
                  "Enter the URL of the website to add"))
 
595
 
 
596
        if url is not None:
 
597
            messages.NewGuide(url).send_to_backend()
 
598
 
 
599
    def remove_something(self):
 
600
        t, infos = app.tab_list_manager.get_selection_and_children()
 
601
        if t in ('feed', 'audio-feed'):
 
602
            self.remove_feeds(infos)
 
603
        elif t in('site'):
 
604
            self.remove_sites(infos)
 
605
 
 
606
    def remove_current_feed(self):
 
607
        t, channel_infos = app.tab_list_manager.get_selection_and_children()
 
608
        if t in ('feed', 'audio-feed'):
 
609
            self.remove_feeds(channel_infos)
 
610
 
 
611
    def remove_feeds(self, channel_infos):
 
612
        has_watched_feeds = False
 
613
        downloaded_items = False
 
614
        downloading_items = False
 
615
 
 
616
        for ci in channel_infos:
 
617
            if not ci.is_directory_feed:
 
618
                if ci.num_downloaded > 0:
 
619
                    downloaded_items = True
 
620
 
 
621
                if ci.has_downloading:
 
622
                    downloading_items = True
 
623
            else:
 
624
                has_watched_feeds = True
 
625
 
 
626
        ret = removefeeds.run_dialog(channel_infos, downloaded_items,
 
627
                downloading_items, has_watched_feeds)
 
628
        if ret:
 
629
            for ci in channel_infos:
 
630
                if ci.is_directory_feed:
 
631
                    messages.SetWatchedFolderVisible(ci.id, False).send_to_backend()
 
632
                else:
 
633
                    messages.DeleteFeed(ci.id, ci.is_folder,
 
634
                        ret[removefeeds.KEEP_ITEMS]
 
635
                    ).send_to_backend()
 
636
 
 
637
    def update_selected_feeds(self):
 
638
        t, channel_infos = app.tab_list_manager.get_selection()
 
639
        if t in ('feed', 'audio-feed'):
 
640
            for ci in channel_infos:
 
641
                if ci.is_folder:
 
642
                    messages.UpdateFeedFolder(ci.id).send_to_backend()
 
643
                else:
 
644
                    messages.UpdateFeed(ci.id).send_to_backend()
 
645
 
 
646
    def update_all_feeds(self):
 
647
        messages.UpdateAllFeeds().send_to_backend()
 
648
 
 
649
    def import_feeds(self):
 
650
        title = _('Import OPML File')
 
651
        filename = dialogs.ask_for_open_pathname(title,
 
652
                filters=[(_('OPML Files'), ['opml'])])
 
653
        if not filename:
 
654
            return
 
655
 
 
656
        if os.path.isfile(filename):
 
657
            messages.ImportFeeds(filename).send_to_backend()
 
658
        else:
 
659
            dialogs.show_message(_('Import OPML File - Error'),
 
660
                                 _('File %(filename)s does not exist.',
 
661
                                   {"filename": filename}),
 
662
                                 dialogs.WARNING_MESSAGE)
 
663
 
 
664
    def export_feeds(self):
 
665
        title = _('Export OPML File')
 
666
        slug = config.get(prefs.SHORT_APP_NAME).lower()
 
667
        slug = slug.replace(' ', '_').replace('-', '_')
 
668
        filepath = dialogs.ask_for_save_pathname(title, "%s_subscriptions.opml" % slug)
 
669
 
 
670
        if not filepath:
 
671
            return
 
672
 
 
673
        messages.ExportSubscriptions(filepath).send_to_backend()
 
674
 
 
675
    def copy_feed_url(self):
 
676
        t, channel_infos = app.tab_list_manager.get_selection()
 
677
        if t in ('feed', 'audio-feed') and len(channel_infos) == 1:
 
678
            app.widgetapp.copy_text_to_clipboard(channel_infos[0].url)
 
679
 
 
680
    def copy_site_url(self):
 
681
        t, site_infos = app.tab_list_manager.get_selection()
 
682
        if t == 'site':
 
683
            app.widgetapp.copy_text_to_clipboard(site_infos[0].url)
 
684
 
 
685
    def add_new_playlist(self):
 
686
        selection = app.item_list_controller_manager.get_selection()
 
687
        ids = [s.id for s in selection if s.downloaded]
 
688
 
 
689
        title = _('Create Playlist')
 
690
        description = _('Enter a name for the new playlist')
 
691
 
 
692
        name = dialogs.ask_for_string(title, description)
 
693
        if name:
 
694
            messages.NewPlaylist(name, ids).send_to_backend()
 
695
 
 
696
    def add_to_playlist(self):
 
697
        selection = app.item_list_controller_manager.get_selection()
 
698
        ids = [s.id for s in selection if s.downloaded]
 
699
 
 
700
        data = addtoplaylistdialog.run_dialog()
 
701
 
 
702
        if not data:
 
703
            return
 
704
 
 
705
        if data[0] == "existing":
 
706
            messages.AddVideosToPlaylist(data[1].id, ids).send_to_backend()
 
707
        elif data[0] == "new":
 
708
            messages.NewPlaylist(data[1], ids).send_to_backend()
 
709
 
 
710
    def add_new_playlist_folder(self, add_selected=False):
 
711
        title = _('Create Playlist Folder')
 
712
        description = _('Enter a name for the new playlist folder')
 
713
 
 
714
        name = dialogs.ask_for_string(title, description)
 
715
        if name:
 
716
            if add_selected:
 
717
                t, infos = app.tab_list_manager.get_selection()
 
718
                child_ids = [info.id for info in infos]
 
719
            else:
 
720
                child_ids = None
 
721
            messages.NewPlaylistFolder(name, child_ids).send_to_backend()
 
722
 
 
723
    def rename_something(self):
 
724
        t, channel_infos = app.tab_list_manager.get_selection()
 
725
        info = channel_infos[0]
 
726
 
 
727
        if t in ('feed', 'audio-feed') and info.is_folder:
 
728
            t = 'feed-folder'
 
729
        elif t == 'playlist' and info.is_folder:
 
730
            t = 'playlist-folder'
 
731
 
 
732
        if t == 'feed-folder':
 
733
            title = _('Rename Feed Folder')
 
734
            description = _('Enter a new name for the feed folder %(name)s',
 
735
                            {"name": info.name})
 
736
 
 
737
        elif t in ('feed', 'audio-feed'):
 
738
            title = _('Rename Feed')
 
739
            description = _('Enter a new name for the feed %(name)s',
 
740
                            {"name": info.name})
 
741
 
 
742
        elif t == 'playlist':
 
743
            title = _('Rename Playlist')
 
744
            description = _('Enter a new name for the playlist %(name)s',
 
745
                            {"name": info.name})
 
746
 
 
747
        elif t == 'playlist-folder':
 
748
            title = _('Rename Playlist Folder')
 
749
            description = _('Enter a new name for the playlist folder %(name)s',
 
750
                            {"name": info.name})
 
751
        elif t == 'site':
 
752
            title = _('Rename Website')
 
753
            description = _('Enter a new name for the website %(name)s',
 
754
                            {"name": info.name})
 
755
 
 
756
        else:
 
757
            raise AssertionError("Unknown tab type: %s" % t)
 
758
 
 
759
        name = dialogs.ask_for_string(title, description,
 
760
                                      initial_text=info.name)
 
761
        if name:
 
762
            messages.RenameObject(t, info.id, name).send_to_backend()
 
763
 
 
764
    def revert_feed_name(self):
 
765
        t, channel_infos = app.tab_list_manager.get_selection()
 
766
        if not channel_infos:
 
767
            return
 
768
        info = channel_infos[0]
 
769
        messages.RevertFeedTitle(info.id).send_to_backend()
 
770
 
 
771
    def remove_current_playlist(self):
 
772
        t, infos = app.tab_list_manager.get_selection()
 
773
        if t == 'playlist':
 
774
            self.remove_playlists(infos)
 
775
 
 
776
    def remove_playlists(self, playlist_infos):
 
777
        title = ngettext('Remove playlist', 'Remove playlists', len(playlist_infos))
 
778
        description = ngettext(
 
779
            'Are you sure you want to remove this playlist?',
 
780
            'Are you sure you want to remove these %(count)s playlists?',
 
781
            len(playlist_infos),
 
782
            {"count": len(playlist_infos)}
 
783
            )
 
784
 
 
785
        ret = dialogs.show_choice_dialog(title, description,
 
786
                                         [dialogs.BUTTON_REMOVE,
 
787
                                          dialogs.BUTTON_CANCEL])
 
788
 
 
789
        if ret == dialogs.BUTTON_REMOVE:
 
790
            for pi in playlist_infos:
 
791
                messages.DeletePlaylist(pi.id, pi.is_folder).send_to_backend()
 
792
 
 
793
    def remove_current_site(self):
 
794
        t, infos = app.tab_list_manager.get_selection()
 
795
        if t == 'site':
 
796
            self.remove_sites(infos)
 
797
    
 
798
    def remove_sites(self, infos):
 
799
        title = ngettext('Remove website', 'Remove websites', len(infos))
 
800
        description = ngettext(
 
801
            'Are you sure you want to remove this website?',
 
802
            'Are you sure you want to remove these %(count)s websites?',
 
803
            len(infos),
 
804
            {"count": len(infos)}
 
805
            )
 
806
 
 
807
        ret = dialogs.show_choice_dialog(title, description,
 
808
                [dialogs.BUTTON_REMOVE, dialogs.BUTTON_CANCEL])
 
809
 
 
810
        if ret == dialogs.BUTTON_REMOVE:
 
811
            for si in infos:
 
812
                messages.DeleteSite(si.id).send_to_backend()
 
813
 
 
814
    def quit_ui(self):
 
815
        """Quit  out of the UI event loop."""
 
816
        raise NotImplementedError()
 
817
 
 
818
    def about(self):
 
819
        dialogs.show_about()
 
820
 
 
821
    def diagnostics(self):
 
822
        diagnostics.run_dialog()
 
823
 
 
824
    def on_close(self):
 
825
        """This is called when the close button is pressed."""
 
826
        self.quit()
 
827
 
 
828
    def quit(self):
 
829
        ok1 = self._confirm_quit_if_downloading()
 
830
        ok2 = self._confirm_quit_if_converting()
 
831
        if ok1 and ok2:
 
832
            self.do_quit()
 
833
 
 
834
    def _confirm_quit_if_downloading(self):
 
835
        if config.get(prefs.WARN_IF_DOWNLOADING_ON_QUIT) and self.download_count > 0:
 
836
            ret = quitconfirmation.rundialog(
 
837
                _("Are you sure you want to quit?"),
 
838
                ngettext(
 
839
                    "You have %(count)d download in progress.  Quit anyway?",
 
840
                    "You have %(count)d downloads in progress.  Quit anyway?",
 
841
                    self.download_count,
 
842
                    {"count": self.download_count}
 
843
                ),
 
844
                _("Warn me when I attempt to quit with downloads in progress"),
 
845
                prefs.WARN_IF_DOWNLOADING_ON_QUIT
 
846
            )
 
847
            return ret
 
848
        return True
 
849
    
 
850
    def _confirm_quit_if_converting(self):
 
851
        running_count = videoconversion.conversion_manager.running_tasks_count()
 
852
        pending_count = videoconversion.conversion_manager.pending_tasks_count()
 
853
        conversions_count = running_count + pending_count
 
854
        if config.get(prefs.WARN_IF_CONVERTING_ON_QUIT) and conversions_count > 0:
 
855
            ret = quitconfirmation.rundialog(
 
856
                _("Are you sure you want to quit?"),
 
857
                ngettext(
 
858
                    "You have %(count)d conversion in progress.  Quit anyway?",
 
859
                    "You have %(count)d conversions in progress or pending.  Quit anyway?",
 
860
                    conversions_count,
 
861
                    {"count": conversions_count}
 
862
                ),
 
863
                _("Warn me when I attempt to quit with conversions in progress"),
 
864
                prefs.WARN_IF_CONVERTING_ON_QUIT
 
865
            )
 
866
            return ret
 
867
        return True
 
868
 
 
869
    def do_quit(self):
 
870
        if self.window is not None:
 
871
            self.window.close()
 
872
        if self.ui_initialized:
 
873
            if app.playback_manager.is_playing:
 
874
                app.playback_manager.stop()
 
875
            app.display_manager.deselect_all_displays()
 
876
        if self.window is not None:
 
877
            self.window.destroy()
 
878
        app.controller.shutdown()
 
879
        self.quit_ui()
 
880
 
 
881
    def connect_to_signals(self):
 
882
        signals.system.connect('error', self.handle_error)
 
883
        signals.system.connect('update-available', self.handle_update_available)
 
884
        signals.system.connect('new-dialog', self.handle_dialog)
 
885
        signals.system.connect('shutdown', self.on_backend_shutdown)
 
886
    
 
887
    def handle_unwatched_count_changed(self):
 
888
        pass
 
889
 
 
890
    def handle_dialog(self, obj, dialog):
 
891
        call_on_ui_thread(rundialog.run, dialog)
 
892
 
 
893
    def handle_update_available(self, obj, item):
 
894
        print "Update available! -- not implemented"
 
895
 
 
896
    def handle_up_to_date(self):
 
897
        print "Up to date! -- not implemented"
 
898
 
 
899
    def handle_error(self, obj, report):
 
900
        call_on_ui_thread(self.handle_crash_report, report)
 
901
 
 
902
    def handle_crash_report(self, report):
 
903
        if self.ignore_errors:
 
904
            logging.warn("Ignoring Error:\n%s", report)
 
905
            return
 
906
 
 
907
        ret = crashdialog.run_dialog(report)
 
908
        if ret == crashdialog.IGNORE_ERRORS:
 
909
            self.ignore_errors = True
 
910
 
 
911
    def on_backend_shutdown(self, obj):
 
912
        logging.info('Shutting down...')
 
913
 
 
914
class InfoUpdaterCallbackList(object):
 
915
    """Tracks the list of callbacks for InfoUpdater.
 
916
    """
 
917
 
 
918
    def __init__(self):
 
919
        self._callbacks = {}
 
920
 
 
921
    def add(self, type_, id_, callback):
 
922
        """Adds the callback to the list for ``type_`` ``id_``.
 
923
 
 
924
        :param type_: the type of the thing (feed, audio-feed, site, ...)
 
925
        :param id_: the id for the thing
 
926
        :param callback: the callback function to add
 
927
        """
 
928
        key = (type_, id_)
 
929
        self._callbacks.setdefault(key, set()).add(callback)
 
930
 
 
931
    def remove(self, type_, id_, callback):
 
932
        """Removes the callback from the list for ``type_`` ``id_``.
 
933
 
 
934
        :param type_: the type of the thing (feed, audio-feed, site, ...)
 
935
        :param id_: the id for the thing
 
936
        :param callback: the callback function to remove
 
937
        """
 
938
        key = (type_, id_)
 
939
        callback_set = self._callbacks[key]
 
940
        callback_set.remove(callback)
 
941
        if len(callback_set) == 0:
 
942
            del self._callbacks[key]
 
943
 
 
944
    def get(self, type_, id_):
 
945
        """Get the list of callbacks for ``type_``, ``id_``.
 
946
 
 
947
        :param type_: the type of the thing (feed, audio-feed, site, ...)
 
948
        :param id_: the id for the thing
 
949
        """
 
950
        key = (type_, id_)
 
951
        if key not in self._callbacks:
 
952
            return []
 
953
        else:
 
954
            # return a new list of callbacks, so that if we iterate over the
 
955
            # return value, we don't have to worry about callbacks being
 
956
            # removed midway.
 
957
            return list(self._callbacks[key])
 
958
 
 
959
class InfoUpdater(signals.SignalEmitter):
 
960
    """Track channel/item updates from the backend.
 
961
 
 
962
    To track item updates, use ``add_item_callback()``.  To track tab
 
963
    updates, connect to one of the signals below.
 
964
 
 
965
    Signals:
 
966
 
 
967
    * feeds-added (self, info_list) -- New video feeds were added
 
968
    * feeds-changed (self, info_list) -- Video feeds were changed
 
969
    * feeds-removed (self, info_list) -- Video feeds were removed
 
970
    * audio-feeds-added (self, info_list) -- New audio feeds were added
 
971
    * audio-feeds-changed (self, info_list) -- Audio feeds were changed
 
972
    * audio-feeds-removed (self, info_list) -- Audio feeds were removed
 
973
    * sites-added (self, info_list) -- New sites were added
 
974
    * sites-changed (self, info_list) -- Sites were changed
 
975
    * sites-removed (self, info_list) -- Sites were removed
 
976
    * playlists-added (self, info_list) -- New playlists were added
 
977
    * playlists-changed (self, info_list) -- Playlists were changed
 
978
    * playlists-removed (self, info_list) -- Playlists were removed
 
979
    """
 
980
    def __init__(self):
 
981
        signals.SignalEmitter.__init__(self)
 
982
        for prefix in ('feeds', 'audio-feeds', 'sites', 'playlists'):
 
983
            self.create_signal('%s-added' % prefix)
 
984
            self.create_signal('%s-changed' % prefix)
 
985
            self.create_signal('%s-removed' % prefix)
 
986
 
 
987
        self.item_list_callbacks = InfoUpdaterCallbackList()
 
988
        self.item_changed_callbacks = InfoUpdaterCallbackList()
 
989
 
 
990
    def handle_items_changed(self, message):
 
991
        callback_list = self.item_changed_callbacks
 
992
        for callback in callback_list.get(message.type, message.id):
 
993
            callback(message)
 
994
 
 
995
    def handle_item_list(self, message):
 
996
        callback_list = self.item_list_callbacks
 
997
        for callback in callback_list.get(message.type, message.id):
 
998
            callback(message)
 
999
 
 
1000
    def handle_tabs_changed(self, message):
 
1001
        if message.type == 'feed':
 
1002
            signal_start = 'feeds'
 
1003
        elif message.type == 'audio-feed':
 
1004
            signal_start = 'audio-feeds'
 
1005
        elif message.type == 'guide':
 
1006
            signal_start = 'sites'
 
1007
        elif message.type == 'playlist':
 
1008
            signal_start = 'playlists'
 
1009
        else:
 
1010
            return
 
1011
        if message.added:
 
1012
            self.emit('%s-added' % signal_start, message.added)
 
1013
        if message.changed:
 
1014
            self.emit('%s-changed' % signal_start, message.changed)
 
1015
        if message.removed:
 
1016
            self.emit('%s-removed' % signal_start, message.removed)
 
1017
 
 
1018
class WidgetsMessageHandler(messages.MessageHandler):
 
1019
    """Handles frontend messages.
 
1020
 
 
1021
    There's a method to handle each frontend message type.  See
 
1022
    :mod:`miro.messages` (``lib/messages.py``) and
 
1023
    :mod:`miro.messagehandler` (``lib/messagehandler.py``) for
 
1024
    more details.
 
1025
    """
 
1026
    def __init__(self):
 
1027
        messages.MessageHandler.__init__(self)
 
1028
        # Messages that we need to see before the UI is ready
 
1029
        self._pre_startup_messages = set([
 
1030
            'guide-list',
 
1031
            'search-info',
 
1032
            'frontend-state',
 
1033
        ])
 
1034
        if config.get(prefs.OPEN_CHANNEL_ON_STARTUP) is not None or \
 
1035
                config.get(prefs.OPEN_FOLDER_ON_STARTUP) is not None:
 
1036
            self._pre_startup_messages.add('feed-tab-list')
 
1037
            self._pre_startup_messages.add('audio-feed-tab-list')
 
1038
        self.progress_dialog = None
 
1039
        self.dbupgrade_progress_dialog = None
 
1040
 
 
1041
    def handle_frontend_quit(self, message):
 
1042
        app.widgetapp.do_quit()
 
1043
 
 
1044
    def handle_database_upgrade_start(self, message):
 
1045
        if self.dbupgrade_progress_dialog is None:
 
1046
            self.dbupgrade_progress_dialog = dialogs.DBUpgradeProgressDialog(
 
1047
                    _('Upgrading database'))
 
1048
            self.dbupgrade_progress_dialog.run()
 
1049
            # run() will return when we destroy the dialog because of a future
 
1050
            # message.
 
1051
            return
 
1052
 
 
1053
    def handle_database_upgrade_progress(self, message):
 
1054
        self.dbupgrade_progress_dialog.update(message.stage,
 
1055
                message.stage_progress, message.total_progress)
 
1056
 
 
1057
    def handle_database_upgrade_end(self, message):
 
1058
        self.dbupgrade_progress_dialog.destroy()
 
1059
        self.dbupgrade_progress_dialog = None
 
1060
 
 
1061
    def handle_startup_failure(self, message):
 
1062
        if hasattr(self, "_startup_failure_mode"):
 
1063
            return
 
1064
        self._startup_failure_mode = True
 
1065
 
 
1066
        dialogs.show_message(message.summary, message.description,
 
1067
                dialogs.CRITICAL_MESSAGE)
 
1068
        app.widgetapp.do_quit()
 
1069
 
 
1070
    def handle_startup_database_failure(self, message):
 
1071
        if hasattr(self, "_database_failure_mode"):
 
1072
            logging.info("already in db failure mode--skipping")
 
1073
            return
 
1074
        self._database_failure_mode = True
 
1075
 
 
1076
        ret = dialogs.show_choice_dialog(
 
1077
            message.summary, message.description,
 
1078
            choices=[dialogs.BUTTON_QUIT, dialogs.BUTTON_START_FRESH])
 
1079
        if ret == dialogs.BUTTON_START_FRESH:
 
1080
            def start_fresh():
 
1081
                try:
 
1082
                    app.db.reset_database()
 
1083
                except Exception:
 
1084
                    logging.exception(
 
1085
                        "gah!  exception in the "
 
1086
                        "handle_startup_database_failure section")
 
1087
                app.widgetapp.do_quit()
 
1088
            eventloop.add_urgent_call(start_fresh, "start fresh and quit")
 
1089
 
 
1090
        else:
 
1091
            app.widgetapp.do_quit()
 
1092
 
 
1093
    def handle_startup_success(self, message):
 
1094
        app.widgetapp.startup_ui()
 
1095
 
 
1096
    def _saw_pre_startup_message(self, name):
 
1097
        if name not in self._pre_startup_messages:
 
1098
            # we get here with the (audio-)?feed-tab-list messages when
 
1099
            # OPEN_(CHANNEL|FOLDER)_ON_STARTUP isn't defined
 
1100
            return
 
1101
        self._pre_startup_messages.remove(name)
 
1102
        if len(self._pre_startup_messages) == 0:
 
1103
            app.widgetapp.build_window()
 
1104
 
 
1105
    def call_handler(self, method, message):
 
1106
        # uncomment this next line if you need frontend messages
 
1107
        # logging.debug("handling frontend %s", message)
 
1108
        call_on_ui_thread(method, message)
 
1109
 
 
1110
    def handle_current_search_info(self, message):
 
1111
        app.search_manager.set_search_info(message.engine, message.text)
 
1112
        self._saw_pre_startup_message('search-info')
 
1113
 
 
1114
    def tablist_for_message(self, message):
 
1115
        if message.type == 'feed':
 
1116
            return app.tab_list_manager.feed_list
 
1117
        elif message.type == 'audio-feed':
 
1118
            return app.tab_list_manager.audio_feed_list
 
1119
        elif message.type == 'playlist':
 
1120
            return app.tab_list_manager.playlist_list
 
1121
        elif message.type == 'guide':
 
1122
            return app.tab_list_manager.site_list
 
1123
        else:
 
1124
            raise ValueError("Unknown Type: %s" % message.type)
 
1125
 
 
1126
    def handle_tab_list(self, message):
 
1127
        tablist = self.tablist_for_message(message)
 
1128
        tablist.reset_list(message)
 
1129
        if 'feed' in message.type:
 
1130
            pre_startup_message = message.type + '-tab-list'
 
1131
            self._saw_pre_startup_message(pre_startup_message)
 
1132
 
 
1133
    def handle_guide_list(self, message):
 
1134
        app.widgetapp.default_guide_info = message.default_guide
 
1135
        self.initial_guides = message.added_guides
 
1136
        self._saw_pre_startup_message('guide-list')
 
1137
 
 
1138
    def update_default_guide(self, guide_info):
 
1139
        app.widgetapp.default_guide_info = guide_info
 
1140
        guide_tab = app.tab_list_manager.static_tab_list.get_tab('guide')
 
1141
        guide_tab.update(guide_info)
 
1142
 
 
1143
    def handle_watched_folder_list(self, message):
 
1144
        app.watched_folder_manager.handle_watched_folder_list(
 
1145
                message.watched_folders)
 
1146
 
 
1147
    def handle_watched_folders_changed(self, message):
 
1148
        app.watched_folder_manager.handle_watched_folders_changed(
 
1149
                message.added, message.changed, message.removed)
 
1150
 
 
1151
    def handle_tabs_changed(self, message):
 
1152
        if message.type == 'guide':
 
1153
            for info in list(message.changed):
 
1154
                if info.default:
 
1155
                    self.update_default_guide(info)
 
1156
                    message.changed.remove(info)
 
1157
                    break
 
1158
        tablist = self.tablist_for_message(message)
 
1159
        if message.removed:
 
1160
            tablist.remove(message.removed)
 
1161
        for info in message.changed:
 
1162
            tablist.update(info)
 
1163
        for info in message.added:
 
1164
            # some things don't have parents (e.g. sites)
 
1165
            if hasattr(info, "parent_id"):
 
1166
                tablist.add(info, info.parent_id)
 
1167
            else:
 
1168
                tablist.add(info)
 
1169
        tablist.model_changed()
 
1170
        app.info_updater.handle_tabs_changed(message)
 
1171
 
 
1172
    def handle_item_list(self, message):
 
1173
        app.info_updater.handle_item_list(message)
 
1174
        app.menu_manager.update_menus()
 
1175
 
 
1176
    def handle_items_changed(self, message):
 
1177
        app.info_updater.handle_items_changed(message)
 
1178
        app.menu_manager.update_menus()
 
1179
 
 
1180
    def handle_download_count_changed(self, message):
 
1181
        app.widgetapp.download_count = message.count
 
1182
        library_tab_list = app.tab_list_manager.library_tab_list
 
1183
        library_tab_list.update_download_count(message.count, 
 
1184
                                               message.non_downloading_count)
 
1185
 
 
1186
    def handle_paused_count_changed(self, message):
 
1187
        app.widgetapp.paused_count = message.count
 
1188
 
 
1189
    def handle_new_video_count_changed(self, message):
 
1190
        library_tab_list = app.tab_list_manager.library_tab_list
 
1191
        library_tab_list.update_new_video_count(message.count)
 
1192
 
 
1193
    def handle_new_audio_count_changed(self, message):
 
1194
        library_tab_list = app.tab_list_manager.library_tab_list
 
1195
        library_tab_list.update_new_audio_count(message.count)
 
1196
 
 
1197
    def handle_unwatched_count_changed(self, message):
 
1198
        app.widgetapp.unwatched_count = message.count
 
1199
        app.widgetapp.handle_unwatched_count_changed()
 
1200
 
 
1201
    def handle_video_conversions_count_changed(self, message):
 
1202
        library_tab_list = app.tab_list_manager.library_tab_list
 
1203
        library_tab_list.update_conversions_count(message.running_count,
 
1204
                message.other_count)
 
1205
 
 
1206
    def handle_video_conversion_tasks_list(self, message):
 
1207
        current_display = app.display_manager.get_current_display()
 
1208
        if isinstance(current_display, displays.VideoConversionsDisplay):
 
1209
            current_display.controller.handle_task_list(message.running_tasks,
 
1210
                    message.pending_tasks, message.finished_tasks)
 
1211
 
 
1212
    def handle_video_conversion_task_created(self, message):
 
1213
        current_display = app.display_manager.get_current_display()
 
1214
        if isinstance(current_display, displays.VideoConversionsDisplay):
 
1215
            current_display.controller.handle_task_added(message.task)
 
1216
 
 
1217
    def handle_video_conversion_task_removed(self, message):
 
1218
        current_display = app.display_manager.get_current_display()
 
1219
        if isinstance(current_display, displays.VideoConversionsDisplay):
 
1220
            current_display.controller.handle_task_removed(message.task)
 
1221
 
 
1222
    def handle_all_video_conversion_task_removed(self, message):
 
1223
        current_display = app.display_manager.get_current_display()
 
1224
        if isinstance(current_display, displays.VideoConversionsDisplay):
 
1225
            current_display.controller.handle_all_tasks_removed()
 
1226
 
 
1227
    def handle_video_conversion_task_changed(self, message):
 
1228
        current_display = app.display_manager.get_current_display()
 
1229
        if isinstance(current_display, displays.VideoConversionsDisplay):
 
1230
            current_display.controller.handle_task_changed(message.task)
 
1231
    
 
1232
    def handle_play_movie(self, message):
 
1233
        app.playback_manager.start_with_items(message.item_infos)
 
1234
 
 
1235
    def handle_open_in_external_browser(self, message):
 
1236
        app.widgetapp.open_url(message.url)
 
1237
 
 
1238
    def handle_message_to_user(self, message):
 
1239
        title = message.title or _("Message")
 
1240
        desc = message.desc
 
1241
        print "handle_message_to_user"
 
1242
        dialogs.show_message(title, desc)
 
1243
 
 
1244
    def handle_notify_user(self, message):
 
1245
        # if the user has selected that they aren't interested in this
 
1246
        # notification type, return here...
 
1247
 
 
1248
        # otherwise, we default to sending the notification
 
1249
        app.widgetapp.send_notification(message.title, message.body)
 
1250
 
 
1251
    def handle_search_complete(self, message):
 
1252
        if app.widgetapp.ui_initialized:
 
1253
            app.search_manager.handle_search_complete(message)
 
1254
 
 
1255
    def handle_current_frontend_state(self, message):
 
1256
        app.frontend_states_memory = FrontendStatesStore(message)
 
1257
        self._saw_pre_startup_message('frontend-state')
 
1258
 
 
1259
    def handle_progress_dialog_start(self, message):
 
1260
        self.progress_dialog = dialogs.ProgressDialog(message.title)
 
1261
        self.progress_dialog.run()
 
1262
        # run() will return when we destroy the dialog because of a future
 
1263
        # message.
 
1264
 
 
1265
    def handle_progress_dialog(self, message):
 
1266
        self.progress_dialog.update(message.description, message.progress)
 
1267
 
 
1268
    def handle_progress_dialog_finished(self, message):
 
1269
        self.progress_dialog.destroy()
 
1270
        self.progress_dialog = None
 
1271
 
 
1272
    def handle_feedless_download_started(self, message):
 
1273
        library_tab_list = app.tab_list_manager.library_tab_list
 
1274
        library_tab_list.blink_tab("downloading")
 
1275
 
 
1276
class FrontendStatesStore(object):
 
1277
    """Stores which views were left in list mode by the user.
 
1278
    """
 
1279
 
 
1280
    # Maybe this should get its own module, but it seems small enough to
 
1281
    # me -- BDK
 
1282
 
 
1283
    def __init__(self, message):
 
1284
        self.current_displays = set(message.list_view_displays)
 
1285
        self.sort_states = message.sort_states
 
1286
        self.active_filters = message.active_filters
 
1287
 
 
1288
    def _key(self, type, id):
 
1289
        return '%s:%s' % (type, id)
 
1290
 
 
1291
    def query_list_view(self, type, id):
 
1292
        return self._key(type, id) in self.current_displays
 
1293
 
 
1294
    def query_sort_state(self, type, id):
 
1295
        key = self._key(type, id)
 
1296
        if key in self.sort_states:
 
1297
            state = self.sort_states[key]
 
1298
            if state.startswith('-'):
 
1299
                sort_key = state[1:]
 
1300
                ascending = False
 
1301
            else:
 
1302
                sort_key = state
 
1303
                ascending = True
 
1304
            return itemlist.SORT_KEY_MAP[sort_key](ascending)
 
1305
        return None
 
1306
 
 
1307
    def query_filters(self, type, id):
 
1308
        return self.active_filters.get(self._key(type, id), [])
 
1309
 
 
1310
    def set_filters(self, type, id, filters):
 
1311
        self.active_filters[self._key(type, id)] = filters
 
1312
        self.save_state()
 
1313
 
 
1314
    def set_sort_state(self, type, id, sorter):
 
1315
        # we have a ItemSort object and we need to create a string that will
 
1316
        # represent it.  Use the sort key, with '-' prepended if the sort is
 
1317
        # descending (for example: "date", "-name", "-size", ...)
 
1318
        state = sorter.KEY
 
1319
        if not sorter.is_ascending():
 
1320
            state = '-' + state
 
1321
        self.sort_states[self._key(type, id)] = state
 
1322
        self.save_state()
 
1323
 
 
1324
    def set_list_view(self, type, id):
 
1325
        self.current_displays.add(self._key(type, id))
 
1326
        self.save_state()
 
1327
 
 
1328
    def set_std_view(self, type, id):
 
1329
        self.current_displays.discard(self._key(type, id))
 
1330
        self.save_state()
 
1331
 
 
1332
    def save_state(self):
 
1333
        m = messages.SaveFrontendState(list(self.current_displays),
 
1334
                self.sort_states, self.active_filters)
 
1335
        m.send_to_backend()