1 # SPDX-FileCopyrightText: 2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
8 from gpu_extras
.batch
import batch_for_shader
9 from mathutils
import Vector
, Matrix
11 from pathlib
import Path
13 from bpy
.props
import (BoolProperty
, IntProperty
)
15 from .prefs
import get_addon_prefs
18 def rectangle_tris_from_coords(quad_list
):
19 '''Get a list of Vector corner for a triangle
20 return a list of TRI for gpu drawing'''
32 def round_to_ceil_even(f
):
33 if (math
.floor(f
) % 2 == 0):
36 return math
.floor(f
) + 1
38 def move_layer_to_index(l
, idx
):
39 a
= [i
for i
, lay
in enumerate(l
.id_data
.layers
) if lay
== l
][0]
43 direction
= 'UP' if move
> 0 else 'DOWN'
44 for _i
in range(abs(move
)):
45 l
.id_data
.layers
.move(l
, direction
)
47 def get_reduced_area_coord(context
):
48 w
, h
= context
.region
.width
, context
.region
.height
50 ## minus tool leftbar + sidebar right
51 regs
= context
.area
.regions
52 toolbar
= next((r
for r
in regs
if r
.type == 'TOOLS'), None)
53 sidebar
= next((r
for r
in regs
if r
.type == 'UI'), None)
54 header
= next((r
for r
in regs
if r
.type == 'HEADER'), None)
55 tool_header
= next((r
for r
in regs
if r
.type == 'TOOL_HEADER'), None)
56 up_margin
= down_margin
= 0
57 if tool_header
.alignment
== 'TOP':
58 up_margin
+= tool_header
.height
60 down_margin
+= tool_header
.height
63 left_down
= (toolbar
.width
, down_margin
+2)
64 right_down
= (w
- sidebar
.width
, down_margin
+2)
65 left_up
= (toolbar
.width
, h
- up_margin
-1)
66 right_up
= (w
- sidebar
.width
, h
- up_margin
-1)
67 return left_down
, right_down
, left_up
, right_up
69 def draw_callback_px(self
, context
):
70 if context
.area
!= self
.current_area
:
74 ## timer for debug purposes
75 # blf.position(font_id, 15, 30, 0)
76 # blf.size(font_id, 20.0)
77 # blf.draw(font_id, "Time " + self.text)
79 shader
= gpu
.shader
.from_builtin('UNIFORM_COLOR') # initiate shader
80 gpu
.state
.blend_set('ALPHA')
81 gpu
.state
.line_width_set(1.0)
85 ## draw one background at once
86 # shader.uniform_float("color", self.bg_color) # (1.0, 1.0, 1.0, 1.0)
87 # self.batch_bg.draw(shader)
89 ## locked layer (individual rectangle)
95 active_width
= float(round_to_ceil_even(4.0 * context
.preferences
.system
.ui_scale
))
98 icons
= {'locked':[],'unlocked':[], 'hide_off':[], 'hide_on':[]}
100 for i
, l
in enumerate(self
.gpl
):
101 ## Rectangle coords CW from bottom-left corner
103 corner
= Vector((self
.left
, self
.bottom
+ self
.px_h
* i
))
105 # With LINE_STRIP: Repeat first coordinate to close square with line_strip shader
106 # active_case = [v + corner for v in self.case] + [self.case[0] + corner]
108 ## With LINES: width offset to avoid jaggy corner
109 # Convert single corners point to flattened line vector pairs
110 active_case
= [v
+ corner
for v
in self
.case
]
111 flattened_line_pairs
= []
112 for i
in range(len(active_case
)):
113 flattened_line_pairs
+= [active_case
[i
], active_case
[(i
+1) % len(active_case
)]]
116 px_offset
= int(active_width
/ 2)
118 Vector((0, -px_offset
)), Vector((0, px_offset
)),
119 Vector((-px_offset
, 0)), Vector((px_offset
, 0)),
120 Vector((0, px_offset
)), Vector((0, -px_offset
)),
121 Vector((px_offset
, 0)), Vector((-px_offset
, 0)),
124 # Apply offset to line tips
125 active_case
= [v
+ offset
for v
, offset
in zip(flattened_line_pairs
, case_px_offsets
)]
128 lock_coord
= corner
+ Vector((self
.px_w
- self
.icons_margin_a
, self
.mid_height
- int(self
.icon_size
/ 2)))
130 hide_coord
= corner
+ Vector((self
.px_w
- self
.icons_margin_b
, self
.mid_height
- int(self
.icon_size
/ 2)))
134 lock_rects
+= rectangle_tris_from_coords(
135 [v
+ corner
for v
in self
.case
]
137 icons
['locked'].append([v
+ lock_coord
for v
in self
.icon_tex_coord
])
139 rects
+= rectangle_tris_from_coords(
140 [v
+ corner
for v
in self
.case
]
142 icons
['unlocked'].append([v
+ lock_coord
for v
in self
.icon_tex_coord
])
146 icons
['hide_on'].append([v
+ hide_coord
for v
in self
.icon_tex_coord
])
148 icons
['hide_off'].append([v
+ hide_coord
for v
in self
.icon_tex_coord
])
151 ## opacity sliders background
152 opacity_bars
+= rectangle_tris_from_coords(
153 [corner
+ v
for v
in self
.opacity_slider
]
157 opacitys
+= rectangle_tris_from_coords(
158 [corner
+ v
for v
in self
.opacity_slider
[:2]]
159 + [corner
+ Vector((int(v
[0] * l
.opacity
), v
[1])) for v
in self
.opacity_slider
[2:]]
162 ### --- Trace squares
163 ## individual unlocked squares
164 shader
.uniform_float("color", self
.bg_color
)
165 batch_squares
= batch_for_shader(shader
, 'TRIS', {"pos": rects
})
166 batch_squares
.draw(shader
)
169 shader
.uniform_float("color", self
.lock_color
)
170 batch_lock
= batch_for_shader(shader
, 'TRIS', {"pos": lock_rects
})
171 batch_lock
.draw(shader
)
174 shader
.uniform_float("color", self
.opacity_bar_color
)
175 batch_lock
= batch_for_shader(shader
, 'TRIS', {"pos": opacity_bars
})
176 batch_lock
.draw(shader
)
179 shader
.uniform_float("color", self
.opacity_color
)
180 batch_lock
= batch_for_shader(shader
, 'TRIS', {"pos": opacitys
})
181 batch_lock
.draw(shader
)
184 gpu
.state
.line_width_set(2.0)
186 ## line color (static)
187 shader
.uniform_float("color", self
.lines_color
)
188 self
.batch_lines
.draw(shader
)
191 if self
.gpl
.active_index
== 0:
192 plus_lines
= self
.plus_lines
[:8]
194 plus_lines
= self
.plus_lines
[self
.gpl
.active_index
* 4 + 4:self
.gpl
.active_index
* 4 + 8]
195 batch_plus
= batch_for_shader(
196 shader
, 'LINES', {"pos": plus_lines
})
197 batch_plus
.draw(shader
)
199 ## Loop draw tex icons
200 for icon_name
, coord_list
in icons
.items():
201 texture
= gpu
.texture
.from_image(self
.icon_tex
[icon_name
])
202 for coords
in coord_list
:
203 shader_tex
= gpu
.shader
.from_builtin('IMAGE')
204 batch_icons
= batch_for_shader(
205 shader_tex
, 'TRI_FAN',
208 "texCoord": ((0, 0), (1, 0), (1, 1), (0, 1)),
212 shader_tex
.uniform_sampler("image", texture
)
213 batch_icons
.draw(shader_tex
)
215 ## Highlight active layer
217 gpu
.state
.line_width_set(active_width
)
218 shader
.uniform_float("color", self
.active_layer_color
)
219 # batch_active = batch_for_shader(shader, 'LINE_STRIP', {"pos": active_case})
220 batch_active
= batch_for_shader(shader
, 'LINES', {"pos": active_case
})
221 batch_active
.draw(shader
)
223 gpu
.state
.line_width_set(1.0)
224 gpu
.state
.blend_set('NONE')
228 for i
, l
in enumerate(self
.gpl
):
229 ## add color underneath active name
230 # if i == self.ui_idx:
231 # ## color = self.active_layer_color # Color active name
232 # blf.position(font_id, self.text_x+1, self.text_pos[i]-1, 0)
233 # blf.size(font_id, self.text_size)
234 # blf.color(font_id, *self.active_layer_color)
235 # blf.draw(font_id, l.info)
237 color
= self
.hided_layer_color
238 elif not len(l
.frames
) or (len(l
.frames
) == 1 and not len(l
.frames
[0].strokes
)):
239 # Show darker color if is empty if layer is empty (or has one empty keyframe)
240 color
= self
.empty_layer_color
242 color
= self
.other_layer_color
244 blf
.position(font_id
, self
.text_x
, self
.text_pos
[i
], 0)
245 blf
.size(font_id
, self
.text_size
)
246 blf
.color(font_id
, *color
)
247 display_name
= l
.info
if len(l
.info
) <= self
.text_char_limit
else l
.info
[:self
.text_char_limit
-3] + '...'
248 blf
.draw(font_id
, display_name
)
251 if self
.dragging
and self
.drag_text
:
252 blf
.position(font_id
, self
.mouse
.x
+ 5, self
.mouse
.y
+ 5, 0)
253 blf
.size(font_id
, self
.text_size
)
254 blf
.color(font_id
, 1.0, 1.0, 1.0, 1.0)
255 if self
.drag_text
== 'opacity_level':
256 blf
.draw(font_id
, f
'{self.gpl[self.ui_idx].opacity:.2f}')
258 blf
.draw(font_id
, self
.drag_text
)
261 class GPT_OT_viewport_layer_nav_osd(bpy
.types
.Operator
):
262 bl_idname
= "gpencil.viewport_layer_nav_osd"
263 bl_label
= "GP Layer Navigator Pop up"
264 bl_description
= "Change active GP layer with a viewport interactive OSD"
265 bl_options
= {'REGISTER', 'INTERNAL'}
268 def poll(cls
, context
):
269 return context
.object is not None and context
.object.type == 'GPENCIL'
279 bg_color
= (0.1, 0.1, 0.1, 0.96)
280 lock_color
= (0.02, 0.02, 0.02, 0.98) # overlap opacity darken
281 lines_color
= (0.5, 0.5, 0.5, 1.0)
282 opacity_bar_color
= (0.25, 0.25, 0.25, 1.0)
283 opacity_color
= (0.4, 0.4, 0.4, 1.0) # (0.28, 0.45, 0.7, 1.0)
285 other_layer_color
= (0.8, 0.8, 0.8, 1.0) # strong grey
286 active_layer_color
= (0.28, 0.45, 0.7, 1.0) # Blue (active color)
287 empty_layer_color
= (0.7, 0.5, 0.4, 1.0) # mid reddish grey # (0.5, 0.5, 0.5, 1.0) # mid grey
288 hided_layer_color
= (0.4, 0.4, 0.4, 1.0) # faded grey
290 def get_icon(self
, img_name
):
291 store_name
= '.' + img_name
292 img
= bpy
.data
.images
.get(store_name
)
294 icon_folder
= Path(__file__
).parent
/ 'icons'
295 img
= bpy
.data
.images
.load(filepath
=str((icon_folder
/ img_name
).with_suffix('.png')), check_existing
=False)
296 img
.name
= store_name
299 def setup(self
, context
):
300 ui_scale
= bpy
.context
.preferences
.system
.ui_scale
301 # if not len(self.gpl):
302 # # Needed if delete is implemented
303 # return {'CANCELLED'}
305 self
.layer_list
= [(l
.info
, l
) for l
in self
.gpl
]
306 self
.ui_idx
= self
.org_index
= context
.object.data
.layers
.active_index
307 self
.id_num
= len(self
.layer_list
)
308 self
.dragging
= False
309 self
.drag_mode
= None
310 self
.drag_text
= None
312 # self.click_time = 0
313 self
.id_src
= self
.click_src
= None
322 # | | <-- bottom_base
325 max_w
= self
.px_w
+ self
.add_box
326 mid_square
= int(self
.px_w
/ 2)
328 bottom_base
= self
.init_mouse
.y
- (self
.org_index
* self
.px_h
)
329 self
.text_bottom
= bottom_base
- int(self
.text_size
/ 2)
331 self
.mid_height
= int(self
.px_h
/ 2)
332 self
.bottom
= bottom_base
- self
.mid_height
333 self
.top
= self
.bottom
+ (self
.px_h
* self
.id_num
)
335 self
.left
= self
.init_mouse
.x
- int(self
.px_w
/ 10)
338 self
.left
= self
.init_mouse
.x
- mid_square
340 ## Push from viewport borders if needed
341 BL
, BR
, _1
, _2
= get_reduced_area_coord(context
)
343 over_right
= (self
.left
+ max_w
) - (BR
[0] + 10 * ui_scale
) # from sidebar border
344 # over_right = (self.left + max_w) - (context.area.width - 20) # from right border
346 self
.left
= self
.left
- over_right
348 # Priority on left push
349 over_left
= BL
[0] - self
.left
# from toolbar border
350 # over_left = 1 - self.left # from left border
352 self
.left
= self
.left
+ over_left
354 self
.right
= self
.left
+ self
.px_w
356 self
.text_x
= (self
.left
+ mid_square
) - int(self
.px_w
/ 3)
362 for i
in range(self
.id_num
):
363 y_coord
= self
.bottom
+ (i
* self
.px_h
)
364 self
.lines
+= [(self
.left
, y_coord
), (self
.right
, y_coord
)]
366 # self.texts.append((self.gpl[i].info, self.text_bottom + (i * self.px_h)))
367 self
.text_pos
.append(self
.text_bottom
+ (i
* self
.px_h
))
369 ## define index ranges
370 self
.ranges
.append((y_coord
, y_coord
+ self
.px_h
))
375 Vector((self
.add_box
, 0)),
376 Vector((self
.add_box
, self
.add_box
)),
377 Vector((0, self
.add_box
)),
380 self
.add_box_zones
= []
381 mid
= self
.add_box
/ 2
382 marg
= round_to_ceil_even(self
.add_box
/ 4)
383 plus_length
= round_to_ceil_even(self
.add_box
- marg
* 2)
386 Vector((mid
, marg
)), Vector((mid
, marg
+ plus_length
)),
387 Vector((marg
, mid
)), Vector((marg
+ plus_length
, mid
)),
391 for i
in range(len(self
.gpl
) + 1):
392 height
= self
.bottom
- self
.add_box
+ (i
* self
.px_h
)
393 self
.add_box_zones
.append(
394 [v
+ Vector((self
.right
, height
)) for v
in box
]
396 self
.plus_lines
+= [v
+ Vector((self
.right
, height
)) for v
in plus
]
398 self
.add_box_rects
= []
399 for box
in self
.add_box_zones
:
400 self
.add_box_rects
+= rectangle_tris_from_coords(box
)
404 Vector((0, self
.px_h
)),
405 Vector((self
.px_w
, self
.px_h
)),
406 Vector((self
.px_w
, 0)),
409 self
.opacity_slider
= [
410 Vector((0, self
.px_h
- self
.slider_height
)),
411 Vector((0, self
.px_h
)),
412 Vector((self
.opacity_slider_length
, self
.px_h
)),
413 Vector((self
.opacity_slider_length
, self
.px_h
- self
.slider_height
)),
418 self
.lines
+= [Vector((self
.left
, self
.top
)), Vector((self
.right
, self
.top
)),
419 Vector((self
.left
, self
.bottom
)), Vector((self
.right
, self
.bottom
)),
420 Vector((self
.left
, self
.top
)), Vector((self
.left
, self
.bottom
)),
421 Vector((self
.right
, self
.top
)), Vector((self
.right
, self
.bottom
))]
422 shader
= gpu
.shader
.from_builtin('UNIFORM_COLOR')
424 self
.batch_lines
= batch_for_shader(
425 shader
, 'LINES', {"pos": self
.lines
[2:]})
426 # shader, 'LINES', {"pos": self.lines[2:] + self.plus_lines}) #Show all '+'
428 def invoke(self
, context
, event
):
429 self
.gpl
= context
.object.data
.layers
430 if not len(self
.gpl
):
431 self
.report({'WARNING'}, "No layer to display")
434 self
.key
= event
.type
435 self
.mouse
= self
.init_mouse
= Vector((event
.mouse_region_x
, event
.mouse_region_y
))
438 ui_scale
= bpy
.context
.preferences
.system
.ui_scale
440 ## Load texture icons
441 self
.icon_tex
= {n
: self
.get_icon(n
) for n
in ('locked','unlocked', 'hide_off', 'hide_on')}
442 self
.icon_size
= int(20 * ui_scale
)
443 self
.icon_tex_coord
= (
445 Vector((self
.icon_size
, 0)),
446 Vector((self
.icon_size
, self
.icon_size
)),
447 Vector((0, self
.icon_size
))
450 prefs
= get_addon_prefs().nav
451 self
.px_h
= int(prefs
.box_height
* ui_scale
)
452 self
.px_w
= int(prefs
.box_width
* ui_scale
)
453 self
.add_box
= int(22 * ui_scale
)
454 self
.text_size
= int(prefs
.text_size
* ui_scale
)
455 self
.text_char_limit
= round((self
.px_w
+ 10 * ui_scale
) / self
.text_size
)
456 self
.left_handed
= prefs
.left_handed
457 self
.icons_margin_a
= int(30 * ui_scale
)
458 self
.icons_margin_b
= int(54 * ui_scale
)
460 self
.opacity_slider_length
= int(self
.px_w
* 72 / 100) # As width's percentage
461 # self.opacity_slider_length = self.px_w # Full width
463 self
.slider_height
= int(self
.px_h
/ 3.7) # Proportional slider
464 # self.slider_height = int(8 * ui_scale) # Fixed size slider
465 ret
= self
.setup(context
)
469 self
.current_area
= context
.area
470 wm
= context
.window_manager
471 args
= (self
, context
)
473 self
.store_settings(context
)
475 self
._handle
= bpy
.types
.SpaceView3D
.draw_handler_add(draw_callback_px
, args
, 'WINDOW', 'POST_PIXEL')
476 # self._timer = wm.event_timer_add(0.1, window=context.window)
478 wm
.modal_handler_add(self
)
479 context
.area
.tag_redraw()
480 return {'RUNNING_MODAL'}
483 def set_fade(self
, context
):
484 context
.space_data
.overlay
.use_gpencil_fade_layers
= True
485 context
.space_data
.overlay
.gpencil_fade_layer
= self
.fade_value
488 def stop_fade(self
, context
):
489 context
.space_data
.overlay
.use_gpencil_fade_layers
= self
.org_use_gpencil_fade_layers
490 context
.space_data
.overlay
.gpencil_fade_layer
= self
.org_gpencil_fade_layer
494 def store_settings(self
, context
):
495 ## store values anyway
496 self
.org_use_gpencil_fade_layers
= context
.space_data
.overlay
.use_gpencil_fade_layers
497 self
.org_gpencil_fade_layer
= context
.space_data
.overlay
.gpencil_fade_layer
499 self
.set_fade(context
)
501 def id_from_coord(self
, v
):
502 if v
.y
< self
.ranges
[0][0]:
503 return 0 # return -1 # below deck
504 for i
, (bottom
, top
) in enumerate(self
.ranges
):
505 if bottom
< v
.y
< top
:
507 # Return min if below and max if above instead of None
510 def id_from_mouse(self
):
511 return self
.id_from_coord(self
.mouse
)
513 def click(self
, context
):
514 '''Handle click in ui, returning True stop the modal'''
516 if self
.add_box_zones
[0][0].x
<= self
.mouse
.x
<= self
.add_box_zones
[0][2].x
:
517 ## if its on a box zone, add layer at this place
518 for i
, zone
in enumerate(self
.add_box_zones
):
519 # if (zone[0].x <= self.mouse.x <= zone[2].x) and (zone[0].y <= self.mouse.y <= zone[2].y):
520 if zone
[0].y
<= self
.mouse
.y
<= zone
[2].y
:
522 nl
= context
.object.data
.layers
.new('GP_Layer')
523 nl
.frames
.new(context
.scene
.frame_current
, active
=True)
524 nl
.use_lights
= False
526 ## bottom layer, need to get down by one
527 # bpy.ops.gpencil.layer_move(type='DOWN')
528 self
.gpl
.move(nl
, type='DOWN')
530 # return True # Stop the modal when a new layer is created
533 new_y
= self
.init_mouse
[1] + self
.px_h
* (self
.ui_idx
- self
.org_index
)
534 self
.init_mouse
= Vector((self
.init_mouse
[0], new_y
))
538 ## check hide / lock toggles
539 hide_col
= lock_col
= False
541 if self
.right
- self
.icons_margin_a
<= self
.mouse
.x
<= self
.right
- self
.icons_margin_a
+ self
.icon_size
:
543 elif self
.right
- self
.icons_margin_b
<= self
.mouse
.x
<= self
.right
- self
.icons_margin_b
+ self
.icon_size
:
546 if hide_col
or lock_col
:
547 dist_from_case_bottom
= self
.mid_height
- int(self
.icon_size
/ 2)
548 for i
, l
in enumerate(self
.gpl
):
549 icon_base
= self
.bottom
+ (i
* self
.px_h
) + dist_from_case_bottom
550 if icon_base
- 4 <= self
.mouse
.y
<= icon_base
+ self
.icon_size
:
552 self
.gpl
[i
].hide
= not self
.gpl
[i
].hide
# l.hide = not l.hide
553 self
.drag_mode
= 'hide' if self
.gpl
[i
].hide
else 'unhide'
555 self
.gpl
[i
].lock
= not self
.gpl
[i
].lock
# l.lock = not l.lock
556 self
.drag_mode
= 'lock' if self
.gpl
[i
].lock
else 'unlock'
559 ## Check if clicked on layer zone and remember which id
561 ## With left drag limits
562 # if (self.left <= self.mouse.x <= self.right) and (self.bottom <= self.mouse.y <= self.top):
563 if (self
.mouse
.x
<= self
.right
) and (self
.bottom
<= self
.mouse
.y
<= self
.top
):
564 ## rename on layer double click
565 # /!\ problem ! 'Y' is still continuously pressed result: 'yyyyyyyyyyyes' !
567 # print('new_time - self.click_time: ', new_time - self.click_time)
568 # if new_time - self.click_time < 0.22:
569 # bpy.ops.wm.call_panel(name="GPTB_PT_layer_name_ui", keep_open=False)
571 # self.click_time = time()
573 self
.id_src
= self
.id_from_mouse() # self.ui_idx
574 self
.click_src
= self
.mouse
.copy()
576 top_case
= self
.bottom
+ self
.px_h
* (self
.ui_idx
+ 1)
577 if (top_case
- self
.slider_height
) <= self
.mouse
.y
<= top_case
:
579 self
.drag_text
= 'opacity_level'
580 self
.drag_mode
= 'opacity'
581 self
.org_opacity
= self
.gpl
[self
.id_src
].opacity
584 self
.drag_text
= self
.gpl
[self
.id_src
].info
585 self
.drag_mode
= 'layer'
588 def modal(self
, context
, event
):
589 context
.area
.tag_redraw()
590 self
.mouse
= Vector((event
.mouse_region_x
, event
.mouse_region_y
))
591 current_idx
= context
.object.data
.layers
.active_index
593 if event
.type in {'RIGHTMOUSE', 'ESC'}:
594 self
.stop_mod(context
)
595 context
.object.data
.layers
.active_index
= self
.org_index
598 if event
.type == self
.key
and event
.value
== 'RELEASE':
599 self
.stop_mod(context
)
603 if event
.type == 'X' and event
.value
== 'PRESS':
604 context
.object.show_in_front
= not context
.object.show_in_front
606 ## set fade with a key
607 # if event.type == 'R' and event.value == 'PRESS':
609 # self.stop_fade(context)
611 # self.set_fade(context)
613 if event
.type == 'LEFTMOUSE' and event
.value
== 'PRESS':
615 stop
= self
.click(context
)
617 self
.stop_mod(context
)
620 ## toggle based on distance
621 # self.dragging = self.pressed and (self.mouse - self.click_src).length > 4
623 ## toggle dragging once passed px amount from source
624 if self
.pressed
and self
.click_src
:
625 if (self
.mouse
- self
.click_src
).length
> 4:
627 if self
.dragging
and self
.drag_mode
== 'opacity':
628 x_travel
= self
.mouse
[0] - self
.click_src
[0]
629 change
= x_travel
/ self
.opacity_slider_length
# value 0 to 1.0
630 self
.gpl
[self
.id_src
].opacity
= self
.org_opacity
+ change
633 if event
.type == 'LEFTMOUSE' and event
.value
== 'RELEASE':
634 ## check if there was an ongoing drag action
636 if self
.drag_mode
== 'layer' and self
.id_src
is not None:
638 # print('move idx:', self.id_src, self.id_from_mouse())
639 move_layer_to_index(self
.gpl
[self
.id_src
], self
.id_from_mouse())
642 self
.pressed
= self
.dragging
= False
643 self
.click_src
= self
.drag_text
= self
.drag_mode
= None
645 ## Set Fade when passing sides of the list (except if dragging opacity)
646 if not (self
.dragging
and self
.drag_mode
== 'opacity'):
647 if self
.left
< self
.mouse
.x
< self
.right
+ self
.add_box
:
649 self
.stop_fade(context
)
651 if not self
.use_fade
:
652 self
.set_fade(context
)
655 if event
.type == 'T' and event
.value
== 'PRESS':
656 context
.object.data
.use_autolock_layers
= not context
.object.data
.use_autolock_layers
657 context
.object.data
.layers
.active
= context
.object.data
.layers
.active
# (force refresh of the autolocking)
659 if event
.type == 'H' and event
.value
== 'PRESS':
660 bpy
.ops
.gpencil
.layer_isolate(affect_visibility
=True)
661 if event
.type == 'L' and event
.value
== 'PRESS':
662 bpy
.ops
.gpencil
.layer_isolate(affect_visibility
=False)
664 # return {'RUNNING_MODAL'}
666 for i
, (bottom
, top
) in enumerate(self
.ranges
):
667 if bottom
< self
.mouse
.y
< top
:
671 if self
.ui_idx
== current_idx
:
672 return {'RUNNING_MODAL'}
674 context
.object.data
.layers
.active_index
= self
.ui_idx
676 ## maybe add a self.state value ?
677 if self
.drag_mode
== 'hide' and not self
.gpl
[self
.ui_idx
].hide
:
678 self
.gpl
[self
.ui_idx
].hide
= True
679 if self
.drag_mode
== 'unhide' and self
.gpl
[self
.ui_idx
].hide
:
680 self
.gpl
[self
.ui_idx
].hide
= False
682 if self
.drag_mode
== 'lock' and not self
.gpl
[self
.ui_idx
].lock
:
683 self
.gpl
[self
.ui_idx
].lock
= True
684 if self
.drag_mode
== 'unlock' and self
.gpl
[self
.ui_idx
].lock
:
685 self
.gpl
[self
.ui_idx
].lock
= False
687 return {'RUNNING_MODAL'} # running modal prevent original usage to be triggered (capture keys)
689 # return {'PASS_THROUGH'}
691 def stop_mod(self
, context
):
694 self
.stop_fade(context
)
695 wm
= context
.window_manager
696 # wm.event_timer_remove(self._timer)
697 bpy
.types
.SpaceView3D
.draw_handler_remove(self
._handle
, 'WINDOW')
699 context
.area
.tag_redraw()
702 class GPNAV_layer_navigator_settings(bpy
.types
.PropertyGroup
):
705 box_height
: IntProperty(
706 name
="Layer Box Height",
707 description
="Individual layer box height.\
708 \na big size take more screen space but allow better targeting",
717 box_width
: IntProperty(
718 name
="Layer Box Width",
719 description
="Individual layer box width.\
720 \na big size take more screen space but allow better targeting",
729 text_size
: IntProperty(
731 description
="Layer name label size",
740 left_handed
: BoolProperty(
742 description
="Pop-up appear offseted at the right of the mouse pointer\
743 \nto avoif hand occluding layer label",
746 def _indented_layout(layout
, level
):
749 level
= 0.0001 # Tweak so that a percentage of 0 won't split by half
750 indent
= level
* indentpx
/ bpy
.context
.region
.width
752 split
= layout
.split(factor
=indent
)
757 def draw_keymap_ui_custom(km
, kmi
, layout
):
758 # col = layout.column()
759 col
= _indented_layout(layout
, 0)
760 if kmi
.show_expanded
:
761 col
= col
.column(align
=True)
768 row
= split
.row(align
=True)
769 row
.prop(kmi
, "show_expanded", text
="", emboss
=False)
770 row
.prop(kmi
, "active", text
="", emboss
=False)
771 row
.label(text
=kmi
.name
)
774 map_type
= kmi
.map_type
775 row
.prop(kmi
, "map_type", text
="")
776 if map_type
== 'KEYBOARD':
777 row
.prop(kmi
, "type", text
="", full_event
=True)
778 elif map_type
== 'MOUSE':
779 row
.prop(kmi
, "type", text
="", full_event
=True)
780 elif map_type
== 'NDOF':
781 row
.prop(kmi
, "type", text
="", full_event
=True)
782 elif map_type
== 'TWEAK':
784 subrow
.prop(kmi
, "type", text
="")
785 subrow
.prop(kmi
, "value", text
="")
786 elif map_type
== 'TIMER':
787 row
.prop(kmi
, "type", text
="")
791 if (not kmi
.is_user_defined
) and kmi
.is_user_modified
:
792 ops
= row
.operator("gp.restore_keymap_item", text
="", icon
='BACK') # modified
793 ops
.km_name
= km
.name
794 ops
.kmi_name
= kmi
.idname
796 row
.label(text
='', icon
='BLANK1')
798 # Expanded, additional event settings
799 if kmi
.show_expanded
:
805 if map_type
not in {'TEXTINPUT', 'TIMER'}:
807 subrow
= sub
.row(align
=True)
809 if map_type
== 'KEYBOARD':
810 subrow
.prop(kmi
, "type", text
="", event
=True)
812 ## Hide value (Should always be Press)
813 # subrow.prop(kmi, "value", text="")
816 # subrow_repeat = subrow.row(align=True)
817 # subrow_repeat.active = kmi.value in {'ANY', 'PRESS'}
818 # subrow_repeat.prop(kmi, "repeat", text="Repeat")
820 elif map_type
in {'MOUSE', 'NDOF'}:
821 subrow
.prop(kmi
, "type", text
="")
822 subrow
.prop(kmi
, "value", text
="")
824 if map_type
in {'KEYBOARD', 'MOUSE'} and kmi
.value
== 'CLICK_DRAG':
826 subrow
.prop(kmi
, "direction")
830 subrow
.scale_x
= 0.75
831 subrow
.prop(kmi
, "any", toggle
=True)
832 if bpy
.app
.version
>= (3,0,0):
833 subrow
.prop(kmi
, "shift_ui", toggle
=True)
834 subrow
.prop(kmi
, "ctrl_ui", toggle
=True)
835 subrow
.prop(kmi
, "alt_ui", toggle
=True)
836 subrow
.prop(kmi
, "oskey_ui", text
="Cmd", toggle
=True)
838 subrow
.prop(kmi
, "shift", toggle
=True)
839 subrow
.prop(kmi
, "ctrl", toggle
=True)
840 subrow
.prop(kmi
, "alt", toggle
=True)
841 subrow
.prop(kmi
, "oskey", text
="Cmd", toggle
=True)
843 subrow
.prop(kmi
, "key_modifier", text
="", event
=True)
845 def draw_nav_pref(prefs
, layout
):
847 layout
.label(text
='Layer Navigator:')
849 col
= layout
.column()
851 row
.prop(prefs
, 'box_height')
852 row
.prop(prefs
, 'box_width')
855 row
.prop(prefs
, 'text_size')
856 row
.prop(prefs
, 'left_handed')
859 if not addon_keymaps
:
863 layout
.label(text
='Keymap:')
866 for akm
, akmi
in addon_keymaps
:
867 km
= bpy
.context
.window_manager
.keyconfigs
.user
.keymaps
.get(akm
.name
)
870 kmi
= km
.keymap_items
.get(akmi
.idname
)
874 draw_keymap_ui_custom(km
, kmi
, layout
)
875 # draw_kmi_custom(km, kmi, box)
880 def register_keymaps():
881 kc
= bpy
.context
.window_manager
.keyconfigs
.addon
885 km
= kc
.keymaps
.new(name
= "Grease Pencil", space_type
= "EMPTY", region_type
='WINDOW')
886 kmi
= km
.keymap_items
.new('gpencil.viewport_layer_nav_osd', type='Y', value
='PRESS')
888 addon_keymaps
.append((km
, kmi
))
890 def unregister_keymaps():
891 for km
, kmi
in addon_keymaps
:
892 km
.keymap_items
.remove(kmi
)
894 addon_keymaps
.clear()
897 GPT_OT_viewport_layer_nav_osd
,
902 bpy
.utils
.register_class(cls
)
907 for cls
in reversed(classes
):
908 bpy
.utils
.unregister_class(cls
)