"""
=============================================== HI ================================================
Hello World ! 
Welcome to Skiin script, this is an evolutive weights / effector manager for Maya. Allowing you
to export / import (almost) whatever you want from your Maya rig scene !

    -   EVOLUTIVE
        Because you can write new 'plugins' to expand the abilities of Skiin.
        A  Skiin's  plugin consists  in  few functions  that  will be  used to  export / import the
        weights / deformers informations see [ http://mehdilouala.com/Skiin_plugins ] for more info
        about how to write  a plugin. If  you feel comfortable  with scripting  you can also have a
        look  by  yourself in  the 'skiin_rsc/plugins'  folder, there  is a  '_model.py'  file with
        explanations

    -   WEIGHTS MANAGER
        It allows you to  store in external files  (.skin files)  all the datas you want to extract
        from your  Maya's nodes. For instance, the most common usage is to extract your skinCluster
        weights, you can save the weights  in an external file,  and paste them on a new mesh super
        easily.

    -   EFFECTOR MANAGER
        It also allows  you to do the same process with the objects which modify your object,  such
        as ffd, cluster,  whatever you want actually,  even the constraints  if you want to !  This
        can be a good way to export everything you did on a rig / skin process in one external file.

============================================= INSTALL =============================================

Copy Skiin.py and the 'skiin_rsc' folder into your Maya's script folder, then
to run Skiin simply type these lines in a new Script Editor Tab :

import Skiin;reload(Skiin)
Skiin.Skiin_UI()

=============================================== UI ================================================
Go to [ http://mehdilouala.com/Skiin ] for better looking explanations of this tool

The UI is divided in several parts listed below ;
    - the active directory :    This is where Skiin will save / load your files by default

    - conformity percentage :   The conformity between the input and the output

    - the two views :           An input and an output view ;
                                    - the Output  view displays the readable  modifiers from  your
                                    selection in Maya
                                    - the Input view displays the loaded .skin files modifiers
                                These two views automatically reorder / colorize so you can easily
                                see what changes has been done

    - the tools & info :        Use advanced tools, and manage your loaded plugins stuff like that
                                ,  also display  an accurate  count of each  deformer / attributes
                                inside the loaded file(s) and inside the current selection, so you
                                can easily see what matches between input and output

    - the options :             All the Skiin options, hold the mouse above an options to see what
                                it does =).

    And some addionnals like progression bars / buttons which speak by themselves
	
Feel free to check-out my other scripts at https://git.mehdilouala.com
"""
__author__ = "Mehdi Louala"
__copyright__ = "Copyright 2017, Mehdi Louala"
__credits__ = ["Mehdi Louala"]
__license__ = "GPL"
__version__ = "2.1.3"
__maintainer__ = "Mehdi Louala"
__email__ = "mlouala@gmail.com"
__status__ = "Stable Release"

import json
import ntpath
import os
import sqlite3
from math import sin, pi

try:
    from PySide.QtCore import Qt
    from PySide.QtGui import QIcon, QLineEdit, QGroupBox, QCheckBox, QFileDialog, QMessageBox, QDialog, \
        QVBoxLayout, QPushButton, QLabel
except ImportError:
    from PySide2.QtCore import Qt
    from PySide2.QtWidgets import QLineEdit, QGroupBox, QCheckBox, QFileDialog, QMessageBox, QDialog, \
        QVBoxLayout, QPushButton, QLabel
    from PySide2.QtGui import QIcon

from maya.OpenMaya import MEventMessage, MMessage, MGlobal
from maya.cmds import listRelatives, getAttr, ls, select, warning, reorderDeformers

from skiin_rsc.widgets import Skiin_Item, IconLabel, Skiin_View, Input_View, QMessage_FileExists, \
    QMessage_ModifierConflict, QMessage_SkinClusterHazard

from skiin_rsc.funcs import maya_tsf_selection, make_unique, objectEvaluate, info, warn, debug, maya_selection, \
    walk_object_tree, mod_deformer_valid, creation_context, db_context, insert_sequence, get_object_from_path, \
    clear_modifier, get_rough_plugs

from skiin_rsc.plugin import *
from skiin_rsc.UI import SkiinUI


