Skip to main content

JXTreeTable and ColumnFactory

13 replies [Last post]
seralph
Offline
Joined: 2010-03-18
Points: 0

Hi everyone,

as this is my first post in this forum I start off with a bit of background information. I'm mostly new to swingx and took over a project which makes heavy use of JXTreeTable. Unfortunately the customized TreeTable is coded against a 0.8.x version and so I have not even the source at hand :). The plan is to migrate to 1.6.x. This affects hundreds of classes. I'm not in hurry so.
The custom TreeTable supports sorting, grouping, column and row filtering and some other gimmicks; most implemented in terms of model changes. From looking at the code and the current implementation of JXTreeTable it's apparent a lot has changed over the curse of the last 5-6 years and so I might have some conceptual difficulties or misunderstandings, so bare with me.

createDefaultColumnsFromModel() became final, which leaves me with two problems:
1. This function was used as a hook to do other stuff as well, which is probably anyway discouraged and at a first glance probably not required anymore.
2. I have no access to my model during the column creation.

Now to the question at hand. Let's have a look at ColumnFactory to illustrate what I want to do. The following won't work due to access restrictions.

@Override
public TableColumnExt createAndConfigureTableColumn(TableModel model, int modelIndex) {
if (model instanceof JXTreeTable.TreeTableModelAdapter) {
final JXTreeTable.TreeTableModelAdapter adapter = (JXTreeTable.TreeTableModelAdapter) model;
final TreeTableModel treeModel = adapter.getTreeTable().getTreeTableModel();
if (treeTableModel instanceof MyTreeTableModel) {
MyTreeTableModel myModel = (MyTreeTableModel) treeTableModel;
........

So the actual question is am I supposed create a ColumnFactory per TreeTable and keep a reference of myModel in the ColumnFactory? That's what I currently do but is uglier than overwriting createDefaultColumnsFromModel().
Is this an known issue, an oversight or is the TableModel interface meant to be sufficient (nowhere near to in my case)? Other ideas on how to solve the issue?

Cheers

Reply viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
kleopatra
Offline
Joined: 2003-06-11
Points: 0

just to keep you informed:
meanwhile we had an issue covering the access to underlying TreeTableModel:
https://swingx.dev.java.net/issues/show_bug.cgi?id=1379
which is fixed, as of revision #3914 :-)
Please let me know if you use-case is covered now, thanks
Jeanette

kleopatra
Offline
Joined: 2003-06-11
Points: 0

your assumption is spot-on: ColumnFactory is _the_ to configure each and every TableColumn. The method createColumnFromModel was made final to emphasize that fact.

So overriding configureTableColumn is the way to go. It's responsibility is to configure the given column from the given tableModel (one-way-street). The idea is that the tableModel contains most of the information needed, that is actually all except config stuff it can't contain, f.i. like formats or other localizable properties. The demos and my incubator have examples about how I think custom ColumnFactory should be implemented.

That said: there is a slight probability that the use case of a TreeTable is not fully supported. The base question I see here is: why exactly do you need to access the underlying TreeTableModel? All it's content should be automatically available in the adapted TableModel. Hmmm... except the hierarchical nature ... maybe you hit a missing piece ...

As Karl already said: we definitely need an example to fully grasp what you want to do and why it doesn't work.

Cheers
Jeanette

seralph
Offline
Joined: 2010-03-18
Points: 0

> That said: there is a slight probability that the use
> case of a TreeTable is not fully supported. The base
> question I see here is: why exactly do you need to
> access the underlying TreeTableModel? All it's
> content should be automatically available in the
> adapted TableModel. Hmmm... except the hierarchical
> nature ... maybe you hit a missing piece ...
>

Another route I see is to simply ignore the ColumnFactory and installing my own TableColumnModel(Ext) which could be built according to my needs and would have the following additional advantages:
- arbitrary order of columns (persistance)
- arbitrary subset of columns (without mapping in the model)
- toggle visible state actions are limited to subset and so the ColumnControlPopup (or it's idea) remains usable.
This seems not a common technique either as JXTreeTable does not offer a constructor JXTreeTable(TreeTableModel model, ColumnModel columnModel).

> As Karl already said: we definitely need an example
> to fully grasp what you want to do and why it doesn't
> work.
>

Well, if implementing the above example of the threshold highlighter helps demonstrating whether it is an issue or not, I'll go ahead and do it. Am I right there are no code tags in this fora? I think I'll end up with around 200 lines of code and so it becomes completely unusable as unformatted text. What is the usual way to share code in here?

Cheers

kleopatra
Offline
Joined: 2003-06-11
Points: 0

>
> Another route I see is to simply ignore the
> ColumnFactory and installing my own
> TableColumnModel(Ext) which could be built according

sure, you can always start from scratch - best re-write the whole of SwingX (please don't forget to inform us when you are ready )

> to my needs and would have the following additional
> advantages:

> - arbitrary order of columns (persistance)

how's that related to persistance? JXTable has a method to rearrange the order.

> - arbitrary subset of columns (without mapping in the
> model)

that's definitely a no-go

> - toggle visible state actions are limited to subset
> and so the ColumnControlPopup (or it's idea) remains
> usable.

?

> This seems not a common technique either as
> JXTreeTable does not offer a constructor
> JXTreeTable(TreeTableModel model, ColumnModel
> columnModel).
>

personally, I never felt any need for that constructor. To start with, a cleanly separated (data vs view) application would never use a constructor with the data model. That's mere convenience for simple contexts. Simple story J/X/Table has more convenience constructors than JXTreeTable ;-) So if you insist on your own TableColumnModel - which I would fill with a custom ColumnFactory - the way to go is something like

[code]
JXTreeTable treeTable = new JXTreeTable();
treeTable.setAutoCreateColumnsFromModel(false);
treeTable.setColumnModel(myPrefilledColumnModel);
treeTable.setTreeTableModel(myTreeTableModel);
[/code]

beware: whatever you do in your custom column model - don't let it have a reference to the tree/table/model!

>
> Well, if implementing the above example of the
> threshold highlighter helps demonstrating whether it
> is an issue or not, I'll go ahead and do it. Am I
> right there are no code tags in this fora? I think
> I'll end up with around 200 lines of code and so it
> becomes completely unusable as unformatted text. What
> is the usual way to share code in here?
>

pretty sure that it's possible in less lines if you try hard enough. Don't forget: leave in only the lines which are absolutely needed.

you can format code by taggin it with [ code ] .... [ /code ] - without the spaces. Please complain to the site admin that this isn't documented anywhere ;-)

CU
Jeanette

seralph
Offline
Joined: 2010-03-18
Points: 0

> >
> > Another route I see is to simply ignore the
> > ColumnFactory and installing my own
> > TableColumnModel(Ext) which could be built
> according
>
> sure, you can always start from scratch - best
> re-write the whole of SwingX (please don't forget to
> inform us when you are ready )
>

The project I took over does exactly that. My goal is to greatly reduce the custom code. So it's not a question of how much I want to write but how much I want to tear down. ;)

> > to my needs and would have the following
> additional
> > advantages:
>
> > - arbitrary order of columns (persistance)
>
> how's that related to persistance? JXTable has a
> method to rearrange the order.

Only based on unique identifiers as far as I see which would need to be setup during column configuration. Column width and visibility state need to be restored as well.

>
> > - arbitrary subset of columns (without mapping in
> the
> > model)
>
> that's definitely a no-go
>
The tableColumns modelIndex would need to be within the range of model.getColumnCount(), but beside this restriction I do not see an issue right away. Removing columns from a valid columnModel should simply work. Whether it might make sense is an other story.

> > - toggle visible state actions are limited to
> subset
> > and so the ColumnControlPopup (or it's idea)
> remains
> > usable.
>
> ?
Well a ColumnControlPopup containing a few hundred toggle actions isn't what i call usable. However loading predefined or user configured subsets which typically contain 10-15 columns into the ColumnModel would result in reasonable popup. Maybe a custom popup should be considered.

>
> > This seems not a common technique either as
> > JXTreeTable does not offer a constructor
> > JXTreeTable(TreeTableModel model, ColumnModel
> > columnModel).
> >
>
> personally, I never felt any need for that
> constructor. To start with, a cleanly separated (data
> vs view) application would never use a constructor
> with the data model. That's mere convenience for
> simple contexts. Simple story J/X/Table has more
> convenience constructors than JXTreeTable ;-) So if
> you insist on your own TableColumnModel - which I
> would fill with a custom ColumnFactory
This is was I had in mind.

> - the way
> to go is something like
>
> [code]
> JXTreeTable treeTable = new JXTreeTable();
> treeTable.setAutoCreateColumnsFromModel(false);
> treeTable.setColumnModel(myPrefilledColumnModel);
> treeTable.setTreeTableModel(myTreeTableModel);
> [/code]
>
> beware: whatever you do in your custom column model -
> don't let it have a reference to the
> tree/table/model!
>
Thanks..

> >
> > Well, if implementing the above example of the
> > threshold highlighter helps demonstrating whether
> it
> > is an issue or not, I'll go ahead and do it. Am I
> > right there are no code tags in this fora? I think
> > I'll end up with around 200 lines of code and so
> it
> > becomes completely unusable as unformatted text.
> What
> > is the usual way to share code in here?
> >
>
> pretty sure that it's possible in less lines if you
> try hard enough. Don't forget: leave in only the
> lines which are absolutely needed.
>
If it needs to be runnable there is some inevitable boilerplate code.

> you can format code by taggin it with [ code ] .... [
> /code ] - without the spaces. Please complain to the
> site admin that this isn't documented anywhere ;-)

Here we go with a little less than 200 lines:

[code]
package demo;

import java.awt.Color;
import java.awt.Component;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.table.TableModel;
import javax.swing.tree.DefaultMutableTreeNode;

import org.jdesktop.swingx.JXTreeTable;
import org.jdesktop.swingx.table.ColumnFactory;
import org.jdesktop.swingx.table.TableColumnExt;
import org.jdesktop.swingx.treetable.AbstractTreeTableModel;
import org.jdesktop.swingx.treetable.TreeTableModel;
import org.jdesktop.swingx.decorator.ColorHighlighter;
import org.jdesktop.swingx.decorator.ComponentAdapter;
import org.jdesktop.swingx.decorator.HighlightPredicate;

public final class TreeTableDemo extends JFrame {

private TreeTableDemo() {
super("Demo");
getContentPane().add(new JScrollPane(createTreeTable()));
setSize(500, 600);
setLocationRelativeTo(null);
setDefaultCloseOperation(EXIT_ON_CLOSE);
}

private JXTreeTable createTreeTable() {
final JXTreeTable treeTable = new JXTreeTable();
final MyTreeTableModel model = new MyTreeTableModel();
treeTable.setColumnFactory(new MyColumnFactory(model));
treeTable.setTreeTableModel(model);
return treeTable;
}

public static void main(String[] args) {
SwingUtilities.invokeLater( new Runnable() {
public void run() {
(new TreeTableDemo()).setVisible(true);
}
});
}

class MyTreeTableModel extends AbstractTreeTableModel {

// Symbolized the column filtering due to user filters or service
// availability. Could also handle ordering. Currently all done by the
// integreted ColumnFactory/ColumnModel within myModel;
private int[] availableColumns;

// The data which needs to be available during TableColumn configuration
private int[] thresholds;

MyTreeTableModel() {
availableColumns = new int[] {1, 8, 32, 17};
thresholds = createDataRow();
root = createATree();
}

public int getThreshold(int column) {
return thresholds[availableColumns[column]];
}

private DefaultMutableTreeNode createATree() {
DefaultMutableTreeNode node =
new DefaultMutableTreeNode(createDataRow());
for (int i = 0; i < 10; i++) {
node.add(createSubTree());
}
return node;
}

private DefaultMutableTreeNode createSubTree() {
DefaultMutableTreeNode node =
new DefaultMutableTreeNode(createDataRow());
for (int i = 0; i < 100; i++) {
node.add(new DefaultMutableTreeNode(createDataRow()));
}
return node;
}

private int[] createDataRow() {
int[] row = new int[100];
for (int i = 0; i < 100; i++) {
row[i] = (int) (Math.random() * 100);
}
return row;
}

@Override
public int getColumnCount() {
return availableColumns.length;
}

@Override
public Object getValueAt(Object node, int column) {
if (node instanceof DefaultMutableTreeNode) {
final Object values = ((DefaultMutableTreeNode) node)
.getUserObject();
return ((int[])values)[availableColumns[column]];
}
return null;
}

@Override
public Object getChild(Object parent, int index) {
if (parent instanceof DefaultMutableTreeNode) {
return ((DefaultMutableTreeNode) parent).getChildAt(index);
}
return null;
}

@Override
public int getChildCount(Object parent) {
if (parent instanceof DefaultMutableTreeNode) {
return ((DefaultMutableTreeNode) parent).getChildCount();
}
return 0;
}

@Override
public int getIndexOfChild(Object parent, Object child) {
if (parent instanceof DefaultMutableTreeNode
&& child instanceof DefaultMutableTreeNode) {
return ((DefaultMutableTreeNode) parent)
.getIndex((DefaultMutableTreeNode) child);
}
return -1;
}

@Override
public boolean isLeaf(Object node) {
if (node instanceof DefaultMutableTreeNode) {
return ((DefaultMutableTreeNode) node).isLeaf();
}
return true;
}
}

class MyColumnFactory extends ColumnFactory {

MyTreeTableModel model;

MyColumnFactory(MyTreeTableModel model) {
this.model = model;
}

// Here is where the TableModel interface is insufficient
private void setThresholdHighlighter(MyTreeTableModel model,
TableColumnExt column)
{
ColorHighlighter highlighter = new ColorHighlighter(
new ThresholdPredicate(model.getThreshold(
column.getModelIndex())),
Color.RED,
null
);
column.setHighlighters(highlighter);
}

@Override
public void configureTableColumn(TableModel model,
TableColumnExt column)
{
// The following wont work (if it would, moving it to
//createAndConfigureTableColumn would be approprieate.
// if (model instanceof JXTreeTable.TreeTableModelAdapter) {
// final JXTreeTable.TreeTableModelAdapter adapter =
// (JXTreeTable.TreeTableModelAdapter) model;
// configureTableColumn(adapter.getTreeTable().
// getTreeTableModel(), column);
// }
// ... and so we cheat
if (this.model instanceof MyTreeTableModel) {
MyTreeTableModel myModel = (MyTreeTableModel) this.model;
setThresholdHighlighter(myModel, column);
}
}
}

class ThresholdPredicate implements HighlightPredicate {

private final int threshold;

ThresholdPredicate(int threshold) {
this.threshold = threshold;
}

@Override
public boolean isHighlighted(Component renderer,
ComponentAdapter adapter)
{
if (adapter.getValue() instanceof Integer) {
final int value = (Integer) adapter.getValue();
if (threshold < value) {
return true;
}
}
return false;
}
}
}

[/code]

Cheers
Ralph

Message was edited by: seralph

Message was edited by: seralph

kleopatra
Offline
Joined: 2003-06-11
Points: 0

Ralph,

okay, I see what you'r doing. And it's a bit as I guessed: I would say the problem is in the modeling. The base question is, what's the nature of the threshold property: is it more data or more decoration? A way to handle the former is to expose it as a column in the treeTableModel, this guarantees that the value is available in the TableModel. Then don't create a tableColumn for that model column and let the highlighter check for the value of the model column. A way to handle the latter is to have a separate map of modelColumnIndex to threshold value and feed the map into the ColumnFactory. (You did look into the example in the demos, didn't you )

CU
Jeanette

PS: would you please edit your post and format the code such that it doesn't exceed a "normal" width (I think the one comment line explaining the treeTableAdapter is the offender). Thanks.

seralph
Offline
Joined: 2010-03-18
Points: 0

Janette,

> Ralph,
>
> okay, I see what you'r doing. And it's a bit as I
> guessed: I would say the problem is in the modeling.
> The base question is, what's the nature of the
> threshold property: is it more data or more
> decoration?

It's metadata and part of a ColumnInformation object. There is an algorithm in place which tries hard to set up all properties of TableColumn based on this info object. Only very rarely it should be required to explicitly set the properties as I did in the above example.

> A way to handle the former is to expose
> it as a column in the treeTableModel, this guarantees
> that the value is available in the TableModel. Then
> don't create a tableColumn for that model column and
> let the highlighter check for the value of the model
> column.

This is indeed a workaround worth thinking about.

> A way to handle the latter is to have a
> separate map of modelColumnIndex to threshold value
> and feed the map into the ColumnFactory.

This is exactly the approach of one ColumnFactory per TreeTable. This might indeed be the right thing to do. I have to figure out what I would gain and what I would lose.

> (You did
> look into the example in the demos, didn't you )

*hide*

Well the 1.6.1 tarball is missing the demos and 1.6.2 doesn't compile here. Didn't got around to fix that yet. I came across 2 custom implementations during some research about the "swingx way", so no idea how they would be seen in here.

>
> CU
> Jeanette
>
> PS: would you please edit your post and format the
> code such that it doesn't exceed a "normal" width (I
> think the one comment line explaining the
> treeTableAdapter is the offender). Thanks.

Oh, different tabstop size, sure I'll try to fix it.

Cheers
Ralph

kleopatra
Offline
Joined: 2003-06-11
Points: 0

Ralf,

> Janette,
>

> It's metadata and part of a ColumnInformation object.
> There is an algorithm in place which tries hard to
> set up all properties of TableColumn based on this
> info object. Only very rarely it should be required
> to explicitly set the properties as I did in the
> above example.
>

thanks for the clarification - so it's not changing dynamically?

> > A way to handle the former is to expose
> > it as a column in the treeTableModel, this
> guarantees
> > that the value is available in the TableModel.
> Then
> > don't create a tableColumn for that model column
> and
> > let the highlighter check for the value of the
> model
> > column.
>
> This is indeed a workaround worth thinking about.
>

could be, but the more I think about it the less I like it ;-) Reason is that it's very artificial: it's per-column-for-all-other-columns stored in one other column, same value for all rows. That smells ...

Another thingy that smells is that the info you need is available to the Highlighter/Predicate (access the view, typecast to JXTreeTable and grab its TreeTableModel) but not at config time. For consistency, both should have the same opportunities.

We had long debates about hiding the modelAdapter (or better: the other way round, it was always private and stayed that way), so I would hate to change that now on the fly. Hmm ... in future's (near or far?) JXXTreeTable the model XOutlineModel is both a TreeModel and a TableModel. And the default implemenation wraps a TreeTableModel which is exposed - on the whole would fit your config needs. Requires more thinking.

> > A way to handle the latter is to have a
> > separate map of modelColumnIndex to threshold
> value
> > and feed the map into the ColumnFactory.
>
> This is exactly the approach of one ColumnFactory per
> TreeTable. This might indeed be the right thing to
> do. I have to figure out what I would gain and what I
> would lose.
>

one columnFactory per table is not bad as such - only its reference to the model is evil :-)

Cheers
Jeanette

seralph
Offline
Joined: 2010-03-18
Points: 0

Janette,

> thanks for the clarification - so it's not changing
> dynamically?

Most rarely during the lifetime of the Table and can so be seen as constant here.

> >
> > This is indeed a workaround worth thinking about.
> >
>
> could be, but the more I think about it the less I
> like it ;-) Reason is that it's very artificial: it's
> per-column-for-all-other-columns stored in one other
> column, same value for all rows. That smells ...
>
> Another thingy that smells is that the info you need
> is available to the Highlighter/Predicate (access the
> view, typecast to JXTreeTable and grab its
> TreeTableModel) but not at config time. For
> consistency, both should have the same
> opportunities.

As you say it's quite artificial and it can be circumvented in an imho more elegant way by setting a prefilled ColumnModel on the table.

> We had long debates about hiding the modelAdapter (or
> better: the other way round, it was always private
> and stayed that way), so I would hate to change that
> now on the fly. Hmm ... in future's (near or far?)
> JXXTreeTable the model XOutlineModel is both a
> TreeModel and a TableModel. And the default
> implemenation wraps a TreeTableModel which is exposed
> - on the whole would fit your config needs. Requires
> more thinking.

This issue alone probably doesn't rectify this. However there are other places where the signature might be an issue, eg. .setRowSorter(RowSorter sorter). Still using the implemented model based row sorting and filtering so I can't say for sure. The transition if at all is scheduled for after migrating and properly splitting up renderer and highlighter. Btw. I really like the Icon-/StringValue and ComponentProvider approach for renderers.

I guess you have other reasons in mind as well. Does there exist a prototype or a listing of pros and cons?

> > This is exactly the approach of one ColumnFactory
> per
> > TreeTable. This might indeed be the right thing to
> > do. I have to figure out what I would gain and what
> I
> > would lose.
> >
>
> one columnFactory per table is not bad as such - only
> its reference to the model is evil :-)

The problem here is that if I do not reference the model directly or indirectly I would have to duplicate information as otherwise there is no chance to get the mapping right based on the TableModel interface and changes to the model would require changes in the factory. So this is out of question from my current point of view.

So for now I stick with the equivalent of prefilling the ColumnModel. Thanks for your help to get some thoughts straight.

Cheers
Ralph

kschaefe
Offline
Joined: 2006-06-08
Points: 0

What's the reason that you need to that TreeTableModel in that location? Where Is was expecting to see how you are using it to give the best feedback, I simply see ellipses.

Karl

seralph
Offline
Joined: 2010-03-18
Points: 0

Well let's say I want to set highlighters for a tableColumn where the highlighters need actual knowledge about the meaning of the data which might be represented by an int. Same for renderer, editor, comparator and other properties of a tableColumn. There are many hundreds of columns, for most the data is represented by a primitive.

As the ColumnFactory is supposed to be the central place to create and configure TableColumnS me thinks this is the right place to do it.

A more concrete example would be a threshold highlighter which is supposed to set the background color to red if the double is above the threshold. The tree tables leaf nodes represent a single point in time for different sensors outputting a double. Each sensor has it's column assigned and a different threshold value.
In this case where else would you setup the highlighter if the knowledge of the threshold is anyway present in the model and column specific?

kschaefe
Offline
Joined: 2006-06-08
Points: 0

TableColumnExt does support per-column highlighters, so you can configure some of that information there and/or you can add highlighters for the table in general. It is the HighlightPredicate that will determine if something is highlighted; predicates can use any logic you want to return a true/false. That includes introspecting indices, cell values, column data types, etc. Just by your description, something feels a bit off with what you are describing, but I can't put my finger on it.

Can you provide a small, runnable version of what you are doing? Getting down to a few columns to demonstrate the issue will help me better understand what your problem is.

Karl

seralph
Offline
Joined: 2010-03-18
Points: 0

> TableColumnExt does support per-column highlighters,
> so you can configure some of that information there
> and/or you can add highlighters for the table in
> general.

When do I do it if not at the time of createAndConfigureTableColumn()
Adding them to the table for either the hole table or per column class won't do.
Looking at the constructors available for TableColumnExt gives another reason why
I believe this to be the right place.

> It is the HighlightPredicate that will
> determine if something is highlighted; predicates can
> use any logic you want to return a true/false. That
> includes introspecting indices, cell values, column
> data types, etc.

Sure it's the predicate but as far as I see they are closely tied. I mean by
threshold highlighter a color highlighter with an individually loaded threshold
predicate.
Neither cell value(double) nor column types(Double) hold the required information(threshold).
Indices only make sense in the context of the model so it seems.
Maybe somewhere in the etc. is the solution.

> Just by your description, something
> feels a bit off with what you are describing, but I
> can't put my finger on it.
>
I try to reduce the problem as good as I can. The actual app does delegate the creation to the
column factory contained in the model. This column factory makes extensive use of the model.
One can probably say the model is overloaded and I intend to shift some of it responsibilities
where appropriate. Here comes the threshold example into play. Even so it's imaginary it's far
simpler than the actual problem and would help me understand what the appropriate place is.

> Can you provide a small, runnable version of what you
> are doing? Getting down to a few columns to
> demonstrate the issue will help me better understand
> what your problem is.
>
> Karl

Well, I don't think this is of any help as of yet as I first have to figure out what I have to demonstrate. Basically I want to set every property of TableColumnExt during createAndConfigureTableColumn(). Those properties can often not be deduced from the TableColumn interface. Where else to obtain the additional information if not from the model?