#
##
##  SPDX-FileCopyrightText: © 2007-2023 Benedict Verhegghe <bverheg@gmail.com>
##  SPDX-License-Identifier: GPL-3.0-or-later
##
##  This file is part of pyFormex 3.4  (Thu Nov 16 18:07:39 CET 2023)
##  pyFormex is a tool for generating, manipulating and transforming 3D
##  geometrical models by sequences of mathematical operations.
##  Home page: https://pyformex.org
##  Project page: https://savannah.nongnu.org/projects/pyformex/
##  Development: https://gitlab.com/bverheg/pyformex
##  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/.
##
"""Toolbars for the pyFormex GUI.
This module defines the functions for creating the pyFormex window toolbars.
"""
import pyformex as pf
from pyformex import utils
from pyformex.gui import QtGui, QPixmap
from pyformex.gui import widgets
############################## functions for the pick button ############
[docs]def pick(mode, *, tool='pix', filter=None, oneshot=False,
         func=None, pickable=None, prompt=None, _rect=None, minobj=0,
         **kargs):
    """Enter interactive picking mode and return selection.
    See :func:`gui.qtcanvas.Canvas.pick` for more details.
    This function differs in that it provides an extra interactive interface
    in the statusbar: OK/Cancel buttons to stop the picking operation,
    and comboboxes to change the picking tool and filters.
    Parameters
    ----------
    mode: str
        Defines what to pick : one of 'actor', 'element' or 'point'.
        'actor' picks complete actors. 'element' picks elements from one or
        more actor(s). 'point' picks points of Formices or nodes of Meshes.
    tool: str
        Defines what picking tool to use. One of 'pix', 'any' or 'all'.
        With 'pix' items are picked if they have any visible pixels in the
        pick rectangle. With 'any', items are picked if any of their points
        are inside the pick rectangle. With 'all', items are picked if all
        their points are inside the rectangle.
    filter: str
        The picking filter that is activated on entering the pick mode.
        It should be one of the Canvas.selection_filters: 'none', 'single',
        'closest', 'connected'.
        The active filter can be changed from a combobox in the statusbar.
        The available filters depend on the picking tool.
    oneshot: bool.
        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 to return only after an explicit
        cancel (ESC or right mouse button).
    func: callable, optional
        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: list of Actors, optional
        List of Actors from which can be picked. The default is to use
        a list with all Actors having the pickable=True attribute (which is
        the default for newly constructed Actors).
    prompt: str
        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.
    minobj: int
    Returns
    -------
    Collection
        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. Ther returned Collection
        also remains available in pf.canvas.selection until a new pick is
        started.
    """
    from pyformex.gui import draw
    def _tool_filter_choices(tool):
        """Return the possible selection filters for the tool"""
        if mode == 'element':
            filters = pf.canvas.selection_filters
        else:
            filters = pf.canvas.selection_filters[:3]
        return filters
    def _set_selection_filter(item):
        """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 = item.value()
        if pf.canvas.pick_mode is not None and s in pf.canvas.selection_filters:
            # filter changed during picking: restart
            pf.canvas.start_selection(None, pf.canvas.pick_tool, s)
    def _set_pick_tool(item):
        """Set the value of the pick tool (first 3 chars)"""
        nonlocal filters, filter_combo, tool
        pf.canvas.pick_tool = item.value()[:3].lower()
        filters = _tool_filter_choices(tool)
        #print("CHANGE FILTERS",tool,filters)
        filter_combo.setChoices(filters)
        filter_combo.setValue(filters[0])
    if pf.canvas.pick_mode is not None:
        draw.warning("You need to finish the previous picking operation first!")
        return
    if mode not in pf.canvas.pick_modes:
        draw.warning(f"Invalid picking {mode=}. "
                     f"Expected one of {pf.canvas.pick_modes=}")
        return
    pick_buttons = widgets.ButtonBox('Selection:', [
        ('Cancel', pf.canvas.cancel_selection),
        ('OK', pf.canvas.accept_selection)])
    # combobox for filter selection
    filters = _tool_filter_choices(tool)
    filter_combo = widgets.InputCombo(
        'Filter:', None, choices=filters, func=_set_selection_filter)
    if filter is not None and filter in filters:
        filter_combo.setValue(filter)
    # combobox for tool switching
    pick_tools = {'pix': 'Pixels', 'any': 'Any Point', 'all': 'All Points'}
    txt = pick_tools[tool]
    tool_combo = widgets.InputCombo(
        'Pick by ', txt, choices=list(pick_tools.values()),
        func=_set_pick_tool)
    if prompt is None:
        prompt = f"Pick: Mode {mode}; Tool {tool}; Filter {filter}"
    if prompt:
        print(prompt)
    pf.GUI.statusbar.addWidget(pick_buttons)
    pf.GUI.statusbar.addWidget(filter_combo)
    pf.GUI.statusbar.addWidget(tool_combo)
    try:
        if pf.debugon(pf.DEBUG.PICK):
            print(f"PICK {mode=}, {tool=}, {func=}, {filter=}, "
                  f"{pickable=}, {_rect=}, {minobj=}")
        sel = pf.canvas.pick(mode, tool, oneshot, func,
                                 filter, pickable, _rect, minobj)
    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)
        pf.GUI.statusbar.removeWidget(tool_combo)
    return sel 
[docs]def picksel(obj_type, **kargs):
    """Pick and print selection"""
    sel = pick(obj_type, **kargs)
    print(sel) 
def report_func(self):
    from pyformex.plugins import tools
    self.removeHighlight()
    self.highlightSelection(self.picked)
    print(tools.report(self.picked))
[docs]def query(obj_type, **kargs):
    """Enter interactive query mode.
    Enters a continuous picking mode where the user can pick objects
    and the picked objects are reported in detail. This is usually called
    from the toolbar Query buttons. The query mode stays active until
    the user cancels pick mode (with ESC or right mouse button). distan
    Parameters
    ----------
    obj_type: str
        Defines what to pick : one of 'actor', 'element' or 'point'
    **kargs:
        Extra parameters to be passed to :func:`pick`
    """
    return pick(obj_type, func=report_func, **kargs) 
# TODO: we could have a mode that shows distance on every point selected
[docs]def query_distance(colorid=0, colorstep=1, show=True, prec=None):
    """Enter interactive distance query mode
    In distance query mode, the user picks subsequent single points.
    For every second point picked, the distance to the previous point
    is shown. The distances are shown and printed with subsequent colors
    from the canvas default colormap or from a custom colormap in
    pf.cfg['draw/querypalette'].
    """
    import numpy as np
    from pyformex.formex import Formex
    from pyformex.gui import draw
    if prec is None:
        prec = np.get_printoptions()['precision']
    def every2_showdistance(self):
        nonlocal points, colorid, drawn
        if not self.picked:
            return
        k, i = next(self.picked.singles())
        self.actors[k].addHighlightPoints(np.array([i]))
        P = self.actors[k].object.points()[i]
        print(f"Actor {k}, point {i}, coords {P}")
        points.append([k, i, P])
        if len(points) % 2 == 0:
            P0, P1 = (points[i][2] for i in range(-2, 0))
            d = P1.distanceFromPoint(P0)
            draw.printc(f"Distance: {d:.{prec}}", color=colorid)
            if show:
                lines = draw.draw(
                    Formex([[P0, P1]]), linewidth=2, color=colorid,
                    rendertype=4, ontop=True, bbox='last', view=None)
                marks = draw.drawMarks(
                    [0.5 * (P0 + P1)], [f"={d:.{prec}}"], size=20, color=colorid)
                pf.canvas.update()
                drawn.extend([lines, marks])
            colorid += colorstep
    points=[]
    drawn = []
    print("Pick single points, every 2nd shows distance")
    print(f"{pf.cfg['draw/querypalette']=}")
    with draw.TempPalette(pf.cfg['draw/querypalette']):
        pick('point', filter='closest', func=every2_showdistance, prompt='')
    return {'points': points, 'drawn': drawn} 
[docs]def query_angle(colorid=0, colorstep=1, show=True, prec=None):
    """Enter interactive angle query mode
    In angle query mode, the user picks subsequent single points.
    For every third point picked, the angle between the vectors from
    first to second and from secont to third is shown.
    The angles are shown and printed with subsequent colors
    from the canvas default colormap or from a custom colormap in
    pf.cfg['draw/querypalette'].
    """
    import numpy as np
    from pyformex.formex import Formex
    from pyformex.gui import draw
    from pyformex import geomtools as gt
    if prec is None:
        prec = np.get_printoptions()['precision']
    def angle_report(P0, P1, P2, color):
        angle, n = gt.rotationAngle(P0-P1, P2-P1)
        angle, n = angle[0], n[0]
        draw.printc(f"Angle: {angle:.{prec}} degrees, axis: {n}",
                        color=color)
        lines = draw.draw(Formex([[P0, P1],[P1, P2]]), linewidth=3, color=color,
                          rendertype=4, ontop=True, bbox='last', view=None)
        marks = draw.drawMarks([(P0+P1+P2)/3], [f"{angle:.{prec}}"], size=20,
                               color=color, gravity='')
        pf.canvas.update()
        return [lines, marks]
    def every3_showangle(self):
        nonlocal points, colorid, drawn, temp
        if not self.picked:
            return
        k, i = next(self.picked.singles())
        self.actors[k].addHighlightPoints(np.array([i]))
        P = self.actors[k].object.points()[i]
        print(f"Actor {k}, point {i}, coords {P}")
        points.append([k, i, P])
        if len(points) % 3 == 2:
            P0, P1 = (points[i][2] for i in range(-2, 0))
            temp = draw.draw(Formex([[P0, P1]]), linewidth=3,
                             color=colorid, rendertype=4, ontop=True,
                             bbox='last', view=None)
        elif len(points) % 3 == 0:
            P0, P1, P2 = (points[i][2] for i in range(-3, 0))
            drawn += angle_report(P0, P1, P2, colorid)
            colorid += colorstep
            draw.undraw(temp)
            temp = None
    temp = None
    points = []
    drawn = []
    print("Pick single points, every 3rd shows angle at middle point")
    with draw.TempPalette(pf.cfg['draw/querypalette']):
        pick('point', filter='closest', func=every3_showangle, prompt='')
    draw.undraw(temp)
    return {'points': points, 'drawn': drawn} 
################### General Button Functions ###########
################### Main toolbar ###########
pickmenudata ={
    'title': 'Pick tools',
    'items' : [
        ('Pick point', picksel, {'data':'point', 'icon':'pick-point'}),
        ('Pick element', picksel, {'data':'element', 'icon':'pick-element'}),
        ('Pick actor', picksel, {'data':'actor', 'icon':'pick-actor'}),
    ],
    'default': 'Pick element',
}
querymenudata = {
    'title': 'Query tools',
    'items' : [
        ('Query point', query, {'data':'point', 'icon':'query-point'}),
        ('Query element', query, {'data':'element', 'icon':'query-element'}),
        ('Query actor', query, {'data':'actor', 'icon':'query-actor'}),
        ('Query distance', query_distance, {'icon':'query-dist'}),
        ('Query angle', query_angle, {'icon':'query-angle'}),
   ],
    'default': 'Query element',
}
################# Camera action toolbar ###############
#######################################################################
# Canvas Toggle buttons #
#########################
[docs]class ViewportToggle(widgets.ToggleToolButton):
    """A toolbar button that toggles the state of a Viewpor attribute
    attr: one of 'perspective',
    """
    def __init__(self, toolbar, icons, attr, checked=False, tooltip=''):
        self._vp = toolbar.parent().viewports
        self.attr = attr
        super().__init__(
            toolbar, icons, func=self.setstate, status=self.getstate,
            checked=self.getstate(), # THIS DOES NOT WORK
            #checked=False,
            tooltip=tooltip)
[docs]    def getstate(self):
        """Get the current state of the viewport attribute."""
        vp = self._vp.current
        return False if vp is None else vp.getToggle(self.attr) 
[docs]    def setstate(self, onoff=None):
        """Toggle the state of the viewport attribute."""
        vp = self._vp.current
        if onoff is None:
            onoff = not vp.getToggle(self.attr)
        else:
            onoff = bool(onoff)
        vp.setToggle(self.attr, onoff)
        vp.update()
        pf.app.processEvents()  
def updateViewportButtons(vp):
    if vp.focus:
        transparency_button.update_status()
        light_button.update_status()
        normals_button.update_status()
        perspective_button.update_status()
        wire_button.update_status()
################# Wire Button ###############
# TODO: this is special: toggles an int between + and -
wire_button = None  # the toggle wire button
def addWireButton(toolbar):
    global wire_button
    def wire_button_getstate():
        vp = toolbar.parent().viewports.current
        return False if vp is None else vp.settings['wiremode'] > 0
    wire_button = widgets.ToggleToolButton(
        toolbar, icons=('wirenone', 'wireall'),
        func=wire_button_setstate, status=wire_button_getstate,
        checked=False, tooltip='Toggle Wire Mode')
def wire_button_setstate():
    vp = pf.GUI.viewports.current
    vp.setWireMode()
    vp.update()
    pf.app.processEvents()
################# Transparency Button ###############
transparency_button = None  # the toggle transparency button
def addTransparencyButton(toolbar):
    global transparency_button
    transparency_button = ViewportToggle(
        toolbar, icons=('transparent', 'transparent'), attr='alphablend',
        checked=False, tooltip='Toggle Transparent Mode')
################# Lights Button ###############
light_button = None
def addLightButton(toolbar):
    global light_button
    light_button = ViewportToggle(
        toolbar, icons=('lamp', 'lamp-on'), attr='lighting',
        checked=False, tooltip='Toggle Lights')
################# Normals Button ###############
normals_button = None
def addNormalsButton(toolbar):
    global normals_button
    normals_button = ViewportToggle(
        toolbar, icons=('normals-ind', 'normals-avg'), attr='avgnormals',
        checked=False, tooltip='Toggle Normals Mode')
################# Perspective Button ###############
perspective_button = None
def addPerspectiveButton(toolbar):
    global perspective_button
    perspective_button = ViewportToggle(
        toolbar, icons=('project', 'perspect'), attr='perspective',
        checked=True, tooltip='Toggle Perspective/Projective Mode')
def setPerspective():
    perspective_button.setstate(True)
def setProjection():
    perspective_button.setstate(False)
################# Timeout Button ###############
timeout_button = None  # the timeout toggle button
def toggleTimeout(onoff=None):
    if onoff is None:
        onoff = widgets.input_timeout < 0
    if onoff:
        timeout = pf.cfg['gui/timeoutvalue']
    else:
        timeout = -1
    widgets.setInputTimeout(timeout)
    onoff = widgets.input_timeout > 0
    if onoff:
        # THIS SUSPENDS ALL WAITING! WE SHOULD IMPLEMENT A TIMEOUT!
        # BY FORCING ALL INDEFINITE PAUSES TO A WAIT TIME EQUAL TO
        # WIDGET INPUT TIMEOUT
        pf.debug("FREEING the draw lock")
        pf.GUI.drawlock.free()
    else:
        pf.debug("ALLOWING the draw lock")
        pf.GUI.drawlock.allow()
    return onoff
[docs]def timeout(onoff=None):
    """Programmatically toggle the timeout button"""
    if timeout_button is not None:
        timeout_button.setChecked(toggleTimeout(onoff)) 
# End