1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
20 from blenderkit
import paths
, rerequests
, image_utils
23 from mathutils
import Vector
32 bk_logger
= logging
.getLogger('blenderkit')
34 ABOVE_NORMAL_PRIORITY_CLASS
= 0x00008000
35 BELOW_NORMAL_PRIORITY_CLASS
= 0x00004000
36 HIGH_PRIORITY_CLASS
= 0x00000080
37 IDLE_PRIORITY_CLASS
= 0x00000040
38 NORMAL_PRIORITY_CLASS
= 0x00000020
39 REALTIME_PRIORITY_CLASS
= 0x00000100
42 def experimental_enabled():
43 preferences
= bpy
.context
.preferences
.addons
['blenderkit'].preferences
44 return preferences
.experimental_features
47 def get_process_flags():
48 flags
= BELOW_NORMAL_PRIORITY_CLASS
49 if sys
.platform
!= 'win32': # TODO test this on windows
55 bpy
.ops
.object.select_all(action
='DESELECT')
57 bpy
.context
.view_layer
.objects
.active
= ob
61 aob
= bpy
.context
.view_layer
.objects
.active
62 selobs
= bpy
.context
.view_layer
.objects
.selected
[:]
66 def selection_set(sel
):
67 bpy
.ops
.object.select_all(action
='DESELECT')
68 bpy
.context
.view_layer
.objects
.active
= sel
[0]
73 def get_active_model():
74 if bpy
.context
.view_layer
.objects
.active
is not None:
75 ob
= bpy
.context
.view_layer
.objects
.active
76 while ob
.parent
is not None:
83 scene
= bpy
.context
.scene
84 ui_props
= scene
.blenderkitUI
85 image
= ui_props
.hdr_upload_image
89 def get_selected_models():
91 Detect all hierarchies that contain asset data from selection. Only parents that have actual ['asset data'] get returned
93 list of objects containing asset data.
96 obs
= bpy
.context
.selected_objects
[:]
101 while ob
.parent
is not None and ob
not in done
and ob
.blenderkit
.asset_base_id
== '' and ob
.instance_collection
is None:
105 if ob
not in parents
and ob
not in done
:
106 if ob
.blenderkit
.name
!= '' or ob
.instance_collection
is not None:
110 # if no blenderkit - like objects were found, use the original selection.
111 if len(parents
) == 0:
116 def get_selected_replace_adepts():
118 Detect all hierarchies that contain either asset data from selection, or selected objects themselves.
120 list of objects for replacement.
123 obs
= bpy
.context
.selected_objects
[:]
126 for selected_ob
in obs
:
129 while ob
.parent
is not None and ob
not in done
and ob
.blenderkit
.asset_base_id
== '' and ob
.instance_collection
is None:
131 # print('step,',ob.name)
134 # print('fin', ob.name)
135 if ob
not in parents
and ob
not in done
:
136 if ob
.blenderkit
.name
!= '' or ob
.instance_collection
is not None:
141 # if no blenderkit - like objects were found, use the original selection.
142 if len(parents
) == 0:
144 pprint('replace adepts')
149 def get_search_props():
150 scene
= bpy
.context
.scene
153 uiprops
= scene
.blenderkitUI
155 if uiprops
.asset_type
== 'MODEL':
156 if not hasattr(scene
, 'blenderkit_models'):
158 props
= scene
.blenderkit_models
159 if uiprops
.asset_type
== 'SCENE':
160 if not hasattr(scene
, 'blenderkit_scene'):
162 props
= scene
.blenderkit_scene
163 if uiprops
.asset_type
== 'HDR':
164 if not hasattr(scene
, 'blenderkit_HDR'):
166 props
= scene
.blenderkit_HDR
167 if uiprops
.asset_type
== 'MATERIAL':
168 if not hasattr(scene
, 'blenderkit_mat'):
170 props
= scene
.blenderkit_mat
172 if uiprops
.asset_type
== 'TEXTURE':
173 if not hasattr(scene
, 'blenderkit_tex'):
175 # props = scene.blenderkit_tex
177 if uiprops
.asset_type
== 'BRUSH':
178 if not hasattr(scene
, 'blenderkit_brush'):
180 props
= scene
.blenderkit_brush
184 def get_active_asset():
185 scene
= bpy
.context
.scene
186 ui_props
= scene
.blenderkitUI
187 if ui_props
.asset_type
== 'MODEL':
188 if bpy
.context
.view_layer
.objects
.active
is not None:
189 ob
= get_active_model()
191 if ui_props
.asset_type
== 'SCENE':
192 return bpy
.context
.scene
193 if ui_props
.asset_type
== 'HDR':
194 return get_active_HDR()
195 elif ui_props
.asset_type
== 'MATERIAL':
196 if bpy
.context
.view_layer
.objects
.active
is not None and bpy
.context
.active_object
.active_material
is not None:
197 return bpy
.context
.active_object
.active_material
198 elif ui_props
.asset_type
== 'TEXTURE':
200 elif ui_props
.asset_type
== 'BRUSH':
201 b
= get_active_brush()
207 def get_upload_props():
208 scene
= bpy
.context
.scene
209 ui_props
= scene
.blenderkitUI
210 if ui_props
.asset_type
== 'MODEL':
211 if bpy
.context
.view_layer
.objects
.active
is not None:
212 ob
= get_active_model()
214 if ui_props
.asset_type
== 'SCENE':
215 s
= bpy
.context
.scene
217 if ui_props
.asset_type
== 'HDR':
219 hdr
= ui_props
.hdr_upload_image
# bpy.data.images.get(ui_props.hdr_upload_image)
222 return hdr
.blenderkit
223 elif ui_props
.asset_type
== 'MATERIAL':
224 if bpy
.context
.view_layer
.objects
.active
is not None and bpy
.context
.active_object
.active_material
is not None:
225 return bpy
.context
.active_object
.active_material
.blenderkit
226 elif ui_props
.asset_type
== 'TEXTURE':
228 elif ui_props
.asset_type
== 'BRUSH':
229 b
= get_active_brush()
235 def previmg_name(index
, fullsize
=False):
237 return '.bkit_preview_' + str(index
).zfill(3)
239 return '.bkit_preview_full_' + str(index
).zfill(3)
242 def get_active_brush():
243 context
= bpy
.context
245 if context
.sculpt_object
:
246 brush
= context
.tool_settings
.sculpt
.brush
247 elif context
.image_paint_object
: # could be just else, but for future possible more types...
248 brush
= context
.tool_settings
.image_paint
.brush
253 user_preferences
= bpy
.context
.preferences
.addons
['blenderkit'].preferences
254 # if user_preferences.api_key == '':
255 fpath
= paths
.BLENDERKIT_SETTINGS_FILENAME
256 if os
.path
.exists(fpath
):
258 with
open(fpath
, 'r', encoding
='utf-8') as s
:
260 user_preferences
.api_key
= prefs
.get('API_key', '')
261 user_preferences
.global_dir
= prefs
.get('global_dir', paths
.default_global_dict())
262 user_preferences
.api_key_refresh
= prefs
.get('API_key_refresh', '')
263 except Exception as e
:
264 print('failed to read addon preferences.')
269 def save_prefs(self
, context
):
270 # first check context, so we don't do this on registration or blender startup
271 if not bpy
.app
.background
: # (hasattr kills blender)
272 user_preferences
= bpy
.context
.preferences
.addons
['blenderkit'].preferences
273 # we test the api key for length, so not a random accidentally typed sequence gets saved.
274 lk
= len(user_preferences
.api_key
)
276 # reset the api key in case the user writes some nonsense, e.g. a search string instead of the Key
277 user_preferences
.api_key
= ''
278 props
= get_search_props()
279 props
.report
= 'Login failed. Please paste a correct API Key.'
282 'API_key': user_preferences
.api_key
,
283 'API_key_refresh': user_preferences
.api_key_refresh
,
284 'global_dir': user_preferences
.global_dir
,
287 fpath
= paths
.BLENDERKIT_SETTINGS_FILENAME
288 if not os
.path
.exists(paths
._presets
):
289 os
.makedirs(paths
._presets
)
290 with
open(fpath
, 'w', encoding
='utf-8') as s
:
291 json
.dump(prefs
, s
, ensure_ascii
=False, indent
=4)
292 except Exception as e
:
296 def uploadable_asset_poll():
297 '''returns true if active asset type can be uploaded'''
298 ui_props
= bpy
.context
.scene
.blenderkitUI
299 if ui_props
.asset_type
== 'MODEL':
300 return bpy
.context
.view_layer
.objects
.active
is not None
301 if ui_props
.asset_type
== 'MATERIAL':
302 return bpy
.context
.view_layer
.objects
.active
is not None and bpy
.context
.active_object
.active_material
is not None
303 if ui_props
.asset_type
== 'HDR':
304 return ui_props
.hdr_upload_image
is not None
308 def get_hidden_texture(img
, force_reload
=False):
309 # i = get_hidden_image(tpath, bdata_name, force_reload=force_reload)
310 # bdata_name = f".{bdata_name}"
311 t
= bpy
.data
.textures
.get(img
.name
)
313 t
= bpy
.data
.textures
.new(img
.name
, 'IMAGE')
319 def get_hidden_image(tpath
, bdata_name
, force_reload
=False, colorspace
='sRGB'):
320 if bdata_name
[0] == '.':
321 hidden_name
= bdata_name
323 hidden_name
= '.%s' % bdata_name
324 img
= bpy
.data
.images
.get(hidden_name
)
326 if tpath
.startswith('//'):
327 tpath
= bpy
.path
.abspath(tpath
)
329 if img
== None or (img
.filepath
!= tpath
):
330 if tpath
.startswith('//'):
331 tpath
= bpy
.path
.abspath(tpath
)
332 if not os
.path
.exists(tpath
) or os
.path
.isdir(tpath
):
333 tpath
= paths
.get_addon_thumbnail_path('thumbnail_notready.jpg')
336 img
= bpy
.data
.images
.load(tpath
)
337 img
.name
= hidden_name
339 if img
.filepath
!= tpath
:
340 if img
.packed_file
is not None:
341 img
.unpack(method
='USE_ORIGINAL')
345 image_utils
.set_colorspace(img
, colorspace
)
348 if img
.packed_file
is not None:
349 img
.unpack(method
='USE_ORIGINAL')
351 image_utils
.set_colorspace(img
, colorspace
)
355 def get_thumbnail(name
):
356 p
= paths
.get_addon_thumbnail_path(name
)
358 img
= bpy
.data
.images
.get(name
)
360 img
= bpy
.data
.images
.load(p
)
361 image_utils
.set_colorspace(img
, 'sRGB')
368 def files_size_to_text(size
):
369 fsmb
= size
// (1024 * 1024)
374 return f
'{fsmb}MB {fskb}KB'
377 def get_brush_props(context
):
378 brush
= get_active_brush()
379 if brush
is not None:
380 return brush
.blenderkit
384 def p(text
, text1
='', text2
='', text3
='', text4
='', text5
='', level
='DEBUG'):
385 '''debug printing depending on blender's debug value'''
387 if 1: # bpy.app.debug_value != 0:
388 # print('-----BKit debug-----\n')
389 # traceback.print_stack()
390 texts
= [text1
, text2
, text3
, text4
, text5
]
396 bk_logger
.debug(text
)
397 # print('---------------------\n')
400 def copy_asset(fp1
, fp2
):
401 '''synchronizes the asset between folders, including it's texture subdirectories'''
403 bk_logger
.debug('copy asset')
404 bk_logger
.debug(fp1
+ ' ' + fp2
)
405 if not os
.path
.exists(fp2
):
406 shutil
.copyfile(fp1
, fp2
)
407 bk_logger
.debug('copied')
408 source_dir
= os
.path
.dirname(fp1
)
409 target_dir
= os
.path
.dirname(fp2
)
410 for subdir
in os
.scandir(source_dir
):
411 if not subdir
.is_dir():
413 target_subdir
= os
.path
.join(target_dir
, subdir
.name
)
414 if os
.path
.exists(target_subdir
):
416 bk_logger
.debug(str(subdir
) + ' ' + str(target_subdir
))
417 shutil
.copytree(subdir
, target_subdir
)
418 bk_logger
.debug('copied')
420 # except Exception as e:
421 # print('BlenderKit failed to copy asset')
426 def pprint(data
, data1
=None, data2
=None, data3
=None, data4
=None):
427 '''pretty print jsons'''
428 p(json
.dumps(data
, indent
=4, sort_keys
=True))
431 def get_hierarchy(ob
):
432 '''get all objects in a tree'''
435 # pprint('get hierarchy')
437 while len(doobs
) > 0:
439 doobs
.extend(o
.children
)
444 def select_hierarchy(ob
, state
=True):
445 obs
= get_hierarchy(ob
)
451 def delete_hierarchy(ob
):
452 obs
= get_hierarchy(ob
)
453 bpy
.ops
.object.delete({"selected_objects": obs
})
456 def get_bounds_snappable(obs
, use_modifiers
=False):
457 # progress('getting bounds of object(s)')
459 while parent
.parent
is not None:
460 parent
= parent
.parent
461 maxx
= maxy
= maxz
= -10000000
462 minx
= miny
= minz
= 10000000
464 s
= bpy
.context
.scene
466 obcount
= 0 # calculates the mesh obs. Good for non-mesh objects
467 matrix_parent
= parent
.matrix_world
472 # while parent.parent is not None:
475 if ob
.type == 'MESH' or ob
.type == 'CURVE':
476 # If to_mesh() works we can use it on curves and any other ob type almost.
477 # disabled to_mesh for 2.8 by now, not wanting to use dependency graph yet.
478 depsgraph
= bpy
.context
.evaluated_depsgraph_get()
480 object_eval
= ob
.evaluated_get(depsgraph
)
481 if ob
.type == 'CURVE':
482 mesh
= object_eval
.to_mesh()
484 mesh
= object_eval
.data
486 # to_mesh(context.depsgraph, apply_modifiers=self.applyModifiers, calc_undeformed=False)
489 for c
in mesh
.vertices
:
491 parent_coord
= matrix_parent
.inverted() @ mw
@ Vector(
492 (coord
[0], coord
[1], coord
[2])) # copy this when it works below.
493 minx
= min(minx
, parent_coord
.x
)
494 miny
= min(miny
, parent_coord
.y
)
495 minz
= min(minz
, parent_coord
.z
)
496 maxx
= max(maxx
, parent_coord
.x
)
497 maxy
= max(maxy
, parent_coord
.y
)
498 maxz
= max(maxz
, parent_coord
.z
)
499 # bpy.data.meshes.remove(mesh)
500 if ob
.type == 'CURVE':
501 object_eval
.to_mesh_clear()
504 minx
, miny
, minz
, maxx
, maxy
, maxz
= 0, 0, 0, 0, 0, 0
506 minx
*= parent
.scale
.x
507 maxx
*= parent
.scale
.x
508 miny
*= parent
.scale
.y
509 maxy
*= parent
.scale
.y
510 minz
*= parent
.scale
.z
511 maxz
*= parent
.scale
.z
513 return minx
, miny
, minz
, maxx
, maxy
, maxz
516 def get_bounds_worldspace(obs
, use_modifiers
=False):
517 # progress('getting bounds of object(s)')
518 s
= bpy
.context
.scene
519 maxx
= maxy
= maxz
= -10000000
520 minx
= miny
= minz
= 10000000
521 obcount
= 0 # calculates the mesh obs. Good for non-mesh objects
525 if ob
.type == 'MESH' or ob
.type == 'CURVE':
526 depsgraph
= bpy
.context
.evaluated_depsgraph_get()
527 ob_eval
= ob
.evaluated_get(depsgraph
)
528 mesh
= ob_eval
.to_mesh()
531 for c
in mesh
.vertices
:
533 world_coord
= mw
@ Vector((coord
[0], coord
[1], coord
[2]))
534 minx
= min(minx
, world_coord
.x
)
535 miny
= min(miny
, world_coord
.y
)
536 minz
= min(minz
, world_coord
.z
)
537 maxx
= max(maxx
, world_coord
.x
)
538 maxy
= max(maxy
, world_coord
.y
)
539 maxz
= max(maxz
, world_coord
.z
)
540 ob_eval
.to_mesh_clear()
543 minx
, miny
, minz
, maxx
, maxy
, maxz
= 0, 0, 0, 0, 0, 0
544 return minx
, miny
, minz
, maxx
, maxy
, maxz
547 def is_linked_asset(ob
):
548 return ob
.get('asset_data') and ob
.instance_collection
!= None
551 def get_dimensions(obs
):
552 minx
, miny
, minz
, maxx
, maxy
, maxz
= get_bounds_snappable(obs
)
553 bbmin
= Vector((minx
, miny
, minz
))
554 bbmax
= Vector((maxx
, maxy
, maxz
))
555 dim
= Vector((maxx
- minx
, maxy
- miny
, maxz
- minz
))
556 return dim
, bbmin
, bbmax
559 def requests_post_thread(url
, json
, headers
):
560 r
= rerequests
.post(url
, json
=json
, verify
=True, headers
=headers
)
563 def get_headers(api_key
):
565 "accept": "application/json",
568 headers
["Authorization"] = "Bearer %s" % api_key
572 def scale_2d(v
, s
, p
):
573 '''scale a 2d vector with a pivot'''
574 return (p
[0] + s
[0] * (v
[0] - p
[0]), p
[1] + s
[1] * (v
[1] - p
[1]))
577 def scale_uvs(ob
, scale
=1.0, pivot
=Vector((.5, .5))):
579 if len(mesh
.uv_layers
) > 0:
580 uv
= mesh
.uv_layers
[mesh
.uv_layers
.active_index
]
582 # Scale a UV map iterating over its coordinates to a given scale and with a pivot point
583 for uvindex
in range(len(uv
.data
)):
584 uv
.data
[uvindex
].uv
= scale_2d(uv
.data
[uvindex
].uv
, scale
, pivot
)
587 # map uv cubic and switch of auto tex space and set it to 1,1,1
588 def automap(target_object
=None, target_slot
=None, tex_size
=1, bg_exception
=False, just_scale
=False):
589 s
= bpy
.context
.scene
590 mat_props
= s
.blenderkit_mat
591 if mat_props
.automap
:
592 tob
= bpy
.data
.objects
[target_object
]
593 # only automap mesh models
594 if tob
.type == 'MESH' and len(tob
.data
.polygons
) > 0:
595 # check polycount for a rare case where no polys are in editmesh
596 actob
= bpy
.context
.active_object
597 bpy
.context
.view_layer
.objects
.active
= tob
600 if tob
.data
.use_auto_texspace
:
601 tob
.data
.use_auto_texspace
= False
604 tob
.data
.texspace_size
= (1, 1, 1)
606 if 'automap' not in tob
.data
.uv_layers
:
607 bpy
.ops
.mesh
.uv_texture_add()
608 uvl
= tob
.data
.uv_layers
[-1]
611 # TODO limit this to active material
612 # tob.data.uv_textures['automap'].active = True
614 scale
= tob
.scale
.copy()
616 if target_slot
is not None:
617 tob
.active_material_index
= target_slot
618 bpy
.ops
.object.mode_set(mode
='EDIT')
619 bpy
.ops
.mesh
.select_all(action
='DESELECT')
621 # this exception is just for a 2.8 background thunmbnailer crash, can be removed when material slot select works...
623 bpy
.ops
.mesh
.select_all(action
='SELECT')
625 bpy
.ops
.object.material_slot_select()
627 scale
= (scale
.x
+ scale
.y
+ scale
.z
) / 3.0
629 bpy
.ops
.uv
.cube_project(
630 cube_size
=scale
* 2.0 / (tex_size
),
631 correct_aspect
=False) # it's * 2.0 because blender can't tell size of a unit cube :)
633 bpy
.ops
.object.editmode_toggle()
634 tob
.data
.uv_layers
.active
= tob
.data
.uv_layers
['automap']
635 tob
.data
.uv_layers
["automap"].active_render
= True
636 # this by now works only for thumbnail preview, but should be extended to work on arbitrary objects.
637 # by now, it takes the basic uv map = 1 meter. also, it now doeasn't respect more materials on one object,
638 # it just scales whole UV.
640 scale_uvs(tob
, scale
=Vector((1 / tex_size
, 1 / tex_size
)))
641 bpy
.context
.view_layer
.objects
.active
= actob
644 def name_update(props
):
646 Update asset name function, gets run also before upload. Makes sure name doesn't change in case of reuploads,
647 and only displayName gets written to server.
649 scene
= bpy
.context
.scene
650 ui_props
= scene
.blenderkitUI
652 # props = get_upload_props()
653 if props
.name_old
!= props
.name
:
654 props
.name_changed
= True
655 props
.name_old
= props
.name
656 nname
= props
.name
.strip()
657 nname
= nname
.replace('_', ' ')
660 nname
= nname
.lower()
661 nname
= nname
[0].upper() + nname
[1:]
663 # here we need to fix the name for blender data = ' or " give problems in path evaluation down the road.
665 fname
= fname
.replace('\'', '')
666 fname
= fname
.replace('\"', '')
667 asset
= get_active_asset()
668 if ui_props
.asset_type
!= 'HDR':
669 # Here we actually rename assets datablocks, but don't do that with HDR's and possibly with others
673 def get_param(asset_data
, parameter_name
):
674 if not asset_data
.get('parameters'):
675 # this can appear in older version files.
678 for p
in asset_data
['parameters']:
679 if p
.get('parameterType') == parameter_name
:
684 def params_to_dict(params
):
687 params_dict
[p
['parameterType']] = p
['value']
691 def dict_to_params(inputs
, parameters
=None):
692 if parameters
== None:
694 for k
in inputs
.keys():
695 if type(inputs
[k
]) == list:
697 for idx
, s
in enumerate(inputs
[k
]):
699 if idx
< len(inputs
[k
]) - 1:
702 value
= "%s" % strlist
703 elif type(inputs
[k
]) != bool:
706 value
= str(inputs
[k
])
715 def update_tags(self
, context
):
718 commasep
= props
.tags
.split(',')
722 short_tags
= tag
.split(' ')
723 for short_tag
in short_tags
:
724 if len(short_tag
) > 19:
725 short_tag
= short_tag
[:18]
726 ntags
.append(short_tag
)
730 ntags
= ntags
[0].split(' ')
740 def user_logged_in():
741 a
= bpy
.context
.window_manager
.get('bkit profile')
747 def profile_is_validator():
748 a
= bpy
.context
.window_manager
.get('bkit profile')
749 if a
is not None and a
['user'].get('exmenu'):
754 def guard_from_crash():
755 '''Blender tends to crash when trying to run some functions with the addon going through unregistration process.'''
756 if bpy
.context
.preferences
.addons
.get('blenderkit') is None:
758 if bpy
.context
.preferences
.addons
['blenderkit'].preferences
is None:
763 def get_largest_area(area_type
='VIEW_3D'):
768 for w
in bpy
.data
.window_managers
[0].windows
:
769 for a
in w
.screen
.areas
:
770 if a
.type == area_type
:
771 asurf
= a
.width
* a
.height
778 if r
.type == 'WINDOW':
780 global active_area_pointer
, active_window_pointer
, active_region_pointer
781 active_window_pointer
= maxw
.as_pointer()
782 active_area_pointer
= maxa
.as_pointer()
783 active_region_pointer
= region
.as_pointer()
784 return maxw
, maxa
, region
787 def get_fake_context(context
, area_type
='VIEW_3D'):
788 C_dict
= {} # context.copy() #context.copy was a source of problems - incompatibility with addons that also define context
789 C_dict
.update(region
='WINDOW')
792 # context = context.copy()
793 # # print('bk context copied successfully')
794 # except Exception as e:
796 # print('BlenderKit: context.copy() failed. Can be a colliding addon.')
799 if context
.get('area') is None or context
.get('area').type != area_type
:
800 w
, a
, r
= get_largest_area(area_type
=area_type
)
802 # sometimes there is no area of the requested type. Let's face it, some people use Blender without 3d view.
803 override
= {'window': w
, 'screen': w
.screen
, 'area': a
, 'region': r
}
804 C_dict
.update(override
)
809 def label_multiline(layout
, text
='', icon
='NONE', width
=-1):
810 ''' draw a ui label, but try to split it in multiple lines.'''
811 if text
.strip() == '':
813 lines
= text
.split('\n')
815 threshold
= int(width
/ 5.5)
821 while len(l
) > threshold
:
822 i
= l
.rfind(' ', 0, threshold
)
826 layout
.label(text
=l1
, icon
=icon
)
834 layout
.label(text
=l
, icon
=icon
)
839 traceback
.print_stack()