# 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
# MERCHANTIBILITY 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/>.

## based on GPclipboard 1.3.2 (just stripped addon prefs)

bl_info = {
    "name": "GP clipboard",
    "description": "Copy/Cut/Paste Grease Pencil strokes to/from OS clipboard across layers and blends",
    "author": "Samuel Bernou",
    "version": (1, 3, 3),
    "blender": (2, 83, 0),
    "location": "View3D > Toolbar > Gpencil > GP clipboard",
    "warning": "",
    "doc_url": "https://github.com/Pullusb/GP_clipboard",
    "category": "Object" }

import bpy
import os
import mathutils
from mathutils import Vector
import json
from time import time
from operator import itemgetter
from itertools import groupby
# from pprint import pprint

def convertAttr(Attr):
    '''Convert given value to a Json serializable format'''
    if isinstance(Attr, (mathutils.Vector,mathutils.Color)):
        return Attr[:]
    elif isinstance(Attr, mathutils.Matrix):
        return [v[:] for v in Attr]
    elif isinstance(Attr,bpy.types.bpy_prop_array):
        return [Attr[i] for i in range(0,len(Attr))]
    else:
        return(Attr)

def getMatrix (layer) :
    matrix = mathutils.Matrix.Identity(4)

    if layer.is_parented:
        if layer.parent_type == 'BONE':
            object = layer.parent
            bone = object.pose.bones[layer.parent_bone]
            matrix = bone.matrix @ object.matrix_world
            matrix = matrix.copy() @ layer.matrix_inverse
        else :
            matrix = layer.parent.matrix_world @ layer.matrix_inverse

    return matrix.copy()

default_pt_uv_fill = Vector((0.5, 0.5))

def dump_gp_point(p, l, obj,
    pressure=True, strength=True, vertex_color=True, uv_fill=True, uv_factor=True, uv_rotation=True):
    '''add properties of a given points to a dic and return it'''
    pdic = {}
    #point_attr_list = ('co', 'pressure', 'select', 'strength') #select#'rna_type'
    #for att in point_attr_list:
    #    pdic[att] = convertAttr(getattr(p, att))
    if l.parent:
        mat = getMatrix(l)
        pdic['co'] = convertAttr(obj.matrix_world @ mat @ getattr(p,'co'))
    else:
        pdic['co'] = convertAttr(obj.matrix_world @ getattr(p,'co'))

    # pdic['select'] = convertAttr(getattr(p,'select')) # need selection ? 
    if pressure and p.pressure != 1.0:
        pdic['pressure'] = convertAttr(getattr(p,'pressure'))
    if strength and p.strength != 1.0:
        pdic['strength'] = convertAttr(getattr(p,'strength'))

    ## get vertex color (long...)
    if vertex_color and p.vertex_color[:] != (0.0, 0.0, 0.0, 0.0):
        pdic['vertex_color'] = convertAttr(p.vertex_color)
    
    ## UV attr (maybe uv fill is always (0.5,0.5) ? also exists at stroke level...)
    if uv_fill and p.uv_fill != default_pt_uv_fill:
        pdic['uv_fill'] = convertAttr(p.uv_fill)
    if uv_factor and p.uv_factor != 0.0:
        pdic['uv_factor'] = convertAttr(p.uv_factor)
    if uv_rotation and p.uv_rotation != 0.0:
        pdic['uv_rotation'] = convertAttr(p.uv_rotation)

    return pdic


