# $Id$
##
##  This file is part of pyFormex 1.0.5  (Sat Feb 16 10:40:32 CET 2019)
##  pyFormex is a tool for generating, manipulating and transforming 3D
##  geometrical models by sequences of mathematical operations.
##  Home page: http://pyformex.org
##  Project page:  http://savannah.nongnu.org/projects/pyformex/
##  Copyright 2004-2018 (C) Benedict Verhegghe (benedict.verhegghe@ugent.be)
##  Distributed under the GNU General Public License version 3 or later.
##
##  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 3 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, see http://www.gnu.org/licenses/.
##
"""Create 3D graphical representations.

The draw module provides the basic user interface to the OpenGL
rendering capabilities of pyFormex. The full contents of this module
is available to scripts running in the pyFormex GUI without the need
to import it.
"""
from __future__ import absolute_import, division, print_function

import numpy as np

import pyformex as pf

from pyformex import utils
from pyformex.gui import widgets

# Things that we want in the scripting language
Dialog = widgets.InputDialog
_I = widgets.simpleInputItem
_G = widgets.groupInputItem
_T = widgets.tabInputItem
from pyformex.script import *
from pyformex.opengl.colors import *
from pyformex.gui.toolbar import timeout
from pyformex.gui import image


#################### Interacting with the user ###############################


def exitGui(res=0):
    """Terminate the GUI with a given status.

    """
    print("Terminating the GUI with a value %s" % res)
    pf.app.closeAllWindows()
    pf.app.exit(res)


def closeGui():
    """Close the GUI.

    Calling this function from a script closes the GUI and terminates
    pyFormex.
    """
    pf.debug("Closing the GUI: currently, this will also terminate pyformex.", pf.DEBUG.GUI)
    pf.GUI.close()


def closeDialog(name):
    """Close the named dialog.

    Closes the InputDialog with the given name. If multiple dialogs are
    open with the same name, all these dialogs are closed.

    This only works for dialogs owned by the pyFormex GUI.
    """
    pf.GUI.closeDialog(name)


def showMessage(text,actions=['OK'],level='info',modal=True,align='00',**kargs):
    """Show a short message widget and wait for user acknowledgement.

    There are three levels of messages: 'info', 'warning' and 'error'.
    They differ only in the icon that is shown next to the test.
    By default, the message widget has a single button with the text 'OK'.
    The dialog is closed if the user clicks a button.
    The return value is the button text.
    """
    w = widgets.MessageBox(text,level=level,actions=actions,**kargs)
    if align == '--':
        w.move(100, 100)
    if modal:
        return w.getResults()
    else:
        w.show()
        return None


def showInfo(text,actions=['OK'],modal=True):
    """Show an informational message and wait for user acknowledgement."""
    return showMessage(text, actions, 'info', modal)

def warning(text,actions=['OK']):
    """Show a warning message and wait for user acknowledgement."""
    return showMessage(text, actions, 'warning')

def error(text,actions=['OK']):
    """Show an error message and wait for user acknowledgement."""
    return showMessage(text, actions, 'error')


def ask(question,choices=None,**kargs):
    """Ask a question and present possible answers.

    Return answer if accepted or default if rejected.
    The remaining arguments are passed to the InputDialog getResult method.
    """
    return showMessage(question,choices,'question',**kargs)


def ack(question,**kargs):
    """Show a Yes/No question and return True/False depending on answer."""
    return ask(question,['No', 'Yes'],**kargs) == 'Yes'


def showText(text,itemtype='text',actions=[('OK', None)],modal=True,mono=False):
    """Display a text in a dialog window.

    Creates a dialog window displaying some text. The dialog can be modal
    (blocking user input to the main window) or modeless.
    Scrollbars are added if the text is too large to display at once.
    By default, the dialog has a single button to close the dialog.

    Parameters:

    - `text`: a multiline text to be displayed. It can be plain text or html
      or reStructuredText (starts with '..').
    - `itemtype`: an InputItem type that can be used for text display. This
      should be either 'text' of 'info'.
    - `actions`: a list of action button definitions.
    - `modal`: bool: if True, a modal dialog is constructed. Else, the dialog
      is modeless.
    - `mono`: if True, a monospace font will be used. This is only useful for
      plain text, e.g. to show the output of an external command.

    Returns:

    :modal dialog: the result of the dialog after closing.
      The result is a dictionary with a single key: 'text' having the
      displayed text as a value. If an itemtype 'text' was used, this may
      be a changed text.
    :modeless dialog: the open dialog window itself.

    """
    if mono:
        font = "DejaVu Sans Mono"
    else:
        font = None
    w = Dialog(size=(0.75, 0.75),
        items=[_I('text', text, itemtype=itemtype, text='', font=font, size=(-1, -1))],
        modal=modal,
        actions=actions,
        caption='pyFormex Text Display',
        )
    if modal:
        return w.getResults()
    else:
        w.show()
        return w


