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
:
179 dlg
= hildon
.Note('information', (self
.main_window
, message
))
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
:
193 message
= '%s\n%s' % (title
, message
)
194 dlg
= hildon
.hildon_note_new_information(self
.main_window
, \
199 hildon
.hildon_banner_show_information(self
.main_window
, \
203 dlg
= gtk
.MessageDialog(self
.main_window
, gtk
.DIALOG_MODAL
, gtk
.MESSAGE_INFO
, gtk
.BUTTONS_OK
)
205 dlg
.set_title(str(title
))
206 dlg
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title
, message
))
208 dlg
.set_markup('<span weight="bold" size="larger">%s</span>' % (message
))
211 elif pynotify
is not None:
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
)
225 if widget
and isinstance(widget
, gtk
.Widget
):
226 if not widget
.window
:
227 widget
= self
.main_window
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
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)
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
)
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
)
272 dlg
.set_title(str(title
))
273 dlg
.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title
, message
))
275 dlg
.set_markup('<span weight="bold" size="larger">%s</span>' % (message
))
278 return response
== gtk
.RESPONSE_YES
279 elif gpodder
.ui
.diablo
:
281 dlg
= hildon
.Note('confirmation', (self
.main_window
, message
))
284 return response
== gtk
.RESPONSE_OK
285 elif gpodder
.ui
.fremantle
:
287 dlg
= hildon
.hildon_note_new_confirmation(self
.main_window
, \
291 return response
== gtk
.RESPONSE_OK
293 raise Exception('Unknown interface type')
295 def show_text_edit_dialog(self
, title
, prompt
, text
=None, empty
=False, \
297 dialog
= gtk
.Dialog(title
, self
.main_window
, \
298 gtk
.DIALOG_MODAL | gtk
.DIALOG_DESTROY_WITH_PARENT
)
300 if gpodder
.ui
.fremantle
:
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
)
307 cancel_button
= dialog
.add_button(gtk
.STOCK_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
:
317 text_entry
= hildon
.Entry(gtk
.HILDON_SIZE_AUTO
)
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
:
325 text_entry
= gtk
.Entry()
326 text_entry
.set_property('hildon-input-mode', \
327 'HILDON_GTK_INPUT_MODE_FULL')
329 text_entry
= gtk
.Entry()
330 text_entry
.set_activates_default(True)
332 text_entry
.set_text(text
)
333 text_entry
.select_region(0, -1)
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
)
341 dialog
.set_response_sensitive(gtk
.RESPONSE_OK
, False)
344 if not gpodder
.ui
.fremantle
:
345 hbox
.set_border_width(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)
352 response
= dialog
.run()
355 if response
== gtk
.RESPONSE_OK
:
356 return text_entry
.get_text()
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
))
370 dialog
= gtk
.MessageDialog(
372 gtk
.DIALOG_MODAL | gtk
.DIALOG_DESTROY_WITH_PARENT
,
373 gtk
.MESSAGE_QUESTION
,
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
:
387 username_entry
= hildon
.Entry(gtk
.HILDON_SIZE_AUTO
)
388 password_entry
= hildon
.Entry(gtk
.HILDON_SIZE_AUTO
)
390 username_entry
= gtk
.Entry()
391 password_entry
= gtk
.Entry()
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)
427 response
= dialog
.run()
429 while response
== gtk
.RESPONSE_HELP
:
431 response
= dialog
.run()
433 password_entry
.set_visibility(True)
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
:
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
)
464 folder
= dst_directory
465 if dlg
.run() == gtk
.RESPONSE_OK
:
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
)
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)
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
)
495 def save_button_press_event(cls
, treeview
, event
):
496 setattr(treeview
, cls
.BUTTON_PRESS
, (event
.x
, event
.y
))
500 def get_button_press_event(cls
, treeview
):
501 return getattr(treeview
, cls
.BUTTON_PRESS
)
504 def make_search_equal_func(gpodder_model
):
505 def func(model
, column
, key
, iter):
509 for column
in gpodder_model
.SEARCH_COLUMNS
:
510 if key
in model
.get_value(iter, column
).lower():