#! /usr/bin/env python
#@+leo-ver=5-thin
#@+node:ekr.20070227091955.1: * @file leoBridge.py
#@@first
'''A module to allow full access to Leo commanders from outside Leo.'''
#@@language python
#@@tabwidth -4
#@+<< about the leoBridge module >>
#@+node:ekr.20070227091955.2: ** << about the leoBridge module >>
#@+at
#@@language rest
#@@wrap
# 
# A **host** program is a Python program separate from Leo. Host programs may
# be created by Leo, but at the time they are run host programs must not be
# part of Leo in any way. So if they are run from Leo, they must be run in a
# separate process.
# 
# The leoBridge module gives host programs access to all aspects of Leo,
# including all of Leo's source code, the contents of any .leo file, all
# configuration settings in .leo files, etc.
# 
# Host programs will use the leoBridge module like this::
# 
#     import leo.core.leoBridge as leoBridge
#     bridge = leoBridge.controller(gui='nullGui',verbose=False)
#     if bridge.isOpen():
#         g = bridge.globals()
#         c = bridge.openLeoFile(path)
# 
# Notes:
# 
# - The leoBridge module imports no modules at the top level.
# 
# - leoBridge.controller creates a singleton *bridge controller* that grants
#   access to Leo's objects, including fully initialized g and c objects. In
#   particular, the g.app and g.app.gui vars are fully initialized.
# 
# - By default, leoBridge.controller creates a null gui so that no Leo
#   windows appear on the screen.
# 
# - As shown above, the host program should gain access to Leo's leoGlobals
#   module using bridge.globals(). The host program should not import
#   leo.core.leoGlobals as leoGlobals directly.
# 
# - bridge.openLeoFile(path) returns a completely standard Leo commander.
#   Host programs can use these commanders as described in Leo's scripting
#   chapter.
#@-<< about the leoBridge module >>
gBridgeController = None # The singleton bridge controller.
# This module must import *no* modules at the outer level!
#@+others
#@+node:ekr.20070227092442: ** controller
def controller(
    gui='nullGui',
    loadPlugins=True,
    readSettings=True,
    silent=False,
    tracePlugins=False,
    verbose=False
):
    '''Create an singleton instance of a bridge controller.'''
    global gBridgeController
    if not gBridgeController:
        gBridgeController = BridgeController(gui, loadPlugins, readSettings, silent, tracePlugins, verbose)
    return gBridgeController