def showFile(filename,mono=True,**kargs):
    """Display a text file.

    This will use the :func:`showText()` function to display a text read
    from a file.
    By default this uses a monospaced font.
    Other arguments may also be passed to ShowText.
    """
    try:
        f = open(filename, 'r')
    except IOError:
        return
    showText(f.read(),mono=mono,**kargs)
    f.close()


def showDoc(obj=None,rst=True,modal=False):
    """Show the docstring of an object.

    Parameters:

    - `obj`: any object (module, class, method, function) that has a
      __doc__ attribute. If None is specified, the docstring of the current
      application is shown.
    - `rst`: bool. If True (default) the docstring is treated as being
      reStructuredText and will be nicely formatted accordingly.
      If False, the docstring is shown as plain text.
    """
    text = None
    if obj is None:
        if not pf.GUI.canPlay:
            return
        obj = pf.prefcfg['curfile']
        if utils.is_script(obj):
            #print "obj is a script"
            from pyformex.utils import getDocString
            text = getDocString(obj)
            obj = None
        else:
            from pyformex import apps
            obj = apps.load(obj)

    if obj:
        text = obj.__doc__

    if text is None:
        raise ValueError("No documentation found for object %s" % obj)

    text = utils.forceReST(text, underline=True)
    if pf.GUI.doc_dialog is None:
        if modal:
            actions=[('OK', None)]
        else:
            actions = [('Close', pf.GUI.close_doc_dialog)]
        pf.GUI.doc_dialog = showText(text, actions=actions, modal=modal)
    else:
        #
        # TODO: check why needed: without sometimes fails
        # RuntimeError: wrapped C/C++ object of %S has been deleted
        # probably when runall?
        #
        try:
            pf.GUI.doc_dialog.updateData({'text':text})
            # pf.GUI.doc_dialog.show()
            pf.GUI.doc_dialog.raise_()
            pf.GUI.doc_dialog.update()
            pf.app.processEvents()
        except:
            pass


def editFile(fn,exist=False):
    """Load a file into the editor.

    Parameters:

    - `fn`: filename. The corresponding file is loaded into the editor.
    - `exist`: bool. If True, only existing filenames will be accepted.

    Loading a file in the editor is done by executing an external command with
    the filename as argument. The command to be used can be set in the
    configuration. If none is set, pyFormex will try to lok at the `EDITOR`
    and `VISUAL` environment settings.

    The main author of pyFormex uses 'emacsclient' as editor command,
    to load the files in a running copy of Emacs.
    """
    print("Edit File: %s" % fn)
    if pf.cfg['editor']:
        if exist and not os.path.exists(fn):
            return
        utils.system('%s %s' % (pf.cfg['editor'], fn),wait=False)
    else:
        warning('No known editor was found or configured')


# widget and result status of the widget in askItems() function
_dialog_widget = None
_dialog_result = None

def askItems(items,timeout=None,**kargs):
    """Ask the value of some items to the user.

    Create an interactive widget to let the user set the value of some items.
    The items are specified as a list of dictionaries. Each dictionary
    contains the input arguments for a widgets.InputItem. It is often
    convenient to use one of the _I, _G, ot _T functions to create these
    dictionaries. These will respectively create the input for a
    simpleInputItem, a groupInputItem or a tabInputItem.

    For convenience, simple items can also be specified as a tuple.
    A tuple (key,value) will be transformed to a dict
    {'key':key, 'value':value}.

    See the widgets.InputDialog class for complete description of the
    available input items.

    A timeout (in seconds) can be specified to have the input dialog
    interrupted automatically and return the default values.

    The remaining arguments are keyword arguments that are passed to the
    widgets.InputDialog.getResult method.

    Returns a dictionary with the results: for each input item there is a
    (key,value) pair. Returns an empty dictionary if the dialog was canceled.
    Sets the dialog timeout and accepted status in global variables.
    """
    global _dialog_widget, _dialog_result

    w = widgets.InputDialog(items,**kargs)

    _dialog_widget = w
    _dialog_result = None
    res = w.getResults(timeout)
    _dialog_widget = None
    _dialog_result = w.result()
    return res


def currentDialog():
    """Returns the current dialog widget.

    This returns the dialog widget created by the askItems() function,
    while the dialog is still active. If no askItems() has been called
    or if the user already closed the dialog, None is returned.
    """
    return _dialog_widget

def dialogAccepted():
    """Returns True if the last askItems() dialog was accepted."""
    return _dialog_result == widgets.ACCEPTED

def dialogRejected():
    """Returns True if the last askItems() dialog was rejected."""
    return _dialog_result == widgets.REJECTED

def dialogTimedOut():
    """Returns True if the last askItems() dialog timed out."""
    return _dialog_result == widgets.TIMEOUT


