~facundo/encuentro/trunk

« back to all changes in this revision

Viewing changes to encuentro/ui/main.py

  • Committer: Facundo Batista
  • Date: 2013-04-16 01:58:03 UTC
  • mfrom: (151.2.7 trunk)
  • Revision ID: facundo@taniquetil.com.ar-20130416015803-btbp3sd6dn5sjyds
Merged trunk back in.

Show diffs side-by-side

added added

removed removed

Lines of Context:
20
20
 
21
21
import logging
22
22
import os
23
 
import pickle
24
23
 
25
24
try:
26
25
    import pynotify
32
31
    QCheckBox,
33
32
    QLabel,
34
33
    QLineEdit,
35
 
    QMainWindow,
36
34
    QMessageBox,
37
 
    QPushButton,
 
35
    QSizePolicy,
38
36
    QStyle,
39
 
    qApp,
 
37
    QWidget,
40
38
)
41
39
from twisted.internet import defer
42
40
 
43
41
from encuentro import platform, data, update
 
42
from encuentro.config import config, signal
44
43
from encuentro.data import Status
45
44
from encuentro.network import (
46
45
    BadCredentialsError,
47
46
    CancelledError,
48
47
    all_downloaders,
49
48
)
50
 
from encuentro.ui import central_panel, wizard, preferences
 
49
from encuentro.ui import (
 
50
    central_panel,
 
51
    preferences,
 
52
    remembering,
 
53
    systray,
 
54
    wizard,
 
55
)
51
56
 
52
57
logger = logging.getLogger('encuentro.main')
53
58
 
62
67
    u"alguna configuración en el programa."
63
68
)
64
69
 
65
 
# FIXME: need an About dialog, connected to the proper signals below
66
 
#   title: Encuentro <version>   <-- need to receive the version when exec'ed
67
 
#   comments: Simple programa que permite buscar, descargar y ver
68
 
#             contenido del canal Encuentro y otros.
69
 
#   smaller: Copyright 2010-2013 Facundo  Batista
70
 
#   url: http://encuentro.taniquetil.com.ar
71
 
#   somewhere (maybe with a button), the license: the content of LICENSE.txt
72
 
 
73
 
# FIXME: need to put an icon that looks nice in alt-tab, taskbar, unity, etc
74
 
 
75
 
# FIXME: need to make Encuentro "iconizable"
76
 
 
77
 
# FIXME: set up a status icon, when the icon is clicked the main window should
78
 
# appear or disappear, keeping the position and size of the position after
79
 
# the sequence
80
 
 
81
 
 
82
 
class MainUI(QMainWindow):
 
70
ABOUT_TEXT = u"""
 
71
<center>
 
72
Simple programa que permite buscar, descargar y ver<br/>
 
73
contenido del canal Encuentro y otros.<br/>
 
74
<br/>
 
75
Versión %s<br/>
 
76
<br/>
 
77
<small>Copyright 2010-2013 Facundo Batista</small><br/>
 
78
<br/>
 
79
<a href="http://encuentro.taniquetil.com.ar">
 
80
    http://encuentro.taniquetil.com.ar
 
81
</a>
 
82
</center>
 
83
"""
 
84
 
 
85
 
 
86
class MainUI(remembering.RememberingMainWindow):
83
87
    """Main UI."""
84
88
 
85
 
    _config_file = os.path.join(platform.config_dir, 'encuentro.conf')
86
 
    print "Using configuration file:", repr(_config_file)
87
89
    _programs_file = os.path.join(platform.data_dir, 'encuentro.data')
88
90
 
89
91
    def __init__(self, version, app_quit):
90
92
        super(MainUI, self).__init__()
91
93
        self.app_quit = app_quit
92
94
        self.finished = False
93
 
        # FIXME: size and positions should remain the same between starts
94
 
        self.resize(800, 600)
95
 
        self.move(300, 300)
 
95
        self.version = version
96
96
        self.setWindowTitle('Encuentro')
97
97
 
98
98
        self.programs_data = data.ProgramsData(self, self._programs_file)
99
 
        self.config = self._load_config()
 
99
        self._touch_config()
100
100
 
101
101
        self.downloaders = {}
102
102
        for downtype, dloader_class in all_downloaders.iteritems():
103
 
            self.downloaders[downtype] = dloader_class(self.config)
 
103
            self.downloaders[downtype] = dloader_class()
104
104
 
105
105
        # finish all gui stuff
106
106
        self.big_panel = central_panel.BigPanel(self)
110
110
 
