#!/usr/bin/env python3

# Notes:
#   1) INI file items:
#      NGCGUI_PREAMBLE
#      NGCGUI_SUBFILE
#      NGCGUI_POSTAMBLE
#      NGCGUI_OPTIONS
#            nonew          disallow new tabs
#            noremove       disallow removal of tabs
#            noauto         don't automatically send result file
#            noexpand       (ngcgui used, not supported pyngcgui)
#            nom2           (no m2 terminator (use %))
#   2) To make pyngcgui embedded fit in small screen:
#       Try:
#         max_parms=10|20|30 (will reject otherwise valid subfiles)
#         image_width=240
#         reduce subroutine parm name lengths and/or comment string length

#------------------------------------------------------------------------------
# Copyright: 2013-6
# Author:    Dewey Garrett <dgarrett@panix.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#------------------------------------------------------------------------------
""" python classes to implement an ngcgui-like application

These INI file items are compatible with ngcgui.tcl:
  [DISPLAY]NGCGUI_PREAMBLE    single specifier
  [DISPLAY]NGCGUI_POSTAMBLE   single specifier
  [DISPLAY]NGCGUI_SUBFILE     multiples allowed, use "" for Custom tab
  [DISPLAY]NGCGUI_OPTIONS
           noremove           disallow tabpage removal
           nonew              disallow tabpage creation
           noiframe           don't show image in tabpage
           noauto             don't automatically send result file
  [DISPLAY]PROGRAM_PREFIX     subroutine path: start
  [RS274NGC]SUBROUTINE_PATH   subroutine path: middle
  [WIZARD]WIZARD_ROOT         subroutine path: end
  [DISPLAY]NGCGUI_FONT        not used
  [DISPLAY]TKPKG              not applicable
"""
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GObject
from   types import * # IntType etc
import os
import sys
import re
import getopt
import datetime
import subprocess
import linuxcnc
import hashlib
import glob
import shutil
import popupkeyboard
import traceback   # for debug printing
import hal         # notused except for debug
from gladevcp import hal_actions

g_ui_dir = linuxcnc.SHARE + "/linuxcnc"

# determine if glade interface designer is running
# in order to prevent connection of most signals
g_is_glade = False
if (     ('glade'        in sys.argv[0])
    and  ('gladevcp' not in sys.argv[0])):
    for d in os.environ['PATH'].split(':'):
        f = os.path.join(d,sys.argv[0])
        if (    os.path.isfile(f)
            and os.access(f, os.X_OK)):
            g_is_glade = True
            break
g_alive = not g_is_glade

import gettext
LOCALEDIR = linuxcnc.SHARE + "/locale"
gettext.install("linuxcnc", localedir=LOCALEDIR)

#------------------------------------------------------------------------------
g_debug             = False
g_verbose           = False
g_nom2              = False # True for no m2 terminator (use %)
g_strict            = False # enforce additional subfile formatting requirements
g_tmode             = 0     # for development
g_entry_height      = 20    # default parm entry height
                            # (override for popupkeyboard)
g_big_height        = 35    # increased parm entry height value

g_image_width       = 320   # image size
g_image_height      = 240   # image size

g_check_interval    = 2 # periodic check (seconds)
g_label_id          = 0 # subroutine labels modifier when expanding in place
g_progname          = os.path.splitext(os.path.basename(__file__))[0]
g_dtfmt             = "%y%m%d:%H.%M.%S"

g_stat              = None # linuxcnc.stat  object
g_popkbd            = None # PopupKeyboard  object
g_candidate_files   = None # CandidateFiles object
g_send_function     = None # function object f(fname) return True for success
g_tab_controls_loc  ='top' # 'top' | 'bottom'

g_keyboardfile      = os.path.join(g_ui_dir,'popupkeyboard.ui')

g_control_font      = None
g_font_users        = []
g_auto_file_ct      = 1

INTERP_SUB_PARAMS = 30 # (1-based) conform to:
# src/emc/rs274ngc/interp_internal.hh:#define INTERP_SUB_PARAMS 30
g_max_parm       = INTERP_SUB_PARAMS
g_max_msg_len    = 500 # limit popup msg len for errant gcmc input

g_gcmc_exe = None
g_gcmc_funcname = 'tmpgcmc'
g_gcmc_id = 0

black_color   = Gdk.color_parse('black')
white_color   = Gdk.color_parse('white')
error_color   = Gdk.color_parse('red')
green_color   = Gdk.color_parse('green')
blue_color    = Gdk.color_parse('blue')
yellow_color  = Gdk.color_parse('yellow')
purple_color  = Gdk.color_parse('purple')
feature_color = Gdk.color_parse('lightslategray')

label_normal_color = Gdk.color_parse('lightsteelblue2')
label_active_color = Gdk.color_parse('ivory2')
base_entry_color   = Gdk.color_parse('azure1')
fg_created_color   = Gdk.color_parse('palegreen')
fg_multiple_color  = Gdk.color_parse('cyan')
fg_normal_color    = black_color

bg_dvalue_color = Gdk.color_parse('darkseagreen2')
#------------------------------------------------------------------------------

def exception_show(ename,detail,src=''):
    print('\n%s:' % src )
    print('Exception: %s' % ename )
    print('   detail: %s' % detail )
    if type(detail) == ValueError:
        for x in detail:
            if isinstance(x,str):
                print('detail(s):',x)
            else:
                for y in x:
                    print('detail(d):',y,)
    elif isinstance(detail,str):
        print('detail(s):',detail)
    elif isinstance(detail,list):
        for x in detail:
            print('detail(l):',x)
    else:
        print(ename,detail)

    if g_debug:
        #print(sys.exc_info())
        print( traceback.format_exc())

def save_a_copy(fname,archive_dir='/tmp/old_ngc'):
    if fname is None:
        return
    try:
        if not os.path.exists(archive_dir):
            os.mkdir(archive_dir)
        shutil.copyfile(fname
              ,os.path.join(archive_dir,dt() + '_' + os.path.basename(fname)))
    except IOError as msg:
        print(_('save_a_copy: IOError copying file to %s') % archive_dir)
        print(msg)
    except Exception as detail:
        exception_show(Exception,detail,src='save_a_copy')
        print(traceback.format_exc())
        sys.exit(1)

def get_linuxcnc_ini_file():
    ps   = subprocess.Popen('ps -C linuxcncsvr --no-header -o args'.split(),
                             stdout=subprocess.PIPE
                           )
    p,e = ps.communicate()

    if ps.returncode:
        print(_('get_linuxcnc_ini_file: stdout= %s') % p)
        print(_('get_linuxcnc_ini_file: stderr= %s') % e)
        return None

    if p is not None: p = p.decode()
    if e is not None: e = e.decode()
    ans = p.split()[p.split().index('-ini')+1]
    return ans

def dummy_send(filename):
    return False # always fail

def default_send(filename):
    import gladevcp.hal_filechooser
    try:
        s = linuxcnc.stat().poll()
    except:
        user_message(mtype=Gtk.MessageType.ERROR
            ,title=_('linuxcnc not running')
            ,msg = _('cannot send, linuxcnc not running'))
        return False
    try:
        fchooser = gladevcp.hal_filechooser.EMC_Action_Open()
        fchooser._hal_init()
        fchooser._load_file(filename)
        return True
    except:
        return False

def send_to_axis(filename): # return True for success
    # NB: file with errors may hang in axis gui
    s = subprocess.Popen(['axis-remote',filename]
                         ,stdout=subprocess.PIPE
                         ,stderr=subprocess.PIPE
                         )
    p,e = s.communicate()
    if s.returncode:
        print(_('%s:send_to_axis: stdout= %s') % (g_progname,p))
        print(_('%s:send_to_axis: stderr= %s') % (g_progname,e))
        return False
    if p: print(_('%s:send_to_axis: stdout= %s') % (g_progname,p))
    if e: print(_('%s:send_to_axis: stderr= %s') % (g_progname,e))
    return True

def file_save(fname,title_message='Save File'):
    start_dir = os.path.dirname(fname)
    if start_dir == '': start_dir = os.path.curdir
    fc = Gtk.FileChooserDialog(title=title_message
       ,parent=None
       ,action=Gtk.FileChooserAction.SAVE
       ,buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL
                ,Gtk.STOCK_OK,     Gtk.ResponseType.OK
                )
       )
    fc.set_current_folder(start_dir)
    fc.set_do_overwrite_confirmation(True)
    filter = Gtk.FileFilter()
    filter.set_name('NGC files')
    filter.add_pattern('*.ngc')
    filter.add_pattern('*.NGC')
    filter.add_pattern('*.nc')
    filter.add_pattern('*.NC')
    filter.add_pattern('*.gcmc')
    filter.add_pattern('*.GCMC')
    fc.add_filter(filter)
    fc.set_current_name(os.path.basename(fname)) # suggest name (for save)
    fname = None
    ans = fc.run()
    if ans == Gtk.ResponseType.OK:
        fname = fc.get_filename()
    elif ans == Gtk.ResponseType.CANCEL:
        print(_('file_save:canceled'))
    elif ans == Gtk.ResponseType.DELETE_EVENT: # window close
        print(_('file_save:window closed'))
    else:
        raise IOError(_('file_save:unexpected'))
    fc.destroy()
    return(fname)

def is_comment(s):
    if s[0] == ';':      return bool(1) # ;xxx
    elif  s[0] == '(':
        if s[-2] == ')': return bool(1) # (yyy)
        else:            return bool(1) # (yyy)zzz  maybe bogus
    return bool(0)

def get_info_item(line):
    # expect line as unaltered line with whitespace
    l = line.translate(' \t').lower()
    r = re.search(r'^\(info:(.*)\)',l)
    if r:
        r = re.search(r'.*info:(.*)\)',line)
        if r: return r.group(1)
    return None

def check_sub_start(s):
    r = re.search(r'^o<(.*)> *sub.*',s.strip())
    if r:
        #print('check_sub_start:g0:',r.group(0))
        #print('check_sub_start:g1:',r.group(1))
        return r.group(1)
    return None

def check_sub_end(s):
    r = re.search(r'^o<(.*)> *endsub.*',s)
    if r:
        #print('check_sub_end:g0:',r.group(0))
        #print('check_sub_end:g1:',r.group(1))
        return r.group(1)
    return None

def check_for_label(s):
    r = re.search(r'^o<(.*?)> *(sub|endsub).*',s)
    if r:
        return 'ignoreme' # do not include on expand

    r = re.search(r'^o<(.*?)> *(call).*',s)
    if r:
        return None # do not mod label on expand

    r = re.search(r'^o<(.*?)>.*',s)
    if r:
        return r.group(1) # make label unique on expand

    return None

def check_positional_parm_range(s,min,max):
    r = re.search(r'#([0-9]+)',s)
    if r: pnum = int(r.group(1))
    # here check is against system limit; g_max_parm applied elsewhere
    if r and (pnum <= INTERP_SUB_PARAMS):
        if pnum < min: min = pnum
        if pnum > max: max = pnum
        return pnum,min,max
    return None,None,None

def find_positional_parms(s):
    # requires original line (mixed case with whitespace)
    # find special association lines for positional parameters

    # The '*', '+', and '?' qualifiers are all greedy.
    #    Greedy <.*>  matches all of <H1>title</H1>
    # NonGreedy <.*?> matches the only first <H1>

    # case1  #<parmname>=#n (=defaultvalue comment_text)
    # case2  #<parmname>=#n (=defaultvalue)
    # case3  #<parmname>=#n (comment_text)
    # case4  #<parmname>=#n

    name    = None
    pnum    = None
    dvalue  = None
    comment = None
    s = s.expandtabs() # tabs to spaces

    r = re.search(
    r' *# *<([a-z0-9_-]+)> *= *#([0-9]+) *\(= *([0-9.+-]+[0-9.]*?) *(.*)\)'
               ,s,re.I)
    #case1   1name               2pnum          3dvalue             4comment

    if r is None: r=re.search(
    r' *# *<([a-z0-9_-]+)> *= *#([0-9]+) *\( *([0-9.+-]+)\)',s,re.I)
    #case2   1name               2pnum         3dvalue

    if r is None: r=re.search(
    r' *# *<([a-z0-9_-]+)> *= *#([0-9]+) *\((.*)\)',s,re.I)
    #case3   1name               2pnum       3comment

    if r is None: r=re.search(
    r' *# *<([a-z0-9_-]+)> *= *#([0-9]+) *$',s,re.I)
    #case4   1name               2pnum

    #   if r:
    #       for i in range(0,1+len(r.groups())):
    #           print('PARSE groups',len(r.groups()),i,r.group(i))

    if r:
        n = len(r.groups())
    if r and n >= 2:
        name = comment = r.group(1) # use name as comment if not specified
        pnum = int(r.group(2))
        # here check is against system limit; g_max_parm applied elsewhere
        if pnum > INTERP_SUB_PARAMS:
            return None,None,None,None
        if n == 3:
            if r.group(3)[0] == '=': dvalue = r.group(3)[1:]
            else:                    comment = r.group(3)[:]
        if n == 4:
            dvalue = r.group(3)
            if dvalue.find('.') >= 0:
                dvalue = float(dvalue)
            else:
                dvalue = int(dvalue)
            if r.group(4): comment = r.group(4)
        if n > 4:
            print('find_positional_parameters unexpected n>4',s,)
            comment = r.group(4)
        if comment is None:
            print('find_positional_parameters:NOCOMMENT') # can't happen
            comment = ''
    return name,pnum,dvalue,comment

def user_message(title=""
                ,mtype=Gtk.MessageType.INFO
                ,flags=Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT
                ,msg=None):
    if msg is None: return(None)
    if isinstance(msg,list):
        txt = "".join(msg)
    else:
        txt = msg

    vprint('USER_MESSAGE:\n%s' % txt)
    popup = Gtk.MessageDialog(parent = None
          ,flags=flags
          ,type=mtype
          ,buttons = Gtk.ButtonsType.OK
          ,message_format = txt
          )
    popup.set_title(title)
    result = popup.run()
    popup.destroy()
    return(result)

def dt():
    return(datetime.datetime.now().strftime(g_dtfmt))

def md5sum(fname):
    if not fname: return None
    return(hashlib.md5(open(fname, 'r').read().encode()).hexdigest())

def find_image(fname):
    found = False
    for suffix in ('png','gif','jpg','pgm'):
        name = os.path.splitext(os.path.basename(fname))[0]
        dir  = os.path.dirname(fname)
        ifile = os.path.join(dir,name + '.' + suffix)
        if os.path.isfile(ifile):
            found = True
            break
    if not found: return None
    return ifile

def sized_image(ifile):
    twidth  = g_image_width
    theight = g_image_height
    img = Gtk.Image()
    img.set_from_file(ifile)

    pixbuf  = img.get_pixbuf()
    iwidth  = pixbuf.get_width()  # image size
    iheight = pixbuf.get_height()
    scale      = min(float(twidth)/iwidth, float(theight)/iheight)
    #print('iw,ih %d,%d tw,th=%d,%d, scale=%f' % (
    #      iwidth,iheight,twidth,theight,scale))
    new_width  = int(scale*iwidth)
    new_height = int(scale*iheight)
    from gi.repository.GdkPixbuf import Pixbuf, InterpType
    pixbuf = pixbuf.scale_simple(new_width,new_height
                                ,InterpType.BILINEAR)
    img.set_from_pixbuf(pixbuf)
    return(img)

