├── .github └── workflows │ └── python-publish.yml ├── .gitignore ├── DesignerWidgets ├── DirectAutoSizer.widget ├── DirectBoxSizer.widget ├── DirectCollapsibleFrame.widget ├── DirectDatePicker.widget ├── DirectDiagram.widget ├── DirectGridSizer.widget ├── DirectMenuBar.widget ├── DirectMenuItem.widget ├── DirectOptionMenu.widget ├── DirectScrolledWindowFrame.widget ├── DirectSpinBox.widget ├── DirectSplitFrame.widget ├── DirectTabbedFrame.widget ├── DirectTooltip.widget └── DirectTreeView.widget ├── DirectGuiExtension ├── DirectAutoSizer.py ├── DirectBoxSizer.py ├── DirectCollapsibleFrame.py ├── DirectDatePicker.py ├── DirectDiagram.py ├── DirectGridSizer.py ├── DirectGuiHelper.py ├── DirectMenuBar.py ├── DirectMenuItem.py ├── DirectOptionMenu.py ├── DirectScrolledWindowFrame.py ├── DirectSpinBox.py ├── DirectSplitFrame.py ├── DirectTabbedFrame.py ├── DirectTooltip.py ├── DirectTreeView.py ├── __init__.py └── data │ ├── icons │ ├── minusnode.gif │ └── plusnode.gif │ ├── maps │ └── shuttle_controls_1.rgb │ └── shuttle_controls.egg ├── LICENSE ├── MANIFEST.in ├── README.md ├── requirements.txt ├── samples.py ├── setup.py ├── tabbedFrameExample.py └── treeViewExample.py /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | /build 3 | /dist 4 | /*.egg-info 5 | *.py[cod] 6 | -------------------------------------------------------------------------------- /DesignerWidgets/DirectAutoSizer.widget: -------------------------------------------------------------------------------- 1 | { 2 | "name":"DirectAutoSizer", 3 | "baseWidget":"DirectFrame", 4 | "moduleName":"DirectAutoSizer", 5 | "displayName":"Auto Sizer", 6 | "className":"DirectAutoSizer", 7 | "classFilePath":"DirectGuiExtension.DirectAutoSizer", 8 | "customProperties":[ 9 | { 10 | "displayName":"Refresh sizer", 11 | "internalName":"refresh", 12 | "internalType":"function", 13 | "editType":"command", 14 | "valueOptions":"refresh" 15 | }, 16 | { 17 | "displayName":"Extend Horizontal", 18 | "internalName":"extendHorizontal", 19 | "internalType":"bool" 20 | }, 21 | { 22 | "displayName":"Extend Vertical", 23 | "internalName":"extendVertical", 24 | "internalType":"bool" 25 | }, 26 | { 27 | "displayName":"Update on window resize", 28 | "internalName":"updateOnWindowResize", 29 | "internalType":"bool" 30 | }, 31 | { 32 | "displayName":"Minimum size", 33 | "internalName":"minSize", 34 | "internalType":"int", 35 | "editType":"base4" 36 | }, 37 | { 38 | "displayName":"Maximum size", 39 | "internalName":"maxSize", 40 | "internalType":"int", 41 | "editType":"base4" 42 | }, 43 | { 44 | "displayName":"Child size update function", 45 | "internalName":"childUpdateSizeFunc", 46 | "internalType":"function" 47 | }, 48 | { 49 | "displayName":"Function returning the size of the parent", 50 | "internalName":"parentGetSizeFunction", 51 | "internalType":"function" 52 | }, 53 | { 54 | "displayName":"Extra arguments for the function returning the size of the parent", 55 | "internalName":"parentGetSizeExtraArgs", 56 | "internalType":"function" 57 | }], 58 | "addItemFunctionName":"setChild", 59 | "removeItemFunctionName":"removeChild", 60 | "importPath":"from DirectGuiExtension.DirectAutoSizer import DirectAutoSizer" 61 | } 62 | -------------------------------------------------------------------------------- /DesignerWidgets/DirectBoxSizer.widget: -------------------------------------------------------------------------------- 1 | { 2 | "name":"DirectBoxSizer", 3 | "baseWidget":"DirectFrame", 4 | "moduleName":"DirectBoxSizer", 5 | "displayName":"Box Sizer", 6 | "className":"DirectBoxSizer", 7 | "classFilePath":"DirectGuiExtension.DirectBoxSizer", 8 | "customProperties":[ 9 | { 10 | "displayName":"Item Alignment", 11 | "internalName":"itemAlign", 12 | "internalType":"str", 13 | "editType":"optionMenu", 14 | "valueOptions":{"Center":1, "Left":2, "Right":4, "Middle":8, "Top":16, "Bottom":32, "Center/Middle":9, "Center/Top":17, "Center/Bottom":33, "Left/Middle":10, "Left/Top":18, "Left/Bottom":34, "Right/Middle":12, "Right/Top":20, "Right/Bottom":36} 15 | }, 16 | { 17 | "displayName":"Refresh position", 18 | "internalName":"refresh", 19 | "editType":"command", 20 | "valueOptions":"refresh" 21 | }, 22 | { 23 | "displayName":"Item Margin", 24 | "internalName":"itemMargin", 25 | "internalType":"int", 26 | "editType":"base4" 27 | }, 28 | { 29 | "displayName":"Orientation", 30 | "internalName":"orientation", 31 | "editType":"optionMenu", 32 | "valueOptions":{"Horizontal":"horizontal", "Horizontal Inverted":"horizontal_inverted", "Vertical":"vertical", "Vertical Inverted":"vertical_inverted"} 33 | }, 34 | { 35 | "displayName":"Update Framesize with items", 36 | "internalName":"autoUpdateFrameSize", 37 | "editType":"bool" 38 | }], 39 | "addItemFunctionName":"addItem", 40 | "removeItemFunctionName":"removeItem", 41 | "importPath":"from DirectGuiExtension.DirectBoxSizer import DirectBoxSizer" 42 | } 43 | -------------------------------------------------------------------------------- /DesignerWidgets/DirectCollapsibleFrame.widget: -------------------------------------------------------------------------------- 1 | { 2 | "name":"DirectCollapsibleFrame", 3 | "baseWidget":"DirectFrame", 4 | "moduleName":"DirectCollapsibleFrame", 5 | "displayName":"Collapsible Frame", 6 | "className":"DirectCollapsibleFrame", 7 | "classFilePath":"DirectGuiExtension.DirectCollapsibleFrame", 8 | "customProperties":[ 9 | { 10 | "displayName":"Header Height", 11 | "internalName":"headerheight", 12 | "editType":"float", 13 | "postProcessFunctionName": "setCollapsed", 14 | "defaultPixel": 20 15 | }, 16 | { 17 | "displayName":"Collapsed", 18 | "internalName":"collapsed", 19 | "editType":"bool" 20 | }, 21 | { 22 | "displayName":"Collapse Text", 23 | "internalName":"collapseText", 24 | "editType":"str", 25 | "postProcessFunctionName": "setCollapsed" 26 | }, 27 | { 28 | "displayName":"Extend Text", 29 | "internalName":"extendText", 30 | "editType":"str", 31 | "postProcessFunctionName": "setCollapsed" 32 | }, 33 | { 34 | "displayName":"Update Frame Size", 35 | "internalName":"updateFrameSize", 36 | "editType":"command", 37 | "valueOptions": "updateFrameSize" 38 | }, 39 | { 40 | "internalName": "frameSize", 41 | "defaultPixel": [-100, 100, -100, 100] 42 | } 43 | ], 44 | "importPath":"from DirectGuiExtension.DirectCollapsibleFrame import DirectCollapsibleFrame" 45 | } 46 | -------------------------------------------------------------------------------- /DesignerWidgets/DirectDatePicker.widget: -------------------------------------------------------------------------------- 1 | { 2 | "name":"DirectDatePicker", 3 | "baseWidget":"DirectGridSizer", 4 | "moduleName":"DirectDatePicker", 5 | "displayName":"Calendar", 6 | "className":"DirectDatePicker", 7 | "classFilePath":"DirectGuiExtension.DirectDatePicker", 8 | "customProperties":[ 9 | { 10 | "displayName":"Year", 11 | "internalName":"year", 12 | "editType":"int" 13 | }, 14 | { 15 | "displayName":"Month", 16 | "internalName":"month", 17 | "editType":"int" 18 | }, 19 | { 20 | "displayName":"Day", 21 | "internalName":"day", 22 | "editType":"int" 23 | }, 24 | { 25 | "displayName":"Normal Day Frame Color", 26 | "internalName":"normalDayFrameColor", 27 | "editType":"base4" 28 | }, 29 | { 30 | "displayName":"Active Day Frame Color", 31 | "internalName":"activeDayFrameColor", 32 | "editType":"base4" 33 | }, 34 | { 35 | "displayName":"Today Frame Color", 36 | "internalName":"todayFrameColor", 37 | "editType":"base4" 38 | }, 39 | { 40 | "internalName": "scale", 41 | "defaultPixel": 200 42 | } 43 | ], 44 | "importPath":"from DirectGuiExtension.DirectDatePicker import DirectDatePicker" 45 | } 46 | -------------------------------------------------------------------------------- /DesignerWidgets/DirectDiagram.widget: -------------------------------------------------------------------------------- 1 | { 2 | "name":"DirectDiagram", 3 | "baseWidget":"DirectFrame", 4 | "moduleName":"DirectDiagram", 5 | "displayName":"Diagram", 6 | "className":"DirectDiagram", 7 | "classFilePath":"DirectGuiExtension.DirectDiagram", 8 | "customProperties":[ 9 | { 10 | "displayName":"Data", 11 | "internalName":"data", 12 | "editType":"list", 13 | "setFunctionName":"setData" 14 | }, 15 | { 16 | "displayName":"Num Pos Steps", 17 | "internalName":"numPosSteps", 18 | "editType":"int" 19 | }, 20 | { 21 | "displayName":"Num Pos Steps Step", 22 | "internalName":"numPosStepsStep", 23 | "editType":"int" 24 | }, 25 | { 26 | "displayName":"Num Neg Steps", 27 | "internalName":"numNegSteps", 28 | "editType":"int" 29 | }, 30 | { 31 | "displayName":"Num Neg Steps Step", 32 | "internalName":"numNegStepsStep", 33 | "editType":"int" 34 | }, 35 | { 36 | "displayName":"Num Text Scale", 37 | "internalName":"numtextScale", 38 | "editType":"float" 39 | }, 40 | { 41 | "displayName":"Show Data Numbers", 42 | "internalName":"showDataNumbers", 43 | "editType":"bool" 44 | }, 45 | { 46 | "displayName":"Data Num Text Scale", 47 | "internalName":"dataNumtextScale", 48 | "editType":"float" 49 | }, 50 | { 51 | "displayName":"Data Num Text Scale", 52 | "internalName":"dataNumtextScale", 53 | "editType":"float" 54 | }, 55 | { 56 | "displayName":"Step Accuracy", 57 | "internalName":"stepAccuracy", 58 | "editType":"int" 59 | }, 60 | { 61 | "displayName":"Step Format", 62 | "internalName":"stepFormat", 63 | "editType":"optionMenu", 64 | "valueOptions":{"int":"int", "float":"float"}, 65 | "loaderFunc": "eval(value)", 66 | "addToExtraOptions": true, 67 | "canGetValueFromElement": false 68 | }, 69 | { 70 | "displayName":"Number Area Width", 71 | "internalName":"numberAreaWidth", 72 | "editType":"float" 73 | }, 74 | { 75 | "displayName":"Refresh position", 76 | "internalName":"refresh", 77 | "editType":"command", 78 | "valueOptions":"refresh" 79 | }, 80 | { 81 | "internalName": "frameSize", 82 | "defaultPixel": [-100, 100, -100, 100] 83 | } 84 | ], 85 | "importPath":"from DirectGuiExtension.DirectDiagram import DirectDiagram" 86 | } 87 | -------------------------------------------------------------------------------- /DesignerWidgets/DirectGridSizer.widget: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DirectGridSizer", 3 | "baseWidget": "DirectFrame", 4 | "moduleName": "DirectGridSizer", 5 | "displayName": "Grid Sizer", 6 | "className": "DirectGridSizer", 7 | "classFilePath": "DirectGuiExtension.DirectGridSizer", 8 | "customProperties": [ 9 | { 10 | "displayName": "Align", 11 | "internalName": "boxAlign", 12 | "editType": "optionMenu", 13 | "valueOptions": {"Left": "left", "Right": "right"} 14 | }, 15 | { 16 | "displayName": "Refresh position", 17 | "internalName": "refresh", 18 | "editType": "command", 19 | "valueOptions": "refresh" 20 | }, 21 | { 22 | "displayName": "Item Margin", 23 | "internalName": "itemMargin", 24 | "editType": "base4" 25 | }, 26 | { 27 | "displayName": "Number of Rows", 28 | "internalName": "numRows", 29 | "editType": "int" 30 | }, 31 | { 32 | "displayName": "Number of Columns", 33 | "internalName": "numColumns", 34 | "editType": "int" 35 | }, 36 | { 37 | "displayName": "Update Framesize with items", 38 | "internalName": "autoUpdateFrameSize", 39 | "editType": "bool" 40 | } 41 | ], 42 | "addItemFunctionName": "addItem", 43 | "addItemExtraArgs": { 44 | "row": { 45 | "type": "int", 46 | "defaultValue": 0 47 | }, 48 | "column": { 49 | "type": "int", 50 | "defaultValue": 0 51 | }, 52 | "widthInColumns": { 53 | "type": "int", 54 | "defaultValue": 1 55 | }, 56 | "heightInRows": { 57 | "type": "int", 58 | "defaultValue": 1 59 | } 60 | }, 61 | "removeItemFunctionName": "removeItem", 62 | "importPath": "from DirectGuiExtension.DirectGridSizer import DirectGridSizer" 63 | } 64 | -------------------------------------------------------------------------------- /DesignerWidgets/DirectMenuBar.widget: -------------------------------------------------------------------------------- 1 | { 2 | "name":"DirectMenuBar", 3 | "baseWidget":"DirectBoxSizer", 4 | "moduleName":"DirectMenuBar", 5 | "displayName":"Menu Bar", 6 | "className":"DirectMenuBar", 7 | "classFilePath":"DirectGuiExtension.DirectMenuBar", 8 | "customProperties":[ 9 | { 10 | "displayName":"Menu Items", 11 | "internalName":"menuItems", 12 | "editType":"list", 13 | "postProcessFunctionName": "setItems" 14 | } 15 | ], 16 | "addItemFunctionName":"addItem", 17 | "removeItemFunctionName":"removeItem", 18 | "importPath":"from DirectGuiExtension.DirectMenuBar import DirectMenuBar" 19 | } 20 | -------------------------------------------------------------------------------- /DesignerWidgets/DirectMenuItem.widget: -------------------------------------------------------------------------------- 1 | { 2 | "name":"DirectMenuItem", 3 | "baseWidget":"DirectButton", 4 | "moduleName":"DirectMenuItem", 5 | "displayName":"Menu Item", 6 | "className":"DirectMenuItem", 7 | "classFilePath":"DirectGuiExtension.DirectMenuItem", 8 | "customProperties":[ 9 | { 10 | "displayName":"Items", 11 | "internalName":"items", 12 | "editType":"list", 13 | "internalType": "list", 14 | "postProcessFunctionName": "setItems" 15 | }, 16 | { 17 | "displayName":"Popup Location", 18 | "internalName":"popupMenuLocation", 19 | "editType":"optionMenu", 20 | "valueOptions":{"Left":"left", "Right":"right", "Above":"above", "Below":"below"} 21 | }, 22 | { 23 | "displayName":"Highlight Color", 24 | "internalName":"highlightColor", 25 | "editType":"base4" 26 | }, 27 | { 28 | "displayName":"Item Frame Color", 29 | "internalName":"itemFrameColor", 30 | "editType":"base4" 31 | }, 32 | { 33 | "displayName":"Separator Frame Color", 34 | "internalName":"separatorFrameColor", 35 | "editType":"base4" 36 | } 37 | ], 38 | "importPath":"from DirectGuiExtension.DirectMenuItem import DirectMenuItem" 39 | } 40 | -------------------------------------------------------------------------------- /DesignerWidgets/DirectOptionMenu.widget: -------------------------------------------------------------------------------- 1 | { 2 | "/name":"DirectOptionMenu", 3 | "baseWidget":"DirectFrame", 4 | "moduleName":"DirectOptionMenu", 5 | "displayName":"Option Menu", 6 | "className":"DirectOptionMenu", 7 | "classFilePath":"DirectGuiExtension.DirectOptionMenu", 8 | "customProperties":[ 9 | { 10 | "displayName":"Items", 11 | "internalName":"items", 12 | "editType":"list", 13 | "postProcessFunctionName": "setItems", 14 | "defaultValue": ["Item1"] 15 | }, 16 | { 17 | "displayName":"Initial Item", 18 | "internalName":"initialitem", 19 | "editType":"int", 20 | "isInitOption": true 21 | }, 22 | { 23 | "displayName":"Popup Marker Border", 24 | "internalName":"popupMarkerBorder", 25 | "editType":"base2" 26 | }, 27 | { 28 | "displayName":"Popup Menu Location", 29 | "internalName":"popupMenuLocation", 30 | "editType":"optionMenu", 31 | "valueOptions":{"Left":"left", "Right":"right", "Above":"above", "Below":"below"} 32 | }, 33 | { 34 | "displayName":"Highlight Color", 35 | "internalName":"highlightColor", 36 | "editType":"base4" 37 | }, 38 | { 39 | "displayName":"Highlight Scale", 40 | "internalName":"highlightScale", 41 | "editType":"base2" 42 | } 43 | ], 44 | "importPath":"from DirectGuiExtension.DirectOptionMenu import DirectOptionMenu" 45 | } 46 | -------------------------------------------------------------------------------- /DesignerWidgets/DirectScrolledWindowFrame.widget: -------------------------------------------------------------------------------- 1 | { 2 | "name":"DirectScrolledWindowFrame", 3 | "baseWidget":"DirectScrolledFrame", 4 | "moduleName":"DirectScrolledWindowFrame", 5 | "displayName":"Scrolled Window Frame", 6 | "className":"DirectScrolledWindowFrame", 7 | "classFilePath":"DirectGuiExtension.DirectScrolledWindowFrame", 8 | "customProperties":[ 9 | { 10 | "displayName":"Drag Area Height", 11 | "internalName":"dragAreaHeight", 12 | "editType":"float" 13 | }, 14 | { 15 | "displayName":"Resort z-order on drag", 16 | "internalName":"resortOnDrag", 17 | "editType":"bool" 18 | }, 19 | { 20 | "displayName":"Show Close Button", 21 | "internalName":"showClose", 22 | "editType":"bool" 23 | }, 24 | { 25 | "displayName":"Close Button Position", 26 | "internalName":"closeButtonPosition", 27 | "editType":"optionMenu", 28 | "valueOptions":{"Left":"Left", "Right":"Right"} 29 | }, 30 | { 31 | "displayName":"Close Button scale", 32 | "internalName":"closeButtonScale", 33 | "editType":"float" 34 | }, 35 | { 36 | "internalName": "scale", 37 | "defaultPixel": 200 38 | } 39 | ], 40 | "addItemNode": "canvas", 41 | "importPath":"from DirectGuiExtension.DirectScrolledWindowFrame import DirectScrolledWindowFrame" 42 | } 43 | -------------------------------------------------------------------------------- /DesignerWidgets/DirectSpinBox.widget: -------------------------------------------------------------------------------- 1 | { 2 | "name":"DirectSpinBox", 3 | "moduleName":"DirectSpinBox", 4 | "displayName":"Spinner", 5 | "className":"DirectSpinBox", 6 | "classFilePath":"DirectGuiExtension.DirectSpinBox", 7 | "baseWidget": "DirectFrame", 8 | "customProperties":[ 9 | { 10 | "displayName":"Step Size", 11 | "internalName":"stepSize", 12 | "editType":"float" 13 | }, 14 | { 15 | "displayName":"Reset Position", 16 | "internalName":"", 17 | "editType":"command", 18 | "valueOptions":"resetPosition" 19 | }, 20 | { 21 | "displayName":"Recalculate Frame Size", 22 | "internalName":"", 23 | "editType":"command", 24 | "internalType":"function", 25 | "valueOptions":"recalcFrameSize" 26 | }, 27 | { 28 | "displayName":"Text format", 29 | "internalName":"textFormat", 30 | "editType":"text" 31 | }, 32 | { 33 | "displayName":"value Type", 34 | "internalName":"valueType", 35 | "editType":"optionMenu", 36 | "valueOptions":{"int":"int", "float":"float"}, 37 | "loaderFunc": "eval(value)", 38 | "addToExtraOptions": true, 39 | "canGetValueFromElement": false 40 | }, 41 | { 42 | "displayName":"Minimum value", 43 | "internalName":"minValue", 44 | "editType":"int" 45 | }, 46 | { 47 | "displayName":"Maximum value", 48 | "internalName":"maxValue", 49 | "editType":"int" 50 | }, 51 | { 52 | "displayName":"Button repeat delay", 53 | "internalName":"repeatdelay", 54 | "editType":"float" 55 | }, 56 | { 57 | "displayName":"Button repeat start delay", 58 | "internalName":"repeatStartdelay", 59 | "editType":"float" 60 | }, 61 | { 62 | "displayName":"Button Orientation", 63 | "internalName":"buttonOrientation", 64 | "editType":"optionMenu", 65 | "valueOptions":{"Vertical":"vertical", "Horizontal":"horizontal"}, 66 | "postProcessFunctionName":"resetPosition" 67 | }, 68 | { 69 | "internalName": "scale", 70 | "defaultAspect": 0.1, 71 | "defaultPixel": 100 72 | } 73 | ], 74 | "importPath":"from DirectGuiExtension.DirectSpinBox import DirectSpinBox" 75 | } 76 | -------------------------------------------------------------------------------- /DesignerWidgets/DirectSplitFrame.widget: -------------------------------------------------------------------------------- 1 | { 2 | "name":"DirectSplitFrame", 3 | "baseWidget":"DirectFrame", 4 | "moduleName":"DirectSplitFrame", 5 | "displayName":"Split Frame", 6 | "className":"DirectSplitFrame", 7 | "classFilePath":"DirectGuiExtension.DirectSplitFrame", 8 | "customProperties":[ 9 | { 10 | "displayName":"Pixel 2d", 11 | "internalName":"pixel2d", 12 | "editType":"bool" 13 | }, 14 | { 15 | "displayName":"Show Splitter", 16 | "internalName":"showSplitter", 17 | "editType":"bool" 18 | }, 19 | { 20 | "displayName":"Splitter Pos", 21 | "internalName":"splitterPos", 22 | "editType":"float" 23 | }, 24 | { 25 | "displayName":"Splitter Width", 26 | "internalName":"splitterWidth", 27 | "editType":"float", 28 | "defaultPixel": 8 29 | }, 30 | { 31 | "displayName":"Orientation", 32 | "internalName":"orientation", 33 | "editType":"optionMenu", 34 | "valueOptions":{"Horizontal":"horizontal", "Vertical":"vertical"}, 35 | "postProcessFunctionName": "refresh" 36 | }, 37 | { 38 | "displayName":"Splitter Color", 39 | "internalName":"splitterColor", 40 | "editType":"base4" 41 | }, 42 | { 43 | "displayName":"Splitter Highlight Color", 44 | "internalName":"splitterHighlightColor", 45 | "editType":"base4" 46 | }, 47 | { 48 | "displayName":"First Frame Update Size Func", 49 | "internalName":"firstFrameUpdateSizeFunc", 50 | "internalType":"function" 51 | }, 52 | { 53 | "displayName":"Second Frame Update Size Func", 54 | "internalName":"secondFrameUpdateSizeFunc", 55 | "internalType":"function" 56 | }, 57 | { 58 | "displayName":"First Frame Min Size", 59 | "internalName":"firstFrameMinSize", 60 | "editType":"float" 61 | }, 62 | { 63 | "displayName":"Second Frame Min Size", 64 | "internalName":"secondFrameMinSize", 65 | "editType":"float" 66 | }, 67 | { 68 | "internalName": "frameSize", 69 | "defaultPixel": [-100, 100, -100, 100] 70 | } 71 | ], 72 | "addItemNode": ["firstFrame", "secondFrame"], 73 | "importPath":"from DirectGuiExtension.DirectSplitFrame import DirectSplitFrame" 74 | } 75 | -------------------------------------------------------------------------------- /DesignerWidgets/DirectTabbedFrame.widget: -------------------------------------------------------------------------------- 1 | { 2 | "name":"DirectTabbedFrame", 3 | "baseWidget":"DirectFrame", 4 | "moduleName":"DirectTabbedFrame", 5 | "displayName":"Tabbed Frame", 6 | "className":"DirectTabbedFrame", 7 | "classFilePath":"DirectGuiExtension.DirectTabbedFrame", 8 | "customProperties":[ 9 | { 10 | "displayName":"Tab Height", 11 | "internalName":"tabHeight", 12 | "editType":"float" 13 | }, 14 | { 15 | "displayName":"Show Close On Tabs", 16 | "internalName":"showCloseOnTabs", 17 | "editType":"bool" 18 | }, 19 | { 20 | "displayName":"Selected Tab Color", 21 | "internalName":"selectedTabColor", 22 | "editType":"base4" 23 | }, 24 | { 25 | "displayName":"Unselected Tab Color", 26 | "internalName":"unselectedTabColor", 27 | "editType":"base4" 28 | }, 29 | { 30 | "displayName":"Reposition Tabs", 31 | "internalName":"reposition_tabs", 32 | "editType":"command", 33 | "valueOptions": "reposition_tabs" 34 | }, 35 | { 36 | "internalName": "scale", 37 | "defaultPixel": 100 38 | } 39 | ], 40 | "addItemFunctionName":"_add_tab", 41 | "addItemExtraArgs": { 42 | "tab_text": { 43 | "type": "str", 44 | "defaultValue": "tab" 45 | } 46 | }, 47 | "importPath":"from DirectGuiExtension.DirectTabbedFrame import DirectTabbedFrame" 48 | } 49 | -------------------------------------------------------------------------------- /DesignerWidgets/DirectTooltip.widget: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DirectTooltip", 3 | "baseWidget": "DirectLabel", 4 | "moduleName": "DirectTooltip", 5 | "displayName": "Tooltip", 6 | "className": "DirectTooltip", 7 | "classFilePath": "DirectGuiExtension.DirectTooltip", 8 | "customProperties": [ 9 | { 10 | "displayName": "Show", 11 | "internalName": "show", 12 | "editType": "command", 13 | "valueOptions": "show" 14 | }, 15 | { 16 | "displayName": "Hide", 17 | "internalName": "hide", 18 | "editType": "command", 19 | "valueOptions": "hide" 20 | }, 21 | { 22 | "internalName": "scale", 23 | "defaultAspect": 0.1, 24 | "defaultPixel": 100 25 | } 26 | ], 27 | "importPath": "from DirectGuiExtension.DirectTooltip import DirectTooltip" 28 | } 29 | -------------------------------------------------------------------------------- /DesignerWidgets/DirectTreeView.widget: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DirectTreeView", 3 | "baseWidget": "DirectBoxSizer", 4 | "moduleName": "DirectTreeView", 5 | "displayName": "Tree View", 6 | "className": "DirectTreeView", 7 | "classFilePath": "DirectGuiExtension.DirectTreeView", 8 | "customProperties": [ 9 | { 10 | "displayName": "Image Collapse", 11 | "internalName": "imageCollapse", 12 | "editType": "str", 13 | "isInitOption": true 14 | }, 15 | { 16 | "displayName": "Image Collapsed", 17 | "internalName": "imageCollapsed", 18 | "editType": "str", 19 | "isInitOption": true 20 | }, 21 | { 22 | "displayName": "Indentation Width", 23 | "internalName": "indentationWidth", 24 | "editType": "float" 25 | }, 26 | { 27 | "displayName": "Refresh Tree", 28 | "internalName": "refreshTree", 29 | "editType": "command", 30 | "valueOptions": "refreshTree" 31 | } 32 | ], 33 | "importPath": "from DirectGuiExtension.DirectTreeView import DirectTreeView" 34 | } 35 | -------------------------------------------------------------------------------- /DirectGuiExtension/DirectAutoSizer.py: -------------------------------------------------------------------------------- 1 | """This module contains the DirectAutoSizer class.""" 2 | 3 | __all__ = ['DirectAutoSizer'] 4 | 5 | import inspect 6 | from panda3d.core import * 7 | from direct.gui import DirectGuiGlobals as DGG 8 | from direct.gui.DirectFrame import DirectFrame 9 | from direct.gui.DirectGuiBase import DirectGuiWidget 10 | from . import DirectGuiHelper as DGH 11 | from direct.showbase import ShowBaseGlobal 12 | 13 | class DirectAutoSizer(DirectFrame): 14 | """ 15 | A frame to Automatically resize the given other DirectGui element 16 | """ 17 | def __init__(self, parent = None, child = None, **kw): 18 | self.skipInitRefresh = True 19 | optiondefs = ( 20 | ('extendHorizontal', True, None), 21 | ('extendVertical', True, None), 22 | ('minSize', (0, 0, 0, 0), self.refresh), 23 | ('maxSize', (0, 0, 0, 0), self.refresh), 24 | ('updateOnWindowResize', True, self.setUpdateOnWindowResize), 25 | ('childUpdateSizeFunc', None, None), 26 | ('parentGetSizeFunction', None, None), 27 | ('parentGetSizeExtraArgs', [], None), 28 | 29 | ('suppressMouse', 0, None), 30 | ) 31 | # Merge keyword options with default options 32 | self.defineoptions(kw, optiondefs) 33 | 34 | # Initialize superclasses 35 | DirectFrame.__init__(self, parent) 36 | 37 | self.parentObject = parent 38 | self.child = child 39 | if child is not None: 40 | child.reparentTo(self) 41 | 42 | # Call option initialization functions 43 | self.initialiseoptions(DirectAutoSizer) 44 | 45 | self.skipInitRefresh = False 46 | # initialize once at the end 47 | self.refresh() 48 | 49 | def setChild(self, child): 50 | if self.child is not None: 51 | self.child.detachNode() 52 | self.child = child 53 | self.child.reparentTo(self) 54 | self.refresh() 55 | 56 | def removeChild(self): 57 | if self.child is not None: 58 | self.child.detachNode() 59 | self.child = None 60 | 61 | def setUpdateOnWindowResize(self): 62 | if self['updateOnWindowResize']: 63 | # Make sure the sizer scales with window size changes 64 | self.screenSize = base.getSize() 65 | self.accept('window-event', self.windowEventHandler) 66 | else: 67 | # stop updating on window resize events 68 | self.ignore('window-event') 69 | self.screenSize = None 70 | 71 | def windowEventHandler(self, window=None): 72 | if window != base.win: 73 | # This event isn't about our window. 74 | return 75 | 76 | if self.screenSize == base.getSize(): 77 | return 78 | self.screenSize = base.getSize() 79 | self.refresh() 80 | 81 | #TODO: for some reason, the first refresh doesn't always refresh everything correct, so we do it twice here 82 | # E.g. if the window is maximized only this refresh actally updates the size right. 83 | self.refresh() 84 | 85 | def refresh(self): 86 | """Resize the sizer and its child element""" 87 | if self.skipInitRefresh: return 88 | if self.child is None: 89 | return 90 | 91 | # store left/right/bottom/top 92 | l=r=b=t=0 93 | 94 | # dependent on our parent, make sure the size values are set correct 95 | if self["parentGetSizeFunction"] is not None: 96 | # we have a user defined way to get the size, this should be treated 97 | # most important 98 | size = self["parentGetSizeFunction"](*self["parentGetSizeExtraArgs"]) 99 | 100 | l=size[0] 101 | r=size[1] 102 | b=size[2] 103 | t=size[3] 104 | elif self.parentObject is None or self.parentObject == ShowBaseGlobal.aspect2d: 105 | # the default parent of directGui widgets 106 | l = base.a2dLeft 107 | r = base.a2dRight 108 | b = base.a2dBottom 109 | t = base.a2dTop 110 | elif self.parentObject == base.pixel2d: 111 | # we are parented to pixel2d 112 | xsize, ysize = base.getSize() 113 | l = 0 114 | r = xsize 115 | b = -ysize 116 | t = 0 117 | elif DirectGuiWidget in inspect.getmro(type(self.parentObject)): 118 | # We have a "normal" DirectGui widget here 119 | bounds = self.parentObject.bounds 120 | l, r, b, t = bounds 121 | else: 122 | # We are parented to something else, probably a nodepath 123 | self.parentObject.node().setBoundsType(BoundingVolume.BT_box) 124 | ll = LPoint3() 125 | ur = LPoint3() 126 | 127 | self.parentObject.calcTightBounds(ll, ur, render) 128 | 129 | l=ll.getX() 130 | r=ur.getX() 131 | b=ll.getZ() 132 | t=ur.getZ() 133 | 134 | childSize = self.child['frameSize'] 135 | if childSize is None: 136 | childSize = DGH.getBounds(self.child) 137 | 138 | if type(self.child["scale"]) == LVecBase3f: 139 | childScale = self.child["scale"] 140 | elif self.child["scale"] is not None: 141 | try: 142 | childScale = LVecBase3f(self.child["scale"]) 143 | except: 144 | childScale = self.child["scale"] 145 | else: 146 | childScale = LVecBase3f(1.0) 147 | 148 | if not self['extendHorizontal']: 149 | l = childSize[0] * childScale.getX() 150 | r = childSize[1] * childScale.getX() 151 | 152 | if not self['extendVertical']: 153 | b = childSize[2] * childScale.getZ() 154 | t = childSize[3] * childScale.getZ() 155 | 156 | #TODO: Better check for positive/negative numbers 157 | if l > self['minSize'][0]: l = self['minSize'][0] 158 | if r < self['minSize'][1]: r = self['minSize'][1] 159 | if b > self['minSize'][2]: b = self['minSize'][2] 160 | if t < self['minSize'][3]: t = self['minSize'][3] 161 | 162 | if self['maxSize'] is not None and self['maxSize'] != (0, 0, 0, 0): 163 | if l < self['maxSize'][0]: l = self['maxSize'][0] 164 | if r > self['maxSize'][1]: r = self['maxSize'][1] 165 | if b < self['maxSize'][2]: b = self['maxSize'][2] 166 | if t > self['maxSize'][3]: t = self['maxSize'][3] 167 | 168 | # recalculate size according to position 169 | l -= self.child.getX() 170 | r += self.child.getX() 171 | b -= self.child.getZ() 172 | t += self.child.getZ() 173 | 174 | # actual resizing of our child element 175 | self.child["frameSize"] = [l/childScale.getX(),r/childScale.getX(),b/childScale.getZ(),t/childScale.getZ()] 176 | self["frameSize"] = self.child["frameSize"] 177 | 178 | base.messenger.send(self.getUpdateSizeEvent()) 179 | if self['childUpdateSizeFunc'] is not None: 180 | self['childUpdateSizeFunc']() 181 | 182 | def getUpdateSizeEvent(self): 183 | return self.uniqueName("update-size") 184 | 185 | def destroy(self): 186 | self.ignoreAll() 187 | self.removeChild() 188 | -------------------------------------------------------------------------------- /DirectGuiExtension/DirectBoxSizer.py: -------------------------------------------------------------------------------- 1 | """This module contains the DirectBoxSizer class.""" 2 | 3 | __all__ = ['DirectBoxSizer'] 4 | 5 | from panda3d.core import * 6 | from direct.gui import DirectGuiGlobals as DGG 7 | from direct.gui.DirectFrame import DirectFrame 8 | from . import DirectGuiHelper as DGH 9 | 10 | 11 | DGG.HORIZONTAL_INVERTED = 'horizontal_inverted' 12 | 13 | 14 | class DirectItemContainer(): 15 | def __init__(self, element, **kw): 16 | self.element = element 17 | self.updateFunc = None 18 | if "updateFunc" in kw: 19 | self.updateFunc = kw.get("updateFunc") 20 | 21 | class DirectBoxSizer(DirectFrame): 22 | """ 23 | A frame to add multiple other directGui elements to that will then be 24 | automatically be placed stacked next to each other. 25 | """ 26 | 27 | # Horizontal 28 | A_Center = 0b1 29 | A_Left = 0b10 30 | A_Right = 0b100 31 | 32 | # Vertical 33 | A_Middle = 0b1000 34 | A_Top = 0b10000 35 | A_Bottom = 0b100000 36 | 37 | def __init__(self, parent = None, **kw): 38 | self.skipInitRefresh = True 39 | optiondefs = ( 40 | # Define type of DirectGuiWidget 41 | ('items', [], self.refresh), 42 | ('pgFunc', PGItem, None), 43 | ('numStates', 1, None), 44 | ('state', DGG.NORMAL, None), 45 | ('borderWidth', (0, 0), self.setBorderWidth), 46 | 47 | ('orientation', DGG.HORIZONTAL, self.refresh), 48 | ('itemMargin', (0,0,0,0), self.refresh), 49 | ('itemAlign', self.A_Left|self.A_Top, self.refresh), 50 | ('autoUpdateFrameSize', True, None), 51 | 52 | ('suppressMouse', 0, None), 53 | ) 54 | # Merge keyword options with default options 55 | self.defineoptions(kw, optiondefs) 56 | 57 | # Initialize superclasses 58 | DirectFrame.__init__(self, parent) 59 | 60 | # Call option initialization functions 61 | self.initialiseoptions(DirectBoxSizer) 62 | 63 | self.skipInitRefresh = False 64 | # initialize once at the end 65 | self.refresh() 66 | 67 | def addItem(self, element, **kw): 68 | """ 69 | Adds the given item to this panel stack 70 | """ 71 | element.reparentTo(self) 72 | container = DirectItemContainer(element, **kw) 73 | self["items"].append(container) 74 | if "skipRefresh" in kw: 75 | return 76 | self.refresh() 77 | 78 | def removeItem(self, element, refresh=True): 79 | """ 80 | Remove this item from the panel 81 | """ 82 | for item in self["items"]: 83 | if element == item.element: 84 | self["items"].remove(item) 85 | if refresh: 86 | self.refresh() 87 | return 1 88 | return 0 89 | 90 | def removeAllItems(self, refresh=True, removeNodes=False): 91 | """ 92 | Remove all items from the panel 93 | """ 94 | for item in list(self["items"]): 95 | self["items"].remove(item) 96 | if removeNodes: 97 | item.element.removeNode() 98 | if refresh: 99 | self.refresh() 100 | 101 | def getRemainingSpace(self): 102 | """ 103 | Gives the space that is left when all items have been placed in the 104 | sizer. 105 | """ 106 | if not hasattr(self, "bounds"): return 0 107 | 108 | if self['orientation'] == DGG.HORIZONTAL \ 109 | or self['orientation'] == DGG.HORIZONTAL_INVERTED: 110 | # Horizontal 111 | width = self.__get_items_width() 112 | return DGH.getRealWidth(self) - width 113 | elif self['orientation'] == DGG.VERTICAL \ 114 | or self['orientation'] == DGG.VERTICAL_INVERTED: 115 | height = self.__get_items_height() 116 | return DGH.getRealHeight(self) - height 117 | 118 | def refresh(self): 119 | """ 120 | Recalculate the position of every item in this panel and set the frame- 121 | size of the panel accordingly if auto update is enabled. 122 | """ 123 | if self.skipInitRefresh: return 124 | # sanity check so we don't get here to early 125 | if not hasattr(self, "bounds") and not self["autoUpdateFrameSize"]: return 126 | if len(self["items"]) == 0: return 127 | 128 | for item in self["items"]: 129 | item.element.frameInitialiseFunc() 130 | 131 | self.__refresh_frame_size() 132 | 133 | # 134 | # Update Item Positions 135 | # 136 | if self['orientation'] == DGG.HORIZONTAL: 137 | # Horizontal - Left to Right placement 138 | self.__refresh_horizontal_ltr() 139 | elif self['orientation'] == DGG.HORIZONTAL_INVERTED: 140 | # Horizontal - Right to Left 141 | self.__refresh_horizontal_rtl() 142 | elif self['orientation'] == DGG.VERTICAL: 143 | # Vertical - Top to Bottom 144 | self.__refresh_vertical_ttb() 145 | elif self['orientation'] == DGG.VERTICAL_INVERTED: 146 | # Vertical - Bottom to Top 147 | self.__refresh_vertical_btt() 148 | else: 149 | raise ValueError('Invalid value for orientation: %s' % (self['orientation'])) 150 | 151 | for item in self["items"]: 152 | if item.updateFunc is not None: 153 | item.updateFunc() 154 | 155 | def __refresh_frame_size(self): 156 | if not self["autoUpdateFrameSize"]: 157 | return 158 | 159 | width = self.__get_items_width()# + self["pad"][0]*2 160 | height = self.__get_items_height()# + self["pad"][1]*2 161 | 162 | # dependent on orientation, start at 0 and extend to the 163 | # maximum height or width and keep the respective other 164 | # direction centered 165 | if self['orientation'] == DGG.HORIZONTAL: 166 | self["frameSize"] = (0, width, -height/2, height/2) 167 | elif self['orientation'] == DGG.HORIZONTAL_INVERTED: 168 | self["frameSize"] = (-width, 0, -height/2, height/2) 169 | elif self['orientation'] == DGG.VERTICAL: 170 | self["frameSize"] = (-width/2, width/2, -height, 0) 171 | elif self['orientation'] == DGG.VERTICAL_INVERTED: 172 | self["frameSize"] = (-width/2, width/2, 0, height) 173 | 174 | def __get_items_width(self): 175 | ''' 176 | Get the maximum item width 177 | ''' 178 | width = 0 179 | for item in self["items"]: 180 | item_width = ( 181 | DGH.getRealWidth(item.element) 182 | + self["itemMargin"][0] # margin left 183 | + self["itemMargin"][1]) # margin right 184 | if self['orientation'] in [DGG.VERTICAL, DGG.VERTICAL_INVERTED]: 185 | # look for the widest item 186 | width = max(width, item_width) 187 | else: 188 | # add up all item widths 189 | width += item_width 190 | return width 191 | 192 | def __get_items_height(self): 193 | ''' 194 | Get the maximum item height 195 | ''' 196 | height = 0 197 | for item in self["items"]: 198 | item_height = ( 199 | DGH.getRealHeight(item.element) 200 | + self["itemMargin"][3] # margin top 201 | + self["itemMargin"][2]) # margin bottom 202 | if self['orientation'] in [DGG.HORIZONTAL, DGG.HORIZONTAL_INVERTED]: 203 | # look for the talest item 204 | height = max(height, item_height) 205 | else: 206 | # add up all item heights 207 | height += item_height 208 | return height 209 | 210 | # 211 | # ITEM ORDER POSITION REFRESH 212 | # 213 | # HORIZONTAL 214 | def __refresh_horizontal_ltr(self): 215 | # Horizontal - Left to Right placement 216 | # get the left side of the box sizer frame 217 | nextX = DGH.getRealLeft(self) 218 | 219 | # go through all items in the box and place them 220 | for item in self["items"]: 221 | # place the element and calculate the next x position 222 | y = self.__get_vertical_item_alignment(item.element) 223 | item.element.setPos(nextX - DGH.getRealLeft(item.element), 0, y) 224 | nextX += DGH.getRealWidth(item.element) 225 | 226 | 227 | def __refresh_horizontal_rtl(self): 228 | # Horizontal - Right to Left 229 | # get the right side of the box sizer frame 230 | nextX = DGH.getRealRight(self) 231 | 232 | # go through all items in the box and place them 233 | for item in self["items"]: 234 | # place the element and calculate the next x position 235 | y = self.__get_vertical_item_alignment(item.element) 236 | item.element.setPos(nextX - DGH.getRealRight(item.element), 0, y) 237 | nextX -= DGH.getRealWidth(item.element) 238 | 239 | # VERTICAL 240 | def __refresh_vertical_ttb(self): 241 | # Vertical - Top to Bottom 242 | # get the top side of the box sizer frame 243 | nextY = DGH.getRealTop(self) 244 | 245 | # go through all items in the box and place them 246 | for item in self["items"]: 247 | # place the element and calculate the next y position 248 | x = self.__get_horizontal_item_alignment(item.element) 249 | item.element.setPos(x, 0, nextY - DGH.getRealTop(item.element)) 250 | nextY -= DGH.getRealHeight(item.element) 251 | 252 | def __refresh_vertical_btt(self): 253 | # Vertical - Bottom to Top 254 | # get the bottom side of the box sizer frame 255 | nextY = DGH.getRealBottom(self) 256 | 257 | # go through all items in the box and place them 258 | for item in self["items"]: 259 | # place the element and calculate the next y position 260 | x = self.__get_horizontal_item_alignment(item.element) 261 | item.element.setPos(x, 0, nextY - DGH.getRealBottom(item.element)) 262 | nextY += DGH.getRealHeight(item.element) 263 | 264 | # 265 | # ITEM ALIGN POSITION CALCULATIONS 266 | # 267 | def __get_horizontal_item_alignment(self, curElem): 268 | # Horizontal Alingment 269 | if self["itemAlign"] & self.A_Left: 270 | # get the left side of the frame 271 | x = self["frameSize"][0] 272 | # shift x right to be aligned with the items left side 273 | x -= DGH.getRealLeft(curElem) 274 | return x 275 | elif self["itemAlign"] & self.A_Right: 276 | # get the right side of the frame 277 | x = self["frameSize"][1] 278 | # shift x left to be aligned with the items right side 279 | x -= DGH.getRealRight(curElem) 280 | return x 281 | elif self["itemAlign"] & self.A_Center: 282 | # aligned by the center of the frame 283 | self_l = DGH.getRealLeft(self) 284 | self_r = DGH.getRealRight(self) 285 | x = (self_l + self_r) / 2 286 | # shift x by items center shift 287 | item_l = DGH.getRealLeft(curElem) 288 | item_r = DGH.getRealRight(curElem) 289 | x += (item_l + item_r) / 2 290 | return x 291 | return 0 292 | 293 | def __get_vertical_item_alignment(self, curElem): 294 | # Vertical Alingment 295 | if self["itemAlign"] & self.A_Bottom: 296 | # Vertical adjustment to the box' size 297 | y = DGH.getRealBottom(self)# self["frameSize"][2] 298 | # shift y up to be aligned with the items bottom side 299 | y += DGH.getRealBottom(curElem) 300 | return y 301 | elif self["itemAlign"] & self.A_Top: 302 | # Items are alligned by their upper edge 303 | y = DGH.getRealTop(self)# self["frameSize"][3] 304 | # shift y down to be aligned with the items top side 305 | y -= DGH.getRealTop(curElem) 306 | return y 307 | elif self["itemAlign"] & self.A_Middle: 308 | # Items are alligned by their center 309 | # aligned by the center of the frame 310 | self_t = DGH.getRealTop(self) 311 | self_b = DGH.getRealBottom(self) 312 | y = (self_t + self_b) / 2 313 | # shift y by items center shift 314 | item_t = DGH.getRealTop(curElem) 315 | item_b = DGH.getRealBottom(curElem) 316 | y += (item_t + item_b) / 2 317 | return y 318 | return 0 319 | -------------------------------------------------------------------------------- /DirectGuiExtension/DirectCollapsibleFrame.py: -------------------------------------------------------------------------------- 1 | """This module contains the DirectCollapsibleFrame class.""" 2 | 3 | __all__ = ['DirectCollapsibleFrame'] 4 | 5 | from panda3d.core import * 6 | from direct.gui import DirectGuiGlobals as DGG 7 | from direct.gui.DirectFrame import DirectFrame 8 | from direct.gui.DirectButton import DirectButton 9 | from . import DirectGuiHelper as DGH 10 | 11 | 12 | class DirectCollapsibleFrame(DirectFrame): 13 | """ 14 | A frame containing a clickable header to show and hide its content frame 15 | """ 16 | 17 | def __init__(self, parent = None, **kw): 18 | self.skipInitRefresh = True 19 | optiondefs = ( 20 | ('headerheight', 0.1, None), 21 | ('collapsed', False, self.setCollapsed), 22 | 23 | ('collapseText', 'collapse >>', None), 24 | ('extendText', 'extend <<', None), 25 | ('frameSize', (-0.5, 0.5, -0.5, 0.5), self.setFrameSize) 26 | ) 27 | # Merge keyword options with default options 28 | self.defineoptions(kw, optiondefs) 29 | 30 | # Initialize superclasses 31 | DirectFrame.__init__(self, parent) 32 | 33 | frameSize = self['frameSize'] 34 | 35 | # set up the header button to collapse/extend 36 | self.toggleCollapseButton = self.createcomponent( 37 | 'toggleCollapseButton', (), None, 38 | DirectButton, (self,), 39 | text=self['extendText'] if self['collapsed'] else self['collapseText'], 40 | text_scale=0.05, 41 | text_align=TextNode.ALeft, 42 | text_pos=(frameSize[0]+0.02, frameSize[3]-self['headerheight']/2.0), 43 | borderWidth=(0.02, 0.02), 44 | relief=DGG.FLAT, 45 | pressEffect=False, 46 | frameSize = ( 47 | frameSize[0], frameSize[1], 48 | frameSize[3]-self['headerheight'], frameSize[3]), 49 | command=self.toggleCollapsed) 50 | 51 | # Call option initialization functions 52 | self.initialiseoptions(DirectCollapsibleFrame) 53 | 54 | self.resetFrameSize() 55 | self.updateFrameSize() 56 | 57 | self.originalFrameSize = self['frameSize'] 58 | 59 | def updateFrameSize(self): 60 | self.toggleCollapseButton['frameSize'] = ( 61 | DGH.getRealLeft(self), DGH.getRealRight(self), 62 | DGH.getRealTop(self)-self['headerheight'], DGH.getRealTop(self)) 63 | self.originalFrameSize = self['frameSize'] 64 | self.toggleCollapseButton["text_pos"] = (DGH.getRealLeft(self)+0.02, DGH.getRealTop(self)-self['headerheight']/2.0) 65 | 66 | def toggleCollapsed(self): 67 | self['collapsed'] = not self['collapsed'] 68 | 69 | def setCollapsedTo(self, collapsed): 70 | self['collapsed'] = collapsed 71 | self.setCollapsed() 72 | 73 | def setCollapsed(self): 74 | # we're probably to early here 75 | if not hasattr(self, 'originalFrameSize'): return 76 | 77 | fs = self['frameSize'] 78 | 79 | if self['collapsed']: 80 | # collapse 81 | self['frameSize'] = (fs[0],fs[1],fs[3]-self['headerheight'],fs[3]) 82 | 83 | # hide all children 84 | for child in self.getChildren(): 85 | # skip our toggle collapse button 86 | if child == self.toggleCollapseButton: continue 87 | # hide the child 88 | child.hide() 89 | 90 | # change the toggle button text 91 | self.toggleCollapseButton['text'] = self['extendText'] 92 | 93 | # send notice about us being collapsed 94 | base.messenger.send(self.getCollapsedEvent()) 95 | 96 | else: 97 | # extend 98 | self['frameSize'] = self.originalFrameSize 99 | for child in self.getChildren(): 100 | # skip our toggle collapse button 101 | if child == self.toggleCollapseButton: continue 102 | # show the child 103 | child.show() 104 | 105 | # change the toggle button text 106 | self.toggleCollapseButton['text'] = self['collapseText'] 107 | 108 | # send notice about us being extended 109 | base.messenger.send(self.getExtendedEvent()) 110 | 111 | def getCollapsedEvent(self): 112 | return self.uniqueName("collapsed") 113 | 114 | def getExtendedEvent(self): 115 | return self.uniqueName("extended") 116 | -------------------------------------------------------------------------------- /DirectGuiExtension/DirectDatePicker.py: -------------------------------------------------------------------------------- 1 | """This module contains the DirectBoxSizer class.""" 2 | 3 | __all__ = ['DirectGridSizer'] 4 | 5 | import calendar 6 | import datetime 7 | from panda3d.core import * 8 | from direct.gui import DirectGuiGlobals as DGG 9 | from direct.gui.DirectButton import DirectButton 10 | from direct.gui.DirectLabel import DirectLabel 11 | from . import DirectGuiHelper as DGH 12 | from .DirectGridSizer import DirectGridSizer 13 | from .DirectSpinBox import DirectSpinBox 14 | from .DirectOptionMenu import DirectOptionMenu 15 | 16 | 17 | class DirectDatePicker(DirectGridSizer): 18 | def __init__(self, parent = None, **kw): 19 | optiondefs = ( 20 | ('year', None, self.refreshPicker), 21 | ('month', None, self.refreshPicker), 22 | ('day', None, self.refreshPicker), 23 | 24 | ('normalDayFrameColor', None, None), 25 | ('activeDayFrameColor', None, None), 26 | ('todayFrameColor', None, None), 27 | ('frameSize', (-1, 1, -1, 1), self.setFrameSize) 28 | # Define type of DirectGuiWidget 29 | ) 30 | # Merge keyword options with default options 31 | self.defineoptions(kw, optiondefs) 32 | 33 | # Initialize superclasses 34 | DirectGridSizer.__init__(self, parent, numRows=7, numColumns=8, autoUpdateFrameSize=True, itemMargin=[0.005, 0.005, 0.005, 0.005]) 35 | 36 | # set up the calendar 37 | self.cal = calendar.Calendar() 38 | 39 | # normal day colors 40 | self['normalDayFrameColor'] = ( 41 | (0.7,0.7,0.7,1), 42 | (0.7,0.7,0.7,1), 43 | (0.7,0.0,0.0,1), 44 | (0.6,0.6,0.6,1) 45 | ) if self['normalDayFrameColor'] is None else self['normalDayFrameColor'] 46 | 47 | # selected day color 48 | self['activeDayFrameColor'] = ( 49 | (0.8,0.8,0.8,1), 50 | (0.8,0.8,0.8,1), 51 | (0.8,0.0,0.0,1), 52 | (0.6,0.6,0.6,1) 53 | ) if self['activeDayFrameColor'] is None else self['activeDayFrameColor'] 54 | 55 | # the systems current day color 56 | self['todayFrameColor'] = ( 57 | (1.0,0.7,0.7,1), 58 | (0.8,0.8,0.8,1), 59 | (0.8,0.0,0.0,1), 60 | (0.6,0.6,0.6,1) 61 | ) if self['todayFrameColor'] is None else self['todayFrameColor'] 62 | 63 | # current date 64 | now = datetime.datetime.now() 65 | 66 | # add the week header 67 | weekDays = calendar.weekheader(2).split(" ") 68 | self.headers = [] 69 | for i in range(1, 8): 70 | lbl = self.createLabel(str(weekDays[i-1])) 71 | self.headers.append(lbl) 72 | self.addItem(lbl, 0, i) 73 | 74 | # add dummy week numbers 75 | self.weekNumbers = [] 76 | for i in range(1, 7): 77 | lbl = self.createLabel("--") 78 | self.weekNumbers.append(lbl) 79 | self.addItem(lbl, i, 0) 80 | 81 | self.dateButtons = [] 82 | day = 0 83 | for row in range(1, 7): 84 | self.dateButtons.append([]) 85 | for column in range(1, 8): 86 | btn = self.createDateButton(day, False) 87 | self.dateButtons[row-1].append(btn) 88 | self.addItem(btn, row, column) 89 | day += 1 90 | 91 | # the year picker 92 | self.yearPicker = self.createcomponent( 93 | 'yearPicker', (), None, 94 | DirectSpinBox, (self,), 95 | valueEntry_width=5, 96 | scale=0.05, 97 | minValue=1, 98 | maxValue=9999, 99 | repeatdelay=0.125, 100 | buttonOrientation=DGG.HORIZONTAL, 101 | valueEntry_text_align=TextNode.ACenter, 102 | borderWidth=(.1,.1), 103 | incButtonCallback=self.__changedYear, 104 | decButtonCallback=self.__changedYear, 105 | command=self.__changedYear) 106 | self.yearPicker.resetFrameSize() 107 | 108 | self.monthPicker = self.createcomponent( 109 | 'monthPicker', (), None, 110 | DirectOptionMenu, (self,), 111 | items=calendar.month_name[1:], 112 | scale=0.05, 113 | command=self.setMonth, 114 | relief=DGG.FLAT) 115 | self.monthPicker.resetFrameSize() 116 | 117 | # Call option initialization functions 118 | self.initialiseoptions(DirectDatePicker) 119 | 120 | self.ll = Point3(self["frameSize"][0], self["frameSize"][2]) 121 | self.ur = Point3(self["frameSize"][1], self["frameSize"][3]) 122 | 123 | self.yearPicker.setPos( 124 | DGH.getRealLeft(self) + DGH.getRealWidth(self.yearPicker) / 2, 125 | 0, 126 | DGH.getRealTop(self) - DGH.getRealBottom(self.yearPicker)) 127 | 128 | self.monthPicker.setPos( 129 | DGH.getRealRight(self) - DGH.getRealWidth(self.monthPicker), 130 | 0, 131 | DGH.getRealTop(self) - DGH.getRealBottom(self.monthPicker) / 2) 132 | 133 | # set the current date if none is given at initialization 134 | self['year'] = now.year if self['year'] is None else self['year'] 135 | self['month'] = now.month if self['month'] is None else self['month'] 136 | self['day'] = now.day if self['day'] is None else self['day'] 137 | 138 | self.yearPicker.setValue(self['year']) 139 | self.monthPicker.set(self['month']-1) 140 | 141 | self.refreshPicker() 142 | 143 | def createDateButton(self, day, enabled=True): 144 | return self.createcomponent( 145 | 'day{}'.format(day), (), None, 146 | DirectButton, (self,), 147 | text=str(day), 148 | text_scale=0.05, 149 | relief=DGG.FLAT, 150 | borderWidth=(0.01, 0.01), 151 | state=DGG.NORMAL if enabled else DGG.DISABLED, 152 | frameColor=self['normalDayFrameColor'], 153 | frameSize=(-0.05,0.05,-0.035,0.0625), 154 | command=self.setDay, 155 | extraArgs=[day]) 156 | 157 | def createLabel(self, txt): 158 | return self.createcomponent( 159 | 'weekNumber', (), None, 160 | DirectLabel, (self,), 161 | text=txt, 162 | text_scale=0.05, 163 | frameColor=(0,0,0,0), 164 | frameSize=(-0.05,0.05,-0.035,0.0625) 165 | ) 166 | 167 | def setDay(self, day): 168 | self['day'] = day 169 | self.refreshPicker() 170 | 171 | def getDay(self): 172 | return self['day'] 173 | 174 | def __changedYear(self, year=None): 175 | year = int(self.yearPicker.get()) if year is None else int(year) 176 | if year > 9999: year = 9999 177 | if year < 1: year = 1 178 | self.setYear(year) 179 | 180 | def setMonth(self, month): 181 | if type(month) == str: 182 | self['month'] = calendar.month_name[:].index(month) 183 | if self.monthPicker.get() != self['month']-1: 184 | self.monthPicker.set(self['month']-1, fCommand=False) 185 | else: 186 | self['month'] = month 187 | if self.monthPicker.get() != self['month']-1: 188 | self.monthPicker.set(self['month']-1, fCommand=False) 189 | self.refreshPicker 190 | 191 | def getMonth(self): 192 | return self['month'] 193 | 194 | def setYear(self, year): 195 | self['year'] = year 196 | self.refreshPicker() 197 | 198 | def getYear(self): 199 | return self['year'] 200 | 201 | def getDate(self): 202 | return datetime.datetime(self['year'], self['month'], self['day']) 203 | 204 | def get(self): 205 | return self.getDate() 206 | 207 | def refreshPicker(self): 208 | # sanity check so we don't get here to early 209 | if self['year'] is None or self['month'] is None or self['day'] is None: 210 | return 211 | 212 | datesPrev = None 213 | if calendar.monthrange(self['year'], self['month'])[0] == 0: 214 | # get the previous dates for months that start with monday as first weekday and need one week prepended 215 | year = self['year'] if self['month'] > 1 else self['year']-1 if self['year'] > 0 else 1 216 | month = self['month']-1 if self['month'] > 1 else 12 217 | datesPrev = self.cal.monthdatescalendar(year,month) 218 | 219 | dates = self.cal.monthdatescalendar(self['year'],self['month']) 220 | 221 | datesNext = None 222 | if datesPrev is None and len(dates) < 7: 223 | # get the next dates for months that do not cover 6 lines of the calendar 224 | year = self['year'] if self['month'] < 12 else self['year']+1 if self['year'] < 9999 else 9999 225 | month = self['month']+1 if self['month'] < 12 else 1 226 | datesNext = self.cal.monthdatescalendar(year,month) 227 | 228 | 229 | def updateInfo(row, column, date): 230 | # update button 231 | btn = self.dateButtons[row][column] 232 | btn["text"] = str(date.day) 233 | btn["extraArgs"] = [date.day] 234 | btn["state"] = DGG.NORMAL if date.month == self['month'] else DGG.DISABLED 235 | now = datetime.datetime.now() 236 | isToday = now.day == date.day and now.month == date.month and now.year == date.year 237 | if isToday: 238 | # the current day 239 | btn["frameColor"] = self['todayFrameColor'] 240 | if self['day'] == date.day: 241 | # the selected one 242 | btn["frameColor"] = self['activeDayFrameColor'] 243 | elif not isToday: 244 | btn["frameColor"] = self['normalDayFrameColor'] 245 | 246 | # update week numbers 247 | if column == 0: 248 | weeknumber = datetime.date(date.year, date.month, date.day).isocalendar()[1] 249 | self.weekNumbers[row]["text"] = str(weeknumber) 250 | 251 | for row in range(1, 7): 252 | r = row - 1 253 | column = 0 254 | if datesPrev and r == 0: 255 | for date in datesPrev[-1]: 256 | updateInfo(r, column, date) 257 | column += 1 258 | 259 | elif len(dates) > r - (1 if datesPrev else 0): 260 | hadPrev = 1 if datesPrev else 0 261 | for date in dates[r - hadPrev]: 262 | updateInfo(r, column, date) 263 | column += 1 264 | 265 | elif datesNext: 266 | idx = 1 267 | if dates[-1][-1].month == self['month']: 268 | idx = 0 269 | 270 | for date in datesNext[idx]: 271 | updateInfo(r, column, date) 272 | column += 1 273 | 274 | -------------------------------------------------------------------------------- /DirectGuiExtension/DirectDiagram.py: -------------------------------------------------------------------------------- 1 | """This module contains the DirectDiagram class.""" 2 | 3 | __all__ = ['DirectDiagram'] 4 | 5 | import math 6 | from panda3d.core import * 7 | from direct.gui import DirectGuiGlobals as DGG 8 | from direct.gui.DirectFrame import DirectFrame 9 | from direct.gui.DirectLabel import DirectLabel 10 | from . import DirectGuiHelper as DGH 11 | from direct.directtools.DirectGeometry import LineNodePath 12 | 13 | class DirectDiagram(DirectFrame): 14 | 15 | def __init__(self, parent = None, **kw): 16 | optiondefs = ( 17 | # Define type of DirectGuiWidget 18 | ('data', [], self.refresh), 19 | ('numPosSteps', 0, self.refresh), 20 | ('numPosStepsStep', 1, self.refresh), 21 | ('numNegSteps', 0, self.refresh), 22 | ('numNegStepsStep', 1, self.refresh), 23 | ('numtextScale', 0.05, self.refresh), 24 | ('showDataNumbers', False, self.refresh), 25 | ('dataNumtextScale',0.05, self.refresh), 26 | ('stepAccuracy', 2, self.refresh), 27 | ('stepFormat', float, self.refresh), 28 | ('numberAreaWidth', 0.15, self.refresh), 29 | #('numStates', 1, None), 30 | #('state', DGG.NORMAL, None), 31 | ("frameSize", (-0.5, 0.5, -0.5, 0.5), self.setFrameSize) 32 | ) 33 | # Merge keyword options with default options 34 | self.defineoptions(kw, optiondefs) 35 | 36 | self.lines = None 37 | self.measureLines = None 38 | self.centerLine = None 39 | self.xDescriptions = [] 40 | self.points = [] 41 | 42 | # Initialize superclasses 43 | DirectFrame.__init__(self, parent) 44 | 45 | # Call option initialization functions 46 | self.initialiseoptions(DirectDiagram) 47 | 48 | self.refresh() 49 | 50 | def setData(self, data): 51 | self["data"] = [float(value) for value in data] 52 | self.refresh() 53 | 54 | def refresh(self): 55 | # sanity check so we don't get here to early 56 | if not hasattr(self, "bounds"): return 57 | self.frameInitialiseFunc() 58 | 59 | textLeftSizeArea = self['numberAreaWidth'] 60 | # get the left and right edge of our frame 61 | left = DGH.getRealLeft(self) 62 | right = DGH.getRealRight(self) 63 | diagramLeft = left + textLeftSizeArea 64 | 65 | # If there is no data we can not calculate 'numPosSteps' and 'numNegSteps' 66 | if not self["data"] and self["numPosSteps"] <= 0: 67 | numPosSteps = 5 68 | else: 69 | numPosSteps = self['numPosSteps'] 70 | 71 | if not self["data"] and self["numNegSteps"] <= 0: 72 | numNegSteps = 5 73 | else: 74 | numNegSteps = self['numNegSteps'] 75 | 76 | xStep = (DGH.getRealWidth(self) - textLeftSizeArea) / max(1, len(self['data'])-1) 77 | posYRes = numPosSteps if numPosSteps > 0 else int(max(self['data'])) 78 | posYRes = DGH.getRealTop(self) / (posYRes if posYRes != 0 else 1) 79 | negYRes = -numNegSteps if numNegSteps > 0 else int(min(self['data'])) 80 | negYRes = DGH.getRealBottom(self) / (negYRes if negYRes != 0 else 1) 81 | 82 | # remove old content 83 | if self.lines is not None: 84 | self.lines.removeNode() 85 | 86 | if self.measureLines is not None: 87 | self.measureLines.removeNode() 88 | 89 | if self.centerLine is not None: 90 | self.centerLine.removeNode() 91 | 92 | for text in self.xDescriptions: 93 | text.removeNode() 94 | self.xDescriptions = [] 95 | 96 | for text in self.points: 97 | text.removeNode() 98 | self.points = [] 99 | 100 | # prepare the line drawings 101 | self.lines = LineNodePath(parent=self, thickness=3.0, colorVec=(1, 0, 0, 1)) 102 | self.measureLines = LineNodePath(parent=self, thickness=1.0, colorVec=(0, 0, 0, 1)) 103 | self.centerLine = LineNodePath(parent=self, thickness=2.0, colorVec=(0, 0, 0, 1)) 104 | 105 | # draw the center line 106 | self.centerLine.reset() 107 | self.centerLine.drawLines([((diagramLeft, 0, 0), (right, 0, 0))]) 108 | self.centerLine.create() 109 | 110 | self.xDescriptions.append( 111 | self.createcomponent( 112 | 'value0', (), None, 113 | DirectLabel, (self,), 114 | text = "0", 115 | text_scale = self['numtextScale'], 116 | text_align = TextNode.ARight, 117 | pos = (diagramLeft, 0, -0.01), 118 | relief = None, 119 | state = 'normal')) 120 | 121 | # calculate the positive measure lines and add the numbers 122 | measureLineData = [] 123 | numSteps = (numPosSteps if numPosSteps > 0 else math.floor(max(self['data']))) + 1 124 | for i in range(1, numSteps, self['numPosStepsStep']): 125 | measureLineData.append( 126 | ( 127 | (diagramLeft, 0, i*posYRes), 128 | (right, 0, i*posYRes) 129 | ) 130 | ) 131 | 132 | calcBase = 1 / DGH.getRealTop(self) 133 | maxData = numPosSteps if numPosSteps > 0 else max(self['data']) 134 | value = self['stepFormat'](round(i * posYRes * calcBase * maxData, self['stepAccuracy'])) 135 | y = i*posYRes 136 | self.xDescriptions.append( 137 | self.createcomponent( 138 | 'value{}'.format(value), (), None, 139 | DirectLabel, (self,), 140 | text = str(value), 141 | text_scale = self['numtextScale'], 142 | text_align = TextNode.ARight, 143 | pos = (diagramLeft, 0, y-0.025), 144 | relief = None, 145 | state = 'normal')) 146 | 147 | # calculate the negative measure lines and add the numbers 148 | numSteps = (numNegSteps if numNegSteps > 0 else math.floor(abs(min(self['data'])))) + 1 149 | for i in range(1, numSteps, self['numNegStepsStep']): 150 | measureLineData.append( 151 | ( 152 | (diagramLeft, 0, -i*negYRes), 153 | (right, 0, -i*negYRes) 154 | ) 155 | ) 156 | 157 | calcBase = 1 / DGH.getRealBottom(self) 158 | maxData = numPosSteps if numPosSteps > 0 else max(self['data']) 159 | value = self['stepFormat'](round(i * negYRes * calcBase * maxData, self['stepAccuracy'])) 160 | y = -i*negYRes 161 | self.xDescriptions.append( 162 | self.createcomponent( 163 | 'value{}'.format(value), (), None, 164 | DirectLabel, (self,), 165 | text = str(value), 166 | text_scale = self['numtextScale'], 167 | text_align = TextNode.ARight, 168 | pos = (diagramLeft, 0, y+0.01), 169 | relief = None, 170 | state = 'normal')) 171 | 172 | # Draw the lines 173 | self.measureLines.reset() 174 | self.measureLines.drawLines(measureLineData) 175 | self.measureLines.create() 176 | 177 | lineData = [] 178 | for i in range(1, len(self['data'])): 179 | yResA = posYRes if self['data'][i-1] >= 0 else negYRes 180 | yResB = posYRes if self['data'][i] >= 0 else negYRes 181 | lineData.append( 182 | ( 183 | # Point A 184 | (diagramLeft+(i-1)*xStep, 0, self['data'][i-1] * yResA), 185 | # Point B 186 | (diagramLeft+i*xStep, 0, self['data'][i] * yResB) 187 | ) 188 | ) 189 | 190 | if (self['showDataNumbers']): 191 | value = round(self['data'][i-1], self['stepAccuracy']) 192 | self.points.append( 193 | self.createcomponent( 194 | 'value{}'.format(value), (), None, 195 | DirectLabel, (self,), 196 | text = str(value), 197 | text_scale = self['dataNumtextScale'], 198 | text_align = TextNode.ARight, 199 | pos = (diagramLeft+(i-1)*xStep, 0, self['data'][i-1] * yResA), 200 | relief = None, 201 | state = 'normal')) 202 | 203 | # Draw the lines 204 | self.lines.reset() 205 | self.lines.drawLines(lineData) 206 | self.lines.create() 207 | -------------------------------------------------------------------------------- /DirectGuiExtension/DirectGridSizer.py: -------------------------------------------------------------------------------- 1 | """This module contains the DirectBoxSizer class.""" 2 | 3 | __all__ = ['DirectGridSizer'] 4 | 5 | from panda3d.core import * 6 | from direct.gui import DirectGuiGlobals as DGG 7 | from direct.gui.DirectFrame import DirectFrame 8 | from . import DirectGuiHelper as DGH 9 | 10 | 11 | class DirectItemContainer(): 12 | def __init__(self, element, rowIndex, columnIndex, widthInColumns, heightInRows): 13 | self.element = element 14 | self.rowIndex = rowIndex 15 | self.columnIndex = columnIndex 16 | self.widthInColumns = widthInColumns 17 | self.heightInRows = heightInRows 18 | 19 | class DirectGridSizer(DirectFrame): 20 | """ 21 | A frame to add multiple other directGui elements to that will then be 22 | automatically be placed stacked next to each other. 23 | """ 24 | def __init__(self, parent = None, **kw): 25 | self.skipInitRefresh = True 26 | optiondefs = ( 27 | # Define type of DirectGuiWidget 28 | ('items', [], self.refresh), 29 | ('pgFunc', PGItem, None), 30 | ('numStates', 1, None), 31 | ('state', DGG.NORMAL, None), 32 | ('borderWidth', (0, 0), self.setBorderWidth), 33 | 34 | ('itemMargin', (0,0,0,0), self.refresh), 35 | ('numRows', 4, self.refresh), 36 | ('numColumns', 4, self.refresh), 37 | ('autoUpdateFrameSize', True, None), 38 | ('boxAlign', TextNode.ALeft, self.refresh), 39 | 40 | ('suppressMouse', 0, None), 41 | ) 42 | # Merge keyword options with default options 43 | self.defineoptions(kw, optiondefs) 44 | 45 | # Initialize superclasses 46 | DirectFrame.__init__(self, parent) 47 | 48 | # Call option initialization functions 49 | self.initialiseoptions(DirectGridSizer) 50 | 51 | self.skipInitRefresh = False 52 | # initialize once at the end 53 | self.refresh() 54 | 55 | def addItem(self, element, row, column, widthInColumns=1, heightInRows=1): 56 | """ 57 | Adds the given item to this panel stack 58 | """ 59 | element.reparentTo(self) 60 | container = DirectItemContainer(element, row, column, widthInColumns, heightInRows) 61 | self["items"].append(container) 62 | self.refresh() 63 | 64 | def removeItem(self, element): 65 | """ 66 | Remove this item from the panel 67 | """ 68 | for item in self["items"]: 69 | if element == item.element: 70 | self["items"].remove(item) 71 | self.refresh() 72 | return 1 73 | return 0 74 | 75 | def clearItems(self): 76 | for item in self["items"][:]: 77 | self["items"].remove(item) 78 | self.refresh() 79 | 80 | def refresh(self): 81 | """ 82 | Recalculate the position of every item in this panel and set the frame- 83 | size of the panel accordingly if auto update is enabled. 84 | """ 85 | # sanity checks so we don't get here to early 86 | if self.skipInitRefresh: return 87 | if not hasattr(self, "bounds"): return 88 | 89 | for item in self["items"]: 90 | item.element.frameInitialiseFunc() 91 | 92 | # variables to store the maximum row and column widths 93 | rowHeights = [0]*self["numRows"] 94 | columnWidths = [0]*self["numColumns"] 95 | 96 | b_top = 0 97 | b_bottom = 0 98 | b_left = 0 99 | b_right = 0 100 | 101 | pad_x = self["pad"][0] 102 | pad_y = self["pad"][1] 103 | 104 | margin_left = self["itemMargin"][0] 105 | margin_right = self["itemMargin"][1] 106 | margin_bottom = self["itemMargin"][2] 107 | margin_top = self["itemMargin"][3] 108 | 109 | if self["autoUpdateFrameSize"]: 110 | self["frameSize"] = (0, 0, 0, 0) 111 | 112 | # get the max row and column sizes 113 | for r in range(self["numRows"]): 114 | for c in range(self["numColumns"]): 115 | 116 | for item in self["items"]: 117 | if item.rowIndex == r and item.columnIndex == c: 118 | rowHeights[r] = max(rowHeights[r], DGH.getRealHeight(item.element) / item.heightInRows + margin_bottom + margin_top) 119 | columnWidths[c] = max(columnWidths[c], DGH.getRealWidth(item.element) / item.widthInColumns + margin_left + margin_right) 120 | break 121 | 122 | for item in self["items"]: 123 | r = item.rowIndex 124 | c = item.columnIndex 125 | 126 | if r >= self["numRows"]: 127 | raise IndexError(f"Row index defined in item {item.element} exceeded number of rows in grid sizer: numRows={self['numRows']}") 128 | if c >= self["numColumns"]: 129 | raise IndexError(f"Column index defined in item {item.element} exceeded number of columns in grid sizer: numColumns={self['numColumns']}") 130 | 131 | z = 0 132 | for i in range(r): 133 | z -= rowHeights[i] 134 | 135 | x = 0 136 | for i in range(c): 137 | x += columnWidths[i] 138 | 139 | item.element.setPos(pad_x + x + margin_left, 0, pad_y + z + margin_top) 140 | 141 | if self["autoUpdateFrameSize"]: 142 | 143 | for item in self["items"]: 144 | b_left = min(b_left, DGH.getRealLeft(item.element) + item.element.getX()) 145 | b_right = max(b_right, DGH.getRealRight(item.element) + item.element.getX()) 146 | 147 | b_bottom = min(b_bottom, DGH.getRealBottom(item.element) + item.element.getZ()) 148 | b_top = max(b_top, DGH.getRealTop(item.element) + item.element.getZ()) 149 | 150 | self["frameSize"] = [b_left+pad_x, b_right+pad_x, b_bottom+pad_y, b_top+pad_y] 151 | 152 | xShift = 0 153 | if self["boxAlign"] == TextNode.ACenter: 154 | self["frameSize"][0] = self["frameSize"][0] - self["frameSize"][1]/2 155 | self["frameSize"][1] = self["frameSize"][1]/2 156 | xShift = -self["frameSize"][1]/2 157 | if self["boxAlign"] == TextNode.ARight: 158 | self["frameSize"][0] = self["frameSize"][1] 159 | self["frameSize"][1] = self["frameSize"][0] 160 | xShift = -self["frameSize"][1] 161 | -------------------------------------------------------------------------------- /DirectGuiExtension/DirectGuiHelper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __author__ = "Fireclaw the Fox" 4 | __license__ = """ 5 | Simplified BSD (BSD 2-Clause) License. 6 | See License.txt or http://opensource.org/licenses/BSD-2-Clause for more info 7 | """ 8 | 9 | from panda3d import core as p3d 10 | 11 | 12 | def getBorderSize(guiItem): 13 | frameType = guiItem.getFrameType() 14 | if guiItem["frameSize"] is not None \ 15 | or frameType == p3d.PGFrameStyle.TNone \ 16 | or frameType == p3d.PGFrameStyle.TFlat: 17 | return (0,0) 18 | return guiItem["borderWidth"] 19 | 20 | def getBounds(guiItem): 21 | #HACK: Sometimes getBounds returns 0s while a frameSize is actually given 22 | if guiItem.bounds == [0,0,0,0] and guiItem['frameSize'] != (0,0,0,0): 23 | if guiItem['frameSize'] is not None: 24 | return guiItem['frameSize'] 25 | else: 26 | if guiItem.guiItem.getFrame() is not None: 27 | return guiItem.guiItem.getFrame() 28 | # well... seems like this element just has no size. 29 | return guiItem.bounds 30 | return guiItem.bounds 31 | 32 | def getRealWidth(guiItem): 33 | guiItem.resetFrameSize() 34 | width = guiItem.getWidth() 35 | if width == 0 and guiItem["frameSize"] is not None: 36 | width = abs(guiItem["frameSize"][0] - guiItem["frameSize"][1]) + 2 * getBorderSize(guiItem)[0] 37 | return width 38 | return (guiItem.getWidth() + 2 * getBorderSize(guiItem)[0]) * guiItem.getScale()[0] 39 | 40 | def getRealHeight(guiItem): 41 | guiItem.resetFrameSize() 42 | height = guiItem.getHeight() 43 | if height == 0 and guiItem["frameSize"] is not None: 44 | height = abs(guiItem["frameSize"][2] - guiItem["frameSize"][3]) + 2 * getBorderSize(guiItem)[0] 45 | return height 46 | return (guiItem.getHeight() + 2 * getBorderSize(guiItem)[1]) * guiItem.getScale()[1] 47 | 48 | def getRealLeft(guiItem): 49 | return (getBounds(guiItem)[0] - getBorderSize(guiItem)[0]) * guiItem.getScale()[0] 50 | 51 | def getRealRight(guiItem): 52 | return (getBounds(guiItem)[1] + getBorderSize(guiItem)[0]) * guiItem.getScale()[0] 53 | 54 | def getRealTop(guiItem): 55 | return (getBounds(guiItem)[3] + getBorderSize(guiItem)[1]) * guiItem.getScale()[1] 56 | 57 | def getRealBottom(guiItem): 58 | return (getBounds(guiItem)[2] - getBorderSize(guiItem)[1]) * guiItem.getScale()[1] 59 | -------------------------------------------------------------------------------- /DirectGuiExtension/DirectMenuBar.py: -------------------------------------------------------------------------------- 1 | """Implements a pop-up menu containing multiple clickable options and sub-menus.""" 2 | 3 | __all__ = ['DirectMenuBar'] 4 | 5 | from panda3d.core import * 6 | from direct.gui import DirectGuiGlobals as DGG 7 | from direct.gui.DirectButton import * 8 | from direct.gui.DirectLabel import * 9 | from direct.gui.DirectFrame import * 10 | from .DirectBoxSizer import DirectBoxSizer 11 | from .DirectAutoSizer import DirectAutoSizer 12 | from . import DirectGuiHelper as DGH 13 | 14 | class DirectMenuBar(DirectBoxSizer): 15 | def __init__(self, parent = None, **kw): 16 | optiondefs = ( 17 | # List of items to display on the popup menu 18 | ('menuItems', [], self.setItems), 19 | ) 20 | # Merge keyword options with default options 21 | self.defineoptions(kw, optiondefs) 22 | 23 | # Initialize superclasses 24 | DirectBoxSizer.__init__(self, parent, **kw) 25 | # Call option initialization functions 26 | self.initialiseoptions(DirectMenuBar) 27 | 28 | self.selected_menu_item = None 29 | 30 | if len(self["menuItems"]) > 0: 31 | self.setItems() 32 | 33 | def setItems(self): 34 | self.removeAllItems() 35 | 36 | for item in self["menuItems"]: 37 | self.addItem(item, skipRefresh=True) 38 | item.onCloseMenuFunc = self.setSelectedMenuItem 39 | item.bind(DGG.B1PRESS, self.showPopupMenuItem, [item]) 40 | item.bind(DGG.WITHIN, self.toggleMenuItem, [item]) 41 | item.cancelFrame.bind(DGG.B1PRESS, self.hidePopupMenu, extraArgs=[item]) 42 | self.refresh() 43 | 44 | def setSelectedMenuItem(self, item=None): 45 | self.selected_menu_item = item 46 | 47 | def showPopupMenuItem(self, item, event=None): 48 | item.showPopupMenu(event) 49 | self.setSelectedMenuItem(item) 50 | 51 | def toggleMenuItem(self, otherItem, args=None): 52 | if self.selected_menu_item is None \ 53 | or self.selected_menu_item == otherItem: 54 | return 55 | 56 | self.selected_menu_item.hidePopupMenu(hideParentMenu=True) 57 | otherItem.showPopupMenu() 58 | self.setSelectedMenuItem(otherItem) 59 | 60 | def hidePopupMenu(self, item, event=None): 61 | item.hidePopupMenu(event, True) 62 | self.setSelectedMenuItem() 63 | -------------------------------------------------------------------------------- /DirectGuiExtension/DirectMenuItem.py: -------------------------------------------------------------------------------- 1 | """Implements a pop-up menu containing multiple clickable options and sub-menus.""" 2 | 3 | __all__ = ['DirectMenuItem', 'DirectMenuItemEntry', 'DirectMenuItemSubMenu', 4 | 'DirectMenuSeparator'] 5 | 6 | from panda3d.core import * 7 | from direct.gui import DirectGuiGlobals as DGG 8 | from direct.gui.DirectButton import * 9 | from direct.gui.DirectLabel import * 10 | from direct.gui.DirectFrame import * 11 | from .DirectBoxSizer import DirectBoxSizer 12 | from . import DirectGuiHelper as DGH 13 | 14 | DGG.MWUP = PGButton.getPressPrefix() + MouseButton.wheel_up().getName() + '-' 15 | DGG.MWDOWN = PGButton.getPressPrefix() + MouseButton.wheel_down().getName() + '-' 16 | DGG.LEFT = "left" 17 | DGG.RIGHT = "right" 18 | DGG.ABOVE = "above" 19 | DGG.BELOW = "below" 20 | 21 | class DirectMenuItemEntry: 22 | def __init__(self, text, command, extraArgs=None): 23 | self.text = text 24 | self.command = command 25 | self.extraArgs = extraArgs 26 | 27 | class DirectMenuItemSubMenu: 28 | def __init__(self, text, items): 29 | self.text = text 30 | self.items = items 31 | 32 | class DirectMenuSeparator: 33 | def __init__(self, height=0.05, padding=(0, 0.1)): 34 | self.height = height 35 | self.padding = padding 36 | 37 | class DirectMenuItem(DirectButton): 38 | def __init__(self, parent = None, **kw): 39 | # this function will be called when the menu gets closed 40 | self.onCloseMenuFunc = None 41 | optiondefs = ( 42 | # List of items to display on the popup menu 43 | ('items', [], self.setItems), 44 | # The position of the popup menu 45 | # possible positions: left, above, right, below 46 | ('popupMenuLocation', DGG.BELOW, None), 47 | # Background color to use to highlight popup menu items 48 | ('highlightColor', (.5, .5, .5, 1), None), 49 | # Background color of unhighlighted menu items 50 | ('itemFrameColor', None, None), 51 | # Alignment to use for text on popup menu button 52 | # Changing this breaks button layout 53 | ('text_align', TextNode.ALeft, None), 54 | # Remove press effect because it looks a bit funny 55 | ('pressEffect', 0, DGG.INITOPT), 56 | ('isSubMenu', False, None), 57 | ('parentMenu', None, None), 58 | # color of the separator line 59 | ('separatorFrameColor', (.2, .2, .2, 1), None), 60 | ) 61 | self.kw_args_copy = kw.copy() 62 | # Merge keyword options with default options 63 | self.defineoptions(kw, optiondefs) 64 | # Initialize superclasses 65 | DirectButton.__init__(self, parent) 66 | # This is created when you set the menu's items 67 | self.popupMenu = None 68 | # the selected item 69 | self.highlightedItem = None 70 | # A big screen encompassing frame to catch the cancel clicks 71 | self.cancelFrame = self.createcomponent( 72 | 'cancelframe', (), None, 73 | DirectFrame, (self,), 74 | frameSize = (-1, 1, -1, 1), 75 | relief = None, 76 | state = 'normal') 77 | # Make sure this is on top of all the other widgets 78 | self.cancelFrame.setBin('gui-popup', 0) 79 | self.cancelFrame.node().setBounds(OmniBoundingVolume()) 80 | self.cancelFrame.bind(DGG.B1PRESS, self.hidePopupMenu, extraArgs=[True]) 81 | # Default action on press is to show popup menu 82 | self.bind(DGG.B1PRESS, self.showPopupMenu) 83 | # Check if item is highlighted on release and select it if it is 84 | self.bind(DGG.B1RELEASE, self._selectHighlighted) 85 | # Call option initialization functions 86 | self.initialiseoptions(DirectMenuItem) 87 | # Need to call this since we explicitly set frame size 88 | self.resetFrameSize() 89 | 90 | def setItems(self): 91 | """ 92 | self['items'] = list of DirectMenuItemEntry and DirectMenuItemSubMenu 93 | Create new popup menu to reflect specified set of items 94 | """ 95 | # Remove old component if it exits 96 | if self.popupMenu != None: 97 | self.destroycomponent('popupMenu') 98 | # Create new component 99 | self.popupMenu = self.createcomponent('popupMenu', (), None, 100 | DirectBoxSizer, 101 | (self,), 102 | itemAlign = DirectBoxSizer.A_Left, 103 | orientation = DGG.VERTICAL, 104 | ) 105 | # Make sure it is on top of all the other gui widgets 106 | self.popupMenu.setBin('gui-popup', 0) 107 | if not self['items']: 108 | return 109 | # Create a new component for each item 110 | # Find the maximum extents of all items 111 | itemIndex = 0 112 | self.minX = self.maxX = self.minZ = self.maxZ = None 113 | for item in self['items']: 114 | if type(item) is DirectMenuItemSubMenu: 115 | subMenuKW = self.kw_args_copy.copy() 116 | toRemove = ["text", "popupMenuLocation", "items", "isSubMenu", "parentMenu", "frameSize", "scale"] 117 | for r in toRemove: 118 | if r in subMenuKW: 119 | del subMenuKW[r] 120 | 121 | c = self.createcomponent( 122 | 'item%d' % itemIndex, (), 'item', 123 | DirectMenuItem, 124 | (self.popupMenu,), 125 | text=item.text, 126 | popupMenuLocation=DGG.RIGHT, 127 | items=item.items, 128 | isSubMenu=True, 129 | parentMenu=self, 130 | **subMenuKW, 131 | ) 132 | elif type(item) is DirectMenuSeparator: 133 | c = self.createcomponent( 134 | 'separator%d' % itemIndex, (), 'separator', 135 | DirectFrame, 136 | (self.popupMenu,), 137 | frameColor=self["separatorFrameColor"] if self["separatorFrameColor"] else self["frameColor"], 138 | # set width to 0, we'll fit it to the width of the box later 139 | frameSize=(0, 0, -item.height/2, item.height/2), 140 | pad=item.padding, 141 | ) 142 | else: 143 | c = self.createcomponent( 144 | 'item%d' % itemIndex, (), 'item', 145 | DirectButton, 146 | (self.popupMenu,), 147 | text=item.text, 148 | text_align=TextNode.ALeft, 149 | command=item.command, 150 | extraArgs=item.extraArgs, 151 | frameColor=self["itemFrameColor"] if self["itemFrameColor"] else self["frameColor"]) 152 | 153 | c.bind(DGG.B1RELEASE, self.hidePopupMenu, extraArgs=[True]) 154 | bounds = DGH.getBounds(c) 155 | 156 | c.resetFrameSize() 157 | 158 | self.popupMenu.addItem(c) 159 | 160 | bw_w = c["borderWidth"][0] + c["pad"][0] 161 | bw_h = c["borderWidth"][1] + c["pad"][1] 162 | 163 | # accept events only for actual selectable elements 164 | if type(item) is not DirectMenuSeparator: 165 | self.minX = min(self.minX if self.minX else bounds[0]-bw_w, bounds[0]-bw_w) 166 | self.maxX = max(self.maxX if self.maxX else bounds[1]+bw_w, bounds[1]+bw_w) 167 | self.minZ = min(self.minZ if self.minZ else bounds[2]-bw_h, bounds[2]-bw_h) 168 | self.maxZ = max(self.maxZ if self.maxZ else bounds[3]+bw_h, bounds[3]+bw_h) 169 | 170 | # Highlight background when mouse is in item 171 | c.bind(DGG.WITHIN, 172 | lambda x, item=c: self._highlightItem(item)) 173 | # Restore specified color upon exiting 174 | fc = self['itemFrameColor'] if self['itemFrameColor'] else c['frameColor'] 175 | c.bind(DGG.WITHOUT, 176 | lambda x, item=c, fc=fc: self._unhighlightItem(item, fc)) 177 | c.bind(DGG.MWDOWN, self.scrollPopUpMenu, [-1]) 178 | c.bind(DGG.MWUP, self.scrollPopUpMenu, [1]) 179 | 180 | itemIndex += 1 181 | 182 | # Calc max width and height 183 | if self.minX is None: 184 | self.minX = 0 185 | self.maxX = 1 186 | self.minZ = 0 187 | self.maxZ = 1 188 | self.maxWidth = self.maxX - self.minX 189 | self.maxHeight = self.maxZ - self.minZ 190 | # Adjust frame size for each item and bind actions to mouse events 191 | for i in self.popupMenu["items"]: 192 | item = i.element 193 | if type(item) is DirectFrame: 194 | fs = item["frameSize"] 195 | item['frameSize'] = (self.minX, self.maxX, fs[2], fs[3]) 196 | else: 197 | # make all entries the same size 198 | item['frameSize'] = (self.minX, self.maxX, self.minZ, self.maxZ) 199 | 200 | # HACK: Set the user defined popup menu relief here so we don't 201 | # break the bounds calculation. 202 | self.popupMenu.setRelief(self['popupMenu_relief']) 203 | 204 | # Set initial state 205 | self.hidePopupMenu() 206 | self.bind(DGG.MWDOWN, self.scrollPopUpMenu, [-1]) 207 | self.bind(DGG.MWUP, self.scrollPopUpMenu, [1]) 208 | self.cancelFrame.bind(DGG.MWDOWN, self.scrollPopUpMenu, [-1]) 209 | self.cancelFrame.bind(DGG.MWUP, self.scrollPopUpMenu, [1]) 210 | 211 | #self.popupMenu.refresh() 212 | 213 | def showPopupMenu(self, event = None): 214 | """ 215 | Make popup visible. 216 | Adjust popup position if default position puts it outside of 217 | visible screen region 218 | """ 219 | 220 | # Needed attributes (such as minZ) won't be set unless the user has specified 221 | # items to display. Let's assert that we've given items to work with. 222 | items = self['items'] 223 | assert items and len(items) > 0, 'Cannot show an empty popup menu! You must add items!' 224 | 225 | # Show the menu 226 | self.popupMenu.show() 227 | # Compute bounds 228 | b = DGH.getBounds(self) #self.getBounds() 229 | fb = DGH.getBounds(self.popupMenu) #self.popupMenu.getBounds() 230 | self.popupMenu["itemAlign"] = DirectBoxSizer.A_Left 231 | 232 | if self['popupMenuLocation'] == DGG.RIGHT: 233 | # Position menu to the right of the menu 234 | self.popupMenu.setX( 235 | DGH.getRealRight(self) / self.getScale()[0] 236 | + -DGH.getRealLeft(self.popupMenu)) 237 | elif self['popupMenuLocation'] == DGG.LEFT: 238 | # Position to the left 239 | # change the item align to right aligned so it will be fitted 240 | # to the left of the menu 241 | self.popupMenu["itemAlign"] = DirectBoxSizer.A_Right 242 | # reposition the popup to the left 243 | self.popupMenu.setX(-DGH.getRealRight(self.popupMenu)) 244 | else: 245 | # position to line up with the left edge if the menu is above or below 246 | self.popupMenu.setX(-DGH.getRealLeft(self.popupMenu)) 247 | 248 | if self['popupMenuLocation'] == DGG.ABOVE: 249 | # Try to set height to line up selected item with button 250 | self.popupMenu.setZ( 251 | self, self.maxZ - fb[2]) 252 | elif self['popupMenuLocation'] == DGG.BELOW: 253 | # Try to set height to line up selected item with button 254 | self.popupMenu.setZ( 255 | self, self.minZ+self["item_pad"][0]*2) 256 | else: 257 | # Try to set height to line up selected item with button 258 | self.popupMenu.setZ( 259 | self, self.maxZ) 260 | # Make sure the whole popup menu is visible 261 | pos = self.popupMenu.getPos(render2d) 262 | scale = self.popupMenu.getScale(render2d) 263 | # How are we doing relative to the right side of the screen 264 | maxX = pos[0] + fb[1] * scale[0] 265 | if maxX > 1.0: 266 | # Need to move menu to the left 267 | self.popupMenu.setX(render2d, pos[0] + (1.0 - maxX)) 268 | # How are we doing relative to the right side of the screen 269 | minX = pos[0] 270 | if minX < -1.0: 271 | # Need to move menu to the right 272 | self.popupMenu.setX(render2d, -1 ) 273 | # How about up and down? 274 | minZ = pos[2] + fb[2] * scale[2] 275 | maxZ = pos[2] + fb[3] * scale[2] 276 | if minZ < -1.0: 277 | # Menu too low, move it up 278 | self.popupMenu.setZ(render2d, pos[2] + (-1.0 - minZ)) 279 | 280 | # recheck the top position once repositioned 281 | pos = self.popupMenu.getPos(render2d) 282 | maxZ = pos[2] + fb[3] * scale[2] 283 | if maxZ > 1.0: 284 | # Menu too large to show on screen entirely 285 | # Try to set height to line up selected item with button 286 | self.popupMenu.setZ( 287 | self, self.minZ) 288 | elif maxZ > 1.0: 289 | # Menu too high, move it down 290 | self.popupMenu.setZ(render2d, pos[2] + (1.0 - maxZ)) 291 | # recheck the top position once repositioned 292 | pos = self.popupMenu.getPos(render2d) 293 | minZ = pos[2] + fb[2] * scale[2] 294 | if minZ < -1.0: 295 | # Menu too large to show on screen entirely 296 | # Try to set height to line up selected item with button 297 | self.popupMenu.setZ( 298 | self, self.minZ) 299 | # Also display cancel frame to catch clicks outside of the popup 300 | self.cancelFrame.show() 301 | # Position and scale cancel frame to fill entire window 302 | self.cancelFrame.setPos(render2d, 0, 0, 0) 303 | self.cancelFrame.setScale(render2d, 1, 1, 1) 304 | 305 | #self.popupMenu.refresh() 306 | 307 | def hidePopupMenu(self, event = None, hideParentMenu=False, callOnClose=True): 308 | """ Put away popup and cancel frame """ 309 | self.popupMenu.hide() 310 | self.cancelFrame.hide() 311 | 312 | # call up the ancestry tree 313 | if hideParentMenu: 314 | if self['isSubMenu']: 315 | self['parentMenu'].hidePopupMenu(hideParentMenu=True) 316 | 317 | if self.onCloseMenuFunc and callOnClose: 318 | self.onCloseMenuFunc() 319 | 320 | def scrollPopUpMenu(self, direction, event = None): 321 | """ Scroll the item frame up and down depending on the direction 322 | which must be a nummeric value. A positive value will scroll up 323 | while a negative value will scroll down. It will only work if 324 | items are out of bounds of the window """ 325 | 326 | #TODO: If the mouse is over a SubMenu, close it to make sure the menu under the mouse is scrolled, not any sub menus 327 | # Also, need to check since we don't want to close the upper levels of menus. (Currently this code closses all) 328 | #if self.highlightedItem: 329 | # #if self.highlightedItem.guiItem.get_state() == 2: 330 | # if self.highlightedItem.guiItem.within_region(base.mouseWatcherNode): 331 | # self.hidePopupMenu(callOnClose=False) 332 | 333 | fb = DGH.getBounds(self.popupMenu)#.getBounds() 334 | pos = self.popupMenu.getPos(render2d) 335 | scale = self.popupMenu.getScale(render2d) 336 | 337 | minZ = pos[2] + fb[2] * scale[2] 338 | maxZ = pos[2] + fb[3] * scale[2] 339 | if (minZ < -1.0 and direction < 0) or (maxZ > 1.0 and direction > 0): 340 | oldZ = self.popupMenu.getZ() 341 | self.popupMenu.setZ(oldZ - direction * self.maxHeight) 342 | 343 | def _selectHighlighted(self, event=None): 344 | """ 345 | Check to see if item is highlighted (by cursor being within 346 | that item). If so, selected it. If not, do nothing 347 | """ 348 | if self.highlightedItem: 349 | # Pass any extra args to command 350 | self.highlightedItem['command'](*self.highlightedItem['extraArgs']) 351 | self.hidePopupMenu(hideParentMenu=True) 352 | 353 | def _highlightItem(self, item): 354 | """ Set frame color of highlighted item, record index """ 355 | 356 | for i in self.popupMenu["items"]: 357 | # make sure all other items are unhighlighted 358 | base.messenger.send(DGG.WITHOUT + i.element.guiId, [""]) 359 | 360 | item['frameColor'] = self['highlightColor'] 361 | self.highlightedItem = item 362 | 363 | if type(item) is DirectMenuItem: 364 | if item['isSubMenu']: 365 | taskMgr.doMethodLater(0.2, item.showPopupMenu, "highlight") 366 | 367 | def _unhighlightItem(self, item, frameColor): 368 | """ Clear frame color """ 369 | item['frameColor'] = frameColor 370 | self.highlightedItem = None 371 | 372 | if type(item) is DirectMenuItem: 373 | if item['isSubMenu']: 374 | def unhiglight(item): 375 | if not item.highlightedItem: 376 | item.hidePopupMenu() 377 | taskMgr.doMethodLater(0.2, unhiglight, "unhighlight", extraArgs=[item]) 378 | -------------------------------------------------------------------------------- /DirectGuiExtension/DirectOptionMenu.py: -------------------------------------------------------------------------------- 1 | """Implements a pop-up menu containing multiple clickable options.""" 2 | 3 | __all__ = ['DirectOptionMenu'] 4 | 5 | from panda3d.core import * 6 | from direct.gui import DirectGuiGlobals as DGG 7 | #import DirectGuiGlobalsExtra as DGG #<- TODO: Why doesn't this work as expected 8 | DGG.MWUP = PGButton.getPressPrefix() + MouseButton.wheel_up().getName() + '-' 9 | DGG.MWDOWN = PGButton.getPressPrefix() + MouseButton.wheel_down().getName() + '-' 10 | DGG.LEFT = "left" 11 | DGG.RIGHT = "right" 12 | DGG.ABOVE = "above" 13 | DGG.BELOW = "below" 14 | from direct.gui.DirectButton import * 15 | from direct.gui.DirectLabel import * 16 | from direct.gui.DirectFrame import * 17 | 18 | class DirectOptionMenu(DirectButton): 19 | """ 20 | DirectOptionMenu(parent) - Create a DirectButton which pops up a 21 | menu which can be used to select from a list of items. 22 | Execute button command (passing the selected item through) if defined 23 | To cancel the popup menu click anywhere on the screen outside of the 24 | popup menu. No command is executed in this case. 25 | """ 26 | def __init__(self, parent = None, **kw): 27 | # Inherits from DirectButton 28 | optiondefs = ( 29 | # List of items to display on the popup menu 30 | ('items', [], self.setItems), 31 | # Initial item to display on menu button 32 | # Can be an integer index or the same string as the button 33 | ('initialitem', None, DGG.INITOPT), 34 | # Amount of padding to place around popup button indicator 35 | ('popupMarkerBorder', (.1, .1), None), 36 | # The initial position of the popup marker 37 | ('popupMarker_pos', None, None), 38 | # The position of the popup menu 39 | # possible positions: left, above, right, below 40 | ('popupMenuLocation', None, None), 41 | # Background color to use to highlight popup menu items 42 | ('highlightColor', (.5, .5, .5, 1), None), 43 | # Extra scale to use on highlight popup menu items 44 | ('highlightScale', (1, 1), None), 45 | # Alignment to use for text on popup menu button 46 | # Changing this breaks button layout 47 | ('text_align', TextNode.ALeft, None), 48 | # Remove press effect because it looks a bit funny 49 | ('pressEffect', 0, DGG.INITOPT), 50 | ) 51 | # Merge keyword options with default options 52 | self.defineoptions(kw, optiondefs) 53 | # Initialize superclasses 54 | DirectButton.__init__(self, parent) 55 | # Record any user specified frame size 56 | self.initFrameSize = self['frameSize'] 57 | # Create a small rectangular marker to distinguish this button 58 | # as a popup menu button 59 | self.popupMarker = self.createcomponent( 60 | 'popupMarker', (), None, 61 | DirectFrame, (self,), 62 | frameSize = (-0.5, 0.5, -0.2, 0.2), 63 | scale = 0.4, 64 | relief = DGG.RAISED) 65 | # Record any user specified popup marker position 66 | self.initPopupMarkerPos = self['popupMarker_pos'] 67 | # This needs to popup the menu too 68 | self.popupMarker.bind(DGG.B1PRESS, self.showPopupMenu) 69 | # Check if item is highlighted on release and select it if it is 70 | self.popupMarker.bind(DGG.B1RELEASE, self.selectHighlightedIndex) 71 | # Make popup marker have the same click sound 72 | if self['clickSound']: 73 | self.popupMarker.guiItem.setSound( 74 | DGG.B1PRESS + self.popupMarker.guiId, self['clickSound']) 75 | else: 76 | self.popupMarker.guiItem.clearSound(DGG.B1PRESS + self.popupMarker.guiId) 77 | # This is created when you set the menu's items 78 | self.popupMenu = None 79 | self.selectedIndex = None 80 | self.highlightedIndex = None 81 | if 'item_text_scale' in kw: 82 | self.prevItemTextScale = kw['item_text_scale'] 83 | else: 84 | self.prevItemTextScale = (1,1) 85 | # A big screen encompassing frame to catch the cancel clicks 86 | self.cancelFrame = self.createcomponent( 87 | 'cancelframe', (), None, 88 | DirectFrame, (self,), 89 | frameSize = (-1, 1, -1, 1), 90 | frameColor = (1, 0, 0, 1), 91 | relief = None, 92 | state = 'normal') 93 | # Make sure this is on top of all the other widgets 94 | self.cancelFrame.setBin('gui-popup', 0) 95 | self.cancelFrame.bind(DGG.B1PRESS, self.hidePopupMenu) 96 | # Default action on press is to show popup menu 97 | self.bind(DGG.B1PRESS, self.showPopupMenu) 98 | # Check if item is highlighted on release and select it if it is 99 | self.bind(DGG.B1RELEASE, self.selectHighlightedIndex) 100 | # Call option initialization functions 101 | self.initialiseoptions(DirectOptionMenu) 102 | # Need to call this since we explicitly set frame size 103 | self.resetFrameSize() 104 | 105 | def setItems(self): 106 | """ 107 | self['items'] = itemList 108 | Create new popup menu to reflect specified set of items 109 | """ 110 | # Remove old component if it exits 111 | if self.popupMenu != None: 112 | self.destroycomponent('popupMenu') 113 | # Create new component 114 | self.popupMenu = self.createcomponent('popupMenu', (), None, 115 | DirectFrame, 116 | (self,), 117 | relief = 'raised', 118 | ) 119 | # Make sure it is on top of all the other gui widgets 120 | self.popupMenu.setBin('gui-popup', 0) 121 | if not self['items']: 122 | return 123 | # Create a new component for each item 124 | # Find the maximum extents of all items 125 | itemIndex = 0 126 | self.minX = self.maxX = self.minZ = self.maxZ = None 127 | for item in self['items']: 128 | c = self.createcomponent( 129 | 'item%d' % itemIndex, (), 'item', 130 | DirectButton, (self.popupMenu,), 131 | text = item, text_align = TextNode.ALeft, 132 | command = lambda i = itemIndex: self.set(i)) 133 | bounds = c.getBounds() 134 | if self.minX == None: 135 | self.minX = bounds[0] 136 | elif bounds[0] < self.minX: 137 | self.minX = bounds[0] 138 | if self.maxX == None: 139 | self.maxX = bounds[1] 140 | elif bounds[1] > self.maxX: 141 | self.maxX = bounds[1] 142 | if self.minZ == None: 143 | self.minZ = bounds[2] 144 | elif bounds[2] < self.minZ: 145 | self.minZ = bounds[2] 146 | if self.maxZ == None: 147 | self.maxZ = bounds[3] 148 | elif bounds[3] > self.maxZ: 149 | self.maxZ = bounds[3] 150 | itemIndex += 1 151 | # Calc max width and height 152 | self.maxWidth = self.maxX - self.minX 153 | self.maxHeight = self.maxZ - self.minZ 154 | # Adjust frame size for each item and bind actions to mouse events 155 | for i in range(itemIndex): 156 | item = self.component('item%d' %i) 157 | # So entire extent of item's slot on popup is reactive to mouse 158 | item['frameSize'] = (self.minX, self.maxX, self.minZ, self.maxZ) 159 | # Move it to its correct position on the popup 160 | item.setPos(-self.minX, 0, -self.maxZ - i * self.maxHeight) 161 | item.bind(DGG.B1RELEASE, self.hidePopupMenu) 162 | # Highlight background when mouse is in item 163 | item.bind(DGG.WITHIN, 164 | lambda x, i=i, item=item:self._highlightItem(item, i)) 165 | # Restore specified color upon exiting 166 | fc = item['frameColor'] 167 | item.bind(DGG.WITHOUT, 168 | lambda x, item=item, fc=fc: self._unhighlightItem(item, fc)) 169 | item.bind(DGG.MWDOWN, self.scrollPopUpMenu, [-1]) 170 | item.bind(DGG.MWUP, self.scrollPopUpMenu, [1]) 171 | # Set popup menu frame size to encompass all items 172 | f = self.component('popupMenu') 173 | f['frameSize'] = (0, self.maxWidth, -self.maxHeight * itemIndex, 0) 174 | # HACK: Set the user defined popup menu relief here so we don't 175 | # break the bounds calculation. 176 | self.popupMenu.setRelief(self['popupMenu_relief']) 177 | 178 | # Determine what initial item to display and set text accordingly 179 | if self['initialitem']: 180 | self.set(self['initialitem'], fCommand = 0) 181 | else: 182 | # No initial item specified, just use first item 183 | self.set(0, fCommand = 0) 184 | 185 | # Position popup Marker to the right of the button 186 | pm = self.popupMarker 187 | pmw = (pm.getWidth() * pm.getScale()[0] + 188 | 2 * self['popupMarkerBorder'][0]) 189 | if self.initFrameSize: 190 | # Use specified frame size 191 | bounds = list(self.initFrameSize) 192 | else: 193 | # Or base it upon largest item 194 | bounds = [self.minX, self.maxX, self.minZ, self.maxZ] 195 | if self.initPopupMarkerPos: 196 | # Use specified position 197 | pmPos = list(self.initPopupMarkerPos) 198 | else: 199 | # Or base the position on the frame size. 200 | pmPos = [bounds[1] + pmw/2.0, 0, bounds[2] + (bounds[3] - bounds[2])/2.0] 201 | pm.setPos(pmPos[0], pmPos[1], pmPos[2]) 202 | # Adjust popup menu button to fit all items (or use user specified 203 | # frame size 204 | bounds[1] += pmw 205 | self['frameSize'] = (bounds[0], bounds[1], bounds[2], bounds[3]) 206 | # Set initial state 207 | self.hidePopupMenu() 208 | self.bind(DGG.MWDOWN, self.scrollPopUpMenu, [-1]) 209 | self.bind(DGG.MWUP, self.scrollPopUpMenu, [1]) 210 | self.cancelFrame.bind(DGG.MWDOWN, self.scrollPopUpMenu, [-1]) 211 | self.cancelFrame.bind(DGG.MWUP, self.scrollPopUpMenu, [1]) 212 | 213 | def showPopupMenu(self, event = None): 214 | """ 215 | Make popup visible and try to position it just to right of 216 | mouse click with currently selected item aligned with button. 217 | Adjust popup position if default position puts it outside of 218 | visible screen region 219 | """ 220 | 221 | # Needed attributes (such as minZ) won't be set unless the user has specified 222 | # items to display. Let's assert that we've given items to work with. 223 | items = self['items'] 224 | # assert items and len(items) > 0, 'Cannot show an empty popup menu! You must add items!' 225 | if not items: 226 | return 227 | 228 | # Show the menu 229 | self.popupMenu.show() 230 | # Make sure its at the right scale 231 | self.popupMenu.setScale(self, VBase3(1)) 232 | # Compute bounds 233 | b = self.getBounds() 234 | fb = self.popupMenu.getBounds() 235 | 236 | if self['popupMenuLocation'] == DGG.RIGHT or self['popupMenuLocation'] == None: 237 | # This is the default to not break existing applications 238 | # Position menu at midpoint of button 239 | xPos = (b[1] - b[0])/2.0 - fb[0] 240 | elif self['popupMenuLocation'] == DGG.LEFT: 241 | # Position to the left 242 | xPos = -fb[1] + (b[1] - b[0])/2.0 243 | else: 244 | # position to line up with the left edge if the menu is above or below 245 | xPos = b[0] 246 | self.popupMenu.setX(self, xPos) 247 | 248 | if self['popupMenuLocation'] == DGG.ABOVE: 249 | # Try to set height to line up selected item with button 250 | self.popupMenu.setZ( 251 | self, self.maxZ - fb[2]) 252 | elif self['popupMenuLocation'] == DGG.BELOW: 253 | # Try to set height to line up selected item with button 254 | self.popupMenu.setZ( 255 | self, self.minZ) 256 | else: 257 | # Try to set height to line up selected item with button 258 | self.popupMenu.setZ( 259 | self, self.minZ + (self.selectedIndex + 1)*self.maxHeight) 260 | # Make sure the whole popup menu is visible 261 | pos = self.popupMenu.getPos(render2d) 262 | scale = self.popupMenu.getScale(render2d) 263 | # How are we doing relative to the right side of the screen 264 | maxX = pos[0] + fb[1] * scale[0] 265 | if maxX > 1.0: 266 | # Need to move menu to the left 267 | self.popupMenu.setX(render2d, pos[0] + (1.0 - maxX)) 268 | # How are we doing relative to the right side of the screen 269 | minX = pos[0] 270 | if minX < -1.0: 271 | # Need to move menu to the right 272 | self.popupMenu.setX(render2d, -1 ) 273 | # How about up and down? 274 | minZ = pos[2] + fb[2] * scale[2] 275 | maxZ = pos[2] + fb[3] * scale[2] 276 | if minZ < -1.0: 277 | # Menu too low, move it up 278 | self.popupMenu.setZ(render2d, pos[2] + (-1.0 - minZ)) 279 | 280 | # recheck the top position once repositioned 281 | pos = self.popupMenu.getPos(render2d) 282 | maxZ = pos[2] + fb[3] * scale[2] 283 | if maxZ > 1.0: 284 | # Menu too large to show on screen entirely 285 | # Try to set height to line up selected item with button 286 | self.popupMenu.setZ( 287 | self, self.minZ + (self.selectedIndex + 1)*self.maxHeight) 288 | elif maxZ > 1.0: 289 | # Menu too high, move it down 290 | self.popupMenu.setZ(render2d, pos[2] + (1.0 - maxZ)) 291 | # recheck the top position once repositioned 292 | pos = self.popupMenu.getPos(render2d) 293 | minZ = pos[2] + fb[2] * scale[2] 294 | if minZ < -1.0: 295 | # Menu too large to show on screen entirely 296 | # Try to set height to line up selected item with button 297 | self.popupMenu.setZ( 298 | self, self.minZ + (self.selectedIndex + 1)*self.maxHeight) 299 | # Also display cancel frame to catch clicks outside of the popup 300 | self.cancelFrame.show() 301 | # Position and scale cancel frame to fill entire window 302 | self.cancelFrame.setPos(render2d, 0, 0, 0) 303 | self.cancelFrame.setScale(render2d, 1, 1, 1) 304 | 305 | def hidePopupMenu(self, event = None): 306 | """ Put away popup and cancel frame """ 307 | self.popupMenu.hide() 308 | self.cancelFrame.hide() 309 | 310 | def scrollPopUpMenu(self, direction, event = None): 311 | """ Scroll the item frame up and down depending on the direction 312 | which must be a nummeric value. A positive value will scroll up 313 | while a negative value will scroll down. It will only work if 314 | items are out of bounds of the window """ 315 | fb = self.popupMenu.getBounds() 316 | pos = self.popupMenu.getPos(render2d) 317 | scale = self.popupMenu.getScale(render2d) 318 | 319 | minZ = pos[2] + fb[2] * scale[2] 320 | maxZ = pos[2] + fb[3] * scale[2] 321 | if (minZ < -1.0 and direction > 0) or (maxZ > 1.0 and direction < 0): 322 | oldZ = self.popupMenu.getZ() 323 | self.popupMenu.setZ(oldZ + direction * self.maxHeight) 324 | 325 | def _highlightItem(self, item, index): 326 | """ Set frame color of highlighted item, record index """ 327 | self.prevItemTextScale = item['text_scale'] 328 | item['frameColor'] = self['highlightColor'] 329 | #item['frameSize'] = (self['highlightScale'][0]*self.minX, self['highlightScale'][0]*self.maxX, self['highlightScale'][1]*self.minZ, self['highlightScale'][1]*self.maxZ) 330 | item['text_scale'] = self['highlightScale'] 331 | self.highlightedIndex = index 332 | 333 | def _unhighlightItem(self, item, frameColor): 334 | """ Clear frame color, clear highlightedIndex """ 335 | item['frameColor'] = frameColor 336 | item['frameSize'] = (self.minX, self.maxX, self.minZ, self.maxZ) 337 | item['text_scale'] = self.prevItemTextScale 338 | self.highlightedIndex = None 339 | 340 | def selectHighlightedIndex(self, event = None): 341 | """ 342 | Check to see if item is highlighted (by cursor being within 343 | that item). If so, selected it. If not, do nothing 344 | """ 345 | if self.highlightedIndex is not None: 346 | self.set(self.highlightedIndex) 347 | self.hidePopupMenu() 348 | 349 | def index(self, index): 350 | intIndex = None 351 | if isinstance(index, int): 352 | intIndex = index 353 | elif index in self['items']: 354 | i = 0 355 | for item in self['items']: 356 | if item == index: 357 | intIndex = i 358 | break 359 | i += 1 360 | return intIndex 361 | 362 | def set(self, index, fCommand = 1): 363 | # Item was selected, record item and call command if any 364 | newIndex = self.index(index) 365 | if newIndex is not None: 366 | self.selectedIndex = newIndex 367 | item = self['items'][self.selectedIndex] 368 | self['text'] = item 369 | if fCommand and self['command']: 370 | # Pass any extra args to command 371 | self['command'](*[item] + self['extraArgs']) 372 | 373 | def get(self): 374 | """ Get currently selected item """ 375 | return self['items'][self.selectedIndex] 376 | 377 | def commandFunc(self, event): 378 | """ 379 | Override popup menu button's command func 380 | Command is executed in response to selecting menu items 381 | """ 382 | pass 383 | -------------------------------------------------------------------------------- /DirectGuiExtension/DirectScrolledWindowFrame.py: -------------------------------------------------------------------------------- 1 | """This module contains the DirectScrolledWindowFrame class. 2 | 3 | It will create a scrolled frame with an extra frame on top of it 4 | """ 5 | 6 | __all__ = ['DirectScrolledWindowFrame'] 7 | 8 | from panda3d.core import * 9 | from direct.gui import DirectGuiGlobals as DGG 10 | from direct.gui.DirectFrame import DirectFrame 11 | from direct.gui.DirectButton import DirectButton 12 | from direct.gui.DirectScrolledFrame import DirectScrolledFrame 13 | 14 | class DirectScrolledWindowFrame(DirectScrolledFrame): 15 | """ 16 | A moveable window with a scrolled content frame 17 | """ 18 | def __init__(self, parent = None, **kw): 19 | optiondefs = ( 20 | # Define type of DirectGuiWidget 21 | # The height of the area to drag the widget around 22 | ('dragAreaHeight', 0.1, None), 23 | ('resortOnDrag', True, None), 24 | ('showClose', True, None), 25 | ('closeButtonPosition', 'Right', None), 26 | ('closeButtonScale', 0.05, None) 27 | ) 28 | # Merge keyword options with default options 29 | self.defineoptions(kw, optiondefs) 30 | 31 | # Initialize superclasses 32 | DirectScrolledFrame.__init__(self, parent) 33 | 34 | self.dragDropTask = None 35 | 36 | b = self["frameSize"] 37 | self.dragFrame = self.createcomponent( 38 | 'dragFrame', (), 'dragFrame', 39 | DirectFrame, 40 | # set the parent of the frame to this class 41 | (self,), 42 | state=DGG.NORMAL, 43 | suppressMouse=True, 44 | frameColor=(0.5,0.5,0.5,1), 45 | relief=1, 46 | pos=(0,0,b[3]), 47 | # set the size 48 | frameSize=(b[0],b[1],0, self['dragAreaHeight'])) 49 | 50 | self.dragFrame.bind(DGG.B1PRESS, self.dragStart) 51 | self.dragFrame.bind(DGG.B1RELEASE, self.dragStop) 52 | 53 | scale = self['closeButtonScale'] 54 | pos = (0,0,self['dragAreaHeight']*0.5) 55 | if self['closeButtonPosition'] == 'Right': 56 | pos = (b[1]-scale*0.5,0,self['dragAreaHeight']*0.5) 57 | elif self['closeButtonPosition'] == 'Left': 58 | pos = (b[0]+scale*0.5,0,self['dragAreaHeight']*0.5) 59 | self.closeButton = self.createcomponent( 60 | 'closeButton', (), 'closeButton', 61 | DirectButton, 62 | (self.dragFrame,), 63 | text='x', 64 | scale=scale, 65 | pos=pos, 66 | command=self.destroy) 67 | 68 | # Call option initialization functions 69 | self.initialiseoptions(DirectScrolledWindowFrame) 70 | 71 | self.dragFrame.setPos(0, 0, self.bounds[3]) 72 | self.dragFrame["frameSize"] = (self.bounds[0], self.bounds[1], 0, self['dragAreaHeight']) 73 | 74 | def dragStart(self, event): 75 | """ 76 | Start dragging the window around 77 | """ 78 | if self.dragDropTask is not None: 79 | # remove any existing tasks 80 | taskMgr.remove(self.dragDropTask) 81 | 82 | if self['resortOnDrag']: 83 | self.reparentTo(self.parent, 0) 84 | 85 | # get the windows position as seen from render2d 86 | vWidget2render2d = self.getPos(render2d) 87 | # get the mouse position as seen from render2d 88 | vMouse2render2d = Point3(event.getMouse()[0], 0, event.getMouse()[1]) 89 | # calculate the vector between the mosue and the window 90 | editVec = Vec3(vWidget2render2d - vMouse2render2d) 91 | # create the task and store the values in it, so we can use it in there 92 | self.dragDropTask = taskMgr.add(self.dragTask, self.taskName("dragDropTask")) 93 | self.dragDropTask.editVec = editVec 94 | self.dragDropTask.mouseVec = vMouse2render2d 95 | 96 | def dragTask(self, t): 97 | """ 98 | Calculate the new window position ever frame 99 | """ 100 | # chec if we have the mouse 101 | mwn = base.mouseWatcherNode 102 | if mwn.hasMouse(): 103 | # get the mouse position 104 | vMouse2render2d = Point3(mwn.getMouse()[0], 0, mwn.getMouse()[1]) 105 | # calculate the new position using the mouse position and the start 106 | # vector of the window 107 | newPos = vMouse2render2d + t.editVec 108 | # Now set the new windows new position 109 | self.setPos(render2d, newPos) 110 | return t.cont 111 | 112 | def dragStop(self, event): 113 | """ 114 | Stop dragging the window around 115 | """ 116 | # kill the drag and drop task 117 | taskMgr.remove(self.dragDropTask) 118 | -------------------------------------------------------------------------------- /DirectGuiExtension/DirectSpinBox.py: -------------------------------------------------------------------------------- 1 | '''This module contains the DirectSpinBox class. 2 | 3 | A DirectSpinBox is a type of Entry specialized for number selection. 4 | It contains two buttons to its right for increasing and decreasing the 5 | numeric value given in the entry field. Values can also be directly 6 | typed in the entry and will be checked against the type given to the 7 | spiner box which is *int* by default. 8 | 9 | Options available for this Widget are: 10 | 11 | * value 12 | The value selected by the spiner. This will always return the number 13 | in the format given in the valueType option. You can also pass a 14 | string or other type which the valueType function is possible to 15 | convert from. 16 | 17 | * textFormat 18 | This is a format string which will be used to write the value into 19 | the entry field 20 | 21 | * stepSize 22 | When the up and down arrows are pressed, stepSize will be used to 23 | determine how much the value should be in-/decreased 24 | 25 | * minValue 26 | The minimum value that can be entered in the spiner value. If values 27 | given to the spiner are lower than the min value, the spiner will 28 | set the minValue as new value. 29 | 30 | * maxValue 31 | The maximum value that can be entered in the spiner value. If values 32 | given to the spiner are larger than the max value, the spiner will 33 | set the maxValue as new value. 34 | 35 | * valueType 36 | A function like float, int or similar which will convert the value 37 | given to the spiner. It's return value if it was able to convert 38 | the value correctly will then be converted using the textFormat to 39 | a string which is entered into the textfield. 40 | 41 | * repeatdelay 42 | Delay in seconds at which the value should be in-/decreased when the 43 | up or down arrow button is held down 44 | 45 | * repeatStartdelay 46 | This delay will be used to determine when the repeat functionality 47 | is actually started after the user presses down any of the buttons 48 | 49 | * command 50 | This command will be passed to the entry field and hence will be 51 | called, whenever the user presses enter in it. 52 | 53 | * extraArgs 54 | Extra arguments passed to the entry field for when the command is 55 | used 56 | 57 | * incButtonCallback 58 | A callback function which will be called when the increase button is 59 | first pressed. It won't be called when the button is held down and 60 | the value is changed repeatedly. 61 | 62 | * decButtonCallback 63 | A callback function which will be called when the decrease button is 64 | first pressed. It won't be called when the button is held down and 65 | the value is changed repeatedly. 66 | 67 | ''' 68 | 69 | __all__ = ['DirectSpinBox'] 70 | 71 | import os 72 | 73 | from panda3d.core import * 74 | from direct.gui import DirectGuiGlobals as DGG 75 | from direct.directnotify import DirectNotifyGlobal 76 | from direct.task.Task import Task 77 | from direct.gui.DirectFrame import * 78 | from direct.gui.DirectEntry import * 79 | from direct.gui.DirectButton import * 80 | 81 | from direct.showbase.MessengerGlobal import messenger 82 | from direct.task.TaskManagerGlobal import taskMgr 83 | 84 | DGG.MWUP = PGButton.getPressPrefix() + MouseButton.wheel_up().getName() + '-' 85 | DGG.MWDOWN = PGButton.getPressPrefix() + MouseButton.wheel_down().getName() + '-' 86 | 87 | class DirectSpinBox(DirectFrame): 88 | notify = DirectNotifyGlobal.directNotify.newCategory('DirectSpinBox') 89 | 90 | def __init__(self, parent = None, **kw): 91 | assert self.notify.debugStateCall(self) 92 | 93 | # Inherits from DirectFrame 94 | optiondefs = ( 95 | # Define type of DirectGuiWidget 96 | ('value', 0, None), 97 | ('textFormat', '{:0d}', None), 98 | ('stepSize', 1, None), 99 | ('minValue', 0, None), 100 | ('maxValue', 100, None), 101 | ('valueType', int, None), 102 | ('repeatdelay', 0.125, None), 103 | ('repeatStartdelay', 0.25, None), 104 | ('command', None, None), 105 | ('extraArgs', [], None), 106 | ('incButtonCallback', None, self.setIncButtonCallback), 107 | ('decButtonCallback', None, self.setDecButtonCallback), 108 | ('buttonOrientation', DGG.VERTICAL, None), 109 | ('state', DGG.NORMAL, None), 110 | # set default border width to 0 111 | ('borderWidth', (0,0), None), 112 | ) 113 | # Merge keyword options with default options 114 | self.defineoptions(kw, optiondefs) 115 | 116 | # Initialize superclasses 117 | DirectFrame.__init__(self, parent) 118 | 119 | # create the textfield that will hold the text value 120 | self.valueEntry = self.createcomponent( 121 | 'valueEntry', (), None, 122 | DirectEntry, (self,), 123 | #overflow = 1, #DOESN'T WORK WITH RIGHT TEXT ALIGN!!! 124 | width = 2, 125 | text_align = TextNode.ARight, 126 | command = self['command'], 127 | extraArgs = self['extraArgs'], 128 | focusOutCommand = self.focusOutCommand, 129 | ) 130 | self.valueEntry.bind(DGG.MWUP, self.__mousewheelUp) 131 | self.valueEntry.bind(DGG.MWDOWN, self.__mousewheelDown) 132 | 133 | # try set the initial value 134 | try: 135 | self.setValue(self['value']) 136 | except: 137 | # Make sure the initial value is good 138 | self.setValue(0) 139 | 140 | # This font contains the up and down arrow 141 | root = Filename.fromOsSpecific(os.path.dirname(__file__)) 142 | shuttle_controls_font = loader.loadFont(f'{root}/data/shuttle_controls') 143 | 144 | # create the up arrow button 145 | self.incButton = self.createcomponent( 146 | 'incButton', (), None, 147 | DirectButton, (self,), 148 | text = '5' if self['buttonOrientation'] == DGG.VERTICAL else '4', 149 | text_font = shuttle_controls_font, 150 | ) 151 | # Set commands for the Inc Button 152 | self.incButton.bind(DGG.B1PRESS, self.__incButtonDown) 153 | self.incButton.bind(DGG.B1RELEASE, self.__buttonUp) 154 | 155 | # create the down arrow button 156 | self.decButton = self.createcomponent( 157 | 'decButton', (), None, 158 | DirectButton, (self,), 159 | text = '6' if self['buttonOrientation'] == DGG.VERTICAL else '3', 160 | text_font = shuttle_controls_font 161 | ) 162 | # Set commands for the Dec Button 163 | self.decButton.bind(DGG.B1PRESS, self.__decButtonDown) 164 | self.decButton.bind(DGG.B1RELEASE, self.__buttonUp) 165 | 166 | # Set the spiners elements position 167 | self.resetPosition() 168 | if self['frameSize'] is None: 169 | # Calculate the spiners frame size only if we don't have a 170 | # custom frameSize 171 | self.recalcFrameSize() 172 | 173 | self.initialiseoptions(DirectSpinBox) 174 | 175 | # Here we check for custom values of properties. 176 | # We need to do this for all components which have been edited 177 | # by code after they have been set up by createcomponent. 178 | if self['incButton_pos'] is not None: 179 | self.incButton.setPos(self['incButton_pos']) 180 | if self['decButton_pos'] is not None: 181 | self.decButton.setPos(self['decButton_pos']) 182 | if self['valueEntry_pos'] is not None: 183 | self.valueEntry.setPos(self['valueEntry_pos']) 184 | 185 | 186 | def resetPosition(self): 187 | ''' 188 | Positions the two buttons to the right of the text entry 189 | ''' 190 | assert self.notify.debugStateCall(self) 191 | # Position the text Entry field centered vertically 192 | valCenter = self.valueEntry.getCenter() 193 | self.valueEntry.setPos(0, 0, -valCenter[1]) 194 | 195 | # Position the Inc Button 196 | incCenter = self.incButton.getCenter() 197 | incWidth = self.incButton.getWidth() 198 | incHeight = self.incButton.getHeight() 199 | incBorderW = self.incButton['borderWidth'][0] 200 | incBorderH = self.incButton['borderWidth'][1] 201 | if self['buttonOrientation'] == DGG.VERTICAL: 202 | self.incButton.setPos(-incCenter[0]+incWidth/2+incBorderW, 0, -incCenter[1]+incHeight/2+incBorderH) 203 | self.incButton.setX(self.incButton, self.valueEntry.bounds[1]) 204 | else: 205 | self.incButton.setPos(-incCenter[0]+incWidth/2+incBorderW, 0, -incCenter[1]) 206 | self.incButton.setX(self.incButton, self.valueEntry.bounds[1]) 207 | 208 | # Position the Dec Button 209 | decCenter = self.decButton.getCenter() 210 | decWidth = self.decButton.getWidth() 211 | decHeight = self.decButton.getHeight() 212 | decBorderW = self.decButton['borderWidth'][0] 213 | decBorderH = self.decButton['borderWidth'][1] 214 | if self['buttonOrientation'] == DGG.VERTICAL: 215 | self.decButton.setPos(-decCenter[0]+decWidth/2+decBorderW, 0, -decCenter[1]-decHeight/2-decBorderH) 216 | self.decButton.setX(self.decButton, self.valueEntry.bounds[1]) 217 | else: 218 | valWidth = self.valueEntry.getWidth() 219 | self.decButton.setPos(-valWidth-decCenter[0]-decWidth/2-decBorderW, 0, -decCenter[1]) 220 | self.decButton.setX(self.decButton, self.valueEntry.bounds[1]) 221 | 222 | 223 | def recalcFrameSize(self): 224 | ''' 225 | Set the surrounding frame so the spinner will actually look 226 | like a box and will be able to give correct values for 227 | functions and properties like getWidth, getCenter and bounds 228 | ''' 229 | assert self.notify.debugStateCall(self) 230 | if self['buttonOrientation'] == DGG.VERTICAL: 231 | l = self.valueEntry.bounds[0] - self['borderWidth'][0] 232 | r = self.decButton.getX() + self.decButton.bounds[1] + self.decButton['borderWidth'][0] + self['borderWidth'][0] 233 | b = self.valueEntry.getZ() + self.valueEntry.bounds[2] - self['borderWidth'][1] 234 | t = self.valueEntry.getZ() + self.valueEntry.bounds[3] + self['borderWidth'][1] 235 | else: 236 | l = self.decButton.getX() + self.decButton.bounds[0] - self.incButton['borderWidth'][0] - self['borderWidth'][0] 237 | r = self.incButton.getX() + self.incButton.bounds[1] + self.incButton['borderWidth'][0] + self['borderWidth'][0] 238 | b = self.valueEntry.getZ() + self.valueEntry.bounds[2] - self['borderWidth'][1] 239 | t = self.valueEntry.getZ() + self.valueEntry.bounds[3] + self['borderWidth'][1] 240 | self['frameSize'] = (l, r, b, t) 241 | 242 | def __repeatStepTask(self, task): 243 | assert self.notify.debugStateCall(self) 244 | ret = self.doStep(task.stepSize) 245 | task.setDelay(self['repeatdelay']) 246 | if ret: 247 | return Task.again 248 | else: 249 | return Task.done 250 | 251 | def __incButtonDown(self, event): 252 | assert self.notify.debugStateCall(self) 253 | task = Task(self.__repeatStepTask) 254 | task.stepSize = self['stepSize'] 255 | taskName = self.taskName('repeatStep') 256 | #print 'incButtonDown: adding ', taskName 257 | taskMgr.doMethodLater(self['repeatStartdelay'], task, taskName) 258 | self.doStep(task.stepSize) 259 | messenger.send('wakeup') 260 | if self.__incButtonCallback: 261 | self.__incButtonCallback() 262 | 263 | def __decButtonDown(self, event): 264 | assert self.notify.debugStateCall(self) 265 | task = Task(self.__repeatStepTask) 266 | task.stepSize = -self['stepSize'] 267 | taskName = self.taskName('repeatStep') 268 | #print 'decButtonDown: adding ', taskName 269 | taskMgr.doMethodLater(self['repeatStartdelay'], task, taskName) 270 | self.doStep(task.stepSize) 271 | messenger.send('wakeup') 272 | if self.__decButtonCallback: 273 | self.__decButtonCallback() 274 | 275 | def __buttonUp(self, event): 276 | assert self.notify.debugStateCall(self) 277 | taskName = self.taskName('repeatStep') 278 | #print 'buttonUp: removing ', taskName 279 | taskMgr.remove(taskName) 280 | 281 | def __mousewheelUp(self, event): 282 | assert self.notify.debugStateCall(self) 283 | self.doStep(self['stepSize']) 284 | 285 | def __mousewheelDown(self, event): 286 | assert self.notify.debugStateCall(self) 287 | self.doStep(-self['stepSize']) 288 | 289 | def doStep(self, stepSize): 290 | """Adds the value given in stepSize to the current value stored 291 | in the spinner. Pass a negative value to subtract from the 292 | current value. 293 | """ 294 | assert self.notify.debugStateCall(self) 295 | #print 'doStep[', stepSize,']' 296 | 297 | return self.setValue(self['value'] + stepSize) 298 | 299 | def __checkValue(self, newValue): 300 | assert self.notify.debugStateCall(self) 301 | try: 302 | value = self['valueType'](newValue) 303 | if value < self['minValue']: 304 | self.notify.info('Value out of range value: {} min allowed value: {}'.format(value, self['minValue'])) 305 | return self['minValue'] 306 | if value > self['maxValue']: 307 | self.notify.info('Value out of range value: {} max allowed value: {}'.format(value, self['maxValue'])) 308 | return self['maxValue'] 309 | return value 310 | except: 311 | self.notify.info('ERROR: NAN {}'.format(newValue)) 312 | return None 313 | 314 | def get(self): 315 | ''' 316 | Returns the value in string format (see getValue to get the value in it's specific type) 317 | ''' 318 | assert self.notify.debugStateCall(self) 319 | return self.valueEntry.get() 320 | 321 | def getValue(self): 322 | ''' 323 | Returns the value in it's actual type (see get to get the value as a string) 324 | ''' 325 | assert self.notify.debugStateCall(self) 326 | return self['value'] 327 | 328 | def setValue(self, newValue): 329 | ''' 330 | Set a new value for the spinbox to display. newValue can be any type which 331 | can be converted by the function set in valueType 332 | ''' 333 | assert self.notify.debugStateCall(self) 334 | value = self.__checkValue(newValue) 335 | if value is None: 336 | self.valueEntry.enterText(self['textFormat'].format(self['value'])) 337 | return False 338 | 339 | self.valueEntry.enterText(self['textFormat'].format(value)) 340 | self['value'] = value 341 | return True 342 | 343 | def focusOutCommand(self): 344 | assert self.notify.debugStateCall(self) 345 | self.setValue(self.get()) 346 | 347 | def setIncButtonCallback(self): 348 | assert self.notify.debugStateCall(self) 349 | self.__incButtonCallback = self['incButtonCallback'] 350 | 351 | def setDecButtonCallback(self): 352 | assert self.notify.debugStateCall(self) 353 | self.__decButtonCallback = self['decButtonCallback'] 354 | 355 | ''' 356 | from direct.showbase.ShowBase import ShowBase 357 | base = ShowBase() 358 | 359 | spinBox = DirectSpinBox(pos=(0,0,-0.25), value=5, minValue=-100, maxValue=100, repeatdelay=0.125, buttonOrientation=DGG.HORIZONTAL, valueEntry_text_align=TextNode.ACenter, borderWidth=(1,1)) 360 | spinBox.setScale(0.1) 361 | spinBox["relief"] = 2 362 | spinBox = DirectSpinBox(pos=(0,0,0.25), valueEntry_width=10, borderWidth=(2,2), frameColor=(1,0,0,1)) 363 | spinBox.setScale(0.1) 364 | base.run() 365 | ''' 366 | -------------------------------------------------------------------------------- /DirectGuiExtension/DirectSplitFrame.py: -------------------------------------------------------------------------------- 1 | #Requires: 2 | # Can we get conjunction between two sizer to work to simultaneously size two splitframes at once? 3 | # Collapse function? 4 | 5 | 6 | 7 | 8 | 9 | """This module contains the DirectSplitFrame class.""" 10 | 11 | __all__ = ['DirectSplitFrame'] 12 | 13 | from panda3d.core import * 14 | from direct.gui import DirectGuiGlobals as DGG 15 | from direct.gui.DirectFrame import DirectFrame 16 | from . import DirectGuiHelper as DGH 17 | 18 | 19 | DGG.HORIZONTAL_INVERTED = 'horizontal_inverted' 20 | 21 | 22 | class DirectItemContainer(): 23 | def __init__(self, element, **kw): 24 | self.element = element 25 | #if "stretch" in kw: 26 | # if kw.get("stretch"): 27 | # pass 28 | # del kw["stretch"] 29 | #self.margin = VBase4(0,0,0,0) 30 | #if "margin" in kw: 31 | # self.margin = kw.get("margin") 32 | 33 | class DirectSplitFrame(DirectFrame): 34 | """ 35 | Two frames that can be resized with a small line between them 36 | """ 37 | 38 | def __init__(self, parent = None, **kw): 39 | self.skipInitRefresh = True 40 | optiondefs = ( 41 | # Define type of DirectGuiWidget 42 | ('pgFunc', PGItem, None), 43 | ('numStates', 1, None), 44 | ('state', DGG.NORMAL, None), 45 | ('borderWidth', (0, 0), self.setBorderWidth), 46 | ('orientation', DGG.HORIZONTAL, self.refresh), 47 | ('frameSize', (-1,1,-1,1), None), 48 | 49 | # TODO: Change this. This only works for certain circumstances 50 | ('pixel2d', False, self.refresh), 51 | 52 | ('showSplitter', True, self.setSplitter), 53 | ('splitterPos', 0, self.refresh), 54 | ('splitterWidth', 0.02, self.refresh), 55 | ('splitterColor', (.7, .7, .7, 1), None), 56 | ('splitterHighlightColor', (.9, .9, .9, 1), None), 57 | 58 | ('firstFrameUpdateSizeFunc', None, None), 59 | ('secondFrameUpdateSizeFunc', None, None), 60 | ('firstFrameMinSize', None, None), 61 | ('secondFrameMinSize', None, None), 62 | 63 | ('suppressMouse', 0, None) 64 | ) 65 | 66 | self._splitterWidth = 0 67 | 68 | # Merge keyword options with default options 69 | self.defineoptions(kw, optiondefs) 70 | 71 | # Initialize superclasses 72 | DirectFrame.__init__(self, parent) 73 | 74 | self.resetFrameSize() 75 | 76 | self.dragDropTask = None 77 | self.ignoreMinSizeCheck = False 78 | 79 | self.firstFrame = self.createcomponent( 80 | 'firstFrame', (), None, 81 | DirectFrame, (self,), 82 | frameSize = (-0.45, 0.45, -1, 1), 83 | pos = (-0.55, 0, 0),) 84 | 85 | self.secondFrame = self.createcomponent( 86 | 'secondFrame', (), None, 87 | DirectFrame, (self,), 88 | frameSize = (-0.45, 0.45, -1, 1), 89 | pos = (0.55, 0, 0),) 90 | 91 | self.splitter = self.createcomponent( 92 | 'splitter', (), None, 93 | DirectFrame, (self,), 94 | numStates = 2, 95 | frameSize = (-0.01, 0.01, -1, 1), 96 | frameColor = self['splitterColor'], 97 | text = "<\n>", 98 | text_scale = 0.05, 99 | text_align = TextNode.ACenter, 100 | state = 'normal') 101 | 102 | # Call option initialization functions 103 | self.initialiseoptions(DirectSplitFrame) 104 | 105 | self.splitter.bind(DGG.ENTER, self.enter) 106 | self.splitter.bind(DGG.EXIT, self.exit) 107 | self.splitter.bind(DGG.B1PRESS, self.dragStart) 108 | self.splitter.bind(DGG.B1RELEASE, self.dragStop) 109 | 110 | self.setSplitter() 111 | 112 | self.skipInitRefresh = False 113 | # initialize once at the end 114 | self.refresh() 115 | 116 | def setSplitter(self): 117 | if not hasattr(self, "splitter"): return 118 | if self['showSplitter'] == False: 119 | self.splitter.hide() 120 | self._splitterWidth = self["splitterWidth"] 121 | self['splitterWidth'] = 0 122 | else: 123 | self.splitter.show() 124 | if self["splitterWidth"] == 0: 125 | self['splitterWidth'] = self._splitterWidth 126 | 127 | def refresh(self): 128 | """ 129 | Recalculate the position of every item in this panel and set the frame- 130 | size of the panel accordingly if auto update is enabled. 131 | """ 132 | # sanity checks so we don't get here to early 133 | if self.skipInitRefresh: return 134 | if not hasattr(self, "bounds"): return 135 | 136 | width = DGH.getRealWidth(self) 137 | height = DGH.getRealHeight(self) 138 | 139 | if self["orientation"] == DGG.HORIZONTAL: 140 | self.splitter.setX(self["splitterPos"]) 141 | self.splitter.setZ(0) 142 | self.checkMinSIze() 143 | 144 | if self["pixel2d"]: 145 | splitterPosInPercent = 1 - self["splitterPos"]/width 146 | leftWidth = width * (1-splitterPosInPercent) - (self["splitterWidth"] / 2) 147 | rightWidth = width * splitterPosInPercent - (self["splitterWidth"] / 2) 148 | else: 149 | leftWidth = (width / 2) + self["splitterPos"] - (self["splitterWidth"] / 2) 150 | rightWidth = (width / 2) - self["splitterPos"] - (self["splitterWidth"] / 2) 151 | 152 | self.firstFrame["frameSize"] = (-leftWidth/2, leftWidth/2, self["frameSize"][2], self["frameSize"][3]) 153 | self.firstFrame.setX((-leftWidth / 2) - (self["splitterWidth"] / 2) + self["splitterPos"]) 154 | self.firstFrame.setZ(0) 155 | self.secondFrame["frameSize"] = (-rightWidth/2, rightWidth/2, self["frameSize"][2], self["frameSize"][3]) 156 | self.secondFrame.setX((rightWidth / 2) + (self["splitterWidth"] / 2) + self["splitterPos"]) 157 | self.secondFrame.setZ(0) 158 | 159 | self.splitter["frameSize"] = (-self["splitterWidth"]/2, self["splitterWidth"]/2, self["frameSize"][2], self["frameSize"][3]) 160 | self.splitter["text_roll"] = 0 161 | 162 | elif self["orientation"] == DGG.VERTICAL: 163 | self.splitter.setZ(self["splitterPos"]) 164 | self.splitter.setX(0) 165 | self.checkMinSIze() 166 | 167 | if self["pixel2d"]: 168 | splitterPosInPercent = 1 - self["splitterPos"]/height 169 | topHeight = height * splitterPosInPercent - (self["splitterWidth"] / 2) 170 | bottomHeight = height * (1-splitterPosInPercent) - (self["splitterWidth"] / 2) 171 | else: 172 | topHeight = (height / 2) - self["splitterPos"] - (self["splitterWidth"] / 2) 173 | bottomHeight = (height / 2) + self["splitterPos"] - (self["splitterWidth"] / 2) 174 | 175 | self.firstFrame["frameSize"] = (self["frameSize"][0], self["frameSize"][1], -topHeight/2, topHeight/2) 176 | self.firstFrame.setX(0) 177 | self.firstFrame.setZ((topHeight / 2) + (self["splitterWidth"] / 2) + self["splitterPos"]) 178 | self.secondFrame["frameSize"] = (self["frameSize"][0], self["frameSize"][1], -bottomHeight/2, bottomHeight/2) 179 | self.secondFrame.setX(0) 180 | self.secondFrame.setZ((-bottomHeight / 2) - (self["splitterWidth"] / 2) + self["splitterPos"]) 181 | 182 | self.splitter["frameSize"] = (self["frameSize"][0], self["frameSize"][1], -self["splitterWidth"]/2, self["splitterWidth"]/2) 183 | self.splitter["text_roll"] = 90 184 | 185 | 186 | base.messenger.send(self.uniqueName("update-size")) 187 | if self['firstFrameUpdateSizeFunc'] is not None: 188 | self['firstFrameUpdateSizeFunc']() 189 | if self['secondFrameUpdateSizeFunc'] is not None: 190 | self['secondFrameUpdateSizeFunc']() 191 | 192 | def checkMinSIze(self): 193 | if self.ignoreMinSizeCheck: return 194 | # ignore further minimum size checks until we are done 195 | self.ignoreMinSizeCheck = True 196 | 197 | if self["orientation"] == DGG.HORIZONTAL: 198 | minLeft = self['firstFrameMinSize'] if self['firstFrameMinSize'] is not None else 0 199 | minRight = self['secondFrameMinSize'] if self['secondFrameMinSize'] is not None else 0 200 | if self.splitter.getX() - self["splitterWidth"] / 2 < self["frameSize"][0] + minLeft: 201 | self.splitter.setX(self["frameSize"][0] + self["splitterWidth"] / 2 + minLeft) 202 | elif self.splitter.getX() + self["splitterWidth"] / 2 > self["frameSize"][1] - minRight: 203 | self.splitter.setX(self["frameSize"][1] - self["splitterWidth"] / 2 - minRight) 204 | self["splitterPos"] = self.splitter.getX() 205 | 206 | elif self["orientation"] == DGG.VERTICAL: 207 | minTop = self['firstFrameMinSize'] if self['firstFrameMinSize'] is not None else 0 208 | minBottom = self['secondFrameMinSize'] if self['secondFrameMinSize'] is not None else 0 209 | if self.splitter.getZ() - self["splitterWidth"] / 2 < self["frameSize"][2] + minTop: 210 | self.splitter.setZ(self["frameSize"][2] + self["splitterWidth"] / 2 + minTop) 211 | elif self.splitter.getZ() + self["splitterWidth"] / 2 > self["frameSize"][3] - minBottom: 212 | self.splitter.setZ(self["frameSize"][3] - self["splitterWidth"] / 2 - minBottom) 213 | self["splitterPos"] = self.splitter.getZ() 214 | 215 | self.ignoreMinSizeCheck = False 216 | 217 | def enter(self, event): 218 | self.splitter["frameColor"] = self['splitterHighlightColor'] 219 | 220 | def exit(self, event): 221 | self.splitter["frameColor"] = self['splitterColor'] 222 | 223 | def dragStart(self, event): 224 | """ 225 | Start dragging the window around 226 | """ 227 | if self.dragDropTask is not None: 228 | # remove any existing tasks 229 | taskMgr.remove(self.dragDropTask) 230 | 231 | self.splitter["frameColor"] = self['splitterHighlightColor'] 232 | 233 | # get the windows position as seen from render2d 234 | vWidget2render2d = self.splitter.getPos(render2d) 235 | # get the mouse position as seen from render2d 236 | vMouse2render2d = Point3(event.getMouse()[0], 0, event.getMouse()[1]) 237 | # calculate the vector between the mosue and the window 238 | editVec = Vec3(vWidget2render2d - vMouse2render2d) 239 | # create the task and store the values in it, so we can use it in there 240 | self.dragDropTask = taskMgr.add(self.dragTask, self.taskName("dragDropTask")) 241 | self.dragDropTask.editVec = editVec 242 | self.dragDropTask.mouseVec = vMouse2render2d 243 | 244 | def dragTask(self, t): 245 | """ 246 | Calculate the new window position ever frame 247 | """ 248 | # chec if we have the mouse 249 | mwn = base.mouseWatcherNode 250 | if mwn.hasMouse(): 251 | # get the mouse position 252 | vMouse2render2d = Point3(mwn.getMouse()[0], 0, mwn.getMouse()[1]) 253 | # calculate the new position using the mouse position and the start 254 | # vector of the window 255 | newPos = vMouse2render2d + t.editVec 256 | # Now set the new windows new position 257 | if self["orientation"] == DGG.HORIZONTAL: 258 | newPos.setY(0) 259 | newPos.setZ(self.splitter.getZ(render2d)) 260 | self.splitter.setPos(render2d, newPos) 261 | self.checkMinSIze() 262 | 263 | elif self["orientation"] == DGG.VERTICAL: 264 | newPos.setX(self.splitter.getX(render2d)) 265 | newPos.setY(0) 266 | self.splitter.setPos(render2d, newPos) 267 | self.checkMinSIze() 268 | self.refresh() 269 | self.splitter["frameColor"] = self['splitterHighlightColor'] 270 | return t.cont 271 | 272 | def dragStop(self, event): 273 | """ 274 | Stop dragging the splitter around 275 | """ 276 | # kill the drag and drop task 277 | taskMgr.remove(self.dragDropTask) 278 | self.splitter["frameColor"] = self['splitterColor'] 279 | self.refresh() 280 | -------------------------------------------------------------------------------- /DirectGuiExtension/DirectTabbedFrame.py: -------------------------------------------------------------------------------- 1 | """This module contains the DirectTabbedFrame class. 2 | 3 | It will create a frame with optionally closable tabs on top to switch between 4 | multiple parented frames 5 | """ 6 | 7 | __all__ = ['DirectTabbedFrame'] 8 | 9 | from uuid import uuid4 10 | from panda3d.core import * 11 | from direct.directnotify import DirectNotifyGlobal 12 | from direct.gui import DirectGuiGlobals as DGG 13 | from direct.gui.DirectFrame import DirectFrame 14 | from direct.gui.DirectButton import DirectButton 15 | from direct.gui.DirectRadioButton import DirectRadioButton 16 | from . import DirectGuiHelper as DGH 17 | 18 | class DirectTabbedFrame(DirectFrame): 19 | """ 20 | A frame with tabs 21 | """ 22 | 23 | notify = DirectNotifyGlobal.directNotify.newCategory('DirectTabbedFrame') 24 | 25 | def __init__(self, parent=None, **kw): 26 | optiondefs = ( 27 | # Define type of DirectGuiWidget 28 | # The height of the area to drag the widget around 29 | ('tabHeight', 0.1, None), 30 | ('showCloseOnTabs', True, None), 31 | ('frameSize', (-1,1,-1,1), None), 32 | ('selectedTabColor', (0.95, 0.95, 0.95, 1), None), 33 | ('unselectedTabColor', (.8, .8, .8, 1), None) 34 | ) 35 | # Merge keyword options with default options 36 | self.defineoptions(kw, optiondefs) 37 | 38 | # Initialize superclasses 39 | DirectFrame.__init__(self, parent) 40 | 41 | pos_x = self['frameSize'][0] 42 | pos_z = self['frameSize'][3]-self['tabHeight']/2 43 | self.prevTabButton = self.createcomponent( 44 | 'prevTabButton', (), None, 45 | DirectButton, 46 | (self,), 47 | text='<', 48 | text_align=TextNode.ALeft, 49 | scale=self['tabHeight'], 50 | borderWidth=(0,0), 51 | pressEffect=False, 52 | pos=(pos_x, 0, pos_z), 53 | frameSize=(0,0.7,-0.5,0.5), 54 | text_pos=(0.1, -0.25), 55 | command=self.show_prev_tab, 56 | ) 57 | 58 | pos_x = self['frameSize'][1] 59 | self.nextTabButton = self.createcomponent( 60 | 'nextTabButton', (), None, 61 | DirectButton, 62 | (self,), 63 | text='>', 64 | text_align=TextNode.ARight, 65 | scale=self['tabHeight'], 66 | borderWidth=(0,0), 67 | pressEffect=False, 68 | pos=(pos_x, 0, pos_z), 69 | frameSize=(-0.7,0,-0.5,0.5), 70 | text_pos=(-0.1, -0.25), 71 | command=self.show_next_tab, 72 | ) 73 | 74 | # Call option initialization functions 75 | self.initialiseoptions(DirectTabbedFrame) 76 | 77 | self.tab_index_from = 0 78 | self.tab_index_to = 0 79 | 80 | self.tab_list = [] 81 | self.selected_content = [None] 82 | self.current_content = None 83 | self.start_idx = 0 84 | 85 | def show_prev_tab(self): 86 | if self.start_idx > 0: 87 | self.start_idx -= 1 88 | self.reposition_tabs() 89 | 90 | def show_next_tab(self): 91 | if self.start_idx < len(self.tab_list) - 1: 92 | self.start_idx += 1 93 | self.reposition_tabs() 94 | 95 | def _add_tab(self, content, tab_text, close_func=None): # method used by DirectGuiDesigner to add tabs 96 | self.add_tab(tab_text, content, close_func) 97 | 98 | def add_tab(self, tab_text, content, close_func=None): 99 | # create the new tab 100 | tab = self.createcomponent( 101 | 'tab', (), 'tab', 102 | DirectRadioButton, 103 | (self,), 104 | text=tab_text, 105 | text_align=TextNode.ALeft, 106 | scale=self['tabHeight'], 107 | boxPlacement='right', 108 | frameColor=self['unselectedTabColor'], 109 | command=self.switch_tab, 110 | variable=self.selected_content, 111 | value=[0] 112 | ) 113 | tab['extraArgs'] = [tab] 114 | tab.resetFrameSize() 115 | # hide the radio button indicator 116 | #tab.indicator.hide() 117 | tab['value'] = [content] 118 | # hide the tabs content by default 119 | content.hide() 120 | 121 | 122 | if self['showCloseOnTabs']: 123 | x_pos = tab.indicator.get_pos() 124 | # create the close button 125 | tab.closeButton = self.createcomponent( 126 | 'closeButton', (), 'closeButton', 127 | DirectButton, 128 | (tab,), 129 | text='x', 130 | pos=x_pos, 131 | frameColor=self['unselectedTabColor'], 132 | command=self.close_tab, 133 | extraArgs=[tab, close_func], 134 | ) 135 | 136 | # add the tab to our list 137 | self.tab_list.append(tab) 138 | 139 | # reposition all tabs 140 | self.reposition_tabs() 141 | 142 | # update all tabs with the new list 143 | for other_tab in self.tab_list: 144 | other_tab.setOthers(self.tab_list) 145 | 146 | return tab 147 | 148 | def switch_tab(self, tab): 149 | if self.current_content: 150 | self.current_content.hide() 151 | self.current_content = self.selected_content[0] 152 | if self.current_content: 153 | self.current_content.show() 154 | 155 | # recolor tabs 156 | for other_tab in self.tab_list: 157 | other_tab['frameColor'] = self['unselectedTabColor'] 158 | tab['frameColor'] = self['selectedTabColor'] 159 | 160 | def close_tab(self, tab, close_func=None): 161 | # get the tabs index 162 | deleted_tab_idx = self.tab_list.index(tab) 163 | 164 | if close_func: 165 | close_func(tab) 166 | 167 | # store if it was the currently opened tab 168 | was_checked = tab['indicatorValue'] 169 | 170 | # actually remove the tab from list and rendering 171 | del self.tab_list[deleted_tab_idx] 172 | if self.current_content == tab['value'][0]: 173 | self.current_content.hide() 174 | self.current_content = None 175 | tab.destroy() 176 | 177 | # update the other tabs with the new list 178 | for other_tab in self.tab_list: 179 | other_tab.setOthers(self.tab_list) 180 | 181 | # check tab selection 182 | if self.start_idx >= len(self.tab_list): 183 | # last tab from the list was deleted, move forward a bit 184 | self.start_idx = len(self.tab_list) - 1 185 | if self.start_idx < 0: 186 | # goodby very last tab 187 | self.start_idx = 0 188 | if len(self.tab_list) > 0 and was_checked: 189 | self.select_tab(self.tab_list[-1]) 190 | elif len(self.tab_list) > deleted_tab_idx and was_checked: 191 | # select the new tab at that position, if we have any 192 | self.select_tab(self.tab_list[deleted_tab_idx]) 193 | elif len(self.tab_list) > 0 and was_checked: 194 | # select the last available tab 195 | self.select_tab(self.tab_list[0]) 196 | self.start_idx = 0 197 | 198 | # some sanity check 199 | if (self.start_idx >= len(self.tab_list) \ 200 | and self.start_idx != 0) \ 201 | or self.start_idx < 0: 202 | # something must have gone wrong, reset the start tab index 203 | self.start_idx = 0 204 | self.notify.info('Reset tab display start index') 205 | 206 | # reposition the existing tabs 207 | self.reposition_tabs() 208 | 209 | def reposition_tabs(self): 210 | # store some information we use in the repositioning 211 | next_x = DGH.getRealWidth(self.prevTabButton) 212 | show_tab = True 213 | next_button_width = DGH.getRealWidth(self.nextTabButton) 214 | 215 | # hide all tabs by default, they will be shown if they fit on the tab 216 | # list and are actually in the range of to be shown tabs 217 | for tab in self.tab_list: 218 | tab.hide() 219 | 220 | # move through all tabs, starting from the desired start index 221 | for tab in self.tab_list[self.start_idx:]: 222 | border_width = tab['borderWidth'][0]*tab['scale'] 223 | tab_bottom = DGH.getRealBottom(tab) 224 | tab_width = DGH.getRealWidth(tab) 225 | 226 | fs = self['frameSize'] 227 | 228 | if fs[0] + next_x + tab_width > fs[1] - next_button_width: 229 | # this tab doesn't fit anymore, skip it and the next 230 | show_tab = False 231 | 232 | if not show_tab: 233 | continue 234 | 235 | # show this tab 236 | tab.show() 237 | # calculate the new position and place it there 238 | tab_pos = ( 239 | fs[0]+next_x+border_width, 240 | 0, 241 | fs[3]-tab_bottom-self['tabHeight']) 242 | tab.set_pos(tab_pos) 243 | 244 | # calculate the X start position shift for the next tab 245 | next_x += tab_width 246 | 247 | def select_tab(self, tab): 248 | tab.commandFunc(None) 249 | -------------------------------------------------------------------------------- /DirectGuiExtension/DirectTooltip.py: -------------------------------------------------------------------------------- 1 | """This snippet shows how to create a tooltip text that will be attached 2 | to the cursor and check it's position to not move out of the screen.""" 3 | 4 | __all__ = ['DirectTooltip'] 5 | 6 | import sys 7 | 8 | from panda3d.core import TextNode 9 | from direct.gui.DirectGui import DirectLabel 10 | 11 | class DirectTooltip(DirectLabel): 12 | def __init__(self, parent = None, **kw): 13 | optiondefs = ( 14 | ('text', 'Tooltip', None), 15 | ('text_align', TextNode.ALeft, None), 16 | #('text_fg', (1, 1, 1, 1), None), 17 | #('text_bg', (0, 0, 0, 0.75), None), 18 | #('text_frame', (0, 0, 0, 0.75), None), 19 | ('borderWidth', (0.05, 0.05), None), 20 | #('parent', base.pixel2d, None), 21 | #('sortOrder', 1000, None), 22 | ) 23 | # Merge keyword options with default options 24 | self.defineoptions(kw, optiondefs) 25 | # Initialize superclasses 26 | DirectLabel.__init__(self, parent) 27 | 28 | # make sure the text apears slightly right below the cursor 29 | if parent is base.pixel2d: 30 | self.textXShift = 10 31 | self.textYShift = -32 32 | else: 33 | self.textXShift = 0.05 34 | self.textYShift = -0.08 35 | 36 | self.mousePos = None 37 | 38 | # this will determine when the tooltip should be moved in the 39 | # respective direction, whereby 40 | # 1 : display edge 41 | # <1 : margin inside the window 42 | # >1 : margin outside the window 43 | self.xEdgeStartShift = 0.99 44 | self.yEdgeStartShift = 0.99 45 | 46 | # make sure the tooltip is shown on top 47 | self.setBin('gui-popup', 0) 48 | 49 | # Call option initialization functions 50 | self.initialiseoptions(DirectTooltip) 51 | 52 | self.hide() 53 | 54 | def show(self, text=None, args=None): 55 | if text is not None: 56 | self.setText(text) 57 | self.resetFrameSize()#setFrameSize(True) 58 | DirectLabel.show(self) 59 | 60 | # add the tooltips update task so it will be updated every frame 61 | base.taskMgr.add(self.updateTooltipPos, self.taskName("task_updateTooltipPos")) 62 | 63 | def hide(self, args=None): 64 | DirectLabel.hide(self) 65 | 66 | # remove the tooltips update task 67 | base.taskMgr.remove(self.taskName("task_updateTooltipPos")) 68 | 69 | def updateTooltipPos(self, task): 70 | # calculate new aspec tratio 71 | wp = base.win.getProperties() 72 | aspX = 1.0 73 | aspY = 1.0 74 | wpXSize = wp.getXSize() 75 | wpYSize = wp.getYSize() 76 | if self.getParent() != base.pixel2d: 77 | # calculate the aspect ratio of the window if we're not reparented 78 | # to the pixel2d nodepath 79 | if wpXSize > wpYSize: 80 | aspX = wpXSize / float(wpYSize) 81 | else: 82 | aspY = wpYSize / float(wpXSize) 83 | 84 | # variables to store the mouses current x and y position 85 | x = 0.0 86 | y = 0.0 87 | if base.mouseWatcherNode.hasMouse(): 88 | DirectLabel.show(self) 89 | 90 | # Move the tooltip to the mouse 91 | 92 | # get the mouse position 93 | if self.getParent() == base.pixel2d: 94 | x = base.win.getPointer(0).getX() 95 | y = -base.win.getPointer(0).getY() 96 | # set the text to the current mouse position 97 | self.setPos(x + self.textXShift, 98 | 0, 99 | y + self.textYShift) 100 | else: 101 | x = (base.mouseWatcherNode.getMouseX()*aspX) 102 | y = (base.mouseWatcherNode.getMouseY()*aspY) 103 | # set the text to the current mouse position 104 | self.setPos(base.aspect2d, 105 | x + self.textXShift, 106 | 0, 107 | y + self.textYShift) 108 | 109 | bounds = self.getBounds() 110 | # bounds = left, right, bottom, top 111 | 112 | # calculate the texts bounds respecting its current position 113 | xLeft = self.getX() + bounds[0]*self.getScale()[0] 114 | xRight = self.getX() + bounds[1]*self.getScale()[0] 115 | yUp = self.getZ() + bounds[3]*self.getScale()[1] 116 | yDown = self.getZ() + bounds[2]*self.getScale()[1] 117 | 118 | # these will be used to shift the text in the desired direction 119 | xShift = 0.0 120 | yShift = 0.0 121 | 122 | if self.getParent() == base.pixel2d: 123 | # make sure to have the correct edges in a pixel2d environment 124 | # even if the window has been resized 125 | self.xEdgeStartShift = wpXSize * 0.99 126 | self.yEdgeStartShift = wpYSize * 0.99 127 | 128 | if xRight/aspX > self.xEdgeStartShift: 129 | # shift to the left 130 | xShift = self.xEdgeStartShift - xRight/aspX 131 | elif xLeft/aspX < -self.xEdgeStartShift: 132 | # shift to the right 133 | xShift = -(self.xEdgeStartShift + xLeft/aspX) 134 | if yUp/aspY > self.yEdgeStartShift: 135 | # shift down 136 | yShift = self.yEdgeStartShift - yUp/aspY 137 | elif yDown/aspY < -self.yEdgeStartShift: 138 | # shift up 139 | yShift = -(self.yEdgeStartShift + yDown/aspY) 140 | 141 | # some aspect ratio calculation 142 | xShift *= aspX 143 | yShift *= aspY 144 | 145 | # move the tooltip to the new position 146 | self.setX(self.getX() + xShift) 147 | self.setZ(self.getZ() + yShift) 148 | 149 | else: 150 | DirectLabel.hide(self) 151 | 152 | 153 | # continue the task until it got manually stopped 154 | return task.cont 155 | 156 | -------------------------------------------------------------------------------- /DirectGuiExtension/DirectTreeView.py: -------------------------------------------------------------------------------- 1 | """This module contains the DirectTreeView class.""" 2 | 3 | __all__ = ['DirectTreeView'] 4 | 5 | import os 6 | import uuid 7 | import logging 8 | 9 | from panda3d.core import * 10 | 11 | from direct.gui.DirectCheckBox import * 12 | from direct.gui.DirectLabel import * 13 | from direct.gui.DirectFrame import * 14 | from .DirectBoxSizer import DirectBoxSizer 15 | from . import DirectGuiHelper as DGH 16 | from direct.gui import DirectGuiGlobals as DGG 17 | #from dataclasses import dataclass 18 | 19 | class DirectTreeEntry: 20 | name = "" 21 | uuid = uuid.uuid4() 22 | def __init__(self, name, uuid=None): 23 | self.name = name 24 | if uuid: 25 | self.uuid = uuid 26 | 27 | class DirectTreeView(DirectBoxSizer): 28 | """ 29 | A frame for displaying a tree structure. 30 | 31 | The Trees visualization can be defined by overwriting the createEntry method 32 | which must return one widget that will be added as a tree node. 33 | """ 34 | def __init__(self, parent = None, **kw): 35 | root = Filename.fromOsSpecific(os.path.dirname(__file__)) 36 | optiondefs = ( 37 | ('imageCollapse', f"{root}/data/icons/minusnode.gif", DGG.INITOPT), 38 | ('imageCollapsed', f"{root}/data/icons/plusnode.gif", DGG.INITOPT), 39 | ('collapseImageScale', 0.025, self.refreshTree), 40 | ('collapseFrameSize', (-0.05, 0.05, -0.05, 0.05), self.refreshTree), 41 | ('treeTextScale', 0.1, self.refreshTree), 42 | ('tree', {}, self.refreshTree), 43 | ('indentationWidth', 0.1, None) 44 | ) 45 | # Merge keyword options with default options 46 | self.defineoptions(kw, optiondefs) 47 | 48 | # Initialize superclasses 49 | DirectBoxSizer.__init__(self, parent, orientation=DGG.VERTICAL, **kw) 50 | 51 | self.collapsedElements = [] 52 | self.indent_level = 0 53 | 54 | # Call option initialization functions 55 | self.initialiseoptions(DirectTreeView) 56 | 57 | self.refreshTree() 58 | 59 | def refreshTree(self): 60 | for item in self["items"]: 61 | item.element.destroy() 62 | self.removeAllItems(False) 63 | self.__createTree(self["tree"]) 64 | self.refresh() 65 | 66 | def __createTree(self, branch, indent_level=0): 67 | for element, sub_branch in branch.items(): 68 | if type(sub_branch) == dict: 69 | entry = self.createEntry(element, True, indent_level, sub_branch) 70 | self.addItem(entry, skipRefresh=True) 71 | if element in self.collapsedElements: 72 | continue 73 | indent_level += 1 74 | self.__createTree(sub_branch, indent_level) 75 | indent_level -= 1 76 | else: 77 | entry = self.createEntry(element, False, indent_level, sub_branch) 78 | self.addItem(entry, skipRefresh=True) 79 | 80 | def createEntry(self, element, hasChildren, indent_level, sub_branch): 81 | frame = DirectFrame( 82 | frameColor=(0,0,0,0) 83 | ) 84 | 85 | indentation = self["indentationWidth"] * indent_level 86 | 87 | img_scale = self["collapseImageScale"] 88 | 89 | if hasChildren: 90 | self.createCollapseCheckBox(frame, indentation, element, img_scale) 91 | 92 | element_name = self.getElementName(element) 93 | 94 | lbl = DirectLabel( 95 | text=element_name, 96 | scale=self["treeTextScale"], 97 | text_align=TextNode.ALeft, 98 | pos=(img_scale*2+0.02+indentation,0,0), 99 | parent=frame 100 | ) 101 | 102 | frame["frameSize"] = (0, img_scale*2+0.02+indentation+DGH.getRealWidth(lbl), DGH.getRealBottom(lbl), DGH.getRealTop(lbl)) 103 | 104 | return frame 105 | 106 | def getElementName(self, element): 107 | if hasattr(element, "name"): 108 | return element.name 109 | elif type(element) == str: 110 | return element 111 | else: 112 | logging.warning(f"Unknow element type {type(element)} for {element}") 113 | return "" 114 | 115 | def createCollapseCheckBox(self, parent, x_pos, element, img_scale=0.025): 116 | imgFilter = SamplerState.FT_nearest 117 | 118 | imgCollapse = loader.loadTexture(self["imageCollapse"]) 119 | imgCollapse.setMagfilter(imgFilter) 120 | imgCollapse.setMinfilter(imgFilter) 121 | 122 | imgCollapsed = loader.loadTexture(self["imageCollapsed"]) 123 | imgCollapsed.setMagfilter(imgFilter) 124 | imgCollapsed.setMinfilter(imgFilter) 125 | 126 | btnC = DirectCheckBox( 127 | relief=DGG.FLAT, 128 | pos=(img_scale+x_pos,0,0.03), 129 | frameSize=self["collapseFrameSize"], 130 | frameColor=(0,0,0,0), 131 | command=self.collapseElement, 132 | extraArgs=[element], 133 | image=imgCollapsed if element in self.collapsedElements else imgCollapse, 134 | uncheckedImage=imgCollapse, 135 | checkedImage=imgCollapsed, 136 | image_scale=img_scale, 137 | isChecked=element in self.collapsedElements, 138 | parent=parent) 139 | btnC.setTransparency(TransparencyAttrib.M_alpha) 140 | 141 | def collapseElement(self, collapse, element): 142 | if element is not None: 143 | base.messenger.send(f"beforeRefreshTreeView-{id(self)}") 144 | if collapse: 145 | self.collapsedElements.append(element) 146 | else: 147 | self.collapsedElements.remove(element) 148 | self.refreshTree() 149 | base.messenger.send(f"afterRefreshTreeView-{id(self)}") 150 | -------------------------------------------------------------------------------- /DirectGuiExtension/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireclawthefox/DirectGuiExtension/35e18f41193548cf11b85ad0f6d66182c28c639b/DirectGuiExtension/__init__.py -------------------------------------------------------------------------------- /DirectGuiExtension/data/icons/minusnode.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireclawthefox/DirectGuiExtension/35e18f41193548cf11b85ad0f6d66182c28c639b/DirectGuiExtension/data/icons/minusnode.gif -------------------------------------------------------------------------------- /DirectGuiExtension/data/icons/plusnode.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireclawthefox/DirectGuiExtension/35e18f41193548cf11b85ad0f6d66182c28c639b/DirectGuiExtension/data/icons/plusnode.gif -------------------------------------------------------------------------------- /DirectGuiExtension/data/maps/shuttle_controls_1.rgb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireclawthefox/DirectGuiExtension/35e18f41193548cf11b85ad0f6d66182c28c639b/DirectGuiExtension/data/maps/shuttle_controls_1.rgb -------------------------------------------------------------------------------- /DirectGuiExtension/data/shuttle_controls.egg: -------------------------------------------------------------------------------- 1 | shuttle_controls061 { 2 | maps/shuttle_controls_1.rgb 3 | format { alpha } 4 | wrap { clamp } 5 | minfilter { linear } 6 | magfilter { linear } 7 | anisotropic-degree { 0 } 8 | { 9 | { 10 | 0.328125 0 0 11 | 0 0.328125 0 12 | 0.28125 0.046875 1 13 | } 14 | } 15 | } 16 | shuttle_controls060 { 17 | maps/shuttle_controls_1.rgb 18 | format { alpha } 19 | wrap { clamp } 20 | minfilter { linear } 21 | magfilter { linear } 22 | anisotropic-degree { 0 } 23 | { 24 | { 25 | 0.328125 0 0 26 | 0 0.328125 0 27 | -0.015625 0.046875 1 28 | } 29 | } 30 | } 31 | shuttle_controls059 { 32 | maps/shuttle_controls_1.rgb 33 | format { alpha } 34 | wrap { clamp } 35 | minfilter { linear } 36 | magfilter { linear } 37 | anisotropic-degree { 0 } 38 | { 39 | { 40 | 0.233218 0 0 41 | 0 0.342988 0 42 | 0.703704 0.344131 1 43 | } 44 | } 45 | } 46 | shuttle_controls058 { 47 | maps/shuttle_controls_1.rgb 48 | format { alpha } 49 | wrap { clamp } 50 | minfilter { linear } 51 | magfilter { linear } 52 | anisotropic-degree { 0 } 53 | { 54 | { 55 | 0.38996 0 0 56 | 0 0.342988 0 57 | 0.344082 0.344131 1 58 | } 59 | } 60 | } 61 | shuttle_controls057 { 62 | maps/shuttle_controls_1.rgb 63 | format { alpha } 64 | wrap { clamp } 65 | minfilter { linear } 66 | magfilter { linear } 67 | anisotropic-degree { 0 } 68 | { 69 | { 70 | 0.38996 0 0 71 | 0 0.342988 0 72 | -0.0152926 0.344131 1 73 | } 74 | } 75 | } 76 | shuttle_controls056 { 77 | maps/shuttle_controls_1.rgb 78 | format { alpha } 79 | wrap { clamp } 80 | minfilter { linear } 81 | magfilter { linear } 82 | anisotropic-degree { 0 } 83 | { 84 | { 85 | 0.358648 0 0 86 | 0 0.359375 0 87 | 0.312863 0.65625 1 88 | } 89 | } 90 | } 91 | shuttle_controls055 { 92 | maps/shuttle_controls_1.rgb 93 | format { alpha } 94 | wrap { clamp } 95 | minfilter { linear } 96 | magfilter { linear } 97 | anisotropic-degree { 0 } 98 | { 99 | { 100 | 0.358648 0 0 101 | 0 0.359375 0 102 | -0.0152616 0.65625 1 103 | } 104 | } 105 | } 106 | shuttle_controls054 { 107 | maps/shuttle_controls_1.rgb 108 | format { alpha } 109 | wrap { clamp } 110 | minfilter { linear } 111 | magfilter { linear } 112 | anisotropic-degree { 0 } 113 | { 114 | { 115 | 0.359375 0 0 116 | 0 0.201766 0 117 | 0.578125 0.172554 1 118 | } 119 | } 120 | } 121 | shuttle_controls053 { 122 | maps/shuttle_controls_1.rgb 123 | format { alpha } 124 | wrap { clamp } 125 | minfilter { linear } 126 | magfilter { linear } 127 | anisotropic-degree { 0 } 128 | { 129 | { 130 | 0.359375 0 0 131 | 0 0.201766 0 132 | 0.578125 0.000679348 1 133 | } 134 | } 135 | } 136 | shuttle_controls052 { 137 | maps/shuttle_controls_1.rgb 138 | format { alpha } 139 | wrap { clamp } 140 | minfilter { linear } 141 | magfilter { linear } 142 | anisotropic-degree { 0 } 143 | { 144 | { 145 | 0.201766 0 0 146 | 0 0.359375 0 147 | 0.813179 0.65625 1 148 | } 149 | } 150 | } 151 | shuttle_controls051 { 152 | maps/shuttle_controls_1.rgb 153 | format { alpha } 154 | wrap { clamp } 155 | minfilter { linear } 156 | magfilter { linear } 157 | anisotropic-degree { 0 } 158 | { 159 | { 160 | 0.201766 0 0 161 | 0 0.359375 0 162 | 0.641304 0.65625 1 163 | } 164 | } 165 | } 166 | { 167 | { 1 } 168 | fps { 2 } 169 | vpool { 170 | 0 { 171 | 0 1.15 0 172 | } 173 | 1 { 174 | 0.216667 -0.0333333 0 175 | { 0.0740741 0.0434783 } 176 | } 177 | 2 { 178 | 0.6 -0.0333333 0 179 | { 0.925926 0.0434783 } 180 | } 181 | 3 { 182 | 0.6 0.666667 0 183 | { 0.925926 0.956522 } 184 | } 185 | 4 { 186 | 0.216667 0.666667 0 187 | { 0.0740741 0.956522 } 188 | } 189 | 5 { 190 | 1 0 0 191 | } 192 | 6 { 193 | 0.333333 -0.0333333 0 194 | { 0.0740741 0.0434783 } 195 | } 196 | 7 { 197 | 0.716667 -0.0333333 0 198 | { 0.925926 0.0434783 } 199 | } 200 | 8 { 201 | 0.716667 0.666667 0 202 | { 0.925926 0.956522 } 203 | } 204 | 9 { 205 | 0.333333 0.666667 0 206 | { 0.0740741 0.956522 } 207 | } 208 | 10 { 209 | 1 0 0 210 | } 211 | 11 { 212 | 0.116667 0.116667 0 213 | { 0.0434783 0.0740741 } 214 | } 215 | 12 { 216 | 0.816667 0.116667 0 217 | { 0.956522 0.0740741 } 218 | } 219 | 13 { 220 | 0.816667 0.5 0 221 | { 0.956522 0.925926 } 222 | } 223 | 14 { 224 | 0.116667 0.5 0 225 | { 0.0434783 0.925926 } 226 | } 227 | 15 { 228 | 1 0 0 229 | } 230 | 16 { 231 | 0.116667 0.05 0 232 | { 0.0434783 0.0740741 } 233 | } 234 | 17 { 235 | 0.816667 0.05 0 236 | { 0.956522 0.0740741 } 237 | } 238 | 18 { 239 | 0.816667 0.433333 0 240 | { 0.956522 0.925926 } 241 | } 242 | 19 { 243 | 0.116667 0.433333 0 244 | { 0.0434783 0.925926 } 245 | } 246 | 20 { 247 | 1 0 0 248 | } 249 | 21 { 250 | 0.1 -0.0666667 0 251 | { 0.0425532 0.0434783 } 252 | } 253 | 22 { 254 | 0.816667 -0.0666667 0 255 | { 0.957447 0.0434783 } 256 | } 257 | 23 { 258 | 0.816667 0.633333 0 259 | { 0.957447 0.956522 } 260 | } 261 | 24 { 262 | 0.1 0.633333 0 263 | { 0.0425532 0.956522 } 264 | } 265 | 25 { 266 | 1 0 0 267 | } 268 | 26 { 269 | 0.183333 -0.0666667 0 270 | { 0.0425532 0.0434783 } 271 | } 272 | 27 { 273 | 0.9 -0.0666667 0 274 | { 0.957447 0.0434783 } 275 | } 276 | 28 { 277 | 0.9 0.633333 0 278 | { 0.957447 0.956522 } 279 | } 280 | 29 { 281 | 0.183333 0.633333 0 282 | { 0.0425532 0.956522 } 283 | } 284 | 30 { 285 | 1 0 0 286 | } 287 | 31 { 288 | 0.1 -0.0666667 0 289 | { 0.0392157 0.0444444 } 290 | } 291 | 32 { 292 | 0.883333 -0.0666667 0 293 | { 0.960784 0.0444444 } 294 | } 295 | 33 { 296 | 0.883333 0.616667 0 297 | { 0.960784 0.955556 } 298 | } 299 | 34 { 300 | 0.1 0.616667 0 301 | { 0.0392157 0.955556 } 302 | } 303 | 35 { 304 | 0.983333 0 0 305 | } 306 | 36 { 307 | 0.1 -0.0666667 0 308 | { 0.0392157 0.0444444 } 309 | } 310 | 37 { 311 | 0.883333 -0.0666667 0 312 | { 0.960784 0.0444444 } 313 | } 314 | 38 { 315 | 0.883333 0.616667 0 316 | { 0.960784 0.955556 } 317 | } 318 | 39 { 319 | 0.1 0.616667 0 320 | { 0.0392157 0.955556 } 321 | } 322 | 40 { 323 | 0.983333 0 0 324 | } 325 | 41 { 326 | 0.266667 -0.0666667 0 327 | { 0.0645161 0.0444444 } 328 | } 329 | 42 { 330 | 0.716667 -0.0666667 0 331 | { 0.935484 0.0444444 } 332 | } 333 | 43 { 334 | 0.716667 0.616667 0 335 | { 0.935484 0.955556 } 336 | } 337 | 44 { 338 | 0.266667 0.616667 0 339 | { 0.0645161 0.955556 } 340 | } 341 | 45 { 342 | 1 0 0 343 | } 344 | 46 { 345 | 0.183333 -0.0166667 0 346 | { 0.047619 0.047619 } 347 | } 348 | 47 { 349 | 0.816667 -0.0166667 0 350 | { 0.952381 0.047619 } 351 | } 352 | 48 { 353 | 0.816667 0.616667 0 354 | { 0.952381 0.952381 } 355 | } 356 | 49 { 357 | 0.183333 0.616667 0 358 | { 0.047619 0.952381 } 359 | } 360 | 50 { 361 | 1 0 0 362 | } 363 | 51 { 364 | 0.183333 -0.0166667 0 365 | { 0.047619 0.047619 } 366 | } 367 | 52 { 368 | 0.816667 -0.0166667 0 369 | { 0.952381 0.047619 } 370 | } 371 | 53 { 372 | 0.816667 0.616667 0 373 | { 0.952381 0.952381 } 374 | } 375 | 54 { 376 | 0.183333 0.616667 0 377 | { 0.047619 0.952381 } 378 | } 379 | 55 { 380 | 1 0 0 381 | } 382 | } 383 | ds { 384 | { 385 | { 0 { vpool } } 386 | } 387 | } 388 | 51 { 389 | { 390 | { shuttle_controls051 } 391 | { 1 2 3 4 { vpool } } 392 | } 393 | { 394 | { 5 { vpool } } 395 | } 396 | } 397 | 52 { 398 | { 399 | { shuttle_controls052 } 400 | { 6 7 8 9 { vpool } } 401 | } 402 | { 403 | { 10 { vpool } } 404 | } 405 | } 406 | 53 { 407 | { 408 | { shuttle_controls053 } 409 | { 11 12 13 14 { vpool } } 410 | } 411 | { 412 | { 15 { vpool } } 413 | } 414 | } 415 | 54 { 416 | { 417 | { shuttle_controls054 } 418 | { 16 17 18 19 { vpool } } 419 | } 420 | { 421 | { 20 { vpool } } 422 | } 423 | } 424 | 55 { 425 | { 426 | { shuttle_controls055 } 427 | { 21 22 23 24 { vpool } } 428 | } 429 | { 430 | { 25 { vpool } } 431 | } 432 | } 433 | 56 { 434 | { 435 | { shuttle_controls056 } 436 | { 26 27 28 29 { vpool } } 437 | } 438 | { 439 | { 30 { vpool } } 440 | } 441 | } 442 | 57 { 443 | { 444 | { shuttle_controls057 } 445 | { 31 32 33 34 { vpool } } 446 | } 447 | { 448 | { 35 { vpool } } 449 | } 450 | } 451 | 58 { 452 | { 453 | { shuttle_controls058 } 454 | { 36 37 38 39 { vpool } } 455 | } 456 | { 457 | { 40 { vpool } } 458 | } 459 | } 460 | 59 { 461 | { 462 | { shuttle_controls059 } 463 | { 41 42 43 44 { vpool } } 464 | } 465 | { 466 | { 45 { vpool } } 467 | } 468 | } 469 | 60 { 470 | { 471 | { shuttle_controls060 } 472 | { 46 47 48 49 { vpool } } 473 | } 474 | { 475 | { 50 { vpool } } 476 | } 477 | } 478 | 61 { 479 | { 480 | { shuttle_controls061 } 481 | { 51 52 53 54 { vpool } } 482 | } 483 | { 484 | { 55 { vpool } } 485 | } 486 | } 487 | } 488 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2019-2020, Fireclaw 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include DirectGuiExtension/data/* 2 | recursive-include DirectGuiExtension/data * 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DirectGuiExtension 2 | A set of extensions for the DirectGUI system of the Panda3D engine 3 | 4 | ## Features 5 | Extends the DirectGUI system with the following features 6 | 7 | Simple layout system 8 | - DirectAutoSizer 9 | - DirectBoxSizer 10 | - DirectGridSizer 11 | - DirectSplitFrame 12 | - DirectCollapsibleFrame 13 | - DirectTabbedFrame 14 | 15 | Widgets 16 | - DirectDatePicker 17 | - DirectDiagram 18 | - DirectMenuItem 19 | - DirectSpinBox 20 | - DirectTooltip 21 | 22 | Window system 23 | - DirectScrolledWindowFrame 24 | 25 | Helper Class 26 | - DirectGuiHelper 27 | 28 | ## Install 29 | Install the DirectGuiExtension via pip 30 | 31 | ```bash 32 | pip install DirectGuiExtension 33 | ``` 34 | 35 | ## How to use 36 | 37 | Check the wiki on github for a detailed documentation about how to use the individual elements of this extension:
38 | wiki (https://github.com/fireclawthefox/DirectGuiExtension/wiki) 39 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | panda3d 2 | DirectFolderBrowser 3 | -------------------------------------------------------------------------------- /samples.py: -------------------------------------------------------------------------------- 1 | import random 2 | from direct.showbase.ShowBase import ShowBase 3 | from panda3d.core import TextNode, loadPrcFileData 4 | from direct.gui import DirectGuiGlobals as DGG 5 | from direct.gui.DirectButton import DirectButton 6 | from direct.gui.DirectFrame import DirectFrame 7 | from direct.gui.DirectLabel import DirectLabel 8 | from direct.gui.DirectEntry import DirectEntry 9 | from DirectGuiExtension.DirectBoxSizer import DirectBoxSizer 10 | from DirectGuiExtension.DirectGridSizer import DirectGridSizer 11 | from DirectGuiExtension.DirectScrolledWindowFrame import DirectScrolledWindowFrame 12 | from DirectGuiExtension.DirectMenuBar import DirectMenuBar 13 | from DirectGuiExtension.DirectMenuItem import DirectMenuItem, DirectMenuItemEntry, DirectMenuItemSubMenu 14 | from DirectGuiExtension.DirectTooltip import DirectTooltip 15 | from DirectGuiExtension.DirectSpinBox import DirectSpinBox 16 | from DirectGuiExtension.DirectDiagram import DirectDiagram 17 | from DirectGuiExtension.DirectDatePicker import DirectDatePicker 18 | from DirectGuiExtension.DirectAutoSizer import DirectAutoSizer 19 | from DirectGuiExtension.DirectSplitFrame import DirectSplitFrame 20 | from DirectGuiExtension import DirectGuiHelper as DGH 21 | 22 | from DirectFolderBrowser.DirectFolderBrowser import DirectFolderBrowser 23 | 24 | loadPrcFileData( 25 | "", 26 | """ 27 | window-title DirectGUI Extensions Demo 28 | show-frame-rate-meter #t 29 | """) 30 | 31 | 32 | app = ShowBase() 33 | 34 | 35 | # MAIN LAYOUT 36 | mainBox = DirectBoxSizer(orientation=DGG.VERTICAL, autoUpdateFrameSize=False) 37 | DirectAutoSizer(child=mainBox, childUpdateSizeFunc=mainBox.refresh) 38 | 39 | 40 | # CALENDAR EXAMPLE 41 | # in a function to be called by a menu item 42 | def showCalendar(): 43 | dp = DirectDatePicker() 44 | calendarDragFrame = DirectScrolledWindowFrame(frameSize=dp["frameSize"]) 45 | dp.reparentTo(calendarDragFrame) 46 | dp.refreshPicker() 47 | calendarDragFrame.setPos(-calendarDragFrame.getWidth()*0.5,0,calendarDragFrame.getHeight()*0.5) 48 | 49 | 50 | # BROWSER EXAMPLE 51 | # tooltip to be used in the browser 52 | tt = DirectTooltip(parent=pixel2d, text_scale=12, frameColor=(1, 1, 0.7, 1), pad=(5,5)) 53 | # in a function to be called by a menu item 54 | def showBrowser(doLoad): 55 | dragFrame = DirectScrolledWindowFrame(parent=pixel2d, frameSize=(-250,250,-200,200), dragAreaHeight=25, pos=(260,0,-300), closeButtonScale=20) 56 | def accept(ok): 57 | print(ok) 58 | dragFrame.destroy() 59 | DirectFolderBrowser(accept, doLoad, tooltip=tt, parent=dragFrame) 60 | 61 | 62 | # MENU ITEMS 63 | itemList = [ 64 | DirectMenuItemEntry("Save", showBrowser, [False]), 65 | DirectMenuItemEntry("Load", showBrowser, [True]), 66 | DirectMenuItemSubMenu("Recent >", [ 67 | DirectMenuItemEntry("Item A", print, ["Item A"]), 68 | DirectMenuItemEntry("Item B", print, ["Item B"]), 69 | DirectMenuItemEntry("Item C", print, ["Item C"]) 70 | ]), 71 | DirectMenuItemEntry("Quit", quit, [])] 72 | fileMenu = DirectMenuItem(text="File", scale=0.1, item_relief=1, items=itemList) 73 | itemList = [ 74 | DirectMenuItemEntry("Show Calendar", showCalendar, []), 75 | DirectMenuItemEntry("Help", print, ["Help"])] 76 | viewMenu = DirectMenuItem(text="View", scale=0.1, item_relief=1, items=itemList) 77 | 78 | menuBar = DirectMenuBar(parent=mainBox, menuItems=[fileMenu, viewMenu]) 79 | 80 | mainBox.addItem(menuBar) 81 | 82 | # Split frame 83 | splitSizer = DirectSplitFrame( 84 | frameSize=(base.a2dLeft,base.a2dRight,-0.5,0.5), 85 | orientation=DGG.HORIZONTAL, 86 | secondFrameMinSize=0.75) 87 | leftBox = DirectBoxSizer( 88 | parent=splitSizer.firstFrame, 89 | orientation=DGG.VERTICAL) 90 | splitterAutoSizer = DirectAutoSizer(extendVertical=False, child=splitSizer, childUpdateSizeFunc=splitSizer.refresh) 91 | mainBox.addItem(splitterAutoSizer) 92 | 93 | 94 | # GRID SIZER 95 | gridSizer = DirectGridSizer(numRows=4, numColumns=5, itemMargin=[0.01, 0.01, 0.01, 0.01]) 96 | 97 | def createButton(txt, right=0.1): 98 | btn = DirectButton( 99 | text=txt, 100 | text_scale=0.1, 101 | borderWidth=(0.01, 0.01), 102 | frameColor=(0.7,0.7,0.7,1), 103 | frameSize=(-0.1,right,-0.07,0.125), 104 | command=base.messenger.send, 105 | extraArgs=["updateEntry", [txt]], 106 | ) 107 | return btn 108 | 109 | gridSizer.addItem(createButton("7"), 0,0) 110 | gridSizer.addItem(createButton("8"), 0,1) 111 | gridSizer.addItem(createButton("9"), 0,2) 112 | gridSizer.addItem(createButton("4"), 1,0) 113 | gridSizer.addItem(createButton("5"), 1,1) 114 | gridSizer.addItem(createButton("6"), 1,2) 115 | gridSizer.addItem(createButton("1"), 2,0) 116 | gridSizer.addItem(createButton("2"), 2,1) 117 | gridSizer.addItem(createButton("3"), 2,2) 118 | gridSizer.addItem(createButton("0"), 3,1) 119 | gridSizer.addItem(createButton("*"), 0,3) 120 | gridSizer.addItem(createButton("-"), 1,3) 121 | gridSizer.addItem(createButton("+"), 2,3) 122 | gridSizer.addItem(createButton("=",0.33), 3,3,2) 123 | gridSizer.addItem(createButton("/"), 0,4) 124 | gridSizer.addItem(createButton("CE"), 1,4) 125 | gridSizer.addItem(createButton("c"), 2,4) 126 | 127 | entry = DirectEntry(scale=.1, text_align=TextNode.ARight, relief=DGG.SUNKEN, overflow=False) 128 | entry["state"] = DGG.DISABLED 129 | 130 | def updateEntry(arg): 131 | if arg == "=": 132 | try: 133 | entry.set(str(eval(entry.get()))) 134 | except: 135 | entry.set("Error") 136 | elif arg == "c": 137 | entry.set(entry.get()[:-1]) 138 | elif arg == "CE": 139 | entry.set("") 140 | else: 141 | entry.set(entry.get() + arg) 142 | 143 | app.accept("updateEntry", updateEntry) 144 | 145 | leftBox.addItem(entry) 146 | leftBox.addItem(gridSizer) 147 | leftBox.setZ(DGH.getRealHeight(leftBox)/2) 148 | 149 | 150 | # SPINNER 151 | spinBox = DirectSpinBox(pos=(0.8,0,0.25), value=0, minValue=-10, maxValue=10, repeatdelay=0.125, buttonOrientation=DGG.HORIZONTAL, valueEntry_text_align=TextNode.ACenter, borderWidth=(.1,.1)) 152 | spinBox.setScale(0.1) 153 | spinBox["relief"] = 2 154 | spinBoxSizer = DirectBoxSizer( 155 | orientation=DGG.VERTICAL, 156 | autoUpdateFrameSize=False, 157 | frameSize=(0,0,spinBox["frameSize"][2]*spinBox.getScale()[0]*2,spinBox["frameSize"][3]*spinBox.getScale()[0]*2), 158 | itemAlign=DirectBoxSizer.A_Center|DirectBoxSizer.A_Middle 159 | ) 160 | spinerInfo = DirectLabel(text="Change diagram\nvalue range: ", scale=0.1) 161 | spinBoxSizer.addItem(spinerInfo) 162 | spinBoxSizer.addItem(spinBox) 163 | spinAutoSizer = DirectAutoSizer(splitSizer.secondFrame, extendVertical=False, child=spinBoxSizer, childUpdateSizeFunc=spinBoxSizer.refresh) 164 | 165 | 166 | # DIAGRAM 167 | data = [10, -5, 1, -1, 1, -1, 1, -1, 1, -1] 168 | height = mainBox.getRemainingSpace() 169 | 170 | dd = DirectDiagram( 171 | data=data, 172 | numberAreaWidth=0.15, 173 | numNegSteps=20, 174 | numPosSteps=20, 175 | numNegStepsStep=2, 176 | numPosStepsStep=2, 177 | stepFormat=int, 178 | numtextScale=0.04, 179 | frameSize=(-.25, .25, -height/2, height/2)) 180 | diagramAutoSizer = DirectAutoSizer(parent=mainBox, extendVertical=False, child=dd) 181 | mainBox.addItem(diagramAutoSizer) 182 | def updateTask(task): 183 | global data 184 | data = data[1:] 185 | data += [random.uniform(-10+spinBox.getValue(), 10+spinBox.getValue())] 186 | dd["data"] = data 187 | return task.again 188 | 189 | base.taskMgr.doMethodLater(0.25, updateTask, "updateDiagram") 190 | 191 | 192 | 193 | app.run() 194 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="DirectGuiExtension", 8 | version="22.09", 9 | author="Fireclaw", 10 | author_email="fireclawthefox@gmail.com", 11 | description="A set of extensions for the DirectGUI system of the Panda3D engine", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/fireclawthefox/DirectGuiExtension", 15 | packages=setuptools.find_packages(), 16 | include_package_data=True, 17 | project_urls = { 18 | "Documentation": "https://github.com/fireclawthefox/DirectGuiExtension/wiki" 19 | }, 20 | classifiers=[ 21 | "Programming Language :: Python :: 3", 22 | "License :: OSI Approved :: BSD License", 23 | "Operating System :: OS Independent", 24 | "Development Status :: 3 - Alpha", 25 | "Intended Audience :: Developers", 26 | "Intended Audience :: End Users/Desktop", 27 | "Topic :: Software Development :: User Interfaces", 28 | ], 29 | install_requires=[ 30 | 'panda3d', 31 | ], 32 | python_requires='>=3.6', 33 | ) 34 | -------------------------------------------------------------------------------- /tabbedFrameExample.py: -------------------------------------------------------------------------------- 1 | from direct.showbase.ShowBase import ShowBase 2 | from direct.gui.DirectFrame import DirectFrame 3 | from DirectGuiExtension.DirectTabbedFrame import DirectTabbedFrame 4 | 5 | ShowBase() 6 | 7 | width = base.get_size()[0] 8 | height = base.get_size()[1] 9 | 10 | dtf = DirectTabbedFrame( 11 | base.pixel2d, 12 | frameSize = (0, width, -height, 0), 13 | tabHeight=24) 14 | 15 | def onCloseFunc(tab): 16 | print(f"closed {tab}") 17 | 18 | fillSize = (0, width, -height, -dtf["tabHeight"]) 19 | 20 | tab1frame = DirectFrame(dtf, frameSize=fillSize, frameColor=(1,0,0,1)) 21 | tab2frame = DirectFrame(dtf, frameSize=fillSize, frameColor=(0,1,0,1)) 22 | tab3frame = DirectFrame(dtf, frameSize=fillSize, frameColor=(0,0,1,1)) 23 | tab4frame = DirectFrame(dtf, frameSize=fillSize, frameColor=(1,0,1,1)) 24 | dtf.add_tab("Tab 1", tab1frame, onCloseFunc) 25 | dtf.add_tab("Tab 2", tab2frame, None) 26 | dtf.add_tab("Tab 3", tab3frame, None) 27 | dtf.add_tab("Tab 4", tab4frame, onCloseFunc) 28 | 29 | base.run() 30 | -------------------------------------------------------------------------------- /treeViewExample.py: -------------------------------------------------------------------------------- 1 | from direct.showbase.ShowBase import ShowBase 2 | from DirectGuiExtension.DirectTreeView import DirectTreeView, DirectTreeEntry 3 | 4 | ShowBase() 5 | 6 | 7 | DirectTreeView( 8 | frameSize=[-0.5,0.5, -0.5,0.5], 9 | autoUpdateFrameSize=False, 10 | tree={ 11 | "A":{DirectTreeEntry("1"):{"X":"0"},"2":None}, 12 | "B":"C", 13 | "D":{DirectTreeEntry("1"):{"Y":{"Z":"0"}}} 14 | }) 15 | 16 | base.run() 17 | --------------------------------------------------------------------------------