~ubuntu-branches/ubuntu/vivid/rapid-photo-downloader/vivid

« back to all changes in this revision

Viewing changes to rapid/rapid.py

  • Committer: Package Import Robot
  • Author(s): Julien Valroff
  • Date: 2013-02-15 18:54:40 UTC
  • mfrom: (10.1.8 sid)
  • Revision ID: package-import@ubuntu.com-20130215185440-14cf477mxn56ghku
Tags: 0.4.6-1
New upstream release

Show diffs side-by-side

added added

removed removed

Lines of Context:
18
18
### Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
19
19
### USA
20
20
 
 
21
use_pynotify = True
21
22
 
22
23
import tempfile
23
24
 
39
40
import sys, time, types, os, datetime
40
41
 
41
42
import gobject, pango, cairo, array, pangocairo, gio
42
 
import pynotify
 
43
 
 
44
if use_pynotify:
 
45
    import pynotify
43
46
 
44
47
from multiprocessing import Process, Pipe, Queue, Event, Value, Array, current_process, log_to_stderr
45
48
from ctypes import c_int, c_bool, c_char
50
53
# Rapid Photo Downloader modules
51
54
 
52
55
import rpdfile
53
 
                  
 
56
 
54
57
import problemnotification as pn
55
58
import thumbnail as tn
56
59
import rpdmultiprocessing as rpdmp
105
108
                    STATUS_NOT_DOWNLOADED, \
106
109
                    STATUS_DOWNLOAD_AND_BACKUP_FAILED, \
107
110
                    STATUS_WARNING
108
 
                    
 
111
 
109
112
DOWNLOADED = [STATUS_DOWNLOADED, STATUS_DOWNLOADED_WITH_WARNING, STATUS_BACKUP_PROBLEM]
110
113
 
111
 
#Translators: if neccessary, for guidance in how to translate this program, you may see http://damonlynch.net/translate.html 
 
114
#Translators: if neccessary, for guidance in how to translate this program, you may see http://damonlynch.net/translate.html
112
115
PROGRAM_NAME = _('Rapid Photo Downloader')
113
116
__version__ = config.version
114
117
 
117
120
        return _("%(date)s\n%(time)s") % {'date':date.strftime("%x"), 'time':date.strftime("%X")}
118
121
    else:
119
122
        return _("%(date)s %(time)s") % {'date':date.strftime("%x"), 'time':date.strftime("%X")}
120
 
        
 
123
 
121
124
def date_time_subseconds_human_readable(date, subseconds):
122
125
    return _("%(date)s %(hour)s:%(minute)s:%(second)s:%(subsecond)s") % \
123
 
            {'date':date.strftime("%x"), 
 
126
            {'date':date.strftime("%x"),
124
127
             'hour':date.strftime("%H"),
125
 
             'minute':date.strftime("%M"), 
 
128
             'minute':date.strftime("%M"),
126
129
             'second':date.strftime("%S"),
127
130
             'subsecond': subseconds}
128
131
 
135
138
    def __init__(self, parent_app):
136
139
 
137
140
        self.parent_app = parent_app
138
 
        # device icon & name, size of images on the device (human readable), 
 
141
        # device icon & name, size of images on the device (human readable),
139
142
        # copy progress (%), copy text, eject button (None if irrelevant),
140
143
        # process id, pulse
141
144
        self.liststore = gtk.ListStore(gtk.gdk.Pixbuf, str, str, float, str,
144
147
        self.devices_by_scan_pid = {}
145
148
 
146
149
        gtk.TreeView.__init__(self, self.liststore)
147
 
        
 
150
 
148
151
        self.props.enable_search = False
149
152
        # make it impossible to select a row
150
153
        selection = self.get_selection()
151
154
        selection.set_mode(gtk.SELECTION_NONE)
152
155
        self.set_headers_visible(False)
153
 
        
154
 
        
155
 
        # Device refers to a thing like a camera, memory card in its reader, 
 
156
 
 
157
 
 
158
        # Device refers to a thing like a camera, memory card in its reader,
156
159
        # external hard drive, Portable Storage Device, etc.
157
160
        column0 = gtk.TreeViewColumn(_("Device"))
158
161
        pixbuf_renderer = gtk.CellRendererPixbuf()
168
171
        column0.add_attribute(text_renderer, 'text', 1)
169
172
        column0.add_attribute(eject_renderer, 'pixbuf', 5)
170
173
        self.append_column(column0)
171
 
        
172
 
        
 
174
 
 
175
 
173
176
        # Size refers to the total size of images on the device, typically in
174
177
        # MB or GB
175
178
        column1 = gtk.TreeViewColumn(_("Size"), gtk.CellRendererText(), text=2)
176
179
        self.append_column(column1)
177
 
        
178
 
        column2 = gtk.TreeViewColumn(_("Download Progress"), 
 
180
 
 
181
        column2 = gtk.TreeViewColumn(_("Download Progress"),
179
182
                                    gtk.CellRendererProgress(),
180
183
                                    value=3,
181
184
                                    text=4,
182
185
                                    pulse=7)
183
186
        self.append_column(column2)
184
187
        self.show_all()
185
 
        
 
188
 
186
189
        icontheme = gtk.icon_theme_get_default()
187
190
        try:
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)
190
193
        except:
191
194
            self.eject_pixbuf = gtk.gdk.pixbuf_new_from_file(
192
195
                                    paths.share_dir('glade3/media-eject.png'))
193
 
                                    
 
196
 
194
197
        self.add_events(gtk.gdk.BUTTON_PRESS_MASK)
195
198
        self.connect('button-press-event', self.button_clicked)
196
 
        
 
199
 
197
200
 
198
201
    def add_device(self, process_id, device, progress_bar_text = ''):
199
 
        
 
202
 
200
203
        # add the row, and get a temporary pointer to the row
201
204
        size_files = ''
202
205
        progress = 0.0
203
 
        
 
206
 
204
207
        if device.mount is None:
205
208
            eject = None
206
209
        else:
207
210
            eject = self.eject_pixbuf
208
 
            
 
211
 
209
212
        self.devices_by_scan_pid[process_id] = device
210
 
            
 
213
 
211
214
        iter = self.liststore.append((device.get_icon(),
212
215
                                      device.get_name(),
213
216
                                      size_files,
216
219
                                      eject,
217
220
                                      process_id,
218
221
                                      -1))
219
 
        
 
222
 
220
223
        self._set_process_map(process_id, iter)
221
 
        
 
224
 
222
225
        # adjust scrolled window height, based on row height and number of ready to start downloads
223
226
 
224
227
        # please note, at program startup, self.row_height() will be less than it will be when already running
228
231
        row_height = self.get_background_area(0, self.get_column(0))[3] + 1
229
232
        height = max(((len(self.map_process_to_row) + 1) * row_height), 24)
230
233
        self.parent_app.device_collection_scrolledwindow.set_size_request(-1, height)
231
 
        
 
234
 
 
235
 
232
236
    def update_device(self, process_id, total_size_files):
233
237
        """
234
238
        Updates the size of the photos and videos on the device, displayed to the user
238
242
            self.liststore.set_value(iter, 2, total_size_files)
239
243
        else:
240
244
            logger.critical("This device is unknown")
241
 
            
 
245
 
242
246
    def get_device(self, process_id):
243
247
        return self.devices_by_scan_pid.get(process_id)
244
 
    
 
248
 
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]
251
 
            
 
255
 
252
256
    def get_all_displayed_processes(self):
253
257
        """
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
255
259
        """
256
260
        return self.map_process_to_row.keys()
257
261
 
258
262
 
259
263
    def _set_process_map(self, process_id, iter):
260
264
        """
261
 
        convert the temporary iter into a tree reference, which is 
 
265
        convert the temporary iter into a tree reference, which is
262
266
        permanent
263
267
        """
264
268
 
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
268
 
    
 
272
 
269
273
    def _get_process_map(self, process_id):
270
274
        """
271
275
        return the tree iter for this process
272
276
        """
273
 
        
 
277
 
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()
278
282
            return iter
279
283
        else:
280
284
            return None
281
 
    
 
285
 
282
286
    def update_progress(self, scan_pid, percent_complete, progress_bar_text, bytes_downloaded, pulse=None):
283
 
        
 
287
 
284
288
        iter = self._get_process_map(scan_pid)
285
289
        if iter:
286
290
            if percent_complete:
287
291
                self.liststore.set_value(iter, 3, percent_complete)
288
292
            if progress_bar_text:
289
293
                self.liststore.set_value(iter, 4, progress_bar_text)
290
 
                
 
294
 
291
295
            if pulse is not None:
292
296
                if pulse:
293
297
                    # Make the bar pulse
316
320
                            iter = self.liststore.get_iter(path)
317
321
                            if self.liststore.get_value(iter, 5) is not None:
318
322
                                self.unmount(process_id = self.liststore.get_value(iter, 6))
319
 
            
 
323
 
320
324
    def unmount(self, process_id):
321
325
        device = self.devices_by_scan_pid[process_id]
322
326
        if device.mount is not None:
323
327
            logger.debug("Unmounting device with scan pid %s", process_id)
324
328
            device.mount.unmount(self.unmount_callback)
325
 
        
326
 
    
 
329
 
 
330
 
327
331
    def unmount_callback(self, mount, result):
328
332
        name = mount.get_name()
329
333
 
332
336
            logger.debug("%s successfully unmounted" % name)
333
337
        except gio.Error, inst:
334
338
            logger.error("%s did not unmount: %s", name, inst)
335
 
            
336
 
            title = _("%(device)s did not unmount") % {'device': name}
337
 
            message = '%s' % inst
338
 
                       
339
 
            n = pynotify.Notification(title, message)
340
 
            n.set_icon_from_pixbuf(self.parent_app.application_icon)
341
 
            n.show()             
 
339
 
 
340
            if use_pynotify:
 
341
                title = _("%(device)s did not unmount") % {'device': name}
 
342
                message = '%s' % inst
 
343
 
 
344
                n = pynotify.Notification(title, message)
 
345
                n.set_icon_from_pixbuf(self.parent_app.application_icon)
 
346
                n.show()
342
347
 
343
348
 
344
349
def create_cairo_image_surface(pil_image, image_width, image_height):
353
358
    __gproperties__ = {
354
359
        "image": (gobject.TYPE_PYOBJECT, "Image",
355
360
        "Image", gobject.PARAM_READWRITE),
356
 
        
357
 
        "filename": (gobject.TYPE_STRING, "Filename", 
 
361
 
 
362
        "filename": (gobject.TYPE_STRING, "Filename",
358
363
        "Filename", '', gobject.PARAM_READWRITE),
359
 
        
 
364
 
360
365
        "status": (gtk.gdk.Pixbuf, "Status",
361
366
        "Status", gobject.PARAM_READWRITE),
362
367
    }
363
 
    
 
368
 
364
369
    def __init__(self, checkbutton_height):
365
370
        gtk.CellRenderer.__init__(self)
366
371
        self.image = None
367
 
        
 
372
 
368
373
        self.image_area_size = 100
369
374
        self.text_area_size = 30
370
375
        self.padding = 6
371
376
        self.checkbutton_height = checkbutton_height
372
377
        self.icon_width = 20
373
 
        
 
378
 
374
379
    def do_set_property(self, pspec, value):
375
380
        setattr(self, pspec.name, value)
376
381
 
377
382
    def do_get_property(self, pspec):
378
383
        return getattr(self, pspec.name)
379
 
        
 
384
 
380
385
    def do_render(self, window, widget, background_area, cell_area, expose_area, flags):
381
 
        
 
386
 
382
387
        cairo_context = window.cairo_create()
383
 
        
 
388
 
384
389
        x = cell_area.x
385
390
        y = cell_area.y + self.checkbutton_height - 8
386
391
        w = cell_area.width
387
392
        h = cell_area.height
388
 
        
389
 
        #constrain operations to cell area, allowing for a 1 pixel border 
 
393
 
 
394
        #constrain operations to cell area, allowing for a 1 pixel border
390
395
        #either side
391
396
        #~ cairo_context.rectangle(x-1, y-1, w+2, h+2)
392
397
        #~ cairo_context.clip()
393
 
        
 
398
 
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()
400
 
        
 
405
 
401
406
        #image width and height
402
407
        image_w = self.image.size[0]
403
408
        image_h = self.image.size[1]
404
 
        
 
409
 
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()
419
 
        
 
424
 
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()
424
 
        
 
429
 
425
430
        #place the image
426
431
        cairo_context.set_source_surface(image, image_x, image_y)
427
432
        cairo_context.paint()
428
 
        
 
433
 
429
434
        #text
430
435
        context = pangocairo.CairoContext(cairo_context)
431
 
        
 
436
 
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)
436
 
        #~ context.clip()        
437
 
        
 
441
        #~ context.clip()
 
442
 
438
443
        layout = context.create_layout()
439
444
 
440
445
        width = text_w * pango.SCALE
441
446
        layout.set_width(width)
442
 
        
 
447
 
443
448
        layout.set_alignment(pango.ALIGN_CENTER)
444
449
        layout.set_ellipsize(pango.ELLIPSIZE_END)
445
 
        
 
450
 
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
453
458
        attr.insert(font_family)
454
459
        layout.set_attributes(attr)
455
460
 
456
 
        layout.set_text(self.filename)        
 
461
        layout.set_text(self.filename)
457
462
 
458
463
        context.move_to(text_x, text_y)
459
464
        context.show_layout(layout)
461
466
        #status
462
467
        cairo_context.set_source_pixbuf(self.status, x, y + self.image_area_size + 10)
463
468
        cairo_context.paint()
464
 
        
 
469
 
465
470
    def do_get_size(self, widget, cell_area):
466
471
        return (0, 0, self.image_area_size, self.image_area_size + self.text_area_size - self.checkbutton_height + 4)
467
 
        
 
472
 
468
473
 
469
474
gobject.type_register(ThumbnailCellRenderer)
470
 
 
 
475
 
471
476
 
472
477
class ThumbnailDisplay(gtk.IconView):
473
478
    def __init__(self, parent_app):
475
480
        self.set_spacing(0)
476
481
        self.set_row_spacing(5)
477
482
        self.set_margin(25)
478
 
        
 
483
 
479
484
        self.set_selection_mode(gtk.SELECTION_MULTIPLE)
480
485
        self.connect('selection-changed', self.on_selection_changed)
481
486
        self._selected_items = []
482
 
        
 
487
 
483
488
        self.rapid_app = parent_app
484
 
        
 
489
 
485
490
        self.batch_size = 10
486
 
        
 
491
 
487
492
        self.thumbnail_manager = ThumbnailManager(self.thumbnail_results, self.batch_size)
488
493
        self.preview_manager = PreviewManager(self.preview_results)
489
 
        
490
 
        self.treerow_index = {} 
491
 
        self.process_index = {} 
492
 
        
 
494
 
 
495
        self.treerow_index = {}
 
496
        self.process_index = {}
 
497
 
493
498
        self.rpd_files = {}
494
 
        
 
499
 
495
500
        self.total_thumbs_to_generate = 0
496
501
        self.thumbnails_generated = 0
497
 
        
 
502
 
498
503
        # dict of scan_pids that are having thumbnails generated
499
504
        # value is the thumbnail process id
500
505
        # this is needed when terminating thumbnailing early such as when
501
506
        # user clicks download before the thumbnailing is finished
502
507
        self.generating_thumbnails = {}
503
 
        
 
508
 
504
509
        self.thumbnails = {}
505
510
        self.previews = {}
506
511
        self.previews_being_fetched = set()
507
 
        
 
512
 
508
513
        self.stock_photo_thumbnails = tn.PhotoIcons()
509
514
        self.stock_video_thumbnails = tn.VideoIcons()
510
 
        
 
515
 
511
516
        self.SELECTED_COL = 1
512
517
        self.UNIQUE_ID_COL = 2
513
518
        self.TIMESTAMP_COL = 4
515
520
        self.CHECKBUTTON_VISIBLE_COL = 6
516
521
        self.DOWNLOAD_STATUS_COL = 7
517
522
        self.STATUS_ICON_COL = 8
518
 
        
 
523
 
519
524
        self._create_liststore()
520
525
 
521
526
        self.clear()
522
 
        
 
527
 
523
528
        checkbutton = gtk.CellRendererToggle()
524
529
        checkbutton.set_radio(False)
525
530
        checkbutton.props.activatable = True
529
534
 
530
535
        self.add_attribute(checkbutton, "active", 1)
531
536
        self.add_attribute(checkbutton, "visible", 6)
532
 
        
 
537
 
533
538
        checkbutton_size = checkbutton.get_size(self, None)
534
539
        checkbutton_height = checkbutton_size[3]
535
540
        checkbutton_width = checkbutton_size[2]
536
 
        
 
541
 
537
542
        image = ThumbnailCellRenderer(checkbutton_height)
538
543
        self.pack_start(image, expand=True)
539
544
        self.add_attribute(image, "image", 0)
542
547
 
543
548
        #set the background color to a darkish grey
544
549
        self.modify_base(gtk.STATE_NORMAL, gtk.gdk.Color('#444444'))
545
 
        
 
550
 
546
551
        self.show_all()
547
552
        self._setup_icons()
548
 
        
 
553
 
549
554
        self.connect('item-activated', self.on_item_activated)
550
 
        
 
555
 
551
556
    def _create_liststore(self):
552
557
        """
553
558
        Creates the default list store to hold the icons
562
567
             gobject.TYPE_BOOLEAN,  # 6 visibility of checkbutton
563
568
             int,                   # 7 status of download
564
569
             gtk.gdk.Pixbuf,        # 8 status icon
565
 
             )        
566
 
        
 
570
             )
 