def dump_gp_stroke_range(s, sid, l, obj,
    pressure=True, strength=True, vertex_color=True, uv_fill=True, uv_factor=True, uv_rotation=True):
    '''Get a grease pencil stroke and return a dic with attribute
    (points attribute being a dic of dics to store points and their attributes)
    '''

    sdic = {}
    stroke_attr_list = ('line_width',) #'select'#read-only: 'triangles'
    for att in stroke_attr_list:
        sdic[att] = getattr(s, att)
        
    ## Dump following these value only if they are non default
    if s.material_index != 0:
        sdic['material_index'] = s.material_index
    
    if getattr(s, 'draw_cyclic', None): # pre-2.92
        sdic['draw_cyclic'] = s.draw_cyclic
    
    if getattr(s, 'use_cyclic', None): # from 2.92
        sdic['use_cyclic'] = s.use_cyclic
    
    if s.uv_scale != 1.0:
        sdic['uv_scale'] = s.uv_scale
    
    if s.uv_rotation != 0.0:
        sdic['uv_rotation'] = s.uv_rotation
    
    if s.hardness != 1.0:
        sdic['hardness'] = s.hardness
    
    if s.uv_translation != Vector((0.0, 0.0)):
        sdic['uv_translation'] = convertAttr(s.uv_translation)

    if s.vertex_color_fill[:] != (0,0,0,0):
        sdic['vertex_color_fill'] = convertAttr(s.vertex_color_fill)

    points = []
    if sid is None: # no ids, just full points...
        for p in s.points:
            points.append(dump_gp_point(p,l,obj,
            pressure=pressure, strength=strength, vertex_color=vertex_color, uv_fill=uv_fill, uv_factor=uv_factor, uv_rotation=uv_rotation))
    else:
        for pid in sid:
            points.append(dump_gp_point(s.points[pid],l,obj,
            pressure=pressure, strength=strength, vertex_color=vertex_color, uv_fill=uv_fill, uv_factor=uv_factor, uv_rotation=uv_rotation))
    sdic['points'] = points
    return sdic



def copycut_strokes(layers=None, copy=True, keep_empty=True):
    '''
    copy all visibles selected strokes on active frame
    layers can be None, a single layer object or list of layer object as filter 
    if keep_empty is False the frame is deleted when all strokes are cutted
    '''
    t0 = time()

    ### must iterate in all layers ! (since all layers are selectable / visible !)
    obj = bpy.context.object
    gp = obj.data
    gpl = gp.layers
    # if not color:#get active color name
    #     color = gp.palettes.active.colors.active.name
    if not layers:
        # by default all visible layers
        layers = [l for l in gpl if not l.hide and not l.lock] # []
    if not isinstance(layers, list):
        # if a single layer object is send put in a list
        layers = [layers]

    stroke_list = [] # one stroke list for all layers.

    for l in layers:
        f = l.current_frame()

        if f: # active frame can be None
            if not copy:
                staylist = [] # init part of strokes that must survive on this layer

            for s in f.strokes:
                if s.select:
                    # separate in multiple stroke if parts of the strokes a selected.
                    sel = [i for i, p in enumerate(s.points) if p.select]
                    substrokes = [] # list of list containing isolated selection
                    for k, g in groupby(enumerate(sel), lambda x:x[0]-x[1]):# continuity stroke have same substract result between point index and enumerator
                        group = list(map(itemgetter(1), g))
                        substrokes.append(group)

                    for ss in substrokes:
                        if len(ss) > 1: # avoid copy isolated points
                            stroke_list.append(dump_gp_stroke_range(s,ss,l,obj))

                    # Cutting operation
                    if not copy:
                        maxindex = len(s.points)-1
                        if len(substrokes) == maxindex+1: # if only one substroke, then it's the full stroke
                            f.strokes.remove(s)
                        else:
                            neg = [i for i, p in enumerate(s.points) if not p.select]

                            staying = []
                            for k, g in groupby(enumerate(neg), lambda x:x[0]-x[1]):
                                group = list(map(itemgetter(1), g))
                                # extend group to avoid gap when cut, a bit dirty
                                if group[0] > 0:
                                    group.insert(0,group[0]-1)
                                if group[-1] < maxindex:
                                    group.append(group[-1]+1)
                                staying.append(group)

                            for ns in staying:
                                 if len(ns) > 1:
                                    staylist.append(dump_gp_stroke_range(s,ns,l,obj))
                            # make a negative list containing all last index


                    '''#full stroke version
                    # if s.colorname == color: #line for future filters
                    stroke_list.append(dump_gp_stroke(s,l))
                    #delete stroke on the fly
                    if not copy:
                        f.strokes.remove(s)
                    '''

            if not copy:
                # delete all selected strokes...
                for s in f.strokes:
                    if s.select:
                        f.strokes.remove(s)
                # ...recreate these uncutted ones
                #pprint(staylist)
                if staylist:
                    add_multiple_strokes(staylist, l)
                #for ns in staylist:#weirdly recreate the stroke twice !
                #    add_stroke(ns, f, l)

            #if nothing left on the frame choose to leave an empty frame or delete it (let previous frame appear)
            if not copy and not keep_empty:#
                if not len(f.strokes):
                    l.frames.remove(f)



    print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
    #print(stroke_list)
    return stroke_list


