diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..ab1f416 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/discord.xml b/.idea/discord.xml new file mode 100644 index 0000000..912db82 --- /dev/null +++ b/.idea/discord.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..4731638 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index f83e6d7..bdf66c9 100644 --- a/pom.xml +++ b/pom.xml @@ -14,6 +14,19 @@ UTF-8 + + + + org.openjfx + javafx-maven-plugin + 0.0.8 + + com.rdesk.Main + + + + + @@ -29,6 +42,56 @@ 3.7.1 runtime + + me.friwi + jcefmaven + 146.0.10 + + + + net.java.dev.jna + jna + 5.14.0 + + + net.java.dev.jna + jna-platform + 5.14.0 + + + org.openjfx + javafx-controls + 21.0.2 + + + org.openjfx + javafx-web + 21.0.2 + + + org.openjfx + javafx-swing + 21.0.2 + + + + com.fifesoft + rsyntaxtextarea + 3.6.2 + compile + + + + com.fifesoft + autocomplete + 3.3.3 + + + com.fifesoft + languagesupport + 3.4.1 + + \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..80a4deb --- /dev/null +++ b/readme.md @@ -0,0 +1,4 @@ +# rDesk + + + \ No newline at end of file diff --git a/rev2.png b/rev2.png new file mode 100644 index 0000000..3e78417 Binary files /dev/null and b/rev2.png differ diff --git a/src/main/java/com/rdesk/Main.java b/src/main/java/com/rdesk/Main.java index 2fba796..831bc02 100644 --- a/src/main/java/com/rdesk/Main.java +++ b/src/main/java/com/rdesk/Main.java @@ -1,5 +1,8 @@ package com.rdesk; +import com.rdesk.applications.productivity.Calculator; +import com.rdesk.applications.productivity.browsing.TabbedBrowser; +import com.rdesk.applications.productivity.code.CodeDesk; import com.rdesk.applications.system.Exit; import com.rdesk.applications.productivity.Notes; import com.rdesk.applications.system.TaskManager; @@ -19,11 +22,10 @@ public class Main { } private static void boot() { - Kernel kernel = Kernel.get(); kernel.boot(); + JFrame frame = new JFrame("rDesk"); - JFrame frame = new JFrame("Java OS"); frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); frame.addWindowListener(new java.awt.event.WindowAdapter() { @Override @@ -80,7 +82,11 @@ public class Main { } private static void registerSystemApps(AppLauncher launcher) { + launcher.addApp(new Notes(), "Productivity"); + launcher.addApp(new TabbedBrowser(), "Productivity"); + launcher.addApp(new CodeDesk(), "Productivity"); + launcher.addApp(new Calculator(), "Productivity"); launcher.addApp(new Exit(), "System"); launcher.addApp(new TaskManager(), "System"); launcher.addApp(new FileExplorerApp(), "System"); diff --git a/src/main/java/com/rdesk/applications/productivity/Calculator.java b/src/main/java/com/rdesk/applications/productivity/Calculator.java new file mode 100644 index 0000000..031ee03 --- /dev/null +++ b/src/main/java/com/rdesk/applications/productivity/Calculator.java @@ -0,0 +1,457 @@ +package com.rdesk.applications.productivity; +import com.rdesk.kernel.*; +import javax.swing.*; +import java.awt.*; +import java.awt.event.*; +import java.math.BigInteger; + +public class Calculator implements DesktopApp { + + @Override + public String getId() { return "calculator"; } + + @Override + public String getName() { return "Calculator"; } + + @Override + public JInternalFrame createWindow(Kernel kernel) { + + JInternalFrame frame = new JInternalFrame( + "Calculator", + true, true, true, true + ); + + frame.setSize(420, 480); + frame.setLayout(new BorderLayout(6,6)); + + JPanel top = new JPanel(new BorderLayout(6,6)); + String[] modes = {"Standard", "Scientific", "Programmer"}; + JComboBox modeBox = new JComboBox<>(modes); + top.add(modeBox, BorderLayout.WEST); + + JTextField display = new JTextField(); + display.setEditable(false); + display.setHorizontalAlignment(SwingConstants.RIGHT); + display.setFont(display.getFont().deriveFont(20f)); + top.add(display, BorderLayout.CENTER); + + frame.add(top, BorderLayout.NORTH); + + JPanel cards = new JPanel(new CardLayout()); + JPanel stdPanel = buildStandardPanel(display); + JPanel sciPanel = buildScientificPanel(display); + JPanel progPanel = buildProgrammerPanel(display); + + cards.add(stdPanel, "Standard"); + cards.add(sciPanel, "Scientific"); + cards.add(progPanel, "Programmer"); + + frame.add(cards, BorderLayout.CENTER); + + modeBox.addActionListener(e -> { + CardLayout cl = (CardLayout)(cards.getLayout()); + cl.show(cards, (String)modeBox.getSelectedItem()); + }); + + frame.setVisible(true); + return frame; + } + + private JPanel buildStandardPanel(JTextField display) { + JPanel panel = new JPanel(new BorderLayout()); + JPanel buttons = new JPanel(new GridLayout(5,4,5,5)); + String[] labels = { + "C", "+-", "%", "/", + "7", "8", "9", "*", + "4", "5", "6", "-", + "1", "2", "3", "+", + "0", ".", "=", "OFF" + }; + buttons.setBorder(BorderFactory.createEmptyBorder(8,8,8,8)); + CalculatorState state = new CalculatorState(display); + addButtons(buttons, labels, state); + panel.add(buttons, BorderLayout.CENTER); + return panel; + } + + private JPanel buildScientificPanel(JTextField display) { + JPanel panel = new JPanel(new BorderLayout()); + JPanel topRow = new JPanel(new GridLayout(1,4,5,5)); + topRow.setBorder(BorderFactory.createEmptyBorder(8,8,0,8)); + + JButton sin = new JButton("sin"); + JButton cos = new JButton("cos"); + JButton tan = new JButton("tan"); + JButton sqrt = new JButton("√"); + topRow.add(sin); topRow.add(cos); topRow.add(tan); topRow.add(sqrt); + + JPanel buttons = new JPanel(new GridLayout(5,4,5,5)); + String[] labels = { + "C", "±", "%", "/", + "7", "8", "9", "*", + "4", "5", "6", "-", + "1", "2", "3", "+", + "0", ".", "=", "ln" + }; + buttons.setBorder(BorderFactory.createEmptyBorder(0,8,8,8)); + CalculatorState state = new CalculatorState(display); + addButtons(buttons, labels, state); + + ActionListener sciListener = e -> { + String cmd = ((JButton)e.getSource()).getText(); + try { + double v = state.currentAsDoubleOrLast(); + double result; + switch (cmd) { + case "sin": result = Math.sin(Math.toRadians(v)); break; + case "cos": result = Math.cos(Math.toRadians(v)); break; + case "tan": result = Math.tan(Math.toRadians(v)); break; + case "sqrt": result = Math.sqrt(v); break; + case "ln": result = Math.log(v); break; + default: return; + } + state.setResult(result); + } catch (NumberFormatException ex) { + state.error(); + } + }; + sin.addActionListener(sciListener); + cos.addActionListener(sciListener); + tan.addActionListener(sciListener); + sqrt.addActionListener(sciListener); + + panel.add(topRow, BorderLayout.NORTH); + panel.add(buttons, BorderLayout.CENTER); + return panel; + } + + private JPanel buildProgrammerPanel(JTextField display) { + JPanel panel = new JPanel(new BorderLayout()); + JPanel baseRow = new JPanel(new GridLayout(1,4,5,5)); + baseRow.setBorder(BorderFactory.createEmptyBorder(8,8,0,8)); + JButton binBtn = new JButton("BIN"); + JButton octBtn = new JButton("OCT"); + JButton decBtn = new JButton("DEC"); + JButton hexBtn = new JButton("HEX"); + baseRow.add(binBtn); baseRow.add(octBtn); baseRow.add(decBtn); baseRow.add(hexBtn); + + JPanel buttons = new JPanel(new GridLayout(6,4,5,5)); + buttons.setBorder(BorderFactory.createEmptyBorder(0,8,8,8)); + String[] labels = { + "A","B","C","D", + "E","F","AND","OR", + "<<",">>","^","~", + "7","8","9","/", + "4","5","6","*", + "1","2","3","-", + "0","","=", "+" + }; + CalculatorStateProg state = new CalculatorStateProg(display); + addButtonsProg(buttons, labels, state); + + // base switching + binBtn.addActionListener(e -> state.setBase(2)); + octBtn.addActionListener(e -> state.setBase(8)); + decBtn.addActionListener(e -> state.setBase(10)); + hexBtn.addActionListener(e -> state.setBase(16)); + state.setBase(10); + + panel.add(baseRow, BorderLayout.NORTH); + panel.add(buttons, BorderLayout.CENTER); + return panel; + } + + private void addButtons(JPanel panel, String[] labels, CalculatorState state) { + for (String label : labels) { + if (label.isEmpty()) { panel.add(new JLabel()); continue; } + JButton b = new JButton(label); + b.setFont(b.getFont().deriveFont(14f)); + b.addActionListener(e -> state.handle(((JButton)e.getSource()).getText())); + panel.add(b); + } + } + + private void addButtonsProg(JPanel panel, String[] labels, CalculatorStateProg state) { + for (String label : labels) { + if (label.isEmpty()) { panel.add(new JLabel()); continue; } + JButton b = new JButton(label); + b.setFont(b.getFont().deriveFont(13f)); + b.addActionListener(e -> state.handle(((JButton)e.getSource()).getText())); + panel.add(b); + } + } + + private static class CalculatorState { + private final JTextField display; + private final StringBuilder current = new StringBuilder(); + private double lastValue = 0; + private String op = null; + private boolean resetOnNext = false; + + CalculatorState(JTextField display) { this.display = display; } + + void handle(String cmd) { + try { + switch (cmd) { + case "C": clear(); break; + case "±": negate(); break; + case "%": percent(); break; + case "+": case "-": case "*": case "/": setOperator(cmd); break; + case "=": compute(); break; + case ".": + if (resetOnNext) { current.setLength(0); resetOnNext = false; } + if (current.indexOf(".") < 0) { + if (current.isEmpty()) current.append("0"); + current.append("."); + display.setText(current.toString()); + } + break; + default: // digits + if (resetOnNext) { current.setLength(0); resetOnNext = false; } + if (current.length() == 1 && current.charAt(0) == '0' && cmd.equals("0")) return; + current.append(cmd); + display.setText(current.toString()); + break; + } + } catch (NumberFormatException ex) { error(); } + } + + double currentAsDoubleOrLast() { + if (!current.isEmpty()) return Double.parseDouble(current.toString()); + return lastValue; + } + + void setOperatorInternal(String operator) { + if (!current.isEmpty()) { + double cur = Double.parseDouble(current.toString()); + if (op != null) lastValue = applyOp(lastValue, cur, op); + else lastValue = cur; + display.setText(strip(lastValue)); + current.setLength(0); + } + op = operator; + resetOnNext = false; + } + + void setOperator(String operator) { setOperatorInternal(operator); } + + void compute() { + if (op == null) { + if (!current.isEmpty()) display.setText(current.toString()); + return; + } + double cur = !current.isEmpty() ? Double.parseDouble(current.toString()) : lastValue; + double result = applyOp(lastValue, cur, op); + display.setText(strip(result)); + current.setLength(0); + current.append(strip(result)); + lastValue = result; + op = null; + resetOnNext = true; + } + + void clear() { current.setLength(0); lastValue = 0; op = null; display.setText(""); } + + void negate() { + if (current.isEmpty()) return; + if (current.charAt(0)=='-') current.deleteCharAt(0); else current.insert(0,'-'); + display.setText(current.toString()); + } + + void percent() { + if (current.isEmpty()) return; + double v = Double.parseDouble(current.toString())/100.0; + current.setLength(0); current.append(strip(v)); + display.setText(current.toString()); + } + + void setResult(double v) { + display.setText(strip(v)); + current.setLength(0); current.append(strip(v)); + lastValue = v; op = null; resetOnNext = true; + } + + void error() { display.setText("Error"); current.setLength(0); op = null; lastValue = 0; } + + private double applyOp(double a, double b, String operator) { + return switch (operator) { + case "+" -> a + b; + case "-" -> a - b; + case "*" -> a * b; + case "/" -> b == 0 ? Double.NaN : a / b; + default -> b; + }; + } + + private String strip(double v) { + if (Double.isNaN(v)) return "Error"; + if (v == (long)v) return String.format("%d", (long)v); + return String.format("%s", v).replaceAll("\\.?0+$", ""); + } + } + + /* ------------------------- + Programmer state + - supports bases 2/8/10/16 + - simple bitwise ops using BigInteger for safety + ------------------------- */ + private static class CalculatorStateProg { + private final JTextField display; + private int base = 10; + private BigInteger acc = BigInteger.ZERO; + private final StringBuilder current = new StringBuilder(); + private String pendingOp = null; + + CalculatorStateProg(JTextField display) { this.display = display; } + + void setBase(int b) { + // convert displayed/current values to new base + String from = !current.isEmpty() ? current.toString() : acc.toString(); + try { + BigInteger val = !current.isEmpty() ? new BigInteger(from, base) : acc; + base = b; + current.setLength(0); + current.append(val.toString(base).toUpperCase()); + display.setText(current.toString()); + } catch (Exception e) { + // if conversion fails, reset + base = b; + current.setLength(0); + acc = BigInteger.ZERO; + display.setText(""); + } + } + + void handle(String cmd) { + try { + switch (cmd) { + case "AND": commitBitwise("AND"); break; + case "OR": commitBitwise("OR"); break; + case "^": commitBitwise("XOR"); break; + case "~": applyNot(); break; + case "<<": commitShiftLeft(); break; + case ">>": commitShiftRight(); break; + case "C": clear(); break; + case "=": compute(); break; + default: + // digit or hex letter or operator symbols used as digits + if (isDigitForBase(cmd)) { + current.append(cmd); + display.setText(current.toString()); + } else if (cmd.equals("+")||cmd.equals("-")||cmd.equals("*")||cmd.equals("/")) { + // allow arithmetic ops in programmer mode by converting to big integer math + commitArithmetic(cmd); + } + break; + } + } catch (Exception ex) { display.setText("Error"); current.setLength(0); acc = BigInteger.ZERO; pendingOp = null; } + } + + private boolean isDigitForBase(String s) { + if (s.length()!=1) return false; + char c = s.charAt(0); + if (Character.isDigit(c)) { + int d = c - '0'; + return d < base; + } else { + // hex letters + if (base<=10) return false; + return "ABCDEF".indexOf(Character.toUpperCase(c))>=0; + } + } + + private BigInteger currentBI() { + if (current.isEmpty()) return acc; + return new BigInteger(current.toString(), base); + } + + private void commitBitwise(String op) { + BigInteger cur = currentBI(); + if (pendingOp == null) { + acc = cur; + } else { + acc = applyBitwise(acc, cur, pendingOp); + } + pendingOp = op; + current.setLength(0); + display.setText(acc.toString(base).toUpperCase()); + } + + private void applyNot() { + BigInteger cur = currentBI(); + // bitwise not in two's complement: choose a width; using 32-bit mask for common programmer behavior + int width = Math.max(32, cur.bitLength()+1); + BigInteger mask = BigInteger.ONE.shiftLeft(width).subtract(BigInteger.ONE); + BigInteger result = cur.not().and(mask); + current.setLength(0); + current.append(result.toString(base).toUpperCase()); + display.setText(current.toString()); + } + + private void commitShiftLeft() { + BigInteger cur = currentBI(); + BigInteger res = cur.shiftLeft(1); + current.setLength(0); + current.append(res.toString(base).toUpperCase()); + display.setText(current.toString()); + } + + private void commitShiftRight() { + BigInteger cur = currentBI(); + BigInteger res = cur.shiftRight(1); + current.setLength(0); + current.append(res.toString(base).toUpperCase()); + display.setText(current.toString()); + } + + private void commitArithmetic(String operator) { + BigInteger cur = currentBI(); + if (pendingOp == null) { + acc = cur; + } else { + acc = applyArithmetic(acc, cur, pendingOp); + } + pendingOp = operator; + current.setLength(0); + display.setText(acc.toString(base).toUpperCase()); + } + + private void compute() { + BigInteger cur = currentBI(); + if (pendingOp == null) { + acc = cur; + } else { + if (pendingOp.equals("AND")||pendingOp.equals("OR")||pendingOp.equals("XOR")) { + acc = applyBitwise(acc, cur, pendingOp); + } else { + acc = applyArithmetic(acc, cur, pendingOp); + } + } + current.setLength(0); + current.append(acc.toString(base).toUpperCase()); + display.setText(current.toString()); + pendingOp = null; + } + + private void clear() { current.setLength(0); acc = BigInteger.ZERO; pendingOp = null; display.setText(""); } + + private BigInteger applyBitwise(BigInteger a, BigInteger b, String op) { + return switch (op) { + case "AND" -> a.and(b); + case "OR" -> a.or(b); + case "XOR" -> a.xor(b); + default -> b; + }; + } + + private BigInteger applyArithmetic(BigInteger a, BigInteger b, String op) { + return switch (op) { + case "+" -> a.add(b); + case "-" -> a.subtract(b); + case "*" -> a.multiply(b); + case "/" -> b.equals(BigInteger.ZERO) ? BigInteger.ZERO : a.divide(b); + default -> b; + }; + } + } +} diff --git a/src/main/java/com/rdesk/applications/productivity/browsing/BrowserUI.java b/src/main/java/com/rdesk/applications/productivity/browsing/BrowserUI.java new file mode 100644 index 0000000..c263d41 --- /dev/null +++ b/src/main/java/com/rdesk/applications/productivity/browsing/BrowserUI.java @@ -0,0 +1,295 @@ +package com.rdesk.applications.productivity.browsing; + +import javafx.application.Platform; +import javafx.concurrent.Worker; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.web.WebEngine; +import javafx.scene.web.WebErrorEvent; +import javafx.scene.web.WebHistory; +import javafx.scene.web.WebView; +import javafx.stage.FileChooser; + +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +public class BrowserUI { + + private final String homeUrl; + private final TabPane tabPane = new TabPane(); + private final Map engineMap = new HashMap<>(); + private final TextField urlBar = new TextField(); + private final ProgressIndicator globalProgress = new ProgressIndicator(); + private final Button backBtn = new Button("<"); + private final Button forwardBtn = new Button(">"); + private final Button refreshBtn = new Button("Refresh"); + private final Button homeBtn = new Button("Home"); + private final Button newTabBtn = new Button("+"); + private final Button openFileBtn = new Button("Open"); + + private Scene scene; + + public BrowserUI(String homeUrl) { + this.homeUrl = homeUrl; + } + + public Scene createScene() { + BorderPane root = new BorderPane(); + root.setTop(createToolbar()); + root.setCenter(createTabPane()); + configureTabSelection(); + this.scene = new Scene(root); + return scene; + } + + private HBox createToolbar() { + HBox toolbar = new HBox(5); + toolbar.setPadding(new Insets(5)); + toolbar.setAlignment(Pos.CENTER_LEFT); + urlBar.setPrefWidth(500); + globalProgress.setVisible(false); + globalProgress.setPrefSize(20, 20); + + toolbar.getChildren().addAll( + backBtn, forwardBtn, refreshBtn, homeBtn, newTabBtn, + openFileBtn, urlBar, globalProgress); + + attachToolbarActions(); + return toolbar; + } + + private TabPane createTabPane() { + tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.ALL_TABS); + addNewTab(homeUrl); + return tabPane; + } + + private void attachToolbarActions() { + Consumer> onCurrentTab = operation -> { + try { + Tab current = tabPane.getSelectionModel().getSelectedItem(); + if (current != null) { + WebEngine engine = engineMap.get(current); + if (engine != null) operation.accept(engine); + } + } catch (Exception ex) { + logError("Toolbar action failed", ex); + } + }; + + backBtn.setOnAction(e -> onCurrentTab.accept(engine -> { + WebHistory history = engine.getHistory(); + if (history.getCurrentIndex() > 0) history.go(-1); + })); + + forwardBtn.setOnAction(e -> onCurrentTab.accept(engine -> { + WebHistory history = engine.getHistory(); + if (history.getCurrentIndex() < history.getEntries().size() - 1) history.go(1); + })); + + refreshBtn.setOnAction(e -> onCurrentTab.accept(WebEngine::reload)); + homeBtn.setOnAction(e -> onCurrentTab.accept(engine -> engine.load(homeUrl))); + newTabBtn.setOnAction(e -> addNewTab(homeUrl)); + + // --- Open local file button (fixed: no owner window cast) --- + openFileBtn.setOnAction(e -> { + try { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Open HTML File"); + fileChooser.getExtensionFilters().add( + new FileChooser.ExtensionFilter( + "Web Pages", "*.html", "*.htm", "*.xhtml", "*.svg", "*.xml", "*.txt")); + // Show without an owner window – works inside an embedded JFXPanel + File selectedFile = fileChooser.showOpenDialog(null); + if (selectedFile != null) { + String fileUrl = selectedFile.toURI().toString(); + onCurrentTab.accept(engine -> engine.load(fileUrl)); + } + } catch (Exception ex) { + logError("Open file action failed", ex); + } + }); + + // --- URL bar --- + urlBar.setOnAction(e -> { + try { + String input = urlBar.getText().trim(); + if (input.isEmpty()) return; + + String url = input; + if (!url.contains("://")) { + if (isLikelyFilePath(url)) { + try { + File file = new File(url); + url = file.toURI().toString(); + } catch (Exception ex) { + url = "file://" + url; + } + } else { + url = "https://" + url; + } + urlBar.setText(url); + } + final String finalUrl = url; + onCurrentTab.accept(engine -> engine.load(finalUrl)); + } catch (Exception ex) { + logError("URL bar action failed", ex); + } + }); + } + + private boolean isLikelyFilePath(String input) { + if (input.startsWith("/") || input.startsWith("~")) { + return true; + } + if (input.length() >= 2 && Character.isLetter(input.charAt(0)) + && input.charAt(1) == ':') { + char third = input.length() > 2 ? input.charAt(2) : 0; + if (third == '\\' || third == '/') { + return true; + } + } + return false; + } + + private void configureTabSelection() { + tabPane.getSelectionModel().selectedItemProperty().addListener((obs, oldTab, newTab) -> { + try { + if (newTab != null) { + WebEngine engine = engineMap.get(newTab); + if (engine != null) { + urlBar.setText(engine.getLocation()); + } + } + } catch (Exception ex) { + logError("Tab selection change failed", ex); + } + }); + } + + private void addNewTab(String initialUrl) { + try { + WebView webView = new WebView(); + WebEngine engine = webView.getEngine(); + + engine.getLoadWorker().exceptionProperty().addListener((obs, oldExc, newExc) -> { + if (newExc != null) { + logError("WebEngine load exception", newExc); + showErrorPage(engine, newExc); + } + }); + + engine.setOnError((WebErrorEvent event) -> { + logError("WebEngine onError: " + event.getMessage(), null); + showErrorPage(engine, new Exception(event.getMessage())); + }); + + engine.getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> { + if (newState == Worker.State.FAILED) { + Throwable ex = engine.getLoadWorker().getException(); + if (ex != null) { + showErrorPage(engine, ex); + } + } + }); + + engine.load(initialUrl); + + ProgressIndicator tabProgress = new ProgressIndicator(); + tabProgress.setPrefSize(16, 16); + + Tab tab = new Tab("Loading...", webView); + tab.setGraphic(tabProgress); + tab.setClosable(true); + + engineMap.put(tab, engine); + + engine.titleProperty().addListener((obs, old, newTitle) -> { + try { + if (newTitle != null && !newTitle.isEmpty()) { + tab.setText(newTitle); + } + } catch (Exception ex) { + logError("Title listener error", ex); + } + }); + + engine.locationProperty().addListener((obs, old, newUrl) -> { + try { + if (newUrl != null && !newUrl.isEmpty()) { + Tab selected = tabPane.getSelectionModel().getSelectedItem(); + if (selected == tab) { + urlBar.setText(newUrl); + } + if (engine.getTitle() == null || engine.getTitle().isEmpty()) { + String domain = newUrl.replaceFirst("^https?://", "") + .replaceFirst("/.*$", ""); + tab.setText(domain); + } + } + } catch (Exception ex) { + logError("Location listener error", ex); + } + }); + + engine.getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> { + try { + boolean running = newState == Worker.State.RUNNING; + tabProgress.setVisible(running); + if (!running && tab.getGraphic() == tabProgress) { + tab.setGraphic(null); + } + } catch (Exception ex) { + logError("State listener error", ex); + } + }); + + engine.getLoadWorker().progressProperty().addListener((obs, old, prog) -> { + try { + Tab selected = tabPane.getSelectionModel().getSelectedItem(); + if (selected == tab) { + double value = prog.doubleValue(); + globalProgress.setVisible(value > 0 && value < 1); + } + } catch (Exception ex) { + logError("Progress listener error", ex); + } + }); + + tabPane.getTabs().add(tab); + tabPane.getSelectionModel().select(tab); + tab.setOnClosed(event -> engineMap.remove(tab)); + + } catch (Exception ex) { + logError("Failed to create new tab", ex); + } + } + + private void showErrorPage(WebEngine engine, Throwable exception) { + StringWriter sw = new StringWriter(); + exception.printStackTrace(new PrintWriter(sw)); + String errorHtml = "" + + "