class Skiin_Class(SkiinUI, QWidget):
    """
    Welcome to Skiin
    """
    app_dir = os.path.dirname(__file__)

    base_icons = [QIcon(':out_particleSamplerInfo.png'), QIcon(':samplerInfo.svg'),
                  QIcon(':constraint.png'), QIcon(':parent.png')]
    base_parameters = ['parameters', 'connections', 'constraint', 'parent']

    def __init__(self, parent=None):
        # first we try to remove the previous window, so it'll save its settings
        win = get_maya_window().findChildren(QWidget, self.title) or []
        for c in win:
            c.close()
            c.deleteLater()

        # here we'll store all our call backs in order to remove them easily
        self.callbacks = []

        # we call the UI to be generated
        super(Skiin_Class, self).__init__(parent)
        self.setAttribute(Qt.WA_QuitOnClose)
        self.setAttribute(Qt.WA_DeleteOnClose)

        # our private active directory
        self._active_dir = self.app_dir
        self.WActivePath.setText(self._active_dir)
        self.config_path = os.path.join(self.active_dir, 'skiin_rsc', 'cfg.json')

        # simple lambda to forward filtering instructions
        self.filter_mod = lambda view, text: view.filter_items(text)

        # this list will be filled until all plugins are loaded in Skiin, to prevent
        # too early calls
        self.pending_plugins = []

        # this is the plugins allowed
        self.allowed_plugins = set()
        self.plugins_view.pluginToggled.connect(self.update_plugin)

        # where the information Widgets will be stored by names
        self.WInformations = {}

        # this set class handle all the Maya's objects used by Skiin either read & write
        self.objects = Object_Pool(self)

        # everytime a plugin is loaded in memory we store its Method class here so we can
        # access static methods such as friends or icons
        self.methods = {}

        # the icons dictionary
        self.modifier_icons = {}

        # if the config file exists we load its settings to maintain a linear workflow
        if os.path.isfile(self.config_path):
            with open(self.config_path, 'r') as f:
                json_data = json.load(f)

                # we first extract the custom data
                if not json_data.pop(u'OptionGroup'):
                    self.WOptionGroup.collapse()

                if not json_data.pop(u'ToolsInfo'):
                    self.WInfoGroup.collapse()

                self.allowed_plugins = set(json_data.pop(u'include'))

                self.setGeometry(*json_data.pop(u'geo_box'))
                self.LViewSplitter.setSizes(json_data.pop(u'view_sz'))

                for key in json_data:
                    if isinstance(json_data[key], bool):
                        self.findChild(QCheckBox, key).setChecked(json_data[key])

                    else:
                        self.findChild(QLineEdit, key).setText(json_data[key])

            self.active_dir = self.WActivePath.text()

        # we see what plugins are allowed AND in the plugins
        allowed = self.allowed_plugins.intersection(set(self.plugins_view.get_plugins_list()))

        # now we have loaded settings, we ask our kind SanityMan to check the sanity of
        # our plugins
        for plug in allowed:
            SanityMan(self.add_plugin, plug, os.path.join(self.app_dir, 'skiin_rsc'))
            self.pending_plugins.append(plug)

        # additionnal information we want to display in the Info & Tools section
        for i, cat in enumerate(self.base_parameters):
            label = IconLabel(cat, self, icon=self.base_icons[i])
            label.setText('%i / %i' % (0, 0))

            self.WInformations[cat] = label
            self.LInformations.addWidget(label)

            if not self.WInfoGroup.expanded:
                label.setVisible(False)

        # we snap the Layout Edit Spliiter to the Layout View Splitter size because
        # they always should be identical
        self.LEditSplitter.setSizes(self.LViewSplitter.sizes())
        self.WFlush.clicked.connect(self.update_informations)

        # forcing focus at start
        self.WInView.setFocus()

        # pre-treatment
        self.addCallback()
        self.selection_changed()

    ### QT ###

    def closeEvent(self, e):
        # when closing, we kill callbacks, closing additionnal dialogs and saving settings
        self.clearCallback()
        self.plugins_view.close()

        # defined above, should be located inside the 'skiin_rsc' folder
        with open(self.config_path, 'w') as f:
            datas = {}
            g = self.geometry()
            datas['geo_box'] = (g.left(), g.top(), g.width(), g.height())
            datas['view_sz'] = self.LViewSplitter.sizes()

            for child in (self.findChildren(QCheckBox) + self.findChildren(QLineEdit)):
                t = child.objectName()
                if len(t):
                    datas[t] = child.isChecked() if isinstance(child, QCheckBox) else child.text()

            for child in self.findChildren(QGroupBox):
                if hasattr(child, 'expanded'):
                    datas[child.objectName()] = child.expanded

            datas['include'] = list(self.allowed_plugins)

            # <3 json :)
            json.dump(datas, f, sort_keys=True,
                      indent=4, separators=(',', ': '))

    def resizeEvent(self, e):
        # we recall our columns resizing if ever the window is rescaled
        super(Skiin_Class, self).resizeEvent(e)
        self.WOutView.resizeColumns()
        self.WInView.resizeColumns()

    def dragEnterEvent(self, e):
        # in case of a drop of a .skin file we're here =)
        for url in e.mimeData().urls():
            if url.isLocalFile():
                obj = self.childAt(e.pos())
                # checking if url is consistent
                if url.path().endswith('.skin') and obj and isinstance(obj.parent(), Input_View):
                    e.accept()

    def dropEvent(self, e):
        # pre-filtering the drops (skipping first character because
        # QUrls starts with a /
        urls = [url.path()[1:] for url in e.mimeData().urls() if url.isLocalFile() and url.path().endswith('.skin')]

        # if they are consistent
        if len(urls):
            self.WInView.clear()
            self.WInView.update()
            self.WGlobalProgress.restart()

            for url in urls:
                # we load each file
                self.load_data(url)

                self.filter_mod(self.WInView, self.WInEdit.text())

                # applying default functions
                self.conform()
                self.WProgress.done('>> %s loaded in memory' % self.WInName.text())
                self.WGlobalProgress.done()
                self.WInView.resizeColumns()

            if len(urls) == 1:
                self.WInName.setText('%s' % ntpath.basename(url))

            else:
                self.WInName.setText('%i files' % len(urls))

            # refresh the information layouts
            self.update_informations()

    ### ACTIVE DIRECTORY ###

    @property
    def active_dir(self):
        # Property returns the current active directory
        return self._active_dir

    @active_dir.setter
    def active_dir(self, directory):
        # Set the active directory if exists
        if os.path.isdir(directory):
            self._active_dir = directory

        # otherwise set the script's location folder
        else:
            self._active_dir = os.path.dirname(__file__)

    def set_active_dir(self, *e):
        """
        Browse to define the current active directory
        """
        options = QFileDialog.DontResolveSymlinks | QFileDialog.ShowDirsOnly | QFileDialog.DontUseNativeDialog
        directory = QFileDialog.getExistingDirectory(self,
                "Set skinMaster active directory",
                self.active_dir, options=options)

        if directory:
            self.active_dir = directory + '/'
            self.WActivePath.setText(directory + '/')
            # updating the WPathPreview
            self.set_name()

    ### MISCELLANEOUS ###

    def addCallback(self):
        """
        Add all the callbacks Skiin needs to be reactive
        """
        self.callbacks.append(MEventMessage.addEventCallback('SelectionChanged', self.selection_changed))
        self.callbacks.append(MEventMessage.addEventCallback('Undo', self.renew_selection))
        self.callbacks.append(MEventMessage.addEventCallback('Redo', self.renew_selection))
        self.callbacks.append(MEventMessage.addEventCallback('NewSceneOpened', self.renew_selection))
        self.callbacks.append(MEventMessage.addEventCallback('SetModified', self.renew_selection))
        self.callbacks.append(MEventMessage.addEventCallback('NameChanged', self.renew_selection))

    def clearCallback(self):
        """
        We remove all the callbacks
        """
        for callback in reversed(self.callbacks):
            MMessage.removeCallback(callback)
            self.callbacks.remove(callback)

    def add_plugin(self, plugin_name, err_no, ico=None):
        """
        Add a plugin to Skiin (these files in the 'skiin_rsc/plugins' folder
        :param   plugin_name: plugin's name
        :param err_no: error code
        """

        # if no error code (-1) or simple warning (>= 10)
        if err_no == -1 or err_no >= 10:
            # some debugging
            if err_no == -1:
                info("SkinMaster loaded plugin '%s'" % plugin_name)
            else:
                warn("SkinMaster loaded plugin '%s' with warnings, see Readers' window" % plugin_name)

            try:
                # we need remove the previous paths from sys.modules
                # to flush the memory
                if plugin_name in modules:
                    modules.pop(plugin_name)

                # then we ask for a fresh loading from imp
                plug_in = imp.load_source(plugin_name, join(self.app_dir,
                                                            'skiin_rsc',
                                                            'plugins',
                                                            '%s.py' % plugin_name))
                imp.reload(plug_in)

                # then we ask to read again the plugin and its icon attributes
                method = plug_in.Method
                assert hasattr(method, '_icon')
                ico = method._icon
                # and we assign the method to our dict for later uses
                self.methods[plugin_name] = method

            except AssertionError:
                ico = 'default.svg'

            except:
                # if ever the attribute doesn't exists we set a default one
                if plugin_name in self.pending_plugins:
                    self.pending_plugins.remove(plugin_name)
                return

            # formatting the icon to be readable by Qt
            icon = QIcon(':%s' % ico)
            self.modifier_icons[plugin_name] = icon

            # we create the plugins widget label in Info & Tools
            label = IconLabel(plugin_name, self, icon=icon)
            label.setText('%i / %i' % (0, 0))

            if not self.WInfoGroup.expanded:
                label.setVisible(False)

            self.WInformations[plugin_name] = label        # updating info Widgets
            self.LInformations.addWidget(label)     # updating info Layout

            # because there was no error, we set the plugin as allowed by default
            self.allowed_plugins.add(plugin_name)

            self.update()

        # we finally update our pending_plugins
        if plugin_name in self.pending_plugins:
            self.pending_plugins.remove(plugin_name)

            # if the current plugin was the last one pending
            # we ask for a complete redraw
            if not len(self.pending_plugins):
                self.update_informations()
                self.renew_selection()

                self.LInformations.reorder()
                self.update()

    def update_plugin(self, plugin_name, status):
        """
        Update a plugin, turning it on or off, equivalent to a redraw
        :param plugin_name: plugin source name
        :type  plugin_name: str
        :param      status: on or off ?
        :type       status: bool
        """
        debug('update_plugin %s' % plugin_name)
        if status:
            # we run an asynchronous loading of the plugin
            SanityMan(self.add_plugin, plugin_name, os.path.join(self.app_dir, 'skiin_rsc'))
            self.pending_plugins.append(plugin_name)

        else:
            # we kick it
            self.remove_plugin(plugin_name)

            objects = set()

            # and we clean our views to remove any elements corresponding to this
            # modifier's type
            for view in (self.WOutView, self.WInView):
                for i in reversed(range(view.topLevelItemCount())):
                    item = view.topLevelItem(i)
                    if item and item.tp() == plugin_name:
                        item.del_checkbox()
                        view.takeTopLevelItem(i)

                        objects.add(item.obj())

        self.objects.clear()
        self.selection_changed()

    def remove_plugin(self, plugin_name):
        """
        Removing a plugin from Skiin's memory
        :param plugin_name: plugin's name
        """
        # we ask everybody to get rid of the plugin
        self.LInformations.removeWidget(plugin_name)
        if plugin_name in self.methods:
            self.methods.pop(plugin_name)
        if plugin_name in self.allowed_plugins:
            self.allowed_plugins.remove(plugin_name)

    def clean_plugins(self):
        """
        Safe removing all the loaded plugins and their widgets
        """
        pending = []

        # we wait until the information layout length match
        # the default parameters, this means all plugins unloaded
        while len(self.LInformations.widgets) != len(self.base_parameters):
            for plugin_name, widget in self.LInformations:
                if plugin_name not in self.base_parameters and plugin_name not in pending:
                    self.remove_plugin(plugin_name)

                    # we make sure the widget will be deleted
                    pending.append(plugin_name)

            # while widget isn't deleted we don't remove from pending
            for widget in pending:
                if widget not in self.LInformations:
                    pending.remove(widget)

        self.update()

    ### FORMATTING ###

    def format_name(self, name, filename=True):
        """
        Format the object's name considering the options parameters
        which are active
        :param     name: the object's name
        :param filename: if set will remove non-allowed file's name characters
        :return:         correctly formatted name
        :rtype:          str
        """
        # No namespace
        if self.WSaveNons.isChecked():
            name = name.split(':')[-1]

        # Only leaf for duplicate names
        if self.WSaveNoabs.isChecked():
            try:
                getAttr(name)

            except (ValueError, RuntimeError, TypeError):
                # if name is multiple in scene
                name = name.split('|')[-1]

        # format for file's name
        if filename:
            name = name.replace(':', '__').replace('|', '__')

        return name

    def set_name(self, obj=None):
        """
        Set the selection's file's name, apply the settings for a nice filename
        formatting and displays the preview path for the current selection
        :param obj: object(s)'s name(s)
        :type  obj: str or list
        :return:    the correct named file
        """
        if obj and isinstance(obj, basestring):
            src = obj

        elif self.WSaveSmart.isChecked():
            src = self.smart_name()

        # if nothing above is matching we consider the last object of the selection
        elif len(maya_tsf_selection()):
            src = maya_tsf_selection()[-1]

        else:
            src = ''

        # post formatting
        src = self.format_name(src)
        src += '.skin'

        # diplaying preview depending on case
        if src == '.skin' or not self.WOutView.topLevelItemCount():
            self.WPathPreview.setText('Empty export !')

        # multiple files case
        elif self.WSaveEach.isChecked() and len(self.WOutView.active_objects()) > 1:
            self.WPathPreview.setText("Preview : <b><font color='white'>%i</font></b> skin files in "
                                      "<font color='white'>%s</font>" % (len(self.WOutView.active_objects()), self.active_dir))

        # one file case
        else:
            self.WPathPreview.setText("Preview : <font color='white'>%s</font>" % os.path.join(self.active_dir, src))

        # enabling buttons depending on what's going on
        self.WSave.turn(self.WOutView.topLevelItemCount())
        self.WSaveAs.turn(self.WOutView.topLevelItemCount())

        return src

    def smart_name(self, objects=None):
        """
        Hehe... 'Smart' naming... We try to extract a relevant naming from a list
        of object, it will try to find the common letters between all the names,
        or the common modifiers between all the names
        :param objects: object(s)'s name(s)
        :type  objects: list
        :return:        'smart' name
        :rtype:         str
        """

        # our comparison tools
        mods = [m for m, o in self.WOutView.active_modifiers()]
        umod = make_unique(mods)
        objs = objects or self.WOutView.active_objects() or self.format_selection()[0]

        # trying to find a common name
        common = ''
        for work in objs:
            subs = [w for w in objs if w != work]

            for sub_work in subs:
                i = 0
                l = 2
                while (i + l) < len(sub_work):
                    part = work[i:i + l]

                    if part in sub_work:
                        if len(common) < len(part):
                            if len(subs) == sum([1 for s in subs if part in s]):
                                common = part

                        l += 1

                    else:
                        i += 1
                        l = 2

            # we break after the first loop because results must be the same
            # we did a for loop just to handle the 0 length case
            break

        # if there is only 1 modifier's type and 1 object but more than one modifier for this type
        if len(umod) == 1 and len(objs) == 1 and len(mods) != 1:
            name = objs[0] + '_' + umod[0]

        # if there is only 1 modifier's type, and multiple object and a common name for them
        # and more than one modifier
        elif len(umod) == 1 and len(objs) and len(common) > 3 and len(mods) != 1:
            name = common + '_' + umod[0]

        # if there is 1 object and many modifier's types
        elif len(objs) == 1 and len(umod) > 1:
            name = objs[0]

        # if no object and no modifier we have a look to the selection
        elif not len(objs) and not len(umod):
            if len(maya_tsf_selection()):
                name = maya_tsf_selection()[0]

            else:
                return ''

        # else we take evaluate the component's length of all object, and take the bigger one
        # as main object
        else:
            try:
                name = (sorted(objs, key=lambda x: objectEvaluate(x)), [common])[len(common) > 3]
                name = name[-1]

            except ValueError:
                name = (objs[0], common)[len(common) > 3]

            if not len(name):
                return ''

        # post formatting the name
        return self.format_name(name)

    def get_context(self, obj=None):
        """
        Setting the context for the given object(s)
        :param obj: object(s)'s name(s)
        :type  obj: list or str
        :return:    object(s)'s path
        """
        return os.path.join(self.active_dir, self.set_name(obj))

    def format_selection(self):
        """
        We read the selection, objects and components and returns a resume
        :return: tuple in shape (selection's object(s), selections's component(s))
        :rtype:  tuple
        """
        s = maya_selection(et='transform')
        v = maya_selection(et='float3')
        v += maya_selection(et='double3')

        # pre treatment for component case selection
        if len(v):
            if len(s):
                v = []
            s.extend(ls(hl=True))
            s.extend(listRelatives(ls(sl=True, o=True), p=True))

        return make_unique(s), v

    ### SELECTION ###

    def selection_changed(self, e=None, force=None):
        """
        Core function executed every time the Maya's selection is changed
        :param     e: event parameter, doesn't matter
        :param force: force reload of an object
        :type  force: list
        """
        # if there is still plugins loading or if we're undoing or redoing
        if len(self.pending_plugins) or MGlobal.isUndoing() or MGlobal.isRedoing():
            self.WProgress.set_mode()
            return

        # initialization
        self.WProgress.start()
        s, v = self.format_selection()
        self.WLocal.setChecked(len(v))

        # if force is set we try to delete every objects already in memory
        for f in (force or []):
            self.objects.delete(f)
            s.append(f)

        self.WForceSelection.setEnabled(True)

        # if there is only one object loading we purge
        if len(s) == 1:
            self.objects.clear()

        else:
            # otherwise we remove object which are not in the new selection
            for obj in self.objects():
                if obj.n not in s:
                    self.objects.delete(obj)

            self.WForceSelection.setEnabled(False)

        # If the view is not holded we go to refresh it
        if not self.WOutView.hold:
            # first we check if there is any invalid item our output view
            for item in self.WOutView.objects():
                if item != '' and item not in self.objects or item not in s:
                    # getting a list of all matchin indexes for this object
                    idx = map(self.WOutView.indexOfTopLevelItem, self.WOutView.findItems(item, Qt.MatchExactly, 2))

                    for id in reversed(idx):
                        self.WOutView.takeTopLevelItem(id)

            # for every object in the selection
            for obj in s:
                try:
                    # if not already loaded we build a new one and connect its signals
                    if obj not in self.objects:
                        new_obj = Skiin_Object(obj, self)
                        new_obj.init.connect(self.WOutView.progress.restart)
                        new_obj.step.connect(self.WOutView.progress.add)
                        new_obj.err.connect(self.WProgress.error)
                        new_obj.done.connect(self.WOutView.progress.done)

                        self.objects.add(new_obj)

                    # if the object is not in the list we need to make a raw evaluation
                    # of its modifiers and add them as new items
                    if obj not in self.WOutView.objects():
                        new_obj = self.objects[obj]

                        new_obj.ping()

                        new_obj.raw_all()

                        for modifier in new_obj.all_modifiers():
                            tp, hndl, mod = modifier

                            # we skip modifiers which are not loaded
                            if tp not in self.methods:
                                continue

                            item = self.insert_modifier_item(new_obj, hndl, mod)

                            self.WOutView.addTopLevelItem(item)
                            self.WOutView.repaint()

                        self.WOutView.resizeColumns()

                    self.objects[obj].components = None

                except IOError:
                    self.WProgress.done('Error with a plugin, no longer exists or deleted, refresh the Readers Plugins')
                    self.WProgress.set_mode('error')
                    return

                except:
                    # here we're trying to catch all error while using the Write plugin's
                    # method with a broad except
                    self.WProgress.error(exc_info())
                    continue

            # if there is some components selected we update the corresponding object
            if len(v):
                self.WCleanOld.setChecked(False)
                obj_from_component = make_unique(listRelatives(ls(v, o=True), p=True))

                for obj in obj_from_component:
                    self.objects[obj].components = v

            # after we updated all the objects' components we refresh the filters
            for obj in s:
                if self.objects[obj].components:
                    self.objects[obj].filter(self.WLocal.isChecked(), self.WLocalSet.isChecked())

        # some formatting
        lv = len(v)
        if lv:
            cmp_title = '%i components' % lv

            for obj in obj_from_component:
                s.remove(obj)

        lo = len(s)
        if lo > 1:
            obj_title = '%i objects' % lo

        elif lo:
            obj_title = '%s' % s[0]

        else:
            obj_title = ''

        if lv and lo:
            self.WOutName.setText("%s and %s" % (cmp_title, obj_title))

        elif lv:
            self.WOutName.setText(cmp_title)

        else:
            self.WOutName.setText(obj_title)

        # we ask Skiin to load the objects if ever the Autoload ability is enabled
        if self.WAutoLoad.isChecked() and not self.WInView.hold and not force:
            self.load_selection(self.objects.flat())

        # re-run views' filtering
        self.filter_mod(self.WOutView, self.WOutEdit.text())
        # applying conformity
        self.conform()

        # finalizing
        self.WProgress.done('>> Selection deformers acquired')

        self.set_name()

        self.WSave.turn(len(self.WOutView.active_objects()))
        self.WSaveAs.turn(len(self.WOutView.active_objects()))

        self.WOutView.resizeColumns()
        self.update_informations()

    def renew_selection(self, *args, **kwargs):
        """
        We force a complete redraw of the selection
        :param   args: args we want to forward to self.selection_changed
        :param kwargs: keyword args we want to forward to self.selection_changed
        """
        self.WOutView.progress.restart()
        self.WGlobalProgress.restart()

        for obj in self.objects:
            self.objects[obj].ping()

        self.selection_changed(*args, **kwargs)
        self.WProgress.done('>> Full Updated')

    def select_all_dag(self):
        """
        This (tries to) select all the DAG of an object
        """
        s = maya_selection()
        self.WProgress.restart('Scanning DAG...')

        if len(s):
            try:
                all_mod = set()
                step = 100.0 / len(listRelatives(s, ad=True) or [1])

                for obj in walk_object_tree(s):
                    self.WProgress.add(step, 'Scanning %s...' % obj)
                    all_mod.add(obj)

                if len(all_mod):
                    select(listRelatives(ls(listHistory(list(all_mod), f=True), type='shape'), p=True, f=True))

            except TypeError:
                select(list(all_mod))
                pass

            except:
                self.WProgress.error(exc_info())
                return

        if len(all_mod):
            select(list(all_mod), add=True)
        else:
            warning('Nothing found in this DAG')
            self.WProgress.restart()

    ### VIEW ITEMS ###

    def insert_modifier_item(self, obj, handle, mod):
        """
        Insert a new item in the Output view

        :param    obj: object's name
        :param handle: handle object
        :type  handle: handles.Handler
        :param    mod: modifier's name
        """
        # we get the modifier's type
        tp = handle.kind

        # creating a new item in the Output view
        item = Skiin_Item(self.WOutView, [mod, tp, obj, ''])
        item.add_checkbox()
        item.setIcon(0, self.modifier_icons[tp])

        self.WOutView.repaint()

        # we add subitems deformers
        self.insert_deformers(item, handle)

        # and we calculate the number of vertices affected
        raw = handle.reader.raw_data[mod]
        total = sum([raw[cnt] for cnt in raw])

        item.setText(3, str(min(total, len(handle))))

        return item

    def insert_deformers(self, parent, handle):
        """
        Insert all subitems of a modifiers, meaning the deformers of
        a modifier, such as joints, clusterHandle, and so on

        :param parent: Subitem's parent
        :type  parent: Skiin_Item
        :param handle: handle object
        :type  handle: handles.Handler
        :return:
        """
        raw = handle.reader.raw_data
        mod = parent.mod()
        tp = handle.kind

        # we set the parent as dead if raw data is empty
        if not len(raw[mod]):
            parent.kill()

        # and we add a sub Skiin_Item for each modifier name in the raw data
        for deformer in raw[mod]:
            tsf = mod_deformer_valid(deformer)
            cnt = len(handle) if self.methods[tp]._is_transform else raw[mod][deformer]

            sub = Skiin_Item(parent, [deformer, tsf, parent.object, str(cnt)])

            parent.addChild(sub)

    def delete_modifier_item(self, item):
        """
        We kick a modifier item

        :param item: the item object we want to remove
        :type  item: Skiin_Item
        """
        obj = item.obj()

        if obj in self.objects:
            self.WOutView.takeTopLevelItem(self.WOutView.indexOfTopLevelItem(item))

    ### SAVING ###

    def save_data_as(self):
        """
        Save modifiers' data to a specific file, asking the user to choose
        :return:
        """
        options = QFileDialog.Options() | QFileDialog.DontUseNativeDialog
        filename, _ = QFileDialog.getSaveFileName(self,
                "Save skinMaster file as..",
                self.active_dir,
                "skinMaster Files (*.skin)", options=options)

        if filename:
            # completing the filename if ever doesn't finish as a skin file
            if not filename.endswith('.skin'):
                filename += '.skin'

            self.save_data(filename, True)

    @creation_context(None)
    def save_data(self, path=None, override=False):
        """
        This core function save all the active modifiers' data to a given file
        :param     path: file path where to save the data
        :param override: if we want to override existing file or no
        """
        def create_file(obj):
            """
            Internal function to create a database file
            :param obj: the file's path
            :return: sqllite3 database, override all next existing files (bool)
            :rtype: tuple
            """
            yta = override

            try:
                # if the checkbox is onwe doesn't ask for overriding and directly try
                # to get rid of the file
                if self.WOverride.isChecked() or override:
                    os.remove(obj)

                # otherwise we double check from the user what to do if file exists
                elif os.path.isfile(obj):
                    reply = QMessage_FileExists(self, obj)
                    if reply == QMessageBox.Yes:
                        os.remove(obj)

                    elif reply == QMessageBox.YesToAll:
                        os.remove(obj)
                        yta = True

                    # open a file save dialog
                    elif reply == QMessageBox.Save:
                        self.save_data_as()
                        return None, False

                    else:
                        return None, False

            # if deletion fails
            except WindowsError:
                # but file still exists this means we dont have the rights
                if os.path.isfile(obj):
                    self.WProgress.error(exc_info())
                    return None, False

            # fastly open / close to create the file
            finally:
                open(obj, 'a').close()

            #
            DB = db_context(obj)
            return DB, yta

        # getting all active objects
        s = self.WOutView.active_objects()
        self.WGlobalProgress.restart()
        self.WProgress.restart('Empty Export !')

        # iif selection is empty we cancel
        if not len(s):
            self.WProgress.message = 'Empty Export'
            return

        one_file = not self.WSaveEach.isChecked() or path

        # also if database creation fails
        if one_file:
            database, override = create_file(os.path.join(self.active_dir, path or self.set_name()))
            if not database:
                return

        try:
            self.WOutView.progress.added.connect(self.WProgress.add)

            global_step = 100.0 / (len([0 for i in self.WOutView.active_items()]) + 1)

            for obj in s:
                # if the Save Each checkbox is on, we're saving a new database's file
                # for each object, skipping the saving if ever db creation fails
                if not one_file:
                    database, override = create_file(os.path.join(self.active_dir, self.set_name(obj)))
                    if not database:
                        continue

                # pre treatment
                self.WOutView.progress.restart()
                self.WProgress.restart(start=False)

                step = 100.0 / (len(self.WOutView.active_types(obj)) + 1) / 4

                debug('saving %s - %f' % (obj, step))
                obj_object = self.objects[obj]

                # we iterate through the list the active items for this object
                for tp, mod in self.WOutView.active_modifiers(obj):
                    self.WGlobalProgress.add(global_step)
                    self.WOutView.progress.add(step, '%s - Saving %s on %s' % (tp, mod, obj))

                    warn('Saving %s' % mod)
                    handle = obj_object.handles[tp]

                    # we write the modifier's data for this active item
                    modifier, generator = handle.write(mod)

                    # reading the deformer order to get which modifier is before and
                    # which one is after
                    obj_order = obj_object.deformer_order
                    try:
                        this = obj_order.index(mod)
                        before = obj_order[min((this + 1), len(obj_order) - 1)]
                        after = obj_order[max((this - 1), 0)]
                    except (ValueError, AttributeError):
                        # this means 'mod' is not contained in obj_order the most probable
                        # reason is that 'mod' affects the transform not the shape
                        before = after = ''

                    # then we start to insert the tables and generic values
                    database.execute(handle.format_DB_table % (obj, mod))
                    database.execute('''INSERT INTO 'summary' SELECT
                                  '%s' AS 'modifer',
                                  '%s' AS 'type',
                                  '%s' AS 'object',
                                  '%i' AS 'count',
                                  '%s' AS 'back',
                                  '%s' AS 'front' ''' % (mod, tp, obj, 0,
                                                              before, after))

                    # saving the weights for this specific modifier
                    self.WOutView.progress.add(step, '%s - Cooking %s weights on %s' % (tp, mod, obj))
                    insert_sequence(database, '%s|%s' % (obj, mod),
                                    generator)

                    # saving all the plugs of the modifier & its relatives
                    self.WOutView.progress.add(step, '%s - Baking connections for %s' % (tp, mod))
                    insert_sequence(database, 'connections',
                                    handle.get_plugs(mod),
                                    add=(mod,))

                    # saving the raw data (deformers count etc) for this modifier
                    self.WOutView.progress.add(step, '%s - Counting data for %s' % (tp, mod))
                    insert_sequence(database, 'raw',
                                    handle.raw_format(mod),
                                    add=(mod, obj))

                    # we finally update the total amount of vertices affected
                    raw = handle.reader.raw_data[mod]
                    total = sum([raw[cnt] for cnt in raw])

                    database.execute("UPDATE 'summary' SET 'count'='%i' WHERE object=='%s' AND modifier=='%s'"
                                     % (min(total, len(handle)), obj, mod))

                    database.commit()

                self.WOutView.progress.done()

        except:
            # broad except for debug purpose
            self.WProgress.error(exc_info())
            if database:
                database.close()
                database = None

        finally:
            # finalizing, we close the database if ever it has been opened
            debug('saving done')
            if not path:
                path = self.get_context()

            if database:
                self.WProgress.done('>> %s' % path)
                self.WGlobalProgress.done()
                self.WOutView.progress.added.disconnect(self.WProgress.add)

                database.close()

                if self.WAutoLoad.isChecked():
                    self.load_selection()

    ### LOADING ###

    def autoload(self, state):
        """
        Handle everytime we want to automatically load the selection
        update or clean
        :param state: update (True) or clean (False) ?
        :type  state: bool
        """
        if not self.WInView.hold and not len(self.pending_plugins):
            # we want to force the loading of the selected objects
            if state:
                self.load_selection(self.format_selection()[0])

            # otherwise we just want to clear the view
            else:
                self.WInView.clear()
                self.update_informations()

    def load_data_from(self):
        """
        We ask the user to give us a .skin file to load
        """
        self.WProgress.restart('Loading file...')
        options = QFileDialog.Options() | QFileDialog.DontUseNativeDialog
        filename, _ = QFileDialog.getOpenFileName(self,
                "Load skinMaster file", self.active_dir,
                "skinMaster Files (*.skin)", options=options)

        if filename:
            # if file is given we initialize a fully new loading
            self.WInView.clear()
            self.WInView.update()
            self.WGlobalProgress.restart()

            self.load_data(filename)

            self.filter_mod(self.WInView, self.WInEdit.text())

            # standard post loading events
            self.conform()
            self.WProgress.done('>> %s loaded in memory' % self.WInName.text())
            self.WGlobalProgress.done()
            self.WInView.resizeColumns()
            self.WFlush.turn(bool(self.WInView.topLevelItemCount()))

            self.update_informations()

    def load_selection(self, selection=None):
        """
        Load a given selection, if not provided will try to load all the Output
        objects, it'll first try to find a common file for all objects, if doesn't
        exists it'll look for a file for each needed object
        :param selection: the objects list
        :type  selection: iterable
        """
        # standard init, cleaning lists etc
        self.WInView.clear()
        self.WInView.clean()
        self.WOutView.clean()
        self.WGlobalProgress.restart()

        items = selection or self.WOutView.active_objects()
        needs = list(items)
        # we try to get a generic context for the current selection
        ctx = self.get_context()

        # if a common file exists for the whole selection
        if os.path.isfile(ctx):
            # we try load the data it contains, asking only for the needed objects
            for item in self.load_data(ctx, needs):
                try:
                    needs.remove(item.n)

                except ValueError:
                    # this means we've loaded an object which was not needed
                    pass

            # updating the list's title with the file's name
            self.WInName.setText('%s' % ntpath.basename(ctx))

        # if no common file, we try to load each object individually
        if len(items) > 1 and len(needs):
            self.WGlobalProgress.restart()
            # getting appliers for each object
            ctxs = [self.get_context(self.format_name(obj)) for obj in needs]
            step = 100.0 / len(ctxs)

            for i, ctx in enumerate(ctxs):
                self.WGlobalProgress.add(step)

                for item in self.load_data(ctx, needs):
                    if item.n in needs:
                        needs.remove(item.n)

            loaded = len(self.WOutView.active_objects()) - len(needs)

            if loaded > 0:
                self.WInName.setText('%i files' % loaded)

        # standard post loading events
        self.filter_mod(self.WInView, self.WInEdit.text())
        self.conform()
        self.WProgress.done('>> %s loaded in memory' % self.WInName.text())
        self.WGlobalProgress.done()
        self.WInView.resizeColumns()

        self.update_informations()

    @creation_context(None)
    def load_data(self, obj, needs=None):
        """
        Core function to load a .skin file into Skiin, if a need list is given
        it'll only try to load the needed object, it'll store the raw data o
        :param   obj: the object's file path or the object's name
        :type    obj: str
        :param needs: the wanted objects from this file path
        :type  needs: tuple
        :return:      loaded objects
        :rtype:       list
        """
        self.WInView.clean()
        self.WOutView.clean()

        if os.path.isfile(obj):
            path = obj
            obj = get_object_from_path(path)

        # if obj variable is not a file path we try to get a context
        else:
            path = self.get_context(obj)

        # if fails => bye bye
        if not os.path.isfile(path):
            return []

        # we hook the database
        database = sqlite3.connect(path)
        sum_cursor = database.cursor()

        # we add some SQL selector if we want specific objects
        if needs:
            selector = "WHERE "
            selector += " OR ".join(["object=='%s'" % need for need in needs])

        else:
            selector = ""

        sum_cursor.execute("SELECT * FROM summary %s" % selector)
        len_summary = database.execute("SELECT COUNT(*) FROM summary %s" % selector).fetchone()[0]

        # if our query is irrelevant
        if not len_summary:
            return []

        # standard pre loading events
        self.WInView.progress.added.connect(self.WProgress.add)
        self.WProgress.set_mode('loading')
        self.WInView.progress.restart()
        self.WProgress.restart(start=False)

        global_step = 100.0 / len_summary
        raw_cursor = database.cursor()
        objs = set()

        custom_cat = ((i, cat) for i, cat in enumerate(self.base_parameters))

        # we loop through our cursor to get each object's summary
        for n in sum_cursor:
            n = list(map(str, n))

            # we directly skip elements which are not loaded
            if n[1] not in self.methods:
                continue

            n[2] = self.objects.get(n[2])
            item = Skiin_Item(self.WInView, n)
            item.add_checkbox()
            item.setIcon(0, self.modifier_icons[n[1]])
            item.path = path

            # getting information for each type of connection the object may have
            item.nfo[item.tp()] = 1
            for i, cat in custom_cat:
                item.nfo[cat] = database.execute("SELECT COUNT(*) FROM connections "
                                                 "WHERE main=='%s' AND connected=='%i' " % (item.mod(), i)).fetchone()[0]

            # getting the raw datas for this modifier
            raw_cursor.execute("SELECT * FROM raw WHERE modifier=='%s' " % item.mod())
            table_len = database.execute("SELECT COUNT(*) FROM raw WHERE modifier=='%s' " % item.mod()).fetchone()[0]

            step = global_step / table_len

            # adding sub items for every deformer for this modifier
            for sn in raw_cursor:
                tsf, tpe, cnt, mod, obj = sn
                obj = self.objects.get(obj)
                sub = Skiin_Item(item, [self.format_name(tsf, False), tpe, obj, str(cnt)])
                item.addChild(sub)

                self.WInView.progress.add(step, 'Reading infos for %s - %s' % (mod, obj))

            objs.add(n[2])

            self.WInView.addTopLevelItem(item)

        # final closing
        database.close()

        # standard post processing
        self.WInView.progress.done('')
        self.WInView.progress.added.disconnect(self.WProgress.add)
        self.WInName.setText(ntpath.basename(path))

        self.WPaste.turn(len(list(objs)) and self.format_selection()[0])
        self.WFlush.turn(len(list(objs)))

        return list(objs)

    ### PASTING ###

    @creation_context(None)
    def paste_data(self):
        """
        Core function which paste the loaded and selected information on Maya's objects
        """
        # getting the active modifiers and the objects we want to paste on
        items = [item for item in self.WInView.active_items()]
        objects = self.WInView.active_objects()
        selection = self.format_selection()[0]
        deformers = set()
        reorder = []

        # abort if no modifier selected
        if not len(items):
            return

        main_step = 25.0 / len(items)
        self.WInView.progress.restart()
        self.WGlobalProgress.restart()

        # for every modifier we ;
        for index, item in enumerate(items):
            # check if the force selection is on, if so, we force the paste target
            # to be the selection
            if self.WForceSelection.isChecked() and self.WForceSelection.isEnabled():
                # we exit if we're trying to reach an unreachable object (or component)
                src = item.obj()
                if len(selection) and src not in selection:
                    obj = selection[0]
                else:
                    obj = src

            else:
                obj = src = item.obj()

            # getting the Skiin_Object of the current object
            obj = self.objects[obj]
            modifier = item.mod()
            mod_type = item.tp()
            path = item.path

            # we create a new handle for the object, if inexistant
            obj.add_handle(mod_type)
            obj.filter(self.WLocal.isChecked(), self.WLocalSet.isChecked())

            # now we load our skiin file database
            DB = sqlite3.connect(path)
            data_cursor = DB.cursor()
            data_cursor.execute("SELECT * FROM '%s|%s'" % (src, modifier))
            data_len = DB.execute("SELECT COUNT(*) FROM '%s|%s'" % (src, modifier))
            data_deformer = DB.execute("SELECT transform FROM 'raw' WHERE modifier=='%s' AND object=='%s'"
                                       % (modifier, src)).fetchall()

            # are we going to make a new modifier or just use the previous one ?
            build_modifier = self.WCleanOld.isChecked()

            if self.WCleanOld.isChecked():
                try:
                    # safe cleaning of the modifier
                    clear_modifier(modifier, mod_type)
                    warn('Clearing modifier %s' % modifier)

                except ValueError:
                    # modifier doesn't exists
                    pass

            else:
                try:
                    # simple ping to know the state of our modifier
                    getAttr(modifier)

                except RuntimeError:
                    # means object exists
                    if self.WForceSelection.isChecked() and obj.n != src:
                        build_modifier = True

                    elif not self.WLocal.isChecked():
                        # if the object exists we ask the user if he wants to remove
                        # the modifier or no
                        reply = QMessage_ModifierConflict(self, modifier)

                        if reply in (QMessageBox.Yes, QMessageBox.YesToAll):
                            if reply == QMessageBox.YesToAll:
                                self.WCleanOld.setChecked(True)
                            clear_modifier(modifier, mod_type)
                            self.WOutView.takeTopLevelItem(self.WInView.indexOfTopLevelItem(item))

                        elif reply == QMessageBox.Cancel:
                            return

                        build_modifier = True

                except ValueError:
                    # means the object doesn't exists
                    if mod_type == 'skinCluster' and self.WLocal.isChecked():
                        # exceptionnal case where we want to paste a skinCluster with
                        # a local selection on
                        reply = QMessage_SkinClusterHazard(self, obj)

                        if reply == QMessageBox.Abort:
                            return

                    build_modifier = True


            # we specify if we want to select the parameters only from this object (object)
            # or from all its relatives (main)
            selector = ('object', 'main')[self.WPropagate.isChecked()]
            # modifiers's constraint query
            constraints = DB.execute("SELECT * FROM 'connections' WHERE main=='%s' AND connected==2" % modifier).fetchall()
            cst_sel = ''
            cst_cnd = 'OR' if self.WPasteConstraint.isChecked() else 'AND NOT'

            # we modify the selector for each modifier's constraint
            for cst in constraints:
                c = cst[0].split('##')[0]
                cst_sel += " %s (object=='%s' OR source LIKE '%s%%' OR destination LIKE '%%%s')" % (cst_cnd, c, c, c)

            # parameters query with the previously generated selectors
            # we skip the connected==3 which are the constraints informations
            params = DB.execute("SELECT * FROM 'connections' WHERE %s=='%s' %s AND NOT connected==3 ORDER BY connected DESC" % (selector, modifier, cst_sel))
            param_len = DB.execute("SELECT COUNT(*) FROM 'connections' WHERE %s=='%s' %s" % (selector, modifier, cst_sel))

            # temporarly turning off the callbacks
            self.clearCallback()
            err = False

            try:
                # first we cook our datas for the database's cursor
                obj.cook(mod_type, data_cursor)

                # specific arguments depending on modifiers' type
                if mod_type in ('wire', 'blendShape'):
                    add = {'w': [s[0] for s in data_deformer]}

                elif mod_type == 'cluster':
                    add = {}

                else:
                    add = {}

                # if the modifier's name change (because of a duplicate) we need
                # to be sure we are still working on the correct one (which name
                # has changed
                new_mod = modifier
                if build_modifier:
                    new_mod = obj.create_modifier(mod_type, modifier, **add)

                else:
                    obj.set_modifier(mod_type, modifier)

                # here we look to the original modifiers' names and relatives and create replacement
                # rules with the new names (if old modifiers aren't deleted on creation
                old = [modifier] + self.methods[mod_type].kinsmen(modifier)
                new_mod = new_mod if isinstance(new_mod, list) else [new_mod]
                new = [new_mod[0]] + self.methods[mod_type].kinsmen(new_mod[0])
                rep = [(old[i], new[i]) for i in range(len(new)) if old[i] != new[i]]
                rep = rep if len(rep) else None

                # then we can apply our database cursor for the current parameters
                obj.apply_parameters(mod_type, params, rep)

                # getting the parenting relations
                if self.WPasteParent.isChecked():
                    params = DB.execute("SELECT * FROM 'connections' WHERE main=='%s' AND connected==3" % modifier)
                    # applying only parenting params
                    obj.apply_parameters(mod_type, params, rep)

                # we apply the modifier's weights for each component
                obj.apply_weights(mod_type)

                # if the paste is local and mode is a set we will only modify
                # the modifier's set
                if self.WLocal.isChecked() and self.WLocalSet.isChecked():
                    obj.set_filter(modifier)

            except:
                # broad exception for debugging purpose
                self.WProgress.error(exc_info())
                err = True
                break

            # here we retrieve which modifier were before and which one where after
            # so we will be able to reorder the modifier stack properly later
            before, after = DB.execute("SELECT before, after FROM 'summary' WHERE object=='%s' AND modifier=='%s'" % (src, modifier)).fetchone()
            reorder.append((before, after, modifier, obj))
            DB.close()

            deformers.add(mod_type[0].upper())

        if self.WCleanOld.isChecked():
            def reorder_the_world(current, order):
                """
                receive list of objects with before after, return a new list
                ordered by safe-level
                """
                safest, safe, unsafe = [], [], []
                i = len(order) - 1

                for o in reversed(order):
                    item = order.pop(i)[1:]
                    if o[0] not in current and o[1] not in current:
                        safest.append(item)

                    elif o[1] in current:
                        safe.append(item)

                    elif o[1] not in current:
                        safe.insert(0, item)

                    elif o[0] not in current:
                        unsafe.append(item)

                    else:
                        order.insert(i, item)

                    i -= 1
                # Oh my God here they are !!
                new_world_order = safest + safe + order + unsafe

                return new_world_order

            # so we got the modifiers' names
            items_names = [i.mod() for i in items]

            # and ask for a relevant order from safest to unsafest
            # so we can reorder the deformers safely
            for after, mod, obj in reorder_the_world(items_names, reorder):
                try:
                    assert after == mod
                    reorderDeformers(after, mod, obj)

                except (AssertionError, RuntimeError):
                    pass

        # now we tell Skiin to listen again to callbacks
        self.addCallback()

        # and setting final visuals
        if not err:
            if len(objects) > 1:
                fmt = 'Deformer(s) %s applied on %i objects' % (', '.join(deformers), len(objects))

            else:
                fmt = 'Deformer(s) %s applied on %s' % (', '.join(deformers), obj)

            self.WInView.progress.done('>> %s' % fmt)
            self.renew_selection(force=list(set([i.obj() for i in items])))

    ### VISUAL ###

    def toggle_sync(self, force=False):
        """
        This toggle the synchronization between the two views, so when you
        scroll, click, double click, check in one view it does the same in
        the other
        :param force: If we want to force the synchronization state function
        :type  force: self.synchronize or self.unsynchronize
        """
        # the views' signals and their corresponding events
        signals = [lambda x: x.click,
                   lambda x: x.double_click,
                   lambda x: x.horizontalScrollBar().valueChanged,
                   lambda x: x.verticalScrollBar().valueChanged,
                   lambda x: x.childCheck]

        event = [lambda x: x.mousePressEvent,
                 lambda x: x.mouseDoubleClickEvent,
                 lambda x: x.horizontalScrollBar().setValue,
                 lambda x: x.verticalScrollBar().setValue,
                 lambda x: x.checkTopLevelItem]

        if force:
            applicator = force

        elif self.WSync.lock:
            applicator = self.unsynchronize()

        elif not self.WSync.lock:
            applicator = self.synchronize()

        try:
            i, o = self.WOutView, self.WInView
            # we connect from and to
            for j in range(len(signals)):
                applicator(signals[j](i), event[j](o))
                applicator(signals[j](o), event[j](i))

            # view filters' exception
            applicator(self.WOutEdit.textChanged, self.WInEdit.setText)
            applicator(self.WInEdit.textChanged, self.WOutEdit.setText)

        except RuntimeError:
            pass

    def synchronize(self, apply=False):
        """
        Sync the view, sending the connect order
        """
        debug('activating synchronization')
        self.WSync.lock = True
        self.WSync.setIcon(QIcon(':Lock_ON.png'))
        self.WSync.setStyleSheet('''border:2px solid rgb(%i, %i, %i);border-radius:4px;''' % (225, 175, 45))
        applicator = lambda s, e: s.connect(e)

        if apply:
            self.toggle_sync(applicator)

        return applicator

    def unsynchronize(self, apply=False):
        """
        Unsync the view, sending the disconnect order
        """
        debug('disabling synchronization')
        self.WSync.lock = False
        self.WSync.setIcon(QIcon(':Lock_OFF_grey.png'))
        self.WSync.setStyleSheet('''border:1px solid rgb(%i, %i, %i);border-radius:4px;''' % (175, 175, 175))
        applicator = lambda s, e: s.disconnect(e)

        if apply:
            self.toggle_sync(applicator)

        return applicator

    def conform(self):
        """
        Conforming the views and visual stuff like that..
        """
        self.WInView.conform(self.WOutView)
        self.set_conformity(Skiin_View.compare(self.WInView, self.WOutView))
        self.update_informations()

    def set_conformity(self, perc):
        """
        Layout the Conformity Label to be visually explicit
        """
        self.WConformity.setText('%.2f %%' % (perc * 100))

        r = min([(1 - perc) * 200 + 225 * sin(perc * pi), 225])
        g = perc * 200 + 100 * sin(perc * pi)
        b = 78 * perc + 45 * (1 - perc)

        self.WConformity.setStyleSheet("QLabel{color:rgb(%i,%i,%i);font-size:16px;font-weight:bold}" % (r, g, b))

        if perc >= 0.95 and not self.WSync.lock:
            self.synchronize(True)

        elif perc == 0.0 and self.WSync.lock:
            self.unsynchronize(True)

    def update_informations(self):
        """
        Update the informations contained in Tools & Info group box, acquiring
        new data if not available
        """
        custom_cat = [(i, cat) for i, cat in enumerate(self.base_parameters)]
        infos = {cat: [0, 0] for cat in self.WInformations}

        # for each view we look at items and look at it 'nfo' dict, if empty
        # or full of data
        for i, view in enumerate((self.WInView, self.WOutView)):
            for item in view.active_items():

                # if empty we need to get the informations
                if not len(item.nfo):
                    item.nfo = {cat:0 for cat in self.WInformations}

                    # adding the relative's data to the current item nfo
                    for relatives in self.methods[item.tp()].kinsmen(item.mod()):
                        plugs = get_rough_plugs(relatives)

                        for j, cat in custom_cat:
                            item.nfo[cat] += plugs[j]

                    plugs = get_rough_plugs(item.mod())

                    for j, cat in custom_cat:
                        item.nfo[cat] += plugs[j]

                    # adding 1 to the current item's type
                    item.nfo[item.tp()] += 1

                for cat in set(item.nfo):
                    try:
                        infos[cat][i] += item.nfo[cat]
                    except KeyError:
                        item.nfo.pop(cat)

        # now we just compare the input and output informations, coloring them
        # depending if they match or now
        for cat in set(self.WInformations):
            try:
                inlen, outlen = infos[cat]

                if (inlen + outlen) == 0:
                    self.WInformations[cat].setText("<font color='#778'>%i</font> <font color='#778'>|</font> "
                                                    "<font color='#778'>%i</font>" % (inlen, outlen))

                elif inlen == outlen:
                    self.WInformations[cat].setText("<font color='#2F6'>%i</font> <font color='#778'>|</font> "
                                                    "<font color='#5F8'>%i</font>" % (inlen, outlen))

                else:
                    self.WInformations[cat].setText("<font color='#F52'>%i</font> <font color='#778'>|</font> "
                                                    "<font color='#F21'>%i</font>" % (inlen, outlen))

            except RuntimeError:
                self.WInformations.pop(cat)


def Skiin_UI():
    """
    Display function
    """
    Skiin_Win = Skiin_Class(get_maya_window())
    Skiin_Win.show()
    return Skiin_Win


class Skiin_Help_UI(QDialog):
    def __init__(self, parent=None):
        super(Skiin_Help_UI, self).__init__(parent)
        self.setFixedWidth(300)
        self.setWindowTitle('Skiin Help')

        L_main = QVBoxLayout()

        self.WB_ok = QPushButton('Bake', self)
        self.WB_ok.clicked.connect(self.close)

        V_info = QLabel("Welcome to <b>Skiin</b>, this is an evolutive weights / effector manager for Maya. "
                        "<br><br>Allowing you to export / import (almost) whatever you want from your Maya <u>rig</u>"
                        " scene !<br><br>See tutorial at at <u>http://www.mehdilouala.com/Skiin</u>")
        V_info.setWordWrap(True)

        L_main.addWidget(V_info)
        L_main.addWidget(self.WB_ok)

        self.setLayout(L_main)


def help():
    Help_Win = Skiin_Help_UI(get_maya_window())
    Help_Win.show()
    return Help_Win