"""# Unused
def copy_all_strokes(layers=None):
    '''
    copy all stroke, not affected by selection on active frame
    layers can be None, a single layer object or list of layer object as filter 
    if keep_empty is False the frame is deleted when all strokes are cutted
    '''
    t0 = time()

    scene = bpy.context.scene
    obj = bpy.context.object
    gp = obj.data
    gpl = gp.layers

    if not layers:
        # by default all visible layers
        layers = [l for l in gpl if not l.hide and not l.lock]# include locked ?
    if not isinstance(layers, list):
        # if a single layer object is send put in a list
        layers = [layers]

    stroke_list = []# one stroke list for all layers.

    for l in layers:
        f = l.current_frame()

        if not f:
            continue# active frame can be None
 
        for s in f.strokes:
            ## full stroke version
            # if s.select:
            stroke_list.append(dump_gp_stroke_range(s, None, l, obj))

    print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
    #print(stroke_list)
    return stroke_list
"""

def copy_all_strokes_in_frame(frame=None, layers=None, obj=None,
    pressure=True, strength=True, vertex_color=True, uv_fill=True, uv_factor=True, uv_rotation=True):
    '''
    copy all stroke, not affected by selection on active frame
    layers can be None, a single layer object or list of layer object as filter 
    '''
    t0 = time()
    scene = bpy.context.scene
    obj = bpy.context.object
    gp = obj.data
    gpl = gp.layers
    
    if not frame or not obj:
        return

    if not layers:
        # by default all visible layers
        layers = [l for l in gpl if not l.hide and not l.lock] # include locked ?
    if not isinstance(layers, list):
        # if a single layer object is send put in a list
        layers = [layers]

    stroke_list = []

    for l in layers:
        f = l.current_frame()

        if not f:
            continue# active frame can be None
 
        for s in f.strokes:
            ## full stroke version
            # if s.select:
            # send index of all points to get the whole stroke with "range"
            stroke_list.append( dump_gp_stroke_range(s, [i for i in range(len(s.points))], l, obj,
            pressure=pressure,strength=strength,vertex_color=vertex_color,uv_fill=uv_fill,uv_factor=uv_factor,uv_rotation=uv_rotation))

    # print(len(stroke_list), 'strokes copied in', time()-t0, 'seconds')
    return stroke_list

def add_stroke(s, frame, layer, obj, select=False):
    '''add stroke on a given frame, (layer is for parentage setting)'''
    # print(3*'-',s)
    ns = frame.strokes.new()

    for att, val in s.items():
        if att not in ('points'):
            setattr(ns, att, val)
    pts_to_add = len(s['points'])
    # print(pts_to_add, 'points')#dbg

    ns.points.add(pts_to_add)
    
    ob_mat_inv = obj.matrix_world.inverted()

    ## patch pressure 1
    # pressure_flat_list = [di['pressure'] for di in s['points']] #get all pressure flatened

    if layer.is_parented:
        mat = getMatrix(layer).inverted()
        for i, pt in enumerate(s['points']):
            for k, v in pt.items():
                if k == 'co':
                    setattr(ns.points[i], k, v)
                    ns.points[i].position = ob_mat_inv @ mat @ ns.points[i].position # invert of object * invert of layer * coordinate
                else:
                    setattr(ns.points[i], k, v)
                if select:
                    ns.points[i].select = True
    
    else:
        for i, pt in enumerate(s['points']):
            for k, v in pt.items():
                if k == 'co':
                    setattr(ns.points[i], k, v)
                    ns.points[i].position = ob_mat_inv @ ns.points[i].position# invert of object * coordinate
                else:
                    setattr(ns.points[i], k, v)
                if select:
                    ns.points[i].select = True

    ## trigger updapte (in 2.93 fix some drawing problem with fills and UVs)
    ns.points.update()
    
    ## patch pressure 2
    # ns.points.foreach_set('pressure', pressure_flat_list)

