Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1b835a4
ScreenshotsPage
Calboot Mar 15, 2026
9ad2041
优化
Calboot Mar 16, 2026
2e8ba2b
清除
Calboot Mar 17, 2026
40540af
reveal
Calboot Mar 17, 2026
bb526d6
按日期选择
Calboot Mar 18, 2026
8655534
更新UI (未完成)
Calboot Mar 20, 2026
7f47893
update
Calboot Mar 21, 2026
9423d16
update
Calboot Mar 22, 2026
a3bbdf9
更多元数据+esc+双击
Calboot Mar 22, 2026
0f64be9
update
Calboot Mar 22, 2026
d1e6440
update
Calboot Mar 22, 2026
f646344
Merge branch 'main' into screenshot
Calboot Mar 22, 2026
ca8db04
背景
Calboot Mar 22, 2026
54042df
update ui and i18n
Calboot Mar 28, 2026
d4e5988
update file size unit
Calboot Mar 28, 2026
f1bd026
copy image
Calboot Mar 28, 2026
8d8c593
Update HMCL/src/main/resources/assets/lang/I18N_zh.properties
Calboot Mar 28, 2026
759387b
Merge remote-tracking branch 'upstream/main' into screenshot
Calboot Mar 28, 2026
af34c74
Merge remote-tracking branch 'upstream/main' into screenshot
CalbootOnceMore May 1, 2026
9129586
update
CalbootOnceMore May 1, 2026
e2f1475
Merge remote-tracking branch 'upstream/main' into screenshot
ToobLac May 15, 2026
4e7dfbd
Merge remote-tracking branch 'upstream/main' into screenshot
ToobLac May 20, 2026
5e47ce0
Merge remote-tracking branch 'upstream/main' into screenshot
ToobLac Jun 19, 2026
eb7b9bf
update
ToobLac Jun 19, 2026
365c218
update
ToobLac Jun 19, 2026
69cc357
Merge remote-tracking branch 'upstream/main' into screenshot
ToobLac Jun 28, 2026
9f8de1b
Merge remote-tracking branch 'upstream/main' into screenshot
ToobLac Jul 7, 2026
51a35a6
complete merging
ToobLac Jul 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions HMCL/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ val addOpens = listOf(
"javafx.controls/com.sun.javafx.scene.control",
"javafx.controls/com.sun.javafx.scene.control.behavior",
"javafx.graphics/com.sun.javafx.tk.quantum",
"javafx.controls/javafx.scene.control",
"javafx.controls/javafx.scene.control.skin",
"jdk.attach/sun.tools.attach",
)
Expand Down
714 changes: 714 additions & 0 deletions HMCL/src/main/java/com/jfoenix/skins/JFXDatePickerContent.java

Large diffs are not rendered by default.

178 changes: 178 additions & 0 deletions HMCL/src/main/java/com/jfoenix/skins/JFXDatePickerSkin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.jfoenix.skins;

import com.jfoenix.adapters.ReflectionHelper;
import com.jfoenix.adapters.skins.DatePickerSkin;
import com.jfoenix.controls.JFXDatePicker;
import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXDialog.DialogTransition;
import javafx.beans.value.ChangeListener;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.TextField;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;

import java.time.LocalDate;
import java.time.YearMonth;
import java.util.Stack;

import static org.jackhuang.hmcl.util.logging.Logger.LOG;

