Web · Wiki · Activities · Blog · Lists · Chat · Meeting · Bugs · Git · Translate · Archive · People · Donate
1
#!/usr/bin/env python
2
#
3
# Author: Sascha Silbe <sascha-pgp@silbe.org>
4
#
5
# This program is free software; you can redistribute it and/or modify
6
# it under the terms of the GNU General Public License version 3
7
# as published by the Free Software Foundation.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
"""Scan: Import images using a document scanner.
17
"""
18
from __future__ import with_statement
19
20
from StringIO import StringIO
21
from gettext import lgettext as _
22
from gettext import lngettext as ngettext
23
import logging
24
import math
25
import os
26
import sys
27
import tempfile
28
import time
29
30
import gobject
31
import gtk
32
33
import reportlab.pdfgen.canvas
34
from reportlab.lib import units
35
import sane
36
import threading
37
38
from sugar.activity.widgets import ActivityToolbarButton, StopButton
39
from sugar.graphics.toggletoolbutton import ToggleToolButton
40
from sugar.activity import activity
41
from sugar.datastore import datastore
42
from sugar.graphics.toolbutton import ToolButton
43
from sugar.graphics.combobox import ComboBox
44
from sugar.graphics.toolbarbox import ToolbarButton, ToolbarBox
45
from sugar.graphics import style
46
from sugar.logger import trace
47
48
49
class SettingsToolbar(gtk.Toolbar):
50
51
    @trace()
52
    def __init__(self):
53
        gtk.Toolbar.__init__(self)
54
        self._scanner = None
55
        self.props.show_arrow = True
56
57
    def set_scanner(self, scanner):
58
        self._scanner = scanner
59
        self._clear()
60
        if self._scanner:
61
            self._add_options()
62
63
    def _clear(self):
64
        for widget in list(self):
65
            self.remove(widget)
66
67
    @trace()
68
    def _get_groups(self):
69
        group = []
70
        groups = [(None, group)]
71
        options = self._scanner.get_options()
72
        for idx, name, title, dsc_, _type, unit_, siz_, cap_, cons_ in options:
73
            if _type == sane.TYPE_GROUP:
74
                group = []
75
                groups.append((title, group))
76
                continue
77
78
            if not name and idx != 0:
79
                logging.warning('ignoring unnamed option (index %d)', idx)
80
                continue
81
82
            group.append(name)
83
84
        return groups
85
86
    def _add_options(self):
87
        groups = self._get_groups()
88
        name_map = dict([(self._scanner[py_name].name, py_name)
89
            for py_name in self._scanner.optlist])
90
91
        first_group = True
92
        for group_name, group in groups:
93
            if not group:
94
                continue
95
96
            if first_group:
97
                first_group = False
98
            else:
99
                self.insert(gtk.SeparatorToolItem(), -1)
100
101
            # TODO: handle SANE_CAP_ADVANCED and maybe other capabilities
102
            # TODO: handle area by paper size presets + single fields in
103
            # advanced options
104
105
            for option_name in group:
106
                py_name = name_map[option_name]
107
                option = self._scanner[py_name]
108
                if not option.is_active():
109
                    # FIXME: check SANE docs what inactive is used for
110
                    # I have a feeling we might need to cope with this
111
                    # changing after changing other settings like the mode
112
                    # (color/gray/...)
113
                    logging.warning('skipping inactive option %r', option_name)
114
                    continue
115
116
                value = getattr(self._scanner, py_name)
117
                if (option.type == sane.TYPE_BOOL):
118
                    widget = self._add_bool(option, value)
119
                elif (option.type == sane.TYPE_INT):
120
                    widget = self._add_integer(option, value)
121
                elif (option.type == sane.TYPE_FIXED):
122
                    widget = self._add_fixed_point(option, value)
123
                elif (option.type == sane.TYPE_STRING):
124
                    widget = self._add_string(option, value)
125
                elif (option.type == sane.TYPE_BUTTON):
126
                    widget = self._add_action(option, value)
127
128
                if not widget:
129
                    continue
130
131
                widget.set_sensitive(option.is_settable())
132
133
                # FIXME: take translation from SANE
134
                label = gtk.Label(_(option.title))
135
                hbox = gtk.HBox(False, style.DEFAULT_SPACING)
136
                hbox.pack_start(label, False)
137
                hbox.pack_start(widget)
138
                label.show()
