GPencil Tools: Optimize Undo for Rotate Canvas
[blender-addons.git] / greasepencil_tools / layer_navigator.py
blobc1d3338297c8c9e9fcc8844e654ce9b3b8bb2fef
1 # SPDX-FileCopyrightText: 2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 import bpy
6 import blf, gpu
7 import math
8 from gpu_extras.batch import batch_for_shader
9 from mathutils import Vector, Matrix
10 from time import time
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'''
21 return [
22 # tri 1
23 quad_list[0],
24 quad_list[1],
25 quad_list[2],
26 # tri 2
27 quad_list[0],
28 quad_list[3],
29 quad_list[2]
32 def round_to_ceil_even(f):
33 if (math.floor(f) % 2 == 0):
34 return math.floor(f)
35 else:
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]
40 move = idx - a
41 if move == 0:
42 return
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
59 else:
60 down_margin += tool_header.height
62 ## set corner values
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:
71 return
72 font_id = 0
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)
83 shader.bind()
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)
90 rects = []
91 lock_rects = []
92 opacitys = []
93 opacity_bars = []
94 active_case = []
95 active_width = float(round_to_ceil_even(4.0 * context.preferences.system.ui_scale))
97 ## tex icon store
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))
104 if i == self.ui_idx:
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)]]
115 # Build offset table
116 px_offset = int(active_width / 2)
117 case_px_offsets = [
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)))
133 if l.lock:
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])
138 else:
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])
145 if l.hide:
146 icons['hide_on'].append([v + hide_coord for v in self.icon_tex_coord])
147 else:
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]
155 ## opacity sliders
156 if l.opacity:
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)
168 ## locked squares
169 shader.uniform_float("color", self.lock_color)
170 batch_lock = batch_for_shader(shader, 'TRIS', {"pos": lock_rects})
171 batch_lock.draw(shader)
173 ## bg_full_bar
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)
178 ## opacity sliders
179 shader.uniform_float("color", self.opacity_color)
180 batch_lock = batch_for_shader(shader, 'TRIS', {"pos": opacitys})
181 batch_lock.draw(shader)
183 ### --- Trace Lines
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)
190 ## "Plus" lines
191 if self.gpl.active_index == 0:
192 plus_lines = self.plus_lines[:8]
193 else:
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',
207 "pos": coords,
208 "texCoord": ((0, 0), (1, 0), (1, 1), (0, 1)),
211 shader_tex.bind()
212 shader_tex.uniform_sampler("image", texture)
213 batch_icons.draw(shader_tex)
215 ## Highlight active layer
216 if active_case:
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')
227 ### --- Texts
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)
236 if l.hide:
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
241 else:
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)
250 ## Drag text
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}')
257 else:
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'}
267 @classmethod
268 def poll(cls, context):
269 return context.object is not None and context.object.type == 'GPENCIL'
271 lapse = 0
272 text = ''
273 color = ''
274 ct = 0
276 use_fade = False
277 fade_value = 0.15
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)
293 if not img:
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
297 return img
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
311 self.pressed = False
312 # self.click_time = 0
313 self.id_src = self.click_src = None
315 ## Structure:
317 # --- <-- top
318 # | |
319 # ---
320 # | |
321 # ---
322 # | | <-- bottom_base
323 # --- <-- bottom
325 max_w = self.px_w + self.add_box
326 mid_square = int(self.px_w / 2)
327 ## define zones
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)
334 if self.left_handed:
335 self.left = self.init_mouse.x - int(self.px_w / 10)
336 else:
337 # right hand
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
345 if over_right > 0:
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
351 if over_left > 0:
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)
358 self.lines = []
359 # self.texts = []
360 self.text_pos = []
361 self.ranges = []
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))
372 ## add boxes
373 box = [
374 Vector((0, 0)),
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)
385 plus = [
386 Vector((mid, marg)), Vector((mid, marg + plus_length)),
387 Vector((marg, mid)), Vector((marg + plus_length, mid)),
390 self.plus_lines = []
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)
402 self.case = [
403 Vector((0, 0)),
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)),
417 ## Add contour lines
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")
432 return {'CANCELLED'}
434 self.key = event.type
435 self.mouse = self.init_mouse = Vector((event.mouse_region_x, event.mouse_region_y))
437 ## Define UI
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 = (
444 Vector((0, 0)),
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)
466 if ret is not None:
467 return ret
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
486 self.use_fade=True
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
491 self.use_fade=False
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
498 if self.use_fade:
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:
506 return i
507 # Return min if below and max if above instead of None
508 return i
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'''
515 ## check "add" zone
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:
521 # add layer
522 nl = context.object.data.layers.new('GP_Layer')
523 nl.frames.new(context.scene.frame_current, active=True)
524 nl.use_lights = False
525 if i == 0:
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
532 ## Reset pop-up
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))
535 self.setup(context)
536 return False
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:
542 lock_col = True
543 elif self.right - self.icons_margin_b <= self.mouse.x <= self.right - self.icons_margin_b + self.icon_size:
544 hide_col = True
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:
551 if hide_col:
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'
554 elif lock_col:
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'
557 return False
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' !
566 # new_time = time()
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)
570 # return True
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:
578 ## On opacity slider
579 self.drag_text = 'opacity_level'
580 self.drag_mode = 'opacity'
581 self.org_opacity = self.gpl[self.id_src].opacity
582 else:
583 ## on layer
584 self.drag_text = self.gpl[self.id_src].info
585 self.drag_mode = 'layer'
586 return False
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
596 return {'CANCELLED'}
598 if event.type == self.key and event.value == 'RELEASE':
599 self.stop_mod(context)
600 return {'FINISHED'}
602 # Toggle Xray
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':
608 # if self.use_fade:
609 # self.stop_fade(context)
610 # else:
611 # self.set_fade(context)
613 if event.type == 'LEFTMOUSE' and event.value == 'PRESS':
614 self.pressed = True
615 stop = self.click(context)
616 if stop:
617 self.stop_mod(context)
618 return {'FINISHED'}
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:
626 self.dragging = True
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
635 if self.dragging:
636 if self.drag_mode == 'layer' and self.id_src is not None:
637 # move layer
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())
640 self.id_src = None
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:
648 if self.use_fade:
649 self.stop_fade(context)
650 else:
651 if not self.use_fade:
652 self.set_fade(context)
654 ## Swap autolock
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:
668 self.ui_idx = i
669 break
671 if self.ui_idx == current_idx:
672 return {'RUNNING_MODAL'}
673 else:
674 context.object.data.layers.active_index = self.ui_idx
675 if self.drag_mode:
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):
692 # restore fade
693 if self.use_fade:
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):
704 # sizes
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",
709 default=30,
710 min=10,
711 max=200,
712 soft_min=26,
713 soft_max=120,
714 step=1,
715 subtype='PIXEL')
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",
721 default=250,
722 min=120,
723 max=500,
724 soft_min=150,
725 soft_max=350,
726 step=1,
727 subtype='PIXEL')
729 text_size: IntProperty(
730 name="Label Size",
731 description="Layer name label size",
732 default=12,
733 min=4,
734 max=40,
735 soft_min=8,
736 soft_max=20,
737 step=1,
738 subtype='PIXEL')
740 left_handed: BoolProperty(
741 name='Left Handed',
742 description="Pop-up appear offseted at the right of the mouse pointer\
743 \nto avoif hand occluding layer label",
744 default=False)
746 def _indented_layout(layout, level):
747 indentpx = 16
748 if level == 0:
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)
753 col = split.column()
754 col = split.column()
755 return col
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)
762 box = col.box()
763 else:
764 box = col.column()
766 split = box.split()
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)
773 row = split.row()
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':
783 subrow = row.row()
784 subrow.prop(kmi, "type", text="")
785 subrow.prop(kmi, "value", text="")
786 elif map_type == 'TIMER':
787 row.prop(kmi, "type", text="")
788 else:
789 row.label()
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
795 else:
796 row.label(text='', icon='BLANK1')
798 # Expanded, additional event settings
799 if kmi.show_expanded:
800 col = col.column()
801 box = col.box()
803 split = box.column()
805 if map_type not in {'TEXTINPUT', 'TIMER'}:
806 sub = split.column()
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="")
815 ## Hide repeat
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':
825 subrow = sub.row()
826 subrow.prop(kmi, "direction")
828 sub = box.column()
829 subrow = sub.row()
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)
837 else:
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):
846 # - General settings
847 layout.label(text='Layer Navigator:')
849 col = layout.column()
850 row = col.row()
851 row.prop(prefs, 'box_height')
852 row.prop(prefs, 'box_width')
854 row = col.row()
855 row.prop(prefs, 'text_size')
856 row.prop(prefs, 'left_handed')
858 # -/ Keymap -
859 if not addon_keymaps:
860 return
862 layout.separator()
863 layout.label(text='Keymap:')
866 for akm, akmi in addon_keymaps:
867 km = bpy.context.window_manager.keyconfigs.user.keymaps.get(akm.name)
868 if not km:
869 continue
870 kmi = km.keymap_items.get(akmi.idname)
871 if not kmi:
872 continue
874 draw_keymap_ui_custom(km, kmi, layout)
875 # draw_kmi_custom(km, kmi, box)
878 addon_keymaps = []
880 def register_keymaps():
881 kc = bpy.context.window_manager.keyconfigs.addon
882 if kc is None:
883 return
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')
887 kmi.repeat = False
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()
896 classes = (
897 GPT_OT_viewport_layer_nav_osd,
900 def register():
901 for cls in classes:
902 bpy.utils.register_class(cls)
903 register_keymaps()
905 def unregister():
906 unregister_keymaps()
907 for cls in reversed(classes):
908 bpy.utils.unregister_class(cls)