def askFile(cur=None,filter='all',exist=True,multi=False,compr=False,change=True,timeout=None,caption=None,sidebar=None):
    """Ask for a file name or multiple file names using a file dialog.

    Parameters:

    - `cur`: directory or filename. Specifies the starting point of the
      selection dialog. All the files in the specified directory (or the
      file's directory) matching the `filter` will be presented to the user.
      If cur is a file, it will be set as the initial selection.
    - `filter`: string or list of strings. Specifies a (set of) filter(s) to
      be applied on the files in the selected directory. This allows to
      narrow down the selection possibilities. The `filter` argument is passed
      through the :func:`utils.fileDescription` function to create the
      actual filter set. If multiple filters are included, the user can
      select the appropriate one from the dialog.
    - `exist`: bool: if True, the filename must exist. The default (False) will
      allow a new file to be created or an existing to be used.
    - `multi`: bool. If True, allows the user to pick multiple file names
      in a single operation.
    - `compr`: bool. If True, the specified pattern will be extended with
      the corresponding compressed file types.
    - `change`: bool. If True (default), the current working directory will
      be changed to the parent directory of the selection.
    - `caption`: string. This string will be displayed as the dialog title
      instead of the default one.
    - `timeout`: float. If specified, the dialog will timeout after the
      specified number of seconds.
    - `caption`: string. If specified, it will be displayed as the FileDialog
      title.
    - `sidebar`: list of paths. If specified, these will be added to the
      sidebar (in addition to the configured paths).

    Returns the result of the file dialog. If the user accepted the selection,
    this will be a Dict with at least a key 'fn' holding the selected
    filename(s): a single file name is if `multi` is False, or a list of file
    names if `multi` is True. If the user canceled the selection process,
    an empty dict is returned.
    """
    if cur is None:
        cur = pf.cfg['workdir']
    if os.path.isdir(cur):
        fn = ''
    else:
        fn = os.path.basename(cur)
        cur = os.path.dirname(cur)
    if filter == 'pgf':
        w = widgets.GeometryFileDialog(cur, filter, exist, compr=compr, caption=caption,sidebar=sidebar)
    else:
        w = widgets.FileDialog(cur, filter, exist, multi=multi, compr=compr, caption=caption, sidebar=sidebar)
    if fn:
        w.selectFile(fn)
    res = w.getResults(timeout)
    if res:

        fn = res.fn
        fs = w.selectedNameFilter()
        if not exist and not multi:
            # Check and force extension for single new file
            okext = utils.fileExtensionsFromFilter(fs)
            print("Accepted extensions: %s" % okext)
            ok = False
            for ext in okext:
                if fn.endswith(ext):
                    ok = True
                    break
            if not ok:
                fn += okext[0]
            res['fn'] = fn

        if fn and change:
            if multi:
                cur = fn[0]
            else:
                cur = fn
            cur = os.path.dirname(cur)
            chdir(cur)
    pf.GUI.update()
    pf.app.processEvents()
    return res


def askFilename(*args,**kargs):
    """Ask for a file name or multiple file names using a file dialog.

    This functions takes the same parameters as :func:`askFile`, and is
    functionally equivalent to it. However, in case of an accepted dialog
    it only returns the filename(s) and in case of a canceled dialog it
    returns None.
    """
    res = askFile(*args,**kargs)
    if res:
        return res['fn']
    else:
        return None


def askNewFilename(cur=None,filter="All files (*.*)",compr=False,timeout=None,caption=None,sidebar=None):
    """Ask a single new filename.

    This is a convenience function for calling askFilename with the
    arguments exist=False.
    """
    return askFilename(cur=cur, filter=filter, exist=False, multi=False, compr=compr, timeout=timeout, caption=caption, sidebar=sidebar)


def askDirname(path=None,change=True,byfile=False,caption=None):
    """Interactively select a directory and change the current workdir.

    The user is asked to select a directory through the standard file
    dialog. Initially, the dialog shows all the subdirectories in the
    specified path, or by default in the current working directory.

    The selected directory becomes the new working directory, unless the
    user canceled the operation, or the change parameter was set to False.
    """
    if path is None:
        path = pf.cfg['workdir']
    if not os.path.isdir(path):
        path = os.path.dirname(path)
    if byfile:
        dirmode = 'auto'
    else:
        dirmode = True
    fn = widgets.FileDialog(path, '*', dir=dirmode,caption=caption).getFilename()
    if fn:
        if not os.path.isdir(fn):
            fn = os.path.dirname(fn)
        if change:
            chdir(fn)
    pf.GUI.update()
    pf.app.processEvents()
    return fn


def askImageFile(fn=None,compr=False):
    if not fn:
        fn = pf.cfg['pyformexdir']
    return askFilename(fn, filter=['img', 'all'], multi=False, exist=True)


def checkWorkdir():
    """Ask the user to change the current workdir if it is not writable.

    Returns True if the new workdir is writable.
    """
    workdir = os.getcwd()
    ok = os.access(workdir, os.W_OK)
    if not ok:
        warning("Your current working directory (%s) is not writable. Change your working directory to a path where you have write permission." % workdir)
        askDirname()
        ok = os.access(os.getcwd(), os.W_OK)
    return ok


logfile = None     # the log file


