1
# (c) 2007 Canonical Ltd.
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.
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.
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.
17
'''Abstract user interface, which provides all logic and strings.
19
Concrete implementations need to implement a set of abstract presentation
20
functions with an appropriate toolkit.
23
# TODO: port update_installed_packages()
25
import gettext, optparse, logging, textwrap, os.path, urllib2, tempfile
27
import detection, oslib
30
'''Abstract user interface.
32
This encapsulates the entire program logic and all strings, but does not
33
implement any concrete user interface.
38
This parses command line arguments, detects available hardware,
39
and already installed drivers and handlers.
41
self.gettext_domain = 'jockey'
42
(self.argv_options, self.argv_args) = self.parse_argv()
44
gettext.textdomain(self.gettext_domain)
47
if self.argv_options.debug:
48
logging.basicConfig(level=logging.DEBUG,
49
format='%(asctime)s %(levelname)s: %(message)s')
51
logging.basicConfig(level=logging.WARNING,
52
format='%(levelname)s: %(message)s')
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)
62
def _(self, str, convert_keybindings=False):
63
'''Keyboard accelerator aware gettext() wrapper.
65
This optionally converts keyboard accelerators to the appropriate
66
format for the frontend.
68
All strings in the source code should use the '_' prefix for key
69
accelerators (like in GTK). For inserting a real '_', use '__'.
71
# KDE compatible conversion
72
result = unicode(gettext.gettext(str), 'UTF-8')
74
if convert_keybindings:
75
result = self.convert_keybindings(result)
79
def init_strings(self):
80
'''Initialize all static strings which are used in UI implementations.'''
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')
89
def main_window_title(self):
90
'''Return an appropriate translated window title.
92
This might depend on the mode the program is called (e. g. showing only
93
free drivers, only restricted ones, etc.).
95
if self.argv_options.mode == detection.MODE_NONFREE:
96
return self._('Restricted Driver Setup')
98
return self._('Driver Setup')
100
def main_window_text(self):
101
'''Return a tuple (heading, subtext) of main window texts.
103
This changes depending on whether restricted or free drivers are
104
used/available, thus the UI should update it whenever it changes a
107
proprietary_in_use = False
108
proprietary_available = False
110
for h in self.handlers:
112
proprietary_available = True
114
proprietary_in_use = True
117
if proprietary_in_use:
118
heading = self._('Proprietary drivers are being used to make '
119
'this computer work properly.')
121
heading = self._('No proprietary drivers are in use on this system.')
123
if proprietary_available:
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}
136
return (heading, subtext)
138
def get_handler_tooltip(self, handler):
139
'''Format handler rationale as a tooltip.
141
Return None if the handler is None or does not have a rationale.
145
for par in handler.rationale().split('\n'):
148
tip += '\n'.join(textwrap.wrap(par, 60))
150
except AttributeError:
153
def parse_argv(self):
154
'''Parse command line arguments, and return (options, args) pair.'''
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.'))
179
(opts, args) = parser.parse_args()
181
# transform mode string into constant
183
'free': detection.MODE_FREE,
184
'nonfree': detection.MODE_NONFREE,
185
'any': detection.MODE_ANY
187
opts.mode = modes[opts.mode]
192
'''Evaluate command line arguments and do the appropriate action.
194
If no argument was specified, this starts the interactive UI.
196
This returns the exit code of the program.
198
if self.argv_options.update_db:
199
self.update_driverdb()
201
if self.argv_options.list:
204
elif self.argv_options.check:
212
return self.ui_main_loop()
215
'''Print a list of available handlers and their status to stdout.'''
218
for h in self.handlers:
222
'''Notify the user about newly used or available drivers since last check().
224
Return True if any new driver is available which is not yet enabled.'''
226
if not oslib.OSLib.inst.is_admin():
227
logging.error('Only administrators can use this function.')
230
# read previously seen/used handlers
234
if os.path.exists(oslib.OSLib.inst.check_cache):
235
f = open(oslib.OSLib.inst.check_cache)
238
(flag, h) = line.split(None, 1)
239
h = unicode(h, 'UTF-8')
241
logging.error('invalid line in %s: %s',
242
oslib.OSLib.inst.check_cache, line)
248
logging.error('invalid flag in %s: %s',
249
oslib.OSLib.inst.check_cache, line)
252
# check for newly used/available handlers
255
for h in self.handlers:
256
id = '%s:%s' % (str(h.__class__).split('.')[-1], h.name())
259
logging.debug('handler %s previously unseen', id)
260
if id not in used and h.used():
262
logging.debug('handler %s previously unused', id)
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')
272
print >> f, 'seen', s
274
print >> f, 'used', u
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)
284
elif not new_avail[h].free():
285
restricted_available = True
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)
295
# launch notifications if anything remains
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.'))
302
self.ui_notification(self._('New drivers available'),
303
self._('There are new or updated drivers available for '
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})
316
# we need to stay in the main loop so that the tray icon stays
322
def update_driverdb(self):
323
'''Query remote driver DB for updates.'''
325
raise NotImplementedError, 'TODO'
327
def toggle_handler(self, handler):
328
'''Callback for toggling the handler enable/disable state in the UI.
330
After this, you need to refresh the UI's handler tree view if this
333
# check if we can change at all
334
ch = handler.can_change()
336
self.error_message(self._('Cannot change driver'), ch)
339
en = handler.enabled()
341
# construct and ask confirmation question
343
title = self._('Disable driver?')
344
action = self._('_Disable', True)
346
title = self._('Enable driver?')
347
action = self._('_Enable', True)
349
d = handler.description() or ''
350
r = handler.rationale() or ''
352
subtext = d.strip() + '\n\n' + r
359
if not self.confirm_action(title, handler.name(), subtext, action):
370
def install_package(self, package):
371
'''Install software package.'''
373
# TODO: port checking of availability (enable repository?)
375
oslib.OSLib.inst.install_package(package, self)
377
def remove_package(self, package):
378
'''Remove software package.'''
380
oslib.OSLib.inst.remove_package(package, self)
382
def download_url(self, url, filename=None, data=None):
383
'''Download an URL into a local file, and display a progress dialog.
385
If filename is not given, a temporary file will be created.
387
Additional POST data can be submitted for HTTP requests in the data
388
argument (see urllib2.urlopen).
390
Return (filename, headers) tuple, or (None, headers) if the user
391
cancelled the download.
396
f = urllib2.urlopen(url)
398
self.error_message(self._('Download error'), str(e))
402
if 'Content-Length' in headers:
403
total_size = int(headers['Content-Length'])
407
self.ui_download_start(url, total_size)
410
tfp = open(filename, 'wb')
411
result_filename = filename
413
(fd, result_filename) = tempfile.mkstemp()
414
tfp = os.fdopen(fd, 'wb')
417
while current_size < total_size:
418
block = f.read(block_size)
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
425
os.unlink(result_filename)
426
result_filename = None
431
self.ui_download_finish()
433
return (result_filename, headers)
436
# The following methods must be implemented in subclasses
439
def convert_keybindings(self, str):
440
'''Convert keyboard accelerators to the particular UI's format.
442
The abstract UI and drivers use the '_' prefix to mark a keyboard
445
A double underscore ('__') is converted to a real '_'.'''
447
raise NotImplementedError, 'subclasses need to implement this'
452
This should set up presentation of self.handlers and show the main
455
raise NotImplementedError, 'subclasses need to implement this'
457
def ui_main_loop(self):
458
'''Main loop for the user interface.
460
This should return if the user wants to quit the program, and return
463
raise NotImplementedError, 'subclasses need to implement this'
465
def error_message(self, title, text):
466
'''Present an error message box.'''
468
raise NotImplementedError, 'subclasses need to implement this'
470
def confirm_action(self, title, text, subtext=None, action=None):
471
'''Present a confirmation dialog.
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.
476
raise NotImplementedError, 'subclasses need to implement this'
478
def ui_notification(self, title, text):
479
'''Present a notification popup.
481
This should preferably create a tray icon. Clicking on the tray icon or
482
notification should run the GUI.
484
raise NotImplementedError, 'subclasses need to implement this'
487
'''Process pending UI events and return.
489
This is called while waiting for external processes such as package
492
raise NotImplementedError, 'subclasses need to implement this'
494
def ui_download_start(self, url, total_size):
495
'''Create a progress dialog for a download of given URL.
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).
501
raise NotImplementedError, 'subclasses need to implement this'
503
def ui_download_progress(self, current_size, total_size):
504
'''Update download progress of current download.
506
This should return True to cancel the current download, and False
509
raise NotImplementedError, 'subclasses need to implement this'
511
def ui_download_finish(self):
512
'''Close the current download progress dialog.'''
514
raise NotImplementedError, 'subclasses need to implement this'
516
if __name__ == '__main__':
517
oslib.OSLib.inst = oslib.OSLib()