Zim

Merge lp:~chaghi/zim/zim-profiles into lp:~jaap.karssenberg/zim/pyzim

Proposed by Mariano Draghi
Status: Merged
Merged at revision: 524
Proposed branch: lp:~chaghi/zim/zim-profiles
Merge into: lp:~jaap.karssenberg/zim/pyzim
Diff against target: 777 lines (+425/-17)
12 files modified
tests/config.py (+31/-0)
tests/gui.py (+7/-0)
tests/notebook.py (+132/-2)
tests/pageview.py (+32/-1)
tests/plugins.py (+10/-0)
zim/__init__.py (+147/-8)
zim/config.py (+6/-0)
zim/gui/__init__.py (+11/-4)
zim/gui/pageview.py (+18/-1)
zim/notebook.py (+18/-0)
zim/plugins/__init__.py (+10/-1)
zim/plugins/automount.py (+3/-0)
To merge this branch: bzr merge lp:~chaghi/zim/zim-profiles
Reviewer Review Type Date Requested Status
Jaap Karssenberg Pending
Review via email: mp+98101@code.launchpad.net

Description of the change

Support for notebook profiles, and preferences and styles per profile. Fixes bug #539370 and bug #656446

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'tests/config.py'
2--- tests/config.py 2011-11-06 12:22:35 +0000
3+++ tests/config.py 2012-03-18 15:46:18 +0000
4@@ -366,6 +366,37 @@
5 ))
6 self.assertTrue(isinstance(mydict.order, list))
7
8+ def testChangeFile(self):
9+ '''Test changing the file used as datastore'''
10+ file = XDG_CONFIG_HOME.file('zim/config_TestConfigFile.conf')
11+ if file.exists():
12+ file.remove()
13+ assert not file.exists()
14+ conf = ConfigDictFile(file)
15+ conf['Foo']['xyz'] = 'foooooo'
16+ conf['Bar']['empty'] = ''
17+ conf.write()
18+ text = u'''\
19+[Foo]
20+xyz=foooooo
21+
22+[Bar]
23+empty=
24+
25+'''
26+ self.assertEqual(file.read(), text)
27+ file_new = XDG_CONFIG_HOME.file('zim/config_TestConfigFile2.conf')
28+ if file_new.exists():
29+ file_new.remove()
30+ assert not file_new.exists()
31+ conf.change_file(file_new)
32+ file.remove()
33+ conf.write()
34+ assert not file.exists()
35+ self.assertEqual(file_new.read(), text)
36+
37+ del conf
38+ file_new.remove()
39
40 class TestHeaders(TestCase):
41
42
43=== modified file 'tests/gui.py'
44--- tests/gui.py 2012-03-17 19:39:28 +0000
45+++ tests/gui.py 2012-03-18 15:46:18 +0000
46@@ -293,6 +293,7 @@
47 'icon': './icon.png',
48 'document_root': File('/foo').path, # win32 save test
49 'shared': False,
50+ 'profile': '',
51 }
52 config2 = {
53 'name': 'Notebook Bar',
54@@ -301,6 +302,7 @@
55 'icon': './picture.png',
56 'document_root': File('/bar').path, # win32 save test
57 'shared': True,
58+ 'profile': 'foo',
59 }
60 notebook.save_properties(**config1)
61 self.assertEqual(notebook.config['Notebook'], config1)
62@@ -417,6 +419,11 @@
63 items = menu.get_children()
64 self.assertGreater(len(items), 3)
65
66+ # open notebook (so the default plugins are loaded)
67+ nb = ui.notebook
68+ ui.notebook = None
69+ ui.open_notebook(nb)
70+
71 # remove plugins
72 self.assertGreater(len(ui.plugins), 3) # default plugins
73 plugins = [p.plugin_key for p in ui.plugins]
74
75=== modified file 'tests/notebook.py'
76--- tests/notebook.py 2011-12-11 14:03:58 +0000
77+++ tests/notebook.py 2012-03-18 15:46:18 +0000
78@@ -9,13 +9,13 @@
79 import os
80
81 from zim.fs import File, Dir
82-from zim.config import config_file
83+from zim.config import config_file, ConfigDictFile, XDG_CONFIG_HOME
84 from zim.notebook import *
85 from zim.index import *
86 import zim.errors
87 from zim.formats import ParseTree
88
89-from zim import _get_default_or_only_notebook
90+from zim import NotebookInterface, _get_default_or_only_notebook
91 # private, but want to check it anyway
92
93
94@@ -745,3 +745,133 @@
95
96 text = ''.join(notebook.get_page(Path('page3:page1:child')).dump('wiki'))
97 self.assertEqual(text, 'I have backlinks !\n')
98+
99+
100+class TestProfiles(tests.TestCase):
101+
102+ def setUp(self):
103+ path = self.get_tmp_name()
104+ self.notebook = tests.new_notebook(fakedir=path)
105+
106+ def testProfilePreferences(self):
107+ '''Test the profile is used and its preferences applied'''
108+
109+ assert not self.notebook.profile
110+
111+ # set up a test profile
112+ file = XDG_CONFIG_HOME.file('zim/profiles/profile_TestProfile.conf')
113+ if file.exists():
114+ file.remove()
115+ assert not file.exists()
116+ profile = ConfigDictFile(file)
117+ profile['GtkInterface'] = {}
118+ profile['General']['plugins'] = ['calendar',]
119+ profile['CalendarPlugin']['embedded'] = True
120+ profile['CalendarPlugin']['granularity'] = 'Week'
121+ profile['CalendarPlugin']['namespace'] = 'TestProfile'
122+ profile.write()
123+
124+ # se the profile name in the notebook, open it, and
125+ # check that the profila was applied
126+ self.notebook.config['Notebook']['profile'] = 'profile_TestProfile'
127+ self.assertEqual(self.notebook.profile, 'profile_TestProfile')
128+ interface = NotebookInterface(self.notebook)
129+ self.assertEqual(interface.preferences.file, file)
130+ self.assertTrue(len(interface.preferences['GtkInterface'].keys()) == 0)
131+ self.assertTrue(len(interface.plugins) == 1)
132+ self.assertEqual(interface.preferences['General']['plugins'][0],
133+ 'calendar')
134+ self.assertTrue(interface.preferences['CalendarPlugin']['embedded'])
135+ self.assertEqual(interface.preferences['CalendarPlugin']['granularity'],
136+ 'Week')
137+ self.assertEqual(interface.preferences['CalendarPlugin']['namespace'],
138+ 'TestProfile')
139+
140+
141+ def testNewProfile(self):
142+ '''Test that current preferences are used if the profile doesn't exist '''
143+ assert not self.notebook.profile
144+
145+ # create a completely default base configuration
146+ base = XDG_CONFIG_HOME.file('zim/preferences.conf')
147+ if base.exists():
148+ base.remove()
149+ assert not base.exists()
150+ interface = NotebookInterface(self.notebook)
151+ interface.preferences.write() # ensure the preferences are saved
152+ assert base.exists()
153+
154+ # set up a test profile
155+ file = XDG_CONFIG_HOME.file('zim/profiles/profile_TestProfile.conf')
156+ if file.exists():
157+ file.remove()
158+ assert not file.exists()
159+
160+ # change the profile name, and reload the profile
161+ # check that the current preferences didn't change
162+ self.notebook.config['Notebook']['profile'] = 'profile_TestProfile'
163+ self.assertEqual(self.notebook.profile, 'profile_TestProfile')
164+ interface.load_profile()
165+ self.assertEqual(interface.preferences.file, file)
166+ interface.preferences.write() # ensure the preferences are saved
167+
168+ self.assertEqual(file.read(), base.read())
169+
170+ def testPluginUnload(self):
171+ '''Test unloading plugins not present in the profile configuration'''
172+ assert not self.notebook.profile
173+
174+ # open notebook with the default profile
175+ interface = NotebookInterface(self.notebook)
176+ # we need more than one plugin loaded for this test
177+ assert len(interface.plugins) > 1
178+
179+ # create a profile just with the 1st loaded plugin
180+ plugin_to_keep = interface.plugins[0].plugin_key
181+ interface.preferences['General']['plugins'] = [plugin_to_keep,]
182+ file = XDG_CONFIG_HOME.file('zim/profiles/profile_TestProfile.conf')
183+ if file.exists():
184+ file.remove()
185+ assert not file.exists()
186+ interface.preferences.change_file(file)
187+ interface.preferences.write()
188+
189+ # load the new profile and check that all plugins but the one
190+ # we kept were unloaded
191+ self.notebook.config['Notebook']['profile'] = 'profile_TestProfile'
192+ interface.load_profile()
193+ self.assertEqual(len(interface.plugins), 1)
194+ self.assertEqual(interface.plugins[0].plugin_key, plugin_to_keep)
195+
196+ def testIndependentPluginsAreKept(self):
197+ '''Test that independent plugins already loaded are added to the profile if not present'''
198+ assert not self.notebook.profile
199+
200+ # ensure that the base configuration declares an independent plugin
201+ interface = NotebookInterface(self.notebook)
202+ if 'automount' not in interface.preferences['General']['plugins']:
203+ interface.preferences['General']['plugins'].append('automount')
204+ interface.preferences.write() # ensure the preferences are saved
205+ base = config_file('preferences.conf')
206+ assert base.file.exists()
207+ self.assertTrue('automount' in base['General']['plugins'])
208+
209+ # recreate the interface ensuring it's loading the automount plugin
210+ interface = NotebookInterface(self.notebook)
211+ self.assertTrue('automount' in [p.plugin_key for p in interface.plugins])
212+
213+ # create a profile without the automount plugin
214+ file = XDG_CONFIG_HOME.file('zim/profiles/profile_TestProfile.conf')
215+ if file.exists():
216+ file.remove()
217+ assert not file.exists()
218+ profile = ConfigDictFile(file)
219+ profile['General']['plugins'] = []
220+ profile.write()
221+
222+ # load the new profile and check that the automount plugin was kept
223+ self.notebook.config['Notebook']['profile'] = 'profile_TestProfile'
224+ interface.load_profile()
225+ self.assertTrue('automount' in interface.preferences['General']['plugins'])
226+ self.assertTrue('automount' in [p.plugin_key for p in interface.plugins])
227+
228
229=== modified file 'tests/pageview.py'
230--- tests/pageview.py 2012-02-17 23:41:57 +0000
231+++ tests/pageview.py 2012-03-18 15:46:18 +0000
232@@ -11,7 +11,7 @@
233 from zim.formats import wiki, ParseTree
234 from zim.notebook import Path
235 from zim.gui.pageview import *
236-from zim.config import ConfigDict
237+from zim.config import ConfigDict, ConfigDictFile, XDG_CONFIG_HOME
238 from zim.gui.clipboard import Clipboard
239
240
241@@ -1793,6 +1793,37 @@
242 buffer = textview.get_buffer()
243 self.assertEqual(buffer.get_text(*buffer.get_bounds()), 'Foo')
244
245+ def testProfile(self):
246+ '''Test that style for a specific profile is applied.'''
247+ # first test without profile
248+ pageview = setUpPageView()
249+ pageview.ui.notebook.config['Notebook']['profile'] = None
250+ pageview.on_preferences_changed(pageview.ui)
251+ file = XDG_CONFIG_HOME.file('zim/style.conf')
252+ self.assertEqual(pageview.style.file, file)
253+
254+ # create a new style based on the default one, changing some properties
255+ new_style = ConfigDictFile(file)
256+ new_style['TextView']['indent'] = 50
257+ new_style['TextView']['font'] = 'Sans 8'
258+ new_style['TextView']['linespacing'] = 10
259+ file = XDG_CONFIG_HOME.file('zim/styles/style_testProfile.conf')
260+ if file.exists():
261+ file.remove()
262+ new_style.change_file(file)
263+ new_style.write()
264+
265+ # test the pageview with the profile
266+ pageview.ui.notebook.config['Notebook']['profile'] = 'style_testProfile'
267+ pageview.on_preferences_changed(pageview.ui)
268+ self.assertEqual(pageview.style.file, file)
269+ self.assertEqual(pageview.style['TextView']['indent'], 50)
270+ self.assertEqual(pageview.style['TextView']['font'], 'Sans 8')
271+ self.assertEqual(pageview.style['TextView']['linespacing'], 10)
272+
273+ # if we don't have a notebook, we shouldn't fail!
274+ pageview.ui.notebook = None
275+ pageview.on_preferences_changed(pageview.ui)
276
277
278 class MockUI(tests.MockObject):
279
280=== modified file 'tests/plugins.py'
281--- tests/plugins.py 2012-01-09 20:51:16 +0000
282+++ tests/plugins.py 2012-03-18 15:46:18 +0000
283@@ -23,6 +23,9 @@
284 self.assertTrue('spell' in plugins)
285 self.assertTrue('linkmap' in plugins)
286
287+ # plugins listed here will be tested for is_profile_independent == True
288+ profile_independent = ['automount',]
289+
290 seen = {
291 'name': set(),
292 'description': set(),
293@@ -70,6 +73,13 @@
294 self.assertTrue(isinstance(dep[i][1],bool))
295 self.assertTrue(isinstance(dep[i][2],bool))
296
297+ # test is_profile_independent
298+ self.assertTrue(isinstance(plugin.is_profile_independent,bool))
299+ if name in profile_independent:
300+ self.assertTrue(plugin.is_profile_independent)
301+ else:
302+ self.assertFalse(plugin.is_profile_independent)
303+
304 def testDefaulPlugins(self):
305 '''Test loading default plugins'''
306 # Note that we use parent interface class here, so plugins
307
308=== modified file 'zim/__init__.py'
309--- zim/__init__.py 2012-02-28 20:28:58 +0000
310+++ zim/__init__.py 2012-03-18 15:46:18 +0000
311@@ -109,7 +109,8 @@
312
313 from zim.fs import File, Dir
314 from zim.errors import Error
315-from zim.config import data_dir, config_file, log_basedirs, ZIM_DATA_DIR
316+from zim.config import data_dir, config_file, log_basedirs, ZIM_DATA_DIR, \
317+ XDG_CONFIG_HOME, ConfigDictFile
318
319
320 logger = logging.getLogger('zim')
321@@ -500,6 +501,9 @@
322 @signal: C{open-notebook (notebook)}:
323 Emitted to open a notebook in this interface
324
325+ @signal: C{preferences-changed ()}:
326+ Emitted when preferences have changed
327+
328 @cvar ui_type: string to tell plugins what interface is supported
329 by this class. Currently this can be "gtk" or "html". If "ui_type"
330 is None we run without interface (e.g. commandline export).
331@@ -516,6 +520,7 @@
332 # define signals we want to use - (closure type, return type and arg types)
333 __gsignals__ = {
334 'open-notebook': (gobject.SIGNAL_RUN_LAST, None, (object,)),
335+ 'preferences-changed': (gobject.SIGNAL_RUN_LAST, None, ()),
336 }
337
338 ui_type = None
339@@ -531,32 +536,51 @@
340 self.notebook = None
341 self.plugins = []
342
343+ # Here we'll keep the list of the base plugins, i.e., those configured
344+ # in the general preferences.conf, or the default ones. This is
345+ # needed in two scenarios:
346+ # - When opening a Notebook, firt we'll load only the 'independent'
347+ # plugins, and will keep only those in the preferences. When the
348+ # profile is loaded, we'll need the complete list of plugins that
349+ # we were supposed to load.
350+ # - When changing to a profile that doesn't exist yet, we'll need
351+ # to know which are the base/default ones
352+ self._base_plugins = None
353+
354 self.preferences = config_file('preferences.conf')
355 self.uistate = None
356
357 if not notebook is None:
358 self.open_notebook(notebook)
359
360- def load_plugins(self):
361+ def load_plugins(self, independent_only=False):
362 '''Loads all the plugins defined in the preferences
363
364 Typically called from the constructor of sub-classes.
365+
366+ @param independent_only: if True, load only the plugins flagged
367+ as profile independent.
368+
369 '''
370 default = ['calendar', 'insertsymbol', 'printtobrowser', 'versioncontrol']
371 self.preferences['General'].setdefault('plugins', default)
372 plugins = self.preferences['General']['plugins']
373 plugins = set(plugins) # Eliminate doubles
374
375+ # Keep the base/default plugins
376+ if independent_only or self._base_plugins is None:
377+ self._base_plugins = sorted(plugins)
378+
379 # Plugins should not have dependency on order of being added
380 # but sort them here to make behavior predictable.
381 for name in sorted(plugins):
382- self.load_plugin(name)
383+ self.load_plugin(name, independent_only)
384
385 loaded = [p.plugin_key for p in self.plugins]
386 if set(loaded) != plugins:
387 self.preferences['General']['plugins'] = sorted(loaded)
388
389- def load_plugin(self, name):
390+ def load_plugin(self, name, independent_only=False):
391 '''Load a single plugin by name
392
393 Load an plugin object and attach it to the current application
394@@ -569,7 +593,10 @@
395 @param name: the plugin name as understood by
396 L{zim.plugins.get_plugin()}
397
398- @returns: the plugin object or C{None} when failed
399+ @param independent_only: if True, load the plugin only if it is
400+ flagged as profile independent. Otherwise ignore it
401+
402+ @returns: the plugin object or C{None} when failed or ignored
403
404 @todo: make load_plugin raise exception on failure
405 '''
406@@ -582,13 +609,19 @@
407
408 try:
409 klass = zim.plugins.get_plugin(name)
410- if not klass.check_dependencies_ok():
411- raise AssertionError, 'Dependencies failed for plugin %s' % name
412- plugin = klass(self)
413+ if independent_only and not klass.is_profile_independent:
414+ plugin = None
415+ logger.debug('Ignoring plugin %s.', name)
416+ else:
417+ if not klass.check_dependencies_ok():
418+ raise AssertionError, 'Dependencies failed for plugin %s' % name
419+ plugin = klass(self)
420 except:
421 logger.exception('Failed to load plugin %s', name)
422 return None
423 else:
424+ if not plugin:
425+ return None
426 self.plugins.append(plugin)
427 logger.debug('Loaded plugin %s (%s)', name, plugin)
428
429@@ -677,6 +710,110 @@
430 self.emit('open-notebook', notebook)
431 return None
432
433+ def load_profile(self, profile_changed=True):
434+ '''Load the specific profile for a Notebook.
435+
436+ If the notebook defines its own profile, update the preferences
437+ with it. Check if there are any plugins to load and initialize them.
438+
439+ @param profile_changed: indicates that the profile is being loaded
440+ on top of a previous one. When opening the L{Notebook} it will be
441+ False. Any other call should use True
442+
443+ @emits: preferences-changed
444+ '''
445+ assert not self.notebook is None, 'BUG: Must open a notebook first'
446+
447+ # restore the complete list of base/default plugins, if needed
448+ # this is important if we are changing the profile (maybe the new one
449+ # does not exist yet) or if we don't have a profile (we are either
450+ # opening the notebook, or going back to the default profile)
451+ if (profile_changed or not self.notebook.profile) and \
452+ self._base_plugins is not None:
453+ self.preferences['General']['plugins'] = self._base_plugins
454+
455+ profile = None
456+ if self.notebook.profile:
457+ # use a specific profile. If it exists, merge its preferences
458+ logger.debug('Using profile %s', self.notebook.profile)
459+ file = XDG_CONFIG_HOME.file(('zim','profiles',self.notebook.profile + '.conf'))
460+ if file.exists():
461+ profile = ConfigDictFile(file)
462+ self._merge_profile_preferences(profile)
463+ else:
464+ # use the general/base profile
465+ logger.debug('Using the base profile')
466+ base_profile = config_file('preferences.conf')
467+ file = base_profile.file
468+ if profile_changed:
469+ self._merge_profile_preferences(base_profile)
470+
471+ if profile_changed or profile:
472+ # unload any loaded plugins not present in the merged
473+ # preferenfces. If we are changing the profile, or have just
474+ # loaded a specific one, we might have loaded plugins that don't
475+ # belong to this configuration
476+ for plugin in [p for p in self.plugins if p.plugin_key not in self.preferences['General']['plugins']]:
477+ # we don't use unload_plugin because it would trigger
478+ # a preferences.write() !!!
479+ plugin.disconnect()
480+ self.plugins.remove(plugin)
481+ logger.debug('Unloaded plugin %s', plugin.plugin_key)
482+
483+ # Load the plugins. This will pull the non-independent plugins
484+ # when opening a Notebook, or the complete set of plugins configured
485+ # for the profile we're just loading
486+ self.load_plugins()
487+
488+ # change the file used to store the preferences, if needed
489+ if profile_changed or self.notebook.profile:
490+ self.preferences.change_file(file)
491+ self.emit('preferences-changed')
492+ if profile_changed:
493+ # refresh the preferences for all the loaded plugins
494+ for plugin in self.plugins:
495+ plugin.preferences = self.preferences[plugin.__class__.__name__]
496+ plugin.emit('preferences-changed')
497+
498+
499+ def _merge_profile_preferences(self, conf):
500+ logger.debug('Merging profile preferences')
501+
502+ # sections of the preferences that might be overriden
503+ # by a notebook profile
504+ overridable_sections = ['GtkInterface', 'PageView', 'General',]
505+
506+ # Override the preferences with the notebook's own profile
507+ for section in overridable_sections:
508+ if conf.has_key(section):
509+ self.preferences[section] = conf[section]
510+ logger.debug('Overriding section %s with with the configured profile', section)
511+
512+ # replace the preferences for each plugin with the ones defined
513+ # in the profile. Ignore the profile independent ones.
514+ import zim.plugins
515+ for name in self.preferences['General']['plugins']:
516+ try:
517+ klass = zim.plugins.get_plugin(name)
518+ except:
519+ logger.exception('Failed to find plugin %s', name)
520+ continue
521+
522+ if klass.is_profile_independent:
523+ continue
524+ config_key = klass.__name__
525+ if conf.has_key(config_key):
526+ self.preferences[config_key] = conf[config_key]
527+ logger.debug('Overriding section %s with the configured profile', config_key)
528+ # add any independent plugins already loaded to the profile
529+ # configuration
530+ plugins = self.preferences['General']['plugins']
531+ loaded = [p.plugin_key for p in self.plugins \
532+ if p.is_profile_independent and p.plugin_key not in plugins]
533+ plugins.extend(loaded)
534+ self.preferences['General']['plugins'] = sorted(plugins)
535+
536+
537 def do_open_notebook(self, notebook):
538 assert self.notebook is None, 'BUG: other notebook opened already'
539 self.notebook = notebook
540@@ -689,6 +826,8 @@
541 from zim.config import ConfigDict
542 self.uistate = ConfigDict()
543
544+ self.load_profile(False)
545+
546 def cmd_export(self, format='html', template=None, page=None, output=None, root_url=None, index_page=None):
547 '''Convenience method hat wraps L{zim.exporter.Exporter} for
548 commandline export
549
550=== modified file 'zim/config.py'
551--- zim/config.py 2012-03-17 17:01:16 +0000
552+++ zim/config.py 2012-03-18 15:46:18 +0000
553@@ -770,6 +770,12 @@
554 self.set_modified(False)
555 return operation
556
557+ def change_file(self, file):
558+ '''Change the underlaying file used to read/write data
559+ '''
560+ self.file = file
561+ self.set_modified(True)
562+
563
564 class ConfigDictFile(ConfigFile, ConfigDict):
565 pass
566
567=== modified file 'zim/gui/__init__.py'
568--- zim/gui/__init__.py 2012-03-17 19:39:28 +0000
569+++ zim/gui/__init__.py 2012-03-18 15:46:18 +0000
570@@ -378,7 +378,6 @@
571 'open-page': (gobject.SIGNAL_RUN_LAST, None, (object, object)),
572 'close-page': (gobject.SIGNAL_RUN_LAST, None, (object, bool)),
573 'new-window': (gobject.SIGNAL_RUN_LAST, None, (object,)),
574- 'preferences-changed': (gobject.SIGNAL_RUN_LAST, None, ()),
575 'readonly-changed': (gobject.SIGNAL_RUN_LAST, None, ()),
576 'quit': (gobject.SIGNAL_RUN_LAST, None, ()),
577 'start-index-update': (gobject.SIGNAL_RUN_LAST, None, ()),
578@@ -479,7 +478,10 @@
579 lambda o, event: event.keyval == gtk.keysyms.F6
580 and self.mainwindow.toggle_fullscreen())
581
582- self.load_plugins()
583+ # If opening a notebook, load only the independent plugins at
584+ # this stage.
585+ independent_only = notebook is not None
586+ self.load_plugins(independent_only)
587
588 self._custom_tool_ui_id = None
589 self._custom_tool_actiongroup = None
590@@ -542,8 +544,8 @@
591 else:
592 pass # Will check default in main()
593
594- def load_plugin(self, name):
595- plugin = NotebookInterface.load_plugin(self, name)
596+ def load_plugin(self, name, independent_only=False):
597+ plugin = NotebookInterface.load_plugin(self, name, independent_only)
598 if plugin and self._finalize_ui:
599 plugin.finalize_ui(self)
600
601@@ -1179,6 +1181,11 @@
602 for action in ('open_document_root', 'open_document_folder'):
603 action = self.actiongroup.get_action(action)
604 action.set_sensitive(has_doc_root)
605+ # check if the profile was changed, and load the new one
606+ if notebook.profile_changed:
607+ logger.debug('Profile changed to "%s"', notebook.profile)
608+ notebook.profile_changed = False # clear the flag
609+ self.load_profile(True) # load the profile
610
611 def open_page(self, path=None):
612 '''Method to open a page in the mainwindow, and menu action for
613
614=== modified file 'zim/gui/pageview.py'
615--- zim/gui/pageview.py 2012-03-17 16:01:31 +0000
616+++ zim/gui/pageview.py 2012-03-18 15:46:18 +0000
617@@ -31,7 +31,7 @@
618 from zim.errors import Error
619 from zim.notebook import Path, interwiki_link
620 from zim.parsing import link_type, Re, url_re
621-from zim.config import config_file
622+from zim.config import config_file, ConfigDictFile, XDG_CONFIG_HOME
623 from zim.formats import get_format, increase_list_iter, \
624 ParseTree, TreeBuilder, ParseTreeBuilder, \
625 BULLET, CHECKED_BOX, UNCHECKED_BOX, XCHECKED_BOX
626@@ -4425,6 +4425,7 @@
627 #~ action.connect('activate', lambda o, *a: logger.warn(o.get_name()))
628 action.connect('activate', self.do_toggle_format_action)
629
630+ self._profile = None # last used profile to avoid reloading the conf
631 if self.style is None:
632 PageView.style = config_file('style.conf')
633 self.on_preferences_changed(self.ui)
634@@ -4437,6 +4438,22 @@
635 self.view.grab_focus()
636
637 def on_preferences_changed(self, ui):
638+ if ui.notebook and (not self._profile or self._profile != ui.notebook.profile):
639+ # the profile has changed. Keep record of the new one
640+ # and if there's a style for the profile, use it
641+ self._profile = ui.notebook.profile # update current profile
642+ if self._profile:
643+ file = XDG_CONFIG_HOME.file(('zim','styles',self._profile + '.conf'))
644+ if self._profile and file.exists():
645+ # use the specific style
646+ PageView.style.change_file(file)
647+ PageView.style.read()
648+ logger.debug('Loaded specific style for profile %s',
649+ self._profile)
650+ else:
651+ # use the general style
652+ PageView.style = config_file('style.conf')
653+ logger.debug('Using the general style')
654 self._reload_style()
655 self.view.set_cursor_visible(
656 self.preferences['read_only_cursor'] or not self.readonly)
657
658=== modified file 'zim/notebook.py'
659--- zim/notebook.py 2012-02-17 19:36:00 +0000
660+++ zim/notebook.py 2012-03-18 15:46:18 +0000
661@@ -534,6 +534,7 @@
662 if os.name == 'nt': endofline = 'dos'
663 else: endofline = 'unix'
664 config['Notebook']['endofline'] = endofline
665+ config['Notebook']['profile'] = None
666 config.write()
667
668
669@@ -688,6 +689,10 @@
670 @ivar config: A L{ConfigDict} for the notebook config
671 (the C{X{notebook.zim}} config file in the notebook folder)
672 @ivar lock: An L{AsyncLock} for async notebook operations
673+ @ivar profile: The name of the profile used by the notebook (empty means
674+ default profile)
675+ @ivar profile_changed: Flag indicating if the profile was changed and the
676+ new one has to be loaded
677
678 In general this lock is not needed when only reading data from
679 the notebook. However it should be used when doing operations that
680@@ -723,6 +728,7 @@
681 ('interwiki', 'string', _('Interwiki Keyword'), lambda v: not v or is_interwiki_keyword_re.search(v)), # T: label for properties dialog
682 ('icon', 'image', _('Icon')), # T: label for properties dialog
683 ('document_root', 'dir', _('Document Root')), # T: label for properties dialog
684+ ('profile', 'string', _('Profile')), # T: label for properties dialog
685 ('shared', 'bool', _('Shared Notebook')), # T: label for properties dialog
686 #~ ('autosave', 'bool', _('Auto-version when closing the notebook')),
687 # T: label for properties dialog
688@@ -750,6 +756,7 @@
689 # async file operations. This one is more abstract for the
690 # notebook as a whole, regardless of storage
691 self.readonly = True
692+ self.profile_changed = False
693
694 if dir:
695 assert isinstance(dir, Dir)
696@@ -797,6 +804,7 @@
697 else: endofline = 'unix'
698 self.config['Notebook'].setdefault('endofline', endofline, check=set(('dos', 'unix')))
699 self.config['Notebook'].setdefault('disable_trash', False)
700+ self.config['Notebook'].setdefault('profile', '', check=basestring)
701
702 self.do_properties_changed()
703
704@@ -834,6 +842,11 @@
705
706 return NotebookInfo(uri, **self.config['Notebook'])
707
708+ @property
709+ def profile(self):
710+ '''The 'profile' property for this notebook'''
711+ return self.config['Notebook'].get('profile', '')
712+
713 def _cache_dir(self, dir):
714 from zim.config import XDG_CACHE_HOME
715 if os.name == 'nt':
716@@ -874,6 +887,11 @@
717 if 'home' in properties and isinstance(properties['home'], Path):
718 properties['home'] = properties['home'].name
719
720+ # When handling properties-changed later, we'll need to know
721+ # which is the current profile, to detect if it's changed
722+ if 'profile' in properties and properties['profile'] != self.profile:
723+ self.profile_changed = True
724+
725 self.config['Notebook'].update(properties)
726 self.config.write()
727 self.emit('properties-changed')
728
729=== modified file 'zim/plugins/__init__.py'
730--- zim/plugins/__init__.py 2011-08-06 17:18:57 +0000
731+++ zim/plugins/__init__.py 2012-03-18 15:46:18 +0000
732@@ -246,7 +246,8 @@
733 will be initialized when the plugin is loaded.
734
735 Plugin classes should define two class attributes: L{plugin_info} and
736- L{plugin_preferences}.
737+ L{plugin_preferences}. Optionally, they can also define the class
738+ attribute L{is_profile_independent}.
739
740 @cvar plugin_info: A dict with basic information about the plugin,
741 it should contain at least the following keys:
742@@ -275,6 +276,12 @@
743 Changes to these preferences will be stored in a config file so
744 they are persistent.
745
746+ @cvar is_profile_independent: A boolean indicating that the plugin
747+ configuration is global and not meant to change between notebooks.
748+ The default value (if undefined) is False. Plugins that set
749+ L{is_profile_independent} to True will be initialized before
750+ opening the notebook.
751+
752 @ivar ui: the main application object, e.g. an instance of
753 L{zim.gui.GtkInterface} or L{zim.www.WWWInterface}
754 @ivar preferences: a C{ListDict()} with plugin preferences
755@@ -304,6 +311,8 @@
756
757 plugin_preferences = ()
758
759+ is_profile_independent = False
760+
761 @classmethod
762 def check_dependencies_ok(klass):
763 '''Checks minimum dependencies are met
764
765=== modified file 'zim/plugins/automount.py'
766--- zim/plugins/automount.py 2011-12-11 18:35:35 +0000
767+++ zim/plugins/automount.py 2012-03-18 15:46:18 +0000
768@@ -26,6 +26,9 @@
769 'help': 'Plugins:Automount',
770 }
771
772+ # this plugin is profile independent
773+ is_profile_independent = True
774+
775 def get_config(self, uri):
776 '''Return the automount config for a specific notebook uri or C{None}
777 @param uri: a notebook uri