571
 
567
572
    def _setup_icons(self):
568
573
        # icons to be displayed in status column
569
574
 
582
587
               size, size)
583
588
        self.download_pending_icon = gtk.gdk.pixbuf_new_from_file_at_size(
584
589
               paths.share_dir('glade3/rapid-photo-downloader-download-pending.png'),
585
 
               size, size) 
 
590
               size, size)
586
591
        self.downloaded_with_warning_icon = gtk.gdk.pixbuf_new_from_file_at_size(
587
592
               paths.share_dir('glade3/rapid-photo-downloader-downloaded-with-warning.svg'),
588
593
               size, size)
589
594
        self.downloaded_with_error_icon = gtk.gdk.pixbuf_new_from_file_at_size(
590
595
               paths.share_dir('glade3/rapid-photo-downloader-downloaded-with-error.svg'),
591
596
               size, size)
592
 
        
 
597
 
593
598
        # make the not yet downloaded icon a transparent square
594
599
        self.not_downloaded_icon = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, 16, 16)
595
600
        self.not_downloaded_icon.fill(0xffffffff)
596
601
        self.not_downloaded_icon = self.not_downloaded_icon.add_alpha(True, chr(255), chr(255), chr(255))
597
 
        
 
602
 
598
603
    def get_status_icon(self, status):
599
604
        """
600
605
        Returns the correct icon, based on the status
616
621
        else:
617
622
            logger.critical("FIXME: unknown status: %s", status)
618
623
            status_icon = self.not_downloaded_icon
619
 
        return status_icon        
620
 
    
 
624
        return status_icon
 
625
 
621
626
    def sort_by_timestamp(self):
622
627
        self.liststore.set_sort_column_id(self.TIMESTAMP_COL, gtk.SORT_ASCENDING)
623
 
    
 
628
 
624
629
    def on_selection_changed(self, iconview):
625
630
        self._selected_items = self.get_selected_items()
626
 
        
 
631
 
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,]
631
 
            
 
636
 
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)
638
 
            
 
643
 
639
644
        self.rapid_app.set_download_action_sensitivity()
640
 
        
641
 
        
 
645
 
 
646
 
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)
645
 
    
 
650
 
646
651
    def add_file(self, rpd_file, generate_thumbnail):
647
652
 
648
653
        thumbnail_icon = self.get_stock_icon(rpd_file.file_type)
650
655
        scan_pid = rpd_file.scan_pid
651
656
 
652
657
        timestamp = int(rpd_file.modification_time)
653
 
        
 
658
 
654
659
        iter = self.liststore.append((thumbnail_icon,
655
660
                                      True,
656
661
                                      unique_id,
661
666
                                      STATUS_NOT_DOWNLOADED,
662
667
                                      self.not_downloaded_icon
663
668
                                      ))
664
 
        
 
669
 
665
670
        path = self.liststore.get_path(iter)
666
671
        treerowref = gtk.TreeRowReference(self.liststore, path)
667
 
        
 
672
 
668
673
        if scan_pid in self.process_index:
669
674
            self.process_index[scan_pid].append(unique_id)
670
675
        else:
671
676
            self.process_index[scan_pid] = [unique_id,]
672
 
            
 
677
 
673
678
        self.treerow_index[unique_id] = treerowref
674
679
        self.rpd_files[unique_id] = rpd_file
675
 
        
 
680
 
676
681
        if generate_thumbnail:
677
682
            self.total_thumbs_to_generate += 1
678
683
 
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
681
686
        not exist"""
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:
685
690
                    return rpd_file
686
 
                    
 
691
 
687
692
        return None
688
 
    
 
693
 
689
694
    def get_unique_id_from_iter(self, iter):
690
695
        return self.liststore.get_value(iter, 2)
691
 
        
 
696
 
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)
696
 
    
697
 
    def on_item_activated(self, iconview, path):        
 
701
 
 
702
    def on_item_activated(self, iconview, path):
698
703
        """
699
704
        """
700
705
        iter = self.liststore.get_iter(path)
701
706
        self.show_preview(iter=iter)
702
707
        self.advance_get_preview_image(iter)
703
708
 
704
 
    
 
709
 
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
711
716
            else:
712
717
                file_location = rpd_file.full_file_name
713
718
                thm_file_name = rpd_file.thm_full_name
714
 
                
 
719
 
715
720
            self.preview_manager.get_preview(unique_id, file_location,
716
721
                                            thm_file_name,
717
722
                                            rpd_file.file_type, size_max=None,)
718
 
                                            
 
723
 
719
724
            self.previews_being_fetched.add(unique_id)
720
 
            
 
725
 
721
726
    def show_preview(self, unique_id=None, iter=None):
722
727
        if unique_id is not None:
723
728
            iter = self.get_iter_from_unique_id(unique_id)
734
739
                path = 0
735
740
            iter = self.liststore.get_iter(path)
736
741
            unique_id = self.get_unique_id_from_iter(iter)
737
 
            
738
 
            
739
 
        rpd_file = self.rpd_files[unique_id]    
740
 
        
 
742
 
 
743
 
 
744
        rpd_file = self.rpd_files[unique_id]
 
745
 
741
746
        if unique_id in self.previews:
742
747
            preview_image = self.previews[unique_id]
743
748
        else:
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]
748
753
            else:
749
754
                preview_image = self.get_stock_icon(rpd_file.file_type)
750
 
        
 
755
 
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)
755
 
            
 
760
 
756
761
    def _get_next_iter(self, iter):
757
762
        iter = self.liststore.iter_next(iter)
758
763
        if iter is None:
759
764
            iter = self.liststore.get_iter_first()
760
765
        return iter
761
 
        
 
766
 
762
767
    def _get_prev_iter(self, iter):
763
768
        row = self.liststore.get_path(iter)[0]
764
769
        if row == 0:
766
771
        else:
767
772
            row -= 1
768
773
        iter = self.liststore.get_iter(row)
769
 
        return iter        
770
 
    
 
774
        return iter
 
775
 
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)
774
779
 
775
780
        if iter is not None:
776
781
            self.show_preview(iter=iter)
777
 
            
 
782
 
778
783
            # cache next image
779
784
            self.advance_get_preview_image(iter, prev=False, next=True)
780
 
            
 
785
 
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)
784
789
 
785
790
        if iter is not None:
786
791
            self.show_preview(iter=iter)
787
 
            
 
792
 
788
793
            # cache next image
789
794
            self.advance_get_preview_image(iter, prev=True, next=False)
790
795
 
791
 
            
 
796
 
792
797
    def advance_get_preview_image(self, iter, prev=True, next=True):
793
798
        unique_ids = []
794
799
        if next:
795
800
            next_iter = self._get_next_iter(iter)
796
801
            unique_ids.append(self.get_unique_id_from_iter(next_iter))
797
 
            
 
802
 
798
803
        if prev:
799
804
            prev_iter = self._get_prev_iter(iter)
800
805
            unique_ids.append(self.get_unique_id_from_iter(prev_iter))
801
 
            
 
806
 
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)
806
 
            
 
811
 
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]:
813
818
                else:
814
819
                    row[self.SELECTED_COL] = check_all
815
820
        self.rapid_app.set_download_action_sensitivity()
816
 
            
 
821
 
817
822
    def files_are_checked_to_download(self):
818
823
        """
819
824
        Returns True if there is any file that the user has indicated they
825
830
                if rpd_file.status not in DOWNLOADED:
826
831
                    return True
827
832
        return False
828
 
        
 
833
 
829
834
    def get_files_checked_for_download(self, scan_pid):
830
835
        """
831
836
        Returns a dict of scan ids and associated files the user has indicated
832
837
        they want to download
833
 
        
 
838
 
834
839
        If scan_pid is not None, then returns only those files from that scan_pid
835
840
        """
836
841
        files = dict()
853
858
                    if self.liststore.get_value(iter, self.SELECTED_COL):
854
859
                        files[scan_pid].append(rpd_file)
855
860
        return files
856
 
                
 
861
 
857
862
    def get_no_files_remaining(self, scan_pid):
858
863
        """
859
864
        Returns the number of files that have not yet been downloaded for the
865
870
            if rpd_file.status == STATUS_NOT_DOWNLOADED:
866
871
                i += 1
867
872
        return i
868
 
        
 
873
 
869
874
    def files_remain_to_download(self):
870
875
        """
871
 
        Returns True if any files remain that are not downloaded, else returns 
 
876
        Returns True if any files remain that are not downloaded, else returns
872
877
        False
873
878
        """
874
879
        for row in self.liststore:
875
880
            if row[self.DOWNLOAD_STATUS_COL] == STATUS_NOT_DOWNLOADED:
876
881
                return True
877
882
        return False
878
 
            
 
883
 
879
884
 
880
885
    def mark_download_pending(self, files_by_scan_pid):
881
886
        """
895
900
                self.liststore.set_value(iter, self.DOWNLOAD_STATUS_COL, STATUS_DOWNLOAD_PENDING)
896
901
                icon = self.get_status_icon(STATUS_DOWNLOAD_PENDING)
897
902
                self.liststore.set_value(iter, self.STATUS_ICON_COL, icon)
898
 
                
 
903
 
899
904
    def select_image(self, unique_id):
900
905
        iter = self.get_iter_from_unique_id(unique_id)
901
906
        path = self.liststore.get_path(iter)
902
907
        self.select_path(path)
903
908
        self.scroll_to_path(path, use_align=False, row_align=0.5, col_align=0.5)
904
 
        
 
909
 
905
910
    def get_stock_icon(self, file_type):
906
911
        if file_type == rpdfile.FILE_TYPE_PHOTO:
907
912
            return self.stock_photo_thumbnails.stock_thumbnail_image_icon
908
913
        else:
909
914
            return self.stock_video_thumbnails.stock_thumbnail_image_icon
910
 
            
 
915
 
911
916
    def update_status_post_download(self, rpd_file):
912
917
        iter = self.get_iter_from_unique_id(rpd_file.unique_id)
913
918
        self.liststore.set_value(iter, self.DOWNLOAD_STATUS_COL, rpd_file.status)
915
920
        self.liststore.set_value(iter, self.STATUS_ICON_COL, icon)
916
921
        self.liststore.set_value(iter, self.CHECKBUTTON_VISIBLE_COL, False)
917
922
        self.rpd_files[rpd_file.unique_id] = rpd_file
918
 
            
 
923
 
919
924
    def generate_thumbnails(self, scan_pid):
920
925
        """Initiate thumbnail generation for files scanned in one process
921
926
        """
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
926
 
    
 
931
 
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)        
932
 
    
 
936
        self.liststore.set(iter, 0, icon)
 
937
 
933
938
    def update_thumbnail(self, thumbnail_data):
934
939
        """
935
940
        Takes the generated thumbnail and updates the display
936
 
        
 
941
 
937
942
        If the thumbnail_data includes a second image, that is used to
938
943
        update the thumbnail list using the unique_id
939
944
        """
940
945
        unique_id = thumbnail_data[0]
941
946
        thumbnail_icon = thumbnail_data[1]
942
 
        
 
947
 
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()
946
 
            
 
951
 
947
952
            if thumbnail_icon:
948
953
                self._set_thumbnail(unique_id, thumbnail_icon)
949
 
                
 
954
 
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()
953
958
 
954
959
    def terminate_thumbnail_generation(self, scan_pid):
955
960
        """
956
 
        Terminates thumbnail generation if thumbnails are currently 
 
961
        Terminates thumbnail generation if thumbnails are currently
957
962
        being generated for this scan_pid
958
963
        """
959
 
        
 
964
 
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]
965
 
            
 
970
 
966
971
            if len(self.generating_thumbnails) == 0:
967
972
                self._reset_thumbnail_tracking_and_display()
968
973
        else:
969
974
            terminated = False
970
 
            
 
975
 
971
976
        return terminated
972
 
                
 
977
 
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
977
 
        
 
982
 
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
983
 
    
 
988
 
984
989
    def thumbnail_results(self, source, condition):
985
990
        connection = self.thumbnail_manager.get_pipe(source)
986
 
        
 
991
 
987
992
        conn_type, data = connection.recv()
988
 
        
 
993
 
989
994
        if conn_type == rpdmp.CONN_COMPLETE:
990
995
            scan_pid = data
991
996
            del self.generating_thumbnails[scan_pid]
992
997
            connection.close()
993
998
            return False
994
999
        else:
995
 
            
 
1000
 
996
1001
            for thumbnail_data in data:
997
1002
                self.update_thumbnail(thumbnail_data)
998
 
            
 
1003
 
999
1004
            self.thumbnails_generated += len(data)
1000
 
            
 
1005
 
1001
1006
            # clear progress bar information if all thumbnails have been
1002
1007
            # extracted
1003
1008
            if self.thumbnails_generated == self.total_thumbs_to_generate:
1006
1011
                if self.total_thumbs_to_generate:
1007
1012
                    self.rapid_app.download_progressbar.set_fraction(
1008
1013
                        float(self.thumbnails_generated) / self.total_thumbs_to_generate)
1009
 
            
1010
 
        
 
1014
 
 
1015
 
1011
1016
        return True
1012
 
        
 
1017
 
1013
1018
    def preview_results(self, unique_id, preview_full_size, preview_small):
1014
1019
        """
1015
1020
        Receive a full size preview image and update
1019
1024
            preview_image = preview_full_size.get_image()
1020
1025
            self.previews[unique_id] = preview_image
1021
1026
            self.rapid_app.update_preview_image(unique_id, preview_image)
1022
 
            
 
1027
 
1023
1028
            # user can turn off option for thumbnail generation after a scan
1024
1029
            if unique_id not in self.thumbnails and preview_small is not None:
1025
1030
                self._set_thumbnail(unique_id, preview_small.get_image())
1026
 
                
1027
 
    
 
1031
 
 
1032
 
1028
1033
    def clear_all(self, scan_pid=None, keep_downloaded_files=False):
1029
1034
        """
1030
1035
        Removes files from display and internal tracking.
1031
 
        
 
1036
 
1032
1037
        If scan_pid is not None, then only files matching that scan_pid will
1033
1038
        be removed. Otherwise, everything will be removed.
1034
 
        
 
1039
 
1035
1040
        If keep_downloaded_files is True, files will not be removed if they
1036
1041
        have been downloaded.
1037
1042
        """
