#
#		Python GUI - Application class - Generic
#

import os, sys, traceback
from Properties import Properties, overridable_property
from MessageHandlers import MessageHandler
from Exceptions import Cancel, Quit, UnimplementedMethod, UsageError, \
    ApplicationError
from StdMenus import basic_menus
from GMenus import MenuState
from Files import FileRef

_application = None

class Application(Properties, MessageHandler):
    """The user should create exactly one Application object,
    or subclass thereof. It implements the main event loop
    and other application-wide behaviour."""
    
    _windows = None				# List of all existing Windows
    _documents = None			# List of all existing Documents
    _menus = None					# Menus to appear in all Windows
    _clipboard = None
    _save_file_type = None

    windows = overridable_property('windows',
        """A list of all existing Windows.""")

    documents = overridable_property('documents',
        """A list of all existing documents.""")

    menus = overridable_property('menus',
        """A list of Menus that are to be available from all Windows.""")
    
    open_file_types = overridable_property('open_file_types',
        """List of FileTypes openable by the default Open... command.""")
    
    save_file_type = overridable_property('save_file_type',
        """Default FileType for Documents that do not specify their own.""")
    
    file_type = overridable_property('file_type',
        """Write only. Sets open_file_types and save_file_type.""")
    
    def __init__(self, title = None):
        global _application, application_name
        if _application is not None:
            raise UsageError("More than one Application instance created")
        if title:
            application_name = title
        self._open_file_types = []
        self._windows = []
        self._documents = []
        self._update_list = []
        self._idle_tasks = []
        _application = self
        #self.menus = self.std_menus
        self._menus = []
        self._quit_flag = False
    
    def destroy(self):
        global _application
        _application = None

    #
    #		Constants
    #

