~alexeftimie/jockey/fix-gobject

« back to all changes in this revision

Viewing changes to jockey/ui.py

  • Committer: Bazaar Package Importer
  • Author(s): Martin Pitt
  • Date: 2008-01-17 15:02:40 UTC
  • Revision ID: james.westby@ubuntu.com-20080117150240-djmsi8giu255vzzn
Tags: 0.1~r118
* Initial release, result of completely rewriting restricted-manager to be
  maintainable, robust, and suitable for other distributions. Some features
  and the KDE UI still need to be ported.
* See restricted-manager-rewrite specification for details.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# (c) 2007 Canonical Ltd.
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License along
 
14
# with this program; if not, write to the Free Software Foundation, Inc.,
 
15
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
16
 
 
17
'''Abstract user interface, which provides all logic and strings.
 
18
 
 
19
Concrete implementations need to implement a set of abstract presentation
 
20
functions with an appropriate toolkit.
 
21
'''
 
22
 
 
23
# TODO: port update_installed_packages()
 
24
 
 
25
import gettext, optparse, logging, textwrap, os.path, urllib2, tempfile
 
26
 
 
27
import detection, oslib
 
28
 
 
29
class AbstractUI:
 
30
    '''Abstract user interface.
 
31
 
 
32
    This encapsulates the entire program logic and all strings, but does not
 
33
    implement any concrete user interface.
 
34
    '''
 
35
    def __init__(self):
 
36
        '''Initialize system.
 
37
        
 
38
        This parses command line arguments, detects available hardware,
 
39
        and already installed drivers and handlers.
 
40
        '''
 
41
        self.gettext_domain = 'jockey'
 
42
        (self.argv_options, self.argv_args) = self.parse_argv()
 
43
 
 
44
        gettext.textdomain(self.gettext_domain)
 
45
 
 
46
        # set up logging
 
47
        if self.argv_options.debug:
 
48
            logging.basicConfig(level=logging.DEBUG, 
 
49
                format='%(asctime)s %(levelname)s: %(message)s')
 
50
        else:
 
51
            logging.basicConfig(level=logging.WARNING, 
 
52
                format='%(levelname)s: %(message)s')
 
53
 
 
54
        self.init_strings()
 
55
 
 
56
        # TODO: use a locally caching driver db
 
57
        self.handlers = detection.get_handlers(self,
 
58
            driverdb=detection.LocalKernelModulesDriverDB(),
 
59
            handler_dir=self.argv_options.handler_dir,
 
60
            mode=self.argv_options.mode)
 
61
 
 
62
    def _(self, str, convert_keybindings=False):
 
63
        '''Keyboard accelerator aware gettext() wrapper.
 
64
        
 
65
        This optionally converts keyboard accelerators to the appropriate
 
66
        format for the frontend.
 
67
 
 
68
        All strings in the source code should use the '_' prefix for key
 
69
        accelerators (like in GTK). For inserting a real '_', use '__'.
 
70
        '''
 
71
        # KDE compatible conversion
 
72
        result = unicode(gettext.gettext(str), 'UTF-8')
 
73
 
 
74
        if convert_keybindings:
 
75
            result = self.convert_keybindings(result)
 
76
 
 
77
        return result
 
78
 
 
79
    def init_strings(self):
 
80
        '''Initialize all static strings which are used in UI implementations.'''
 
81
 
 
82
        self.string_handler = self._('Component')
 
83
        self.string_enabled = self._('Enabled')
 
84
        self.string_status = self._('Status')
 
85
        self.string_restart = self._('Needs computer restart')
 
86
        self.string_in_use = self._('In use')
 
87
        self.string_not_in_use = self._('Not in use')
 
88
 
 
89
    def main_window_title(self):
 
90
        '''Return an appropriate translated window title.
 
91
 
 
92
        This might depend on the mode the program is called (e. g. showing only
 
93
        free drivers, only restricted ones, etc.).
 
94
        '''
 
95
        if self.argv_options.mode == detection.MODE_NONFREE:
 
96
            return self._('Restricted Driver Setup')
 
97
        else:
 
98
            return self._('Driver Setup')
 
