class_name SettingsTeacherSettings extends Control const MAIN_MENU_PATH: String = "res://sources/menus/main/main_menu.tscn" const LOGIN_MENU_PATH: String = "res://sources/menus/login/login.tscn" const DEVICE_SELECTION_SCENE_PATH: String = "res://sources/menus/device_selection/device_selection.tscn" const DEVICE_TAB_SCENE: PackedScene = preload("res://sources/menus/settings/device_tab.tscn") const PASSWORD_VISUALIZER_SCENE: PackedScene = preload("res://sources/menus/components/password_visualizer.tscn") const EXPORT_COLUMNS: int = 2 const EXPORT_MAX_ROWS: int = 18 const EXPORT_TITLE_FONT_SIZE: int = 44 const EXPORT_SECTION_FONT_SIZE: int = 32 const EXPORT_TEXT_FONT_SIZE: int = 28 const EXPORT_STUDENTS_PER_COLUMN: int = 16 const EXPORT_STUDENTS_PER_PAGE: int = EXPORT_STUDENTS_PER_COLUMN * EXPORT_COLUMNS var last_device_id: int = -1 @onready var devices_tab_container: TabContainer = %DevicesTabContainer @onready var lesson_unlocks: LessonUnlocks = $LessonUnlocks @onready var delete_popup: ConfirmPopup = %DeletePopup @onready var loading_popup: LoadingPopup = %LoadingPopup @onready var account_type_option_button: OptionButton = %AccountTypeOptionButton @onready var education_method_option_button: OptionButton = %EducationMethodOptionButton @onready var add_device_button: Button = %AddDeviceButton @onready var add_student_button: Button = %AddStudentButton @onready var label_internet_mandatory: Label = %LabelInternetMandatory @onready var add_device_popup: CanvasLayer = %AddDevicePopup @onready var add_student_popup: CanvasLayer = %AddStudentPopup @onready var delete_student_popup: CanvasLayer = %DeleteStudentPopup @onready var export_codes_file_dialog: FileDialog = %ExportCodesFileDialog func _ready() -> void: refresh_devices_tabs() # Internet mandatory to add student because only the server can ensure the student code is not a duplicate if await ServerManager.check_internet_access(): add_device_button.show() add_student_button.show() label_internet_mandatory.hide() else: add_device_button.hide() add_student_button.hide() label_internet_mandatory.show() OpeningCurtain.open() lesson_unlocks.teacher_settings = self account_type_option_button.select(UserDataManager.teacher_settings.account_type) education_method_option_button.select(UserDataManager.teacher_settings.education_method) UserDataManager.user_database_synchronizer.loading_popup = loading_popup UserDataManager.user_database_synchronizer.account_type_option_button = account_type_option_button UserDataManager.user_database_synchronizer.education_method_option_button = education_method_option_button export_codes_file_dialog.set_title(tr("EXPORT_STUDENT_CODES")) export_codes_file_dialog.set_ok_button_text(tr("EXPORT_STUDENT_CODES")) export_codes_file_dialog.filters = [] export_codes_file_dialog.add_filter("*.pdf", "pdf") export_codes_file_dialog.file_selected.connect(_on_export_codes_file_selected) Log.info("SettingsTeacherSettings: Export codes dialog configured") func _on_account_type_option_button_item_selected(index: int) -> void: if TeacherSettings.AccountType.values().has(index): UserDataManager.teacher_settings.account_type = index as TeacherSettings.AccountType UserDataManager.teacher_settings.last_modified = Time.get_datetime_string_from_system(true) UserDataManager.save_teacher_settings() else: Log.warn("SettingsTeacherSettings: Cannot assign index %d to AccountType" % index) func _on_education_method_option_button_item_selected(index: int) -> void: if TeacherSettings.EducationMethod.values().has(index): UserDataManager.teacher_settings.education_method = index as TeacherSettings.EducationMethod UserDataManager.teacher_settings.last_modified = Time.get_datetime_string_from_system(true) UserDataManager.save_teacher_settings() else: Log.warn("SettingsTeacherSettings: Cannot assign index %d to EducationMethod" % index) func refresh_devices_tabs() -> void: for child: Node in devices_tab_container.get_children(false): child.queue_free() if not UserDataManager.teacher_settings: Log.error("SettingsTeacherSettings: Teacher settings not found") return for device: int in UserDataManager.teacher_settings.students.keys(): var device_tab: DeviceTab device_tab = DEVICE_TAB_SCENE.instantiate() device_tab.device_id = device devices_tab_container.add_child(device_tab) await get_tree().process_frame # Not optional or an auto-rename bug will occur on the tabs (especially if there are a lot of them) (device_tab as DeviceTab).students = UserDataManager.teacher_settings.students[device] as Array[StudentData] device_tab.name = tr("DEVICE_NUMBER").format({"number": device}) device_tab.student_pressed.connect(_on_student_pressed) device_tab.refresh() last_device_id = device func _on_back_button_pressed() -> void: await OpeningCurtain.close() if not UserDataManager.get_device_settings().device_id: get_tree().change_scene_to_file(DEVICE_SELECTION_SCENE_PATH) else: get_tree().change_scene_to_file(LOGIN_MENU_PATH) func _on_delete_button_pressed() -> void: delete_popup.show() func _on_delete_popup_accepted() -> void: var res: Dictionary = await ServerManager.delete_account() if res.code == 200: UserDataManager.delete_teacher_data() UserDataManager.logout() get_tree().change_scene_to_file(MAIN_MENU_PATH) func _on_logout_button_pressed() -> void: await OpeningCurtain.close() UserDataManager.logout() get_tree().change_scene_to_file(MAIN_MENU_PATH) func _on_devices_tab_container_tab_changed(tab: int) -> void: var device_tab: DeviceTab = devices_tab_container.get_tab_control(tab) as DeviceTab if not device_tab: Log.error("SettingsTeacherSettings: DeviceTab not found for tab " + str(tab)) return lesson_unlocks.device = device_tab.device_id func _on_student_pressed(code: int) -> void: lesson_unlocks.student = code lesson_unlocks.show() func _on_add_student_button_pressed() -> void: add_student_popup.show() func _on_add_student_popup_accepted() -> void: var current_tab: DeviceTab = devices_tab_container.get_current_tab_control() as DeviceTab if not current_tab: Log.error("SettingsTeacherSettings: DeviceTab not found") return var res: Dictionary = await ServerManager.add_student({"device": current_tab.device_id}) if res.code == 200: UserDataManager.update_configuration(res.body as Dictionary) current_tab.students = UserDataManager.teacher_settings.students[current_tab.device_id] current_tab.refresh() else: Log.error("SettingsTeacherSettings: Request to add student failed. Error code " + str(res.code)) func _on_add_device_button_pressed() -> void: add_device_popup.show() func _on_add_device_popup_accepted() -> void: var res: Dictionary = await ServerManager.add_student({"device": last_device_id + 1}) if res.code == 200: UserDataManager.update_configuration(res.body as Dictionary) refresh_devices_tabs() await get_tree().create_timer(1).timeout var count: int = devices_tab_container.get_tab_count() devices_tab_container.current_tab = count -1 func _on_lesson_unlocks_student_deleted(_code: int) -> void: delete_student_popup.show() func _on_delete_student_popup_accepted() -> void: var current_tab: DeviceTab = devices_tab_container.get_current_tab_control() as DeviceTab if not current_tab: return var res: Dictionary = await ServerManager.remove_student(int(lesson_unlocks.student)) if res.code == 200: lesson_unlocks.hide() UserDataManager.update_configuration(res.body as Dictionary) await get_tree().create_timer(1).timeout if UserDataManager.teacher_settings.students.has(current_tab.device_id): current_tab.students = UserDataManager.teacher_settings.students[current_tab.device_id] current_tab.refresh() else: refresh_devices_tabs() func update_student_name(student_code: int, student_name: String) -> void: for device: DeviceTab in devices_tab_container.get_children(false): for student_panel: StudentPanel in device.students_container.get_children(false): if student_panel.student_data.code == student_code: student_panel.name_label.text = student_name return Log.warn("SettingsTeacherSettings: update_student_name: student not found with code " + str(student_code)) #region Synchronization func _on_dashboard_button_pressed() -> void: var res: Dictionary = await ServerManager.get_dashboard() if res.code == 200: if res.has("body") and res.body is Dictionary: if (res.body as Dictionary).has("url"): OS.shell_open(res.body.url as String) return Log.error("SettingsTeacherSettings: Request to get Dashboard link has an invalid content") else: Log.error("SettingsTeacherSettings: Request to get Dashboard link failed. Error code " + str(res.code)) func _on_synchronize_button_pressed() -> void: UserDataManager.user_database_synchronizer.synchronize() func _on_loading_popup_ok() -> void: loading_popup.hide() func _on_loading_popup_cancel() -> void: Log.warn("SettingsTeacherSettings: User wanted to cancel synchronization but it is impossible to interrupt.") func _on_export_codes_button_pressed() -> void: if not UserDataManager.teacher_settings: Log.warn("SettingsTeacherSettings: Cannot export student codes without teacher settings") return Log.info("SettingsTeacherSettings: Opening export codes dialog") export_codes_file_dialog.current_file = "Codes.pdf" export_codes_file_dialog.show() func _on_export_codes_file_selected(path: String) -> void: if not UserDataManager.teacher_settings: Log.warn("SettingsTeacherSettings: Export canceled because teacher settings are missing") return Log.info("SettingsTeacherSettings: Export path selected: %s" % path) var code_pages: Array[Control] = _build_export_codes_pages() Log.info("SettingsTeacherSettings: Prepared %d export pages" % code_pages.size()) var page_images: Array[Image] = [] for page: Control in code_pages: var export_viewport: SubViewport = SubViewport.new() export_viewport.disable_3d = true export_viewport.transparent_bg = true export_viewport.size = page.size export_viewport.render_target_update_mode = SubViewport.UPDATE_ONCE export_viewport.add_child(page) add_child(export_viewport) await get_tree().process_frame await get_tree().process_frame Log.info("SettingsTeacherSettings: Capturing export codes layout page %d/%d" % [page_images.size() + 1, code_pages.size()]) page_images.append(export_viewport.get_texture().get_image()) export_viewport.queue_free() var export_path: String = path if not export_path.ends_with(".pdf"): export_path += ".pdf" Log.info("SettingsTeacherSettings: Writing export PDF to %s" % export_path) var save_error: Error = _save_codes_pdf(export_path, page_images) if save_error != OK: Log.error("SettingsTeacherSettings: Failed to save export PDF. Error: %s" % error_string(save_error)) else: Log.info("SettingsTeacherSettings: Export PDF saved successfully") func _build_export_codes_pages() -> Array[Control]: Log.info("SettingsTeacherSettings: Building export codes layout") var pages: Array[Control] = [] var teacher_settings: TeacherSettings = UserDataManager.teacher_settings var devices: Array = teacher_settings.students.keys() devices.sort() Log.info("SettingsTeacherSettings: Building export layout for %d devices" % devices.size()) for device_id: int in devices: var students: Array[StudentData] = teacher_settings.students[device_id] as Array[StudentData] Log.info("SettingsTeacherSettings: Splitting device %d into pages of %d students" % [device_id, EXPORT_STUDENTS_PER_PAGE]) for start_index: int in range(0, students.size(), EXPORT_STUDENTS_PER_PAGE): var end_index: int = min(start_index + EXPORT_STUDENTS_PER_PAGE, students.size()) var current_page: Dictionary = _create_export_codes_page() var column_containers: Array[VBoxContainer] = current_page["columns"] var page_students: Array[StudentData] = students.slice(start_index, end_index) Log.info("SettingsTeacherSettings: Creating export page %d for device %d with %d students" % [ pages.size() + 1, device_id, page_students.size() ]) for column_index: int in range(column_containers.size()): var column_start: int = column_index * EXPORT_STUDENTS_PER_COLUMN var column_end: int = min(column_start + EXPORT_STUDENTS_PER_COLUMN, page_students.size()) if column_start >= page_students.size(): break var column_students: Array[StudentData] = page_students.slice(column_start, column_end) var device_section: VBoxContainer = _build_device_section(device_id, column_students) column_containers[column_index].add_child(device_section) pages.append(current_page["canvas"]) return pages func _create_export_codes_page() -> Dictionary: var export_canvas: Control = Control.new() export_canvas.name = "ExportCodesCanvas" export_canvas.size = Vector2(1920, 1080) var panel: PanelContainer = PanelContainer.new() panel.anchor_left = 0.0 panel.anchor_top = 0.0 panel.anchor_right = 1.0 panel.anchor_bottom = 1.0 panel.offset_left = 40 panel.offset_top = 40 panel.offset_right = -40 panel.offset_bottom = -40 panel.theme_type_variation = &"PanelKaluluBig" var export_panel_style: StyleBoxFlat = StyleBoxFlat.new() export_panel_style.bg_color = Color.WHITE panel.set("theme_override_styles/panel", export_panel_style) export_canvas.add_child(panel) var main_margin: MarginContainer = MarginContainer.new() main_margin.anchor_left = 0.0 main_margin.anchor_top = 0.0 main_margin.anchor_right = 1.0 main_margin.anchor_bottom = 1.0 main_margin.offset_left = 40 main_margin.offset_top = 40 main_margin.offset_right = -40 main_margin.offset_bottom = -40 panel.add_child(main_margin) var main_vbox: VBoxContainer = VBoxContainer.new() main_vbox.set("theme_override_constants/separation", 24) main_margin.add_child(main_vbox) var title_label: Label = Label.new() title_label.text = tr("STUDENT_CODES") title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER title_label.set("theme_override_font_sizes/font_size", EXPORT_TITLE_FONT_SIZE) title_label.set("theme_override_colors/font_color", Color.BLACK) main_vbox.add_child(title_label) var content_hbox: HBoxContainer = HBoxContainer.new() content_hbox.size_flags_vertical = Control.SIZE_EXPAND_FILL content_hbox.set("theme_override_constants/separation", 40) main_vbox.add_child(content_hbox) var column_containers: Array[VBoxContainer] = [] for column_index: int in range(EXPORT_COLUMNS): var column: VBoxContainer = VBoxContainer.new() column.size_flags_horizontal = Control.SIZE_EXPAND_FILL column.set("theme_override_constants/separation", 18) content_hbox.add_child(column) column_containers.append(column) return { "canvas": export_canvas, "columns": column_containers } func _build_device_section(device_id: int, students: Array[StudentData]) -> VBoxContainer: Log.info("SettingsTeacherSettings: Building device %d section with %d students" % [device_id, students.size()]) var section: VBoxContainer = VBoxContainer.new() section.set("theme_override_constants/separation", 10) var device_label: Label = Label.new() device_label.text = tr("DEVICE_NUMBER").format({"number": device_id}) device_label.set("theme_override_font_sizes/font_size", EXPORT_SECTION_FONT_SIZE) device_label.set("theme_override_colors/font_color", Color.BLACK) section.add_child(device_label) var students_container: VBoxContainer = VBoxContainer.new() students_container.set("theme_override_constants/separation", 8) section.add_child(students_container) var sorted_students: Array[StudentData] = students.duplicate() sorted_students.sort_custom(func(a: StudentData, b: StudentData) -> bool: return a.name.naturalnocasecmp_to(b.name) < 0 ) for student_index: int in range(sorted_students.size()): var student_data: StudentData = sorted_students[student_index] Log.info("SettingsTeacherSettings: Adding student row for %s (%s)" % [student_data.name, str(student_data.code)]) students_container.add_child(_build_student_row(student_data)) if student_index < sorted_students.size() - 1: students_container.add_child(_build_student_separator()) section.add_child(_build_device_separator()) return section func _build_student_row(student_data: StudentData) -> HBoxContainer: Log.trace("SettingsTeacherSettings: Building row for student code %s" % str(student_data.code)) var row: HBoxContainer = HBoxContainer.new() row.set("theme_override_constants/separation", 12) var name_label: Label = Label.new() name_label.text = student_data.name if student_data.name else tr("STUDENT_NUM").format({"number": student_data.code}) name_label.set("theme_override_font_sizes/font_size", EXPORT_TEXT_FONT_SIZE) name_label.set("theme_override_colors/font_color", Color.BLACK) name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL row.add_child(name_label) var visualizer: PasswordVisualizer = PASSWORD_VISUALIZER_SCENE.instantiate() as PasswordVisualizer visualizer.key_size = 32 visualizer.password = str(student_data.code) visualizer.show_backgrounds = false row.add_child(visualizer) return row func _build_student_separator() -> HSeparator: var separator: HSeparator = HSeparator.new() separator.size_flags_horizontal = Control.SIZE_EXPAND_FILL var line_style: StyleBoxLine = StyleBoxLine.new() line_style.color = Color("c7c7c7") line_style.thickness = 2 separator.set("theme_override_styles/separator", line_style) return separator func _build_device_separator() -> HSeparator: var separator: HSeparator = HSeparator.new() separator.size_flags_horizontal = Control.SIZE_EXPAND_FILL var line_style: StyleBoxLine = StyleBoxLine.new() line_style.color = Color("8c8c8c") line_style.thickness = 4 separator.set("theme_override_styles/separator", line_style) return separator func _save_codes_pdf(path: String, images: Array[Image]) -> Error: if images.is_empty(): Log.error("SettingsTeacherSettings: No pages to export") return ERR_CANT_CREATE var jpg_pages: Array[PackedByteArray] = [] for page: Image in images: Log.info("SettingsTeacherSettings: Starting PDF build (%dx%d)" % [page.get_width(), page.get_height()]) var jpg_data: PackedByteArray = page.save_jpg_to_buffer(0.9) if jpg_data.is_empty(): Log.error("SettingsTeacherSettings: Failed to encode JPEG buffer for PDF") return ERR_CANT_CREATE jpg_pages.append(jpg_data) var pdf_data: PackedByteArray = PackedByteArray() var xref_offsets: Array[int] = [0] Log.info("SettingsTeacherSettings: Creating PDF objects") _append_pdf_string(pdf_data, "%PDF-1.4\n") _add_pdf_object(pdf_data, xref_offsets, "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n") var page_count: int = images.size() var kids_entries: PackedStringArray = [] for page_index: int in range(page_count): var page_object_id: int = 3 + page_index * 3 kids_entries.append("%d 0 R" % page_object_id) _add_pdf_object( pdf_data, xref_offsets, "2 0 obj\n<< /Type /Pages /Kids [%s] /Count %d >>\nendobj\n" % [" ".join(kids_entries), page_count] ) for page_index: int in range(page_count): var page: Image = images[page_index] var jpg_data: PackedByteArray = jpg_pages[page_index] var width: int = page.get_width() var height: int = page.get_height() var page_object_id: int = 3 + page_index * 3 var image_object_id: int = page_object_id + 1 var content_object_id: int = page_object_id + 2 var image_name: String = "Im%d" % page_index _add_pdf_object( pdf_data, xref_offsets, "%d 0 obj\n<< /Type /Page /Parent 2 0 R /Resources << /XObject << /%s %d 0 R >> /ProcSet [/PDF /ImageC] >> /MediaBox [0 0 %d %d] /Contents %d 0 R >>\nendobj\n" % [page_object_id, image_name, image_object_id, width, height, content_object_id] ) var image_header: String = "%d 0 obj\n<< /Type /XObject /Subtype /Image /Width %d /Height %d /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length %d >>\nstream\n" % [image_object_id, width, height, jpg_data.size()] _add_pdf_object_with_stream(pdf_data, xref_offsets, image_header, jpg_data) var content_stream: String = "q\n%d 0 0 %d 0 0 cm\n/%s Do\nQ\n" % [width, height, image_name] var content_bytes: PackedByteArray = content_stream.to_utf8_buffer() var content_header: String = "%d 0 obj\n<< /Length %d >>\nstream\n" % [content_object_id, content_bytes.size()] _add_pdf_object_with_stream(pdf_data, xref_offsets, content_header, content_bytes) Log.info("SettingsTeacherSettings: Writing PDF xref table") var xref_offset: int = pdf_data.size() var xref_lines: PackedStringArray = [] xref_lines.append("xref") xref_lines.append("0 %d" % xref_offsets.size()) xref_lines.append("0000000000 65535 f ") for offset: int in xref_offsets.slice(1, xref_offsets.size()): xref_lines.append("%010d 00000 n " % offset) _append_pdf_string(pdf_data, "\n".join(xref_lines) + "\n") _append_pdf_string(pdf_data, "trailer\n<< /Size %d /Root 1 0 R >>\nstartxref\n%d\n%%EOF\n" % [xref_offsets.size(), xref_offset]) var file: FileAccess = FileAccess.open(path, FileAccess.WRITE) if not file: Log.error("SettingsTeacherSettings: Failed to open export path for writing") return ERR_CANT_OPEN Log.info("SettingsTeacherSettings: Storing PDF buffer (%d bytes)" % pdf_data.size()) file.store_buffer(pdf_data) file.close() Log.info("SettingsTeacherSettings: PDF file written") return OK func _add_pdf_object(pdf_data: PackedByteArray, offsets: Array[int], object_text: String) -> void: offsets.append(pdf_data.size()) _append_pdf_string(pdf_data, object_text) func _add_pdf_object_with_stream(pdf_data: PackedByteArray, offsets: Array[int], header: String, stream_data: PackedByteArray) -> void: offsets.append(pdf_data.size()) _append_pdf_string(pdf_data, header) pdf_data.append_array(stream_data) _append_pdf_string(pdf_data, "\nendstream\nendobj\n") func _append_pdf_string(pdf_data: PackedByteArray, text: String) -> void: pdf_data.append_array(text.to_utf8_buffer()) #endregion