use convert_case::{Case, Casing};
use regex::Regex;
use roxmltree::{Document, Node};
use std::ffi::OsStr;
use std::fs::{self, File};
use std::io::{self, Read, Write};
use std::path::PathBuf;
type IO<T> = Result<T, Box<dyn std::error::Error>>;
mod config;
pub use config::Config;
struct Field<'a> {
widget: &'a str,
name: &'a str,
name_rs: String,
}
impl<'a> Field<'a> {
fn parse<'input>(node: Node<'a, 'input>, config: &Config) -> Option<Self> {
let name = node.attribute("name")?;
if !config.all
&& (name == "label" || name.starts_with("label_") || name.ends_with("_label"))
{
return None;
}
let widget = match node.tag_name().name() {
"widget" => node.attribute("class"),
"action" => Some("QAction"),
"layout" if config.all || !node.has_children() => node.attribute("class"),
_ => None,
}?;
if !config.all && widget == "QGroupBox" && node.has_children() {
None
} else {
Some(Field {
widget,
name_rs: name.to_case(Case::Snake),
name,
})
}
}
}
pub struct Ruic<W> {
writer: W,
config: Config,
trimmer: Regex,
}
impl<W: Write> Ruic<W> {
pub fn new(writer: W, config: Config) -> Self {
Self {
writer,
config,
trimmer: Regex::new(r"\n\s*").unwrap(),
}
}
pub fn process(&mut self) -> io::Result<()> {
let path = self.config.path.clone();
writeln!(
self.writer,
r#"// This file is automatically generated.
use cpp_core::{{CastInto, Ptr}};
use qt_core::{{QBox, QPtr}};
use qt_ui_tools::QUiLoader;
use qt_widgets::*;
"#
)?;
if path.is_dir() {
self.process_dir(path)
} else {
if let Err(e) = self.process_file(path) {
eprintln!("{}", e);
}
Ok(())
}
}
fn process_dir(&mut self, path: PathBuf) -> io::Result<()> {
for entry in fs::read_dir(path)? {
let entry = entry?;
let subpath = entry.path();
let metadata = entry.metadata()?;
if !self.config.no_recursive && metadata.is_dir() {
self.process_dir(subpath)?;
} else if subpath
.extension()
.and_then(OsStr::to_str)
.unwrap_or("")
.to_lowercase()
== "ui"
{
if let Err(e) = self.process_file(subpath) {
eprintln!("{}", e)
}
}
}
Ok(())
}
fn process_file(&mut self, path: PathBuf) -> IO<()> {
let mut src = String::new();
File::open(&path)?.read_to_string(&mut src)?;
src = self.trimmer.replace_all(&src, "").into_owned();
let doc = Document::parse(&src)?;
let mut els = doc
.root_element()
.children()
.find(|el| el.tag_name().name() == "widget")
.ok_or_else(|| format!("{}: invalid format", path.to_string_lossy()))?
.descendants()
.filter_map(|node| Field::parse(node, &self.config));
let base = els
.next()
.ok_or_else(|| format!("{}: invalid format", path.to_string_lossy()))?;
let fields: Vec<_> = els.collect();
writeln!(
self.writer,
"#[derive(Debug)]
pub struct {}{} {{
pub widget: QBox<{}>,",
base.name, self.config.suffix, base.widget
)?;
for field in &fields {
writeln!(
self.writer,
" pub {}: QPtr<{}>,",
field.name_rs, field.widget
)?;
}
writeln!(
self.writer,
r#"}}
impl {}{} {{
pub fn load<P: CastInto<Ptr<QWidget>>>(parent: P) -> Self {{
unsafe {{
let loader = QUiLoader::new_0a();
loader.set_language_change_enabled(true);
let bytes = {:?}.as_bytes();
let widget = loader.load_bytes_with_parent(bytes, parent);
assert!(!widget.is_null(), "invalid ui file");
Self {{"#,
base.name, self.config.suffix, src
)?;
for field in &fields {
writeln!(
self.writer,
r#" {}: widget.find_child("{}").unwrap(),"#,
field.name_rs, field.name
)?;
}
if base.widget == "QWidget" {
writeln!(self.writer, " widget,")?;
} else {
writeln!(
self.writer,
" widget: QBox::from_q_ptr(widget.into_q_ptr().dynamic_cast()),"
)?;
}
writeln!(
self.writer,
r#" }}
}}
}}
}}"#
)?;
Ok(())
}
}