Browse code

Core update 2.0.0

Hexatron authored on 13/04/2017 07:08:39
Showing 1 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,1011 @@
0
+"""
1
+Massive Attribute Editor;
2
+This tool simply wrap all the common attributes between the selected objects and display them in a list,
3
+you can then filter this list and edit a given attribute for all the selection at the same time.
4
+
5
+Version 2.0 currently handles :
6
+        Float
7
+        Integer
8
+        Enum
9
+        Bool
10
+        Float 3
11
+        Float 4
12
+        Color
13
+        String
14
+    Version 2 includes filtering options,
15
+     improved attributes' values controllers,
16
+     deeper attribute search and control over children.
17
+
18
+More scripts at http://mehdilouala.com/scripts
19
+"""
20
+
21
+__author__ = "Mehdi Louala"
22
+__copyright__ = "Copyright 2017, Mehdi Louala"
23
+__credits__ = ["Mehdi Louala"]
24
+__license__ = "GPL"
25
+__version__ = "2.0.0"
26
+__maintainer__ = "Mehdi Louala"
27
+__email__ = "mlouala@gmail.com"
28
+__status__ = "Stable Version"
29
+
30
+from functools import partial
31
+import re
32
+
33
+from maya import cmds
34
+from maya.OpenMaya import MEventMessage, MMessage
35
+import maya.OpenMayaUI as om
36
+
37
+try:
38
+    from PySide.QtCore import Qt, QObject, Signal, QLocale
39
+    from PySide.QtGui import QTreeWidgetItem, QDialog, QVBoxLayout, QPushButton, QLineEdit, QTreeWidget, QDoubleSpinBox, \
40
+        QSpinBox, QComboBox, QCheckBox, QWidget, QHBoxLayout, QGroupBox, QColorDialog, QIcon, QLabel, QFont, QFrame, \
41
+        QSlider, QApplication
42
+    from shiboken import wrapInstance as wrapinstance
43
+except ImportError:
44
+    from PySide2.QtCore import Qt, QObject, Signal, QLocale
45
+    from PySide2.QtGui import QIcon, QFont
46
+    from PySide2.QtWidgets import QTreeWidgetItem, QDialog, QVBoxLayout, QPushButton, QLineEdit, QTreeWidget, \
47
+        QDoubleSpinBox, QSpinBox, QComboBox, QCheckBox, QWidget, QGroupBox, QHBoxLayout, QLabel, QFrame, QSlider, \
48
+        QApplication
49
+    from shiboken2 import wrapInstance as wrapinstance
50
+
51
+
52
+def get_maya_window():
53
+    return wrapinstance(long(om.MQtUtil.mainWindow()), QWidget)
54
+
55
+
56
+History, Shape, Object, Separator = range(4)
57
+
58
+
59
+class QTreeWidget_Separator(QTreeWidgetItem):
60
+    """
61
+    Simple unselectable item acting as separator in list
62
+    """
63
+    def __init__(self, text='_______'):
64
+        super(QTreeWidget_Separator, self).__init__()
65
+        self.setText(0, text)
66
+        self.setTextAlignment(0, Qt.AlignRight)
67
+        self.setDisabled(True)
68
+
69
+
70
+class Double3(QFrame):
71
+    """
72
+    A 3 slider Line for 3d arrays
73
+    """
74
+    valuesChanged = Signal(list)
75
+
76
+    def __init__(self, parent=None):
77
+        super(Double3, self).__init__(parent)
78
+
79
+        self.x = QDoubleSpinBox()
80
+        self.y = QDoubleSpinBox()
81
+        self.z = QDoubleSpinBox()
82
+
83
+        self.setLayout(line(self.x, self.y, self.z))
84
+        # connecting all signals
85
+        for elem in (self.x, self.y, self.z):
86
+            elem.valueChanged.connect(self.element_value_changed)
87
+
88
+    def element_value_changed(self):
89
+        self.valuesChanged.emit([self.x.value(), self.y.value(), self.z.value()])
90
+
91
+    def setValues(self, xyz):
92
+        x, y, z = xyz
93
+        self.x.setValue(x)
94
+        self.y.setValue(y)
95
+        self.z.setValue(z)
96
+
97
+
98
+class Double4(Double3):
99
+    """
100
+    A 4 slider Line for 4d arrays (quaternion)
101
+    """
102
+    def __init__(self, parent=None):
103
+        super(Double4, self).__init__(parent)
104
+        self.w = QDoubleSpinBox()
105
+        self.layout().addWidget(self.w)
106
+
107
+        self.w.valueChanged.connect(self.element_value_changed)
108
+
109
+    def element_value_changed(self):
110
+        self.valuesChanged.emit([self.x.value(), self.y.value(), self.z.value(), self.w.value()])
111
+
112
+    def setValues(self, xyzw):
113
+        x, y, z, w = xyzw
114
+        self.x.setValue(x)
115
+        self.y.setValue(y)
116
+        self.z.setValue(z)
117
+        self.z.setValue(w)
118
+
119
+
120
+class ColorPicker(QPushButton):
121
+    """
122
+    A simple widget to pick a color and replace the background's color of the button with the new
123
+    selected color
124
+    """
125
+    colorChanged = Signal(list)
126
+
127
+    def __init__(self, parent=None):
128
+        super(ColorPicker, self).__init__(parent)
129
+        self.setText('Pick color...')
130
+        self.clicked.connect(self.get_color)
131
+
132
+    def get_color(self):
133
+        """
134
+        Raise a QColorDialog and apply the color to the background color of the button
135
+        """
136
+        dial = QColorDialog()
137
+        dial.setOptions(QColorDialog.DontUseNativeDialog)
138
+        res = dial.exec_()
139
+
140
+        if res:
141
+            rgb = dial.currentColor().red(), dial.currentColor().green(), dial.currentColor().blue()
142
+            self.setColor(rgb)
143
+            rgb = [c / 255.0 for c in rgb]
144
+            self.colorChanged.emit(rgb)
145
+
146
+    def setColor(self, rgb=(255, 255, 255)):
147
+        """
148
+        Apply the given rgb to the background and switching the text color depending on color's value
149
+        :param rgb: red green blue
150
+        :type  rgb: tuple
151
+        """
152
+        r, g, b = rgb
153
+        color = 'rgb({},{},{})'.format(r, g, b)
154
+        fg = 'white' if sum([r, g, b]) < 255 else 'black'
155
+
156
+        self.setStyleSheet('QPushButton{background-color:%s;color:%s;}' % (color, fg))
157
+        self.update()
158
+
159
+
160
+class NumericBox(QFrame):
161
+    """
162
+    A box with a spinner and a slider to allow easier value manipulation by user
163
+    """
164
+    valueChanged = Signal(float)
165
+    spinner = QDoubleSpinBox
166
+    step = 0.1
167
+
168
+    def __init__(self, parent=None):
169
+        super(NumericBox, self).__init__(parent)
170
+        self.slider = QSlider(Qt.Horizontal)
171
+
172
+        self.spinner = self.spinner()
173
+        self.spinner.setSingleStep(self.step)
174
+
175
+        self.slider.setTickInterval(self.step * 100)
176
+        self.slider.setSingleStep(self.step * 100)
177
+
178
+        self.slider.sliderMoved.connect(lambda x: self.spinner.setValue(x / 100))
179
+        self.spinner.valueChanged.connect(self.applyValue)
180
+
181
+        self.setLayout(line(self.slider, self.spinner))
182
+        self.layout().setStretch(1, 0)
183
+
184
+    def applyValue(self, value):
185
+        self.valueChanged.emit(value)
186
+        self.slider.setValue(value * 100)
187
+
188
+    def setValue(self, value):
189
+        self.spinner.setValue(value)
190
+        self.slider.setValue(value * 100)
191
+
192
+    def setRange(self, mini, maxi):
193
+        self.spinner.setRange(mini, maxi)
194
+        self.slider.setRange(mini * 100, maxi * 100)
195
+
196
+    def value(self):
197
+        return self.spinner.value()
198
+
199
+
200
+class FloatBox(NumericBox):
201
+    """
202
+    Float case
203
+    """
204
+    spinner = QDoubleSpinBox
205
+    step = 0.1
206
+
207
+    def __init__(self, parent=None):
208
+        super(FloatBox, self).__init__(parent)
209
+
210
+
211
+class IntBox(NumericBox):
212
+    """
213
+    Integer case
214
+    """
215
+    spinner = QSpinBox
216
+    step = 1
217
+
218
+    def __init__(self, parent=None):
219
+        super(IntBox, self).__init__(parent)
220
+
221
+
222
+class Filter(QLineEdit):
223
+    """
224
+    Filter widget to display a little X button on the right of the field if ever something's inside
225
+    """
226
+    def __init__(self, *args):
227
+        super(Filter, self).__init__(*args)
228
+        self.textChanged.connect(self.isClean)
229
+
230
+        self.clear_button = QPushButton('x', self)
231
+        self.clear_button.setVisible(False)
232
+        self.clear_button.setCursor(Qt.ArrowCursor)
233
+        self.clear_button.clicked.connect(self.clear)
234
+
235
+    def isClean(self, text):
236
+        """ Check the emptyness of the field """
237
+        self.clear_button.setVisible(text != '')
238
+
239
+    def resizeEvent(self, e):
240
+        super(Filter, self).resizeEvent(e)
241
+        self.clear_button.setGeometry(self.width() - 18, 2, 16, 16)
242
+
243
+
244
+def line(*widgets):
245
+    """
246
+    Creates a horizontal layout
247
+
248
+    :param widgets: the widgets you wan to add to the layout
249
+    :type  widgets: tuple[QWidget]
250
+
251
+    :return: the horizontal layout
252
+    :rtype: QHBoxLayout
253
+    """
254
+    l = QHBoxLayout()
255
+    apply_layout(l, *widgets)
256
+    return l
257
+
258
+
259
+def col(*widgets):
260
+    """
261
+    Creates a vertical layout
262
+
263
+    :param widgets: the widgets you wan to add to the layout
264
+    :type  widgets: tuple[QWidget]
265
+
266
+    :return: the vertical layout
267
+    :rtype: QVBoxLayout
268
+    """
269
+    l = QVBoxLayout()
270
+    apply_layout(l, *widgets)
271
+    return l
272
+
273
+
274
+def group(title='', *widgets):
275
+    """
276
+    Creates a groupBox with the widgets inside
277
+
278
+    :param   title: group's title
279
+    :param widgets: widgets you wqnt to qdd
280
+    :type  widgets: tuple[QWidget]
281
+
282
+    :return: the group box
283
+    :rtype: QGroupBox
284
+    """
285
+    g = QGroupBox()
286
+    g.setTitle(title)
287
+    l = QVBoxLayout()
288
+    apply_layout(l, *widgets)
289
+    l.setContentsMargins(4, 4, 4, 4)
290
+    g.setLayout(l)
291
+    return g
292
+
293
+
294
+def apply_layout(l, *widgets):
295
+    """
296
+    Apply the widgets to the given layout
297
+
298
+    :param       l: the layout
299
+    :type        l: QHBoxLayout | QVBoxLayout
300
+    :param widgets: the widgets you want to add
301
+    :type  widgets: tuple[QWidget]
302
+    """
303
+    # looping through widget and add them to the given layout
304
+    for i, widget in enumerate(widgets):
305
+        if isinstance(widget, basestring):
306
+            widget = QLabel(widget)
307
+
308
+        try:
309
+            l.addWidget(widget)
310
+
311
+        except TypeError:
312
+            l.addLayout(widget)
313
+
314
+        l.setStretch(i, int(not isinstance(widget, QLabel)))
315
+
316
+    l.setContentsMargins(0, 0, 0, 0)
317
+
318
+
319
+class MassAttribute_UI(QDialog):
320
+    """
321
+    The main UI
322
+    """
323
+    class Applikator(QObject):
324
+        """
325
+        This is the core applier which toggle the display of the corresponding widget and handling events' connections
326
+        """
327
+        def __init__(self, parent=None):
328
+            super(MassAttribute_UI.Applikator, self).__init__()
329
+            self.root = parent
330
+
331
+        def widget_event(self, t):
332
+            """
333
+            Return the correct widget's event depending on attribute's type
334
+            :param t: the attribute's type
335
+            :type  t: str
336
+            :return: the event
337
+            :rtype : Signal
338
+            """
339
+            return {'float': self.root.W_EDI_float.valueChanged, 'enum': self.root.W_EDI_enum.currentIndexChanged,
340
+                    'int': self.root.W_EDI_int.valueChanged, 'bool': self.root.W_EDI_bool.stateChanged,
341
+                    'str': self.root.W_EDI_str.textChanged, 'd3': self.root.W_EDI_d3.valuesChanged,
342
+                    'd4': self.root.W_EDI_d4.valuesChanged, 'color': self.root.W_EDI_color.colorChanged}[t]
343
+
344
+        def unset_editors(self):
345
+            """
346
+            Toggle off all editors and disconnect the current one
347
+            """
348
+            for widget in (self.root.W_EDI_float, self.root.W_EDI_int, self.root.W_EDI_enum,
349
+                           self.root.W_EDI_bool, self.root.W_EDI_str, self.root.W_EDI_d3,
350
+                           self.root.W_EDI_d4, self.root.W_EDI_color):
351
+                widget.setVisible(False)
352
+
353
+            # trying to force disconnection
354
+            try:
355
+                self.widget_event(self.root.ctx).disconnect(self.root.apply_value)
356
+            except (KeyError, RuntimeError):
357
+                pass
358
+
359
+        def prepare(applier_name):
360
+            """
361
+            A decorator to prepare the attribute depending on type for the corresponding widget and getting the
362
+            attribute's value
363
+            :param applier_name: attribute's type
364
+            :type  applier_name: str
365
+            """
366
+            def sub_wrapper(func):
367
+                def wrapper(self, attr_path):
368
+                    self.unset_editors()
369
+                    self.root.ctx = applier_name
370
+                    self.root.__getattribute__('W_EDI_%s' % applier_name).setVisible(True)
371
+                    ret = func(self, cmds.getAttr(attr_path), attr_path)
372
+                    return ret
373
+                return wrapper
374
+            return sub_wrapper
375
+
376
+        @staticmethod
377
+        def get_bounds(obj, attr, min_default, max_default):
378
+            """
379
+            Try to retrieve the range for the given attribute, if min or max fail it'll set default values
380
+            :param         obj: the object's name
381
+            :type          obj: str
382
+            :param        attr: attribute's name
383
+            :type         attr: str
384
+            :param min_default: minimum default value
385
+            :param max_default: max default value
386
+            :type  min_default: float | int
387
+            :type  max_default: float | int
388
+            :return: minimum, maximum
389
+            :rtype : tuple
390
+            """
391
+            try:
392
+                assert cmds.attributeQuery(attr, n=obj, mxe=True)
393
+                maxi = cmds.attributeQuery(attr, n=obj, max=True)[0]
394
+            except (RuntimeError, AssertionError):
395
+                maxi = max_default
396
+            try:
397
+                assert cmds.attributeQuery(attr, n=obj, mne=True)
398
+                mini = cmds.attributeQuery(attr, n=obj, min=True)[0]
399
+            except (RuntimeError, AssertionError):
400
+                mini = min_default
401
+            return mini, maxi
402
+
403
+        @prepare('float')
404
+        def apply_float(self, value, path):
405
+            """
406
+            Float attribute case
407
+            :param value: attribute's value
408
+            :param  path: attribute's path = obj.attr
409
+            """
410
+            obj, attr = path.split('.', 1)
411
+            self.root.W_EDI_float.setRange(*self.get_bounds(obj, attr, -100.0, 100.0))
412
+            self.root.W_EDI_float.setValue(value)
413
+
414
+        @prepare('enum')
415
+        def apply_enum(self, value, path):
416
+            """Enum case"""
417
+            self.root.W_EDI_enum.clear()
418
+            obj, attr = path.split('.', 1)
419
+            try:
420
+                enums = [enum.split('=')[0] for enum in cmds.attributeQuery(attr, n=obj, listEnum=True)[0].split(':')]
421
+            except RuntimeError:
422
+                self.apply_int(path)
423
+            else:
424
+                self.root.W_EDI_enum.addItems(enums)
425
+                self.root.W_EDI_enum.setCurrentIndex(enums.index(cmds.getAttr(path, asString=True)))
426
+
427
+        @prepare('int')
428
+        def apply_int(self, value, path):
429
+            """Integer case"""
430
+            obj, attr = path.split('.', 1)
431
+            self.root.W_EDI_int.setRange(*self.get_bounds(obj, attr, -1000, 1000))
432
+            self.root.W_EDI_int.setValue(value)
433
+
434
+        @prepare('bool')
435
+        def apply_bool(self, value, path):
436
+            """Boolean case"""
437
+            self.root.W_EDI_bool.setChecked(value)
438
+            self.root.W_EDI_bool.setText(path.split('.', 1)[1])
439
+
440
+        @prepare('str')
441
+        def apply_str(self, value, path):
442
+            """String case"""
443
+            self.root.W_EDI_str.setText(value)
444
+
445
+        @prepare('d3')
446
+        def apply_d3(self, value, path):
447
+            """3D array case"""
448
+            self.root.W_EDI_d3.setValues(value[0])
449
+
450
+        @prepare('d4')
451
+        def apply_d4(self, value, path):
452
+            """4D array case"""
453
+            self.root.W_EDI_d4.setValues(value[0])
454
+
455
+        @prepare('color')
456
+        def apply_color(self, value, path):
457
+            """Color case"""
458
+            try:
459
+                colors = value[0]
460
+                self.root.W_EDI_color.setColor([int(c * 255) for c in colors])
461
+            except TypeError:
462
+                self.apply_int(value, path)
463
+
464
+    class Attribute(str):
465
+        """
466
+        A custom string attribute class to ship more information into the string variable
467
+        """
468
+        def __new__(cls, path='', super_type=Object):
469
+            obj, attr = path.split('.', 1)
470
+
471
+            str_obj = str.__new__(cls, attr)
472
+
473
+            str_obj.obj, str_obj.attr = obj, attr
474
+            str_obj.path = path
475
+            str_obj.super_type = super_type
476
+            str_obj.type = None
477
+
478
+            return str_obj
479
+
480
+    # static variables to pre-load icons and attributes short names
481
+    ctx_icons = {'float': QIcon(':render_decomposeMatrix.png'),
482
+                 'enum': QIcon(':showLineNumbers.png'),
483
+                 'bool': QIcon(':out_decomposeMatrix.png'),
484
+                 'time': QIcon(':time.svg'),
485
+                 'byte': QIcon(':out_defaultTextureList.png'),
486
+                 'angle': QIcon(':angleDim.png'),
487
+                 'string': QIcon(':text.png'),
488
+                 'float3': QIcon(':animCurveTA.svg'),
489
+                 'float4': QIcon(':animCurveTA.svg'),
490
+                 'color': QIcon(':clampColors.svg')}
491
+
492
+    for ctx in ('doubleLinear', 'double', 'long', 'short'):
493
+        ctx_icons[ctx] = ctx_icons['float']
494
+
495
+    ctx_icons['double3'] = ctx_icons['float3']
496
+    ctx_icons['double4'] = ctx_icons['float4']
497
+
498
+    ctx_wide = {'float': ('float', 'doubleLinear', 'double', 'long', 'short'),
499
+                'enum': ('enum',),
500
+                'bool': ('bool',),
501
+                'time': ('time',),
502
+                'byte': ('byte',),
503
+                'angle': ('doubleAngle',),
504
+                'string': ('string',),
505
+                'float3': ('double3', 'float3'),
506
+                'float4': ('double4', 'float4'),
507
+                'color': ('color',)}
508
+
509
+    def __init__(self, parent=None):
510
+        super(MassAttribute_UI, self).__init__(parent)
511
+        # Abstract
512
+        self.applier = self.Applikator(self)
513
+        self.selection = []
514
+        self.callback = None
515
+        self.ctx = None
516
+        # storing found attributes' types to avoid double check
517
+        self.solved = {}
518
+        self.setLocale(QLocale.C)
519
+        self.setAttribute(Qt.WA_DeleteOnClose)
520
+        self.setAttribute(Qt.WA_QuitOnClose)
521
+
522
+        self.setFixedWidth(300)
523
+        self.setWindowTitle('Massive Attribute Modifier')
524
+
525
+        # UI
526
+        L_main = QVBoxLayout()
527
+
528
+        self.WV_title = QLabel('')
529
+        self.WV_title.setVisible(False)
530
+        self.WV_title.setFont(QFont('Verdana', 10))
531
+        self.WV_title.setContentsMargins(0, 0, 0, 7)
532
+
533
+        self.WB_select = QPushButton('Select')
534
+        self.WB_select.setVisible(False)
535
+        self.WB_select.setFixedWidth(50)
536
+        self.WB_select.clicked.connect(lambda: cmds.select(self.selection))
537
+
538
+        self.WB_update = QPushButton('Update')
539
+        self.WB_update.setFixedWidth(50)
540
+        self.WB_update.clicked.connect(lambda:self.update_attributes(cmds.ls(sl=True)))
541
+
542
+        self.WV_search = Filter()
543
+        self.WV_search.textChanged.connect(self.filter)
544
+
545
+        self.WC_cases = QCheckBox('Case sensitive')
546
+        self.WC_cases.stateChanged.connect(self.filter)
547
+
548
+        self.WC_types = QCheckBox('Type filtering')
549
+
550
+        self.WL_attrtype = QComboBox()
551
+        self.WL_attrtype.setEnabled(False)
552
+
553
+        for i, ctx in enumerate(sorted(self.ctx_wide)):
554
+            self.WL_attrtype.addItem(ctx.title())
555
+            self.WL_attrtype.setItemIcon(i, self.ctx_icons[ctx])
556
+
557
+        L_attrtype = line(self.WC_types, self.WL_attrtype)
558
+
559
+        self.WC_types.stateChanged.connect(partial(self.update_attributes, self.selection))
560
+        self.WC_types.stateChanged.connect(self.WL_attrtype.setEnabled)
561
+        self.WL_attrtype.currentIndexChanged.connect(self.filter)
562
+
563
+        self.WC_liveu = QCheckBox('Live')
564
+        self.WC_liveu.stateChanged.connect(self.WB_update.setDisabled)
565
+        self.WC_liveu.stateChanged.connect(self.set_callback)
566
+
567
+        self.WC_histo = QCheckBox('Load history')
568
+        self.WC_histo.setChecked(True)
569
+        self.WC_histo.stateChanged.connect(partial(self.update_attributes, self.selection))
570
+
571
+        self.WC_child = QCheckBox('Children')
572
+        self.WC_child.stateChanged.connect(partial(self.update_attributes, self.selection))
573
+
574
+        options = group('Options', line(self.WC_cases, L_attrtype),
575
+                        line(self.WC_child, self.WC_histo, self.WC_liveu, self.WB_update))
576
+        options.layout().setSpacing(2)
577
+
578
+        self.WL_attributes = QTreeWidget()
579
+        self.WL_attributes.setStyleSheet('QTreeView {alternate-background-color: #1b1b1b;}')
580
+        self.WL_attributes.setAlternatingRowColors(True)
581
+        self.WL_attributes.setHeaderHidden(True)
582
+        self.WL_attributes.setRootIsDecorated(False)
583
+
584
+        self.objs_attr = set()
585
+        self.shps_attr = set()
586
+
587
+        self.W_EDI_float = FloatBox()
588
+        self.W_EDI_int = IntBox()
589
+        self.W_EDI_enum = QComboBox()
590
+        self.W_EDI_bool = QCheckBox()
591
+        self.W_EDI_str = QLineEdit()
592
+        self.W_EDI_d3 = Double3()
593
+        self.W_EDI_d4 = Double4()
594
+        self.W_EDI_color = ColorPicker()
595
+
596
+        # Final layout
597
+        L_title = line(self.WV_title, self.WB_select)
598
+        L_title.setStretch(0, 1)
599
+        L_main.addLayout(L_title)
600
+        L_main.setAlignment(Qt.AlignLeft)
601
+        L_main.addWidget(self.WV_search)
602
+        L_main.addWidget(options)
603
+        L_main.addWidget(self.WL_attributes)
604
+        L_edits = col(self.W_EDI_bool, self.W_EDI_int, self.W_EDI_float,
605
+                      self.W_EDI_enum, self.W_EDI_str, self.W_EDI_d3, self.W_EDI_d4,
606
+                      self.W_EDI_color)
607
+        L_edits.setContentsMargins(0, 8, 0, 0)
608
+        L_main.addLayout(L_edits)
609
+        L_main.setStretch(3, 1)
610
+        L_main.setSpacing(2)
611
+
612
+        self.appliers = {'float': self.applier.apply_float,
613
+                         'enum': self.applier.apply_enum,
614
+                         'bool': self.applier.apply_bool,
615
+                         'time': self.applier.apply_float,
616
+                         'byte': self.applier.apply_int,
617
+                         'angle': self.applier.apply_float,
618
+                         'string': self.applier.apply_str,
619
+                         'float3': self.applier.apply_d3,
620
+                         'float4': self.applier.apply_d4,
621
+                         'color': self.applier.apply_color}
622
+
623
+        self.setLayout(L_main)
624
+
625
+        # final settings
626
+        self.WL_attributes.itemSelectionChanged.connect(self.update_setter)
627
+        self.applier.unset_editors()
628
+
629
+    def closeEvent(self, *args, **kwargs):
630
+        self.set_callback(False)
631
+
632
+    def set_callback(self, state):
633
+        """
634
+        Toggle selection event callback
635
+        :param state: checkbox's state
636
+        :type  state: bool | int
637
+        """
638
+        if state and not self.callback:
639
+            self.callback = MEventMessage.addEventCallback('SelectionChanged', self.update_attributes)
640
+            self.update_attributes(cmds.ls(sl=True))
641
+
642
+        elif not state and self.callback:
643
+            MMessage.removeCallback(self.callback)
644
+            self.callback = None
645
+
646
+    @staticmethod
647
+    def format_title(nodes):
648
+        """
649
+        Extract the matching characters from a given nodes selection, if begin matches it will return "joint*" with a
650
+        wildcard when names don't match
651
+        :param nodes: objects' list
652
+        :type  nodes: list | tuple
653
+        :return: the formatted name with the corresponding characters
654
+        :rtype : str
655
+        """
656
+        res = None
657
+
658
+        if nodes:
659
+            # we get the first node as a reference
660
+            node = nodes[0]
661
+            # and compare with the other nodes
662
+            subs = [w for w in nodes if w != node]
663
+
664
+            l = 1
665
+            valid = True
666
+            # will continue until l (length) match the full name's length or until names don't match
667
+            while l < len(node) and valid:
668
+                for sub in subs:
669
+                    if not sub.startswith(node[:l]):
670
+                        valid = False
671
+                        break
672
+
673
+                else:
674
+                    l += 1
675
+
676
+            # if matching characters isn't long enough we only display the number of nodes selected
677
+            if l <= 3:
678
+                res = '%i objects' % len(nodes)
679
+
680
+            # otherwise showing matching pattern
681
+            elif l < len(node) or len(nodes) > 1:
682
+                res = node[:l - 1] + '* (%i objects)' % len(nodes)
683
+
684
+            else:
685
+                res = node
686
+
687
+        return res
688
+
689
+    @staticmethod
690
+    def get_history(node):
691
+        """
692
+        Extract history for the given node
693
+        :rtype: list
694
+        """
695
+        return cmds.listHistory(node, il=2, pdo=True) or []
696
+
697
+    @staticmethod
698
+    def get_shapes(node):
699
+        """
700
+        Extract shape(s) for the given node
701
+        :rtype: list
702
+        """
703
+        return cmds.listRelatives(node, s=True, ni=True, f=True)
704
+
705
+    def get_attributes_type(self, attrs):
706
+        """
707
+        For a given list of attributes of type Attribute, will loop through and fill the type parameter of the
708
+         attribute with the corresponding type, if type is invalid or not handled, it'll remove it
709
+        :param attrs: attributes' list
710
+        :type  attrs: [MassAttribute_UI.Attribute]
711
+        :return: cleaned and filled attributes' list
712
+        :rtype: [MassAttribute_UI.Attribute]
713
+        """
714
+        attrs = list(attrs)
715
+        # first we sort the attributes' list
716
+        attrs.sort()
717
+
718
+        # then we try to extract the attribute's type
719
+        for i, attr in enumerate(attrs):
720
+            try:
721
+                if attr.attr in self.solved:
722
+                    attr.type = self.solved[attr.attr]
723
+                    raise RuntimeError
724
+                tpe = cmds.getAttr(attr.path, typ=True)
725
+                assert tpe
726
+                attr.type = tpe
727
+                self.solved[attr.attr] = tpe
728
+            except (AssertionError, ValueError, RuntimeError):
729
+                pass
730
+
731
+        # defining a to-remove list
732
+        rm_list = set()
733
+
734
+        layers = {'3': 'XYZ', '4': 'XYZW'}
735
+        for i, attr in enumerate(attrs):
736
+            if i in rm_list:
737
+                continue
738
+
739
+            # we handle some special cases here, if ever the attribute list contains RGB and separate R, G and B we
740
+            # assume it's a color, if it's a double3 or float3 and we find the corresponding XYZ, we remove then to
741
+            # avoid duplicates
742
+
743
+            if attr.endswith('RGB'):
744
+                if '%sR' % attr[:-3] in attrs:
745
+                    attr.type = 'color'
746
+                    for chan in 'RGB':
747
+                        rm_list.add(attrs.index('%s%s' % (attr[:-3], chan)))
748
+
749
+            # if the attribute's type isn't in the list, we remove
750
+            elif attr.type not in MassAttribute_UI.ctx_icons:
751
+                rm_list.add(i)
752
+
753
+            elif attr.endswith('R'):
754
+                if '%sG' % attr[:-1] in attrs and attr[:-1] in attrs:
755
+                    attr.type = 'color'
756
+                    for chan in 'RGB':
757
+                        rm_list.add(attrs.index('%s%s' % (attr[:-1], chan)))
758
+
759
+            elif attr.type in ('double3', 'double4', 'float3', 'float4'):
760
+                if '%sX' % attr in attrs:
761
+                    for chan in layers[attr.type[-1]]:
762
+                        rm_list.add(attrs.index('%s%s' % (attr, chan)))
763
+
764
+        # finally cleaning the list
765
+        for i in sorted(rm_list, reverse=True):
766
+            attrs.pop(i)
767
+
768
+        return attrs
769
+
770
+    def apply_value(self, value):
771
+        """
772
+        When the value is modified in the UI, we forward the given value and applies to the object's
773
+        :param value: attribute's value, mixed type
774
+        :type  value: mixed
775
+        """
776
+        # We get the only selected object in list and get it's super type (Shape, History or Object) and
777
+        # type (float, int, string)
778
+        item = self.WL_attributes.selectedItems()[0]
779
+        attr = item.attribute
780
+        shape = attr.super_type == Shape
781
+        histo = attr.super_type == History
782
+        tpe = item.attribute.type
783
+
784
+        # eq dict for each context
785
+        value = {'bool': bool,
786
+                 'int': int,
787
+                 'float': float,
788
+                 'enum': int,
789
+                 'str': str,
790
+                 'd3': list,
791
+                 'd4': list,
792
+                 'color': list}[self.ctx](value)
793
+
794
+        # converting the selection into a set
795
+        cmds.undoInfo(openChunk=True)
796
+        targets = set(self.selection)
797
+
798
+        # we propagate to children if 'Children' checkbox is on
799
+        if self.WC_child.isChecked():
800
+            for obj in list(targets):
801
+                targets |= set(cmds.listRelatives(obj, ad=True))
802
+
803
+        # if the target attribute is on the history, we add all selection's history to the list
804
+        if histo:
805
+            for obj in list(targets):
806
+                targets.remove(obj)
807
+                targets |= set(self.get_history(obj))
808
+
809
+        # then we loop through target objects
810
+        for obj in targets:
811
+            # if the target is on the shape we get object's shape
812
+            if shape and not histo:
813
+                shapes = self.get_shapes(obj)
814
+
815
+                if obj in shapes:
816
+                    continue
817
+                else:
818
+                    obj = shapes[0]
819
+
820
+            # then we try to apply depending on attribute's type
821
+            try:
822
+                correct_path = attr.path.replace(attr.obj, obj)
823
+
824
+                if tpe == 'string':
825
+                    cmds.setAttr(correct_path, value, type='string')
826
+
827
+                elif tpe in ('double3', 'double4', 'float3', 'float4', 'color'):
828
+                    cmds.setAttr(correct_path, *value, type='double%d' % len(value))
829
+
830
+                else:
831
+                    cmds.setAttr(correct_path, value)
832
+
833
+            except RuntimeError:
834
+                pass
835
+
836
+        cmds.undoInfo(closeChunk=True)
837
+
838
+    def update_setter(self):
839
+        """
840
+        When the list's selection changes we update the applier widget
841
+        """
842
+        item = self.WL_attributes.selectedItems()
843
+        # abort if no item is selected
844
+        if not len(item):
845
+            return
846
+
847
+        # getting attribute's parameter
848
+        attr = item[0].attribute
849
+
850
+        if len(self.selection):
851
+            try:
852
+                # looping until we find a context having the current attribute's type
853
+                for applier in self.ctx_wide:
854
+                    if attr.type in self.ctx_wide[applier]:
855
+                        break
856
+                # then we apply for the given path (obj.attribute)
857
+                self.appliers[applier](attr.path)
858
+
859
+                # and connecting event to the self.apply_value function
860
+                self.applier.widget_event(self.ctx).connect(self.apply_value)
861
+
862
+            # otherwise selection or type is invalid
863
+            except IndexError:
864
+                self.ctx = None
865
+
866
+    def update_attributes(self, selection=None, *args):
867
+        """
868
+        Update the attributes for the given selection, looping through objects' attributes, finding attr in common
869
+        between all objects then cleaning the lists, doing the same for shapes and / or histories
870
+        :param selection: object's selection
871
+        """
872
+        # redefining lists as set to intersect union etc
873
+        self.objs_attr = set()
874
+        self.shps_attr = set()
875
+
876
+        # pre init
877
+        self.WL_attributes.clear()
878
+        self.applier.unset_editors()
879
+
880
+        self.selection = selection or (cmds.ls(sl=True) if self.WC_liveu.isChecked() else self.selection)
881
+
882
+        self.WV_title.setText(self.format_title(self.selection))
883
+        self.WV_title.setVisible(bool(len(self.selection)))
884
+        self.WB_select.setVisible(bool(len(self.selection)))
885
+
886
+        if not len(self.selection):
887
+            return
888
+
889
+        def get_usable_attrs(obj, super_type):
890
+            """
891
+            Small internal function to get a compatible attributes' list for the given object and assign the given
892
+            super_type to it (Object, Shape or History)
893
+            :param        obj: object's name
894
+            :type         obj: str
895
+            :param super_type: attribute's main type
896
+            :type  super_type: Object | Shape | History
897
+            :return:
898
+            """
899
+            return set([MassAttribute_UI.Attribute('%s.%s' % (obj, attr), super_type) for attr in
900
+                        cmds.listAttr(obj, se=True, ro=False, m=True, w=True)])
901
+
902
+        if len(self.selection):
903
+            self.objs_attr = get_usable_attrs(self.selection[0], Object)
904
+
905
+            # if we also want the object's history we add it to the initial set
906
+            if self.WC_histo.isChecked():
907
+                for histo in self.get_history(self.selection[0]):
908
+                    self.objs_attr |= get_usable_attrs(histo, History)
909
+
910
+            # filling the shape's set
911
+            for shape in (self.get_shapes(self.selection[0]) or []):
912
+                self.shps_attr |= get_usable_attrs(shape, Shape)
913
+
914
+            # if selection's length bigger than one we compare by intersection with the other sets
915
+            if len(self.selection) > 1:
916
+                for obj in self.selection:
917
+                    sub_attr = get_usable_attrs(obj, Object)
918
+
919
+                    if self.WC_histo.isChecked():
920
+                        for histo in self.get_history(obj):
921
+                            sub_attr |= get_usable_attrs(histo, History)
922
+
923
+                    self.objs_attr.intersection_update(sub_attr)
924
+
925
+                    for shape in (self.get_shapes(self.selection[0]) or []):
926
+                        self.shps_attr.intersection_update(get_usable_attrs(shape, Shape))
927
+
928
+            # finally getting all intersecting attributes' types
929
+            self.objs_attr = self.get_attributes_type(self.objs_attr)
930
+            self.shps_attr = self.get_attributes_type(self.shps_attr)
931
+
932
+        # and filtering the list
933
+        self.filter()
934
+
935
+    def add_set(self, iterable, title=None):
936
+        """
937
+        Adding the given iterable to the list with a first Separator object with given title
938
+        :param iterable: list of item's attributes
939
+        :param    title: Separator's name
940
+        """
941
+        if len(iterable):
942
+            # if title is given we first add a Separator item to indicate coming list title
943
+            if title:
944
+                self.WL_attributes.addTopLevelItem(QTreeWidget_Separator(title))
945
+
946
+            items = []
947
+            for attr in sorted(iterable):
948
+                item = QTreeWidgetItem([attr])
949
+                # assigning the attribute itself inside a custom parameter
950
+                item.attribute = attr
951
+                items.append(item)
952
+
953
+            # finally adding all the items to the list
954
+            self.WL_attributes.addTopLevelItems(items)
955
+
956
+    def filter(self):
957
+        """
958
+        Filter the list with UI's parameters, such as name or type filtering, etc
959
+        """
960
+        # pre cleaning
961
+        self.WL_attributes.clear()
962
+
963
+        # using regex compile to avoid re execution over many attributes
964
+        mask = self.WV_search.text()
965
+        case = 0 if self.WC_cases.isChecked() else re.IGNORECASE
966
+        re_start = re.compile(r'^%s.*?' % mask, case)
967
+        re_cont = re.compile(r'.*?%s.*?' % mask, case)
968
+
969
+        # getting the four different lists
970
+        obj_start = set([at for at in self.objs_attr if re_start.search(at)])
971
+        shp_start = set([at for at in self.shps_attr if re_start.search(at)])
972
+
973
+        # if type filtering is one we only extract the wanted attribute's type
974
+        if self.WC_types.isChecked():
975
+            obj_start = set([at for at in obj_start if
976
+                             at.type in self.ctx_wide[self.WL_attrtype.currentText().lower()]])
977
+            shp_start = set([at for at in shp_start if
978
+                             at.type in self.ctx_wide[self.WL_attrtype.currentText().lower()]])
979
+
980
+        # finally adding the current sets if there is a mask we add the also the containing matches
981
+        if mask:
982
+            # getting contains filtering and type containers filtering
983
+            obj_contains = obj_start.symmetric_difference(set([at for at in self.objs_attr if re_cont.search(at)]))
984
+            shp_contains = shp_start.symmetric_difference(set([at for at in self.shps_attr if re_cont.search(at)]))
985
+            if self.WC_types.isChecked():
986
+                obj_contains = set([at for at in obj_contains if
987
+                                    at.type in self.ctx_wide[self.WL_attrtype.currentText().lower()]])
988
+                shp_contains = set([at for at in shp_contains if
989
+                                    at.type in self.ctx_wide[self.WL_attrtype.currentText().lower()]])
990
+
991
+            # adding the sets
992
+            self.add_set(obj_start, 'Obj attributes starting with')
993
+            self.add_set(obj_contains, 'Obj attributes containing')
994
+            self.add_set(shp_start, 'Shape attributes starting with')
995
+            self.add_set(shp_contains, 'Shape attributes containing')
996
+
997
+        else:
998
+            self.add_set(obj_start, 'Object\'s attributes')
999
+            self.add_set(shp_start, 'Shape\'s attributes')
1000
+
1001
+        # and we select the first one if ever there is something in the list
1002
+        if self.WL_attributes.topLevelItemCount():
1003
+            self.WL_attributes.setItemSelected(self.WL_attributes.topLevelItem(1), True)
1004
+
1005
+
1006
+def Display_Massive_Toggle():
1007
+    """ Display function """
1008
+    Mass_Win = MassAttribute_UI(get_maya_window())
1009
+    Mass_Win.show()
1010
+    return Mass_Win