Skip to content

Commit

Permalink
Rework modals (#100)
Browse files Browse the repository at this point in the history
* feat(framework): Add builder for modals
* feat(framework): Finish modal builder class
* refactor(framework): Deprecate old class
* tests(framework): Fix tests
* feat(framework): Apply suggestions
* refactor(framework): Replace custom stage class with property
* feat(framework): Separate build and show for modals
* feat(framework): Only allow components extending parent to be used in the modal builder
* docs(framework): Update modal docs
  • Loading branch information
LeStegii authored May 22, 2024
1 parent dfc427c commit 9e32a62
Show file tree
Hide file tree
Showing 7 changed files with 378 additions and 124 deletions.
11 changes: 10 additions & 1 deletion ERROR_CODES.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@ public class MyController {
- Runtime: ✅
- Annotation Processor: ✅

This error if an event method overrides another event method as this would lead to the overriding method being called twice.
This error is thrown if an event method overrides another event method as this would lead to the overriding method being
called twice.

```java
public class MyController extends BaseController {
Expand All @@ -230,6 +231,14 @@ public class BaseController {
}
```

### 1014: `The same component instance can only be included in one scene.`

- Runtime: ✅
- Annotation Processor: ❌

This error is thrown when a component instance that is already included in a scene is used in a modal.
A node can only be included in one scene, therefore using an already used component instance in a modal

## Resources

### 2000: `Could not find resource '*'.`
Expand Down
31 changes: 24 additions & 7 deletions docs/features/4-modals.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,20 @@ Modals are a special type of window that can be used to display a controller on
can be used to display popup windows, dialogs, etc.

The framework provides a `Modals` class that can be used to display a modal.
When using `showModal()` a stage will be created and configured to be displayed above the current stage.
A BiConsumer provides access to the component instance and the stage for configuring other things.
Using a builder, a stage can be configured and then be displayed.

A BiConsumer provides access to the component instance and the stage for configuring things the builder doesn't provide.
It can be set using `init()`.

Enabling `dialog()` adds some default styling to the stage.
This has been added as an option to enable the legacy default styling present in the old modal class.

Parameters can be passed by using the `param()` method in form of a map (see `show()` in the `FulibFxApp` class).

Enabling `destroyOnClose()` configures the component to be destroyed upon closing the modal.
This is enabled by default and shouldn't be changed if not necessary.

The stage can either be built using `build()` and then displayed by yourself, or built and displayed at once using `show()`.

When displaying the component, the parameters `modalStage` and `ownerStage` will be passed so that the modal can for
example be closed from inside the component class.
Expand Down Expand Up @@ -37,11 +49,16 @@ public class ModalComponent extends VBox {
```

```java
// As every modal needs its own instance, we use a provider (e.g. with Dagger)
Modals.showModal(app, modalComponentProvider.get(), (stage, component) -> {
stage.doSomething();
component.doSomethingElse();
});
Modals modals = new Modals(app); // Can also be injected by Dagger

modals
.modal(myModalComponent)
.dialog(true) // Apply the legacy fulibFx styling
.init((stage, component) -> {
stage.setTitle(component.getString());
})
.params(Map.of("key", value))
.show();
```

---
Expand Down
228 changes: 228 additions & 0 deletions framework/src/main/java/org/fulib/fx/constructs/Modals.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package org.fulib.fx.constructs;

import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.paint.Paint;
import javafx.stage.*;
import org.fulib.fx.FulibFxApp;
import org.fulib.fx.util.ControllerUtil;

import javax.inject.Inject;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;

import static org.fulib.fx.util.FrameworkUtil.error;

public class Modals {

/**
* Key of the property for defining a stage as a modal stage
*/
private static final String MODAL_STAGE = "fulibFx.stage.isModal";

FulibFxApp app;

@Inject
public Modals(FulibFxApp app) {
this.app = app;
}

/**
* Creates a new modal instance for building modals.
*
* @param component The component instance to display
* @param <T> The type of component
* @return A modal instance
*/
public <T extends Parent> ModalBuilder<T> modal(T component) {
if (!ControllerUtil.isComponent(component)) {
throw new IllegalArgumentException(error(1000));
}
return new ModalBuilder<>(app, component);
}


/**
* Builder class for displaying modals.
*
* @param <T> The type of the component
*/
public static class ModalBuilder<T extends Parent> {

/**
* Initializes the stage with some default options like transparency and modality
*/
private final BiConsumer<Stage, T> FULIBFX_DIALOG = ((modalStage, component) -> {
modalStage.getScene().setFill(Paint.valueOf("transparent"));
modalStage.initStyle(StageStyle.TRANSPARENT);
modalStage.initModality(Modality.WINDOW_MODAL);
modalStage.setAlwaysOnTop(true);
});

private final T component;
private final FulibFxApp app;

private BiConsumer<Stage, T> initializer;
private Stage owner;
private Map<String, Object> params;
private boolean destroyOnClose = true;
private boolean dialog = false;

public ModalBuilder(FulibFxApp app, T component) {
this.app = app;
this.component = component;
this.owner = app.stage();
}

/**
* Adds an initializer.
* <p>
* If another initializer has been added already, the new initializer will be called after the previous one.
*
* @param initializer The initializer to add
* @return The current modal instance
*/
public ModalBuilder<T> init(BiConsumer<Stage, T> initializer) {
if (this.initializer == null) {
this.initializer = initializer;
return this;
}
this.initializer = this.initializer.andThen(initializer);
return this;
}

/**
* Sets the owner stage.
* <p>
* If the owner stage is closed, the modal will be closed as well.
* <p>
* If no owner is set, the {@link FulibFxApp#stage()} will be used.
*
* @param owner The owner stage.
* @return The current modal instance
*/
public ModalBuilder<T> owner(Stage owner) {
this.owner = owner;
return this;
}

/**
* Adds a pre-made initializer with some default options regarding transparency and modality.
* <p>
* This will set the stage style to {@link StageStyle#TRANSPARENT}, the modality to {@link Modality#WINDOW_MODAL}
* and the scene fill to transparent (see {@link ModalBuilder#FULIBFX_DIALOG}).
*
* @param dialog Whether the default dialog options should be used
* @return The current modal instance
*/
public ModalBuilder<T> dialog(boolean dialog) {
this.dialog = dialog;
return this;
}

/**
* Sets the parameters to be used when initializing/rendering the component.
* <p>
* The default parameters "modalStage" and "ownerStage" will be added automatically if they are not present already.
* The modal stage is the current modal stage and the owner stage is the stage that opened the modal (see {@link ModalBuilder#owner(Stage)}).
*
* @param params The parameter map
* @return The current modal instance
*/
public ModalBuilder<T> params(Map<String, Object> params) {
this.params = params;
return this;
}

public ModalBuilder<T> destroyOnClose(boolean destroyOnClose) {
this.destroyOnClose = destroyOnClose;
return this;
}

/**
* Builds the stage for the current modal.
* <p>
* This can only be called once per modal builder.
*
* @return The stage for the current modal
*/
public Stage build() {

if (component.getScene() != null) {
throw new RuntimeException(error(1014));
}

Stage modalStage = new Stage();

modalStage.getProperties().put(MODAL_STAGE, true);

if (destroyOnClose) {
modalStage.addEventHandler(WindowEvent.WINDOW_HIDING, event -> app.frameworkComponent().controllerManager().destroy(component));
}

// Add additional default parameters
Map<String, Object> parameters = params == null ? new HashMap<>() : new HashMap<>(params);
parameters.putIfAbsent("modalStage", modalStage);
parameters.putIfAbsent("ownerStage", owner);

// Initialize and render the component
app.frameworkComponent().controllerManager().init(component, parameters);
Node rendered = app.frameworkComponent().controllerManager().render(component, parameters);

// As the displayed component will be the root of a stage, it has to be a parent
if (!(rendered instanceof Parent parent)) {
throw new IllegalArgumentException(error(1011).formatted(component.getClass().getName()));
}

// Set the title if present
app.applyTitle(component, modalStage);

// Setup the stage and scene
Scene scene = new Scene(parent);
modalStage.setScene(scene);
modalStage.initOwner(owner);
if (dialog) {
FULIBFX_DIALOG.accept(modalStage, component);
}
if (initializer != null) {
initializer.accept(modalStage, component);
}
return modalStage;
}

/**
* Builds and displays the stage for the current modal.
* <p>
* This can only be called once per modal builder.
*
* @return The stage for the current modal
*/
public Stage show() {
Stage modalStage = build();
modalStage.show();
modalStage.requestFocus();
return modalStage;
}
}

/**
* Returns a list of all visible modal stages
*
* @return A list of all visible modal stages
*/
public static List<Stage> getModalStages() {
return Window.getWindows()
.stream()
.filter(Modals::isModal)
.map(window -> (Stage) window)
.toList();
}

public static boolean isModal(Window window) {
return Boolean.parseBoolean(String.valueOf(window.getProperties().get(MODAL_STAGE)));
}

}
Loading

0 comments on commit 9e32a62

Please sign in to comment.