def add_multiple_strokes(stroke_list, layer=None, use_current_frame=True, select=False):
    '''
    add a list of strokes to active frame of given layer
    if no layer specified, active layer is used
    if use_current_frame is True, a new frame will be created only if needed
    if select is True, newly added strokes are set selected
    if stroke list is empty create an empty frame at current frame 
    '''
    scene = bpy.context.scene
    obj = bpy.context.object
    gp = obj.data
    gpl = gp.layers

    #default: active
    if not layer:
        layer = gpl.active

    fnum = scene.frame_current
    target_frame = False
    act = layer.current_frame()
    ## set frame if needed
    if act:
        if use_current_frame or act.frame_number == fnum:
            #work on current frame if exists
            # use current frame anyway if one key exist at this scene.frame
            target_frame = act

    if not target_frame:
        #no active frame
        #or active exists but not aligned scene.current with use_current_frame disabled
        target_frame = layer.frames.new(fnum)

    for s in stroke_list:
        add_stroke(s, target_frame, layer, obj, select=select)
        '''
        for s in stroke_data:
            add_stroke(s, target_frame)
        '''
    
    # print(len(stroke_list), 'strokes pasted')


### OPERATORS

class GPCLIP_OT_copy_strokes(bpy.types.Operator):
    bl_idname = "gp.copy_strokes"
    bl_label = "GP Copy strokes"
    bl_description = "Copy strokes to text in paperclip"
    bl_options = {"REGISTER"}

    #copy = bpy.props.BoolProperty(default=True)
    @classmethod
    def poll(cls, context):
        return context.object and context.object.type == 'GREASEPENCIL'

    def execute(self, context):
        # if not context.object or not context.object.type == 'GREASEPENCIL':
        #     self.report({'ERROR'},'No GP object selected')
        #     return {"CANCELLED"}

        t0 = time()
        #ct = check_pressure()
        strokelist = copycut_strokes(copy=True, keep_empty=True)
        if not strokelist:
            self.report({'ERROR'}, 'Nothing to copy')
            return {"CANCELLED"}
        bpy.context.window_manager.clipboard = json.dumps(strokelist)#copy=self.copy
        #if ct:
        #    self.report({'ERROR'}, "Copie OK\n{} points ont une épaisseur supérieure a 1.0 (max = {:.2f})\nCes épaisseurs seront plafonnées à 1 au 'coller'".format(ct[0], ct[1]))
        self.report({'INFO'}, f'Copied (time : {time() - t0:.4f})')
        # print('copy total time:', time() - t0)
        return {"FINISHED"}


class GPCLIP_OT_cut_strokes(bpy.types.Operator):
    bl_idname = "gp.cut_strokes"
    bl_label = "GP Cut strokes"
    bl_description = "Cut strokes to text in paperclip"
    bl_options = {"REGISTER"}

    @classmethod
    def poll(cls, context):
        return context.object and context.object.type == 'GREASEPENCIL'

    def execute(self, context):
        # if not context.object or not context.object.type == 'GREASEPENCIL':
        #     self.report({'ERROR'},'No GP object selected')
        #     return {"CANCELLED"}

        t0 = time()
        strokelist = copycut_strokes(copy=False, keep_empty=True) # ct = check_pressure()
        if not strokelist:
            self.report({'ERROR'},'Nothing to cut')
            return {"CANCELLED"}
        bpy.context.window_manager.clipboard = json.dumps(strokelist)
        
        self.report({'INFO'}, f'Cutted (time : {time() - t0:.4f})')
        return {"FINISHED"}

