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