1038
1043
        if scan_pid is None and not keep_downloaded_files:
1039
 
            
 
1044
 
1040
1045
            # Here it is critically important to create a brand new liststore,
1041
1046
            # because the old one is set to be sorted, which is extremely slow.
1042
1047
            logger.debug("Creating new thumbnails model")
1045
1050
 
1046
1051
            self.treerow_index = {}
1047
1052
            self.process_index = {}
1048
 
            
 
1053
 
1049
1054
            self.rpd_files = {}
1050
1055
        else:
1051
1056
            if scan_pid in self.process_index:
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]
1063
 
                    
 
1068
 
1064
1069
    def display_thumbnails(self):
1065
1070
        self.set_model(self.liststore)
1066
 
    
 
1071
 
1067
1072
class TaskManager:
1068
1073
    def __init__(self, results_callback, batch_size):
1069
1074
        self.results_callback = results_callback
1070
 
        
 
1075
 
1071
1076
        # List of actual process, it's terminate_queue, and it's run_event
1072
1077
        self._processes = []
1073
 
        
 
1078
 
1074
1079
        self._pipes = {}
1075
1080
        self.batch_size = batch_size
1076
 
        
 
1081
 
1077
1082
        self.paused = False
1078
1083
        self.no_tasks = 0
1079
 
       
1080
 
    
 
1084
 
 
1085
 
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
1085
1090
        return pid
1086
1091
 
1087
 
        
 
1092
 
1088
1093
    def _setup_task(self, task):
1089
1094
        task_results_conn, task_process_conn = self._setup_pipe()
1090
 
        
 
1095
 
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)
1094
 
        
 
1099
 
1095
1100
        terminate_queue = Queue()
1096
1101
        run_event = Event()
1097
1102
        run_event.set()
1098
 
        
1099
 
        return self._initiate_task(task, task_results_conn, task_process_conn, 
 
1103
 
 
1104
        return self._initiate_task(task, task_results_conn, task_process_conn,
1100
1105
                                   terminate_queue, run_event)
1101
 
        
 
1106
 
1102
1107
    def _setup_pipe(self):
1103
1108
        return Pipe(duplex=False)
1104
 
        
 
1109
 
1105
1110
    def _initiate_task(self, task, task_process_conn, terminate_queue, run_event):
1106
1111
        logger.error("Implement child class method!")
1107
 
        
1108
 
    
 
1112
 
 
1113
 
1109
1114
    def processes(self):
1110
1115
        for i in range(len(self._processes)):
1111
1116
            yield self._processes[i]
1112
 
    
 
1117
 
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()
1119
 
                
 
1124
 
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()
1126
 
                
 
1131
 
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()
1133
 
            
 
1138
 
1134
1139
    def _send_termination_msg(self, p):
1135
1140
        p[1].put(None)
1136
 
    
 
1141
 
1137
1142
    def terminate_process(self, process_id):
1138
1143
        """
1139
1144
        Send a signal to process with matching process_id that it should
1143
1148
            if p[0].pid == process_id:
1144
1149
                if p[0].is_alive():
1145
1150
                    self._terminate_process(p)
1146
 
    
 
1151
 
1147
1152
    def request_termination(self):
1148
1153
        """
1149
1154
        Send a signal to processes that they should immediately terminate
1153
1158
            if p[0].is_alive():
1154
1159
                requested = True
1155
1160
                self._terminate_process(p)
1156
 
                    
 
1161
 
1157
1162
        return requested
1158
 
    
 
1163
 
1159
1164
    def terminate_forcefully(self):
1160
1165
        """
1161
1166
        Forcefully terminates any running processes. Use with great caution.
1162
 
        No cleanup action is performed. 
1163
 
        
 
1167
        No cleanup action is performed.
 
1168
 
1164
1169
        As python essential reference (4th edition) says, if the process
1165
1170
        'holds a lock or is involved with interprocess communication,
1166
1171
        terminating it might cause a deadlock or corrupted I/O.'
1167
1172
        """
1168
 
        
 
1173
 
1169
1174
        for p in self.processes():
1170
1175
            if p[0].is_alive():
1171
 
                logger.info("Forcefully terminating %s in %s" , p[0].name, 
 
1176
                logger.info("Forcefully terminating %s in %s" , p[0].name,
1172
1177
                                                self.__class__.__name__)
1173
1178
                p[0].terminate()
1174
1179
 
1175
 
            
 
1180
 
1176
1181
    def get_pipe(self, source):
1177
1182
        return self._pipes[source]
1178
 
        
 
1183
 
1179
1184
    def get_no_active_processes(self):
1180
1185
        """
1181
1186
        Returns how many processes are currently active, i.e. running
1188
1193
 
1189
1194
 
1190
1195
class ScanManager(TaskManager):
1191
 
    
1192
 
    def __init__(self, results_callback, batch_size, 
 
1196
 
 
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
1196
 
        
1197
 
    def _initiate_task(self, task, task_results_conn, task_process_conn, 
 
1201
 
 
1202
    def _initiate_task(self, task, task_results_conn, task_process_conn,
1198
1203
                       terminate_queue, run_event):
1199
 
        
 
1204
 
1200
1205
        device = task[0]
1201
1206
        ignored_paths = task[1]
1202
1207
        use_re_ignored_paths = task[2]
1203
 
        
 
1208
 
1204
1209
        scan = scan_process.Scan(device.get_path(),
1205
1210
                                ignored_paths,
1206
1211
                                use_re_ignored_paths,
1207
 
                                self.batch_size, 
 
1212
                                self.batch_size,
1208
1213
                                task_process_conn, terminate_queue, run_event)
1209
1214
        scan.start()
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...'))
1217
 
            
 
1222
 
1218
1223
        return scan.pid
1219
 
            
 
1224
 
1220
1225
class CopyFilesManager(TaskManager):
1221
 
    
1222
 
    def _initiate_task(self, task, task_results_conn, 
 
1226
 
 
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]
1230
 
        
 
1235
 
1231
1236
        copy_files = copyfiles.CopyFiles(photo_download_folder,
1232
1237
                                video_download_folder,
1233
 
                                files, 
 
1238
                                files,
1234
1239
                                modify_files_during_download,
1235
1240
                                modify_pipe,
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
1241
 
        
 
1246
 
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,
1249
1254
                                          run_event)
1250
1255
        generator.start()
1251
1256
        self._processes.append((generator, terminate_queue, run_event))
1256
1261
    Duplex, multiprocess, similar to BackupFilesManager
1257
1262
    """
1258
1263
    def __init__(self, results_callback):
1259
 
        TaskManager.__init__(self, results_callback=results_callback, 
 
1264
        TaskManager.__init__(self, results_callback=results_callback,
1260
1265
                            batch_size=0)
1261
1266
        self.file_modify_by_scan_pid = {}
1262
 
                            
1263
 
    def _initiate_task(self, task, task_results_conn, task_process_conn, 
 
1267
 
 
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]
1268
 
        
 
1273
 
1269
1274
        file_modify = filemodify.FileModify(auto_rotate_jpeg, focal_length,
1270
 
                                        task_process_conn, terminate_queue, 
 
1275
                                        task_process_conn, terminate_queue,
1271
1276
                                        run_event)
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))
1275
 
                                
 
1280
 
1276
1281
        self.file_modify_by_scan_pid[scan_pid] = (task_results_conn, file_modify.pid)
1277
 
        
 
1282
 
1278
1283
        return file_modify.pid
1279
1284
 
1280
1285
    def _setup_pipe(self):
1281
1286
        return Pipe(duplex=True)
1282
 
        
 
1287
 
1283
1288
    def _send_termination_msg(self, p):
1284
1289
        p[1].put(None)
1285
1290
        p[3].send((None, None))
1286
 
        
 
1291
 
1287
1292
    def get_modify_pipe(self, scan_pid):
1288
1293
        return self.file_modify_by_scan_pid[scan_pid][0]
1289
 
        
1290
 
        
 
1294
 
 
1295
 
1291
1296
class BackupFilesManager(TaskManager):
1292
1297
    """
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.
1296
 
    
 
1301
 
1297
1302
    Duplex, multiprocess.
1298
1303
    """
1299
1304
    def __init__(self, results_callback, batch_size):
1302
1307
 
1303
1308
    def _setup_pipe(self):
1304
1309
        return Pipe(duplex=True)
1305
 
        
 
1310
 
1306
1311
    def _send_termination_msg(self, p):
1307
1312
        p[1].put(None)
1308
 
        p[3].send((None, None, None, None))
1309
 
        
1310
 
    def _initiate_task(self, task, task_results_conn, task_process_conn, 
 
1313
        p[3].send((None, None, None, None, None))
 
1314
 
 
1315
    def _initiate_task(self, task, task_results_conn, task_process_conn,
1311
1316
                       terminate_queue, run_event):
1312
1317
        path = task[0]
1313
1318
        name = task[1]
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,
1317
1322
                                        run_event)
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))
1321
 
        
 
1326
 
1322
1327
        self.backup_devices_by_path[path] = (task_results_conn, backup_files.pid,
1323
1328
                                            backup_type)
1324
 
        
 
1329
 
1325
1330
        return backup_files.pid
1326
 
        
1327
 
    def backup_file(self, move_succeeded, rpd_file, path_suffix, 
1328
 
                                                backup_duplicate_overwrite):
 
1331
 
 
1332
    def backup_file(self, move_succeeded, rpd_file, path_suffix,
 
1333
                                                backup_duplicate_overwrite,
 
1334
                                                download_count):
1329
1335
 
1330
1336
        if rpd_file.file_type == rpdfile.FILE_TYPE_PHOTO:
1331
1337
            logger.debug("Backing up photo %s", rpd_file.download_name)
1334
1340
 
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))
1344
1350
            else:
1345
1351
                logger.debug("Not backing up to %s", path)
1346
 
            
 
1352
 
1347
1353
    def add_device(self, path, name, backup_type):
1348
1354
        """
1349
1355
        Convenience function to setup adding a backup device
1350
1356
        """
1351
1357
        return self.add_task((path, name, backup_type))
1352
 
    
 
1358
 
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]
1357
 
        
1358
 
                
 
1363
 
 
1364
 
1359
1365
class SingleInstanceTaskManager:
1360
1366
    """
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.
1363
 
    
 
1369
 
1364
1370
    Core (infrastructure) functionality is implemented in this class.
1365
1371
    Derived classes should implemented functionality to actually implement
1366
1372
    specific tasks.
1367
1373
    """
1368
 
    def __init__(self, results_callback):    
 
1374
    def __init__(self, results_callback):
1369
1375
        self.results_callback = results_callback
1370
 
        
 
1376
 
1371
1377
        self.task_results_conn, self.task_process_conn = Pipe(duplex=True)
1372
 
        
 
1378
 
1373
1379
        source = self.task_results_conn.fileno()
1374
1380
        gobject.io_add_watch(source, gobject.IO_IN, self.task_results)
1375
1381
 
1376
 
        
 
1382
 
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()
1382
 
        
 
1388
 
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))
1385
 
        
 
1391
 
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)
1389
 
        return True 
1390
 
        
 
1395
        return True
 
1396
 
1391
1397
class SubfolderFileManager(SingleInstanceTaskManager):
1392
1398
    """
1393
1399
    Manages the daemon process that renames files and creates subfolders
1394
1400
    """
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)
1401
 
        
1402
 
    def rename_file_and_move_to_subfolder(self, download_succeeded, 
 
1407
 
 
1408
    def rename_file_and_move_to_subfolder(self, download_succeeded,
1403
1409
            download_count, rpd_file):
1404
 
                                              
1405
 
        self.task_results_conn.send((download_succeeded, download_count, 
 
1410
 
 
1411
        logger.debug("Sending file for rename: %s.", download_count)
 
1412
        self.task_results_conn.send((download_succeeded, download_count,
1406
1413
            rpd_file))
1407
 
        logger.debug("Download count: %s.", download_count)
1408
 
        
 
1414
 
1409
1415
 
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)
1413
1419
        return True
1414
 
        
 
1420
 
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)
1421
 
        
 
1427
 
1422
1428
    def set_image(self, image):
1423
1429
        self.base_image = image
1424
 
        
 
1430
 
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
1429
 
        
 
1435
 
1430
1436
        self.queue_draw()
1431
 
        
 
1437
 
1432
1438
    def expose(self, widget, event):
1433
1439
 
1434
1440
        cairo_context = self.window.cairo_create()
1435
 
        
1436
 
        x = event.area.x 
1437
 
        y = event.area.y 
 
1441
 
 
1442
        x = event.area.x
 
1443
        y = event.area.y
1438
1444
        w = event.area.width
1439
1445
        h = event.area.height
1440
 
        
1441
 
        #constrain operations to event area 
 
1446
 
 
1447
        #constrain operations to event area
1442
1448
        cairo_context.rectangle(x, y, w, h)
1443
1449
        cairo_context.clip_preserve()
1444
 
        
 
1450
 
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()
1449
1455
 
1450
1456
        if not self.base_image:
1451
1457
            return False
1452
 
            
 
1458
 
1453
1459
        frame_aspect = float(w) / h
1454
 
        
 
1460
 
1455
1461
        if frame_aspect > self.base_image_aspect:
1456
1462
            # Frame is wider than image
1457
1463
            height = h
1460
1466
            # Frame is taller than image
1461
1467
            width = w
1462
1468
            height = int(width / self.base_image_aspect)
1463
 
            
 
1469
 
1464
1470
        #resize image
1465
1471
        pil_image = self.base_image.copy()
1466
1472
        if self.base_image_w < width or self.base_image_h < height:
1473
1479
        #image width and height
1474
1480
        image_w = pil_image.size[0]
1475
1481
        image_h = pil_image.size[1]
1476
 
        
 
1482
 
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)
1481
 
        
 
1487
 
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()        
1485
 
 
1486
 
        return False    
1487
 
        
1488
 
        
 
1490
        cairo_context.paint()
 
1491
 
 
1492
        return False
 
1493
 
 
1494
 
1489
1495
 
1490
1496
class PreviewImage:
1491
 
    
 
1497
 
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
1500
 
        
 
1506
 
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)
1504
 
        
 
1510
 
1505
1511
        self.unique_id = None
1506
 
        
1507
 
    def set_preview_image(self, unique_id, pil_image, include_checkbutton_visible=None, 
 
1512
 
 
1513
    def set_preview_image(self, unique_id, pil_image, include_checkbutton_visible=None,
1508
1514
                          checked=None):
1509
1515
        """
1510
1516
        """
1516
1522
 
1517
1523
        if include_checkbutton_visible is not None:
1518
1524
            self.download_this_checkbutton.props.visible = include_checkbutton_visible
1519
 
        
 
1525
 
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)
1523
1529
 
1524
 
        
 
1530
 
1525
1531
class RapidApp(dbus.service.Object):
1526
1532
    """
1527
1533
    The main Rapid Photo Downloader application class.
1528
 
    
 
1534
 
1529
1535
    Contains functionality for main program window, and directs all other
1530
1536
    processes.
1531
1537
    """
1532
 
     
 
1538
 
1533
1539
    def __init__(self,  bus, path, name, taskserver=None, focal_length=None,
1534
 
    auto_detect=None, device_location=None): 
1535
 
        
 
1540
    auto_detect=None, device_location=None):
 
1541
 
1536
1542
        dbus.service.Object.__init__ (self, bus, path, name)
1537
1543
        self.running = False
1538
 
        
 
1544
 
1539
1545
        self.taskserver = taskserver
1540
 
        
 
1546
 
1541
1547
        self.focal_length = focal_length
1542
 
        
 
1548
 
1543
1549
        # Setup program preferences, and set callback for when they change
1544
1550
        self._init_prefs(auto_detect, device_location)
1545
 
        
 
1551
 
