コード例 #1
0
# Copyright 2008-2018 Jaap Karssenberg <*****@*****.**>

import logging

logger = logging.getLogger('zim.gui')

from gi.repository import Gtk

from zim.plugins import PluginManager
from zim.gui.widgets import Dialog, get_window, InputForm
from zim.parsing import is_interwiki_keyword_re

notebook_properties = (
    ('name', 'string', _('Name')),  # T: label for properties dialog
    ('interwiki', 'string', _('Interwiki Keyword'),
     lambda v: not v or is_interwiki_keyword_re.search(v)
     ),  # 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
    ('short_relative_links', 'bool', _('Paste short relative link names'),
     False),  # T: label for properties dialog
    # 'shared' property is not shown in properties anymore
)


class PropertiesDialog(Dialog):
    def __init__(self, parent, notebook, chosen_plugin=None):
        Dialog.__init__(self, parent, _('Properties'),
                        help='Help:Properties')  # T: Dialog title
コード例 #2
0
class Notebook(ConnectorMixin, SignalEmitter):
    '''Main class to access a notebook

	This class defines an API that proxies between backend L{zim.stores}
	and L{Index} objects on the one hand and the user interface on the
	other hand. (See L{module docs<zim.notebook>} for more explanation.)

	@signal: C{store-page (page)}: emitted before actually storing the page
	@signal: C{stored-page (page)}: emitted after storing the page
	@signal: C{move-page (oldpath, newpath)}: emitted before
	actually moving a page
	@signal: C{moved-page (oldpath, newpath)}: emitted after
	moving the page
	@signal: C{delete-page (path)}: emitted before deleting a page
	@signal: C{deleted-page (path)}: emitted after deleting a page
	means that the preferences need to be loaded again as well
	@signal: C{properties-changed ()}: emitted when properties changed
	@signal: C{suggest-link (path, text)}: hook that is called when trying
	to resolve links
	@signal: C{new-page-template (path, template)}: emitted before
	evaluating a template for a new page, intended for plugins that want
	to extend page templates

	@ivar name: The name of the notebook (string)
	@ivar icon: The path for the notebook icon (if any)
	# FIXME should be L{File} object
	@ivar document_root: The L{Dir} object for the X{document root} (if any)
	@ivar dir: Optional L{Dir} object for the X{notebook folder}
	@ivar file: Optional L{File} object for the X{notebook file}
	@ivar cache_dir: A L{Dir} object for the folder used to cache notebook state
	@ivar config: A L{SectionedConfigDict} for the notebook config
	(the C{X{notebook.zim}} config file in the notebook folder)
	@ivar profile: The name of the profile used by the notebook or C{None}
	@ivar index: The L{Index} object used by the notebook
	'''

    # define signals we want to use - (closure type, return type and arg types)
    __signals__ = {
        'store-page': (SIGNAL_NORMAL, None, (object, )),
        'stored-page': (SIGNAL_NORMAL, None, (object, )),
        'move-page': (SIGNAL_NORMAL, None, (object, object)),
        'moved-page': (SIGNAL_NORMAL, None, (object, object)),
        'delete-page': (SIGNAL_NORMAL, None, (object, )),
        'deleted-page': (SIGNAL_NORMAL, None, (object, )),
        'page-info-changed': (SIGNAL_NORMAL, None, (object, )),
        'properties-changed': (SIGNAL_NORMAL, None, ()),
        'new-page-template': (SIGNAL_NORMAL, None, (object, object)),

        # Hooks
        'suggest-link': (SIGNAL_NORMAL, object, (object, object)),
    }

    properties = (
        ('name', 'string', _('Name')),  # T: label for properties dialog
        ('interwiki', 'string', _('Interwiki Keyword'),
         lambda v: not v or is_interwiki_keyword_re.search(v)
         ),  # 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
        #~ ('profile', 'string', _('Profile'), list_profiles), # T: label for properties dialog
        ('profile', 'string', _('Profile')),  # T: label for properties dialog
        # 'shared' property is not shown in properties anymore
    )

    @classmethod
    def new_from_dir(klass, dir):
        '''Constructor to create a notebook based on a specific
		file system location.
		Since the file system is an external resource, this method
		will return unique objects per location and keep (weak)
		references for re-use.

		@param dir: a L{Dir} object
		@returns: a L{Notebook} object
		'''
        assert isinstance(dir, Dir)

        nb = _NOTEBOOK_CACHE.get(dir.uri)
        if nb:
            return nb

        from .index import Index
        from .layout import FilesLayout

        config = NotebookConfig(dir.file('notebook.zim'))
        endofline = config['Notebook']['endofline']
        shared = config['Notebook']['shared']

        subdir = dir.subdir('.zim')
        if not shared and subdir.exists() and _iswritable(subdir):
            cache_dir = subdir
        else:
            cache_dir = _cache_dir_for_dir(dir)

        folder = LocalFolder(dir.path)
        layout = FilesLayout(folder, endofline)
        cache_dir.touch()  # must exist for index to work
        index = Index(cache_dir.file('index.db').path, layout)

        nb = klass(dir, cache_dir, config, folder, layout, index)
        _NOTEBOOK_CACHE[dir.uri] = nb
        return nb

    def __init__(self, dir, cache_dir, config, folder, layout, index):
        self.dir = dir  # TODO remove
        self.folder = folder
        self.cache_dir = cache_dir
        self.config = config
        self.layout = layout
        self.index = index
        self._operation_check = NOOP

        self.readonly = not _iswritable(dir) if dir else None  # XXX

        if self.readonly:
            logger.info('Notebook read-only: %s', dir.path)

        self.namespace_properties = HierarchicDict({'template': 'Default'})
        self._page_cache = weakref.WeakValueDictionary()

        self.name = None
        self.icon = None
        self.document_root = None

        from .index import PagesView, LinksView, TagsView
        self.pages = PagesView.new_from_index(self.index)
        self.links = LinksView.new_from_index(self.index)
        self.tags = TagsView.new_from_index(self.index)

        def on_page_row_changed(o, row):
            if row['name'] in self._page_cache:
                self._page_cache[
                    row['name']].haschildren = row['n_children'] > 0
                self.emit('page-info-changed', self._page_cache[row['name']])

        def on_page_row_deleted(o, row):
            if row['name'] in self._page_cache:
                self._page_cache[row['name']].haschildren = False
                self.emit('page-info-changed', self._page_cache[row['name']])

        self.index.update_iter.pages.connect('page-row-changed',
                                             on_page_row_changed)
        self.index.update_iter.pages.connect('page-row-deleted',
                                             on_page_row_deleted)

        self.do_properties_changed()

    @property
    def uri(self):
        '''Returns a file:// uri for this notebook that can be opened by zim'''
        return self.layout.root.uri

    @property
    def info(self):
        '''The L{NotebookInfo} object for this notebook'''
        try:
            uri = self.uri
        except AssertionError:
            uri = None

        return NotebookInfo(uri, **self.config['Notebook'])

    @property
    def profile(self):
        '''The 'profile' property for this notebook'''
        return self.config['Notebook'].get(
            'profile') or None  # avoid returning ''

    @notebook_state
    def save_properties(self, **properties):
        '''Save a set of properties in the notebook config

		This method does an C{update()} on the dict with properties but
		also updates the object attributes that map those properties.

		@param properties: the properties to update

		@emits: properties-changed
		'''
        dir = Dir(self.layout.root.path)  # XXX

        # Check if icon is relative
        icon = properties.get('icon')
        if icon and not isinstance(icon, basestring):
            assert isinstance(icon, File)
            if icon.ischild(dir):
                properties['icon'] = './' + icon.relpath(dir)
            else:
                properties['icon'] = icon.user_path or icon.path

        # Check document root is relative
        root = properties.get('document_root')
        if root and not isinstance(root, basestring):
            assert isinstance(root, Dir)
            if root.ischild(dir):
                properties['document_root'] = './' + root.relpath(dir)
            else:
                properties['document_root'] = root.user_path or root.path

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

        # Actual update and signals
        # ( write is the last action - in case update triggers a crash
        #   we don't want to get stuck with a bad config )
        self.config['Notebook'].update(properties)
        self.emit('properties-changed')

        if hasattr(self.config, 'write'):  # Check needed for tests
            self.config.write()

    def do_properties_changed(self):
        config = self.config['Notebook']
        dir = Dir(self.layout.root.path)  # XXX

        self.name = config['name']
        icon, document_root = _resolve_relative_config(dir, config)
        if icon:
            self.icon = icon.path  # FIXME rewrite to use File object
        else:
            self.icon = None
        self.document_root = document_root

        # TODO - can we switch cache_dir on run time when 'shared' changed ?

    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 using the 'C{suggest-link}'
		signal.
		'''
        return self.emit('suggest-link', source, word)

    def get_page(self, path):
        '''Get a L{Page} object for a given path

		Typically a Page object will be returned even when the page
		does not exist. In this case the C{hascontent} attribute of
		the Page will be C{False} and C{get_parsetree()} will return
		C{None}. This means that you do not have to create a page
		explicitly, just get the Page object and store it with new
		content (if it is not read-only of course).

		However in some cases this method will return C{None}. This
		means that not only does the page not exist, but also that it
		can not be created. This should only occur for certain special
		pages and depends on the store implementation.

		@param path: a L{Path} object
		@returns: a L{Page} object or C{None}
		'''
        # 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:
            page = self._page_cache[path.name]
            assert isinstance(page, Page)
            page._check_source_etag()
            return page
        else:
            file, folder = self.layout.map_page(path)
            folder = self.layout.get_attachments_folder(path)
            page = Page(path, False, file, folder)
            try:
                indexpath = self.pages.lookup_by_pagename(path)
            except IndexNotFoundError:
                pass
                # TODO trigger indexer here if page exists !
            else:
                if indexpath and indexpath.haschildren:
                    page.haschildren = True
                # page might be the parent of a placeholder, in that case
                # the index knows it has children, but the store does not

            # TODO - set haschildren if page maps to a store namespace
            self._page_cache[path.name] = page
            return page

    def get_new_page(self, path):
        '''Like get_page() but guarantees the page does not yet exist
		by adding a number to the name to make it unique.

		This method is intended for cases where e.g. a automatic script
		wants to store a new page without user interaction. Conflicts
		are resolved automatically by appending a number to the name
		if the page already exists. Be aware that the resulting Page
		object may not match the given Path object because of this.

		@param path: a L{Path} object
		@returns: a L{Page} object
		'''
        i = 0
        base = path.name
        page = self.get_page(path)
        while page.hascontent or page.haschildren:
            i += 1
            path = Path(base + ' %i' % i)
            page = self.get_page(path)
        return page

    @notebook_state
    def flush_page_cache(self, path):
        '''Flush the cache used by L{get_page()}

		After this method calling L{get_page()} for C{path} or any of
		its children will return a fresh page object. Be aware that the
		old Page objects may still be around but will be flagged as
		invalid and can no longer be used in the API.

		@param path: a L{Path} object
		'''
        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 L{Page} object for the home page'''
        return self.get_page(self.config['Notebook']['home'])

    @notebook_state
    def store_page(self, page):
        '''Save the data from the page in the storage backend

		@param page: a L{Page} object
		@emits: store-page before storing the page
		@emits: stored-page on success
		'''
        assert page.valid, 'BUG: page object no longer valid'
        logger.debug('Store page: %s', page)
        self.emit('store-page', page)
        page._store()
        file, folder = self.layout.map_page(page)
        self.index.update_file(file)
        page.modified = False
        self.emit('stored-page', page)

    @notebook_state
    def store_page_async(self, page, parsetree_func):
        assert page.valid, 'BUG: page object no longer valid'
        logger.debug('Store page in background: %s', page)
        self.emit('store-page', page)
        error = threading.Event()
        thread = threading.Thread(target=partial(
            self._store_page_async_thread_main, page, parsetree_func, error))
        thread.start()
        pre_modified = page.modified
        op = SimpleAsyncOperation(notebook=self,
                                  message='Store page in progress',
                                  thread=thread,
                                  post_handler=partial(
                                      self._store_page_async_finished, page,
                                      error, pre_modified))
        op.error_event = error
        op.run_on_idle()
        return op

    def _store_page_async_thread_main(self, page, parsetree_func, error):
        try:
            tree = parsetree_func()
            page._store_tree(tree)
        except:
            error.set()
            logger.exception('Error in background save')

    def _store_page_async_finished(self, page, error, pre_modified):
        if not error.is_set():
            file, folder = self.layout.map_page(page)
            self.index.update_file(file)
            if page.modified == pre_modified:
                # HACK: Checking modified state protects against race condition
                # in async store. Works because pageview sets "page.modified"
                # to a counter rather than a boolean
                page.modified = False
                self.emit('stored-page', page)

    def move_page(self, path, newpath, update_links=True):
        '''Move a page in the notebook

		@param path: a L{Path} object for the old/current page name
		@param newpath: a L{Path} object for the new page name
		@param update_links: if C{True} all links B{from} and B{to} this
		page and any of it's children will be updated to reflect the
		new page name

		The original page C{path} does not have to exist, in this case
		only the link update will done. This is useful to update links
		for a placeholder.

		Where:
		  - C{page} is the L{Page} object for the page being updated
		  - C{total} is an optional parameter for the number of pages
		    still to go - if known

		@raises PageExistsError: if C{newpath} already exists

		@emits: move-page before the move
		@emits: moved-page after succesful move
		'''
        for p in self.move_page_iter(path, newpath, update_links):
            pass

    @assert_index_uptodate
    @notebook_state
    def move_page_iter(self, path, newpath, update_links=True):
        '''Like L{move_page()} but yields pages that are being updated
		if C{update_links} is C{True}
		'''
        logger.debug('Move page %s to %s', path, newpath)

        self.emit('move-page', path, newpath)
        n_links = self.links.n_list_links_section(path, LINK_DIR_BACKWARD)
        self._move_file_and_folder(path, newpath)
        self.flush_page_cache(path)
        self.emit('moved-page', path, newpath)

        if update_links:
            for p in self._update_links_in_moved_page(path, newpath):
                yield p

            for p in self._update_links_to_moved_page(path, newpath):
                yield p

            new_n_links = self.links.n_list_links_section(
                newpath, LINK_DIR_BACKWARD)
            if new_n_links != n_links:
                logger.warn(
                    'Number of links after move (%i) does not match number before move (%i)',
                    new_n_links, n_links)
            else:
                logger.debug(
                    'Number of links after move does match number before move (%i)',
                    new_n_links)

    def _move_file_and_folder(self, path, newpath):
        file, folder = self.layout.map_page(path)
        if not (file.exists() or folder.exists()):
            raise PageNotFoundError(path)

        newfile, newfolder = self.layout.map_page(newpath)
        if file.path.lower() == newfile.path.lower():
            if newfile.isequal(file) or newfolder.isequal(folder):
                pass  # renaming on case-insensitive filesystem
            elif newfile.exists() or newfolder.exists():
                raise PageExistsError(newpath)
        elif newfile.exists() or newfolder.exists():
            raise PageExistsError(newpath)

        # First move the dir - if it fails due to some file being locked
        # the whole move is cancelled. Chance is bigger than the other
        # way around, e.g. attachment open in external program.

        if folder.exists():
            if newfolder.ischild(folder):
                # special case where we want to move a page down
                # into it's own namespace
                parent = folder.parent()
                tmp = parent.new_folder(folder.basename)
                folder.moveto(tmp)
                tmp.moveto(newfolder)
            else:
                folder.moveto(newfolder)

            self.index.file_moved(folder, newfolder)

            # check if we also moved the file inadvertently
            if file.ischild(folder):
                rel = file.relpath(folder)
                movedfile = newfolder.file(rel)
                if movedfile.exists() and movedfile.path != newfile.path:
                    movedfile.moveto(newfile)
                    self.index.file_moved(movedfile, newfile)
            elif file.exists():
                file.moveto(newfile)
                self.index.file_moved(file, newfile)

        elif file.exists():
            file.moveto(newfile)
            self.index.file_moved(file, newfile)

    def _update_links_in_moved_page(self, oldtarget, newtarget):
        # Find (floating) links that originate from the moved page
        # check if they would resolve different from the old location
        seen = set()
        for link in list(self.links.list_links_section(newtarget)):
            if link.source.name not in seen \
            and not (
             link.target == newtarget
             or link.target.ischild(newtarget)
            ):
                if link.source == newtarget:
                    oldpath = oldtarget
                else:
                    oldpath = oldtarget + link.source.relname(newtarget)

                yield link.source
                self._update_moved_page(link.source, oldpath, newtarget,
                                        oldtarget)
                seen.add(link.source.name)

    def _update_moved_page(self, path, oldpath, newroot, oldroot):
        logger.debug('Updating links in page moved from %s to %s', oldpath,
                     path)
        page = self.get_page(path)
        tree = page.get_parsetree()
        if not tree:
            return 0

        def replacefunc(elt):
            text = elt.attrib['href']
            if link_type(text) != 'page':
                raise zim.formats.VisitorSkip

            href = HRef.new_from_wiki_link(text)
            if href.rel == HREF_REL_FLOATING:
                newtarget = self.pages.resolve_link(page, href)
                oldtarget = self.pages.resolve_link(oldpath, href)

                if newtarget != oldtarget:
                    try:
                        update = \
                         newtarget.relname(newroot) != oldtarget.relname(oldroot)
                    except ValueError:
                        update = True

                    if update:
                        return self._update_link_tag(elt, page, oldtarget,
                                                     href)

            raise zim.formats.VisitorSkip

        tree.replace(zim.formats.LINK, replacefunc)
        page.set_parsetree(tree)
        self.store_page(page)

    def _update_links_to_moved_page(self, oldtarget, newtarget):
        # 1. Check remaining placeholders, update pages causing them
        seen = set()
        try:
            oldtarget = self.pages.lookup_by_pagename(oldtarget)
        except IndexNotFoundError:
            pass
        else:
            for link in list(
                    self.links.list_links_section(oldtarget,
                                                  LINK_DIR_BACKWARD)):
                if link.source.name not in seen:
                    yield link.source
                    self._move_links_in_page(link.source, oldtarget, newtarget)
                    seen.add(link.source.name)

        # 2. Check for links that have anchor of same name as the moved page
        # and originate from a (grand)child of the parent of the moved page
        # and no longer resolve to the moved page
        parent = oldtarget.parent
        for link in list(self.links.list_floating_links(oldtarget.basename)):
            if link.source.name not in seen \
            and link.source.ischild(parent) \
            and not (
             link.target == newtarget
             or link.target.ischild(newtarget)
            ):
                yield link.source
                self._move_links_in_page(link.source, oldtarget, newtarget)
                seen.add(link.source.name)

    def _move_links_in_page(self, path, oldtarget, newtarget):
        logger.debug('Updating page %s to move link from %s to %s', path,
                     oldtarget, newtarget)
        page = self.get_page(path)
        tree = page.get_parsetree()
        if not tree:
            return 0

        def replacefunc(elt):
            text = elt.attrib['href']
            if link_type(text) != 'page':
                raise zim.formats.VisitorSkip

            href = HRef.new_from_wiki_link(text)
            target = self.pages.resolve_link(page, href)

            if target == newtarget or target.ischild(newtarget):
                raise zim.formats.VisitorSkip

            elif target == oldtarget:
                return self._update_link_tag(elt, page, newtarget, href)
            elif target.ischild(oldtarget):
                mynewtarget = newtarget.child(target.relname(oldtarget))
                return self._update_link_tag(elt, page, mynewtarget, href)

            elif href.rel == HREF_REL_FLOATING \
            and href.parts()[0] == newtarget.basename \
            and page.ischild(oldtarget.parent) \
            and not target.ischild(oldtarget.parent):
                # Edge case: an link that was anchored to the moved page,
                # and now resolves somewhere higher in the tree
                if href.names == newtarget.basename:
                    return self._update_link_tag(elt, page, newtarget, href)
                else:
                    mynewtarget = newtarget.child(':'.join(href.parts[1:]))
                    return self._update_link_tag(elt, page, mynewtarget, href)

            else:
                raise zim.formats.VisitorSkip

        tree.replace(zim.formats.LINK, replacefunc)
        page.set_parsetree(tree)
        self.store_page(page)

    def _update_link_tag(self, elt, source, target, oldhref):
        if oldhref.rel == HREF_REL_ABSOLUTE:  # prefer to keep absolute links
            newhref = HRef(HREF_REL_ABSOLUTE, target.name)
        else:
            newhref = self.pages.create_link(source, target)

        text = newhref.to_wiki_link()
        if elt.gettext() == elt.get('href'):
            elt[:] = [text]
        elt.set('href', text)
        return elt

    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.

		This is similar to moving within the same namespace, but
		conceptually different in the user interface. Internally
		L{move_page()} is used here as well.

		@param path: a L{Path} object for the old/current page name
		@param newbasename: new name as string
		@param update_heading: if C{True} the first heading in the
		page will be updated to the new name
		@param update_links: if C{True} all links B{from} and B{to} this
		page and any of it's children will be updated to reflect the
		new page name

		@emits: move-page before the move
		@emits: moved-page after succesful move
		'''
        newbasename = Path.makeValidPageName(newbasename)
        newpath = Path(path.namespace + ':' + newbasename)

        for p in self.rename_page_iter(path, newbasename, update_heading,
                                       update_links):
            pass

        return newpath

    @assert_index_uptodate
    @notebook_state
    def rename_page_iter(self,
                         path,
                         newbasename,
                         update_heading=True,
                         update_links=True):
        '''Like L{rename_page()} but yields pages that are being updated
		if C{update_links} is C{True}
		'''
        logger.debug('Rename %s to "%s" (%s, %s)', path, newbasename,
                     update_heading, update_links)

        newbasename = Path.makeValidPageName(newbasename)
        newpath = Path(path.namespace + ':' + newbasename)

        for p in self.move_page_iter(path, newpath, update_links):
            yield p

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

    @assert_index_uptodate
    @notebook_state
    def delete_page(self, path, update_links=True):
        '''Delete a page from the notebook

		@param path: a L{Path} object
		@param update_links: if C{True} pages linking to the
		deleted page will be updated and the link are removed.

		@returns: C{True} when the page existed and was deleted,
		C{False} when the page did not exist in the first place.

		Raises an error when delete failed.

		@emits: delete-page before the actual delete
		@emits: deleted-page after succesful deletion
		'''
        existed = self._delete_page(path)

        for p in self._deleted_page(path, update_links):
            pass

        return existed

    @assert_index_uptodate
    @notebook_state
    def delete_page_iter(self, path, update_links=True):
        '''Like L{delete_page()}'''
        self._delete_page(path)

        for p in self._deleted_page(path, update_links):
            yield p

    def _delete_page(self, path):
        logger.debug('Delete page: %s', path)
        self.emit('delete-page', path)

        file, folder = self.layout.map_page(path)
        assert file.path.startswith(self.folder.path)
        assert folder.path.startswith(self.folder.path)

        if not (file.exists() or folder.exists()):
            return False
        else:
            if folder.exists():
                folder.remove_children()
                folder.remove()
            if file.exists():
                file.remove()

            self.index.update_file(file)
            self.index.update_file(folder)

            return True

    @assert_index_uptodate
    @notebook_state
    def trash_page(self, path, update_links=True):
        '''Move a page to Trash

		Like L{delete_page()} but will use the system Trash (which may
		depend on the OS we are running on). This is used in the
		interface as a more user friendly version of delete as it is
		undoable.

		@param path: a L{Path} object
		@param update_links: if C{True} pages linking to the
		deleted page will be updated and the link are removed.

		@returns: C{True} when the page existed and was deleted,
		C{False} when the page did not exist in the first place.

		Raises an error when trashing failed.

		@raises TrashNotSupportedError: if trashing is not supported by
		the storage backend or when trashing is explicitly disabled
		for this notebook.

		@emits: delete-page before the actual delete
		@emits: deleted-page after succesful deletion
		'''
        existed = self._trash_page(path)

        for p in self._deleted_page(path, update_links):
            pass

        return existed

    @assert_index_uptodate
    @notebook_state
    def trash_page_iter(self, path, update_links=True):
        '''Like L{trash_page()}'''
        self._trash_page(path)

        for p in self._deleted_page(path, update_links):
            yield p

    def _trash_page(self, path):
        from zim.newfs.helpers import TrashHelper

        logger.debug('Trash page: %s', path)

        if self.config['Notebook']['disable_trash']:
            raise TrashNotSupportedError, 'disable_trash is set'

        self.emit('delete-page', path)

        file, folder = self.layout.map_page(path)
        helper = TrashHelper()

        re = False
        if folder.exists():
            re = helper.trash(folder)
            if isinstance(path, Page):
                path.haschildren = False

        if file.exists():
            re = helper.trash(file) or re

        self.index.update_file(file)
        self.index.update_file(folder)

        return re

    def _deleted_page(self, path, update_links):
        self.flush_page_cache(path)
        path = Path(path.name)

        if update_links:
            # remove persisting links
            try:
                indexpath = self.pages.lookup_by_pagename(path)
            except IndexNotFoundError:
                pass
            else:
                pages = set(l.source for l in self.links.list_links_section(
                    path, LINK_DIR_BACKWARD))

                for p in pages:
                    yield p
                    page = self.get_page(p)
                    self._remove_links_in_page(page, path)
                    self.store_page(page)

        # let everybody know what happened
        self.emit('deleted-page', path)

    def _remove_links_in_page(self, page, path):
        logger.debug('Removing links in %s to %s', page, path)
        tree = page.get_parsetree()
        if not tree:
            return

        def replacefunc(elt):
            href = elt.attrib['href']
            type = link_type(href)
            if type != 'page':
                raise zim.formats.VisitorSkip

            hrefpath = self.pages.lookup_from_user_input(href, page)
            #~ print 'LINK', hrefpath
            if hrefpath == path \
            or hrefpath.ischild(path):
                # Replace the link by it's text
                return zim.formats.DocumentFragment(*elt)
            else:
                raise zim.formats.VisitorSkip

        tree.replace(zim.formats.LINK, replacefunc)
        page.set_parsetree(tree)

    def resolve_file(self, filename, path=None):
        '''Resolve a file or directory path relative to a page or
		Notebook

		This method is intended to lookup file links found in pages and
		turn resolve the absolute path of those files.

		File URIs and paths that start with '~/' or '~user/' are
		considered absolute paths. Also windows path names like
		'C:\user' are recognized as absolute paths.

		Paths that starts with a '/' are taken relative to the
		to the I{document root} - this can e.g. be a parent directory
		of the notebook. Defaults to the filesystem root when no document
		root is set. (So can be relative or absolute depending on the
		notebook settings.)

		Paths starting with any other character are considered
		attachments. If C{path} is given they are resolved relative to
		the I{attachment folder} of that page, otherwise they are
		resolved relative to the I{notebook folder} - if any.

		The file is resolved purely based on the path, it does not have
		to exist at all.

		@param filename: the (relative) file path or uri as string
		@param path: a L{Path} object for the page
		@returns: a L{File} object.
		'''
        assert isinstance(filename, basestring)
        filename = filename.replace('\\', '/')
        if filename.startswith('~') or filename.startswith('file:/'):
            return File(filename)
        elif filename.startswith('/'):
            dir = self.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:
            if path:
                dir = self.get_attachments_dir(path)
                return File(
                    (dir.path, filename)
                )  # XXX LocalDir --> File -- will need get_abspath to resolve
            else:
                dir = Dir(self.layout.root.path)  # XXX
                return File((dir, filename))

    def relative_filepath(self, file, path=None):
        '''Get a file path relative to the notebook or page

		Intended as the counter part of L{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 with
		this method.

		The link can be relative:
		  - to the I{document root} (link will start with "/")
		  - the attachments dir (if a C{path} is given) or the notebook
		    (links starting with "./" or "../")
		  - or the users home dir (link like "~/user/")

		Relative file paths are always given with Unix path semantics
		(so "/" even on windows). But a leading "/" does not mean the
		path is absolute, but rather that it is relative to the
		X{document root}.

		@param file: L{File} object we want to link
		@keyword path: L{Path} object for the page where we want to
		link this file

		@returns: relative file path as string, or C{None} when no
		relative path was found
		'''
        from zim.newfs import LocalFile, LocalFolder
        file = LocalFile(file.path)  # XXX
        notebook_root = self.layout.root
        document_root = LocalFolder(
            self.document_root.path) if self.document_root else None  # XXX

        rootdir = '/'
        mydir = '.' + os.sep
        updir = '..' + os.sep

        # Look within the notebook
        if path:
            attachments_dir = self.get_attachments_dir(path)

            if file.ischild(attachments_dir):
                return mydir + file.relpath(attachments_dir)
            elif document_root and notebook_root \
            and document_root.ischild(notebook_root) \
            and file.ischild(document_root) \
            and not attachments_dir.ischild(document_root):
                # special case when document root is below notebook root
                # the case where document_root == attachment_folder is
                # already caught by above if clause
                return rootdir + file.relpath(document_root)
            elif notebook_root \
            and file.ischild(notebook_root) \
            and attachments_dir.ischild(notebook_root):
                parent = file.commonparent(attachments_dir)
                uppath = attachments_dir.relpath(parent)
                downpath = file.relpath(parent)
                up = 1 + uppath.count('/')
                return updir * up + downpath
        else:
            if document_root and notebook_root \
            and document_root.ischild(notebook_root) \
            and file.ischild(document_root):
                # special case when document root is below notebook root
                return rootdir + file.relpath(document_root)
            elif notebook_root and file.ischild(notebook_root):
                return mydir + file.relpath(notebook_root)

        # If that fails look for global folders
        if document_root and file.ischild(document_root):
            return rootdir + file.relpath(document_root)

        # Finally check HOME or give up
        path = file.userpath
        return path if path.startswith('~') else None

    def get_attachments_dir(self, path):
        '''Get the X{attachment folder} for a specific page

		@param path: a L{Path} object
		@returns: a L{Dir} object or C{None}

		Always returns a Dir object when the page can have an attachment
		folder, even when the folder does not (yet) exist. However when
		C{None} is returned the store implementation does not support
		an attachments folder for this page.
		'''
        return self.layout.get_attachments_folder(path)

    def get_template(self, path):
        '''Get a template for the intial text on new pages
		@param path: a L{Path} object
		@returns: a L{ParseTree} object
		'''
        # FIXME hardcoded that template must be wiki format

        template = self.namespace_properties[path]['template']
        logger.debug('Found template \'%s\' for %s', template, path)
        template = zim.templates.get_template('wiki', template)
        return self.eval_new_page_template(path, template)

    def eval_new_page_template(self, path, template):
        lines = []
        context = {
            'page': {
                'name': path.name,
                'basename': path.basename,
                'section': path.namespace,
                'namespace': path.namespace,  # backward compat
            }
        }
        self.emit('new-page-template', path, template)  # plugin hook
        template.process(lines, context)

        parser = zim.formats.get_parser('wiki')
        return parser.parse(lines)
コード例 #3
0
# Copyright 2008-2018 Jaap Karssenberg <*****@*****.**>

import logging

logger = logging.getLogger('zim.gui')


from gi.repository import Gtk

from zim.plugins import PluginManager
from zim.gui.widgets import Dialog, get_window, InputForm
from zim.parsing import is_interwiki_keyword_re

notebook_properties = (
	('name', 'string', _('Name')), # T: label for properties dialog
	('interwiki', 'string', _('Interwiki Keyword'), lambda v: not v or is_interwiki_keyword_re.search(v)), # 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
	('short_links', 'bool', _('Prefer short names for page links'), False), # T: label for properties dialog
	('disable_trash', 'bool', _('Do not use system trash for this notebook'), False) # T: label for properties dialog
	# 'shared' property is not shown in properties anymore
)


class PropertiesDialog(Dialog):

	def __init__(self, parent, notebook, chosen_plugin=None):
		Dialog.__init__(self, parent, _('Properties'), help='Help:Properties') # T: Dialog title
		self.notebook = notebook