#@+leo-ver=5-thin
#@+node:ekr.20101220161557.6014: * @file inactiveTests.txt
#@@language python
#@+all
#@+node:ekr.20111113194727.3869: ** @@@test BaseTextWrapper methods
import leo.core.leoFrame as leoFrame

w = leoFrame.BaseTextWrapper(c,'base-name','class-name',widget=None)

def check(expected):
    s = w.getAllText()
    assert s == expected,'expected %s got %s' % (expected,s)

w.setAllText('')        ; check('')
w.appendText('abc')     ; check('abc')
w.delete(1,2)           ; check('ac')
w.insert(0,'xy')        ; check('xyac')
w.insert(1,'z')         ; check('xzyac')
    # w.replace(2,4,'ABCD') ; check('xzABCDc')
        # This method no longer exists.  It is not used anywhere.
    # w.setSelectionRange(3,6)
    # s = w.getSelectedText()
    # assert s == 'BCD',repr(s)

w.deleteTextSelection() ; check('xzyac')
#@+node:ekr.20110615130436.3319: ** @@@test writing a .leo file retains orphan bits
# Not valid for external tests: uses @<file> node.
if not g.app.isExternalUnitTest:
    
    # Writing a .leo file must retain orphan bits of erroneous external files.
    
    h = '@file nonexistent-directory/orphan-bit-test.txt'
    p2 = g.findNodeAnywhere(c,h)
    assert p2,'not found: %s' % (h)
    assert p2.isOrphan(),'not an orphan originally'
    
    # It's dangerous to do the write, but this does test the bug fix.
    c.atFileCommands.clearAllOrphanBits(p2)
    assert p2.isOrphan(),'not an orphan after calling at.clearAllOrphanBits'
#@+node:ekr.20150529183247.1: ** @@test ptb.comment_leo_lines & ptb.uncomment_leo_lines
import leo.core.leoBeautify as leoBeautify
for p in p.children():
    b = leoBeautify.PythonTokenBeautifier(c)
    s1 = p.b
    comment,s = b.comment_leo_lines(p)
    s2 = b.uncomment_leo_lines(comment,p,s)
    assert s1 == s2,('\ns...\n%s\ns2...\n%s' % (s,s2))
#@+node:ekr.20150529183259.1: *3* indented docstring
def f ():
    pass
@ Line 1
    Line 2
#@+node:ekr.20150529184545.1: *3* indented docstring2
def f(): 
    pass
@ The following could be added to the 'else' clause::
    # Accumulate everything else.
#@+node:ekr.20150214063940.11: ** @ignore @shadow tests
#@+node:ekr.20111210175541.3957: *3* @@test at.readOneAtShadowNode retains @shadow links clones
# Important: the child of this node must be a clone of
# the corresponding node in @shadow unittest/at-shadow-unlink-clones.py

# The @shadow node will not exist for an external test.
if not g.app.isExternalUnitTest:
    try:
        # print('start',p.h)
        b = c.undoer.beforeChangeTree(p)
        h = '@shadow unittest/at-shadow-unlink-clones.py'
        root = g.findNodeAnywhere(c,h)
        assert root
        assert root.h == h,repr(root.h)
        child = p.firstChild()
        assert child
        assert child.isCloned(),'fail 1: test not set up properly'
        c.selectPosition(root)
        fn = root.atShadowFileNodeName()
        assert fn
        c.atFileCommands.readOneAtShadowNode (fn,root,force=True)
        c.undoer.afterChangeTree(p,'fc.readOneAtShadowNode',b)
        assert child.isCloned(),'fail 2: intended test fails'
        c.undoer.undo()
    finally:
        c.selectPosition(p)
        c.redraw()
#@+node:ekr.20120228174052.3929: *4* Node 1
# node 1 text A.
#@+node:ekr.20110610122533.3397: *3* @@test goto-global-line @shadow
# Not valid for external tests: uses @<file> node.
if not g.app.isExternalUnitTest:

    h = '@shadow unittest/at-shadow-line-number-test.py'
    root1 = g.findNodeAnywhere(c,h)
    assert root1
    assert root1.isAnyAtFileNode()
    
    fileName,lines,n,root2 = c.GoToLineNumber(c).setup_file(n=6,p=root1)
    assert fileName == h[8:],'fileName'
    assert root2 == root1
    
    if 0:
        print('root:%s, isRaw:%s, n:%s, len(lines): %s' % (
            root and root.h,isRaw,n,len(lines)))
#@+node:ekr.20130503061511.4137: ** @ignore from leoPy.leo
import leo.core.leoImport as leoImport
ic = c.importCommands
hs = leoImport.HtmlScanner(importCommands=ic,atAuto=True)

s1 = '''
<table id="1"> <table id="2">
<contents/>
</table>
</table>
'''

s2 = '''
<table id="1"> 
<table id="2">
<contents/>
</table>
</table>
'''

t1 = 
assert result == expected,'expected...\n%s\ngot...\n%s' % (
    repr(expected),repr(result))
#@+node:ekr.20130503061511.4140: *3* @test html string
s = '''\
<HTML>
<head>
    <title>Bodystring</title>
</head>
<body class='bodystring'>
<div id='bodydisplay'></div>
</body>
</html>
'''

html_tags = ('body','head','html','table',) # 'div',
setting = 'import_html_tags'

# Settings now work when run externally.
c.config.set(setting,'data',html_tags)
tags = c.config.getData(setting)
assert tags == html_tags,len(tags)

g.app.unitTestDict ['expectedErrors'] = 0

showTree = True

c.importCommands.htmlUnitTest(p,s=s,showTree=showTree)

if showTree:
    # g.cls()
    for p in p.subtree():
        print('\n***** %s\n' %p.h)
        print(p.b)
#@+node:ekr.20130503061511.4141: *4* @file c:/leo.repo/trunk/leo/core/html string
@language xml
@tabwidth -4
@others

#@+node:ekr.20130503061511.4150: *5* html
<HTML>
@others
</html>
#@+node:ekr.20130503061511.4151: *6* head

<head>
    <title>Bodystring</title>
</head>
#@+node:ekr.20130503061511.4152: *6* body
<body class='bodystring'>
<div id='bodydisplay'></div>
</body>
#@+node:ekr.20130503061511.4145: *4* @file c:/leo.repo/trunk/leo/core/html string
@language xml
@tabwidth -4
@others

#@+node:ekr.20130503061511.4150: *5* html
<HTML>
@others
</html>
#@+node:ekr.20130503061511.4151: *6* head

<head>
    <title>Bodystring</title>
</head>
#@+node:ekr.20130503061511.4152: *6* body
<body class='bodystring'>
<div id='bodydisplay'></div>
</body>
#@+node:ekr.20130503061511.4149: *4* @file c:/leo.repo/trunk/leo/core/html string
@language xml
@tabwidth -4
@others

#@+node:ekr.20130503061511.4150: *5* html
<HTML>
@others
</html>
#@+node:ekr.20130503061511.4151: *6* head

<head>
    <title>Bodystring</title>
</head>
#@+node:ekr.20130503061511.4152: *6* body
<body class='bodystring'>
<div id='bodydisplay'></div>
</body>
#@+node:ekr.20130503061511.4153: *3* @test HtmlScanner.filterTokens
import leo.core.leoImport as leoImport
ic = c.importCommands
hs = leoImport.HtmlScanner(importCommands=ic,atAuto=True)
strip = hs.stripTokens
dump  = hs.formatTokens

s1 = '''<table id="1"><table id="2">
<contents/>
</table>
</table>'''

s2 = '<table id="1"><table id="2"><contents/></table></table>'
    
t1 = hs.tokenize(s1)
t2 = hs.tokenize(s2)
f1 = hs.filterTokens(t1)
f2 = hs.filterTokens(t2)

assert strip(f1) == strip(f2),'f1...\n%s\nf2...\n%s' % (
    dump(f1),dump(f2))
    
if 0:
    print(dump(f1))
#@+node:ekr.20130503061511.4154: *3* @test import dataN.html
fn = r'c:\recent\data.html'

# fn = r'c:\recent\data-smaller.html'
# fn = r'c:\recent\data666.html'

# These all pass on data.html:
    # html_tags = ('html','head','body',)
    # html_tags = ('html','head','body','table',)
    # html_tags = ('html','head','body','table','div',)
    # html_tags = ('html','head','body','table','div','script',)
    # html_tags = ('html','head','body','table','div','script','link',)
    # html_tags = ('html','head','body','table','div','script','link','p',)

html_tags = ('html','head','body','table','div','script','p','td','tr',)

# Settings now work when run externally.
setting = 'import_html_tags'
c.config.set(setting,'data',html_tags)
tags = c.config.getData(setting)
assert tags == html_tags,len(tags)

g.cls()

c.importCommands.importFilesCommand(files=[fn], treeType='@file')
#@+node:ekr.20130503061511.4155: *3* @test unicode stuff
@first # -*- coding: utf-8 -*-

table = (
    'test',
    'Ä 궯 奠',
    'Ä 궯 奠 after', # fails with cp6501: after is duplicated.
)

print('*'*20)
print('isPython3: %s' % g.isPython3)

for s in table:
    if g.isPython3:
        s = s.encode('ascii','replace') # create bytes.
    g.es(repr(s))
    g.es(s)
    g.pr ('g.pr(s)       : %s' % s)
    g.pr ('g.pr(repr(s)) : %s' % repr(s))
    print('print(s)      : %s' % s)
    print('print(repr(s)): %s' % s)
#@+node:ekr.20130503061511.4156: *3* @test external text operations
assert g.app.isExternalUnitTest

body = c.frame.body
assert repr(body.widget).startswith('stringTextWidget')
assert body.widget == body.bodyCtrl

w = body.bodyCtrl
w.setAllText(p.b)
assert p.b == w.getAllText()
#@+node:ekr.20130503061511.4157: *3* @test nullBody text operations
# print('isExternalUnitTest',g.app.isExternalUnitTest)

if g.app.isExternalUnitTest:
    body = c.frame.body
else:
    import leo.core.leoCommands as leoCommands
    import leo.core.leoFrame as leoFrame
    import leo.core.leoGui as leoGui
    
    # Important: external unit tests should execute in this environment.
    nullGui   = leoGui.NullGui('null gui')
    nullFrame = leoFrame.NullFrame(title='nullFrame title',gui=nullGui)
    c2 = leoCommands.Commands(nullFrame,fileName='<empty fileName>')
    nullFrame.c = c2
    body = leoFrame.nullBody(frame=nullFrame,parentFrame=None)
    assert repr(body).startswith('<leo.core.leoFrame.nullBody')

# Now test some basic operations.
assert repr(body.widget).startswith('stringTextWidget')
assert body.widget == body.bodyCtrl
w = body.bodyCtrl

w.setAllText(p.b)
assert p.b == w.getAllText()
#@+node:ekr.20130503061511.4158: *3* @test g.python_tokenize
# h = 'g.python_tokenize'
# p = p.firstChild()
# assert p.h == h
tokens = g.python_tokenize(p.b,line_numbers=False)

# tokens = [(kind,val) for (kind,val,line_number) in tokens]

# First, the basic check
tokens1 = [val for kind,val in tokens]
s = ''.join(tokens1)
assert p.b == s,repr(s)

if 0:
    for z in tokens:
        kind,val = z
        print('%6s %s' % (kind,repr(val)))
        
# Next, start filtering.
tokens = [(kind,g.choose(kind=='string','"S"',val)) for kind,val in tokens]

if 0: # Delete whitespace.
    tokens = [(kind,val) for (kind,val) in tokens if kind != 'ws']
    tokens = [(kind,g.choose(kind=='id',val+' ',val)) for (kind,val) in tokens]

# Last: stringize.
tokens = [val for kind,val in tokens if kind != 'comment']
# print(''.join(tokens))

if 0: # Print lines containing '='
    s = ''.join(tokens)
    for ch in '()[]{}<>.,:=+-/':
        s = s.replace(' '+ch,ch)
    aList = [z for z in g.splitLines(s)
        if z.find('=') > -1] # and not z.find('+=')>-1 and not z.find('-=')>-1]
    print(''.join(aList))
    
#@+node:ekr.20130503061820.4217: ** @ignore LeoInspect unit tests
@language python
#@+node:ekr.20130503061820.4218: *3* Basic tests
#@+node:ekr.20130503061820.4219: *4* @test leoInspect with multiple files
import leo.core.leoInspect as leoInspect
import os
import time

<< define old_s >>
<< define s >>

@others

aList = (
    'leoAtFile.py',
    'leoEditCommands.py',
)
test(files=aList,print_stats=False,s=None,print_times=True)
#@+node:ekr.20130503061820.4220: *5* << define old_s >>
# import leo.core.leoGlobals
# import leo.core.leoGlobals as g
# from leo.core.leoGlobals import pr as pr2
# from leo.core.leoGlobals import trace

s_old = '''\
import sys

aGlobal = 5
# aGlobal2 is not explicitly defined.

c = [z for z in 'abc']

def myFunc():
    n1,n2,n3,junk,junk=sys.version_info
    a = self.b
    for z in a:
        print(z)
    with A() as a:
        print(a,b)
        
def test():
    a = b # UnboundLocalError.
    b = 1
    c = 2 # Any def will do at present.
    print(g)
    print(c.frame.body)
    print(c.frame.body.xxx.yyy)
    print(b.yyy) # no check will be made.
    print(xxx.yyy)
    for c in 'abc':
        print(c,b)
        print(g)


class myClass:
    
    def __init__(self,c):
        self.a = True
        self.b = None
        c.frame.xxxx
        
    def spam(self,a,b,c=5,*args,**keys):
        global aGlobal2
        aGlobal2 = 'abc'
        self.a = b
        self.a = x
        
    def no_self(a):
        pass
        
    def test_lambda(self):
        f = lambda a,b: a

    def test_comprehension(self):
        z2 = [z for z in 'abc']
        
aGlobal3 = 4 # This should be defined everywhere.

def test():
    # a = ','.join(['a','b'])
    p = 5
    # print(p.parent().h)
    # print(g.app.windowList[0])
    print(p)
    print(g)

'''

# import leo.core.leoCommands as leoCommands
#@+node:ekr.20130503061820.4221: *5* << define s >>
s = '''\

import leo.core.leoGlobals as g

def test(c):
    a = 5
    f = c.frame
    c.frame.body.bodyCtrl = w

'''

s = g.adjustTripleString(s,-4)
# print(s)
#@+node:ekr.20130503061820.4222: *5* test
def test(files,print_stats=True,s=None,print_times=True):
   
    t1 = time.time()
    sd = leoInspect.SemanticData(controller=None)

    if s: # Use test string.
        fn = '<test file>'
        leoInspect.InspectTraverser(fn,sd).traverse(s)
    else:
        for fn in files:
            print(g.shortFileName(fn))
            s = leoInspect.LeoCoreFiles().get_source(fn)
            if s:
                leoInspect.InspectTraverser(fn,sd).traverse(s)
            else:
                print('file not found: %s' % (fn))
           
    sd.total_time = time.time()-t1
    
    if print_times: sd.print_times()
    if print_stats: sd.print_stats()
#@+node:ekr.20130503061820.4223: *4* @test leoInspect.module.classes
import leo.core.leoInspect as leoInspect

dump_classes = False
print_modules = True
print_functions = True
print_stats = False
print_times = False

# if dump_modules or print_stats:
    # g.cls()

m = leoInspect.module(fn='leoApp.py',sd=None,
    print_stats=print_stats,print_times=print_times)
    
if 0:
    print(m)
    for o in m.classes():
        if dump_classes:
            o.dump()
        if print_modules:
            print(o)
        if print_functions:
            for f in o.functions():
                print('  %s' % f)
#@+node:ekr.20130503061820.4224: *4* @test leoInspect.module.defs
import leo.core.leoInspect as leoInspect

# g.cls()

m = leoInspect.module(fn='leoEditCommands.py')
    #,sd=None,print_stats=False,print_times=False)
    
if 0:
    print(m)
    for z in m.classes():
        print(z)
        for z2 in z.defs():
            name = z2.tree_ptr.name
            aList = z2.call_args_of(name)
            args = ','.join(aList)
            print(' %s(%s)' % (name,args))
#@+node:ekr.20130503061820.4225: *4* @test leoInspect.module.classes 2
import leo.core.leoInspect as leoInspect

# g.cls()

m = leoInspect.module(fn='leoEditCommands.py')
    #,sd=None,print_stats=False,print_times=False)
    
if 0:
    for z in m.classes():
        print(z)
#@+node:ekr.20130503061820.4226: *4* @test leoInspect.module.statements
import leo.core.leoInspect as leoInspect

# g.cls()

m = leoInspect.module(fn='leoEditCommands.py')
    #,sd=None,print_stats=False,print_times=False)

if 0:
    print(m)
    for z in m.classes():
        print(z)
        for z2 in z.defs():
            print(z2)
#@+node:ekr.20130503061820.4227: *4* @test leoInspect (leoEditCommands.py)
import leo.core.leoInspect as leoInspect

# g.cls()

m = leoInspect.module(fn='leoEditCommands.py')

def show(o):
    print('%-5s %s' % (o.line_number(),o.format()))

var = '.widget'
func = 'w.insert'

if 0:
    print('\nAssignments to %s...\n' % (var))
    for o in m.assignments_to(var):
        show(o)
        
    print('\nAssignments using %s...\n' % (var))
    for o in m.assignments_using(var):
        show(o)
        
    print('\nCalls to %s...\n' % (func))
    for o in m.calls_to(func):
        show(o)
        
    if 1:
        classes = m.classes()
        for d in classes[0].defs():
            print('')
            print(d)
            for z in d.statements():
                # print(z.tree())
                # print(z.sd.dump_ast(z.tree()))
                lines = g.splitLines(z.format())
                for line in lines:
                    print('  %s' % (line))
#@+node:ekr.20130503061820.4228: *4* @test leoInspect.module (s)
import leo.core.leoInspect as leoInspect

def show(o,indent=0):
    # print('\n%s\n' % o.sd.dump_ast(o.tree()))
    print('%s%s' % (' '*4*indent,o.format()))

<< define s >>
print('Input...\n%s\n' % (s.rstrip()))

m = leoInspect.module(s=s)

# print(show(m,0))

print('\nStatements...\n')
for o in m.statements():
    print(o.format())
    
if 0:
    print('\nAssignments...\n')
    for o in m.assignments():
        print(o.format())

print('\nAssignments to a...\n')
for o in m.assignments_to('a'):
    print(o.format())
    
print('\nAssignments using d...\n')
for o in m.assignments_using('d'):
    print(o.format())
    
print('\nCalls to f...\n')
for o in m.calls_to('f'):
    print(o.format())

if 0:
    for f in m.functions():
        show(f,0)
        for z in f.statements():
            show(z,1)
    
    for cls in m.classes():
        show(cls,0)
        for d in cls.defs():
            show(d,1)
            for z in d.statements():
                show(z,1)
#@+node:ekr.20130503061820.4229: *5* << define s >>
s = '''
x.y = b(arg1,arg2=5,*args,**args).c[1:2:3].d
a=b+c
p,d,q[5]=f(a=1,b=2,*args,**keys)
'''


# def outer_function(a,b=99,c=88,*args,**keys):
    # print('hello')
    
# class myClass:
    # def method():
        # pass
    
#@+node:ekr.20130503061820.4230: *3* leoInspect speed tests
#@+node:ekr.20130503061820.4231: *4* @test speed of leoInspect.module (all core files)
import leo.core.leoInspect as leoInspect
import time

t1 = time.time()

sd = leoInspect.SemanticData()
count = 0
for fn in leoInspect.LeoCoreFiles().files:
    # print(fn)
    m = leoInspect.module(fn,sd=sd)
    count += 1

t2 = time.time()

print('file: %s time: %2.2f sec' % (count,t2-t1))

if 0:
    sd.print_stats()
if 1:
    sd.print_times()
#@+node:ekr.20130503061820.4232: *4* @test speed of AstTraverser (all Leo core files)
import leo.core.leoGlobals as g
import leo.core.leoInspect as leoInspect
import ast
import time

read_time,parse_time,traverse_time = 0.0,0.0,0.0
t_start = time.time()
count = 0
for fn in leoInspect.LeoCoreFiles().files:
    count += 1
    t2 = time.time()
    s = open(fn,'r').read()
    t3 = time.time()
    tree = ast.parse(s,filename=fn,mode='exec')
    t4 = time.time()
    leoInspect.AstTraverser(fn).visit(tree)
    t5 = time.time()

    read_time += t3-t2
    parse_time += t4-t3
    traverse_time += t5-t4
t_end = time.time()
total_time = t_end-t_start
if 1:
    print('files:     %s' % (count))
    print('read:      %2.3f sec.' % (read_time))
    print('ast.parse: %2.3f sec.' % (parse_time))
    print('traverse:  %2.3f sec.' % (traverse_time))
    print('total:     %2.3f sec.' % (total_time))

#@+node:ekr.20130503061820.4233: *3* @test compute all ivars in all classes
import imp
import time

import leo.core.leoInspect as li
imp.reload(li)
g_dump,g_format,g_kind = li.g_dump,li.g_format,li.g_kind

t1 = time.time()

print('starting pass 1...')

# Globals...
sd = li.SemanticData(controller=None)
g_d = {} # Keys are ivars, values are lists of classes.
n_files = 0

for fn in li.LeoCoreFiles().files:

    # Most of the time is spent creating the context objects.
    m = li.module(fn=fn,sd=sd)
    n_files += 1
    
    ### To do: create global assignments list.
    
    if 1: # Look for all targets. Takes about 0.15 sec.
        for class_ in m.classes():
            for def_ in class_.defs():
                for a in def_.assignments():
                    tree = a.tree()
                    kind = g_kind(tree)
                    # if g_kind(tree.value) == 'ListComp':
                        # print(a.format())
                        # print(g_dump(tree.value))
                    # print(a.format())
                    if kind == 'Assign':
                        for target in tree.targets:
                            # if False and g_kind(target) not in ('Attribute','Name','Tuple','Subscript'):
                                # print('assn target: %s' % g_format(target))
                                # print('assn target: %s' % g_dump(target))
                            name = g_format(target)
                            if name.startswith('self.'):
                                name = name[5:]
                            i = name.find('[')
                            if i > -1:
                                name = name[:i]
                            aList = g_d.get(name,[])
                            if class_.name() not in aList:
                                aList.append(class_.name())
                                g_d[name] = aList
                    else:
                        assert kind == 'AugAssign',kind
                        name = g_format(tree.target)
                        aList = g_d.get(name,[])
                        if class_.name() not in aList:
                            aList.append(class_.name())
                            g_d[name] = aList

    # Takes about 0.1 sec. cumulative.
    if 0: # Look for all ivars.
        for class_ in m.classes():
            for def_ in class_.defs():
                if def_.name() == '__init__':
                    for a in def_.assignments_to('self'):
                        # Not all targets are ivars.
                        for target in a.tree().targets:
                            name = g_format(target)
                            if name.startswith('self.'):
                                name = name[5:]
                                aList = g_d.get(name,[])
                                if class_.name() not in aList:
                                    aList.append(class_.name())
                                    g_d[name] = aList
                       

t2 = time.time()

print('files: %s time: %2.2f sec, total ivars: %s' % (
    n_files,t2-t1,len(list(g_d.keys()))))

if 0:
    ambiguous,total = 0,0
    for key in sorted(g_d.keys()):
        aList = sorted(g_d.get(key))
        if 1 and len(aList) > 1:
            w = 30 # Width of left column
            if total == 0: print('Global ivars dict...')
            if len(key) + 3 > w: key = key[:w-3]+'...'
            aList2 = aList[:3]
            if len(aList2) < len(aList): aList2.append('...')
            print('%30s %2s %s' % (key,len(aList),aList2))
        total += 1
        if len(aList) > 1: ambiguous += 1
    print('total ivars: %s ambiguous: %s' % (total,ambiguous))

if 0:
    sd.print_stats()
#@+node:ekr.20130503061820.4234: *3* @test find all ctors
g.cls()

import imp
import time

import leo.core.leoInspect as li
imp.reload(li)
g_dump,g_format,g_kind = li.g_dump,li.g_format,li.g_kind

t1 = time.time()

# Globals...
sd = li.SemanticData(controller=None)
d = sd.modules_dict

# Pass 1: Load all modules.
print('starting pass 1...')
for fn in li.LeoCoreFiles().files:
    li.module(fn=fn,sd=sd)

t2 = time.time()
print('pass 1: %2.3f sec files: %s' % (t2-t1,len(list(d.keys()))))
    
# Pass 2: compute all class names.
classes = set()
for fn in sorted(d):
    m = d.get(fn)
    for class_ in m.classes():
        classes.add(class_.name())
        
t3 = time.time()
print('pass 2: %2.3f sec' % (t3-t2))

classes = sorted(list(classes))

if 0:
    for z in classes:
        print(z)
        
# Pass 3: Find all calls to ctors.
ctors = set()
ctors_assns = []
for fn in sorted(d):
    m = d.get(fn)
    for class_ in m.classes():
        for def_ in class_.defs():
            for a in def_.assignments():
                rhs = a.tree().value
                if m.tree_kind(rhs) == 'Call':
                    s = li.g_find_function_call(rhs.func)
                    if s in classes:
                        ctors.add(s)
                        ctors_assns.append(a.format())
 
t4 = time.time()                            

if 0:
    for z in sorted(list(ctors)):
        print(z)
if 0:
    for s in ctors_assns:
        aList = s.split('=')
        print('%30s = %s' % (aList[0],'='.join(aList[1:])[:80]))

print('pass 3: %2.3f sec ctors assigns: %s' % (t4-t3,len(ctors_assns)))

if 1:
    sd.print_stats()
if 1:
    sd.print_times()
#@+node:ekr.20130503061820.4235: *3* @test pickling
# g.cls()

import imp
import pickle
import time

import leo.core.leoInspect as li
imp.reload(li)
    
# Works, because TestPickleClass is a top-level class.
o = li.TestPickleClass()

try:
    s = pickle.dumps(o)
    print(len(s),o)
except pickle.PicklingError:
    print('can not pickle: %s' % repr(o))
#@+node:ekr.20130503061820.4236: *3* @test cache contexts
# g.cls()

import imp
import pickle
import time

import leo.core.leoInspect as li
imp.reload(li)
g_dump,g_format,g_kind = li.g_dump,li.g_format,li.g_kind

# Pass 1: Load all modules.
t1 = time.time()
sd = li.SemanticData(controller=None)
files = li.LeoCoreFiles().files

print('starting pass 1...')
for fn in files:
    li.module(fn=fn,sd=sd)

t2 = time.time()
print('pass 1: %2.3f sec files: %s' % (t2-t1,len(list(sd.modules_dict.keys()))))

# Pass 2: pickle all modules.
for fn in sorted(sd.modules_dict.keys()):
    m = sd.modules_dict.get(fn)
    try:
        s = pickle.dumps(m)
        print('%6s %s' % (len(s),fn))
    except pickle.PicklingError:
        print('can not pickle: %s' % repr(m))
        
t3 = time.time()
print('pass 2: %2.3f sec' % (t3-t2))
#@+node:ekr.20130503061820.4237: *3* @test AstFormatter
g.cls()

import imp

import leo.core.leoInspect as li
imp.reload(li)
g_format = li.g_format

# Pass 1: Load all modules.
sd = li.SemanticData(controller=None)
files = li.LeoCoreFiles().files # [0:2]

s = '''
def spam():
    """This is a docstring"""
    a = 2
    try:
        pass
    except Exception as message: ###
        pass
    if f(1):
        g('a')
    else:
        g(2)
    while 1 < 2:
        pass
    raise AttributeError ###
    return 2
'''

if 0: # String
    li.module(s=s,sd=sd)
else:
    print('starting pass 1...')
    for fn in files:
        li.module(fn=fn,sd=sd)
    
# Pass 2: format the module.
for fn in sorted(sd.modules_dict.keys()):
    m = sd.modules_dict.get(fn)
    # print(m.format())
    m.format() # Run for warnings.
#@+node:ekr.20130503061820.4238: *3* @test g_files_in_dir
import imp
import leo.core.leoInspect as li
imp.reload(li)

g.cls()

aList = li.g_files_in_dir(r'C:\Python26\Lib\lib2to3',
    extList = ['.py'],
    excludeDirs= ['tests'])

for z in aList:
    print(z)

print('files: %s' % (len(z)))
#@+node:ekr.20130503061820.4239: *3* @test print-chains
import ast
import imp
import time
import leo.core.leoInspect as li
imp.reload(li)

t1 = time.time()

g.cls()

last = c.rootPosition()
while last.hasNext():
    last = last.next()
    
parent = last.insertAfter()
parent.h = 'Chains: %s' % time.strftime('%Y/%m/%d/%H:%M:%S',time.localtime())
parent.b = '@killcolor'

count, total_chains,unusual_chains = 0,0,0
for fn in li.LeoCoreFiles().files: # [:2]:
    # print()
    # print(fn)
    s = open(fn,'r').read()
    tree = ast.parse(s,filename=fn,mode='exec')
    cp = li.ChainPrinter(fn)
    cp.visit(tree)
    p2 = parent.insertAsLastChild()
    p2.h = g.shortFileName(fn)
    n1,n2 = cp.showChains(p2)
    total_chains += n1
    unusual_chains += n2
    # print('chains: %s' % (n))
    count += 1
    
c.redraw(parent)

t2 = time.time()

print('files: %s total chains: %s unusual_chains: %s time: %2.2f sec' % (
    count,total_chains,unusual_chains,t2-t1))

# if 0:
    # sd.print_stats()
# if 0:
    # sd.print_times()
#@+node:ekr.20130503061820.4240: *3* @test print-calls
import ast
import imp
import time
import leo.core.leoInspect as li
imp.reload(li)

g.cls()

t1 = time.time()

last = c.rootPosition()
while last.hasNext():
    last = last.next()
    
parent = last.insertAfter()
parent.h = 'Calls: %s' % time.strftime('%Y/%m/%d/%H:%M:%S',time.localtime())
parent.b = '@killcolor'

g_d = {}
count=0
for fn in li.LeoCoreFiles().files: # [:3]:
    # print()
    # print(fn)
    s = open(fn,'r').read()
    tree = ast.parse(s,filename=fn,mode='exec')
    cp = li.CallPrinter(fn)
    cp.visit(tree)
    p2 = parent.insertAsLastChild()
    p2.h = g.shortFileName(fn)
    cp.showCalls(p2)
    for key in cp.d.keys():
        aList = g_d.get(key,[])
        aList.extend(cp.d.get(key))
        g_d[key] = sorted(list(set(aList)))
    count += 1
    
p2 = parent.insertAsLastChild()
p2.h = 'global calls'
cp.showCalls(p2,d=g_d)
c.selectPosition(parent)
c.redraw()

t2 = time.time()

print('files: %s time: %2.2f sec' % (count,t2-t1))
#@+node:ekr.20130503061820.4241: *3* @test print-returns
import ast
import imp
import time
import leo.core.leoInspect as li
imp.reload(li)

g.cls()

t1 = time.time()

last = c.rootPosition()
while last.hasNext():
    last = last.next()
    
project_name,verbose = 'leo',False # False: only print defs with more than one return.
files = li.g_get_files_by_project_name(project_name)

parent = last.insertAfter()
parent.h = 'Returns: %s verbose=%s %s' % (
    project_name, verbose,
    time.strftime('%Y/%m/%d/%H:%M:%S',time.localtime()))
parent.b = '@killcolor'

g_d = {}
count=0
for fn in files:
    # print(fn)
    s = open(fn,'r').read()
    try:
        tree = ast.parse(s,filename=fn,mode='exec')
    except SyntaxError:
        print('Syntax error in %s' % (fn))
        continue
    rp = li.ReturnPrinter(fn)
    rp.visit(tree)
    p2 = parent.insertAsLastChild()
    p2.h = g.shortFileName(fn)
    p2.b = rp.showReturns(verbose=verbose)
    for key in rp.d.keys():
        aList = g_d.get(key,[])
        aList2 = rp.d.get(key)
        if aList2:
            aList.extend(aList2)
            g_d[key] = aList
    count += 1
    
if 0:
    p2 = parent.insertAsLastChild()
    p2.h = 'global returns'
    rp.showReturns(p2,d=g_d)

c.redraw(parent)

t2 = time.time()

print('files: %s time: %2.2f sec' % (
    count,t2-t1))
#@+node:ekr.20130503061820.4242: *3* @test global names
# import ast
import imp
import time
import leo.core.leoInspect as li
imp.reload(li)

g.cls()

project_name,verbose = 'leo',False
files = li.g_get_files_by_project_name(project_name)
result = []

def put(s):
    result.append(s)
    # print(s)
    
t1 = time.time()

# Globals...
sd = li.SemanticData(controller=None)
m_d = sd.modules_dict

# Pass 1: Load all modules.
print('starting pass 1...')
for fn in files: ### [:2]:
    li.module(fn=fn,sd=sd)

t2 = time.time()
put('pass 1: %2.3f sec files: %s' % (t2-t1,len(list(m_d.keys()))))

# Pass 2: Update g_d.
g_d = {} # Keys are names, values are sets of Context names.
contexts = 0
for fn in sorted(m_d):
    m = m_d.get(fn)
    for cx in m.contexts(include_temp=True):
        # put(' '*len(cx.parent_contexts()),cx)
        contexts += 1
        d = cx.st.d # Keys are names, values are symbol table entries.
        for key in d.keys():
            e = d.get(key)
            name = e.name
            aSet = g_d.get(name,set())
            aSet.add(cx)
            g_d[name] = aSet
            
names = sorted(g_d.keys())
distribution = {} # Keys are lengths of context sets; values are number of ids with that length.
for key in names:
    aSet = g_d.get(key)
    context_list = sorted(list(set([repr(z) for z in aSet])))
    n = len(context_list)
    distribution[n] = distribution.get(n,0) + 1
    if verbose or n > 9:
        if n > 3:
            put('%20s %4s %s...' % (key,len(context_list),context_list[:3]))
        else:
            put('%20s %4s %s' % (key,len(context_list),context_list))
ids = len(names)
        
t3 = time.time()
put('pass 2: contexts: %s ids: %s %2.2f sec' % (contexts,ids,t3-t2))

if 1:
    put('\nDistribution of context lengths...')
    for key in sorted(distribution.keys()):
        put('%4s %s' % (key,distribution.get(key)))
    
if 1:
    last = c.rootPosition()
    while last.hasNext():
        last = last.next()
    parent = last.insertAfter()
    parent.h = 'Global names: %s verbose=%s %s' % (
        project_name, verbose,
        time.strftime('%Y/%m/%d/%H:%M:%S',time.localtime()))
    parent.b = '@killcolor\n\n%s' % '\n'.join(result)
    c.redraw(parent)

if 0:
    sd.print_stats()
if 0:
    sd.print_times()

#@+node:ekr.20130503061820.4189: *3* @test leoInspect.token_range (s)
import leo.core.leoInspect as inspect

# g.cls()

testing = g.unitTesting

def show(o,indent=0):
    pad = ' '*4*indent
    if not testing:
        # print('\n%s\n' % o.sd.dump_ast(o.tree()))
        print('%s%s' % (pad,o.format()))
        print('token range: %s' % (repr(o.token_range())))

<< define s >>
if not testing:
    print('Input...\n%s\n' % (s.rstrip()))

m = inspect.module(s=s)

if 0:
    show(m,0)
    
if 1:
    if not testing: print('\nAssignments to a...\n')
    for o in m.assignments_to('a'):
        # print(o.format())
        show(o)
if 0:
    if not testing: print('\nAssignments using d...\n')
    for o in m.assignments_using('d'):
        # print(o.format())
        show(o)
if 0:    
    if not testing: print('\nCalls to f...\n')
    for o in m.calls_to('f'):
        # print(o.format())
        show(o)
if 0:
    for s in m.statements():
        show(s)
    
    for f in m.functions():
        show(f,0)
        for z in f.statements():
            show(z,1)
    
    for cls in m.classes():
        show(cls,0)
        for d in cls.defs():
            show(d,1)
            for z in d.statements():
                show(z,1)
#@+node:ekr.20130503061820.4190: *4* << define s >>
s = '''
# x.y = b(arg1,arg2=5,*args,**args).c[1:2:3].d
a=b+c
# f(a=1,b=2,*args,**keys)
'''


# def outer_function(a,b=99,c=88,*args,**keys):
    # print('hello')
    
# class myClass:
    # def method():
        # pass
    
#@+node:ekr.20131231093529.3984: ** @ignore leoViews.py
#@+node:ekr.20140110141116.4267: *3* @@@test vc.create_tree_structure (rewrite)
vc = c.viewController
views = g.findNodeAnywhere(c,'@views')
if views: views.deleteAllChildren()
    # Start with a pristine @views tree.
root_before  = g.findNodeInTree(c,p,'root_before')
    # Root before: used to generate @auto-view tree.
root_restore = g.findNodeInTree(c,p,'root_restore')
    # Create root_after from root_restore.
assert root_before,root_restore
root_after  = g.findNodeInTree(c,p,'root_after')
if root_after: root_after.doDelete()
    # Root after: the results of the previous test.
c.selectPosition(root_restore)
c.copyOutline()
c.selectPosition(root_before)
c.pasteOutline()
# The roots must look like @auto nodes.
root_after = c.p
assert root_after.h == 'root_restore',root_after.h
root_before.h = '@auto root_before'
root_after.h = '@auto root_after' 
try:
    vc.update_before_write_at_auto_file(root_before)
    at_organizers = vc.has_at_organizers_node(root_before)
    assert at_organizers
    # Called by vc.create_organizer_nodes(organizers,root_after):
    root = root_after
    vc.create_organizer_data(at_organizers,root)
    vc.create_actual_organizer_nodes()
    vc.create_tree_structure(root)
    # The body of demote_organized_nodes:
    for od in vc.all_ods:
        # Called by vc.update_helper.
        od_list = vc.find_all_organizer_nodes(od)
        assert od in od_list,od_list
    d = {
        'organizer node': ['intermediate node','inner org1','inner org2'],
        'intermediate node': ['inner org1','inner org2'],
    }
    for od in vc.organizer_data_list:
        aList = d.get(od.h,[])
        aList2 = [z.h for z in od.descendants or []]
        assert sorted(aList) == sorted(aList2),(aList,aList2)
finally:
    # Make sure the roots are *not* @auto nodes.
    vc.temp_node.doDelete()
    root_before.h = 'root_before'
    root_after.h = 'root_after'
    c.redraw()
#@+node:ekr.20140110141116.4322: *4* root_restore
@others
#@+node:ekr.20140110141116.4323: *5* aClass
class aClass:
    @others
#@+node:ekr.20140110141116.4324: *6* before
def spam_before(self):
    pass
#@+node:ekr.20140110141116.4325: *6* child11
def spam11(self):
    pass
#@+node:ekr.20140110141116.4326: *6* child12
def spam12(self):
    pass
#@+node:ekr.20140110141116.4327: *6* middle
def middle_spam():
    pass
#@+node:ekr.20140110141116.4328: *6* child21
def spam21(self):
    pass
#@+node:ekr.20140110141116.4329: *6* child22
def spam22(self):
    pass
#@+node:ekr.20140110141116.4330: *6* after
def spam_after(self):
    pass
#@+node:ekr.20140110141116.4331: *6* last1
def spam_last1():
    pass
#@+node:ekr.20140110141116.4332: *5* top1
def spam_top1():
    pass
#@+node:ekr.20140110141116.4333: *5* top2
def spam_top2():
    pass
#@+node:ekr.20140110141116.4351: *4* root_before
@others
#@+node:ekr.20140110141116.4352: *5* aClass
class aClass:
    @others
#@+node:ekr.20140110141116.4353: *6* before
def spam_before(self):
    pass
#@+node:ekr.20140110141116.4354: *6* organizer node
#@+node:ekr.20140110141116.4368: *7* intermediate node
#@+node:ekr.20140110141116.4355: *8* inner org1
#@+node:ekr.20140110141116.4356: *9* child11
def spam11(self):
    pass
#@+node:ekr.20140110141116.4357: *9* child12
def spam12(self):
    pass
#@+node:ekr.20140110141116.4358: *8* middle
def middle_spam():
    pass
#@+node:ekr.20140110141116.4359: *8* inner org2
#@+node:ekr.20140110141116.4360: *9* child21
def spam21(self):
    pass
#@+node:ekr.20140110141116.4361: *9* child22
def spam22(self):
    pass
#@+node:ekr.20140110141116.4362: *6* after
def spam_after(self):
    pass
#@+node:ekr.20140110141116.4363: *6* last organizer
#@+node:ekr.20140110141116.4364: *7* last1
def spam_last1():
    pass
#@+node:ekr.20140110141116.4365: *5* top-level
#@+node:ekr.20140110141116.4366: *6* top1
def spam_top1():
    pass
#@+node:ekr.20140110141116.4367: *6* top2
def spam_top2():
    pass
#@+node:ekr.20140121160228.4834: *4* root_after
@others
#@+node:ekr.20140121160228.4835: *5* aClass
class aClass:
    @others
#@+node:ekr.20140121160228.4836: *6* before
def spam_before(self):
    pass
#@+node:ekr.20140121160228.4837: *6* child11
def spam11(self):
    pass
#@+node:ekr.20140121160228.4838: *6* child12
def spam12(self):
    pass
#@+node:ekr.20140121160228.4839: *6* middle
def middle_spam():
    pass
#@+node:ekr.20140121160228.4840: *6* child21
def spam21(self):
    pass
#@+node:ekr.20140121160228.4841: *6* child22
def spam22(self):
    pass
#@+node:ekr.20140121160228.4842: *6* after
def spam_after(self):
    pass
#@+node:ekr.20140121160228.4843: *6* last1
def spam_last1():
    pass
#@+node:ekr.20140121160228.4844: *5* top1
def spam_top1():
    pass
#@+node:ekr.20140121160228.4845: *5* top2
def spam_top2():
    pass
#@+node:ekr.20140110044125.4362: *3* @test atFile.new_auto
# To remind ourselves of the status of new_import.
import leo.core.leoAtFile as atFile
print('new_import: %s' % atFile.new_auto)
#@+node:ekr.20140109162112.4219: *3* @test p.sort_key
aList = [p.copy() for p in c.all_positions()]
aList2 = sorted(reversed(aList),key=p.sort_key)
i = 0
for p in aList2:
    p2 = aList[i]
    i += 1
    assert p == p2,'\n%s:%s\n%s:%s' % (
        p.sort_key(p),p.h,p2.sort_key(p2),p2.h)
#@+node:ekr.20140105222052.4170: *3* @test vc.clean_nodes (to do)
vc = c.viewController
views = g.findNodeAnywhere(c,'@views')
assert views
views.deleteAllChildren()
views.b = None ####
root1 = g.findNodeInTree(c,p,'root1')
assert root1
try:
    root1.h = '@auto root1'
finally:
    root1.h = 'root1'
#@+node:ekr.20140105222052.4171: *4* root1
#@+node:ekr.20140105222052.4172: *4* root2
#@+node:ekr.20140103102956.4142: *3* @test vc.create_clone_links
vc = c.viewController
clones = g.findNodeInTree(c,p,'@clones')
root = g.findNodeInTree(c,p,'root')
node1 = g.findNodeInTree(c,p,'node1')
assert clones and root and node1
root.deleteAllChildren()
new_node1 = root.insertAsLastChild()
new_node1.h = 'node1'
clones.b = 'gnx: %s\nunl: %s\n' % (node1.v.gnx,'node1')
try:
    ok = vc.create_clone_links(clones,root)
    assert ok
    # Important: p._relinkAsCloneOf leaves new_node1 unchanged,
    # but new_node1 should not be used.
finally:
    c.redraw()
#@+node:ekr.20140103102956.4143: *4* @clones
gnx: ekr.20140211085929.5552
unl: node1
#@+node:ekr.20140211090146.4438: *4* node1
#@+node:ekr.20140103102956.4145: *4* root
#@+node:ekr.20140211090146.4438: *5* node1
#@+node:ekr.20140105185509.4140: *3* @test vc.create_organizer_node
vc = c.viewController
views = g.findNodeAnywhere(c,'@views')
if views: views.deleteAllChildren()
root_restore = g.findNodeInTree(c,p,'root_restore')
assert root_restore
root_before = g.findNodeInTree(c,p,'root_before')
assert root_before
root_after = g.findNodeInTree(c,p,'root_after')
if root_after: root_after.doDelete()
    # Root after: the results of the previous test.
c.selectPosition(root_restore)
c.copyOutline()
c.selectPosition(root_before)
c.pasteOutline()
root_after = c.p
try:
    # The roots must look like @auto nodes.
    root_before.h = '@auto root_before'
    root_after.h = '@auto root_after'
    vc.init() # Required.
    vc.update_before_write_at_auto_file(root_before)
    organizers = vc.has_at_organizers_node(root_before)
    assert organizers
    vc.create_organizer_nodes(organizers,root_after)
    ok = vc.compare_test_trees(root_before,root_after)
    assert ok,'\n\nexpected...\n%s\ngot...\n%s' % (
        vc.trial_write(root_before),vc.trial_write(root_after))
finally:
     # Make sure the roots are *not* @auto nodes.
    root_before.h = 'root_before'
    root_after.h = 'root_after'
    # Replace root_after by a copy of root_restore.
    # This ensures that the unit test can be run more than once.
    if 0:
        root_after.doDelete()
        c.selectPosition(root_restore)
        c.copyOutline()
        c.selectPosition(root_before)
        c.pasteOutline()
        assert c.p.h == 'root_restore'
        c.p.h = 'root_after'
        c.redraw()
#@+node:ekr.20140105190941.4170: *4* root_restore
@others
#@+node:ekr.20140105190941.4171: *5* aClass
class aClass:
    @others
#@+node:ekr.20140105190941.4172: *6* before
def before():
    pass
#@+node:ekr.20140105190941.4174: *6* child1
def child1():
    pass
#@+node:ekr.20140105190941.4175: *6* extra2
def extra2():
    pass
#@+node:ekr.20140105190941.4176: *6* child2
def child2():
    pass
#@+node:ekr.20140105190941.4178: *6* after
def after():
    pass
#@+node:ekr.20140105185509.4141: *4* root_before
@others
#@+node:ekr.20140105185509.4142: *5* aClass
class aClass:
    @others
#@+node:ekr.20140105185509.4143: *6* before
def before():
    pass
#@+node:ekr.20140105185509.4144: *6* organizer node
#@+node:ekr.20140105185509.4145: *7* child1
def child1():
    pass
#@+node:ekr.20140113034711.4461: *7* extra2
def extra2():
    pass
#@+node:ekr.20140105185509.4146: *7* child2
def child2():
    pass
#@+node:ekr.20140105185509.4147: *6* after
def after():
    pass
#@+node:ekr.20140211090146.4446: *4* root_after
@others
#@+node:ekr.20140211090146.4447: *5* aClass
class aClass:
    @others
#@+node:ekr.20140211090146.4448: *6* before
def before():
    pass
#@+node:ekr.20140211090146.4459: *6* organizer node
#@+node:ekr.20140211090146.4460: *7* child1
def child1():
    pass
#@+node:ekr.20140211090146.4461: *7* extra2
def extra2():
    pass
#@+node:ekr.20140211090146.4462: *7* child2
def child2():
    pass
#@+node:ekr.20140211090146.4452: *6* after
def after():
    pass
#@+node:ekr.20140106094713.4176: *3* @test vc.drop_all_organizers_in_unl
vc = c.viewController
organizer_unls = [
    'a-->O1',
    'a-->O1-->O2',
    'a-->O3',
    'a-->O3-->b-->O4',
    'z',
]
table = (
    ('z-->x','x'), # Test of dropping a leading -->.
    ('a-->O1-->unl1','a-->unl1'),
    ('a-->O1-->O2-->unl2','a-->unl2'),
    ('a-->O3-->unl3','a-->unl3'),
    ('a-->O3-->b-->O4-->unl4','a-->b-->unl4'),
)
for unl,expected in table:
    got = vc.drop_all_organizers_in_unl(organizer_unls,unl)
    assert expected == got,'\nunl:      %s\nexpected: %s\ngot:      %s' % (unl,expected,got)
#@+node:ekr.20140103102956.4146: *3* @test vc.find_absolute_unl_node
vc = c.viewController
root = c.rootPosition().insertAfter()
root.h = 'root'
child1 = root.insertAsLastChild()
child1.h = 'child1'
child2 = child1.insertAfter()
child2.h = 'child2'
child11 = child1.insertAsLastChild()
child11.h = 'child11'
try:
    for unl in ('root','root-->child1','root-->child2','root-->child1-->child11'):
        p = vc.find_absolute_unl_node(unl)
        parts = unl.split('-->')
        assert p,unl
        assert p.h == parts[-1],p.h
finally:
    root.doDelete()
    c.selectPosition(p)
    c.redraw()
#@+node:ekr.20140103102956.4165: *3* @test vc.find_at_views_node
vc = c.viewController
tag = '@views'
views = g.findNodeAnywhere(c,tag)
assert views
views2 = vc.has_at_views_node()
assert views == views2,(views,views2)
#@+node:ekr.20140103102956.4147: *3* @test vc.find_position_for_relative_unl
vc = c.viewController
parent = p.copy()
node1 = p.firstChild()
node2 = node1.next()
assert node1 and node2
child11 = node1.firstChild()
child12 = child11.next()
assert child11 and child12
child21 = node2.firstChild()
child22 = child21.next()
assert child21 and child22
table = (
    # ('node1',node1),
    ('',parent), # This special case is important.
    ('node1-->child11',child11),
    ('node1-->child12',child12),
    ('node2',node2),
    ('node2-->child21',child21),
    ('node2-->child22',child22),
    ('node3',None),
    ('node1-->childx',None),
    ('node3-->childx',None),
)
for unl,expected in table:
    got = vc.find_position_for_relative_unl(parent,unl)
    assert got == expected,'unl: %s expected: %s got: %s' % (
        unl,expected and expected.h,got and got.h)
#@+node:ekr.20140103102956.4148: *4* node1
#@+node:ekr.20140103102956.4149: *5* child11
#@+node:ekr.20140103102956.4150: *5* child12
#@+node:ekr.20140103102956.4151: *4* node2
#@+node:ekr.20140103102956.4152: *5* child21
#@+node:ekr.20140103102956.4153: *5* child22
#@+node:ekr.20140103102956.4154: *3* @test vc.find_representative_node
vc = c.viewController
root = g.findNodeInTree(c,p,'root')
assert root
root.h = '@auto root'
try:
    clone = root.next()
    assert clone and clone.h == 'clone'
    inner_clone = root.firstChild()
    assert inner_clone
    assert clone.v == inner_clone.v
    rep = vc.find_representative_node(root,inner_clone)
    # Careful: cloning this test can cause problems.
    oops = '\n  rep: %s\nparent:%s\nclone: %s\nparent:%s\ninner: %s\nparent: %s' % (
        rep,rep.parent(),clone,clone.parent(),inner_clone,inner_clone.parent())
    if True: ### p.isCloned():
        assert rep.v == clone.v and rep.parent().v == clone.parent().v,oops
    else:
        assert rep == clone
finally:
    root.h = 'root' # root must not be an @auto node.
    c.redraw()
#@+node:ekr.20140103102956.4155: *4* root
#@+node:ekr.20140103102956.4157: *5* clone
#@+node:ekr.20140103102956.4157: *4* clone
#@+node:ekr.20140103102956.4158: *3* @test vc.find_views/clones/organizers_node
# Also a test of find_at_views_node, find_at_organizers_node and find_at_clones_node.
vc = c.viewController
root = g.findNodeInTree(c,p,'root')
assert root
views = g.findNodeAnywhere(c,'@views')
assert views,'1'
if views:
    views.deleteAllChildren()
try:
    root.h = '@auto root' # root must look like an @auto node.
    views = vc.find_at_views_node()
    assert views,'2'
    views2 = vc.find_at_views_node()
    assert views2 == views
    assert vc.find_at_clones_node(root)
    assert vc.find_at_organizers_node(root)
finally:
    root.h = 'root' # Make sure root is *not* an @auto node.
    # views.deleteAllChildren()
    c.selectPosition(p)
    c.redraw()
#@+node:ekr.20140103102956.4159: *4* root
#@+node:ekr.20140103102956.4160: *5* aClass
class aClass:
    @others
#@+node:ekr.20140103102956.4164: *6* clone
#@+node:ekr.20140103102956.4162: *5* organizer node
#@+node:ekr.20140103102956.4163: *6* child2
def spam():
    pass
#@+node:ekr.20140103102956.4164: *4* clone
#@+node:ekr.20140105185509.4198: *3* @test vc.has_*_node
# Test vc.has_at_auto_view_node, vc.has_at_clones_node and vc.has_at_organizers_node.
vc = c.viewController
views = g.findNodeAnywhere(c,'@views')
if views:
    assert vc.has_at_views_node()
    views.deleteAllChildren()
else:
    assert not vc.has_at_views_node()
    views = vc.find_at_views_node()
assert views
root = g.findNodeInTree(c,p,'root')
assert root
# The representative of clone_test node must appear outside of root's tree.
clone_test = g.findNodeInTree(c,p,'clone-test')
assert clone_test
assert clone_test.v == root.next().v,(clone_test.v,root.next().v)
try:
    root.h = '@auto root' # root must look like an @auto node.
    vc.update_before_write_at_auto_file(root)
    auto_view = g.findNodeInTree(c,views,'@auto-view:root')
    assert auto_view
    auto_view2 = vc.has_at_auto_view_node(root)
    assert auto_view2
    assert auto_view2 == auto_view,(auto_view,auto_view2)
    clones = g.findNodeInTree(c,auto_view,'@clones')
    assert clones
    clones2 = vc.has_at_clones_node(root)
    assert clones2
    assert clones2 == clones
    organizers = g.findNodeInTree(c,auto_view,'@organizers')
    assert organizers
    organizers2 = vc.has_at_organizers_node(root)
    assert organizers2
    assert organizers2 == organizers
finally:
    root.h = 'root' # Make sure root is *not* an @auto node.
    c.redraw()
#@+node:ekr.20140105185509.4199: *4* root
@others
#@+node:ekr.20140105185509.4200: *5* aClass
class aClass:
    @others
#@+node:ekr.20140105185509.4204: *6* clone-test
def clone_test():
    pass
#@+node:ekr.20140105185509.4202: *6* organizer node
#@+node:ekr.20140105185509.4203: *7* child2
def spam():
    pass
#@+node:ekr.20140105185509.4204: *4* clone-test
def clone_test():
    pass
#@+node:ekr.20140103102956.4166: *3* @test vc.is_at_auto_node
vc = c.viewController
p.deleteAllChildren()
auto = p.insertAsLastChild()
auto.h = '@auto test.py'
auto2 = p.insertAsLastChild()
auto2.h = '@auto-rst test2.py'
try:
    assert vc.is_at_auto_node(auto)
    assert not vc.is_at_auto_node(auto2)
finally:
    # This is required.
    p.deleteAllChildren()
    c.redraw()
#@+node:ekr.20140103102956.4167: *3* @test vc.is_organizer_node
vc = c.viewController
redraw_flag = False
for child in p.children():
    # Add a child so the test doesn't depend on that.
    if not child.hasChildren():
        child2 = child.insertAsLastChild()
        child2.h = 'child'
        redraw_flag = True
        
    expected = child.h.strip().endswith('True')
    got = vc.is_organizer_node(child,child)
    assert expected == got,'expected: %s in: %s body...\n%s' % (
        expected,child.h,child.b)
if redraw_flag:
    c.redraw()
#@+node:ekr.20140103102956.4168: *4* test python True
@language python

# An organizer node

# Another line.
#@+node:ekr.20140103102956.4169: *5* child
#@+node:ekr.20140103102956.4170: *4* test python 2
@language python

def spam():
    pass
#@+node:ekr.20140103102956.4171: *5* child
#@+node:ekr.20140103102956.4172: *4* test html True
@language html

<!-- comment -->

<!-- comment
continued comment
-->

#@+node:ekr.20140103102956.4173: *5* child
#@+node:ekr.20140103102956.4174: *4* test html 2
@language html

<!-- comment -->

<p> oops </p>

<!-- comment
continued comment
-->

#@+node:ekr.20140103102956.4175: *5* child
#@+node:ekr.20140103102956.4176: *3* @test vc.unl
vc = c.viewController
unl = vc.unl(p)
assert unl.endswith('-->'+p.h),repr(unl)
#@+node:ekr.20140105185509.4181: *3* @test vc.update_before_write_at_auto_file
vc = c.viewController
root = g.findNodeInTree(c,p,'root')
assert root
views = g.findNodeAnywhere(c,'@views')
if views:
    views.deleteAllChildren()
try:
    root.h = '@auto root' # root must look like an @auto node.
    vc.update_before_write_at_auto_file(root)
    views = g.findNodeAnywhere(c,'@views')
    assert views
    clones = g.findNodeInTree(c,views,'@clones')
    assert clones
    assert clones.b.endswith('aClass-->clone\n'),repr(clones.b)
    organizer = g.findNodeInTree(c,views,'@organizer: organizer node')
    assert organizer
    s1 = 'unl: organizer node-->child1'
    s2 = 'unl: organizer node-->child2'
    assert organizer.b == '\n'.join([s1,s2]),organizer.b
finally:
    root.h = 'root' # Make sure root is *not* an @auto node.
    if False and views:
        views.deleteAllChildren()
    c.redraw()
#@+node:ekr.20140105185509.4182: *4* root
#@+node:ekr.20140105185509.4183: *5* aClass
class aClass:
    @others
#@+node:ekr.20140105185509.4187: *6* clone
#@+node:ekr.20140105185509.4185: *5* organizer node
#@+node:ekr.20140105185509.4216: *6* child1
#@+node:ekr.20140105185509.4186: *6* child2
def spam():
    pass
#@+node:ekr.20140105185509.4187: *4* clone
#@+node:ekr.20140103102956.4184: *3* @test view-pack & view_unpack
vc = c.viewController
views = g.findNodeAnywhere(c,'@views')
assert views
views.deleteAllChildren()
view = g.findNodeInTree(c,p,'@view test')
assert view
assert c.positionExists(view)
try:
    c.selectPosition(view)
    v_b = view.b
    vc.pack()
    assert c.p.v == view.v
    vc.unpack()
    assert view.b == v_b,view.b
    assert view.lastChild().isCloned()
finally:
    # views.deleteAllChildren()
    c.undoer.clearUndoState()
    c.redraw()
#@+node:ekr.20140103102956.4188: *4* clone
clone body
#@+node:ekr.20140103102956.4186: *4* @view test
view body
#@+node:ekr.20140103102956.4187: *5* not a clone
not a clone text
#@+node:ekr.20140103102956.4188: *5* clone
clone body
#@+node:ekr.20140106135225.4203: *3* @test vc.create_organizer_node (nested organizers)
vc = c.viewController
views = g.findNodeAnywhere(c,'@views')
if views: views.deleteAllChildren()
    # Start with a pristine @views tree.
root_before  = g.findNodeInTree(c,p,'root_before')
    # Root before: used to generate @auto-view tree.
root_restore = g.findNodeInTree(c,p,'root_restore')
    # Create root_after from root_restore.
assert root_before,root_restore
root_after  = g.findNodeInTree(c,p,'root_after')
if root_after: root_after.doDelete()
    # Root after: the results of the previous test.
c.selectPosition(root_restore)
c.copyOutline()
c.selectPosition(root_before)
c.pasteOutline()
# The roots must look like @auto nodes.
root_after = c.p
assert root_after.h == 'root_restore',root_after.h
root_before.h = '@auto root_before'
root_after.h = '@auto root_after' 
try:
    vc.init() # Required.
    vc.update_before_write_at_auto_file(root_before)
    at_organizers = vc.has_at_organizers_node(root_before)
    assert at_organizers
    vc.create_organizer_nodes(at_organizers,root_after)
    ok = vc.compare_test_trees(root_before,root_after)
    assert ok,'\n\nexpected...\n%s\ngot...\n%s' % (
        vc.trial_write(root_before),vc.trial_write(root_after))
finally:
     # Make sure the roots are *not* @auto nodes.
    root_before.h = 'root_before'
    root_after.h = 'root_after'
    c.redraw()
#@+node:ekr.20140106135225.4204: *4* root_restore
@others
#@+node:ekr.20140106135225.4285: *5* aClass
class aClass:
    @others
#@+node:ekr.20140106135225.4286: *6* before
def before(self):
    pass
#@+node:ekr.20140106135225.4289: *6* child11
def child11(self):
    pass
#@+node:ekr.20140106135225.4290: *6* child12
def child12(self):
    pass
#@+node:ekr.20140108143431.4248: *6* middle
def middle():
    pass
#@+node:ekr.20140106135225.4292: *6* child21
def child21(self):
    pass
#@+node:ekr.20140106135225.4293: *6* child22
def child22(self):
    pass
#@+node:ekr.20140106135225.4294: *6* after
def after(self):
    pass
#@+node:ekr.20140109035139.4277: *6* last1
def last1():
    pass
#@+node:ekr.20140109180433.4248: *5* top1
def top1():
    pass
#@+node:ekr.20140109180433.4249: *5* top2
def top2():
    pass
#@+node:ekr.20140106135225.4213: *4* root_before
@others
#@+node:ekr.20140106135225.4214: *5* aClass
class aClass:
    @others
#@+node:ekr.20140106135225.4215: *6* before
def before(self):
    pass
#@+node:ekr.20140106135225.4216: *6* organizer node
#@+node:ekr.20140106135225.4229: *7* inner org1
#@+node:ekr.20140106135225.4217: *8* child11
def child11(self):
    pass
#@+node:ekr.20140106135225.4218: *8* child12
def child12(self):
    pass
#@+node:ekr.20140108143431.4249: *7* middle
def middle():
    pass
#@+node:ekr.20140106135225.4233: *7* inner org2
#@+node:ekr.20140106135225.4234: *8* child21
def child21(self):
    pass
#@+node:ekr.20140106135225.4235: *8* child22
def child22(self):
    pass
#@+node:ekr.20140106135225.4219: *6* after
def after(self):
    pass
#@+node:ekr.20140109035139.4274: *6* last organizer
#@+node:ekr.20140109035139.4276: *7* last1
def last1():
    pass
#@+node:ekr.20140109180433.4253: *5* top-level
#@+node:ekr.20140109180433.4254: *6* top1
def top1():
    pass
#@+node:ekr.20140109180433.4255: *6* top2
def top2():
    pass
#@+node:ekr.20140211090146.4494: *4* root_after
@others
#@+node:ekr.20140211090146.4495: *5* aClass
class aClass:
    @others
#@+node:ekr.20140211090146.4496: *6* before
def before(self):
    pass
#@+node:ekr.20140211090146.4516: *6* organizer node
#@+node:ekr.20140211090146.4518: *7* inner org1
#@+node:ekr.20140211090146.4526: *8* child11
def child11(self):
    pass
#@+node:ekr.20140211090146.4527: *8* child12
def child12(self):
    pass
#@+node:ekr.20140211090146.4525: *7* middle
def middle():
    pass
#@+node:ekr.20140211090146.4520: *7* inner org2
#@+node:ekr.20140211090146.4528: *8* child21
def child21(self):
    pass
#@+node:ekr.20140211090146.4529: *8* child22
def child22(self):
    pass
#@+node:ekr.20140211090146.4502: *6* after
def after(self):
    pass
#@+node:ekr.20140211090146.4522: *6* last organizer
#@+node:ekr.20140211090146.4530: *7* last1
def last1():
    pass
#@+node:ekr.20140111164124.4414: *3* @test vc.create_organizer_node (intermediate organizers)
vc = c.viewController
views = g.findNodeAnywhere(c,'@views')
if views: views.deleteAllChildren()
    # Start with a pristine @views tree.
root_before  = g.findNodeInTree(c,p,'root_before')
    # Root before: used to generate @auto-view tree.
root_restore = g.findNodeInTree(c,p,'root_restore')
    # Create root_after from root_restore.
assert root_before,root_restore
root_after  = g.findNodeInTree(c,p,'root_after')
if root_after: root_after.doDelete()
    # Root after: the results of the previous test.
c.selectPosition(root_restore)
c.copyOutline()
c.selectPosition(root_before)
c.pasteOutline()
# The roots must look like @auto nodes.
root_after = c.p
assert root_after.h == 'root_restore',root_after.h
root_before.h = '@auto root_before'
root_after.h = '@auto root_after' 
try:
    vc.init() # Required.
    vc.update_before_write_at_auto_file(root_before)
    at_organizers = vc.has_at_organizers_node(root_before)
    assert at_organizers
    vc.create_organizer_nodes(at_organizers,root_after)
    ok = vc.compare_test_trees(root_before,root_after)
    assert ok,'\n\nexpected...\n%s\ngot...\n%s' % (
        vc.trial_write(root_before),vc.trial_write(root_after))
finally:
     # Make sure the roots are *not* @auto nodes.
    root_before.h = 'root_before'
    root_after.h = 'root_after'
    c.redraw()
#@+node:ekr.20140111164124.4415: *4* root_restore
@others
#@+node:ekr.20140111164124.4416: *5* aClass
class aClass:
    @others
#@+node:ekr.20140111164124.4417: *6* before
def before(self):
    pass
#@+node:ekr.20140111164124.4418: *6* child11
def child11(self):
    pass
#@+node:ekr.20140111164124.4419: *6* child12
def child12(self):
    pass
#@+node:ekr.20140111164124.4420: *6* middle
def middle():
    pass
#@+node:ekr.20140111164124.4421: *6* child21
def child21(self):
    pass
#@+node:ekr.20140111164124.4422: *6* child22
def child22(self):
    pass
#@+node:ekr.20140111164124.4423: *6* after
def after(self):
    pass
#@+node:ekr.20140111164124.4424: *6* last1
def last1():
    pass
#@+node:ekr.20140111164124.4425: *5* top1
def top1():
    pass
#@+node:ekr.20140111164124.4426: *5* top2
def top2():
    pass
#@+node:ekr.20140111164124.4427: *4* root_before
@others
#@+node:ekr.20140111164124.4428: *5* aClass
class aClass:
    @others
#@+node:ekr.20140111164124.4429: *6* before
def before(self):
    pass
#@+node:ekr.20140111164124.4430: *6* organizer node
#@+node:ekr.20140111164124.4431: *7* inner org1
#@+node:ekr.20140111164124.4432: *8* child11
def child11(self):
    pass
#@+node:ekr.20140111164124.4433: *8* child12
def child12(self):
    pass
#@+node:ekr.20140111164124.4434: *7* middle
def middle():
    pass
#@+node:ekr.20140111164124.4435: *7* inner org2
#@+node:ekr.20140111164124.4436: *8* child21
def child21(self):
    pass
#@+node:ekr.20140111164124.4437: *8* child22
def child22(self):
    pass
#@+node:ekr.20140111164124.4438: *6* after
def after(self):
    pass
#@+node:ekr.20140111164124.4439: *6* last organizer
#@+node:ekr.20140111164124.4440: *7* last1
def last1():
    pass
#@+node:ekr.20140111164124.4441: *5* top-level
#@+node:ekr.20140111164124.4442: *6* top1
def top1():
    pass
#@+node:ekr.20140111164124.4443: *6* top2
def top2():
    pass
#@+node:ekr.20140211090146.4545: *4* root_after
@others
#@+node:ekr.20140211090146.4546: *5* aClass
class aClass:
    @others
#@+node:ekr.20140211090146.4547: *6* before
def before(self):
    pass
#@+node:ekr.20140211090146.4567: *6* organizer node
#@+node:ekr.20140211090146.4569: *7* inner org1
#@+node:ekr.20140211090146.4577: *8* child11
def child11(self):
    pass
#@+node:ekr.20140211090146.4578: *8* child12
def child12(self):
    pass
#@+node:ekr.20140211090146.4576: *7* middle
def middle():
    pass
#@+node:ekr.20140211090146.4571: *7* inner org2
#@+node:ekr.20140211090146.4579: *8* child21
def child21(self):
    pass
#@+node:ekr.20140211090146.4580: *8* child22
def child22(self):
    pass
#@+node:ekr.20140211090146.4553: *6* after
def after(self):
    pass
#@+node:ekr.20140211090146.4573: *6* last organizer
#@+node:ekr.20140211090146.4581: *7* last1
def last1():
    pass
#@+node:ekr.20131111160618.4261: ** @ignore leoVim
#@+node:ekr.20131111162157.4276: *3* Unused
#@+node:ekr.20131111162157.4270: *4* @@test command regex
@language python
# http://docs.python.org/2/library/re.html
import re
# g.cls()
trace = False
n =     r'(?P<n>[0-9]*)'    # Optional digits
cmd =   r'(?P<cmd>[^0-9]+)' # Required: anything *except* digits.
n2 =    r'(?P<n2>[0-9]*)'   # Optional digits
cmd2 =  r'(?P<cmd2>[a-zA-Z]?)' # Optional letter.
n_c = n+cmd+n2+cmd2
tables = (
    (n_c,('35N','N','2d2','d2d','gg',)),
)
for pat,aList in tables:
    fields = re.findall('\(\?P<([a-z_A-Z0-9]+)>',pat)
    if trace: print('pattern: %s\n fields: %s' % (pat,','.join(fields)))
    for s in aList:
        if trace: print('  %s' % s)
        m = re.search(pat,s)
        for field in fields:
            try:
                val = m.group(field)
            except Exception:
                g.es_exception()
                val = None
            if trace: print('    %7s %s' % (field,val or 'None'))
#@+node:ekr.20131111162157.4272: *4* @@test motion regex
@language python

# http://docs.python.org/2/library/re.html
import re
# g.cls()

def escape(ch):
    return ch if ch.isalnum() else '\\%s' % ch
# Not yet.
# N /<CR> (motion) repeat last search, in the forward direction

# 0 (motion) to first character in the line (also: <Home> key)
# ^ (motion) go to first non-blank character in the line
# % (motion) find the next brace, bracket, comment,
#            or "#if"/ "#else"/"#endif" in this line and go to its match
plain_motion_chars = '0^%'
plain_motion =   '|'.join([escape(ch) for ch in plain_motion_chars])
# N + (motion) down N lines, on the first non-blank character (also: CTRL-M and <CR>)
# N _ (motion) down N-1 lines, on the first non-blank character
# N - (motion) up N lines, on the first non-blank character
# N , (motion) repeat the last "f", "F", "t", or "T" N times in opposite direction
# N ; (motion) repeat the last "f", "F", "t", or "T" N times
# N ( (motion) N sentences backward
# N ) (motion) N sentences forward
# N { (motion) N paragraphs backward
# N } (motion) N paragraphs forward
# N | (motion) to column N (default: 1)
# N $ (motion) go to the last character in the line (N-1 lines lower) (also: <End> key)
# N % (motion) goto line N percentage down in the file.  N must be given, otherwise it is the % command.
# N # (motion) search backward for the identifier under the cursor
# N * (motion) search forward for the identifier under the cursor
n_motion_chars = '+_-,;(){}|$%#*'
n_motion_alts = ' | '.join([escape(ch) for ch in n_motion_chars])
# N [#  (motion) N times back to unclosed "#if" or "#else"
# N [(  (motion) N times back to unclosed '('
# N [*  (motion) N times back to start of comment "/*"
# N [[  (motion) N sections backward, at start of section
# N []  (motion) N sections backward, at end of section
# N [{  (motion) N times back to unclosed '{'
open_bracket_chars  = ['[%s' % (ch) for ch in '#(*[]{']
# N ]#  (motion) N times forward to unclosed "#else" or "#endif"
# N ])  (motion) N times forward to unclosed ')'
# N ]*  (motion) N times forward to end of comment "*/"
# N ][  (motion) N sections forward, at end of section
# N ]]  (motion) N sections forward, at start of section
# N ]}  (motion) N times forward to unclosed '}'
close_bracket_chars = [']%s' % (ch) for ch in '#)*[]}']
bracket_chars = open_bracket_chars + close_bracket_chars
bracket_motion = ' | '.join(['%s%s' % (escape(s[0]),escape(s[1])) for s in bracket_chars])
bracket_alts =      r'(?P<bracket_alt>%s)' % (bracket_motion)
# print(bracket_alts)
# gD (motion) goto global declaration of identifier under the cursor
# gd (motion) goto local declaration of identifier under the cursor
g_bare_alts =   r'(?P<bare_g_alts>(gD|gd))'
# N g^      (motion) to first non-blank character in screen line (differs from "^" when lines wrap)
# N g#      (motion) like "#", but also find partial matches
# N g$      (motion) to last character in screen line (differs from "$" when lines wrap)
# N g*      (motion) like "*", but also find partial matches
# N g0      (motion) to first character in screen line (differs from "0" when lines wrap)
# N gE      (motion) backward to the end of the Nth blank-separated WORD
# N ge      (motion) backward to the end of the Nth word
# N gg      (motion) goto line N (default: first line), on the first non-blank character
# N gj      (motion) down N screen lines (differs from "j" when line wraps)
# N gk      (motion) up N screen lines (differs from "k" when line wraps)
g_alt_chars =   ' | '.join([escape(ch) for ch in '^#$*0Eegjk'])
g_alts =        r'(?P<g_n>[0-9]*)(?P<g_alt>g(%s))' % (g_alt_chars)
g_motion = 'g(%s | %s)' % (g_bare_alts,g_alts)
print(g_motion)
#@+node:ekr.20131111162157.4271: *4* @@test vim motion
@language python
# g.cls()

# Unknown:
# N   H  (motion?) go to the Nth line in the window, on the first non-blank
# N   J  (motion?) join N-1 lines (delete newlines)
# VIS J  (motion?) join the highlighted lines
    # M  (motion?) go to the middle line in the window, on the first non-blank
# N   L  (motion?) go to the Nth line from the bottom, on the first non-blank
# o      (motion?) exchange cursor position with start of highlighting

# Not used:
# N %    goto line N percentage down in the file.
#        N must be given, otherwise it is the % command.

#   0    to first character in the line (also: <Home> key)
#   ^    go to first non-blank character in the line
#   %    find the next brace, bracket, comment,
#        or "#if"/ "#else"/"#endif" in this line and go to its match
# N +    down N lines, on the first non-blank character (also: CTRL-M and <CR>)
# N _    down N-1 lines, on the first non-blank character
# N -    up N lines, on the first non-blank character
# N ,    repeat the last "f", "F", "t", or "T" N times in opposite direction
# N ;    repeat the last "f", "F", "t", or "T" N times
# N (    N sentences backward
# N )    N sentences forward
# N {    N paragraphs backward
# N }    N paragraphs forward
# N |    to column N (default: 1)
# N $    go to the last character in the line (N-1 lines lower) (also: <End> key)
# N #    search backward for the identifier under the cursor
# N *    search forward  for the identifier under the cursor
# N B    N blank-separated WORDS backward
# N E    forward to the end of the Nth blank-separated WORD
# N G    goto line N (default: last line), on the first non-blank character
# N N    repeat last search, in opposite direction
# N W    N blank-separated WORDS forward
# N b    N words backward
# N e    forward to the end of the Nth word
# N h    left (also: CTRL-H, <BS>, or <Left> key)
# N j    down N lines (also: CTRL-J, CTRL-N, <NL>, and <Down>)
# N k    up N lines (also: CTRL-P and <Up>)
# N l    right (also: <Space> or <Right> key)
# N n    repeat last search
# N w    N words forward
single_char_motions = [ch for ch in '0^%_+-,;(){}|$#*BEGNWbehjklnw']
# N [#   N times back to unclosed "#if" or "#else"
# N [(   N times back to unclosed '('
# N [*   N times back to start of comment "/*"
# N [[   N sections backward, at start of section
# N []   N sections backward, at end of section
# N [{   N times back to unclosed '{'
m1  = ['['+ ch for ch in '#(*[]{']
# N ]#   N times forward to unclosed "#else" or "#endif"
# N ])   N times forward to unclosed ')'
# N ]*   N times forward to end of comment "*/"
# N ][   N sections forward, at end of section
# N ]]   N sections forward, at start of section
# N ]}   N times forward to unclosed '}'
m2 = [']'+ch for ch in '#)*[]}']
#   gD   goto global declaration of identifier under the cursor
#   gd   goto local declaration of identifier under the cursor
# N g^   to first non-blank character in screen line (differs from "^" when lines wrap)
# N g#   like "#", but also find partial matches
# N g$   to last character in screen line (differs from "$" when lines wrap)
# N g*   like "*", but also find partial matches
# N g0   to first character in screen line (differs from "0" when lines wrap)
# N gE   backward to the end of the Nth blank-separated WORD
# N ge   backward to the end of the Nth word
# N gg   goto line N (default: first line), on the first non-blank character
# N gj   down N screen lines (differs from "j" when line wraps)
# N gk   up N screen lines (differs from "k" when line wraps)
m3 = ['g'+ch for ch in '^#$*0DEdegjk']
# N /<CR>  repeat last search, in the forward direction
m4 = ['/\\n',]
# N F<char>  to the Nth occurrence of <char> to the left
# N T<char>  till before the Nth occurrence of <char> to the left
# N f<char>  to the Nth occurrence of <char> to the right
# N t<char>  till before the Nth occurrence of <char> to the right
char_motions = [ch for ch in 'FTft']
multi_char_leadins = '/g[]'
multi_char_motions = m1+m2+m3+m4
print('\n'.join(single_char_motions))
print('\n'.join(multi_char_motions))
print('\n'.join(['%s<char>' % (ch) for ch in char_motions]))
#@+node:ekr.20131111155107.4239: *3* @test h middle of line
c.testManager.runVimTest(p)

#@+node:ekr.20131111155107.4240: *4* work
first line
#@+node:ekr.20131111155107.4241: *4* before sel=1.5,1.5
first line
#@+node:ekr.20131111155107.4242: *4* after sel=1.4,1.4
first line
#@+node:ekr.20131111162157.4275: *3* @test vr.exec_
import leo.core.leoVim as leoVim
if 0: # When running from leoPy.leo
    import imp
    imp.reload(leoVim)
vc = leoVim.VimCommands(c)
table = (
    # 'gg','gk','#','dd','d3j',
    'h', # works
    # 'l', # works
    # 'j', # Not yet.
    # 'ggg',
)
for s in table:
    status,n1,command,n2,motion = vc.scan(s)
    # print('status',status,'command',command)
    if status == 'done':
        vc.exec_(command,n1,n2,motion)
    else:
        print('status: %s %s' % (status,s))
        vc.command = s
        vc.n1 = n1
        vc.n2 = n2
        vc.motion = motion
        vc.oops()

if g.unitTesting:
    # Unit testing messes up the focus.
    vc.runAtIdle(c.bodyWantsFocusNow)
#@+node:ekr.20131113071911.4281: *3* @test vr.scan
import leo.core.leoVim as leoVim
if 0: # When running from leoPy.leo
    import imp
    imp.reload(leoVim)
import time
trace = False
trace_time = False
<< define test tables >>
vc = leoVim.VimCommands(c)
test_table = (
    ('done',complete_table),
    ('scan',incomplete_table),
    ('oops',error_table),
)
if trace_time: t1 = time.clock()
n = 0
for i in range(1):
    for expected,table in test_table:
        for s in table:
            if table == complete_table:
                command = s
                for expected,command2 in vc.simulate_typing(command):
                    status,n1,command3,n2,motion = vc.scan(command2)
                    n += 1
                    if trace:
                        err = '   ' if status == expected else '***'
                        print('%s%s %s' % (err,status,command2))
                    else:
                        assert status == expected,'expected %s, got %s command: %s' % (
                        expected,status,command2)
            else:
                for prefix in ('','1023456789'):
                    command = prefix + s
                    status,n1,command2,n2,motion = vc.scan(command)
                    n += 1
                    if trace:
                        err = '   ' if status == expected else '***'
                        print('%s%s %s' % (err,status,command))
                    else:
                        assert status == expected,'expected %s, got %s command: %s' % (
                        expected,status,command)
if trace_time:
    delta = time.clock()-t1
    print("%s %6.6f sec." % (n,delta/n))

#@+node:ekr.20131113071911.4282: *4* << define test tables >>
# To do: handle d2d, 2dd, etc.
if 0: # Individual test:
    complete_table = ('ta',)
        # Note: gu is complete, so gu[] is an invalid test.
    incomplete_table = () # 'd3','d4t','dt',
    error_table = ()
else:
    complete_table = (
        '0',
        'N',
        '#',
        'gg','gk','dd',
        'd3j',
        '2dta', # d is not (yet) a motion
        'dFb',
        'gu',
        'g[]',
        'ta',
        't!',
    )
    incomplete_table = (
        'g',
        '[',
        ']',
        '25',
        'd3t',
        'd3',
    )
    error_table = (
        'gX','ZA',
    )
#@+node:ekr.20080703104536.1: ** @ignore mini tests
#@+node:ekr.20100203163606.5365: *3* 2to3 script
import os

def run(files):
    args = [r'python c:\python26\Tools\Scripts\2to3.py']
    for z in files:
        args.append(z)
        # args.append('-xprint')
    args.append('>out2')
    args = ','.join(args)
    os.system(args)

tkPass = (
    'EditAttributes','Library',
    'URLloader','UniversalScrolling','UASearch',
    'autotrees','chapter_hoist','cleo','dump_globals',
    'expfolder','geotag','graphed','groupOperations',
    'hoist','import_cisco_config',
    'keybindings','leoupdate',
    'maximizeNewWindows', 'mnplugins','mod_labels',
    'mod_read_dir_outline','mod_tempfname','multifile',
    'newButtons','nodeActions','nodenavigator',
    'open_with','pie_menus','pluginsTest',
    'read_only_nodes','rClick',
    'scheduler','searchbar','searchbox','shortcut_button',
    'script_io_to_body','searchbox',
    'templates','textnode','tkGui','toolbar',
    'xcc_nodes',
)

passList = (
    '__init__','FileActions','UNL',
    'active_path','add_directives','attrib_edit',
    'backlink','base64Packager','baseNativeTree','bibtex','bookmarks',
    'codewisecompleter','colorize_headlines','contextmenu',
    'ctagscompleter','cursesGui','datenodes','debugger_pudb',
    'detect_urls','dtest','empty_leo_file','enable_gc','initinclass',
    'leo_to_html','leo_interface','leo_pdf','leo_to_rtf',
    'leoOPML','leoremote','lineNumbers',
    'macros','mime','mod_autosave','mod_framesize','mod_leo2ascd',
    'mod_scripting','mod_speedups','mod_timestamp',
    'nav_buttons','nav_qt','niceNosent','nodeActions','nodebar',
    'open_shell','outline_export','quit_leo',
    'paste_as_headlines','plugins_menu','pretty_print','projectwizard',
    'qt_main','qt_quicksearch','qtframecommands',
    'quickMove',
        # Warning: changed this line by guessing!
        # func = types.MethodType(func, quickMove)
    'quicksearch','redirect_to_log','rClickBasePluginClasses',
    'run_nodes', # Changed thread.allocate_lock to threading.lock().acquire()
    'rst3',
    'scrolledmessage','setHomeDirectory','slideshow','spydershell','startfile',
    'testRegisterCommand','todo','trace_gc_plugin','trace_keys','trace_tags',
    'vim','xemacs',
)
core_files = (
    'leoApp','leoAtFile','leoCache','leoChapters','leoCommands',
    'leoEditCommands','leoFileCommands','leoFind','leoFrame',
    'leoGlobals','leoGui','leoImport','leoMenu','leoNodes',
    'leoPlugins','leoShadow','leoTangle','leoUndo',
)
external_files = (
    'ipy_leo','lproto',
)
table = (
    ('plugins',passList),
    ('plugins',tkPass),
    ('core',core_files),
    ('external',external_files),
)
files = []
for theDir,aList in table:
    for z in aList:
        if not z.endswith('.py'): z = z + '.py'
        # print(z)
        fn = os.path.abspath(os.path.join('leo',theDir,z))
        if os.path.exists(fn): files.append(fn)
        else: print('*** file not found:',fn)

run(files)
print('done: results are in out2')
#@+node:ekr.20100127162342.5123: *3* Import all plugins script
import glob,os

tkPass = (
    'EditAttributes','Library',
    'URLloader','UniversalScrolling','UASearch',
    'autotrees','chapter_hoist','cleo','dump_globals',
    'expfolder','geotag','graphed','groupOperations',
    'hoist','import_cisco_config',
    'keybindings','leoupdate',
    'maximizeNewWindows', 'mnplugins','mod_labels',
    'mod_read_dir_outline','mod_tempfname','multifile',
    'newButtons','nodeActions','nodenavigator',
    'open_with','pie_menus','pluginsTest',
    'read_only_nodes','rClick',
    'scheduler','searchbar','searchbox','shortcut_button',
    'script_io_to_body','searchbox',
    'templates','textnode','tkGui','toolbar',
    'xcc_nodes',
)
tkPassWithProblems = (
    'at_view', # at_view plugin not loaded: win32Clipboard not present.
    'image', # can not import ImageTk.
    'table', # failed to import 'tktable'
    'xsltWithNodes', # Can not import Ft from plugin leo.plugins.xsltWithNodes.
)
tkFail = (
    'ConceptualSort','at_produce','autocompleter','rowcol',
)
passList = (
    '__init__','FileActions','UNL',
    'active_path','add_directives','attrib_edit',
    'backlink','base64Packager','baseNativeTree','bibtex','bookmarks',
    'codewisecompleter','colorize_headlines','contextmenu',
    'ctagscompleter','cursesGui','datenodes','debugger_pudb',
    'detect_urls','dtest','empty_leo_file','enable_gc','initinclass',
    'leo_to_html','leo_interface','leo_pdf','leo_to_rtf',
    'leoOPML','leoremote','lineNumbers',
    'macros','mime','mod_autosave','mod_framesize','mod_leo2ascd',
    'mod_scripting','mod_speedups','mod_timestamp',
    'nav_buttons','nav_qt','niceNosent','nodeActions','nodebar',
    'open_shell','outline_export','quit_leo',
    'paste_as_headlines','plugins_menu','pretty_print','projectwizard',
    'qt_main','qt_quicksearch','qtframecommands',
    'quickMove',
        # Warning: changed this line by guessing!
        # func = types.MethodType(func, quickMove)
    'quicksearch','redirect_to_log','rClickBasePluginClasses',
    'run_nodes', # Changed thread.allocate_lock to threading.lock().acquire()
    'rst3',
    'scrolledmessage','setHomeDirectory','slideshow','spydershell','startfile',
    'testRegisterCommand','todo','trace_gc_plugin','trace_keys','trace_tags',
    'vim','xemacs',
)
passWithImportProblems = ( # Other than tk input problems.
    'ipython','word_export',
)
dead = (
    'at_folder','exampleTemacsExtension','ironPythonGui','LeoN',
    'rst2','swing_gui','temacs','usetemacs','wxGui',)
error = ( # Real errors with tracebacks.
)
fail = (
    'stickynotes_plus', # requires markdown.
    'zenity_file_dialogs', # requires zenity, and probably ubuntu.
)
noAttribute = (
    # AttributeError: 'module' object has no attribute <module name>
    # This was a sign of a missing init top-level function.
)
changed = (
    'LeoN',
)
plugins = g.os_path_abspath(g.os_path_join(
    g.app.loadDir,'..','plugins','*.py'))
files = glob.glob(plugins)
files.sort()
os.system('cls') # Clear the screen on windows.
for fn in files:
    m = g.shortFileName(fn)[:-3]
    # Change the next line to choose different plugins.
    if m in passList:
        try:
            __import__('leo.plugins.%s' % m)
            if 1: print('pass %s' % m)
        except ImportError:
            if 1: print('FAIL %s' % m)
        except Exception:
            if 1: g.es_exception()
            if 1: print('error %s' % m)
#@+node:ekr.20051104081502.99: *3* Manual tests...
#@+node:ekr.20051104081502.101: *4* Other Reformat Paragraph tests
@language plain
@pagewidth 40

A one-line paragraph one two three four five six seven eight nine ten...

An @rawfile tree is a tree whose root headline starts with
@rawfile <filename>. Similarly, an @silentfile tree is a
tree whose root headline starts with an @silentfile
<filename> directive.

    Leo creates derived files from @rawfile and @silentfile trees by writing the body text of all nodes of the tree in outline order.  Leo writes the body text _as is_, without recognizing section definitions, without expanding section references, and without treating directives specially in any way.  In particular, Leo copies all directives, including @space or @c directives, to the derived file as text. Exception: Leo recognizes the @ignore directive in @rawfile or @silentfile nodes, so you may use the @ignore directive as usual to prevent Leo from writing @rawfile and @silentfile trees.

There are several difference between @rawfile and @silentfile trees:

  This
  is
  a
  test.

  1. This is the first line and it is really really really long. And it has
     a hanging indentation.
     and another line.

  2. This is a lllllllllllllllllllllllllllllllllllloooooooooooooooooong
     next item.
     And it too has a hanging indentation.

  3. This is an exxxxxxxxxxxxxxxxxxxxxxxxtrrrrrrrrrrrrrrreeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeemlylong
     word.
And it too has a hanging indentation.
#@+node:ekr.20051104081502.102: *4* Test of @tabwidth
@tabwidth -4
@language plain


    a   b   c
a   b   c   d
aa  b   c   d
aaa b   c   d
end
#@+node:ekr.20051104081502.103: *4* Test of pasting into big node
@killcolor
@language plain

Note: Previously, one could crash Leo by pasting a large text into a headline.  Leo now truncates that text, and furthermore Leo no longer makes all headline text into one gigantic line.  Therefore, we don't have to test Tk's ability to handle super-long lines.

The test:  Copy the following and paste it into a headline.  Leo should give 2 truncation messages:

- Truncating headline to one line.
- Truncating headline to 250 characters.

About a year ago I found the website at www.literateprogamming.com and was immediately convinced that the basic idea of Literate Programming is an important breakthrough. At the time I was working on a contract trying to decipher a true masterpiece of over-engineering, and if at any time during the construction of this masterpiece the perpetrators had been required to explain themselves in English, my client would have saved millions of dollars.

I never did try CWEB or NOWEB though, because on the literate programming site I read about a tool named “Leo” that combined outlines with Literate Programming techniques. Since I’ve always found outlining tools very useful I downloaded and tried this. I found that using this tool completely changed my programming practice and brought out all of the power inherent in the original Literate Programming idea as I understood it.
#@+node:ekr.20051104081502.104: *4* Test of Remove sentinels
import os

g.pr(os.getcwd())
name = g.os_path_join("test","removeSentTest.txt")
c.importCommands.removeSentinelsCommand(name)
#@+node:ekr.20051104081502.105: *4* Test new docutils stull
import glob

g.pr('-' * 40)

tm = c.testManager

if 0:
    g.pr("modules in test.leo...")
    paths = tm.findAllAtFileNodes(c)
    modules = tm.importAllModulesInPathList(paths)
    for module in modules:
        g.pr(module)

if 1:
    g.pr("modules in leo/src...")
    path = g.os_path_join(g.app.loadDir,"..","src")
    modules = tm.importAllModulesInPath(path)
    for module in modules:
        g.pr(module)

if 0:
    directory = g.os_path_join(g.app.loadDir,"..","src")
    glob_path = g.os_path_join(directory,"leo*.py")
    files = glob.glob(glob_path)
    modules = tm.importAllModulesInPathList(files)
    for module in modules:
        g.pr(module)
#@+node:ekr.20040712101754.221: *4* Manual test of TM.replaceOutline
outline1 = p.firstChild()
outline2 = outline1.next()
assert(outline1.h=="outline1")
assert(outline2.h=="outline2")

c.testManager.replaceOutline(outline1,outline2)
c.redraw()
c.checkOutline()
#@+node:ekr.20040712101754.222: *5* outline1
#@+node:ekr.20040712101754.223: *6* a
#@+node:ekr.20040712101754.224: *5* outline2
#@+node:ekr.20040712101754.225: *6* b
#@+node:ekr.20051104081502.220: *3* Mini test arguments to hooks
"""Mini test that documentation of hooks in leoDocs.leo is correct.

hookData should match that documentation for this test to be effective.

This is not a complete unit test:  it does not force executions of all hooks.
"""

<< imports >>
<< define hookData >>
<< define typeData >>
checked = [] # List of all hooks that have been checked.

@others

tags = [] 
for name,args in hookData:
    tags.append(name)
    << define checkHook >>
    leoPlugins.registerHandler(name,checkHook)

if 0: # print all hooks.
    handlers = leoPlugins.getHandlersForTag(tags)
    if handlers:
        g.pr("-" * 20)
        for h in handlers:
            g.pr(h)
#@+node:ekr.20051104081502.221: *4* << imports >>
import leoColor
import leoCommands
import leoNodes
import leoPlugins
import leoTkinterTree

import types
import Tkinter as Tk
#@+node:ekr.20051104081502.222: *4* << define hookData >>
hookData = (
    ("bodyclick1",   ("c","p","v","event")),
    ("bodyclick2",   ("c","p","v","event")),
    ("bodydclick1",  ("c","p","v","event")),
    ("bodydclick2",  ("c","p","v","event")),
    ("bodykey1",     ("c","p","v","ch","oldSel","undoType")),
    ("bodykey2",     ("c","p","v","ch","oldSel","undoType")),
    ("bodyrclick1",  ("c","p","v","event")),
    ("bodyrclick2",  ("c","p","v","event")),
    ("boxclick1",    ("c","p","v","event")),
    ("boxclick2",    ("c","p","v","event")),
    ("command1",     ("c","p","v","label")),
    ("command2",     ("c","p","v","label")),
    ("drag1",        ("c","p","v","event")),
    ("drag2",        ("c","p","v","event")),
    ("dragging1",    ("c","p","v","event")),
    ("dragging2",    ("c","p","v","event")),
    ("end1",         None),
    ("enddrag1",     ("c","p","v","event")),
    ("enddrag2",     ("c","p","v","event")),
    ("headclick1",   ("c","p","v","event")),
    ("headclick2",   ("c","p","v","event")),
    ("headrclick1",  ("c","p","v","event")),
    ("headrclick2",  ("c","p","v","event")),
    ("headkey1",     ("c","p","v","ch")),
    ("headkey2",     ("c","p","v","ch")),
    ("hypercclick1", ("c","p","v","event")),
    ("hypercclick2", ("c","p","v","event")),
    ("hyperenter1",  ("c","p","v","event")),
    ("hyperenter2",  ("c","p","v","event")),
    ("hyperleave1",  ("c","p","v","event")),
    ("hyperleave2",  ("c","p","v","event")),
    ("iconclick1",   ("c","p","v","event")),
    ("iconclick2",   ("c","p","v","event")),
    ("iconrclick1",  ("c","p","v","event")),
    ("iconrclick2",  ("c","p","v","event")),
    ("icondclick1",  ("c","p","v","event")),
    ("icondclick2",  ("c","p","v","event")),
    ("idle",         ("c",)),
    ("menu1",        ("c","p","v")),
    ("menu2",        ("c","p","v")),
    ("open1",        ("old_c","new_c","fileName")),
    ("open2",        ("old_c","new_c","fileName")),
    ("openwith1",    ("c","p","v","openType","arg","ext")),
    ("openwith2",    ("c","p","v","openType","arg,ext" )),
    ("recentfiles1", ("c","p","v","fileName","closeFlag")),
    ("recentfiles2", ("c","p","v","fileName","closeFlag")),
    ("save1",        ("c","p","v","fileName" )),
    ("save2",        ("c","p","v","fileName" )),
    ("select1",      ("c","new_p","old_p","new_v","old_v")),
    ("select2",      ("c","new_p","old_p","new_v","old_v")),
    ("select3",      ("c","new_p","old_p","new_v","old_v")),
    ("set-mark",     ("c","p","v")),
    ("start1",       None),
    ("start2",       ("c","p","v","fileName" )),
    ("unselect1",    ("c","new_p","old_p","new_v","old_v")),
    ("unselect2",    ("c","new_p","old_p","new_v","old_v")),
    ("@url1",        ("c","p","v")),
    ("@url2",        ("c","p","v")),
    # Stub hooks.
    ("after-redraw-outline",         ("c",)),
    ("clear-mark",                   ("c","p","v")),
    ("close-frame",                  ("c",)),
    ("color-optional-markup",        ("colorer","p","v","s","i","j","colortag")),
    ("create-optional-menus",        ("c",)),
    ("destroy-all-global-windows",   None),
    ("draw-outline-box",             ("tree","p","v","x","y")), #
    ("draw-outline-icon",            ("tree","p","v","x","y")), #
    ("draw-outline-node",            ("tree","p","v","x","y")), #
    ("draw-outline-text-box",        ("tree","p","v","x","y")), #
    ("create-popup-menu-items",      ("c","p","v","event")),
    ("enable-popup-menu-items",      ("c","p","v","event")),
    ("init-color-markup",            ("colorer","p","v")),
    ("new",                          ("old_c","new_c")),
    ("redraw-entire-outline",        ("c",)),
    ("scan-directives",              ("c","p","v","s","old_dict","dict","pluginsList")),
    ("set-mark",                     ("c","p","v" )),
    ("show-popup-menu",              ("c","p","v","event")),
)
#@+node:ekr.20051104081502.223: *4* << define typeData >>
typeData = {
    "arg":      types.StringType,
    "c":        leoCommands.Commands,
    "ch":       types.StringType,
    "closeFlag":types.StringType,
    "colorer":  leoColor.colorizer,
    "colortag": types.StringType,
    "dict":     types.DictType,
    "event":    Tk.Event,
    "ext":      types.StringType,
    "fileName": types.StringType,
    "i":        types.IntType,
    "j":        types.IntType,
    "label":    types.StringType,
    "new_c":    leoCommands.Commands,
    "new_p":    leoNodes.position,
    "newSel":   types.TupleType,
    "new_v":    leoNodes.position,
    "old_c":    leoCommands.Commands,
    "old_dict": types.DictType,
    "old_p":    leoNodes.position,
    "oldSel":   types.TupleType,
    "old_v":    leoNodes.position,
    "openType": types.StringType,
    "p":        leoNodes.position,
    "pluginsList": types.ListType,
    "s":        types.UnicodeType,
    "tree":     leoTkinterTree.leoTkinterTree,
    "v":        leoNodes.position,
    "undoType": types.StringType,
    "x":        types.IntType,
    "y":        types.IntType,
}
#@+node:ekr.20051104081502.224: *4* << defineCheckHook >>
def checkHook (tag,keywords,args=args):

    """Check to see that the keywords passed to the hook are as described in args.
    Each arg is a list of strings whose type is defined in typeData."""

    global checked, verbose
    if tag in checked: return
    ok = True
    checked.append(tag)
    if args is None: args = []
    args = list(args)
    args.sort()
    keys = list(keywords.keys())
    keys.sort()

    if len(args) != len(keys):
        g.pr("%25s expected:" % (tag),args)
        g.pr("%25s      got:" % (tag),keys)
        ok = False
    else:
        for arg,key in zip(args,keys):
            arg_type = typeData.get(arg)
            val = keywords.get(key)
            if not checkOneHook(arg_type,val):
                g.pr("%25s      arg:" % (tag), arg)
                g.pr("%25s expected:" % (tag), arg_type)
                g.pr("%25s      got:" % (tag), type(val))
                ok = False
    if ok:
        g.pr(tag)
#@+node:ekr.20051104081502.225: *4* checkOneHook
def checkOneHook (arg_type, val):

    if 0:
        if arg_type != type(val):
            g.trace(arg,key,arg_type,type(val))

    return (
        (arg_type is type(val)) or
        (arg_type == types.StringType and type(val) is types.UnicodeType) or
        (type(arg_type) == types.ClassType and isinstance(val,arg_type)))
#@+node:ekr.20111121113227.4035: *3* Mini test of add-editor
c.k.simulateCommand('add-editor')
c.k.simulateCommand('delete-editor')
#@+node:ekr.20051104081502.312: *3* Mini test of g.es_exception
try:
    assert False, 'Assert False'
except AssertionError:
    g.es_exception()
#@+node:ekr.20051104081502.311: *3* Mini test of g.pdb
# Running this as a unit test would hang the unit tests!
g.pdb()
#@+node:ekr.20111108170253.3968: *3* Mini test of unicode stuff
@first # -*- coding: utf-8 -*-

table = (
    'test',
    'Ä 궯 奠',
    'Ä 궯 奠 after', # fails with cp6501: after is duplicated.
)

print('*'*20)
print('isPython3: %s' % g.isPython3)

for s in table:
    if g.isPython3:
        s = s.encode('ascii','replace') # create bytes.
    g.es(repr(s))
    g.es(s)
    g.pr ('g.pr(s)       : %s' % s)
    g.pr ('g.pr(repr(s)) : %s' % repr(s))
    print('print(s)      : %s' % s)
    print('print(repr(s)): %s' % s)
#@+node:ekr.20111121081052.3909: *3* Mini test that g.es leaves focus unchanged
if g.app.isExternalUnitTest:
    pass # Prints to console, which is annoying.
else:
    old_focus = c.get_focus()
    
    for flag in (False,True):
        g.es('Hi')
        
    if flag:
        c.outerUpdate() # Restores focus, especially when run from a script.
    new_focus = c.get_focus()
    
    try:
        assert old_focus == new_focus,'old focus: %s new focus: %s' % (
            old_focus,new_focus)
    except AssertionError:
        c.bodyWantsFocusNow()
#@+node:ekr.20111121090700.3914: *3* mini test that print-bindings puts results in Bindings tab
if g.app.isExternalUnitTest:
    pass
else:
    c.k.simulateCommand('print-bindings')
    log = c.frame.log
    
    # This works when run via execute-script.
    assert log.tabName == 'Bindings',log.tabName
    
    # w = c.frame.log.contentsDict.get('Bindings')
    # assert w,'no Bindings widget'
    # wrapper = w.leo_log_wrapper
    # s = wrapper.getAllText()
    # assert s,wrapper
#@+node:ekr.20051104081502.106: *3* Mini tests of script buttons
#@+node:ekr.20051104081502.107: *4* Redundant: @suite run all doctests in @file nodes
import doctest
import unittest

tm = c.testManager

createUnitTest = True

if createUnitTest:
    suite = unittest.makeSuite(unittest.TestCase)
else:
    suite = None

paths   = tm.findAllAtFileNodes(c)
modules = tm.importAllModulesInPathList(paths)

if createUnitTest:
    suite = tm.createUnitTestsFromDoctests(modules)
else:
    for module in modules:
        doctest.testmod(module,verbose=True,report=False)

if suite:
    g.app.scriptDict['suite'] = suite
#@+node:ekr.20051104081502.108: *4* runProfile button mini-test
for i in range(10000):
    if i and (i % 1000) == 0:
        g.pr(i)
#@+node:ekr.20051104081502.109: *4* runTimeit mini-test
i = 0
for i in range(100000):
    i += 1
    i -= 1
#@+node:ekr.20051104081502.110: *4* profile redraws
# c.redraw just schedules the actual drawing.
# We want to profile the actual idle-time drawing.

c.frame.tree.idle_redraw()
#@+node:ekr.20051104081502: *3* Other tests
@language python
@tabwidth -4
#@+node:ekr.20051104081502.542: *4* @@nosent test-niceNosent
#@+node:ekr.20051104081502.543: *5* part 1
part 1, line 1
part 2, line 2, no newline
#@+node:ekr.20051104081502.544: *5* part 2
part 2, line 1, no newline
#@+node:ekr.20051104081502.545: *5* part 3
part 3, line 1
part 3, line 2, newline
#@+node:ekr.20051104081502.320: *4* @nowrap tests
@nowrap
aaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccc ddddddddddddddd eeeeeeeeeeeeeee ffffffffffffffff 
#@+node:ekr.20051104081502.22: *4* Experiments
#@+node:ekr.20051104081502.23: *5* Test of moving positions
class position:
    def __init__(self):
        self.v = "a"
    def move(self):
        self.v = "b"

p = position()
v = p.v
g.pr("before", v, p.v, v is p.v)
p.move()
g.pr("after ", v, p.v, v is p.v)
#@+node:ekr.20051104081502.24: *5* Test of using an iterator inside a list comprehension
class test_iter_class:
    def __init__ (self):
        self.vals = ("a","b","c")
        self.n = 0
    def __iter__(self):
        return self
    def next(self):
        if self.n < len(self.vals):
            val = self.vals[self.n]
            self.n += 1
            return val
        else:
            raise StopIteration

def test_iter(): return test_iter_class()

vals = [val for val in test_iter()]

g.pr(vals)
#@+node:ekr.20051104081502.25: *5* Test of using c.allNodes_iter to create a list of all positions
g.pr('-'*20)

# These are equivalent.
positions1 = [p for p in c.allNodes_iter(copy=True)]
positions2 = [p.copy() for p in c.allNodes_iter()]

assert(len(positions1) == len(positions2))
for i in range(len(positions1)):
    assert(positions1[i] == positions2[i])

if 0:
    for p in positions1:
        g.pr(p)
g.pr("done")
#@+node:ekr.20051104081502.26: *5* Creating a list of distinct vnodes
g.pr('-'*20)

positions = [p.copy() for p in c.allNodes_iter()]

tnodes = {} ; vnodes = []
for p in c.allNodes_iter():
    t = p.v.t
    if tnodes.get(t) is None:
        tnodes[t]=t
        vnodes.append(p.v)

g.pr(len(positions),len(vnodes))

for v in vnodes:
    g.pr(v)
#@+node:ekr.20051104081502.28: *5* test of list comparisons
stack1 = ["a","b","c"]
stack2 = ["a","b","c"]
stack3 = ["a","b","d"]
stack4 = ["a","b"]
g.pr(stack1 == stack2)
g.pr(stack1 == stack3)
g.pr(stack1 == stack4)
#@+node:ekr.20051104081502.29: *5* test that childIndex doesn't mess with p
g.pr(p.h)
g.pr(p.childIndex())
g.pr(p.h)
#@+node:ekr.20051104081502.30: *5* Test of __cmp__ vrs equal
import timeit

s1 = '''\
class test(object):
    def __cmp__(self,p2):   return 0
    def equal(self,p2):     return 0
p1 = test() ; p2 = test()'''

s2 = '''\
class test:
    def __cmp__(self,p2):   return 0
    def equal(self,p2):     return 0
p1 = test() ; p2 = test()'''

s3 = '''\
import leoNodes
p1 = leoNodes.position(None,[])
p2 = leoNodes.position(None,[])'''

for s in (s1,s2,s3):
    t1 = timeit.Timer(stmt='p1==p2',setup=s).timeit()
    t2 = timeit.Timer(stmt='p1.equal(p2)',setup=s).timeit()
    g.pr("%2.2f,%2.2f,%0.2f" % (t1,t2,t1/t2))
#@+node:ekr.20051104081502.31: *5* Test print
# "LPT1:", "PRN:" and "PRN" all freeze

s = 'stuff\n'
port = 'USB002'

try:
    f = file(port,'w')
    f.write(s)
    f.flush()
    f.close()
    g.pr("done")
except IOError:
    g.pr("Can not open",port)
#@+node:ekr.20051104081502.32: *5* String-based imports...
@ By far the simplest way is just to write the string to a temp file, then import the temp files.

All other approaches quickly get deeply involved with Leo's internals...
#@+node:ekr.20051104081502.33: *6* import from string
@language plain

The first idea was to use Python's imp module to simulate an import from a file.  This does not work well because imp expects a file, not a StringIO object.

The second idea was to use Python's parser module.  But this returns an instance type, not a module.

A third idea would be to subclass the file type to fool the imp module.

A fourth idea would be to use the ihooks module.  Apparently this module was designed to do something like what I am trying to do!  However, there doesn't seem to be docs for it, so I have imported the code...

@color
#@+node:ekr.20051104081502.34: *7* @@test import from string
import imp
import StringIO

@
load_module( name, file, filename, description) 

Load a module that was previously found by find_module() (or by an otherwise conducted search yielding compatible results). This function does more than importing the module: if the module was already imported, it is equivalent to a reload()! The name argument indicates the full module name (including the package name, if this is a submodule of a package). The file argument is an open file, and filename is the corresponding file name; these can be None and '', respectively, when the module is not being loaded from a file. The description argument is a tuple, as would be returned by get_suffixes(), describing what kind of module must be loaded. 
If the load is successful, the return value is the module object; otherwise, an exception (usually ImportError) is raised. 

Important: the caller is responsible for closing the file argument, if it was not None, even when an exception is raised. This is best done using a try ... finally statement.
@c

s = """

def foobar(): pass

"""

@ get_suffixes( ) 

Return a list of triples, each describing a particular type of module. Each triple has the form (suffix, mode, type), where suffix is a string to be appended to the module name to form the filename to search for, mode is the mode string to pass to the built-in open() function to open the file (this can be 'r' for text files or 'rb' for binary files), and type is the file type, which has one of the values PY_SOURCE, PY_COMPILED, or C_EXTENSION, described below.
@c

g.pr('-' * 20)
description = (".py","r",imp.PY_SOURCE)
theFile = StringIO.StringIO(s) # Create a file-like object
g.pr(repr(theFile))
try:
    imp.load_module("myModule",theFile,"myFileName",description)
except:
    g.es_exception()



#@+node:ekr.20051104081502.35: *6* Subclass the file type for use with imp module
if 0:
    class myFile(file):
        pass

    g.pr(myFile)
    g.pr(issubclass(myFile,file))
    g.pr(isinstance(myFile,file))
    g.pr(super(myFile))
    g.pr(__import__)

if 0:
    old_import = __import__

    def myImport(*args,**keys):
        g.pr("myImport")
        global old_import
        old_import(*args,**keys)

    __import__ = myImport

mod = __import__("leoApp")
g.pr(mod)
#@+node:ekr.20051104081502.36: *6* Use parser module to simulate import from string
import compiler

for child in p.children_iter():
    h = child.h
    body = child.b

    try:
        val = compiler.parse(body)
        g.pr(type(val))
        g.pr(val)
    except SyntaxError:
        g.es("Syntax error: %s" % h,color="blue")
#@+node:ekr.20051104081502.37: *7* test1
import doctest
g.pr(doctest)
#@+node:ekr.20051104081502.209: *4* Make sure openWith changes are benign
arg = "arg" ; filename = "fileName"
path = "path" ; shortPath = "shortPath"
vtuple = "vtuple"

def test(a,b):
    assert(a==b)

test(
    "os.system("+arg+shortPath+")",
    "os.system(%s)" % (arg+shortPath))
test(
    "os.startfile("+arg+shortPath+")",
    "os.startfile(%s)" % (arg+shortPath))
# test(
    # "exec("+arg+shortPath+")",
    # "exec(%s)" % (arg+shortPath))
test(
    "os.spawnl("+arg+","+filename+','+ shortPath+")",
    "os.spawnl(%s,%s,%s)" % (arg,filename,shortPath))
test(
    "os.spawnv("+arg[0]+","+repr(vtuple)+")",
    "os.spawnv(%s,%s)" % (arg[0],repr(vtuple)))
#@+node:ekr.20051104081502.111: *4* Perfect import stuff...
@language python
@tabwidth -4
#@+node:ekr.20051104081502.112: *5* Mulder Update script
# EKR: I don't remember the status of this.

@language python

import shutil

testing = True
sourcedir=r"c:/prog/test/perfectImport"
targetdir=r"c:/prog/test/perfectImport/leo"
s1 = g.os_path_join(sourcedir,"leoAtFile.py")
t1 = g.os_path_join(targetdir,"leoAtFile.py")
files = [(s1,t1)]

@others

g.pr('\n' + '-' * 20)
sync(files) # push or pull, depending on date.
#@+node:ekr.20051104081502.113: *6* sync
def sync(files):

    """Do a pull or a push, depending on the date of the files."""

    none, push, pull = 'None', 'push', 'pull'
    mu = g.mulderUpdateAlgorithm()

    for sourcefilename, targetfilename in files:
        << compute sourcetime and targettime >>
        << compute operation >>
        if operation == push:
            if testing: g.pr(push, sourcefilename, targetfilename)
            strippedLines = mu.removeSentinelsFromFile(sourcefilename)
            mu.write_if_changed(strippedLines,sourcefilename,targetfilename)
            mu.copy_time(sourcefilename,targetfilename)
        elif operation == pull:
            if testing: g.pr(pull, sourcefilename, targetfilename)
            if sourcetime:
                mu.propagateDiffsToSentinelsFile(sourcefilename,targetfilename)
                mu.copy_time(targetfilename,sourcefilename)
            else:
                shutil.copy2(targetfilename,sourcefilename)
#@+node:ekr.20051104081502.114: *7* << compute sourcetime and targettime >>
sourcetime = targettime = None

if g.os_path_exists(sourcefilename):
    sourcetime = g.os_path_getmtime(sourcefilename)

if g.os_path_exists(targetfilename):
    targettime = g.os_path_getmtime(targetfilename)
#@+node:ekr.20051104081502.115: *7* << compute operation >>
operation = None
if sourcetime:
    if targettime:
        if sourcetime > targettime:
            operation = push
        elif sourcetime < targettime:
            operation = pull
    else:
        operation = push
elif targettime:
    operation = pull
#@+node:ekr.20051104081502.116: *5* Perfect Import Script
# Run this script to import a file.
# This is undoable because the Import @file command is undoable.

path = r"c:\prog\test\perfectImport"

# Two files from Python23/Lib
name1 = g.os_path_join(path,"formatter.py")
name2 = g.os_path_join(path,"SimpleHTTPServer.py")
names = [name1]

c.importCommands.importFilesCommand (names,"@file",
    perfectImport=True,testing=False,verbose=True)
#@+node:ekr.20051104081502.117: *4* Printing tests...
#@+node:ekr.20051104081502.118: *5* Print findAllPotentiallyDirtyNodes
g.pr('-'*20)

for p in c.allNodes_iter():
    if p.isDirty():
        vnodes = p.findAllPotentiallyDirtyNodes()
        g.pr('-'*5, p)
        for v in vnodes:
            g.pr(v)

g.pr("done")
#@+node:ekr.20051104081502.119: *5* Print iterations: do not delete
import leoNodes

position = leoNodes.position

@others

current = pos = c.p
child1 = current.firstChild()
child2 = child1.firstChild()

if 0:
    g.pr('-'*10, "parents")
    for p in child2.parents_iter(): g.pr(p)
if 0:
    g.pr('-'*10, "subtree")
    for p in pos.subtree_iter(): g.pr(p)
if 0:
    g.pr('-'*10, "children")
    for p in child1.children_iter(): g.pr(p)
if 0:
    g.pr('-'*10, "siblings")
    for p in pos.siblings_iter(): g.pr(p)
if 1:
    g.pr('-'*10, "all nodes")
    for p in c.allNodes_iter():
        g.pr(p.isCloned(),p)
#@+node:ekr.20051104081502.120: *6* b
#@+node:ekr.20051104081502.121: *7* c
#@+node:ekr.20051104081502.122: *8* c2
#@+node:ekr.20051104081502.123: *9* c3
#@+node:ekr.20051104081502.124: *9* c4
#@+node:ekr.20051104081502.125: *6* Clone test data
#@+node:ekr.20051104081502.126: *7* aa
#@+node:ekr.20051104081502.127: *7* a
#@+node:ekr.20051104081502.120: *8* b
#@+node:ekr.20051104081502.121: *9* c
#@+node:ekr.20051104081502.122: *10* c2
#@+node:ekr.20051104081502.123: *11* c3
#@+node:ekr.20051104081502.124: *11* c4
#@+node:ekr.20051104081502.128: *7* d
#@+node:ekr.20051104081502.127: *8* a
#@+node:ekr.20051104081502.120: *9* b
#@+node:ekr.20051104081502.121: *10* c
#@+node:ekr.20051104081502.122: *11* c2
#@+node:ekr.20051104081502.123: *12* c3
#@+node:ekr.20051104081502.124: *12* c4
#@+node:ekr.20051104081502.129: *7* e
#@+node:ekr.20051104081502.130: *7* z
#@+node:ekr.20051104081502.131: *6* last node
#@+node:ekr.20051104081502.145: *5* Print isAnyAtFileNode
g.pr('-'*20)

for p in c.allNodes_iter():
    if p.isAnyAtFileNode():
        g.pr(p)

g.pr("done")
#@+node:ekr.20051104081502.146: *5* Print fundChildrenOf and
tm = c.testManager

g.pr("children", '-' * 20)
children = tm.findChildrenOf(p)
for child in children: g.pr(child.h)

g.pr("subtree", '-' * 20)
descendants = tm.findSubnodesOf(p)
for descendant in descendants: g.pr(descendant.h)
#@+node:ekr.20051104081502.147: *5* Tests of pickle & hexlify
import binascii
import pickle

d = { "a":True }

g.pr('-' * 40)

s = pickle.dumps(d,bin=True)
s2 = binascii.hexlify(s)
g.pr(`s`,s2)

s3 = binascii.unhexlify(s2)
d2 = cPickle.loads(s3)

g.pr(`d2`)
g.pr(d == d2, d is d2)
#@+node:ekr.20051104081502.148: *5* Test of undo registration
def redoBletch(self):
    g.trace()

def undoBletch(self):
    g.trace()

u = c.undoer

if 0:
    # bad functions
    u.registerUndoHandlers("Bletch","abc","xyz")
else:
    u.registerUndoHandlers("Bletch",undoBletch,redoBletch)

# "Execute" the Bletch command :-)  The Edit command should contain "Undo Bletch"
u.setUndoParams("Bletch",p)

# Selecting "Undo Bletch" will enable "Redo Bletch", etc.
#@+node:ekr.20051104081502.149: *5* Test of unknownAttributes
d = { "a":True }

if 1:
    # Warning: executing this in the a2 code base will cause any save operation to fail.
    p.v.unknownAttributes = { "myPlugin" : d }

g.pr(repr(p.v.unknownAttributes))
#@+node:ekr.20051104081502.150: *5* Test of "end1" hook
import leoPlugins

def onEnd (tag,keys):
    g.pr("onEnd",tag,keys)

count = 0

def onIdle (tag,keys):
    global count ; count += 1
    if count % 10 == 0:
        g.pr("onIdle",count,keys.get("c"))

leoPlugins.registerHandler("end1", onEnd)
g.pr("onEnd registered as end1 hook")

leoPlugins.registerHandler("idle", onIdle)
g.pr("onIdle registered as idle hook")
#@+node:ekr.20051104081502.151: *5* Print timestamps of all nodes
for p in c.all_positions_iter():
    g.pr(p.v.t.fileIndex)
#@+node:ekr.20051104081502.152: *5* test of focus
g.pr(c.frame.bodyCtrl.focus())
#@+node:ekr.20051104081502.153: *5* Using a generator instead of readLinesClass
# This kind of code is used in the prototypes of new commands.

from __future__ import generators

@others

lines = "a\nb\nc\nd"

if 1: # Both work
    readline = g.readLinesGenerator(lines).next
else:
    readline = g.readLinesClass(lines).next

g.pr('-' * 20)

if 1: # Both work
    for s in g.readLinesGenerator(lines):
        g.pr(s,)
else:
    while 1:
        s = readline()
        if s: g.pr(s,)
        else: break

g.pr('\n' + '-' * 20)
#@+node:ekr.20051104081502.154: *5* Test of better error messages in Execute Script command
# Test
@others
# Last
#@+node:ekr.20051104081502.155: *6* Contains error
a = 1
g.pr("hello")
c = b
#@+node:ekr.20051104081502.157: *5* print all docstrings from a module
import leoTest
import types

specialDictNames = ('__builtins__','__doc__','__name__','__file__','__module__')

def printDoc(x,s):
    if hasattr(x,"__doc__") and x.__doc__:
        g.pr("%4d %s" % (len(x.__doc__),s))
    else:
        g.pr("%4s %s" % (' ',s))

g.pr('-' * 60)
g.pr("%4d %s" % (len(leoTest.__doc__),"leoTest"))

if 1:
    for s in leoTest.__dict__:
        if s not in specialDictNames:
            x = getattr(leoTest,s)
            if type(x) != types.ModuleType:
                printDoc(x,s)
                if type(x) == types.ClassType:
                    for s2 in x.__dict__:
                        x2 = getattr(x,s2)
                        if s2 not in specialDictNames:
                            g.pr(' '*4,)
                            printDoc(x2,s2)
else:
    << print names sorted by type >>
#@+node:ekr.20051104081502.158: *6* << print names sorted by type >>
for theType,typeName in (
    (types.ModuleType,"modules"),
    (types.ClassType,"classes"),
    (types.FunctionType,"functions"),
):

    g.pr("\n%s..." % typeName)
    for s in leoTest.__dict__:

        if s not in specialDictNames:
            x = getattr(leoTest,s)
            if type(x) == theType:
                printDoc(x,s)
                if theType == types.ClassType:
                    g.pr("\tmethods...")
                    for s2 in x.__dict__:
                        x2 = getattr(x,s2)
                        if s2 not in specialDictNames:
                            g.pr("\t",newline=False)
                            printDoc(x2,s2)
#@+node:ekr.20051104081502.216: *4* Registering & unregistering the "new" drawing hooks
#@+node:ekr.20051104081502.217: *5* Register all new hooks
import leoPlugins as plugins

def traceHook(tag,event):
    g.trace(tag)

tags = (
    "boxclick1","boxclick2",
    "drag1","drag2",
    "dragging1","dragging2",
    "enddrag1","enddrag2",
    "iconclick1","iconclick2"  , 
    "iconrclick1","iconrclick2",
    "icondclick1","icondclick2",
)

plugins.registerHandler(tags,traceHook)

handlers = plugins.getHandlersForTag(tags)
if handlers:
    g.pr("-" * 20)
    for h in handlers:
        g.pr(h)
#@+node:ekr.20051104081502.218: *5* Unregister all new hooks
import leoPlugins as plugins

tags = (
    "boxclick1","boxclick2",
    "drag1","drag2",
    "dragging1","dragging2",
    "enddrag1","enddrag2",
    "iconclick1","iconclick2"  , 
    "iconrclick1","iconrclick2",
    "icondclick1","icondclick2",
)

for tag in tags:
    handlers = plugins.getHandlersForTag(tag)
    if handlers:
        g.pr(handlers)
        for f in handlers:
            plugins.unregisterHandler(tag,f)

handlers = plugins.getHandlersForTag(tags)
if handlers:
    g.pr("-" * 20)
    for h in handlers:
        g.pr(h)
#@+node:ekr.20051104081502.219: *5* Print all new hooks
import leoPlugins as plugins

tags = (
    "boxclick1","boxclick2",
    "drag1","drag2",
    "dragging1","dragging2",
    "enddrag1","enddrag2",
    "iconclick1","iconclick2"  , 
    "iconrclick1","iconrclick2",
    "icondclick1","icondclick2",
)

handlers = plugins.getHandlersForTag(tags)
if handlers:
    g.pr("-" * 20)
    for h in handlers:
        g.pr(h)
#@+node:ekr.20051104081502.538: *4* ReportLab sample scripts
import sys
sys.path.append(r'c:\reportlab_1_20')

debug = True

@others

from reportlab.pdfgen import canvas
c = canvas.Canvas('hello.pdf')
for i in (10,50):
    text(c,'x'*10,i,i)
# pencil(c,text='Note')

key = 'key1'

c.bookmarkPage(key)
c.addOutlineEntry('OutlineEntry',key)


c.showPage()
c.save()
#@+node:ekr.20051104081502.539: *5* text
def text(c,text,i=100,j=100):
    c.drawString(i,j,text)
#@+node:ekr.20051104081502.540: *5* pencil
def pencil(canvas, text="No.2"):
    from reportlab.lib.colors import yellow, red, black,white
    from reportlab.lib.units import inch
    u = inch/10.0
    canvas.setStrokeColor(black)
    canvas.setLineWidth(4)
    # draw erasor
    canvas.setFillColor(red)
    canvas.circle(30*u, 5*u, 5*u, stroke=1, fill=1)
    # draw all else but the tip (mainly rectangles with different fills)
    canvas.setFillColor(yellow)
    canvas.rect(10*u,0,20*u,10*u, stroke=1, fill=1)
    canvas.setFillColor(black)
    canvas.rect(23*u,0,8*u,10*u,fill=1)
    canvas.roundRect(14*u, 3.5*u, 8*u, 3*u, 1.5*u, stroke=1, fill=1)
    canvas.setFillColor(white)
    canvas.rect(25*u,u,1.2*u,8*u, fill=1,stroke=0)
    canvas.rect(27.5*u,u,1.2*u,8*u, fill=1, stroke=0)
    canvas.setFont("Times-Roman", 3*u)
    canvas.drawCentredString(18*u, 4*u, text)
    # now draw the tip
    penciltip(canvas,debug=0)
    # draw broken lines across the body.
    canvas.setDash([10,5,16,10],0)
    canvas.line(11*u,2.5*u,22*u,2.5*u)
    canvas.line(22*u,7.5*u,12*u,7.5*u)
#@+node:ekr.20051104081502.541: *5* penciltip
def penciltip(canvas, debug=1):
    from reportlab.lib.colors import tan, black, green
    from reportlab.lib.units import inch
    u = inch/10.0
    canvas.setLineWidth(4)
    if debug:
        canvas.scale(2.8,2.8) # make it big
        canvas.setLineWidth(1) # small lines
    canvas.setStrokeColor(black)
    canvas.setFillColor(tan)
    p = canvas.beginPath()
    p.moveTo(10*u,0)
    p.lineTo(0,5*u)
    p.lineTo(10*u,10*u)
    p.curveTo(11.5*u,10*u, 11.5*u,7.5*u, 10*u,7.5*u)
    p.curveTo(12*u,7.5*u, 11*u,2.5*u, 9.7*u,2.5*u)
    p.curveTo(10.5*u,2.5*u, 11*u,0, 10*u,0)
    canvas.drawPath(p, stroke=1, fill=1)
    canvas.setFillColor(black)
    p = canvas.beginPath()
    p.moveTo(0,5*u)
    p.lineTo(4*u,3*u)
    p.lineTo(5*u,4.5*u)
    p.lineTo(3*u,6.5*u)
    canvas.drawPath(p, stroke=1, fill=1)
    if debug:
        canvas.setStrokeColor(green) # put in a frame of reference
        canvas.grid([0,5*u,10*u,15*u], [0,5*u,10*u])
#@+node:ekr.20051104081502.272: *4* Standalone imports
#@+node:ekr.20051104081502.273: *5* test that of standalone imports of leo files
import glob,sys,traceback

def printModules():
    mods = sys.modules.keys()
    mods.sort()
    for mod in mods: g.pr(mod)

def leoModules():
    files = glob.glob(r'%s\*.py' % g.app.loadDir)
    modules = []
    for file in files:
        path,file = g.os_path_split(file)
        module,ext = g.os_path_splitext(file)
        if g.match(module,0,'leo'):
            modules.append(module)
    return modules

def delLeoModules():
    for module in leoModules():
        if module in sys.modules:
            del sys.modules[module]

def test():
    for module in leoModules():
        g.pr(module)
        exec 'import %s' % module in {},{}
        del sys.modules[module]

delLeoModules()        
test()
# printModules()
#@+node:ekr.20051104081502.274: *5* Script to run in Idle
def test():
    '''Tests whether all files can be imported.'''
    import glob, os, sys, traceback
    dir = r'c:\prog\leoCVS\leo\src'
    files = glob.glob(r'%s\*.py' % dir)
    modules = []
    for file in files:
        path,file = os.path.split(file)
        module,ext = os.path.splitext(file)
        if module[:3] == 'leo':
            modules.append(module)
    for module in modules:
        g.pr(module)
        try:
            exec 'import %s' % module in {},{}
            del sys.modules[module]
        except:
            traceback.print_exc()

def printModules():
    import sys
    mods = sys.modules.keys()
    mods.sort()
    for mod in mods: g.pr(mod)

#@+node:ekr.20051104081502.307: *4* test local settings (c.redirect_execute_script_output_to_log_pane)
g.es(c.redirect_execute_script_output_to_log_pane)
g.es(c.config.redirect_execute_script_output_to_log_pane)
g.pr('hello')

#assert c.redirect_execute_script_output_to_log_pane is True
#assert c.config.redirect_execute_script_output_to_log_pane is True
#@+node:ekr.20051104081502.164: *4* test of 4.3 str_ attributes
#@+node:ekr.20051104081502.165: *5* set
# Set the attribute.
d = {'str_ekr_attribute': 'abc'}
p.v.t.unknownAttributes = d
#@+node:ekr.20051104081502.166: *5* get
for p in c.allNodes_iter():
    h = p.h
    if hasattr(p.v.t,'unknownAttributes'):
        d = p.v.t.unknownAttributes
        val = d.get('str_ekr_attribute')
        if val:
           g.es('str_ekr_attribute is: %s' % val)
#@+node:ekr.20051104081502.316: *4* test of an exception in another module
c.testManager.throwAssertionError()
#@+node:ekr.20051104081502.210: *4* Test of autocompleter
@language python

# Type a period to autocomplete
leoTest

# Type an open paren to bring up calltip.
c.testManager.findAllAtFileNodes
#@+node:ekr.20051104081502.322: *4* Test of g.getScript with forcePythonSentinels = False
@language html
#@+node:ekr.20051104081502.323: *5* g.getScript
g.pr('-'*20)
g.pr(g.getScript(c,p,forcePythonSentinels=False))
#@+node:ekr.20051104081502.324: *5* html stuff
<body>
@others
</body>
#@+node:ekr.20051104081502.325: *6* body
This is a body
#@+node:ekr.20051104081502.315: *4* test of NameError traceback
# Comment

g.pr(z)
#@+node:ekr.20051104081502.318: *4* Test of os.spawnv calls to c.openWith
table = ('spawnv',None,(
    'os.spawnv',[
    r'c:\vim\vim63\gvim.exe',
    ' --servername LEO ',
    ' --remote-silent ',
    ],
    ".py")),

c.frame.menu.createOpenWithMenuFromTable(table)
#@+node:ekr.20051104081502.321: *4* Test of redirected scipt with error
# To run this test, set @bool redirect_execute_script_output_to_log_pane = True in the @settings tree.

g.pr('hi')
g.pr(c.config.redirect_execute_script_output_to_log_pane)
g.pr(c.xyzzy)
#@+node:ekr.20051104081502.183: *4* Test of reportBadChars
s = u"ß"

g.reportBadChars(s,"latin_1")

g.pr(g.toEncodedString(s,"latin_1"))
#@+node:ekr.20051104081502.234: *4* test of tab_width & tab_width ivars
g.pr(c)
g.pr('use_plugins',c.use_plugins)
g.pr('tab_width',c.tab_width)
g.pr('page_width',c.page_width)
#@+node:ekr.20051104081502.211: *4* Test of template plugin
#@+node:ekr.20051104081502.213: *5* A node that uses the template
#@+node:ekr.20051104081502.214: *4* test of using changes to Go To Line number to handle scripts
# Go To Line number now assumes that selected node is
# the root of a script if there is no ancestor @file node.

@others

# last line
#@+node:ekr.20051104081502.215: *5* node that throws exception
# We should also be able to use the goto line number command to get to the erroneous line.

a = 1/0 # ZeroDivisionError

b = 2
#@+node:ekr.20051104081502.547: *4* Test of warnings of conflicting shortcuts
# This problem has been around forever.
g.pr('-' * 40)
# Yes. We *do* want to warn in c.config.exists.
g.pr('exists',g.app.config.exists(c,'showMinibuffer','bool'))
val = c.config.getBool('showMinibuffer')
g.pr('bool:showMinibuffer',val)
val = c.config.getShortcut('showMinibuffer')
g.pr('shortcut:showMinibuffer',val)
#@+node:ekr.20051104081502.551: *4* test k.registerCommand
k = c.keyHandler

def f (event):
    g.es_print('Hello',color='purple')

def f2 (event):
    g.es_print('Hello2',color='purple')

k.registerCommand('print-hello','Alt-Ctrl-Shift-p',f)
k.registerCommand('print-hello2',None,f2)
#@+node:ekr.20051104081502.226: *4* Tests of leoGlobals
#@+node:ekr.20051104081502.227: *5* @test g.rawPrint
g.rawPrint("Test of g.rawPrint")
g.redirectStdout()
g.rawPrint("Test of g.rawPrint")
g.restoreStdout()
#@+node:ekr.20051104081502.232: *5* test of g.pdb
g.pdb()
#@+node:ekr.20051104081502.233: *5* Test of g.app.debugSwitch
g.pr(g.app.debugSwitch)

g.app.debugSwitch = 0 # 2: drop into pdb

zerodivide = 1 / 0
#@+node:ekr.20051104081502.326: *4* Tests of rst3 plugin
@nocolor
@pagewidth 100
@language python
#@+node:ekr.20051104081502.327: *5* @rst ../doc/ListManagerDocs.html
@language python

@ @rst-options
code_mode = False
show_leo_directives = True
number_code_lines = False
@c

#########################
ListManager Documentation
#########################

:Author: Steven Zatz, Modified by EKR.
:Contact: slzatz@hotmail.com
:Date: $Date: 2008/02/14 14:59:04 $
:Status: This is a "work in progress"
:Revision: $Revision: 1.247 $
:Copyright: Application and documentation use the Python license which is compatible with the GPL. 

This is experimental documentation of a program called ListManager, written in
Python and wxPython using Leo to create both the application code and the
associated reST documentation.

ListManager is an application that allows a group of people working on a joint
project to maintain a common list of todos and related items that have owners,
due dates and associated notes. The application uses mysql as its database for
group use and also uses sqlite for locally resident databases for personal
lists. It works in conjunction with Outlook to allow email messages to be sent
to ListManager for inclusion in lists and uses Outlook to mail messages to
users.

.. contents:: Table of Contents
#@+node:ekr.20051104081502.328: *6* wxListManager.py
@language python
@color
@others

@ @rst-options
code_mode = True
@c
#@+node:ekr.20051104081502.329: *7* Initial stuff
@ @rst-markup

Nothing unusual in what follows:  we start with the module imports, setting some global constants including Menu Ids and read the ListManager.ini file.
#@+node:ekr.20051104081502.330: *8* Module Imports
from wxPython.wx import *
from wxPython.lib.mixins.listctrl import wxListCtrlAutoWidthMixin

import os
import time
import pickle
import socket
import select
import random
import ConfigParser
import threading
import re
import sys

from pywintypes import CreateGuid
from win32com.client import Dispatch
#import win32pdh
import win32api
#from win32com.client import constants #--> just needed two constants...

import MySQLdb
import sqlite
import mx.DateTime

from LMDialogs import CalendarDialog, ModifierDialog, TicklerDialog, MailDialog,LoggerDialog, FinishedDialog, FindDialog, EvalDialog, TreeDialog, StartupDialog
#from wxTreeCtrl import TreeDialog

from printout import PrintTable
#@+node:ekr.20051104081502.331: *9* @rst-no-head About imports
@nocolor

os
    uses ``os.getcwd``, ``os.path.split``, ``os.chdir``, ``os.path.join``, ``os.path.getmtime``, ``os.startfile``, ``os.environ``

time
    uses ``time.sleep``, ``time.asctime``

pickle
    used to serialize data that is moved from Outlook to ListManager via sockets.  

socket
    as noted above, a socket is opened between Outlook and ListManager to move messages back and forth

select
    ListManager selects on the socket to see if there is a message that has been queued by Outlook

random
    used by the reminder popup to select messages

ConfigParser
    not surprisingly, using ConfiParser to parse the ListManager.ini file.  

threading
    more for fun than absolute necessity, a thread is opened on starting the program that constructs the list of owners for items.  In theory, if the datasize and number of Lists were large enough it could delay the appearance of the GUI and its initial responsiveness if we didn't construct the ownerlist in a thread.  On the other hand, it really let me play with threads and with creating a custom event that signalled the construction of the owner list to the main thread by posting a custom event.

re
    mainly using ``re.sub('[\\/:*"<>|\?]','-',f)`` to make sure that files are constructed only with legal characters.  Also searching the body text of nodes using re because it allows case insensitive searches through ``re.compile(pat, re.I)``.

pywintypes.CreateGuid
    probably should use pure python GUID that is in ASPN cookbook but it was easiest to just use the Windows GUID function.  Thank you Mark Hammond for win32all.

win32com.client.Dispatch
    used when launching Outlook to send email messages

win32api
    using win32api.GetUserName() in case there is no user name in the ini file or no ini file

MySQLdb
    using Andy Dustman's python extension module to connect to mysql back-end.

sqlite
    using  D. Richard Hipp's python extension to connect to local sqlite databases

import mx.DateTime
    using Marc-André Lemburg's mx.DateTime for dealing with datetime stuff in the databases

CalendarDialog, ModifierDialog, TicklerDialog, MailDialog,LoggerDialog, FinishedDialog, FindDialog, EvalDialog, TreeDialog, StartupDialog
   should just import LMDialogs and then access each dialog class by LM.WhateverDialog

printout.PrintTable
    There was an existing wxPython print module for printing from tables that I have modified to print Lists.

*#from win32com.client import constants*
    probably not wise but since the app only needs two constants from this module, just set the directly.  If MSFT decides to change the api, this is not good.
#@+node:ekr.20051104081502.332: *8* Constants
cwd = os.getcwd()
DIRECTORY = os.path.split(cwd)[0]
os.chdir(DIRECTORY)
del cwd

#Outlook Constants
olMailItem = 0x0
olFlagMarked = 0x2

OFFLINE_ONLY = False #False-> Online only  ; True-> Online and Offline possible; REMOTE_HOST = None -> Offline only

VERSION = '1.02'

@ @rst-markup

The following two global constants are needed to create emails through Outlook via COM::

    olMailItem = 0x0
    olFlagMarked = 0x2

For some reason, it seemed easier to just include them explicitly rather than worrying about generating all the Outlook constants in order to use early binding.  I supppose if MSFT changes the api, that would be a problem.
#@+node:ekr.20051104081502.333: *8* Menu IDs
@ @rst-markup
Menu Ids -- not much more to say although there should be something to say.
@c

#File Menu-----------------#
idNEWLIST = 1000
idOPENLIST = 1010
idCLOSELIST = 1015
idCLOSEALL = 1017
idSAVEAS = 1020
idDELETELIST = 1025
idPAGESETUP = 1030
idPRINT = 1035
idPRINTPREV = 1040
idMAILLIST = 1045
idOFFLINE = 1048
idEXIT = 1050

#Edit Menu-----------------
idCUT = 1055
idCOPY = 1060
idPASTE = 1065
idDELETEITEMS = 1070
idCOMBINEITEMS = 1075
idFIND = 1080

#Item Menu-------------------
idNEWITEM = 1085
idTOGGLEFINISHED = 1090
idEDITOWNER = 1095
idDUEDATE = 1100
idEDITNOTE = 1105
idMAILITEM =1110

#Diplay Menu---------------------
idSHOWFINISHED = 1115
idSHOWALL = 1120
idREFRESH = 1125
idDISPLAYDATE = 1130

#Tool Menu------------------------
idTICKLERACTIVE = 1135
idSHOWNEXT = 1140
idSYNC = 1145
idARCHIVE = 1150
idEVALUATE = 1155
idTOOLPRINT = 1165
idSENDTO = 1170

#Help Menu-------------------------
idABOUT = 1175
idHELP = 1180


#@+node:ekr.20051104081502.334: *8* Read Config File
config_file = os.path.join(DIRECTORY, "List Manager.ini")
defaults = dict(pw='python', db='listmanager', ext='txt', local='wxLMDB:sqlite', x='700', y='400')
cp = ConfigParser.ConfigParser(defaults=defaults)
cp.read(config_file) #ConfigParser closes the file

USER = cp.has_option('User','user') and cp.get('User','user') or win32api.GetUserName()

# the following all have default values provided in the constructor
PW = cp.get('User','pw')
DB = cp.get('Database','db')
NOTE_EXT = cp.get('Note','ext')
LOCAL_HOST = cp.get('Hosts','local')
X = cp.getint('Configuration','x')
Y = cp.getint('Configuration','y')

# the folloowing default to None
MAIL_LIST_PATH = cp.has_option('Mail','path') and cp.get('Mail','path') or None
QUICK_LIST = cp.has_option('User','quicklist') and cp.get('User','quicklist') or None

# the following default to False
STARTUP_DIALOG = cp.has_option('User','startup_dialog') and cp.getboolean('User','startup_dialog')
DELETE_LIST = cp.has_option('User','delete_list') and cp.getboolean('User','delete_list')
OUTLOOK = cp.has_option('Mail','outlook') and cp.getboolean('Mail','outlook')

if cp.has_option('Hosts','remote'):
    REMOTE_HOST = cp.get('Hosts','remote')
else:
    REMOTE_HOST = None
    OFFLINE_ONLY = True

# reading it again because of the way defaults are handled
cp = ConfigParser.ConfigParser()
cp.read(config_file) #ConfigParser closes the file

if cp.has_section('Synchronization'):
    SYNC_TABLES = [t[1] for t in cp.items('Synchronization')]
else:
    SYNC_TABLES = ['follow_ups']

#@+node:ekr.20051104081502.335: *9* @rst-no-head About configuration files
@nocolor

.. sidebar:: A typical *List Manager.ini* file:

    ::

        [Files]
        path0 = wxLMDB:sqlite:mine
        path1 = nycpsszatzsql:mysql:follow_ups

        [Database]
        db = listmanager

        [Note]
        ext = txt

        [Synchronization]
        sync2 = follow_ups
	sync1 = test

        [Hosts]
        remote = nycpsszatzsql:mysql
        local = wxLMDB:sqlite

        [User]
        startup_dialog = true
        user = szatz
        pw = python

        [Mail]
	outlook = true
	path = wxLMDB:sqlite:mail_transfer

        [Configuration]
        y = 642
        x = 975

Application uses the ``ConfigParser`` module ito parse the ini file.  Unfortunately, ``ConfigParser`` doesn't work exactly like I think it should although it has been improved in 2.3.  My main issue is in the handling of default options.  The default options specified through the constructor show up in every section.  For example, if you use the items(*section*) method
then in addition to returning a list of tuples with whatever option/value pairs exist in the section, the list will include all the default option/value pairs, which does not make a whole lot of sense to me.  At the least, there should be a 'nodefaults' argument whose default was *False* but which could be set to *True*.  The following methods should have this option:

- items
- options
- has_option

In any event, because a nodefaults option does not exist, I create the ConfigParser object twice -- once with default options and once without them.  

The application will work fine if there is no ini file. In an effort to save some typing but not be too obscure, many of the options are read such that they default to the correct value either through explicit defaults in the constructor or statements that evaluate to *None* or *False*.

    ``QUICK_LIST = cp.has_option('User','quicklist') and cp.get('User','quicklist') or None``

    ``OUTLOOK = cp.has_option('Mail','outlook') and cp.getboolean('Mail,'outlook')``
#@+node:ekr.20051104081502.336: *7* class ListManager
class ListManager(wxFrame):
    @others

@ @rst-markup

ListManager is the main class in the application and is a sublass of ``wxFrame``, which is typical for a wxPython application.  From a GUI standpoint, the main child window of the ListManager object is a ``wxNoteBook`` object that holds one ``wxListCtrl`` per notebook page and one ``wxListBox``.  The ``wxListCtrl``\s display item information (e.g., name of the item, owners of the item, etc.) for a particular List and the ``wxListBox``\es displays a list of owners that is used to filter the items displayed by the ``wxListCtrl`` object.

Each ``wxListCtrl`` object has its own set of events that it is hooked to (see CreateNewNotebookPage`<< ListControl Events >>`_.
#@+node:ekr.20051104081502.337: *8* Instantiation
#@+node:ekr.20051104081502.338: *9* def __init__
def __init__(self, parent, id, title, size):
    wxFrame.__init__(self, parent, id, title, size = size)

    self.SetIcon(wxIcon('bitmaps//wxpdemo.ico', wxBITMAP_TYPE_ICO))
    self.CreateStatusBar()

    << ListManager Attributes >>
    << Menu Setup >>
    << Toolbar Setup >>
    << Menu/Toolbar Events >>
    << Create Controls>>
    << Layout Stuff >>
    << Other Events >>
    << GUI Instance Objects >>
    << Create Socket >>
    << Load Recent Files >>
    << Idle Timer >>

    ownerthread = threading.Thread(target=self.createownerlist)
    ownerthread.start()
    self.ModifierDialog = None

#@+node:ekr.20051104081502.339: *10* @rst-no-head About the ctor
@nocolor

The ListManager ``__init__`` method is pretty straightforward.  The ``__init__`` arguments are the ones that need to be passed to ``wxFrame __init__`` method. The wxFrame class has the following form:

    ``wxFrame(parent, id, title, pos=wxDefaultPosition, size=wxDefaultSize, style=wxDEFAULT_FRAME_STYLE, name="frame")``

The default style (``wxDEFAULT_FRAME_STYLE``) includes ``wxMINIMIZE_BOX``, ``wxMAXIMIZE_BOX``, ``wxRESIZE_BORDER``, ``wxSYSTEM_MENU``, ``wxCAPTION`` (the latter is the text that appears in the title bar).

``SetIcon`` is a method of ``wxFrame`` that sets the icon in the upper left of the title bar of the frame.  The wxIcon class has the following form:

    ``wxIcon(filename, type, desiredWidth=-1, desiredHeight=-1)``

``CreateStatusBar`` is a method of ``wxFrame``. The wxPython form is:

        ``CreateStatusBar(number=1, style=0, id=-1)``

*number* -->
    number of fields to create. Specify a value greater than 1 to create a multi-field status bar.

``CreateStatusBar`` needs to be called before << Load Recent Files >>.

The various sections of ``__init__`` are explained in their corresponding section::

    << ListManager Attributes >>
    << Menu Setup >>
    << Toolbar Setup >>
    << Menu/Toolbar Events >>
    << Create Controls>>
    << Layout Stuff >>
    << Other Events >>
    << GUI Instance Objects >>
    << Create Socket >>
    << Load Recent Files >>
#@+node:ekr.20051104081502.340: *10* << List Manager Attributes >>
self.PropertyDicts = []
self.ItemLists = []
self.ListCtrls = []
self.OwnerLBoxes = []

self.L = -1
self.curIdx = -1

self.printdata = wxPrintData()
self.printdata.SetPaperId(wxPAPER_LETTER)
self.printdata.SetOrientation(wxPORTRAIT)

#self._options = {} #would be used in loadconfig

self.copyitems = []    
self.modified = {}
self.tickler_active = False

#there is a wxPanel in the AddListControl method so each wxListCtrl has a different panel as parent
#there is a nb_sizer = wxNotebookSizer(nb) class but doesn't seem to make any difference

self.editor = []

self.Cursors = {}
self.sqlite_connections = []
self.popupvisible = False
self.in_place_editor = None
self.showrecentcompleted = 0

self.LC_font = wxFont(9, wxSWISS, wxNORMAL, wxNORMAL)

self.date_titles = {'createdate':"Create Date",'duedate':"Due Date",'timestamp':"Last Modified",'finisheddate':"Completion Date"}
self.attr2col_num = {'priority':0, 'name':1,'owners':2, 'date':3}

self.FindDialog = FindDialog(self, "Find...", "")
self.EvalDialog = EvalDialog(self, "Evaluate...", "")
#@+node:ekr.20051104081502.341: *11* @rst
@nocolor

self.PropertyDicts
    list of dictionaries that describe properties of each ListManager List (note that when referring to a collection of ListManager items a capital *L* List and table are used interchangeably).

self.ItemLists
    list of lists that consist of instance objects of class ``Item``.  Each of the lists contained in self.ItemLists correspond to the items that are being displayed in the ListCtrl.  So ``self.Itemlist[2]`` corresponds to the 2nd tab of the notebook and to the items in self.ListCtrls[2].

The class ``Item`` is just an empty class being used as a convenience to hold item attributes::

    class Item:
        pass

The purpose of the class is just to create an object that can have various attributes as follows:

+-----------------+----------------------------------------------------+
|item.id          |GUID                                                |
+-----------------+----------------------------------------------------+
|item.name        |string that describes the item                      |
+-----------------+----------------------------------------------------+
|item.priority    |integer ranging from 1 (high) to 3 (low)            |
+-----------------+----------------------------------------------------+
|item.owners      |list of the form ["Zatz, Steve", "Hoffman, Steve"]  |
+-----------------+----------------------------------------------------+
|item.note        |string that provides additional info on item        |
+-----------------+----------------------------------------------------+
|item.timestamp   |timestamp indicating when an item was last modified |
+-----------------+----------------------------------------------------+
|item.duedate     |default is None; mx.DateTime date                   |
+-----------------+----------------------------------------------------+
|item.createdate  |mx.DateTime.now() mx.DateTime timestamp             |
+-----------------+----------------------------------------------------+
|item.finisheddate|efaut is None; mx.DateTime date                     |
+-----------------+----------------------------------------------------+

self.ListCtrls
    list of of instance objects of class ListCtrls, which are a subclass of wxPython class wxListCtrl.

self.OwnerLBoxes
    list of of instance objects of wxPython class wxListBox, which is a simple one column List Control.

The wxPython constructor for a wxListBox is:

    ``wxListBox(parent, id, pos=wxDefaultPosition, size=wxDefaultSize, choices=[], style=0)``

self.L
    index of the currently active notebook tab.  If there are any tabs in the notebook then one of them is always selected.  If there are no tabs then this is indicated by setting ``self.L = -1``.

self.curIdx
    currently selected row in the active ``ListCtrl``.  There are times like after a row is deleted in which there may be rows visible but no row is selected.

The following lines set the default printer data::

    self.printdata = wxPrintData()
    self.printdata.SetPaperId(wxPAPER_LETTER)
    self.printdata.SetOrientation(wxPORTRAIT)


The wxPython class ``wxPrintData`` holds a variety of information related to printers and printer device contexts. This class is used to create a wxPrinterDC and a wxPostScriptDC. It is also used as a data member of wxPrintDialogData and wxPageSetupDialogData, as part of the mechanism for transferring data between the print dialogs and the application.

self.copyitems
    list that contains item instance objects that have been copied from one list to be moved to another list.

self.modified
    dictionary that contains the information concerning whether any of several elements have been changed.  Chose a dictionary more to test the idea that I could create a simple method that would update the dictionary and here is an example:

    ``EVT_TEXT(self, self.name.GetId(), lambda e: self.modified.update({'name':1}))``

So this lambda function means that if an ``EVT_TEXT`` event occurs then update the dictionary by adding the key to the dictionary (the value is not used and arbitrarily set to 1).  The wxPython form for the macro ``EVT_TEXT`` is:

    ``EVT_TEXT(window, id, func)``

A ``wxEVT_COMMAND_TEXT_UPDATED`` event is generated when the text in a ``wxTextCtrl`` changes and that is what ``EVT_TEXT`` catches. Note that this event will always be sent when the text control’s content changes - whether this is due to user input or comes programmatically (for example, if ``SetValue()`` is called)

self.Cursors
    dictionary that holds the database cursor objects.  For example, it will look like:  ``{'sqlite':<sqlite cursor object>,'nycpsltszatz':<mysql cursor object>}``

self.tickler_active
    booean determines whether the tickler capabililty is active; can be shut off by unchecking Tickler menu item

self.editor
    list that holds the dictionaries that describe the notes that are edited by the external text editor::

        [
        {
        'table': 'mine',
        'host': 'wxLMDB:sqlite',
        'path': 'C:\\DOCUME~1\\STEVEN~1\\LOCALS~1\\Temp\\Journal Scan schedule.txt',
        'id': '1AB34FB9-9EE6-4AFC-8AF0-FFCA50103BF3',
        'time': 1070850894
        }, 
        {
        'table': 'factoids',
        'host': 'wxLMDB:sqlite',
        'path': 'C:\\DOCUME~1\\STEVEN~1\\LOCALS~1\\Temp\\How many cme programs are sponsored- - 91%.txt', 
        'id': '9CAC4D18-DE1C-4535-B9A5-4CDB1AD3F304', 
        'time': 1070850908
        }
        ]

The method that uses self.editor is `<< Check if Edited File has Changed >>`_.

There is a ``wxPanel`` in the ``AddListControl`` method so each ``wxListCtrl`` has a different panel as parent.

There is a nb_sizer = wxNotebookSizer(nb) class but doesn't seem to make any difference.

self.sqlite_connections
    Here because the sqlite connection has a weakreference that deletes it when you want it around

self.popupvisible
    boolean that is used to ensure that two reminder popups aren't visible at the same time.

self.in_place_editor 
    boolean that indicates whether the inplace item name text editor is active or not.

self.showrecentcompleted
    integer that determines the number of days in the past to retain completed items in the display.

self.LC_font
    default font for all of the ``ListCtrls``:  ``self.LC_font = wxFont(9, wxSWISS, wxNORMAL, wxNORMAL)``

The wxPython ``wxFont`` constructor is:

    ``wxFont(pointSize, family, style, weight, underline=False, faceName="", wencoding=wxFONTENCODING_DEFAULT)``

self.date_titles
    dictionary that holds the various dates that are associated with each item and which can be displayed in the date column.  The dictionary is not modified.  We use one column of each ``ListCtrl`` to display any one of the four dates that that the application tracks. This dictionary associates the item attribute with the text that will be displayed in both the column header for the date and in the dropdown that allows you to change the date:  ``self.date_titles = {'createdate':"Create Date",'duedate':"Due Date",'timestamp':"Last Modified",'finisheddate':"Completion Date"}``

self.attr2col_num
    dictionary that associates the item attribute with the column that attribute is displayed in in the ``ListCtrl``:  ``self.attr2col_num = {'priority':0, 'name':1,'owners':2, 'date':3}``

The following lines construct the Find Dialog and the Dialog that catches errors and shows expressions for debugging::

    self.FindDialog = FindDialog(self, "Find...", "")
    self.EvalDialog = EvalDialog(self, "Evaluate...", "")
#@+node:ekr.20051104081502.342: *10* << Menu Setup >>
filemenu = wxMenu()
filemenu.Append(idNEWLIST, "New List...", "Create a new List")
filemenu.Append(idOPENLIST, "Open List...", "Open a List")
filemenu.Append(idCLOSELIST, "Close", "Close the current List")
filemenu.Append(idCLOSEALL, "Close All", "Close all open Lists")
filemenu.Append(idSAVEAS, "Save As Text File...", "Save the current List")
filemenu.AppendSeparator()
filemenu.Append(idDELETELIST, "Delete List...", "Select a list to delete")
filemenu.AppendSeparator()
filemenu.Append(idPAGESETUP, "Page Setup...")
filemenu.Append(idPRINT, "Print...", "Print the current view")
filemenu.Append(idPRINTPREV, "Print Preview")
filemenu.AppendSeparator()
filemenu.Append(idMAILLIST, "Mail...", "Mail the current view")
filemenu.AppendSeparator()
filemenu.AppendCheckItem(idOFFLINE, "Work Offline")
filemenu.AppendSeparator()
filemenu.Append(idEXIT, "Exit", "Exit the program")

editmenu = wxMenu()
editmenu.Append(idCUT, "Cut\tCtrl+X")
editmenu.Append(idCOPY, "Copy\tCtrl+C")
editmenu.Append(idPASTE, "Paste\tCtrl+V")
editmenu.AppendSeparator()
editmenu.Append(idDELETEITEMS, "Delete")
editmenu.AppendSeparator()
editmenu.Append(idCOMBINEITEMS, "Combine Items...")
editmenu.AppendSeparator()
editmenu.Append(idFIND, "Find...")

itemmenu = wxMenu()
itemmenu.Append(idNEWITEM, "New Item")
itemmenu.AppendSeparator()
itemmenu.Append(idTOGGLEFINISHED, "Toggle Finished")
itemmenu.Append(idEDITOWNER, "Owner...")
itemmenu.Append(idDUEDATE, "Due Date...")
itemmenu.Append(idEDITNOTE, "Note...")
itemmenu.AppendSeparator()
itemmenu.Append(idMAILITEM, "Mail...")

displaymenu = wxMenu()
displaymenu.Append(idSHOWFINISHED, "Show/Hide Finished...")
displaymenu.AppendSeparator()
displaymenu.Append(idSHOWALL, "Show All", "Show all items in the current list")
displaymenu.AppendSeparator()
displaymenu.Append(idREFRESH, "Refresh Display", "Refresh the Display")
displaymenu.Append(idDISPLAYDATE, "Select Date to Display")

toolmenu = wxMenu()
toolmenu.AppendCheckItem(idTICKLERACTIVE, "Tickler Active")
toolmenu.Check(idTICKLERACTIVE,False)
toolmenu.Append(idSHOWNEXT, "Show Next Reminder")
toolmenu.Append(idSYNC, "Synchronize local and remote DBs")
toolmenu.Append(idARCHIVE, "Archive completed items in list...")
toolmenu.Append(idEVALUATE, "Evaluate an expression...")

helpmenu = wxMenu()
helpmenu.Append(idABOUT, "About ListManager")
helpmenu.Append(idHELP, "Help")

menubar = wxMenuBar()
menubar.Append(filemenu, '&File')
menubar.Append(editmenu, 'Edit')
menubar.Append(itemmenu, 'Item')
menubar.Append(displaymenu, 'Display')
menubar.Append(toolmenu, 'Tools')
menubar.Append(helpmenu, 'Help')
self.SetMenuBar(menubar)
toolmenu.Enable(idSHOWNEXT,self.tickler_active)
filemenu.Enable(idDELETELIST,DELETE_LIST)
filemenu.Check(idOFFLINE,OFFLINE_ONLY)

#file history
self.filehistory = wxFileHistory()
self.filehistory.UseMenu(filemenu)

#@+node:ekr.20051104081502.343: *11* @rst
@nocolor

+------------------------+------------------------------------------------+
|**File Menu**           |                                                |
+------------------------+------------------------------------------------+
| "New List... "         ||nl| ``self.OnNewList``                         |
+------------------------+------------------------------------------------+
| "Open List..."         ||ol| ``self.OnOpenList``                        |
+------------------------+------------------------------------------------+
| "Close"                |``self.OnCloseList``                            |
+------------------------+------------------------------------------------+
| "Close All"            |``self.OnCloseAll``                             |
+------------------------+------------------------------------------------+
| "Save As Text File..." |``self.OnSaveAsText``                           |
+------------------------+------------------------------------------------+
| "Delete List..."       ||de| ``self.OnDeleteList``                      |
+------------------------+------------------------------------------------+
| "Page Setup..."        ||ps| ``self.OnPageSetup``                       |
+------------------------+------------------------------------------------+
| "Print..."             ||pt| ``self.OnPrint``                           |
+------------------------+------------------------------------------------+
| "Print Preview"        ||pp| ``lambda e: self.OnPrint(e, prev=True)``   |
+------------------------+------------------------------------------------+
| "Mail..."              |``self.OnMailView``                             |
+------------------------+------------------------------------------------+
| "Work Offline"         |``self.OnWorkOffline``                          |
+------------------------+------------------------------------------------+
| "Exit"                 |``self.OnExit``                                 |
+------------------------+------------------------------------------------+
| **Edit Menu**          |                                                |
+------------------------+------------------------------------------------+
| "Cut" [Ctrl+X ]        ||ec| ``lambda e: self.OnCopyItems(e, cut=True)``|
+------------------------+------------------------------------------------+
| "Copy" [Ctrl+C]        ||ey| ``self.OnCopyItems``                       |
+------------------------+------------------------------------------------+
| "Paste" [Ctrl+V]       ||ep| ``self.OnPasteItems``                      |
+------------------------+------------------------------------------------+
| "Delete"               ||de| ``self.OnDeleteItems``                     |
+------------------------+------------------------------------------------+
| "Combine Items..."     |``self.OnCombineItems``                         |
+------------------------+------------------------------------------------+
| "Find..."              ||fi| ``self.OnFind``                            |
+------------------------+------------------------------------------------+
| **Item Menu**          |                                                |
+------------------------+------------------------------------------------+
| "New Item"             ||ni| ``self.OnNewItem``                         |
+------------------------+------------------------------------------------+
| "Toggle Finished"      ||co| ``self.OnToggleFinished``                  |
+------------------------+------------------------------------------------+
| "Owner..."             ||ow| ``self.OnEditOwner``                       |
+------------------------+------------------------------------------------+
| "Due Date..."          ||dd| ``self.OnDueDate``                         |
+------------------------+------------------------------------------------+
| "Note..."              ||en| ``self.OnEditNote``                        |
+------------------------+------------------------------------------------+
| "Mail..."              ||mi| ``self.OnMailItem``                        |
+------------------------+------------------------------------------------+
| **Display Menu**       |                                                |
+------------------------+------------------------------------------------+
| "Show/Hide Finished..."|``self.OnShowFinished``                         |
+------------------------+------------------------------------------------+
| "Show All"             |``self.OnShowAll``                              |
+------------------------+------------------------------------------------+
| "Refresh Display"      ||re| ``self.OnRefresh``                         |
+------------------------+------------------------------------------------+
|"Select Date to Display"|``self.OnDisplayDateCategory``                  |
+------------------------+------------------------------------------------+
| **Tool Menu**          |                                                |
+------------------------+------------------------------------------------+
| "Tickler Active"       |``self.OnActivateTickler``                      |
+------------------------+------------------------------------------------+
| "Show Next Reminder"   |``self.OnShowTickler``                          |
+------------------------+------------------------------------------------+
| "Synchronize ..."      |``self.OnSync``                                 |
+------------------------+------------------------------------------------+
| "Archive completed..." |``self.OnArchive``                              |
+------------------------+------------------------------------------------+
| "Evaluate expression"  |``self.OnShowEvaluate``                         |
+------------------------+------------------------------------------------+
| **Help Menu**          |                                                |
+------------------------+------------------------------------------------+
| "About ListManager"    |``self.OnShowAbout``                            |
+------------------------+------------------------------------------------+
| "Help"                 |``self.OnShowHelp``                             |
+------------------------+------------------------------------------------+




#@+node:ekr.20051104081502.344: *10* << Toolbar Setup >>
tb = self.CreateToolBar(wxTB_HORIZONTAL|wxTB_FLAT)

tb.AddLabelTool(idNEWLIST, "New (local) List", wxBitmap('bitmaps\\new.bmp'), shortHelp="Create New List")
tb.AddLabelTool(idOPENLIST, "Open", wxBitmap('bitmaps\\open.bmp'), shortHelp="Open List")
tb.AddSeparator()
tb.AddLabelTool(idTOOLPRINT, "Print", wxBitmap('bitmaps\\print.bmp'), shortHelp="Print List")
tb.AddLabelTool(idPRINTPREV, "Preview", wxBitmap('bitmaps\\preview.bmp'), shortHelp="Print Preview")
tb.AddLabelTool(idPAGESETUP, "Setup", wxBitmap('bitmaps\\setup.bmp'), shortHelp="Page Setup")
tb.AddSeparator()
tb.AddLabelTool(idNEWITEM, "New Item", wxBitmap('bitmaps\\new_item.bmp'), shortHelp="Create New Item")
tb.AddSeparator()
tb.AddLabelTool(idREFRESH, "Refresh", wxBitmap('bitmaps\\refresh.bmp'), shortHelp="Refresh Display")     
tb.AddSeparator()
tb.AddLabelTool(idEDITNOTE, "Edit Note", wxBitmap('bitmaps\\edit_doc.bmp'), shortHelp="Edit Note")
tb.AddSeparator()
tb.AddLabelTool(idFIND, "Find", wxBitmap('bitmaps\\find.bmp'), shortHelp = "Find Item")        
tb.AddSeparator()
tb.AddLabelTool(idCUT, "Cut", wxBitmap('bitmaps\\editcut.bmp'), shortHelp ="Cut Item")        
tb.AddLabelTool(idCOPY, "Copy", wxBitmap('bitmaps\\copy.bmp'), shortHelp ="Copy Item")
tb.AddLabelTool(idPASTE, "Paste", wxBitmap('bitmaps\\paste.bmp'), shortHelp="Paste Item")
tb.AddSeparator()
tb.AddLabelTool(idTOGGLEFINISHED, "Toggle Date", wxBitmap('bitmaps\\filledbox.bmp'), shortHelp="Toggle Finished Date")
tb.AddLabelTool(idDELETEITEMS, "Delete", wxBitmap('bitmaps\\delete.bmp'), shortHelp="Delete Item")
tb.AddLabelTool(idDUEDATE, "Due Date", wxBitmap('bitmaps\\calendar.bmp'), shortHelp="Enter Due Date")
tb.AddLabelTool(idEDITOWNER,"Owner", wxBitmap('bitmaps\\owners.bmp'), shortHelp="Select Owner(s)")
tb.AddSeparator()
tb.AddLabelTool(idMAILITEM, "Mail", wxBitmap('bitmaps\\mail.bmp'), shortHelp="Mail Item")

if QUICK_LIST:
    tb.AddSeparator()
    tb.AddLabelTool(idSENDTO, "Send to", wxBitmap('bitmaps\\sendto.bmp'), shortHelp="Send to %s"%QUICK_LIST)

tb.Realize()
#@+node:ekr.20051104081502.346: *10* << Menu/Toolbar Events >>
#File Menu ------------------------------------
EVT_MENU(self, idNEWLIST, self.OnNewList)
EVT_MENU(self, idOPENLIST, self.OnOpenList)
EVT_MENU(self, idCLOSELIST, self.OnCloseList)
EVT_MENU(self, idCLOSEALL, self.OnCloseAll)
EVT_MENU(self, idSAVEAS, self.OnSaveAsText)
EVT_MENU(self, idDELETELIST, self.OnDeleteList)
EVT_MENU(self, idPAGESETUP, self.OnPageSetup)
EVT_MENU(self, idPRINT, self.OnPrint)
EVT_MENU(self, idPRINTPREV, lambda e: self.OnPrint(e, prev=True))
EVT_MENU(self, idOFFLINE, self.OnWorkOffline)
EVT_MENU(self, idMAILLIST, self.OnMailView)      
EVT_MENU_RANGE(self, wxID_FILE1, wxID_FILE9, self.OnFileList)
EVT_MENU(self, idEXIT, self.OnExit)
#Edit Menu ------------------------------------
EVT_MENU(self, idCUT, lambda e: self.OnCopyItems(e, cut=True))        
EVT_MENU(self, idCOPY, self.OnCopyItems)
EVT_MENU(self, idPASTE, self.OnPasteItems)
EVT_MENU(self, idDELETEITEMS, self.OnDeleteItems)
EVT_MENU(self, idCOMBINEITEMS, self.OnCombineItems)
EVT_MENU(self, idFIND, self.OnFind)
#item Menu ------------------------------------
EVT_MENU(self, idNEWITEM, self.OnNewItem)
EVT_MENU(self, idTOGGLEFINISHED, self.OnToggleFinished)             
EVT_MENU(self, idDUEDATE, self.OnDueDate)
EVT_MENU(self, idEDITOWNER, self.OnEditOwner)
EVT_MENU(self, idEDITNOTE, self.OnEditNote)
EVT_MENU(self, idMAILITEM, self.OnMailItem)
#Dips Menu ------------------------------------
EVT_MENU(self, idSHOWFINISHED, self.OnShowFinished)
EVT_MENU(self, idSHOWALL, self.OnShowAll)
EVT_MENU(self, idREFRESH, self.OnRefresh)
EVT_MENU(self, idDISPLAYDATE, self.OnDisplayDateCategory)
#Tool Menu ---------------------------------------
EVT_MENU(self, idTICKLERACTIVE, self.OnActivateTickler)
EVT_MENU(self, idSHOWNEXT, self.OnShowTickler)
EVT_MENU(self, idSYNC, self.OnSync)
EVT_MENU(self, idARCHIVE, self.OnArchive)
EVT_MENU(self, idEVALUATE, self.OnShowEvaluate)
#Help Menu -----------------------------------------
EVT_MENU(self, idABOUT, self.OnShowAbout)
EVT_MENU(self, idHELP, self.OnShowHelp)

EVT_TOOL(self, idTOOLPRINT, lambda e: self.OnPrint(e,showprtdlg=False))

if QUICK_LIST:
    EVT_TOOL(self, idSENDTO, lambda e: self.OnMoveToSpecificList(e,QUICK_LIST))
#@+node:ekr.20051104081502.348: *10* << Create Controls>>
upper_panel = wxPanel(self, -1)   #size = (900,400)
bottom_panel = wxPanel(self, -1, size = (900,150)) #900 note that 000 seems to work???

nb = wxNotebook(upper_panel, -1, size=(900,500), style=wxNB_BOTTOM)

f = wxFont(10, wxSWISS, wxNORMAL, wxNORMAL)
self.name = wxTextCtrl(bottom_panel, -1, size = (285,42), style = wxTE_MULTILINE|wxTE_RICH2)#34 #wxTE_PROCESS_ENTER
self.name.SetDefaultStyle(wxTextAttr("BLACK", font = f))

self.owners = wxTextCtrl(bottom_panel, -1, size = (250,42),style = wxTE_MULTILINE|wxTE_RICH2)
self.owners.SetDefaultStyle(wxTextAttr("BLACK", font = f))

self.note = wxTextCtrl(bottom_panel, -1, size = (400,50), style=wxTE_MULTILINE)

#@+node:ekr.20051104081502.350: *10* << Other Events >>
EVT_TEXT(self, self.name.GetId(), lambda e: self.modified.update({'name':1}))
EVT_TEXT(self, self.note.GetId(), lambda e: self.modified.update({'note':1}))
EVT_TEXT(self, self.owners.GetId(), lambda e: self.modified.update({'owners':1}))

EVT_CLOSE(self, self.OnWindowExit)

EVT_IDLE(self, self.OnIdle)

#@+node:ekr.20051104081502.352: *10* << Layout Stuff >>
#Appears necessary to really get the listcontrol to size with the overall window  
#upper_panel sizer
sizer = wxBoxSizer(wxHORIZONTAL)
sizer.Add(nb,1,wxALIGN_LEFT|wxEXPAND)
upper_panel.SetSizer(sizer)        

#sizer for the row of data items
box = wxBoxSizer(wxHORIZONTAL)
box.Add(self.name,1,wxEXPAND)
box.Add(self.owners,0)

#bottom_panel sizer  
sizer = wxBoxSizer(wxVERTICAL)        
sizer.AddSizer(box, 0, wxGROW|wxALIGN_CENTER_VERTICAL|wxALL, 5)
sizer.Add(self.note,1,wxALIGN_LEFT|wxEXPAND)
bottom_panel.SetSizer(sizer)

sizer = wxBoxSizer(wxVERTICAL)
sizer.Add(upper_panel,1,wxALIGN_TOP|wxEXPAND)
sizer.Add(bottom_panel,0,wxALIGN_TOP|wxEXPAND)

self.SetAutoLayout(1)
self.SetSizer(sizer)
#sizer.Fit(self) #actively does bad things to the dimensions on startup
#@+node:ekr.20051104081502.354: *10* << GUI Instance Objects >>
self.toolmenu = toolmenu
self.filemenu = filemenu
self.nb = nb
self.tb = tb
#@+node:ekr.20051104081502.356: *10* << Create Socket >>
if OUTLOOK:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Create a TCP socket
    s.bind(('localhost',8888)) # Bind to port 8888
    s.listen(5) # Listen, but allow no more than
    self.sock = s
#@+node:ekr.20051104081502.358: *10* << Load Recent Files >>
try:
    pathlist = [f[1] for f in cp.items('Files')]
except:
    pathlist = []

if pathlist:
    pathlist.sort()
    pathlist.reverse()
    for path in pathlist[1:]:
        self.OnFileList(path=path)

    #don't want to trigger the page change event until n-1 of n files are loaded
    EVT_NOTEBOOK_PAGE_CHANGED(self,nb.GetId(),self.OnPageChange)

    self.OnFileList(path=pathlist[0])
else:
    EVT_NOTEBOOK_PAGE_CHANGED(self,nb.GetId(),self.OnPageChange)



#@+node:ekr.20051104081502.360: *10* << Idle Timer >>
ID_TIMER = wxNewId()
self.timer = wxTimer(self, ID_TIMER) 
EVT_TIMER(self,  ID_TIMER, self.OnIdle)
self.timer.Start(3000)
#@+node:ekr.20051104081502.362: *8* Ownerlist creation methods (used by thread)
#@+node:ekr.20051104081502.364: *9* def createownerlist
def createownerlist(self):

    if REMOTE_HOST and OFFLINE_ONLY is False:
        cursor = self.GetCursor(REMOTE_HOST)
        sql = "SHOW TABLES" #sorted
    else:
        cursor = self.GetCursor(LOCAL_HOST)
        sql = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"

    cursor.execute(sql)
    results = cursor.fetchall()

    #excluding 'system' tables and archive tables
    excluded_tables = ['user_sync','sync','owners']
    tables = [t for (t,) in results if t.find('_archive')== -1 and t not in excluded_tables]

    sql_list = []
    for table in tables:
        sql_list.append("""SELECT owner1 FROM %s UNION SELECT owner2 FROM %s UNION SELECT owner3 FROM %s"""%((table,)*3))

    sql = " UNION ".join(sql_list)
    cursor.execute(sql)
    results = cursor.fetchall()

    _list = [x[0] for x in results]
    if '' in _list:
        _list.remove('')
    if None in _list:
        _list.remove(None)

    self._list = _list

    #posting custom event to signal that this thread is done
    evt = wxPyEvent()
    evt_id = wxNewEventType()
    evt.SetEventType(evt_id)
    self.Connect(-1, -1, evt_id, self.createownerdialog)
    wxPostEvent(self, evt)

#@+node:ekr.20051104081502.366: *9* def createownerdialog
def createownerdialog(self, evt=None):
    self.ModifierDialog = ModifierDialog(parent=self, title="Select owner(s)", size=(180,300), style=wxCAPTION, modifierlist = self._list)
    del self._list

#@+node:ekr.20051104081502.368: *8* Notebook methods
#@+node:ekr.20051104081502.370: *9* def CreateNewNotebookPage
def CreateNewNotebookPage(self, host, table):

    Properties = {'owner':'*ALL',
                'LCdate':'duedate',
                'sort':{'attribute':'priority','direction':0}, #these could be set in Config
                'showfinished':0} #-1 show them all; 0 show none; integer show for that many days

    Properties['table'] = table
    Properties['host'] = host

    self.PropertyDicts.append(Properties)

    self.L = len(self.ItemLists)#could use self.ListCtrls, self.OwnerLBoxes, etc. with a -1

    results = self.ReadFromDB()
    if results is None:
        self.PropertyDicts = self.PropertyDicts[:-1]
        self.L = self.L - 1
        return

    panel = wxPanel(self.nb, -1, size = (900,400))
    LCtrl = ListCtrl(panel, -1, style=wxLC_REPORT|wxSUNKEN_BORDER|wxLC_VRULES|wxLC_HRULES)
    LCtrl.SetFont(self.LC_font)
    self.ListCtrls.append(LCtrl)

    OLBox = wxListBox(panel, -1, size = (126,550), choices = [""], style=wxLB_SORT|wxSUNKEN_BORDER)
    self.OwnerLBoxes.append(OLBox)

    sizer = wxBoxSizer(wxHORIZONTAL)
    sizer.Add(OLBox,0,wxALIGN_LEFT|wxEXPAND)
    sizer.Add(LCtrl,1,wxALIGN_LEFT|wxEXPAND)
    panel.SetSizer(sizer)

    self.ItemLists.append(self.CreateAndDisplayList(results)) 

    << Fill OwnerListBox >>
    << ListControl Events >>

    #img_num = LCtrl.arrows[Properties['sort']['direction']]
    #LCtrl.SetColumnImage(self.attr2col_num[Properties['sort']['attribute']], img_num)

    rdbms = host.split(':')[1]
    if rdbms == 'mysql':
        tab_title = '%s (remote)'%table
    else:
        tab_title = table

    if table in SYNC_TABLES:
        tab_title = '*'+tab_title

    self.nb.AddPage(panel,tab_title)
    self.nb.SetSelection(self.L)

    self.filehistory.AddFileToHistory('%s:%s'%(host,table))

    self.SetStatusText("Successfully loaded %s"%tab_title)

#@+node:ekr.20051104081502.372: *10* << Fill OwnerListBox >>
cursor = self.GetCursor(host)
if cursor is None:
    g.pr("Couldn't get cursor to fill OwnerListBox")
    return

cursor.execute("SELECT owner1 FROM %s UNION SELECT owner2 FROM %s UNION SELECT owner3 FROM %s"%((table,)*3))

owners = [x for (x,) in cursor.fetchall()]

if None in owners:
    owners.remove(None)
if '' in owners:
    owners.remove('')

OLBox.Clear()
for name in owners: 
    OLBox.Append(name)
OLBox.Append('*ALL')
OLBox.SetSelection(0)

#@+node:ekr.20051104081502.374: *10* << ListControl Events >>
LCId = LCtrl.GetId()
EVT_LIST_ITEM_SELECTED(self, LCId, self.OnItemSelected)
EVT_LIST_ITEM_ACTIVATED(self, LCId, self.OnDisplayInPlaceEditor)
EVT_LEFT_DOWN(LCtrl, self.OnLeftDown) 
EVT_LEFT_DCLICK(LCtrl, self.OnLeftDown)
EVT_RIGHT_DOWN(LCtrl, self.OnRightDown)
EVT_LIST_COL_CLICK(self, LCId, self.OnColumnClick)
EVT_LIST_COL_RIGHT_CLICK(self, LCId, self.OnColumnRightClick)

# the following is a ListBox event
EVT_LISTBOX(self, OLBox.GetId(), self.OnFilterOwners)

#@+node:ekr.20051104081502.376: *9* def OnPageChange
def OnPageChange(self, evt=None):
    if self.modified:
        self.OnUpdate()

    self.L = L = self.nb.GetSelection()

    << Find Highlighted Row >>
    << Update Title >>

    evt.Skip() #051403

#@+node:ekr.20051104081502.378: *10* << Find Highlighted Row >>
idx = self.ListCtrls[L].GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)
if idx != -1:
    self.curIdx = idx
    #LCtrl.EnsureVisible(idx)
    self.OnItemSelected()
elif self.ItemLists[L]:
    self.curIdx = 0
    self.ListCtrls[L].SetItemState(0, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED)
    #the line above triggers an OnItemSelected EVT so don't need self.OnItemSelected() 092803
else:
    self.curIdx = -1

#@+node:ekr.20051104081502.380: *10* << Update Title >>
location,rdbms = self.PropertyDicts[L]['host'].split(':')
table = self.PropertyDicts[L]['table']
self.SetTitle("List Manager:  %s:  %s:  %s"%(location,rdbms,table))

#@+node:ekr.20051104081502.382: *8* Tickler methods
#@+node:ekr.20051104081502.383: *9* def OnShowTickler
def OnShowTickler(self, evt=None):
    if self.popupvisible:
        return

    self.popupvisible = True

    host = 'wxLMDB:sqlite'
    cursor = self.Cursors[host]
    table = 'follow_ups'

    sql = "SELECT COUNT() FROM "+table+" WHERE finisheddate IS NULL AND priority > 1"
    cursor.execute(sql)
    results = cursor.fetchone()

    num_items = int(results[0])

    if not num_items:
        return

    if self.modified: #Should decide if this should be put back or not
        self.OnUpdate()

    n = random.randint(0,num_items-1)

    sql = "SELECT priority,name,createdate,finisheddate,duedate,owner1,owner2,owner3,id,timestamp,note FROM "+table+" WHERE finisheddate IS NULL AND priority > 1 LIMIT 1 OFFSET %d"%n

    try:
        cursor.execute(sql)
    except:
        g.pr("In OnShowTickler and attempt to Select an item failed")
        return

    row = cursor.fetchone()

    class Item: pass
    item = Item()

    item.priority = int(row[0]) #int(row[0]) needs int because it seems to come back as a long from MySQL
    item.name = row[1]
    item.createdate = row[2]
    item.finisheddate = row[3]
    item.duedate = row[4]
    item.owners = [z for z in row[5:7] if z is not None] #if you carry around ['tom',None,None] you have an issue when you go write it
    item.id = row[8]
    item.timestamp = row[9]
    item.note = row[10]

    dlg = TicklerDialog(self, "", "Do something about this!!!", size=(550,350))
    TC = dlg.TC

    f = wxFont(14, wxSWISS, wxITALIC, wxBOLD, False)
    TC.SetDefaultStyle(wxTextAttr("BLUE",wxNullColour, f))
    TC.AppendText("%s..."%item.name)

    if item.priority == 3:
        TC.SetDefaultStyle(wxTextAttr("RED","YELLOW",f))
    TC.AppendText("%d\n\n"%item.priority)

    f = wxFont(8, wxSWISS, wxNORMAL, wxNORMAL)
    TC.SetDefaultStyle(wxTextAttr("BLACK","WHITE", f))
    TC.AppendText("owners: %s\n"%", ".join(item.owners))
    TC.AppendText("created on: %s\n"%item.createdate.Format('%m/%d/%y'))
    if item.duedate:
        ddate = item.duedate.Format('%m/%d/%y')
    else:
        ddate = "<no due date>"
    TC.AppendText("due on: %s\n\n"%ddate)

    note = item.note
    if not note:
        note = "<no note>"
    TC.AppendText("%s\n\n"%note)
    f = wxFont(10, wxSWISS, wxITALIC, wxBOLD)
    TC.SetDefaultStyle(wxTextAttr("BLACK",wxNullColour, f))
    TC.AppendText('follow_ups')
    TC.ShowPosition(0)   #did not do anything
    TC.SetInsertionPoint(0)
    result = dlg.ShowModal()
    dlg.Destroy()
    self.popupvisible = False     

    if result in (wxID_OK, wxID_APPLY):

        for L,Properties in enumerate(self.PropertyDicts):
            if Properties['table'] == table:
                break
        else:
            g.pr("Can't find %s"%table)
            return

        self.nb.SetSelection(L) #if the page changes it sends a EVT_NOTEBOOK_PAGE_CHANGED, which calls OnPageChange
        self.L = L
        self.FindNode(item)
        if result==wxID_APPLY:
            self.OnMailItem(item)

    elif result==wxID_FORWARD:
        self.OnShowTickler()

#@+node:ekr.20051104081502.384: *9* def OnActivateTickler
def OnActivateTickler(self, evt):
    self.tickler_active = not self.tickler_active
    self.toolmenu.Enable(idSHOWNEXT,self.tickler_active)


#@+node:ekr.20051104081502.385: *8* Email methods
#@+node:ekr.20051104081502.386: *9* OnMailItem
def OnMailItem(self, evt=None, item=None):
    if item is None:
        if self.curIdx == -1:
            return
        else:
            item = self.ItemLists[self.L][self.curIdx]

    dlg = MailDialog(self,"Mail a reminder", size=(450,500),
               recipients=item.owners,    
               subject=item.name,
               body=self.GetNote())          
    result = dlg.ShowModal()
    if result==wxID_OK:
        outlook= Dispatch("Outlook.Application")
        newMsg = outlook.CreateItem(olMailItem) #outlook.CreateItem(constants.olMailItem)
        newMsg.To = to = dlg.RTC.GetValue()
        newMsg.Subject = subject = dlg.STC.GetValue()
        newMsg.Body = body = dlg.BTC.GetValue()

        #newMsg.FlagStatus = constants.olFlagMarked

        newMsg.Display()

        dlg.Destroy()            
        #del outlook

        self.note.SetSelection(0,0)
        self.note.WriteText("**************************************************\n")
        self.note.WriteText("Email sent on %s\n"%mx.DateTime.today().Format("%m/%d/%y"))
        self.note.WriteText("To: %s\n"%to)
        self.note.WriteText("Subject: %s\n"%subject)
        self.note.WriteText("%s\n"%body)
        self.note.WriteText("**************************************************\n")

#@+node:ekr.20051104081502.387: *9* OnMailView
def OnMailView(self, evt=None):
    recipients = [self.PropertyDicts[self.L]['owner']]

    body = ""
    for i,item in enumerate(self.ItemLists[self.L]):
        body = body+"%d. %s (%d)\n"%(i+1, item.name, item.priority)

    subject = "Follow-ups " + mx.DateTime.today().Format("%m/%d/%y")

    dlg = MailDialog(self,"Follow-up List", size=(450,500),
               recipients=recipients,
               subject=subject,
               body=body)

    val = dlg.ShowModal()
    dlg.Destroy()
    if val==wxID_OK:
        outlook= Dispatch("Outlook.Application")
        newMsg = outlook.CreateItem(olMailItem) #outlook.CreateItem(constants.olMailItem)
        newMsg.To = dlg.RTC.GetValue()
        newMsg.Subject = dlg.STC.GetValue()
        newMsg.Body = dlg.BTC.GetValue()

        newMsg.FlagStatus = olFlagMarked #constants.olFlagMarked
        newMsg.Categories = "Follow-up"

        newMsg.Display()

        #del outlook

#@+node:ekr.20051104081502.388: *8* Cut/Copy/Paste methods
#@+node:ekr.20051104081502.389: *9* OnCopyItems
def OnCopyItems(self, event=None, cut=False):
    if self.curIdx == -1:
        return

    L = self.L
    IList = self.ItemLists[L]
    LCtrl = self.ListCtrls[L]

    << Find Highlighted Items >>

    self.SetStatusText("%d items copied"%len(copyitems))
    if cut:
        self.OnDeleteItems()

#@+node:ekr.20051104081502.390: *10* << Find Highlighted Items >>
copyitems = []
i = -1
while 1:
    i = LCtrl.GetNextItem(i, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)
    if i==-1:
        break
    item = IList[i]
    item.notes = self.GetNote(L,item) #handles the database situation
    copyitems.append(item)

self.copyitems = copyitems
#@+node:ekr.20051104081502.391: *9* OnPasteItems
def OnPasteItems(self, evt=None, L=None): #noselect 051603
    #used by OnMoveToList, OnMoveToSpecificList and called directly
    if not self.copyitems:
        g.pr("Nothing was selected to be copied")
        return

    if L is None: #this is not needed by OnMoveTo or OnDragToTab but is for a straight call
        L = self.L

    Properties = self.PropertyDicts[L]
    LCtrl = self.ListCtrls[L]
    IList = self.ItemLists[L]

    items = self.copyitems
    numitems = len(items)

    for item in items:

        z = item.owners+[None,None,None]

        id = self.GetUID() #we do give it a new id
        host = Properties['host']
        cursor = self.Cursors[host]
        table = Properties['table']

        createdate = mx.DateTime.now() #need this or else it won't be seen as a new item when synching; would be seen as updated
        command = "INSERT INTO "+table+" (priority,name,createdate,finisheddate,duedate,note,owner1,owner2,owner3,id) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)"
        cursor.execute(command,(item.priority,item.name,createdate,item.finisheddate,item.duedate,item.notes,z[0],z[1],z[2],id))

        timestamp = self.TimeStamper(host, cursor, table, id)

        #creating a new item breaks the connection between item.x and new_item.x
        class Item: pass
        new_item = Item()
        new_item.id = id
        new_item.priority = item.priority
        new_item.owners = item.owners
        new_item.name = item.name
        new_item.timestamp = timestamp
        new_item.duedate =item.duedate
        new_item.finisheddate = item.finisheddate
        new_item.createdate = createdate
        IList.insert(0,new_item)

    self.DisplayList(IList,L)

    #If we didn't come from OnMoveToList or OnMoveToSpecificList where L != self.L
    if L==self.L:
        for i in range(numitems):
            LCtrl.SetItemState(i, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED)
        self.curIdx = numitems-1



#@+node:ekr.20051104081502.392: *9* OnDeleteItems
def OnDeleteItems(self, event=None):
    """Called directly and by OnCopyItems (cut = true)
    """
    if self.curIdx == -1: #not absolutely necessary but gets you out quickly
        return

    L = self.L
    LCtrl = self.ListCtrls[L]
    IList = self.ItemLists[L]
    Properties = self.PropertyDicts[L]

    i = -1
    while 1:
        i = LCtrl.GetNextItem(i, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)
        if i==-1:
            break
        item = IList.pop(i)
        LCtrl.DeleteItem(i)

        host = Properties['host']
        cursor = self.Cursors[host]
        table = Properties['table']

        cursor.execute("DELETE from "+table+" WHERE id = %s", (item.id,))

        #Track Deletes for Syncing ############################################
        if table in SYNC_TABLES:
            if host.split(':')[1] == 'sqlite':
                timestamp = mx.DateTime.now()
                cursor.execute("INSERT INTO sync (id,action,table_name,name,timestamp) VALUES (%s,%s,%s,%s,%s)",(item.id,'d',table,item.name,timestamp))
            else:
                cursor.execute("INSERT INTO sync (id,action,table_name,user,name) VALUES (%s,%s,%s,%s,%s)",(item.id,'d',table,USER,item.name))
        #########################################################################
        i-=1

    self.name.Clear()
    self.owners.Clear()
    self.note.Clear()
    #note that Clearing does cause self.modified -->{'name':1}
    self.modified = {}
    self.curIdx = -1

#@+node:ekr.20051104081502.393: *8* MouseDown methods
#@+node:ekr.20051104081502.394: *9* OnLeftDown (Action depends on x coordinate)
def OnLeftDown(self, evt):
    g.pr("Here")
    if self.modified:
        #if inplace editor is open and you click anywhere (same or different row from current row) but in the editor itself then just to close editor
        flag = self.modified.has_key('inplace')
        self.OnUpdate()
        if flag:
            evt.Skip() #without Skip, EVT_LIST_ITEM_SELECTED is not generated if you click in a new row
            return

    x,y = evt.GetPosition()
    LCtrl = self.ListCtrls[self.L]

    #Using HitTest to obtain row clicked on because there was a noticable delay in the generation of an
    #EVT_LIST_ITEM_SELECTED event when you click on the already selected row
    idx,flags = LCtrl.HitTest((x,y))

    #if you are below rows of items then idx = -1 which could match self.curIdx = -1
    if idx == -1:
        return

    # only if you click on the currently selected row do the following events occur
    if idx == self.curIdx:
        if x < 18:
            self.OnToggleFinished()
        elif x < 33:
            self.OnPriority()
        elif x < 33 + LCtrl.GetColumnWidth(1):
            self.OnDisplayInPlaceEditor()
        elif x < 33 + LCtrl.GetColumnWidth(1) + LCtrl.GetColumnWidth(2): 
            self.OnEditOwner()
        else:
            self.OnDueDate
    else:
        evt.Skip() #without Skip, EVT_LIST_ITEM_SELECTED is not generated if you click in a new row



#@+node:ekr.20051104081502.395: *9* OnRightDown (Display popup sendto menu)
def OnRightDown(self, evt):
    x,y = evt.GetPosition()

    sendtomenu = wxMenu()

    open_tables = []
    for page,Properties in enumerate(self.PropertyDicts):
        host,table = Properties['host'],Properties['table']
        open_tables.append((host,table))
        sendtomenu.Append(1+page,"%s (%s)"%(table,host))
        EVT_MENU(self, 1+page, lambda e,p=page: self.OnMoveToList(e,p))

    sendtomenu.Delete(self.L+1) # don't send it to the page you're already on
    sendtomenu.AppendSeparator()

    self.closed_tables = []
    for host,cursor in self.Cursors.items():

        location, rdbms = host.split(':')

        if rdbms == 'sqlite':
            cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
        elif rdbms == 'mysql':
            cursor.execute("SHOW tables")

        results = cursor.fetchall()

        page+=1
        for (table,) in results:
            if not ((host,table) in open_tables or table in ['user_sync','owners','sync']):
                self.closed_tables.append((host,table))
                sendtomenu.Append(1+page,"%s (%s)"%('*'+table,host))
                EVT_MENU(self, 1+page, lambda e,p=page: self.OnMoveToList(e,p))
                page+=1

    self.PopupMenu(sendtomenu,(x+125,y+40))
    sendtomenu.Destroy()

#@+node:ekr.20051104081502.396: *8* Move/Combine items methods
#@+node:ekr.20051104081502.397: *9* OnCombineItems
def OnCombineItems(self, evt):
    L = self.L
    idx = self.curIdx
    IList = self.ItemLists[L]
    LCtrl = self.ListCtrls[L]

    combine_list = []
    i = -1
    while 1:
        i = LCtrl.GetNextItem(i, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)
        if i==-1:
            break
        combine_list.append((IList[i].createdate,IList[i]))


    if len(combine_list) < 2:
        g.pr("Fewer than two items highlighted")
        return

    combine_list.sort()
    combine_list.reverse()

    dlg = wxMessageDialog(self,
                        "Combine the %d selected items?"%len(combine_list),
                        "Combine Items?",
                        wxICON_QUESTION|wxYES_NO)

    if dlg.ShowModal() == wxID_YES:
        Properties = self.PropertyDicts[L]
        host = Properties['host']
        cursor = self.Cursors[host]
        table = Properties['table']

        t_item = combine_list[0][1]
        merge_list = combine_list[1:]
        new_note = ""

        for date,item in merge_list:
            note = self.GetNote(item=item)
            date = date.Format("%m/%d/%y")
            new_note = "%s\n%s %s\n\n%s"%(new_note, date, item.name, note)

            cursor.execute("DELETE from "+table+" WHERE id = %s", (item.id,))
            #Track Deletes for Syncing ############################################
            if table in SYNC_TABLES:
                if host.split(':')[1] == 'sqlite':
                    timestamp = mx.DateTime.now()
                    cursor.execute("INSERT INTO sync (id,action,table_name,name,timestamp) VALUES (%s,%s,%s,%s,%s)",(item.id,'d',table,item.name,timestamp))
                else:
                    cursor.execute("INSERT INTO sync (id,action,table_name,user,name) VALUES (%s,%s,%s,%s,%s)",(item.id,'d',table,USER,item.name))
            #########################################################################

        t_note = self.GetNote(item=t_item)
        t_note = "%s\n%s"%(t_note,new_note)

        #What about combining owners?######################################

        cursor.execute("UPDATE "+table+" SET name = %s, note = %s WHERE id = %s", (t_item.name+"*",t_note,t_item.id))
        t_item.timestamp = self.TimeStamper(host, cursor, table, t_item.id)

        self.OnRefresh()
        LCtrl.SetItemState(0, 0, wxLIST_STATE_SELECTED)
        IList = self.ItemLists[L]
        id = t_item.id
        idx = -1
        for item in IList:
            idx+=1
            if id == item.id:
                break
        else:
            idx = -1 

        #should never be -1
        if idx != -1:	
            LCtrl.SetItemState(idx, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED)
            LCtrl.EnsureVisible(idx)
        self.curIdx = idx

    dlg.Destroy()

#@+node:ekr.20051104081502.398: *9* OnMoveToList
def OnMoveToList(self, evt=None, page=0):
    self.OnCopyItems(cut=True)
    pc = self.nb.GetPageCount()
    if page < pc:		
        self.OnPasteItems(L=page)
    else:
        host,table = self.closed_tables[page-pc]
        cursor = self.Cursors[host]# in ini self.Cursors[host]

        for item in self.copyitems:
            z = item.owners+[None,None,None]
            id = self.GetUID() #give it a new id

            #need this or else it won't be seen as a new item when syncing; would be seen as updated
            createdate = mx.DateTime.now() 
            command = "INSERT INTO "+table+" (priority,name,createdate,finisheddate,duedate,note,owner1,owner2,owner3,id) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)"
            cursor.execute(command,(item.priority,item.name,createdate,item.finisheddate,item.duedate,item.notes,z[0],z[1],z[2],id))
            timestamp = self.TimeStamper(host, cursor, table, id)

    self.copyitems = []

#@+node:ekr.20051104081502.399: *9* OnMoveToSpecificList
def OnMoveToSpecificList(self, evt=None, table='follow_ups'):
    matches = {}
    for page,Properties in enumerate(self.PropertyDicts):
        host,tble = Properties['host'],Properties['table']
        if tble == table:
            rdbms = host.split(':')[1]
            matches[rdbms] = page

    self.OnCopyItems(cut=True)

    if matches:
        if matches.get('mysql'):	
            self.OnPasteItems(L=matches['mysql'])
        else:
            self.OnPasteItems(L=matches['sqlite'])
    else:
        cursor = self.Cursors[LOCAL_HOST]

        for item in self.copyitems:
            z = item.owners+[None,None,None]
            id = self.GetUID() #give it a new id

            #need this or else it won't be seen as a new item when syncing; would be seen as updated
            createdate = mx.DateTime.now() 
            command = "INSERT INTO "+table+" (priority,name,createdate,finisheddate,duedate,note,owner1,owner2,owner3,id) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)"
            cursor.execute(command,(item.priority,item.name,createdate,item.finisheddate,item.duedate,item.notes,z[0],z[1],z[2],id))
            timestamp = self.TimeStamper(host, cursor, table, id)

    self.copyitems = []



#@+node:ekr.20051104081502.400: *8* Change/update items methods
#@+node:ekr.20051104081502.401: *9* OnToggleFinished
def OnToggleFinished(self, evt=None):
    L = self.L
    LCtrl = self.ListCtrls[L]
    Properties = self.PropertyDicts[L]
    idx = self.curIdx

    item = self.ItemLists[L][idx]
    LC_Item = LCtrl.GetItem(idx)

    if not item.finisheddate:
        item.finisheddate = mx.DateTime.today()
        LC_Item.SetImage(LCtrl.idx0)
    else:
        item.finisheddate = None
        LC_Item.SetImage(LCtrl.idx1)

    << draw item >>

    self.tb.EnableTool(30, True)

    host = Properties['host']	
    cursor = self.Cursors[host]
    table = Properties['table']

    cursor.execute("UPDATE "+table+" SET finisheddate = %s WHERE id = %s", (item.finisheddate, item.id))
    item.timestamp = self.TimeStamper(host, cursor, table, item.id)

    if Properties['LCdate'] == 'timestamp':
        LCtrl.SetStringItem(idx, self.attr2col_num['date'], item.timestamp.Format("%m/%d %H:%M:%S"))
    elif Properties['LCdate'] == 'finisheddate':
        LCtrl.SetStringItem(idx, self.attr2col_num['date'], item.finisheddate.Format('%m/%d/%y'))



#@+node:ekr.20051104081502.402: *10* << draw item >>
if item.finisheddate:
    #It appears that SetTextColour resets font weight to Normal but this makes no sense
    #This means that all finished items have Normal weight whether they are priority 3,2 or 1
    #May actually be that GetItem() and then SetItem() sets the weight to Normal no matter what it was originally
    LC_Item.SetTextColour(wxLIGHT_GREY)

elif item.priority==1:
    #see note above about SetTextColour apparently resetting weight
    LC_Item.SetTextColour(wxBLACK)

elif item.priority==2:
    #LC_Item.SetTextColour(wxBLACK) -- this line should be necessary but it does not appear to be
    # ? font is black so ? if have to reset it
    f = self.LC_font
    f.SetWeight(wxBOLD)
    LC_Item.SetFont(f)
    f.SetWeight(wxNORMAL) # resetting font weight

else:
    LC_Item.SetTextColour(wxRED) #appears to be the only way to set color - can't through font
    f = self.LC_font #LCtrl.font
    f.SetWeight(wxBOLD)
    LC_Item.SetFont(f)
    f.SetWeight(wxNORMAL) # resetting font weight

LCtrl.SetItem(LC_Item)
#@+node:ekr.20051104081502.403: *9* OnPriority
def OnPriority(self, event=None, input=None):
    L = self.L
    idx = self.curIdx
    LCtrl = self.ListCtrls[L]
    Properties = self.PropertyDicts[L]
    item = self.ItemLists[L][idx]

    if input:
        item.priority=input

    else:
        if item.priority < 3:
            item.priority+= 1
        else:
            item.priority=1

    LC_Item = LCtrl.GetItem(idx)

    << draw item >>

    text = str(item.priority)        
    LCtrl.SetStringItem(idx, 0, text)

    host = Properties['host']
    cursor = self.Cursors[host]
    table = Properties['table']

    cursor.execute("UPDATE "+table+" SET priority = %s WHERE id = %s", (item.priority,item.id))
    item.timestamp = self.TimeStamper(host, cursor, table, item.id)

    if Properties['LCdate'] == 'timestamp':
        LCtrl.SetStringItem(idx, self.attr2col_num['date'], item.timestamp.Format('%m/%d %H:%M:%S'))

    wxCallAfter(LCtrl.SetFocus)

#@+node:ekr.20051104081502.404: *10* << draw item >>
if item.finisheddate:
    #It appears that SetTextColour resets font weight to Normal but this makes no sense
    #This means that all finished items have Normal weight whether they are priority 3,2 or 1
    #May actually be that GetItem() and then SetItem() sets the weight to Normal no matter what it was originally
    LC_Item.SetTextColour(wxLIGHT_GREY)

elif item.priority==1:
    #see note above about SetTextColour apparently resetting weight
    LC_Item.SetTextColour(wxBLACK)

elif item.priority==2:
    #LC_Item.SetTextColour(wxBLACK) -- this line should be necessary but it does not appear to be
    # ? font is black so ? if have to reset it
    f = self.LC_font
    f.SetWeight(wxBOLD)
    LC_Item.SetFont(f)
    f.SetWeight(wxNORMAL) # resetting font weight

else:
    LC_Item.SetTextColour(wxRED) #appears to be the only way to set color - can't through font
    f = self.LC_font #LCtrl.font
    f.SetWeight(wxBOLD)
    LC_Item.SetFont(f)
    f.SetWeight(wxNORMAL) # resetting font weight

LCtrl.SetItem(LC_Item)
#@+node:ekr.20051104081502.405: *9* Inplace Edit Methods
#@+node:ekr.20051104081502.406: *10* OnDisplayInPlaceEditor
def OnDisplayInPlaceEditor(self,evt=None):
    L = self.L
    LCtrl = self.ListCtrls[L]
    Properties = self.PropertyDicts[L]
    idx = self.curIdx
    item = self.ItemLists[L][idx]

    host = Properties['host']
    cursor = self.Cursors[host]
    table = Properties['table']

    #if self.Conflict(host, cursor, table, item): return #works -- may be overkill so i've commented it out

    TCid = wxNewId()
    y = LCtrl.GetItemPosition(idx)[1] 
    length = LCtrl.GetColumnWidth(1)

    editor = wxTextCtrl(self, TCid, pos = (167,y+28), size = (length,23), style=wxTE_PROCESS_ENTER)
    editor.SetFont(wxFont(9, wxSWISS, wxNORMAL, wxNORMAL))
    editor.SetBackgroundColour(wxColour(red=255,green=255,blue=175)) #Yellow
    editor.AppendText(item.name)
    editor.Show(True)
    editor.Raise()
    editor.SetSelection(-1,-1)
    editor.SetFocus()	

    EVT_TEXT_ENTER(self, TCid, self.OnCloseInPlaceEditor)		

    self.in_place_editor = editor
    self.modified['inplace'] = 1	





#@+node:ekr.20051104081502.407: *10* OnCloseInPlaceEditor
def OnCloseInPlaceEditor(self,evt=None):
    L = self.L
    LCtrl = self.ListCtrls[L]
    Properties = self.PropertyDicts[L]
    idx = self.curIdx
    item = self.ItemLists[L][idx]

    host = Properties['host']
    cursor = self.Cursors[host]
    table = Properties['table']
    LCdate = Properties['LCdate']

    #if self.Conflict(host, cursor, table, item)...

    text = self.in_place_editor.GetValue().strip()[:150]
    item.name = text
    LCtrl.SetStringItem(idx, self.attr2col_num['name'], text)
    self.in_place_editor.Destroy()

    cursor.execute("UPDATE "+table+" SET name = %s WHERE id = %s", (text, item.id))
    item.timestamp = self.TimeStamper(host, cursor, table, item.id)

    if Properties['LCdate'] == 'timestamp':
        LCtrl.SetStringItem(idx, self.attr2col_num['date'], item.timestamp.Format('%m/%d %H:%M:%S'))

    self.name.Clear()
    self.name.AppendText(text) #this will cause self.modified['name'] = 1, which is dealt with below

    #using default in case for some reason self.modified does not have the keys
    self.modified.pop('inplace', None)
    self.modified.pop('name', None)

    wxCallAfter(LCtrl.SetFocus) #sets focus on LCtrl and current selection to be highlighted



#@+node:ekr.20051104081502.408: *9* OnDueDate
def OnDueDate(self, evt=None):
    idx = self.curIdx
    if idx == -1:
        return
    L = self.L
    Properties = self.PropertyDicts[L]
    item = self.ItemLists[L][idx]
    LCtrl = self.ListCtrls[L]

    if item.duedate:
        date = wxDateTime()
        date.SetTimeT(item.duedate) #I am surprised it takes a mx.DateTime object; supposed to need ticks
    else:
        date = 0
    dlg = CalendarDialog(parent=self,
                 title="Select a date",
                 size=(400,400),
                 style=wxCAPTION,
                 date = date)
    if dlg.ShowModal()==wxID_OK:
        date = dlg.cal.GetDate() # this is some date object
        #date = date.GetTicks()
        item.duedate = mx.DateTime.DateFromTicks(date.GetTicks())

        host = Properties['host']
        cursor = self.Cursors[host]
        table = Properties['table']

        cursor.execute("UPDATE "+table+" SET duedate = %s WHERE id = %s", (item.duedate,item.id))
        item.timestamp = self.TimeStamper(host, cursor, table, item.id)
        if Properties['LCdate'] == 'timestamp':
            LCtrl.SetStringItem(idx, self.attr2col_num['date'], item.timestamp.Format("%m/%d %H:%M:%S"))
        elif Properties['LCdate'] == 'duedate':
            LCtrl.SetStringItem(idx, self.attr2col_num['date'], item.duedate.Format('%m/%d/%y'))
    dlg.cal.Destroy()
    dlg.Destroy()

#@+node:ekr.20051104081502.409: *9* OnEditOwner
def OnEditOwner(self, evt=None): #, new=False) removed Aug. 31 for simplicity
    idx = self.curIdx
    if idx == -1:
        return
    L = self.L
    Properties = self.PropertyDicts[L]
    LCtrl = self.ListCtrls[L]
    item = self.ItemLists[L][idx]
    if not self.ModifierDialog:
        g.pr("self.ModifierDialog is still being constructed")
        return
    #need to clear the current selections or you'll just be making more and more selections
    self.ModifierDialog.SelectCurrent(item.owners)
    self.ModifierDialog.tc.Clear()
    self.ModifierDialog.CenterOnParent()

    val = self.ModifierDialog.ShowModal()

    if val == wxID_OK:
        item.owners, new_names = self.ModifierDialog.GetUserInput()

        << Common Owner Code >>

        for owner in item.owners:
            if self.OwnerLBoxes[L].FindString(owner) == -1:
                self.OwnerLBoxes[L].Append(owner)

        for owner in new_names:
            self.ModifierDialog.lb.Append(owner)

        host = Properties['host']
        cursor = self.Cursors[host]
        table = Properties['table']

        cursor.execute("UPDATE "+table+" SET owner1 = %s, owner2 = %s, owner3 = %s WHERE id = %s", (z[0],z[1],z[2],item.id))
        item.timestamp = self.TimeStamper(host, cursor, table, item.id)
        if Properties['LCdate'] == 'timestamp':
            LCtrl.SetStringItem(idx, self.attr2col_num['date'], item.timestamp.Format("%m/%d %H:%M:%S"))

        if 'owners' in self.modified:
            del self.modified['owners']

    wxCallAfter(LCtrl.SetFocus)

#@+node:ekr.20051104081502.410: *10* << Common Owner Code >>
owner_str = '; '.join(item.owners)
LCtrl.SetStringItem(idx, self.attr2col_num['owners'], owner_str)
self.owners.Clear()
self.owners.AppendText(owner_str)

z = item.owners+[None,None,None] #note that + creates a new list
#@+node:ekr.20051104081502.411: *9* OnUpdate
def OnUpdate(self, evt=None):
    if 'inplace' in self.modified:
        self.OnCloseInPlaceEditor()
        if not self.modified:
            return

    L = self.L
    LCtrl = self.ListCtrls[L]
    IList = self.ItemLists[L]
    Properties = self.PropertyDicts[L]
    OLBox = self.OwnerLBoxes[L]
    idx = self.curIdx

    # there is some chance that it is never true that idx == -1 and then this could be eliminated
    if idx != -1:
        item = IList[idx]
    else:
        msg = wxMessageDialog(self, "There is no selected item to update", "", wxICON_ERROR|wxOK)
        msg.ShowModal()
        msg.Destroy()
        self.modified = {}
        return

    host = Properties['host']
    cursor = self.Cursors[host]
    table = Properties['table']

    if 'name' in self.modified:
        item.name = self.name.GetValue().strip()[:150]
        LCtrl.SetStringItem(idx, self.attr2col_num['name'], item.name)
        cursor.execute("UPDATE "+table+" SET name =%s WHERE id = %s",(item.name,item.id))

    if 'note' in self.modified:
        note = self.note.GetValue() #a blank note starts out as None but after this it becomes '' -- ??
        cursor.execute("UPDATE "+table+" SET note =%s WHERE id = %s",(note,item.id))

    if 'owners' in self.modified:
        owner_str = self.owners.GetValue().strip()
        item.owners = []
        if owner_str:
            owner_list = [x.strip() for x in owner_str.split(';')]
            for owner in owner_list:
                owner = ", ".join([x.strip().title() for x in owner.split(',')])
                item.owners.append(owner)

        << Common Owner Code >>

        cursor.execute("UPDATE "+table+" SET owner1 = %s, owner2 = %s, owner3 = %s WHERE id = %s", (z[0],z[1],z[2],item.id))

        for owner in item.owners:
            if self.ModifierDialog.lb.FindString(owner) == -1:
                self.ModifierDialog.lb.Append(owner)
                OLBox.Append(owner)
            elif OLBox.FindString(owner) == -1:
                OLBox.Append(owner)		

    item.timestamp = self.TimeStamper(host, cursor, table, item.id)
    if Properties['LCdate'] == 'timestamp':
        LCtrl.SetStringItem(idx, 3, item.timestamp.Format("%m/%d %H:%M:%S"))

    self.modified = {}


#@+node:ekr.20051104081502.412: *10* << Common Owner Code >>
owner_str = '; '.join(item.owners)
LCtrl.SetStringItem(idx, self.attr2col_num['owners'], owner_str)
self.owners.Clear()
self.owners.AppendText(owner_str)

z = item.owners+[None,None,None] #note that + creates a new list
#@+node:ekr.20051104081502.413: *9* OnNewItem
def OnNewItem(self, evt=None):
    L=self.L
    LCtrl = self.ListCtrls[L]
    Properties = self.PropertyDicts[L]

    if self.curIdx != -1:
        LCtrl.SetItemState(self.curIdx, 0, wxLIST_STATE_SELECTED)

    << Clear data fields >>

    class Item: pass
    item = Item()
    item.name = '<New Item>'
    item.priority = 1
    item.owners = []
    item.createdate = mx.DateTime.now() #need this to be a timestamp and not just date for syncing
    item.duedate = item.finisheddate = None

    self.ItemLists[L].insert(0,item)

    host = Properties['host']
    cursor = self.Cursors[host]
    table = Properties['table']
    item.id = self.GetUID()

    cursor.execute("INSERT INTO "+table+" (priority,name,createdate,finisheddate,duedate,id) VALUES (%s,%s,%s,%s,%s,%s)",
                (item.priority,item.name,item.createdate,None,None,item.id))

    item.timestamp = self.TimeStamper(host, cursor, table, item.id)

    #tracking new item for syncing will happen in Edit Name

    LCtrl.InsertImageStringItem(0,"1", LCtrl.idx1)
    LCtrl.SetStringItem(0,1,item.name)

    if Properties['LCdate'] == 'timestamp':
        LCtrl.SetStringItem(0, self.attr2col_num['date'], item.timestamp.Format("%m/%d %H:%M:%S"))
    elif Properties['LCdate'] == 'createdate':
        LCtrl.SetStringItem(0, self.attr2col_num['date'], item.createdate.Format('%m/%d/%y'))

    self.curIdx = 0

    #if Display is being filtered we assume that is the owner of the new node
    owner = Properties['owner']	
    if owner and owner!='*ALL':
        self.ListCtrls[L].SetStringItem(0, self.attr2col_num['owners'], owner)
        item.owners = [owner]

        self.owners.Clear()
        self.owners.AppendText(owner)

        cursor.execute("UPDATE "+table+" SET owner1 = %s WHERE id = %s", (owner,item.id))
        item.timestamp = self.TimeStamper(host, cursor, table, item.id)  #not really necessary since just got a timestamp

    # decided that it was actually better not to ask for the owner on a new node	
    #else:
        #self.OnEditOwner()

    LCtrl.SetFocus() #needed for the in place editor to look right
    LCtrl.SetItemState(0, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED)

    self.OnDisplayInPlaceEditor() #(new=True)
#@+node:ekr.20051104081502.414: *10* << Clear data fields >>
self.name.Clear()
self.owners.Clear()
self.note.Clear()
#@+node:ekr.20051104081502.415: *9* Conflict (not in use)
@ Need to decide if we are going to have timestamp checking to be sure something hasn't changed
Note that there would not need to be timestamp checking on a new node
Also  there is no need to timestamp check on a local DB
The following code seems to work fine, however, I have just commented out the calls to it in NameEditor methods
@c
def Conflict(self, host, cursor, table, item):
    if host is 'sqlite':
        return False
    cursor.execute("Select timestamp from "+table+" WHERE id = %s", (item.id,))
    db_timestamp = cursor.fetchone()[0]
    if db_timestamp != item.timestamp:
        g.pr("There is a conflict and you should refresh display")
        return True
    else:
        return False
#@+node:ekr.20051104081502.416: *9* OnEditNote
def OnEditNote(self, evt=None):
    if self.modified:
        self.OnUpdate()

    idx = self.curIdx

    if idx == -1:
        return

    L = self.L

    #if self.editor:
        #machine = None
        #win32pdh.EnumObjects(None, machine, 0, 1) # resets Enum otherwise it seems to hold onto old data
        #object = "Process"
        #items, instances = win32pdh.EnumObjectItems(None,None,"Process", -1)
        #if 'TextPad' in instances:
            #g.pr("TextPad is running")
        #else:
            #self.editor = {}

    item = self.ItemLists[L][idx]
    file_name = re.sub('[\\/:*"<>|\?]','-',item.name) #make sure all chars are legal file name characters

    path = os.path.join(os.environ['TMP'],file_name[:50])+'.%s'%NOTE_EXT

    f = file(path,'w')
    f.write(self.GetNote())
    f.close()

    os.startfile(path)

    id = item.id
    for d in self.editor:
        if d['id'] == id:
            return

    ed = {}
    ed['time'] = os.path.getmtime(path)
    ed['host'] = self.PropertyDicts[L]['host']
    ed['table'] = self.PropertyDicts[L]['table']
    ed['path'] = path
    ed['id'] = item.id

    self.editor.append(ed)

    time.sleep(.1)
#@+node:ekr.20051104081502.417: *8* File menu methods
#@+node:ekr.20051104081502.418: *9* OnNewList
def OnNewList(self, event=None):
    if self.modified:
        self.OnUpdate()

    if OFFLINE_ONLY is True or REMOTE_HOST is None:
        hosts = [LOCAL_HOST]
    else:
        hosts = [LOCAL_HOST, REMOTE_HOST]

    dlg = wxSingleChoiceDialog(self, 'Databases', 'Choose a database:', hosts, wxCHOICEDLG_STYLE)
    val = dlg.ShowModal()
    dlg.Destroy()
    if val == wxID_OK:
        host = dlg.GetStringSelection()
    else:
        return

    cursor = self.GetCursor(host)
    if cursor is None:
        return

    dlg = wxTextEntryDialog(self, 'What is the name of the new table?', 'Create Table')
    val = dlg.ShowModal()
    dlg.Destroy()
    if val == wxID_OK:
        table = dlg.GetValue()
    else:
        return

    if not table:
        return

    location, rdbms = host.split(':')

    if rdbms == 'sqlite':
        cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
    else:
        cursor.execute("SHOW tables")

    if (table,) in cursor.fetchall():
        msg = wxMessageDialog(self,
                              "Table '%s' already exists"%table,
                              "Duplicate Table",
                              wxICON_ERROR|wxOK)
        msg.ShowModal()
        msg.Destroy()
        return

    dlg = wxMessageDialog(self,
          "Are you sure you want to create Table '%s'?"%table,
          "Create Table?",
          wxICON_QUESTION|wxYES_NO)

    if dlg.ShowModal() == wxID_YES:
        self.CreateTable(host,table)
        self.CreateNewNotebookPage(host,table)

        #self.AddListControl(tab_title) #add listcontrol displays the list

        #self.OnNewItem()

    dlg.Destroy()


#@+node:ekr.20051104081502.419: *9* OnFileList
def OnFileList(self, evt=None, path=None):
    if self.modified:
        self.OnUpdate()

    #if there is no event, we got here through the start up loading of lists
    if evt:
        fileNum = evt.GetId() - wxID_FILE1			
        path = self.filehistory.GetHistoryFile(fileNum)
        location, rdbms, table = path.split(':')
        host = '%s:%s'%(location, rdbms)
        # only need to check if table is open if this is not at startup
        if table in [p['table'] for p in self.PropertyDicts if p['host'] == host]:
            dlg = wxMessageDialog(self,"%s (%s) is already open!"%(table,host),"List Open",wxICON_ERROR|wxOK)
            dlg.ShowModal()
            dlg.Destroy()
            return

    else:
        location, rdbms, table = path.split(':')
        host = '%s:%s'%(location, rdbms)

    cursor = self.GetCursor(host)
    if cursor is None:
        return

    if rdbms == 'sqlite':
        sql = "SELECT name FROM sqlite_master WHERE name = '%s'"%table
    else:
        sql = "SHOW TABLES LIKE '%s'"%table

    cursor.execute(sql)
    if not cursor.fetchall():
        dlg = wxMessageDialog(self,
                    "Table '%s' at host '%s' does not appear to exist!"%(table,host),
                    "Table does not exist",
                    wxICON_ERROR|wxOK)
        dlg.ShowModal()
        dlg.Destroy()
        return

    self.CreateNewNotebookPage(host,table)

#@+node:ekr.20051104081502.420: *9* OnOpenList
def OnOpenList(self, evt=None):
    if self.modified:
        self.OnUpdate()

    tree = {}

    if OFFLINE_ONLY is True or REMOTE_HOST is None:
        hosts = [LOCAL_HOST]
    else:
        hosts = [LOCAL_HOST, REMOTE_HOST]

    for host in hosts:
        cursor = self.GetCursor(host)
        if cursor:
            if host.split(':')[1] == 'sqlite':
                sql = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
            else:
                sql = "SHOW TABLES" #sorted

            cursor.execute(sql)
            results = cursor.fetchall()

            #excluding already open tables + 'system' tables
            excluded_tables = [p['table'] for p in self.PropertyDicts if p['host'] == host]
            excluded_tables.extend(['user_sync','sync','owners'])

            tables = [t for (t,) in results if t not in excluded_tables]

            tree[host] = tables

    dlg = TreeDialog(self, "Open List", tree=tree)
    val = dlg.ShowModal()
    dlg.Destroy()
    if val == wxID_OK:
        sel = dlg.TreeCtrl.GetSelection()
        table = dlg.TreeCtrl.GetItemText(sel)
        sel = dlg.TreeCtrl.GetItemParent(sel)
        host = dlg.TreeCtrl.GetItemText(sel)

        if host in hosts: #takes care of highlighting root or hosts
            self.CreateNewNotebookPage(host,table)
#@+node:ekr.20051104081502.421: *9* OnDeleteList
def OnDeleteList(self, evt=None):
    #ini controls whether the menu item is enabled
    Properties = self.PropertyDicts[self.L]
    host = Properties['host']
    table = Properties['table']

    #if table is in SYNC_TABLES, should we make a point of that?
    dlg = wxMessageDialog(self,
                        "Are you sure that you want to delete table %s (%s)?\n(Please note that you cannot recover it once it is deleted!)"%(table,host),
                        "Delete Table...",
                        wxICON_EXCLAMATION|wxYES_NO|wxNO_DEFAULT)

    val = dlg.ShowModal()
    dlg.Destroy()
    if val == wxID_NO:
        return

    rdbms = host.split(':')[1]

    if rdbms == 'mysql':
        dlg = wxMessageDialog(self,
                        "Are you sure really really sure you want to delete table %s (%s)?\n(You really really cannot recover it once it is deleted)"%(table,host),
                        "Delete Table...",
                        wxICON_EXCLAMATION|wxYES_NO|wxNO_DEFAULT)

        val = dlg.ShowModal()
        dlg.Destroy()
        if val == wxID_NO:
            return

    cursor = self.Cursors[host]
    cursor.execute("DROP TABLE %s"%table)

    self.OnCloseList()

#@+node:ekr.20051104081502.422: *9* OnCloseList
def OnCloseList(self, evt=None):
    if self.modified:
        self.OnUpdate()

    L = self.L

    del self.ItemLists[L]
    del self.PropertyDicts[L]
    del self.ListCtrls[L]
    del self.OwnerLBoxes[L]

    self.nb.DeletePage(L)        

    ln = len(self.PropertyDicts)
    if ln:
        self.nb.SetSelection(0)
        self.L = 0
    else:
        self.L = -1




#@+node:ekr.20051104081502.423: *9* OnCloseAll
def OnCloseAll(self, evt=None):
    if self.modified:
        self.OnUpdate()

    while self.L != -1:
        self.OnCloseList()

    self.name.Clear()
    self.owners.Clear()
    self.note.Clear()
    #note that Clearing does set self.modified (eg {'name':1})
    self.modified = {}

#@+node:ekr.20051104081502.424: *9* OnSaveAsText
def OnSaveAsText(self, evt=None):
    if self.modified:
        self.OnUpdate()

    Properties = self.PropertyDicts[self.L]
    wildcard = "txt files (*.txt)|*.txt|All files (*.*)|*.*"
    #dlg = wxFileDialog(self, "Save file", "", Properties['table'], wildcard, wxSAVE|wxOVERWRITE_PROMPT|wxCHANGE_DIR)

    body = ""
    for i,item in enumerate(self.ItemLists[self.L]):
        body = body+"%d. %s (%d)\n"%(i+1, item.name, item.priority)

    table = Properties['table']
    location, rdbms = Properties['host'].split(':')
    filename = re.sub('[\\/:*"<>|\?]','-','%s-%s-%s'%(location,rdbms,table)) 
    filename = filename[:50]+'.txt'

    path = os.path.join(DIRECTORY,filename)

    f = file(path,'w')
    f.write(body)
    f.close()

    os.startfile(path)

    self.SetStatusText("Saved file %s"%path)

#@+node:ekr.20051104081502.425: *9* OnArchive
def OnArchive(self, evt=None):
    if self.modified:
        self.OnUpdate()

    Properties = self.PropertyDicts[self.L]
    host = Properties['host']
    cursor = self.Cursors[host]
    table = Properties['table']
    rdbms = host.split(':')[1]

    table_archive = table+'_archive'

    #need to test for existence of table_archive
    if rdbms == 'sqlite':
        cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
    else:
        cursor.execute("SHOW tables")

    results = cursor.fetchall()

    if (table_archive,) not in results:
        dlg = wxMessageDialog(self,
                    "Do you want to create an archive for table %s (%s)"%(table,rdbms),
                    "Create an archive...",
                    wxICON_QUESTION|wxYES_NO)
        val = dlg.ShowModal()
        dlg.Destroy()
        if val==wxID_YES:
            self.CreateTable(host,table_archive)
        else:
            return

    label1 = "In table %s (%s) \narchive all finished items older than:"%(table,rdbms)
    label2 = "Archive all finished items"
    dlg = FinishedDialog(self, "Archive completed items", days=7, spin_label=label1, check_label=label2)

    val = dlg.ShowModal()
    dlg.Destroy() #dialogs and frames not destroyed right away to allow processing events, methods
    if val==wxID_CANCEL:
        return

    if dlg.check.GetValue():
        cursor.execute("SELECT id,priority,name,createdate,finisheddate,duedate,owner1,owner2,owner3,note FROM "+table+" WHERE finisheddate IS NOT NULL")
    else:
        days = dlg.text.GetValue()
        date = mx.DateTime.today() - int(days)
        cursor.execute("SELECT id,priority,name,createdate,finisheddate,duedate,owner1,owner2,owner3,note FROM "+table+" WHERE finisheddate < %s",(date,))

    results = cursor.fetchall()
    dlg = wxMessageDialog(self,
                        "Archiving will remove %d records from %s.\nDo you want to proceed?"%(len(results),table),
                        "Proceed to archive...",
                        wxICON_QUESTION|wxYES_NO)

    val = dlg.ShowModal()
    dlg.Destroy()
    if val == wxID_NO:
        return

    if table in SYNC_TABLES:
        if rdbms == 'sqlite':
            def track_deletes():
                timestamp = mx.DateTime.now()
                cursor.execute("INSERT INTO sync (id,action,table_name,name,timestamp) VALUES (%s,%s,%s,%s,%s)",(id,'d',table,name,timestamp))
        else:
            def track_deletes():
                cursor.execute("INSERT INTO sync (id,action,table_name,user,name) VALUES (%s,%s,%s,%s,%s)",(id,'d',table,USER,name))
    else:
        def track_deletes():
            pass	

    for row in results:
        # the next line is necessary because pysqlite returns a tuple-like object that is not a tuple
        r = tuple(row)
        id = r[0]
        name = r[2]
        cursor.execute("INSERT INTO "+table_archive+"  (id,priority,name,createdate,finisheddate,duedate,owner1,owner2,owner3,note) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)",r)
        timestamp = self.TimeStamper(host, cursor, table_archive, id)
        cursor.execute("DELETE from "+table+" WHERE id = %s", (id,))
        track_deletes()

    self.OnRefresh()
    dlg = wxMessageDialog(self,
                        "Table %s had items older than %s days successfully archived"%(table,days),
                        "Archiving successful...",
                        wxICON_INFORMATION|wxOK)
    dlg.ShowModal()
#@+node:ekr.20051104081502.426: *9* OnWorkOffline
def OnWorkOffline(self, evt=None):
    global OFFLINE_ONLY
    OFFLINE_ONLY = not OFFLINE_ONLY
    if OFFLINE_ONLY:
        del self.Cursors[REMOTE_HOST]
    else:
        server = REMOTE_HOST.split(':')[0]
        try:
            socket.gethostbyname(server)
        except:
            dlg = wxMessageDialog(None, "Cannot connect to remote server! Will set to work offline.", "ListManager", style=wxOK|wxICON_EXCLAMATION|wxSTAY_ON_TOP)
            dlg.ShowModal()
            dlg.Destroy()
            OFFLINE_ONLY = True

    self.filemenu.Check(idOFFLINE,OFFLINE_ONLY)

#@+node:ekr.20051104081502.428: *8* Display methods
#@+node:ekr.20051104081502.429: *9* OnItemSelected
def OnItemSelected(self, evt=None):
    if self.modified:
        self.OnUpdate()

    if evt:
        idx = evt.GetIndex()
    elif self.curIdx != -1:
        idx = self.curIdx
    else: # really to catch self.curIdx = -1 (see OnDelete and OnRefresh)
        self.name.Clear() # could be moved out of if
        self.owners.Clear() # could be moved out of if
        self.note.Clear()
        #note that Clearing does set self.modified (eg {'name':1})
        self.modified = {}
        return

    L = self.L
    item = self.ItemLists[L][idx]

    self.name.Clear()
    self.name.AppendText(item.name) #SetValue(item.name) - if you use setvalue you don't get the font

    self.owners.Clear()
    self.owners.AppendText('; '.join(item.owners))

    note = self.GetNote(L,item)
    if note.find("<leo_file>") != -1:
        self.note.SetValue("Leo Outline")
        self.note.SetEditable(False)
    else:
        self.note.SetValue(note)
        self.note.SetEditable(True)

    self.ListCtrls[L].EnsureVisible(idx)
    self.curIdx = idx

    #writing to text widgets caused wxEVT_COMMAND_TEXT_UPDATED which is caught by EVT_TEXT, which updates self.modified
    self.modified={}

#@+node:ekr.20051104081502.430: *9* OnItemActivated
def OnItemActivated(self,evt):
    g.pr("On Activated")

#@+node:ekr.20051104081502.431: *9* OnShowAll
def OnShowAll(self, evt=None):
    L = self.L
    OLBox = self.OwnerLBoxes[L]

    Properties = self.PropertyDicts[L]
    Properties['showfinished'] = -1
    Properties['owner'] = '*ALL'

    OLBox.SetStringSelection('*ALL')

    self.OnRefresh()
#@+node:ekr.20051104081502.432: *9* OnRefresh
def OnRefresh(self, evt=None):
    #OnItemSelected should be able to handle no items so this could be very short
    L = self.L

    results = self.ReadFromDB()
    self.ItemLists[L] = self.CreateAndDisplayList(results)

    if self.ItemLists[L]:
        self.ListCtrls[L].SetItemState(0, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED)
        self.curIdx = 0
    else:
        self.curIdx = -1		

    self.OnItemSelected()
#@+node:ekr.20051104081502.433: *9* OnFilterOwners
def OnFilterOwners(self, evt=None):
    if self.modified:
        self.OnUpdate()
    sel = self.OwnerLBoxes[self.L].GetStringSelection()

    if sel:
        self.PropertyDicts[self.L]['owner'] = sel
        self.OnRefresh()
#@+node:ekr.20051104081502.434: *9* OnColumnClick (to sort columns)
def OnColumnClick(self, evt):
    col_num = evt.GetColumn()
    L = self.L
    LCtrl = self.ListCtrls[L]
    Sort = self.PropertyDicts[L]['sort']
    attr2col = self.attr2col_num

    prev_sort_attr = Sort.get('attribute') #if this is the first sort Properties['sort'] is {}

    #following is a little bit ugly but gets the key from the value, which is col_num
    Sort['attribute'] = attr2col.keys()[attr2col.values().index(col_num)]

    if prev_sort_attr == Sort['attribute']:
        Sort['direction'] = not Sort['direction']
    else:
        Sort['direction'] = 0

    self.OnRefresh()

    LCtrl.ClearColumnImage(attr2col['priority'])
    LCtrl.ClearColumnImage(attr2col['date'])
    img_num = LCtrl.arrows[Sort['direction']]
    LCtrl.SetColumnImage(col_num, img_num)

#@+node:ekr.20051104081502.435: *9* OnShowFinished
def OnShowFinished(self,evt):
    Properties = self.PropertyDicts[self.L]
    label1 = "Enter the number of days to retain\ncompleted tasks in the display:"
    label2 = "Show all finished items"
    dlg = FinishedDialog(self, "Display of completed items", days=Properties['showfinished'], spin_label=label1, check_label=label2)
    if dlg.ShowModal()==wxID_OK:
        if dlg.check.GetValue():
            Properties['showfinished'] = -1
        else:
            days = dlg.text.GetValue()
            Properties['showfinished'] = int(days)			
        self.OnRefresh()
    dlg.Destroy()

#@+node:ekr.20051104081502.436: *9* OnColumnRightClick (popup to change date displayed)
def OnColumnRightClick(self, evt=None):
    col = evt.GetColumn()
    if col != self.attr2col_num['date']:
        return

    L = self.L
    LCtrl = self.ListCtrls[L]
    Properties = self.PropertyDicts[L]

    #x,y = evt.GetPosition()
    datemenu = wxMenu()

    for i,date in enumerate(['Create Date','Last Modified','Due Date','Completion Date']):
        datemenu.Append(200+i, date)
        EVT_MENU(self, 200+i, lambda e, i=i: self.ChangeDateDisplayed(e,i))

    x = LCtrl.GetColumnWidth(1)+ LCtrl.GetColumnWidth(2) + LCtrl.GetColumnWidth(3)
    self.PopupMenu(datemenu,(x,40))
    datemenu.Destroy()


#@+node:ekr.20051104081502.437: *9* OnDisplayDateCategory
def OnDisplayDateCategory(self, evt=None):
    dlg = wxSingleChoiceDialog(self, 'Date Display', 'Choose a date to display:',
                    ['Create Date','Last Modified','Due Date','Completion Date']
                    , wxOK|wxCANCEL)
    val = dlg.ShowModal()
    dlg.Destroy()

    if val == wxID_OK:
        idx = dlg.GetSelection()
        self.ChangeDateDisplayed(i=idx)

#@+node:ekr.20051104081502.438: *9* ChangeDateDisplayed
def ChangeDateDisplayed(self, evt=None, i=0):
    L = self.L
    LCtrl = self.ListCtrls[L]
    self.PropertyDicts[L]['LCdate'] = displaydate = ('createdate','timestamp','duedate','finisheddate')[i]	
    col_num = self.attr2col_num['date']
    col_info = LCtrl.GetColumn(col_num)
    col_info.SetText(self.date_titles[displaydate])
    LCtrl.SetColumn(col_num,col_info)
    self.DisplayList(self.ItemLists[L])
    #self.OnRefresh() #have gone back and forth but think that it should be self.DisplayList
#@+node:ekr.20051104081502.439: *9* DisplayList
def DisplayList(self, List, L=None):
    #OnPasteItems needs to be able to have an L that is not self.L
    if L is None:
        L = self.L
    LCtrl = self.ListCtrls[L]
    LCdate = self.PropertyDicts[L]['LCdate']
    if LCdate == 'timestamp':
        format = '%m/%d %H:%M:%S'
    else:
        format = '%m/%d/%y'
    LCtrl.DeleteAllItems()

    for x,item in enumerate(List):
        << draw item >>


#@+node:ekr.20051104081502.440: *10* << draw item >>
LCtrl.InsertImageStringItem(x, str(item.priority), LCtrl.idx1)
LCtrl.SetStringItem(x,1,item.name)
LCtrl.SetStringItem(x,2,'; '.join(item.owners))
date = item.__dict__[LCdate]
LCtrl.SetStringItem(x,3,date and date.Format(format) or "")

if item.finisheddate:
    LC_Item = LCtrl.GetItem(x)
    LC_Item.SetImage(LCtrl.idx0) #might just want generic number or greyed one two three
    LC_Item.SetTextColour(wxLIGHT_GREY)
    LCtrl.SetItem(LC_Item)

elif item.priority==2:
    LC_Item = LCtrl.GetItem(x)
    f = self.LC_font
    f.SetWeight(wxBOLD)
    LC_Item.SetFont(f)
    f.SetWeight(wxNORMAL) #resetting weight
    LCtrl.SetItem(LC_Item)

elif item.priority==3:
    LC_Item = LCtrl.GetItem(x)
    f = self.LC_font
    f.SetWeight(wxBOLD)
    LC_Item.SetFont(f)
    f.SetWeight(wxNORMAL) #return to normal
    LC_Item.SetTextColour(wxRED)
    LCtrl.SetItem(LC_Item)
#@+node:ekr.20051104081502.441: *8* Printing methods
#@+node:ekr.20051104081502.442: *9* OnPageSetup
def OnPageSetup(self, evt):
    #need to pass printdata to tableprint

    psdata = wxPageSetupDialogData()

    # if want to vary margins will need to save them as ivars and then set
    #psdata.SetMarginTopLeft((self.Left,self.Top))
    psdata.EnableMargins(False)
    psdata.SetPrintData(self.printdata) #gets Paper Orientation and PaperId info from printdata

    dlg = wxPageSetupDialog(self, psdata)
    if dlg.ShowModal() == wxID_OK:
        self.printdata = dlg.GetPageSetupData().GetPrintData()
        dlg.Destroy()
#@+node:ekr.20051104081502.443: *9* OnPrint
def OnPrint(self, evt=None, prev=False, showprtdlg=True): 		#???self.psdata = psdata
    IList = self.ItemLists[self.L]
    Properties = self.PropertyDicts[self.L]

    prt = PrintTable(self.printdata) #self.printdata is the wxPrintData object with Orientation Info

    font_name = prt.default_font_name
    prt.text_font = {'Name':font_name, 'Size':11, 'Colour':[0, 0, 0], 'Attr':[0, 0, 0]}
    prt.label_font = {'Name':font_name, 'Size':12, 'Colour':[0, 0, 0], 'Attr':[1, 0, 0]}
    prt.header_font = {'Name':font_name, 'Size':14, 'Colour':[0, 0, 0], 'Attr':[1, 0, 0]}

    prt.row_def_line_colour = wxLIGHT_GREY
    prt.column_def_line_colour = wxLIGHT_GREY

    prt.left_margin = 0.5

    data = []
    for row,item in enumerate(IList):	
        data.append([str(item.priority),
                    item.name,
                    item.duedate and item.duedate.Format('%m/%d/%y') or '',
                    '; '.join([x.split(',')[0] for x in item.owners])]) #just last names

        if item.finisheddate:
            prt.SetCellText(row, 0, wxLIGHT_GREY)
            prt.SetCellText(row, 1, wxLIGHT_GREY)
            prt.SetCellText(row, 2, wxLIGHT_GREY)
            prt.SetCellText(row, 3, wxLIGHT_GREY)

    prt.data = data
    prt.label = ['P','Item','Due','Owner']

    if self.printdata.GetOrientation() == wxPORTRAIT:
        prt.set_column = [.2, 5, .65, 1]
    else:
        prt.set_column = [.2, 7, .65, 1.5]

    title = "Table: %s   Owner: %s    "%(Properties['table'],Properties['owner'])
    prt.SetHeader(title, type='Date & Time', align=wxALIGN_LEFT, indent = 1.5)
    prt.SetFooter("Page No ", type ="Num")

    if prev:
        prt.Preview()
    else:
        prt.Print(prompt=showprtdlg)
#@+node:ekr.20051104081502.444: *8* Exiting methods
#@+node:ekr.20051104081502.445: *9* OnWindowExit
def OnWindowExit(self, evt):
    #this is called if you close the ListManager Window with the X
    if evt.CanVeto():
        self.OnExit()
    else:
        evt.Skip()
#@+node:ekr.20051104081502.446: *9* OnExit
def OnExit(self, event=None):   
    << save configuration file >>
    sys.stderr.dlg.Destroy() #destroys the error dialog; need to do this to shut down correctly
    if self.ModifierDialog: #only reason to check is if closed before ModifierDialog is constructed
        self.ModifierDialog.Destroy()
    self.Close(1)
#@+node:ekr.20051104081502.447: *10* <<save configuration file>>
cp.remove_section('Files')
cp.add_section("Files")

x,y = self.GetSizeTuple()

cp.set('Configuration','x', str(x))
cp.set('Configuration','y', str(y))

numfiles = self.filehistory.GetNoHistoryFiles()

for n in range(numfiles):
    cp.set("Files", "path%d"%n, self.filehistory.GetHistoryFile(n))

try:
    #you have to give ConfigParser a writable object
    cfile = file(config_file, 'w')
    cp.write(cfile)
    cfile.close()
except IOError:
    g.pr("The configuration file can't be written!")
    time.sleep(10) #so you can see that there was a problem
#@+node:ekr.20051104081502.448: *8* Find methods
#@+node:ekr.20051104081502.449: *9* OnFind
def OnFind(self, evt=None):
    self.FindDialog.Show(True)
    self.FindDialog.FindText.SetSelection(-1,-1)
    self.FindDialog.FindText.SetFocus()


#@+node:ekr.20051104081502.450: *9* FindString
def FindString(self, evt=None):
    L = self.L
    Properties = self.PropertyDicts[L]
    cursor = self.Cursors[Properties['host']]
    table = Properties['table']

    pat = self.FindDialog.FindText.GetValue()
    likepat = r"'%"+pat+r"%'"
    finished = self.FindDialog.SearchFinished.GetValue()
    notes = self.FindDialog.SearchNotes.GetValue()

    if finished:
        WHERE = "WHERE "
    else:
        WHERE = "WHERE finisheddate IS NULL AND "

    if notes:
        SELECT = "SELECT priority,name,createdate,finisheddate,duedate,owner1,owner2,owner3,id,timestamp,note FROM %s "%table
        WHERE = WHERE + "(name LIKE %s OR note LIKE %s) ORDER BY timestamp DESC"%(likepat,likepat)
    else:
        SELECT = "SELECT priority,name,createdate,finisheddate,duedate,owner1,owner2,owner3,id,timestamp FROM %s "%table
        WHERE = WHERE + "name LIKE %s ORDER BY timestamp DESC"%likepat

    sql = SELECT + WHERE			
    try:
        cursor.execute(sql)
    except:
        g.pr("Cannot read %s: %s"%(Properties['host'],table))
        return
    else:
        results = cursor.fetchall()

    case = self.FindDialog.MatchCase.GetValue()
    whole = self.FindDialog.MatchWhole.GetValue()

    if whole:
        pat = '\\b%s\\b'%pat

    if case:
        z = re.compile(pat)
    else:
        z =re.compile(pat, re.I)

    if notes:
        results = [x for x in results if re.search(z,x[1]) or re.search(z,x[10])]
    else:
        results = [x for x in results if re.search(z,x[1])]

    Properties['LCdate'] = 'timestamp'
    self.ItemLists[L]= IList = self.CreateAndDisplayList(results)

    LCtrl = self.ListCtrls[L]
    col_num = self.attr2col_num['date']
    col_info = LCtrl.GetColumn(col_num)
    col_info.SetText(self.date_titles['timestamp'])
    LCtrl.SetColumn(col_num,col_info)

    if IList:
        self.curIdx = 0
        LCtrl.SetItemState(0, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED)
    else:		
        self.curIdx = -1

    self.OnItemSelected()

    Properties['sort'] = {'direction':0,'attribute':'date'}
    Properties['owner'] = '*ALL'

    owner_idx = self.OwnerLBoxes[L].GetSelection()
    if owner_idx != -1:
        self.OwnerLBoxes[L].SetSelection(owner_idx, 0) #get exception if index = -1

    self.SetStatusText("Found %d items"%len(IList))
#@+node:ekr.20051104081502.451: *9* FindNode
def FindNode(self, item, showfinished=True):
    L = self.L
    LCtrl = self.ListCtrls[L]
    Properties = self.PropertyDicts[L]

    Properties['owner'] = '*ALL'
    Properties['showfinished'] = showfinished

    self.ItemLists[L] = IList = self.CreateAndDisplayList(self.ReadFromDB())

    id = item.id
    idx = -1
    for item in IList:
        idx+=1
        if id == item.id:
            break
    else:
        idx = -1

    if idx != -1:	
        LCtrl.SetItemState(idx, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED)
        LCtrl.EnsureVisible(idx)
    self.curIdx = idx

#@+node:ekr.20051104081502.452: *8* Database-related methods
#@+node:ekr.20051104081502.453: *9* GetCursor
def GetCursor(self, host):
    cursor = self.Cursors.get(host)
    if cursor:
        return cursor

    location, rdbms = host.split(':')

    if rdbms == 'sqlite':
        db = os.path.join(DIRECTORY,location,DB)
        try:
            Con = sqlite.connect(db=db, autocommit=1)
            cursor = Con.cursor()
            self.sqlite_connections.append(Con)  #getting a weak reference error from PySQLite and this makes it go away
        except:
            dlg = wxMessageDialog(self,
                    "Could not connect to SQLite database at %s"%location,
                    "Connection problem!",
                    wxICON_ERROR|wxOK)
            dlg.ShowModal()
            dlg.Destroy()
            cursor = None

    elif not OFFLINE_ONLY:
        try:
            Con = MySQLdb.connect(host=location, user=USER, passwd=PW, db=DB)
            cursor = Con.cursor()
        except:
            dlg = wxMessageDialog(self,
                    "host = %s | user = %s | password = %s**** | db = %s - could not connect!"%(host,USER,PW[:3],DB),
                    "Connection problem",
                    wxICON_ERROR|wxOK)
            dlg.ShowModal()
            dlg.Destroy()
            cursor = None

    if cursor:
        self.Cursors[host] = cursor

    return cursor


#@+node:ekr.20051104081502.454: *9* GetNote
def GetNote(self, L=None, item=None):
    if L is None:
        L = self.L

    if item is None:
        if self.curIdx != -1:
            item = self.ItemLists[L][self.curIdx]
        else:
            return ''

    Properties = self.PropertyDicts[L]

    cursor = self.Cursors[Properties['host']]
    table = Properties['table']
    cursor.execute("SELECT note from "+table+" WHERE id = %s", (item.id,))

    ###### Debug -- this does happen where note brings back None 053003
    z = cursor.fetchone()
    if z is None:
        g.pr("In GetNote -> SELECT should not bring back None")
        g.pr("           -> item.id=",item.id)
        z = (None,)
    note = z[0]
    if note is None:
        note = ''
    return note

#@+node:ekr.20051104081502.455: *9* CreateTable
def CreateTable(self, host, table):
    cursor = self.Cursors[host]
    rdbms = host.split(':')[1]
    if rdbms == 'sqlite':
        sql = """CREATE TABLE '%s' ('id' varchar(36) PRIMARY KEY,
'priority' int(1),
'name' varchar(150),
'createdate' datetime,
'finisheddate' date,
'duedate' date,
'owner1' varchar(25),
'owner2' varchar(25),
'owner3' varchar(25),
'note' text,
'timestamp' timestamp(14))"""%table
    else:
        sql = """CREATE TABLE `%s` (`id` varchar(36) NOT NULL default '',
`priority` int(1) NOT NULL default '1',
`name` varchar(150) NOT NULL default '',
`createdate` datetime NOT NULL default '0000-00-00 00:00:00',
`finisheddate` date default '0000-00-00',
`duedate` date default '0000-00-00',
`owner1` varchar(25) default '',
`owner2` varchar(25) default '',
`owner3` varchar(25) default '',
`note` text,
`timestamp` timestamp(14) NOT NULL,PRIMARY KEY  (`id`)) TYPE=MyISAM"""%table

    cursor.execute(sql)
#@+node:ekr.20051104081502.456: *9* ReadFromDB (returns db results)
def ReadFromDB(self):
    L = self.L
    Properties = self.PropertyDicts[L]

    host = Properties['host']
    cursor = self.GetCursor(host)
    if cursor is None:
        return None

    table = Properties['table']

    owner = Properties['owner']
    if owner == '*ALL':
        WHERE = ""
    else:
        WHERE = 'WHERE (owner1 = "%s" OR owner2 = "%s" OR owner3 = "%s")'%(owner,owner,owner)

    #-1 show them all; 0 show none; integer show for that many days
    days = Properties['showfinished']	
    if days != -1:
        if days:
            date = mx.DateTime.now() - days
            t = "(finisheddate IS NULL OR finisheddate > '%s')"%date
        else:
            t = "finisheddate IS NULL"

        if WHERE:
            WHERE = "%s AND %s"%(WHERE,t)
        else:
            WHERE = " WHERE %s"%t

    Sort = Properties['sort']
    if Sort:
        sort_attr = Sort['attribute']
        if sort_attr == 'date':
            sort_attr = Properties['LCdate']
        elif sort_attr == 'owners':
            sort_attr = 'owner1'

        WHERE = WHERE + " ORDER BY " + sort_attr
        #if not direction: WHERE = WHERE + " DESC"   works because ASC is the default
        if not Sort['direction']:
            WHERE = WHERE + " DESC" 

    sql = "SELECT priority,name,createdate,finisheddate,duedate,owner1,owner2,owner3,id,timestamp FROM %s %s"%(table,WHERE)

    try:
        cursor.execute(sql)
    except:
        g.pr("Cannot read %s: %s"%(Properties['host'],table))
        return None #[]
    else:
        return cursor.fetchall()



#@+node:ekr.20051104081502.457: *9* CreateAndDisplayList (returns Item List)
def CreateAndDisplayList(self, results):
    LCtrl = self.ListCtrls[self.L]
    LCdate = self.PropertyDicts[self.L]['LCdate']
    if LCdate == 'timestamp':
        format = '%m/%d %H:%M:%S'
    else:
        format = '%m/%d/%y'
    itemlist = []

    LCtrl.DeleteAllItems()
    class Item: pass

    for x,row in enumerate(results):

        item = Item()
        << assign item attributes >>
        itemlist.append(item)

        << draw item >>

    return itemlist


#@+node:ekr.20051104081502.458: *10* << assign item attributes >>
item.priority = int(row[0]) #int(row[0]) needs int because it seems to come back as a long from MySQL
item.name = row[1]
item.createdate = row[2]
item.finisheddate = row[3]
item.duedate = row[4]
item.owners = [y for y in row[5:8] if y] #if you carry around ['tom',None,None] Note this is 5:8 not 5:7
item.id = row[8]
item.timestamp = row[9]

#@+node:ekr.20051104081502.459: *10* << draw item >>
LCtrl.InsertImageStringItem(x, str(item.priority), LCtrl.idx1)
LCtrl.SetStringItem(x,1,item.name)
LCtrl.SetStringItem(x,2,'; '.join(item.owners))
date = item.__dict__[LCdate]
LCtrl.SetStringItem(x,3,date and date.Format(format) or "")

if item.finisheddate:
    LC_Item = LCtrl.GetItem(x)
    LC_Item.SetImage(LCtrl.idx0) #might just want generic number or greyed one two three
    LC_Item.SetTextColour(wxLIGHT_GREY)
    LCtrl.SetItem(LC_Item)

elif item.priority==2:
    LC_Item = LCtrl.GetItem(x)
    f = self.LC_font
    f.SetWeight(wxBOLD)
    LC_Item.SetFont(f)
    f.SetWeight(wxNORMAL) #resetting weight
    LCtrl.SetItem(LC_Item)

elif item.priority==3:
    LC_Item = LCtrl.GetItem(x)
    f = self.LC_font
    f.SetWeight(wxBOLD)
    LC_Item.SetFont(f)
    f.SetWeight(wxNORMAL) #return to normal
    LC_Item.SetTextColour(wxRED)
    LCtrl.SetItem(LC_Item)
#@+node:ekr.20051104081502.460: *9* OnSync
def OnSync(self, evt=None):
    if self.modified:
        self.OnUpdate()
    #Note that the results of an sqlite query are an instance that you need to turn into a tuple or MySQL gets unhappy

    if OFFLINE_ONLY:
        dlg = wxMessageDialog(self, "You need to be online to synchronize!", style = wxOK|wxICON_ERROR)
        dlg.ShowModal()
        dlg.Destroy()
        return

    dlg = wxMessageDialog(self,"Synchronize Table(s): "+" and ".join(SYNC_TABLES),"Synchronize...",wxICON_QUESTION|wxYES_NO)
    val = dlg.ShowModal()
    dlg.Destroy()
    if val == wxID_NO:
        return

    if REMOTE_HOST is None:
        g.pr("There doesn't appear to be a Remote Server")
        return

    if LOCAL_HOST is None:
        g.pr("There doesn't appear to be a Local Server")
        return

    g.pr("LOCAL_HOST=",LOCAL_HOST)
    g.pr("REMOTE_HOST=",REMOTE_HOST)

    r_cursor = self.GetCursor(REMOTE_HOST)
    if r_cursor is None:
        g.pr("Couldn't get a cursor for %s"%REMOTE_HOST)
        return

    l_cursor = self.GetCursor(LOCAL_HOST)
    if l_cursor is None:
        g.pr("Couldn't get a cursor for %s"%LOCAL_HOST)
        return

    # moving the sync time back a second to make sure that we don't lose track of any nodes
    #that are being updated or inserted at the same time as we are syncing
    r_cursor.execute("SELECT NOW()")
    l_now = mx.DateTime.now()-mx.DateTime.oneSecond
    r_now = r_cursor.fetchone()[0]-mx.DateTime.oneSecond
    #because of some inconsistent rounding appears necessary to make sure the sqlite timestamp is less than l_now
    #having seen same issue for mysql but for consistency (and because sqlite could also be "server" rdbms
    l_ts = l_now - mx.DateTime.DateTimeDelta(0,0,0,0.02)
    r_ts = r_now - mx.DateTime.DateTimeDelta(0,0,0,0.02)
    g.pr("l_now=",l_now, "l_ts =",l_ts)
    g.pr("r_now=",r_now, "r_ts=",r_ts)

    r_cursor.execute("SELECT MAX(last_sync) FROM user_sync WHERE user = %s", (USER,))
    r_last_sync = r_cursor.fetchone()[0]
    g.pr("last sync (remote time) =",r_last_sync)

    l_cursor.execute("SELECT MAX(last_sync) FROM user_sync")
    l_last_sync = l_cursor.fetchone()[0] #note MAX returns a string with sqlite so we turn it make into DateTime
    l_last_sync = mx.DateTime.DateTimeFrom(l_last_sync)
    g.pr("last sync (local time) =",l_last_sync)

    for table in SYNC_TABLES:
        # Need to pick up changes for both so syncing one doesn't add new things and screw up the second sync
        g.pr("Checking "+table+" on the Remote Server; changes (excluding deletes) are:")
        r_cursor.execute("SELECT id,createdate from "+table+" WHERE timestamp > %s AND timestamp <= %s",(r_last_sync,r_now)) 
        r_results = r_cursor.fetchall()
        g.pr("Server changes (excluding deletes)")
        g.pr(r_results)

        g.pr("Checking "+table+" on Local; changes (excluding deletes) are:")
        l_cursor.execute("SELECT id,createdate from "+table+" WHERE timestamp > %s AND timestamp <= %s",(l_last_sync,l_now))
        l_results = l_cursor.fetchall()
        g.pr("Local changes (excluding deletes)")
        g.pr(l_results)

        for id, createdate in r_results:
            r_cursor.execute("SELECT priority,name,owner1,owner2,owner3,createdate,finisheddate,duedate,note,id FROM "+table+" WHERE ID = %s",(id,))
            row = r_cursor.fetchone()
            if row:
                if createdate > r_last_sync:
                    l_cursor.execute("INSERT INTO "+table+" (priority,name,owner1,owner2,owner3,createdate,finisheddate,duedate,note,id) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", row) #*row also works
                    g.pr("Created %s in %s on Local"%(id,table))
                else:
                    l_cursor.execute("UPDATE "+table+" SET priority = %s, name =%s, owner1 = %s, owner2 = %s, owner3 = %s, createdate = %s, finisheddate = %s, duedate = %s, note = %s WHERE id = %s", row)
                    g.pr("Updated %s in %s on Local"%(id,table))
                # for reasons I don't understand l_now here is a 1/100 ahead of l_now when inserted into user_sync
                l_cursor.execute("UPDATE "+table+" SET timestamp = %s WHERE id = %s", (l_ts,id))

        for id, createdate in l_results:
            l_cursor.execute("SELECT priority,name,owner1,owner2,owner3,createdate,finisheddate,duedate,note,id FROM "+table+" WHERE ID = %s",(id,))
            row = l_cursor.fetchone()
            if row:
                row = tuple(row)
                #above needed because sqlite returns an enhanced tuple-like object that is not a tuple
                if createdate > l_last_sync:
                    r_cursor.execute("INSERT INTO "+table+" (priority,name,owner1,owner2,owner3,createdate,finisheddate,duedate,note,id) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", row)
                    g.pr("Created %s in %s on Server"%(id,table))
                else:
                    r_cursor.execute("UPDATE "+table+" SET priority = %s, name =%s, owner1 = %s, owner2 = %s, owner3 = %s, createdate = %s, finisheddate = %s, duedate = %s, note = %s WHERE id = %s", row)
                    g.pr("Updated %s in %s on Server"%(id,table))
                r_cursor.execute("UPDATE "+table+" SET timestamp = %s WHERE id = %s", (r_ts,id))

    #Handle the deletes; Note if at some point only 'd's are being written won't have to check for 'd'
    r_cursor.execute("SELECT id,table_name FROM sync WHERE timestamp > %s AND timestamp <= %s AND action = 'd'",(r_last_sync,r_now))
    r_results = r_cursor.fetchall()

    l_cursor.execute("SELECT id,table_name FROM sync WHERE timestamp > %s AND timestamp <= %s AND action = 'd'",(l_last_sync,l_now))
    l_results = l_cursor.fetchall()

    for id,table in l_results:
        r_cursor.execute("DELETE from "+table+" WHERE id = %s", (id,))
        g.pr("Deleted %s from %s on Server (if it existed there)"%(id,table))

    for id,table in r_results:
        l_cursor.execute("DELETE from "+table+" WHERE id = %s", (id,))
        g.pr("Deleted %s from %s on Local (if it existed there)"%(id,table)	)
    #End of deletes code

    #update the user_sync database with the latest sync times
    l_cursor.execute("INSERT INTO user_sync (user,last_sync) VALUES (%s,%s)", (USER,l_now)) #don't really need USER for local
    r_cursor.execute("INSERT INTO user_sync (user,last_sync) VALUES (%s,%s)", (USER,r_now)) 

    g.pr("Synchronization completed")

#@+node:ekr.20051104081502.461: *9* TimeStamper
def TimeStamper(self, host, cursor, table, id):
    #note that you can insert a timestamp value into an mysql timestamp field
    if host.split(':')[1] == 'sqlite': #host -> location:rdbms
        timestamp = mx.DateTime.now()
        cursor.execute("UPDATE "+table+" SET timestamp = %s WHERE id = %s", (timestamp,id))
    else:
        cursor.execute("Select timestamp from "+table+" WHERE id = %s", (id,))
        timestamp = cursor.fetchone()[0]

    return timestamp
#@+node:ekr.20051104081502.462: *8* Evaluate methods
#@+node:ekr.20051104081502.463: *9* OnShowEvaluate
def OnShowEvaluate(self, evt=None):

    self.EvalDialog.Show(True)
    self.EvalDialog.EvalText.SetSelection(-1,-1)
    self.EvalDialog.EvalText.SetFocus()

#@+node:ekr.20051104081502.464: *9* OnEvaluate
def OnEvaluate(self, evt=None):
    expr = self.EvalDialog.EvalText.GetValue()
    g.pr("%s => "%expr,newline=False)
    g.pr(eval(expr))

#@+node:ekr.20051104081502.465: *8* Help menu methods
#@+node:ekr.20051104081502.466: *9* OnShowAbout
def OnShowAbout(self, evt=None):
    from about import AboutBox
    dlg = AboutBox(self, app_version = VERSION)
    dlg.ShowModal()
    dlg.Destroy()

#@+node:ekr.20051104081502.467: *9* OnShowHelp
def OnShowHelp(self, evt=None):
    os.startfile('ListManager.chm')

#@+node:ekr.20051104081502.468: *8* GetUID
def GetUID(self):
    pyiid = CreateGuid()
    # the str(pyiid) looks like {....} and doing [1:-1] strips that off
    return str(pyiid)[1:-1]

#@+node:ekr.20051104081502.469: *8* OnIdle
def OnIdle(self, evt):	
    << Check for Transfers From Outlook >>
    << Check if Edited File has Changed >>

#@+node:ekr.20051104081502.471: *9* << Check for Transfers From Outlook >>
if OUTLOOK:
    input,output,exc = select.select([self.sock],[],[],0)
    if input:
        client,addr = self.sock.accept() # Get a connection
        rec = client.recv(8192)
        d = pickle.loads(rec)

        class Item: pass

        item = Item()
        item.id = self.GetUID()
        item.priority = 1
        item.createdate = mx.DateTime.now()
        item.duedate = item.finisheddate = None

        #outlook strings are unicode; ascii encode makes sure no chars above 127
        name = d['Subject'].encode('ascii','replace') 
        item.name = name[:150]

        owner = d['SenderName'].encode('ascii','replace') #encode takes unicode to standard strings
        owner = owner[:25]
        item.owners = [owner]

        note = d['CreationTime'] + '\n' + d['Body'].encode('ascii','replace')
        #foldername = d['Parent.Name']

        #location, rdbms, table = MAIL_LIST_PATH.split(':')
        #host = '%s:%s'%(location,rdbms)
        host, table = re.split('(.*?:.*?):', MAIL_LIST_PATH)[1:3] #really just for fun

        cursor = self.Cursors[host]

        cursor.execute("INSERT INTO "+table+" (priority,name,createdate,finisheddate,duedate,owner1,note,id) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)",
            (item.priority, name, item.createdate, item.finisheddate, item.duedate, owner, note, item.id))

        item.timestamp = self.TimeStamper(host, cursor, table, item.id)

        #check to see if table is open
        for L,Properties in enumerate(self.PropertyDicts):
            if Properties['table'] == table and Properties['host'] == host:
                break
        else:
            g.pr("Table not open but wrote to database anyway") #Needs to be a dialog box
            return

        # could have started to edit something and never finished it
        if self.modified:
            self.OnUpdate()

        if self.L != L:
            self.nb.SetSelection(L) # Note that this does not call OnPageChange if the page doesn't change

        LCtrl = self.ListCtrls[L]

        if self.curIdx != -1:
            LCtrl.SetItemState(self.curIdx, 0, wxLIST_STATE_SELECTED)

        self.ItemLists[L].insert(0,item)    
        LCtrl.InsertImageStringItem(0,"1", LCtrl.idx1)
        LCtrl.SetStringItem(0,self.attr2col_num['name'],name)
        LCtrl.SetStringItem(0, self.attr2col_num['owners'], owner)

        if Properties['LCdate'] == 'timestamp':
            LCtrl.SetStringItem(0, self.attr2col_num['date'], item.timestamp.Format("%m/%d %H:%M:%S"))
        elif Properties['LCdate'] == 'createdate':
            LCtrl.SetStringItem(0, self.attr2col_num['date'], item.createdate.Format('%m/%d/%y'))

        LCtrl.SetItemState(0, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED)
        self.curIdx = 0 

#@+node:ekr.20051104081502.473: *9* << Check if Edited File has Changed >>
for ed in self.editor:
    path = ed['path']
    t = os.path.getmtime(path)
    if t != ed['time']:
        f = file(path,'r')
        note = f.read()
        f.close()
        ed['time'] = t

        host = ed['host']
        cursor = self.Cursors[host]
        table = ed['table']
        id = ed['id']
        cursor.execute("UPDATE "+table+" SET note = %s WHERE id = %s", (note,id)) 
        # see @rst documentation note
        ts = self.TimeStamper(host, cursor, table, id)

        idx = self.curIdx
        L = self.L
        if idx != -1:
            item = self.ItemLists[L][idx]
            if item.id == id:
                self.note.SetValue(note)
                item.timestamp = ts

                if self.PropertyDicts[L]['LCdate'] == 'timestamp':
                    self.ListCtrls[L].SetStringItem(idx, self.attr2col_num['date'], item.timestamp.Format("%m/%d %H:%M:%S"))

                if 'note' in self.modified: #if necessary only if somehow note text didn't change
                    del self.modified['note']

#@+node:ekr.20051104081502.475: *7* class ListCtrl
class ListCtrl(wxListCtrl, wxListCtrlAutoWidthMixin):
    @others
#@+node:ekr.20051104081502.476: *8* __init__
def __init__(self, parent, ID, pos=wxDefaultPosition, size=wxDefaultSize, style=0):
    wxListCtrl.__init__(self, parent, ID, pos, size, style)
    wxListCtrlAutoWidthMixin.__init__(self)

    self.il = wxImageList(16,16)

    sm_up = self.il.Add(wxBitmap('bitmaps\\up_arrow.bmp')) #(images.getSmallUpArrowBitmap())
    sm_dn = self.il.Add(wxBitmap('bitmaps\\down_arrow.bmp'))
    self.arrows = (sm_up,sm_dn)

    self.idx1 = self.il.Add(wxBitmap('bitmaps\\box.bmp'))
    self.idx0 = self.il.Add(wxBitmap('bitmaps\\filledwhitebox.bmp'))    

    self.SetImageList(self.il, wxIMAGE_LIST_SMALL)

    EVT_LIST_COL_BEGIN_DRAG(self, self.GetId(), self.OnColBeginDrag)    

    self.SetUpColumns()

#@+node:ekr.20051104081502.477: *8* SetUpColumns
def SetUpColumns(self):
    #Need to to construct column heads for columns with sorting by hand to get sorting images on columns
    info = wxListItem()
    info.m_mask = wxLIST_MASK_TEXT | wxLIST_MASK_IMAGE | wxLIST_MASK_FORMAT
    info.m_image = -1

    #Oth column is priority which is sortable
    info.m_format = wxLIST_FORMAT_LEFT
    info.m_text = "P"
    self.InsertColumnInfo(0, info)
    self.SetColumnWidth(0, 35)

    self.InsertColumn(1, "Name")
    self.SetColumnWidth(1, 590)

    self.InsertColumn(2, "Owner")
    self.SetColumnWidth(2, 100)

    #3th column is create ate and same as with priority - needs to constructed by hand
    info.m_format = wxLIST_FORMAT_LEFT
    info.m_text = "Due Date"
    self.InsertColumnInfo(3, info)
    self.SetColumnWidth(3, 75)                

#@+node:ekr.20051104081502.478: *8* OnColBeginDrag
def OnColBeginDrag(self, evt):
    #if inplace editor then change its dimensions
    if evt.GetColumn() == 0:
        evt.Veto()
#@+node:ekr.20051104081502.479: *7* class MyApp
class MyApp(wxApp):
    @others
#@+node:ekr.20051104081502.480: *8* OnInit
def OnInit(self):
    global OFFLINE_ONLY, CANCEL
    wxInitAllImageHandlers()

    if STARTUP_DIALOG:
        startup = StartupDialog(None, 'List Manager')
        val = startup.ShowModal()
        startup.Destroy()
        if val == wxID_YES:
            OFFLINE_ONLY = True
        elif val == wxID_NO:
            OFFLINE_ONLY = False
        elif val == wxID_CANCEL:
            CANCEL = True
            return True

    if OFFLINE_ONLY is False:
        server = REMOTE_HOST.split(':')[0]
        try:
            socket.gethostbyname(server)
        except:
            dlg = wxMessageDialog(None, "Cannot connect to remote server! Only offline access is possible.", "ListManager", style=wxOK|wxICON_EXCLAMATION|wxSTAY_ON_TOP)
            dlg.ShowModal()
            dlg.Destroy()
            OFFLINE_ONLY = True

    frame = ListManager(None, -1, "List Manager", size = (X,Y))
    frame.Show(True)
    self.SetTopWindow(frame)
    CANCEL = False
    return True


#@+node:ekr.20051104081502.481: *7* class Logger
class Logger:
    def __init__(self):
        self.dlg = LoggerDialog(None, "", "Alerts and Exceptions", dir=DIRECTORY)
    def write(self, error_msg):
        if not self.dlg.IsShown():
            self.dlg.text.AppendText("\n%s\n"%time.asctime())
            self.dlg.Show(True)

        self.dlg.text.AppendText(error_msg)

#@+node:ekr.20051104081502.482: *7* run
def run():
    app = MyApp(0)
    if not CANCEL:
        sys.stderr = sys.stdout = Logger()
        app.MainLoop()

if __name__ == '__main__':
    run()
#@+node:ekr.20051104081502.483: *6* LMDialogs.py
@ @rst-options
code_mode = True
@c


@language python
from wxPython.wx import *
# the following two are needed for the calendar
from wxPython.calendar import *
from wxPython.utils import *
import os
@others
#@+node:ekr.20051104081502.484: *7* class PopDialog
class TicklerDialog(wxDialog):
    @others
#@+node:ekr.20051104081502.485: *8* __init__
def __init__(self, parent, msg, caption, pos = wxDefaultPosition, size = wxDefaultSize):
    wxDialog.__init__(self, parent, -1, caption, pos, size, style=wxSTAY_ON_TOP | wxTHICK_FRAME | wxCAPTION)

    TC = wxTextCtrl(self, -1, msg, wxDefaultPosition,
                    (450,250),
                    wxTE_MULTILINE | wxTE_READONLY | wxTE_RICH2)

    sizer = wxBoxSizer(wxVERTICAL)
    box = wxBoxSizer(wxHORIZONTAL)        

    sizer.Add(TC, 1, wxALIGN_CENTRE|wxALL, 5)
    line = wxStaticLine(self, -1, size = (20,-1), style = wxLI_HORIZONTAL)

    sizer.Add(line, 0, wxGROW|wxALIGN_CENTER_VERTICAL|wxTOP, 5)
    btn = wxButton(self, wxID_OK, "GO TO ITEM")
    box.Add(btn, 0, wxALIGN_CENTRE|wxALL, 5)
    btn.SetDefault()

    btn = wxButton(self, wxID_FORWARD, "SHOW NEXT")
    box.Add(btn, 0, wxALIGN_CENTRE|wxALL, 5)

    btn = wxButton(self, wxID_APPLY, "MAIL")
    box.Add(btn, 0, wxALIGN_CENTRE|wxALL, 5)        

    btn = wxButton(self, wxID_CANCEL, "CANCEL")
    box.Add(btn, 0, wxALIGN_CENTRE|wxALL, 5)

    sizer.AddSizer(box, 0, wxGROW|wxALIGN_CENTER_VERTICAL|wxALL, 5)
    self.SetSizer(sizer)
    self.SetAutoLayout(True)
    sizer.Fit(self)

    EVT_LEFT_DOWN(TC, self.OnLeftDown)
    EVT_BUTTON(self, wxID_FORWARD, self.OnForward)
    EVT_BUTTON(self, wxID_APPLY, self.OnMail)

    TC.SetCursor(wxStockCursor(wxCURSOR_ARROW))        

    self.TC = TC
#@+node:ekr.20051104081502.486: *8* OnLeftDown
def OnLeftDown(self, evt):
    self.EndModal(wxID_OK)
#@+node:ekr.20051104081502.487: *8* OnForward
def OnForward(self, evt):
    self.EndModal(wxID_FORWARD)
#@+node:ekr.20051104081502.488: *8* OnMail
def OnMail(self, evt):
    self.EndModal(wxID_APPLY)        
#@+node:ekr.20051104081502.489: *7* class StartupDialog
class StartupDialog(wxDialog):
    @others
#@+node:ekr.20051104081502.490: *8* __init__
def __init__(self, parent, caption, pos=wxDefaultPosition, size=(300,115)):
    wxDialog.__init__(self, parent, -1, caption, pos, size, style=wxSTAY_ON_TOP|wxCAPTION)

    msg = "You can connect to the server using the network,\nor work offline, or cancel this logon."

    image = wxStaticBitmap(self, -1, wxBitmap('bitmaps\\wxpdemo.bmp'), (-1,-1), size=(32,32)) #sizer determines position
    text = wxStaticText(self, -1, msg, (-1,-1), size=(250,32)) #sizer determines position

    rect = wxBoxSizer(wxHORIZONTAL)
    rect.Add(image, 0, wxALIGN_LEFT|wxALL, 4)
    rect.Add(text, 1, wxALIGN_CENTER|wxTOP, 7)
    sizer = wxBoxSizer(wxVERTICAL)


    box = wxBoxSizer(wxHORIZONTAL)
    btn = wxButton(self, wxID_NO, 'Connect')
    box.Add(btn, 0, wxALL, 10)
    btn.SetDefault()

    btn = wxButton(self, wxID_YES, 'Work Offline')
    box.Add(btn, 0, wxALL, 10)

    btn = wxButton(self, wxID_CANCEL, 'Cancel')
    box.Add(btn, 0, wxALL, 10)

    sizer.AddSizer(rect)
    sizer.AddSizer(box)

    self.SetSizer(sizer)

    EVT_BUTTON(self, wxID_NO, self.OnSelection)
    EVT_BUTTON(self, wxID_YES, self.OnSelection)
    EVT_BUTTON(self, wxID_CANCEL, self.OnSelection)
#@+node:ekr.20051104081502.491: *8* OnSelection
def OnSelection(self,evt):
    val = evt.GetId()
    self.EndModal(val)
#@+node:ekr.20051104081502.492: *7* class ModifierDialog
class ModifierDialog(wxDialog):
    @others
#@+node:ekr.20051104081502.493: *8* __init__
def __init__(self, parent, title,
             pos=wxDefaultPosition,
             size=wxDefaultSize,
             style=wxCAPTION,
             modifierlist=None,
             curselections = ''):
    wxDialog.__init__(self, parent, -1, title, pos, size, style)

    sizer1 = wxBoxSizer(wxVERTICAL)
    sizer2 = wxBoxSizer(wxHORIZONTAL)

    tc = wxTextCtrl(self, -1, "", size = (150,-1))
    sizer1.Add(tc, 0, wxALIGN_CENTRE|wxALL, 5)
    self.tc = tc

    if not modifierlist:
        modifierlist = []
    lb = wxListBox(self, -1,  wxDefaultPosition, (150,300), #wxPoint(90, 80)
                    modifierlist, wxLB_MULTIPLE|wxLB_SORT)

    sizer1.Add(lb, 1, wxALIGN_CENTRE|wxALL, 5)

    line = wxStaticLine(self, -1, size = (20,-1), style = wxLI_HORIZONTAL)
    sizer1.Add(line, 0, wxGROW|wxALIGN_CENTER_VERTICAL|wxTOP, 5)


    btn = wxButton(self, wxID_OK, "OK")
    sizer2.Add(btn, 0, wxALIGN_CENTRE|wxALL, 5)
    btn.SetDefault()

    btn = wxButton(self, wxID_CANCEL, "CANCEL")
    sizer2.Add(btn, 0, wxALIGN_CENTRE|wxALL, 5)

    sizer1.AddSizer(sizer2, 0, wxGROW|wxALIGN_CENTER_VERTICAL|wxALL, 5)
    self.SetSizer(sizer1)
    self.SetAutoLayout(True)
    sizer1.Fit(self)

    for sel in curselections:
        index = lb.FindString(sel)
        if index !=-1:
            lb.SetSelection(index)

    self.lb = lb

    EVT_BUTTON(self, wxID_CANCEL, self.ClearSelections)


#@+node:ekr.20051104081502.494: *8* GetUserInput
def GetUserInput(self):
    idx_list = self.lb.GetSelections()
    mod_list =[]
    for i in idx_list:
        mod_list.append(self.lb.GetString(i))
        self.lb.Deselect(i) #071203

    new_list = []
    manual_string = self.tc.GetValue() #text entry box

    if manual_string:
        manual_list = [x.strip() for x in manual_string.split(';')]
        for name in manual_list:
            clean_name = ", ".join([x.strip().title() for x in name.split(',')])
            if clean_name not in mod_list:
                mod_list.append(clean_name)
                new_list.append(clean_name)


    return (mod_list, new_list)
#@+node:ekr.20051104081502.495: *8* SelectCurrent
def SelectCurrent(self, cur_sel):
    for sel in cur_sel:
        index = self.lb.FindString(sel)
        if index !=-1:
            self.lb.SetSelection(index)


#@+node:ekr.20051104081502.496: *8* ClearSelections
def ClearSelections(self, evt=None):
    idx_list = self.lb.GetSelections() #note you can't just use the indexes of the SelectCurrent since they may have clicked before cancelling
    for i in idx_list:
        self.lb.Deselect(i)

    evt.Skip()
#@+node:ekr.20051104081502.497: *7* class MailDialog
class MailDialog(wxDialog):
    @others
#@+node:ekr.20051104081502.498: *8* __init__
def __init__(self, parent, title,
             pos=wxDefaultPosition,
             size=wxDefaultSize,
             style=wxSTAY_ON_TOP| wxTHICK_FRAME|wxCAPTION|wxSYSTEM_MENU,
             recipients='',
             subject = '',
             body = ''):

    wxDialog.__init__(self, parent, -1, title, pos, size, style)

    sizer = wxBoxSizer(wxVERTICAL)
    box = wxBoxSizer(wxHORIZONTAL)

    recipients = "; ".join(recipients)
    label = wxStaticText(self, -1, "To:",wxDefaultPosition, size=(40,-1), style=wxALIGN_LEFT)
    RTC = wxTextCtrl(self, -1, recipients, size = (480,-1))
    box.Add(label)
    box.Add(RTC)

    #sizer.Add(10,10,0)      

    sizer.AddSizer(box)        

    box = wxBoxSizer(wxHORIZONTAL)       
    label = wxStaticText(self, -1, "Subject:",wxDefaultPosition, size=(40,-1),style=wxALIGN_LEFT)
    STC = wxTextCtrl(self, -1, subject, size = (480,-1)) 
    box.Add(label)
    box.Add(STC)

    sizer.AddSizer(box)
    sizer.Add(1, 5, 0)

    BTC = wxTextCtrl(self, -1, body, wxDefaultPosition, size = (500,400), style=wxTE_MULTILINE|wxTE_RICH2)

    sizer.Add(BTC)

    box = wxBoxSizer(wxHORIZONTAL)
    btn = wxButton(self, wxID_OK, "SEND MAIL")
    box.Add(btn, 0, wxALIGN_CENTRE|wxALL, 5)
    btn.SetDefault()

    btn = wxButton(self, wxID_CANCEL, "CANCEL")
    box.Add(btn, 0, wxALIGN_CENTRE|wxALL, 5)

    sizer.AddSizer(box)
    self.SetSizer(sizer)
    self.SetAutoLayout(True)
    sizer.Fit(self)

    self.RTC = RTC
    self.STC = STC
    self.BTC = BTC

#@+node:ekr.20051104081502.499: *7* class CalendarDialog
class CalendarDialog(wxDialog):
    @others
#@+node:ekr.20051104081502.500: *8* __init__
def __init__(self, parent, title,
             pos=wxDefaultPosition,
             size=wxDefaultSize,
             style=wxCAPTION,
             date=0):

    wxDialog.__init__(self, parent, -1, title, pos, size, style)

    if not date:
        date = wxDateTime_Now()

    cal = wxCalendarCtrl(self, -1, date, #pos = (25,50),
                         style = wxCAL_SHOW_HOLIDAYS | wxCAL_SUNDAY_FIRST)

    EVT_CALENDAR(self, cal.GetId(), self.OnCalSelected)

    #EVT_CLOSE(self, self.OnCloseWindow)          

    self.cal = cal

    # Set up control to display a set of holidays:
    EVT_CALENDAR_MONTH(self, cal.GetId(), self.OnChangeMonth)

    self.holidays = [(1,1), (10,31), (12,25) ]    # (these don't move around)

    self.OnChangeMonth()        

#-------------------------------------------------------------------------        
    sizer1 = wxBoxSizer(wxVERTICAL)
    sizer2 = wxBoxSizer(wxHORIZONTAL)

    sizer1.Add(cal, 0, wxALIGN_CENTRE|wxALL, 5)

    line = wxStaticLine(self, -1, size = (20,-1), style = wxLI_HORIZONTAL)
    sizer1.Add(line, 0, wxGROW|wxALIGN_CENTER_VERTICAL|wxRIGHT|wxTOP, 5)


    btn = wxButton(self, wxID_OK, "OK")
    btn.SetDefault()
    sizer2.Add(btn, 0, wxALIGN_CENTRE|wxALL, 5)

    btn = wxButton(self, wxID_CANCEL, "CANCEL")
    #btn.SetDefault()
    sizer2.Add(btn, 0, wxALIGN_CENTRE|wxALL, 5)

    sizer1.AddSizer(sizer2, 0, wxGROW|wxALIGN_CENTER_VERTICAL|wxALL, 5)
    self.SetSizer(sizer1)
    self.SetAutoLayout(True)
    sizer1.Fit(self)
#@+node:ekr.20051104081502.501: *8* OnCalSelected
def OnCalSelected(self, evt):
    self.result = evt.GetDate()
    self.EndModal(wxID_OK)
#@+node:ekr.20051104081502.502: *8* OnChangeMonth
def OnChangeMonth(self, evt=None):
    cur_month = self.cal.GetDate().GetMonth() + 1   # convert wxDateTime 0-11 => 1-12
    for month, day in self.holidays:
        if month == cur_month:
            self.cal.SetHoliday(day)        
#@+node:ekr.20051104081502.503: *8* OnCloseWindow
def OnCloseWindow(self, event):
    #self.cal.Destroy
    #self.Destroy()
    g.pr("I got to close window")
#@+node:ekr.20051104081502.504: *8* GetDate
def GetDate(self):
    return self.result
#@+node:ekr.20051104081502.505: *7* class FindDialog
class FindDialog(wxDialog):
    @others
#@+node:ekr.20051104081502.506: *8* __init__
def __init__(self, parent, caption, msg, pos=wxDefaultPosition, size=(300,120)):
    wxDialog.__init__(self, parent, -1, caption, pos, size, style=wxSTAY_ON_TOP|wxCAPTION)    

    self.FindText = wxTextCtrl(self, -1, msg, wxDefaultPosition,(200,24))

    box_a = wxBoxSizer(wxHORIZONTAL)
    box_a.Add(self.FindText, 1, wxALIGN_CENTER|wxALL, 5)

    box_b = wxBoxSizer(wxVERTICAL)        
    btn = wxButton(self, wxID_OK, "OK")
    box_b.Add(btn, 0, wxALIGN_CENTER|wxALL,5)
    btn.SetDefault()               

    btn = wxButton(self, wxID_CANCEL, "CANCEL")
    box_b.Add(btn, 0, wxALIGN_CENTER)

    box_a.AddSizer(box_b)

    self.MatchCase = wxCheckBox(self, -1, "Match Case")
    self.MatchWhole = wxCheckBox(self, -1, "Match Whole Word")
    box_c = wxBoxSizer(wxVERTICAL)
    box_c.Add(self.MatchCase, 0, wxLEFT|wxBOTTOM, 5)
    box_c.Add(self.MatchWhole, 0, wxLEFT, 5)

    self.SearchNotes = wxCheckBox(self, -1, "Search Notes")
    self.SearchFinished = wxCheckBox(self, -1, "Search Finished")
    box_d = wxBoxSizer(wxVERTICAL)
    box_d.Add(self.SearchNotes, 0, wxLEFT|wxBOTTOM, 5)
    box_d.Add(self.SearchFinished, 0, wxLEFT, 5)

    box_e = wxBoxSizer(wxHORIZONTAL)
    box_e.AddSizer(box_c)
    box_e.AddSizer(box_d)

    sizer = wxBoxSizer(wxVERTICAL)
    sizer.AddSizer(box_a)
    sizer.AddSizer(box_e)

    self.SetSizer(sizer)

    EVT_BUTTON(self, wxID_OK, parent.FindString)


#@+node:ekr.20051104081502.507: *7* class EvalDialog
class EvalDialog(wxDialog):
    @others
#@+node:ekr.20051104081502.508: *8* __init__
def __init__(self, parent, caption, msg, pos=wxDefaultPosition, size=(300,80)):
    wxDialog.__init__(self, parent, -1, caption, pos, size, style=wxSTAY_ON_TOP|wxCAPTION)    

    EvalText = wxTextCtrl(self, -1, msg, wxDefaultPosition,(200,24))


    box_a = wxBoxSizer(wxHORIZONTAL)
    box_a.Add(EvalText, 1, wxALIGN_CENTER|wxALL, 5)

    box_b = wxBoxSizer(wxVERTICAL)        
    btn = wxButton(self, wxID_OK, "OK")
    box_b.Add(btn, 0, wxALIGN_CENTER|wxALL,5)
    btn.SetDefault()               

    btn = wxButton(self, wxID_CANCEL, "CANCEL")
    box_b.Add(btn, 0, wxALIGN_CENTER)

    box_a.AddSizer(box_b)

    self.SetSizer(box_a)

    self.EvalText = EvalText
    self.parent = parent

    #EVT_BUTTON(self, wxID_OK, self.PostOKEvent)
    EVT_BUTTON(self, wxID_OK, parent.OnEvaluate)



#@+node:ekr.20051104081502.509: *8* PostOKEvent
def PostOKEvent(self, evt=None):
    wxPostEvent(self.parent, evt)
#@+node:ekr.20051104081502.510: *7* class LoggerDialog
class LoggerDialog(wxDialog):
    @others
#@+node:ekr.20051104081502.511: *8* __init__
def __init__(self, parent, msg, caption, pos=(-1,-1), size=(500,300), dir=None):
    wxDialog.__init__(self, parent, -1, caption, pos, size)
    #if pos == (-1,-1):
        #self.CenterOnScreen(wxBOTH)

    if dir:
        self.dir = dir
    else:
        self.dir = os.getcwd()

    text = wxTextCtrl(self, -1, msg, (-1,-1), (450,250), wxTE_MULTILINE | wxTE_READONLY)

    sizer = wxBoxSizer(wxVERTICAL)
    box = wxBoxSizer(wxHORIZONTAL)        

    sizer.Add(text, 1, wxALIGN_CENTRE|wxALL, 5)

    btn = wxButton(self, wxID_OK, "Close")
    box.Add(btn, 0, wxALIGN_CENTRE|wxALL, 5)
    btn.SetDefault()

    ID_SAVE = wxNewId()

    btn = wxButton(self, ID_SAVE, "Save to File")
    box.Add(btn, 0, wxALIGN_CENTRE|wxALL, 5)        

    sizer.AddSizer(box, 0, wxGROW|wxALIGN_CENTER_VERTICAL|wxALL, 5)
    self.SetSizer(sizer)
    self.SetAutoLayout(True)
    sizer.Fit(self)

    self.text = text

    EVT_BUTTON(self, ID_SAVE, self.OnSave)
#@+node:ekr.20051104081502.512: *8* OnSave
def OnSave(self, evt):

    path = os.path.join(self.dir, 'logfile.txt')

    f = file(path,'a')
    f.write(self.text.GetValue())
    f.close()

    dlg = wxMessageDialog(self,"Appended text to logfile.text", "Notice", wxICON_INFORMATION|wxOK)
    dlg.ShowModal()
    dlg.Destroy()

    self.text.Clear()
#@+node:ekr.20051104081502.513: *7* class FinishedDialog
class FinishedDialog(wxDialog):
    @others
#@+node:ekr.20051104081502.514: *8* __init__
def __init__(self, parent, title,
            pos=wxDefaultPosition,
            size=wxDefaultSize,
            style=wxCAPTION,
            days=0,
            spin_label="",
            check_label=""):

    wxDialog.__init__(self, parent, -1, title, pos, size)
    self.Centre()

    self.check = wxCheckBox(self, -1, check_label)

    if days == -1:
        self.check.SetValue(True)
        days = 0

    panel = wxPanel(self, -1, (-1,-1),(225,75))
    wxStaticText(panel, -1, spin_label,(15, 15))
    self.text = wxTextCtrl(panel, -1, str(days), (30, 50), (30, -1))
    h = self.text.GetSize().height
    self.spin = wxSpinButton(panel, -1, (56, 50), (h, h), wxSP_VERTICAL)
    wxStaticText(panel, -1, 'days',(76, 53))
    self.spin.SetRange(0, 14)
    self.spin.SetValue(days)

    H_sizer = wxBoxSizer(wxHORIZONTAL)

    line = wxStaticLine(self, -1, size = (20,-1), style = wxLI_HORIZONTAL)

    btn = wxButton(self, wxID_OK, "OK")
    H_sizer.Add(btn, 0, wxALIGN_CENTRE|wxALL, 5)
    btn.SetDefault()

    btn = wxButton(self, wxID_CANCEL, "CANCEL")
    H_sizer.Add(btn, 0, wxALIGN_CENTRE|wxALL, 5)

    V_sizer = wxBoxSizer(wxVERTICAL)
    V_sizer.Add(panel,1,wxALIGN_CENTER|wxEXPAND)
    V_sizer.Add(-1,5)
    V_sizer.Add(self.check,0,wxALIGN_LEFT|wxALL,5)
    V_sizer.Add(line,0, wxGROW|wxALIGN_CENTER_VERTICAL|wxTOP, 5)
    V_sizer.AddSizer(H_sizer, 0, wxGROW|wxALIGN_CENTER_VERTICAL|wxALL, 5)

    self.SetSizer(V_sizer)
    self.SetAutoLayout(True)
    V_sizer.Fit(self)

    EVT_SPIN(self, self.spin.GetId(), self.OnSpin)
    EVT_CHECKBOX(self, self.check.GetId(), self.OnCheck)

    if self.check.GetValue():
        self.spin.Enable(False)
        self.text.Enable(False)

    self.Layout() #doesn't appear necessary


#@+node:ekr.20051104081502.515: *8* OnSpin
def OnSpin(self, evt):
    self.text.SetValue(str(evt.GetPosition()))
#@+node:ekr.20051104081502.516: *8* OnCheck
def OnCheck(self, evt=None):
    if self.check.GetValue():
        self.spin.Enable(False)
        self.text.Enable(False)
    else:
        self.spin.Enable(True)
        self.text.Enable(True)
#@+node:ekr.20051104081502.517: *7* class TreeDialog
class TreeDialog(wxDialog):
    @others
#@+node:ekr.20051104081502.518: *8* __init__
def __init__(self, parent, caption, pos=wxDefaultPosition, size=(300,400), tree={}):
    wxDialog.__init__(self, parent, -1, caption, pos, size, style=wxSTAY_ON_TOP|wxCAPTION)

    TreeCtrl = wxTreeCtrl(self, -1, wxDefaultPosition, (300,400), wxTR_HAS_BUTTONS)    #|wxTR_HIDE_ROOT)#wxDefaultSize,

    sizer = wxBoxSizer(wxVERTICAL)
    sizer.Add(TreeCtrl, 1, wxALIGN_CENTER|wxALL, 5)

    box = wxBoxSizer(wxHORIZONTAL)
    btn = wxButton(self, wxID_OK, "OK")
    box.Add(btn, 0, wxALIGN_CENTRE|wxALL, 5)
    btn.SetDefault()

    btn = wxButton(self, wxID_CANCEL, "CANCEL")
    box.Add(btn, 0, wxALIGN_CENTRE|wxALL, 5)

    sizer.AddSizer(box)
    self.SetAutoLayout(1)
    self.SetSizer(sizer)

    il = wxImageList(16,16)

    fldridx = il.Add(wxBitmap('bitmaps\\folder.bmp'))
    fldropenidx = il.Add(wxBitmap('bitmaps\\folder_open.bmp'))
    listidx =  il.Add(wxBitmap('bitmaps\\list.bmp'))

    TreeCtrl.SetImageList(il)

    root = TreeCtrl.AddRoot("List Manager")
    TreeCtrl.SetItemImage(root, fldridx, wxTreeItemIcon_Normal)
    TreeCtrl.SetItemImage(root, fldropenidx, wxTreeItemIcon_Expanded)

    for host in tree:
        child = TreeCtrl.AppendItem(root, host)
        TreeCtrl.SetItemImage(child, fldridx, wxTreeItemIcon_Normal)
        TreeCtrl.SetItemImage(child, fldropenidx, wxTreeItemIcon_Expanded)
        for listname in tree[host]:
            last = TreeCtrl.AppendItem(child, listname)
            TreeCtrl.SetItemImage(last, listidx, wxTreeItemIcon_Normal)
            TreeCtrl.SetItemImage(last, listidx, wxTreeItemIcon_Selected)

    TreeCtrl.Expand(root)

    self.TreeCtrl= TreeCtrl
    self.il = il #? prevents GC

    EVT_LEFT_DCLICK(TreeCtrl, self.OnLeftDClick)
#@+node:ekr.20051104081502.519: *8* OnLeftDClick:
def OnLeftDClick(self, event=None):
    self.EndModal(wxID_OK)
#@+node:ekr.20051104081502.520: *6* outlookAddin.py
@ @rst-options
code_mode = True
@c

@language python
<< outlookAddin declarations >>
@others

if __name__ == '__main__':
    import win32com.server.register
    win32com.server.register.UseCommandLine(OutlookAddin)
    if "--unregister" in sys.argv:
        UnregisterAddin(OutlookAddin)
    else:
        RegisterAddin(OutlookAddin)
#@+node:ekr.20051104081502.521: *7* << outlookAddin declarations >>
# This is mainly stolen from Mark Hammond's demo plugin for win32com.client
# A demo plugin for Microsoft Outlook (NOT Outlook Express)

from win32com import universal
from win32com.server.exception import COMException
from win32com.client import gencache, DispatchWithEvents
from win32com.client import Dispatch
import winerror
import pythoncom
from win32com.client import constants
import win32ui ##
import sys
from socket import *
import pickle

# Support for COM objects we use.
#sz comment gencache.EnsureModule makes sure you are using makepy if the makepy-derived
#file doesn't already exist
#but as long as you did run makepy then you should just be alble to do a normal dispatch

mod = gencache.EnsureModule('{00062FFF-0000-0000-C000-000000000046}', 0, 9, 0, bForDemand=True) # Outlook 9
gencache.EnsureModule('{2DF8D04C-5BFA-101B-BDE5-00AA0044DE52}', 0, 2, 1, bForDemand=True) # Office 9

# The TLB defining the interfaces we implement
universal.RegisterInterfaces('{AC0714F2-3D04-11D1-AE7D-00A0C90F26F4}', 0, 1, 0, ["_IDTExtensibility2"])

Target = 'mail_transfer'


#@+node:ekr.20051104081502.522: *7* class ButtonEvent
class ButtonEvent:
    @others
#@+node:ekr.20051104081502.523: *8* OnClick
def OnClick(self, button, cancel):
    #activeExplorer and MailTransferFolder are globals defined in OnConnection
    sel = activeExplorer.Selection

    for i in range(1,sel.Count+1):
        item = sel.Item(i)
        item.Move(MailTransferFolder)

    return cancel

#@+node:ekr.20051104081502.524: *7* class FolderEvent
class FolderEvent:
    @others
#@+node:ekr.20051104081502.525: *8* OnItemAdd
def OnItemAdd(self, item):
    try:
        s = socket(AF_INET,SOCK_STREAM)
        s.connect(('localhost', 8888))
        d = {}
        d['Parent.Name'] = item.Parent.Name
        d['SenderName'] = item.SenderName
        d['Subject'] = item.Subject
        d['Body'] = item.Body[:5000]
        d['CreationTime'] = item.CreationTime.Format()
        str = pickle.dumps(d)
        s.send(str) # ?Receive no more than 1024 bytes
        s.close()
        win32ui.MessageBox("Sent %s to ListManager"%item.Subject)
    except:
        pass
#@+node:ekr.20051104081502.526: *7* class OutlookAddin
class OutlookAddin:
    << class OutlookAddin declarations >>
    @others
#@+node:ekr.20051104081502.527: *8* << class OutlookAddin declarations >>
_com_interfaces_ = ['_IDTExtensibility2']
_public_methods_ = []
_reg_clsctx_ = pythoncom.CLSCTX_INPROC_SERVER
_reg_clsid_ = "{0F47D9F3-598B-4d24-B7E3-92AC15ED27E2}"
_reg_progid_ = "Python.Test.OutlookAddin"
_reg_policy_spec_ = "win32com.server.policy.EventHandlerPolicy"
#@+node:ekr.20051104081502.528: *8* OnConnection
def OnConnection(self, application, connectMode, addin, custom):
    global MailTransferFolder
    global activeExplorer
    # ActiveExplorer may be none when started without a UI (eg, WinCE synchronisation)
    activeExplorer = application.ActiveExplorer()
    if activeExplorer:
        bars = activeExplorer.CommandBars
        toolbar = bars.Item("Standard")
        item = toolbar.Controls.Add(Type=constants.msoControlButton, Temporary=True)
        item = self.toolbarButton = DispatchWithEvents(item, ButtonEvent) #? just need this to be an ivar
        item.Caption="List Manager"
        item.TooltipText = "Click to move"
        item.Enabled = True
        #self.toolbarButton = DispatchWithEvents(item, ButtonEvent) #need something that won't get GC'd. Note Dispatch returns item

    ns = application.GetNamespace("MAPI")
    Folders = ns.Folders

    for i in range(1,len(Folders)+1):
        if Folders[i].Name.find("Mailbox") != -1:
            folders = Folders[i].Folders
            break
    else:
        win32ui.MessageBox("Can't find Mailbox!")
        return	

    for i in range(1,len(folders)+1):
        if folders[i].Name == Target:
            MailTransferFolder = folders[i]
            self.targetMailbox = DispatchWithEvents(folders[i].Items, FolderEvent) #? just need this to be an ivar
            win32ui.MessageBox("Enabled: %s\nOutlookAddin3"%Target)
            break
    else:
        win32ui.MessageBox("Could not find mail folder: %s\nOutlookAddin3"%Target)
#@+node:ekr.20051104081502.529: *8* OnDisconnection
def OnDisconnection(self, mode, custom):
    g.pr("OnDisconnection")
#@+node:ekr.20051104081502.530: *8* OnAddInsUpdate
def OnAddInsUpdate(self, custom):
    g.pr("OnAddInsUpdate", custom)
#@+node:ekr.20051104081502.531: *8* OnStartupComplete
def OnStartupComplete(self, custom):
    g.pr("OnStartupComplete", custom)
#@+node:ekr.20051104081502.532: *8* OnBeginShutdown
def OnBeginShutdown(self, custom):
    g.pr("OnBeginShutdown", custom)
#@+node:ekr.20051104081502.533: *7* RegisterAddin
def RegisterAddin(klass):
    import _winreg
    key = _winreg.CreateKey(_winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Office\\Outlook\\Addins")
    subkey = _winreg.CreateKey(key, klass._reg_progid_)
    _winreg.SetValueEx(subkey, "CommandLineSafe", 0, _winreg.REG_DWORD, 0)
    _winreg.SetValueEx(subkey, "LoadBehavior", 0, _winreg.REG_DWORD, 3)
    _winreg.SetValueEx(subkey, "Description", 0, _winreg.REG_SZ, klass._reg_progid_)
    _winreg.SetValueEx(subkey, "FriendlyName", 0, _winreg.REG_SZ, klass._reg_progid_)
#@+node:ekr.20051104081502.534: *7* UnregisterAddin
def UnregisterAddin(klass):
    import _winreg
    try:
        _winreg.DeleteKey(_winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Office\\Outlook\\Addins\\" + klass._reg_progid_)
    except WindowsError:
        pass
#@+node:ekr.20051104081502.535: *5* @rst ../doc/pdfTest.pdf
########
Headline
########

@ @rst-options
.. These options have NO EFFECT for rst2 plugin!
code_mode=False
generate_rst=True
http_server_support = False
show_organizer_nodes=True
show_headlines=True
show_leo_directives=True
stylesheet_path=c:\prog\leoCVS\leo\doc
write_intermediate_file = True
verbose=True
@c

This is a test of pdf stuff

.. contents::
#@+node:ekr.20051104081502.536: *6* child node
#@+node:ekr.20051104081502.537: *7* @rst
child node text
#@+node:ekr.20051104081502.546: *4* Tests of settings when opened from another .leo file
# g.app.config.updateSettings(c)
g.es('test_setting',c.config.getBool('test_setting'))
#@+node:ekr.20051104081502.159: *4* Tk bindtags test
import Tkinter as Tk

root = Tk.Tk()
c = Tk.Canvas(root,background='white')
g.pr(c.bindtags())

if 0:
    c.pack(expand=1,fill='both')
    f = Tk.Frame(c)
    c.create_window(0,0,window=f,anchor='nw')
    f.pack_configure(fill='both',expand=1)
    body = olCreateControl(self,frame,f)
    c.on = False 
    sel = lambda event, c = c, body = body:select(event,c,body)
    ai = lambda event, c = c, body = body, colorizer = frame.body:add_item(event,c,body,colorizer.getColorizer())
    c.bind("<Key>",watcher,'+')
    c.bind("<Key>",sel,'+')
    c.bind("<Key>",ai,'+')
    ctags = c.bindtags()
    btags = body.bindtags()
    btags =(ctags[0],btags[0],btags[1],btags[2],btags[3])
    body.bindtags(btags)
#@+node:ekr.20051104081502.305: *4* Unicode stuff
stuff = g.toEncodedString(u'∑','utf-8')
g.pr(type(stuff))
g.pr('*' * 10)
for ch in stuff:
    g.pr(ch, ord(ch),newline=False)
g.pr()
#@+node:ekr.20051104081502.549: *4* Write to log tab
c.frame.log.selectTab('Log')
g.es('Test',color='blue')
#@+node:ekr.20051104081502.548: *4* Write to test tab
c.frame.log.selectTab('Test')
g.es('Test',color='red',tabName='Test')
#@+node:ekr.20100125180231.5120: *3* Print a unicode character
@first # -*- coding: utf-8 -*-

s = g.ue('炰','utf-8')
g.es(s)
g.pr(s)
s = '炰'
g.pr(s)
#@+node:ekr.20051104081502.108: *3* runProfile button mini-test
for i in range(10000):
    if i and (i % 1000) == 0:
        g.pr(i)
#@+node:ekr.20051104081502.109: *3* runTimeit mini-test
i = 0
for i in range(100000):
    i += 1
    i -= 1
#@+node:ekr.20080821123427.2: *3* Standard print test
@first # -*- coding: utf-8 -*-

import sys

print('=' * 40)

e = sys.getdefaultencoding()
print('encoding',e)

table = (
    'La Peña',
    g.u('La Peña'),
    # u'La Peña',
    g.u('La Pe\xf1a')
)

for s in table:
    print(type(s))
    g.es_print('g.es_print',s)
    if type(s) != type(u'a'):
        s = unicode(s,e)
    print('print     ',s)
    print('repr(s)   ',repr(s))
#@+node:ekr.20051104081502.160: *3* User Icon tests
#@+node:ekr.20051104081502.161: *4* Delete user icons
for p in c.allNodes_iter():

    if hasattr(p.v.t,"unknownAttributes"):
        a = p.v.t.unknownAttributes
        iconsList = a.get("icons")
        if dict:
            a["icons"] = []
            a["lineYOffset"] = 0

c.redraw()
#@+node:ekr.20051104081502.162: *4* Test of user icons
p.v.t.unknownAttributes = {}
a = p.v.t.unknownAttributes

<< define event callbacks >>

path = g.os_path_join(g.app.loadDir,"..","Icons")
icon1 = g.os_path_join(path,"lt_arrow_enabled.gif")
icon2 = g.os_path_join(path,"rt_arrow_enabled.gif")

d1 = {
    "type" : "file", "file" : icon1,
    "where" : "beforeIcon",
    "yoffset" : -3,
    # "yoffset" : 5, "ypad" : -5,
    # "height" : 40, # automatically adjust headline y position.
    "xpad": 2
}

# Classes and functions can only be pickled if they are at the top level of a module.
    #"onClick" : onClick,
    #"onRightClick" : onRightClick,
    #"onDoubleClick" : onDoubleClick }

d2 = {
    "type" : "file", "file" : icon2,
    "where" : "beforeHeadline",
    "yoffset" : -3,
    "xoffset" : 2, "xpad" : -2 }

a["icons"] = [d1,d2] # [d1,d2]
a["lineYOffset"] = 3

c.redraw()
#@+node:ekr.20051104081502.163: *5* << define event callbacks >>
def onClick(p=p):

    g.trace(p)

def onRightClick(p=p):

    g.trace(p)

def onDoubleClick(p=p):

    g.trace(p)
#@+node:ekr.20140906072925.5224: ** @ignore Scintilla fails
#@+node:ekr.20100223123103.5382: *3* @test expand/contract-pane
import leo.core.leoFrame as leoFrame

# Do nothing when run externally.
if g.app.isExternalUnitTest:
    pass
else:
    assert not isinstance(c.frame,leoFrame.NullFrame)
    def closeEnough(f1,f2):
        return abs(f1-f2) < 0.0001
    f = c.frame
    ratio,ratio2 = f.ratio,f.secondary_ratio
    table = (
        c.bodyWantsFocusNow,
        c.logWantsFocusNow,
        c.treeWantsFocusNow,
    )
    for func in table:
        func()
        f.contractPane()
        if func == c.logWantsFocusNow:
            assert ratio2 != f.secondary_ratio,'fail 1'
        else:
            assert ratio != f.ratio,'fail 2: %s, %s' % (ratio,f.ratio)
        func()
        f.expandPane()
        assert closeEnough(ratio,f.ratio),'fail 3 %s != %s' % (
            ratio,f.ratio)
        assert closeEnough(ratio2,f.secondary_ratio),'fail 4 %s != %s' % (
            ratio2,f.secondary_ratio)
#@+node:ekr.20111112171235.3854: *3* @test add/delete html comments
w = c.frame.body.wrapper
p = g.findNodeInTree(c,p,'html')
assert p,'no test node'
s = p.b
indent = c.config.getBool('indent_added_comments',default=True)
try:
    i = p.b.find('text')
    assert i > -1,'fail1: %s' % (repr(p.b))
    c.selectPosition(p)
    w.setSelectionRange(i,i+4)
    c.addComments()
    if indent:
        i = p.b.find('<!-- text')
    else:
        i = p.b.find('<!--     text')
    assert i > -1,'fail2: %s' % (repr(p.b))
    c.deleteComments()
    assert p.b == s,'fail3: s\n%s\nresult\n%s' % (repr(s),repr(p.b))
    # Add a comment delim without a blank.
    c.addComments()
    p.b = p.b.replace('<!-- ','<!--')
    i = p.b.find('<!--')
    w.setSelectionRange(i,i+4)
    c.deleteComments()
    assert p.b == s,'fail5: s\n%s\nresult\n%s' % (repr(s),repr(p.b))
finally:
    # print('\n'.join([repr(z) for z in g.splitLines(p.b)]))
    p.b = s
#@+node:ekr.20111112171235.3855: *4* html
@language html
<html>
    text 
</html>
#@+node:ekr.20111112171235.3858: *3* @test add/delete python comments
# Can't be run externally.
w = c.frame.body.wrapper
p = g.findNodeInTree(c,p,'python')
assert p,'no test node'
s = p.b
indent = c.config.getBool('indent_added_comments',default=True)

try:
    i = p.b.find('pass')
    assert i > -1,'fail1: %s' % (repr(p.b))
    c.selectPosition(p)
    w.setSelectionRange(i,i+4)
    c.addComments()
    if indent:
        i = p.b.find('# pass')
    else:
        i = p.b.find('#     pass')
    assert i > -1,'fail2: %s' % (repr(p.b))
    c.deleteComments()
    assert p.b == s,'fail3: %s' % (repr(p.b))
    # Add a comment delim without a blank.
    c.addComments()
    p.b = p.b.replace('# pass','#pass')
    i = p.b.find('#')
    w.setSelectionRange(i,i+4)
    c.deleteComments()
    assert p.b == s,'fail5: s\n%s\nresult\n%s' % (repr(s),repr(p.b))
finally:
    # print('\n'.join([repr(z) for z in g.splitLines(p.b)]))
    p.b = s
#@+node:ekr.20111112171235.3859: *4* python
@language python

def spam():
    pass

# after
#@+node:ekr.20061101121602.126: *3* @test backward-kill-paragraph
c.testManager.runEditCommandTest(p)
#@+node:ekr.20061101121602.127: *4* work
Americans live in the most severe weather-prone country on Earth. Each year,
Americans cope with an average of 10,000 thunderstorms, 2,500 floods, 1,000
tornadoes, as well as an average of 6 deadly hurricanes. Potentially deadly
weather impacts every American. Communities can now rely on the National Weather
Service’s StormReady program to help them guard against the ravages of Mother
Nature.
to around 500 deaths per year and nearly $14 billion in damage. StormReady, a
program started in 1999 in Tulsa, OK, helps arm America's communities with the
communication and safety skills needed to save lives and property– before and
during the event. StormReady helps community leaders and emergency managers
strengthen local safety programs.

StormReady communities are better prepared to save lives from the onslaught of
severe weather through better planning, education, and awareness. No community
is storm proof, but StormReady can help communities save lives. Does StormReady
make a difference?
#@+node:ekr.20061101121602.128: *4* before sel=9.0,9.0
Americans live in the most severe weather-prone country on Earth. Each year,
Americans cope with an average of 10,000 thunderstorms, 2,500 floods, 1,000
tornadoes, as well as an average of 6 deadly hurricanes. Potentially deadly
weather impacts every American. Communities can now rely on the National Weather
Service’s StormReady program to help them guard against the ravages of Mother
Nature.

Some 90% of all presidentially declared disasters are weather related, leading
to around 500 deaths per year and nearly $14 billion in damage. StormReady, a
program started in 1999 in Tulsa, OK, helps arm America's communities with the
communication and safety skills needed to save lives and property– before and
during the event. StormReady helps community leaders and emergency managers
strengthen local safety programs.

StormReady communities are better prepared to save lives from the onslaught of
severe weather through better planning, education, and awareness. No community
is storm proof, but StormReady can help communities save lives. Does StormReady
make a difference?
#@+node:ekr.20061101121602.129: *4* after sel=7.0,7.0
Americans live in the most severe weather-prone country on Earth. Each year,
Americans cope with an average of 10,000 thunderstorms, 2,500 floods, 1,000
tornadoes, as well as an average of 6 deadly hurricanes. Potentially deadly
weather impacts every American. Communities can now rely on the National Weather
Service’s StormReady program to help them guard against the ravages of Mother
Nature.
to around 500 deaths per year and nearly $14 billion in damage. StormReady, a
program started in 1999 in Tulsa, OK, helps arm America's communities with the
communication and safety skills needed to save lives and property– before and
during the event. StormReady helps community leaders and emergency managers
strengthen local safety programs.

StormReady communities are better prepared to save lives from the onslaught of
severe weather through better planning, education, and awareness. No community
is storm proof, but StormReady can help communities save lives. Does StormReady
make a difference?
#@+node:ekr.20061101121602.154: *3* @test center-region
@pagewidth 70 # Required for unit test.

c.testManager.runEditCommandTest(p)
#@+node:ekr.20061101121602.155: *4* work
Some 90% of all presidentially declared disasters are weather related,
leading to around 500 deaths per year and nearly $14 billion in damage.
         StormReady, a program started in 1999 in Tulsa, OK,
  helps arm America's communities with the communication and safety
skills needed to save lives and property– before and during the event.
StormReady helps community leaders and emergency managers strengthen local safety programs.
#@+node:ekr.20061101121602.156: *4* before sel=1.0,7.0
Some 90% of all presidentially declared disasters are weather related,
leading to around 500 deaths per year and nearly $14 billion in damage.
StormReady, a program started in 1999 in Tulsa, OK,
helps arm America's communities with the communication and safety
skills needed to save lives and property– before and during the event.
StormReady helps community leaders and emergency managers strengthen local safety programs.
#@+node:ekr.20061101121602.157: *4* after sel=1.0,7.0
Some 90% of all presidentially declared disasters are weather related,
leading to around 500 deaths per year and nearly $14 billion in damage.
         StormReady, a program started in 1999 in Tulsa, OK,
  helps arm America's communities with the communication and safety
skills needed to save lives and property– before and during the event.
StormReady helps community leaders and emergency managers strengthen local safety programs.
#@+node:ekr.20061101121602.174: *3* @test downcase-region
c.testManager.runEditCommandTest(p)
assert g.app.unitTestDict.get('colorized')
#@+node:ekr.20061101121602.175: *4* work
Americans live in the most severe weather-prone country on Earth. Each year, Americans cope with an average of 10,000 thunderstorms, 2,500 floods, 1,000 tornadoes, as well as an average of 6 deadly hurricanes. Potentially deadly weather impacts every American. Communities can now rely on the National Weather Service’s StormReady program to help them guard against the ravages of Mother Nature.

some 90% of all presidentially declared disasters are weather related, leading to around 500 deaths per year and nearly $14 billion in damage. stormready, a program started in 1999 in tulsa, ok, helps arm america's communities with the communication and safety skills needed to save lives and property– before and during the event. stormready helps community leaders and emergency managers strengthen local safety programs.

StormReady communities are better prepared to save lives from the onslaught of severe weather through better planning, education, and awareness. No community is storm proof, but StormReady can help communities save lives. Does StormReady make a difference?
#@+node:ekr.20061101121602.176: *4* before sel=3.0,4.0
Americans live in the most severe weather-prone country on Earth. Each year, Americans cope with an average of 10,000 thunderstorms, 2,500 floods, 1,000 tornadoes, as well as an average of 6 deadly hurricanes. Potentially deadly weather impacts every American. Communities can now rely on the National Weather Service’s StormReady program to help them guard against the ravages of Mother Nature.

Some 90% of all presidentially declared disasters are weather related, leading to around 500 deaths per year and nearly $14 billion in damage. StormReady, a program started in 1999 in Tulsa, OK, helps arm America's communities with the communication and safety skills needed to save lives and property– before and during the event. StormReady helps community leaders and emergency managers strengthen local safety programs.

StormReady communities are better prepared to save lives from the onslaught of severe weather through better planning, education, and awareness. No community is storm proof, but StormReady can help communities save lives. Does StormReady make a difference?
#@+node:ekr.20061101121602.177: *4* after sel=3.0,4.0
Americans live in the most severe weather-prone country on Earth. Each year, Americans cope with an average of 10,000 thunderstorms, 2,500 floods, 1,000 tornadoes, as well as an average of 6 deadly hurricanes. Potentially deadly weather impacts every American. Communities can now rely on the National Weather Service’s StormReady program to help them guard against the ravages of Mother Nature.

some 90% of all presidentially declared disasters are weather related, leading to around 500 deaths per year and nearly $14 billion in damage. stormready, a program started in 1999 in tulsa, ok, helps arm america's communities with the communication and safety skills needed to save lives and property– before and during the event. stormready helps community leaders and emergency managers strengthen local safety programs.

StormReady communities are better prepared to save lives from the onslaught of severe weather through better planning, education, and awareness. No community is storm proof, but StormReady can help communities save lives. Does StormReady make a difference?
#@+node:ekr.20061101121602.282: *3* @test kill-paragraph
c.testManager.runEditCommandTest(p)
#@+node:ekr.20061101121602.283: *4* work
Americans live in the most severe weather-prone country on Earth. Each year,
Americans cope with an average of 10,000 thunderstorms, 2,500 floods, 1,000
tornadoes, as well as an average of 6 deadly hurricanes. Potentially deadly
weather impacts every American. Communities can now rely on the National Weather
Service’s StormReady program to help them guard against the ravages of Mother
Nature.



StormReady communities are better prepared to save lives from the onslaught of
severe weather through better planning, education, and awareness. No community
is storm proof, but StormReady can help communities save lives. Does StormReady
make a difference?
#@+node:ekr.20061101121602.285: *4* before sel=9.0,9.0
Americans live in the most severe weather-prone country on Earth. Each year,
Americans cope with an average of 10,000 thunderstorms, 2,500 floods, 1,000
tornadoes, as well as an average of 6 deadly hurricanes. Potentially deadly
weather impacts every American. Communities can now rely on the National Weather
Service’s StormReady program to help them guard against the ravages of Mother
Nature.

Some 90% of all presidentially declared disasters are weather related, leading
to around 500 deaths per year and nearly $14 billion in damage. StormReady, a
program started in 1999 in Tulsa, OK, helps arm America's communities with the
communication and safety skills needed to save lives and property– before and
during the event. StormReady helps community leaders and emergency managers
strengthen local safety programs.

StormReady communities are better prepared to save lives from the onslaught of
severe weather through better planning, education, and awareness. No community
is storm proof, but StormReady can help communities save lives. Does StormReady
make a difference?
#@+node:ekr.20061101121602.284: *4* after sel=8.0,8.0
Americans live in the most severe weather-prone country on Earth. Each year,
Americans cope with an average of 10,000 thunderstorms, 2,500 floods, 1,000
tornadoes, as well as an average of 6 deadly hurricanes. Potentially deadly
weather impacts every American. Communities can now rely on the National Weather
Service’s StormReady program to help them guard against the ravages of Mother
Nature.



StormReady communities are better prepared to save lives from the onslaught of
severe weather through better planning, education, and awareness. No community
is storm proof, but StormReady can help communities save lives. Does StormReady
make a difference?
#@+node:ekr.20071113145804.4: *3* @test selfInsertCommand-1
@first # -*- coding: utf-8 -*-
@language python

try:
    ec = c.editCommands ; w = c.frame.body.wrapper
    s = w.getAllText()

    # This strings tests unicode, paren matching, and auto-indentation.
    u = g.u('(a\u00c9\u03a9B\u3045\u4e7cz):\n') # '(aÉ©BE|cz):\n'
    u = g.u('(pdq):\n')
    w.setInsertPoint(len(s))
    for char in u:
        stroke = g.choose(char=='\n','Return',char)
        event = g.app.gui.create_key_event(c,char,stroke,w)
        ec.selfInsertCommand(event)
    result = w.getAllText()
    #g.trace('result',repr(result))
    assert result.endswith('    '),'result:\n%s' % result
    # Test of autocompleter.
finally:
    if 1:
        w.setAllText(s)
        p.setBodyString(s)
        # g.trace(repr(s))
        c.recolor()

# end:
#@+node:ekr.20061101121602.350: *3* @test upcase-region
c.testManager.runEditCommandTest(p)
assert g.app.unitTestDict.get('colorized')
#@+node:ekr.20061101121602.351: *4* work
Americans live in the most severe weather-prone country on Earth. Each year, Americans cope with an average of 10,000 thunderstorms, 2,500 floods, 1,000 tornadoes, as well as an average of 6 deadly hurricanes. Potentially deadly weather impacts every American. Communities can now rely on the National Weather Service’s StormReady program to help them guard against the ravages of Mother Nature.

SOME 90% OF ALL PRESIDENTIALLY DECLARED DISASTERS ARE WEATHER RELATED, LEADING TO AROUND 500 DEATHS PER YEAR AND NEARLY $14 BILLION IN DAMAGE. STORMREADY, A PROGRAM STARTED IN 1999 IN TULSA, OK, HELPS ARM AMERICA'S COMMUNITIES WITH THE COMMUNICATION AND SAFETY SKILLS NEEDED TO SAVE LIVES AND PROPERTY– BEFORE AND DURING THE EVENT. STORMREADY HELPS COMMUNITY LEADERS AND EMERGENCY MANAGERS STRENGTHEN LOCAL SAFETY PROGRAMS.

StormReady communities are better prepared to save lives from the onslaught of severe weather through better planning, education, and awareness. No community is storm proof, but StormReady can help communities save lives. Does StormReady make a difference?
#@+node:ekr.20061101121602.352: *4* before sel=3.0,4.0
Americans live in the most severe weather-prone country on Earth. Each year, Americans cope with an average of 10,000 thunderstorms, 2,500 floods, 1,000 tornadoes, as well as an average of 6 deadly hurricanes. Potentially deadly weather impacts every American. Communities can now rely on the National Weather Service’s StormReady program to help them guard against the ravages of Mother Nature.

Some 90% of all presidentially declared disasters are weather related, leading to around 500 deaths per year and nearly $14 billion in damage. StormReady, a program started in 1999 in Tulsa, OK, helps arm America's communities with the communication and safety skills needed to save lives and property– before and during the event. StormReady helps community leaders and emergency managers strengthen local safety programs.

StormReady communities are better prepared to save lives from the onslaught of severe weather through better planning, education, and awareness. No community is storm proof, but StormReady can help communities save lives. Does StormReady make a difference?
#@+node:ekr.20061101121602.353: *4* after sel=3.0,4.0
Americans live in the most severe weather-prone country on Earth. Each year, Americans cope with an average of 10,000 thunderstorms, 2,500 floods, 1,000 tornadoes, as well as an average of 6 deadly hurricanes. Potentially deadly weather impacts every American. Communities can now rely on the National Weather Service’s StormReady program to help them guard against the ravages of Mother Nature.

SOME 90% OF ALL PRESIDENTIALLY DECLARED DISASTERS ARE WEATHER RELATED, LEADING TO AROUND 500 DEATHS PER YEAR AND NEARLY $14 BILLION IN DAMAGE. STORMREADY, A PROGRAM STARTED IN 1999 IN TULSA, OK, HELPS ARM AMERICA'S COMMUNITIES WITH THE COMMUNICATION AND SAFETY SKILLS NEEDED TO SAVE LIVES AND PROPERTY– BEFORE AND DURING THE EVENT. STORMREADY HELPS COMMUNITY LEADERS AND EMERGENCY MANAGERS STRENGTHEN LOCAL SAFETY PROGRAMS.

StormReady communities are better prepared to save lives from the onslaught of severe weather through better planning, education, and awareness. No community is storm proof, but StormReady can help communities save lives. Does StormReady make a difference?
#@+node:ekr.20110118082508.3766: *3* @test reformat-paragraph paragraph 1 of 3
# Required when running tests externally
@language plain
@pagewidth 40
@tabwidth 8

c.testManager.runEditCommandTest(p)
#@+node:ekr.20110118082508.3772: *4* work
Americans live in the most severe
weather-prone country on Earth. Each
year, Americans cope with an average of
10,000 thunderstorms, 2,500 floods,
1,000 tornadoes, as well as an average
of 6 deadly hurricanes. Potentially
deadly weather impacts every American.
Communities can now rely on the National
Weather Service’s StormReady program to
help them guard against the ravages of
Mother Nature.

Some 90% of all presidentially declared disasters are weather related, leading to around 500 deaths per year and nearly $14 billion in damage. StormReady, a program started in 1999 in Tulsa, OK, helps arm America's communities with the communication and safety skills needed to save lives and property– before and during the event. StormReady helps community leaders and emergency managers strengthen local safety programs.

StormReady communities are better prepared to save lives from the onslaught of severe weather through better planning, education, and awareness. No community is storm proof, but StormReady can help communities save lives. Does StormReady make a difference?

Last paragraph.
#@+node:ekr.20110118082508.3773: *4* before sel=1.0,1.0
Americans live in the most severe weather-prone country on Earth. Each year, Americans cope with an average of 10,000 thunderstorms, 2,500 floods, 1,000 tornadoes, as well as an average of 6 deadly hurricanes. Potentially deadly weather impacts every American. Communities can now rely on the National Weather Service’s StormReady program to help them guard against the ravages of Mother Nature.

Some 90% of all presidentially declared disasters are weather related, leading to around 500 deaths per year and nearly $14 billion in damage. StormReady, a program started in 1999 in Tulsa, OK, helps arm America's communities with the communication and safety skills needed to save lives and property– before and during the event. StormReady helps community leaders and emergency managers strengthen local safety programs.

StormReady communities are better prepared to save lives from the onslaught of severe weather through better planning, education, and awareness. No community is storm proof, but StormReady can help communities save lives. Does StormReady make a difference?

Last paragraph.
#@+node:ekr.20110118082508.3792: *4* after sel=11.14,11.14
Americans live in the most severe
weather-prone country on Earth. Each
year, Americans cope with an average of
10,000 thunderstorms, 2,500 floods,
1,000 tornadoes, as well as an average
of 6 deadly hurricanes. Potentially
deadly weather impacts every American.
Communities can now rely on the National
Weather Service’s StormReady program to
help them guard against the ravages of
Mother Nature.

Some 90% of all presidentially declared disasters are weather related, leading to around 500 deaths per year and nearly $14 billion in damage. StormReady, a program started in 1999 in Tulsa, OK, helps arm America's communities with the communication and safety skills needed to save lives and property– before and during the event. StormReady helps community leaders and emergency managers strengthen local safety programs.

StormReady communities are better prepared to save lives from the onslaught of severe weather through better planning, education, and awareness. No community is storm proof, but StormReady can help communities save lives. Does StormReady make a difference?

Last paragraph.
#@+node:ekr.20110118082508.3779: *3* @test reformat-paragraph paragraph 2 of 3
# Required when running tests externally
@language plain
@pagewidth 40
@tabwidth 8

c.testManager.runEditCommandTest(p)
#@+node:ekr.20110118082508.3780: *4* work
Americans live in the most severe
weather-prone country on Earth. Each
year, Americans cope with an average of
10,000 thunderstorms, 2,500 floods,
1,000 tornadoes, as well as an average
of 6 deadly hurricanes. Potentially
deadly weather impacts every American.
Communities can now rely on the National
Weather Service’s StormReady program to
help them guard against the ravages of
Mother Nature.

Some 90% of all presidentially declared
disasters are weather related, leading
to around 500 deaths per year and nearly
$14 billion in damage. StormReady, a
program started in 1999 in Tulsa, OK,
helps arm America's communities with the
communication and safety skills needed
to save lives and property– before and
during the event. StormReady helps
community leaders and emergency managers
strengthen local safety programs.

StormReady communities are better prepared to save lives from the onslaught of severe weather through better planning, education, and awareness. No community is storm proof, but StormReady can help communities save lives. Does StormReady make a difference?

Last paragraph.
#@+node:ekr.20110118082508.3781: *4* before sel=13.0,13.0
Americans live in the most severe
weather-prone country on Earth. Each
year, Americans cope with an average of
10,000 thunderstorms, 2,500 floods,
1,000 tornadoes, as well as an average
of 6 deadly hurricanes. Potentially
deadly weather impacts every American.
Communities can now rely on the National
Weather Service’s StormReady program to
help them guard against the ravages of
Mother Nature.

Some 90% of all presidentially declared disasters are weather related, leading to around 500 deaths per year and nearly $14 billion in damage. StormReady, a program started in 1999 in Tulsa, OK, helps arm America's communities with the communication and safety skills needed to save lives and property– before and during the event. StormReady helps community leaders and emergency managers strengthen local safety programs.

StormReady communities are better prepared to save lives from the onslaught of severe weather through better planning, education, and awareness. No community is storm proof, but StormReady can help communities save lives. Does StormReady make a difference?

Last paragraph.
#@+node:ekr.20110118082508.3782: *4* after sel=23.33,23.33
Americans live in the most severe
weather-prone country on Earth. Each
year, Americans cope with an average of
10,000 thunderstorms, 2,500 floods,
1,000 tornadoes, as well as an average
of 6 deadly hurricanes. Potentially
deadly weather impacts every American.
Communities can now rely on the National
Weather Service’s StormReady program to
help them guard against the ravages of
Mother Nature.

Some 90% of all presidentially declared
disasters are weather related, leading
to around 500 deaths per year and nearly
$14 billion in damage. StormReady, a
program started in 1999 in Tulsa, OK,
helps arm America's communities with the
communication and safety skills needed
to save lives and property– before and
during the event. StormReady helps
community leaders and emergency managers
strengthen local safety programs.

StormReady communities are better prepared to save lives from the onslaught of severe weather through better planning, education, and awareness. No community is storm proof, but StormReady can help communities save lives. Does StormReady make a difference?

Last paragraph.
#@+node:ekr.20110118082508.3787: *3* @test reformat-paragraph paragraph 3 of 3
# Required when running tests externally
@language plain
@pagewidth 40
@tabwidth 8

c.testManager.runEditCommandTest(p)
#@+node:ekr.20110118082508.3788: *4* work
Americans live in the most severe
weather-prone country on Earth. Each
year, Americans cope with an average of
10,000 thunderstorms, 2,500 floods,
1,000 tornadoes, as well as an average
of 6 deadly hurricanes. Potentially
deadly weather impacts every American.
Communities can now rely on the National
Weather Service’s StormReady program to
help them guard against the ravages of
Mother Nature.

Some 90% of all presidentially declared
disasters are weather related, leading
to around 500 deaths per year and nearly
$14 billion in damage. StormReady, a
program started in 1999 in Tulsa, OK,
helps arm America's communities with the
communication and safety skills needed
to save lives and property– before and
during the event. StormReady helps
community leaders and emergency managers
strengthen local safety programs.

StormReady communities are better
prepared to save lives from the
onslaught of severe weather through
better planning, education, and
awareness. No community is storm proof,
but StormReady can help communities save
lives. Does StormReady make a
difference?

Last paragraph.
#@+node:ekr.20110118082508.3789: *4* before sel=25.10,25.10
Americans live in the most severe
weather-prone country on Earth. Each
year, Americans cope with an average of
10,000 thunderstorms, 2,500 floods,
1,000 tornadoes, as well as an average
of 6 deadly hurricanes. Potentially
deadly weather impacts every American.
Communities can now rely on the National
Weather Service’s StormReady program to
help them guard against the ravages of
Mother Nature.

Some 90% of all presidentially declared
disasters are weather related, leading
to around 500 deaths per year and nearly
$14 billion in damage. StormReady, a
program started in 1999 in Tulsa, OK,
helps arm America's communities with the
communication and safety skills needed
to save lives and property– before and
during the event. StormReady helps
community leaders and emergency managers
strengthen local safety programs.

StormReady communities are better prepared to save lives from the onslaught of severe weather through better planning, education, and awareness. No community is storm proof, but StormReady can help communities save lives. Does StormReady make a difference?

Last paragraph.
#@+node:ekr.20110118082508.3790: *4* after sel=32.11,32.11
Americans live in the most severe
weather-prone country on Earth. Each
year, Americans cope with an average of
10,000 thunderstorms, 2,500 floods,
1,000 tornadoes, as well as an average
of 6 deadly hurricanes. Potentially
deadly weather impacts every American.
Communities can now rely on the National
Weather Service’s StormReady program to
help them guard against the ravages of
Mother Nature.

Some 90% of all presidentially declared
disasters are weather related, leading
to around 500 deaths per year and nearly
$14 billion in damage. StormReady, a
program started in 1999 in Tulsa, OK,
helps arm America's communities with the
communication and safety skills needed
to save lives and property– before and
during the event. StormReady helps
community leaders and emergency managers
strengthen local safety programs.

StormReady communities are better
prepared to save lives from the
onslaught of severe weather through
better planning, education, and
awareness. No community is storm proof,
but StormReady can help communities save
lives. Does StormReady make a
difference?

Last paragraph.
#@+node:ekr.20100204165850.5373: *3* @test most toggle commands
k = c.k
colorizer = c.frame.body.getColorizer()
ed = c.editCommands

# These don't set ivars
    # 'toggle-active-pane'),
    # 'toggle-angle-brackets',
    # 'toggle-input-state'),
    # 'toggle-mini-buffer'),
    # 'toggle-split-direction'),

table = [
    (k,'abbrevOn','toggle-abbrev-mode'),
    (ed,'extendMode','toggle-extend-mode'),
]

# Not valid for external tests.
table2 = [
    (k,'enable_autocompleter','toggle-autocompleter'),
    (k,'enable_calltips','toggle-calltips'),
    (c,'sparse_find','toggle-find-collapses-nodes'),
    (colorizer,'showInvisibles','toggle-invisibles'),
    (c,'sparse_move','toggle-sparse-move'),
]

if not g.app.isExternalUnitTest:
    table.extend(table2)

for obj,ivar,command in table:
    val1 = getattr(obj,ivar)
    try:
        k.simulateCommand(command)
        val2 = getattr(obj,ivar)
        assert val2 == (not val1),'failed 1 %s' % command
        k.simulateCommand(command)
        val3 = getattr(obj,ivar)
        assert val3 == val1,'failed 2 %s' % command
        # print('pass',command)
    finally:
        setattr(obj,ivar,val1)
#@+node:ekr.20040712101754.37: *3* @suite Edit body tests
# Create unit tests in g.app.scriptDict["suite"]

suite = c.testManager.makeEditBodySuite(p)

# g.app.scriptDict['suite'] = suite
#@+node:ekr.20040712101754.38: *4* editBodyTests
@language plain
@

The names of child nodes are the names of commander methods to be called to do the test.

Each child node will in turn have two or more children:

- a "before" node
- an "after" node
- an optional selection node containing two lines giving the selection range in Tk coordinates.
- An optional insert node containing one line giving the insert point in Tk coordinates.
#@+node:ekr.20060127120604: *5* tempNode
#@+node:ekr.20050417202713: *5* addComments
#@+node:ekr.20050417202713.1: *6* before
@language python

def addCommentTest():

    if 1:
        a = 2
        b = 3

    pass
#@+node:ekr.20050417202713.2: *6* after
@language python

def addCommentTest():

    # if 1:
        # a = 2
        # b = 3

    pass
#@+node:ekr.20050417202713.3: *6* selection
5.0
7.8
#@+node:ekr.20050417204940: *5* convertAllBlanks
#@+node:ekr.20050417204940.1: *6* before
@tabwidth -4

line 1
    line 2
      line 3
line4
#@+node:ekr.20050417204940.2: *6* after
@tabwidth -4

line 1
	line 2
	  line 3
line4
#@+node:ekr.20050417204940.3: *6* selection
1.0
6.5
#@+node:ekr.20050417205012: *5* convertAllTabs
#@+node:ekr.20050417205012.1: *6* before
@tabwidth -4

line 1
	line 2
	  line 3
line4
#@+node:ekr.20050417205012.2: *6* after
@tabwidth -4

line 1
    line 2
      line 3
line4
#@+node:ekr.20050417205012.3: *6* selection
1.0
6.5
#@+node:ekr.20050417203114: *5* convertBlanks
#@+node:ekr.20050417203310: *6* before
@tabwidth -4

line 1
    line 2
      line 3
line4
#@+node:ekr.20050417203310.1: *6* after
@tabwidth -4

line 1
	line 2
	  line 3
line4
#@+node:ekr.20050417203336: *6* selection
1.0
6.5
#@+node:ekr.20050417203114.1: *5* convertTabs
#@+node:ekr.20050417204834: *6* before
@tabwidth -4

line 1
	line 2
	  line 3
line4
#@+node:ekr.20050417204830: *6* after
@tabwidth -4

line 1
    line 2
      line 3
line4
#@+node:ekr.20050417204901: *6* selection
1.0
6.5
#@+node:ekr.20040712101754.49: *5* dedentBody
#@+node:ekr.20040712101754.50: *6* before
line 1
    line 2
    line 3
line 4
#@+node:ekr.20040712101754.51: *6* after
line 1
line 2
line 3
line 4
#@+node:ekr.20040712101754.52: *6* selection
2.0
3.5
#@+node:ekr.20050417202817: *5* deleteComments
# created by new add-comments
#@+node:ekr.20050417202817.1: *6* before
@language python

def deleteCommentTest():

#     if 1:
#         a = 2
#         b = 3

    pass
#@+node:ekr.20050417202817.2: *6* after
@language python

def deleteCommentTest():

    if 1:
        a = 2
        b = 3

    pass
#@+node:ekr.20050417202817.3: *6* selection
5.0
7.8
#@+node:ekr.20111112211307.3910: *5* deleteComments
# created by old and new add-comments.
#@+node:ekr.20111112211307.3911: *6* before
@language python

def deleteCommentTest():

#     if 1:
#         a = 2
#         b = 3

    # if 1:
        # a = 2
        # b = 3

    pass
#@+node:ekr.20111112211307.3912: *6* after
@language python

def deleteCommentTest():

    if 1:
        a = 2
        b = 3

    if 1:
        a = 2
        b = 3

    pass
#@+node:ekr.20111112211307.3913: *6* selection
5.0
12.8
#@+node:ekr.20050417201845: *5* extract test1
#@+node:ekr.20050417201845.1: *6* before
before
    << section >>
    sec line 1
        sec line 2 indented
sec line 3
after
#@+node:ekr.20050417201845.2: *6* after
before
    << section >>
after
#@+node:ekr.20050417201845.3: *7* << section >> @nonl
sec line 1
    sec line 2 indented
sec line 3
#@+node:ekr.20050417201845.4: *6* selection
2.0
5.10
#@+node:ekr.20050518070540: *5* extract test2
#@+node:ekr.20050518070540.1: *6* before
before
    << section >>
    sec line 1
        sec line 2 indented
sec line 3
after
#@+node:ekr.20050518070545: *6* after
before
    << section >>
    sec line 1
        sec line 2 indented
sec line 3
after
#@+node:ekr.20050518070540.4: *6* selection
2.0
2.16
#@+node:ekr.20050518070927: *5* extractSection test1
#@+node:ekr.20050518070927.1: *6* before
before
    << section >>
    sec line 1
        sec line 2 indented
sec line 3
after
#@+node:ekr.20050518070927.2: *6* after
before
    << section >>
after
#@+node:ekr.20050518070927.3: *7* << section >> @nonl
sec line 1
    sec line 2 indented
sec line 3
#@+node:ekr.20050518070927.4: *6* selection
2.0
5.10
#@+node:ekr.20050518071251: *5* extractSection test2
#@+node:ekr.20050518071251.1: *6* before
before
    << section >>
    sec line 1
        sec line 2 indented
sec line 3
after
#@+node:ekr.20050518071258: *6* after
before
    << section >>
    sec line 1
        sec line 2 indented
sec line 3
after
#@+node:ekr.20050518071251.4: *6* selection
2.0
2.16
#@+node:ekr.20130503061511.4175: ** @ignore Unit tests for settings
#@+node:ekr.20130503061511.4176: *3* print dicts unit tests
#@+node:ekr.20130503061511.4177: *4* @test printMenusList
def printMenusList(aList,level=0):
    
    for z in aList:
        a,b,c = z
        print('*** kind',a)
        if type(b) in (type(()),type([])):
            for z2 in b:
                a1,b1,c1 = z2
                if a1.startswith('@menu') and type(b1) in (type(()),type([])):
                    print()
                    print('*** inner menu: %s' % (level+1))
                    print(a1)
                    for z3 in b1:
                        print(z3)
                    if c1: print(c1)
                else:
                    print(z2)
            if c: print(c)
        else:
            print(b)
        print()
        break #
        
printMenusList(c.config.getMenusList())
       
#@+node:ekr.20130503061511.4178: *4* @test printInverseBindingDict
print('\ninverseBindingDict...\n')

d = c.k.computeInverseBindingDict()

for key in sorted(list(d.keys())):

    if 1 == len(d.get(key)):
        print(key,d.get(key))
    else:
        print()
        print(key)
        print(d.get(key))
        print()
#@+node:ekr.20130503061511.4179: *4* @test printBindingsDict
import leo.core.leoConfig as leoConfig # for ShortcutInfo
    
partial = True

d = c.k.bindingsDict
    # Keys are shortcuts; values are *lists* leoConfig.ShortcutInfo objects.
    
print('\nk.bindingsDict%s...\n' % ' (partial)' if partial else '')
    
for key in list(sorted(d.keys())):
    aList = d.get(key,[])
    for b in aList:
        assert isinstance(b,leoConfig.ShortcutInfo)
        if not partial or b.kind != 'leosettings.leo':
            print(b)
#@+node:ekr.20130503061511.4180: *4* @test printMasterBindingsDict
partial = True

panes = ('all','body','button','log','tree','text',
    'command','insert','overwrite',)

d = c.k.masterBindingsDict
    # Keys are scope names (in panes) or mode names.
    # Values are dicts:
        # keys are strokes; values are leoConfig.ShortcutInfo objects.
        
print('\nk.masterBindingsDict%s...\n' % ' (partial)' if partial else '')

for pane in sorted(list(d.keys())):
    kind = 'pane' if pane in panes else 'mode'
    print('%s: %s...' % (kind,pane))
    d2 = d.get(pane)
    for stroke in sorted(list(d2.keys())):
        b = d2.get(stroke)
        if not partial or b.kind != 'leosettings.leo':
            print('%6s %25s %17s %s' % (b.pane,stroke,b.kind,b.commandName))
            assert b.pane == pane
            assert b.stroke == stroke
    print()
#@+node:ekr.20130503061511.4181: *3* passed
#@+node:ekr.20130503061511.4182: *4* @test mode-related info
@

g.app.config.modeCommandsDict
    Keys are command names: enter-x-mode.
    Values are inner dictionaries:
        Keys are command names, values are lists of ShortcutInfo nodes.
@c

d = g.app.config.modeCommandsDict
    
for key in sorted(d.keys()):
    print('*** mode ***',key)
    d2 = d.get(key)
    for key2 in sorted(d2.keys()):
        aList = d2.get(key2)
        print(key2)
        for si in aList:
            print('   ',si)
#@+node:ekr.20130503061511.4183: *4* @test types of contents of settings dicts
@
ivar                    Keys                Values
----                    ----                ------
c.commandsDict          command names (1)   functions
k.inverseCommandsDict   func.__name__       command names
k.bindingsDict          shortcuts           list of ShortcutInfo objects
k.masterBindingsDict    scope names (2)     inner masterBindingDicts (3)
k.masterGuiBindingsDict strokes             list of widgets in which stoke is bound
k.settingsNameDict (4)  settings.lower()    "Real" Tk specifiers
inverseBindingDict (5)  command names       lists of tuples (pane,key)
modeCommandsDict (6)    command name (7)    inner modeCommandsDicts (8)

Notes:
(1) Command names are minibuffer names (strings)
(2) Scope names are 'all','text',etc.
(3) inner masterBindingDicts: Keys are strokes; values are ShortcutInfo objects.
(4) k.settingsNameDict has no inverse.
(5) inverseBindingDict is **not** an ivar: it is computed by k.computeInverseBindingDict.
(6) A global dict: g.app.gui.modeCommandsDict
(7) enter-x-command
(8) Keys are command names, values are lists of ShortcutInfo objects.
@c

si_type = c.k.ShortcutInfo
disabled_func_type = None # Should be any bound method.
k = c.k

@others

test_dict_of_objects(c.commandsDict,type('s'),disabled_func_type,'commandsDict')
test_dict_of_objects(k.inverseCommandsDict,type('s'),type('s'),'inverseCommandsDict')
test_dict_of_lists(k.bindingsDict,si_type,'bindingsDict')
test_dict_of_dicts(k.masterBindingsDict,si_type,'masterBindingsDict')
test_dict_of_lists(k.masterGuiBindingsDict,None,'masterGuiBindingsDict')
test_dict_of_objects(k.settingsNameDict,type('s'),type('s'),'settingsNameDict')
test_dict_of_lists(k.computeInverseBindingDict(),type(tuple()),'inverseBindingDict')

# Test individual dicts separately.
d = g.app.config.modeCommandsDict
test_dict_of_dicts(d,None,'modeCommandsDict')
for key in sorted(d.keys()):
    d2 = d.get(key)
    test_dict_of_lists(d2,si_type,'inner modeCommandsDict')
        # This requires a hack to special-case the
        # '*entry-commands*' and '*command-prompt*' keys.
#@+node:ekr.20130503061511.4184: *5* test_dict_of_dicts
def test_dict_of_dicts(d,theType,tag):

    assert d,tag

    for key in d.keys():
        d2 = d.get(key)
        assert type(d2) == type({})
        for key in d2.keys():
            obj = d2.get(key)
            if theType:
                assert type(obj) == theType,repr(obj)
#@+node:ekr.20130503061511.4185: *5* test_dict_of_lists
def test_dict_of_lists(d,theType,tag):

    assert d,tag

    for key in d.keys():
        obj = d.get(key)
        if key in ('*entry-commands*','*command-prompt*'):
            # Special case for g.app.config.modeCommandsDict
            assert type(obj)==type([]),repr(obj)
        else:
            assert type(obj) == type([])
            # Don't check types of list elements if theType is None.
            if theType:
                for z in obj:
                    assert type(z)==theType,'key: %s obj: %s' % (key,repr(obj))
#@+node:ekr.20130503061511.4186: *5* test_dict_of_objects
def test_dict_of_objects(d,keyType,valueType,tag):

    assert d,tag

    for key in d.keys():
        assert type(key) == keyType,repr(key)
        obj = d.get(key)
        # Don't check type of obj if valueType is None.
        if valueType:
            assert type(obj) == valueType,'\nobj: %s\nvalueType: %s' % (repr(obj),valueType)
#@+node:ekr.20130503061511.4187: *5* Unused
# import types
# types.ListType does not exist in Python 3.x.
# assert isinstance(aList,list().__class__)
#@+node:ekr.20130503061511.4188: *4* @test merge_settings_dicts
@others

# import os ; os.system('cls')
    
d1 = g.app.config.immutable_leo_settings_shortcuts_dict
d2 = g.app.config.immutable_my_leo_settings_shortcuts_dict
d3 = g.app.config.merge_settings_dicts(d1,d2)

if False:
    patterns = (
        'backward-find-character-extend-selection',
    )
    for pattern in patterns:
        print(dump_dict(d1,pattern,tag='d1'))
        print(dump_dict(d2,pattern,tag='d2'))
        print(dump_dict(d3,pattern,tag='d3'))

test(d1,d2,d3)
#@+node:ekr.20130503061511.4189: *5* dump & dump_dict (@test merge_settings_dicts)
def dump(aList,pattern=None,tag=None):
    
    return '\n'.join([repr(z) for z in aList])
    

def dump_dict(d,pattern=None,tag=None):
    
    result = [] # '\ndump of %s...' % (tag)
    
    for key in d.keys():
        if pattern in (key,None):
            result.append(key)
            aList = d.get(key)
            for z in aList:
                result.append('    %s' % (z))
                
    return '\n'.join(result)
#@+node:ekr.20130503061511.4190: *5* test (@test merge_settings_dicts)
def test(old_d,new_d,result_d):
    
    '''Test that result_d is the result of upating old_d with new_d.
    
    This test is tricky: only inverted dicts have ShortcutInfo nodes as keys.'''
    
    invert,uninvert = g.app.config.invert,g.app.config.uninvert

    # Compute the inversions of all the dicts.
    inv_old,inv_new,inv_res = invert(old_d),invert(new_d),invert(result_d)
    
    # Part 1: Ensure we test all keys.
    keys = list(inv_old.keys())
    keys.extend(list(inv_new.keys()))
    keys.extend(list(inv_res.keys()))
    keys = sorted(list(set(keys)))
    assert None not in keys
    for key in inv_old.keys(): assert key in keys,key
    for key in inv_new.keys(): assert key in keys,key
    for key in inv_res.keys(): assert key in keys,key
    
    # Part 2: Carefully test the inverted result.
    def si_name_key(si): return si.commandName or ''

    for key in keys:
        # Compute the *sorted* list of 
        res_list = sorted(inv_res.get(key,[]),key=si_name_key)
        old_list = sorted(inv_old.get(key,[]),key=si_name_key)
        new_list = sorted(inv_new.get(key,[]),key=si_name_key)
        assert res_list,'no res_list.get(%s)' % (key)
        # if new_list: print(key,dump(new_list))
        if new_list:
            assert new_list == res_list,'key %s\nnew:\n%s\nres:\n%s' % (
                key,dump(new_list),dump(res_list))
        else:
            assert old_list == res_list,'key %s\nold:\n%s\nres:\n%s' % (
                key,dump(old_list),dump(res_list))
    
    # Part 3: Test that result_d == uninvert(invert(result_d)).
    # A.  They must have the same keys.
    unv_res = uninvert(inv_res)
    assert sorted(list(result_d.keys())) == sorted(list(unv_res.keys()))

    # B. The values of for each key must match after being sorted.
    def si_stroke_key(si): return si.stroke or ''
        
    for key in sorted(result_d.keys()):
        res_list = sorted(result_d.get(key,[]),key=si_stroke_key)
        unv_list = sorted( unv_res.get(key,[]),key=si_stroke_key)
        assert res_list == unv_list,'key %s\nres:\n%s\nunv:\n%s' % (
            key,dump(res_list),dump(unv_list))
   
#@+node:ekr.20130503061511.4191: *4* @test KeyStroke
ks = c.k.KeyStroke

@others

a1 = ks('a')
a2 = ks('a')
b1 = ks('b')
assert a1 == a2
d = {}
d[a1] = a1.s
d[a2] = a2.s
d[b1] = b1.s

for key in sorted(d):
    print(key,d.get(key))
#@+node:ekr.20130503061511.4192: *4* @test g.TypedDict
d = g.TypedDictOfLists('ks',type('s'),type(9))
d.add('a',1)
d.add('a',2)
d.add('b',3)

print(d)
for s in sorted(d.keys()):
    print(s,d.get(s,[]))

print('after replace...')
d.replace('a',[8,9,10])

for s in sorted(d.keys()):
    print(s,d.get(s,[]))
#@+node:ekr.20111107113442.3849: ** @ignore unused tests
#@+node:ekr.20111112193817.3908: *3* @@test delete-body-editor 3 times
if 0:
    # This works, but the label sticks, presumably because of a race condition.
    # Therefore, this is best left as a mini-test.

    w = c.frame.body
    for i in range(4):
        print(i)
        w.addEditor()
        w.deleteEditor()
#@+node:ekr.20100225094004.5405: *3* @@test loading .leo file with @file nodes
# test/at-file-test.leo contains tnodeList.
# test/at-file-test.py contains file-like sentinels.
g.app.unitTestDict={}
fn = g.os_path_finalize_join(g.app.loadDir,'..','test','unittest','at-file-test.leo')

try:
    c2 = g.openWithFileName(fn,c)
    assert c2
    assert c2.changed,'not changed'
    p2 = g.findNodeAnywhere(c2,'@file at-file-test.py')
    assert p2,'no p2'
    assert p2.isDirty(),'not dirty'
    assert g.app.unitTestDict.get('read-convert'),'not converted'
    ok = True
finally:
    if True: # and ok:
        # Close the frame without prompt.
        g.app.destroyWindow(c2.frame)
        c.setLog()
        c.bodyWantsFocus()
#@+node:ekr.20061104172236.23: *3* @@test tkBody.onClick
w = c.frame.body.bodyCtrl
y = 10
for x in range(0,100,10):
    event = g.app.gui.create_key_event(c,None,None,w,x=x,y=y)
    c.frame.body.onClick(event)
#@+node:ekr.20051107115231.25: *3* @@test typing in headline recomputes width
# getWidth no longer exists.
k = c.keyHandler
h = 'Test headline abc'
p = c.testManager.findNodeAnywhere(h)
assert p,'node not found: %s' % h
c.redraw(p) # To make node visible
c.frame.tree.editLabel(p)
w = c.edit_widget(p)
try:
    assert w
    g.app.gui.set_focus(c,w)
    w2 = g.app.gui.get_focus(c)
    # assert w == w2 or hasattr(w,'widget') and w.widget == w2,'w: %s\nw2: %s' % (w,w2)
    w.setSelectionRange('end','end')
    n = w.getWidth()
    g.app.gui.event_generate(c,'X','Shift+X',w)
    g.app.gui.event_generate(c,'Y','Shift+Y',w)
    g.app.gui.event_generate(c,'Z','Shift+Z',w)
    g.app.gui.event_generate(c,'\n','Return',w)
    w.update()
    assert w, 'fail 2'
   
finally:
    if 1:
        c.setHeadString(p,h) # Essential
        c.redraw(p)
#@+node:ekr.20061008162912: *3* @@test write .leo file with @ignore node
assert p.firstChild(), 'no child node'
assert p.firstChild().b.startswith('@ignore'), 'No @ignore in child'
ok = c.fileCommands.write_Leo_file(
    'file-name',outlineOnlyFlag=True,toString=True,toOPML=False)
assert ok, 'error writing file'
count = 0
s = g.app.write_Leo_file_string
for line in g.splitLines(s):
    if line.find('@ignore') != -1:
        count += 1
assert count >=1, "not enough @ignore's in written file: count: %s, lines:\n%s" % (count,s)
#@+node:ekr.20061008162912.1: *4* child
@ignore # Test that this node gets written.
#@+node:ekr.20101021205258.6013: *3* general file stuff
#@+node:ekr.20100204153116.5369: *4* @@test raw file copy
import tempfile
import os

s = 'Select the following string: वादक. Typing and undo now work.'
fd,fn = tempfile.mkstemp(text=False)
s = g.toEncodedString(s)
os.write(fd,s)
os.close(fd)
f = open(fn,'rb')
s2 = f.read()
f.close()
assert s==s2
os.remove(fn)
print('deleted',fn)
#@+node:ekr.20080806211453.5: *4* @@test round-trip-uAs for @shadow
root = p.firstChild()
uA = 'unknownAttributes'
tag = 'round-trip-u.uA'
ttag = 'round-trip-t.uA'
trace = False

if 0: # Set the uA's.
    for p2 in root.self_and_subtree_iter():
        p2.v.unknownAttributes = {tag: p2.h}
else: # Test the uA's.
    # The root is a special case.
    v = root.v
    assert hasattr(v,uA),'no v.uA for %s' % v
    assert getattr(v,uA),'empty v.uA for %s' % v
    for p2 in root.self_and_subtree_iter():
        v = p2.v
        assert hasattr(v,uA),'no v.uA for %s' % v
        a = getattr(v,uA)
        d = {tag: v.h}
        if trace: print(d)
        assert a == d, 'expected v.uA: "%s", got "%s"' % (d,a)
#@+node:ekr.20080806211453.1: *5* @@shadow uA_test_shadow_file.py
@language python
@tabwidth -4
@others
#@+node:ekr.20080822160527.1: *6* uA_test_shadow_file declarations
pass
pass
pass
#@+node:ekr.20080806095923.2: *4* @@test round-trip-uAs for @thin
root = p.firstChild()
uA = 'unknownAttributes'
tag = 'round-trip-u.uA'
ttag = 'round-trip-t.uA'
trace = False

if 0: # Set the uA's.
    for p2 in root.self_and_subtree_iter():
        p2.v.unknownAttributes = {tag: p2.h}
else: # Test the uA's.
    # The root is a special case.
    v = root.v
    assert hasattr(v,uA),'no v.uA for %s' % v
    assert getattr(v,uA),'empty v.uA for %s' % v
    for p2 in root.self_and_subtree_iter():
        v = p2.v
        assert hasattr(v,uA),'no v.uA for %s' % v
        a = getattr(v,uA)
        d = {tag: v.h}
        if trace: print(d)
        assert a == d, 'expected v.uA: "%s", got "%s"' % (d,a)
#@+node:ekr.20080806084924.2: *5* @@thin uA_test_file.py
@others
#@+node:ekr.20080806084924.3: *6* child1
pass
#@+node:ekr.20080806084924.4: *7* grandChild1
pass
#@+node:ekr.20080806084924.5: *8* greatGrandChild1
pass
#@+node:ekr.20100223094723.5375: *3* New import tests
#@+node:ekr.20100223094723.5376: *4* @test skipToTheNextClassOrFunction (a class next)
import leo.core.leoImport as leoImport
ic = c.importCommands

s = '''\

def one():
    pass

import a
from . import a

@language python

d = {} # An interior comment.

# This is a comment.
# and another comment.
@aDecorator
class cl: # An interior comment
    def method(self):
        pass

def two():
    pass

'''

# tree = c.importCommands.pythonUnitTest(p,s=s,showTree=True)

expected = s.find('# This is a comment')
scanner = leoImport.PythonScanner(importCommands=ic,atAuto=False)
i = s.find('import a')
assert i > -1
i = scanner.skipToTheNextClassOrFunction(s,i,lastIndent=0)
assert i==expected,'expected %s, got %s %s' % (
    expected,i,repr(s[i:]))
#@+node:ekr.20100223094723.5377: *4* @test skipToTheNextClassOrFunction (a function next)
import leo.core.leoImport as leoImport
ic = c.importCommands

s = '''\

def one():
    pass

import a
from . import a

d = {}

# This is a comment.
@tabwith -4 # This looks like a comment.
# and another comment.
@aDecorator
def two():
    pass

'''

# tree = c.importCommands.pythonUnitTest(p,s=s,showTree=True)

expected = s.find('# This is a comment')
scanner = leoImport.PythonScanner(importCommands=ic,atAuto=False)
i = s.find('import a')
assert i > -1
i = scanner.skipToTheNextClassOrFunction(s,i,lastIndent=0)
assert i==expected,'expected %s, got %s %s' % (
    expected,i,repr(s[i:]))
#@+node:ekr.20100223094723.5378: *4* @test skipToTheNextClassOrFunction (nothing next)
import leo.core.leoImport as leoImport
ic = c.importCommands

s = '''\

def one():
    pass

import a
from . import a

d = {}

# This is a comment.
# and another comment.

@tabwidth -4

aList = ('a','b','def')

if __name__ == '__main__':
    pass

'''

lastLine = 'pass\n'
expected = s.find(lastLine) + len(lastLine) + 1
scanner = leoImport.PythonScanner(importCommands=ic,atAuto=False)
i = s.find('import a')
assert i > -1
i = scanner.skipToTheNextClassOrFunction(s,i,lastIndent=0)
assert i==expected,'expected %s, got %s %s' % (
    expected,i,repr(s[i:]))
#@+node:ekr.20100223094723.5379: *4* @test skipToTheNextClassOrFunction (indented def next)
import leo.core.leoImport as leoImport
ic = c.importCommands

s = '''\

def one():
    pass

import a
from . import a

if 0:
    def two():
        pass

if __name__ == '__main__':
    pass

'''

scanner = leoImport.PythonScanner(importCommands=ic,atAuto=False)
expected = i = s.find('import a')
assert i > -1
i = scanner.skipToTheNextClassOrFunction(s,i,lastIndent=0)
assert i==expected,'expected %s, got %s %s' % (
    expected,i,repr(s[i:]))
#@+node:ekr.20110610122533.3388: *3* Old Tests with @auto as the root
#@+node:ekr.20110610122533.3389: *4* @test goto-line-number @auto 1
# Not valid for external tests: uses @<file> node.
if not g.app.isExternalUnitTest:
    root = p.parent().parent()
    h = '@auto unittest/at-auto-line-number-test.py'
    target = g.findNodeAnywhere(c, h)
    assert target, 'no @auto node'
    p, n, found = c.gotoCommands.find_file_line(1, target)
    assert found, 'not found'
    assert n == 1, 'n: %s' % (n)
    assert p.h == 'at_auto_child', p.h
#@+node:ekr.20110610122533.3390: *4* @test goto-global-line @auto 2
# Not valid for external tests: uses @<file> node.
if not g.app.isExternalUnitTest:
    h = '@auto unittest/at-auto-line-number-test.py'
    root = g.findNodeAnywhere(c, h)
    assert root, 'no root'
    p, n, found = c.gotoCommands.find_file_line(20, root)
    assert not found, 'not found'
#@+node:ekr.20110610122533.3391: *4* @test goto-global-line @auto 3
# Not valid for external tests: uses @<file> node.
if not g.app.isExternalUnitTest:
    h = '@auto unittest/at-auto-line-number-test.py'
    root1 = g.findNodeAnywhere(c, h)
    assert root1
    assert root1.isAnyAtFileNode()
    fileName, isScript, lines, n, root = c.gotoCommands.setup_file(n=3, p=root1)
    assert fileName == h[6:], 'fileName: %s' % (fileName)
    assert root == root1, 'root: %s, root1: %s' % (root and root.h, root1 and root1.h)
    if 0:
        print('root:%s, isRaw:%s, n:%s, len(lines): %s' % (
            root and root.h, isRaw, n, len(lines)))
#@+node:ekr.20110610122533.3392: *4* @test goto-global-line @auto 4
# Not valid for external tests: uses @<file> node.
if not g.app.isExternalUnitTest:
    h = '@auto unittest/at-auto-line-number-test.py'
    root1 = g.findNodeAnywhere(c, h)
    assert root1
    assert root1.isAnyAtFileNode()
    scriptData = {'p': root1.copy(), 'lines': ['a', 'b', 'c']}
    fileName, lines2, p2, root2 = c.gotoCommands.setup_script(scriptData)
    assert fileName == h[6:], 'fileName'
    assert lines2 == scriptData.get('lines'), 'lines'
    assert p2 == root1, 'p'
    assert root2 == root1, 'root'
    if 0:
        print('root:%s, n:%s, len(lines): %s' % (
            root and root.h, n, len(lines)))
#@+node:ekr.20110610122533.3406: *4* @test goto-global-line @auto 5
# Not valid for external tests: uses @<file> node.
if not g.app.isExternalUnitTest:
    fn = '../test/at-auto-unit-test.py'
    root = g.findNodeAnywhere(c, '@auto %s' % (fn))
    assert root, 'no root'
    child1 = root.firstChild()
    assert child1, 'no child1'
    grand11 = child1.firstChild()
    assert grand11, 'no grand11'
    grand12 = grand11.next()
    assert grand12, 'no grand12'
    child2 = child1.next()
    assert child2, 'no child2'
    grand21 = child2.firstChild()
    assert grand21, 'no grand21'
    grand22 = grand21.next()
    assert grand22, 'no grand22'

    def oops(found, p2, node, n, n2):
        result = [' ']
        result.append('goto-global-line test failed at line %s' % (n))
        if not found:
            result.append('line %s not found', n)
        else:
            result.append('got node "%s", expected "%s"' % (p2.h, node.h))
            result.append('got offset %s, expected %s' % (n2, n))
        return '\n'.join(result)

    table = (
        # Use 1-based numbers externally.
        # find_file_line converts to zero-based numbers.
        (1, child1, 0),
        (2, grand11, 0),
        (3, grand11, 1),
        (4, grand12, 0),
        (5, grand12, 1),
        (6, child1, 2),
        (7, child2, 0),
        (8, grand21, 0),
        (9, grand21, 1),
        (10, grand22, 0),
        (11, grand22, 1),
        (12, root, 3),
    )
    # Test against actual lines of the file.
    path = g.os_path_finalize_join(g.app.loadDir, '..', 'test', fn)
    f = open(path, 'r'); s = f.read(); f.close()
    lines = g.splitLines(s) # The lines from the file.
    for n, node, index in table:
        p2, n2, found = c.gotoCommands.find_file_line(n, root)
            # n, the argument to find_file_line, is 1-based
            # n2, the returned index into p2.b, is zero-based
        n -= 1
            # Convert n to zero-based for the comparisons below.
        lines2 = g.splitLines(p2.b)
        if 0:
            print('%2d %s' % (n, repr(lines[n])))
        else:
            ok = lines2[n2].lstrip() == lines[n].lstrip()
            if not ok:
                i = 0
                for z in lines:
                    print('%2d %s' % (i, repr(z)))
                    i += 1
            if not ok:
                print('at line %s, index %s, node %s\ngot line %s\nexpected %s' % (
                    n, n2, p2.h, repr(lines2[n2].lstrip()), repr(lines[n].lstrip())))
            assert ok
#@+node:ekr.20110610122533.3401: *4* @test GoToLineNumber.find_root 1
# Not valid for external tests: uses @<file> node.
if not g.app.isExternalUnitTest:
    root = p.parent().parent()
    h = '@auto unittest/at-auto-line-number-test.py'
    target = g.findNodeAnywhere(c, h)
    assert target, 'no target'
    child = target.firstChild()
    assert child.h == 'at_auto_child', 'child.h'
    p, found = c.gotoCommands.find_root(child)
    assert p == target, 'p' #p and p.h
    assert found, 'not found'
#@+node:ekr.20100208230953.5383: ** Cache tests
#@+node:ekr.20100208095817.5387: *3* @@test leoCache
# Disabled this test because it's best to open this file without caching.

import leo.core.leoCache as leoCache

cacher = leoCache.cacher(c)

assert cacher.test()
#@+node:ekr.20040712101754.2: ** Load tests for .leo files
@language python
@tabwidth -4

# These work well with the qttabs gui.
#@+node:ekr.20040712101754.3: *3* @test test.leo
path = g.os_path_join(g.app.loadDir,"..","test","test.leo")
c.testManager.runLeoTest(path)
#@+node:ekr.20040803090901: *3* @test leoDist.leo
path = g.os_path_join(g.app.loadDir,"..","dist","leoDist.leo")
c.testManager.runLeoTest(path)
#@+node:ekr.20081121164135.1: *3* @test leoGuiPluginsRef.leo
path = g.os_path_join(g.app.loadDir,"..","plugins","leoGuiPluginsRef.leo")
c.testManager.runLeoTest(path)
#@+node:ekr.20040712101754.4: *3* @test LeoPyRef.leo
path = g.os_path_join(g.app.loadDir,"..","core","LeoPyRef.leo")
c.testManager.runLeoTest(path)
#@+node:ekr.20040712101754.5: *3* @test leoPluginsRef.leo
path = g.os_path_join(g.app.loadDir,"..","plugins","leoPluginsRef.leo")
c.testManager.runLeoTest(path)
#@+node:ekr.20040712101754.6: *3* @test LeoDocs.leo
path = g.os_path_join(g.app.loadDir,"..","doc","LeoDocs.leo")
c.testManager.runLeoTest(path)
#@+node:ekr.20040730181601: *3* @test minimalLeoFile.leo
path = g.os_path_join(g.app.loadDir,"..","test","unittest","minimalLeoFile.leo")
c.testManager.runLeoTest(path)
#@+node:ekr.20040730181610: *3* @test minimalLeoFile2.leo
path = g.os_path_join(g.app.loadDir,"..","test","unittest","minimalLeoFile2.leo")
c.testManager.runLeoTest(path)
#@+node:ekr.20040831104758: *3* @test minimalLeoFile3.leo
path = g.os_path_join(g.app.loadDir,"..","test","unittest","minimalLeoFile3.leo")
c.testManager.runLeoTest(path)
#@+node:ekr.20080405085247.1: ** slow tests
#@+node:ekr.20060208195054: *3* @@test k.inverseCommandsDict is inverse of c.commandsDict
# c.commandsDict: keys are emacs command names, values are functions f.
# k.inverseCommandsDict: keys are f.__name__, values are emacs command names.

@others

d1 = c.commandsDict ; d2 = c.k.inverseCommandsDict

if 0:
    vals = d2.values() ; vals.sort()
    vals = [z for z in vals if z.startswith('contract')]
    g.pr('inverseCommandsDict.values()',vals)

keys1 = d1.keys() ; keys1.sort()
vals1 = d1.values()
vals1 = [f.__name__ for f in vals1]
vals1.sort()

keys2 = d2.keys() ; keys2.sort()
vals2 = d2.values(); vals2.sort()

if 0:
    g.pr(keys1,'\n\n')
    g.pr(vals2,'\n\n')
    g.pr(keys2,'\n\n')
    g.pr(vals1)

# g.trace(g.dictToString(c.k.abbreviationsDict))

abbrevDict = c.config.getAbbrevDict()

# Find @button and @command nodes in this file.
@others
buttonKeys = []
for p in c.allNodes_iter():
    h = p.h.strip().lower()
    for kind in ('@button','@command'):
        if h.startswith(kind):
            key1 = mungeKey(h,kind,substitute=False)
            if key1 not in buttonKeys:
                buttonKeys.append(key1)
            key = mungeKey(h,kind,substitute=True)
            if key not in buttonKeys:
                buttonKeys.append(key)

for z in g.app.config.atCommonButtonsList:
    h,junk = z
    key = mungeKey(h,'@button')
    # g.trace(key)
    if key not in buttonKeys:
        buttonKeys.append(key)

# g.pr('buttonKeys',buttonKeys)

for key in keys1:
    if key not in vals2 and key.lower() not in vals2:
        if (
            key.startswith('enter-') and key.endswith('-mode') or
            key.startswith('press-') and key.endswith('-button') or
            key.startswith('delete-') and key.endswith('-button') or
            key.startswith('nav-') and key.endswith('-menu')
        ):
            vals2.append(key)
        elif key in buttonKeys or key.lower() in buttonKeys:
            # List of buttons defined in this file, or in @settings tree.
            vals2.append(key)
        elif key.startswith('open-with-'):
            vals2.append(key)
        elif key in abbrevDict.keys():
            pass # g.trace('abbrev',key)
        else:
            assert False, '%s not in inverseCommandsDict.values()' % key

vals2.sort()
for val in vals2:
    if val not in keys1:
        assert False, '%s not in commandsDict.keys()' % (val)
#@+node:ekr.20070926095117: *4* mungeKey
def mungeKey (h,kind,substitute=True):

    key = h[len(kind):].strip()
    i = key.find('@key')
    if i > -1: key = key[:i].strip()
    if substitute:
        key = key.replace(' ','-')
    # g.trace(key)
    return key


#@+node:ekr.20110728154927.3324: *3* @@test position > operator
# Disabled because very slow
aList = [z.copy() for z in c.all_positions()]

total = 0

for i in range(len(aList)):
    for j in range(0,i):
        assert aList[j] < aList[i],(i,j)
        total += 1

g.es('"%s": total tests: %s' % (p.h,total))
#@-all
#@-leo
