From efe955657991fbd4e02525483e9c3779fa7f3d8d Mon Sep 17 00:00:00 2001 From: Patrick Wuttke Date: Sat, 7 Aug 2021 21:15:07 +0200 Subject: [PATCH] Initial commit --- fonts/Vera.ttf | Bin 0 -> 65932 bytes fonts/VeraLarge.tres | 7 + fonts/VeraMedium.tres | 6 + fonts/VeraMono.ttf | Bin 0 -> 49224 bytes fonts/VeraMonoLarge.tres | 7 + fonts/VeraMonoMedium.tres | 6 + fonts/VeraMonoSmall.tres | 7 + fonts/VeraSmall.tres | 7 + fonts/Vera_COPYRIGHT.TXT | 124 ++++++ images/action.svg | 75 ++++ images/action.svg.import | 34 ++ images/close.svg | 68 ++++ images/close.svg.import | 34 ++ images/favourite.svg | 63 +++ images/favourite.svg.import | 34 ++ images/favourite32.svg | 63 +++ images/favourite32.svg.import | 34 ++ images/favourite_inactive.svg | 63 +++ images/favourite_inactive.svg.import | 34 ++ images/file.svg | 65 +++ images/file.svg.import | 34 ++ images/folder.svg | 66 ++++ images/folder.svg.import | 34 ++ images/folder32.svg | 66 ++++ images/folder32.svg.import | 34 ++ images/home32.svg | 66 ++++ images/home32.svg.import | 34 ++ images/maximize.svg | 63 +++ images/maximize.svg.import | 34 ++ images/minimize.svg | 67 ++++ images/minimize.svg.import | 34 ++ plugin.cfg | 7 + plugin.gd | 8 + scenes/dynamic_tabs.tscn | 22 ++ scenes/editor_file_edit.tscn | 32 ++ scenes/editor_node_edit.tscn | 36 ++ scenes/flexview.tscn | 97 +++++ scenes/flexview_hspliter.tscn | 11 + scenes/flexview_vspliter.tscn | 8 + scenes/flexview_window.tscn | 21 + scenes/line_edit_suggestion_popup.tscn | 27 ++ scenes/line_input_dialog.tscn | 51 +++ scenes/node_selection_dialog.tscn | 26 ++ scenes/scripts/dynamic_tabs.gd | 174 ++++++++ scenes/scripts/editor_file_edit.gd | 31 ++ scenes/scripts/editor_node_edit.gd | 31 ++ scenes/scripts/flexview.gd | 371 ++++++++++++++++++ scenes/scripts/flexview_splitter.gd | 38 ++ scenes/scripts/flexview_window.gd | 141 +++++++ scenes/scripts/line_edit_suggestion_popup.gd | 100 +++++ scenes/scripts/line_input_dialog.gd | 47 +++ scenes/scripts/node_selection_dialog.gd | 5 + scenes/scripts/selector_popup.gd | 174 ++++++++ scenes/scripts/variant_editor.gd | 77 ++++ scenes/selector_popup.tscn | 40 ++ scenes/variant_editor.tscn | 34 ++ scripts/libs/ui_util.gd | 55 +++ scripts/libs/ux.gd | 77 ++++ scripts/types/controls/action_button.gd | 82 ++++ scripts/types/controls/action_menu_button.gd | 57 +++ scripts/types/controls/action_popup_menu.gd | 50 +++ scripts/types/controls/action_toolbar.gd | 29 ++ scripts/types/controls/border_container.gd | 250 ++++++++++++ .../types/controls/dynamic_grid_container.gd | 95 +++++ .../controls/editor_inspector_category.gd | 57 +++ scripts/types/controls/editor_node_tree.gd | 203 ++++++++++ scripts/types/controls/file_list.gd | 280 +++++++++++++ scripts/types/controls/flow_container.gd | 43 ++ scripts/types/controls/folder_edit.gd | 79 ++++ scripts/types/controls/folder_sync.gd | 104 +++++ scripts/types/controls/named_tab_container.gd | 16 + scripts/types/controls/places_list.gd | 122 ++++++ scripts/types/controls/recent_places_list.gd | 18 + scripts/types/controls/scroll_parallax_bg.gd | 58 +++ scripts/types/controls/setting_check_box.gd | 11 + .../types/controls/setting_option_button.gd | 45 +++ scripts/types/controls/suggestion_popup.gd | 231 +++++++++++ scripts/types/controls/tabs_item_list.gd | 41 ++ .../types/controls/touch_scroll_container.gd | 77 ++++ .../types/controls/window_border_container.gd | 28 ++ scripts/types/misc/action_reference.gd | 8 + .../constraints/dynamic_grid_constraints.gd | 25 ++ scripts/types/misc/file_adapter.gd | 50 +++ scripts/types/misc/file_extension_filter.gd | 20 + scripts/types/misc/file_filter.gd | 16 + scripts/types/misc/local_file_adapter.gd | 110 ++++++ scripts/types/misc/seperator.gd | 5 + scripts/types/misc/simple_action.gd | 102 +++++ scripts/types/misc/ui_action.gd | 71 ++++ styles/suggestion_line_active.stylebox | Bin 0 -> 482 bytes styles/suggestion_line_normal.stylebox | Bin 0 -> 421 bytes 91 files changed, 5387 insertions(+) create mode 100644 fonts/Vera.ttf create mode 100644 fonts/VeraLarge.tres create mode 100644 fonts/VeraMedium.tres create mode 100644 fonts/VeraMono.ttf create mode 100644 fonts/VeraMonoLarge.tres create mode 100644 fonts/VeraMonoMedium.tres create mode 100644 fonts/VeraMonoSmall.tres create mode 100644 fonts/VeraSmall.tres create mode 100644 fonts/Vera_COPYRIGHT.TXT create mode 100644 images/action.svg create mode 100644 images/action.svg.import create mode 100644 images/close.svg create mode 100644 images/close.svg.import create mode 100644 images/favourite.svg create mode 100644 images/favourite.svg.import create mode 100644 images/favourite32.svg create mode 100644 images/favourite32.svg.import create mode 100644 images/favourite_inactive.svg create mode 100644 images/favourite_inactive.svg.import create mode 100644 images/file.svg create mode 100644 images/file.svg.import create mode 100644 images/folder.svg create mode 100644 images/folder.svg.import create mode 100644 images/folder32.svg create mode 100644 images/folder32.svg.import create mode 100644 images/home32.svg create mode 100644 images/home32.svg.import create mode 100644 images/maximize.svg create mode 100644 images/maximize.svg.import create mode 100644 images/minimize.svg create mode 100644 images/minimize.svg.import create mode 100644 plugin.cfg create mode 100644 plugin.gd create mode 100644 scenes/dynamic_tabs.tscn create mode 100644 scenes/editor_file_edit.tscn create mode 100644 scenes/editor_node_edit.tscn create mode 100644 scenes/flexview.tscn create mode 100644 scenes/flexview_hspliter.tscn create mode 100644 scenes/flexview_vspliter.tscn create mode 100644 scenes/flexview_window.tscn create mode 100644 scenes/line_edit_suggestion_popup.tscn create mode 100644 scenes/line_input_dialog.tscn create mode 100644 scenes/node_selection_dialog.tscn create mode 100644 scenes/scripts/dynamic_tabs.gd create mode 100644 scenes/scripts/editor_file_edit.gd create mode 100644 scenes/scripts/editor_node_edit.gd create mode 100644 scenes/scripts/flexview.gd create mode 100644 scenes/scripts/flexview_splitter.gd create mode 100644 scenes/scripts/flexview_window.gd create mode 100644 scenes/scripts/line_edit_suggestion_popup.gd create mode 100644 scenes/scripts/line_input_dialog.gd create mode 100644 scenes/scripts/node_selection_dialog.gd create mode 100644 scenes/scripts/selector_popup.gd create mode 100644 scenes/scripts/variant_editor.gd create mode 100644 scenes/selector_popup.tscn create mode 100644 scenes/variant_editor.tscn create mode 100644 scripts/libs/ui_util.gd create mode 100644 scripts/libs/ux.gd create mode 100644 scripts/types/controls/action_button.gd create mode 100644 scripts/types/controls/action_menu_button.gd create mode 100644 scripts/types/controls/action_popup_menu.gd create mode 100644 scripts/types/controls/action_toolbar.gd create mode 100644 scripts/types/controls/border_container.gd create mode 100644 scripts/types/controls/dynamic_grid_container.gd create mode 100644 scripts/types/controls/editor_inspector_category.gd create mode 100644 scripts/types/controls/editor_node_tree.gd create mode 100644 scripts/types/controls/file_list.gd create mode 100644 scripts/types/controls/flow_container.gd create mode 100644 scripts/types/controls/folder_edit.gd create mode 100644 scripts/types/controls/folder_sync.gd create mode 100644 scripts/types/controls/named_tab_container.gd create mode 100644 scripts/types/controls/places_list.gd create mode 100644 scripts/types/controls/recent_places_list.gd create mode 100644 scripts/types/controls/scroll_parallax_bg.gd create mode 100644 scripts/types/controls/setting_check_box.gd create mode 100644 scripts/types/controls/setting_option_button.gd create mode 100644 scripts/types/controls/suggestion_popup.gd create mode 100644 scripts/types/controls/tabs_item_list.gd create mode 100644 scripts/types/controls/touch_scroll_container.gd create mode 100644 scripts/types/controls/window_border_container.gd create mode 100644 scripts/types/misc/action_reference.gd create mode 100644 scripts/types/misc/constraints/dynamic_grid_constraints.gd create mode 100644 scripts/types/misc/file_adapter.gd create mode 100644 scripts/types/misc/file_extension_filter.gd create mode 100644 scripts/types/misc/file_filter.gd create mode 100644 scripts/types/misc/local_file_adapter.gd create mode 100644 scripts/types/misc/seperator.gd create mode 100644 scripts/types/misc/simple_action.gd create mode 100644 scripts/types/misc/ui_action.gd create mode 100644 styles/suggestion_line_active.stylebox create mode 100644 styles/suggestion_line_normal.stylebox diff --git a/fonts/Vera.ttf b/fonts/Vera.ttf new file mode 100644 index 0000000000000000000000000000000000000000..58cd6b5e61eff273e920942e28041f8ddcf1e1b5 GIT binary patch literal 65932 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