def printMessage(s,**kargs):
    """Print a message on the message board.

    Parameters:

    - `s`: string to print
    - `kargs`: more keyword arguments are passed to meth:`MessageBpard.write`.

    This function forces an update of the GUI, so that the output message is
    guaranteed to be visible.
    If a logfile was opened, the message is also written to the log file.
    """
    if logfile is not None:
        logfile.write(str(s)+'\n')
    pf.GUI.board.write(str(s),**kargs)
    pf.GUI.update()
    pf.app.processEvents()


def delay(s=None):
    """Get/Set the draw delay time.

    Returns the current setting of the draw wait time (in seconds).
    This drawing delay is obeyed by drawing and viewing operations.

    A parameter may be given to set the delay time to a new value.
    It should be convertable to a float.
    The function still returns the old setting. This may be practical
    to save that value to restore it later.
    """
    saved = pf.GUI.drawwait
    if s is not None:
        pf.GUI.drawwait = float(s)
    return saved


def wait(relock=True):
    """Wait until the drawing lock is released.

    This uses the drawing lock mechanism to pause. The drawing lock
    ensures that subsequent draws are retarded to give the user the time
    to view. The use of this function is prefered over that of
    :func:`pause` or :func:`sleep`, because it allows your script to
    continue the numerical computations while waiting to draw the next
    screen.

    This function can be used to retard other functions than `draw` and `view`.
    """
    pf.GUI.drawlock.wait()
    if relock:
        pf.GUI.drawlock.lock()


# Functions corresponding with control buttons

def play(refresh=False):
    """Start the current script or if already running, continue it.

    """
    if len(pf.scriptlock) > 0:
        # An application is running
        if pf.GUI.drawlock.locked:
            pf.GUI.drawlock.release()

    else:
        # Start current application
        runAny(refresh=refresh)


def replay():
    """Replay the current app.

    This works pretty much like the play() function, but will
    reload the current application prior to running it.
    This function is especially interesting during development
    of an application.
    If the current application is a script, then it is equivalent with
    play().
    """
    appname = pf.cfg['curfile']
    play(refresh=utils.is_app(appname))



def fforward():
    """Releases the drawing lock mechanism indefinely.

    Releasing the drawing lock indefinely means that the lock will not
    be set again and your script will execute till the end.
    """
    pf.GUI.drawlock.free()


#
# IDEA: The pause() could display a progress bar showing how much time
# is left in the pause,
# maybe also with buttons to repeat, pause indefinitely, ...
#
def pause(timeout=None,msg=None):
    """Pause the execution until an external event occurs or timeout.

    When the pause statement is executed, execution of the pyformex script
    is suspended until some external event forces it to proceed again.
    Clicking the PLAY, STEP or CONTINUE button will produce such an event.

    - `timeout`: float: if specified, the pause will only last for this
      many seconds. It can still be interrupted by the STEP buttons.

    - `msg`: string: a message to write to the board to explain the user
      about the pause
    """
    from pyformex.gui.drawlock import Repeater
    def _continue_():
        return not pf.GUI.drawlock.locked

    if msg is None and timeout is None:
        msg = "Use the Play/Step/Continue button to proceed"

    pf.debug("Pause (%s): %s" % (timeout, msg), pf.DEBUG.SCRIPT)
    if msg:
        print(msg)

    pf.GUI.enableButtons(pf.GUI.actions, ['Step', 'Continue'], True)
    pf.GUI.drawlock.release()
    if pf.GUI.drawlock.allowed:
        pf.GUI.drawlock.locked = True
    if timeout is None:
        timeout = widgets.input_timeout
    R = Repeater(_continue_, timeout, sleep=0.1)
    R.start()
    pf.GUI.drawlock.release()


################### EXPERIMENTAL STUFF: AVOID! ###############


def sleep(duration,granularity=0.01):
    from pyformex.gui.drawlock import Repeater
    R = Repeater(None, duration, sleep=granularity)
    R.start()


########################## print information ################################


def printbbox():
    print(pf.canvas.bbox)

def printviewportsettings():
    pf.GUI.viewports.printSettings()

def reportCamera():
    print(pf.canvas.camera.report())


#################### camera ##################################

def zoom_factor(factor=None):
    if factor is None:
        factor = pf.cfg['gui/zoomfactor']
    return float(factor)

def pan_factor(factor=None):
    if factor is None:
        factor = pf.cfg['gui/panfactor']
    return float(factor)

def rot_factor(factor=None):
    if factor is None:
        factor = pf.cfg['gui/rotfactor']
    return float(factor)

def zoomIn(factor=None):
    pf.canvas.camera.zoomArea(1./zoom_factor(factor))
    pf.canvas.update()
def zoomOut(factor=None):
    pf.canvas.camera.zoomArea(zoom_factor(factor))
    pf.canvas.update()

def panRight(factor=None):
    pf.canvas.camera.transArea(-pan_factor(factor), 0.)
    pf.canvas.update()
