feat(instance): 截图管理 (#6096)

Resolves #5535

Co-authored-by: Calboot <calboot39@outlook.com>
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
Co-authored-by: CalbootOnceMore <calboot43@outlook.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
This commit is contained in:
ToobLac
2026-10-11 21:47:13 +08:00
committed by GitHub
parent 6f406cede8
commit 5768cd446e
21 changed files with 1731 additions and 12 deletions
+1
View File
@@ -145,6 +145,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",
)
@@ -0,0 +1,731 @@
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.jfoenix.skins;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDatePicker;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.svg.SVGGlyph;
import com.jfoenix.transitions.CachedTransition;
import javafx.animation.*;
import javafx.animation.Animation.Status;
import javafx.beans.InvalidationListener;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.SnapshotParameters;
import javafx.scene.control.*;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.util.Duration;
import org.jackhuang.hmcl.setting.SettingsManager;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.SVGContainer;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.Year;
import java.time.YearMonth;
import java.time.chrono.ChronoLocalDate;
import java.time.chrono.Chronology;
import java.time.format.DateTimeFormatter;
import java.time.format.DecimalStyle;
import java.time.temporal.ChronoUnit;
import java.time.temporal.WeekFields;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.stream.IntStream;
public class JFXDatePickerContent extends VBox {
private static final String SPINNER_LABEL = "spinner-label";
private static final String ROBOTO = "Roboto";
protected JFXDatePicker datePicker;
private JFXButton backMonthButton;
private JFXButton forwardMonthButton;
private Label selectedDateLabel;
private Label selectedYearLabel;
private Label monthYearLabel;
protected GridPane contentGrid;
private final StackPane calendarPlaceHolder = new StackPane();
private final CachedTransition showTransition;
private final CachedTransition hideTransition;
private ParallelTransition tempImageTransition;
private final int daysPerWeek = 7;
private final List<DateCell> weekDaysCells = new ArrayList<>();
private final List<DateCell> weekNumberCells = new ArrayList<>();
protected List<DateCell> dayCells = new ArrayList<>();
private LocalDate[] dayCellDates;
private DateCell currentFocusedDayCell = null;
private final ListView<String> yearsListView = new JFXListView<>() {
{
this.getStyleClass().addAll("date-picker-list-view", "no-padding");
this.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
this.setCellFactory((listView) -> new ListCell<>() {
static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
final Label label = new Label();
final StackPane root;
final RipplerContainer ripplerContainer;
final WeakListenerHolder holder = new WeakListenerHolder();
{
label.getStyleClass().add(SPINNER_LABEL);
label.setMaxWidth(Double.MAX_VALUE);
label.textProperty().bind(itemProperty());
StackPane pane = new StackPane(label);
pane.setPadding(new Insets(7));
ripplerContainer = new RipplerContainer(pane);
root = new StackPane(ripplerContainer);
root.getStyleClass().add("data-picker-list-cell");
this.setGraphic(root);
var selectedListenerWeak = holder.weak((InvalidationListener) observable -> {
String newVal = JFXDatePickerContent.this.selectedYearLabel.getText();
if (newVal != null && getItem() != null && getItem().equals(newVal))
updateSelected(true);
});
JFXDatePickerContent.this.selectedYearLabel.textProperty().addListener(selectedListenerWeak);
itemProperty().addListener(selectedListenerWeak);
FXUtils.onChangeAndOperate(selectedProperty(), selected -> {
if (getListView() != null) {
if (selected) {
getListView().getSelectionModel().select(getIndex());
} else {
getListView().getSelectionModel().clearSelection(getIndex());
}
}
root.pseudoClassStateChanged(SELECTED, selected);
if (selected) {
int offset = Integer.parseInt(getItem()) - Integer.parseInt(JFXDatePickerContent.this.selectedYearLabel.getText());
JFXDatePickerContent.this.forward(offset, ChronoUnit.YEARS, false, false);
}
});
}
public void updateItem(String item, boolean empty) {
String oldItem = getItem();
boolean oldEmpty = isEmpty();
super.updateItem(item, empty);
setText(null);
if (Objects.equals(oldItem, item) && oldEmpty == empty) return;
ripplerContainer.releaseRippleImmediately();
if (!empty && item != null) {
setGraphic(root);
} else {
setGraphic(null);
}
}
});
}
};
final DateTimeFormatter fullFormatter = DateTimeFormatter.ofPattern("EEE, MMM yyyy");
final DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("MMMM");
final DateTimeFormatter yearFormatter = DateTimeFormatter.ofPattern("y");
final DateTimeFormatter weekNumberFormatter = DateTimeFormatter.ofPattern("w");
final DateTimeFormatter weekDayNameFormatter = DateTimeFormatter.ofPattern("ccc");
final DateTimeFormatter dayCellFormatter = DateTimeFormatter.ofPattern("d");
private final ObjectProperty<YearMonth> selectedYearMonth = new SimpleObjectProperty<>(this, "selectedYearMonth");
final ObjectProperty<LocalDate> valueProperty = new SimpleObjectProperty<>(this, "value");
JFXDatePickerContent(final DatePicker datePicker) {
this.datePicker = (JFXDatePicker) datePicker;
this.getStyleClass().add("date-picker-popup");
LocalDate date = datePicker.getValue();
this.valueProperty.set(date);
this.valueProperty.addListener((o, old, n) -> {
this.displayedYearMonthProperty().set(n != null ? YearMonth.from(n) : YearMonth.now());
this.updateValues();
});
this.selectedYearMonth.set(date != null ? YearMonth.from(date) : YearMonth.now());
this.selectedYearMonth.addListener((observable, oldValue, newValue) -> this.updateValues());
this.getChildren().add(this.createHeaderPane());
this.getChildren().add(new Separator());
this.contentGrid = new GridPane() {
protected double computePrefWidth(double height) {
int nCols = JFXDatePickerContent.this.daysPerWeek + (datePicker.isShowWeekNumbers() ? 1 : 0);
double leftSpace = this.snapSpaceX(this.getInsets().getLeft());
double rightSpace = this.snapSpaceX(this.getInsets().getRight());
double hgaps = this.snapSpaceX(this.getHgap()) * (double) (nCols - 1);
double contentWidth = super.computePrefWidth(height) - leftSpace - rightSpace - hgaps;
return this.snapSizeX(contentWidth / (double) nCols) * (double) nCols + leftSpace + rightSpace + hgaps;
}
protected void layoutChildren() {
if (this.getWidth() > (double) 0.0F && this.getHeight() > (double) 0.0F) {
super.layoutChildren();
}
}
};
this.contentGrid.setFocusTraversable(true);
this.contentGrid.getStyleClass().add("calendar-grid");
this.contentGrid.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)));
this.contentGrid.setPadding(new Insets(0.0F, 12.0F, 12.0F, 12.0F));
this.contentGrid.setVgap(4);
this.contentGrid.setHgap(4);
this.createWeekDaysCells();
this.createDayCells();
this.yearsListView.getItems().addAll(IntStream.rangeClosed(1900, Year.now().getValue() + 100).mapToObj(Integer::toString).toList());
StackPane main = new StackPane();
this.yearsListView.setVisible(false);
this.yearsListView.setOpacity(0.0F);
this.yearsListView.maxWidthProperty().bind(main.widthProperty());
this.yearsListView.maxHeightProperty().bind(main.heightProperty());
main.getChildren().setAll(this.contentGrid, this.yearsListView);
VBox contentHolder = new VBox();
contentHolder.getChildren().setAll(this.createCalendarMonthLabelPane(), main);
this.calendarPlaceHolder.getStyleClass().add("content");
this.calendarPlaceHolder.getChildren().setAll(contentHolder, this.createCalendarArrowsPane());
FXUtils.setOverflowHidden(this.calendarPlaceHolder);
this.getChildren().add(this.calendarPlaceHolder);
this.getChildren().add(this.createActions());
this.refresh();
this.addEventHandler(KeyEvent.ANY, (event) -> {
Node node = this.getScene().getFocusOwner();
if (node instanceof DateCell) {
this.currentFocusedDayCell = (DateCell) node;
}
switch (event.getCode()) {
case HOME -> {
this.init();
this.goToDate(LocalDate.now(), true);
event.consume();
}
case PAGE_UP -> {
if (!this.backMonthButton.isDisabled()) {
this.forward(-1, ChronoUnit.MONTHS, true, true);
}
event.consume();
}
case PAGE_DOWN -> {
if (!this.forwardMonthButton.isDisabled()) {
this.forward(1, ChronoUnit.MONTHS, true, true);
}
event.consume();
}
case ESCAPE -> {
fireEvent(new DialogCloseEvent());
event.consume();
}
default -> event.consume();
}
});
this.showTransition = new CachedTransition(this.yearsListView, new Timeline(new KeyFrame(Duration.millis(0.0F), new KeyValue(this.yearsListView.opacityProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 1, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(500.0F), new KeyValue(this.yearsListView.opacityProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 0, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(1000.0F), new KeyValue(this.yearsListView.opacityProperty(), 1, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 0, Interpolator.EASE_BOTH)))) {
{
this.setCycleDuration(Duration.millis(320.0F));
this.setDelay(Duration.seconds(0.0F));
}
protected void starting() {
super.starting();
JFXDatePickerContent.this.yearsListView.setVisible(true);
}
};
this.hideTransition = new CachedTransition(this.yearsListView, new Timeline(new KeyFrame(Duration.millis(0.0F), new KeyValue(this.yearsListView.opacityProperty(), 1, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 0, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(500.0F), new KeyValue(this.yearsListView.opacityProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 0, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(1000.0F), new KeyValue(this.yearsListView.opacityProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 1, Interpolator.EASE_BOTH)))) {
{
this.setCycleDuration(Duration.millis(320.0F));
this.setDelay(Duration.seconds(0.0F));
}
protected void stopping() {
super.stopping();
JFXDatePickerContent.this.yearsListView.setVisible(false);
}
};
}
ObjectProperty<YearMonth> displayedYearMonthProperty() {
return this.selectedYearMonth;
}
private void createWeekDaysCells() {
for (int i = 0; i < this.daysPerWeek; ++i) {
DateCell cell = new DateCell();
cell.getStyleClass().add("day-name-cell");
cell.setFont(Font.font(ROBOTO, FontWeight.BOLD, 12.0F));
cell.setAlignment(Pos.BASELINE_CENTER);
this.weekDaysCells.add(cell);
}
for (int i = 0; i < 6; ++i) {
DateCell cell = new DateCell();
cell.getStyleClass().add("week-number-cell");
cell.setFont(Font.font(ROBOTO, FontWeight.BOLD, 12.0F));
this.weekNumberCells.add(cell);
}
}
protected VBox createHeaderPane() {
Label title = new Label(I18n.i18n("button.select_date"));
title.getStyleClass().add("title");
title.setFont(Font.font(ROBOTO, FontWeight.NORMAL, 12.0F));
HBox titleContainer = new HBox(title);
titleContainer.setAlignment(Pos.CENTER_LEFT);
titleContainer.setPadding(new Insets(0, 0, 5, 0));
this.selectedYearLabel = new Label();
this.selectedYearLabel.getStyleClass().add(SPINNER_LABEL);
this.selectedYearLabel.setFont(Font.font(ROBOTO, FontWeight.BOLD, 14.0F));
HBox yearLabelContainer = new HBox();
yearLabelContainer.getStyleClass().add("spinner");
yearLabelContainer.getChildren().addAll(this.selectedYearLabel);
yearLabelContainer.setAlignment(Pos.CENTER_LEFT);
yearLabelContainer.setFillHeight(false);
yearLabelContainer.setOnMouseClicked((click) -> {
if (!this.yearsListView.isVisible()) {
int yearIndex = Integer.parseInt(this.selectedYearLabel.getText()) - 1900 - 2;
this.yearsListView.scrollTo(yearIndex >= 0 ? yearIndex : yearIndex + 2);
this.hideTransition.stop();
this.showTransition.play();
}
});
this.selectedDateLabel = new Label();
this.selectedDateLabel.getStyleClass().add(SPINNER_LABEL);
this.selectedDateLabel.setFont(Font.font(ROBOTO, FontWeight.BOLD, 32.0F));
HBox selectedDateContainer = new HBox();
selectedDateContainer.getStyleClass().add("spinner");
selectedDateContainer.getChildren().addAll(this.selectedDateLabel);
selectedDateContainer.setAlignment(Pos.CENTER_LEFT);
selectedDateContainer.setOnMouseClicked((click) -> {
if (this.yearsListView.isVisible()) {
this.showTransition.stop();
this.hideTransition.play();
}
});
VBox headerPanel = new VBox();
headerPanel.getStyleClass().add("month-year-pane");
headerPanel.setPadding(new Insets(12.0F, 24.0F, 12.0F, 24.0F));
headerPanel.getChildren().addAll(titleContainer, yearLabelContainer, selectedDateContainer);
return headerPanel;
}
protected BorderPane createCalendarArrowsPane() {
SVGGlyph leftChevron = new SVGGlyph(0, "CHEVRON_LEFT", "M 742,-37 90,614 Q 53,651 53,704.5 53,758 90,795 l 652,651 q 37,37 90.5,37 53.5,0 90.5,-37 l 75,-75 q 37,-37 37,-90.5 0,-53.5 -37,-90.5 L 512,704 998,219 q 37,-38 37,-91 0,-53 -37,-90 L 923,-37 Q 886,-74 832.5,-74 779,-74 742,-37 z", Color.GRAY);
SVGGlyph rightChevron = new SVGGlyph(0, "CHEVRON_RIGHT", "m 1099,704 q 0,-52 -37,-91 L 410,-38 q -37,-37 -90,-37 -53,0 -90,37 l -76,75 q -37,39 -37,91 0,53 37,90 l 486,486 -486,485 q -37,39 -37,91 0,53 37,90 l 76,75 q 36,38 90,38 54,0 90,-38 l 652,-651 q 37,-37 37,-90 z", Color.GRAY);
leftChevron.setSize(6.0F, 11.0F);
rightChevron.setSize(6.0F, 11.0F);
this.backMonthButton = new JFXButton();
this.backMonthButton.setMinSize(40.0F, 40.0F);
this.backMonthButton.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, new CornerRadii(40.0F), Insets.EMPTY)));
this.backMonthButton.getStyleClass().add("left-button");
this.backMonthButton.setGraphic(leftChevron);
this.backMonthButton.setRipplerFill(this.datePicker.getDefaultColor());
this.backMonthButton.setOnAction((t) -> this.forward(-1, ChronoUnit.MONTHS, false, true));
this.forwardMonthButton = new JFXButton();
this.forwardMonthButton.setMinSize(40.0F, 40.0F);
this.forwardMonthButton.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, new CornerRadii(40.0F), Insets.EMPTY)));
this.forwardMonthButton.getStyleClass().add("right-button");
this.forwardMonthButton.setGraphic(rightChevron);
this.forwardMonthButton.setRipplerFill(this.datePicker.getDefaultColor());
this.forwardMonthButton.setOnAction((t) -> this.forward(1, ChronoUnit.MONTHS, false, true));
BorderPane arrowsContainer = new BorderPane();
arrowsContainer.setLeft(this.backMonthButton);
arrowsContainer.setRight(this.forwardMonthButton);
arrowsContainer.setPadding(new Insets(4.0F, 12.0F, 2.0F, 12.0F));
arrowsContainer.setPickOnBounds(false);
return arrowsContainer;
}
protected BorderPane createCalendarMonthLabelPane() {
this.monthYearLabel = new Label();
this.monthYearLabel.getStyleClass().add(SPINNER_LABEL);
this.monthYearLabel.setFont(Font.font(ROBOTO, FontWeight.BOLD, 13.0F));
SVGContainer svgContainer = new SVGContainer(13);
HBox box = new HBox(monthYearLabel, svgContainer);
box.setSpacing(3);
box.setAlignment(Pos.CENTER);
FXUtils.onChangeAndOperate(this.yearsListView.visibleProperty(), b -> svgContainer.setIcon(b ? SVG.ARROW_DROP_UP : SVG.ARROW_DROP_DOWN));
FXUtils.onClicked(box, () -> {
if (!this.yearsListView.isVisible()) {
int yearIndex = Integer.parseInt(this.selectedYearLabel.getText()) - 1900 - 2;
this.yearsListView.scrollTo(yearIndex >= 0 ? yearIndex : yearIndex + 2);
this.hideTransition.stop();
this.showTransition.play();
} else {
this.showTransition.stop();
this.hideTransition.play();
}
});
BorderPane monthContainer = new BorderPane();
monthContainer.setMinHeight(50.0F);
monthContainer.setCenter(box);
monthContainer.setPadding(new Insets(2.0F, 12.0F, 2.0F, 12.0F));
return monthContainer;
}
protected HBox createActions() {
JFXButton cancelButton = new JFXButton(I18n.i18n("button.cancel"));
cancelButton.getStyleClass().add("dialog-cancel");
FXUtils.onClicked(cancelButton, () -> fireEvent(new DialogCloseEvent()));
JFXButton acceptButton = new JFXButton(I18n.i18n("button.ok"));
acceptButton.getStyleClass().add("dialog-accept");
FXUtils.onClicked(acceptButton, () -> {
datePicker.setValue(valueProperty.get());
fireEvent(new DialogCloseEvent());
});
HBox hBox = new HBox(cancelButton, acceptButton);
hBox.setPadding(new Insets(0, 5, 3, 0));
hBox.setSpacing(5);
hBox.setAlignment(Pos.CENTER_RIGHT);
return hBox;
}
void updateContentGrid() {
this.contentGrid.getColumnConstraints().clear();
this.contentGrid.getChildren().clear();
int colsNumber = this.daysPerWeek + (this.datePicker.isShowWeekNumbers() ? 1 : 0);
ColumnConstraints columnConstraints = new ColumnConstraints();
columnConstraints.setPercentWidth(100.0F);
for (int i = 0; i < colsNumber; ++i) {
this.contentGrid.getColumnConstraints().add(columnConstraints);
}
for (int i = 0; i < this.daysPerWeek; ++i) {
this.contentGrid.add(this.weekDaysCells.get(i), i + colsNumber - this.daysPerWeek, 1);
}
if (this.datePicker.isShowWeekNumbers()) {
for (int i = 0; i < 6; ++i) {
this.contentGrid.add(this.weekNumberCells.get(i), 0, i + 2);
}
}
for (int row = 0; row < 6; ++row) {
for (int col = 0; col < this.daysPerWeek; ++col) {
this.contentGrid.add(this.dayCells.get(row * this.daysPerWeek + col), col + colsNumber - this.daysPerWeek, row + 2);
}
}
}
private void refresh() {
this.updateDayNameCells();
this.updateValues();
}
private void updateDayNameCells() {
int weekFirstDay = WeekFields.of(this.getLocale()).getFirstDayOfWeek().getValue();
LocalDate date = LocalDate.of(2009, 7, 12 + weekFirstDay);
for (int i = 0; i < this.daysPerWeek; ++i) {
String name = this.weekDayNameFormatter.withLocale(this.getLocale()).format(date.plusDays(i));
if (!LocaleUtils.isChinese(this.weekNumberFormatter.getLocale())) {
name = name.substring(0, 1).toUpperCase(Locale.ROOT);
}
this.weekDaysCells.get(i).setText(name);
}
}
void updateValues() {
this.updateWeekNumberDateCells();
this.updateDayCells();
this.updateMonthYearPane();
}
void updateWeekNumberDateCells() {
if (this.datePicker.isShowWeekNumbers()) {
Locale locale = this.getLocale();
LocalDate firstDayOfMonth = this.selectedYearMonth.get().atDay(1);
for (int i = 0; i < 6; ++i) {
LocalDate date = firstDayOfMonth.plusWeeks(i);
String weekNumber = this.weekNumberFormatter.withLocale(locale).withDecimalStyle(DecimalStyle.of(locale)).format(date);
this.weekNumberCells.get(i).setText(weekNumber);
}
}
}
private void updateDayCells() {
Locale locale = this.getLocale();
Chronology chrono = this.getPrimaryChronology();
int firstDayOfWeek = WeekFields.of(this.getLocale()).getFirstDayOfWeek().getValue();
int firstOfMonthIndex = this.selectedYearMonth.get().atDay(1).getDayOfWeek().getValue() - firstDayOfWeek;
firstOfMonthIndex += firstOfMonthIndex < 0 ? this.daysPerWeek : 0;
YearMonth currentYearMonth = this.selectedYearMonth.get();
int daysInCurMonth = -1;
for (int i = 0; i < 6 * this.daysPerWeek; ++i) {
DateCell dayCell = this.dayCells.get(i);
try {
if (daysInCurMonth == -1) {
daysInCurMonth = currentYearMonth.lengthOfMonth();
}
int dayIndex = i - firstOfMonthIndex + 1;
LocalDate date;
if (dayIndex <= 0) {
YearMonth previous = currentYearMonth.minusMonths(1);
date = previous.atDay(previous.lengthOfMonth() + dayIndex);
} else if (dayIndex > daysInCurMonth) {
YearMonth next = currentYearMonth.plusMonths(1);
date = next.atDay(dayIndex - daysInCurMonth);
} else {
date = currentYearMonth.atDay(dayIndex);
}
this.dayCellDates[i] = date;
if (date.equals(LocalDate.now())) {
dayCell.getStyleClass().add("today");
} else {
dayCell.getStyleClass().removeAll("today");
}
if (date.equals(valueProperty.get())) {
dayCell.getStyleClass().add("selected");
this.currentFocusedDayCell = dayCell;
} else {
dayCell.getStyleClass().removeAll("selected");
}
ChronoLocalDate cDate = chrono.date(date);
String cellText = this.dayCellFormatter.withLocale(locale).withChronology(chrono).withDecimalStyle(DecimalStyle.of(locale)).format(cDate);
dayCell.setText(cellText);
if (i < firstOfMonthIndex) {
dayCell.getStyleClass().add("previous-month");
dayCell.getStyleClass().removeAll("next-month");
dayCell.setDisable(true);
} else if (i >= firstOfMonthIndex + daysInCurMonth) {
dayCell.getStyleClass().add("next-month");
dayCell.getStyleClass().removeAll("previous-month");
dayCell.setDisable(true);
} else {
dayCell.getStyleClass().removeAll("previous-month", "next-month");
dayCell.setDisable(false);
}
dayCell.updateItem(date, false);
} catch (DateTimeException var13) {
dayCell.setText("");
dayCell.setDisable(true);
}
}
}
protected void updateMonthYearPane() {
YearMonth yearMonth = this.selectedYearMonth.get();
this.selectedDateLabel.setText(fullFormatter.withLocale(this.getLocale()).format(Objects.requireNonNullElseGet(valueProperty.get(), LocalDate::now)));
this.selectedYearLabel.setText(this.formatYear(yearMonth));
this.monthYearLabel.setText(this.formatMonth(yearMonth) + " " + this.formatYear(yearMonth));
Chronology chrono = this.datePicker.getChronology();
LocalDate firstDayOfMonth = yearMonth.atDay(1);
this.backMonthButton.setDisable(!this.isValidDate(chrono, firstDayOfMonth, -1, ChronoUnit.DAYS));
this.forwardMonthButton.setDisable(!this.isValidDate(chrono, firstDayOfMonth, 1, ChronoUnit.MONTHS));
}
private String formatMonth(YearMonth yearMonth) {
try {
Chronology chrono = this.getPrimaryChronology();
ChronoLocalDate cDate = chrono.date(yearMonth.atDay(1));
return this.monthFormatter.withLocale(this.getLocale()).withChronology(chrono).format(cDate);
} catch (DateTimeException var4) {
return "";
}
}
private String formatYear(YearMonth yearMonth) {
try {
Chronology chrono = this.getPrimaryChronology();
ChronoLocalDate cDate = chrono.date(yearMonth.atDay(1));
return this.yearFormatter.withLocale(this.getLocale()).withChronology(chrono).withDecimalStyle(DecimalStyle.of(this.getLocale())).format(cDate);
} catch (DateTimeException var4) {
return "";
}
}
protected LocalDate dayCellDate(DateCell dateCell) {
assert this.dayCellDates != null;
return this.dayCellDates[this.dayCells.indexOf(dateCell)];
}
protected void forward(int offset, ChronoUnit unit, boolean focusDayCell, boolean withAnimation) {
if (withAnimation && (this.tempImageTransition == null || this.tempImageTransition.getStatus() == Status.STOPPED)) {
Pane monthContent = (Pane) this.calendarPlaceHolder.getChildren().get(0);
this.getParent().setManaged(false);
SnapshotParameters snapShotparams = new SnapshotParameters();
snapShotparams.setFill(Color.TRANSPARENT);
WritableImage temp = monthContent.snapshot(snapShotparams, new WritableImage((int) monthContent.getWidth(), (int) monthContent.getHeight()));
ImageView tempImage = new ImageView(temp);
this.calendarPlaceHolder.getChildren().add(this.calendarPlaceHolder.getChildren().size() - 2, tempImage);
TranslateTransition imageTransition = new TranslateTransition(Duration.millis(160.0F), tempImage);
imageTransition.setToX((double) (-offset) * this.calendarPlaceHolder.getWidth());
imageTransition.setOnFinished((finish) -> this.calendarPlaceHolder.getChildren().remove(tempImage));
monthContent.setTranslateX((double) offset * this.calendarPlaceHolder.getWidth());
TranslateTransition contentTransition = new TranslateTransition(Duration.millis(160.0F), monthContent);
contentTransition.setToX(0.0F);
this.tempImageTransition = new ParallelTransition(imageTransition, contentTransition);
this.tempImageTransition.setOnFinished((finish) -> {
this.calendarPlaceHolder.getChildren().remove(tempImage);
this.getParent().setManaged(true);
});
this.tempImageTransition.play();
}
YearMonth yearMonth = this.selectedYearMonth.get();
DateCell dateCell = this.currentFocusedDayCell;
if (dateCell == null || this.dayCellDate(dateCell).getMonth() != yearMonth.getMonth()) {
dateCell = this.findDayCellOfDate(yearMonth.atDay(1));
}
this.goToDayCell(dateCell, offset, unit, focusDayCell);
}
private void goToDayCell(DateCell dateCell, int offset, ChronoUnit unit, boolean focusDayCell) {
this.goToDate(this.dayCellDate(dateCell).plus(offset, unit), focusDayCell);
}
private void goToDate(LocalDate date, boolean focusDayCell) {
if (this.isValidDate(this.datePicker.getChronology(), date)) {
this.valueProperty.set(date);
this.selectedYearMonth.set(YearMonth.from(date));
if (focusDayCell) {
this.findDayCellOfDate(date).requestFocus();
}
}
}
private void selectDayCell(DateCell dateCell) {
this.valueProperty.set(this.dayCellDate(dateCell));
}
private DateCell findDayCellOfDate(LocalDate date) {
for (int i = 0; i < this.dayCellDates.length; ++i) {
if (date.equals(this.dayCellDates[i])) {
return this.dayCells.get(i);
}
}
return this.dayCells.get(this.dayCells.size() / 2 + 1);
}
void init() {
this.contentGrid.setOpacity(1.0F);
this.yearsListView.setOpacity(0.0F);
this.yearsListView.setVisible(false);
}
void clearFocus() {
LocalDate focusDate = this.valueProperty.get();
if (focusDate == null) {
focusDate = LocalDate.now();
}
if (YearMonth.from(focusDate).equals(this.selectedYearMonth.get())) {
this.goToDate(focusDate, true);
}
}
protected void createDayCells() {
for (int row = 0; row < 6; ++row) {
for (int col = 0; col < this.daysPerWeek; ++col) {
DateCell dayCell = this.createDayCell();
dayCell.addEventHandler(MouseEvent.MOUSE_CLICKED, (click) -> {
if (click.getButton() == MouseButton.PRIMARY) {
DateCell selectedDayCell = (DateCell) click.getSource();
this.selectDayCell(selectedDayCell);
}
});
dayCell.setOnMouseEntered((event) -> dayCell.getStyleClass().add("hovered"));
dayCell.setOnMouseExited((event) -> dayCell.getStyleClass().removeAll("hovered"));
dayCell.setAlignment(Pos.BASELINE_CENTER);
dayCell.setBorder(new Border(new BorderStroke(Color.TRANSPARENT, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(5.0F))));
dayCell.setFont(Font.font(ROBOTO, FontWeight.BOLD, 12.0F));
this.dayCells.add(dayCell);
}
}
this.dayCellDates = new LocalDate[6 * this.daysPerWeek];
this.updateContentGrid();
}
private DateCell createDayCell() {
DateCell dayCell = null;
if (this.datePicker.getDayCellFactory() != null) {
dayCell = this.datePicker.getDayCellFactory().call(this.datePicker);
}
if (dayCell == null) {
dayCell = new DateCell();
}
dayCell.getStyleClass().add("day-cell");
dayCell.setPrefSize(36.0F, 36.0F);
dayCell.setDisable(false);
dayCell.setStyle(null);
dayCell.setGraphic(null);
dayCell.setTooltip(null);
return dayCell;
}
protected Chronology getPrimaryChronology() {
return this.datePicker.getChronology();
}
protected Locale getLocale() {
return SettingsManager.settings().languageProperty().get().getLocale();
}
protected boolean isValidDate(Chronology chrono, LocalDate date, int offset, ChronoUnit unit) {
return date != null && this.isValidDate(chrono, date.plus(offset, unit));
}
protected boolean isValidDate(Chronology chrono, LocalDate date) {
try {
if (date != null) {
chrono.date(date);
}
return true;
} catch (DateTimeException var4) {
return false;
}
}
}
@@ -0,0 +1,181 @@
//
// 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;
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());
});
}
@Override
public JFXDatePickerContent getPopupContent() {
if (this.content == null) {
this.content = new JFXDatePickerContent(this.jfxDatePicker);
}
return this.content;
}
@Override
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();
}
jfxDatePicker.hide();
}
};
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;
}
@Override
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();
}
}
}
@@ -50,9 +50,7 @@ import javafx.scene.control.skin.VirtualFlow;
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;
@@ -69,6 +67,7 @@ import org.jackhuang.hmcl.task.Schedulers;
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;
@@ -76,6 +75,7 @@ import org.jackhuang.hmcl.ui.image.ImageLoader;
import org.jackhuang.hmcl.ui.image.ImageUtils;
import org.jackhuang.hmcl.util.FXThread;
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;
@@ -83,6 +83,7 @@ import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.javafx.Subscription;
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.jsoup.Jsoup;
import org.w3c.dom.Document;
@@ -107,6 +108,7 @@ import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.time.LocalDate;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.*;
@@ -1195,6 +1197,15 @@ public final class FXUtils {
});
}
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 void addMacOSCloseWindowHandler(Window window, @Nullable Supplier<Boolean> restriction) {
window.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
@@ -1272,6 +1283,34 @@ public final class FXUtils {
}
}
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));
@@ -1522,4 +1561,52 @@ public final class FXUtils {
}
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());
fromPicker.setOverLay(true);
fromPicker.setDialogParent(Controllers.getDecorator().getDialogContainer());
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());
toPicker.setOverLay(true);
toPicker.setDialogParent(Controllers.getDecorator().getDialogContainer());
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);
}
}
}
@@ -24,7 +24,7 @@ import javafx.scene.shape.SVGPath;
/// All vector icons used in the launcher.
///
/// Unless otherwise stated,
/// these icons are from <a href="https://fonts.google.com/icons?icon.size=24&icon.color=%235f6368&icon.query=list&icon.set=Material+Symbols">Material Symbols</a>,
/// these icons are from <a href="https://fonts.google.com/icons?icon.size=24&icon.color=%235f6368&icon.set=Material+Symbols">Material Symbols</a>,
/// with a style of outlined, a weight of 400, a grade of 0, and an optical size of 24 px.
/// The view boxes of all icons are normalized to `0 0 24 24`.
public enum SVG {
@@ -49,6 +49,7 @@ public enum SVG {
CONTENT_COPY("M9 18Q8.175 18 7.5875 17.4125T7 16V4Q7 3.175 7.5875 2.5875T9 2H18Q18.825 2 19.4125 2.5875T20 4V16Q20 16.825 19.4125 17.4125T18 18H9ZM9 16H18V4H9V16ZM5 22Q4.175 22 3.5875 21.4125T3 20V6H5V20H16V22H5ZM9 16V4 16Z"),
CONTENT_PASTE("M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h4.175q.275-.875 1.075-1.437T12 1q1 0 1.788.563T14.85 3H19q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm0-2h14V5h-2v3H7V5H5zm7-14q.425 0 .713-.288T13 4t-.288-.712T12 3t-.712.288T11 4t.288.713T12 5"),
CREATE_NEW_FOLDER("M14 16h2V14h2V12H16V10H14v2H12v2h2v2ZM4 20q-.825 0-1.4125-.5875T2 18V6q0-.825.5875-1.4125T4 4h6l2 2h8q.825 0 1.4125.5875T22 8V18q0 .825-.5875 1.4125T20 20H4Zm0-2H20V8H11.175l-2-2H4V18ZV6 18Z"),
DATE_RANGE("M7.2875 13.7125Q7 13.425 7 13t0.2875 -0.7125Q7.575 12 8 12t0.7125 0.2875Q9 12.575 9 13t-0.2875 0.7125Q8.425 14 8 14t-0.7125 -0.2875zm4 0Q11 13.425 11 13t0.2875 -0.7125Q11.575 12 12 12t0.7125 0.2875Q13 12.575 13 13t-0.2875 0.7125Q12.425 14 12 14t-0.7125 -0.2875zm4 0Q15 13.425 15 13t0.2875 -0.7125Q15.575 12 16 12t0.7125 0.2875Q17 12.575 17 13t-0.2875 0.7125Q16.425 14 16 14t-0.7125 -0.2875zM5 22q-0.825 0 -1.4125 -0.5875T3 20l0 -14q0 -0.825 0.5875 -1.4125T5 4l1 0l0 -2l2 0l0 2l8 0l0 -2l2 0l0 2l1 0q0.825 0 1.4125 0.5875T21 6l0 14q0 0.825 -0.5875 1.4125T19 22L5 22zm0 -2l14 0l0 -10L5 10l0 10zm0 -12l14 0l0 -2L5 6l0 2zm0 0l0 -2 0 2z"),
DELETE("M7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM9 17H11V8H9V17ZM13 17H15V8H13V17ZM7 6V19 6Z"),
DELETE_FOREVER("M9.4 16.5 12 13.9 14.6 16.5 16 15.1 13.4 12.5 16 9.9 14.6 8.5 12 11.1 9.4 8.5 8 9.9 10.6 12.5 8 15.1 9.4 16.5ZM7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM7 6V19 6Z"),
DEPLOYED_CODE("M11 19.425V12.575L5 9.1V15.95L11 19.425ZM13 19.425 19 15.95V9.1L13 12.575V19.425ZM12 10.85 17.925 7.425 12 4 6.075 7.425 12 10.85ZM4 17.7Q3.525 17.425 3.2625 16.975T3 15.975V8.025Q3 7.475 3.2625 7.025T4 6.3L11 2.275Q11.475 2 12 2T13 2.275L20 6.3Q20.475 6.575 20.7375 7.025T21 8.025V15.975Q21 16.525 20.7375 16.975T20 17.7L13 21.725Q12.525 22 12 22T11 21.725L4 17.7ZM12 12Z"),
@@ -56,6 +57,7 @@ public enum SVG {
DOWNLOAD("M12 16 7 11 8.4 9.55 11 12.15V4H13V12.15L15.6 9.55 17 11 12 16ZM6 20Q5.175 20 4.5875 19.4125T4 18V15H6V18H18V15H20V18Q20 18.825 19.4125 19.4125T18 20H6Z"),
DRESSER("M4 21V5Q4 4.175 4.5875 3.5875T6 3H18Q18.825 3 19.4125 3.5875T20 5V21H18V19H6V21H4ZM6 11H11V5H6V11ZM13 7H18V5H13V7ZM13 11H18V9H13V11ZM10 16H14V14H10V16ZM6 13V17H18V13H6ZM6 13V17 13Z"),
EDIT("M5 19H6.425L16.2 9.225 14.775 7.8 5 17.575V19ZM3 21V16.75L16.2 3.575Q16.5 3.3 16.8625 3.15T17.625 3Q18.025 3 18.4 3.15T19.05 3.6L20.425 5Q20.725 5.275 20.8625 5.65T21 6.4Q21 6.8 20.8625 7.1625T20.425 7.825L7.25 21H3ZM19 6.4 17.6 5 19 6.4ZM15.475 8.525 14.775 7.8 16.2 9.225 15.475 8.525Z"),
EDIT_CALENDAR("M5 22q-0.825 0 -1.4125 -0.5875T3 20l0 -14q0 -0.825 0.5875 -1.4125T5 4l1 0l0 -2l2 0l0 2l8 0l0 -2l2 0l0 2l1 0q0.825 0 1.4125 0.5875T21 6l0 5l-2 0l0 -1L5 10l0 10l7 0l0 2L5 22zm0 -14l14 0l0 -2L5 6l0 2zm0 0l0 -2 0 2zM14 22l0 -3.075l5.525 -5.5q0.225 -0.225 0.5 -0.325t0.55 -0.1q0.3 0 0.575 0.1125t0.5 0.3375l0.925 0.925q0.2 0.225 0.3125 0.5t0.1125 0.55q0 0.275 -0.1 0.5625T22.575 16.5L17.075 22L14 22zm7.5 -6.575 -0.925 -0.925 0.925 0.925zM15.5 20.5l0.95 0l3.025 -3.05 -0.45 -0.475 -0.475 -0.45 -3.05 3.025l0 0.95zm3.525 -3.525 -0.475 -0.45 0.925 0.925 -0.45 -0.475z"),
ERROR("M12 17Q12.425 17 12.7125 16.7125T13 16Q13 15.575 12.7125 15.2875T12 15Q11.575 15 11.2875 15.2875T11 16Q11 16.425 11.2875 16.7125T12 17ZM11 13H13V7H11V13ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
EXPLORE("M12 12Zm0 8q-3.325 0-5.6625-2.3375T4 12Q4 8.675 6.3375 6.3375T12 4q3.325-0 5.6625 2.3375T20 12q0 3.325-2.3375 5.6625T12 20Zm0 2q2.075-0 3.9-.7875t3.175-2.1375q1.35-1.35 2.1375-3.175T22 12q-0-2.075-.7875-3.9T19.075 4.925q-1.35-1.35-3.175-2.1375T12 2q-2.075 0-3.9.7875T4.925 4.925Q3.575 6.275 2.7875 8.1T2 12q0 2.075.7875 3.9T4.925 19.075q1.35 1.35 3.175 2.1375T12 22Zm0-8.5q.625 0 1.0625-.4375T13.5 12t-.4375-1.0625T12 10.5t-1.0625.4375T10.5 12t.4375 1.0625T12 13.5Zm-4.5 3 2-7 7-2-2 7-7 2Z"),
EXTENSION("M8.8 21H5Q4.175 21 3.5875 20.4125T3 19V15.2Q4.2 15.2 5.1 14.4375T6 12.5Q6 11.325 5.1 10.5625T3 9.8V6Q3 5.175 3.5875 4.5875T5 4H9Q9 2.95 9.725 2.225T11.5 1.5Q12.55 1.5 13.275 2.225T14 4H18Q18.825 4 19.4125 4.5875T20 6V10Q21.05 10 21.775 10.725T22.5 12.5Q22.5 13.55 21.775 14.275T20 15V19Q20 19.825 19.4125 20.4125T18 21H14.2Q14.2 19.75 13.4125 18.875T11.5 18Q10.375 18 9.5875 18.875T8.8 21ZM5 19H7.125Q7.725 17.35 9.05 16.675T11.5 16Q12.625 16 13.95 16.675T15.875 19H18V13H20Q20.2 13 20.35 12.85T20.5 12.5Q20.5 12.3 20.35 12.15T20 12H18V6H12V4Q12 3.8 11.85 3.65T11.5 3.5Q11.3 3.5 11.15 3.65T11 4V6H5V8.2Q6.35 8.7 7.175 9.875T8 12.5Q8 13.925 7.175 15.1T5 16.8V19ZM11.5 12.5Z"),
@@ -109,6 +111,7 @@ public enum SVG {
SCHEMA("M4 23V17H6.5V15H4V9H6.5V7H4V1h7V7H8.5V9H11v2h3V9h7v6H14V13H11v2H8.5v2H11v6H4Zm2-2H9V19H6v2Zm0-8H9V11H6v2Zm10 0h3V11H16v2ZM6 5H9V3H6V5ZM7.5 4Zm0 8Zm10 0Zm-10 8Z"),
SCHEMA_FILL("M4 23V17H6.5V15H4V9H6.5V7H4V1h7V7H8.5V9H11v2h3V9h7v6H14V13H11v2H8.5v2H11v6H4Z"),
SCREENSHOT_MONITOR("M15 16H19V12H17.5V14.5H15V16ZM5 10H6.5V7.5H9V6H5V10ZM8 21V19H4Q3.175 19 2.5875 18.4125T2 17V5Q2 4.175 2.5875 3.5875T4 3H20Q20.825 3 21.4125 3.5875T22 5V17Q22 17.825 21.4125 18.4125T20 19H16V21H8ZM4 17H20V5H4V17ZM4 17V5 17Z"),
SCREENSHOT_MONITOR_FILL("M15 16H19V12H17.5V14.5H15V16ZM5 10H6.5V7.5H9V6H5V10ZM8 21V19H4Q3.175 19 2.5875 18.4125T2 17V5Q2 4.175 2.5875 3.5875T4 3H20Q20.825 3 21.4125 3.5875T22 5V17Q22 17.825 21.4125 18.4125T20 19H16V21H8Z"),
SCRIPT("M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z"), // Not Material
SEARCH("M19.6 21 13.3 14.7Q12.55 15.3 11.575 15.65T9.5 16Q6.775 16 4.8875 14.1125T3 9.5Q3 6.775 4.8875 4.8875T9.5 3Q12.225 3 14.1125 4.8875T16 9.5Q16 10.6 15.65 11.575T14.7 13.3L21 19.6 19.6 21ZM9.5 14Q11.375 14 12.6875 12.6875T14 9.5Q14 7.625 12.6875 6.3125T9.5 5Q7.625 5 6.3125 6.3125T5 9.5Q5 11.375 6.3125 12.6875T9.5 14Z"),
SELECT_ALL("M7 17V7H17V17H7ZM9 15H15V9H9V15ZM5 19V21Q4.175 21 3.5875 20.4125T3 19H5ZM3 17V15H5V17H3ZM3 13V11H5V13H3ZM3 9V7H5V9H3ZM5 5H3Q3 4.175 3.5875 3.5875T5 3V5ZM7 21V19H9V21H7ZM7 5V3H9V5H7ZM11 21V19H13V21H11ZM11 5V3H13V5H11ZM15 21V19H17V21H15ZM15 5V3H17V5H15ZM19 21V19H21Q21 19.825 20.4125 20.4125T19 21ZM19 17V15H21V17H19ZM19 13V11H21V13H19ZM19 9V7H21V9H19ZM19 5V3Q19.825 3 20.4125 3.5875T21 5H19Z"),
@@ -64,6 +64,33 @@ public class ImageContainer extends StackPane {
this.getChildren().setAll(imageView);
}
public ImageContainer(Image image, double maxWidth, double maxHeight) {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
double width = maxWidth;
double height = width / image.getWidth() * image.getHeight();
if (height > maxHeight) {
height = maxHeight;
width = height / image.getHeight() * image.getWidth();
}
FXUtils.setLimitWidth(this, width);
FXUtils.setLimitHeight(this, height);
imageView.setPreserveRatio(true);
FXUtils.limitSize(imageView, width, height);
StackPane.setAlignment(imageView, Pos.CENTER);
clip.setWidth(width);
clip.setHeight(height);
updateCornerRadius(getCornerRadius());
this.setClip(clip);
setImage(image);
this.getChildren().setAll(imageView);
}
private void updateCornerRadius(double radius) {
clip.setArcWidth(radius);
clip.setArcHeight(radius);
@@ -19,6 +19,8 @@ package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXSnackbar;
import com.jfoenix.controls.JFXSnackbarLayout;
import com.jfoenix.skins.JFXDatePickerContent;
import com.jfoenix.skins.JFXDatePickerSkin;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
@@ -81,6 +83,7 @@ import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import java.util.Stack;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
@@ -1130,13 +1133,23 @@ public final class Decorator {
}
Node newTarget;
@Nullable JFXDialogPane currentDialogPane =
(JFXDialogPane) getDialogContainer().getProperties()
.get(DialogUtils.PROPERTY_DIALOG_PANE_INSTANCE);
if (currentDialogPane != null && currentDialogPane.peek().isPresent()) {
newTarget = currentDialogPane.peek().orElseThrow();
} else {
newTarget = navigator.getCurrentPage();
{
//noinspection unchecked
@Nullable Stack<JFXDatePickerContent> stack =
(Stack<JFXDatePickerContent>) getDialogContainer().getProperties()
.get(JFXDatePickerSkin.PROPERTY_DATE_PICKER_CONTENTS);
if (stack != null && !stack.isEmpty()) {
newTarget = stack.peek();
} else {
@Nullable JFXDialogPane currentDialogPane =
(JFXDialogPane) getDialogContainer().getProperties()
.get(DialogUtils.PROPERTY_DIALOG_PANE_INSTANCE);
if (currentDialogPane != null && currentDialogPane.peek().isPresent()) {
newTarget = currentDialogPane.peek().orElseThrow();
} else {
newTarget = navigator.getCurrentPage();
}
}
}
for (@Nullable Node node = target; node != null; node = node.getParent()) {
@@ -60,6 +60,7 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator
private final TabHeader.Tab<WorldListPage> worldListTab = new TabHeader.Tab<>("worldList");
private final TabHeader.Tab<SchematicsPage> schematicsTab = new TabHeader.Tab<>("schematicsTab");
private final TabHeader.Tab<ResourcePackListPage> resourcePackTab = new TabHeader.Tab<>("resourcePackTab");
private final TabHeader.Tab<ScreenshotsPage> screenshotsTab = new TabHeader.Tab<>("screenshotsTab");
private final TransitionPane transitionPane = new TransitionPane();
private final BooleanProperty currentInstanceUpgradable = new SimpleBooleanProperty();
private final ObjectProperty<HMCLGameInstance.@Nullable Optional> instance =
@@ -92,8 +93,9 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator
resourcePackTab.setNodeSupplier(() -> new ResourcePackListPage(instance));
worldListTab.setNodeSupplier(() -> new WorldListPage(instance));
schematicsTab.setNodeSupplier(() -> new SchematicsPage(instance));
screenshotsTab.setNodeSupplier(() -> new ScreenshotsPage(instance));
tab = new TabHeader(transitionPane, gameSettingsTab, installerListTab, modListTab, resourcePackTab, worldListTab, schematicsTab);
tab = new TabHeader(transitionPane, gameSettingsTab, installerListTab, modListTab, resourcePackTab, worldListTab, schematicsTab, screenshotsTab);
tab.select(gameSettingsTab);
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
@@ -387,6 +389,10 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator
new IconedMenuItem(SVG.DELETE, i18n("instance.manage.remove"), control::remove, managementPopup),
new IconedMenuItem(SVG.OUTPUT, i18n("modpack.export"), control::export, managementPopup),
new MenuSeparator(),
// Due to the sidebar height limit, we temporarily place the entries for these tabs
// in the management menu; they will be moved back to the sidebar after a future UI refactor.
new IconedMenuItem(SVG.SCREENSHOT_MONITOR, i18n("screenshots.manage"), () -> control.tab.select(control.screenshotsTab), managementPopup),
new MenuSeparator(),
new IconedMenuItem(null, i18n("instance.manage.redownload_assets_index"), control::redownloadAssetIndex, managementPopup),
new IconedMenuItem(null, i18n("instance.manage.remove_assets"), control::clearAssets, managementPopup),
new IconedMenuItem(null, i18n("instance.manage.remove_libraries"), control::clearLibraries, managementPopup),
@@ -0,0 +1,504 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.instances;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXListView;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.game.HMCLGameInstance;
import org.jackhuang.hmcl.setting.BackgroundType;
import org.jackhuang.hmcl.setting.LauncherSettings;
import org.jackhuang.hmcl.setting.SettingsManager;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.ItemPropertyAsyncCache;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Instant;
import java.time.ZoneId;
import java.util.*;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// Lists screenshots of the current game instance and provides preview and file actions.
public final class ScreenshotsPage extends ListPageBase<ScreenshotsPage.Screenshot> {
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
private @Nullable Path screenshotsDir;
public ScreenshotsPage(ObservableValue<? extends HMCLGameInstance.Optional> instanceContext) {
Objects.requireNonNull(instanceContext, "instanceContext");
listenerHolder.add(FXUtils.onWeakChangeAndOperate(instanceContext, current -> {
if (current != null) {
loadInstance(current);
}
}));
}
@Override
protected Skin<?> createDefaultSkin() {
return new ScreenshotsPageSkin(this);
}
public void loadInstance(HMCLGameInstance.Optional instance) {
HMCLGameInstance gameInstance = instance.instance();
if (gameInstance == null) return;
screenshotsDir = gameInstance.getRunDirectory().resolve("screenshots");
refresh();
}
public void refresh() {
if (screenshotsDir == null) return;
setLoading(true);
Task.supplyAsync(Schedulers.io(), () -> {
if (screenshotsDir == null) return List.<Screenshot>of();
try (Stream<Path> stream = Files.list(screenshotsDir)) {
return stream.map(Screenshot::fromFile).filter(Objects::nonNull).sorted(Comparator.reverseOrder()).toList();
}
}).whenComplete(Schedulers.javafx(), (list, exception) -> {
if (exception != null) {
LOG.warning("Failed to load screenshots in: " + screenshotsDir, exception);
getItems().clear();
} else {
getItems().setAll(list);
}
setLoading(false);
}).start();
}
private void revealFolder() {
if (screenshotsDir != null) FXUtils.openFolder(screenshotsDir);
}
private void deleteAt(Path path) {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
LOG.warning("Failed to delete screenshot: " + path, e);
}
}
private void delete(Screenshot screenshot) {
deleteAt(screenshot.getPath());
refresh();
}
private void delete(Collection<Screenshot> screenshots) {
screenshots.stream().map(Screenshot::getPath).forEach(this::deleteAt);
refresh();
}
private void clear() {
if (screenshotsDir == null) return;
try (var stream = Files.list(screenshotsDir)) {
stream.filter(Screenshot::isFileScreenshot).forEach(this::deleteAt);
} catch (IOException e) {
LOG.warning("Failed to clear screenshots at: " + screenshotsDir, e);
}
refresh();
}
private void selectDate(JFXListView<Screenshot> listView) {
FXUtils.chooseDateRange(pair -> {
Instant from = pair.key() != null ? pair.key().atStartOfDay().atZone(ZoneId.systemDefault()).toInstant() : Instant.MIN;
Instant to = pair.value() != null ? pair.value().plusDays(1).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant() : Instant.MAX;
if (from.compareTo(to) >= 0) return;
List<Screenshot> items = listView.getItems();
int start = -1, end = -1;
for (int i = 0; i < items.size(); i++) {
// Reversed order
var creationTime = items.get(i).getCreationTime();
if ((creationTime == null || creationTime.compareTo(from) < 0) && end < 0) end = i;
if ((creationTime == null || creationTime.compareTo(to) < 0) && start < 0) start = i;
}
if (start < 0) return;
if (end < 0) end = items.size();
listView.getSelectionModel().selectRange(start, end);
listView.scrollTo(start);
});
}
/// Describes a screenshot file and loads its preview images on demand.
public static final class Screenshot implements Comparable<Screenshot> {
private final Path path;
private final String fileName;
private final BasicFileAttributes attributes;
/// Caches asynchronous thumbnail loads with a soft reference.
private final ItemPropertyAsyncCache<@Nullable Image, Screenshot> thumbnailCache =
new ItemPropertyAsyncCache.Soft<>(this, this::loadThumbnail, null);
/// Retains the full image after a successful load, or is null before one succeeds.
private @Nullable Image fullImage;
public static boolean isFileScreenshot(Path path) {
return Files.isRegularFile(path) && "png".equalsIgnoreCase(FileUtils.getExtension(path));
}
public static Screenshot fromFile(Path path) {
if (!isFileScreenshot(path)) return null;
BasicFileAttributes attr = null;
try {
attr = Files.readAttributes(path, BasicFileAttributes.class);
} catch (IOException e) {
LOG.warning("Failed to read screenshot attributes at: " + path, e);
}
return new Screenshot(path, FileUtils.getName(path), attr);
}
public Screenshot(Path path, String fileName, BasicFileAttributes attributes) {
this.path = path;
this.fileName = fileName;
this.attributes = attributes;
}
public Path getPath() {
return path;
}
public String getFileName() {
return fileName;
}
public BasicFileAttributes getAttributes() {
return attributes;
}
public Instant getCreationTime() {
if (attributes == null) return null;
return attributes.creationTime().toInstant();
}
public long getFileSize() {
if (attributes == null) return -1;
return attributes.size();
}
public boolean isFullImageLoaded() {
return fullImage != null;
}
/// Loads an aspect-ratio-preserving thumbnail bounded by 72 by 72 pixels.
///
/// @return the thumbnail, or null if loading throws an exception
@Nullable
private Image loadThumbnail() {
try {
return FXUtils.loadImage(path, 72, 72, true, false);
} catch (Exception e) {
LOG.warning("Failed to load screenshot thumbnail at: " + path, e);
return null;
}
}
@Nullable
public Image getFullImage() {
if (fullImage == null) {
try {
fullImage = FXUtils.loadImage(path);
} catch (Exception e) {
LOG.warning("Failed to load screenshot content at: " + path, e);
}
}
return fullImage;
}
@Override
public boolean equals(Object obj) {
return obj instanceof Screenshot that && Objects.equals(this.path, that.path);
}
@Override
public int hashCode() {
return Objects.hashCode(path);
}
@Override
public int compareTo(@NotNull ScreenshotsPage.Screenshot o) {
return this.path.compareTo(o.path);
}
}
/// Displays a screenshot thumbnail, metadata, and file actions in a reusable cell.
private static final class ScreenshotCell extends MDListCell<Screenshot> {
/// Holds the thumbnail or the placeholder icon when no thumbnail is available.
private final StackPane imagePane = new StackPane();
/// Receives the cached thumbnail for the current item.
private final ObjectProperty<@Nullable Image> thumbnail = new SimpleObjectProperty<>();
/// Displays the screenshot file name and creation time.
private final TwoLineListItem content = new TwoLineListItem();
/// Creates a cell whose file actions operate on the supplied page.
public ScreenshotCell(JFXListView<Screenshot> listView, ScreenshotsPage page) {
super(listView);
setSelectable();
FXUtils.onChangeAndOperate(thumbnail, image -> imagePane.getChildren().setAll(
image == null ? SVG.SCREENSHOT_MONITOR.createIcon(36) : new ImageContainer(image, 36, 36)));
HBox container = new HBox(8);
container.setPickOnBounds(false);
container.setAlignment(Pos.CENTER_LEFT);
content.setMouseTransparent(true);
HBox.setHgrow(content, Priority.ALWAYS);
JFXButton copyButton = FXUtils.newToggleButton4(SVG.CONTENT_COPY);
copyButton.setOnAction(e -> {
@Nullable Screenshot screenshot = getItem();
if (screenshot != null) {
@Nullable Image image = screenshot.getFullImage();
if (image != null) FXUtils.copyImage(image);
}
});
JFXButton revealButton = FXUtils.newToggleButton4(SVG.FOLDER);
revealButton.setOnAction(e -> {
@Nullable Screenshot screenshot = getItem();
if (screenshot != null) {
FXUtils.showFileInExplorer(screenshot.getPath());
}
});
JFXButton infoButton = FXUtils.newToggleButton4(SVG.INFO);
infoButton.setOnAction(e -> {
@Nullable Screenshot screenshot = getItem();
if (screenshot != null) {
Controllers.dialog(new ScreenshotDialog(screenshot));
}
});
FXUtils.onDoubleClicked(this, infoButton::fire);
JFXButton deleteButton = FXUtils.newToggleButton4(SVG.DELETE_FOREVER);
deleteButton.setOnAction(e -> {
@Nullable Screenshot screenshot = getItem();
if (screenshot != null) {
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"),
() -> page.delete(screenshot), null);
}
});
container.getChildren().setAll(imagePane, content, copyButton, revealButton, infoButton, deleteButton);
StackPane.setMargin(container, new Insets(8));
getContainer().getChildren().setAll(container);
}
/// Attaches the current item's cached thumbnail and updates its metadata.
/// Clears the displayed thumbnail when the cell becomes empty.
@Override
protected void updateControl(@Nullable Screenshot item, boolean empty) {
if (item == null || empty) {
thumbnail.set(null);
return;
}
item.thumbnailCache.attachValue(thumbnail, new WeakReference<>(itemProperty()));
content.setTitle(item.getFileName());
@Nullable Instant creationTime = item.getCreationTime();
if (creationTime != null) content.setSubtitle(I18n.formatDateTime(creationTime));
}
}
private static final class ScreenshotDialog extends JFXDialogLayout {
public ScreenshotDialog(Screenshot screenshot) {
TwoLineListItem head = new TwoLineListItem();
head.setTitle(screenshot.getFileName());
setHeading(head);
var image = screenshot.getFullImage();
if (image == null) {
setBody(SVG.SCREENSHOT_MONITOR.createIcon(360));
if (screenshot.getAttributes() != null)
head.setSubtitle(I18n.formatDateTime(screenshot.getCreationTime()) + " " + FileUtils.parseFileSize(screenshot.getFileSize()));
} else {
setBody(new ImageContainer(image, Math.min(Controllers.getDecorator().contentWidthProperty().get() * 0.8, image.getWidth()), Controllers.getDecorator().contentHeightProperty().get() * 0.57));
if (screenshot.getAttributes() != null)
head.setSubtitle(
I18n.formatDateTime(screenshot.getCreationTime())
+ " " + FileUtils.parseFileSize(screenshot.getFileSize())
+ " " + (int) image.getWidth() + " * " + (int) image.getHeight()
);
else head.setSubtitle((int) image.getWidth() + " * " + (int) image.getHeight());
}
JFXButton setAsBgButton = new JFXButton(i18n("screenshots.set_as_bg"));
setAsBgButton.getStyleClass().add("dialog-accept");
setAsBgButton.setOnAction(e -> {
SettingsManager.settings().getThemeAppearanceOverrides().add(LauncherSettings.THEME_APPEARANCE_BACKGROUND);
SettingsManager.settings().backgroundTypeProperty().set(BackgroundType.CUSTOM);
SettingsManager.settings().customBackgroundImagePathProperty().set(screenshot.getPath().toString());
});
JFXButton copyButton = new JFXButton(i18n("menu.copy"));
copyButton.getStyleClass().add("dialog-accept");
copyButton.setDisable(image == null);
copyButton.setOnAction(e -> {
if (image != null) FXUtils.copyImage(image);
});
JFXButton okButton = new JFXButton(i18n("button.ok"));
okButton.getStyleClass().add("dialog-accept");
okButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
FXUtils.onEscPressed(this, okButton::fire);
setActions(setAsBgButton, copyButton, okButton);
}
}
private static final class ScreenshotsPageSkin extends SkinBase<ScreenshotsPage> {
private final TransitionPane toolbarPane;
private final HBox toolbarNormal;
private final HBox toolbarSelecting;
private final JFXListView<Screenshot> listView;
public ScreenshotsPageSkin(ScreenshotsPage skinnable) {
super(skinnable);
StackPane pane = new StackPane();
pane.setPadding(new Insets(10));
pane.getStyleClass().addAll("notice-pane");
ComponentList root = new ComponentList();
root.getStyleClass().add("no-padding");
listView = new JFXListView<>();
listView.getStyleClass().add("no-horizontal-scrollbar");
{
toolbarPane = new TransitionPane();
toolbarNormal = new HBox();
toolbarSelecting = new HBox();
// Toolbar Normal
toolbarNormal.getChildren().setAll(
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::revealFolder),
createToolbarButton2(i18n("button.clear"), SVG.DELETE_FOREVER, () -> {
Controllers.confirm(i18n("screenshots.clear.confirm"), i18n("button.clear"), skinnable::clear, null);
}),
createToolbarButton2(i18n("button.select_date"), SVG.DATE_RANGE, () -> skinnable.selectDate(listView))
);
// Toolbar Selecting
toolbarSelecting.getChildren().setAll(
createToolbarButton2(i18n("menu.copy"), SVG.FOLDER_COPY, () -> {
FXUtils.copyFiles(listView.getSelectionModel().getSelectedItems().stream().map(Screenshot::getPath).toList());
}),
createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> {
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () ->
skinnable.delete(listView.getSelectionModel().getSelectedItems()), null);
}),
createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () ->
listView.getSelectionModel().selectAll()),
createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () ->
listView.getSelectionModel().clearSelection())
);
FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(),
selectedItem -> {
if (selectedItem == null)
changeToolbar(toolbarNormal);
else
changeToolbar(toolbarSelecting);
});
root.getContent().add(toolbarPane);
// Clear selection when pressing ESC
root.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
if (e.getCode() == KeyCode.ESCAPE) {
if (listView.getSelectionModel().getSelectedItem() != null) {
listView.getSelectionModel().clearSelection();
e.consume();
}
}
});
}
{
SpinnerPane center = new SpinnerPane();
ComponentList.setVgrow(center, Priority.ALWAYS);
center.loadingProperty().bind(skinnable.loadingProperty());
listView.setCellFactory(x -> new ScreenshotCell(listView, skinnable));
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
Bindings.bindContent(listView.getItems(), skinnable.getItems());
listView.setOnContextMenuRequested(event -> {
Screenshot selectedItem = listView.getSelectionModel().getSelectedItem();
if (selectedItem != null && listView.getSelectionModel().getSelectedItems().size() == 1) {
listView.getSelectionModel().clearSelection();
Controllers.dialog(new ScreenshotDialog(selectedItem));
}
});
// ListViewBehavior would consume ESC pressed event, preventing us from handling it
// So we ignore it here
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
center.setContent(listView);
root.getContent().add(center);
}
pane.getChildren().setAll(root);
getChildren().setAll(pane);
}
private void changeToolbar(HBox newToolbar) {
if (newToolbar != toolbarPane.getCurrentNode()) toolbarPane.setContent(newToolbar, ContainerAnimations.FADE);
}
}
}
@@ -2009,3 +2009,82 @@
.addon-changelog-table-cell {
-fx-text-fill: -monet-primary;
}
/*******************************************************************************
* *
* JFX Date Picker *
* *
******************************************************************************/
.jfx-date-picker {
-jfx-default-color: -monet-primary;
}
.date-picker-popup {
-fx-padding: 5;
-fx-background-color: -monet-surface-container-high;
-fx-background-radius: 4;
}
.date-picker-popup .spinner .spinner-label {
-fx-text-fill: -monet-on-surface;
}
.date-picker-popup .month-year-pane,
.date-picker-popup .content {
-fx-background-color: transparent;
}
.date-picker-popup .separator .line {
-fx-border-width: 1;
-fx-border-color: -monet-outline-variant;
}
.date-picker-popup .content .spinner-label,
.date-picker-popup .data-picker-list-cell .spinner-label {
-fx-text-fill: -monet-on-surface;
}
.date-picker-popup .data-picker-list-cell:selected {
-fx-background-color: -monet-secondary-container;
}
.date-picker-popup .date-cell {
-fx-text-fill: -monet-on-surface;
}
.date-picker-popup .day-cell {
-fx-border-radius: 20;
-fx-background-radius: 20;
-fx-background-color: transparent;
}
.date-picker-popup .day-cell.today {
-fx-border-color: -monet-primary;
-fx-text-fill: -monet-primary;
}
.date-picker-popup .day-cell.previous-month,
.date-picker-popup .day-cell.next-month {
-fx-background-color: transparent;
-fx-text-fill: -monet-on-surface-variant;
}
.date-picker-popup .day-cell.hovered {
-fx-background-color: -monet-surface-container-highest;
}
.date-picker-popup .day-cell.selected,
.date-picker-popup .day-cell.selected.hovered{
-fx-background-color: -monet-primary;
-fx-text-fill: -monet-on-primary;
}
.date-picker-popup .left-button .jfx-svg-glyph,
.date-picker-popup .right-button .jfx-svg-glyph {
-fx-fill: -monet-on-surface-variant;
}
.date-picker-popup .title {
-fx-text-fill: -monet-on-surface-variant;
}
@@ -227,6 +227,9 @@ button.remove.confirm=Are you sure you want to permanently remove it? This actio
button.save=Save
button.save_as=Save As
button.select_all=Select All
button.select_date=Select by date
button.select_date.from=From
button.select_date.to=To
button.view=View
button.yes=Yes
@@ -1358,6 +1361,10 @@ schematics.install_mod=Install Litematica Mod
schematics.manage=Schematics
schematics.sub_items=%d sub-item(s)
screenshots.clear.confirm=Are you sure you want to permanently remove the screenshots? This action cannot be undone!
screenshots.manage=Screenshots
screenshots.set_as_bg=Set as Launcher Background
search=Search
search.hint.chinese=Search in English and Chinese
search.hint.english=Search in English only
@@ -228,6 +228,9 @@ button.remove.confirm=هل أنت متأكد من الإزالة النهائي
button.save=حفظ
button.save_as=حفظ باسم
button.select_all=تحديد الكل
# button.select_date=
# button.select_date.from=
# button.select_date.to=
button.view=عرض
button.yes=نعم
@@ -1357,6 +1360,10 @@ schematics.info.version=إصدار المخطط
schematics.manage=المخططات
schematics.sub_items=%d عنصر فرعي
# screenshots.clear.confirm=
# screenshots.manage=
# screenshots.set_as_bg=
search=بحث
search.hint.chinese=البحث بالإنجليزية والصينية
search.hint.english=البحث بالإنجليزية فقط
@@ -226,6 +226,9 @@ button.remove.confirm=Sind Sie sicher, dass Sie dies dauerhaft entfernen möchte
button.save=Speichern
button.save_as=Speichern unter
button.select_all=Alles auswählen
# button.select_date=
# button.select_date.from=
# button.select_date.to=
button.view=Anzeigen
button.yes=Ja
@@ -1356,6 +1359,10 @@ schematics.info.version=Schematik-Version
schematics.manage=Schematiken
schematics.sub_items=%d Unterelement(e)
# screenshots.clear.confirm=
# screenshots.manage=
# screenshots.set_as_bg=
search=Suchen
search.hint.chinese=Auf Englisch und Chinesisch suchen
search.hint.english=Nur auf Englisch suchen
@@ -226,6 +226,9 @@ button.remove.confirm=¿Estás seguro de que deseas eliminarlo de forma permanen
button.save=Guardar
button.save_as=Guardar como
button.select_all=Seleccionar todo
# button.select_date=
# button.select_date.from=
# button.select_date.to=
button.view=Vista
button.yes=Sí
@@ -1349,6 +1352,10 @@ schematics.info.version=Versión de esquema
schematics.manage=Esquemas
schematics.sub_items=%d elemento(s)
# screenshots.clear.confirm=
# screenshots.manage=
# screenshots.set_as_bg=
search=Búsqueda
search.hint.chinese=Buscar en inglés y chino
search.hint.english=Buscar sólo en inglés
@@ -215,6 +215,9 @@ button.remove.confirm=本当に完全に削除しますか?この操作は元
button.save=保存
button.save_as=名前を付けて保存
button.select_all=すべて選択
# button.select_date=
# button.select_date.from=
# button.select_date.to=
button.view=読む
button.yes=はい
@@ -1153,6 +1156,10 @@ reveal.in_file_manager=ファイルマネージャーで表示する
# schematics.manage=
# schematics.sub_items=
# screenshots.clear.confirm=
# screenshots.manage=
# screenshots.set_as_bg=
search=検索
search.hint.chinese=中国語と英語での検索をサポート
search.hint.english=英語でのみ検索をサポート
@@ -226,6 +226,9 @@ button.remove.confirm=誠刪之?刪則不復!
button.save=存
button.save_as=另存
button.select_all=悉擇之
# button.select_date=
# button.select_date.from=
# button.select_date.to=
button.view=覽
button.yes=然
@@ -1165,6 +1168,10 @@ schematics.info.version=蜃圖之版
schematics.manage=司蜃圖
schematics.sub_items=%d 子项
# screenshots.clear.confirm=
# screenshots.manage=
# screenshots.set_as_bg=
search=尋
search.hint.chinese=以中英文尋
search.hint.english=惟以英文尋
@@ -222,6 +222,9 @@ button.remove.confirm=Удалить навсегда? Это действие
button.save=Сохранить
button.save_as=Сохранить как
button.select_all=Выбрать все
# button.select_date=
# button.select_date.from=
# button.select_date.to=
button.view=Просмотреть
button.yes=Да
@@ -1289,6 +1292,10 @@ schematics.info.version=Версия схемы
schematics.manage=Схемы
schematics.sub_items=%d элемент(ов)
# screenshots.clear.confirm=
# screenshots.manage=
# screenshots.set_as_bg=
search=Поиск
search.hint.chinese=Поиск на китайском и английском языках
search.hint.english=Поиск только на английском языке
@@ -228,6 +228,9 @@ button.remove.confirm=Ви впевнені, що хочете назавжди
button.save=Зберегти
button.save_as=Зберегти як
button.select_all=Вибрати все
# button.select_date=
# button.select_date.from=
# button.select_date.to=
button.view=Переглянути
button.yes=Так
@@ -1312,6 +1315,10 @@ schematics.info.version=Версія схематики
schematics.manage=Схематики
schematics.sub_items=%d підпунктів
# screenshots.clear.confirm=
# screenshots.manage=
# screenshots.set_as_bg=
search=Пошук
search.hint.chinese=Пошук англійською та китайською
search.hint.english=Пошук лише англійською
@@ -227,6 +227,9 @@ button.remove.confirm=你确定要删除吗?此操作无法撤销!
button.save=保存
button.save_as=另存为
button.select_all=全选
button.select_date=按日期选择
button.select_date.from=开始
button.select_date.to=截止
button.view=查看
button.yes=是
@@ -1170,6 +1173,10 @@ schematics.install_mod=安装投影模组
schematics.manage=原理图管理
schematics.sub_items=%d 个子项
screenshots.clear.confirm=你确定要清除所有截图吗?此操作无法撤销!
screenshots.manage=截图管理
screenshots.set_as_bg=设为启动器背景
search=搜索
search.hint.chinese=支持中英文搜索
search.hint.english=仅支持英文搜索
@@ -225,6 +225,9 @@ button.remove.confirm=你確認要刪除嗎?該操作無法復原!
button.save=儲存
button.save_as=另存新檔
button.select_all=全選
button.select_date=按日期選擇
button.select_date.from=開始
button.select_date.to=截止
button.view=查看
button.yes=是
@@ -1164,6 +1167,10 @@ schematics.install_mod=安裝投影模組
schematics.manage=原理圖管理
schematics.sub_items=%d 個子項
screenshots.clear.confirm=你確認要清除所有擷圖嗎?該操作無法復原!
screenshots.manage=擷圖管理
screenshots.set_as_bg=設成啟動器背景
search=搜尋
search.hint.chinese=支援中英文搜尋
search.hint.english=僅支援英文搜尋
@@ -48,6 +48,11 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
*/
public final class FileUtils {
public static final long KB = 1024;
public static final long MB = KB * KB;
public static final long GB = KB * MB;
public static final long TB = KB * GB;
private FileUtils() {
}
@@ -238,6 +243,18 @@ public final class FileUtils {
}
}
public static String parseFileSize(long sizeInBytes) {
if (sizeInBytes >= TB)
return String.format(Locale.ROOT, "%.2f TiB", (float) sizeInBytes / TB);
if (sizeInBytes >= GB)
return String.format(Locale.ROOT, "%.2f GiB", (float) sizeInBytes / GB);
if (sizeInBytes >= MB)
return String.format(Locale.ROOT, "%.2f MiB", (float) sizeInBytes / MB);
if (sizeInBytes >= KB)
return String.format(Locale.ROOT, "%.2f KiB", (float) sizeInBytes / KB);
return String.format(Locale.ROOT, "%d B", sizeInBytes);
}
public static String readTextMaybeNativeEncoding(Path file) throws IOException {
byte[] bytes = Files.readAllBytes(file);