commit efe955657991fbd4e02525483e9c3779fa7f3d8d Author: Patrick Wuttke Date: Sat Aug 7 21:15:07 2021 +0200 Initial commit diff --git a/fonts/Vera.ttf b/fonts/Vera.ttf new file mode 100644 index 0000000..58cd6b5 Binary files /dev/null and b/fonts/Vera.ttf differ diff --git a/fonts/VeraLarge.tres b/fonts/VeraLarge.tres new file mode 100644 index 0000000..3bd4229 --- /dev/null +++ b/fonts/VeraLarge.tres @@ -0,0 +1,7 @@ +[gd_resource type="DynamicFont" load_steps=2 format=2] + +[ext_resource path="res://addons/de.mewin.gduibasics/fonts/Vera.ttf" type="DynamicFontData" id=1] + +[resource] +size = 20 +font_data = ExtResource( 1 ) diff --git a/fonts/VeraMedium.tres b/fonts/VeraMedium.tres new file mode 100644 index 0000000..162b377 --- /dev/null +++ b/fonts/VeraMedium.tres @@ -0,0 +1,6 @@ +[gd_resource type="DynamicFont" load_steps=2 format=2] + +[ext_resource path="res://addons/de.mewin.gduibasics/fonts/Vera.ttf" type="DynamicFontData" id=1] + +[resource] +font_data = ExtResource( 1 ) diff --git a/fonts/VeraMono.ttf b/fonts/VeraMono.ttf new file mode 100644 index 0000000..139f0b4 Binary files /dev/null and b/fonts/VeraMono.ttf differ diff --git a/fonts/VeraMonoLarge.tres b/fonts/VeraMonoLarge.tres new file mode 100644 index 0000000..d2cfa1b --- /dev/null +++ b/fonts/VeraMonoLarge.tres @@ -0,0 +1,7 @@ +[gd_resource type="DynamicFont" load_steps=2 format=2] + +[ext_resource path="res://addons/de.mewin.gduibasics/fonts/VeraMono.ttf" type="DynamicFontData" id=1] + +[resource] +size = 20 +font_data = ExtResource( 1 ) diff --git a/fonts/VeraMonoMedium.tres b/fonts/VeraMonoMedium.tres new file mode 100644 index 0000000..5cf16cb --- /dev/null +++ b/fonts/VeraMonoMedium.tres @@ -0,0 +1,6 @@ +[gd_resource type="DynamicFont" load_steps=2 format=2] + +[ext_resource path="res://addons/de.mewin.gduibasics/fonts/VeraMono.ttf" type="DynamicFontData" id=1] + +[resource] +font_data = ExtResource( 1 ) diff --git a/fonts/VeraMonoSmall.tres b/fonts/VeraMonoSmall.tres new file mode 100644 index 0000000..204f3c9 --- /dev/null +++ b/fonts/VeraMonoSmall.tres @@ -0,0 +1,7 @@ +[gd_resource type="DynamicFont" load_steps=2 format=2] + +[ext_resource path="res://addons/de.mewin.gduibasics/fonts/VeraMono.ttf" type="DynamicFontData" id=1] + +[resource] +size = 12 +font_data = ExtResource( 1 ) diff --git a/fonts/VeraSmall.tres b/fonts/VeraSmall.tres new file mode 100644 index 0000000..4f7b121 --- /dev/null +++ b/fonts/VeraSmall.tres @@ -0,0 +1,7 @@ +[gd_resource type="DynamicFont" load_steps=2 format=2] + +[ext_resource path="res://addons/de.mewin.gduibasics/fonts/Vera.ttf" type="DynamicFontData" id=1] + +[resource] +size = 12 +font_data = ExtResource( 1 ) diff --git a/fonts/Vera_COPYRIGHT.TXT b/fonts/Vera_COPYRIGHT.TXT new file mode 100644 index 0000000..e651be1 --- /dev/null +++ b/fonts/Vera_COPYRIGHT.TXT @@ -0,0 +1,124 @@ +Bitstream Vera Fonts Copyright + +The fonts have a generous copyright, allowing derivative works (as +long as "Bitstream" or "Vera" are not in the names), and full +redistribution (so long as they are not *sold* by themselves). They +can be be bundled, redistributed and sold with any software. + +The fonts are distributed under the following copyright: + +Copyright +========= + +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream +Vera is a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the fonts accompanying this license ("Fonts") and associated +documentation files (the "Font Software"), to reproduce and distribute +the Font Software, including without limitation the rights to use, +copy, merge, publish, distribute, and/or sell copies of the Font +Software, and to permit persons to whom the Font Software is furnished +to do so, subject to the following conditions: + +The above copyright and trademark notices and this permission notice +shall be included in all copies of one or more of the Font Software +typefaces. + +The Font Software may be modified, altered, or added to, and in +particular the designs of glyphs or characters in the Fonts may be +modified and additional glyphs or characters may be added to the +Fonts, only if the fonts are renamed to names not containing either +the words "Bitstream" or the word "Vera". + +This License becomes null and void to the extent applicable to Fonts +or Font Software that has been modified and is distributed under the +"Bitstream Vera" names. + +The Font Software may be sold as part of a larger software package but +no copy of one or more of the Font Software typefaces may be sold by +itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL +BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, +OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT +SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome +Foundation, and Bitstream Inc., shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this Font +Software without prior written authorization from the Gnome Foundation +or Bitstream Inc., respectively. For further information, contact: +fonts at gnome dot org. + +Copyright FAQ +============= + + 1. I don't understand the resale restriction... What gives? + + Bitstream is giving away these fonts, but wishes to ensure its + competitors can't just drop the fonts as is into a font sale system + and sell them as is. It seems fair that if Bitstream can't make money + from the Bitstream Vera fonts, their competitors should not be able to + do so either. You can sell the fonts as part of any software package, + however. + + 2. I want to package these fonts separately for distribution and + sale as part of a larger software package or system. Can I do so? + + Yes. A RPM or Debian package is a "larger software package" to begin + with, and you aren't selling them independently by themselves. + See 1. above. + + 3. Are derivative works allowed? + Yes! + + 4. Can I change or add to the font(s)? + Yes, but you must change the name(s) of the font(s). + + 5. Under what terms are derivative works allowed? + + You must change the name(s) of the fonts. This is to ensure the + quality of the fonts, both to protect Bitstream and Gnome. We want to + ensure that if an application has opened a font specifically of these + names, it gets what it expects (though of course, using fontconfig, + substitutions could still could have occurred during font + opening). You must include the Bitstream copyright. Additional + copyrights can be added, as per copyright law. Happy Font Hacking! + + 6. If I have improvements for Bitstream Vera, is it possible they might get + adopted in future versions? + + Yes. The contract between the Gnome Foundation and Bitstream has + provisions for working with Bitstream to ensure quality additions to + the Bitstream Vera font family. Please contact us if you have such + additions. Note, that in general, we will want such additions for the + entire family, not just a single font, and that you'll have to keep + both Gnome and Jim Lyles, Vera's designer, happy! To make sense to add + glyphs to the font, they must be stylistically in keeping with Vera's + design. Vera cannot become a "ransom note" font. Jim Lyles will be + providing a document describing the design elements used in Vera, as a + guide and aid for people interested in contributing to Vera. + + 7. I want to sell a software package that uses these fonts: Can I do so? + + Sure. Bundle the fonts with your software and sell your software + with the fonts. That is the intent of the copyright. + + 8. If applications have built the names "Bitstream Vera" into them, + can I override this somehow to use fonts of my choosing? + + This depends on exact details of the software. Most open source + systems and software (e.g., Gnome, KDE, etc.) are now converting to + use fontconfig (see www.fontconfig.org) to handle font configuration, + selection and substitution; it has provisions for overriding font + names and subsituting alternatives. An example is provided by the + supplied local.conf file, which chooses the family Bitstream Vera for + "sans", "serif" and "monospace". Other software (e.g., the XFree86 + core server) has other mechanisms for font substitution. + diff --git a/images/action.svg b/images/action.svg new file mode 100644 index 0000000..bb6c520 --- /dev/null +++ b/images/action.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/images/action.svg.import b/images/action.svg.import new file mode 100644 index 0000000..9497a93 --- /dev/null +++ b/images/action.svg.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/action.svg-b508461e00794f206592b7f3abb0bf50.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/de.mewin.gduibasics/images/action.svg" +dest_files=[ "res://.import/action.svg-b508461e00794f206592b7f3abb0bf50.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/images/close.svg b/images/close.svg new file mode 100644 index 0000000..df37d94 --- /dev/null +++ b/images/close.svg @@ -0,0 +1,68 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/images/close.svg.import b/images/close.svg.import new file mode 100644 index 0000000..db94bcb --- /dev/null +++ b/images/close.svg.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/close.svg-4a9c4498aff69d70db99b31f6d40ad26.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/de.mewin.gduibasics/images/close.svg" +dest_files=[ "res://.import/close.svg-4a9c4498aff69d70db99b31f6d40ad26.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/images/favourite.svg b/images/favourite.svg new file mode 100644 index 0000000..1a857ce --- /dev/null +++ b/images/favourite.svg @@ -0,0 +1,63 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/images/favourite.svg.import b/images/favourite.svg.import new file mode 100644 index 0000000..4039016 --- /dev/null +++ b/images/favourite.svg.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/favourite.svg-e7f3ad202c86f3b36183a480a4c15217.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/de.mewin.gduibasics/images/favourite.svg" +dest_files=[ "res://.import/favourite.svg-e7f3ad202c86f3b36183a480a4c15217.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/images/favourite32.svg b/images/favourite32.svg new file mode 100644 index 0000000..6f8dc1d --- /dev/null +++ b/images/favourite32.svg @@ -0,0 +1,63 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/images/favourite32.svg.import b/images/favourite32.svg.import new file mode 100644 index 0000000..6a3d71e --- /dev/null +++ b/images/favourite32.svg.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/favourite32.svg-c47e3f97bb8ca7f215e11bab06aac90a.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/de.mewin.gduibasics/images/favourite32.svg" +dest_files=[ "res://.import/favourite32.svg-c47e3f97bb8ca7f215e11bab06aac90a.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/images/favourite_inactive.svg b/images/favourite_inactive.svg new file mode 100644 index 0000000..83f510d --- /dev/null +++ b/images/favourite_inactive.svg @@ -0,0 +1,63 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/images/favourite_inactive.svg.import b/images/favourite_inactive.svg.import new file mode 100644 index 0000000..8aafda1 --- /dev/null +++ b/images/favourite_inactive.svg.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/favourite_inactive.svg-0714561913ff7c32510529fe22e8eec7.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/de.mewin.gduibasics/images/favourite_inactive.svg" +dest_files=[ "res://.import/favourite_inactive.svg-0714561913ff7c32510529fe22e8eec7.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/images/file.svg b/images/file.svg new file mode 100644 index 0000000..ee99c75 --- /dev/null +++ b/images/file.svg @@ -0,0 +1,65 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/images/file.svg.import b/images/file.svg.import new file mode 100644 index 0000000..96159d7 --- /dev/null +++ b/images/file.svg.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/file.svg-4e6b8b924038da5cf70f0f5467754065.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/de.mewin.gduibasics/images/file.svg" +dest_files=[ "res://.import/file.svg-4e6b8b924038da5cf70f0f5467754065.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/images/folder.svg b/images/folder.svg new file mode 100644 index 0000000..a628045 --- /dev/null +++ b/images/folder.svg @@ -0,0 +1,66 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/images/folder.svg.import b/images/folder.svg.import new file mode 100644 index 0000000..9d9955a --- /dev/null +++ b/images/folder.svg.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/folder.svg-7e8d363eac08897a3ba3577c7fe476ba.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/de.mewin.gduibasics/images/folder.svg" +dest_files=[ "res://.import/folder.svg-7e8d363eac08897a3ba3577c7fe476ba.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/images/folder32.svg b/images/folder32.svg new file mode 100644 index 0000000..c3a644c --- /dev/null +++ b/images/folder32.svg @@ -0,0 +1,66 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/images/folder32.svg.import b/images/folder32.svg.import new file mode 100644 index 0000000..30220ea --- /dev/null +++ b/images/folder32.svg.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/folder32.svg-c88050110add556743258a22e5552a5f.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/de.mewin.gduibasics/images/folder32.svg" +dest_files=[ "res://.import/folder32.svg-c88050110add556743258a22e5552a5f.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/images/home32.svg b/images/home32.svg new file mode 100644 index 0000000..7b49f27 --- /dev/null +++ b/images/home32.svg @@ -0,0 +1,66 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/images/home32.svg.import b/images/home32.svg.import new file mode 100644 index 0000000..6baa3f1 --- /dev/null +++ b/images/home32.svg.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/home32.svg-6a061fc9d14c6834c99c31a441f002f7.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/de.mewin.gduibasics/images/home32.svg" +dest_files=[ "res://.import/home32.svg-6a061fc9d14c6834c99c31a441f002f7.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/images/maximize.svg b/images/maximize.svg new file mode 100644 index 0000000..90041ac --- /dev/null +++ b/images/maximize.svg @@ -0,0 +1,63 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/images/maximize.svg.import b/images/maximize.svg.import new file mode 100644 index 0000000..447524c --- /dev/null +++ b/images/maximize.svg.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/maximize.svg-3fa9bb18e25e21cc6b72c8baa93b174b.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/de.mewin.gduibasics/images/maximize.svg" +dest_files=[ "res://.import/maximize.svg-3fa9bb18e25e21cc6b72c8baa93b174b.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/images/minimize.svg b/images/minimize.svg new file mode 100644 index 0000000..e025c00 --- /dev/null +++ b/images/minimize.svg @@ -0,0 +1,67 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/images/minimize.svg.import b/images/minimize.svg.import new file mode 100644 index 0000000..cf4e9e4 --- /dev/null +++ b/images/minimize.svg.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/minimize.svg-79dc3404e073f085703d8b02fd39b093.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/de.mewin.gduibasics/images/minimize.svg" +dest_files=[ "res://.import/minimize.svg-79dc3404e073f085703d8b02fd39b093.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/plugin.cfg b/plugin.cfg new file mode 100644 index 0000000..0652f84 --- /dev/null +++ b/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="GDScript UI Basics" +description="Basic and advanced UI elements for Godot. (Requires GDBasics)" +author="Patrick Wuttke" +version="0.1" +script="plugin.gd" diff --git a/plugin.gd b/plugin.gd new file mode 100644 index 0000000..824ed80 --- /dev/null +++ b/plugin.gd @@ -0,0 +1,8 @@ +tool +extends EditorPlugin + +func _enter_tree(): + pass + +func _exit_tree(): + pass diff --git a/scenes/dynamic_tabs.tscn b/scenes/dynamic_tabs.tscn new file mode 100644 index 0000000..b6f447f --- /dev/null +++ b/scenes/dynamic_tabs.tscn @@ -0,0 +1,22 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://addons/de.mewin.gduibasics/scenes/scripts/dynamic_tabs.gd" type="Script" id=1] + +[node name="dynamic_tabs" type="VBoxContainer"] +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="tabs" type="Tabs" parent="."] +margin_right = 1024.0 +margin_bottom = 24.0 + +[node name="container" type="TabContainer" parent="."] +margin_top = 28.0 +margin_right = 1024.0 +margin_bottom = 600.0 +size_flags_vertical = 3 +tabs_visible = false diff --git a/scenes/editor_file_edit.tscn b/scenes/editor_file_edit.tscn new file mode 100644 index 0000000..f5ae5a4 --- /dev/null +++ b/scenes/editor_file_edit.tscn @@ -0,0 +1,32 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://addons/de.mewin.gduibasics/scenes/scripts/editor_file_edit.gd" type="Script" id=1] + +[node name="editor_file_edit" type="HBoxContainer"] +anchor_right = 1.0 +margin_bottom = 24.0 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="line_edit" type="LineEdit" parent="."] +margin_right = 969.0 +margin_bottom = 24.0 +size_flags_horizontal = 3 +size_flags_vertical = 0 +editable = false + +[node name="button" type="Button" parent="."] +margin_left = 973.0 +margin_right = 1024.0 +margin_bottom = 24.0 +text = "Select" + +[node name="file_dialog" type="FileDialog" parent="."] +margin_right = 307.0 +margin_bottom = 130.0 +window_title = "Open a File" +mode = 0 +[connection signal="pressed" from="button" to="." method="_on_button_pressed"] +[connection signal="file_selected" from="file_dialog" to="." method="_on_file_dialog_file_selected"] diff --git a/scenes/editor_node_edit.tscn b/scenes/editor_node_edit.tscn new file mode 100644 index 0000000..9a0786c --- /dev/null +++ b/scenes/editor_node_edit.tscn @@ -0,0 +1,36 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://addons/de.mewin.gduibasics/scenes/scripts/editor_node_edit.gd" type="Script" id=1] +[ext_resource path="res://addons/de.mewin.gduibasics/scenes/node_selection_dialog.tscn" type="PackedScene" id=2] + +[node name="editor_node_edit" type="HBoxContainer"] +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_bottom = -576.0 +script = ExtResource( 1 ) +__meta__ = { +"__property_type__": 15, +"_edit_use_anchors_": false +} + +[node name="line_edit" type="LineEdit" parent="."] +margin_right = 969.0 +margin_bottom = 24.0 +size_flags_horizontal = 3 +size_flags_vertical = 0 +editable = false + +[node name="button" type="Button" parent="."] +margin_left = 973.0 +margin_right = 1024.0 +margin_bottom = 24.0 +text = "Select" + +[node name="node_selection_dialog" parent="." instance=ExtResource( 2 )] +margin_left = 824.0 +margin_top = 0.0 +margin_right = 1024.0 +margin_bottom = 70.0 +window_title = "Select a Node ..." +[connection signal="pressed" from="button" to="." method="_on_button_pressed"] +[connection signal="confirmed" from="node_selection_dialog" to="." method="_on_node_selection_dialog_confirmed"] diff --git a/scenes/flexview.tscn b/scenes/flexview.tscn new file mode 100644 index 0000000..ab312bb --- /dev/null +++ b/scenes/flexview.tscn @@ -0,0 +1,97 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://addons/de.mewin.gduibasics/scenes/dynamic_tabs.tscn" type="PackedScene" id=1] +[ext_resource path="res://addons/de.mewin.gduibasics/scenes/scripts/flexview.gd" type="Script" id=2] + +[sub_resource type="StyleBoxFlat" id=1] +bg_color = Color( 0, 0.388235, 1, 0.705882 ) +border_width_left = 5 +border_width_top = 5 +border_width_right = 5 +border_width_bottom = 5 +border_color = Color( 0, 0.301961, 0.796078, 0.705882 ) +border_blend = true + +[node name="flex_view" type="Control"] +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource( 2 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="tabs" parent="." instance=ExtResource( 1 )] +drag_to_rearrange_enabled = true +drag_out_enabled = true +tab_close_display_policy = 2 +tab_align = 0 + +[node name="drop_indicator" type="Control" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 +mouse_filter = 2 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="rect_left" type="Control" parent="drop_indicator"] +visible = false +anchor_right = 0.5 +anchor_bottom = 1.0 +__meta__ = { +"_edit_use_anchors_": true +} + +[node name="rect_right" type="Control" parent="drop_indicator"] +visible = false +anchor_left = 0.5 +anchor_right = 1.00098 +anchor_bottom = 1.0 +__meta__ = { +"_edit_use_anchors_": true +} + +[node name="rect_top" type="Control" parent="drop_indicator"] +visible = false +anchor_right = 1.0 +anchor_bottom = 0.5 +__meta__ = { +"_edit_use_anchors_": true +} + +[node name="rect_bottom" type="Control" parent="drop_indicator"] +visible = false +anchor_top = 0.5 +anchor_right = 1.0 +anchor_bottom = 1.0 +__meta__ = { +"_edit_use_anchors_": true +} + +[node name="rect_center" type="Control" parent="drop_indicator"] +visible = false +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_left = 200.0 +margin_top = 117.0 +margin_right = -200.0 +margin_bottom = -117.0 +__meta__ = { +"_edit_use_anchors_": true +} + +[node name="drop_indicator" type="Panel" parent="drop_indicator"] +visible = false +modulate = Color( 1, 1, 1, 0 ) +anchor_right = 1.0 +anchor_bottom = 1.0 +mouse_filter = 2 +custom_styles/panel = SubResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="tween" type="Tween" parent="drop_indicator"] + +[connection signal="tab_close" from="tabs" to="." method="_on_tabs_tab_close"] +[connection signal="tab_dragged_out" from="tabs" to="." method="_on_tabs_tab_dragged_out"] diff --git a/scenes/flexview_hspliter.tscn b/scenes/flexview_hspliter.tscn new file mode 100644 index 0000000..d878c45 --- /dev/null +++ b/scenes/flexview_hspliter.tscn @@ -0,0 +1,11 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://addons/de.mewin.gduibasics/scenes/scripts/flexview_splitter.gd" type="Script" id=1] + +[node name="flexview_hsplitter" type="HSplitContainer"] +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} diff --git a/scenes/flexview_vspliter.tscn b/scenes/flexview_vspliter.tscn new file mode 100644 index 0000000..13604e5 --- /dev/null +++ b/scenes/flexview_vspliter.tscn @@ -0,0 +1,8 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://addons/de.mewin.gduibasics/scenes/scripts/flexview_splitter.gd" type="Script" id=1] + +[node name="flexview_hsplitter" type="VSplitContainer"] +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource( 1 ) diff --git a/scenes/flexview_window.tscn b/scenes/flexview_window.tscn new file mode 100644 index 0000000..58a9128 --- /dev/null +++ b/scenes/flexview_window.tscn @@ -0,0 +1,21 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://addons/de.mewin.gduibasics/scenes/flexview.tscn" type="PackedScene" id=1] +[ext_resource path="res://addons/de.mewin.gduibasics/scenes/scripts/flexview_window.gd" type="Script" id=2] + +[node name="flexview_window" type="WindowDialog"] +anchor_right = 1.0 +anchor_bottom = 1.0 +resizable = true +script = ExtResource( 2 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="flex_view" parent="." instance=ExtResource( 1 )] + +[connection signal="item_rect_changed" from="." to="." method="_on_flexview_window_item_rect_changed"] +[connection signal="visibility_changed" from="." to="." method="_on_flexview_window_visibility_changed"] +[connection signal="current_view_changed" from="flex_view" to="." method="_on_flex_view_current_view_changed"] +[connection signal="view_added" from="flex_view" to="." method="_on_flex_view_view_added"] +[connection signal="view_removed" from="flex_view" to="." method="_on_flex_view_view_removed"] diff --git a/scenes/line_edit_suggestion_popup.tscn b/scenes/line_edit_suggestion_popup.tscn new file mode 100644 index 0000000..f198c3d --- /dev/null +++ b/scenes/line_edit_suggestion_popup.tscn @@ -0,0 +1,27 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://addons/de.mewin.gduibasics/scenes/scripts/line_edit_suggestion_popup.gd" type="Script" id=1] + + +[node name="line_edit_suggestion_popup" type="Popup"] +anchor_right = 1.0 +anchor_bottom = 1.0 +rect_min_size = Vector2( 0, 100 ) +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="panel" type="Panel" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="v_box_container" type="VBoxContainer" parent="panel"] +anchor_right = 1.0 +anchor_bottom = 1.0 +__meta__ = { +"_edit_use_anchors_": false +} diff --git a/scenes/line_input_dialog.tscn b/scenes/line_input_dialog.tscn new file mode 100644 index 0000000..db0e263 --- /dev/null +++ b/scenes/line_input_dialog.tscn @@ -0,0 +1,51 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://addons/de.mewin.gduibasics/scenes/scripts/line_input_dialog.gd" type="Script" id=1] + +[node name="line_input_dialog" type="ConfirmationDialog"] +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_right = -524.0 +margin_bottom = -499.0 +rect_min_size = Vector2( 500, 90 ) +window_title = "Enter a text..." +dialog_text = "Enter a text:" +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="container" type="VBoxContainer" parent="."] +margin_left = 8.0 +margin_top = 22.0 +margin_right = 492.0 +margin_bottom = 79.0 +mouse_filter = 2 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="control" type="Control" parent="container"] +margin_right = 484.0 +margin_bottom = 5.0 +rect_min_size = Vector2( 0, 5 ) + +[node name="line_edit" type="LineEdit" parent="container"] +margin_top = 9.0 +margin_right = 484.0 +margin_bottom = 33.0 +size_flags_horizontal = 3 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="control2" type="Control" parent="container"] +margin_top = 37.0 +margin_right = 484.0 +margin_bottom = 57.0 +rect_min_size = Vector2( 0, 20 ) +mouse_filter = 2 +[connection signal="about_to_show" from="." to="." method="_on_line_input_dialog_about_to_show"] +[connection signal="visibility_changed" from="." to="." method="_on_line_input_dialog_visibility_changed"] +[connection signal="text_changed" from="container/line_edit" to="." method="_on_line_edit_text_changed"] +[connection signal="text_entered" from="container/line_edit" to="." method="_on_line_edit_text_entered"] diff --git a/scenes/node_selection_dialog.tscn b/scenes/node_selection_dialog.tscn new file mode 100644 index 0000000..b9ac83f --- /dev/null +++ b/scenes/node_selection_dialog.tscn @@ -0,0 +1,26 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://addons/de.mewin.gduibasics/scripts/types/controls/editor_node_tree.gd" type="Script" id=1] +[ext_resource path="res://addons/de.mewin.gduibasics/scenes/scripts/node_selection_dialog.gd" type="Script" id=2] + +[node name="node_selection_dialog" type="ConfirmationDialog"] +margin_left = 233.0 +margin_top = 45.0 +margin_right = 843.0 +margin_bottom = 516.0 +script = ExtResource( 2 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="tree" type="Tree" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_left = 8.0 +margin_top = 8.0 +margin_right = -8.0 +margin_bottom = -36.0 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} diff --git a/scenes/scripts/dynamic_tabs.gd b/scenes/scripts/dynamic_tabs.gd new file mode 100644 index 0000000..8668ff8 --- /dev/null +++ b/scenes/scripts/dynamic_tabs.gd @@ -0,0 +1,174 @@ +extends Control + +enum CloseButtonPolicy { + SHOW_NEVER = 0, + SHOW_ACTIVE_ONLY = 1, + SHOW_ALWAYS = 2 +} + +enum TabAlign { + ALIGN_LEFT = 0, + ALIGN_CENTER = 1, + ALIGN_RIGHT = 2 +} + +export var current_tab := 0 setget _set_current_tab, _get_current_tab +export var drag_to_rearrange_enabled := false setget _set_drag_to_rearrange_enabled +export var drag_out_enabled := false +export var show_tabs := true setget _set_show_tabs +export(CloseButtonPolicy) var tab_close_display_policy := CloseButtonPolicy.SHOW_NEVER setget _set_tab_close_display_policy +export(TabAlign) var tab_align := TabAlign.ALIGN_CENTER setget _set_tab_align + +var __mouse_was_inside := false + +############# +# overrides # +############# +func _ready() -> void: + $tabs.connect("tab_changed", self, "_on_tabs_tab_changed") + $tabs.connect("tab_close", self, "_on_tabs_tab_close") + $tabs.connect("reposition_active_tab_request", self, "_on_tabs_reposition_active_tab_request") + $tabs.connect("gui_input", self, "_on_tabs_gui_input") + + set_process(false) # only enabled for dragging + +func _process(delta : float) -> void: + if !Input.is_mouse_button_pressed(BUTTON_LEFT): + __stop_tab_dragging() + return + var mouse_pos := get_viewport().get_mouse_position() + var mouse_is_inside : bool = $tabs.get_global_rect().has_point(mouse_pos) + if mouse_is_inside == __mouse_was_inside: + return + __mouse_was_inside = mouse_is_inside + if mouse_is_inside: + emit_signal("tab_dragged_in", _get_current_tab(), mouse_pos) + else: + emit_signal("tab_dragged_out", _get_current_tab(), mouse_pos) + +################ +# public stuff # +################ +func add_tab(title : String, control : Control, icon : Texture = null) -> int: + $tabs.add_tab(title, icon) + $container.add_child(control) + + var tab := control.get_position_in_parent() + emit_signal("tab_added", tab) + return tab + +func move_tab(from : int, to : int) -> void: + $tabs.move_tab(from, to) + $container.move_child($container.get_child(from), to) + +func remove_tab(tab : int) -> void: + if tab == current_tab && __is_tab_dragging(): + __stop_tab_dragging() + + $tabs.remove_tab(tab) + $container.remove_child($container.get_child(tab)) + emit_signal("tab_removed", tab) + +func find_tab(control : Control) -> int: + for tab in range(get_tab_count()): + if get_tab_control(tab) == control: + return tab + return -1 + +func remove_all_tabs() -> void: + while $tabs.get_tab_count() > 0: + $tabs.remove_tab(0) + while $container.get_child_count() > 0: + $container.remove_child($container.get_child(0)) + +func get_tab_control(tab : int) -> Control: + return $container.get_tab_control(tab) + +func get_tab_title(tab : int) -> String: + return $tabs.get_tab_title(tab) + +func get_tabs() -> Array: + return $container.get_children() + +func get_tab_count() -> int: + return $container.get_child_count() + +func get_tab_rect(tab : int) -> Rect2: + var rect : Rect2 = $tabs.get_tab_rect(tab) + return $tabs.get_transform().xform_inv(rect) + +################# +# private stuff # +################# +func __handle_tabs_gui_input(event : InputEvent) -> void: + if drag_out_enabled \ + && event is InputEventMouseButton \ + && event.pressed \ + && event.button_index == BUTTON_LEFT: + yield(get_tree(), "idle_frame") # wait for the original control to handle the input + + var tab_rect : Rect2 = $tabs.get_tab_rect(_get_current_tab()) + if tab_rect.has_point(event.position): + __start_tab_dragging() + +func __start_tab_dragging() -> void: + __mouse_was_inside = true + set_process(true) + +func __stop_tab_dragging() -> void: + set_process(false) + +func __is_tab_dragging() -> bool: + return is_processing() + +########### +# setters # +########### +func _set_current_tab(tab : int) -> void: + current_tab = tab + $tabs.current_tab = current_tab + +func _get_current_tab() -> int: + return $tabs.current_tab + +func _set_drag_to_rearrange_enabled(enabled : bool) -> void: + drag_to_rearrange_enabled = enabled + $tabs.drag_to_rearrange_enabled = enabled + +func _set_tab_close_display_policy(policy : int) -> void: + tab_close_display_policy = policy + $tabs.tab_close_display_policy = policy + +func _set_tab_align(align : int) -> void: + tab_align = align + $tabs.tab_align = align + +func _set_show_tabs(show : bool) -> void: + show_tabs = show + $tabs.visible = show + +################### +# signal handlers # +################### +func _on_tabs_tab_changed(tab : int) -> void: + current_tab = tab + $container.current_tab = tab + +func _on_tabs_tab_close(tab : int) -> void: + emit_signal("tab_close", tab) + +func _on_tabs_reposition_active_tab_request(to : int) -> void: + $container.move_child($container.get_child(_get_current_tab()), to) + current_tab = to + +func _on_tabs_gui_input(event : InputEvent) -> void: + __handle_tabs_gui_input(event) + +########### +# signals # +########### +signal tab_added(tab) +signal tab_removed(tab) +signal tab_close(tab) +signal tab_dragged_out(tab, position) +signal tab_dragged_in(tab, position) diff --git a/scenes/scripts/editor_file_edit.gd b/scenes/scripts/editor_file_edit.gd new file mode 100644 index 0000000..f1c0ae1 --- /dev/null +++ b/scenes/scripts/editor_file_edit.gd @@ -0,0 +1,31 @@ +tool +extends Control + +onready var line_edit := $line_edit +onready var button := $button +onready var file_dialog := $file_dialog + +export(String, FILE) var selected_file setget _set_selected_file +var file_filter = "" + +func _ready(): + line_edit.text = selected_file + +func _set_selected_file(value : String): + if value != selected_file: + selected_file = value + if line_edit: + line_edit.text = selected_file + +func _on_button_pressed(): + file_dialog.clear_filters() + file_dialog.add_filter(file_filter) + file_dialog.filename = selected_file + file_dialog.popup_centered_ratio(0.5) + +func _on_file_dialog_file_selected(path : String): + if path != selected_file: + _set_selected_file(path) + emit_signal("selected_file_changed") + +signal selected_file_changed() diff --git a/scenes/scripts/editor_node_edit.gd b/scenes/scripts/editor_node_edit.gd new file mode 100644 index 0000000..1ea234a --- /dev/null +++ b/scenes/scripts/editor_node_edit.gd @@ -0,0 +1,31 @@ +tool +extends Control + +onready var line_edit := $line_edit +onready var button := $button +onready var node_selection_dialog := $node_selection_dialog + +export(NodePath) var selected_node setget _set_selected_node +var node_type_filter = Node + +func _ready(): + line_edit.text = str(selected_node) + +func _set_selected_node(value : NodePath): + if value != selected_node: + selected_node = value + if line_edit: + line_edit.text = str(selected_node) + +func _on_button_pressed(): + node_selection_dialog.get_editor_node_tree().node_type_filter = node_type_filter + node_selection_dialog.get_editor_node_tree().selected_node = selected_node + node_selection_dialog.popup_centered_ratio(0.5) + +func _on_node_selection_dialog_confirmed(): + var tree = node_selection_dialog.get_editor_node_tree() + if tree.selected_node != selected_node: + _set_selected_node(tree.selected_node) + emit_signal("selected_node_changed") + +signal selected_node_changed() diff --git a/scenes/scripts/flexview.gd b/scenes/scripts/flexview.gd new file mode 100644 index 0000000..fa937e0 --- /dev/null +++ b/scenes/scripts/flexview.gd @@ -0,0 +1,371 @@ +extends Control + +enum SplitLocation { + TOP, + BOTTOM, + LEFT, + RIGHT +} +enum DropPosition { + TOP, + BOTTOM, + LEFT, + RIGHT, + CENTER +} + +const FlexViewSplitter = preload("res://addons/de.mewin.gduibasics/scenes/scripts/flexview_splitter.gd") + +const META_UNIQUE_TYPE = "__flexview_unique_type__" +const META_UNIQUE_KEY = "__flexview_unique_key__" +const META_VIEW_TITLE = "__flexview_title__" +const META_LAST_DROP_TARGET = "__flexview_last_drop_target__" +const GROUP_NAME = "__flexview__" +const INDICATOR_ANIM_DURATION = 0.1 +const __HANDLERS_TO_COPY = ["view_added", "view_removed", "view_closing"] + +onready var _rect_left := $drop_indicator/rect_left +onready var _rect_right := $drop_indicator/rect_right +onready var _rect_top := $drop_indicator/rect_top +onready var _rect_bottom := $drop_indicator/rect_bottom +onready var _rect_center := $drop_indicator/rect_center +onready var _drop_indicator := $drop_indicator/drop_indicator +onready var _tween := $drop_indicator/tween + +var current_view := 0 setget _set_current_view, _get_current_view +export var show_tabs := true setget _set_show_tabs + +var __drop_position := -1 + +############# +# overrides # +############# +func _ready() -> void: + set_process(false) # only used for the drop indicator + add_to_group(GROUP_NAME) + $tabs.show_tabs = show_tabs + +func _process(delta : float) -> void: + var mouse_pos := get_viewport().get_mouse_position() + var drop_position : int = DropPosition.CENTER + + if get_view_count() > 0: # only allow splitting if there is at least one view + var my_rect := get_global_rect() + var distances := [ + (mouse_pos.y - my_rect.position.y) / my_rect.size.y, + (my_rect.position.y + my_rect.size.y - mouse_pos.y) / my_rect.size.y, + (mouse_pos.x - my_rect.position.x) / my_rect.size.x, + (my_rect.position.x + my_rect.size.x - mouse_pos.x) / my_rect.size.x + ] + var idx := GDBMath.min_element(distances) + if distances[idx] < 0.25: + drop_position = idx + + if __drop_position == drop_position: + return + __drop_position = drop_position + + var new_rect := __get_drop_rect(drop_position) + _tween.stop(_drop_indicator, "rect_size") + _tween.stop(_drop_indicator, "rect_position") + _tween.interpolate_property(_drop_indicator, "rect_size", _drop_indicator.rect_size, new_rect.size, INDICATOR_ANIM_DURATION) + _tween.interpolate_property(_drop_indicator, "rect_position", _drop_indicator.rect_position, new_rect.position, INDICATOR_ANIM_DURATION) + _tween.start() + +################ +# public stuff # +################ +func add_view(title : String, control : Control, make_current := true) -> int: + control.set_meta(META_VIEW_TITLE, title) + + var view : int = $tabs.add_tab(get_view_title_from_control(control), control) + emit_signal("view_added", view, self) + if make_current: + _set_current_view(view) + + # support for control-controlled (lel) titles + if control.has_signal("flexview_title_changed"): + control.connect("flexview_title_changed", self, "_on_flexview_title_changed") + if control.has_signal("flexview_close_me"): + control.connect("flexview_close_me", self, "_on_flexview_close_me") + + return view + +func remove_view(view : int) -> void: + var control := get_view_control(view) + if control: + GDBUtility.disconnect_all(control, self) + + $tabs.remove_tab(view) + emit_signal("view_removed", view, self) + + if get_view_count() == 0 && get_parent() is FlexViewSplitter: + self.queue_free() + +func close_view(view : int) -> void: + emit_signal("view_closing", view, self) + +func remove_all_views() -> void: + for view in range(get_view_count() - 1, -1, -1): + remove_view(view) + +func find_view(control : Control) -> int: + return $tabs.find_tab(control) + +func close_all_views() -> void: + for view in range(get_view_count() - 1, -1, -1): + close_view(view) + +func set_view_title(view : int, title : String) -> void: + $tabs.set_tab_title(view, title) + +func get_view_control(view : int) -> Control: + return $tabs.get_tab_control(view) + +func get_view_title(view : int) -> String: + return $tabs.get_tab_title(view) + +func get_views() -> Array: + return $tabs.get_tabs() + +func get_view_count() -> int: + return $tabs.get_tab_count() + +func split_view(placement : int, title : String, view : Control) -> Control: + var parent := get_parent() + var self_first : bool = (placement == SplitLocation.BOTTOM || placement == SplitLocation.RIGHT) + + # add splitter + var splitter : SplitContainer + if placement == SplitLocation.TOP || placement == SplitLocation.BOTTOM: + splitter = preload("res://addons/de.mewin.gduibasics/scenes/flexview_vspliter.tscn").instance() + else: + splitter = preload("res://addons/de.mewin.gduibasics/scenes/flexview_hspliter.tscn").instance() + parent.add_child_below_node(self, splitter) + parent.remove_child(self) + + GDBUIUtility.copy_size(splitter, self) + + # add self first (if applicable) + if self_first: + splitter.add_child(self) + + # create new flex container for other half + var new_container : Control = load("res://addons/de.mewin.gduibasics/scenes/flexview.tscn").instance() + new_container.add_view(title, view) + __copy_signal_handlers(new_container) + splitter.add_child(new_container) + + # add self second (if applicable) + if !self_first: + splitter.add_child(self) + + if placement == SplitLocation.TOP || placement == SplitLocation.BOTTOM: + splitter.split_offset = 0.5 * splitter.rect_size.y + else: + splitter.split_offset = 0.5 * splitter.rect_size.x + splitter.clamp_split_offset() + return new_container + +################ +# static stuff # +################ +static func get_view_title_from_control(control : Control) -> String: + var prop = control.get("flexview_title") + if prop is String: + return prop + + var title = control.get_meta(META_VIEW_TITLE) + if title is String: + return title + return control.name + +static func get_all_flex_views() -> Array: + return GDBUtility.get_scene_tree().get_nodes_in_group(GROUP_NAME) + +static func get_all_views() -> Array: + var views := [] + for flex_view in get_all_flex_views(): + views.append_array(flex_view.get_views()) + return views + +static func get_top_flex_view() -> Control: + # can't use GDBUtility.find_node_by_type as we cannot use our own type + var all_views := get_all_flex_views() + var top_view : Control = null + for flex_view in all_views: + if top_view == null || top_view.is_greater_than(flex_view): + top_view = flex_view + return top_view + +static func add_view_anywhere(title : String, view : Control, make_current := true) -> Control: + var flex_view := _get_last_drop_target() + if flex_view == null: + flex_view = get_top_flex_view() + if flex_view != null: + flex_view.add_view(title, view, make_current) + return flex_view + +static func add_unique_view_anywhere(title : String, view : Control, type : String, key, make_current := true) -> Control: + var flex_view := add_view_anywhere(title, view, make_current) + if flex_view == null: # failed + return null + view.set_meta(META_UNIQUE_TYPE, type) + view.set_meta(META_UNIQUE_KEY, key) + return flex_view + +static func remove_all_views_anywhere() -> void: + for flex_view in get_all_flex_views(): + flex_view.remove_all_views() + +static func find_unique_view(type : String, key) -> Control: + for view_ctrl in get_all_views(): + if view_ctrl.has_meta(META_UNIQUE_TYPE) && view_ctrl.has_meta(META_UNIQUE_KEY)\ + && view_ctrl.get_meta(META_UNIQUE_TYPE) == type && view_ctrl.get_meta(META_UNIQUE_KEY) == key: + return view_ctrl + return null + +static func find_flex_view(view_control : Control) -> Control: + var my_script : Script = load("res://addons/de.mewin.gduibasics/scripts/types/controls/flexview.gd") + return GDBUtility.find_parent_with_type(view_control, my_script) as Control + +static func make_view_current(view_control : Control) -> void: + var flex_view := find_flex_view(view_control) + if flex_view: + flex_view.current_view = view_control.get_position_in_parent() + +static func _set_last_drop_target(flex_view : Control) -> void: + flex_view.get_tree().set_meta(META_LAST_DROP_TARGET, flex_view.get_path()) + +static func _get_last_drop_target() -> Control: + var tree := GDBUtility.get_scene_tree() + if tree.has_meta(META_LAST_DROP_TARGET): + var flex_view_path := tree.get_meta(META_LAST_DROP_TARGET) as NodePath + if flex_view_path: + var flex_view := tree.root.get_node_or_null(flex_view_path) as Control + return flex_view + return null + +################# +# private stuff # +################# +func _start_dropping() -> void: + __drop_position = -1 + _drop_indicator.visible = true + _tween.stop_all() + _tween.interpolate_property(_drop_indicator, "modulate", Color.transparent, Color.white, INDICATOR_ANIM_DURATION) + _tween.start() + set_process(true) + +func _stop_dropping() -> void: + _tween.stop_all() + _tween.interpolate_property(_drop_indicator, "modulate", Color.white, Color.transparent, INDICATOR_ANIM_DURATION) + _tween.start() + set_process(false) + + yield(get_tree().create_timer(INDICATOR_ANIM_DURATION), "timeout") + if !is_processing(): # mOybe we are dragging again + _drop_indicator.visible = false + +func _drop(view : Control) -> void: + _stop_dropping() + + var title := get_view_title_from_control(view) + if __drop_position != DropPosition.CENTER: + var new_container := split_view(__drop_position, title, view) + _set_last_drop_target(new_container) + else: + add_view(title, view) + _set_last_drop_target(self) + +func __make_floating_tab(tab : int) -> void: + var window : WindowDialog = load("res://addons/de.mewin.gduibasics/scenes/flexview_window.tscn").instance() + get_tree().root.add_child(window) + + # remove tab + var tab_ctrl : Control = $tabs.get_tab_control(tab) + remove_view(tab) + + # add to new window + var title = get_view_title_from_control(tab_ctrl) + window.flex_view.add_view(title, tab_ctrl) + __copy_signal_handlers(window.flex_view) + + # place window + var vp_size := get_viewport().size + var mouse_pos := get_viewport().get_mouse_position() + var win_size := vp_size * Vector2(0.5, 0.5) + var min_size := window.get_combined_minimum_size() + var title_height := window.get_constant("title_height") + + window.rect_size = Vector2(max(win_size.x, min_size.x), max(win_size.y, min_size.y)) + window.rect_position = mouse_pos - Vector2(0.5 * window.rect_size.x, -0.5 * title_height) + window.visible = true + + # emulate mouse down for dragging the window + var event := InputEventMouseButton.new() + event.button_index = BUTTON_LEFT + event.pressed = true + event.button_mask = BUTTON_MASK_LEFT + event.position = mouse_pos + event.global_position = mouse_pos + Input.parse_input_event(event) + +func __get_drop_rect(drop_position : int) -> Rect2: + var full_rect := Rect2(Vector2(), rect_size) + match drop_position: + DropPosition.TOP: + return full_rect.grow_individual(0.0, 0.0, 0.0, -0.5 * full_rect.size.y) + DropPosition.BOTTOM: + return full_rect.grow_individual(0.0, -0.5 * full_rect.size.y, 0.0, 0.0) + DropPosition.LEFT: + return full_rect.grow_individual(0.0, 0.0, -0.5 * full_rect.size.x, 0.0) + DropPosition.RIGHT: + return full_rect.grow_individual(-0.5 * full_rect.size.x, 0.0, 0.0, 0.0) + return full_rect + +func __copy_signal_handlers(other_view : Control) -> void: + GDBUtility.copy_all_signal_handlers(other_view, self, __HANDLERS_TO_COPY) + +##################### +# setters & getters # +##################### +func _set_current_view(val : int) -> void: + $tabs.current_tab = val + emit_signal("current_view_changed", self) + +func _get_current_view() -> int: + return $tabs.current_tab + +func _set_show_tabs(show : bool) -> void: + show_tabs = show + if $tabs: + $tabs.show_tabs = show + +################### +# signal handlers # +################### +func _on_tabs_tab_dragged_out(tab : int, position : Vector2) -> void: + __make_floating_tab(tab) + +func _on_tabs_tab_close(tab : int): + close_view(tab) + +func _on_flexview_title_changed(control : Control) -> void: + var view := find_view(control) + var title = control.get("flexview_title") + if view >= 0 && title is String: + set_view_title(view, title) + +func _on_flexview_close_me(control : Control) -> void: + var view := find_view(control) + if view >= 0: + remove_view(view) + +########### +# signals # +########### +signal view_added(view, flex_view) +signal view_removed(view, flex_view) +signal view_closing(view, flex_view) +signal current_view_changed(flex_view) diff --git a/scenes/scripts/flexview_splitter.gd b/scenes/scripts/flexview_splitter.gd new file mode 100644 index 0000000..0b91922 --- /dev/null +++ b/scenes/scripts/flexview_splitter.gd @@ -0,0 +1,38 @@ +extends SplitContainer + +############# +# overrides # +############# +func add_child(child : Node, legible_unique_name := false) -> void: + .add_child(child, legible_unique_name) + + child.connect("tree_exited", self, "_on_child_tree_exited") + +################# +# private stuff # +################# +func __try_collapse() -> void: + var count := get_child_count() + if count == 0: # nothing left? I can go now + self.queue_free() + return + elif count > 1: # more than one? I have to stay + return + + # exactly one? replace me with them + var child := get_child(0) + remove_child(child) + + get_parent().add_child_below_node(self, child) + GDBUIUtility.copy_size(child, self) + self.queue_free() + +################### +# signal handlers # +################### +func _on_child_tree_exited() -> void: + if !get_tree(): + return + + yield(get_tree(), "idle_frame") # wait until child is actually gone + __try_collapse() diff --git a/scenes/scripts/flexview_window.gd b/scenes/scripts/flexview_window.gd new file mode 100644 index 0000000..8e18ac1 --- /dev/null +++ b/scenes/scripts/flexview_window.gd @@ -0,0 +1,141 @@ +extends WindowDialog + +const FlexView = preload("res://addons/de.mewin.gduibasics/scenes/scripts/flexview.gd") + +var flex_view : FlexView setget _set_flex_view, _get_flex_view + +var __drop_target : FlexView = null +var __is_dragging := false + +############# +# overrides # +############# +func _ready() -> void: + set_process(false) # only used to stop dropping when mouse button is lifted + __update_title() + +func _process(delta : float) -> void: + if !Input.is_mouse_button_pressed(BUTTON_LEFT): + __drop() + +func _input(event : InputEvent) -> void: + if event is InputEventKey && event.scancode == KEY_SHIFT && __is_dragging: + __update_dragging() + +################# +# private stuff # +################# +func __update_view_count() -> void: + var child := __get_flex_child() + if !(child is FlexView): + return + + var count : int = child.get_view_count() + if count == 0: + self.queue_free() + return + else: + # child.show_tabs = (count > 1) + pass + +func __update_dragging() -> void: + var child := __get_flex_child() + if !(child is FlexView) || child.get_view_count() > 1: + set_process(false) + __is_dragging = false + return + + __is_dragging = true + set_process(true) + + # dragging with shift disables dropping + if Input.is_key_pressed(KEY_SHIFT): + __set_drop_target(null) + return + + var mouse_pos := get_viewport().get_mouse_position() + var flex_views := get_tree().get_nodes_in_group(FlexView.GROUP_NAME) + var views_under_cursor := [] + + for view in flex_views: + if !self.is_a_parent_of(view) && view.visible && view.get_global_rect().has_point(mouse_pos): + views_under_cursor.append(view) + + if views_under_cursor.empty(): + __set_drop_target(null) + return + + # find first in tree + var target : FlexView = views_under_cursor[0] + for i in range(1, views_under_cursor.size()): + var node : FlexView = views_under_cursor[i] + if GDBUtility.control_is_in_front_of(node, target): + target = node + __set_drop_target(target) + +func __set_drop_target(target : FlexView) -> void: + if target == __drop_target: + return + set_process(is_instance_valid(__drop_target)) # process to check if mouse button is lifted + + if is_instance_valid(__drop_target): + __drop_target._stop_dropping() + __drop_target = target + if is_instance_valid(__drop_target): + __drop_target._start_dropping() + +func __drop() -> void: + var child := __get_flex_child() + set_process(false) + __is_dragging = false + + if !is_instance_valid(__drop_target) || !(child is FlexView) || child.get_view_count() != 1: + return + + var control : Control = child.get_view_control(0) + child.remove_view(0) + __drop_target._drop(control) + self.queue_free() + +func __update_title() -> void: + var child := __get_flex_child() + if !(child is FlexView): + window_title = "" + elif child.get_view_count() > 0: + window_title = child.get_view_title(child.current_view) + +func __get_flex_child() -> Control: + return get_child(1) as Control # TODO: something better (and safer) than a fixed index + +func __close_requested() -> void: + for flex_view in GDBUtility.find_nodes_by_type(self, FlexView): + flex_view.close_all_views() + +##################### +# setters & getters # +##################### +func _set_flex_view(val) -> void: + assert(0, "Cannot set this.") + +func _get_flex_view() -> FlexView: + return $flex_view as FlexView + +################### +# signal handlers # +################### +func _on_flex_view_view_added(view : int, flex_view : FlexView) -> void: + __update_view_count() + +func _on_flex_view_view_removed(view : int, flex_view : FlexView) -> void: + __update_view_count() + +func _on_flexview_window_item_rect_changed() -> void: + __update_dragging() + +func _on_flex_view_current_view_changed(flex_view : FlexView): + __update_title() + +func _on_flexview_window_visibility_changed(): + if !visible: + __close_requested() + visible = true diff --git a/scenes/scripts/line_edit_suggestion_popup.gd b/scenes/scripts/line_edit_suggestion_popup.gd new file mode 100644 index 0000000..5486a4d --- /dev/null +++ b/scenes/scripts/line_edit_suggestion_popup.gd @@ -0,0 +1,100 @@ +extends Popup + +enum Position { + ABOVE, + BELOW +} + +var line_edit : LineEdit setget _set_line_edit +export(Position) var position = Position.BELOW + +############# +# overrides # +############# +func _ready(): + call_deferred("__setup") + +################# +# private stuff # +################# +func __setup(): + __connect() + +func __connect(): + if !line_edit: + return + + line_edit.connect("focus_entered", self, "_on_line_edit_focus_entered") + line_edit.connect("focus_exited", self, "_on_line_edit_focus_exited") + line_edit.connect("item_rect_changed", self, "_on_line_edit_item_rect_changed") + line_edit.connect("resized", self, "_on_line_edit_resized") + +func __disconnect(): + if !line_edit: + return + + line_edit.disconnect("focus_entered", self, "_on_line_edit_focus_entered") + line_edit.disconnect("focus_exited", self, "_on_line_edit_focus_exited") + line_edit.disconnect("item_rect_changed", self, "_on_line_edit_item_rect_changed") + line_edit.disconnect("resized", self, "_on_line_edit_resized") + +func __calc_rect() -> Rect2: + var position_ = position + var pos := line_edit.rect_global_position + var vp_pos = get_viewport_rect().position + var vp_size = get_viewport_rect().size + var size := rect_min_size + var le_size := line_edit.rect_size + var max_height : float + + if pos.y + size.y > vp_size.y: + position_ = Position.ABOVE + elif pos.y < vp_pos.y: + position_ = Position.BELOW + + if position_ == Position.ABOVE: + max_height = pos.y - vp_pos.y + else: + max_height = (vp_pos.y + vp_size.y) - (pos.y + size.y) + + var res_size = Vector2(le_size.x, 100.0) + if position_ == Position.ABOVE: + return Rect2(pos - Vector2(0.0, res_size.y), res_size) + else: + return Rect2(pos + Vector2(0.0, size.y), res_size) + +func __update(): + var should_be_visible = line_edit.has_focus() + if !should_be_visible: + hide() + else: + var rect := __calc_rect() + if visible: + rect_position = rect.position + rect_size = rect.size + else: + popup(rect) + +############ +# handlers # +############ +func _on_line_edit_focus_entered(): + __update() + +func _on_line_edit_focus_exited(): + hide() + +func _on_line_edit_item_rect_changed(): + __update() + +func _on_line_edit_resized(): + __update() + +########### +# setters # +########### +func _set_line_edit(value : LineEdit): + if value != line_edit: + __disconnect() + line_edit = value + __connect() diff --git a/scenes/scripts/line_input_dialog.gd b/scenes/scripts/line_input_dialog.gd new file mode 100644 index 0000000..8f89d07 --- /dev/null +++ b/scenes/scripts/line_input_dialog.gd @@ -0,0 +1,47 @@ +tool +extends ConfirmationDialog + +export(bool) var allow_empty := true setget _set_allow_empty + +################ +# public stuff # +################ +func get_line_edit() -> LineEdit: + return $container/line_edit as LineEdit + +################# +# private stuff # +################# +func __update(): + var valid := true + if !allow_empty && !$container/line_edit.text: + valid = false + get_ok().disabled = !valid + +########### +# setters # +########### +func _set_allow_empty(value : bool): + if value != allow_empty: + allow_empty = value + __update() + +############ +# handlers # +############ +func _on_line_input_dialog_about_to_show(): + __update() + +func _on_line_edit_text_changed(new_text : String): + __update() + +func _on_line_edit_text_entered(new_text): + if !get_ok().disabled: + visible = false + emit_signal("confirmed") + +func _on_line_input_dialog_visibility_changed(): + if visible: + yield(get_tree(), "idle_frame") + $container/line_edit.grab_focus() + $container/line_edit.caret_position = $container/line_edit.text.length() diff --git a/scenes/scripts/node_selection_dialog.gd b/scenes/scripts/node_selection_dialog.gd new file mode 100644 index 0000000..0a70083 --- /dev/null +++ b/scenes/scripts/node_selection_dialog.gd @@ -0,0 +1,5 @@ +tool +extends ConfirmationDialog + +func get_editor_node_tree() -> UIB_EditorNodeTree: + return $tree as UIB_EditorNodeTree diff --git a/scenes/scripts/selector_popup.gd b/scenes/scripts/selector_popup.gd new file mode 100644 index 0000000..2c7c9fd --- /dev/null +++ b/scenes/scripts/selector_popup.gd @@ -0,0 +1,174 @@ +extends PopupPanel + +class_name SelectorPopup + +class _Item: + var label : String + var icon : Texture + var metadata + var current_list_index := -1 + + func _init(label_ : String, icon_ : Texture, metadata_): + self.label = label_ + self.icon = icon_ + self.metadata = metadata_ + +export var match_case := false + +onready var _ledit_search : LineEdit = GDBUtility.find_node_by_name(self, "ledit_search") +onready var _ilist_content : ItemList = GDBUtility.find_node_by_name(self, "ilist_content") + +var _items := [] +var __needs_update := false +var __caret := 0 +var __submitted := false + +################ +# overridables # +################ +func _should_show(item : _Item) -> bool: + var search_text := _ledit_search.text.strip_edges() + if search_text == "": + return true + var item_text := item.label + if !match_case: + search_text = search_text.to_lower() + item_text = item_text.to_lower() + return search_text in item_text + +################ +# public stuff # +################ +func add_item(label : String, icon : Texture = null, metadata = null) -> void: + _items.append(_Item.new(label, icon, metadata)) + invalidate() + +func clear_items() -> void: + _items.clear() + invalidate() + +func get_item_label(idx : int) -> String: + if idx < 0 || idx >= _items.size(): + return "" + return _items[idx].label + +func get_item_metadata(idx : int): + if idx < 0 || idx >= _items.size(): + return null + return _items[idx].metadata + +func invalidate() -> void: + # only updates once if multiple calls are done in a single frame + __needs_update = true + yield(get_tree(), "idle_frame") + if __needs_update: + __needs_update = false + __update() + +################# +# private stuff # +################# +func __update() -> void: + var idx_increment := 0 + var insert_index := 0 + + for item in self._items: + var should_show := self._should_show(item) + var is_visible : bool = (item.current_list_index > -1) + + if is_visible: + item.current_list_index += idx_increment + + if should_show && !is_visible: + self.__add_item(item, insert_index) + item.current_list_index = insert_index + idx_increment += 1 + elif !should_show && is_visible: + _ilist_content.remove_item(item.current_list_index) + item.current_list_index = -1 + idx_increment -= 1 + + if should_show: + insert_index += 1 + + if !_ilist_content.is_anything_selected() && _ilist_content.get_item_count() > 0: + __select(0) + +func __add_item(item : _Item, insert_index : int) -> void: + var new_idx := _ilist_content.get_item_count() + _ilist_content.add_item(item.label, item.icon) + _ilist_content.set_item_metadata(new_idx, item) + if insert_index != new_idx: + _ilist_content.move_item(new_idx, insert_index) + +func __select(idx : int) -> void: + _ilist_content.select(idx) + _ilist_content.ensure_current_is_visible() + +func __select_next() -> void: + var selected := _ilist_content.get_selected_items() + var idx := 0 + if !selected.empty(): + idx = selected[0] + 1 + if idx >= _ilist_content.get_item_count(): + return + __select(idx) + +func __select_prev() -> void: + var selected := _ilist_content.get_selected_items() + var idx := 0 + if !selected.empty(): + idx = selected[0] - 1 + if idx < 0: + return + __select(idx) + +func __item_from_list_index(idx : int) -> _Item: + return _ilist_content.get_item_metadata(idx) as _Item + +func __item_index(itm : _Item) -> int: + return _items.find(itm) + +################### +# signal handlers # +################### +func _on_selector_popup_about_to_show() -> void: + __submitted = false + _ledit_search.clear() + _ledit_search.grab_focus() + _ilist_content.unselect_all() + __update() + +func _on_ledit_search_text_changed(new_text : String) -> void: + __update() + +func _on_ledit_search_gui_input(event : InputEvent) -> void: + if !event.is_pressed(): + return + + if event.is_action("ui_down"): + __select_next() + elif event.is_action("ui_up"): + __select_prev() + else: + __caret = _ledit_search.caret_position + return + _ledit_search.caret_position = __caret + +func _on_ledit_search_text_entered(new_text : String) -> void: + var selected := _ilist_content.get_selected_items() + if selected.empty(): + return + var idx := __item_index(__item_from_list_index(selected[0])) + __submitted = true + emit_signal("selected", idx) + hide() + +func _on_selector_popup_popup_hide() -> void: + if !__submitted: + emit_signal("selected", -1) + +########### +# signals # +########### +signal selected(index) diff --git a/scenes/scripts/variant_editor.gd b/scenes/scripts/variant_editor.gd new file mode 100644 index 0000000..c9e9d31 --- /dev/null +++ b/scenes/scripts/variant_editor.gd @@ -0,0 +1,77 @@ +tool +extends Control + +enum Type { + STRING, + INT, + REAL +} + +onready var edt_text := $edt_text +onready var spin_number := $spin_number + +export var value = "" setget _set_value +export(Type) var type = Type.STRING setget _set_type + +func _ready(): + __setup_type() + +func __setup_type(): + match type: + Type.STRING: + edt_text.visible = true + Type.INT, Type.REAL: + spin_number.visible = true + _set_value(convert(value, __type_id())) + __fill_value() + emit_signal("value_changed") + +func __fill_value(): + match type: + Type.STRING: + edt_text.text = str(value) + Type.INT, Type.REAL: + spin_number.value = float(value) + +func __type_id() -> int: + match type: + Type.STRING: + return TYPE_STRING + Type.INT: + return TYPE_INT + Type.REAL: + return TYPE_REAL + _: + assert(false) + return TYPE_STRING + +func _set_value(val): + val = convert(val, __type_id()) + if typeof(value) != __type_id() || value != val: + value = val + __fill_value() + +func _set_type(value : int): + if value != type: + type = value + for child in get_children(): + child.visible = false + __setup_type() + +func _on_edt_text_text_changed(new_text : String): + assert(type == Type.STRING) + if !value is String || value != new_text: + value = new_text + emit_signal("value_changed") + +func _on_spin_number_value_changed(value_ : float): + assert(type == Type.INT || type == Type.REAL) + if type == Type.INT: + value_ = int(value_) + if typeof(value) != __type_id() || value != value_: + value = value_ + emit_signal("value_changed") + +signal value_changed() + + diff --git a/scenes/selector_popup.tscn b/scenes/selector_popup.tscn new file mode 100644 index 0000000..bdff1b4 --- /dev/null +++ b/scenes/selector_popup.tscn @@ -0,0 +1,40 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://addons/de.mewin.gduibasics/scenes/scripts/selector_popup.gd" type="Script" id=1] + +[node name="selector_popup" type="PopupPanel"] +margin_right = 139.0 +margin_bottom = 198.0 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="vbox_main" type="VBoxContainer" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_left = 5.0 +margin_top = 5.0 +margin_right = -5.0 +margin_bottom = -5.0 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="ledit_search" type="LineEdit" parent="vbox_main"] +margin_right = 129.0 +margin_bottom = 22.0 +focus_neighbour_top = NodePath(".") +focus_neighbour_bottom = NodePath(".") + +[node name="ilist_content" type="ItemList" parent="vbox_main"] +margin_top = 26.0 +margin_right = 129.0 +margin_bottom = 188.0 +size_flags_vertical = 3 + +[connection signal="about_to_show" from="." to="." method="_on_selector_popup_about_to_show"] +[connection signal="popup_hide" from="." to="." method="_on_selector_popup_popup_hide"] +[connection signal="gui_input" from="vbox_main/ledit_search" to="." method="_on_ledit_search_gui_input"] +[connection signal="text_changed" from="vbox_main/ledit_search" to="." method="_on_ledit_search_text_changed"] +[connection signal="text_entered" from="vbox_main/ledit_search" to="." method="_on_ledit_search_text_entered"] diff --git a/scenes/variant_editor.tscn b/scenes/variant_editor.tscn new file mode 100644 index 0000000..17ef7e3 --- /dev/null +++ b/scenes/variant_editor.tscn @@ -0,0 +1,34 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://addons/de.mewin.gduibasics/scenes/scripts/variant_editor.gd" type="Script" id=1] + +[node name="variant_editor" type="Control"] +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_right = -617.0 +margin_bottom = -576.0 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="edt_text" type="LineEdit" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_bottom = -24.0 +size_flags_horizontal = 3 +caret_blink = true +caret_blink_speed = 0.5 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="spin_number" type="SpinBox" parent="."] +visible = false +anchor_right = 1.0 +margin_bottom = 24.0 +__meta__ = { +"_edit_use_anchors_": false +} +[connection signal="text_changed" from="edt_text" to="." method="_on_edt_text_text_changed"] +[connection signal="value_changed" from="spin_number" to="." method="_on_spin_number_value_changed"] diff --git a/scripts/libs/ui_util.gd b/scripts/libs/ui_util.gd new file mode 100644 index 0000000..ce2a053 --- /dev/null +++ b/scripts/libs/ui_util.gd @@ -0,0 +1,55 @@ +extends Node + +class_name GDBUIUtility + +static func uncollapse_tree_item(tree_item : TreeItem): + while tree_item: + tree_item.collapsed = false + tree_item = tree_item.get_parent() + +static func find_scene_tree_editor(root : Node = null) -> Node: + if not root: + root = (Engine.get_main_loop() as SceneTree).root + if root.get_class() == "SceneTreeEditor": + return root + for child in root.get_children(): + var scene_tree_editor := find_scene_tree_editor(child) + if scene_tree_editor: + return scene_tree_editor + return null + +static func new_item_sorted(tree : Tree, parent : TreeItem, key : String, meta_column := 0) -> TreeItem: + var item := parent.get_children() + var found_itm : TreeItem + var idx := 0 + while true: + if item == null: + break + + var itm_key := item.get_metadata(meta_column) as String + if itm_key == key: + return item + elif itm_key > key: + break + item = item.get_next() + idx += 1 + + # not found + var new_itm : TreeItem = tree.create_item(parent, idx) + new_itm.set_metadata(meta_column, key) + return new_itm + +static func copy_size(target : Control, source : Control) -> void: + target.rect_min_size = source.rect_min_size + target.rect_size = source.rect_size + target.size_flags_horizontal = source.size_flags_horizontal + target.size_flags_vertical = source.size_flags_vertical + target.size_flags_stretch_ratio = source.size_flags_stretch_ratio + target.anchor_top = source.anchor_top + target.anchor_bottom = source.anchor_bottom + target.anchor_left = source.anchor_left + target.anchor_right = source.anchor_right + target.margin_top = source.margin_top + target.margin_bottom = source.margin_bottom + target.margin_left = source.margin_left + target.margin_right = source.margin_right diff --git a/scripts/libs/ux.gd b/scripts/libs/ux.gd new file mode 100644 index 0000000..d582777 --- /dev/null +++ b/scripts/libs/ux.gd @@ -0,0 +1,77 @@ +# various UX stuff +extends Object + +class_name GDBUX + +const __META_STATE = "__gdbui_ux_state__" + +class __State: + signal recent_places_changed() + signal favourite_places_changed() + +################ +# public stuff # +################ +static func get_recent_places() -> Array: + return GDBSettings.get_value(GDBConstants.SETTING_RECENT_PLACES, []) + +static func add_recent_place(place) -> void: + if place is UIB_FileAdapter.FileEntry: + add_recent_place(place._get_path()) + return + + assert(place is String) + var recent_places : Array = GDBSettings.get_value(GDBConstants.SETTING_RECENT_PLACES, []) + recent_places.erase(place) + recent_places.push_front(place) + GDBSettings.set_value(GDBConstants.SETTING_RECENT_PLACES, recent_places) + + __get_state().emit_signal("recent_places_changed") + +static func get_favourite_places() -> Array: + return GDBSettings.get_value(GDBConstants.SETTING_FAVOURITE_PLACES, []) + +static func is_favourite_place(place) -> bool: + if place is UIB_FileAdapter.FileEntry: + return is_favourite_place(place._get_path()) + + assert(place is String) + return GDBSettings.array_has_value(GDBConstants.SETTING_FAVOURITE_PLACES, place) + +static func add_favourite_place(place) -> void: + if place is UIB_FileAdapter.FileEntry: + add_favourite_place(place._get_path()) + return + + assert(place is String) + var places = GDBSettings.get_value(GDBConstants.SETTING_FAVOURITE_PLACES, []) + if !places.has(place): + places.append(place) + GDBSettings.set_value(GDBConstants.SETTING_FAVOURITE_PLACES, places) + + __get_state().emit_signal("favourite_places_changed") + +static func remove_favourite_place(place) -> void: + if place is UIB_FileAdapter.FileEntry: + remove_favourite_place(place._get_path()) + return + + assert(place is String) + var places = GDBSettings.get_value(GDBConstants.SETTING_FAVOURITE_PLACES, []) + if places.has(place): + places.erase(place) + GDBSettings.set_value(GDBConstants.SETTING_FAVOURITE_PLACES, places) + + __get_state().emit_signal("favourite_places_changed") + +static func connect_static(sig_name : String, receiver : Object, method : String, binds := []) -> int: + return __get_state().connect(sig_name, receiver, method, binds) + +static func disconnect_static(sig_name : String, receiver : Object, method : String) -> void: + __get_state().disconnect(sig_name, receiver, method) + +################# +# private stuff # +################# +static func __get_state() -> __State: + return GDBUtility.get_state_object(__META_STATE, __State) diff --git a/scripts/types/controls/action_button.gd b/scripts/types/controls/action_button.gd new file mode 100644 index 0000000..3c02001 --- /dev/null +++ b/scripts/types/controls/action_button.gd @@ -0,0 +1,82 @@ +extends Button + +class_name UIB_ActionButton + +var action : UIB_Action setget _set_action + +############# +# overrides # +############# +func _ready(): + if !action: + _set_action(GDBUtility.find_node_by_type(self, UIB_Action)) + if !action: + var action_reference : UIB_ActionReference = GDBUtility.find_node_by_type(self, UIB_ActionReference) + if action_reference: + _set_action(action_reference.get_action()) + +func _pressed(): + if action: + action._apply() + +################# +# private stuff # +################# +func __connect(): + if !action: + return + action.connect("disabled_changed", self, "_on_action_disabled_changed") + action.connect("toggled_changed", self, "_on_action_toggled_changed") + action.connect("text_changed", self, "_on_action_text_changed") + action.connect("icon_changed", self, "_on_action_icon_changed") + +func __disconnect(): + if !action: + return + action.disconnect("disabled_changed", self, "_on_action_disabled_changed") + action.disconnect("toggled_changed", self, "_on_action_toggled_changed") + action.disconnect("text_changed", self, "_on_action_text_changed") + action.disconnect("icon_changed", self, "_on_action_icon_changed") + +############ +# handlers # +############ +func _on_action_disabled_changed(): + disabled = action._is_disabled() + +func _on_action_toggled_changed(): + toggle_mode = action._is_toggled() + pressed = action._is_toggled() + +func _on_action_text_changed(): + if !icon: + text = action._get_text() + hint_tooltip = action._get_text() + +func _on_action_icon_changed(): + icon = action._get_icon() + _on_action_text_changed() # update hint/text + +########### +# setters # +########### +func _set_action(value : UIB_Action): + if value != action: + __disconnect() + action = value + if action: + icon = action._get_icon() + if !icon: + text = action._get_text() + hint_tooltip = action._get_text() + disabled = action._is_disabled() + toggle_mode = action._is_toggled() + pressed = action._is_toggled() + __connect() + else: + icon = null + text = "" + hint_tooltip = "" + disabled = true + toggle_mode = false + pressed = false diff --git a/scripts/types/controls/action_menu_button.gd b/scripts/types/controls/action_menu_button.gd new file mode 100644 index 0000000..c3c6bba --- /dev/null +++ b/scripts/types/controls/action_menu_button.gd @@ -0,0 +1,57 @@ +extends MenuButton + +class_name UIB_ActionMenuButton + +onready var __popup := get_popup() + +############# +# overrides # +############# +func _ready(): + for child in get_children(): + if child is UIB_Action: + add_action(child) + elif child is UIB_Seperator: + add_seperator(child) + elif child.has_method("get_action"): + add_action(child.get_action()) + + connect("about_to_show", self, "_on_about_to_show") + __popup.connect("index_pressed", self, "_on_popup_index_pressed") + +################ +# public stuff # +################ +func add_action(action : UIB_Action): + if !action: + return + + var idx = __popup.get_item_count() + __popup.add_item(action._get_text()) + __popup.set_item_metadata(idx, action) + +func add_seperator(seperator : UIB_Seperator): + __popup.add_separator(seperator.text) + +############ +# handlers # +############ +func _on_about_to_show(): + for i in range(__popup.get_item_count()): + var action := __popup.get_item_metadata(i) as UIB_Action + if !action: + continue + + action._update() + __popup.set_item_text(i, action._get_text()) + __popup.set_item_icon(i, action._get_icon()) + __popup.set_item_shortcut(i, action._get_shortcut()) + __popup.set_item_disabled(i, action._is_disabled()) + __popup.set_item_as_checkable(i, action._is_toggleable()) + __popup.set_item_checked(i, action._is_toggled()) + +func _on_popup_index_pressed(index : int): + var action := __popup.get_item_metadata(index) as UIB_Action + if !action: + return + action._apply() diff --git a/scripts/types/controls/action_popup_menu.gd b/scripts/types/controls/action_popup_menu.gd new file mode 100644 index 0000000..b7fc526 --- /dev/null +++ b/scripts/types/controls/action_popup_menu.gd @@ -0,0 +1,50 @@ +extends PopupMenu + +class_name UIB_ActionPopupMenu + +############# +# overrides # +############# +func _ready(): + for child in get_children(): + if child is UIB_Action: + add_action(child) + elif child.has_method("get_action"): + add_action(child.get_action()) + + connect("about_to_show", self, "_on_about_to_show") + connect("index_pressed", self, "_on_popup_index_pressed") + +################ +# public stuff # +################ +func add_action(action : UIB_Action): + if !action: + return + + var idx = get_item_count() + add_item(action._get_text()) + set_item_metadata(idx, action) + +############ +# handlers # +############ +func _on_about_to_show(): + for i in range(get_item_count()): + var action := get_item_metadata(i) as UIB_Action + if !action: + continue + + action._update() + set_item_text(i, action._get_text()) + set_item_icon(i, action._get_icon()) + set_item_shortcut(i, action._get_shortcut()) + set_item_disabled(i, action._is_disabled()) + set_item_as_checkable(i, action._is_toggleable()) + set_item_checked(i, action._is_toggled()) + +func _on_popup_index_pressed(index : int): + var action := get_item_metadata(index) as UIB_Action + if !action: + return + action._apply() diff --git a/scripts/types/controls/action_toolbar.gd b/scripts/types/controls/action_toolbar.gd new file mode 100644 index 0000000..5134fbe --- /dev/null +++ b/scripts/types/controls/action_toolbar.gd @@ -0,0 +1,29 @@ +extends BoxContainer + +class_name UIB_ActionToolbar + +############# +# overrides # +############# +func _ready(): + for child in get_children(): + if child is UIB_Action: + add_action(child) + elif child.has_method("get_action"): + add_action(child.get_action()) + +################ +# public stuff # +################ +func add_action(action : UIB_Action): + var btn = ToolButton.new() + btn.set_script(preload("res://addons/de.mewin.gduibasics/scripts/types/controls/action_button.gd")) + btn.action = action + call_deferred("add_child", btn) + +func remove_action(action : UIB_Action): + for child in get_children(): + if child == action || child.get("action") == action \ + || (child is UIB_ActionReference && child.get_action() == action): + remove_child(child) + child.queue_free() diff --git a/scripts/types/controls/border_container.gd b/scripts/types/controls/border_container.gd new file mode 100644 index 0000000..7f79d6f --- /dev/null +++ b/scripts/types/controls/border_container.gd @@ -0,0 +1,250 @@ +tool +extends MarginContainer + +class_name UIB_BorderContainer + +enum __MouseMode { + NONE, + MOVING, + RESIZE_L, + RESIZE_R, + RESIZE_T, + RESIZE_B, + RESIZE_TL, + RESIZE_TR, + RESIZE_BL, + RESIZE_BR +} + +export var resizable := true +export var show_title := false setget _set_show_title +export var title := "" setget _set_title +export var grab_everywhere := false +export(Texture) var title_image = null +export(StyleBox) var style_left setget _set_style_left +export(StyleBox) var style_right setget _set_style_right +export(StyleBox) var style_top setget _set_style_top +export(StyleBox) var style_bottom setget _set_style_bottom + +var __mouse_mode = __MouseMode.NONE +var __mouse_down_pos : Vector2 + +############# +# overrides # +############# +func _gui_input(event): + if event is InputEventMouseButton && event.pressed && event.button_index == BUTTON_LEFT: + if resizable: + __mouse_mode = __resize_mode(event.position) + if __mouse_mode != __MouseMode.NONE: + __mouse_down_pos = _win_get_mouse_position() - _win_get_position() + if __mouse_mode == __MouseMode.RESIZE_B || __mouse_mode == __MouseMode.RESIZE_BL || __mouse_mode == __MouseMode.RESIZE_BR: + __mouse_down_pos.y -= _win_get_size().y + if __mouse_mode == __MouseMode.RESIZE_R || __mouse_mode == __MouseMode.RESIZE_TR || __mouse_mode == __MouseMode.RESIZE_BR: + __mouse_down_pos.x -= _win_get_size().x + set_process(true) + return + if grab_everywhere || event.position.y < __get_margin("top"): + __mouse_mode = __MouseMode.MOVING + __mouse_down_pos = _win_get_mouse_position() - _win_get_position() + set_process(true) + accept_event() + elif event is InputEventMouseMotion && resizable: + match __resize_mode(event.position): + __MouseMode.RESIZE_L, __MouseMode.RESIZE_R: + mouse_default_cursor_shape = CURSOR_HSIZE + __MouseMode.RESIZE_T, __MouseMode.RESIZE_B: + mouse_default_cursor_shape = CURSOR_VSIZE + __MouseMode.RESIZE_TL, __MouseMode.RESIZE_BR: + mouse_default_cursor_shape = CURSOR_FDIAGSIZE + __MouseMode.RESIZE_TR, __MouseMode.RESIZE_BL: + mouse_default_cursor_shape = CURSOR_BDIAGSIZE + _: + mouse_default_cursor_shape = CURSOR_ARROW + +func _process(delta): + if !Input.is_mouse_button_pressed(BUTTON_LEFT): + __mouse_mode = __MouseMode.NONE + + if __mouse_mode == __MouseMode.NONE: + set_process(false) + return + + if __mouse_mode == __MouseMode.MOVING: + _win_set_position(_win_get_mouse_position() - __mouse_down_pos) + return + + var win_pos := _win_get_position() + var win_size := _win_get_size() + var win_min_size := _win_get_minimum_size() + var mouse_pos := _win_get_mouse_position() + + if __mouse_mode == __MouseMode.RESIZE_L || __mouse_mode == __MouseMode.RESIZE_TL || __mouse_mode == __MouseMode.RESIZE_BL: + var global_right := win_pos.x + win_size.x + var x_pos := mouse_pos.x - __mouse_down_pos.x + x_pos = min(x_pos, win_pos.x + win_size.x - win_min_size.x) + win_pos.x = x_pos + win_size.x = global_right - win_pos.x + elif __mouse_mode == __MouseMode.RESIZE_R || __mouse_mode == __MouseMode.RESIZE_TR || __mouse_mode == __MouseMode.RESIZE_BR: + var x_pos := mouse_pos.x - __mouse_down_pos.x + win_size.x = x_pos - win_pos.x + win_size.x = max(win_size.x, win_min_size.x) + + if __mouse_mode == __MouseMode.RESIZE_T || __mouse_mode == __MouseMode.RESIZE_TL || __mouse_mode == __MouseMode.RESIZE_TR: + var global_bottom := win_pos.y + win_size.y + var y_pos := mouse_pos.y - __mouse_down_pos.y + y_pos = min(y_pos, win_pos.y + win_size.y - win_min_size.y) + win_pos.y = y_pos + win_size.y = global_bottom - win_pos.y + elif __mouse_mode == __MouseMode.RESIZE_B || __mouse_mode == __MouseMode.RESIZE_BL || __mouse_mode == __MouseMode.RESIZE_BR: + var y_pos := mouse_pos.y - __mouse_down_pos.y + win_size.y = y_pos - win_pos.y + win_size.y = max(win_size.y, win_min_size.y) + + _win_set_position(win_pos) + _win_set_size(win_size) + +func _draw(): + var marg_left = __get_margin("left") + var marg_right = __get_margin("right") + var marg_top = __get_margin("top") + var marg_bottom = __get_margin("bottom") + + if show_title: + var title_height := 0 + if title_image: + # title_height = title_image.get_height() + title_height = 0.0 + else: + var font := get_font("font", "Label") + title_height = font.get_height() + font.get_descent() + if marg_top < title_height: + marg_top = title_height + set("custom_constants/margin_top", marg_top) + + if style_left: + var min_size = style_left.get_minimum_size() + min_size.x = max(min_size.x, marg_left) + draw_style_box(style_left, Rect2(Vector2(), Vector2(min_size.x, rect_size.y))) + + if style_right: + var min_size = style_right.get_minimum_size() + min_size.x = max(min_size.x, marg_right) + draw_style_box(style_right, Rect2(Vector2(rect_size.x - min_size.x, 0.0), Vector2(min_size.x, rect_size.y))) + + if style_top: + var min_size = style_top.get_minimum_size() + min_size.y = max(min_size.y, marg_top) + draw_style_box(style_top, Rect2(Vector2(), Vector2(rect_size.x, min_size.y))) + + if style_bottom: + var min_size = style_bottom.get_minimum_size() + min_size.y = max(min_size.y, marg_bottom) + draw_style_box(style_bottom, Rect2(Vector2(0.0, rect_size.y - min_size.y), Vector2(rect_size.x, min_size.y))) + + if show_title: + if title_image: + draw_texture(title_image, Vector2(0.5 * (rect_size.x - title_image.get_width()), 0.0)) + else: + var font := get_font("font", "Label") + var string_size := font.get_string_size(title) + draw_string(font, Vector2(0.5 * (rect_size.x - string_size.x), string_size.y), title) + +################ +# overridables # +################ +func _win_get_position() -> Vector2: + return get_parent().rect_global_position + +func _win_get_size() -> Vector2: + return get_parent().rect_size + +func _win_get_mouse_position() -> Vector2: + return get_viewport().get_mouse_position() + +func _win_get_minimum_size() -> Vector2: + return get_parent().rect_min_size + +func _win_set_position(pos : Vector2): + get_parent().rect_global_position = pos + +func _win_set_size(size : Vector2): + get_parent().rect_size = size + +################# +# private stuff # +################# +func __get_margin(what : String) -> int: + var marg = get("custom_constants/margin_%s" % what) + if !marg: + return 0 + return marg + +func __resize_mode(cursor_pos : Vector2): + var resize_left := false + var resize_right := false + var resize_top := false + var resize_bottom := false + + if cursor_pos.y < __get_margin("bottom"): + resize_top = true + elif cursor_pos.y > rect_size.y - __get_margin("bottom"): + resize_bottom = true + if cursor_pos.x < __get_margin("left"): + resize_left = true + elif cursor_pos.x > rect_size.x - __get_margin("right"): + resize_right = true + + if resize_top: + if resize_left: + return __MouseMode.RESIZE_TL + elif resize_right: + return __MouseMode.RESIZE_TR + else: + return __MouseMode.RESIZE_T + elif resize_bottom: + if resize_left: + return __MouseMode.RESIZE_BL + elif resize_right: + return __MouseMode.RESIZE_BR + else: + return __MouseMode.RESIZE_B + elif resize_left: + return __MouseMode.RESIZE_L + elif resize_right: + return __MouseMode.RESIZE_R + else: + return __MouseMode.NONE + +########### +# setters # +########### +func _set_show_title(val : bool): + if val != show_title: + show_title = val + update() + +func _set_title(val : String): + if val != title: + title = val + update() + +func _set_style_left(val : StyleBox): + if val != style_left: + style_left = val + update() + +func _set_style_right(val : StyleBox): + if val != style_right: + style_right = val + update() + +func _set_style_top(val : StyleBox): + if val != style_top: + style_top = val + update() + +func _set_style_bottom(val : StyleBox): + if val != style_bottom: + style_bottom = val + update() diff --git a/scripts/types/controls/dynamic_grid_container.gd b/scripts/types/controls/dynamic_grid_container.gd new file mode 100644 index 0000000..27e8bea --- /dev/null +++ b/scripts/types/controls/dynamic_grid_container.gd @@ -0,0 +1,95 @@ +tool +extends Container + +class_name UIB_DynamicGridContainer + +var __DEFAULT_CONSTRAINTS = UIB_DynamicGridConstraints.new() + +export(int, 1, 100) var columns := 1 setget _set_columns + +func _notification(what): + if what == NOTIFICATION_SORT_CHILDREN: + __sort_children() + +func __get_constraints(node : Node) -> UIB_DynamicGridConstraints: + for child in node.get_children(): + if child is UIB_DynamicGridConstraints: + return child + return __DEFAULT_CONSTRAINTS + +func __sort_children(): + var column_widths := [] + var column_expand := [] + + for i in range(columns): + column_widths.append(0) + column_expand.append(0.0) + + var rows := [[]] + var col := 0 + var row_width := 0.0 + + for child in get_children(): + if !child is Control || !child.visible: + continue + var constraints := __get_constraints(child) + + if col + constraints.colspan > columns: + col = 0 + row_width = 0.0 + rows.append([]) + + rows[rows.size() - 1].append(child) + + child.rect_size = child.get_combined_minimum_size() + column_widths[col] = max(column_widths[col], child.rect_size.x) + + if child.size_flags_horizontal & Control.SIZE_EXPAND: + column_expand[col] = max(column_expand[col], child.size_flags_stretch_ratio) + row_width += child.rect_size.x + + col = col + constraints.colspan + + var full_width := 0.0 + for col_width in column_widths: + full_width += col_width + var remaining_width := rect_size.x - full_width + if remaining_width > 0.0: + var combined_expand_ratio := 0.0 + for expand in column_expand: + combined_expand_ratio += expand + if combined_expand_ratio > 0.0: + for c in range(columns): + column_widths[c] += remaining_width * (column_expand[c] / combined_expand_ratio) + + var pos := Vector2() + for row in rows: + var row_height := 0.0 + col = 0 + for child in row: + var constraints := __get_constraints(child) + child.rect_position = pos + + var width := 0.0 + for i in range(min(constraints.colspan, columns - col)): + width += column_widths[col + i] + + if child.size_flags_horizontal & Control.SIZE_FILL: + child.rect_size.x = width + pos.x += width + + row_height = max(row_height, child.rect_size.y) + col = col + constraints.colspan + + for child in row: + if child.size_flags_vertical & Control.SIZE_FILL: + child.rect_size.y = row_height + + pos.y += row_height + pos.x = 0.0 + rect_min_size.y = pos.y + +func _set_columns(value : int): + if value != columns: + columns = value + __sort_children() diff --git a/scripts/types/controls/editor_inspector_category.gd b/scripts/types/controls/editor_inspector_category.gd new file mode 100644 index 0000000..1c89dae --- /dev/null +++ b/scripts/types/controls/editor_inspector_category.gd @@ -0,0 +1,57 @@ +extends Control + +class_name UIB_EditorInspectorCategory + +var label := "" setget _set_label +var icon : Texture setget _set_icon +onready var bg_color := get_color("prop_category", "Editor") setget _set_bg_color + +func _ready(): + update() + +func _draw(): + draw_rect(Rect2(Vector2(), get_size()), bg_color) + var font := get_font("font", "Tree") + + var hs := get_constant("hseparation", "Tree") + + var w := font.get_string_size(label).x + if icon: + w += hs + icon.get_width() + + var ofs := (get_size().x - w) / 2 + + if icon: + draw_texture(icon, Vector2(ofs, (get_size().y - icon.get_height()) / 2).floor()) + ofs += hs + icon.get_width() + + var color := get_color("font_color", "Tree"); + draw_string(font, Vector2(ofs, font.get_ascent() + (get_size().y - font.get_height()) / 2).floor(), label, color, get_size().x) + +func _get_minimum_size() -> Vector2: + var ms : Vector2 + var font := get_font("font", "Tree"); + + ms.x = 1 + ms.y = font.get_height() +# if (icon.is_valid()) { +# ms.height = MAX(icon->get_height(), ms.height); +# } + ms.y += get_constant("vseparation", "Tree") + + return ms + +func _set_label(value : String): + if value != label: + label = value + update() + +func _set_icon(value : Texture): + if value != icon: + icon = value + update() + +func _set_bg_color(value : Color): + if value != bg_color: + bg_color = value + update() diff --git a/scripts/types/controls/editor_node_tree.gd b/scripts/types/controls/editor_node_tree.gd new file mode 100644 index 0000000..7f100a8 --- /dev/null +++ b/scripts/types/controls/editor_node_tree.gd @@ -0,0 +1,203 @@ +tool +extends Tree + +class_name UIB_EditorNodeTree + +var __tree : Tree +var selected_node : NodePath setget _set_selected_node +var node_type_filter = Node + +export var show_properties := false +export var show_methods := false + +############# +# overrides # +############# +func _ready(): + assert(Engine.editor_hint) + update() + connect("visibility_changed", self, "_on_visiblity_changed") + connect("item_selected", self, "_on_item_selected") + +################ +# public stuff # +################ +func update(): + if !__tree: + var scene_tree_editor := GDBUIUtility.find_scene_tree_editor() + if !scene_tree_editor: + return + __tree = __find_tree(scene_tree_editor) + if !__tree: + return + __copy_from(__tree) + if !selected_node.is_empty(): + var full_path := __full_path(selected_node) + if !full_path.is_empty(): + __set_selected_node(full_path, get_root()) + +################# +# private stuff # +################# +func __copy_from(tree : Tree): + clear() + columns = tree.columns + __copy_items(tree.get_root()) + +func __copy_items(item : TreeItem, parent : TreeItem = null): + if !item: + return + + var new_item := create_item(parent) + for i in range(columns): + new_item.set_text(i, item.get_text(i)) + new_item.set_icon(i, item.get_icon(i)) + new_item.set_metadata(i, item.get_metadata(i)) + var meta0 = item.get_metadata(0) + if meta0 is NodePath: + if show_properties: + var prop_item := create_item(new_item) + prop_item.set_text(0, tr("Properties")) + __fill_node_path_properties(prop_item, meta0) + prop_item.collapsed = true + prop_item.set_selectable(0, false) + if show_methods: + var method_item := create_item(new_item) + method_item.set_text(0, tr("Methods")) + __fill_methods(method_item, meta0) + method_item.collapsed = true + method_item.set_selectable(0, false) + var node : Node = get_node(meta0) + if !node is node_type_filter: + new_item.set_selectable(0, false) + else: + new_item.set_selectable(0, false) + if !new_item.is_selectable(0): + new_item.set_custom_color(0, Color.darkgray) + __copy_items(item.get_children(), new_item) + __copy_items(item.get_next(), parent) + +func __fill_node_path_properties(parent : TreeItem, node_path : NodePath): + assert(node_path.is_absolute()) + var node := get_node_or_null(node_path) + if !node: + return + __fill_properties(parent, node.get_property_list(), node_path) + +func __fill_properties(parent : TreeItem, properties : Array, parent_path : NodePath): + GDBAlgorithm.remove_if(properties, 'return prop.get("usage", PROPERTY_USAGE_EDITOR) & PROPERTY_USAGE_EDITOR == 0', ["prop"]) + GDBAlgorithm.sort_by(properties, "name") + for prop in properties: + var prop_item := create_item(parent) + var node_path := NodePath(str(parent_path) + ":" + prop["name"]) + prop_item.set_text(0, "%s : %s" % [prop["name"], GDBUtility.format_type(prop, "any")]) + prop_item.set_metadata(0, node_path) + __fill_properties(prop_item, GDBUtility.get_type_property_list(prop), node_path) + prop_item.collapsed = true + +func __fill_methods(parent : TreeItem, node_path : NodePath): + assert(node_path.is_absolute()) + var node := get_node_or_null(node_path) + if !node: + return + var methods := node.get_method_list() + GDBAlgorithm.sort_by(methods, "name") + for method in methods: + var method_item := create_item(parent) + method_item.set_text(0, GDBUtility.format_method_signature(method)) + +func __find_tree(root : Node) -> Tree: + if root is Tree: + return root as Tree + for child in root.get_children(): + var tree := __find_tree(child) + if tree: + return tree + return null + +func __set_selected_node(node : NodePath, root : TreeItem): + if !root: + return false + for i in range(columns): + if root.get_metadata(i) == node: + GDBUIUtility.uncollapse_tree_item(root) + root.select(i) + return true + if __set_selected_node(node, root.get_children()): + return true + else: + return __set_selected_node(node, root.get_next()) + +func __full_path(rel_path : NodePath) -> NodePath: + assert(!rel_path.is_absolute()) + var node := get_tree().edited_scene_root.get_node_or_null(rel_path) + if !node: + # printerr("EditorNodeTree: could not find node: %s" % rel_path) + return NodePath() + var res_path := node.get_path() + if rel_path.get_subname_count() > 0: + res_path = NodePath(str(res_path) + ":" + rel_path.get_concatenated_subnames()) + return res_path + +func __format_tree_item(tree : Tree, item : TreeItem) -> String: + var parts := PoolStringArray() + for i in range(tree.columns): + parts.append("%s[%s]" % [item.get_text(i), item.get_metadata(i)]) + return parts.join("|") + +func __dump_tree_item(tree : Tree, item : TreeItem, indent := 0): + if !item: + return + print(" ".repeat(indent), __format_tree_item(tree, item)) + __dump_tree_item(tree, item.get_children(), indent + 2) + __dump_tree_item(tree, item.get_next(), indent) + +func _set_selected_node(value : NodePath): + if value == selected_node: + return + if value.is_empty(): + var selected_item := get_selected() + if selected_item: + selected_item.deselect(0) + return + + if value.is_absolute(): + printerr("EditorNodeTree: trying to set absolute NodePath, aborting.") + return + + var full_path := __full_path(value) + if full_path.is_empty(): + return + __set_selected_node(full_path, get_root()) + selected_node = value + +############ +# handlers # +############ +func _on_visiblity_changed(): + if visible: + update() + +func _on_item_selected(): + var item := get_selected() + if !item: + selected_node = NodePath() + emit_signal("selected_node_changed") + return + for i in range(columns): + var meta = item.get_metadata(i) + if meta is NodePath: + assert(meta.is_absolute()) + var node : Node = get_node(meta) + if !node: + printerr("EditorNodeTree contains invalid node path: %s" % meta) + return + selected_node = NodePath(str(get_tree().edited_scene_root.get_path_to(node)) + ":" + meta.get_concatenated_subnames()) + emit_signal("selected_node_changed") + return + printerr("EditorNodeTree: selected node without NodePath?!") + +########### +# signals # +########### +signal selected_node_changed() diff --git a/scripts/types/controls/file_list.gd b/scripts/types/controls/file_list.gd new file mode 100644 index 0000000..2247d1a --- /dev/null +++ b/scripts/types/controls/file_list.gd @@ -0,0 +1,280 @@ +extends Tree + +class_name UIB_FileList + +enum ViewMode { + LIST, + DETAILS +} +enum SpecialColumn { + CHECKBOX +} +enum Checkable { + NONE, + FILES, + FOLDERS, + BOTH +} + +class DetailsColumn: + func _get_name() -> String: + return "" + + func _get_value(entry) -> String: + return "" + + func _get_expand() -> bool: + return false + + func _get_min_width() -> float: + return 10.0 + + func _compare(entry0, entry1) -> bool: + return _get_value(entry0) < _get_value(entry1) + +class DetailsColumnName: + extends DetailsColumn + func _get_name() -> String: + return tr("Name") + func _get_value(entry) -> String: + return entry._get_name() + func _get_expand() -> bool: + return true + +class DetailsColumnType: + extends DetailsColumn + func _get_name() -> String: + return tr("Type") + func _get_value(entry) -> String: + if entry._is_folder(): + return tr("Folder") + else: + return tr("File") + func _get_min_width() -> float: + return 100.0 + +class DetailsColumnSize: + extends DetailsColumn + func _get_name() -> String: + return tr("Size") + func _get_value(entry) -> String: + if entry._is_folder(): + return "" + else: + return String.humanize_size(entry._get_size()) + func _get_min_width() -> float: + return 100.0 + func _compare(entry0, entry1) -> bool: + return entry0._get_size() < entry1._get_size() + +class DetailsColumnModifiedTime: + extends DetailsColumn + func _get_name() -> String: + return tr("Modified") + func _get_value(entry) -> String: + if entry._is_folder(): + return "" + else: + return GDBFormat.smart_format_unixtime(entry._get_modified_time()) + func _get_min_width() -> float: + return 150.0 + func _compare(entry0, entry1) -> bool: + return entry0._get_modified_time() < entry1._get_modified_time() + +class __ParentFolderEntry: + extends UIB_FileAdapter.FileEntry + + var real_entry + + func _init(real_entry_): + real_entry = real_entry_ + + func _get_name() -> String: + return tr("") + + func _is_folder() -> bool: + return true + +const CHECKBOX_COLUMN_WIDTH = 25 + +export(ViewMode) var view_mode = ViewMode.DETAILS setget _set_view_mode +export(Checkable) var checkable = Checkable.NONE setget _set_checkable +export var allow_navigation := true +export var show_up_entry := true +var adapter : UIB_FileAdapter setget _set_adapter +var filter : UIB_FileFilter setget _set_filter +var current_folder : UIB_FileAdapter.FileEntry setget _set_current_folder +var details_columns := [ + DetailsColumnName.new(), + DetailsColumnType.new(), + DetailsColumnSize.new(), + DetailsColumnModifiedTime.new() +] +var __special_columns := [] +var __sort_column := 0 +var __sort_reverse := false + +func _ready(): + hide_root = true + select_mode = Tree.SELECT_ROW + + if adapter == null: + adapter = UIB_LocalFileAdapter.new() + + if current_folder == null: + current_folder = adapter._get_root() + + __update_entries() + + connect("item_activated", self, "_on_item_activated") + connect("item_selected", self, "_on_item_selected") + connect("column_title_pressed", self, "_on_column_title_pressed") + +################# +# private stuff # +################# +func __compare_entries(entry0, entry1): + if entry0._is_folder() != entry1._is_folder(): + return entry0._is_folder() + var val = details_columns[__sort_column]._compare(entry0, entry1) + if __sort_reverse: + return !val + return val + +func __update_columns(): + __special_columns.clear() + if checkable != Checkable.NONE: + __special_columns.append(SpecialColumn.CHECKBOX) + + match view_mode: + _: # DETAILS + columns = __special_columns.size() + details_columns.size() + set_column_titles_visible(true) + for i in range(details_columns.size()): + set_column_title(__special_columns.size() + i, details_columns[i]._get_name()) + set_column_expand(i, details_columns[i]._get_expand()) + set_column_min_width(i, details_columns[i]._get_min_width()) + + for i in range(__special_columns.size()): + set_column_title(i, "") + set_column_expand(i, false) + set_column_min_width(i, CHECKBOX_COLUMN_WIDTH) + +func __entry_is_checkable(entry): + match checkable: + Checkable.BOTH: + return true + Checkable.FILES: + return !entry._is_folder() + Checkable.FOLDERS: + return entry._is_folder() + _: + return false + +func __update_entries(): + if !current_folder: + return + + clear() + create_item() # root + + __update_columns() + if columns < 1: + return + + var entries := current_folder._list_files() + entries.sort_custom(self, "__compare_entries") + + if allow_navigation && show_up_entry: + var parent_folder = current_folder._get_parent() + if parent_folder: + entries.push_front(__ParentFolderEntry.new(parent_folder)) + + for entry in entries: + if filter && !entry._is_folder() && !filter._accepts(entry): + continue + + var itm = create_item() + for i in range(__special_columns.size()): + match __special_columns[i]: + SpecialColumn.CHECKBOX: + if __entry_is_checkable(entry): + itm.set_cell_mode(i, TreeItem.CELL_MODE_CHECK) + for i in range(details_columns.size()): + itm.set_text(__special_columns.size() + i, details_columns[i]._get_value(entry)) + itm.set_icon(__special_columns.size(), adapter._get_icon(entry)) + itm.set_metadata(0, entry) + +########### +# setters # +########### +func _set_view_mode(value): + if value != view_mode: + view_mode = value + __update_entries() + +func _set_adapter(value : UIB_FileAdapter): + if value != adapter: + adapter = value + __update_entries() + +func _set_filter(value : UIB_FileFilter): + if value != filter: + filter = value + __update_entries() + +func _set_current_folder(value : UIB_FileAdapter.FileEntry): + if value != current_folder: + current_folder = value + emit_signal("current_folder_changed") + __update_entries() + +func _set_checkable(value): + if value != checkable: + checkable = value + __update_entries() + +############ +# handlers # +############ +func _on_item_activated(): + var selected := get_selected() + if !selected: + return + var entry = selected.get_metadata(0) + if !entry: + return + if allow_navigation && entry._is_folder(): + if entry is __ParentFolderEntry: + _set_current_folder(entry.real_entry) + else: + _set_current_folder(entry) + else: + emit_signal("file_activated", entry) + +func _on_item_selected(): + var selected := get_selected() + if !selected: + return + var entry = selected.get_metadata(0) + if !entry: + return + emit_signal("file_selected", entry) + +func _on_column_title_pressed(idx : int): + var sort_idx := idx - __special_columns.size() + if sort_idx >= details_columns.size(): + return + if sort_idx == __sort_column: + __sort_reverse = !__sort_reverse + else: + __sort_column = sort_idx + __sort_reverse = false + __update_entries() + +########### +# signals # +########### +signal current_folder_changed() +signal file_activated(file_entry) +signal file_selected(file_entry) diff --git a/scripts/types/controls/flow_container.gd b/scripts/types/controls/flow_container.gd new file mode 100644 index 0000000..e33d7cc --- /dev/null +++ b/scripts/types/controls/flow_container.gd @@ -0,0 +1,43 @@ +tool +extends Container + +const SORT_DELAY = 0.02 + +class_name UIB_FlowContainer + +export var use_minimum_size := false +export var gap := 0.0 + +############# +# overrides # +############# +func _notification(what): + if what == NOTIFICATION_SORT_CHILDREN: + __sort_children() + +################# +# private stuff # +################# +func __sort_children(): + var pos := Vector2() + var row_height := 0.0 + var vp_rect := get_viewport_rect() + + for child in get_children(): + if !child.visible || !child is Control: + continue + + if use_minimum_size: + child.rect_size = child.get_combined_minimum_size() + + if pos.x > 0.0 && pos.x + child.rect_size.x > rect_size.x: + pos.x = 0.0 + pos.y += row_height + gap + row_height = 0.0 + + child.rect_position = pos + + row_height = max(row_height, child.rect_size.y) + pos.x += child.rect_size.x + gap + + rect_min_size.y = pos.y + row_height diff --git a/scripts/types/controls/folder_edit.gd b/scripts/types/controls/folder_edit.gd new file mode 100644 index 0000000..c471d9e --- /dev/null +++ b/scripts/types/controls/folder_edit.gd @@ -0,0 +1,79 @@ +extends LineEdit + +class_name UIB_FolderEdit + +var suggestion_popup := UIB_SuggestionPopup.new() +var adapter : UIB_FileAdapter +var current_folder : UIB_FileAdapter.FileEntry setget _set_current_folder + +############# +# overrides # +############# +func _ready(): + call_deferred("__setup") + connect("text_changed", self, "_on_text_changed") + connect("text_entered", self, "_on_text_entered") + +################# +# private stuff # +################# +func __setup(): + suggestion_popup.connect("suggestion_accepted", self, "_on_suggestion_accepted") + add_child(suggestion_popup) + +func __set_suggestions(files : Array): + var suggestions := [] + + for file in files: + if file._is_folder(): + suggestions.append(file._get_path() + "/") + + suggestion_popup.suggestions = suggestions + +func __update_suggestions(): + var folder = adapter._get_file(text) + + if folder && folder._is_folder(): + __set_suggestions(folder._list_files()) + return + var parent_path = text.get_base_dir() + + if parent_path: + folder = adapter._get_file(parent_path) + if folder && folder._is_folder(): + __set_suggestions(folder._list_files()) + return + suggestion_popup.suggestions = [] + +func __update_current_folder(): + var new_file = adapter._get_file(text) + if new_file: + _set_current_folder(new_file) + caret_position = text.length() + +########### +# setters # +########### +func _set_current_folder(value): + if value != current_folder: + current_folder = value + text = current_folder._get_path() + emit_signal("current_folder_changed") + +############ +# handlers # +############ +func _on_text_changed(new_text): + __update_suggestions() + +func _on_text_entered(new_text): + __update_current_folder() + +func _on_suggestion_accepted(suggestion): + __update_current_folder() + __update_suggestions() + +########### +# signals # +########### +signal current_folder_changed() diff --git a/scripts/types/controls/folder_sync.gd b/scripts/types/controls/folder_sync.gd new file mode 100644 index 0000000..40e0d74 --- /dev/null +++ b/scripts/types/controls/folder_sync.gd @@ -0,0 +1,104 @@ +extends Node + +class_name UIB_FolderSync + +enum DefaultFolder { + HOME, + ROOT, + CUSTOM +} + +export(Array, NodePath) var nodes = [] +export(DefaultFolder) var default_folder = DefaultFolder.HOME +export(NodePath) var folder_up_action +export(NodePath) var folder_favourite_action +export var custom_default_folder := "" +var adapter : UIB_FileAdapter = UIB_LocalFileAdapter.new() + +var current_folder : UIB_FileAdapter.FileEntry setget set_current_folder +var __folder_up_action : UIB_SimpleAction +var __folder_favourite_action : UIB_SimpleAction +var __nodes := [] +var __locked := false + +############# +# overrides # +############# +func _ready(): + __folder_up_action = __get_action(folder_up_action) + __folder_favourite_action = __get_action(folder_favourite_action) + + if __folder_up_action: + __folder_up_action.connect("applied", self, "_on_folder_up_action") + + if __folder_favourite_action: + __folder_favourite_action.connect("applied", self, "_on_folder_favourite_action") + + var folder := "/" + match default_folder: + DefaultFolder.HOME: + folder = GDBFsUtil.get_home_folder() + DefaultFolder.CUSTOM: + folder = custom_default_folder + + for path in nodes: + var node := get_node(path) + assert(node) + if !node: + continue + node.adapter = adapter + node.connect("current_folder_changed", self, "_on_node_current_folder_changed", [node]) + __nodes.append(node) + + var folder_entry = adapter._get_file(folder) + if folder_entry && folder_entry._is_folder(): + set_current_folder(folder_entry) + +################ +# public stuff # +################ +func set_current_folder(folder, ignore_node : Node = null): + if __locked: + return + __locked = true + + for node in __nodes: + if node != ignore_node: + node.current_folder = folder + + __locked = false + current_folder = folder + + if __folder_up_action: + __folder_up_action.disabled = !folder || !folder._get_parent() + if __folder_favourite_action: + __folder_favourite_action.toggled = GDBUX.is_favourite_place(current_folder) + +func __get_action(node_path : NodePath) -> UIB_SimpleAction: + var action_node = get_node_or_null(node_path) + + if action_node is UIB_SimpleAction: + return action_node + elif action_node.has_method("get_action"): + return action_node.get_action() as UIB_SimpleAction + + return null + +############ +# handlers # +############ +func _on_node_current_folder_changed(source_node : Node): + set_current_folder(source_node.current_folder, source_node) + +func _on_folder_up_action(): + var parent = current_folder._get_parent() + if parent: + set_current_folder(parent) + +func _on_folder_favourite_action(): + if GDBUX.is_favourite_place(current_folder): + GDBUX.remove_favourite_place(current_folder) + __folder_favourite_action.toggled = false + else: + GDBUX.add_favourite_place(current_folder) + __folder_favourite_action.toggled = true diff --git a/scripts/types/controls/named_tab_container.gd b/scripts/types/controls/named_tab_container.gd new file mode 100644 index 0000000..02d1c0d --- /dev/null +++ b/scripts/types/controls/named_tab_container.gd @@ -0,0 +1,16 @@ +extends TabContainer + +class_name UIB_NamedTabContainer + +export(Array, String) var headers := [] +export(Array, Texture) var icons := [] + +func _ready(): + for i in range(headers.size()): + if i >= get_child_count(): + break + set_tab_title(i, headers[i]) + for i in range(icons.size()): + if i >= get_child_count(): + break + set_tab_icon(i, icons[i]) diff --git a/scripts/types/controls/places_list.gd b/scripts/types/controls/places_list.gd new file mode 100644 index 0000000..685c8cf --- /dev/null +++ b/scripts/types/controls/places_list.gd @@ -0,0 +1,122 @@ +extends ItemList + +class_name UIB_PlacesList + +enum Mode { + DEFAULT, +# FAVOURITES, + CUSTOM +} + +class __DefaultItem: + var place : String + var icon : Texture + var label : String + + func _init(place_ : String, icon_ : Texture, label_ : String): + place = place_ + icon = icon_ + label = label_ + +var __DEFAULT_ITEMS := [ + __DefaultItem.new(GDBFsUtil.get_home_folder(), preload("res://addons/de.mewin.gduibasics/images/home32.svg"), tr("Home")) +] + +export(Mode) var mode = Mode.DEFAULT setget _set_mode +export(Array, String) var places := [] setget _set_places +export(Array, Texture) var icons := [] +export(Array, String) var names := [] + +var adapter : UIB_FileAdapter +var current_folder : UIB_FileAdapter.FileEntry setget _set_current_folder + +############# +# overrides # +############# +func _ready(): + call_deferred("__update_items") + + connect("item_activated", self, "_on_item_activated") + GDBUX.connect_static("favourite_places_changed", self, "_on_favourite_places_changed") + +################# +# private stuff # +################# +func __update_items(): + clear() + if !adapter: + return + + match mode: + Mode.DEFAULT: + __add_default_places() + _: + __add_custom_places() + +func __add_default_places(): + for default_place in __DEFAULT_ITEMS: + var folder := adapter._get_file(default_place.place) + if !folder || !folder._is_folder(): + continue + __add_place(folder, default_place.icon, default_place.label) + + for favourite in GDBUX.get_favourite_places(): + var folder := adapter._get_file(favourite) + if !folder || !folder._is_folder(): + continue + __add_place(folder, preload("res://addons/de.mewin.gduibasics/images/favourite32.svg")) + +func __add_custom_places(): + for i in range(places.size()): + var folder = adapter._get_file(places[i]) + if !folder || !folder._is_folder(): + continue + var label : String = names[i] if i < names.size() else "" + var icon : Texture = icons[i] if i < icons.size() else null + __add_place(folder, icon, label) + +func __add_place(folder : UIB_FileAdapter.FileEntry, icon : Texture, label := ""): + if !icon: + icon = preload("res://addons/de.mewin.gduibasics/images/folder32.svg") + if !label: + label = folder._get_name() + var idx = get_item_count() + add_item(label, icon) + set_item_metadata(idx, folder) + +func __select_current_folder(): + for i in range(get_item_count()): + if get_item_metadata(i)._get_path() == current_folder._get_path(): + select(i) + +########### +# setters # +########### +func _set_mode(value): + if value != mode: + mode = value + __update_items() + +func _set_places(value): + if value != places: + places = value + __update_items() + +func _set_current_folder(value): + if value != current_folder: + current_folder = value + emit_signal("current_folder_changed") + +############ +# handlers # +############ +func _on_item_activated(idx): + _set_current_folder(get_item_metadata(idx)) + +func _on_favourite_places_changed(): + if mode == Mode.DEFAULT: + __update_items() +########### +# signals # +########### +signal current_folder_changed() diff --git a/scripts/types/controls/recent_places_list.gd b/scripts/types/controls/recent_places_list.gd new file mode 100644 index 0000000..e7a29f9 --- /dev/null +++ b/scripts/types/controls/recent_places_list.gd @@ -0,0 +1,18 @@ +extends UIB_PlacesList + +class_name UIB_RecentPlacesList + +############# +# overrides # +############# +func _ready(): + places = GDBSettings.get_value(GDBConstants.SETTING_RECENT_PLACES, []) + + GDBSettings.connect_static("setting_changed", self, "_on_setting_changed") + +############ +# handlers # +############ +func _on_setting_changed(setting, value): + if setting == GDBConstants.SETTING_RECENT_PLACES: + _set_places(value) diff --git a/scripts/types/controls/scroll_parallax_bg.gd b/scripts/types/controls/scroll_parallax_bg.gd new file mode 100644 index 0000000..60b6919 --- /dev/null +++ b/scripts/types/controls/scroll_parallax_bg.gd @@ -0,0 +1,58 @@ +extends Control + +class_name UIB_ScrollParallaxBackground + +export(Array, Texture) var textures := [] +export(Array, float) var speeds := [] +export(NodePath) var scroll_container + +var __scroll_container : ScrollContainer + +func _ready(): + show_behind_parent = true + rect_clip_content = true + + if !scroll_container: + for child in get_children(): + if child is ScrollContainer: + scroll_container = get_path_to(child) + break + + __scroll_container = get_node_or_null(scroll_container) as ScrollContainer + + set_process(false) + + if __scroll_container: +# __scroll_container.connect("scroll_started", self, "_on_scroll_started") +# __scroll_container.connect("scroll_ended", self, "_on_scroll_ended") + __scroll_container.get_h_scrollbar().connect("value_changed", self, "_on_scroll_value_changed") + __scroll_container.get_v_scrollbar().connect("value_changed", self, "_on_scroll_value_changed") + +func _draw(): + if !__scroll_container || __scroll_container.get_child_count() < 1: + return + var first_child := __scroll_container.get_child(0) as Control + if !first_child: + return + + for i in range(textures.size()): + var texture : Texture = textures[i] + var speed : float = speeds[i] if i < speeds.size() else 1.0 + var rel_scroll := __scroll_container.scroll_vertical / max(first_child.rect_size.y - __scroll_container.rect_size.y, 1.0) + var tex_size := rect_size + var tex_pos := Vector2() + tex_size.y = tex_size.x * (texture.get_height() / texture.get_width()) + tex_pos.y = speed * -rel_scroll * (tex_size.y - rect_size.y) + draw_texture_rect(texture, Rect2(tex_pos, tex_size), false) + +func _process(delta): + update() + +func _on_scroll_started(): + set_process(true) + +func _on_scroll_ended(): + set_process(false) + +func _on_scroll_value_changed(value : float): + update() diff --git a/scripts/types/controls/setting_check_box.gd b/scripts/types/controls/setting_check_box.gd new file mode 100644 index 0000000..a843dba --- /dev/null +++ b/scripts/types/controls/setting_check_box.gd @@ -0,0 +1,11 @@ +extends CheckBox + +class_name UIB_SettingCheckBox + +export var setting_name = "" + +func _ready(): + pressed = GDBSettings.get_value(setting_name, false) + +func _pressed(): + GDBSettings.set_value(setting_name, pressed) diff --git a/scripts/types/controls/setting_option_button.gd b/scripts/types/controls/setting_option_button.gd new file mode 100644 index 0000000..c27bd69 --- /dev/null +++ b/scripts/types/controls/setting_option_button.gd @@ -0,0 +1,45 @@ +extends OptionButton + +class_name UIB_SettingOptionButton + +export var setting_name := "" +export(Array) var values := [] +export(Array, String) var labels := [] +export(Array, Texture) var icons := [] + +############# +# overrides # +############# +func _ready(): + __setup() + + connect("item_selected", self, "_on_item_selected") + +################# +# private stuff # +################# +func __setup(): + clear() + + for i in range(values.size()): + var value : String = values[i] + var label : String = labels[i] if i < labels.size() else str(value) + var icn : Texture = icons[i] if i < icons.size() else null + + add_item(label) + set_item_icon(i, icn) + + if !setting_name: + return + + var current_value = GDBSettings.get_value(setting_name) + var current_idx = values.find(current_value) + if current_idx >= 0: + select(current_idx) + +############ +# handlers # +############ +func _on_item_selected(idx : int): + if setting_name: + GDBSettings.set_value(setting_name, values[idx]) diff --git a/scripts/types/controls/suggestion_popup.gd b/scripts/types/controls/suggestion_popup.gd new file mode 100644 index 0000000..ffd8e51 --- /dev/null +++ b/scripts/types/controls/suggestion_popup.gd @@ -0,0 +1,231 @@ +extends Popup + +class_name UIB_SuggestionPopup + +export(NodePath) var line_edit_path = ".." +export(int, 1, 20) var max_values = 5 +export(Array, String) var suggestions = [] setget _set_suggestions +export(StyleBox) var style_normal = preload("res://addons/de.mewin.gduibasics/styles/suggestion_line_normal.stylebox") +export(StyleBox) var style_active = preload("res://addons/de.mewin.gduibasics/styles/suggestion_line_active.stylebox") +onready var line_edit : LineEdit = get_node(line_edit_path) + +var __v_box_container := VBoxContainer.new() +var __panel := Panel.new() +var __labels := [] +var __filtered_suggestions := [] +var __filter_text := "" +var __active_label := -1 +var __scroll := 0 +var __up_direction := false + +############# +# overrides # +############# +func _ready(): + call_deferred("__initial_setup") + +func _gui_input(event : InputEvent): + if !event.is_pressed() || !event is InputEventMouseButton: + return + + if event.button_index == BUTTON_LEFT: + __accept_suggestion() + accept_event() + elif event.button_index == BUTTON_WHEEL_DOWN: + if !__up_direction: + __focus_down() + else: + __focus_up() + elif event.button_index == BUTTON_WHEEL_UP: + if !__up_direction: + __focus_up() + else: + __focus_down() + +################# +# private stuff # +################# +func __initial_setup(): + __panel.show_behind_parent = true + add_child(__panel) + + add_child(__v_box_container) + + for i in range(max_values): + var label := Label.new() + label.size_flags_horizontal |= Control.SIZE_EXPAND + label.mouse_filter = Control.MOUSE_FILTER_PASS + label.connect("mouse_entered", self, "_on_label_mouse_entered", [i]) + + __labels.append(label) + __v_box_container.add_child(label) + __setup() + +func __setup(): + if !line_edit: + return + line_edit.focus_neighbour_top = "." + line_edit.focus_neighbour_bottom = "." + line_edit.connect("focus_entered", self, "_on_line_edit_focus_entered") + line_edit.connect("focus_exited", self, "_on_line_edit_focus_exited") + line_edit.connect("text_changed", self, "_on_line_edit_text_changed") + line_edit.connect("gui_input", self, "_on_line_edit_gui_input") + +func __calc_rect() -> Rect2: + var size := Vector2(line_edit.rect_size.x, __v_box_container.get_combined_minimum_size().y) + var lower_rect := Rect2(line_edit.rect_global_position \ + + Vector2(0.0, line_edit.rect_size.y), size) + if get_viewport_rect().encloses(lower_rect): + if __up_direction: + __up_direction = false + __update_labels(false) + return lower_rect + else: + var upper_rect := Rect2(line_edit.rect_global_position \ + - Vector2(0.0, size.y), size) + if !__up_direction: + __up_direction = true + __update_labels(false) + return upper_rect + +func __get_label(idx : int): + assert(idx < __labels.size()) + if __up_direction: + return __labels[__labels.size() - 1 - idx] + return __labels[idx] + +func __score_suggestion(suggestion : String) -> float: + if suggestion.to_lower().begins_with(__filter_text): + return 0.5 + 0.5 * suggestion.similarity(line_edit.text) + else: + return 0.5 * suggestion.similarity(line_edit.text) + +func __compare_suggestions(sugg0 : String, sugg1 : String): + var score0 := __score_suggestion(sugg0) + var score1 := __score_suggestion(sugg1) + if score0 != score1: + return score0 > score1 + else: + return sugg0 < sugg1 + +func __filter(): + __filter_text = line_edit.text.to_lower() # .left(line_edit.caret_position).to_lower() + __filtered_suggestions = suggestions.duplicate() + __filtered_suggestions.sort_custom(self, "__compare_suggestions") + +func __update_labels(update_size := true): + for i in range(__labels.size()): + var label : Label = __get_label(i) + if i >= __filtered_suggestions.size(): + label.visible = false + else: + label.visible = true + label.text = __filtered_suggestions[(i + __scroll) % __filtered_suggestions.size()] + if i == __active_label: + label.add_stylebox_override("normal", style_active) + else: + label.add_stylebox_override("normal", style_normal) + + if update_size: + var rect := __calc_rect() + rect_position = rect.position + rect_size = rect.size + __panel.rect_size = rect_size + __v_box_container.rect_size = rect_size + +func __focus_down(): + if __active_label == __labels.size() - 1: + __scroll = min(__scroll + 1, __filtered_suggestions.size() - __labels.size()) + if __scroll < 0: + __scroll = 0 + else: + __active_label += 1 + __update_labels() + line_edit.caret_position = line_edit.text.length() + +func __focus_up(): + if __active_label < 0: + return + elif __active_label > 0: + __active_label -= 1 + elif __scroll > 0: + __scroll -= 1 + else: + __active_label = -1 + __update_labels() + line_edit.caret_position = line_edit.text.length() + +func __accept_suggestion(): + if __active_label < 0: + return + var suggestion = __get_label(__active_label).text + line_edit.text = suggestion + line_edit.caret_position = suggestion.length() + __active_label = -1 + __scroll = 0 + __filter() + __update_labels() + emit_signal("suggestion_accepted", suggestion) + +########### +# setters # +########### +func _set_suggestions(value : Array): + suggestions = value + if visible: + __filter() + __update_labels() + +############ +# handlers # +############ +func _on_line_edit_focus_entered(): + __filter() + __update_labels() + + popup() + +func _on_line_edit_focus_exited(): + visible = false + +func _on_line_edit_text_changed(new_text : String): + if !line_edit || !line_edit.has_focus(): + return + + __active_label = -1 + __scroll = 0 + + __filter() + __update_labels() + if !visible: + popup() + +func _on_line_edit_gui_input(input_event : InputEvent): + if !input_event.is_pressed(): + return + + if input_event.is_action_pressed("ui_accept"): + __accept_suggestion() + elif input_event.is_action("ui_down"): + if !__up_direction: + __focus_down() + else: + __focus_up() + elif input_event.is_action("ui_up"): + if !__up_direction: + __focus_up() + else: + __focus_down() + +func _on_label_mouse_entered(index : int): + if __up_direction: + index = __labels.size() - index - 1 + + if __active_label != index: + __active_label = index + __update_labels() + +########### +# signals # +########### +signal suggestion_accepted(suggestion) diff --git a/scripts/types/controls/tabs_item_list.gd b/scripts/types/controls/tabs_item_list.gd new file mode 100644 index 0000000..f454fca --- /dev/null +++ b/scripts/types/controls/tabs_item_list.gd @@ -0,0 +1,41 @@ +extends ItemList + +class_name UIB_TabsItemList + +export(NodePath) var tab_container + +############# +# overrides # +############# +func _ready(): + call_deferred("__setup") + connect("item_selected", self, "_on_item_selected") + +################# +# private stuff # +################# +func __get_tab_container() -> TabContainer: + return get_node_or_null(tab_container) as TabContainer + +func __setup(): + var tab_container_ := __get_tab_container() + clear() + + if !tab_container_: + return + + for i in range(tab_container_.get_child_count()): + var title := tab_container_.get_tab_title(i) + var icon := tab_container_.get_tab_icon(i) + add_item(title, icon) + + select(tab_container_.current_tab) + +############ +# handlers # +############ +func _on_item_selected(idx : int): + var tab_container_ := __get_tab_container() + + if tab_container_: + tab_container_.current_tab = idx diff --git a/scripts/types/controls/touch_scroll_container.gd b/scripts/types/controls/touch_scroll_container.gd new file mode 100644 index 0000000..c19faf1 --- /dev/null +++ b/scripts/types/controls/touch_scroll_container.gd @@ -0,0 +1,77 @@ +extends ScrollContainer + +class_name UIB_TouchScrollContainer + +export(float) var drag_threshold := 20.0 + +var __dragging := false +var __drag_start := Vector2() +var __drag_start_scroll := Vector2() +var __momentum := Vector2() + +func _ready(): + set_process(false) + + get_h_scrollbar().modulate = Color.transparent + get_v_scrollbar().modulate = Color.transparent + +func _input(event): + if event is InputEventMouseMotion && (event.button_mask & BUTTON_MASK_LEFT) && \ + (__dragging || get_global_rect().has_point(event.position)): + if !__dragging: + var dragged = event.position - __drag_start + if abs(dragged.x) > drag_threshold \ + || abs(dragged.y) > drag_threshold: + __dragging = true + else: + __momentum = event.relative + if scroll_horizontal_enabled: + scroll_horizontal = __drag_start_scroll.x + __drag_start.x - event.position.x + if scroll_vertical_enabled: + scroll_vertical = __drag_start_scroll.y + __drag_start.y - event.position.y + elif event is InputEventMouseButton && event.button_index == BUTTON_LEFT \ + && get_global_rect().has_point(event.position): + if !event.pressed: + if __dragging: + __dragging = false + set_process(true) # process momentum + else: + __emulate_click(event) + else: + __drag_start = event.position + __drag_start_scroll = Vector2(scroll_horizontal, scroll_vertical) + get_tree().set_input_as_handled() + +func _process(delta : float): + __momentum *= 1.0 - 1.5 * delta + if abs(__momentum.x) < 0.01 && abs(__momentum.y) < 0.01: + set_process(false) + return + + if scroll_horizontal_enabled: + scroll_horizontal -= __momentum.x * 50.0 * delta + + if scroll_vertical_enabled: + scroll_vertical -= __momentum.y * 50.0 * delta + +func __emulate_click(orig_event : InputEventMouseButton): + if get_child_count() < 1: + return + + var child = get_child(0) as Control + if !child: + return + + set_process_input(false) + var down_event : InputEventMouseButton = orig_event.duplicate() + down_event.position = orig_event.global_position + down_event.pressed = true + Input.parse_input_event(down_event) + + yield(get_tree(), "idle_frame") + + var up_event : InputEventMouseButton = orig_event.duplicate() + up_event.position = orig_event.global_position + up_event.pressed = false + Input.parse_input_event(up_event) + set_process_input(true) diff --git a/scripts/types/controls/window_border_container.gd b/scripts/types/controls/window_border_container.gd new file mode 100644 index 0000000..81df172 --- /dev/null +++ b/scripts/types/controls/window_border_container.gd @@ -0,0 +1,28 @@ +extends UIB_BorderContainer + +class_name UIB_WindowBorderContainer + +export var minimum_size := Vector2(100.0, 100.0) + +############# +# overrides # +############# +func _win_get_position() -> Vector2: + return OS.window_position + +func _win_get_size() -> Vector2: + return OS.window_size + +func _win_get_mouse_position() -> Vector2: + return get_viewport().get_mouse_position() + OS.window_position + +func _win_get_minimum_size() -> Vector2: + return minimum_size + +func _win_set_position(pos : Vector2): + if pos != OS.window_position: + OS.window_position = pos + OS.window_maximized = false + +func _win_set_size(size : Vector2): + OS.window_size = size diff --git a/scripts/types/misc/action_reference.gd b/scripts/types/misc/action_reference.gd new file mode 100644 index 0000000..b334046 --- /dev/null +++ b/scripts/types/misc/action_reference.gd @@ -0,0 +1,8 @@ +extends Node + +class_name UIB_ActionReference + +export(NodePath) var action + +func get_action() -> UIB_Action: + return get_node_or_null(action) as UIB_Action diff --git a/scripts/types/misc/constraints/dynamic_grid_constraints.gd b/scripts/types/misc/constraints/dynamic_grid_constraints.gd new file mode 100644 index 0000000..d57a39b --- /dev/null +++ b/scripts/types/misc/constraints/dynamic_grid_constraints.gd @@ -0,0 +1,25 @@ +tool +extends Node + +class_name UIB_DynamicGridConstraints + +export(int, 1, 100) var colspan := 1 setget _set_colspan +export(int, 1, 100) var rowspan := 1 setget _set_rowspan + +func _ready(): + __trigger_update() + +func __trigger_update(): + var container := get_node_or_null("../..") as Container # cyclic inclusion, cannot use UIB_DynamicGridContainer + if container != null: + container.notification(Container.NOTIFICATION_SORT_CHILDREN) + +func _set_colspan(value : int): + if value != colspan: + colspan = value + __trigger_update() + +func _set_rowspan(value : int): + if value != rowspan: + rowspan = value + __trigger_update() diff --git a/scripts/types/misc/file_adapter.gd b/scripts/types/misc/file_adapter.gd new file mode 100644 index 0000000..b7e1fc0 --- /dev/null +++ b/scripts/types/misc/file_adapter.gd @@ -0,0 +1,50 @@ +extends Reference + +class_name UIB_FileAdapter + +class FileEntry: + func _is_folder() -> bool: + return false + + func _is_link() -> bool: + return false + + func _list_files() -> Array: + return [] + + func _get_name() -> String: + return "" + + func _get_size() -> int: + return 0 + + func _get_modified_time() -> int: + return 0 + + func _get_parent() -> FileEntry: + return null + + func _get_path() -> String: + var parent := _get_parent() + if parent: + return parent._get_path().plus_file(_get_name()) + var name := _get_name() + if name == "": + return "/" + return name + +func _get_root() -> FileEntry: + return null + +func _get_file(path : String) -> FileEntry: + return null + +func _get_icon(entry) -> Texture: + if entry._is_folder(): + return preload("res://addons/de.mewin.gduibasics/images/folder.svg") + else: + + return preload("res://addons/de.mewin.gduibasics/images/file.svg") + +func _get_drives() -> Array: + return [""] diff --git a/scripts/types/misc/file_extension_filter.gd b/scripts/types/misc/file_extension_filter.gd new file mode 100644 index 0000000..6699a01 --- /dev/null +++ b/scripts/types/misc/file_extension_filter.gd @@ -0,0 +1,20 @@ +extends UIB_FileFilter + +class_name UIB_FileExtensionFilter + +var __extensions : PoolStringArray + +func _init(extensions_ : Array): + for ext in extensions_: + if !ext.begins_with("."): + ext = "." + ext + __extensions.append(ext) + +func _accepts(entry) -> bool: + for ext in __extensions: + if entry._get_name().ends_with(ext): + return true + return false + +func _get_default_name() -> String: + return tr("%s-Files") % __extensions.join(", ") diff --git a/scripts/types/misc/file_filter.gd b/scripts/types/misc/file_filter.gd new file mode 100644 index 0000000..aa4c19a --- /dev/null +++ b/scripts/types/misc/file_filter.gd @@ -0,0 +1,16 @@ +extends Reference + +class_name UIB_FileFilter + +var name := "" + +func _accepts(entry) -> bool: + return true + +func _get_name() -> String: + if name: + return name + return _get_default_name() + +func _get_default_name() -> String: + return "" diff --git a/scripts/types/misc/local_file_adapter.gd b/scripts/types/misc/local_file_adapter.gd new file mode 100644 index 0000000..39a4ca5 --- /dev/null +++ b/scripts/types/misc/local_file_adapter.gd @@ -0,0 +1,110 @@ +extends UIB_FileAdapter + +class_name UIB_LocalFileAdapter + +var __dir = Directory.new() + +class LocalFileEntry: + extends UIB_FileAdapter.FileEntry + + var path : String + var is_link : bool + var is_folder : bool + var size := -1 + var modified_time := -1 + + func _init(path_ : String, is_link_ : bool, is_folder_ : bool): + path = path_ + is_link = is_link_ + is_folder = is_folder_ + if path.ends_with("/") && path != "/": + path = path.left(path.length() - 1) + + func _is_folder() -> bool: + return is_folder + + func _is_link() -> bool: + return is_link + + func _list_files() -> Array: + if !is_folder: + return [] + var dir := Directory.new() + if dir.open(path + "/") != OK: + return [] + if dir.list_dir_begin(true) != OK: + return [] + var fname := dir.get_next() + var files := [] + while fname: + var fpath := path.plus_file(fname) + var ffolder := dir.current_is_dir() + var flink := false + + files.append(LocalFileEntry.new(fpath, flink, ffolder)) + + fname = dir.get_next() + return files + + func _get_name() -> String: + return path.get_file() + + func _get_size() -> int: + if size == -1: + size = __calc_size() + return size + + func _get_modified_time() -> int: + if modified_time == -1: + modified_time = __calc_modified_time() + return modified_time + + func _get_parent() -> UIB_FileAdapter.FileEntry: + var parent_path := path.get_base_dir() + if parent_path && parent_path != path: + return LocalFileEntry.new(parent_path, false, true) + return null + + func __calc_size() -> int: + if is_folder: + return 0 + var file := File.new() + if file.open(path, File.READ) != OK: + return 0 + return file.get_len() + + func __calc_modified_time() -> int: + if is_folder: + return 0 + var file := File.new() + return file.get_modified_time(path) + +func _get_file(path : String) -> FileEntry: + if OS.has_feature("Windows"): # has "feature" + path = path.replace("\\", "/") + if path.length() < 2 || path[1] != ":": + path = "c:/" + path.lstrip("/") + if path.length() == 2: # + path += "/" + elif !path.begins_with("/"): + path = "/" + path # only drive letter, dir_exists() wouldnt work + + var dir := Directory.new() + if dir.dir_exists(path): + return LocalFileEntry.new(path, false, true) + elif dir.file_exists(path): + return LocalFileEntry.new(path, false, false) + else: + return null + +func _get_root() -> UIB_FileAdapter.FileEntry: + return _get_file("/") + +func _get_drives() -> Array: + var dir := Directory.new() + var drives := [] + + for i in range(dir.get_drive_count()): + drives.append(dir.get_drive(i)) + + return drives diff --git a/scripts/types/misc/seperator.gd b/scripts/types/misc/seperator.gd new file mode 100644 index 0000000..4d266a8 --- /dev/null +++ b/scripts/types/misc/seperator.gd @@ -0,0 +1,5 @@ +extends Node + +class_name UIB_Seperator + +export(String) var text := "" diff --git a/scripts/types/misc/simple_action.gd b/scripts/types/misc/simple_action.gd new file mode 100644 index 0000000..f3931cd --- /dev/null +++ b/scripts/types/misc/simple_action.gd @@ -0,0 +1,102 @@ +extends UIB_Action + +class_name UIB_SimpleAction + +export var text := "" setget _set_text +export(Texture) var icon setget _set_icon +export(Texture) var untoggled_icon setget _set_untoggled_icon +export(String) var shortcut_action setget _set_shortcut_action +export var disabled := false setget _set_disabled +export var toggleable := false setget _set_toggleable +export var toggled := false setget _set_toggled + +############# +# overrides # +############# +func _get_text() -> String: + return text + +func _get_icon() -> Texture: + if untoggled_icon && !toggled: + return untoggled_icon + return icon + +func _get_shortcut() -> ShortCut: + if shortcut_action: + var action_list = InputMap.get_action_list(shortcut_action) + if !action_list: + return null + var shortcut := ShortCut.new() + shortcut.shortcut = action_list[0] + return shortcut + return null + +func _is_disabled() -> bool: + return disabled + +func _is_toggleable() -> bool: + return toggleable + +func _is_toggled() -> bool: + return toggled + +func _toggle(value : bool): + if toggleable: + _set_toggled(value) + +func _apply(): + emit_signal("applied") + +func _update(): + emit_signal("update", self) + +########### +# setters # +########### +func _set_text(value : String): + if value != text: + text = value + emit_signal("text_changed") + +func _set_icon(value : Texture): + if value != icon: + icon = value + emit_signal("icon_changed") + +func _set_untoggled_icon(value : Texture): + if value != untoggled_icon: + untoggled_icon = value + emit_signal("icon_changed") + +func _set_shortcut_action(value : String): + if value != shortcut_action: + shortcut_action = value + emit_signal("shortcut_changed") + +func _set_disabled(value : bool): + if value != disabled: + disabled = value + emit_signal("disabled_changed") + +func _set_toggled(value : bool): + if value != toggled: + toggled = value + emit_signal("toggled_changed") + if toggled: + _set_toggleable(true) + if untoggled_icon: + emit_signal("icon_changed") + +func _set_toggleable(value : bool): + if value != toggleable: + toggleable = value + emit_signal("toggleable_changed") + if !toggleable: + _set_toggled(false) + +########### +# signals # +########### +signal applied() +signal update(action) +signal toggleable_changed() diff --git a/scripts/types/misc/ui_action.gd b/scripts/types/misc/ui_action.gd new file mode 100644 index 0000000..89e084f --- /dev/null +++ b/scripts/types/misc/ui_action.gd @@ -0,0 +1,71 @@ +extends Node + +class_name UIB_Action, "res://addons/de.mewin.gduibasics/images/action.svg" + +export var continuous_update = false setget _set_continuous_update + +############# +# overrides # +############# +func _ready(): + set_process(continuous_update) + +func _process(delta): + _update() + +func _unhandled_input(event : InputEvent): + if self._is_disabled() || !event.is_pressed(): + return + + var shortcut := self._get_shortcut() + if !shortcut || !shortcut.is_shortcut(event): + return + + self._apply() + +################ +# overridables # +################ +func _get_text() -> String: + return "" + +func _get_icon() -> Texture: + return null + +func _get_shortcut() -> ShortCut: + return null + +func _is_disabled() -> bool: + return false + +func _is_toggleable() -> bool: + return false + +func _is_toggled() -> bool: + return false + +func _toggle(value : bool): + pass + +func _apply(): + _toggle(!_is_toggled()) + +func _update(): + pass + +########### +# setters # +########### +func _set_continuous_update(value): + if value != continuous_update: + continuous_update = value + set_process(continuous_update) + +########### +# signals # +########### +signal text_changed() +signal icon_changed() +signal shortcut_changed() +signal disabled_changed() +signal toggled_changed() diff --git a/styles/suggestion_line_active.stylebox b/styles/suggestion_line_active.stylebox new file mode 100644 index 0000000..4487781 Binary files /dev/null and b/styles/suggestion_line_active.stylebox differ diff --git a/styles/suggestion_line_normal.stylebox b/styles/suggestion_line_normal.stylebox new file mode 100644 index 0000000..1623b26 Binary files /dev/null and b/styles/suggestion_line_normal.stylebox differ