"""ZV Radial Blend Shape

Copyright (C) 2021 Paolo Dominici

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

Usage:
import zvradialblendshape.ui
zvradialblendshape.ui.show()
"""

__product__ = 'ZV Radial Blend Shape'
__copyright__ = 'Copyright (c) 2021 Paolo Dominici'
__author__ = 'Paolo Dominici (paolodominici@gmail.com)'
__version__ = '3.0.3'
__date__ = '2021/08/22'


import sys
import webbrowser
from maya import cmds

# change the following suffixes as you like
_rbs_node_name = 'radialBlendShape'
_eyelid_loc_sfx = '_lid_loc'
_eye_loc_sfx = '_eye_loc'
_eye_parent_loc_sfx = '_eyeRest_loc'
_head_grp_sfx = '_parentToHead_grp'
_eyelid_grp_name = '_lid_grp'
_loc_grp_name = '%s_grp' % _rbs_node_name

_controls = {}
_label_list = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
_ordered_label_list = 'LRABCDEFGHIJKMNOPQSTUVWXYZ'

_auto_memb_tolerance = 0.0001

# use component tags if maya version is greater or equal than 2022
_use_cmp_tags = int(cmds.about(mjv=True)) >= 2022
_cmp_tag_name = 'rbs'

# the following names match the attribute names from the deformer node, do not touch!
_attrs = ['blend', 'twist', 'follow', 'followMaxAngle', 'followOpen', 'followClose', 'maxDisplacement']
_blink_attrs = ['blink', 'blinkBias']
_bulge_attrs = ['bulgeDisplacement', 'bulgeRadius']
_offset_attrs = ['offsetIn', 'offsetMid', 'offsetOut']
_offset_influence_attrs = ['offsetInfluenceAngle']
_target_attrs = ['inputMesh', 'inputOppositeMesh', 'inputMatrix', 'eyeMatrix', 'eyeParentMatrix', 'targetWeights']
_compute_disp_attr = 'computeDisplacements'
_def_attr_values = [0.0, 0.0, 1.0, 40.0, 0.2, 0.2, -1.0]
_def_bulge_values = [0.5, 30.0]
_def_offset_influence_values = [120.0]
_w_attr = _target_attrs[-1]


def cb(method, *args):
    """Easy-call UI callback method."""

    return lambda *x: method(*args)


def _get_membership_set(rbs_node):
    return cmds.listConnections('%s.message' % rbs_node, type='objectSet', s=False)[0]


def _parent(obj, parent):
    objParentList = cmds.listRelatives(obj, p=True, pa=True)
    if objParentList and objParentList[0] == parent:
        return obj

    return cmds.parent(obj, parent, r=True)[0]


def _set_alias(attr, alias):
    if cmds.aliasAttr(attr, q=True) != alias:
        cmds.aliasAttr(alias, attr)


def _get_eye_idx_list(rbs_node):
    return _get_attr_idx_list(rbs_node, 'it')


def _get_attr_idx_list(rbs_node, attr_name):
    return cmds.getAttr('%s.%s' % (rbs_node, attr_name), mi=True) or []


def _get_eye_idx_prfx_list(rbs_node):
    """Returns the eye prefixes (like (0, 'L') or (2, 'R'))."""

    miList = _get_eye_idx_list(rbs_node)
    nameList = [(idx, _get_target_name_from_idx(rbs_node, idx)) for idx in miList]
    result = []
    for idx, name in nameList:
        if name:
            elem = (idx - (idx%2), name[0])
            if not elem in result:
                result.append(elem)

    return result


def _get_target_name_from_idx(rbs_node, idx):
    """Returns the input target name from the given index."""
    return cmds.aliasAttr('%s.it[%d]' % (rbs_node, idx), q=True) or None


def _get_selected_shape(shapeType=None):
    if shapeType:
        sel = cmds.ls(sl=True, typ=shapeType) or cmds.listRelatives(s=True, ni=True, pa=True, typ=shapeType)
    else:
        sel = cmds.ls(sl=True, shapes=True) or cmds.listRelatives(s=True, ni=True, pa=True)

    return sel and cmds.listRelatives(sel[0], p=True, pa=True)[0] or None


def _get_rbs(geo):
    """Get rbs from history."""

    rbs_nodeList = [s for s in cmds.listHistory(geo, pdo=True, il=2) or [] if cmds.nodeType(s) == _rbs_node_name]
    return rbs_nodeList and rbs_nodeList[0] or None


def _get_geo_from_ui():
    return cmds.text(_controls['geo'], q=True, l=True) or None


def _add_eye_dialog(rbs_node):
    form = cmds.setParent(q=True)

    # used prefixes
    idxPrfxList = _get_eye_idx_prfx_list(rbs_node)
    prfxList = [s[1] for s in idxPrfxList]

    # available prefixes
    availList = [s for s in _label_list if not s in prfxList]

    # preferred prefix
    selPrfx = None
    for prfx in _ordered_label_list:
        if prfx in availList:
            selPrfx = prfx
            break

    menu = cmds.optionMenu(label='Please choose a name.\nUsually "L" or "R"   ')
    for l in availList:
        cmds.menuItem(label=l)
    if selPrfx:
        cmds.optionMenu(menu, e=True, v=selPrfx)

    sep = cmds.separator(style='none')
    b = cmds.button(l='OK', c=lambda *x: cmds.layoutDialog(dismiss=cmds.optionMenu(menu, q=True, v=True)))
    cmds.formLayout(form, e=True, attachForm=[(menu, 'top', 10), (menu, 'left', 10), (menu, 'right', 2), (b, 'bottom', 2), (b, 'left', 2), (b, 'right', 2)], attachControl=[(sep, 'top', 4, menu), (sep, 'bottom', 4, b)])


def _mirror_weights_dialog(rbs_node, destPrfx):
    """Mirror bulge weights dialog box."""

    form = cmds.setParent(q=True)

    # used prefixes
    idxPrfxList = _get_eye_idx_prfx_list(rbs_node)
    prfxList = [s[1] for s in idxPrfxList if s[1] != destPrfx]

    menu = cmds.optionMenu(label='Mirror bulge weights from\n(the current weights will be destroyed):')
    for l in prfxList:
        cmds.menuItem(label=l)

    cmds.optionMenu(menu, e=True, sl=1)

    sep = cmds.separator(style='none')
    b = cmds.button(l='OK', c=lambda *x: cmds.layoutDialog(dismiss=cmds.optionMenu(menu, q=True, v=True)))
    cmds.formLayout(form, e=True, attachForm=[(menu, 'top', 10), (menu, 'left', 10), (menu, 'right', 2), (b, 'bottom', 2), (b, 'left', 2), (b, 'right', 2)], attachControl=[(sep, 'top', 4, menu), (sep, 'bottom', 4, b)])