def show_dir(x,tag=''):
    l = []
    for name in sorted(dir(x)):
        if  name[0:2] == '__': continue
        item = getattr(x,name)
        ty = type(item)
        if ty == MethodType:
            l.append('%-8s %s()' % ('0 Meth',name))
        elif isinstance(ty,list):
            i = 0
            for v in item:
                try:
                    vnonewline = v[:-1] if v.endswith('\n') else v
                    l.append('%-8s %s[%2s] = %s' % ('2 List',name,i,vnonewline))
                    i += 1
                except:
                    l.append('xxx %s %s' % (name,str(item)))
        elif ty == DictionaryType:
            for k in sorted(item):
                l.append('%-8s %s[%2s] = %s' % ('3 Dict',name,k,item[k]))
        elif ty == BooleanType:
            l.append('%-8s %s = %s' % ('4 Bool',name,str(item)))
        elif ty == IntType:
            l.append('%-8s %s = %s' % ('5 Int',name,str(item)))
        elif ty == FloatType:
            l.append('%-8s %s = %s' % ('6 Float',name,str(item)))
        elif isinstance(ty,str):
            l.append('%-8s %s = %s' % ('7 Str',name,item))
        else:
            s = str(item).split(' ')[0] + '>'
            s=item
            l.append('%-8s %s = %s' % ('1 Obj',name,s))

    print('\n')
    print('%s----------------------------------------------------------' % tag)
    for i in sorted(l):
        print(i)
    print('%s==========================================================' % tag)

def dprint(txt):
    if g_debug:
        print(':' + txt)

def vprint(txt):
    if g_verbose:
        print('::' + txt)

def spath_from_inifile(fname):
    if not fname:
        return []
    ini = linuxcnc.ini(fname)
    homedir = os.path.dirname(os.path.realpath(fname))
    # http://www.linuxcnc.org/docs/devel/html/config/ini_config.html
    l = []
    p = ini.find('DISPLAY','PROGRAM_PREFIX')
    if p:
        l = [p]
    p = ini.find('RS274NGC','SUBROUTINE_PATH')
    if p:
        newdirs = p.split(':')
        for dir in newdirs:
            # dont add duplicates
            if dir not in l:
                l.append(dir)
    p = ini.find('WIZARD','WIZARD_ROOT')
    if p:
        l.extend(p.split(':'))
    lfull = []
    for d in l:
        d = os.path.expanduser(d)
        if os.path.isabs(d):
            lfull.append(d)
        else:
            # relative path implies cwd is correct
            d2 = os.path.join(homedir,d)
            lfull.append(os.path.abspath(d2))
    if lfull:
        return lfull
    return []

def mpath_from_inifile(fname):
    if not fname:
        return None
    ini = linuxcnc.ini(ifname)
    homedir = os.path.dirname(os.path.abspath(fname))
    l = []
    p = ini.find('DISPLAY','PROGRAM_PREFIX')
    if p:
        l = [p]
    else:
        l = 'nc_files'
    p = ini.find('RS274NGC','USER_M_PATH')
    if p:
        l.extend(p.split(':'))
    lfull = []
    for d in l:
        if os.path.isabs(d):
            lfull.append(d)
        else:
            d2 = os.path.join(homedir,d)
            lfull.append(os.path.abspath(d2))
    if lfull:
        return lfull
    return None

def spath_from_files(pre_file,sub_files,pst_file):
    # when there is no INI file for path because
    #   linuxcnc not running
    # and
    #   no INI specified on cmd line
    l = []

    slist = []
    if isinstance(sub_files,str) and sub_files:
        slist.append(sub_files)
    else:
        slist = sub_files

    for sub_file in slist:
        dir = os.path.dirname(os.path.abspath(sub_file))
        if dir not in l:
            l.append(dir)

    if pre_file:
        dir = os.path.dirname(os.path.abspath(pre_file))
        if dir not in l:
            l.append(dir)

    if pst_file:
        dir = os.path.dirname(os.path.abspath(pst_file))
        if dir not in l:
            l.append(dir)

    if l:
        return l
    return []

def long_name(name):
    if   name == 'pre':
        return 'Preamble'
    elif name == 'sub':
        return 'Subroutine'
    elif name == 'pst':
        return 'Postamble'
    else:
        return 'Unknown'

def show_parent(w,ct=0):
    if w is None:
        print('show_parent: None')
        return
    print('show_parent:',ct,w)
    if w.is_toplevel():
        print('TOP\n')
        return
    else:
        show_parent(w.get_parent(),ct+1)

def all_coords(iterable):
    ans = ''
    for t in iterable:
        ans = ans + '%7.3f' % t
    return ans

def show_position():
    g_stat.poll()
    print('POSITION-----------------------------------------------------')
    print('       ap',all_coords(g_stat.actual_position))
    print('        p',all_coords(g_stat.position))
    l = []
    p = g_stat.actual_position
    for i in range(9): l.append(p[i]
                               - g_stat.g5x_offset[i]
                               - g_stat.tool_offset[i]
                               )
    print('offset ap',all_coords(l))

    l = []
    p = g_stat.position
    for i in range(9): l.append(p[i]
                               - g_stat.g5x_offset[i]
                               - g_stat.tool_offset[i]
                               )
    print('offset  p',all_coords(l))
    print('POSITION=====================================================')

def coord_value(char):
    # offset calc from emc_interface.py (touchy et al)
    # char = 'x' | 'y' | ...
    # 'd' is for diameter
    c = char.lower()
    g_stat.poll()
    p = g_stat.position # tuple: (xvalue, yvalue, ...
    if (c == 'd'):
        if (1 & g_stat.axis_mask):
            # diam = 2 * x
            return (p[0] - g_stat.g5x_offset[0] - g_stat.tool_offset[0])* 2
        else:
            return 'xxx' # return a string that will convert with float()

    axno = 'xyzabcuvw'.find(c)
    if not ( (1 << axno) & g_stat.axis_mask ):
        return 'xxx' # return a string that will convert with float()
    return p[axno] - g_stat.g5x_offset[axno] - g_stat.tool_offset[axno]

def make_g_styles():

    dummylabel = Gtk.Label()

    global g_lbl_style_default
    g_lbl_style_default   = dummylabel.get_style().copy()
    #g_lbl_style_default.bg[Gtk.StateType.NORMAL] = label_normal_color TODO:
    #g_lbl_style_default.bg[Gtk.StateType.ACTIVE] = label_active_color

    global g_lbl_style_created
    g_lbl_style_created  = dummylabel.get_style().copy()

    global g_lbl_style_multiple
    g_lbl_style_multiple = dummylabel.get_style().copy()

    #g_lbl_style_multiple.bg[Gtk.StateType.NORMAL] = feature_color
    #g_lbl_style_multiple.bg[Gtk.StateType.ACTIVE] = feature_color

    #g_lbl_style_created.bg[Gtk.StateType.NORMAL] = feature_color
    #g_lbl_style_created.bg[Gtk.StateType.ACTIVE] = feature_color

    del dummylabel


    dummyentry = Gtk.Entry()

    global g_ent_style_normal
    g_ent_style_normal  = dummyentry.get_style().copy()

    global g_ent_style_default
    g_ent_style_default = dummyentry.get_style().copy()

    global g_ent_style_error
    g_ent_style_error   = dummyentry.get_style().copy()

    #g_ent_style_normal.base[Gtk.StateType.NORMAL]  = base_entry_color

    #g_ent_style_default.base[Gtk.StateType.NORMAL] = bg_dvalue_color

    #g_ent_style_error.text[Gtk.StateType.NORMAL]   = error_color
    #g_ent_style_error.base[Gtk.StateType.NORMAL]   = base_entry_color

    del dummyentry

def mod_font_by_category(obj,mode='control'):
    #dbg; crippled at gtk3
    return
    # currently mode = control (only)
    # touchy has 4 font categories: control,dro,error,listing
    if mode == 'control':
        font = g_control_font
    else:
        print('mod_font_by_category:unknown mode %s' % mode)
        return

    targetobj = None
    if type(obj) == type(Gtk.Label()):
        targetobj = obj
    elif type(obj) == type(Gtk.Entry()):
        targetobj = obj
    elif type(obj) == type(Gtk.Button()):
        #Gtk.Alignment object
        if isinstance(obj.child, Gtk.Label):
            targetobj = obj.child
        elif isinstance(obj.child, Gtk.Alignment):
            pass
        elif hasattr(obj,'modify_font'):
            targetobj = obj
        else:
            raise ValueError('mod_font_by_category: no child')
            return
    else:
        raise ValueError('mod_font_by_category: unsupported:').with_traceback(type(obj))
        return

    if targetobj is None:
        return
    if font is None:
        #print('mod_font_by_category:nofont available for %s' % mode)
        return # silently
    targetobj.modify_font(g_control_font)

    global g_font_users
    if targetobj not in g_font_users:
        g_font_users.append(targetobj)

def update_fonts(fontname):
    global g_control_font
    g_control_font = fontname
    for obj in g_font_users:
        mod_font_by_category(obj)

def clean_tmpgcmc(odir):
    if odir == "":
        odir = g_searchpath[0]
    savedir = os.path.join("/tmp", g_gcmc_funcname) # typ /tmp/tmpgcmc
    if not os.path.isdir(savedir):
        os.mkdir(savedir,0o755)
    for f in glob.glob(os.path.join(odir,g_gcmc_funcname + "*.ngc")):
        # rename ng across file systems
        shutil.move(f,os.path.join(savedir,os.path.basename(f)))

def find_gcmc():
    global g_gcmc_exe # find on first request
    if g_gcmc_exe == "NOTFOUND": return False # earlier search failed
    if g_gcmc_exe is not None: return True    # already found

    for dir in os.environ["PATH"].split(os.pathsep):
        exe = os.path.join(dir,'gcmc')
        if os.path.isfile(exe):
            if os.access(exe,os.X_OK):
                clean_tmpgcmc("") # clean on first find_gcmc
                g_gcmc_exe = exe
                return True # success
    g_gcmc_exe = "NOTFOUND"
    user_message(mtype=Gtk.MessageType.ERROR
                ,title=_('Error for:')
                ,msg = _('gcmc executable not available:'
                + '\nCheck path and permissions'))
    return False # fail

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

make_g_styles()


class CandidateDialog():
    """CandidateDialog: dialog with a treeview in a scrollwindow"""
    def __init__(self,ftype=''):
        self.ftype = ftype
        lname = long_name(self.ftype)
        title = "Choose %s file" % lname

        btns=(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT
             ,Gtk.STOCK_OK,     Gtk.ResponseType.ACCEPT)
        if ( (self.ftype == 'pre') or (self.ftype == 'pst') ):
            # NO used to allow 'nofile' for 'pre','pst'
            btns = btns + ('No %s File' % lname, Gtk.ResponseType.NO)

        self.fdialog = Gtk.Dialog(title=title
                     ,parent=None
                     ,flags=Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT
                     ,buttons=btns
                     )
        self.fdialog.set_size_request(600,600)

        scrollw = Gtk.ScrolledWindow()
        scrollw.set_border_width(5)
        scrollw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS)
        scrollw.show()

        box = self.fdialog.get_content_area()
        box.pack_start(scrollw, True, True, 0)

        global g_candidate_files
        self.canfiles = g_candidate_files
        self.canfiles.refresh()
        self.treestore = g_candidate_files.treestore

        self.treeview = Gtk.TreeView(self.treestore)
        if g_alive: self.treeview.connect('row-activated',self.row_activated)


        column0 = Gtk.TreeViewColumn('Subroutine Directories')
        self.treeview.append_column(column0)
        cell0 = Gtk.CellRendererText()
        column0.pack_start(cell0, True)
        column0.add_attribute(cell0, 'text', 0)

        column1 = Gtk.TreeViewColumn('Hint')
        self.treeview.append_column(column1)
        cell1 = Gtk.CellRendererText()
        column1.pack_start(cell1, True)
        column1.add_attribute(cell1, 'text', 1)

        column2 = Gtk.TreeViewColumn('mtime')
        self.treeview.append_column(column2)
        cell2 = Gtk.CellRendererText()
        column2.pack_start(cell2, True)
        column2.add_attribute(cell2, 'text', 2)

        scrollw.add_with_viewport(self.treeview)
        scrollw.show_all()

    def get_file_result(self):
        # return: (name,errmsg)
        try:
            (model,iter) = self.treeview.get_selection().get_selected()
        except AttributeError:
            return(None,'') # nothing selected
        if not iter:
            return(None,'')
        fname,status,mtime = self.canfiles.get_tree_data(iter)

        if os.path.isdir(fname):
            return(None,'') # cannot use a selected dir

        ok = True # contradict this
        if (self.ftype == 'pre') or (self.ftype == 'pst'):
            if status.find('not_a_subfile') >= 0: ok = True
            if status.find('Preempted')     >= 0: ok = False
        else:
            if status.find('not_a_subfile') >= 0: ok = False
            if status.find('not_allowed')   >= 0: ok = False
            if status.find('Preempted')     >= 0: ok = False

        if ok:
            return (fname,'')

        emsg = (_('The selected file is not usable\n'
                  'as a %s file\n'
                  '(%s)') % (long_name(self.ftype),status)
               )
        return('TRYAGAIN',emsg)

    def row_activated(self,tview,iter,column):
        self.fdialog.response(Gtk.ResponseType.ACCEPT)
        pass

    def run(self):
        return(self.fdialog.run())

    def destroy(self):
        self.fdialog.destroy()


class CandidateFiles():
    """CandidateFiles treestore for candidate files"""
    def __init__(self,dirlist):
        self.dirlist=dirlist
        self.treestore = Gtk.TreeStore(str,str,str)
        self.tdict = {}
        self.make_tree()

    def refresh(self):
        # currently, just do over
        # potential to reread only files with modified mtimes
        self.__init__(self.dirlist)

    def make_tree(self):
        didx = 0
        flist = []
        for dir in self.dirlist:
            self.tdict[didx,] = dir
            # row must be a tuple or list containing as many items
            # as the number of columns
            try:
                mtime = datetime.datetime.fromtimestamp(os.path.getmtime(dir))
            except OSError as detail:
                print(_('%s:make_tree:%s' % (g_progname,detail) ))
                continue # try to skip this dir with message
            mtime = mtime.strftime(g_dtfmt) # truncate fractional seconds
            iter = self.treestore.append(None, [dir,"Directory",mtime])
            fidx = 0
            for f in ( sorted(glob.glob(os.path.join(dir,"*.ngc")))
                     + sorted(glob.glob(os.path.join(dir,"*.NGC")))
                     + sorted(glob.glob(os.path.join(dir,"*.gcmc")))
                     + sorted(glob.glob(os.path.join(dir,"*.GCMC")))
                     ):
                fname = os.path.basename(f)
                self.tdict[didx,fidx] = fname

                stat = ""
                fd = open(f)
                ftxt = fd.read()
                fd.close()

                if os.path.splitext(fname)[-1] in ['.gcmc','.GCMC']:
                    stat = '%sgcmc:ok' % stat

                if ftxt.find('not_a_subfile') >= 0:
                    stat = '%snot_a_subfile ' % stat
                if ftxt.find('(info:') >= 0:
                    stat = '%sngcgui-ok ' % stat
                if fname in flist:
                    stat = '%sPreempted ' % stat
                if ftxt.find('FEATURE') >= 0:
                    stat = '%snot_allowed ' % stat
                if stat == "":
                    stat = "?"
                if stat.find("Preempted") >= 0:
                    stat = "Preempted" # suppress ok

                flist.append(fname)
                mtime = datetime.datetime.fromtimestamp(os.path.getmtime(f))
                mtime = mtime.strftime(g_dtfmt) # truncate fractional seconds
                self.treestore.append(iter, [fname,stat,mtime])
                fidx += 1
            didx += 1

    def get_tree_data(self,iter):
        path = self.treestore.get_path(iter)
        if len(path) > 1:
            row,col = path
            dir = self.tdict[row,]
            fname = self.treestore.get_value(iter,0)
            status = self.treestore.get_value(iter,1)
            mtime = self.treestore.get_value(iter,2)
        else:
            dir = self.tdict[path]
            fname = ''
            status = ''
            mtime = ''
        return os.path.join(dir,fname),status,mtime


