diff --git a/.gitignore b/.gitignore index 78596d7..07cd66f 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,4 @@ bin/ ### Mac OS ### .DS_Store /src/main/resources/config/ -/src/main/resources/engine.json -/src/main/resources/log4j2.xml /cache/ diff --git a/logs/lastlog.log b/logs/lastlog.log index c349a7e..33ab7b5 100644 --- a/logs/lastlog.log +++ b/logs/lastlog.log @@ -1,5 +1,13 @@ -[03:30:18.666][INFO] ===== Engine Initialization ===== -[03:30:18.668][INFO] +[07:39:48.308][INFO] +╔════════════════════════════════════════╗ +║ ===== FoxEngine Initialization ===== ║ +║ Engine Version: 1.15.16 ║ +║ Operating System: windows ║ +║ Available Processors: 20 ║ +║ System Load Average: -1,00 ║ +║ Log Level: INFO ║ +╚════════════════════════════════════════╝ + _____ _____ _____ /\ \ /\ \ /\ \ /::\ \ /::\ \ /::\ \ @@ -20,37 +28,39 @@ \:::\ \ \:::\____\ \::/ / - \/____/ -[03:30:18.674][INFO] Engine Version: 1.15.15 -[03:30:18.674][INFO] Application Title: FoxesLauncher-1.24.1 -[03:30:18.674][INFO] Operating System: windows -[03:30:18.674][INFO] Available Processors: 20 -[03:30:18.675][INFO] System Load Average: -1.0 -[03:30:18.675][INFO] Log Level: INFO -[03:30:18.679][INFO] Log level set to INFO -[03:30:18.762][INFO] Initializing ExecutorService with pool size: 10 -[03:30:18.763][WARN] executor-config.properties not found, using default settings. -[03:30:18.763][INFO] Creating default ExecutorService with pool size: 10 -[03:30:18.884][INFO]forge-0 Sounds loaded successfully -[03:30:19.087][INFO] FrameConstructor initialization -[03:30:19.091][INFO] Building FrameConstructor... -[03:30:19.210][INFO]forge-1 Initializing StyleProvider with path: assets/styles/ -[03:30:19.220][INFO]forge-1 Registering json adapter... -[03:30:19.221][INFO]forge-1 Registering json5 adapter... -[03:30:19.221][INFO]forge-1 Registered adapters: [json5, json] -[03:30:19.233][INFO]forge-1 - Registered component: label -[03:30:19.235][INFO]forge-1 - Registered component: progressBar -[03:30:19.235][INFO]forge-1 - Registered component: button -[03:30:19.235][INFO]forge-1 - Registered component: textArea -[03:30:19.236][INFO]forge-1 - Registered component: checkBox -[03:30:19.236][INFO]forge-1 - Registered component: textField -[03:30:19.236][INFO]forge-1 - Registered component: spriteImage -[03:30:19.237][INFO]forge-1 - Registered component: passField -[03:30:19.237][INFO]forge-1 - Registered component: spinner -[03:30:19.237][INFO]forge-1 - Registered component: multiButton -[03:30:19.238][INFO]forge-1 - Registered component: dropBox -[03:30:19.238][INFO]forge-1 - Registered component: slider -[03:30:19.238][INFO]forge-1 - Registered component: compositeSlider -[03:30:19.239][INFO]forge-1 - Registered component: fileSelector -[03:30:19.239][INFO]forge-1 - Registered component: compositeComponent -[03:30:19.345][INFO] Created font - mcfontBold + \/____/ +— TestApp-1.0.0 +[07:39:48.315][INFO] Log level set to INFO +[07:39:48.402][INFO] Initializing ExecutorService with pool size: 10 +[07:39:48.403][WARN] executor-config.properties not found, using default settings. +[07:39:48.403][INFO] Creating default ExecutorService with pool size: 10 +[07:39:48.524][INFO]forge-0 Sounds loaded successfully +[07:39:48.702][INFO] FrameConstructor: initialization started +[07:39:48.702][INFO] FrameConstructor: building AppFrame from 'assets/frames/frame.json' +[07:39:48.846][INFO] FrameConstructor: frame is visible (title='ЛисийМир', size=850x500) +[07:39:48.846][INFO] FrameConstructor: frame successfully built from 'assets/frames/frame.json' +[07:39:48.847][INFO] FrameConstructor: initialization completed +[07:39:48.849][INFO]forge-1 Initializing StyleProvider with path: assets/styles/ +[07:39:48.861][INFO]forge-1 Registering json adapter... +[07:39:48.861][INFO]forge-1 Registering json5 adapter... +[07:39:48.861][INFO]forge-1 Registered adapters: [json5, json] +[07:39:48.874][INFO]forge-1 - Registered component: label +[07:39:48.874][INFO]forge-1 - Registered component: progressBar +[07:39:48.875][INFO]forge-1 - Registered component: button +[07:39:48.875][INFO]forge-1 - Registered component: textArea +[07:39:48.875][INFO]forge-1 - Registered component: checkBox +[07:39:48.876][INFO]forge-1 - Registered component: textField +[07:39:48.876][INFO]forge-1 - Registered component: spriteImage +[07:39:48.876][INFO]forge-1 - Registered component: passField +[07:39:48.877][INFO]forge-1 - Registered component: spinner +[07:39:48.877][INFO]forge-1 - Registered component: multiButton +[07:39:48.877][INFO]forge-1 - Registered component: dropBox +[07:39:48.878][INFO]forge-1 - Registered component: slider +[07:39:48.878][INFO]forge-1 - Registered component: compositeSlider +[07:39:48.878][INFO]forge-1 - Registered component: fileSelector +[07:39:48.879][INFO]forge-1 - Registered component: compositeComponent +[07:39:48.884][INFO]forge-1 ActionHandler created with mainFrame panel and listens [class org.foxesworld.engine.gui.components.multiButton.MultiButton, class org.foxesworld.engine.gui.components.button.Button] +[07:39:48.974][INFO] FrameConstructor: frame focus gained +[07:39:48.975][INFO] FrameConstructor: frame focus gained +[07:39:49.004][INFO] Created font - mcfontBold +[07:39:49.093][INFO] Created font - mcfont diff --git a/src/main/java/org/foxesworld/engine/Engine.java b/src/main/java/org/foxesworld/engine/Engine.java index 66bde76..89a34e7 100644 --- a/src/main/java/org/foxesworld/engine/Engine.java +++ b/src/main/java/org/foxesworld/engine/Engine.java @@ -165,17 +165,17 @@ public Engine(int poolSize, String worker, Map> configFiles) { appTitle = engineData.getLauncherBrand() + '-' + engineData.getLauncherVersion(); this.panelVisibility = new PanelVisibility(this); - LOGGER.info("===== Engine Initialization ====="); - LOGGER.info(this.engineInfo.getEngineBrand()); - LOGGER.info("Engine Version: {}", this.engineInfo.getEngineVersion()); - LOGGER.info("Application Title: {}", appTitle); - LOGGER.info("Operating System: {}", currentOS); - LOGGER.info("Available Processors: {}", osBean.getAvailableProcessors()); - LOGGER.info("System Load Average: {}", osBean.getSystemLoadAverage()); - LOGGER.info("Log Level: {}", engineData.getLogLevel()); - if(configFiles != null) { - LOGGER.info("Configuration Files: {}", configFiles.keySet()); - } + logEngineInfoBox( + LOGGER, + this.engineInfo.getEngineBrand(), + this.engineInfo.getEngineVersion(), + appTitle, + currentOS, + osBean, + engineData.getLogLevel(), + configFiles + ); + this.FONTUTILS = new FontUtils(this); setLogLevel(Level.valueOf(engineData.getLogLevel())); @@ -183,6 +183,12 @@ public Engine(int poolSize, String worker, Map> configFiles) { this.imageUtils = new ImageUtils(); FlatIntelliJLaf.setup(); + + //Basic Components Initialisation + this.LANG = new LanguageProvider(this, fileProperties.getLocaleFile(), 0); + this.SOUND = new Sound(this, getClass().getClassLoader().getResourceAsStream(fileProperties.getSoundsFile())); + this.frameConstructor = new FrameConstructor(this); + this.CRYPTO = new CryptUtils(); } /** @@ -195,6 +201,56 @@ public void setLogLevel(Level level) { LOGGER.info("Log level set to " + level); } + public static void logEngineInfoBox( + Logger LOGGER, + String engineBrand, + String engineVersion, + String appTitle, + String currentOS, + OperatingSystemMXBean osBean, + String logLevel, + Map configFiles // nullable + ) { + String header = String.format( + "%s \n— %s", + engineBrand != null ? engineBrand : "Unknown Engine", + appTitle != null ? appTitle : "Untitled" + ); + + // Prepare lines + List lines = new ArrayList<>(); + lines.add("===== FoxEngine Initialization ====="); + lines.add(String.format("Engine Version: %s", engineVersion)); + lines.add(String.format("Operating System: %s", currentOS)); + if (osBean != null) { + lines.add(String.format("Available Processors: %d", osBean.getAvailableProcessors())); + lines.add(String.format("System Load Average: %.2f", osBean.getSystemLoadAverage())); + } + lines.add(String.format("Log Level: %s", logLevel)); + if (configFiles != null && !configFiles.isEmpty()) { + lines.add(String.format("Configuration Files: %s", configFiles.keySet())); + } + + // Compute max width + int max = 0; + for (String l : lines) if (l.length() > max) max = l.length(); + int padding = 2; // left + right inner padding + int innerWidth = max + padding * 2; + + // Box drawing + String top = "╔" + "═".repeat(innerWidth) + "╗\n"; + String middle = ""; + String bottom = "╚" + "═".repeat(innerWidth) + "╝"; + + for (String l : lines) { + String padded = " ".repeat(padding) + l + " ".repeat(innerWidth - padding - l.length()); + middle += "║" + padded + "║\n"; + } + String box = top + middle + bottom; + LOGGER.info("\n" + box + "\n" + header ); + } + + /** * Abstract method for initializing the concrete engine implementation. * Implement application-specific initialization (load services, GUI, etc.). @@ -676,6 +732,10 @@ public IconUtils getIconUtils() { return iconUtils; } + public FileProperties getFileProperties() { + return fileProperties; + } + /** * Sets the icon utilities. * diff --git a/src/main/java/org/foxesworld/engine/Test.java b/src/main/java/org/foxesworld/engine/Test.java index d7498b2..833a2ba 100644 --- a/src/main/java/org/foxesworld/engine/Test.java +++ b/src/main/java/org/foxesworld/engine/Test.java @@ -4,12 +4,10 @@ import org.foxesworld.engine.gui.GuiBuilder; import org.foxesworld.engine.gui.components.ComponentAttributes; import org.foxesworld.engine.gui.components.ComponentFactoryListener; -import org.foxesworld.engine.gui.components.frame.FrameConstructor; +import org.foxesworld.engine.gui.components.button.Button; import org.foxesworld.engine.gui.components.frame.OptionGroups; +import org.foxesworld.engine.gui.components.multiButton.MultiButton; import org.foxesworld.engine.gui.styles.StyleProvider; -import org.foxesworld.engine.locale.LanguageProvider; -import org.foxesworld.engine.sound.Sound; -import org.foxesworld.engine.utils.Crypt.CryptUtils; import org.foxesworld.engine.utils.IconUtils; import javax.swing.*; @@ -53,20 +51,17 @@ protected void preInit() { System.setProperty("AppDir", System.getenv("APPDATA")); System.setProperty("RamAmount", String.valueOf(Runtime.getRuntime().maxMemory() / 45)); - this.LANG = new LanguageProvider(this, fileProperties.getLocaleFile(), 0); - this.SOUND = new Sound(this, getClass().getClassLoader().getResourceAsStream(fileProperties.getSoundsFile())); - this.frameConstructor = new FrameConstructor(this); - this.CRYPTO = new CryptUtils(); this.frameConstructor.setFocusStatusListener(this); } @Override protected void postInit() { + setActionHandler(new ActionHandler(this.getGuiBuilder(), "mainFrame", List.of(MultiButton.class, Button.class))); } @Override public void onPanelsBuilt() { - + this.getFrame().repaint(); } @Override @@ -76,7 +71,6 @@ public void onAdditionalPanelBuild(JPanel panel) { @Override public void onGuiBuilt() { - this.getFrame().repaint(); } @Override @@ -86,7 +80,7 @@ public void onPanelBuild(Map panels, String componentGroup @Override public void actionPerformed(ActionEvent e) { - + actionHandler.handleAction(e); } @Override @@ -94,7 +88,7 @@ public void updateFocus(boolean hasFocus) { } - class InitialValue extends ComponentValue implements ComponentFactoryListener { + static class InitialValue extends ComponentValue implements ComponentFactoryListener { private int count; private final Test launcher; @@ -121,7 +115,7 @@ public void setInitialData(ComponentAttributes componentAttributes) { } } - class ActionHandler extends org.foxesworld.engine.gui.ActionHandler { + static class ActionHandler extends org.foxesworld.engine.gui.ActionHandler { public ActionHandler(GuiBuilder guiBuilder, String panelId, List> componentTypes) { super(guiBuilder, panelId, componentTypes); @@ -130,7 +124,15 @@ public ActionHandler(GuiBuilder guiBuilder, String panelId, List> compo @Override public void handleAction(ActionEvent e) { - + switch (e.getActionCommand()){ + case "closeButton": + System.exit(0); + break; + + case "hideButton": + engine.getFrame().setExtendedState(Frame.ICONIFIED); + break; + } } @Override @@ -145,7 +147,7 @@ public void unregisterCommand(String key) { @Override public void executeCommand(String key, ActionEvent event) { - + System.out.println(key); } } } diff --git a/src/main/java/org/foxesworld/engine/gui/ActionHandler.java b/src/main/java/org/foxesworld/engine/gui/ActionHandler.java index 3754f68..2a86e0d 100644 --- a/src/main/java/org/foxesworld/engine/gui/ActionHandler.java +++ b/src/main/java/org/foxesworld/engine/gui/ActionHandler.java @@ -14,6 +14,8 @@ public abstract class ActionHandler extends ComponentsAccessor implements Dynami public ActionHandler(GuiBuilder guiBuilder, String panelId, List> componentTypes) { super(guiBuilder, panelId, componentTypes); + this.engine = guiBuilder.getEngine(); + Engine.LOGGER.info("ActionHandler created with {} panel and listens {}", panelId, componentTypes); } @SuppressWarnings("unused") diff --git a/src/main/java/org/foxesworld/engine/gui/componentAccessor/ComponentsAccessor.java b/src/main/java/org/foxesworld/engine/gui/componentAccessor/ComponentsAccessor.java index c659a5e..5cd642c 100644 --- a/src/main/java/org/foxesworld/engine/gui/componentAccessor/ComponentsAccessor.java +++ b/src/main/java/org/foxesworld/engine/gui/componentAccessor/ComponentsAccessor.java @@ -89,13 +89,13 @@ public ComponentsAccessor(GuiBuilder guiBuilder, String panelId, List> * Scans declared fields of this class for the {@code @Component} annotation and injects * matching {@link JComponent} instances into annotated fields. * - *

