Ejemplo n.º 1
0
    def __init__(self, notebook, page, config, navigation):
        Window.__init__(self)
        self.navigation = navigation

        self.set_title(page.name + ' - Zim')
        #if ui.notebook.icon:
        #	try:
        #		self.set_icon_from_file(ui.notebook.icon)
        #	except GObject.GError:
        #		logger.exception('Could not load icon %s', ui.notebook.icon)

        page = notebook.get_page(page)

        if hasattr(config, 'uistate'):
            self.uistate = config.uistate['PageWindow']
        else:
            self.uistate = ConfigDict()

        self.uistate.setdefault('windowsize', (500, 400), check=value_is_coord)
        w, h = self.uistate['windowsize']
        self.set_default_size(w, h)

        self.pageview = PageView(notebook, config, navigation, secondary=True)
        self.pageview.set_page(page)
        self.add(self.pageview)
Ejemplo n.º 2
0
	def parse_attrib(self, attrib):
		'''Convenience method to enforce the supported attributes and their
		types.
		@returns: a L{ConfigDict} using the C{object_attr} dict as definition
		'''
		if not isinstance(attrib, ConfigDict):
			attrib = ConfigDict(attrib)
			attrib.define(self.object_attr)
		return attrib
Ejemplo n.º 3
0
	def setUp(self):
		self.PATHS = ('Parent:Daughter:Granddaughter',
				 'Test:tags', 'Test:foo', 'Books')
		self.LEN_PATHS = len(self.PATHS)
		self.PATHS_NAMES = {self.PATHS[0]:'name 1', self.PATHS[1]:'name 2', self.PATHS[2]:'name 3'}

		self.uistate = ConfigDict()
		self.uistate.setdefault('bookmarks', [])
		self.uistate.setdefault('bookmarks_names', {})
		self.uistate.setdefault('show_full_page_name', True)
Ejemplo n.º 4
0
	def __init__(self, dir=None, file=None, config=None, index=None):
		assert not (dir and file), 'BUG: can not provide both dir and file '
		gobject.GObject.__init__(self)
		self._namespaces = []	# list used to resolve stores
		self._stores = {}		# dict mapping namespaces to stores
		self.namespace_properties = HierarchicDict()
		self._page_cache = weakref.WeakValueDictionary()
		self.dir = None
		self.file = None
		self.cache_dir = None
		self.name = None
		self.icon = None
		self.config = config

		if dir:
			assert isinstance(dir, Dir)
			self.dir = dir
			self.readonly = not dir.iswritable()
			self.cache_dir = dir.subdir('.zim')
			if self.readonly or not self.cache_dir.iswritable():
				self.cache_dir = self._cache_dir(dir)
			logger.debug('Cache dir: %s', self.cache_dir)
			if self.config is None:
				self.config = ConfigDictFile(dir.file('notebook.zim'))
			# TODO check if config defined root namespace
			self.add_store(Path(':'), 'files') # set root
			# TODO add other namespaces from config
		elif file:
			assert isinstance(file, File)
			self.file = file
			self.readonly = not file.iswritable()
			assert False, 'TODO: support for single file notebooks'

		if index is None:
			import zim.index # circular import
			self.index = zim.index.Index(notebook=self)
		else:
			self.index = index
			self.index.set_notebook(self)

		if self.config is None:
			self.config = ConfigDict()

		self.config['Notebook'].setdefault('name', None, klass=basestring)
		self.config['Notebook'].setdefault('home', ':Home', klass=basestring)
		self.config['Notebook'].setdefault('icon', None, klass=basestring)
		self.config['Notebook'].setdefault('document_root', None, klass=basestring)
		self.config['Notebook'].setdefault('slow_fs', False)
		self.do_properties_changed()
Ejemplo n.º 5
0
    def __init__(self):
        tests.MockObject.__init__(self)

        self.pageview = setUpPageView()
        self.uimanager = tests.MockObject()
        self.ui = tests.MockObject()
        self.ui.uistate = ConfigDict()
Ejemplo n.º 6
0
	def runTest(self):
		window = tests.MockObject()
		window.pageview = setUpPageView()
		window.ui = tests.MockObject()
		window.ui.uimanager = tests.MockObject()
		window.ui.uistate = ConfigDict()
		window.ui.mainwindow = window # XXX

		plugin = TableEditorPlugin()
		extension = MainWindowExtension(plugin, window)

		with tests.DialogContext(self.checkInsertTableDialog):
			extension.insert_table()

		tree = window.pageview.get_parsetree()
		#~ print tree.tostring()
		obj = tree.find('table')

		self.assertTrue(obj.attrib['aligns'] == 'left')
		self.assertTrue(obj.attrib['wraps'] == '0')

		# Parses tree to a table object
		tabledata = tree.tostring().replace("<?xml version='1.0' encoding='utf-8'?>", '')\
			.replace('<zim-tree>', '').replace('</zim-tree>', '')\
			.replace('<td> </td>', '<td>text</td>')

		table = plugin.create_table({'type': 'table'}, ElementTree.fromstring(tabledata))

		self.assertTrue(isinstance(table, TableViewObject))
Ejemplo n.º 7
0
	def setUp(self):
		self.PATHS = ('Parent:Daughter:Granddaughter',
				 'Test:tags', 'Test:foo', 'Books')
		self.LEN_PATHS = len(self.PATHS)
		self.PATHS_NAMES = {self.PATHS[0]:'name 1', self.PATHS[1]:'name 2', self.PATHS[2]:'name 3'}

		self.uistate = ConfigDict()
		self.uistate.setdefault('bookmarks', [])
		self.uistate.setdefault('bookmarks_names', {})
		self.uistate.setdefault('show_full_page_name', True)
Ejemplo n.º 8
0
	def runTest(self):
		ui = MockUI()
		ui.notebook = tests.new_notebook()
		uistate = ConfigDict()
		widget = TagsPluginWidget(ui.notebook.index, uistate, ui)

		# Excersize all model switches and check we still have a sane state
		widget.toggle_treeview()
		widget.toggle_treeview()

		path = Path('Test:tags')
		ui.notebook.pages.lookup_by_pagename(path)
		treepath = widget.treeview.get_model().find(path)

		widget.disconnect_model()
		widget.reconnect_model()

		path = Path('Test:tags')
		treepath = widget.treeview.get_model().find(path)

		# Check signals
		widget.treeview.emit('populate-popup', gtk.Menu())
		widget.treeview.emit('insert-link', path)

		# Toggles in popup
		widget.toggle_show_full_page_name()
		widget.toggle_show_full_page_name()

		# Check tag filtering
		cloud = widget.tagcloud
		self.assertFalse(cloud.get_tag_filter())
		tag = None
		for button in cloud.get_children():
			if button.indextag.name == 'tags':
				tag = button.indextag
				button.clicked()
				break
		else:
			raise AssertionError('No button for @tags ?')

		selected = cloud.get_tag_filter()
		self.assertEqual(selected, [tag])
		model = widget.treeview.get_model()
		self.assertIsInstance(model, TaggedPageTreeStore)
		self.assertEqual(model.tags, [tag.name])

		# check menu and sorting of tag cloud
		cloud.emit('populate-popup', gtk.Menu())
		mockaction = tests.MockObject()
		mockaction.get_active = lambda: True
		cloud._switch_sorting(mockaction)
		mockaction.get_active = lambda: False
		cloud._switch_sorting(mockaction)
Ejemplo n.º 9
0
	def testSerialize(self):
		'''Test parsing the history from the state file'''
		uistate = ConfigDict()
		history = History(self.notebook, uistate)

		for page in self.pages:
			history.append(page)
		self.assertHistoryEquals(history, self.pages)
		self.assertCurrentEquals(history, self.pages[-1])

		# rewind 2
		for i in range(2):
			prev = history.get_previous()
			history.set_current(prev)

		# check state
		#~ import pprint
		#~ pprint.pprint(uistate)
		self.assertHistoryEquals(history, uistate['History']['list'])
		self.assertRecentEquals(history, uistate['History']['recent'])
		self.assertEqual(uistate['History']['current'], len(self.pages) - 3)

		# clone uistate by text
		lines = uistate.dump()
		newuistate = ConfigDict()
		newuistate.parse(lines)

		# check new state
		self.assertHistoryEquals(history, [Path(t[0]) for t in newuistate['History']['list']])
		self.assertRecentEquals(history, [Path(t[0]) for t in newuistate['History']['recent']])
		self.assertEqual(newuistate['History']['current'], len(self.pages) - 3)

		# and compare resulting history object
		newhistory = History(self.notebook, newuistate)
		self.assertEqual(list(newhistory.get_history()), list(history.get_history()))
		self.assertEqual(list(newhistory.get_recent()), list(history.get_recent()))
		self.assertEqual(newhistory.get_current(), history.get_current())

		# Check recent is initialized if needed
		newuistate = ConfigDict()
		newuistate.parse(lines)
		newuistate['History'].pop('recent')
		newhistory = History(self.notebook, newuistate)

		self.assertEqual(list(newhistory.get_history()), list(history.get_history()))
		self.assertEqual(list(newhistory.get_recent()), list(history.get_recent()))
		self.assertEqual(newhistory.get_current(), history.get_current())
Ejemplo n.º 10
0
	def testSerialize(self):
		'''Test parsing the history from the state file'''
		uistate = ConfigDict()
		history = History(self.notebook, uistate)

		for page in self.pages:
			history.append(page)
		self.assertEqual(len(history.history), len(self.pages))
		self._assertCurrent(history, self.pages[-1])

		# rewind 2
		for i in range(2):
			prev = history.get_previous()
			history.set_current(prev)

		# check state
		#~ import pprint
		#~ pprint.pprint(uistate)
		self.assertEqual(len(uistate['History']['history']), len(history.history))
		#~ self.assertEqual(len(uistate['History']['pages']), len(history.history))
		self.assertEqual(uistate['History']['current'], len(history.history)-3)

		# clone uistate by text
		lines = uistate.dump()
		newuistate = ConfigDict()
		newuistate.parse(lines)

		# check new state
		self.assertEqual(len(uistate['History']['history']), len(history.history))
		#~ self.assertEqual(len(newuistate['History']['pages']), len(history.history))
		self.assertEqual(newuistate['History']['current'], len(history.history)-3)

		# and compare resulting history object
		newhistory = History(self.notebook, newuistate)
		self.assertEqual(newhistory.history, history.history)
		self.assertEqual(newhistory.current, history.current)