111
111
        # the setting of menubar should be almost in the end, because it may
112
112
        # trigger the wizard, which needs big_panel and etc.
 
113
        self.action_play = self.action_download = None
 
114
        self.filter_line = self.filter_cbox = self.needsomething_alert = None
113
115
        self._menubar()
 
116
 
 
117
        systray.show(self)
 
118
        if config['autorefresh']:
 
119
            ue = update.UpdateEpisodes(self)
 
120
            ue.background()
114
121
        self.show()
115
122
        logger.debug("Main UI started ok")
116
123
 
117
 
    def _save_config(self):
118
 
        """Save the config to disk."""
119
 
        with open(self._config_file, 'wb') as fh:
120
 
            pickle.dump(self.config, fh)
121
 
 
122
 
    def _load_config(self):
123
 
        """Load the config from disk."""
124
 
        # get config from file, or defaults
125
 
        if os.path.exists(self._config_file):
126
 
            with open(self._config_file, 'rb') as fh:
127
 
                config = pickle.load(fh)
128
 
                if self.programs_data.reset_config_from_migration:
129
 
                    config['user'] = ''
130
 
                    config['password'] = ''
131
 
                    config.pop('cols_width', None)
132
 
                    config.pop('cols_order', None)
133
 
                    config.pop('selected_row', None)
134
 
        else:
135
 
            config = {}
136
 
 
 
124
    def _touch_config(self):
 
125
        """Do some config processing."""
137
126
        # log the config, but without user and pass
138
127
        safecfg = config.copy()
139
128
        if 'user' in safecfg:
145
134
        # we have a default for download dir
146
135
        if not config.get('downloaddir'):
147
136
            config['downloaddir'] = platform.get_download_dir()
148
 
        return config
 
137
 
 
138
        # maybe clean some config
 
139
        if self.programs_data.reset_config_from_migration:
 
140
            config['user'] = ''
 
141
            config['password'] = ''
 
142
            config.pop('cols_width', None)
 
143
            config.pop('cols_order', None)
 
144
            config.pop('selected_row', None)
149
145
 
150
146
    def have_config(self):
151
147
        """Return if some config is needed."""
152
 
        return self.config.get('user') and self.config.get('password')
 
148
        return config.get('user') and config.get('password')
153
149
 
154
150
    def have_metadata(self):
155
151
        """Return if metadata is needed."""
169
165
        action_reload.triggered.connect(self.refresh_episodes)
170
166
        menu_appl.addAction(action_reload)
171
167
 
172
 
        # FIXME: set an icon for preferences
173
 
        action_preferences = QAction(u'&Preferencias', self)
 
168
        icon = self.style().standardIcon(QStyle.SP_FileDialogDetailedView)
 
169
        action_preferences = QAction(icon, u'&Preferencias', self)
174
170
        action_preferences.triggered.connect(self.open_preferences)
175
171
        action_preferences.setToolTip(
176
172
            u'Configurar distintos parámetros del programa')
178
174
 
179
175
        menu_appl.addSeparator()
180
176
 
181
 
        # FIXME: set an icon for about
182
 
        _act = QAction('&Acerca de', self)
183
 
        # FIXME: connect signal
 
177
        icon = self.style().standardIcon(QStyle.SP_MessageBoxInformation)
 
178
        _act = QAction(icon, '&Acerca de', self)
 
179
        _act.triggered.connect(self.open_about_dialog)
184
180
        _act.setToolTip(u'Muestra información de la aplicación')
185
181
        menu_appl.addAction(_act)
186
182
 
188
184
        _act = QAction(icon, '&Salir', self)
189
185
        _act.setShortcut('Ctrl+Q')
190
186
        _act.setToolTip(u'Sale de la aplicación')
191
 
        _act.triggered.connect(qApp.quit)
 
187
        _act.triggered.connect(self.on_close)
192
188
        menu_appl.addAction(_act)
193
189
 
194
190
        # program menu
206
202
        self.action_play = QAction(icon, '&Reproducir', self)
207
203
        self.action_play.setEnabled(False)
208
204
        self.action_play.setToolTip(TTIP_PLAY_D)
209
 
        self.action_play.triggered.connect(self.play_episode)
 
205
        self.action_play.triggered.connect(self.on_play_action)
210
206
        menu_prog.addAction(self.action_play)
211
207
 
212
208
        # toolbar for buttons
217
213
        toolbar.addAction(action_reload)
218
214
        toolbar.addAction(action_preferences)
219
215
 
220
 
        # toolbar for filter
