1# ##### BEGIN GPL LICENSE BLOCK ##### 2# 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. 7# 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. 12# 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. 16# 17# ##### END GPL LICENSE BLOCK ##### 18 19# <pep8 compliant> 20 21 22bl_info = { 23 "name": "Icon Viewer", 24 "description": "Click an icon to copy its name to the clipboard", 25 "author": "roaoao", 26 "version": (1, 4, 0), 27 "blender": (2, 80, 0), 28 "location": "Text Editor > Dev Tab > Icon Viewer", 29 "doc_url": "{BLENDER_MANUAL_URL}/addons/development/icon_viewer.html", 30 "category": "Development", 31} 32 33import bpy 34import math 35from bpy.props import ( 36 BoolProperty, 37 StringProperty, 38) 39 40DPI = 72 41POPUP_PADDING = 10 42PANEL_PADDING = 44 43WIN_PADDING = 32 44ICON_SIZE = 20 45HISTORY_SIZE = 100 46HISTORY = [] 47 48 49def ui_scale(): 50 prefs = bpy.context.preferences.system 51 return prefs.dpi * prefs.pixel_size / DPI 52 53 54def prefs(): 55 return bpy.context.preferences.addons[__name__].preferences 56 57 58class Icons: 59 def __init__(self, is_popup=False): 60 self._filtered_icons = None 61 self._filter = "" 62 self.filter = "" 63 self.selected_icon = "" 64 self.is_popup = is_popup 65 66 @property 67 def filter(self): 68 return self._filter 69 70 @filter.setter 71 def filter(self, value): 72 if self._filter == value: 73 return 74 75 self._filter = value 76 self.update() 77 78 @property 79 def filtered_icons(self): 80 if self._filtered_icons is None: 81 self._filtered_icons = [] 82 icon_filter = self._filter.upper() 83 self.filtered_icons.clear() 84 pr = prefs() 85 86 icons = bpy.types.UILayout.bl_rna.functions[ 87 "prop"].parameters["icon"].enum_items.keys() 88 for icon in icons: 89 if icon == 'NONE' or \ 90 icon_filter and icon_filter not in icon or \ 91 not pr.show_brush_icons and "BRUSH_" in icon and \ 92 icon != 'BRUSH_DATA' or \ 93 not pr.show_matcap_icons and "MATCAP_" in icon or \ 94 not pr.show_event_icons and ( 95 "EVENT_" in icon or "MOUSE_" in icon 96 ) or \ 97 not pr.show_colorset_icons and "COLORSET_" in icon: 98 continue 99 self._filtered_icons.append(icon) 100 101 return self._filtered_icons 102 103 @property 104 def num_icons(self): 105 return len(self.filtered_icons) 106 107 def update(self): 108 if self._filtered_icons is not None: 109 self._filtered_icons.clear() 110 self._filtered_icons = None 111 112 def draw(self, layout, num_cols=0, icons=None): 113 if icons: 114 filtered_icons = reversed(icons) 115 else: 116 filtered_icons = self.filtered_icons 117 118 column = layout.column(align=True) 119 row = column.row(align=True) 120 row.alignment = 'CENTER' 121 122 selected_icon = self.selected_icon if self.is_popup else \ 123 bpy.context.window_manager.clipboard 124 col_idx = 0 125 for i, icon in enumerate(filtered_icons): 126 p = row.operator( 127 IV_OT_icon_select.bl_idname, text="", 128 icon=icon, emboss=icon == selected_icon) 129 p.icon = icon 130 p.force_copy_on_select = not self.is_popup 131 132 col_idx += 1 133 if col_idx > num_cols - 1: 134 if icons: 135 break 136 col_idx = 0 137 if i < len(filtered_icons) - 1: 138 row = column.row(align=True) 139 row.alignment = 'CENTER' 140 141 if col_idx != 0 and not icons and i >= num_cols: 142 for _ in range(num_cols - col_idx): 143 row.label(text="", icon='BLANK1') 144 145 if not filtered_icons: 146 row.label(text="No icons were found") 147 148 149class IV_Preferences(bpy.types.AddonPreferences): 150 bl_idname = __name__ 151 152 panel_icons = Icons() 153 popup_icons = Icons(is_popup=True) 154 155 def update_icons(self, context): 156 self.panel_icons.update() 157 self.popup_icons.update() 158 159 def set_panel_filter(self, value): 160 self.panel_icons.filter = value 161 162 panel_filter: StringProperty( 163 description="Filter", 164 default="", 165 get=lambda s: s.panel_icons.filter, 166 set=set_panel_filter, 167 options={'TEXTEDIT_UPDATE'}) 168 show_panel_icons: BoolProperty( 169 name="Show Icons", 170 description="Show icons", default=True) 171 show_history: BoolProperty( 172 name="Show History", 173 description="Show history", default=True) 174 show_brush_icons: BoolProperty( 175 name="Show Brush Icons", 176 description="Show brush icons", default=True, 177 update=update_icons) 178 show_matcap_icons: BoolProperty( 179 name="Show Matcap Icons", 180 description="Show matcap icons", default=True, 181 update=update_icons) 182 show_event_icons: BoolProperty( 183 name="Show Event Icons", 184 description="Show event icons", default=True, 185 update=update_icons) 186 show_colorset_icons: BoolProperty( 187 name="Show Colorset Icons", 188 description="Show colorset icons", default=True, 189 update=update_icons) 190 copy_on_select: BoolProperty( 191 name="Copy Icon On Click", 192 description="Copy icon on click", default=True) 193 close_on_select: BoolProperty( 194 name="Close Popup On Click", 195 description=( 196 "Close the popup on click.\n" 197 "Not supported by some windows (User Preferences, Render)" 198 ), 199 default=False) 200 auto_focus_filter: BoolProperty( 201 name="Auto Focus Input Field", 202 description="Auto focus input field", default=True) 203 show_panel: BoolProperty( 204 name="Show Panel", 205 description="Show the panel in the Text Editor", default=True) 206 show_header: BoolProperty( 207 name="Show Header", 208 description="Show the header in the Python Console", 209 default=True) 210 211 def draw(self, context): 212 layout = self.layout 213 row = layout.row() 214 row.scale_y = 1.5 215 row.operator(IV_OT_icons_show.bl_idname) 216 217 row = layout.row() 218 219 col = row.column(align=True) 220 col.label(text="Icons:") 221 col.prop(self, "show_matcap_icons") 222 col.prop(self, "show_brush_icons") 223 col.prop(self, "show_colorset_icons") 224 col.prop(self, "show_event_icons") 225 col.separator() 226 col.prop(self, "show_history") 227 228 col = row.column(align=True) 229 col.label(text="Popup:") 230 col.prop(self, "auto_focus_filter") 231 col.prop(self, "copy_on_select") 232 if self.copy_on_select: 233 col.prop(self, "close_on_select") 234 235 col = row.column(align=True) 236 col.label(text="Panel:") 237 col.prop(self, "show_panel") 238 if self.show_panel: 239 col.prop(self, "show_panel_icons") 240 241 col.separator() 242 col.label(text="Header:") 243 col.prop(self, "show_header") 244 245 246class IV_PT_icons(bpy.types.Panel): 247 bl_space_type = "TEXT_EDITOR" 248 bl_region_type = "UI" 249 bl_label = "Icon Viewer" 250 bl_category = "Dev" 251 bl_options = {'DEFAULT_CLOSED'} 252 253 @staticmethod 254 def tag_redraw(): 255 wm = bpy.context.window_manager 256 if not wm: 257 return 258 259 for w in wm.windows: 260 for a in w.screen.areas: 261 if a.type == 'TEXT_EDITOR': 262 for r in a.regions: 263 if r.type == 'UI': 264 r.tag_redraw() 265 266 def draw(self, context): 267 pr = prefs() 268 row = self.layout.row(align=True) 269 if pr.show_panel_icons: 270 row.prop(pr, "panel_filter", text="", icon='VIEWZOOM') 271 else: 272 row.operator(IV_OT_icons_show.bl_idname) 273 row.operator( 274 IV_OT_panel_menu_call.bl_idname, text="", icon='COLLAPSEMENU') 275 276 _, y0 = context.region.view2d.region_to_view(0, 0) 277 _, y1 = context.region.view2d.region_to_view(0, 10) 278 region_scale = 10 / abs(y0 - y1) 279 280 num_cols = max( 281 1, 282 (context.region.width - PANEL_PADDING) // 283 math.ceil(ui_scale() * region_scale * ICON_SIZE)) 284 285 col = None 286 if HISTORY and pr.show_history: 287 col = self.layout.column(align=True) 288 pr.panel_icons.draw(col.box(), num_cols, HISTORY) 289 290 if pr.show_panel_icons: 291 col = col or self.layout.column(align=True) 292 pr.panel_icons.draw(col.box(), num_cols) 293 294 @classmethod 295 def poll(cls, context): 296 return prefs().show_panel 297 298 299class IV_HT_icons(bpy.types.Header): 300 bl_space_type = 'CONSOLE' 301 302 def draw(self, context): 303 if not prefs().show_header: 304 return 305 layout = self.layout 306 layout.separator() 307 layout.operator(IV_OT_icons_show.bl_idname) 308 309 310class IV_OT_panel_menu_call(bpy.types.Operator): 311 bl_idname = "iv.panel_menu_call" 312 bl_label = "" 313 bl_description = "Menu" 314 bl_options = {'INTERNAL'} 315 316 def menu(self, menu, context): 317 pr = prefs() 318 layout = menu.layout 319 layout.prop(pr, "show_panel_icons") 320 layout.prop(pr, "show_history") 321 322 if not pr.show_panel_icons: 323 return 324 325 layout.separator() 326 layout.prop(pr, "show_matcap_icons") 327 layout.prop(pr, "show_brush_icons") 328 layout.prop(pr, "show_colorset_icons") 329 layout.prop(pr, "show_event_icons") 330 331 def execute(self, context): 332 context.window_manager.popup_menu(self.menu, title="Icon Viewer") 333 return {'FINISHED'} 334 335 336class IV_OT_icon_select(bpy.types.Operator): 337 bl_idname = "iv.icon_select" 338 bl_label = "" 339 bl_description = "Select the icon" 340 bl_options = {'INTERNAL'} 341 342 icon: StringProperty() 343 force_copy_on_select: BoolProperty() 344 345 def execute(self, context): 346 pr = prefs() 347 pr.popup_icons.selected_icon = self.icon 348 if pr.copy_on_select or self.force_copy_on_select: 349 context.window_manager.clipboard = self.icon 350 self.report({'INFO'}, self.icon) 351 352 if pr.close_on_select and IV_OT_icons_show.instance: 353 IV_OT_icons_show.instance.close() 354 355 if pr.show_history: 356 if self.icon in HISTORY: 357 HISTORY.remove(self.icon) 358 if len(HISTORY) >= HISTORY_SIZE: 359 HISTORY.pop(0) 360 HISTORY.append(self.icon) 361 return {'FINISHED'} 362 363 364class IV_OT_icons_show(bpy.types.Operator): 365 bl_idname = "iv.icons_show" 366 bl_label = "Icon Viewer" 367 bl_description = "Icon viewer" 368 bl_property = "filter_auto_focus" 369 370 instance = None 371 372 def set_filter(self, value): 373 prefs().popup_icons.filter = value 374 375 def set_selected_icon(self, value): 376 if IV_OT_icons_show.instance: 377 IV_OT_icons_show.instance.auto_focusable = False 378 379 filter_auto_focus: StringProperty( 380 description="Filter", 381 get=lambda s: prefs().popup_icons.filter, 382 set=set_filter, 383 options={'TEXTEDIT_UPDATE', 'SKIP_SAVE'}) 384 filter: StringProperty( 385 description="Filter", 386 get=lambda s: prefs().popup_icons.filter, 387 set=set_filter, 388 options={'TEXTEDIT_UPDATE'}) 389 selected_icon: StringProperty( 390 description="Selected Icon", 391 get=lambda s: prefs().popup_icons.selected_icon, 392 set=set_selected_icon) 393 394 def get_num_cols(self, num_icons): 395 return round(1.3 * math.sqrt(num_icons)) 396 397 def draw_header(self, layout): 398 pr = prefs() 399 header = layout.box() 400 header = header.split(factor=0.75) if self.selected_icon else \ 401 header.row() 402 row = header.row(align=True) 403 row.prop(pr, "show_matcap_icons", text="", icon='SHADING_RENDERED') 404 row.prop(pr, "show_brush_icons", text="", icon='BRUSH_DATA') 405 row.prop(pr, "show_colorset_icons", text="", icon='COLOR') 406 row.prop(pr, "show_event_icons", text="", icon='HAND') 407 row.separator() 408 409 row.prop( 410 pr, "copy_on_select", text="", 411 icon='COPYDOWN', toggle=True) 412 if pr.copy_on_select: 413 sub = row.row(align=True) 414 if bpy.context.window.screen.name == "temp": 415 sub.alert = True 416 sub.prop( 417 pr, "close_on_select", text="", 418 icon='RESTRICT_SELECT_OFF', toggle=True) 419 row.prop( 420 pr, "auto_focus_filter", text="", 421 icon='OUTLINER_DATA_FONT', toggle=True) 422 row.separator() 423 424 if self.auto_focusable and pr.auto_focus_filter: 425 row.prop(self, "filter_auto_focus", text="", icon='VIEWZOOM') 426 else: 427 row.prop(self, "filter", text="", icon='VIEWZOOM') 428 429 if self.selected_icon: 430 row = header.row() 431 row.prop(self, "selected_icon", text="", icon=self.selected_icon) 432 433 def draw(self, context): 434 pr = prefs() 435 col = self.layout 436 self.draw_header(col) 437 438 history_num_cols = int( 439 (self.width - POPUP_PADDING) / (ui_scale() * ICON_SIZE)) 440 num_cols = min( 441 self.get_num_cols(len(pr.popup_icons.filtered_icons)), 442 history_num_cols) 443 444 subcol = col.column(align=True) 445 446 if HISTORY and pr.show_history: 447 pr.popup_icons.draw(subcol.box(), history_num_cols, HISTORY) 448 449 pr.popup_icons.draw(subcol.box(), num_cols) 450 451 def close(self): 452 bpy.context.window.screen = bpy.context.window.screen 453 454 def check(self, context): 455 return True 456 457 def cancel(self, context): 458 IV_OT_icons_show.instance = None 459 IV_PT_icons.tag_redraw() 460 461 def execute(self, context): 462 if not IV_OT_icons_show.instance: 463 return {'CANCELLED'} 464 IV_OT_icons_show.instance = None 465 466 pr = prefs() 467 if self.selected_icon and not pr.copy_on_select: 468 context.window_manager.clipboard = self.selected_icon 469 self.report({'INFO'}, self.selected_icon) 470 pr.popup_icons.selected_icon = "" 471 472 IV_PT_icons.tag_redraw() 473 return {'FINISHED'} 474 475 def invoke(self, context, event): 476 pr = prefs() 477 pr.popup_icons.selected_icon = "" 478 pr.popup_icons.filter = "" 479 IV_OT_icons_show.instance = self 480 self.auto_focusable = True 481 482 num_cols = self.get_num_cols(len(pr.popup_icons.filtered_icons)) 483 self.width = min( 484 ui_scale() * (num_cols * ICON_SIZE + POPUP_PADDING), 485 context.window.width - WIN_PADDING) 486 487 return context.window_manager.invoke_props_dialog( 488 self, width=self.width) 489 490 491classes = ( 492 IV_PT_icons, 493 IV_HT_icons, 494 IV_OT_panel_menu_call, 495 IV_OT_icon_select, 496 IV_OT_icons_show, 497 IV_Preferences, 498) 499 500 501def register(): 502 if bpy.app.background: 503 return 504 505 for cls in classes: 506 bpy.utils.register_class(cls) 507 508 509def unregister(): 510 if bpy.app.background: 511 return 512 513 for cls in classes: 514 bpy.utils.unregister_class(cls) 515