class GPCLIP_OT_paste_strokes(bpy.types.Operator):
    bl_idname = "gp.paste_strokes"
    bl_label = "GP Paste strokes"
    bl_description = "paste stroke from paperclip"
    bl_options = {"REGISTER"}

    @classmethod
    def poll(cls, context):
        return context.object and context.object.type == 'GREASEPENCIL'

    def execute(self, context):
        # if not context.object or not context.object.type == 'GREASEPENCIL':
        #     self.report({'ERROR'},'No GP object selected to paste on')
        #     return {"CANCELLED"}
        
        t0 = time()
        #add a validity check por the content of the paperclip (check if not data.startswith('[{') ? )
        try:
            data = json.loads(bpy.context.window_manager.clipboard)
        except:
            mess = 'Clipboard does not contain drawing data (load error)'
            self.report({'ERROR'}, mess)
            return {"CANCELLED"}

        print('data loaded', time() - t0)
        add_multiple_strokes(data, use_current_frame=True, select=True)
        print('total_time', time() - t0)

        return {"FINISHED"}

### --- multi copy

class GPCLIP_OT_copy_multi_strokes(bpy.types.Operator):
    bl_idname = "gp.copy_multi_strokes"
    bl_label = "GP Copy Multi Strokes"
    bl_description = "Copy multiple layers>frames>strokes from selected layers to str in paperclip"
    bl_options = {"REGISTER"}

    #copy = bpy.props.BoolProperty(default=True)
    @classmethod
    def poll(cls, context):
        return context.object and context.object.type == 'GREASEPENCIL'

    pressure : bpy.props.BoolProperty(name='pressure', default=True,
        description='Dump point pressure attribute (already skipped if at default value)')
    strength : bpy.props.BoolProperty(name='strength', default=True,
        description='Dump point strength attribute (already skipped if at default value)')
    vertex_color : bpy.props.BoolProperty(name='vertex color', default=True,
        description='Dump point vertex_color attribute (already skipped if at default value)')
    uv_fill : bpy.props.BoolProperty(name='uv fill', default=True,
        description='Dump point uv_fill attribute (already skipped if at default value)')
    uv_factor : bpy.props.BoolProperty(name='uv factor', default=True,
        description='Dump point uv_factor attribute (already skipped if at default value)')
    uv_rotation : bpy.props.BoolProperty(name='uv rotation', default=True,
        description='Dump point uv_rotation attribute (already skipped if at default value)')

    def invoke(self, context, event):
        # self.file_dump = event.ctrl
        return context.window_manager.invoke_props_dialog(self) # , width=400
        # return self.execute(context)

    def draw(self, context):
        layout=self.layout
        layout.use_property_split = True
        col = layout.column()
        col.label(text='Keep following point attributes:')
        col.prop(self, 'pressure')
        col.prop(self, 'strength')
        col.prop(self, 'vertex_color')
        col.prop(self, 'uv_fill')
        col.prop(self, 'uv_factor')
        col.prop(self, 'uv_rotation')
        return

    def execute(self, context):
        bake_moves = True
        skip_empty_frame = False

        org_frame = context.scene.frame_current
        obj = context.object
        gpl = obj.data.layers
        t0 = time()
        #ct = check_pressure()
        layerdic = {}

        layerpool = [l for l in gpl if not l.hide and l.select] # and not l.lock
        if not layerpool:
            self.report({'ERROR'}, 'No layers selected in GP dopesheet (needs to be visible and selected to be copied)\nHint: Changing active layer reset selection to active only')
            return {"CANCELLED"}
            
        if not bake_moves: # copy only drawed frames as is.
            for l in layerpool:
                if not l.frames:
                    continue# skip empty layers

                frame_dic = {}
                for f in l.frames:
                    if skip_empty_frame and not len(f.strokes):
                        continue
                    context.scene.frame_set(f.frame_number) # use matrix of this frame
                    strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj,
                    pressure=self.pressure, strength=self.strength, vertex_color=self.vertex_color,
                    uv_fill=self.uv_fill, uv_factor=self.uv_factor, uv_rotation=self.uv_rotation)
                    
                    frame_dic[f.frame_number] = strokelist
                
                layerdic[l.name] = frame_dic

        else: # bake position: copy frame where object as moved even if frame is unchanged 
            for l in layerpool:
                print('dump layer:', l.name)
                if not l.frames:
                    continue# skip empty layers
                
                frame_dic = {}

                fnums_dic = {f.frame_number: f for f in l.frames}

                context.scene.frame_set(context.scene.frame_start)
                curmat = prevmat = obj.matrix_world.copy()

                for i in range(context.scene.frame_start, context.scene.frame_end):
                    context.scene.frame_set(i) # use matrix of this frame
                    curmat = obj.matrix_world.copy()

                    # if object has moved or current time is on a draw key
                    if prevmat != curmat or i in fnums_dic.keys():
                        # get the current used frame
                        for j in fnums_dic.keys():
                            if j >= i:
                                f = fnums_dic[j]
                                break
                        
                        ## skip empty frame if specified
                        if skip_empty_frame and not len(f.strokes):
                            continue
                        
                        strokelist = copy_all_strokes_in_frame(frame=f, layers=l, obj=obj,
                        pressure=self.pressure, strength=self.strength, vertex_color=self.vertex_color,
                        uv_fill=self.uv_fill, uv_factor=self.uv_factor, uv_rotation=self.uv_rotation)

                        frame_dic[i] = strokelist

                    prevmat = curmat    
                layerdic[l.name] = frame_dic                

        ## All to clipboard manager
        bpy.context.window_manager.clipboard = json.dumps(layerdic)

        # reset original frame.
        context.scene.frame_set(org_frame)
        self.report({'INFO'}, f'Copied layers (time : {time() - t0:.4f})')
        # print('copy total time:', time() - t0)
        return {"FINISHED"}

