Skip to content
Browse files

startup: add support for XDG user config dirs

Add support for XDG-style user config and data areas on POSIX systems, for
new users of MyPaint only. "User data" means a user's brushes, backgrounds,
and scratchpads. "User configurations" are the keybindings and preferences.

* User data will be stored in ~/.local/share/mypaint
* User configurations will be stored in ~/.config/mypaint

There is no migration script, but if ~/.mypaint exists it will continue to
work for now. Experienced users can migrate their existing configs and data
to the new location manually, and then delete the old .mypaint directory,
but be careful about scratchpad settings using the old directory's path.

The old command-line -c option will still make an old-style combined config
area. Use this if you need to be certain, or use the XDG environment vars.

On the Windows platform, both locations should use CSIDL_LOCAL_APPDATA same
as before - a nice quirk of what glib gives us. However, not all glib
versions do this correctly, so this commit also officially removes support
for pre-2.28 glib in order to support Windows more consistently.

Shorter version: we now require the lowest stable glib version which
includes commit 9d80c361418f94c609840ec9f83741aede7e482c. That's glib
2.28+ (Debian testing/Wheezy and up, or Ubuntu Oneiric).
  • Loading branch information...
achadwick committed Mar 12, 2013
1 parent 2f982ce commit cf723b74cde5d1878a2bc148e5e22b5c60a6e2a1
Showing with 193 additions and 113 deletions.
  1. +63 −37 gui/
  2. +5 −3 gui/
  3. +8 −17 gui/
  4. +74 −24 gui/
  5. +43 −32
@@ -10,8 +10,9 @@
import gettext
import os, sys
from os.path import join
import gtk, gobject
gdk = gtk.gdk
import gobject
import gtk
from gtk import gdk
from lib import brush, helpers, mypaintlib
import filehandling, keyboard, brushmanager, windowing, document, layout
import brushmodifier, linemode
@@ -29,37 +30,51 @@ class Application: # singleton
initialization, called by or by the testing scripts.

