/*
* *********************************************************************************************************************
*
* SteelBlue: DCI User Interfaces
* http://tidalwave.it/projects/steelblue
*
* Copyright (C) 2015 - 2023 by Tidalwave s.a.s. (http://tidalwave.it)
*
* *********************************************************************************************************************
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* *********************************************************************************************************************
*
* git clone https://bitbucket.org/tidalwave/steelblue-src
* git clone https://github.com/tidalwave-it/steelblue-src
*
* *********************************************************************************************************************
*/
package it.tidalwave.role.ui.javafx.impl.common;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import javafx.collections.ObservableList;
import javafx.scene.control.Cell;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.KeyCode;
import it.tidalwave.ui.role.javafx.CustomGraphicProvider;
import it.tidalwave.util.As;
import it.tidalwave.util.annotation.VisibleForTesting;
import it.tidalwave.role.ui.Displayable;
import it.tidalwave.role.ui.UserAction;
import it.tidalwave.role.ui.UserActionProvider;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import static it.tidalwave.ui.role.javafx.CustomGraphicProvider._CustomGraphicProvider_;
import static java.util.stream.Collectors.*;
import static it.tidalwave.role.ui.Displayable._Displayable_;
import static it.tidalwave.role.ui.Styleable._Styleable_;
import static it.tidalwave.role.ui.UserActionProvider._UserActionProvider_;
/***********************************************************************************************************************
*
* An implementation of {@link CellBinder} that extracts information from a {@link UserActionProvider}.
*
* @author Fabrizio Giudici
*
**********************************************************************************************************************/
@RequiredArgsConstructor @Slf4j
public class DefaultCellBinder implements CellBinder
{
/** Roles to preload, so they are computed in the background thread. */
private static final List<Class<?>> PRELOADING_ROLE_TYPES = List.of(
_Displayable_, _UserActionProvider_, _Styleable_, _CustomGraphicProvider_);
private static final String ROLE_STYLE_PREFIX = "-rs-";
@Nonnull
private final Executor executor;
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override
public void bind (@Nonnull final Cell<?> cell, @Nullable final As item, final boolean empty)
{
log.trace("bind({}, {}, {})", cell, item, empty);
clearBindings(cell);
if (!empty && (item != null))
{
JavaFXWorker.run(executor,
() -> new RoleBag(item, PRELOADING_ROLE_TYPES),
roles -> bindAll(cell, roles));
}
}
/*******************************************************************************************************************
*
* Binds everything provided by the given {@link RoleBag} to the given {@link Cell}.
*
* @param cell the {@code Cell}
* @param roles the role bag
*
******************************************************************************************************************/
private void bindAll (@Nonnull final Cell<?> cell, @Nonnull final RoleBag roles)
{
bindTextAndGraphic(cell, roles);
bindDefaultAction(cell, roles);
bindContextMenu(cell, roles);
bindStyles(cell.getStyleClass(), roles);
}
/*******************************************************************************************************************
*
* Binds the text and eventual custom {@link javafx.scene.Node} provided by the given {@link RoleBag} to the given
* {@link Cell}.
*
* @param cell the {@code Cell}
* @param roles the role bag
*
******************************************************************************************************************/
private void bindTextAndGraphic (@Nonnull final Cell<?> cell, @Nonnull final RoleBag roles)
{
final var cgp = roles.get(_CustomGraphicProvider_);
cell.setGraphic(cgp.map(CustomGraphicProvider::getGraphic).orElse(null));
cell.setText(cgp.map(c -> "").orElse(roles.get(_Displayable_).map(Displayable::getDisplayName).orElse("")));
}
/*******************************************************************************************************************
*
* Binds the default {@link UserAction}s provided by the given {@link RoleBag} as the default action of the given
* {@link Cell} (activated by double click or key pressure).
*
* @param cell the {@code Cell}
* @param roles the role bag
*
******************************************************************************************************************/
private void bindDefaultAction (@Nonnull final Cell<?> cell, @Nonnull final RoleBag roles)
{
roles.getDefaultUserAction().ifPresent(defaultAction ->
{
// FIXME: doesn't work - keyevents are probably handled by ListView
cell.setOnKeyPressed(event ->
{
log.debug("onKeyPressed: {}", event);
if (event.getCode().equals(KeyCode.SPACE))
{
executor.execute(defaultAction::actionPerformed);
}
});
// FIXME: depends on mouse click, won't handle keyboard
cell.setOnMouseClicked(event ->
{
if (event.getClickCount() == 2)
{
executor.execute(defaultAction::actionPerformed);
}
});
});
}
/*******************************************************************************************************************
*
* Binds the {@link UserAction}s provided by the given {@link RoleBag} as items of the contextual menu of a
* {@link Cell}.
*
* @param cell the {@code Cell}
* @param roles the role bag
*
******************************************************************************************************************/
private void bindContextMenu (@Nonnull final Cell<?> cell, @Nonnull final RoleBag roles)
{
final var menuItems = createMenuItems(roles);
cell.setContextMenu(menuItems.isEmpty() ? null : new ContextMenu(menuItems.toArray(new MenuItem[0])));
}
/*******************************************************************************************************************
*
* Adds all the styles provided by the given {@link RoleBag} to a {@link ObservableList} of styles.
*
* @param styleClasses the destination where to add styles
* @param roles the role bag
*
******************************************************************************************************************/
@Nonnull
private void bindStyles (@Nonnull final ObservableList<String> styleClasses, @Nonnull final RoleBag roles)
{
final var styles = styleClasses.stream()
.filter(s -> !s.startsWith(ROLE_STYLE_PREFIX))
.collect(toList());
// FIXME: shouldn't reset them? In case of cell reuse, they get accumulated
styles.addAll(roles.getMany(_Styleable_)
.stream()
.flatMap(styleable -> styleable.getStyles().stream())
.map(s -> ROLE_STYLE_PREFIX + s)
.collect(toList()));
styleClasses.setAll(styles);
}
/*******************************************************************************************************************
*
* Create a list of {@link MenuItem}s for each action provided by the given {@link RoleBag}.
* Don't directly return a ContextMenu otherwise it will be untestable.
*
* @param roles the role bag
* @return the list of {@MenuItem}s
*
******************************************************************************************************************/
@Nonnull
@VisibleForTesting public List<MenuItem> createMenuItems (@Nonnull final RoleBag roles)
{
return roles.getMany(_UserActionProvider_).stream()
.flatMap(uap -> uap.getActions().stream())
.map(this::createMenuItem)
.collect(Collectors.toList());
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
private void clearBindings (@Nonnull final Cell<?> cell)
{
cell.setText("");
cell.setGraphic(null);
cell.setContextMenu(null);
cell.setOnKeyPressed(null);
cell.setOnMouseClicked(null);
}
/*******************************************************************************************************************
*
* Creates a {@link MenuItem} bound to the given action.
*
* @param action the action
* @return the bound {@code MenuItem}
*
******************************************************************************************************************/
@Nonnull
private MenuItem createMenuItem (@Nonnull final UserAction action)
{
final var menuItem = new MenuItem(action.as(_Displayable_).getDisplayName());
menuItem.setOnAction(new EventHandlerUserActionAdapter(executor, action));
return menuItem;
}
}
Too many static imports may lead to messy code.
If you overuse the static import feature, it can make your program unreadable and
unmaintainable, polluting its namespace with all the static members you import.
Readers of your code (including you, a few months after you wrote it) will not know
which class a static member comes from (Sun 1.5 Language Guide).
import static Lennon;
import static Ringo;
import static George;
import static Paul;
import static Yoko; // Too much !