Logo Search packages:      
Sourcecode: zim version File versions  Download package

__init__.py

# -*- coding: utf-8 -*-

# Copyright 2009 Jaap Karssenberg <pardus@cpan.org>

import gtk

import logging

from zim.fs import File
from zim.plugins import PluginClass

from zim.gui.widgets import SingleClickTreeView, Dialog, PageEntry, IconButton, scrolled_text_view, QuestionDialog

# FUTURE allow vcs modules like bzr to have their own UI classes
# these can add additional menu items e.g. Tools->Bazaar-> ...
# or use their own graphical interfaces, like bzr gdiff

# FUTURE add option to also pull & push versions automatically

logger = logging.getLogger('zim.plugins.versioncontrol')


ui_xml = '''
<ui>
<menubar name='menubar'>
      <menu action='file_menu'>
            <placeholder name='versioning_actions'>
                  <menuitem action='save_version'/>
                  <menuitem action='show_versions'/>
            </placeholder>
      </menu>
</menubar>
</ui>
'''


ui_actions = (
      # name, stock id, label, accelerator, tooltip, readonly
      ('save_version', 'gtk-save-as', _('S_ave Version...'), '<ctrl><shift>S', '', False), # T: menu item
      ('show_versions', None, _('_Versions...'), '', '', True), # T: menu item
)


class VersionControlPlugin(PluginClass):

      plugin_info = {
            'name': _('Version Control'), # T: plugin name
            'description': _('''\
This plugin adds version control for notebooks.

This plugin is based on the Bazaar version control system.

This is a core plugin shipping with zim.
'''), # T: plugin description
            'author': 'Jaap Karssenberg',
            'help': 'Plugins:Version Control',
      }

      plugin_preferences = (
            ('autosave', 'bool', _('Autosave version on regular intervals'), False), # T: Label for plugin preference
      )

      def __init__(self, ui):
            PluginClass.__init__(self, ui)
            self.vcs = None
            if self.ui.ui_type == 'gtk':
                  self.ui.add_actions(ui_actions, self)
                  self.ui.add_ui(ui_xml, self)
                  self.actiongroup.get_action('show_versions').set_sensitive(False)
                  if self.ui.notebook:
                        self.detect_vcs()
                  else:
                        self.ui.connect_after('open-notebook',
                              lambda o, n: self.detect_vcs() )

                  def on_quit(o):
                        if self.preferences['autosave']:
                              self.autosave()
                  self.ui.connect('quit', on_quit)

      def detect_vcs(self):
            dir = self._get_notebook_dir()
            self.vcs = self._detect_vcs(dir)
            if self.vcs:
                  self.actiongroup.get_action('show_versions').set_sensitive(True)
                  if self.preferences['autosave']:
                        self.autosave()

      def _get_notebook_dir(self):
            notebook  = self.ui.notebook
            if notebook.dir:
                  return notebook.dir
            elif notebook.file:
                  return notebook.file.dir
            else:
                  assert 'Notebook is not based on a file or folder'

      @staticmethod
      def _detect_vcs(dir):
            # split off because it is easier to test this way
            vcs = None

            for path in reversed(list(dir)):
                  if path.subdir('.bzr').exists():
                        from zim.plugins.versioncontrol.bzr import BazaarVCS
                        vcs = BazaarVCS(path)
                  #~ elif path.subdir('.svn'):
                  #~ elif path.subdir('CVS'):
                  else:
                        continue

            if vcs:
                  logger.info('VCS detected: %s', vcs)
            else:
                  logger.info('No VCS detected')

            return vcs

      def autosave(self):
            assert self.vcs

            if self.ui.page and self.ui.page.modified:
                  self.ui.save_page()

            if self.vcs.modified:
                  logger.info('Automatically saving version')
                  self.vcs.commit(_('Automatically saved version from zim'))
                        # T: default version comment for auto-saved versions
            else:
                  logger.debug('No autosave version needed - no changes')

      def save_version(self):
            if not self.vcs:
                  # TODO choice from multiple version control systems
                  if QuestionDialog(self, (
                        _("Enable Version Control?"), # T: Question dialog
                        _("Version control is currently not enabled for this notebook.\n"
                          "Do you want to enable it?" ) # T: Detailed question
                  ) ).run():
                        self.init_vcs('bzr')

            if self.ui.page.modified:
                  self.ui.save_page()

            SaveVersionDialog(self.ui, self.vcs).run()

      def init_vcs(self, vcs):
            dir = self._get_notebook_dir()
            if vcs == 'bzr':
                  from zim.plugins.versioncontrol.bzr import BazaarVCS
                  self.vcs = BazaarVCS(dir)
            else:
                  assert False, 'Unkown VCS: %s' % vcs

            if self.vcs:
                  self.vcs.init()
                  self.actiongroup.get_action('show_versions').set_sensitive(True)

      def show_versions(self):
            dialog = VersionsDialog.unique(self, self.ui, self.vcs)
            dialog.present()


#~ class VersionControlInitDialog(Dialog):
      #~ pass


