fixes ; kernel stability
This commit is contained in:
@@ -8,6 +8,7 @@ import com.rdesk.applications.productivity.Notes;
|
|||||||
import com.rdesk.applications.system.TaskManager;
|
import com.rdesk.applications.system.TaskManager;
|
||||||
import com.rdesk.applications.system.filehaj.FileExplorerApp;
|
import com.rdesk.applications.system.filehaj.FileExplorerApp;
|
||||||
import com.rdesk.kernel.*;
|
import com.rdesk.kernel.*;
|
||||||
|
import com.rdesk.kernel.launcher.AppLauncher;
|
||||||
|
|
||||||
import javax.swing.*;
|
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);
|
app.onStop(kernel);
|
||||||
System.out.println("[AppManager] Stopped: " + app.getName());
|
System.out.println("[AppManager] Stopped: " + app.getName());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
JOptionPane.showMessageDialog(
|
||||||
|
null,
|
||||||
|
"AppManager Fault",
|
||||||
|
"Fault when trying to stop all Applications in ApplicationManager",
|
||||||
|
JOptionPane.ERROR_MESSAGE
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
runningApps.clear();
|
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 EventBus eventBus;
|
||||||
private final ServiceRegistry services;
|
private final ServiceRegistry services;
|
||||||
|
|
||||||
private DesktopIconManager iconManager;
|
|
||||||
private JFrame desktopFrame;
|
private JFrame desktopFrame;
|
||||||
|
|
||||||
private boolean running = false;
|
private boolean running = false;
|
||||||
@@ -44,20 +43,6 @@ public class Kernel {
|
|||||||
this.desktopFrame = frame;
|
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() {
|
public void shutdown() {
|
||||||
if (!running) return;
|
if (!running) return;
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,31 @@
|
|||||||
package com.rdesk.kernel;
|
package com.rdesk.kernel;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class WindowManager {
|
public class WindowManager {
|
||||||
|
|
||||||
private final Kernel kernel;
|
|
||||||
private JDesktopPane desktop;
|
private JDesktopPane desktop;
|
||||||
|
private final WindowSnapper snapper;
|
||||||
|
|
||||||
public WindowManager(Kernel kernel) {
|
public WindowManager(Kernel kernel) {
|
||||||
this.kernel = kernel;
|
this.snapper = new WindowSnapper(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void attachDesktop(JDesktopPane desktop) {
|
public void attachDesktop(JDesktopPane desktop) {
|
||||||
this.desktop = desktop;
|
this.desktop = desktop;
|
||||||
|
snapper.attachToDesktop(desktop);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void open(JInternalFrame frame) {
|
public void open(JInternalFrame frame) {
|
||||||
desktop.add(frame);
|
desktop.add(frame);
|
||||||
frame.setVisible(true);
|
frame.setVisible(true);
|
||||||
|
snapper.attach(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void closeAll() {
|
public void closeAll() {
|
||||||
if (desktop == null) return;
|
if (desktop == null) return;
|
||||||
for (JInternalFrame frame : desktop.getAllFrames()) {
|
for (JInternalFrame frame : desktop.getAllFrames()) {
|
||||||
try {
|
try { frame.setClosed(true); } catch (Exception ignored) {}
|
||||||
frame.setClosed(true);
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,15 +39,16 @@ public class WindowManager {
|
|||||||
for (JInternalFrame frame : desktop.getAllFrames()) {
|
for (JInternalFrame frame : desktop.getAllFrames()) {
|
||||||
Object prop = frame.getClientProperty("appId");
|
Object prop = frame.getClientProperty("appId");
|
||||||
if (appId.equals(prop)) {
|
if (appId.equals(prop)) {
|
||||||
try {
|
try { frame.setClosed(true); } catch (Exception ignored) {}
|
||||||
frame.setClosed(true);
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public JDesktopPane getDesktop() {
|
public JDesktopPane getDesktop() {
|
||||||
// allows to change stuff for the desktop pane, essential for our bgchanger class
|
|
||||||
return desktop;
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user