1546
1552
        # Initialize widgets in the main window, and variables that point to them
1547
1553
        self._init_widgets()
1548
 
        self._init_pynotify()
1549
 
        
 
1554
 
 
1555
        if use_pynotify:
 
1556
            self._init_pynotify()
 
1557
 
1550
1558
        # Initialize job code handling
1551
1559
        self._init_job_code()
1552
 
        
 
1560
 
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()
1556
 
        
 
1564
 
1557
1565
        # Setup various widgets
1558
1566
        self._setup_buttons()
1559
1567
        self._setup_error_icons()
1560
1568
        self._setup_icons()
1561
 
            
 
1569
 
1562
1570
        # Show the main window
1563
1571
        self.rapidapp.show()
1564
 
        
 
1572
 
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)
1569
 
        
 
1577
 
1570
1578
        # Initialize variables with which to track important downloads results
1571
1579
        self._init_download_tracking()
1572
 
        
 
1580
 
1573
1581
        # Set up process managers.
1574
1582
        # A task such as scanning a device or copying files is handled in its
1575
1583
        # own process.
1576
1584
        self._start_process_managers()
1577
 
        
 
1585
 
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)
1581
 
        
 
1589
 
1582
1590
        # Ensure the device collection scrolled window is not too small
1583
1591
        self._set_device_collection_size()
1584
 
    
 
1592
 
1585
1593
    def on_rapidapp_destroy(self, widget, data=None):
1586
1594
 
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
1595
 
        
 
1603
 
1596
1604
        self.prefs.set_downloads_today_from_tracker(self.downloads_today_tracker)
1597
 
        
 
1605
 
1598
1606
        gtk.main_quit()
1599
 
        
 
1607
 
1600
1608
    def _terminate_processes(self, terminate_file_copies=False):
1601
 
        
 
1609
 
1602
1610
        if terminate_file_copies:
1603
1611
            logger.info("Terminating all processes...")
1604
1612
 
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()
1609
 
        
 
1617
 
1610
1618
        if terminate_file_copies:
1611
1619
            copy_files_termination_requested = self.copy_files_manager.request_termination()
1612
1620
        else:
1613
1621
            copy_files_termination_requested = False
1614
 
        
 
1622
 
1615
1623
        if (scan_termination_requested or thumbnails_termination_requested or
1616
1624
                backup_termination_requested or file_modify_termination_requested):
1617
1625
            time.sleep(1)
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):
1626
1634
                self.scan_manager.terminate_forcefully()
1627
1635
                self.backup_manager.terminate_forcefully()
1628
1636
                self.file_modify_manager.terminate_forcefully()
1629
 
                
 
1637
 
1630
1638
        if terminate_file_copies and copy_files_termination_requested:
1631
1639
            time.sleep(1)
1632
1640
            self.copy_files_manager.terminate_forcefully()
1633
 
        
 
1641
 
1634
1642
        if terminate_file_copies:
1635
1643
            self._clean_all_temp_dirs()
1636
 
        
 
1644
 
1637
1645
    # # #
1638
1646
    # Events and tasks related to displaying preview images and thumbnails
1639
1647
    # # #
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()
1645
 
    
 
1653
 
1646
1654
    def on_preview_eventbox_button_press_event(self, widget, event):
1647
 
        
 
1655
 
1648
1656
        if event.type == gtk.gdk._2BUTTON_PRESS and event.button == 1:
1649
 
            self.show_thumbnails()    
1650
 
    
 
1657
            self.show_thumbnails()
 
1658
 
1651
1659
    def on_show_thumbnails_action_activate(self, action):
1652
1660
        logger.debug("on_show_thumbnails_action_activate")
1653
1661
        self.show_thumbnails()
1654
 
        
 
1662
 
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()
1658
 
        
 
1666
 
1659
1667
    def on_check_all_action_activate(self, action):
1660
1668
        self.thumbnails.check_all(check_all=True)
1661
 
        
 
1669
 
1662
1670
    def on_uncheck_all_action_activate(self, action):
1663
1671
        self.thumbnails.check_all(check_all=False)
1664
1672
 
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)
1668
 
        
 
1676
 
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)
1672
 
                                  
 
1680
 
1673
1681
    def on_quit_action_activate(self, action):
1674
1682
        self.on_rapidapp_destroy(widget=self.rapidapp, data=None)
1675
 
        
 
1683
 
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)
1680
 
                           
 
1688
 
1681
1689
    def on_get_help_action_activate(self, action):
1682
1690
        webbrowser.open("http://www.damonlynch.net/rapid/help.html")
1683
 
        
 
1691
 
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(
1687
1695
                                                                __version__))
1688
1696
        self.about.run()
1689
1697
        self.about.hide()
1690
 
        
 
1698
 
1691
1699
    def on_report_problem_action_activate(self, action):
1692
1700
        webbrowser.open("https://bugs.launchpad.net/rapid")
1693
 
        
 
1701
 
1694
1702
    def on_translate_action_activate(self, action):
1695
1703
        webbrowser.open("http://www.damonlynch.net/rapid/translate.html")
1696
 
     
 
1704
 
1697
1705
    def on_donate_action_activate(self, action):
1698
1706
        webbrowser.open("http://www.damonlynch.net/rapid/donate.html")
1699
 
             
 
1707
 
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)
1707
 
        
 
1715
 
1708
1716
    def update_preview_image(self, unique_id, image):
1709
1717
        self.preview_image.update_preview_image(unique_id, image)
1710
 
        
 
1718
 
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)
1717
 
        
1718
 
        
 
1725
 
 
1726
 
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)
1722
 
    
 
1730
 
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)
1726
 
        
 
1734
 
1727
1735
    def display_scan_thumbnails(self):
1728
1736
        """
1729
1737
        If all the scans are complete, sets the sort order and displays
1737
1745
    # # #
1738
1746
    # Volume management
1739
1747
    # # #
1740
 
    
 
1748
 
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) 
1746
 
    
1747
 
    
 
1753
            self.vmonitor.connect("mount-removed", self.on_mount_removed)
 
1754
 
 
1755
 
1748
1756
    def _backup_device_name(self, path):
1749
1757
        if self.backup_devices[path][0] is None:
1750
1758
            name = path
1751
1759
        else:
1752
1760
            name = self.backup_devices[path][0].get_name()
1753
1761
        return name
1754
 
        
 
1762
 
1755
1763
    def start_device_scan(self, device):
1756
1764
        """
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
1759
1767
        """
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])
1763
 
        
 
1772
 
1764
1773
    def confirm_manual_location(self):
1765
1774
        """
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.
1768
 
        
 
1777
 
1769
1778
        Returns True if yes or there was no need to ask the user, False if the
1770
1779
        user said no.
1771
1780
        """
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,
1784
 
                    use_markup=True)        
 
1793
                    use_markup=True)
1785
1794
            response = c.run()
1786
1795
            user_confirmed = response == gtk.RESPONSE_OK
1787
1796
            c.destroy()
1788
1797
            if not user_confirmed:
1789
1798
                return False
1790
1799
        return True
1791
 
    
 
1800
 
1792
1801
    def setup_devices(self, on_startup, on_preference_change, block_auto_start):
1793
1802
        """
1794
 
        
 
1803
 
1795
1804
        Setup devices from which to download from and backup to
1796
 
        
 
1805
 
1797
1806
        Sets up volumes for downloading from and backing up to
1798
 
        
1799
 
        on_startup should be True if the program is still starting, 
 
1807
 
 
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.
1801
 
        
 
1810
 
1802
1811
        on_preference_change should be True if this is being called as the
1803
1812
        result of a preference being changed
1804
 
        
 
1813
 
1805
1814
        block_auto_start should be True if automation options to automatically
1806
1815
        start a download should be ignored
1807
 
        
1808
 
        Removes any image media that are currently not downloaded, 
1809
 
        or finished downloading        
 
1816
 
 
1817
        Removes any image media that are currently not downloaded,
 
1818
        or finished downloading
1810
1819
        """
1811
 
        
 
1820
 
1812
1821
        if self.using_volume_monitor():
1813
1822
            self.start_volume_monitor()
1814
1823
 
1816
1825
        if not self.prefs.device_autodetection:
1817
1826
            if not self.confirm_manual_location():
1818
1827
                return
1819
 
            
 
1828
 
1820
1829
        mounts = []
1821
1830
        self.backup_devices = {}
1822
 
        
 
1831
 
1823
1832
        if self.using_volume_monitor():
1824
1833
            # either using automatically detected backup devices
1825
1834
            # or download devices
1827
1836
                if not mount.is_shadowed():
1828
1837
                    path = mount.get_root().get_path()
1829
1838
                    if path:
1830
 
                        if (path in self.prefs.device_blacklist and 
 
1839
                        if (path in self.prefs.device_blacklist and
1831
1840
                                    self.search_for_PSD()):
1832
1841
                            logger.info("%s ignored", mount.get_name())
1833
1842
                        else:
1835
1844
                            is_backup_mount, backup_file_type = self.check_if_backup_mount(path)
1836
1845
                            if is_backup_mount:
1837
1846
                                self.backup_devices[path] = (mount, backup_file_type)
1838
 
                            elif (self.prefs.device_autodetection and 
1839
 
                                 (dv.is_DCIM_device(path) or 
 
1847
                            elif (self.prefs.device_autodetection and
 
1848
                                 (dv.is_DCIM_device(path) or
1840
1849
                                  self.search_for_PSD())):
1841
1850
                                logger.debug("Appending %s", mount.get_name())
1842
1851
                                mounts.append((path, mount))
1843
1852
                            else:
1844
1853
                                logger.debug("Ignoring %s", mount.get_name())
1845
 
                    
1846
 
        
 
1854
 
 
1855
 
1847
1856
        if not self.prefs.device_autodetection:
1848
 
            # user manually specified the path from which to download 
 
1857
            # user manually specified the path from which to download
1849
1858
            path = self.prefs.device_location
1850
1859
            if path:
1851
1860
                logger.info("Using manually specified path %s", path)
1858
1867
            if not self.prefs.backup_device_autodetection:
1859
1868
                self._setup_manual_backup()
1860
1869
            self._add_backup_devices()
1861
 
                
 
1870
 
1862
1871
        self.update_no_backup_devices()
1863
 
        
 
1872
 
1864
1873
        # Display amount of free space in a status bar message
1865
1874
        self.display_free_space()
1866
 
        
 
1875
 
1867
1876
        if block_auto_start:
1868
1877
            self.auto_start_is_on = False
1869
1878
        else:
1870
1879
            self.auto_start_is_on = ((not on_preference_change) and
1871
 
                                    ((self.prefs.auto_download_at_startup and 
1872
 
                                      on_startup) or 
 
1880
                                    ((self.prefs.auto_download_at_startup and
 
1881
                                      on_startup) or
1873
1882
                                      (self.prefs.auto_download_upon_device_insertion and
1874
1883
                                       not on_startup)))
1875
 
        
 
1884
 
 
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)
1883
 
            else:
1884
 
                scan_pid = self.start_device_scan(device)
1885
 
                if mount is not None:
1886
 
                    self.mounts_by_path[path] = scan_pid
 
1889
 
 
1890
 
 
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)
 
1896
                else:
 
1897
                    scan_pid = self.start_device_scan(device)
 
1898
                    if mount is not None:
 
1899
                        self.mounts_by_path[path] = scan_pid
1887
1900
        if not mounts:
1888
1901
            self.set_download_action_sensitivity()
1889
 
        
 
1902
 
 
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)
 
1907
            return True
 
1908
        else:
 
1909
            return False
 
1910
 
1890
1911
    def _setup_manual_backup(self):
1891
1912
        """
1892
1913
        Setup backup devices that the user has manually specified.
1894
1915
        video backup will either be the same or they will differ.
1895
1916
        """
1896
1917
        # user manually specified backup locations
1897
 
        # will backup to these paths, but don't need any volume info 
1898
 
        # associated with them        
 
1918
        # will backup to these paths, but don't need any volume info
 
1919
        # associated with them
1899
1920
        self.backup_devices[self.prefs.backup_location] = (None, PHOTO_BACKUP)
1900
1921
        if DOWNLOAD_VIDEO:
1901
1922
            if self.prefs.backup_location <> self.prefs.backup_video_location:
1908
1929
                logger.info("Backing up photos and videos to %s", self.prefs.backup_location)
1909
1930
        else:
1910
1931
            logger.info("Backing up photos to %s", self.prefs.backup_location)
1911
 
            
 
1932
 
1912
1933
    def _add_backup_devices(self):
1913
1934
        """
1914
1935
        Add each backup devices / path to backup manager
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)
1920
 
        
1921
 
        
1922
 
    def get_use_device(self, device):  
 
1941
 
 
1942
 
 
1943
    def get_use_device(self, device):
1923
1944
        """ Prompt user whether or not to download from this device """
1924
 
        
 
1945
 
1925
1946
        logger.info("Prompting whether to use %s", device.get_name())
 
1947
 
 
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.
 
1952
 
 
1953
        self.mounts_by_path[device.get_path()] = "PROMPTING"
 
1954
 
1926
1955
        d = dv.UseDeviceDialog(self.rapidapp, device, self.got_use_device)
1927
 
        
 
1956
 
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()
1931
 
        
 
1960
 
1932
1961
        path = device.get_path()
1933
 
        
 
1962
 
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
1944
 
            
 
1973
 
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]
1949
1978
            else:
1950
 
                self.prefs.device_blacklist = [path]    
1951
 
     
 
1979
                self.prefs.device_blacklist = [path]
 
1980
 
1952
1981
    def search_for_PSD(self):
1953
1982
        """
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
1956
1985
        """
1957
1986
        return self.prefs.device_autodetection_psd and self.prefs.device_autodetection
1958
1987
 
1959
1988
    def check_if_backup_mount(self, path):
1960
1989
        """
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.
1963
 
        
 
1992
 
1964
1993
        Checks against user preferences.
1965
 
        
 
1994
 
1966
1995
        Returns a tuple:
1967
1996
        (True, <backup-type> (one of PHOTO_VIDEO_BACKUP, PHOTO_BACKUP, or VIDEO_BACKUP)) or
1968
 
        (False, None) 
 
1997
        (False, None)
1969
1998
        """
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)
1991
2020
                    logger.info("Videos will be backed up to %s", path)
1992
2021
                    return (True, VIDEO_BACKUP)
1993
2022
            elif path == self.prefs.backup_location:
1994
 
                # user manually specified the path    
 
2023
                # user manually specified the path
1995
2024
                if os.access(self.prefs.backup_location, os.W_OK):
1996
2025
                    return (True, PHOTO_BACKUP)
1997
2026
            elif path == self.prefs.backup_video_location:
2013
2042
                #both videos and photos are backed up to this device / path
2014
2043
                self.no_photo_backup_devices += 1
2015
2044
                self.no_video_backup_devices += 1
