#
#		Python GUI - Documents - Generic
#

import os, tempfile
from GUI import export
from GUI.Alerts import confirm, confirm_or_cancel
from GUI.Properties import overridable_property
from GUI import Model
from GUI import MessageHandler
from GUI.Files import FileRef, DirRef
from GUI.FileDialogs import request_new_file
from GUI import application
from GUI.Exceptions import Cancel, UnimplementedMethod, ApplicationError
from GUI.Printing import PageSetup, present_page_setup_dialog

_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."""
    
    #  The following attribute prevents a Document that is the parent
    #  of a Model from being pickled along with that Model.
    pickle_as_parent_model = False
    
    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.")
        
    page_setup = overridable_property('page_setup',
        "The PageSetup to be used for printing this document.")
    
    binary = True       # True if files are to be opened in binary mode

    _file_type = None   # Type of file to create when saving
    _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
    _page_setup = None  # Document-specific PageSetup instance

    #
    #		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):
        if self._needs_saving <> x:
            self._needs_saving = x
            for window in self._windows:
                window._document_needs_saving(x)

    def get_file(self):
        return self._file

    def set_file(self, x):
        self._file = x
        if x is not None:
            application()._last_directory = x.dir
        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
    
    def get_page_setup(self):
        ps = self._page_setup
        if not ps:
            ps = PageSetup()
            self._page_setup = ps
        return ps
    
    def set_page_setup(self, ps):
        self._page_setup = ps

    #
    #		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 page_setup_cmd(self):
        if present_page_setup_dialog(self.page_setup):
            self.changed()

    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."""
        file = self._file
        if file:
            self.title = file.name
    
    def get_default_save_directory(self):
        """Called when the user is about to be asked for a location in which
        to save a document that has not been saved before, to find a default
        directory for request_new_file(). Should return a DirRef or FileRef,
        or None if there is no particular preferred location."""
        return None
    
    def get_default_save_filename(self):
        """Called when the user is about to be asked for a location in which
        to save a document that has not been saved before, to find a default
        file name for request_new_file(). Should return a suggested file name,
        or an empty string to require the user to enter a file name."""
        return ""

    #
    #		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
            old_dir = old_file.dir
        else:
            old_name = self.get_default_save_filename()
            old_dir = self.get_default_save_directory()
        #print "Document.get_new_file_name: old_dir =", old_dir, "old_name =", old_name ###
        new_file = request_new_file(
            #'Save "%s" as:' % self.title,
            default_dir = old_dir,
            default_name = old_name,
            file_type = self.file_type or application().save_file_type)
        if new_file is 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.
        The document is initially written to a temporary file which
        is then renamed, so if writing fails part way through, the
        original file is undisturbed."""
        if self.binary:
            mode = "wb"
        else:
            mode = "w"
        dir_path = self.file.dir.path
        fd, temp_path = tempfile.mkstemp(dir = dir_path, text = not self.binary)
        file = os.fdopen(fd, mode)
        try:
            try:
                self.write_contents(file)
            finally:
                file.close()
        except EnvironmentError:
            os.unlink(fd)
            raise
        path = self.file.path
        try:
            os.unlink(path)
        except EnvironmentError:
            pass
        os.rename(temp_path, path)
        self.needs_saving = 0

    def setup_menus(self, m):
        #print "Document.setup_menus" ###
        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
        m.page_setup_cmd.enabled = 1

    def next_handler(self):
        return application()

export(Document)
