alt: root page tab

This commit is contained in:
huanghongxun 2020-03-02 18:06:53 +08:00
parent e11a5389c6
commit 1c3b3bf050
23 changed files with 517 additions and 265 deletions

View File

@ -23,7 +23,6 @@ import com.google.gson.stream.JsonWriter;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.io.FileUtils;
@ -50,12 +49,14 @@ public class Theme {
Color.web("#B71C1C") // red
};
private final Color paint;
private final String color;
private final String name;
Theme(String name, String color) {
this.name = name;
this.color = color;
this.paint = Color.web(color);
}
public String getName() {
@ -84,6 +85,7 @@ public class Theme {
File temp = File.createTempFile("hmcl", ".css");
FileUtils.writeText(temp, IOUtils.readFullyAsString(ResourceNotFoundError.getResourceAsStream("/assets/css/custom.css"))
.replace("%base-color%", color)
.replace("%base-rippler-color%", String.format("rgba(%d, %d, %d, 0.3)", (int)Math.ceil(paint.getRed() * 256), (int)Math.ceil(paint.getGreen() * 256), (int)Math.ceil(paint.getBlue() * 256)))
.replace("%font-color%", getColorDisplayName(getForegroundColor())));
css = temp.toURI().toString();
} catch (IOException | NullPointerException e) {

View File

@ -100,7 +100,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
right.getChildren().add(btnRemove);
root.setRight(right);
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;");
root.setStyle("-fx-background-color: white; -fx-background-radius: 4; -fx-padding: 8 8 8 0;");
JFXDepthManager.setDepth(root, 1);
getChildren().setAll(root);

View File

@ -23,6 +23,7 @@ import javafx.collections.ObservableList;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.ListPage;
import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
@ -37,6 +38,7 @@ public class AuthlibInjectorServersPage extends ListPage<AuthlibInjectorServerIt
public AuthlibInjectorServersPage() {
serverItems = MappedObservableList.create(config().getAuthlibInjectorServers(), this::createServerItem);
Bindings.bindContent(itemsProperty(), serverItems);
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onDecoratorPageNavigating);
}
private AuthlibInjectorServerItem createServerItem(AuthlibInjectorServer server) {

View File

@ -30,10 +30,12 @@ public class AdvancedListItem extends Control {
private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
private final ObjectProperty<Node> rightGraphic = new SimpleObjectProperty<>(this, "rightGraphic");
private final StringProperty title = new SimpleStringProperty(this, "title");
private final BooleanProperty active = new SimpleBooleanProperty(this, "active");
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
private final BooleanProperty actionButtonVisible = new SimpleBooleanProperty(this, "actionButtonVisible", true);
public AdvancedListItem() {
getStyleClass().add("advanced-list-item");
addEventHandler(MouseEvent.MOUSE_CLICKED, e -> fireEvent(new ActionEvent()));
}
@ -73,6 +75,18 @@ public class AdvancedListItem extends Control {
this.title.set(title);
}
public boolean isActive() {
return active.get();
}
public BooleanProperty activeProperty() {
return active;
}
public void setActive(boolean active) {
this.active.set(active);
}
public String getSubtitle() {
return subtitle.get();
}

View File

@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
@ -30,13 +31,19 @@ import javafx.scene.text.TextAlignment;
import org.jackhuang.hmcl.ui.FXUtils;
public class AdvancedListItemSkin extends SkinBase<AdvancedListItem> {
private final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
public AdvancedListItemSkin(AdvancedListItem skinnable) {
super(skinnable);
StackPane stackPane = new StackPane();
stackPane.getStyleClass().add("container");
RipplerContainer container = new RipplerContainer(stackPane);
FXUtils.onChangeAndOperate(skinnable.activeProperty(), active -> {
skinnable.pseudoClassStateChanged(SELECTED, active);
});
BorderPane root = new BorderPane();
root.setPickOnBounds(false);

View File

@ -117,9 +117,11 @@ public class ComponentList extends Control {
protected static class Skin extends SkinBase<ComponentList> {
private static final PseudoClass PSEUDO_CLASS_FIRST = PseudoClass.getPseudoClass("first");
private static final PseudoClass PSEUDO_CLASS_LAST = PseudoClass.getPseudoClass("last");
private final ObservableList<Node> list;
private final ObjectBinding<Node> firstItem;
private final ObjectBinding<Node> lastItem;
protected Skin(ComponentList control) {
super(control);
@ -140,6 +142,16 @@ public class ComponentList extends Control {
if (!list.isEmpty())
list.get(0).pseudoClassStateChanged(PSEUDO_CLASS_FIRST, true);
lastItem = Bindings.valueAt(list, Bindings.subtract(Bindings.size(list), 1));
lastItem.addListener((observable, oldValue, newValue) -> {
if (newValue != null)
newValue.pseudoClassStateChanged(PSEUDO_CLASS_LAST, true);
if (oldValue != null)
oldValue.pseudoClassStateChanged(PSEUDO_CLASS_LAST, false);
});
if (!list.isEmpty())
list.get(list.size() - 1).pseudoClassStateChanged(PSEUDO_CLASS_LAST, true);
VBox vbox = new VBox();
Bindings.bindContent(vbox.getChildren(), list);
getChildren().setAll(vbox);

View File

@ -1,4 +1,4 @@
/**
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*

View File

@ -17,7 +17,9 @@
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.Event;
import javafx.event.EventHandler;
@ -37,11 +39,13 @@ import java.util.logging.Level;
public class Navigator extends TransitionPane {
private static final String PROPERTY_DIALOG_CLOSE_HANDLER = Navigator.class.getName() + ".closeListener";
private final BooleanProperty backable = new SimpleBooleanProperty(this, "backable");
private final Stack<Node> stack = new Stack<>();
private boolean initialized = false;
public void init(Node init) {
stack.push(init);
backable.set(canGoBack());
getChildren().setAll(init);
fireEvent(new NavigationEvent(this, init, NavigationEvent.NAVIGATED));
@ -62,6 +66,7 @@ public class Navigator extends TransitionPane {
Logging.LOG.info("Navigate to " + node);
stack.push(node);
backable.set(canGoBack());
NavigationEvent navigating = new NavigationEvent(this, from, NavigationEvent.NAVIGATING);
fireEvent(navigating);
@ -103,6 +108,7 @@ public class Navigator extends TransitionPane {
Logging.LOG.info("Closed page " + from);
stack.pop();
backable.set(canGoBack());
Node node = stack.peek();
NavigationEvent navigating = new NavigationEvent(this, from, NavigationEvent.NAVIGATING);
@ -131,6 +137,18 @@ public class Navigator extends TransitionPane {
return stack.size() > 1;
}
public boolean isBackable() {
return backable.get();
}
public BooleanProperty backableProperty() {
return backable;
}
public void setBackable(boolean backable) {
this.backable.set(backable);
}
public int size() {
return stack.size();
}

View File

@ -0,0 +1,249 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 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.construct;
import javafx.beans.property.*;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.AccessibleAttribute;
import javafx.scene.Node;
import javafx.scene.control.SingleSelectionModel;
import java.util.function.Supplier;
public interface TabControl {
ObservableList<Tab> getTabs();
class TabControlSelectionModel extends SingleSelectionModel<Tab> {
private final TabControl tabHeader;
public TabControlSelectionModel(final TabControl t) {
if (t == null) {
throw new NullPointerException("TabPane can not be null");
}
this.tabHeader = t;
// watching for changes to the items list content
final ListChangeListener<Tab> itemsContentObserver = c -> {
while (c.next()) {
for (Tab tab : c.getRemoved()) {
if (tab != null && !tabHeader.getTabs().contains(tab)) {
if (tab.isSelected()) {
tab.setSelected(false);
final int tabIndex = c.getFrom();
// we always try to select the nearest, non-disabled
// tab from the position of the closed tab.
findNearestAvailableTab(tabIndex, true);
}
}
}
if (c.wasAdded() || c.wasRemoved()) {
// The selected tab index can be out of sync with the list of tab if
// we add or remove tabs before the selected tab.
if (getSelectedIndex() != tabHeader.getTabs().indexOf(getSelectedItem())) {
clearAndSelect(tabHeader.getTabs().indexOf(getSelectedItem()));
}
}
}
if (getSelectedIndex() == -1 && getSelectedItem() == null && tabHeader.getTabs().size() > 0) {
// we go looking for the first non-disabled tab, as opposed to
// just selecting the first tab (fix for RT-36908)
findNearestAvailableTab(0, true);
} else if (tabHeader.getTabs().isEmpty()) {
clearSelection();
}
};
if (this.tabHeader.getTabs() != null) {
this.tabHeader.getTabs().addListener(itemsContentObserver);
}
}
// API Implementation
@Override public void select(int index) {
if (index < 0 || (getItemCount() > 0 && index >= getItemCount()) ||
(index == getSelectedIndex() && getModelItem(index).isSelected())) {
return;
}
// Unselect the old tab
if (getSelectedIndex() >= 0 && getSelectedIndex() < tabHeader.getTabs().size()) {
tabHeader.getTabs().get(getSelectedIndex()).setSelected(false);
}
setSelectedIndex(index);
Tab tab = getModelItem(index);
if (tab != null) {
setSelectedItem(tab);
}
// Select the new tab
if (getSelectedIndex() >= 0 && getSelectedIndex() < tabHeader.getTabs().size()) {
tabHeader.getTabs().get(getSelectedIndex()).setSelected(true);
}
/* Does this get all the change events */
((Node) tabHeader).notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_ITEM);
}
@Override public void select(Tab tab) {
final int itemCount = getItemCount();
for (int i = 0; i < itemCount; i++) {
final Tab value = getModelItem(i);
if (value != null && value.equals(tab)) {
select(i);
return;
}
}
if (tab != null) {
setSelectedItem(tab);
}
}
@Override protected Tab getModelItem(int index) {
final ObservableList<Tab> items = tabHeader.getTabs();
if (items == null) return null;
if (index < 0 || index >= items.size()) return null;
return items.get(index);
}
@Override protected int getItemCount() {
final ObservableList<Tab> items = tabHeader.getTabs();
return items == null ? 0 : items.size();
}
private Tab findNearestAvailableTab(int tabIndex, boolean doSelect) {
// we always try to select the nearest, non-disabled
// tab from the position of the closed tab.
final int tabCount = getItemCount();
int i = 1;
Tab bestTab = null;
while (true) {
// look leftwards
int downPos = tabIndex - i;
if (downPos >= 0) {
Tab _tab = getModelItem(downPos);
if (_tab != null) {
bestTab = _tab;
break;
}
}
// look rightwards. We subtract one as we need
// to take into account that a tab has been removed
// and if we don't do this we'll miss the tab
// to the right of the tab (as it has moved into
// the removed tabs position).
int upPos = tabIndex + i - 1;
if (upPos < tabCount) {
Tab _tab = getModelItem(upPos);
if (_tab != null) {
bestTab = _tab;
break;
}
}
if (downPos < 0 && upPos >= tabCount) {
break;
}
i++;
}
if (doSelect && bestTab != null) {
select(bestTab);
}
return bestTab;
}
}
public static class Tab {
private final StringProperty id = new SimpleStringProperty(this, "id");
private final StringProperty text = new SimpleStringProperty(this, "text");
private final ReadOnlyBooleanWrapper selected = new ReadOnlyBooleanWrapper(this, "selected");
private final ObjectProperty<Node> node = new SimpleObjectProperty<>(this, "node");
private Supplier<Node> nodeSupplier;
public Tab(String id) {
setId(id);
}
public Tab(String id, String text) {
setId(id);
setText(text);
}
public Supplier<Node> getNodeSupplier() {
return nodeSupplier;
}
public void setNodeSupplier(Supplier<Node> nodeSupplier) {
this.nodeSupplier = nodeSupplier;
}
public String getId() {
return id.get();
}
public StringProperty idProperty() {
return id;
}
public void setId(String id) {
this.id.set(id);
}
public String getText() {
return text.get();
}
public StringProperty textProperty() {
return text;
}
public void setText(String text) {
this.text.set(text);
}
public boolean isSelected() {
return selected.get();
}
public ReadOnlyBooleanProperty selectedProperty() {
return selected.getReadOnlyProperty();
}
private void setSelected(boolean selected) {
this.selected.set(selected);
}
public Node getNode() {
return node.get();
}
public ObjectProperty<Node> nodeProperty() {
return node;
}
public void setNode(Node node) {
this.node.set(node);
}
}
}

View File

@ -21,14 +21,13 @@ import com.jfoenix.controls.JFXRippler;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Side;
import javafx.scene.AccessibleAttribute;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.MouseButton;
@ -40,7 +39,7 @@ import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
public class TabHeader extends Control {
public class TabHeader extends Control implements TabControl {
public TabHeader(Tab... tabs) {
getStyleClass().setAll("tab-header");
@ -51,11 +50,12 @@ public class TabHeader extends Control {
private ObservableList<Tab> tabs = FXCollections.observableArrayList();
@Override
public ObservableList<Tab> getTabs() {
return tabs;
}
private final ObjectProperty<SingleSelectionModel<Tab>> selectionModel = new SimpleObjectProperty<>(this, "selectionModel", new TabHeaderSelectionModel(this));
private final ObjectProperty<SingleSelectionModel<Tab>> selectionModel = new SimpleObjectProperty<>(this, "selectionModel", new TabControlSelectionModel(this));
public SingleSelectionModel<Tab> getSelectionModel() {
return selectionModel.get();
@ -69,151 +69,6 @@ public class TabHeader extends Control {
this.selectionModel.set(selectionModel);
}
static class TabHeaderSelectionModel extends SingleSelectionModel<Tab> {
private final TabHeader tabHeader;
public TabHeaderSelectionModel(final TabHeader t) {
if (t == null) {
throw new NullPointerException("TabPane can not be null");
}
this.tabHeader = t;
// watching for changes to the items list content
final ListChangeListener<Tab> itemsContentObserver = c -> {
while (c.next()) {
for (Tab tab : c.getRemoved()) {
if (tab != null && !tabHeader.getTabs().contains(tab)) {
if (tab.isSelected()) {
tab.setSelected(false);
final int tabIndex = c.getFrom();
// we always try to select the nearest, non-disabled
// tab from the position of the closed tab.
findNearestAvailableTab(tabIndex, true);
}
}
}
if (c.wasAdded() || c.wasRemoved()) {
// The selected tab index can be out of sync with the list of tab if
// we add or remove tabs before the selected tab.
if (getSelectedIndex() != tabHeader.getTabs().indexOf(getSelectedItem())) {
clearAndSelect(tabHeader.getTabs().indexOf(getSelectedItem()));
}
}
}
if (getSelectedIndex() == -1 && getSelectedItem() == null && tabHeader.getTabs().size() > 0) {
// we go looking for the first non-disabled tab, as opposed to
// just selecting the first tab (fix for RT-36908)
findNearestAvailableTab(0, true);
} else if (tabHeader.getTabs().isEmpty()) {
clearSelection();
}
};
if (this.tabHeader.getTabs() != null) {
this.tabHeader.getTabs().addListener(itemsContentObserver);
}
}
// API Implementation
@Override public void select(int index) {
if (index < 0 || (getItemCount() > 0 && index >= getItemCount()) ||
(index == getSelectedIndex() && getModelItem(index).isSelected())) {
return;
}
// Unselect the old tab
if (getSelectedIndex() >= 0 && getSelectedIndex() < tabHeader.getTabs().size()) {
tabHeader.getTabs().get(getSelectedIndex()).setSelected(false);
}
setSelectedIndex(index);
Tab tab = getModelItem(index);
if (tab != null) {
setSelectedItem(tab);
}
// Select the new tab
if (getSelectedIndex() >= 0 && getSelectedIndex() < tabHeader.getTabs().size()) {
tabHeader.getTabs().get(getSelectedIndex()).setSelected(true);
}
/* Does this get all the change events */
tabHeader.notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_ITEM);
}
@Override public void select(Tab tab) {
final int itemCount = getItemCount();
for (int i = 0; i < itemCount; i++) {
final Tab value = getModelItem(i);
if (value != null && value.equals(tab)) {
select(i);
return;
}
}
if (tab != null) {
setSelectedItem(tab);
}
}
@Override protected Tab getModelItem(int index) {
final ObservableList<Tab> items = tabHeader.getTabs();
if (items == null) return null;
if (index < 0 || index >= items.size()) return null;
return items.get(index);
}
@Override protected int getItemCount() {
final ObservableList<Tab> items = tabHeader.getTabs();
return items == null ? 0 : items.size();
}
private Tab findNearestAvailableTab(int tabIndex, boolean doSelect) {
// we always try to select the nearest, non-disabled
// tab from the position of the closed tab.
final int tabCount = getItemCount();
int i = 1;
Tab bestTab = null;
while (true) {
// look leftwards
int downPos = tabIndex - i;
if (downPos >= 0) {
Tab _tab = getModelItem(downPos);
if (_tab != null) {
bestTab = _tab;
break;
}
}
// look rightwards. We subtract one as we need
// to take into account that a tab has been removed
// and if we don't do this we'll miss the tab
// to the right of the tab (as it has moved into
// the removed tabs position).
int upPos = tabIndex + i - 1;
if (upPos < tabCount) {
Tab _tab = getModelItem(upPos);
if (_tab != null) {
bestTab = _tab;
break;
}
}
if (downPos < 0 && upPos >= tabCount) {
break;
}
i++;
}
if (doSelect && bestTab != null) {
select(bestTab);
}
return bestTab;
}
}
@Override
protected Skin<?> createDefaultSkin() {
return new TabHeaderSkin(this);
@ -450,55 +305,4 @@ public class TabHeader extends Control {
}
}
}
public static class Tab {
private final StringProperty id = new SimpleStringProperty(this, "id");
private final StringProperty text = new SimpleStringProperty(this, "text");
private final ReadOnlyBooleanWrapper selected = new ReadOnlyBooleanWrapper(this, "selected");
public Tab(String id) {
setId(id);
}
public Tab(String id, String text) {
setId(id);
setText(text);
}
public String getId() {
return id.get();
}
public StringProperty idProperty() {
return id;
}
public void setId(String id) {
this.id.set(id);
}
public String getText() {
return text.get();
}
public StringProperty textProperty() {
return text;
}
public void setText(String text) {
this.text.set(text);
}
public boolean isSelected() {
return selected.get();
}
public ReadOnlyBooleanProperty selectedProperty() {
return selected.getReadOnlyProperty();
}
private void setSelected(boolean selected) {
this.selected.set(selected);
}
}
}

View File

@ -17,13 +17,9 @@
*/
package org.jackhuang.hmcl.ui.decorator;
import javafx.beans.binding.Bindings;
import javafx.scene.Node;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
public abstract class DecoratorNavigatorPage extends DecoratorTransitionPage {
protected final Navigator navigator = new Navigator();
@ -31,6 +27,7 @@ public abstract class DecoratorNavigatorPage extends DecoratorTransitionPage {
{
this.navigator.setOnNavigating(this::onNavigating);
this.navigator.setOnNavigated(this::onNavigated);
backableProperty().bind(navigator.backableProperty());
}
@Override
@ -50,39 +47,11 @@ public abstract class DecoratorNavigatorPage extends DecoratorTransitionPage {
private void onNavigating(Navigator.NavigationEvent event) {
if (event.getSource() != this.navigator) return;
Node from = event.getNode();
if (from instanceof DecoratorPage)
((DecoratorPage) from).back();
onNavigating(event.getNode());
}
private void onNavigated(Navigator.NavigationEvent event) {
if (event.getSource() != this.navigator) return;
Node to = event.getNode();
if (to instanceof Refreshable) {
refreshableProperty().bind(((Refreshable) to).refreshableProperty());
} else {
refreshableProperty().unbind();
refreshableProperty().set(false);
}
if (to instanceof DecoratorPage) {
state.bind(Bindings.createObjectBinding(() -> {
State state = ((DecoratorPage) to).stateProperty().get();
return new State(state.getTitle(), state.getTitleNode(), navigator.canGoBack(), state.isRefreshable(), true);
}, ((DecoratorPage) to).stateProperty()));
} else {
state.unbind();
state.set(new State("", null, navigator.canGoBack(), false, true));
}
if (to instanceof Region) {
Region region = (Region) to;
// Let root pane fix window size.
StackPane parent = (StackPane) region.getParent();
region.prefWidthProperty().bind(parent.widthProperty());
region.prefHeightProperty().bind(parent.heightProperty());
}
onNavigated(event.getNode());
}
}

View File

@ -0,0 +1,74 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 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.decorator;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.SingleSelectionModel;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.construct.TabControl;
import org.jackhuang.hmcl.ui.construct.TabHeader;
public abstract class DecoratorTabPage extends DecoratorTransitionPage implements TabControl {
public DecoratorTabPage() {
getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
if (newValue.getNode() == null && newValue.getNodeSupplier() != null) {
newValue.setNode(newValue.getNodeSupplier().get());
}
if (newValue.getNode() != null) {
onNavigating(getCurrentPage());
if (getCurrentPage() != null) getCurrentPage().fireEvent(new Navigator.NavigationEvent(null, getCurrentPage(), Navigator.NavigationEvent.NAVIGATING));
navigate(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer());
onNavigated(getCurrentPage());
if (getCurrentPage() != null) getCurrentPage().fireEvent(new Navigator.NavigationEvent(null, getCurrentPage(), Navigator.NavigationEvent.NAVIGATED));
}
});
}
public DecoratorTabPage(TabHeader.Tab... tabs) {
this();
if (tabs != null) {
getTabs().addAll(tabs);
}
}
private ObservableList<TabHeader.Tab> tabs = FXCollections.observableArrayList();
@Override
public ObservableList<TabHeader.Tab> getTabs() {
return tabs;
}
private final ObjectProperty<SingleSelectionModel<TabHeader.Tab>> selectionModel = new SimpleObjectProperty<>(this, "selectionModel", new TabControl.TabControlSelectionModel(this));
public SingleSelectionModel<TabHeader.Tab> getSelectionModel() {
return selectionModel.get();
}
public ObjectProperty<SingleSelectionModel<TabHeader.Tab>> selectionModelProperty() {
return selectionModel;
}
public void setSelectionModel(SingleSelectionModel<TabHeader.Tab> selectionModel) {
this.selectionModel.set(selectionModel);
}
}

View File

@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui.decorator;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
@ -24,6 +25,8 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
@ -32,13 +35,45 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public abstract class DecoratorTransitionPage extends Control implements DecoratorPage {
protected final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("")));
private final BooleanProperty backable = new SimpleBooleanProperty(false);
private final BooleanProperty refreshable = new SimpleBooleanProperty(false);
private Node currentPage;
protected final TransitionPane transitionPane = new TransitionPane();
protected void navigate(Node page, AnimationProducer animation) {
transitionPane.setContent(currentPage = page, animation);
refreshable.setValue(page instanceof Refreshable);
}
protected void onNavigating(Node from) {
if (from instanceof DecoratorPage)
((DecoratorPage) from).back();
}
protected void onNavigated(Node to) {
if (to instanceof Refreshable) {
refreshableProperty().bind(((Refreshable) to).refreshableProperty());
} else {
refreshableProperty().unbind();
refreshableProperty().set(false);
}
if (to instanceof DecoratorPage) {
state.bind(Bindings.createObjectBinding(() -> {
State state = ((DecoratorPage) to).stateProperty().get();
return new State(state.getTitle(), state.getTitleNode(), backable.get(), state.isRefreshable(), true);
}, ((DecoratorPage) to).stateProperty()));
} else {
state.unbind();
state.set(new State("", null, backable.get(), false, true));
}
if (to instanceof Region) {
Region region = (Region) to;
// Let root pane fix window size.
StackPane parent = (StackPane) region.getParent();
region.prefWidthProperty().bind(parent.widthProperty());
region.prefHeightProperty().bind(parent.heightProperty());
}
}
@Override
@ -48,6 +83,18 @@ public abstract class DecoratorTransitionPage extends Control implements Decorat
return currentPage;
}
public boolean isBackable() {
return backable.get();
}
public BooleanProperty backableProperty() {
return backable;
}
public void setBackable(boolean backable) {
this.backable.set(backable);
}
public boolean isRefreshable() {
return refreshable.get();
}

View File

@ -42,7 +42,7 @@ public class DecoratorWizardDisplayer extends DecoratorTransitionPage implements
wizardController.setProvider(provider);
wizardController.onStart();
addEventHandler(Navigator.NavigationEvent.NAVIGATING, this::onDecoratorPageNavigating);
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onDecoratorPageNavigating);
}
@Override
@ -77,6 +77,13 @@ public class DecoratorWizardDisplayer extends DecoratorTransitionPage implements
else
title = "";
state.set(new State(title, null, true, refreshableProperty().get(), true));
if (page instanceof Refreshable) {
refreshableProperty().bind(((Refreshable) page).refreshableProperty());
} else {
refreshableProperty().unbind();
refreshableProperty().set(false);
}
}
@Override

View File

@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.main;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
@ -37,10 +38,10 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem;
import org.jackhuang.hmcl.ui.account.AccountList;
import org.jackhuang.hmcl.ui.account.AddAccountPane;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorNavigatorPage;
import org.jackhuang.hmcl.ui.construct.TabHeader;
import org.jackhuang.hmcl.ui.decorator.DecoratorTabPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.profile.ProfileAdvancedListItem;
import org.jackhuang.hmcl.ui.profile.ProfileList;
@ -63,19 +64,48 @@ import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class RootPage extends DecoratorNavigatorPage {
public class RootPage extends DecoratorTabPage {
private MainPage mainPage = null;
private SettingsPage settingsPage = null;
private GameList gameListPage = null;
private AccountList accountListPage = null;
private ProfileList profileListPage = null;
private final TabHeader.Tab mainTab = new TabHeader.Tab("main");
private final TabHeader.Tab settingsTab = new TabHeader.Tab("settings");
private final TabHeader.Tab gameTab = new TabHeader.Tab("game");
private final TabHeader.Tab accountTab = new TabHeader.Tab("account");
private final TabHeader.Tab profileTab = new TabHeader.Tab("profile");
public RootPage() {
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(event -> onRefreshedVersions((HMCLGameRepository) event.getSource()));
Profile profile = Profiles.getSelectedProfile();
if (profile != null && profile.getRepository().isLoaded())
onRefreshedVersions(Profiles.selectedProfileProperty().get().getRepository());
mainTab.setNodeSupplier(this::getMainPage);
settingsTab.setNodeSupplier(this::getSettingsPage);
gameTab.setNodeSupplier(this::getGameListPage);
accountTab.setNodeSupplier(this::getAccountListPage);
profileTab.setNodeSupplier(this::getProfileListPage);
getTabs().setAll(mainTab, settingsTab, gameTab, accountTab, profileTab);
}
@Override
public boolean back() {
if (mainTab.isSelected()) return true;
else {
getSelectionModel().select(mainTab);
return false;
}
}
@Override
protected void onNavigated(Node to) {
backableProperty().set(!(to instanceof MainPage));
super.onNavigated(to);
}
@Override
@ -156,7 +186,8 @@ public class RootPage extends DecoratorNavigatorPage {
// first item in left sidebar
AccountAdvancedListItem accountListItem = new AccountAdvancedListItem();
accountListItem.setOnAction(e -> getSkinnable().navigate(getSkinnable().getAccountListPage(), ContainerAnimations.FADE.getAnimationProducer()));
accountListItem.activeProperty().bind(control.accountTab.selectedProperty());
accountListItem.setOnAction(e -> control.getSelectionModel().select(control.accountTab));
accountListItem.accountProperty().bind(Accounts.selectedAccountProperty());
// second item in left sidebar
@ -166,7 +197,7 @@ public class RootPage extends DecoratorNavigatorPage {
Profile profile = Profiles.getSelectedProfile();
String version = Profiles.getSelectedVersion();
if (version == null) {
getSkinnable().navigate(getSkinnable().getGameListPage(), ContainerAnimations.FADE.getAnimationProducer());
control.getSelectionModel().select(control.gameTab);
} else {
Versions.modifyGameSettings(profile, version);
}
@ -174,20 +205,23 @@ public class RootPage extends DecoratorNavigatorPage {
// third item in left sidebar
AdvancedListItem gameItem = new AdvancedListItem();
gameItem.activeProperty().bind(control.gameTab.selectedProperty());
gameItem.setImage(newImage("/assets/img/bookshelf.png"));
gameItem.setTitle(i18n("version.manage"));
gameItem.setOnAction(e -> getSkinnable().navigate(getSkinnable().getGameListPage(), ContainerAnimations.FADE.getAnimationProducer()));
gameItem.setOnAction(e -> control.getSelectionModel().select(control.gameTab));
// forth item in left sidebar
ProfileAdvancedListItem profileListItem = new ProfileAdvancedListItem();
profileListItem.setOnAction(e -> getSkinnable().navigate(getSkinnable().getProfileListPage(), ContainerAnimations.FADE.getAnimationProducer()));
profileListItem.activeProperty().bind(control.profileTab.selectedProperty());
profileListItem.setOnAction(e -> control.getSelectionModel().select(control.profileTab));
profileListItem.profileProperty().bind(Profiles.selectedProfileProperty());
// fifth item in left sidebar
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
launcherSettingsItem.activeProperty().bind(control.settingsTab.selectedProperty());
launcherSettingsItem.setImage(newImage("/assets/img/command.png"));
launcherSettingsItem.setTitle(i18n("settings.launcher"));
launcherSettingsItem.setOnAction(e -> getSkinnable().navigate(getSkinnable().getSettingsPage(), ContainerAnimations.FADE.getAnimationProducer()));
launcherSettingsItem.setOnAction(e -> control.getSelectionModel().select(control.settingsTab));
// the left sidebar
AdvancedListBox sideBar = new AdvancedListBox()
@ -217,10 +251,10 @@ public class RootPage extends DecoratorNavigatorPage {
}
{
control.navigator.getStyleClass().add("jfx-decorator-content-container");
control.navigator.init(getSkinnable().getMainPage());
FXUtils.setOverflowHidden(control.navigator, 8);
StackPane wrapper = new StackPane(control.navigator);
control.transitionPane.getStyleClass().add("jfx-decorator-content-container");
control.transitionPane.getChildren().setAll(getSkinnable().getMainPage());
FXUtils.setOverflowHidden(control.transitionPane, 8);
StackPane wrapper = new StackPane(control.transitionPane);
wrapper.setPadding(new Insets(4));
root.setCenter(wrapper);
}

View File

@ -73,7 +73,7 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
public SettingsPage() {
FXUtils.smoothScrolling(scroll);
addEventHandler(Navigator.NavigationEvent.NAVIGATING, this::onDecoratorPageNavigating);
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onDecoratorPageNavigating);
// ==== Download sources ====
cboDownloadSource.getItems().setAll(DownloadProviders.providersById.keySet());

View File

@ -73,7 +73,7 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
right.getChildren().add(btnRemove);
root.setRight(right);
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;");
root.setStyle("-fx-background-color: white; -fx-background-radius: 4; -fx-padding: 8 8 8 0;");
JFXDepthManager.setDepth(root, 1);
item.titleProperty().bind(skinnable.titleProperty());
item.subtitleProperty().bind(skinnable.subtitleProperty());

View File

@ -57,7 +57,7 @@ public class GameList extends ListPageBase<GameListItem> implements DecoratorPag
});
Profiles.registerVersionsListener(this::loadVersions);
addEventHandler(Navigator.NavigationEvent.NAVIGATING, this::onDecoratorPageNavigating);
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onDecoratorPageNavigating);
}
private void loadVersions(Profile profile) {

View File

@ -80,6 +80,11 @@ public class VersionPage extends Control implements DecoratorPage {
loadVersion(newValue, profile);
});
versionSettingsTab.setNode(versionSettingsPage);
modListTab.setNode(modListPage);
installerListTab.setNode(installerListPage);
worldListTab.setNode(worldListPage);
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
}
@ -211,15 +216,7 @@ public class VersionPage extends Control implements DecoratorPage {
control.worldListTab);
control.selectedTab.bind(tabPane.getSelectionModel().selectedItemProperty());
FXUtils.onChangeAndOperate(tabPane.getSelectionModel().selectedItemProperty(), newValue -> {
if (control.versionSettingsTab.equals(newValue)) {
control.transitionPane.setContent(control.versionSettingsPage, ContainerAnimations.FADE.getAnimationProducer());
} else if (control.modListTab.equals(newValue)) {
control.transitionPane.setContent(control.modListPage, ContainerAnimations.FADE.getAnimationProducer());
} else if (control.installerListTab.equals(newValue)) {
control.transitionPane.setContent(control.installerListPage, ContainerAnimations.FADE.getAnimationProducer());
} else if (control.worldListTab.equals(newValue)) {
control.transitionPane.setContent(control.worldListPage, ContainerAnimations.FADE.getAnimationProducer());
}
control.transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE.getAnimationProducer());
});
HBox toolBar = new HBox();

View File

@ -105,7 +105,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
public VersionSettingsPage() {
FXUtils.loadFXML(this, "/assets/fxml/version/version-settings.fxml");
addEventHandler(Navigator.NavigationEvent.NAVIGATING, this::onDecoratorPageNavigating);
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onDecoratorPageNavigating);
cboLauncherVisibility.getItems().setAll(LauncherVisibility.values());
cboLauncherVisibility.setConverter(stringConverter(e -> i18n("settings.advanced.launcher_visibility." + e.name().toLowerCase())));

View File

@ -19,5 +19,6 @@
-fx-base-color: #5c6bc0;
-fx-base-darker-color: derive(-fx-base-color, -10%);
-fx-base-check-color: derive(-fx-base-color, 30%);
-fx-base-rippler-color: rgba(92, 107, 192, 0.3);
-fx-base-text-fill: white;
}

View File

@ -19,5 +19,6 @@
-fx-base-color: %base-color%;
-fx-base-darker-color: derive(-fx-base-color, -10%);
-fx-base-check-color: derive(-fx-base-color, 30%);
-fx-base-rippler-color: %base-rippler-color%;
-fx-base-text-fill: %font-color%;
}

View File

@ -71,6 +71,10 @@
-fx-text-fill: -fx-base-text-fill;
}
.advanced-list-item:selected .container {
-fx-background-color: -fx-base-rippler-color;
}
.notice-pane > .label {
-fx-text-fill: #0079FF;
-fx-font-size: 20;
@ -692,6 +696,16 @@
}
.options-list-item:first {
-fx-background-radius: 4 4 0 0;
-fx-border-width: 0;
}
.options-list-item:last {
-fx-background-radius: 0 0 4 4;
}
.options-list-item:first:last {
-fx-background-radius: 4 4 4 4;
-fx-border-width: 0;
}