diff --git a/.gitignore b/.gitignore
index 4709183..ab0abeb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,2 @@
-# Godot 4+ specific ignores
.godot/
+bin/
diff --git a/Console.tscn b/Console.tscn
new file mode 100644
index 0000000..94d11bc
--- /dev/null
+++ b/Console.tscn
@@ -0,0 +1,97 @@
+[gd_scene load_steps=10 format=3 uid="uid://btyi16dkvbly1"]
+
+[ext_resource type="Script" path="res://src/console.gd" id="1_viv5y"]
+[ext_resource type="FontFile" uid="uid://bl4vgye7bg8kf" path="res://docs/static/assets/fonts/FiraCode-Regular.ttf" id="2_ebuk0"]
+[ext_resource type="FontFile" uid="uid://b5qsng8gpvos3" path="res://docs/static/assets/fonts/FiraCode-Bold.ttf" id="3_102to"]
+[ext_resource type="FontFile" uid="uid://c5jkbqx10tj8e" path="res://docs/static/assets/fonts/FiraCode-Medium.ttf" id="4_jqfsc"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_u40jo"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_wj4aw"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ime8b"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_hjtld"]
+
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_xa0gg"]
+
+[node name="Console" type="ColorRect"]
+offset_right = 767.0
+offset_bottom = 426.0
+script = ExtResource("1_viv5y")
+
+[node name="Bar" type="ColorRect" parent="."]
+layout_mode = 0
+offset_left = 2.5
+offset_top = 2.5
+offset_right = 764.5
+offset_bottom = 27.5
+color = Color(0.839216, 0.0196078, 0.196078, 1)
+
+[node name="X1" type="Line2D" parent="Bar"]
+points = PackedVector2Array(760, 2, 739, 23)
+width = 2.0
+
+[node name="X2" type="Line2D" parent="Bar"]
+points = PackedVector2Array(760, 23, 739, 2)
+width = 2.0
+
+[node name="CloseButton" type="Button" parent="Bar"]
+layout_mode = 0
+offset_left = 737.0
+offset_right = 762.0
+offset_bottom = 25.0
+theme_override_styles/normal = SubResource("StyleBoxEmpty_u40jo")
+theme_override_styles/hover = SubResource("StyleBoxEmpty_wj4aw")
+theme_override_styles/pressed = SubResource("StyleBoxEmpty_ime8b")
+theme_override_styles/disabled = SubResource("StyleBoxEmpty_hjtld")
+theme_override_styles/focus = SubResource("StyleBoxEmpty_xa0gg")
+
+[node name="DragButton" type="Button" parent="Bar"]
+layout_mode = 0
+offset_right = 737.0
+offset_bottom = 25.0
+theme_override_styles/normal = SubResource("StyleBoxEmpty_u40jo")
+theme_override_styles/hover = SubResource("StyleBoxEmpty_wj4aw")
+theme_override_styles/pressed = SubResource("StyleBoxEmpty_ime8b")
+theme_override_styles/disabled = SubResource("StyleBoxEmpty_hjtld")
+theme_override_styles/focus = SubResource("StyleBoxEmpty_xa0gg")
+
+[node name="Shell" type="ColorRect" parent="."]
+layout_mode = 0
+offset_left = 2.5
+offset_top = 27.5
+offset_right = 764.5
+offset_bottom = 423.5
+color = Color(0, 0, 0, 1)
+
+[node name="Output" type="RichTextLabel" parent="Shell"]
+layout_mode = 0
+offset_right = 762.0
+offset_bottom = 361.0
+theme_override_fonts/normal_font = ExtResource("2_ebuk0")
+theme_override_fonts/bold_font = ExtResource("3_102to")
+theme_override_fonts/italics_font = ExtResource("2_ebuk0")
+theme_override_fonts/bold_italics_font = ExtResource("3_102to")
+theme_override_fonts/mono_font = ExtResource("2_ebuk0")
+theme_override_font_sizes/normal_font_size = 18
+theme_override_font_sizes/bold_font_size = 18
+theme_override_font_sizes/italics_font_size = 18
+theme_override_font_sizes/bold_italics_font_size = 18
+theme_override_font_sizes/mono_font_size = 18
+bbcode_enabled = true
+scroll_following = true
+
+[node name="Input" type="TextEdit" parent="Shell"]
+layout_mode = 0
+offset_top = 361.0
+offset_right = 762.0
+offset_bottom = 396.0
+theme_override_colors/background_color = Color(0, 0, 0, 1)
+theme_override_colors/font_color = Color(1, 1, 1, 1)
+theme_override_fonts/font = ExtResource("4_jqfsc")
+theme_override_font_sizes/font_size = 18
+placeholder_text = "Enter a command here"
+
+[connection signal="pressed" from="Bar/CloseButton" to="." method="close_console"]
+[connection signal="text_changed" from="Shell/Input" to="." method="input_changed"]
diff --git a/docs/docs/reference/console.md b/docs/docs/reference/console.md
new file mode 100644
index 0000000..d5e3b7c
--- /dev/null
+++ b/docs/docs/reference/console.md
@@ -0,0 +1,60 @@
+---
+sidebar_position: 3
+---
+
+# Debugging console
+Presencode includes a debugging console that you can use to debug your presentation code, Presencode itself... or to have fun destroying Presencode from the inside.
+
+## Accessing the debug console
+To make the debug console visible, press the `^` key on your keyboard.
+To get started, type `help` into the input box.
+
+## Commands
+You can use these commands inside the debugging console:
+
+
+ Command |
+ Description |
+ Arguments |
+
+
+ clear |
+ Clears the console output |
+ |
+
+
+ exit |
+ Starts a fresh console session |
+ |
+
+
+ shutdown |
+ Shuts Presencode down |
+ [exitcode: int = 0] |
+
+
+ help |
+ Displays information about certain topics |
+ [topic: String = "INDEX"] |
+
+
+ config |
+ Reads and writes to the in-memory configuration |
+ [get <key: String>|set <key: String> <value: Variant>|list] |
+
+
+ pmana |
+ Controls the Presentation Manager |
+ [register <version: int> <slides: int> <animations: bool> <quit_last_slide: bool> <controller: NodePath<String>>|unregister|change_slide <slide: int> [no_animations: bool = false]|clear_viewport|hide_log|show_log] |
+
+
+ preader |
+ Controls the Presentation Reader |
+ <get <topic|authors|ratio|resolution>> |
+
+
+ arbitrary |
+ Execute arbitrary GDScript expressions |
+ <gdscript expression> |
+
+
\ No newline at end of file
diff --git a/example/test.gd b/example/test.gd
index d676101..e5970f3 100644
--- a/example/test.gd
+++ b/example/test.gd
@@ -78,6 +78,7 @@ func presentation_start(viewport_: Control) -> void:
## without calling this function.
func presentation_end() -> void:
logger.diag("presentation_end() called")
+ queue_free()
# Change the current slide to another one
func change_slide(new_slide: int) -> void:
diff --git a/project.godot b/project.godot
index 1766963..1b80571 100644
--- a/project.godot
+++ b/project.godot
@@ -70,6 +70,12 @@ content_scale_switch={
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":71,"physical_keycode":0,"key_label":0,"unicode":103,"echo":false,"script":null)
]
}
+console={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":0,"key_label":96,"unicode":0,"echo":false,"script":null)
+, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":96,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
+]
+}
[rendering]
diff --git a/src/clickoverlay.gd b/src/clickoverlay.gd
index 0ac901d..47d6ba4 100644
--- a/src/clickoverlay.gd
+++ b/src/clickoverlay.gd
@@ -46,7 +46,5 @@ func clicked() -> void:
pmana.change_slide(pmana.current_slide+1, true)
func _process(_delta: float) -> void:
- # Move button to top
- get_tree().root.move_child(self, get_tree().root.get_child_count(true))
# Scale clickoverlay to window size
size = DisplayServer.window_get_size()
diff --git a/src/console.gd b/src/console.gd
new file mode 100644
index 0000000..b21f435
--- /dev/null
+++ b/src/console.gd
@@ -0,0 +1,457 @@
+##############################################################################
+### PRESENCODE SOURCE FILE ###
+### Copyright (c) 2024 JeremyStarTM & Contributors ###
+### Licensed under the GNU General Public License v3 ###
+### ###
+### This program is free software: you can redistribute it and/or modify ###
+### it under the terms of the GNU General Public License as published by ###
+### the Free Software Foundation, either version 3 of the License, or ###
+### (at your option) any later version. ###
+### ###
+### This program is distributed in the hope that it will be useful, ###
+### but WITHOUT ANY WARRANTY; without even the implied warranty of ###
+### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ###
+### GNU General Public License for more details. ###
+### ###
+### You should have received a copy of the GNU General Public License ###
+### along with this program. If not, see . ###
+##############################################################################
+### src/console.gd (Debug console) ###
+### ###
+### This source file controls the debug console, which can be enabled by ###
+### pressing the ^ key. ###
+##############################################################################
+extends ColorRect
+
+# Enums
+enum BooleanState {
+ TRUE,
+ FALSE,
+ INVALID
+}
+
+# Nodes
+@onready var input: TextEdit = get_node("Shell/Input")
+@onready var output: RichTextLabel = get_node("Shell/Output")
+var expressionscript: Node = Node.new()
+
+# Console information
+@onready var info: ConsoleInfo = ConsoleInfo.new()
+
+# States
+var dragging: bool = false
+var drag_area: Vector2 = Vector2(0, 0)
+var cursor_origin: Vector2 = Vector2(0, 0)
+
+func _ready() -> void:
+ logger.info("Initializing debug console")
+ position = Vector2(30, 30)
+ calculate_drag_area()
+ # Setup window dragging
+ $Bar/DragButton.connect("button_down", func():
+ dragging = true
+ cursor_origin = get_tree().root.get_viewport().get_mouse_position()
+ )
+ $Bar/DragButton.connect("button_up", func():
+ dragging = false
+ cursor_origin = Vector2(0,0)
+ )
+ expressionscript.name = "ExpressionScript"
+ expressionscript.set_script(ResourceLoader.load("res://src/console_expressionscript.gd"))
+ expressionscript.logger = get_node("/root/logger")
+ expressionscript.misc = get_node("/root/misc")
+ expressionscript.preader = get_node("/root/preader")
+ expressionscript.pmana = get_node("/root/pmana")
+ expressionscript.console = self
+ expressionscript.loader = get_node("/root/Presencode")
+ add_child(expressionscript)
+ await process_command(PackedStringArray(["exit"]))
+ logger.info("Debug console initialized")
+
+func _process(_delta: float) -> void:
+ # Move to top
+ get_tree().root.move_child(self, get_tree().root.get_child_count(true))
+ # Visibility toggle key
+ if Input.is_action_just_pressed("console"):
+ logger.info("Toggling console visibility")
+ visible = !visible
+ if dragging:
+ # Get new cursor position
+ var cursor_position = get_tree().root.get_viewport().get_mouse_position()
+ # If nothing changed, don't do anything
+ if cursor_origin != cursor_position:
+ # Calculate cursor offset
+ var cursor_offset: Vector2 = Vector2(0, 0)
+ cursor_offset.x = cursor_origin.x-cursor_position.x
+ cursor_offset.y = cursor_origin.y-cursor_position.y
+ # Change console position
+ position.x = position.x-cursor_offset.x
+ position.y = position.y-cursor_offset.y
+ # Prevent console from going out of scope
+ var position_new: Vector2 = check_violation()
+ if position != position_new:
+ position = position_new
+ # Set new cursor_origin
+ cursor_origin = cursor_position
+
+# Helper function, prevents console from going out of scope
+func check_violation() -> Vector2:
+ var new_position: Vector2 = position
+ if position.x <= 0:
+ new_position.x = 0
+ elif position.x >= drag_area.x:
+ new_position.x = drag_area.x
+ if position.y <= 0:
+ new_position.y = 0
+ elif position.y >= drag_area.y:
+ new_position.y = drag_area.y
+ return new_position
+
+# Calculates a new drag_area
+func calculate_drag_area(area: Vector2i = DisplayServer.window_get_size()) -> void:
+ drag_area.x = area.x-size.x
+ drag_area.y = area.y-size.y
+
+# Close console on close button press
+func close_console() -> void:
+ logger.info("Closing console")
+ visible = false
+
+# Input changed
+func input_changed() -> void:
+ if input.text.contains("\n"):
+ input.text = input.text.replace("\n", "")
+ var input2: String = input.text
+ input.text = ""
+ input.editable = false
+ await process_command(input2.split(" ", false))
+ input.editable = true
+
+# Process commands
+func process_command(command: PackedStringArray) -> void:
+ logger.diag("Processing command [" + " ".join(command) + "]")
+ await get_tree().process_frame
+ append_output("[color=gray]$ " + " ".join(command) + "[color=white]")
+ if command.size() == 0: return
+ match(command[0]):
+ "clear":
+ match(command.size()):
+ _: append_output(info.get_error_string(info.ConsoleError.TOO_MANY_ARGUMENTS))
+ output.text = ""
+ "exit":
+ match(command.size()):
+ _: append_output(info.get_error_string(info.ConsoleError.TOO_MANY_ARGUMENTS))
+ await process_command(PackedStringArray(["clear"]))
+ append_output("""[color=#d60532]Welcome to the Presencode debug console!
+
+To get started, enter \"help\". To close the console, press the X button.[color=white]""")
+ "shutdown":
+ append_output("Shutting down...")
+ var exitcode: int = 0
+ match(command.size()):
+ 1: exitcode = 0
+ 2: exitcode = int(command[1])
+ _: append_output(info.get_error_string(info.ConsoleError.TOO_MANY_ARGUMENTS))
+ pmana.shutdown(exitcode)
+ "help":
+ match(command.size()):
+ 1: append_output(info.get_help_topic(info.HelpTopic.INDEX))
+ 2:
+ match(command[1]):
+ "index": append_output(info.get_help_topic(info.HelpTopic.INDEX))
+ "clear": append_output(info.get_help_topic(info.HelpTopic.CLEAR))
+ "exit": append_output(info.get_help_topic(info.HelpTopic.EXIT))
+ "shutdown": append_output(info.get_help_topic(info.HelpTopic.SHUTDOWN))
+ "help": append_output(info.get_help_topic(info.HelpTopic.HELP))
+ "config": append_output(info.get_help_topic(info.HelpTopic.CONFIG))
+ "pmana": append_output(info.get_help_topic(info.HelpTopic.PMANA))
+ "preader": append_output(info.get_help_topic(info.HelpTopic.PREADER))
+ "arbitrary": append_output(info.get_help_topic(info.HelpTopic.ARBITRARY))
+ _: append_output(info.get_error_string(info.ConsoleError.INVALID_HELP_TOPIC))
+ _: append_output(info.get_error_string(info.ConsoleError.TOO_MANY_ARGUMENTS))
+ "config":
+ match(command.size()):
+ 1: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS))
+ _:
+ match(command[1]):
+ "list":
+ match(command.size()):
+ 2:
+ append_output("""These configuration keys can be used:
+Key | Type | Description
+LOGGER_ENABLED | bool | Toggles the logger
+LOGGER_DIAGNOSTIC | bool | Toggles diagnostic log output
+LOGGER_COLORED | bool | Toggles colored log output
+LOGGER_HARDFAIL | bool | Toggles hardfailing errors (don't set this to false)
+LOGGER_LOGSTRING | String | The logging template, variables: runtime, time, file, function, line, color, type, message
+PMANA_ALLOW_FULLSCREEN | bool | Toggles window mode switching
+MISC_SHUTDOWN_INVISIBLE | bool | Toggles if the window should be made invisible on shutdown, displays white texture if disabled""")
+ _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_MANY_ARGUMENTS))
+ "set":
+ match(command.size()):
+ 2: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS))
+ 3: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS))
+ _:
+ match(command[2]):
+ "LOGGER_ENABLED":
+ match(get_boolean(" ".join(get_last(command, 2)))):
+ BooleanState.TRUE:
+ logger.config_enabled = true
+ append_output("Set \"" + str(command[2]) + "\" to [b]true[/b].")
+ BooleanState.FALSE:
+ logger.config_enabled = false
+ append_output("Set \"" + str(command[2]) + "\" to [b]false[/b]")
+ _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "bool"}))
+ "LOGGER_DIAGNOSTIC":
+ match(get_boolean(" ".join(get_last(command, 2)))):
+ BooleanState.TRUE:
+ logger.config_diagnostic = true
+ append_output("Set \"" + str(command[2]) + "\" to [b]true[/b]")
+ BooleanState.FALSE:
+ logger.config_diagnostic = false
+ append_output("Set \"" + str(command[2]) + "\" to [b]false[/b]")
+ _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "bool"}))
+ "LOGGER_COLORED":
+ match(get_boolean(" ".join(get_last(command, 2)))):
+ BooleanState.TRUE:
+ logger.config_colored = true
+ append_output("Set \"" + str(command[2]) + "\" to [b]true[/b]")
+ BooleanState.FALSE:
+ logger.config_colored = false
+ append_output("Set \"" + str(command[2]) + "\" to [b]false[/b]")
+ _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "bool"}))
+ "LOGGER_HARDFAIL":
+ match(get_boolean(" ".join(get_last(command, 2)))):
+ BooleanState.TRUE:
+ logger.config_hardfail = true
+ append_output("Set \"" + str(command[2]) + "\" to [b]true[/b]")
+ BooleanState.FALSE:
+ logger.config_hardfail = false
+ append_output("Set \"" + str(command[2]) + "\" to [b]false[/b]")
+ _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "bool"}))
+ "LOGGER_LOGSTRING":
+ logger.config_logstring = " ".join(PackedStringArray(get_last(command, 2)))
+ append_output("Set \"" + str(command[2]) + "\" to [b]\"" + logger.config_logstring + "\"[/b]")
+ "PMANA_ALLOW_FULLSCREEN":
+ match(get_boolean(" ".join(get_last(command, 2)))):
+ BooleanState.TRUE:
+ pmana.config_allow_fullscreen = true
+ append_output("Set \"" + str(command[2]) + "\" to [b]true[/b]")
+ BooleanState.FALSE:
+ pmana.config_allow_fullscreen = false
+ append_output("Set \"" + str(command[2]) + "\" to [b]false[/b]")
+ _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "bool"}))
+ "MISC_SHUTDOWN_INVISIBLE":
+ match(get_boolean(" ".join(get_last(command, 2)))):
+ BooleanState.TRUE:
+ misc.config_shutdown_invisible = true
+ append_output("Set \"" + str(command[2]) + "\" to [b]true[/b]")
+ BooleanState.FALSE:
+ misc.config_shutdown_invisible = false
+ append_output("Set \"" + str(command[2]) + "\" to [b]false[/b]")
+ _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "bool"}))
+ _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_CONFIG_KEY, {"key": command[2]}))
+ "get":
+ match(command.size()):
+ 2: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS))
+ 3:
+ match(command[2]):
+ "LOGGER_ENABLED": append_output("[b]" + command[2] + "[/b] is set to [b]" + str(logger.config_enabled) + "[/b]")
+ "LOGGER_DIAGNOSTIC": append_output("[b]" + command[2] + "[/b] is set to [b]" + str(logger.config_diagnostic) + "[/b]")
+ "LOGGER_COLORED": append_output("[b]" + command[2] + "[/b] is set to [b]" + str(logger.config_colored) + "[/b]")
+ "LOGGER_HARDFAIL": append_output("[b]" + command[2] + "[/b] is set to [b]" + str(logger.config_hardfail) + "[/b]")
+ "LOGGER_LOGSTRING": append_output("\"" + command[2] + "[/b] is set to [b]\"" + logger.config_logstring + "\"[/b]")
+ "PMANA_ALLOW_FULLSCREEN": append_output("[b]" + command[2] + "[/b] is set to [b]" + str(pmana.config_allow_fullscreen) + "[/b]")
+ "MISC_SHUTDOWN_INVISIBLE": append_output("[b]" + command[2] + "[/b] is set to [b]" + str(misc.config_shutdown_invisible) + "[/b]")
+ _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_CONFIG_KEY, {"key": command[2]}))
+ _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_MANY_ARGUMENTS))
+ _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_ARGUMENT))
+ "pmana":
+ match(command.size()):
+ 1: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS))
+ _:
+ match(command[1]):
+ "register":
+ match(command.size()):
+ 1: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS))
+ 2: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS))
+ 3: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS))
+ 4: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS))
+ 5: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS))
+ 6: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS))
+ 7:
+ var version: Vector2 = get_int_direct(command[2])
+ var slides: Vector2 = get_int_direct(command[3])
+ var animations: BooleanState = get_boolean(command[4])
+ var animations_bool: bool = false
+ var quit_last_slide: BooleanState = get_boolean(command[5])
+ var quit_last_slide_bool: bool = false
+ var controller: NodePath = NodePath(command[6])
+ if version.x != 0:
+ append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "int"}))
+ return
+ if slides.x != 0:
+ append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "int"}))
+ return
+ match(animations):
+ BooleanState.TRUE: animations_bool = true
+ BooleanState.FALSE: animations_bool = false
+ _:
+ append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "bool"}))
+ return
+ match(quit_last_slide):
+ BooleanState.TRUE: quit_last_slide_bool = true
+ BooleanState.FALSE: quit_last_slide_bool = false
+ _:
+ append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "bool"}))
+ return
+ if get_node(controller) == null or typeof(get_node(controller)) != TYPE_OBJECT:
+ append_output(info.get_error_string(ConsoleInfo.ConsoleError.NOT_AN_OBJECT))
+ return
+ if get_node(controller).get_script() == null:
+ append_output(info.get_error_string(ConsoleInfo.ConsoleError.NO_SCRIPT_ATTACHED))
+ return
+ @warning_ignore("narrowing_conversion")
+ pmana.register(version.y, slides.y, animations_bool, quit_last_slide_bool, controller)
+ append_output("A new presentation controller has been registered. Look into the Presencode log output if nothing happens.")
+ _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_MANY_ARGUMENTS))
+ "unregister":
+ if !pmana.registered: append_output(info.get_error_string(ConsoleInfo.ConsoleError.NO_CONTROLLER_REGISTERED))
+ else:
+ await pmana.unregister()
+ append_output("The current presentation controller has been unregistered.")
+ "change_slide":
+ match(command.size()):
+ 3:
+ if !pmana.registered: append_output(info.get_error_string(ConsoleInfo.ConsoleError.NO_CONTROLLER_REGISTERED))
+ else:
+ var slide: Vector2i = get_int_direct(command[2])
+ match(slide.x):
+ 0:
+ pmana.change_slide(slide.y)
+ append_output("Switched to slide [b]" + str(slide.y) + "[/b]")
+ 1: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "int"}))
+ _: append_output(info.generate_internal_error("Invalid get_int() status number"))
+ 4:
+ if !pmana.registered: append_output(info.get_error_string(ConsoleInfo.ConsoleError.NO_CONTROLLER_REGISTERED))
+ else:
+ var slide: Vector2i = get_int_direct(command[2])
+ match(slide.x):
+ 0:
+ var no_animations: BooleanState = get_boolean(command[3])
+ match(no_animations):
+ BooleanState.TRUE:
+ pmana.change_slide(slide.y, true)
+ append_output("Switched to slide [b]" + str(slide.y) + "[/b] without animations")
+ BooleanState.FALSE:
+ pmana.change_slide(slide.y, false)
+ append_output("Switched to slide [b]" + str(slide.y) + "[/b]")
+ BooleanState.INVALID:
+ append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "bool"}))
+ 1: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_TYPE, {"expected_type": "int"}))
+ _: append_output(info.generate_internal_error("Invalid get_int() status number"))
+ _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_MANY_ARGUMENTS))
+ "clear_viewport":
+ if !pmana.registered: append_output(info.get_error_string(ConsoleInfo.ConsoleError.NO_CONTROLLER_REGISTERED))
+ else:
+ pmana.clear_viewport()
+ append_output("The viewport has been cleared.")
+ "hide_log":
+ if !pmana.registered: append_output(info.get_error_string(ConsoleInfo.ConsoleError.NO_CONTROLLER_REGISTERED))
+ else:
+ pmana.hide_log()
+ append_output("The log output is now [b]invisible[/b].")
+ "show_log":
+ if !pmana.registered: append_output(info.get_error_string(ConsoleInfo.ConsoleError.NO_CONTROLLER_REGISTERED))
+ else:
+ pmana.show_log()
+ append_output("The log output is now [b]visible[/b].")
+ _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_ARGUMENT))
+ "preader":
+ match(command.size()):
+ 1: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS))
+ _:
+ match(command[1]):
+ "get":
+ match(command.size()):
+ 2: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS))
+ 3:
+ match(command[2]):
+ "topic":
+ if preader.get_topic() == "": append_output(info.get_error_string(ConsoleInfo.ConsoleError.NO_PRESENTATION_OPEN))
+ else: append_output("The presentation topic is [b]" + preader.get_topic() + "[/b]")
+ "authors":
+ if preader.get_authors() == "": append_output(info.get_error_string(ConsoleInfo.ConsoleError.NO_PRESENTATION_OPEN))
+ else: append_output("The presentation author is/authors are: [b]" + preader.get_authors() + "[/b]")
+ "ratio":
+ if preader.get_ratio() == "": append_output(info.get_error_string(ConsoleInfo.ConsoleError.NO_PRESENTATION_OPEN))
+ else: append_output("The presentation ratio is [b]" + preader.get_ratio() + "[/b]")
+ "resolution":
+ if preader.get_ratio_resolution() == Vector2i(0, 0): append_output(info.get_error_string(ConsoleInfo.ConsoleError.NO_PRESENTATION_OPEN))
+ else:
+ var resolution: Vector2i = preader.get_ratio_resolution()
+ append_output("The presentation resolution is [b]" + str(resolution.x) + "x" + str(resolution.y) + "[/b].")
+ _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_ARGUMENT))
+ _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_MANY_ARGUMENTS))
+ _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_ARGUMENT))
+ "arbitrary":
+ match(command.size()):
+ 1: append_output(info.get_error_string(ConsoleInfo.ConsoleError.TOO_FEW_ARGUMENTS))
+ _:
+ var expression_raw: String = " ".join(PackedStringArray(get_last(command, 0)))
+ var expression: Expression = Expression.new()
+ if expression.parse(expression_raw) != Error.OK:
+ append_output(info.get_error_string(ConsoleInfo.ConsoleError.EXPRESSION_PARSING_FAILED, {"error": expression.get_error_text()}))
+ else:
+ var returned = expression.execute([], expressionscript)
+ if expression.has_execute_failed():
+ append_output(info.get_error_string(ConsoleInfo.ConsoleError.EXPRESSION_EXECUTION_FAILED, {"error": expression.get_error_text(), "returned": returned}))
+ else:
+ append_output("""Executed arbitrary expression successfully.
+Returned: """ + str(returned))
+ _: append_output(info.get_error_string(ConsoleInfo.ConsoleError.INVALID_COMMAND))
+
+# Helper function. I'm bad at explaining and naming things, just look at the code... please.
+func get_last(array: Array, n: int) -> Array:
+ # Makes it easier
+ n = n+1
+ # Create new array
+ var new_array: Array = []
+ if n >= array.size():
+ logger.error("n is bigger than array.size()")
+ return []
+ for value in array:
+ if n == 0: # Append to new_array
+ new_array.append(value)
+ else: # Count down
+ n = n-1
+ return new_array
+
+# Helper function. Tries to get a bool from an Array.
+func get_boolean(string: String) -> BooleanState:
+ match(string):
+ "true": return BooleanState.TRUE
+ "false": return BooleanState.FALSE
+ _: return BooleanState.INVALID
+
+# Helper function. Tries to get a int from an Array.
+## Returns Vector2i(0, NUMBER) if successful
+## Returns Vector2i(1, 0) if not successful
+func get_int(array: Array) -> Vector2i:
+ for value in array:
+ if str(value).is_valid_int(): return Vector2i(0, int(value))
+ return Vector2i(1, 0)
+
+# Helper function. Tries to get a int from a String.
+## Returns Vector2i(0, NUMBER) if successful
+## Returns Vector2i(1, 0) if not successful
+func get_int_direct(string: String) -> Vector2i:
+ if string.is_valid_int(): return Vector2i(0, int(string))
+ else: return Vector2i(1, 0)
+
+# Helper function. Appends text to output.text
+func append_output(text: String, newline: bool = true) -> void:
+ if newline:
+ output.text = output.text + text + "\n"
+ else:
+ output.text = output.text + text
diff --git a/src/console_expressionscript.gd b/src/console_expressionscript.gd
new file mode 100644
index 0000000..cb640b8
--- /dev/null
+++ b/src/console_expressionscript.gd
@@ -0,0 +1,33 @@
+##############################################################################
+### PRESENCODE SOURCE FILE ###
+### Copyright (c) 2024 JeremyStarTM & Contributors ###
+### Licensed under the GNU General Public License v3 ###
+### ###
+### This program is free software: you can redistribute it and/or modify ###
+### it under the terms of the GNU General Public License as published by ###
+### the Free Software Foundation, either version 3 of the License, or ###
+### (at your option) any later version. ###
+### ###
+### This program is distributed in the hope that it will be useful, ###
+### but WITHOUT ANY WARRANTY; without even the implied warranty of ###
+### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ###
+### GNU General Public License for more details. ###
+### ###
+### You should have received a copy of the GNU General Public License ###
+### along with this program. If not, see . ###
+##############################################################################
+### src/console_expressionscript.gd (Debug console expression playground) ###
+### ###
+### This file will be used as a script for executing arbitrary expressions ###
+### by the debugging console. ###
+##############################################################################
+extends Node
+
+# @GlobalScope
+var logger: Node = null
+var misc: Node = null
+var preader: Node = null
+var pmana: Node = null
+# SceneTree
+var console: Node = null
+var loader: Node = null
diff --git a/src/console_info.gd b/src/console_info.gd
new file mode 100644
index 0000000..9c15e15
--- /dev/null
+++ b/src/console_info.gd
@@ -0,0 +1,156 @@
+##############################################################################
+### PRESENCODE SOURCE FILE ###
+### Copyright (c) 2024 JeremyStarTM & Contributors ###
+### Licensed under the GNU General Public License v3 ###
+### ###
+### This program is free software: you can redistribute it and/or modify ###
+### it under the terms of the GNU General Public License as published by ###
+### the Free Software Foundation, either version 3 of the License, or ###
+### (at your option) any later version. ###
+### ###
+### This program is distributed in the hope that it will be useful, ###
+### but WITHOUT ANY WARRANTY; without even the implied warranty of ###
+### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ###
+### GNU General Public License for more details. ###
+### ###
+### You should have received a copy of the GNU General Public License ###
+### along with this program. If not, see . ###
+##############################################################################
+### src/console_info.gd (Debug console information) ###
+### ###
+### This source file holds console information such as help topics or ###
+### console errors. ###
+##############################################################################
+extends Node
+class_name ConsoleInfo
+
+enum ConsoleError {
+ OK,
+ UNIMPLEMENTED,
+ INVALID_COMMAND,
+ TOO_MANY_ARGUMENTS,
+ TOO_FEW_ARGUMENTS,
+ INVALID_ARGUMENT,
+ INVALID_TYPE,
+ NOT_AN_OBJECT,
+ NO_SCRIPT_ATTACHED,
+ INVALID_HELP_TOPIC,
+ INVALID_CONFIG_KEY,
+ NO_PRESENTATION_OPEN,
+ CONTROLLER_ALREADY_REGISTERED,
+ NO_CONTROLLER_REGISTERED,
+ EXPRESSION_PARSING_FAILED,
+ EXPRESSION_EXECUTION_FAILED
+}
+enum HelpTopic {
+ INDEX,
+ CLEAR,
+ EXIT,
+ SHUTDOWN,
+ HELP,
+ CONFIG,
+ PMANA,
+ PREADER,
+ ARBITRARY
+}
+
+var error_color: String = "[color=red]"
+var internal_error_color: String = "[b][color=red]"
+
+func _init() -> void:
+ logger.diag("ConsoleInfo initialized")
+
+func generate_internal_error(error: String) -> String:
+ var origin = misc.get_origin()
+ logger.warn("Debug console (" + origin["file"] + ":" + origin["function"] + ":" + str(origin["line"]) + ") experienced an internal error: " + error)
+ return internal_error_color + "INTERNAL ERROR (" + error + ") [" + origin["file"] + ":" + origin["function"] + ":" + str(origin["line"]) + "]"
+
+func get_error_string(error: ConsoleError, context: Dictionary = {}) -> String:
+ match(error):
+ ConsoleError.OK:
+ return generate_internal_error("ConsoleError.OK is not a valid error")
+ ConsoleError.UNIMPLEMENTED:
+ return error_color + "Function unimplemented, aborting."
+ ConsoleError.INVALID_COMMAND:
+ return error_color + "Invalid command. Execute \"help\" for a list of all available commands."
+ ConsoleError.TOO_MANY_ARGUMENTS:
+ return error_color + "Too many arguments."
+ ConsoleError.TOO_FEW_ARGUMENTS:
+ return error_color + "Too few arguments."
+ ConsoleError.INVALID_ARGUMENT:
+ return error_color + "Invalid argument."
+ ConsoleError.INVALID_TYPE:
+ return error_color + "Invalid type. Command expected type " + str(context["expected_type"]) + "."
+ ConsoleError.NOT_AN_OBJECT:
+ return error_color + "Invalid type. Command expected a valid NodePath."
+ ConsoleError.NO_SCRIPT_ATTACHED:
+ return error_color + "Invalid object. Command expected a script-attached object."
+ ConsoleError.INVALID_HELP_TOPIC:
+ return error_color + "Invalid help topic."
+ ConsoleError.INVALID_CONFIG_KEY:
+ return error_color + "Invalid config key \"" + str(context["key"]) + "\". Execute \"config list\" for a list of all config keys."
+ ConsoleError.NO_PRESENTATION_OPEN:
+ return error_color + "No presentation is currently open."
+ ConsoleError.CONTROLLER_ALREADY_REGISTERED:
+ return error_color + "A presentation controller has been registered already."
+ ConsoleError.NO_CONTROLLER_REGISTERED:
+ return error_color + "No presentation controller has been registered yet."
+ ConsoleError.EXPRESSION_PARSING_FAILED:
+ return error_color + """Could not parse arbitrary expression successfully.
+Error: """ + str(context["error"])
+ ConsoleError.EXPRESSION_EXECUTION_FAILED:
+ return error_color + """Could not execute arbitrary expression successfully.
+Error: """ + str(context["error"]) + """
+Returned: """ + str(context["returned"])
+ _:
+ return generate_internal_error("Invalid ConsoleError \"" + str(error) + "\", context: " + str(context))
+
+func get_help_topic(topic: HelpTopic) -> String:
+ match(topic):
+ HelpTopic.INDEX:
+ return """Command | Description
+clear | Clears the console output
+exit | Starts a fresh session
+shutdown | Shuts Presencode down
+help | Displays information about certain topics
+config | Reads and writes to the in-memory configuration
+pmana | Controls the Presentation Manager
+preader | Controls the Presentation Reader
+arbitrary | Execute arbitrary GDScript expressions
+
+To view more information about one topic, type \"help [topic]\"."""
+ HelpTopic.CLEAR:
+ return """clear
+
+Clears the console output."""
+ HelpTopic.EXIT:
+ return """exit
+
+Clears the console output and starts a fresh session."""
+ HelpTopic.SHUTDOWN:
+ return """shutdown [exitcode: int = 0]
+
+Shuts Presencode down, accepts a exitcode ranging from 0-255."""
+ HelpTopic.HELP:
+ return """help [topic: String = "INDEX"]
+
+Displays useful information about commands."""
+ HelpTopic.CONFIG:
+ return """config [get |set |list]
+
+Returns, lists or overwrites Presencode's configuration. Lives in memory and cannot be saved to persistent storage."""
+ HelpTopic.PMANA:
+ return """pmana [register >|unregister|change_slide [no_animations: bool = false]|clear_viewport|hide_log|show_log]
+
+Calls functions belonging to the Presentation Manager."""
+ HelpTopic.PREADER:
+ return """preader >
+
+Calls functions belonging to the Presentation Reader"""
+ HelpTopic.ARBITRARY:
+ return """arbitrary
+
+Executes arbitrary GDScript expressions.
+-> EXPERIMENTAL"""
+ _:
+ return generate_internal_error("Invalid HelpTopic \"" + str(topic) + "\"")
diff --git a/src/loader.gd b/src/loader.gd
index 9c53531..9900fc2 100644
--- a/src/loader.gd
+++ b/src/loader.gd
@@ -124,6 +124,9 @@ ONLY VIEW PRESENCODE PRESENTATIONS IF YOU TRUST THE
###################################################
""")
await get_tree().create_timer(5).timeout
+ logger.info("Injecting console")
+ var console: Control = ResourceLoader.load("res://Console.tscn").instantiate()
+ get_tree().root.add_child.call_deferred(console)
if config_slow_init: await get_tree().create_timer(randf_range(0.1, 0.15)).timeout
# Open presentation archive/directory
var path: String = "".join(OS.get_cmdline_user_args())
@@ -142,6 +145,7 @@ ONLY VIEW PRESENCODE PRESENTATIONS IF YOU TRUST THE
preader.read_manifest()
preader.read_entrypoint()
# Update window properties
+ console.calculate_drag_area(preader.get_ratio_resolution())
DisplayServer.window_set_title("Presencode ยป Presenting \"" + preader.get_topic() + "\" by \"" + preader.get_authors() + "\"")
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
DisplayServer.window_set_size(preader.get_ratio_resolution())
diff --git a/src/misc.gd b/src/misc.gd
index ac8e15f..a4550f6 100644
--- a/src/misc.gd
+++ b/src/misc.gd
@@ -53,7 +53,7 @@ func get_origin(n: int = 0) -> Dictionary:
# Shutdown Presencode safely
func shutdown(exitcode: int = 0) -> void:
- logger.info("Shutting down")
+ logger.info("Shutting down (code " + str(exitcode) + ")")
get_tree().paused = true
# Display white texture
var npr: NinePatchRect = NinePatchRect.new()
diff --git a/src/pmana.gd b/src/pmana.gd
index 7131d29..0dd92ca 100644
--- a/src/pmana.gd
+++ b/src/pmana.gd
@@ -51,6 +51,9 @@ func register(version: int, slides_: int, animations_: bool, quit_last_slide_: b
# Check entrypoint version
if version != entrypoint_version:
await logger.error("Presentation entrypoint version does not match Presencode's entrypoint version")
+ # Check controller nodepath
+ if get_node(controller_) == null:
+ await logger.error("The presentation controller could not be located.")
# Set controller information
slides = slides_
animations = animations_
@@ -75,17 +78,18 @@ func register(version: int, slides_: int, animations_: bool, quit_last_slide_: b
for function in lacking_functions:
logger.warn("- " + function)
logger.warn("Consult the documentation or take a look at the example presentation if you need help.")
- # Create clickoverlay
- clickoverlay = Button.new()
- clickoverlay.name = "ClickOverlay"
- clickoverlay.set_script(ResourceLoader.load("res://src/clickoverlay.gd"))
- get_tree().root.add_child(clickoverlay)
+ await misc.shutdown(1)
# Create presentation viewport
viewport = Control.new()
viewport.name = "Viewport"
viewport.size = preader.get_ratio_resolution()
viewport.position = Vector2(0, 0)
get_node("/root/Presencode").add_child(viewport)
+ # Create clickoverlay
+ clickoverlay = Button.new()
+ clickoverlay.name = "ClickOverlay"
+ clickoverlay.set_script(ResourceLoader.load("res://src/clickoverlay.gd"))
+ get_node("/root/Presencode").add_child(clickoverlay)
# Did you know that every big galaxy contains a massive black hole in it's center?
registered = true
# Invoke presentation_start()
@@ -103,10 +107,11 @@ func unregister() -> void:
animation_active = false
config_allow_fullscreen = true
# Remove clickoverlay
- get_tree().root.remove_child(clickoverlay)
+ get_node("/root/Presencode/").remove_child(clickoverlay)
clickoverlay = null
get_node("/root/Presencode/").remove_child(viewport)
viewport = null
+ await get_tree().process_frame
# Change slide
func change_slide(slide: int, no_animations: bool = false) -> void:
@@ -126,8 +131,7 @@ func change_slide(slide: int, no_animations: bool = false) -> void:
# Quit after last slide?
if quit_last_slide:
logger.info("Ending presentation")
- await controller.presentation_end()
- await misc.shutdown(0)
+ await shutdown()
return
else:
logger.warn("Over max slides by " + str(slide-slides))
@@ -138,8 +142,7 @@ func change_slide(slide: int, no_animations: bool = false) -> void:
return
else: # End presentation (slide is over max slides by two or more)
logger.info("Ending presentation")
- await controller.presentation_end()
- await misc.shutdown(0)
+ await shutdown()
return
# Slide is not over max slides, continue slide change
# Play switch away animation
@@ -186,6 +189,12 @@ func show_log() -> void:
return
get_node("/root/Presencode/Log").modulate = Color8(255, 255, 255, 255)
+# Helper function. Shutdown Presencode
+func shutdown(exitcode: int = 0) -> void:
+ logger.diag("Shutting down Presencode from Presentation Manager")
+ if registered: await controller.presentation_end()
+ await misc.shutdown(exitcode)
+
# Runs every frame (duh)
func _process(_delta: float) -> void:
# Fullscreen key combo
diff --git a/src/preader.gd b/src/preader.gd
index e485ae6..ebdfa5d 100644
--- a/src/preader.gd
+++ b/src/preader.gd
@@ -131,6 +131,7 @@ func read_entrypoint() -> misc.Error:
# Check if manifest is loaded in memory
if manifest == {}:
logger.error("Manifest not loaded in memory, please call read_manifest() first")
+ return misc.Error.PREADER_NO_MANIFEST
# Create empty script variable
var script: Script
if directorypath == "":
@@ -169,21 +170,50 @@ func get_entrypoint() -> Node:
# Return presentation topic
func get_topic() -> String:
+ if !is_open:
+ logger.error("No presentation is currently opened")
+ return ""
+ # Check if manifest is loaded in memory
+ if manifest == {}:
+ logger.error("Manifest not loaded in memory, please call read_manifest() first")
+ return ""
return manifest["topic"]
# Return presentation authors
func get_authors() -> String:
+ if !is_open:
+ logger.error("No presentation is currently opened")
+ return ""
+ # Check if manifest is loaded in memory
+ if manifest == {}:
+ logger.error("Manifest not loaded in memory, please call read_manifest() first")
+ return ""
var authors: String = ""
for author in manifest["authors"]:
- authors = authors + ", " + author
+ if authors == "": authors = author
+ else: authors = authors + ", " + author
return authors
# Return display ratio
func get_ratio() -> String:
+ if !is_open:
+ logger.error("No presentation is currently opened")
+ return ""
+ # Check if manifest is loaded in memory
+ if manifest == {}:
+ logger.error("Manifest not loaded in memory, please call read_manifest() first")
+ return ""
return manifest["ratio"]
# Return display resolution from display ratio
func get_ratio_resolution() -> Vector2i:
+ if !is_open:
+ logger.error("No presentation is currently opened")
+ return Vector2i(0, 0)
+ # Check if manifest is loaded in memory
+ if manifest == {}:
+ logger.error("Manifest not loaded in memory, please call read_manifest() first")
+ return Vector2i(0, 0)
match(manifest["ratio"]):
"16:9": return Vector2i(1920, 1080)
"4:3": return Vector2i(1920, 1440)