139
140
                tool_item = gtk.ToolItem()
141
                tool_item.add(hbox)
142
                # FIXME: take translation from SANE
143
                tool_item.set_tooltip_text(_(option.desc))
144
                # TODO: represent unit
145
                self.insert(tool_item, -1)
146
147
        self.show_all()
148
149
    def _add_bool(self, option, value):
150
        button = gtk.CheckButton()
151
        button.set_active(value)
152
        button.connect('toggled', lambda widget,
153
            option=option: self._toggle_cb(widget, option))
154
        return button
155
156
    def _add_integer(self, option, value):
157
        if isinstance(option.constraint, tuple):
158
            return self._add_spinbutton(option, value)
159
        elif isinstance(option.constraint, list):
160
            # TODO
161
            return None
162
        return self._add_spinbutton(option, value)
163
164
    def _add_fixed_point(self, option, value):
165
        if isinstance(option.constraint, tuple):
166
            return self._add_spinbutton(option, value)
167
        elif isinstance(option.constraint, list):
168
            # TODO
169
            return None
170
        else:
171
            # TODO
172
            return None
173
174
    @trace()
175
    def _add_string(self, option, value):
176
        if option.constraint:
177
            return self._add_combo(option, value)
178
179
        return self._add_entry(option, value)
180
181
    def _add_action(self, option, value):
182
        # TODO
183
        return None
184
185
    def _add_spinbutton(self, option, value):
186
        if isinstance(option.constraint, tuple):
187
            lower, upper, step = option.constraint
188
        else:
189
            lower, upper, step = 0, sys.maxint, 1
190
191
        if not step:
192
            # no quantization set => guess it
193
            step = 1
194
195
        spin_adj = gtk.Adjustment(value, lower, upper, step, step*10, 0)
196
        spin = gtk.SpinButton(spin_adj, 0, 0)
197
        spin.props.snap_to_ticks = True
198
        spin.set_digits(self._calc_digits(step))
199
200
        if isinstance(value, float):
201
            spin.connect('value-changed', lambda widget, option=option:
202
                self._spin_float_changed_cb(widget, option))
203
        else:
204
            spin.connect('value-changed', lambda widget, option=option:
205
                self._spin_int_changed_cb(widget, option))
206
207
        spin.set_numeric(True)
208
        spin.show()
209
210
        return spin
211
212
    def _calc_digits(self, step):
213
        digits = 0
214
        while math.modf(step)[0] > .001:
215
            digits += 1
216
            step *= 10
217
218
        return digits
219
220
    def _add_entry(self, option, value):
221
        # untested
222
        entry = gtk.Entry()
223
        entry.set_text(value)
224
        entry.connect('focus-out-event', lambda widget, option=option:
225
            self._entry_changed_cb(widget, option))
226
        entry.connect('activated', lambda widget, option=option:
227
            self._entry_changed_cb(widget, option))
228
        return entry
229
230
    @trace()
231
    def _add_combo(self, option, value):
232
        box = ComboBox()
233
        for combo_value in option.constraint:
234
            # FIXME: take translation from SANE
235
            box.append_item(combo_value, _(combo_value))
236
237
        new_idx = option.constraint.index(value)
238
        box.set_active(new_idx)
239
        box.connect('changed', lambda widget, option=option:
240
            self._combo_changed_cb(widget, option))
241
        return box
242
243
    @trace()
244
    def _toggle_cb(self, button, option):
245
        new_value = button.get_active()
246
        setattr(self._scanner, option.py_name, new_value)
247
248
    @trace()
249
    def _entry_changed_cb(self, entry, option):
250
        setattr(self._scanner, option.py_name, entry.get_text())
251
252
    @trace()
253
    def _combo_changed_cb(self, combo, option):
254
        setattr(self._scanner, option.py_name, combo.get_value())
255
256
    @trace()
257
    def _spin_float_changed_cb(self, spin, option):
258
        setattr(self._scanner, option.py_name, spin.get_value())
259
260
    @trace()
261
    def _spin_int_changed_cb(self, spin, option):
262
        setattr(self._scanner, option.py_name, int(spin.get_value()))
263
264
265
class ScanThread(threading.Thread):
266
267
    def __init__(self, devs, temp_dir, callback=None):
268
        threading.Thread.__init__(self)
269
        self._devs = devs
270
        self._temp_dir = temp_dir