class LinuxcncInterface():
    """LinuxcncInterface: INI file and running linuxcnc data"""
    def __init__(self,cmdline_ini_file=''):
        self.lrunning = False
        self.ini_data = None
        self.subroutine_path = []
        self.user_m_path = None
        self.ini_file = None
        self.ngcgui_options = []
        self.editor = os.environ.get("VISUAL")
        use_ini_file = None

        l_ini_file = ''
        stat = linuxcnc.stat()


        try:
            global g_stat
            g_stat = linuxcnc.stat()
            g_stat.poll() # poll faults if linuxcnc not running
            self.lrunning = True
            l_ini_file = get_linuxcnc_ini_file()
        except linuxcnc.error as msg:
            g_stat = None
            print('INTFC:err:',msg)
            print('INTFC:' + _('Warning: linuxcnc not running'))

        print('%s:INTFC:linuxcnc running=%d' % (g_progname,self.lrunning))
        print('%s:INTFC:ini_file=<%s>' % (g_progname,l_ini_file))

        # cmdline_ini_file can be specified on cmdline and from intfc:
        # if neither ok: if no cmdline subfile, make custom page
        # if cmdonly ok
        # if runonly ok
        # if both    ok: warn message and continue

        if cmdline_ini_file:
            cmdline_spath = spath_from_inifile(cmdline_ini_file)
        if l_ini_file:
            l_spath = spath_from_inifile(l_ini_file)

        if not cmdline_ini_file and not l_ini_file:
            ini_file = None
            spath    = []
            #print('NEITHER')
        if not cmdline_ini_file and l_ini_file:
            ini_file = l_ini_file
            spath    = l_spath
            #print("OK running only <,",cmdline_ini_file,l_ini_file,">")
        if cmdline_ini_file and not l_ini_file:
            ini_file = cmdline_ini_file
            spath    = cmdline_spath
            #print('OK cmdline only')
        if cmdline_ini_file and l_ini_file:
            #print("INI file on both cmdline and running linuxcnc")
            msg = ""
            if os.path.abspath(cmdline_ini_file) != l_ini_file:
                ini_file = l_ini_file
                msg = (_('The ini file specified on cmdline') + ':\n'
                + os.path.abspath(cmdline_ini_file) + '\n\n'
                + _('is different from the one used by the running linuxcnc')
                + ':\n'
                + l_ini_file + '\n\n'
                )

            if cmdline_spath == l_spath:
                ini_file = cmdline_ini_file
                spath    = cmdline_spath
                msg = msg + _('Using cmd line INI file (same paths)')
            else:
                ini_file = l_ini_file
                spath    = l_spath
                msg = msg + _('Ignoring cmd line INI file (different paths)')

            user_message(mtype=Gtk.MessageType.WARNING
                        ,title=_('Warning')
                        ,msg=msg
                        )

        if ini_file:
            self.ini_file = ini_file
            self.ini_data = linuxcnc.ini(self.ini_file)
            # get it again to avoid (unlikely) race
            self.subroutine_path = spath_from_inifile(ini_file)
            self.ngcgui_options = self.ini_data.find('DISPLAY','NGCGUI_OPTIONS')

            self.editor = (   self.editor
                           or self.ini_data.find('DISPLAY','EDITOR'))

        # create at startup, refresh as required
        global g_candidate_files
        g_candidate_files = CandidateFiles(self.get_subroutine_path())


    def addto_spath(self,pathtoadd):
        if not isinstance(pathtoadd,list):
            raise ValueError(
                'addto_spath: List required not: %s %s'
                % (pathtoadd,type(pathtoadd)))
        # dont add duplicates
        if pathtoadd not in self.subroutine_path:
            self.subroutine_path.extend(pathtoadd)

    def get_editor(self):
        return self.editor or 'gedit'

    def get_ini_file(self):
        return(self.ini_file)

    def get_subroutine_path(self):
        return(self.subroutine_path)

    def get_user_m_path(self):
        return(self.user_m_path)

    def find_file_in_path(self,fname):
        # return tuple:
        #               '', 'NULLFILE' if fname None or ''
        #            fname, 'NOPATH'   no path defined (eg no INI file)
        #    foundfilename, 'FOUND'    found in path
        #            fname, 'NOTFOUND' not in path (may exist)
        if not fname:
            return('','NULLFILE')
        if not self.subroutine_path:
            return(fname,'NOPATH')
        bname = os.path.basename(fname) # only basename used
        foundlist = []
        foundfilename = None
        for p in self.subroutine_path:
            f = os.path.join(p,bname)
            if os.path.isfile(f):
                if not foundfilename:
                    foundfilename = f #first one wins
                foundlist.append(f)

        if len(foundlist) > 1:
            print(_('find_file_in_path:Multiple Results: %s') % foundlist)
            print(_('      Search path: %s') % self.subroutine_path)
        if foundfilename:
            vprint('find_file_in_path:%s' % foundfilename)
            return(foundfilename,'FOUND')
        print('find_file_in_path<%s> NOTFOUND' % fname)
        return(fname,'NOTFOUND')

    def get_subfiles(self):
        if self.ini_data:
            #returns list
            return(self.ini_data.findall('DISPLAY','NGCGUI_SUBFILE'))
        else:
            return(None)

    def get_preamble(self):
        if self.ini_data:
            return(self.ini_data.find('DISPLAY','NGCGUI_PREAMBLE'))
        else:
            return(None)

    def get_postamble(self):
        if self.ini_data:
            return(self.ini_data.find('DISPLAY','NGCGUI_POSTAMBLE'))
        else:
            return(None)

    def get_font(self):
        if self.ini_data:
            return(self.ini_data.find('DISPLAY','NGCGUI_FONT'))
        else:
            return(None)

    def get_ngcgui_options(self):
        return(self.ngcgui_options or [])

    def get_gcmc_include_path(self):
        dirs = (self.ini_data.find('DISPLAY','GCMC_INCLUDE_PATH'))
        return(dirs)

    def get_program_prefix(self):
        if self.ini_data:
            dir = self.ini_data.find('DISPLAY','PROGRAM_PREFIX')
            if not dir: return(None)
            dir = os.path.expanduser(dir)
            if not os.path.isabs(dir):
                # relative, base on inidir
                dir = os.path.join(os.path.dirname(self.ini_file),dir)
            return(dir)
        else:
            return(None)


class PreFile():
    """PreFile: preamble file data"""
    def __init__(self,thefile):
        self.pre_file = thefile
        self.read()

    def clear(self):
        self.pre_file = ''
        self.inputlines=[]

    def read(self):
        #print('PreFile read')

        self.md5 = None
        self.mtime = None
        self.inputlines = []
        if self.pre_file == "": return

        self.mtime = os.path.getmtime(self.pre_file)
        f = open(self.pre_file)
        for l in f.readlines():
            # dont include not_a_subfile lines
            if (l.find('not_a_subfile') < 0) and (l.strip() != ''):
                self.inputlines.append(l)
        f.close()
        self.md5 = md5sum(self.pre_file)

class PstFile():
    """PstFile: postamble file data"""
    def __init__(self,thefile):
        self.pst_file = thefile
        self.read()

    def clear(self):
        self.pst_file = ''
        self.inputlines = []

    def read(self):
        #print('PstFile read')
        self.md5 = None
        self.mtime = None
        self.inputlines = []

        if self.pst_file == "": return
        self.mtime = os.path.getmtime(self.pst_file)
        f = open(self.pst_file)
        for l in f.readlines():
            # dont include not_a_subfile lines
            if (l.find('not_a_subfile') < 0) and (l.strip() != ''):
                self.inputlines.append(l)
        f.close()
        self.md5 = md5sum(self.pst_file)


class SubFile():
    """SubFile: subfile data"""
    def __init__(self,thefile):
        self.sub_file = thefile
        self.min_num = sys.maxsize
        self.max_num = 0
        self.pdict = {} # named items:   pdict[keyword] = value
        self.ndict = {} # ordinal items: ndict[idx] = (name,dvalue,comment)
        self.ldict = {} # label items:   ldict[lno] = thelabel
        self.pdict['info'] = ''
        self.pdict['lastparm'] = 0
        self.pdict['subname'] = ''
        self.inputlines = []
        self.errlist=[]
        self.md5 = None
        self.mtime = None
        if self.sub_file == '': return

        self.mtime = os.path.getmtime(self.sub_file)
        self.md5 = md5sum(self.sub_file)

        if os.path.splitext(self.sub_file)[-1] in ['.ngc','.NGC','.nc','.NC']:
            self.read_ngc()
        elif os.path.splitext(self.sub_file)[-1] in ['.gcmc','.GCMC']:
            self.read_gcmc()
        else:
            user_message(mtype=Gtk.MessageType.ERROR
                    ,title=_('Unknown file suffix')
                    ,msg = _('Unknown suffix for: %s:')
                             % os.path.basename(self.sub_file)
                            )
            return

    def clear(self):
        self.sub_file = ''
        self.pdict = {}
        self.ndict = {}
        self.ldict = {}
        self.inputlines = []

    def flagerror(self,e):
        # accumulate errors from read() so entire file can be processed
        self.errlist.append(e)

    def specialcomments_ngc(self,s):
        if s.find(' FEATURE ')    >= 0 :
            self.flagerror(
            "Disallowed use of ngcgui generated file as Subfile")
        if s.find('not_a_subfile') >= 0 :
            self.flagerror(
            "marked (not_a_subfile)\nNot intended for use as a subfile")

    def re_read(self):
        if 'isgcmc' in self.pdict:
            self.read_gcmc()
        else:
            self.read_ngc()

    def read_ngc(self):

        thesubname = os.path.splitext(os.path.basename(self.sub_file))[0]

        f = open(self.sub_file)
        self.inputlines = [] # in case rereading
        for l in f.readlines():
            self.specialcomments_ngc(l) # for compat, check on unaltered line
            self.inputlines.append(l)
        idx = 1 # 1 based for labels ldict
        nextparm = 0
        subname = None
        endsubname = None
        for line in self.inputlines:
            # rs274: no whitespace, simplify with lowercase
            info = get_info_item(line) # check on unaltered line
            l = line.translate(' \t').lower()
            lineiscomment = is_comment(l)
            if info is not None: self.pdict['info'] = info
            sname = check_sub_start(l)
            if subname is not None and sname is not None:
                self.flagerror("Multiple subroutines in file not allowed")
            if subname is None and sname is not None:
                subname = sname
                if subname is not None and subname != thesubname:
                    self.flagerror("sub label "
                    "%s does not match subroutine file name" % thesubname)

            if endsubname is not None:
                if lineiscomment or (l.strip() == ''):
                    pass
                elif  l.find('m2') >= 0:
                    # linuxcnc ignores m2 after endsub in
                    # single-file subroutines
                    # mark as ignored here for use with expandsub option
                    self.inputlines[-1] = (';' + g_progname +
                                        ' ignoring: ' +  self.inputlines[-1])
                    pass
                else:
                    self.flagerror('file contains lines after subend:\n'
                                  '%s' % l)

            ename = check_sub_end(l)
            if subname is None and ename is not None:
                self.flagerror("endsub before sub %s" % ename)
            if subname is not None and ename is not None:
               endsubname = ename
               if endsubname != subname:
                   self.flagerror("endsubname different from subname")

            label = check_for_label(l)
            if label: self.ldict[idx] = label

            if (    subname is not None
                and endsubname is None
                and (not lineiscomment)):

                pparm,min,max= check_positional_parm_range(l
                               ,self.min_num,self.max_num)
                if pparm is not None and pparm > g_max_parm:
                    self.flagerror(
                      _('parm #%s exceeds config limit on no. of parms= %d\n')
                        % (pparm,g_max_parm))
                if pparm:
                    self.min_num = min
                    self.max_num = max

                # blanks required for this, use line not l
                name,pnum,dvalue,comment = find_positional_parms(line)
                if name:
                    self.ndict[pnum] = (name,dvalue,comment)
                    # require parms in sequence to minimize user errors
                    nextparm = nextparm + 1
                    if g_strict:
                        if pnum != nextparm:
                            self.flagerror(
                                _('out of sequence positional parameter'
                                  '%d expected: %d')
                                % (pnum, nextparm))
                    while pnum > nextparm:
                        makename = "#"+str(nextparm)
                        self.ndict[nextparm] = makename,"",makename
                        nextparm = nextparm + 1
                    self.pdict['lastparm'] = pnum
            idx = idx + 1
        f.close()

        if    subname is None: self.flagerror(_('no sub found in file\n'))
        if endsubname is None: self.flagerror(_('no endsub found in file\n'))

        if g_strict:
            if nextparm == 0: self.flagerror(_('no subroutine parms found\n'))

        self.pdict['subname'] = subname
        if self.pdict['info'] == '':
            self.pdict['info'] = 'sub: '+str(subname)
        if self.errlist:
            user_message(mtype=Gtk.MessageType.ERROR
                        ,title=_('Error for: %s ')
                                 % os.path.basename(self.sub_file)
                        ,msg = self.errlist)
            self.errlist.append('SUBERROR')
            raise ValueError(self.errlist)

    def read_gcmc(self):
        self.gcmc_opts = [] # list of options for gcmc
        pnum = 0
        f = open(self.sub_file)
        for l in f.readlines():
            rinfo = re.search(r'^ *\/\/ *ngcgui *: *info: *(.*)' ,l)
            if rinfo:
                #print 'info read_gcmc:g1:',rinfo.group(1)
                self.pdict['info'] = rinfo.group(1) # last one wins
                continue

            ropt = re.search(r'^ *\/\/ *ngcgui *: *(-.*)$' ,l)
            if ropt:
                gopt = ropt.group(1)
                gopt = gopt.split("//")[0] ;# trailing comment
                gopt = gopt.split(";")[0]  ;# convenience
                gopt = gopt.strip()        ;# leading/trailing spaces
                self.gcmc_opts.append(gopt)
                continue

            name = None
            dvalue =  None
            comment = ''
            r3 = re.search(r'^ *\/\/ *ngcgui *: *(.*?) *= *(.*?) *\, *(.*?) *$', l)
            r2 = re.search(r'^ *\/\/ *ngcgui *: *(.*?) *= *(.*?) *$', l)
            r1 = re.search(r'^ *\\/\\/ *ngcgui *: *\(.*?\) *$', l)
            if r3:
                name = r3.group(1)
                dvalue = r3.group(2)
                comment = r3.group(3)
            elif r2:
                name = r2.group(1)
                dvalue = r2.group(2)
            elif r1:
                print('r1-1 opt read_gcmc:g1:',r1.group(1))
                name = r1.group(1)

            if dvalue:
                # this is a convenience to make it simple to edit to
                # add a var without removing the semicolon
                #    xstart = 10;
                #    //ngcgui: xstart = 10;
                dvalue = dvalue.split(";")[0] # ignore all past a ;
            else:
                dvalue = ''

            if name:
                if comment == '':
                    comment = name
                pnum += 1
                self.ndict[pnum] = (name,dvalue,comment)

        self.pdict['isgcmc'] = True
        self.pdict['lastparm'] = pnum
        self.pdict['subname'] = os.path.splitext(os.path.basename(self.sub_file))[0]
        if self.pdict['info'] == '':
            self.pdict['info'] = 'gcmc: '+ self.pdict['subname']
        f.close()
        return True # ok