99
 
 
100
    def main_window_text(self):
 
101
        '''Return a tuple (heading, subtext) of main window texts.
 
102
 
 
103
        This changes depending on whether restricted or free drivers are
 
104
        used/available, thus the UI should update it whenever it changes a
 
105
        handler.
 
106
        '''
 
107
        proprietary_in_use = False
 
108
        proprietary_available = False
 
109
 
 
110
        for h in self.handlers:
 
111
            if not h.free():
 
112
                proprietary_available = True
 
113
                if h.used():
 
114
                    proprietary_in_use = True
 
115
                    break
 
116
 
 
117
        if proprietary_in_use:
 
118
            heading = self._('Proprietary drivers are being used to make '
 
119
                    'this computer work properly.')
 
120
        else:
 
121
            heading = self._('No proprietary drivers are in use on this system.')
 
122
 
 
123
        if proprietary_available:
 
124
            subtext = self._(
 
125
            # %(os)s stands for the OS name. Prefix it or suffix it,
 
126
            # but do not replace it.
 
127
            'Proprietary drivers do not have public source code that %(os)s '
 
128
            'developers are free to modify. They represent a risk to you '
 
129
            'because they are only available on the types of computer chosen by '
 
130
            'the manufacturer, and security updates to them depend solely on the '
 
131
            'responsiveness of the manufacturer. %(os)s cannot fix or improve '
 
132
            'these drivers.') % {'os': oslib.OSLib.inst.os_vendor}
 
133
        else:
 
134
            subtext = ''
 
135
 
 
136
        return (heading, subtext)
 
137
 
 
138
    def get_handler_tooltip(self, handler):
 
139
        '''Format handler rationale as a tooltip.
 
140
 
 
141
        Return None if the handler is None or does not have a rationale.
 
142
        '''
 
143
        try:
 
144
            tip = ''
 
145
            for par in handler.rationale().split('\n'):
 
146
                if tip:
 
147
                    tip += '\n'
 
148
                tip += '\n'.join(textwrap.wrap(par, 60))
 
149
            return tip
 
150
        except AttributeError:
 
151
            return None
 
152
 
 
153
    def parse_argv(self):
 
154
        '''Parse command line arguments, and return (options, args) pair.'''
 
155
 
 
156
        parser = optparse.OptionParser()
 
157
        parser.add_option ('-c', '--check', action='store_true',
 
158
                dest='check', default=False,
 
159
                help=self._('Check for newly used or usable drivers and notify the user.'))
 
160
        parser.add_option ('-u', '--update-db', action='store_true',
 
161
                dest='update_db', default=False,
 
162
                help=self._('Query driver databases for newly available or updated drivers.'))
 
163
        parser.add_option ('-l', '--list', action='store_true',
 
164
                dest='list', default=False,
 
165
                help=self._('List available drivers and their status.'))
 
166
        parser.add_option ('-H', '--handler-dir',
 
167
                type='string', dest='handler_dir', metavar='DIR', default=None,
 
168
                help=self._('Add a custom handler directory.'))
 
169
        parser.add_option ('-m', '--mode',
 
170
                type='choice', dest='mode', default='any',
 
171
                choices=['free', 'nonfree', 'any'],
 
172
                metavar='free|nonfree|any',
 
173
                help=self._('Only manage free/nonfree drivers. By default, all'
 
174
                ' available drivers with any license are presented.'))
 
175
        parser.add_option ('--debug', action='store_true',
 
176
                dest='debug', default=False,
 
177
                help=self._('Enable debugging messages.'))
 
178
 
 
179
        (opts, args) = parser.parse_args()
 
180
 
 
181
        # transform mode string into constant
 
182
        modes = {
 
183
            'free': detection.MODE_FREE,
 
184
            'nonfree': detection.MODE_NONFREE,
 
185
            'any': detection.MODE_ANY
 
186
        }
 
187
        opts.mode = modes[opts.mode]
 
188
 
 
189
        return (opts, args)
 
190
 
 
191
    def run(self):
 
