1use super::super::action_label::ActionLabel;
2use super::super::EntryKind;
3use super::super::{Align, Bus, Event, Hub, Id, RenderQueue, View, ID_FEEDER};
4use crate::context::Context;
5use crate::framebuffer::Framebuffer;
6use crate::geom::Rectangle;
7use crate::settings::{ButtonScheme, IntermKind, Settings};
8use crate::view::toggle::Toggle;
9use crate::view::{EntryId, ToggleEvent};
10use anyhow::Error;
11use std::fs;
12use std::path::Path;
13
14#[derive(Debug, Clone)]
15pub enum ToggleSettings {
16 SleepCover,
18 AutoShare,
20 ButtonScheme,
22}
23
24#[derive(Debug, Clone)]
29pub enum Kind {
30 KeyboardLayout,
32 AutoSuspend,
34 AutoPowerOff,
36
37 Toggle(ToggleSettings),
39
40 LibraryInfo(usize),
42 LibraryName(usize),
44 LibraryPath(usize),
46 LibraryMode(usize),
48 IntermissionSuspend,
50 IntermissionPowerOff,
52 IntermissionShare,
54 SettingsRetention,
56}
57
58impl Kind {
59 pub fn matches_interm_kind(&self, interm_kind: &IntermKind) -> bool {
60 matches!(
61 (self, interm_kind),
62 (Kind::IntermissionSuspend, IntermKind::Suspend)
63 | (Kind::IntermissionPowerOff, IntermKind::PowerOff)
64 | (Kind::IntermissionShare, IntermKind::Share)
65 )
66 }
67}
68
69#[derive(Debug)]
75pub struct SettingValue {
76 id: Id,
78 kind: Kind,
80 rect: Rectangle,
82 children: Vec<Box<dyn View>>,
84 entries: Vec<EntryKind>,
91}
92
93impl SettingValue {
94 pub fn new(
95 kind: Kind,
96 rect: Rectangle,
97 settings: &Settings,
98 fonts: &mut crate::font::Fonts,
99 ) -> SettingValue {
100 let (value, entries, enabled_toggle) = Self::fetch_data_for_kind(&kind, settings);
101
102 let mut setting_value = SettingValue {
103 id: ID_FEEDER.next(),
104 kind,
105 rect,
106 children: vec![],
107 entries,
108 };
109
110 setting_value.children =
111 vec![setting_value.kind_to_child_view(value, enabled_toggle, fonts)];
112
113 setting_value
114 }
115
116 fn kind_to_child_view(
117 &self,
118 value: String,
119 enabled_toggle: Option<bool>,
120 fonts: &mut crate::font::Fonts,
121 ) -> Box<dyn View> {
122 let event = self.create_tap_event();
123
124 match self.kind {
125 Kind::Toggle(ref toggle) => match toggle {
126 ToggleSettings::AutoShare => Box::new(Toggle::new(
127 self.rect,
128 "on",
129 "off",
130 enabled_toggle.expect("enabled bool should be Some for toggle settings"),
131 event.expect("Event should not be None for toggle"),
132 fonts,
133 Align::Right(10),
134 )),
135 ToggleSettings::ButtonScheme => Box::new(Toggle::new(
136 self.rect,
137 ButtonScheme::Natural.to_string().as_str(),
138 ButtonScheme::Inverted.to_string().as_str(),
139 enabled_toggle.expect("enabled bool should be Some for toggle settings"),
140 event.expect("Event should not be None for toggle"),
141 fonts,
142 Align::Right(10),
143 )),
144 ToggleSettings::SleepCover => Box::new(Toggle::new(
145 self.rect,
146 "on",
147 "off",
148 enabled_toggle.expect("enabled bool should be Some for toggle settings"),
149 event.expect("Event should not be None for toggle"),
150 fonts,
151 Align::Right(10),
152 )),
153 },
154 _ => Box::new(ActionLabel::new(self.rect, value, Align::Right(10)).event(event)),
155 }
156 }
157
158 pub fn refresh_from_context(&mut self, context: &Context, rq: &mut RenderQueue) {
163 let (value, entries, _enabled_toggle) =
164 Self::fetch_data_for_kind(&self.kind, &context.settings);
165 self.entries = entries;
166 let event = self.create_tap_event();
167
168 if let Some(action_label) = self.children.get_mut(0) {
169 if let Some(label) = action_label.as_any_mut().downcast_mut::<ActionLabel>() {
170 label.update(&value, rq);
171 label.set_event(event);
172 }
173 }
174 }
175
176 fn fetch_data_for_kind(
177 kind: &Kind,
178 settings: &Settings,
179 ) -> (String, Vec<EntryKind>, Option<bool>) {
180 match kind {
181 Kind::KeyboardLayout => Self::fetch_keyboard_layout_data(settings),
182 Kind::AutoSuspend => Self::fetch_auto_suspend_data(settings),
183 Kind::AutoPowerOff => Self::fetch_auto_power_off_data(settings),
184 Kind::LibraryInfo(index) => Self::fetch_library_info_data(*index, settings),
185 Kind::LibraryName(index) => Self::fetch_library_name_data(*index, settings),
186 Kind::LibraryPath(index) => Self::fetch_library_path_data(*index, settings),
187 Kind::LibraryMode(index) => Self::fetch_library_mode_data(*index, settings),
188 Kind::IntermissionSuspend => {
189 Self::fetch_intermission_data(crate::settings::IntermKind::Suspend, settings)
190 }
191 Kind::IntermissionPowerOff => {
192 Self::fetch_intermission_data(crate::settings::IntermKind::PowerOff, settings)
193 }
194 Kind::IntermissionShare => {
195 Self::fetch_intermission_data(crate::settings::IntermKind::Share, settings)
196 }
197 Kind::SettingsRetention => Self::fetch_settings_retention_data(settings),
198 Kind::Toggle(toggle) => match toggle {
199 ToggleSettings::SleepCover => Self::fetch_sleep_cover_data(settings),
200 ToggleSettings::AutoShare => Self::fetch_auto_share_data(settings),
201 ToggleSettings::ButtonScheme => Self::fetch_button_scheme_data(settings),
202 },
203 }
204 }
205
206 fn fetch_keyboard_layout_data(settings: &Settings) -> (String, Vec<EntryKind>, Option<bool>) {
207 let current_layout = settings.keyboard_layout.clone();
208 let available_layouts = Self::get_available_layouts().unwrap_or_default();
209
210 let entries: Vec<EntryKind> = available_layouts
211 .iter()
212 .map(|layout| {
213 EntryKind::RadioButton(
214 layout.clone(),
215 EntryId::SetKeyboardLayout(layout.clone()),
216 current_layout == *layout,
217 )
218 })
219 .collect();
220
221 (current_layout, entries, None)
222 }
223
224 fn fetch_sleep_cover_data(settings: &Settings) -> (String, Vec<EntryKind>, Option<bool>) {
225 let enabled = settings.sleep_cover;
226 let value = if enabled {
227 "Enabled".to_string()
228 } else {
229 "Disabled".to_string()
230 };
231
232 (value, vec![], Some(settings.sleep_cover))
233 }
234
235 fn fetch_auto_share_data(settings: &Settings) -> (String, Vec<EntryKind>, Option<bool>) {
236 let enabled = settings.auto_share;
237 let value = if enabled {
238 "Enabled".to_string()
239 } else {
240 "Disabled".to_string()
241 };
242
243 (value, vec![], Some(settings.auto_share))
244 }
245
246 fn fetch_button_scheme_data(settings: &Settings) -> (String, Vec<EntryKind>, Option<bool>) {
247 let current_scheme = settings.button_scheme;
248 let value = format!("{:?}", current_scheme);
249
250 (
251 value,
252 vec![],
253 Some(settings.button_scheme == ButtonScheme::Natural),
254 )
255 }
256
257 fn fetch_auto_suspend_data(settings: &Settings) -> (String, Vec<EntryKind>, Option<bool>) {
258 let value = if settings.auto_suspend == 0.0 {
259 "Never".to_string()
260 } else {
261 format!("{:.1}", settings.auto_suspend)
262 };
263
264 (value, vec![], None)
265 }
266
267 fn fetch_auto_power_off_data(settings: &Settings) -> (String, Vec<EntryKind>, Option<bool>) {
268 let value = if settings.auto_power_off == 0.0 {
269 "Never".to_string()
270 } else {
271 format!("{:.1}", settings.auto_power_off)
272 };
273
274 (value, vec![], None)
275 }
276
277 #[inline]
278 fn fetch_settings_retention_data(
279 settings: &Settings,
280 ) -> (String, Vec<EntryKind>, Option<bool>) {
281 let value = settings.settings_retention.to_string();
282
283 (value, vec![], None)
284 }
285
286 fn fetch_library_info_data(
287 index: usize,
288 settings: &Settings,
289 ) -> (String, Vec<EntryKind>, Option<bool>) {
290 if let Some(library) = settings.libraries.get(index) {
291 let value = library.path.display().to_string();
292
293 (value, vec![], None)
294 } else {
295 ("Unknown".to_string(), vec![], None)
296 }
297 }
298
299 fn fetch_library_name_data(
300 index: usize,
301 settings: &Settings,
302 ) -> (String, Vec<EntryKind>, Option<bool>) {
303 if let Some(library) = settings.libraries.get(index) {
304 (library.name.clone(), vec![], None)
305 } else {
306 ("Unknown".to_string(), vec![], None)
307 }
308 }
309
310 fn fetch_library_path_data(
311 index: usize,
312 settings: &Settings,
313 ) -> (String, Vec<EntryKind>, Option<bool>) {
314 if let Some(library) = settings.libraries.get(index) {
315 (library.path.display().to_string(), vec![], None)
316 } else {
317 ("Unknown".to_string(), vec![], None)
318 }
319 }
320
321 fn fetch_library_mode_data(
322 index: usize,
323 settings: &Settings,
324 ) -> (String, Vec<EntryKind>, Option<bool>) {
325 use crate::settings::LibraryMode;
326 let mut mode = LibraryMode::Filesystem;
327
328 if let Some(library) = settings.libraries.get(index) {
329 mode = library.mode;
330 }
331
332 let entries = vec![
333 EntryKind::RadioButton(
334 LibraryMode::Database.to_string(),
335 EntryId::SetLibraryMode(LibraryMode::Database),
336 mode == LibraryMode::Database,
337 ),
338 EntryKind::RadioButton(
339 LibraryMode::Filesystem.to_string(),
340 EntryId::SetLibraryMode(LibraryMode::Filesystem),
341 mode == LibraryMode::Filesystem,
342 ),
343 ];
344 (mode.to_string(), entries, None)
345 }
346
347 fn get_available_layouts() -> Result<Vec<String>, Error> {
348 let layouts_dir = Path::new("keyboard-layouts");
349 let mut layouts = Vec::new();
350
351 if layouts_dir.exists() {
352 for entry in fs::read_dir(layouts_dir)? {
353 let entry = entry?;
354 let path = entry.path();
355
356 if path.extension().and_then(|s| s.to_str()) == Some("json") {
357 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
358 let layout_name = stem
359 .chars()
360 .enumerate()
361 .map(|(i, c)| {
362 if i == 0 {
363 c.to_uppercase().collect::<String>()
364 } else {
365 c.to_string()
366 }
367 })
368 .collect::<String>();
369 layouts.push(layout_name);
370 }
371 }
372 }
373 }
374
375 layouts.sort();
376 Ok(layouts)
377 }
378
379 fn fetch_intermission_data(
380 kind: IntermKind,
381 settings: &Settings,
382 ) -> (String, Vec<EntryKind>, Option<bool>) {
383 use crate::settings::IntermissionDisplay;
384
385 let display = &settings.intermissions[kind];
386
387 let (value, is_logo, is_cover) = match display {
388 IntermissionDisplay::Logo => ("Logo".to_string(), true, false),
389 IntermissionDisplay::Cover => ("Cover".to_string(), false, true),
390 IntermissionDisplay::Image(path) => {
391 let display_name = path
392 .file_name()
393 .and_then(|n| n.to_str())
394 .unwrap_or("Custom")
395 .to_string();
396 (display_name, false, false)
397 }
398 };
399
400 let entries = vec![
401 EntryKind::RadioButton(
402 "Logo".to_string(),
403 EntryId::SetIntermission(kind, IntermissionDisplay::Logo),
404 is_logo,
405 ),
406 EntryKind::RadioButton(
407 "Cover".to_string(),
408 EntryId::SetIntermission(kind, IntermissionDisplay::Cover),
409 is_cover,
410 ),
411 EntryKind::Command(
412 "Custom Image...".to_string(),
413 EntryId::EditIntermissionImage(kind),
414 ),
415 ];
416
417 (value, entries, None)
418 }
419
420 pub fn update(&mut self, value: String, rq: &mut RenderQueue) {
421 if let Some(action_label) = self.children[0].downcast_mut::<ActionLabel>() {
422 action_label.update(&value, rq);
423 }
424 }
425
426 pub fn value(&self) -> String {
427 if let Some(action_label) = self.children[0].downcast_ref::<ActionLabel>() {
428 action_label.value()
429 } else {
430 String::new()
431 }
432 }
433
434 fn create_tap_event(&self) -> Option<Event> {
455 match self.kind {
456 Kind::LibraryInfo(index) => Some(Event::EditLibrary(index)),
457 Kind::LibraryName(_) => Some(Event::Select(EntryId::EditLibraryName)),
458 Kind::LibraryPath(_) => Some(Event::Select(EntryId::EditLibraryPath)),
459 Kind::AutoSuspend => Some(Event::Select(EntryId::EditAutoSuspend)),
460 Kind::AutoPowerOff => Some(Event::Select(EntryId::EditAutoPowerOff)),
461 Kind::SettingsRetention => Some(Event::Select(EntryId::EditSettingsRetention)),
462 Kind::Toggle(ref toggle) => {
463 Some(Event::NewToggle(ToggleEvent::Setting(toggle.clone())))
464 }
465 _ if !self.entries.is_empty() => Some(Event::SubMenu(self.rect, self.entries.clone())),
466 _ => None,
467 }
468 }
469}
470
471impl View for SettingValue {
472 #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _hub, _bus, _rq, _context), fields(event = ?_evt), ret(level=tracing::Level::TRACE)))]
473 fn handle_event(
474 &mut self,
475 _evt: &Event,
476 _hub: &Hub,
477 _bus: &mut Bus,
478 _rq: &mut RenderQueue,
479 _context: &mut Context,
480 ) -> bool {
481 false
482 }
483
484 #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _fb, _fonts), fields(rect = ?_rect)))]
485 fn render(&self, _fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut crate::font::Fonts) {
486 }
487
488 fn rect(&self) -> &Rectangle {
489 &self.rect
490 }
491
492 fn rect_mut(&mut self) -> &mut Rectangle {
493 &mut self.rect
494 }
495
496 fn children(&self) -> &Vec<Box<dyn View>> {
497 &self.children
498 }
499
500 fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
501 &mut self.children
502 }
503
504 fn id(&self) -> Id {
505 self.id
506 }
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512 use crate::context::test_helpers::create_test_context;
513 use crate::gesture::GestureEvent;
514 use crate::settings::Settings;
515 use crate::view::RenderQueue;
516 use std::collections::VecDeque;
517 use std::path::PathBuf;
518 use std::sync::mpsc::channel;
519
520 #[test]
521 fn test_file_chooser_closed_updates_all_intermission_values() {
522 let mut context = create_test_context();
523 let settings = Settings::default();
524 let rect = rect![0, 0, 200, 50];
525
526 let mut suspend_value = SettingValue::new(
527 Kind::IntermissionSuspend,
528 rect,
529 &settings,
530 &mut context.fonts,
531 );
532 let mut power_off_value = SettingValue::new(
533 Kind::IntermissionPowerOff,
534 rect,
535 &settings,
536 &mut context.fonts,
537 );
538 let mut share_value =
539 SettingValue::new(Kind::IntermissionShare, rect, &settings, &mut context.fonts);
540
541 let (hub, _receiver) = channel();
542 let mut bus = VecDeque::new();
543 let mut rq = RenderQueue::new();
544
545 let initial_suspend = suspend_value.value().clone();
546 let initial_power_off = power_off_value.value().clone();
547 let initial_share = share_value.value().clone();
548
549 let test_path = PathBuf::from("/mnt/onboard/test_image.png");
550 let event = Event::FileChooserClosed(Some(test_path.clone()));
551
552 suspend_value.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
553 power_off_value.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
554 share_value.handle_event(&event, &hub, &mut bus, &mut rq, &mut context);
555
556 println!("Initial suspend value: {}", initial_suspend);
557 println!("After event suspend value: {}", suspend_value.value());
558 println!("Initial power_off value: {}", initial_power_off);
559 println!("After event power_off value: {}", power_off_value.value());
560 println!("Initial share value: {}", initial_share);
561 println!("After event share value: {}", share_value.value());
562
563 assert_eq!(suspend_value.value(), initial_suspend);
564 assert_eq!(power_off_value.value(), initial_power_off);
565 assert_eq!(share_value.value(), initial_share);
566 }
567
568 #[test]
569 fn test_intermission_values_update_via_submit_event() {
570 use crate::settings::IntermKind;
571 let mut context = create_test_context();
572 let settings = Settings::default();
573 let rect = rect![0, 0, 200, 50];
574
575 let mut suspend_value = SettingValue::new(
576 Kind::IntermissionSuspend,
577 rect,
578 &settings,
579 &mut context.fonts,
580 );
581 let mut power_off_value = SettingValue::new(
582 Kind::IntermissionPowerOff,
583 rect,
584 &settings,
585 &mut context.fonts,
586 );
587 let mut share_value =
588 SettingValue::new(Kind::IntermissionShare, rect, &settings, &mut context.fonts);
589
590 let mut rq = RenderQueue::new();
591
592 context.settings.intermissions[IntermKind::Suspend] =
593 crate::settings::IntermissionDisplay::Image(PathBuf::from("suspend_image.png"));
594 context.settings.intermissions[IntermKind::PowerOff] =
595 crate::settings::IntermissionDisplay::Image(PathBuf::from("poweroff_image.png"));
596 context.settings.intermissions[IntermKind::Share] =
597 crate::settings::IntermissionDisplay::Image(PathBuf::from("share_image.png"));
598
599 suspend_value.refresh_from_context(&context, &mut rq);
600 power_off_value.refresh_from_context(&context, &mut rq);
601 share_value.refresh_from_context(&context, &mut rq);
602
603 assert_eq!(suspend_value.value(), "suspend_image.png");
604 assert_eq!(power_off_value.value(), "poweroff_image.png");
605 assert_eq!(share_value.value(), "share_image.png");
606 }
607
608 #[test]
609 fn test_keyboard_layout_select_updates_value() {
610 let mut context = create_test_context();
611 let settings = Settings {
612 keyboard_layout: "English".to_string(),
613 ..Default::default()
614 };
615 let rect = rect![0, 0, 200, 50];
616
617 let mut value =
618 SettingValue::new(Kind::KeyboardLayout, rect, &settings, &mut context.fonts);
619 let mut rq = RenderQueue::new();
620
621 context.settings.keyboard_layout = "French".to_string();
622 value.refresh_from_context(&context, &mut rq);
623
624 assert_eq!(value.value(), "French");
625 assert!(!rq.is_empty());
626 }
627
628 #[test]
629 fn test_library_mode_select_updates_value() {
630 use crate::settings::{LibraryMode, LibrarySettings};
631 let mut settings = Settings::default();
632 settings.libraries.clear();
633 let library = LibrarySettings {
634 name: "Test Library".to_string(),
635 path: PathBuf::from("/tmp"),
636 mode: LibraryMode::Filesystem,
637 ..Default::default()
638 };
639 settings.libraries.push(library);
640 let rect = rect![0, 0, 200, 50];
641
642 let mut context = create_test_context();
643 let mut value =
644 SettingValue::new(Kind::LibraryMode(0), rect, &settings, &mut context.fonts);
645 let mut rq = RenderQueue::new();
646
647 assert_eq!(value.value(), "Filesystem");
648
649 context.settings.libraries[0].mode = LibraryMode::Database;
650 value.refresh_from_context(&context, &mut rq);
651
652 assert_eq!(value.value(), "Database");
653 assert!(!rq.is_empty());
654 }
655
656 #[test]
657 fn test_auto_suspend_submit_updates_value() {
658 let mut context = create_test_context();
659 let settings = Settings::default();
660 let rect = rect![0, 0, 200, 50];
661
662 let mut value = SettingValue::new(Kind::AutoSuspend, rect, &settings, &mut context.fonts);
663 let mut rq = RenderQueue::new();
664
665 context.settings.auto_suspend = 15.0;
666 value.refresh_from_context(&context, &mut rq);
667
668 assert_eq!(value.value(), "15.0");
669 assert!(!rq.is_empty());
670 }
671
672 #[test]
673 fn test_auto_power_off_submit_updates_value() {
674 let mut context = create_test_context();
675 let settings = Settings::default();
676 let rect = rect![0, 0, 200, 50];
677
678 let mut value = SettingValue::new(Kind::AutoPowerOff, rect, &settings, &mut context.fonts);
679 let mut rq = RenderQueue::new();
680
681 context.settings.auto_power_off = 7.0;
682 value.refresh_from_context(&context, &mut rq);
683
684 assert_eq!(value.value(), "7.0");
685 assert!(!rq.is_empty());
686 }
687
688 #[test]
689 fn test_library_name_submit_updates_value() {
690 use crate::settings::LibrarySettings;
691 let mut settings = Settings::default();
692 settings.libraries.push(LibrarySettings {
693 name: "Old Name".to_string(),
694 path: PathBuf::from("/tmp"),
695 mode: crate::settings::LibraryMode::Filesystem,
696 ..Default::default()
697 });
698 let rect = rect![0, 0, 200, 50];
699
700 let mut context = create_test_context();
701 let mut value =
702 SettingValue::new(Kind::LibraryName(0), rect, &settings, &mut context.fonts);
703 let mut rq = RenderQueue::new();
704
705 context.settings.libraries[0].name = "New Name".to_string();
706 value.refresh_from_context(&context, &mut rq);
707
708 assert_eq!(value.value(), "New Name");
709 assert!(!rq.is_empty());
710 }
711
712 #[test]
713 fn test_library_path_file_chooser_closed_updates_value() {
714 use crate::settings::LibrarySettings;
715 let mut settings = Settings::default();
716 settings.libraries.push(LibrarySettings {
717 name: "Test Library".to_string(),
718 path: PathBuf::from("/tmp"),
719 mode: crate::settings::LibraryMode::Filesystem,
720 ..Default::default()
721 });
722 let rect = rect![0, 0, 200, 50];
723
724 let mut context = create_test_context();
725 let mut value =
726 SettingValue::new(Kind::LibraryPath(0), rect, &settings, &mut context.fonts);
727 let mut rq = RenderQueue::new();
728
729 let new_path = PathBuf::from("/mnt/onboard/new_library");
730 context.settings.libraries[0].path = new_path.clone();
731 value.refresh_from_context(&context, &mut rq);
732
733 assert_eq!(value.value(), new_path.display().to_string());
734 assert!(!rq.is_empty());
735 }
736
737 #[test]
738 fn test_tap_gesture_on_library_info_emits_edit_event() {
739 use crate::settings::LibrarySettings;
740 let mut settings = Settings::default();
741 settings.libraries.push(LibrarySettings {
742 name: "Test Library".to_string(),
743 path: PathBuf::from("/tmp"),
744 mode: crate::settings::LibraryMode::Filesystem,
745 ..Default::default()
746 });
747 let rect = rect![0, 0, 200, 50];
748
749 let mut context = create_test_context();
750 let value = SettingValue::new(Kind::LibraryInfo(0), rect, &settings, &mut context.fonts);
751 let (hub, _receiver) = channel();
752 let mut bus = VecDeque::new();
753 let mut rq = RenderQueue::new();
754
755 let point = crate::geom::Point::new(100, 25);
756 let event = Event::Gesture(GestureEvent::Tap(point));
757
758 let mut boxed: Box<dyn View> = Box::new(value);
759 crate::view::handle_event(
760 boxed.as_mut(),
761 &event,
762 &hub,
763 &mut bus,
764 &mut rq,
765 &mut context,
766 );
767
768 assert_eq!(bus.len(), 1);
769 if let Some(Event::EditLibrary(index)) = bus.pop_front() {
770 assert_eq!(index, 0);
771 } else {
772 panic!("Expected EditLibrary event");
773 }
774 }
775}