Merge lp:~chaghi/zim/zim-profiles into lp:~jaap.karssenberg/zim/pyzim
- zim-profiles
- Merge into 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 | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jaap Karssenberg | Pending | ||
Review via email: mp+98101@code.launchpad.net |
Commit message
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 |