20
20
from os.path import basename
22
22
import pygtk; pygtk.require ("2.0")
23
import gobject, gtk, gtk.glade
23
import gobject, gtk, gtk.glade, gconf
25
25
from AboutDialog import AboutDialog
26
26
from AuthenticationDialog import AuthenticationDialog
27
27
from ProgressDialog import ProgressDialog
28
28
from ErrorDialog import ErrorDialog
29
import ImageStore, ImageList, StatusBar
29
import ImageStore, ImageList, StatusBar, PrivacyCombo, SafetyCombo, GroupSelector
31
31
from flickrest import Flickr
32
32
from twisted.web.client import getPage
89
97
self.model = ImageStore.ImageStore ()
98
self.model.connect("row-inserted", self.on_model_changed)
99
self.model.connect("row-deleted", self.on_model_changed)
90
101
self.thumbview.set_model(self.model)
91
102
self.thumbview.connect("drag_data_received", self.on_drag_data_received)
93
104
selection = self.thumbview.get_selection()
94
105
selection.connect("changed", self.on_selection_changed)
96
108
self.current_it = None
97
109
self.last_folder = None
98
110
self.upload_quota = None
112
self.thumbnail_image.clear()
113
self.thumbnail_image.set_size_request(128, 128)
100
self.change_signals = []
115
self.change_signals = [] # List of (widget, signal ID) tuples
101
116
self.change_signals.append((self.title_entry, self.title_entry.connect('changed', self.on_field_changed, ImageStore.COL_TITLE)))
102
self.change_signals.append((self.desc_entry, self.desc_entry.connect('changed', self.on_field_changed, ImageStore.COL_DESCRIPTION)))
117
self.change_signals.append((self.desc_view.get_buffer(), self.desc_view.get_buffer().connect('changed', self.on_field_changed, ImageStore.COL_DESCRIPTION)))
103
118
self.change_signals.append((self.tags_entry, self.tags_entry.connect('changed', self.on_field_changed, ImageStore.COL_TAGS)))
119
self.change_signals.append((self.group_selector, self.group_selector.connect('changed', self.on_field_changed, ImageStore.COL_GROUPS)))
120
self.change_signals.append((self.privacy_combo, self.privacy_combo.connect('changed', self.on_field_changed, ImageStore.COL_PRIVACY)))
121
self.change_signals.append((self.safety_combo, self.safety_combo.connect('changed', self.on_field_changed, ImageStore.COL_SAFETY)))
122
self.change_signals.append((self.visible_check, self.visible_check.connect('toggled', self.on_field_changed, ImageStore.COL_VISIBLE)))
104
124
self.thumbnail_image.connect('size-allocate', self.update_thumbnail)
105
125
self.old_thumb_allocation = None
107
127
# The set selector combo
108
128
self.sets = gtk.ListStore (gobject.TYPE_STRING, # ID
109
129
gobject.TYPE_STRING, # Name
128
150
self.progress_dialog = ProgressDialog(cancel)
129
151
self.progress_dialog.set_transient_for(self.window)
130
152
# Disable the Upload menu until the user has authenticated
131
self.upload_menu.set_sensitive(False)
155
# Update the proxy configuration
156
client = gconf.client_get_default()
157
client.add_dir("/system/http_proxy", gconf.CLIENT_PRELOAD_RECURSIVE)
158
client.notify_add("/system/http_proxy", self.proxy_changed)
159
self.proxy_changed(client, 0, None, None)
133
161
# Connect to flickr, go go go
134
162
self.flickr.authenticate_1().addCallbacks(self.auth_open_url, self.twisted_error)
136
164
def twisted_error(self, failure):
137
167
dialog = ErrorDialog(self.window)
138
168
dialog.set_from_failure(failure)
171
def proxy_changed(self, client, cnxn_id, entry, something):
172
if client.get_bool("/system/http_proxy/use_http_proxy"):
173
host = client.get_string("/system/http_proxy/host")
174
port = client.get_int("/system/http_proxy/port")
175
if host is None or host == "" or port == 0:
176
self.flickr.set_proxy(None)
179
if client.get_bool("/system/http_proxy/use_authentication"):
180
user = client.get_string("/system/http_proxy/authentication_user")
181
password = client.get_string("/system/http_proxy/authentication_password")
182
if user and user != "":
183
url = "http://%s:%s@%s:%d" % (user, password, host, port)
185
url = "http://%s:%d" % (host, port)
187
url = "http://%s:%d" % (host, port)
189
self.flickr.set_proxy(url)
191
self.flickr.set_proxy(None)
141
193
def get_custom_handler(self, glade, function_name, widget_name, str1, str2, int1, int2):
142
194
"""libglade callback to create custom widgets."""
143
handler = getattr(self, function_name)
144
return handler(str1, str2, int1, int2)
195
handler = getattr(self, function_name, None)
197
return handler(str1, str2, int1, int2)
199
widget = eval(function_name)
203
def group_selector_new (self, *args):
204
w = GroupSelector.GroupSelector(self.flickr)
146
208
def image_list_new (self, *args):
147
209
"""Custom widget creation function to make the image list."""
206
280
url = "http://static.flickr.com/%s/%s_%s%s.jpg" % (photoset.get("server"), photoset.get("primary"), photoset.get("secret"), "_s")
207
281
getPage (url).addCallback (self.got_set_thumb, it).addErrback(self.twisted_error)
209
def on_field_changed(self, entry, column):
283
def on_field_changed(self, widget, column):
210
284
"""Callback when the entry fields are changed."""
285
if isinstance(widget, gtk.Entry) or isinstance(widget, gtk.TextBuffer):
286
value = widget.get_property("text")
287
elif isinstance(widget, gtk.ToggleButton):
288
value = widget.get_active()
289
elif isinstance(widget, gtk.ComboBox):
290
value = widget.get_active_iter()
291
elif isinstance(widget, GroupSelector.GroupSelector):
292
value = widget.get_selected_groups()
294
raise "Unhandled widget type %s" % widget
211
296
selection = self.thumbview.get_selection()
212
297
(model, items) = selection.get_selected_rows()
213
298
for path in items:
214
299
it = self.model.get_iter(path)
215
self.model.set_value (it, column, entry.get_text())
216
(title, desc, tags) = self.model.get(it,
217
ImageStore.COL_TITLE,
218
ImageStore.COL_DESCRIPTION,
220
self.model.set_value (it, ImageStore.COL_INFO, self.get_image_info(title, desc, tags))
300
self.model.set_value (it, column, value)
302
# TODO: remove this and use the field-changed logic
222
303
def on_set_combo_changed(self, combo):
223
304
"""Callback when the set combo is changed."""
224
305
set_it = self.set_combo.get_active_iter()
285
366
if response == gtk.RESPONSE_CANCEL:
368
elif self.is_connected and self.model.iter_n_children(None) > 0:
369
dialog = gtk.MessageDialog(type=gtk.MESSAGE_WARNING, parent=self.window)
370
dialog.add_buttons(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
371
gtk.STOCK_QUIT, gtk.RESPONSE_OK)
372
dialog.set_markup(_('<b>Photos to be uploaded</b>'))
373
dialog.format_secondary_text(_('There are photos pending to '
375
'Are you sure you want to quit?'))
376
response = dialog.run()
378
if response == gtk.RESPONSE_CANCEL:
288
381
import twisted.internet.reactor
289
382
twisted.internet.reactor.stop()
291
def on_remove_activate(self, menuitem):
292
"""Callback from File->Remove."""
384
def on_remove_activate(self, widget):
385
"""Callback from File->Remove or Remove button."""
293
386
selection = self.thumbview.get_selection()
294
387
(model, items) = selection.get_selected_rows()
399
495
[obj.handler_block(i) for obj,i in self.change_signals]
401
def enable_field(field, text):
497
def enable_field(field, value):
402
498
field.set_sensitive(True)
499
if isinstance(field, gtk.Entry):
500
field.set_text(value)
501
elif isinstance(field, gtk.TextView):
502
field.get_buffer().set_text (value)
503
elif isinstance(field, gtk.ToggleButton):
504
field.set_active(value)
505
elif isinstance(field, gtk.ComboBox):
507
field.set_active_iter(value)
509
# This means the default value is always the first
511
elif isinstance(field, GroupSelector.GroupSelector):
512
field.set_selected_groups(value)
514
raise "Unhandled widget type %s" % field
404
515
def disable_field(field):
405
516
field.set_sensitive(False)
517
if isinstance(field, gtk.Entry):
519
elif isinstance(field, gtk.TextView):
520
field.get_buffer().set_text ("")
521
elif isinstance(field, gtk.ToggleButton):
522
field.set_active(True)
523
elif isinstance(field, gtk.ComboBox):
525
elif isinstance(field, GroupSelector.GroupSelector):
526
field.set_selected_groups(())
528
raise "Unhandled widget type %s" % field
408
530
(model, items) = selection.get_selected_rows()
411
533
# TODO: do something clever with multiple selections
412
534
self.current_it = self.model.get_iter(items[0])
413
(title, desc, tags, set_it) = self.model.get(self.current_it,
414
ImageStore.COL_TITLE,
415
ImageStore.COL_DESCRIPTION,
535
(title, desc, tags, set_it, groups, privacy_it, safety_it, visible) = self.model.get(self.current_it,
536
ImageStore.COL_TITLE,
537
ImageStore.COL_DESCRIPTION,
540
ImageStore.COL_GROUPS,
541
ImageStore.COL_PRIVACY,
542
ImageStore.COL_SAFETY,
543
ImageStore.COL_VISIBLE)
419
545
enable_field(self.title_entry, title)
420
enable_field(self.desc_entry, desc)
546
enable_field(self.desc_view, desc)
421
547
enable_field(self.tags_entry, tags)
422
self.set_combo.set_sensitive(True)
424
self.set_combo.set_active_iter(set_it)
426
self.set_combo.set_active(0)
548
enable_field(self.set_combo, set_it)
549
enable_field(self.group_selector, groups)
550
enable_field(self.privacy_combo, privacy_it)
551
enable_field(self.safety_combo, safety_it)
552
enable_field(self.visible_check, visible)
427
554
self.update_thumbnail(self.thumbnail_image)
429
556
self.current_it = None
430
557
disable_field(self.title_entry)
431
disable_field(self.desc_entry)
558
disable_field(self.desc_view)
432
559
disable_field(self.tags_entry)
433
self.set_combo.set_sensitive(False)
434
self.set_combo.set_active(-1)
560
disable_field(self.set_combo)
561
disable_field(self.group_selector)
562
disable_field(self.privacy_combo)
563
disable_field(self.safety_combo)
564
disable_field(self.visible_check)
436
566
self.thumbnail_image.set_from_pixbuf(None)
438
568
[obj.handler_unblock(i) for obj,i in self.change_signals]
440
def get_image_info(self, title, description, tags):
441
from xml.sax.saxutils import escape
445
info_title = _("No title")
448
info_desc = description
450
info_desc = _("No description")
452
s = "<b><big>%s</big></b>\n%s\n" % (escape (info_title), escape (info_desc))
454
colour = self.window.style.text[gtk.STATE_INSENSITIVE].pixel
455
s = s + "<span color='#%X'>%s</span>" % (colour, escape (tags))
458
570
def add_image_filename(self, filename):
459
571
"""Add a file to the image list. Called by the File->Add Photo and drag
460
572
and drop callbacks."""
461
573
# TODO: MIME type check
575
# Check the file size
576
filesize = os.path.getsize(filename)
577
if filesize > 20 * 1024 * 1024:
578
d = ErrorDialog(self.window)
579
d.set_from_string("Image %s is too large, images must be no larger than 20MB in size." % filename)
463
583
# TODO: we open the file three times now, which is madness, especially
464
584
# if gnome-vfs is used to read remote files. Need to find/write EXIF
465
585
# and IPTC parsers that are incremental.
530
650
self.model.set(self.model.append(),
531
651
ImageStore.COL_FILENAME, filename,
532
ImageStore.COL_SIZE, os.path.getsize(filename),
652
ImageStore.COL_SIZE, filesize,
533
653
ImageStore.COL_IMAGE, None,
534
654
ImageStore.COL_PREVIEW, preview,
535
655
ImageStore.COL_THUMBNAIL, thumb,
536
656
ImageStore.COL_TITLE, title,
537
657
ImageStore.COL_DESCRIPTION, desc,
538
658
ImageStore.COL_TAGS, tags,
539
ImageStore.COL_INFO, self.get_image_info(title, desc, tags))
659
ImageStore.COL_VISIBLE, True)
541
661
self.update_statusbar()
543
664
def on_drag_data_received(self, widget, context, x, y, selection, targetType, timestamp):
544
665
"""Drag and drop callback when data is received."""
618
739
def add_to_set(self, rsp, set):
619
740
"""Callback from the upload method to add the picture to a set."""
620
self.flickr.photosets_addPhoto(photoset_id=set,
621
photo_id=rsp.find("photoid").text)
624
def upload_error(self, failure):
625
self.twisted_error(failure)
626
# TODO: nasty duplicate of the code in upload()
741
photo_id=rsp.find("photoid").text
742
self.flickr.photosets_addPhoto(photo_id=photo_id, photoset_id=set).addErrback(self.twisted_error)
745
def add_to_groups(self, rsp, groups):
746
"""Callback from the upload method to add the picture to a groups."""
747
photo_id=rsp.find("photoid").text
750
# Code 6 means "moderated", which isn't an error
751
if failure.value.code != 6:
752
twisted_error(self, failure)
753
self.flickr.groups_pools_add(photo_id=photo_id, group_id=group).addErrback(error)
756
def upload_done(self):
627
757
self.cancel_upload = False
628
758
self.window.set_title(_("Flickr Uploader"))
629
759
self.upload_menu.set_sensitive(True)
760
self.upload_button.set_sensitive(True)
630
761
self.uploading = False
631
762
self.progress_dialog.hide()
632
763
self.thumbview.set_sensitive(True)
764
self.update_statusbar()
633
765
self.statusbar.update_quota()
767
def upload_error(self, failure):
768
self.twisted_error(failure)
635
771
def upload(self, response=None):
636
772
"""Upload worker function, called by the File->Upload callback. As this
637
773
calls itself in the deferred callback, it takes a response argument."""
644
780
it = self.model.get_iter_first()
645
781
if self.cancel_upload or it is None:
646
self.cancel_upload = False
647
self.window.set_title(_("Flickr Uploader"))
648
self.upload_menu.set_sensitive(True)
649
self.uploading = False
650
self.progress_dialog.hide()
651
self.thumbview.set_sensitive(True)
652
self.statusbar.update_quota()
655
(filename, thumb, pixbuf, title, desc, tags, set_it) = self.model.get(it,
656
ImageStore.COL_FILENAME,
657
ImageStore.COL_THUMBNAIL,
658
ImageStore.COL_IMAGE,
659
ImageStore.COL_TITLE,
660
ImageStore.COL_DESCRIPTION,
785
(filename, thumb, pixbuf, title, desc, tags, set_it, groups, privacy_it, safety_it, visible) = self.model.get(it,
786
ImageStore.COL_FILENAME,
787
ImageStore.COL_THUMBNAIL,
788
ImageStore.COL_IMAGE,
789
ImageStore.COL_TITLE,
790
ImageStore.COL_DESCRIPTION,
793
ImageStore.COL_GROUPS,
794
ImageStore.COL_PRIVACY,
795
ImageStore.COL_SAFETY,
796
ImageStore.COL_VISIBLE)
663
797
# Lookup the set ID from the iterator
665
799
(set_id,) = self.sets.get (set_it, 0)
804
(is_public, is_family, is_friend) = self.privacy_combo.get_acls_for_iter(privacy_it)
806
is_public = is_family = is_friend = None
809
safety = self.safety_combo.get_safety_for_iter(safety_it)
669
813
self.update_progress(filename, title, thumb)
670
814
self.upload_index += 1
671
815
self.current_upload_it = it
674
818
d = self.flickr.upload(filename=filename,
675
title=title, desc=desc,
678
d.addCallback(self.add_to_set, set_id)
679
d.addCallbacks(self.upload, self.upload_error)
819
title=title, desc=desc,
820
tags=tags, search_hidden=not visible, safety=safety,
821
is_public=is_public, is_family=is_family, is_friend=is_friend)
681
823
# This isn't very nice, but might be the best way
683
825
pixbuf.save_to_callback(lambda d: data.append(d), "png", {})
684
826
d = self.flickr.upload(imageData=''.join(data),
685
title=title, desc=desc,
688
d.addCallback(self.add_to_set, set_id)
689
d.addCallbacks(self.upload, self.upload_error)
827
title=title, desc=desc, tags=tags,
828
search_hidden=not visible, safety=safety,
829
is_public=is_public, is_family=is_family, is_friend=is_friend)
691
831
print "No filename or pixbuf stored"
834
d.addCallback(self.add_to_set, set_id)
836
d.addCallback(self.add_to_groups, groups)
837
d.addCallbacks(self.upload, self.upload_error)