Skip to content

Commit

Permalink
Automatically load the most recently saved dashboard file (#277)
Browse files Browse the repository at this point in the history
* Automatically load the most recently saved dashboard file

This behavior can be toggled on or off in the application settings
Refactors DashboardTab into its own upper-level class

* Automatically restore sources for components loaded from save files

Not stable for single-source widgets that support multiple data types (eg TextViewWidget)

* Restore sources for all components

* Disable widgets, not tiles, when a source is a DestroyedSource

* Disable widgets when any source is disconnected

* Fix occassionally restoring data of wrong type if > 1 type is possible

* Catch DataTypeChangedException when restoring sources from plugin load

This is needed since destroyed sources may be present after loading a save file
  • Loading branch information
SamCarlberg authored and bradamiller committed Nov 17, 2017
1 parent 57c8d3c commit f669518
Show file tree
Hide file tree
Showing 26 changed files with 627 additions and 320 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ public abstract class AbstractDataSource<T> implements DataSource<T> {
protected final BooleanProperty active = new SimpleBooleanProperty(this, "active", false);
protected final Property<T> data = new AsyncProperty<>(this, "data", null);
protected final BooleanProperty connected = new SimpleBooleanProperty(this, "connected", false);
protected final DataType dataType;
protected final DataType<T> dataType;

protected AbstractDataSource(DataType dataType) {
protected AbstractDataSource(DataType<T> dataType) {
this.dataType = requireNonNull(dataType, "dataType");
}

Expand Down Expand Up @@ -53,7 +53,7 @@ protected void setActive(boolean active) {
}

@Override
public DataType getDataType() {
public DataType<T> getDataType() {
return dataType;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ default void setData(T newValue) {
/**
* Gets the type of data that this source is providing.
*/
DataType getDataType();
DataType<T> getDataType();

/**
* Closes this data source and frees any used resources.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class DummySource<T> extends AbstractDataSource<T> {
/**
* Create a new static, unchanging source for the given data type and value.
*/
public DummySource(DataType dataType, T value) {
public DummySource(DataType<T> dataType, T value) {
super(dataType);
this.setActive(true);
this.setName(dataType.getName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public DataSource<?> forUri(String uri) {
if (!uri.startsWith(protocol)) {
throw new IllegalArgumentException("URI does not start with the correct protocol: " + uri);
}
return Sources.getDefault().computeIfAbsent(uri, () -> sourceSupplier.apply(removeProtocol(uri)));
return Sources.getDefault().get(uri).orElseGet(() -> sourceSupplier.apply(removeProtocol(uri)));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,6 @@ public List<DataSource> forType(SourceType type) {
.collect(Collectors.toList());
}

public DataSource<?> forUri(String uri) {
return computeIfAbsent(uri, () -> SourceTypes.getDefault().forUri(uri));
}

@SuppressWarnings("unchecked") //NOPMD multiple occurrences of string literal
public <T> Optional<DataSource<T>> get(String id) {
return Optional.ofNullable(sources.get(id));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,11 @@ public Duration getDebounceDelay() {
return debounceDelay;
}

/**
* Cancels the debouncer. The target will not run unless {@link #run()} is called later.
*/
public void cancel() {
future.cancel(true);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ protected AbstractWidget() {
} else {
setTitle(getName() + " (" + sources.size() + " sources)");
}
getView().setDisable(sources.stream().anyMatch(s -> !s.isConnected()));
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package edu.wpi.first.shuffleboard.api.widget;

import edu.wpi.first.shuffleboard.api.util.TypeUtils;

import java.util.stream.Stream;

/**
Expand All @@ -19,4 +21,16 @@ public interface ComponentContainer {
*/
Stream<Component> components();

/**
* Gets a stream of all the components in this container.
*/
default Stream<Component> allComponents() {
return Stream.concat(
components(),
components()
.flatMap(TypeUtils.castStream(ComponentContainer.class))
.flatMap(ComponentContainer::allComponents)
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,11 @@ public abstract class SingleSourceWidget extends AbstractWidget {

protected final ObjectProperty<DataSource> source = new SimpleObjectProperty<>(this, "source", DataSource.none());

@SuppressWarnings("JavadocMethod")
public SingleSourceWidget() {
source.addListener(__ -> sources.setAll(getSource()));
}

@Override
public final void addSource(DataSource source) throws IncompatibleSourceException {
if (getDataTypes().contains(source.getDataType())) {
this.source.set(source);
this.sources.setAll(source);
} else {
throw new IncompatibleSourceException(getDataTypes(), source.getDataType());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package edu.wpi.first.shuffleboard.api.widget;

import edu.wpi.first.shuffleboard.api.data.DataType;
import edu.wpi.first.shuffleboard.api.data.DataTypes;
import edu.wpi.first.shuffleboard.api.data.IncompatibleSourceException;
import edu.wpi.first.shuffleboard.api.sources.DataSource;

import com.google.common.collect.ImmutableSet;

import java.util.Set;

import javafx.collections.ObservableList;

/**
Expand All @@ -24,4 +30,11 @@ public interface Sourced {
*/
ObservableList<DataSource> getSources();

/**
* Gets the allowable data types for sources. Defaults to {@link DataTypes#All}.
*/
default Set<DataType> getDataTypes() {
return ImmutableSet.of(DataTypes.All);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import edu.wpi.first.shuffleboard.api.sources.DataSource;

import java.util.List;
import java.util.Set;
import java.util.stream.Stream;

import javafx.beans.property.Property;
Expand Down Expand Up @@ -51,11 +50,6 @@
*/
public interface Widget extends Component, Sourced {

/**
* Gets an unmodifiable copy of this widgets supported data types.
*/
Set<DataType> getDataTypes();

/**
* Gets the user-configurable properties for this widget.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import edu.wpi.first.shuffleboard.api.util.Storage;
import edu.wpi.first.shuffleboard.api.util.TypeUtils;
import edu.wpi.first.shuffleboard.api.widget.Components;
import edu.wpi.first.shuffleboard.app.components.DashboardTab;
import edu.wpi.first.shuffleboard.app.components.DashboardTabPane;
import edu.wpi.first.shuffleboard.app.components.WidgetGallery;
import edu.wpi.first.shuffleboard.api.components.WidgetPropertySheet;
Expand Down Expand Up @@ -75,6 +76,7 @@
/**
* Controller for the main UI window.
*/
@SuppressWarnings("PMD.GodClass") // TODO refactor this class
public class MainWindowController {

private static final Logger log = Logger.getLogger(MainWindowController.class.getName());
Expand Down Expand Up @@ -231,9 +233,9 @@ private void tearDown(Plugin plugin) {
sourcesAccordion.getPanes().removeAll(sourcePanes.removeAll(plugin));
// Remove widgets
dashboard.getTabs().stream()
.filter(tab -> tab instanceof DashboardTabPane.DashboardTab)
.map(tab -> (DashboardTabPane.DashboardTab) tab)
.map(DashboardTabPane.DashboardTab::getWidgetPane)
.filter(tab -> tab instanceof DashboardTab)
.map(tab -> (DashboardTab) tab)
.map(DashboardTab::getWidgetPane)
.forEach(pane ->
pane.getTiles().stream()
.filter(tile -> plugin.getComponents().stream()
Expand Down Expand Up @@ -285,6 +287,7 @@ private MenuItem createShowAsMenuItem(String componentName, DataSource<?> source
public void setDashboard(DashboardTabPane dashboard) {
dashboard.setId("dashboard");
centerSplitPane.getItems().remove(this.dashboard);
this.dashboard.getTabs().clear(); // Lets tabs get cleaned up (e.g. cancelling deferred autopopulation calls)
this.dashboard = dashboard;
centerSplitPane.getItems().add(dashboard);
}
Expand Down Expand Up @@ -342,6 +345,7 @@ private void saveFile(File selected) {
}

currentFile = selected;
AppPreferences.getInstance().setSaveFile(currentFile);
}


Expand All @@ -356,13 +360,20 @@ public void load() throws IOException {
new FileChooser.ExtensionFilter("SmartDashboard Save File (.json)", "*.json"));

final File selected = chooser.showOpenDialog(root.getScene().getWindow());
load(selected);
}

if (selected == null) {
/**
* Loads a saved dashboard layout.
*
* @param saveFile the save file to load
*/
public void load(File saveFile) {
if (saveFile == null) {
return;
}

try {
Reader reader = Files.newReader(selected, Charset.forName("UTF-8"));
Reader reader = Files.newReader(saveFile, Charset.forName("UTF-8"));

DashboardData dashboardData = JsonBuilder.forSaveFile().fromJson(reader, DashboardData.class);
setDashboard(dashboardData.getTabPane());
Expand All @@ -372,7 +383,8 @@ public void load() throws IOException {
return;
}

currentFile = selected;
currentFile = saveFile;
AppPreferences.getInstance().setSaveFile(currentFile);
}

/**
Expand Down Expand Up @@ -451,14 +463,14 @@ private void closeCurrentTab() {
@FXML
private void showCurrentTabPrefs() {
Tab currentTab = dashboard.getSelectionModel().getSelectedItem();
if (currentTab instanceof DashboardTabPane.DashboardTab) {
((DashboardTabPane.DashboardTab) currentTab).showPrefsDialog();
if (currentTab instanceof DashboardTab) {
((DashboardTab) currentTab).showPrefsDialog();
}
}

@FXML
private void newTab() {
DashboardTabPane.DashboardTab newTab = dashboard.addNewTab();
DashboardTab newTab = dashboard.addNewTab();
dashboard.getSelectionModel().select(newTab);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import edu.wpi.first.shuffleboard.api.util.Storage;
import edu.wpi.first.shuffleboard.api.util.Time;
import edu.wpi.first.shuffleboard.app.plugin.PluginLoader;
import edu.wpi.first.shuffleboard.app.prefs.AppPreferences;
import edu.wpi.first.shuffleboard.plugin.base.BasePlugin;
import edu.wpi.first.shuffleboard.plugin.cameraserver.CameraServerPlugin;
import edu.wpi.first.shuffleboard.plugin.networktables.NetworkTablesPlugin;
Expand Down Expand Up @@ -63,8 +64,12 @@ public void start(Stage primaryStage) throws IOException {
// Set up the application thread to log exceptions instead of using printStackTrace()
// Must be called in start() because init() is run on the main thread, not the FX application thread
Thread.currentThread().setUncaughtExceptionHandler(Shuffleboard::uncaughtException);
mainPane = FXMLLoader.load(MainWindowController.class.getResource("MainWindow.fxml"));
onOtherAppStart = () -> Platform.runLater(primaryStage::toFront);

FXMLLoader loader = new FXMLLoader(MainWindowController.class.getResource("MainWindow.fxml"));
mainPane = loader.load();
final MainWindowController mainWindowController = loader.getController();

primaryStage.setScene(new Scene(mainPane));

PluginLoader.getDefault().load(new BasePlugin());
Expand All @@ -75,6 +80,11 @@ public void start(Stage primaryStage) throws IOException {
PluginLoader.getDefault().load(new NetworkTablesPlugin());
PluginLoader.getDefault().loadAllJarsFromDir(Storage.getPluginPath());

// Load the most recent save file after loading all plugins
if (AppPreferences.getInstance().isAutoLoadLastSaveFile()) {
mainWindowController.load(AppPreferences.getInstance().getSaveFile());
}

primaryStage.setTitle("Shuffleboard");
primaryStage.setMinWidth(640);
primaryStage.setMinHeight(480);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package edu.wpi.first.shuffleboard.app;

import edu.wpi.first.shuffleboard.api.components.WidgetPropertySheet;
import edu.wpi.first.shuffleboard.api.dnd.DataFormats;
import edu.wpi.first.shuffleboard.api.sources.DataSource;
import edu.wpi.first.shuffleboard.api.sources.DummySource;
import edu.wpi.first.shuffleboard.api.sources.SourceEntry;
import edu.wpi.first.shuffleboard.api.sources.SourceTypes;
import edu.wpi.first.shuffleboard.api.util.FxUtils;
import edu.wpi.first.shuffleboard.api.util.GridPoint;
import edu.wpi.first.shuffleboard.api.util.RoundingMode;
import edu.wpi.first.shuffleboard.api.util.TypeUtils;
import edu.wpi.first.shuffleboard.api.widget.Component;
import edu.wpi.first.shuffleboard.api.widget.ComponentContainer;
import edu.wpi.first.shuffleboard.api.widget.Components;
import edu.wpi.first.shuffleboard.api.widget.Layout;
import edu.wpi.first.shuffleboard.api.widget.LayoutType;
Expand All @@ -19,10 +22,11 @@
import edu.wpi.first.shuffleboard.app.components.Tile;
import edu.wpi.first.shuffleboard.app.components.TileLayout;
import edu.wpi.first.shuffleboard.app.components.WidgetPane;
import edu.wpi.first.shuffleboard.api.components.WidgetPropertySheet;
import edu.wpi.first.shuffleboard.app.components.WidgetTile;
import edu.wpi.first.shuffleboard.app.dnd.TileDragResizer;
import edu.wpi.first.shuffleboard.app.json.SourcedRestorer;
import edu.wpi.first.shuffleboard.app.prefs.AppPreferences;
import edu.wpi.first.shuffleboard.app.sources.DestroyedSource;

import org.fxmisc.easybind.EasyBind;

Expand All @@ -31,6 +35,7 @@
import java.util.Optional;
import java.util.WeakHashMap;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
Expand Down Expand Up @@ -198,6 +203,34 @@ private void initialize() {
.forEach(tile -> collapseTile(tile, oldCount - newCount, false));
}
});

// Handle restoring data sources after a widget is added from a save file before its source(s) are available
SourceTypes.getDefault().allAvailableSourceUris().addListener((ListChangeListener<String>) c -> {
while (c.next()) {
if (c.wasAdded()) {
SourcedRestorer restorer = new SourcedRestorer();
// Restore sources for top-level Sourced objects
pane.getTiles().stream()
.map(Tile::getContent)
.flatMap(TypeUtils.castStream(Sourced.class))
.forEach(sourced -> restorer.restoreSourcesFor(
sourced,
c.getAddedSubList(),
WidgetPaneController::destroyedSourceCouldNotBeRestored));

// Restore sources for all nested Sourced objects
pane.getTiles().stream()
.map(Tile::getContent)
.flatMap(TypeUtils.castStream(ComponentContainer.class))
.flatMap(ComponentContainer::allComponents)
.flatMap(TypeUtils.castStream(Sourced.class))
.forEach(sourced -> restorer.restoreSourcesFor(
sourced,
c.getAddedSubList(),
WidgetPaneController::destroyedSourceCouldNotBeRestored));
}
}
});
}

/**
Expand Down Expand Up @@ -523,4 +556,8 @@ private Optional<Runnable> collapseTile(Tile tile,
}
}

private static void destroyedSourceCouldNotBeRestored(DestroyedSource source, Throwable error) {
log.log(Level.WARNING, "Could not restore source: " + source.getId(), error);
}

}
Loading

0 comments on commit f669518

Please sign in to comment.