Ejemplo n.º 11
0
    def runTest(self):
        window = tests.MockObject()
        window.pageview = setUpPageView()
        window.ui = tests.MockObject()
        window.ui.uimanager = tests.MockObject()
        window.ui.uistate = ConfigDict()
        window.ui.mainwindow = window  # XXX

        plugin = SourceViewPlugin()
        extension = MainWindowExtension(plugin, window)

        with tests.DialogContext(self.checkInsertCodeBlockDialog):
            extension.insert_sourceview()

        tree = window.pageview.get_parsetree()
        #~ print tree.tostring()
        obj = tree.find('object')
        self.assertTrue(obj.attrib['type'] == 'code')
Ejemplo n.º 12
0
    def runTest(self):
        ui = MockUI()
        ui.notebook = tests.new_notebook()
        uistate = ConfigDict()
        widget = TagsPluginWidget(ui.notebook.index, uistate, ui)

        # Excersize all model switches and check we still have a sane state
        widget.toggle_treeview()
        widget.toggle_treeview()

        path = Path('Test:foo')
        treepath = widget.treeview.get_model().get_treepath(path)
        self.assertTrue(not treepath is None)

        widget.disconnect_model()
        widget.reload_model()

        path = Path('Test:foo')
        treepath = widget.treeview.get_model().get_treepath(path)
        self.assertTrue(not treepath is None)

        # Check signals
        #~ widget.treeview.emit('popup-menu')
        widget.treeview.emit('insert-link', path)

        # Check tag filtering
        cloud = widget.tagcloud
        self.assertEqual(cloud.get_tag_filter(), None)
        tag = None
        for button in cloud.get_children():
            if button.indextag.name == 'tags':
                tag = button.indextag
                button.clicked()
                break
        else:
            raise AssertionError, 'No button for @tags ?'

        selected, filtered = cloud.get_tag_filter()
        self.assertEqual(selected, [tag])
        self.assertTrue(len(filtered) > 3)
        self.assertTrue(tag in filtered)

        self.assertTrue(not widget.treeview._tag_filter is None)

        # check filtering in treestore
        tagfilter = (selected, filtered)
        selected = frozenset(selected)
        filtered = frozenset(filtered)

        def toplevel(model):
            iter = model.get_iter_first()
            assert not iter is None
            while not iter is None:
                yield iter
                iter = model.iter_next(iter)

        def childiter(model, iter):
            iter = model.iter_children(iter)
            assert not iter is None
            while not iter is None:
                yield iter
                iter = model.iter_next(iter)

        self.assertEqual(uistate['treeview'], 'tagged')
        filteredmodel = widget.treeview.get_model()
        for iter in toplevel(filteredmodel):
            path = filteredmodel.get_indexpath(iter)
            self.assertTrue(not path is None)
            tags = list(ui.notebook.index.list_tags(path))
            tags = frozenset(tags)
            self.assertTrue(
                selected.issubset(tags))  # Needs to contains selected tags
            self.assertTrue(tags.issubset(
                filtered))  # All other tags should be in the filter selection
            treepaths = filteredmodel.get_treepaths(path)
            self.assertTrue(filteredmodel.get_path(iter) in treepaths)

        widget.toggle_treeview()
        self.assertEqual(uistate['treeview'], 'tags')
        filteredmodel = widget.treeview.get_model()
        for iter in toplevel(filteredmodel):
            self.assertEqual(filteredmodel.get_indexpath(iter), None)
            # toplevel has tags, not pages
            tag = filteredmodel[iter][PATH_COL]
            self.assertTrue(tag in filtered)
            for iter in childiter(filteredmodel, iter):
                path = filteredmodel.get_indexpath(iter)
                self.assertTrue(not path is None)
                tags = list(ui.notebook.index.list_tags(path))
                tags = frozenset(tags)
                self.assertTrue(
                    selected.issubset(tags))  # Needs to contains selected tags
                self.assertTrue(
                    tags.issubset(filtered)
                )  # All other tags should be in the filter selection
                treepaths = filteredmodel.get_treepaths(path)
                self.assertTrue(filteredmodel.get_path(iter) in treepaths)
Ejemplo n.º 13
0
class TestBookmarksBar(tests.TestCase):

	@classmethod
	def setUpClass(cls):
		cls.notebook = tests.new_notebook()
		cls.index = cls.notebook.index
		cls.ui = MockUI()
		cls.ui.notebook = cls.notebook
		cls.ui.page = Path('Test:foo')


	def setUp(self):
		self.PATHS = ('Parent:Daughter:Granddaughter',
				 'Test:tags', 'Test:foo', 'Books')
		self.LEN_PATHS = len(self.PATHS)
		self.PATHS_NAMES = {self.PATHS[0]:'name 1', self.PATHS[1]:'name 2', self.PATHS[2]:'name 3'}

		self.uistate = ConfigDict()
		self.uistate.setdefault('bookmarks', [])
		self.uistate.setdefault('bookmarks_names', {})
		self.uistate.setdefault('show_full_page_name', True)


	def testGeneral(self):
		'''Test general functions: add, delete bookmarks.'''

		self.assertTrue(self.notebook.get_page(self.ui.page).exists())

		Bar = BookmarkBar(self.ui, self.uistate, get_page_func = lambda: '')
		Bar.max_bookmarks = 15 # set maximum number of bookmarks

		# Add paths to the beginning of the bar.
		for i, path in enumerate(self.PATHS):
			Bar._add_new(path, add_bookmarks_to_beginning = True)
			self.assertEqual(len(Bar.paths), i + 1)
		self.assertTrue(Bar.paths == list(reversed(self.PATHS)))

		# Add paths to the end of the bar.
		Bar.paths = []
		for i, path in enumerate(self.PATHS):
			Bar._add_new(path, add_bookmarks_to_beginning = False)
			self.assertEqual(len(Bar.paths), i + 1)
		self.assertEqual(Bar.paths, list(self.PATHS))

		# Check that the same path can't be added to the bar.
		Bar._add_new(self.PATHS[0])
		Bar._add_new(self.PATHS[1])
		self.assertEqual(Bar.paths, list(self.PATHS))

		# Delete paths from the bar.
		for i, button in enumerate(Bar.container.get_children()[2:]):
			path = button.zim_path
			self.assertTrue(path in Bar.paths)
			Bar.delete(button.zim_path)
			self.assertEqual(len(Bar.paths), self.LEN_PATHS - i - 1)
			self.assertTrue(path not in Bar.paths)
		self.assertEqual(Bar.paths, [])

		# Delete all bookmarks from the bar.
		Bar.delete_all()
		self.assertEqual(Bar.paths, [])


	def testDeletePages(self):
		'''Check deleting a bookmark after deleting a page in the notebook.'''

		notebook = tests.new_notebook()
		ui = MockUI()
		ui.notebook = notebook
		self.uistate['bookmarks'] = list(self.PATHS)

		Bar = BookmarkBar(ui, self.uistate, get_page_func = lambda: '')
		for i, path in enumerate(self.PATHS):
			self.assertTrue(path in Bar.paths)
			notebook.delete_page(Path(path))
			self.assertTrue(path not in Bar.paths)
			self.assertEqual(len(Bar.paths), self.LEN_PATHS - i - 1)
		self.assertEqual(Bar.paths, [])


	def testFunctions(self):
		'''Test bookmark functions: changing, reordering, ranaming.'''

		Bar = BookmarkBar(self.ui, self.uistate, get_page_func = lambda: '')
		Bar.max_bookmarks = 15 # set maximum number of bookmarks

		# Check changing a bookmark.
		for i, path in enumerate(self.PATHS):
			Bar._add_new(path, add_bookmarks_to_beginning = False)

		self.assertTrue('Test' not in Bar.paths)
		self.assertTrue('Books' in Bar.paths)
		Bar.change_bookmark('Books', 'Books')
		self.assertEqual(Bar.paths, list(self.PATHS))
		Bar.change_bookmark('Books', 'Test')
		self.assertTrue('Test' in Bar.paths)
		self.assertTrue('Books' not in Bar.paths)
		_result = [a if a != 'Books' else 'Test' for a in self.PATHS]
		self.assertEqual(Bar.paths, _result)

		Bar.change_bookmark('Test', 'Books')
		self.assertEqual(Bar.paths, list(self.PATHS))

		# Check reordering bookmarks.
		new_paths = ('1','2','3','4','5')

		Bar.paths = list(new_paths)
		Bar.move_bookmark(new_paths[2], new_paths[2], 'left')
		self.assertEqual(Bar.paths, list(new_paths))
		Bar.move_bookmark(new_paths[3], new_paths[3], 'right')
		self.assertEqual(Bar.paths, list(new_paths))
		Bar.move_bookmark('3', '1', 'left')
		self.assertEqual(Bar.paths, ['3','1','2','4','5'])
		Bar.move_bookmark('5', '1', 'left')
		self.assertEqual(Bar.paths, ['3','5','1','2','4'])
		Bar.move_bookmark('5', '1', 'right')
		self.assertEqual(Bar.paths, ['3','1','5','2','4'])
		Bar.move_bookmark('3', '4', 'right')
		self.assertEqual(Bar.paths, ['1','5','2','4','3'])
		Bar.move_bookmark('5', '4', '-')
		self.assertEqual(Bar.paths, ['1','5','2','4','3'])

		# Check rename_bookmark and save options.
		preferences_changed = lambda save: Bar.on_preferences_changed({'save': save,
				'add_bookmarks_to_beginning': False,
				'max_bookmarks': 15})

		new_path_names = {new_paths[0]:'11', new_paths[1]:'22', new_paths[2]:'33'}
		Bar.paths = list(new_paths)
		preferences_changed(True)
		Bar._reload_bar()

		def rename_check(label, path, paths_names, path_names_uistate):
			self.assertEqual(button.get_label(), label)
			self.assertEqual(button.zim_path, path)
			self.assertEqual(Bar.paths_names, paths_names)
			self.assertEqual(self.uistate['bookmarks_names'], path_names_uistate)

		button = gtk.Button(label = new_paths[0], use_underline = False)
		button.zim_path = new_paths[0]
		rename_check(new_paths[0], new_paths[0], {}, {})

		Clipboard.set_text('new name')
		Bar.rename_bookmark(button)
		rename_check('new name', new_paths[0], {new_paths[0]:'new name'}, {new_paths[0]:'new name'})
		preferences_changed(False)
		rename_check('new name', new_paths[0], {new_paths[0]:'new name'}, {})
		preferences_changed(True)
		rename_check('new name', new_paths[0], {new_paths[0]:'new name'}, {new_paths[0]:'new name'})
		Bar.rename_bookmark(button)
		rename_check(new_paths[0], new_paths[0], {}, {})

		# Check delete with renaming.
		preferences_changed(True)
		paths_names_copy = dict(new_path_names)
		Bar.paths_names = dict(new_path_names)
		for key in new_path_names:
			Bar.delete(key)
			del paths_names_copy[key]
			self.assertEqual(Bar.paths_names, paths_names_copy)
			self.assertEqual(self.uistate['bookmarks_names'], Bar.paths_names)

		# Check delete all with renaming.
		Bar.paths_names = dict(new_path_names)
		Bar.delete_all()
		self.assertEqual(Bar.paths_names, {})
		self.assertEqual(self.uistate['bookmarks_names'], {})

		# Check change bookmark with renaming.
		new_path_names = {new_paths[0]:'11', new_paths[1]:'22', new_paths[2]:'33'}

		Bar.paths = list(new_paths)
		Bar.paths_names = dict(new_path_names)
		paths_names_copy = dict(new_path_names)
		_name = paths_names_copy.pop(new_paths[0])
		paths_names_copy['new path'] = _name
		Bar.change_bookmark(new_paths[0], 'new path')
		self.assertEqual(Bar.paths_names, paths_names_copy)
		self.assertEqual(Bar.paths, ['new path'] + list(new_paths[1:]))


	def testPreferences(self):
		'''Check preferences: full/short page names, save option, 
		max number of bookmarks.'''

		# Check short page names.
		Bar = BookmarkBar(self.ui, self.uistate, get_page_func = lambda: '')
		self.uistate['show_full_page_name'] = False
		for path in self.PATHS:
			Bar._add_new(path)
		self.assertEqual(Bar.paths, list(self.PATHS))
		for i, button in enumerate(Bar.container.get_children()[2:]):
			self.assertEqual(self.PATHS[i], button.zim_path)
			self.assertEqual(Path(self.PATHS[i]).basename, button.get_label())

		# Show full page names.
		Bar.toggle_show_full_page_name()
		self.assertEqual(Bar.paths, list(self.PATHS))
		for i, button in enumerate(Bar.container.get_children()[2:]):
			self.assertEqual(self.PATHS[i], button.zim_path)
			self.assertEqual(self.PATHS[i], button.get_label())

		# Check save option.
		self.uistate['bookmarks'] = list(self.PATHS)
		self.uistate['bookmarks_names'] = dict(self.PATHS_NAMES)
		Bar = BookmarkBar(self.ui, self.uistate, get_page_func = lambda: '')
		self.assertEqual(Bar.paths, list(self.PATHS))
		self.assertEqual(Bar.paths_names, self.PATHS_NAMES)

		self.uistate['bookmarks'] = []
		self.uistate['bookmarks_names'] = {}
		Bar = BookmarkBar(self.ui, self.uistate, get_page_func = lambda: '')
		self.assertEqual(Bar.paths, [])
		self.assertEqual(Bar.paths_names, {})

		# Get pages to check max number of bookmarks.
		pagelist = set(self.index.list_pages(None))
		_enhanced_pagelist = set()
		for page in pagelist:
			_enhanced_pagelist.update( set(self.index.list_pages(page)) )
			if len(_enhanced_pagelist) > 20:
				break
		pagelist.update(_enhanced_pagelist)
		pagelist = [a.name for a in pagelist if a.exists()]
		self.assertTrue(len(pagelist) > 20)

		def preferences_changed(save, max_b):
			Bar.on_preferences_changed({
				'save': save,
				'add_bookmarks_to_beginning': False,
				'max_bookmarks': max_b})

		# Check that more than max bookmarks can be loaded at start.
		self.uistate['bookmarks'] = pagelist
		Bar = BookmarkBar(self.ui, self.uistate, get_page_func = lambda: '')
		self.assertEqual(pagelist, Bar.paths)
		preferences_changed(True, 5)
		self.assertEqual(pagelist, Bar.paths)
		self.assertEqual(pagelist, self.uistate['bookmarks'])

		# Set maximum number of bookmarks.
		self.uistate['bookmarks'] = []
		Bar = BookmarkBar(self.ui, self.uistate, get_page_func = lambda: '')
		for max_bookmarks in (5, 10, 15, 20):
			preferences_changed(False, max_bookmarks)
			for page in pagelist:
				Bar._add_new(page)
			self.assertEqual(len(Bar.paths), max_bookmarks)
			self.assertEqual(Bar.paths, pagelist[:max_bookmarks])
			Bar.delete_all()

		# Check 'save' option in preferences.
		for i, path in enumerate(self.PATHS):
			preferences_changed(False, 15)
			Bar._add_new(path)
			self.assertEqual(self.uistate['bookmarks'], [])
			preferences_changed(True, 15)
			self.assertEqual(self.uistate['bookmarks'], list(self.PATHS[:i+1]))
		self.assertEqual(self.uistate['bookmarks'], list(self.PATHS))
