fixes ; kernel stability

This commit is contained in:
2026-05-17 17:50:55 +02:00
parent 86799e4b19
commit d07cd476de
9 changed files with 407 additions and 450 deletions
+1
View File
@@ -8,6 +8,7 @@ import com.rdesk.applications.productivity.Notes;
import com.rdesk.applications.system.TaskManager;
import com.rdesk.applications.system.filehaj.FileExplorerApp;
import com.rdesk.kernel.*;
import com.rdesk.kernel.launcher.AppLauncher;
import javax.swing.*;
@@ -1,238 +0,0 @@
package com.rdesk.kernel;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* A clean, functional sidebar that lists applications by category.
* Categories tile vertically, each taking full width.
*/
public class AppLauncher extends JPanel {
private static final String DEFAULT_CATEGORY = "General";
private static final int EXPANDED_WIDTH = 220;
private static final int COLLAPSED_WIDTH = 40;
private final Kernel kernel;
private final Map<String, CategoryPanel> categories = new LinkedHashMap<>();
private final JPanel mainPanel;
private final JButton globalToggleButton;
private final JScrollPane scrollPane;
private boolean expanded = true;
public AppLauncher(Kernel kernel) {
this.kernel = kernel;
setLayout(new BorderLayout());
setPreferredSize(new Dimension(EXPANDED_WIDTH, 0));
globalToggleButton = new JButton("<<");
globalToggleButton.setToolTipText("Collapse sidebar");
globalToggleButton.setFocusPainted(false);
globalToggleButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
globalToggleButton.addActionListener(this::toggleSidebar);
add(globalToggleButton, BorderLayout.NORTH);
mainPanel = new JPanel();
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
scrollPane = new JScrollPane(mainPanel);
scrollPane.setBorder(null);
scrollPane.getVerticalScrollBar().setUnitIncrement(10);
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
add(scrollPane, BorderLayout.CENTER);
}
// Public API -----------------------------------------------------------------
public void addApp(DesktopApp app) {
addApp(app, DEFAULT_CATEGORY);
}
public void addApp(DesktopApp app, String category) {
if (!kernel.apps().isRegistered(app.getId())) {
kernel.apps().register(app);
}
SwingUtilities.invokeLater(() -> getCategoryPanel(category).addApp(app));
}
public void loadAppsFromFolder(String folderPath) {
loadAppsFromFolder(folderPath, DEFAULT_CATEGORY);
}
public void loadAppsFromFolder(String folderPath, String category) {
File folder = new File(folderPath);
if (!folder.exists() || !folder.isDirectory()) {
System.err.println("[AppLauncher] Folder not found: " + folderPath);
return;
}
File[] jars = folder.listFiles((dir, name) -> name.endsWith(".jar"));
if (jars == null) return;
for (File jar : jars) loadJarApp(jar, category);
}
public void setCategoryExpanded(String category, boolean expand) {
CategoryPanel panel = categories.get(category);
if (panel != null) panel.setExpanded(expand);
}
// Internal logic ------------------------------------------------------------
private void toggleSidebar(ActionEvent e) {
expanded = !expanded;
globalToggleButton.setText(expanded ? "<<" : ">>");
globalToggleButton.setToolTipText(expanded ? "Collapse sidebar" : "Expand sidebar");
scrollPane.setVisible(expanded);
setPreferredSize(new Dimension(expanded ? EXPANDED_WIDTH : COLLAPSED_WIDTH, 0));
revalidate();
Container parent = getParent();
if (parent instanceof JSplitPane split) {
split.setDividerLocation(expanded ? EXPANDED_WIDTH : COLLAPSED_WIDTH);
}
}
private CategoryPanel getCategoryPanel(String name) {
return categories.computeIfAbsent(name, n -> {
CategoryPanel panel = new CategoryPanel(n);
mainPanel.add(panel);
mainPanel.revalidate();
return panel;
});
}
private void loadJarApp(File jarFile, String category) {
try (URLClassLoader loader = new URLClassLoader(
new URL[]{jarFile.toURI().toURL()},
getClass().getClassLoader())) {
String className = findDesktopAppClass(loader, jarFile);
if (className == null) {
System.err.println("[AppLauncher] No DesktopApp in " + jarFile.getName());
return;
}
Class<?> clazz = Class.forName(className, true, loader);
DesktopApp app = (DesktopApp) clazz.getDeclaredConstructor().newInstance();
addApp(app, category);
System.out.println("[AppLauncher] Loaded: " + app.getName());
} catch (Exception e) {
System.err.println("[AppLauncher] Failed to load " + jarFile.getName());
e.printStackTrace();
}
}
private String findDesktopAppClass(URLClassLoader loader, File jarFile) {
try (JarFile jar = new JarFile(jarFile)) {
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (!entry.getName().endsWith(".class")) continue;
String className = entry.getName().replace("/", ".").replace(".class", "");
try {
Class<?> clazz = Class.forName(className, false, loader);
for (Class<?> iface : clazz.getInterfaces()) {
if (iface == DesktopApp.class) return className;
}
} catch (Throwable ignored) {}
}
} catch (Exception e) {
JOptionPane.showMessageDialog(
null,
"Kernel Fault",
"The Kernel has encoutered a fault when trying to find DesktopAppClass" + e.getStackTrace(),
JOptionPane.WARNING_MESSAGE
);
}
return null;
}
// Inner classes -------------------------------------------------------------
private class CategoryPanel extends JPanel {
private final String categoryName;
private final JButton headerButton;
private final JPanel appsContainer;
private boolean expanded = true;
CategoryPanel(String categoryName) {
this.categoryName = categoryName;
setLayout(new BorderLayout());
setAlignmentX(LEFT_ALIGNMENT);
setBorder(BorderFactory.createEmptyBorder(0, 0, 4, 0));
headerButton = new JButton();
headerButton.setHorizontalAlignment(SwingConstants.LEFT);
headerButton.setFocusPainted(false);
headerButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
headerButton.addActionListener(e -> setExpanded(!expanded));
updateHeaderText();
appsContainer = new JPanel();
appsContainer.setLayout(new BoxLayout(appsContainer, BoxLayout.Y_AXIS));
appsContainer.setBorder(BorderFactory.createEmptyBorder(2, 12, 2, 2));
appsContainer.setVisible(true);
appsContainer.setAlignmentX(LEFT_ALIGNMENT);
add(headerButton, BorderLayout.NORTH);
add(appsContainer, BorderLayout.CENTER);
}
// Override getMaximumSize to allow full width while respecting preferred height
@Override
public Dimension getMaximumSize() {
Dimension preferred = getPreferredSize();
return new Dimension(Integer.MAX_VALUE, preferred.height);
}
private void updateHeaderText() {
String prefix = expanded ? "[-] " : "[+] ";
headerButton.setText(prefix + categoryName);
}
void addApp(DesktopApp app) {
JButton appButton = new JButton(app.getName());
appButton.setHorizontalAlignment(SwingConstants.LEFT);
appButton.setFocusPainted(false);
appButton.setBorder(BorderFactory.createEmptyBorder(4, 8, 4, 8));
appButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
appButton.addActionListener(e -> {
try {
kernel.apps().launch(app.getId());
} catch (IOException ex) {
JOptionPane.showMessageDialog(
appsContainer,
"Kernel Fault",
"The Kernel encoutered a critical error when launching an Application",
JOptionPane.ERROR_MESSAGE
);
}
});
// Make each app button fill the width of its container
appButton.setAlignmentX(LEFT_ALIGNMENT);
appButton.setMaximumSize(new Dimension(Integer.MAX_VALUE, appButton.getPreferredSize().height));
appsContainer.add(appButton);
appsContainer.revalidate();
appsContainer.repaint();
}
void setExpanded(boolean expand) {
if (this.expanded == expand) return;
this.expanded = expand;
appsContainer.setVisible(expanded);
updateHeaderText();
revalidate();
repaint();
}
}
}
@@ -46,7 +46,12 @@ public class AppManager {
app.onStop(kernel);
System.out.println("[AppManager] Stopped: " + app.getName());
} catch (Exception e) {
e.printStackTrace();
JOptionPane.showMessageDialog(
null,
"AppManager Fault",
"Fault when trying to stop all Applications in ApplicationManager",
JOptionPane.ERROR_MESSAGE
);
}
}
runningApps.clear();
@@ -1,185 +0,0 @@
package com.rdesk.kernel;
import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.jar.*;
public class DesktopIconManager {
private final Kernel kernel;
private final JDesktopPane desktop;
private final int cellWidth = 110;
private final int cellHeight = 110;
private final int iconWidth = 90;
private final int iconHeight = 70;
/** Tracks how many icons have been placed so far. */
private int index = 0;
public DesktopIconManager(Kernel kernel, JDesktopPane desktop) {
this.kernel = kernel;
this.desktop = desktop;
this.desktop.setLayout(null);
}
// ------------------------------------------------------------------ //
// Public API //
// ------------------------------------------------------------------ //
/**
* Scan a folder for {@code .jar} files and create a desktop icon for
* every {@link DesktopApp} implementation found inside them.
*/
public void loadAppsFromFolder(String path) {
File folder = new File(path);
if (!folder.exists() || !folder.isDirectory()) {
System.out.println("[IconManager] applications folder not found: " + path);
return;
}
File[] jars = folder.listFiles((d, name) -> name.endsWith(".jar"));
if (jars == null) return;
for (File jar : jars) {
loadJar(jar);
}
}
/**
* Directly add a {@link DesktopApp} icon to the desktop.
* Use this for system / built-in apps registered from {@code main()}.
*/
public void addApp(DesktopApp app) {
SwingUtilities.invokeLater(() -> createIcon(app));
}
// ------------------------------------------------------------------ //
// JAR loading //
// ------------------------------------------------------------------ //
private void loadJar(File jarFile) {
// URLClassLoader implements Closeable — always close it when done
try (URLClassLoader loader = new URLClassLoader(
new URL[]{jarFile.toURI().toURL()},
getClass().getClassLoader())) {
String className = findAppClass(loader, jarFile);
if (className == null) {
System.out.println("[IconManager] No DesktopApp found in " + jarFile.getName());
return;
}
Class<?> clazz = Class.forName(className, true, loader);
DesktopApp app = (DesktopApp) clazz.getDeclaredConstructor().newInstance();
SwingUtilities.invokeLater(() -> createIcon(app));
System.out.println("[IconManager] Loaded: " + app.getName());
} catch (Exception e) {
System.err.println("[IconManager] Failed to load " + jarFile.getName() + ": " + e.getMessage());
e.printStackTrace();
}
}
private String findAppClass(URLClassLoader loader, File jarFile) {
try (JarFile jar = new JarFile(jarFile)) {
var entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (!entry.getName().endsWith(".class")) continue;
String className = entry.getName()
.replace("/", ".")
.replace(".class", "");
try {
Class<?> clazz = Class.forName(className, false, loader);
for (Class<?> iface : clazz.getInterfaces()) {
if (iface == DesktopApp.class) {
return className;
}
}
} catch (Throwable ignored) {}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
// ------------------------------------------------------------------ //
// Icon creation & layout //
// ------------------------------------------------------------------ //
/**
* Calculate the next grid position.
*
* Column count is derived lazily from the desktop's current width so it
* remains correct even when the frame has been packed / resized before
* icons are added. Falls back to 1 column if the pane has no width yet.
*/
private Point nextGridPosition() {
int desktopWidth = desktop.getWidth();
int cols = (desktopWidth > 0) ? Math.max(1, desktopWidth / cellWidth) : 1;
int row = index / cols;
int col = index % cols;
int x = col * cellWidth + (cellWidth - iconWidth) / 2;
int y = row * cellHeight + (cellHeight - iconHeight) / 2;
index++;
return new Point(x, y);
}
private void createIcon(DesktopApp app) {
JButton button = new JButton("<html><center>" + app.getName() + "</center></html>");
// Style — a clean, dark desktop-icon look
button.setFont(new Font("Segoe UI", Font.PLAIN, 11));
button.setForeground(Color.WHITE);
button.setBackground(new Color(40, 40, 60, 200));
button.setBorderPainted(false);
button.setFocusPainted(false);
button.setOpaque(true);
button.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
button.setToolTipText(app.getName());
button.addMouseListener(new java.awt.event.MouseAdapter() {
@Override public void mouseEntered(java.awt.event.MouseEvent e) {
button.setBackground(new Color(70, 70, 100, 220));
}
@Override public void mouseExited(java.awt.event.MouseEvent e) {
button.setBackground(new Color(40, 40, 60, 200));
}
});
Point p = nextGridPosition();
button.setBounds(p.x, p.y, iconWidth, iconHeight);
button.addActionListener(e -> {
// app may be pre registered, so dont do shit
if (!kernel.apps().isRegistered(app.getId())) {
kernel.apps().register(app);
}
try {
kernel.apps().launch(app.getId());
} catch (IOException ex) {
throw new RuntimeException(ex);
}
});
desktop.add(button);
desktop.repaint();
}
}
@@ -11,7 +11,6 @@ public class Kernel {
private final EventBus eventBus;
private final ServiceRegistry services;
private DesktopIconManager iconManager;
private JFrame desktopFrame;
private boolean running = false;
@@ -44,20 +43,6 @@ public class Kernel {
this.desktopFrame = frame;
}
public void attachIconManager(DesktopIconManager iconManager) {
this.iconManager = iconManager;
}
public void registerSystemApp(DesktopApp app) {
if (iconManager == null) {
throw new IllegalStateException(
"[Kernel] attachIconManager() must be called before registerSystemApp()");
}
appManager.register(app);
iconManager.addApp(app);
System.out.println("[Kernel] System app registered: " + app.getName());
}
public void shutdown() {
if (!running) return;
@@ -1,33 +1,31 @@
package com.rdesk.kernel;
import javax.swing.*;
import java.util.ArrayList;
import java.util.List;
public class WindowManager {
private final Kernel kernel;
private JDesktopPane desktop;
private final WindowSnapper snapper;
public WindowManager(Kernel kernel) {
this.kernel = kernel;
this.snapper = new WindowSnapper(this);
}
public void attachDesktop(JDesktopPane desktop) {
this.desktop = desktop;
snapper.attachToDesktop(desktop);
}
public void open(JInternalFrame frame) {
desktop.add(frame);
frame.setVisible(true);
snapper.attach(frame);
}
public void closeAll() {
if (desktop == null) return;
for (JInternalFrame frame : desktop.getAllFrames()) {
try {
frame.setClosed(true);
} catch (Exception ignored) {}
try { frame.setClosed(true); } catch (Exception ignored) {}
}
}
@@ -41,15 +39,16 @@ public class WindowManager {
for (JInternalFrame frame : desktop.getAllFrames()) {
Object prop = frame.getClientProperty("appId");
if (appId.equals(prop)) {
try {
frame.setClosed(true);
} catch (Exception ignored) {}
try { frame.setClosed(true); } catch (Exception ignored) {}
}
}
}
public JDesktopPane getDesktop() {
// allows to change stuff for the desktop pane, essential for our bgchanger class
return desktop;
}
public WindowSnapper getSnapper() {
return snapper;
}
}
@@ -0,0 +1,221 @@
package com.rdesk.kernel;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class WindowSnapper {
private static final int SNAP_THRESHOLD = 28;
private final WindowManager windowManager;
private int sidebarWidth = 0;
private JWindow snapOverlay;
private SnapZone activeZone = SnapZone.NONE;
private JInternalFrame draggingFrame = null;
private boolean mouseDown = false;
private AWTEventListener awtListener;
public WindowSnapper(WindowManager windowManager) {
this.windowManager = windowManager;
}
public void setSidebarWidth(int width) {
this.sidebarWidth = width;
}
public void attachToDesktop(JDesktopPane desktop) {
if (awtListener != null) {
Toolkit.getDefaultToolkit().removeAWTEventListener(awtListener);
}
awtListener = event -> {
if (!(event instanceof MouseEvent me)) return;
if (!isInsideDesktop(me.getComponent(), desktop)) return;
int id = me.getID();
switch (id) {
case MouseEvent.MOUSE_PRESSED -> {
mouseDown = true;
draggingFrame = desktop.getSelectedFrame();
}
case MouseEvent.MOUSE_DRAGGED -> {
if (!mouseDown) return;
JInternalFrame frame = draggingFrame != null
? draggingFrame : desktop.getSelectedFrame();
draggingFrame = frame;
if (frame == null || isMaximised(frame)) {
if (activeZone != SnapZone.NONE) {
activeZone = SnapZone.NONE;
hideOverlay();
}
return;
}
Point screenCursor = me.getLocationOnScreen();
Rectangle usable = usableAreaOnScreen(desktop);
SnapZone zone = detectZone(screenCursor, usable);
if (zone != activeZone) {
activeZone = zone;
if (zone == SnapZone.NONE) {
hideOverlay();
} else {
Rectangle overlayBounds = boundsForZone(zone, usable);
if (overlayBounds != null) showOverlay(overlayBounds);
}
}
}
case MouseEvent.MOUSE_RELEASED -> {
if (!mouseDown) return;
mouseDown = false;
hideOverlay();
commitSnap(desktop);
activeZone = SnapZone.NONE;
draggingFrame = null;
}
}
};
Toolkit.getDefaultToolkit().addAWTEventListener(
awtListener, AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK);
}
public void attach(JInternalFrame frame) {
}
private void commitSnap(JDesktopPane desktop) {
if (draggingFrame == null || activeZone == SnapZone.NONE) return;
Rectangle usable = usableAreaOnScreen(desktop);
Rectangle snapScreen = boundsForZone(activeZone, usable);
if (snapScreen == null) return;
Point desktopOrigin = desktop.getLocationOnScreen();
Rectangle local = new Rectangle(
snapScreen.x - desktopOrigin.x,
snapScreen.y - desktopOrigin.y,
snapScreen.width,
snapScreen.height
);
if (activeZone == SnapZone.TOP) {
try { draggingFrame.setMaximum(true); }
catch (Exception ignored) {}
} else {
draggingFrame.setBounds(local);
draggingFrame.revalidate();
desktop.repaint();
}
}
private enum SnapZone {
LEFT, RIGHT, TOP,
TOP_LEFT, TOP_RIGHT,
BOTTOM_LEFT, BOTTOM_RIGHT,
NONE
}
private Rectangle usableAreaOnScreen(JDesktopPane desktop) {
Point origin = desktop.getLocationOnScreen();
int x = origin.x + sidebarWidth;
int y = origin.y;
int w = desktop.getWidth() - sidebarWidth;
int h = desktop.getHeight();
return new Rectangle(x, y, w, h);
}
private SnapZone detectZone(Point screenCursor, Rectangle usable) {
int rx = screenCursor.x - usable.x; // position within usable area
int ry = screenCursor.y - usable.y;
int w = usable.width;
int h = usable.height;
boolean nearLeft = rx <= SNAP_THRESHOLD || screenCursor.x <= usable.x + SNAP_THRESHOLD;
boolean nearRight = rx >= w - SNAP_THRESHOLD;
boolean nearTop = ry <= SNAP_THRESHOLD;
boolean nearBottom = ry >= h - SNAP_THRESHOLD;
if (nearTop && nearLeft) return SnapZone.TOP_LEFT;
if (nearTop && nearRight) return SnapZone.TOP_RIGHT;
if (nearBottom && nearLeft) return SnapZone.BOTTOM_LEFT;
if (nearBottom && nearRight) return SnapZone.BOTTOM_RIGHT;
if (nearTop) return SnapZone.TOP;
if (nearLeft) return SnapZone.LEFT;
if (nearRight) return SnapZone.RIGHT;
return SnapZone.NONE;
}
private Rectangle boundsForZone(SnapZone zone, Rectangle u) {
int hw = u.width / 2;
int hh = u.height / 2;
return switch (zone) {
case LEFT -> new Rectangle(u.x, u.y, hw, u.height);
case RIGHT -> new Rectangle(u.x + hw, u.y, hw, u.height);
case TOP -> new Rectangle(u.x, u.y, u.width, u.height);
case TOP_LEFT -> new Rectangle(u.x, u.y, hw, hh);
case TOP_RIGHT -> new Rectangle(u.x + hw, u.y, hw, hh);
case BOTTOM_LEFT -> new Rectangle(u.x, u.y + hh, hw, hh);
case BOTTOM_RIGHT -> new Rectangle(u.x + hw, u.y + hh, hw, hh);
default -> null;
};
}
private void showOverlay(Rectangle screenBounds) {
if (snapOverlay == null) {
snapOverlay = new JWindow();
snapOverlay.setBackground(new Color(0, 0, 0, 0));
JPanel glass = new JPanel() {
@Override protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(0, 120, 215, 55));
g2.fillRoundRect(3, 3, getWidth() - 6, getHeight() - 6, 10, 10);
g2.setColor(new Color(0, 120, 215, 180));
g2.setStroke(new BasicStroke(2f));
g2.drawRoundRect(3, 3, getWidth() - 6, getHeight() - 6, 10, 10);
g2.dispose();
}
};
glass.setOpaque(false);
snapOverlay.setContentPane(glass);
snapOverlay.setAlwaysOnTop(true);
}
snapOverlay.setBounds(screenBounds);
if (!snapOverlay.isVisible()) snapOverlay.setVisible(true);
snapOverlay.repaint();
}
private void hideOverlay() {
if (snapOverlay != null && snapOverlay.isVisible())
snapOverlay.setVisible(false);
}
private boolean isMaximised(JInternalFrame f) {
try { return f.isMaximum(); } catch (Exception e) { return false; }
}
private boolean isInsideDesktop(Component component, JDesktopPane desktop) {
Component c = component;
while (c != null) {
if (c == desktop) return true;
c = c.getParent();
}
return false;
}
}
@@ -0,0 +1,79 @@
package com.rdesk.kernel.launcher;
import com.rdesk.kernel.DesktopApp;
import com.rdesk.kernel.Kernel;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.util.*;
/**
* A clean, functional sidebar that lists applications by category.
* Categories tile vertically, each taking full width.
*/
public class AppLauncher extends JPanel {
private static final int EXPANDED_WIDTH = 220;
private static final int COLLAPSED_WIDTH = 40;
private final Kernel kernel;
private final Map<String, CategoryPanel> categories = new LinkedHashMap<>();
private final JPanel mainPanel;
private final JButton globalToggleButton;
private final JScrollPane scrollPane;
private boolean expanded = true;
public AppLauncher(Kernel kernel) {
this.kernel = kernel;
setLayout(new BorderLayout());
setPreferredSize(new Dimension(EXPANDED_WIDTH, 0));
globalToggleButton = new JButton("<<");
globalToggleButton.setToolTipText("Collapse sidebar");
globalToggleButton.setFocusPainted(false);
globalToggleButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
globalToggleButton.addActionListener(this::toggleSidebar);
add(globalToggleButton, BorderLayout.NORTH);
mainPanel = new JPanel();
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
scrollPane = new JScrollPane(mainPanel);
scrollPane.setBorder(null);
scrollPane.getVerticalScrollBar().setUnitIncrement(10);
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
add(scrollPane, BorderLayout.CENTER);
}
public void addApp(DesktopApp app, String category) {
if (!kernel.apps().isRegistered(app.getId())) {
kernel.apps().register(app);
}
SwingUtilities.invokeLater(() -> getCategoryPanel(category).addApp(app));
}
private void toggleSidebar(ActionEvent e) {
expanded = !expanded;
globalToggleButton.setText(expanded ? "<<" : ">>");
globalToggleButton.setToolTipText(expanded ? "Collapse sidebar" : "Expand sidebar");
scrollPane.setVisible(expanded);
setPreferredSize(new Dimension(expanded ? EXPANDED_WIDTH : COLLAPSED_WIDTH, 0));
revalidate();
Container parent = getParent();
if (parent instanceof JSplitPane split) {
split.setDividerLocation(expanded ? EXPANDED_WIDTH : COLLAPSED_WIDTH);
}
}
private CategoryPanel getCategoryPanel(String name) {
return categories.computeIfAbsent(name, n -> {
CategoryPanel panel = new CategoryPanel(n, this.kernel);
mainPanel.add(panel);
mainPanel.revalidate();
return panel;
});
}
}
@@ -0,0 +1,90 @@
package com.rdesk.kernel.launcher;
import com.rdesk.kernel.DesktopApp;
import com.rdesk.kernel.Kernel;
import com.sun.jna.platform.win32.WinReg;
import javax.swing.*;
import java.awt.*;
import java.io.IOException;
class CategoryPanel extends JPanel {
private final String categoryName;
private final JButton headerButton;
private final JPanel appsContainer;
private boolean expanded = true;
private final Kernel kernel;
CategoryPanel(String categoryName, Kernel kernel) {
this.categoryName = categoryName;
this.kernel = kernel;
setLayout(new BorderLayout());
setAlignmentX(LEFT_ALIGNMENT);
setBorder(BorderFactory.createEmptyBorder(0, 0, 4, 0));
headerButton = new JButton();
headerButton.setHorizontalAlignment(SwingConstants.LEFT);
headerButton.setFocusPainted(false);
headerButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
headerButton.addActionListener(e -> setExpanded(!expanded));
updateHeaderText();
appsContainer = new JPanel();
appsContainer.setLayout(new BoxLayout(appsContainer, BoxLayout.Y_AXIS));
appsContainer.setBorder(BorderFactory.createEmptyBorder(2, 12, 2, 2));
appsContainer.setVisible(true);
appsContainer.setAlignmentX(LEFT_ALIGNMENT);
add(headerButton, BorderLayout.NORTH);
add(appsContainer, BorderLayout.CENTER);
}
// Override getMaximumSize to allow full width while respecting preferred height
@Override
public Dimension getMaximumSize() {
Dimension preferred = getPreferredSize();
return new Dimension(Integer.MAX_VALUE, preferred.height);
}
private void updateHeaderText() {
String prefix = expanded ? "[-] " : "[+] ";
headerButton.setText(prefix + categoryName);
}
void addApp(DesktopApp app) {
JButton appButton = new JButton(app.getName());
appButton.setHorizontalAlignment(SwingConstants.LEFT);
appButton.setFocusPainted(false);
appButton.setBorder(BorderFactory.createEmptyBorder(4, 8, 4, 8));
appButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
appButton.addActionListener(e -> {
try {
kernel.apps().launch(app.getId());
} catch (IOException ex) {
JOptionPane.showMessageDialog(
appsContainer,
"Kernel Fault",
"The Kernel encoutered a critical error when launching an Application",
JOptionPane.ERROR_MESSAGE
);
}
});
// Make each app button fill the width of its container
appButton.setAlignmentX(LEFT_ALIGNMENT);
appButton.setMaximumSize(new Dimension(Integer.MAX_VALUE, appButton.getPreferredSize().height));
appsContainer.add(appButton);
appsContainer.revalidate();
appsContainer.repaint();
}
void setExpanded(boolean expand) {
if (this.expanded == expand) return;
this.expanded = expand;
appsContainer.setVisible(expanded);
updateHeaderText();
revalidate();
repaint();
}
}