Page could not be loaded

" + + "

" + exception.getMessage() + "

" + + "
"
+                + sw.toString().replace("&", "&").replace("<", "<")
+                + "
" + + ""; + Platform.runLater(() -> engine.loadContent(errorHtml)); + } + + private static void logError(String message, Throwable t) { + System.err.println("[BrowserUI ERROR] " + message); + if (t != null) { + t.printStackTrace(System.err); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/rdesk/applications/productivity/browsing/TabbedBrowser.java b/src/main/java/com/rdesk/applications/productivity/browsing/TabbedBrowser.java new file mode 100644 index 0000000..12cc90c --- /dev/null +++ b/src/main/java/com/rdesk/applications/productivity/browsing/TabbedBrowser.java @@ -0,0 +1,61 @@ +package com.rdesk.applications.productivity.browsing; + +import com.rdesk.kernel.DesktopApp; +import com.rdesk.kernel.Kernel; +import javafx.application.Platform; +import javafx.embed.swing.JFXPanel; +import javafx.scene.Scene; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; + +public class TabbedBrowser implements DesktopApp { + + private static final String HOME_URL = "https://www.google.com"; + + @Override + public String getId() { + return "sharkbrowser"; + } + + @Override + public String getName() { + return "SharkBrowser"; + } + + @Override + public JInternalFrame createWindow(Kernel kernel) { + JInternalFrame frame = new JInternalFrame( + "SharkBrowser", true, true, true, false); + frame.setSize(1200, 800); + frame.setLayout(new BorderLayout()); + + JFXPanel fxPanel = new JFXPanel(); + frame.add(fxPanel, BorderLayout.CENTER); + + Platform.runLater(() -> { + BrowserUI ui = new BrowserUI(HOME_URL); + Scene scene = ui.createScene(); + fxPanel.setScene(scene); + + frame.addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent e) { + Platform.runLater(() -> { + double w = fxPanel.getWidth(); + double h = fxPanel.getHeight(); + if (w > 0 && h > 0) { + scene.getWindow().setWidth(w); + scene.getWindow().setHeight(h); + } + }); + } + }); + }); + + frame.setVisible(true); + return frame; + } +} \ No newline at end of file diff --git a/src/main/java/com/rdesk/applications/productivity/code/CodeDesk.java b/src/main/java/com/rdesk/applications/productivity/code/CodeDesk.java new file mode 100644 index 0000000..5ecda8d --- /dev/null +++ b/src/main/java/com/rdesk/applications/productivity/code/CodeDesk.java @@ -0,0 +1,23 @@ +package com.rdesk.applications.productivity.code; + +import com.rdesk.kernel.*; +import javax.swing.*; + +public class CodeDesk implements DesktopApp { + + @Override + public String getId() { + return "CodeDesk"; + } + + @Override + public String getName() { + return "CodeDesk"; + } + + @Override + public JInternalFrame createWindow(Kernel kernel) { + EditorWindow window = new EditorWindow(); + return window.getFrame(); + } +} \ No newline at end of file diff --git a/src/main/java/com/rdesk/applications/productivity/code/EditorTab.java b/src/main/java/com/rdesk/applications/productivity/code/EditorTab.java new file mode 100644 index 0000000..39bd009 --- /dev/null +++ b/src/main/java/com/rdesk/applications/productivity/code/EditorTab.java @@ -0,0 +1,153 @@ +package com.rdesk.applications.productivity.code; + +import org.fife.rsta.ac.AbstractLanguageSupport; +import org.fife.ui.rsyntaxtextarea.*; +import org.fife.ui.rtextarea.*; +import javax.swing.*; +import javax.swing.event.*; +import java.awt.*; +import java.io.*; + +public class EditorTab { + + private final TabManager tabManager; + private RSyntaxTextArea textArea; + private RTextScrollPane scrollPane; + private File file; + private boolean dirty; + private AbstractLanguageSupport langSupport; + private String title; // base title without dirty marker + private JPanel tabHeaderComponent; + + public EditorTab(String title, TabManager tabManager) { + this.tabManager = tabManager; + this.title = title; + this.file = null; + this.dirty = false; + buildTextArea(); + buildScrollPane(); + installDocumentListener(); + installLanguageSupport(null); + } + + + public RSyntaxTextArea getTextArea() { return textArea; } + public RTextScrollPane getScrollPane() { return scrollPane; } + public File getFile() { return file; } + public boolean isDirty() { return dirty; } + public String getTitle() { return title; } + + public void setTitle(String title) { + this.title = title; + tabManager.updateTabTitle(this); + } + + + public void loadFile(File file) throws IOException { + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + textArea.read(reader, null); + } + this.file = file; + this.dirty = false; + setTitle(file.getName()); + String style = LanguageSupport.syntaxStyleFor(file.getName()); + textArea.setSyntaxEditingStyle(style); + installLanguageSupport(style); + tabManager.updateTabTitle(this); + } + + public void saveToFile(File file) { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { + textArea.write(writer); + this.file = file; + this.dirty = false; + setTitle(file.getName()); + tabManager.updateTabTitle(this); + } catch (IOException ex) { + JOptionPane.showMessageDialog(tabManager.window.getFrame(), + "Error saving file: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); + } + } + + public void performSaveAs(Component parent) { + JFileChooser fc = tabManager.window.buildFileChooser(); + if (fc.showSaveDialog(parent) != JFileChooser.APPROVE_OPTION) return; + File chosen = fc.getSelectedFile(); + saveToFile(chosen); + String style = LanguageSupport.syntaxStyleFor(chosen.getName()); + textArea.setSyntaxEditingStyle(style); + installLanguageSupport(style); + } + + + public JPanel getTabHeaderComponent() { + if (tabHeaderComponent == null) { + tabHeaderComponent = buildTabHeader(); + } + return tabHeaderComponent; + } + + private JPanel buildTabHeader() { + JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); + panel.setOpaque(false); + JLabel label = new JLabel(title); + JButton close = new JButton("✕"); + close.setBorder(BorderFactory.createEmptyBorder(0, 4, 0, 0)); + close.setContentAreaFilled(false); + close.setFocusable(false); + close.addActionListener(e -> tabManager.closeTab(this)); + panel.add(label); + panel.add(close); + return panel; + } + + public void dispose() { + if (langSupport != null) { + langSupport.uninstall(textArea); + langSupport = null; + } + file = null; + } + + + private void buildTextArea() { + textArea = new RSyntaxTextArea(25, 80); + textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_NONE); + textArea.setCodeFoldingEnabled(true); + textArea.setAntiAliasingEnabled(true); + textArea.setBracketMatchingEnabled(true); + textArea.setPaintMatchedBracketPair(true); + textArea.setMarkOccurrences(true); + textArea.setAutoIndentEnabled(true); + textArea.setTabSize(4); + textArea.setTabsEmulated(true); + } + + private void buildScrollPane() { + scrollPane = new RTextScrollPane(textArea); + scrollPane.setFoldIndicatorEnabled(true); + scrollPane.setLineNumbersEnabled(true); + } + + private void installDocumentListener() { + textArea.getDocument().addDocumentListener(new DocumentListener() { + private void markDirty() { + if (!dirty) { + dirty = true; + tabManager.updateTabTitle(EditorTab.this); + } + } + @Override public void insertUpdate(DocumentEvent e) { markDirty(); } + @Override public void removeUpdate(DocumentEvent e) { markDirty(); } + @Override public void changedUpdate(DocumentEvent e) { markDirty(); } + }); + } + + private void installLanguageSupport(String style) { + if (langSupport != null) { + langSupport.uninstall(textArea); + langSupport = null; + } + langSupport = LanguageSupport.install(textArea, style); + } +} \ No newline at end of file diff --git a/src/main/java/com/rdesk/applications/productivity/code/EditorWindow.java b/src/main/java/com/rdesk/applications/productivity/code/EditorWindow.java new file mode 100644 index 0000000..020ddf9 --- /dev/null +++ b/src/main/java/com/rdesk/applications/productivity/code/EditorWindow.java @@ -0,0 +1,122 @@ +package com.rdesk.applications.productivity.code; + +import org.fife.ui.rtextarea.*; +import javax.swing.*; +import javax.swing.event.*; +import java.awt.*; +import java.awt.event.*; +import java.io.*; + +public class EditorWindow { + + private JInternalFrame frame; + final TabManager tabManager; + private JToolBar toolbar; + + public EditorWindow() { + frame = new JInternalFrame("CodeDesk", true, true, true, true); + frame.setSize(960, 680); + frame.setLayout(new BorderLayout()); + + tabManager = new TabManager(this); + + toolbar = buildToolBar(); + frame.add(toolbar, BorderLayout.NORTH); + frame.add(tabManager.getTabbedPane(), BorderLayout.CENTER); + + installGlobalShortcuts(); + tabManager.addTab("Untitled"); + + frame.setVisible(true); + } + + public JInternalFrame getFrame() { + return frame; + } + + private JToolBar buildToolBar() { + JToolBar bar = new JToolBar(); + bar.setFloatable(false); + + JButton newTabBtn = new JButton("New Tab"); + JButton openBtn = new JButton("Open"); + JButton saveBtn = new JButton("Save"); + JButton saveAsBtn = new JButton("Save As"); + + newTabBtn.addActionListener(e -> tabManager.addTab("Untitled")); + openBtn.addActionListener(e -> actionOpen()); + saveBtn.addActionListener(e -> actionSave()); + saveAsBtn.addActionListener(e -> actionSaveAs()); + + bar.add(newTabBtn); + bar.add(openBtn); + bar.add(saveBtn); + bar.add(saveAsBtn); + return bar; + } + + private void installGlobalShortcuts() { + InputMap im = frame.getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); + ActionMap am = frame.getRootPane().getActionMap(); + + bind(im, am, "open", KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK), + e -> actionOpen()); + bind(im, am, "save", KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK), + e -> actionSave()); + bind(im, am, "saveAs", KeyStroke.getKeyStroke(KeyEvent.VK_S, + InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK), + e -> actionSaveAs()); + bind(im, am, "closeTab", KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.CTRL_DOWN_MASK), + e -> tabManager.closeCurrentTab()); + bind(im, am, "newTab", KeyStroke.getKeyStroke(KeyEvent.VK_T, InputEvent.CTRL_DOWN_MASK), + e -> tabManager.addTab("Untitled")); + } + + private void bind(InputMap im, ActionMap am, String key, KeyStroke ks, ActionListener handler) { + im.put(ks, key); + am.put(key, new AbstractAction() { + @Override public void actionPerformed(ActionEvent e) { + handler.actionPerformed(e); + } + }); + } + + + private void actionOpen() { + JFileChooser fc = buildFileChooser(); + if (fc.showOpenDialog(frame) != JFileChooser.APPROVE_OPTION) return; + tabManager.openFile(fc.getSelectedFile()); + } + + private void actionSave() { + EditorTab tab = tabManager.getCurrentTab(); + if (tab == null) return; + if (tab.getFile() != null) { + tab.saveToFile(tab.getFile()); + } else { + tab.performSaveAs(frame); + } + } + + private void actionSaveAs() { + EditorTab tab = tabManager.getCurrentTab(); + if (tab != null) tab.performSaveAs(frame); + } + + + JFileChooser buildFileChooser() { + JFileChooser fc = new JFileChooser(); + fc.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter( + "Source Files (java, js, ts, html, css, php, c, cpp, py, rb, pl, sh, sql, xml)", + "java", "js", "ts", "html", "htm", "css", "php", + "c", "h", "cpp", "cc", "cxx", "hpp", + "py", "rb", "pl", "sh", "bash", "sql", "xml")); + fc.setAcceptAllFileFilterUsed(true); + return fc; + } + + void showError(String prefix, Exception ex) { + JOptionPane.showMessageDialog(frame, + prefix + ": " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); + } +} \ No newline at end of file diff --git a/src/main/java/com/rdesk/applications/productivity/code/LanguageSupport.java b/src/main/java/com/rdesk/applications/productivity/code/LanguageSupport.java new file mode 100644 index 0000000..95787be --- /dev/null +++ b/src/main/java/com/rdesk/applications/productivity/code/LanguageSupport.java @@ -0,0 +1,86 @@ +package com.rdesk.applications.productivity.code; + +import org.fife.rsta.ac.AbstractLanguageSupport; +import org.fife.rsta.ac.css.CssLanguageSupport; +import org.fife.rsta.ac.html.HtmlLanguageSupport; +import org.fife.rsta.ac.java.JavaLanguageSupport; +import org.fife.rsta.ac.js.JavaScriptLanguageSupport; +import org.fife.ui.autocomplete.*; +import org.fife.ui.rsyntaxtextarea.*; +import javax.swing.*; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.io.IOException; + +public class LanguageSupport { + public static String syntaxStyleFor(String fileName) { + if (fileName == null) return SyntaxConstants.SYNTAX_STYLE_NONE; + String n = fileName.toLowerCase(); + if (n.endsWith(".java")) return SyntaxConstants.SYNTAX_STYLE_JAVA; + if (n.endsWith(".js") || n.endsWith(".ts")) return SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT; + if (n.endsWith(".html") || n.endsWith(".htm")) return SyntaxConstants.SYNTAX_STYLE_HTML; + if (n.endsWith(".css")) return SyntaxConstants.SYNTAX_STYLE_CSS; + if (n.endsWith(".php")) return SyntaxConstants.SYNTAX_STYLE_PHP; + if (n.endsWith(".c") || n.endsWith(".h")) return SyntaxConstants.SYNTAX_STYLE_C; + if (n.endsWith(".cpp") || n.endsWith(".cc") + || n.endsWith(".cxx") || n.endsWith(".hpp")) return SyntaxConstants.SYNTAX_STYLE_CPLUSPLUS; + if (n.endsWith(".py")) return SyntaxConstants.SYNTAX_STYLE_PYTHON; + if (n.endsWith(".rb")) return SyntaxConstants.SYNTAX_STYLE_RUBY; + if (n.endsWith(".pl")) return SyntaxConstants.SYNTAX_STYLE_PERL; + if (n.endsWith(".sh") || n.endsWith(".bash")) return SyntaxConstants.SYNTAX_STYLE_UNIX_SHELL; + if (n.endsWith(".sql")) return SyntaxConstants.SYNTAX_STYLE_SQL; + if (n.endsWith(".xml")) return SyntaxConstants.SYNTAX_STYLE_XML; + return SyntaxConstants.SYNTAX_STYLE_NONE; + } + + public static AbstractLanguageSupport install(RSyntaxTextArea area, String style) { + if (style == null) style = area.getSyntaxEditingStyle(); + + AbstractLanguageSupport ls = buildLanguageSupport(style); + if (ls != null) { + try { + configureSupport(ls); + ls.install(area); + return ls; + } catch (Exception ex) { + System.err.println("CodeDesk: could not install language support for " + + style + ": " + ex.getMessage()); + installFallbackCompletion(area); + return null; + } + } else { + installFallbackCompletion(area); + return null; + } + } + + + private static AbstractLanguageSupport buildLanguageSupport(String style) { + return switch (style) { + case SyntaxConstants.SYNTAX_STYLE_JAVA -> new JavaLanguageSupport(); + case SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT -> new JavaScriptLanguageSupport(); + case SyntaxConstants.SYNTAX_STYLE_HTML -> new HtmlLanguageSupport(); + case SyntaxConstants.SYNTAX_STYLE_CSS -> new CssLanguageSupport(); + default -> null; + }; + } + + private static void configureSupport(AbstractLanguageSupport ls) throws IOException { + ls.setAutoActivationEnabled(true); + ls.setAutoActivationDelay(400); + ls.setAutoCompleteEnabled(true); + ls.setParameterAssistanceEnabled(true); + ls.setShowDescWindow(true); + if (ls instanceof JavaLanguageSupport jls) { + jls.getJarManager().addCurrentJreClassFileSource(); + } + } + + private static void installFallbackCompletion(RSyntaxTextArea area) { + AutoCompletion ac = new AutoCompletion(new DefaultCompletionProvider()); + ac.setAutoCompleteEnabled(true); + ac.setAutoActivationEnabled(false); + ac.setTriggerKey(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, InputEvent.CTRL_DOWN_MASK)); + ac.install(area); + } +} \ No newline at end of file diff --git a/src/main/java/com/rdesk/applications/productivity/code/TabManager.java b/src/main/java/com/rdesk/applications/productivity/code/TabManager.java new file mode 100644 index 0000000..40e9ca1 --- /dev/null +++ b/src/main/java/com/rdesk/applications/productivity/code/TabManager.java @@ -0,0 +1,135 @@ +package com.rdesk.applications.productivity.code; + +import org.fife.ui.rsyntaxtextarea.*; +import org.fife.ui.rtextarea.RTextScrollPane; + +import javax.swing.*; +import java.awt.*; +import java.io.*; +import java.util.*; +import java.util.List; + +public class TabManager { + + final EditorWindow window; + private final JTabbedPane tabbedPane; + private final List tabs = new ArrayList<>(); + + public TabManager(EditorWindow window) { + this.window = window; + this.tabbedPane = new JTabbedPane(); + tabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); + } + + public JTabbedPane getTabbedPane() { + return tabbedPane; + } + + + public void addTab(String title) { + EditorTab tab = new EditorTab(title, this); + addTabComponent(tab); + } + + public void openFile(File file) { + for (EditorTab tab : tabs) { + if (file.equals(tab.getFile())) { + selectTabFor(tab); + return; + } + } + + EditorTab current = getCurrentTab(); + if (current != null + && current.getTextArea().getText().isEmpty() + && current.getFile() == null) { + loadFileInTab(current, file); + } else { + EditorTab newTab = new EditorTab(file.getName(), this); + addTabComponent(newTab); + loadFileInTab(newTab, file); + } + } + + public void closeCurrentTab() { + int idx = tabbedPane.getSelectedIndex(); + if (idx < 0) return; + closeTab(tabs.get(idx)); + } + + public void closeTab(EditorTab tab) { + if (!confirmDiscardOrSave(tab)) return; + + int idx = tabs.indexOf(tab); + if (idx >= 0) { + tabbedPane.removeTabAt(idx); + tabs.remove(idx); + } + tab.dispose(); + } + + public void updateTabTitle(EditorTab tab) { + int idx = tabs.indexOf(tab); + if (idx < 0) return; + + String title = (tab.isDirty() ? "*" : "") + + (tab.getFile() != null ? tab.getFile().getName() : "Untitled"); + tabbedPane.setTitleAt(idx, title); + + // Update the label inside the custom tab component + Component comp = tabbedPane.getTabComponentAt(idx); + if (comp instanceof JPanel p && p.getComponentCount() > 0 + && p.getComponent(0) instanceof JLabel lbl) { + lbl.setText(title); + } + } + + public EditorTab getCurrentTab() { + int idx = tabbedPane.getSelectedIndex(); + return (idx >= 0 && idx < tabs.size()) ? tabs.get(idx) : null; + } + + + private void addTabComponent(EditorTab tab) { + tabs.add(tab); + RTextScrollPane scroll = tab.getScrollPane(); + tabbedPane.addTab(tab.getTitle(), scroll); + int idx = tabbedPane.indexOfComponent(scroll); + tabbedPane.setTabComponentAt(idx, tab.getTabHeaderComponent()); + tabbedPane.setSelectedIndex(idx); + updateTabTitle(tab); + } + + private void loadFileInTab(EditorTab tab, File file) { + try { + tab.loadFile(file); + } catch (IOException ex) { + window.showError("Error opening file", ex); + } + } + + private void selectTabFor(EditorTab tab) { + int idx = tabs.indexOf(tab); + if (idx >= 0) tabbedPane.setSelectedIndex(idx); + } + + private boolean confirmDiscardOrSave(EditorTab tab) { + if (!tab.isDirty()) return true; + + String name = (tab.getFile() != null) ? tab.getFile().getName() : "Untitled"; + int result = JOptionPane.showConfirmDialog( + window.getFrame(), + "Save changes to \"" + name + "\"?", + "Unsaved Changes", + JOptionPane.YES_NO_CANCEL_OPTION); + + if (result == JOptionPane.YES_OPTION) { + if (tab.getFile() != null) { + tab.saveToFile(tab.getFile()); + } else { + tab.performSaveAs(window.getFrame()); + } + } + return result != JOptionPane.CANCEL_OPTION; + } +} \ 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 index 5c8e8b3..4dc9fbb 100644 --- a/src/main/java/com/rdesk/applications/system/Exit.java +++ b/src/main/java/com/rdesk/applications/system/Exit.java @@ -5,6 +5,7 @@ import com.rdesk.kernel.Kernel; import javax.swing.*; import java.awt.*; +import java.io.IOException; public class Exit implements DesktopApp { @@ -35,10 +36,20 @@ public class Exit implements DesktopApp { label.setFont(label.getFont().deriveFont(Font.BOLD)); mainPanel.add(label, gbc); - JButton shutdownButton = new JButton("Shutdown OS"); + JButton shutdownButton = new JButton("Shutdown rDesk"); + JButton shutdownOS = new JButton("Shutdown Host OS"); shutdownButton.setFocusPainted(false); shutdownButton.addActionListener(e -> confirmAndShutdown(kernel, frame)); + shutdownOS.setFocusPainted(false); + shutdownOS.addActionListener(e -> { + try { + shutdownHost(kernel, frame); + } catch (IOException | InterruptedException ex) { + throw new RuntimeException(ex); + } + }); mainPanel.add(shutdownButton, gbc); + mainPanel.add(shutdownOS, gbc); JButton cancelButton = new JButton("Cancel"); cancelButton.setFocusPainted(false); @@ -50,10 +61,47 @@ public class Exit implements DesktopApp { return frame; } + public void performShutdownCmd() throws IOException, InterruptedException { + String os = System.getProperty("os.name").toLowerCase(); + String[] cmd; + + if (os.contains("win")) { + cmd = new String[] {"shutdown.exe", "-s", "-t", "2"}; + } else if (os.contains("mac") || os.contains("darwin") || os.contains("nix") || os.contains("nux") || os.contains("aix")) { + cmd = new String[] {"shutdown", "-h", "now"}; + } else { + throw new RuntimeException("Unknown OS: " + os); + } + + ProcessBuilder pb = new ProcessBuilder(cmd); + pb.inheritIO(); + Process p = pb.start(); + int exit = p.waitFor(); + if (exit != 0) { + throw new IOException("Shutdown command exited with code " + exit); + } + } + + private void shutdownHost(Kernel kernel, JInternalFrame frame) throws IOException, InterruptedException { + int choice = JOptionPane.showConfirmDialog( + frame, + "Confirm Host Shutdown", + "Are you sure? This will shutdown the Host OS", + JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE + ); + + if (choice == JOptionPane.YES_OPTION) { + frame.dispose(); + performShutdownCmd(); // cause else the call wouldnt be called + kernel.shutdown(); + } + } + 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.", + "Are you sure?", "Confirm Shutdown", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE diff --git a/src/main/java/com/rdesk/kernel/AppLauncher.java b/src/main/java/com/rdesk/kernel/AppLauncher.java index 4d239e5..a8367e9 100644 --- a/src/main/java/com/rdesk/kernel/AppLauncher.java +++ b/src/main/java/com/rdesk/kernel/AppLauncher.java @@ -4,6 +4,7 @@ import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.io.File; +import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; import java.util.*; @@ -144,7 +145,12 @@ public class AppLauncher extends JPanel { } catch (Throwable ignored) {} } } catch (Exception e) { - e.printStackTrace(); + JOptionPane.showMessageDialog( + null, + "Kernel Fault", + "The Kernel has encoutered a fault when trying to find DesktopAppClass" + e.getStackTrace(), + JOptionPane.WARNING_MESSAGE + ); } return null; } @@ -198,7 +204,18 @@ public class AppLauncher extends JPanel { 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())); + appButton.addActionListener(e -> { + try { + kernel.apps().launch(app.getId()); + } catch (IOException ex) { + JOptionPane.showMessageDialog( + appsContainer, + "Kernel Fault", + "The Kernel encoutered a critical error when launching an Application", + JOptionPane.ERROR_MESSAGE + ); + } + }); // Make each app button fill the width of its container appButton.setAlignmentX(LEFT_ALIGNMENT); diff --git a/src/main/java/com/rdesk/kernel/AppManager.java b/src/main/java/com/rdesk/kernel/AppManager.java index 78a5295..d89ab9e 100644 --- a/src/main/java/com/rdesk/kernel/AppManager.java +++ b/src/main/java/com/rdesk/kernel/AppManager.java @@ -1,6 +1,7 @@ package com.rdesk.kernel; import javax.swing.*; +import java.io.IOException; import java.util.*; public class AppManager { @@ -22,7 +23,7 @@ public class AppManager { return installedApps.containsKey(id); } - public void launch(String id) { + public void launch(String id) throws IOException { DesktopApp app = installedApps.get(id); if (app == null) { System.err.println("[AppManager] App not found: " + id); diff --git a/src/main/java/com/rdesk/kernel/DesktopApp.java b/src/main/java/com/rdesk/kernel/DesktopApp.java index f960422..b1636f4 100644 --- a/src/main/java/com/rdesk/kernel/DesktopApp.java +++ b/src/main/java/com/rdesk/kernel/DesktopApp.java @@ -1,6 +1,7 @@ package com.rdesk.kernel; import javax.swing.*; +import java.io.IOException; public interface DesktopApp { @@ -12,7 +13,7 @@ public interface DesktopApp { return null; } - JInternalFrame createWindow(Kernel kernel); + JInternalFrame createWindow(Kernel kernel) throws IOException; default void onStart(Kernel kernel) {} diff --git a/src/main/java/com/rdesk/kernel/DesktopIconManager.java b/src/main/java/com/rdesk/kernel/DesktopIconManager.java index 31d8a61..ace0b7a 100644 --- a/src/main/java/com/rdesk/kernel/DesktopIconManager.java +++ b/src/main/java/com/rdesk/kernel/DesktopIconManager.java @@ -3,6 +3,7 @@ package com.rdesk.kernel; import javax.swing.*; import java.awt.*; import java.io.File; +import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; import java.util.jar.*; @@ -171,7 +172,11 @@ public class DesktopIconManager { if (!kernel.apps().isRegistered(app.getId())) { kernel.apps().register(app); } - kernel.apps().launch(app.getId()); + try { + kernel.apps().launch(app.getId()); + } catch (IOException ex) { + throw new RuntimeException(ex); + } }); desktop.add(button); diff --git a/src/main/java/com/rdesk/kernel/Kernel.java b/src/main/java/com/rdesk/kernel/Kernel.java index 062d446..c6d2941 100644 --- a/src/main/java/com/rdesk/kernel/Kernel.java +++ b/src/main/java/com/rdesk/kernel/Kernel.java @@ -23,7 +23,6 @@ public class Kernel { this.windowManager = new WindowManager(this); } - /** Thread-safe singleton accessor. */ public static Kernel get() { if (instance == null) { synchronized (Kernel.class) { @@ -45,29 +44,10 @@ public class Kernel { 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( @@ -96,10 +76,6 @@ public class Kernel { System.exit(0); } - // ------------------------------------------------------------------ // - // Accessors // - // ------------------------------------------------------------------ // - public AppManager apps() { return appManager; } public WindowManager windows() { return windowManager; } public EventBus events() { return eventBus; }