#@+node:ekr.20070227092442.2: ** class BridgeController
class BridgeController(object):
    '''Creates a way for host programs to access Leo.'''
    #@+others
    #@+node:ekr.20070227092442.3: *3* bridge.ctor
    def __init__(self, guiName, loadPlugins, readSettings, silent, tracePlugins, verbose):
        '''Ctor for the BridgeController class.'''
        self.g = None
        self.gui = None
        self.guiName = guiName or 'nullGui'
        self.loadPlugins = loadPlugins
        self.readSettings = readSettings
        self.silentMode = silent
        self.tracePlugins = tracePlugins
        self.verbose = verbose
        self.mainLoop = False # True only if a non-null-gui mainloop is active.
        self.initLeo()
    #@+node:ekr.20070227092442.4: *3* bridge.globals
    def globals(self):
        '''Return a fully initialized leoGlobals module.'''
        return self.isOpen() and self.g
    #@+node:ekr.20070227093530: *3* bridge.initLeo & helpers
    def initLeo(self):
        '''
        Init the Leo app to which this class gives access.
        This code is based on leo.run().
        '''
        trace = False
        if not self.isValidPython(): return
        #@+<< initLeo imports >>
        #@+node:ekr.20070227093629.1: *4* << initLeo imports >> initLeo (leoBridge)
        # Import leoGlobals, but do NOT set g.
        try:
            import leo.core.leoGlobals as leoGlobals
        except ImportError:
            print("Error importing leoGlobals.py")
        # Create the application object.
        try:
            leoGlobals.in_bridge = True
                # Tell leoApp.createDefaultGui not to create a gui.
                # This module will create the gui later.
            import leo.core.leoApp as leoApp
            leoGlobals.app = leoApp.LeoApp()
        except ImportError:
            print("Error importing leoApp.py")
        # NOW we can set g.
        self.g = g = leoGlobals
        assert(g.app)
        g.app.leoID = None
        g.app.trace_plugins = self.tracePlugins
        g.app.silentMode = self.silentMode
        if trace:
            import sys
            g.trace(sys.argv)
            g.trace('g.app.silentMode', g.app.silentMode)
        # Create the g.app.pluginsController here.
        import leo.core.leoPlugins as leoPlugins
        leoPlugins.init() # Necessary. Sets g.app.pluginsController.
        try:
            import leo.core.leoNodes as leoNodes
        except ImportError:
            print("Error importing leoNodes.py")
            import traceback; traceback.print_exc()
        try:
            import leo.core.leoConfig as leoConfig
        except ImportError:
            print("Error importing leoConfig.py")
            import traceback; traceback.print_exc()
        # Set leoGlobals.g here, rather than in leoGlobals.
        leoGlobals.g = leoGlobals
        #@-<< initLeo imports >>
        g.app.recentFilesManager = leoApp.RecentFilesManager()
        g.app.loadManager = lm = leoApp.LoadManager()
        g.app.loadManager.computeStandardDirectories()
        if not self.getLeoID(): return
        g.app.inBridge = True # Added 2007/10/21: support for g.getScript.
        g.app.nodeIndices = leoNodes.NodeIndices(g.app.leoID)
        g.app.config = leoConfig.GlobalConfigManager()
        if self.readSettings:
            lm.readGlobalSettingsFiles()
                # reads only standard settings files, using a null gui.
                # uses lm.files[0] to compute the local directory
                # that might contain myLeoSettings.leo.
        else:
            # Bug fix: 2012/11/26: create default global settings dicts.
            settings_d, shortcuts_d = lm.createDefaultSettingsDicts()
            lm.globalSettingsDict = settings_d
            lm.globalShortcutsDict = shortcuts_d
        self.createGui() # Create the gui *before* loading plugins.
        if self.verbose: self.reportDirectories()
        self.adjustSysPath()
        # Kill all event handling if plugins not loaded.
        if not self.loadPlugins:

            def dummyDoHook(tag, *args, **keys):
                pass

            g.doHook = dummyDoHook
        g.doHook("start1") # Load plugins.
        g.app.computeSignon()
        g.app.initing = False
        g.doHook("start2", c=None, p=None, v=None, fileName=None)
    #@+node:ekr.20070302061713: *4* bridge.adjustSysPath
    def adjustSysPath(self):
        '''Adjust sys.path to enable imports as usual with Leo.'''
        import sys
        g = self.g
        # g.trace('loadDir',g.app.loadDir)
        leoDirs = ('config', 'doc', 'extensions', 'modes', 'plugins', 'core', 'test') # 2008/7/30
        for theDir in leoDirs:
            path = g.os_path_finalize_join(g.app.loadDir, '..', theDir)
            if path not in sys.path:
                sys.path.append(path)
        # Attempt a fix for bug #258: leoBridge does not work with @auto-md subtrees.
        # https://github.com/leo-editor/leo-editor/issues/258
        for theDir in ('importers', 'writers'):
            path = g.os_path_finalize_join(g.app.loadDir, '..', 'plugins', theDir)
            if path not in sys.path:
                sys.path.append(path)
    #@+node:ekr.20070227095743: *4* bridge.createGui
    def createGui(self):
        g = self.g
        if self.guiName == 'nullGui':
            g.app.gui = g.app.nullGui
            g.app.log = g.app.gui.log = log = g.app.nullLog
            log.isNull = False
            log.enabled = True # Allow prints from NullLog.
            log.logInited = True # Bug fix: 2012/10/17.
        elif self.guiName == 'qt':
            import leo.plugins.qt_gui as qt_gui
            g.app.gui = qt_gui.LeoQtGui()
            print('Qt gui created')
        else:
            assert False, 'leoBridge.py: unsupported gui: %s' % self.guiName
    #@+node:ekr.20070227093629.4: *4* bridge.isValidPython
    def isValidPython(self):
        import sys
        if sys.platform == 'cli':
            return True
        message = """\
    Leo requires Python 2.2.1 or higher.
    You may download Python from http://python.org/download/
    """
        try:
            # This will fail if True/False are not defined.
            import leo.core.leoGlobals as g
            # print('leoBridge:isValidPython:g',g)
            # Set leoGlobals.g here, rather than in leoGlobals.py.
            leoGlobals = g # Don't set g.g, it would pollute the autocompleter.
            leoGlobals.g = g
        except ImportError:
            print("isValidPython: can not import leo.core.leoGlobals as leoGlobals")
            return 0
        except Exception:
            print("isValidPytyhon: unexpected exception: import leo.core.leoGlobals as leoGlobals.py as g")
            import traceback; traceback.print_exc()
            return 0
        try:
            version = '.'.join([str(sys.version_info[i]) for i in (0, 1, 2)])
            ok = g.CheckVersion(version, '2.2.1')
            if not ok:
                print(message)
                g.app.gui.runAskOkDialog(None, "Python version error", message=message, text="Exit")
            return ok
        except Exception:
            print("isValidPython: unexpected exception: g.CheckVersion")
            import traceback; traceback.print_exc()
            return 0
    #@+node:ekr.20070227094232: *4* bridge.getLeoID
    def getLeoID(self):
        import os
        import sys
        g = self.g; tag = ".leoID.txt"
        homeDir = g.app.homeLeoDir
        globalConfigDir = g.app.globalConfigDir
        loadDir = g.app.loadDir
        verbose = False and not g.app.unitTesting
        #@+<< try to get leoID from sys.leoID >>
        #@+node:ekr.20070227094232.1: *5* << try to get leoID from sys.leoID>>
        # This would be set by in Python's sitecustomize.py file.
        # Use hasattr & getattr to suppress pylint warning.
        # We also have to use a "non-constant" attribute to suppress another warning!
        nonConstantAttr = "leoID"
        if hasattr(sys, nonConstantAttr):
            g.app.leoID = getattr(sys, nonConstantAttr)
            if verbose and not g.app.silentMode:
                g.red("leoID=", g.app.leoID, spaces=False)
        #@-<< try to get leoID from sys.leoID >>
        if not g.app.leoID:
            #@+<< try to get leoID from "leoID.txt" >>
            #@+node:ekr.20070227094232.2: *5* << try to get leoID from "leoID.txt" >>
            for theDir in (homeDir, globalConfigDir, loadDir):
                # N.B. We would use the _working_ directory if theDir is None!
                if theDir:
                    try:
                        fn = g.os_path_join(theDir, tag)
                        f = open(fn, 'r')
                        s = f.readline()
                        f.close()
                        if s and len(s) > 0:
                            g.app.leoID = s.strip()
                            if verbose and not g.app.silentMode:
                                g.red('leoID=', g.app.leoID, ' (in ', theDir, ')', spaces=False)
                            break
                        elif verbose:
                            g.red('empty ', tag, ' (in ', theDir, ')', spaces=False)
                    except IOError:
                        g.app.leoID = None
                    except Exception:
                        g.app.leoID = None
                        g.error('unexpected exception in app.setLeoID')
                        g.es_exception()
            #@-<< try to get leoID from "leoID.txt" >>
        if not g.app.leoID:
            #@+<< try to get leoID from os.getenv('USER') >>
            #@+node:ekr.20070227094232.3: *5* << try to get leoID from os.getenv('USER') >>
            try:
                theId = os.getenv('USER')
                if theId:
                    if verbose: g.red("using os.getenv('USER'):", repr(theId))
                    g.app.leoID = theId
            except Exception:
                pass
            #@-<< try to get leoID from os.getenv('USER') >>
        return g.app.leoID
    #@+node:ekr.20070227093629.9: *4* bridge.reportDirectories
    def reportDirectories(self):
        if not self.silentMode:
            g = self.g
            for kind, theDir in (
                ("global config", g.app.globalConfigDir),
                ("home", g.app.homeDir),
            ):
                g.blue('', kind, 'directory', '', ':', theDir)
    #@+node:ekr.20070227093918: *3* bridge.isOpen
    def isOpen(self):
        '''Return True if the bridge is open.'''
        g = self.g
        return bool(g and g.app and g.app.gui)
    #@+node:ekr.20070227092442.5: *3* bridge.openLeoFile & helpers
    def openLeoFile(self, fileName):
        '''Open a .leo file, or create a new Leo frame if no fileName is given.'''
        g = self.g
        g.app.silentMode = self.silentMode
        useLog = False
        if self.isOpen():
            fileName = self.completeFileName(fileName)
            c = self.createFrame(fileName)
            g.app.nodeIndices.compute_last_index(c)
                # New in Leo 5.1. An alternate fix for bug #130.
                # When using a bridge Leo might open a file, modify it,
                # close it, reopen it and change it all within one second.
                # In that case, this code must properly compute the next
                # available gnx by scanning the entire outline.
            if useLog:
                g.app.gui.log = log = c.frame.log
                log.isNull = False
                log.enabled = True
            return c
        else:
            return None
    #@+node:ekr.20070227093629.5: *4* bridge.completeFileName
    def completeFileName(self, fileName):
        g = self.g
        if not (fileName and fileName.strip()): return ''
        import os
        fileName = g.os_path_finalize_join(os.getcwd(), fileName)
        head, ext = g.os_path_splitext(fileName)
        if not ext: fileName = fileName + ".leo"
        return fileName
    #@+node:ekr.20070227093629.6: *4* bridge.createFrame
    def createFrame(self, fileName):
        '''Create a commander and frame for the given file.
        Create a new frame if the fileName is empty or non-exisent.'''
        trace = False
        g = self.g
        if fileName.strip():
            if g.os_path_exists(fileName):
                if trace:
                    import time; t1 = time.time()
                # This takes a long time due to imports in c.__init__
                c = g.openWithFileName(fileName)
                if trace:
                    t2 = time.time()
                    g.trace('g.openWithFileName: %0.2fsec' % (t2 - t1))
                if c: return c
            elif not self.silentMode:
                print('file not found: %s. creating new window' % (fileName))
        # Create a new frame. Unlike leo.run, this is not a startup window.
        c = g.app.newCommander(fileName)
        frame = c.frame
        frame.createFirstTreeNode() # 2013/09/27: bug fix.
        assert c.rootPosition()
        frame.setInitialWindowGeometry()
        frame.resizePanesToRatio(frame.ratio, frame.secondary_ratio)
        # Call the 'new' hook for compatibility with plugins.
        # 2011/11/07: Do this only if plugins have been loaded.
        g.doHook("new", old_c=None, c=c, new_c=c)
        return c
    #@-others
#@-others
#@-leo
