Skip to main content

pacenotes_installer\wizard\scene/
game_folder.rs

1use windows_sys::Win32::Foundation::{HWND, LPARAM, WPARAM};
2use windows_sys::Win32::UI::Controls as ctl;
3use windows_sys::Win32::UI::WindowsAndMessaging as wm;
4
5/// Re-scanning on every keystroke would stat a folder per character and
6/// complain halfway through a valid path.
7const DETECT_DEBOUNCE_MS: u32 = 400;
8const TIMER_DETECT: usize = 1;
9
10const EN_CHANGE: u16 = 0x0300;
11const BN_CLICKED: u16 = 0;
12const CBN_SELCHANGE: u16 = 1;
13
14/// Scene 3: the game folder, and which game in it when it holds several.
15pub unsafe extern "system" fn game_folder_page(
16    dialog: HWND,
17    message: u32,
18    wparam: WPARAM,
19    lparam: LPARAM,
20) -> isize {
21    match message {
22        wm::WM_INITDIALOG => {
23            crate::wizard::page::adopt_state(dialog, lparam);
24            crate::ui::set_text(
25                dialog,
26                crate::wizard::control_ids::IDC_S3_PATH_LABEL,
27                crate::i18n::IDS_S3_PATH,
28            );
29            crate::ui::set_text(
30                dialog,
31                crate::wizard::control_ids::IDC_S3_GAME_LABEL,
32                crate::i18n::IDS_S3_GAME,
33            );
34            crate::ui::set_text(
35                dialog,
36                crate::wizard::control_ids::IDC_S3_BROWSE,
37                crate::i18n::IDS_BTN_BROWSE,
38            );
39            for (control, text) in [
40                (
41                    crate::wizard::control_ids::IDC_S3_ROUTES,
42                    crate::i18n::IDS_S3_ROUTES,
43                ),
44                (
45                    crate::wizard::control_ids::IDC_S3_DOWNLOAD,
46                    crate::i18n::IDS_S3_DOWNLOAD,
47                ),
48                (
49                    crate::wizard::control_ids::IDC_S3_CONFIGURE,
50                    crate::i18n::IDS_S3_CONFIGURE,
51                ),
52                (
53                    crate::wizard::control_ids::IDC_S3_RESTORE,
54                    crate::i18n::IDS_S3_RESTORE,
55                ),
56            ] {
57                crate::ui::set_text(dialog, control, text);
58            }
59            super::game_folder_routes::enter_takes_main_route(dialog);
60            show_carried_folder(dialog);
61            1
62        }
63        wm::WM_COMMAND => {
64            on_command(dialog, wparam);
65            1
66        }
67        wm::WM_TIMER if wparam == TIMER_DETECT => {
68            wm::KillTimer(dialog, TIMER_DETECT);
69            redetect(dialog, false);
70            1
71        }
72        wm::WM_NOTIFY => match crate::wizard::page::notify_code(lparam) {
73            Some(ctl::PSN_SETACTIVE) => {
74                crate::wizard::page::set_sheet_caption(dialog);
75                crate::wizard::page::set_header(dialog, crate::i18n::IDS_S3_TITLE);
76                // The way on is one of the two route buttons, so the sheet's Next stays disabled.
77                crate::wizard::page::set_buttons(dialog, ctl::PSWIZB_BACK);
78                crate::wizard::page::set_next_text(dialog, crate::i18n::IDS_BTN_NEXT);
79                super::game_folder_routes::show_route_buttons(dialog);
80                crate::wizard::page::reply(dialog, 0)
81            }
82            Some(ctl::PSN_QUERYCANCEL) => crate::wizard::page::query_cancel(dialog),
83            Some(ctl::NM_CUSTOMDRAW) => super::game_folder_routes::paint_main_route(dialog, lparam),
84            _ => 0,
85        },
86        _ => 0,
87    }
88}
89
90unsafe fn on_command(dialog: HWND, wparam: WPARAM) {
91    let control = crate::ui::loword(wparam);
92    let code = crate::ui::hiword(wparam);
93    match (control, code) {
94        (crate::wizard::control_ids::IDC_S3_PATH, EN_CHANGE) => {
95            wm::SetTimer(dialog, TIMER_DETECT, DETECT_DEBOUNCE_MS, None);
96        }
97        (crate::wizard::control_ids::IDC_S3_BROWSE, BN_CLICKED) => on_browse(dialog),
98        (crate::wizard::control_ids::IDC_S3_GAME, CBN_SELCHANGE) => on_game_picked(dialog),
99        (crate::wizard::control_ids::IDC_S3_DOWNLOAD, BN_CLICKED) => {
100            super::game_folder_routes::take_route(dialog, crate::wizard::state::Route::Download)
101        }
102        (crate::wizard::control_ids::IDC_S3_CONFIGURE, BN_CLICKED) => {
103            super::game_folder_routes::take_route(dialog, crate::wizard::state::Route::Configure)
104        }
105        (crate::wizard::control_ids::IDC_S3_RESTORE, BN_CLICKED) => {
106            super::game_folder_routes::take_route(dialog, crate::wizard::state::Route::Restore)
107        }
108        _ => {}
109    }
110}
111
112/// Browsing is deliberate, so an unusable pick is reported at once.
113unsafe fn on_browse(dialog: HWND) {
114    let prompt = crate::i18n::load_string(crate::i18n::IDS_S3_PATH);
115    let Some(picked) = crate::ui::browse_for_folder(dialog, &prompt) else {
116        return;
117    };
118    crate::ui::set_text_raw(dialog, crate::wizard::control_ids::IDC_S3_PATH, &picked);
119    redetect(dialog, true);
120}
121
122/// An instance restarted as administrator arrives with its folder chosen, so
123/// it is shown and detected at once; otherwise the page starts empty.
124unsafe fn show_carried_folder(dialog: HWND) {
125    let carried = crate::wizard::page::state(dialog)
126        .map(|state| state.folder.clone())
127        .filter(|folder| !folder.is_empty());
128    match carried {
129        Some(folder) => {
130            crate::ui::set_text_raw(dialog, crate::wizard::control_ids::IDC_S3_PATH, &folder);
131            redetect(dialog, false);
132        }
133        None => show_detection(dialog),
134    }
135}
136
137/// Keeps the previous game while the folder still holds it, else takes the first match.
138unsafe fn redetect(dialog: HWND, complain_if_none: bool) {
139    let folder = crate::ui::get_text(dialog, crate::wizard::control_ids::IDC_S3_PATH);
140    let detected = pacenotes_installer::gamedir::detect(&folder);
141    let Some(state) = crate::wizard::page::state(dialog) else {
142        return;
143    };
144    state.folder = folder;
145    state.game = match state.game {
146        Some(previous) if detected.contains(&previous) => Some(previous),
147        _ => detected.first().copied(),
148    };
149    state.detected = detected;
150    let none_found = state.detected.is_empty();
151
152    show_detection(dialog);
153    super::game_folder_routes::show_route_buttons(dialog);
154    if complain_if_none && none_found {
155        warn_no_game(dialog);
156    }
157}
158
159/// A disabled placeholder when nothing is found, the one game disabled when
160/// there is no choice, and only the matches enabled when there is.
161unsafe fn show_detection(dialog: HWND) {
162    let combo = crate::wizard::control_ids::IDC_S3_GAME;
163    let (detected, chosen) = match crate::wizard::page::state(dialog) {
164        Some(state) => (state.detected.clone(), state.game),
165        None => (Vec::new(), None),
166    };
167
168    crate::ui::combo_reset(dialog, combo);
169    if detected.is_empty() {
170        crate::ui::combo_add(
171            dialog,
172            combo,
173            &crate::i18n::load_string(crate::i18n::IDS_S3_DETECTING),
174        );
175        crate::ui::combo_select(dialog, combo, 0);
176        crate::ui::enable(dialog, combo, false);
177        return;
178    }
179    for game in &detected {
180        crate::ui::combo_add(dialog, combo, game.title());
181    }
182    let selected = chosen
183        .and_then(|chosen| detected.iter().position(|game| *game == chosen))
184        .unwrap_or(0);
185    crate::ui::combo_select(dialog, combo, selected);
186    crate::ui::enable(dialog, combo, detected.len() > 1);
187}
188
189unsafe fn on_game_picked(dialog: HWND) {
190    let selection = crate::ui::combo_selection(dialog, crate::wizard::control_ids::IDC_S3_GAME);
191    if let (Some(index), Some(state)) = (selection, crate::wizard::page::state(dialog)) {
192        state.game = state.detected.get(index).copied();
193    }
194    super::game_folder_routes::show_route_buttons(dialog);
195}
196
197pub(super) unsafe fn warn_no_game(dialog: HWND) {
198    crate::ui::message_box_warning(
199        dialog,
200        &crate::i18n::load_string(crate::i18n::IDS_S3_NOGAME),
201    );
202}