Ejemplo n.º 14
0
class DumperClass(Visitor):
	'''Base class for dumper classes. Dumper classes serialize the content
	of a parse tree back to a text representation of the page content.
	Therefore this class implements the visitor API, so it can be
	used with any parse tree implementation or parser object that supports
	this API.

	To implement a dumper class, you need to define handlers for all
	tags that can appear in a page. Tags that are represented by a simple
	prefix and postfix string can be defined in the dictionary C{TAGS}.
	For example to define the italic tag in html output the dictionary
	should contain a definition like: C{EMPHASIS: ('<i>', '</i>')}.

	For tags that require more complex logic you can define a method to
	format the tag. Typical usage is to format link attributes in such
	a method. The method name should be C{dump_} + the name of the tag,
	e.g. C{dump_link()} for links (see the constants with tag names for
	the other tags). Such a sump method will get 3 arguments: the tag
	name itself, a dictionary with the tag attributes and a list of
	strings that form the tag content. The method should return a list
	of strings that represents the formatted text.

	This base class takes care of a stack of nested formatting tags and
	when a tag is closed either picks the appropriate prefix and postfix
	from C{TAGS} or calls the corresponding C{dump_} method. As a result
	tags are serialized depth-first.

	@ivar linker: the (optional) L{Linker} object, used to resolve links
	@ivar template_options: a L{ConfigDict} with options that may be set
	in a template (so inherently not safe !) to control the output style.
	Formats using this need to define the supported keys in the dict
	C{TEMPLATE_OPTIONS}.
	@ivar context: the stack of open tags maintained by this class. Can
	be used in C{dump_} methods to inspect the parent scope of the
	format. Elements on this stack have "tag", "attrib" and "text"
	attributes. Keep in mind that the parent scope is not yet complete
	when a tag is serialized.
	'''

	TAGS = {} #: dict mapping formatting tags to 2-tuples of a prefix and a postfix string

	TEMPLATE_OPTIONS = {} #: dict mapping ConfigDefinitions for template options

	def __init__(self, linker=None, template_options=None):
		self.linker = linker
		self.template_options = ConfigDict(template_options)
		self.template_options.define(self.TEMPLATE_OPTIONS)
		self.context = []
		self._text = []

	def dump(self, tree):
		'''Format a parsetree to text
		@param tree: a parse tree object that supports a C{visit()} method
		@returns: a list of lines
		'''
		# FIXME - issue here is that we need to reset state - should be in __init__
		self._text = []
		self.context = [DumperContextElement(None, None, self._text)]
		tree.visit(self)
		if len(self.context) != 1:
			raise AssertionError('Unclosed tags on tree: %s' % self.context[-1].tag)
		#~ import pprint; pprint.pprint(self._text)
		return self.get_lines() # FIXME - maybe just return text ?

	def get_lines(self):
		'''Return the dumped content as a list of lines
		Should only be called after closing the top level element
		'''
		return ''.join(self._text).splitlines(1)

	def start(self, tag, attrib=None):
		if attrib:
			attrib = attrib.copy() # Ensure dumping does not change tree
		self.context.append(DumperContextElement(tag, attrib, []))

	def text(self, text):
		assert not text is None
		if self.context[-1].tag != OBJECT:
			text = self.encode_text(self.context[-1].tag, text)
		self.context[-1].text.append(text)

	def end(self, tag):
		if not tag or tag != self.context[-1].tag:
			raise AssertionError('Unexpected tag closed: %s' % tag)
		_, attrib, strings = self.context.pop()

		if tag in self.TAGS:
			assert strings, 'Can not append empty %s element' % tag
			start, end = self.TAGS[tag]
			strings.insert(0, start)
			strings.append(end)
		elif tag == FORMATTEDTEXT:
			pass
		else:
			try:
				method = getattr(self, 'dump_' + tag)
			except AttributeError:
				raise AssertionError('BUG: Unknown tag: %s' % tag)

			strings = method(tag, attrib, strings)
			#~ try:
				#~ u''.join(strings)
			#~ except:
				#~ print("BUG: %s returned %s" % ('dump_'+tag, strings))

		if strings is not None:
			self.context[-1].text.extend(strings)

	def append(self, tag, attrib=None, text=None):
		strings = None
		if tag in self.TAGS:
			assert text is not None, 'Can not append empty %s element' % tag
			start, end = self.TAGS[tag]
			text = self.encode_text(tag, text)
			strings = [start, text, end]
		elif tag == FORMATTEDTEXT:
			if text is not None:
				strings = [self.encode_text(tag, text)]
		else:
			if attrib:
				attrib = attrib.copy() # Ensure dumping does not change tree

			try:
				method = getattr(self, 'dump_' + tag)
			except AttributeError:
				raise AssertionError('BUG: Unknown tag: %s' % tag)

			if text is None:
				strings = method(tag, attrib, [])
			elif tag == OBJECT:
				strings = method(tag, attrib, [text])
			else:
				strings = method(tag, attrib, [self.encode_text(tag, text)])

		if strings is not None:
			self.context[-1].text.extend(strings)

	def encode_text(self, tag, text):
		'''Optional method to encode text elements in the output

		@note: Do not apply text encoding in the C{dump_} methods, the
		list of strings given there may contain prefix and postfix
		formatting of nested tags.

		@param tag: formatting tag
		@param text: text to be encoded
		@returns: encoded text
		@implementation: optional, default just returns unmodified input
		'''
		return text

	def prefix_lines(self, prefix, strings):
		'''Convenience method to wrap a number of lines with e.g. an
		indenting sequence.
		@param prefix: a string to prefix each line
		@param strings: a list of pieces of text
		@returns: a new list of lines, each starting with prefix
		'''
		lines = ''.join(strings).splitlines(1)
		return [prefix + l for l in lines]

	def dump_object(self, tag, attrib, strings=[]):
		'''Dumps objects defined by L{InsertedObjectType}'''
		format = str(self.__class__.__module__).split('.')[-1]
		try:
			obj = PluginManager.insertedobjects[attrib['type']]
		except KeyError:
			pass
		else:
			try:
				output = obj.format(format, self, attrib, ''.join(strings))
			except ValueError:
				pass
			else:
				assert isinstance(output, (list, tuple)), "Invalid output: %r" % output
				return output

		if attrib['type'].startswith('image+'):
			# Fallback for backward compatibility of image generators < zim 0.70
			attrib = attrib.copy()
			attrib['type'] = attrib['type'][6:]
			return self.dump_img(IMAGE, attrib, None)
		else:
			return self.dump_object_fallback(tag, attrib, strings)

	def dump_object_fallback(self, tag, attrib, strings=None):
		'''Method to serialize objects that do not have their own
		handler for this format.
		@implementation: must be implemented in sub-classes
		'''
		raise NotImplementedError

	def isrtl(self, text):
		'''Check for Right To Left script
		@param text: the text to check
		@returns: C{True} if C{text} starts with characters in a
		RTL script, or C{None} if direction is not determined.
		'''
		if Pango is None:
			return None

		# It seems the find_base_dir() function is not documented in the
		# python language bindings. The Gtk C code shows the signature:
		#
		#     Pango.find_base_dir(text, length)
		#
		# It either returns a direction, or NEUTRAL if e.g. text only
		# contains punctuation but no real characters.

		dir = Pango.find_base_dir(text, len(text))
		if dir == Pango.Direction.NEUTRAL:
			return None
		else:
			return dir == Pango.Direction.RTL
