commit e905891b114033078b4fb7d4ff85cc1e80ea83fe Author: rattatwinko Date: Sat May 16 22:56:08 2026 +0200 initial AI slop commit. to be refactored! diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2485648 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/applications/swtc-8.0-STAB.jar b/applications/swtc-8.0-STAB.jar new file mode 100644 index 0000000..9fb321c Binary files /dev/null and b/applications/swtc-8.0-STAB.jar differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f83e6d7 --- /dev/null +++ b/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + com.rdesk + rdesk + 1.0-SNAPSHOT + + + 17 + 17 + UTF-8 + + + + + + com.google.code.gson + gson + 2.14.0 + compile + + + + com.formdev + flatlaf + 3.7.1 + runtime + + + + \ No newline at end of file diff --git a/src/main/java/com/rdesk/Main.java b/src/main/java/com/rdesk/Main.java new file mode 100644 index 0000000..2fba796 --- /dev/null +++ b/src/main/java/com/rdesk/Main.java @@ -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"); + } +} \ No newline at end of file diff --git a/src/main/java/com/rdesk/applications/productivity/Notes.java b/src/main/java/com/rdesk/applications/productivity/Notes.java new file mode 100644 index 0000000..c39211a --- /dev/null +++ b/src/main/java/com/rdesk/applications/productivity/Notes.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/rdesk/applications/system/Exit.java b/src/main/java/com/rdesk/applications/system/Exit.java new file mode 100644 index 0000000..5c8e8b3 --- /dev/null +++ b/src/main/java/com/rdesk/applications/system/Exit.java @@ -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(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/rdesk/applications/system/TaskManager.java b/src/main/java/com/rdesk/applications/system/TaskManager.java new file mode 100644 index 0000000..8330711 --- /dev/null +++ b/src/main/java/com/rdesk/applications/system/TaskManager.java @@ -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 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 processes = new ArrayList<>(); + private final String[] columns = {"Application", "Memory (MB)", "Windows"}; + + ProcessTableModel(Kernel kernel) { + this.kernel = kernel; + refresh(); + } + + void refresh() { + processes.clear(); + + Map 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 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; + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/rdesk/applications/system/filehaj/FileExplorerApp.java b/src/main/java/com/rdesk/applications/system/filehaj/FileExplorerApp.java new file mode 100644 index 0000000..0a7ba83 --- /dev/null +++ b/src/main/java/com/rdesk/applications/system/filehaj/FileExplorerApp.java @@ -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 backStack = new java.util.Stack<>(); + private final java.util.Stack 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); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/rdesk/applications/system/filehaj/FileListPanel.java b/src/main/java/com/rdesk/applications/system/filehaj/FileListPanel.java new file mode 100644 index 0000000..02c5ddf --- /dev/null +++ b/src/main/java/com/rdesk/applications/system/filehaj/FileListPanel.java @@ -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 listModel; + private JList fileList; + + public FileListPanel(java.util.function.Consumer 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 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; + } + } +} diff --git a/src/main/java/com/rdesk/applications/system/filehaj/FileTreePanel.java b/src/main/java/com/rdesk/applications/system/filehaj/FileTreePanel.java new file mode 100644 index 0000000..9fe548c --- /dev/null +++ b/src/main/java/com/rdesk/applications/system/filehaj/FileTreePanel.java @@ -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 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 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 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; + } + } +} diff --git a/src/main/java/com/rdesk/applications/system/filehaj/FileUtils.java b/src/main/java/com/rdesk/applications/system/filehaj/FileUtils.java new file mode 100644 index 0000000..7e95eeb --- /dev/null +++ b/src/main/java/com/rdesk/applications/system/filehaj/FileUtils.java @@ -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 filterAndSort(File[] files) { + List 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() { + @Override protected Void doInBackground() { background.run(); return null; } + @Override protected void done() { uiUpdate.run(); } + }.execute(); + } + + // Generic async with result + public static void runAsync(Supplier background, Consumer uiUpdate) { + new SwingWorker() { + @Override protected T doInBackground() { return background.get(); } + @Override protected void done() { + try { uiUpdate.accept(get()); } catch (Exception ignored) {} + } + }.execute(); + } +} \ No newline at end of file diff --git a/src/main/java/com/rdesk/applications/system/filehaj/NamedFile.java b/src/main/java/com/rdesk/applications/system/filehaj/NamedFile.java new file mode 100644 index 0000000..46497f1 --- /dev/null +++ b/src/main/java/com/rdesk/applications/system/filehaj/NamedFile.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/rdesk/applications/system/filehaj/NavigationPanel.java b/src/main/java/com/rdesk/applications/system/filehaj/NavigationPanel.java new file mode 100644 index 0000000..fe5ee2f --- /dev/null +++ b/src/main/java/com/rdesk/applications/system/filehaj/NavigationPanel.java @@ -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 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()); } +} diff --git a/src/main/java/com/rdesk/kernel/AppLauncher.java b/src/main/java/com/rdesk/kernel/AppLauncher.java new file mode 100644 index 0000000..4d239e5 --- /dev/null +++ b/src/main/java/com/rdesk/kernel/AppLauncher.java @@ -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 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 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(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/rdesk/kernel/AppManager.java b/src/main/java/com/rdesk/kernel/AppManager.java new file mode 100644 index 0000000..78a5295 --- /dev/null +++ b/src/main/java/com/rdesk/kernel/AppManager.java @@ -0,0 +1,57 @@ +package com.rdesk.kernel; + +import javax.swing.*; +import java.util.*; + +public class AppManager { + + private final Kernel kernel; + private final Map installedApps = new HashMap<>(); + private final Map 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); + } +} \ No newline at end of file diff --git a/src/main/java/com/rdesk/kernel/DesktopApp.java b/src/main/java/com/rdesk/kernel/DesktopApp.java new file mode 100644 index 0000000..f960422 --- /dev/null +++ b/src/main/java/com/rdesk/kernel/DesktopApp.java @@ -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) {} +} \ No newline at end of file diff --git a/src/main/java/com/rdesk/kernel/DesktopIconManager.java b/src/main/java/com/rdesk/kernel/DesktopIconManager.java new file mode 100644 index 0000000..31d8a61 --- /dev/null +++ b/src/main/java/com/rdesk/kernel/DesktopIconManager.java @@ -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("
" + app.getName() + "
"); + + // 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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/rdesk/kernel/EventBus.java b/src/main/java/com/rdesk/kernel/EventBus.java new file mode 100644 index 0000000..aca78ad --- /dev/null +++ b/src/main/java/com/rdesk/kernel/EventBus.java @@ -0,0 +1,26 @@ +package com.rdesk.kernel; + +import java.util.*; +import java.util.function.Consumer; + +public class EventBus { + + private final Map>> listeners = new HashMap<>(); + + public void on(String event, Consumer 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> list = listeners.get(event); + if (list == null) return; + + for (Consumer h : list) { + h.accept(data); + } + } +} diff --git a/src/main/java/com/rdesk/kernel/Kernel.java b/src/main/java/com/rdesk/kernel/Kernel.java new file mode 100644 index 0000000..062d446 --- /dev/null +++ b/src/main/java/com/rdesk/kernel/Kernel.java @@ -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. + * + *
{@code
+     *   Kernel kernel = Kernel.get();
+     *   kernel.registerSystemApp(new MyCalculatorApp());
+     * }
+ * + * @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; } +} \ No newline at end of file diff --git a/src/main/java/com/rdesk/kernel/ServiceRegistry.java b/src/main/java/com/rdesk/kernel/ServiceRegistry.java new file mode 100644 index 0000000..72b29f7 --- /dev/null +++ b/src/main/java/com/rdesk/kernel/ServiceRegistry.java @@ -0,0 +1,16 @@ +package com.rdesk.kernel; + +import java.util.*; + +public class ServiceRegistry { + + private final Map services = new HashMap<>(); + + public void register(String name, Object service) { + services.put(name, service); + } + + public T get(String name, Class type) { + return type.cast(services.get(name)); + } +} diff --git a/src/main/java/com/rdesk/kernel/WindowManager.java b/src/main/java/com/rdesk/kernel/WindowManager.java new file mode 100644 index 0000000..6a25a86 --- /dev/null +++ b/src/main/java/com/rdesk/kernel/WindowManager.java @@ -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; + } +} \ No newline at end of file