def _disconnect_and_remove_attr(attr, remove=False):
    """Disconnects inputs and outputs from the given attribute."""

    sel = cmds.ls(sl=True)
    cmds.select(cl=True)

    # unlock if needed
    cmds.setAttr(attr, l=False)

    # if any connection, disconnect
    srcAttrs = cmds.listConnections(attr, d=False, p=True) or []
    destAttrs = cmds.listConnections(attr, s=False, p=True) or []
    for srcAttr in srcAttrs:
        cmds.disconnectAttr(srcAttr, attr)

    for destAttr in destAttrs:
        cmds.disconnectAttr(attr, destAttr)

    # remove element
    if remove:
        cmds.removeMultiInstance(attr)

        # remove alias
        if cmds.aliasAttr(attr, q=True):
            cmds.aliasAttr(attr, rm=True)

    cmds.select(sel or None)


def _set_attr_aliases(rbs_node, idx):
    """Sets aliases for blend, twist, etc... and remove unused attributes."""

    targetName = _get_target_name_from_idx(rbs_node, idx)
    if not targetName:
        return

    # set upper and lower attr names
    for attr_name in _attrs + _offset_attrs:
        if not idx in _get_attr_idx_list(rbs_node, attr_name):
            continue

        attr = '%s.%s[%d]' % (rbs_node, attr_name, idx)
        name = '%s_%s' % (targetName, attr_name)
        _set_alias(attr, name)

    # set common attr names, up only
    for attr_name in _bulge_attrs + _blink_attrs:
        if not idx in _get_attr_idx_list(rbs_node, attr_name):
            continue

        attr = '%s.%s[%d]' % (rbs_node, attr_name, idx)
        name = '%s_%s' % (targetName[0], attr_name)
        _set_alias(attr, name)

    # set the up only attributes which are set even if the idx is lower
    for attr_name in _offset_influence_attrs:
        upIdx = idx + 1 - idx%2
        if not upIdx in _get_attr_idx_list(rbs_node, attr_name):
            continue

        attr = '%s.%s[%d]' % (rbs_node, attr_name, upIdx)
        name = '%s_%s' % (targetName[0], attr_name)
        _set_alias(attr, name)


def _create_eye_locators(rbs_node, lowerIdx):
    prfx = _get_target_name_from_idx(rbs_node, lowerIdx)[0]

    # create locs or get existing ones
    eyelidLocList = cmds.ls(prfx + _eyelid_loc_sfx, transforms=True)
    eyeLocList = cmds.ls(prfx + _eye_loc_sfx, transforms=True)
    eyeParentLocList = cmds.ls(prfx + _eye_parent_loc_sfx, transforms=True)
    headGrpList = cmds.ls(prfx + _head_grp_sfx, transforms=True)

    eyelidLoc = eyelidLocList and eyelidLocList[0] or cmds.spaceLocator(n=prfx + _eyelid_loc_sfx)[0]
    eyeLoc = eyeLocList and eyeLocList[0] or cmds.spaceLocator(n=prfx + _eye_loc_sfx)[0]
    eyeParentLoc = eyeParentLocList and eyeParentLocList[0] or cmds.spaceLocator(n=prfx + _eye_parent_loc_sfx)[0]
    headGrp = headGrpList and headGrpList[0] or cmds.group(em=True, n=prfx + _head_grp_sfx)

    # make the hierarchy
    eyeLoc = _parent(eyeLoc, eyeParentLoc)
    eyeParentLoc = _parent(eyeParentLoc, headGrp)

    # colors
    eyelidLocShape = cmds.listRelatives(eyelidLoc, s=True, pa=True)[0]
    eyeLocShape = cmds.listRelatives(eyeLoc, s=True, pa=True)[0]
    eyeParentLocShape = cmds.listRelatives(eyeParentLoc, s=True, pa=True)[0]
    cmds.setAttr(eyelidLocShape + ".overrideEnabled", True)
    cmds.setAttr(eyelidLocShape + ".overrideColor", 17)
    cmds.setAttr(eyeLocShape + ".overrideEnabled", True)
    cmds.setAttr(eyeLocShape + ".overrideColor", 28)
    cmds.setAttr(eyeParentLocShape + ".localScale", 0.2, 0.2, 0.2)
    cmds.setAttr(eyeParentLocShape + ".overrideEnabled", True)
    cmds.setAttr(eyeParentLocShape + ".overrideColor", 29)

    # connect
    for idx in [lowerIdx, lowerIdx+1]:
        cmds.connectAttr('%s.worldMatrix' % eyelidLoc, '%s.it[%d].%s' % (rbs_node, idx, _target_attrs[2]))
        cmds.connectAttr('%s.matrix' % eyeLoc, '%s.it[%d].%s' % (rbs_node, idx, _target_attrs[3]))
        cmds.connectAttr('%s.matrix' % eyeParentLoc, '%s.it[%d].%s' % (rbs_node, idx, _target_attrs[4]))

    # get the sub group
    subGrpList = cmds.ls(prfx + _eyelid_grp_name, transforms=True)
    subGrp = subGrpList and subGrpList[0] or cmds.group(em=True, n=prfx + _eyelid_grp_name)

    # get or create the top group
    locGrpList = cmds.ls(_loc_grp_name, transforms=True)
    locGrp = locGrpList and locGrpList[0] or cmds.group(em=True, n=_loc_grp_name)

    # parent to top group
    _parent(eyelidLoc, subGrp)
    _parent(headGrp, subGrp)
    _parent(subGrp, locGrp)


def _select_tab(prfx):
    tabNameList = cmds.tabLayout(_controls['tab_lay'], q=True, tli=True) or []
    if prfx in tabNameList:
        cmds.tabLayout(_controls['tab_lay'], e=True, sti=tabNameList.index(prfx)+1)