192
        '''Evaluate command line arguments and do the appropriate action.
 
193
 
 
194
        If no argument was specified, this starts the interactive UI.
 
195
        
 
196
        This returns the exit code of the program.
 
197
        '''
 
198
        if self.argv_options.update_db:
 
199
            self.update_driverdb()
 
200
 
 
201
        if self.argv_options.list:
 
202
            self.list()
 
203
            return 0
 
204
        elif self.argv_options.check:
 
205
            if self.check():
 
206
                return 0
 
207
            else:
 
208
                return 1
 
209
 
 
210
        # start the UI
 
211
        self.ui_init()
 
212
        return self.ui_main_loop()
 
213
 
 
214
    def list(self):
 
215
        '''Print a list of available handlers and their status to stdout.'''
 
216
 
 
217
        import sys
 
218
        for h in self.handlers:
 
219
            print h
 
220
 
 
221
    def check(self):
 
222
        '''Notify the user about newly used or available drivers since last check().
 
223
        
 
224
        Return True if any new driver is available which is not yet enabled.'''
 
225
 
 
226
        if not oslib.OSLib.inst.is_admin():
 
227
            logging.error('Only administrators can use this function.')
 
228
            return False
 
229
 
 
230
        # read previously seen/used handlers
 
231
        seen = set()
 
232
        used = set()
 
233
 
 
234
        if os.path.exists(oslib.OSLib.inst.check_cache):
 
235
            f = open(oslib.OSLib.inst.check_cache)
 
236
            for line in f:
 
237
                try:
 
238
                    (flag, h) = line.split(None, 1)
 
239
                    h = unicode(h, 'UTF-8')
 
240
                except ValueError:
 
241
                    logging.error('invalid line in %s: %s',
 
242
                        oslib.OSLib.inst.check_cache, line)
 
243
                if flag == 'seen':
 
244
                    seen.add(h.strip())
 
245
                elif flag == 'used':
 
246
                    used.add(h.strip())
 
247
                else:
 
248
                    logging.error('invalid flag in %s: %s',
 
249
                        oslib.OSLib.inst.check_cache, line)
 
250
            f.close()
 
251
 
 
252
        # check for newly used/available handlers
 
253
        new_avail = {}
 
254
        new_used = {}
 
255
        for h in self.handlers:
 
256
            id = '%s:%s' % (str(h.__class__).split('.')[-1], h.name())
 
257
            if id not in seen:
 
258
                new_avail[id] = h
 
259
                logging.debug('handler %s previously unseen', id)
 
260
            if id not in used and h.used():
 
261
                new_used[id] = h
 
262
                logging.debug('handler %s previously unused', id)
 
263
 
 
264
        # write back cache
 
265
        if new_avail or new_used:
 
266
            logging.debug('new available/used drivers, writing back check cache %s', 
 
267
                oslib.OSLib.inst.check_cache)
 
268
            seen.update(new_avail.keys())
 
269
            used.update(new_used.keys())
 
270
            f = open(oslib.OSLib.inst.check_cache, 'w')
 
271
            for s in seen:
 
272
                print >> f, 'seen', s
 
273
            for u in used:
 
274
                print >> f, 'used', u
 
275
            f.close()
 
276
 
 
277
        # throw out newly available handlers which are already enabled, no need
 
278
        # to bother people about them
 
279
        restricted_available = False
 
280
        for h in new_avail.keys(): # create a copy for iteration
 
281
            if new_avail[h].enabled():
 
282
                logging.debug('%s is already enabled or not available, not announcing', id)
 
283
                del new_avail[h]
 
284
            elif not new_avail[h].free():
 
285
                restricted_available = True
 
286
 
 
287
        # throw out newly used free drivers; no need for education here
 
288
        for h in new_used.keys():
 
289
            if new_used[h].free():
 
290
                logging.debug('%s is a newly used free driver, not announcing', id)
 
291
                del new_used[h]
 
292
 
 
293
        notified = False
 
294
 
 
295
        # launch notifications if anything remains
 
296
        if new_avail:
 
297
            if restricted_available:
 
298
                self.ui_notification(self._('Restricted drivers available'),
 
299
                    self._('In order to use your hardware more efficiently, you'
 
300
                    ' can enable drivers which are not free software.'))
 
