forked from jayanam/bl_ui_widgets
-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathdemo_panel_op.py
509 lines (442 loc) · 21.6 KB
/
demo_panel_op.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
# ##### BEGIN GPL LICENSE BLOCK #####
#
# 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 2
# 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, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# --- ### Header
bl_info = {"name": "BL UI Widgets",
"description": "UI Widgets to draw in the 3D view",
"author": "Marcelo M. Marques (fork of Jayanam's original project)",
"version": (1, 0, 1),
"blender": (2, 80, 75),
"location": "View3D > viewport area",
"support": "COMMUNITY",
"category": "3D View",
"warning": "Version numbering diverges from Jayanam's original project",
"doc_url": "https://github.com/mmmrqs/bl_ui_widgets",
"tracker_url": "https://github.com/mmmrqs/bl_ui_widgets/issues"
}
# --- ### Change log
# v1.0.1 (09.20.2021) - by Marcelo M. Marques
# Chang: just some pep8 code formatting
# v1.0.0 (09.01.2021) - by Marcelo M. Marques
# Added: 'terminate_execution' function that can be overriden by programmer to command termination of the 'Floating Panel'.
# Added: New properties and functions to all widgets (check their corresponding modules for more information).
# --- ### Imports
import bpy
import os
from bpy.types import Operator
from .bl_ui_label import BL_UI_Label
from .bl_ui_patch import BL_UI_Patch
from .bl_ui_checkbox import BL_UI_Checkbox
from .bl_ui_slider import BL_UI_Slider
from .bl_ui_textbox import BL_UI_Textbox
from .bl_ui_button import BL_UI_Button
from .bl_ui_tooltip import BL_UI_Tooltip
from .bl_ui_draw_op import BL_UI_OT_draw_operator
from .bl_ui_drag_panel import BL_UI_Drag_Panel
class DP_OT_draw_operator(BL_UI_OT_draw_operator): # in: bl_ui_draw_op.py ##
bl_idname = "object.dp_ot_draw_operator"
bl_label = "bl ui widgets custom operator"
bl_description = "Operator for bl ui widgets"
bl_options = {'REGISTER'}
# -- Blender interface methods quick documentation --
# def poll: checked before running the operator, which will never run when poll fails.
# used to check if an operator can run, menu items will be greyed out and if key bindings should be ignored.
#
# def invoke: called by default when accessed from a key binding and menu, this takes the current context - mouse location.
# used for interactive operations such as dragging & drawing. (hint: think of this as "run by a person")
#
# def description: allows a dynamic tooltip that changes based on the context and operator parameters.
#
# def draw: called to draw options, giving control over the layout. Without this, options will draw in the order they are defined.
#
# def modal: handles events which would normally access other operators, they keep running until they return FINISHED.
# used for operators which continuously run, eg: fly mode, knife tool, circle select are all examples of modal operators.
#
# def execute: runs the operator, assuming values are set by the caller (else use defaults).
# used for undo/redo, and executing operators from Python.
#
# def cancel: called when Blender cancels a modal operator, not used often. Internal cleanup can be done here if needed.
# --- methods
@classmethod
def poll(cls, context):
# Show this panel in View_3D only
return (context.space_data.type == 'VIEW_3D')
def __init__(self):
super().__init__()
if __package__.find(".") != -1:
package = __package__[0:__package__.find(".")]
else:
package = __package__
# From Preferences/Themes/"Text Style"
theme = bpy.context.preferences.themes[0]
ui = theme.user_interface
widget_style = getattr(ui, "wcol_state")
status_color = tuple(widget_style.inner_changed) + (0.3,)
btnC = 0 # Element counter
btnS = 4 # Button separation (for the smaller ones)
btnG = 0 # Button gap (for the bigger ones)
btnW = 56 # Button width
btnH = 40 + btnS # Button height (takes 2 small buttons plus their separation)
marginX = 16 # Margin from left border
marginY = 27 # Margin from top border
btnX = marginX + 1 # Button position X (for the very first button)
btnY = marginY + 1 # Button position Y (for the very first button)
self.button1 = BL_UI_Button(btnX, btnY, btnW, btnH)
self.button1.style = 'RADIO'
self.button1.text = "PUSH"
self.button1.textwo = "-ME-"
self.button1.text_size = 13
self.button1.textwo_size = 10
self.button1.rounded_corners = (1, 1, 0, 0)
self.button1.set_mouse_up(self.button1_click)
self.button1.set_button_pressed(self.button1_pressed)
self.button1.description = "Press this button to unlock its brothers (after the button" + \
"with a 'LOCK' caption has been pressed). {Let me " + \
"type a very large description here to showcase a " + \
"tooltip text automatically being wrapped around " + \
"multiple lines inside its containing box}. Whatever " + \
"comes beyond the defined limit of maximum lines " + \
"will be left out! You'll see this text will be truncated..."
self.button1.shortcut = "Shortcut: None"
self.button1.python_cmd = "bpy.ops.object.dp_ot_draw_operator.button1_click()"
if self.button1_pressed(self.button1):
self.button1.state = 3
btnC += 1
#
self.button2 = BL_UI_Button((btnX + ((btnW - 1 + btnG) * btnC)), btnY, btnW, btnH)
self.button2.style = 'RADIO'
self.button2.text = "HELP"
self.button2.textwo = "(Go)"
self.button2.text_size = 13
self.button2.textwo_size = 10
self.button2.rounded_corners = (0, 0, 0, 0)
self.button2.set_mouse_up(self.button2_click)
self.button2.set_button_pressed(self.button2_pressed)
self.button2.description = "You can help me by doing as follows:\n" +\
" -Press button 4 to Disable Me\n" +\
" -Press button 1 to Enable Me"
self.button2.python_cmd = "bpy.ops.object.dp_ot_draw_operator.button2_click()"
if self.button2_pressed(self.button2):
self.button2.state = 3
btnC += 1
#
self.button3 = BL_UI_Button((btnX + ((btnW - 1 + btnG) * btnC)), btnY, btnW, btnH)
self.button3.style = 'RADIO'
self.button3.text = "ADD"
self.button3.text_size = 13
self.button3.rounded_corners = (0, 0, 0, 0)
self.button3.set_mouse_up(self.button3_click)
self.button3.set_button_pressed(self.button3_pressed)
self.button3.description = "Adds one little 'MONKEY' object to 3D View area"
self.button3.python_cmd = "bpy.ops.object.dp_ot_draw_operator.button3_click()"
if self.button3_pressed(self.button3):
self.button3.state = 3
btnC += 1
#
self.button4 = BL_UI_Button((btnX + ((btnW - 1 + btnG) * btnC)), btnY, btnW, btnH)
self.button4.style = 'RADIO'
self.button4.text = "LOCK"
self.button4.textwo = "(CTRL-L)"
self.button4.text_size = 13
self.button4.textwo_size = 10
self.button4.rounded_corners = (0, 0, 1, 1)
self.button4.set_mouse_up(self.button4_click)
self.button4.set_button_pressed(self.button4_pressed)
self.button4.enabled = (not bpy.context.scene.var.OpState6)
self.button4.description = "This button does nothing more than to disable 2 and 3.\n" +\
"Note: CTRL-L does not actually work as a short-cut, but " +\
"you could have programmed it to do so"
# In this example there is no .python_cmd populated, thus only the main description appears in the tooltip
if self.button4_pressed(self.button4):
self.button4.state = 3
btnC += 1
oldX = (btnX + ((btnW - 1 + btnG) * btnC))
oldH = btnH
oldW = btnW
newX = oldX + marginX + btnW - 1 + btnS
btnW = 96
btnH = 20
#
self.button5 = BL_UI_Button(newX, btnY, btnW, btnH)
self.button5.text = "Do Nothing"
newY = btnY + btnH + btnS
#
self.button6 = BL_UI_Button(newX, newY, btnW, btnH)
self.button6.selected_color = status_color
self.button6.text = "Switch Btn 4"
self.button6.set_mouse_up(self.button6_click)
self.button6.set_button_pressed(self.button6_pressed)
self.button6.description = "Switches button 4's state"
if self.button6_pressed(self.button6):
self.button6.state = 3
#
newX = newX + btnW - 1 + btnS
btnW = 120
#
self.number1 = BL_UI_Slider(newX, btnY, btnW, btnH)
self.number1.style = 'NUMBER_CLICK'
self.number1.value = 500
self.number1.step = 100
self.number1.unit = "m"
self.number1.precision = 0
self.number1.description = "This is a click slider component and it can work with a full range of values"
self.number1.set_value_updated(self.number1_update)
#
self.slider1 = BL_UI_Slider(newX, newY, btnW, btnH)
self.slider1.style = 'NUMBER_SLIDE'
self.slider1.text = "Z Rot"
self.slider1.value = 180
self.slider1.min = 0
self.slider1.max = 360
self.slider1.description = "This is a standard slider component with a 0 to 100 sliding percent bar.\nYou can use it to rotate the selected object(s) in the scene"
self.slider1.set_value_updated(self.slider1_update)
#
self.objname = "<Press the ADD button so you can edit here>"
self.textbox1 = BL_UI_Textbox(btnX, newY + 35, 350, btnH)
self.textbox1.text = self.objname
self.textbox1.max_input_chars = 50
self.textbox1.description = "Textbox editing entry field"
self.textbox1.set_value_changed(self.textbox1_changed)
self.textbox1.enabled = False
#
self.check1 = BL_UI_Checkbox(newX, newY + 37, btnW, btnH)
self.check1.text = "Unregit"
self.check1.set_value_changed(self.check1_changed)
self.check1.description = "This checkbox accesses an 'UNREGISTER' fully circular button"
self.check1.python_cmd = "bpy.ops.object.dp_ot_draw_operator.check1_changed()"
self.check1.is_checked = False
# -----------
panW = newX + btnW + 2 + marginX # Panel desired width (beware: this math is good for my setup only)
panH = newY + btnH + 0 + 10 + 35 # Panel desired height (ditto)
# Save the panel's size to preferences properties to be used in there
bpy.context.preferences.addons[package].preferences.RC_PAN_W = panW
bpy.context.preferences.addons[package].preferences.RC_PAN_H = panH
# Need this just because I want the panel to be centered
if bpy.context.preferences.addons[package].preferences.RC_UI_BIND:
# From Preferences/Interface/"Display"
ui_scale = bpy.context.preferences.view.ui_scale
else:
ui_scale = 1
over_scale = bpy.context.preferences.addons[package].preferences.RC_SCALE
# The panel X and Y coords are in relation to the bottom-left corner of the 3D viewport area
panX = int((bpy.context.area.width - panW * ui_scale * over_scale) / 2.0) + 1 # Panel X coordinate, for panel's top-left corner
panY = panH + 100 - 1 # The '100' is just a spacing # Panel Y coordinate, for panel's top-left corner
self.panel = BL_UI_Drag_Panel(panX, panY, panW, panH)
self.panel.style = 'PANEL' # Options are: {HEADER,PANEL,SUBPANEL,TOOLTIP,NONE}
self.tooltip = BL_UI_Tooltip() # This is for displaying the widgets tooltips. Only need one instance!
self.patch1 = BL_UI_Patch(0, 0, panW, 17)
self.patch1.style = 'HEADER'
self.patch1.set_mouse_move(self.patch1_mouse_move)
self.label1 = BL_UI_Label(5, 12, panW, 17)
self.label1.style = "TITLE"
self.label1.text = "Panel Title For Example"
self.label1.size = 12
self.label2 = BL_UI_Label(panW - 100, 12, 100, 17)
self.label2.text = "" # This empty text label will be used later on to display some dynamic values on the panel
self.patch2 = BL_UI_Patch(oldX + 10, btnY, oldW, oldH)
self.patch2.bg_color = (0, 0, 0, 0)
self.patch2.outline_color = (1, 1, 1, 0.4)
self.patch2.roundness = 0.4
self.patch2.corner_radius = 10
self.patch2.shadow = True
self.patch2.rounded_corners = (1, 1, 1, 1)
self.patch2.description = "This could be any image (size, color, transparent etc)\n" + \
"and the cool part is that it scales together with the panel"
script_file = os.path.realpath(__file__)
directory = os.path.dirname(script_file)
imagePath = directory + "\\img\\rotate.png"
self.patch2.set_image(imagePath)
self.patch2.set_image_size((32, 32))
self.patch2.set_image_position((11, 5))
# -----------
# Display an 'UNRegister' button on screen
# ==================================================================
# -- This is just for demonstration, not to be used in production
# ==================================================================
btnH = 32
newX = panW - marginX - btnH - 2
newY = panH - btnH - 6
self.buttonU = BL_UI_Button(newX, newY, btnH, btnH)
self.buttonU.text = "UNR"
self.buttonU.text_size = 12
self.buttonU.text_color = (1, 1, 1, 1)
self.buttonU.bg_color = (0.5, 0, 0, 1)
self.buttonU.outline_color = (0.6, 0.6, 0.6, 0.8)
self.buttonU.corner_radius = btnH / 2 - 1
self.buttonU.roundness = 1.0
self.buttonU.set_mouse_up(self.buttonU_click)
self.buttonU.description = "Unregisters the Remote Control panel object and closes it"
self.buttonU.python_cmd = "bpy.ops.object.dp_ot_draw_operator.buttonU_click()"
self.buttonU.visible = False
def on_invoke(self, context, event):
# Add your widgets here (TODO: perhaps a better, more automated solution?)
# --------------------------------------------------------------------------------------------------
widgets_panel = [self.panel
]
widgets_items = [self.patch1, self.patch2, self.label1, self.label2,
self.button1, self.button2, self.button3, self.button4, self.button5, self.button6,
self.buttonU, self.slider1, self.number1, self.check1, self.textbox1,
self.tooltip, # <-- If there is a tooltip object, it must be the last in this list
]
# --------------------------------------------------------------------------------------------------
widgets = widgets_panel + widgets_items
self.init_widgets(context, widgets)
self.panel.add_widgets(widgets_items)
self.panel.set_location(self.panel.x, self.panel.y)
# -- Helper function
def terminate_execution(self):
'''
This is a special case 'overriding function' to allow subclass control for terminating/closing the panel.
Function is defined in class BL_UI_OT_draw_operator (bl_ui_draw_op.py) and available to be inherited here.
If not included here the function in the superclass just returns 'False' and no termination is executed.
When 'True" is returned below, the execution is auto terminated and the 'Remote Control' panel closes itself.
'''
return (not bpy.context.scene.var.RemoVisible)
# -- Button press handlers
def button1_click(self, widget, event, x, y):
self.button2.enabled = True
self.button3.enabled = True
self.press_only(1)
def button2_click(self, widget, event, x, y):
self.press_only(2)
def button3_click(self, widget, event, x, y):
bpy.ops.mesh.primitive_monkey_add()
self.objname = bpy.context.object.name
self.textbox1.text = "'" + self.objname + "' is her name, but you can edit it here!"
self.textbox1.enabled = True
self.press_only(3)
def button4_click(self, widget, event, x, y):
self.button2.enabled = False
self.button3.enabled = False
self.press_only(4)
# I am not even obligated to create any of these functions, see?
# button5 does not have an active function tied to it at all.
#
# def button5_click(self, widget, event, x, y):
# # Miss Me
def button6_click(self, widget, event, x, y):
var = bpy.context.scene.var
var.OpState6 = (not var.OpState6)
self.button4.enabled = (not self.button4.enabled)
def buttonU_click(self, widget, event, x, y):
self.finish()
def button1_pressed(self, widget):
return (bpy.context.scene.var.OpState1)
def button2_pressed(self, widget):
return (bpy.context.scene.var.OpState2)
def button3_pressed(self, widget):
return (bpy.context.scene.var.OpState3)
def button4_pressed(self, widget):
return (bpy.context.scene.var.OpState4)
# I am not even obligated to create any of these functions, see?
# button5 does not have an active function tied to it at all.
#
# def button5_pressed(self, widget):
# return (bpy.context.scene.var.OpState5)
def button6_pressed(self, widget):
return (bpy.context.scene.var.OpState6)
def patch1_mouse_move(self, widget, event, x, y):
self.label2.text = "x: " + str(x) + " y: " + str(y)
return False
def number1_update(self, widget, value):
# Example of a dynamic unit conversion with dynamic min/max limits
converted = False
if widget.unit == "mm" and value >= 1000:
# Upscale to meters
value = value / 1000
widget.unit = "m"
widget.step = 10
widget.precision = 0
converted = True
if widget.unit == "m" and value >= 1000:
# Upscale to kilometers
value = value / 1000
widget.unit = "km"
widget.step = 0.1
widget.precision = 1
converted = True
if widget.unit == "km" and value >= 10:
# I want my hardcoded max limit to be 10 km
value = 10
converted = True
if widget.unit == "km" and value < 1:
# Downscale to meters
value = value * 1000
widget.unit = "m"
widget.step = 10
widget.precision = 0
converted = True
if widget.unit == "m" and value < 1:
# Downscale to millimeters
value = value * 1000
widget.unit = "mm"
widget.step = 10
widget.precision = 0
converted = True
if widget.unit == "mm" and value < 1:
# I want my hardcoded min limit to be 1 mm
value = 1
converted = True
if converted:
widget.value = round(value, widget.precision)
return False
else:
# By returning True the 'value' argument will be committed to the widget.value property
return True
def slider1_update(self, widget, value):
import math
try:
for obj in bpy.context.selected_objects:
if obj.type == 'MESH':
obj.rotation_euler[2] = math.radians(value)
except Exception as e:
pass
return True
def textbox1_changed(self, widget, context, former_text, updated_text):
# This is just an example done in a rush, so not much thinking and probably with bugs ;-)
if updated_text != self.objname:
if updated_text.strip() == "":
self.objname = "<Why have you tried to blank her name?>"
widget.text = self.objname
return True
try:
for obj in bpy.data.objects:
if obj.type == 'MESH' and obj.name == self.objname:
obj.name = updated_text
self.objname = obj.name
break
except Exception as e:
self.objname = "<Error trying to assign this name to object>"
widget.text = self.objname
return True
def check1_changed(self, widget, event, x, y):
self.buttonU.visible = not self.check1.is_checked
return True
# -- Helper functions
def press_only(self, button):
var = bpy.context.scene.var
var.OpState1 = (button == 1)
var.OpState2 = (button == 2)
var.OpState3 = (button == 3)
var.OpState4 = (button == 4)
# -Register/unregister processes
def register():
bpy.utils.register_class(DP_OT_draw_operator)
def unregister():
bpy.utils.unregister_class(DP_OT_draw_operator)
if __name__ == '__main__':
register()