#	def get_std_menus(self):
#		"""Returns a list of Menus containing the standard
#		framework-defined menu commands in their standard 
#		positions."""
#		return basic_menus()
#	
#	std_menus = property(get_std_menus)

    #
    #		Properties
    #

    def get_windows(self):
        return self._windows
    
    def get_documents(self):
        return self._documents
    
    def get_menus(self):
        return self._menus

    def set_menus(self, menus):
        #if not isinstance(self, MenuList):
        #	menus = MenuList(menus)
        self._menus = menus

    def get_open_file_types(self):
        return self._open_file_types
    
    def set_open_file_types(self, x):
        self._open_file_types = x
    
    def get_save_file_type(self):
        return self._save_file_type
    
    def set_file_type(self, x):
        self._open_file_types = [x]
        self._save_file_type = x

    #
    #		Event loop
    #

    def run(self):
        """The main event loop. Runs until a Quit exception
        is raised or _quit_flag is set."""
        self.process_args(sys.argv[1:])
        if not self.menus:
            self.menus = basic_menus()
        while not self._quit_flag:
            try:
                self.event_loop()
            except (KeyboardInterrupt, Quit), e:
                return
            except SystemExit:
                raise
            except:
                self.report_error()
    
    def event_loop(self):
        """Loop reading and handling events until _quit_flag is set
        or an exception other than Cancel is raised."""
        while not self._quit_flag:
            try:
                self.handle_events()
            except Cancel:
                pass
    
    def event_loop_until(self, exit):
        """Loop reading and handling events until exit() returns
        true, _quit_flag is set or an exception other than Cancel
        is raised."""
        while not exit() and not self._quit_flag:
            try:
                self.handle_next_event()
            except Cancel:
                pass

    def handle_events(self):
        """Handle events until an exception occurs. Waits for at least one event;
        may handle more, at the discretion of the implementation."""
        self.handle_next_event()
    
    def handle_next_event(self):
        """Wait for the next event to arrive and handle it. Transparently handles 
        any internal events such as window updates, etc., and executes any idle tasks 
        that become due while waiting for an event."""
        raise UnimplementedMethod(self, "handle_next_event")
    
    #
    #		Menu commands
    #

    def setup_menus(self, m):
        m.new_cmd.enabled = 1
        m.open_cmd.enabled = 1
        m.quit_cmd.enabled = 1

    def new_cmd(self):
        "Handle the New menu command."
        doc = self.make_new_document()
        if not doc:
            raise UsageError(
                "Application.make_document(None) did not return a Document.")
        doc.new_contents()
        self.make_window(doc)

    def open_cmd(self):
        "Handle the Open... menu command."
        from FileDialogs import request_old_file
        fileref = request_old_file(file_types = self._open_file_types)
        if fileref:
            self.open_fileref(fileref)

    def quit_cmd(self):
        """Handle the Quit menu command."""
        while self._documents:
            self._documents[0].close_cmd()
        while self._windows:
            self._windows[-1].destroy()
        self._quit_flag = True
        self._quit()

    def _quit(self):
        #  Implementations can override this to use some other means
        #  of stimulating the main event loop to exit.
        raise Quit
    
    #
    #   Opening files
    #
    
    def process_args(self, args):
        """Process command line arguments. Called by run() when the application
        is starting up."""
        if not args:
            self.open_app()
        else:
            for arg in args:
                if os.path.exists(arg):
                    self.open_path(arg)
    
    def open_app(self):
        """Called by run() when the application is opened with no arguments."""
        pass
    
    def open_path(self, path):
        """Open document specified by a pathname. Called for each command line
        argument when the application is starting up."""
        self.open_fileref(FileRef(path = path))
    
    def open_fileref(self, fileref):
        """Open document specified by a FileRef."""
        doc = self.make_file_document(fileref)
        if not doc:
            raise ApplicationError("The file '%s' is not recognised by %s." % (
                fileref.name, application_name))
        doc.set_file(fileref)
        try:
            doc.read()
        except EnvironmentError, e:
            raise ApplicationError("Unable to open '%s'." % fileref.name, e)
        self.make_window(doc)

    #
    #   Message dispatching
    #
    
    def dispatch(self, message, *args):
        target_window = self._find_target_window()
        if target_window:
            target_window.dispatch(message, *args)
        else:
            self.handle(message, *args)

    def _find_target_window(self):
        """Return the window to which messages should be
        dispatched, or None."""
        raise NotImplementedError
    
    #
    #		Abstract
    #

    def make_new_document(self):
        """Create a new Document object of the appropriate 
        class in response to a New command."""
        return self.make_document(None)

    def make_file_document(self, fileref):
        """Create a new Document object of the appropriate 
        class for the given FileRef."""
        return self.make_document(fileref)
    
    def make_document(self, fileref):
        """Should create a new Document object of the appropriate 
        class for the given FileRef, or if FileRef is None, a new
        empty Document of the appropriate class for the New command."""
        return None

    def make_window(self, document):
        """Should create a Window set up appropriately for viewing
        the given Document."""
        raise UnimplementedMethod(self, 'make_window')

    #
    #		Clipboard
    #

    def query_clipboard(self):
        "Tests whether the clipboard contains any data."
        return not not self._clipboard
    
    def get_clipboard(self):
        return self._clipboard

    def set_clipboard(self, x):
        self._clipboard = x
    
    #
    #   Window list management
    #
    
    def _add_window(self, window):
        if window not in self._windows:
            self._windows.append(window)
    
    def _remove_window(self, window):
        if window in self._windows:
            self._windows.remove(window)

    #
    #   Document list management
    #
    
    def _add_document(self, doc):
        if doc not in self._documents:
            self._documents.append(doc)
    
    def _remove_document(self, doc):
        if doc in self._documents:
            self._documents.remove(doc)
    
    #
    #   Exception reporting
    #
    
    def report_error(self):
        """Display an appropriate error message for the most recent
        exception caught."""
        try:
            raise
        except Cancel:
            pass
        except ApplicationError, e:
            from AlertFunctions import stop_alert
            stop_alert(str(e))
        except:
            self.report_exception()

    def report_exception(self):
        """Display an alert box describing the most recent exception, and
        giving the options Continue, Traceback or Abort. Traceback displays
        a traceback and continues; Abort re-raises the exception so that
        the current run() call is terminated."""
        traceback.print_exc() # Show traceback first in case we fail below
        try:
            exc_type, exc_val, exc_tb = sys.exc_info()
            exc_desc = "%s: %s" % (exc_type.__name__, exc_val)
            from AlertFunctions import alert3
            message = "Sorry, %s encountered a problem and was unable to" \
                " complete the operation." % application_name
            result = alert3('stop', "%s\n\n%s" % (message, exc_desc),
                "Continue", "Abort", "Traceback",
                default = 1, cancel = None, width = 450, lines = 5)
            if result == 1: # Continue
                return
            elif result == -1: # Traceback
                self.display_traceback(exc_desc, exc_tb)
                return
            else: # Abort
                raise SystemExit
        except (KeyboardInterrupt, SystemExit):
            raise
        except:
            print >>sys.stderr, "---------- Exception while reporting exception ----------"
            traceback.print_exc()
            print >>sys.stderr, "------------------ Original exception -------------------"
            raise exc_type, exc_val, exc_tb
        # Never get here
    
    def display_traceback(self, exc_desc, exc_tb):
        """Display an exception description and traceback. Currently the
        info is written to standard error; should be replaced sometime with
        a window."""
        import traceback
        sys.stderr.write("\nTraceback (most recent call last):\n")
        traceback.print_tb(exc_tb)
        sys.stderr.write("%s\n\n" % exc_desc)

    #
    #   Other
    #
    
    def zero_windows_allowed(self):
        """Platforms should implement this to return false if there
        must be at least one window open at all times. Returning false 
        here forces the Quit command to be used instead of Close when 
        there is only one window open."""
        # TODO: Move this somewhere more global.
        raise UnimplementedMethod(self, 'zero_windows_allowed')
    
    def _perform_menu_setup(self, menus = None):
        """Given a list of Menu objects, perform menu setup processing
        and update associated platform menus ready for popping up or
        pulling down."""
        if not menus:
            menus = self._effective_menus()
        menu_state = MenuState(menus)
        menu_state.reset()
        self._dispatch_menu_setup(menu_state)
        for menu in menus:
            menu._update_platform_menu()
    
    def _dispatch_menu_setup(self, menu_state):
        self.dispatch('_setup_menus', menu_state)

    def _effective_menus(self):
        """Return a list of the currently active menus, including both
        application-wide and window-specific menus, in an appropriate order
        according to platform conventions."""
        menus = self._menus
        window = self._find_target_window()
        if window:
            menus = menus + window._menus
        regular_menus = []
        special_menus = []
        for menu in menus:
            if menu.special:
                special_menus.insert(0, menu)
            else:
                regular_menus.append(menu)
        return regular_menus + special_menus

#------------------------------------------------------------------------------

_main_file_name = os.path.basename(sys.argv[0])
application_name = os.path.splitext(_main_file_name)[0]

#------------------------------------------------------------------------------

def application():
    """Returns the global Application object. Creates a default one if needed."""
    global _application
    if not _application:
        from Applications import Application
        _application = Application()
    return _application

def run():
    """Runs the application, retaining control until the application is quit."""
    application().run()

