#
#		Python GUI - Documents - Generic
#

from AlertFunctions import confirm, confirm_or_cancel
from Properties import overridable_property
from Models import Model
from MessageHandlers import MessageHandler
from Files import FileRef, DirRef
from FileDialogs import request_new_file
from Applications import application
from Exceptions import Cancel, UnimplementedMethod, ApplicationError

_next_doc_number = 1	# Counter for generating default titles

class Document(Model, MessageHandler):
    """A Document represents an
    application data structure that can be stored in a file. It
    implements the standard parts of asking the user for file names and
    reading and writing files.

    Each Document can have one or more windows associated with it. When
    the last window belonging to a document is closed, the document itself
    is closed.

    A Document provides support for keeping track of whether it has been
    edited, and asking the user whether to save changes when it is
    closed."""
    
    needs_saving = overridable_property('needs_saving',
        "True if the document has been edited and needs to be saved.")
        
    file = overridable_property('file', 
        """FileRef of the file that the document was read from or last written
        to, or None. Changing this causes update_title to be called.""")
    
    file_type = overridable_property('file_type',
        """FileType specifying the type of file handled by this document.""")
        
    title = overridable_property('title',
        """The title of the document. Changing this causes update_title of each
        associated window to be called.""")

    windows = overridable_property('windows',
        "List of windows associated with the document. Do not modify directly.")
    
    _file_type = None    # Type of file to create when saving
    binary = True       # True if files are to be opened in binary mode

    _needs_saving = 0   # True if has been edited
    _file = None		    # FileRef of associated file, if any
    _title = None		    # Title for use in window banners, etc.
    _windows = None     # List of associated windows

    #
    #		Initialisation and destruction
    #

    def __init__(self, **kwds):
        self._windows = []
        Model.__init__(self, **kwds)
        application()._add_document(self)

    def destroy(self):
        """Destroy any associated windows, then destroy document contents."""
        #print "Document.destroy:", self ###
        for win in self._windows[:]:
            win.destroy()
        application()._remove_document(self)
        self.destroy_contents()
        Model.destroy(self)

    #
    #		Properties
    #

    def get_needs_saving(self):
        return self._needs_saving

    def set_needs_saving(self, x):
        self._needs_saving = x

    def get_file(self):
        return self._file

    def set_file(self, x):
        self._file = x
        self.update_title()
    
    def get_file_type(self):
        return self._file_type
    
    def set_file_type(self, x):
        self._file_type = x

    def get_title(self):
        t = self._title
        if t == None:
            t = self.make_title()
            self._title = t
        return t

    def set_title(self, x):
        self._title = x
        for win in self._windows:
            win.update_title()

    def get_windows(self):
        return self._windows

    #
    #		Methods
    #

    def changed(self):
        "Set the needs_saving property to true."
        self.needs_saving = 1

    def new_contents(self):
        """Should initialise the document to the appropriate state following a New
        command."""
        pass

    def read_contents(self, file):
        """Should initialise the document's contents by reading it from the given
        file object."""
        raise UnimplementedMethod(self, 'read_contents')

    def write_contents(self, file):
        """Should write the document's contents to the given file object."""
        raise UnimplementedMethod(self, 'write_contents')

    def destroy_contents(self):
        """Called when the contents of the document are about to be discarded. 
        If the contents contains any Model objects, they should be destroyed."""
    
    def save_changes(self):
        """If the document has been edited, ask the user whether to save changes,
        and do so if requested."""
        if self._needs_saving:
            result = confirm_or_cancel('Save changes to "%s"?' % self.title,
                "Save", "Don't Save", "Cancel")
            if result < 0:
                raise Cancel
            if result:
                self.save_cmd()

    def save_cmd(self):
        """Implements the standard Save command. Writes the document to its
        associated file, asking the user for one first if necessary."""
        if self._file == None:
            self.get_new_file_name()
        try:
            self.write()
        except EnvironmentError, e:
            raise ApplicationError("Unable to save '%s'." % self._file.name, e)

    def save_as_cmd(self):
        """Implements the standard Save As... command. Asks the user for a new file
        and writes the document to it."""
        self.get_new_file_name()
        self.save_cmd()

    def revert_cmd(self):
        """Implements the standard Revert command. Discards the current contents
        of the document and re-reads it from the associated file."""
        if self._file != None:
            if confirm(
                    'Revert to the last saved version of "%s"?' % self.title,
                    "Revert", "Cancel"):
                self.destroy_contents()
                self.read()

    def close_cmd(self):
        """Implements the standard Close command. Asks whether to save any
        changes, then destroys the document."""
        self.save_changes()
        self.destroy()

    def make_title(self):
        """Generates a title for the document. If associated with a file,
        uses its last pathname component, else generates 'Untitled-n'."""
        global _next_doc_number
        if self._file != None:
            return os.path.basename(self._file)
        else:
            n = _next_doc_number
            _next_doc_number = n + 1
            return "Untitled-%d" % n

    def update_title(self):
        """Called when the file property changes, to update the
        title property appropriately."""
        self.title = self._file.name

    #
    #		Internal methods
    #

    def get_new_file_name(self):
        """Ask the user for a new file and associate the document with it."""
        old_file = self.file
        if old_file:
            old_name = old_file.name
        else:
            old_name = ""
        new_file = request_new_file(
            #'Save "%s" as:' % self.title,
            default_name = old_name,
            file_type = self.file_type or application().save_file_type)
        if new_file == None:
            raise Cancel()
        self.file = new_file

    def read(self):
        """Read the document from its currently associated file. The
        document must be associated with a file and not have any existing
        contents when this is called."""
        if self.binary:
            mode = "rb"
        else:
            mode = "rU"
        file = self.file.open(mode)
        try:
            self.read_contents(file)
        finally:
            file.close()
        self.needs_saving = 0

    def write(self):
        """Write the document to its currently associated file. The
        document must be associated with a file when this is called."""
        if self.binary:
            mode = "wb"
        else:
            mode = "w"
        file = self.file.open(mode, self._file_type)
        try:
            self.write_contents(file)
        finally:
            file.close()
        self.needs_saving = 0

    def setup_menus(self, m):
        if self._needs_saving or not self._file:
            m.save_cmd.enabled = 1
        if self._needs_saving and self._file:
            m.revert_cmd.enabled = 1
        m.save_as_cmd.enabled = 1

    def next_handler(self):
        return application()