class GPCLIP_OT_paste_multi_strokes(bpy.types.Operator):
    bl_idname = "gp.paste_multi_strokes"
    bl_label = "GP Paste Multi Strokes"
    bl_description = "Paste multiple layers>frames>strokes from paperclip on active layer"
    bl_options = {"REGISTER"}

    #copy = bpy.props.BoolProperty(default=True)
    @classmethod
    def poll(cls, context):
        return context.object and context.object.type == 'GREASEPENCIL'

    def execute(self, context):
        org_frame = context.scene.frame_current
        obj = context.object
        gpl = obj.data.layers
        t0 = time()
        #add a validity check por the content of the paperclip (check if not data.startswith('[{') ? )
        try:
            data = json.loads(bpy.context.window_manager.clipboard)
        except:
            mess = 'Clipboard does not contain drawing data (load error)'
            self.report({'ERROR'}, mess)
            return {"CANCELLED"}

        print('data loaded', time() - t0)
        # add layers (or merge with existing names ?)
        
        ### structure
        # {layername :
        #       {1: [strokelist of frame 1], 3: [strokelist of frame 3]}
        # }

        for layname, allframes in data.items():
            layer = gpl.get(layname)
            if not layer:
                layer = gpl.new(layname)
            for fnum, fstrokes in allframes.items():
                context.scene.frame_set(int(fnum)) # use matrix of this frame for copying (maybe just evaluate depsgraph for object
                add_multiple_strokes(fstrokes, use_current_frame=False) # create a new frame at each encoutered occurence
        
        print('total_time', time() - t0)

        # reset original frame.
        context.scene.frame_set(org_frame)
        self.report({'INFO'}, f'Copied layers (time : {time() - t0:.4f})')
        # print('copy total time:', time() - t0)
        return {"FINISHED"}