301
            else:
 
302
                self.ui_notification(self._('New drivers available'),
 
303
                    self._('There are new or updated drivers available for '
 
304
                    'your hardware.'))
 
305
            notified = True
 
306
        elif new_used:
 
307
            self.ui_notification(self._('New restricted drivers in use'),
 
308
                # %(os)s stands for the OS name. Prefix it or suffix it,
 
309
                # but do not replace it.
 
310
                self._('In order for this computer to function properly, %(os)s is '
 
311
                'using driver software that cannot be supported by %(os)s.') % 
 
312
                    {'os': oslib.OSLib.inst.os_vendor})
 
313
            notified = True
 
314
 
 
315
        if notified:
 
316
            # we need to stay in the main loop so that the tray icon stays
 
317
            # around
 
318
            self.ui_main_loop()
 
319
 
 
320
        return new_avail
 
321
 
 
322
    def update_driverdb(self):
 
323
        '''Query remote driver DB for updates.'''
 
324
 
 
325
        raise NotImplementedError, 'TODO'
 
326
 
 
327
    def toggle_handler(self, handler):
 
328
        '''Callback for toggling the handler enable/disable state in the UI.
 
329
        
 
330
        After this, you need to refresh the UI's handler tree view if this
 
331
        method returns True.
 
332
        '''
 
333
        # check if we can change at all
 
334
        ch = handler.can_change()
 
335
        if ch:
 
336
            self.error_message(self._('Cannot change driver'), ch)
 
337
            return False
 
338
 
 
339
        en = handler.enabled()
 
340
 
 
341
        # construct and ask confirmation question
 
342
        if en:
 
343
            title = self._('Disable driver?')
 
344
            action = self._('_Disable', True)
 
345
        else:
 
346
            title = self._('Enable driver?')
 
347
            action = self._('_Enable', True)
 
348
 
 
349
        d = handler.description() or ''
 
350
        r = handler.rationale() or ''
 
351
        if d and r:
 
352
            subtext = d.strip() + '\n\n' + r
 
353
        elif d:
 
354
            subtext = d
 
355
        elif r:
 
356
            subtext = r
 
357
        else:
 
358
            subtext = None
 
359
        if not self.confirm_action(title, handler.name(), subtext, action):
 
360
            return False
 
361
 
 
362
        # go
 
363
        if en:
 
364
            handler.disable()
 
365
        else:
 
366
            handler.enable()
 
367
 
 
368
        return True
 
369
 
 
370
    def install_package(self, package):
 
371
        '''Install software package.'''
 
372
 
 
373
        # TODO: port checking of availability (enable repository?)
 
374
 
 
375
        oslib.OSLib.inst.install_package(package, self)
 
376
 
 
377
    def remove_package(self, package):
 
378
        '''Remove software package.'''
 
379
 
 
380
        oslib.OSLib.inst.remove_package(package, self)
 
381
 
 
382
    def download_url(self, url, filename=None, data=None):
 
383
        '''Download an URL into a local file, and display a progress dialog.
 
384
        
 
385
        If filename is not given, a temporary file will be created.
 
386
 
 
387
        Additional POST data can be submitted for HTTP requests in the data
 
388
        argument (see urllib2.urlopen).
 
389
 
 
390
        Return (filename, headers) tuple, or (None, headers) if the user
 
391
        cancelled the download.
 
392
        '''
 
393
        block_size = 8192
 
394
        current_size = 0
 
395
        try:
 
396
            f = urllib2.urlopen(url)
 
397
        except Exception, e:
 
398
            self.error_message(self._('Download error'), str(e))
 
399
            return (None, None)
 
400
        headers = f.info()
 
401
 
 
402
        if 'Content-Length' in headers:
 
403
            total_size = int(headers['Content-Length'])
 
404
        else:
 
405
            total_size = -1
 
406
 
 
407
        self.ui_download_start(url, total_size)
 
408
 
 
409
        if filename:
 
410
            tfp = open(filename, 'wb')
 
411
            result_filename = filename
 
412
        else:
 
