Забытое искусство подсказки
Давным-давно, когда люди ещё делали домашние странички, интернет был по шипучему модему, а 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.isEmpty()){ 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 библиотеки расширений — возможно. стоит попытаться пристроить этот интерфейс в одну из них?
