initial AI slop commit. to be refactored!
This commit is contained in:
+40
@@ -0,0 +1,40 @@
|
|||||||
|
target/
|
||||||
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
|
!**/src/main/**/target/
|
||||||
|
!**/src/test/**/target/
|
||||||
|
.kotlin
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea/modules.xml
|
||||||
|
.idea/jarRepositories.xml
|
||||||
|
.idea/compiler.xml
|
||||||
|
.idea/libraries/
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
### Eclipse ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
build/
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
### Mac OS ###
|
||||||
|
.DS_Store
|
||||||
|
/.idea/
|
||||||
Binary file not shown.
@@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>com.rdesk</groupId>
|
||||||
|
<artifactId>rdesk</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Source: https://mvnrepository.com/artifact/com.google.code.gson/gson -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.code.gson</groupId>
|
||||||
|
<artifactId>gson</artifactId>
|
||||||
|
<version>2.14.0</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<!-- Source: https://mvnrepository.com/artifact/com.formdev/flatlaf -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.formdev</groupId>
|
||||||
|
<artifactId>flatlaf</artifactId>
|
||||||
|
<version>3.7.1</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package com.rdesk;
|
||||||
|
|
||||||
|
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.kernel.*;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
|
||||||
|
public class Main {
|
||||||
|
|
||||||
|
private static final boolean RUN_IN_FS = true;
|
||||||
|
private static final boolean USE_SYSDEFAULT_LAF = false;
|
||||||
|
private static final boolean USE_FLATLAF = false;
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SwingUtilities.invokeLater(Main::boot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void boot() {
|
||||||
|
|
||||||
|
Kernel kernel = Kernel.get();
|
||||||
|
kernel.boot();
|
||||||
|
|
||||||
|
JFrame frame = new JFrame("Java OS");
|
||||||
|
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) {
|
||||||
|
launcher.addApp(new Notes(), "Productivity");
|
||||||
|
launcher.addApp(new Exit(), "System");
|
||||||
|
launcher.addApp(new TaskManager(), "System");
|
||||||
|
launcher.addApp(new FileExplorerApp(), "System");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.rdesk.applications.productivity;
|
||||||
|
import com.rdesk.kernel.*;
|
||||||
|
import javax.swing.*;
|
||||||
|
|
||||||
|
public class Notes implements DesktopApp {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return "notes";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "Notes";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JInternalFrame createWindow(Kernel kernel) {
|
||||||
|
|
||||||
|
JInternalFrame frame = new JInternalFrame(
|
||||||
|
"Notes",
|
||||||
|
true, // resizable
|
||||||
|
true, // closable
|
||||||
|
true, // maximizable
|
||||||
|
true // iconifiable
|
||||||
|
);
|
||||||
|
|
||||||
|
frame.setSize(400, 300);
|
||||||
|
frame.setLayout(new java.awt.BorderLayout());
|
||||||
|
|
||||||
|
JTextArea textArea = new JTextArea();
|
||||||
|
frame.add(new JScrollPane(textArea), java.awt.BorderLayout.CENTER);
|
||||||
|
|
||||||
|
frame.setVisible(true);
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.rdesk.applications.system;
|
||||||
|
|
||||||
|
import com.rdesk.kernel.DesktopApp;
|
||||||
|
import com.rdesk.kernel.Kernel;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
|
||||||
|
public class Exit implements DesktopApp {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return "exit";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "System Exit";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JInternalFrame createWindow(Kernel kernel) {
|
||||||
|
JInternalFrame frame = new JInternalFrame("System Controls", true, true);
|
||||||
|
frame.setSize(350, 180);
|
||||||
|
frame.setResizable(false);
|
||||||
|
|
||||||
|
JPanel mainPanel = new JPanel(new GridBagLayout());
|
||||||
|
mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
|
||||||
|
GridBagConstraints gbc = new GridBagConstraints();
|
||||||
|
gbc.gridwidth = GridBagConstraints.REMAINDER;
|
||||||
|
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||||
|
gbc.insets = new Insets(5, 5, 5, 5);
|
||||||
|
|
||||||
|
JLabel label = new JLabel("Choose an action:");
|
||||||
|
label.setFont(label.getFont().deriveFont(Font.BOLD));
|
||||||
|
mainPanel.add(label, gbc);
|
||||||
|
|
||||||
|
JButton shutdownButton = new JButton("Shutdown OS");
|
||||||
|
shutdownButton.setFocusPainted(false);
|
||||||
|
shutdownButton.addActionListener(e -> confirmAndShutdown(kernel, frame));
|
||||||
|
mainPanel.add(shutdownButton, gbc);
|
||||||
|
|
||||||
|
JButton cancelButton = new JButton("Cancel");
|
||||||
|
cancelButton.setFocusPainted(false);
|
||||||
|
cancelButton.addActionListener(e -> frame.dispose());
|
||||||
|
mainPanel.add(cancelButton, gbc);
|
||||||
|
|
||||||
|
frame.add(mainPanel);
|
||||||
|
frame.setDefaultCloseOperation(JInternalFrame.DISPOSE_ON_CLOSE);
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void confirmAndShutdown(Kernel kernel, JInternalFrame frame) {
|
||||||
|
int choice = JOptionPane.showConfirmDialog(
|
||||||
|
frame,
|
||||||
|
"Are you sure you want to shut down the operating system?\nAll unsaved work will be lost.",
|
||||||
|
"Confirm Shutdown",
|
||||||
|
JOptionPane.YES_NO_OPTION,
|
||||||
|
JOptionPane.WARNING_MESSAGE
|
||||||
|
);
|
||||||
|
if (choice == JOptionPane.YES_OPTION) {
|
||||||
|
frame.dispose(); // close the confirmation window first
|
||||||
|
kernel.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,342 @@
|
|||||||
|
package com.rdesk.applications.system;
|
||||||
|
|
||||||
|
import com.rdesk.kernel.DesktopApp;
|
||||||
|
import com.rdesk.kernel.Kernel;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.Timer;
|
||||||
|
import javax.swing.event.InternalFrameAdapter;
|
||||||
|
import javax.swing.event.InternalFrameEvent;
|
||||||
|
import javax.swing.table.AbstractTableModel;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.geom.Path2D;
|
||||||
|
import java.lang.management.ManagementFactory;
|
||||||
|
import java.lang.management.MemoryMXBean;
|
||||||
|
import java.lang.management.MemoryUsage;
|
||||||
|
import java.lang.management.OperatingSystemMXBean;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class TaskManager implements DesktopApp {
|
||||||
|
|
||||||
|
private ScheduledExecutorService scheduler;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return "taskmanager";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "Task Manager";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JInternalFrame createWindow(Kernel kernel) {
|
||||||
|
JInternalFrame frame = new JInternalFrame("Task Manager", true, true, true, true);
|
||||||
|
frame.setSize(750, 550);
|
||||||
|
frame.setMinimumSize(new Dimension(600, 400));
|
||||||
|
frame.setResizable(true);
|
||||||
|
|
||||||
|
JTabbedPane tabbedPane = new JTabbedPane();
|
||||||
|
|
||||||
|
// ---- Processes Tab ----
|
||||||
|
JPanel processesPanel = createProcessesPanel(kernel);
|
||||||
|
tabbedPane.addTab("Processes", processesPanel);
|
||||||
|
|
||||||
|
// ---- Performance Tab ----
|
||||||
|
JPanel performancePanel = createPerformancePanel();
|
||||||
|
tabbedPane.addTab("Performance", performancePanel);
|
||||||
|
|
||||||
|
frame.add(tabbedPane, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
// Start background updater for the graphs
|
||||||
|
scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
Runnable updateTask = () -> {
|
||||||
|
if (!frame.isVisible()) return;
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
// Update performance graphs
|
||||||
|
Component[] comps = performancePanel.getComponents();
|
||||||
|
for (Component c : comps) {
|
||||||
|
if (c instanceof GraphPanel) {
|
||||||
|
((GraphPanel) c).addDataPoint();
|
||||||
|
((GraphPanel) c).repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
scheduler.scheduleAtFixedRate(updateTask, 0, 1, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
frame.addInternalFrameListener(new InternalFrameAdapter() {
|
||||||
|
@Override
|
||||||
|
public void internalFrameClosed(InternalFrameEvent e) {
|
||||||
|
if (scheduler != null && !scheduler.isShutdown()) {
|
||||||
|
scheduler.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
frame.setDefaultCloseOperation(JInternalFrame.DISPOSE_ON_CLOSE);
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Processes Tab
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
private JPanel createProcessesPanel(Kernel kernel) {
|
||||||
|
JPanel panel = new JPanel(new BorderLayout());
|
||||||
|
|
||||||
|
ProcessTableModel model = new ProcessTableModel(kernel);
|
||||||
|
JTable table = new JTable(model);
|
||||||
|
table.setFillsViewportHeight(true);
|
||||||
|
table.getColumnModel().getColumn(0).setPreferredWidth(200);
|
||||||
|
table.getColumnModel().getColumn(1).setPreferredWidth(100);
|
||||||
|
table.getColumnModel().getColumn(2).setPreferredWidth(80);
|
||||||
|
table.setRowHeight(22);
|
||||||
|
|
||||||
|
JScrollPane scrollPane = new JScrollPane(table);
|
||||||
|
panel.add(scrollPane, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
JPanel toolbar = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||||
|
JButton endTaskButton = new JButton("End Task");
|
||||||
|
endTaskButton.addActionListener(e -> {
|
||||||
|
int selected = table.getSelectedRow();
|
||||||
|
if (selected >= 0) {
|
||||||
|
String appId = model.getAppIdAt(selected);
|
||||||
|
confirmAndKill(kernel, appId, model);
|
||||||
|
} else {
|
||||||
|
JOptionPane.showMessageDialog(panel, "Select a task to end.", "No Selection", JOptionPane.INFORMATION_MESSAGE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
JButton refreshButton = new JButton("Refresh");
|
||||||
|
refreshButton.addActionListener(e -> model.refresh());
|
||||||
|
toolbar.add(endTaskButton);
|
||||||
|
toolbar.add(refreshButton);
|
||||||
|
panel.add(toolbar, BorderLayout.SOUTH);
|
||||||
|
|
||||||
|
Timer timer = new Timer(2000, e -> model.refresh());
|
||||||
|
timer.start();
|
||||||
|
panel.putClientProperty("timer", timer);
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void confirmAndKill(Kernel kernel, String appId, ProcessTableModel model) {
|
||||||
|
/*
|
||||||
|
int confirm = JOptionPane.showConfirmDialog(null,
|
||||||
|
|
||||||
|
"End task '" + appId + "'?\nAll unsaved work will be lost.",
|
||||||
|
"Confirm End Task",
|
||||||
|
JOptionPane.YES_NO_OPTION,
|
||||||
|
JOptionPane.WARNING_MESSAGE);
|
||||||
|
if (confirm == JOptionPane.YES_OPTION) {
|
||||||
|
*/
|
||||||
|
kernel.windows().closeAllByAppId(appId);
|
||||||
|
model.refresh();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel createPerformancePanel() {
|
||||||
|
JPanel panel = new JPanel(new GridLayout(2, 1, 10, 10));
|
||||||
|
panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||||
|
|
||||||
|
long maxMemoryMB = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getMax() / (1024 * 1024);
|
||||||
|
GraphPanel cpuGraph = new GraphPanel("CPU Usage", "%", Color.BLUE, 0, 100);
|
||||||
|
GraphPanel memoryGraph = new GraphPanel("Memory Usage", "MB", Color.GREEN, 0, (int) maxMemoryMB);
|
||||||
|
|
||||||
|
panel.add(cpuGraph);
|
||||||
|
panel.add(memoryGraph);
|
||||||
|
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class GraphPanel extends JPanel {
|
||||||
|
private final String title;
|
||||||
|
private final String unit;
|
||||||
|
private final Color lineColor;
|
||||||
|
private final int maxY;
|
||||||
|
private final LinkedList<Integer> data = new LinkedList<>();
|
||||||
|
private final int maxDataPoints = 60; // 60 points = 1 minute at 1 update/sec
|
||||||
|
|
||||||
|
public GraphPanel(String title, String unit, Color lineColor, int minY, int maxY) {
|
||||||
|
this.title = title;
|
||||||
|
this.unit = unit;
|
||||||
|
this.lineColor = lineColor;
|
||||||
|
this.maxY = maxY;
|
||||||
|
setBackground(Color.WHITE);
|
||||||
|
setBorder(BorderFactory.createTitledBorder(title));
|
||||||
|
// Initialize with zeros
|
||||||
|
for (int i = 0; i < maxDataPoints; i++) data.add(0);
|
||||||
|
setPreferredSize(new Dimension(400, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addDataPoint() {
|
||||||
|
int value = 0;
|
||||||
|
if (title.contains("CPU")) {
|
||||||
|
OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
|
||||||
|
double load = osBean.getSystemLoadAverage();
|
||||||
|
if (load > 0) {
|
||||||
|
int processors = osBean.getAvailableProcessors();
|
||||||
|
double cpuPercent = (load / processors) * 100;
|
||||||
|
value = (int) Math.min(Math.max(cpuPercent, 0), maxY);
|
||||||
|
}
|
||||||
|
} else if (title.contains("Memory")) {
|
||||||
|
MemoryUsage heap = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage();
|
||||||
|
long usedMB = heap.getUsed() / (1024 * 1024);
|
||||||
|
value = (int) Math.min(usedMB, maxY);
|
||||||
|
}
|
||||||
|
synchronized (data) {
|
||||||
|
data.addLast(value);
|
||||||
|
if (data.size() > maxDataPoints) data.removeFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics g) {
|
||||||
|
super.paintComponent(g);
|
||||||
|
Graphics2D g2 = (Graphics2D) g;
|
||||||
|
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
|
||||||
|
int w = getWidth();
|
||||||
|
int h = getHeight();
|
||||||
|
int leftMargin = 50;
|
||||||
|
int rightMargin = 20;
|
||||||
|
int topMargin = 40; // increased for title space
|
||||||
|
int bottomMargin = 30;
|
||||||
|
int graphWidth = w - leftMargin - rightMargin;
|
||||||
|
int graphHeight = h - topMargin - bottomMargin;
|
||||||
|
|
||||||
|
if (graphWidth <= 0 || graphHeight <= 0) return;
|
||||||
|
|
||||||
|
// Clear background
|
||||||
|
g2.setColor(Color.WHITE);
|
||||||
|
g2.fillRect(0, 0, w, h);
|
||||||
|
|
||||||
|
// Draw axes
|
||||||
|
g2.setColor(Color.BLACK);
|
||||||
|
g2.drawLine(leftMargin, topMargin, leftMargin, h - bottomMargin);
|
||||||
|
g2.drawLine(leftMargin, h - bottomMargin, w - rightMargin, h - bottomMargin);
|
||||||
|
|
||||||
|
// Draw grid lines (horizontal)
|
||||||
|
g2.setColor(Color.LIGHT_GRAY);
|
||||||
|
for (int i = 0; i <= 4; i++) {
|
||||||
|
int y = topMargin + (graphHeight * i / 4);
|
||||||
|
g2.drawLine(leftMargin, y, w - rightMargin, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw Y-axis labels
|
||||||
|
g2.setColor(Color.BLACK);
|
||||||
|
FontMetrics fm = g2.getFontMetrics();
|
||||||
|
for (int i = 0; i <= 4; i++) {
|
||||||
|
int value = maxY - (maxY * i / 4);
|
||||||
|
String label = String.valueOf(value);
|
||||||
|
int labelWidth = fm.stringWidth(label);
|
||||||
|
int y = topMargin + (graphHeight * i / 4) + fm.getHeight() / 2 - 2;
|
||||||
|
g2.drawString(label, leftMargin - labelWidth - 5, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw unit on Y axis
|
||||||
|
g2.drawString(unit, leftMargin - 25, topMargin - 5);
|
||||||
|
|
||||||
|
// Plot data
|
||||||
|
if (data.isEmpty()) return;
|
||||||
|
synchronized (data) {
|
||||||
|
Path2D path = new Path2D.Double();
|
||||||
|
double xStep = (double) graphWidth / (data.size() - 1);
|
||||||
|
boolean first = true;
|
||||||
|
for (int i = 0; i < data.size(); i++) {
|
||||||
|
double x = leftMargin + i * xStep;
|
||||||
|
double value = data.get(i);
|
||||||
|
double y = h - bottomMargin - (value * graphHeight / maxY);
|
||||||
|
if (first) {
|
||||||
|
path.moveTo(x, y);
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
path.lineTo(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g2.setColor(lineColor);
|
||||||
|
g2.setStroke(new BasicStroke(2f));
|
||||||
|
g2.draw(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw current value in top-right corner
|
||||||
|
int currentValue = data.isEmpty() ? 0 : data.getLast();
|
||||||
|
String valueStr = String.format("Current: %d %s", currentValue, unit);
|
||||||
|
g2.setColor(Color.BLACK);
|
||||||
|
Font smallFont = g2.getFont().deriveFont(Font.PLAIN, 11f);
|
||||||
|
g2.setFont(smallFont);
|
||||||
|
int valueWidth = fm.stringWidth(valueStr);
|
||||||
|
g2.drawString(valueStr, w - rightMargin - valueWidth - 5, topMargin - 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ProcessTableModel extends AbstractTableModel {
|
||||||
|
private final Kernel kernel;
|
||||||
|
private List<ProcessInfo> processes = new ArrayList<>();
|
||||||
|
private final String[] columns = {"Application", "Memory (MB)", "Windows"};
|
||||||
|
|
||||||
|
ProcessTableModel(Kernel kernel) {
|
||||||
|
this.kernel = kernel;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void refresh() {
|
||||||
|
processes.clear();
|
||||||
|
|
||||||
|
Map<String, Integer> windowCounts = new LinkedHashMap<>();
|
||||||
|
for (JInternalFrame frame : kernel.windows().getAllWindows()) {
|
||||||
|
Object prop = frame.getClientProperty("appId");
|
||||||
|
if (prop instanceof String) {
|
||||||
|
String appId = (String) prop;
|
||||||
|
windowCounts.put(appId, windowCounts.getOrDefault(appId, 0) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<String, Integer> entry : windowCounts.entrySet()) {
|
||||||
|
String appId = entry.getKey();
|
||||||
|
DesktopApp app = kernel.apps().getApp(appId);
|
||||||
|
String appName = (app != null) ? app.getName() : appId;
|
||||||
|
long memoryMB = entry.getValue() * 8L; // rough estimation
|
||||||
|
processes.add(new ProcessInfo(appName, appId, entry.getValue(), memoryMB));
|
||||||
|
}
|
||||||
|
|
||||||
|
fireTableDataChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
String getAppIdAt(int row) {
|
||||||
|
return processes.get(row).id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getRowCount() { return processes.size(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getColumnCount() { return columns.length; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getColumnName(int col) { return columns[col]; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getValueAt(int row, int col) {
|
||||||
|
ProcessInfo info = processes.get(row);
|
||||||
|
switch (col) {
|
||||||
|
case 0: return info.name;
|
||||||
|
case 1: return info.memoryMB;
|
||||||
|
case 2: return info.windowCount;
|
||||||
|
default: return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ProcessInfo {
|
||||||
|
String name, id;
|
||||||
|
int windowCount;
|
||||||
|
long memoryMB;
|
||||||
|
ProcessInfo(String name, String id, int windowCount, long memoryMB) {
|
||||||
|
this.name = name; this.id = id; this.windowCount = windowCount; this.memoryMB = memoryMB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
// FileExplorerApp.java - fix border and import
|
||||||
|
package com.rdesk.applications.system.filehaj;
|
||||||
|
|
||||||
|
import com.rdesk.kernel.DesktopApp;
|
||||||
|
import com.rdesk.kernel.Kernel;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class FileExplorerApp implements DesktopApp {
|
||||||
|
private JInternalFrame frame;
|
||||||
|
private File currentDirectory;
|
||||||
|
private final java.util.Stack<File> backStack = new java.util.Stack<>();
|
||||||
|
private final java.util.Stack<File> forwardStack = new java.util.Stack<>();
|
||||||
|
|
||||||
|
private NavigationPanel navPanel;
|
||||||
|
private FileTreePanel treePanel;
|
||||||
|
private FileListPanel listPanel;
|
||||||
|
private JLabel statusBar;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() { return "filehaj"; }
|
||||||
|
@Override
|
||||||
|
public String getName() { return "FileHaj"; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JInternalFrame createWindow(Kernel kernel) {
|
||||||
|
frame = new JInternalFrame("FileHaj", true, true, true, true);
|
||||||
|
frame.setSize(750, 450);
|
||||||
|
frame.setMinimumSize(new Dimension(550, 300));
|
||||||
|
|
||||||
|
currentDirectory = new File(System.getProperty("user.home"));
|
||||||
|
|
||||||
|
navPanel = new NavigationPanel(
|
||||||
|
this::goBack, this::goForward, this::goUp,
|
||||||
|
this::refreshAll, this::navigateTo, this::refreshAll
|
||||||
|
);
|
||||||
|
treePanel = new FileTreePanel(this::navigateTo);
|
||||||
|
listPanel = new FileListPanel(f -> {
|
||||||
|
if (f.isDirectory()) navigateTo(f);
|
||||||
|
else openFile(f);
|
||||||
|
});
|
||||||
|
statusBar = new JLabel(" ");
|
||||||
|
statusBar.setBorder(BorderFactory.createEmptyBorder(2, 5, 2, 5));
|
||||||
|
|
||||||
|
frame.add(navPanel, BorderLayout.NORTH);
|
||||||
|
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, treePanel, listPanel);
|
||||||
|
split.setDividerLocation(200);
|
||||||
|
frame.add(split, BorderLayout.CENTER);
|
||||||
|
frame.add(statusBar, BorderLayout.SOUTH);
|
||||||
|
|
||||||
|
refreshAll();
|
||||||
|
|
||||||
|
frame.setDefaultCloseOperation(JInternalFrame.DISPOSE_ON_CLOSE);
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void navigateTo(File dir) {
|
||||||
|
if (dir == null || !dir.exists() || !dir.isDirectory() || !dir.canRead()) return;
|
||||||
|
if (!dir.equals(currentDirectory)) {
|
||||||
|
backStack.push(currentDirectory);
|
||||||
|
forwardStack.clear();
|
||||||
|
}
|
||||||
|
currentDirectory = dir;
|
||||||
|
refreshAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void goBack() {
|
||||||
|
if (!backStack.isEmpty()) {
|
||||||
|
forwardStack.push(currentDirectory);
|
||||||
|
currentDirectory = backStack.pop();
|
||||||
|
refreshAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void goForward() {
|
||||||
|
if (!forwardStack.isEmpty()) {
|
||||||
|
backStack.push(currentDirectory);
|
||||||
|
currentDirectory = forwardStack.pop();
|
||||||
|
refreshAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void goUp() {
|
||||||
|
File parent = currentDirectory.getParentFile();
|
||||||
|
if (parent != null && parent.canRead()) navigateTo(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshAll() {
|
||||||
|
navPanel.setAddress(currentDirectory.getAbsolutePath());
|
||||||
|
navPanel.updateNavButtons(!backStack.isEmpty(), !forwardStack.isEmpty());
|
||||||
|
statusBar.setText("Loading...");
|
||||||
|
|
||||||
|
FileUtils.runAsync(() -> FileUtils.filterAndSort(currentDirectory.listFiles()), items -> {
|
||||||
|
listPanel.setFiles(items);
|
||||||
|
statusBar.setText(currentDirectory.getAbsolutePath() + " — " + items.size() + " items");
|
||||||
|
treePanel.selectFile(currentDirectory);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openFile(File file) {
|
||||||
|
try {
|
||||||
|
Desktop.getDesktop().open(file);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
JOptionPane.showMessageDialog(frame, "Cannot open file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.rdesk.applications.system.filehaj;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class FileListPanel extends JPanel {
|
||||||
|
private DefaultListModel<File> listModel;
|
||||||
|
private JList<File> fileList;
|
||||||
|
|
||||||
|
public FileListPanel(java.util.function.Consumer<File> onDoubleClick) {
|
||||||
|
setLayout(new BorderLayout());
|
||||||
|
listModel = new DefaultListModel<>();
|
||||||
|
fileList = new JList<>(listModel);
|
||||||
|
fileList.setCellRenderer(new FileListCellRenderer());
|
||||||
|
fileList.addMouseListener(new java.awt.event.MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseClicked(java.awt.event.MouseEvent e) {
|
||||||
|
if (e.getClickCount() == 2) {
|
||||||
|
File f = fileList.getSelectedValue();
|
||||||
|
if (f != null) onDoubleClick.accept(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
add(new JScrollPane(fileList), BorderLayout.CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFiles(List<File> files) {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
listModel.clear();
|
||||||
|
files.forEach(listModel::addElement);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class FileListCellRenderer extends DefaultListCellRenderer {
|
||||||
|
@Override
|
||||||
|
public Component getListCellRendererComponent(JList<?> list, Object value,
|
||||||
|
int index, boolean isSelected, boolean cellHasFocus) {
|
||||||
|
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
||||||
|
if (value instanceof File) {
|
||||||
|
File f = (File) value;
|
||||||
|
setText(f.getName());
|
||||||
|
setIcon(UIManager.getIcon(f.isDirectory() ? "FileView.directoryIcon" : "FileView.fileIcon"));
|
||||||
|
if (f.isFile()) {
|
||||||
|
setText(f.getName() + " (" + FileUtils.formatFileSize(f.length()) + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package com.rdesk.applications.system.filehaj;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.event.TreeExpansionEvent;
|
||||||
|
import javax.swing.event.TreeWillExpandListener;
|
||||||
|
import javax.swing.tree.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class FileTreePanel extends JPanel {
|
||||||
|
private JTree tree;
|
||||||
|
private DefaultTreeModel treeModel;
|
||||||
|
private DefaultMutableTreeNode rootNode;
|
||||||
|
|
||||||
|
public FileTreePanel(java.util.function.Consumer<File> onNavigate) {
|
||||||
|
setLayout(new BorderLayout());
|
||||||
|
rootNode = new DefaultMutableTreeNode("Computer");
|
||||||
|
treeModel = new DefaultTreeModel(rootNode);
|
||||||
|
tree = new JTree(treeModel);
|
||||||
|
tree.setCellRenderer(new MinimalTreeCellRenderer());
|
||||||
|
tree.setShowsRootHandles(true);
|
||||||
|
tree.addTreeSelectionListener(e -> {
|
||||||
|
DefaultMutableTreeNode node = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
|
||||||
|
if (node != null && node.getUserObject() instanceof File) {
|
||||||
|
onNavigate.accept((File) node.getUserObject());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tree.addTreeWillExpandListener(new TreeWillExpandListener() {
|
||||||
|
@Override public void treeWillExpand(TreeExpansionEvent event) {
|
||||||
|
DefaultMutableTreeNode node = (DefaultMutableTreeNode) event.getPath().getLastPathComponent();
|
||||||
|
if (node.getChildCount() == 1 && node.getFirstChild() instanceof PlaceholderNode) {
|
||||||
|
loadTreeChildren(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Override public void treeWillCollapse(TreeExpansionEvent event) {}
|
||||||
|
});
|
||||||
|
add(new JScrollPane(tree), BorderLayout.CENTER);
|
||||||
|
|
||||||
|
// Build initial tree
|
||||||
|
buildInitialTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildInitialTree() {
|
||||||
|
String home = System.getProperty("user.home");
|
||||||
|
addNode(new NamedFile(new File(home), "Home"), true);
|
||||||
|
addNode(new NamedFile(new File(home, "Desktop"), "Desktop"), true);
|
||||||
|
addNode(new NamedFile(new File(home, "Documents"), "Documents"), true);
|
||||||
|
addNode(new NamedFile(new File(home, "Downloads"), "Downloads"), true);
|
||||||
|
addNode(new NamedFile(new File(home, "Music"), "Music"), true);
|
||||||
|
addNode(new NamedFile(new File(home, "Pictures"), "Pictures"), true);
|
||||||
|
addNode(new NamedFile(new File(home, "Videos"), "Videos"), true);
|
||||||
|
|
||||||
|
for (File root : File.listRoots()) {
|
||||||
|
if (root.getAbsolutePath().startsWith("C:")) {
|
||||||
|
addNode(root, true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other drives asynchronously
|
||||||
|
FileUtils.runAsync(() -> {
|
||||||
|
List<DefaultMutableTreeNode> others = new ArrayList<>();
|
||||||
|
for (File root : File.listRoots()) {
|
||||||
|
if (!root.getAbsolutePath().startsWith("C:") && root.exists() && root.canRead()) {
|
||||||
|
DefaultMutableTreeNode node = new DefaultMutableTreeNode(root);
|
||||||
|
node.add(new PlaceholderNode());
|
||||||
|
others.add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
for (DefaultMutableTreeNode node : others) {
|
||||||
|
rootNode.add(node);
|
||||||
|
}
|
||||||
|
treeModel.nodeStructureChanged(rootNode);
|
||||||
|
});
|
||||||
|
}, () -> tree.expandRow(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addNode(Object userObject, boolean withPlaceholder) {
|
||||||
|
DefaultMutableTreeNode node = new DefaultMutableTreeNode(userObject);
|
||||||
|
if (withPlaceholder) node.add(new PlaceholderNode());
|
||||||
|
rootNode.add(node);
|
||||||
|
treeModel.nodeStructureChanged(rootNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadTreeChildren(DefaultMutableTreeNode parent) {
|
||||||
|
File dir = (File) parent.getUserObject();
|
||||||
|
parent.removeAllChildren();
|
||||||
|
parent.add(new PlaceholderNode());
|
||||||
|
treeModel.nodeStructureChanged(parent);
|
||||||
|
|
||||||
|
FileUtils.runAsync(() -> {
|
||||||
|
File[] subdirs = dir.listFiles(f -> f.isDirectory() && FileUtils.shouldShow(f));
|
||||||
|
List<DefaultMutableTreeNode> children = new ArrayList<>();
|
||||||
|
if (subdirs != null) {
|
||||||
|
Arrays.sort(subdirs);
|
||||||
|
for (File child : subdirs) {
|
||||||
|
DefaultMutableTreeNode node = new DefaultMutableTreeNode(child);
|
||||||
|
if (hasVisibleSubdirs(child)) node.add(new PlaceholderNode());
|
||||||
|
children.add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
parent.removeAllChildren();
|
||||||
|
children.forEach(parent::add);
|
||||||
|
treeModel.nodeStructureChanged(parent);
|
||||||
|
});
|
||||||
|
}, () -> {});
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasVisibleSubdirs(File dir) {
|
||||||
|
File[] sub = dir.listFiles(f -> f.isDirectory() && FileUtils.shouldShow(f));
|
||||||
|
return sub != null && sub.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void selectFile(File target) {
|
||||||
|
FileUtils.runAsync(() -> findNode(rootNode, target), node -> {
|
||||||
|
if (node != null) {
|
||||||
|
TreePath path = new TreePath(node.getPath());
|
||||||
|
tree.setSelectionPath(path);
|
||||||
|
tree.scrollPathToVisible(path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private DefaultMutableTreeNode findNode(DefaultMutableTreeNode node, File target) {
|
||||||
|
Object obj = node.getUserObject();
|
||||||
|
File file = (obj instanceof File) ? (File) obj : null;
|
||||||
|
if (file != null && file.equals(target)) return node;
|
||||||
|
for (int i = 0; i < node.getChildCount(); i++) {
|
||||||
|
DefaultMutableTreeNode found = findNode((DefaultMutableTreeNode) node.getChildAt(i), target);
|
||||||
|
if (found != null) return found;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PlaceholderNode extends DefaultMutableTreeNode {
|
||||||
|
PlaceholderNode() { super("..."); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class MinimalTreeCellRenderer extends DefaultTreeCellRenderer {
|
||||||
|
@Override
|
||||||
|
public Component getTreeCellRendererComponent(JTree tree, Object value,
|
||||||
|
boolean sel, boolean expanded,
|
||||||
|
boolean leaf, int row, boolean hasFocus) {
|
||||||
|
super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
|
||||||
|
if (value instanceof DefaultMutableTreeNode) {
|
||||||
|
Object obj = ((DefaultMutableTreeNode) value).getUserObject();
|
||||||
|
if (obj instanceof File) {
|
||||||
|
File f = (File) obj;
|
||||||
|
setText(f.getName().isEmpty() ? f.getPath() : f.getName());
|
||||||
|
setIcon(UIManager.getIcon(f.isDirectory() ? "FileView.directoryIcon" : "FileView.fileIcon"));
|
||||||
|
} else if (obj instanceof NamedFile) {
|
||||||
|
setText(obj.toString());
|
||||||
|
setIcon(UIManager.getIcon("FileView.directoryIcon"));
|
||||||
|
} else if (obj instanceof String && "...".equals(obj)) {
|
||||||
|
setText("Loading...");
|
||||||
|
setIcon(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.rdesk.applications.system.filehaj;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public class FileUtils {
|
||||||
|
private static boolean showHidden = false;
|
||||||
|
|
||||||
|
public static boolean isShowHidden() { return showHidden; }
|
||||||
|
public static void setShowHidden(boolean show) { showHidden = show; }
|
||||||
|
|
||||||
|
public static boolean shouldShow(File f) {
|
||||||
|
if (showHidden) return true;
|
||||||
|
return !f.isHidden() && !f.getName().startsWith(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<File> filterAndSort(File[] files) {
|
||||||
|
List<File> result = new ArrayList<>();
|
||||||
|
if (files == null) return result;
|
||||||
|
for (File f : files) {
|
||||||
|
if (shouldShow(f)) result.add(f);
|
||||||
|
}
|
||||||
|
result.sort((a, b) -> {
|
||||||
|
if (a.isDirectory() && !b.isDirectory()) return -1;
|
||||||
|
if (!a.isDirectory() && b.isDirectory()) return 1;
|
||||||
|
return a.getName().compareToIgnoreCase(b.getName());
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String formatFileSize(long size) {
|
||||||
|
if (size < 1024) return size + " B";
|
||||||
|
int exp = (int) (Math.log(size) / Math.log(1024));
|
||||||
|
char pre = "KMGTPE".charAt(exp - 1);
|
||||||
|
return String.format("%.1f %sB", size / Math.pow(1024, exp), pre);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void runAsync(Runnable background, Runnable uiUpdate) {
|
||||||
|
new SwingWorker<Void, Void>() {
|
||||||
|
@Override protected Void doInBackground() { background.run(); return null; }
|
||||||
|
@Override protected void done() { uiUpdate.run(); }
|
||||||
|
}.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic async with result
|
||||||
|
public static <T> void runAsync(Supplier<T> background, Consumer<T> uiUpdate) {
|
||||||
|
new SwingWorker<T, Void>() {
|
||||||
|
@Override protected T doInBackground() { return background.get(); }
|
||||||
|
@Override protected void done() {
|
||||||
|
try { uiUpdate.accept(get()); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.rdesk.applications.system.filehaj;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
public class NamedFile extends File {
|
||||||
|
private final String displayName;
|
||||||
|
|
||||||
|
public NamedFile(File file, String displayName) {
|
||||||
|
super(file.getAbsolutePath());
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.rdesk.applications.system.filehaj;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.border.EmptyBorder;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.event.ActionListener;
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
public class NavigationPanel extends JPanel {
|
||||||
|
private JButton backButton, forwardButton, upButton, refreshButton;
|
||||||
|
private JTextField addressBar;
|
||||||
|
private JCheckBox hiddenToggle;
|
||||||
|
|
||||||
|
public NavigationPanel(Runnable onBack, Runnable onForward, Runnable onUp,
|
||||||
|
Runnable onRefresh, java.util.function.Consumer<File> onNavigate,
|
||||||
|
Runnable onToggleHidden) {
|
||||||
|
setLayout(new BorderLayout(5, 5));
|
||||||
|
setBorder(new EmptyBorder(5, 5, 5, 5));
|
||||||
|
|
||||||
|
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 2, 0));
|
||||||
|
backButton = new JButton("<");
|
||||||
|
forwardButton = new JButton(">");
|
||||||
|
upButton = new JButton("^");
|
||||||
|
refreshButton = new JButton("R");
|
||||||
|
|
||||||
|
backButton.setToolTipText("Back");
|
||||||
|
forwardButton.setToolTipText("Forward");
|
||||||
|
upButton.setToolTipText("Up");
|
||||||
|
refreshButton.setToolTipText("Refresh");
|
||||||
|
|
||||||
|
backButton.addActionListener(e -> onBack.run());
|
||||||
|
forwardButton.addActionListener(e -> onForward.run());
|
||||||
|
upButton.addActionListener(e -> onUp.run());
|
||||||
|
refreshButton.addActionListener(e -> onRefresh.run());
|
||||||
|
|
||||||
|
buttonPanel.add(backButton);
|
||||||
|
buttonPanel.add(forwardButton);
|
||||||
|
buttonPanel.add(upButton);
|
||||||
|
buttonPanel.add(refreshButton);
|
||||||
|
|
||||||
|
hiddenToggle = new JCheckBox("Show hidden items");
|
||||||
|
hiddenToggle.setToolTipText("Show hidden files and folders");
|
||||||
|
hiddenToggle.addActionListener(e -> {
|
||||||
|
FileUtils.setShowHidden(hiddenToggle.isSelected());
|
||||||
|
onToggleHidden.run();
|
||||||
|
});
|
||||||
|
hiddenToggle.setSelected(FileUtils.isShowHidden());
|
||||||
|
buttonPanel.add(hiddenToggle);
|
||||||
|
|
||||||
|
addressBar = new JTextField();
|
||||||
|
addressBar.addActionListener(e -> onNavigate.accept(new File(addressBar.getText())));
|
||||||
|
|
||||||
|
add(buttonPanel, BorderLayout.WEST);
|
||||||
|
add(addressBar, BorderLayout.CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAddress(String path) { addressBar.setText(path); }
|
||||||
|
public void updateNavButtons(boolean backEnabled, boolean forwardEnabled) {
|
||||||
|
backButton.setEnabled(backEnabled);
|
||||||
|
forwardButton.setEnabled(forwardEnabled);
|
||||||
|
}
|
||||||
|
public void updateHiddenToggle() { hiddenToggle.setSelected(FileUtils.isShowHidden()); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
package com.rdesk.kernel;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.event.ActionEvent;
|
||||||
|
import java.io.File;
|
||||||
|
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) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
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 -> kernel.apps().launch(app.getId()));
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.rdesk.kernel;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class AppManager {
|
||||||
|
|
||||||
|
private final Kernel kernel;
|
||||||
|
private final Map<String, DesktopApp> installedApps = new HashMap<>();
|
||||||
|
private final Map<String, DesktopApp> runningApps = new HashMap<>();
|
||||||
|
|
||||||
|
public AppManager(Kernel kernel) {
|
||||||
|
this.kernel = kernel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void register(DesktopApp app) {
|
||||||
|
installedApps.put(app.getId(), app);
|
||||||
|
System.out.println("[AppManager] Registered: " + app.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRegistered(String id) {
|
||||||
|
return installedApps.containsKey(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void launch(String id) {
|
||||||
|
DesktopApp app = installedApps.get(id);
|
||||||
|
if (app == null) {
|
||||||
|
System.err.println("[AppManager] App not found: " + id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JInternalFrame frame = app.createWindow(kernel);
|
||||||
|
|
||||||
|
frame.putClientProperty("appId", id);
|
||||||
|
|
||||||
|
runningApps.put(id, app);
|
||||||
|
kernel.windows().open(frame);
|
||||||
|
app.onStart(kernel);
|
||||||
|
System.out.println("[AppManager] Launched: " + app.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopAll() {
|
||||||
|
for (DesktopApp app : runningApps.values()) {
|
||||||
|
try {
|
||||||
|
app.onStop(kernel);
|
||||||
|
System.out.println("[AppManager] Stopped: " + app.getName());
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runningApps.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public DesktopApp getApp(String id) {
|
||||||
|
return installedApps.get(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.rdesk.kernel;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
|
||||||
|
public interface DesktopApp {
|
||||||
|
|
||||||
|
String getId();
|
||||||
|
|
||||||
|
String getName();
|
||||||
|
|
||||||
|
default ImageIcon getIcon() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
JInternalFrame createWindow(Kernel kernel);
|
||||||
|
|
||||||
|
default void onStart(Kernel kernel) {}
|
||||||
|
|
||||||
|
default void onStop(Kernel kernel) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package com.rdesk.kernel;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.io.File;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
kernel.apps().launch(app.getId());
|
||||||
|
});
|
||||||
|
|
||||||
|
desktop.add(button);
|
||||||
|
desktop.repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.rdesk.kernel;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class EventBus {
|
||||||
|
|
||||||
|
private final Map<String, List<Consumer<Object>>> listeners = new HashMap<>();
|
||||||
|
|
||||||
|
public void on(String event, Consumer<Object> handler) {
|
||||||
|
listeners.computeIfAbsent(event, k -> new ArrayList<>()).add(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void emit(String event) {
|
||||||
|
emit(event, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void emit(String event, Object data) {
|
||||||
|
List<Consumer<Object>> list = listeners.get(event);
|
||||||
|
if (list == null) return;
|
||||||
|
|
||||||
|
for (Consumer<Object> h : list) {
|
||||||
|
h.accept(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package com.rdesk.kernel;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
|
||||||
|
public class Kernel {
|
||||||
|
|
||||||
|
private static volatile Kernel instance;
|
||||||
|
|
||||||
|
private final AppManager appManager;
|
||||||
|
private final WindowManager windowManager;
|
||||||
|
private final EventBus eventBus;
|
||||||
|
private final ServiceRegistry services;
|
||||||
|
|
||||||
|
private DesktopIconManager iconManager;
|
||||||
|
private JFrame desktopFrame;
|
||||||
|
|
||||||
|
private boolean running = false;
|
||||||
|
|
||||||
|
private Kernel() {
|
||||||
|
this.eventBus = new EventBus();
|
||||||
|
this.services = new ServiceRegistry();
|
||||||
|
this.appManager = new AppManager(this);
|
||||||
|
this.windowManager = new WindowManager(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Thread-safe singleton accessor. */
|
||||||
|
public static Kernel get() {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (Kernel.class) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new Kernel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void boot() {
|
||||||
|
running = true;
|
||||||
|
System.out.println("[Kernel] Booting...");
|
||||||
|
eventBus.emit("kernel.boot");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void attachFrame(JFrame frame) {
|
||||||
|
this.desktopFrame = frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach the icon manager so the kernel can delegate system-app
|
||||||
|
* registration to it. Call this after the desktop pane is ready.
|
||||||
|
*/
|
||||||
|
public void attachIconManager(DesktopIconManager iconManager) {
|
||||||
|
this.iconManager = iconManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
|
// System-app API — call from main() before or after boot() //
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a built-in / system app and place its icon on the desktop.
|
||||||
|
*
|
||||||
|
* <pre>{@code
|
||||||
|
* Kernel kernel = Kernel.get();
|
||||||
|
* kernel.registerSystemApp(new MyCalculatorApp());
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @param app the {@link DesktopApp} implementation to register
|
||||||
|
* @throws IllegalStateException if {@link #attachIconManager} has not been called yet
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
|
||||||
|
System.out.println("[Kernel] Shutdown started");
|
||||||
|
running = false;
|
||||||
|
|
||||||
|
appManager.stopAll();
|
||||||
|
windowManager.closeAll();
|
||||||
|
|
||||||
|
if (desktopFrame != null) {
|
||||||
|
desktopFrame.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
eventBus.emit("kernel.shutdown");
|
||||||
|
System.out.println("[Kernel] Shutdown complete");
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
|
// Accessors //
|
||||||
|
// ------------------------------------------------------------------ //
|
||||||
|
|
||||||
|
public AppManager apps() { return appManager; }
|
||||||
|
public WindowManager windows() { return windowManager; }
|
||||||
|
public EventBus events() { return eventBus; }
|
||||||
|
public ServiceRegistry services() { return services; }
|
||||||
|
public boolean isRunning() { return running; }
|
||||||
|
public JFrame getDesktopFrame() { return desktopFrame; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.rdesk.kernel;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class ServiceRegistry {
|
||||||
|
|
||||||
|
private final Map<String, Object> services = new HashMap<>();
|
||||||
|
|
||||||
|
public void register(String name, Object service) {
|
||||||
|
services.put(name, service);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T get(String name, Class<T> type) {
|
||||||
|
return type.cast(services.get(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
public WindowManager(Kernel kernel) {
|
||||||
|
this.kernel = kernel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void attachDesktop(JDesktopPane desktop) {
|
||||||
|
this.desktop = desktop;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open(JInternalFrame frame) {
|
||||||
|
desktop.add(frame);
|
||||||
|
frame.setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void closeAll() {
|
||||||
|
if (desktop == null) return;
|
||||||
|
for (JInternalFrame frame : desktop.getAllFrames()) {
|
||||||
|
try {
|
||||||
|
frame.setClosed(true);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public JInternalFrame[] getAllWindows() {
|
||||||
|
if (desktop == null) return new JInternalFrame[0];
|
||||||
|
return desktop.getAllFrames();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void closeAllByAppId(String appId) {
|
||||||
|
if (desktop == null) return;
|
||||||
|
for (JInternalFrame frame : desktop.getAllFrames()) {
|
||||||
|
Object prop = frame.getClientProperty("appId");
|
||||||
|
if (appId.equals(prop)) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user