def _update_tab_ui(rbs_node):
    """Re-generate tabs, one per eye."""

    # delete all content
    content = cmds.tabLayout(_controls['tab_lay'], q=True, ca=True) or []
    for control in content:
        cmds.deleteUI(control)

    if not rbs_node:
        return

    content = []
    idxPrfxList = _get_eye_idx_prfx_list(rbs_node)

    # for each eye, create content
    for lowerIdx, prfx in idxPrfxList:
        upperIdx = lowerIdx + 1

        # get info for enabling ui elements
        isBulgeEnabled = (lowerIdx+1) in _get_attr_idx_list(rbs_node, _bulge_attrs[0])
        isMirrorEnabled = len(_get_attr_idx_list(rbs_node, _bulge_attrs[0])) > 1
        connList = cmds.listConnections('%s.it[%d].%s' % (rbs_node, lowerIdx, _target_attrs[0]), d=False)
        lowerGeo = connList and connList[0]
        lowerGeoData = bool(cmds.getAttr('%s.it[%d].%s' % (rbs_node, lowerIdx, _target_attrs[0]), type=True))
        connList = cmds.listConnections('%s.it[%d].%s' % (rbs_node, lowerIdx, _target_attrs[1]), d=False)
        lowerOppGeo = connList and connList[0]
        lowerOppGeoData = bool(cmds.getAttr('%s.it[%d].%s' % (rbs_node, lowerIdx, _target_attrs[1]), type=True))
        connList = cmds.listConnections('%s.it[%d].%s' % (rbs_node, upperIdx, _target_attrs[0]), d=False)
        upperGeo = connList and connList[0]
        upperGeoData = bool(cmds.getAttr('%s.it[%d].%s' % (rbs_node, upperIdx, _target_attrs[0]), type=True))
        connList = cmds.listConnections('%s.it[%d].%s' % (rbs_node, upperIdx, _target_attrs[1]), d=False)
        upperOppGeo = connList and connList[0]
        upperOppGeoData = bool(cmds.getAttr('%s.it[%d].%s' % (rbs_node, upperIdx, _target_attrs[1]), type=True))
        connList = cmds.listConnections('%s.%s[%d]' % (rbs_node, _attrs[0], upperIdx), d=False)
        upperMainCtrl = connList and connList[0]
        upperMainCtrlData = bool(connList)
        connList = cmds.listConnections('%s.%s[%d]' % (rbs_node, _attrs[0], lowerIdx), d=False)
        lowerMainCtrl = connList and connList[0]
        lowerMainCtrlData = bool(connList)
        upperOffsetCtrls = []
        upperOffsetCtrlsData = []
        lowerOffsetCtrls = []
        lowerOffsetCtrlsData = []
        for attr in _offset_attrs:
            connList = cmds.listConnections('%s.%s[%d]' % (rbs_node, attr, upperIdx), d=False)
            upperOffsetCtrls.append(connList and connList[0])
            upperOffsetCtrlsData.append(bool(connList))
            connList = cmds.listConnections('%s.%s[%d]' % (rbs_node, attr, lowerIdx), d=False)
            lowerOffsetCtrls.append(connList and connList[0])
            lowerOffsetCtrlsData.append(bool(connList))

        cmds.setParent(_controls['tab_lay'])

        # create tab layout
        eyeTab = cmds.formLayout()

        # add the remove target button
        rmSideBT = cmds.button(l='Remove %s' % prfx, ann='Remove shape group', c=cb(remove_eye_cmd, rbs_node, lowerIdx))

        # upper shape
        cmds.setParent(eyeTab)
        f1 = cmds.frameLayout(l='Upper eyelid', mw=2, mh=2)
        cmds.columnLayout(adj=True)
        cmds.rowLayout(nc=4, adj=4, ct4=['both', 'both', 'both', 'both'], cl4=['center', 'center', 'center', 'left'], cw=[1, 120])
        cmds.button(l='Set upper closed', ann='Set upper lid closed shape', bgc=(0.4, 0.8, 0.4), c=cb(set_lid_from_selection_cmd, rbs_node, upperIdx))
        cmds.button(l='Regen', ann='Regenerate shape', en=upperGeoData and not upperGeo, c=cb(regen_cmd, rbs_node, upperIdx))
        cmds.separator(w=8, st='none')
        cmds.text(l=upperGeo or upperGeoData and '*STORED*' or '*MISSING*')
        cmds.setParent('..')
        cmds.rowLayout(nc=4, adj=4, ct4=['both', 'both', 'both', 'both'], cl4=['center', 'center', 'center', 'left'], cw=[1, 120])
        cmds.button(l='Set upper open', ann='Set upper lid open shape', bgc=(0.4, 0.7, 0.9), en=upperGeoData, c=cb(set_lid_from_selection_cmd, rbs_node, upperIdx, True))
        cmds.button(l='Regen', ann='Regenerate shape', en=upperOppGeoData and not upperOppGeo, c=cb(regen_cmd, rbs_node, upperIdx, True))
        cmds.separator(w=8, st='none')
        cmds.text(l=upperOppGeo or upperOppGeoData and '*STORED*' or '*OPTIONAL*')
        cmds.setParent('..')
        cmds.separator(h=8, st='none')

        # main upper control button
        cmds.rowLayout(nc=4, adj=4, ct4=['both', 'both', 'both', 'both'], cl4=['center', 'center', 'center', 'left'], cw=[1, 100])
        cmds.button(l='Set main control', ann='Set upper lid control', bgc=(0.7, 0.5, 0.9), en=upperGeoData, c=cb(set_main_control_cmd, rbs_node, upperIdx), h=18)
        cmds.button(l='Select', ann='Select the assigned upper lid control', en=upperMainCtrlData, c=cb(select_main_control_cmd, rbs_node, upperIdx), h=18)
        cmds.separator(w=8, st='none')
        cmds.text(l=upperMainCtrl or '*MISSING*')
        cmds.setParent('..')

        for i, attr in enumerate(_offset_attrs):
            cmds.rowLayout(nc=4, adj=4, ct4=['both', 'both', 'both', 'both'], cl4=['center', 'center', 'center', 'left'], cw=[1, 100])
            name = attr.replace('offset', '').upper()
            cmds.button(l='Set %s control' % name, ann='Set upper lid %s offset control' % name, en=upperGeoData, c=cb(set_offset_control_cmd, rbs_node, upperIdx, i), h=18)
            cmds.button(l='Select', ann='Select the assigned offset control', en=upperOffsetCtrlsData[i], c=cb(select_offset_control_cmd, rbs_node, upperIdx, i), h=18)
            cmds.separator(w=8, st='none')
            cmds.text(l=upperOffsetCtrls[i] or '*OPTIONAL*')
            cmds.setParent('..')


        # lower shape
        cmds.setParent(eyeTab)
        f2 = cmds.frameLayout(l='Lower eyelid', mw=2, mh=2)
        cmds.columnLayout(adj=True)
        cmds.rowLayout(nc=4, adj=4, ct4=['both', 'both', 'both', 'both'], cl4=['center', 'center', 'center', 'left'], cw=[1, 120])
        cmds.button(l='Set lower closed', ann='Set lower lid closed shape', bgc=(0.4, 0.8, 0.4), c=cb(set_lid_from_selection_cmd, rbs_node, lowerIdx))
        cmds.button(l='Regen', ann='Regenerate shape', en=lowerGeoData and not lowerGeo, c=cb(regen_cmd, rbs_node, lowerIdx))
        cmds.separator(w=8, st='none')
        cmds.text(l=lowerGeo or lowerGeoData and '*STORED*' or '*MISSING*')
        cmds.setParent('..')
        cmds.rowLayout(nc=4, adj=4, ct4=['both', 'both', 'both', 'both'], cl4=['center', 'center', 'center', 'left'], cw=[1, 120])
        cmds.button(l='Set lower open', ann='Set lower lid open shape', bgc=(0.4, 0.7, 0.9), en=lowerGeoData, c=cb(set_lid_from_selection_cmd, rbs_node, lowerIdx, True))
        cmds.button(l='Regen', ann='Regenerate shape', en=lowerOppGeoData and not lowerOppGeo, c=cb(regen_cmd, rbs_node, lowerIdx, True))
        cmds.separator(w=8, st='none')
        cmds.text(l=lowerOppGeo or lowerOppGeoData and '*STORED*' or '*OPTIONAL*')
        cmds.setParent('..')
        cmds.separator(h=8, st='none')

        # main lower control button
        cmds.rowLayout(nc=4, adj=4, ct4=['both', 'both', 'both', 'both'], cl4=['center', 'center', 'center', 'left'], cw=[1, 100])
        cmds.button(l='Set main control', ann='Set lower lid control', bgc=(0.7, 0.5, 0.9), en=lowerGeoData, c=cb(set_main_control_cmd, rbs_node, lowerIdx), h=18)
        cmds.button(l='Select', ann='Select the assigned lower lid control', en=lowerMainCtrlData, c=cb(select_main_control_cmd, rbs_node, lowerIdx), h=18)
        cmds.separator(w=8, st='none')
        cmds.text(l=lowerMainCtrl or '*MISSING*')
        cmds.setParent('..')

        for i, attr in enumerate(_offset_attrs):
            cmds.rowLayout(nc=4, adj=4, ct4=['both', 'both', 'both', 'both'], cl4=['center', 'center', 'center', 'left'], cw=[1, 100])
            name = attr.replace('offset', '').upper()
            cmds.button(l='Set %s control' % name, ann='Set lower lid %s offset control' % name, en=lowerGeoData, c=cb(set_offset_control_cmd, rbs_node, lowerIdx, i), h=18)
            cmds.button(l='Select', ann='Select the assigned offset control', en=lowerOffsetCtrlsData[i], c=cb(select_offset_control_cmd, rbs_node, lowerIdx, i), h=18)
            cmds.separator(w=8, st='none')
            cmds.text(l=lowerOffsetCtrls[i] or '*OPTIONAL*')
            cmds.setParent('..')

        # bulging
        cmds.setParent(eyeTab)
        f3 = cmds.frameLayout(l='Eye bulge', mw=2, mh=2)
        cmds.columnLayout(adj=True)
        cmds.rowLayout(nc=5, ct5=['both', 'both', 'both', 'both', 'both'], cl5=['center', 'center', 'center', 'center', 'left'])
        cmds.separator(w=8, st='none')
        cmds.checkBox(l='Enable', v=isBulgeEnabled, ofc=cb(set_bulge_cmd, rbs_node, upperIdx, False), onc=cb(set_bulge_cmd, rbs_node, upperIdx, True))
        cmds.separator(w=4, st='none')
        cmds.button(l='Paint bulge weights', ann='Paint bulge weights for %s eye' % prfx, en=isBulgeEnabled, c=cb(paint_weights_cmd, rbs_node, upperIdx))
        cmds.button(l='Mirror weights from...', ann='Copy and mirror bulge weights', en=isMirrorEnabled, c=cb(mirror_weights_cmd, rbs_node, upperIdx))

        cmds.formLayout(eyeTab, e=True, attachForm=[(rmSideBT, 'top', 0), (rmSideBT, 'left', 0), (rmSideBT, 'right', 0), (f1, 'left', 0), (f1, 'right', 0), (f2, 'left', 0), (f2, 'right', 0), (f3, 'left', 0), (f3, 'right', 0), (f3, 'bottom', 0)],
                        attachControl=[(f1, 'top', 0, rmSideBT), (f2, 'top', 0, f1), (f3, 'top', 0, f2)])

        content.append((eyeTab, prfx))

    if content:
        cmds.tabLayout(_controls['tab_lay'], edit=True, tabLabel=content)