def panLeft(factor=None):
    pf.canvas.camera.transArea(pan_factor(factor), 0.)
    pf.canvas.update()
def panUp(factor=None):
    pf.canvas.camera.transArea(0., -pan_factor(factor))
    pf.canvas.update()
def panDown(factor=None):
    pf.canvas.camera.transArea(0., pan_factor(factor))
    pf.canvas.update()

def rotRight(factor=None):
    pf.canvas.camera.rotate(rot_factor(factor), 0, 1, 0)
    pf.canvas.update()
def rotLeft(factor=None):
    pf.canvas.camera.rotate(-rot_factor(factor), 0, 1, 0)
    pf.canvas.update()
def rotUp(factor=None):
    pf.canvas.camera.rotate(-rot_factor(factor), 1, 0, 0)
    pf.canvas.update()
def rotDown(factor=None):
    pf.canvas.camera.rotate(rot_factor(factor), 1, 0, 0)
    pf.canvas.update()
def twistLeft(factor=None):
    pf.canvas.camera.rotate(rot_factor(factor), 0, 0, 1)
    pf.canvas.update()
def twistRight(factor=None):
    pf.canvas.camera.rotate(-rot_factor(factor), 0, 0, 1)
    pf.canvas.update()
def barrelRoll(n=36):
    d = 360./n
    t = 2./n
    for i in range(n):
        twistRight(d)
        sleep(t)
def transLeft(factor=None):
    val = pan_factor(factor) * pf.canvas.camera.dist
    pf.canvas.camera.translate(-val, 0, 0, pf.cfg['draw/localaxes'])
    pf.canvas.update()
def transRight(factor=None):
    val = pan_factor(factor) * pf.canvas.camera.dist
    pf.canvas.camera.translate(+val, 0, 0, pf.cfg['draw/localaxes'])
    pf.canvas.update()
def transDown(factor=None):
    val = pan_factor(factor) * pf.canvas.camera.dist
    pf.canvas.camera.translate(0, -val, 0, pf.cfg['draw/localaxes'])
    pf.canvas.update()
def transUp(factor=None):
    val = pan_factor(factor) * pf.canvas.camera.dist
    pf.canvas.camera.translate(0, +val, 0, pf.cfg['draw/localaxes'])
    pf.canvas.update()
def dollyIn(factor=None):
    pf.canvas.camera.dolly(1./zoom_factor(factor))
    pf.canvas.update()
def dollyOut(factor=None):
    pf.canvas.camera.dolly(zoom_factor(factor))
    pf.canvas.update()

def lockCamera():
    pf.canvas.camera.lock()
def unlockCamera():
    pf.canvas.camera.lock(False)


def zoomRectangle():
    """Zoom a rectangle selected by the user."""
    pf.canvas.zoom_rectangle()
    pf.canvas.update()


def getRectangle():
    """Zoom a rectangle selected by the user."""
    r = pf.canvas.get_rectangle()
    print(r)
    pf.canvas.update()


def zoomBbox(bb):
    """Zoom thus that the specified bbox becomes visible."""
    pf.canvas.setCamera(bbox=bb)
    pf.canvas.update()


def zoomObj(object):
    """Zoom thus that the specified object becomes visible.

    object can be anything having a bbox() method or a list thereof.
    """
    zoomBbox(coords.bbox(object))


def zoomAll():
    """Zoom thus that all actors become visible."""
    zoomBbox(pf.canvas.sceneBbox())


# Can this be replaced with zoomIn/Out?
def zoom(f):
    """Zoom with a factor f

    A factor > 1.0 zooms out, a factor < 1.0 zooms in.
    """
    pf.canvas.zoom(f)
    pf.canvas.update()


def focus(point):
    """Move the camera focus to the specified point.

    Parameters:

    - `point`: float(3,) or alike

    The camera focus is set to the specified point, while keeping
    a parallel camera direction and same zoom factor.
    The specified point becomes the center of the screen and
    the center of camera rotations.
    """
    pf.canvas.camera.focus = point
    pf.canvas.camera.setArea(0.,0.,1.,1.,True,center=True)
    pf.canvas.update()