class SaveVersionDialog(Dialog):

      def __init__(self, ui, vcs):
            Dialog.__init__(self, ui, _('Save Version'), # T: dialog title
                  button=(None, 'gtk-save'), help='Plugins:Version Control')
            self.vcs = vcs

            self.add_text(_("Please enter a comment for this version")) # T: Dialog text

            vpaned = gtk.VPaned()
            self.vbox.add(vpaned)

            window, self.textview = scrolled_text_view(_('Saved version from zim'))
                  # T: default version comment in the "save version" dialog
            self.textview.set_editable(True)
            vpaned.add1(window)

            vbox = gtk.VBox()
            vpaned.add2(vbox)

            label = gtk.Label('<b>'+_('Details')+'</b>')
                  # T: section for version details in "save version" dialog
            label.set_use_markup(True)
            label.set_alignment(0, 0.5)
            vbox.pack_start(label, False)

            status = self.vcs.get_status()
            window, textview = scrolled_text_view(text=''.join(status), monospace=True)
            vbox.add(window)


      def do_response_ok(self):
            buffer = self.textview.get_buffer()
            start, end = buffer.get_bounds()
            msg = buffer.get_text(start, end, False).strip()
            if msg:
                  self.vcs.commit(msg)
                  return True
            else:
                  return False


class VersionsDialog(Dialog):

      # TODO put state in uistate ..

      def __init__(self, ui, vcs):
            Dialog.__init__(self, ui, _('Versions'), # T: dialog title
                  buttons=gtk.BUTTONS_CLOSE, help='Plugins:Version Control')
            self.vcs = vcs

            self.uistate.setdefault('windowsize', (600, 500), check=self.uistate.is_coord)
            self.uistate.setdefault('vpanepos', 300)

            self.vpaned = gtk.VPaned()
            self.vpaned.set_position(self.uistate['vpanepos'])
            self.vbox.add(self.vpaned)

            vbox = gtk.VBox(spacing=5)
            self.vpaned.pack1(vbox, resize=True)

            # Choice between whole notebook or page
            label = gtk.Label('<b>'+_('Versions')+':</b>') # section label
            label.set_use_markup(True)
            label.set_alignment(0, 0.5)
            vbox.pack_start(label, False)

            self.notebook_radio = gtk.RadioButton(None, _('Complete _notebook'))
                  # T: Option in versions dialog to show version for complete notebook
            self.page_radio = gtk.RadioButton(self.notebook_radio, _('_Page')+':')
                  # T: Option in versions dialog to show version for single page
            #~ recursive_box = gtk.CheckButton('Recursive')
            vbox.pack_start(self.notebook_radio, False)

            # Page entry
            hbox = gtk.HBox(spacing=5)
            vbox.pack_start(hbox, False)
            hbox.pack_start(self.page_radio, False)
            self.page_entry = PageEntry(self.ui.notebook)
            self.page_entry.set_path(ui.page)
            hbox.pack_start(self.page_entry, False)

            # View annotated button
            ann_button = gtk.Button(_('View _Annotated')) # T: Button label
            ann_button.connect('clicked', lambda o: self.show_annotated())
            hbox.pack_start(ann_button, False)

            # Help text
            label = gtk.Label('<i>\n'+_( '''\
Select a version to see changes between that version and the current
state. Or select multiple versions to see changes between those versions.
''' ).strip()+'</i>') # T: Help text in versions dialog
            label.set_use_markup(True)
            #~ label.set_alignment(0, 0.5)
            vbox.pack_start(label, False)

            # Version list
            self.versionlist = VersionsTreeView()
            self.versionlist.load_versions(vcs.list_versions())
            scrolled = gtk.ScrolledWindow()
            scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
            scrolled.set_shadow_type(gtk.SHADOW_IN)
            scrolled.add(self.versionlist)
            vbox.add(scrolled)

            # -----
            vbox = gtk.VBox(spacing=5)
            self.vpaned.pack2(vbox, resize=False)

            frame = gtk.Frame()
            label = gtk.Label('<b>'+_('Comment')+'</b>') # T: version details
            label.set_use_markup(True)
            frame.set_label_widget(label)
            vbox.add(frame)

            # Comment text
            window, textview = scrolled_text_view()
            self.comment_textview = textview
            window.set_border_width(10)
            frame.add(window)

            buttonbox = gtk.HButtonBox()
            buttonbox.set_layout(gtk.BUTTONBOX_END)
            vbox.pack_start(buttonbox, False)

            # Restore version button
            revert_button = gtk.Button(_('_Restore Version')) # T: Button label
            revert_button.connect('clicked', lambda o: self.restore_version())
            buttonbox.add(revert_button)

            # Notebook Changes button
            diff_button = gtk.Button(_('Show _Changes'))
                  # T: button in versions dialog for diff
            diff_button.connect('clicked', lambda o: self.show_changes())
            buttonbox.add(diff_button)

            # Compare page button
            comp_button = gtk.Button(_('_Side by Side'))
                  # T: button in versions dialog for side by side comparison
            comp_button.connect('clicked', lambda o: self.show_side_by_side())
            buttonbox.add(comp_button)


            # UI interaction between selections and buttons

            def on_row_activated(o, iter, path):
                  model = self.versionlist.get_model()
                  comment = model[iter][3]
                  buffer = textview.get_buffer()
                  buffer.set_text(comment)

            self.versionlist.connect('row-activated', on_row_activated)


            def on_ui_change(o):
                  usepage = self.page_radio.get_active()
                  self.page_entry.set_sensitive(usepage)
                  ann_button.set_sensitive(usepage)

                  # side by side comparison can only be done for one page
                  # revert can only be done to one version, not multiple
                  selection = self.versionlist.get_selection()
                  model, rows = selection.get_selected_rows()
                  if not rows:
                        revert_button.set_sensitive(False)
                        diff_button.set_sensitive(False)
                        comp_button.set_sensitive(False)
                  elif len(rows) == 1:
                        revert_button.set_sensitive(usepage)
                        diff_button.set_sensitive(True)
                        comp_button.set_sensitive(usepage)
                  else:
                        revert_button.set_sensitive(False)
                        diff_button.set_sensitive(True)
                        comp_button.set_sensitive(usepage)

            self.page_radio.connect('toggled', on_ui_change)
            selection = self.versionlist.get_selection()
            selection.connect('changed', on_ui_change)

            # select last version
            self.versionlist.get_selection().select_path((0,))
            col = self.versionlist.get_column(0)
            self.versionlist.row_activated(0, col)

      def save_uistate(self):
            self.uistate['vpanepos'] = self.vpaned.get_position()

      def _get_file(self):
            if self.notebook_radio.get_active():
                  if self.ui.page.modified:
                        self.ui.save_page()

                  return None
            else:
                  path = self.page_entry.get_path()
                  if path:
                        page = self.ui.notebook.get_page(path)
                        if page == self.ui.page and page.modified:
                              self.ui.save_page()
                  else:
                        return None # TODO error message valid page name?

                  if page \
                  and hasattr(page, 'source') \
                  and isinstance(page.source, File) \
                  and page.source.ischild(self.vcs.root):
                        return page.source
                  else:
                        return None # TODO error message ?

      def show_annotated(self):
            # TODO check for gannotated
            file = self._get_file()
            assert not file is None
            annotated = self.vcs.get_annotated(file)
            TextDialog(self, _('Annotated Page Source'), annotated).run()
                  # T: dialog title

      def restore_version(self):
            file = self._get_file()
            path = self.page_entry.get_path()
            version = self.versionlist.get_versions()[0]
            assert not file is None
            if QuestionDialog(self, (
                  _('Restore page to saved version?'), # T: Confirmation question
                  _('Do you want to restore page: %(page)s\n'
                    'to saved version: %(version)s ?\n\n'
                    'All changes since the last saved version will be lost !')
                    % {'page': path.name, 'version': str(version)}
                    # T: Detailed question, "%(page)s" is replaced by the page, "%(version)s" by the version id
            ) ).run():
                  self.vcs.revert(file=file, version=version)
                  self.ui.reload_page()

      def show_changes(self):
            # TODO check for gdiff
            file = self._get_file()
            versions = self.versionlist.get_versions()
            diff = self.vcs.get_diff(file=file, versions=versions)
            TextDialog(self, _('Changes'), diff).run()
                  # T: dialog title

      def show_side_by_side(self):
            print 'TODO - need config for an application like meld'


