Loading of Custom Logisim Libraries at Startup

Added a method to load any .circ libraries in a folder named `logisim-defaults` in the user's home directory. This is run in `Startup.run()`, allowing the libraries to be automatically loaded when Logisim is first run. Unit tests and documentation for the method have also been added.
This commit is contained in:
Katie Page 2025-05-01 15:05:37 +01:00
parent 8895e463ac
commit b37ea4c429
6 changed files with 170 additions and 13 deletions

View File

@ -33,6 +33,7 @@ Project highlights:
* VHDL components (components behavior can be specified in VHDL!),
* TCL/TK console (interfaces between the circuit and the user),
* huge library of components (LEDs, TTLs, switches, SoCs),
* allows for custom libraries to be [loaded on startup](docs/automatic_library_import.md)
* supports [multiple languages](docs/docs.md#translations),
* and more!

View File

@ -0,0 +1,7 @@
# Automatically Importing Logisim Libraries
Logisim Evolution supports loading custom libraries at startup, contained in Logisim `.circ` files.
To do this, create a directory in your **home directory** named `logisim-defaults` and insert any Logisim libraries you
would like to load every time Logsim Evolution starts. **Every circuit must have a unique name, and must not be called
"main"**. This is to avoid conflicts caused by loading libraries with the same name.
![Logisim-defaults](img/logisim-defaults.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -10,7 +10,6 @@
package com.cburch.logisim.file;
import static com.cburch.logisim.file.Strings.S;
import com.cburch.logisim.gui.generic.OptionPane;
import com.cburch.logisim.std.Builtin;
import com.cburch.logisim.tools.Library;
@ -26,11 +25,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Stack;
import java.util.*;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
@ -352,6 +347,39 @@ public class Loader implements LibraryLoader {
return ret;
}
// Loads all the custom logisim (.circ) libraries found in the default library folder
public Library[] loadCustomStartupLibraries(String customLibraryDirectoryPath) {
File directory = new File(customLibraryDirectoryPath);
if (!directory.exists() || !LOGISIM_DIRECTORY.accept(directory)) {
return new Library[0];
}
var files = directory.listFiles();
if (files == null) {
return new Library[0];
}
List<Library> loadedLibraries = new ArrayList<>();
for (File file : files) {
if(!LOGISIM_FILTER.accept(file)) continue;
try{
var library = loadLogisimLibrary(file);
if(library != null && !library.getLibraries().isEmpty())
loadedLibraries.add(library);
}
catch (NullPointerException e) {
continue;
}
}
return loadedLibraries.toArray(new Library[0]);
}
public void reload(LoadedLibrary lib) {
LibraryManager.instance.reload(this, lib);
}

View File

@ -14,6 +14,7 @@ import static com.cburch.logisim.gui.Strings.S;
import com.cburch.logisim.Main;
import com.cburch.logisim.file.LoadFailedException;
import com.cburch.logisim.file.Loader;
import com.cburch.logisim.file.LogisimFileActions;
import com.cburch.logisim.fpga.download.Download;
import com.cburch.logisim.fpga.file.BoardReaderClass;
import com.cburch.logisim.generated.BuildInfo;
@ -43,11 +44,7 @@ import java.awt.event.AWTEventListener;
import java.awt.event.ContainerEvent;
import java.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import javax.help.JHelp;
import javax.swing.JButton;
import javax.swing.JCheckBox;
@ -885,6 +882,10 @@ public class Startup implements AWTEventListener {
System.exit(-1);
}
// Load in any user-defined default circuit files
var defaultLibraries = templLoader.loadCustomStartupLibraries(
System.getProperty("user.home") + File.separator + "logisim-defaults");
// load in template
loadTemplate(templLoader, templFile, templEmpty);
@ -994,8 +995,9 @@ public class Startup implements AWTEventListener {
}
// load file
Project proj = null;
if (filesToOpen.isEmpty()) {
final var proj = ProjectActions.doNew(monitor);
proj = ProjectActions.doNew(monitor);
proj.setStartupScreen(true);
if (showSplash) {
monitor.close();
@ -1003,7 +1005,6 @@ public class Startup implements AWTEventListener {
} else {
var numOpened = 0;
var first = true;
Project proj;
for (final var fileToOpen : filesToOpen) {
try {
if (testVector != null) {
@ -1047,6 +1048,9 @@ public class Startup implements AWTEventListener {
if (numOpened == 0) System.exit(-1);
}
if(proj != null)
proj.doAction(LogisimFileActions.loadLibraries(defaultLibraries, proj.getLogisimFile()));
for (final var fileToPrint : filesToPrint) {
doPrintFile(fileToPrint);
}

View File

@ -0,0 +1,117 @@
/*
* Logisim-evolution - digital logic design tool and simulator
* Copyright by the Logisim-evolution developers
*
* https://github.com/logisim-evolution/
*
* This is free software released under GNU GPLv3 license
*/
package com.cburch.logisim.file;
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.cburch.logisim.gui.start.SplashScreen;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.UUID;
public class CustomLibraryImportTest {
private static final String VALID_LOGISIM_LIBRARY_XML = """
<?xml version="1.0" encoding="UTF-8" standalone="no"?><project source="4.0.0dev" version="1.0">
This file is intended to be loaded by Logisim-evolution v4.0.0dev(https://github.com/logisim-evolution/).
<lib desc="#Wiring" name="0"><tool name="Pin"><a name="appearance" val="classic"/></tool></lib><lib desc="#Gates" name="1"/><lib desc="#Plexers" name="2"/><lib desc="#Arithmetic" name="3"/><lib desc="#Memory" name="4"/><lib desc="#I/O" name="5"/><lib desc="#TTL" name="6"/><lib desc="#TCL" name="7"/><lib desc="#Base" name="8"/><lib desc="#BFH-Praktika" name="9"/><lib desc="#Input/Output-Extra" name="10"/><lib desc="#Soc" name="11"/><main name="FourAdder"/><options><a name="gateUndefined" val="ignore"/><a name="simlimit" val="1000"/><a name="simrand" val="0"/></options><mappings><tool lib="8" map="Button2" name="Poke Tool"/><tool lib="8" map="Button3" name="Menu Tool"/><tool lib="8" map="Ctrl Button1" name="Menu Tool"/></mappings><toolbar><tool lib="8" name="Poke Tool"/><tool lib="8" name="Edit Tool"/><tool lib="8" name="Wiring Tool"/><tool lib="8" name="Text Tool"/><sep/><tool lib="0" name="Pin"/><tool lib="0" name="Pin"><a name="facing" val="west"/><a name="output" val="true"/></tool><sep/><tool lib="1" name="NOT Gate"/><tool lib="1" name="AND Gate"/><tool lib="1" name="OR Gate"/><tool lib="1" name="XOR Gate"/><tool lib="1" name="NAND Gate"/><tool lib="1" name="NOR Gate"/><sep/><tool lib="4" name="D Flip-Flop"/><tool lib="4" name="Register"/></toolbar><circuit name="FourAdder"><a name="appearance" val="logisim_evolution"/><a name="circuit" val="FourAdder"/><a name="circuitnamedboxfixedsize" val="true"/><a name="simulationFrequency" val="1.0"/><comp lib="0" loc="(110,140)" name="Pin"><a name="appearance" val="NewPins"/></comp><comp lib="0" loc="(110,180)" name="Pin"><a name="appearance" val="NewPins"/></comp><comp lib="0" loc="(110,210)" name="Pin"><a name="appearance" val="NewPins"/></comp><comp lib="0" loc="(110,250)" name="Pin"><a name="appearance" val="NewPins"/></comp><comp lib="0" loc="(290,190)" name="Pin"><a name="appearance" val="NewPins"/><a name="facing" val="west"/><a name="output" val="true"/></comp><comp lib="1" loc="(190,160)" name="AND Gate"/><comp lib="1" loc="(190,230)" name="AND Gate"/><comp lib="1" loc="(260,190)" name="AND Gate"/><wire from="(110,140)" to="(140,140)"/><wire from="(110,180)" to="(140,180)"/><wire from="(110,210)" to="(140,210)"/><wire from="(110,250)" to="(140,250)"/><wire from="(190,160)" to="(210,160)"/><wire from="(190,230)" to="(210,230)"/><wire from="(210,160)" to="(210,170)"/><wire from="(210,210)" to="(210,230)"/><wire from="(260,190)" to="(290,190)"/></circuit></project>
""";
private static final String RANDOM_FILE_DATA = "Lorem Ipsum";
private static class TestFile {
public String contents;
public String fileName;
public TestFile(String contents, String fileName) {
this.contents = contents;
this.fileName = fileName;
}
}
/**
* Test method for {@link com.cburch.logisim.file.Loader#loadCustomStartupLibraries(String)}
*/
@Test
public final void testLoadCustomStartupLibraries() {
// Create a loader to test loading files with
var testLoader = new Loader(new SplashScreen());
// Test for an invalid file type
TestFile[] files = new TestFile[]{ new TestFile(RANDOM_FILE_DATA, "not_a_logisim_file.txt") };
String testDirectoryPath = generateTestDefaultLibrary(files);
var loadedLibraries = testLoader.loadCustomStartupLibraries(testDirectoryPath);
assertEquals(0, loadedLibraries.length);
// Test for if the directory does not exist
String fakeDirectory = findFakeDirectory();
loadedLibraries = testLoader.loadCustomStartupLibraries(fakeDirectory);
assertEquals(0, loadedLibraries.length);
// Test for if the directory contains no files
files = new TestFile[0];
testDirectoryPath = generateTestDefaultLibrary(files);
loadedLibraries = testLoader.loadCustomStartupLibraries(testDirectoryPath);
assertEquals(0, loadedLibraries.length);
// Test if the file given is invalid
files = new TestFile[]{ new TestFile(RANDOM_FILE_DATA, "bad_file.circ") };
testDirectoryPath = generateTestDefaultLibrary(files);
loadedLibraries = testLoader.loadCustomStartupLibraries(testDirectoryPath);
assertEquals(0, loadedLibraries.length);
// Test if the system works correctly under normal circumstances
files = new TestFile[]{ new TestFile(VALID_LOGISIM_LIBRARY_XML, "good_file.circ") };
testDirectoryPath = generateTestDefaultLibrary(files);
loadedLibraries = testLoader.loadCustomStartupLibraries(testDirectoryPath);
assertEquals(1, loadedLibraries.length);
}
private String findFakeDirectory(){
String directoryPath;
while(true) {
directoryPath = System.getProperty("java.io.tmpdir") + File.separator + UUID.randomUUID();
File directory = new File(directoryPath);
if(!directory.exists()) break;
}
return directoryPath;
}
private String generateTestDefaultLibrary(TestFile[] files){
String directoryPath;
while(true) {
directoryPath = System.getProperty("java.io.tmpdir") + File.separator + UUID.randomUUID();
File directory = new File(directoryPath);
if(directory.mkdir()) break;
}
for (TestFile file : files) {
String fileName = directoryPath + File.separator + file.fileName;
File newDirectoryFile = new File(fileName);
try {
boolean fileCreatedSuccess = newDirectoryFile.createNewFile();
if (!fileCreatedSuccess) throw new IOException("Unable to create new directory");
FileWriter fileWriter = new FileWriter(fileName);
fileWriter.write(file.contents);
fileWriter.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return directoryPath;
}
}