def flyAlong(path,upvector=[0., 1., 0.],sleeptime=None):
    """Fly through the current scene along the specified path.

    - `path`: a plex-2 or plex-3 Formex (or convertibel to such Formex)
      specifying the paths of camera eye and center (and upvector).
    - `upvector`: the direction of the vertical axis of the camera, in case
      of a 2-plex camera path.
    - `sleeptime`: a delay between subsequent images, to slow down
      the camera movement.

    This function moves the camera through the subsequent elements of the
    Formex. For each element the first point is used as the center of the
    camera and the second point as the eye (the center of the scene looked at).
    For a 3-plex Formex, the third point is used to define the upvector
    (i.e. the vertical axis of the image) of the camera. For a 2-plex
    Formex, the upvector is constant as specified in the arguments.
    """
    try:
        if not isinstance(path, Formex):
            path = path.toFormex()
        if not path.nplex() in (2, 3):
            raise ValueError
    except:
        raise ValueError("The camera path should be (convertible to) a plex-2 or plex-3 Formex!")

    nplex = path.nplex()
    if sleeptime is None:
        sleeptime = pf.cfg['draw/flywait']
    saved = delay(sleeptime)
    saved1 = pf.GUI.actions['Continue'].isEnabled()
    pf.GUI.enableButtons(pf.GUI.actions, ['Continue'], True)

    for elem in path:
        eye, center = elem[:2]
        if nplex == 3:
            upv = elem[2] - center
        else:
            upv = upvector
        pf.canvas.camera.lookAt(eye, center, upv)
        wait()
        pf.canvas.display()
        pf.canvas.update()
        image.saveNext()

    delay(saved)
    pf.GUI.enableButtons(pf.GUI.actions, ['Continue'], saved1)
    pf.canvas.camera.focus = center
    pf.canvas.camera.dist = length(center-eye)
    pf.canvas.update()


#################### viewports ##################################

### BEWARE FOR EFFECTS OF SPLITTING pf.canvas and pf.canvas if these
### are called from interactive functions!


def viewport(n=None):
    """Select the current viewport.

    n is an integer number in the range of the number of viewports,
    or is one of the viewport objects in pyformex.GUI.viewports

    if n is None, selects the current GUI viewport for drawing
    """
    if n is not None:
        pf.canvas.update()
        pf.GUI.viewports.setCurrent(n)
    pf.canvas = pf.GUI.viewports.current


def nViewports():
    """Return the number of viewports."""
    return len(pf.GUI.viewports.all)

def layout(nvps=None,ncols=None,nrows=None,pos=None,rstretch=None,cstretch=None):
    """Set the viewports layout."""
    pf.GUI.viewports.changeLayout(nvps, ncols, nrows, pos, rstretch, cstretch)
    viewport()

def addViewport():
    """Add a new viewport."""
    pf.GUI.viewports.addView()
    viewport()

def removeViewport():
    """Remove the last viewport."""
    if nViewports() > 1:
        pf.GUI.viewports.removeView()
    viewport()

def linkViewport(vp, tovp):
    """Link viewport vp to viewport tovp.

    Both vp and tovp should be numbers of viewports.
    """
    pf.GUI.viewports.link(vp, tovp)
    viewport()

####################

def updateGUI():
    """Update the GUI."""
    pf.GUI.update()
    pf.canvas.update()
    pf.app.processEvents()


######### Highlighting ###############
#
# Most highlight functions have been moved to canvas.py
# They are retained here for compatibility, but should be deprecated
#


def highlightActor(actor):
    """Highlight an actor in the scene."""
    pf.canvas.highlightActor(actor)
    pf.canvas.update()


def removeHighlight():
    """Remove the highlights from the current viewport"""
    pf.canvas.removeHighlight()
    pf.canvas.update()


