168
171
column0.add_attribute(text_renderer, 'text', 1)
169
172
column0.add_attribute(eject_renderer, 'pixbuf', 5)
170
173
self.append_column(column0)
173
176
# Size refers to the total size of images on the device, typically in
175
178
column1 = gtk.TreeViewColumn(_("Size"), gtk.CellRendererText(), text=2)
176
179
self.append_column(column1)
178
column2 = gtk.TreeViewColumn(_("Download Progress"),
181
column2 = gtk.TreeViewColumn(_("Download Progress"),
179
182
gtk.CellRendererProgress(),
183
186
self.append_column(column2)
186
189
icontheme = gtk.icon_theme_get_default()
188
self.eject_pixbuf = icontheme.load_icon('media-eject', 16,
191
self.eject_pixbuf = icontheme.load_icon('media-eject', 16,
189
192
gtk.ICON_LOOKUP_USE_BUILTIN)
191
194
self.eject_pixbuf = gtk.gdk.pixbuf_new_from_file(
192
195
paths.share_dir('glade3/media-eject.png'))
194
197
self.add_events(gtk.gdk.BUTTON_PRESS_MASK)
195
198
self.connect('button-press-event', self.button_clicked)
198
201
def add_device(self, process_id, device, progress_bar_text = ''):
200
203
# add the row, and get a temporary pointer to the row
204
207
if device.mount is None:
207
210
eject = self.eject_pixbuf
209
212
self.devices_by_scan_pid[process_id] = device
211
214
iter = self.liststore.append((device.get_icon(),
212
215
device.get_name(),
238
242
self.liststore.set_value(iter, 2, total_size_files)
240
244
logger.critical("This device is unknown")
242
246
def get_device(self, process_id):
243
247
return self.devices_by_scan_pid.get(process_id)
245
249
def remove_device(self, process_id):
246
250
if process_id in self.map_process_to_row:
247
251
iter = self._get_process_map(process_id)
248
252
self.liststore.remove(iter)
249
253
del self.map_process_to_row[process_id]
250
254
del self.devices_by_scan_pid[process_id]
252
256
def get_all_displayed_processes(self):
254
returns a list of the processes currently being displayed to the user
258
returns a list of the processes currently being displayed to the user
256
260
return self.map_process_to_row.keys()
259
263
def _set_process_map(self, process_id, iter):
261
convert the temporary iter into a tree reference, which is
265
convert the temporary iter into a tree reference, which is
265
269
path = self.liststore.get_path(iter)
266
270
treerowref = gtk.TreeRowReference(self.liststore, path)
267
271
self.map_process_to_row[process_id] = treerowref
269
273
def _get_process_map(self, process_id):
271
275
return the tree iter for this process
274
278
if process_id in self.map_process_to_row:
275
279
treerowref = self.map_process_to_row[process_id]
276
280
path = treerowref.get_path()
353
358
__gproperties__ = {
354
359
"image": (gobject.TYPE_PYOBJECT, "Image",
355
360
"Image", gobject.PARAM_READWRITE),
357
"filename": (gobject.TYPE_STRING, "Filename",
362
"filename": (gobject.TYPE_STRING, "Filename",
358
363
"Filename", '', gobject.PARAM_READWRITE),
360
365
"status": (gtk.gdk.Pixbuf, "Status",
361
366
"Status", gobject.PARAM_READWRITE),
364
369
def __init__(self, checkbutton_height):
365
370
gtk.CellRenderer.__init__(self)
366
371
self.image = None
368
373
self.image_area_size = 100
369
374
self.text_area_size = 30
371
376
self.checkbutton_height = checkbutton_height
372
377
self.icon_width = 20
374
379
def do_set_property(self, pspec, value):
375
380
setattr(self, pspec.name, value)
377
382
def do_get_property(self, pspec):
378
383
return getattr(self, pspec.name)
380
385
def do_render(self, window, widget, background_area, cell_area, expose_area, flags):
382
387
cairo_context = window.cairo_create()
385
390
y = cell_area.y + self.checkbutton_height - 8
386
391
w = cell_area.width
387
392
h = cell_area.height
389
#constrain operations to cell area, allowing for a 1 pixel border
394
#constrain operations to cell area, allowing for a 1 pixel border
391
396
#~ cairo_context.rectangle(x-1, y-1, w+2, h+2)
392
397
#~ cairo_context.clip()
394
399
#fill in the background with dark grey
395
400
#this ensures that a selected cell's fill does not make
396
401
#the text impossible to read
397
402
#~ cairo_context.rectangle(x, y, w, h)
398
403
#~ cairo_context.set_source_rgb(0.267, 0.267, 0.267)
399
404
#~ cairo_context.fill()
401
406
#image width and height
402
407
image_w = self.image.size[0]
403
408
image_h = self.image.size[1]
405
410
#center the image horizontally
406
411
#bottom align vertically
407
412
#top left and right corners for the image:
416
421
cairo_context.set_line_width(1)
417
422
cairo_context.rectangle(image_x-.5, image_y-.5, image_w+1, image_h+1)
418
423
cairo_context.stroke()
420
425
# draw a thin border around each cell
421
426
#~ cairo_context.set_source_rgb(0.33,0.33,0.33)
422
427
#~ cairo_context.rectangle(x, y, w, h)
423
428
#~ cairo_context.stroke()
426
431
cairo_context.set_source_surface(image, image_x, image_y)
427
432
cairo_context.paint()
430
435
context = pangocairo.CairoContext(cairo_context)
432
437
text_y = y + self.image_area_size + 10
433
438
text_w = w - self.icon_width
434
439
text_x = x + self.icon_width
435
440
#~ context.rectangle(text_x, text_y, text_w, 15)
438
443
layout = context.create_layout()
440
445
width = text_w * pango.SCALE
441
446
layout.set_width(width)
443
448
layout.set_alignment(pango.ALIGN_CENTER)
444
449
layout.set_ellipsize(pango.ELLIPSIZE_END)
446
451
#font color and size
447
452
fg_color = pango.AttrForeground(65535, 65535, 65535, 0, -1)
448
453
font_size = pango.AttrSize(8192, 0, -1) # 8 * 1024 = 8192
617
622
logger.critical("FIXME: unknown status: %s", status)
618
623
status_icon = self.not_downloaded_icon
621
626
def sort_by_timestamp(self):
622
627
self.liststore.set_sort_column_id(self.TIMESTAMP_COL, gtk.SORT_ASCENDING)
624
629
def on_selection_changed(self, iconview):
625
630
self._selected_items = self.get_selected_items()
627
632
def on_checkbutton_toggled(self, cellrenderertoggle, path):
628
633
paths = [p[0] for p in self._selected_items]
629
634
if int(path) not in paths:
630
635
self._selected_items = [path,]
632
637
for path in self._selected_items:
633
638
iter = self.liststore.get_iter(path)
634
639
status = self.liststore.get_value(iter, self.DOWNLOAD_STATUS_COL)
635
640
if status == STATUS_NOT_DOWNLOADED:
636
641
self.liststore.set_value(iter, self.SELECTED_COL, not cellrenderertoggle.get_active())
637
642
self.select_path(path)
639
644
self.rapid_app.set_download_action_sensitivity()
642
647
def set_selected(self, unique_id, value):
643
648
iter = self.get_iter_from_unique_id(unique_id)
644
649
self.liststore.set_value(iter, self.SELECTED_COL, value)
646
651
def add_file(self, rpd_file, generate_thumbnail):
648
653
thumbnail_icon = self.get_stock_icon(rpd_file.file_type)
661
666
STATUS_NOT_DOWNLOADED,
662
667
self.not_downloaded_icon
665
670
path = self.liststore.get_path(iter)
666
671
treerowref = gtk.TreeRowReference(self.liststore, path)
668
673
if scan_pid in self.process_index:
669
674
self.process_index[scan_pid].append(unique_id)
671
676
self.process_index[scan_pid] = [unique_id,]
673
678
self.treerow_index[unique_id] = treerowref
674
679
self.rpd_files[unique_id] = rpd_file
676
681
if generate_thumbnail:
677
682
self.total_thumbs_to_generate += 1
679
684
def get_sample_file(self, file_type):
680
"""Returns an rpd_file for of a given file type, or None if it does
685
"""Returns an rpd_file for of a given file type, or None if it does
682
687
for unique_id, rpd_file in self.rpd_files.iteritems():
683
688
if rpd_file.file_type == file_type:
684
689
if rpd_file.status <> STATUS_CANNOT_DOWNLOAD:
689
694
def get_unique_id_from_iter(self, iter):
690
695
return self.liststore.get_value(iter, 2)
692
697
def get_iter_from_unique_id(self, unique_id):
693
698
treerowref = self.treerow_index[unique_id]
694
699
path = treerowref.get_path()
695
700
return self.liststore.get_iter(path)
697
def on_item_activated(self, iconview, path):
702
def on_item_activated(self, iconview, path):
700
705
iter = self.liststore.get_iter(path)
701
706
self.show_preview(iter=iter)
702
707
self.advance_get_preview_image(iter)
705
710
def _get_preview(self, unique_id, rpd_file):
706
711
if unique_id not in self.previews_being_fetched:
707
712
#check if preview should be from a downloaded file, or the source
735
740
iter = self.liststore.get_iter(path)
736
741
unique_id = self.get_unique_id_from_iter(iter)
739
rpd_file = self.rpd_files[unique_id]
744
rpd_file = self.rpd_files[unique_id]
741
746
if unique_id in self.previews:
742
747
preview_image = self.previews[unique_id]
744
749
# request daemon process to get a full size thumbnail
745
750
self._get_preview(unique_id, rpd_file)
746
if unique_id in self.thumbnails:
751
if unique_id in self.thumbnails:
747
752
preview_image = self.thumbnails[unique_id]
749
754
preview_image = self.get_stock_icon(rpd_file.file_type)
751
756
checked = self.liststore.get_value(iter, self.SELECTED_COL)
752
757
include_checkbutton_visible = rpd_file.status == STATUS_NOT_DOWNLOADED
753
self.rapid_app.show_preview_image(unique_id, preview_image,
758
self.rapid_app.show_preview_image(unique_id, preview_image,
754
759
include_checkbutton_visible, checked)
756
761
def _get_next_iter(self, iter):
757
762
iter = self.liststore.iter_next(iter)
759
764
iter = self.liststore.get_iter_first()
762
767
def _get_prev_iter(self, iter):
763
768
row = self.liststore.get_path(iter)[0]
768
773
iter = self.liststore.get_iter(row)
771
776
def show_next_image(self, unique_id):
772
777
iter = self.get_iter_from_unique_id(unique_id)
773
778
iter = self._get_next_iter(iter)
775
780
if iter is not None:
776
781
self.show_preview(iter=iter)
778
783
# cache next image
779
784
self.advance_get_preview_image(iter, prev=False, next=True)
781
786
def show_prev_image(self, unique_id):
782
787
iter = self.get_iter_from_unique_id(unique_id)
783
788
iter = self._get_prev_iter(iter)
785
790
if iter is not None:
786
791
self.show_preview(iter=iter)
788
793
# cache next image
789
794
self.advance_get_preview_image(iter, prev=True, next=False)
792
797
def advance_get_preview_image(self, iter, prev=True, next=True):
795
800
next_iter = self._get_next_iter(iter)
796
801
unique_ids.append(self.get_unique_id_from_iter(next_iter))
799
804
prev_iter = self._get_prev_iter(iter)
800
805
unique_ids.append(self.get_unique_id_from_iter(prev_iter))
802
807
for unique_id in unique_ids:
803
808
if not unique_id in self.previews:
804
809
rpd_file = self.rpd_files[unique_id]
805
810
self._get_preview(unique_id, rpd_file)
807
812
def check_all(self, check_all, file_type=None):
808
813
for row in self.liststore:
809
814
if row[self.CHECKBUTTON_VISIBLE_COL]:
923
928
rpd_files = [self.rpd_files[unique_id] for unique_id in self.process_index[scan_pid]]
924
929
thumbnail_pid = self.thumbnail_manager.add_task((scan_pid, rpd_files))
925
930
self.generating_thumbnails[scan_pid] = thumbnail_pid
927
932
def _set_thumbnail(self, unique_id, icon):
928
933
treerowref = self.treerow_index[unique_id]
929
934
path = treerowref.get_path()
930
935
iter = self.liststore.get_iter(path)
931
self.liststore.set(iter, 0, icon)
936
self.liststore.set(iter, 0, icon)
933
938
def update_thumbnail(self, thumbnail_data):
935
940
Takes the generated thumbnail and updates the display
937
942
If the thumbnail_data includes a second image, that is used to
938
943
update the thumbnail list using the unique_id
940
945
unique_id = thumbnail_data[0]
941
946
thumbnail_icon = thumbnail_data[1]
943
948
if thumbnail_icon is not None:
944
949
# get the thumbnail icon in PIL format
945
950
thumbnail_icon = thumbnail_icon.get_image()
947
952
if thumbnail_icon:
948
953
self._set_thumbnail(unique_id, thumbnail_icon)
950
955
if len(thumbnail_data) > 2:
951
956
# get the 2nd image in PIL format
952
957
self.thumbnails[unique_id] = thumbnail_data[2].get_image()
954
959
def terminate_thumbnail_generation(self, scan_pid):
956
Terminates thumbnail generation if thumbnails are currently
961
Terminates thumbnail generation if thumbnails are currently
957
962
being generated for this scan_pid
960
965
if scan_pid in self.generating_thumbnails:
961
966
terminated = True
962
967
self.thumbnail_manager.terminate_process(
963
968
self.generating_thumbnails[scan_pid])
964
969
del self.generating_thumbnails[scan_pid]
966
971
if len(self.generating_thumbnails) == 0:
967
972
self._reset_thumbnail_tracking_and_display()
969
974
terminated = False
971
976
return terminated
973
978
def mark_thumbnails_needed(self, rpd_files):
974
979
for rpd_file in rpd_files:
975
980
if rpd_file.unique_id not in self.thumbnails:
976
981
rpd_file.generate_thumbnail = True
978
983
def _reset_thumbnail_tracking_and_display(self):
979
984
self.rapid_app.download_progressbar.set_fraction(0.0)
980
985
self.rapid_app.download_progressbar.set_text('')
981
986
self.thumbnails_generated = 0
982
987
self.total_thumbs_to_generate = 0
984
989
def thumbnail_results(self, source, condition):
985
990
connection = self.thumbnail_manager.get_pipe(source)
987
992
conn_type, data = connection.recv()
989
994
if conn_type == rpdmp.CONN_COMPLETE:
991
996
del self.generating_thumbnails[scan_pid]
992
997
connection.close()
996
1001
for thumbnail_data in data:
997
1002
self.update_thumbnail(thumbnail_data)
999
1004
self.thumbnails_generated += len(data)
1001
1006
# clear progress bar information if all thumbnails have been
1003
1008
if self.thumbnails_generated == self.total_thumbs_to_generate:
1060
1065
del self.rpd_files[rpd_file.unique_id]
1061
1066
if not keep_downloaded_files or not len(self.process_index[scan_pid]):
1062
1067
del self.process_index[scan_pid]
1064
1069
def display_thumbnails(self):
1065
1070
self.set_model(self.liststore)
1067
1072
class TaskManager:
1068
1073
def __init__(self, results_callback, batch_size):
1069
1074
self.results_callback = results_callback
1071
1076
# List of actual process, it's terminate_queue, and it's run_event
1072
1077
self._processes = []
1074
1079
self._pipes = {}
1075
1080
self.batch_size = batch_size
1077
1082
self.paused = False
1078
1083
self.no_tasks = 0
1081
1086
def add_task(self, task):
1082
1087
pid = self._setup_task(task)
1083
1088
logger.debug("TaskManager PID: %s", pid)
1084
1089
self.no_tasks += 1
1088
1093
def _setup_task(self, task):
1089
1094
task_results_conn, task_process_conn = self._setup_pipe()
1091
1096
source = task_results_conn.fileno()
1092
1097
self._pipes[source] = task_results_conn
1093
1098
gobject.io_add_watch(source, gobject.IO_IN, self.results_callback)
1095
1100
terminate_queue = Queue()
1096
1101
run_event = Event()
1097
1102
run_event.set()
1099
return self._initiate_task(task, task_results_conn, task_process_conn,
1104
return self._initiate_task(task, task_results_conn, task_process_conn,
1100
1105
terminate_queue, run_event)
1102
1107
def _setup_pipe(self):
1103
1108
return Pipe(duplex=False)
1105
1110
def _initiate_task(self, task, task_process_conn, terminate_queue, run_event):
1106
1111
logger.error("Implement child class method!")
1109
1114
def processes(self):
1110
1115
for i in range(len(self._processes)):
1111
1116
yield self._processes[i]
1113
1118
def start(self):
1114
1119
self.paused = False
1115
1120
for scan in self.processes():
1116
1121
run_event = scan[2]
1117
1122
if not run_event.is_set():
1118
1123
run_event.set()
1120
1125
def pause(self):
1121
1126
self.paused = True
1122
1127
for scan in self.processes():
1123
1128
run_event = scan[2]
1124
1129
if run_event.is_set():
1125
1130
run_event.clear()
1127
1132
def _terminate_process(self, p):
1128
1133
self._send_termination_msg(p)
1129
1134
# The process might be paused: let it run
1130
1135
run_event = p[2]
1131
1136
if not run_event.is_set():
1132
1137
run_event.set()
1134
1139
def _send_termination_msg(self, p):
1137
1142
def terminate_process(self, process_id):
1139
1144
Send a signal to process with matching process_id that it should
1190
1195
class ScanManager(TaskManager):
1192
def __init__(self, results_callback, batch_size,
1197
def __init__(self, results_callback, batch_size,
1193
1198
add_device_function):
1194
1199
TaskManager.__init__(self, results_callback, batch_size)
1195
1200
self.add_device_function = add_device_function
1197
def _initiate_task(self, task, task_results_conn, task_process_conn,
1202
def _initiate_task(self, task, task_results_conn, task_process_conn,
1198
1203
terminate_queue, run_event):
1200
1205
device = task[0]
1201
1206
ignored_paths = task[1]
1202
1207
use_re_ignored_paths = task[2]
1204
1209
scan = scan_process.Scan(device.get_path(),
1206
1211
use_re_ignored_paths,
1208
1213
task_process_conn, terminate_queue, run_event)
1210
1215
self._processes.append((scan, terminate_queue, run_event))
1211
self.add_device_function(scan.pid, device,
1216
self.add_device_function(scan.pid, device,
1212
1217
# This refers to when a device like a hard drive is having its contents scanned,
1213
# looking for photos or videos. It is visible initially in the progress bar for each device
1218
# looking for photos or videos. It is visible initially in the progress bar for each device
1214
1219
# (which normally holds "x photos and videos").
1215
1220
# It maybe displayed only briefly if the contents of the device being scanned is small.
1216
1221
progress_bar_text=_('scanning...'))
1218
1223
return scan.pid
1220
1225
class CopyFilesManager(TaskManager):
1222
def _initiate_task(self, task, task_results_conn,
1227
def _initiate_task(self, task, task_results_conn,
1223
1228
task_process_conn, terminate_queue, run_event):
1224
1229
photo_download_folder = task[0]
1225
1230
video_download_folder = task[1]
1227
1232
files = task[3]
1228
1233
modify_files_during_download = task[4]
1229
1234
modify_pipe = task[5]
1231
1236
copy_files = copyfiles.CopyFiles(photo_download_folder,
1232
1237
video_download_folder,
1234
1239
modify_files_during_download,
1236
scan_pid, self.batch_size,
1241
scan_pid, self.batch_size,
1237
1242
task_process_conn, terminate_queue, run_event)
1238
1243
copy_files.start()
1239
1244
self._processes.append((copy_files, terminate_queue, run_event))
1240
1245
return copy_files.pid
1242
1247
class ThumbnailManager(TaskManager):
1243
1248
def _initiate_task(self, task, task_results_conn,
1244
1249
task_process_conn, terminate_queue, run_event):
1245
1250
scan_pid = task[0]
1246
1251
files = task[1]
1247
generator = tn.GenerateThumbnails(scan_pid, files, self.batch_size,
1248
task_process_conn, terminate_queue,
1252
generator = tn.GenerateThumbnails(scan_pid, files, self.batch_size,
1253
task_process_conn, terminate_queue,
1250
1255
generator.start()
1251
1256
self._processes.append((generator, terminate_queue, run_event))
1256
1261
Duplex, multiprocess, similar to BackupFilesManager
1258
1263
def __init__(self, results_callback):
1259
TaskManager.__init__(self, results_callback=results_callback,
1264
TaskManager.__init__(self, results_callback=results_callback,
1261
1266
self.file_modify_by_scan_pid = {}
1263
def _initiate_task(self, task, task_results_conn, task_process_conn,
1268
def _initiate_task(self, task, task_results_conn, task_process_conn,
1264
1269
terminate_queue, run_event):
1265
1270
scan_pid = task[0]
1266
1271
auto_rotate_jpeg = task[1]
1267
1272
focal_length = task[2]
1269
1274
file_modify = filemodify.FileModify(auto_rotate_jpeg, focal_length,
1270
task_process_conn, terminate_queue,
1275
task_process_conn, terminate_queue,
1272
1277
file_modify.start()
1273
self._processes.append((file_modify, terminate_queue, run_event,
1278
self._processes.append((file_modify, terminate_queue, run_event,
1274
1279
task_results_conn))
1276
1281
self.file_modify_by_scan_pid[scan_pid] = (task_results_conn, file_modify.pid)
1278
1283
return file_modify.pid
1280
1285
def _setup_pipe(self):
1281
1286
return Pipe(duplex=True)
1283
1288
def _send_termination_msg(self, p):
1285
1290
p[3].send((None, None))
1287
1292
def get_modify_pipe(self, scan_pid):
1288
1293
return self.file_modify_by_scan_pid[scan_pid][0]
1291
1296
class BackupFilesManager(TaskManager):
1293
1298
Handles backup processes. This is a little different from some other Task
1294
1299
Manager classes in that its pipe is Duplex, and the work done by it
1295
1300
is not pre-assigned when the process is started.
1297
1302
Duplex, multiprocess.
1299
1304
def __init__(self, results_callback, batch_size):
1303
1308
def _setup_pipe(self):
1304
1309
return Pipe(duplex=True)
1306
1311
def _send_termination_msg(self, p):
1308
p[3].send((None, None, None, None))
1310
def _initiate_task(self, task, task_results_conn, task_process_conn,
1313
p[3].send((None, None, None, None, None))
1315
def _initiate_task(self, task, task_results_conn, task_process_conn,
1311
1316
terminate_queue, run_event):
1314
1319
backup_type = task[2]
1315
backup_files = backupfile.BackupFiles(path, name, self.batch_size,
1316
task_process_conn, terminate_queue,
1320
backup_files = backupfile.BackupFiles(path, name, self.batch_size,
1321
task_process_conn, terminate_queue,
1318
1323
backup_files.start()
1319
self._processes.append((backup_files, terminate_queue, run_event,
1324
self._processes.append((backup_files, terminate_queue, run_event,
1320
1325
task_results_conn))
1322
1327
self.backup_devices_by_path[path] = (task_results_conn, backup_files.pid,
1325
1330
return backup_files.pid
1327
def backup_file(self, move_succeeded, rpd_file, path_suffix,
1328
backup_duplicate_overwrite):
1332
def backup_file(self, move_succeeded, rpd_file, path_suffix,
1333
backup_duplicate_overwrite,
1330
1336
if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO:
1331
1337
logger.debug("Backing up photo %s", rpd_file.download_name)
1335
1341
for path in self.backup_devices_by_path:
1336
1342
backup_type = self.backup_devices_by_path[path][2]
1337
if ((backup_type == PHOTO_VIDEO_BACKUP) or
1343
if ((backup_type == PHOTO_VIDEO_BACKUP) or
1338
1344
(rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO and backup_type == PHOTO_BACKUP) or
1339
1345
(rpd_file.file_type == rpdfile.FILE_TYPE_VIDEO and backup_type == VIDEO_BACKUP)):
1340
1346
logger.debug("Backing up to %s", path)
1341
1347
task_results_conn = self.backup_devices_by_path[path][0]
1342
task_results_conn.send((move_succeeded, rpd_file, path_suffix,
1343
backup_duplicate_overwrite))
1348
task_results_conn.send((move_succeeded, rpd_file, path_suffix,
1349
backup_duplicate_overwrite, download_count))
1345
1351
logger.debug("Not backing up to %s", path)
1347
1353
def add_device(self, path, name, backup_type):
1349
1355
Convenience function to setup adding a backup device
1351
1357
return self.add_task((path, name, backup_type))
1353
1359
def remove_device(self, path):
1354
1360
pid = self.backup_devices_by_path[path][1]
1355
1361
self.terminate_process(pid)
1356
1362
del self.backup_devices_by_path[path]
1359
1365
class SingleInstanceTaskManager:
1361
1367
Base class to manage single instance processes. Examples are daemon
1362
1368
processes, but also a non-daemon process that has one simple task.
1364
1370
Core (infrastructure) functionality is implemented in this class.
1365
1371
Derived classes should implemented functionality to actually implement
1366
1372
specific tasks.
1368
def __init__(self, results_callback):
1374
def __init__(self, results_callback):
1369
1375
self.results_callback = results_callback
1371
1377
self.task_results_conn, self.task_process_conn = Pipe(duplex=True)
1373
1379
source = self.task_results_conn.fileno()
1374
1380
gobject.io_add_watch(source, gobject.IO_IN, self.task_results)
1377
1383
class PreviewManager(SingleInstanceTaskManager):
1378
1384
def __init__(self, results_callback):
1379
1385
SingleInstanceTaskManager.__init__(self, results_callback)
1380
1386
self._get_preview = tn.GetPreviewImage(self.task_process_conn)
1381
1387
self._get_preview.start()
1383
1389
def get_preview(self, unique_id, full_file_name, thm_file_name, file_type, size_max):
1384
1390
self.task_results_conn.send((unique_id, full_file_name, thm_file_name, file_type, size_max))
1386
1392
def task_results(self, source, condition):
1387
1393
unique_id, preview_full_size, preview_small = self.task_results_conn.recv()
1388
1394
self.results_callback(unique_id, preview_full_size, preview_small)
1391
1397
class SubfolderFileManager(SingleInstanceTaskManager):
1393
1399
Manages the daemon process that renames files and creates subfolders
1395
1401
def __init__(self, results_callback, sequence_values):
1396
1402
SingleInstanceTaskManager.__init__(self, results_callback)
1397
self._subfolder_file = subfolderfile.SubfolderFile(self.task_process_conn,
1403
self._subfolder_file = subfolderfile.SubfolderFile(self.task_process_conn,
1398
1404
sequence_values)
1399
1405
self._subfolder_file.start()
1400
1406
logger.debug("SubfolderFile PID: %s", self._subfolder_file.pid)
1402
def rename_file_and_move_to_subfolder(self, download_succeeded,
1408
def rename_file_and_move_to_subfolder(self, download_succeeded,
1403
1409
download_count, rpd_file):
1405
self.task_results_conn.send((download_succeeded, download_count,
1411
logger.debug("Sending file for rename: %s.", download_count)
1412
self.task_results_conn.send((download_succeeded, download_count,
1407
logger.debug("Download count: %s.", download_count)
1410
1416
def task_results(self, source, condition):
1411
move_succeeded, rpd_file = self.task_results_conn.recv()
1412
self.results_callback(move_succeeded, rpd_file)
1417
move_succeeded, rpd_file, download_count = self.task_results_conn.recv()
1418
self.results_callback(move_succeeded, rpd_file, download_count)
1415
1421
class ResizblePilImage(gtk.DrawingArea):
1416
1422
def __init__(self, bg_color=None):
1417
1423
gtk.DrawingArea.__init__(self)
1418
1424
self.base_image = None
1419
1425
self.bg_color = bg_color
1420
1426
self.connect('expose_event', self.expose)
1422
1428
def set_image(self, image):
1423
1429
self.base_image = image
1425
1431
#set up sizes and ratio used for drawing the derived image
1426
1432
self.base_image_w = self.base_image.size[0]
1427
1433
self.base_image_h = self.base_image.size[1]
1428
1434
self.base_image_aspect = float(self.base_image_w) / self.base_image_h
1430
1436
self.queue_draw()
1432
1438
def expose(self, widget, event):
1434
1440
cairo_context = self.window.cairo_create()
1438
1444
w = event.area.width
1439
1445
h = event.area.height
1441
#constrain operations to event area
1447
#constrain operations to event area
1442
1448
cairo_context.rectangle(x, y, w, h)
1443
1449
cairo_context.clip_preserve()
1445
1451
#set background color, if needed
1446
1452
if self.bg_color:
1447
1453
cairo_context.set_source_rgb(*self.bg_color)
1448
cairo_context.fill_preserve()
1454
cairo_context.fill_preserve()
1450
1456
if not self.base_image:
1453
1459
frame_aspect = float(w) / h
1455
1461
if frame_aspect > self.base_image_aspect:
1456
1462
# Frame is wider than image
1473
1479
#image width and height
1474
1480
image_w = pil_image.size[0]
1475
1481
image_h = pil_image.size[1]
1477
1483
#center the image horizontally and vertically
1478
1484
#top left and right corners for the image:
1479
1485
image_x = x + ((w - image_w) / 2)
1480
1486
image_y = y + ((h - image_h) / 2)
1482
1488
image = create_cairo_image_surface(pil_image, image_w, image_h)
1483
1489
cairo_context.set_source_surface(image, image_x, image_y)
1484
cairo_context.paint()
1490
cairo_context.paint()
1490
1496
class PreviewImage:
1492
1498
def __init__(self, parent_app, builder):
1493
1499
#set background color to equivalent of '#444444
1494
self.preview_image = ResizblePilImage(bg_color=(0.267, 0.267, 0.267))
1500
self.preview_image = ResizblePilImage(bg_color=(0.267, 0.267, 0.267))
1495
1501
self.preview_image_eventbox = builder.get_object("preview_eventbox")
1496
1502
self.preview_image_eventbox.add(self.preview_image)
1497
1503
self.preview_image.show()
1498
1504
self.download_this_checkbutton = builder.get_object("download_this_checkbutton")
1499
1505
self.rapid_app = parent_app
1501
1507
self.base_preview_image = None # large size image used to scale down from
1502
1508
self.current_preview_size = (0,0)
1503
1509
self.preview_image_size_limit = (0,0)
1505
1511
self.unique_id = None
1507
def set_preview_image(self, unique_id, pil_image, include_checkbutton_visible=None,
1513
def set_preview_image(self, unique_id, pil_image, include_checkbutton_visible=None,
1517
1523
if include_checkbutton_visible is not None:
1518
1524
self.download_this_checkbutton.props.visible = include_checkbutton_visible
1520
1526
def update_preview_image(self, unique_id, pil_image):
1521
1527
if unique_id == self.unique_id:
1522
1528
self.set_preview_image(unique_id, pil_image)
1525
1531
class RapidApp(dbus.service.Object):
1527
1533
The main Rapid Photo Downloader application class.
1529
1535
Contains functionality for main program window, and directs all other
1533
1539
def __init__(self, bus, path, name, taskserver=None, focal_length=None,
1534
auto_detect=None, device_location=None):
1540
auto_detect=None, device_location=None):
1536
1542
dbus.service.Object.__init__ (self, bus, path, name)
1537
1543
self.running = False
1539
1545
self.taskserver = taskserver
1541
1547
self.focal_length = focal_length
1543
1549
# Setup program preferences, and set callback for when they change
1544
1550
self._init_prefs(auto_detect, device_location)
1546
1552
# Initialize widgets in the main window, and variables that point to them
1547
1553
self._init_widgets()
1548
self._init_pynotify()
1556
self._init_pynotify()
1550
1558
# Initialize job code handling
1551
1559
self._init_job_code()
1553
1561
# Remember the window size from the last time the program was run, or
1554
1562
# set a default size
1555
1563
self._set_window_size()
1557
1565
# Setup various widgets
1558
1566
self._setup_buttons()
1559
1567
self._setup_error_icons()
1560
1568
self._setup_icons()
1562
1570
# Show the main window
1563
1571
self.rapidapp.show()
1565
1573
# Check program preferences - don't allow auto start if there is a problem
1566
1574
prefs_valid, msg = prefsrapid.check_prefs_for_validity(self.prefs)
1567
1575
if not prefs_valid:
1568
1576
self.notify_prefs_are_invalid(details=msg)
1570
1578
# Initialize variables with which to track important downloads results
1571
1579
self._init_download_tracking()
1573
1581
# Set up process managers.
1574
1582
# A task such as scanning a device or copying files is handled in its
1576
1584
self._start_process_managers()
1578
1586
# Setup devices from which to download from and backup to
1579
self.setup_devices(on_startup=True, on_preference_change=False,
1587
self.setup_devices(on_startup=True, on_preference_change=False,
1580
1588
block_auto_start=not prefs_valid)
1582
1590
# Ensure the device collection scrolled window is not too small
1583
1591
self._set_device_collection_size()
1585
1593
def on_rapidapp_destroy(self, widget, data=None):
1587
1595
self._terminate_processes(terminate_file_copies = True)
1592
1600
x, y, width, height = self.rapidapp.get_allocation()
1593
1601
self.prefs.main_window_size_x = width
1594
1602
self.prefs.main_window_size_y = height
1596
1604
self.prefs.set_downloads_today_from_tracker(self.downloads_today_tracker)
1598
1606
gtk.main_quit()
1600
1608
def _terminate_processes(self, terminate_file_copies=False):
1602
1610
if terminate_file_copies:
1603
1611
logger.info("Terminating all processes...")
1605
scan_termination_requested = self.scan_manager.request_termination()
1613
scan_termination_requested = self.scan_manager.request_termination()
1606
1614
thumbnails_termination_requested = self.thumbnails.thumbnail_manager.request_termination()
1607
1615
backup_termination_requested = self.backup_manager.request_termination()
1608
1616
file_modify_termination_requested = self.file_modify_manager.request_termination()
1610
1618
if terminate_file_copies:
1611
1619
copy_files_termination_requested = self.copy_files_manager.request_termination()
1613
1621
copy_files_termination_requested = False
1615
1623
if (scan_termination_requested or thumbnails_termination_requested or
1616
1624
backup_termination_requested or file_modify_termination_requested):
1618
if (self.scan_manager.get_no_active_processes() > 0 or
1626
if (self.scan_manager.get_no_active_processes() > 0 or
1619
1627
self.thumbnails.thumbnail_manager.get_no_active_processes() > 0 or
1620
1628
self.backup_manager.get_no_active_processes() > 0 or
1621
1629
self.file_modify_manager.get_no_active_processes() > 0):
1642
1650
value = checkbutton.get_active()
1643
1651
self.thumbnails.set_selected(self.preview_image.unique_id, value)
1644
1652
self.set_download_action_sensitivity()
1646
1654
def on_preview_eventbox_button_press_event(self, widget, event):
1648
1656
if event.type == gtk.gdk._2BUTTON_PRESS and event.button == 1:
1649
self.show_thumbnails()
1657
self.show_thumbnails()
1651
1659
def on_show_thumbnails_action_activate(self, action):
1652
1660
logger.debug("on_show_thumbnails_action_activate")
1653
1661
self.show_thumbnails()
1655
1663
def on_show_image_action_activate(self, action):
1656
1664
logger.debug("on_show_image_action_activate")
1657
1665
self.thumbnails.show_preview()
1659
1667
def on_check_all_action_activate(self, action):
1660
1668
self.thumbnails.check_all(check_all=True)
1662
1670
def on_uncheck_all_action_activate(self, action):
1663
1671
self.thumbnails.check_all(check_all=False)
1665
1673
def on_check_all_photos_action_activate(self, action):
1666
self.thumbnails.check_all(check_all=True,
1674
self.thumbnails.check_all(check_all=True,
1667
1675
file_type=rpdfile.FILE_TYPE_PHOTO)
1669
1677
def on_check_all_videos_action_activate(self, action):
1670
self.thumbnails.check_all(check_all=True,
1678
self.thumbnails.check_all(check_all=True,
1671
1679
file_type=rpdfile.FILE_TYPE_VIDEO)
1673
1681
def on_quit_action_activate(self, action):
1674
1682
self.on_rapidapp_destroy(widget=self.rapidapp, data=None)
1676
1684
def on_refresh_action_activate(self, action):
1677
1685
self.thumbnails.clear_all()
1678
1686
self.setup_devices(on_startup=False, on_preference_change=False,
1679
1687
block_auto_start=True)
1681
1689
def on_get_help_action_activate(self, action):
1682
1690
webbrowser.open("http://www.damonlynch.net/rapid/help.html")
1684
1692
def on_about_action_activate(self, action):
1685
1693
self.about.set_property("name", PROGRAM_NAME)
1686
1694
self.about.set_property("version", utilities.human_readable_version(
1688
1696
self.about.run()
1689
1697
self.about.hide()
1691
1699
def on_report_problem_action_activate(self, action):
1692
1700
webbrowser.open("https://bugs.launchpad.net/rapid")
1694
1702
def on_translate_action_activate(self, action):
1695
1703
webbrowser.open("http://www.damonlynch.net/rapid/translate.html")
1697
1705
def on_donate_action_activate(self, action):
1698
1706
webbrowser.open("http://www.damonlynch.net/rapid/donate.html")
1700
1708
def show_preview_image(self, unique_id, image, include_checkbutton_visible, checked):
1701
1709
if self.main_notebook.get_current_page() == 0: # thumbnails
1702
1710
logger.debug("Switching to preview image display")
1704
1712
self.preview_image.set_preview_image(unique_id, image, include_checkbutton_visible, checked)
1705
1713
self.next_image_action.set_sensitive(True)
1706
1714
self.prev_image_action.set_sensitive(True)
1708
1716
def update_preview_image(self, unique_id, image):
1709
1717
self.preview_image.update_preview_image(unique_id, image)
1711
1719
def show_thumbnails(self):
1712
1720
logger.debug("Switching to thumbnails display")
1713
1721
self.main_notebook.set_current_page(0)
1714
1722
self.thumbnails.select_image(self.preview_image.unique_id)
1715
1723
self.next_image_action.set_sensitive(False)
1716
1724
self.prev_image_action.set_sensitive(False)
1719
1727
def on_next_image_action_activate(self, action):
1720
1728
if self.preview_image.unique_id is not None:
1721
1729
self.thumbnails.show_next_image(self.preview_image.unique_id)
1723
1731
def on_prev_image_action_activate(self, action):
1724
if self.preview_image.unique_id is not None:
1732
if self.preview_image.unique_id is not None:
1725
1733
self.thumbnails.show_prev_image(self.preview_image.unique_id)
1727
1735
def display_scan_thumbnails(self):
1729
1737
If all the scans are complete, sets the sort order and displays
1738
1746
# Volume management
1741
1749
def start_volume_monitor(self):
1742
1750
if not self.vmonitor:
1743
1751
self.vmonitor = gio.volume_monitor_get()
1744
1752
self.vmonitor.connect("mount-added", self.on_mount_added)
1745
self.vmonitor.connect("mount-removed", self.on_mount_removed)
1753
self.vmonitor.connect("mount-removed", self.on_mount_removed)
1748
1756
def _backup_device_name(self, path):
1749
1757
if self.backup_devices[path][0] is None:
1752
1760
name = self.backup_devices[path][0].get_name()
1755
1763
def start_device_scan(self, device):
1757
Commences the scanning of a device using the preference values for
1765
Commences the scanning of a device using the preference values for
1758
1766
any paths to ignore while scanning
1760
return self.scan_manager.add_task([device,
1768
logger.debug("Starting a device scan for device %s", device.get_name())
1769
return self.scan_manager.add_task([device,
1761
1770
self.prefs.ignored_paths,
1762
1771
self.prefs.use_re_ignored_paths])
1764
1773
def confirm_manual_location(self):
1766
Queries the user to ask if they really want to download from locations
1775
Queries the user to ask if they really want to download from locations
1767
1776
that could take a very long time to scan. They can choose yes or no.
1769
1778
Returns True if yes or there was no need to ask the user, False if the
1781
1790
question="<b>" + _("Downloading from %(location)s.") % {'location': l} + "</b>\n\n" +
1782
1791
_("Do you really want to download from here? On some systems, scanning this location can take a very long time."),
1783
1792
default_to_yes=False,
1785
1794
response = c.run()
1786
1795
user_confirmed = response == gtk.RESPONSE_OK
1788
1797
if not user_confirmed:
1792
1801
def setup_devices(self, on_startup, on_preference_change, block_auto_start):
1795
1804
Setup devices from which to download from and backup to
1797
1806
Sets up volumes for downloading from and backing up to
1799
on_startup should be True if the program is still starting,
1808
on_startup should be True if the program is still starting,
1800
1809
i.e. this is being called from the program's initialization.
1802
1811
on_preference_change should be True if this is being called as the
1803
1812
result of a preference being changed
1805
1814
block_auto_start should be True if automation options to automatically
1806
1815
start a download should be ignored
1808
Removes any image media that are currently not downloaded,
1809
or finished downloading
1817
Removes any image media that are currently not downloaded,
1818
or finished downloading
1812
1821
if self.using_volume_monitor():
1813
1822
self.start_volume_monitor()
1858
1867
if not self.prefs.backup_device_autodetection:
1859
1868
self._setup_manual_backup()
1860
1869
self._add_backup_devices()
1862
1871
self.update_no_backup_devices()
1864
1873
# Display amount of free space in a status bar message
1865
1874
self.display_free_space()
1867
1876
if block_auto_start:
1868
1877
self.auto_start_is_on = False
1870
1879
self.auto_start_is_on = ((not on_preference_change) and
1871
((self.prefs.auto_download_at_startup and
1880
((self.prefs.auto_download_at_startup and
1873
1882
(self.prefs.auto_download_upon_device_insertion and
1874
1883
not on_startup)))
1885
logger.debug("Working with %s devices", len(mounts))
1876
1886
for m in mounts:
1877
1887
path, mount = m
1878
1888
device = dv.Device(path=path, mount=mount)
1879
if (self.search_for_PSD() and
1880
path not in self.prefs.device_whitelist):
1881
# prompt user to see if device should be used or not
1882
self.get_use_device(device)
1884
scan_pid = self.start_device_scan(device)
1885
if mount is not None:
1886
self.mounts_by_path[path] = scan_pid
1891
if not self._device_already_detected(device):
1892
if (self.search_for_PSD() and
1893
path not in self.prefs.device_whitelist):
1894
# prompt user to see if device should be used or not
1895
self.get_use_device(device)
1897
scan_pid = self.start_device_scan(device)
1898
if mount is not None:
1899
self.mounts_by_path[path] = scan_pid
1888
1901
self.set_download_action_sensitivity()
1903
def _device_already_detected(self, device):
1904
path = device.get_path()
1905
if path in self.mounts_by_path:
1906
logger.debug("Ignoring device %s as already have path %s", device.get_name(), path)
1890
1911
def _setup_manual_backup(self):
1892
1913
Setup backup devices that the user has manually specified.
1917
1938
name = self._backup_device_name(path)
1918
1939
backup_type = self.backup_devices[path][1]
1919
1940
self.backup_manager.add_device(path, name, backup_type)
1922
def get_use_device(self, device):
1943
def get_use_device(self, device):
1923
1944
""" Prompt user whether or not to download from this device """
1925
1946
logger.info("Prompting whether to use %s", device.get_name())
1948
# On some systems, e.g. Ubuntu 12.10, the GTK/Gnome environment
1949
# unexpectedly results in a device being added twice and not once.
1950
# The hack on the next line ensures the user is not prompted twice
1951
# for the same device.
1953
self.mounts_by_path[device.get_path()] = "PROMPTING"
1926
1955
d = dv.UseDeviceDialog(self.rapidapp, device, self.got_use_device)
1928
1957
def got_use_device(self, dialog, user_selected, permanent_choice, device):
1929
1958
""" User has chosen whether or not to use a device to download from """
1930
1959
dialog.destroy()
1932
1961
path = device.get_path()
1934
1963
if user_selected:
1935
1964
if permanent_choice and path not in self.prefs.device_whitelist:
1936
1965
# do NOT do a list append operation here without the assignment,
1941
1970
self.prefs.device_whitelist = [path]
1942
1971
scan_pid = self.start_device_scan(device)
1943
1972
self.mounts_by_path[path] = scan_pid
1945
1974
elif permanent_choice and path not in self.prefs.device_blacklist:
1946
1975
# do not do a list append operation here without the assignment, or the preferences will not be updated!
1947
1976
if len(self.prefs.device_blacklist):
1948
1977
self.prefs.device_blacklist = self.prefs.device_blacklist + [path]
1950
self.prefs.device_blacklist = [path]
1979
self.prefs.device_blacklist = [path]
1952
1981
def search_for_PSD(self):
1954
Check to see if user preferences are to automatically search for
1983
Check to see if user preferences are to automatically search for
1955
1984
Portable Storage Devices or not
1957
1986
return self.prefs.device_autodetection_psd and self.prefs.device_autodetection
1959
1988
def check_if_backup_mount(self, path):
1961
Checks to see if backups are enabled and path represents a valid backup
1990
Checks to see if backups are enabled and path represents a valid backup
1962
1991
location. It must be writeable.
1964
1993
Checks against user preferences.
1966
1995
Returns a tuple:
1967
1996
(True, <backup-type> (one of PHOTO_VIDEO_BACKUP, PHOTO_BACKUP, or VIDEO_BACKUP)) or
1970
1999
if self.prefs.backup_images:
1971
2000
if self.prefs.backup_device_autodetection:
1972
# Determine if the auto-detected backup device is
2001
# Determine if the auto-detected backup device is
1973
2002
# to be used to backup only photos, or videos, or both.
1974
# Use the presence of a corresponding directory to
2003
# Use the presence of a corresponding directory to
1975
2004
# determine this.
1976
2005
# The directory must be writable.
1977
2006
photo_path = os.path.join(path, self.prefs.backup_identifier)
2087
2116
self.update_no_backup_devices()
2088
2117
self.display_free_space()
2090
elif self.prefs.device_autodetection and (dv.is_DCIM_device(path) or
2119
elif self.prefs.device_autodetection and (dv.is_DCIM_device(path) or
2091
2120
self.search_for_PSD()):
2093
2122
self.auto_start_is_on = self.prefs.auto_download_upon_device_insertion
2094
2123
device = dv.Device(path=path, mount=mount)
2095
if self.search_for_PSD() and path not in self.prefs.device_whitelist:
2096
# prompt user if device should be used or not
2097
self.get_use_device(device)
2099
scan_pid = self.start_device_scan(device)
2100
self.mounts_by_path[path] = scan_pid
2125
if not self._device_already_detected(device):
2126
if self.search_for_PSD() and path not in self.prefs.device_whitelist:
2127
# prompt user if device should be used or not
2128
self.get_use_device(device)
2130
scan_pid = self.start_device_scan(device)
2131
self.mounts_by_path[path] = scan_pid
2102
2133
def on_mount_removed(self, vmonitor, mount):
2104
2135
callback run when gio indicates a new volume
2105
2136
has been mounted
2108
2139
path = mount.get_root().get_path()
2110
2141
# three scenarios -
2111
2142
# the mount has been scanned but downloading has not yet started
2112
2143
# files are being downloaded from mount (it must be a messy unmount)
2113
2144
# files have finished downloading from mount
2115
2146
if path in self.mounts_by_path:
2116
2147
scan_pid = self.mounts_by_path[path]
2117
2148
del self.mounts_by_path[path]
2118
2149
# temp directory should be cleaned by finishing of process
2120
self.thumbnails.clear_all(scan_pid = scan_pid,
2151
self.thumbnails.clear_all(scan_pid = scan_pid,
2121
2152
keep_downloaded_files = True)
2122
2153
self.device_collection.remove_device(scan_pid)
2126
2157
# remove backup volumes
2127
2158
elif path in self.backup_devices:
2128
2159
del self.backup_devices[path]
2129
2160
self.display_free_space()
2130
2161
self.backup_manager.remove_device(path)
2131
2162
self.update_no_backup_devices()
2133
2164
# may need to disable download button and menu
2134
2165
self.set_download_action_sensitivity()
2136
2167
def clear_non_running_downloads(self):
2138
2169
Clears the display of downloads that are currently not running
2141
2172
# Stop any processes currently scanning or creating thumbnails
2142
2173
self._terminate_processes(terminate_file_copies=False)
2144
2175
# Remove them from the user interface
2145
2176
for scan_pid in self.device_collection.get_all_displayed_processes():
2146
2177
if scan_pid not in self.download_active_by_scan_pid:
2147
2178
self.device_collection.remove_device(scan_pid)
2148
2179
self.thumbnails.clear_all(scan_pid=scan_pid)
2154
2185
# Download and help buttons, and menu items
2157
2188
def on_download_action_activate(self, action):
2159
2190
Called when a download is activated
2162
2193
if self.copy_files_manager.paused:
2163
2194
logger.debug("Download resumed")
2164
2195
self.resume_download()
2166
2197
logger.debug("Download activated")
2168
2199
if self.download_action_is_download:
2169
2200
if self.need_job_code_for_naming and not self.prompting_for_job_code:
2170
2201
self.get_job_code()
2220
2251
def assign_job_code(self, code):
2221
2252
""" assign job code (which may be empty) to member variable and update user preferences
2223
2254
Update preferences only if code is not empty. Do not duplicate job code.
2226
2257
self.job_code = code
2229
2260
#add this value to job codes preferences
2230
2261
#delete any existing value which is the same
2231
2262
#(this way it comes to the front, which is where it should be)
2232
2263
#never modify self.prefs.job_codes in place! (or prefs become screwed up)
2234
2265
jcs = self.prefs.job_codes
2235
2266
while code in jcs:
2236
2267
jcs.remove(code)
2238
2269
self.prefs.job_codes = [code] + jcs
2240
2271
def _get_job_code(self, post_job_code_entry_callback):
2241
2272
""" prompt for a job code """
2243
2274
if not self.prompting_for_job_code:
2244
2275
logger.debug("Prompting for Job Code")
2245
2276
self.prompting_for_job_code = True
2246
2277
j = preferencesdialog.JobCodeDialog(parent_window = self.rapidapp,
2247
2278
job_codes = self.prefs.job_codes,
2248
default_job_code = self.last_chosen_job_code,
2279
default_job_code = self.last_chosen_job_code,
2249
2280
post_job_code_entry_callback=post_job_code_entry_callback,
2250
2281
entry_only = False)
2252
2283
logger.debug("Already prompting for Job Code, do not prompt again")
2254
2285
def get_job_code(self):
2255
2286
self._get_job_code(self.got_job_code)
2257
2288
def got_job_code(self, dialog, user_chose_code, code):
2258
2289
dialog.destroy()
2259
2290
self.prompting_for_job_code = False
2261
2292
if user_chose_code:
2262
2293
if code is None:
2264
2295
self.assign_job_code(code)
2265
2296
self.last_chosen_job_code = code
2266
2297
logger.debug("Job Code %s entered", self.job_code)
2267
self.start_download()
2298
self.start_download()
2270
2301
# user cancelled
2271
2302
logger.debug("No Job Code entered")
2272
2303
self.job_code = ''
2273
2304
self.auto_start_is_on = False
2280
2311
def _init_download_tracking(self):
2282
2313
Initialize variables to track downloads
2319
2350
# set time download is starting if it is not already set
2320
2351
# it is unset when all downloads are completed
2321
2352
if self.download_start_time is None:
2322
self.download_start_time = datetime.datetime.now()
2353
self.download_start_time = datetime.datetime.now()
2324
# Set status to download pending
2355
# Set status to download pending
2325
2356
self.thumbnails.mark_download_pending(files_by_scan_pid)
2327
2358
# disable refresh and preferences change while download is occurring
2328
2359
self.enable_prefs_and_refresh(enabled=False)
2330
2361
for scan_pid in files_by_scan_pid:
2331
2362
files = files_by_scan_pid[scan_pid]
2332
2363
# if generating thumbnails for this scan_pid, stop it
2333
2364
if self.thumbnails.terminate_thumbnail_generation(scan_pid):
2334
2365
self.thumbnails.mark_thumbnails_needed(files)
2336
2367
self.download_files(files, scan_pid)
2338
2369
self.set_download_action_label(is_download = False)
2340
2371
def pause_download(self):
2342
2373
self.copy_files_manager.pause()
2344
2375
# set action to display Download
2345
2376
if not self.download_action_is_download:
2346
2377
self.set_download_action_label(is_download = True)
2348
2379
self.time_check.pause()
2350
2381
def resume_download(self):
2351
2382
for scan_pid in self.download_active_by_scan_pid:
2352
2383
self.time_remaining.set_time_mark(scan_pid)
2354
2385
self.time_check.set_download_mark()
2356
2387
self.copy_files_manager.start()
2358
2389
def download_files(self, files, scan_pid):
2380
2411
video_download_folder = None
2381
2412
no_videos_to_download = 0
2383
2414
photo_download_size, video_download_size = self.size_files_to_be_downloaded(files)
2384
self.download_tracker.init_stats(scan_pid=scan_pid,
2385
photo_size_in_bytes=photo_download_size,
2415
self.download_tracker.init_stats(scan_pid=scan_pid,
2416
photo_size_in_bytes=photo_download_size,
2386
2417
video_size_in_bytes=video_download_size,
2387
2418
no_photos_to_download=no_photos_to_download,
2388
2419
no_videos_to_download=no_videos_to_download)
2391
2422
download_size = photo_download_size + video_download_size
2393
2424
if self.prefs.backup_images:
2394
2425
download_size = download_size + ((self.no_photo_backup_devices * photo_download_size) +
2395
2426
(self.no_video_backup_devices * video_download_size))
2397
2428
self.time_remaining.set(scan_pid, download_size)
2398
2429
self.time_check.set_download_mark()
2400
2431
self.download_active_by_scan_pid.append(scan_pid)
2403
2434
if len(self.download_active_by_scan_pid) > 1:
2404
2435
self.display_summary_notification = True
2406
2437
if self.auto_start_is_on and self.prefs.generate_thumbnails:
2407
2438
for rpd_file in files:
2408
2439
rpd_file.generate_thumbnail = True
2457
2488
self.time_remaining.update(scan_pid, bytes_downloaded=chunk_downloaded)
2458
2489
elif msg_type == rpdmp.MSG_FILE:
2459
2490
self.copy_file_results_single_file(data)
2463
2494
# Process is complete, i.e. conn_type == rpdmp.CONN_COMPLETE
2464
2495
connection.close()
2468
2499
def copy_file_results_single_file(self, data):
2470
2501
Handles results from one of two processes:
2474
2505
Operates after a single file has been copied from the download device
2475
2506
to the local folder.
2477
2508
Calls the process to rename files and create subfolders (subfolderfile)
2480
2511
download_succeeded, rpd_file, download_count, temp_full_file_name, thumbnail_icon, thumbnail = data
2482
2513
if thumbnail is not None or thumbnail_icon is not None:
2483
self.thumbnails.update_thumbnail((rpd_file.unique_id,
2514
self.thumbnails.update_thumbnail((rpd_file.unique_id,
2487
2518
self.download_tracker.set_download_count_for_file(
2488
2519
rpd_file.unique_id, download_count)
2489
2520
self.download_tracker.set_download_count(
2490
2521
rpd_file.scan_pid, download_count)
2491
2522
rpd_file.download_start_time = self.download_start_time
2493
2524
if download_succeeded:
2494
2525
# Insert preference values needed for name generation
2495
2526
rpd_file = prefsrapid.insert_pref_lists(self.prefs, rpd_file)
2521
2552
# Process is complete, i.e. conn_type == rpdmp.CONN_COMPLETE
2522
2553
connection.close()
2526
2557
def download_is_occurring(self):
2527
"""Returns True if a file is currently being downloaded, renamed or
2558
"""Returns True if a file is currently being downloaded, renamed or
2530
2561
return not len(self.download_active_by_scan_pid) == 0
2533
2564
# Create folder and file names for downloaded files
2536
def subfolder_file_results(self, move_succeeded, rpd_file):
2567
def subfolder_file_results(self, move_succeeded, rpd_file, download_count):
2538
2569
Handle results of subfolder creation and file renaming
2541
2572
scan_pid = rpd_file.scan_pid
2542
2573
unique_id = rpd_file.unique_id
2544
2575
if rpd_file.status == config.STATUS_DOWNLOADED_WITH_WARNING:
2545
self.log_error(config.WARNING, rpd_file.error_title,
2576
self.log_error(config.WARNING, rpd_file.error_title,
2546
2577
rpd_file.error_msg, rpd_file.error_extra_detail)
2548
2579
if self.prefs.backup_images and len(self.backup_devices):
2549
2580
if self.prefs.backup_device_autodetection:
2550
2581
if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO:
2553
2584
path_suffix = self.prefs.video_backup_identifier
2555
2586
path_suffix = None
2557
self.backup_manager.backup_file(move_succeeded, rpd_file,
2588
self.backup_manager.backup_file(move_succeeded, rpd_file,
2559
self.prefs.backup_duplicate_overwrite)
2590
self.prefs.backup_duplicate_overwrite,
2561
2593
self.file_download_finished(move_succeeded, rpd_file)
2564
2596
def multiple_backup_devices(self, file_type):
2565
2597
"""Returns true if more than one backup device is being used for that
2568
return ((file_type == rpdfile.FILE_TYPE_PHOTO and
2600
return ((file_type == rpdfile.FILE_TYPE_PHOTO and
2569
2601
self.no_photo_backup_devices > 1) or
2570
(file_type == rpdfile.FILE_TYPE_VIDEO and
2602
(file_type == rpdfile.FILE_TYPE_VIDEO and
2571
2603
self.no_video_backup_devices > 1))
2573
2605
def backup_results(self, source, condition):
2575
2607
Handle results sent from backup processes
2578
2610
conn_type, msg_data = connection.recv()
2579
2611
if conn_type == rpdmp.CONN_PARTIAL:
2580
2612
msg_type, data = msg_data
2582
2614
if msg_type == rpdmp.MSG_BYTES:
2583
2615
scan_pid, backup_pid, total_downloaded, chunk_downloaded = data
2584
self.download_tracker.increment_bytes_backed_up(scan_pid,
2616
self.download_tracker.increment_bytes_backed_up(scan_pid,
2585
2617
chunk_downloaded)
2586
2618
self.time_check.increment(bytes_downloaded=chunk_downloaded)
2587
2619
percent_complete = self.download_tracker.get_percent_complete(scan_pid)
2588
2620
self.device_collection.update_progress(scan_pid, percent_complete,
2590
2622
self.time_remaining.update(scan_pid, bytes_downloaded=chunk_downloaded)
2592
2624
elif msg_type == rpdmp.MSG_FILE:
2593
2625
backup_succeeded, rpd_file = data
2595
2627
# Only show an error message if there is more than one device
2596
2628
# backing up files of this type - if that is the case,
2597
# do not want to reply on showing an error message in the
2629
# do not want to reply on showing an error message in the
2598
2630
# function file_download_finished, as it is only called once,
2599
2631
# when all files have been backed up
2600
2632
if not backup_succeeded and self.multiple_backup_devices(rpd_file.file_type):
2601
self.log_error(config.SERIOUS_ERROR,
2602
rpd_file.error_title,
2633
self.log_error(config.SERIOUS_ERROR,
2634
rpd_file.error_title,
2603
2635
rpd_file.error_msg, rpd_file.error_extra_detail)
2605
2637
self.download_tracker.file_backed_up(rpd_file.unique_id)
2606
2638
if self.download_tracker.all_files_backed_up(rpd_file.unique_id,
2607
2639
rpd_file.file_type):
2619
2651
unique_id = rpd_file.unique_id
2620
2652
# Update error log window if neccessary
2621
2653
if not succeeded and not self.multiple_backup_devices(rpd_file.file_type):
2622
self.log_error(config.SERIOUS_ERROR, rpd_file.error_title,
2654
self.log_error(config.SERIOUS_ERROR, rpd_file.error_title,
2623
2655
rpd_file.error_msg, rpd_file.error_extra_detail)
2624
2656
elif self.prefs.auto_delete:
2625
# record which files to automatically delete when download
2657
# record which files to automatically delete when download
2627
2659
self.download_tracker.add_to_auto_delete(rpd_file)
2629
2661
self.thumbnails.update_status_post_download(rpd_file)
2630
self.download_tracker.file_downloaded_increment(scan_pid,
2662
self.download_tracker.file_downloaded_increment(scan_pid,
2631
2663
rpd_file.file_type,
2632
2664
rpd_file.status)
2634
2666
completed, files_remaining = self._update_file_download_device_progress(scan_pid, unique_id, rpd_file.file_type)
2636
2668
if self.download_is_occurring():
2637
2669
self.update_time_remaining()
2640
2672
# Last file for this scan pid has been downloaded, so clean temp directory
2641
2673
logger.debug("Purging temp directories")
2649
2681
self.notify_downloaded_from_device(scan_pid)
2650
2682
if files_remaining == 0 and self.prefs.auto_unmount:
2651
2683
self.device_collection.unmount(scan_pid)
2654
2686
if not self.download_is_occurring():
2655
2687
logger.debug("Download completed")
2656
2688
self.enable_prefs_and_refresh(enabled=True)
2657
2689
self.notify_download_complete()
2658
2690
self.download_progressbar.set_fraction(0.0)
2660
2692
self.prefs.stored_sequence_no = self.stored_sequence_value.value
2661
2693
self.downloads_today_tracker.set_raw_downloads_today_from_int(self.downloads_today_value.value)
2662
2694
self.downloads_today_tracker.set_raw_downloads_today_date(self.downloads_today_date_value.value)
2663
2695
self.prefs.set_downloads_today_from_tracker(self.downloads_today_tracker)
2665
if ((self.prefs.auto_exit and self.download_tracker.no_errors_or_warnings())
2697
if ((self.prefs.auto_exit and self.download_tracker.no_errors_or_warnings())
2666
2698
or self.prefs.auto_exit_force):
2667
2699
if not self.thumbnails.files_remain_to_download():
2668
2700
self._terminate_processes()
2669
2701
gtk.main_quit()
2671
2703
self.download_tracker.purge_all()
2672
2704
self.speed_label.set_label(" ")
2674
2706
self.display_free_space()
2676
2708
self.set_download_action_label(is_download=True)
2677
2709
self.set_download_action_sensitivity()
2679
2711
self.job_code = ''
2680
2712
self.download_start_time = None
2683
2715
def update_time_remaining(self):
2684
2716
update, download_speed = self.time_check.check_for_update()
2686
2718
self.speed_label.set_text(download_speed)
2688
2720
time_remaining = self.time_remaining.time_remaining()
2689
2721
if time_remaining:
2690
2722
secs = int(time_remaining)
2694
2726
elif secs == 1:
2695
2727
message = _("About 1 second remaining")
2696
2728
elif secs < 60:
2697
message = _("About %i seconds remaining") % secs
2729
message = _("About %i seconds remaining") % secs
2698
2730
elif secs == 60:
2699
2731
message = _("About 1 minute remaining")
2701
# Translators: in the text '%(minutes)i:%(seconds)02i', only the : should be translated, if needed.
2733
# Translators: in the text '%(minutes)i:%(seconds)02i', only the : should be translated, if needed.
2702
2734
# '%(minutes)i' and '%(seconds)02i' should not be modified or left out. They are used to format and display the amount
2703
2735
# of time the download has remainging, e.g. 'About 5:36 minutes remaining'
2704
2736
message = _("About %(minutes)i:%(seconds)02i minutes remaining") % {'minutes': secs / 60, 'seconds': secs % 60}
2706
2738
self.rapid_statusbar.pop(self.statusbar_context_id)
2707
self.rapid_statusbar.push(self.statusbar_context_id, message)
2739
self.rapid_statusbar.push(self.statusbar_context_id, message)
2709
2741
def auto_delete(self, scan_pid):
2710
2742
"""Delete files from download device at completion of download"""
2711
2743
for file in self.download_tracker.get_files_to_auto_delete(scan_pid):
2759
2791
no_files_downloaded = no_photos_downloaded + no_videos_downloaded
2760
2792
no_files_failed = no_photos_failed + no_videos_failed
2761
2793
no_warnings = self.download_tracker.get_no_warnings(scan_pid)
2763
2795
file_types = self.file_types_by_number(no_photos_downloaded, no_videos_downloaded)
2764
2796
file_types_failed = self.file_types_by_number(no_photos_failed, no_videos_failed)
2765
2797
message = _("%(noFiles)s %(filetypes)s downloaded") % \
2766
2798
{'noFiles':no_files_downloaded, 'filetypes': file_types}
2768
2800
if no_files_failed:
2769
2801
message += "\n" + _("%(noFiles)s %(filetypes)s failed to download") % {'noFiles':no_files_failed, 'filetypes':file_types_failed}
2771
2803
if no_warnings:
2772
message = "%s\n%s " % (message, no_warnings) + _("warnings")
2774
n = pynotify.Notification(notification_name, message)
2775
n.set_icon_from_pixbuf(icon)
2804
message = "%s\n%s " % (message, no_warnings) + _("warnings")
2807
n = pynotify.Notification(notification_name, message)
2808
n.set_icon_from_pixbuf(icon)
2779
2812
def notify_download_complete(self):
2780
2813
if self.display_summary_notification:
2781
2814
message = _("All downloads complete")
2783
2816
# photo downloads
2784
2817
photo_downloads = self.download_tracker.total_photos_downloaded
2785
2818
if photo_downloads:
2786
2819
filetype = self.file_types_by_number(photo_downloads, 0)
2787
2820
message += "\n" + _("%(number)s %(numberdownloaded)s") % \
2788
{'number': photo_downloads,
2821
{'number': photo_downloads,
2789
2822
'numberdownloaded': _("%(filetype)s downloaded") % \
2790
2823
{'filetype': filetype}}
2792
2825
# photo failures
2793
2826
photo_failures = self.download_tracker.total_photo_failures
2794
2827
if photo_failures:
2815
2848
{'number': video_failures,
2816
2849
'numberdownloaded': _("%(filetype)s failed to download") % \
2817
2850
{'filetype': filetype}}
2820
warnings = self.download_tracker.total_warnings
2853
warnings = self.download_tracker.total_warnings
2822
2855
message += "\n" + _("%(number)s %(numberdownloaded)s") % \
2823
{'number': warnings,
2856
{'number': warnings,
2824
2857
'numberdownloaded': _("warnings")}
2826
n = pynotify.Notification(PROGRAM_NAME, message)
2827
n.set_icon_from_pixbuf(self.application_icon)
2860
n = pynotify.Notification(PROGRAM_NAME, message)
2861
n.set_icon_from_pixbuf(self.application_icon)
2829
2863
self.display_summary_notification = False # don't show it again unless needed
2832
2866
def _update_file_download_device_progress(self, scan_pid, unique_id, file_type):
2834
2868
Increments the progress bar for an individual device
2836
2870
Returns if the download is completed for that scan_pid
2837
2871
It also returns the number of files remaining for the scan_pid, BUT
2838
2872
this value is valid ONLY if the download is completed
2841
2875
files_downloaded = self.download_tracker.get_download_count_for_file(unique_id)
2842
2876
files_to_download = self.download_tracker.get_no_files_in_download(scan_pid)
2843
2877
file_types = self.download_tracker.get_file_types_present(scan_pid)
2844
2878
completed = files_downloaded == files_to_download
2845
2879
if completed and (self.prefs.backup_images and len(self.backup_devices)):
2846
2880
completed = self.download_tracker.all_files_backed_up(unique_id, file_type)
2849
2883
files_remaining = self.thumbnails.get_no_files_remaining(scan_pid)
2851
2885
files_remaining = 0
2853
2887
if completed and files_remaining:
2854
2888
# e.g.: 3 of 205 photos and videos (202 remaining)
2855
2889
progress_bar_text = _("%(number)s of %(total)s %(filetypes)s (%(remaining)s remaining)") % {
2856
'number': files_downloaded,
2890
'number': files_downloaded,
2857
2891
'total': files_to_download,
2858
2892
'filetypes': file_types,
2859
2893
'remaining': files_remaining}
2861
2895
# e.g.: 205 of 205 photos and videos
2862
2896
progress_bar_text = _("%(number)s of %(total)s %(filetypes)s") % \
2863
{'number': files_downloaded,
2897
{'number': files_downloaded,
2864
2898
'total': files_to_download,
2865
2899
'filetypes': file_types}
2866
2900
percent_complete = self.download_tracker.get_percent_complete(scan_pid)
2867
2901
self.device_collection.update_progress(scan_pid=scan_pid,
2868
2902
percent_complete=percent_complete,
2869
progress_bar_text=progress_bar_text,
2903
progress_bar_text=progress_bar_text,
2870
2904
bytes_downloaded=None)
2872
2906
percent_complete = self.download_tracker.get_overall_percent_complete()
2873
2907
self.download_progressbar.set_fraction(percent_complete)
2875
2909
return (completed, files_remaining)
2878
2912
def _clean_all_temp_dirs(self):
2933
2967
elif device_location:
2934
2968
self.prefs.device_location = device_location
2935
2969
self.prefs.device_autodetection = False
2937
2971
self.prefs.notify_add(self.on_preference_changed)
2939
# flag to indicate whether the user changed some preferences that
2973
# flag to indicate whether the user changed some preferences that
2940
2974
# indicate the image and backup devices should be setup again
2941
2975
self.rerun_setup_available_image_and_video_media = False
2942
2976
self.rerun_setup_available_backup_media = False
2944
# flag to indicate that the preferences dialog window is being
2978
# flag to indicate that the preferences dialog window is being
2945
2979
# displayed to the user
2946
2980
self.preferences_dialog_displayed = False
2948
2982
# flag to indicate that the user has modified the download today
2949
2983
# related values in the preferences dialog window
2950
2984
self.refresh_downloads_today = False
2952
# these values are used to track the number of backup devices /
2986
# these values are used to track the number of backup devices /
2953
2987
# locations for each file type
2954
2988
self.no_photo_backup_devices = 0
2955
2989
self.no_video_backup_devices = 0
2957
2991
self.downloads_today_tracker = self.prefs.get_downloads_today_tracker()
2959
2993
downloads_today = self.downloads_today_tracker.get_and_maybe_reset_downloads_today()
2960
2994
if downloads_today > 0:
2961
2995
logger.info("Downloads that have occurred so far today: %s", downloads_today)
2963
logger.info("No downloads have occurred so far today")
2997
logger.info("No downloads have occurred so far today")
2965
self.downloads_today_value = Value(c_int,
2999
self.downloads_today_value = Value(c_int,
2966
3000
self.downloads_today_tracker.get_raw_downloads_today())
2967
3001
self.downloads_today_date_value = Array(c_char,
2968
3002
self.downloads_today_tracker.get_raw_downloads_today_date())
2969
self.day_start_value = Array(c_char,
3003
self.day_start_value = Array(c_char,
2970
3004
self.downloads_today_tracker.get_raw_day_start())
2971
3005
self.refresh_downloads_today_value = Value(c_bool, False)
2972
3006
self.stored_sequence_value = Value(c_int, self.prefs.stored_sequence_no)
2973
3007
self.uses_stored_sequence_no_value = Value(c_bool, self.prefs.any_pref_uses_stored_sequence_no())
2974
3008
self.uses_session_sequece_no_value = Value(c_bool, self.prefs.any_pref_uses_session_sequece_no())
2975
3009
self.uses_sequence_letter_value = Value(c_bool, self.prefs.any_pref_uses_sequence_letter_value())
2977
3011
self.check_prefs_upgrade(__version__)
2978
3012
self.prefs.program_version = __version__
2980
3014
def _check_for_sequence_value_use(self):
2981
3015
self.uses_stored_sequence_no_value.value = self.prefs.any_pref_uses_stored_sequence_no()
2982
3016
self.uses_session_sequece_no_value.value = self.prefs.any_pref_uses_session_sequece_no()
2983
self.uses_sequence_letter_value.value = self.prefs.any_pref_uses_sequence_letter_value()
3017
self.uses_sequence_letter_value.value = self.prefs.any_pref_uses_sequence_letter_value()
2985
3019
def check_prefs_upgrade(self, running_version):
2987
Checks if the running version of the program is different from the
3021
Checks if the running version of the program is different from the
2988
3022
version recorded in the preferences.
2990
3024
If the version is different, the preferences are checked to see
2991
3025
whether they should be upgraded or not.
2993
3027
previous_version = self.prefs.program_version
2994
3028
if len(previous_version) > 0:
2995
3029
# the program has been run previously for this user
2997
3031
pv = utilities.pythonify_version(previous_version)
2998
3032
rv = utilities.pythonify_version(running_version)
3001
3035
# 0.4.1 and below had only one manual backup location
3002
3036
# 0.4.2 introduced a distinct video back up location that can be manually set
3003
# Therefore must duplicate the previous photo & video manual backup location into the
3037
# Therefore must duplicate the previous photo & video manual backup location into the
3004
3038
# new video field, unless it has already been changed already.
3006
3040
if pv < utilities.pythonify_version('0.4.2'):
3007
3041
if self.prefs.backup_video_location == os.path.expanduser('~'):
3008
3042
self.prefs.backup_video_location = self.prefs.backup_location
3009
logger.info("Migrated manual backup location preference to videos: %s",
3043
logger.info("Migrated manual backup location preference to videos: %s",
3010
3044
self.prefs.backup_video_location)
3012
3046
def on_preference_changed(self, key, value):
3014
3048
Called when user changes the program's preferences
3018
3052
if key == 'show_log_dialog':
3019
3053
self.menu_log_window.set_active(value)
3020
elif key in ['device_autodetection', 'device_autodetection_psd',
3054
elif key in ['device_autodetection', 'device_autodetection_psd',
3021
3055
'device_location', 'ignored_paths',
3022
3056
'use_re_ignored_paths', 'device_blacklist']:
3023
3057
self.rerun_setup_available_image_and_video_media = True
3024
3058
self._set_from_toolbar_state()
3025
3059
if not self.preferences_dialog_displayed:
3026
3060
self.post_preference_change()
3029
elif key in ['backup_images', 'backup_device_autodetection',
3030
'backup_location', 'backup_video_location',
3063
elif key in ['backup_images', 'backup_device_autodetection',
3064
'backup_location', 'backup_video_location',
3031
3065
'backup_identifier', 'video_backup_identifier']:
3032
3066
self.rerun_setup_available_backup_media = True
3033
3067
if not self.preferences_dialog_displayed:
3034
3068
self.post_preference_change()
3036
3070
# Downloads today and stored sequence numbers are kept in shared memory,
3037
3071
# so that the subfolderfile daemon process can access and modify them
3039
3073
# Note, totally ignore any changes in downloads today, as it
3040
3074
# is modified in a special manner via a tracking class
3042
3076
elif key == 'stored_sequence_no':
3043
3077
if type(value) <> types.IntType:
3044
3078
logger.critical("Stored sequence number value is malformed")
3046
3080
self.stored_sequence_value.value = value
3048
3082
elif key in ['image_rename', 'subfolder', 'video_rename', 'video_subfolder']:
3049
3083
self.need_job_code_for_naming = self.prefs.any_pref_uses_job_code()
3050
3084
# Check if stored sequence no is being used
3051
3085
self._check_for_sequence_value_use()
3053
3087
elif key in ['download_folder', 'video_download_folder']:
3054
3088
self._set_to_toolbar_values()
3055
3089
self.display_free_space()
3057
3091
def post_preference_change(self):
3058
3092
if self.rerun_setup_available_image_and_video_media:
3060
3094
logger.info("Download device settings preferences were changed")
3062
3096
self.thumbnails.clear_all()
3063
3097
self.setup_devices(on_startup = False, on_preference_change = True, block_auto_start = True)
3064
3098
self._set_device_collection_size()
3066
3100
if self.main_notebook.get_current_page() == 1: # preview of file
3067
3101
self.main_notebook.set_current_page(0)
3069
3103
self.rerun_setup_available_image_and_video_media = False
3071
3105
if self.rerun_setup_available_backup_media:
3072
3106
if self.using_volume_monitor():
3073
self.start_volume_monitor()
3107
self.start_volume_monitor()
3074
3108
logger.info("Backup preferences were changed.")
3076
3110
self.refresh_backup_media()
3078
3112
self.rerun_setup_available_backup_media = False
3080
3114
if self.refresh_downloads_today:
3081
3115
self.downloads_today_value.value = self.downloads_today_tracker.get_raw_downloads_today()
3082
3116
self.downloads_today_date_value.value = self.downloads_today_tracker.get_raw_downloads_today_date()
3083
3117
self.day_start_value.value = self.downloads_today_tracker.get_raw_day_start()
3084
3118
self.refresh_downloads_today_value.value = True
3085
3119
self.prefs.set_downloads_today_from_tracker(self.downloads_today_tracker)
3090
3124
# Main app window management and setup
3093
3127
def _init_pynotify(self):
3095
3129
Initialize system notification messages
3098
3132
if not pynotify.init("TestCaps"):
3099
3133
logger.warning("There might be problems using pynotify.")
3102
3135
do_not_size_icon = False
3103
self.notification_icon_size = 48
3136
self.notification_icon_size = 48
3105
3138
info = pynotify.get_server_info()
3148
3181
self.speed_label = builder.get_object("speed_label")
3149
3182
self.refresh_action = builder.get_object("refresh_action")
3150
3183
self.preferences_action = builder.get_object("preferences_action")
3152
3185
# Only enable this action when actually displaying a preview
3153
3186
self.next_image_action.set_sensitive(False)
3154
3187
self.prev_image_action.set_sensitive(False)
3156
3189
self._init_toolbars()
3159
3192
builder.add_from_file(paths.share_dir("glade3/about.ui"))
3160
3193
self.about = builder.get_object("about")
3162
3195
builder.connect_signals(self)
3164
3197
self.preview_image = PreviewImage(self, builder)
3166
3199
thumbnails_scrolledwindow = builder.get_object('thumbnails_scrolledwindow')
3167
3200
self.thumbnails = ThumbnailDisplay(self)
3168
thumbnails_scrolledwindow.add(self.thumbnails)
3201
thumbnails_scrolledwindow.add(self.thumbnails)
3170
3203
#collection of devices from which to download
3171
3204
self.device_collection_viewport = builder.get_object("device_collection_viewport")
3172
3205
self.device_collection = DeviceCollection(self)
3173
3206
self.device_collection_viewport.add(self.device_collection)
3175
3208
#error log window
3176
3209
self.error_log = errorlog.ErrorLog(self)
3178
3211
# monitor to handle mounts and dismounts
3179
3212
self.vmonitor = None
3180
3213
# track scan ids for mount paths - very useful when a device is unmounted
3181
3214
self.mounts_by_path = {}
3183
3216
# Download action state
3184
3217
self.download_action_is_download = True
3186
3219
# Track the time a download commences
3187
3220
self.download_start_time = None
3189
3222
# Whether a system wide notifcation message should be shown
3190
3223
# after a download has occurred in parallel
3191
3224
self.display_summary_notification = False
3193
3226
# Values used to display how much longer a download will take
3194
3227
self.time_remaining = downloadtracker.TimeRemaining()
3195
3228
self.time_check = downloadtracker.TimeCheck()
3197
3230
def _init_toolbars(self):
3198
3231
""" Setup the 3 vertical toolbars on the main screen """
3199
3232
self._setup_from_toolbar()
3200
3233
self._setup_copy_move_toolbar()
3201
3234
self._setup_dest_toolbar()
3203
3236
# size label widths so they are equal, or else the left border of the file chooser will not match
3204
3237
self.photo_dest_label.realize()
3205
3238
self._make_widget_widths_equal(self.photo_dest_label, self.video_dest_label)
3206
3239
self.photo_dest_label.set_alignment(xalign=0.0, yalign=0.5)
3207
3240
self.video_dest_label.set_alignment(xalign=0.0, yalign=0.5)
3209
3242
# size copy / move buttons so they are equal in length, so arrows align
3210
3243
self._make_widget_widths_equal(self.copy_button, self.move_button)
3212
3245
def _setup_from_toolbar(self):
3213
3246
self.from_toolbar.set_style(gtk.TOOLBAR_TEXT)
3214
3247
self.from_toolbar.set_border_width(5)
3216
3249
from_label = gtk.Label()
3217
3250
from_label.set_markup("<i>" + _("From") + "</i>")
3218
3251
self.from_toolbar_label = gtk.ToolItem()
3229
3262
_("Select a folder containing %(file_types)s") % {'file_types':file_types_to_download()})
3230
3263
self.from_filechooser_button.set_action(
3231
3264
gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
3233
3266
self.from_filechooser = gtk.ToolItem()
3234
3267
self.from_filechooser.set_is_important(True)
3235
3268
self.from_filechooser.add(self.from_filechooser_button)
3236
3269
self.from_filechooser.set_expand(True)
3237
3270
self.from_toolbar.insert(self.from_filechooser, 2)
3239
3272
self._set_from_toolbar_state()
3241
3274
#set events after having initialized the values
3242
3275
self.auto_detect_button.connect("toggled", self.on_auto_detect_button_toggled_event)
3243
self.from_filechooser_button.connect("selection-changed",
3276
self.from_filechooser_button.connect("selection-changed",
3244
3277
self.on_from_filechooser_button_selection_changed)
3246
3279
self.from_toolbar.show_all()
3248
3281
def _setup_copy_move_toolbar(self):
3249
3282
self.copy_toolbar.set_style(gtk.TOOLBAR_TEXT)
3250
3283
self.copy_toolbar.set_border_width(5)
3252
3285
copy_move_label = gtk.Label(" ")
3253
3286
self.copy_move_toolbar_label = gtk.ToolItem()
3254
3287
self.copy_move_toolbar_label.add(copy_move_label)
3255
3288
self.copy_move_toolbar_label.set_is_important(True)
3256
3289
self.copy_toolbar.insert(self.copy_move_toolbar_label, 0)
3258
3291
self.copy_hbox = gtk.HBox()
3259
3292
self.move_hbox = gtk.HBox()
3260
3293
self.forward_image = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_SMALL_TOOLBAR)
3290
3323
move_box = gtk.ToolItem()
3291
3324
move_box.add(self.move_hbox)
3292
3325
self.copy_toolbar.insert(move_box, 2)
3294
3327
self.move_button.set_active(self.prefs.auto_delete)
3295
self.copy_button.connect("toggled", self.on_copy_button_toggle_event)
3328
self.copy_button.connect("toggled", self.on_copy_button_toggle_event)
3297
3330
self.copy_toolbar.show_all()
3298
self._set_copy_toolbar_active_arrows()
3331
self._set_copy_toolbar_active_arrows()
3300
3333
def _setup_dest_toolbar(self):
3301
3334
#Destination Toolbar
3302
3335
self.dest_toolbar.set_border_width(5)
3304
3337
dest_label = gtk.Label()
3305
3338
dest_label.set_markup("<i>" + _("To") + "</i>")
3306
3339
self.dest_toolbar_label = gtk.ToolItem()
3307
3340
self.dest_toolbar_label.add(dest_label)
3308
3341
self.dest_toolbar_label.set_is_important(True)
3309
3342
self.dest_toolbar.insert(self.dest_toolbar_label, 0)
3311
3344
photo_dest_hbox = gtk.HBox()
3312
3345
self.photo_dest_label = gtk.Label(_("Photos:"))
3314
3347
self.to_photo_filechooser_button = gtk.FileChooserButton(
3315
3348
_("Select a folder to download photos to"))
3316
3349
self.to_photo_filechooser_button.set_action(
3377
3410
def on_copy_button_toggle_event(self, radio_button):
3378
3411
self._set_copy_toolbar_active_arrows()
3379
3412
self.prefs.auto_delete = not self.copy_button.get_active()
3381
3414
def _set_from_toolbar_state(self):
3382
3415
logger.debug("_set_from_toolbar_state")
3383
3416
self.auto_detect_button.set_active(self.prefs.device_autodetection)
3384
3417
if self.prefs.device_autodetection:
3385
3418
self.from_filechooser_button.set_sensitive(False)
3386
3419
self.from_filechooser_button.set_current_folder(self.prefs.device_location)
3388
3421
def on_auto_detect_button_toggled_event(self, button):
3389
3422
logger.debug("on_auto_detect_button_toggled_event")
3390
3423
self.from_filechooser_button.set_sensitive(not button.get_active())
3391
3424
if not self.rerun_setup_available_image_and_video_media:
3392
3425
self.prefs.device_autodetection = button.get_active()
3394
3427
def on_from_filechooser_button_selection_changed(self, filechooserbutton):
3395
3428
logger.debug("on_from_filechooser_button_selection_changed")
3396
3429
path = filechooserbutton.get_current_folder()
3397
3430
if path and not self.rerun_setup_available_image_and_video_media:
3398
3431
self.prefs.device_location = path
3400
3433
def on_to_photo_filechooser_button_selection_changed(self, filechooserbutton):
3401
3434
path = filechooserbutton.get_current_folder()
3403
3436
self.prefs.download_folder = path
3405
3438
def on_to_video_filechooser_button_selection_changed(self, filechooserbutton):
3406
3439
path = filechooserbutton.get_current_folder()
3408
3441
self.prefs.video_download_folder = path
3410
3443
def _set_to_toolbar_values(self):
3411
3444
self.to_photo_filechooser_button.set_current_folder(self.prefs.download_folder)
3412
3445
self.to_video_filechooser_button.set_current_folder(self.prefs.video_download_folder)
3446
3479
# don't allow the media collection to be absolutely empty
3447
3480
self.device_collection_scrolledwindow.set_size_request(-1, 47)
3450
3483
def on_rapidapp_window_state_event(self, widget, event):
3451
3484
""" Records the window maximization state in the preferences."""
3453
3486
if event.changed_mask & gdk.WINDOW_STATE_MAXIMIZED:
3454
3487
self.prefs.main_window_maximized = event.new_window_state & gdk.WINDOW_STATE_MAXIMIZED
3456
3489
def _setup_buttons(self):
3457
3490
thumbnails_button = self.builder.get_object("thumbnails_button")
3458
3491
image = gtk.image_new_from_file(paths.share_dir('glade3/thumbnails_icon.png'))
3459
3492
thumbnails_button.set_image(image)
3461
3494
preview_button = self.builder.get_object("preview_button")
3462
3495
image = gtk.image_new_from_file(paths.share_dir('glade3/photo_icon.png'))
3463
3496
preview_button.set_image(image)
3465
3498
next_image_button = self.builder.get_object("next_image_button")
3466
3499
image = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_BUTTON)
3467
3500
next_image_button.set_image(image)
3469
3502
prev_image_button = self.builder.get_object("prev_image_button")
3470
3503
image = gtk.image_new_from_stock(gtk.STOCK_GO_BACK, gtk.ICON_SIZE_BUTTON)
3471
3504
prev_image_button.set_image(image)
3473
3506
def _setup_icons(self):
3474
3507
icons = ['rapid-photo-downloader-jobcode',]
3475
icon_list = [(icon, paths.share_dir('glade3/%s.svg' % icon)) for icon in icons]
3508
icon_list = [(icon, paths.share_dir('glade3/%s.svg' % icon)) for icon in icons]
3476
3509
register_iconsets(icon_list)
3478
3511
def _setup_error_icons(self):
3480
3513
hide display of warning and error symbols in the taskbar until they
3604
3637
'path':self.prefs.backup_location,
3605
3638
'path2': self.prefs.backup_video_location}
3607
msg2 = self.display_backup_mounts()
3640
msg2 = self.display_backup_mounts()
3610
3643
msg = _("%(freespace)s. %(backuppaths)s.") % {'freespace': msg, 'backuppaths': msg2}
3614
3647
msg = msg.rstrip()
3616
3649
self.statusbar_message(msg)
3618
3651
def log_error(self, severity, problem, details, extra_detail=None):
3620
3653
Display error and warning messages to user in log window
3622
3655
self.error_log.add_message(severity, problem, details, extra_detail)
3625
3658
def on_error_eventbox_button_press_event(self, widget, event):
3626
3659
self.prefs.show_log_dialog = True
3627
self.error_log.widget.show()
3660
self.error_log.widget.show()
3630
3663
def on_menu_log_window_toggled(self, widget):
3631
3664
active = widget.get_active()
3632
3665
self.prefs.show_log_dialog = active
3702
3735
if not need_video_folder:
3703
3736
if self.files_of_type_present(files, rpdfile.FILE_TYPE_VIDEO):
3704
3737
need_video_folder = True
3706
3739
# second, check validity
3707
3740
if need_photo_folder:
3708
if not self.is_valid_download_dir(self.prefs.download_folder,
3741
if not self.is_valid_download_dir(self.prefs.download_folder,
3709
3742
is_photo_dir=True):
3711
3744
invalid_dirs.append(self.prefs.download_folder)
3713
logger.debug("Photo download folder is valid: %s",
3746
logger.debug("Photo download folder is valid: %s",
3714
3747
self.prefs.download_folder)
3716
3749
if need_video_folder:
3717
3750
if not self.is_valid_download_dir(self.prefs.video_download_folder,
3718
is_photo_dir=False):
3751
is_photo_dir=False):
3720
3753
invalid_dirs.append(self.prefs.video_download_folder)
3722
logger.debug("Video download folder is valid: %s",
3755
logger.debug("Video download folder is valid: %s",
3723
3756
self.prefs.video_download_folder)
3726
3759
return (valid, invalid_dirs)
3728
3761
def same_file_system(self, file1, file2):
3811
3844
except gio.Error, inst:
3812
3845
logger.error("Error checking download directory %s", path)
3813
3846
logger.error(inst)
3820
3853
# Process results and management
3824
3857
def _start_process_managers(self):
3826
3859
Set up process managers.
3828
3861
A task such as scanning a device or copying files is handled in its
3832
3865
self.batch_size = 10
3833
3866
self.batch_size_MB = 2
3835
3868
sequence_values = (self.downloads_today_value,
3836
3869
self.downloads_today_date_value,
3837
3870
self.day_start_value,
3838
3871
self.refresh_downloads_today_value,
3839
self.stored_sequence_value,
3872
self.stored_sequence_value,
3840
3873
self.uses_stored_sequence_no_value,
3841
3874
self.uses_session_sequece_no_value,
3842
3875
self.uses_sequence_letter_value)
3844
# daemon process to rename files and create subfolders
3877
# daemon process to rename files and create subfolders
3845
3878
self.subfolder_file_manager = SubfolderFileManager(
3846
3879
self.subfolder_file_results,
3847
3880
sequence_values)
3849
3882
# process to scan source devices / paths
3850
self.scan_manager = ScanManager(self.scan_results, self.batch_size,
3883
self.scan_manager = ScanManager(self.scan_results, self.batch_size,
3851
3884
self.device_collection.add_device)
3853
3886
#process to copy files from source to destination
3854
self.copy_files_manager = CopyFilesManager(self.copy_files_results,
3887
self.copy_files_manager = CopyFilesManager(self.copy_files_results,
3855
3888
self.batch_size_MB)
3857
3890
#process to back files up
3858
3891
self.backup_manager = BackupFilesManager(self.backup_results,
3859
3892
self.batch_size_MB)
3861
3894
#process to enhance files after they've been copied and before they're
3863
3896
self.file_modify_manager = FileModifyManager(self.file_modify_results)
3866
3899
def scan_results(self, source, condition):
3868
3901
Receive results from scan processes
3870
3903
connection = self.scan_manager.get_pipe(source)
3872
3905
conn_type, data = connection.recv()
3874
3907
if conn_type == rpdmp.CONN_COMPLETE:
3875
3908
connection.close()
3876
3909
self.scan_manager.no_tasks -= 1