Забытое искусство подсказки
Давным-давно, когда люди ещё делали домашние странички, интернет был по шипучему модему, а Napster казался опасной провокацией коммунистов, простенькие оконные приложения под Windows очень часто писали на библиотеке VCL. Одни пользовались Delphi (но признавались только домашним), другие смело запускали её из C++ Builder (и удивлялись ещё одному String, а также спискам, которые начинаются с 1). А кто-то ухитрялся писать на нём под unix-ы (вы помните Kylix? А он был!)
В VCL почти у всех визуальных компонент были свойства ShowHint и Hint. Если быть точным, они были у всех компонент оконного типа (кнопок, выпадающих списков и прочих панелек).
В строковой Hint писался текст подсказки, а булевый ShowHint мог её отключить. А более прокачанные даже знали, что можно сделать расширенный вариант подсказки. Если написать в Hint Нажми меня|Кнопка просит, чтобы вы её нажали, то левая часть всплывёт, а правая будет передана в событие. Это событие перехватывали и показывали полученный текст в строке состояния.
Настолько простые и удобные подсказки были предметом величайшей зависти тех, кто сидел на MFC (никаких WinForms в тогдашней Visual Studio ещё не было, не говоря о WPF), поддерживал OWL или штурмовал чистый WinAPI с Петцольдом наперевес. Кто знает, может быть кто-то из них и приложил свою мозолистую от кодинга руку к тому, что уникальная по своей удобности технология подсказок оказалась полностью утрачена в JavaFX.
Что у нас есть?
В JavaFX за подсказки отвечает компонент Tooltip. По всей видимости, это тайный компонент, потому что, к примеру, SceneBuilder его знать не знает.
Если мы создадим новый Tooltip, а потом привяжем через setTooltip, то при наведении курсора на компонент, к которому привязывали, мы и правда увидим подсказку. На чёрном фоне (так надо). И с поддержкой картинок (спасибо).
Но JavaFX не позволит нам расслабиться: свойство Tooltip (и соответствующие методы) есть только у наследников класса javafx.scene.control.Control. А все панели и прочие области наследуются от javafx.scene.layout.Region. И никаких подсказок на них всплывать, получается, не может. Видимо, раз в приложении есть панель, то пользователю должно быть и так ясно, что там находится.
А может, разработчиков в школе слишком часто заставляли решать задачу «двумя разными способами»? Что до поддержки подсказок в панели статуса, то их нет даже близко.
А ТЗ требовало панель состояния и подсказки в ней в том числе и на панелях. И я справился своими силами.
Показываем и подсказываем
Я не стал пытаться заставить Tooltip всплывать над компонентами «неправильного» типа. Я просто хотел получить простой стандартный интерфейс, чтобы привязывать к компонентам подсказки в строке состояния, И, по возможности, делать всплывающие подсказки.
Причём интерфейс должен быть настолько простым, чтобы я мог поручить непосредственное привязывание подсказок студенту-стажёру, который работает на полставки.
Как и положено рабочему прототипу, это решение выглядит почти тривиальным. Поэтому я исключил все комментарии, которые бы только растянули статью. Но у него есть маленькое, но преимущество: приспособить его к своему проекту намного быстрее и проще, чем писать с нуля.
Особенно если вы такой же лентай, как и я.
Подсказываем
Саму подсказку мы будем хранить в двух классах, унаследованных от общего предка. Сначала создадим абстрактный класс подсказки.
Буква A перед названием пошла из старых учебников C++, по которым я учился программировать. Я не против, если вы привыкли по-другому (например, с постфиксом Base и т. п.). Я люблю начальную A, потому что она короткая.
И, как вы наверное догадались, любовь к шаблонам и генетикам у меня тоже из C++:
import javafx.scene.Node;
public abstract class ATooltipHintItem<N extends Node> {
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.equals("")){
initStatusBar();
this.setStatusBarHint(statusBarHint);
}
}
private void initStatusBar() {
getAttachedNode().setOnMouseEntered(observableValue -> {
this.showStatusBarHint();
});
getAttachedNode().setOnMouseExited(observableValue -> {
getTooltipHintController().setDefaultStatusBarText();
});
}
}
Теперь создадим по классу на оба случая. Вот класс для Region:
import javafx.scene.control.Control;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
public final class TooltipHintRegionItem extends ATooltipHintItem<Region>{
public TooltipHintRegionItem(Region attachedNode, ITooltipHintController tooltipHintController, String statusBarHint) {
super(attachedNode, tooltipHintController, statusBarHint);
}
}
А вот для Control:
import javafx.scene.control.Control;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
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(hint == null || hint.trim().length() <= 0)
return this;
if(tooltip == null) {
initTooltip();
}
tooltip.setText(hint);
return this;
}
public String getTooltipHint(){
return tooltipHint;
}
private Image tooltipImage;
public TooltipHintControlItem setTooltipImage(Image image){
tooltipImage = image;
if(tooltip != null) 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);
}
}
Контролируем
Теперь берёмся за Controller. А контроллер у нас начинается с интерфейса. Java без интерфейса — это как хакер без ноутбука.
public interface ITooltipHintController {
void setStatusBarText(String text);
String getStatusBarText();
void setDefaultStatusBarText();
}
А вот и сам контроллер:
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;
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) {
if(statusBarControl == null) {
return;
}
statusBarControl.setText(text);
}
@Override
public void setStatusBarText(String text) {
if(!isStatusBarLocked){
setStatusBarTextForce(text);
}
}
@Override
public String getStatusBarText() {
return (statusBarControl != null) ? 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){
this.statusBarControl = statusBarControl;
tooltipHintItems = FXCollections.observableList(new ArrayList<>());
}
public TooltipHintController(){
this(null);
}
private static TooltipHintController mainInstance;
public static TooltipHintController getMainInstance() {
if(mainInstance == null){
mainInstance = new TooltipHintController();
}
return mainInstance;
}
public static void setMainInstance(TooltipHintController tooltipHintController) {
mainInstance = tooltipHintController;
}
}
Дальше можно смело писать, даже не задумываясь, кто от кого унаследован:
TooltipHintController.getMainInstance().addTooltipHint(buttonStart, "Нажми меня", "Нажми эту кнопку");
TooltipHintController.getMainInstance().addTooltipHint(paneButtons, "Здесь нажимают");
А если у нас есть labelStatusBar в качестве строки состояния, то мы можем использовать и его:
TooltipHintController.setMainInstance(new TooltipHintController(labelStatusBar));
Конечно, эту реализацию стоит доработать — ведь чисто теоретически в приложении может быть и больше одной строки состояния. Если вам такие известны (разумеется, современные, на JavaFx и активно используемые) — дайте знать.
Заключение
К сожалению, этот набор из 4 классов слишком объёмен, чтобы свестись к одному документу на pastebin. Но и слишком мал и несамостоятелен, чтобы стать гордым maven-пакетом и занять почётное место в известном репозитории.
Но я всё равно надеюсь, что он найдэт своего пользователя (точнее, своего разработчика). А может, он попадётся на глаза разработчикам из Oracle и убедит их сделать подсказки удобней.
Есть одобренные самой Oracle библиотеки расширений — возможно. стоит попытаться пристроить этот интерфейс в одну из них?
Автор: RikkiMongoose