JavaFX TableView multiline tooltip with cell factory and bindings - javafx

I have using this code snippet to create tooltips for every tableView cell I propagate via cell factories:
private <T> void addFileTooltipToCells(TableColumn<FileTableBean,T> column) {
Callback<TableColumn<FileTableBean, T>, TableCell<FileTableBean,T>> existingCellFactory = column.getCellFactory();
column.setCellFactory(c -> {
TableCell<FileTableBean, T> cell = existingCellFactory.call(c);
Tooltip tooltip = new Tooltip();
tooltip.textProperty().bind(cell.itemProperty().asString());
tooltip.setShowDuration(Duration.seconds(30));
tooltip.setStyle("-fx-font-size: 12");
cell.setTooltip(tooltip);
return cell;
});
}
Problem is that the tooltips can sometimes have lots of data and should be multilined. From here I found that one should simply wrap the tooltips with and possibly set where necessary. However, the above carefully somewhere copied and trial-error tested code snippet doesn't like the idea. I have tried:
tooltip.textProperty().bind("<html>"+cell.itemProperty().asString()+"</html>");
tooltip.setText("<html>"+cell.itemProperty().asString()+"</html>");
First says in IDE: Incompatible String cannot be converted to ObservableValue
Second one generater following tooltips: " String binding [invalid] " :)
And before somebody starts with the usual "the error message says it all, read it" stuff, I know that there are probably a simple answer for this, but having these lambdas combined with the binding concept and cellfactories, I just have to give up and try to get some guidance from here.

Answer to those who face the same problem. You can use the "normal" binding but just apply the tooltip preferences to every tooltip the cellfactory creates simply by defining:
tooltip.setMaxWidth(750);
tooltip.setWrapText(true);
Setting preferable width will create tooltips with lots of extra blanks if the data is shorter than the width. Max width works like a charm. Next step is probably to make width parameter dynamic so that it calculates the max width based on stage resolution.

Related

How can I detect whether a tooltip is visible at a given moment?

I'm looking for a way to detect whether a Qt widget's ToolTip is visible at the moment when a particular key combination is pressed. If it is, I want to copy the ToolTip's text to the clipboard.
Specifically, I have a QListView containing abbreviated strings, which is set up (via the Qt::ToolTipRole of the associated model) to show the full string of the appropriate list item when the mouse is hovered over it. The behaviour I'm looking for is that if the user presses CTRL-C (as detected by a QShortcut) while the tooltip is visible, then the tooltip text is copied to the clipboard.
My original idea was to use the children() method of the QListView widget to see if there was a tooltip preset among them:
// Inisde the slot connected to QShortcut::activated...
auto children = _ui -> myListView -> children();
QString selectionText;
for (const auto & child : children)
{
if (qobject_cast<QToolTip *>(child))
{
selectionText = qobject_cast<QToolTip *>(child) -> text();
break;
}
}
...but this failed because it turns out that QToolTip does not inherit from QObject.
I've also thought of screening for QEvent::QToolTip events in the ListView's main event handler, and while I could probably get this to work it seems excessively low-level; I'd need to use screen co-ordinates to determine which item in the list was being hovered over and look for the widget's timeout to check that the tooltip hadn't disappeared again by the time that the QShortcut was fired. I'd be disappointed if there weren't a simpler way.
Is there an obvious way forward that I've failed to see?
There are probably several possible solutions, but I am afraid none of them is simple. What I would do is to use the implementation detail that the tooltip actual widget is called QTipLabel. See https://code.woboq.org/qt5/qtbase/src/widgets/kernel/qtooltip.cpp.html#QTipLabel and it inherits from QLabel so you can easily get the text from it.
I am afraid the following solution is just a savage hack. I have not tested it, but it should work.
I would override the data model for your view, specifically override method data() which would call the data() method of the original model class but cache the last value which was returned when this method is called with role == Qt::ToolTipRole.
Then you need to catch the shortcut you are interested in. After it is caught, you get all qApp->topLevelWidgets() https://doc.qt.io/qt-5/qapplication.html#topLevelWidgets` and go through them and check if any of them has class name equal to QTipLabel (use QMetaObject::className()) and is visible, i.e. isVisible() == true.
If you get this visible QTipLabel widget (you hold it via QWidget*), qobject_cast it to QLabel* (you cannot cast it to QTipLabel beause you do not have access to the definition of QTipLabel class because it is in private Qt source file) and get the text with QLabel::text(). If the text is the same as the text which you stored in step 1, then yes, this is the text you are looking for and you can copy it to clipboard or do whatever yo want with it.
Nasty, isn't it? But it is the simplest what I can think of.
PS: I believe that step 1 can be implemented also by catching QEvent::QToolTip for your view and then do some magic to get the text, but I think that overriding data() for model can be a bit easier.
PPS: One obvious drawback is that Qt can rename QTipLabel class in the future. But I would not be worry about it. That won't happen becaus ethey do not change QtWidgets module any more. And if it happens, then you just rename the class in your code. No problem.
PPPS: Another potential corner-case is that some other widget (whose tooltip you do NOT want to capture with that shortcut) actually has the same tooltip text as any of the items in your list view (which you DO want to capture). Then if you display tooltip for your list item, then you move your mouse over to that other widget and hover so that its tooltip gets shown (but you do NOT want to capture it) and then you press that shortcut... But I guess that in reality this will not be your case. I doubt there will be this unlikely clash of tooltips.
With thanks to #V.K., this is what worked:
auto candidates = qApp->topLevelWidgets();
QString selectionText;
for (const auto & candidate : candidates)
{
if (strcmp(candidate->metaObject()->className(), "QTipLabel") == 0)
{
QLabel * label = qobject_cast<QLabel *>(candidate);
if (label->isVisible())
{
selectionText = label -> text();
break;
}
}
}
if (!selectionText.isEmpty())
QGuiApplication::clipboard() -> setText(selectionText);