2016
 
        logger.info("# photo backup devices: %s; # video backup devices: %s", 
 
2045
        logger.info("# photo backup devices: %s; # video backup devices: %s",
2017
2046
                    self.no_photo_backup_devices, self.no_video_backup_devices)
2018
 
        self.download_tracker.set_no_backup_devices(self.no_photo_backup_devices, 
 
2047
        self.download_tracker.set_no_backup_devices(self.no_photo_backup_devices,
2019
2048
                                                    self.no_video_backup_devices)
2020
2049
 
2021
2050
    def refresh_backup_media(self):
2022
2051
        """
2023
2052
        Setup the backup media
2024
 
        
2025
 
        Assumptions: this is being called after the user has changed their 
 
2053
 
 
2054
        Assumptions: this is being called after the user has changed their
2026
2055
        preferences AND download media has already been setup
2027
2056
        """
2028
 
        
 
2057
 
2029
2058
        # terminate any running backup processes
2030
2059
        self.backup_manager.request_termination()
2031
 
        
 
2060
 
2032
2061
        self.backup_devices = {}
2033
2062
        if self.prefs.backup_images:
2034
2063
            if not self.prefs.backup_device_autodetection:
2043
2072
                                # is a backup volume
2044
2073
                                if path not in self.backup_devices:
2045
2074
                                    self.backup_devices[path] = (mount, backup_file_type)
2046
 
            
 
2075
 
2047
2076
            self._add_backup_devices()
2048
2077
 
2049
2078
        self.update_no_backup_devices()
2050
2079
        self.display_free_space()
2051
 
            
 
2080
 
2052
2081
    def using_volume_monitor(self):
2053
2082
        """
2054
2083
        Returns True if programs needs to use gio volume monitor
2055
2084
        """
2056
 
        
2057
 
        return (self.prefs.device_autodetection or 
2058
 
                (self.prefs.backup_images and 
 
2085
 
 
2086
        return (self.prefs.device_autodetection or
 
2087
                (self.prefs.backup_images and
2059
2088
                self.prefs.backup_device_autodetection
2060
2089
                ))
2061
 
                    
 
2090
 
2062
2091
    def on_mount_added(self, vmonitor, mount):
2063
2092
        """
2064
2093
        callback run when gio indicates a new volume
2069
2098
        if mount.is_shadowed():
2070
2099
            # ignore this type of mount
2071
2100
            return
2072
 
            
 
2101
 
2073
2102
        path = mount.get_root().get_path()
2074
2103
        if path is not None:
2075
2104
 
2078
2107
                            'device': mount.get_name(), 'path': path})
2079
2108
            else:
2080
2109
                is_backup_mount, backup_file_type = self.check_if_backup_mount(path)
2081
 
                            
 
2110
 
2082
2111
                if is_backup_mount:
2083
2112
                    if path not in self.backup_devices:
2084
2113
                        self.backup_devices[path] = mount
2087
2116
                        self.update_no_backup_devices()
2088
2117
                        self.display_free_space()
2089
2118
 
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()):
2092
 
                    
 
2121
 
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)
2098
 
                    else:   
2099
 
                        scan_pid = self.start_device_scan(device)
2100
 
                        self.mounts_by_path[path] = scan_pid
2101
 
            
 
2124
 
 
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)
 
2129
                        else:
 
2130
                            scan_pid = self.start_device_scan(device)
 
2131
                            self.mounts_by_path[path] = scan_pid
 
2132
 
2102
2133
    def on_mount_removed(self, vmonitor, mount):
2103
2134
        """
2104
2135
        callback run when gio indicates a new volume
2105
2136
        has been mounted
2106
2137
        """
2107
 
        
 
2138
 
2108
2139
        path = mount.get_root().get_path()
2109
2140
 
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
2114
 
        
 
2145
 
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
2119
 
            
2120
 
            self.thumbnails.clear_all(scan_pid = scan_pid, 
 
2150
 
 
2151
            self.thumbnails.clear_all(scan_pid = scan_pid,
2121
2152
                                      keep_downloaded_files = True)
2122
2153
            self.device_collection.remove_device(scan_pid)
2123
 
            
2124
 
                
2125
 
                    
 
2154
 
 
2155
 
 
2156
 
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()
2132
 
                
 
2163
 
2133
2164
        # may need to disable download button and menu
2134
2165
        self.set_download_action_sensitivity()
2135
 
            
 
2166
 
2136
2167
    def clear_non_running_downloads(self):
2137
2168
        """
2138
2169
        Clears the display of downloads that are currently not running
2139
2170
        """
2140
 
        
 
2171
 
2141
2172
        # Stop any processes currently scanning or creating thumbnails
2142
2173
        self._terminate_processes(terminate_file_copies=False)
2143
 
        
 
2174
 
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)
2149
 
            
2150
 
        
2151
 
 
2152
 
    
 
2180
 
 
2181
 
 
2182
 
 
2183
 
2153
2184
    # # #
2154
2185
    # Download and help buttons, and menu items
2155
2186
    # # #
2156
 
    
 
2187
 
2157
2188
    def on_download_action_activate(self, action):
2158
2189
        """
2159
2190
        Called when a download is activated
2160
2191
        """
2161
 
        
 
2192
 
2162
2193
        if self.copy_files_manager.paused:
2163
2194
            logger.debug("Download resumed")
2164
2195
            self.resume_download()
2165
2196
        else:
2166
2197
            logger.debug("Download activated")
2167
 
            
 
2198
 
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()
2173
2204
            else:
2174
2205
                self.pause_download()
2175
2206
 
2176
 
    
 
2207
 
2177
2208
    def on_help_action_activate(self, action):
2178
2209
        webbrowser.open("http://www.damonlynch.net/rapid/documentation")
2179
 
        
 
2210
 
2180
2211
    def on_preferences_action_activate(self, action):
2181
2212
 
2182
2213
        preferencesdialog.PreferencesDialog(self)
2183
 
        
 
2214
 
2184
2215
    def set_download_action_sensitivity(self):
2185
2216
        """
2186
2217
        Sets sensitivity of Download action to enable or disable it
2187
 
        
 
2218
 
2188
2219
        Affects download button and menu item
2189
2220
        """
2190
2221
        if not self.download_is_occurring():
2192
2223
            if self.scan_manager.no_tasks == 0:
2193
2224
                if self.thumbnails.files_are_checked_to_download():
2194
2225
                    sensitivity = True
2195
 
                    
 
2226
 
2196
2227
            self.download_action.set_sensitive(sensitivity)
2197
 
            
 
2228
 
2198
2229
    def set_download_action_label(self, is_download):
2199
2230
        """
2200
 
        Toggles label betwen pause and download 
 
2231
        Toggles label betwen pause and download
2201
2232
        """
2202
 
        
 
2233
 
2203
2234
        if is_download:
2204
2235
            self.download_action.set_label(_("Download"))
2205
2236
            self.download_action_is_download = True
2206
2237
        else:
2207
2238
            self.download_action.set_label(_("Pause"))
2208
2239
            self.download_action_is_download = False
2209
 
    
 
2240
 
2210
2241
    # # #
2211
2242
    # Job codes
2212
2243
    # # #
2213
 
    
2214
 
    
 
2244
 
 
2245
 
2215
2246
    def _init_job_code(self):
2216
2247
        self.job_code = self.last_chosen_job_code = ''
2217
2248
        self.need_job_code_for_naming = self.prefs.any_pref_uses_job_code()
2219
2250
 
2220
2251
    def assign_job_code(self, code):
2221
2252
        """ assign job code (which may be empty) to member variable and update user preferences
2222
 
        
 
2253
 
2223
2254
        Update preferences only if code is not empty. Do not duplicate job code.
2224
2255
        """
2225
2256
 
2226
2257
        self.job_code = code
2227
 
        
 
2258
 
2228
2259
        if 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)
2233
 
            
 
2264
 
2234
2265
            jcs = self.prefs.job_codes
2235
2266
            while code in jcs:
2236
2267
                jcs.remove(code)
2237
 
                
 
2268
 
2238
2269
            self.prefs.job_codes = [code] + jcs
2239
2270
 
2240
2271
    def _get_job_code(self, post_job_code_entry_callback):
2241
2272
        """ prompt for a job code """
2242
 
        
 
2273
 
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)
2251
2282
        else:
2252
2283
            logger.debug("Already prompting for Job Code, do not prompt again")
2253
 
        
 
2284
 
2254
2285
    def get_job_code(self):
2255
2286
        self._get_job_code(self.got_job_code)
2256
 
        
 
2287
 
2257
2288
    def got_job_code(self, dialog, user_chose_code, code):
2258
2289
        dialog.destroy()
2259
2290
        self.prompting_for_job_code = False
2260
 
        
 
2291
 
2261
2292
        if user_chose_code:
2262
2293
            if code is None:
2263
2294
                code = ''
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() 
2268
 
                
 
2298
            self.start_download()
 
2299
 
2269
2300
        else:
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
2274
 
   
2275
 
    
 
2305
 
 
2306
 
2276
2307
    # # #
2277
2308
    # Download
2278
2309
    # # #
2279
 
    
 
2310
 
2280
2311
    def _init_download_tracking(self):
2281
2312
        """
2282
2313
        Initialize variables to track downloads
2285
2316
        # (Scan id acts as an index to each device. A device could be scanned
2286
2317
        #  more than once).
2287
2318
        self.download_tracker = downloadtracker.DownloadTracker()
2288
 
        
 
2319
 
2289
2320
        # Track which temporary directories are created when downloading files
2290
2321
        self.temp_dirs_by_scan_pid = dict()
2291
 
        
 
2322
 
2292
2323
        # Track which downloads are running
2293
2324
        self.download_active_by_scan_pid = []
2294
 
        
 
2325
 
2295
2326
    def modify_files_during_download(self):
2296
2327
        """ Returns True if there is a need to modify files during download"""
2297
2328
        return self.prefs.auto_rotate_jpeg or (self.focal_length is not None)
2298
2329
 
2299
 
    
 
2330
 
2300
2331
    def start_download(self, scan_pid=None):
2301
2332
        """
2302
2333
        Start download, renaming and backup of files.
2303
 
        
 
2334
 
2304
2335
        If scan_pid is specified, only files matching it will be downloaded
2305
2336
        """
2306
 
        
 
2337
 
2307
2338
        files_by_scan_pid = self.thumbnails.get_files_checked_for_download(scan_pid)
2308
2339
        folders_valid, invalid_dirs = self.check_download_folder_validity(files_by_scan_pid)
2309
 
        
 
2340
 
2310
2341
        if not folders_valid:
2311
2342
            if len(invalid_dirs) > 1:
2312
2343
                msg = _("These download folders are invalid:\n%(folder1)s\n%(folder2)s") % {
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()
2323
2354
 
2324
 
            # Set status to download pending 
 
2355
            # Set status to download pending
2325
2356
            self.thumbnails.mark_download_pending(files_by_scan_pid)
2326
 
            
 
2357
 
2327
2358
            # disable refresh and preferences change while download is occurring
2328
2359
            self.enable_prefs_and_refresh(enabled=False)
2329
 
            
 
2360
 
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)
2335
 
                
 
2366
 
2336
2367
                self.download_files(files, scan_pid)
2337
 
                
 
2368
 
2338
2369
            self.set_download_action_label(is_download = False)
2339
 
        
 
2370
 
2340
2371
    def pause_download(self):
2341
 
        
 
2372
 
2342
2373
        self.copy_files_manager.pause()
2343
 
        
 
2374
 
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)
2347
 
            
 
2378
 
2348
2379
        self.time_check.pause()
2349
 
            
 
2380
 
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)
2353
 
        
 
2384
 
2354
2385
        self.time_check.set_download_mark()
2355
 
            
 
2386
 
2356
2387
        self.copy_files_manager.start()
2357
2388
 
2358
2389
    def download_files(self, files, scan_pid):
2360
2391
        Initiate downloading and renaming of files
2361
2392
        """
2362
2393
        # Check which file types will be downloaded for this particular process
2363
 
        no_photos_to_download = self.files_of_type_present(files, 
2364
 
                                                    rpdfile.FILE_TYPE_PHOTO, 
 
2394
        no_photos_to_download = self.files_of_type_present(files,
 
2395
                                                    rpdfile.FILE_TYPE_PHOTO,
2365
2396
                                                    return_file_count=True)
2366
2397
        if no_photos_to_download:
2367
2398
            photo_download_folder = self.prefs.download_folder
2368
2399
        else:
2369
2400
            photo_download_folder = None
2370
 
            
 
2401
 
2371
2402
        if DOWNLOAD_VIDEO:
2372
 
            no_videos_to_download = self.files_of_type_present(files, 
 
2403
            no_videos_to_download = self.files_of_type_present(files,
2373
2404
                                        rpdfile.FILE_TYPE_VIDEO,
2374
2405
                                        return_file_count=True)
2375
2406
            if no_videos_to_download:
2379
2410
        else:
2380
2411
            video_download_folder = None
2381
2412
            no_videos_to_download = 0
2382
 
        
 
2413
 
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)
2389
 
        
2390
 
        
 
2420
 
 
2421
 
2391
2422
        download_size = photo_download_size + video_download_size
2392
 
        
 
2423
 
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))
2396
 
            
 
2427
 
2397
2428
        self.time_remaining.set(scan_pid, download_size)
2398
2429
        self.time_check.set_download_mark()
2399
 
            
 
2430
 
2400
2431
        self.download_active_by_scan_pid.append(scan_pid)
2401
 
        
2402
 
        
 
2432
 
 
2433
 
2403
2434
        if len(self.download_active_by_scan_pid) > 1:
2404
2435
            self.display_summary_notification = True
2405
 
            
 
2436
 
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
2414
2445
        else:
2415
2446
            modify_pipe = None
2416
2447
 
2417
 
            
 
2448
 
2418
2449
        # Initiate copy files process
2419
 
        self.copy_files_manager.add_task((photo_download_folder, 
 
2450
        self.copy_files_manager.add_task((photo_download_folder,
2420
2451
                              video_download_folder, scan_pid,
2421
2452
                              files, modify_files_during_download,
2422
2453
                              modify_pipe))
2423
 
                              
 
2454
 
2424
2455
    def copy_files_results(self, source, condition):
2425
2456
        """
2426
2457
        Handle results from copy files process
2434
2465
            if msg_type == rpdmp.MSG_TEMP_DIRS:
2435
2466
                scan_pid, photo_temp_dir, video_temp_dir = data
2436
2467
                self.temp_dirs_by_scan_pid[scan_pid] = (photo_temp_dir, video_temp_dir)
2437
 
                
 
2468
 
2438
2469
                # Report which temporary directories are being used for this
2439
2470
                # download
2440
2471
                if photo_temp_dir and video_temp_dir:
2445
2476
                                photo_temp_dir)
2446
2477
                else:
2447
2478
                    logger.debug("Using temp dir %s (videos)",
2448
 
                                video_temp_dir)                    
 
2479
                                video_temp_dir)
2449
2480
            elif msg_type == rpdmp.MSG_BYTES:
2450
2481
                scan_pid, total_downloaded, chunk_downloaded = data
2451
 
                self.download_tracker.set_total_bytes_copied(scan_pid, 
 
2482
                self.download_tracker.set_total_bytes_copied(scan_pid,
2452
2483
                                                             total_downloaded)
2453
2484
                self.time_check.increment(bytes_downloaded=chunk_downloaded)
2454
2485
                percent_complete = self.download_tracker.get_percent_complete(scan_pid)
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)
2460
 
                
 
2491
 
2461
2492
            return True
2462
2493
        else:
2463
2494
            # Process is complete, i.e. conn_type == rpdmp.CONN_COMPLETE
2464
2495
            connection.close()
2465
2496
            return False
2466
 
            
 
2497
 
2467
2498
 
2468
2499
    def copy_file_results_single_file(self, data):
2469
2500
        """
2470
2501
        Handles results from one of two processes:
2471
2502
        1. copy_files
2472
2503
        2. file_modify
2473
 
        
 
2504
 
2474
2505
        Operates after a single file has been copied from the download device
2475
2506
        to the local folder.
2476
 
        
 
2507
 
2477
2508
        Calls the process to rename files and create subfolders (subfolderfile)
2478
2509
        """
2479
 
        
 
2510
 
2480
2511
        download_succeeded, rpd_file, download_count, temp_full_file_name, thumbnail_icon, thumbnail = data
2481
 
        
 
2512
 
2482
2513
        if thumbnail is not None or thumbnail_icon is not None:
2483
 
            self.thumbnails.update_thumbnail((rpd_file.unique_id, 
2484
 
                                              thumbnail_icon, 
 
2514
            self.thumbnails.update_thumbnail((rpd_file.unique_id,
 
2515
                                              thumbnail_icon,
2485
2516
                                              thumbnail))
2486
 
        
 
2517
 
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
2492
 
        
 
2523
 
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)
2498
2529
            rpd_file.download_conflict_resolution = self.prefs.download_conflict_resolution
2499
2530
            rpd_file.synchronize_raw_jpg = self.prefs.must_synchronize_raw_jpg()
2500
2531
            rpd_file.job_code = self.job_code
2501
 
            
 
2532
 
2502
2533
            self.subfolder_file_manager.rename_file_and_move_to_subfolder(
2503
 
                    download_succeeded, 
2504
 
                    download_count, 
 
2534
                    download_succeeded,
 
2535
                    download_count,
2505
2536
                    rpd_file
2506
 
                    )         
 
2537
                    )
2507
2538
    def file_modify_results(self, source, condition):
2508
2539
        """
2509
 
        'file modify' is a process that runs immediately after 'copy files', 
2510
 
        meaning there can be more than one at one time. 
2511
 
        
 
2540
        'file modify' is a process that runs immediately after 'copy files',
 
2541
        meaning there can be more than one at one time.
 
2542
 
2512
2543
        It runs before the renaming process.
2513
2544
        """
2514
2545
        connection = self.file_modify_manager.get_pipe(source)
2515
 
        
 
2546
 
2516
2547
        conn_type, data = connection.recv()
2517
2548
        if conn_type == rpdmp.CONN_PARTIAL:
2518
2549
            self.copy_file_results_single_file(data)
2520
2551
        else:
2521
2552
            # Process is complete, i.e. conn_type == rpdmp.CONN_COMPLETE
2522
2553
            connection.close()
2523
 
            return False            
2524
 
 
2525
 
    
 
2554
            return False
 
2555
 
 
2556
 
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
2528
2559
           backed up
2529
2560
        """
2530
2561
        return not len(self.download_active_by_scan_pid) == 0
2531
 
    
 
2562
 
2532
2563
    # # #
2533
2564
    # Create folder and file names for downloaded files
2534
2565
    # # #
2535
 
    
2536
 
    def subfolder_file_results(self, move_succeeded, rpd_file):
 
2566
 
 
2567
    def subfolder_file_results(self, move_succeeded, rpd_file, download_count):
2537
2568
        """
2538
2569
        Handle results of subfolder creation and file renaming
2539
2570
        """
2540
 
            
 
2571
 
2541
2572
        scan_pid = rpd_file.scan_pid
2542
2573
        unique_id = rpd_file.unique_id
2543
 
        
 
2574
 
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)
2547
 
                           
 
2578
 
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
2554
2585
            else:
2555
2586
                path_suffix = None
2556
 
            
2557
 
            self.backup_manager.backup_file(move_succeeded, rpd_file, 
 
2587
 
 
2588
            self.backup_manager.backup_file(move_succeeded, rpd_file,
2558
2589
                                    path_suffix,
2559
 
                                    self.prefs.backup_duplicate_overwrite)
 
2590
                                    self.prefs.backup_duplicate_overwrite,
 
2591
                                    download_count)
2560
2592
        else:
2561
2593
            self.file_download_finished(move_succeeded, rpd_file)
2562
 
     
2563
 
    
 
2594
 
 
2595
 
2564
2596
    def multiple_backup_devices(self, file_type):
2565
2597
        """Returns true if more than one backup device is being used for that
2566
2598
        file type
2567
2599
        """
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))
2572
 
                                 
 
2604
 
2573
2605
    def backup_results(self, source, condition):
2574
2606
        """
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
2581
 
            
 
2613
 
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,
2589
2621
                                            None, None)
2590
2622
                self.time_remaining.update(scan_pid, bytes_downloaded=chunk_downloaded)
2591
 
                
 
2623
 
2592
2624
            elif msg_type == rpdmp.MSG_FILE:
2593
2625
                backup_succeeded, rpd_file = data
2594
 
                
 
2626
 
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)
2604
 
                
 
2636
 
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):
2610
2642
        else:
2611
2643
            return False
2612
2644
 
2613
 
    
 
2645
 
2614
2646
    def file_download_finished(self, succeeded, rpd_file):
2615
2647
        """
2616
2648
        Called when a file has been downloaded i.e. copied, renamed, and backed up
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
2626
2658
            # completes
2627
2659
            self.download_tracker.add_to_auto_delete(rpd_file)
2628
 
                           
 
2660
 
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)
2633
 
                                                        
 
2665
 
2634
2666
        completed, files_remaining = self._update_file_download_device_progress(scan_pid, unique_id, rpd_file.file_type)
2635
 
        
 
2667
 
2636
2668
        if self.download_is_occurring():
2637
2669
            self.update_time_remaining()
2638
 
                
 
2670
 
2639
2671
        if completed:
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)
2652
 
            
2653
 
            
 
2684
 
 
2685
 
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)
2659
 
                
 
2691
 
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)
2664
2696
 
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()
2670
 
                        
 
2702
 
2671
2703
                self.download_tracker.purge_all()
2672
2704
                self.speed_label.set_label(" ")
2673
 
                                        
 
2705
 
2674
2706
                self.display_free_space()
2675
 
                
 
2707
 
2676
2708
                self.set_download_action_label(is_download=True)
2677
2709
                self.set_download_action_sensitivity()
2678
 
                
 
2710
 
2679
2711
                self.job_code = ''
2680
2712
                self.download_start_time = None
2681
 
        
2682
 
        
 
2713
 
 
2714
 
2683
2715
    def update_time_remaining(self):
2684
2716
        update, download_speed = self.time_check.check_for_update()
2685
2717
        if update:
2686
2718
            self.speed_label.set_text(download_speed)
2687
 
            
 
2719
 
2688
2720
            time_remaining = self.time_remaining.time_remaining()
2689
2721
            if time_remaining:
2690
2722
                secs =  int(time_remaining)
2691
 
            
 
2723
 
2692
2724
                if secs == 0:
2693
2725
                    message = ""
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")
2700
2732
                else:
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}
2705
 
                
 
2737
 
2706
2738
                self.rapid_statusbar.pop(self.statusbar_context_id)
2707
 
                self.rapid_statusbar.push(self.statusbar_context_id, message)         
2708
 
            
 
2739
                self.rapid_statusbar.push(self.statusbar_context_id, message)
 
2740
 
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):
2714
2746
                f.delete(cancellable=None)
2715
2747
            except gio.Error, inst:
2716
2748
                logger.error("Failure deleting file %s", file)
2717
 
                logger.error(inst)            
2718
 
    
 
2749
                logger.error(inst)
 
2750
 
2719
2751
    def file_types_by_number(self, no_photos, no_videos):
2720
 
        """ 
 
2752
        """
2721
2753
        returns a string to be displayed to the user that can be used
2722
2754
        to show if a value refers to photos or videos or both, or just one
2723
2755
        of each
2740
2772
 
2741
2773
    def notify_downloaded_from_device(self, scan_pid):
2742
2774
        device = self.device_collection.get_device(scan_pid)
2743
 
        
 
2775
 
2744
2776
        if device.mount is None:
2745
2777
            notification_name = PROGRAM_NAME
2746
2778
            icon = self.application_icon
2747
2779
        else:
2748
2780
            notification_name  = device.get_name()
2749
2781
            icon = device.get_icon(self.notification_icon_size)
2750
 
        
 
2782
 
2751
2783
        no_photos_downloaded = self.download_tracker.get_no_files_downloaded(
2752
2784
                                            scan_pid, rpdfile.FILE_TYPE_PHOTO)
2753
2785
        no_videos_downloaded = self.download_tracker.get_no_files_downloaded(
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)
2762
 
                                            
 
2794
 
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}
2767
 
        
 
2799
 
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}
2770
 
        
 
2802
 
2771
2803
        if no_warnings:
2772
 
            message = "%s\n%s " % (message,  no_warnings) + _("warnings") 
2773
 
  
2774
 
        n = pynotify.Notification(notification_name, message)
2775
 
        n.set_icon_from_pixbuf(icon)
2776
 
        
2777
 
        n.show()
2778
 
    
 
2804
            message = "%s\n%s " % (message,  no_warnings) + _("warnings")
 
2805
 
 
2806
        if use_pynotify:
 
2807
            n = pynotify.Notification(notification_name, message)
 
2808
            n.set_icon_from_pixbuf(icon)
 
2809
 
 
2810
            n.show()
 
2811
 
2779
2812
    def notify_download_complete(self):
2780
2813
        if self.display_summary_notification:
2781
2814
            message = _("All downloads complete")
2782
 
            
 
2815
 
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}}
2791
 
            
 
2824
 
2792
2825
            # photo failures
2793
2826
            photo_failures = self.download_tracker.total_photo_failures
2794
2827
            if photo_failures:
2797
2830
                            {'number': photo_failures,
2798
2831
                            'numberdownloaded': _("%(filetype)s failed to download") % \
2799
2832
                            {'filetype': filetype}}
2800
 
                            
 
2833
 
2801
2834
            # video downloads
2802
2835
            video_downloads = self.download_tracker.total_videos_downloaded
2803
2836
            if video_downloads:
2804
2837
                filetype = self.file_types_by_number(0, video_downloads)
2805
2838
                message += "\n" + _("%(number)s %(numberdownloaded)s") % \
2806
 
                            {'number': video_downloads, 
 
2839
                            {'number': video_downloads,
2807
2840
                            'numberdownloaded': _("%(filetype)s downloaded") % \
2808
2841
                            {'filetype': filetype}}
2809
 
                            
 
2842
 
2810
2843
            # video failures
2811
2844
            video_failures = self.download_tracker.total_video_failures
2812
2845
            if video_failures:
2815
2848
                            {'number': video_failures,
2816
2849
                            'numberdownloaded': _("%(filetype)s failed to download") % \
2817
2850
                            {'filetype': filetype}}
2818
 
            
 
2851
 
2819
2852
            # warnings
2820
 
            warnings = self.download_tracker.total_warnings 
 
2853
            warnings = self.download_tracker.total_warnings
2821
2854
            if warnings:
2822
2855
                message += "\n" + _("%(number)s %(numberdownloaded)s") % \
2823
 
                            {'number': warnings, 
 
2856
                            {'number': warnings,
2824
2857
                            'numberdownloaded': _("warnings")}
2825
 
                            
2826
 
            n = pynotify.Notification(PROGRAM_NAME, message)
2827
 
            n.set_icon_from_pixbuf(self.application_icon)
2828
 
            n.show()
 
2858
 
 
2859
            if use_pynotify:
 
2860
                n = pynotify.Notification(PROGRAM_NAME, message)
 
2861
                n.set_icon_from_pixbuf(self.application_icon)
 
2862
                n.show()
2829
2863
            self.display_summary_notification = False # don't show it again unless needed
2830
 
      
2831
 
        
 
2864
 
 
2865
 
2832
2866
    def _update_file_download_device_progress(self, scan_pid, unique_id, file_type):
2833
2867
        """
2834
2868
        Increments the progress bar for an individual device
2835
 
        
 
2869
 
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
2839
2873
        """
2840
 
        
 
2874
 
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)
2847
 
        
 
2881
 
2848
2882
        if completed:
2849
2883
            files_remaining = self.thumbnails.get_no_files_remaining(scan_pid)
2850
2884
        else:
2851
2885
            files_remaining = 0
2852
 
                    
 
2886
 
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}
2860
2894
        else:
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)
2871
 
        
 
2905
 
2872
2906
        percent_complete = self.download_tracker.get_overall_percent_complete()
2873
2907
        self.download_progressbar.set_fraction(percent_complete)
2874
 
                                        
 
2908
 
2875
2909
        return (completed, files_remaining)
2876
 
        
 
2910
 
2877
2911
 
2878
2912
    def _clean_all_temp_dirs(self):
2879
2913
        """
2882
2916
        for scan_pid in self.temp_dirs_by_scan_pid:
2883
2917
            for temp_dir in self.temp_dirs_by_scan_pid[scan_pid]:
2884
2918
                self._purge_dir(temp_dir)
2885
 
                
 
2919
 
2886
2920
        self.temp_dirs_by_scan_pid = {}
2887
 
            
2888
 
    
 
2921
 
 
2922
 
2889
2923
    def _clean_temp_dirs_for_scan_pid(self, scan_pid):
2890
2924
        """
2891
2925
        Deletes temp files and folders used in download
2897
2931
    def _purge_dir(self, directory):
2898
2932
        """
2899
2933
        Deletes all files in the directory, and the directory itself.
2900
 
        
 
2934
 
2901
2935
        Does not recursively traverse any subfolders in the directory.
2902
2936
        """
2903
 
        
 
2937
 
2904
2938
        if directory:
2905
2939
            try:
2906
2940
                path = gio.File(directory)
2916
2950
            except gio.Error, inst:
2917
2951
                logger.error("Failure deleting temporary folder %s", directory)
2918
2952
                logger.error(inst)
2919
 
    
2920
 
    
2921
 
    # # # 
 
2953
 
 
2954
 
 
2955
    # # #
2922
2956
    # Preferences
2923
2957
    # # #
2924
 
        
2925
 
        
2926
 
    def _init_prefs(self, auto_detect, device_location): 
 
2958
 
 
2959
 
 
2960
    def _init_prefs(self, auto_detect, device_location):
2927
2961
        self.prefs = prefsrapid.RapidPreferences()
2928
 
        
 
2962
 
2929
2963
        # handle device preferences set from the command line
2930
2964
        # do this before preference changes are handled with notify_add
2931
2965
        if auto_detect:
2933
2967
        elif device_location:
2934
2968
            self.prefs.device_location = device_location
2935
2969
            self.prefs.device_autodetection = False
2936
 
                    
 
2970
 
2937
2971
        self.prefs.notify_add(self.on_preference_changed)
2938
 
        
2939
 
        # flag to indicate whether the user changed some preferences that 
 
2972
 
 
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
2943
 
        
2944
 
        # flag to indicate that the preferences dialog window is being 
 
2977
 
 
2978
        # flag to indicate that the preferences dialog window is being
2945
2979
        # displayed to the user
2946
2980
        self.preferences_dialog_displayed = False
2947
2981
 
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
2951
 
        
2952
 
        # these values are used to track the number of backup devices / 
 
2985
 
 
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
2956
 
        
 
2990
 
2957
2991
        self.downloads_today_tracker = self.prefs.get_downloads_today_tracker()
2958
 
        
 
2992
 
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)
2962
2996
        else:
2963
 
            logger.info("No downloads have occurred so far today")        
 
2997
            logger.info("No downloads have occurred so far today")
2964
2998
 
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())
2976
 
        
 
3010
 
2977
3011
        self.check_prefs_upgrade(__version__)
2978
3012
        self.prefs.program_version = __version__
2979
 
        
 
3013
 
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()    
2984
 
    
 
3017
        self.uses_sequence_letter_value.value = self.prefs.any_pref_uses_sequence_letter_value()
 
3018
 
2985
3019
    def check_prefs_upgrade(self, running_version):
2986
3020
        """
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.
2989
 
        
 
3023
 
2990
3024
        If the version is different, the preferences are checked to see
2991
3025
        whether they should be upgraded or not.
2992
3026
        """
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
2996
 
        
 
3030
 
2997
3031
            pv = utilities.pythonify_version(previous_version)
2998
3032
            rv = utilities.pythonify_version(running_version)
2999
 
        
 
3033
 
3000
3034
            if pv <> rv:
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.
3005
 
                
 
3039
 
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)
3011
 
    
 
3045
 
3012
3046
    def on_preference_changed(self, key, value):
3013
3047
        """
3014
3048
        Called when user changes the program's preferences