+ * * Injection rules: *

    *
  • If annotation's {@code value()} is non-empty, it is used as the component id.
  • *
  • Otherwise the Java field name is used as the component id.
  • *
- *

+ * * * @throws RuntimeException if a field cannot be set due to access restrictions. * @throws IllegalArgumentException if a required component id cannot be found in the collected components. diff --git a/src/main/java/org/foxesworld/engine/gui/components/frame/FrameConstructor.java b/src/main/java/org/foxesworld/engine/gui/components/frame/FrameConstructor.java index 89f7edd..66a51c9 100644 --- a/src/main/java/org/foxesworld/engine/gui/components/frame/FrameConstructor.java +++ b/src/main/java/org/foxesworld/engine/gui/components/frame/FrameConstructor.java @@ -11,9 +11,19 @@ import java.awt.event.WindowEvent; import java.awt.event.WindowFocusListener; import java.awt.geom.RoundRectangle2D; +import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +/** + * Top-level application frame constructor. + * + *

+ * Responsible for creating and configuring the main application JFrame using {@link FrameAttributes} + * loaded from a template or provided directly. Provides focus tracking, shaped window support, + * seasonal background support (via {@link Panel}) and centralized logging for important lifecycle events. + *

+ */ public class FrameConstructor extends JFrame { private final Engine engine; private FocusStatusListener focusStatusListener; @@ -23,106 +33,248 @@ public class FrameConstructor extends JFrame { private final LanguageProvider LANG; private boolean hasFocus; + /** + * Creates and initializes the application frame. + * + *

+ * This constructor immediately triggers the frame build process using the template path + * configured in the engine's file properties. + *

+ * + * @param engine engine instance used for resources, localization and configuration; must not be {@code null}. + */ public FrameConstructor(Engine engine) { - Engine.getLOGGER().info("FrameConstructor initialization"); + Engine.LOGGER.info("FrameConstructor: initialization started"); this.engine = engine; this.LANG = engine.getLANG(); this.hasFocus = false; this.focusStatusListener = engine; - buildFrame("assets/frames/frame.json"); + // build frame from engine-provided template (may log errors if template missing/invalid) + buildFrame(engine.getFileProperties().getFrameTpl()); + Engine.LOGGER.info("FrameConstructor: initialization completed"); } + /** + * Loads FrameAttributes from the given resource path and builds the frame. + * + *

+ * This method logs the path being loaded and any errors encountered while reading or parsing the template. + *

+ * + * @param path resource path to the frame template (for example "assets/frame/frame.json") + */ private void buildFrame(String path) { + Engine.LOGGER.info("FrameConstructor: building AppFrame from '{}'", path); Gson gson = new Gson(); - FrameAttributes frameAttributes; - InputStreamReader reader = new InputStreamReader( - this.getClass().getClassLoader().getResourceAsStream(path), StandardCharsets.UTF_8); - frameAttributes = gson.fromJson(reader, FrameAttributes.class); - buildFrame(frameAttributes); + + try (InputStream in = this.getClass().getClassLoader().getResourceAsStream(path)) { + if (in == null) { + Engine.LOGGER.error("FrameConstructor: resource not found at path '{}'", path); + return; + } + try (InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { + FrameAttributes frameAttributes = gson.fromJson(reader, FrameAttributes.class); + if (frameAttributes == null) { + Engine.LOGGER.error("FrameConstructor: parsed FrameAttributes is null for path '{}'", path); + return; + } + Engine.LOGGER.debug("FrameConstructor: parsed FrameAttributes: width={}, height={}, resizable={}, undecorated={}, borderRadius={}", + frameAttributes.getWidth(), frameAttributes.getHeight(), + frameAttributes.isResizable(), frameAttributes.isUndecorated(), frameAttributes.getBorderRadius()); + buildFrame(frameAttributes); + Engine.LOGGER.info("FrameConstructor: frame successfully built from '{}'", path); + } + } catch (Exception ex) { + Engine.LOGGER.error("FrameConstructor: failed to build frame from '{}'", path, ex); + } } + /** + * Configures the Swing {@link JFrame} using provided {@link FrameAttributes}. + * + *

+ * Sets icon, title, size, decoration, window shape (if border radius provided), centers on screen, + * and installs the {@link Panel} root panel. Also sets the frame visible. + *

+ * + * @param frameAttributes attributes describing appearance and behavior of the frame. + */ public void buildFrame(FrameAttributes frameAttributes) { - Engine.getLOGGER().info("Building FrameConstructor..."); - if (!frameAttributes.getAppIcon().endsWith(".svg")) { - setIconImage(this.engine.getImageUtils().getLocalImage(frameAttributes.getAppIcon())); - } else { - setIconImage(new FlatSVGIcon(frameAttributes.getAppIcon(), 1).getImage()); - } - setTitle(LANG.getString(frameAttributes.getAppTitle())); - setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - setSize(frameAttributes.getWidth(), frameAttributes.getHeight()); - setResizable(frameAttributes.isResizable()); - setUndecorated(frameAttributes.isUndecorated()); - if (frameAttributes.getBorderRadius() != 0) { - this.setShape(new RoundRectangle2D.Double( - 0, 0, getWidth(), getHeight(), - frameAttributes.getBorderRadius(), - frameAttributes.getBorderRadius() - )); - } + try { + Engine.LOGGER.debug("FrameConstructor: applying FrameAttributes to JFrame"); + + if (frameAttributes.getAppIcon() != null && !frameAttributes.getAppIcon().isEmpty()) { + if (!frameAttributes.getAppIcon().endsWith(".svg")) { + Engine.LOGGER.debug("FrameConstructor: loading raster icon '{}'", frameAttributes.getAppIcon()); + setIconImage(this.engine.getImageUtils().getLocalImage(frameAttributes.getAppIcon())); + } else { + Engine.LOGGER.debug("FrameConstructor: loading SVG icon '{}'", frameAttributes.getAppIcon()); + setIconImage(new FlatSVGIcon(frameAttributes.getAppIcon(), 1).getImage()); + } + } else { + Engine.LOGGER.warn("FrameConstructor: no app icon configured in FrameAttributes"); + } + + setTitle(LANG.getString(frameAttributes.getAppTitle())); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setSize(frameAttributes.getWidth(), frameAttributes.getHeight()); + setResizable(frameAttributes.isResizable()); + setUndecorated(frameAttributes.isUndecorated()); + + if (frameAttributes.getBorderRadius() != 0) { + Engine.LOGGER.debug("FrameConstructor: applying window shape with border radius {}", frameAttributes.getBorderRadius()); + this.setShape(new RoundRectangle2D.Double( + 0, 0, getWidth(), getHeight(), + frameAttributes.getBorderRadius(), + frameAttributes.getBorderRadius() + )); + } + + screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + int x = (screenSize.width - getWidth()) / 2; + int y = (screenSize.height - getHeight()) / 2; + setLocation(x, y); + Engine.LOGGER.debug("FrameConstructor: window positioned at x={}, y={}, screenWidth={}, screenHeight={}", x, y, screenSize.width, screenSize.height); - screenSize = Toolkit.getDefaultToolkit().getScreenSize(); - int x = (screenSize.width - getWidth()) / 2; - int y = (screenSize.height - getHeight()) / 2; - setLocation(x, y); - panel = new Panel(this); - this.rootPanel = panel.setRootPanel(frameAttributes); - this.rootPanel.setName("rootPanel"); - setContentPane(this.rootPanel); - setVisible(true); + panel = new Panel(this); + this.rootPanel = panel.setRootPanel(frameAttributes); + this.rootPanel.setName("rootPanel"); + setContentPane(this.rootPanel); + + // ensure focus listeners are set up before showing the frame + setupFocusListeners(); + + setVisible(true); + Engine.LOGGER.info("FrameConstructor: frame is visible (title='{}', size={}x{})", getTitle(), getWidth(), getHeight()); + } catch (Exception ex) { + Engine.LOGGER.error("FrameConstructor: error while applying FrameAttributes", ex); + } } + /** + * Installs a WindowFocusListener that updates the configured {@link FocusStatusListener}. + * + *

+ * This method logs listener setup and will report focus gained/lost events to the listener. + *

+ */ private void setupFocusListeners() { + Engine.LOGGER.debug("FrameConstructor: setting up focus listeners"); addWindowFocusListener(new WindowFocusListener() { @Override public void windowGainedFocus(WindowEvent e) { + Engine.LOGGER.debug("FrameConstructor: window gained focus event"); onFrameFocusGained(); } @Override public void windowLostFocus(WindowEvent e) { + Engine.LOGGER.debug("FrameConstructor: window lost focus event"); onFrameFocusLost(); } }); } + /** + * Internal handler for frame gaining focus. + * Notifies the {@link FocusStatusListener} and logs the change. + */ private void onFrameFocusGained() { hasFocus = true; - focusStatusListener.updateFocus(hasFocus); + Engine.LOGGER.info("FrameConstructor: frame focus gained"); + if (focusStatusListener != null) { + try { + focusStatusListener.updateFocus(hasFocus); + } catch (Exception ex) { + Engine.LOGGER.error("FrameConstructor: error notifying focusStatusListener on focus gained", ex); + } + } else { + Engine.LOGGER.warn("FrameConstructor: focusStatusListener is null when focus gained"); + } } + /** + * Internal handler for frame losing focus. + * Notifies the {@link FocusStatusListener} and logs the change. + */ private void onFrameFocusLost() { hasFocus = false; - focusStatusListener.updateFocus(hasFocus); + Engine.LOGGER.info("FrameConstructor: frame focus lost"); + if (focusStatusListener != null) { + try { + focusStatusListener.updateFocus(hasFocus); + } catch (Exception ex) { + Engine.LOGGER.error("FrameConstructor: error notifying focusStatusListener on focus lost", ex); + } + } else { + Engine.LOGGER.warn("FrameConstructor: focusStatusListener is null when focus lost"); + } } + + /** + * Returns whether the frame currently has focus. + * + * @return {@code true} if frame has focus; {@code false} otherwise. + */ public boolean hasFocus() { return hasFocus; } + /** + * Returns the screen size where the frame was created. + * + * @return screen {@link Dimension}. + */ public Dimension getScreenSize() { return screenSize; } + /** + * Returns the root content panel that was installed on this frame. + * + * @return root {@link JPanel}. + */ public JPanel getRootPanel() { return this.rootPanel; } + /** + * Convenience accessor for the underlying {@link Engine} instance. + * + * @return engine. + */ public Engine getAppFrame() { return engine; } + /** + * Returns the internal {@link Panel} helper instance used by this frame. + * + * @return panel. + */ public Panel getPanel() { return panel; } + /** + * Changes frame size and logs the new dimensions. + * + * @param width new width in pixels. + * @param height new height in pixels. + */ public void setFrameSize(int width, int height) { + Engine.LOGGER.info("FrameConstructor: resizing frame to {}x{}", width, height); this.setSize(width, height); } + /** + * Sets the listener that will be updated on window focus changes. + * + * @param focusStatusListener listener to notify when frame focus changes. + */ public void setFocusStatusListener(FocusStatusListener focusStatusListener) { + Engine.LOGGER.debug("FrameConstructor: setting FocusStatusListener -> {}", focusStatusListener); this.focusStatusListener = focusStatusListener; setupFocusListeners(); } - - -} \ No newline at end of file +} diff --git a/src/main/java/org/foxesworld/engine/gui/components/panel/Panel.java b/src/main/java/org/foxesworld/engine/gui/components/panel/Panel.java index 8b51484..008fc7f 100644 --- a/src/main/java/org/foxesworld/engine/gui/components/panel/Panel.java +++ b/src/main/java/org/foxesworld/engine/gui/components/panel/Panel.java @@ -15,25 +15,64 @@ import static org.foxesworld.engine.utils.FontUtils.hexToColor; +/** + * A custom Swing {@link JPanel} implementation used by the GUI frame system. + * + *

+ * Responsibilities: + *

    + *
  • Act as a base panel that supports an alpha transparency field (0.0 — fully transparent, 1.0 — fully opaque).
  • + *
  • Provide a root panel that respects the {@code alpha} composite when painting and optionally + * draws a seasonal or configured background texture.
  • + *
  • Create configured group panels with rounded corners, borders, background images or textures, + * custom layouts, focus and drag listeners.
  • + *
+ * + * + *

+ * Painting operations apply the panel-wide {@code alpha} value so that children and backgrounds + * are rendered with the specified transparency. + *

+ */ public class Panel extends JPanel { private FrameAttributes frameAttributes; private JPanel groupPanel; private final FrameConstructor frameConstructor; private BufferedImage texture; - // Поле прозрачности: 1.0 - полностью непрозрачно, 0.0 - полностью прозрачно. + // Alpha transparency field: 1.0 = fully opaque, 0.0 = fully transparent. private float alpha = 1.0f; + /** + * Constructs a Panel backed by the provided {@link FrameConstructor}. + * + * @param frameConstructor frame constructor used to resolve resources (fonts, images, etc.). + */ public Panel(FrameConstructor frameConstructor) { this.frameConstructor = frameConstructor; - // Устанавливаем режим непрозрачности в зависимости от alpha + // Set opacity according to alpha this.setOpaque(alpha >= 1.0f); } - // Геттер прозрачности + /** + * Returns the current alpha (transparency) value. + * + * @return alpha in range [0.0, 1.0]. + */ public float getAlpha() { return alpha; } + + /** + * Sets the panel alpha (transparency). + * + *

+ * Values outside the [0.0, 1.0] range are clamped. Changing the alpha updates the panel's + * opaque flag and triggers a repaint. + *

+ * + * @param alpha new alpha value (0.0 — fully transparent, 1.0 — fully opaque). + */ public void setAlpha(float alpha) { if (alpha < 0f) { alpha = 0f; @@ -45,16 +84,28 @@ public void setAlpha(float alpha) { repaint(); } + /** + * Creates and returns the root panel for the frame using the provided {@link FrameAttributes}. + * + *

+ * The returned panel paints using the parent's {@link #alpha} composite. If a texture is set + * via {@link #setTexture(BufferedImage)} it will be drawn stretched to the panel size; + * otherwise a seasonally chosen background is loaded and darkened according to frame attributes. + *

+ * + * @param frameAttributes frame attributes describing background and seasonal images. + * @return a configured root {@link JPanel} instance (named "rootPanel"). + */ public JPanel setRootPanel(FrameAttributes frameAttributes) { this.frameAttributes = frameAttributes; - // Пример корневой панели, в которой используется прозрачность из поля alpha + // Example root panel that respects the alpha field JPanel rootPanel = new JPanel(null, true) { @Override protected void paintComponent(Graphics g) { Graphics2D g2d = (Graphics2D) g.create(); try { - // Применяем прозрачность, заданную в поле alpha + // Apply panel alpha g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, Panel.this.alpha)); super.paintComponent(g2d); if (texture != null) { @@ -74,15 +125,20 @@ protected void paintComponent(Graphics g) { } /** - * Устанавливает текстуру для панели. + * Sets a custom texture image for the panel. The texture will be used when painting the panel. * - * @param newTexture изображение текстуры. + * @param newTexture texture image to apply; may be {@code null} to revert to the seasonal background. */ public void setTexture(BufferedImage newTexture) { this.texture = newTexture; - repaint(); // Перерисовываем панель с новой текстурой + repaint(); // Repaint panel with the new texture } + /** + * Draws a darkened background based on the frame's seasonal background image. + * + * @param g graphics context to draw into. + */ private void drawDarkenedBackground(Graphics g) { BufferedImage backgroundImage = frameConstructor.getAppFrame() .getImageUtils() @@ -90,6 +146,11 @@ private void drawDarkenedBackground(Graphics g) { g.drawImage(applyDarkening(backgroundImage, hexToColor(frameAttributes.getBackgroundBlur())), 0, 0, null); } + /** + * Selects a seasonal background image path according to the current month. + * + * @return the configured seasonal image key from {@link FrameAttributes}. + */ private String getSeasonalBackground() { return switch (CurrentMonth.getCurrentMonth()) { case DECEMBER, JANUARY, FEBRUARY -> frameAttributes.getWinterImage(); @@ -99,6 +160,13 @@ private String getSeasonalBackground() { }; } + /** + * Returns a new image that is visually darkened by overlaying a semi-transparent color. + * + * @param image original source image. + * @param darkeningColor overlay color used to darken (alpha of overlay is set to 0.5f). + * @return a new {@link BufferedImage} with the darkening applied. + */ private BufferedImage applyDarkening(BufferedImage image, Color darkeningColor) { int width = image.getWidth(); int height = image.getHeight(); @@ -125,13 +193,32 @@ private BufferedImage applyDarkening(BufferedImage image, Color darkeningColor) return darkenedImage; } + /** + * Creates a configured group panel according to {@link PanelAttributes}. + * + *

+ * The created panel supports: + *

    + *
  • Optional background texture or image (darkened by provided color).
  • + *
  • Rounded corners controlled by {@code cornerRadius}.
  • + *
  • Custom border parsed from a comma-separated string.
  • + *
  • Focusable and dragger listener support.
  • + *
  • Bounds and layout application.
  • + *
+ * + * + * @param panelOptions configuration options for the panel. + * @param groupName name to assign to the created group panel. + * @param frameConstructor frame constructor used for resource lookup. + * @return configured {@link JPanel} instance. + */ public JPanel createGroupPanel(PanelAttributes panelOptions, String groupName, FrameConstructor frameConstructor) { groupPanel = new JPanel(null, panelOptions.isDoubleBuffered()) { @Override protected void paintComponent(Graphics g) { Graphics2D g2d = (Graphics2D) g.create(); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - // Применяем прозрачность панели + // Apply panel alpha g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, Panel.this.alpha)); super.paintComponent(g2d); @@ -198,6 +285,13 @@ protected void paintBorder(Graphics g) { return groupPanel; } + /** + * Resolves a layout manager from a string identifier. + * + * @param layout layout name (case-insensitive), e.g. "flow", "border", "grid", "gridbag", "box". + * @param panel panel required by some layout constructors (BoxLayout). + * @return {@link LayoutManager} instance or {@code null} if the layout type is invalid. + */ private LayoutManager getLayout(String layout, JPanel panel) { return switch (layout.toLowerCase()) { case "flow" -> new FlowLayout(); @@ -212,6 +306,17 @@ private LayoutManager getLayout(String layout, JPanel panel) { }; } + /** + * Parses a comma-separated border definition and applies a {@link MatteBorder} to the panel. + * + *

+ * Expected format: "top,left,bottom,right,#RRGGBB" where the first four values are integer thicknesses + * and the last value is a hex color string. + *

+ * + * @param groupPanel target panel to apply the border to. + * @param border comma-separated border descriptor. + */ private void createBorder(JPanel groupPanel, String border) { String[] borderData = border.split(","); int top = Integer.parseInt(borderData[0]); @@ -221,4 +326,4 @@ private void createBorder(JPanel groupPanel, String border) { Color borderColor = hexToColor(borderData[4]); groupPanel.setBorder(new MatteBorder(top, left, bottom, right, borderColor)); } -} +} \ No newline at end of file diff --git a/src/main/resources/buildInfo.json b/src/main/resources/buildInfo.json index e65933e..ce6d4f8 100644 --- a/src/main/resources/buildInfo.json +++ b/src/main/resources/buildInfo.json @@ -1,2 +1,2 @@ -{"engineVersion": "1.15.15", +{"engineVersion": "1.15.16", "engineBrand": " \n _____ _____ _____ \n /\\ \\ /\\ \\ /\\ \\ \n /::\\ \\ /::\\ \\ /::\\ \\ \n \\:::\\ \\ /::::\\ \\ \\:::\\ \\ \n \\:::\\ \\ /::::::\\ \\ \\:::\\ \\ \n \\:::\\ \\ /:::/\\:::\\ \\ \\:::\\ \\ \n \\:::\\ \\ /:::/__\\:::\\ \\ \\:::\\ \\ \n /::::\\ \\ /::::\\ \\:::\\ \\ /::::\\ \\ \n /::::::\\ \\ /::::::\\ \\:::\\ \\ /::::::\\ \\ \n /:::/\\:::\\ \\ /:::/\\:::\\ \\:::\\ \\ /:::/\\:::\\ \\ \n /:::/ \\:::\\____\\/:::/__\\:::\\ \\:::\\____\\ /:::/ \\:::\\____\\\n /:::/ \\::/ /\\:::\\ \\:::\\ \\::/ / /:::/ \\::/ /\n /:::/ / \\/____/ \\:::\\ \\:::\\ \\/____/ /:::/ / \\/____/ \n /:::/ / \\:::\\ \\:::\\ \\ /:::/ / \n/:::/ / \\:::\\ \\:::\\____\\ /:::/ / \n\\::/ / \\:::\\ \\::/ / \\::/ / \n \\/____/ \\:::\\ \\/____/ \\/____/ \n \\:::\\ \\ \n \\:::\\____\\ \n \\::/ / \n \\/____/ "} \ No newline at end of file