413
            (fd, result_filename) = tempfile.mkstemp()
 
414
            tfp = os.fdopen(fd, 'wb')
 
415
 
 
416
        try:
 
417
            while current_size < total_size:
 
418
                block = f.read(block_size)
 
419
                tfp.write (block)
 
420
                current_size += len(block)
 
421
                # if True, user canceled download
 
422
                if self.ui_download_progress(current_size, total_size):
 
423
                    # if we created a temporary file, clean it up
 
424
                    if not filename:
 
425
                        os.unlink(result_filename)
 
426
                    result_filename = None
 
427
                    break
 
428
        finally:
 
429
            tfp.close()
 
430
            f.close()
 
431
            self.ui_download_finish()
 
432
 
 
433
        return (result_filename, headers)
 
434
 
 
435
    #
 
436
    # The following methods must be implemented in subclasses
 
437
    # 
 
438
 
 
439
    def convert_keybindings(self, str):
 
440
        '''Convert keyboard accelerators to the particular UI's format.
 
441
 
 
442
        The abstract UI and drivers use the '_' prefix to mark a keyboard
 
443
        accelerator.
 
444
 
 
445
        A double underscore ('__') is converted to a real '_'.'''
 
446
 
 
447
        raise NotImplementedError, 'subclasses need to implement this'
 
448
 
 
449
    def ui_init(self):
 
450
        '''Initialize UI.
 
451
        
 
452
        This should set up presentation of self.handlers and show the main
 
453
        window.
 
454
        '''
 
455
        raise NotImplementedError, 'subclasses need to implement this'
 
456
 
 
457
    def ui_main_loop(self):
 
458
        '''Main loop for the user interface.
 
459
        
 
460
        This should return if the user wants to quit the program, and return
 
461
        the exit code.
 
462
        '''
 
463
        raise NotImplementedError, 'subclasses need to implement this'
 
464
 
 
465
    def error_message(self, title, text):
 
466
        '''Present an error message box.'''
 
467
 
 
468
        raise NotImplementedError, 'subclasses need to implement this'
 
469
 
 
470
    def confirm_action(self, title, text, subtext=None, action=None):
 
471
        '''Present a confirmation dialog.
 
472
 
 
473
        If action is given, it is used as button label instead of the default
 
474
        'OK'.  Return True if the user confirms, False otherwise.
 
475
        '''
 
476
        raise NotImplementedError, 'subclasses need to implement this'
 
477
 
 
478
    def ui_notification(self, title, text):
 
479
        '''Present a notification popup.
 
480
 
 
481
        This should preferably create a tray icon. Clicking on the tray icon or
 
482
        notification should run the GUI.
 
483
        '''
 
484
        raise NotImplementedError, 'subclasses need to implement this'
 
485
 
 
486
    def ui_idle(self):
 
487
        '''Process pending UI events and return.
 
488
 
 
489
        This is called while waiting for external processes such as package
 
490
        installers.
 
491
        '''
 
492
        raise NotImplementedError, 'subclasses need to implement this'
 
493
 
 
494
    def ui_download_start(self, url, total_size):
 
495
        '''Create a progress dialog for a download of given URL.
 
496
 
 
497
        total_size specifes the number of bytes to download, or -1 if it cannot
 
498
        be determined. In this case the dialog should display an indeterminated
 
499
        progress bar (bouncing back and forth).
 
500
        '''
 
501
        raise NotImplementedError, 'subclasses need to implement this'
 
502
 
 
503
    def ui_download_progress(self, current_size, total_size):
 
504
        '''Update download progress of current download.
 
505
        
 
506
        This should return True to cancel the current download, and False
 
507
        otherwise.
 
508
        '''
 
509
        raise NotImplementedError, 'subclasses need to implement this'
 
510
 
 
511
    def ui_download_finish(self):
 
512
        '''Close the current download progress dialog.'''
 
513
 
 
514
        raise NotImplementedError, 'subclasses need to implement this'
 
515
 
 
516
if __name__ == '__main__':
 
517
    oslib.OSLib.inst = oslib.OSLib()
 
518
    u = AbstractUI()
 
519
    u.run()