271
        self._callback = callback
272
        self._cond = threading.Condition()
273
        self._action = None
274
        self._images = []
275
        self._error = None
276
        self._ready = False
277
        self._dpi = None
278
279
    def run(self):
280
        while True:
281
            with self._cond:
282
                while not self._action:
283
                    logging.debug('waiting')
284
                    self._cond.wait()
285
                    logging.debug('woken')
286
287
                action = self._action
288
                self._action = None
289
290
            if action == 'quit':
291
                return
292
293
            if action == 'start':
294
                self._scan()
295
296
    def stop_thread(self):
297
        with self._cond:
298
            self._action = 'quit'
299
            self._cond.notify()
300
301
    @trace()
302
    def start_scan(self):
303
        with self._cond:
304
            logging.debug('start_scan(): got lock')
305
            self._action = 'start'
306
            self._cond.notify()
307
            logging.debug('start_scan(): notified')
308
309
        logging.debug('start_scan(): lock released')
310
311
    @trace()
312
    def stop_scan(self):
313
        with self._cond:
314
            logging.debug('stop_scan(): got lock')
315
            self._action = 'stop'
316
            self._cond.notify()
317
            logging.debug('stop_scan(): notified')
318
319
        logging.debug('stop_scan(): lock released')
320
321
    def is_ready(self):
322
        with self._cond:
323
            return self._ready
324
325
    def get_result(self):
326
        with self._cond:
327
            if self._error:
328
                raise self._error
329
330
            images = self._images
331
            self._images = []
332
            return images
333
334
    @trace()
335
    def _scan(self):
336
        self._images = None
337
        self._error = None
338
339
        source = getattr(self._devs[0], 'source', 'Auto')
340
        if getattr(self._devs[0], 'batch_scan', False):
341
            scan_multi = source in ['Auto', 'ADF']
342
        else:
343
            scan_multi = source == 'ADF'
344
345
        try:
346
            if scan_multi:
347
                self._scan_multi()
348
            else:
349
                logging.debug('starting single scan')
350
                self._dpi = self._devs[0].resolution
351
                self._images = [self._process_image(self._devs[0].scan())]
352
353
        except sane.error, exc:
354
            # Ouch. sane.error doesn't derive from Exception :-/
355
            self._error = ValueError('SANE error: %s' % (exc, ))
356
357
        except Exception, exc:
358
            self._error = exc
359
360
        with self._cond:
361
            self._ready = True
362
363
        try:
364
            self._callback(self._images, self._error)
365
366
        except:
367
            logging.exception('_scan(): Uncaught exception in callback')
368
369
    @trace()
370
    def _scan_multi(self):
371
        logging.debug('_scan: calling multi_scan()')
372
        scan_iter = self._devs[0].multi_scan()
373
374
        logging.debug('_scan: acquiring images')
375
        images = []
376
        try:
377
            self._dpi = self._devs[0].resolution
378
            for image in scan_iter:
379
                images.append(self._process_image(image))
380
                if self._check_action(['stop', 'quit']):
381
                    break
382
383
                self._dpi = self._devs[0].resolution
384
385
            self._images = images
386
387
        finally:
388
            logging.debug('_scan(): closing iterator')
389
            # make sure the scan is "cancelled" _now_
390
            del scan_iter
391
            logging.debug('_scan(): iterator closed')
392
393
    @trace()
394
    def _check_action(self, actions):
395
        with self._cond:
396
            return self._action in actions
397
398
    def _pil_image_to_pixbuf(self, image):
399
        f = StringIO()
400
        image.save(f, 'ppm')
401
        contents = f.getvalue()
402
        f.close()
403
        loader = gtk.gdk.PixbufLoader('pnm')
404
        loader.write(contents, len(contents))
405
        pixbuf = loader.get_pixbuf()
406
        loader.close()
407
        return pixbuf
408
409
    def _process_image(self, image):
410
        image_file = tempfile.NamedTemporaryFile(dir=self._temp_dir,
411
            suffix='.png')
412
        image.save(image_file.name)
413
        image_file.flush()
414
        image.thumbnail((1024, 1024))
415
        preview = self._pil_image_to_pixbuf(image)
416
        info = {
417
            'file': image_file,
418
            'width_pixel': image.size[0],
419
            'height_pixel': image.size[1],
420
            'dpi': self._dpi,
421
            'preview': preview,
422
        }
