#@+leo-ver=5-thin
#@+node:TL.20090225102340.32: * @file nodeActions.py
#@+<< docstring >>
#@+node:TL.20080507213950.3: ** << docstring >>
""" Allows the definition of double-click actions.

When the user double-clicks a node this plugin checks for a match of the clicked
node's headline text with a list of patterns. If a match occurs, the plugin
executes the associated script.

**nodeAction** nodes may be located anywhere in the outline. Such nodes should
contain one or more **pattern nodes** as children. The headline of each pattern
node contains the pattern; the body text contains the script to be executed when
the pattern matches the double-clicked node.

For example, the "nodeActions" node containing a "launch URL" pattern node
and a "pre-process python code" node could be placed under an "@settings"
node::

   @settings
   |
   +- nodeActions
      |
      +- http:\\*
      |
      +- @file *.py

**Configuration**

The nodeActions plugin supports the following global configurations using
Leo's support for setting global variables within an @settings node's
sub-nodes in the leoSettings.leo, myLeoSettings.leo, and the project Leo
file:

@bool nodeActions_save_atFile_nodes = False

  :True:
     Double-click on an @file type node will save the file to disk
     before executing the script.

  :False:
     Double-click on an @file type node will **not** save the file to disk
     before executing the script. (default)

@int nodeActions_message_level = 1

  Specifies the type of messages to be sent to the log pane.  Specifying a
  higher message level will display that level and all lower levels.
  The following integer values are supported::

    0 no messages
    1 Plugin triggered and the patterns that were matched (default)
    2 Double-click event passed or not to next plugin
    3 Patterns that did not match
    4 Code debugging messages

**Patterns**

Pattern matching is performed using python's support for Unix
shell-style patterns unless overwritten by the "X" pattern directive.
The following pattern elements are supported::

    *           matches everything
    ?           matches any single character
    [<seq>]     matches any character in <seq>
    [!<seq>]    matches any character **not** in <seq>

Unix shell-style pattern matching is case insensitive and always starts from
the beginning of the headline.  For example:

     ======= =========== ==============
     Pattern   Matches   Does not match
     ======= =========== ==============
     \*.py   Abc_Test.py
     .py     .py - Test  Abc_Test.py
     test*   Test_Abc.py Abc_Test.py
     ======= =========== ==============

To enable a script to run on any type of @file node (@thin, @shadow, ...),
the pattern can start with "@files" to match on any
external file type.  For example, the pattern "@files \*.py" will
match a node with the headline "@file abcd.py".

The headline of the double-clicked node is matched against the patterns
starting from the first sub-node under the "nodeActions" node to the last
sub-node.

Only the script associated with the first matching pattern is
invoked unless overwritten by the "V" pattern directive.

Using the "V" pattern directive allows a broad pattern such
as "@files \*.py" to be invoked, and then, by placing a more restrictive
pattern above it, such as "@files \*_test.py", a different script can be
executed for those files requiring pre-processing::

  +- nodeActions
     |
     +- @files *_test.py
     |
     +- @files *.py

**Note**: To prevent Leo from trying to save patterns that begin with a derived
file directive (@file, @auto, ...) to disk, such as "@file \*.py", place the
"@ignore" directive in the body of the "nodeActions" node.

Pattern nodes can be placed at any level under the "nodeActions" node.
Only nodes with no child nodes are considered pattern nodes.
This allows patterns that are to be used in multiple Leo files to be read
from a file.  For example, the following structure reads the pattern
definition from the "C:\\Leo\\nodeActions_Patterns.txt" file::

    +- nodeActions
    |
    +- @files C:\\Leo\\nodeActions_Patterns.txt
        |
        +- http:\\*
        |
        +- @file *.py

**Pattern directives**

The following pattern specific directives can be appended to the end of a
pattern (do not include the ':'):

:[X]:
  Use python's regular expression type patterns instead of the Unix
  shell-style pattern syntax.

  For example, the following patterns will match the same headline string::

     Unix shell-style pattern:
        @files *.py

     Regular Expression pattern:
        ^@files .*\.py$ [X]

:[V]:
  Matching the pattern will not block the double-click event from
  being passed to the remaining patterns.
  The "V" represents a down arrow that symbolizes the passing of the event
  to the next pattern below it.

  For example, adding the "[V]" directive to the "@files \*_test.py" in
  the Patterns section above, changes its script from being 'an
  alternate to' to being 'a pre-processor for' the "@files \*.py" script::

     +- nodeActions
        |
        +- @files *_test.py [V]
        |
        +- @files *.py

:[>]:
  Matching the pattern will not block the double-click event from being
  passed to other plugins.
  The ">" represents a right arrow that
  symbolizes the passing of the event to the next plugin.

  If the headline matched more than one headline,
  the double-click event will be passed to the next plugin if the
  directive is associated with any of the matched patterns.

The directive(s) for a pattern must be contained within a single set of
brackets, separated from the pattern by a space, with or without a comma
separator.  For example, the following specifies all three directives::

    ^@files .*\.py$ [X,V>]

**Scripts**

The script for a pattern is located in the body of the pattern's node.
The following global variables are available to the script::

    c
    g
    pClicked - node position of the double-clicked node
    pScript - node position of the invoked script

**Examples**

Double-clicking on a node with a "http:\\\\www.google.com" headline
will invoke the script associated with the
"http:\\\\\*" pattern.  The following script in the body of the pattern's
node displays the URL in a browser::

     import webbrowser
     hClicked = pClicked.h     #Clicked node's Headline text
     webbrowser.open(hClicked) #Invoke browser

The following script can be placed in the body of a pattern's node to
execute a command in the first line of the body of a double-clicked node::

     g.os.system('"Start /b ' + pClicked.bodyString() + '"')

"""

