Handle pynotify exceptions (bug 966)
[gpodder.git] / src / gpodder / gtkui / interface / common.py
bloba138960fdc83ea330a0ccb35fd25cea66e1e9798
1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2010 Thomas Perl and the gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 import gtk
21 import os
22 import shutil
23 import sys
25 import gpodder
27 _ = gpodder.gettext
29 from gpodder import util
31 from gpodder.gtkui.base import GtkBuilderWidget
33 from gpodder.gtkui.widgets import NotificationWindow
35 # For gPodder on Fremantle
36 class Orientation(object):
37 LANDSCAPE, PORTRAIT = range(2)
39 try:
40 import pynotify
41 if not pynotify.init('gPodder'):
42 pynotify = None
43 except ImportError:
44 pynotify = None
46 class BuilderWidget(GtkBuilderWidget):
47 finger_friendly_widgets = []
49 def __init__(self, parent, **kwargs):
50 self._window_iconified = False
51 self._window_visible = False
53 # Enable support for portrait mode changes on Maemo 5
54 if gpodder.ui.fremantle:
55 self._window_orientation = Orientation.LANDSCAPE
57 GtkBuilderWidget.__init__(self, gpodder.ui_folders, gpodder.textdomain, **kwargs)
59 # Enable support for portrait mode changes on Maemo 5
60 if gpodder.ui.fremantle:
61 self.main_window.connect('configure-event', \
62 self._on_configure_event_maemo)
64 # Enable support for fullscreen toggle key on Maemo 4
65 if gpodder.ui.diablo:
66 self._maemo_fullscreen = False
67 self._maemo_fullscreen_chain = None
68 self.main_window.connect('key-press-event', \
69 self._on_key_press_event_maemo)
70 self.main_window.connect('window-state-event', \
71 self._on_window_state_event_maemo)
73 # Enable support for tracking iconified state
74 if hasattr(self, 'on_iconify') and hasattr(self, 'on_uniconify'):
75 self.main_window.connect('window-state-event', \
76 self._on_window_state_event_iconified)
78 # Enable support for tracking visibility state
79 if gpodder.ui.desktop:
80 self.main_window.connect('visibility-notify-event', \
81 self._on_window_state_event_visibility)
83 # Set widgets to finger-friendly mode if on Maemo
84 for widget_name in self.finger_friendly_widgets:
85 if hasattr(self, widget_name):
86 self.set_finger_friendly(getattr(self, widget_name))
88 if parent is not None:
89 self.main_window.set_transient_for(parent)
91 if gpodder.ui.desktop:
92 if hasattr(self, 'center_on_widget'):
93 (x, y) = parent.get_position()
94 a = self.center_on_widget.allocation
95 (x, y) = (x + a.x, y + a.y)
96 (w, h) = (a.width, a.height)
97 (pw, ph) = self.main_window.get_size()
98 self.main_window.move(x + w/2 - pw/2, y + h/2 - ph/2)
99 elif gpodder.ui.diablo:
100 self._maemo_fullscreen_chain = parent
101 if parent.window.get_state() & gtk.gdk.WINDOW_STATE_FULLSCREEN:
102 self.main_window.fullscreen()
103 self._maemo_fullscreen = True
105 def on_window_orientation_changed(self, orientation):
106 """Override this method to relayout a window for portrait mode."""
107 pass
109 def _on_configure_event_maemo(self, window, event):
110 if float(event.width)/float(event.height) < 1:
111 orientation = Orientation.PORTRAIT
112 else:
113 orientation = Orientation.LANDSCAPE
115 if orientation != self._window_orientation:
116 self.on_window_orientation_changed(orientation)
117 self._window_orientation = orientation
119 def _on_key_press_event_maemo(self, widget, event):
120 window_type = widget.get_type_hint()
121 if window_type != gtk.gdk.WINDOW_TYPE_HINT_NORMAL:
122 return False
124 if event.keyval == gtk.keysyms.F6:
125 if self._maemo_fullscreen:
126 if self._maemo_fullscreen_chain is not None:
127 self._maemo_fullscreen_chain.unfullscreen()
128 self.main_window.unfullscreen()
129 if not self.use_fingerscroll:
130 self.main_window.set_border_width(0)
131 else:
132 if self._maemo_fullscreen_chain is not None:
133 self._maemo_fullscreen_chain.fullscreen()
134 self.main_window.fullscreen()
135 if not self.use_fingerscroll:
136 self.main_window.set_border_width(12)
137 return True
138 else:
139 return False
141 def _on_window_state_event_maemo(self, widget, event):
142 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
143 self._maemo_fullscreen = True
144 else:
145 self._maemo_fullscreen = False
147 return False
149 def _on_window_state_event_visibility(self, widget, event):
150 if event.state & gtk.gdk.VISIBILITY_FULLY_OBSCURED:
151 self._window_visible = False
152 else:
153 self._window_visible = True
155 return False
157 def _on_window_state_event_iconified(self, widget, event):
158 if event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED:
159 if not self._window_iconified:
160 self._window_iconified = True
161 self.on_iconify()
162 else:
163 if self._window_iconified:
164 self._window_iconified = False
165 self.on_uniconify()
167 return False
169 def is_iconified(self):
170 return self._window_iconified
172 def notification(self, message, title=None, important=False, widget=None):
173 util.idle_add(self.show_message, message, title, important, widget)
175 def show_message(self, message, title=None, important=False, widget=None):
176 if gpodder.ui.diablo:
177 import hildon
178 if important:
179 try:
180 dlg = hildon.Note('information', (self.main_window, message))
181 except TypeError:
182 if title is None:
183 message = message
184 else:
185 message = '%s\n%s' % (title, message)
186 dlg = hildon.hildon_note_new_information(self.main_window, \
187 message)
188 dlg.run()
189 dlg.destroy()
190 else:
191 if title is None:
192 title = 'gPodder'
193 pango_markup = '<b>%s</b>\n<small>%s</small>' % (title, message)
194 try:
195 hildon.hildon_banner_show_information_with_markup(gtk.Label(''), None, pango_markup)
196 except TypeError:
197 # We're probably running the Diablo UI on Maemo 5 :)
198 hildon.hildon_banner_show_information(self.main_window, \
199 '', message)
200 elif gpodder.ui.fremantle:
201 import hildon
202 if important:
203 if title is None:
204 message = message
205 else:
206 message = '%s\n%s' % (title, message)
207 dlg = hildon.hildon_note_new_information(self.main_window, \
208 message)
209 dlg.run()
210 dlg.destroy()
211 else:
212 hildon.hildon_banner_show_information(self.main_window, \
213 '', message)
214 else:
215 # XXX: Dirty hack to get access to the gPodder-specific config object
216 config = getattr(self, '_config', getattr(self, 'config', None))
218 if important:
219 dlg = gtk.MessageDialog(self.main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK)
220 if title:
221 dlg.set_title(str(title))
222 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
223 else:
224 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
225 dlg.run()
226 dlg.destroy()
227 elif config is not None and config.enable_notifications:
228 if pynotify is not None:
229 if title is None:
230 title = 'gPodder'
231 notification = pynotify.Notification(title, message,\
232 gpodder.icon_file)
233 try:
234 notification.show()
235 except:
236 # See http://gpodder.org/bug/966
237 pass
238 elif widget and isinstance(widget, gtk.Widget):
239 if not widget.window:
240 widget = self.main_window
241 else:
242 widget = self.main_window
243 notification = NotificationWindow(message, title, important=False, widget=widget)
244 notification.show_timeout()
246 def set_finger_friendly(self, widget):
248 If we are on Maemo, we carry out the necessary
249 operations to turn a widget into a finger-friendly
250 one, depending on which type of widget it is (i.e.
251 buttons will have more padding, TreeViews a thick
252 scrollbar, etc..)
254 if widget is None:
255 return None
257 if gpodder.ui.diablo or gpodder.ui.fremantle:
258 if isinstance(widget, gtk.Misc):
259 widget.set_padding(0, 5)
260 elif isinstance(widget, gtk.Button):
261 for child in widget.get_children():
262 if isinstance(child, gtk.Alignment):
263 child.set_padding(5, 5, 5, 5)
264 else:
265 child.set_padding(5, 5)
266 elif isinstance(widget, gtk.TreeView) or isinstance(widget, gtk.TextView):
267 parent = widget.get_parent()
268 elif isinstance(widget, gtk.MenuItem):
269 for child in widget.get_children():
270 self.set_finger_friendly(child)
271 submenu = widget.get_submenu()
272 if submenu is not None:
273 for child in submenu.get_children():
274 self.set_finger_friendly(child)
275 elif isinstance(widget, gtk.Menu):
276 for child in widget.get_children():
277 self.set_finger_friendly(child)
279 return widget
281 def show_confirmation(self, message, title=None):
282 if gpodder.ui.desktop:
283 dlg = gtk.MessageDialog(self.main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO)
284 if title:
285 dlg.set_title(str(title))
286 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
287 else:
288 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
289 response = dlg.run()
290 dlg.destroy()
291 return response == gtk.RESPONSE_YES
292 elif gpodder.ui.diablo:
293 import hildon
294 try:
295 dlg = hildon.Note('confirmation', (self.main_window, message))
296 except TypeError:
297 # Kludgy workaround: We're running the Diablo UI on Maemo 5 :)
298 dlg = hildon.hildon_note_new_confirmation(self.main_window, \
299 message)
300 response = dlg.run()
301 dlg.destroy()
302 return response == gtk.RESPONSE_OK
303 elif gpodder.ui.fremantle:
304 import hildon
305 dlg = hildon.hildon_note_new_confirmation(self.main_window, \
306 message)
307 response = dlg.run()
308 dlg.destroy()
309 return response == gtk.RESPONSE_OK
310 else:
311 raise Exception('Unknown interface type')
313 def show_text_edit_dialog(self, title, prompt, text=None, empty=False, \
314 is_url=False):
315 dialog = gtk.Dialog(title, self.main_window, \
316 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT)
318 if gpodder.ui.fremantle:
319 import hildon
320 button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | \
321 gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
322 button.set_text(_('OK'), '')
323 dialog.add_action_widget(button, gtk.RESPONSE_OK)
324 else:
325 cancel_button = dialog.add_button(gtk.STOCK_CANCEL, \
326 gtk.RESPONSE_CANCEL)
327 ok_button = dialog.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK)
329 dialog.set_has_separator(False)
330 if gpodder.ui.desktop:
331 dialog.set_default_size(300, -1)
332 else:
333 dialog.set_default_size(650, -1)
334 dialog.set_default_response(gtk.RESPONSE_OK)
336 if gpodder.ui.fremantle:
337 import hildon
338 text_entry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
339 if is_url:
340 # Disable word capitalization and word completion when
341 # requesting an URL to be entered (see Maemo bug 5184)
342 text_entry.set_property('hildon-input-mode', \
343 gtk.HILDON_GTK_INPUT_MODE_FULL)
344 elif gpodder.ui.diablo:
345 import hildon
346 text_entry = gtk.Entry()
347 text_entry.set_property('hildon-input-mode', \
348 'HILDON_GTK_INPUT_MODE_FULL')
349 else:
350 text_entry = gtk.Entry()
351 text_entry.set_activates_default(True)
352 if text is not None:
353 text_entry.set_text(text)
354 text_entry.select_region(0, -1)
356 if not empty:
357 def on_text_changed(editable):
358 can_confirm = (editable.get_text() != '')
359 dialog.set_response_sensitive(gtk.RESPONSE_OK, can_confirm)
360 text_entry.connect('changed', on_text_changed)
361 if text is None:
362 dialog.set_response_sensitive(gtk.RESPONSE_OK, False)
364 hbox = gtk.HBox()
365 if not gpodder.ui.fremantle:
366 hbox.set_border_width(10)
367 hbox.set_spacing(10)
368 hbox.pack_start(gtk.Label(prompt), False, False)
369 hbox.pack_start(text_entry, True, True)
370 dialog.vbox.pack_start(hbox, True, True)
372 dialog.show_all()
373 response = dialog.run()
374 dialog.destroy()
376 if response == gtk.RESPONSE_OK:
377 return text_entry.get_text()
378 else:
379 return None
381 def show_login_dialog(self, title, message, username=None, password=None, username_prompt=_('Username'), register_callback=None):
382 """ An authentication dialog based on
383 http://ardoris.wordpress.com/2008/07/05/pygtk-text-entry-dialog/ """
385 if gpodder.ui.fremantle:
386 dialog = gtk.Dialog(title, self.main_window,
387 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
388 (str(_('Login')), gtk.RESPONSE_OK))
389 dialog.vbox.add(gtk.Label(message))
390 else:
391 dialog = gtk.MessageDialog(
392 self.main_window,
393 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
394 gtk.MESSAGE_QUESTION,
395 gtk.BUTTONS_CANCEL)
396 dialog.add_button(_('Login'), gtk.RESPONSE_OK)
397 dialog.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG))
398 dialog.set_title(_('Authentication required'))
399 dialog.set_markup('<span weight="bold" size="larger">' + title + '</span>')
400 dialog.format_secondary_markup(message)
401 dialog.set_default_response(gtk.RESPONSE_OK)
403 if register_callback is not None:
404 dialog.add_button(_('New user'), gtk.RESPONSE_HELP)
406 if gpodder.ui.fremantle:
407 import hildon
408 username_entry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
409 password_entry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
410 else:
411 username_entry = gtk.Entry()
412 password_entry = gtk.Entry()
414 if gpodder.ui.maemo:
415 # Disable input capitalization for the login fields
416 username_entry.set_property('hildon-input-mode', \
417 gtk.HILDON_GTK_INPUT_MODE_FULL)
418 password_entry.set_property('hildon-input-mode', \
419 gtk.HILDON_GTK_INPUT_MODE_FULL)
421 username_entry.connect('activate', lambda w: password_entry.grab_focus())
422 password_entry.set_visibility(False)
423 password_entry.set_activates_default(True)
425 if username is not None:
426 username_entry.set_text(username)
427 if password is not None:
428 password_entry.set_text(password)
430 table = gtk.Table(2, 2)
431 table.set_row_spacings(6)
432 table.set_col_spacings(6)
434 username_label = gtk.Label()
435 username_label.set_markup('<b>' + username_prompt + ':</b>')
436 username_label.set_alignment(0.0, 0.5)
437 table.attach(username_label, 0, 1, 0, 1, gtk.FILL, 0)
438 table.attach(username_entry, 1, 2, 0, 1)
440 password_label = gtk.Label()
441 password_label.set_markup('<b>' + _('Password') + ':</b>')
442 password_label.set_alignment(0.0, 0.5)
443 table.attach(password_label, 0, 1, 1, 2, gtk.FILL, 0)
444 table.attach(password_entry, 1, 2, 1, 2)
446 dialog.vbox.pack_end(table, True, True, 0)
447 dialog.show_all()
448 response = dialog.run()
450 while response == gtk.RESPONSE_HELP:
451 register_callback()
452 response = dialog.run()
454 password_entry.set_visibility(True)
455 dialog.destroy()
457 return response == gtk.RESPONSE_OK, ( username_entry.get_text(), password_entry.get_text() )
459 def show_copy_dialog(self, src_filename, dst_filename=None, dst_directory=None, title=_('Select destination')):
460 if dst_filename is None:
461 dst_filename = src_filename
463 if dst_directory is None:
464 dst_directory = os.path.expanduser('~')
466 base, extension = os.path.splitext(src_filename)
468 if not dst_filename.endswith(extension):
469 dst_filename += extension
471 if gpodder.ui.desktop or gpodder.ui.fremantle:
472 # FIXME: Hildonization for Fremantle
473 dlg = gtk.FileChooserDialog(title=title, parent=self.main_window, action=gtk.FILE_CHOOSER_ACTION_SAVE)
474 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
475 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
476 elif gpodder.ui.diablo:
477 import hildon
478 dlg = hildon.FileChooserDialog(self.main_window, gtk.FILE_CHOOSER_ACTION_SAVE)
480 dlg.set_do_overwrite_confirmation(True)
481 dlg.set_current_name(os.path.basename(dst_filename))
482 dlg.set_current_folder(dst_directory)
484 result = False
485 folder = dst_directory
486 if dlg.run() == gtk.RESPONSE_OK:
487 result = True
488 dst_filename = dlg.get_filename()
489 folder = dlg.get_current_folder()
490 if not dst_filename.endswith(extension):
491 dst_filename += extension
493 shutil.copyfile(src_filename, dst_filename)
495 dlg.destroy()
496 return (result, folder)
498 class TreeViewHelper(object):
499 """Container for gPodder-specific TreeView attributes."""
500 BUTTON_PRESS = '_gpodder_button_press'
501 LAST_TOOLTIP = '_gpodder_last_tooltip'
502 CAN_TOOLTIP = '_gpodder_can_tooltip'
503 ROLE = '_gpodder_role'
505 # Enum for the role attribute
506 ROLE_PODCASTS, ROLE_EPISODES, ROLE_DOWNLOADS = range(3)
508 @classmethod
509 def set(cls, treeview, role):
510 setattr(treeview, cls.BUTTON_PRESS, (0, 0))
511 setattr(treeview, cls.LAST_TOOLTIP, None)
512 setattr(treeview, cls.CAN_TOOLTIP, True)
513 setattr(treeview, cls.ROLE, role)
515 @classmethod
516 def save_button_press_event(cls, treeview, event):
517 setattr(treeview, cls.BUTTON_PRESS, (event.x, event.y))
518 return True
520 @classmethod
521 def get_button_press_event(cls, treeview):
522 return getattr(treeview, cls.BUTTON_PRESS)
524 @staticmethod
525 def make_search_equal_func(gpodder_model):
526 def func(model, column, key, iter):
527 if model is None:
528 return True
529 key = key.lower()
530 for column in gpodder_model.SEARCH_COLUMNS:
531 if key in model.get_value(iter, column).lower():
532 return False
533 return True
534 return func