-
-
Notifications
You must be signed in to change notification settings - Fork 25
/
Copy pathshapekeytransfer.py
649 lines (551 loc) · 25.7 KB
/
shapekeytransfer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
#----------------------------------------------------------
# File shapekeytransfer.py
#----------------------------------------------------------
#
# ShapeKeyTransfer - Copyright (C) 2018 Ajit Christopher D'Monte
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# ----------------------------------------------------------
import bpy
import bmesh
from mathutils import Vector
from bpy.types import (Operator,
UIList,
Panel)
# __reload_order_index__ = 1
# Class which handles shape key transfers
# ----------------------------------------------------------
class ShapeKeyTransfer:
"""
Class with required methods to transfer shape keys between 2 meshes.
"""
def __init__(self):
# Increment radius used to select nearby vertices in the source mesh for copying its positions to the destination mesh vertex
self.increment_radius = .05
self.number_of_increments = 20
# set total vertices incase you want to run for less number of vertices also set specify_end_vertex to True
# set current_vertex index incase you want to continue from another index
# use_one_vertex will transfer the weight of the closest vertex within the selection sphere
self.current_vertex_index = 0
self.total_vertices = 0
self.specify_end_vertex = False # not yet implemented
self.use_one_vertex = True
# shape keys to ignore
self.default_excluded_keys = self.excluded_shape_keys = ['Basis', 'Expressions_IDHumans_max']
# internal references used by this class
self.dest_mesh = None
self.src_mesh = None
self.src_mwi = None
self.dest_shape_key_index = 0
self.src_shape_key_index = 0
self.do_once_per_vertex = False
self.current_vertex = None
self.src_chosen_vertices = []
self.message = ""
self.skip_vertices_with_no_pair = False
# select required vertices within a radius and return array of indices
def select_vertices(self, center, radius):
src_chosen_vertices = []
closest_vertex_index = -1
radius_vec = center + Vector((0, 0, radius))
# put selection sphere in local coords.
lco = self.src_mwi @ center
r = self.src_mwi @ (radius_vec) - lco
closest_length = r.length
# select verts within radius
for index, v in enumerate(self.src_mesh.data.shape_keys.key_blocks[0].data):
is_selected = (v.co - lco).length <= r.length
if(is_selected):
src_chosen_vertices.append(index)
if(self.use_one_vertex):
if((v.co - lco).length <= closest_length):
closest_length = (v.co - lco).length
closest_vertex_index = index
# update closest vertex
if(self.use_one_vertex):
src_chosen_vertices = []
if(closest_vertex_index > - 1):
src_chosen_vertices.append(closest_vertex_index)
return src_chosen_vertices
# this select function initially starts (if level=0) by matching a point in same space as the source mesh and if it cant find similar positioned point we increment search radius
def select_required_verts(self, vert, rad, level=0):
verts = []
if(level > self.number_of_increments):
return verts
verts = self.select_vertices(vert, rad)
if(len(verts) == 0):
return self.select_required_verts(vert, rad + self.increment_radius, level + 1)
else:
return verts
# set the new vertex position on the shape key
def set_vertex_position(self, v_pos):
self.dest_mesh.data.shape_keys.key_blocks[self.dest_shape_key_index].data[self.current_vertex_index].co = v_pos
# update 1 vertex of destination mesh
def update_vertex(self):
if(self.current_vertex_index >= self.total_vertices ):
return False
if(self.do_once_per_vertex):
#mathutils now uses the PEP 465 binary operator for multiplying matrices change * to @
self.current_vertex = self.dest_mesh.matrix_world @ self.dest_mesh.data.shape_keys.key_blocks[0].data[self.current_vertex_index].co
self.src_chosen_vertices = self.select_required_verts(self.current_vertex,0)
self.do_once_per_vertex = False
if(len(self.src_chosen_vertices) == 0):
self.message = ("Failed to find surrounding vertices | Try increasing increment radius | vertex index " + str(self.current_vertex_index) + " at shape key index " + str(self.src_shape_key_index))
self.current_vertex_index += 1
if(not self.skip_vertices_with_no_pair):
return True
else:
return False
result_position = Vector()
for v in self.src_chosen_vertices:
result_position += self.src_mesh.data.shape_keys.key_blocks[0].data[v].co
result_position /= len(self.src_chosen_vertices)
result_position2 = Vector()
for v in self.src_chosen_vertices:
result_position2 += self.src_mesh.data.shape_keys.key_blocks[self.src_shape_key_index].data[v].co
result_position2 /= len(self.src_chosen_vertices)
result = result_position2 - result_position + self.current_vertex
self.set_vertex_position(result)
return False
# store shapekey index
def update_global_shapekey_indices(self, p_key_name):
for index, sk in enumerate(self.dest_mesh.data.shape_keys.key_blocks):
if sk.name == p_key_name:
self.dest_shape_key_index = index
for index, sk in enumerate(self.src_mesh.data.shape_keys.key_blocks):
if sk.name == p_key_name:
self.src_shape_key_index = index
def get_parent(self, mesh):
for ob in bpy.data.objects:
if (ob.data) == mesh:
return ob
return None
def transfer_shape_keys(self, src, dest, use_only_excluded_shape_keys = False):
self.src_mesh = self.get_parent(src)
self.dest_mesh = self.get_parent(dest)
self.src_mwi = self.src_mesh.matrix_world.inverted()
self.current_vertex_index = 0
local_shape_key_list = [] # used only when considering excluded shape keys
if(not(self.src_mesh and self.dest_mesh)):
self.message = "The meshes are not valid!"
return True
if(self.specify_end_vertex == False):
self.total_vertices = len(self.dest_mesh.data.vertices)
if(not hasattr(self.src_mesh.data.shape_keys, "key_blocks")):
self.message = "There are no Shape Keys in the source mesh!"
return True
# Check if dest_mesh has any shape key if not create one
if(not hasattr(self.dest_mesh.data.shape_keys, "key_blocks")):
self.dest_mesh.shape_key_add(name="Basis")
# add missing shape keys to dest_mesh
for src_shape_key_iter in self.src_mesh.data.shape_keys.key_blocks:
valid_shape_key = False
if(use_only_excluded_shape_keys and (src_shape_key_iter.name in self.excluded_shape_keys)):
local_shape_key_list.append(src_shape_key_iter.name)
if((not use_only_excluded_shape_keys) and (src_shape_key_iter.name in self.excluded_shape_keys)):
continue
if(use_only_excluded_shape_keys and (not (src_shape_key_iter.name in self.excluded_shape_keys))):
continue
for dest_shape_key_iter in self.dest_mesh.data.shape_keys.key_blocks:
if(src_shape_key_iter.name == dest_shape_key_iter.name):
valid_shape_key = True
if(not valid_shape_key):
self.dest_mesh.shape_key_add(name=src_shape_key_iter.name)
# all vertices in destination mesh
while(self.current_vertex_index < self.total_vertices):
self.do_once_per_vertex = True
print("Vertex: " + str(self.current_vertex_index + 1) + "/" + str(self.total_vertices))
if(use_only_excluded_shape_keys):
for key_name in local_shape_key_list:
self.update_global_shapekey_indices(key_name)
if(self.update_vertex()):
return True
else:
# Iterate all shape keys
for shape_key_iter in self.src_mesh.data.shape_keys.key_blocks:
key_name = shape_key_iter.name
# exclude shapekeys not needed
if(not (key_name in self.excluded_shape_keys)):
self.update_global_shapekey_indices(key_name)
if(self.update_vertex()):
return True
self.current_vertex_index += 1
self.message = "Transferred Shape Keys successfully!"
return False
# get the default excluded shape keys
def get_default_excluded_keys(self):
return list(set(self.default_excluded_keys) - set(["Basis"]))
# Update the excluded shape keys list
def update_shape_keys_list(self, excluded_keys):
keys = []
for item in excluded_keys:
keys.append(item.name)
keys.append("Basis")
# remove duplicates
keys = list(set(keys))
self.excluded_shape_keys = keys
#get shape keys of a mesh
def get_shape_keys_mesh(self, mesh):
obj = self.get_parent(mesh)
keys = []
if(not hasattr(obj.data.shape_keys, "key_blocks")):
self.message = "There are no Shape Keys in the mesh!"
return True
for shape_key_iter in obj.data.shape_keys.key_blocks:
keys.append(shape_key_iter.name)
self.message = keys
return False
# Instantiate the class
# ----------------------------------------------------------
SKT = ShapeKeyTransfer()
# Helper function to check if a valid selection is made
# ----------------------------------------------------------
def can_transfer_keys(context):
"""Checks if selected source and destination meshes are valid"""
skt = context.scene.shapekeytransfer
if(skt.src_mesh and skt.dest_mesh):
if(skt.src_mesh == skt.dest_mesh):
return False
else:
return True
else:
return False
# Copy all Shape Key names to Clipboard Button
# ----------------------------------------------------------
class SKT_OT_copyKeyNames(Operator):
"""Copy all Shape Key names to Clipboard"""
bl_idname = "skt.copy_key_names"
bl_label = "Copy Shape Key Names"
bl_description = "Copy Shape Key Names from Source Mesh to Clipboard"
bl_options = {'INTERNAL'}
@classmethod
def poll(cls, context):
skt = context.scene.shapekeytransfer
return (skt.src_mesh is not None)
def execute(self, context):
global SKT
skt = context.scene.shapekeytransfer
if(skt.src_mesh):
if(SKT.get_shape_keys_mesh(skt.src_mesh)):
self.report({'INFO'}, SKT.message)
else:
keys = SKT.message
temp_str = ""
for key in keys:
if(key == "Basis"):
continue
temp_str += key + "\n"
context.window_manager.clipboard = temp_str
self.report({'INFO'}, "Copied to Clipboard")
else:
self.report({'INFO'}, "Invalid Source Mesh")
return{'FINISHED'}
# Copy all Shape Key names from clipboard Button
# ----------------------------------------------------------
class SKT_OT_insertKeyNames(Operator):
"""Copy all Shape Key Names from the Clipboard"""
bl_idname = "skt.insert_key_names"
bl_label = "Insert Shape Key Names"
bl_description = "Insert Shape Key Names from Clipboard (Each name per Row)"
bl_options = {'INTERNAL'}
def execute(self, context):
scn = context.scene
for key in context.window_manager.clipboard.split("\n"):
if(len(key)):
item = scn.customshapekeylist.add()
item.name = key
item.obj_type = "STRING"
item.obj_id = len(scn.customshapekeylist)
scn.shapekeytransfer_list_index = len(scn.customshapekeylist)-1
self.report({'INFO'}, "Added shape key names from Clipboard")
return{'FINISHED'}
# Transfer Shape Keys Button (Operator)
# ----------------------------------------------------------
class SKT_OT_transferShapeKeys(Operator):
"""Transfers Shape Keys to Selected Mesh"""
bl_idname = "skt.transfer_shape_keys"
bl_label = "Transfer Shape Keys"
bl_description = "The two meshes should overlap each other or positioned pretty close"
bl_context = 'objectmode'
bl_options = {'REGISTER', 'INTERNAL','UNDO'}
@classmethod
def poll(cls, context):
return can_transfer_keys(context)
def execute(self, context):
global SKT
skt = context.scene.shapekeytransfer
SKT.increment_radius = skt.increment_radius
SKT.use_one_vertex = skt.use_one_vertex
SKT.skip_vertices_with_no_pair = skt.skip_unpaired_vertices
SKT.number_of_increments = skt.number_of_increments
SKT.update_shape_keys_list(context.scene.customshapekeylist)
result = SKT.transfer_shape_keys(skt.src_mesh, skt.dest_mesh)
if(result):
self.report({'ERROR'}, SKT.message)
else:
self.report({'INFO'}, SKT.message)
return {'FINISHED'}
def draw(self, context):
layout = self.layout
skt = context.scene.shapekeytransfer
col = layout.column()
col.label(text="Vertex influence:")
col.prop(skt, "increment_radius")
col.prop(skt, "use_one_vertex")
col.prop(skt, "skip_unpaired_vertices")
col.prop(skt, "number_of_increments")
# Transfer Shape Keys in excluded shape keys list Button (Operator)
# ----------------------------------------------------------
class SKT_OT_transferExcludedShapeKeys(Operator):
"""Transfers Shape Keys from excluded Shape key list"""
bl_idname = "skt.transfer_excluded_shape_keys"
bl_label = "Transfer Excluded Shape Keys Only"
bl_description = "The two meshes should overlap each other or positioned pretty close"
bl_context = 'objectmode'
bl_options = {'REGISTER', 'INTERNAL','UNDO'}
@classmethod
def poll(cls, context):
return can_transfer_keys(context)
def execute(self, context):
global SKT
skt = context.scene.shapekeytransfer
SKT.increment_radius = skt.increment_radius
SKT.use_one_vertex = skt.use_one_vertex
SKT.skip_vertices_with_no_pair = skt.skip_unpaired_vertices
SKT.number_of_increments = skt.number_of_increments
SKT.update_shape_keys_list(context.scene.customshapekeylist)
result = SKT.transfer_shape_keys(skt.src_mesh, skt.dest_mesh, True)
if(result):
self.report({'ERROR'}, SKT.message)
else:
self.report({'INFO'}, SKT.message)
return {'FINISHED'}
def draw(self, context):
skt = context.scene.shapekeytransfer
layout = self.layout
col = layout.column()
col.label(text="Vertex influence:")
col.prop(skt, "increment_radius")
col.prop(skt, "use_one_vertex")
col.prop(skt, "skip_unpaired_vertices")
col.prop(skt, "number_of_increments")
# Remove all Shape Keys in source mesh Button (Operator)
# ----------------------------------------------------------
class SKT_OT_removeShapeKeys(Operator):
"""Remove all Shape Keys of Source Mesh"""
bl_idname = "skt.remove_src_shape_keys"
bl_label = "Remove Shape Keys of Source"
bl_description = "Remove all Shape Keys of Source Mesh"
bl_context = 'objectmode'
bl_options = {'REGISTER', 'INTERNAL','UNDO'}
@classmethod
def poll(cls, context):
skt = context.scene.shapekeytransfer
return (skt.src_mesh is not None)
def execute(self, context):
global SKT
skt = context.scene.shapekeytransfer
if(skt.src_mesh):
ob = SKT.get_parent(skt.src_mesh)
if(ob.data.shape_keys):
basis = None
for x in ob.data.shape_keys.key_blocks:
if(basis):
ob.shape_key_remove(x)
else:
basis = x
ob.shape_key_remove(basis)
self.report({'INFO'}, "Removed all shape keys in source mesh!")
else:
self.report({'ERROR'}, "Select a valid source mesh!")
return {'FINISHED'}
# Manage customshapekeylist items (Operator)
# ----------------------------------------------------------
class SKT_OT_actions(Operator):
"""Move items up and down, add and remove"""
bl_idname = "customshapekeylist.list_action"
bl_label = "List Actions"
bl_description = "Move items up and down, add and remove"
bl_options = {'REGISTER'}
action : bpy.props.EnumProperty(
items=(
('UP', "Up", ""),
('DOWN', "Down", ""),
('REMOVE', "Remove", ""),
('ADD', "Add", ""),
('DEFAULT', "Default", "")))
def invoke(self, context, event):
scn = context.scene
idx = scn.shapekeytransfer_list_index
try:
item = scn.customshapekeylist[idx]
except IndexError:
pass
else:
if self.action == 'DOWN' and idx < len(scn.customshapekeylist) - 1:
item_next = scn.customshapekeylist[idx+1].name
scn.customshapekeylist.move(idx, idx+1)
scn.shapekeytransfer_list_index += 1
info = 'Item "%s" moved to position %d' % (item.name, scn.shapekeytransfer_list_index + 1)
self.report({'INFO'}, info)
elif self.action == 'UP' and idx >= 1:
item_prev = scn.customshapekeylist[idx-1].name
scn.customshapekeylist.move(idx, idx-1)
scn.shapekeytransfer_list_index -= 1
info = 'Item "%s" moved to position %d' % (item.name, scn.shapekeytransfer_list_index + 1)
self.report({'INFO'}, info)
elif self.action == 'REMOVE':
info = 'Item "%s" removed from list' % (scn.customshapekeylist[idx].name)
scn.shapekeytransfer_list_index -= 1
scn.customshapekeylist.remove(idx)
self.report({'INFO'}, info)
if self.action == 'ADD':
scn = context.scene
item = scn.customshapekeylist.add()
item.name = "key"
item.obj_type = "STRING"
item.obj_id = len(scn.customshapekeylist)
scn.shapekeytransfer_list_index = len(scn.customshapekeylist)-1
info = '"%s" added to list' % (item.name)
self.report({'INFO'}, info)
elif self.action == 'DEFAULT':
for key in SKT.get_default_excluded_keys():
item = scn.customshapekeylist.add()
item.name = key
item.obj_type = "STRING"
item.obj_id = len(scn.customshapekeylist)
scn.shapekeytransfer_list_index = len(scn.customshapekeylist)-1
info = '"%s" added to list' % (item.name)
self.report({'INFO'}, info)
return {"FINISHED"}
# Clear customshapekeylist items (Operator)
# ----------------------------------------------------------
class SKT_OT_clearList(Operator):
"""Clear all items of the list"""
bl_idname = "customshapekeylist.clear_list"
bl_label = "Clear List"
bl_description = "Clear all items of the list"
bl_options = {'INTERNAL'}
@classmethod
def poll(cls, context):
return bool(context.scene.customshapekeylist)
def invoke(self, context, event):
return context.window_manager.invoke_confirm(self, event)
def execute(self, context):
if bool(context.scene.customshapekeylist):
context.scene.customshapekeylist.clear()
self.report({'INFO'}, "All items removed")
else:
self.report({'INFO'}, "Nothing to remove")
return{'FINISHED'}
# Remove duplicates among customshapekeylist items (Operator)
# ----------------------------------------------------------
class SKT_OT_removeDuplicates(Operator):
"""Remove all duplicates in the list"""
bl_idname = "customshapekeylist.remove_duplicates"
bl_label = "Remove Doubles"
bl_description = "Remove all Duplicates in the List"
bl_options = {'INTERNAL'}
def find_duplicates(self, context):
"""find all duplicates by name"""
name_lookup = {}
for c, i in enumerate(context.scene.customshapekeylist):
name_lookup.setdefault(i.name, []).append(c)
duplicates = set()
for name, indices in name_lookup.items():
for i in indices[1:]:
duplicates.add(i)
return sorted(list(duplicates))
@classmethod
def poll(cls, context):
return bool(context.scene.customshapekeylist)
def execute(self, context):
scn = context.scene
removed_items = []
# Reverse the list before removing the items
for i in self.find_duplicates(context)[::-1]:
scn.customshapekeylist.remove(i)
removed_items.append(i)
if removed_items:
scn.shapekeytransfer_list_index = len(scn.customshapekeylist)-1
info = ', '.join(map(str, removed_items))
self.report({'INFO'}, "Removed indices: %s" % (info))
else:
self.report({'INFO'}, "No duplicates")
return{'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_confirm(self, event)
# customshapekeylist items (UIList)
# ----------------------------------------------------------
class SKT_UL_items(UIList):
"""Item in customshapekeylist"""
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
#split = layout.split(0.3)
#split.label("Index: %d" % (index))
#custom_icon = "OUTLINER_OB_%s" % item.obj_type
#split.prop(item, "name", text="", emboss=False, translate=False, icon=custom_icon)
#split.label(item.name, icon=custom_icon) # avoids renaming the item by accident
layout.prop(item, "name", text="", emboss=False, icon_value=icon)
def invoke(self, context, event):
pass
# Main addon panel (Panel)
# ----------------------------------------------------------
class SKT_PT_view3D(Panel):
"""Shape Key Tools Panel layout"""
bl_label = "Shape Key Transfer"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_context = 'objectmode'
bl_category = "Tools"
@classmethod
def poll(self,context):
return context.object is not None
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
scn = context.scene
skt = scn.shapekeytransfer
'''
if not can_transfer_keys(context):
layout.label(text="Set required meshes", icon='INFO')
'''
layout.prop(skt, "src_mesh", text="Source Mesh")
layout.prop(skt, "dest_mesh", text="Destination Mesh")
layout.separator()
layout.operator(SKT_OT_transferShapeKeys.bl_idname, icon='ARROW_LEFTRIGHT')
layout.operator(SKT_OT_transferExcludedShapeKeys.bl_idname, icon='KEYINGSET')
layout.separator()
layout.operator(SKT_OT_removeShapeKeys.bl_idname, icon='CANCEL')
layout.separator()
layout.label(text="Excluded Shape Keys")
rows = 5
row = layout.row()
row.template_list("SKT_UL_items", "", scn, "customshapekeylist", scn, "shapekeytransfer_list_index", rows=rows)
col = row.column(align=True)
col.operator(SKT_OT_actions.bl_idname, icon='PLUS', text="").action = 'ADD'
col.operator(SKT_OT_actions.bl_idname, icon='REMOVE', text="").action = 'REMOVE'
col.separator()
col.operator(SKT_OT_actions.bl_idname, icon='TRIA_UP', text="").action = 'UP'
col.operator(SKT_OT_actions.bl_idname, icon='TRIA_DOWN', text="").action = 'DOWN'
col.operator(SKT_OT_actions.bl_idname, icon='RECOVER_LAST', text="").action = 'DEFAULT'
col = layout.column(align=True)
row = col.row(align=True)
row.operator(SKT_OT_removeDuplicates.bl_idname, icon="FORCE_VORTEX")
row.operator(SKT_OT_clearList.bl_idname, icon="X")
col = col.column(align=True)
col.operator(SKT_OT_copyKeyNames.bl_idname, icon="COPYDOWN")
col.operator(SKT_OT_insertKeyNames.bl_idname, icon="IMPORT")
layout.separator()