#@-<< docstring >>

# Written by TL.
# Derived from the fileActions plugin.
# Distributed under the same licence as Leo.

#@@language python
#@@tabwidth -4

__version__ = "0.4"
#@+<< version history >>
#@+node:TL.20080507213950.4: ** << version history >>
#@@nocolor
#@+at
# 0.3 : 02-Apr-10 : TL : Support search all sub-nodes for pattern match
# 0.2 : 02-Mar-09 : TL : Support for 'X', 'V', and  '>' directives added
# 0.1 : 27-Feb-09 : TL : Initial code (modified from FileActions plugin)
#@-<< version history >>
#@+<< imports >>
#@+node:ekr.20040915110738.1: ** << imports >>
import leo.core.leoGlobals as g

import fnmatch
import os
import re
import sys
import tempfile
#@-<< imports >>

atFileTypes = [
    "@file", "@thin", "@file-thin",   "@thinfile",
    "@asis",   "@file-asis","@silentfile",
    "@nosent","@file-nosent", "@nosentinelsfile",
    "@shadow", "@edit",
]

#@+others
#@+node:TL.20080507213950.8: ** onIconDoubleClickNA
def onIconDoubleClickNA(tag, keywords):

    c = keywords.get("c")
    p = keywords.get("p")

    if not c or not p:
        return None

    if doNodeAction(p,c):
        return True #Action was taken - Stop other double-click handlers from running
    else:
        return None #No action taken - Let other double-click handlers run


#@+node:TL.20080507213950.7: ** init
def init():

	 g.es("nodeActions: Init", color='blue')
	 ok = not g.app.unitTesting # Dangerous for unit testing.
	 if ok:
		  g.registerHandler("icondclick1", onIconDoubleClickNA)
		  g.plugin_signon(__name__)
	 return ok