def _update_ui():
    """Enable/disable buttons."""

    geo = _get_geo_from_ui()
    rbs_node = geo and _get_rbs(geo)

    idxList = rbs_node and _get_eye_idx_list(rbs_node) or []
    anyShape = any([bool(cmds.getAttr('%s.it[%d].%s' % (rbs_node, idx, _target_attrs[0]), type=True)) for idx in idxList])

    # enable/disable ui
    cmds.button(_controls['create_bt'], e=True, en=bool(geo) and not bool(rbs_node))
    cmds.text(_controls['rbs'], e=True, l=rbs_node or '')
    cmds.button(_controls['add_eye_bt'], e=True, en=bool(rbs_node))
    if not _use_cmp_tags:
        cmds.button(_controls['edit_memb_bt'], e=True, en=bool(rbs_node))
    cmds.button(_controls['auto_memb_bt'], e=True, en=anyShape)
    cmds.button(_controls['compute_disp_bt'], e=True, en=anyShape)
    cmds.button(_controls['delete_bt'], e=True, en=bool(rbs_node))

    _update_tab_ui(rbs_node)


def _disconnect_target_weights(rbs_node, idx):
    """Disconnects targetWeight elements."""

    # check incoming connections to targetWeight attr
    wAttrFull = '%s.it[%d].%s' % (rbs_node, idx, _w_attr)
    connList = cmds.listConnections(wAttrFull, d=False, p=True)

    if not connList:
        return

    # disconnect targetWeights
    cmds.disconnectAttr(connList[0], wAttrFull)


