ScalaFX: Is it possible to define controls in objects other than the application object? - controls

What I am trying to accomplish is: having a ScalaFX application with some nice ordered objects called Buttons, Labels, Checkboxes and so on to keep everything nice and in order.
Here a little example to show what I mean:
package ButtonsAndLabel
import scalafx.Includes._
import scalafx.application.JFXApp
import scalafx.scene.Scene
import scalafx.scene.control.{ Button, Label }
import scalafx.event.ActionEvent
object Main extends JFXApp {
stage = new JFXApp.PrimaryStage {
title = "Test-Program"
scene = new Scene(300, 200) {
val label = new Label("Nothing happened yet") {
layoutX = 20
layoutY = 20
}
val button1 = new Button("Button 1") {
layoutX = 20
layoutY = 50
onAction = (e: ActionEvent) => {
label.text = "B1 klicked"
}
}
val button2 = new Button("Button 2") {
layoutX = 20
layoutY = 80
onAction = (e: ActionEvent) => {
label.text = "B2 klicked"
}
}
content = List(label, button1, button2)
}
}
}
This code shows a window with a label and two buttons, and the buttons change the text of the label.
That works fine.
But when my code grows with a lot more controls, things get messy.
That's why I tried to transfer the controls into other objects (in different files). I've put the label into an object called Labels:
package ButtonsAndLabel
import scalafx.scene.control.Label
import scalafx.event.ActionEvent
object Labels {
val label = new Label("Nothing happened yet") {
layoutX = 20
layoutY = 20
}
}
when I import this into the main-file with
import Labels.label
everything works fine.
But then I try to put the buttons into a Buttons object:
package ButtonsAndLabel
import scalafx.scene.control.Button
import scalafx.event.ActionEvent
import Labels.label
object Buttons {
val button1 = new Button("Button 1") {
layoutX = 20
layoutY = 50
onAction = (e: ActionEvent) => {
label.text = "B1 klicked"
}
}
val button2 = new Button("Button 2") {
layoutX = 20
layoutY = 80
onAction = (e: ActionEvent) => {
label.text = "B2 klicked"
}
}
}
this brings the error message when I try to compile:
[error] found : scalafx.event.ActionEvent => Unit
[error] required: javafx.event.EventHandler[javafx.event.ActionEvent]
[error] onAction = (e: ActionEvent) => {
and now I am stuck, as I don't know any Java.
Does anybody know if it is even possible what I am trying to do?
So far I have not found anything about this on the net. The problem doesn't keep me from writing the program I want, but the last application I wrote was a real mess with all the controls in one file.
Am I overlooking something obvious here?
Any help would be really appreciated.

Firstly, your approach is perfectly OK.
The error you're seeing actually has nothing to do with Java—it's output by the Scala compiler! All it's saying is that it has been supplied one type of element (in this case, a function that takes a scalafx.event.ActionEvent and that returns Unit) when it was expecting another type of element (a javafx.event.EventHandler[javafx.event.ActionEvent] instance, in this case).
ScalaFX is just a set of Scala-friendly wrappers for the JavaFX library; without the implicit conversion functions that convert between the two sets of elements, the Scala compiler will complain about finding ScalaFX elements when it needs JavaFX elements, and vice versa.
The solution is to ensure that the following import is added to each of your ScalaFX source files:
import scalafx.Includes._
(You have this at the top of your main source file, but not the others.)
This will ensure that your ScalaFX ActionEvent handler is converted into the JavaFX equivalent, thereby making your life a little easier.
This is a very common type of error with ScalaFX, which is nearly always fixed by specifying the above import. (If the import doesn't fix your problem, then you will typically have a genuine case of type confusion, in which you just plain used the wrong type of object.)
So, here's what I think your code needs to look like:
Main.scala:
import scalafx.Includes._
import scalafx.application.JFXApp
import scalafx.scene.Scene
import buttonsandlabel._
object Main extends JFXApp {
stage = new JFXApp.PrimaryStage {
title = "Test-Program"
scene = new Scene(300, 200) {
content = List(Labels.label, Buttons.button1, Buttons.button2)
}
}
}
buttonsandlabel/Labels.scala:
package buttonsandlabel
import scalafx.Includes._
import scalafx.scene.control.Label
object Labels {
val label = new Label("Nothing happened yet") {
layoutX = 20
layoutY = 20
}
}
buttonsandlabel/Buttons.scala:
package buttonsandlabel
import scalafx.Includes._
import scalafx.scene.control.Button
import scalafx.event.ActionEvent
import Labels.label
object Buttons {
val button1 = new Button("Button 1") {
layoutX = 20
layoutY = 50
onAction = (e: ActionEvent) => {
label.text = "B1 klicked"
}
}
val button2 = new Button("Button 2") {
layoutX = 20
layoutY = 80
onAction = (e: ActionEvent) => {
label.text = "B2 klicked"
}
}
}
(Note that package names, by convention, are typically all lowercase.)
One thing that you'll need to be aware of is the JavaFX Application Thread: all of your code that interacts with ScalaFX (or JavaFX) must execute on this thread. If you access ScalaFX/JavaFX from a different thread, you'll get an error exception. (This ensures that all such applications are thread-safe.) If you're unfamiliar with multi-threading, don't worry, ScalaFX initializes your application in such a way that this is fairly trivial. Usually, all that's needed is that your initialization code goes into your main application object's constructor (the object that extends JFXApp).
When you start creating ScalaFX elements in other classes and objects, you need to take extra care. An object is initialized when first referenced. If it is first referenced by code that is not executing on the JavaFX Application Thread, then you'll get thread error exceptions. One possible option is to put such code into def or lazy val members, so that they are only executed when referenced directly.
Alternatively, you may have to invoke your code via scalafx.application.Platform.runLater().
For more information on the JavaFX Application Thread, refer to the JavaFX documentation.

Related

JavaFX Media Player - Binding Progress bar with Media Player (Mac m1 Silicon)

I want to update Progress Bar with Media Player Playing. But, after start playing my progressBar fill 100% within one second while the media is 15 seconds - 5 minutes long. I can't figure out the cause.
My codes are as follows:
public static ProgressBar progress = new ProgressBar();
ObjectBinding<TimeElapsed> elapsedBinding =createElapsedBindingByBindingsAPI(player);
DoubleBinding elapsedDoubleBinding =createDoubleBindingByBindingsAPI(elapsedBinding);
progress.progressProperty().bind(elapsedDoubleBinding);
And The methods are :
public static #NotNull ObjectBinding<TimeElapsed> createElapsedBindingByBindingsAPI(
final #NotNull MediaPlayer player
) {
return Bindings.createObjectBinding(
new Callable<TimeElapsed>() {
#Override
public TimeElapsed call() throws Exception {
return new TimeElapsed(player.getCurrentTime());
}
},
player.currentTimeProperty()
);
}
public static #NotNull DoubleBinding createDoubleBindingByBindingsAPI(
final ObjectBinding<TimeElapsed> elapsedBinding
) {
return Bindings.createDoubleBinding(
new Callable<Double>() {
#Override
public Double call() throws Exception {
return elapsedBinding.getValue().getElapsed();
}
},
elapsedBinding
);
}
And the TimeElapsed class :
static class TimeElapsed {
private final double elapsed;
TimeElapsed(#NotNull Duration duration) {
elapsed = duration.toSeconds();
}
public double getElapsed() {
return elapsed;
}
}
So, what's the code changes that 1) update the progressBar with Playing, and 2) seek the song with progress bar clicked or dragged?
The progress of a ProgressBar should be, when determinate, between the values of 0.0 and 1.0 (inclusive). This means you should be dividing the current time by the total duration to get the progress and bind the progress property of the bar to that value. Note that the duration of a Media is observable and is pretty much guaranteed to be set some time after it was instantiated.
As for being able to seek when the progress bar is clicked or dragged, the simplest way—which is what I show in the example below—is to add a MOUSE_CLICKED and a MOUSE_DRAGGED handler to the progress bar, determine the ratio between the mouse's x position and the bar's width, and then seek the calculated time. Unfortunately, this setup may not exactly match up with the visuals of the progress bar because the actual "bar" is smaller than the entire space taken up by the node (at least with default styling). You would probably have to create your own control if you want "pixel perfect" behavior.
Here is a minimal example demonstrating what's discussed above:
import java.util.Optional;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.ProgressBar;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaView;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
var mediaView = new MediaView();
var progressBar = new ProgressBar();
progressBar.setMaxWidth(Double.MAX_VALUE);
StackPane.setAlignment(progressBar, Pos.BOTTOM_CENTER);
StackPane.setMargin(progressBar, new Insets(10));
var root = new StackPane(mediaView, progressBar);
primaryStage.setScene(new Scene(root, 1000, 650));
primaryStage.setTitle("Video Progress Demo");
primaryStage.show();
chooseMediaFile(primaryStage)
.ifPresentOrElse(
uri -> {
var media = new Media(uri);
var mediaPlayer = new MediaPlayer(media);
mediaPlayer.setAutoPlay(true);
mediaView.setMediaPlayer(mediaPlayer);
bindProgress(mediaPlayer, progressBar);
addSeekBehavior(mediaPlayer, progressBar);
},
Platform::exit);
}
private void bindProgress(MediaPlayer player, ProgressBar bar) {
var binding =
Bindings.createDoubleBinding(
() -> {
var currentTime = player.getCurrentTime();
var duration = player.getMedia().getDuration();
if (isValidDuration(currentTime) && isValidDuration(duration)) {
return currentTime.toMillis() / duration.toMillis();
}
return ProgressBar.INDETERMINATE_PROGRESS;
},
player.currentTimeProperty(),
player.getMedia().durationProperty());
bar.progressProperty().bind(binding);
}
private void addSeekBehavior(MediaPlayer player, ProgressBar bar) {
EventHandler<MouseEvent> onClickAndOnDragHandler =
e -> {
var duration = player.getMedia().getDuration();
if (isValidDuration(duration)) {
var seekTime = duration.multiply(e.getX() / bar.getWidth());
player.seek(seekTime);
e.consume();
}
};
bar.addEventHandler(MouseEvent.MOUSE_CLICKED, onClickAndOnDragHandler);
bar.addEventHandler(MouseEvent.MOUSE_DRAGGED, onClickAndOnDragHandler);
}
private boolean isValidDuration(Duration d) {
return d != null && !d.isIndefinite() && !d.isUnknown();
}
private Optional<String> chooseMediaFile(Stage owner) {
var chooser = new FileChooser();
chooser
.getExtensionFilters()
.add(new FileChooser.ExtensionFilter("Media Files", "*.mp4", "*.mp3", "*.wav"));
var file = chooser.showOpenDialog(owner);
return Optional.ofNullable(file).map(f -> f.toPath().toUri().toString());
}
}