class TextDialog(Dialog):

      def __init__(self, ui, title, lines):
            Dialog.__init__(self, ui, title, buttons=gtk.BUTTONS_CLOSE)
            self.set_default_size(600, 300)
            window, textview = scrolled_text_view(''.join(lines), monospace=True)
            self.vbox.add(window)


class VersionsTreeView(SingleClickTreeView):

      # We are on purpose _not_ a subclass of the BrowserTreeView widget
      # because we utilize multiple selection to select versions for diffs

      def __init__(self):
            model = gtk.ListStore(int, str, str, str) # rev, date, user, msg
            gtk.TreeView.__init__(self, model)

            self.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
            self.set_rubber_banding(True)

            cell_renderer = gtk.CellRendererText()
            for name, i in (
                  (_('Rev'), 0), # T: Column header versions dialog
                  (_('Date'), 1), # T: Column header versions dialog
                  (_('Author'), 2), # T: Column header versions dialog
            ):
                  column = gtk.TreeViewColumn(name, cell_renderer, text=i)
                  column.set_sort_column_id(i)
                  if i == 0:
                        column.set_expand(True)
                  self.append_column(column)

            model.set_sort_column_id(0, gtk.SORT_DESCENDING)
                  # By default sort by rev

      def load_versions(self, versions):
            model = self.get_model()
            for version in versions:
                  model.append(version)

      def get_versions(self):
            model, rows = self.get_selection().get_selected_rows()
            if len(rows) == 1:
                  rev = int(model[rows[0]][0])
                  return (rev,)
            else:
                  rev = map(int, [model[path][0] for path in rows])
                  return (min(rev), max(rev))

Generated by  Doxygen 1.6.0   Back to index