Ejemplo n.º 15
0
	def __init__(self, linker=None, template_options=None):
		self.linker = linker
		self.template_options = ConfigDict(template_options)
		self.template_options.define(self.TEMPLATE_OPTIONS)
		self.context = []
		self._text = []
Ejemplo n.º 16
0
class DumperClass(Visitor):
	'''Base class for dumper classes. Dumper classes serialize the content
	of a parse tree back to a text representation of the page content.
	Therefore this class implements the visitor API, so it can be
	used with any parse tree implementation or parser object that supports
	this API.

	To implement a dumper class, you need to define handlers for all
	tags that can appear in a page. Tags that are represented by a simple
	prefix and postfix string can be defined in the dictionary C{TAGS}.
	For example to define the italic tag in html output the dictionary
	should contain a definition like: C{EMPHASIS: ('<i>', '</i>')}.

	For tags that require more complex logic you can define a method to
	format the tag. Typical usage is to format link attributes in such
	a method. The method name should be C{dump_} + the name of the tag,
	e.g. C{dump_link()} for links (see the constants with tag names for
	the other tags). Such a sump method will get 3 arguments: the tag
	name itself, a dictionary with the tag attributes and a list of
	strings that form the tag content. The method should return a list
	of strings that represents the formatted text.

	This base class takes care of a stack of nested formatting tags and
	when a tag is closed either picks the appropriate prefix and postfix
	from C{TAGS} or calls the corresponding C{dump_} method. As a result
	tags are serialized depth-first.

	@ivar linker: the (optional) L{Linker} object, used to resolve links
	@ivar template_options: a L{ConfigDict} with options that may be set
	in a template (so inherently not safe !) to control the output style.
	Formats using this need to define the supported keys in the dict
	C{TEMPLATE_OPTIONS}.
	@ivar context: the stack of open tags maintained by this class. Can
	be used in C{dump_} methods to inspect the parent scope of the
	format. Elements on this stack have "tag", "attrib" and "text"
	attributes. Keep in mind that the parent scope is not yet complete
	when a tag is serialized.
	'''

	TAGS = {} #: dict mapping formatting tags to 2-tuples of a prefix and a postfix string

	TEMPLATE_OPTIONS = {} #: dict mapping ConfigDefinitions for template options

	def __init__(self, linker=None, template_options=None):
		self.linker = linker
		self.template_options = ConfigDict(template_options)
		self.template_options.define(self.TEMPLATE_OPTIONS)
		self.context = []
		self._text = []

	def dump(self, tree):
		'''Convenience methods to dump a given tree.
		@param tree: a parse tree object that supports a C{visit()} method
		'''
		# FIXME - issue here is that we need to reset state - should be in __init__
		self._text = []
		self.context = [DumperContextElement(None, None, self._text)]
		tree.visit(self)
		if len(self.context) != 1:
			raise AssertionError, 'Unclosed tags on tree: %s' % self.context[-1].tag
		#~ import pprint; pprint.pprint(self._text)
		return self.get_lines() # FIXME - maybe just return text ?

	def get_lines(self):
		'''Return the dumped content as a list of lines
		Should only be called after closing the top level element
		'''
		return u''.join(self._text).splitlines(1)

	def start(self, tag, attrib=None):
		if attrib:
			attrib = attrib.copy() # Ensure dumping does not change tree
		self.context.append(DumperContextElement(tag, attrib, []))

	def text(self, text):
		assert not text is None
		if self.context[-1].tag != OBJECT:
			text = self.encode_text(self.context[-1].tag, text)
		self.context[-1].text.append(text)

	def end(self, tag):
		if not tag or tag != self.context[-1].tag:
			raise AssertionError, 'Unexpected tag closed: %s' % tag
		_, attrib, strings = self.context.pop()

		if tag in self.TAGS:
			assert strings, 'Can not append empty %s element' % tag
			start, end = self.TAGS[tag]
			strings.insert(0, start)
			strings.append(end)
		elif tag == FORMATTEDTEXT:
			pass
		else:
			try:
				method = getattr(self, 'dump_'+tag)
			except AttributeError:
				raise AssertionError, 'BUG: Unknown tag: %s' % tag

			strings = method(tag, attrib, strings)
			#~ try:
				#~ u''.join(strings)
			#~ except:
				#~ print "BUG: %s returned %s" % ('dump_'+tag, strings)

		if strings is not None:
			self.context[-1].text.extend(strings)

	def append(self, tag, attrib=None, text=None):
		strings = None
		if tag in self.TAGS:
			assert text is not None, 'Can not append empty %s element' % tag
			start, end = self.TAGS[tag]
			text = self.encode_text(tag, text)
			strings = [start, text, end]
		elif tag == FORMATTEDTEXT:
			if text is not None:
				strings = [self.encode_text(tag, text)]
		else:
			if attrib:
				attrib = attrib.copy() # Ensure dumping does not change tree

			try:
				method = getattr(self, 'dump_'+tag)
			except AttributeError:
				raise AssertionError, 'BUG: Unknown tag: %s' % tag

			if text is None:
				strings = method(tag, attrib, [])
			elif tag == OBJECT:
				strings = method(tag, attrib, [text])
			else:
				strings = method(tag, attrib, [self.encode_text(tag, text)])

		if strings is not None:
			self.context[-1].text.extend(strings)

	def encode_text(self, tag, text):
		'''Optional method to encode text elements in the output

		@note: Do not apply text encoding in the C{dump_} methods, the
		list of strings given there may contain prefix and postfix
		formatting of nested tags.

		@param tag: formatting tag
		@param text: text to be encoded
		@returns: encoded text
		@implementation: optional, default just returns unmodified input
		'''
		return text

	def prefix_lines(self, prefix, strings):
		'''Convenience method to wrap a number of lines with e.g. an
		indenting sequence.
		@param prefix: a string to prefix each line
		@param strings: a list of pieces of text
		@returns: a new list of lines, each starting with prefix
		'''
		lines = u''.join(strings).splitlines(1)
		return [prefix + l for l in lines]

	def dump_object(self, tag, attrib, strings=None):
		'''Dumps object using proper ObjectManager'''
		format = str(self.__class__.__module__).split('.')[-1]
		if 'type' in attrib:
			obj = ObjectManager.get_object(attrib['type'], attrib, u''.join(strings))
			output = obj.dump(format, self, self.linker)
			if isinstance(output, basestring):
				return [output]
			elif output is not None:
				return output

		return self.dump_object_fallback(tag, attrib, strings)

		# TODO put content in attrib, use text for caption (with full recursion)
		# See img

	def dump_object_fallback(self, tag, attrib, strings=None):
		'''Method to serialize objects that do not have their own
		handler for this format.
		@implementation: must be implemented in sub-classes
		'''
		raise NotImplementedError

	def isrtl(self, text):
		'''Check for Right To Left script
		@param text: the text to check
		@returns: C{True} if C{text} starts with characters in a
		RTL script, or C{None} if direction is not determined.
		'''
		if pango is None:
			return None

		# It seems the find_base_dir() function is not documented in the
		# python language bindings. The Gtk C code shows the signature:
		#
		#     pango.find_base_dir(text, length)
		#
		# It either returns a direction, or NEUTRAL if e.g. text only
		# contains punctuation but no real characters.

		dir = pango.find_base_dir(text, len(text))
		if dir == pango.DIRECTION_NEUTRAL:
			return None
		else:
			return dir == pango.DIRECTION_RTL