def pick(mode='actor',filter=None,oneshot=False,func=None,pickable=None,prompt=None):
    """Enter interactive picking mode and return selection.

    See :func:`Canvas.pick` for more details.
    This function differs in that it provides default highlighting
    during the picking operation and a OK/Cancel buttons to stop
    the picking operation.

    Parameters:

    - `mode`: defines what to pick : one of 'actor', 'element', 'face',
      'edge', 'point' or 'prop'.
      'actor' picks complete actors. 'element' picks elements from one or
      more actor(s). 'face' and 'edge' pick faces, resp. edges of elements
      (only available for Mesh objects). 'point' picks points of Formices
      or nodes of Meshes. 'prop' is like 'element', but returns the property
      numbers of the picked elements instead of the element numbers.
    - `filter`: one of the `selection_filters`. The default picking filter is
      activated on entering the pick mode. All available filters are
      presented in a combobox.
    - `oneshot`: if True, the function returns as soon as the user ends
      a picking operation. The default is to let the user
      modify his selection and only to return after an explicit
      cancel (ESC or right mouse button).
    - `func`: if specified, this function will be called after each
      atomic pick operation. The Collection with the currently selected
      objects is passed as an argument. This can e.g. be used to highlight
      the selected objects during picking.
    - `pickable`: a list of Actors to pick from. The default is to use
      a list with all Actors having the pickable=True attribute (which is
      the default for newly constructed Actors).
    - `prompt`: the text printed to prompt the user to start picking. If None,
      a default prompt is printed. Specify an empty string to avoid printing
      a prompt.

    Returns a (possibly empty) Collection with the picked items.
    After return, the value of the pf.canvas.selection_accepted variable
    can be tested to find how the picking operation was exited:
    True means accepted (right mouse click, ENTER key, or OK button),
    False means canceled (ESC key, or Cancel button). In the latter case,
    the returned Collection is always empty
    """
    if mode == 'prop':
        return pickProps(filter,oneshot,func,pickable,prompt)

    subsel_values = { 'any': 'any vertex', 'all': 'all vertices' }

    def _set_selection_filter(s):
        """Set the selection filter mode

        This function is used to change the selection filter from the
        selection InputCombo widget.
        s is one of the strings in selection_filters.
        """
        s = str(s)
        if pf.canvas.pick_mode is not None and s in pf.canvas.selection_filters:
            pf.canvas.start_selection(None, s)


    def _set_subsel_mode(val):
        """Toggle the value of the subsel mode"""
        pf.canvas.pick_mode_subsel = str(val)[:3]


    if pf.canvas.pick_mode is not None:
        warning("You need to finish the previous picking operation first!")
        return

    if mode not in pf.canvas.getPickModes():
        warning("Invalid picking mode: %s. Expected one of %s." % (mode, pf.canvas.getPickModes()))
        return

    pick_buttons = widgets.ButtonBox('Selection:', [('Cancel', pf.canvas.cancel_selection), ('OK', pf.canvas.accept_selection)])

    if mode == 'element':
        filters = pf.canvas.selection_filters
    else:
        filters = pf.canvas.selection_filters[:3]
    filter_combo = widgets.InputCombo('Filter:', None, choices=filters, onselect=_set_selection_filter)
    if filter is not None and filter in pf.canvas.selection_filters:
        filter_combo.setValue(filter)

    if mode in [ 'actor', 'element', 'face', 'edge' ]:
        txt = subsel_values[pf.canvas.pick_mode_subsel]
        #subsel_button = widgets.ButtonBox('Pick by ', [(txt, _toggle_subsel_mode), ])
        subsel_button = widgets.InputCombo('Pick by ', txt, choices=list(subsel_values.values()), onselect=_set_subsel_mode)
    else:
        subsel_button = None

    if func is None:
        func = pf.canvas.highlight_funcs.get(mode, None)

    if prompt is None:
        prompt = "Pick: Mode %s; Filter %s" % (mode,filter)
    if prompt:
        print(prompt)

    pf.GUI.statusbar.addWidget(pick_buttons)
    pf.GUI.statusbar.addWidget(filter_combo)
    if subsel_button:
        pf.GUI.statusbar.addWidget(subsel_button)
    try:
        sel = pf.canvas.pick(mode, oneshot, func, filter, pickable)
    finally:
        # cleanup
        if pf.canvas.pick_mode is not None:
            pf.canvas.finish_selection()
        pf.GUI.statusbar.removeWidget(pick_buttons)
        pf.GUI.statusbar.removeWidget(filter_combo)
        if subsel_button:
            pf.GUI.statusbar.removeWidget(subsel_button)
    return sel


# These are undocumented, and deprecated: use pick() instead
def pickActors(filter=None,oneshot=False,func=None):
    return pick('actor', filter, oneshot, func)

def pickElements(filter=None,oneshot=False,func=None):
    return pick('element', filter, oneshot, func)

def pickPoints(filter=None,oneshot=False,func=None):
    return pick('point', filter, oneshot, func)

def pickEdges(filter=None,oneshot=False,func=None):
    return pick('edge', filter, oneshot, func)


# This could probably be moved into the canvas picking functions
def pickProps(filter=None,oneshot=False,func=None,pickable=None,prompt=None):
    """Pick property numbers

    This is like pick('element'), but returns the (unique) property numbers
    of the picked elements of the actors instead.
    """
    C = pick('element', filter, oneshot, func)
    for a in C.keys():
        actor = pf.canvas.actors[a]
        object = actor.object
        elems = C[a]
        if hasattr(object,'prop') and object.prop is not None:
            # Replace elem ids with unique props
            C[a] = np.unique(object.prop[elems])
        else:
            # Actor with no props: delete it
            del C.d[a]
    C.setType('prop')
    return C


def pickNumbers(marks=None):
    """Pick drawn numbers"""
    if marks:
        pf.canvas.numbers = marks
    return pf.canvas.pickNumbers()


def pickFocus():
    """Enter interactive focus setting.

    This enters interactive point picking mode and
    sets the focus to the center of the picked points.
    """
    K = pick('point',oneshot=True)
    removeHighlight()
    if K:
        X = []
        for k in K:
            a = pf.canvas.actors[k]
            o = a.object
            x = o.points()[K[k]]
            X.append(x.center())
        X = Coords(X).center()
        focus(X)


LineDrawing = None
edit_modes = ['undo', 'clear', 'close']


def set_edit_mode(s):
    """Set the drawing edit mode."""
    s = str(s)
    if s in edit_modes:
        pf.canvas.edit_drawing(s)


