root/branches/0.3/src/main_window.py

Revision 743, 28.2 kB (checked in by nicfit, 1 year ago)

Open tabs explicitly when possible (python 2.5)

Line 
1 # -*- coding: utf-8 -*-
2 ################################################################################
3 #  Copyright (C) 2006  Travis Shirk <travis@pobox.com>
4 #
5 #  This program is free software; you can redistribute it and/or modify
6 #  it under the terms of the GNU General Public License as published by
7 #  the Free Software Foundation; either version 2 of the License, or
8 #  (at your option) any later version.
9 #
10 #  This program is distributed in the hope that it will be useful,
11 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #  GNU General Public License for more details.
14 #
15 #  You should have received a copy of the GNU General Public License
16 #  along with this program; if not, write to the Free Software
17 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18 #
19 #  $Id$
20 ################################################################################
21 import os, sys
22 import gtk, gtk.glade
23
24 import mesk
25 import mesk.plugin
26 import mesk.gtk_utils
27 import mesk.window
28 from mesk.i18n import _
29
30 import config
31 from control import EmptyControl
32 from audio_control import AudioControl
33 from about_dialog import AboutDialog
34 from playlist_control import PlaylistControl, PlaylistPropertiesDialog
35 from album_cover_control import AlbumCoverControl
36 from preference_window import PreferenceWindow
37 from dialogs import ErrorDialog, ConfirmationWithDisableOptionDialog
38 from status_bar import StatusBar
39
40 import devices
41 if mesk.info.DISABLE_CDROM_SUPPORT or mesk.info.DISABLE_DBUS_SUPPORT:
42     class CDROMControl:
43         pass  # A stub for type checks
44 else:
45     from cdrom_control import CDROMControl
46
47 class MainWindow(mesk.window.Window):
48     # DND_TARGETS is the targets needed by drag_source_set and drag_dest_set
49     DND_TARGETS = [('MESK_TAB', 0, 81)]
50
51     def __init__(self, profile):
52         self.profile = profile
53         self._is_compact = False
54         self._controls = []
55
56         mesk.window.Window.__init__(self, 'main_window', 'main_window.glade')
57         self.window.connect('key-press-event',
58                             self._on_window_key_press_event)
59         self.window.connect('focus-in-event', self._on_window_focus_in_event)
60
61         self._notebook = self.xml.get_widget('notebook')
62         # Remove all glade pages from notebook
63         while self._notebook.get_n_pages():
64             self._notebook.remove_page(0)
65         self._notebook.set_show_tabs(True)
66
67         self._status_bar = StatusBar(self.xml)
68         self._marquee_label = self.xml.get_widget('marquee_label')
69
70         self._audio_control = AudioControl(self.xml, self.window)
71         self._audio_control.connect('source-changed',
72                                     self._on_audio_source_changed)
73         self._audio_control.connect('tag-update',
74                                     self._on_audio_source_tag_update)
75
76         if not mesk.info.DISABLE_DBUS_SUPPORT:
77             # Initialize Dbus service
78             import dbus, dbus_service
79             service_name = dbus_service.get_service_name(self.profile)
80             bus_name = dbus.service.BusName(service_name, bus=dbus.SessionBus())
81             self.mesk_dbus = dbus_service.MeskDbusService(bus_name,
82                                                           self.profile,
83                                                           self)
84             mesk.log.verbose("DBus service activated")
85         else:
86             self.mesk_dbus = None
87             mesk.log.info('DBus disabled')
88
89         self._album_cover_control = AlbumCoverControl(self.xml,
90                                                       self._audio_control)
91
92         # Status tray icon
93         try:
94             # tray support requires gtk >= 2.10
95             import status_icon
96             self.status_icon = status_icon.StatusIcon(self, self._audio_control)
97         except AttributeError:
98             mesk.log.info('Status tray support disabled, upgrade to Gtk+ 2.10 '
99                           'for support.')
100             self.status_icon = None
101
102         # The active control is the one that has ownership of the AudioControl
103         self._active_control = None
104         self._empty_control = EmptyControl()
105
106         # Load active playlists
107         playlists = mesk.config.getlist(mesk.CONFIG_MAIN, 'playlists')
108         active = mesk.config.get(mesk.CONFIG_MAIN, 'active_playlist')
109         for pl_name in playlists:
110             self.add_control(PlaylistControl, pl_name,
111                              set_active=(active==pl_name))
112
113         if self._notebook.get_n_pages() == 0:
114             # Add a placeholder when there is nothing else to display
115             self.add_notebook_control(self._empty_control)
116             self._empty_control.widget.show()
117             self._notebook.set_show_tabs(False)
118         elif not self._active_control:
119             self.set_active_control(self._controls[0])
120
121         # Show active control
122         if self._active_control:
123             page_num = self._notebook.page_num(self._active_control.widget)
124             self._notebook.set_current_page(page_num)
125
126         self._pref_window = None
127         self._logs_window = None
128
129         # Set up DnD tab reordering
130         self._notebook.connect('drag_data_received',
131                                self._on_tab_drag_data_received)
132         self._notebook.drag_dest_set(gtk.DEST_DEFAULT_ALL, self.DND_TARGETS,
133                                      gtk.gdk.ACTION_MOVE)
134
135         # Register for device changes
136         devices.get_mgr().connect('media-changed', self._media_changed)
137
138         # Initialize plugins
139         mesk.plugin.set_manager(mesk.plugin.PluginMgr())
140
141     def add_control(self, clazz, name, set_active=False):
142         status_msg = _('Loading playlist \'%s\'...') % name
143         self._status_bar.push_status_msg(status_msg)
144         mesk.log.verbose(status_msg)
145
146         ctrl = None
147         try:
148             ctrl = clazz(name, self._status_bar)
149         except mesk.MeskException, ex:
150             if ex.primary_msg:
151                 d = ErrorDialog(self.window)
152                 d.set_markup('<b>%s</b>' % ex.primary_msg)
153                 d.format_secondary_text(ex.secondary_msg)
154                 d.run()
155                 d.destroy()
156         except Exception, ex:
157             import traceback
158             d = ErrorDialog(self.window)
159             d.set_markup("<b>%s</b>" % (_("Error loading '%s'") % name))
160             d.format_secondary_text('%s\n%s' % (str(ex),
161                                                 traceback.format_exc()))
162             d.run()
163             d.destroy()
164         else:
165             if isinstance(ctrl, PlaylistControl):
166                 ctrl.connect('playlist_changed', self._on_playlist_ctrl_changed)
167             ctrl.connect('control_request_active',
168                          self._on_control_request_active)
169             ctrl.connect('control_request_close',
170                          self._on_control_request_close)
171
172             self.add_notebook_control(ctrl)
173             if set_active or self._notebook.get_n_pages() == 1:
174                 self.set_active_control(ctrl)
175
176         self._status_bar.pop_status_msg(status_msg)
177         status_msg = _('\'%s\' loaded') % name
178         self._status_bar.push_status_msg(status_msg)
179         self._status_bar.pop_status_msg(status_msg, delay=1500)
180
181         return ctrl
182
183     def _on_playlist_ctrl_changed(self, ctrl):
184         if ctrl == self._active_control:
185             if not len(ctrl.get_playlist()):
186                 # Playlist is empty, current display
187                 self._update_current_display(None)
188
189     def _on_control_request_close(self, ctrl):
190         self.remove_notebook_control(ctrl)
191
192     def _on_control_request_active(self, ctrl):
193         # A control is requesting the AudioControl.  Grant it.
194         self.set_active_control(ctrl)
195
196     def set_active_control(self, ctrl):
197         if ctrl == self._active_control:
198             return
199
200         # Clear state
201         self._update_current_display(None)
202
203         self._audio_control.stop()
204
205         # If we have an active control, tell it is no longer active
206         if self._active_control:
207             self._active_control.set_active(False, None)
208
209         self._active_control = ctrl
210         if self._active_control:
211             pl = self._active_control.get_playlist()
212             self._audio_control.set_playlist(pl)
213             self._active_control.set_active(True, self._audio_control)
214         else:
215             self._audio_control.set_playlist(None)
216
217     def add_notebook_control(self, ctrl):
218
219         new_index = self._notebook.append_page(ctrl.widget, ctrl.tab_widget)
220         self._notebook.set_current_page(new_index)
221
222         if ctrl != self._empty_control:
223             self._notebook.set_show_tabs(True)
224             # Handle tab close button if it is to be displayed, otherwise hide
225             # it
226             close_button = \
227                 ctrl.tab_widget.get_children()[0].get_children()[2]
228             if mesk.config.getboolean(mesk.CONFIG_UI, 'show_tab_close_button'):
229                 close_button.connect('clicked',
230                                      self._on_tab_close_button_clicked, ctrl)
231             else:
232                 close_button.hide()
233
234             # Remove placeholder control
235             page_num = self._notebook.page_num(self._empty_control.widget)
236             if page_num >= 0:
237                 self._notebook.remove_page(page_num)
238         else:
239             self._notebook.set_show_tabs(False)
240
241         # This list order does not necessarily correspond to the tab order
242         self._controls.append(ctrl)
243         self._uptate_open_menus()
244
245         # Set tab label as drag source and connect the drag_data_get signal
246         ctrl.tab_widget.dnd_handler = \
247             ctrl.tab_widget.connect('drag_data_get', self._on_tab_drag_data_get)
248         ctrl.tab_widget.drag_source_set(gtk.gdk.BUTTON1_MASK, self.DND_TARGETS,
249                                         gtk.gdk.ACTION_MOVE)
250
251     def remove_notebook_control(self, ctrl):
252         page_num = self._notebook.page_num(ctrl.widget)
253         new_active = False
254
255         # Transition active tab if necessary
256         if ctrl == self._active_control:
257             new_active = True
258             ctrl.set_active(False)
259
260         # Remove
261         self._controls.remove(ctrl)
262         self._notebook.remove_page(page_num)
263
264         # Set the new active control
265         if new_active and self._notebook.get_n_pages():
266             page = self._notebook.get_current_page()
267             page = self._notebook.get_nth_page(page)
268             ctrl = self.get_control_by_widget(page)
269             self.set_active_control(ctrl)
270
271         self._uptate_open_menus()
272
273         # Add special widget for whenever there are none
274         if self._notebook.get_n_pages() == 0:
275             self.add_notebook_control(self._empty_control)
276             self.set_active_control(None)
277         elif self._notebook.get_n_pages() < 2:
278             # Disable DnD when num tabs < 2
279             self._notebook.drag_dest_unset()
280
281     def get_control_by_widget(self, widget):
282         for ctrl in self._controls:
283             if ctrl.widget == widget:
284                 return ctrl
285         return None
286
287     def get_controls_by_type(self, t, accept_base_type=False):
288         controls = []
289         for ctrl in self._controls:
290             if (accept_base_type and isinstance(ctrl, t)) or type(ctrl) is t:
291                 controls.append(ctrl)
292         return controls
293
294     def get_focused_control(self):
295         curr = self._notebook.get_current_page()
296         page_widget = self._notebook.get_nth_page(curr)
297         return self.get_control_by_widget(page_widget)
298
299     def _on_window_focus_in_event(self, window, event):
300         control = self.get_focused_control()
301         control and control.set_focused()
302
303     def _on_notebook_switch_page(self, notebook, page, page_num):
304         page = self._notebook.get_nth_page(page_num)
305         ctrl = self.get_control_by_widget(page)
306         if ctrl:
307             ctrl.set_focused()
308
309     def _on_tab_close_button_clicked(self, widget, ctrl):
310         self.remove_notebook_control(ctrl)
311
312     def show(self):
313         if not hasattr(self, "_first_show"):
314             self._first_show = False
315             compact = mesk.config.getboolean(mesk.CONFIG_UI, 'compact_state')
316             self.xml.get_widget('compact_menuitem').set_active(compact)
317             self.set_compact_mode(compact)
318         self._restore_window_attrs()
319         mesk.window.Window.show(self)
320
321     def quit(self, prompt=mesk.config.getboolean(mesk.CONFIG_MAIN,
322                                                  'confirm_quit')):
323         if prompt:
324             # Confirm the quit
325             d = ConfirmationWithDisableOptionDialog(self.window)
326             d.set_markup('<b>%s</b>' % _('Are you sure you want to quit?'))
327             (confirmed, disable_prompt) = d.confirm()
328             if not confirmed:
329                 return False
330             mesk.config.set(mesk.CONFIG_MAIN, 'confirm_quit',
331                             str(not disable_prompt))
332
333         self.window.hide()
334
335         # Cleanup hidden windows
336         if self._pref_window:
337             self._pref_window.window.destroy()
338
339         # Shutdown plugins
340         mesk.plugin.shutdown()
341
342         # Shutdown device manager and monitoring
343         devices.get_mgr().shutdown()
344
345         # Shutdown all controls
346         playlists = []
347         for i in range(self._notebook.get_n_pages()):
348             # Notebook order traversal for saving state
349             ctrl = self.get_control_by_widget(self._notebook.get_nth_page(i))
350             if ctrl.is_playlist_saved():
351                 playlists.append(ctrl.name)
352             ctrl.shutdown()
353
354         # Remember open playlists
355         mesk.config.set(mesk.CONFIG_MAIN, 'playlists', playlists)
356         active_playlist = ''
357         if self._active_control and self._active_control.get_playlist():
358             active_playlist = self._active_control.name
359         mesk.config.set(mesk.CONFIG_MAIN, 'active_playlist', active_playlist)
360
361         # Exit the gtk event loop
362         try:
363             gtk.main_quit()
364         except RuntimeError:
365             # This has already happened
366             pass
367
368         return True
369
370     def _restore_window_attrs(self):
371         if not self._is_compact:
372             x = mesk.config.getint(mesk.CONFIG_UI, 'main_window_pos_x')
373             y = mesk.config.getint(mesk.CONFIG_UI, 'main_window_pos_y')
374             self.window.move(x, y)
375             mesk.log.debug('_restore_window_attrs pos (%d,%d)' % (x, y))
376
377             width = mesk.config.getint(mesk.CONFIG_UI, 'main_window_width')
378             height = mesk.config.getint(mesk.CONFIG_UI, 'main_window_height')
379             self.window.resize(width, height)
380             mesk.log.debug('_restore_window_attrs size %dx%d' % (width, height))
381         else:
382             # Restore compact window position
383             x = mesk.config.getint(mesk.CONFIG_UI, 'compact_main_window_pos_x')
384             y = mesk.config.getint(mesk.CONFIG_UI, 'compact_main_window_pos_y')
385             self.window.move(x, y)
386             mesk.log.debug('_restore_window_attrs (compact) pos (%d,%d)' %
387                            (x, y))
388
389     def _on_window_configure_event(self, win, event):
390         mesk.window.Window._on_window_configure_event(self, win, event)
391         (width, height) = self._window_size
392         (x, y) = self._window_pos
393
394         if not self._is_compact:
395             mesk.config.set(mesk.CONFIG_UI, 'main_window_width', str(width))
396             mesk.config.set(mesk.CONFIG_UI, 'main_window_height', str(height))
397             mesk.config.set(mesk.CONFIG_UI, 'main_window_pos_x', str(x))
398             mesk.config.set(mesk.CONFIG_UI, 'main_window_pos_y', str(y))
399             mesk.log.debug('window attrs: pos (MAIN) (%d,%d)' % (x, y))
400             mesk.log.debug('window attrs: size (MAIN) %dx%d' % (width,
401                                                                      height))
402         else:
403             mesk.config.set(mesk.CONFIG_UI, 'compact_main_window_pos_x', str(x))
404             mesk.config.set(mesk.CONFIG_UI, 'compact_main_window_pos_y', str(y))
405             mesk.log.debug('window attrs: pos (COMPACT) (%d,%d)' % (x, y))
406
407     def _on_window_delete_event(self, widget, event):
408         '''Overridden from mesk.window.Window'''
409         if mesk.config.getboolean(mesk.CONFIG_UI, 'window_hide_on_close'):
410             self.window.hide()
411             return True
412         else:
413             if not self.quit():
414                 return True
415             else:
416                 # Return yes, you may delete me
417                 return False
418
419     def set_compact_mode(self, state):
420         if self._is_compact == state:
421             return
422
423         mesk.config.set(mesk.CONFIG_UI, 'compact_state', str(state))
424         if state:
425             self._notebook.hide()
426             self._status_bar.hide()
427
428             # Size window for compact view
429             (curr_width, curr_height) = self.window.get_size()
430             (pref_width, pref_height) = self.window.size_request()
431             self.window.resize(curr_width, pref_height)
432             self.window.set_resizable(False)
433         else:
434             self.window.set_resizable(True)
435             self._notebook.show()
436             self._status_bar.show()
437
438         self._is_compact = state
439         self._restore_window_attrs()
440
441     def display_current_playlist(self):
442         if self._active_control and isinstance(self._active_control,
443                                                PlaylistControl):
444             self._active_control.scroll_to_current()
445             if self._active_control != self.get_focused_control():
446                 num = self._notebook.page_num(self._active_control.widget)
447                 self._notebook.set_current_page(num)
448
449     ### Menu callbacks ###
450     def _on_quit_menuitem_activate(self, widget):
451         self.quit()
452
453     def _on_preferences_menuitem_activate(self, widget):
454         if self._pref_window is None:
455             self._pref_window = PreferenceWindow()
456             self._pref_window.window.set_transient_for(self.window)
457         self._pref_window.present()
458
459     def _on_view_menu_activate(self, widget):
460         plugins_menu = self.xml.get_widget('plugins_view_menu')
461         from mesk.plugin.interfaces import ViewMenuProvider
462         menuitems = mesk.plugin.get_menuitems(ViewMenuProvider)
463         if not menuitems:
464             plugins_menu.hide()
465         else:
466             plugins_menu.show()
467             submenu = gtk.Menu()
468             plugins_menu.set_submenu(submenu)
469             for item in menuitems:
470                 item.show()
471                 submenu.append(item)
472             submenu.show()
473
474     def _on_compact_menuitem_activate(self, widget):
475         self.set_compact_mode(widget.get_active())
476
477     def _on_jump_to_current_menuitem_activate(self, widget):
478         self.display_current_playlist()
479
480     def _on_logs_menuitem_activate(self, widget):
481         if self._logs_window is None:
482             import log_window
483             self._logs_window = log_window.LogWindow()
484             # Associate log handler with window textview
485             for h in mesk.log.getLogger().handlers:
486                 if isinstance(h, mesk.log.TextBufferLogHandler):
487                     buff = self._logs_window.log_textview.get_buffer()
488                     h.set_text_buffer(buff)
489                     break
490         self._logs_window.show()
491
492     def _on_about_menuitem_activate(self, widget):
493         self.about_dialog = AboutDialog()
494         self.about_dialog.dialog.set_transient_for(self.window)
495         self.about_dialog.dialog.run()
496         self.about_dialog.dialog.destroy()
497
498     def _on_online_help_menuitem_activate(self, widget):
499         mesk.utils.load_web_page('http://mesk.nicfit.net/')
500
501     def _on_window_key_press_event(self, window, event):
502         mesk.log.debug('Window key-press: %s' % str(event))
503
504         # Control bindings
505         if event.state & gtk.gdk.CONTROL_MASK:
506             # CTRL+w: Close tab
507             if (event.keyval == gtk.keysyms.w):
508                 self.remove_notebook_control(self.get_focused_control())
509                 return True
510         # Alt bindings