Initial import
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
use crate::ui::pages::PageBuildContext;
|
||||
use crate::ui::toast::show_toast;
|
||||
use crate::ui::widgets::tool_page;
|
||||
use crate::window::{recompute_validation, refresh_save_button};
|
||||
use gtk4::prelude::*;
|
||||
use libadwaita::prelude::*;
|
||||
use mangotune::config::parser::Parser;
|
||||
|
||||
pub fn build_page(ctx: &PageBuildContext) -> gtk4::ScrolledWindow {
|
||||
let (page, body) = tool_page::build_tool_page(
|
||||
"Raw Editor",
|
||||
"Advanced",
|
||||
"Edit the full MangoHud config as plain text when you need absolute control, while still keeping MangoTune validation and save protection in the loop.",
|
||||
&["full config", "advanced", "validation-aware"],
|
||||
);
|
||||
|
||||
tool_page::append_callout(
|
||||
&body,
|
||||
"Power tool",
|
||||
"Raw edits bypass the guided controls",
|
||||
"Use this when you know exactly what you want to change or when MangoHud adds an option before MangoTune has a polished control for it. Validation still runs before save.",
|
||||
Some("tool-callout-warning"),
|
||||
);
|
||||
|
||||
let session_group = tool_page::append_custom_section(
|
||||
&body,
|
||||
"Editor session",
|
||||
"Open the dedicated raw text window, apply edits back into the in-memory config, then save normally once validation is clean.",
|
||||
Some("Manual mode"),
|
||||
);
|
||||
|
||||
let source_row = libadwaita::ActionRow::builder()
|
||||
.title("Current config source")
|
||||
.subtitle(current_config_path(ctx))
|
||||
.build();
|
||||
source_row.add_css_class("control-row");
|
||||
session_group.add(&source_row);
|
||||
|
||||
let stats_row = libadwaita::ActionRow::builder()
|
||||
.title("Current config snapshot")
|
||||
.subtitle(current_stats_label(ctx))
|
||||
.build();
|
||||
stats_row.add_css_class("control-row");
|
||||
session_group.add(&stats_row);
|
||||
|
||||
let launch_row = libadwaita::ActionRow::builder()
|
||||
.title("Open raw text editor")
|
||||
.subtitle("Launch a dedicated editor window with apply and reload actions")
|
||||
.build();
|
||||
launch_row.add_css_class("control-row");
|
||||
|
||||
let button_box = gtk4::Box::new(gtk4::Orientation::Horizontal, 8);
|
||||
let open_button = gtk4::Button::with_label("Open Editor");
|
||||
open_button.add_css_class("suggested-action");
|
||||
let reload_button = gtk4::Button::with_label("Refresh Snapshot");
|
||||
button_box.append(&reload_button);
|
||||
button_box.append(&open_button);
|
||||
|
||||
let ctx_reload = ctx.clone();
|
||||
let source_row_reload = source_row.clone();
|
||||
let stats_row_reload = stats_row.clone();
|
||||
reload_button.connect_clicked(move |_| {
|
||||
source_row_reload.set_subtitle(¤t_config_path(&ctx_reload));
|
||||
stats_row_reload.set_subtitle(¤t_stats_label(&ctx_reload));
|
||||
});
|
||||
|
||||
let ctx_open = ctx.clone();
|
||||
let source_row_open = source_row.clone();
|
||||
let stats_row_open = stats_row.clone();
|
||||
open_button.connect_clicked(move |_| {
|
||||
source_row_open.set_subtitle(¤t_config_path(&ctx_open));
|
||||
stats_row_open.set_subtitle(¤t_stats_label(&ctx_open));
|
||||
open_editor_window(&ctx_open);
|
||||
});
|
||||
|
||||
launch_row.add_suffix(&button_box);
|
||||
session_group.add(&launch_row);
|
||||
|
||||
let workflow_group = tool_page::append_custom_section(
|
||||
&body,
|
||||
"When raw editing helps",
|
||||
"Keep this lightweight: the guided controls should still be your default for most changes.",
|
||||
Some("Quick guidance"),
|
||||
);
|
||||
|
||||
let workflow_note = gtk4::Label::new(Some(
|
||||
"Use the raw editor when you want to paste a known-good MangoHud snippet, touch an uncommon option before MangoTune has a dedicated control for it, or make several text edits at once without jumping across pages.",
|
||||
));
|
||||
workflow_note.set_wrap(true);
|
||||
workflow_note.set_xalign(0.0);
|
||||
workflow_note.add_css_class("dim-label");
|
||||
workflow_group.add(&workflow_note);
|
||||
|
||||
page
|
||||
}
|
||||
|
||||
fn open_editor_window(ctx: &PageBuildContext) {
|
||||
let window = gtk4::Window::builder()
|
||||
.title("MangoTune Raw Editor")
|
||||
.default_width(940)
|
||||
.default_height(680)
|
||||
.transient_for(&ctx.parent_window)
|
||||
.modal(false)
|
||||
.build();
|
||||
window.add_css_class("preferences-shell");
|
||||
|
||||
let (outer, body, footer_row) = tool_page::build_utility_window_shell(
|
||||
"Manual editing",
|
||||
"Raw Config Editor",
|
||||
"Apply updates back into MangoTune when you want the dashboard and page controls to reflect them. Reload discards unsaved raw edits and restores the current in-memory config.",
|
||||
);
|
||||
|
||||
let buffer = gtk4::TextBuffer::new(None::<>k4::TextTagTable>);
|
||||
if let Ok(state) = ctx.state.lock() {
|
||||
buffer.set_text(&Parser::to_string(&state.config));
|
||||
}
|
||||
|
||||
let text_view = gtk4::TextView::builder()
|
||||
.monospace(true)
|
||||
.vexpand(true)
|
||||
.hexpand(true)
|
||||
.buffer(&buffer)
|
||||
.top_margin(12)
|
||||
.bottom_margin(12)
|
||||
.left_margin(12)
|
||||
.right_margin(12)
|
||||
.build();
|
||||
|
||||
let scrolled = gtk4::ScrolledWindow::builder()
|
||||
.child(&text_view)
|
||||
.vexpand(true)
|
||||
.hexpand(true)
|
||||
.build();
|
||||
scrolled.add_css_class("utility-window-scroller");
|
||||
|
||||
let footer = gtk4::Label::new(Some("Line count: 0 | Option count: 0"));
|
||||
footer.add_css_class("dim-label");
|
||||
footer.set_xalign(0.0);
|
||||
|
||||
let spacer = gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
|
||||
spacer.set_hexpand(true);
|
||||
let reload = gtk4::Button::with_label("Reload");
|
||||
let apply = gtk4::Button::with_label("Apply to Workspace");
|
||||
apply.add_css_class("suggested-action");
|
||||
footer_row.append(&spacer);
|
||||
footer_row.append(&reload);
|
||||
footer_row.append(&apply);
|
||||
|
||||
let ctx_apply = ctx.clone();
|
||||
let buffer_apply = buffer.clone();
|
||||
let footer_apply = footer.clone();
|
||||
apply.connect_clicked(move |_| {
|
||||
let start = buffer_apply.start_iter();
|
||||
let end = buffer_apply.end_iter();
|
||||
let text = buffer_apply.text(&start, &end, false).to_string();
|
||||
|
||||
if let Ok(mut state) = ctx_apply.state.lock() {
|
||||
let parsed = Parser::parse_str(&text, state.config.path.clone());
|
||||
state.config = parsed;
|
||||
state.config.dirty = true;
|
||||
state.dirty = true;
|
||||
}
|
||||
|
||||
update_footer(&footer_apply, &text);
|
||||
recompute_validation(&ctx_apply.state);
|
||||
refresh_save_button(&ctx_apply.state, &ctx_apply.save_button);
|
||||
show_toast(
|
||||
&ctx_apply.toast_overlay,
|
||||
"Applied raw text changes to the workspace",
|
||||
);
|
||||
});
|
||||
|
||||
let ctx_reload = ctx.clone();
|
||||
let buffer_reload = buffer.clone();
|
||||
let footer_reload = footer.clone();
|
||||
reload.connect_clicked(move |_| {
|
||||
if let Ok(state) = ctx_reload.state.lock() {
|
||||
let text = Parser::to_string(&state.config);
|
||||
buffer_reload.set_text(&text);
|
||||
update_footer(&footer_reload, &text);
|
||||
}
|
||||
});
|
||||
|
||||
let initial = buffer.text(&buffer.start_iter(), &buffer.end_iter(), false);
|
||||
update_footer(&footer, &initial);
|
||||
|
||||
body.append(&scrolled);
|
||||
body.append(&footer);
|
||||
window.set_child(Some(&outer));
|
||||
window.present();
|
||||
}
|
||||
|
||||
fn current_config_path(ctx: &PageBuildContext) -> String {
|
||||
ctx.state
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|state| {
|
||||
state
|
||||
.config
|
||||
.path
|
||||
.as_ref()
|
||||
.map(|path| path.display().to_string())
|
||||
})
|
||||
.unwrap_or_else(|| "Unsaved session".to_string())
|
||||
}
|
||||
|
||||
fn current_stats_label(ctx: &PageBuildContext) -> String {
|
||||
ctx.state
|
||||
.lock()
|
||||
.ok()
|
||||
.map(|state| {
|
||||
let text = Parser::to_string(&state.config);
|
||||
let (line_count, option_count) = count_text_stats(&text);
|
||||
format!("{line_count} lines | {option_count} active option rows")
|
||||
})
|
||||
.unwrap_or_else(|| "Unavailable".to_string())
|
||||
}
|
||||
|
||||
fn update_footer(footer: >k4::Label, text: &str) {
|
||||
let (line_count, option_count) = count_text_stats(text);
|
||||
footer.set_text(&format!(
|
||||
"Line count: {line_count} | Option count: {option_count}"
|
||||
));
|
||||
}
|
||||
|
||||
fn count_text_stats(text: &str) -> (usize, usize) {
|
||||
let line_count = text.lines().count();
|
||||
let option_count = text
|
||||
.lines()
|
||||
.filter(|line| {
|
||||
let trimmed = line.trim();
|
||||
!trimmed.is_empty() && !trimmed.starts_with('#')
|
||||
})
|
||||
.count();
|
||||
(line_count, option_count)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::count_text_stats;
|
||||
|
||||
#[test]
|
||||
fn counts_only_active_option_lines() {
|
||||
let text = "# comment\nfps\n\nposition=top-left\n";
|
||||
assert_eq!(count_text_stats(text), (4, 2));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user