1use crate::{Element, Widget};
22
23const SCRIPT: &str = "get_pv_name.py";
25
26const SYMBOL_FRAMES: &[(&str, i64)] = &[
30 ("mks937aImg-symbol-34.png", 16),
31 ("mks937bImg-symbol-34.png", 16),
32 ("mks937aPirg-symbol-34.png", 16),
33 ("mks937bPirg-symbol-34.png", 16),
34 ("digitelMpcIonp-symbol-34.png", 9),
35 ("vacuumValve-symbol-34.png", 5),
36];
37
38const DEVICE_MACRO: &str = "device";
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub enum LinkSets {
45 Text,
47 Pv,
50}
51
52#[derive(Clone, Debug, PartialEq)]
54pub struct GroupLink {
55 pub pv_name: String,
57 pub sets: LinkSets,
58 pub frames: Option<i64>,
61}
62
63impl Widget {
64 pub fn group_link(&self) -> Option<GroupLink> {
66 let el = self.get("group_link")?;
67 let pv_name = el.child_text("pv_name")?.trim().to_string();
68 let sets = match el.child_text("sets").map(str::trim) {
69 Some("text") => LinkSets::Text,
70 _ => LinkSets::Pv,
71 };
72 let frames = el.child_text("frames").and_then(|f| f.trim().parse().ok());
73 (!pv_name.is_empty()).then_some(GroupLink {
74 pv_name,
75 sets,
76 frames,
77 })
78 }
79}
80
81impl GroupLink {
82 pub fn point_at(&self, props: &mut Element, device: &str) {
85 let mut set = |name: &str, value: String| props.set_child(Element::with_text(name, value));
86 match self.sets {
87 LinkSets::Text => set("text", device.to_string()),
88 LinkSets::Pv => {
89 set("pv_name", format!("{device}:STA"));
90 set("name", device.to_string());
91 set("tooltip", "$(pv_name)\n$(pv_value)".to_string());
94 if let Some(macros) = props
95 .child_mut("actions")
96 .and_then(|a| a.children.first_mut())
97 .filter(|a| a.attr("type") == Some("open_display"))
98 .map(|a| {
99 if a.child("macros").is_none() {
100 a.children.push(Element::new("macros"));
101 }
102 a.child_mut("macros").unwrap()
103 })
104 {
105 macros.set_child(Element::with_text(DEVICE_MACRO, device));
106 }
107 }
108 }
109 }
110}
111
112pub fn group_device(link: &str) -> Option<String> {
117 let pv = link.split_whitespace().next()?;
118 let (scheme, name) = match pv.split_once("://") {
119 Some((scheme, name)) => (&pv[..scheme.len() + 3], name),
120 None => ("", pv),
121 };
122 let device = name.split(':').next().unwrap_or("");
123 (!device.is_empty()).then(|| format!("{scheme}{device}"))
124}
125
126pub fn group_frame(status: Option<i64>, top: i64) -> i64 {
129 match status.map(|s| s + 1) {
130 Some(frame) if (1..=top).contains(&frame) => frame,
131 _ => 0,
132 }
133}
134
135pub(crate) fn from_opi(el: &Element) -> Option<Element> {
139 let path = el
140 .child("scripts")?
141 .children
142 .iter()
143 .filter(|p| p.name == "path")
144 .find(|p| {
145 let path = p.attr("pathString").unwrap_or("");
146 path.rsplit(['/', '\\']).next() == Some(SCRIPT)
147 })?;
148 let link = path.children.iter().filter(|p| p.name == "pv").nth(1)?;
149 let widget_type = el.child_text("widget_type").unwrap_or("").trim();
150 let mut out =
151 Element::new("group_link").with_child(Element::with_text("pv_name", link.text.trim()));
152 if widget_type == "Label" {
153 return Some(out.with_child(Element::with_text("sets", "text")));
154 }
155 out.children.push(Element::with_text("sets", "pv"));
156 if widget_type == "Edm Symbol Widget" {
157 let image = el.child_text("image_file").unwrap_or("").trim();
158 let image = image.rsplit(['/', '\\']).next().unwrap_or("");
159 if let Some((_, top)) = SYMBOL_FRAMES.iter().find(|(f, _)| *f == image) {
160 out.children
161 .push(Element::with_text("frames", top.to_string()));
162 }
163 }
164 Some(out)
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn link_fields_name_the_group() {
173 let device = |l: &str| group_device(l);
174 assert_eq!(
175 device("SR03S-VA-GIONP-01:STA NPP NMS").as_deref(),
176 Some("SR03S-VA-GIONP-01")
177 );
178 assert_eq!(
179 device("SR03S-VA-GVALV-01:STA NPP MS").as_deref(),
180 Some("SR03S-VA-GVALV-01")
181 );
182 assert_eq!(
183 device("SR03S-VA-GPIRG-01:CTL:SP NPP MS").as_deref(),
184 Some("SR03S-VA-GPIRG-01")
185 );
186 assert_eq!(
187 device(" BL01T-VA-IMG-01 CA").as_deref(),
188 Some("BL01T-VA-IMG-01")
189 );
190 assert_eq!(device("loc://G1:STA").as_deref(), Some("loc://G1"));
191 assert_eq!(device(""), None);
192 assert_eq!(device(" "), None);
193 assert_eq!(device(":STA NPP"), None);
194 }
195
196 #[test]
197 fn status_picks_a_frame_within_the_strip() {
198 assert_eq!(group_frame(Some(0), 9), 1);
199 assert_eq!(group_frame(Some(8), 9), 9);
200 assert_eq!(group_frame(Some(9), 9), 0);
202 assert_eq!(group_frame(Some(-1), 9), 0);
203 assert_eq!(group_frame(Some(5), 5), 0);
204 assert_eq!(group_frame(None, 16), 0);
205 }
206
207 #[test]
208 fn point_at_sets_what_the_script_does() {
209 let props = |xml: &str| {
210 let doc = roxmltree::Document::parse(xml).unwrap();
211 Element::from_node(doc.root_element())
212 };
213 let mut sym = props(
214 r#"<widget><name></name><pv_name></pv_name><actions>
215 <action type="open_display"><file>g.opi</file><macros><device>$(name)</device></macros></action>
216 </actions></widget>"#,
217 );
218 let link = GroupLink {
219 pv_name: "L".into(),
220 sets: LinkSets::Pv,
221 frames: Some(9),
222 };
223 link.point_at(&mut sym, "SR03S-VA-GIONP-01");
224 let w = Widget::from_element(sym);
225 assert_eq!(w.text("pv_name"), Some("SR03S-VA-GIONP-01:STA"));
226 assert_eq!(w.name(), "SR03S-VA-GIONP-01");
227 let crate::Action::OpenDisplay { macros, .. } = &w.actions()[0] else {
228 panic!("not an open display action")
229 };
230 assert_eq!(macros.get("device"), Some("SR03S-VA-GIONP-01"));
231
232 let mut label = props("<widget><text>SRXXC-VA-GXXX-YY</text></widget>");
233 let link = GroupLink {
234 sets: LinkSets::Text,
235 frames: None,
236 ..link
237 };
238 link.point_at(&mut label, "SR03S-VA-GIONP-01");
239 let w = Widget::from_element(label);
240 assert_eq!(w.text("text"), Some("SR03S-VA-GIONP-01"));
241 assert!(w.get("pv_name").is_none());
242 }
243}