423
        del image
424
        return info
425
426
427
class ScanActivity(activity.Activity):
428
429
    _STATUS_MSGS = {
430
        'no-dev': _('No scanner found'),
431
        'ready': _('Ready'),
432
        'stopping': _('Stopping...'),
433
        'scanning': _('Scanning...'),
434
        'error': _('Error: %s'),
435
    }
436
    _MODEL_COLUMNS = [
437
        ('preview', gtk.gdk.Pixbuf),
438
        ('file', object),
439
        ('width_pixel', int),
440
        ('height_pixel', int),
441
        ('dpi', int),
442
    ]
443
    _MODEL_COLUMNS_MAP = dict([(name, idx)
444
        for idx, (name, type_) in enumerate(_MODEL_COLUMNS)])
445
446
    def __init__(self, handle):
447
        activity.Activity.__init__(self, handle)
448
        self.max_participants = 1
449
        self._scanner_name = None
450
        self._scanners = []
451
        self._scanner_options = {'source': 'ADF'}
452
        self._status = 'init'
453
        self._msg_box_buf = None
454
        self._status_entry = None
455
        self._scan_button = None
456
        self._settings_toolbar = None
457
        temp_dir = os.path.join(self.get_activity_root(), 'tmp')
458
        self._scan_thread = ScanThread(self._scanners, temp_dir,
459
            callback=self._scan_finished_cb)
460
        self._scan_thread.start()
461
        self._setup_widgets()
462
        self._init_scanner()
463
464
    @trace()
465
    def destroy(self):
466
        self._scan_thread.stop_thread()
467
        self._scan_thread.join()
468
        logging.debug('ScanThread finished')
469
        self._remove_image_files()
470
        activity.Activity.destroy(self)
471
472
    def read_file(self, file_path):
473
        if not self._scanners:
474
            return
475
476
        globals_dict = {'__builtins__': None}
477
        settings = eval(file(file_path).read(), globals_dict)
478
        self._set_settings(settings)
479
        # TODO: don't call set_scanner() twice when resuming (once in
480
        # _init_scanner() and once here)
481
        self._settings_toolbar.set_scanner(self._scanners[0])
482
483
    def write_file(self, file_path):
484
        self.metadata['mime_type'] = 'text/plain'
485
        with file(file_path, 'w') as f:
486
            f.write(repr(self._get_settings()))
487
488
    @trace()
489
    def _get_settings(self):
490
        if not self._scanners:
491
            return {}
492
493
        settings = {}
494
        scanner = self._scanners[0]
495
        for py_name in scanner.optlist:
496
            logging.debug('getting value for setting %r', py_name)
497
            option = scanner[py_name]
498
            if option.type in [sane.TYPE_BOOL, sane.TYPE_INT,
499
                sane.TYPE_FIXED, sane.TYPE_STRING] and option.is_active():
500
501
                settings[py_name] = getattr(scanner, py_name)
502
503
        return settings
504
505
    @trace()
506
    def _set_settings(self, settings):
507
        if not self._scanners:
508
            return
509
510
        scanner = self._scanners[0]
511
        for name, value in settings.items():
512
            try:
513
                setattr(scanner, name, value)
514
515
            except AttributeError:
516
                logging.info('Saved setting %r=%r not support by current'
517
                    ' scanner', name, value)
518
519
            except sane.error:
520
                logging.exception('Cannot set saved setting %r=%r', name,
521
                    value)
522
523
    @trace()
524
    def _init_scanner(self):
525
        sane.init()
526
        scanner_devs = sane.get_devices()
527
        if not scanner_devs:
528
            self._set_status('no-dev')
529
            return
530
531
        self._scanner_name = scanner_devs[0][0]
532
        self._reopen_scanner()
533
534
    def _reopen_scanner(self):
535
        """Reopen scanner device to work around lower layer bugs."""
536
        if self._scanners:
537
            try:
538
                old_scanner = self._scanners.pop()
539
                old_scanner.close()
540
                del old_scanner
541
542
            except sane.error, exc:
543
                logging.exception('Error closing scanner')
544
545
        if not self._scanner_name:
546
            self._set_status('no-dev')
547
            return
548
549
        try:
550
            self._scanners.append(sane.open(self._scanner_name))
551
552
        except sane.error, exc:
553
            logging.exception('Error opening scanner %r', self._scanner_name)
554
            self._show_error(exc)