class FileSet():
    """FileSet: set of preamble,subfile,postamble files"""
    def __init__(self,pre_file
                     ,sub_file
                     ,pst_file
                ):
        # sub_file=='' is not an error, opens Custom
        self.pre_data = PreFile(pre_file)
        self.sub_data = SubFile(sub_file)
        self.pst_data = PstFile(pst_file)

class OneParmEntry():
    """OneParmEntry: one parameter labels and entry box"""
    def __init__(self,ltxt='ltxt' ,etxt='etxt' ,rtxt='rtxt'):

        self.box = Gtk.HBox()

        self.ll  = Gtk.Label()
        self.en  = Gtk.Entry()
        self.lr  = Gtk.Label()

        self.dv  = None

        ww = -1
        hh = g_entry_height

        self.ll.set_label(ltxt)
        self.ll.set_width_chars(2)
        self.ll.set_justify(Gtk.Justification.RIGHT)
        self.ll.set_alignment(xalign=.90,yalign=0.5) # right aligned
        self.ll.set_size_request(ww,hh)

        self.en.set_text(etxt)
        self.en.set_width_chars(6)
        self.en.set_alignment(xalign=.90) # right aligned
        self.en.set_size_request(ww,hh)
        self.en.hide()

        #self.en.connect("button-press-event",self.grabit)
        if g_popkbd is not None:
            if g_alive: self.en.connect("button-press-event",self.popkeyboard)

        if g_alive: self.en.connect('changed', self.entry_changed) #-->w + txt

        self.lr.set_label(rtxt)
        self.lr.set_width_chars(0) # allow any width for compat with ngcgui
        self.lr.set_justify(Gtk.Justification.LEFT)
        self.lr.set_alignment(xalign=0.2,yalign=0.5) # left aligned
        self.lr.set_size_request(ww,hh)
        self.lr.hide()
        mod_font_by_category(self.lr,'control')

        self.tbtns = Gtk.HBox(homogeneous=0,spacing=2)
        self.tbtns.set_border_width(0)

        self.box.pack_start(self.tbtns, expand=0, fill=0, padding=0)

        self.tbtns.pack_start(self.ll, expand=0, fill=0, padding=0)
        self.tbtns.pack_start(self.en, expand=0, fill=0, padding=0)
        self.tbtns.pack_start(self.lr, expand=0, fill=0, padding=0)

    def grabit(self,*args,**kwargs):
        #print 'grabit',self,args,kwargs
        print('\ngrabit:can_get_focus:',self.en.get_can_focus())
        self.en.grab_focus()
        print('grabit:has_focus',self.en.has_focus())
        print('grabit: is_focus',self.en.is_focus())

    def popkeyboard(self,widget,v):
        origtxt = self.en.get_text()
        title = '#%s, <%s> %s' % (self.ll.get_text()
                                 ,self.en.get_text()
                                 ,self.lr.get_text()
                                 )
        self.en.set_text('')
        if g_popkbd.run(initial_value='',title=title):
            self.en.set_text(g_popkbd.get_result())
        else:
            # user canceled
            self.en.set_text(origtxt)

    def entry_changed(self,w):
        v = w.get_text().lower()
        if g_stat:
            r = re.search('[xyzabcuvwd]',v)
            if r:
                char = r.group(0)
                try:
                    w.set_text("%.4f" % coord_value(char))
                except TypeError:
                    pass
                except Exception as detail:
                    exception_show(Exception,detail,'entry_changed')
                    pass

        if v == '':
            w.set_style(g_ent_style_normal)
            return
        else:
            try:
                float(v)
                w.set_style(g_ent_style_normal)
            except ValueError:
                w.set_style(g_ent_style_error)
                return
        try:
            if (    (self.dv is not None)
                and (float(v) == float(self.dv)) ):
                w.set_style(g_ent_style_default)
                return
        except ValueError:
            pass
        w.set_style(g_ent_style_normal)
        return

    def getentry(self):
        return(self.en.get_text())

    def setentry(self,v):
        self.en.set_text(v)

    def clear_pentry(self):
        self.ll.set_text('')
        self.en.set_text('')
        self.lr.set_text('')
        self.ll.hide()
        self.en.hide()
        self.lr.hide()

    def make_pentry(self,ll,dvalue,lr,emode='initial'):
        # modes 'initial'
        #       'keep'
        self.dv = dvalue
        if dvalue is None:
            en = ''
        else:
            en = dvalue

        if ll is None: ll=''
        if lr is None: lr=''
        self.ll.set_text(str(ll))

        if emode == 'initial':
            self.en.set_text(str(en))

        # on reread, may be new parms with no text so use default
        # if (emode == 'keep') and (not self.en.get_text()):
        if (emode == 'keep') and (self.en.get_text() is None):
            self.en.set_text(str(en))

        self.lr.set_text(str(lr))
        if dvalue is None or dvalue == '':
            self.en.set_style(g_ent_style_normal) # normal (not a dvalue)
        else:
            self.en.set_style(g_ent_style_default) # a dvalue

        self.ll.show()
        self.en.show()
        self.lr.show()
        self.entry_changed(self.en)


class EntryFields():
    """EntryFields: Positional Parameters entry fields in a frame """
    def __init__(self,nparms=INTERP_SUB_PARAMS):
        if nparms > g_max_parm:
            raise ValueError(_(
                  'EntryFields:nparms=%d g_max_parm=%d')
                  % (nparms,g_max_parm))
        self.ebox = Gtk.Frame()
        self.ebox.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        self.ebox.set_border_width(2)

        efbox = Gtk.VBox()
        evb = Gtk.VBox(homogeneous=0,spacing=2)
        xpositionalp = Gtk.Label('Positional Parameters')
        xpositionalp.set_alignment(xalign=0.0,yalign=0.5) # left aligned
        epositionalp = Gtk.EventBox()
        epositionalp.add(xpositionalp)
        epositionalp.modify_bg(Gtk.StateType.NORMAL,label_normal_color)
        lpositionalp = Gtk.Frame()
        lpositionalp.set_shadow_type(Gtk.ShadowType.IN)
        lpositionalp.set_border_width(0)
        lpositionalp.add(epositionalp)


        self.boxofcolumns = Gtk.HBox(homogeneous=0,spacing=2)

        evb.pack_start(lpositionalp,expand=0,fill=1,padding=0)
        evb.pack_start(self.boxofcolumns,   expand=1,fill=1,padding=4)

        efbox.pack_start(evb, expand=1,fill=1,padding=0)
        self.ebox.add(efbox)

        self.make_entryfields(nparms) # initialize for EntryFields

    def make_entryfields(self,nparms):
        self.no_of_entries = nparms
        # make VBoxes as required to accommodate entries
        # destroy them when starting over -- this occurs
        # when a OnePg is reused for a different subfile
        try:
            type(self.columnbox) # test for existence
            # destroy prior VBoxes packed in self.boxofcolumns
            for c in self.boxofcolumns.children():
                 self.boxofcolumns.remove(c)
                 c.destroy()
                 del(c)
        except AttributeError:
            # first-time: create initial VBox for entries
            self.columnbox = Gtk.VBox(homogeneous=0,spacing=2)

        self.boxofcolumns.pack_start(self.columnbox,expand=0,fill=0,padding=0)

        # try to use minimum height if less than 3 columns
        if nparms > 20:
            rowmax = 10
        else:
            rowmax  = int(nparms/2 + 0.5)

        self.pentries = {}
        row      = 0
        idx      = 1 # 1-based to agree with parm no.s
        for i in range(0,nparms):
            if row >= rowmax:
                row = 0
                # make a new VBox for next column of entries
                self.columnbox = Gtk.VBox(homogeneous=0,spacing=2)
                self.boxofcolumns.pack_start(self.columnbox
                                             ,expand=0,fill=0,padding=0)
            self.pentries[idx] = OneParmEntry('','','')
            self.columnbox.pack_start(self.pentries[idx].box
                                     ,expand=0,fill=0,padding=0)
            row += 1
            idx += 1
        self.boxofcolumns.show_all()

    def getentry_byidx(self,idx):
        return(self.pentries[idx].getentry())

    def clear_pentry_byidx(self,idx):
        self.pentries[idx].clear_pentry()

    def make_pentry_byidx(self,idx,ll,en,lr,emode='initial'):
        self.pentries[idx].make_pentry(ll,en,lr,emode)

    def getstuff_byidx(self,idx):
        print("1getstuff idx=",idx)
        self.pentries[idx].getstuff()

    def get_box(self):
        return self.ebox

    def clear_parm_entries(self):
        for pidx in range(1,self.no_of_entries+1):
            self.clear_pentry_byidx(pidx)

    def set_parm_entries(self,parms,emode='initial'):
        lastpidx = 0
        for pidx in sorted(parms.sub_data.ndict):
            name,dvalue,comment = parms.sub_data.ndict[pidx]
            self.make_pentry_byidx(pidx
                                                 ,str(pidx)
                                                 ,dvalue
                                                 ,comment
                                                 ,emode
                                                 )
            lastpidx = pidx


class TestButtons():
    """TestButtons: debugging buttons"""
    def __init__(self,mypg):
        self.box  = Gtk.HBox()
        self.mypg = mypg
        lbl       = Gtk.Label('Debug:')
        lbl.set_alignment(xalign=0.9,yalign=0.5) # rt aligned
        self.box.pack_start(lbl,expand=0,fill=0,padding=2)
        for item in ('info'
                    ,'intfc'
                    ,'nset'
                    ,'nb'
                    ,'page'
                    ,'fset'
                    ,'pre'
                    ,'sub'
                    ,'pst'
                    ,'ent'
                    ,'cp'
                    ,'lcnc'
                    ,'hal'
                    ,'pos'
                    ,'glo'
                    ,'loc'
                    ,'tst'
                    ):
            button = Gtk.Button(item)
            if g_alive: button.connect("clicked", self.btest, item)
            button.show_all()
            self.box.pack_start(button,expand=0,fill=0,padding=2)
        bclose = Gtk.Button('Close')
        if g_alive: bclose.connect("clicked", lambda x: self.delete())
        self.box.pack_start(bclose,expand=0,fill=0,padding=2)

    def btest(self,widget,v):
        m = self.mypg
        if   v == 'info':
            p = m.nset
            print('INFO--------------------------------------------------')
            print('       sys.argv = %s' % sys.argv)
            print('            cwd = %s' % os.getcwd())
            print('       sys.path = %s' % sys.path)
            print('       ini_file = %s' % p.intfc.get_ini_file())
            print('      auto_file = %s' % p.auto_file)
            print('subroutine_path = %s' % p.intfc.get_subroutine_path())
            print('    user_m_path = %s' % p.intfc.get_user_m_path())
            print('       pre_file = %s' % p.intfc.get_preamble())
            print('        sublist = %s' % p.intfc.get_subfiles())
            print('       pst_file = %s' % p.intfc.get_postamble())
            print('  startpage_idx = %s' % p.startpage_idx)
            print('')
            print('      __file__  = %s' % __file__)
            print('g_send_function = %s' % g_send_function)
            print('       g_popkbd = %s' % g_popkbd)
            print('         g_stat = %s' % g_stat)
            print('     g_progname = %s' % g_progname)
            print('      g_verbose = %s' % g_verbose)
            print('        g_debug = %s' % g_debug)
            print('        g_tmode = %s' % g_tmode)
            print('     g_label_id = %s' % g_label_id)
        elif v  == 'ent':
            print('ENTRIES--------------------------------------------------')
            x = m.efields.pentries
            pmax = m.fset.sub_data.pdict['lastparm']
            print('efields.pentries[]')
            for pidx in range(1,pmax+1):
                print("%2d: %4s %-8s %-20s" % (pidx
                                           ,x[pidx].ll.get_text()
                                           ,x[pidx].en.get_text()
                                           ,x[pidx].lr.get_text()
                                           ))
            print('ENTRIES==================================================')
        elif v == 'intfc': d = m.nset.intfc;    show_dir(d,tag='intfc')
        elif v == 'page':
            d = m;               show_dir(d,tag='mypg')
            x=self.mypg.efields.pentries[1].en
            print('x=',x)
            print('            has_focus:',x.has_focus())
            print('             is_focus:',x.is_focus())
            print('        get_can_focus:',x.get_can_focus())
        elif v == 'pre':   d = m.fset.pre_data; show_dir(d,tag='pre_data')
        elif v == 'sub':   d = m.fset.sub_data; show_dir(d,tag='sub_data')
        elif v == 'pst':   d = m.fset.pst_data; show_dir(d,tag='pst_data')
        elif v == 'fset':  d = m.fset;          show_dir(d,tag='fset')
        elif v == 'nset':  d = m.nset;          show_dir(d,tag='nset')
        elif v == 'cp':    d = m.cpanel;        show_dir(d,tag='cpanel')
        elif v == 'loc':   show_dir(locals(),tag='locals')
        elif v == 'glo':   show_dir(globals(),tag='globals')
        elif v == 'lcnc':  show_dir(linuxcnc,tag='lcnc')
        elif v == 'hal':   show_dir(hal,tag='hal')
        elif v == 'pos':   show_position()
        elif v == 'tst':
            print('cpanel size:',m.cpanel.box.size_request())
            print('mtable size:',m.mtable.size_request())
        elif v == 'nb':
            print('NB--------------------------------------------------')
            for pno in range(m.nset.startpage_idx
                            ,m.mynb.get_n_pages()):
                npage = m.mynb.get_nth_page(pno)
                pg    = m.nset.pg_for_npage[npage]
                ltxt  = pg.the_lbl.get_text()
                print('%10s %s' % (ltxt,pg))
            print('NB==================================================')
        else: print('btest unknown:',v)

    def delete(self):
        Gtk.main_quit()
        return False