Ejemplo n.º 17
0
	def runTest(self):
		'''There is one long test.'''

		ui = MockUI()
		ui.notebook = self.notebook
		ui.page = Path('Test:foo')
		uistate = ConfigDict()
		self.assertTrue(self.notebook.get_page(ui.page).exists())

		PATHS = ('Parent:Daughter:Granddaughter',
				 'Test:tags', 'Test:foo', 'Books')
		LEN_PATHS = len(PATHS)
		PATHS_NAMES = {PATHS[0]:'name 1', PATHS[1]:'name 2', PATHS[2]:'name 3'}

		# Check correctness of reading uistate.
		uistate.setdefault('bookmarks', [])
		uistate.setdefault('bookmarks_names', {})

		uistate['bookmarks'] = list(PATHS)
		uistate['bookmarks_names'] = dict(PATHS_NAMES)
		Bar = BookmarkBar(ui, uistate, get_page_func = lambda: '')
		self.assertTrue(Bar.paths == list(PATHS))
		self.assertTrue(Bar.paths_names == PATHS_NAMES)

		uistate['bookmarks'] = []
		uistate['bookmarks_names'] = {}
		Bar = BookmarkBar(ui, uistate, get_page_func = lambda: '')
		self.assertTrue(Bar.paths == [])
		self.assertTrue(Bar.paths_names == {})

		# Add paths to the beginning of the bar.
		for i, path in enumerate(PATHS):
			Bar._add_new(path, add_bookmarks_to_beginning = True)
			self.assertTrue(len(Bar.paths) == i + 1)
		self.assertTrue(Bar.paths == list(reversed(PATHS)))

		# Add paths to the end of the bar.
		Bar.paths = []
		for i, path in enumerate(PATHS):
			Bar._add_new(path, add_bookmarks_to_beginning = False)
			self.assertTrue(len(Bar.paths) == i + 1)
		self.assertTrue(Bar.paths == list(PATHS))

		# Check that the same path can't be added to the bar.
		Bar._add_new(PATHS[0])
		Bar._add_new(PATHS[1])
		self.assertTrue(Bar.paths == list(PATHS))

		# Delete paths from the bar.
		for i, button in enumerate(Bar.container.get_children()[2:]):
			path = button.zim_path
			self.assertTrue(path in Bar.paths)
			Bar.delete(button.zim_path)
			self.assertTrue(len(Bar.paths) == LEN_PATHS - i - 1)
			self.assertTrue(path not in Bar.paths)
		self.assertTrue(Bar.paths == [])

		# Check short page names.
		uistate['show_full_page_name'] = False
		for path in PATHS:
			Bar._add_new(path)
		self.assertTrue(Bar.paths == list(PATHS))
		for i, button in enumerate(Bar.container.get_children()[2:]):
			self.assertTrue(PATHS[i] == button.zim_path)
			self.assertTrue(Path(PATHS[i]).basename == button.get_label())
		uistate['show_full_page_name'] = True

		# Delete all bookmarks from the bar.
		Bar.delete_all()
		self.assertTrue(Bar.paths == [])

		# Check restriction of max bookmarks in the bar.
		pagelist = set(self.index.list_pages(None))
		_enhanced_pagelist = set()
		for page in pagelist:
			_enhanced_pagelist.update( set(self.index.list_pages(page)) )
			if len(_enhanced_pagelist) > MAX_BOOKMARKS:
				break
		pagelist.update(_enhanced_pagelist)
		self.assertTrue(len(pagelist) > MAX_BOOKMARKS)
		pagelist = list(pagelist)
		for page in pagelist:
			Bar._add_new(page.name)
		self.assertTrue(len(Bar.paths) == MAX_BOOKMARKS)
		self.assertTrue(Bar.paths == [a.name for a in pagelist[:MAX_BOOKMARKS]])
		Bar.delete_all()

		# Check 'save' option in preferences.
		for i, path in enumerate(PATHS):
			Bar.on_preferences_changed({'save':False, 'add_bookmarks_to_beginning':False})
			Bar._add_new(path)
			self.assertTrue(uistate['bookmarks'] == [])
			Bar.on_preferences_changed({'save':True, 'add_bookmarks_to_beginning':False})
			self.assertTrue(uistate['bookmarks'] == list(PATHS[:i+1]))
		self.assertTrue(uistate['bookmarks'] == list(PATHS))

		# Check changing a bookmark.
		self.assertTrue('Test' not in Bar.paths)
		self.assertTrue('Books' in Bar.paths)
		Bar.change_bookmark('Books', 'Books')
		self.assertTrue(Bar.paths == list(PATHS))
		_b_paths = [a for a in Bar.paths if a != 'Books']
		Bar.change_bookmark('Books', 'Test')
		self.assertTrue('Test' in Bar.paths)
		self.assertTrue('Books' not in Bar.paths)
		_e_paths = [a for a in Bar.paths if a != 'Test']
		self.assertTrue(_b_paths == _e_paths)

		Bar.change_bookmark('Test', 'Books')
		self.assertTrue(Bar.paths == list(PATHS))

		# Check deleting a bookmark after deleting a page in the notebook.
		self.assertTrue(len(Bar.paths) == LEN_PATHS)
		for i, path in enumerate(PATHS):
			self.assertTrue(path in Bar.paths)
			self.notebook.delete_page(Path(path))
			self.assertTrue(path not in Bar.paths)
			self.assertTrue(len(Bar.paths) == LEN_PATHS - i - 1)
		self.assertTrue(Bar.paths == [])

		# Check reordering bookmarks.
		PATHS_2 = ('1','2','3','4','5')
		PATHS_NAMES_2 = {PATHS_2[0]:'11', PATHS_2[1]:'22', PATHS_2[2]:'33'}

		Bar.paths = list(PATHS_2)
		Bar.move_bookmark(PATHS_2[2], PATHS_2[2], 'left')
		self.assertTrue(Bar.paths == list(PATHS_2))
		Bar.move_bookmark(PATHS_2[3], PATHS_2[3], 'right')
		self.assertTrue(Bar.paths == list(PATHS_2))
		Bar.move_bookmark('3', '1', 'left')
		self.assertTrue(Bar.paths == ['3','1','2','4','5'])
		Bar.move_bookmark('5', '1', 'left')
		self.assertTrue(Bar.paths == ['3','5','1','2','4'])
		Bar.move_bookmark('5', '1', 'right')
		self.assertTrue(Bar.paths == ['3','1','5','2','4'])
		Bar.move_bookmark('3', '4', 'right')
		self.assertTrue(Bar.paths == ['1','5','2','4','3'])
		Bar.move_bookmark('5', '4', '-')
		self.assertTrue(Bar.paths == ['1','5','2','4','3'])

		# CHECK RENAMING
		# Check rename_bookmark and save options.
		Bar.paths = list(PATHS_2)
		button = gtk.Button(label = PATHS_2[0], use_underline = False)
		button.zim_path = PATHS_2[0]
		Bar.on_preferences_changed({'save':True, 'add_bookmarks_to_beginning':False})
		Bar._reload_bar()

		def rename_check(label, path, paths_names, path_names_uistate):
			self.assertTrue(button.get_label() == label)
			self.assertTrue(button.zim_path == path)
			self.assertTrue(Bar.paths_names == paths_names)
			self.assertTrue(uistate['bookmarks_names'] == path_names_uistate)

		rename_check(PATHS_2[0], PATHS_2[0], {}, {})
		Clipboard.set_text('new name')
		Bar.rename_bookmark(button)
		rename_check('new name', PATHS_2[0], {PATHS_2[0]:'new name'}, {PATHS_2[0]:'new name'})
		Bar.on_preferences_changed({'save':False, 'add_bookmarks_to_beginning':False})
		rename_check('new name', PATHS_2[0], {PATHS_2[0]:'new name'}, {})
		Bar.on_preferences_changed({'save':True, 'add_bookmarks_to_beginning':False})
		rename_check('new name', PATHS_2[0], {PATHS_2[0]:'new name'}, {PATHS_2[0]:'new name'})
		Bar.rename_bookmark(button)
		rename_check(PATHS_2[0], PATHS_2[0], {}, {})

		# Check delete with renaming.
		Bar.on_preferences_changed({'save':True, 'add_bookmarks_to_beginning':False})
		paths_names_copy = dict(PATHS_NAMES_2)
		Bar.paths_names = dict(PATHS_NAMES_2)
		for key in PATHS_NAMES_2:
			Bar.delete(key)
			del paths_names_copy[key]
			self.assertTrue(Bar.paths_names == paths_names_copy)
			self.assertTrue(uistate['bookmarks_names'] == Bar.paths_names)

		# Check delete all with renaming.
		Bar.paths_names = dict(PATHS_NAMES_2)
		Bar.delete_all()
		self.assertTrue(Bar.paths_names == {})
		self.assertTrue(uistate['bookmarks_names'] == {})

		# Check change bookmark with renaming.
		Bar.paths = list(PATHS_2)
		Bar.paths_names = dict(PATHS_NAMES_2)
		paths_names_copy = dict(PATHS_NAMES_2)
		paths_names_copy.pop(PATHS_2[0], None)
		Bar.change_bookmark(PATHS_2[0], 'new path')
		self.assertTrue(Bar.paths_names == paths_names_copy)
		self.assertTrue(Bar.paths == ['new path'] + list(PATHS_2[1:]))

		# Check that paths and paths_names didn't change in the process.
		self.assertTrue(PATHS_2 == ('1','2','3','4','5'))
		self.assertTrue(PATHS_NAMES_2 == {PATHS_2[0]:'11', PATHS_2[1]:'22', PATHS_2[2]:'33'})