3017
3051
 
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()
3027
 
            
3028
 
                
3029
 
        elif key in ['backup_images', 'backup_device_autodetection', 
3030
 
                     'backup_location', 'backup_video_location', 
 
3061
 
 
3062
 
 
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()
3035
 
                
 
3069
 
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
3038
 
        
 
3072
 
3039
3073
        # Note, totally ignore any changes in downloads today, as it
3040
3074
        # is modified in a special manner via a tracking class
3041
 
                
 
3075
 
3042
3076
        elif key == 'stored_sequence_no':
3043
3077
            if type(value) <> types.IntType:
3044
3078
                logger.critical("Stored sequence number value is malformed")
3045
3079
            else:
3046
3080
                self.stored_sequence_value.value = value
3047
 
                
 
3081
 
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()
3052
 
            
 
3086
 
3053
3087
        elif key in ['download_folder', 'video_download_folder']:
3054
3088
            self._set_to_toolbar_values()
3055
3089
            self.display_free_space()
3056
 
    
 
3090
 
3057
3091
    def post_preference_change(self):
3058
3092
        if self.rerun_setup_available_image_and_video_media:
3059
3093
 
3060
3094
            logger.info("Download device settings preferences were changed")
3061
 
            
 
3095
 
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()
3065
 
            
 
3099
 
3066
3100
            if self.main_notebook.get_current_page() == 1: # preview of file
3067
3101
                self.main_notebook.set_current_page(0)
3068
 
                
 
3102
 
3069
3103
            self.rerun_setup_available_image_and_video_media = False
3070
 
            
 
3104
 
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.")
3075
 
            
 
3109
 
3076
3110
            self.refresh_backup_media()
3077
 
            
 
3111
 
3078
3112
            self.rerun_setup_available_backup_media = False
3079
 
            
 
3113
 
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)
3086
 
            
3087
 
 
3088
 
    
 
3120
 
 
3121
 
 
3122
 
3089
3123
    # # #
3090
3124
    # Main app window management and setup
3091
3125
    # # #
3092
 
    
 
3126
 
3093
3127
    def _init_pynotify(self):
3094
3128
        """
3095
3129
        Initialize system notification messages
3096
3130
        """
3097
 
        
 
3131
 
3098
3132
        if not pynotify.init("TestCaps"):
3099
3133
            logger.warning("There might be problems using pynotify.")
3100
 
            #~ sys.exit(1)
3101
3134
 
3102
3135
        do_not_size_icon = False
3103
 
        self.notification_icon_size = 48 
 
3136
        self.notification_icon_size = 48
3104
3137
        try:
3105
3138
            info = pynotify.get_server_info()
3106
3139
        except:
3111
3144
                    do_not_size_icon = True
3112
3145
            except:
3113
3146
                pass
3114
 
        
 
3147
 
3115
3148
        if do_not_size_icon:
3116
3149
            self.application_icon = gtk.gdk.pixbuf_new_from_file(
3117
3150
                        paths.share_dir('glade3/rapid-photo-downloader.svg'))
3137
3170
        self.main_notebook = builder.get_object("main_notebook")
3138
3171
        self.download_action = builder.get_object("download_action")
3139
3172
        self.download_button = builder.get_object("download_button")
3140
 
        
 
3173
 
3141
3174
        self.download_progressbar = builder.get_object("download_progressbar")
3142
3175
        self.rapid_statusbar = builder.get_object("rapid_statusbar")
3143
3176
        self.statusbar_context_id = self.rapid_statusbar.get_context_id("progress")
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")
3151
 
        
 
3184
 
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)
3155
 
        
 
3188
 
3156
3189
        self._init_toolbars()
3157
 
        
 
3190
 
3158
3191
        # About dialog
3159
3192
        builder.add_from_file(paths.share_dir("glade3/about.ui"))
3160
3193
        self.about = builder.get_object("about")
3161
 
        
 
3194
 
3162
3195
        builder.connect_signals(self)
3163
 
        
 
3196
 
3164
3197
        self.preview_image = PreviewImage(self, builder)
3165
3198
 
3166
3199
        thumbnails_scrolledwindow = builder.get_object('thumbnails_scrolledwindow')
3167
3200
        self.thumbnails = ThumbnailDisplay(self)
3168
 
        thumbnails_scrolledwindow.add(self.thumbnails)        
3169
 
        
 
3201
        thumbnails_scrolledwindow.add(self.thumbnails)
 
3202
 
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)
3174
 
        
 
3207
 
3175
3208
        #error log window
3176
3209
        self.error_log = errorlog.ErrorLog(self)
3177
 
        
 
3210
 
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 = {}
3182
 
        
 
3215
 
3183
3216
        # Download action state
3184
3217
        self.download_action_is_download = True
3185
 
        
 
3218
 
3186
3219
        # Track the time a download commences
3187
3220
        self.download_start_time = None
3188
 
        
 
3221
 
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
3192
 
        
 
3225
 
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()
3196
 
        
 
3229
 
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()
3202
 
        
 
3235
 
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)
3208
 
        
 
3241
 
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)
3211
 
        
 
3244
 
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)
3215
 
        
 
3248
 
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)
3232
 
        
 
3265
 
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)
3238
 
        
 
3271
 
3239
3272
        self._set_from_toolbar_state()
3240
 
        
 
3273
 
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)
3245
 
                
 
3278
 
3246
3279
        self.from_toolbar.show_all()
3247
 
    
 
3280
 
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)
3251
 
        
 
3284
 
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)
3257
 
        
 
3290
 
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)
3265
3298
        self.forward_label2 = gtk.Label(" ")
3266
3299
        self.forward_label3 = gtk.Label(" ")
3267
3300
        self.forward_label4 = gtk.Label(" ")
3268
 
        
 
3301
 
3269
3302
        self.copy_button = gtk.RadioToolButton()
3270
3303
        self.copy_button.set_label(_("Copy"))
3271
3304
        self.copy_button.set_is_important(True)
3278
3311
        copy_box = gtk.ToolItem()
3279
3312
        copy_box.add(self.copy_hbox)
3280
3313
        self.copy_toolbar.insert(copy_box, 1)
3281
 
            
 
3314
 
3282
3315
        self.move_button = gtk.RadioToolButton(self.copy_button)
3283
3316
        self.move_button.set_label(_("Move"))
3284
3317
        self.move_button.set_is_important(True)
3290
3323
        move_box = gtk.ToolItem()
3291
3324
        move_box.add(self.move_hbox)
3292
3325
        self.copy_toolbar.insert(move_box, 2)
3293
 
        
 
3326
 
3294
3327
        self.move_button.set_active(self.prefs.auto_delete)
3295
 
        self.copy_button.connect("toggled", self.on_copy_button_toggle_event)        
3296
 
        
 
3328
        self.copy_button.connect("toggled", self.on_copy_button_toggle_event)
 
3329
 
3297
3330
        self.copy_toolbar.show_all()
3298
 
        self._set_copy_toolbar_active_arrows()    
3299
 
    
 
3331
        self._set_copy_toolbar_active_arrows()
 
3332
 
3300
3333
    def _setup_dest_toolbar(self):
3301
3334
        #Destination Toolbar
3302
3335
        self.dest_toolbar.set_border_width(5)
3303
 
        
 
3336
 
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)
3310
 
        
 
3343
 
3311
3344
        photo_dest_hbox = gtk.HBox()
3312
3345
        self.photo_dest_label = gtk.Label(_("Photos:"))
3313
 
        
 
3346
 
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(
3336
3369
        self.to_video_filechooser.set_expand(True)
3337
3370
        self.to_video_filechooser.add(video_dest_hbox)
3338
3371
        self.dest_toolbar.insert(self.to_video_filechooser, 2)
3339
 
        
 
3372
 
3340
3373
        self._set_to_toolbar_values()
3341
 
        self.to_photo_filechooser_button.connect("selection-changed", 
 
3374
        self.to_photo_filechooser_button.connect("selection-changed",
3342
3375
                        self.on_to_photo_filechooser_button_selection_changed)
3343
3376
        self.to_video_filechooser_button.connect("selection-changed",
3344
3377
                        self.on_to_video_filechooser_button_selection_changed)
3346
3379
 
3347
3380
    def _make_widget_widths_equal(self, widget1, widget2):
3348
3381
        """takes two widgets and sets a width for both equal to widest one"""
3349
 
        
 
3382
 
3350
3383
        x1, y1, w1, h1 = widget1.get_allocation()
3351
3384
        x2, y2, w2, h2 = widget2.get_allocation()
3352
3385
        w = max(w1, w2)
3353
3386
        h = max(h1, h2)
3354
3387
        widget1.set_size_request(w,h)
3355
3388
        widget2.set_size_request(w,h)
3356
 
        
 
3389
 
3357
3390
    def _set_copy_toolbar_active_arrows(self):
3358
3391
        if self.copy_button.get_active():
3359
3392
            self.forward_image.set_visible(True)
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()
3380
 
                    
 
3413
 
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)
3387
 
    
 
3420
 
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()
3393
 
                
 
3426
 
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
3399
 
            
 
3432
 
3400
3433
    def on_to_photo_filechooser_button_selection_changed(self, filechooserbutton):
3401
3434
        path = filechooserbutton.get_current_folder()
3402
3435
        if path:
3403
3436
            self.prefs.download_folder = path
3404
 
    
 
3437
 
3405
3438
    def on_to_video_filechooser_button_selection_changed(self, filechooserbutton):
3406
3439
        path = filechooserbutton.get_current_folder()
3407
3440
        if path:
3408
3441
            self.prefs.video_download_folder = path
3409
 
            
 
3442
 
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)
3419
3452
    def _set_window_size(self):
3420
3453
        """
3421
3454
        Remember the window size from the last time the program was run, or
3422
 
        set a default size        
 
3455
        set a default size
3423
3456
        """
3424
 
        
 
3457
 
3425
3458
        if self.prefs.main_window_maximized:
3426
3459
            self.rapidapp.maximize()
3427
 
            self.rapidapp.set_default_size(config.DEFAULT_WINDOW_WIDTH, 
 
3460
            self.rapidapp.set_default_size(config.DEFAULT_WINDOW_WIDTH,
3428
3461
                                           config.DEFAULT_WINDOW_HEIGHT)
3429
3462
        elif self.prefs.main_window_size_x > 0:
3430
3463
            self.rapidapp.set_default_size(self.prefs.main_window_size_x, self.prefs.main_window_size_y)
3431
3464
        else:
3432
3465
            # set a default size
3433
 
            self.rapidapp.set_default_size(config.DEFAULT_WINDOW_WIDTH, 
 
3466
            self.rapidapp.set_default_size(config.DEFAULT_WINDOW_WIDTH,
3434
3467
                                           config.DEFAULT_WINDOW_HEIGHT)
3435
 
        
 
3468
 
3436
3469
 
3437
3470
    def _set_device_collection_size(self):
3438
3471
        """
3445
3478
        else:
3446
3479
            # don't allow the media collection to be absolutely empty
3447
3480
            self.device_collection_scrolledwindow.set_size_request(-1, 47)
3448
 
        
3449
 
            
 
3481
 
 
3482
 
3450
3483
    def on_rapidapp_window_state_event(self, widget, event):
3451
3484
        """ Records the window maximization state in the preferences."""
3452
 
        
 
3485
 
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
3455
 
        
 
3488
 
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)
3460
 
        
 
3493
 
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)
3464
 
        
 
3497
 
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)
3468
 
        
 
3501
 
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)
3472
 
    
 
3505
 
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)
3477
 
    
 
3510
 
3478
3511
    def _setup_error_icons(self):
3479
3512
        """
3480
3513
        hide display of warning and error symbols in the taskbar until they
3486
3519
        self.error_image.hide()
3487
3520
        self.warning_image.hide()
3488
3521
        self.warning_vseparator.hide()
3489
 
        
 
3522
 
3490
3523
    def enable_prefs_and_refresh(self, enabled):
3491
3524
        """
3492
3525
        If enable is true, then the user is able to activate the preferences
3495
3528
        """
3496
3529
        self.refresh_action.set_sensitive(enabled)
3497
3530
        self.preferences_action.set_sensitive(enabled)
3498
 
    
 
3531
 
3499
3532
    def statusbar_message(self, msg):
3500
3533
        self.rapid_statusbar.push(self.statusbar_context_id, msg)
3501
 
        
 
3534
 
3502
3535
    def statusbar_message_remove(self):
3503
3536
        self.rapid_statusbar.pop(self.statusbar_context_id)
3504
3537
 
3505
3538
    def display_backup_mounts(self):
3506
3539
        """
3507
 
        Create a message to be displayed to the user showing which backup 
 
3540
        Create a message to be displayed to the user showing which backup
3508
3541
        mounts will be used
3509
3542
        """
3510
3543
        message =  ''
3511
 
        
 
3544
 
3512
3545
        paths = self.backup_devices.keys()
3513
3546
        i = 0
3514
3547
        v = len(paths)
3521
3554
                    prefix = " " + _("and")  + " "
3522
3555
            i += 1
3523
3556
            message = "%s%s'%s'" % (message,  prefix, self.backup_devices[b][0].get_name())
3524
 
        
 
3557
 
3525
3558
        if v > 1:
3526
3559
            message = _("Using backup devices") + " %s" % message
3527
3560
        elif v == 1:
3528
3561
            message = _("Using backup device") + " %s"  % message
3529
3562
        else:
3530
3563
            message = _("No backup devices detected")
3531
 
            
 
3564
 
3532
3565
        return message
3533
 
        
 
3566
 
3534
3567
    def display_free_space(self):
3535
3568
        """
3536
 
        Displays the amount of space free on the filesystem the files will be 
 
3569
        Displays the amount of space free on the filesystem the files will be
3537
3570
        downloaded to.
3538
 
        
3539
 
        Also displays backup volumes / path being used. 
 
3571
 
 
3572
        Also displays backup volumes / path being used.
3540
3573
        """
3541
3574
        photo_dir = self.is_valid_download_dir(path=self.prefs.download_folder, is_photo_dir=True, show_error_in_log=True)
3542
3575
        video_dir = self.is_valid_download_dir(path=self.prefs.video_download_folder, is_photo_dir=False, show_error_in_log=True)
3545
3578
                                            self.prefs.video_download_folder)
3546
3579
        else:
3547
3580
            same_file_system = False
3548
 
                
 
3581
 
3549
3582
        dirs = []
3550
3583
        if photo_dir:
3551
3584
            dirs.append((self.prefs.download_folder, _("photos")))
3552
3585
        if video_dir and not same_file_system:
3553
3586
            dirs.append((self.prefs.video_download_folder, _("videos")))
3554
 
        
 
3587
 
3555
3588
        msg = ''
3556
3589
        if len(dirs) > 1:
3557
3590
            msg = ' ' + _('Free space:') + ' '
3558
 
            
 
3591
 
3559
3592
        for i in range(len(dirs)):
3560
3593
            dir_info = dirs[i]
3561
3594
            folder = gio.File(dir_info[0])
3563
3596
            size = file_info.get_attribute_uint64(gio.FILE_ATTRIBUTE_FILESYSTEM_FREE)
3564
3597
            free = format_size_for_user(bytes=size)
3565
3598
            if len(dirs) > 1:
3566
 
                #(videos) or (photos) will be appended to the free space message displayed to the 
 
3599
                #(videos) or (photos) will be appended to the free space message displayed to the
3567
3600
                #user in the status bar.
3568
 
                #you should only translate this if your language does not use parantheses 
 
3601
                #you should only translate this if your language does not use parantheses
3569
3602
                file_type = _("(%(file_type)s)") % {'file_type': dir_info[1]}
3570
3603
 
3571
3604
                #Freespace available on the filesystem for downloading to
3572
 
                #Displayed in status bar message on main window                
 
3605
                #Displayed in status bar message on main window
3573
3606
                msg += _("%(free)s %(file_type)s") % {'free': free, 'file_type': file_type}
3574
3607
                if i == 0:
3575
3608
                    #Inserted in the middle of the statusbar message concerning the amount of freespace
3579
3612
                elif not self.prefs.backup_images:
3580
3613
                    #Inserted at the end of the statusbar message concerning the amount of freespace
3581
3614
                    #Used to differentiate between two different file systems
3582
 
                    #e.g. Free space: 21.3GB (photos); 14.7GB (videos).                    
 
3615
                    #e.g. Free space: 21.3GB (photos); 14.7GB (videos).
3583
3616
                    msg += _(".")
3584
 
                
 
3617
 
3585
3618
            else:
3586
3619
                #Freespace available on the filesystem for downloading to
3587
3620
                #Displayed in status bar message on main window
3588
3621
                #e.g. 14.7GB available
3589
3622
                msg = " " + _("%(free)s free") % {'free': free}
3590
 
        
3591
 
            
3592
 
        if self.prefs.backup_images: 
 
3623
 
 
3624
 
 
3625
        if self.prefs.backup_images:
3593
3626
            if not self.prefs.backup_device_autodetection:
3594
3627
                if self.prefs.backup_location == self.prefs.backup_video_location:
3595
3628
                    if DOWNLOAD_VIDEO:
3604
3637
                             'path':self.prefs.backup_location,
3605
3638
                             'path2': self.prefs.backup_video_location}
