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/>.
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)
41 if not pynotify
.init('gPodder'):
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
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."""
109 def _on_configure_event_maemo(self
, window
, event
):
110 if float(event
.width
)/float(event
.height
) < 1:
111 orientation
= Orientation
.PORTRAIT
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
:
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)
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)
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
145 self
._maemo
_fullscreen
= 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
153 self
._window
_visible
= True
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
163 if self
._window
_iconified
:
164 self
._window
_iconified
= 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
:
180 dlg
= hildon
.Note('information', (self
.main_window
, message
))
185 message
= '%s\n%s' % (title
, message
)
186 dlg
= hildon
.hildon_note_new_information(self
.main_window
, \
193 pango_markup
= '<b>%s</b>\n<small>%s</small>' % (title
, message
)
195 hildon
.hildon_banner_show_information_with_markup(gtk
.Label(''), None, pango_markup
)
197 # We're probably running the Diablo UI on Maemo 5 :)
198 hildon
.hildon_banner_show_information(self
.main_window
, \
200 elif gpodder
.ui
.fremantle
:
206 message
= '%s\n%s' % (title
, message
)
207 dlg
= hildon
.hildon_note_new_information(self
.main_window
, \
212 hildon
.hildon_banner_show_information(self
.main_window
, \
215 # XXX: Dirty hack to get access to the gPodder-specific config object
216 config
= getattr(self
, '_config', getattr(self
, 'config', None))
219 dlg
= gtk
.MessageDialog(self
.main_window
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_INFO
, gtk
.BUTTONS_OK
)
221 dlg
.set_title(str(title
))
222 dlg
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title
, message
))
224 dlg
.set_markup('<span weight="bold" size="larger">%s</span>' % (message
))
227 elif config
is not None and config
.enable_notifications
:
228 if pynotify
is not None:
231 notification
= pynotify
.Notification(title
, message
,\
236 # See http://gpodder.org/bug/966
238 elif widget
and isinstance(widget
, gtk
.Widget
):
239 if not widget
.window
:
240 widget
= self
.main_window
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
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)
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
)
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
)
285 dlg
.set_title(str(title
))
286 dlg
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title
, message
))
288 dlg
.set_markup('<span weight="bold" size="larger">%s</span>' % (message
))
291 return response
== gtk
.RESPONSE_YES
292 elif gpodder
.ui
.diablo
:
295 dlg
= hildon
.Note('confirmation', (self
.main_window
, message
))
297 # Kludgy workaround: We're running the Diablo UI on Maemo 5 :)
298 dlg
= hildon
.hildon_note_new_confirmation(self
.main_window
, \
302 return response
== gtk
.RESPONSE_OK
303 elif gpodder
.ui
.fremantle
:
305 dlg
= hildon
.hildon_note_new_confirmation(self
.main_window
, \
309 return response
== gtk
.RESPONSE_OK
311 raise Exception('Unknown interface type')
313 def show_text_edit_dialog(self
, title
, prompt
, text
=None, empty
=False, \
315 dialog
= gtk
.Dialog(title
, self
.main_window
, \
316 gtk
.DIALOG_MODAL | gtk
.DIALOG_DESTROY_WITH_PARENT
)
318 if gpodder
.ui
.fremantle
:
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
)
325 cancel_button
= dialog
.add_button(gtk
.STOCK_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)
333 dialog
.set_default_size(650, -1)
334 dialog
.set_default_response(gtk
.RESPONSE_OK
)
336 if gpodder
.ui
.fremantle
:
338 text_entry
= hildon
.Entry(gtk
.HILDON_SIZE_AUTO
)
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
:
346 text_entry
= gtk
.Entry()
347 text_entry
.set_property('hildon-input-mode', \
348 'HILDON_GTK_INPUT_MODE_FULL')
350 text_entry
= gtk
.Entry()
351 text_entry
.set_activates_default(True)
353 text_entry
.set_text(text
)
354 text_entry
.select_region(0, -1)
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
)
362 dialog
.set_response_sensitive(gtk
.RESPONSE_OK
, False)
365 if not gpodder
.ui
.fremantle
:
366 hbox
.set_border_width(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)
373 response
= dialog
.run()
376 if response
== gtk
.RESPONSE_OK
:
377 return text_entry
.get_text()
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
))
391 dialog
= gtk
.MessageDialog(
393 gtk
.DIALOG_MODAL | gtk
.DIALOG_DESTROY_WITH_PARENT
,
394 gtk
.MESSAGE_QUESTION
,
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
:
408 username_entry
= hildon
.Entry(gtk
.HILDON_SIZE_AUTO
)
409 password_entry
= hildon
.Entry(gtk
.HILDON_SIZE_AUTO
)
411 username_entry
= gtk
.Entry()
412 password_entry
= gtk
.Entry()
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)
448 response
= dialog
.run()
450 while response
== gtk
.RESPONSE_HELP
:
452 response
= dialog
.run()
454 password_entry
.set_visibility(True)
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
:
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
)
485 folder
= dst_directory
486 if dlg
.run() == gtk
.RESPONSE_OK
:
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
)
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)
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
)
516 def save_button_press_event(cls
, treeview
, event
):
517 setattr(treeview
, cls
.BUTTON_PRESS
, (event
.x
, event
.y
))
521 def get_button_press_event(cls
, treeview
):
522 return getattr(treeview
, cls
.BUTTON_PRESS
)
525 def make_search_equal_func(gpodder_model
):
526 def func(model
, column
, key
, iter):
530 for column
in gpodder_model
.SEARCH_COLUMNS
:
531 if key
in model
.get_value(iter, column
).lower():