├── .gitignore ├── icons ├── tag.png ├── 10500.png ├── 12000.png ├── 1700.png ├── 3200.png ├── 5500.png ├── 6500.png ├── 8000.png ├── num_1.png ├── num_10.png ├── num_2.png ├── num_3.png ├── num_4.png ├── num_5.png ├── num_6.png ├── num_7.png ├── num_8.png ├── num_9.png ├── random.png ├── hdri_haven.png ├── text-cursor.png ├── number_icons.blend ├── special │ └── missing_thumb.png └── random.png license.txt ├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── README.md ├── resize.py ├── constants.py ├── __init__.py ├── addon_updater_ops.py └── ui.py /.gitignore: -------------------------------------------------------------------------------- 1 | gaffer_updater 2 | __pycache__ 3 | *~syncthing~* 4 | .vscode 5 | -------------------------------------------------------------------------------- /icons/tag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregzaal/Gaffer/HEAD/icons/tag.png -------------------------------------------------------------------------------- /icons/10500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregzaal/Gaffer/HEAD/icons/10500.png -------------------------------------------------------------------------------- /icons/12000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregzaal/Gaffer/HEAD/icons/12000.png -------------------------------------------------------------------------------- /icons/1700.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregzaal/Gaffer/HEAD/icons/1700.png -------------------------------------------------------------------------------- /icons/3200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregzaal/Gaffer/HEAD/icons/3200.png -------------------------------------------------------------------------------- /icons/5500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregzaal/Gaffer/HEAD/icons/5500.png -------------------------------------------------------------------------------- /icons/6500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregzaal/Gaffer/HEAD/icons/6500.png -------------------------------------------------------------------------------- /icons/8000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregzaal/Gaffer/HEAD/icons/8000.png -------------------------------------------------------------------------------- /icons/num_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregzaal/Gaffer/HEAD/icons/num_1.png -------------------------------------------------------------------------------- /icons/num_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregzaal/Gaffer/HEAD/icons/num_10.png -------------------------------------------------------------------------------- /icons/num_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregzaal/Gaffer/HEAD/icons/num_2.png -------------------------------------------------------------------------------- /icons/num_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregzaal/Gaffer/HEAD/icons/num_3.png -------------------------------------------------------------------------------- /icons/num_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregzaal/Gaffer/HEAD/icons/num_4.png -------------------------------------------------------------------------------- /icons/num_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregzaal/Gaffer/HEAD/icons/num_5.png -------------------------------------------------------------------------------- /icons/num_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregzaal/Gaffer/HEAD/icons/num_6.png -------------------------------------------------------------------------------- /icons/num_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregzaal/Gaffer/HEAD/icons/num_7.png -------------------------------------------------------------------------------- /icons/num_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregzaal/Gaffer/HEAD/icons/num_8.png -------------------------------------------------------------------------------- /icons/num_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregzaal/Gaffer/HEAD/icons/num_9.png -------------------------------------------------------------------------------- /icons/random.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregzaal/Gaffer/HEAD/icons/random.png -------------------------------------------------------------------------------- /icons/hdri_haven.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregzaal/Gaffer/HEAD/icons/hdri_haven.png -------------------------------------------------------------------------------- /icons/text-cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregzaal/Gaffer/HEAD/icons/text-cursor.png -------------------------------------------------------------------------------- /icons/number_icons.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregzaal/Gaffer/HEAD/icons/number_icons.blend -------------------------------------------------------------------------------- /icons/special/missing_thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregzaal/Gaffer/HEAD/icons/special/missing_thumb.png -------------------------------------------------------------------------------- /icons/random.png license.txt: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 2 | 3 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ***PLEASE NOTE*** 11 | If you did not purchase Gaffer, I cannot provide you any user support. This especially includes helping you install the add-on correctly. Check #112 (https://github.com/gregzaal/Gaffer/issues/112) if you are having installation problems. 12 | 13 | ---- 14 | 15 | **Describe the bug** 16 | A clear and concise description of what the bug is. 17 | 18 | **To Reproduce** 19 | Steps to reproduce the behavior: 20 | 1. Go to '...' 21 | 2. Click on '....' 22 | 3. Scroll down to '....' 23 | 4. See error 24 | 25 | **Expected behavior** 26 | A clear and concise description of what you expected to happen. 27 | 28 | **Screenshots** 29 | If applicable, add screenshots to help explain your problem. 30 | 31 | **Desktop (please complete the following information):** 32 | - OS: [e.g. iOS] 33 | - Blender Version [e.g. 3.3] 34 | - Gaffer Version [e.g. 3.1.14] 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is the Git repository for [Gaffer], the Blender Add-on for speeding up your lighting workflow. 2 | 3 | You can download it here to try it out. If you like it, please consider [purchasing it from the Blender Market]. $20 isn't much, and every sale puts a smile on my face and motivates me to keep developing it :) 4 | 5 | A portion of every sale is donated to the [Blender Development Fund] too. 6 | 7 | Documentation can be found in [the wiki], and a list of changes in the [commit logs] or on the [Releases page]. 8 | 9 | # Download and install 10 | 11 | [Download the Trial](http://bit.ly/gaf-trial-dl) 12 | 13 | This is identical to the paid product, just with a small "Trial version" text in some places. There are no time limits, nagging popups, or any restrictions. 14 | 15 | If you later choose to [purchase Gaffer](https://blendermarket.com/products/gaffer-light-manager) to support its development, as well as the work we do on [Poly Haven](https://polyhaven.com), simply uninstall the Trial version and install the version you purchased. 16 | 17 | If you download this Git repository instead of the trial linked above, you'll need to manually install it by unzipping and placing the files in the correct location. 18 | 19 | [Gaffer]:https://blendermarket.com/products/gaffer-light-manager 20 | [purchasing it from the Blender Market]:https://blendermarket.com/products/gaffer-light-manager 21 | [Blender Development Fund]:https://www.blender.org/foundation/development-fund/ 22 | [the wiki]:https://github.com/gregzaal/Gaffer/wiki 23 | [commit logs]:https://github.com/gregzaal/Gaffer/commits/master 24 | [Releases page]:https://github.com/gregzaal/Gaffer/releases 25 | -------------------------------------------------------------------------------- /resize.py: -------------------------------------------------------------------------------- 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 | # Args: 20 | # img input path 21 | # size X 22 | # img output path 23 | 24 | # example usage: 25 | # blender --background --factory-startup --python resize.py -- "C:\big image.hdr" 200 "C:\small image.jpg" 26 | 27 | import bpy 28 | import sys 29 | from math import floor 30 | 31 | argv = sys.argv 32 | argv = argv[argv.index("--") + 1 :] # Get all args after '--' 33 | FILEPATH, SIZE_X, OUTPATH = argv 34 | SIZE_X = int(SIZE_X) 35 | 36 | context = bpy.context 37 | scene = context.scene 38 | 39 | if hasattr(scene, "use_nodes"): # Deprecated in Blender 5.0 40 | scene.use_nodes = True 41 | 42 | if hasattr(scene, "compositing_node_group"): 43 | node_tree = scene.compositing_node_group 44 | if not node_tree: 45 | # We need to create it first 46 | node_tree = bpy.data.node_groups.new(name="COMP", type="CompositorNodeTree") 47 | scene.compositing_node_group = node_tree 48 | else: 49 | # Pre Blender 5.0 50 | node_tree = scene.node_tree 51 | 52 | n_comp = None 53 | if bpy.app.version < (5, 0, 0): 54 | # Remove default nodes, except composite 55 | for n in node_tree.nodes: 56 | if not n.type == "COMPOSITE": 57 | node_tree.nodes.remove(n) 58 | else: 59 | n_comp = n 60 | else: 61 | # In Blender 5, there are no default nodes, so we need to make the group output node. 62 | n_comp = node_tree.nodes.new("NodeGroupOutput") 63 | node_tree.interface.new_socket(name="Output", in_out="OUTPUT", socket_type="NodeSocketColor") 64 | 65 | img = bpy.data.images.load(FILEPATH) 66 | n_img = node_tree.nodes.new("CompositorNodeImage") 67 | n_img.image = img 68 | 69 | n_blur = node_tree.nodes.new("CompositorNodeBlur") 70 | if bpy.app.version < (5, 0, 0): 71 | n_blur.filter_type = "FLAT" 72 | n_blur.size_x = floor(img.size[0] / SIZE_X / 2) 73 | n_blur.size_y = n_blur.size_x 74 | 75 | n_scale = node_tree.nodes.new("CompositorNodeScale") 76 | if bpy.app.version < (5, 0, 0): 77 | n_scale.space = "RENDER_SIZE" 78 | n_scale.frame_method = "CROP" 79 | else: 80 | n_scale.inputs["Type"].default_value = "Render Size" 81 | n_scale.inputs["Frame Type"].default_value = "Crop" 82 | 83 | # Links 84 | links = node_tree.links 85 | links.new(n_img.outputs[0], n_blur.inputs[0]) 86 | links.new(n_blur.outputs[0], n_scale.inputs[0]) 87 | links.new(n_scale.outputs[0], n_comp.inputs[0]) 88 | 89 | # Render 90 | r = scene.render 91 | r.image_settings.file_format = "JPEG" 92 | r.image_settings.quality = 95 93 | r.resolution_x = SIZE_X 94 | SIZE_Y = floor(SIZE_X / (img.size[0] / img.size[1])) 95 | r.resolution_y = SIZE_Y 96 | r.resolution_percentage = 100 97 | r.filepath = OUTPATH 98 | 99 | bpy.ops.render.render(write_still=True) 100 | -------------------------------------------------------------------------------- /constants.py: -------------------------------------------------------------------------------- 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 | import bpy 20 | import os 21 | 22 | supported_renderers = ["CYCLES", "BLENDER_EEVEE", "BLENDER_EEVEE_NEXT"] 23 | 24 | col_temp = { 25 | "01_Flame (1700)": 1700, 26 | "02_Tungsten (3200)": 3200, 27 | "03_Daylight (5500)": 5500, 28 | "04_Overcast (6500)": 6500, 29 | "05_Shade (8000)": 8000, 30 | "06_LCD (10500)": 10500, 31 | "07_Sky (12000)": 12000, 32 | } 33 | 34 | # List of RGB values that correlate to the 380-780 wavelength range. Even though this 35 | # is the exact list from the Cycles code, for some reason it doesn't always match :( 36 | wavelength_list = ( 37 | (0.0014, 0.0000, 0.0065), 38 | (0.0022, 0.0001, 0.0105), 39 | (0.0042, 0.0001, 0.0201), 40 | (0.0076, 0.0002, 0.0362), 41 | (0.0143, 0.0004, 0.0679), 42 | (0.0232, 0.0006, 0.1102), 43 | (0.0435, 0.0012, 0.2074), 44 | (0.0776, 0.0022, 0.3713), 45 | (0.1344, 0.0040, 0.6456), 46 | (0.2148, 0.0073, 1.0391), 47 | (0.2839, 0.0116, 1.3856), 48 | (0.3285, 0.0168, 1.6230), 49 | (0.3483, 0.0230, 1.7471), 50 | (0.3481, 0.0298, 1.7826), 51 | (0.3362, 0.0380, 1.7721), 52 | (0.3187, 0.0480, 1.7441), 53 | (0.2908, 0.0600, 1.6692), 54 | (0.2511, 0.0739, 1.5281), 55 | (0.1954, 0.0910, 1.2876), 56 | (0.1421, 0.1126, 1.0419), 57 | (0.0956, 0.1390, 0.8130), 58 | (0.0580, 0.1693, 0.6162), 59 | (0.0320, 0.2080, 0.4652), 60 | (0.0147, 0.2586, 0.3533), 61 | (0.0049, 0.3230, 0.2720), 62 | (0.0024, 0.4073, 0.2123), 63 | (0.0093, 0.5030, 0.1582), 64 | (0.0291, 0.6082, 0.1117), 65 | (0.0633, 0.7100, 0.0782), 66 | (0.1096, 0.7932, 0.0573), 67 | (0.1655, 0.8620, 0.0422), 68 | (0.2257, 0.9149, 0.0298), 69 | (0.2904, 0.9540, 0.0203), 70 | (0.3597, 0.9803, 0.0134), 71 | (0.4334, 0.9950, 0.0087), 72 | (0.5121, 1.0000, 0.0057), 73 | (0.5945, 0.9950, 0.0039), 74 | (0.6784, 0.9786, 0.0027), 75 | (0.7621, 0.9520, 0.0021), 76 | (0.8425, 0.9154, 0.0018), 77 | (0.9163, 0.8700, 0.0017), 78 | (0.9786, 0.8163, 0.0014), 79 | (1.0263, 0.7570, 0.0011), 80 | (1.0567, 0.6949, 0.0010), 81 | (1.0622, 0.6310, 0.0008), 82 | (1.0456, 0.5668, 0.0006), 83 | (1.0026, 0.5030, 0.0003), 84 | (0.9384, 0.4412, 0.0002), 85 | (0.8544, 0.3810, 0.0002), 86 | (0.7514, 0.3210, 0.0001), 87 | (0.6424, 0.2650, 0.0000), 88 | (0.5419, 0.2170, 0.0000), 89 | (0.4479, 0.1750, 0.0000), 90 | (0.3608, 0.1382, 0.0000), 91 | (0.2835, 0.1070, 0.0000), 92 | (0.2187, 0.0816, 0.0000), 93 | (0.1649, 0.0610, 0.0000), 94 | (0.1212, 0.0446, 0.0000), 95 | (0.0874, 0.0320, 0.0000), 96 | (0.0636, 0.0232, 0.0000), 97 | (0.0468, 0.0170, 0.0000), 98 | (0.0329, 0.0119, 0.0000), 99 | (0.0227, 0.0082, 0.0000), 100 | (0.0158, 0.0057, 0.0000), 101 | (0.0114, 0.0041, 0.0000), 102 | (0.0081, 0.0029, 0.0000), 103 | (0.0058, 0.0021, 0.0000), 104 | (0.0041, 0.0015, 0.0000), 105 | (0.0029, 0.0010, 0.0000), 106 | (0.0020, 0.0007, 0.0000), 107 | (0.0014, 0.0005, 0.0000), 108 | (0.0010, 0.0004, 0.0000), 109 | (0.0007, 0.0002, 0.0000), 110 | (0.0005, 0.0002, 0.0000), 111 | (0.0003, 0.0001, 0.0000), 112 | (0.0002, 0.0001, 0.0000), 113 | (0.0002, 0.0001, 0.0000), 114 | (0.0001, 0.0000, 0.0000), 115 | (0.0001, 0.0000, 0.0000), 116 | (0.0001, 0.0000, 0.0000), 117 | (0.0000, 0.0000, 0.0000), 118 | ) 119 | 120 | data_dir = os.path.join(os.path.abspath(os.path.join(bpy.utils.resource_path("USER"), "..")), "data", "gaffer") 121 | log_file = os.path.join(data_dir, "logs.txt") 122 | thumbnail_dir = os.path.join(data_dir, "thumbs") 123 | if not os.path.exists(thumbnail_dir): 124 | os.makedirs(thumbnail_dir) 125 | thumb_endings = ["preview", "thumb", "thumbnail"] 126 | hdr_file_types = [".tif", ".tiff", ".hdr", ".exr"] 127 | allowed_file_types = hdr_file_types + [".jpg", ".jpeg", ".png", ".tga"] 128 | jpg_dir = os.path.join(data_dir, "hdri_jpgs") 129 | if not os.path.exists(jpg_dir): 130 | os.makedirs(jpg_dir) 131 | hdri_list_path = os.path.join(data_dir, "gaffer_hdris.json") 132 | tags_path = os.path.join(data_dir, "tags.json") 133 | favorites = {} 134 | favorites_path = os.path.join(data_dir, "favorites.json") 135 | defaults_path = os.path.join(data_dir, "hdri_defaults.json") 136 | defaults_stored = [ 137 | "rotation", 138 | "brightness", 139 | "contrast", 140 | "saturation", 141 | "warmth", 142 | "tint", 143 | "clamp", 144 | "horz_shift", 145 | "horz_exp", 146 | "use_separate_brightness", 147 | "use_separate_contrast", 148 | "use_separate_saturation", 149 | "use_separate_warmth", 150 | "use_separate_tint", 151 | "background_brightness", 152 | "background_contrast", 153 | "background_saturation", 154 | "background_warmth", 155 | "background_tint", 156 | "use_bg_reflections", 157 | "use_jpg_background", 158 | "use_darkened_jpg", 159 | ] 160 | settings_file = os.path.join(data_dir, "settings.json") 161 | preview_collections = {} 162 | icon_dir = os.path.join(os.path.dirname(__file__), "icons") 163 | hdri_list = {} 164 | hdri_haven_list = [] 165 | hdri_haven_list_path = os.path.join(data_dir, "hdri_haven_hdris.json") 166 | custom_icons = None 167 | default_tags = [ 168 | "outdoor", 169 | "indoor", 170 | "##split##", 171 | "rural", 172 | "urban", 173 | "##split##", 174 | "clear", 175 | "partly cloudy", 176 | "overcast", 177 | "sun", 178 | "##split##", 179 | "early morning", 180 | "midday", 181 | "late afternoon", 182 | "night", 183 | "##split##", 184 | "low contrast", 185 | "medium contrast", 186 | "high contrast", 187 | "##split##", 188 | "natural light", 189 | "artificial light", 190 | "##split##", 191 | ] 192 | possible_tags = [] 193 | 194 | # List of types from: https://docs.blender.org/api/current/bpy_types_enum_items/id_type_items.html 195 | depsgraph_id_types = [ 196 | "ACTION", 197 | "ARMATURE", 198 | "BRUSH", 199 | "CACHEFILE", 200 | "CAMERA", 201 | "COLLECTION", 202 | "CURVE", 203 | "CURVES", 204 | "FONT", 205 | "GREASEPENCIL", 206 | "GREASEPENCIL_V3", 207 | "IMAGE", 208 | "KEY", 209 | "LATTICE", 210 | "LIBRARY", 211 | "LIGHT", 212 | "LIGHT_PROBE", 213 | "LINESTYLE", 214 | "MASK", 215 | "MATERIAL", 216 | "MESH", 217 | "META", 218 | "MOVIECLIP", 219 | "NODETREE", 220 | "OBJECT", 221 | "PAINTCURVE", 222 | "PALETTE", 223 | "PARTICLE", 224 | "POINTCLOUD", 225 | "SCENE", 226 | "SCREEN", 227 | "SOUND", 228 | "SPEAKER", 229 | "TEXT", 230 | "TEXTURE", 231 | "VOLUME", 232 | "WINDOWMANAGER", 233 | "WORKSPACE", 234 | "WORLD", 235 | ] 236 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 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 | bl_info = { 20 | "name": "Gaffer", 21 | "description": "Master your lighting workflow with easy access to light properties, HDRIs and other tools", 22 | "author": "Greg Zaal", 23 | "version": (3, 2, 9), 24 | "blender": (3, 4, 0), 25 | "location": "3D View > Sidebar & World Settings > HDRI", 26 | "warning": "", 27 | "wiki_url": "https://github.com/gregzaal/Gaffer/wiki", 28 | "tracker_url": "https://github.com/gregzaal/Gaffer/issues?q=is%3Aopen+is%3Aissue+label%3Abug", 29 | "category": "Lighting", 30 | } 31 | 32 | if "bpy" in locals(): 33 | import imp 34 | 35 | imp.reload(constants) 36 | imp.reload(functions) 37 | imp.reload(operators) 38 | imp.reload(ui) 39 | imp.reload(addon_updater) 40 | imp.reload(addon_updater_ops) 41 | else: 42 | from . import constants, functions, operators, ui # noqa: F401 (imported but unused, needed for reload) 43 | 44 | import bpy 45 | import os 46 | from . import addon_updater_ops 47 | from collections import OrderedDict 48 | 49 | 50 | class GafferPreferences(bpy.types.AddonPreferences): 51 | bl_idname = __package__ 52 | 53 | # Add-on Updater Prefs 54 | auto_check_update: bpy.props.BoolProperty( 55 | name="Auto-check for Update", 56 | description="If enabled, auto-check for updates using an interval", 57 | default=True, 58 | ) 59 | updater_interval_months: bpy.props.IntProperty( 60 | name="Months", 61 | description="Number of months between checking for updates", 62 | default=0, 63 | min=0, 64 | ) 65 | updater_interval_days: bpy.props.IntProperty( 66 | name="Days", 67 | description="Number of days between checking for updates", 68 | default=1, 69 | min=0, 70 | ) 71 | updater_interval_hours: bpy.props.IntProperty( 72 | name="Hours", 73 | description="Number of hours between checking for updates", 74 | default=0, 75 | min=0, 76 | max=23, 77 | ) 78 | updater_interval_minutes: bpy.props.IntProperty( 79 | name="Minutes", 80 | description="Number of minutes between checking for updates", 81 | default=0, 82 | min=0, 83 | max=59, 84 | ) 85 | updater_expand_prefs: bpy.props.BoolProperty(default=False) 86 | 87 | # Add-on Prefs 88 | show_hdri_list: bpy.props.BoolProperty( 89 | name="Show", 90 | description="List all the detected HDRIs and their detected resolutions/variants below", 91 | default=False, 92 | ) 93 | include_8bit: bpy.props.BoolProperty( 94 | name="Detect 8-bit images as HDRIs", 95 | description=( 96 | "Include LDR images like JPGs and PNGs when searching for files in the HDRI folder. " 97 | "Sometimes example renders are put next to HDRI files which can confuse the HDRI detection " 98 | "process, so they are ignored by default. Enable this to include them" 99 | ), 100 | default=False, 101 | update=functions.update_hdri_path, 102 | ) 103 | panel_category: bpy.props.StringProperty( 104 | name="Panel Category/Tab", 105 | description=("Select which sidebar category/tab to place Gaffer's panels in"), 106 | default="Gaffer", 107 | update=ui.update_category, 108 | ) 109 | offline_mode: bpy.props.BoolProperty( 110 | name="Offline Mode", 111 | description=("Stop Gaffer from checking polyhaven.com for the latest HDRI and tag lists"), 112 | default=False, 113 | update=functions.update_offline_mode, 114 | ) 115 | auto_refresh_light_list: bpy.props.BoolProperty( 116 | name="Auto-Refresh Light List", 117 | description=( 118 | "Watch for changes to the scene and automatically refresh the light list. " 119 | "May impact performance in heavy scenes. If disabled, " 120 | "you'll need to manually refresh the light list when you add or remove lights" 121 | ), 122 | default=True, 123 | ) 124 | 125 | show_debug: bpy.props.BoolProperty( 126 | name="Show Debug Tools", 127 | description="Expand this box to show various debugging tools", 128 | default=False, 129 | ) 130 | 131 | ForcePreviewsRefresh: bpy.props.BoolProperty(default=True, options={"HIDDEN"}) 132 | RequestThumbGen: bpy.props.BoolProperty(default=False, options={"HIDDEN"}) 133 | 134 | def draw(self, context): 135 | layout = self.layout 136 | 137 | main_col = layout.column() 138 | 139 | row = main_col.row(align=True) 140 | row.label(text="HDRI Folders:") 141 | hdri_paths = functions.get_persistent_setting("hdri_paths") 142 | if hdri_paths[0] != "": 143 | sub = row.column(align=True) 144 | sub.alignment = "RIGHT" 145 | sub.operator("gaffer.hdri_path_add", text="Add another", icon="ADD") 146 | hp_col = main_col.column(align=True) 147 | for i, hp in enumerate(hdri_paths): 148 | row = hp_col.row(align=True) 149 | row.operator( 150 | "gaffer.hdri_path_edit", 151 | text=hp + (" (missing!)" if not os.path.exists(hp) else ""), 152 | icon=("ERROR" if not os.path.exists(hp) else "NONE"), 153 | ).folder_index = i 154 | if len(hdri_paths) > 1: 155 | row.operator("gaffer.hdri_path_remove", text="", icon="X").folder_index = i 156 | row.operator("gaffer.hdri_path_edit", text="", icon="FILE_FOLDER").folder_index = i 157 | 158 | if hdri_paths[0] != "": 159 | hdris = functions.get_hdri_list() 160 | if hdris: 161 | num_files = sum(len(x) for x in hdris.values()) 162 | hdris = OrderedDict(sorted(hdris.items(), key=lambda x: x[0].lower())) 163 | num_hdris = len(hdris) 164 | row = main_col.row() 165 | row.alignment = "RIGHT" 166 | row.label(text="Found {} HDRIs ({} files)".format(num_hdris, num_files)) 167 | if num_hdris > 0: 168 | row.prop(self, "show_hdri_list", toggle=True) 169 | row.operator("gaffer.detect_hdris", text="Refresh", icon="FILE_REFRESH") 170 | 171 | if self.show_hdri_list: 172 | col = main_col.column(align=True) 173 | for name in hdris: 174 | col.label(text=name) 175 | variations = hdris[name] 176 | if len(variations) >= 10: 177 | row = col.row() 178 | row.alignment = "CENTER" 179 | row.label( 180 | text=( 181 | "There are quite a few varations of this HDRI, " "maybe they were wrongly detected?" 182 | ), 183 | icon="QUESTION", 184 | ) 185 | row = col.row() 186 | row.alignment = "CENTER" 187 | op = row.operator( 188 | "wm.url_open", 189 | text="Click here to learn how to fix this", 190 | icon="URL", 191 | ) 192 | op.url = "https://github.com/gregzaal/Gaffer/wiki/HDRI-Detection-and-Grouping" 193 | for v in variations: 194 | col.label(text=" " + v) 195 | else: 196 | row = main_col.row() 197 | row.alignment = "RIGHT" 198 | row.label(text="Select the folder that contains all your HDRIs. Subfolders will be included.") 199 | 200 | row = main_col.row() 201 | row.alignment = "RIGHT" 202 | row.prop(self, "include_8bit") 203 | 204 | row = main_col.row() 205 | row.label(text="Settings:") 206 | col = main_col.column() 207 | col.prop(self, "panel_category") 208 | col.prop(self, "offline_mode") 209 | col.prop(self, "auto_refresh_light_list") 210 | 211 | addon_updater_ops.update_settings_ui(self, context) 212 | 213 | box = layout.box() 214 | col = box.column() 215 | row = col.row(align=True) 216 | row.alignment = "LEFT" 217 | row.prop( 218 | self, 219 | "show_debug", 220 | text="Debug Tools", 221 | emboss=False, 222 | icon="TRIA_DOWN" if self.show_debug else "TRIA_RIGHT", 223 | ) 224 | if self.show_debug: 225 | col = box.column() 226 | col.operator("gaffer.dbg_delete_thumbs") 227 | col.operator("gaffer.dbg_upload_hdri_list") 228 | col.operator("gaffer.dbg_upload_logs") 229 | 230 | 231 | class BlacklistedObject(bpy.types.PropertyGroup): 232 | name: bpy.props.StringProperty(default="") 233 | 234 | 235 | class GafferProperties(bpy.types.PropertyGroup): 236 | Lights: bpy.props.StringProperty(name="Lights", default="", description="The objects to include in the isolation") 237 | ColTempExpand: bpy.props.BoolProperty( 238 | name="Color Temperature Presets", 239 | default=False, 240 | description="Preset color temperatures based on real-world light sources", 241 | ) 242 | MoreExpand: bpy.props.StringProperty( 243 | name="Show more options", 244 | default="", 245 | description="Show settings such as MIS, falloff, ray visibility...", 246 | ) 247 | MoreExpandAll: bpy.props.BoolProperty( 248 | name="Show more options", 249 | default=False, 250 | description="Show settings such as MIS, falloff, ray visibility...", 251 | ) 252 | LightUIIndex: bpy.props.IntProperty(name="light index", default=0, min=0, description="light index") 253 | LightsHiddenRecord: bpy.props.StringProperty(name="hidden record", default="", description="hidden record") 254 | WorldHiddenRecord: bpy.props.StringProperty(name="hidden record", default="", description="hidden record") 255 | SoloActive: bpy.props.StringProperty(name="soloactive", default="", description="soloactive") 256 | VisibleCollectionsOnly: bpy.props.BoolProperty( 257 | name="Visible Collections Only", 258 | default=True, 259 | description="Only show lights that are in visible collections", 260 | ) 261 | VisibleLightsOnly: bpy.props.BoolProperty( 262 | name="Visible Lights Only", 263 | default=False, 264 | description="Only show lights that are not hidden", 265 | ) 266 | WorldVis: bpy.props.BoolProperty( 267 | name="Hide World lighting", 268 | default=True, 269 | description="Don't display (or render) the environment lighting", 270 | update=functions._update_world_vis, 271 | ) 272 | WorldReflOnly: bpy.props.BoolProperty( 273 | name="Reflection Only", 274 | default=False, 275 | description="Only show the World lighting in reflections", 276 | update=functions._update_world_refl_only, 277 | ) 278 | LightRadiusAlpha: bpy.props.FloatProperty( 279 | name="Alpha", 280 | default=0.6, 281 | min=0, 282 | max=1, 283 | description="The opacity of the overlaid circles", 284 | ) 285 | LightRadiusUseColor: bpy.props.BoolProperty( 286 | name="Use Color", 287 | default=True, 288 | description="Draw the radius of each light in the same color as the light", 289 | ) 290 | LabelUseColor: bpy.props.BoolProperty( 291 | name="Use Color", 292 | default=True, 293 | description="Draw the label of each light in the same color as the light", 294 | ) 295 | LightRadiusSelectedOnly: bpy.props.BoolProperty( 296 | name="Selected Only", 297 | default=False, 298 | description="Draw the radius for every visible light, or only selected lights", 299 | ) 300 | LightRadiusXray: bpy.props.BoolProperty( 301 | name="X-Ray", 302 | default=False, 303 | description="Draw the circle in front of all objects", 304 | ) 305 | LightRadiusDrawType: bpy.props.EnumProperty( 306 | name="Draw Type", 307 | description="How should the radius display look?", 308 | default="dotted", 309 | items=( 310 | ("filled", "Filled", "Draw a circle filled with a solid color"), 311 | ("solid", "Solid", "Draw a solid outline of the circle"), 312 | ("dotted", "Dotted", "Draw a dotted outline of the circle"), 313 | ), 314 | ) 315 | DefaultRadiusColor: bpy.props.FloatVectorProperty( 316 | name="Default Color", 317 | description=( 318 | "When 'Use Color' is disaled, or when the color of a light is unknown " 319 | "(such as when using a texture), this color is used instead" 320 | ), 321 | subtype="COLOR", 322 | size=3, 323 | min=0.0, 324 | max=1.0, 325 | default=(1.0, 1.0, 1.0), 326 | ) 327 | DefaultLabelBGColor: bpy.props.FloatVectorProperty( 328 | name="Background Color", 329 | description=( 330 | "When 'Use Color' is disaled, or when the color of a light is unknown " 331 | "(such as when using a texture), this color is used instead" 332 | ), 333 | subtype="COLOR", 334 | size=3, 335 | min=0.0, 336 | max=1.0, 337 | default=(0.0, 0.0, 0.0), 338 | ) 339 | LabelAlpha: bpy.props.FloatProperty( 340 | name="Alpha", 341 | default=0.5, 342 | min=0, 343 | max=1, 344 | description="The opacity of the drawn labels", 345 | ) 346 | LabelFontSize: bpy.props.IntProperty(name="Font Size", default=14, min=1, description="How large the text is drawn") 347 | LabelDrawType: bpy.props.EnumProperty( 348 | name="Draw Type", 349 | description="How should the label look?", 350 | default="color_bg", 351 | items=( 352 | ( 353 | "color_bg", 354 | "Colored background, plain text", 355 | "Show the label name on a colored background", 356 | ), 357 | ( 358 | "plain_bg", 359 | "Colored text in plain background", 360 | "Show the label name in color, on a plain background", 361 | ), 362 | ( 363 | "color_text", 364 | "Text only, no background", 365 | "Show the text without any background", 366 | ), 367 | ), 368 | ) 369 | LabelTextColor: bpy.props.FloatVectorProperty( 370 | name="Text Color", 371 | description="The color of the label name text", 372 | subtype="COLOR", 373 | size=3, 374 | min=0.0, 375 | max=1.0, 376 | default=(1.0, 1.0, 1.0), 377 | ) 378 | LabelAlign: bpy.props.EnumProperty( 379 | name="Alignment", 380 | description="The positioning of the label relative to the light", 381 | default="r", 382 | items=( 383 | ("c", "Centered", "Positioned exactly on the light"), 384 | ("t", "Top", "Positioned above the light"), 385 | ("b", "Bottom", "Positioned below the light"), 386 | ("l", "Left", "Positioned to the left of the light"), 387 | ("r", "Right", "Positioned to the right of the light"), 388 | ("bl", "Bottom Left", "Positioned below and to the left of the light"), 389 | ("tl", "Top Left", "Positioned above and to the left of the light"), 390 | ("tr", "Top Right", "Positioned below and to the right of the light"), 391 | ("br", "Bottom Right", "Positioned above and to the right of the light"), 392 | ), 393 | ) 394 | LabelMargin: bpy.props.IntProperty( 395 | name="Margin", 396 | default=90, 397 | description="Draw the label this distance away from the light", 398 | ) 399 | SunObject: bpy.props.StringProperty( 400 | name="Sun Obj", 401 | default="", 402 | description="The light object to use to drive the Sky rotation", 403 | ) 404 | 405 | # Internal vars (not shown in UI) 406 | IsShowingRadius: bpy.props.BoolProperty(default=False, options={"HIDDEN"}) 407 | IsShowingLabel: bpy.props.BoolProperty(default=False, options={"HIDDEN"}) 408 | BlacklistIndex: bpy.props.IntProperty(default=0, options={"HIDDEN"}) 409 | VarNameCounter: bpy.props.IntProperty(default=0, options={"HIDDEN"}) 410 | HDRIList: bpy.props.StringProperty(default="", options={"HIDDEN"}) 411 | RequestJPGGen: bpy.props.BoolProperty(default=False, options={"HIDDEN"}) 412 | ShowProgress: bpy.props.BoolProperty(default=False, options={"HIDDEN"}) 413 | Progress: bpy.props.FloatProperty(default=0.0, options={"HIDDEN"}) 414 | ProgressText: bpy.props.StringProperty(default="", options={"HIDDEN"}) 415 | ProgressBarText: bpy.props.StringProperty(default="", options={"HIDDEN"}) 416 | ShowHDRIHaven: bpy.props.BoolProperty(default=False, options={"HIDDEN"}) 417 | ThumbnailsBigHDRIFound: bpy.props.BoolProperty(default=False, options={"HIDDEN"}) 418 | FileNotFoundError: bpy.props.BoolProperty(default=False, options={"HIDDEN"}) 419 | Blacklist: bpy.props.CollectionProperty(type=BlacklistedObject) # must be registered after classes 420 | 421 | 422 | class GafferHDRIProperties(bpy.types.PropertyGroup): 423 | # HDRI Handler stuffs 424 | hdri_handler_enabled: bpy.props.BoolProperty( 425 | name="Enable", 426 | description="Turn on/off Gaffer's HDRI handler", 427 | default=False, 428 | update=functions.hdri_enable, 429 | ) 430 | hdri: bpy.props.EnumProperty(name="HDRIs", items=functions.hdri_enum_previews, update=functions.switch_hdri) 431 | hdri_variation: bpy.props.EnumProperty( 432 | name="Resolution / Variation", 433 | items=functions.variation_enum_previews, 434 | update=functions.update_variation, 435 | ) 436 | hdri_search: bpy.props.StringProperty( 437 | name="Search", 438 | description="Show only HDRIs matching this text - name, subfolder and tags will match", 439 | default="", 440 | update=functions.update_search, 441 | ) 442 | hdri_favorite: bpy.props.BoolProperty( 443 | name="Show Only Favorites", 444 | description="Filter to show only your favorite HDRIs in the list", 445 | default=False, 446 | update=functions.update_search, 447 | ) 448 | hdri_folder_filter: bpy.props.StringProperty( 449 | name="Folder Filter", 450 | description="Show only HDRIs from this subfolder", 451 | default="", 452 | update=functions.update_search, 453 | ) 454 | hdri_rotation: bpy.props.FloatProperty( 455 | name="Rotation", 456 | description="Rotate the HDRI (in degrees) around the Z-axis", 457 | default=0, 458 | soft_min=-180, 459 | soft_max=180, 460 | update=functions.update_rotation, 461 | ) 462 | hdri_brightness: bpy.props.FloatProperty( 463 | name="Brightness", 464 | description="Change the exposure of the HDRI to emit more or less light (measured in EVs)", 465 | default=0, 466 | soft_min=-10, 467 | soft_max=10, 468 | update=functions.update_brightness, 469 | ) 470 | hdri_contrast: bpy.props.FloatProperty( 471 | name="Contrast", 472 | description="Adjust the Gamma, making light sources stonger or weaker compared to darker parts of the HDRI", 473 | default=1, 474 | min=0, 475 | soft_max=2, 476 | update=functions.update_contrast, 477 | ) 478 | hdri_saturation: bpy.props.FloatProperty( 479 | name="Saturation", 480 | description="Control how strong the colours in the HDRI are", 481 | default=1, 482 | min=0, 483 | soft_max=2, 484 | update=functions.update_saturation, 485 | ) 486 | hdri_warmth: bpy.props.FloatProperty( 487 | name="Warmth", 488 | description="Control the relative color temperature of the HDRI (blue/orange)", 489 | default=1, 490 | soft_min=0, 491 | soft_max=2, 492 | update=functions.update_warmth, 493 | ) 494 | hdri_tint: bpy.props.FloatProperty( 495 | name="Purple/Green Tint", 496 | description="Control the purple/green color balance, usually used together with the warmth setting", 497 | default=1, 498 | soft_min=0, 499 | soft_max=2, 500 | update=functions.update_tint, 501 | ) 502 | hdri_color: bpy.props.FloatVectorProperty( 503 | name="Mix Color", 504 | description="Tint, or fully color, the environment. Use Alpha to control strength/opacity", 505 | subtype="COLOR", 506 | size=4, 507 | soft_min=0.0, 508 | soft_max=1.0, 509 | default=(0.5, 0.15, 0.075, 0.0), 510 | update=functions.update_color, 511 | ) 512 | hdri_horz_shift: bpy.props.FloatProperty( 513 | name="Horizon Shift", 514 | description="Move the horizon down to view more of the sky", 515 | default=0, 516 | soft_min=0, 517 | soft_max=1, 518 | update=functions.update_horizon, 519 | ) 520 | hdri_horz_exp: bpy.props.FloatProperty( 521 | name="Warp", 522 | description="Adjust this if the sky looks distorted", 523 | default=0, 524 | soft_min=-1, 525 | soft_max=1, 526 | update=functions.update_horizon, 527 | ) 528 | hdri_use_jpg_background: bpy.props.BoolProperty( 529 | name="High-res JPG background", 530 | default=False, 531 | description=( 532 | "Use a higher-res JPG image for the background, keeping the HDR just for lighting - " 533 | "enable this and set the main resolution to a low option to save memory" 534 | ), 535 | update=functions.setup_hdri, 536 | ) 537 | hdri_use_darkened_jpg: bpy.props.BoolProperty( 538 | name="Pre-darkened", 539 | default=False, 540 | description=( 541 | "Use a darker version of the JPG to avoid clipped highlights (but at the cost of potential banding)" 542 | ), 543 | update=functions.setup_hdri, 544 | ) 545 | hdri_use_bg_reflections: bpy.props.BoolProperty( 546 | name="Use for reflections", 547 | default=False, 548 | description="Use these settings for the appearance of reflections as well", 549 | update=functions.setup_hdri, 550 | ) 551 | hdri_use_separate_rotation: bpy.props.BoolProperty( 552 | name="Rotation", 553 | default=False, 554 | description="Adjust the rotation for the background separately from the lighting", 555 | update=functions.setup_hdri, 556 | ) 557 | hdri_background_rotation: bpy.props.FloatProperty( 558 | name="Value", 559 | description="Rotate the HDRI (in degrees) around the Z-axis", 560 | default=0, 561 | soft_min=-180, 562 | soft_max=180, 563 | update=functions.update_background_rotation, 564 | ) 565 | hdri_use_separate_brightness: bpy.props.BoolProperty( 566 | name="Brightness", 567 | default=False, 568 | description="Adjust the brightness value for the background separately from the lighting", 569 | update=functions.setup_hdri, 570 | ) 571 | hdri_background_brightness: bpy.props.FloatProperty( 572 | name="Value", 573 | description="Make the background image brighter or darker without affecting the lighting", 574 | default=0, 575 | soft_min=-10, 576 | soft_max=10, 577 | update=functions.update_background_brightness, 578 | ) 579 | hdri_use_separate_contrast: bpy.props.BoolProperty( 580 | name="Contrast", 581 | default=False, 582 | description="Adjust the contrast value for the background separately from the lighting", 583 | update=functions.setup_hdri, 584 | ) 585 | hdri_background_contrast: bpy.props.FloatProperty( 586 | name="Value", 587 | description="Give the background image more or less contrast without affecting the lighting", 588 | default=1, 589 | min=0, 590 | soft_max=2, 591 | update=functions.update_background_contrast, 592 | ) 593 | hdri_use_separate_saturation: bpy.props.BoolProperty( 594 | name="Saturation", 595 | default=False, 596 | description="Adjust the saturation value for the background separately from the lighting", 597 | update=functions.setup_hdri, 598 | ) 599 | hdri_background_saturation: bpy.props.FloatProperty( 600 | name="Value", 601 | description="Change the saturation of the background image without affecting the lighting", 602 | default=1, 603 | min=0, 604 | soft_max=2, 605 | update=functions.update_background_saturation, 606 | ) 607 | hdri_use_separate_warmth: bpy.props.BoolProperty( 608 | name="Warmth", 609 | default=False, 610 | description="Adjust the warmth value for the background separately from the lighting", 611 | update=functions.setup_hdri, 612 | ) 613 | hdri_background_warmth: bpy.props.FloatProperty( 614 | name="Value", 615 | description="Change the warmth of the background image without affecting the lighting", 616 | default=1, 617 | soft_min=0, 618 | soft_max=2, 619 | update=functions.update_background_warmth, 620 | ) 621 | hdri_use_separate_tint: bpy.props.BoolProperty( 622 | name="Tint", 623 | default=False, 624 | description="Adjust the tint value for the background separately from the lighting", 625 | update=functions.setup_hdri, 626 | ) 627 | hdri_background_tint: bpy.props.FloatProperty( 628 | name="Value", 629 | description="Change the tint of the background image without affecting the lighting", 630 | default=1, 631 | soft_min=0, 632 | soft_max=2, 633 | update=functions.update_background_tint, 634 | ) 635 | hdri_use_separate_color: bpy.props.BoolProperty( 636 | name="Mix Color", 637 | default=False, 638 | description="Adjust the color value for the background separately from the lighting", 639 | update=functions.setup_hdri, 640 | ) 641 | hdri_background_color: bpy.props.FloatVectorProperty( 642 | name="Background Color", 643 | description="Tint, or fully color, the background. Use Alpha to control strength/opacity", 644 | subtype="COLOR", 645 | size=4, 646 | soft_min=0.0, 647 | soft_max=1.0, 648 | default=(0.5, 0.15, 0.075, 0.0), 649 | update=functions.update_background_color, 650 | ) 651 | hdri_clamp: bpy.props.FloatProperty( 652 | name="Clamp Brightness", 653 | description=( 654 | "Set any values brighter than this value to this value. " 655 | "Disabled when on 0. Use when bright lights (e.g. sun) are too bright" 656 | ), 657 | default=0, 658 | min=0, 659 | soft_max=50000, 660 | update=functions.update_clamp, 661 | ) 662 | hdri_advanced: bpy.props.BoolProperty(name="Advanced", description="Show/hide advanced settings", default=False) 663 | hdri_jpg_gen_all: bpy.props.BoolProperty( 664 | name="Generate for ALL HDRIs", 665 | description="Generate the JPG and darkened JPG for all HDRIs that you have. This will probably take a while", 666 | default=False, 667 | ) 668 | hdri_show_tags_ui: bpy.props.BoolProperty( 669 | name="Tags", 670 | description="Choose some tags for this HDRI to help you search for it later", 671 | default=False, 672 | ) 673 | hdri_custom_tags: bpy.props.StringProperty( 674 | name="New Tag(s)", 675 | description="Add some of your own tags to this HDRI - separate by commas or semi-colons", 676 | default="", 677 | update=functions.set_custom_tags, 678 | ) 679 | 680 | # Internal vars (not shown in UI) 681 | OldWorldSettings: bpy.props.StringProperty(default="", options={"HIDDEN"}) 682 | 683 | 684 | classes = [ 685 | GafferPreferences, 686 | BlacklistedObject, 687 | GafferProperties, 688 | GafferHDRIProperties, 689 | operators.GAFFER_OT_rename, 690 | operators.GAFFER_OT_set_strength, 691 | operators.GAFFER_OT_set_temp, 692 | operators.GAFFER_OT_show_temp_list, 693 | operators.GAFFER_OT_hide_temp_list, 694 | operators.GAFFER_OT_show_more, 695 | operators.GAFFER_OT_hide_more, 696 | operators.GAFFER_OT_hide_show_light, 697 | operators.GAFFER_OT_select_light, 698 | operators.GAFFER_OT_solo, 699 | operators.GAFFER_OT_light_use_nodes, 700 | operators.GAFFER_OT_node_set_strength, 701 | operators.GAFFER_OT_refresh_light_list, 702 | operators.GAFFER_OT_set_light_data_user_names, 703 | operators.GAFFER_OT_apply_exposure, 704 | operators.GAFFER_OT_link_sky_to_sun, 705 | operators.GAFFER_OT_aim_light, 706 | operators.GAFFER_OT_aim_light_with_view, 707 | operators.GAFFER_OT_show_light_radius, 708 | operators.GAFFER_OT_show_light_label, 709 | operators.GAFFER_OT_refresh_bgl, 710 | operators.GAFFER_OT_add_blacklisted, 711 | operators.GAFFER_OT_remove_blacklisted, 712 | operators.GAFFER_OT_detect_hdris, 713 | operators.GAFFER_OT_hdri_path_edit, 714 | operators.GAFFER_OT_hdri_path_add, 715 | operators.GAFFER_OT_hdri_path_remove, 716 | operators.GAFFER_OT_hdri_thumb_gen, 717 | operators.GAFFER_OT_hdri_jpg_gen, 718 | operators.GAFFER_OT_hdri_clear_search, 719 | operators.GAFFER_OT_hdri_set_favorite, 720 | operators.GAFFER_OT_hdri_set_folder_filter, 721 | operators.GAFFER_OT_hdri_paddles, 722 | operators.GAFFER_OT_hdri_variation_paddles, 723 | operators.GAFFER_OT_hdri_add_tag, 724 | operators.GAFFER_OT_hdri_random, 725 | operators.GAFFER_OT_hdri_reset, 726 | operators.GAFFER_OT_hdri_save, 727 | operators.GAFFER_OT_fix_mis, 728 | operators.GAFFER_OT_get_hdrihaven, 729 | operators.GAFFER_OT_hide_hdrihaven, 730 | operators.GAFFER_OT_open_hdrihaven, 731 | operators.GAFFER_OT_hdri_open_data_folder, 732 | operators.GAFFER_OT_debug_delete_thumbs, 733 | operators.GAFFER_OT_debug_upload_hdri_list, 734 | operators.GAFFER_OT_debug_upload_logs, 735 | ui.GAFFER_PT_hdris, 736 | ui.GAFFER_MT_folder_filter, 737 | ui.OBJECT_UL_object_list, 738 | ] 739 | 740 | 741 | def register(): 742 | addon_updater_ops.register(bl_info) 743 | 744 | functions.previews_register() 745 | functions.cleanup_logs() 746 | 747 | bpy.types.NODE_PT_active_node_generic.append(ui.gaffer_node_menu_func) 748 | 749 | from bpy.utils import register_class 750 | 751 | for cls in classes: 752 | register_class(cls) 753 | ui.update_category(bpy.context.preferences.addons[__name__].preferences, bpy.context) 754 | 755 | bpy.types.Scene.gaf_props = bpy.props.PointerProperty(type=GafferProperties) 756 | bpy.types.World.gaf_hdri_props = bpy.props.PointerProperty(type=GafferHDRIProperties) 757 | bpy.app.handlers.load_post.append(operators.load_handler) 758 | bpy.app.handlers.depsgraph_update_post.append(functions.depsgraph_update_post_handler) 759 | 760 | 761 | def unregister(): 762 | addon_updater_ops.unregister() 763 | 764 | bpy.app.handlers.load_post.remove(operators.load_handler) 765 | bpy.app.handlers.depsgraph_update_post.remove(functions.depsgraph_update_post_handler) 766 | 767 | functions.previews_unregister() 768 | 769 | if operators.GAFFER_OT_show_light_radius._handle is not None: 770 | bpy.types.SpaceView3D.draw_handler_remove(operators.GAFFER_OT_show_light_radius._handle, "WINDOW") 771 | bpy.context.scene.gaf_props.IsShowingRadius = False 772 | if operators.GAFFER_OT_show_light_label._handle is not None: 773 | bpy.types.SpaceView3D.draw_handler_remove(operators.GAFFER_OT_show_light_label._handle, "WINDOW") 774 | bpy.context.scene.gaf_props.IsShowingLabel = False 775 | 776 | del bpy.types.Scene.gaf_props 777 | del bpy.types.World.gaf_hdri_props 778 | 779 | bpy.types.NODE_PT_active_node_generic.remove(ui.gaffer_node_menu_func) 780 | 781 | from bpy.utils import unregister_class 782 | 783 | for cls in reversed(classes): 784 | unregister_class(cls) 785 | 786 | 787 | if __name__ == "__main__": 788 | register() 789 | -------------------------------------------------------------------------------- /addon_updater_ops.py: -------------------------------------------------------------------------------- 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 | """Blender UI integrations for the addon updater. 20 | 21 | Implements draw calls, popups, and operators that use the addon_updater. 22 | """ 23 | 24 | import os 25 | import traceback 26 | 27 | import bpy 28 | from bpy.app.handlers import persistent 29 | 30 | # Safely import the updater. 31 | # Prevents popups for users with invalid python installs e.g. missing libraries 32 | # and will replace with a fake class instead if it fails (so UI draws work). 33 | try: 34 | from .addon_updater import Updater as updater 35 | except Exception as e: 36 | print("ERROR INITIALIZING UPDATER") 37 | print(str(e)) 38 | traceback.print_exc() 39 | 40 | class SingletonUpdaterNone(object): 41 | """Fake, bare minimum fields and functions for the updater object.""" 42 | 43 | def __init__(self): 44 | self.invalid_updater = True # Used to distinguish bad install. 45 | 46 | self.addon = None 47 | self.verbose = False 48 | self.use_print_traces = True 49 | self.error = None 50 | self.error_msg = None 51 | self.async_checking = None 52 | 53 | def clear_state(self): 54 | self.addon = None 55 | self.verbose = False 56 | self.invalid_updater = True 57 | self.error = None 58 | self.error_msg = None 59 | self.async_checking = None 60 | 61 | def run_update(self, force, callback, clean): 62 | pass 63 | 64 | def check_for_update(self, now): 65 | pass 66 | 67 | updater = SingletonUpdaterNone() 68 | updater.error = "Error initializing updater module" 69 | updater.error_msg = str(e) 70 | 71 | # Must declare this before classes are loaded, otherwise the bl_idname's will 72 | # not match and have errors. Must be all lowercase and no spaces! Should also 73 | # be unique among any other addons that could exist (using this updater code), 74 | # to avoid clashes in operator registration. 75 | updater.addon = "gaffer" 76 | 77 | 78 | # ----------------------------------------------------------------------------- 79 | # Blender version utils 80 | # ----------------------------------------------------------------------------- 81 | def make_annotations(cls): 82 | """Add annotation attribute to fields to avoid Blender 2.8+ warnings""" 83 | if not hasattr(bpy.app, "version") or bpy.app.version < (2, 80): 84 | return cls 85 | if bpy.app.version < (2, 93, 0): 86 | bl_props = {k: v for k, v in cls.__dict__.items() if isinstance(v, tuple)} 87 | else: 88 | bl_props = { 89 | k: v 90 | for k, v in cls.__dict__.items() 91 | if isinstance(v, bpy.props._PropertyDeferred) 92 | } 93 | if bl_props: 94 | if "__annotations__" not in cls.__dict__: 95 | setattr(cls, "__annotations__", {}) 96 | annotations = cls.__dict__["__annotations__"] 97 | for k, v in bl_props.items(): 98 | annotations[k] = v 99 | delattr(cls, k) 100 | return cls 101 | 102 | 103 | def layout_split(layout, factor=0.0, align=False): 104 | """Intermediate method for pre and post blender 2.8 split UI function""" 105 | if not hasattr(bpy.app, "version") or bpy.app.version < (2, 80): 106 | return layout.split(percentage=factor, align=align) 107 | return layout.split(factor=factor, align=align) 108 | 109 | 110 | def get_user_preferences(context=None): 111 | """Intermediate method for pre and post blender 2.8 grabbing preferences""" 112 | if not context: 113 | context = bpy.context 114 | prefs = None 115 | if hasattr(context, "user_preferences"): 116 | prefs = context.user_preferences.addons.get(__package__, None) 117 | elif hasattr(context, "preferences"): 118 | prefs = context.preferences.addons.get(__package__, None) 119 | if prefs: 120 | return prefs.preferences 121 | # To make the addon stable and non-exception prone, return None 122 | # raise Exception("Could not fetch user preferences") 123 | return None 124 | 125 | 126 | # ----------------------------------------------------------------------------- 127 | # Updater operators 128 | # ----------------------------------------------------------------------------- 129 | 130 | 131 | # Simple popup to prompt use to check for update & offer install if available. 132 | class AddonUpdaterInstallPopup(bpy.types.Operator): 133 | """Check and install update if available""" 134 | 135 | bl_label = "Update {x} addon".format(x=updater.addon) 136 | bl_idname = updater.addon + ".updater_install_popup" 137 | bl_description = "Popup to check and display current updates available" 138 | bl_options = {"REGISTER", "INTERNAL"} 139 | 140 | # if true, run clean install - ie remove all files before adding new 141 | # equivalent to deleting the addon and reinstalling, except the 142 | # updater folder/backup folder remains 143 | clean_install = bpy.props.BoolProperty( 144 | name="Clean install", 145 | description=( 146 | "If enabled, completely clear the addon's folder before " 147 | "installing new update, creating a fresh install" 148 | ), 149 | default=False, 150 | options={"HIDDEN"}, 151 | ) 152 | 153 | ignore_enum = bpy.props.EnumProperty( 154 | name="Process update", 155 | description="Decide to install, ignore, or defer new addon update", 156 | items=[ 157 | ("install", "Update Now", "Install update now"), 158 | ("ignore", "Ignore", "Ignore this update to prevent future popups"), 159 | ("defer", "Defer", "Defer choice till next blender session"), 160 | ], 161 | options={"HIDDEN"}, 162 | ) 163 | 164 | def check(self, context): 165 | return True 166 | 167 | def invoke(self, context, event): 168 | return context.window_manager.invoke_props_dialog(self) 169 | 170 | def draw(self, context): 171 | layout = self.layout 172 | if updater.invalid_updater: 173 | layout.label(text="Updater module error") 174 | return 175 | elif updater.update_ready: 176 | col = layout.column() 177 | col.scale_y = 0.7 178 | col.label( 179 | text="Update {} ready!".format(updater.update_version), 180 | icon="LOOP_FORWARDS", 181 | ) 182 | col.label(text="Choose 'Update Now' & press OK to install, ", icon="BLANK1") 183 | col.label(text="or click outside window to defer", icon="BLANK1") 184 | row = col.row() 185 | row.prop(self, "ignore_enum", expand=True) 186 | col.split() 187 | elif not updater.update_ready: 188 | col = layout.column() 189 | col.scale_y = 0.7 190 | col.label(text="No updates available") 191 | col.label(text="Press okay to dismiss dialog") 192 | # add option to force install 193 | else: 194 | # Case: updater.update_ready = None 195 | # we have not yet checked for the update. 196 | layout.label(text="Check for update now?") 197 | 198 | # Potentially in future, UI to 'check to select/revert to old version'. 199 | 200 | def execute(self, context): 201 | # In case of error importing updater. 202 | if updater.invalid_updater: 203 | return {"CANCELLED"} 204 | 205 | if updater.manual_only: 206 | bpy.ops.wm.url_open(url=updater.website) 207 | elif updater.update_ready: 208 | 209 | # Action based on enum selection. 210 | if self.ignore_enum == "defer": 211 | return {"FINISHED"} 212 | elif self.ignore_enum == "ignore": 213 | updater.ignore_update() 214 | return {"FINISHED"} 215 | 216 | res = updater.run_update( 217 | force=False, callback=post_update_callback, clean=self.clean_install 218 | ) 219 | 220 | # Should return 0, if not something happened. 221 | if updater.verbose: 222 | if res == 0: 223 | print("Updater returned successful") 224 | else: 225 | print("Updater returned {}, error occurred".format(res)) 226 | elif updater.update_ready is None: 227 | _ = updater.check_for_update(now=True) 228 | 229 | # Re-launch this dialog. 230 | atr = AddonUpdaterInstallPopup.bl_idname.split(".") 231 | getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT") 232 | else: 233 | updater.print_verbose("Doing nothing, not ready for update") 234 | return {"FINISHED"} 235 | 236 | 237 | # User preference check-now operator 238 | class AddonUpdaterCheckNow(bpy.types.Operator): 239 | bl_label = "Check now for " + updater.addon + " update" 240 | bl_idname = updater.addon + ".updater_check_now" 241 | bl_description = "Check now for an update to the {} addon".format(updater.addon) 242 | bl_options = {"REGISTER", "INTERNAL"} 243 | 244 | def execute(self, context): 245 | if updater.invalid_updater: 246 | return {"CANCELLED"} 247 | 248 | if updater.async_checking and updater.error is None: 249 | # Check already happened. 250 | # Used here to just avoid constant applying settings below. 251 | # Ignoring if error, to prevent being stuck on the error screen. 252 | return {"CANCELLED"} 253 | 254 | # apply the UI settings 255 | settings = get_user_preferences(context) 256 | if not settings: 257 | updater.print_verbose( 258 | "Could not get {} preferences, update check skipped".format(__package__) 259 | ) 260 | return {"CANCELLED"} 261 | 262 | updater.set_check_interval( 263 | enabled=settings.auto_check_update, 264 | months=settings.updater_interval_months, 265 | days=settings.updater_interval_days, 266 | hours=settings.updater_interval_hours, 267 | minutes=settings.updater_interval_minutes, 268 | ) 269 | 270 | # Input is an optional callback function. This function should take a 271 | # bool input. If true: update ready, if false: no update ready. 272 | updater.check_for_update_now(ui_refresh) 273 | 274 | return {"FINISHED"} 275 | 276 | 277 | class AddonUpdaterUpdateNow(bpy.types.Operator): 278 | bl_label = "Update " + updater.addon + " addon now" 279 | bl_idname = updater.addon + ".updater_update_now" 280 | bl_description = "Update to the latest version of the {x} addon".format( 281 | x=updater.addon 282 | ) 283 | bl_options = {"REGISTER", "INTERNAL"} 284 | 285 | # If true, run clean install - ie remove all files before adding new 286 | # equivalent to deleting the addon and reinstalling, except the updater 287 | # folder/backup folder remains. 288 | clean_install = bpy.props.BoolProperty( 289 | name="Clean install", 290 | description=( 291 | "If enabled, completely clear the addon's folder before " 292 | "installing new update, creating a fresh install" 293 | ), 294 | default=False, 295 | options={"HIDDEN"}, 296 | ) 297 | 298 | def execute(self, context): 299 | 300 | # in case of error importing updater 301 | if updater.invalid_updater: 302 | return {"CANCELLED"} 303 | 304 | if updater.manual_only: 305 | bpy.ops.wm.url_open(url=updater.website) 306 | if updater.update_ready: 307 | # if it fails, offer to open the website instead 308 | try: 309 | res = updater.run_update( 310 | force=False, callback=post_update_callback, clean=self.clean_install 311 | ) 312 | 313 | # Should return 0, if not something happened. 314 | if updater.verbose: 315 | if res == 0: 316 | print("Updater returned successful") 317 | else: 318 | print("Updater error response: {}".format(res)) 319 | except Exception as expt: 320 | updater._error = "Error trying to run update" 321 | updater._error_msg = str(expt) 322 | updater.print_trace() 323 | atr = AddonUpdaterInstallManually.bl_idname.split(".") 324 | getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT") 325 | elif updater.update_ready is None: 326 | (update_ready, version, link) = updater.check_for_update(now=True) 327 | # Re-launch this dialog. 328 | atr = AddonUpdaterInstallPopup.bl_idname.split(".") 329 | getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT") 330 | 331 | elif not updater.update_ready: 332 | self.report({"INFO"}, "Nothing to update") 333 | return {"CANCELLED"} 334 | else: 335 | self.report({"ERROR"}, "Encountered a problem while trying to update") 336 | return {"CANCELLED"} 337 | 338 | return {"FINISHED"} 339 | 340 | 341 | class AddonUpdaterUpdateTarget(bpy.types.Operator): 342 | bl_label = updater.addon + " version target" 343 | bl_idname = updater.addon + ".updater_update_target" 344 | bl_description = "Install a targeted version of the {x} addon".format( 345 | x=updater.addon 346 | ) 347 | bl_options = {"REGISTER", "INTERNAL"} 348 | 349 | def target_version(self, context): 350 | # In case of error importing updater. 351 | if updater.invalid_updater: 352 | ret = [] 353 | 354 | ret = [] 355 | i = 0 356 | for tag in updater.tags: 357 | ret.append((tag, tag, "Select to install " + tag)) 358 | i += 1 359 | return ret 360 | 361 | target = bpy.props.EnumProperty( 362 | name="Target version to install", 363 | description="Select the version to install", 364 | items=target_version, 365 | ) 366 | 367 | # If true, run clean install - ie remove all files before adding new 368 | # equivalent to deleting the addon and reinstalling, except the 369 | # updater folder/backup folder remains. 370 | clean_install = bpy.props.BoolProperty( 371 | name="Clean install", 372 | description=( 373 | "If enabled, completely clear the addon's folder before " 374 | "installing new update, creating a fresh install" 375 | ), 376 | default=False, 377 | options={"HIDDEN"}, 378 | ) 379 | 380 | @classmethod 381 | def poll(cls, context): 382 | if updater.invalid_updater: 383 | return False 384 | return updater.update_ready is not None and len(updater.tags) > 0 385 | 386 | def invoke(self, context, event): 387 | return context.window_manager.invoke_props_dialog(self) 388 | 389 | def draw(self, context): 390 | layout = self.layout 391 | if updater.invalid_updater: 392 | layout.label(text="Updater error") 393 | return 394 | split = layout_split(layout, factor=0.5) 395 | sub_col = split.column() 396 | sub_col.label(text="Select install version") 397 | sub_col = split.column() 398 | sub_col.prop(self, "target", text="") 399 | 400 | def execute(self, context): 401 | # In case of error importing updater. 402 | if updater.invalid_updater: 403 | return {"CANCELLED"} 404 | 405 | res = updater.run_update( 406 | force=False, 407 | revert_tag=self.target, 408 | callback=post_update_callback, 409 | clean=self.clean_install, 410 | ) 411 | 412 | # Should return 0, if not something happened. 413 | if res == 0: 414 | updater.print_verbose("Updater returned successful") 415 | else: 416 | updater.print_verbose("Updater returned {}, , error occurred".format(res)) 417 | return {"CANCELLED"} 418 | 419 | return {"FINISHED"} 420 | 421 | 422 | class AddonUpdaterInstallManually(bpy.types.Operator): 423 | """As a fallback, direct the user to download the addon manually""" 424 | 425 | bl_label = "Install update manually" 426 | bl_idname = updater.addon + ".updater_install_manually" 427 | bl_description = "Proceed to manually install update" 428 | bl_options = {"REGISTER", "INTERNAL"} 429 | 430 | error = bpy.props.StringProperty( 431 | name="Error Occurred", default="", options={"HIDDEN"} 432 | ) 433 | 434 | def invoke(self, context, event): 435 | return context.window_manager.invoke_popup(self) 436 | 437 | def draw(self, context): 438 | layout = self.layout 439 | 440 | if updater.invalid_updater: 441 | layout.label(text="Updater error") 442 | return 443 | 444 | # Display error if a prior autoamted install failed. 445 | if self.error != "": 446 | col = layout.column() 447 | col.scale_y = 0.7 448 | col.label(text="There was an issue trying to auto-install", icon="ERROR") 449 | col.label(text="Press the download button below and install", icon="BLANK1") 450 | col.label(text="the zip file like a normal addon.", icon="BLANK1") 451 | else: 452 | col = layout.column() 453 | col.scale_y = 0.7 454 | col.label(text="Install the addon manually") 455 | col.label(text="Press the download button below and install") 456 | col.label(text="the zip file like a normal addon.") 457 | 458 | # If check hasn't happened, i.e. accidentally called this menu, 459 | # allow to check here. 460 | 461 | row = layout.row() 462 | 463 | if updater.update_link is not None: 464 | row.operator( 465 | "wm.url_open", text="Direct download" 466 | ).url = updater.update_link 467 | else: 468 | row.operator("wm.url_open", text="(failed to retrieve direct download)") 469 | row.enabled = False 470 | 471 | if updater.website is not None: 472 | row = layout.row() 473 | ops = row.operator("wm.url_open", text="Open website") 474 | ops.url = updater.website 475 | else: 476 | row = layout.row() 477 | row.label(text="See source website to download the update") 478 | 479 | def execute(self, context): 480 | return {"FINISHED"} 481 | 482 | 483 | class AddonUpdaterUpdatedSuccessful(bpy.types.Operator): 484 | """Addon in place, popup telling user it completed or what went wrong""" 485 | 486 | bl_label = "Installation Report" 487 | bl_idname = updater.addon + ".updater_update_successful" 488 | bl_description = "Update installation response" 489 | bl_options = {"REGISTER", "INTERNAL", "UNDO"} 490 | 491 | error = bpy.props.StringProperty( 492 | name="Error Occurred", default="", options={"HIDDEN"} 493 | ) 494 | 495 | def invoke(self, context, event): 496 | return context.window_manager.invoke_props_popup(self, event) 497 | 498 | def draw(self, context): 499 | layout = self.layout 500 | 501 | if updater.invalid_updater: 502 | layout.label(text="Updater error") 503 | return 504 | 505 | saved = updater.json 506 | if self.error != "": 507 | col = layout.column() 508 | col.scale_y = 0.7 509 | col.label(text="Error occurred, did not install", icon="ERROR") 510 | if updater.error_msg: 511 | msg = updater.error_msg 512 | else: 513 | msg = self.error 514 | col.label(text=str(msg), icon="BLANK1") 515 | rw = col.row() 516 | rw.scale_y = 2 517 | rw.operator( 518 | "wm.url_open", text="Click for manual download.", icon="BLANK1" 519 | ).url = updater.website 520 | elif not updater.auto_reload_post_update: 521 | # Tell user to restart blender after an update/restore! 522 | if "just_restored" in saved and saved["just_restored"]: 523 | col = layout.column() 524 | col.label(text="Addon restored", icon="RECOVER_LAST") 525 | alert_row = col.row() 526 | alert_row.alert = True 527 | alert_row.operator( 528 | "wm.quit_blender", text="Restart blender to reload", icon="BLANK1" 529 | ) 530 | updater.json_reset_restore() 531 | else: 532 | col = layout.column() 533 | col.label(text="Addon successfully installed", icon="FILE_TICK") 534 | alert_row = col.row() 535 | alert_row.alert = True 536 | alert_row.operator( 537 | "wm.quit_blender", text="Restart blender to reload", icon="BLANK1" 538 | ) 539 | 540 | else: 541 | # reload addon, but still recommend they restart blender 542 | if "just_restored" in saved and saved["just_restored"]: 543 | col = layout.column() 544 | col.scale_y = 0.7 545 | col.label(text="Addon restored", icon="RECOVER_LAST") 546 | col.label( 547 | text="Consider restarting blender to fully reload.", icon="BLANK1" 548 | ) 549 | updater.json_reset_restore() 550 | else: 551 | col = layout.column() 552 | col.scale_y = 0.7 553 | col.label(text="Addon successfully installed", icon="FILE_TICK") 554 | col.label( 555 | text="Consider restarting blender to fully reload.", icon="BLANK1" 556 | ) 557 | 558 | def execute(self, context): 559 | return {"FINISHED"} 560 | 561 | 562 | class AddonUpdaterRestoreBackup(bpy.types.Operator): 563 | """Restore addon from backup""" 564 | 565 | bl_label = "Restore backup" 566 | bl_idname = updater.addon + ".updater_restore_backup" 567 | bl_description = "Restore addon from backup" 568 | bl_options = {"REGISTER", "INTERNAL"} 569 | 570 | @classmethod 571 | def poll(cls, context): 572 | try: 573 | return os.path.isdir(os.path.join(updater.stage_path, "backup")) 574 | except: 575 | return False 576 | 577 | def execute(self, context): 578 | # in case of error importing updater 579 | if updater.invalid_updater: 580 | return {"CANCELLED"} 581 | updater.restore_backup() 582 | return {"FINISHED"} 583 | 584 | 585 | class AddonUpdaterIgnore(bpy.types.Operator): 586 | """Ignore update to prevent future popups""" 587 | 588 | bl_label = "Ignore update" 589 | bl_idname = updater.addon + ".updater_ignore" 590 | bl_description = "Ignore update to prevent future popups" 591 | bl_options = {"REGISTER", "INTERNAL"} 592 | 593 | @classmethod 594 | def poll(cls, context): 595 | if updater.invalid_updater: 596 | return False 597 | elif updater.update_ready: 598 | return True 599 | else: 600 | return False 601 | 602 | def execute(self, context): 603 | # in case of error importing updater 604 | if updater.invalid_updater: 605 | return {"CANCELLED"} 606 | updater.ignore_update() 607 | self.report({"INFO"}, "Open addon preferences for updater options") 608 | return {"FINISHED"} 609 | 610 | 611 | class AddonUpdaterEndBackground(bpy.types.Operator): 612 | """Stop checking for update in the background""" 613 | 614 | bl_label = "End background check" 615 | bl_idname = updater.addon + ".end_background_check" 616 | bl_description = "Stop checking for update in the background" 617 | bl_options = {"REGISTER", "INTERNAL"} 618 | 619 | def execute(self, context): 620 | # in case of error importing updater 621 | if updater.invalid_updater: 622 | return {"CANCELLED"} 623 | updater.stop_async_check_update() 624 | return {"FINISHED"} 625 | 626 | 627 | # ----------------------------------------------------------------------------- 628 | # Handler related, to create popups 629 | # ----------------------------------------------------------------------------- 630 | 631 | 632 | # global vars used to prevent duplicate popup handlers 633 | ran_auto_check_install_popup = False 634 | ran_update_success_popup = False 635 | 636 | # global var for preventing successive calls 637 | ran_background_check = False 638 | 639 | 640 | @persistent 641 | def updater_run_success_popup_handler(scene): 642 | global ran_update_success_popup 643 | ran_update_success_popup = True 644 | 645 | # in case of error importing updater 646 | if updater.invalid_updater: 647 | return 648 | 649 | try: 650 | if "scene_update_post" in dir(bpy.app.handlers): 651 | bpy.app.handlers.scene_update_post.remove(updater_run_success_popup_handler) 652 | else: 653 | bpy.app.handlers.depsgraph_update_post.remove( 654 | updater_run_success_popup_handler 655 | ) 656 | except: 657 | pass 658 | 659 | atr = AddonUpdaterUpdatedSuccessful.bl_idname.split(".") 660 | getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT") 661 | 662 | 663 | @persistent 664 | def updater_run_install_popup_handler(scene): 665 | global ran_auto_check_install_popup 666 | ran_auto_check_install_popup = True 667 | updater.print_verbose("Running the install popup handler.") 668 | 669 | # in case of error importing updater 670 | if updater.invalid_updater: 671 | return 672 | 673 | try: 674 | if "scene_update_post" in dir(bpy.app.handlers): 675 | bpy.app.handlers.scene_update_post.remove(updater_run_install_popup_handler) 676 | else: 677 | bpy.app.handlers.depsgraph_update_post.remove( 678 | updater_run_install_popup_handler 679 | ) 680 | except: 681 | pass 682 | 683 | if "ignore" in updater.json and updater.json["ignore"]: 684 | return # Don't do popup if ignore pressed. 685 | elif "version_text" in updater.json and updater.json["version_text"].get("version"): 686 | version = updater.json["version_text"]["version"] 687 | ver_tuple = updater.version_tuple_from_text(version) 688 | 689 | if ver_tuple < updater.current_version: 690 | # User probably manually installed to get the up to date addon 691 | # in here. Clear out the update flag using this function. 692 | updater.print_verbose( 693 | "{} updater: appears user updated, clearing flag".format(updater.addon) 694 | ) 695 | updater.json_reset_restore() 696 | return 697 | atr = AddonUpdaterInstallPopup.bl_idname.split(".") 698 | getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT") 699 | 700 | 701 | def background_update_callback(update_ready): 702 | """Passed into the updater, background thread updater""" 703 | global ran_auto_check_install_popup 704 | updater.print_verbose("Running background update callback") 705 | 706 | # In case of error importing updater. 707 | if updater.invalid_updater: 708 | return 709 | if not updater.show_popups: 710 | return 711 | if not update_ready: 712 | return 713 | 714 | # See if we need add to the update handler to trigger the popup. 715 | handlers = [] 716 | if "scene_update_post" in dir(bpy.app.handlers): # 2.7x 717 | handlers = bpy.app.handlers.scene_update_post 718 | else: # 2.8+ 719 | handlers = bpy.app.handlers.depsgraph_update_post 720 | in_handles = updater_run_install_popup_handler in handlers 721 | 722 | if in_handles or ran_auto_check_install_popup: 723 | return 724 | 725 | if "scene_update_post" in dir(bpy.app.handlers): # 2.7x 726 | bpy.app.handlers.scene_update_post.append(updater_run_install_popup_handler) 727 | else: # 2.8+ 728 | bpy.app.handlers.depsgraph_update_post.append(updater_run_install_popup_handler) 729 | ran_auto_check_install_popup = True 730 | updater.print_verbose("Attempted popup prompt") 731 | 732 | 733 | def post_update_callback(module_name, res=None): 734 | """Callback for once the run_update function has completed. 735 | 736 | Only makes sense to use this if "auto_reload_post_update" == False, 737 | i.e. don't auto-restart the addon. 738 | 739 | Arguments: 740 | module_name: returns the module name from updater, but unused here. 741 | res: If an error occurred, this is the detail string. 742 | """ 743 | 744 | # In case of error importing updater. 745 | if updater.invalid_updater: 746 | return 747 | 748 | if res is None: 749 | # This is the same code as in conditional at the end of the register 750 | # function, ie if "auto_reload_post_update" == True, skip code. 751 | updater.print_verbose( 752 | "{} updater: Running post update callback".format(updater.addon) 753 | ) 754 | 755 | atr = AddonUpdaterUpdatedSuccessful.bl_idname.split(".") 756 | getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT") 757 | global ran_update_success_popup 758 | ran_update_success_popup = True 759 | else: 760 | # Some kind of error occurred and it was unable to install, offer 761 | # manual download instead. 762 | atr = AddonUpdaterUpdatedSuccessful.bl_idname.split(".") 763 | getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT", error=res) 764 | return 765 | 766 | 767 | def ui_refresh(update_status): 768 | """Redraw the ui once an async thread has completed""" 769 | for windowManager in bpy.data.window_managers: 770 | for window in windowManager.windows: 771 | for area in window.screen.areas: 772 | area.tag_redraw() 773 | 774 | 775 | def check_for_update_background(): 776 | """Function for asynchronous background check. 777 | 778 | *Could* be called on register, but would be bad practice as the bare 779 | minimum code should run at the moment of registration (addon ticked). 780 | """ 781 | if updater.invalid_updater: 782 | return 783 | global ran_background_check 784 | if ran_background_check: 785 | # Global var ensures check only happens once. 786 | return 787 | elif updater.update_ready is not None or updater.async_checking: 788 | # Check already happened. 789 | # Used here to just avoid constant applying settings below. 790 | return 791 | 792 | # Apply the UI settings. 793 | settings = get_user_preferences(bpy.context) 794 | if not settings: 795 | return 796 | updater.set_check_interval( 797 | enabled=settings.auto_check_update, 798 | months=settings.updater_interval_months, 799 | days=settings.updater_interval_days, 800 | hours=settings.updater_interval_hours, 801 | minutes=settings.updater_interval_minutes, 802 | ) 803 | 804 | # Input is an optional callback function. This function should take a bool 805 | # input, if true: update ready, if false: no update ready. 806 | updater.check_for_update_async(background_update_callback) 807 | ran_background_check = True 808 | 809 | 810 | def check_for_update_nonthreaded(self, context): 811 | """Can be placed in front of other operators to launch when pressed""" 812 | if updater.invalid_updater: 813 | return 814 | 815 | # Only check if it's ready, ie after the time interval specified should 816 | # be the async wrapper call here. 817 | settings = get_user_preferences(bpy.context) 818 | if not settings: 819 | if updater.verbose: 820 | print( 821 | "Could not get {} preferences, update check skipped".format(__package__) 822 | ) 823 | return 824 | updater.set_check_interval( 825 | enabled=settings.auto_check_update, 826 | months=settings.updater_interval_months, 827 | days=settings.updater_interval_days, 828 | hours=settings.updater_interval_hours, 829 | minutes=settings.updater_interval_minutes, 830 | ) 831 | 832 | (update_ready, version, link) = updater.check_for_update(now=False) 833 | if update_ready: 834 | atr = AddonUpdaterInstallPopup.bl_idname.split(".") 835 | getattr(getattr(bpy.ops, atr[0]), atr[1])("INVOKE_DEFAULT") 836 | else: 837 | updater.print_verbose("No update ready") 838 | self.report({"INFO"}, "No update ready") 839 | 840 | 841 | def show_reload_popup(): 842 | """For use in register only, to show popup after re-enabling the addon. 843 | 844 | Must be enabled by developer. 845 | """ 846 | if updater.invalid_updater: 847 | return 848 | saved_state = updater.json 849 | global ran_update_success_popup 850 | 851 | has_state = saved_state is not None 852 | just_updated = "just_updated" in saved_state 853 | updated_info = saved_state["just_updated"] 854 | 855 | if not (has_state and just_updated and updated_info): 856 | return 857 | 858 | updater.json_reset_postupdate() # So this only runs once. 859 | 860 | # No handlers in this case. 861 | if not updater.auto_reload_post_update: 862 | return 863 | 864 | # See if we need add to the update handler to trigger the popup. 865 | handlers = [] 866 | if "scene_update_post" in dir(bpy.app.handlers): # 2.7x 867 | handlers = bpy.app.handlers.scene_update_post 868 | else: # 2.8+ 869 | handlers = bpy.app.handlers.depsgraph_update_post 870 | in_handles = updater_run_success_popup_handler in handlers 871 | 872 | if in_handles or ran_update_success_popup: 873 | return 874 | 875 | if "scene_update_post" in dir(bpy.app.handlers): # 2.7x 876 | bpy.app.handlers.scene_update_post.append(updater_run_success_popup_handler) 877 | else: # 2.8+ 878 | bpy.app.handlers.depsgraph_update_post.append(updater_run_success_popup_handler) 879 | ran_update_success_popup = True 880 | 881 | 882 | # ----------------------------------------------------------------------------- 883 | # Example UI integrations 884 | # ----------------------------------------------------------------------------- 885 | def update_notice_box_ui(self, context): 886 | """Update notice draw, to add to the end or beginning of a panel. 887 | 888 | After a check for update has occurred, this function will draw a box 889 | saying an update is ready, and give a button for: update now, open website, 890 | or ignore popup. Ideal to be placed at the end / beginning of a panel. 891 | """ 892 | 893 | if updater.invalid_updater: 894 | return 895 | 896 | saved_state = updater.json 897 | if not updater.auto_reload_post_update: 898 | if "just_updated" in saved_state and saved_state["just_updated"]: 899 | layout = self.layout 900 | box = layout.box() 901 | col = box.column() 902 | alert_row = col.row() 903 | alert_row.alert = True 904 | alert_row.operator("wm.quit_blender", text="Restart blender", icon="ERROR") 905 | col.label(text="to complete update") 906 | return 907 | 908 | # If user pressed ignore, don't draw the box. 909 | if "ignore" in updater.json and updater.json["ignore"]: 910 | return 911 | if not updater.update_ready: 912 | return 913 | 914 | layout = self.layout 915 | box = layout.box() 916 | col = box.column(align=True) 917 | col.alert = True 918 | col.label(text="Update ready!", icon="ERROR") 919 | col.alert = False 920 | col.separator() 921 | row = col.row(align=True) 922 | split = row.split(align=True) 923 | colL = split.column(align=True) 924 | colL.scale_y = 1.5 925 | colL.operator(AddonUpdaterIgnore.bl_idname, icon="X", text="Ignore") 926 | colR = split.column(align=True) 927 | colR.scale_y = 1.5 928 | if not updater.manual_only: 929 | colR.operator( 930 | AddonUpdaterUpdateNow.bl_idname, text="Update", icon="LOOP_FORWARDS" 931 | ) 932 | col.operator("wm.url_open", text="Open website").url = updater.website 933 | # ops = col.operator("wm.url_open",text="Direct download") 934 | # ops.url=updater.update_link 935 | col.operator(AddonUpdaterInstallManually.bl_idname, text="Install manually") 936 | else: 937 | # ops = col.operator("wm.url_open", text="Direct download") 938 | # ops.url=updater.update_link 939 | col.operator("wm.url_open", text="Get it now").url = updater.website 940 | 941 | 942 | def update_settings_ui(self, context, element=None): 943 | """Preferences - for drawing with full width inside user preferences 944 | 945 | A function that can be run inside user preferences panel for prefs UI. 946 | Place inside UI draw using: 947 | addon_updater_ops.update_settings_ui(self, context) 948 | or by: 949 | addon_updater_ops.update_settings_ui(context) 950 | """ 951 | 952 | # Element is a UI element, such as layout, a row, column, or box. 953 | if element is None: 954 | element = self.layout 955 | box = element.box() 956 | 957 | # In case of error importing updater. 958 | if updater.invalid_updater: 959 | box.label(text="Error initializing updater code:") 960 | box.label(text=updater.error_msg) 961 | return 962 | settings = get_user_preferences(context) 963 | if not settings: 964 | box.label(text="Error getting updater preferences", icon="ERROR") 965 | return 966 | 967 | # auto-update settings 968 | box.label(text="Updater Settings") 969 | row = box.row() 970 | 971 | # special case to tell user to restart blender, if set that way 972 | if not updater.auto_reload_post_update: 973 | saved_state = updater.json 974 | if "just_updated" in saved_state and saved_state["just_updated"]: 975 | row.alert = True 976 | row.operator( 977 | "wm.quit_blender", 978 | text="Restart blender to complete update", 979 | icon="ERROR", 980 | ) 981 | return 982 | 983 | split = layout_split(row, factor=0.4) 984 | sub_col = split.column() 985 | sub_col.prop(settings, "auto_check_update") 986 | sub_col = split.column() 987 | 988 | if not settings.auto_check_update: 989 | sub_col.enabled = False 990 | sub_row = sub_col.row() 991 | sub_row.label(text="Interval between checks") 992 | sub_row = sub_col.row(align=True) 993 | check_col = sub_row.column(align=True) 994 | check_col.prop(settings, "updater_interval_months") 995 | check_col = sub_row.column(align=True) 996 | check_col.prop(settings, "updater_interval_days") 997 | check_col = sub_row.column(align=True) 998 | 999 | # Consider un-commenting for local dev (e.g. to set shorter intervals) 1000 | # check_col.prop(settings,"updater_interval_hours") 1001 | # check_col = sub_row.column(align=True) 1002 | # check_col.prop(settings,"updater_interval_minutes") 1003 | 1004 | # Checking / managing updates. 1005 | row = box.row() 1006 | col = row.column() 1007 | if updater.error is not None: 1008 | sub_col = col.row(align=True) 1009 | sub_col.scale_y = 1 1010 | split = sub_col.split(align=True) 1011 | split.scale_y = 2 1012 | if "ssl" in updater.error_msg.lower(): 1013 | split.enabled = True 1014 | split.operator(AddonUpdaterInstallManually.bl_idname, text=updater.error) 1015 | else: 1016 | split.enabled = False 1017 | split.operator(AddonUpdaterCheckNow.bl_idname, text=updater.error) 1018 | split = sub_col.split(align=True) 1019 | split.scale_y = 2 1020 | split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") 1021 | 1022 | elif updater.update_ready is None and not updater.async_checking: 1023 | col.scale_y = 2 1024 | col.operator(AddonUpdaterCheckNow.bl_idname) 1025 | elif updater.update_ready is None: # async is running 1026 | sub_col = col.row(align=True) 1027 | sub_col.scale_y = 1 1028 | split = sub_col.split(align=True) 1029 | split.enabled = False 1030 | split.scale_y = 2 1031 | split.operator(AddonUpdaterCheckNow.bl_idname, text="Checking...") 1032 | split = sub_col.split(align=True) 1033 | split.scale_y = 2 1034 | split.operator(AddonUpdaterEndBackground.bl_idname, text="", icon="X") 1035 | 1036 | elif ( 1037 | updater.include_branches 1038 | and len(updater.tags) == len(updater.include_branch_list) 1039 | and not updater.manual_only 1040 | ): 1041 | # No releases found, but still show the appropriate branch. 1042 | sub_col = col.row(align=True) 1043 | sub_col.scale_y = 1 1044 | split = sub_col.split(align=True) 1045 | split.scale_y = 2 1046 | update_now_txt = "Update directly to {}".format(updater.include_branch_list[0]) 1047 | split.operator(AddonUpdaterUpdateNow.bl_idname, text=update_now_txt) 1048 | split = sub_col.split(align=True) 1049 | split.scale_y = 2 1050 | split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") 1051 | 1052 | elif updater.update_ready and not updater.manual_only: 1053 | sub_col = col.row(align=True) 1054 | sub_col.scale_y = 1 1055 | split = sub_col.split(align=True) 1056 | split.scale_y = 2 1057 | split.operator( 1058 | AddonUpdaterUpdateNow.bl_idname, 1059 | text="Update now to " + str(updater.update_version), 1060 | ) 1061 | split = sub_col.split(align=True) 1062 | split.scale_y = 2 1063 | split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") 1064 | 1065 | elif updater.update_ready and updater.manual_only: 1066 | col.scale_y = 2 1067 | dl_now_txt = "Download " + str(updater.update_version) 1068 | col.operator("wm.url_open", text=dl_now_txt).url = updater.website 1069 | else: # i.e. that updater.update_ready == False. 1070 | sub_col = col.row(align=True) 1071 | sub_col.scale_y = 1 1072 | split = sub_col.split(align=True) 1073 | split.enabled = False 1074 | split.scale_y = 2 1075 | split.operator(AddonUpdaterCheckNow.bl_idname, text="Addon is up to date") 1076 | split = sub_col.split(align=True) 1077 | split.scale_y = 2 1078 | split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") 1079 | 1080 | if not updater.manual_only: 1081 | col = row.column(align=True) 1082 | if updater.include_branches and len(updater.include_branch_list) > 0: 1083 | branch = updater.include_branch_list[0] 1084 | col.operator( 1085 | AddonUpdaterUpdateTarget.bl_idname, 1086 | text="Install {} / old version".format(branch), 1087 | ) 1088 | else: 1089 | col.operator( 1090 | AddonUpdaterUpdateTarget.bl_idname, text="(Re)install addon version" 1091 | ) 1092 | last_date = "none found" 1093 | backup_path = os.path.join(updater.stage_path, "backup") 1094 | if "backup_date" in updater.json and os.path.isdir(backup_path): 1095 | if updater.json["backup_date"] == "": 1096 | last_date = "Date not found" 1097 | else: 1098 | last_date = updater.json["backup_date"] 1099 | backup_text = "Restore addon backup ({})".format(last_date) 1100 | col.operator(AddonUpdaterRestoreBackup.bl_idname, text=backup_text) 1101 | 1102 | row = box.row() 1103 | row.scale_y = 0.7 1104 | last_check = updater.json["last_check"] 1105 | if updater.error is not None and updater.error_msg is not None: 1106 | row.label(text=updater.error_msg) 1107 | elif last_check: 1108 | last_check = last_check[0 : last_check.index(".")] 1109 | row.label(text="Last update check: " + last_check) 1110 | else: 1111 | row.label(text="Last update check: Never") 1112 | 1113 | 1114 | def update_settings_ui_condensed(self, context, element=None): 1115 | """Preferences - Condensed drawing within preferences. 1116 | 1117 | Alternate draw for user preferences or other places, does not draw a box. 1118 | """ 1119 | 1120 | # Element is a UI element, such as layout, a row, column, or box. 1121 | if element is None: 1122 | element = self.layout 1123 | row = element.row() 1124 | 1125 | # In case of error importing updater. 1126 | if updater.invalid_updater: 1127 | row.label(text="Error initializing updater code:") 1128 | row.label(text=updater.error_msg) 1129 | return 1130 | settings = get_user_preferences(context) 1131 | if not settings: 1132 | row.label(text="Error getting updater preferences", icon="ERROR") 1133 | return 1134 | 1135 | # Special case to tell user to restart blender, if set that way. 1136 | if not updater.auto_reload_post_update: 1137 | saved_state = updater.json 1138 | if "just_updated" in saved_state and saved_state["just_updated"]: 1139 | row.alert = True # mark red 1140 | row.operator( 1141 | "wm.quit_blender", 1142 | text="Restart blender to complete update", 1143 | icon="ERROR", 1144 | ) 1145 | return 1146 | 1147 | col = row.column() 1148 | if updater.error is not None: 1149 | sub_col = col.row(align=True) 1150 | sub_col.scale_y = 1 1151 | split = sub_col.split(align=True) 1152 | split.scale_y = 2 1153 | if "ssl" in updater.error_msg.lower(): 1154 | split.enabled = True 1155 | split.operator(AddonUpdaterInstallManually.bl_idname, text=updater.error) 1156 | else: 1157 | split.enabled = False 1158 | split.operator(AddonUpdaterCheckNow.bl_idname, text=updater.error) 1159 | split = sub_col.split(align=True) 1160 | split.scale_y = 2 1161 | split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") 1162 | 1163 | elif updater.update_ready is None and not updater.async_checking: 1164 | col.scale_y = 2 1165 | col.operator(AddonUpdaterCheckNow.bl_idname) 1166 | elif updater.update_ready is None: # Async is running. 1167 | sub_col = col.row(align=True) 1168 | sub_col.scale_y = 1 1169 | split = sub_col.split(align=True) 1170 | split.enabled = False 1171 | split.scale_y = 2 1172 | split.operator(AddonUpdaterCheckNow.bl_idname, text="Checking...") 1173 | split = sub_col.split(align=True) 1174 | split.scale_y = 2 1175 | split.operator(AddonUpdaterEndBackground.bl_idname, text="", icon="X") 1176 | 1177 | elif ( 1178 | updater.include_branches 1179 | and len(updater.tags) == len(updater.include_branch_list) 1180 | and not updater.manual_only 1181 | ): 1182 | # No releases found, but still show the appropriate branch. 1183 | sub_col = col.row(align=True) 1184 | sub_col.scale_y = 1 1185 | split = sub_col.split(align=True) 1186 | split.scale_y = 2 1187 | now_txt = "Update directly to " + str(updater.include_branch_list[0]) 1188 | split.operator(AddonUpdaterUpdateNow.bl_idname, text=now_txt) 1189 | split = sub_col.split(align=True) 1190 | split.scale_y = 2 1191 | split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") 1192 | 1193 | elif updater.update_ready and not updater.manual_only: 1194 | sub_col = col.row(align=True) 1195 | sub_col.scale_y = 1 1196 | split = sub_col.split(align=True) 1197 | split.scale_y = 2 1198 | split.operator( 1199 | AddonUpdaterUpdateNow.bl_idname, 1200 | text="Update now to " + str(updater.update_version), 1201 | ) 1202 | split = sub_col.split(align=True) 1203 | split.scale_y = 2 1204 | split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") 1205 | 1206 | elif updater.update_ready and updater.manual_only: 1207 | col.scale_y = 2 1208 | dl_txt = "Download " + str(updater.update_version) 1209 | col.operator("wm.url_open", text=dl_txt).url = updater.website 1210 | else: # i.e. that updater.update_ready == False. 1211 | sub_col = col.row(align=True) 1212 | sub_col.scale_y = 1 1213 | split = sub_col.split(align=True) 1214 | split.enabled = False 1215 | split.scale_y = 2 1216 | split.operator(AddonUpdaterCheckNow.bl_idname, text="Addon is up to date") 1217 | split = sub_col.split(align=True) 1218 | split.scale_y = 2 1219 | split.operator(AddonUpdaterCheckNow.bl_idname, text="", icon="FILE_REFRESH") 1220 | 1221 | row = element.row() 1222 | row.prop(settings, "auto_check_update") 1223 | 1224 | row = element.row() 1225 | row.scale_y = 0.7 1226 | last_check = updater.json["last_check"] 1227 | if updater.error is not None and updater.error_msg is not None: 1228 | row.label(text=updater.error_msg) 1229 | elif last_check != "" and last_check is not None: 1230 | last_check = last_check[0 : last_check.index(".")] 1231 | row.label(text="Last check: " + last_check) 1232 | else: 1233 | row.label(text="Last check: Never") 1234 | 1235 | 1236 | def skip_tag_function(self, tag): 1237 | """A global function for tag skipping. 1238 | 1239 | A way to filter which tags are displayed, e.g. to limit downgrading too 1240 | long ago. 1241 | 1242 | Args: 1243 | self: The instance of the singleton addon update. 1244 | tag: the text content of a tag from the repo, e.g. "v1.2.3". 1245 | 1246 | Returns: 1247 | bool: True to skip this tag name (ie don't allow for downloading this 1248 | version), or False if the tag is allowed. 1249 | """ 1250 | 1251 | # In case of error importing updater. 1252 | if self.invalid_updater: 1253 | return False 1254 | 1255 | # ---- write any custom code here, return true to disallow version ---- # 1256 | # 1257 | # # Filter out e.g. if 'beta' is in name of release 1258 | # if 'beta' in tag.lower(): 1259 | # return True 1260 | # ---- write any custom code above, return true to disallow version --- # 1261 | 1262 | if self.include_branches: 1263 | for branch in self.include_branch_list: 1264 | if tag["name"].lower() == branch: 1265 | return False 1266 | 1267 | # Function converting string to tuple, ignoring e.g. leading 'v'. 1268 | # Be aware that this strips out other text that you might otherwise 1269 | # want to be kept and accounted for when checking tags (e.g. v1.1a vs 1.1b) 1270 | tupled = self.version_tuple_from_text(tag["name"]) 1271 | if not isinstance(tupled, tuple): 1272 | return True 1273 | 1274 | # Select the min tag version - change tuple accordingly. 1275 | if self.version_min_update is not None: 1276 | if tupled < self.version_min_update: 1277 | return True # Skip if current version below this. 1278 | 1279 | # Select the max tag version. 1280 | if self.version_max_update is not None: 1281 | if tupled >= self.version_max_update: 1282 | return True # Skip if current version at or above this. 1283 | 1284 | # In all other cases, allow showing the tag for updating/reverting. 1285 | # To simply and always show all tags, this return False could be moved 1286 | # to the start of the function definition so all tags are allowed. 1287 | return False 1288 | 1289 | 1290 | def select_link_function(self, tag): 1291 | """Only customize if trying to leverage "attachments" in *GitHub* releases. 1292 | 1293 | A way to select from one or multiple attached downloadable files from the 1294 | server, instead of downloading the default release/tag source code. 1295 | """ 1296 | 1297 | # -- Default, universal case (and is the only option for GitLab/Bitbucket) 1298 | link = tag["zipball_url"] 1299 | 1300 | # -- Example: select the first (or only) asset instead source code -- 1301 | # if "assets" in tag and "browser_download_url" in tag["assets"][0]: 1302 | # link = tag["assets"][0]["browser_download_url"] 1303 | 1304 | # -- Example: select asset based on OS, where multiple builds exist -- 1305 | # # not tested/no error checking, modify to fit your own needs! 1306 | # # assume each release has three attached builds: 1307 | # # release_windows.zip, release_OSX.zip, release_linux.zip 1308 | # # This also would logically not be used with "branches" enabled 1309 | # if platform.system() == "Darwin": # ie OSX 1310 | # link = [asset for asset in tag["assets"] if 'OSX' in asset][0] 1311 | # elif platform.system() == "Windows": 1312 | # link = [asset for asset in tag["assets"] if 'windows' in asset][0] 1313 | # elif platform.system() == "Linux": 1314 | # link = [asset for asset in tag["assets"] if 'linux' in asset][0] 1315 | 1316 | return link 1317 | 1318 | 1319 | # ----------------------------------------------------------------------------- 1320 | # Register, should be run in the register module itself 1321 | # ----------------------------------------------------------------------------- 1322 | classes = ( 1323 | AddonUpdaterInstallPopup, 1324 | AddonUpdaterCheckNow, 1325 | AddonUpdaterUpdateNow, 1326 | AddonUpdaterUpdateTarget, 1327 | AddonUpdaterInstallManually, 1328 | AddonUpdaterUpdatedSuccessful, 1329 | AddonUpdaterRestoreBackup, 1330 | AddonUpdaterIgnore, 1331 | AddonUpdaterEndBackground, 1332 | ) 1333 | 1334 | 1335 | def register(bl_info): 1336 | """Registering the operators in this module""" 1337 | # Safer failure in case of issue loading module. 1338 | if updater.error: 1339 | print("Exiting updater registration, " + updater.error) 1340 | return 1341 | updater.clear_state() # Clear internal vars, avoids reloading oddities. 1342 | 1343 | # Confirm your updater "engine" (Github is default if not specified). 1344 | updater.engine = "Github" 1345 | # updater.engine = "GitLab" 1346 | # updater.engine = "Bitbucket" 1347 | 1348 | # If using private repository, indicate the token here. 1349 | # Must be set after assigning the engine. 1350 | # **WARNING** Depending on the engine, this token can act like a password!! 1351 | # Only provide a token if the project is *non-public*, see readme for 1352 | # other considerations and suggestions from a security standpoint. 1353 | updater.private_token = None # "tokenstring" 1354 | 1355 | # Choose your own username, must match website (not needed for GitLab). 1356 | updater.user = "gregzaal" 1357 | 1358 | # Choose your own repository, must match git name for GitHUb and Bitbucket, 1359 | # for GitLab use project ID (numbers only). 1360 | updater.repo = "Gaffer" 1361 | 1362 | # updater.addon = # define at top of module, MUST be done first 1363 | 1364 | # Website for manual addon download, optional but recommended to set. 1365 | updater.website = "https://blendermarket.com/products/gaffer-light-manager" 1366 | 1367 | # Addon subfolder path. 1368 | # "sample/path/to/addon" 1369 | # default is "" or None, meaning root 1370 | updater.subfolder_path = "" 1371 | 1372 | # Used to check/compare versions. 1373 | updater.current_version = bl_info["version"] 1374 | 1375 | # Optional, to hard-set update frequency, use this here - however, this 1376 | # demo has this set via UI properties. 1377 | # updater.set_check_interval(enabled=False, months=0, days=0, hours=0, minutes=2) 1378 | 1379 | # Optional, consider turning off for production or allow as an option 1380 | # This will print out additional debugging info to the console 1381 | updater.verbose = False # make False for production default 1382 | 1383 | # Optional, customize where the addon updater processing subfolder is, 1384 | # essentially a staging folder used by the updater on its own 1385 | # Needs to be within the same folder as the addon itself 1386 | # Need to supply a full, absolute path to folder 1387 | # updater.updater_path = # set path of updater folder, by default: 1388 | # /addons/{__package__}/{__package__}_updater 1389 | 1390 | # Auto create a backup of the addon when installing other versions. 1391 | updater.backup_current = True # True by default 1392 | 1393 | # Sample ignore patterns for when creating backup of current during update. 1394 | updater.backup_ignore_patterns = ["__pycache__"] 1395 | # Alternate example patterns: 1396 | # updater.backup_ignore_patterns = [".git", "__pycache__", "*.bat", ".gitignore", "*.exe"] 1397 | 1398 | # Patterns for files to actively overwrite if found in new update file and 1399 | # are also found in the currently installed addon. Note that by default 1400 | # (ie if set to []), updates are installed in the same way as blender: 1401 | # .py files are replaced, but other file types (e.g. json, txt, blend) 1402 | # will NOT be overwritten if already present in current install. Thus 1403 | # if you want to automatically update resources/non py files, add them as a 1404 | # part of the pattern list below so they will always be overwritten by an 1405 | # update. If a pattern file is not found in new update, no action is taken 1406 | # NOTE: This does NOT delete anything proactively, rather only defines what 1407 | # is allowed to be overwritten during an update execution. 1408 | updater.overwrite_patterns = ["*.png", "*.jpg", "*.md", "*.txt"] 1409 | # updater.overwrite_patterns = [] 1410 | # other examples: 1411 | # ["*"] means ALL files/folders will be overwritten by update, was the 1412 | # behavior pre updater v1.0.4. 1413 | # [] or ["*.py","*.pyc"] matches default blender behavior, ie same effect 1414 | # if user installs update manually without deleting the existing addon 1415 | # first e.g. if existing install and update both have a resource.blend 1416 | # file, the existing installed one will remain. 1417 | # ["some.py"] means if some.py is found in addon update, it will overwrite 1418 | # any existing some.py in current addon install, if any. 1419 | # ["*.json"] means all json files found in addon update will overwrite 1420 | # those of same name in current install. 1421 | # ["*.png","README.md","LICENSE.txt"] means the readme, license, and all 1422 | # pngs will be overwritten by update. 1423 | 1424 | # Patterns for files to actively remove prior to running update. 1425 | # Useful if wanting to remove old code due to changes in filenames 1426 | # that otherwise would accumulate. Note: this runs after taking 1427 | # a backup (if enabled) but before placing in new update. If the same 1428 | # file name removed exists in the update, then it acts as if pattern 1429 | # is placed in the overwrite_patterns property. Note this is effectively 1430 | # ignored if clean=True in the run_update method. 1431 | updater.remove_pre_update_patterns = ["*.py", "*.pyc"] 1432 | # Note setting ["*"] here is equivalent to always running updates with 1433 | # clean = True in the run_update method, ie the equivalent of a fresh, 1434 | # new install. This would also delete any resources or user-made/modified 1435 | # files setting ["__pycache__"] ensures the pycache folder always removed. 1436 | # The configuration of ["*.py", "*.pyc"] is a safe option as this 1437 | # will ensure no old python files/caches remain in event different addon 1438 | # versions have different filenames or structures. 1439 | 1440 | # Allow branches like 'master' as an option to update to, regardless 1441 | # of release or version. 1442 | # Default behavior: releases will still be used for auto check (popup), 1443 | # but the user has the option from user preferences to directly 1444 | # update to the master branch or any other branches specified using 1445 | # the "install {branch}/older version" operator. 1446 | updater.include_branches = True 1447 | 1448 | # (GitHub only) This options allows using "releases" instead of "tags", 1449 | # which enables pulling down release logs/notes, as well as installs update 1450 | # from release-attached zips (instead of the auto-packaged code generated 1451 | # with a release/tag). Setting has no impact on BitBucket or GitLab repos. 1452 | updater.use_releases = False 1453 | # Note: Releases always have a tag, but a tag may not always be a release. 1454 | # Therefore, setting True above will filter out any non-annotated tags. 1455 | # Note 2: Using this option will also display (and filter by) the release 1456 | # name instead of the tag name, bear this in mind given the 1457 | # skip_tag_function filtering above. 1458 | 1459 | # Populate if using "include_branches" option above. 1460 | # Note: updater.include_branch_list defaults to ['master'] branch if set to 1461 | # none. Example targeting another multiple branches allowed to pull from: 1462 | # updater.include_branch_list = ['master', 'dev'] 1463 | updater.include_branch_list = None # None is the equivalent = ['master'] 1464 | 1465 | # Only allow manual install, thus prompting the user to open 1466 | # the addon's web page to download, specifically: updater.website 1467 | # Useful if only wanting to get notification of updates but not 1468 | # directly install. 1469 | updater.manual_only = False 1470 | 1471 | # Used for development only, "pretend" to install an update to test 1472 | # reloading conditions. 1473 | updater.fake_install = False # Set to true to test callback/reloading. 1474 | 1475 | # Show popups, ie if auto-check for update is enabled or a previous 1476 | # check for update in user preferences found a new version, show a popup 1477 | # (at most once per blender session, and it provides an option to ignore 1478 | # for future sessions); default behavior is set to True. 1479 | updater.show_popups = True 1480 | # note: if set to false, there will still be an "update ready" box drawn 1481 | # using the `update_notice_box_ui` panel function. 1482 | 1483 | # Override with a custom function on what tags 1484 | # to skip showing for updater; see code for function above. 1485 | # Set the min and max versions allowed to install. 1486 | # Optional, default None 1487 | # min install (>=) will install this and higher 1488 | # updater.version_min_update = (0, 0, 0) 1489 | updater.version_min_update = None # None or default for no minimum. 1490 | 1491 | # Max install (<) will install strictly anything lower than this version 1492 | # number, useful to limit the max version a given user can install (e.g. 1493 | # if support for a future version of blender is going away, and you don't 1494 | # want users to be prompted to install a non-functioning addon) 1495 | # updater.version_max_update = (9,9,9) 1496 | updater.version_max_update = None # None or default for no max. 1497 | 1498 | # Function defined above, customize as appropriate per repository 1499 | updater.skip_tag = skip_tag_function # min and max used in this function 1500 | 1501 | # Function defined above, optionally customize as needed per repository. 1502 | updater.select_link = select_link_function 1503 | 1504 | # Recommended false to encourage blender restarts on update completion 1505 | # Setting this option to True is NOT as stable as false (could cause 1506 | # blender crashes). 1507 | updater.auto_reload_post_update = False 1508 | 1509 | # The register line items for all operators/panels. 1510 | # If using bpy.utils.register_module(__name__) to register elsewhere 1511 | # in the addon, delete these lines (also from unregister). 1512 | for cls in classes: 1513 | # Apply annotations to remove Blender 2.8+ warnings, no effect on 2.7 1514 | make_annotations(cls) 1515 | # Comment out this line if using bpy.utils.register_module(__name__) 1516 | bpy.utils.register_class(cls) 1517 | 1518 | # Special situation: we just updated the addon, show a popup to tell the 1519 | # user it worked. Could enclosed in try/catch in case other issues arise. 1520 | show_reload_popup() 1521 | 1522 | 1523 | def unregister(): 1524 | for cls in reversed(classes): 1525 | # Comment out this line if using bpy.utils.unregister_module(__name__). 1526 | bpy.utils.unregister_class(cls) 1527 | 1528 | # Clear global vars since they may persist if not restarting blender. 1529 | updater.clear_state() # Clear internal vars, avoids reloading oddities. 1530 | 1531 | global ran_auto_check_install_popup 1532 | ran_auto_check_install_popup = False 1533 | 1534 | global ran_update_success_popup 1535 | ran_update_success_popup = False 1536 | 1537 | global ran_background_check 1538 | ran_background_check = False 1539 | -------------------------------------------------------------------------------- /ui.py: -------------------------------------------------------------------------------- 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 | import bpy 20 | import os 21 | from . import addon_updater_ops 22 | from collections import OrderedDict 23 | 24 | from . import constants as const 25 | from . import functions as fn 26 | from . import operators as ops 27 | 28 | 29 | def update_category(self, context): 30 | classes = [GAFFER_PT_lights, GAFFER_PT_tools] 31 | for panel in classes: 32 | try: 33 | bpy.utils.unregister_class(panel) 34 | except RuntimeError: 35 | # Not yet registered 36 | pass 37 | panel.bl_category = self.panel_category 38 | bpy.utils.register_class(panel) 39 | 40 | 41 | # UI stuff that's shown for all renderers 42 | def draw_renderer_independant(gaf_props, row, light, icons, users=[None, 1]): 43 | 44 | if bpy.context.scene.render.engine in const.supported_renderers: 45 | if "_Light:_(" + light.name + ")_" in gaf_props.MoreExpand and not gaf_props.MoreExpandAll: 46 | row.operator( 47 | ops.GAFFER_OT_hide_more.bl_idname, 48 | icon="DOWNARROW_HLT", 49 | text="", 50 | emboss=False, 51 | ).light = light.name 52 | elif not gaf_props.MoreExpandAll: 53 | row.operator( 54 | ops.GAFFER_OT_show_more.bl_idname, 55 | icon="RIGHTARROW", 56 | text="", 57 | emboss=False, 58 | ).light = light.name 59 | 60 | data_name = light.name if users[1] == 1 else (users[0][5:] if users[0].startswith("LIGHT") else users[0][3:]) 61 | if gaf_props.SoloActive == "": 62 | prefs = bpy.context.preferences.addons[__package__].preferences 63 | if users[1] == 1: 64 | if prefs.auto_refresh_light_list: 65 | row.prop(light, "name", text="") 66 | else: 67 | row.operator(ops.GAFFER_OT_rename.bl_idname, text=data_name).light = light.name 68 | else: 69 | op = row.operator( 70 | ops.GAFFER_OT_set_light_data_user_names.bl_idname, 71 | text="", 72 | icon_value=icons[f"num_{str(min(10, users[1]))}"].icon_id, 73 | ) 74 | op.data_name = data_name 75 | op.data_type = "lights" if users[0].startswith("LIGHT") else "materials" 76 | if prefs.auto_refresh_light_list: 77 | if light.type == "LIGHT": 78 | row.prop(light.data, "name", text="") 79 | else: 80 | row.prop(bpy.data.materials[data_name], "name", text="") 81 | else: 82 | op = row.operator( 83 | ops.GAFFER_OT_rename.bl_idname, 84 | text=data_name, 85 | ) 86 | op.multiuser = users[0] 87 | op.light = data_name 88 | else: 89 | # Don't allow names to be edited during solo, will break the record of what was originally hidden 90 | row.label(text=data_name) 91 | 92 | visop = row.operator( 93 | ops.GAFFER_OT_hide_show_light.bl_idname, 94 | text="", 95 | icon="%s" % "HIDE_ON" if light.hide_viewport else "HIDE_OFF", 96 | emboss=False, 97 | ) 98 | visop.light = light.name 99 | visop.dataname = users[0] if users[1] > 1 else "__SINGLE_USER__" 100 | visop.hide = not light.hide_viewport 101 | 102 | sub = row.column(align=True) 103 | sub.alert = light.select_get() 104 | selop = sub.operator( 105 | ops.GAFFER_OT_select_light.bl_idname, 106 | text="", 107 | icon="%s" % "RESTRICT_SELECT_OFF" if light.select_get() else "RESTRICT_SELECT_ON", 108 | emboss=False, 109 | ) 110 | selop.light = light.name 111 | selop.dataname = users[0] if users[1] > 1 else "__SINGLE_USER__" 112 | 113 | if gaf_props.SoloActive == "": 114 | sub = row.column(align=True) 115 | solobtn = sub.operator(ops.GAFFER_OT_solo.bl_idname, icon="EVENT_S", text="", emboss=False) 116 | solobtn.light = light.name 117 | solobtn.showhide = True 118 | solobtn.worldsolo = False 119 | solobtn.dataname = users[0] if users[1] > 1 else "__SINGLE_USER__" 120 | elif gaf_props.SoloActive == light.name: 121 | sub = row.column(align=True) 122 | sub.alert = True 123 | solobtn = sub.operator(ops.GAFFER_OT_solo.bl_idname, icon="EVENT_S", text="", emboss=False) 124 | solobtn.light = light.name 125 | solobtn.showhide = False 126 | solobtn.worldsolo = False 127 | 128 | 129 | def draw_cycles_eevee_UI(context, layout, lights): 130 | def draw_strength_cycles(col, light, material, node_strength, socket_strength_type, socket_strength): 131 | row = col.row(align=True) 132 | strength_sockets = node_strength.inputs 133 | if socket_strength_type == "o": 134 | strength_sockets = node_strength.outputs 135 | if light.type == "LIGHT": 136 | row.prop( 137 | light.data, 138 | "type", 139 | text="", 140 | icon="LIGHT_%s" % light.data.type, 141 | icon_only=True, 142 | ) 143 | else: 144 | row.label(text="", icon="MESH_GRID") 145 | 146 | try: 147 | if ( 148 | (socket_strength_type == "i" and not strength_sockets[socket_strength].is_linked) 149 | or (socket_strength_type == "o" and strength_sockets[socket_strength].is_linked) 150 | ) and hasattr(strength_sockets[socket_strength], "default_value"): 151 | op = row.operator(ops.GAFFER_OT_set_strength.bl_idname, text="", icon="REMOVE") 152 | op.light = light.name 153 | op.node = node_strength.name 154 | op.material = material.name if material else "" 155 | op.socket_strength = socket_strength 156 | op.socket_strength_type = socket_strength_type 157 | op.increase = False 158 | row.prop(strength_sockets[socket_strength], "default_value", text="Strength") 159 | op = row.operator(ops.GAFFER_OT_set_strength.bl_idname, text="", icon="ADD") 160 | op.light = light.name 161 | op.node = node_strength.name 162 | op.material = material.name if material else "" 163 | op.socket_strength = socket_strength 164 | op.socket_strength_type = socket_strength_type 165 | op.increase = True 166 | else: 167 | row.label(text=" Node Invalid") 168 | except (KeyError, IndexError, AttributeError): 169 | row.label(text=" Node Invalid") 170 | 171 | def draw_strength_eevee(col, light): 172 | row = col.row(align=True) 173 | row.prop( 174 | light.data, 175 | "type", 176 | text="", 177 | icon="LIGHT_%s" % light.data.type, 178 | icon_only=True, 179 | ) 180 | op = row.operator(ops.GAFFER_OT_set_strength.bl_idname, text="", icon="REMOVE") 181 | op.light = light.name 182 | op.node = "" 183 | op.material = "" 184 | op.socket_strength = 0 185 | op.socket_strength_type = "" 186 | op.increase = False 187 | row.prop(light.data, "energy", text="Strength") 188 | op = row.operator(ops.GAFFER_OT_set_strength.bl_idname, text="", icon="ADD") 189 | op.light = light.name 190 | op.node = "" 191 | op.material = "" 192 | op.socket_strength = 0 193 | op.socket_strength_type = "" 194 | op.increase = True 195 | 196 | def draw_color_cycles(gaf_props, i, icons, col, row, light, material): 197 | if light.type == "LIGHT": 198 | nodes = light.data.node_tree.nodes 199 | else: 200 | nodes = material.node_tree.nodes 201 | socket_color = 0 202 | node_color = None 203 | emissions = [] # make a list of all linked Emission shaders, use the right-most one 204 | for node in nodes: 205 | if node.type == "EMISSION": 206 | if node.outputs[0].is_linked: 207 | emissions.append(node) 208 | if emissions: 209 | node_color = sorted(emissions, key=lambda x: x.location.x, reverse=True)[0] 210 | 211 | if not node_color.inputs[socket_color].is_linked: 212 | subcol = row.column(align=True) 213 | subrow = subcol.row(align=True) 214 | subrow.scale_x = 0.3 215 | subrow.prop(node_color.inputs[socket_color], "default_value", text="") 216 | else: 217 | from_node = node_color.inputs[socket_color].links[0].from_node 218 | if from_node.type == "RGB": 219 | subcol = row.column(align=True) 220 | subrow = subcol.row(align=True) 221 | subrow.scale_x = 0.3 222 | subrow.prop(from_node.outputs[0], "default_value", text="") 223 | elif from_node.type == "TEX_IMAGE" or from_node.type == "TEX_ENVIRONMENT": 224 | row.prop(from_node, "image", text="") 225 | elif from_node.type == "BLACKBODY": 226 | row.prop(from_node.inputs[0], "default_value", text="Temperature") 227 | if gaf_props.ColTempExpand and gaf_props.LightUIIndex == i: 228 | row.operator( 229 | ops.GAFFER_OT_hide_temp_list.bl_idname, 230 | text="", 231 | icon="TRIA_UP", 232 | ) 233 | col = col.column(align=True) 234 | col.separator() 235 | col.label(text="Color Temp. Presets:") 236 | ordered_col_temps = OrderedDict(sorted(const.col_temp.items())) 237 | for temp in ordered_col_temps: 238 | op = col.operator( 239 | ops.GAFFER_OT_set_temp.bl_idname, 240 | text=temp[3:], 241 | icon_value=icons[str(const.col_temp[temp])].icon_id, 242 | ) 243 | op.temperature = temp 244 | op.light = light.name 245 | if material: 246 | op.material = material.name 247 | if node_color: 248 | op.node = node_color.name 249 | col.separator() 250 | else: 251 | row.operator( 252 | ops.GAFFER_OT_show_temp_list.bl_idname, 253 | text="", 254 | icon="COLOR", 255 | ).l_index = i 256 | elif from_node.type == "WAVELENGTH": 257 | row.prop(from_node.inputs[0], "default_value", text="Wavelength") 258 | 259 | def draw_color_eevee(row, light): 260 | subcol = row.column(align=True) 261 | subrow = subcol.row(align=True) 262 | subrow.scale_x = 0.3 263 | subrow.prop(light.data, "color", text="") 264 | 265 | def draw_more_options_cycles(box, scene, light, material, node_strength, is_portal): 266 | col = box.column() 267 | row = col.row(align=True) 268 | 269 | visibility = [ 270 | ("camera", "Cam"), 271 | ("diffuse", "Diff"), 272 | ("glossy", "Spec"), 273 | ] 274 | 275 | if hasattr(light.data, "shape"): 276 | row.prop(light.data, "shape", text="") 277 | row = col.row(align=True) 278 | 279 | if light.type == "LIGHT": 280 | if light.data.type == "AREA": 281 | if light.data.shape in ["RECTANGLE", "ELLIPSE"]: 282 | row.prop(light.data, "size") 283 | row.prop(light.data, "size_y") 284 | row = col.row(align=True) 285 | else: 286 | row.prop(light.data, "size") 287 | if hasattr(light.data, "spread"): 288 | row.prop(light.data, "spread") 289 | elif light.data.type == "SUN": 290 | row.prop(light.data, "angle") 291 | else: 292 | row.prop(light.data, "shadow_soft_size", text="Size") 293 | 294 | if hasattr(scene.cycles, "progressive") and scene.cycles.progressive == "BRANCHED_PATH": 295 | row.prop(light.data.cycles, "samples") 296 | 297 | if not is_portal: 298 | row = col.row(align=True) 299 | row.prop( 300 | light.data.cycles, 301 | "use_multiple_importance_sampling", 302 | text="MIS", 303 | toggle=True, 304 | ) 305 | if hasattr(light.data.cycles, "cast_shadow"): # Before Blender 4.2 306 | row.prop(light.data.cycles, "cast_shadow", text="Shadows", toggle=True) 307 | else: # Blender 4.2+ 308 | row.prop(light.data, "use_shadow", text="Shadows", toggle=True) 309 | row.separator() 310 | for v in visibility[1:]: # All but camera 311 | if bpy.app.version_string >= "3.0": 312 | row.prop(light, "visible_" + v[0], text=v[1], toggle=True) 313 | else: 314 | row.prop(light.cycles_visibility, v[0], text=v[1], toggle=True) 315 | 316 | if light.data.type == "SPOT": 317 | row = col.row(align=True) 318 | row.prop(light.data, "spot_size", text="Spot Size") 319 | row.prop(light.data, "spot_blend", text="Blend", slider=True) 320 | row.prop(light.data, "show_cone", text="", toggle=True, icon="CONE") 321 | 322 | else: # MESH light 323 | if hasattr(material.cycles, "sample_as_light"): 324 | row.prop(material.cycles, "sample_as_light", text="MIS", toggle=True) 325 | row.separator() 326 | for v in visibility: 327 | if bpy.app.version_string >= "3.0": 328 | row.prop(light, "visible_" + v[0], text=v[1], toggle=True) 329 | else: 330 | row.prop(light.cycles_visibility, v[0], text=v[1], toggle=True) 331 | 332 | if hasattr(light, "GafferFalloff") and scene.render.engine == "CYCLES": 333 | drawfalloff = True 334 | if light.type == "LIGHT": 335 | if ( 336 | light.data.type == "SUN" 337 | or light.data.type == "HEMI" 338 | or (light.data.type == "AREA" and light.data.cycles.is_portal) 339 | ): 340 | drawfalloff = False 341 | if not light.data.use_nodes: 342 | drawfalloff = False 343 | if drawfalloff and node_strength is not None: 344 | col.prop(light, "GafferFalloff", text="Falloff") 345 | if node_strength.type != "LIGHT_FALLOFF" and light.GafferFalloff != "quadratic": 346 | col.label(text="Light Falloff node is missing", icon="ERROR") 347 | 348 | if light.type == "LIGHT": 349 | if light.data.type == "AREA": 350 | col.prop(light.data.cycles, "is_portal") 351 | 352 | # Light groups 353 | if len(context.view_layer.lightgroups) > 0: 354 | row = col.row(align=True) 355 | row.use_property_decorate = False 356 | 357 | sub = row.column(align=True) 358 | sub.prop_search( 359 | light, "lightgroup", context.view_layer, "lightgroups", text="Group", results_are_suggestions=True 360 | ) 361 | 362 | sub = row.column(align=True) 363 | if bool(light.lightgroup) and not any(lg.name == light.lightgroup for lg in context.view_layer.lightgroups): 364 | row.operator("scene.view_layer_add_lightgroup", icon="ADD", text="").name = light.lightgroup 365 | 366 | def draw_more_options_eevee(box, scene, light): 367 | col = box.column() 368 | row = col.row(align=True) 369 | 370 | if light.data.type == "AREA": 371 | if light.data.shape in ["RECTANGLE", "ELLIPSE"]: 372 | row.prop(light.data, "size") 373 | row.prop(light.data, "size_y") 374 | row = col.row(align=True) 375 | else: 376 | row.prop(light.data, "size") 377 | elif light.data.type == "SUN": 378 | row.prop(light.data, "angle") 379 | else: 380 | row.prop(light.data, "shadow_soft_size", text="Size") 381 | 382 | row = col.row(align=True) 383 | row.prop(light.data, "use_shadow", text="Shadows", toggle=True) 384 | row.separator() 385 | row.prop(light.data, "specular_factor") 386 | 387 | if light.data.type == "SPOT": 388 | row = col.row(align=True) 389 | row.prop(light.data, "spot_size", text="Spot Size") 390 | row.prop(light.data, "spot_blend", text="Blend", slider=True) 391 | row.prop(light.data, "show_cone", text="", toggle=True, icon="CONE") 392 | 393 | def draw_world(context, layout, gaf_props, gaf_hdri_props, scene, prefs, icons): 394 | world = context.scene.world 395 | box = layout.box() 396 | worldcol = box.column(align=True) 397 | col = worldcol.column(align=True) 398 | 399 | row = col.row(align=True) 400 | 401 | if "_Light:_(WorldEnviroLight)_" in gaf_props.MoreExpand and not gaf_props.MoreExpandAll: 402 | row.operator( 403 | ops.GAFFER_OT_hide_more.bl_idname, 404 | icon="TRIA_DOWN", 405 | text="", 406 | emboss=False, 407 | ).light = "WorldEnviroLight" 408 | elif not gaf_props.MoreExpandAll: 409 | row.operator( 410 | ops.GAFFER_OT_show_more.bl_idname, 411 | text="", 412 | icon="TRIA_RIGHT", 413 | emboss=False, 414 | ).light = "WorldEnviroLight" 415 | 416 | row.label(text="World") 417 | if scene.render.engine == "CYCLES": 418 | row.prop( 419 | gaf_props, 420 | "WorldVis", 421 | text="", 422 | icon="%s" % "HIDE_OFF" if gaf_props.WorldVis else "HIDE_ON", 423 | emboss=False, 424 | ) 425 | 426 | if gaf_props.SoloActive == "": 427 | sub = row.column(align=True) 428 | solobtn = sub.operator(ops.GAFFER_OT_solo.bl_idname, icon="EVENT_S", text="", emboss=False) 429 | solobtn.light = "WorldEnviroLight" 430 | solobtn.showhide = True 431 | solobtn.worldsolo = True 432 | elif gaf_props.SoloActive == "WorldEnviroLight": 433 | sub = row.column(align=True) 434 | sub.alert = True 435 | solobtn = sub.operator(ops.GAFFER_OT_solo.bl_idname, icon="EVENT_S", text="", emboss=False) 436 | solobtn.light = "WorldEnviroLight" 437 | solobtn.showhide = False 438 | solobtn.worldsolo = True 439 | 440 | col = worldcol.column() 441 | 442 | if gaf_hdri_props.hdri_handler_enabled: 443 | draw_hdri_handler( 444 | context, 445 | col, 446 | gaf_props, 447 | gaf_hdri_props, 448 | fn.get_persistent_setting("hdri_paths"), 449 | prefs, 450 | icons, 451 | toolbar=True, 452 | ) 453 | else: 454 | row = col.row(align=True) 455 | 456 | row.label(text="", icon="WORLD") 457 | row.separator() 458 | 459 | color_node = None 460 | if world.use_nodes: 461 | backgrounds = [] # make a list of all linked Background shaders, use the right-most one 462 | background = None 463 | for node in world.node_tree.nodes: 464 | if node.type == "BACKGROUND": 465 | if not node.name.startswith("HDRIHandler_"): 466 | if node.outputs[0].is_linked: 467 | backgrounds.append(node) 468 | if backgrounds: 469 | background = sorted(backgrounds, key=lambda x: x.location.x, reverse=True)[0] 470 | # Strength 471 | if background.inputs[1].is_linked: 472 | strength_node = None 473 | current_node = background.inputs[1].links[0].from_node 474 | temp_current_node = None 475 | # Failsafe in case of infinite loop (which can happen from accidental cyclic links) 476 | i = 0 477 | while strength_node is None and i < 1000: # limitted to 100 chained nodes 478 | i += 1 479 | connected_inputs = False 480 | if temp_current_node: 481 | current_node = temp_current_node 482 | for socket in current_node.inputs: 483 | # stop at first node with an unconnected Value socket 484 | if socket.type == "VALUE" and not socket.is_linked: 485 | strength_node = current_node 486 | else: 487 | if socket.is_linked: 488 | temp_current_node = socket.links[0].from_node 489 | 490 | if strength_node: 491 | for socket in strength_node.inputs: 492 | if socket.type == "VALUE" and not socket.is_linked: # use first color socket 493 | row.prop(socket, "default_value", text="Strength") 494 | break 495 | else: 496 | row.prop(background.inputs[1], "default_value", text="Strength") 497 | 498 | # Color 499 | if background.inputs[0].is_linked: 500 | current_node = background.inputs[0].links[0].from_node 501 | # Failsafe in case of infinite loop (which can happen from accidental cyclic links) 502 | i = 0 503 | while color_node is None and i < 100: # limitted to 100 chained nodes 504 | i += 1 505 | connected_inputs = False 506 | for socket in current_node.inputs: 507 | # stop at node end of chain, or node with only vector inputs: 508 | if socket.type != "VECTOR" and socket.is_linked: 509 | connected_inputs = True 510 | current_node = socket.links[0].from_node 511 | if not connected_inputs: 512 | color_node = current_node 513 | 514 | if color_node.type == "TEX_IMAGE" or color_node.type == "TEX_ENVIRONMENT": 515 | row.prop(color_node, "image", text="") 516 | elif color_node.type == "TEX_SKY": 517 | row.prop(color_node, "sun_direction", text="") 518 | else: 519 | if color_node.inputs: 520 | for socket in color_node.inputs: 521 | if socket.type == "RGBA": # use first color socket 522 | row.prop(socket, "default_value", text="") 523 | break 524 | else: 525 | row.prop(background.inputs[0], "default_value", text="") 526 | else: 527 | row.label(text="No node found!") 528 | else: 529 | row.prop(world, "horizon_color", text="") 530 | 531 | # Extra 532 | if "_Light:_(WorldEnviroLight)_" in gaf_props.MoreExpand or gaf_props.MoreExpandAll: 533 | worldcol.separator() 534 | col = worldcol.column() 535 | if scene.render.engine == "CYCLES": 536 | row = col.row() 537 | row.prop(world.cycles, "sampling_method", text="") 538 | row.prop(gaf_props, "WorldReflOnly", text="Refl Only") 539 | if world.cycles.sampling_method != "NONE": 540 | col = worldcol.column() 541 | row = col.row(align=True) 542 | if world.cycles.sampling_method == "MANUAL": 543 | row.prop(world.cycles, "sample_map_resolution", text="MIS res") 544 | if hasattr(scene.cycles, "progressive") and scene.cycles.progressive == "BRANCHED_PATH": 545 | row.prop(world.cycles, "samples", text="Samples") 546 | worldcol.separator() 547 | col = worldcol.column(align=True) 548 | if hasattr(scene.cycles, "use_fast_gi"): 549 | col.prop( 550 | scene.cycles, 551 | "use_fast_gi", 552 | text="Fast GI", 553 | ) 554 | if scene.cycles.use_fast_gi: 555 | row = col.row(align=True) 556 | row.prop(scene.cycles, "fast_gi_method", text="") 557 | row.prop(world.light_settings, "ao_factor") 558 | row.prop(world.light_settings, "distance") 559 | else: 560 | # Legacy Blender support 561 | col.prop( 562 | world.light_settings, 563 | "use_ambient_occlusion", 564 | text="Ambient Occlusion", 565 | ) 566 | if world.light_settings.use_ambient_occlusion: 567 | row = col.row(align=True) 568 | row.prop(world.light_settings, "ao_factor") 569 | row.prop(world.light_settings, "distance") 570 | 571 | # Light groups 572 | if len(context.view_layer.lightgroups) > 0: 573 | row = col.row(align=True) 574 | row.use_property_decorate = False 575 | 576 | sub = row.column(align=True) 577 | sub.prop_search( 578 | world, 579 | "lightgroup", 580 | context.view_layer, 581 | "lightgroups", 582 | text="Group", 583 | results_are_suggestions=True, 584 | ) 585 | 586 | sub = row.column(align=True) 587 | if bool(world.lightgroup) and not any( 588 | lg.name == world.lightgroup for lg in context.view_layer.lightgroups 589 | ): 590 | row.operator("scene.view_layer_add_lightgroup", icon="ADD", text="").name = world.lightgroup 591 | else: 592 | worldcol.separator() 593 | col = worldcol.column(align=True) 594 | col.prop(scene.eevee, "use_gtao", text="Ambient Occlusion") 595 | if scene.eevee.use_gtao: 596 | row = col.row(align=True) 597 | row.prop(scene.eevee, "gtao_factor") 598 | row.prop(scene.eevee, "gtao_distance") 599 | 600 | if not gaf_hdri_props.hdri_handler_enabled: 601 | if color_node: 602 | if color_node.type == "TEX_SKY": 603 | if world.node_tree and world.use_nodes: 604 | col = worldcol.column(align=True) 605 | row = col.row(align=True) 606 | if gaf_props.SunObject: 607 | row.operator( 608 | ops.GAFFER_OT_link_sky_to_sun.bl_idname, 609 | icon="LIGHT_SUN", 610 | ).node_name = color_node.name 611 | else: 612 | row.label(text="Link Sky Texture:") 613 | row.prop_search(gaf_props, "SunObject", bpy.data, "objects", text="") 614 | 615 | maincol = layout.column(align=False) 616 | scene = context.scene 617 | gaf_props = scene.gaf_props 618 | prefs = context.preferences.addons[__package__].preferences 619 | icons = fn.get_icons() 620 | 621 | lights_to_show = [] 622 | # Check validity of list and make list of lights to display 623 | vis_cols = fn.visibleCollections() 624 | for light in lights: 625 | try: 626 | if light[0]: 627 | # Will cause KeyError exception if obj no longer exists 628 | a = bpy.data.objects[light[0][1:-1]] 629 | if (gaf_props.VisibleLightsOnly and not a.hide_viewport) or (not gaf_props.VisibleLightsOnly): 630 | if a.type != "LIGHT": 631 | b = bpy.data.materials[light[1][1:-1]] 632 | if b.use_nodes: 633 | b.node_tree.nodes[light[2][1:-1]] 634 | if (gaf_props.VisibleCollectionsOnly and fn.isInVisibleCollection(a, vis_cols)) or ( 635 | not gaf_props.VisibleCollectionsOnly 636 | ): 637 | if a.name not in [o.name for o in gaf_props.Blacklist]: 638 | lights_to_show.append(light) 639 | except KeyError: 640 | box = maincol.box() 641 | row = box.row(align=True) 642 | row.label(text="Light list out of date") 643 | fn.tag_refresh_light_list() # We can't refresh the list here, so we tag it for the next depsgraph update 644 | row.operator(ops.GAFFER_OT_refresh_light_list.bl_idname, icon="FILE_REFRESH", text="") 645 | 646 | # Don't show lights that share the same data 647 | duplicates = {} 648 | """ 649 | duplicates: 650 | A dict with the key: object type + data name (cannot use only the name in case of conflicts). 651 | The values are the number of duplicates for that key. 652 | """ 653 | templist = [] 654 | for item in lights_to_show: 655 | light = scene.objects[item[0][1:-1]] # drop the apostrophes 656 | if light.type == "LIGHT": 657 | if ("LIGHT" + light.data.name) in duplicates: 658 | duplicates["LIGHT" + light.data.name] += 1 659 | else: 660 | templist.append(item) 661 | duplicates["LIGHT" + light.data.name] = 1 662 | else: 663 | mat = bpy.data.materials[item[1][1:-1]] 664 | if ("MAT" + mat.name) in duplicates: 665 | duplicates["MAT" + mat.name] += 1 666 | else: 667 | templist.append(item) 668 | duplicates["MAT" + mat.name] = 1 669 | lights_to_show = templist 670 | 671 | i = 0 672 | for item in lights_to_show: 673 | light = scene.objects[item[0][1:-1]] # drop the apostrophes 674 | light_uses_nodes = True 675 | is_portal = False 676 | if light.type == "LIGHT": 677 | material = None 678 | if light.data.use_nodes: 679 | try: 680 | node_strength = light.data.node_tree.nodes[item[2][1:-1]] 681 | except KeyError: 682 | light_uses_nodes = False 683 | else: 684 | light_uses_nodes = False 685 | 686 | if light.data.type == "AREA" and light.data.cycles.is_portal and scene.render.engine == "CYCLES": 687 | is_portal = True 688 | else: 689 | material = bpy.data.materials[item[1][1:-1]] 690 | if material.use_nodes: 691 | node_strength = material.node_tree.nodes[item[2][1:-1]] 692 | else: 693 | light_uses_nodes = False 694 | 695 | if light.type == "LIGHT": 696 | users = ["LIGHT" + light.data.name, duplicates["LIGHT" + light.data.name]] 697 | else: 698 | users = ["MAT" + material.name, duplicates["MAT" + material.name]] 699 | 700 | if light_uses_nodes and scene.render.engine == "CYCLES": 701 | box = maincol.box() 702 | rowmain = box.row() 703 | split = rowmain.split() 704 | col = split.column() 705 | row = col.row(align=True) 706 | 707 | if item[3].startswith("'"): 708 | socket_strength_str = str(item[3][1:-1]) 709 | else: 710 | socket_strength_str = str(item[3]) 711 | 712 | if socket_strength_str.startswith("o"): 713 | socket_strength_type = "o" 714 | socket_strength = int(socket_strength_str[1:]) 715 | elif socket_strength_str.startswith("i"): 716 | socket_strength_type = "i" 717 | socket_strength = int(socket_strength_str[1:]) 718 | else: 719 | socket_strength_type = "i" 720 | socket_strength = int(socket_strength_str) 721 | 722 | draw_renderer_independant(gaf_props, row, light, icons, users) 723 | 724 | if not is_portal: 725 | draw_strength_cycles(col, light, material, node_strength, socket_strength_type, socket_strength) 726 | 727 | draw_color_cycles(gaf_props, i, icons, col, row, light, material) 728 | 729 | if "_Light:_(" + light.name + ")_" in gaf_props.MoreExpand or gaf_props.MoreExpandAll: 730 | draw_more_options_cycles(box, scene, light, material, node_strength, is_portal) 731 | i += 1 732 | elif light.type == "LIGHT": 733 | box = maincol.box() 734 | rowmain = box.row() 735 | split = rowmain.split() 736 | col = split.column() 737 | row = col.row(align=True) 738 | 739 | draw_renderer_independant(gaf_props, row, light, icons, users) 740 | 741 | if not is_portal: 742 | draw_strength_eevee(col, light) 743 | 744 | draw_color_eevee(row, light) 745 | 746 | if "_Light:_(" + light.name + ")_" in gaf_props.MoreExpand or gaf_props.MoreExpandAll: 747 | if scene.render.engine == "CYCLES": 748 | draw_more_options_cycles(box, scene, light, material, None, is_portal) 749 | else: 750 | draw_more_options_eevee(box, scene, light) 751 | i += 1 752 | 753 | if len(lights_to_show) == 0: 754 | row = maincol.row() 755 | row.alignment = "CENTER" 756 | row.label(text="No lights to show :)") 757 | 758 | if context.scene.world: 759 | gaf_hdri_props = scene.world.gaf_hdri_props 760 | draw_world(context, layout, gaf_props, gaf_hdri_props, scene, prefs, icons) 761 | 762 | 763 | def draw_unsupported_renderer_UI(context, layout, lights): 764 | maincol = layout.column(align=False) 765 | scene = context.scene 766 | gaf_props = scene.gaf_props 767 | gaf_hdri_props = scene.world.gaf_hdri_props 768 | prefs = context.preferences.addons[__package__].preferences 769 | icons = fn.get_icons() 770 | 771 | lights_to_show = [] 772 | # Check validity of list and make list of lights to display 773 | vis_cols = fn.visibleCollections() 774 | for light in lights: 775 | try: 776 | if light[0]: 777 | # Will cause KeyError exception if obj no longer exists 778 | a = bpy.data.objects[light[0][1:-1]] 779 | if (gaf_props.VisibleLightsOnly and not a.hide_viewport) or (not gaf_props.VisibleLightsOnly): 780 | if (gaf_props.VisibleCollectionsOnly and fn.isInVisibleCollection(a, vis_cols)) or ( 781 | not gaf_props.VisibleCollectionsOnly 782 | ): 783 | if a.name not in [o.name for o in gaf_props.Blacklist]: 784 | lights_to_show.append(light) 785 | except KeyError: 786 | box = maincol.box() 787 | row = box.row(align=True) 788 | row.label(text="Light list out of date") 789 | row.operator(ops.GAFFER_OT_refresh_light_list.bl_idname, icon="FILE_REFRESH", text="") 790 | 791 | # Don't show lights that share the same data 792 | duplicates = {} 793 | """ 794 | duplicates: 795 | A dict with the key: object type + data name (cannot use only the name in case of conflicts). 796 | The values are the number of duplicates for that key. 797 | """ 798 | templist = [] 799 | for item in lights_to_show: 800 | light = scene.objects[item[0][1:-1]] # drop the apostrophes 801 | if light.type == "LIGHT": 802 | if ("LIGHT" + light.data.name) in duplicates: 803 | duplicates["LIGHT" + light.data.name] += 1 804 | else: 805 | templist.append(item) 806 | duplicates["LIGHT" + light.data.name] = 1 807 | lights_to_show = templist 808 | 809 | i = 0 810 | for item in lights_to_show: 811 | light = scene.objects[item[0][1:-1]] # drop the apostrophes 812 | 813 | box = maincol.box() 814 | rowmain = box.row() 815 | split = rowmain.split() 816 | col = split.column() 817 | row = col.row(align=True) 818 | 819 | if light.type == "LIGHT": 820 | users = ["LIGHT" + light.data.name, duplicates["LIGHT" + light.data.name]] 821 | else: 822 | users = ["MAT" + light.name, duplicates["MAT" + light.name]] 823 | draw_renderer_independant(gaf_props, row, light, icons, users) 824 | i += 1 825 | 826 | if len(lights_to_show) == 0: 827 | row = maincol.row() 828 | row.alignment = "CENTER" 829 | row.label(text="No lights to show :)") 830 | 831 | # World 832 | if ( 833 | context.scene.world 834 | and gaf_hdri_props.hdri_handler_enabled 835 | and context.scene.render.engine in ["BLENDER_EEVEE", "BLENDER_EEVEE_NEXT"] 836 | ): 837 | box = layout.box() 838 | worldcol = box.column(align=True) 839 | col = worldcol.column(align=True) 840 | 841 | row = col.row(align=True) 842 | 843 | if "_Light:_(WorldEnviroLight)_" in gaf_props.MoreExpand and not gaf_props.MoreExpandAll: 844 | row.operator( 845 | ops.GAFFER_OT_hide_more.bl_idname, 846 | icon="TRIA_DOWN", 847 | text="", 848 | emboss=False, 849 | ).light = "WorldEnviroLight" 850 | elif not gaf_props.MoreExpandAll: 851 | row.operator( 852 | ops.GAFFER_OT_show_more.bl_idname, 853 | text="", 854 | icon="TRIA_RIGHT", 855 | emboss=False, 856 | ).light = "WorldEnviroLight" 857 | 858 | row.label(text="World") 859 | col = worldcol.column() 860 | draw_hdri_handler( 861 | context, col, gaf_props, gaf_hdri_props, fn.get_persistent_setting("hdri_paths"), prefs, icons, toolbar=True 862 | ) 863 | 864 | 865 | class GAFFER_PT_lights(bpy.types.Panel): 866 | 867 | bl_label = "Lights" 868 | bl_space_type = "VIEW_3D" 869 | bl_region_type = "UI" 870 | bl_category = "Gaffer" 871 | 872 | def draw(self, context): 873 | addon_updater_ops.check_for_update_background() 874 | 875 | scene = context.scene 876 | gaf_props = scene.gaf_props 877 | lights_str = gaf_props.Lights 878 | lights = fn.stringToNestedList(lights_str) 879 | layout = self.layout 880 | col = layout.column(align=True) 881 | 882 | row = col.row(align=True) 883 | if gaf_props.SoloActive != "": # if in solo mode 884 | sub = row.column(align=True) 885 | sub.alert = True 886 | solobtn = sub.operator(ops.GAFFER_OT_solo.bl_idname, icon="EVENT_S", text="") 887 | solobtn.light = "None" 888 | solobtn.showhide = False 889 | solobtn.worldsolo = False 890 | 891 | # may not be needed if drawing errors are cought correctly (eg newly added lights): 892 | row.operator( 893 | ops.GAFFER_OT_refresh_light_list.bl_idname, 894 | text="Refresh", 895 | icon="FILE_REFRESH", 896 | ) 897 | 898 | row.prop(gaf_props, "VisibleCollectionsOnly", text="", icon="LAYER_ACTIVE") 899 | row.prop(gaf_props, "VisibleLightsOnly", text="", icon="HIDE_OFF") 900 | row.prop(gaf_props, "MoreExpandAll", text="", icon="PREFERENCES") 901 | 902 | if gaf_props.SoloActive != "": 903 | try: 904 | # Will cause KeyError if object by that name doesn't exist 905 | bpy.data.objects[gaf_props.SoloActive] 906 | except KeyError: 907 | if gaf_props.SoloActive != "WorldEnviroLight": 908 | # In case solo'd light changes name, theres no other way to exit solo mode 909 | col.separator() 910 | row = col.row() 911 | row.alert = True 912 | solobtn = row.operator( 913 | ops.GAFFER_OT_solo.bl_idname, 914 | icon="EVENT_S", 915 | text="Light not found, reset Solo", 916 | ) 917 | solobtn.showhide = False 918 | 919 | row = col.row(align=True) 920 | row.prop( 921 | bpy.context.scene.view_settings, 922 | "exposure", 923 | text="Global Exposure", 924 | slider=False, 925 | ) 926 | if bpy.context.scene.render.engine in const.supported_renderers: 927 | row.operator(ops.GAFFER_OT_apply_exposure.bl_idname, text="", icon="CHECKBOX_HLT") 928 | 929 | if scene.render.engine == "CYCLES": 930 | draw_cycles_eevee_UI(context, layout, lights) 931 | elif scene.render.engine in ["BLENDER_EEVEE", "BLENDER_EEVEE_NEXT"]: 932 | draw_cycles_eevee_UI(context, layout, lights) 933 | else: 934 | draw_unsupported_renderer_UI(context, layout, lights) 935 | box = layout.box() 936 | col = box.column(align=True) 937 | row = col.row() 938 | row.alignment = "CENTER" 939 | row.label(text="Warning", icon="ERROR") 940 | row = col.row() 941 | row.alignment = "CENTER" 942 | row.label(text="Render engine not fully supported.") 943 | row = col.row() 944 | row.alignment = "CENTER" 945 | row.label(text="Gaffer functionality is limitted.") 946 | row = col.row(align=True) 947 | row.alignment = "CENTER" 948 | row.label(text="Click here to add your vote:") 949 | row.operator("wm.url_open", text="", icon="URL").url = "https://forms.gle/R22DphecWsXmaLAr9" 950 | 951 | addon_updater_ops.update_notice_box_ui(self, context) 952 | 953 | 954 | class GAFFER_PT_tools(bpy.types.Panel): 955 | 956 | bl_label = "Tools" 957 | bl_space_type = "VIEW_3D" 958 | bl_region_type = "UI" 959 | bl_category = "Gaffer" 960 | 961 | def draw(self, context): 962 | scene = context.scene 963 | gaf_props = scene.gaf_props 964 | layout = self.layout 965 | 966 | maincol = layout.column() 967 | 968 | # Aiming 969 | maincol.separator() 970 | box = maincol.box() 971 | subcol = box.column(align=True) 972 | row = subcol.row() 973 | row.alignment = "CENTER" 974 | row.label(text="Aim:", icon="LIGHT_AREA") 975 | row = subcol.row() 976 | col = row.column(align=True) 977 | col.label(text="Selected:") 978 | col.operator(ops.GAFFER_OT_aim_light.bl_idname, text="at 3D cursor", icon="PIVOT_CURSOR").target_type = "CURSOR" 979 | col.operator(ops.GAFFER_OT_aim_light.bl_idname, text="at active", icon="FULLSCREEN_EXIT").target_type = "ACTIVE" 980 | col = row.column(align=True) 981 | col.label(text="Active:") 982 | col.operator(ops.GAFFER_OT_aim_light.bl_idname, text="at selected", icon="PARTICLES").target_type = "SELECTED" 983 | col.operator( 984 | ops.GAFFER_OT_aim_light_with_view.bl_idname, 985 | text="w/ 3D View", 986 | icon="VIEW_CAMERA", 987 | ) 988 | 989 | maincol.separator() 990 | 991 | # Draw Radius 992 | if context.scene.render.engine in const.supported_renderers: 993 | box = maincol.box() if gaf_props.IsShowingRadius else maincol.column() 994 | sub = box.column(align=True) 995 | row = sub.row(align=True) 996 | row.operator( 997 | ops.GAFFER_OT_show_light_radius.bl_idname, 998 | text="Show Radius" if not gaf_props.IsShowingRadius else "Hide Radius", 999 | icon="MESH_CIRCLE", 1000 | ) 1001 | if gaf_props.IsShowingRadius: 1002 | row.operator(ops.GAFFER_OT_refresh_bgl.bl_idname, text="", icon="FILE_REFRESH") 1003 | sub.prop(gaf_props, "LightRadiusAlpha", slider=True) 1004 | row = sub.row(align=True) 1005 | row.active = gaf_props.IsShowingRadius 1006 | row.prop(gaf_props, "LightRadiusDrawType", text="") 1007 | row.prop(gaf_props, "LightRadiusUseColor") 1008 | row = sub.row(align=True) 1009 | row.active = gaf_props.IsShowingRadius 1010 | row.prop(gaf_props, "LightRadiusXray") 1011 | row.prop(gaf_props, "LightRadiusSelectedOnly") 1012 | row = sub.row(align=True) 1013 | row.prop(gaf_props, "DefaultRadiusColor") 1014 | 1015 | # Draw Label 1016 | box = maincol.box() if gaf_props.IsShowingLabel else maincol.column() 1017 | sub = box.column(align=True) 1018 | row = sub.row(align=True) 1019 | row.operator( 1020 | ops.GAFFER_OT_show_light_label.bl_idname, 1021 | text="Show Label" if not gaf_props.IsShowingLabel else "Hide Label", 1022 | icon="ALIGN_LEFT", 1023 | ) 1024 | if gaf_props.IsShowingLabel: 1025 | row.operator(ops.GAFFER_OT_refresh_bgl.bl_idname, text="", icon="FILE_REFRESH") 1026 | label_draw_type = gaf_props.LabelDrawType 1027 | sub.prop(gaf_props, "LabelAlpha", slider=True) 1028 | sub.prop(gaf_props, "LabelFontSize") 1029 | row = sub.row(align=True) 1030 | row.prop(gaf_props, "LabelDrawType", text="") 1031 | row.prop(gaf_props, "LabelUseColor") 1032 | if label_draw_type == "color_bg" or not gaf_props.LabelUseColor: 1033 | row = sub.row(align=True) 1034 | row.prop(gaf_props, "LabelTextColor") 1035 | if label_draw_type == "plain_bg" or (not gaf_props.LabelUseColor and label_draw_type != "color_text"): 1036 | row = sub.row(align=True) 1037 | row.prop(gaf_props, "DefaultLabelBGColor") 1038 | row = sub.row(align=True) 1039 | row.prop(gaf_props, "LabelAlign", text="") 1040 | if gaf_props.LabelAlign != "c": 1041 | row.prop(gaf_props, "LabelMargin") 1042 | 1043 | maincol.separator() 1044 | 1045 | # Blacklist 1046 | box = maincol.box() 1047 | sub = box.column(align=True) 1048 | sub.label(text="Blacklist:") 1049 | if gaf_props.Blacklist: 1050 | sub.template_list( 1051 | "OBJECT_UL_object_list", 1052 | "", 1053 | gaf_props, 1054 | "Blacklist", 1055 | gaf_props, 1056 | "BlacklistIndex", 1057 | rows=2, 1058 | ) 1059 | row = sub.row(align=True) 1060 | row.operator(ops.GAFFER_OT_add_blacklisted.bl_idname, icon="ADD") 1061 | row.operator(ops.GAFFER_OT_remove_blacklisted.bl_idname, icon="REMOVE") 1062 | 1063 | 1064 | def draw_progress_bar(gaf_props, layout): 1065 | if gaf_props.ShowProgress: 1066 | layout.separator() 1067 | b = layout.box() 1068 | col = b.column(align=True) 1069 | col.label(text=gaf_props.ProgressText) 1070 | split = col.split(factor=max(0.01, gaf_props.Progress), align=True) 1071 | r = split.row() 1072 | r.alert = True 1073 | r.prop(gaf_props, "ProgressBarText", text="") 1074 | r = split.row() 1075 | r.label(text="") 1076 | c = b.column(align=True) 1077 | c.label(text="Large HDRI files may take a while") 1078 | c.label(text="You can stop this any time by closing Blender") 1079 | layout.separator() 1080 | 1081 | 1082 | def draw_hdri_handler(context, layout, gaf_props, gaf_hdri_props, hdri_paths, prefs, icons, toolbar=False): 1083 | 1084 | def draw_filter_row(col, no_matches=False): 1085 | row = col.row(align=True) 1086 | row.operator( 1087 | ops.GAFFER_OT_hdri_set_favorite.bl_idname, 1088 | text="", 1089 | icon="FUND" if gaf_hdri_props.hdri in fn.get_favorites() else "HEART", 1090 | ).name = gaf_hdri_props.hdri 1091 | row.prop(gaf_hdri_props, "hdri_favorite", text="", icon="FILTER") 1092 | row.separator() 1093 | if len(hdri_paths) > 1: 1094 | row.menu( 1095 | "GAFFER_MT_folder_filter", text="", icon="X" if gaf_hdri_props.hdri_folder_filter else "FILE_FOLDER" 1096 | ) 1097 | row.separator() 1098 | row.prop(gaf_hdri_props, "hdri_search", text="", expand=True, icon="VIEWZOOM") 1099 | if gaf_hdri_props.hdri_search or no_matches: 1100 | row.operator(ops.GAFFER_OT_hdri_clear_search.bl_idname, text="", icon="X") 1101 | subrow = row.row(align=True) 1102 | subrow.alignment = "RIGHT" 1103 | subrow.label(text=str(len(fn.hdri_enum_previews(gaf_props, context))) + " matches") 1104 | 1105 | if gaf_hdri_props.hdri: 1106 | col = layout.column(align=True) 1107 | 1108 | if not toolbar or "_Light:_(WorldEnviroLight)_" in gaf_props.MoreExpand or gaf_props.MoreExpandAll: 1109 | draw_filter_row(col) 1110 | 1111 | col = layout.column(align=True) 1112 | 1113 | row = col.row(align=True) 1114 | 1115 | tmpc = row.column(align=True) 1116 | tmpr = tmpc.column(align=True) 1117 | tmpr.scale_y = 1 1118 | tmpr.operator(ops.GAFFER_OT_hdri_save.bl_idname, text="", icon="FILE_TICK").hdri = gaf_hdri_props.hdri 1119 | tmpcc = tmpc.column(align=True) 1120 | tmpcc.scale_y = 9 if not toolbar else 3.5 1121 | tmpcc.operator(ops.GAFFER_OT_hdri_paddles.bl_idname, text="", icon="TRIA_LEFT").do_next = False 1122 | tmpr = tmpc.column(align=True) 1123 | tmpr.scale_y = 1 1124 | tmpr.operator(ops.GAFFER_OT_hdri_reset.bl_idname, text="", icon="FILE_REFRESH").hdri = gaf_hdri_props.hdri 1125 | 1126 | tmpc = row.column() 1127 | tmpc.scale_y = 1 / (2 if toolbar else 1) 1128 | tmpc.template_icon_view(gaf_hdri_props, "hdri", show_labels=True, scale=11) 1129 | 1130 | tmpc = row.column(align=True) 1131 | tmpr = tmpc.column(align=True) 1132 | tmpr.scale_y = 1 1133 | tmpr.prop( 1134 | gaf_hdri_props, 1135 | "hdri_show_tags_ui", 1136 | text="", 1137 | toggle=True, 1138 | icon_value=icons["tag"].icon_id, 1139 | ) 1140 | tmpcc = tmpc.column(align=True) 1141 | tmpcc.scale_y = 9 if not toolbar else 3.5 1142 | tmpcc.operator(ops.GAFFER_OT_hdri_paddles.bl_idname, text="", icon="TRIA_RIGHT").do_next = True 1143 | tmpr = tmpc.column(align=True) 1144 | tmpr.scale_y = 1 1145 | tmpr.operator( 1146 | ops.GAFFER_OT_hdri_random.bl_idname, 1147 | text="", 1148 | icon_value=icons["random"].icon_id, 1149 | ) 1150 | 1151 | if gaf_hdri_props.hdri_show_tags_ui: 1152 | col.separator() 1153 | box = col.box() 1154 | tags_col = box.column(align=True) 1155 | tags_col.label(text="Choose some tags:") 1156 | tags_col.separator() 1157 | 1158 | current_tags = fn.get_tags() 1159 | if gaf_hdri_props.hdri in current_tags: 1160 | current_tags = current_tags[gaf_hdri_props.hdri] 1161 | else: 1162 | current_tags = [] 1163 | 1164 | i = 0 1165 | for t in const.possible_tags: 1166 | if i % 4 == 0 or t == "##split##": # Split tags into columns 1167 | row = tags_col.row(align=True) 1168 | if t != "##split##": 1169 | 1170 | op = row.operator( 1171 | ops.GAFFER_OT_hdri_add_tag.bl_idname, 1172 | text=t.title(), 1173 | icon="CHECKBOX_HLT" if t in current_tags else "NONE", 1174 | ) 1175 | op.hdri = gaf_hdri_props.hdri 1176 | op.tag = t 1177 | i += 1 1178 | else: 1179 | i = 0 1180 | tags_col.prop( 1181 | gaf_hdri_props, 1182 | "hdri_custom_tags", 1183 | icon_value=icons["text-cursor"].icon_id, 1184 | ) 1185 | tags_col.separator() 1186 | tags_col.prop(gaf_hdri_props, "hdri_show_tags_ui", text="Done", toggle=True) 1187 | col.separator() 1188 | 1189 | col = layout.column(align=True) 1190 | 1191 | if prefs.RequestThumbGen: 1192 | row = col.row(align=True) 1193 | row.alignment = "CENTER" 1194 | row.operator(ops.GAFFER_OT_hdri_thumb_gen.bl_idname, icon="IMAGE") 1195 | col.separator() 1196 | 1197 | row = col.row(align=True) 1198 | vp_icon = "TRIA_LEFT" if gaf_hdri_props["hdri_variation"] != 0 else "TRIA_LEFT_BAR" 1199 | row.operator(ops.GAFFER_OT_hdri_variation_paddles.bl_idname, text="", icon=vp_icon).do_next = False 1200 | row.prop(gaf_hdri_props, "hdri_variation", text="") 1201 | if const.hdri_haven_list and const.hdri_list: 1202 | if gaf_hdri_props.hdri in const.hdri_haven_list and gaf_hdri_props.hdri in const.hdri_list: 1203 | if not any(("_16k" in h or "_8k" in h) for h in const.hdri_list[gaf_hdri_props.hdri]): 1204 | row.operator(ops.GAFFER_OT_open_hdrihaven.bl_idname, text="", icon="ADD").url = ( 1205 | "https://polyhaven.com/a/" + gaf_hdri_props.hdri 1206 | ) 1207 | 1208 | if gaf_hdri_props.hdri in const.hdri_list: # Rare case of hdri_list not being initialized 1209 | vp_icon = ( 1210 | "TRIA_RIGHT" 1211 | if gaf_hdri_props["hdri_variation"] < len(const.hdri_list[gaf_hdri_props.hdri]) - 1 1212 | else "TRIA_RIGHT_BAR" 1213 | ) 1214 | else: 1215 | vp_icon = "TRIA_RIGHT" 1216 | row.operator(ops.GAFFER_OT_hdri_variation_paddles.bl_idname, text="", icon=vp_icon).do_next = True 1217 | col.separator() 1218 | 1219 | if gaf_props.FileNotFoundError: 1220 | row = col.row(align=True) 1221 | row.scale_y = 1.5 1222 | row.alert = True 1223 | row.alignment = "CENTER" 1224 | row.label(text="File not found. Try refreshing your HDRI list:", icon="ERROR") 1225 | row.operator( 1226 | ops.GAFFER_OT_detect_hdris.bl_idname, 1227 | text="Refresh", 1228 | icon="FILE_REFRESH", 1229 | ) 1230 | 1231 | col.separator() 1232 | col.prop(gaf_hdri_props, "hdri_rotation", slider=True) 1233 | col.separator() 1234 | row = col.row(align=True) 1235 | row.prop(gaf_hdri_props, "hdri_brightness", slider=True) 1236 | if not toolbar or "_Light:_(WorldEnviroLight)_" in gaf_props.MoreExpand or gaf_props.MoreExpandAll: 1237 | row.prop(gaf_hdri_props, "hdri_saturation", slider=True) 1238 | row = col.row(align=True) 1239 | row.prop(gaf_hdri_props, "hdri_contrast", slider=True) 1240 | row.prop(gaf_hdri_props, "hdri_warmth", slider=True) 1241 | 1242 | wc = context.scene.world.cycles 1243 | if context.scene.render.engine == "CYCLES" and ( 1244 | wc.sampling_method == "NONE" or (wc.sampling_method == "MANUAL" and wc.sample_map_resolution < 1000) 1245 | ): 1246 | col.separator() 1247 | col.separator() 1248 | if wc.sampling_method == "NONE": 1249 | col.label(text="Importance sampling is disabled", icon="ERROR") 1250 | else: 1251 | col.label(text="Sampling resolution is low", icon="ERROR") 1252 | row = col.row() 1253 | row.alignment = "LEFT" 1254 | row.label(text="Your renders may be noisy") 1255 | row.operator(ops.GAFFER_OT_fix_mis.bl_idname) 1256 | col.separator() 1257 | 1258 | if not toolbar: 1259 | col.separator() 1260 | col.separator() 1261 | 1262 | layout_panels_supported = bpy.app.version >= (4, 1, 0) 1263 | if layout_panels_supported: 1264 | header, panel = layout.panel("gaffer_advanced", default_closed=True) 1265 | header.label(text="Advanced") 1266 | if panel: 1267 | box = panel.column(align=True) 1268 | else: 1269 | box = col.box() 1270 | col = box.column(align=True) 1271 | row = col.row(align=True) 1272 | row.alignment = "LEFT" 1273 | row.prop( 1274 | gaf_hdri_props, 1275 | "hdri_advanced", 1276 | icon="TRIA_DOWN" if gaf_hdri_props.hdri_advanced else "TRIA_RIGHT", 1277 | emboss=False, 1278 | toggle=True, 1279 | ) 1280 | if (gaf_hdri_props.hdri_advanced and not layout_panels_supported) or (layout_panels_supported and panel): 1281 | col = box.column(align=True) 1282 | split = col.split(factor=0.5) 1283 | r = split.row() 1284 | r.prop(gaf_hdri_props, "hdri_tint", slider=True) 1285 | r = split.row(align=True) 1286 | mix_node = fn.handler_node(context, "ShaderNodeMix", fetch_only=True) 1287 | if mix_node: 1288 | r.prop(mix_node, "blend_type", text="") 1289 | r.prop(gaf_hdri_props, "hdri_color", text="") 1290 | col.separator() 1291 | 1292 | col.prop(gaf_hdri_props, "hdri_clamp", slider=True) 1293 | split = col.split(factor=0.75, align=True) 1294 | r = split.row(align=True) 1295 | r.prop(gaf_hdri_props, "hdri_horz_shift", slider=True) 1296 | r = split.row(align=True) 1297 | r.prop(gaf_hdri_props, "hdri_horz_exp", slider=False) 1298 | col.separator() 1299 | 1300 | col.label(text="Control background separately:") 1301 | row = col.row(align=True) 1302 | row.prop(gaf_hdri_props, "hdri_use_separate_rotation", toggle=True) 1303 | sub = row.row(align=True) 1304 | sub.active = gaf_hdri_props.hdri_use_separate_rotation 1305 | sub.prop(gaf_hdri_props, "hdri_background_rotation", slider=True) 1306 | row = col.row(align=True) 1307 | row.prop(gaf_hdri_props, "hdri_use_separate_brightness", toggle=True) 1308 | sub = row.row(align=True) 1309 | sub.active = gaf_hdri_props.hdri_use_separate_brightness 1310 | sub.prop(gaf_hdri_props, "hdri_background_brightness", slider=True) 1311 | row = col.row(align=True) 1312 | row.prop(gaf_hdri_props, "hdri_use_separate_contrast", toggle=True) 1313 | sub = row.row(align=True) 1314 | sub.active = gaf_hdri_props.hdri_use_separate_contrast 1315 | sub.prop(gaf_hdri_props, "hdri_background_contrast", slider=True) 1316 | row = col.row(align=True) 1317 | row.prop(gaf_hdri_props, "hdri_use_separate_saturation", toggle=True) 1318 | sub = row.row(align=True) 1319 | sub.active = gaf_hdri_props.hdri_use_separate_saturation 1320 | sub.prop(gaf_hdri_props, "hdri_background_saturation", slider=True) 1321 | row = col.row(align=True) 1322 | row.prop(gaf_hdri_props, "hdri_use_separate_warmth", toggle=True) 1323 | sub = row.row(align=True) 1324 | sub.active = gaf_hdri_props.hdri_use_separate_warmth 1325 | sub.prop(gaf_hdri_props, "hdri_background_warmth", slider=True) 1326 | row = col.row(align=True) 1327 | row.prop(gaf_hdri_props, "hdri_use_separate_tint", toggle=True) 1328 | sub = row.row(align=True) 1329 | sub.active = gaf_hdri_props.hdri_use_separate_tint 1330 | sub.prop(gaf_hdri_props, "hdri_background_tint", slider=True) 1331 | split = col.split(factor=0.5, align=True) 1332 | r = split.row(align=True) 1333 | r.prop(gaf_hdri_props, "hdri_use_separate_color", toggle=True) 1334 | r = split.row(align=True) 1335 | r.active = gaf_hdri_props.hdri_use_separate_color 1336 | mix_node_bg = fn.handler_node(context, "ShaderNodeMix", background=True, fetch_only=True) 1337 | if mix_node_bg: 1338 | r.prop(mix_node_bg, "blend_type", text="") 1339 | elif mix_node: 1340 | r.prop(mix_node, "blend_type", text="") 1341 | r.prop(gaf_hdri_props, "hdri_background_color", text="") 1342 | 1343 | col.separator() 1344 | sub = col.row(align=True) 1345 | sub.active = any( 1346 | [ 1347 | gaf_hdri_props.hdri_use_jpg_background, 1348 | gaf_hdri_props.hdri_use_separate_rotation, 1349 | gaf_hdri_props.hdri_use_separate_brightness, 1350 | gaf_hdri_props.hdri_use_separate_contrast, 1351 | gaf_hdri_props.hdri_use_separate_saturation, 1352 | gaf_hdri_props.hdri_use_separate_warmth, 1353 | gaf_hdri_props.hdri_use_separate_tint, 1354 | gaf_hdri_props.hdri_use_separate_color, 1355 | ] 1356 | ) 1357 | sub.prop(gaf_hdri_props, "hdri_use_bg_reflections") 1358 | 1359 | col.separator() 1360 | row = col.row(align=True) 1361 | row.prop(gaf_hdri_props, "hdri_use_jpg_background") 1362 | sub = row.row(align=True) 1363 | sub.active = gaf_hdri_props.hdri_use_jpg_background 1364 | sub.prop(gaf_hdri_props, "hdri_use_darkened_jpg") 1365 | if ( 1366 | gaf_hdri_props.hdri_use_jpg_background and gaf_hdri_props.hdri_use_bg_reflections 1367 | ) and not gaf_hdri_props.hdri_use_darkened_jpg: 1368 | col.label(text="Enabling 'Pre-Darkened' is recommended to") 1369 | col.label(text="get more accurate reflections.") 1370 | if gaf_props.RequestJPGGen and gaf_hdri_props.hdri_use_jpg_background: 1371 | col.separator() 1372 | col.separator() 1373 | col.label(text="No JPGs have been created yet,", icon="ERROR") 1374 | col.label(text="please click 'Generate JPGs' below.") 1375 | col.label(text="Note: This may take a while for high-res images") 1376 | col.operator(ops.GAFFER_OT_hdri_jpg_gen.bl_idname) 1377 | col.prop(gaf_hdri_props, "hdri_jpg_gen_all") 1378 | if gaf_hdri_props.hdri_jpg_gen_all: 1379 | col.label(text="This is REALLY going to take a while.") 1380 | col.label(text="See the console for progress.") 1381 | col.separator() 1382 | elif gaf_hdri_props.hdri_search or gaf_hdri_props.hdri_favorite or gaf_hdri_props.hdri_folder_filter: 1383 | prefs.ForcePreviewsRefresh = True 1384 | draw_filter_row(layout, no_matches=True) 1385 | else: 1386 | prefs.ForcePreviewsRefresh = True 1387 | row = layout.row() 1388 | row.alignment = "CENTER" 1389 | row.label(text="No HDRIs found") 1390 | row = layout.row() 1391 | row.alignment = "CENTER" 1392 | row.label(text="Please put some in the HDRI folder:") 1393 | 1394 | 1395 | class GAFFER_PT_hdris(bpy.types.Panel): 1396 | 1397 | bl_label = " " 1398 | bl_space_type = "PROPERTIES" 1399 | bl_region_type = "WINDOW" 1400 | bl_context = "world" 1401 | 1402 | @classmethod 1403 | def poll(cls, context): 1404 | return context.scene.render.engine in ["CYCLES", "BLENDER_EEVEE", "BLENDER_EEVEE_NEXT"] and context.scene.world 1405 | 1406 | def draw_header(self, context): 1407 | gaf_hdri_props = context.scene.world.gaf_hdri_props 1408 | 1409 | layout = self.layout 1410 | row = layout.row(align=True) 1411 | row.prop(gaf_hdri_props, "hdri_handler_enabled", text="") 1412 | if gaf_hdri_props.hdri and gaf_hdri_props.hdri_handler_enabled: 1413 | row.label(text="HDRI: " + fn.nice_hdri_name(gaf_hdri_props.hdri)) 1414 | else: 1415 | row.label(text="HDRI") 1416 | 1417 | def draw(self, context): 1418 | gaf_props = context.scene.gaf_props 1419 | gaf_hdri_props = context.scene.world.gaf_hdri_props 1420 | prefs = context.preferences.addons[__package__].preferences 1421 | icons = fn.get_icons() 1422 | 1423 | layout = self.layout 1424 | 1425 | draw_progress_bar(gaf_props, layout) 1426 | 1427 | col = layout.column() 1428 | hdri_paths = fn.get_persistent_setting("hdri_paths") 1429 | if not os.path.exists(hdri_paths[0]): 1430 | row = col.row() 1431 | row.alignment = "CENTER" 1432 | row.label(text="Select a folder in the Add-on User Preferences") 1433 | row = col.row() 1434 | row.alignment = "CENTER" 1435 | row.label(text="Preferences > Add-ons > Gaffer > HDRI Folder") 1436 | else: 1437 | if gaf_hdri_props.hdri_handler_enabled: 1438 | draw_hdri_handler(context, col, gaf_props, gaf_hdri_props, hdri_paths, prefs, icons) 1439 | 1440 | if gaf_props.ShowHDRIHaven: 1441 | layout.separator() 1442 | row = layout.row(align=True) 1443 | row.alignment = "CENTER" 1444 | row.scale_y = 1.5 1445 | row.scale_x = 1.5 1446 | row.operator( 1447 | ops.GAFFER_OT_get_hdrihaven.bl_idname, 1448 | icon_value=icons["hdri_haven"].icon_id, 1449 | ) 1450 | row.operator(ops.GAFFER_OT_hide_hdrihaven.bl_idname, text="", icon="X") 1451 | else: 1452 | col = layout.column() 1453 | row = col.row() 1454 | row.alignment = "CENTER" 1455 | row.label(text="Gaffer's HDRI handler is disabled.") 1456 | row = col.row() 1457 | row.alignment = "CENTER" 1458 | row.label(text="Enable it with the checkbox in this panel's header") 1459 | 1460 | 1461 | class OBJECT_UL_object_list(bpy.types.UIList): 1462 | def draw_item(self, context, layout, data, item, icon, active_data, active_propname): 1463 | obj = item 1464 | layout.prop(obj, "name", text="", emboss=False) 1465 | 1466 | 1467 | class GAFFER_MT_folder_filter(bpy.types.Menu): 1468 | bl_label = "Folder Filter" 1469 | bl_description = "Show only HDRIs from this subfolder" 1470 | 1471 | def draw(self, context): 1472 | gaf_hdri_props = context.scene.world.gaf_hdri_props 1473 | col = self.layout.column() 1474 | 1475 | if gaf_hdri_props.hdri_folder_filter: 1476 | col.operator( 1477 | ops.GAFFER_OT_hdri_set_folder_filter.bl_idname, 1478 | text="Remove Filter", 1479 | icon="X", 1480 | ).folder = "" 1481 | 1482 | hdri_paths = fn.get_persistent_setting("hdri_paths") 1483 | for path in hdri_paths: 1484 | col.operator(ops.GAFFER_OT_hdri_set_folder_filter.bl_idname, text=path, icon="FILE_FOLDER").folder = path 1485 | # Subfolders 1486 | for subfolder in os.listdir(path): 1487 | if os.path.isdir(os.path.join(path, subfolder)): 1488 | col.operator( 1489 | ops.GAFFER_OT_hdri_set_folder_filter.bl_idname, 1490 | text=subfolder, 1491 | icon="DOT", 1492 | ).folder = os.path.join(path, subfolder) 1493 | 1494 | 1495 | def gaffer_node_menu_func(self, context): 1496 | if context.space_data.node_tree.type == "SHADER" and context.space_data.shader_type == "OBJECT": 1497 | light_dict = fn.dictOfLights() 1498 | if context.object.name in light_dict: 1499 | layout = self.layout 1500 | layout.operator(ops.GAFFER_OT_node_set_strength.bl_idname) 1501 | --------------------------------------------------------------------------------