class ControlPanel():
    """ControlPanel: Controls and image display"""
    def __init__(self
                ,mypg
                ,pre_file=''
                ,sub_file=''
                ,pst_file=''
                ):
        self.mypg = mypg
 
        frame = Gtk.Frame()
        frame.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        frame.set_border_width(2)
        self.box = frame
        
        cpbox  = Gtk.VBox()
        # fixed width so it doesn't change when switching tabs
        # fixed height to allow room for buttons below image
        #cpbox.set_size_request(g_image_width,g_image_height)

        bw = 1
        bpre = Gtk.Button(_('Preamble'))
        bpre.set_border_width(bw)
        mod_font_by_category(bpre)

        bsub = Gtk.Button(_('Subfile'))
        bsub.set_border_width(bw)
        mod_font_by_category(bsub)

        bpst = Gtk.Button(_('Postamble'))
        bpst.set_border_width(bw)
        mod_font_by_category(bpst)

        self.pre_entry = Gtk.Entry()
        self.pre_entry.set_state(Gtk.StateType.INSENSITIVE)

        self.sub_entry = Gtk.Entry()
        self.sub_entry.set_state(Gtk.StateType.INSENSITIVE)

        self.pst_entry = Gtk.Entry()
        self.pst_entry.set_state(Gtk.StateType.INSENSITIVE)

        chars=10

        self.pre_entry.set_width_chars(chars)
        self.pre_entry.set_alignment(xalign=0.1)
        self.pre_entry.set_text(os.path.basename(pre_file))
        if g_alive: self.pre_entry.connect("activate", self.file_choose, 'pre')

        self.sub_entry.set_width_chars(chars)
        self.sub_entry.set_alignment(xalign=0.1)
        self.sub_entry.set_text(os.path.basename(sub_file))
        if g_alive: self.sub_entry.connect("activate", self.file_choose, 'sub')

        self.pst_entry.set_width_chars(chars)
        self.pst_entry.set_alignment(xalign=0.1)
        self.pst_entry.set_text(os.path.basename(pst_file))
        if g_alive: self.pst_entry.connect("activate", self.file_choose, 'pst')

        xcontrol = Gtk.Label('Controls')
        xcontrol.set_alignment(xalign=0.0,yalign=0.5) # left aligned
        econtrol = Gtk.EventBox()
        econtrol.add(xcontrol)
        econtrol.modify_bg(Gtk.StateType.NORMAL,label_normal_color)
        lcontrol= Gtk.Frame()
        lcontrol.set_shadow_type(Gtk.ShadowType.IN)
        lcontrol.set_border_width(1)
        lcontrol.add(econtrol)

        tfiles = Gtk.Table(rows=3, columns=2, homogeneous=0)

        bx = Gtk.AttachOptions.FILL|Gtk.AttachOptions.EXPAND; by = 0

        tfiles.attach(bpre,0,1,0,1,xoptions=bx,yoptions=by)
        tfiles.attach(bsub,0,1,1,2,xoptions=bx,yoptions=by)
        tfiles.attach(bpst,0,1,2,3,xoptions=bx,yoptions=by)

        tfiles.attach(self.pre_entry,1,2,0,1,xoptions=bx,yoptions=by)
        tfiles.attach(self.sub_entry,1,2,1,2,xoptions=bx,yoptions=by)
        tfiles.attach(self.pst_entry,1,2,2,3,xoptions=bx,yoptions=by)

        if g_alive: bpre.connect("clicked", self.file_choose, 'pre')
        if g_alive: bsub.connect("clicked", self.file_choose, 'sub')
        if g_alive: bpst.connect("clicked", self.file_choose, 'pst')

        #bretain   = Gtk.CheckButton('Retain values on Subfile read')
        self.bexpand   = Gtk.CheckButton('Expand Subroutine')
        self.bexpand.set_active(self.mypg.expandsub)
        if g_alive: self.bexpand.connect("toggled", self.toggle_expandsub)

        self.bautosend = Gtk.CheckButton('Autosend')
        self.bautosend.set_active(self.mypg.autosend)
        if g_alive: self.bautosend.connect("toggled", self.toggle_autosend)

        tchkbs = Gtk.Table(rows=3, columns=1, homogeneous=0)
        bx = Gtk.AttachOptions.FILL|Gtk.AttachOptions.EXPAND; by = Gtk.AttachOptions.FILL|Gtk.AttachOptions.EXPAND
        #tchkbs.attach(bretain,  0,1,0,1,xoptions=bx,yoptions=by)
        tchkbs.attach(self.bexpand,  0,1,1,2,xoptions=bx,yoptions=by)

        nopts = self.mypg.nset.intfc.get_ngcgui_options()
        if (nopts is None) or ('noauto' not in nopts):
            tchkbs.attach(self.bautosend,0,1,2,3,xoptions=bx,yoptions=by)

        bw = 1

        bcreate   = Gtk.Button(_('Create Feature'))
        bcreate.set_border_width(bw)
        if g_alive: bcreate.connect("clicked", lambda x: self.create_feature())
        mod_font_by_category(bcreate)

        bfinalize = Gtk.Button(_('Finalize'))
        bfinalize.set_border_width(bw)
        if g_alive: bfinalize.connect("clicked"
                                     ,lambda x: self.finalize_features())
        mod_font_by_category(bfinalize)

        self.lfct = Gtk.Label(str(mypg.feature_ct))
        self.lfct.set_alignment(xalign=0.9,yalign=0.5) # right aligned
        mod_font_by_category(self.lfct)

        lfctf = Gtk.Frame()
        lfctf.set_shadow_type(Gtk.ShadowType.IN)
        lfctf.set_border_width(2)
        lfctf.add(self.lfct)

        self.breread   = Gtk.Button(_('Reread'))
        self.breread.set_border_width(bw)
        if g_alive: self.breread.connect("clicked"
                                        ,lambda x: self.reread_files())
        mod_font_by_category(self.breread)

        brestart  = Gtk.Button(_('Restart'))
        brestart.set_border_width(bw)
        if g_alive: brestart.connect("clicked"
                                    ,lambda x: self.restart_features())
        mod_font_by_category(brestart)

        self.lmsg = Gtk.Label(_('Ctrl-k for key shortcuts'))
        self.lmsg.set_alignment(xalign=0.05,yalign=0.5) # left aligned

        lmsgf = Gtk.Frame()
        lmsgf.set_shadow_type(Gtk.ShadowType.IN)
        lmsgf.set_border_width(2)
        lmsgf.add(self.lmsg)

        tactions = Gtk.Table(rows=3, columns=3, homogeneous=1)
        bx = Gtk.AttachOptions.FILL|Gtk.AttachOptions.EXPAND; by = Gtk.AttachOptions.FILL|Gtk.AttachOptions.EXPAND
        tactions.attach(bcreate,  0,2,0,1,xoptions=bx,yoptions=by)
        tactions.attach(bfinalize,2,3,0,1,xoptions=bx,yoptions=by)

        # only if image (see below)
        # tactions.attach(self.breread ,0,1,1,2,xoptions=bx,yoptions=by)
        tactions.attach(brestart,   2,3,1,2,xoptions=bx,yoptions=by)

        bx = Gtk.AttachOptions.FILL|Gtk.AttachOptions.EXPAND; by = 0
        #tactions.attach(self.lmsg,0,3,2,3,xoptions=bx,yoptions=by)
        tactions.attach(lmsgf,0,3,2,3,xoptions=bx,yoptions=by)

        nopts = self.mypg.nset.intfc.get_ngcgui_options()
        image_file = find_image(sub_file)
        if image_file:
            img = sized_image(image_file)
        if (    (not image_file)
             or (nopts is not None and 'noiframe' in nopts)
             or mypg.imageoffpage
           ):
            # show all controls
            bx = Gtk.AttachOptions.FILL|Gtk.AttachOptions.EXPAND; by = Gtk.AttachOptions.FILL|Gtk.AttachOptions.EXPAND
            tactions.attach(self.breread, 0,1,1,2,xoptions=bx,yoptions=by)
            tactions.attach(lfctf,        1,2,1,2,xoptions=bx,yoptions=by)
            cpbox.pack_start(lcontrol,expand=0,fill=0,padding=0)
            cpbox.pack_start(tfiles,  expand=0,fill=0,padding=0)
            cpbox.pack_start(tchkbs,  expand=0,fill=0,padding=0)
            if image_file:
                self.separate_image(img,sub_file,show=False)
                mypg.imageoffpage = True
        else:
            bx = Gtk.AttachOptions.FILL|Gtk.AttachOptions.EXPAND; by = Gtk.AttachOptions.FILL|Gtk.AttachOptions.EXPAND
            tactions.attach(lfctf,  0,2,1,2,xoptions=bx,yoptions=by)
            # show image instead of controls
            if image_file:
                cpbox.pack_start(img,expand=0,fill=0,padding=0)
                mypg.imageoffpage = False
        cpbox.pack_start(tactions,expand=1,fill=1,padding=0)
        cpbox.show()
        frame.add(cpbox)

    def separate_image(self,img,fname='',show=True):
        self.mypg.imgw = Gtk.Window(Gtk.WindowType.TOPLEVEL)
        w = self.mypg.imgw
        w.hide()
        w.iconify()
        w.set_title(os.path.basename(fname))
        w.add(img)
        if g_alive: w.connect("destroy",self.wdestroy)
        if show:
            w.show_all()
            w.deiconify()

    def wdestroy(self,widget):
        del self.mypg.imgw

    def set_message(self,msg):
        self.lmsg.set_label(msg)

    def reread_files(self):
        vprint('REREAD')
        # user can edit file and use button to reread it
        if self.mypg.sub_file == '':
            vprint('reread_files NULL subfile')
            return False
        self.mypg.fset.pre_data.read()
        self.mypg.fset.sub_data.re_read() # handle ngc or gcmc
        self.mypg.fset.pst_data.read()

        self.mypg.update_onepage('pre',self.mypg.pre_file)
        self.mypg.update_onepage('sub',self.mypg.sub_file)
        self.mypg.update_onepage('pst',self.mypg.pst_file)
        self.set_message(_('Reread files'))
        return True # success

    def restart_features(self):
        try:
            type(self.mypg.savesec) # test for existence
            self.mypg.savesec = []
        except AttributeError:
            pass
        self.mypg.feature_ct = 0
        self.lfct.set_label(str(self.mypg.feature_ct))
        self.mypg.savesec = []
        self.mypg.update_tab_label('default')
        self.set_message(_('Restart'))

    def toggle_autosend(self, widget):
        self.mypg.autosend = (0,1)[widget.get_active()]
        self.set_message(_('Toggle autosend %s ') % str(self.mypg.autosend))

    def toggle_expandsub(self, widget):
        self.mypg.expandsub = (0,1)[widget.get_active()]
        self.set_message(_('Toggle expandsub %s') % str(self.mypg.expandsub))

    def checkb_toggle(self, widget, var):
        print('1T',var,type(var))
        var = (0,1)[widget.get_active()]
        print('2T',var,type(var))

    def create_feature(self):
        m=self.mypg
        p=self.mypg.fset

        fpre,fprestat = m.nset.intfc.find_file_in_path(m.pre_file)
        fsub,fsubstat = m.nset.intfc.find_file_in_path(m.sub_file)
        fpst,fpststat = m.nset.intfc.find_file_in_path(m.pst_file)

        if fsubstat == 'NULLFILE':
            vprint('create_feature: NULLFILE')
            return
        # the test for NOPATH is for special cases
        if (   (fpre != p.pre_data.pre_file) and fprestat != 'NOPATH'
            or (fsub != p.sub_data.sub_file) and fsubstat != 'NOPATH'
            or (fpst != p.pst_data.pst_file) and fpststat != 'NOPATH'
            ):
            print('\nUSER changed filename entry without loading\n')

        try:
            type(self.mypg.savesec) # test for existence
        except AttributeError:
            self.mypg.savesec = []


        self.set_message(_('Create feature'))
        # update for current entry filenames
        p.pre_data = PreFile(m.pre_file) # may be ''
        p.sub_data = SubFile(m.sub_file) # error for ''
        p.pst_data = PstFile(m.pst_file) # may be ''

        if 'isgcmc' in p.sub_data.pdict:
            stat = self.savesection_gcmc()
        else:
            stat = self.savesection_ngc()

        if stat:
            if m.feature_ct > 0:
                self.mypg.update_tab_label('multiple')
            else:
                self.mypg.update_tab_label('created')

            m.feature_ct = m.feature_ct + 1
            self.lfct.set_label(str(m.feature_ct))

            self.set_message(_('Created Feature #%d') % m.feature_ct)
        else:
            #print "savesection fail"
            pass

    def savesection_ngc(self):
        m=self.mypg
        p=self.mypg.fset
        force_expand = False
        # if file not in path and got this far, force expand
        fname,stat = m.nset.intfc.find_file_in_path(m.sub_file)

        if stat == 'NOTFOUND':
            force_expand = True
            user_message(mtype=Gtk.MessageType.INFO
                ,title=_('Expand Subroutine')
                ,msg=_('The selected file') + ':\n\n'
                + '%s\n\n'
                + _('is not in the linuxcnc path\n'
                    'Expanding in place.\n\n'
                    'Note: linuxcnc will fail if it calls\n'
                    'subfiles that are not in path\n')
                % fname)

        try:
            self.mypg.savesec.append(
                     SaveSection(mypg     = self.mypg
                                ,pre_info = p.pre_data
                                ,sub_info = p.sub_data
                                ,pst_info = p.pst_data
                                ,force_expand = force_expand
                                )
                     )
        except ValueError:
            dprint('SAVESECTION_ngc: failed')
        return True # success

    def savesection_gcmc(self):
        m=self.mypg
        p=self.mypg.fset
        intfc = self.mypg.nset.intfc

        global g_gcmc_exe
        if g_gcmc_exe is None:
            if not find_gcmc():
                return False ;# fail
        xcmd = []
        xcmd.append(g_gcmc_exe)

        global g_gcmc_funcname
        global g_gcmc_id
        g_gcmc_id += 1
        # gcmc chars in funcname: (allowed: [a-z0-9_-])
        funcname = "%s_%02d"%(g_gcmc_funcname,g_gcmc_id)

        p.sub_data.pdict['subname'] = funcname

        include_path = intfc.get_gcmc_include_path()
        if include_path is not None:
            for dir in include_path.split(":"):
                xcmd.append("--include")
                xcmd.append(os.path.expanduser(dir))
        # maybe: xcmd.append("--include")
        # maybe: xcmd.append(os.path.dirname(m.sub_file))
        # note: gcmc also adds the current directory
        #       to the search path as last entry.

        outdir = g_searchpath[0] # first in path
        ofile = os.path.join(outdir,funcname) + ".ngc"

        xcmd.append("--output")
        xcmd.append(ofile)

        xcmd.append('--gcode-function')
        xcmd.append(funcname)

        for opt in p.sub_data.gcmc_opts:
            splitopts = opt.split(' ')
            xcmd.append(str(splitopts[0]))
            if len(splitopts) > 1:
                xcmd.append(str(splitopts[1])) # presumes only one token


        for k in list(p.sub_data.ndict.keys()):
            #print 'k=',k,p.sub_data.ndict[k]
            name,dvalue,comment = p.sub_data.ndict[k]
            # make all entry box values explicitly floating point
            try:
                fvalue = str(float(m.efields.pentries[k].getentry()))
            except ValueError:
                user_message(mtype=Gtk.MessageType.ERROR
                    ,title='gcmc input ERROR'
                    ,msg=_('<%s> must be a number' % m.efields.pentries[k].getentry())
                    )
                return False ;# fail
            xcmd.append('--define=' + name + '=' + fvalue)

        xcmd.append(m.sub_file)
        print("xcmd=",xcmd)
        e_message = ".*Runtime message\(\): *(.*)"
        e_warning = ".*Runtime warning\(\): *(.*)"
        e_error   = ".*Runtime error\(\): *(.*)"

        s = subprocess.Popen(xcmd
                             ,stdout=subprocess.PIPE
                             ,stderr=subprocess.PIPE
                             )
        sout,eout = s.communicate()
        m_txt = ""
        w_txt = ""
        e_txt = ""
        compile_txt = ""

        if eout:
            if (len(eout) > g_max_msg_len):
                # limit overlong, errant msgs
                eout = eout[0:g_max_msg_len] + "..."
            for line in eout.split("\n"):
                r_message = re.search(e_message,line)
                r_warning = re.search(e_warning,line)
                r_error = re.search(e_error,line)
                if r_message:
                    m_txt += r_message.group(1) + "\n"
                elif r_warning:
                    w_txt += r_warning.group(1) + "\n"
                elif r_error:
                    e_txt += r_error.group(1) + "\n"
                else:
                    compile_txt += line

        if m_txt != "":
            user_message(mtype=Gtk.MessageType.INFO
                ,title='gcmc INFO'
                ,msg="gcmc File:\n%s\n\n%s"%(m.sub_file,m_txt)
                )
        if w_txt != "":
            user_message(mtype=Gtk.MessageType.WARNING
                ,title='gcmc WARNING'
                ,msg="gcmc File:\n%s\n\n%s"%(m.sub_file,w_txt)
                )
        if e_txt != "":
            user_message(mtype=Gtk.MessageType.ERROR
                ,title='gcmc ERROR'
                ,msg="gcmc File:\n%s\n\n%s"%(m.sub_file,e_txt)
                )
        if compile_txt != "":
            user_message(mtype=Gtk.MessageType.ERROR
                ,title='gcmc Compile ERROR'
                ,msg="gcmc File:%s"%(compile_txt)
                )
        if s.returncode:
            return False ;# fail

        self.mypg.savesec.append(
                 SaveSection(mypg     = self.mypg
                            ,pre_info = p.pre_data
                            ,sub_info = p.sub_data
                            ,pst_info = p.pst_data
                            ,force_expand = False # never for gcmc
                            )
                 )
        return True # success

    def finalize_features(self):
        mypg = self.mypg
        nb   = self.mypg.mynb
        nset = self.mypg.nset
        if mypg.feature_ct <= 0:
            msg = _('No features specified on this page')
            self.set_message(msg)
            user_message(mtype=Gtk.MessageType.WARNING
                    ,title='No Features'
                    ,msg=msg)
            return

        if len(mypg.savesec) == 0:
            msg = 'finalize_features: Unexpected: No features'
            self.set_message(_('No features'))
            raise ValueError(msg)
            return
        txt = ''
        plist = []
        sequence = ""
        # these are in left-to-right order
        for pno in range(nset.startpage_idx,nb.get_n_pages()):
            npage = nb.get_nth_page(pno)
            #Using EventBox for tabpage labels: dont use get_tab_label_text()
            pg = nset.pg_for_npage[npage]
            ltxt = pg.the_lbl.get_text()
            howmany = len(pg.savesec)
            if howmany > 0:
                plist.append(pg)
                sequence = sequence + " " + ltxt
                txt = txt + "%s has %d features\n" % (ltxt,howmany)
        vprint(txt)

        if len(plist) > 1:
            msg = (_('Finalize all Tabs?\n\n'
                     'No:     Current page only\n'
                     'Yes:    All pages\n'
                     'Cancel: Nevermind\n\n'
                     'Order:'
                    )
                  + '\n<' + sequence + '>\n\n'
                     'You can Cancel and change the order with the\n'
                     'Forward and Back buttons\n'
                  )
            popup = Gtk.Dialog(title='Page Selection'
                  ,parent=None
                  ,flags=Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT
                  ,buttons=(Gtk.STOCK_NO,     Gtk.ResponseType.NO
                           ,Gtk.STOCK_YES,    Gtk.ResponseType.YES
                           ,Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL
                           )
                  )
            finbox = popup.get_content_area()
            l = Gtk.Label(msg)
            finbox.pack_start(l,expand=0,fill=0,padding=0)
            popup.show_all()
            ans = popup.run()
            popup.destroy()
            if   ans == Gtk.ResponseType.YES:
                pass # use plist for all pages
            elif ans == Gtk.ResponseType.NO:
                pno = self.mypg.mynb.get_current_page()
                npage = nb.get_nth_page(pno)
                plist = [nset.pg_for_npage[npage]]
            elif (   ans == Gtk.ResponseType.CANCEL
                  or ans == Gtk.ResponseType.DELETE_EVENT): # window close
                return # do nothing
            else:
                raise ValueError('finalize_features:unknown ans<%d>'%ans)

        # make a unique filename
        # (avoids problems with gremlin ignoring new file with same name)
        global g_auto_file_ct
        autoname = nset.auto_file
        dirname = os.path.realpath(os.path.dirname(autoname))
        basename = str(g_auto_file_ct) + "." + os.path.basename(autoname)
        tmpname  = os.path.join(dirname,basename)
        if os.path.exists(tmpname):
            os.remove(tmpname)
        # hack: alternate names (0,1) to force gremlin file loading
        #       and touchy filechooser updates
        g_auto_file_ct = (g_auto_file_ct + 1)%2
        basename = str(g_auto_file_ct) + "." + os.path.basename(autoname)
        tmpname  = os.path.join(dirname,basename)
        self.mypg.nset.last_file = tmpname

        savename = None
        f = open(tmpname,'w')
        nopts = self.mypg.nset.intfc.get_ngcgui_options()
        if (('nom2' in nopts) or g_nom2):
            f.write("%\n")
            f.write("(%s: nom2 option)\n" % g_progname)

        featurect = 0; features_total=0
        for pg in plist:
            features_total = features_total + len(pg.savesec)
        for pg in plist:
            ct = self.write_to_file(f,pg,featurect,features_total)
            featurect += ct
            pg.feature_ct = 0
            self.lfct.set_label(str(pg.feature_ct))
            pg.savesec = []

        if (('nom2' in nopts) or g_nom2):
            f.write("%\n")
        else:
            f.write("(%s: m2 line added) m2 (g54 activated)\n" % g_progname)
        f.close()

        user_must_save = True # disprove with send_function
        title_message = ''
        if self.mypg.autosend:
            if g_send_function(tmpname):
                user_must_save = False
                self.set_message(_('Finalize: Sent file'))
                save_a_copy(tmpname)
                print('%s:SENT: %s' % (g_progname,tmpname))
                print('%s:SENT:using: %s' % (g_progname,g_send_function.__name__))
            else:
                title_message = (
                  _('Sending file failed using function: <%s>, user must save')
                  % g_send_function.__name__)
                self.set_message(_('Finalize: Sent file failed'))
                print('%s:SAVEDFILE: after send failed: %s'
                     % (g_progname,tmpname))

        if user_must_save:
            fname  = os.path.abspath(nset.auto_file)
            if self.mypg.nset.last_file is not None:
                fname = self.mypg.nset.last_file # last user choice
            savename = file_save(fname,title_message) # user may change name
            if savename is not None:
                shutil.move(tmpname,savename)
                save_a_copy(savename)
                self.mypg.nset.last_file = savename

        for pg in plist:
            pg.cpanel.restart_features()
            pg.update_tab_label('default')

        global g_label_id
        g_label_id = 0 # reinitialize
        return

    def write_to_file(self,file,pg,featurect,features_total):
        ct = 0
        for i in range(0,len(pg.savesec) ):
            ct += 1
            for l in pg.savesec[i].sdata:
                if l.find("#<_feature:>") == 0:
                    file.write(
                      "(%s: feature line added) #<_feature:> = %d\n"\
                      % (g_progname,featurect))
                    featurect += 1
                    file.write(
                      "(%s: remaining_features line added) "
                      " #<_remaining_features:> = %d\n"\
                      % (g_progname,features_total - featurect))
                else:
                    file.write(l)
        return(ct)

    def file_choose(self,widget,ftype):
        mydiag = CandidateDialog(ftype=ftype)

        while True:
            response   = mydiag.run()
            fname,errmsg = mydiag.get_file_result()
            if   response == Gtk.ResponseType.ACCEPT:
                vprint('file_choose: ACCEPT')
                self.mypg.cpanel.set_message(_('file_choose ACCEPT'))
                pass
            elif response == Gtk.ResponseType.REJECT:
                self.mypg.cpanel.set_message(_('file_choose REJECT'))
                vprint('file_choose: REJECT')
                mydiag.destroy()
                return None
            elif response == Gtk.ResponseType.NO:
                self.mypg.cpanel.set_message(_('No File'))
                fname = 'nofile' # allow pre,pst nofile
                vprint('file_choose: No File')
            else:
                self.mypg.cpanel.set_message(_('file_choose OTHER'))
                mydiag.destroy()
                raise ValueError(_('file_choose OTHER %s') % str(response))
                return None

            if fname == 'TRYAGAIN':
                user_message(mtype=Gtk.MessageType.INFO
                            ,title=_('Try Again')
                            ,msg=errmsg
                            )
                continue
            break
        mydiag.destroy()

        if   ftype == 'pre':
            self.mypg.fset.pre_file = fname
        elif ftype == 'sub':
             self.mypg.fset.sub_file = fname
        elif ftype == 'pst':
             self.mypg.fset.pst_file = fname
        else:
            raise ValueError("file_choose ftype?").with_traceback(ftype)

        # None for no file selected, null out field could be useful
        if not fname:
            self.mypg.cpanel.set_message(_('file_choose no file?'))
            return None

        if   ftype == 'pre':
            if fname == 'nofile':
                fname = ''
            self.pre_entry.set_text(os.path.basename(fname))
            self.mypg.update_onepage('pre',fname)
        elif ftype == 'sub':
            image_file = find_image(fname)
            if image_file:
                img = sized_image(image_file)
                self.separate_image(img,fname,show=True)
                self.mypg.imageoffpage = True
            if self.mypg.update_onepage('sub',fname):
                self.sub_entry.set_text(os.path.basename(fname))
        elif ftype == 'pst':
            if fname == 'nofile':
                fname = ''
            self.pst_entry.set_text(os.path.basename(fname))
            self.mypg.update_onepage('pst',fname)
        else:
            raise ValueError('file_choose:Unexpected ftype <%s>' %ftype)

        self.mypg.cpanel.set_message(_('Read %s') % os.path.basename(fname))
        return


