#![allow(unsafe_op_in_unsafe_fn)]
use windows_sys::Win32::Foundation::{HWND, LPARAM, WPARAM, POINT};
use windows_sys::Win32::UI::WindowsAndMessaging::{
SendMessageW, MessageBoxW, SetWindowTextW, MB_OK, MB_ICONINFORMATION,
GetCursorPos, TrackPopupMenu, CreatePopupMenu, AppendMenuW, DestroyMenu,
TPM_RETURNCMD, TPM_LEFTALIGN, MF_STRING, CB_GETCURSEL,
WM_COMMAND,
};
use windows_sys::Win32::System::DataExchange::{
OpenClipboard, CloseClipboard, GetClipboardData, IsClipboardFormatAvailable
};
use windows_sys::Win32::System::Memory::{GlobalLock, GlobalUnlock};
use windows_sys::Win32::UI::Shell::{DragQueryFileW, DragFinish, HDROP};
use windows_sys::Win32::UI::Controls::{NM_CLICK, NM_DBLCLK, NMLISTVIEW, LVM_GETSUBITEMRECT};
use windows_sys::Win32::Graphics::Gdi::{InvalidateRect, ScreenToClient};
use std::thread;
use std::sync::atomic::Ordering;
use std::cmp::Ordering as CmpOrdering;
use crate::ui::state::{AppState, BatchAction, ProcessingState, BatchStatus};
use crate::ui::taskbar::TaskbarState; use crate::ui::controls::*;
use crate::ui::theme;
use crate::engine::wof::WofAlgorithm;
use crate::engine::worker::batch_process_worker;
use crate::utils::{to_wstring, u64_to_wstring, concat_wstrings, reveal_path_in_explorer};
use crate::ui::file_dialog::{pick_files, pick_folder};
pub unsafe fn on_add_files(st: &mut AppState) {
if let Ok(files) = pick_files() {
st.ingest_paths(files);
}
}
pub unsafe fn on_add_folder(st: &mut AppState) {
if let Ok(folder) = pick_folder() {
st.ingest_paths(vec![folder]);
}
}
pub unsafe fn on_remove_selected(st: &mut AppState) {
let mut selected_indices = if let Some(ctrls) = &st.controls {
ctrls.file_list.get_selected_indices()
} else { Vec::new() };
selected_indices.sort_by(|a, b| b.cmp(a));
let ids_to_remove: Vec<u32> = selected_indices.iter()
.filter_map(|&idx| st.batch_items.get(idx).map(|item| item.id))
.collect();
for id in ids_to_remove {
st.remove_batch_item(id);
}
if let Some(ctrls) = &st.controls {
for idx in selected_indices {
ctrls.file_list.remove_item(idx as i32);
}
}
}
pub unsafe fn on_clear_all(st: &mut AppState) {
if st.batch_items.is_empty() { return; }
st.clear_batch();
st.next_item_id = 1;
if let Some(ctrls) = &st.controls {
ctrls.file_list.clear_all();
}
st.global_progress_current.store(0, std::sync::atomic::Ordering::Relaxed);
st.global_progress_total.store(0, std::sync::atomic::Ordering::Relaxed);
}
pub unsafe fn on_process_all(st: &mut AppState, hwnd: HWND, is_auto_start: bool) {
if st.batch_items.is_empty() {
let w_info = to_wstring("Info");
let w_msg = to_wstring("Add folders first!");
MessageBoxW(hwnd, w_msg.as_ptr(), w_info.as_ptr(), MB_OK | MB_ICONINFORMATION);
return;
}
if let Some(ctrls) = &st.controls {
let mut indices = ctrls.file_list.get_selected_indices();
if is_auto_start && indices.is_empty() { return; }
if indices.is_empty() { indices = (0..st.batch_items.len()).collect(); }
start_processing(st, hwnd, indices);
}
}
pub unsafe fn start_processing(st: &mut AppState, hwnd: HWND, indices_to_process: Vec<usize>) {
if indices_to_process.is_empty() { return; }
if let Some(ctrls) = &st.controls {
let idx = SendMessageW(ctrls.action_panel.combo_hwnd(), CB_GETCURSEL, 0, 0);
let use_as_listed = idx == 0;
let global_algo = match idx {
1 => WofAlgorithm::Xpress4K,
3 => WofAlgorithm::Xpress16K,
4 => WofAlgorithm::Lzx,
_ => WofAlgorithm::Xpress8K,
};
if !use_as_listed {
let algo_name = match global_algo {
WofAlgorithm::Xpress4K => "XPRESS4K",
WofAlgorithm::Xpress8K => "XPRESS8K",
WofAlgorithm::Xpress16K => "XPRESS16K",
WofAlgorithm::Lzx => "LZX",
};
for &row in &indices_to_process {
ctrls.file_list.update_item_text(row as i32, 2, to_wstring(algo_name));
}
}
if let Some(tb) = &st.taskbar { tb.set_state(TaskbarState::Normal); }
windows_sys::Win32::UI::Input::KeyboardAndMouse::EnableWindow(ctrls.action_panel.cancel_hwnd(), 1);
let count_w = u64_to_wstring(indices_to_process.len() as u64);
let status_msg = concat_wstrings(&[&to_wstring("Processing "), &count_w, &to_wstring(" items...")]);
SetWindowTextW(ctrls.status_bar.label_hwnd(), status_msg.as_ptr());
let tx = st.tx.clone();
let state_global = st.global_state.clone();
state_global.store(ProcessingState::Running as u8, Ordering::Relaxed);
let action_mode_idx = SendMessageW(ctrls.action_panel.action_mode_hwnd(), CB_GETCURSEL, 0, 0);
let items: Vec<_> = indices_to_process.into_iter().filter_map(|idx| {
st.batch_items.get(idx).map(|item| {
let effective_action = match action_mode_idx {
1 => BatchAction::Compress, 2 => BatchAction::Decompress, _ => item.action,
};
let effective_algo = if use_as_listed { item.algorithm } else { global_algo };
(item.path.clone(), effective_action, idx, effective_algo)
})
}).collect();
let force = st.force_compress;
let guard = st.config.enable_system_guard;
let low_power = st.low_power_mode;
let max_threads = st.config.max_threads;
let main_hwnd_usize = hwnd as usize;
let global_cur = st.global_progress_current.clone();
let global_tot = st.global_progress_total.clone();
thread::spawn(move || {
batch_process_worker(items, tx, state_global, force, main_hwnd_usize, guard, low_power, max_threads, global_cur, global_tot);
});
}
}
pub unsafe fn on_stop_processing(st: &mut AppState) {
st.global_state.store(ProcessingState::Stopped as u8, Ordering::Relaxed);
for item in &st.batch_items {
if let Some(flag) = &item.state_flag { flag.store(ProcessingState::Stopped as u8, Ordering::Relaxed); }
}
if let Some(tb) = &st.taskbar { tb.set_state(TaskbarState::Paused); }
if let Some(ctrls) = &st.controls {
windows_sys::Win32::UI::Input::KeyboardAndMouse::EnableWindow(ctrls.action_panel.cancel_hwnd(), 0);
let w_stop = to_wstring("Stopping...");
SetWindowTextW(ctrls.status_bar.label_hwnd(), w_stop.as_ptr());
for (i, _item) in st.batch_items.iter().enumerate() {
ctrls.file_list.update_item_text(i as i32, 10, to_wstring("▶ Start"));
ctrls.file_list.update_item_text(i as i32, 9, to_wstring("Cancelled"));
}
}
}
pub unsafe fn on_open_settings(st: &mut AppState, hwnd: HWND) {
let current_theme = st.theme;
let is_dark = theme::resolve_mode(st.theme);
let (new_theme, new_force, new_ctx, new_guard, new_low_power, new_threads) = crate::ui::dialogs::show_settings_modal(
hwnd, current_theme, is_dark, st.enable_force_stop, st.config.enable_context_menu, st.config.enable_system_guard, st.low_power_mode, st.config.max_threads
);
if let Some(t) = new_theme {
st.theme = t;
let new_is_dark = theme::resolve_mode(st.theme);
theme::set_window_frame_theme(hwnd, new_is_dark);
if let Some(ctrls) = &mut st.controls { ctrls.update_theme(new_is_dark, hwnd); }
InvalidateRect(hwnd, std::ptr::null(), 1);
}
st.enable_force_stop = new_force;
st.config.enable_context_menu = new_ctx;
st.config.enable_system_guard = new_guard;
st.low_power_mode = new_low_power;
st.config.max_threads = new_threads;
crate::engine::power::set_process_eco_mode(st.low_power_mode);
}
pub unsafe fn on_list_click(st: &mut AppState, hwnd: HWND, row: i32, col: i32, code: u32) {
if row < 0 { return; }
if col == 0 && code == NM_DBLCLK { if let Some(item) = st.batch_items.get(row as usize) {
reveal_path_in_explorer(&item.path);
}
} else if col == 2 && code == NM_DBLCLK { if let Some(item) = st.batch_items.get_mut(row as usize) {
item.algorithm = match item.algorithm {
WofAlgorithm::Xpress4K => WofAlgorithm::Xpress8K,
WofAlgorithm::Xpress8K => WofAlgorithm::Xpress16K,
WofAlgorithm::Xpress16K => WofAlgorithm::Lzx,
WofAlgorithm::Lzx => WofAlgorithm::Xpress4K,
};
let name = match item.algorithm {
WofAlgorithm::Xpress4K => "XPRESS4K", WofAlgorithm::Xpress8K => "XPRESS8K",
WofAlgorithm::Xpress16K => "XPRESS16K", WofAlgorithm::Lzx => "LZX",
};
let algo = item.algorithm;
if let Some(cached) = item.get_cached_estimate(algo) {
item.estimated_size = cached;
let est_str = crate::utils::format_size(cached);
if let Some(ctrls) = &st.controls {
ctrls.file_list.update_item_text(row, 2, to_wstring(name));
ctrls.file_list.update_item_text(row, 5, est_str);
}
} else {
let path = item.path.clone();
let id = item.id;
if let Some(ctrls) = &st.controls {
ctrls.file_list.update_item_text(row, 2, to_wstring(name));
ctrls.file_list.update_item_text(row, 5, to_wstring("Estimating..."));
}
let tx = st.tx.clone();
thread::spawn(move || {
let estimated = crate::engine::estimator::estimate_path(&path, algo);
let est_str = crate::utils::format_size(estimated);
let _ = tx.send(crate::ui::state::UiMessage::UpdateEstimate(id, algo, estimated, est_str));
});
}
}
} else if col == 3 && code == NM_DBLCLK { if let Some(item) = st.batch_items.get_mut(row as usize) {
item.action = match item.action {
BatchAction::Compress => BatchAction::Decompress,
BatchAction::Decompress => BatchAction::Compress,
};
let name = match item.action {
BatchAction::Compress => "Compress", BatchAction::Decompress => "Decompress",
};
if let Some(ctrls) = &st.controls { ctrls.file_list.update_item_text(row, 3, to_wstring(name)); }
}
} else if col == 10 && code == NM_CLICK { if let Some(_item) = st.batch_items.get_mut(row as usize) {
let list_hwnd = if let Some(ctrls) = &st.controls {
ctrls.file_list.hwnd()
} else {
return;
};
let mut rect: windows_sys::Win32::Foundation::RECT = unsafe { std::mem::zeroed() };
rect.top = 10; rect.left = windows_sys::Win32::UI::Controls::LVIR_BOUNDS as i32;
unsafe {
let _ = SendMessageW(list_hwnd, LVM_GETSUBITEMRECT, row as usize, &mut rect as *mut _ as isize);
}
let mut pt: POINT = unsafe { std::mem::zeroed() };
unsafe {
GetCursorPos(&mut pt);
ScreenToClient(list_hwnd, &mut pt);
}
let width = rect.right - rect.left;
let is_right_half = if width > 0 {
pt.x > (rect.left + width / 2)
} else {
false
};
let current_state_val = st.global_state.load(Ordering::Relaxed);
let current_state = ProcessingState::from_u8(current_state_val);
match current_state {
ProcessingState::Idle | ProcessingState::Stopped => {
let indices = vec![row as usize];
start_processing(st, hwnd, indices);
if let Some(ctrls) = &st.controls {
ctrls.file_list.update_item_text(row, 10, to_wstring("⏸ ⏹"));
}
},
ProcessingState::Running => {
if is_right_half {
on_stop_processing(st);
if let Some(ctrls) = &st.controls {
ctrls.file_list.update_item_text(row, 10, to_wstring("▶ Start"));
}
} else {
st.global_state.store(ProcessingState::Paused as u8, Ordering::Relaxed);
if let Some(tb) = &st.taskbar { tb.set_state(TaskbarState::Paused); }
if let Some(ctrls) = &st.controls {
ctrls.file_list.update_item_text(row, 10, to_wstring("▶ ⏹"));
ctrls.file_list.update_item_text(row, 9, to_wstring("Paused"));
let msg = to_wstring("Paused.");
SetWindowTextW(ctrls.status_bar.label_hwnd(), msg.as_ptr());
}
}
},
ProcessingState::Paused => {
if is_right_half {
on_stop_processing(st);
} else {
st.global_state.store(ProcessingState::Running as u8, Ordering::Relaxed);
if let Some(tb) = &st.taskbar { tb.set_state(TaskbarState::Normal); }
if let Some(ctrls) = &st.controls {
ctrls.file_list.update_item_text(row, 10, to_wstring("⏸ ⏹"));
ctrls.file_list.update_item_text(row, 9, to_wstring("Processing"));
let msg = to_wstring("Resumed.");
SetWindowTextW(ctrls.status_bar.label_hwnd(), msg.as_ptr());
}
}
},
}
}
}
}
pub unsafe fn on_list_keydown(st: &mut AppState, _hwnd: HWND, key: u16) {
if key == windows_sys::Win32::UI::Input::KeyboardAndMouse::VK_DELETE as u16 {
on_remove_selected(st);
}
}
pub unsafe fn on_list_rclick(st: &mut AppState, hwnd: HWND, row: i32, col: i32) -> bool {
if row < 0 { return false; }
if col == 2 {
let mut pt: POINT = std::mem::zeroed();
GetCursorPos(&mut pt);
let menu = CreatePopupMenu();
if menu != std::ptr::null_mut() {
let _ = AppendMenuW(menu, MF_STRING, 2001, to_wstring("XPRESS4K").as_ptr());
let _ = AppendMenuW(menu, MF_STRING, 2002, to_wstring("XPRESS8K").as_ptr());
let _ = AppendMenuW(menu, MF_STRING, 2003, to_wstring("XPRESS16K").as_ptr());
let _ = AppendMenuW(menu, MF_STRING, 2004, to_wstring("LZX").as_ptr());
if let Some(item) = st.batch_items.get(row as usize) {
let check_id = match item.algorithm {
WofAlgorithm::Xpress4K => 2001,
WofAlgorithm::Xpress8K => 2002,
WofAlgorithm::Xpress16K => 2003,
WofAlgorithm::Lzx => 2004,
};
use windows_sys::Win32::UI::WindowsAndMessaging::{CheckMenuItem, MF_CHECKED};
CheckMenuItem(menu, check_id, MF_CHECKED);
}
let cmd = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_LEFTALIGN | windows_sys::Win32::UI::WindowsAndMessaging::TPM_RIGHTBUTTON, pt.x, pt.y, 0, hwnd, std::ptr::null());
DestroyMenu(menu);
if cmd >= 2001 && cmd <= 2004 {
let new_algo = match cmd {
2001 => WofAlgorithm::Xpress4K,
2002 => WofAlgorithm::Xpress8K,
2003 => WofAlgorithm::Xpress16K,
2004 => WofAlgorithm::Lzx,
_ => WofAlgorithm::Xpress8K,
};
if let Some(item) = st.batch_items.get_mut(row as usize) {
if item.algorithm != new_algo {
item.algorithm = new_algo;
let name = match item.algorithm {
WofAlgorithm::Xpress4K => "XPRESS4K",
WofAlgorithm::Xpress8K => "XPRESS8K",
WofAlgorithm::Xpress16K => "XPRESS16K",
WofAlgorithm::Lzx => "LZX",
};
if let Some(ctrls) = &st.controls {
ctrls.file_list.update_item_text(row, 2, to_wstring(name));
}
if let Some(cached) = item.get_cached_estimate(new_algo) {
item.estimated_size = cached;
let est_str = crate::utils::format_size(cached);
if let Some(ctrls) = &st.controls {
ctrls.file_list.update_item_text(row, 5, est_str);
}
} else {
let path = item.path.clone();
let id = item.id;
if let Some(ctrls) = &st.controls {
ctrls.file_list.update_item_text(row, 5, to_wstring("Estimating..."));
}
let tx = st.tx.clone();
thread::spawn(move || {
let estimated = crate::engine::estimator::estimate_path(&path, new_algo);
let est_str = crate::utils::format_size(estimated);
let _ = tx.send(crate::ui::state::UiMessage::UpdateEstimate(id, new_algo, estimated, est_str));
});
}
}
}
}
return true;
}
}
else if col == 3 {
let mut pt: POINT = std::mem::zeroed();
GetCursorPos(&mut pt);
let menu = CreatePopupMenu();
if menu != std::ptr::null_mut() {
let _ = AppendMenuW(menu, MF_STRING, 3001, to_wstring("Compress").as_ptr());
let _ = AppendMenuW(menu, MF_STRING, 3002, to_wstring("Decompress").as_ptr());
if let Some(item) = st.batch_items.get(row as usize) {
let check_id = match item.action {
crate::ui::state::BatchAction::Compress => 3001,
crate::ui::state::BatchAction::Decompress => 3002,
};
use windows_sys::Win32::UI::WindowsAndMessaging::{CheckMenuItem, MF_CHECKED};
CheckMenuItem(menu, check_id, MF_CHECKED);
}
let cmd = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_LEFTALIGN | windows_sys::Win32::UI::WindowsAndMessaging::TPM_RIGHTBUTTON, pt.x, pt.y, 0, hwnd, std::ptr::null());
DestroyMenu(menu);
if cmd >= 3001 && cmd <= 3002 {
let new_action = match cmd {
3001 => crate::ui::state::BatchAction::Compress,
3002 => crate::ui::state::BatchAction::Decompress,
_ => crate::ui::state::BatchAction::Compress,
};
if let Some(item) = st.batch_items.get_mut(row as usize) {
if item.action != new_action {
item.action = new_action;
let name = match item.action {
crate::ui::state::BatchAction::Compress => "Compress",
crate::ui::state::BatchAction::Decompress => "Decompress",
};
if let Some(ctrls) = &st.controls {
ctrls.file_list.update_item_text(row, 3, to_wstring(name));
}
}
}
}
return true;
}
}
false
}
pub unsafe fn on_column_click(st: &mut AppState, lparam: LPARAM) {
let nmlv = &*(lparam as *const NMLISTVIEW);
let column = nmlv.iSubItem;
if st.sort_column == column {
st.sort_ascending = !st.sort_ascending;
} else {
st.sort_column = column;
st.sort_ascending = true;
}
if let Some(ctrls) = &st.controls {
let context = st as *const AppState as isize;
ctrls.file_list.sort_items(compare_items, context);
ctrls.file_list.set_sort_indicator(st.sort_column, st.sort_ascending);
}
}
pub unsafe fn handle_context_menu(st: &mut AppState, hwnd: HWND, wparam: WPARAM) {
let hwnd_from = wparam as HWND;
if let Some(ctrls) = &st.controls {
if hwnd_from == ctrls.file_list.hwnd() {
let selected = ctrls.file_list.get_selected_indices();
if !selected.is_empty() {
let mut pt: POINT = std::mem::zeroed();
GetCursorPos(&mut pt);
let menu = CreatePopupMenu();
if menu != std::ptr::null_mut() {
let mut any_processing = false;
let mut any_pending = false;
for &idx in &selected {
if let Some(item) = st.batch_items.get(idx as usize) {
match item.status {
BatchStatus::Processing => { any_processing = true; },
BatchStatus::Pending => { any_pending = true; },
_ => {}
}
}
}
if any_processing {
let _ = AppendMenuW(menu, MF_STRING, 1003, to_wstring("Stop").as_ptr());
} else if any_pending {
let _ = AppendMenuW(menu, MF_STRING, 1005, to_wstring("Start Selected").as_ptr());
}
let _ = AppendMenuW(menu, MF_STRING, 1004, to_wstring("Remove").as_ptr());
let _ = AppendMenuW(menu, MF_STRING, 1006, to_wstring("Open File Location").as_ptr());
let _cmd = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_LEFTALIGN, pt.x, pt.y, 0, hwnd, std::ptr::null());
DestroyMenu(menu);
match _cmd {
1003 => { let _ = SendMessageW(hwnd, WM_COMMAND, IDC_BTN_CANCEL as usize, 0); },
1004 => { let _ = SendMessageW(hwnd, WM_COMMAND, IDC_BTN_REMOVE as usize, 0); },
1005 => start_processing(st, hwnd, selected.clone()),
1006 => {
if let Some(&first_idx) = selected.first() {
if let Some(item) = st.batch_items.get(first_idx as usize) {
reveal_path_in_explorer(&item.path);
}
}
},
_ => {}
}
}
}
}
}
}
pub unsafe fn process_hdrop(_hwnd: HWND, hdrop: HDROP, st: &mut AppState) {
let count = DragQueryFileW(hdrop, 0xFFFFFFFF, std::ptr::null_mut(), 0);
let mut paths = Vec::new();
let mut buffer = [0u16; 1024];
for i in 0..count {
let len = DragQueryFileW(hdrop, i, buffer.as_mut_ptr(), 1024);
if len > 0 {
let s = String::from_utf16_lossy(&buffer[..len as usize]);
paths.push(s);
}
}
DragFinish(hdrop);
st.ingest_paths(paths);
}
pub unsafe fn process_clipboard(hwnd: HWND, st: &mut AppState) {
if OpenClipboard(hwnd) == 0 { return; }
if IsClipboardFormatAvailable(15) != 0 {
let hdrop = GetClipboardData(15) as HDROP;
if !hdrop.is_null() {
process_hdrop(hwnd, hdrop, st);
}
} else if IsClipboardFormatAvailable(13) != 0 {
let h_global = GetClipboardData(13);
if !h_global.is_null() {
let ptr = GlobalLock(h_global) as *const u16;
if !ptr.is_null() {
let mut len = 0;
while *ptr.add(len) != 0 {
len += 1;
}
let slice = std::slice::from_raw_parts(ptr, len);
let text = String::from_utf16_lossy(slice);
GlobalUnlock(h_global);
let path_str = text.trim().trim_matches('"').to_string();
if std::path::Path::new(&path_str).exists() {
st.ingest_paths(vec![path_str]);
}
}
}
}
CloseClipboard();
}
pub unsafe extern "system" fn compare_items(lparam1: isize, lparam2: isize, lparam_sort: isize) -> i32 {
let state = &*(lparam_sort as *const AppState);
let id1 = lparam1 as u32;
let id2 = lparam2 as u32;
let item1 = state.batch_items.iter().find(|i| i.id == id1);
let item2 = state.batch_items.iter().find(|i| i.id == id2);
match (item1, item2) {
(Some(i1), Some(i2)) => {
let ord = match state.sort_column {
0 => i1.path.to_lowercase().cmp(&i2.path.to_lowercase()),
2 => format!("{:?}", i1.algorithm).cmp(&format!("{:?}", i2.algorithm)),
3 => format!("{:?}", i1.action).cmp(&format!("{:?}", i2.action)),
4 => i1.logical_size.cmp(&i2.logical_size),
5 => i1.estimated_size.cmp(&i2.estimated_size),
6 => i1.disk_size.cmp(&i2.disk_size),
8 => format!("{:?}", i1.status).cmp(&format!("{:?}", i2.status)),
_ => CmpOrdering::Equal,
};
let result = match ord {
CmpOrdering::Less => -1,
CmpOrdering::Equal => 0,
CmpOrdering::Greater => 1,
};
if state.sort_ascending { result } else { -result }
},
_ => 0
}
}