221
 
        # FIXME: see if we can put this toolbar to the extreme
222
 
        # right of the window
223
 
        toolbar = self.addToolBar('')
 
216
        # filter text and button, to the right
 
217
        spacer = QWidget()
 
218
        spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
 
219
        toolbar.addWidget(spacer)
224
220
        toolbar.addWidget(QLabel(u"Filtro: "))
225
221
        self.filter_line = QLineEdit()
 
222
        self.filter_line.setMaximumWidth(150)
226
223
        self.filter_line.textChanged.connect(self.on_filter_changed)
227
224
        toolbar.addWidget(self.filter_line)
228
225
        self.filter_cbox = QCheckBox(u"Sólo descargados")
229
226
        self.filter_cbox.stateChanged.connect(self.on_filter_changed)
230
227
        toolbar.addWidget(self.filter_cbox)
231
228
 
 
229
        # if needed, a warning that stuff needs to be configured
232
230
        icon = self.style().standardIcon(QStyle.SP_MessageBoxWarning)
233
231
        m = u"Necesita configurar algo; haga click aquí para abrir el wizard"
234
232
        self.needsomething_alert = QAction(icon, m, self)
235
233
        self.needsomething_alert.triggered.connect(self._start_wizard)
236
234
        toolbar.addAction(self.needsomething_alert)
237
 
        if not self.config.get('nowizard'):
 
235
        if not config.get('nowizard'):
238
236
            self._start_wizard()
239
237
        self._review_need_something_indicator()
240
238
 
251
249
        cbox = self.filter_cbox.checkState()
252
250
        self.episodes_list.set_filter(text, cbox)
253
251
 
 
252
        # after applying filter, nothing is selected, so check buttons
 
253
        # (easiest way to clean them all)
 
254
        self.check_download_play_buttons()
 
255
 
254
256
    def _review_need_something_indicator(self):
255
257
        """Hide/show/enable/disable different indicators if need sth."""