class OnePg():
    """OnePg: ngcgui info for one tab page"""
    def __init__(self
                ,pre_file
                ,sub_file
                ,pst_file
                ,mynb
                ,nset
                ,imageoffpage=False
                ):

        self.imageoffpage   = imageoffpage # for clone of Custom pages
        self.garbagecollect = False
        self.key_enable     = False

        self.pre_file,stat = nset.intfc.find_file_in_path(pre_file)
        self.sub_file,stat = nset.intfc.find_file_in_path(sub_file)
        self.pst_file,stat = nset.intfc.find_file_in_path(pst_file)

        self.nset = nset
        self.mynb = mynb

        self.autosend = nset.autosend
        self.expandsub = nset.expandsub

        self.feature_ct = 0
        self.savesec = []

        self.cpanel = ControlPanel(mypg=self
                                  ,pre_file=self.pre_file
                                  ,sub_file=self.sub_file
                                  ,pst_file=self.pst_file
                                  )

        bw = 1

        #bremove = Gtk.Button(_('Remove'))
        bremove = Gtk.Button(stock=Gtk.STOCK_DELETE)
        bremove.set_border_width(bw)
        if g_alive: bremove.connect("clicked", lambda x: self.remove_page())

        #bclone = Gtk.Button(_('Clone'))
        bclone = Gtk.Button(stock=Gtk.STOCK_ADD)
        bclone.set_border_width(bw)
        if g_alive: bclone.connect("clicked", lambda x: self.clone_page())

        #bnew = Gtk.Button(_('New'))
        bnew = Gtk.Button(stock=Gtk.STOCK_NEW)
        bnew.set_border_width(bw)
        if g_alive: bnew.connect("clicked", lambda x: self.new_empty_page())

        #bmoveleft = Gtk.Button(_('<==Move'))
        bmoveleft = Gtk.Button(stock=Gtk.STOCK_GO_BACK,label='')
        bmoveleft.set_border_width(bw)
        if g_alive: bmoveleft.connect("clicked", lambda x: self.move_left())

        #bmoveright = Gtk.Button(_('Move==>'))
        bmoveright = Gtk.Button(stock=Gtk.STOCK_GO_FORWARD,label='')
        bmoveright.set_border_width(bw)
        if g_alive: bmoveright.connect("clicked", lambda x: self.move_right())

        # stock buttons notwork with mod_font_by_category
        #mod_font_by_category(bremove)
        #mod_font_by_category(bclone)
        #mod_font_by_category(bnew)
        #mod_font_by_category(bmoveleft)
        #mod_font_by_category(bmoveright)

        tabarrange_buttons = Gtk.HBox()        # main buttons

        self.mtable = Gtk.Table(rows=1, columns=2, homogeneous=0)
        bx = Gtk.AttachOptions.FILL|Gtk.AttachOptions.EXPAND; by = 0
        no_of_parms = g_max_parm


        self.make_fileset()
        no_of_parms = self.fset.sub_data.pdict['lastparm']

        self.efields = EntryFields(no_of_parms) # uses MultipleParmEntries item

        self.fill_entrypage(emode='initial')

        bx = 0; by = Gtk.AttachOptions.FILL|Gtk.AttachOptions.EXPAND
        self.mtable.attach(self.cpanel.box, 0,1,0,1,xoptions=bx,yoptions=by)

        bx = Gtk.AttachOptions.FILL; by = Gtk.AttachOptions.FILL|Gtk.AttachOptions.EXPAND
        bx = Gtk.AttachOptions.FILL|Gtk.AttachOptions.EXPAND ; by = Gtk.AttachOptions.FILL|Gtk.AttachOptions.EXPAND
        entrystuff = self.efields.get_box()
        self.mtable.attach(entrystuff, 1,2,0,1,xoptions=bx,yoptions=by)

        tbtns = TestButtons(mypg=self) # TestButtons

        nopts = nset.intfc.get_ngcgui_options()

        if (nopts is None) or ('noremove' not in nopts):
            tabarrange_buttons.pack_start(bremove,expand=0,fill=0,padding=0)

        if (nopts is None) or ('nonew' not in nopts):
            tabarrange_buttons.pack_start(bclone,expand=0,fill=0,padding=0)
            tabarrange_buttons.pack_start(bnew,expand=0,fill=0,padding=0)

        tabarrange_buttons.pack_start(bmoveleft,expand=0,fill=0,padding=0)
        tabarrange_buttons.pack_start(bmoveright,expand=0,fill=0,padding=0)

        op_box = Gtk.VBox()

        if g_tab_controls_loc == 'top':
            op_box.pack_start(tabarrange_buttons,expand=0,fill=0,padding=0)
        elif g_tab_controls_loc == 'bottom':
            op_box.pack_end(tabarrange_buttons,expand=0,fill=0,padding=0)
        else:
            raise ValueError(g_progname
                  + ' unknown tab_controls_loc %s' % g_tab_controls_loc)

        op_box.pack_start(self.linfof, expand=0,fill=0,padding=0)
        op_box.pack_start(self.mtable, expand=1,fill=1,padding=0)

        ct=0
        if g_debug:
            op_box.pack_end(tbtns.box,  expand=0,fill=0,padding=5)
        op_box.show_all()

        self.pgbox = Gtk.EventBox()
        self.pgbox.add(op_box)
        self.pgbox.show_all()

        if g_alive: self.pgbox.connect('event',self.any_event)

        # establish size with max no of entries

        #wip: gtk3 notworking:
        #     "cannot unpack non-iterable Requisition object
        #ww,hh = self.mtable.size_request()
        #print('size for mtable:',ww,hh)
        #self.mtable.set_size_request(ww,hh)

        lastpidx = self.fset.sub_data.pdict['lastparm']

        GObject.timeout_add_seconds(g_check_interval,self.periodic_check)

    def periodic_check(self):
        try:
            for i in ('pre','sub','pst'):
                o_entry = getattr(self.cpanel,i + '_entry')
                if o_entry.get_text().strip() == '': continue
                o_file  = getattr(self,      i + '_file')
                o_data  = getattr(self.fset, i + '_data')
                o_md5   = getattr(o_data,        'md5')
                o_mtime = getattr(o_data,        'mtime')
                if (    (o_mtime != None)
                    and (o_mtime == os.path.getmtime(o_file))):
                    state = o_entry.get_state()
                    o_entry.modify_text(state,black_color)
                    continue

                if (o_md5 != md5sum(o_file)):
                    #print('%s,%s>' % (o_md5,md5sum(o_file)))
                    #print(i,'CHANGED md5',o_file,o_md5)
                    state = o_entry.get_state()
                    o_entry.modify_text(state,purple_color)
                else:
                    #print(i,'SAME md5',o_file,o_md5)
                    o_entry.modify_text(Gtk.StateType.NORMAL,black_color)
        except OSError as detail:
            print((_('%s:periodic_check:OSError:%s') % detail))
            pass # continue without checks after showing message
        except Exception as detail:
            exception_show(Exception,detail,'periodic_check')
            raise Exception(detail) # reraise
        if self.garbagecollect:
            return False # False to norepeat (respond to del for self)
        return True      # True to repeat

    def any_event(self,widget,event):
        if   event.type == Gdk.EventType.ENTER_NOTIFY:
            #widget.set_can_focus(True)
            self.key_enable = True
            #print('ENTER enable')
            return
        elif event.type == Gdk.EventType.LEAVE_NOTIFY:
            #print "LEAVE can, is",widget.is_focus(),widget.get_can_focus(),'\n'
            if widget.get_can_focus():
                #widget.set_can_focus(False)
                self.key_enable = False
                #print('LEAVE disable')
            return
        elif event.type == Gdk.EventType.EXPOSE:
            widget.grab_focus()
            return
        elif event.type == Gdk.EventType.KEY_PRESS:
            if not self.key_enable:
                #print('IGNORE')
                return
            keyname = Gdk.keyval_name(event.keyval)
            kl = keyname.lower()
            # ignore special keys (until they modify)
            if kl in ['alt_r','alt_l']         : return
            if kl in ['control_r','control_l'] : return
            if kl in ['shift_r','shift_l']     : return
            pre = ''
            if  event.state & Gdk.ModifierType.CONTROL_MASK:
                pre = "Control-"
            elif event.state & Gdk.ModifierType.MOD1_MASK:
                pre = "Alt-"
            elif event.state & Gdk.ModifierType.SHIFT_MASK:
                pre = "Shift-"
            k = pre + keyname
            #print("%10s (%03d=%#2X)" % (k, event.keyval,event.keyval))
            self.handle_key(k)
            return False # allow other handlers

    def handle_key(self,k):
        if k == 'Control-d':
            self.make_fileset()
            self.fill_entrypage(emode='initial')
        if k == 'Control-a':
            self.cpanel.bautosend.clicked()
        if k == 'Control-#':
            self.cpanel.bexpand.clicked()
        if k == 'Control-k':
            self.show_special_keys()
        if k == 'Control-r':
            # was ctrl-p,P,r in ngcgui
            self.cpanel.breread.clicked()
        if k == 'Control-e':
            self.edit_any_file(self.nset.last_file,'last')
        if k == 'Control-E':
            self.cpanel.bexpand.clicked()
        if k == 'Control-u':
            self.edit_std_file('sub')
        if k == 'Control-U':
            self.edit_std_file('pre')
        #else:
        #    print('handle_key: k=',k)
        return False # False: allow more handlers

    def edit_any_file(self,fname,ftype=''):
        if not fname:
            user_message(mtype=Gtk.MessageType.ERROR
                        ,title=_('No file')
                        ,msg=_('No %s file specified') % ftype
                        )
            return
        subprocess.Popen([self.nset.intfc.editor, fname])

    def edit_std_file(self,which):
        o_file  = getattr(self, which + '_file')
        self.edit_any_file(o_file,which)

    #NB some key bindings are claimed on touchy
    def show_special_keys(self):
        msg = []
        msg.append('Control-a  ' + _('Toggle autosend') + '\n')
        msg.append('Control-e  ' + _('Edit last result file') + '\n')
        msg.append('Control-E  ' + _('Toggle expandsubroutines') + '\n')
        msg.append('Control-d  ' + _('Set Entry defaults') + '\n')
        msg.append('Control-k  ' + _('Show keys (this)') + '\n')
        msg.append('Control-r  ' + _('Reread files') + '\n')
        msg.append('Control-u  ' + _('Edit sub file') + '\n')
        msg.append('Control-U  ' + _('Edit preamble file') + '\n')
        user_message(mtype=Gtk.MessageType.INFO
                    ,title=_('Special Keys')
                    ,flags=0 #still MODAL ??
                    ,msg=msg)

    def set_page_label(self,lbl):
        self.lbl = lbl

    def save_onepage_tablabel(self,eb_lbl,the_lbl):
        self.eb_lbl  = eb_lbl
        self.the_lbl = the_lbl

    def update_tab_label(self,umode):
        if   umode == 'created':
            newcolor = fg_created_color
            newstyle = g_lbl_style_created
        elif umode == 'multiple':
            newcolor = fg_multiple_color
            newstyle = g_lbl_style_multiple
        elif umode == 'default':
            newcolor = fg_normal_color
            newstyle = g_lbl_style_default
        else:
            newstyle = g_lbl_style_default
            newcolor = fg_normal_color

        self.eb_lbl.set_style(newstyle)
        self.the_lbl.modify_fg(Gtk.StateType.NORMAL, newcolor)
        self.the_lbl.modify_fg(Gtk.StateType.ACTIVE, newcolor)

    def make_fileset(self):
        try:
            self.fset = FileSet(pre_file=self.pre_file
                               ,sub_file=self.sub_file
                               ,pst_file=self.pst_file
                               )
        except OSError as detail:
            print(_('%s:make_fileset:%s' % (g_progname,detail) ))
            raise OSError(detail) # reraise

    def fill_entrypage(self,emode='initial'):
        self.efields.set_parm_entries(self.fset,emode)

        try:
            type(self.info_label) # test for existence
        except AttributeError:
            self.info_label = Gtk.Label()
            self.linfof = Gtk.Frame()
            self.linfof.set_shadow_type(Gtk.ShadowType.IN)
            self.linfof.set_border_width(2)
            self.linfof.add(self.info_label)

        self.info_label.set_label(self.fset.sub_data.pdict['info'])
        self.info_label.set_alignment(xalign=0.0,yalign=0.5) # left aligned
        self.cpanel.set_message(_('Set Entry defaults'))

    def clear_entrypage(self):
        self.efields.clear_parm_entries()
        self.info_label.set_label('')

    def update_onepage(self,type,fname):
        vprint('UPDATE_PAGE  %s file=%s' % (type,fname))
        if   type == 'pre':
            foundname,stat = self.nset.intfc.find_file_in_path(fname)
            if stat == 'NOTFOUND':
                 self.clear_entries('pre')
                 return
            self.pre_file = foundname
            self.fset.pre_data = PreFile(self.pre_file)
        elif type == 'sub':
            foundname,stat = self.nset.intfc.find_file_in_path(fname)
            if stat == 'NOTFOUND':
                 self.clear_entries('sub')
                 return
            self.sub_file = foundname
            try:
                self.make_fileset()
                lastparm = self.fset.sub_data.pdict['lastparm']
                self.efields.make_entryfields(lastparm) # update_onepage
                self.fill_entrypage()
                self.info_label.set_label(self.fset.sub_data.pdict['info'])
                lbltxt = self.fset.sub_data.pdict['subname']
                lbltxt = self.nset.make_unique_tab_name(lbltxt)
                self.the_lbl.set_text(lbltxt)
                return True
            except Exception as detail:
                exception_show(Exception,detail,'update_onepage')
                return False
        elif type == 'pst':
            foundname,stat = self.nset.intfc.find_file_in_path(fname)
            if stat == 'NOTFOUND':
                 self.clear_entries('pst')
                 return
            self.pst_file = foundname
            self.fset.pst_data = PstFile(self.pst_file)
        else:
            raise ValueError('update_onepage unexpected type <%s>' % type)

        return

    def clear_entries(self,fmode):
        if   fmode == 'pre':
            self.pre_file = ''
            self.cpanel.pre_entry.set_text('')
            self.fset.pre_data.clear()
        elif fmode == 'sub':
            self.sub_file = ''
            self.cpanel.sub_entry.set_text('')
            self.clear_entrypage()
            self.fset.sub_data.clear()
        elif fmode == 'pst':
            self.pst_file = ''
            self.cpanel.pst_entry.set_text('')
            self.fset.pst_data.clear()
        else:
            raise ValueError('clear_entries:unexpected fmode= %s' % fmode)

    def move_left(self):
        page_idx = self.mynb.get_current_page()
        page_ct = self.mynb.get_n_pages()
        page = self.mynb.get_nth_page(page_idx)
        new_pg_idx = page_idx - 1
        if new_pg_idx < self.nset.startpage_idx:
            new_pg_idx = page_ct -1
        self.mynb.reorder_child(page,new_pg_idx%page_ct)

    def move_right(self):
        page_idx = self.mynb.get_current_page()
        page_ct = self.mynb.get_n_pages()
        page = self.mynb.get_nth_page(page_idx)
        new_pg_idx = (page_idx + 1)%page_ct
        if new_pg_idx < self.nset.startpage_idx:
            new_pg_idx = self.nset.startpage_idx
        self.mynb.reorder_child(page,new_pg_idx%page_ct)

    def clone_page(self):
        newpage = self.nset.add_page(self.pre_file
                                    ,self.sub_file
                                    ,self.pst_file
                                    ,self.imageoffpage #preserve for clone
                                    )
        for idx in self.efields.pentries:
            ev = self.efields.pentries[idx].getentry()
            newpage.efields.pentries[idx].setentry(ev)

    def new_empty_page(self):
        self.nset.add_page('','','')

    def remove_page(self):
        page_ct = self.mynb.get_n_pages()
        if page_ct - self.nset.startpage_idx == 1:
            user_message(mtype=Gtk.MessageType.INFO
                ,title=_('Remove not allowed')
                ,msg=_('One tabpage must remain')
                )
        else:
            current_pno = self.mynb.get_current_page()
            npage = self.mynb.get_nth_page(current_pno)

            self.mynb.remove_page(current_pno)
            thispg = self.nset.pg_for_npage[npage]
            thispg.garbagecollect = True
            del thispg
            del npage



