experimenting with sessions and stuff like that.

Changes:
+ Login Panel
+ Some Kernel Booting stuff
This commit is contained in:
2026-05-27 07:21:30 +02:00
parent 307062eb35
commit f160b07bf4
8 changed files with 603 additions and 102 deletions
+2 -96
View File
@@ -1,103 +1,9 @@
package com.rdesk;
import com.rdesk.applications.entertainment.comic.Xkcd;
import com.rdesk.applications.productivity.Calculator;
import com.rdesk.applications.productivity.browsing.TabbedBrowser;
import com.rdesk.applications.productivity.code.CodeDesk;
import com.rdesk.applications.system.Exit;
import com.rdesk.applications.productivity.Notes;
import com.rdesk.applications.system.TaskManager;
import com.rdesk.applications.system.filehaj.FileExplorerApp;
import com.rdesk.applications.system.rsys.KbChanger;
import com.rdesk.kernel.*;
import com.rdesk.kernel.launcher.AppLauncher;
import javax.swing.*;
import com.rdesk.applications.system.login.Login;
public class Main {
private static final boolean RUN_IN_FS = false;
private static final boolean USE_SYSDEFAULT_LAF = false;
private static final boolean USE_FLATLAF = false;
private static final boolean loadSettingsKernelFlag = true;
public static void main(String[] args) {
SwingUtilities.invokeLater(Main::boot);
}
private static void boot() {
Kernel kernel = Kernel.get();
kernel.initSettings(loadSettingsKernelFlag);
kernel.boot();
JFrame frame = new JFrame("rDesk");
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
frame.addWindowListener(new java.awt.event.WindowAdapter() {
@Override
public void windowClosing(java.awt.event.WindowEvent e) {
kernel.shutdown();
}
});
if (USE_FLATLAF) {
try {
UIManager.setLookAndFeel(
"com.formdev.flatlaf.themes.FlatMacLightLaf"
);
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
JOptionPane.showMessageDialog(
null,
ex.getMessage() + "@USE_FLATLAF"
);
}
}
if (USE_SYSDEFAULT_LAF) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
JOptionPane.showMessageDialog(
null,
ex.getMessage() + "@USE_SYSDEFAULT_LAF"
);
}
}
if (RUN_IN_FS) {
frame.setUndecorated(true);
frame.setExtendedState(JFrame.MAXIMIZED_BOTH);
} else {
frame.setSize(1200, 800);
frame.setLocationRelativeTo(null);
}
JDesktopPane desktop = new JDesktopPane();
kernel.windows().attachDesktop(desktop);
AppLauncher launcher = new AppLauncher(kernel);
registerSystemApps(launcher);
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, launcher, desktop);
split.setDividerSize(0);
split.setEnabled(false);
frame.setContentPane(split);
frame.setVisible(true);
}
private static void registerSystemApps(AppLauncher launcher) {
// productivity
launcher.addApp(new Notes(), "Productivity");
launcher.addApp(new TabbedBrowser(), "Productivity");
launcher.addApp(new CodeDesk(), "Productivity");
launcher.addApp(new Calculator(), "Productivity");
// entertainment
launcher.addApp(new Xkcd(), "Entertainment");
// System apps
launcher.addApp(new Exit(), "System");
launcher.addApp(new TaskManager(), "System");
launcher.addApp(new FileExplorerApp(), "System");
launcher.addApp(new KbChanger(), "System");
Login.main(args);
}
}
@@ -1,11 +1,13 @@
package com.rdesk.applications.system;
import com.rdesk.applications.system.login.Login;
import com.rdesk.kernel.DesktopApp;
import com.rdesk.kernel.Kernel;
import javax.swing.*;
import java.awt.*;
import java.io.IOException;
import java.awt.Window;
public class Exit implements DesktopApp {
@@ -22,7 +24,7 @@ public class Exit implements DesktopApp {
@Override
public JInternalFrame createWindow(Kernel kernel) {
JInternalFrame frame = new JInternalFrame("System Controls", true, true);
frame.setSize(350, 180);
frame.setSize(420, 240);
frame.setResizable(false);
JPanel mainPanel = new JPanel(new GridBagLayout());
@@ -30,7 +32,7 @@ public class Exit implements DesktopApp {
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.insets = new Insets(5, 5, 5, 5);
gbc.insets = new Insets(6, 6, 6, 6);
JLabel label = new JLabel("Choose an action:");
label.setFont(label.getFont().deriveFont(Font.BOLD));
@@ -38,8 +40,11 @@ public class Exit implements DesktopApp {
JButton shutdownButton = new JButton("Shutdown rDesk");
JButton shutdownOS = new JButton("Shutdown Host OS");
JButton logoffButton = new JButton("Log off");
shutdownButton.setFocusPainted(false);
shutdownButton.addActionListener(e -> confirmAndShutdown(kernel, frame));
shutdownOS.setFocusPainted(false);
shutdownOS.addActionListener(e -> {
try {
@@ -48,8 +53,38 @@ public class Exit implements DesktopApp {
throw new RuntimeException(ex);
}
});
logoffButton.setFocusPainted(false);
logoffButton.addActionListener(e -> {
int choice = JOptionPane.showConfirmDialog(
frame,
"Log off and return to the login screen?"
+ "This will end all running Tasks!",
"Confirm Log off",
JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE
);
if (choice != JOptionPane.YES_OPTION) return;
SwingUtilities.invokeLater(() -> {
try {
Login.main(null);
} catch (Exception ex) {
JOptionPane.showMessageDialog(frame,
"Failed to launch login screen: " + ex.getMessage(),
"Error",
JOptionPane.ERROR_MESSAGE);
return;
}
kernel.logoff();
});
});
mainPanel.add(shutdownButton, gbc);
mainPanel.add(shutdownOS, gbc);
mainPanel.add(logoffButton, gbc);
JButton cancelButton = new JButton("Cancel");
cancelButton.setFocusPainted(false);
@@ -89,11 +124,11 @@ public class Exit implements DesktopApp {
"Are you sure? This will shutdown the Host OS",
JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE
);
);
if (choice == JOptionPane.YES_OPTION) {
frame.dispose();
performShutdownCmd(); // cause else the call wouldnt be called
performShutdownCmd();
kernel.shutdown();
}
}
@@ -107,8 +142,8 @@ public class Exit implements DesktopApp {
JOptionPane.WARNING_MESSAGE
);
if (choice == JOptionPane.YES_OPTION) {
frame.dispose(); // close the confirmation window first
frame.dispose();
kernel.shutdown();
}
}
}
}
@@ -0,0 +1,126 @@
package com.rdesk.applications.system.login;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.annotations.SerializedName;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.awt.Window;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
public class Handler {
private static final String FILENAME = System.getProperty("user.home") + File.separator + "rsys_passwdstore.json";
private static final SecureRandom RNG = new SecureRandom();
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final int SALT_LEN = 16;
private static final int KEY_LEN = 256; // bits
private static final int ITERATIONS = 200_000;
private static final String ALGO = "PBKDF2WithHmacSHA256";
public static boolean storeExists() {
return new File(FILENAME).exists();
}
public static UserRecord load() throws Exception {
File f = new File(FILENAME);
if (!f.exists()) return null;
try (FileReader fr = new FileReader(f)) {
return GSON.fromJson(fr, UserRecord.class);
}
}
public static void save(UserRecord u) throws Exception {
try (FileWriter fw = new FileWriter(FILENAME)) {
GSON.toJson(u, fw);
}
}
public static UserRecord createUser(String username, char[] password) throws Exception {
byte[] salt = new byte[SALT_LEN];
RNG.nextBytes(salt);
byte[] hash = pbkdf2(password, salt, ITERATIONS, KEY_LEN);
UserRecord u = new UserRecord();
u.username = username;
u.saltB64 = Base64.getEncoder().encodeToString(salt);
u.hashB64 = Base64.getEncoder().encodeToString(hash);
u.iterations = ITERATIONS;
return u;
}
public static boolean verifyPassword(char[] password, UserRecord stored) throws Exception {
byte[] salt = Base64.getDecoder().decode(stored.saltB64);
byte[] expected = Base64.getDecoder().decode(stored.hashB64);
byte[] computed = pbkdf2(password, salt, stored.iterations, expected.length * 8);
return slowEquals(expected, computed);
}
public static boolean verifyAndClose(Window window, char[] password, UserRecord stored) throws Exception {
try {
boolean ok = verifyPassword(password, stored);
if (ok && window != null) {
window.dispose();
}
return ok;
} finally {
clearPassword(password);
}
}
private static byte[] pbkdf2(char[] password, byte[] salt, int iterations, int keyLengthBits) throws Exception {
PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, keyLengthBits);
SecretKeyFactory skf = SecretKeyFactory.getInstance(ALGO);
return skf.generateSecret(spec).getEncoded();
}
private static boolean slowEquals(byte[] a, byte[] b) {
if (a == null || b == null || a.length != b.length) return false;
int diff = 0;
for (int i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
return diff == 0;
}
public static void clearPassword(char[] p) {
if (p == null) return;
Arrays.fill(p, (char) 0);
}
public static String[] listUsers() {
File f = new File(FILENAME);
if (!f.exists()) return new String[0];
try (FileReader fr = new FileReader(f)) {
JsonElement root = JsonParser.parseReader(fr);
List<String> users = new ArrayList<>();
if (root.isJsonArray()) {
JsonArray arr = root.getAsJsonArray();
for (JsonElement el : arr) {
try {
UserRecord ur = GSON.fromJson(el, UserRecord.class);
if (ur != null && ur.username != null) users.add(ur.username);
} catch (Throwable ignore) { /* skip malformed entry */ }
}
} else if (root.isJsonObject()) {
try {
UserRecord ur = GSON.fromJson(root, UserRecord.class);
if (ur != null && ur.username != null) users.add(ur.username);
} catch (Throwable ignore) { /* malformed object -> no users */ }
}
return users.toArray(new String[0]);
} catch (Exception e) {
return new String[0];
}
}
}
@@ -0,0 +1,267 @@
package com.rdesk.applications.system.login;
import com.rdesk.kernel.BootMgr;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.image.BufferedImage;
import java.util.logging.Logger;
public class Login {
private JFrame frame;
private JComboBox<String> userCombo;
private JTextField userFieldFallback;
private JPasswordField passField;
private JLabel statusLabel;
private UserRecord stored;
private JButton loginBtn;
private JButton closeBtn;
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
try {
new Login().start();
} catch (Exception e) {
JOptionPane.showMessageDialog(null, e);
System.exit(1);
}
});
}
private void start() throws Exception {
log("start(): checking store existence");
if (!Handler.storeExists()) {
showCreateUserDialog();
}
stored = Handler.load();
log("start(): loaded user record: " + (stored != null ? stored.username : "null"));
buildAndShowGui();
}
private void buildAndShowGui() {
log("buildAndShowGui(): building GUI");
frame = new JFrame("rDesk Login");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setResizable(false);
Image icon = createImageFromSwingIcon(UIManager.getIcon("FileView.computerIcon"));
if (icon != null) frame.setIconImage(icon);
JPanel panel = new JPanel(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(6, 6, 6, 6);
c.anchor = GridBagConstraints.WEST;
c.fill = GridBagConstraints.HORIZONTAL;
int row = 0;
if (icon != null) {
JLabel iconLabel = new JLabel(new ImageIcon(icon.getScaledInstance(64, 64, Image.SCALE_SMOOTH)));
GridBagConstraints ic = (GridBagConstraints) c.clone();
ic.gridx = 0;
ic.gridy = row;
ic.gridheight = 2;
ic.anchor = GridBagConstraints.CENTER;
panel.add(iconLabel, ic);
}
int colOffset = (icon != null) ? 1 : 0;
c.gridx = colOffset;
c.gridy = row;
c.gridwidth = 1;
panel.add(new JLabel("Username:"), c);
c.gridx = colOffset + 1;
c.gridy = row++;
c.weightx = 1.0;
String[] users = fetchUserList();
if (users.length > 0) {
userCombo = new JComboBox<>(users);
userCombo.setEditable(false);
userCombo.setPreferredSize(new Dimension(160, 24));
panel.add(userCombo, c);
} else {
userFieldFallback = new JTextField(16);
userFieldFallback.setPreferredSize(new Dimension(160, 24));
panel.add(userFieldFallback, c);
}
c.gridx = colOffset;
c.gridy = row;
c.weightx = 0;
panel.add(new JLabel("Password:"), c);
c.gridx = colOffset + 1;
c.gridy = row++;
c.weightx = 1.0;
passField = new JPasswordField(16);
passField.setPreferredSize(new Dimension(160, 24));
panel.add(passField, c);
closeBtn = new JButton("Close");
closeBtn.addActionListener(e -> frame.dispose());
loginBtn = new JButton("Login");
loginBtn.addActionListener(e -> onLogin());
c.gridx = colOffset;
c.gridy = row;
c.gridwidth = 1;
c.weightx = 0.5;
c.fill = GridBagConstraints.HORIZONTAL;
panel.add(closeBtn, c);
c.gridx = colOffset + 1;
c.gridy = row++;
c.weightx = 0.5;
panel.add(loginBtn, c);
statusLabel = new JLabel(" ");
statusLabel.setForeground(Color.BLUE);
c.gridx = colOffset;
c.gridy = row;
c.gridwidth = 2;
c.weightx = 1.0;
c.fill = GridBagConstraints.HORIZONTAL;
panel.add(statusLabel, c);
frame.getContentPane().add(panel);
frame.getRootPane().setDefaultButton(loginBtn);
passField.addActionListener(e -> onLogin());
if (userFieldFallback != null) userFieldFallback.addActionListener(e -> onLogin());
if (userCombo != null) {
userCombo.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke("ENTER"), "login");
userCombo.getActionMap().put("login", new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) { onLogin(); }
});
}
frame.pack();
Dimension total = frame.getContentPane().getSize();
int gap = 12;
int buttonWidth = Math.max(90, (total.width - gap) / 2);
closeBtn.setPreferredSize(new Dimension(buttonWidth, 28));
loginBtn.setPreferredSize(new Dimension(buttonWidth, 28));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
log("buildAndShowGui(): GUI visible");
}
private Image createImageFromSwingIcon(Icon icon) {
if (icon == null) return null;
int w = Math.max(1, icon.getIconWidth());
int h = Math.max(1, icon.getIconHeight());
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = img.createGraphics();
try {
icon.paintIcon(null, g, 0, 0);
} finally {
g.dispose();
}
return img;
}
private void onLogin() {
String userAttempt = (userCombo != null)
? String.valueOf(userCombo.getSelectedItem()).trim()
: userFieldFallback.getText().trim();
char[] passAttempt = passField.getPassword();
log("onLogin(): attempt for user='" + userAttempt + "'");
if (stored == null) {
statusLabel.setText("No user record found.");
Handler.clearPassword(passAttempt);
return;
}
if (!stored.username.equals(userAttempt)) {
statusLabel.setText("Invalid username or password.");
Handler.clearPassword(passAttempt);
return;
}
try {
boolean ok = Handler.verifyAndClose(frame, passAttempt, stored);
if (ok) {
log("onLogin(): authentication successful for user='" + userAttempt + "'");
statusLabel.setText(" ");
SwingUtilities.invokeLater(() -> {
new SwingWorker<Void, Void>() {
@Override protected Void doInBackground() {
try {
BootMgr.launchOrShow();
log("[Login]: BootManager.launchOrShow() completed");
} catch (Exception e) {
log("onLogin(): BootManager error: " + e.getMessage());
}
return null;
}
}.execute();
});
} else {
statusLabel.setText("Invalid username or password.");
log("onLogin(): password verification failed for user='" + userAttempt + "'");
}
} catch (Exception ex) {
ex.printStackTrace();
statusLabel.setText("Error during verification.");
log("onLogin(): exception during verification: " + ex.getMessage());
} finally {
Handler.clearPassword(passAttempt);
passField.setText("");
}
}
private void showCreateUserDialog() throws Exception {
log("showCreateUserDialog(): creating user dialog");
JPanel p = new JPanel(new GridLayout(3,2,6,6));
JTextField user = new JTextField(16);
JPasswordField pass = new JPasswordField(16);
JPasswordField pass2 = new JPasswordField(16);
p.add(new JLabel("Username:")); p.add(user);
p.add(new JLabel("Password:")); p.add(pass);
p.add(new JLabel("Confirm:")); p.add(pass2);
int res = JOptionPane.showConfirmDialog(null, p, "Create User", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
if (res != JOptionPane.OK_OPTION) {
log("showCreateUserDialog(): user cancelled creation, exiting");
System.exit(0);
}
if (!String.valueOf(pass.getPassword()).equals(String.valueOf(pass2.getPassword()))) {
JOptionPane.showMessageDialog(null, "Passwords do not match.", "Error", JOptionPane.ERROR_MESSAGE);
log("showCreateUserDialog(): passwords did not match");
showCreateUserDialog();
return;
}
UserRecord u = Handler.createUser(user.getText().trim(), pass.getPassword());
Handler.save(u);
log("showCreateUserDialog(): created and saved user '" + u.username + "'");
JOptionPane.showMessageDialog(null, "User created.", "Success", JOptionPane.INFORMATION_MESSAGE);
}
private String[] fetchUserList() {
try {
log("fetchUserList(): attempting to retrieve user list from Handler");
String[] users = Handler.listUsers();
if (users == null || users.length == 0) {
if (stored != null) {
return new String[]{stored.username};
}
return new String[0];
}
return users;
} catch (Exception t) {
if (stored != null) return new String[]{stored.username};
return new String[0];
}
}
private void log(String msg) {
System.out.println("[Login]: " + msg);
}
}
@@ -0,0 +1,10 @@
package com.rdesk.applications.system.login;
import com.google.gson.annotations.SerializedName;
public class UserRecord {
@SerializedName("username") public String username;
@SerializedName("salt_b64") public String saltB64;
@SerializedName("hash_b64") public String hashB64;
@SerializedName("iterations") public int iterations;
}
@@ -0,0 +1,40 @@
package com.rdesk.kernel;
import javax.swing.*;
import java.awt.*;
public final class BootMgr {
private static JFrame desktopFrame;
private BootMgr() {}
public static synchronized void launchOrShow() {
if (desktopFrame == null) {
JFrame f = BootProc.boot();
if (f != null) desktopFrame = f;
else {
desktopFrame = new JFrame();
desktopFrame.setContentPane(f);
desktopFrame.pack();
}
desktopFrame.setVisible(true);
desktopFrame.toFront();
} else {
if (!desktopFrame.isVisible()) desktopFrame.setVisible(true);
desktopFrame.toFront();
desktopFrame.requestFocus();
}
}
public static synchronized void hideDesktopOnLogout() {
if (desktopFrame != null) {
desktopFrame.setVisible(false);
}
}
public static synchronized void disposeDesktop() {
if (desktopFrame != null) {
desktopFrame.dispose();
desktopFrame = null;
}
}
}
@@ -0,0 +1,109 @@
package com.rdesk.kernel;
import com.rdesk.applications.entertainment.comic.Xkcd;
import com.rdesk.applications.productivity.Calculator;
import com.rdesk.applications.productivity.Notes;
import com.rdesk.applications.productivity.browsing.TabbedBrowser;
import com.rdesk.applications.productivity.code.CodeDesk;
import com.rdesk.applications.system.Exit;
import com.rdesk.applications.system.TaskManager;
import com.rdesk.applications.system.filehaj.FileExplorerApp;
import com.rdesk.applications.system.rsys.KbChanger;
import com.rdesk.kernel.launcher.AppLauncher;
import javax.swing.*;
public class BootProc {
private static final boolean RUN_IN_FS = true;
private static final boolean USE_SYSDEFAULT_LAF = false;
private static final boolean USE_FLATLAF = false;
private static final boolean loadSettingsKernelFlag = true;
private static JFrame mainFrame;
private static JDesktopPane desktopPane;
public static synchronized JFrame boot() {
if (mainFrame != null) {
if (!mainFrame.isVisible()) mainFrame.setVisible(true);
mainFrame.toFront();
mainFrame.requestFocus();
return mainFrame;
}
Kernel kernel = Kernel.get();
kernel.initSettings(loadSettingsKernelFlag);
kernel.boot();
mainFrame = new JFrame("rDesk");
mainFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
mainFrame.addWindowListener(new java.awt.event.WindowAdapter() {
@Override
public void windowClosing(java.awt.event.WindowEvent e) {
kernel.shutdown();
}
});
if (USE_FLATLAF) {
try {
UIManager.setLookAndFeel("com.formdev.flatlaf.themes.FlatMacLightLaf");
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
JOptionPane.showMessageDialog(null, ex.getMessage() + "@USE_FLATLAF");
}
}
if (USE_SYSDEFAULT_LAF) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
JOptionPane.showMessageDialog(null, ex.getMessage() + "@USE_SYSDEFAULT_LAF");
}
}
if (RUN_IN_FS) {
mainFrame.setUndecorated(true);
mainFrame.setExtendedState(JFrame.MAXIMIZED_BOTH);
} else {
mainFrame.setSize(1200, 800);
mainFrame.setLocationRelativeTo(null);
}
desktopPane = new JDesktopPane();
kernel.windows().attachDesktop(desktopPane);
kernel.attachFrame(mainFrame);
AppLauncher launcher = new AppLauncher(kernel);
registerSystemApps(launcher);
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, launcher, desktopPane);
split.setDividerSize(0);
split.setEnabled(false);
mainFrame.setContentPane(split);
mainFrame.setVisible(true);
return mainFrame;
}
public static synchronized JFrame getMainFrame() {
return mainFrame;
}
public static synchronized JDesktopPane getDesktopPane() {
return desktopPane;
}
private static void registerSystemApps(AppLauncher launcher) {
// productivity
launcher.addApp(new Notes(), "Productivity");
launcher.addApp(new TabbedBrowser(), "Productivity");
launcher.addApp(new CodeDesk(), "Productivity");
launcher.addApp(new Calculator(), "Productivity");
// entertainment
launcher.addApp(new Xkcd(), "Entertainment");
// System apps
launcher.addApp(new Exit(), "System");
launcher.addApp(new TaskManager(), "System");
launcher.addApp(new FileExplorerApp(), "System");
launcher.addApp(new KbChanger(), "System");
}
}
@@ -64,6 +64,14 @@ public class Kernel {
this.desktopFrame = frame;
}
public void logoff() {
if (running) SwingUtilities.invokeLater(() -> {
desktopFrame.setVisible(false);
});
}
public void shutdown() {
if (!running) return;