How do I initialise a NativeScript app fully programmatically (without XML)?

Here's what I have so far. The background goes green (the colour of the Page), but I'd expect a purple ContentView with some text inside to fill the page, too.
Is there anything further I'm missing?
import { on, run, launchEvent } from "tns-core-modules/application";
import { Frame } from "tns-core-modules/ui/frame/frame";
import { ContentView } from "tns-core-modules/ui/content-view/content-view";
import { TextBase } from "tns-core-modules/ui/text-base/text-base";
import { Page } from "tns-core-modules/ui/page/page";
on(launchEvent, (data) => {
const frame = new Frame();
const page = new Page();
page.backgroundColor = "green";
const contentView = new ContentView();
const textBase = new TextBase();
contentView.height = 100;
contentView.width = 100;
contentView.backgroundColor = "purple";
textBase.text = "Hello, world!";
contentView._addView(textBase);
page.bindingContext = contentView;
frame.navigate({ create: () => page });
data.root = page; // Incidentally, should this be the frame or the page?
});
run();
You are almost on track, you just need slight modification on your code.
import { on, run, launchEvent } from 'tns-core-modules/application';
import { Frame } from 'tns-core-modules/ui/frame/frame';
import { ContentView } from 'tns-core-modules/ui/content-view/content-view';
import { TextField } from 'tns-core-modules/ui/text-field';
import { Page } from 'tns-core-modules/ui/page/page';
run({
create: () => {
const frame = new Frame();
frame.navigate({
create: () => {
const page = new Page();
page.backgroundColor = "green";
const contentView = new ContentView();
const textField = new TextField();
contentView.height = 100;
contentView.width = 100;
contentView.backgroundColor = "purple";
textField.text = "Hello, world!";
contentView.content = textField;
page.content = contentView;
return page;
}
});
return frame;
}
});
You don't have to wait for launch event, you could set the root frame in run method itself.
In your code, you were creating the frame but never adding it to root UI element or mark the frame itself as root element
It's recommended to use .content to add child for a ContentView / Page as they are originally designed to hold one child element only.
Use TextField / TextView for input text, TextBase is just a base class.
It seems to me that you try to overcomplicate. You can replace XML with code just by implementing createPage method - Create a page via code.
I just modified default NS + TypeScript Playground template to operate without XML - NS + TypeScript template without XML.
I think you can't leave run as empty as it is expecting an entry to start the app. From {NS} website,
You can use this file to perform app-level initializations, but the
primary purpose of the file is to pass control to the app's root
module. To do this, you need to call the application.run() method and
pass a NavigationEntry with the desired moduleName as the path to the
root module relative to your /app folder.
if you look for run code in "tns-core-modules/application"
function run(entry) {
createRootFrame.value = false;
start(entry);
}
exports.run = run;
and
function start(entry) {
if (started) {
throw new Error("Application is already started.");
}
started = true;
mainEntry = typeof entry === "string" ? { moduleName: entry } : entry;
if (!androidApp.nativeApp) {
var nativeApp = getNativeApplication();
androidApp.init(nativeApp);
}
}