Ejemplo n.º 18
0
class TestBookmarksBar(tests.TestCase):

	def setUp(self):
		self.PATHS = ('Parent:Daughter:Granddaughter', 'Test:tags', 'Test:foo', 'Books')
		self.LEN_PATHS = len(self.PATHS)
		self.PATHS_NAMES = {self.PATHS[0]: 'name 1', self.PATHS[1]: 'name 2', self.PATHS[2]: 'name 3'}

		self.notebook = self.setUpNotebook(content=self.PATHS)

		self.uistate = ConfigDict()
		self.uistate.setdefault('bookmarks', [])
		self.uistate.setdefault('bookmarks_names', {})
		self.uistate.setdefault('show_full_page_name', True)


	def testGeneral(self):
		'''Test general functions: add, delete bookmarks.'''
		navigation = tests.MockObject()
		bar = BookmarkBar(self.notebook, navigation, self.uistate, get_page_func = lambda: '')
		bar.max_bookmarks = 15 # set maximum number of bookmarks

		# Add paths to the beginning of the bar.
		for i, path in enumerate(self.PATHS):
			bar._add_new(path, add_bookmarks_to_beginning = True)
			self.assertEqual(len(bar.paths), i + 1)
		self.assertTrue(bar.paths == list(reversed(self.PATHS)))

		# Add paths to the end of the bar.
		bar.paths = []
		for i, path in enumerate(self.PATHS):
			bar._add_new(path, add_bookmarks_to_beginning = False)
			self.assertEqual(len(bar.paths), i + 1)
		self.assertEqual(bar.paths, list(self.PATHS))

		# Check that the same path can't be added to the bar.
		bar._add_new(self.PATHS[0])
		bar._add_new(self.PATHS[1])
		self.assertEqual(bar.paths, list(self.PATHS))

		# Delete paths from the bar.
		for i, button in enumerate(bar.scrolledbox.get_scrolled_children()):
			path = button.zim_path
			self.assertTrue(path in bar.paths)
			bar.delete(button.zim_path)
			self.assertEqual(len(bar.paths), self.LEN_PATHS - i - 1)
			self.assertTrue(path not in bar.paths)
		self.assertEqual(bar.paths, [])

		# Delete all bookmarks from the bar.
		bar.delete_all()
		self.assertEqual(bar.paths, [])


	def testDeletePages(self):
		'''Check deleting a bookmark after deleting a page in the notebook.'''
		self.uistate['bookmarks'] = list(self.PATHS)
		navigation = tests.MockObject()
		bar = BookmarkBar(self.notebook, navigation, self.uistate, get_page_func = lambda: '')
		for i, path in enumerate(self.PATHS):
			self.assertTrue(path in bar.paths)
			self.notebook.delete_page(Path(path))
			self.assertTrue(path not in bar.paths)
			self.assertEqual(len(bar.paths), self.LEN_PATHS - i - 1)
		self.assertEqual(bar.paths, [])


	def testFunctions(self):
		'''Test bookmark functions: changing, reordering, ranaming.'''
		navigation = tests.MockObject()
		bar = BookmarkBar(self.notebook, navigation, self.uistate, get_page_func = lambda: '')
		bar.max_bookmarks = 15 # set maximum number of bookmarks

		# Check changing a bookmark.
		for i, path in enumerate(self.PATHS):
			bar._add_new(path, add_bookmarks_to_beginning = False)

		self.assertTrue('Test' not in bar.paths)
		self.assertTrue('Books' in bar.paths)
		bar.change_bookmark('Books', 'Books')
		self.assertEqual(bar.paths, list(self.PATHS))
		bar.change_bookmark('Books', 'Test')
		self.assertTrue('Test' in bar.paths)
		self.assertTrue('Books' not in bar.paths)
		_result = [a if a != 'Books' else 'Test' for a in self.PATHS]
		self.assertEqual(bar.paths, _result)

		bar.change_bookmark('Test', 'Books')
		self.assertEqual(bar.paths, list(self.PATHS))

		# Check reordering bookmarks.
		new_paths = ('1', '2', '3', '4', '5')

		bar.paths = list(new_paths)
		bar.move_bookmark(new_paths[2], new_paths[2], 'left')
		self.assertEqual(bar.paths, list(new_paths))
		bar.move_bookmark(new_paths[3], new_paths[3], 'right')
		self.assertEqual(bar.paths, list(new_paths))
		bar.move_bookmark('3', '1', 'left')
		self.assertEqual(bar.paths, ['3', '1', '2', '4', '5'])
		bar.move_bookmark('5', '1', 'left')
		self.assertEqual(bar.paths, ['3', '5', '1', '2', '4'])
		bar.move_bookmark('5', '1', 'right')
		self.assertEqual(bar.paths, ['3', '1', '5', '2', '4'])
		bar.move_bookmark('3', '4', 'right')
		self.assertEqual(bar.paths, ['1', '5', '2', '4', '3'])
		bar.move_bookmark('5', '4', '-')
		self.assertEqual(bar.paths, ['1', '5', '2', '4', '3'])

		# Check rename_bookmark and save options.
		preferences_changed = lambda save: bar.on_preferences_changed({'save': save,
				'add_bookmarks_to_beginning': False,
				'max_bookmarks': 15})

		new_path_names = {new_paths[0]: '11', new_paths[1]: '22', new_paths[2]: '33'}
		bar.paths = list(new_paths)
		preferences_changed(True)
		bar._reload_bar()

		def rename_check(label, path, paths_names, path_names_uistate):
			self.assertEqual(button.get_label(), label)
			self.assertEqual(button.zim_path, path)
			self.assertEqual(bar.paths_names, paths_names)
			self.assertEqual(self.uistate['bookmarks_names'], path_names_uistate)

		button = Gtk.Button(label = new_paths[0], use_underline = False)
		button.zim_path = new_paths[0]
		rename_check(new_paths[0], new_paths[0], {}, {})

		Clipboard.set_text('new name')
		bar.rename_bookmark(button)
		rename_check('new name', new_paths[0], {new_paths[0]: 'new name'}, {new_paths[0]: 'new name'})
		preferences_changed(False)
		rename_check('new name', new_paths[0], {new_paths[0]: 'new name'}, {})
		preferences_changed(True)
		rename_check('new name', new_paths[0], {new_paths[0]: 'new name'}, {new_paths[0]: 'new name'})
		bar.rename_bookmark(button)
		rename_check(new_paths[0], new_paths[0], {}, {})

		# Check delete with renaming.
		preferences_changed(True)
		paths_names_copy = dict(new_path_names)
		bar.paths_names = dict(new_path_names)
		for key in new_path_names:
			bar.delete(key)
			del paths_names_copy[key]
			self.assertEqual(bar.paths_names, paths_names_copy)
			self.assertEqual(self.uistate['bookmarks_names'], bar.paths_names)

		# Check delete all with renaming.
		bar.paths_names = dict(new_path_names)
		bar.delete_all()
		self.assertEqual(bar.paths_names, {})
		self.assertEqual(self.uistate['bookmarks_names'], {})

		# Check change bookmark with renaming.
		new_path_names = {new_paths[0]: '11', new_paths[1]: '22', new_paths[2]: '33'}

		bar.paths = list(new_paths)
		bar.paths_names = dict(new_path_names)
		paths_names_copy = dict(new_path_names)
		_name = paths_names_copy.pop(new_paths[0])
		paths_names_copy['new path'] = _name
		bar.change_bookmark(new_paths[0], 'new path')
		self.assertEqual(bar.paths_names, paths_names_copy)
		self.assertEqual(bar.paths, ['new path'] + list(new_paths[1:]))


	def testPreferences(self):
		'''Check preferences: full/short page names, save option,
		max number of bookmarks.'''

		# Check short page names.
		navigation = tests.MockObject()
		bar = BookmarkBar(self.notebook, navigation, self.uistate, get_page_func = lambda: '')
		self.uistate['show_full_page_name'] = False
		for path in self.PATHS:
			bar._add_new(path)
		self.assertEqual(bar.paths, list(self.PATHS))
		for i, button in enumerate(bar.scrolledbox.get_scrolled_children()):
			self.assertEqual(self.PATHS[i], button.zim_path)
			self.assertEqual(Path(self.PATHS[i]).basename, button.get_label())

		# Show full page names.
		bar.toggle_show_full_page_name()
		self.assertEqual(bar.paths, list(self.PATHS))
		for i, button in enumerate(bar.scrolledbox.get_scrolled_children()):
			self.assertEqual(self.PATHS[i], button.zim_path)
			self.assertEqual(self.PATHS[i], button.get_label())

		# Check save option.
		self.uistate['bookmarks'] = list(self.PATHS)
		self.uistate['bookmarks_names'] = dict(self.PATHS_NAMES)
		bar = BookmarkBar(self.notebook, navigation, self.uistate, get_page_func = lambda: '')
		self.assertEqual(bar.paths, list(self.PATHS))
		self.assertEqual(bar.paths_names, self.PATHS_NAMES)

		self.uistate['bookmarks'] = []
		self.uistate['bookmarks_names'] = {}
		bar = BookmarkBar(self.notebook, navigation, self.uistate, get_page_func = lambda: '')
		self.assertEqual(bar.paths, [])
		self.assertEqual(bar.paths_names, {})

		# Get pages to check max number of bookmarks.
		pagelist = []
		for path in [Path('Page %i' % i) for i in range(25)]:
			page = self.notebook.get_page(path)
			page.parse('wiki', 'TEst 123')
			self.notebook.store_page(page)
			pagelist.append(path.name)
		self.assertTrue(len(pagelist) > 20)

		def preferences_changed(save, max_b):
			bar.on_preferences_changed({
				'save': save,
				'add_bookmarks_to_beginning': False,
				'max_bookmarks': max_b})

		# Check that more than max bookmarks can be loaded at start.
		self.uistate['bookmarks'] = pagelist
		bar = BookmarkBar(self.notebook, navigation, self.uistate, get_page_func = lambda: '')
		self.assertEqual(pagelist, bar.paths)
		preferences_changed(True, 5)
		self.assertEqual(pagelist, bar.paths)
		self.assertEqual(pagelist, self.uistate['bookmarks'])

		# Set maximum number of bookmarks.
		self.uistate['bookmarks'] = []
		bar = BookmarkBar(self.notebook, navigation, self.uistate, get_page_func = lambda: '')
		for max_bookmarks in (5, 10, 15, 20):
			preferences_changed(False, max_bookmarks)
			for page in pagelist:
				bar._add_new(page)
			self.assertEqual(len(bar.paths), max_bookmarks)
			self.assertEqual(bar.paths, pagelist[:max_bookmarks])
			bar.delete_all()

		# Check 'save' option in preferences.
		for i, path in enumerate(self.PATHS):
			preferences_changed(False, 15)
			bar._add_new(path)
			self.assertEqual(self.uistate['bookmarks'], [])
			preferences_changed(True, 15)
			self.assertEqual(self.uistate['bookmarks'], list(self.PATHS[:i + 1]))
		self.assertEqual(self.uistate['bookmarks'], list(self.PATHS))
Ejemplo n.º 19
0
	def __init__(self, linker=None, template_options=None):
		self.linker = linker
		self.template_options = ConfigDict(template_options)
		self.template_options.define(self.TEMPLATE_OPTIONS)
		self.context = []
		self._text = []
