Add 2010 to the years in copyright notice
[gpodder.git] / src / gpodder / gtkui / interface / common.py
blobd7fbd2b967da8e10c2bab8dde33a2cfa85807d22
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 dlg = hildon.Note('information', (self.main_window, message))
180 dlg.run()
181 dlg.destroy()
182 else:
183 if title is None:
184 title = 'gPodder'
185 pango_markup = '<b>%s</b>\n<small>%s</small>' % (title, message)
186 hildon.hildon_banner_show_information_with_markup(gtk.Label(''), None, pango_markup)
187 elif gpodder.ui.fremantle:
188 import hildon
189 if important:
190 if title is None:
191 message = message
192 else:
193 message = '%s\n%s' % (title, message)
194 dlg = hildon.hildon_note_new_information(self.main_window, \
195 message)
196 dlg.run()
197 dlg.destroy()
198 else:
199 hildon.hildon_banner_show_information(self.main_window, \
200 '', message)
201 else:
202 if important:
203 dlg = gtk.MessageDialog(self.main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK)
204 if title:
205 dlg.set_title(str(title))
206 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
207 else:
208 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
209 dlg.run()
210 dlg.destroy()
211 elif pynotify is not None:
212 if title is None:
213 title = 'gPodder'
214 notification = pynotify.Notification(title, message, gpodder.icon_file)
216 if not self._window_iconified and self.main_window.is_active:
217 if self._window_visible:
218 if widget and isinstance(widget, gtk.Widget):
219 if not widget.window:
220 widget = self.main_window
221 notification.attach_to_widget(widget)
223 notification.show()
224 else:
225 if widget and isinstance(widget, gtk.Widget):
226 if not widget.window:
227 widget = self.main_window
228 else:
229 widget = self.main_window
230 notification = NotificationWindow(message, title, important=False, widget=widget)
231 notification.show_timeout()
233 def set_finger_friendly(self, widget):
235 If we are on Maemo, we carry out the necessary
236 operations to turn a widget into a finger-friendly
237 one, depending on which type of widget it is (i.e.
238 buttons will have more padding, TreeViews a thick
239 scrollbar, etc..)
241 if widget is None:
242 return None
244 if gpodder.ui.diablo or gpodder.ui.fremantle:
245 if isinstance(widget, gtk.Misc):
246 widget.set_padding(0, 5)
247 elif isinstance(widget, gtk.Button):
248 for child in widget.get_children():
249 if isinstance(child, gtk.Alignment):
250 child.set_padding(5, 5, 5, 5)
251 else:
252 child.set_padding(5, 5)
253 elif isinstance(widget, gtk.TreeView) or isinstance(widget, gtk.TextView):
254 parent = widget.get_parent()
255 elif isinstance(widget, gtk.MenuItem):
256 for child in widget.get_children():
257 self.set_finger_friendly(child)
258 submenu = widget.get_submenu()
259 if submenu is not None:
260 for child in submenu.get_children():
261 self.set_finger_friendly(child)
262 elif isinstance(widget, gtk.Menu):
263 for child in widget.get_children():
264 self.set_finger_friendly(child)
266 return widget
268 def show_confirmation(self, message, title=None):
269 if gpodder.ui.desktop:
270 dlg = gtk.MessageDialog(self.main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO)
271 if title:
272 dlg.set_title(str(title))
273 dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
274 else:
275 dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
276 response = dlg.run()
277 dlg.destroy()
278 return response == gtk.RESPONSE_YES
279 elif gpodder.ui.diablo:
280 import hildon
281 dlg = hildon.Note('confirmation', (self.main_window, message))
282 response = dlg.run()
283 dlg.destroy()
284 return response == gtk.RESPONSE_OK
285 elif gpodder.ui.fremantle:
286 import hildon
287 dlg = hildon.hildon_note_new_confirmation(self.main_window, \
288 message)
289 response = dlg.run()
290 dlg.destroy()
291 return response == gtk.RESPONSE_OK
292 else:
293 raise Exception('Unknown interface type')
295 def show_text_edit_dialog(self, title, prompt, text=None, empty=False, \
296 is_url=False):
297 dialog = gtk.Dialog(title, self.main_window, \
298 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT)
300 if gpodder.ui.fremantle:
301 import hildon
302 button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | \
303 gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
304 button.set_text(_('OK'), '')
305 dialog.add_action_widget(button, gtk.RESPONSE_OK)
306 else:
307 cancel_button = dialog.add_button(gtk.STOCK_CANCEL, \
308 gtk.RESPONSE_CANCEL)
309 ok_button = dialog.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK)
311 dialog.set_has_separator(False)
312 dialog.set_default_size(650, -1)
313 dialog.set_default_response(gtk.RESPONSE_OK)
315 if gpodder.ui.fremantle:
316 import hildon
317 text_entry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
318 if is_url:
319 # Disable word capitalization and word completion when
320 # requesting an URL to be entered (see Maemo bug 5184)
321 text_entry.set_property('hildon-input-mode', \
322 gtk.HILDON_GTK_INPUT_MODE_FULL)
323 elif gpodder.ui.diablo:
324 import hildon
325 text_entry = gtk.Entry()
326 text_entry.set_property('hildon-input-mode', \
327 'HILDON_GTK_INPUT_MODE_FULL')
328 else:
329 text_entry = gtk.Entry()
330 text_entry.set_activates_default(True)
331 if text is not None:
332 text_entry.set_text(text)
333 text_entry.select_region(0, -1)
335 if not empty:
336 def on_text_changed(editable):
337 can_confirm = (editable.get_text() != '')
338 dialog.set_response_sensitive(gtk.RESPONSE_OK, can_confirm)
339 text_entry.connect('changed', on_text_changed)
340 if text is None:
341 dialog.set_response_sensitive(gtk.RESPONSE_OK, False)
343 hbox = gtk.HBox()
344 if not gpodder.ui.fremantle:
345 hbox.set_border_width(10)
346 hbox.set_spacing(10)
347 hbox.pack_start(gtk.Label(prompt), False, False)
348 hbox.pack_start(text_entry, True, True)
349 dialog.vbox.pack_start(hbox, True, True)
351 dialog.show_all()
352 response = dialog.run()
353 dialog.destroy()
355 if response == gtk.RESPONSE_OK:
356 return text_entry.get_text()
357 else:
358 return None
360 def show_login_dialog(self, title, message, username=None, password=None, username_prompt=_('Username'), register_callback=None):
361 """ An authentication dialog based on
362 http://ardoris.wordpress.com/2008/07/05/pygtk-text-entry-dialog/ """
364 if gpodder.ui.fremantle:
365 dialog = gtk.Dialog(title, self.main_window,
366 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
367 (str(_('Login')), gtk.RESPONSE_OK))
368 dialog.vbox.add(gtk.Label(message))
369 else:
370 dialog = gtk.MessageDialog(
371 self.main_window,
372 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
373 gtk.MESSAGE_QUESTION,
374 gtk.BUTTONS_CANCEL)
375 dialog.add_button(_('Login'), gtk.RESPONSE_OK)
376 dialog.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG))
377 dialog.set_title(_('Authentication required'))
378 dialog.set_markup('<span weight="bold" size="larger">' + title + '</span>')
379 dialog.format_secondary_markup(message)
380 dialog.set_default_response(gtk.RESPONSE_OK)
382 if register_callback is not None:
383 dialog.add_button(_('New user'), gtk.RESPONSE_HELP)
385 if gpodder.ui.fremantle:
386 import hildon
387 username_entry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
388 password_entry = hildon.Entry(gtk.HILDON_SIZE_AUTO)
389 else:
390 username_entry = gtk.Entry()
391 password_entry = gtk.Entry()
393 if gpodder.ui.maemo:
394 # Disable input capitalization for the login fields
395 username_entry.set_property('hildon-input-mode', \
396 gtk.HILDON_GTK_INPUT_MODE_FULL)
397 password_entry.set_property('hildon-input-mode', \
398 gtk.HILDON_GTK_INPUT_MODE_FULL)
400 username_entry.connect('activate', lambda w: password_entry.grab_focus())
401 password_entry.set_visibility(False)
402 password_entry.set_activates_default(True)
404 if username is not None:
405 username_entry.set_text(username)
406 if password is not None:
407 password_entry.set_text(password)
409 table = gtk.Table(2, 2)
410 table.set_row_spacings(6)
411 table.set_col_spacings(6)
413 username_label = gtk.Label()
414 username_label.set_markup('<b>' + username_prompt + ':</b>')
415 username_label.set_alignment(0.0, 0.5)
416 table.attach(username_label, 0, 1, 0, 1, gtk.FILL, 0)
417 table.attach(username_entry, 1, 2, 0, 1)
419 password_label = gtk.Label()
420 password_label.set_markup('<b>' + _('Password') + ':</b>')
421 password_label.set_alignment(0.0, 0.5)
422 table.attach(password_label, 0, 1, 1, 2, gtk.FILL, 0)
423 table.attach(password_entry, 1, 2, 1, 2)
425 dialog.vbox.pack_end(table, True, True, 0)
426 dialog.show_all()
427 response = dialog.run()
429 while response == gtk.RESPONSE_HELP:
430 register_callback()
431 response = dialog.run()
433 password_entry.set_visibility(True)
434 dialog.destroy()
436 return response == gtk.RESPONSE_OK, ( username_entry.get_text(), password_entry.get_text() )
438 def show_copy_dialog(self, src_filename, dst_filename=None, dst_directory=None, title=_('Select destination')):
439 if dst_filename is None:
440 dst_filename = src_filename
442 if dst_directory is None:
443 dst_directory = os.path.expanduser('~')
445 base, extension = os.path.splitext(src_filename)
447 if not dst_filename.endswith(extension):
448 dst_filename += extension
450 if gpodder.ui.desktop or gpodder.ui.fremantle:
451 # FIXME: Hildonization for Fremantle
452 dlg = gtk.FileChooserDialog(title=title, parent=self.main_window, action=gtk.FILE_CHOOSER_ACTION_SAVE)
453 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
454 dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
455 elif gpodder.ui.diablo:
456 import hildon
457 dlg = hildon.FileChooserDialog(self.main_window, gtk.FILE_CHOOSER_ACTION_SAVE)
459 dlg.set_do_overwrite_confirmation(True)
460 dlg.set_current_name(os.path.basename(dst_filename))
461 dlg.set_current_folder(dst_directory)
463 result = False
464 folder = dst_directory
465 if dlg.run() == gtk.RESPONSE_OK:
466 result = True
467 dst_filename = dlg.get_filename()
468 folder = dlg.get_current_folder()
469 if not dst_filename.endswith(extension):
470 dst_filename += extension
472 shutil.copyfile(src_filename, dst_filename)
474 dlg.destroy()
475 return (result, folder)
477 class TreeViewHelper(object):
478 """Container for gPodder-specific TreeView attributes."""
479 BUTTON_PRESS = '_gpodder_button_press'
480 LAST_TOOLTIP = '_gpodder_last_tooltip'
481 CAN_TOOLTIP = '_gpodder_can_tooltip'
482 ROLE = '_gpodder_role'
484 # Enum for the role attribute
485 ROLE_PODCASTS, ROLE_EPISODES, ROLE_DOWNLOADS = range(3)
487 @classmethod
488 def set(cls, treeview, role):
489 setattr(treeview, cls.BUTTON_PRESS, (0, 0))
490 setattr(treeview, cls.LAST_TOOLTIP, None)
491 setattr(treeview, cls.CAN_TOOLTIP, True)
492 setattr(treeview, cls.ROLE, role)
494 @classmethod
495 def save_button_press_event(cls, treeview, event):
496 setattr(treeview, cls.BUTTON_PRESS, (event.x, event.y))
497 return True
499 @classmethod
500 def get_button_press_event(cls, treeview):
501 return getattr(treeview, cls.BUTTON_PRESS)
503 @staticmethod
504 def make_search_equal_func(gpodder_model):
505 def func(model, column, key, iter):
506 if model is None:
507 return True
508 key = key.lower()
509 for column in gpodder_model.SEARCH_COLUMNS:
510 if key in model.get_value(iter, column).lower():
511 return False
512 return True
513 return func