##--PANEL

class GPCLIP_PT_clipboard_ui(bpy.types.Panel):
    # bl_idname = "gp_clipboard_panel"
    bl_label = "GP Clipboard"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = "Gpencil"
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        
        col = layout.column(align=True)
        row = col.row(align=True)
        row.operator('gp.copy_strokes', text='Copy Strokes', icon='COPYDOWN')
        row.operator('gp.cut_strokes', text='Cut Strokes', icon='PASTEFLIPUP')
        col.operator('gp.paste_strokes', text='Paste Strokes', icon='PASTEDOWN')
        # layout.separator()
        col = layout.column(align=True)
        col.operator('gp.copy_multi_strokes', text='Copy Layers', icon='COPYDOWN')
        col.operator('gp.paste_multi_strokes', text='Paste Layers', icon='PASTEDOWN')

###---TEST zone

"""
##main defs
def copy_strokes_to_paperclip():
    bpy.context.window_manager.clipboard = json.dumps(copycut_strokes(copy=True, keep_empty=True))#default layers are visible one

def cut_strokes_to_paperclip():
    bpy.context.window_manager.clipboard = json.dumps(copycut_strokes(copy=False, keep_empty=True))

def paste_strokes_from_paperclip():
    #add condition to detect if clipboard contains loadable values
    add_multiple_strokes(json.loads(bpy.context.window_manager.clipboard), use_current_frame=True)#layer= layers.active

#copy_strokes_to_paperclip()
#paste_strokes_from_paperclip()

#test direct
#li = copycut_strokes(copy=True)
#add_multiple_strokes(li, bpy.context.scene.grease_pencil.layers['correct'])
"""


#use directly operator idname in shortcut settings :
# gp.copy_strokes
# gp.cut_strokes
# gp.paste_strokes
# gp.copy_multi_strokes
# gp.paste_multi_strokes

###---REGISTER + copy cut paste keymapping

addon_keymaps = []
def register_keymaps():
    addon = bpy.context.window_manager.keyconfigs.addon
    km = addon.keymaps.new(name = "Grease Pencil", space_type = "EMPTY", region_type='WINDOW')# in Grease context
    # km = addon.keymaps.new(name = "3D View", space_type = "VIEW_3D")# in 3D context
    # km = addon.keymaps.new(name = "Window", space_type = "EMPTY")# from everywhere
    
    kmi = km.keymap_items.new("gp.copy_strokes", type = "C", value = "PRESS", ctrl=True, shift=True)
    kmi.repeat = False
    addon_keymaps.append((km, kmi))

    kmi = km.keymap_items.new("gp.cut_strokes", type = "X", value = "PRESS", ctrl=True, shift=True)
    kmi.repeat = False
    addon_keymaps.append((km, kmi))

    kmi = km.keymap_items.new("gp.paste_strokes", type = "V", value = "PRESS", ctrl=True, shift=True)
    kmi.repeat = False
    addon_keymaps.append((km, kmi))

def unregister_keymaps():
    for km, kmi in addon_keymaps:
        km.keymap_items.remove(kmi)
    addon_keymaps.clear()


classes = (
GPCLIP_OT_copy_strokes,
GPCLIP_OT_cut_strokes,
GPCLIP_OT_paste_strokes,
GPCLIP_OT_copy_multi_strokes,
GPCLIP_OT_paste_multi_strokes,
GPCLIP_PT_clipboard_ui,
)

def register():
    if bpy.app.background:
        return

    for cl in classes:
        bpy.utils.register_class(cl)

    ## make scene property for empty key preservation and bake movement for layers...
    register_keymaps()

def unregister():
    if bpy.app.background:
        return

    unregister_keymaps()
    for cl in reversed(classes):
        bpy.utils.unregister_class(cl)

if __name__ == "__main__":
    register()