def _disconnect_paint_weights(rbs_node):
    """Disconnects any outgoing connection from the paintable weights."""

    pAttr = '%s.weightList[0].weights' % rbs_node
    destConnList = cmds.listConnections(pAttr, s=False, p=True) or []
    for dest in destConnList:
        src = cmds.listConnections(dest, d=False, p=True)[0]
        cmds.disconnectAttr(src, dest)


def get_geo_cmd():
    geo = _get_selected_shape('mesh')

    cmds.text(_controls['geo'], e=True, l=geo or '')

    _update_ui()


def create_cmd():
    """Create node."""

    geo = _get_geo_from_ui()
    rbs_node = geo and _get_rbs(geo)

    if rbs_node:
        _update_ui()
        return

    # create the rbs
    cmds.deformer(geo, typ=_rbs_node_name, foc=True)

    _update_ui()


def add_eye_cmd():
    """Adds new eyelid inputs."""

    geo = _get_geo_from_ui()
    rbs_node = geo and _get_rbs(geo)

    prfx = cmds.layoutDialog(t='Add eye', ui=lambda *x: _add_eye_dialog(rbs_node))
    if len(prfx) > 1:
        return

    # find first available lower lid index
    miList = _get_eye_idx_list(rbs_node)
    idx = 0
    while idx in miList or (idx+1) in miList:
        idx += 2

    lowerAttr = '%s.it[%d]' % (rbs_node, idx)
    upperAttr = '%s.it[%d]' % (rbs_node, idx+1)
    lowerName = '%s_lower' % prfx
    upperName = '%s_upper' % prfx

    # add lower and upper indices
    cmds.getAttr('%s.im' % lowerAttr)
    cmds.getAttr('%s.im' % upperAttr)

    # set the aliases for the inputTarget attributes (lower and upper)
    if cmds.aliasAttr(lowerAttr, q=True) != lowerName:
        _set_alias(lowerAttr, lowerName)
    if cmds.aliasAttr(upperAttr, q=True) != upperName:
        _set_alias(upperAttr, upperName)

    # create and connect locators
    _create_eye_locators(rbs_node, idx)

    # update tab ui
    _update_ui()
    _select_tab(prfx)


def paint_weights_cmd(rbs_node, upperIdx):
    wAttr = '%s.it[%d].%s' % (rbs_node, upperIdx, _w_attr)
    pAttr = '%s.weightList[0].weights' % rbs_node

    # disconnect
    _disconnect_paint_weights(rbs_node)

    # copy and connect
    cmds.connectAttr(wAttr, pAttr, f=True)
    cmds.getAttr(pAttr, mi=True)
    cmds.disconnectAttr(wAttr, pAttr)
    cmds.connectAttr(pAttr, wAttr, f=True)

    # paint tool
    obj = cmds.listRelatives(cmds.deformer(rbs_node, q=True, g=True), p=True, pa=True)[0]
    cmds.select(obj)
    cmds.setToolTo(cmds.artAttrCtx(oaa='%s.%s.weights' % (_rbs_node_name, rbs_node)))


def mirror_weights_cmd(rbs_node, upperIdx):
    destIdx = upperIdx

    # get destination prefix
    destPrfx = _get_target_name_from_idx(rbs_node, destIdx)[0]

    # ask for the source
    srcPrfx = cmds.layoutDialog(t='Mirror bulge weights', ui=lambda *x: _mirror_weights_dialog(rbs_node, destPrfx))
    if len(srcPrfx) > 1:
        return

    # find the source index
    idxPrfxList = _get_eye_idx_prfx_list(rbs_node)

    srcIdx = -1
    for lowerIdx, prfx in idxPrfxList:
        if prfx == srcPrfx:
            srcIdx = lowerIdx + 1
            break

    if srcIdx < 0:
        raise Exception('Cannot find the index of target %s' % srcPrfx)

    # membership set
    objSetNode = _get_membership_set(rbs_node)
    affectedVtxList = cmds.ls(cmds.sets(objSetNode, q=True) or [], fl=True)

    # find original shape
    obj = cmds.listRelatives(cmds.deformer(rbs_node, q=True, g=True), p=True, pa=True)[0]
    origShape = [s for s in cmds.listRelatives(obj, s=True, pa=True) if cmds.getAttr('%s.intermediateObject' % s)][0]

    # get the original point position
    origAffectedVtxList = ['%s%s' % (origShape, vtx[vtx.rindex('.'):]) for vtx in affectedVtxList]
    vtxCount = len(origAffectedVtxList)
    posList = cmds.xform(origAffectedVtxList, q=True, os=True, t=True)
    posList = [posList[i:i+3] for i in range(0, len(posList), 3)]

    # create a symmetry map (it stores the indices of the opposite vertices)
    cmds.progressBar('mainProgressBar', e=True, beginProgress=True, isInterruptable=False, status='Generating symmetry map...', maxValue=vtxCount)
    symmMap = [-1]*len(affectedVtxList)
    for i, pos in enumerate(posList):
        cmds.progressBar('mainProgressBar', e=True, step=1)
        # avoid doing the test twice for the opposite vertex
        if symmMap[i] != -1:
            continue

        oppIdx = -1
        minDistance = 1000000.0

        for j, otherPos in enumerate(posList):
            dist = (otherPos[0]+pos[0])**2 + (otherPos[1]-pos[1])**2 + (otherPos[2]-pos[2])**2
            if dist < minDistance:
                minDistance = dist
                oppIdx = j

        symmMap[i] = oppIdx
        symmMap[oppIdx] = i
    cmds.progressBar('mainProgressBar', e=True, endProgress=True)

    # disconnect paint weights
    _disconnect_paint_weights(rbs_node)

    srcAttr = '%s.it[%d].%s' % (rbs_node, srcIdx, _w_attr)
    destAttr = '%s.it[%d].%s' % (rbs_node, destIdx, _w_attr)
    pAttr = '%s.weightList[0].weights' % rbs_node

    # copy weights from source to paint
    cmds.connectAttr(srcAttr, pAttr, f=True)
    cmds.getAttr(pAttr, mi=True)
    cmds.disconnectAttr(srcAttr, pAttr)

    # connect paint to destination weights
    cmds.connectAttr(pAttr, destAttr, f=True)

    affectedWeightList = cmds.percent(rbs_node, affectedVtxList, q=True, v=True)

    # set weights
    cmds.progressBar('mainProgressBar', e=True, beginProgress=True, isInterruptable=False, status='Setting weights...', maxValue=vtxCount)
    for i, vtx in enumerate(affectedVtxList):
        cmds.progressBar('mainProgressBar', e=True, step=1)
        cmds.percent(rbs_node, vtx, v=affectedWeightList[symmMap[i]])
    cmds.progressBar('mainProgressBar', e=True, endProgress=True)

    # show the paint tool
    cmds.select(obj)
    cmds.setToolTo(cmds.artAttrCtx(oaa='%s.%s.weights' % (_rbs_node_name, rbs_node)))


