Verifying the text of a comboBox - javafx

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");

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);

JavaFX TableView multiline tooltip with cell factory and bindings

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.

How to listen to visible changes to the JavaFX SceneGraph for specific node

We created a small painting application in JavaFX. A new requirement arose, where we have to warn the user, that he made changes, which are not yet persisted and asking him, if the user might like to save first before closing.
Sample Snapshot:
Unfortunately there are a lot of different Nodes, and Nodes can be changed in many ways, like for example a Polygon point can move. The Node itself can be dragged. They can be rotated and many more. So before firing a zillion events for every possible change of a Node object to the canvas I`d like to ask, if anyone might have an idea on how to simplify this approach. I am curious, if there are any listeners, that I can listen to any changes of the canvas object within the scene graph of JavaFX.
Especially since I just want to know if anything has changed and not really need to know the specific change.
Moreover, I also do not want to get every single event, like a simple select, which causes a border to be shown around the selected node (like shown on the image), which does not necessary mean, that the user has to save his application before leaving.
Anyone have an idea? Or do I really need to fire Events for every single change within a Node?
I think you are approaching this problem in the wrong way. The nodes displayed on screen should just be a visual representation of an underlying model. All you really need to know is that the underlying model has changed.
If, for example, you were writing a text editor, the text displayed on the screen would be backed by some sort of model. Let's assume the model is a String. You wouldn't need to check if any of the text nodes displayed on screen had changed you would just need to compare the original string data with the current string data to determine if you need to prompt the user to save.
Benjamin's answer is probably the best one here: you should use an underlying model, and that model can easily check if relevant state has changed. At some point in the development of your application, you will come to the point where you realize this is the correct way to do things. It seems like you have reached that point.
However, if you want to delay the inevitable redesign of your application a little further (and make it a bit more painful when you do get to that point ;) ), here's another approach you might consider.
Obviously, you have some kind of Pane that is holding the objects that are being painted. The user must be creating those objects and you're adding them to the pane at some point. Just create a method that handles that addition, and registers an invalidation listener with the properties of interest when you do. The structure will look something like this:
private final ReadOnlyBooleanWrapper unsavedChanges =
new ReadOnlyBooleanWrapper(this, "unsavedChanged", false);
private final ChangeListener<Object> unsavedChangeListener =
(obs, oldValue, newValue) -> unsavedChanges.set(true);
private Pane drawingPane ;
// ...
Button saveButton = new Button("Save");
saveButton.disableProperty().bind(unsavedChanges.not());
// ...
#SafeVarArgs
private final <T extends Node> void addNodeToDrawingPane(
T node, Function<T, ObservableValue<?>>... properties) {
Stream.of(properties).forEach(
property -> property.apply(node).addListener(unsavedChangeListener));
drawingPane.getChildren().add(node);
}
Now you can do things like
Rectangle rect = new Rectangle();
addNodeToDrawingPane(rect,
Rectangle::xProperty, Rectangle::yProperty,
Rectangle::widthProperty, Rectangle::heightProperty);
and
Text text = new Text();
addNodeToDrawingPane(text,
Text::xProperty, Text::yProperty, Text::textProperty);
I.e. you just specify the properties to observe when you add the new node. You can create a remove method which removes the listener too. The amount of extra code on top of what you already have is pretty minimal, as (probably, I haven't seen your code) is the refactoring.
Again, you should really have a separate view model, etc. I wanted to post this to show that #kleopatra's first comment on the question ("Listen for invalidation of relevant state") doesn't necessarily involve a lot of work if you approach it in the right way. At first, I thought this approach was incompatible with #Tomas Mikula's mention of undo/redo functionality, but you may even be able to use this approach as a basis for that too.

QML TableView access model properties from delegate

I have a TableView for which I've defined my own itemDelegate. Now, from within this delegate I can access the value for the column using styleData.value, but I'd also need to access the other properties in this same item but I can't find how to.
I need this, because the text styling needs to change depending on some other property of the item model.
Any ideas? thanks!
There is some documentation missing. Within the item delegate you can access the following (taken from the source code of TreeView.qml):
styleData (see documentation)
model (currently not documented)
modelData (currently not documented, not sure about this but I guess it's similar to ListView)
(By the way, what's also missing in the documentation but which is useful is styleData.role. Also, the documentation of the other delegates lacks some available properties too; the best is to peek into the source code of the QML file and have a look for the Loader element which instantiates your delegate. As a plus you learn how that creepy stuff works. ;))
With model and the row/column information you can then navigate to the item data. This code depends on the type of model.
If you're using QML's ListModel, then you can use model.get: model.get(styleData.row)[styleData.role] should then work (untested since I use it rarely, please give feedback).
If you're using a C++ QAbstractItemModel or friends, the best is to add a slot to the model class which takes just the row and role name, since that's the information the TableView works with (nor with role numbers nor with columns...).
However in both cases you shouldn't use the expression in a property binding! The notification system will not work since you don't use the property system for accessing the data. According to your question, I guess you wanted to use it in a expression with binding. I don't know how to properly listen to changes in the model manually.
An alternative approach is to access the other items of the row and provide a property there. Some hints:
From within one item, you can access other items of the same row by walking the object tree up twice (first to the Loader which instantiates your component, then to the actual row) and then down twice (first to the particular child object which is a Loader, then its instantiated item). You need to know the column number you want to access (not the role name), I assume you want to access the first column (index 0):
parent.parent.children[0].item
You can provide the model data using a property in each item. Assuming a simple Text element this might be:
Text {
property variant value: styleData.value // <-- Here you make it available
// your other stuff
}
Putting them together could look like the following. In this example I assume the first row contains an integer, and if it is zero, the second column should be red.
// (within TableView)
itemDelegate: Text {
property variant value: styleData.value
text: styleData.value
color: (styleData.column == 1 && parent.parent.children[0].item.value === 0)
"red" : "black"
}
I think it's pretty easy if you read the source code of TableViewItemDelegateLoader.qml (it is a private code in qtquickcontrol)
To access any role you use use : model[your_role_name] .
For exp: model["comment"]
Faced with same problem today, this is result of my investigations (Qt 5.2.x)
If you have hard limit to TableView, there is only one correct solution - use model.get(styleData.row)["roleForStyling"] as #leemes wrote. But it will very slow if you have big amount of data in model and using, for example, proxy model for sorting/filtering.
Direct solution from #leemes answer is great, but in general case not be working, because in TableView any Item wrapped in Loader and therefore independent from parent and other items:
When some item is created (where you want to change text style)
another element (from which to receive identity) cannot yet be
created
You may not have "parent" on item creation (i.e. binding will
be broken)
In my case, the best solution for deep customise was creation of the simple wrapper for ListView. In this case you have access for complete row data in delegate without the overhead. Highlights for making component ("My own ListView as table"):
Create standalone header (Rectangle or Item) - do not use header form ListView.This make it fixed for any amount of data.
Wrap ListView to ScrollView (if you need scrollbars)
Use Clip: true property in list for make correct
Set style for highlight and set highlightFollowsCurrentItem:true in ListView
As bonus in future this may be used for make "TreeTable" :)

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.

Resources