public class JFXDatePickerSkin extends DatePickerSkin {
public static final String PROPERTY_DATE_PICKER_CONTENTS = JFXDatePicker.class.getName() + ".dialog.contents";

private final JFXDatePicker jfxDatePicker;
private TextField displayNode;
private JFXDatePickerContent content;
private JFXDialog dialog;

public JFXDatePickerSkin(JFXDatePicker datePicker) {
super(datePicker);
this.jfxDatePicker = datePicker;
super.getPopupContent();

try {
Object expressionHelper = ReflectionHelper.getFieldContent(datePicker.focusedProperty().getClass().getSuperclass(), datePicker.focusedProperty(), "helper");
ChangeListener<Boolean>[] changeListeners = ReflectionHelper.getFieldContent(expressionHelper, "changeListeners");

int i = changeListeners.length - 1;
if (i >= 0) {
while (i >= 0 && changeListeners[i] == null) {
--i;
}
if (i >= 0) datePicker.focusedProperty().removeListener(changeListeners[i]);
}
} catch (NullPointerException e) {
LOG.warning("Cannot remove focusedProperty listener", e);
}

datePicker.focusedProperty().addListener((obj, oldVal, newVal) -> {
if (this.getEditor() != null && !newVal) {
this.setTextFromTextFieldIntoComboBoxValue2();
}

});
var arrowBox = new HBox(new RipplerContainer(SVG.EDIT_CALENDAR.createIcon(30)));
arrowBox.setAlignment(Pos.CENTER);
this.setArrow(arrowBox);
this.getArrowButton().getChildren().setAll(arrowBox);
this.getArrowButton().setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)));
this.getArrowButton().setPadding(new Insets(1.0F, 8.0F, 1.0F, 8.0F));
this.registerChangeListener2(datePicker.converterProperty(), "CONVERTER", this::updateDisplayNode2);
this.registerChangeListener2(datePicker.dayCellFactoryProperty(), "DAY_CELL_FACTORY", () -> {
this.updateDisplayNode2();
this.content = null;
this.setPopup2(null);
});
this.registerChangeListener2(datePicker.editorProperty(), "EDITOR", this::getEditableInputNode2);
this.registerChangeListener2(datePicker.showingProperty(), "SHOWING", () -> {
if (this.jfxDatePicker.isShowing()) {
if (this.content != null) {
LocalDate date = this.jfxDatePicker.getValue();
this.content.displayedYearMonthProperty().set(date != null ? YearMonth.from(date) : YearMonth.now());
this.content.updateValues();
}

this.show();
} else {
this.hide();
}

});
this.registerChangeListener2(datePicker.showWeekNumbersProperty(), "SHOW_WEEK_NUMBERS", () -> {
if (this.content != null) {
this.content.updateContentGrid();
this.content.updateWeekNumberDateCells();
}

});
this.registerChangeListener2(datePicker.valueProperty(), "VALUE", () -> {
this.updateDisplayNode2();
this.jfxDatePicker.fireEvent(new ActionEvent());
});
}

public JFXDatePickerContent getPopupContent() {
if (this.content == null) {
this.content = new JFXDatePickerContent(this.jfxDatePicker);
}

return this.content;
}

public void show() {
if (!((JFXDatePicker) this.getSkinnable()).isOverLay()) {
super.show();
}

if (this.content != null) {
this.content.init();
this.content.clearFocus();
}

if (((JFXDatePicker) this.getSkinnable()).isOverLay() && this.dialog == null) {
StackPane dialogParent = this.jfxDatePicker.getDialogParent();
if (dialogParent == null) {
dialogParent = (StackPane) this.getSkinnable().getScene().getRoot();
}

this.dialog = new JFXDialog(dialogParent, this.getPopupContent(), DialogTransition.CENTER, true) {
@Override
public void close() {
super.close();
Object o = getDialogContainer().getProperties().get(PROPERTY_DATE_PICKER_CONTENTS);
if (o instanceof Stack<?> stack) {
stack.pop();
}
}
};
this.getPopupContent().addEventHandler(DialogCloseEvent.CLOSE, e -> this.dialog.close());
getArrowButton().setOnMouseClicked((click) -> {
if (((JFXDatePicker) this.getSkinnable()).isOverLay()) {
StackPane parent = this.jfxDatePicker.getDialogParent();
if (parent == null) {
parent = (StackPane) this.getSkinnable().getScene().getRoot();
}
Stack<JFXDatePickerContent> contentStack = (Stack<JFXDatePickerContent>) parent.getProperties().computeIfAbsent(PROPERTY_DATE_PICKER_CONTENTS, k -> new Stack<>());
contentStack.push(this.getPopupContent());

this.getPopupContent().valueProperty.set(this.getSkinnable().getValue());
this.dialog.show(parent);
}
});
}

}

JFXDialog getDialog() {
return this.dialog;
}

public Node getDisplayNode() {
if (this.displayNode == null) {
this.displayNode = this.getEditableInputNode2();
this.displayNode.getStyleClass().add("date-picker-display-node");
this.updateDisplayNode2();
}

this.displayNode.setEditable(this.jfxDatePicker.isEditable());
return this.displayNode;
}