#@+node:TL.20080507213950.9: ** doNodeAction
def doNodeAction(pClicked, c):

   hClicked = pClicked.h.strip()

   #Display messages based on 'messageLevel'.  Valid values:
   #   0 = log no messages
   #   1 = log that the plugin was triggered and each matched patterns
   #   2 = log 1 & 'event passed'
   #   3 = log 1,2 & 'no match to pattern'
   #   4 = log 1,2,3, & any code debugging messages,
   #           matched pattern's 'directives', and '@file saved' settings
   messageLevel = c.config.getInt('nodeActions_message_level')

   if messageLevel >= 1:
      g.es( "nodeActions: triggered" )

   #Save @file type nodes before running script if enabled
   saveAtFile = c.config.getBool('nodeActions_save_atFile_nodes')
   if messageLevel >= 4:
      g.es( "nA: Global nodeActions_save_atFile_nodes=", saveAtFile, \
                                                         color='blue')
   #Find the "nodeActions" node
   pNA = g.findNodeAnywhere(c,"nodeActions")
   if not pNA:
      pNA = g.findNodeAnywhere(c,"NodeActions")

   if pNA:
      #Found "nodeActions" node
      foundPattern = False
      passEventExternal = False  #No pass to next plugin after pattern matched
      #Check entire subtree under the "nodeActions" node for pattern
      for pScript in pNA.subtree():

         #Nodes with subnodes are not tested for a match
         if pScript.hasChildren():
            continue

         #Don't trigger on double click of a nodeActions' pattern node
         if pClicked == pScript:
            continue

         pattern = pScript.h.strip()   #Pattern node's header
         if messageLevel >= 4:
            g.es( "nA: Checking pattern '" + pattern, color='blue' )

         #if directives exist, parse them and set directive flags for later use
         directiveExists = re.search( " \[[V>X],?[V>X]?,?[V>X]?]$", pattern )
         if directiveExists:
            directives = directiveExists.group(0)
         else:
            directives = "[]"
         #What directives exist?
         useRegEx = re.search("X", directives) != None
         passEventInternal = re.search("V", directives) != None
         if not passEventExternal: #don't disable once enabled.
            passEventExternal = re.search(">", directives) != None
         #Remove the directives from the end of the pattern (if they exist)
         pattern = re.sub( " \[.*]$", "", pattern, 1)
         if messageLevel >= 4:
            g.es( "nA:   Pattern='" + pattern + "' " \
                                 + "(after directives removed)", color='blue' )

         #Keep copy of pattern without directives for message log
         patternOriginal = pattern

         #if pattern begins with "@files" and clicked node is an @file type
         #node then replace "@files" in pattern with clicked node's @file type
         patternBeginsWithAtFiles = re.search( "^@files ", pattern )
         clickedAtFileTypeNode = False #assume @file type node not clicked
         if patternBeginsWithAtFiles:
            #Check if first word in clicked header is in list of @file types
            firstWordInClickedHeader = hClicked.split()[0]
            if firstWordInClickedHeader in atFileTypes:
               clickedAtFileTypeNode = True #Tell "write @file type nodes" code
               #Replace "@files" in pattern with clicked node's @file type
               pattern = re.sub( "^@files", firstWordInClickedHeader, pattern)
               if messageLevel >= 4:
                  g.es( "nA:   Pattern='" + pattern + "' " \
                               + "(after @files substitution)", color='blue' )

         #Check for pattern match to clicked node's header
         if useRegEx:
            match = re.search(pattern, hClicked)
         else:
            match = fnmatch.fnmatchcase(hClicked, pattern)
         if match:
            if messageLevel >= 1:
               g.es( "nA: Matched pattern '" + patternOriginal + "'"																					, color='blue' )
            if messageLevel >= 4:
               g.es( "nA:   Directives: X=",useRegEx, "V=",passEventInternal, \
                                        ">=",passEventExternal, color='blue')
            #if @file type node, save node to disk (if configured)
            if clickedAtFileTypeNode:
               if saveAtFile:
                  #Problem - No way found to just save clicked node, saving all
                  c.fileCommands.writeAtFileNodes()
                  c.requestRedrawFlag = True
                  c.redraw()
                  if messageLevel >= 3:
                     g.es( "nA:   Saved '" + hClicked + "'", color='blue' )
            #Run the script
            applyNodeAction(pScript, pClicked, c)
            #Indicate that at least one pattern was matched
            foundPattern = True
            #Don't trigger more patterns unless enabled in patterns' headline
            if passEventInternal == False:
               break
         else:
            if messageLevel >= 3:
               g.es("nA: Did not match '" + patternOriginal + "'", color='blue')

      #Finished checking headline against patterns
      if not foundPattern:
         #no match to any pattern, always pass event to next plugin
         if messageLevel >= 1:
            g.es("nA: No patterns matched to """ + hClicked + '"', color='blue')
         return False #TL - Inform onIconDoubleClick that no action was taken
      elif passEventExternal == True:
         #last matched pattern has directive to pass event to next plugin
         if messageLevel >= 2:
            g.es("nA: Event passed to next plugin", color='blue')
         return False #TL - Inform onIconDoubleClick to pass double-click event
      else:
         #last matched pattern did not have directive to pass event to plugin
         if messageLevel >= 2:
            g.es("nA: Event not passed to next plugin", color='blue')
         return True #TL - Inform onIconDoubleClick to not pass double-click
   else:
      #nodeActions plugin enabled without a 'nodeActions' node
      if messageLevel >= 4:
         g.es("nA: The ""nodeActions"" node does not exist", color='blue')
      return False #TL - Inform onIconDoubleClick that no action was taken