def remove_eye_cmd(rbs_node, lowerIdx):
    """Removes lower and upper eyelid of the given side."""

    idxList = [lowerIdx, lowerIdx+1]

    attrList = _attrs + _bulge_attrs + _blink_attrs + _offset_influence_attrs + _offset_attrs

    for idx in idxList:
        # attrs in the inputTarget array
        if idx in _get_eye_idx_list(rbs_node):
            _disconnect_target_weights(rbs_node, idx)

            for attr_name in _target_attrs:
                attr = '%s.it[%d].%s' % (rbs_node, idx, attr_name)
                _disconnect_and_remove_attr(attr)
            _disconnect_and_remove_attr('%s.it[%d]' % (rbs_node, idx), True)

        # channel box attrs
        for attr_name in attrList:
            if idx in _get_attr_idx_list(rbs_node, attr_name):
                attr = '%s.%s[%d]' % (rbs_node, attr_name, idx)
                _disconnect_and_remove_attr(attr, True)

    _update_ui()


def edit_membership_cmd():
    """Membership tool."""

    geo = _get_geo_from_ui()
    rbs_node = geo and _get_rbs(geo)

    if rbs_node:
        cmds.select(rbs_node)
        cmds.setToolTo('setEditContext')
        sys.stdout.write('Use shift/ctrl to add/remove vertices from the deformer\n')

    _update_ui()


def auto_membership_cmd():
    geo = _get_geo_from_ui()
    rbs_node = geo and _get_rbs(geo)

    if not rbs_node:
        raise Exception('No %s found' % _rbs_node_name)

    # orig shape coords
    origShape = [s for s in cmds.listRelatives(geo, s=True, pa=True) if cmds.getAttr('%s.intermediateObject' % s)][0]
    origCoords = cmds.xform('%s.vtx[*]' % origShape, q=True, os=True, t=True)
    vtxCount = cmds.polyEvaluate(origShape, v=True)

    tidxList = _get_eye_idx_list(rbs_node)
    shapeList = []
    deleteList = []

    # get connected shapes or regen them if absent
    for tidx in tidxList:
        for i in [0, 1]:
            attr = '%s.it[%d].%s' % (rbs_node, tidx, _target_attrs[i])
            connList = cmds.listConnections(attr, d=False)
            if connList:
                shapeList.append(connList[0])
            elif cmds.getAttr(attr, type=True):
                obj = regen_cmd(rbs_node, tidx, i == 1)
                shapeList.append(obj)
                deleteList.append(obj)

    mod_idx_set = set()

    for shape in shapeList:
        targetCoords = cmds.xform('%s.vtx[*]' % shape, q=True, os=True, t=True)
        for idx in range(vtxCount):
            i = idx*3
            if (abs(targetCoords[i]-origCoords[i]) > _auto_memb_tolerance) or (abs(targetCoords[i + 1] - origCoords[i + 1]) > _auto_memb_tolerance) or (abs(targetCoords[i + 2] - origCoords[i + 2]) > _auto_memb_tolerance):
                mod_idx_set.add(idx)

    # delete any shape I created
    if deleteList:
        cmds.delete(deleteList)

    if len(mod_idx_set) == 0:
        cmds.warning('No shape deltas found in %s. No point will be removed.' % geo)
        return

    # using component tags (Maya 2022 or higher)
    if _use_cmp_tags:
        # get component tag indices
        tag_idx_list = cmds.getAttr('%s.componentTags' % origShape, mi=True)
        create_tag = True

        # if there are tags
        if tag_idx_list:
            tag_names = cmds.getAttr('%s.componentTags[:].componentTagName' % origShape)

            # if the cmp tag name is already in the list, use that index
            if _cmp_tag_name in tag_names:
                rbs_tag_idx = tag_idx_list[tag_names.index(_cmp_tag_name)]
                create_tag = False

            # otherwise use the next available index
            else:
                rbs_tag_idx = tag_idx_list[-1]+1

        # otherwise the index will be zero
        else:
            rbs_tag_idx = 0

        # create tag if needed
        if create_tag:
            cmds.setAttr('%s.componentTags[%d].componentTagName' % (origShape, rbs_tag_idx), _cmp_tag_name, type='string')

        # finally set the components for the radial blend shape tag
        vtx_cmp_list = ['vtx[%d]' % i for i in mod_idx_set]
        cmds.setAttr('%s.componentTags[%d].componentTagContents' % (origShape, rbs_tag_idx), len(mod_idx_set),
                     *vtx_cmp_list, type='componentList')

        # set the deformer tag expression
        cmds.setAttr('%s.input[0].componentTagExpression' % rbs_node, _cmp_tag_name, type='string')

        # select those vertices
        cmds.select(['%s.vtx[%d]' % (geo, i) for i in mod_idx_set])


    # using old fashion sets
    else:
        # fill the set
        objSetNode = _get_membership_set(rbs_node)
        cmds.sets('%s.vtx[*]' % geo, add=objSetNode)

        # remove from the set
        idxToRemSet = set(range(vtxCount)) - mod_idx_set
        pListToRemove = ['%s.vtx[%d]' % (geo, idx) for idx in idxToRemSet]
        cmds.sets(pListToRemove, rm=objSetNode)

        # membership tool
        cmds.select(rbs_node)
        cmds.setToolTo('setEditContext')

    cmds.confirmDialog(t='Auto membership', m='Optimization complete.\nUsing %d of %d points.' % (len(mod_idx_set), vtxCount))