public void syncWithAutoUpdate() {
if (!this.getPopup2().isShowing() && this.jfxDatePicker.isShowing()) {
this.jfxDatePicker.hide();
}

}
}
95 changes: 92 additions & 3 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,7 @@
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.*;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle;
Expand All @@ -69,19 +67,22 @@
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.animation.Motion;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.IconedMenuItem;
import org.jackhuang.hmcl.ui.construct.MenuSeparator;
import org.jackhuang.hmcl.ui.construct.PopupMenu;
import org.jackhuang.hmcl.ui.image.ImageLoader;
import org.jackhuang.hmcl.ui.image.ImageUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
Expand All @@ -105,6 +106,8 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.time.Clock;
import java.time.LocalDate;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
Expand Down Expand Up @@ -1456,6 +1459,15 @@ public static void onSecondaryButtonClicked(Node node, Runnable action) {
});
}

public static void onDoubleClicked(Node node, Runnable action) {
node.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
if (e.getButton() == MouseButton.PRIMARY && e.getClickCount() == 2) {
action.run();
e.consume();
}
});
}

public static <N extends Parent> N prepareNode(N node) {
Scene dummyScene = new Scene(node);
StyleSheets.init(dummyScene);
Expand Down Expand Up @@ -1521,6 +1533,34 @@ public static void copyText(String text, @Nullable String toastMessage) {
}
}

public static void copyFiles(List<Path> paths) {
copyFiles(paths.stream().map(Path::toFile).toList(), i18n("message.copied"));
}

public static void copyFiles(List<File> files, @Nullable String toastMessage) {
ClipboardContent content = new ClipboardContent();
content.putFiles(files);
Clipboard.getSystemClipboard().setContent(content);

if (toastMessage != null && !Controllers.isStopped()) {
Controllers.showToast(toastMessage);
}
}

public static void copyImage(Image image) {
copyImage(image, i18n("message.copied"));
}

public static void copyImage(Image image, @Nullable String toastMessage) {
ClipboardContent content = new ClipboardContent();
content.putImage(image);
Clipboard.getSystemClipboard().setContent(content);

if (toastMessage != null && !Controllers.isStopped()) {
Controllers.showToast(toastMessage);
}
}

public static List<Node> parseSegment(String segment, Consumer<String> hyperlinkAction) {
if (segment.indexOf('<') < 0)
return Collections.singletonList(new Text(segment));
Expand Down Expand Up @@ -1713,4 +1753,53 @@ public static void useJFXContextMenu(TextInputControl control) {
e.consume();
});
}

public static void chooseDateRange(Consumer<@NotNull Pair<@Nullable LocalDate, @Nullable LocalDate>> onConfirm) {
Controllers.dialog(new DateRangeDialog(onConfirm));
}

private static class DateRangeDialog extends JFXDialogLayout {

private final JFXDatePicker fromPicker, toPicker;

public DateRangeDialog(Consumer<Pair<LocalDate, LocalDate>> onConfirm) {
super();

setHeading(new Label(i18n("button.select_date")));

GridPane body = new GridPane(4, 8);
{
Label fromLabel = new Label(i18n("button.select_date.from"));
this.fromPicker = new JFXDatePicker(LocalDate.now(Clock.systemDefaultZone()));
fromPicker.setOverLay(true);
fromPicker.setDialogParent(Controllers.getDecorator().getDecorator().getDrawerWrapper());
body.add(fromLabel, 0, 0);
body.add(fromPicker, 1, 0);
}
{
Label toLabel = new Label(i18n("button.select_date.to"));
this.toPicker = new JFXDatePicker(LocalDate.now(Clock.systemDefaultZone()));
toPicker.setOverLay(true);
toPicker.setDialogParent(Controllers.getDecorator().getDecorator().getDrawerWrapper());
body.add(toLabel, 0, 1);
body.add(toPicker, 1, 1);
}
setBody(body);

JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.getStyleClass().add("dialog-cancel");
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));

JFXButton okButton = new JFXButton(i18n("button.ok"));
okButton.getStyleClass().add("dialog-accept");
okButton.setOnAction(e -> {
if (onConfirm != null) onConfirm.accept(Pair.pair(fromPicker.getValue(), toPicker.getValue()));
fireEvent(new DialogCloseEvent());
});

setActions(cancelButton, okButton);
onEscPressed(this, cancelButton::fire);
}

}
}
Loading