#@+node:TL.20080507213950.10: ** applyNodeAction
def applyNodeAction(pScript, pClicked, c):

   script = g.getScript(c, pScript)
   if script:
       working_directory = os.getcwd()
       file_directory = c.frame.openDirectory
       os.chdir(file_directory)
       script += '\n'
       #Redirect output
       if c.config.redirect_execute_script_output_to_log_pane:
           g.redirectStdout() # Redirect stdout
           g.redirectStderr() # Redirect stderr
       try:
           namespace = {
              'c':c, 'g':g,
              'pClicked': pClicked,
              'pScript' : pScript,
              'shellScriptInWindowNA': shellScriptInWindowNA }
           # exec script in namespace
           exec(script,namespace)
           #Unredirect output
           if c.config.redirect_execute_script_output_to_log_pane:
               g.restoreStderr()
               g.restoreStdout()
       except:
           #Unredirect output
           if c.config.redirect_execute_script_output_to_log_pane:
               g.restoreStderr()
               g.restoreStdout()
           g.es("exception in NodeAction plugin")
           g.es_exception(full=False,c=c)

       os.chdir(working_directory)
#@+node:TL.20080507213950.13: ** shellScriptInWindowNA
def shellScriptInWindowNA(c,script):

    if sys.platform == 'darwin':
        #@+<< write script to temporary MacOS file >>
        #@+node:TL.20080507213950.14: *3* << write script to temporary MacOS file >>
        handle, path = tempfile.mkstemp(text=True)
        directory = c.frame.openDirectory
        script = ("cd %s\n" % directory) + script + '\n' + ("rm -f %s\n" % path)
        os.write(handle, script)
        os.close(handle)
        os.chmod(path,0x700)
        #@-<< write script to temporary MacOS file >>
        os.system("open -a /Applications/Utilities/Terminal.app " + path)

    elif sys.platform == 'win32':
        g.es("shellScriptInWindow not ready for Windows",color='red')

    else:
        #@+<< write script to temporary Unix file >>
        #@+node:TL.20080507213950.15: *3* << write script to temporary Unix file >>
        handle, path = tempfile.mkstemp(text=True)
        directory = c.frame.openDirectory
        script = ("cd %s\n" % directory) + script + '\n' + ("rm -f %s\n" % path)
        os.write(handle, script)
        os.close(handle)
        os.chmod(path,0x700)
        #@-<< write script to temporary Unix file >>
        os.system("xterm -e sh  " + path)
#@-others
#@-leo
