├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── __init__backups ├── README.md ├── bl_ui_widgets_demo.zip └── reference_cameras.zip ├── addon ├── __init__.py ├── bl_ui_widget_demo.py ├── demo_panel_op.py ├── drag_panel_op.py └── reference_cameras.py ├── bl_ui_widgets ├── __init__.py ├── bl_ui_button.py ├── bl_ui_checkbox.py ├── bl_ui_drag_panel.py ├── bl_ui_draw_op.py ├── bl_ui_label.py ├── bl_ui_patch.py ├── bl_ui_slider.py ├── bl_ui_textbox.py ├── bl_ui_tooltip.py └── bl_ui_widget.py ├── documentation └── README.md ├── img ├── rotate.png ├── rotate.psd ├── scale.png ├── scale.psd └── scale_24.png └── prefs.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # VS Code IDE 10 | .vscode/ 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reference Cameras Control Panel addon for Blender 3D 2 | 3 | This add-on adds a new tab to Blender's N-Panel which groups many utilities for managing cameras that can be used for “fitting” the 3D models into reference photos. With it the user can easily operate multiple reference camera setups containing real world object photos taken from different directions. 4 | 5 | This add-on was [originally created](http://airplanes3d.net/scripts-257_e.xml) by ***Witold Jaworski*** and has been enhanced by me with additional features and a handy floating "cameras remote control" panel built upon the [BL UI Widgets](https://github.com/jayanam/bl_ui_widgets) add-on created by ***Jayanam***. 6 | 7 | Feel free to create issues, file requests etc but be aware of that I may not find time to work on this as much as I'd probably like to. 8 | 9 | > This fork is intended to serve as a tool set for managing the perspective view cameras in **Blender 2.8** and newer versions. 10 | 11 | ![floating panel](https://github.com/mmmrqs/media/blob/main/fpanel_sample.png) 12 | 13 | 14 | ## Main features 15 | 16 | - Can be accessed via Blender's side N-Panel (see image below) or via a standalone remote control **floating panel** (image above), which can be dragged around the screen at will. 17 | - Group of buttons to automatically set the transformation orientation and select the camera/target set in one click. 18 | - Up to 7 camera adjustment modes for **Zoom** (dolly), **Translation** (truck & pedestal), **Rotation** (pan & roll), **Horizontal** (orbit), **Vertical** (orbit), **Tilt** and **Perspective** change. Three different layout display options. 19 | - Auto blinking button to turn the active meshe(s) visibility on/off (great to quickly flash the mesh against the background image helping with visual positioning accuracy). 20 | - Memory slots that can handle up to 4 camera setup stages so that the user can easily and quickly shift among those stages to visually compare the results and decide which adjustment to keep. 21 | - Button to globally toggle the visibility of subdivision modifiers for all objects in the scene, to help with better alignment of objects against the camera's background reference image. 22 | - Buttons to lock target position and/or target rotation to prevent accidentally messing the camera's target. 23 | - Buttons to add to the scene new 'camera/target sets' already configured to work with the addon. 24 | - Camera sets can be automatically organized in distinct subgroups (per folder name, alphabetically sorted and collapsible), for easier operation. 25 | - Buttons to temporarily remove (hide) cameras from the N-Panel group of buttons (so that it became less cluttered). 26 | - Addon preferences panel for customization of various behaviors and elements default values. 27 | 28 | ![n-side panel](https://github.com/mmmrqs/media/blob/main/npanel_sample.png) 29 | 30 | > Note: Some functions (buttons) only appear when the 'Extended' layout option is selected in Preferences. 31 | 32 | 33 | ## Tutorial 34 | 35 | Please visit Witold's fabulous [Airplanes in 3D Virtual Aircraft](http://airplanes3d.net/scripts-257_e.xml) site to learn **how to use** this tool and see its benefits. Also check all the awesome stuff that Witold has gotten for you in his site. 36 | 37 | 38 | ## Installation 39 | First get the addon by clicking the green "Code" button (top right of files list above) and select the "**Download ZIP**" option, then use Blender's own "Install..." (button) option after navigating to Blender's Main Menu --> Edit --> Preferences... --> Add-ons. 40 | 41 | 42 | ## Camera modes 43 | 44 | The standard 'adjustment camera modes' available to user selection are as follows: 45 | 46 | **Zoom**: Dolly moves only back and forth on camera's axis (as per G + Z + move mouse) 47 | - [x] Good to adjust 'Distance/Size' 48 | - [x] *Characteristics - Selected: Camera; Transformation: Local; Pivot: Active Element (=Camera)* 49 | 50 | **Horizontal Orbit**: The camera rotates around the target which stays in place (as per R + Z + move mouse) 51 | - [x] Good to adjust 'Rotation' 52 | - [x] *Characteristics - Selected: Camera; Transformation: Global; Pivot: 3D Cursor (which is moved to 'Target' origin)* 53 | 54 | **Vertical Orbit**: The camera rotates around the target which stays in place (as per R + X + move mouse) 55 | - [x] Good to adjust 'Elevation/Azimuth' 56 | - [x] *Characteristics - Selected: Camera; Transformation: Local; Pivot: 3D Cursor (which is moved to 'Target' origin)* 57 | 58 | **Tilt**: Camera stays still, moves from up and down (as per R + Y + move mouse) 59 | - [x] Good to adjust 'Inclination' 60 | - [x] *Characteristics - Selected: Target; Transformation: Local; Pivot: Active Element (=Target)* 61 | 62 | **Translation**: Truck/Pedestal moves only from left to right on camera's axis (as per G + X/Y/Z + move mouse) 63 | - [x] Good to adjust 'Position' 64 | - [x] *Characteristics - Selected: Camera+Target; Transformation: Global; Pivot: Active Element (=Target)* 65 | 66 | **Roll**: Camera stays still, lean from left to right (as per R + X/Y + move mouse) 67 | - [x] Good to adjust 'Angle' 68 | - [x] *Characteristics - Selected: Camera+Target; Transformation: Global; Pivot: Active Element (=Target)* 69 | 70 | **Perspective**: combination of Camera's Translation with Elevation/Rotation (as per G + X/Y/Z + mouse move) 71 | - [x] Good to adjust 'Point of View' 72 | - [x] *Characteristics - Selected: Camera; Transformation: Global; Pivot: Active Element (=Target)* 73 | -------------------------------------------------------------------------------- /__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 | ''' 20 | Reference Cameras add-on 21 | ''' 22 | # --- ### Header 23 | bl_info = {"name": "Reference Cameras Control Panel", 24 | "description": "Handles cameras associated with reference photos", 25 | "author": "Marcelo M. Marques (fork of Witold Jaworski's & Jayanam's projects)", 26 | "version": (1, 0, 5), 27 | "blender": (2, 80, 75), 28 | "location": "View3D > side panel ([N]), [Cameras] tab", 29 | "support": "COMMUNITY", 30 | "category": "3D View", 31 | "warning": "Version numbering diverges from Witold's original project", 32 | "doc_url": "http://airplanes3d.net/scripts-257_e.xml", 33 | "tracker_url": "https://github.com/mmmrqs/Blender-Reference-Camera-Panel-addon/issues" 34 | } 35 | 36 | # --- ### Change log 37 | 38 | # Note: Because the way Blender's Preferences window displays the Addon version number, 39 | # I am forced to keep this file in sync with the greatest version number of all modules. 40 | 41 | # v1.0.5 (09.28.2022) - by Marcelo M. Marques 42 | # Chang: updated version to keep this file in sync 43 | 44 | # v1.0.4 (09.25.2022) - by Marcelo M. Marques 45 | # Chang: updated version to keep this file in sync 46 | 47 | # v1.0.3 (10.31.2021) - by Marcelo M. Marques 48 | # Chang: updated version with improvements and some clean up 49 | 50 | # v1.0.2 (09.20.2021) - by Marcelo M. Marques 51 | # Chang: just some pep8 code formatting 52 | 53 | # v1.0.1 (09.12.2021) - by Marcelo M. Marques 54 | # Chang: updated version with bug fixes 55 | 56 | # v1.0.0 (09.01.2021) - by Marcelo M. Marques 57 | # Added: initial creation 58 | 59 | # --- ### Imports 60 | import sys 61 | import importlib 62 | 63 | modulesFullNames = {} 64 | 65 | modulesNames = ['prefs', 66 | 'bl_ui_widgets.bl_ui_draw_op', 67 | 'bl_ui_widgets.bl_ui_widget', 68 | 'bl_ui_widgets.bl_ui_label', 69 | 'bl_ui_widgets.bl_ui_patch', 70 | 'bl_ui_widgets.bl_ui_button', 71 | 'bl_ui_widgets.bl_ui_checkbox', 72 | 'bl_ui_widgets.bl_ui_textbox', 73 | 'bl_ui_widgets.bl_ui_slider', 74 | 'bl_ui_widgets.bl_ui_tooltip', 75 | 'bl_ui_widgets.bl_ui_drag_panel', 76 | 'addon.drag_panel_op', 77 | 'addon.reference_cameras', 78 | ] 79 | 80 | # This sys.argv is for an imported command line argument passed into python script by external IDE 81 | for currentModuleName in modulesNames: 82 | if 'DEBUG_MODE' in sys.argv: 83 | modulesFullNames[currentModuleName] = ('{}'.format(currentModuleName)) 84 | else: 85 | modulesFullNames[currentModuleName] = ('{}.{}'.format(__name__, currentModuleName)) 86 | 87 | if 'DEBUG_MODE' in sys.argv: 88 | import os 89 | import time 90 | os.system("cls") 91 | timestr = time.strftime("%Y-%m-%d %H:%M:%S") 92 | print('---------------------------------------') 93 | print('-------------- RESTART ----------------') 94 | print('---------------------------------------') 95 | print(timestr, __name__ + ": registered") 96 | print() 97 | sys.argv.remove('DEBUG_MODE') 98 | 99 | for currentModuleFullName in modulesFullNames.values(): 100 | if currentModuleFullName in sys.modules: 101 | importlib.reload(sys.modules[currentModuleFullName]) 102 | else: 103 | globals()[currentModuleFullName] = importlib.import_module(currentModuleFullName) 104 | setattr(globals()[currentModuleFullName], 'modulesNames', modulesFullNames) 105 | 106 | 107 | def register(): 108 | for currentModuleName in modulesFullNames.values(): 109 | if currentModuleName in sys.modules: 110 | if hasattr(sys.modules[currentModuleName], 'register'): 111 | sys.modules[currentModuleName].register() 112 | 113 | 114 | def unregister(): 115 | for currentModuleName in modulesFullNames.values(): 116 | if currentModuleName in sys.modules: 117 | if hasattr(sys.modules[currentModuleName], 'unregister'): 118 | sys.modules[currentModuleName].unregister() 119 | 120 | 121 | if __name__ == "__main__": 122 | register() 123 | -------------------------------------------------------------------------------- /__init__backups/README.md: -------------------------------------------------------------------------------- 1 | ### These 2 zip files contain versions for the "__\_\_init\_\_.py__" and "prefs.py" that can be unzipped and copied to the main folder before installing the addon. 2 | 3 | - [x] reference_cameras.zip - contains the default "__\_\_init\_\_.py__" and "prefs.py" files used by the Reference Camera Panel addon for Blender 4 | 5 | - [x] bl_ui_widgets_demo.zip - contains an alternative "__\_\_init\_\_.py__" and "prefs.py" files to showcase a demo panel displaying all the available widget components. It may be usefull for people that intends to use the BL_UI_Widgets API for building their own addons. 6 | 7 | 8 | __Note:__ in order to use one or the other, the user just need to unzip the desired files in the main folder of the addon and re-install the addon in Blender. In the case of the widgets demo there will be a separate tab in Blender's N-Panel of the 3D Viewport window, with a label set as "BL_UI_Widget". 9 | -------------------------------------------------------------------------------- /__init__backups/bl_ui_widgets_demo.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmmrqs/Blender-Reference-Camera-Panel-addon/2a101eee822d1f58eb53173df0d62d3e15d6d1f1/__init__backups/bl_ui_widgets_demo.zip -------------------------------------------------------------------------------- /__init__backups/reference_cameras.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmmrqs/Blender-Reference-Camera-Panel-addon/2a101eee822d1f58eb53173df0d62d3e15d6d1f1/__init__backups/reference_cameras.zip -------------------------------------------------------------------------------- /addon/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /addon/bl_ui_widget_demo.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 | # --- ### Header 20 | bl_info = {"name": "BL UI Widgets", 21 | "description": "UI Widgets to draw in the 3D view", 22 | "author": "Marcelo M. Marques", 23 | "version": (1, 0, 3), 24 | "blender": (2, 80, 75), 25 | "location": "View3D > side panel ([N]), [BL_UI_Widget] tab", 26 | "support": "COMMUNITY", 27 | "category": "3D View", 28 | "warning": "", 29 | "doc_url": "https://github.com/mmmrqs/bl_ui_widgets", 30 | "tracker_url": "https://github.com/mmmrqs/bl_ui_widgets/issues" 31 | } 32 | 33 | # --- ### Change log 34 | 35 | # v1.0.3 (09.28.2022) - by Marcelo M. Marques 36 | # Fixed: issue with a 'context is incorrect' situation that would be caused by user calling 'Set_Demo_Panel' repeatedly and too fast 37 | 38 | # v1.0.2 (09.25.2022) - by Marcelo M. Marques 39 | # Added: 'is_quadview_region' function to identify whether screen is in QuadView mode and if yes to return the corresponding area and region. 40 | # Added: 'btnRemoTime' session variable to hold the clock time which is constantly updated by the 'terminate_execution' function in demo_panel_op.py 41 | # so that the N-panel is more precisely informed about the remote panel status when it has to determine how to display the Open/Close button. 42 | # Added: Logic to the 'execute' method of the Set_Demo_Panel class for better displaying the Open/Close button, per item described above. 43 | 44 | # v1.0.1 (09.20.2021) - by Marcelo M. Marques 45 | # Chang: just some pep8 code formatting 46 | 47 | # v1.0.0 (09.01.2021) - by Marcelo M. Marques 48 | # Added: initial creation 49 | 50 | # --- ### Imports 51 | import bpy 52 | import time 53 | 54 | from bpy.props import StringProperty, IntProperty, BoolProperty 55 | 56 | # --- ### Helper functions 57 | def is_quadview_region(context): 58 | """ Identifies whether screen is in QuadView mode and if yes returns the corresponding area and region 59 | """ 60 | for area in context.screen.areas: 61 | if area.type == 'VIEW_3D': 62 | if len(area.spaces.active.region_quadviews) > 0: 63 | region = [region for region in area.regions if region.type == 'WINDOW'][3] 64 | return (True, area, region) 65 | return (False, None, None) 66 | 67 | 68 | # --- ### Properties 69 | class Variables(bpy.types.PropertyGroup): 70 | OpState1: bpy.props.BoolProperty(default=False) 71 | OpState2: bpy.props.BoolProperty(default=False) 72 | OpState3: bpy.props.BoolProperty(default=False) 73 | OpState4: bpy.props.BoolProperty(default=False) 74 | OpState5: bpy.props.BoolProperty(default=False) 75 | OpState6: bpy.props.BoolProperty(default=False) 76 | RemoVisible: bpy.props.BoolProperty(default=False) 77 | btnRemoText: bpy.props.StringProperty(default="Open Demo Panel") 78 | btnRemoTime: IntProperty(default=0) 79 | 80 | 81 | def is_desired_mode(context=None): 82 | """ Returns True, when Blender is in one of the desired Modes 83 | Arguments: 84 | @context (Context): current context (optional - as received by the operator) 85 | 86 | Possible desired mode options (as of Blender 2.8): 87 | 'EDIT_MESH', 'EDIT_CURVE', 'EDIT_SURFACE', 'EDIT_TEXT', 'EDIT_ARMATURE', 'EDIT_METABALL', 88 | 'EDIT_LATTICE', 'POSE', 'SCULPT', 'PAINT_WEIGHT', 'PAINT_VERTEX', 'PAINT_TEXTURE', 'PARTICLE', 89 | 'OBJECT', 'PAINT_GPENCIL', 'EDIT_GPENCIL', 'SCULPT_GPENCIL', 'WEIGHT_GPENCIL', 90 | Additional valid mode option (as of Blender 2.9): 91 | 'VERTEX_GPENCIL' 92 | Additional valid mode options (as of Blender 3.2): 93 | 'EDIT_CURVES', 'SCULPT_CURVES' 94 | """ 95 | desired_modes = ['OBJECT', 'EDIT_MESH', 'POSE', ] 96 | if context: 97 | return (context.mode in desired_modes) 98 | else: 99 | return (bpy.context.mode in desired_modes) 100 | 101 | 102 | class Set_Demo_Panel(bpy.types.Operator): 103 | ''' Opens/Closes the remote control demo panel ''' 104 | bl_idname = "object.set_demo_panel" 105 | bl_label = "Open Demo Panel" 106 | bl_description = "Turns the remote control demo panel on/off" 107 | 108 | # --- Blender interface methods 109 | @classmethod 110 | def poll(cls, context): 111 | return is_desired_mode(context) 112 | 113 | def invoke(self, context, event): 114 | # input validation: 115 | return self.execute(context) 116 | 117 | def execute(self, context): 118 | if context.scene.var.RemoVisible and int(time.time()) - context.scene.var.btnRemoTime <= 1: 119 | # If it is active then set its visible status to False so that it be closed and reset the button label 120 | context.scene.var.btnRemoText = "Open Remote Control" 121 | context.scene.var.RemoVisible = False 122 | else: 123 | # If it is not active then set its visible status to True so that it be opened and reset the button label 124 | context.scene.var.btnRemoText = "Close Remote Control" 125 | context.scene.var.RemoVisible = True 126 | is_quadview, area, region = is_quadview_region(context) 127 | if is_quadview: 128 | override = bpy.context.copy() 129 | override["area"] = area 130 | override["region"] = region 131 | # Had to put this "try/except" statement because when user clicked repeatedly too fast 132 | # on the operator's button it would crash the call due to a context incorrect situation 133 | try: 134 | if is_quadview: 135 | context.scene.var.objRemote = bpy.ops.object.dp_ot_draw_operator(override, 'INVOKE_DEFAULT') 136 | else: 137 | context.scene.var.objRemote = bpy.ops.object.dp_ot_draw_operator('INVOKE_DEFAULT') 138 | except: 139 | return {'CANCELLED'} 140 | 141 | return {'FINISHED'} 142 | 143 | 144 | class OBJECT_PT_Demo(bpy.types.Panel): 145 | bl_space_type = 'VIEW_3D' 146 | bl_region_type = 'UI' 147 | bl_category = "BL_UI_Widget" 148 | bl_label = "BL_UI_Widget" 149 | 150 | @classmethod 151 | def poll(cls, context): 152 | return is_desired_mode() 153 | 154 | def draw(self, context): 155 | if context.space_data.type == 'VIEW_3D' and is_desired_mode(): 156 | remoteVisible = (context.scene.var.RemoVisible and int(time.time()) - context.scene.var.btnRemoTime <= 1) 157 | # -- remote control switch button 158 | if remoteVisible: 159 | op = self.layout.operator(Set_Demo_Panel.bl_idname, text="Close Remote Control") 160 | else: 161 | # Make sure the button starts turned off every time 162 | op = self.layout.operator(Set_Demo_Panel.bl_idname, text="Open Remote Control") 163 | return None 164 | 165 | 166 | import bpy.app 167 | from bpy.utils import unregister_class, register_class 168 | 169 | # List of classes in this add-on to be registered in Blender's API: 170 | classes = [Variables, 171 | Set_Demo_Panel, 172 | OBJECT_PT_Demo, 173 | ] 174 | 175 | 176 | def register(): 177 | for cls in classes: 178 | register_class(cls) 179 | bpy.types.Scene.var = bpy.props.PointerProperty(type=Variables) 180 | 181 | 182 | def unregister(): 183 | del bpy.types.Scene.var 184 | for cls in reversed(classes): 185 | unregister_class(cls) 186 | 187 | 188 | if __name__ == '__main__': 189 | register() 190 | -------------------------------------------------------------------------------- /addon/demo_panel_op.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 | # --- ### Header 20 | bl_info = {"name": "BL UI Widgets", 21 | "description": "UI Widgets to draw in the 3D view", 22 | "author": "Marcelo M. Marques (fork of Jayanam's original project)", 23 | "version": (1, 0, 4), 24 | "blender": (2, 80, 75), 25 | "location": "View3D > viewport area", 26 | "support": "COMMUNITY", 27 | "category": "3D View", 28 | "warning": "Version numbering diverges from Jayanam's original project", 29 | "doc_url": "https://github.com/mmmrqs/bl_ui_widgets", 30 | "tracker_url": "https://github.com/mmmrqs/bl_ui_widgets/issues" 31 | } 32 | 33 | # --- ### Change log 34 | 35 | # v1.0.4 (09.28.2022) - by Marcelo M. Marques 36 | # Added: Logic to 'poll' classmethod that prevents the panel to be opened in multiple simultaneous instances. 37 | 38 | # v1.0.3 (09.25.2022) - by Marcelo M. Marques 39 | # Added: Logic to 'on_invoke' function to identify when in QuadView mode and to position the remote panel appropriately so it 40 | # does not get stuck lost out of visible region. 41 | # Added: New statement necessary for the remote panel to stay open when called by Blender's menu. 42 | # Added: Logic to 'terminate_execution' function to automatically close the panel if user switches between regular and QuadView display modes. 43 | # Added: Logic to 'terminate_execution' function to keep a session variable updated with the clock time that will allow the modal operator 44 | # to identify any odd situation in which the panel is no more active and must be automatically closed. 45 | # Chang: Some notes and comments for better documentation 46 | 47 | # v1.0.2 (10.31.2021) - by Marcelo M. Marques 48 | # Added: 'valid_modes' property to indicate the 'bpy.context.mode' valid values for displaying the panel. 49 | # Added: 'suppress_rendering' function that can be optionally used to control render bypass of the panel widget. 50 | # Added: 'area' and 'region' input parameters to the overridable 'terminate_execution()' function. 51 | 52 | # v1.0.1 (09.20.2021) - by Marcelo M. Marques 53 | # Chang: just some pep8 code formatting 54 | 55 | # v1.0.0 (09.01.2021) - by Marcelo M. Marques 56 | # Added: 'terminate_execution' function that can be overriden by programmer to command termination of the 'Floating Panel'. 57 | # Added: New properties and functions to all widgets (check their corresponding modules for more information). 58 | 59 | # --- ### Imports 60 | import bpy 61 | import time 62 | import os 63 | 64 | from bpy.types import Operator 65 | 66 | from ..bl_ui_widgets.bl_ui_label import BL_UI_Label 67 | from ..bl_ui_widgets.bl_ui_patch import BL_UI_Patch 68 | from ..bl_ui_widgets.bl_ui_checkbox import BL_UI_Checkbox 69 | from ..bl_ui_widgets.bl_ui_slider import BL_UI_Slider 70 | from ..bl_ui_widgets.bl_ui_textbox import BL_UI_Textbox 71 | from ..bl_ui_widgets.bl_ui_button import BL_UI_Button 72 | from ..bl_ui_widgets.bl_ui_tooltip import BL_UI_Tooltip 73 | from ..bl_ui_widgets.bl_ui_draw_op import BL_UI_OT_draw_operator 74 | from ..bl_ui_widgets.bl_ui_drag_panel import BL_UI_Drag_Panel 75 | 76 | from . bl_ui_widget_demo import is_quadview_region 77 | 78 | 79 | class DP_OT_draw_operator(BL_UI_OT_draw_operator): # in: bl_ui_draw_op.py ## 80 | bl_idname = "object.dp_ot_draw_operator" 81 | bl_label = "bl ui widgets custom operator" 82 | bl_description = "Operator for bl ui widgets" 83 | bl_options = {'REGISTER'} 84 | 85 | # --- Blender interface methods quick documentation 86 | # def poll: checked before running the operator, which will never run when poll fails. 87 | # used to check if an operator can run, menu items will be greyed out and if key bindings should be ignored. 88 | # 89 | # def invoke: called by default when accessed from a key binding and menu, this takes the current context - mouse location. 90 | # used for interactive operations such as dragging & drawing. (hint: think of this as "run by a person") 91 | # 92 | # def description: allows a dynamic tooltip that changes based on the context and operator parameters. 93 | # 94 | # def draw: called to draw options, giving control over the layout. Without this, options will draw in the order they are defined. 95 | # 96 | # def modal: handles events which would normally access other operators, they keep running until they return FINISHED. 97 | # used for operators which continuously run, eg: fly mode, knife tool, circle select are all examples of modal operators. 98 | # 99 | # def execute: runs the operator, assuming values are set by the caller (else use defaults). 100 | # used for undo/redo, and executing operators from Python. 101 | # 102 | # def cancel: called when Blender cancels a modal operator, not used often. Internal cleanup can be done here if needed. 103 | 104 | # --- methods 105 | @classmethod 106 | def poll(cls, context): 107 | # Show this panel in View_3D only 108 | if context.space_data.type != 'VIEW_3D': 109 | return False 110 | # Prevents multiple instances of panel 111 | try: 112 | if context.scene.var.RemoVisible and int(time.time()) - context.scene.var.btnRemoTime <= 1: 113 | return False 114 | except: 115 | return False 116 | return True 117 | 118 | def __init__(self): 119 | 120 | super().__init__() 121 | 122 | package = __package__[0:__package__.find(".")] 123 | 124 | # The values assigned to the self.valid_modes variable below will be used to restrict panel display 125 | # to those options only. Set it accordingly to the desired functionality of your particular addon. 126 | # 127 | # Possible valid mode options (as of Blender 2.8): 128 | # 'EDIT_MESH', 'EDIT_CURVE', 'EDIT_SURFACE', 'EDIT_TEXT', 'EDIT_ARMATURE', 'EDIT_METABALL', 129 | # 'EDIT_LATTICE', 'POSE', 'SCULPT', 'PAINT_WEIGHT', 'PAINT_VERTEX', 'PAINT_TEXTURE', 'PARTICLE', 130 | # 'OBJECT', 'PAINT_GPENCIL', 'EDIT_GPENCIL', 'SCULPT_GPENCIL', 'WEIGHT_GPENCIL' 131 | # Additional valid mode option (as of Blender 2.9): 132 | # 'VERTEX_GPENCIL' 133 | # Additional valid mode options (as of Blender 3.2): 134 | # 'EDIT_CURVES', 'SCULPT_CURVES' 135 | 136 | # Note: Leave it empty, e.g. self.valid_modes = {}, for no restrictions to be applied. 137 | self.valid_modes = {'OBJECT', 'EDIT_MESH'} 138 | 139 | # From Preferences/Themes/User Interface/"State" 140 | theme = bpy.context.preferences.themes[0] 141 | ui = theme.user_interface 142 | widget_style = getattr(ui, "wcol_state") 143 | status_color = tuple(widget_style.inner_changed) + (0.3,) 144 | 145 | btnC = 0 # Element counter 146 | btnS = 4 # Button separation (for the smallest ones) 147 | btnG = 0 # Button gap (for the biggest ones) 148 | btnW = 56 # Button width 149 | btnH = 40 + btnS # Button height (takes 2 small buttons plus their separation) 150 | 151 | marginX = 16 # Margin from left border 152 | marginY = 27 # Margin from top border 153 | 154 | btnX = marginX + 1 # Button position X (for the very first button) 155 | btnY = marginY + 1 # Button position Y (for the very first button) 156 | 157 | self.button1 = BL_UI_Button(btnX, btnY, btnW, btnH) 158 | self.button1.style = 'RADIO' 159 | self.button1.text = "PUSH" 160 | self.button1.textwo = "-ME-" 161 | self.button1.text_size = 13 162 | self.button1.textwo_size = 10 163 | self.button1.rounded_corners = (1, 1, 0, 0) 164 | self.button1.set_mouse_up(self.button1_click) 165 | self.button1.set_button_pressed(self.button1_pressed) 166 | self.button1.description = "Press this button to unlock its brothers (after the button " + \ 167 | "with a 'LOCK' caption has been pressed). {Let me " + \ 168 | "type a very large description here to showcase a " + \ 169 | "tooltip text automatically being wrapped around " + \ 170 | "multiple lines inside its containing box}. Whatever " + \ 171 | "comes beyond the defined limit of maximum lines " + \ 172 | "will be left out! You'll see this text will be truncated..." 173 | self.button1.shortcut = "Shortcut: None" 174 | self.button1.python_cmd = "bpy.ops.object.dp_ot_draw_operator.button1_click()" 175 | if self.button1_pressed(self.button1): 176 | self.button1.state = 3 177 | btnC += 1 178 | # 179 | self.button2 = BL_UI_Button((btnX + ((btnW - 1 + btnG) * btnC)), btnY, btnW, btnH) 180 | self.button2.style = 'RADIO' 181 | self.button2.text = "HELP" 182 | self.button2.textwo = "(Go)" 183 | self.button2.text_size = 13 184 | self.button2.textwo_size = 10 185 | self.button2.rounded_corners = (0, 0, 0, 0) 186 | self.button2.set_mouse_up(self.button2_click) 187 | self.button2.set_button_pressed(self.button2_pressed) 188 | self.button2.description = "You can help me by doing as follows:\n" + \ 189 | " -Press button 4 to Disable Me\n" + \ 190 | " -Press button 1 to Enable Me" 191 | self.button2.python_cmd = "bpy.ops.object.dp_ot_draw_operator.button2_click()" 192 | if self.button2_pressed(self.button2): 193 | self.button2.state = 3 194 | btnC += 1 195 | # 196 | self.button3 = BL_UI_Button((btnX + ((btnW - 1 + btnG) * btnC)), btnY, btnW, btnH) 197 | self.button3.style = 'RADIO' 198 | self.button3.text = "ADD" 199 | self.button3.text_size = 13 200 | self.button3.rounded_corners = (0, 0, 0, 0) 201 | self.button3.set_mouse_up(self.button3_click) 202 | self.button3.set_button_pressed(self.button3_pressed) 203 | self.button3.description = "Adds one little 'MONKEY' object to 3D View area" 204 | self.button3.python_cmd = "bpy.ops.object.dp_ot_draw_operator.button3_click()" 205 | if self.button3_pressed(self.button3): 206 | self.button3.state = 3 207 | btnC += 1 208 | # 209 | self.button4 = BL_UI_Button((btnX + ((btnW - 1 + btnG) * btnC)), btnY, btnW, btnH) 210 | self.button4.style = 'RADIO' 211 | self.button4.text = "LOCK" 212 | self.button4.textwo = "(CTRL-L)" 213 | self.button4.text_size = 13 214 | self.button4.textwo_size = 10 215 | self.button4.rounded_corners = (0, 0, 1, 1) 216 | self.button4.set_mouse_up(self.button4_click) 217 | self.button4.set_button_pressed(self.button4_pressed) 218 | self.button4.enabled = (not bpy.context.scene.var.OpState6) 219 | self.button4.description = "This button does nothing more than to disable 2 and 3.\n" +\ 220 | "Note: CTRL-L does not actually work as a short-cut, but " +\ 221 | "you could have programmed it to do so" 222 | # In this example there is no .python_cmd populated, thus only the main description appears in the tooltip 223 | if self.button4_pressed(self.button4): 224 | self.button4.state = 3 225 | btnC += 1 226 | oldX = (btnX + ((btnW - 1 + btnG) * btnC)) 227 | oldH = btnH 228 | oldW = btnW 229 | newX = oldX + marginX + btnW - 1 + btnS 230 | btnW = 96 231 | btnH = 20 232 | # 233 | self.button5 = BL_UI_Button(newX, btnY, btnW, btnH) 234 | self.button5.text = "Do Nothing" 235 | newY = btnY + btnH + btnS 236 | # 237 | self.button6 = BL_UI_Button(newX, newY, btnW, btnH) 238 | self.button6.selected_color = status_color 239 | self.button6.text = "Switch Btn 4" 240 | self.button6.set_mouse_up(self.button6_click) 241 | self.button6.set_button_pressed(self.button6_pressed) 242 | self.button6.description = "Switches button 4's state" 243 | if self.button6_pressed(self.button6): 244 | self.button6.state = 3 245 | # 246 | newX = newX + btnW - 1 + btnS 247 | btnW = 120 248 | # 249 | self.number1 = BL_UI_Slider(newX, btnY, btnW, btnH) 250 | self.number1.style = 'NUMBER_CLICK' 251 | self.number1.value = 500 252 | self.number1.step = 100 253 | self.number1.unit = "m" 254 | self.number1.precision = 0 255 | self.number1.description = "This is a click slider component and it can work with a full range of values" 256 | self.number1.set_value_updated(self.number1_update) 257 | # 258 | self.slider1 = BL_UI_Slider(newX, newY, btnW, btnH) 259 | self.slider1.style = 'NUMBER_SLIDE' 260 | self.slider1.text = "Z Rot" 261 | self.slider1.value = 180 262 | self.slider1.min = 0 263 | self.slider1.max = 360 264 | self.slider1.description = "This is a standard slider component with a 0 to 100 sliding percent bar.\nYou can use it to rotate the selected object(s) in the scene" 265 | self.slider1.set_value_updated(self.slider1_update) 266 | # 267 | self.objname = "" 268 | self.textbox1 = BL_UI_Textbox(btnX, newY + 35, 350, btnH) 269 | self.textbox1.text = self.objname 270 | self.textbox1.max_input_chars = 50 271 | self.textbox1.description = "Textbox editing entry field" 272 | self.textbox1.set_value_changed(self.textbox1_changed) 273 | self.textbox1.enabled = False 274 | # 275 | self.check1 = BL_UI_Checkbox(newX, newY + 37, btnW, btnH) 276 | self.check1.text = "Unregit" 277 | self.check1.set_value_changed(self.check1_changed) 278 | self.check1.description = "This checkbox accesses an 'UNREGISTER' fully circular button" 279 | self.check1.python_cmd = "bpy.ops.object.dp_ot_draw_operator.check1_changed()" 280 | self.check1.is_checked = False 281 | # ----------- 282 | 283 | panW = newX + btnW + 2 + marginX # Panel desired width (beware: this math is good for my setup only) 284 | panH = newY + btnH + 0 + 10 + 35 # Panel desired height (ditto) 285 | 286 | # Save the panel's size to preferences properties to be used in there 287 | bpy.context.preferences.addons[package].preferences.RC_PAN_W = panW 288 | bpy.context.preferences.addons[package].preferences.RC_PAN_H = panH 289 | 290 | # Need this just because I want the panel to be centered 291 | if bpy.context.preferences.addons[package].preferences.RC_UI_BIND: 292 | # From Preferences/Interface/"Display" 293 | ui_scale = bpy.context.preferences.view.ui_scale 294 | else: 295 | ui_scale = 1 296 | over_scale = bpy.context.preferences.addons[package].preferences.RC_SCALE 297 | 298 | # The panel X and Y coords are in relation to the bottom-left corner of the 3D viewport area 299 | panX = int((bpy.context.area.width - panW * ui_scale * over_scale) / 2.0) + 1 # Panel X coordinate, for panel's top-left corner 300 | panY = panH + 100 - 1 # The '100' is just a spacing # Panel Y coordinate, for panel's top-left corner 301 | 302 | self.panel = BL_UI_Drag_Panel(panX, panY, panW, panH) 303 | self.panel.style = 'PANEL' # Options are: {HEADER,PANEL,SUBPANEL,TOOLTIP,NONE} 304 | 305 | self.tooltip = BL_UI_Tooltip() # This is for displaying the widgets tooltips. Only need one instance! 306 | 307 | self.patch1 = BL_UI_Patch(0, 0, panW, 17) 308 | self.patch1.style = 'HEADER' 309 | self.patch1.set_mouse_move(self.patch1_mouse_move) 310 | 311 | self.label1 = BL_UI_Label(5, 12, panW, 17) 312 | self.label1.style = 'TITLE' 313 | self.label1.text = "Panel Title For Example" 314 | self.label1.size = 12 315 | 316 | self.label2 = BL_UI_Label(panW - 100, 12, 100, 17) 317 | self.label2.text = "" # This empty text label will be used later on to display some dynamic values on the panel 318 | 319 | self.patch2 = BL_UI_Patch(oldX + 10, btnY, oldW, oldH) 320 | self.patch2.bg_color = (0, 0, 0, 0) 321 | self.patch2.outline_color = (1, 1, 1, 0.4) 322 | self.patch2.roundness = 0.4 323 | self.patch2.corner_radius = 10 324 | self.patch2.shadow = True 325 | self.patch2.rounded_corners = (1, 1, 1, 1) 326 | self.patch2.description = "This could be any image (size, color, transparent etc)\n" + \ 327 | "and the cool part is that it scales together with the panel" 328 | 329 | script_file = os.path.realpath(__file__) 330 | directory = os.path.dirname(script_file) 331 | imagePath = directory + "\\..\\img\\rotate.png" 332 | self.patch2.set_image(imagePath) 333 | self.patch2.set_image_size((32, 32)) 334 | self.patch2.set_image_position((11, 5)) 335 | 336 | # ----------- 337 | # Display an 'UNRegister' button on screen 338 | # ================================================================== 339 | # -- This is just for demonstration, not to be used in production 340 | # ================================================================== 341 | btnH = 32 342 | newX = panW - marginX - btnH - 2 343 | newY = panH - btnH - 6 344 | self.buttonU = BL_UI_Button(newX, newY, btnH, btnH) 345 | self.buttonU.text = "UNR" 346 | self.buttonU.text_size = 12 347 | self.buttonU.text_color = (1, 1, 1, 1) 348 | self.buttonU.bg_color = (0.5, 0, 0, 1) 349 | self.buttonU.outline_color = (0.6, 0.6, 0.6, 0.8) 350 | self.buttonU.corner_radius = btnH / 2 - 1 351 | self.buttonU.roundness = 1.0 352 | self.buttonU.set_mouse_up(self.buttonU_click) 353 | self.buttonU.description = "Unregisters the Remote Control panel object and closes it" 354 | self.buttonU.python_cmd = "bpy.ops.object.dp_ot_draw_operator.buttonU_click()" 355 | self.buttonU.visible = False 356 | 357 | def on_invoke(self, context, event): 358 | # Add your widgets here (TODO: perhaps a better, more automated solution?) 359 | # -------------------------------------------------------------------------------------------------- 360 | widgets_panel = [self.panel 361 | ] 362 | widgets_items = [self.patch1, self.patch2, self.label1, self.label2, 363 | self.button1, self.button2, self.button3, self.button4, self.button5, self.button6, 364 | self.buttonU, self.slider1, self.number1, self.check1, self.textbox1, 365 | self.tooltip, # <-- If there is a tooltip object, it must be the last in this list 366 | ] 367 | # -------------------------------------------------------------------------------------------------- 368 | 369 | widgets = widgets_panel + widgets_items 370 | 371 | self.init_widgets(context, widgets, self.valid_modes) 372 | 373 | self.panel.add_widgets(widgets_items) 374 | 375 | self.panel.quadview, _, region = is_quadview_region(context) 376 | 377 | if self.panel.quadview and region: 378 | # When in QuadView mode it has to be manually repositioned otherwise may get stuck out of region space 379 | package = __package__[0:__package__.find(".")] 380 | if bpy.context.preferences.addons[package].preferences.RC_UI_BIND: 381 | # From Preferences/Interface/"Display" 382 | ui_scale = bpy.context.preferences.view.ui_scale 383 | else: 384 | ui_scale = 1 385 | over_scale = bpy.context.preferences.addons[package].preferences.RC_SCALE 386 | panX = int((region.width - (self.panel.width * ui_scale * over_scale)) / 2.0) + 1 387 | panY = self.panel.height + 10 - 1 # The '10' is just a spacing from region border 388 | self.panel.set_location(panX, panY) 389 | else: 390 | self.panel.set_location(self.panel.x, self.panel.y) 391 | 392 | # This statement is necessary so that the remote panel stays open when called by Blender's menu 393 | bpy.context.scene.var.RemoVisible = True 394 | 395 | # -- Helper function 396 | 397 | def suppress_rendering(self, area, region): 398 | ''' 399 | This is a special case 'overriding function' to allow subclass control for displaying (rendering) the panel. 400 | Function is defined in class BL_UI_OT_draw_operator (bl_ui_draw_op.py) and available to be inherited here. 401 | If not included here the function in the superclass just returns 'False' and rendering is always executed. 402 | When 'True" is returned below, the rendering of the entire panel is bypassed and it is not drawn on screen. 403 | ''' 404 | return False 405 | 406 | def terminate_execution(self, area, region, event): 407 | ''' 408 | This is a special case 'overriding function' to allow subclass control for terminating/closing the panel. 409 | Function is defined in class BL_UI_OT_draw_operator (bl_ui_draw_op.py) and available to be inherited here. 410 | If not included here the function in the superclass just returns 'False' and no termination is executed. 411 | When 'True" is returned below, the execution is auto terminated and the 'Remote Control' panel closes itself. 412 | ''' 413 | ''' 414 | BEWARE THAT ARGUMENTS 'AREA' AND/OR 'REGION' CAN BE EQUAL TO "NONE" 415 | ''' 416 | if self.panel.quadview and area is None: 417 | bpy.context.scene.var.RemoVisible = False 418 | else: 419 | if not area is None: 420 | if area.type == 'VIEW_3D': 421 | # If user switches between regular and QuadView display modes, the panel is automatically closed 422 | is_quadview = (len(area.spaces.active.region_quadviews) > 0) 423 | if self.panel.quadview != is_quadview: 424 | bpy.context.scene.var.RemoVisible = False 425 | 426 | if event.type == 'TIMER' and bpy.context.scene.var.RemoVisible: 427 | # Update the remote panel "clock marker". This marker is used to keep track if the remote panel is 428 | # actually opened and active. In the case that bpy.context.scene.var.RemoVisible state gets misleading 429 | # the panel will be automatically closed when this clock marker has not been updated for more than 1 sec 430 | bpy.context.scene.var.btnRemoTime = int(time.time()) 431 | 432 | return (not bpy.context.scene.var.RemoVisible) 433 | 434 | # -- Button press handlers 435 | 436 | def button1_click(self, widget, event, x, y): 437 | self.button2.enabled = True 438 | self.button3.enabled = True 439 | self.press_only(1) 440 | 441 | def button2_click(self, widget, event, x, y): 442 | self.press_only(2) 443 | 444 | def button3_click(self, widget, event, x, y): 445 | bpy.ops.mesh.primitive_monkey_add() 446 | self.objname = bpy.context.object.name 447 | self.textbox1.text = "'" + self.objname + "' is her name, but you can edit it here!" 448 | self.textbox1.enabled = True 449 | self.press_only(3) 450 | 451 | def button4_click(self, widget, event, x, y): 452 | self.button2.enabled = False 453 | self.button3.enabled = False 454 | self.press_only(4) 455 | 456 | # I am not even obligated to create any of these functions, see? 457 | # button5 does not have an active function tied to it at all. 458 | # 459 | # def button5_click(self, widget, event, x, y): 460 | # # Miss Me 461 | 462 | def button6_click(self, widget, event, x, y): 463 | var = bpy.context.scene.var 464 | var.OpState6 = (not var.OpState6) 465 | self.button4.enabled = (not self.button4.enabled) 466 | 467 | def buttonU_click(self, widget, event, x, y): 468 | self.finish() 469 | 470 | def button1_pressed(self, widget): 471 | return (bpy.context.scene.var.OpState1) 472 | 473 | def button2_pressed(self, widget): 474 | return (bpy.context.scene.var.OpState2) 475 | 476 | def button3_pressed(self, widget): 477 | return (bpy.context.scene.var.OpState3) 478 | 479 | def button4_pressed(self, widget): 480 | return (bpy.context.scene.var.OpState4) 481 | 482 | # I am not even obligated to create any of these functions, see? 483 | # button5 does not have an active function tied to it at all. 484 | # 485 | # def button5_pressed(self, widget): 486 | # return (bpy.context.scene.var.OpState5) 487 | 488 | def button6_pressed(self, widget): 489 | return (bpy.context.scene.var.OpState6) 490 | 491 | def patch1_mouse_move(self, widget, event, x, y): 492 | self.label2.text = "x: " + str(x) + " y: " + str(y) 493 | return False 494 | 495 | def number1_update(self, widget, value): 496 | # Example of a dynamic unit conversion with dynamic min/max limits 497 | converted = False 498 | if widget.unit == "mm" and value >= 1000: 499 | # Upscale to meters 500 | value = value / 1000 501 | widget.unit = "m" 502 | widget.step = 10 503 | widget.precision = 0 504 | converted = True 505 | if widget.unit == "m" and value >= 1000: 506 | # Upscale to kilometers 507 | value = value / 1000 508 | widget.unit = "km" 509 | widget.step = 0.1 510 | widget.precision = 1 511 | converted = True 512 | if widget.unit == "km" and value >= 10: 513 | # I want my hardcoded max limit to be 10 km 514 | value = 10 515 | converted = True 516 | if widget.unit == "km" and value < 1: 517 | # Downscale to meters 518 | value = value * 1000 519 | widget.unit = "m" 520 | widget.step = 10 521 | widget.precision = 0 522 | converted = True 523 | if widget.unit == "m" and value < 1: 524 | # Downscale to millimeters 525 | value = value * 1000 526 | widget.unit = "mm" 527 | widget.step = 10 528 | widget.precision = 0 529 | converted = True 530 | if widget.unit == "mm" and value < 1: 531 | # I want my hardcoded min limit to be 1 mm 532 | value = 1 533 | converted = True 534 | 535 | if converted: 536 | widget.value = round(value, widget.precision) 537 | return False 538 | else: 539 | # By returning True the 'value' argument will be committed to the widget.value property 540 | return True 541 | 542 | def slider1_update(self, widget, value): 543 | import math 544 | try: 545 | for obj in bpy.context.selected_objects: 546 | if obj.type == 'MESH': 547 | obj.rotation_euler[2] = math.radians(value) 548 | except Exception as e: 549 | pass 550 | return True 551 | 552 | def textbox1_changed(self, widget, context, former_text, updated_text): 553 | # This is just an example done in a rush, so not much thinking and probably with bugs ;-) 554 | # self.objname is a variable declared in this module (around the place where self.textbox1 is declared) 555 | if updated_text != self.objname: 556 | if updated_text.strip() == "": 557 | self.objname = "" 558 | widget.text = self.objname 559 | return True 560 | try: 561 | for obj in bpy.data.objects: 562 | if obj.type == 'MESH' and obj.name == self.objname: 563 | obj.name = updated_text 564 | self.objname = obj.name 565 | break 566 | except Exception as e: 567 | self.objname = "" 568 | widget.text = self.objname 569 | return True 570 | 571 | def check1_changed(self, widget, event, x, y): 572 | self.buttonU.visible = not self.check1.is_checked 573 | return True 574 | 575 | # -- Helper functions 576 | 577 | def press_only(self, button): 578 | var = bpy.context.scene.var 579 | var.OpState1 = (button == 1) 580 | var.OpState2 = (button == 2) 581 | var.OpState3 = (button == 3) 582 | var.OpState4 = (button == 4) 583 | 584 | 585 | # -Register/unregister processes 586 | 587 | def register(): 588 | bpy.utils.register_class(DP_OT_draw_operator) 589 | 590 | 591 | def unregister(): 592 | bpy.utils.unregister_class(DP_OT_draw_operator) 593 | 594 | 595 | if __name__ == '__main__': 596 | register() 597 | -------------------------------------------------------------------------------- /bl_ui_widgets/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /bl_ui_widgets/bl_ui_button.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 | # --- ### Header 20 | bl_info = {"name": "BL UI Widgets", 21 | "description": "UI Widgets to draw in the 3D view", 22 | "author": "Marcelo M. Marques (fork of Jayanam's original project)", 23 | "version": (1, 0, 2), 24 | "blender": (2, 80, 75), 25 | "location": "View3D > viewport area", 26 | "support": "COMMUNITY", 27 | "category": "3D View", 28 | "warning": "Version numbering diverges from Jayanam's original project", 29 | "doc_url": "https://github.com/mmmrqs/bl_ui_widgets", 30 | "tracker_url": "https://github.com/mmmrqs/bl_ui_widgets/issues" 31 | } 32 | 33 | # --- ### Change log 34 | 35 | # v1.0.2 (10.31.2021) - by Marcelo M. Marques 36 | # Chang: improved reliability on 'mouse_down' and 'mouse_up' overridable functions by conditioning the returned value 37 | 38 | # v1.0.1 (09.20.2021) - by Marcelo M. Marques 39 | # Chang: just some pep8 code formatting 40 | 41 | # v1.0.0 (09.01.2021) - by Marcelo M. Marques 42 | # Added: Logic to scale the button according to both Blender's ui scale configuration and this addon 'preferences' setup 43 | # Added: 'textwo' property and coding to allow a second line of text in the button's caption (value is string). 44 | # The 1 line or 2 lines are always centered both in horizontal and vertical dimensions. 45 | # Added: 'textwo_size' property to allow different size from the other text line (value is integer). 46 | # Added: 'textwo_color' property to allow different color from the other text line (value is standard color tuple). 47 | # Added: 'text_highlight' and 'textwo_highlight' properties to allow different text colors on the selected button (value is standard color tuple). 48 | # Added: 'outline_color' property to allow different color on the button outline (value is standard color tuple). 49 | # Added: 'roundness' property to allow the button to be painted with rounded corners, 50 | # same as that property available in Blender's user themes and it works together with 'rounded_corners' below. 51 | # Added: 'corner_radius' property to allow a limit for the roundness curvature, more useful when 'roundness' property 52 | # is not overriden by programmer and the one from Blender's user themes is used instead. 53 | # Added: 'rounded_corners' property and coding to allow the button to be painted with rounded corners (value is a 4 elements tuple). 54 | # Each elements is a boolean value (0 or 1) which indicates whether the corresponding corner is to be rounded or straight 55 | # in the following clockwise sequence: bottom left, top left, top right, bottom right. 56 | # Added: 'shadow' property and coding to allow the button to be painted with a shadow (value is boolean). 57 | # Added: 'alignment' property and coding to allow customization of button's text lines alignment. 58 | # Added: Logic to allow a button to be disabled (darkned out) and turned off to user interaction. 59 | # Added: A third state for the button to allow it to stay in a state of 'pressed' (similar to a radio button). 60 | # Added: A fourth and fifth state for special case use regarding the 'NUMBER_CLICK' slider widget type. 61 | # Added: 'mouse_up_over' internal function to control the button 'pressed' state. It is called by BL_UI_Widget class 62 | # and allows the wrap up of events when the user finishes clicking a button. 63 | # Added: 'set_button_pressed' function to allow assignment of an external function to be called by 'mouse_up_func' and 'mouse_up_over'. 64 | # Added: Shadow and Kerning related properties that allow the text to be painted using these characteristics. 65 | # Added: Size, Shadow and Kerning attributes default to values retrieved from user theme (may be overriden by programmer). 66 | # Chang: Made it a subclass of 'BL_UI_Patch' instead of 'BL_UI_Widget' so that it can inherit the layout features from there. 67 | # Chang: Instead of hardcoded logic it is now leveraging 'BL_UI_Label' to paint the button text lines. 68 | # Chang: 'draw_text' function logic leveraged to be used by 'BL_UI_Textbox' and 'BL_UI_Slider' classes. 69 | # Added: 'set_mouse_down' function moved into 'BL_UI_Widget' superclass. 70 | # Fixed: The calculation of vertical text centering because it was varying depending on which letters presented in the text. 71 | 72 | # --- ### Imports 73 | import bpy 74 | import blf 75 | 76 | from . bl_ui_patch import BL_UI_Patch 77 | from . bl_ui_label import BL_UI_Label 78 | 79 | 80 | class BL_UI_Button(BL_UI_Patch): 81 | 82 | def __init__(self, x, y, width, height): 83 | super().__init__(x, y, width, height) 84 | 85 | # Note: '_style' value will always be ignored if the bg_color value is overriden after object initialization. 86 | 87 | self._text = "Button" 88 | self._textwo = "" 89 | self._text_color = None # Button text color (first row) 90 | self._text_highlight = None # Button high color (first row) 91 | self._textwo_color = None # Button text color (second row) 92 | self._textwo_highlight = None # Button high color (second row) 93 | 94 | self._style = 'TOOL' # Button color styles are: {TOOL,RADIO,TOGGLE,NUMBER_CLICK,NUMBER_SLIDE,TEXTBOX} 95 | self._bg_color = None # Button face color (when pressed state == 0) 96 | self._selected_color = None # Button face color (when pressed state == 3) 97 | self._outline_color = None # Button outline color 98 | self._roundness = None # Button corners roundness factor [0..1] 99 | self._radius = 8.5 # Button corners circular radius 100 | self._rounded_corners = (1, 1, 1, 1) # 1=Round/0=Straight, coords:(bottomLeft,topLeft,topRight,bottomRight) 101 | self._has_shadow = True # Indicates whether a shadow must be drawn around the button 102 | self._alignment = 'CENTER' # Text alignment options: {CENTER,LEFT,RIGHT} 103 | 104 | self._text_size = None # Button text line 1 size 105 | self._textwo_size = None # Button text line 2 size 106 | self._text_margin = 0 # Margin for left aligned text (used by slider and textbox objects) 107 | 108 | self._text_kerning = None # Button text kerning (True/False) 109 | self._text_shadow_size = None # Button text shadow size 110 | self._text_shadow_offset_x = None # Button text shadow offset x (positive goes right) 111 | self._text_shadow_offset_y = None # Button text shadow offset y (negative goes down) 112 | self._text_shadow_color = None # Button text shadow color [0..1] = gray tone, from dark to clear 113 | self._text_shadow_alpha = None # Button text shadow alpha value [0..1] 114 | 115 | self._textpos = (x, y) 116 | 117 | self.__state = 0 # 0 is UP; 1 is Down; 2 is Hover when not pressed or down; 3 is Pressed 118 | 119 | @property 120 | def state(self): 121 | return self.__state 122 | 123 | @state.setter 124 | def state(self, value): 125 | self.__state = value 126 | 127 | @property 128 | def selected_color(self): 129 | return self._selected_color 130 | 131 | @selected_color.setter 132 | def selected_color(self, value): 133 | self._selected_color = value 134 | 135 | @property 136 | def alignment(self): 137 | return self._alignment 138 | 139 | @alignment.setter 140 | def alignment(self, value): 141 | self._alignment = value 142 | 143 | @property 144 | def text(self): 145 | return self._text 146 | 147 | @text.setter 148 | def text(self, value): 149 | self._text = value 150 | 151 | @property 152 | def text_size(self): 153 | return self._text_size 154 | 155 | @text_size.setter 156 | def text_size(self, value): 157 | self._text_size = value 158 | 159 | @property 160 | def text_color(self): 161 | return self._text_color 162 | 163 | @text_color.setter 164 | def text_color(self, value): 165 | self._text_color = value 166 | 167 | @property 168 | def text_highlight(self): 169 | return self._text_highlight 170 | 171 | @text_highlight.setter 172 | def text_highlight(self, value): 173 | self._text_highlight = value 174 | 175 | @property 176 | def textwo(self): 177 | return self._textwo 178 | 179 | @textwo.setter 180 | def textwo(self, value): 181 | self._textwo = value 182 | 183 | @property 184 | def textwo_size(self): 185 | return self._textwo_size 186 | 187 | @textwo_size.setter 188 | def textwo_size(self, value): 189 | self._textwo_size = value 190 | 191 | @property 192 | def textwo_color(self): 193 | return self._textwo_color 194 | 195 | @textwo_color.setter 196 | def textwo_color(self, value): 197 | self._textwo_color = value 198 | 199 | @property 200 | def textwo_highlight(self): 201 | return self._textwo_highlight 202 | 203 | @textwo_highlight.setter 204 | def textwo_highlight(self, value): 205 | self._textwo_highlight = value 206 | 207 | @property 208 | def text_margin(self): 209 | return self._text_margin 210 | 211 | @text_margin.setter 212 | def text_margin(self, value): 213 | self._text_margin = value 214 | 215 | @property 216 | def text_kerning(self): 217 | return self._text_kerning 218 | 219 | @text_kerning.setter 220 | def text_kerning(self, value): 221 | self._text_kerning = value 222 | 223 | @property 224 | def text_shadow_size(self): 225 | return self._text_shadow_size 226 | 227 | @text_shadow_size.setter 228 | def text_shadow_size(self, value): 229 | self._text_shadow_size = value 230 | 231 | @property 232 | def text_shadow_offset_x(self): 233 | return self._text_shadow_offset_x 234 | 235 | @text_shadow_offset_x.setter 236 | def text_shadow_offset_x(self, value): 237 | self._text_shadow_offset_x = value 238 | 239 | @property 240 | def text_shadow_offset_y(self): 241 | return self._text_shadow_offset_y 242 | 243 | @text_shadow_offset_y.setter 244 | def text_shadow_offset_y(self, value): 245 | self._text_shadow_offset_y = value 246 | 247 | @property 248 | def text_shadow_color(self): 249 | return self._text_shadow_color 250 | 251 | @text_shadow_color.setter 252 | def text_shadow_color(self, value): 253 | self._text_shadow_color = value 254 | 255 | @property 256 | def text_shadow_alpha(self): 257 | return self._text_shadow_alpha 258 | 259 | @text_shadow_alpha.setter 260 | def text_shadow_alpha(self, value): 261 | self._text_shadow_alpha = value 262 | 263 | def set_button_pressed(self, button_pressed_func): 264 | self.button_pressed_func = button_pressed_func 265 | 266 | def button_pressed_func(self, widget): 267 | # This must return False when function is not overriden, so that button does 268 | # not turn into pressed mode everytime the user clicks over it. 269 | return False 270 | 271 | # Overrides base class function 272 | def mouse_down_func(self, widget, event, x, y): 273 | # This must return True when function is not overriden, so that button action 274 | # only works while mouse is over the button (that is while it is_in_rect(x,y)). 275 | return True 276 | 277 | # Overrides base class function 278 | def mouse_up_func(self, widget, event, x, y): 279 | # This must return True when function is not overriden, so that button action 280 | # only works while mouse is over the button (that is while it is_in_rect(x,y)). 281 | return True 282 | 283 | # Overrides base class function 284 | def update(self, x, y): 285 | super().update(x, y) 286 | self._textpos = [x, y] 287 | 288 | # Overrides base class function 289 | def set_colors(self): 290 | if not self._is_enabled: 291 | if self._bg_color is None: 292 | theme = bpy.context.preferences.themes[0] 293 | widget_style = getattr(theme.user_interface, self.my_style()) 294 | color = widget_style.inner 295 | else: 296 | color = self._bg_color 297 | # Take the "state 0" background color and "dark" it by either 20% or 10% 298 | color = self.shade_color(color, (0.2 if color[0] < 0.5 else 0.1)) 299 | else: 300 | # Up 301 | if self.__state == 0: 302 | if self._bg_color is None: 303 | theme = bpy.context.preferences.themes[0] 304 | widget_style = getattr(theme.user_interface, self.my_style()) 305 | color = widget_style.inner 306 | else: 307 | color = self._bg_color 308 | # Down 309 | elif self.__state == 1: 310 | if self._selected_color is None: 311 | theme = bpy.context.preferences.themes[0] 312 | widget_style = getattr(theme.user_interface, self.my_style()) 313 | color = widget_style.inner_sel 314 | else: 315 | color = self._selected_color 316 | # Hover 317 | elif self.__state == 2: 318 | if self._bg_color is None: 319 | theme = bpy.context.preferences.themes[0] 320 | widget_style = getattr(theme.user_interface, self.my_style()) 321 | color = widget_style.inner 322 | else: 323 | color = self._bg_color 324 | # Take the "state 0" background color and "tint" it by either 20% or 10% 325 | color = self.tint_color(color, (0.2 if color[0] < 0.5 else 0.1)) 326 | # Pressed 327 | elif self.__state == 3: 328 | if self._selected_color is None: 329 | theme = bpy.context.preferences.themes[0] 330 | widget_style = getattr(theme.user_interface, self.my_style()) 331 | color = widget_style.inner_sel 332 | else: 333 | color = self._selected_color 334 | # Hover++ (special case used by 'NUMBER_CLICK' sliders only); this is similar to __state == 2 335 | elif self.__state == 4: 336 | if self._bg_color is None: 337 | theme = bpy.context.preferences.themes[0] 338 | widget_style = getattr(theme.user_interface, self.my_style()) 339 | basecolor = widget_style.inner 340 | else: 341 | basecolor = self._bg_color 342 | # Has to tint the color twice the same factor values used for "state 0" 343 | color = self.tint_color(basecolor, (0.2 if basecolor[0] < 0.5 else 0.1)) 344 | color = self.tint_color(color, (0.2 if basecolor[0] < 0.5 else 0.1)) 345 | # Down++ (special case used by 'NUMBER_CLICK' sliders only); this is similar to __state == 1 346 | elif self.__state == 5: 347 | if self._selected_color is None: 348 | theme = bpy.context.preferences.themes[0] 349 | widget_style = getattr(theme.user_interface, self.my_style()) 350 | basecolor = widget_style.inner_sel 351 | else: 352 | basecolor = self._selected_color 353 | # Has to tint the color twice the same factor values used for "state 1" 354 | color = self.tint_color(basecolor, (0.2 if basecolor[0] < 0.5 else 0.1)) 355 | color = self.tint_color(color, (0.2 if basecolor[0] < 0.5 else 0.1)) 356 | 357 | self.shader.uniform_float("color", color) 358 | 359 | # Overrides base class function 360 | def draw_text(self): 361 | if not self._is_visible: 362 | return 363 | 364 | if self._text == "" and self._textwo == "": 365 | return 366 | 367 | theme = bpy.context.preferences.themes[0] 368 | widget_style = getattr(theme.user_interface, self.my_style()) 369 | 370 | if self._is_enabled and (self.button_pressed_func(self) or self.__state in [1, 3, 5]): 371 | text_color = tuple(widget_style.text_sel) + (1.0,) if self._text_highlight is None else self._text_highlight 372 | textwo_color = tuple(widget_style.text_sel) + (1.0,) if self._textwo_highlight is None else self._textwo_highlight 373 | else: 374 | text_color = tuple(widget_style.text) + (1.0,) if self._text_color is None else self._text_color 375 | textwo_color = tuple(widget_style.text) + (1.0,) if self._textwo_color is None else self._textwo_color 376 | 377 | theme = bpy.context.preferences.ui_styles[0] 378 | widget_style = getattr(theme, "widget") 379 | 380 | if self._text_size is None: 381 | text_size = widget_style.points 382 | leveraged_text_size = text_size 383 | else: 384 | text_size = self._text_size 385 | leveraged_text_size = self.leverage_text_size(text_size, "widget") 386 | scaled_size = int(self.over_scale(leveraged_text_size)) 387 | 388 | if bpy.app.version >= (3, 0, 0): # 3.00 issue: 'font_kerning_style' has become extinct 389 | text_kerning = False 390 | else: 391 | text_kerning = (widget_style.font_kerning_style == 'FITTED') if self._text_kerning is None else self._text_kerning 392 | if text_kerning: 393 | blf.enable(0, blf.KERNING_DEFAULT) 394 | 395 | blf.size(0, leveraged_text_size, 72) 396 | normal1 = blf.dimensions(0, "W")[1] # This is to keep a regular pattern since letters differ in height 397 | 398 | blf.size(0, scaled_size, 72) 399 | length1 = blf.dimensions(0, self._text)[0] 400 | height1 = blf.dimensions(0, "W")[1] 401 | 402 | if self._textwo != "": 403 | if self._textwo_size is None: 404 | textwo_size = widget_style.points 405 | leveraged_text_size = textwo_size 406 | else: 407 | textwo_size = self._textwo_size 408 | leveraged_text_size = self.leverage_text_size(textwo_size, "widget") 409 | scaled_size = int(self.over_scale(leveraged_text_size)) 410 | blf.size(0, leveraged_text_size, 72) 411 | normal2 = blf.dimensions(0, "W")[1] # This is to keep a regular pattern since letters differ in height 412 | blf.size(0, scaled_size, 72) 413 | length2 = blf.dimensions(0, self._textwo)[0] 414 | height2 = blf.dimensions(0, "W")[1] 415 | else: 416 | normal2 = 0 417 | length2 = 0 418 | height2 = 0 419 | 420 | if text_kerning: 421 | blf.disable(0, blf.KERNING_DEFAULT) 422 | 423 | if self._text == "" or self._textwo == "": 424 | middle_gap = 0 425 | else: 426 | middle_gap = 4 427 | 428 | over_scale = self.over_scale(1) 429 | 430 | if self._style in {'NUMBER_CLICK', 'NUMBER_SLIDE', 'TEXTBOX'}: 431 | top_margin = int((self.height - normal1) / 2.0) 432 | else: 433 | top_margin = int((self.height - int(round(normal1 + 0.499)) - int(round(normal2 + 0.499)) - middle_gap) / 2.0) 434 | 435 | textpos_y = self.y_screen - top_margin - int(round(normal1 + 0.499)) + 1 436 | 437 | shadow_size = widget_style.shadow if self._text_shadow_size is None else self._text_shadow_size 438 | shadow_offset_x = widget_style.shadow_offset_x if self._text_shadow_offset_x is None else self._text_shadow_offset_x 439 | shadow_offset_y = widget_style.shadow_offset_y if self._text_shadow_offset_y is None else self._text_shadow_offset_y 440 | shadow_color = widget_style.shadow_value if self._text_shadow_color is None else self._text_shadow_color 441 | shadow_alpha = widget_style.shadow_alpha if self._text_shadow_alpha is None else self._text_shadow_alpha 442 | 443 | if self._text != "": 444 | if self._alignment == 'LEFT' or self._style in {'NUMBER_SLIDE', 'TEXTBOX'} or (self._style == 'NUMBER_CLICK' and self._is_mslider): 445 | textpos_x = self.x_screen + self._text_margin 446 | elif self._alignment == 'RIGHT': 447 | textpos_x = self.x_screen + int((self.width - (length1 / over_scale)) - self._text_margin) 448 | else: 449 | textpos_x = self.x_screen + int((self.width - (length1 / over_scale)) / 2.0) 450 | 451 | label = BL_UI_Label(textpos_x, textpos_y, length1, height1) 452 | label.style = 'BUTTON' 453 | label.text = self._text 454 | label.clip = (self.x_screen, self.y_screen, self.width, self.height) 455 | 456 | if self._text_size is None: 457 | # Do not populate the label.text_size property to avoid it being leveraged and scaled twice 458 | pass 459 | else: 460 | # Send the original programmer's overriding value and let it be leveraged and scaled by BL_UI_Label class 461 | label.text_size = text_size 462 | 463 | label.text_kerning = text_kerning 464 | label.shadow_size = shadow_size 465 | label.shadow_offset_x = shadow_offset_x 466 | label.shadow_offset_y = shadow_offset_y 467 | label.shadow_color = shadow_color 468 | label.shadow_alpha = shadow_alpha 469 | 470 | if self._is_enabled: 471 | label.text_color = text_color 472 | else: 473 | # Take the text color and "dark" it by either 40% or 20% 474 | label.text_color = self.shade_color(text_color, (0.4 if text_color[0] < 0.5 else 0.2)) 475 | 476 | label.context_it(self.context) 477 | label.draw() 478 | 479 | if self._style in {'NUMBER_CLICK', 'NUMBER_SLIDE', 'TEXTBOX'}: 480 | pass 481 | else: 482 | textpos_y = textpos_y - middle_gap - int(round(normal1 + 0.499)) 483 | 484 | if self._textwo != "": 485 | if self._alignment == 'RIGHT' or (self._text != "" and (self._style in {'NUMBER_CLICK', 'NUMBER_SLIDE'})): 486 | textpos_x = self.x_screen + int((self.width - (length2 / over_scale)) - self._text_margin) 487 | elif self._alignment == 'LEFT': 488 | textpos_x = self.x_screen + self._text_margin 489 | else: 490 | textpos_x = self.x_screen + int((self.width - (length2 / over_scale)) / 2.0) 491 | 492 | label = BL_UI_Label(textpos_x, textpos_y, length2, height2) 493 | label.style = 'BUTTON' 494 | label.text = self._textwo 495 | label.clip = (self.x_screen, self.y_screen, self.width, self.height) 496 | 497 | if self._textwo_size is None: 498 | # Do not populate the label.text_size property to avoid it being leveraged and scaled twice 499 | pass 500 | else: 501 | # Send the original programmer's overriding value and let it be leveraged and scaled by BL_UI_Label class 502 | label.text_size = textwo_size 503 | 504 | label.text_kerning = text_kerning 505 | label.shadow_size = shadow_size 506 | label.shadow_offset_x = shadow_offset_x 507 | label.shadow_offset_y = shadow_offset_y 508 | label.shadow_color = shadow_color 509 | label.shadow_alpha = shadow_alpha 510 | 511 | if self._is_enabled: 512 | label.text_color = textwo_color 513 | else: 514 | # Take the textwo color and "dark" it by either 40% or 20% 515 | label.text_color = self.shade_color(textwo_color, (0.4 if textwo_color[0] < 0.5 else 0.2)) 516 | 517 | label.context_it(self.context) 518 | label.draw() 519 | 520 | # Overrides base class function 521 | def mouse_down(self, event, x, y): 522 | if self.is_in_rect(x, y): 523 | # When button is disabled, just ignore the click 524 | if not self._is_enabled: 525 | # Consume the mouse event to avoid the camera/target be unselected 526 | return True 527 | # Down state 528 | self.__state = 1 529 | return (self.mouse_down_func(self, event, x, y) == True) 530 | else: 531 | return False 532 | 533 | # Overrides base class function 534 | def mouse_move(self, event, x, y): 535 | if self.is_in_rect(x, y): 536 | # When button is disabled, just ignore the hover 537 | if not self._is_enabled: 538 | return False 539 | # When button is pressed, just ignore the hover 540 | if self.__state == 3: 541 | return False 542 | if self.__state != 1: 543 | # Hover state 544 | self.__state = 2 545 | else: 546 | if self.__state == 2: 547 | # Up state 548 | self.__state = 0 549 | return False 550 | 551 | # Overrides base class function 552 | def mouse_up(self, event, x, y): 553 | result = False 554 | if self.is_in_rect(x, y): 555 | # When button is disabled, just ignore the click 556 | if not self._is_enabled: 557 | # Consume the mouse event to avoid the camera/target be unselected 558 | return True 559 | if self.__state == 1 or self.__state == 5: # states 1 and 5 are equivalent for 'SLIDER' widget 560 | result = (self.mouse_up_func(self, event, x, y) == True) 561 | if self.button_pressed_func(self): 562 | # Pressed state 563 | self.__state = 3 564 | else: 565 | # Up state 566 | self.__state = 0 567 | return result 568 | 569 | # Overrides base class function 570 | def mouse_up_over(self): 571 | if self.button_pressed_func(self): 572 | # Pressed state 573 | self.__state = 3 574 | else: 575 | # Up state 576 | self.__state = 0 577 | -------------------------------------------------------------------------------- /bl_ui_widgets/bl_ui_checkbox.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 | # --- ### Header 20 | bl_info = {"name": "BL UI Widgets", 21 | "description": "UI Widgets to draw in the 3D view", 22 | "author": "Marcelo M. Marques (fork of Jayanam's original project)", 23 | "version": (1, 0, 1), 24 | "blender": (2, 80, 75), 25 | "location": "View3D > viewport area", 26 | "support": "COMMUNITY", 27 | "category": "3D View", 28 | "warning": "Version numbering diverges from Jayanam's original project", 29 | "doc_url": "https://github.com/mmmrqs/bl_ui_widgets", 30 | "tracker_url": "https://github.com/mmmrqs/bl_ui_widgets/issues" 31 | } 32 | 33 | # --- ### Change log 34 | 35 | # v1.0.1 (09.20.2021) - by Marcelo M. Marques 36 | # Chang: just some pep8 code formatting 37 | 38 | # v1.0.0 (09.01.2021) - by Marcelo M. Marques 39 | # Added: Logic to scale the checkbox according to both Blender's ui scale configuration and this addon 'preferences' setup 40 | # Added: 'text_highlight' property to allow different text color on the selected checkbox. 41 | # Added: 'outline_color' property to allow different color on the checkbox outline (value is standard color tuple). 42 | # Added: 'mark_color' property to allow different color on the check mark tick. 43 | # Added: 'roundness' property to allow the checkbox to be painted with rounded corners, 44 | # same as that property available in Blender's user themes and it works together with 'rounded_corners' below. 45 | # Added: 'corner_radius' property to allow a limit for the roundness curvature, more useful when 'roundness' property 46 | # is not overriden by programmer and the one from Blender's user themes is used instead. 47 | # Added: 'rounded_corners' property and coding to allow the checkbox to be painted with rounded corners (value is a 4 elements tuple). 48 | # Each elements is a boolean value (0 or 1) which indicates whether the corresponding corner is to be rounded or straight 49 | # in the following clockwise sequence: bottom left, top left, top right, bottom right. 50 | # Added: 'shadow' property and coding to allow the checkbox to be painted with a shadow (value is boolean). 51 | # Added: Logic to allow a checkbox to be disabled (darkned out) and turned off to user interaction. 52 | # Added: 'set_value_changed' function to allow assignment of an external function to be called by mouse_down function. 53 | # Added: Shadow and Kerning related properties that allow the text to be painted using these characteristics. 54 | # Added: Size, Shadow and Kerning attributes default to values retrieved from user theme (may be overriden by programmer). 55 | # Chang: Design of the checkmark changed from 'cross' to a 'tick' symbol. 56 | # Chang: Made it a subclass of 'BL_UI_Patch' instead of 'BL_UI_Widget' so that it can inherit the layout features from there. 57 | # Chang: Instead of hardcoded logic it is now leveraging 'BL_UI_Label' to paint the checkbox text. 58 | # Chang: Mouse over detection changed to cover the area that includes the checkbox label text. 59 | # Fixed: New call to verify_screen_position() so that object behaves alright when viewport is resized. 60 | 61 | # --- ### Imports 62 | import bpy 63 | import gpu 64 | import bgl 65 | import blf 66 | 67 | from gpu_extras.batch import batch_for_shader 68 | 69 | from . bl_ui_patch import BL_UI_Patch 70 | from . bl_ui_label import BL_UI_Label 71 | 72 | 73 | class BL_UI_Checkbox(BL_UI_Patch): 74 | 75 | def __init__(self, x, y, width, height): 76 | 77 | width = 15 # <-- Fixed-size attempt to match Blender's ui at 1.0 resolution scale 78 | height = width 79 | 80 | super().__init__(x, y, width, height) 81 | 82 | self._text = "Checkbox" # Checkbox text 83 | self._text_color = None # Checkbox text color 84 | self._text_highlight = None # Checkbox high color 85 | self._mark_color = None # Checkmark color 86 | 87 | self._style = 'CHECKBOX' # Checkbox style indicator (fixed value) 88 | self._bg_color = None # Checkbox face color (when pressed state == 0) 89 | self._selected_color = None # Checkbox face color (when pressed state == 3) 90 | self._outline_color = None # Checkbox outline color 91 | self._roundness = None # Checkbox corners roundness factor [0..1] 92 | self._radius = round((width - 1) / 2) - 1 # Checkbox corners circular radius (adjusted for better fitting) 93 | self._rounded_corners = (1, 1, 1, 1) # 1=Round/0=Straight, coords:(bottomLeft,topLeft,topRight,bottomRight) 94 | self._has_shadow = True # Indicates whether a shadow must be drawn around the Checkbox 95 | 96 | self._text_size = None # Checkbox text size 97 | self._text_kerning = None # Checkbox text kerning (True/False) 98 | self._text_shadow_size = None # Checkbox text shadow size 99 | self._text_shadow_offset_x = None # Checkbox text shadow offset x (positive goes right) 100 | self._text_shadow_offset_y = None # Checkbox text shadow offset y (negative goes down) 101 | self._text_shadow_color = None # Checkbox text shadow color [0..1] = gray tone, from dark to clear 102 | self._text_shadow_alpha = None # Checkbox text shadow alpha value [0..1] 103 | 104 | self.__state = 0 # 0 is UP; 1 is Selected; 2 is Hover when not selected 105 | self.__label_width = 0 # Additional information to compute the full widget area 106 | 107 | @property 108 | def text(self): 109 | return self._text 110 | 111 | @text.setter 112 | def text(self, value): 113 | self._text = value 114 | 115 | @property 116 | def text_color(self): 117 | return self._text_color 118 | 119 | @text_color.setter 120 | def text_color(self, value): 121 | self._text_color = value 122 | 123 | @property 124 | def text_highlight(self): 125 | return self._text_highlight 126 | 127 | @text_highlight.setter 128 | def text_highlight(self, value): 129 | self._text_highlight = value 130 | 131 | @property 132 | def mark_color(self): 133 | return self._mark_color 134 | 135 | @mark_color.setter 136 | def mark_color(self, value): 137 | self._mark_color = value 138 | 139 | @property 140 | def selected_color(self): 141 | return self._selected_color 142 | 143 | @selected_color.setter 144 | def selected_color(self, value): 145 | self._selected_color = value 146 | 147 | @property 148 | def text_size(self): 149 | return self._text_size 150 | 151 | @text_size.setter 152 | def text_size(self, value): 153 | self._text_size = value 154 | 155 | @property 156 | def text_kerning(self): 157 | return self._text_kerning 158 | 159 | @text_kerning.setter 160 | def text_kerning(self, value): 161 | self._text_kerning = value 162 | 163 | @property 164 | def text_shadow_size(self): 165 | return self._text_shadow_size 166 | 167 | @text_shadow_size.setter 168 | def text_shadow_size(self, value): 169 | self._text_shadow_size = value 170 | 171 | @property 172 | def text_shadow_offset_x(self): 173 | return self._text_shadow_offset_x 174 | 175 | @text_shadow_offset_x.setter 176 | def text_shadow_offset_x(self, value): 177 | self._text_shadow_offset_x = value 178 | 179 | @property 180 | def text_shadow_offset_y(self): 181 | return self._text_shadow_offset_y 182 | 183 | @text_shadow_offset_y.setter 184 | def text_shadow_offset_y(self, value): 185 | self._text_shadow_offset_y = value 186 | 187 | @property 188 | def text_shadow_color(self): 189 | return self._text_shadow_color 190 | 191 | @text_shadow_color.setter 192 | def text_shadow_color(self, value): 193 | self._text_shadow_color = value 194 | 195 | @property 196 | def text_shadow_alpha(self): 197 | return self._text_shadow_alpha 198 | 199 | @text_shadow_alpha.setter 200 | def text_shadow_alpha(self, value): 201 | self._text_shadow_alpha = value 202 | 203 | @property 204 | def is_checked(self): 205 | return (self.__state == 1) 206 | 207 | @is_checked.setter 208 | def is_checked(self, value): 209 | self.__state = 1 if value else 0 210 | 211 | def set_value_changed(self, value_changed_func): 212 | self.value_changed_func = value_changed_func 213 | 214 | def value_changed_func(self, widget, event, x, y): 215 | # This must return False when function is not overriden, so that checkbox 216 | # turns into pressed mode everytime the user clicks over it. 217 | return True 218 | 219 | # Overrides base class function 220 | def is_in_rect(self, x, y): 221 | extra_width = self.width + self.__label_width # Extended width to include label size 222 | widget_x = self.over_scale(self.x_screen) 223 | widget_y = self.over_scale(self.y_screen) 224 | if ( 225 | (widget_x <= x <= self.over_scale(self.x_screen + extra_width)) and 226 | (widget_y >= y >= self.over_scale(self.y_screen - self.height)) 227 | ): 228 | return True 229 | 230 | return False 231 | 232 | # Overrides base class function 233 | def set_colors(self): 234 | # Up 235 | if self.__state == 0: 236 | if self._bg_color is None: 237 | theme = bpy.context.preferences.themes[0] 238 | widget_style = getattr(theme.user_interface, "wcol_option") 239 | color = widget_style.inner 240 | else: 241 | color = self._bg_color 242 | # Down 243 | elif self.__state == 1: 244 | if self._selected_color is None: 245 | theme = bpy.context.preferences.themes[0] 246 | widget_style = getattr(theme.user_interface, "wcol_option") 247 | color = widget_style.inner_sel 248 | else: 249 | color = self._selected_color 250 | # Hover 251 | elif self.__state == 2: 252 | if self._bg_color is None: 253 | theme = bpy.context.preferences.themes[0] 254 | widget_style = getattr(theme.user_interface, "wcol_option") 255 | color = widget_style.inner 256 | else: 257 | color = self._bg_color 258 | # Take the "state 0" background color and "tint" it by either 20% or 10% 259 | color = self.tint_color(color, (0.2 if color[0] < 0.5 else 0.1)) 260 | 261 | if not self._is_enabled: 262 | # Take the resulting state background color and "dark" it by either 40% or 20% 263 | color = self.shade_color(color, (0.4 if color[0] > 0.5 else 0.2)) 264 | 265 | self.shader.uniform_float("color", color) 266 | 267 | # Overrides base class function 268 | def draw(self): 269 | 270 | super().draw() 271 | 272 | if not self._is_visible: 273 | return 274 | 275 | if self.__state != 1: 276 | return None 277 | 278 | if self._mark_color is None: 279 | theme = bpy.context.preferences.themes[0] 280 | widget_style = getattr(theme.user_interface, "wcol_option") 281 | color = widget_style.item 282 | else: 283 | color = self._mark_color 284 | 285 | if not self._is_enabled: 286 | # Take the checkmark color and "dark" it by either 40% or 20% 287 | color = self.shade_color(color, (0.4 if color[0] > 0.5 else 0.2)) 288 | 289 | self.shader_mark = gpu.shader.from_builtin('2D_UNIFORM_COLOR') 290 | 291 | self.shader_mark.bind() 292 | self.shader_mark.uniform_float("color", color) 293 | 294 | # Applying UI Scale: 295 | x_screen = self.over_scale(self.x_screen) 296 | y_screen = self.over_scale(self.y_screen) 297 | width = self.over_scale(self.width) 298 | height = self.over_scale(self.height) 299 | 300 | vertices = ((x_screen + (3 / self.width * width), y_screen - (7 / self.height * height)), 301 | (x_screen + (6 / self.width * width), y_screen - (10 / self.height * height)), 302 | (x_screen + (11 / self.width * width), y_screen - (3 / self.height * height))) 303 | 304 | self.batch_mark = batch_for_shader(self.shader_mark, 'LINE_STRIP', {"pos": vertices}) 305 | 306 | bgl.glEnable(bgl.GL_BLEND) 307 | bgl.glEnable(bgl.GL_LINE_SMOOTH) 308 | 309 | bgl.glLineWidth(self.over_scale(1.5)) 310 | self.batch_mark.draw(self.shader_mark) 311 | 312 | bgl.glDisable(bgl.GL_LINE_SMOOTH) 313 | bgl.glDisable(bgl.GL_BLEND) 314 | 315 | # Overrides base class function 316 | def draw_text(self): 317 | if not (self._is_visible and self._text != ""): 318 | return 319 | 320 | theme = bpy.context.preferences.themes[0] 321 | widget_style = getattr(theme.user_interface, "wcol_option") 322 | 323 | if self.__state == 0: 324 | text_color = tuple(widget_style.text) + (1.0,) if self._text_color is None else self._text_color 325 | elif self.__state == 1: 326 | text_color = tuple(widget_style.text_sel) + (1.0,) if self._text_highlight is None else self._text_highlight 327 | elif self.__state == 2: 328 | text_color = tuple(widget_style.text) + (1.0,) if self._text_color is None else self._text_color 329 | # Take the "state 0" text color and "tint" it by either 20% or 10% 330 | text_color = self.tint_color(text_color, (0.2 if text_color[0] < 0.5 else 0.1)) 331 | 332 | theme = bpy.context.preferences.ui_styles[0] 333 | widget_style = getattr(theme, "widget") 334 | 335 | if self._text_size is None: 336 | text_size = widget_style.points 337 | leveraged_text_size = text_size 338 | else: 339 | text_size = self._text_size 340 | leveraged_text_size = self.leverage_text_size(text_size, "widget") 341 | scaled_size = int(self.over_scale(leveraged_text_size)) 342 | 343 | if bpy.app.version >= (3, 0, 0): # 3.00 issue: 'font_kerning_style' has become extinct 344 | text_kerning = False 345 | else: 346 | text_kerning = (widget_style.font_kerning_style == 'FITTED') if self._text_kerning is None else self._text_kerning 347 | if text_kerning: 348 | blf.enable(0, blf.KERNING_DEFAULT) 349 | 350 | rounded_scale = int(round(self.over_scale(1))) 351 | margin_space = (" " * rounded_scale) if rounded_scale > 0 else " " 352 | spaced_text = margin_space + self._text + margin_space 353 | 354 | blf.size(0, leveraged_text_size, 72) 355 | normal = blf.dimensions(0, "W")[1] # This is to keep a regular pattern since letters differ in height 356 | 357 | blf.size(0, scaled_size, 72) 358 | length = blf.dimensions(0, spaced_text)[0] 359 | height = blf.dimensions(0, "W")[1] 360 | 361 | self.__label_width = length 362 | 363 | if text_kerning: 364 | blf.disable(0, blf.KERNING_DEFAULT) 365 | 366 | textpos_x = self.x_screen + self.width 367 | 368 | top_margin = int((self.height - int(round(normal + 0.499))) / 2.0) 369 | 370 | textpos_y = self.y_screen - top_margin - int(round(normal + 0.499)) + 1 371 | 372 | label = BL_UI_Label(textpos_x, textpos_y, length, height) 373 | label.style = 'CHECKBOX' 374 | 375 | label.text = spaced_text 376 | label.text_kerning = text_kerning 377 | 378 | if self._text_size is None: 379 | # Do not populate the text_size property to avoid it being leveraged and scaled twice 380 | pass 381 | else: 382 | # Send the original programmer's overriding value and let it be leveraged and scaled by BL_UI_Label class 383 | label.text_size = text_size 384 | 385 | label.shadow_size = widget_style.shadow if self._text_shadow_size is None else self._text_shadow_size 386 | label.shadow_offset_x = widget_style.shadow_offset_x if self._text_shadow_offset_x is None else self._text_shadow_offset_x 387 | label.shadow_offset_y = widget_style.shadow_offset_y if self._text_shadow_offset_y is None else self._text_shadow_offset_y 388 | label.shadow_color = widget_style.shadow_value if self._text_shadow_color is None else self._text_shadow_color 389 | label.shadow_alpha = widget_style.shadow_alpha if self._text_shadow_alpha is None else self._text_shadow_alpha 390 | 391 | if self._is_enabled: 392 | label.text_color = text_color 393 | else: 394 | if text_color[0] > 0.5: 395 | # Take the text color and "dark" it by 30% 396 | label.text_color = self.shade_color(text_color, 0.3) 397 | else: 398 | # Take the text color and "tint" it by 30% 399 | label.text_color = self.tint_color(text_color, 0.3) 400 | 401 | label.context_it(self.context) 402 | label.draw() 403 | 404 | # Overrides base class function 405 | def mouse_down(self, event, x, y): 406 | if self.is_in_rect(x, y): 407 | # When checkbox is disabled, just ignore the click 408 | if not self._is_enabled: 409 | # Consume the mouse event to avoid the camera/target be unselected 410 | return True 411 | if not self.value_changed_func(self, event, x, y): 412 | # Consume the mouse event to avoid the camera/target be unselected 413 | return True 414 | # Invert state 415 | if self.__state != 1: 416 | # Marked state 417 | self.__state = 1 418 | else: 419 | # Hover state 420 | self.__state = 2 421 | return True 422 | else: 423 | return False 424 | 425 | # Overrides base class function 426 | def mouse_move(self, event, x, y): 427 | if self.is_in_rect(x, y): 428 | # When checkbox is disabled, just ignore the hover 429 | if not self._is_enabled: 430 | return False 431 | # When checkbox is marked, just ignore the hover 432 | if self.__state != 1: 433 | # Hover state 434 | self.__state = 2 435 | else: 436 | if self.__state != 1: 437 | # Up state 438 | self.__state = 0 439 | return False 440 | 441 | # Overrides base class function 442 | def mouse_up(self, event, x, y): 443 | if self.is_in_rect(x, y): 444 | # When checkbox is disabled, just ignore the click 445 | if not self._is_enabled: 446 | return True 447 | else: 448 | if self.__state != 1: 449 | # Hover state 450 | self.__state = 2 451 | return False 452 | -------------------------------------------------------------------------------- /bl_ui_widgets/bl_ui_drag_panel.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 | # --- ### Header 20 | bl_info = {"name": "BL UI Widgets", 21 | "description": "UI Widgets to draw in the 3D view", 22 | "author": "Marcelo M. Marques (fork of Jayanam's original project)", 23 | "version": (1, 0, 2), 24 | "blender": (2, 80, 75), 25 | "location": "View3D > viewport area", 26 | "support": "COMMUNITY", 27 | "category": "3D View", 28 | "warning": "Version numbering diverges from Jayanam's original project", 29 | "doc_url": "https://github.com/mmmrqs/bl_ui_widgets", 30 | "tracker_url": "https://github.com/mmmrqs/bl_ui_widgets/issues" 31 | } 32 | 33 | # --- ### Change log 34 | 35 | # v1.0.2 (09.25.2022) - by Marcelo M. Marques 36 | # Added: 'quadview' property to indicate whether panel is opened in the QuadView mode or not 37 | # Chang: Logic to save panel screen position only when not in QuadView mode. In some future release this may get improved 38 | # to save the position in both cases, but to distinct sets of variables in the session's saved data dictionary 39 | 40 | # v1.0.1 (09.20.2021) - by Marcelo M. Marques 41 | # Chang: just some pep8 code formatting 42 | 43 | # v1.0.0 (09.01.2021) - by Marcelo M. Marques 44 | # Added: A control to check if the panel can be dragged by the user or must stay locked in position 45 | # Added: Logic to save/restore panel position from last session, or from last use (depending on addon 'preferences' setup) 46 | # Added: Logic to scale the panel according to both Blender's ui scale configuration and this addon 'preferences' setup 47 | # Added: 'style' property which automatically sets the visual according to Blender's user themes. 48 | # Added: 'outline_color' property to allow different color on the panel outline (value is standard color tuple). 49 | # Added: 'roundness' property to allow the panel to be painted with rounded corners, 50 | # same as that property available in Blender's user themes and it works together with 'rounded_corners' below. 51 | # Added: 'corner_radius' property to allow a limit for the roundness curvature, more useful when 'roundness' property 52 | # is not overriden by programmer and the one from Blender's user themes is used instead. 53 | # Added: 'rounded_corners' property to allow the panel to be painted with rounded corners (value is a 4 elements tuple). 54 | # Each elements is a boolean value (0 or 1) which indicates whether the corresponding corner is to be rounded or straight 55 | # in the following clockwise sequence: bottom left, top left, top right, bottom right. 56 | # Added: 'shadow' property to allow the panel to be painted with a shadow (value is boolean). 57 | # Chang: Made it a subclass of 'BL_UI_Patch' instead of 'BL_UI_Widget' so that it can inherit the layout features from there. 58 | # Chang: Renamed some local variables so that those become restricted to this class only. 59 | 60 | # --- ### Imports 61 | import bpy 62 | 63 | from . bl_ui_patch import BL_UI_Patch 64 | 65 | 66 | class BL_UI_Drag_Panel(BL_UI_Patch): 67 | 68 | def __init__(self, x, y, width, height): 69 | 70 | try: 71 | if __package__.find(".") != -1: 72 | package = __package__[0:__package__.find(".")] 73 | else: 74 | package = __package__ 75 | RC_POSITION = bpy.context.preferences.addons[package].preferences.RC_POSITION 76 | RC_POS_X = bpy.context.preferences.addons[package].preferences.RC_POS_X 77 | RC_POS_Y = bpy.context.preferences.addons[package].preferences.RC_POS_Y 78 | except Exception as e: 79 | RC_POSITION = False 80 | 81 | if RC_POSITION: 82 | if RC_POS_X != -10000 and RC_POS_Y != -10000: 83 | # Override input values with the ones saved from last time (any scene/session) 84 | x = RC_POS_X 85 | y = RC_POS_Y 86 | else: 87 | if bpy.context.scene.get("bl_ui_panel_saved_data") is None: 88 | pass 89 | else: 90 | # Override input values with the ones saved from last session 91 | x = bpy.context.scene.get("bl_ui_panel_saved_data")["panX"] 92 | y = bpy.context.scene.get("bl_ui_panel_saved_data")["panY"] 93 | 94 | # Need to apply scale to compensate for posterior calculations 95 | x = (x / self.over_scale(1)) 96 | y = (y / self.over_scale(1)) 97 | 98 | super().__init__(x, y, width, height) 99 | 100 | self.widgets = [] 101 | 102 | # Note: '_style' value will always be ignored if the bg_color value is overriden after object initialization. 103 | 104 | self._style = 'NONE' # Panel background color styles are: {HEADER,PANEL,SUBPANEL,TOOLTIP,NONE} 105 | self._bg_color = None # Panel background color (defaults to invisible) 106 | self._outline_color = None # Panel outline color (defaults to invisible) 107 | self._roundness = 0 # Panel corners roundness factor [0..1] 108 | self._radius = 0 # Panel corners circular radius 109 | self._rounded_corners = (0, 0, 0, 0) # 1=Round/0=Straight, coords:(bottomLeft,topLeft,topRight,bottomRight) 110 | self._has_shadow = False # Indicates whether a shadow must be drawn around the panel 111 | 112 | self._anchored = False # Indicates whether panel can be dragged around the viewport or not 113 | self._quadview = False # Indicates whether panel is opened in the QuadView mode or not 114 | 115 | self.__drag_offset_x = 0 116 | self.__drag_offset_y = 0 117 | self.__is_drag = False 118 | 119 | @property 120 | def anchored(self): 121 | return self._anchored 122 | 123 | @anchored.setter 124 | def anchored(self, value): 125 | self._anchored = value 126 | 127 | @property 128 | def quadview(self): 129 | return self._quadview 130 | 131 | @quadview.setter 132 | def quadview(self, value): 133 | self._quadview = value 134 | 135 | def add_widget(self, widget): 136 | self.widgets.append(widget) 137 | 138 | def add_widgets(self, widgets): 139 | for widget in widgets: 140 | self.add_widget(widget) 141 | 142 | def layout_widgets(self): 143 | for widget in self.widgets: 144 | widget.update(self.x_screen + widget.x, self.y_screen - widget.y) 145 | 146 | def child_widget_focused(self, x, y): 147 | for widget in self.widgets: 148 | if widget.visible: 149 | if widget.is_in_rect(x, y): 150 | return True 151 | return False 152 | 153 | def save_panel_coords(self, x, y): 154 | # Update the new coord values in the session's saved data dictionary, only when not in QuadView mode 155 | if self.quadview: 156 | return None 157 | # Note: Because of the scaling logic it was necessary to make this weird correction math below 158 | new_x = self.over_scale(x) 159 | new_y = self.over_scale(y) 160 | bpy.context.scene["bl_ui_panel_saved_data"] = {"panX": new_x, "panY": new_y} 161 | try: 162 | # Update values also in the add-on's preferences properties 163 | if __package__.find(".") != -1: 164 | package = __package__[0:__package__.find(".")] 165 | else: 166 | package = __package__ 167 | bpy.context.preferences.addons[package].preferences.RC_POS_X = new_x 168 | bpy.context.preferences.addons[package].preferences.RC_POS_Y = new_y 169 | except Exception as e: 170 | pass 171 | 172 | # Overrides base class function 173 | def update(self, x, y): 174 | super().update(x, y) 175 | if self.__is_drag: 176 | # Inform that widget has shift position so that tooltip know it must be recalculated 177 | base_class = super().__thisclass__.__mro__[-2] # This stunt only to avoid hard coding the Base class name 178 | widget = base_class.g_tooltip_widget 179 | if widget is None: 180 | pass 181 | else: 182 | widget.tooltip_moved = True 183 | 184 | # Overrides base class function 185 | def set_location(self, x, y): 186 | super().set_location(x, y) 187 | self.layout_widgets() 188 | 189 | # Overrides base class function 190 | def mouse_down(self, event, x, y): 191 | if self.anchored: 192 | # Means the panel is not draggable 193 | return False 194 | if self.is_in_rect(x, y): 195 | if self.child_widget_focused(x, y): 196 | # Means the focus is on some sub-widget (e.g.: a button) 197 | return False 198 | # When panel is disabled, just ignore the click 199 | if self._is_enabled: 200 | height = self.get_area_height() 201 | self.__is_drag = True 202 | self.__drag_offset_x = x - self.x_screen 203 | self.__drag_offset_y = y - self.y_screen 204 | return True # <-- Perhaps should only return 'True' if self._is_enabled (TBD) 205 | else: 206 | return False 207 | 208 | # Overrides base class function 209 | def mouse_move(self, event, x, y): 210 | if self.__is_drag: 211 | # Recalculate and update the new position on the viewport 212 | new_x = x - self.__drag_offset_x 213 | new_y = y - self.__drag_offset_y 214 | self.save_panel_coords(new_x, new_y) 215 | self.update(new_x, new_y) 216 | self.layout_widgets() 217 | return False 218 | 219 | # Overrides base class function 220 | def mouse_up(self, event, x, y): 221 | self.__is_drag = False 222 | self.__drag_offset_x = 0 223 | self.__drag_offset_y = 0 224 | return False 225 | -------------------------------------------------------------------------------- /bl_ui_widgets/bl_ui_draw_op.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 | # --- ### Header 20 | bl_info = {"name": "BL UI Widgets", 21 | "description": "UI Widgets to draw in the 3D view", 22 | "author": "Marcelo M. Marques (fork of Jayanam's original project)", 23 | "version": (1, 0, 3), 24 | "blender": (2, 80, 75), 25 | "location": "View3D > viewport area", 26 | "support": "COMMUNITY", 27 | "category": "3D View", 28 | "warning": "Version numbering diverges from Jayanam's original project", 29 | "doc_url": "https://github.com/mmmrqs/bl_ui_widgets", 30 | "tracker_url": "https://github.com/mmmrqs/bl_ui_widgets/issues" 31 | } 32 | 33 | # --- ### Change log 34 | 35 | # v1.0.3 (09.25.2021) - by Marcelo M. Marques 36 | # Added: Many improvements to help with identifying when the panel must be automatically terminated either due to a change 37 | # in region or for any other issues. These changes may help with making the code more stable and reliable. 38 | 39 | # v1.0.2 (10.31.2021) - by Marcelo M. Marques 40 | # Added: 'region_pointer' class level property to indicate the region in which the drag_panel operator instance has been invoked(). 41 | # Added: 'valid_modes' property to indicate the 'bpy.context.mode' valid values for displaying the panel. 42 | # Added: 'valid_scenario' function to validate whether the events must be handled by the widgets during a modal pass. 43 | # Added: 'get_region_pointer' function to retrieve the value of the 'region_pointer' class level property. 44 | # Added: 'get_quadview_index' function to retrieve the region under which the mouse is hovering or being clicked. 45 | # Added: 'get_3d_area_and_region' function to retrieve the correct area and region (because those are not guaranteed to remain 46 | # the same after maximizing/restoring screen areas). 47 | # Added: 'valid_display_mode' function to determine whether the user has moved out of the valid area/region. 48 | # Added: 'suppress_rendering' function that can be overriden by programmer in the subclass to control render bypass of the panel widget. 49 | # Added: Logic to the 'invoke' method to avoid "internal error" terminal messages, after maximizing the viewport. 50 | # Chang: How we determine whether the user has moved out of the valid area/region, now using the 'valid_display_mode()' function. 51 | # Chang: Renamed function 'validate()' to 'valid_handler()' for better understanding of its purpose. 52 | 53 | # v1.0.1 (09.20.2021) - by Marcelo M. Marques 54 | # Chang: just some pep8 code formatting 55 | 56 | # v1.0.0 (09.01.2021) - by Marcelo M. Marques 57 | # Added: 'terminate_execution' function that can be overriden by programmer in the subclass to control termination of the panel widget. 58 | # Added: A call to a new 'handle_event_finalize' function in the widgets so that after finishing processing of all the widgets primary 'handle_event' 59 | # function, a final pass is done one more time to wrap up any pending change of state for prior widgets already on the widgets list. Without 60 | # this additional pass it was not possible to make widgets that keep a 'pressed' state in relation to others, to work alright. 61 | # Added: New logic to finish execution of the widget whenever the user moves out of the 3D VIEW display mode (e.g. going into Sculpt editor). 62 | # Added: New logic to only allow paint onto the screen if the user is in the 3D VIEW display mode. 63 | # Added: New logic to detect when drawback handler gets lost (e.g. after opening other blender file) so that it can finish the operator without crashing. 64 | # Chang: Disabled code that finished execution by pressing the ESC key, since the addon has control to finish it by a 'terminate_execution' function. 65 | # Chang: Renamed some local variables so that those become restricted to this class only. 66 | 67 | # --- ### Imports 68 | import bpy 69 | import sys 70 | 71 | from bpy.types import Operator 72 | 73 | 74 | class BL_UI_OT_draw_operator(Operator): 75 | bl_idname = "object.bl_ui_ot_draw_operator" 76 | bl_label = "bl ui widgets operator" 77 | bl_description = "Operator for bl ui widgets" 78 | bl_options = {'REGISTER'} 79 | 80 | handlers = [] 81 | region_pointer = 0 # Uniquely identifies the region that this (drag_panel) operator instance has been invoked() 82 | 83 | def __init__(self): 84 | self.widgets = [] 85 | self.valid_modes = [] 86 | # self.__draw_handle = None # <-- Was like this before I had implemented the 'lost handler detection logic' 87 | # self.__draw_events = None # <--(ditto) 88 | self.__finished = False 89 | self.__informed = False 90 | 91 | @classmethod 92 | def valid_handler(cls): 93 | """ A draw callback belonging to the space is persistent when another file is opened, whereas a modal operator is not. 94 | Solution below removes the draw callback if the operator becomes invalid. The RNA is how Blender objects store their 95 | properties under the hood. When the instance of the Blender operator is no longer required its RNA is trashed. 96 | Using 'repr()' avoids using a try catch clause. Would be keen to find out if there is a nicer way to check for this. 97 | """ 98 | invalids = [(type, op, context, handler) for type, op, context, handler in cls.handlers if repr(op).endswith("invalid>")] 99 | valid = not(invalids) 100 | while invalids: 101 | type, op, context, handler = invalids.pop() 102 | if type == 'H': 103 | bpy.types.SpaceView3D.draw_handler_remove(handler, 'WINDOW') 104 | if type == 'T': 105 | context.window_manager.event_timer_remove(handler) 106 | cls.handlers.remove((type, op, context, handler)) 107 | return valid 108 | 109 | def get_region_pointer(self): 110 | return BL_UI_OT_draw_operator.region_pointer 111 | 112 | def init_widgets(self, context, widgets, valid_modes): 113 | self.widgets = widgets 114 | for widget in self.widgets: 115 | widget.init(context, valid_modes) 116 | 117 | def on_invoke(self, context, event): 118 | pass 119 | 120 | def on_finish(self, context): 121 | self.__finished = True 122 | 123 | def invoke(self, context, event): 124 | # Avoid "internal error: modal gizmo-map handler has invalid area" terminal messages, after maximizing the viewport, 125 | # by switching the workspace back and forth. Not pretty, but at least it avoids the terminal output getting spammed. 126 | current = context.workspace 127 | others = [ws for ws in bpy.data.workspaces if ws != current] 128 | if others: 129 | bpy.context.window.workspace = others[0] 130 | bpy.context.window.workspace = current 131 | # ----------------------------------------------------------------- 132 | BL_UI_OT_draw_operator.region_pointer = context.region.as_pointer() 133 | # ----------------------------------------------------------------- 134 | self.on_invoke(context, event) 135 | args = (self, context) 136 | self.register_handlers(args, context) 137 | context.window_manager.modal_handler_add(self) 138 | return {'RUNNING_MODAL'} 139 | 140 | def register_handlers(self, args, context): 141 | BL_UI_OT_draw_operator.handlers = [] 142 | BL_UI_OT_draw_operator.handlers.append(('H', self, context, bpy.types.SpaceView3D.draw_handler_add(self.draw_callback_px, args, 'WINDOW', 'POST_PIXEL'))) 143 | BL_UI_OT_draw_operator.handlers.append(('T', self, context, context.window_manager.event_timer_add(0.1, window=context.window))) 144 | # Was as below before implementing the 'lost handler detection logic' 145 | # self.__draw_handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback_px, args, "WINDOW", "POST_PIXEL") 146 | # self.__draw_events = context.window_manager.event_timer_add(0.1, window=context.window) 147 | 148 | def unregister_handlers(self, context): 149 | for handler in BL_UI_OT_draw_operator.handlers: 150 | if handler[0] == 'H': 151 | bpy.types.SpaceView3D.draw_handler_remove(handler[3], 'WINDOW') 152 | if handler[0] == 'T': 153 | context.window_manager.event_timer_remove(handler[3]) 154 | BL_UI_OT_draw_operator.handlers = [] 155 | # Was as below before implementing the 'lost handler detection logic' 156 | # context.window_manager.event_timer_remove(self.__draw_events) 157 | # bpy.types.SpaceView3D.draw_handler_remove(self.__draw_handle, "WINDOW") 158 | # self.__draw_handle = None 159 | # self.__draw_events = None 160 | 161 | def modal(self, context, event): 162 | if self.__finished: 163 | return {'FINISHED'} 164 | 165 | valid, area, region = self.valid_scenario(context, event) 166 | if area: 167 | area.tag_redraw() 168 | if valid: 169 | if self.handle_widget_events(event, area, region): 170 | return {'RUNNING_MODAL'} 171 | 172 | # Not using any escape option, but left it here for documentation purposes 173 | # if event.type in {"ESC"}: 174 | # self.finish() 175 | 176 | return {'PASS_THROUGH'} 177 | 178 | def valid_scenario(self, context, event): 179 | valid = True 180 | area, region, abend = get_3d_area_and_region() 181 | if abend: 182 | self.finish() 183 | valid = False 184 | elif not (area and region): 185 | if self.terminate_execution(area, region, event): 186 | self.finish() 187 | # Return but do not finish 188 | valid = False 189 | elif self.terminate_execution(area, region, event): 190 | area.tag_redraw() 191 | self.finish() 192 | valid = False 193 | elif event.type != 'TIMER': 194 | # Check whether it is drawing on the same region where the panel was initially opened 195 | mouse_region, abend = get_region(context, event.mouse_x, event.mouse_y) 196 | if abend: 197 | self.finish() 198 | valid = False 199 | else: 200 | if mouse_region is None or mouse_region.as_pointer() != self.get_region_pointer(): 201 | # Not the same region, so skip handling events at this time, but do not finish 202 | valid = False 203 | return (valid, area, region) 204 | 205 | def handle_widget_events(self, event, area, region): 206 | # Consider not a valid display mode when the overridable custom function 207 | # returns True (meaning that it wants to suppress the rendering anyway). 208 | if event.type != 'TIMER': 209 | if self.suppress_rendering(area, region): 210 | return False 211 | 212 | result = False 213 | for widget in self.widgets: 214 | if widget.visible or event.type == 'TIMER': 215 | if widget.handle_event(event): 216 | result = True 217 | break 218 | if event.type != 'TIMER': 219 | for widget in self.widgets: 220 | if widget.visible: 221 | # Need to pass one more time to wrap up any pending change of state for widgets on the widgets list 222 | widget.handle_event_finalize(event) 223 | return result 224 | 225 | def suppress_rendering(self, area, region): 226 | # This might be overriden by one same named function in the derived (child) class 227 | return False 228 | 229 | def terminate_execution(self, area, region, event): 230 | # This might be overriden by one same named function in the derived (child) class 231 | return False 232 | 233 | def finish(self): 234 | # -- personalized criteria for the Remote Control panel addon -- 235 | # This is a temporary workaround till I figure out how to signal to 236 | # the N-panel coding that the remote control panel has been finished. 237 | bpy.context.scene.var.RemoVisible = False 238 | bpy.context.scene.var.btnRemoText = "Open Remote Control" 239 | # -- end of the personalized criteria for the given addon -- 240 | 241 | self.unregister_handlers(bpy.context) 242 | self.on_finish(bpy.context) 243 | 244 | def cancel(self, context): 245 | # Called when Blender cancels the modal operator 246 | self.finish() 247 | 248 | # Draw handler to paint onto the screen 249 | def draw_callback_px(self, op, context): 250 | # Check whether handles are still valid 251 | if not BL_UI_OT_draw_operator.valid_handler(): 252 | try: 253 | # -- personalized criteria for the Remote Control panel addon -- 254 | # This is a temporary workaround till I figure out how to signal to 255 | # the N-panel coding that the remote control panel has been finished. 256 | bpy.context.scene.var.RemoVisible = False 257 | bpy.context.scene.var.btnRemoText = "Open Remote Control" 258 | # -- end of the personalized criteria for the given addon -- 259 | except: 260 | pass 261 | return 262 | # Check whether it is drawing on the same region where the panel was initially opened 263 | for region in [region for region in context.area.regions if region.type == 'WINDOW']: 264 | if context.region == region: 265 | if region.as_pointer() != self.get_region_pointer(): 266 | # Not the same region, so skip drawing there 267 | return 268 | break 269 | # This is to detect when user moved into an undesired 'bpy.context.mode' 270 | # and it will check also the programmer's defined suppress_rendering function 271 | if valid_display_mode(self.valid_modes, self.suppress_rendering): 272 | for widget in self.widgets: 273 | widget.draw() 274 | 275 | 276 | # --- ### Helper functions 277 | 278 | def get_region(context, x, y): 279 | abend = False 280 | try: 281 | for area in [area for area in context.screen.areas if area.type == 'VIEW_3D']: 282 | for region in [region for region in area.regions if region.type == 'WINDOW']: 283 | if (x >= region.x and 284 | y >= region.y and 285 | x < region.width + region.x and 286 | y < region.height + region.y): 287 | return (region, abend) 288 | except Exception as e: 289 | if __package__.find(".") != -1: 290 | package = __package__[0:__package__.find(".")] 291 | else: 292 | package = __package__ 293 | print("**WARNING** " + package + " addon issue:") 294 | print(" +--> unexpected result in 'get_region' function of bl_ui_draw_op.py module!") 295 | print(" " + e) 296 | abend = True 297 | return (None, abend) 298 | 299 | def get_3d_area_and_region(prefs=None): 300 | abend = False 301 | try: 302 | # Left this commented code for a while until I make sure it will not be needed. 303 | # Case we want to put this back, it will need to import parameter 'idx', and in 304 | # the calling module the 'idx' value must be set as follows: 305 | # --------------------------------------------------------------------- 306 | # idx = bpy.context.window_manager.windows[:].index(bpy.context.window) 307 | # --------------------------------------------------------------------- 308 | # 309 | # if bpy.app.version >= (2, 90, 0): 310 | # areas = bpy.context.window.screen.areas 311 | # else: 312 | # areas = bpy.context.window_manager.windows[idx].screen.areas 313 | # 314 | # if prefs: 315 | # location = bpy.data.screens['Layout'].areas 316 | # else: 317 | # location = bpy.context.window.screen.areas 318 | # for area in location: 319 | # if area.type == 'VIEW_3D': 320 | # for region in area.regions: 321 | # if region.type == 'WINDOW': 322 | # if region.as_pointer() == BL_UI_OT_draw_operator.region_pointer: 323 | # return (area, region, abend) 324 | # 325 | for screen in bpy.data.screens: 326 | for area in [area for area in screen.areas if area.type == 'VIEW_3D']: 327 | for region in [region for region in area.regions if region.type == 'WINDOW']: 328 | if region.as_pointer() == BL_UI_OT_draw_operator.region_pointer: 329 | return (area, region, abend) 330 | except Exception as e: 331 | if __package__.find(".") != -1: 332 | package = __package__[0:__package__.find(".")] 333 | else: 334 | package = __package__ 335 | print("**WARNING** " + package + " addon issue:") 336 | print(" +--> unexpected result in 'get_3d_area_and_region' function of bl_ui_draw_op.py module!") 337 | print(" " + e) 338 | abend = True 339 | return (None, None, abend) 340 | 341 | 342 | def valid_display_mode(valid_modes, suppress_rendering=None): 343 | if valid_modes: 344 | if bpy.context.mode not in valid_modes: 345 | return False 346 | 347 | area, region, abend = get_3d_area_and_region() 348 | if abend or not area or not region: 349 | return False 350 | else: 351 | if suppress_rendering is not None: 352 | # Consider not a valid display mode when the overridable custom function 353 | # returns True (meaning that it wants to suppress the rendering anyway). 354 | if suppress_rendering(area, region): 355 | return False 356 | return True 357 | -------------------------------------------------------------------------------- /bl_ui_widgets/bl_ui_label.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 | # --- ### Header 20 | bl_info = {"name": "BL UI Widgets", 21 | "description": "UI Widgets to draw in the 3D view", 22 | "author": "Marcelo M. Marques (fork of Jayanam's original project)", 23 | "version": (1, 0, 1), 24 | "blender": (2, 80, 75), 25 | "location": "View3D > viewport area", 26 | "support": "COMMUNITY", 27 | "category": "3D View", 28 | "warning": "Version numbering diverges from Jayanam's original project", 29 | "doc_url": "https://github.com/mmmrqs/bl_ui_widgets", 30 | "tracker_url": "https://github.com/mmmrqs/bl_ui_widgets/issues" 31 | } 32 | 33 | # --- ### Change log 34 | 35 | # v1.0.1 (09.20.2021) - by Marcelo M. Marques 36 | # Chang: just some pep8 code formatting 37 | 38 | # v1.0.0 (09.01.2021) - by Marcelo M. Marques 39 | # Added: Logic to scale the label's text according to both Blender's ui scale configuration and this addon 'preferences' setup 40 | # Added: 'style' property that allows the text to take different style colors per Blender's user themes. 41 | # Added: 'text_title' property that allows the highlight color to be overriden by code. 42 | # Added: 'text_kerning' property that allows the text kerning to be adjusted accordingly. 43 | # Added: 'text_rotation' property that allows the text to be painted in any direction (value must be in radians). 44 | # Added: 'clip' property that allows the text to be clipped (value must be a 4_coordinates tuple) 45 | # Added: Shadow and Kerning related properties that allow the text to be painted using these characteristics. 46 | # Added: Colors, Size, Shadow and Kerning attributes default to values retrieved from user theme (can be overriden). 47 | # Fixed: New call to verify_screen_position() so that object behaves alright when viewport is resized. 48 | 49 | # --- ### Imports 50 | import bpy 51 | import blf 52 | 53 | from . bl_ui_widget import BL_UI_Widget 54 | 55 | 56 | class BL_UI_Label(BL_UI_Widget): 57 | 58 | def __init__(self, x, y, width, height): 59 | super().__init__(x, y, width, height) 60 | 61 | self._text = "Label" 62 | self._style = 'REGULAR' # Label color style options are: {REGULAR,TITLE,BOX,BUTTON,CHECKBOX,TOOLTIP} 63 | self._text_color = None # Label normal color 64 | self._text_title = None # Label titles color 65 | 66 | self._text_size = None # Label size in points (pixels) 67 | self._text_kerning = None # Label kerning (True/False) 68 | self._text_rotation = 0.0 # Angle value in radians (90 is vertical) 69 | 70 | self._shadow_size = None # Label shadow size 71 | self._shadow_offset_x = None # Label shadow offset x (positive goes right) 72 | self._shadow_offset_y = None # Label shadow offset y (negative goes down) 73 | self._shadow_color = None # Label shadow color [0..1] = gray tone, from dark to clear 74 | self._shadow_alpha = None # Label shadow alpha value [0..1] 75 | 76 | self._clip = None # Clipping coordinates (populated only by BL_UI_Button class) 77 | 78 | @property 79 | def text(self): 80 | return self._text 81 | 82 | @text.setter 83 | def text(self, value): 84 | self._text = value 85 | 86 | @property 87 | def text_color(self): 88 | return self._text_color 89 | 90 | @text_color.setter 91 | def text_color(self, value): 92 | self._text_color = value 93 | 94 | @property 95 | def text_title(self): 96 | return self._text_title 97 | 98 | @text_title.setter 99 | def text_title(self, value): 100 | self._text_title = value 101 | 102 | @property 103 | def text_size(self): 104 | return self._text_size 105 | 106 | @text_size.setter 107 | def text_size(self, value): 108 | self._text_size = value 109 | 110 | @property 111 | def text_kerning(self): 112 | return self._text_kerning 113 | 114 | @text_kerning.setter 115 | def text_kerning(self, value): 116 | self._text_kerning = value 117 | 118 | @property 119 | def text_rotation(self): 120 | return self._text_rotation 121 | 122 | @text_rotation.setter 123 | def text_rotation(self, value): 124 | self._text_rotation = value 125 | 126 | @property 127 | def shadow_size(self): 128 | return self._shadow_size 129 | 130 | @shadow_size.setter 131 | def shadow_size(self, value): 132 | self._shadow_size = value 133 | 134 | @property 135 | def shadow_offset_x(self): 136 | return self._shadow_offset_x 137 | 138 | @shadow_offset_x.setter 139 | def shadow_offset_x(self, value): 140 | self._shadow_offset_x = value 141 | 142 | @property 143 | def shadow_offset_y(self): 144 | return self._shadow_offset_y 145 | 146 | @shadow_offset_y.setter 147 | def shadow_offset_y(self, value): 148 | self._shadow_offset_y = value 149 | 150 | @property 151 | def shadow_color(self): 152 | return self._shadow_color 153 | 154 | @shadow_color.setter 155 | def shadow_color(self, value): 156 | self._shadow_color = value 157 | 158 | @property 159 | def shadow_alpha(self): 160 | return self._shadow_alpha 161 | 162 | @shadow_alpha.setter 163 | def shadow_alpha(self, value): 164 | self._shadow_alpha = value 165 | 166 | @property 167 | def clip(self): 168 | return self._clip 169 | 170 | @clip.setter 171 | def clip(self, value): 172 | self._clip = value 173 | 174 | # Overrides base class function 175 | def my_style(self): 176 | if self._style == 'TITLE': 177 | style = "panel_title" 178 | elif self._style == 'REGULAR' or self._style == 'BOX': 179 | style = "widget_label" 180 | elif self._style == 'BUTTON' or self._style == 'CHECKBOX' or self._style == 'TOOLTIP': 181 | style = "widget" 182 | else: 183 | style = "widget_label" 184 | return style 185 | 186 | # Overrides base class function 187 | def is_in_rect(self, x, y): 188 | # This type of object must not react to mouse events 189 | return False 190 | 191 | # Overrides base class function 192 | def update(self, x, y): 193 | self.x_screen = x 194 | self.y_screen = y 195 | 196 | # Overrides base class function 197 | def draw(self): 198 | if not self._is_visible: 199 | area_height = self.get_area_height() 200 | self.verify_screen_position(area_height) 201 | return 202 | 203 | if self._style == 'REGULAR' or self._style == 'TOOLTIP': 204 | if self._text_color is None: 205 | # From Preferences/Themes/3D Viewport/"Theme Space" 206 | theme = bpy.context.preferences.themes[0] 207 | widget_style = getattr(theme.view_3d, "space") 208 | text_color = tuple(widget_style.button_text) + (1.0,) 209 | else: 210 | text_color = self._text_color 211 | 212 | elif self._style == 'TITLE': 213 | if self._text_title is None: 214 | # From Preferences/Themes/3D Viewport/"Theme Space" 215 | theme = bpy.context.preferences.themes[0] 216 | widget_style = getattr(theme.view_3d, "space") 217 | text_color = tuple(widget_style.button_title) + (1.0,) 218 | else: 219 | text_color = self._text_title 220 | 221 | elif self._style == 'BOX': 222 | if self._text_color is None: 223 | # From Preferences/Themes/User Interface/"Box" 224 | theme = bpy.context.preferences.themes[0] 225 | widget_style = getattr(theme.user_interface, "wcol_box") 226 | text_color = tuple(widget_style.text) + (1.0,) 227 | else: 228 | text_color = self._text_color 229 | else: 230 | if self._text_color is None: 231 | # Warning error out color :-) 232 | text_color = (1, 0, 0, 1) 233 | else: 234 | text_color = self._text_color 235 | 236 | if not self._is_enabled: 237 | if text_color[0] > 0.5: 238 | # Take the text color and "dark" it by 30% 239 | text_color = self.shade_color(text_color, 0.3) 240 | else: 241 | # Take the text color and "tint" it by 30% 242 | text_color = self.tint_color(text_color, 0.3) 243 | 244 | theme = bpy.context.preferences.ui_styles[0] 245 | widget_style = getattr(theme, self.my_style()) 246 | if self._text_size is None: 247 | text_size = widget_style.points 248 | else: 249 | text_size = self.leverage_text_size(self._text_size, self.my_style()) 250 | 251 | if self._style == 'TOOLTIP': 252 | text_size = int(self.ui_scale(text_size)) 253 | else: 254 | text_size = int(self.over_scale(text_size)) 255 | 256 | if bpy.app.version >= (3, 0, 0): # 3.00 issue: 'font_kerning_style' has become extinct 257 | text_kerning = True if self._text_kerning is None else self._text_kerning 258 | else: 259 | text_kerning = (widget_style.font_kerning_style == 'FITTED') if self._text_kerning is None else self._text_kerning 260 | 261 | shadow_size = widget_style.shadow if self._shadow_size is None else self._shadow_size 262 | shadow_offset_x = widget_style.shadow_offset_x if self._shadow_offset_x is None else self._shadow_offset_x 263 | shadow_offset_y = widget_style.shadow_offset_y if self._shadow_offset_y is None else self._shadow_offset_y 264 | shadow_color = widget_style.shadow_value if self._shadow_color is None else self._shadow_color 265 | shadow_alpha = widget_style.shadow_alpha if self._shadow_alpha is None else self._shadow_alpha 266 | 267 | # These few statements below to fix the shadow size into a valid value otherwise blf() errors out 268 | if shadow_size == 0: 269 | pass 270 | else: 271 | if shadow_size < 0: 272 | shadow_size = 0 273 | else: 274 | shadow_size = 3 if (shadow_size < 3) else shadow_size 275 | shadow_size = 5 if (shadow_size > 3) else shadow_size 276 | 277 | # DOCUMENTATION: 278 | # blf.shadow(fontid, level, r, g, b, a) 279 | # Shadow options, enable/disable using SHADOW 280 | # Parameters: 281 | # fontid (int) – The id of the typeface as returned by blf.load(), for default font use 0. 282 | # level (int) – The blur level, can be 3, 5 or 0. 283 | # r (float) – Shadow color (red channel 0.0 - 1.0) 284 | # g (float) – Shadow color (green channel 0.0 - 1.0) 285 | # b (float) – Shadow color (blue channel 0.0 - 1.0) 286 | # a (float) – Shadow color (alpha channel 0.0 - 1.0) 287 | 288 | # blf.shadow_offset(fontid, x, y) 289 | # Set the offset for shadow text. 290 | # Parameters: 291 | # fontid (int) – The id of the typeface as returned by blf.load(), for default font use 0. 292 | # x (float) – Horizontal shadow offset value in pixels (positive is right, negative is left) 293 | # y (float) – Vertical shadow offset value in pixels (positive is up, negative is down) 294 | 295 | if bpy.app.version >= (3, 0, 0): # 3.00 issue: 'font_kerning_style' has become extinct 296 | text_kerning = False 297 | 298 | if shadow_size: 299 | blf.enable(0, blf.SHADOW) 300 | blf.shadow(0, shadow_size, shadow_color, shadow_color, shadow_color, shadow_alpha) 301 | blf.shadow_offset(0, shadow_offset_x, shadow_offset_y) 302 | if self._text_rotation: 303 | blf.enable(0, blf.ROTATION) 304 | blf.rotation(0, self._text_rotation) 305 | if text_kerning: 306 | blf.enable(0, blf.KERNING_DEFAULT) 307 | if self._clip is not None: 308 | blf.enable(0, blf.CLIPPING) 309 | x_min = self.over_scale(self._clip[0]) 310 | y_max = self.over_scale(self._clip[1]) 311 | x_max = x_min + self.over_scale(self._clip[2]) 312 | y_min = y_max - self.over_scale(self._clip[3]) 313 | blf.clipping(0, x_min, y_min, x_max, y_max) 314 | 315 | area_height = self.get_area_height() 316 | 317 | self.verify_screen_position(area_height) 318 | 319 | blf.size(0, text_size, 72) 320 | 321 | blf.position(0, self.over_scale(self.x_screen), self.over_scale(self.y_screen), 0) 322 | 323 | r, g, b, a = text_color 324 | 325 | blf.color(0, r, g, b, a) 326 | 327 | blf.draw(0, self._text) 328 | 329 | if shadow_size: 330 | blf.disable(0, blf.SHADOW) 331 | if self._text_rotation: 332 | blf.disable(0, blf.ROTATION) 333 | if text_kerning: 334 | blf.disable(0, blf.KERNING_DEFAULT) 335 | if self._clip is not None: 336 | blf.disable(0, blf.CLIPPING) 337 | -------------------------------------------------------------------------------- /bl_ui_widgets/bl_ui_patch.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 | # --- ### Header 20 | bl_info = {"name": "BL UI Widgets", 21 | "description": "UI Widgets to draw in the 3D view", 22 | "author": "Marcelo M. Marques", 23 | "version": (1, 0, 1), 24 | "blender": (2, 80, 75), 25 | "location": "View3D > viewport area", 26 | "support": "COMMUNITY", 27 | "category": "3D View", 28 | "warning": "", 29 | "doc_url": "https://github.com/mmmrqs/bl_ui_widgets", 30 | "tracker_url": "https://github.com/mmmrqs/bl_ui_widgets/issues" 31 | } 32 | 33 | # --- ### Change log 34 | 35 | # v1.0.1 (09.20.2021) - by Marcelo M. Marques 36 | # Chang: just some pep8 code formatting 37 | 38 | # v1.0.0 (09.01.2021) - by Marcelo M. Marques 39 | # Added: initial creation 40 | # Added: This new class to paint custom rectangles on screen. Useful for creating header and subpanel areas. 41 | # It is used as a base class for the following widgets: 42 | # {'BL_UI_Drag_Panel','BL_UI_Button','BL_UI_Slider','BL_UI_Checkbox','BL_UI_Tooltip'} 43 | 44 | # --- ### Imports 45 | import bpy 46 | import time 47 | 48 | from . bl_ui_widget import BL_UI_Widget 49 | 50 | 51 | class BL_UI_Patch(BL_UI_Widget): 52 | 53 | def __init__(self, x, y, width, height): 54 | super().__init__(x, y, width, height) 55 | 56 | # Note: '_style' value will always be ignored if the bg_color value is overriden after object initialization. 57 | 58 | self._style = 'NONE' # Patch background color styles are: {HEADER,PANEL,SUBPANEL,BOX,TOOLTIP,NONE} 59 | self._bg_color = None # Patch background color (defaults to invisible) 60 | self._shadow_color = None # Panel shadow color (defaults to invisible) 61 | self._outline_color = None # Panel outline color (defaults to invisible) 62 | self._roundness = 0 # Patch corners roundness factor [0..1] 63 | self._radius = 0 # Patch corners circular radius 64 | self._rounded_corners = (0, 0, 0, 0) # 1=Round/0=Straight, coords:(bottomLeft,topLeft,topRight,bottomRight) 65 | self._has_shadow = False # Indicates whether a shadow must be drawn around the patch 66 | 67 | self._image = None # Image file to be loaded 68 | self._image_size = (24, 24) # Image size in pixels; values are (width, height) 69 | self._image_position = (4, 2) # Image position inside the patch area; values are (x, y) 70 | 71 | self.__image_file = None 72 | self.__image_time = 0 73 | 74 | @property 75 | def bg_color(self): 76 | return self._bg_color 77 | 78 | @bg_color.setter 79 | def bg_color(self, value): 80 | self._bg_color = value 81 | 82 | @property 83 | def shadow_color(self): 84 | return self._shadow_color 85 | 86 | @shadow_color.setter 87 | def shadow_color(self, value): 88 | self._shadow_color = value 89 | 90 | @property 91 | def outline_color(self): 92 | return self._outline_color 93 | 94 | @outline_color.setter 95 | def outline_color(self, value): 96 | self._outline_color = value 97 | 98 | @property 99 | def roundness(self): 100 | return self._roundness 101 | 102 | @roundness.setter 103 | def roundness(self, value): 104 | if value is None: 105 | self._roundness = None 106 | elif value < 0: 107 | self._roundness = 0.0 108 | elif value > 1: 109 | self._roundness = 1.0 110 | else: 111 | self._roundness = value 112 | 113 | @property 114 | def corner_radius(self): 115 | return self._radius 116 | 117 | @corner_radius.setter 118 | def corner_radius(self, value): 119 | self._radius = value 120 | 121 | @property 122 | def rounded_corners(self): 123 | return self._rounded_corners 124 | 125 | @rounded_corners.setter 126 | def rounded_corners(self, value): 127 | self._rounded_corners = value 128 | 129 | @property 130 | def shadow(self): 131 | return self._has_shadow 132 | 133 | @shadow.setter 134 | def shadow(self, value): 135 | self._has_shadow = value 136 | 137 | def set_image_size(self, image_size): 138 | self._image_size = image_size 139 | 140 | def set_image_position(self, image_position): 141 | self._image_position = image_position 142 | 143 | def set_image(self, rel_filepath): 144 | self.__image_file = rel_filepath 145 | self.__image_time = time.time() 146 | try: 147 | self._image = bpy.data.images.load(self.__image_file, check_existing=True) 148 | self._image.gl_load() 149 | self._image.pack(as_png=True) 150 | except Exception as e: 151 | pass 152 | 153 | # Overrides base class function 154 | def is_in_rect(self, x, y): 155 | """ 156 | The statement with super() is equivalent to writing either one of the following, 157 | but with the advantage of not having the class name hard coded. 158 | - if type(self).__name__ == "BL_UI_Patch": 159 | - if type(self) is BL_UI_Patch: 160 | """ 161 | # This distincts whether it is the Base or a Derived class 162 | if super().__self_class__ is super().__thisclass__: 163 | # This object type must not react to mouse events 164 | return False 165 | else: 166 | return super().is_in_rect(x, y) 167 | 168 | # Overrides base class function 169 | def draw(self): 170 | 171 | super().draw() 172 | 173 | if not self._is_visible: 174 | return 175 | 176 | # Attempt to refresh the image because it has an issue that causes it to black out after a while 177 | if self._image is not None: 178 | if time.time() - self.__image_time >= 10: 179 | self.set_image(self.__image_file) 180 | -------------------------------------------------------------------------------- /bl_ui_widgets/bl_ui_tooltip.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 | # --- ### Header 20 | bl_info = {"name": "BL UI Widgets", 21 | "description": "UI Widgets to draw in the 3D view", 22 | "author": "Marcelo M. Marques", 23 | "version": (1, 0, 1), 24 | "blender": (2, 80, 75), 25 | "location": "View3D > viewport area", 26 | "support": "COMMUNITY", 27 | "category": "3D View", 28 | "warning": "", 29 | "doc_url": "https://github.com/mmmrqs/bl_ui_widgets", 30 | "tracker_url": "https://github.com/mmmrqs/bl_ui_widgets/issues" 31 | } 32 | 33 | # --- ### Change log 34 | 35 | # v1.0.1 (09.20.2021) - by Marcelo M. Marques 36 | # Chang: just some pep8 code formatting 37 | 38 | # v1.0.0 (09.01.2021) - by Marcelo M. Marques 39 | # Added: initial creation 40 | # Added: This new class to display tooltips for each widget. If design properties are not overriden by programmer 41 | # then those will be inherited from Blender's user themes. 42 | 43 | # --- ### Imports 44 | import bpy 45 | import blf 46 | 47 | from . bl_ui_patch import BL_UI_Patch 48 | from . bl_ui_label import BL_UI_Label 49 | 50 | 51 | class BL_UI_Tooltip(BL_UI_Patch): 52 | 53 | def __init__(self): 54 | super().__init__(0, 0, 0, 0) # These arguments have no use in the case of a tooltip object 55 | 56 | self._text_color = None # Tooltip text color 57 | self._shortcut_color = (185/255, 185/255, 185/255, 1) # Tooltip shortcut color medium gray (seems to be fixed) 58 | self._python_color = (122/255, 122/255, 122/255, 1) # Tooltip python cmd color dark gray (seems to be fixed) 59 | 60 | # Note: '_style' value will always be ignored if the bg_color value is overriden after object initialization. 61 | 62 | self._style = 'TOOLTIP' # Tooltip background color styles are: {HEADER,PANEL,SUBPANEL,TOOLTIP,NONE} 63 | self._bg_color = None # Tooltip background color (defaults to 'TOOLTIP') 64 | self._outline_color = None # Tooltip outline color (defaults to 'TOOLTIP') 65 | self._roundness = None # Tooltip corners roundness factor [0..1] 66 | self._radius = 8.5 # Tooltip corners circular radius 67 | self._rounded_corners = (1, 1, 1, 1) # 1=Round/0=Straight, coords:(bottomLeft,topLeft,topRight,bottomRight) 68 | self._has_shadow = True # Indicates whether a shadow must be drawn around the tooltip 69 | 70 | self._text_size = None # Tooltip text line 1 size 71 | self._text_kerning = None # Tooltip text kerning (True/False) 72 | self._text_shadow_size = None # Tooltip text shadow size 73 | self._text_shadow_offset_x = None # Tooltip text shadow offset x (positive goes right) 74 | self._text_shadow_offset_y = None # Tooltip text shadow offset y (negative goes down) 75 | self._text_shadow_color = None # Tooltip text shadow color [0..1] = gray tone, from dark to clear 76 | self._text_shadow_alpha = None # Tooltip text shadow alpha value [0..1] 77 | 78 | self._is_tooltip = True # Indicates that object generated by this class is a Tooltip type 79 | 80 | self.__over_scale = 0 # Saves the latest scaling value 81 | self.__area_height = 0 # Saves the latest area height value 82 | self.__area_width = 0 # Saves the latest area width value 83 | 84 | self.__tooltip_widget = None # Identifies the widget object to have tooltip displayed 85 | self.__tooltip_textsize = None # Text size used on drawing the tooltip last time 86 | self.__tooltip_textkern = None # Text kerning used on drawing the tooltip last time 87 | self.__tooltip_showpyth = None # Former setup for displaying python commands 88 | self.__tooltip_text_lines = [] # Tooltip text lines to be prepared by internal function 89 | self.__tooltip_shortcut_lines = [] # Tooltip shortcut text lines to be prepared by internal function 90 | self.__tooltip_python_lines = [] # Tooltip python text lines to be prepared by internal function 91 | 92 | self.__line_space = 6 # Standard vertical spacing between tooltip text lines (units in pixels) 93 | self.__text_margin = 12 # Standard margin size for tooltip text arrangement (units in pixels) 94 | 95 | self.__max_lines_text = 3 # Limit of 3 text lines for the main tooltip 96 | self.__max_lines_shortcut = 1 # Limit of 1 text line for the shortcut 97 | self.__max_lines_python = 2 # Limit of 2 text lines for the python command 98 | self.__max_tooltip_width = 450 # Limit of 450px for the tooltip box width (which is variable) 99 | 100 | @property 101 | def text_color(self): 102 | return self._text_color 103 | 104 | @text_color.setter 105 | def text_color(self, value): 106 | self._text_color = value 107 | 108 | @property 109 | def text_highlight(self): 110 | return self._text_highlight 111 | 112 | @text_highlight.setter 113 | def text_highlight(self, value): 114 | self._text_highlight = value 115 | 116 | @property 117 | def text_size(self): 118 | return self._text_size 119 | 120 | @text_size.setter 121 | def text_size(self, value): 122 | self._text_size = value 123 | 124 | @property 125 | def text_kerning(self): 126 | return self._text_kerning 127 | 128 | @text_kerning.setter 129 | def text_kerning(self, value): 130 | self._text_kerning = value 131 | 132 | @property 133 | def text_shadow_size(self): 134 | return self._text_shadow_size 135 | 136 | @text_shadow_size.setter 137 | def text_shadow_size(self, value): 138 | self._text_shadow_size = value 139 | 140 | @property 141 | def text_shadow_offset_x(self): 142 | return self._text_shadow_offset_x 143 | 144 | @text_shadow_offset_x.setter 145 | def text_shadow_offset_x(self, value): 146 | self._text_shadow_offset_x = value 147 | 148 | @property 149 | def text_shadow_offset_y(self): 150 | return self._text_shadow_offset_y 151 | 152 | @text_shadow_offset_y.setter 153 | def text_shadow_offset_y(self, value): 154 | self._text_shadow_offset_y = value 155 | 156 | @property 157 | def text_shadow_color(self): 158 | return self._text_shadow_color 159 | 160 | @text_shadow_color.setter 161 | def text_shadow_color(self, value): 162 | self._text_shadow_color = value 163 | 164 | @property 165 | def text_shadow_alpha(self): 166 | return self._text_shadow_alpha 167 | 168 | @text_shadow_alpha.setter 169 | def text_shadow_alpha(self, value): 170 | self._text_shadow_alpha = value 171 | 172 | @property 173 | def max_lines_description(self): 174 | return self.__max_lines_text 175 | 176 | @max_lines_description.setter 177 | def max_lines_description(self, value): 178 | self.__max_lines_text = value 179 | 180 | @property 181 | def max_lines_shortcut(self): 182 | return self.__max_lines_shortcut 183 | 184 | @max_lines_shortcut.setter 185 | def max_lines_shortcut(self, value): 186 | self.__max_lines_shortcut = value 187 | 188 | @property 189 | def max_lines_python(self): 190 | return self.__max_lines_python 191 | 192 | @max_lines_python.setter 193 | def max_lines_python(self, value): 194 | self.__max_lines_python = value 195 | 196 | @property 197 | def max_width(self): 198 | return self.__max_tooltip_width 199 | 200 | @max_width.setter 201 | def max_width(self, value): 202 | self.__max_tooltip_width = value 203 | 204 | # Overrides base class function 205 | def is_in_rect(self, x, y): 206 | return False 207 | 208 | def prepare_tooltip_data(self, widget): 209 | base_class = super().__thisclass__.__mro__[-2] # This stunt only to avoid hard coding the Base class name 210 | if base_class.g_tooltip_widget is None: 211 | return False 212 | 213 | # These many checks below is to try saving runtime by skipping unnecessary text processing 214 | if self.__tooltip_widget is not widget: 215 | self.__tooltip_widget = widget 216 | 217 | elif not widget.tooltip_moved: 218 | area_height = self.get_area_height() 219 | if self.__area_height != area_height or self.__over_scale != self.over_scale(1): 220 | self.__area_height = area_height 221 | self.__over_scale = self.over_scale(1) 222 | else: 223 | theme = bpy.context.preferences.ui_styles[0] 224 | widget_style = getattr(theme, "widget") 225 | text_size = widget_style.points if self._text_size is None else self._text_size 226 | if bpy.app.version >= (3, 0, 0): # 3.00 issue: 'font_kerning_style' has become extinct 227 | text_kerning = True if self._text_kerning is None else self._text_kerning 228 | else: 229 | text_kerning = (widget_style.font_kerning_style == 'FITTED') if self._text_kerning is None else self._text_kerning 230 | if self.__tooltip_textsize != text_size or self.__tooltip_textkern != text_kerning: 231 | self.__tooltip_textsize = text_size 232 | self.__tooltip_textkern = text_kerning 233 | else: 234 | prefs = bpy.context.preferences.view 235 | display_python = bpy.context.preferences.view.show_tooltips_python 236 | if self.__tooltip_showpyth != display_python: 237 | self.__tooltip_showpyth = display_python 238 | else: 239 | return False 240 | 241 | measurements = self.get_tooltip_measurements() 242 | self.x = measurements['x'] 243 | self.y = measurements['y'] 244 | self.x_screen = self.x 245 | self.y_screen = self.y 246 | self.width = measurements['width'] 247 | self.height = measurements['height'] 248 | return True 249 | 250 | def get_tooltip_measurements(self): 251 | widget = self.__tooltip_widget 252 | 253 | self.__tooltip_text_lines = [] 254 | self.__tooltip_shortcut_lines = [] 255 | self.__tooltip_python_lines = [] 256 | 257 | if widget.description == "" and widget.shortcut == "" and widget.python_cmd == "": 258 | measurements = {'x': 0, 259 | 'y': 0, 260 | 'width': 0, 261 | 'height': 0, 262 | } 263 | return (measurements) 264 | 265 | if self._text_size is None: 266 | theme = bpy.context.preferences.ui_styles[0] 267 | widget_style = getattr(theme, "widget") 268 | text_size = widget_style.points 269 | else: 270 | text_size = self.leverage_text_size(self._text_size, "widget") 271 | 272 | if bpy.app.version >= (3, 0, 0): # 3.00 issue: 'font_kerning_style' has become extinct 273 | text_kerning = False 274 | else: 275 | text_kerning = (widget_style.font_kerning_style == 'FITTED') if self._text_kerning is None else self._text_kerning 276 | 277 | # Will send these scaled values to text_wrap function to get a more precise measurement for the widest 278 | # text line because the font characters do not scale so well proportionally to the supplied factor. 279 | scaled_text_size = int(self.ui_scale(text_size)) 280 | scaled_max_width = self.ui_scale(self.__max_tooltip_width) 281 | 282 | if text_kerning: 283 | blf.enable(0, blf.KERNING_DEFAULT) 284 | blf.size(0, text_size, 72) 285 | text_normal = blf.dimensions(0, "W")[1] # This is to keep a regular pattern since letters differ in height 286 | blf.size(0, scaled_text_size, 72) 287 | text_height = blf.dimensions(0, "W")[1] 288 | if text_kerning: 289 | blf.disable(0, blf.KERNING_DEFAULT) 290 | 291 | # From Preferences/Interface/"Display" 292 | prefs = bpy.context.preferences.view 293 | display_python = bpy.context.preferences.view.show_tooltips_python 294 | 295 | line_spacing = self.ui_scale(self.__line_space) 296 | 297 | widest_line = 0 298 | total_height = 0 299 | 300 | if widget.description != "": 301 | line_count = 0 302 | self.__tooltip_text_lines = self.text_wrap(widget.description, scaled_text_size, text_kerning, scaled_max_width, self.__max_lines_text) 303 | for line in self.__tooltip_text_lines: 304 | if line_count == self.__max_lines_text: 305 | break 306 | widest_line = line[1][0] if line[1][0] > widest_line else widest_line 307 | total_height += round(text_height + 0.499) + line_spacing 308 | line_count += 1 309 | if widget.shortcut != "": 310 | line_count = 0 311 | self.__tooltip_shortcut_lines = self.text_wrap(widget.shortcut, scaled_text_size, text_kerning, scaled_max_width, self.__max_lines_shortcut) 312 | for line in self.__tooltip_shortcut_lines: 313 | if line_count == self.__max_lines_shortcut: 314 | break 315 | widest_line = line[1][0] if line[1][0] > widest_line else widest_line 316 | total_height += round(text_height + 0.499) + line_spacing 317 | line_count += 1 318 | if widget.python_cmd != "" and display_python: 319 | line_count = 0 320 | self.__tooltip_python_lines = self.text_wrap(widget.python_cmd, scaled_text_size, text_kerning, scaled_max_width, self.__max_lines_python) 321 | for line in self.__tooltip_python_lines: 322 | if line_count == self.__max_lines_python: 323 | break 324 | widest_line = line[1][0] if line[1][0] > widest_line else widest_line 325 | total_height += round(text_height + 0.499) + line_spacing 326 | line_count += 1 327 | 328 | if widget.description != "" and widget.shortcut != "": 329 | total_height += line_spacing 330 | if (widget.description != "" or widget.shortcut != "") and widget.python_cmd != "" and display_python: 331 | total_height += line_spacing 332 | 333 | # Now that we have got the text precisely wrapped, we can un-scale back the numbers before returning 334 | # them to the calling function. This is desired because they will be scaled up again during drawing. 335 | widest_line = round(widest_line + 0.499) / self.ui_scale(1) 336 | total_height = total_height / self.ui_scale(1) 337 | 338 | total_width = widest_line + (3 * self.__text_margin) 339 | total_height = total_height + (2 * self.__text_margin) 340 | 341 | tooltip_x = widget.x_screen - 6 342 | tooltip_y = widget.y_screen - widget.height - 12 343 | 344 | area_width = self.get_area_width() 345 | 346 | # Back off tooltip box placement in the X axis if it does not fit the space between the panel and viewport right border 347 | if self.over_scale(tooltip_x) + self.ui_scale(total_width) > area_width - 50: 348 | tooltip_x = (area_width - self.ui_scale(total_width) - 50) / self.over_scale(1) 349 | 350 | # Invert tooltip box placement in the Y axis if it does not fit the space between the panel and viewport bottom border 351 | if self.over_scale(tooltip_y) - self.ui_scale(total_height) < 12: 352 | tooltip_y = (self.over_scale(widget.y_screen) + self.ui_scale(total_height) + 12) / self.over_scale(1) 353 | 354 | measurements = {'x': tooltip_x, 355 | 'y': tooltip_y, 356 | 'width': total_width, 357 | 'height': total_height, 358 | } 359 | return (measurements) 360 | 361 | def text_wrap(self, text, text_size, text_kerning, max_width_px, max_lines_count): 362 | line_break = "\n" 363 | text = text.rstrip() 364 | 365 | blf.size(0, text_size, 72) 366 | if text_kerning: 367 | blf.enable(0, blf.KERNING_DEFAULT) 368 | 369 | # Note: the '3*self.__text_margin' below came from 'get_tooltip_measurements' function 370 | # where it is added as margins to the total_width of the tooltip box, so it has to be discounted here. 371 | split_point = max_width_px - (3 * self.__text_margin) 372 | 373 | line_array = [] 374 | 375 | cr = len(line_break) 376 | lstrip_it = False 377 | text_length = len(text) 378 | text_line = "" 379 | char_pos = 0 380 | 381 | while char_pos < text_length and len(line_array) < max_lines_count: 382 | next_chars = text[char_pos:(char_pos + cr)] 383 | if next_chars == line_break: 384 | text_line = text_line.lstrip() if lstrip_it else text_line 385 | dimensions = blf.dimensions(0, text_line) 386 | line_array.append((text_line, dimensions)) 387 | char_pos += len(line_break) 388 | lstrip_it = False 389 | text_line = "" 390 | else: 391 | text_line += next_chars[0] 392 | dimensions = blf.dimensions(0, text_line) 393 | if dimensions[0] > split_point: 394 | last_space = text_line.rfind(" ") 395 | if last_space == -1: 396 | # Have to break the one-word-sentence wherever it is 397 | sub_line = text_line[0:(len(text_line) - 1)] 398 | text_line = text_line[-1] 399 | else: 400 | # Cut the sentence at its closest space character 401 | sub_line = text_line[0:last_space] 402 | text_line = text_line[(last_space + 1):] 403 | sub_line = sub_line.lstrip() if lstrip_it else sub_line 404 | dimensions = blf.dimensions(0, sub_line) 405 | line_array.append((sub_line, dimensions)) 406 | lstrip_it = True 407 | char_pos += 1 408 | if char_pos == text_length: 409 | text_line = text_line.lstrip() if lstrip_it else text_line 410 | dimensions = blf.dimensions(0, text_line) 411 | line_array.append((text_line, dimensions)) 412 | break 413 | if text_kerning: 414 | blf.disable(0, blf.KERNING_DEFAULT) 415 | return line_array 416 | 417 | # Overrides base class function 418 | def draw_text(self): 419 | if not self._is_visible: 420 | return 421 | 422 | if len(self.__tooltip_text_lines) == 0: 423 | if len(self.__tooltip_shortcut_lines) == 0 and len(self.__tooltip_python_lines) == 0: 424 | return 425 | else: 426 | if self._text_color is None: 427 | theme = bpy.context.preferences.themes[0] 428 | widget_style = getattr(theme.user_interface, "wcol_tooltip") 429 | text_color = tuple(widget_style.text) + (1.0,) 430 | else: 431 | text_color = self._text_color 432 | 433 | theme = bpy.context.preferences.ui_styles[0] 434 | widget_style = getattr(theme, "widget") 435 | 436 | if self._text_size is None: 437 | text_size = widget_style.points 438 | leveraged_text_size = text_size 439 | else: 440 | text_size = self._text_size 441 | leveraged_text_size = self.leverage_text_size(text_size, "widget") 442 | 443 | if bpy.app.version >= (3, 0, 0): # 3.00 issue: 'font_kerning_style' has become extinct 444 | text_kerning = False 445 | else: 446 | text_kerning = (widget_style.font_kerning_style == 'FITTED') if self._text_kerning is None else self._text_kerning 447 | 448 | # Will send this scaled value to text_wrap function to get a more precise measurement for the widest 449 | # text line because the font characters do not scale so well proportionally to the supplied factor. 450 | scaled_text_size = int(self.ui_scale(leveraged_text_size)) 451 | 452 | if text_kerning: 453 | blf.enable(0, blf.KERNING_DEFAULT) 454 | blf.size(0, leveraged_text_size, 72) 455 | text_normal = blf.dimensions(0, "W")[1] # This is to keep a regular pattern since letters differ in height 456 | blf.size(0, scaled_text_size, 72) 457 | text_height = blf.dimensions(0, "W")[1] # This is to keep a regular pattern since letters differ in height 458 | if text_kerning: 459 | blf.disable(0, blf.KERNING_DEFAULT) 460 | 461 | # Need to unapply the over scale to compensate for posterior calculations. 462 | # This way when BL_UI_Label applies self.over_scale() function to the entire text, 463 | # the result will be that only the scale regarding the ui_scale factor will effect. 464 | over_scale = self.over_scale(1) 465 | textpos_x = self.x + (self.ui_scale(self.__text_margin) / over_scale) - 1 466 | textpos_y = self.y - (self.ui_scale(self.__text_margin + round(text_normal + 0.499)) / over_scale) + 1 467 | 468 | label = BL_UI_Label(textpos_x, textpos_y, self.width, text_height) 469 | label.context_it(self.context) 470 | label.style = 'TOOLTIP' 471 | 472 | if self._text_size is None: 473 | # Do not populate the text_size property to avoid it being leveraged and scaled twice 474 | pass 475 | else: 476 | # Send the original programmer's overriding value and let it be leveraged and scaled by BL_UI_Label class 477 | label.text_size = text_size 478 | 479 | label.text_kerning = text_kerning 480 | label.shadow_size = widget_style.shadow if self._text_shadow_size is None else self._text_shadow_size 481 | label.shadow_offset_x = widget_style.shadow_offset_x if self._text_shadow_offset_x is None else self._text_shadow_offset_x 482 | label.shadow_offset_y = widget_style.shadow_offset_y if self._text_shadow_offset_y is None else self._text_shadow_offset_y 483 | label.shadow_color = widget_style.shadow_value if self._text_shadow_color is None else self._text_shadow_color 484 | label.shadow_alpha = widget_style.shadow_alpha if self._text_shadow_alpha is None else self._text_shadow_alpha 485 | 486 | line_spacing = self.ui_scale(self.__line_space) 487 | line_count = 0 488 | 489 | if len(self.__tooltip_text_lines) > 0: 490 | label.text_color = text_color 491 | for line in self.__tooltip_text_lines: 492 | label.text = line[0] 493 | label.draw() 494 | line_count += 1 495 | # Need to unapply the over scale to compensate for posterior calculations 496 | textpos_y -= (round(text_height + 0.499) + line_spacing) / over_scale 497 | label.y_screen = textpos_y 498 | if line_count >= self.__max_lines_text: 499 | break 500 | 501 | if len(self.__tooltip_shortcut_lines) > 0: 502 | label.text_color = self._shortcut_color 503 | textpos_y -= line_spacing if line_count > 0 else 0 504 | label.y_screen = textpos_y 505 | line_count = 0 506 | for line in self.__tooltip_shortcut_lines: 507 | label.text = line[0] 508 | label.draw() 509 | line_count += 1 510 | # Need to unapply the over scale to compensate for posterior calculations 511 | textpos_y -= (round(text_height + 0.499) + line_spacing) / over_scale 512 | label.y_screen = textpos_y 513 | if line_count >= self.__max_lines_shortcut: 514 | break 515 | 516 | if len(self.__tooltip_python_lines) > 0: 517 | label.text_color = self._python_color 518 | textpos_y -= line_spacing if line_count > 0 else 0 519 | label.y_screen = textpos_y 520 | line_count = 0 521 | for line in self.__tooltip_python_lines: 522 | label.text = line[0] 523 | label.draw() 524 | line_count += 1 525 | # Need to unapply the over scale to compensate for posterior calculations 526 | textpos_y -= (round(text_height + 0.499) + line_spacing) / over_scale 527 | label.y_screen = textpos_y 528 | if line_count >= self.__max_lines_python: 529 | break 530 | 531 | # This piece of logic below would be used to merge/abbreviate the latest line to the last one displayed 532 | # when going over the configured max lines count, however it needed to take into account the actual 533 | # pixel-length of the strings instead of characters count, so it has been left for future development. 534 | 535 | # def abbreviate_text(self, limit_chars, this_line, last_line): 536 | # this_line = this_line.rstrip() 537 | # last_line = last_line.strip() 538 | # last_save = last_line 539 | 540 | # half_size = round(limit_chars / 2.0) - 1 541 | 542 | # if len(last_line) >= half_size: 543 | # last_line = last_line[(len(last_line) - half_size + 2):].lstrip() 544 | 545 | # comb_size = len(this_line) + len(last_line) 546 | 547 | # if comb_size >= limit_chars: 548 | # over_size = comb_size - limit_chars 549 | # this_line = this_line[0:(len(this_line) - over_size - 2)].rstrip() 550 | # else: 551 | # over_size = limit_chars - len(this_line) - 2 552 | # if over_size <= len(last_save): 553 | # last_line = last_save[(len(last_save) - over_size):].lstrip() 554 | 555 | # return (this_line + " ... " + last_line) 556 | -------------------------------------------------------------------------------- /documentation/README.md: -------------------------------------------------------------------------------- 1 | # Some Basic Documentation 2 | 3 | ## Camera Movements Cheat Sheet 4 | ![Standard Camera Movements](https://github.com/mmmrqs/media/blob/main/Camera%20Movements%20Cheat%20Sheet.jpg) 5 | 6 | # Blender UI Widgets 7 | 8 | The Reference Cameras Panel addon uses my fork of **Jayanam's Blender UI Widgets** for displaying a floating 'Remote Control' panel. This is a collection of UI Widgets that allows the creation of addons with a persistent (modal) draggable floating panel, textboxes, checkboxes, buttons and sliders for **Blender 2.8** and newer versions. 9 | 10 | Each widget object has many attributes that can be set by the programmer to customize its appearance and behavior. One can opt to let the widgets automatically take the appearance of the selected Blender's Theme or can override any of those characteristics individually, and per widget. 11 | 12 | The widgets are also fully scalable, bound to Blender's Resolution Scale configuration ("ui_scale") and/or by programmer's customization. It is also ready to get tied to an Addon Preferences setup page, as can be seen in the included **demo panel** (more about that below). 13 | 14 | Not much documentation is available for now, but the code has a lot of annotations to help you out and each module has its mod log listing all added features. Also, at each module's init method you can find all available attributes described with detailed information. 15 | 16 | The GPU module of Blender 2.8 is used for drawing. This package has a demo panel to showcase all available widgets so that you can install it and have a quick testing. It also serves as a template or a baseline for creating **your own addons**. I attempted to add a little bit of each feature to the demo code in order to help starters. You can have it installed by using the alternate **\_\_init\_\_.py** and **prefs.py** files available in the '\_\_init\_\_backups' folder, so check that out please. 17 | 18 | ## Classes relationships for the draggable panel 19 | ![BL_UI_Widgets UML](https://github.com/mmmrqs/media/blob/main/Classes_UML1.png) 20 | 21 | ## Classes relationships for the integrated BL_UI_Widgets 22 | ![BL_UI_Widgets UML](https://github.com/mmmrqs/media/blob/main/Classes_UML2.png) 23 | -------------------------------------------------------------------------------- /img/rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmmrqs/Blender-Reference-Camera-Panel-addon/2a101eee822d1f58eb53173df0d62d3e15d6d1f1/img/rotate.png -------------------------------------------------------------------------------- /img/rotate.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmmrqs/Blender-Reference-Camera-Panel-addon/2a101eee822d1f58eb53173df0d62d3e15d6d1f1/img/rotate.psd -------------------------------------------------------------------------------- /img/scale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmmrqs/Blender-Reference-Camera-Panel-addon/2a101eee822d1f58eb53173df0d62d3e15d6d1f1/img/scale.png -------------------------------------------------------------------------------- /img/scale.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmmrqs/Blender-Reference-Camera-Panel-addon/2a101eee822d1f58eb53173df0d62d3e15d6d1f1/img/scale.psd -------------------------------------------------------------------------------- /img/scale_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmmrqs/Blender-Reference-Camera-Panel-addon/2a101eee822d1f58eb53173df0d62d3e15d6d1f1/img/scale_24.png -------------------------------------------------------------------------------- /prefs.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 | ''' 20 | Reference Cameras add-on 21 | ''' 22 | # --- ### Header 23 | bl_info = {"name": "Reference Cameras Control Panel", 24 | "description": "Handles cameras associated with reference photos", 25 | "author": "Marcelo M. Marques (fork of Witold Jaworski's & Jayanam's projects)", 26 | "version": (1, 0, 3), 27 | "blender": (2, 80, 75), 28 | "location": "View3D > side panel ([N]), [Cameras] tab", 29 | "support": "COMMUNITY", 30 | "category": "3D View", 31 | "warning": "Version numbering diverges from Witold's original project", 32 | "doc_url": "http://airplanes3d.net/scripts-257_e.xml", 33 | "tracker_url": "https://github.com/mmmrqs/Blender-Reference-Camera-Panel-addon/issues" 34 | } 35 | 36 | # --- ### Change log 37 | 38 | # v1.0.3 (09.25.2022) - by Marcelo M. Marques 39 | # Chang: Just small updates in some comments on the code 40 | 41 | # v1.0.2 (10.31.2021) - by Marcelo M. Marques 42 | # Added: New 'RC_BLINK_ALT' property to configure alternative operation mode of the Blink Mesh(es) function 43 | # Added: New 'update_subpanel' helper function to reinitialize the "panel_switch" variables after property's been updated. 44 | # Chang: The logic that retrieves region.width of the 3d view screen which has the Remote Control 45 | 46 | # v1.0.1 (09.20.2021) - by Marcelo M. Marques 47 | # Chang: Just some pep8 code formatting 48 | 49 | # v1.0.0 (09.01.2021) - by Marcelo M. Marques 50 | # Added: Initial creation 51 | 52 | # --- ### Imports 53 | import bpy 54 | 55 | from bpy.types import AddonPreferences, Operator 56 | from bpy.props import StringProperty, IntProperty, BoolProperty, EnumProperty, FloatProperty, FloatVectorProperty 57 | 58 | from .bl_ui_widgets.bl_ui_draw_op import get_3d_area_and_region 59 | 60 | 61 | # --- ### Helper functions 62 | 63 | def update_subpanel(self, context): 64 | # Dynamically updates the number of subpanels on the N-Panel region when the preference is changed 65 | num_panels = bpy.context.preferences.addons[__package__].preferences.RC_SUBPANELS 66 | for i in range(100): 67 | propSubPanel = f"panel_switch_{i+1:03d}" 68 | try: 69 | if hasattr(bpy.types.Scene, propSubPanel): 70 | if i >= num_panels: 71 | #delete leftover subpanel prop from Blender's Scene 72 | delattr(bpy.types.Scene, propSubPanel) 73 | else: 74 | if i < num_panels: 75 | #add missing subpanel prop to Blender's Scene 76 | setattr(bpy.types.Scene, propSubPanel, BoolProperty(name=propSubPanel, default=True, description="Collapse/Expand this subpanel")) 77 | else: 78 | break 79 | except: 80 | pass 81 | 82 | 83 | class ReferenceCameraPreferences(AddonPreferences): 84 | bl_idname = __package__ 85 | 86 | RC_MESHES: StringProperty( 87 | name="", 88 | description="Name (or suffix) for a collection where the work in progress mesh(es) should be placed\nso that the 'switch mesh visibility' feature can be used", 89 | default="RC:WIP" 90 | ) 91 | 92 | RC_CAMERAS: StringProperty( 93 | name="", 94 | description="Name (or suffix) for a collection where to place your reference cameras", 95 | default="RC:Cameras" 96 | ) 97 | 98 | RC_TARGETS: StringProperty( 99 | name="", 100 | description=" Name (or suffix) for a collection where the target objects will be moved upon creation of new camera sets. If left blank targets will be placed in the main camera collection", 101 | default="RC:Targets" 102 | ) 103 | 104 | RC_TEMP: StringProperty( 105 | name="", 106 | description="Name (or suffix) for a 'working' collection for convenient view adjustments of the current camera", 107 | default="RC:Temporary" 108 | ) 109 | 110 | RC_SUBPANELS: IntProperty( 111 | name="", 112 | description="Maximum number of dynamic subpanels for grouping camera selection buttons (when children collections exist under the main camera collection). Set it to zero to not use grouping at all", 113 | default=15, 114 | max=99, 115 | min=0, 116 | soft_max=32, 117 | soft_min=0, 118 | update=update_subpanel 119 | ) 120 | 121 | # items=[identifier, name, description, icon, number] 122 | RC_SUBP_MODE: EnumProperty( 123 | name="N-Panel Layout option", 124 | items=[ 125 | ('COMPACT', "Compact", "Display the 5 main camera modes using large buttons.", '', 0), 126 | ('FULL', "Full", "Display all the 7 camera modes using narrow buttons.", '', 1), 127 | ('EXTENDED', "Extended", "Display all features likewise the 'Remote Control' panel.", '', 2) 128 | ], 129 | default='COMPACT' 130 | ) 131 | 132 | RC_ACTION_MAIN: BoolProperty( 133 | name="Camera Action mode (N-Panel)", 134 | description="If (ON): camera action start when mode button is pressed.\nIf (OFF): just set the adjustment mode but do not start camera action", 135 | default=False 136 | ) 137 | 138 | RC_FOCUS: FloatProperty( 139 | name="", 140 | description="Perspective Camera lens initial value in millimeters", 141 | default=50.0, 142 | min=1.0, 143 | soft_max=5000, 144 | soft_min=1.0, 145 | step=100, 146 | precision=2, 147 | unit='CAMERA', 148 | subtype=('DISTANCE_CAMERA' if bpy.app.version >= (2, 90, 0) else 'DISTANCE') # 2.80 issue: 'DISTANCE_CAMERA' subtype unknown 149 | ) 150 | 151 | RC_SENSOR: FloatProperty( 152 | name="", 153 | description="Perspective Camera sensor width in millimeters", 154 | default=36.0, 155 | min=1.0, 156 | soft_max=100, 157 | soft_min=1.0, 158 | step=100, 159 | precision=2, 160 | unit='CAMERA' 161 | ) 162 | 163 | # items=[identifier, name, description, icon, number] 164 | RC_TRGMODE: EnumProperty( 165 | name="", 166 | items=[ 167 | ('TEXTURED', "Textured", "Display the camera's target object with textures", '', 0), 168 | ('SOLID', "Solid", "Display the camera's target object as a solid", '', 1), 169 | ('WIRE', "Wire", "Display the camera's target object as a wireframe", '', 2), 170 | ('BOUNDS', "Bounds", "Display the bounds of the camera's target object", '', 3) 171 | ], 172 | default='SOLID' 173 | ) 174 | 175 | RC_TRGCOLOR: FloatVectorProperty( 176 | name="", 177 | description="Color and alpha for the camera target object", 178 | default=(1.0, 0.075, 0.0, 1.0), 179 | max=1.0, 180 | min=0.0, 181 | size=4, 182 | subtype='COLOR' 183 | ) 184 | 185 | RC_OPACITY: FloatProperty( 186 | name="", 187 | description="Opacity level for the camera's background image to blend against the viewport's background color", 188 | default=0.5, 189 | max=1.0, 190 | min=0.0, 191 | soft_max=1.0, 192 | soft_min=0.0, 193 | step=1, 194 | precision=3, 195 | unit='NONE', 196 | subtype='FACTOR' 197 | ) 198 | 199 | # items=[identifier, name, description, icon, number] 200 | RC_DEPTH: EnumProperty( 201 | name="Depth option for rendering the camera's background image", 202 | items=[ 203 | ('BACK', "Back", "Display under everything.", '', 0), 204 | ('FRONT', "Front", "Display over everything.", '', 1) 205 | ], 206 | default='FRONT' 207 | ) 208 | 209 | RC_UI_BIND: BoolProperty( 210 | name="General scaling for 'Remote Control' panel", 211 | description="If (ON): remote panel size changes per Blender interface's resolution scale.\nIf (OFF): remote panel size can only change per its own addon scaling factor", 212 | default=True 213 | ) 214 | 215 | RC_SCALE: FloatProperty( 216 | name="", 217 | description="Scaling to be applied on the 'Remote Control' panel over (in addition to) the interface's resolution scale", 218 | default=1.0, 219 | max=2.00, 220 | min=0.50, 221 | soft_max=2.00, 222 | soft_min=0.50, 223 | step=1, 224 | precision=2, 225 | unit='NONE' 226 | ) 227 | 228 | RC_BLINK_ON: FloatProperty( 229 | name="", 230 | description="Time duration for the 'ON' stage of the blinking mesh cycle, in units of 1/10th of a second", 231 | default=0.4, 232 | max=1.0, 233 | min=0.1, 234 | soft_max=1.0, 235 | soft_min=0.1, 236 | step=10, 237 | precision=1, 238 | unit='NONE' 239 | ) 240 | 241 | RC_BLINK_OFF: FloatProperty( 242 | name="", 243 | description="Time duration for the 'OFF' stage of the blinking mesh cycle, in units of 1/10th of a second", 244 | default=0.3, 245 | max=1.0, 246 | min=0.1, 247 | soft_max=1.0, 248 | soft_min=0.1, 249 | step=10, 250 | precision=1, 251 | unit='NONE' 252 | ) 253 | 254 | RC_BLINK_ALT: BoolProperty( 255 | name="Alternative mode that makes the button to operate in a three stages action sequence.\n" + 256 | "First click to start blinking, next click to stop blinking and to leave mesh(es) hidden,\n" + 257 | "next click to finally unhide the mesh(es) and finish the cycle.", 258 | description="If (ON): button works in a three stages sequence (Blink/Hide/Unhide).\nIf (OFF): button works in a two stages sequence (Blink/Stop Blinking)", 259 | default=False 260 | ) 261 | 262 | RC_ACTION_REMO: BoolProperty( 263 | name="Camera Action mode (Remote Control panel)", 264 | description="If (ON): camera action start when mode button is pressed.\nIf (OFF): just set the adjustment mode but do not start camera action", 265 | default=True 266 | ) 267 | 268 | RC_SLIDE: BoolProperty( 269 | name="Keep Remote Control panel pinned when resizing viewport", 270 | description="If (ON): remote panel slides together with viewport's bottom border.\nIf (OFF): remote panel stays in place regardless of viewport resizing", 271 | default=False 272 | ) 273 | 274 | RC_POSITION: BoolProperty( 275 | name="Remote Control panel position per scene", 276 | description="If (ON): remote panel initial position is the same as in the last opened scene.\nIf (OFF): remote panel remembers its position per each scene", 277 | default=False 278 | ) 279 | 280 | RC_POS_X: IntProperty( 281 | name="", 282 | description="Remote Control panel position 'X' from latest opened scene", 283 | default=-10000 284 | ) 285 | 286 | RC_POS_Y: IntProperty( 287 | name="", 288 | description="Remote Control panel position 'Y' from latest opened scene", 289 | default=-10000 290 | ) 291 | 292 | RC_PAN_W: IntProperty( 293 | name="", 294 | description="Panel width saved on 'drag_panel_op.py'" 295 | ) 296 | 297 | RC_PAN_H: IntProperty( 298 | name="", 299 | description="Panel height saved on 'drag_panel_op.py'" 300 | ) 301 | 302 | def ui_scale(self, value): 303 | if bpy.context.preferences.addons[__package__].preferences.RC_UI_BIND: 304 | # From Preferences/Interface/"Display" 305 | return (value * bpy.context.preferences.view.ui_scale) 306 | else: 307 | return (value) 308 | 309 | def over_scale(self, value): 310 | over_scale = bpy.context.preferences.addons[__package__].preferences.RC_SCALE 311 | return (self.ui_scale(value) * over_scale) 312 | 313 | def draw(self, context): 314 | layout = self.layout 315 | 316 | # -- N-Panel configuration 317 | 318 | layout.separator() 319 | layout.label(text=" N-Panel configuration") 320 | 321 | split = layout.split(factor=0.45, align=True) 322 | split.label(text="Meshes Collection name suffix:", icon='DECORATE') 323 | splat = split.split(factor=0.8, align=True) 324 | splat.prop(self, 'RC_MESHES', text="") 325 | 326 | split = layout.split(factor=0.45, align=True) 327 | split.label(text="Cameras Collection name suffix:", icon='DECORATE') 328 | splat = split.split(factor=0.8, align=True) 329 | splat.prop(self, 'RC_CAMERAS', text="") 330 | 331 | split = layout.split(factor=0.45, align=True) 332 | split.label(text="Targets Collection name suffix:", icon='DECORATE') 333 | splat = split.split(factor=0.8, align=True) 334 | splat.prop(self, 'RC_TARGETS', text="") 335 | 336 | split = layout.split(factor=0.45, align=True) 337 | split.label(text="Temporary Collection name suffix:", icon='DECORATE') 338 | splat = split.split(factor=0.8, align=True) 339 | splat.prop(self, 'RC_TEMP', text="") 340 | 341 | split = layout.split(factor=0.45, align=True) 342 | split.label(text="Subpanels max number:", icon='DECORATE') 343 | splat = split.split(factor=0.4, align=True) 344 | splat.prop(self, 'RC_SUBPANELS', text="") 345 | 346 | split = layout.split(factor=0.45, align=True) 347 | split.label(text="N-Panel layout option:", icon='DECORATE') 348 | splat = split.split(factor=0.8, align=True) 349 | row = splat.row() 350 | row.prop(self, 'RC_SUBP_MODE', expand=True) 351 | 352 | split = layout.split(factor=0.45, align=True) 353 | split.label(text="N-Panel action mode:", icon='DECORATE') 354 | splat = split.split(factor=0.8, align=True) 355 | splat.prop(self, 'RC_ACTION_MAIN', text=" Start action immediately") 356 | 357 | # -- Defaults for creating new camera/target sets 358 | 359 | layout.separator() 360 | box = layout.box() 361 | box.ui_units_y = 1 362 | 363 | layout.label(text=" Defaults for creating new camera/target sets") 364 | 365 | split = layout.split(factor=0.45, align=True) 366 | split.label(text="Perspective Camera focal length:", icon='DECORATE') 367 | splat = split.split(factor=0.4, align=True) 368 | splat.prop(self, 'RC_FOCUS', expand=True) 369 | 370 | split = layout.split(factor=0.45, align=True) 371 | split.label(text="Perspective Camera sensor width:", icon='DECORATE') 372 | splat = split.split(factor=0.4, align=True) 373 | splat.prop(self, 'RC_SENSOR', expand=True) 374 | 375 | split = layout.split(factor=0.45, align=True) 376 | split.label(text="Target Object display mode:", icon='DECORATE') 377 | splat = split.split(factor=0.4, align=True) 378 | splat.prop(self, 'RC_TRGMODE', text="", expand=False) 379 | 380 | split = layout.split(factor=0.45, align=True) 381 | split.label(text="Target Object display color:", icon='DECORATE') 382 | splat = split.split(factor=0.4, align=True) 383 | splat.prop(self, 'RC_TRGCOLOR', text="") 384 | 385 | split = layout.split(factor=0.45, align=True) 386 | split.label(text="Background Image opacity level:", icon='DECORATE') 387 | splat = split.split(factor=0.4, align=True) 388 | splat.prop(self, 'RC_OPACITY', text="") 389 | 390 | split = layout.split(factor=0.45, align=True) 391 | split.label(text="Background Image depth option:", icon='DECORATE') 392 | splat = split.split(factor=0.8, align=True) 393 | row = splat.row() 394 | row.prop(self, 'RC_DEPTH', expand=True) 395 | 396 | # -- Remote Control Panel configuration 397 | 398 | layout.separator() 399 | box = layout.box() 400 | box.ui_units_y = 1 401 | 402 | layout.label(text=" Remote Control Panel configuration") 403 | 404 | split = layout.split(factor=0.45, align=True) 405 | split.label(text="General scaling for panel:", icon='DECORATE') 406 | splat = split.split(factor=0.8, align=True) 407 | splat.prop(self, 'RC_UI_BIND', text=" Bound to Blender's UI") 408 | 409 | split = layout.split(factor=0.45, align=True) 410 | split.label(text="User defined addon scaling:", icon='DECORATE') 411 | splat = split.split(factor=0.4, align=True) 412 | splat.prop(self, 'RC_SCALE', text="") 413 | 414 | split = layout.split(factor=0.45, align=True) 415 | split.label(text="Blinking Cycle duration (On / Off):", icon='DECORATE') 416 | splat = split.split(factor=0.4, align=True) 417 | splot = splat.split(factor=0.5, align=True) 418 | row = splot.row(align=False) 419 | row.prop(self, 'RC_BLINK_ON', text="") 420 | row = splot.row(align=False) 421 | row.prop(self, 'RC_BLINK_OFF', text="") 422 | 423 | split = layout.split(factor=0.45, align=True) 424 | split.label(text="Blinking operation option:", icon='DECORATE') 425 | splat = split.split(factor=0.8, align=True) 426 | splat.prop(self, 'RC_BLINK_ALT', text=" Alternative mode") 427 | 428 | split = layout.split(factor=0.45, align=True) 429 | split.label(text="Panel action mode:", icon='DECORATE') 430 | splat = split.split(factor=0.8, align=True) 431 | splat.prop(self, 'RC_ACTION_REMO', text=" Start action immediately") 432 | 433 | split = layout.split(factor=0.45, align=True) 434 | split.label(text="Panel sliding option:", icon='DECORATE') 435 | splat = split.split(factor=0.8, align=True) 436 | splat.prop(self, 'RC_SLIDE', text=" Move along viewport border") 437 | 438 | split = layout.split(factor=0.45, align=True) 439 | split.label(text="Opening screen position:", icon='DECORATE') 440 | splat = split.split(factor=0.8, align=True) 441 | splat.prop(self, 'RC_POSITION', text=" Same as in the last opened scene") 442 | 443 | if bpy.context.scene.get("bl_ui_panel_saved_data") is None: 444 | coords = "x: 0 " + \ 445 | "y: 0 " 446 | else: 447 | panH = bpy.context.preferences.addons[__package__].preferences.RC_PAN_H # Panel height 448 | pos_x = int(round(bpy.context.scene.get("bl_ui_panel_saved_data")["panX"])) 449 | pos_y = int(round(bpy.context.scene.get("bl_ui_panel_saved_data")["panY"])) 450 | # Note: Because of the scaling logic it was necessary to do this weird math correction below 451 | coords = "x: " + str(pos_x) + " " + \ 452 | "y: " + str(pos_y + int(panH * (self.over_scale(1) - 1))) + " " 453 | 454 | split = layout.split(factor=0.45, align=True) 455 | split.label(text="Current screen position:", icon='DECORATE') 456 | splat = split.split(factor=0.4, align=True) 457 | splat.label(text=coords) 458 | splot = splat.split(factor=0.455, align=True) 459 | splot.operator(Reset_Coords.bl_idname) 460 | 461 | layout.separator() 462 | box = layout.box() 463 | row = box.row(align=True) 464 | box.scale_y = 0.5 465 | box.label(text=" Additional information and Acknowledge:") 466 | box.label(text="") 467 | box.label(text=" - This addon prepared and packaged by Marcelo M Marques (mmmrqs@gmail.com)") 468 | box.label(text=" (updates at https://github.com/mmmrqs/Blender-Reference-Camera-Panel-addon)") 469 | box.label(text=" - Object Reference Cameras original project by Witold Jaworski (wjaworski@airplanes3d.net)") 470 | box.label(text=" (download it from http://airplanes3d.net/scripts-257_e.xml)") 471 | box.label(text=" - BL UI Widgets original project by Jayanam (jayanam.games@gmail.com)") 472 | box.label(text=" (download it from https://github.com/jayanam/bl_ui_widgets)") 473 | box.label(text="") 474 | box.label(text=" Special thanks to: @batFINGER, Shane Ambler (sambler), vananders, and many others,") 475 | box.label(text=" for their posts on the community forums, which have been crucial for making this addon.") 476 | box.label(text="") 477 | 478 | 479 | class Reset_Coords(bpy.types.Operator): 480 | bl_idname = "object.reset_coords" 481 | bl_label = "Reset Pos" 482 | bl_description = "Resets the 'Remote Control' panel screen position for this current session only.\n" \ 483 | "Use this button to recover the panel if it has got stuck out of the viewport area.\n" \ 484 | "You will need to reopen the panel for the new screen position to take effect" 485 | 486 | @classmethod 487 | def poll(cls, context): 488 | return (not bpy.context.scene.get("bl_ui_panel_saved_data") is None) 489 | 490 | def invoke(self, context, event): 491 | return self.execute(context) 492 | 493 | def execute(self, context): 494 | panW = bpy.context.preferences.addons[__package__].preferences.RC_PAN_W # Panel width 495 | panH = bpy.context.preferences.addons[__package__].preferences.RC_PAN_H # Panel height 496 | panX = 100 # Panel X coordinate, for top-left corner (some default, case it fails below) 497 | panY = panH + 40 - 1 # Panel Y coordinate, for top-left corner 498 | 499 | region = get_3d_area_and_region(prefs=True)[1] 500 | if region: 501 | if bpy.context.preferences.addons[__package__].preferences.RC_UI_BIND: 502 | # From Preferences/Interface/"Display" 503 | ui_scale = bpy.context.preferences.view.ui_scale 504 | else: 505 | ui_scale = 1 506 | over_scale = bpy.context.preferences.addons[__package__].preferences.RC_SCALE 507 | # Need this just because I want the panel to be centered 508 | panX = int((region.width - (panW * ui_scale * over_scale)) / 2.0) + 1 509 | try: 510 | bpy.context.preferences.addons[__package__].preferences.RC_POS_X = panX 511 | bpy.context.preferences.addons[__package__].preferences.RC_POS_Y = panY 512 | bpy.context.scene.get("bl_ui_panel_saved_data")["panX"] = panX 513 | bpy.context.scene.get("bl_ui_panel_saved_data")["panY"] = panY 514 | # These two next statements cause the remote panel to be closed by the BL_UI_OT_draw_operator modal class 515 | # and changes the operator's label on the N-Panel UI accordingly to indicate panel can be opened again. 516 | bpy.context.scene.var.RemoVisible = False 517 | bpy.context.scene.var.btnRemoText = "Open Remote Control" 518 | except Exception as e: 519 | pass 520 | return {'FINISHED'} 521 | 522 | 523 | # Registration 524 | def register(): 525 | bpy.utils.register_class(Reset_Coords) 526 | bpy.utils.register_class(ReferenceCameraPreferences) 527 | 528 | 529 | def unregister(): 530 | bpy.utils.unregister_class(ReferenceCameraPreferences) 531 | bpy.utils.unregister_class(Reset_Coords) 532 | 533 | 534 | if __name__ == '__main__': 535 | register() 536 | --------------------------------------------------------------------------------