def __init__(self, datapath, extradata, confpath, filenames):
def __init__(self, filenames, app_datapath, app_extradatapath,
user_datapath, user_confpath, fullscreen=False):
"""Construct, but do not run.
Usually ``$PREFIX/share/mypaint``. Where MyPaint should find its
app-specific read-only data, e.g. UI definition XML, backgrounds
and brush defintions.
Where to find the defaults for MyPaint's themeable UI icons. This
will be effectively used in addition to ``$XDG_DATA_DIRS`` for the
purposes of icon lookup. Normally it's ``$PREFIX/share``, to support
unusual installations outside the usual locations. It should contain
an ``icons/`` subdirectory.
Where the user's configuration is stored. ``$HOME/.mypaint`` is
typical on Unix-like OSes.
:params filenames: The list of files to load.
Note: only the first is used.
:param app_datapath: App-specific read-only data area.
Path used for UI definition XML, and the default sets of backgrounds,
palettes, and brush defintions. Often $PREFIX/share/.
:param app_extradatapath: Extra search path for themeable UI icons.
This will be used in addition to $XDG_DATA_DIRS for the purposes of
icon lookup. Normally it's $PREFIX/share, to support unusual
installations outside the usual locations. It should contain an
icons/ subdirectory.
:param user_datapath: Location of the user's app-specific data.
For MyPaint, this means the user's brushes, backgrounds, and
scratchpads. Commonly $XDG_DATA_HOME/mypaint, i.e.
:param user_confpath: Location of the user's app-specific config area.
This is where MyPaint will save user preferences data and the
keyboard accelerator map. Commonly $XDG_CONFIG_HOME/mypaint, i.e.
:param fullscreen: Go fullscreen after starting.
self.confpath = confpath
self.datapath = datapath

self.user_confpath = user_confpath #: User configs (see __init__)
self.user_datapath = user_datapath #: User data (see __init__)

self.datapath = app_datapath

# create config directory, and subdirs where the user might drop files
# TODO make scratchpad dir something pulled from preferences #PALETTE1
for d in ['', 'backgrounds', 'brushes', 'scratchpads']:
d = os.path.join(self.confpath, d)
if not os.path.isdir(d):
print 'Created', d
for basedir in [self.user_confpath, self.user_datapath]:
if not os.path.isdir(basedir):
print 'Created basedir', basedir
for datasubdir in ['backgrounds', 'brushes', 'scratchpads']:
datadir = os.path.join(self.user_datapath, datasubdir)
if not os.path.isdir(datadir):
print 'Created data subdir', datadir

# Default location for our icons. The user's theme can override these.
icon_theme = gtk.icon_theme_get_default()
icon_theme.append_search_path(join(extradata, "icons"))
icon_theme.append_search_path(join(app_extradatapath, "icons"))

# Icon sanity check
if not icon_theme.has_icon('mypaint') \
@@ -77,7 +92,7 @@ def __init__(self, datapath, extradata, confpath, filenames):

# Stock items, core actions, and menu structure
builder_xml = join(datapath, "gui", "mypaint.xml")
builder_xml = join(self.datapath, "gui", "mypaint.xml")
self.builder = gtk.Builder()
@@ -113,7 +128,10 @@ def __init__(self, datapath, extradata, confpath, filenames):
self.scratchpad_doc = document.Document(self, leader=self.doc)
self.brushmanager = brushmanager.BrushManager(join(datapath, 'brushes'), join(confpath, 'brushes'), self)
self.brushmanager = brushmanager.BrushManager(
join(app_datapath, 'brushes'),
join(user_datapath, 'brushes'),
self.filehandler = filehandling.FileHandler(self)
self.brushmodifier = brushmodifier.BrushModifier(self)
@@ -131,7 +149,7 @@ def __init__(self, datapath, extradata, confpath, filenames):

self.brush_color_manager = BrushColorManager(self)


@@ -151,7 +169,8 @@ def __init__(self, datapath, extradata, confpath, filenames):
self.filehandler.doc = self.doc
self.filehandler.filename = None
pygtkcompat.gtk.accel_map_load(join(self.confpath, 'accelmap.conf'))

# Load the background settings window.
# FIXME: this line shouldn't be needed, but we need to load this up
@@ -170,34 +189,41 @@ def at_application_start(*junk):
if filenames:
# Open only the first file, no matter how many has been specified
# If the file does not exist just set it as the file to save to
fn = filenames[0].replace('file:///', '/') # some filebrowsers do this (should only happen with outdated mypaint.desktop)
fn = filenames[0].replace('file:///', '/')
# ^ some filebrowsers do this (should only happen with outdated
# mypaint.desktop)
if not os.path.exists(fn):
self.filehandler.filename = fn

# Load last scratchpad
if not self.preferences["scratchpad.last_opened_scratchpad"]:
self.preferences["scratchpad.last_opened_scratchpad"] = self.filehandler.get_scratchpad_autosave()
self.scratchpad_filename = self.preferences["scratchpad.last_opened_scratchpad"]
self.preferences["scratchpad.last_opened_scratchpad"] \
= self.filehandler.get_scratchpad_autosave()
self.scratchpad_filename \
= self.preferences["scratchpad.last_opened_scratchpad"]
if os.path.isfile(self.scratchpad_filename):
except AttributeError, e:
print "Scratchpad widget isn't initialised yet, so cannot centre"

if not self.pressure_devices:
print 'No pressure sensitive devices found.'

# Handle fullscreen command line option
if fullscreen:


def save_settings(self):
"""Saves the current settings to persistent storage."""
def save_config():
settingspath = join(self.confpath, 'settings.json')
settingspath = join(self.user_confpath, 'settings.json')
jsonstr = helpers.json_dumps(self.preferences)
f = open(settingspath, 'w')
@@ -221,15 +247,15 @@ def load_settings(self):
def get_legacy_config():
dummyobj = {}
tmpdict = {}
settingspath = join(self.confpath, 'settings.conf')
settingspath = join(self.user_confpath, 'settings.conf')
if os.path.exists(settingspath):
exec open(settingspath) in dummyobj
tmpdict['saving.scrap_prefix'] = dummyobj['save_scrap_prefix']
tmpdict['input.device_mode'] = dummyobj['input_devices_mode']
tmpdict['input.global_pressure_mapping'] = dummyobj['global_pressure_mapping']
return tmpdict
def get_json_config():
settingspath = join(self.confpath, 'settings.json')
settingspath = join(self.user_confpath, 'settings.json')
jsonstr = open(settingspath).read()
return helpers.json_loads(jsonstr)
@@ -526,7 +552,7 @@ def update_input_devices(self):
print ''

def save_gui_config(self):
pygtkcompat.gtk.accel_map_save(join(self.confpath, 'accelmap.conf'))
pygtkcompat.gtk.accel_map_save(join(self.user_confpath, 'accelmap.conf'))

def message_dialog(self, text, type=gtk.MESSAGE_INFO, flags=0,
@@ -71,7 +71,8 @@ def color_changed_cb(self, widget):

def save_as_default_cb(self):
pixbuf = self.current_background_pixbuf
path = os.path.join(, 'backgrounds', 'default.png')
path = os.path.join(,
'backgrounds', 'default.png'), path, 'png')

@@ -84,7 +85,8 @@ def add_color_to_patterns_cb(self, widget):
pixbuf = self.current_background_pixbuf
i = 1
while 1:
filename = os.path.join(, 'backgrounds', 'color%02d.png' % i)
filename = os.path.join(,
'backgrounds', 'color%02d.png' % i)
if not os.path.exists(filename):
i += 1
@@ -100,7 +102,7 @@ def __init__(self, win): = win

stock_path = os.path.join(, 'backgrounds')
user_path = os.path.join(, 'backgrounds')
user_path = os.path.join(, 'backgrounds')
if not os.path.isdir(user_path):
self.backgrounds = []
@@ -10,6 +10,7 @@
from glob import glob
import sys

import glib
import gtk
from gettext import gettext as _
from gettext import ngettext
@@ -588,33 +589,23 @@ def get_scrap_prefix(self):
return prefix

def get_scratchpad_prefix(self):
# TODO make this something pulled from preferences #PALETTE1
prefix = os.path.abspath(os.path.join(, 'scratchpads'))
# TODO allow override via prefs, maybe
prefix = os.path.join(, 'scratchpads')
prefix = os.path.abspath(prefix)
if os.path.isdir(prefix):
if not prefix.endswith(os.path.sep):
prefix += os.path.sep
return prefix

def get_scratchpad_default(self):
# TODO get the default name from preferences
return os.path.join(self.get_scratchpad_prefix(), "scratchpad_default.ora")
prefix = self.get_scratchpad_prefix()
return os.path.join(prefix, "scratchpad_default.ora")

def get_scratchpad_autosave(self):
# TODO get the default name from preferences
return os.path.join(self.get_scratchpad_prefix(), "autosave.ora")

def get_gimp_prefix(self):
from lib import helpers
homepath = helpers.expanduser_unicode(u'~')
if sys.platform == 'win32':
# using patched win32 glib using correct CSIDL_LOCAL_APPDATA
import glib
confpath = os.path.join(glib.get_user_config_dir().decode('utf-8'),'gimp-2.6')
elif homepath == '~':
confpath = os.path.join(prefix, 'UserData')
confpath = os.path.join(homepath, '.gimp-2.6')
return confpath
prefix = self.get_scratchpad_prefix()
return os.path.join(prefix, "autosave.ora")

def list_scraps(self):
prefix = self.get_scrap_prefix()
Oops, something went wrong.

0 comments on commit cf723b7

Please sign in to comment.
You can’t perform that action at this time.