def delete_cmd():
    """Delete node."""

    geo = _get_geo_from_ui()
    rbs_node = geo and _get_rbs(geo)

    if rbs_node:
        cmds.delete(rbs_node)

    _update_ui()


def set_lid_from_selection_cmd(rbs_node, idx, opposite=False):
    """Adds an eyelid target."""

    lidGeo = _get_selected_shape('mesh')

    if not lidGeo:
        raise Exception('Please select a polygon mesh')

    if _get_rbs(lidGeo):
        raise Exception('The main geometry cannot be used as a shape')

    targetName = _get_target_name_from_idx(rbs_node, idx)

    if idx == -1:
        raise Exception('Input target %s does not exist, please refresh GUI' % targetName)

    # make the connection if not yet done
    srcAttr = '%s.outMesh' % lidGeo
    destAttr = '%s.it[%d].%s' % (rbs_node, idx, _target_attrs[int(opposite)])
    connections = cmds.listConnections(destAttr, d=False) or []
    if lidGeo in connections:
        cmds.warning('The geometry you selected is already connected to %s' % destAttr)
        return

    # connect geometry
    cmds.connectAttr(srcAttr, destAttr, f=True)

    if not opposite:
        # set default values
        for i, attr in enumerate(_attrs):
            cmds.setAttr('%s.%s[%d]' % (rbs_node, attr, idx), _def_attr_values[i])

        for i, attr in enumerate(_offset_attrs):
            cmds.getAttr('%s.%s[%d]' % (rbs_node, attr, idx))

        for i, attr in enumerate(_offset_influence_attrs):
            upIdx = idx + 1 - idx%2
            # don't set it if it has already been set by the other lid (since it's a common value)
            if not upIdx in _get_attr_idx_list(rbs_node, attr):
                cmds.setAttr('%s.%s[%d]' % (rbs_node, attr, upIdx), _def_offset_influence_values[i])

        # just for odd indices (upper lid) set the blink attribute
        if idx % 2 == 1:
            for attr in _blink_attrs:
                cmds.setAttr('%s.%s[%d]' % (rbs_node, attr, idx), 0.0)

        # set aliases
        _set_attr_aliases(rbs_node, idx)

    _update_ui()
    _select_tab(targetName[0])


def set_offset_control_cmd(rbs_node, idx, side):
    sel = cmds.ls(sl=True, transforms=True)
    if not sel:
        raise Exception('Please select the desired control')

    ctrl = sel[0]

    # check if this object is already connected to the attribute
    destAttr = '%s.%s[%d]' % (rbs_node, _offset_attrs[side], idx)
    connList = cmds.listConnections(destAttr, d=False) or []

    if ctrl in connList:
        cmds.warning('The object you selected is already connected to %s' % destAttr)
        return

    # connect it
    cmds.connectAttr('%s.t' % ctrl, destAttr, f=True)

    # set alias (in case I set the control before the shape)
    _set_attr_aliases(rbs_node, idx)

    # update tab ui
    targetName = _get_target_name_from_idx(rbs_node, idx)
    _update_tab_ui(rbs_node)
    _select_tab(targetName[0])


def select_offset_control_cmd(rbs_node, idx, side):
    ctrl = cmds.listConnections('%s.%s[%d]' % (rbs_node, _offset_attrs[side], idx), d=False)[0]
    cmds.select(ctrl)


def set_main_control_cmd(rbs_node, idx):
    # get selection
    sel = cmds.ls(sl=True, transforms=True)
    if not sel:
        raise Exception('Please select the desired control')

    idxList = _get_eye_idx_list(rbs_node)
    if not idx in idxList:
        raise Exception('Eye index %d does not exist' % idx)

    ctrl = sel[0]

    attrPairs = [['ty', _attrs[0]], ['tx', _attrs[1]]]

    # connect translation attrs
    for srcAttr, destAttr in attrPairs:
        # check if this object is already connected to the attribute
        dest = '%s.%s[%d]' % (rbs_node, destAttr, idx)
        connList = cmds.listConnections(dest, d=False) or []
        if not ctrl in connList:
            src = '%s.%s' % (ctrl, srcAttr)
            cmds.connectAttr(src, dest, f=True)
            sys.stdout.write('%s -> %s\n' % (src, dest))

    # user defined attributes
    newAttrPairs = [[_attrs[2], _attrs[2], 1.0]]

    # if it's upper, add the blink attr
    if idx % 2 == 1:
        newAttrPairs.append([_blink_attrs[0], _blink_attrs[0], 0.0])
        newAttrPairs.append([_blink_attrs[1], _blink_attrs[1], 0.0])

    # user defined attrs
    udAttrs = cmds.listAttr(ctrl, k=True, ud=True) or []

    # create misc attr
    if not udAttrs:
        cmds.addAttr(ctrl, ln='misc', at='enum', en=' ', k=True)
        cmds.setAttr('%s.misc' % ctrl, l=True)

    for srcAttr, destAttr, defVal in newAttrPairs:
        # create attr if needed
        if not srcAttr in udAttrs:
            cmds.addAttr(ln=srcAttr, at='double', min=0.0, max=1.0, dv=defVal, k=True)

        # check if this object is already connected to the attribute
        dest = '%s.%s[%d]' % (rbs_node, destAttr, idx)
        connList = cmds.listConnections(dest, d=False) or []
        if not ctrl in connList:
            src = '%s.%s' % (ctrl, srcAttr)
            cmds.connectAttr(src, dest, f=True)
            sys.stdout.write('%s -> %s\n' % (src, dest))

    # update tab ui
    targetName = _get_target_name_from_idx(rbs_node, idx)
    _update_tab_ui(rbs_node)
    _select_tab(targetName[0])


def select_main_control_cmd(rbs_node, idx):
    ctrl = cmds.listConnections('%s.%s[%d]' % (rbs_node, _attrs[0], idx), d=False)[0]
    cmds.select(ctrl)


def regen_cmd(rbs_node, idx, opposite=False):
    """Regenerates geometry from rbs shape."""

    attr = '%s.it[%d].%s' % (rbs_node, idx, _target_attrs[int(opposite)])
    obj = cmds.listRelatives(cmds.createNode('mesh'), p=True, pa=True)[0]
    cmds.connectAttr(attr, '%s.inMesh' % obj)
    cmds.delete(obj, ch=True)
    cmds.connectAttr('%s.outMesh' % obj, attr, f=True)
    cmds.sets(obj, fe='initialShadingGroup')

    targetName = _get_target_name_from_idx(rbs_node, idx)
    if targetName:
        obj = cmds.rename(obj, '%s_%s' % (targetName, opposite and 'open' or 'closed'))

    cmds.select(obj)

    _update_tab_ui(rbs_node)
    _select_tab(targetName[0])

    return obj