JavaFX "quickfixes" with tooltips and hyperlinks

does JavaFX provide something like Eclipse Quickfixes? Meaning that you hover over a thing that is broken and got some solutions for it that you can apply immediately.
I know that there are tooltips but they can only contain text, I would need something clickable. Another solution would be something like Dialogs, but I don't want to open another window. I want it to appear on the current stage.
Any suggestions?
Edit: to make it clear, I want to adopt the concept of eclipse quickfixes onto a JavaFX based application, maybe showing a "quickfix" when hovering over a circle instance. I don't want to check any (java/javafx) source code.
Edit2: I've got a hyperlink on a tooltip now:
HBox box = new HBox();
Tooltip tooltip = new Tooltip();
tooltip.setText("Select an option:");
tooltip.setGraphic(new Hyperlink("Option 1"));
Tooltip.install(box, tooltip);
I've got three new problems now:
How to make the tooltip not disappear when leaving the HBox and staying there when entering the mouse into the tooltip?
How to add mulitple graphics / hyperlinks? Is it even possible?
How to first show the text and then, in a new line, display the graphics?
Thanks in advance!
You can add any node to a tooltip using the setGraphic() method. Here is a simple example demonstrating using a tooltip for "quick fix" functionality:
import java.util.Random;
import javafx.application.Application;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TooltipWithQuickfix extends Application {
#Override
public void start(Stage primaryStage) {
TextField textField = new TextField();
textField.pseudoClassStateChanged(PseudoClass.getPseudoClass("invalid"), true);
textField.setTextFormatter(new TextFormatter<Integer>(c -> {
if (c.getText().matches("\\d*")) {
return c ;
}
return null ;
}));
textField.textProperty().isEmpty().addListener((obs, wasEmpty, isNowEmpty) ->
textField.pseudoClassStateChanged(PseudoClass.getPseudoClass("invalid"), isNowEmpty));
Tooltip quickFix = new Tooltip();
Hyperlink setToDefault = new Hyperlink("Set to default");
Hyperlink setToRandom = new Hyperlink("Set to random");
setToDefault.setOnAction(e -> {
textField.setText("42");
quickFix.hide();
});
Random rng = new Random();
setToRandom.setOnAction(e -> {
textField.setText(Integer.toString(rng.nextInt(100)));
quickFix.hide();
});
VBox quickFixContent = new VBox(new Label("Field cannot be empty"), setToDefault, setToRandom);
quickFixContent.setOnMouseExited(e -> quickFix.hide());
quickFix.setGraphic(quickFixContent);
textField.setOnMouseEntered(e -> {
if (textField.getText().isEmpty()) {
quickFix.show(textField, e.getScreenX(), e.getScreenY());
}
});
VBox root = new VBox(textField);
root.getStylesheets().add("style.css");
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
with the stylesheet (style.css):
.root {
-fx-alignment: center ;
-fx-padding: 24 10 ;
}
.text-field:invalid {
-fx-control-inner-background: #ff7979 ;
-fx-focus-color: red ;
}

Haxe Type Not Found

I'm trying to run the most basic Haxe program but keep getting errors.
The Main.hx file looks like this:
package;
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.Event;
import flash.Lib;
import flixel.FlxGame;
import flixel.FlxState;
class Main extends Sprite {
var gameWidth:Int = 640; // Width of the game in pixels (might be less / more in actual pixels depending on your zoom).
var gameHeight:Int = 480; // Height of the game in pixels (might be less / more in actual pixels depending on your zoom).
var initialState:Class<FlxState> = MenuState; // The FlxState the game starts with.
var zoom:Float = -1; // If -1, zoom is automatically calculated to fit the window dimensions.
var framerate:Int = 60; // How many frames per second the game should run at.
var skipSplash:Bool = false; // Whether to skip the flixel splash screen that appears in release mode.
var startFullscreen:Bool = false; // Whether to start the game in fullscreen on desktop targets
// You can pretty much ignore everything from here on - your code should go in your states.
public static function main():Void
{
Lib.current.addChild(new Main());
}
public function new()
{
super();
if (stage != null)
{
init();
}
else
{
addEventListener(Event.ADDED_TO_STAGE, init);
}
}
private function init(?E:Event):Void
{
if (hasEventListener(Event.ADDED_TO_STAGE))
{
removeEventListener(Event.ADDED_TO_STAGE, init);
}
setupGame();
}
private function setupGame():Void
{
var stageWidth:Int = Lib.current.stage.stageWidth;
var stageHeight:Int = Lib.current.stage.stageHeight;
if (zoom == -1)
{
var ratioX:Float = stageWidth / gameWidth;
var ratioY:Float = stageHeight / gameHeight;
zoom = Math.min(ratioX, ratioY);
gameWidth = Math.ceil(stageWidth / zoom);
gameHeight = Math.ceil(stageHeight / zoom);
}
addChild(new FlxGame(gameWidth, gameHeight, initialState, zoom, framerate, framerate, skipSplash, startFullscreen));
}
}
Just the generic template file. When I run it in Terminal (running Mac OS X El Capitan), I get this error:
Main.hx:8: characters 7-21 : Type not found : flixel.FlxGame
Haven't had problems with the installations or anything and I am new to Haxe so I don't know where to start. Any ideas?
Thanks :)
Did you add the library when you try to run your game ?
You can do that by using the command line haxe -lib flixel -main Main ....
Or by writting an hxml file containing all your CLI arguments :
-lib flixel
-main Main
Update after #Gama11 comment :
HaxeFlixel used the OpenFL format for the compilation information (see http://www.openfl.org/documentation/projects/project-files/xml-format/).
So you should include include flixel library using : <haxelib name="flixel" />in your Project.xml file.

Can't add Keyboard Eventlistener actionscript

I am just starting to learn actionscript, and to help get used to the syntax, I am challenging myself to make a simple game where you are a circle that shoots falling blocks.
For some reason every time I try to add a keyboard event listener the game doesn't run.
Here is my player file.
package
{
import flash.display.Sprite;
import flash.events.Event;
import flash.events.KeyboardEvent;
import flash.ui.Keyboard;
public class Player extends Sprite
{
//Variables
private var playerRadius:Number = 50;
private var playerX:Number = 5;
private var playerY:Number = 5;
private var speed:Number = 0;
private var xvel:Number = 0;
public function Player()
{
init();
//Drawing
drawPlayer();
//Event Listeners
this.addEventListener(Event.ENTER_FRAME, updatePlayer);
stage.addEventListener(KeyboardEvent.KEY_DOWN, controlPlayer);
}
//Update
public function updatePlayer(event:Event):void{
this.x ++;
}
//Draw
private function drawPlayer():void{
graphics.beginFill(0xFF0000);
graphics.drawCircle(10,10,50);
graphics.endFill();
}
//Control
public function controlPlayer(event:KeyboardEvent):void{
if (event.keyCode == Keyboard.RIGHT) {
speed = 5;
}
}
}
}
With this code I just get a white screen, but if I comment out
stage.addEventListener(KeyboardEvent.KEY_DOWN, controlPlayer);
it works, but I don't have control of the player.
I'd appreciate any and all help!
Using your code I was able to figure out your issue which ultimately turned out to be a couple problems with your code. I'm surprised you were not seeing the following error in the Flash 'Output' Panel when you tested the application:
TypeError: Error #1009: Cannot access a property or method of a null object reference.
at Player()
at Player_fla::MainTimeline/frame1()
The first issue is that when you create an object of the type Player, it isn't yet added to the Stage, so it does not yet have access to the stage object.
Once the player object is added to the Stage, only then will you be able to add the listener for keyboard events to the stage; however, for this to happen, your Player class needs to be made aware of the fact that an instance of it was added to the stage so that it knows exactly when it should register the keyboard event listener.
Here is an updated version of your code that should resolve these issues:
package
{
import flash.display.Sprite;
import flash.events.Event;
import flash.events.KeyboardEvent;
import flash.ui.Keyboard;
public class Player extends Sprite
{
//Variables
private var playerRadius:Number = 50;
private var playerX:Number = 5;
private var playerY:Number = 5;
private var speed:Number = 0;
private var xvel:Number = 0;
public function Player()
{
init();
//Drawing
drawPlayer();
//Event Listeners
this.addEventListener(Event.ENTER_FRAME, updatePlayer);
this.addEventListener(Event.ADDED_TO_STAGE, initKeyboardListener);
}
public function initKeyboardListener(event:Event) {
stage.addEventListener(KeyboardEvent.KEY_DOWN, controlPlayer);
}
//Update
public function updatePlayer(event:Event):void{
this.x++;
}
//Draw
private function drawPlayer():void{
graphics.beginFill(0xFF0000);
graphics.drawCircle(10,10,50);
graphics.endFill();
}
//Control
public function controlPlayer(event:KeyboardEvent):void {
if (event.keyCode == Keyboard.RIGHT) {
this.speed = 5;
}
}
} // end class
} // end package
For all of this to work, don't forget to add the player object to the stage. I can only assume you have done this since you haven't shared any code showing where you use the Player class, but here is an example of what I am referring to:
import Player;
var player:Player = new Player();
stage.addChild(player);
Also, the keyboard listener simply alters the speed variable; however the speed variable hasn't been implemented anywhere else in your code, so you won't see the difference in the GUI until this is fixed. I verified that all the listeners were working as they should using trace statements.

Resources