B-* 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4487781bd12addbb85711513b9e84526dadc4501 GIT binary patch literal 482 zcmV<80UiEQQ$s@n000005C8!B0{{TZ0RR9fwJ-f(`T_+G0JfAxG*E{q0k75rG#;vr z2LOnywH)#f*gtvjEB8-H`dzEo62yYd^r(x1Sb!E})4B$%0lP%e2QdIN05|{&SzJoC zignbE?EYJ>Rg05rR5uwngUvBy>ZHX}<$uh7ig$w=AjVJ_YR-Sj!GFgeuSq5~N{GLx zaFSRm4xWB{F{w{7!f5Rn1c8r?-M_zF^PY?faSKc0K<>lQ8ZCdM5nYfo$ z8GX@={9cQqi;e#VpOr3wiaXoF## zKLjI52ogvD6f9s^h$u)E1`bFO0|@*G5X~PBO@Tig76vp6ItT z1;9(dT^maS&kZZ2iResM3F)GVaSSL0Wt50RYR3fKHDdNjO2LGi0w=c$x|9$$43*F! z`#)%sd*G@*;ApTM+Go!>Ho}Rme&7i-kX|DMpc1L=rW1d5W;6PQ@+QCtD{;tD0F4${ YOBayKlk3a#bXJ$CJ)r}^A5v38Ls$OLssI20 literal 0 HcmV?d00001 diff --git a/styles/suggestion_line_normal.stylebox b/styles/suggestion_line_normal.stylebox new file mode 100644 index 0000000000000000000000000000000000000000..1623b26e5389771f1363a70e947192efcbc3d446 GIT binary patch literal 421 zcmV;W0b2f2Q$s@n000005C8y<0{{Sx0RR9fwJ-f(jshJF05%gmDNsR`0{;v^;G4a6 zqSI2!J#mvH<;0Gdp0c>8F=aPGxlS~f87D8GwGS)+FaR2xw)R= zRt*<7mCH%|(*2kGUwC7K(6B?cOLTw;Hnj=`%osXOL&X1pjJh=dtv=L-N_#Qi<-!2;P0SGU zA`Z5g(PUHXWQ_y`$&-|!Q!dvboNW&kOXdF;lq)C!u2>`(uHk3WFmk*qSDRYaehER+ zb6&PhKj$rd_^8C7$?BBRB$@LzJ|JKM5L8w>c@ywT0GZ+CJ_-vl0m2nc#weFIV-ya% zaLf+9D?