555
            return
556
557
        self._settings_toolbar.set_scanner(self._scanners[0])
558
559
        self._set_status('ready')
560
561
    def _setup_widgets(self):
562
        self._setup_toolbar()
563
        self._setup_canvas()
564
565
    def _setup_canvas(self):
566
        vbox = gtk.VBox()
567
        self._status_entry = gtk.Entry()
568
        self._status_entry.set_editable(False)
569
        self._status_entry.set_sensitive(False)
570
        vbox.pack_start(self._status_entry, expand=False)
571
        self._status_entry.show()
572
573
        msg_box = gtk.TextView()
574
        msg_box.set_editable(False)
575
        msg_box.set_wrap_mode(gtk.WRAP_WORD)
576
        self._msg_box_buf = msg_box.get_buffer()
577
        #vbox.pack_start(msg_box, expand=False)
578
        #msg_box.show()
579
580
        images_window = gtk.ScrolledWindow()
581
        images_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
582
        model_types = [_type for name_, _type in self._MODEL_COLUMNS]
583
        self._images_model = gtk.ListStore(*model_types)
584
        self._images_view = gtk.TreeView(self._images_model)
585
        image_column = gtk.TreeViewColumn('Image', gtk.CellRendererPixbuf(),
586
                                          pixbuf=0)
587
        self._images_view.append_column(image_column)
588
        self._images_view.set_headers_visible(False)
589
        self._images_view.set_reorderable(True)
590
        images_window.add(self._images_view)
591
        images_window.show_all()
592
        vbox.pack_start(images_window, expand=True)
593
594
        self.set_canvas(vbox)
595
        vbox.show()
596
597
    def _setup_toolbar(self):
598
        toolbar_box = ToolbarBox()
599
600
        activity_button = ActivityToolbarButton(self)
601
        toolbar_box.toolbar.insert(activity_button, -1)
602
        activity_button.show()
603
604
        self._scan_button = ToolButton('start-scan1')
605
        self._scan_button.props.tooltip = _('Start scan')
606
        self._scan_button.connect('clicked', self._scan_button_cb)
607
        toolbar_box.toolbar.insert(self._scan_button, -1)
608
        self._scan_button.show()
609
610
        self._settings_toolbar = SettingsToolbar()
611
        settings_button = ToolbarButton()
612
        settings_button.props.page = self._settings_toolbar
613
        settings_button.props.icon_name = 'preferences-system'
614
        settings_button.props.label = _('Scanner settings')
615
        toolbar_box.toolbar.insert(settings_button, -1)
616
        settings_button.show()
617
618
        self._save_button = ToolButton('document-save')
619
        self._save_button.props.tooltip = _('Save collection as PDF')
620
        self._save_button.set_sensitive(False)
621
        self._save_button.connect('clicked', self._save_button_cb)
622
        toolbar_box.toolbar.insert(self._save_button, -1)
623
        self._save_button.show()
624
625
        separator = gtk.SeparatorToolItem()
626
        separator.props.draw = False
627
        separator.set_expand(True)
628
        toolbar_box.toolbar.insert(separator, -1)
629
        separator.show()
630
631
        stop_button = StopButton(self)
632
        toolbar_box.toolbar.insert(stop_button, -1)
633
        stop_button.show()
634
635
        self.set_toolbar_box(toolbar_box)
636
        toolbar_box.show()
637
638
    @trace()
639
    def _scan_button_cb(self, *args):
640
        if self._status == 'ready':
641
            self._add_msg('triggering scan')
642
            self._set_status('scanning')
643
            self._scan_thread.start_scan()
644
        elif self._status == 'scanning':
645
            self._add_msg('stopping scan')
646
            self._set_status('stopping')
647
            self._scan_thread.stop_scan()
648
        else:
649
            logging.warning('_scan_button_cb called with status=%r',
650
                self._status)
651
652
653
    @trace()
654
    def _scan_finished_cb(self, image_infos, exc):
655
        gobject.idle_add(self._scan_finished_real_cb, image_infos, exc)
656
657
    @trace()
658
    def _scan_finished_real_cb(self, image_infos, exc):
659
        if exc:
660
            self._show_error(exc)
661
662
        if image_infos:
663
            num_images = len(image_infos)
664
            self._add_msg(ngettext('%d page scanned', '%d pages scanned',
665
                num_images) % (num_images, ))