256
258
        needsomething = bool(not self.have_config() or
263
265
        This shutdown con be called at any time, even on init, so we have
264
266
        extra precautions about which attributes we have.
265
267
        """
266
 
        # self._save_states()  FIXME: if we need to save states, the call is here
267
 
        self._save_config()
 
268
        signal.emit('save_state')
 
269
        config.save()
268
270
        self.finished = True
269
271
 
270
272
        programs_data = getattr(self, 'programs_data', None)
278
280
        # bye bye
279
281
        self.app_quit()
280
282
 
 
283
    def on_close(self, _):
 
284
        """Close signal."""
 
285
        if self._should_close():
 
286
            self.shutdown()
 
287
 
281
288
    def closeEvent(self, event):
282
289
        """All is being closed."""
283
290
        if self._should_close():
313
320
                program.state = Status.none
314
321
        return True
315
322
 
316
 
    def _show_message(self, err_type, text):
 
323
    def show_message(self, err_type, text):
317
324
        """Show different messages to the user."""
318
325
        if self.finished:
319
326
            logger.debug("Ignoring message: %r", text)
337
344
 
338
345
    def refresh_episodes(self, _=None):
339
346
        """Update and refresh episodes."""
340
 
        update.UpdateEpisodes(self)
 
347
        ue = update.UpdateEpisodes(self)
 
348
        ue.interactive()
341
349
 
342
350
    def download_episode(self, _=None):
343
351
        """Download the episode(s)."""
344
352
        items = self.episodes_list.selectedItems()
345
353
        for item in items:
346
354
            episode = self.programs_data[item.episode_id]
347
 
            self._queue_download(episode)
 
355
            self.queue_download(episode)
348
356
 
349
357
    @defer.inlineCallbacks
350
 
    def _queue_download(self, episode):
 
358
    def queue_download(self, episode):
351
359
        """User indicated to download something."""
352
360
        logger.debug("Download requested of %s", episode)
353
361
        if episode.state != Status.none:
357
365
 
358
366
        # queue
359
367
        self.episodes_download.append(episode)
 
368
        self.adjust_episode_info(episode)
360
369
        self.check_download_play_buttons()
361
370
        if self.episodes_download.downloading:
362
371
            return
368
377
                filename, episode = yield self._episode_download(episode)
369
378
            except CancelledError:
370
379
                logger.debug("Got a CancelledError!")
371
 
                self.episodes_download.end(error=u"Cancelao")
 
380
                self.episodes_download.end(error=u"Cancelado")
372
381
            except BadCredentialsError:
373
382
                logger.debug("Bad credentials error!")
374
383
                msg = (u"Error con las credenciales: hay que configurar "
375
384
                       u"usuario y clave correctos")
376
 
                self._show_message('BadCredentialsError', msg)
 
385
                self.show_message('BadCredentialsError', msg)
377
386
                self.episodes_download.end(error=msg)
378
387
            except Exception, e:
379
388
                logger.debug("Unknown download error: %s", e)
380
389
                err_type = e.__class__.__name__
381
 
                self._show_message(err_type, str(e))
 
390
                self.show_message(err_type, str(e))
382
391
                self.episodes_download.end(error=u"Error: " + str(e))
383
392
            else:
384
393
                logger.debug("Episode downloaded: %s", episode)
386
395
                episode.filename = filename
387
396
 
388
397
            # check buttons
 
398
            self.adjust_episode_info(episode)
389
399
            self.check_download_play_buttons()
390
400
 
391
401
        logger.debug("Downloads: finished")
404
414
                                          self.episodes_download.progress)
405
415
        episode_name = u"%s - %s - %s" % (episode.channel, episode.section,
406
416
                                          episode.title)
407
 
        if self.config.get('notification', True) and pynotify is not None:
 
417
        if config.get('notification', True) and pynotify is not None:
408
418
            n = pynotify.Notification(u"Descarga finalizada", episode_name)
409
419
            n.show()
410
420
        defer.returnValue((fname, episode))
413
423
        """Open the preferences dialog."""
414
424
        dlg = preferences.PreferencesDialog()
415
425
        dlg.exec_()
416
 
        # FIXME: el dialogo debería grabar solo cuando lo cierran
417
 
        dlg.save_config()
 
426
        # after dialog closes, config changed, so review indicators
418
427
        self._review_need_something_indicator()
419
428
 
 
429
    def adjust_episode_info(self, episode):
 
430
        """Adjust the episode info."""
 
431
        self.episodes_list.episode_info.update(episode)
 
432
 
420
433
    def check_download_play_buttons(self):
421
434
        """Set both buttons state according to the selected episodes."""
422
435
        items = self.episodes_list.selectedItems()
423
 
        if not items:
424
 
            return
425
436
 
426
437
        # 'play' button should be enabled if only one row is selected and
427
438
        # its state is 'downloaded'
447
458
        self.action_download.setEnabled(download_enabled)
448
459
        self.action_download.setToolTip(ttip)
449
460
 
450
 
    def play_episode(self, _=None):
 
461
    def on_play_action(self, _=None):
451
462
        """Play the selected episode."""
452
463
        items = self.episodes_list.selectedItems()
453
464
        if len(items) != 1:
455
466
                             % len(items))
456
467
        item = items[0]
457
468
        episode = self.programs_data[item.episode_id]
458
 
        downloaddir = self.config.get('downloaddir', '')
 
469
        self.play_episode(episode)
 
470
 
 
471
    def play_episode(self, episode):
 
472
        """Play an episode."""
 
473
        downloaddir = config.get('downloaddir', '')
459
474
        filename = os.path.join(downloaddir, episode.filename)
460
475
 
461
476
        logger.info("Play requested of %s", episode)
468
483
            logger.warning("Aborted playing, file not found: %r", filename)
469
484
            msg = (u"No se encontró el archivo para reproducir: " +
470
485
                   repr(filename))
471
 
            self._show_message('Error al reproducir', msg)
 
486
            self.show_message('Error al reproducir', msg)
472
487
            episode.state = Status.none
473
488
            self.episodes_list.set_color(episode)
474
489
 
475
 
    def cancel_download(self):
 
490
    def cancel_download(self, episode):
476
491
        """Cancel the downloading of an episode."""
477
 
        items = self.episodes_list.selectedItems()
478
 
        if len(items) != 1:
479
 
            raise ValueError("Wrong call to cancel_download, with %d "
480
 
                             "selections" % len(items))
481
 
        item = items[0]
482
 
        episode = self.programs_data[item.episode_id]
483
492
        logger.info("Cancelling download of %s", episode)
484
493
        self.episodes_download.cancel()
485
494
        downloader = self.downloaders[episode.downtype]
486
495
        downloader.cancel()
 
496
        episode.state = Status.none
 
497
 
 
498
    def open_about_dialog(self):
 
499
        """Show the about dialog."""
 
500
        title = "Encuentro v" + self.version
 
501
        text = ABOUT_TEXT % (self.version,)
 
502
        QMessageBox.about(self, title, text)