def set_bulge_cmd(rbs_node, upperIdx, enabled):
    """Enables/disables bulge deformation."""

    targetName = _get_target_name_from_idx(rbs_node, upperIdx)

    if enabled:
        # set def values
        for i, attr in enumerate(_bulge_attrs):
            cmds.setAttr('%s.%s[%d]' % (rbs_node, attr, upperIdx), _def_bulge_values[i])
        _set_attr_aliases(rbs_node, upperIdx)
    else:
        for attr in _bulge_attrs:
            _disconnect_and_remove_attr('%s.%s[%d]' % (rbs_node, attr, upperIdx), True)

    _update_tab_ui(rbs_node)
    _select_tab(targetName[0])


def compute_disp_cmd():
    """Forces recomputing the max displacement values."""

    geo = _get_geo_from_ui()
    rbs_node = geo and _get_rbs(geo)

    if rbs_node:
        cmds.setAttr('%s.%s' % (rbs_node, _compute_disp_attr), 0)


def help_cmd():
    webbrowser.open("http://www.paolodominici.com/products/zvradialblendshape/")


def show():
    # load plugin
    if not cmds.pluginInfo(_rbs_node_name, q=True, l=True):
        cmds.loadPlugin(_rbs_node_name)

    # make attribute paintable
    cmds.makePaintable(_rbs_node_name, 'weights', attrType='multiFloat', sm='deformer')

    # create window
    win_name = 'ZvRadialBlendShapeWin'
    if cmds.window(win_name, exists=True):
        cmds.deleteUI(win_name, window=True)

    cmds.window(win_name, title='%s %s' % (__product__, __version__))

    main_lay = cmds.formLayout()

    # geometry
    f1 = cmds.frameLayout(l='Geometry', mw=2, mh=2)
    cmds.columnLayout(adj=True)
    cmds.rowLayout(nc=3, adj=3, ct3=['both', 'both', 'both'], cl3=['center', 'center', 'left'], cw=[1, 120])
    cmds.button(l='Get geometry', ann='Refresh UI using selected geometry', c=cb(get_geo_cmd))
    cmds.separator(w=8, st='none')
    geo_txt = cmds.text(l='')
    cmds.setParent(f1)
    cmds.rowLayout(nc=3, adj=3, ct3=['both', 'both', 'both'], cl3=['center', 'center', 'left'], cw=[1, 120])
    create_bt = cmds.button(l='Create deformer', ann='Create Radial Blend Shape node', c=cb(create_cmd))
    cmds.separator(w=8, st='none')
    rbs_txt = cmds.text(l='')

    # eyes
    cmds.setParent(main_lay)
    f2 = cmds.frameLayout(l='Eyes', mw=2, mh=2)
    eye_form_lay = cmds.formLayout(nd=20)
    add_eye_bt = cmds.button(l='Add...', ann='Add shape group', c=cb(add_eye_cmd))
    tabs = cmds.tabLayout(innerMarginWidth=2, innerMarginHeight=2)
    cmds.formLayout(eye_form_lay, e=True,
                    attachForm=[(add_eye_bt, 'top', 0), (add_eye_bt, 'left', 0), (add_eye_bt, 'right', 0), (tabs, 'left', 0), (tabs, 'right', 0), (tabs, 'bottom', 0)],
                    attachControl=[(tabs, 'top', 4, add_eye_bt)])

    # tools
    cmds.setParent(main_lay)
    f3 = cmds.frameLayout(l='Tools', mw=2, mh=2)
    cmds.columnLayout(adj=True, rs=2)

    if not _use_cmp_tags:
        memb_lay = cmds.formLayout(nd=20)
        edit_memb_bt = cmds.button(l='Edit membership', ann='Edit affected vertices', c=cb(edit_membership_cmd))
    auto_memb_bt = cmds.button(l='Auto membership', ann='Optimize affected vertices', c=cb(auto_membership_cmd))

    if not _use_cmp_tags:
        cmds.formLayout(memb_lay, e=True, attachForm=[(edit_memb_bt, 'top', 0), (edit_memb_bt, 'left', 0), (edit_memb_bt, 'bottom', 0), (auto_memb_bt, 'top', 0), (auto_memb_bt, 'right', 0), (auto_memb_bt, 'bottom', 0)],
                        attachPosition=[(edit_memb_bt, 'right', 1, 10), (auto_memb_bt, 'left', 1, 10)])

        cmds.setParent('..')
    compute_disp_bt = cmds.button(l='Compute displacements', ann='Compute maximum displacements for all shapes.\nThis must be done after a shape edit to make sure the offset controls work properly.', c=cb(compute_disp_cmd))
    delete_bt = cmds.button(l='Delete deformer', ann='Remove the Radial Blend Shape node attached to this geometry', c=cb(delete_cmd))
    cmds.button(l='Help', ann='Online documentation', bgc=(0.9, 0.8, 0.4), c=cb(help_cmd))

    cmds.formLayout(main_lay, e=True,
                    attachForm=[(f1, 'top', 0), (f1, 'left', 0), (f1, 'right', 0), (f2, 'left', 0), (f2, 'right', 0), (f3, 'left', 0), (f3, 'right', 0), (f3, 'bottom', 0)],
                    attachControl=[(f2, 'top', 10, f1), (f2, 'bottom', 10, f3)])

    # control dict
    _controls['geo'] = geo_txt
    _controls['rbs'] = rbs_txt
    _controls['create_bt'] = create_bt
    _controls['add_eye_bt'] = add_eye_bt
    _controls['tab_lay'] = tabs
    if not _use_cmp_tags:
        _controls['edit_memb_bt'] = edit_memb_bt
    _controls['auto_memb_bt'] = auto_memb_bt
    _controls['delete_bt'] = delete_bt
    _controls['compute_disp_bt'] = compute_disp_bt

    _update_ui()

    # show window
    cmds.showWindow(win_name)
    cmds.window(win_name, edit=True, widthHeight=(320, 750))

    sys.stdout.write("%s %s          %s\n" % (__product__, __version__, __author__))