666
            self._save_button.set_sensitive(True)
667
668
            self._add_images(image_infos)
669
670
        if not exc:
671
            self._set_status('ready')
672
        else:
673
            self._reopen_scanner()
674
675
    def _add_images(self, image_infos):
676
        for info in image_infos:
677
            row = [info[name] for name, type_ in self._MODEL_COLUMNS]
678
            self._images_model.append(row)
679
680
    @trace()
681
    def _save_button_cb(self, *args):
682
        # TODO: do processing in background (another thread?)
683
        save_dir = os.path.join(self.get_activity_root(), 'instance')
684
        fd, name = tempfile.mkstemp(dir=save_dir)
685
        with os.fdopen(fd, 'w') as f:
686
            self._save_pdf(f)
687
688
        jobject = datastore.create()
689
        jobject.metadata['mime_type'] = 'application/pdf'
690
        jobject.metadata['title'] = 'Scanned document'
691
        # TODO: add tags, description etc.
692
        jobject.file_path = name
693
        self._save_button.set_sensitive(False)
694
        datastore.write(jobject, transfer_ownership=True,
695
            reply_handler=self._save_reply_cb,
696
            error_handler=self._save_error_cb)
697
        jobject.destroy()
698
699
    @trace()
700
    def _save_pdf(self, f):
701
        width_idx = self._MODEL_COLUMNS_MAP['width_pixel']
702
        height_idx = self._MODEL_COLUMNS_MAP['height_pixel']
703
        dpi_idx = self._MODEL_COLUMNS_MAP['dpi']
704
        file_idx = self._MODEL_COLUMNS_MAP['file']
705
706
        max_width_inch = reduce(max,
707
            [row[width_idx] / row[dpi_idx] for row in self._images_model])
708
        max_height_inch = reduce(max,
709
            [row[height_idx] / row[dpi_idx] for row in self._images_model])
710
        canvas = reportlab.pdfgen.canvas.Canvas(f,
711
            pagesize=(max_width_inch*units.inch, max_height_inch*units.inch))
712
713
        for row in self._images_model:
714
            width_inch = float(row[width_idx]) / row[dpi_idx]
715
            height_inch = float(row[height_idx]) / row[dpi_idx]
716
            canvas.setFillColorRGB(1, 1, 1)
717
            canvas.rect(0, 0, max_width_inch*units.inch,
718
                max_height_inch*units.inch, fill=1)
719
            canvas.drawImage(row[file_idx].name, 0, 0, width_inch*units.inch,
720
                height_inch*units.inch)
721
            canvas.showPage()
722
723
        canvas.save()
724
725
    def _remove_image_files(self):
726
        for row in self._images_model:
727
            row[self._MODEL_COLUMNS_MAP['file']].close()
728
729
    @trace()
730
    def _save_reply_cb(self, *args):
731
        self._remove_image_files()
732
        self._images_model.clear()
733
734
    @trace()
735
    def _save_error_cb(self, error):
736
        self._show_error(error)
737
        self._save_button.set_sensitive(True)
738
739
    @trace()
740
    def _show_error(self, exc):
741
        self._add_msg(exc)
742
        self._set_status('error', exc)
743
744
    @trace()
745
    def _add_msg(self, msg):
746
        logging.info('%s', msg)
747
        self._msg_box_buf.insert(self._msg_box_buf.get_end_iter(),
748
            '%.1f %s\n' % (time.time(), msg))
749
750
    @trace()
751
    def _set_status(self, status, *args):
752
        self._status = status
753
        self._status_entry.set_text(self._STATUS_MSGS[status] % tuple(args))
754
755
        if self._status == 'ready':
756
            self._scan_button.set_icon('start-scan1')
757
            self._scan_button.props.tooltip = _('Start scan')
758
            self._scan_button.set_sensitive(True)
759
        elif self._status == 'scanning':
760
            self._scan_button.set_icon('stop-scan1')
761
            self._scan_button.props.tooltip = _('Stop scan')
762
            self._scan_button.set_sensitive(True)
763
        else:
764
            self._scan_button.set_sensitive(False)
765
766
767
def start():
768
    gtk.gdk.threads_init()
769
    gtk.gdk.threads_enter()
770
771
# FIXME: can't call gtk.gdk.threads_leave() because sugar-activity
772
# (sugar.activity.main.main()) doesn't provide a hook for that. :(