Накопилась куча материала (кое-что годится на полноценную статью, а что-то просто может забиться).
Буду понемногу выкладывать, чтобы не хранить на бумаге.
Итак, всплывающие подсказки в JavaFx.
В давние времена, когда приложения оставались десктопными, окошечки под Windows было удобней всего рисовать с помощью библиотеки VCL, той самой, которая жила внутри Delphi. Конечно, кто-то писал на MFC в тогда ещё доисторической Visual Studio, кто-то был вынужден поддерживать проекты на OWL, а отдельные маньяки - даже на голом WinAPI, но их мы трогать не будем. И ещё не будем трогать тех, кто писал на VCL для Unix-ов (помните Kylix? А он был!)
Итак, внутри Delphi жил VCL. Если вы писали на C++Builder, он там тоже был. И дополнял C++ своей уникальной String, а также списками, которые начинались с 1.
И почти у всех визуальных компонент были свойства ShowHint и Hint. В Hint писался текст подсказки, а ShowHint мог её отключить. А более прокачанные даже знали, что можно сделать расширенный вариант подсказки. Если написать "Нажми меня|Кнопка просит, чтобы вы её нажали", то левая часть всплывёт, а правая будет передана сообщением, которое можно перехватить и вывести, например, в Status Bar.
Но пришла новая эра, VCL ушёл в историю, а у нас теперь, к примеру, кроссплатформенный JavaFX. И никакого свойства Hint у его компонентов нет. И status bar-а среди компонентов тоже нет. Такие дела.
Что же делать?
Для подсказок всплывающих есть невидимый для SceneBuilder компонент Tooltip, который отвечает за всплывающие подсказки. Если его создать, а потом привязать через setTooltip, то при наведении курсора мы и правда увидим подсказку (на чёрном фоне, но так надо).
Но при этом свойство Tooltip (и соответствующие методы) есть только у наследников класса javafx.scene.control.Control. А все панели и прочие области наследуются от javafx.scene.layout.Region. И никаких подсказок на них всплывать не может.
К тому же, разумеется, мы по прежнему не приблизились к StatusBar-у. Придётся писать всё своими руками.
Сначала класс привязки, который хранит ссылку на элемент управления и текст, который к нему относится:
ATooltipHintItem.java
import javafx.scene.Node;
/**
* Базовый класс для элемента подсказки. Связывает вместе компонент и
* текст подсказки.
*
* Created by a.teut on 18.03.15.
*/
public abstract class ATooltipHintItem<N> {
private N attachedNode;
protected void setAttachedNode(N node) {
attachedNode = node;
}
public N getAttachedNode() {
return attachedNode;
}
private String statusBarHint;
protected void setStatusBarHint(String hint){
statusBarHint = hint;
}
public String getStatusBarHint(){
return statusBarHint;
}
private ITooltipHintController tooltipHintController;
public ITooltipHintController getTooltipHintController() {
return tooltipHintController;
}
public void showStatusBarHint(){
tooltipHintController.setStatusBarText(statusBarHint);
}
public ATooltipHintItem(N attachedNode, ITooltipHintController tooltipHintController, String statusBarHint) {
this.attachedNode = attachedNode;
this.tooltipHintController = tooltipHintController;
if(statusBarHint != null && statusBarHint != ""){
initStatusBar();
this.setStatusBarHint(statusBarHint);
}
}
private void initStatusBar() {
getAttachedNode().setOnMouseEntered(observableValue -> {
this.showStatusBarHint();
});
getAttachedNode().setOnMouseExited(observableValue -> {
getTooltipHintController().setDefaultStatusBarText();
});
}
}
Теперь реализация для Region:
TooltipHintRegionItem.java
import javafx.scene.layout.Region;
/**
* Реализация для компонент, унаследованных от Region. Всплывающие подсказки
* прицепить нельзя.
*
* Created by a.teut on 18.03.15.
*/
public final class TooltipHintRegionItem extends ATooltipHintItem<Region>{
public TooltipHintRegionItem(Region attachedNode, ITooltipHintController tooltipHintController, String statusBarHint) {
super(attachedNode, tooltipHintController, statusBarHint);
}
}
А у Control могут быть Tooltip-ы:
TooltipHintRegionItem.java
import javafx.scene.control.Control;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
/**
* Реализация для компонент, унаследованных от Control. Можно делать и всплывающие подсказки.
*
* Created by a.teut on 18.03.15.
*/
/**
* Реализация для компонент, унаследованных от Control. Можно делать и всплывающие подсказки.
*
* Created by a.teut on 18.03.15.
*/
public final class TooltipHintControlItem extends ATooltipHintItem<Control> {
private Tooltip tooltip;
public Tooltip getTooltip() {
return tooltip;
}
private String tooltipHint;
public TooltipHintControlItem setTooltipHint(String hint){
tooltipHint = hint;
if(tooltip == null) {
initTooltip();
}
tooltip.setText(hint);
return this;
}
public String getTooltipHint(){
return tooltipHint;
}
private Image tooltipImage;
public TooltipHintControlItem setTooltipImage(Image image){
tooltipImage = image;
tooltip.setGraphic((image != null) ? new ImageView(image) : null);
return this;
}
public Image getTooltipImage(){
return tooltipImage;
}
public TooltipHintControlItem(Control attachedNode, ITooltipHintController tooltipHintController, String statusBarHint, String tooltipHint, Image imageHint) {
super(attachedNode, tooltipHintController, statusBarHint);
if(tooltipHint != null && tooltipHint != ""){
initTooltip();
}
setTooltipHint(tooltipHint);
if(imageHint == null) {
setTooltipImage(imageHint);
}
}
public TooltipHintControlItem(Control attachedNode, ITooltipHintController tooltipHintController, String statusBarHint, String tooltipHint) {
this(attachedNode, tooltipHintController, statusBarHint, tooltipHint, null);
}
public TooltipHintControlItem(Control attachedNode, ITooltipHintController tooltipHintController, String statusBarHint) {
this(attachedNode, tooltipHintController, statusBarHint, null, null);
}
private void initTooltip() {
tooltip = new Tooltip();
getAttachedNode().setTooltip(tooltip);
}
public Image getTooltipImage(){
return tooltipImage;
}
public TooltipHintControlItem(Control attachedNode, ITooltipHintController tooltipHintController, String statusBarHint, String tooltipHint) {
super(attachedNode, tooltipHintController, statusBarHint);
if(tooltipHint != null && tooltipHint != ""){
initTooltip();
}
setTooltipHint(tooltipHint);
}
public TooltipHintControlItem(Control attachedNode, ITooltipHintController tooltipHintController, String statusBarHint) {
this(attachedNode, tooltipHintController, statusBarHint, null);
}
private void initTooltip() {
tooltip = new Tooltip();
getAttachedNode().setTooltip(tooltip);
}
}
Теперь распишем интерфейс, который их вызывает:
ITooltipHintController.java
/**
* Функции контроллера, которые вызывают сами элементы привязок.
*
* Created by a.teut on 18.03.15.
*/
public interface ITooltipHintController {
void setStatusBarText(String text);
String getStatusBarText();
void setDefaultStatusBarText();
}
И, наконец, контроллер:
TooltipHintController.java
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Labeled;
import javafx.scene.image.Image;
import javafx.scene.layout.Region;
import java.util.ArrayList;
import java.util.Iterator;
/**
* Контроллер подсказок. Хранит привязки подсказок к компонентом и, если нужно,
* всплывающие подсказки.
*
* Как правило, один на всё приложение.
*
* Created by a.teut on 18.03.15.
*/
public final class TooltipHintController implements ITooltipHintController {
private final String DefaultStatusBarText = "";
private final Labeled statusBarControl;
private final ObservableList<ATooltipHintItem> tooltipHintItems;
private boolean isStatusBarLocked = false;
public boolean getIsStatusBarLocked() {
return isStatusBarLocked;
}
public void setIsStatusBarLocked(boolean isStatusBarLocked) {
this.isStatusBarLocked = isStatusBarLocked;
}
public Labeled getStatusBarControl() {
return this.statusBarControl;
}
public void setStatusBarTextForce(String text) {
statusBarControl.setText(text);
}
@Override
public void setStatusBarText(String text) {
if(!isStatusBarLocked){
setStatusBarTextForce(text);
}
}
@Override
public String getStatusBarText() {
return statusBarControl.getText();
}
@Override
public void setDefaultStatusBarText(){
setStatusBarTextForce(DefaultStatusBarText);
}
//тут есть дублирование кода, но пока ничего серьёзного
public void addTooltipHint(Region region, String statusBarHint){
// Tooltip нас не интересует - у регионов в JavaFX не бывает всплывающих подсказок
ATooltipHintItem tooltipHintItem = findTooltipHint(region);
if(tooltipHintItem == null) {
tooltipHintItem = new TooltipHintRegionItem(region, this, statusBarHint);
tooltipHintItems.add(tooltipHintItem);
} else {
TooltipHintControlItem tooltipHintControlItem = (TooltipHintControlItem)tooltipHintItem;
if(statusBarHint != null && tooltipHintControlItem.getStatusBarHint() == null)
tooltipHintControlItem.setStatusBarHint(statusBarHint);
}
}
public void addTooltipHint(Control control, String statusBarHint){
addTooltipHint(control, statusBarHint, null, null);
}
public void addTooltipHint(Control control, String statusBarHint, String tooltipHint){
addTooltipHint(control, statusBarHint, tooltipHint, null);
}
public void addTooltipHint(Control control, String statusBarHint, String tooltipHint, Image image){
ATooltipHintItem tooltipHintItem = findTooltipHint(control);
if(tooltipHintItem == null) {
tooltipHintItem = new TooltipHintControlItem(control, this, statusBarHint, tooltipHint, image);
tooltipHintItems.add(tooltipHintItem);
} else {
TooltipHintControlItem tooltipHintControlItem = (TooltipHintControlItem)tooltipHintItem;
if(statusBarHint != null && tooltipHintControlItem.getStatusBarHint() == null)
tooltipHintControlItem.setStatusBarHint(statusBarHint);
if(tooltipHint != null && tooltipHintControlItem.getTooltipHint() == null)
tooltipHintControlItem.setTooltipHint(tooltipHint);
if(image != null && tooltipHintControlItem.getTooltipImage() == null)
tooltipHintControlItem.setTooltipImage(image);
}
}
public void removeTooltipHint(Node control){
ATooltipHintItem tooltipHintItem = null;
Iterator<ATooltipHintItem> iteratorTooltipHintItems = tooltipHintItems.iterator();
while(iteratorTooltipHintItems.hasNext()){
tooltipHintItem = iteratorTooltipHintItems.next();
if(tooltipHintItem.getAttachedNode() == control){
tooltipHintItems.remove(tooltipHintItem);
break;
}
}
}
public ATooltipHintItem findTooltipHint(Node control){
for(ATooltipHintItem tooltipHintItem : tooltipHintItems)
if(tooltipHintItem.getAttachedNode() == control)
return tooltipHintItem;
return null;
}
/**
* При создании нужно привязать контроллер к компоненту, который будет
* показывать подсказки.
*
* @param statusBarControl Компонент для подсказок
*/
public TooltipHintController(Labeled statusBarControl){
if(statusBarControl == null) {
throw new NullPointerException("Unable to create TooltipHintController. statusBarControl can't be null.");
}
this.statusBarControl = statusBarControl;
tooltipHintItems = FXCollections.observableList(new ArrayList<>());
}
private static TooltipHintController mainInstance;
public static TooltipHintController getMainInstance() {
if(mainInstance == null){
throw new NullPointerException("Main instance of TooltipHintController isn't initialised.");
}
return mainInstance;
}
public static void setMainInstance(TooltipHintController tooltipHintController) {
mainInstance = tooltipHintController;
}
}
Чтобы заработало, надо инициализировать контроллер тем самым элементом, в который надо отображать подсказки:
TooltipHintController.setMainInstance(new TooltipHintController(labelStatusBar));
И привязываем к компонентам:
TooltipHintController.getMainInstance().addTooltipHint(buttonStart, "Нажми меня", "Нажми эту кнопку");
TooltipHintController.getMainInstance().addTooltipHint(paneButtons, "Здесь нажимают");