package bluej;
import bluej.collect.DataCollector;
import bluej.extensions.event.ApplicationEvent;
import bluej.extmgr.ExtensionWrapper;
import bluej.extmgr.ExtensionsManager;
import bluej.pkgmgr.PkgMgrFrame;
import bluej.pkgmgr.Project;
import bluej.prefmgr.PrefMgr;
import bluej.utility.Debug;
import bluej.utility.DialogManager;
import bluej.utility.Utility;
import bluej.utility.javafx.FXPlatformRunnable;
import bluej.utility.javafx.JavaFXUtil;
import de.codecentric.centerdevice.MenuToolkit;
import de.codecentric.centerdevice.dialogs.about.AboutStageBuilder;
import javafx.application.Platform;
import javafx.concurrent.Worker.State;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.layout.BorderPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.html.HTMLAnchorElement;
import threadchecker.OnThread;
import threadchecker.Tag;
import javax.swing.*;
import java.awt.*;
import java.awt.desktop.QuitResponse;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.time.LocalDate;
import java.util.EventListener;
import java.util.List;
import java.util.Properties;
import java.util.Scanner;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
| BlueJ starts here. The Boot class, which is responsible for dealing with
| specialised class loaders, constructs an object of this class to initiate the
| "real" BlueJ.
|*
* @author Michael Kolling
*/
public class Main{
private static final String MESSAGE_ROOT = "https://www.bluej.org/message/";
private static final String TESTING_MESSAGE_ROOT = "https://www.bluej.org/message_test/";
/**
* Whether we've officially launched yet. While false "open file" requests only
* set initialProject.
*/
private static boolean launched = false;
/** On MacOS X, this will be set to the project we should open (if any) */
private static List<File> initialProjects;
|
|private static QuitResponse macEventResponse = null; // used to respond to external quit events on MacOS
|
|/**
| Only used on Mac. For some reason, executing the AppleJavaExtensions open
| file handler (that is set from Boot.main) on initial load (e.g. because
| the user double-clicked a project.greenfoot file) means that later on,
| the context class loader is null on the JavaFX thread.
| Honestly, I [NB] have no idea what the hell is going on there.
| But the work-around is apparent: store the context class loader early on,
| then if it is null later, restore it. (There's no problem if the file
| handler is not executed; the context class loader is the same early on as
| later on the FX thread).
|
private static ClassLoader storedContextClassLoader;
| The mechanism to show the initial GUI
|
private static GuiHandler guiHandler = null;
| Entry point to starting up the system. Initialise the system and start
| the first package manager frame.
|
@OnThread(Tag.Any)
public Main()
{
Boot boot = Boot.getInstance();
final String[] args = Boot.cmdLineArgs;
Properties commandLineProps = boot.getCommandLineProperties();
File bluejLibDir = Boot.getBluejLibDir();
Config.initialise(bluejLibDir, commandLineProps, boot.isGreenfoot());
CompletableFuture<Stage> futureMainWindow = new CompletableFuture<>();
if (!Config.isGreenfoot())
new Thread(() -> fetchAndShowCentralMsg(PrefMgr.getFlag(PrefMgr.NEWS_TESTING) ? TESTING_MESSAGE_ROOT : MESSAGE_ROOT, futureMainWindow)).start();
if (guiHandler == null) {
guiHandler = new BlueJGuiHandler();
}
if (Config.isMacOS()) {
prepareMacOSApp();
}
SwingUtilities.invokeLater(() -> {
List<ExtensionWrapper> loadedExtensions = ExtensionsManager.getInstance().getLoadedExtensions(null);
Platform.runLater(() -> {
DataCollector.bluejOpened(getOperatingSystem(), getJavaVersion(), getBlueJVersion(), getInterfaceLanguage(), loadedExtensions);
Stage stage = processArgs(args);
futureMainWindow.complete(stage);
});
});
new Thread() {
@Override
public void run()
{
updateStats();
}
}.start();
}
| Start everything off. This is used to open the projects specified on the
| command line when starting BlueJ. Any parameters starting with '-' are
| ignored for now.
|
| @return A handle to the main window which was opened, or null if there was no window opened.
|
@OnThread(Tag.FXPlatform)
private static Stage processArgs(String[] args)
{
launched = true;
boolean oneOpened = false;
if (args.length > 0) {
for (String arg : args) {
if (!arg.startsWith("-")) {
oneOpened |= guiHandler.tryOpen(new File(arg), true);
}
}
}
if (initialProjects != null) {
for (File initialProject : initialProjects) {
oneOpened |= guiHandler.tryOpen(initialProject, true);
}
}
if (!oneOpened) {
boolean openOrphans = "true".equals(Config.getPropString("bluej.autoOpenLastProject"));
if (openOrphans && hadOrphanPackages()) {
String exists = "";
for (int i = 1; exists != null; i++) {
exists = Config.getPropString(Config.BLUEJ_OPENPACKAGE + i, null);
if (exists != null) {
oneOpened |= guiHandler.tryOpen(new File(exists), false);
}
}
}
}
Stage window = guiHandler.initialOpenComplete(oneOpened);
Boot.getInstance().disposeSplashWindow();
ExtensionsManager.getInstance().delegateEvent(new ApplicationEvent(ApplicationEvent.APP_READY_EVENT));
return window;
}
| Prepare MacOS specific behaviour (About menu, Preferences menu, Quit
| menu)
|
private static void prepareMacOSApp()
{
storedContextClassLoader = Thread.currentThread().getContextClassLoader();
initialProjects = Boot.getMacInitialProjects();
prepareMacOSMenuSwing();
prepareMacOSMenuFX();
if (Config.isGreenfoot())
{
Debug.message("Disabling App Nap");
try
{
Runtime.getRuntime().exec("defaults write org.greenfoot NSAppSleepDisabled -bool YES");
}
catch (IOException e)
{
Debug.reportError("Error disabling App Nap", e);
}
}
}
| Prepare Mac Application FX menu using the NSMenuFX library.
| This is needed for due to a bug in the JDK APIs, not responding to
| handleAbout() etc, when the menu is on FX.
|
private static void prepareMacOSMenuFX()
{
Platform.runLater(() -> {
FXMLLoader.setDefaultClassLoader(AboutStageBuilder.class.getClassLoader());
MenuToolkit menuToolkit = MenuToolkit.toolkit();
Menu defaultApplicationMenu = menuToolkit.createDefaultApplicationMenu(Config.getApplicationName());
menuToolkit.setApplicationMenu(defaultApplicationMenu);
defaultApplicationMenu.getItems().get(0).setOnAction(event -> guiHandler.handleAbout());
MenuItem preferences = new MenuItem(Config.getString("menu.tools.preferences"));
if (Config.hasAcceleratorKey("menu.tools.preferences")) {
preferences.setAccelerator(Config.getAcceleratorKeyFX("menu.tools.preferences"));
}
preferences.setOnAction(event -> guiHandler.handlePreferences());
defaultApplicationMenu.getItems().add(1, preferences);
defaultApplicationMenu.getItems().get(defaultApplicationMenu.getItems().size()-1).
setOnAction(event -> guiHandler.handleQuit());
});
}
| Prepare Mac Application Swing menu using the java.awt.Desktop APIs.
|
@SuppressWarnings("threadchecker")
private static void prepareMacOSMenuSwing()
{
Desktop.getDesktop().setAboutHandler(e -> {
Platform.runLater(() -> guiHandler.handleAbout());
});
Desktop.getDesktop().setPreferencesHandler(e -> {
Platform.runLater(() -> guiHandler.handlePreferences());
});
Desktop.getDesktop().setQuitHandler((e, response) -> {
macEventResponse = response;
Platform.runLater(() -> guiHandler.handleQuit());
});
Desktop.getDesktop().setOpenFileHandler(e -> {
if (launched)
{
List<File> files = e.getFiles();
Platform.runLater(() ->
{
for (File file : files)
{
guiHandler.tryOpen(file, true);
}
});
}
else
{
initialProjects = e.getFiles();
}
});
Boot.getInstance().setQuitHandler(() -> Platform.runLater(() -> guiHandler.handleQuit()));
}
| Handle the "quit" command: if any projects are open, prompt user to make sure, and quit if
|* confirmed.
*/
@OnThread(Tag.FXPlatform)
public static void wantToQuit()
{
int projectCount = Project.getOpenProjectCount();
|
|// We set a null owner here to make the dialog come to the front of all windows;
|
|// the user may have triggered the quit shortcut from any window, not just a PkgMgrFrame:
|
|int answer = projectCount <= 1 ? 0 : DialogManager.askQuestionFX(null, "quit-all");
if (answer == 0)
{
doQuit();
}
else
{
SwingUtilities.invokeLater(() ->
{
if (macEventResponse != null)
{
macEventResponse.cancelQuit();
|
|macEventResponse = null;
|
|}
|
|});
|
|}
|
|}
|
|/**
| Perform the closing down and quitting of BlueJ, including unloading
| extensions.
|
@OnThread(Tag.FXPlatform)
public static void doQuit()
{
guiHandler.doExitCleanup();
SwingUtilities.invokeLater(() -> {
ExtensionsManager extMgr = ExtensionsManager.getInstance();
extMgr.unloadExtensions();
Platform.runLater(() -> bluej.Main.exit());
});
}
| Checks if there were orphan packages on last exit by looking for
| existence of a valid BlueJ project among the saved values for the
| orphaned packages.
|
| @return whether a valid orphaned package exist.
|
public static boolean hadOrphanPackages()
{
String dir = "";
for (int i = 1; dir != null; i++) {
dir = Config.getPropString(Config.BLUEJ_OPENPACKAGE + i, null);
if (dir != null) {
if (Project.isProject(dir)) {
return true;
}
}
}
return false;
}
| Send statistics of use back to bluej.org
|
private static void updateStats()
{
String uidPropName;
String baseURL;
String appVersion;
if (Config.isGreenfoot()) {
uidPropName = "greenfoot.uid";
baseURL = "http://stats.greenfoot.org/updateGreenfoot.php";
appVersion = Boot.GREENFOOT_VERSION;
}
else {
uidPropName = "bluej.uid";
baseURL = "http://stats.bluej.org/updateBlueJ.php";
appVersion = getBlueJVersion();
}
String language = getInterfaceLanguage();
String javaVersion = getJavaVersion();
String systemID = getOperatingSystem();
String editorStats = "";
int javaEditors = Config.getEditorCount(Config.SourceType.Java);
int strideEditors = Config.getEditorCount(Config.SourceType.Stride);
try
{
if (javaEditors != -1 && strideEditors != -1)
{
editorStats = "&javaeditors=" + URLEncoder.encode(Integer.toString(javaEditors), "UTF-8")
+ "&strideeditors=" + URLEncoder.encode(Integer.toString(strideEditors), "UTF-8");
}
}
catch (UnsupportedEncodingException ex)
{
Debug.reportError(ex);
}
Config.resetEditorsCount();
String uid = Config.getPropString(uidPropName, null);
if (uid == null) {
uid = UUID.randomUUID().toString();
Config.putPropString(uidPropName, uid);
}
else if (uid.equalsIgnoreCase("private")) {
return;
}
try {
URL url = new URL(baseURL +
"?uid=" + URLEncoder.encode(uid, "UTF-8") +
"&osname=" + URLEncoder.encode(systemID, "UTF-8") +
"&appversion=" + URLEncoder.encode(appVersion, "UTF-8") +
"&javaversion=" + URLEncoder.encode(javaVersion, "UTF-8") +
"&language=" + URLEncoder.encode(language, "UTF-8") +
editorStats
);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.connect();
int rc = conn.getResponseCode();
conn.disconnect();
if (rc != 200) Debug.reportError("Update stats failed, HTTP response code: " + rc);
} catch (Exception ex) {
Debug.reportError("Update stats failed: " + ex.getClass().getName() + ": " + ex.getMessage());
}
}
private static String getBlueJVersion()
{
return Boot.BLUEJ_VERSION;
}
private static String getOperatingSystem()
{
String osArch = System.getProperty("os.arch");
if (Config.isWinOS())
{
String arch = System.getenv("PROCESSOR_ARCHITECTURE");
String wow64Arch = System.getenv("PROCESSOR_ARCHITEW6432");
osArch += (arch != null && arch.endsWith("64"))
|| (wow64Arch != null && wow64Arch.endsWith("64"))
? "(64)" : "";
}
return System.getProperty("os.name") +
"/" + osArch +
"/" + System.getProperty("os.version");
}
private static String getJavaVersion()
{
return System.getProperty("java.version");
}
private static String getInterfaceLanguage()
{
return Config.language;
}
| Exit BlueJ.
|
| The open frame count should be zero by this point as PkgMgrFrame is
| responsible for cleaning itself up before getting here.
|
@OnThread(Tag.FXPlatform)
private static void exit()
{
DataCollector.bluejClosed();
Config.handleExit();
JavaFXUtil.runAfterCurrent(() -> SwingUtilities.invokeLater(() -> System.exit(0)));
}
public static ClassLoader getStoredContextClassLoader()
{
return storedContextClassLoader;
}
| Set the inital GUI, created after the initial project is opened.
| @param initialGUI A consume which displays the GUI for the initial project.
|
public static void setGuiHandler(GuiHandler initialGUI)
{
Main.guiHandler = initialGUI;
}
| Fetch and show a message from bluej.org, by looking at the central index then fetching
| the current message (if any, and if unseen).
|
| @param withStage A future which will complete with a parent window (or null if none).
| The message should be shown as the modal child of this window.
|
private static void fetchAndShowCentralMsg(String messageRoot, CompletableFuture<Stage> withStage)
{
try
{
Scanner scanner = new Scanner(new URL(messageRoot + "latest.txt").openStream(), "UTF-8").useDelimiter("\n");
LocalDate startDate = LocalDate.parse(scanner.nextLine());
LocalDate endDate = LocalDate.parse(scanner.nextLine());
LocalDate lastSeen = null;
try
{
lastSeen = LocalDate.parse(Config.getPropString(Config.MESSAGE_LATEST_SEEN));
}
catch (Exception e)
{
}
boolean seenMessage = lastSeen != null && (startDate.isBefore(lastSeen) || startDate.isEqual(lastSeen));
boolean expired = LocalDate.now().isAfter(endDate);
if (!seenMessage && !expired)
{
Platform.runLater(() -> {
WebView webView = new WebView();
FXPlatformRunnable preventTimeout = JavaFXUtil.runAfter(Duration.seconds(5), () -> {
webView.getEngine().getLoadWorker().cancel();
});
AtomicBoolean shownWindow = new AtomicBoolean(false);
JavaFXUtil.addChangeListener(webView.getEngine().getLoadWorker().stateProperty(), state -> {
if (state == State.SUCCEEDED && !shownWindow.get())
{
shownWindow.set(true);
JavaFXUtil.runNowOrLater(() -> {
preventTimeout.run();
makeLinksOpenExternally(webView.getEngine().getDocument());
});
withStage.handle((parent, error) -> {
if (parent != null)
{
JavaFXUtil.runNowOrLater(() -> showMessageWindow(startDate, webView, parent));
}
return null;
});
}
});
webView.getEngine().load(messageRoot + startDate.toString() + ".html");
});
}
}
catch (MalformedURLException e)
{
Debug.reportError(e);
}
catch (IOException e)
{
}
}
| Overrides the click behaviour of all <a> tags in the document
| so that they open in an external browser window.
|
| @param document The document in which to override the links
|
private static void makeLinksOpenExternally(Document document)
{
NodeList nodeList = document.getElementsByTagName("a");
for (int i = 0; i < nodeList.getLength(); i++)
{
Node node= nodeList.item(i);
EventTarget eventTarget = (EventTarget) node;
eventTarget.addEventListener("click", new org.w3c.dom.events.EventListener()
{
@Override
public void handleEvent(org.w3c.dom.events.Event evt)
{
EventTarget target = evt.getCurrentTarget();
HTMLAnchorElement anchorElement = (HTMLAnchorElement) target;
String href = anchorElement.getHref();
SwingUtilities.invokeLater(() -> Utility.openWebBrowser(href));
evt.preventDefault();
}
}, false);
}
}
| Shows the message fetched from the server in a new modal window. When the window
| is closed, record that the user has seen the message.
|
| @param startDate The start date (identifier) of the message being shown.
| @param webView The WebView component in which the message has been loaded
| @param parent The parent window. Must be non-null
|
@OnThread(Tag.FXPlatform)
private static void showMessageWindow(LocalDate startDate, WebView webView, Stage parent)
{
Stage window = new Stage();
window.initModality(Modality.WINDOW_MODAL);
window.initOwner(parent);
window.setTitle(Config.getString("bluej.central.msg.title"));
Button button = new Button(Config.getString("okay"));
button.setDefaultButton(true);
button.setOnAction(e -> {
window.hide();
});
window.setOnHidden(e -> {
Config.recordLatestSeen(startDate);
});
BorderPane.setAlignment(button, Pos.CENTER);
BorderPane.setMargin(button, new Insets(15));
BorderPane.setMargin(webView, new Insets(10));
webView.setPrefWidth(650);
webView.setPrefHeight(400);
window.setScene(new Scene(new BorderPane(webView, null, null, button, null)));
window.show();
window.toFront();
}
}
. Main
. run
. processArgs
. prepareMacOSApp
. prepareMacOSMenuFX
. prepareMacOSMenuSwing
. doQuit
. hadOrphanPackages
. updateStats
. getBlueJVersion
. getOperatingSystem
. getJavaVersion
. getInterfaceLanguage
. exit
. getStoredContextClassLoader
. setGuiHandler
. fetchAndShowCentralMsg
. makeLinksOpenExternally
. handleEvent
. showMessageWindow
679 neLoCode
+ 62 LoComm