# (c) Copyright 2009-2011. CodeWeavers, Inc.
import UserDict
import os
import bottlequery
import cxconfig
import cxutils
import cxlog
class Error(Exception):
pass
def cxassoc_path():
return os.path.join(cxutils.CX_ROOT, "bin", "cxassoc")
def run_cxassoc(args, grab_stdout=False, background=False):
if grab_stdout:
stdout = cxutils.GRAB
else:
stdout = None
if background:
stderr = None
else:
stderr = cxutils.GRAB
retcode, out, err = cxutils.run((cxassoc_path(),)+tuple(args), stderr=stderr, stdout=stdout, background=background)
if retcode:
raise Error(err)
else:
return out
class Association(object):
"""A single extension/appname/verbname set, which corresponds to one
section in the cxassoc.conf file.
"""
def __init__(self, parent, section):
"""Initializes an Association object based on the specified
cxassoc.conf section.
"""
self.parent = parent
# The section name in cxassoc.conf, i.e. the EAssoc.
self.eassoc = section.name
# The extension, of the form '.exe'.
self.extension = self.eassoc.split('/', 1)[0]
# The mode currently stored in cxassoc.conf, one of
# ('ignore', 'mime', 'alternative', 'default')
self.current_mode = section.get('mode', 'ignore')
if self.current_mode == 'alternate':
# For compatibility with pre-5.0.3 files
self.current_mode = 'alternative'
# The new mode, to be committed by CXAssocPrefs.commit().
self.new_mode = self.current_mode
# The MIME type in the windows registry, or a made up type if the
# registry has none.
self.mimetype = section.get('mimetype') or ('application/x-crossover-%s' % self.extension.lstrip('.').lower())
# The file type description in the windows registry, may be an empty
# string.
self.description = section.get('description', '')
# The appname in the windows registry.
self.appname = section.get('appname', '')
if '/' in self.eassoc:
# The internal verb name. Common internal verb names include:
# 'open', 'edit', 'opennew', 'play', 'preview', and 'print'
self.verb = self.eassoc.rsplit('/', 1)[1]
# A string of the form '/app/verb' that identifies the action
# taken by this association, or '' if it's the default in the
# windows registry.
self.action = self.eassoc[self.eassoc.index('/'):]
else:
self.verb = 'open'
self.action = ''
# The user-friendly verb name in the windows registry, or the internal
# verb name if necessary.
self.verbname = section.get('verbname', self.verb)
# The configured file name of the icon if there is one use
# get_iconfilename to get the full path.
self.iconname = bottlequery.expand_unix_string(self.parent.bottlename, section.get('icon', ''))
def get_iconfilename(self):
"""Returns the full path of the icon for this association, or '' if
there is no icon.
"""
if self.iconname and not self.iconname.startswith('/'):
# resolve the path if it's relative
wineprefix = bottlequery.get_prefix_for_bottle(self.parent.bottlename)
return os.path.join(wineprefix, 'windata', 'Associations', self.iconname)
return self.iconname
def set_mode(self, new_mode):
# pylint: disable=E1101
self.parent.set_mode(self.eassoc, new_mode)
class CXAssocPrefs(object, UserDict.IterableUserDict):
"""A set of preferences for the associations in a bottle.
As the modes of the preferences are changed, this class keeps the changes
in memory. They can then be applied using commit()
Changing one preference can have a side-effect of changing others. When
this happens, the caller will be notified with the mode_changed()
function.
"""
def __init__(self, bottlename, managed):
UserDict.IterableUserDict.__init__(self)
self.bottlename = bottlename
self.managed = managed
def config_filename(self):
return os.path.join(bottlequery.get_prefix_for_bottle(self.bottlename), 'cxassoc.conf')
def read_config(self):
"""Reads the association information from cxassoc.conf."""
config = cxconfig.get(self.config_filename())
self.data.clear()
for section in config.itervalues():
self.data[section.name] = Association(self, section)
def query_config(self):
"""Updates association states based on cxassoc --query"""
if self.managed:
scope = "managed"
else:
scope = "private"
retcode, out, err = cxutils.run((cxassoc_path(), '--query', '--bottle', self.bottlename, '--scope', scope), stdout=cxutils.GRAB, stderr=cxutils.GRAB)
if retcode:
raise Error(err)
config = cxconfig.Raw()
config.read_string(out)
for eassoc in self.data:
if eassoc in config:
if config[eassoc].get('default'):
new_mode = 'default'
elif config[eassoc].get('alternative'):
new_mode = 'alternative'
elif config[eassoc].get('mime'):
new_mode = 'mime'
else:
new_mode = 'ignore'
self.data[eassoc].current_mode = self.data[eassoc].new_mode = new_mode
def refresh(self):
"""Reads the association information for this bottle."""
self.read_config()
self.query_config()
def mode_changed(self, eassoc, newmode, user):
"""Called when the mode of an association has changed.
user is True if the change was requested by a call to set_mode().
user is False if the change was made automatically.
Users should override this method.
"""
pass
def _set_mode(self, assoc, new_mode, user):
if assoc.new_mode == new_mode:
return
cxlog.log_('assoc', 'Set mode for %s to %s (user=%s)' % (cxlog.to_str(assoc.eassoc), cxlog.to_str(new_mode), cxlog.to_str(user)))
assoc.new_mode = new_mode
self.mode_changed(assoc.eassoc, new_mode, user)
def set_mode(self, eassoc, new_mode):
"""Set an association's mode, updating others as necessary."""
assoc = self.data[eassoc]
if new_mode not in ('ignore', 'mime', 'alternative', 'default'):
raise ValueError("new_mode must be 'ignore', 'mime', 'alternative', or 'default'")
self._set_mode(assoc, new_mode, True)
mimetype = assoc.mimetype
action = assoc.action
# update other associations as necessary
for other_assoc in self.data.values():
if other_assoc.mimetype == mimetype:
if other_assoc.action == action:
# mime type and action are identical, so the settings must
# be the same
self._set_mode(other_assoc, new_mode, False)
else:
# mime type is the same but the action is different
if new_mode == 'ignore':
# not registering the mime type -> ignore
self._set_mode(other_assoc, 'ignore', False)
else:
if other_assoc.new_mode == 'ignore':
# registering the mime type -> the others can't
# be 'ignore'
self._set_mode(other_assoc, 'mime', False)
elif new_mode == 'default' and other_assoc.new_mode == 'default':
# this is 'default' -> other can't be 'default' too
self._set_mode(other_assoc, 'alternative', False)
def commit(self):
"""Save and apply the changes using cxassoc."""
# Find all changed associations, grouped by the new setting
changes = {'ignore': [],
'mime': [],
'alternative': [],
'default': [],
'install': [],
'all': []}
for assoc in self.data.values():
if assoc.new_mode != assoc.current_mode:
changes[assoc.new_mode].append(assoc.eassoc)
mode_spec = []
for mode in ('ignore', 'mime', 'alternative', 'default'):
if changes[mode]:
mode_spec.append("%s=%s" % (mode, ":".join(changes[mode])))
changes['all'].extend(changes[mode])
if mode != 'ignore':
changes['install'].extend(changes[mode])
if changes['all']:
# Apply all the changes in one go
cmd = [cxassoc_path(), '--bottle', self.bottlename,
'--mode', ";".join(mode_spec).replace(".", "\\."),
'--mode-filter', ':'.join(changes['all'])]
if changes['install']:
cmd.extend(('--install', '--install-filter',
':'.join(changes['install'])))
if changes['ignore']:
cmd.extend(('--uninstall', '--uninstall-filter',
':'.join(changes['ignore'])))
retcode, _out, err = cxutils.run(cmd, stderr=cxutils.GRAB)
if retcode:
raise Error(err)
for eassoc in changes['all']:
self.data[eassoc].current_mode = self.data[eassoc].new_mode
def revert(self):
"""Discard local changes."""
raise cxutils.not_yet_implemented()
def recreate_assocs(self):
run_cxassoc(('--bottle', self.bottlename, '--sync',
'--mode', 'mime', '--removeall', '--install'))