question_bank_creator/lib.rs
1
2/*
3 // TODO: Font Libre Baskerville, 14 pt. -- next iteration.
4 // TODO: All this needs to be user modifiable. -- next iteration.
5 // TODO: Goal is for WYSIWIG. -- in the far, far future.
6
7 // TODO: Add second line to the title containing the associated textbook text.
8 // TODO: Question display should show calculated values for the variables
9 // rather than the variable ID. Maybe highlight the values so
10 // that the variable can be easily located. May also want to set up
11 // a hover over the value/variable to show the variable's value/variable name.
12
13 // TODO: Refine the implementation of how you use LastDirUsed. It is currently
14 // inconsistent in how it is applied. Check every instance where a
15 // file is read or written -- primarily in the *_read() and *_save()
16 // functions. Also, check the three utility crates. The
17 // file_browse_save_fltr() function will need attention.
18
19 */ // TODO's
20
21use fltk::window;
22use fltk::window::Window;
23use crate::banks::Bank;
24use fltk::app::App;
25use fltk::group::Scroll;
26use fltk::prelude::{GroupExt, WidgetBase, WidgetExt};
27use fltk::text::{TextDisplay, TextEditor};
28use fltk::utils::oncelock::Lazy;
29use std::sync::Mutex;
30
31// region Global Constants
32
33/// The current iteration of the program being worked on.
34///
35pub const DEVELOPMENT_VERSION: &str = "Question Bank Rebuild 4";
36/// The title of the project.
37///
38pub const PROGRAM_TITLE: &str = "Question Bank Creator";
39/// The current version..
40///
41pub const VERSION: &str = "0.29.8"; // Note: Versioning, while semantic in format, is decimal in nature.
42
43
44// Note: The constants below are here during development. They will be moved to
45// a config file once the program is more stable.
46
47/// The default folder where data is saved.
48///
49pub const DATA_GENERAL_FOLDER: &str = "/home/jtreagan/programming/mine/qbnk_rb7/src/qbnk_data";
50/// The default folder for saving Lists.
51///
52pub const LIST_DIR: &str = "/home/jtreagan/programming/mine/qbnk_rb7/src/qbnk_data/lists";
53/// The default folder for saving Variables.
54///
55pub const VARIABLE_DIR: &str = "/home/jtreagan/programming/mine/qbnk_rb7/src/qbnk_data/variables";
56/// The default folder for saving Banks.
57///
58pub const BANK_DIR: &str = "/home/jtreagan/programming/mine/qbnk_rb7/src/qbnk_data/banks";
59
60/// Default height of the question display.
61///
62pub const QDISP_HEIGHT: i32 = 150;
63/// Default width of the scrollbar group.
64///
65pub const SCROLLBAR_WIDTH: i32 = 15;
66// endregion
67
68//region Global Variables
69/// Contains the question Bank that is currently being edited.
70///
71pub static CURRENT_BANK: Lazy<Mutex<Bank>> = Lazy::new(|| Mutex::new(Bank::new()));
72/// Contains the last directory path that was used.
73///
74pub static LAST_DIR_USED: Lazy<Mutex<String>> = Lazy::new(|| Mutex::new(String::new()));
75/// Holds the currently running FLTK App.
76///
77pub static APP_FLTK: Lazy<Mutex<App>> = Lazy::new(|| Mutex::new(App::default()));
78/// Holds the FLTK widgets currently being used.
79///
80pub static WIDGETS: Lazy<Mutex<Wdgts>> = Lazy::new(|| Mutex::new(Wdgts::new()));
81//endregion
82
83// region Global Structs & Enums
84/// Struct that holds the primary window's widgets.
85///
86pub struct Wdgts {
87 pub prim_win: Window,
88 pub title_editbox: TextEditor,
89 pub scroll: Scroll,
90 pub qstn_boxes: Vec<TextDisplay>,
91}
92
93impl Wdgts {
94 pub fn new() -> Self {
95 let prim_win = window::Window::new(1100, 200, 825, 900, PROGRAM_TITLE)
96 .with_size(825, 900)
97 .with_pos(1100, 200);
98 prim_win.end();
99
100 Self {
101 prim_win,
102 title_editbox: TextEditor::default(),
103 scroll: Scroll::default(),
104 qstn_boxes: Vec::new(),
105 }
106 }
107}
108
109impl Clone for Wdgts {
110 fn clone(&self) -> Self {
111 Self {
112 prim_win: self.prim_win.clone(),
113 title_editbox: self.title_editbox.clone(),
114 scroll: self.scroll.clone(),
115 qstn_boxes: self.qstn_boxes.clone(),
116 }
117 }
118}
119
120// endregioncar
121
122/// Holds the TypeWrapper enum.
123///
124pub mod global {
125 // todo: Do you really need this? I don't think you can
126 // eliminate it, but maybe you can move it to the
127 // global structs section and eliminate the global module.
128
129 use serde::{Deserialize, Serialize};
130
131 #[derive(Debug, Serialize, Deserialize, Clone)]
132 pub enum TypeWrapper {
133 Alphanum(String),
134 Letter(char),
135 Integer(i64),
136 Floating(f64),
137 }
138} // End global module
139
140/// Functions that deal with the Bank struct.
141///
142pub mod banks {
143 use crate::misc::{make_question_boxes, make_scrollgroup, make_title_txtedtr};
144 use crate::{questions::*, Wdgts, APP_FLTK, BANK_DIR, CURRENT_BANK, LAST_DIR_USED, WIDGETS};
145 use fltk::prelude::{DisplayExt, GroupExt, WidgetExt};
146 use fltk::text::TextBuffer;
147 use lib_file::{file_fltk::*, file_mngmnt::file_read_to_string};
148 use lib_myfltk::{input_fltk::*};
149 use serde::{Deserialize, Serialize};
150 use std::{fs::File, io::Write};
151
152 //region Struct Section
153
154 /// The outermost of the three structs QBC is built around.
155 /// Note that the field `question_vec: Vec<Question>` is a vector
156 /// of type `Question`. `Question` is the second layer in the
157 /// three-struct nest.
158 #[derive(Debug, Serialize, Deserialize)]
159 pub struct Bank {
160 pub bank_title: String, // Also used for file name.
161 pub associated_textbook: String, // Use "" if no text being used.
162 pub question_vec: Vec<Question>,
163 }
164
165 impl Bank {
166
167 /// Initialize a new question Bank.
168 pub fn new() -> Bank {
169 Self {
170 bank_title: "No Bank Loaded".to_string(),
171 associated_textbook: "Untitled Textbook".to_string(),
172 question_vec: Vec::new(),
173 }
174 }
175 }
176
177 impl Clone for Bank {
178
179 /// Clone a question Bank.
180 fn clone(&self) -> Self {
181 Self {
182 bank_title: self.bank_title.clone(),
183 associated_textbook: self.associated_textbook.clone(),
184 question_vec: self.question_vec.clone(), // Vec itself does implement Clone.
185 }
186 }
187 }
188
189 //endregion
190
191 /// Refreshes the contents of the widgets that are currently being displayed
192 /// and edited in the primary window.
193 pub fn bnk_refresh_widgets() {
194 let mut wdgts: Wdgts;
195 {
196 wdgts = WIDGETS.lock().unwrap().clone();
197 }
198
199 wdgts.scroll.clear();
200 wdgts.scroll.redraw();
201
202 // Create/refresh widgets based on data in CURRENT_BANK.
203 make_title_txtedtr();
204 make_scrollgroup();
205 make_question_boxes();
206 }
207
208 /// Creates a brand new question bank.
209 ///
210 pub fn bnk_create() {
211
212 // todo: Check for bank in memory before proceeding.
213
214 let app;
215 {
216 app = APP_FLTK.lock().unwrap().clone();
217 } // Access the main app.
218
219 // Input values into the struct fields.
220 let mut newbank = Bank::new();
221 newbank.bank_title = input_string(&app, "Please enter the bank's title.", 300, 90);
222 newbank.associated_textbook = input_string(&app, "If you are using an associated textbook \n please enter its info. \n Press Enter if no textbook is being used.",
223 800, 200);
224 // Pass the new bank into CURRENT_BANK
225 {
226 *CURRENT_BANK.lock().unwrap() = newbank.clone();
227 }
228
229 // Save and display the bank.
230 bnk_save();
231
232 // Create widgets based on the data in CURRENT_BANK.
233 //make_title_txtedtr();
234 //make_scrollgroup();
235 //make_question_boxes();
236 }
237
238 /// Reads a question bank's data from a file.
239 ///
240 pub fn bnk_read() {
241
242 // todo: Check for bank in memory before proceeding.
243
244 // region Set up directories.
245
246 // Setup proper directory and read the file.
247 println!("\n Please choose the Bank file to be read.");
248
249 let usepath: String;
250
251 { // Global variable scope is restricted to avoid Mutex lock.
252 if LAST_DIR_USED.lock().unwrap().clone() == "" {
253 *LAST_DIR_USED.lock().unwrap() = BANK_DIR.to_string().clone();
254 }
255 let lastdir = LAST_DIR_USED.lock().unwrap().clone();
256 usepath = file_fullpath(&lastdir);
257 *LAST_DIR_USED.lock().unwrap() = usepath.clone(); // Update LAST_DIR_USED
258 }
259 //endregion
260
261 // region Read the chosen file.
262 let usebank: Bank;
263 match file_read_to_string(&usepath) {
264 Ok(contents) => {
265 usebank = serde_json::from_str(&contents).unwrap();
266 *CURRENT_BANK.lock().unwrap() = usebank.clone();
267 }
268 // TODO: Fix error handling. This is terrible. See thread in forum at
269 // https://users.rust-lang.org/t/help-understanding-never-used-warning/125562/2
270 Err(err) => {
271 eprintln!("\n Error reading the file: {} \n", err);
272 panic!("\n Error reading the file. \n");
273 }
274 }
275 // endregion
276
277 {
278 *CURRENT_BANK.lock().unwrap() = usebank.clone();
279 } // Pass the new bank into CURRENT_BANK
280 }
281
282 /// Refreshes the contents of the title box of a bank's display.
283 ///
284 pub fn bnk_refresh_title() {
285 let usebank: Bank;
286 let mut wdgts: Wdgts;
287 {
288 usebank = CURRENT_BANK.lock().unwrap().clone();
289 wdgts = WIDGETS.lock().unwrap().clone();
290 } // Access global structs.
291
292 let mut buf = TextBuffer::default();
293 buf.set_text(usebank.bank_title.as_str()); // Uses the title from the current bank.
294 wdgts.title_editbox.set_buffer(buf);
295
296 // let title_text = // There is likely to be a use for title_text in the future.
297 wdgts.title_editbox.buffer().unwrap().text();
298 }
299
300 /// Prepares a Bank struct for saving.
301 ///
302 pub fn bnk_save() {
303 // region TODO's
304 // TODO: Find way to insert bank title into the save-file dialog.
305 // TODO: Find way to append correct extension automatically.
306 // endregion
307
308 if LAST_DIR_USED.lock().unwrap().clone() == "" {
309 *LAST_DIR_USED.lock().unwrap() = BANK_DIR.to_string().clone();
310 } // If no bank loaded, use default.
311
312 let lastdir: String;
313 {
314 lastdir = LAST_DIR_USED.lock().unwrap().clone();
315 }
316
317 println!("Please choose a directory and file name for saving. \n");
318 let usepath = file_browse_save_fltr(&lastdir, "*.bnk");
319
320 {
321 *LAST_DIR_USED.lock().unwrap() = usepath.clone();
322 } // Set the last used directory.
323
324 bnk_save_as_json(&usepath);
325 }
326
327 /// Saves a Bank struct to a file in json format.
328 ///
329 pub fn bnk_save_as_json(usepath: &String) {
330
331 let usebank;
332 {
333 usebank = CURRENT_BANK.lock().unwrap().clone();
334 }
335
336 let bnk_as_json = serde_json::to_string(&usebank).unwrap(); // Convert bank to json string.
337
338 let mut file = File::create(usepath).expect("Could not create file!");
339
340 file.write_all(bnk_as_json.as_bytes())
341 .expect("Cannot write to the file!");
342 }
343
344 /// Recalculates the variables in the questions of a Bank.
345 ///
346 pub fn bnk_recalc() {
347 let usebank;
348 {
349 usebank = CURRENT_BANK.lock().unwrap().clone();
350 }
351
352 println!("\n Recalculate variables in a bank. Not yet implemented. \n");
353 println!("\n The current bank is: \n {:?} \n", usebank);
354
355 // Read the passed bank
356 // for q in usebank.question_vec.iter() {
357 // step thru and recalc each variable
358 // in each question.
359 // }
360 }
361
362 /*
363 pub fn test_globalbank_access() {
364 let testbank = CURRENT_BANK.lock().unwrap().clone();
365 println!("\n The test bank is: \n {:?} \n", testbank);
366 }
367
368
369 pub fn temp_listwindows(app: &App) {
370
371 // Retrieve all open child windows
372 let windows = app::windows();
373 println!("\n Currently Open Child Windows: \n");
374
375 for item in windows.iter() {
376 let winlabel = item.label();
377 println!("\n Window: {} \n", winlabel);
378 }
379}
380*/ // delete later
381
382} // End bank module
383
384/// Functions that deal with the Question struct.
385///
386pub mod questions {
387 use crate::banks::{bnk_refresh_widgets, bnk_save, Bank};
388 use crate::variable::*;
389 use crate::{APP_FLTK, CURRENT_BANK, LAST_DIR_USED, VARIABLE_DIR};
390 use fltk::app::set_font_size;
391 use fltk::enums::{Color, Shortcut};
392 use fltk::prelude::{DisplayExt, GroupExt, MenuExt, WidgetBase, WidgetExt, WindowExt};
393 use fltk::text::{TextBuffer, TextEditor};
394 use fltk::{app, menu, text, window};
395 use lib_file::file_fltk::*;
396 use lib_file::file_mngmnt::{file_get_dir_list};
397 use lib_myfltk::fltkutils::*;
398 use lib_myfltk::input_fltk::{input_string, input_strvec};
399 use lib_utils::utilities::*;
400 use serde::{Deserialize, Serialize};
401
402 //region Struct Section
403
404 /// The second layer of the three structs QBC is built around.
405 /// Note that the field `var_vec: Vec<Variable>` is a vector
406 /// of type `Variable`. `Variable` is the third and innermost layer in the
407 /// three-struct nest.
408 #[derive(Debug, Serialize, Deserialize, Clone)]
409 pub struct Question {
410 pub qtext: String,
411 pub var_dirpath: String,
412 pub var_vec: Vec<Variable>,
413 pub answer: String,
414 pub objectives: Vec<String>,
415 pub prereqs: Vec<String>,
416 }
417
418 impl Question {
419
420 /// Initialize a new Question.
421 fn new() -> Question {
422 Self {
423 qtext: "Please enter the text of your question. Use real values. You will replace those values with variables later. Be sure to delete these instructions before entering your question text.".to_string(),
424 var_dirpath: VARIABLE_DIR.to_string(),
425 var_vec: Vec::new(),
426 answer: "Answer".to_string(),
427 objectives: Vec::new(),
428 prereqs: Vec::new(),
429 }
430 }
431 } // End Question impl
432 //endregion
433
434 /// Create a new question.
435 ///
436 pub fn qst_create() {
437 // todo: The answer will need to parse inserted variables.
438
439 let mut newquest = Question::new();
440
441 // region Question data entry
442
443 let nowtext = qst_editor(newquest.qtext.as_str(), "Question Editor");
444 newquest.qtext = nowtext.clone();
445
446 // Pull the flagged variables from the text and push them to the variable vector.
447 qst_fill_varvec_parsetext(&mut newquest);
448
449 // Answer will eventually need to be calculated.
450
451 let app;
452 {
453 app = APP_FLTK.lock().unwrap().clone();
454 }
455
456 newquest.answer = input_string(&app, "Please input the question's answer: ", 790, 300);
457 newquest.objectives = input_strvec(&app, "Please enter the question objectives: ", 790, 300);
458 newquest.prereqs = input_strvec(&app, "Please enter the question prerequisites: ", 790, 300);
459 // endregion
460
461// region Save and store the data
462 let mut usebank: Bank;
463 {
464 usebank = CURRENT_BANK.lock().unwrap().clone();
465 } // Access the global Bank variable
466 usebank.question_vec.push(newquest); // Store the new question in the bank
467 { // Pass the modified bank into the global variable.
468 *CURRENT_BANK.lock().unwrap() = usebank.clone();
469 }
470 bnk_save();
471// endregion
472
473 }
474
475 /// Edit a question.
476 ///
477 pub fn qst_edit(qst_idx: usize) {
478
479 let app;
480 let mut usebank;
481 {
482 app = APP_FLTK.lock().unwrap().clone();
483 usebank = CURRENT_BANK.lock().unwrap().clone();
484 } // Access global variables.
485
486 let mut editqst = usebank.question_vec.get(qst_idx).unwrap().clone();
487
488 let nowtext = qst_editor(editqst.qtext.as_str(), "Question Editor");
489 editqst.qtext = nowtext.clone();
490
491 // Pull the flagged variables from the text and push them to the variable vector.
492 qst_fill_varvec_parsetext(&mut editqst); // Need to clear the vector first.
493
494 // Answer will eventually need to be calculated.
495
496 editqst.answer = input_string(&app,"Please input the question's answer: ", 790, 300);
497 editqst.objectives = input_strvec(&app,"Please enter the question objectives: ", 790, 300);
498 editqst.prereqs = input_strvec(&app,"Please enter the question prerequisites: ", 790, 300);
499
500 // Push the question to the vector in the bank and save the bank.
501 //let mut usebank = CURRENT_BANK.lock().unwrap();
502
503 // todo: This won't work. push() appends to the end of the vector. Fix it.
504 usebank.question_vec.push(editqst.clone());
505 bnk_save();
506 bnk_refresh_widgets();
507 }
508
509
510 /// // Is this necessary now?
511 ///
512 pub fn qst_chooseqst() -> Question {
513
514 // TODO: Instead of trying to put the whole text of the question
515 // body in the radio button, number each question in the
516 // bank display and choose by the question number.
517
518 // Note: This function may not be necessary.
519
520 let mut usevec: Vec<String> = Vec::new();
521
522 let usebank;
523 {
524 usebank = CURRENT_BANK.lock().unwrap().clone();
525 }
526
527 for item in usebank.question_vec.iter() {
528 usevec.push(item.qtext.clone());
529 }
530
531 let usequest = fltk_radio_lightbtn_menu(&usevec, "");
532 let mut editquest = Question::new();
533
534 for item in usebank.question_vec.iter() {
535 if item.qtext == usequest {
536 editquest = item.clone();
537 }
538 }
539
540 editquest
541 }
542
543
544 /// Parses the text of a question looking for flags that mark
545 /// variables. Separates out the flagged variables, reading the
546 /// data into Variable structs and saving them in the
547 /// `quest.var_vec` vector field of the current question.
548 pub fn qst_fill_varvec_parsetext(quest: &mut Question) {
549 // region Create a vector of the variable names that have been flagged in the text.
550 let mut usevec = util_flaggedtxt_2vec(&quest.qtext, '§');
551 usevec.sort();
552 usevec.dedup(); // Remove repeats of the flagged variable names.
553 // endregion
554
555 // region Read the variable files from disk and insert them into the variable vector.
556 quest.var_vec.clear();
557 for _item in usevec {
558 let newvar = vrbl_read();
559 quest.var_vec.push(newvar);
560 }
561 // endregion
562 }
563
564 /// Calls up an FLTK TextEditor for entering/editing question text
565 /// and variables.
566 pub fn qst_editor(startertxt: &str, winlabel: &str) -> String {
567
568 let mut buf = TextBuffer::default();
569 let mut edtrwin = window::Window::default().with_size(800, 300);
570 set_font_size(20);
571 edtrwin.set_color(Color::Blue);
572 edtrwin.set_label(winlabel);
573 edtrwin.make_resizable(true);
574
575 buf.set_text(startertxt);
576 let mut edtr = TextEditor::default()
577 .with_size(770, 222)
578 .center_of_parent();
579
580 qst_editor_menubar(&edtr, &mut edtrwin, &mut buf);
581
582 edtr.set_buffer(buf.clone()); // Clone is used here to avoid an ownership error.
583 edtr.wrap_mode(text::WrapMode::AtBounds, 0);
584 edtr.set_color(Color::White);
585 edtr.set_text_size(22);
586 edtr.set_text_color(Color::Black);
587
588 edtrwin.end();
589 edtrwin.show();
590
591 while edtrwin.shown() {
592 app::wait();
593 }
594
595 println!("\n W5: End of qst_editor(). The quesion text is: {} \n", buf.text());
596
597 buf.text()
598 }
599
600 /// Menu bar for the `qst_editor`.
601 ///
602 pub fn qst_editor_menubar(edtr: &TextEditor, edtrwin: &mut window::Window, buf: &mut TextBuffer) -> menu::MenuBar {
603 let mut menubar = menu::MenuBar::new(0, 0, edtrwin.width(), 40, "");
604
605 // region "Finished" menu item
606 let mut edtrwin_clone = edtrwin.clone();
607 let quit_idx = menubar.add(
608 "Finished\t",
609 Shortcut::None,
610 menu::MenuFlag::Normal,
611 move |_| {
612 edtrwin_clone.hide();
613 },
614 );
615 menubar.at(quit_idx).unwrap().set_label_color(Color::Red);
616 // endregion
617
618 // region "Insert Variable" menu item
619 let edtr_clone = edtr.clone();
620 let mut buf_clone = buf.clone();
621 menubar.add(
622 "Insert_Variable\t",
623 Shortcut::None,
624 menu::MenuFlag::Normal,
625 move |_| {
626 let newtext = qst_make_var_replace_text();
627 fltk_replace_highlighted_text(&edtr_clone, &mut buf_clone, &newtext);
628 },
629 );
630 // endregion
631
632 menubar
633 }
634
635 /// This function is called when the user highlights text that is
636 /// to be replaced by a variable. This function uses the variable name
637 /// to create text -- between flags -- that then replaces the highlighted text.
638 /// Returns the replacement text.
639 pub fn qst_make_var_replace_text() -> String {
640
641 // todo: Allow for user to input a more readable variable name
642 // than the name of the variable file name on disk.
643
644 let usedir = VARIABLE_DIR.to_string();
645
646 println!("Please choose the variable you want to insert. \n");
647
648 let path = file_pathonly(&usedir);
649 {
650 LAST_DIR_USED.lock().unwrap().clone_from(&path); // Refresh LAST_DIR_USED
651 }
652
653 let flist = file_get_dir_list(&path);
654 let varname = fltk_radio_lightbtn_menu(&flist, "");
655 let rpltxt = format!("§{}§", varname);
656
657 rpltxt
658 }
659
660 /*
661 /// This is no longer needed.
662 pub fn qst_read() -> Question {
663
664 // region Choose the desired path.
665 let mut usedir = String::new();
666 {
667 usedir = LAST_DIR_USED.lock().unwrap().clone();
668 }
669 println!("\n Please choose the Question file to be read.");
670 let usepath = file_fullpath(&usedir);
671 // endregion
672
673 match file_read_to_string(&usepath) {
674 Ok(contents) => {
675 let newquest = serde_json::from_str(&contents).unwrap();
676 newquest
677 }
678 Err(err) => {
679 eprintln!("\n Error reading the file: {} \n", err);
680 panic!("\n Error reading the file. \n");
681 }
682 }
683
684 }
685
686
687 /*
688 pub fn qst_fill_varvec_dirlist() -> Vec<Variable> {
689 println!("\n Please choose the variables you want to include as part of your question: ");
690 let path = file_pathonly();
691 let flist = file_get_dir_list(&path);
692 let flist_vec = chkbox_shift_menu(&flist);
693 let mut usevec: Vec<Variable> = Vec::new();
694
695 for item in flist_vec {
696 let flist_fullpath = format!("{}/{}", path, item);
697 println!("{}", flist_fullpath);
698 usevec.push(vrbl_read_nogetpath(&flist_fullpath));
699 };
700 usevec
701 }
702*/ //fn qst_fill_varvec_dirlist() -- delete later.
703
704 */ // Delete later.
705
706
707 /*
708 -- Answers will be calculated from the current variable values.
709
710 -- What are you going to do about operators and how they interact, especially
711 when the operator is given in the question in verbal format?
712 -- The answer equation will have to be entered by the user.
713
714 -- And then there be equations!!!!
715
716 */ // Issues & questions
717
718} // End questions module
719
720/// Functions that deal with the Variable struct.
721///
722pub mod variable {
723
724 use crate::global::{TypeWrapper, TypeWrapper::*};
725 use crate::{lists::*, math_functions::*, LAST_DIR_USED, VARIABLE_DIR};
726 use lib_file::{file_fltk::*, file_mngmnt::*};
727 use lib_utils::vec::*;
728 use serde::{Deserialize, Serialize};
729 use std::{fs::File, io::Write};
730 use std::cell::RefCell;
731 use std::rc::Rc;
732 use fltk::app;
733 use fltk::button::{Button, CheckButton, RadioLightButton};
734 use fltk::enums::{Color, FrameType};
735 use fltk::{frame::Frame, group::Group, window::Window};
736 use fltk::input::{FloatInput, IntInput};
737 use fltk::prelude::{ButtonExt, GroupExt, InputExt, WidgetBase, WidgetExt, WindowExt};
738
739 //region Struct Section
740
741 /// The third and innermost layer of the three struct nest
742 /// that QBC is built around.
743 #[derive(Debug, Serialize, Deserialize, Clone)]
744 pub struct Variable {
745 pub var_fname: String,
746 pub params: VarPrmtrs,
747 pub list_fname: String,
748 pub content: TypeWrapper,
749 pub var_type: String,
750 }
751
752 impl Variable {
753
754 /// Initialize a new Variable.
755 pub fn new() -> Variable {
756 Self {
757 var_fname: "new_variable".to_string(),
758 params: VarPrmtrs::new(),
759 list_fname: "".to_string(),
760 content: Integer(0),
761 var_type: "Strings".to_string(), // "Strings", "chars", "ints", "floats"
762 }
763 }
764 } // End Variable impl
765
766 /// Struct that holds the parameters that determine the behavior
767 /// of a Variable.
768 #[derive(Debug, Serialize, Deserialize, Clone)]
769 pub struct VarPrmtrs {
770 pub is_string: bool,
771 pub is_char: bool,
772 pub is_from_list: bool,
773 pub is_int: bool,
774 pub is_float: bool,
775 pub num_min_int: i64,
776 pub num_max_int: i64,
777 pub num_min_float: f64,
778 pub num_max_float: f64,
779 pub num_dcml_places: usize,
780 pub num_comma_frmttd: bool,
781 }
782
783 impl VarPrmtrs {
784 /// Creates a new variable. Sets the
785 /// default variable type to `int` and all ranges to 0.
786 pub fn new() -> VarPrmtrs {
787 Self {
788 is_string: false,
789 is_char: false,
790 is_from_list: false,
791 is_int: true,
792 is_float: false,
793 num_min_int: 0,
794 num_max_int: 0,
795 num_min_float: 0.0,
796 num_max_float: 0.0,
797 num_dcml_places: 0,
798 num_comma_frmttd: false, // Leave implementing this until you need to output it.
799
800 // Default values all assume that the variable is an i64.
801 }
802 }
803
804 } // ~~~~~ End VarPrmtrs impl ~~~~~
805 //endregion
806
807 /// Create a new variable.
808 ///
809 pub fn vrbl_create() {
810
811 //todo: Instead of passing a reference to the variable struct,
812 // have the parameters box return the variable struct.
813
814 let mut var1 = Variable::new();
815
816 vrbl_parameters_input_box(&mut var1);
817
818 println!("\n W3 -- Back in vrbl_create(): The Variable struct now contains: {:?} \n", var1);
819
820 vrbl_save(&mut var1);
821 }
822
823 /// Input and save a new variable's parameters into the Variable struct.
824 ///
825 pub fn vrbl_parameters_input_box(var1: &mut Variable) {
826
827 // todo: Grey out the input fields when the variable type is not "int" or "float".
828 // Use the `deactivate()` method. First attempt didn't work.
829
830 let mut win = Window::new(900, 100, 600, 400, "Variable Parameters");
831 win.set_color(Color::Cyan);
832 win.make_resizable(true);
833
834 // region Create the radio buttons for the variable type.
835 let radio_group = Group::new(0, 0, 600, 50, None);
836
837 // Create horizontal radio light buttons across the top -- initial spacing.
838 let bttn_w = 120;
839 let bttn_h = 30;
840 let spacing = 20;
841 let types_xxx = 40;
842 let types_yyy = 20;
843
844 let mut strings_btn = RadioLightButton::new(types_xxx, types_yyy, bttn_w, bttn_h, "Strings");
845 let chars_btn = RadioLightButton::new(types_xxx + bttn_w + spacing, types_yyy, bttn_w, bttn_h, "Characters");
846 let ints_btn = RadioLightButton::new(types_xxx + 2 * (bttn_w + spacing), types_yyy, bttn_w, bttn_h, "Integers");
847 let decimals_btn = RadioLightButton::new(types_xxx + 3 * (bttn_w + spacing), types_yyy, bttn_w, bttn_h, "Decimals");
848
849 // Set Integers as default selection
850 strings_btn.set_value(true);
851
852 radio_group.end();
853 // endregion
854
855 // region Create "comma" & "list" check boxes in row below the radio buttons.
856
857 // Calculate the position & size of the check boxes.
858 let ckbx_y = types_yyy + bttn_h + 20; // Position below radio buttons
859 let ckbx_w = 150;
860 let ckbx_h = 25;
861 let ckbx_spacing = 55;
862
863 let total_radio_width = bttn_w * 4 + spacing * 2; // Width of all radio buttons + spacing
864 let start_x = types_xxx + (total_radio_width - (ckbx_w * 2 + ckbx_spacing)) / 2;
865
866 // Create the check boxes.
867 let usecommas = CheckButton::new(start_x, ckbx_y, ckbx_w, ckbx_h, "Comma Formatted");
868 let fromlist = CheckButton::new(start_x + ckbx_w + ckbx_spacing, ckbx_y,
869 ckbx_w, ckbx_h, "Value to come from a List");
870
871
872 // endregion
873
874 // region Set up frames -- for Integer & Decimal parameter entry.
875
876 // region Set up frame parameters
877 let frame_y = ckbx_y + ckbx_h + 20; // Position below checkboxes
878 let frame_w = 250;
879 let input_w = 100;
880 let input_h = 25;
881 let label_h = 20;
882 let field_spacing = 10;
883 let frame_spacing = 20;
884 let frame_h = 30 + (3 * (label_h + input_h + field_spacing)) + 15;
885 // endregion
886
887 // region Create Integers frame & input fields
888 let mut int_frame = Group::new(types_xxx, frame_y, frame_w, frame_h, None);
889 let mut int_label = Frame::new(types_xxx, frame_y, frame_w, 30, "Integer Parameters");
890 int_label.set_label_size(14);
891
892 // Calculate centered position for input fields in integer frame
893 let int_input_x = types_xxx + (frame_w - input_w) / 2;
894 let int_first_y = frame_y + 35; // Start below the frame label
895
896 // Integer Minimum Value
897 let _intmin_label = Frame::new(int_input_x, int_first_y, input_w, label_h, "Minimum Value");
898 let mut intmin = IntInput::new(int_input_x, int_first_y + label_h, input_w, input_h, "");
899
900 // Integer Maximum Value
901 let _intmax_label = Frame::new(int_input_x, int_first_y + label_h + input_h + field_spacing,
902 input_w, label_h, "Maximum Value");
903 let mut intmax = IntInput::new(int_input_x, int_first_y + label_h + input_h + field_spacing + label_h,
904 input_w, input_h, "");
905
906 int_frame.set_frame(FrameType::DownBox); // Add frame border
907 int_frame.end();
908 // endregion
909
910 // region Create Decimals frame & input fields
911 let mut decimal_frame = Group::new(types_xxx + frame_w + frame_spacing, frame_y, frame_w, frame_h, None);
912 let mut decimal_label = Frame::new(types_xxx + frame_w + frame_spacing, frame_y, frame_w, 30, "Decimal Parameters");
913 decimal_label.set_label_size(14);
914
915 // Calculate centered position for input fields in decimal frame
916 let dec_input_x = types_xxx + frame_w + frame_spacing + (frame_w - input_w) / 2;
917 let dec_first_y = frame_y + 35; // Start below the frame label
918
919 // Decimal Minimum Value
920 let _decmin_label = Frame::new(dec_input_x, dec_first_y, input_w, label_h, "Minimum Value");
921 let mut decmin = FloatInput::new(dec_input_x, dec_first_y + label_h, input_w, input_h, "");
922
923 // Decimal Maximum Value
924 let _decmax_label = Frame::new(dec_input_x, dec_first_y + label_h + input_h + field_spacing,
925 input_w, label_h, "Maximum Value");
926 let mut decmax = FloatInput::new(dec_input_x, dec_first_y + label_h + input_h + field_spacing + label_h,
927 input_w, input_h, "");
928
929 // Decimal Places
930 let _decplaces_label = Frame::new(dec_input_x, dec_first_y + 2 * (label_h + input_h + field_spacing),
931 input_w, label_h, "Decimal Places");
932 let mut decplaces = IntInput::new(dec_input_x, dec_first_y + 2 * (label_h + input_h + field_spacing) + label_h,
933 input_w, input_h, "");
934
935 decimal_frame.set_frame(FrameType::DownBox); // Add frame border
936 decimal_frame.end();
937 // endregion
938
939 // endregion
940
941 // region Create the Submit button
942 let submit_btn_w = 100;
943 let submit_btn_h = 40;
944
945 // Calculate center position based on the frames
946 let total_frames_width = frame_w * 2 + frame_spacing;
947 let submit_btn_x = types_xxx + (total_frames_width - submit_btn_w) / 2;
948 let submit_btn_y = frame_y + frame_h + 20; // 20 pixels gap after frames
949
950 let mut submit_btn = Button::new(submit_btn_x, submit_btn_y, submit_btn_w, submit_btn_h, "Submit");
951 // endregion
952
953 win.end();
954 win.show();
955
956 // region Clone variables for the callback
957 let strings_btn = strings_btn.clone();
958 let chars_btn = chars_btn.clone();
959 let ints_btn = ints_btn.clone();
960 let decimals_btn = decimals_btn.clone();
961 let mut win_clone = win.clone();
962
963 let datavar = Rc::new(RefCell::new(Variable::new()));
964 let datavar_outside = datavar.clone(); // Create a second Rc pointing to the same RefCell
965
966
967 // endregion
968
969 // region Do the callback for the Submit button
970 submit_btn.set_callback(move |_| {
971
972 // region Deal with the radio buttons.
973 let vartype = if strings_btn.value() {
974 decmin.deactivate();
975 decmax.deactivate();
976 decplaces.deactivate();
977 intmin.deactivate();
978 intmax.deactivate();
979 "Strings"
980 } else if chars_btn.value() {
981 decmin.deactivate();
982 decmax.deactivate();
983 decplaces.deactivate();
984 intmin.deactivate();
985 intmax.deactivate();
986 "Characters"
987 } else if ints_btn.value() {
988 decmin.deactivate();
989 decmax.deactivate();
990 decplaces.deactivate();
991 "Integers"
992 } else if decimals_btn.value() {
993 intmin.deactivate();
994 intmax.deactivate();
995 "Decimals"
996 } else {
997 "None"
998 };
999
1000 datavar.borrow_mut().var_type = vartype.to_string();
1001 // endregion
1002
1003 //region Deal with the "comma" & "list" check boxes.
1004 if usecommas.is_checked() {
1005 datavar.borrow_mut().params.num_comma_frmttd = true;
1006 print!("\n Comma Formatted == true \n");
1007 } else {
1008 datavar.borrow_mut().params.num_comma_frmttd = false;
1009 print!("\n Comma Formatted == false \n");
1010 }
1011
1012 if fromlist.is_checked() {
1013 datavar.borrow_mut().params.is_from_list = true;
1014 print!("\n List == true \n");
1015 } else {
1016 datavar.borrow_mut().params.is_from_list = false;
1017 print!("\nList == false \n");
1018 }
1019 // endregion
1020
1021 // region Deal with the Integer input fields.
1022 if vartype == "Integers" {
1023 datavar.borrow_mut().params.is_int = true;
1024 datavar.borrow_mut().params.is_float = false;
1025 datavar.borrow_mut().params.num_min_int = intmin.value().parse::<i64>().unwrap();
1026 datavar.borrow_mut().params.num_max_int = intmax.value().parse::<i64>().unwrap();
1027 }
1028 // endregion
1029
1030 // region Deal with the Decimal input fields.
1031 if vartype == "Decimals" {
1032 datavar.borrow_mut().params.is_int = false;
1033 datavar.borrow_mut().params.is_float = true;
1034 datavar.borrow_mut().params.num_min_float = decmin.value().parse::<f64>().unwrap();
1035 datavar.borrow_mut().params.num_max_float = decmax.value().parse::<f64>().unwrap();
1036 datavar.borrow_mut().params.num_dcml_places = decplaces.value().parse::<usize>().unwrap();
1037 }
1038 // endregion
1039
1040 println!("\n In the callback, datavar == {:?} \n", datavar);
1041
1042 // Close the window
1043 win_clone.hide();
1044 });
1045 // endregion
1046
1047 // Keep window active until hidden
1048 while win.shown() {
1049 app::wait();
1050 }
1051
1052 *var1 = datavar_outside.borrow().clone();
1053 }
1054
1055
1056/*
1057 /// Set the parameters for a Variable.
1058 ///
1059 pub fn vrbl_input_parameters(data: &mut Variable) { // Set boolean parameters only. Leave data alone.
1060
1061 // todo: Turn all this into a window of radio and checkbox buttons for setting
1062 // these parameters.
1063
1064 match data.var_type.as_str() {
1065 "Strings" => { // Note that Strings should only come from a list.
1066 data.params.is_string = true;
1067 data.params.is_int = false;
1068 data.params.is_from_list = true;
1069 }
1070
1071 "Character" => { // Note that characters also can only come from a list.
1072 data.params.is_char = true;
1073 data.params.is_int = false;
1074 data.params.is_from_list = true;
1075 }
1076
1077 "Integers" => {
1078 data.params.num_comma_frmttd = input_bool_prompt("\n Is the value to be comma formatted? ");
1079
1080 let mini_choice = input_bool_prompt("\n Is the variable contents to come from a list? ");
1081 if mini_choice {
1082 data.params.is_from_list = true;
1083 return;
1084 }
1085 data.params.num_min_int = input_num_prompt("\n Please enter the minimum int value: ");
1086 data.params.num_max_int = input_num_prompt("\n Please enter the maximum int value: ");
1087 }
1088
1089 "Decimals" => {
1090 data.params.is_int = false;
1091 data.params.num_dcml_places = input_num_prompt("\n How many decimal places are allowed? ");
1092 data.params.num_comma_frmttd = input_bool_prompt("\n Is the value to be comma formatted? ");
1093
1094 let mini_choice = input_bool_prompt("\n Is the variable contents to come from a list? ");
1095 if mini_choice {
1096 data.params.is_from_list = true;
1097 return;
1098 }
1099 data.params.num_min_float = input_num_prompt("\n Please enter the minimum float value: ");
1100 data.params.num_max_float = input_num_prompt("\n Please enter the maximum float value: ");
1101 }
1102
1103 _ => { unreachable!(); }
1104 }
1105 }
1106
1107 /// Input more data to be used in a variable.
1108 ///
1109 pub fn vrbl_input_vardata(data: &mut Variable) {
1110 data.var_fname = input_string_prompt("\n Please enter a title/filename for your new variable: ");
1111 vrbl_setvalues(data);
1112 }
1113 */ // I think these can now be deleted.
1114
1115
1116
1117 /// Prepare a Variable for saving.
1118 ///
1119 pub fn vrbl_save(var1: &mut Variable) {
1120 {
1121 if LAST_DIR_USED.lock().unwrap().clone() == "" {
1122 *LAST_DIR_USED.lock().unwrap() = VARIABLE_DIR.to_string().clone();
1123 } // If no path loaded, use default.
1124 } // Access the global variable.
1125 let lastdir = LAST_DIR_USED.lock().unwrap().clone();
1126 let usepath = file_browse_save_fltr(&lastdir, "Variable Files \t*.vrbl\nText Files \t*.txt\nList Files \t*.lst\nAll Files \t*.*");
1127
1128 {
1129 *LAST_DIR_USED.lock().unwrap() = usepath.clone();
1130 } // Set LAST_DIR_USED to the new path.
1131
1132 var1.var_fname = file_path_to_fname(&usepath);
1133 vrbl_save_as_json(&var1, &usepath);
1134
1135 println!("\n The variable has been saved.");
1136 }
1137
1138 /// Save a Variable in json format.
1139 ///
1140 pub fn vrbl_save_as_json(var: &Variable, usepath: &String) {
1141 let var_as_json = serde_json::to_string(var).unwrap();
1142
1143 let mut file = File::create(usepath.as_str()).expect("Could not create file!");
1144
1145 file.write_all(var_as_json.as_bytes())
1146 .expect("Cannot write to the file!");
1147 }
1148
1149 /// Read a variable from a file.
1150 ///
1151 pub fn vrbl_read() -> Variable {
1152
1153 // region Choose the correct directory path
1154 let mut usepath = VARIABLE_DIR.to_string();
1155 println!("\n Please choose the variable file to be used.");
1156 usepath = file_fullpath(&usepath);
1157 {
1158 *LAST_DIR_USED.lock().unwrap() = usepath.clone();
1159 } // Set LAST_DIR_USED to the new path.
1160 // endregion
1161
1162 match file_read_to_string(&usepath) {
1163 Ok(contents) => {
1164 let newvariable = serde_json::from_str(&contents).unwrap();
1165 newvariable
1166 }
1167 Err(err) => {
1168 eprintln!("\n Error reading the file: {} \n", err);
1169 panic!("\n Error reading the file. \n");
1170 }
1171 }
1172 }
1173
1174
1175
1176 /// Sets and calculates the values of non-boolean fields in the Variable struct.
1177 ///
1178 pub fn vrbl_setvalues(var1: &mut Variable) {
1179 //let lastdir = LAST_DIR_USED.lock().unwrap();
1180
1181 if var1.params.is_from_list { // The variable content is to come from a list.
1182 match var1.var_type.as_str() {
1183 "Strings" => {
1184 println!("\n Please choose the list you want to use.");
1185 let newlist = list_read("Strings"); // Returns a tuple (listname, List)
1186 var1.list_fname = newlist.0; // Sets the value of the variable's listname field.
1187
1188 let usevec = newlist.1.words.clone(); // Clones the list content vector.
1189 //let usevec_str = vec_string_to_str(&usevec);
1190 let item = vec_random_choice(&usevec);
1191 match item {
1192 Some(x) => {
1193 println!("\n The chosen item is: {:?}", x);
1194 var1.content = Alphanum(x.0.to_string());
1195 },
1196 None => panic!("No item was chosen."),
1197 }
1198 },
1199
1200 "chars" => {
1201 println!("\n Please choose a list to be read.");
1202 let newlist = list_read("chars");
1203 var1.list_fname = newlist.0;
1204
1205 let item = vec_random_choice(&newlist.1.runes);
1206 match item {
1207 Some(x) => {
1208 println!("\n The chosen item is: {:?}", x);
1209 var1.content = Letter(*x.0);
1210 },
1211 None => panic!("No item was chosen."),
1212 }
1213 },
1214
1215 "ints" => {
1216 println!("\n Please choose a list to be read.");
1217 let newlist = list_read("ints");
1218 var1.list_fname = newlist.0;
1219
1220 let item = vec_random_choice(&newlist.1.intsigned);
1221 match item {
1222 Some(x) => {
1223 println!("\n The chosen item is: {:?}", x);
1224 var1.content = Integer(*x.0);
1225 },
1226 None => panic!("No item was chosen."),
1227 }
1228 },
1229
1230 "floats" => {
1231 println!("\n Please choose a list to be read.");
1232 let newlist = list_read("floats");
1233 var1.list_fname = newlist.0;
1234
1235 let item = vec_random_choice(&newlist.1.decimals);
1236 match item {
1237 Some(x) => {
1238 println!("\n The chosen item is: {:?}", x);
1239 var1.content = Floating(*x.0);
1240 },
1241 None => panic!("No item was chosen."),
1242 }
1243 },
1244
1245 _ => {}
1246 }
1247 } else {
1248 if var1.params.is_int { // Numeric values will always be randomly generated.
1249 let numint: i64 = math_gen_random_num(var1.params.num_min_int, var1.params.num_max_int);
1250 var1.content = Integer(numint);
1251 } else { // The content is a float.
1252 let mut numfloat: f64 = math_gen_random_num(var1.params.num_min_float, var1.params.num_max_float);
1253 numfloat = math_round_to_place_f64(&numfloat, var1.params.num_dcml_places);
1254 var1.content = Floating(numfloat);
1255 }
1256 }
1257 }
1258
1259
1260 /// Recalculates the values in the non-boolean fields of a Variable struct.
1261 ///
1262 pub fn vrbl_recalc() {
1263 let mut usevar = vrbl_read();
1264
1265 println!("\n The variable before recalc is: \n {:?}", usevar);
1266
1267 vrbl_setvalues(&mut usevar);
1268 vrbl_save(&mut usevar);
1269
1270 println!("\n The variable after recalc is: \n {:?} \n", usevar);
1271
1272 }
1273
1274
1275
1276} // End variable module
1277
1278/// Functions for creating and manipulating lists.
1279///
1280pub mod lists {
1281 use std::{fs::File, io::Write};
1282 use serde::{Deserialize, Serialize};
1283 use lib_file::file_fltk::{file_browse_save_fltr, file_fullpath_fltr};
1284 use lib_file::file_mngmnt::file_read_to_string;
1285 use lib_myfltk::input_fltk::*;
1286 use crate::{APP_FLTK, LAST_DIR_USED, VARIABLE_DIR};
1287
1288 // region Struct section
1289
1290 /// Contains a vector field for each of the four data types
1291 /// allowed in lists associated with QBC.
1292 #[derive(Debug, Serialize, Deserialize)]
1293 pub struct List {
1294 pub words: Vec<String>,
1295 pub runes: Vec<char>,
1296 pub intsigned: Vec<i64>,
1297 pub decimals: Vec<f64>,
1298 pub typechoice: String, // "Strings", "chars", "ints", "floats"
1299 }
1300
1301 impl List {
1302
1303 /// Initializes a new list struct.
1304 pub fn new() -> List {
1305 Self {
1306 words: Vec::new(),
1307 runes: Vec::new(),
1308 intsigned: Vec::new(),
1309 decimals: Vec::new(),
1310 typechoice: "Strings".to_string(),
1311 }
1312 }
1313 } // ---------- End List impl ----------
1314
1315 // endregion
1316
1317 /// Create a new list.
1318 ///
1319 pub fn list_create(typech: &str) {
1320 let mut newlist = List::new();
1321 newlist.typechoice = typech.to_string();
1322
1323 let app;
1324 {
1325 app = APP_FLTK.lock().unwrap().clone();
1326 }
1327
1328 match typech {
1329 "String" | "Strings" => { // String
1330 let uselist = input_strvec(&app, "Please enter a string.", 790, 300);
1331 newlist.words = uselist.clone();
1332 list_save(&newlist);
1333 }
1334
1335 "char" | "chars" => { // char
1336 let uselist = input_charvec(&app, "Please enter a character." );
1337 newlist.runes = uselist.clone();
1338 list_save(&newlist);
1339 }
1340
1341 "int" | "ints" => { // i64
1342 let uselist = input_i64vec(&app, "Please enter an integer." );
1343 newlist.intsigned = uselist.clone();
1344 list_save(&newlist);
1345 }
1346
1347 "float" | "floats" => { // f64
1348
1349 let uselist = input_f64vec(&app, "Please enter a floating point number.");
1350 newlist.decimals = uselist.clone();
1351 list_save(&newlist);
1352 }
1353
1354 _ => {
1355 panic!("\n\n No match found! Fix it!!\n\n");
1356 }
1357 }
1358
1359 // Note that the function saves the list, but does not return it.
1360 }
1361
1362 /// Read a list (in json format) from a file. Returns a tuple (filename, List)
1363 /// containing the file name that was read along with the reconstituted list.
1364 pub fn list_read(typech: &str) -> (String, List) {
1365
1366 {
1367 if LAST_DIR_USED.lock().unwrap().clone() == "" {
1368 *LAST_DIR_USED.lock().unwrap() = VARIABLE_DIR.to_string().clone();
1369 } // If no path loaded, use default.
1370 } // Access the global variable.
1371 let lastdir = LAST_DIR_USED.lock().unwrap().clone();
1372
1373 //let mut startdir = String::new();
1374 //{
1375 //startdir = LAST_DIR_USED.lock().unwrap().clone();
1376 //}
1377
1378 // TODO: Should this return an option or result rather than `unwrap()` or `panic!()`?
1379 // TODO: This has not been tested after the last modifications were made.
1380 // todo: You aren't dealing with LAST_DIR_USED correctly.
1381 // todo: The returns of this function don't look right. Check it out.
1382
1383 let readlist = loop {
1384 let usename = file_fullpath_fltr(&lastdir, "*.lst");
1385 *LAST_DIR_USED.lock().unwrap() = usename.clone();
1386
1387 match file_read_to_string(&usename) {
1388 Ok(contents) => {
1389 let newlist = serde_json::from_str(&contents).unwrap();
1390 let typchk = list_check_typematch(&newlist, typech);
1391 if !typchk { continue }
1392 else { break (usename, newlist)}
1393 }
1394 Err(err) => {
1395 eprintln!("\n Error reading the file: {} \n", err);
1396 panic!("\n Error reading the file. \n");
1397 }
1398 }
1399 };
1400
1401 readlist
1402 }
1403
1404 /// Edit an existing list. Not yet implementd
1405 ///
1406 pub fn list_edit() {
1407
1408 println!("\n Someday I'll write this function. \n");
1409 }
1410
1411 /// Prepare a list for saving to a file.
1412 ///
1413 pub fn list_save(list: &List) -> String {
1414
1415 let startdir = LAST_DIR_USED.lock().unwrap().clone(); // Access the last used directory.
1416
1417 let path = file_browse_save_fltr(&startdir, "List Files \t*.lst\nVariable Files \t*.vrbl\nText Files \t*.txt\nAll Files");
1418 *LAST_DIR_USED.lock().unwrap() = path.clone(); // Store the current path in global.
1419
1420 list_save_as_json(&list, &path);
1421
1422 path
1423 }
1424
1425 /// Save a list in json format.
1426 ///
1427 pub fn list_save_as_json(list: &List, fname: &str) {
1428 let list_as_json = serde_json::to_string(list).unwrap();
1429
1430 let mut file = File::create(fname).expect("Could not create file!");
1431
1432 file.write_all(list_as_json.as_bytes())
1433 .expect("Cannot write to the file!");
1434 }
1435
1436 /// Check that a list contains the correct type of data.
1437 ///
1438 pub fn list_check_typematch(uselist: &List, typech: &str) -> bool {
1439 if uselist.typechoice.as_str() != typech {
1440 println!("\n The data type of that list does not match your typechoice. \n");
1441 println!("Please choose a different list file. \n");
1442 false
1443 } else { true }
1444 }
1445
1446} // End lists module
1447
1448/// Functions for use in creating menus.
1449///
1450pub mod menus {
1451 use fltk::{app::quit, menu, window::Window};
1452 use fltk::enums::{Color, Shortcut};
1453 use fltk::prelude::{MenuExt, WidgetBase, WidgetExt};
1454 use crate::{banks::*, questions::*, variable::*, lists::*};
1455 use crate::misc::check_for_bank_loaded;
1456
1457 /// Create a menubar for the primary window.
1458 pub fn qbnk_menubar(primwin: &mut Window) -> menu::MenuBar {
1459 // todo: `primwin` is in the global Widgets variable. Access it
1460 // there rather than passing it to this function.
1461
1462 let mut menubar = menu::MenuBar::new(0, 0, primwin.width(), 40, "");
1463
1464 //region File section
1465
1466 menubar.add(
1467 "File/Print/Question Bank\t",
1468 Shortcut::None,
1469 menu::MenuFlag::Normal,
1470 |_| println!("Printing a new Question Bank."),
1471 );
1472 menubar.add(
1473 "File/Print/Question\t",
1474 Shortcut::None,
1475 menu::MenuFlag::Normal,
1476 |_| println!("Printing a new Question."),
1477 );
1478 menubar.add(
1479 "File/Print/Variable\t",
1480 Shortcut::None,
1481 menu::MenuFlag::Normal,
1482 |_| println!("Printing a new Variable."),
1483 );
1484 menubar.add(
1485 "File/Print/List\t", // Where does versioning come in?
1486 Shortcut::None,
1487 menu::MenuFlag::Normal,
1488 |_| println!("Printing a new List."),
1489 );
1490 menubar.add(
1491 "File/Save\t", // Save always focuses on the Question Bank.
1492 Shortcut::None,
1493 menu::MenuFlag::Normal,
1494 |_| println!("Saving a Question Bank."),
1495 );
1496 menubar.add(
1497 "File/Save-as\t",
1498 Shortcut::None,
1499 menu::MenuFlag::Normal,
1500 |_| println!("Saving a Question Bank with a new name."),
1501 );
1502
1503 let quit_idx = menubar.add(
1504 "File/Quit\t",
1505 Shortcut::None,
1506 menu::MenuFlag::Normal,
1507 |_| {
1508 quit();
1509 },
1510 );
1511 menubar.at(quit_idx).unwrap().set_label_color(Color::Red);
1512 //endregion
1513
1514 //region Bank section
1515
1516 menubar.add(
1517 "Bank/New\t",
1518 Shortcut::None,
1519 menu::MenuFlag::Normal,
1520 move |_| {
1521 bnk_create();
1522 bnk_refresh_widgets();
1523 },
1524 );
1525
1526 menubar.add(
1527 "Bank/Open\t",
1528 Shortcut::None,
1529 menu::MenuFlag::Normal,
1530 move |_| {
1531 bnk_read();
1532 bnk_refresh_widgets();
1533 },
1534 );
1535
1536 // TODO: Add ctrl-s as option for saving.
1537 // TODO: Right now Save and Save-As are the same thing.
1538 // Differentiate them.
1539
1540 menubar.add(
1541 "Bank/Save\t",
1542 Shortcut::None,
1543 menu::MenuFlag::Normal,
1544 |_| {
1545 bnk_save();
1546 println!("/n The question bank has been saved. \n")
1547 },
1548 );
1549 menubar.add(
1550 "Bank/Save-as\t",
1551 Shortcut::None,
1552 menu::MenuFlag::Normal,
1553 |_| {
1554 bnk_save();
1555 println!("/n The question bank has been saved. \n")
1556 }, );
1557
1558
1559 menubar.add(
1560 "Bank/Recalculate\t",
1561 Shortcut::None,
1562 menu::MenuFlag::Normal,
1563 |_| println!("\n Not yet implemented. \n"),
1564 );
1565
1566
1567 menubar.add(
1568 "Bank/Export\t",
1569 Shortcut::None,
1570 menu::MenuFlag::Normal,
1571 |_| println!("\n Not yet implemented. \n"),
1572 );
1573
1574 //endregion
1575
1576 //region Question Section
1577
1578 menubar.add(
1579 "Question/New\t",
1580 Shortcut::None,
1581 menu::MenuFlag::Normal,
1582 move |_| {
1583 if check_for_bank_loaded() {
1584 qst_create();
1585 bnk_refresh_widgets();
1586 }
1587
1588 // Keep the window display open after this function finishes.
1589 // while primwin1.shown() {
1590 // app::wait();
1591 // }
1592 },
1593 );
1594
1595 menubar.add(
1596 "Question/Recalculate\t",
1597 Shortcut::None,
1598 menu::MenuFlag::Normal,
1599 |_| println!("Not yet implemented. Recalculating dynamic content in a Question."),
1600 );
1601
1602 //endregion
1603
1604 //region Variable Section
1605
1606 menubar.add(
1607 "Variable/New\t",
1608 Shortcut::None,
1609 menu::MenuFlag::Normal,
1610 move |_| {
1611 vrbl_create();
1612 },
1613 );
1614
1615
1616 /*
1617 menubar.add(
1618 "Variable/New/String\t",
1619 Shortcut::None,
1620 menu::MenuFlag::Normal,
1621 move |_| {
1622 vrbl_create("Strings");
1623 },
1624 );
1625
1626 menubar.add(
1627 "Variable/New/Characters\t",
1628 Shortcut::None,
1629 menu::MenuFlag::Normal,
1630 move |_| {
1631 vrbl_create("chars");
1632 },
1633 );
1634
1635 menubar.add(
1636 "Variable/New/Integers\t",
1637 Shortcut::None,
1638 menu::MenuFlag::Normal,
1639 move |_| {
1640 vrbl_create("ints");
1641 },
1642 );
1643
1644 menubar.add(
1645 "Variable/New/Floats\t",
1646 Shortcut::None,
1647 menu::MenuFlag::Normal,
1648 move |_| {
1649 vrbl_create("floats");
1650 },
1651 );
1652
1653 menubar.add(
1654 "Variable/Recalculate\t", // Does this make sense as a user task? Yes, definitely.
1655 Shortcut::None,
1656 menu::MenuFlag::Normal,
1657 move |_| vrbl_recalc(),
1658 );
1659
1660 //endregion
1661
1662 */ // Uneeded sub menu items.
1663 // endregion
1664
1665 //region List Section
1666
1667 menubar.add(
1668 "List/New/Strings\t",
1669 Shortcut::None,
1670 menu::MenuFlag::Normal,
1671 move |_| {
1672 list_create("Strings");
1673 },
1674 );
1675
1676 menubar.add(
1677 "List/New/Characters\t",
1678 Shortcut::None,
1679 menu::MenuFlag::Normal,
1680 move |_| {
1681 list_create("chars");
1682 },
1683 );
1684
1685 menubar.add(
1686 "List/New/Integers\t",
1687 Shortcut::None,
1688 menu::MenuFlag::Normal,
1689 move |_| {
1690 list_create("ints");
1691 },
1692 );
1693
1694 menubar.add(
1695 "List/New/Floats\t",
1696 Shortcut::None,
1697 menu::MenuFlag::Normal,
1698 move |_| {
1699 list_create("floats");
1700 },
1701 );
1702
1703 menubar.add(
1704 "List/Edit\t",
1705 Shortcut::None,
1706 menu::MenuFlag::Normal,
1707 |_| println!("\n List editing will be added in a later iteration. \n"),
1708 );
1709
1710 // ------- End list section
1711 //endregion
1712
1713 menubar
1714 }
1715} // End menus module
1716
1717/// Math-based functions.
1718///
1719pub mod math_functions {
1720 use num_traits::pow;
1721 use rand::distributions::uniform::SampleUniform;
1722 use rand::{thread_rng, Rng};
1723
1724 /// Round an f64 to a given decimal place.
1725 ///
1726 pub fn math_round_to_place_f64(num: &f64, place: usize) -> f64 {
1727 let factor = pow(10, place);
1728 let rounded = (num * factor as f64).round() / factor as f64;
1729 rounded
1730 }
1731
1732 /// Generate and return a random number between the given min and max.
1733 ///
1734 /// Example:
1735 ///
1736 /// fn main() {
1737 /// let choice: i64 = math_gen_random_num(-9, 9);
1738 ///
1739 /// println!("\n Your random number is: {} \n", choice);
1740 /// }
1741 ///
1742 pub fn math_gen_random_num<T: SampleUniform + PartialOrd>(min: T, max: T) -> T {
1743 let mut rng = thread_rng();
1744 rng.gen_range(min..=max)
1745 }
1746
1747} // End math_functions module
1748
1749/// Miscellaneous functions used by other modules.
1750///
1751pub mod misc {
1752 use crate::{banks::Bank, questions::qst_edit};
1753 use crate::{Wdgts, CURRENT_BANK, DEVELOPMENT_VERSION, PROGRAM_TITLE, QDISP_HEIGHT, SCROLLBAR_WIDTH, VERSION, WIDGETS};
1754 use fltk::prelude::{DisplayExt, GroupExt, WidgetBase, WidgetExt};
1755 use fltk::text::{TextBuffer, TextDisplay, TextEditor};
1756 use fltk::{text, window::Window};
1757 use fltk::{button::Button, enums::Color, group::Scroll};
1758
1759 /// Gets and returns the text from a given FLTK TextEditor.
1760 ///
1761 pub fn get_text_from_editor(editor: &TextEditor) -> String {
1762 if let Some(buffer) = editor.buffer() {
1763 let text = buffer.text(); // Retrieve the text from the associated buffer
1764 return text;
1765 } else {
1766 String::new() // If no buffer is set, return an empty string
1767 }
1768 }
1769
1770 /// Sets up the primary window for QBC.
1771 ///
1772 pub fn primwin_setup(primwin: &mut Window) { // Set up the primary window.
1773 //let mut primwin = Window::default().with_size(825, 900).with_pos(1000, 100);
1774 primwin.set_color(Color::Blue);
1775 let fulltitle = format!("{} -- {} -- Version {}", DEVELOPMENT_VERSION, PROGRAM_TITLE, VERSION);
1776 primwin.set_label(fulltitle.as_str());
1777 primwin.make_resizable(true);
1778 }
1779
1780
1781 /*
1782 pub fn onopen_popup() -> Window {
1783 // On program opening, pop up a window with choice for new bank or open existing bank.
1784
1785 // region Set up button callback closures.
1786 // Button -- Create a new question bank.
1787 let bttn_newbank = move || {
1788 bnk_create();
1789 bnk_refresh_widgets();
1790 };
1791
1792 // Button -- Open an existing question bank.
1793 let bttn_openbank = move || {
1794 bnk_read();
1795 bnk_refresh_widgets();
1796 };
1797 // endregion
1798
1799 let mut wdgts = Wdgts::new();
1800 {
1801 wdgts = WIDGETS.lock().unwrap().clone();
1802 } // Access the WIDGETS struct
1803
1804 // todo: The function below is way too complex. Find another solution.
1805 let mut pop = fltk_popup_2btn(&wdgts.prim_win, Box::new(bttn_newbank), "Create new bank",
1806 Box::new(bttn_openbank), "Open existing bank");
1807 pop.set_color(Color::Red);
1808
1809 pop
1810 }
1811
1812 */ // No longer needed -- onopen_popup()
1813
1814
1815 /// Creates and sets up an FLTK TextEditor window to use for
1816 /// a title box when displaying a question Bank.
1817 pub fn make_title_txtedtr() {
1818
1819 // todo: Add a line, smaller font, below the main title for
1820 // a subtitle or maybe the title of the textbook being used.
1821
1822 let usebank: Bank;
1823 let mut wdgts: Wdgts;
1824 {
1825 usebank = CURRENT_BANK.lock().unwrap().clone();
1826 wdgts = WIDGETS.lock().unwrap().clone();
1827 } // Load the global structs.
1828
1829 let mut buf = TextBuffer::default();
1830 buf.set_text(usebank.bank_title.as_str()); // Uses the title from the current bank.
1831
1832 let mut ted = TextEditor::new(0, 40, wdgts.prim_win.width(), 60, "");
1833 ted.set_text_size(32);
1834 ted.set_text_color(Color::White);
1835 ted.set_color(Color::DarkMagenta);
1836 ted.set_buffer(buf.clone()); // Clone is used here to avoid an ownership error.
1837
1838 wdgts.title_editbox = ted.clone(); // Store the widgit in the widget struct.
1839 wdgts.prim_win.add(&ted.clone());
1840
1841 *WIDGETS.lock().unwrap() = wdgts.clone(); // Update the WIDGET global variable.
1842
1843 // todo: It would be nice to center and bold the text, but that is really
1844 // difficult to do, so leave for later.
1845 }
1846
1847 /// Creates and sets up an FLTK scrollgroup to use
1848 /// when displaying a question Bank.
1849 pub fn make_scrollgroup() {
1850 let mut wdgts: Wdgts;
1851 let usebank: Bank;
1852 {
1853 wdgts = WIDGETS.lock().unwrap().clone();
1854 usebank = CURRENT_BANK.lock().unwrap().clone();
1855 } // Access the global structs.
1856
1857 // Create scroll group
1858 let mut scroll = Scroll::new(0, wdgts.title_editbox.height() + 40,
1859 wdgts.prim_win.width(),
1860 usebank.question_vec.len() as i32 * QDISP_HEIGHT,
1861 "");
1862 scroll.set_scrollbar_size(SCROLLBAR_WIDTH);
1863
1864 // Add scroll to the Wdgts struct & window.
1865 wdgts.scroll = scroll.clone();
1866 wdgts.prim_win.add(&scroll.clone());
1867
1868 *WIDGETS.lock().unwrap() = wdgts.clone(); // Update the WIDGET global variable.
1869 }
1870
1871 /// Creates and sets up an FLTK TextDisplay boxes to use
1872 /// when displaying the questions in a question Bank.
1873 pub fn make_question_boxes() {
1874 // region TODO's
1875 //todo: The question numbers are displaying weird. First question's label doesn't
1876 // even show. Work on that. Next iteration.
1877 // TODO: Set up show/edit prereqs and objectives button
1878 // TODO: Create a subframe to display/edit the answer.
1879
1880 // endregion
1881
1882 let usebank: Bank;
1883 let mut wdgts: Wdgts;
1884 {
1885 usebank = CURRENT_BANK.lock().unwrap().clone();
1886 wdgts = WIDGETS.lock().unwrap().clone();
1887 } // Access the global structs.
1888
1889 // region Calculate size and position values.
1890 let mut box_y = wdgts.title_editbox.height() + 1; // Allow room for the Title Box
1891 box_y += 60; // Allow room for the label of the first question box.
1892 let mut qnum = 1; // Question number -- starts at 1.
1893 // endregion
1894
1895 // Set up a display box for each question in the bank.
1896 for item in usebank.question_vec.iter() {
1897
1898 // region Create the question label and set up text buffer.
1899 let qlabel = format!("Question {} : ", qnum);
1900 let mut txtbuff = TextBuffer::default();
1901 txtbuff.set_text(item.qtext.as_str());
1902 // endregion
1903
1904 // region Setup the display box and it's attributes.
1905 let mut quest_disp = TextDisplay::new(0,
1906 box_y,
1907 wdgts.scroll.w() - SCROLLBAR_WIDTH,
1908 QDISP_HEIGHT,
1909 qlabel.as_str());
1910 quest_disp.set_buffer(txtbuff);
1911 quest_disp.wrap_mode(text::WrapMode::AtBounds, 0);
1912 quest_disp.set_color(fltk::enums::Color::White);
1913 quest_disp.set_text_size(22);
1914 quest_disp.set_text_color(fltk::enums::Color::Black);
1915 // endregion
1916
1917 // region Setup the edit button & callback. Buttons not added to widget struct.
1918 let editbtn_x = quest_disp.x() + quest_disp.w() - 65; // Button is sized 50 X 30
1919 let editbtn_y = quest_disp.y() + quest_disp.h() - 35;
1920 let mut editbtn = Button::new(editbtn_x, editbtn_y, 50, 30, "Edit");
1921
1922 editbtn.set_callback(move |_| {
1923 println!("\n Edit button for Question #{} has been pressed. \n", qnum);
1924 qst_edit(qnum - 1);
1925 });
1926 // endregion
1927
1928 // region Increment values and push/add items to WIDGETS struct
1929 box_y += QDISP_HEIGHT; // Increment the question display widget position.
1930 qnum += 1; // Increment the question display number.
1931
1932 wdgts.qstn_boxes.push(quest_disp.clone());
1933 wdgts.scroll.add(&quest_disp);
1934 wdgts.scroll.add(&editbtn);
1935 // endregion
1936 }
1937 *WIDGETS.lock().unwrap() = wdgts.clone(); // Update the WIDGET global variable.
1938 }
1939
1940 /// Check to see whether or not a bank has been loaded into memory.
1941 ///
1942 pub fn check_for_bank_loaded() -> bool {
1943 let usebank = CURRENT_BANK.lock().unwrap().clone();
1944 if usebank.bank_title.is_empty() {
1945 println!("\n No bank has been loaded. \n"); // todo: Find a non-terminal way to display this.
1946 println!(" Please open a bank. \n");
1947 false
1948 } else {
1949 true
1950 }
1951 }
1952
1953 /// Gets an FLTK window's position and dimension attributes.
1954 /// Here for debugging purposes only.
1955 pub fn get_window_attrs(win: &Window) {
1956 // For debugging purposes. Move to lib.utils???
1957
1958 let xxx = win.x();
1959 let yyy = win.y();
1960 let www = win.w();
1961 let hhh = win.h();
1962
1963 println!("\n The position of the primary window is : ({}, {}) \n", xxx, yyy);
1964 println!("The size of the primary window is : ({}, {}) \n", www, hhh);
1965 }
1966
1967} // End misc module
1968