class NgcGui():
    """NgcGui: set of ngcgui OnePg items"""
    # make a set of pages in parent that depends on type(w)
    def __init__(self,w=None
                ,verbose=False
                ,debug=False
                ,noauto=False
                ,keyboardfile='' # None | ['default'|'yes'] | fullfilename
                ,tmode=0
                ,send_function=default_send # prototype: (fname)
                ,ini_file=''
                ,auto_file=''
                ,pre_file=''
                ,sub_files=''
                ,pst_file=''
                ,tab_controls_loc='top'  # option for touchy
                ,control_font=None       # option for touchy
                ,max_parm=None           # for small display, reject some subs
                ,image_width=None        # for small display
                ):

        global g_send_function;    g_send_function    = send_function
        global g_tmode;            g_tmode            = tmode
        global g_verbose;          g_verbose          = verbose
        global g_debug;            g_debug            = debug

        global g_tab_controls_loc; g_tab_controls_loc = tab_controls_loc
        global g_control_font;     g_control_font     = control_font

        try:
            type(g_send_function) # test existence
            if g_send_function == None:
                g_send_function = dummy_send
        except AttributeError:
            print('INVALID send_function, using dummy')
            g_send_function = dummy_send

        if max_parm is not None:
            global g_max_parm
            g_max_parm = max_parm

        if image_width is not None:
            global g_image_width
            if image_width > g_image_width:
                raise ValueError(_('NgcGui image_width=%d too big, max=%d')
                                 % (image_width,g_image_width))
            g_image_width = image_width

        if g_max_parm > INTERP_SUB_PARAMS:
            raise ValueError(_('max_parms=%d exceeds INTERP_SUB_PARAMS=%d')
                            %  (g_max_parm,INTERP_SUB_PARAMS))

        ct_of_pages = 0
        try:
            import popupkeyboard
            import glib # for glib.GError
            if keyboardfile is not None:
                global g_popkbd
                if (keyboardfile in ('default','yes') ):
                    keyboardfile = g_keyboardfile
                g_popkbd = popupkeyboard.PopupKeyboard(glade_file=keyboardfile
                             ,use_coord_buttons=True
                             )
                global g_entry_height
                g_entry_height = g_big_height # bigger for popupkeyboard
        except ImportError as msg:
            print('\nImportError:\n%s', msg)
            print('keyboardfile=%s' % keyboardfile)
            print('popup keyboard unavailable\n')
        except glib.GError as msg:
            # can occur for toohigh version in ui file
            print('\nglib.GError:\n%s' % msg)
            print('keyboardfile=%s' % keyboardfile)
            print('popup keyboard unavailable\n')

        self.last_file = None
        self.nb = None
        self.autosend = not noauto
        self.expandsub = False
        self.nextpage_idx = 0
        self.startpage_idx = 0
        self.pg_for_npage = {}
        if w is None:
            # standalone operation
            self.nb = Gtk.Notebook()
            w = Gtk.Window(Gtk.WindowType.TOPLEVEL)
            if g_alive: w.connect("destroy", Gtk.main_quit)
            w.set_title(sys.argv[0])
            w.add(self.nb)
            self.nb.show()
            w.show()
        elif type(w) == Gtk.Frame:
            # demo -- embed as a notebook in a provider's frame
            self.nb = Gtk.Notebook()
            w.add(self.nb)
            self.nb.show()
            w.show()
        elif type(w) == Gtk.Notebook:
            # demo -- embed as additional pages in a provider's notebook
            self.nb = w
            self.startpage_idx = self.nb.get_n_pages()
        else:
            raise ValueError('NgcGui:bogus w= %s' % type(w))

        self.nb.set_scrollable(True)

        self.intfc = LinuxcncInterface(ini_file)

        if len(self.intfc.subroutine_path) == 0:
            self.intfc.addto_spath(
                       spath_from_files(pre_file,sub_files,pst_file))
            if len(self.intfc.subroutine_path) != 0:
                user_message(mtype=Gtk.MessageType.WARNING
                    ,title=_('Simulated subroutine path')
                    ,msg=_('No subroutine path available.\n'
                      'Simulating subroutine path:\n\n')
                      + str(self.intfc.subroutine_path)
                      + '\n'
                      + _('Generated results may not be usable with linuxcnc')
                    )
        if len(self.intfc.subroutine_path) == 0:
            if g_alive:
                # no message if glade designer is running:
                user_message(mtype=Gtk.MessageType.ERROR
                    ,title=_('No Subroutine Paths')
                    ,msg='\n' +
                        _('No paths available!\n'
                          'Make sure there is a valid\n'
                          '    [RS274]SUBROUTINE_PATH\n\n'
                          '     1) Start linuxcnc\n'
                          'or\n'
                          '     2) Specify an ini file\n'
                          'or\n'
                          '     3) Specify at least one subfile\n'
                          '\n')
                    )
                sys.exit(1)

        global g_searchpath; g_searchpath = self.intfc.subroutine_path


        # multiple pages can be specified with __init__()
        initsublist= []
        if isinstance(sub_files,str) and sub_files:
            initsublist.append(sub_files)
        else:
            initsublist = sub_files

        nogo_l = []
        for sub_file in initsublist:
            if not g_alive: continue
            if os.path.dirname(sub_file) in self.intfc.subroutine_path:
                self.add_page(pre_file,sub_file,pst_file)
                ct_of_pages += 1
            else:
                nogo_l.append(sub_file)
        if nogo_l:
            user_message(mtype=Gtk.MessageType.INFO
                    ,title=_('Cannot use files not in subroutine path')
                    ,msg=_('Files not in subroutine path:\n')
                         + str(nogo_l) +
                         '\n\n'
                         + _('Subroutine path is:\n')
                         + str(self.intfc.subroutine_path)
                    )

        nogo_l = []
        # multiple pages can be specified with an INI file
        sublist  = self.intfc.get_subfiles()  #returns list
        pre_file = self.intfc.get_preamble()
        pst_file = self.intfc.get_postamble()

        # auto_file directory:
        # if specified, verify in path, give message if not
        # if nil
        #   if    PROGRAM_PREFIX  put there
        #   else                  put in cwd
        if auto_file:
            dir = os.path.abspath(os.path.dirname(auto_file))
            spath = self.intfc.get_subroutine_path()
            try:
                spath.index(dir) # check that auto_file dir is in path
                # auto_file ok
            except ValueError:
                # it's called autofile in --help
                pass
                #user_message(mtype=Gtk.MessageType.WARNING
                #        ,title=_('Warning: autofile not in path')
                #        ,msg=_('autofile==%s is not in linuxcnc\n'
                #               'subroutine search path:\n'
                #               '  %s\n') % (auto_file,spath)
                #        )
            self.auto_file = auto_file
        else:
            pprefix = self.intfc.get_program_prefix()
            if pprefix:
                self.auto_file = os.path.join(pprefix,'auto.ngc')
            else:
                self.auto_file = os.path.join(os.path.curdir,'auto.ngc')

        dprint('input for auto_file=%s\nfinal auto_file=%s'
              % (auto_file,self.auto_file))

        if pre_file is None: pre_file  = ''
        if pst_file is None: pst_file = ''

