Skip to content

Commit

Permalink
implement module import/export
Browse files Browse the repository at this point in the history
  • Loading branch information
Spydr06 committed Apr 2, 2023
1 parent e6128f4 commit d915ab8
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 11 deletions.
2 changes: 1 addition & 1 deletion examples/4-bit-adder.lrsproj
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"tps": 10,
"tps": 10,
"modules": {
"Half-Adder": {
"name": "Half-Adder",
Expand Down
102 changes: 97 additions & 5 deletions src/application/gactions.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::*;
use crate::{fatal::*, project::Project, simulator::Simulator};
use crate::{fatal::*, project::Project, simulator::Simulator, FileExtension, export::ModuleFile};

#[derive(Default, Clone, Copy)]
enum Theme {
Expand Down Expand Up @@ -82,7 +82,7 @@ impl<'a> From<&GAction<'a>> for gio::SimpleAction {
}

lazy_static! {
pub(super) static ref ACTIONS: [GAction<'static>; 19] = [
pub(super) static ref ACTIONS: [GAction<'static>; 21] = [
GAction::new("quit", &["<primary>Q", "<primary>W"], None, None, Application::gaction_quit),
GAction::new("about", &["<primary>comma"], None, None, Application::gaction_about),
GAction::new("save", &["<primary>S"], None, None, Application::gaction_save),
Expand All @@ -101,7 +101,9 @@ lazy_static! {
GAction::new("edit-module", &[], Some(glib::VariantTy::STRING), None, Application::gaction_edit_module),
GAction::new("search-module", &["<primary>F"], None, None, Application::gaction_search_module),
GAction::new("change-theme", &[], None, Some((glib::VariantTy::BYTE, Theme::SystemPreference.to_variant())), Application::gaction_change_theme),
GAction::new("change-tick-speed", &[], None, Some((glib::VariantTy::INT32, Simulator::DEFAULT_TICKS_PER_SECOND.to_variant())), Application::gaction_change_tps)
GAction::new("change-tick-speed", &[], None, Some((glib::VariantTy::INT32, Simulator::DEFAULT_TICKS_PER_SECOND.to_variant())), Application::gaction_change_tps),
GAction::new("export-module", &[], Some(glib::VariantTy::STRING), None, Application::gaction_export_module),
GAction::new("import-module", &[], None, None, Application::gaction_import_module)
];
}

Expand Down Expand Up @@ -224,6 +226,93 @@ impl Application {
action.set_state(&new.to_variant());
}

fn gaction_export_module(self, _: &gio::SimpleAction, parameter: Option<&glib::Variant>) {
let module_id = parameter
.expect("could not get module paramerter")
.get::<String>()
.expect("the parameter needs to be of type `String`");

let window = self.active_window().unwrap();
let export_dialog = gtk::FileChooserNative::builder()
.transient_for(&window)
.modal(true)
.title(&format!("Export `{module_id}` As"))
.action(gtk::FileChooserAction::Save)
.accept_label("Save")
.filter(&ModuleFile::file_filter())
.cancel_label("Cancel")
.build();

export_dialog.connect_response({
let file_chooser = RefCell::new(Some(export_dialog.clone()));
glib::clone!(@weak self as app, @weak window => move |_, response| {
if let Some(file_chooser) = file_chooser.take() {
if response != gtk::ResponseType::Accept {
return;
}
if let Some(file) = file_chooser.files().snapshot().into_iter().nth(0) {
let file: gio::File = file
.downcast()
.expect("unexpected type returned from file chooser");
if !file.query_exists(gio::Cancellable::NONE) {
file.create(gio::FileCreateFlags::NONE, gio::Cancellable::NONE).unwrap_or_die();
}
let app_template = app.imp();
let mod_file = ModuleFile::from_existing(&app_template.project().lock().unwrap(), module_id.clone()).unwrap();
if let Err(msg) = mod_file.export(&file) {
dialogs::run(app, window, msg, dialogs::basic_error);
}
}
} else {
warn!("got file chooser response more than once");
}
})
});

export_dialog.show();
}

fn gaction_import_module(self, _: &gio::SimpleAction, _: Option<&glib::Variant>) {
let window = self.active_window().unwrap();

let open_dialog = gtk::FileChooserNative::builder()
.transient_for(&window)
.modal(true)
.title("Import Module")
.action(gtk::FileChooserAction::Open)
.accept_label("Open")
.cancel_label("Cancel")
.filter(&ModuleFile::file_filter())
.build();

open_dialog.connect_response({
let file_chooser = RefCell::new(Some(open_dialog.clone()));
glib::clone!(@weak self as app => move |_, response| {
if let Some(file_chooser) = file_chooser.take() {
if response != gtk::ResponseType::Accept {
return;
}
for file in file_chooser.files().snapshot().into_iter() {
let file: gio::File = file
.downcast()
.expect("unexpected type returned from file chooser");
let app = app.clone();
if let Err(message) = ModuleFile::import(&file)
.map(|mod_file| mod_file.merge(&app)).flatten() {
let window = app.active_window().unwrap();
dialogs::run(app, window, message, dialogs::basic_error);
}
}
}
else {
warn!("got file chooser response after window was freed");
}
})
});

open_dialog.show();
}

pub(super) fn close_current_file<F>(&self, after: F)
where
F: Fn(&str) + 'static,
Expand Down Expand Up @@ -308,7 +397,7 @@ impl Application {

open_dialog.connect_response({
let file_chooser = RefCell::new(Some(open_dialog.clone()));
glib::clone!(@weak app => move |_, response| {
glib::clone!(@weak app, @weak window => move |_, response| {
if let Some(file_chooser) = file_chooser.take() {
if response != gtk::ResponseType::Accept {
return;
Expand All @@ -319,7 +408,10 @@ impl Application {
.expect("unexpected type returned from file chooser");
match Project::load_from(&file) {
Ok(project) => app.imp().set_project(project, Some(file)),
_ => error!("Error opening file")
Err(error) => {
dialogs::run(app, window, format!("Error opening `{}`: {}", file.path().unwrap().to_str().unwrap(), error), dialogs::basic_error);
break;
}
}
}
}
Expand Down
94 changes: 94 additions & 0 deletions src/export.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use crate::{simulator::Module, project::Project, FileExtension, ui::dialogs, application::Application};

use serde::{Serialize, Deserialize};
use gtk::{gio, prelude::FileExt, subclass::prelude::ObjectSubclassIsExt};
use std::{fs::{OpenOptions, File}, io::{Write, BufReader}, collections::HashMap};

#[derive(Serialize, Deserialize)]
pub struct ModuleFile {
main_name: String,
modules: HashMap<String, Module>
}

impl FileExtension for ModuleFile {
const FILE_EXTENSION: &'static str = "lrsmod";
const FILE_PATTERN: &'static str = "*.lrsmod";

fn file_filter() -> gtk::FileFilter {
let filter = gtk::FileFilter::new();
filter.set_name(Some("LogicRs module files"));
filter.add_pattern(Self::FILE_PATTERN);
filter
}
}

impl ModuleFile {
pub fn from_existing(project: &Project, mod_name: String) -> Option<Self> {
project.module(&mod_name).map(|module| {
let mut mod_file = Self {
main_name: mod_name.clone(),
modules: HashMap::from([(mod_name.clone(), module.clone())])
};

project.collect_dependencies(&mod_name, &mut mod_file.modules);
mod_file
})
}

pub fn export(&self, file: &gio::File) -> Result<(), String> {
info!("Exporting to `{}`...", file.path().unwrap().to_str().unwrap());
let mut f = OpenOptions::new()
.write(true)
.truncate(true)
.open(file.path().unwrap())
.map_err(|err| err.to_string())?;

let serialized = serde_json::to_string(self)
.map_err(|err| err.to_string())?;
let bytes_written = f.write(serialized.as_bytes())
.map_err(|err| err.to_string())?;

info!("Wrote {bytes_written} bytes to `{}` successfully", file.path().unwrap().to_str().unwrap());
Ok(())
}

pub fn import(file: &gio::File) -> Result<Self, String> {
let f = File::open(file.path().unwrap())
.map_err(|err| err.to_string())?;
let mod_file: Self = serde_json::from_reader(BufReader::new(f))
.map_err(|err| err.to_string())?;

info!("Imported module `{}` from file `{}`", mod_file.main_name, file.path().unwrap().to_str().unwrap());
Ok(mod_file)
}

fn check_compat(&self, project: &mut Project) -> Vec<String> {
self.modules.iter()
.map(|(name, _)| name.clone())
.filter(|name| project.module(name).is_some())
.collect::<Vec<_>>()
}

pub fn merge(self, app: &Application) -> Result<(), String> {
let project = &mut app.imp().project().lock().unwrap();
let window = app.imp().window().borrow();
let window = window.as_ref().unwrap();

let conflicting = self.check_compat(project);
if conflicting.is_empty() {
// everything's good
for (_, module) in self.modules.into_iter() {
window.add_module_to_ui(app, &module);
project.add_existing_module(module);
}
return Ok(());
}

// construct error message
let message = format!(
"Error importing `{}`; Conflicting modules exist:\n\t{}",
self.main_name, conflicting.join(",\n\t")
);
Err(message)
}
}
8 changes: 8 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod simulator;
mod config;
mod fatal;
mod project;
mod export;

#[macro_use]
extern crate log;
Expand All @@ -20,6 +21,13 @@ extern crate lazy_static;
use adw::prelude::ApplicationExtManual;
use application::Application;

trait FileExtension {
const FILE_EXTENSION: &'static str;
const FILE_PATTERN: &'static str;

fn file_filter() -> gtk::FileFilter;
}

fn main() {
env_logger::init();
info!("Starting up LogicRs...");
Expand Down
31 changes: 26 additions & 5 deletions src/project.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::{collections::*, sync::*, fs::{OpenOptions, File}, io::{Write, BufReader}};
use serde::{Serialize, Deserialize, ser::SerializeStruct};
use gtk::{gio, prelude::FileExt};
use crate::{simulator::{*, builtin::BUILTINS}, renderer::vector::Vector2};
use crate::{simulator::{*, builtin::BUILTINS}, renderer::vector::Vector2, FileExtension};

pub type ProjectRef = Arc<Mutex<Project>>;

Expand All @@ -25,21 +25,24 @@ impl Serialize for Project {
let mut state = serializer.serialize_struct("Project", 2)?;
state.serialize_field("modules", &HashMap::<&String, &Module>::from_iter(self.modules.iter().filter(|(_, module)| !module.builtin())))?;
state.serialize_field("main_plot", &self.main_plot)?;
state.serialize_field("tps", &self.tps)?;
state.end()
}
}

impl Project {
pub const FILE_EXTENSION: &'static str = "lrsproj";
pub const FILE_PATTERN: &'static str = "*.lrsproj";
impl FileExtension for Project {
const FILE_EXTENSION: &'static str = "lrsproj";
const FILE_PATTERN: &'static str = "*.lrsproj";

pub fn file_filter() -> gtk::FileFilter {
fn file_filter() -> gtk::FileFilter {
let filter = gtk::FileFilter::new();
filter.set_name(Some("LogicRs project files"));
filter.add_pattern(Self::FILE_PATTERN);
filter
}
}

impl Project {
pub fn new(modules: Vec<Module>) -> Self {
Self {
modules: modules.iter().map(|module| (module.name().to_owned(), module.clone())).collect(),
Expand Down Expand Up @@ -93,6 +96,10 @@ impl Project {
&mut self.modules
}

pub fn add_existing_module(&mut self, module: Module) {
self.modules.insert(module.name().clone(), module);
}

pub fn add_module(&mut self, mut module: Module) {
if module.plot().is_some() {
let num_inputs = module.get_num_inputs();
Expand Down Expand Up @@ -148,4 +155,18 @@ impl Project {
pub fn set_tps(&mut self, tps: i32) {
self.tps = tps
}

pub fn collect_dependencies(&self, mod_name: &String, modules: &mut HashMap<String, Module>) {
self.modules.get(mod_name)
.map(|module| module.plot())
.flatten()
.map(|plot| plot.blocks()
.iter()
.for_each(|(_, block)| {
if let Some(module) = self.modules.get(block.module_id()).filter(|m| !m.builtin() && !modules.contains_key(m.name())) {
modules.insert(module.name().clone(), module.clone());
self.collect_dependencies(module.name(), modules);
}
}));
}
}

0 comments on commit d915ab8

Please sign in to comment.