Verifying the text of a comboBox

I am using https://github.com/TestFX/TestFX for gui tests of a javafx client. With a testfx query I get the comboBox but I cannot get its text for verification. The comboBox displays enum values whose text is resolved by a converter and a given resource bundle. The scene graph for the comboBox looks like that:
javafx.scene.control.ComboBox
javafx.scene.layout.StackPane:arrow-button
javafx.scene.layout.Region:arrow
com.sun.javafx.scene.control.skin.ComboBoxListViewSkin$4$1:null
com.sun.javafx.scene.control.skin.LabeledText:null
comboBox.getValue() gives me only the enum value but not the text (I could verify the enum value but since it is a gui test the displayed text should be verified). By trying out I found out that comboBox.getChildrenUnmodifiable().toString() prints
[StackPane[id=arrow-button, styleClass=arrow-button], ComboBoxListViewSkin$5[id=list-view, styleClass=list-view], ComboBoxListViewSkin$4$1#4f65f1d7[styleClass=cell indexed-cell list-cell]'StringOfInterest']
The string 'StringOfInterest' at the end is exactly what I need but it is unclear where it comes from. By looking into the source code of javafx it seems that Node#toString is being used. However, it is unclear where the last part ('StringOfInterest') comes from. I tried to get the text of all children of the ComboBox but the string in question is not part of it.
How can I extract the string?
I found a way to get the text on the combobox using TestFX 4 and JavaFX 12. Not sure if the below works on other versions as well. Admittedly, it feels a bit hacky and brittle, but it gives me the desired result.
ComboBox<String> comboBox = robot.lookup("#comboBox").queryComboBox();
ListCell<String> listCell = robot
.from(comboBox)
.lookup((Node node) -> node.getStyleClass().contains("list-cell")
&& node.getParent() instanceof ComboBox)
.<ListCell<String>>query();
I first tried to just lookup(".list-cell"), but that actually gave me two results, one with null as text and one with the desired text. The one with null is nested somewhere in the scene graph, but the one we are interested in has the combobox as parent. And that's what the lookup checks.
You can now verify the text of your combobox:
assertThat(listCell.getText()).isEqualTo("expected text");

QT: How to show combo boxes as cells in a tree view

In a designer-based QT GUI application I'm using a QTreeView to show a tree of elements that is provided by an instance of QStandardItemModel.
The tree-view is multi-column and all elements in the first column are checkable. The screenshot shows an example of how this currently looks like:
Now let's say I'd like the user to be able to select different names for "point". The idea is to have a QComboBox right next to each of the checkboxes. And as alternatives to "point" he may chose from a set of strings, e.g. "point", "pt" and "coord2D". Later on I'd like all selections for all duplicates of "point" to be synchronized but let's start simple...
I'm not too familiar with the idea but to me it looks like the way to go is to create an ItemDelegate for the view as described in the QT Documentation or in this topic (both links refer to QTableWidgets instead of QTreeViews).
So what I did as a first step is I took the example delegate ComboBoxDelegate from the stack overflow question mentioned above and called it from within my application using this code also taken from a related question:
QStandardItemModel* model = new QStandardItemModel(20,2);
ui.tvStructures->setModel(model);
ui.tvStructures->setItemDelegate(new ComboBoxDelegate());
for (int row = 0 ; row < 20; ++row)
{
for (int col = 0; col < 2; ++col)
{
QModelIndex index = model->index(row, col, QModelIndex());
model->setData(index, QVariant((row+1) * (col+1)));
}
}
Note that I placed this code inside the constructor of the parent QDialog where the control element is located. What I ended up is a 2-column table as expected but without any combo boxes. In fact when debugging the code I observe that the constructor of the delegate is called (during the new operation) but none of createEditor, setEditorData, setModelData or updateEditorGeometry every get called. I thought this may be due to the fact that some connection magic is overwriting triggers required to do the drawing but even if I remove all code that refers to the tvStructures QTreeView apart from what I have posted I still can't see any combo boxes.
What's missing?
Note that I'm using the somewhat outdated QT 4.7.1
Looks like you're missing a parent for new QComboBoxDelegate. You can use the QDialog you mentioned as parent.
Also: follow this lengthy example to make sure you're not missing anything else.

How to handle first tab size on formatting?

I create an extension, that format vs editor tabs in custom ways, using ITextParagraphPropertiesFactoryService class. Everything works just fine, expect the fact, that when user enter new line, ITextParagraphPropertiesFactoryService doesnt affect to the new line
For simplifying the problem, I create a new MEF project, add a format provider like this
[Export(typeof(ITextParagraphPropertiesFactoryService))]
[ContentType("text")]
[TextViewRole(PredefinedTextViewRoles.Document)]
internal class ElasticTabstopsProvider : ITextParagraphPropertiesFactoryService
{
/// <summary>
/// Creates an ElasticTabstopsFormatters for
/// the provided configuration.
/// </summary>
public TextParagraphProperties Create(IFormattedLineSource formattedLineSource, TextFormattingRunProperties textProperties,
IMappingSpan line, IMappingPoint lineStart, int lineSegment)
{
return new TextFormattingParagraphProperties(textProperties, 1);
}
}
And it changes all tabs width from my editor to 1. Great! This is what I want. But now when I press Enter(new line) new cursor is set under Main, however I expect tab widths to be 1.
After I start typing it goes to expected position.
The question is, how can I set new line empty line tab size?
I try to override ISmartIndentProvider, but seems vs ignore that value.
Debuger stops on breakpoint in method
int? GetDesiredIndentation(ITextSnapshotLine currentLine)
of ISmartIndent, but indent stays the same no matter what value I return...
There are at least two reasons why your ISmartIndentProvider's indent is being ignored:
First, there are lots of places with the current C# and VB language services where we explicitly set the caret position in response to certain keypresses. Enter is one of them. It's quite possible that in your scenario, we're explicitly setting the position. Short of disabling smart indenting in Tools > Options, there's nothing you can do to override that. Since you said you're getting a debugger hit in your ISmartIndentProvider, that's probably the issue here.
Second, if you're trying to define a ISmartIndentProvider for content type "text", yours won't get called if there is a language-specific provider. There's also another provider for "text" already (which calls the shimmed old language services) which might win over yours anyways.
To be honest, if you're trying to do something fancy where you don't want automatic indenting, then you really should just turn it off to ensure it's not getting in your way.

How to bind background of a column in silverlight during runtime?

I have a grid with bunch of columns in it. To start with this grid is build during run time in the code behind and not much code in XAML except the generic grid, so I can't bind the column background during design time. I have been reading various blogs, questions and found the following as the closest answer.
SolidColorBrush backgroundBrush = new SolidColorBrush();
Binding b = new Binding("BackGroundColor");
b.Converter = new ColorConverterForReadOnly(); //This converter return color based on parameter
b.ConverterParameter = data;
BindingOperations.SetBinding(backgroundBrush, SolidColorBrush.ColorProperty, b);
column.Background = backgroundBrush;
When I ran the code, the binding did not happen, I put a break point (in the first line) inside the converter to see if debug hits the converter at all and it did not hit the converter at all. If I would put
column.Background = new SolidColorBrush(Colors.Blue)
I can see the the column colors set to blue.
What am I missing in the binding that is not letting converter invoked?
Thanks,
The binding is targeting a "BackGroundColor" property. For the binding to be hit, the DataContext of the column control would need to be an object that has a "BackGroundColor" property.

Resources