#       vprint('SAVE_FILE: %s' % self.auto_file)
        if sublist and g_alive:
            for sub_file in sublist:
                if sub_file == '""': #beware code for custom is '""'
                    sub_file = ''
                try:
                    self.add_page(pre_file,sub_file,pst_file)
                    ct_of_pages += 1
                except Exception as detail:
                    exception_show(Exception,detail,src='NgcGui init')
                    print(_('CONTINUING without %s') % sub_file)
        else:
            if not sub_files:
                vprint('NgcGui: no INI file with sublist '
                       'and no cmdline sub_file:'
                       'making Custom page')
                self.add_page('','','')
                ct_of_pages += 1
            pass

        self.current_page = None
        # self.nb.set_current_page(self.startpage_idx)
        # start at page 0 to respect caller's ordering
        self.nb.set_current_page(0)

        if g_alive: self.nb.connect('switch-page',   self.page_switched)
        w.show_all()

        if ct_of_pages == 0:
            usage()
            print(_('No valid subfiles specified'))
            sys.exit(1)
        return

    def update_fonts(self,fontname):
        update_fonts(fontname)

    def page_switched(self,notebook,npage,pno):
        if self.current_page:
            curpage = self.current_page
            if hasattr(curpage,'imgw'):
                w = getattr(curpage,'imgw')
                w.iconify()
        try:
            mypg = self.pg_for_npage[self.nb.get_nth_page(pno)]
            if hasattr(mypg,'imgw'):
                w = getattr(mypg,'imgw')
                w.deiconify()
                w.show_all()
            self.current_page = mypg
        except KeyError as msg:
            # can occur when embedded in providers notebook
            # print('page_switched: Caught KeyError')
            pass

    def add_page(self,pre_file,sub_file,pst_file,imageoffpage=False):
        # look for gcmc on first request for .gcmc file:
        if os.path.splitext(sub_file)[-1] in ['.gcmc','.GCMC']:
            if not find_gcmc(): return None

        self.nextpage_idx = self.nextpage_idx + 1
        opage = OnePg(pre_file=pre_file
                     ,sub_file=sub_file
                     ,pst_file=pst_file
                     ,mynb=self.nb
                     ,nset=self # an NgcGui set of pages
                     ,imageoffpage=imageoffpage
                     )
        if opage.fset.sub_data.pdict['subname'] == '':
            ltxt = 'Custom'
        else:
            ltxt = opage.fset.sub_data.pdict['subname']
        ltxt = self.make_unique_tab_name(ltxt)

        eb_lbl = Gtk.EventBox()
        mylbl  = Gtk.Label(ltxt)
        if g_popkbd is not None:
             mylbl.set_size_request(-1,g_big_height)
        eb_lbl.add(mylbl)
        mylbl.show()
        eb_lbl.set_style(g_lbl_style_default)

        opage.pgbox.set_border_width(2)
        pno  = self.nb.append_page(opage.pgbox,eb_lbl)
        if g_control_font is not None:
            mod_font_by_category(mylbl)

        # An EventBox is needed to change bg of tabpage label
        # When using EventBox:
        #      don't use get_tab_label_text()
        opage.save_onepage_tablabel(eb_lbl,mylbl)

        self.pg_for_npage[self.nb.get_nth_page(pno)] = opage
        self.nb.set_current_page(pno) # move to the new page
        return opage

    def make_unique_tab_name(self,name):
        l = []
        if not name: return None
        for pno in range(self.startpage_idx,self.nb.get_n_pages()):
            npage = self.nb.get_nth_page(pno)
            pg = self.pg_for_npage[npage]
            # using EventBox for label, dont use get_tab_label_text()
            ltxt = pg.the_lbl.get_text()
            if ltxt.find(name) == 0:
                l.append(ltxt)
        if len(l) == 0:
            return(name)
        if len(l) == 1:
            return(name + '-1')
        last = l[-1]
        idx = last.find('-')
        return(name + '-' + str(int(last[idx+1:]) + 1) )


class SaveSection():
    """SaveSection: lines ready for result file"""
    def __init__(self,mypg,pre_info,sub_info,pst_info,force_expand=False):
        global g_label_id
        g_label_id += 1
        self.sdata=[]

        self.sdata.append("(%s: FEATURE %s)\n"% (g_progname,dt() ))

        self.sdata.append("(%s: files: <%s,%s,%s>)\n"
                       % (g_progname
                         ,pre_info.pre_file
                         ,sub_info.sub_file
                         ,pst_info.pst_file
                         )
                       )

        # note: this line will be replaced on file output with a count
        # that can span multiple pages
        self.sdata.append("#<_feature:> = 0\n")

        self.sdata.append("(%s: preamble file: %s)\n" % (
                          g_progname,pre_info.pre_file))
        self.sdata.extend(pre_info.inputlines)

        emsg = '' # accumulate errors for emsg

        calltxt = 'o<%s> call ' % sub_info.pdict['subname']
        parmlist = []
        tmpsdata = []
        for idx in sub_info.ndict:
            name,dvalue,comment = sub_info.ndict[idx]
            value=mypg.efields.getentry_byidx(idx)
            try:
                v = float(value)
            except ValueError:
                emsg = emsg + (
                     _('Entry for parm %2d is not a number\n <%s>\n')
                     % (int(idx),value))
            #note: e formats not accepted by linuxcnc (like 1e2)
            #      but using float(value) --->mmm.nnnnn everywhere
            #      makes long call line
            #      so try to send entry value, but if it has e, use float
            if 'e' in value:
                value = str(float(value.lower() ))

            parmlist.append(value)
            if 'isgcmc' in sub_info.pdict:
                # just print value of gcmc parm embedded in gcmc result
                # the call requires no parms
                pass
            else:
                calltxt = calltxt + '[%s]' % value
            # these appear only for not-expandsub
            tmpsdata.append("(%11s = %12s = %12s)\n" % (
                              '#'+str(idx),name,value))
        if emsg:
            user_message(mtype=Gtk.MessageType.ERROR
                        ,title=_('SaveSection Error')
                        ,msg=emsg)
            mypg.cpanel.set_message(_('Failed to create feature'))
            raise ValueError
        calltxt = calltxt + '\n'
        # expandsub not honored for gcmc
        if (mypg.expandsub and 'isgcmc' in sub_info.pdict):
            print(_('expandsub not honored for gcmc file: %s')%
                     os.path.basename(sub_info.sub_file))
            mypg.expandsub = 0
        #---------------------------------------------------------------------
        if (not mypg.expandsub) and (not force_expand):
            self.sdata.append("(%s: call subroutine file: %s)\n" % (
                              g_progname,sub_info.sub_file) )
            self.sdata.append("(%s: positional parameters:)\n"% g_progname)
            self.sdata.extend(tmpsdata)
            self.sdata.append(calltxt) # call the subroutine
        else:
            # expand the subroutine in place with unique labels
            self.sdata.append('(Positional parameters for %s)\n'
                             % mypg.sub_file)
            for i in range(0,idx):
                self.sdata.append('        #%d = %s\n' % (i+1,parmlist[i]))
            self.sdata.append('(expanded file: %s)\n' % mypg.sub_file)
            blank = ''
            idx = 0
            for line in sub_info.inputlines:
                idx += 1
                if line.strip() == '':
                    continue
                if idx in sub_info.ldict:
                    modlabel = sub_info.ldict[idx]
                    if modlabel == 'ignoreme':
                        continue
                    modlabel = 'o<%03d%s>' % (g_label_id,modlabel)
                    r = re.search(r'^o<(.*?)>(.*)',line.strip())
                    if r:
                        modline = r.group(2) + '\n'
                    else:
                        print('SaveSection__init__:unexpected:',line)
                    self.sdata.append('%11s %s' % (modlabel,modline))
                else:
                    theline = '%11s %s' % (blank,line)
                    # hack: try to reduce long line length so linuxcnc wont
                    #       choke on files that work otherwise but fail
                    #       when expanded here
                    # example: 246 chars observed for
                    #  qpex --> the call to qpocket uses many named parms
                    # hardcoded for # config.h.in #define LINELEN 255
                    # hardcoded 252 empirically determined
                    if len(theline) >= 252:
                        theline = line
                    self.sdata.append(theline)
        #---------------------------------------------------------------------

        if pst_info.inputlines:
            self.sdata.append("(%s: postamble file: %s)\n" % (
                              g_progname,pst_info.pst_file))
            self.sdata.extend(pst_info.inputlines)
        #for line in self.sdata:
        #    print('line:',line,)


def usage():
    print("""
Usage:
%s [Options] [sub_filename]
Options requiring values:
    [-d | --demo] [0|1|2] (0: DEMO standalone toplevel)
                          (1: DEMO embed new notebook)
                          (2: DEMO embed within existing notebook)
    [-S | --subfile       sub_filename]
    [-p | --preamble      preamble_filename]
    [-P | --postamble     postamble_filename]
    [-i | --ini           inifile_name]
    [-a | --autofile      auto_filename]
    [-t | --test          testno]
    [-K | --keyboardfile  glade_file] (use custom popupkeyboard glade file)
Solo Options:
    [-v | --verbose]
    [-D | --debug]
    [-N | --nom2]         (no m2 terminator (use %%))
    [-n | --noauto]       (save but do not automatically send result)
    [-k | --keyboard]     (use default popupkeybaord)
    [-s | --sendtoaxis]   (send generated ngc file to axis gui)
Notes:
      A set of files is comprised of a preamble, subfile, postamble.
      The preamble and postamble are optional.
      One set of files can be specified from cmdline.
      Multiple sets of files can be specified from an INI file.
      If --ini is NOT specified:
         search for a running linuxcnc and use its INI file
    """ % g_progname)
#-----------------------------------------------------------------------------
# Standalone (and demo) usage:

def standalone_pyngcgui():
    # make widgets for test cases:
    top = Gtk.Window(Gtk.WindowType.TOPLEVEL)
    top.set_title('top')
    hbox  = Gtk.HBox()
    top.add(hbox)
    l1 = Gtk.Label('LABEL')
    hbox.pack_start(l1,expand=0,fill=0,padding=0)
    e1 = Gtk.Entry()
    hbox.pack_start(e1,expand=0,fill=0,padding=0)
    e1.set_width_chars(4)
    f1 = Gtk.Frame()
    hbox.pack_start(f1,expand=0,fill=0,padding=0)
    f2 = Gtk.Frame()
    hbox.pack_start(f2,expand=0,fill=0,padding=0)

    n = Gtk.Notebook()
    n.set_scrollable(True)
    b1 = Gtk.Button('b1-filler')
    b2 = Gtk.Button('b2-filler')
    n.append_page(b1,Gtk.Label('Mb1-filler'))
    n.append_page(b2,Gtk.Label('Mb2-filler'))
    f1.add(n)
    top.show_all()


    demo         = 0 # 0 ==> standalone operation
    subfilenames = ''
    prefilename  = ''
    pstfilename  = ''
    vbose        = False
    dbg          = False
    noauto       = False
    keyboard     = False
    keyboardfile = 'default'
    ini_file     = ''
    auto_file    = ''
    tmode        = 0
    send_f       = default_send
    try:
        options,remainder = getopt.getopt(sys.argv[1:]
                          ,'a:Dd:hi:kK:Nnp:P:sS:t:v'
                          , ['autofile'
                            ,'demo='
                            ,'debug'
                            ,'help'
                            ,'ini='
                            ,'keyboard'
                            ,'keyboardfile='
                            ,'noauto'
                            ,'preamble='
                            ,'postamble='
                            ,'subfile='
                            ,'verbose'
                            ,'sendtoaxis'
                            ,'nom2'
                            ]
                          )
    except getopt.GetoptError as msg:
        usage()
        print('\nGetoptError:%s' % msg)
        sys.exit(1)
    except Exception as detail:
        exception_show(Exception,detail,'__main__')
        sys.exit(1)
    for opt,arg in options:
        #print('#opt=%s arg=%s' % (opt,arg))
        if opt in ('-h','--help'):      usage(),sys.exit(0)
        if opt in ('-d','--demo'):      demo = arg


        if opt in ('-i','--ini'):       ini_file = arg
        if opt in ('-a','--autofile'):  auto_file = arg

        if opt in ('-p','--preamble'):  prefilename=arg
        if opt in ('-P','--postamble'): pstfilename=arg
        if opt in ('-S','--subfile'):   subfilenames=arg

        if opt in ('-t','--test'):      tmode=arg


        if opt in ('-k','--keyboard'):   keyboard=True
        if opt in ('-K','--keyboardfile'):
            keyboard=True
            keyboardfile=arg

        if opt in ('-N','--nom2'):       dbg = g_nom2 = True
        if opt in ('-D','--debug'):      dbg = True
        if opt in ('-n','--noauto'):     noauto = True
        if opt in ('-v','--verbose'):
            vbose = True
            continue
        if opt in ('-s','--sendtoaxis'):
            send_f = send_to_axis
            continue
    if remainder: subfilenames = remainder # ok for shell glob e.g., *.ngc
    demo = int(demo)
    if not keyboard: keyboardfile=None

    if (dbg):
        print(g_progname + ' BEGIN-----------------------------------------------')
        print('    __file__= %s' % __file__)
        print('    ini_file= %s' % ini_file)
        print('    sys.argv= %s' % sys.argv)
        print('   os.getcwd= %s' % os.getcwd())
        print('    sys.path= %s' % sys.path)
        print('        demo= %s' % demo)
        print(' prefilename= %s' % prefilename)
        print('subfilenames= %s' % subfilenames)
        print(' pstfilename= %s' % pstfilename)
        print('    keyboard= %s, keyboardfile= <%s>' % (keyboard,keyboardfile))
    try:
        if demo == 0:
            top.hide()
            NgcGui(w=None
                  ,verbose=vbose,debug=dbg,noauto=noauto
                  ,keyboardfile=keyboardfile
                  ,tmode=tmode
                  ,send_function=send_f # prototype: (fname)
                  ,ini_file=ini_file,auto_file=auto_file
                  ,pre_file=prefilename,sub_files=subfilenames,pst_file=pstfilename
                  )
        elif demo == 1:
            NgcGui(w=f2
                  ,verbose=vbose,debug=dbg,noauto=noauto
                  ,keyboardfile=keyboardfile
                  ,tmode=tmode
                  ,send_function=send_f # prototype: (fname)
                  ,ini_file=ini_file,auto_file=auto_file
                  ,pre_file=prefilename,sub_files=subfilenames,pst_file=pstfilename
                  )
            top.set_title('Create OnePg inside a new frame')
        elif demo == 2:
            NgcGui(w=n
                  ,verbose=vbose,debug=dbg,noauto=noauto
                  ,keyboardfile=keyboardfile
                  ,tmode=tmode
                  ,send_function=send_f # prototype: (fname)
                  ,ini_file=ini_file,auto_file=auto_file
                  ,pre_file=prefilename,sub_files=subfilenames,pst_file=pstfilename
                  )
            top.set_title('Create OnePg inside an existing notebook')
        else:
            print('unknown demo',demo)
            usage()
            sys.exit(1)
    except Exception as detail:
        exception_show(Exception,detail,'__main__')
        print('in main()')
        sys.exit(11)

    try:
        Gtk.main()
    except KeyboardInterrupt:
        sys.exit(0)

# vim: sts=4 sw=4 et