def drawLinesInter(mode ='line',single=False,func=None):
    """Enter interactive drawing mode and return the line drawing.

    See viewport.py for more details.
    This function differs in that it provides default displaying
    during the drawing operation and a button to stop the drawing operation.

    The drawing can be edited using the methods 'undo', 'clear' and 'close', which
    are presented in a combobox.
    """
    if pf.canvas.drawing_mode is not None:
        warning("You need to finish the previous drawing operation first!")
        return
    if func is None:
        func = showLineDrawing
    drawing_buttons = widgets.ButtonBox('Drawing:', [('Cancel', pf.canvas.cancel_drawing), ('OK', pf.canvas.accept_drawing)])
    pf.GUI.statusbar.addWidget(drawing_buttons)
    edit_combo = widgets.InputCombo('Edit:', None, choices=edit_modes, onselect=set_edit_mode)
    pf.GUI.statusbar.addWidget(edit_combo)
    lines = pf.canvas.drawLinesInter(mode, single, func)
    pf.GUI.statusbar.removeWidget(drawing_buttons)
    pf.GUI.statusbar.removeWidget(edit_combo)
    return lines


def showLineDrawing(L):
    """Show a line drawing.

    L is usually the return value of an interactive draw operation, but
    might also be set by the user.
    """
    global LineDrawing
    if LineDrawing:
        undecorate(LineDrawing)
        LineDrawing = None
    if L.size != 0:
        LineDrawing = decors.Lines(L, color='yellow', linewidth=3)
        decorate(LineDrawing)


def exportWebGL(fn,createdby=50,**kargs):
    """Export the current scene to WebGL.

    Parameters:

    - `fn` : string: the (relative or absolute) filename of the .html, .js
      and .pgf files comprising the WebGL model. It can contain a directory
      path and an any extension. The latter is dropped and not used.
    - `createdby`: int: width in pixels of the 'Created by pyFormex' logo
      appearing on the page. If < 0, the logo is displayed at its natural
      width. If 0, the logo is suppressed.
    - `**kargs`: any other keyword parameteris passed to the
      :class:`WebGL` initialization. The `name` can not be specified: it
      is derived from the `fn` parameter.

    Returns the absolute pathname of the generated .html file.
    """
    from pyformex.plugins.webgl import WebGL
    print("Exporting current scene to %s" % fn)
    pf.GUI.setBusy()
    if os.path.isabs(fn):
        chdir(os.path.dirname(fn))
    fn = os.path.basename(fn)
    name = utils.projectName(fn)
    W = WebGL(name=name,**kargs)
    W.addScene(name)
    fn = W.export(createdby=createdby)
    pf.GUI.setBusy(False)
    return fn


the_multiWebGL = None

def multiWebGL(name=None,fn=None,title=None,description=None,keywords=None,author=None,createdby=50):
    """Export the current scene to WebGL.

    fn is the (relative or absolute) pathname of the .html and .js files to be
    created.

    When the export is finished, returns the absolute pathname of the
    generated .html file. Else, returns None.
    """
    global the_multiWebGL

    from pyformex.plugins.webgl import WebGL
    ret = None
    pf.GUI.setBusy()
    if fn is not None:
        if the_multiWebGL is not None:
            the_multiWebGL.export()
            the_multiWebGL = None

        print("OK",the_multiWebGL)

        if os.path.isabs(fn):
            chdir(os.path.dirname(fn))
        fn = os.path.basename(fn)
        proj = utils.projectName(fn)
        print("PROJECT %s" % proj)
        the_multiWebGL = WebGL(proj, title=title, description=description, keywords=keywords, author=author)

    if the_multiWebGL is not None:
        if name is not None:
            print("Exporting current scene to %s" % the_multiWebGL.name)
            the_multiWebGL.addScene(name)

        elif fn is None: # No name, and not just starting
            print("Finishing export of %s" % the_multiWebGL.name)
            ret = the_multiWebGL.export(createdby=createdby)
            the_multiWebGL = None

    pf.GUI.setBusy(False)
    return ret


def showURL(url):
    """Show an URL in the browser

    - `url`: url to load
    """
    from pyformex.gui.menus.Help import help
    help(url)


def showHTML(fn=None):
    """Show a local .html file in the browser

    - `fn`: name of a local .html file. If unspecified, a FileDialog
      dialog is popped up to select a file.
    """
    if not fn:
        fn = askFilename(filter='html')
    if fn:
        showURL('file:%s' % fn)


################################

def setLocalAxes(mode=True):
    pf.cfg['draw/localaxes'] = mode
def setGlobalAxes(mode=True):
    setLocalAxes(not mode)


def resetGUI():
    """Reset the GUI to its default operating mode.

    When an exception is raised during the execution of a script, the GUI
    may be left in a non-consistent state.
    This function may be called to reset most of the GUI components
    to their default operating mode.
    """
    ## resetPick()
    pf.GUI.resetCursor()
    pf.GUI.enableButtons(pf.GUI.actions, ['Play', 'Step'], True)
    pf.GUI.enableButtons(pf.GUI.actions, ['Continue', 'Stop'], False)


###########################################################################
# import opengl specific drawing functions
#
from pyformex.opengl.draw import *


###########################################################################
# Make _I, _G and _T be included when doing 'from gui.draw import *'
#

__all__ = [ n for n in list(globals().keys()) if not n.startswith('_')] + ['_I', '_G', '_T']

#### End
