Redesign question bank screen to match new mockup
Adapts the quiz screen's header, toolbar, choice cards, and bottom bar
to the designer's new layout:
- Header: adds the RanaZe logo/wordmark and a question-progress bar +
percentage alongside the existing "Question N of M" text.
- Toolbar: consolidates 12 icon-only/chip buttons into 6 labeled items
(Text size, Calculator, Clear highlight, Notes, Bookmark, More), with
a new "More" popup holding the rest (labs, flag, report, flashcard,
theme toggle, feedback). The underlying buttons are reparented, not
duplicated, so every existing theming/visibility/state call site
keeps working unmodified.
- Category tag: shows the question's discipline (falling back to
system) above the stem.
- Choice cards: every choice now renders as a subtly-bordered rounded
card even before answering, not just a flat divider row; the
currently-selected pre-submit choice shows a checkmark.
- Bottom bar: merges the Skip/Submit row and the timer row into one -
Skip as a plain link, a centered "X of Y" progress bar, and a
time slot that shows the existing countdown when a limit applies or
a new count-up stopwatch ("Time elapsed") otherwise, plus a
Ctrl+Enter/Cmd+Enter submit shortcut with a visible hint.
This commit is contained in:
+351
-42
@@ -65,7 +65,14 @@ var awaiting_next_question := false
|
||||
var simulation_submit_pending := false
|
||||
|
||||
var progress_button: Button
|
||||
var progress_bar: ProgressBar
|
||||
var progress_percent_label: Label
|
||||
var bottom_progress_label: Label
|
||||
var bottom_progress_bar: ProgressBar
|
||||
var header_wordmark_label: Label
|
||||
var category_label: Label
|
||||
var timer_label: Label
|
||||
var timer_caption_label: Label
|
||||
var stem_label: RichTextLabel
|
||||
var lead_label: Label
|
||||
var explanation_label: RichTextLabel
|
||||
@@ -92,6 +99,10 @@ var flashcard_prefill_dialog: Control
|
||||
var bookmark_button: Button
|
||||
var app_feedback_button: Button
|
||||
var theme_button: Button
|
||||
var more_button: Button
|
||||
var more_menu_popup: PopupPanel
|
||||
var text_size_button: Button
|
||||
var text_size_popup: PopupPanel
|
||||
var question_toolbar: HFlowContainer
|
||||
var header_stack: VBoxContainer
|
||||
var header_row: HBoxContainer
|
||||
@@ -107,6 +118,7 @@ var media_container: VBoxContainer
|
||||
var timer_bar: ProgressBar
|
||||
var choice_badges: Array[Label] = []
|
||||
var choice_badge_textures: Array[TextureRect] = []
|
||||
var choice_selected_icons: Array[TextureRect] = []
|
||||
var feedback_fog: ColorRect
|
||||
var labs_overlay: Control
|
||||
var labs_panel: PanelContainer
|
||||
@@ -146,6 +158,7 @@ var green_fog_time := 0.0
|
||||
var correct_advance_delay := 0.0
|
||||
var shake_time := 0.0
|
||||
var shake_origin := Vector2.ZERO
|
||||
var untimed_elapsed_start_msec := 0
|
||||
|
||||
func _ready() -> void:
|
||||
theme_service = get_node("/root/UIThemeService")
|
||||
@@ -213,17 +226,53 @@ func _ready() -> void:
|
||||
header_stack.add_theme_constant_override("separation", 8)
|
||||
card_content.add_child(header_stack)
|
||||
header_row = HBoxContainer.new()
|
||||
header_row.add_theme_constant_override("separation", 10)
|
||||
header_row.add_theme_constant_override("separation", 16)
|
||||
header_stack.add_child(header_row)
|
||||
|
||||
var header_brand := HBoxContainer.new()
|
||||
header_brand.add_theme_constant_override("separation", 8)
|
||||
header_row.add_child(header_brand)
|
||||
var header_brand_mark := TextureRect.new()
|
||||
header_brand_mark.texture = Brand.ranaze_mark_texture()
|
||||
header_brand_mark.custom_minimum_size = Vector2(28, 28)
|
||||
header_brand_mark.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
header_brand_mark.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
header_brand.add_child(header_brand_mark)
|
||||
header_wordmark_label = Label.new()
|
||||
header_wordmark_label.text = "RanaZe"
|
||||
header_wordmark_label.add_theme_font_override("font", Brand.font)
|
||||
header_wordmark_label.add_theme_font_size_override("font_size", 18)
|
||||
header_brand.add_child(header_wordmark_label)
|
||||
|
||||
var progress_group := VBoxContainer.new()
|
||||
progress_group.add_theme_constant_override("separation", 2)
|
||||
progress_group.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
header_row.add_child(progress_group)
|
||||
|
||||
progress_button = Button.new()
|
||||
progress_button.flat = true
|
||||
progress_button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
progress_button.custom_minimum_size = Vector2(126, 34)
|
||||
progress_button.custom_minimum_size = Vector2(126, 20)
|
||||
progress_button.tooltip_text = "Open question navigator"
|
||||
_set_scaled_button_font(progress_button, 14)
|
||||
progress_button.pressed.connect(_toggle_question_navigator)
|
||||
header_row.add_child(progress_button)
|
||||
progress_group.add_child(progress_button)
|
||||
|
||||
var progress_bar_row := HBoxContainer.new()
|
||||
progress_bar_row.add_theme_constant_override("separation", 8)
|
||||
progress_group.add_child(progress_bar_row)
|
||||
|
||||
progress_bar = ProgressBar.new()
|
||||
progress_bar.show_percentage = false
|
||||
progress_bar.min_value = 0.0
|
||||
progress_bar.max_value = 100.0
|
||||
progress_bar.custom_minimum_size = Vector2(120, 6)
|
||||
progress_bar.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
progress_bar_row.add_child(progress_bar)
|
||||
|
||||
progress_percent_label = Label.new()
|
||||
_set_scaled_label(progress_percent_label, 13, Brand.SLATE)
|
||||
progress_bar_row.add_child(progress_percent_label)
|
||||
|
||||
var header_spacer := Control.new()
|
||||
header_spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
@@ -235,10 +284,14 @@ func _ready() -> void:
|
||||
timer_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
|
||||
question_toolbar = HFlowContainer.new()
|
||||
question_toolbar.add_theme_constant_override("h_separation", 8)
|
||||
question_toolbar.add_theme_constant_override("h_separation", 14)
|
||||
question_toolbar.add_theme_constant_override("v_separation", 8)
|
||||
question_toolbar.alignment = FlowContainer.ALIGNMENT_END
|
||||
|
||||
# All the underlying buttons are constructed up front (same as before) so every
|
||||
# other place in this file that mutates them by reference - theming, visibility,
|
||||
# tooltips, badge state - keeps working unmodified. Only *where* each one is
|
||||
# parented (directly in question_toolbar vs. reparented into a popup) changes.
|
||||
theme_button = Button.new()
|
||||
theme_button.custom_minimum_size = Vector2(44, 44)
|
||||
theme_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||
@@ -248,7 +301,6 @@ func _ready() -> void:
|
||||
theme_button.focus_mode = Control.FOCUS_NONE
|
||||
UISoundService.set_button_sound(theme_button, "toggle")
|
||||
theme_button.pressed.connect(theme_service.toggle_theme)
|
||||
question_toolbar.add_child(theme_button)
|
||||
_refresh_theme_button()
|
||||
|
||||
text_smaller_button = Button.new()
|
||||
@@ -258,7 +310,6 @@ func _ready() -> void:
|
||||
Style.style_chip(text_smaller_button, Style.CYAN)
|
||||
UISoundService.set_button_sound(text_smaller_button, "toggle")
|
||||
text_smaller_button.pressed.connect(func(): _adjust_question_text_scale(-TEXT_SCALE_STEP))
|
||||
question_toolbar.add_child(text_smaller_button)
|
||||
|
||||
text_larger_button = Button.new()
|
||||
text_larger_button.text = "A+"
|
||||
@@ -267,7 +318,6 @@ func _ready() -> void:
|
||||
Style.style_chip(text_larger_button, Style.CYAN)
|
||||
UISoundService.set_button_sound(text_larger_button, "toggle")
|
||||
text_larger_button.pressed.connect(func(): _adjust_question_text_scale(TEXT_SCALE_STEP))
|
||||
question_toolbar.add_child(text_larger_button)
|
||||
|
||||
calculator_button = Button.new()
|
||||
calculator_button.name = "CalculatorButton"
|
||||
@@ -280,21 +330,18 @@ func _ready() -> void:
|
||||
calculator_button.focus_mode = Control.FOCUS_NONE
|
||||
UISoundService.set_button_sound(calculator_button, "open")
|
||||
calculator_button.pressed.connect(_toggle_calculator)
|
||||
question_toolbar.add_child(calculator_button)
|
||||
|
||||
labs_button = Button.new()
|
||||
labs_button.tooltip_text = "Review lab results"
|
||||
labs_button.custom_minimum_size = Vector2(44, 44)
|
||||
UISoundService.set_button_sound(labs_button, "open")
|
||||
labs_button.pressed.connect(_show_labs_modal)
|
||||
question_toolbar.add_child(labs_button)
|
||||
|
||||
clear_highlight_button = Button.new()
|
||||
clear_highlight_button.tooltip_text = "Clear highlights"
|
||||
clear_highlight_button.custom_minimum_size = Vector2(44, 44)
|
||||
UISoundService.set_button_sound(clear_highlight_button, "eliminate")
|
||||
clear_highlight_button.pressed.connect(_clear_current_highlights)
|
||||
question_toolbar.add_child(clear_highlight_button)
|
||||
|
||||
flag_button = Button.new()
|
||||
flag_button.toggle_mode = true
|
||||
@@ -308,7 +355,6 @@ func _ready() -> void:
|
||||
if BlockSessionService.session_kind == "simulation":
|
||||
_save_simulation_current_response()
|
||||
)
|
||||
question_toolbar.add_child(flag_button)
|
||||
|
||||
flashcard_button = Button.new()
|
||||
flashcard_button.tooltip_text = "Save as flashcard (available once answered)"
|
||||
@@ -321,7 +367,6 @@ func _ready() -> void:
|
||||
flashcard_button.disabled = true
|
||||
UISoundService.set_button_sound(flashcard_button, "toggle")
|
||||
flashcard_button.pressed.connect(_toggle_flashcard_current_question)
|
||||
question_toolbar.add_child(flashcard_button)
|
||||
flashcard_prefill_dialog = FlashcardPrefillDialog.new()
|
||||
flashcard_prefill_dialog.name = "FlashcardPrefillDialog"
|
||||
add_child(flashcard_prefill_dialog)
|
||||
@@ -336,14 +381,12 @@ func _ready() -> void:
|
||||
bookmark_button.focus_mode = Control.FOCUS_NONE
|
||||
UISoundService.set_button_sound(bookmark_button, "toggle")
|
||||
bookmark_button.pressed.connect(_toggle_bookmark_current_question)
|
||||
question_toolbar.add_child(bookmark_button)
|
||||
|
||||
report_button = Button.new()
|
||||
report_button.tooltip_text = "Report question"
|
||||
report_button.custom_minimum_size = Vector2(44, 44)
|
||||
UISoundService.set_button_sound(report_button, "open")
|
||||
report_button.pressed.connect(_toggle_report_current_question)
|
||||
question_toolbar.add_child(report_button)
|
||||
|
||||
notes_button = Button.new()
|
||||
notes_button.tooltip_text = "Open notes"
|
||||
@@ -355,7 +398,6 @@ func _ready() -> void:
|
||||
notes_button.focus_mode = Control.FOCUS_NONE
|
||||
UISoundService.set_button_sound(notes_button, "open")
|
||||
notes_button.pressed.connect(func(): main.open_notes_dialog())
|
||||
question_toolbar.add_child(notes_button)
|
||||
|
||||
app_feedback_button = Button.new()
|
||||
app_feedback_button.text = "Feedback"
|
||||
@@ -363,15 +405,69 @@ func _ready() -> void:
|
||||
app_feedback_button.custom_minimum_size = Vector2(92, 44)
|
||||
Style.style_chip(app_feedback_button, Style.CYAN)
|
||||
app_feedback_button.pressed.connect(func(): main.open_feedback_dialog())
|
||||
question_toolbar.add_child(app_feedback_button)
|
||||
question_toolbar.move_child(labs_button, 0)
|
||||
question_toolbar.move_child(theme_button, question_toolbar.get_child_count() - 2)
|
||||
|
||||
# Text size: single entry point opening a small popup with the existing A-/A+
|
||||
# controls untouched, instead of two always-visible buttons.
|
||||
text_size_button = Button.new()
|
||||
text_size_button.text = "Aa"
|
||||
text_size_button.tooltip_text = "Text size"
|
||||
text_size_button.custom_minimum_size = Vector2(44, 44)
|
||||
text_size_button.focus_mode = Control.FOCUS_NONE
|
||||
text_size_button.pressed.connect(_toggle_text_size_popup)
|
||||
question_toolbar.add_child(_toolbar_labeled_button(text_size_button, "Text size"))
|
||||
|
||||
question_toolbar.add_child(_toolbar_labeled_button(calculator_button, "Calculator"))
|
||||
question_toolbar.add_child(_toolbar_labeled_button(clear_highlight_button, "Clear highlight"))
|
||||
question_toolbar.add_child(_toolbar_labeled_button(notes_button, "Notes"))
|
||||
question_toolbar.add_child(_toolbar_labeled_button(bookmark_button, "Bookmark"))
|
||||
|
||||
more_button = Button.new()
|
||||
more_button.text = "..."
|
||||
more_button.tooltip_text = "More actions"
|
||||
more_button.custom_minimum_size = Vector2(44, 44)
|
||||
more_button.focus_mode = Control.FOCUS_NONE
|
||||
more_button.pressed.connect(_toggle_more_menu)
|
||||
question_toolbar.add_child(_toolbar_labeled_button(more_button, "More"))
|
||||
|
||||
header_toolbar_host = HBoxContainer.new()
|
||||
header_toolbar_host.alignment = BoxContainer.ALIGNMENT_END
|
||||
header_toolbar_host.add_child(question_toolbar)
|
||||
header_row.add_child(header_toolbar_host)
|
||||
|
||||
text_size_popup = PopupPanel.new()
|
||||
text_size_popup.name = "TextSizePopup"
|
||||
add_child(text_size_popup)
|
||||
var text_size_margin := MarginContainer.new()
|
||||
for side in ["left", "right", "top", "bottom"]:
|
||||
text_size_margin.add_theme_constant_override("margin_%s" % side, 12)
|
||||
text_size_popup.add_child(text_size_margin)
|
||||
var text_size_row := HBoxContainer.new()
|
||||
text_size_row.add_theme_constant_override("separation", 8)
|
||||
text_size_margin.add_child(text_size_row)
|
||||
text_size_row.add_child(text_smaller_button)
|
||||
text_size_row.add_child(text_larger_button)
|
||||
|
||||
more_menu_popup = PopupPanel.new()
|
||||
more_menu_popup.name = "MoreMenu"
|
||||
more_menu_popup.min_size = Vector2i(260, 0)
|
||||
add_child(more_menu_popup)
|
||||
var more_menu_margin := MarginContainer.new()
|
||||
for side in ["left", "right", "top", "bottom"]:
|
||||
more_menu_margin.add_theme_constant_override("margin_%s" % side, 10)
|
||||
more_menu_popup.add_child(more_menu_margin)
|
||||
var more_menu_content := VBoxContainer.new()
|
||||
more_menu_content.add_theme_constant_override("separation", 4)
|
||||
more_menu_margin.add_child(more_menu_content)
|
||||
for entry in [
|
||||
[labs_button, "Labs"],
|
||||
[flag_button, "Flag question"],
|
||||
[report_button, "Report question"],
|
||||
[flashcard_button, "Save as flashcard"],
|
||||
[theme_button, "Switch theme"],
|
||||
[app_feedback_button, "Feedback"],
|
||||
]:
|
||||
more_menu_content.add_child(_more_menu_row(entry[0], entry[1]))
|
||||
|
||||
navigator_popup = PopupPanel.new()
|
||||
navigator_popup.name = "QuestionNavigator"
|
||||
navigator_popup.min_size = Vector2i(340, 180)
|
||||
@@ -421,6 +517,11 @@ func _ready() -> void:
|
||||
question_content.add_theme_constant_override("separation", 12)
|
||||
content_margin.add_child(question_content)
|
||||
|
||||
category_label = Label.new()
|
||||
category_label.visible = false
|
||||
_set_scaled_label(category_label, 14, Brand.SKY_BLUE)
|
||||
question_content.add_child(category_label)
|
||||
|
||||
stem_label = RichTextLabel.new()
|
||||
stem_label.fit_content = true
|
||||
stem_label.bbcode_enabled = true
|
||||
@@ -521,6 +622,23 @@ func _ready() -> void:
|
||||
button.add_child(percent_label)
|
||||
choice_percent_labels.append(percent_label)
|
||||
|
||||
var selected_icon := TextureRect.new()
|
||||
selected_icon.visible = false
|
||||
selected_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
selected_icon.texture = preload("res://assets/icons/lucide/circle-check.svg")
|
||||
selected_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
selected_icon.modulate = Brand.PURPLE
|
||||
selected_icon.anchor_left = 1.0
|
||||
selected_icon.anchor_top = 0.0
|
||||
selected_icon.anchor_right = 1.0
|
||||
selected_icon.anchor_bottom = 0.0
|
||||
selected_icon.offset_left = -50
|
||||
selected_icon.offset_top = 18
|
||||
selected_icon.offset_right = -18
|
||||
selected_icon.offset_bottom = 50
|
||||
button.add_child(selected_icon)
|
||||
choice_selected_icons.append(selected_icon)
|
||||
|
||||
var top_label := RichTextLabel.new()
|
||||
top_label.visible = false
|
||||
top_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
@@ -556,24 +674,59 @@ func _ready() -> void:
|
||||
_build_answer_review_ui()
|
||||
|
||||
actions = HBoxContainer.new()
|
||||
actions.add_theme_constant_override("separation", 10)
|
||||
actions.add_theme_constant_override("separation", 16)
|
||||
card_content.add_child(actions)
|
||||
|
||||
skip_button = Button.new()
|
||||
skip_button.text = "Skip"
|
||||
skip_button.custom_minimum_size = Vector2(120, 48)
|
||||
Style.style_chip(skip_button, Style.AMBER)
|
||||
skip_button.text = "Skip question"
|
||||
skip_button.flat = true
|
||||
skip_button.custom_minimum_size = Vector2(0, 40)
|
||||
UISoundService.set_button_sound(skip_button, "back")
|
||||
skip_button.pressed.connect(_skip_current_question)
|
||||
actions.add_child(skip_button)
|
||||
|
||||
var bottom_progress_group := VBoxContainer.new()
|
||||
bottom_progress_group.add_theme_constant_override("separation", 4)
|
||||
bottom_progress_group.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
bottom_progress_group.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
actions.add_child(bottom_progress_group)
|
||||
|
||||
bottom_progress_label = Label.new()
|
||||
bottom_progress_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_set_scaled_label(bottom_progress_label, 13, Brand.SLATE)
|
||||
bottom_progress_group.add_child(bottom_progress_label)
|
||||
|
||||
bottom_progress_bar = ProgressBar.new()
|
||||
bottom_progress_bar.show_percentage = false
|
||||
bottom_progress_bar.min_value = 0.0
|
||||
bottom_progress_bar.max_value = 100.0
|
||||
bottom_progress_bar.custom_minimum_size = Vector2(160, 6)
|
||||
bottom_progress_bar.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||
bottom_progress_group.add_child(bottom_progress_bar)
|
||||
|
||||
var time_group := VBoxContainer.new()
|
||||
time_group.add_theme_constant_override("separation", 0)
|
||||
actions.add_child(time_group)
|
||||
|
||||
timer_caption_label = Label.new()
|
||||
timer_caption_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
_set_scaled_label(timer_caption_label, 12, Brand.SLATE)
|
||||
time_group.add_child(timer_caption_label)
|
||||
time_group.add_child(timer_label)
|
||||
|
||||
next_button = Button.new()
|
||||
next_button.text = "Submit Block" if BlockSessionService.session_kind == "simulation" and BlockSessionService.current_index + 1 == BlockSessionService.active_questions.size() else "Next" if BlockSessionService.session_kind == "simulation" else "Submit"
|
||||
next_button.tooltip_text = ""
|
||||
next_button.custom_minimum_size = Vector2(140, 48)
|
||||
next_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
Style.style_primary_button(next_button)
|
||||
next_button.pressed.connect(_submit_or_next)
|
||||
actions.add_child(next_button)
|
||||
card_content.add_child(actions)
|
||||
|
||||
var shortcut_hint_label := Label.new()
|
||||
shortcut_hint_label.name = "SubmitShortcutHint"
|
||||
shortcut_hint_label.text = "⌘ Enter" if OS.get_name() == "macOS" else "Ctrl+Enter"
|
||||
_set_scaled_label(shortcut_hint_label, 11, Brand.SLATE)
|
||||
actions.add_child(shortcut_hint_label)
|
||||
|
||||
timer_footer = HBoxContainer.new()
|
||||
timer_footer.add_theme_constant_override("separation", 12)
|
||||
@@ -588,7 +741,6 @@ func _ready() -> void:
|
||||
timer_bar.add_theme_stylebox_override("background", Style.button_box(Color("232b3d"), Color("303b53")))
|
||||
timer_bar.add_theme_stylebox_override("fill", Style.button_box(Style.LIME, Style.LIME))
|
||||
timer_footer.add_child(timer_bar)
|
||||
timer_footer.add_child(timer_label)
|
||||
|
||||
feedback_fog = ColorRect.new()
|
||||
feedback_fog.color = Color(1.0, 0.0, 0.0, 0.0)
|
||||
@@ -677,7 +829,10 @@ func _fit_modal_panels() -> void:
|
||||
func _refresh_responsive_layout() -> void:
|
||||
if header_toolbar_host == null or header_row == null or header_stack == null:
|
||||
return
|
||||
var compact := size.x < 1120.0
|
||||
# The toolbar is now 6 labeled items (with "More" absorbing the rest) instead of
|
||||
# 12 icon-only buttons, so it's meaningfully narrower - the compact-mode breakpoint
|
||||
# is tuned down to match rather than reusing the old wider-toolbar threshold.
|
||||
var compact := size.x < 860.0
|
||||
var target_parent: Control = header_row
|
||||
if compact:
|
||||
target_parent = header_stack
|
||||
@@ -685,9 +840,7 @@ func _refresh_responsive_layout() -> void:
|
||||
header_toolbar_host.reparent(target_parent)
|
||||
header_toolbar_host.size_flags_horizontal = Control.SIZE_EXPAND_FILL if compact else Control.SIZE_SHRINK_END
|
||||
question_toolbar.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
header_toolbar_host.custom_minimum_size.x = 0.0 if compact else 520.0
|
||||
app_feedback_button.text = "" if size.x < 760.0 else "Feedback"
|
||||
app_feedback_button.custom_minimum_size.x = 44 if size.x < 760.0 else 92
|
||||
header_toolbar_host.custom_minimum_size.x = 0.0 if compact else 380.0
|
||||
if size.x < 560.0:
|
||||
nav_palette.columns = 5
|
||||
else:
|
||||
@@ -717,12 +870,12 @@ func _answer_box(dark: bool, state: String, attached: bool = false) -> StyleBoxF
|
||||
box.content_margin_top = 22
|
||||
box.content_margin_bottom = 22
|
||||
if state == "default" or state == "eliminated":
|
||||
# Flat, divider-style row - most choices sit in a plain list; only the
|
||||
# states below need to visually pop out as their own card.
|
||||
# Every choice reads as its own subtly-bordered card, even before answering -
|
||||
# only selected/correct/wrong states get a stronger accent border/background.
|
||||
box.bg_color = Color("0b172b") if dark else Color("ffffff")
|
||||
box.border_color = Color("22304d") if dark else Color("e7ebf2")
|
||||
box.border_width_bottom = 1
|
||||
box.set_corner_radius_all(0)
|
||||
box.set_border_width_all(1)
|
||||
box.set_corner_radius_all(14)
|
||||
return box
|
||||
var background := Color("17183b") if dark else Color("eff4ff")
|
||||
var border := Brand.PURPLE
|
||||
@@ -916,10 +1069,37 @@ func _apply_question_theme() -> void:
|
||||
question_panel.add_theme_stylebox_override("panel", _question_content_box(dark))
|
||||
if navigator_popup != null:
|
||||
navigator_popup.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
|
||||
if text_size_popup != null:
|
||||
text_size_popup.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
|
||||
if more_menu_popup != null:
|
||||
more_menu_popup.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
|
||||
for label in more_menu_popup.find_children("MoreMenuLabel", "Label", true, false):
|
||||
(label as Label).add_theme_color_override("font_color", Brand.text_color(dark))
|
||||
if question_toolbar != null:
|
||||
for label in question_toolbar.find_children("ToolbarLabel", "Label", true, false):
|
||||
(label as Label).add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
progress_button.add_theme_color_override("font_color", Brand.text_color(dark))
|
||||
progress_button.add_theme_color_override("font_hover_color", Brand.SKY_BLUE)
|
||||
progress_button.add_theme_color_override("font_disabled_color", Brand.text_color(dark))
|
||||
if header_wordmark_label != null:
|
||||
header_wordmark_label.add_theme_color_override("font_color", Brand.text_color(dark))
|
||||
var progress_track := Style.button_box(Color("232b3d") if dark else Color("e2e8f0"), Color("303b53") if dark else Color("cbd5e1"))
|
||||
var progress_fill := Style.button_box(Brand.ROYAL_BLUE, Brand.ROYAL_BLUE)
|
||||
if progress_bar != null:
|
||||
progress_bar.add_theme_stylebox_override("background", progress_track)
|
||||
progress_bar.add_theme_stylebox_override("fill", progress_fill)
|
||||
if progress_percent_label != null:
|
||||
progress_percent_label.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
if bottom_progress_bar != null:
|
||||
bottom_progress_bar.add_theme_stylebox_override("background", progress_track)
|
||||
bottom_progress_bar.add_theme_stylebox_override("fill", progress_fill)
|
||||
if bottom_progress_label != null:
|
||||
bottom_progress_label.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
if category_label != null:
|
||||
category_label.add_theme_color_override("font_color", Brand.SKY_BLUE if dark else Brand.ROYAL_BLUE)
|
||||
timer_label.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
if timer_caption_label != null:
|
||||
timer_caption_label.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
stem_label.add_theme_color_override("default_color", Brand.text_color(dark))
|
||||
lead_label.add_theme_color_override("font_color", Brand.text_color(dark))
|
||||
explanation_label.add_theme_color_override("default_color", Brand.text_color(dark))
|
||||
@@ -935,7 +1115,7 @@ func _apply_question_theme() -> void:
|
||||
var navigator_heading := navigator_popup.find_child("NavigatorHeading", true, false) as Label
|
||||
if navigator_heading != null:
|
||||
navigator_heading.add_theme_color_override("font_color", Brand.text_color(dark))
|
||||
for control in [text_smaller_button, text_larger_button, calculator_button, bookmark_button, notes_button, flashcard_button, app_feedback_button]:
|
||||
for control in [text_smaller_button, text_larger_button, calculator_button, bookmark_button, notes_button, flashcard_button, app_feedback_button, text_size_button, more_button]:
|
||||
if control == null:
|
||||
continue
|
||||
var active: bool = control == bookmark_button and BookmarkService.is_bookmarked(str(BlockSessionService.get_current_question().get("id", "")))
|
||||
@@ -952,9 +1132,17 @@ func _apply_question_theme() -> void:
|
||||
if calculator_panel != null:
|
||||
calculator_panel.refresh_theme_controls()
|
||||
if skip_button != null:
|
||||
skip_button.add_theme_stylebox_override("normal", _toolbar_box(dark))
|
||||
skip_button.add_theme_stylebox_override("hover", _toolbar_box(dark, true))
|
||||
skip_button.add_theme_color_override("font_color", Brand.text_color(dark))
|
||||
var skip_link_color := Brand.SKY_BLUE if dark else Brand.ROYAL_BLUE
|
||||
skip_button.add_theme_stylebox_override("normal", StyleBoxEmpty.new())
|
||||
skip_button.add_theme_stylebox_override("hover", StyleBoxEmpty.new())
|
||||
skip_button.add_theme_stylebox_override("pressed", StyleBoxEmpty.new())
|
||||
skip_button.add_theme_stylebox_override("disabled", StyleBoxEmpty.new())
|
||||
skip_button.add_theme_color_override("font_color", skip_link_color)
|
||||
skip_button.add_theme_color_override("font_hover_color", skip_link_color)
|
||||
skip_button.add_theme_color_override("font_disabled_color", Brand.muted_color(dark))
|
||||
var shortcut_hint := find_child("SubmitShortcutHint", true, false) as Label
|
||||
if shortcut_hint != null:
|
||||
shortcut_hint.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
if next_button != null:
|
||||
next_button.add_theme_stylebox_override("normal", _primary_action_box(dark, "normal"))
|
||||
next_button.add_theme_stylebox_override("hover", _primary_action_box(dark, "hover"))
|
||||
@@ -1054,6 +1242,12 @@ func _process(delta: float) -> void:
|
||||
_update_timer_bar()
|
||||
if BlockSessionService.seconds_remaining <= 0:
|
||||
_finish()
|
||||
if visible and not _is_session_timer_visible() and (report_overlay == null or not report_overlay.visible):
|
||||
# No real time limit applies here - show a simple local count-up stopwatch
|
||||
# ("Time elapsed") so the bottom-bar time slot is never blank, without
|
||||
# touching BlockSessionService's own (block-scoped, not per-question) timing.
|
||||
var elapsed_seconds := int(floor(float(Time.get_ticks_msec() - untimed_elapsed_start_msec) / 1000.0))
|
||||
_set_timer_display(maxi(0, elapsed_seconds))
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
@@ -1085,6 +1279,15 @@ func _input(event: InputEvent) -> void:
|
||||
if not visible or pending_correct_advance or stem_label == null:
|
||||
return
|
||||
|
||||
if event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_ENTER and (event.ctrl_pressed or event.meta_pressed):
|
||||
var focus_owner := get_viewport().gui_get_focus_owner()
|
||||
var focus_blocks_shortcut := focus_owner is TextEdit or focus_owner is LineEdit
|
||||
var overlay_blocks_shortcut := (report_overlay != null and report_overlay.visible) or (labs_overlay != null and labs_overlay.visible)
|
||||
if not focus_blocks_shortcut and not overlay_blocks_shortcut and next_button != null and next_button.visible and not next_button.disabled:
|
||||
get_viewport().set_input_as_handled()
|
||||
_submit_or_next()
|
||||
return
|
||||
|
||||
var mouse_event := event as InputEventMouseButton
|
||||
if mouse_event == null or mouse_event.button_index != MOUSE_BUTTON_LEFT:
|
||||
return
|
||||
@@ -1124,7 +1327,7 @@ func _load_question() -> void:
|
||||
next_button.disabled = true
|
||||
skip_button.disabled = false
|
||||
skip_button.visible = BlockSessionService.session_kind not in ["review", "dynamic"]
|
||||
skip_button.text = "Skip"
|
||||
skip_button.text = "Skip question"
|
||||
report_button.visible = BlockSessionService.session_kind != "simulation"
|
||||
|
||||
var question := BlockSessionService.get_current_question()
|
||||
@@ -1141,21 +1344,35 @@ func _load_question() -> void:
|
||||
progress_button.text = _question_progress_text()
|
||||
progress_button.disabled = BlockSessionService.session_kind in ["core", "bookmark", "review", "dynamic"]
|
||||
progress_button.tooltip_text = "" if BlockSessionService.session_kind in ["core", "bookmark", "review", "dynamic"] else "Open question navigator"
|
||||
_update_progress_indicators()
|
||||
untimed_elapsed_start_msec = Time.get_ticks_msec()
|
||||
var show_timer := _is_session_timer_visible()
|
||||
timer_label.visible = show_timer
|
||||
# The time slot in the bottom bar is always visible - a countdown ("Time remaining")
|
||||
# when a time limit applies, otherwise a simple count-up stopwatch ("Time elapsed")
|
||||
# so the area is never blank for untimed practice.
|
||||
timer_label.visible = true
|
||||
timer_bar.visible = show_timer
|
||||
timer_footer.visible = show_timer
|
||||
if timer_caption_label != null:
|
||||
timer_caption_label.text = "Time remaining" if show_timer else "Time elapsed"
|
||||
if show_timer:
|
||||
if BlockSessionService.session_kind == "core":
|
||||
_set_timer_display(int(floor(BlockSessionService.get_core_question_total_elapsed_seconds())))
|
||||
else:
|
||||
_set_timer_display(int(ceil(BlockSessionService.seconds_remaining)))
|
||||
else:
|
||||
_set_timer_display(0)
|
||||
_update_timer_bar()
|
||||
stem_plain_text = str(question.get("stem", ""))
|
||||
explanation_plain_text = str(question.get("explanation", ""))
|
||||
_render_stem()
|
||||
_load_question_media(question)
|
||||
lead_label.text = question.get("lead_in", "")
|
||||
var category_text := str(question.get("discipline", "")).strip_edges()
|
||||
if category_text.is_empty():
|
||||
category_text = str(question.get("system", "")).strip_edges()
|
||||
category_label.text = category_text
|
||||
category_label.visible = not category_text.is_empty()
|
||||
_refresh_labs_button()
|
||||
|
||||
presented_choices = question.get("presented_choices", question.get("choices", [])).duplicate(true)
|
||||
@@ -1247,9 +1464,11 @@ func _should_clear_selection_from_click(mouse_event: InputEventMouseButton) -> b
|
||||
return false
|
||||
if labs_button != null and labs_button.visible and labs_button.get_global_rect().has_point(pointer_position):
|
||||
return false
|
||||
if flag_button != null and flag_button.get_global_rect().has_point(pointer_position):
|
||||
if flag_button != null and flag_button.visible and flag_button.get_global_rect().has_point(pointer_position):
|
||||
return false
|
||||
if report_button != null and report_button.get_global_rect().has_point(pointer_position):
|
||||
if report_button != null and report_button.visible and report_button.get_global_rect().has_point(pointer_position):
|
||||
return false
|
||||
if more_menu_popup != null and more_menu_popup.visible and Rect2(more_menu_popup.position, more_menu_popup.size).has_point(pointer_position):
|
||||
return false
|
||||
for button in choice_buttons:
|
||||
if button.visible and button.get_global_rect().has_point(pointer_position):
|
||||
@@ -1533,7 +1752,7 @@ func _show_answer_review() -> void:
|
||||
if skip_button != null:
|
||||
skip_button.disabled = true
|
||||
skip_button.visible = BlockSessionService.session_kind == "bookmark"
|
||||
skip_button.text = "Back to Bookmarks" if BlockSessionService.session_kind == "bookmark" else "Skip"
|
||||
skip_button.text = "Back to Bookmarks" if BlockSessionService.session_kind == "bookmark" else "Skip question"
|
||||
skip_button.disabled = false if BlockSessionService.session_kind == "bookmark" else true
|
||||
if question_scroll != null:
|
||||
question_scroll.call_deferred("ensure_control_visible", explanation_label)
|
||||
@@ -1928,6 +2147,34 @@ func _question_progress_text() -> String:
|
||||
]
|
||||
|
||||
|
||||
func _update_progress_indicators() -> void:
|
||||
# "core" sessions have no fixed total (endless queue), so there's no meaningful
|
||||
# ratio/percentage to show - hide the bars rather than showing a misleading value.
|
||||
var show_progress := BlockSessionService.session_kind != "core"
|
||||
if progress_bar != null:
|
||||
progress_bar.visible = show_progress
|
||||
if progress_percent_label != null:
|
||||
progress_percent_label.visible = show_progress
|
||||
if bottom_progress_bar != null:
|
||||
bottom_progress_bar.visible = show_progress
|
||||
if bottom_progress_label != null:
|
||||
bottom_progress_label.visible = show_progress
|
||||
if not show_progress:
|
||||
return
|
||||
var total := BlockSessionService.active_questions.size()
|
||||
var current := mini(BlockSessionService.current_index + 1, maxi(total, 1))
|
||||
var ratio := float(current) / float(maxi(total, 1))
|
||||
var percent := int(round(ratio * 100.0))
|
||||
if progress_bar != null:
|
||||
progress_bar.value = ratio * 100.0
|
||||
if progress_percent_label != null:
|
||||
progress_percent_label.text = "%d%%" % percent
|
||||
if bottom_progress_bar != null:
|
||||
bottom_progress_bar.value = ratio * 100.0
|
||||
if bottom_progress_label != null:
|
||||
bottom_progress_label.text = "%d of %d" % [current, total]
|
||||
|
||||
|
||||
func _toggle_question_navigator() -> void:
|
||||
if navigator_popup == null or BlockSessionService.session_kind == "core":
|
||||
return
|
||||
@@ -1943,6 +2190,66 @@ func _toggle_question_navigator() -> void:
|
||||
navigator_popup.popup(Rect2i(Vector2i(popup_x, popup_y), popup_size))
|
||||
|
||||
|
||||
func _toolbar_labeled_button(button: Button, label_text: String) -> Control:
|
||||
var wrap := VBoxContainer.new()
|
||||
wrap.add_theme_constant_override("separation", 2)
|
||||
wrap.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
wrap.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||
wrap.add_child(button)
|
||||
var label := Label.new()
|
||||
label.name = "ToolbarLabel"
|
||||
label.text = label_text
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||
_set_scaled_label(label, 11, Brand.SLATE)
|
||||
wrap.add_child(label)
|
||||
return wrap
|
||||
|
||||
|
||||
func _more_menu_row(button: Button, label_text: String) -> Control:
|
||||
# Reparents the *actual* button (not a duplicate/proxy) so its existing icon,
|
||||
# tooltip, toggle state, and pressed connection keep working unmodified wherever
|
||||
# it's mutated by reference elsewhere in this file.
|
||||
var row := HBoxContainer.new()
|
||||
row.name = "MoreMenuRow"
|
||||
row.add_theme_constant_override("separation", 10)
|
||||
row.add_child(button)
|
||||
var label := Label.new()
|
||||
label.name = "MoreMenuLabel"
|
||||
label.text = label_text
|
||||
_set_scaled_label(label, 14, Brand.SLATE)
|
||||
row.add_child(label)
|
||||
return row
|
||||
|
||||
|
||||
func _toggle_text_size_popup() -> void:
|
||||
if text_size_popup == null:
|
||||
return
|
||||
if text_size_popup.visible:
|
||||
text_size_popup.hide()
|
||||
return
|
||||
var popup_size := Vector2i(140, 76)
|
||||
var anchor := text_size_button.get_global_rect()
|
||||
var viewport_size := get_viewport_rect().size
|
||||
var popup_x := int(clampf(anchor.position.x, 8.0, maxf(8.0, viewport_size.x - popup_size.x - 8.0)))
|
||||
var popup_y := int(minf(anchor.end.y + 6.0, viewport_size.y - popup_size.y - 8.0))
|
||||
text_size_popup.popup(Rect2i(Vector2i(popup_x, popup_y), popup_size))
|
||||
|
||||
|
||||
func _toggle_more_menu() -> void:
|
||||
if more_menu_popup == null:
|
||||
return
|
||||
if more_menu_popup.visible:
|
||||
more_menu_popup.hide()
|
||||
return
|
||||
var popup_size := Vector2i(260, 300)
|
||||
var anchor := more_button.get_global_rect()
|
||||
var viewport_size := get_viewport_rect().size
|
||||
var popup_x := int(clampf(anchor.position.x - popup_size.x + anchor.size.x, 8.0, maxf(8.0, viewport_size.x - popup_size.x - 8.0)))
|
||||
var popup_y := int(minf(anchor.end.y + 6.0, viewport_size.y - popup_size.y - 8.0))
|
||||
more_menu_popup.popup(Rect2i(Vector2i(popup_x, popup_y), popup_size))
|
||||
|
||||
|
||||
func _navigate_to_question(target_index: int) -> void:
|
||||
if target_index == BlockSessionService.current_index:
|
||||
navigator_popup.hide()
|
||||
@@ -3147,6 +3454,8 @@ func _style_choice_button_for_state(index: int, choice_id: String) -> void:
|
||||
badge_texture.texture = BADGE_CIRCLE_FILLED_ICON if filled else BADGE_CIRCLE_OUTLINE_ICON
|
||||
badge_texture.modulate = accent
|
||||
badge.add_theme_color_override("font_color", Color.WHITE if filled else Brand.muted_color(dark))
|
||||
if index < choice_selected_icons.size():
|
||||
choice_selected_icons[index].visible = state == "selected"
|
||||
|
||||
|
||||
func _row_has_attached_panel(index: int) -> bool:
|
||||
|
||||
@@ -357,7 +357,9 @@ func _test_player_runtime_construction() -> void:
|
||||
_assert(player.question_card != null, "question player should construct its branded card")
|
||||
_assert(player.navigator_popup != null, "question player should construct its popup navigator")
|
||||
_assert(player.header_row.get_node_or_null("StepBadge") == null, "question player should not display a redundant STEP 3 badge")
|
||||
_assert(player.header_row.get_child(0) == player.progress_button, "question progress should be the first header item")
|
||||
_assert(player.header_row.get_child(0) != player.progress_button, "the brand mark should be the first header item, ahead of question progress")
|
||||
_assert(player.progress_button.get_parent().get_parent() == player.header_row, "question progress should still live in the header row")
|
||||
_assert(player.progress_bar != null and player.progress_percent_label != null, "question player should construct a header progress bar and percentage label")
|
||||
_assert(player.clear_highlight_button.icon != null, "clear highlights should use the supplied toolbar artwork")
|
||||
_assert(player.report_button.icon != null, "report question should use the supplied toolbar artwork")
|
||||
_assert(player.labs_button.icon != null, "lab results should use the supplied toolbar artwork")
|
||||
|
||||
Reference in New Issue
Block a user