3606
3639
            else:
3607
 
                msg2 = self.display_backup_mounts() 
3608
 
                
 
3640
                msg2 = self.display_backup_mounts()
 
3641
 
3609
3642
            if msg:
3610
3643
                msg = _("%(freespace)s. %(backuppaths)s.") % {'freespace': msg, 'backuppaths': msg2}
3611
3644
            else:
3612
3645
                msg = msg2
3613
 
        
 
3646
 
3614
3647
        msg = msg.rstrip()
3615
 
            
 
3648
 
3616
3649
        self.statusbar_message(msg)
3617
 
        
 
3650
 
3618
3651
    def log_error(self, severity, problem, details, extra_detail=None):
3619
3652
        """
3620
3653
        Display error and warning messages to user in log window
3621
3654
        """
3622
3655
        self.error_log.add_message(severity, problem, details, extra_detail)
3623
 
        
3624
 
    
 
3656
 
 
3657
 
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()     
3628
 
        
3629
 
        
 
3660
        self.error_log.widget.show()
 
3661
 
 
3662
 
3630
3663
    def on_menu_log_window_toggled(self, widget):
3631
3664
        active = widget.get_active()
3632
3665
        self.prefs.show_log_dialog = active
3634
3667
            self.error_log.widget.show()
3635
3668
        else:
3636
3669
            self.error_log.widget.hide()
3637
 
            
 
3670
 
3638
3671
    def notify_prefs_are_invalid(self, details):
3639
3672
        title = _("Program preferences are invalid")
3640
3673
        logger.critical(title)
3641
3674
        self.log_error(severity=config.CRITICAL_ERROR, problem=title,
3642
3675
                       details=details)
3643
 
    
3644
 
    
 
3676
 
 
3677
 
3645
3678
    # # #
3646
3679
    # Utility functions
3647
3680
    # # #
3650
3683
        """
3651
3684
        Returns true if there is at least one instance of the file_type
3652
3685
        in the list of files to be copied
3653
 
        
 
3686
 
3654
3687
        If return_file_count is True, then the number of files of that type
3655
3688
        will be counted and returned instead of True or False
3656
3689
        """
3665
3698
            return False
3666
3699
        else:
3667
3700
            return i
3668
 
                
 
3701
 
3669
3702
    def size_files_to_be_downloaded(self, files):
3670
3703
        """
3671
3704
        Returns the total sizes of the photos and videos to be downloaded in bytes
3679
3712
                video_size += rpd_file.size
3680
3713
 
3681
3714
        return (photo_size, video_size)
3682
 
                                              
 
3715
 
3683
3716
    def check_download_folder_validity(self, files_by_scan_pid):
3684
3717
        """
3685
3718
        Checks validity of download folders based on the file types the user
3686
3719
        is attempting to download.
3687
 
        
 
3720
 
3688
3721
        If valid, returns a tuple of True and an empty list.
3689
3722
        If invalid, returns a tuple of False and a list of the invalid directores.
3690
3723
        """
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
3705
 
            
 
3738
 
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):
3710
3743
                valid = False
3711
3744
                invalid_dirs.append(self.prefs.download_folder)
3712
3745
            else:
3713
 
                logger.debug("Photo download folder is valid: %s", 
 
3746
                logger.debug("Photo download folder is valid: %s",
3714
3747
                        self.prefs.download_folder)
3715
 
                
 
3748
 
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):
3719
3752
                valid = False
3720
3753
                invalid_dirs.append(self.prefs.video_download_folder)
3721
3754
            else:
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)
3724
3757
 
3725
 
                
 
3758
 
3726
3759
        return (valid, invalid_dirs)
3727
3760
 
3728
3761
    def same_file_system(self, file1, file2):
3735
3768
        f2_info = f2.query_info(gio.FILE_ATTRIBUTE_ID_FILESYSTEM)
3736
3769
        f2_id = f2_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILESYSTEM)
3737
3770
        return f1_id == f2_id
3738
 
        
3739
 
    
 
3771
 
 
3772
 
3740
3773
    def same_file(self, file1, file2):
3741
3774
        """Returns True if the files / directories are the same
3742
3775
        """
3743
3776
        f1 = gio.File(file1)
3744
3777
        f2 = gio.File(file2)
3745
 
        
 
3778
 
3746
3779
        file_attributes = "id::file"
3747
3780
        f1_info = f1.query_filesystem_info(file_attributes)
3748
3781
        f1_id = f1_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILE)
3749
3782
        f2_info = f2.query_filesystem_info(file_attributes)
3750
3783
        f2_id = f2_info.get_attribute_string(gio.FILE_ATTRIBUTE_ID_FILE)
3751
3784
        return f1_id == f2_id
3752
 
        
 
3785
 
3753
3786
    def is_valid_download_dir(self, path, is_photo_dir, show_error_in_log=False):
3754
3787
        """
3755
3788
        Checks the following conditions:
3756
3789
        Does the directory exist?
3757
3790
        Is it writable?
3758
 
        
 
3791
 
3759
3792
        if show_error_in_log is True, then display warning in log window, using
3760
3793
        is_photo_dir, which if true means the download directory is for photos,
3761
3794
        if false, for Videos
3765
3798
            download_folder_type = _("Photo")
3766
3799
        else:
3767
3800
            download_folder_type = _("Video")
3768
 
            
 
3801
 
3769
3802
        try:
3770
3803
            d = gio.File(path)
3771
3804
            if not d.query_exists(cancellable=None):
3772
 
                logger.error("%s download folder does not exist: %s", 
 
3805
                logger.error("%s download folder does not exist: %s",
3773
3806
                             download_folder_type, path)
3774
3807
                if show_error_in_log:
3775
3808
                    severity = config.WARNING
3781
3814
                file_attributes = "standard::type,access::can-read,access::can-write"
3782
3815
                file_info = d.query_filesystem_info(file_attributes)
3783
3816
                file_type = file_info.get_file_type()
3784
 
                
 
3817
 
3785
3818
                if file_type != gio.FILE_TYPE_DIRECTORY and file_type != gio.FILE_TYPE_UNKNOWN:
3786
 
                    logger.error("%s download folder is invalid: %s", 
 
3819
                    logger.error("%s download folder is invalid: %s",
3787
3820
                                 download_folder_type, path)
3788
3821
                    if show_error_in_log:
3789
3822
                        severity = config.WARNING
3790
3823
                        problem = _("%(file_type)s download folder is invalid") % {
3791
3824
                                    'file_type': download_folder_type}
3792
3825
                        details = _("Folder: %s") % path
3793
 
                        self.log_error(severity, problem, details)                  
 
3826
                        self.log_error(severity, problem, details)
3794
3827
                else:
3795
3828
                    # is the directory writable?
3796
3829
                    try:
3803
3836
                            problem = _("%(file_type)s download folder is not writable") % {
3804
3837
                                        'file_type': download_folder_type}
3805
3838
                            details = _("Folder: %s") % path
3806
 
                            self.log_error(severity, problem, details)                          
 
3839
                            self.log_error(severity, problem, details)
3807
3840
                    else:
3808
3841
                        f = gio.File(temp_dir)
3809
3842
                        f.delete(cancellable=None)
3811
3844
        except gio.Error, inst:
3812
3845
            logger.error("Error checking download directory %s", path)
3813
3846
            logger.error(inst)
3814
 
            
 
3847
 
3815
3848
        return valid
3816
 
                
3817
 
    
3818
 
    
 
3849
 
 
3850
 
 
3851
 
3819
3852
    # # #
3820
3853
    #  Process results and management
3821
3854
    # # #
3822
 
        
3823
 
        
 
3855
 
 
3856
 
3824
3857
    def _start_process_managers(self):
3825
3858
        """
3826
3859
        Set up process managers.
3827
 
        
 
3860
 
3828
3861
        A task such as scanning a device or copying files is handled in its
3829
3862
        own process.
3830
3863
        """
3831
 
        
 
3864
 
3832
3865
        self.batch_size = 10
3833
3866
        self.batch_size_MB = 2
3834
 
        
 
3867
 
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)
3843
 
        
3844
 
        # daemon process to rename files and create subfolders                   
 
3876
 
 
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)
3848
 
        
 
3881
 
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)
3852
 
                                        
 
3885
 
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)
3856
 
                                                   
 
3889
 
3857
3890
        #process to back files up
3858
3891
        self.backup_manager = BackupFilesManager(self.backup_results,
3859
3892
                                                 self.batch_size_MB)
3860
 
                                                 
 
3893
 
3861
3894
        #process to enhance files after they've been copied and before they're
3862
3895
        #renamed
3863
3896
        self.file_modify_manager = FileModifyManager(self.file_modify_results)
3864
 
        
3865
 
        
 
3897
 
 
3898
 
3866
3899
    def scan_results(self, source, condition):
3867
3900
        """
3868
3901
        Receive results from scan processes
3869
3902
        """
3870
3903
        connection = self.scan_manager.get_pipe(source)
3871
 
        
 
3904
 
3872
3905
        conn_type, data = connection.recv()
3873
 
        
 
3906
 
3874
3907
        if conn_type == rpdmp.CONN_COMPLETE:
3875
3908
            connection.close()
3876
3909
            self.scan_manager.no_tasks -= 1
3883
3916
            self.device_collection.update_device(scan_pid, size)
3884
3917
            self.device_collection.update_progress(scan_pid, 0.0, results_summary, 0, pulse=False)
3885
3918
            self.set_download_action_sensitivity()
3886
 
                        
 
3919
 
3887
3920
            if (not self.auto_start_is_on and
3888
3921
                self.prefs.generate_thumbnails):
3889
3922
                self.download_progressbar.set_text(_("Thumbnails"))
3897
3930
            logger.debug("Turning on display of thumbnails")
3898
3931
            self.display_scan_thumbnails()
3899
3932
            self.download_button.grab_focus()
3900
 
            
 
3933
 
3901
3934
            # signal that no more data is coming, finishing io watch for this pipe
3902
3935
            return False
3903
3936
        else:
3910
3943
                scanning_progress = file_type_counter.running_file_count()
3911
3944
                self.device_collection.update_device(scan_pid, size)
3912
3945
                self.device_collection.update_progress(scan_pid, 0.0, scanning_progress, 0, pulse=True)
3913
 
                
 
3946
 
3914
3947
                for rpd_file in rpd_files:
3915
 
                    self.thumbnails.add_file(rpd_file=rpd_file, 
 
3948
                    self.thumbnails.add_file(rpd_file=rpd_file,
3916
3949
                                        generate_thumbnail = not self.auto_start_is_on)
3917
 
        
 
3950
 
3918
3951
        # must return True for this method to be called again
3919
3952
        return True
3920
 
        
 
3953
 
3921
3954
 
3922
3955
    @dbus.service.method (config.DBUS_NAME,
3923
3956
                           in_signature='', out_signature='b')
3924
3957
    def is_running (self):
3925
3958
        return self.running
3926
 
    
 
3959
 
3927
3960
    @dbus.service.method (config.DBUS_NAME,
3928
3961
                            in_signature='', out_signature='')
3929
3962
    def start (self):
3932
3965
        else:
3933
3966
            self.running = True
3934
3967
            gtk.main()
3935
 
        
 
3968
 
3936
3969
def start():
3937
3970
 
3938
3971
    is_beta = config.version.find('~') > 0
3939
 
    
 
3972
 
3940
3973
    parser = OptionParser(version= "%%prog %s" % utilities.human_readable_version(config.version))
3941
3974
    parser.set_defaults(verbose=is_beta,  extensions=False)
3942
 
    # Translators: this text is displayed to the user when they request information on the command line options. 
 
3975
    # Translators: this text is displayed to the user when they request information on the command line options.
3943
3976
    # The text %default should not be modified or left out.
3944
3977
    parser.add_option("-v",  "--verbose",  action="store_true", dest="verbose",  help=_("display program information on the command line as the program runs (default: %default)"))
3945
3978
    parser.add_option("-d", "--debug", action="store_true", dest="debug", help=_('display debugging information when run from the command line'))
3951
3984
    parser.add_option("-l", "--device-location", type="string", metavar="PATH", dest="device_location", help=_("manually specify the PATH of the device from which to download, overwriting existing program preferences"))
3952
3985
    parser.add_option("--reset-settings", action="store_true", dest="reset", help=_("reset all program settings and preferences and exit"))
3953
3986
    (options, args) = parser.parse_args()
3954
 
    
 
3987
 
3955
3988
    if options.debug:
3956
3989
        logging_level = logging.DEBUG
3957
3990
    elif options.verbose:
3958
3991
        logging_level = logging.INFO
3959
3992
    else:
3960
3993
        logging_level = logging.ERROR
3961
 
    
 
3994
 
3962
3995
    logger.setLevel(logging_level)
3963
 
    
 
3996
 
3964
3997
    if options.auto_detect and options.device_location:
3965
3998
        logger.info(_("Error: specify device auto-detection or manually specify a device's path from which to download, but do not do both."))
3966
3999
        sys.exit(1)
3967
 
        
 
4000
 
3968
4001
    if options.auto_detect:
3969
4002
        auto_detect=True
3970
4003
        logger.info("Device auto detection set from command line")
3971
4004
    else:
3972
4005
        auto_detect=None
3973
 
        
 
4006
 
3974
4007
    if options.device_location:
3975
4008
        device_location=options.device_location
3976
4009
        if device_location[-1]=='/':
3987
4020
                v += '%s, ' % e.upper()
3988
4021
            v = file_type + " " + v[:-1] + ' '+ (_('and %s') % exts[-1].upper())
3989
4022
            print v
3990
 
            
 
4023
 
3991
4024
        sys.exit(0)
3992
 
        
 
4025
 
3993
4026
    if options.reset:
3994
4027
        prefs = prefsrapid.RapidPreferences()
3995
4028
        prefs.reset()
3996
4029
        print _("All settings and preferences have been reset")
3997
4030
        sys.exit(0)
3998
 
        
 
4031
 
3999
4032
    if options.focal_length:
4000
4033
        focal_length = options.focal_length
4001
4034
    else:
4017
4050
 
4018
4051
    bus = dbus.SessionBus ()
4019
4052
    request = bus.request_name (config.DBUS_NAME, dbus.bus.NAME_FLAG_DO_NOT_QUEUE)
4020
 
    if request != dbus.bus.REQUEST_NAME_REPLY_EXISTS: 
 
4053
    if request != dbus.bus.REQUEST_NAME_REPLY_EXISTS:
4021
4054
        app = RapidApp(bus, '/', config.DBUS_NAME, focal_length=focal_length,
4022
4055
        auto_detect=auto_detect, device_location=device_location)
4023
4056
    else:
4025
4058
        print "Rapid Photo Downloader is already running"
4026
4059
        object = bus.get_object (config.DBUS_NAME, "/")
4027
4060
        app = dbus.Interface (object, config.DBUS_NAME)
4028
 
    
4029
 
    app.start()            
 
4061
 
 
4062
    app.start()
4030
4063
 
4031
4064
if __name__ == "__main__":
4032
4065
    start()