Ejemplo n.º 20
0
class Notebook(gobject.GObject):
	'''Main class to access a notebook. Proxies between backend Store
	and Index objects on the one hand and the gui application on the other

	This class has the following signals:
		* store-page (page)
		* move-page (oldpath, newpath, update_links)
		* delete-page (path)
		* properties-changed ()

	All signals are defined with the SIGNAL_RUN_LAST type, so any
	handler connected normally will run before the actual action.
	Use "connect_after()" to install handlers after storing, moving
	or deleting a page.
	'''

	# TODO add checks for read-only page in much more methods

	# define signals we want to use - (closure type, return type and arg types)
	__gsignals__ = {
		'store-page': (gobject.SIGNAL_RUN_LAST, None, (object,)),
		'move-page': (gobject.SIGNAL_RUN_LAST, None, (object, object, bool)),
		'delete-page': (gobject.SIGNAL_RUN_LAST, None, (object,)),
		'properties-changed': (gobject.SIGNAL_RUN_FIRST, None, ()),
	}

	properties = (
		('name', 'string', _('Name')), # T: label for properties dialog
		('home', 'page', _('Home Page')), # T: label for properties dialog
		('icon', 'image', _('Icon')), # T: label for properties dialog
		('document_root', 'dir', _('Document Root')), # T: label for properties dialog
		('slow_fs', 'bool', _('Slow file system')), # T: label for properties dialog
		#~ ('autosave', 'bool', _('Auto-version when closing the notebook')),
			# T: label for properties dialog
	)

	def __init__(self, dir=None, file=None, config=None, index=None):
		assert not (dir and file), 'BUG: can not provide both dir and file '
		gobject.GObject.__init__(self)
		self._namespaces = []	# list used to resolve stores
		self._stores = {}		# dict mapping namespaces to stores
		self.namespace_properties = HierarchicDict()
		self._page_cache = weakref.WeakValueDictionary()
		self.dir = None
		self.file = None
		self.cache_dir = None
		self.name = None
		self.icon = None
		self.config = config

		if dir:
			assert isinstance(dir, Dir)
			self.dir = dir
			self.readonly = not dir.iswritable()
			self.cache_dir = dir.subdir('.zim')
			if self.readonly or not self.cache_dir.iswritable():
				self.cache_dir = self._cache_dir(dir)
			logger.debug('Cache dir: %s', self.cache_dir)
			if self.config is None:
				self.config = ConfigDictFile(dir.file('notebook.zim'))
			# TODO check if config defined root namespace
			self.add_store(Path(':'), 'files') # set root
			# TODO add other namespaces from config
		elif file:
			assert isinstance(file, File)
			self.file = file
			self.readonly = not file.iswritable()
			assert False, 'TODO: support for single file notebooks'

		if index is None:
			import zim.index # circular import
			self.index = zim.index.Index(notebook=self)
		else:
			self.index = index
			self.index.set_notebook(self)

		if self.config is None:
			self.config = ConfigDict()

		self.config['Notebook'].setdefault('name', None, klass=basestring)
		self.config['Notebook'].setdefault('home', ':Home', klass=basestring)
		self.config['Notebook'].setdefault('icon', None, klass=basestring)
		self.config['Notebook'].setdefault('document_root', None, klass=basestring)
		self.config['Notebook'].setdefault('slow_fs', False)
		self.do_properties_changed()

	@property
	def uri(self):
		'''Returns a file:// uri for this notebook that can be opened by zim'''
		assert self.dir or self.file, 'Notebook does not have a dir or file'
		if self.dir:
			return self.dir.uri
		else:
			return self.file.uri

	def _cache_dir(self, dir):
		from zim.config import XDG_CACHE_HOME
		path = 'notebook-' + dir.path.replace('/', '_').strip('_')
		return XDG_CACHE_HOME.subdir(('zim', path))

	def save_properties(self, **properties):
		# Check if icon is relative
		if 'icon' in properties and properties['icon'] \
		and self.dir and properties['icon'].startswith(self.dir.path):
			i = len(self.dir.path)
			path = './' + properties['icon'][i:].lstrip('/\\')
			# TODO use proper fs routine(s) for this substitution
			properties['icon'] = path

		# Set home page as string
		if 'home' in properties and isinstance(properties['home'], Path):
			properties['home'] = properties['home'].name

		self.config['Notebook'].update(properties)
		self.config.write()
		self.emit('properties-changed')

	def do_properties_changed(self):
		#~ import pprint
		#~ pprint.pprint(self.config)
		config = self.config['Notebook']

		# Set a name for ourselves
		if config['name']: 	self.name = config['name']
		elif self.dir: self.name = self.dir.basename
		elif self.file: self.name = self.file.basename
		else: self.name = 'Unnamed Notebook'

		# We should always have a home
		config.setdefault('home', ':Home')

		# Resolve icon, can be relative
		# TODO proper FS routine to check abs path - also allowed without the "./" - so e.g. icon.png should be resolved as well
		if self.dir and config['icon'] and config['icon'].startswith('.'):
			self.icon = self.dir.file(config['icon']).path
		elif config['icon']:
			self.icon = File(config['icon']).path
		else:
			self.icon = None

		# Set FS property
		if config['slow_fs']: print 'TODO: hook slow_fs property'

	def add_store(self, path, store, **args):
		'''Add a store to the notebook to handle a specific path and all
		it's sub-pages. Needs a Path and a store name, all other args will
		be passed to the store. Alternatively you can pass a store object
		but in that case no arguments are allowed.
		Returns the store object.
		'''
		assert not path.name in self._stores, 'Store for "%s" exists' % path
		if isinstance(store, basestring):
			mod = zim.stores.get_store(store)
			mystore = mod.Store(notebook=self, path=path, **args)
		else:
			assert not args
			mystore = store
		self._stores[path.name] = mystore
		self._namespaces.append(path.name)

		# keep order correct for lookup
		self._namespaces.sort(reverse=True)

		return mystore

	def get_store(self, path):
		'''Returns the store object to handle a page or namespace.'''
		for namespace in self._namespaces:
			# longest match first because of reverse sorting
			if namespace == ''			\
			or page.name == namespace	\
			or page.name.startswith(namespace+':'):
				return self._stores[namespace]
		else:
			raise LookupError, 'Could not find store for: %s' % name

	def get_stores(self):
		return self._stores.values()

	def resolve_path(self, name, source=None, index=None):
		'''Returns a proper path name for page names given in links
		or from user input. The optional argument 'source' is the
		path for the refering page, if any, or the path of the "current"
		page in the user interface.

		The 'index' argument allows specifying an index object, if
		none is given the default index for this notebook is used.

		If no source path is given or if the page name starts with
		a ':' the name is considered an absolute name and only case is
		resolved. If the page does not exist the last part(s) of the
		name will remain in the case as given.

		If a source path is given and the page name starts with '+'
		it will be resolved as a direct child of the source.

		Else we first look for a match of the first part of the name in the
		source path. If that fails we do a search for the first part of
		the name through all namespaces in the source path, starting with
		pages below the namespace of the source. If no existing page was
		found in this search we default to a new page below this namespace.

		So if we for example look for "baz" with as source ":foo:bar:dus"
		the following pages will be checked in a case insensitive way:

			:foo:bar:baz
			:foo:baz
			:baz

		And if none exist we default to ":foo:bar:baz"

		However if for example we are looking for "bar:bud" with as source
		":foo:bar:baz:dus", we only try to resolve the case for ":foo:bar:bud"
		and default to the given case if it does not yet exist.

		This method will raise a PageNameError if the name resolves
		to an empty string. Since all trailing ":" characters are removed
		there is no way for the name to address the root path in this method -
		and typically user input should not need to able to address this path.
		'''
		assert name, 'BUG: name is empty string'
		startswith = name[0]
		if startswith == '.':
			startswith = '+' # backward compat
		if startswith == '+':
			name = name[1:]
		name = self.cleanup_pathname(name)

		if index is None:
			index = self.index

		if startswith == ':' or source == None:
			return index.resolve_case(name) or Path(name)
		elif startswith == '+':
			if not source:
				raise PageNameError, '+'+name
			return index.resolve_case(source.name+':'+name)  \
						or Path(source.name+':'+name)
			# FIXME use parent as argument
		else:
			# first check if we see an explicit match in the path
			assert isinstance(source, Path)
			anchor = name.split(':')[0].lower()
			path = source.namespace.lower().split(':')
			if anchor in path:
				# ok, so we can shortcut to an absolute path
				path.reverse() # why is there no rindex or rfind ?
				i = path.index(anchor) + 1
				path = path[i:]
				path.reverse()
				path.append( name.lstrip(':') )
				name = ':'.join(path)
				return index.resolve_case(name) or Path(name)
				# FIXME use parentt as argument
				# FIXME use short cut when the result is the parent
			else:
				# no luck, do a search through the whole path - including root
				source = index.lookup_path(source) or source
				for parent in source.parents():
					candidate = index.resolve_case(name, namespace=parent)
					if not candidate is None:
						return candidate
				else:
					# name not found, keep case as is
					return source.parent + name

	def relative_link(self, source, href):
		'''Returns a link for a path 'href' relative to path 'source'.
		More or less the opposite of resolve_path().
		'''
		if href == source:
			return href.basename
		elif href > source:
			return '+' + href.relname(source)
		else:
			parent = source.commonparent(href)
			if parent.isroot:
				return ':' + href.name
			else:
				return parent.basename + ':' + href.relname(parent)

	def register_hook(self, name, handler):
		'''Register a handler method for a specific hook'''
		register = '_register_%s' % name
		if not hasattr(self, register):
			setattr(self, register, [])
		getattr(self, register).append(handler)

	def unregister_hook(self, name, handler):
		'''Remove a handler method for a specific hook'''
		register = '_register_%s' % name
		if hasattr(self, register):
			getattr(self, register).remove(handler)

	def suggest_link(self, source, word):
		'''Suggest a link Path for 'word' or return None if no suggestion is
		found. By default we do not do any suggestion but plugins can
		register handlers to add suggestions. See 'register_hook()' to
		register a handler.
		'''
		if not  hasattr(self, '_register_suggest_link'):
			return None

		for handler in self._register_suggest_link:
			link = handler(source, word)
			if not link is None:
				return link
		else:
			return None

	@staticmethod
	def cleanup_pathname(name):
		'''Returns a safe version of name, used internally by functions like
		resolve_path() to parse user input.
		'''
		orig = name
		name = ':'.join( map(unicode.strip,
				filter(lambda n: len(n)>0, unicode(name).split(':')) ) )

		# Reserved characters are:
		# The ':' is reserrved as seperator
		# The '?' is reserved to encode url style options
		# The '#' is reserved as anchor separator
		# The '/' and '\' are reserved to distinquise file links & urls
		# First character of each part MUST be alphanumeric
		#		(including utf8 letters / numbers)

		# Zim version < 0.42 restricted all special charachters but
		# white listed ".", "-", "_", "(", ")", ":" and "%".

		# TODO check for illegal characters in the name

		if not name or name.isspace():
			raise PageNameError, orig

		return name

	def get_page(self, path):
		'''Returns a Page object. This method uses a weakref dictionary to
		ensure that an unique object is being used for each page that is
		given out.
		'''
		# As a special case, using an invalid page as the argument should
		# return a valid page object.
		assert isinstance(path, Path)
		if path.name in self._page_cache \
		and self._page_cache[path.name].valid:
			return self._page_cache[path.name]
		else:
			store = self.get_store(path)
			page = store.get_page(path)
			# TODO - set haschildren if page maps to a store namespace
			self._page_cache[path.name] = page
			return page

	def flush_page_cache(self, path):
		'''Remove a page from the page cache, calling get_page() after this
		will return a fresh page object. Be aware that the old object
		may still be around but will have its 'valid' attribute set to False.
		This function also removes all child pages of path from the cache.
		'''
		names = [path.name]
		ns = path.name + ':'
		names.extend(k for k in self._page_cache.keys() if k.startswith(ns))
		for name in names:
			if name in self._page_cache:
				page = self._page_cache[name]
				assert not page.modified, 'BUG: Flushing page with unsaved changes'
				page.valid = False
				del self._page_cache[name]

	def get_home_page(self):
		'''Returns a page object for the home page.'''
		path = self.resolve_path(self.config['Notebook']['home'])
		return self.get_page(path)

	def get_pagelist(self, path):
		'''Returns a list of page objects.'''
		store = self.get_store(path)
		return store.get_pagelist(path)
		# TODO: add sub-stores in this namespace if any

	def store_page(self, page):
		'''Store a page permanently. Commits the parse tree from the page
		object to the backend store.
		'''
		assert page.valid, 'BUG: page object no longer valid'
		with SignalExceptionContext(self, 'store-page'):
			self.emit('store-page', page)

	def do_store_page(self, page):
		with SignalRaiseExceptionContext(self, 'store-page'):
			store = self.get_store(page)
			store.store_page(page)

	def revert_page(self, page):
		'''Reloads the parse tree from the store into the page object.
		In a sense the opposite to store_page(). Used in the gui to
		discard changes in a page.
		'''
		# get_page without the cache
		assert page.valid, 'BUG: page object no longer valid'
		store = self.get_store(page)
		storedpage = store.get_page(page)
		page.set_parsetree(storedpage.get_parsetree())
		page.modified = False

	def move_page(self, path, newpath, update_links=True):
		'''Move a page from 'path' to 'newpath'. If 'update_links' is
		True all links from and to the page will be modified as well.
		'''
		if update_links and self.index.updating:
			raise IndexBusyError, 'Index busy'
			# Index need to be complete in order to be 100% sure we
			# know all backlinks, so no way we can update links before.

		page = self.get_page(path)
		if not (page.hascontent or page.haschildren):
			raise LookupError, 'Page does not exist: %s' % path.name
		assert not page.modified, 'BUG: moving a page with uncomitted changes'

		with SignalExceptionContext(self, 'move-page'):
			self.emit('move-page', path, newpath, update_links)

	def do_move_page(self, path, newpath, update_links):
		logger.debug('Move %s to %s (%s)', path, newpath, update_links)

		with SignalRaiseExceptionContext(self, 'move-page'):
			# Collect backlinks
			if update_links:
				from zim.index import LINK_DIR_BACKWARD
				backlinkpages = set(
					l.source for l in
						self.index.list_links(path, LINK_DIR_BACKWARD) )
				for child in self.index.walk(path):
					backlinkpages.update(set(
						l.source for l in
							self.index.list_links(path, LINK_DIR_BACKWARD) ))

			# Do the actual move
			store = self.get_store(path)
			newstore = self.get_store(newpath)
			if newstore == store:
				store.move_page(path, newpath)
			else:
				assert False, 'TODO: move between stores'
				# recursive + move attachments as well

			self.flush_page_cache(path)
			self.flush_page_cache(newpath)

			# Update links in moved pages
			page = self.get_page(newpath)
			if page.hascontent:
				self._update_links_from(page, path)
				store = self.get_store(page)
				store.store_page(page)
				# do not use self.store_page because it emits signals
			for child in self._no_index_walk(newpath):
				if not child.hascontent:
					continue
				oldpath = path + child.relname(newpath)
				self._update_links_from(child, oldpath)
				store = self.get_store(child)
				store.store_page(child)
				# do not use self.store_page because it emits signals

			# Update links to the moved page tree
			if update_links:
				# Need this indexed before we can resolve links to it
				self.index.delete(path)
				self.index.update(newpath)
				#~ print backlinkpages
				for p in backlinkpages:
					if p == path or p > path:
						continue
					page = self.get_page(p)
					self._update_links_in_page(page, path, newpath)
					self.store_page(page)

	def _no_index_walk(self, path):
		'''Walking that can be used when the index is not in sync'''
		# TODO allow this to cross several stores
		store = self.get_store(path)
		for page in store.get_pagelist(path):
			yield page
			for child in self._no_index_walk(page): # recurs
				yield child

	@staticmethod
	def _update_link_tag(tag, newhref):
		newhref = str(newhref)
		haschildren = bool(list(tag.getchildren()))
		if not haschildren and tag.text == tag.attrib['href']:
			tag.text = newhref
		tag.attrib['href'] = newhref

	def _update_links_from(self, page, oldpath):
		logger.debug('Updating links in %s (was %s)', page, oldpath)
		tree = page.get_parsetree()
		if not tree:
			return

		for tag in tree.getiterator('link'):
			href = tag.attrib['href']
			type = link_type(href)
			if type == 'page':
				hrefpath = self.resolve_path(href, source=page)
				oldhrefpath = self.resolve_path(href, source=oldpath)
				#~ print 'LINK', oldhrefpath, '->', hrefpath
				if hrefpath != oldhrefpath:
					if hrefpath >= page and oldhrefpath >= oldpath:
						#~ print '\t.. Ignore'
						pass
					else:
						newhref = self.relative_link(page, oldhrefpath)
						#~ print '\t->', newhref
						self._update_link_tag(tag, newhref)

		page.set_parsetree(tree)

	def _update_links_in_page(self, page, oldpath, newpath):
		# Maybe counter intuitive, but pages below oldpath do not need
		# to exist anymore while we still try to resolve links to these
		# pages. The reason is that all pages that could link _upward_
		# to these pages are below and are moved as well.
		logger.debug('Updating links in %s to %s (was: %s)', page, newpath, oldpath)
		tree = page.get_parsetree()
		if not tree:
			logger.warn('Page turned out to be empty: %s', page)
			return

		for tag in tree.getiterator('link'):
			href = tag.attrib['href']
			type = link_type(href)
			if type == 'page':
				hrefpath = self.resolve_path(href, source=page)
				#~ print 'LINK', hrefpath
				if hrefpath == oldpath:
					newhrefpath = newpath
					#~ print '\t==', oldpath, '->', newhrefpath
				elif hrefpath > oldpath:
					rel = hrefpath.relname(oldpath)
					newhrefpath = newpath + rel
					#~ print '\t>', oldpath, '->', newhrefpath
				else:
					continue

				newhref = self.relative_link(page, newhrefpath)
				self._update_link_tag(tag, newhref)

		page.set_parsetree(tree)

	def rename_page(self, path, newbasename,
						update_heading=True, update_links=True):
		'''Rename page to a page in the same namespace but with a new basename.
		If 'update_heading' is True the first heading in the page will be updated to it's
		new name.  If 'update_links' is True all links from and to the page will be
		modified as well.
		'''
		logger.debug('Rename %s to "%s" (%s, %s)',
			path, newbasename, update_heading, update_links)

		newbasename = self.cleanup_pathname(newbasename)
		newpath = Path(path.namespace + ':' + newbasename)
		if newbasename.lower() != path.basename.lower():
			# allow explicit case-sensitive renaming
			newpath = self.index.resolve_case(
				newbasename, namespace=path.parent) or newpath

		self.move_page(path, newpath, update_links=update_links)
		if update_heading:
			page = self.get_page(newpath)
			tree = page.get_parsetree()
			if not tree is None:
				tree.set_heading(newbasename.title())
				page.set_parsetree(tree)
				self.store_page(page)

		return newpath

	def delete_page(self, path):
		with SignalExceptionContext(self, 'delete-page'):
			self.emit('delete-page', path)

	def do_delete_page(self, path):
		with SignalRaiseExceptionContext(self, 'delete-page'):
			store = self.get_store(path)
			store.delete_page(path)
			self.flush_page_cache(path)

	def resolve_file(self, filename, path):
		'''Resolves a file or directory path relative to a page. Returns a
		File object. However the file does not have to exist.

		File urls and paths that start with '~/' or '~user/' are considered
		absolute paths and are returned unmodified.

		In case the file path starts with '/' the the path is taken relative
		to the document root - this can e.g. be a parent directory of the
		notebook. Defaults to the home dir.

		Other paths are considered attachments and are resolved relative
		to the namespce below the page.

		Because this is used to resolve file links and is supposed to be
		platform independent it tries to convert windows filenames to
		unix equivalents.
		'''
		filename = filename.replace('\\', '/')
		if filename.startswith('~') or filename.startswith('file:/'):
			return File(filename)
		elif filename.startswith('/'):
			dir = self.get_document_root() or Dir('~')
			return dir.file(filename)
		elif is_win32_path_re.match(filename):
			if not filename.startswith('/'):
				filename = '/'+filename
				# make absolute on unix
			return File(filename)
		else:
			# TODO - how to deal with '..' in the middle of the path ?
			filepath = [p for p in filename.split('/') if len(p) and p != '.']
			if not filepath: # filename is e.g. "."
				return self.get_attachments_dir(path)
			pagepath = path.name.split(':')
			filename = filepath.pop()
			while filepath and filepath[0] == '..':
				if not pagepath:
					print 'TODO: handle paths relative to notebook but outside notebook dir'
					return File('/TODO')
				else:
					filepath.pop(0)
					pagepath.pop()
			pagename = ':'+':'.join(pagepath + filepath)
			dir = self.get_attachments_dir(Path(pagename))
			return dir.file(filename)

	def relative_filepath(self, file, path=None):
		'''Returns a filepath relative to either the documents dir (/xxx), the
		attachments dir (if a path is given) (./xxx or ../xxx) or the users
		home dir (~/xxx). Returns None otherwise.

		Intended as the counter part of resolve_file().
		Typically this function is used to present the user with readable paths
		or to shorten the paths inserted in the wiki code. It is advised to
		use file uris for links that can not be made relative.
		'''
		if path:
			root = self.dir
			dir = self.get_attachments_dir(path)
			if file.ischild(dir):
				return './'+file.relpath(dir)
			elif root and file.ischild(root) and dir.ischild(root):
				parent = file.commonparent(dir)
				uppath = dir.relpath(parent)
				downpath = file.relpath(parent)
				up = 1 + uppath.count('/')
				return '../'*up + downpath

		dir = self.get_document_root()
		if dir and file.ischild(dir):
			return '/'+file.relpath(dir)

		dir = Dir('~')
		if file.ischild(dir):
			return '~/'+file.relpath(dir)

		return None

	def get_attachments_dir(self, path):
		'''Returns a Dir object for the attachments directory for 'path'.
		The directory does not need to exist.
		'''
		store = self.get_store(path)
		return store.get_attachments_dir(path)

	def get_document_root(self):
		'''Returns the Dir object for the document root or None'''
		path = self.config['Notebook']['document_root']
		if path: return Dir(path)
		else: return None

	def get_template(self, path):
		'''Returns a template object for path. Typically used to set initial
		content for a new page.
		'''
		from zim.templates import get_template
		template = self.namespace_properties[path].get('template', '_New')
		logger.debug('Found template \'%s\' for %s', template, path)
		return get_template('wiki', template)

	def walk(self, path=None):
		'''Generator function which iterates through all pages, depth first.
		If a path is given, only iterates through sub-pages of that path.

		If you are only interested in the paths using Index.walk() will be
		more efficient.
		'''
		if path == None:
			path = Path(':')
		for p in self.index.walk(path):
			page = self.get_page(p)
			yield page

	def get_pagelist_indexkey(self, path):
		store = self.get_store(path)
		return store.get_pagelist_indexkey(path)

	def get_page_indexkey(self, path):
		store = self.get_store(path)
		return store.get_page_indexkey(path)