I have a class which is inherited from QLineEdit, and I set an icon as an action button for this.
MyEdit::MyEdit( QWidget *p_parent ) : QLineEdit( p_parent )
{
m_buttonAction = addAction( QIcon( "search.png" ), QLineEdit::TrailingPosition );
QAbstractButton *button = qobject_cast<QAbstractButton *>( m_buttonAction->associatedWidgets().last() );
m_buttonAction->setVisible( false );
connect( m_buttonAction, &QAction::triggered, this, &MyEdit::openCompleter );
m_completer = new QCompleter( this );
m_sourceModel = new CompleterSourceModel( m_completer );
m_view = new CompleterView();
m_view->setStyle( &m_style );
m_delegate = new CompleterDelegate( m_view );
m_completer->setPopup( m_view );
m_completer->setModel( m_sourceModel );
m_view->setItemDelegate( m_delegate );
setCompleter( m_completer );
}
void MyEdit::setDataForCompleter( const CompleterData &p_data )
{
m_sourceModel->setCompleterData( p_data );
m_buttonAction->setVisible( p_data.data().size() > 0 );
}
When I import data for completer, the icon is always shown. Now I need to hide this icon in case MyEdit is disabled or as ReadOnly.
I am thinking about override setDisabled and setReadOnly for my class, and in there setVisible for the icon. But these functions are not virtual, so can not be overridden.
I am thinking also about a signal like stateChanged of my class, so I can do it in a slot. But I can not find any signal like that for QLineEdit. Do you have any idea how to do it?
You can handle events QEvent::ReadOnlyChange or QEvent::EnabledChange by overriding the QLineEdit::event method
UPDATE:
Here is an example implementation:
bool MyEdit::event(QEvent *e) override {
const auto type = e->type();
if (type == QEvent::ReadOnlyChange || type == QEvent::EnabledChange) {
m_buttonAction->setVisible(m_sourceModel->rowCount() > 0 ? isEnabled() && isReadOnly() : false);
}
return QLineEdit::event(e);
}
Related
I'm trying to create an API, which can dynamically populate tableview with different Nested UI components in each cell of tableview. I'm able to bind the data set to Model object with the table.
The problem is when I try to dynamically add and try to enable the edit, the object reference is seems to messed up.
FYI:
As you see I've last column with 4 buttons, which is Add, Edit, Delete, Reset features. Once click on Add - it clones the current row, click on edit - it enable the ComboBox of Coulmn Category, click on delete - it deletes current row.
What I face is that upon adding multiple entries, I do get the row added dynamically, but then click on first row edit button - then multiple ComboBox is enabled, which is not intended use. The use case is current row's ComboBox must be only be enabled.
Implementation: I've written custom API which extends TableView<S>.
Following snippet may help:
//column category
final ClumpElement< ConstraintsDataModel, String > categoryElement =
new ClumpElement<>( ClumpType.COMBOBOX, true, getCategoryData() );
categoryElement.setClumpTableCellValue( data -> data.categoryProperty() );
categoryElement.setClumpTableNodeAction( ( control, data ) -> {
final ComboBox< String > comboBox = (ComboBox< String >)control;
comboBox.disableProperty().bind( data.disableProperty() );
} );
clumpTableView.addNewColumn( "Category", categoryElement );
// column Action
final ClumpElement< ConstraintsDataModel, String > buttonsElement =
new ClumpElement<>( ClumpType.GROUP_BUTTONS, 4, "+", "✎", "X", "↻" );
buttonsElement.setClumpTableNodeAction( ( control, data ) -> {
final Button button = (Button)control;
switch( button.getText() ) {
case "+":
final ConstraintsDataModel ref =
clumpTableView.getItems().get( clumpTableView.getItems().size() - 1 );
if( ConstraintsDataModel.isValidModel( ref ) )
clumpTableView.getItems().add( new ConstraintsDataModel( data ) );
else
System.out.println( "ERROR: Finish previous constraints" );
break;
case "✎":
data.setDisableValue( false );
button.setText( "✔" );
break;
case "✔":
data.setDisableValue( true );
button.setText( "✎" );
break;
default:
//NOTHING
break;
}
} );
clumpTableView.addNewColumn( "Action", buttonsElement );
clumpTableView.setItems( getData() );
This is my CustomTableView class:
public < T > void addNewColumn( final String columnName, final ClumpElement< S, T > element ) {
final TableColumn< S, T > column = new TableColumn<>( columnName );
getColumns().add( column );
if( element.getClumpTableCellValue() != null ) {
column.setCellValueFactory( param -> element.getClumpTableCellValue()
.act( param.getValue() ) );
}
clumpCellCall( columnName, element, column );
}
private < T > void clumpCellCall( final String colName, final ClumpElement< S, T > element,
final TableColumn< S, T > column ) {
switch( element.getUiNode() ) {
case COMBOBOX:
if( element.getItems() != null && !element.getItems().isEmpty() ) {
column.setCellFactory( param -> {
final ClumpComboBoxTableCell< S, T > clumpComboBoxTableCell =
new ClumpComboBoxTableCell<>( element.isDisable(), element.getItems() );
clumpComboBoxTableCell.prefWidthProperty().bind( column.widthProperty() );
clumpComboBoxTableCell.selectionListener( element );
return clumpComboBoxTableCell;
} );
}
break;
case GROUP_BUTTONS:
column.setCellFactory( param -> {
final ClumpButtonsTableCell< S, T > clumpButtonsTableCell =
new ClumpButtonsTableCell<>( element.getNoOfElements() );
clumpButtonsTableCell.prefWidthProperty().bind( column.widthProperty() );
IntStream.range( 0, element.getNoOfElements() ).forEach( item -> {
final Button button = clumpButtonsTableCell.getButtons().get( item );
button.setText( element.getNames().get( item ) );
button.setOnAction( event -> {
if( element.getClumpTableNodeAction() != null
&& clumpButtonsTableCell.getIndex() < getItems().size() ) {
element.getClumpTableNodeAction()
.act( button, getItems().get( clumpButtonsTableCell.getIndex() ) );
}
} );
} );
return clumpButtonsTableCell;
} );
break;
default:
column.setCellFactory( params -> {
final TextFieldTableCell< S, T > textFieldTableCell = new TextFieldTableCell<>();
textFieldTableCell.setConverter( new StringConverter< T >() {
#Override
public String toString( final T object ) {
return (String)object;
}
#Override
public T fromString( final String string ) {
return (T)string;
}
} );
return textFieldTableCell;
} );
break;
}
}
In my custom API, which shall invoke a custom TableCell<S,T> which has ComboBox<T> pretty standard implementation as per docs. Here its inside a selection listener, as I found that when the cell renders, only this selection listener is called.
public abstract class AbstractClumpTableCell< S, T > extends TableCell< S, T > {
public AbstractClumpTableCell() {
setContentDisplay( ContentDisplay.GRAPHIC_ONLY );
setAlignment(Pos.CENTER);
}
public abstract void renewItem( T item );
#Override
protected void updateItem( T item, boolean empty ) {
super.updateItem( item, empty );
if( empty ) {
setGraphic( null );
} else {
renewItem( item );
}
}
}
public class ClumpComboBoxTableCell< S, T > extends AbstractClumpTableCell< S, T > {
private final ComboBox< T > comboBox;
#SuppressWarnings( "unchecked" )
public ClumpComboBoxTableCell( final boolean isDisable, final ObservableList< T > item ) {
super();
this.comboBox = new ComboBox<>( item );
this.comboBox.setDisable( isDisable );
this.comboBox.valueProperty().addListener( ( obs, oVal, nVal ) -> {
ObservableValue< T > property = getTableColumn().getCellObservableValue( getIndex() );
if( property instanceof WritableValue ) {
((WritableValue< T >)property).setValue( nVal );
}
} );
}
#Override
public void renewItem( T item ) {
comboBox.setValue( item );
setGraphic( comboBox );
}
public ComboBox< T > getComboBox() {
return comboBox;
}
protected void selectionListener( final ClumpElement< S, T > element ) {
this.comboBox.getSelectionModel().selectedItemProperty().addListener( ( obs, oVal, nVal ) -> {
if( element.getClumpTableNodeAction() != null
&& getIndex() < getTableView().getItems().size() ) {
element.getClumpTableNodeAction().act( this.comboBox,
getTableView().getItems().get( getIndex() ) );
}
} );
}
}
And my Data Model has a SimpleStringProperty that is binded to the column accordingly.
So, How can I bind the Nested UI elements correctly rowise within the TableView<S>? Is my approach right or is there alternatives?
I will make an attempt to answer, but as I said the code is hard for me to follow (especially as it is partial, so some methods I can only assume the purpose of).
The issue, as stated in the comments, is node virtualization in TableView. You can't go around it, and you really don't want to - it is a means to vastly improve performance, as you don't need hundreds or thousands of UI nodes (which are "heavy" and degrade performance), but only enough to fill the displayed portion of the table, thus supporting a much larger dataset.
The problem, as far as I can see, as that you have some property of the row (is it currently editable or not) which you need to be reflected in certain columns. More specifically, you want the combo box's disable property to always reflect the disable property of the row it pertains to, so in updateItem you will have to do something like this:
#Override
protected void updateItem(T item, boolean empty) {
super.updateItem(T, empty);
if (empty) {
setGraphic(null);
} else {
renewItem(item);
// since the disable property if given by the row value, not only the column value
// we need to get the row value. The cast is needed due to a design oversight
// in JavaFX 8, which is fixed in newer versions. See https://bugs.openjdk.java.net/browse/JDK-8144088
ConstraintsDataModel data = ((TableRow<ConstraintsDataModel>)getTableRow())
.getItem();
combobox.disableProperty().unbind();
combobox.disableProperty().bind(data.disableProperty());
}
}
This is assuming your row data type is indeed ConstaintDataModel, I couldn't quite follow through.
Another option which may be more elegant is to use the editing property of a row - bind the combo box's disable property to the negation of the editing property of the row, and use startEdit and cancelEdit/commitEdit when you start and end editing. This way you will not have to re-bind the disable property of the combo box, as it will always refer to the correct row.
I'm trying to use a ListView in my application, which has to run on a Windows tablet. The problem is that, to select multiple item on a ListView, the user has to maintain the CTRL key pressed, which is impossible on a tablet.
So my question is : Is there a way to select multiple item in a ListView with a simple click on it?
You may filter the normal mouse Click event and convert it to Ctrl+Click. Actually to Shortcut+Click since the shortcut key may differ on the platform the app run.
EventHandler<MouseEvent> eventHandler = ( event ) ->
{
if ( !event.isShortcutDown() )
{
Event.fireEvent( event.getTarget(), cloneMouseEvent( event ) );
event.consume();
}
};
listview.getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE );
listview.addEventFilter( MouseEvent.MOUSE_PRESSED, eventHandler );
listview.addEventFilter( MouseEvent.MOUSE_RELEASED, eventHandler );
where cloneMouseEvent is
private MouseEvent cloneMouseEvent( MouseEvent event )
{
switch (Toolkit.getToolkit().getPlatformShortcutKey())
{
case SHIFT:
return new MouseEvent(
event.getSource(),
event.getTarget(),
event.getEventType(),
event.getX(),
event.getY(),
event.getScreenX(),
event.getScreenY(),
event.getButton(),
event.getClickCount(),
true,
event.isControlDown(),
event.isAltDown(),
event.isMetaDown(),
event.isPrimaryButtonDown(),
event.isMiddleButtonDown(),
event.isSecondaryButtonDown(),
event.isSynthesized(),
event.isPopupTrigger(),
event.isStillSincePress(),
event.getPickResult()
);
case CONTROL:
return new MouseEvent(
event.getSource(),
event.getTarget(),
event.getEventType(),
event.getX(),
event.getY(),
event.getScreenX(),
event.getScreenY(),
event.getButton(),
event.getClickCount(),
event.isShiftDown(),
true,
event.isAltDown(),
event.isMetaDown(),
event.isPrimaryButtonDown(),
event.isMiddleButtonDown(),
event.isSecondaryButtonDown(),
event.isSynthesized(),
event.isPopupTrigger(),
event.isStillSincePress(),
event.getPickResult()
);
case ALT:
return new MouseEvent(
event.getSource(),
event.getTarget(),
event.getEventType(),
event.getX(),
event.getY(),
event.getScreenX(),
event.getScreenY(),
event.getButton(),
event.getClickCount(),
event.isShiftDown(),
event.isControlDown(),
true,
event.isMetaDown(),
event.isPrimaryButtonDown(),
event.isMiddleButtonDown(),
event.isSecondaryButtonDown(),
event.isSynthesized(),
event.isPopupTrigger(),
event.isStillSincePress(),
event.getPickResult()
);
case META:
return new MouseEvent(
event.getSource(),
event.getTarget(),
event.getEventType(),
event.getX(),
event.getY(),
event.getScreenX(),
event.getScreenY(),
event.getButton(),
event.getClickCount(),
event.isShiftDown(),
event.isControlDown(),
event.isAltDown(),
true,
event.isPrimaryButtonDown(),
event.isMiddleButtonDown(),
event.isSecondaryButtonDown(),
event.isSynthesized(),
event.isPopupTrigger(),
event.isStillSincePress(),
event.getPickResult()
);
default: // well return itself then
return event;
}
}
I'm using QTreeWidget with icons and strings data.
Right now I'm using my own filter function to filter out string via QTreeWidget::findItems function and it's works pretty good.
BUT, how can I filter/findItems by icons ?
The Qt findItems functions can get only strings as input...
Any suggestions ?
Try this:
QTreeWidgetItem* findByIcon( QTreeWidget* aTreeWidget, const QIcon& aIcon, const int aColumn = 0 )
{
QTreeWidgetItemIterator iterator( aTreeWidget );
while ( *iterator )
{
if ( (*iterator)->data( aColumn, Qt::DecorationRole ) == aIcon )
{
return (*iterator);
}
++iterator;
}
return nullptr;
}
This will find the first match only, is it enough ?
If not then here is a function which finds all the items with the given icon:
QList< QTreeWidgetItem* > findByIcon( QTreeWidget* aTreeWidget, const QIcon& aIcon, const int aColumn = 0 )
{
QList< QTreeWidgetItem* > items;
QTreeWidgetItemIterator iterator( aTreeWidget );
while ( *iterator )
{
if ( (*iterator)->data( aColumn, Qt::DecorationRole ) == aIcon )
{
items << (*iterator);
}
++iterator;
}
return items;
}
Generally I can get this to work no problem when I reimplement QTableView::mousePressEvent( QMouseEvent* ). However, doing it on QHeaderView is not working for me. Code is simple.
void my_header_t::mousePressEvent( QMouseEvent* event )
{
if ( !event ) {
return;
}
if ( event->button() == Qt::RightButton ) {
QPoint point( event->x(), event->y() );
QModelIndex index = indexAt( point );
printf( "%s data %s %d,%d %s (point: %d,%d )\n",
ts().c_str(), index.data().toString().toStdString().c_str(),
index.row(), index.column(), index.isValid() ? "True" : "False",
event->x(), event->y() );
handle_right_click( index.data().toString() );
} else {
QHeaderView::mousePressEvent( event );
}
x() and y() from the QMouseEvent are fine. However, it creates an invalid index, with row() of -1, and column() of -1. Obviously, I'm passing an empty string to handle_right_click() which kicks off a menu. That menu is not going to know which column called it, and the mayhem will further ensue.
I know that clicked( const QModelIndex& ) will just tell me the right index, with the text. However, I need to differentiate between buttons.
QHeaderView provides an alternative function, logicalIndexAt, for determining the index of the header item that you're interested in. Using your code from above:
void my_header_t::mousePressEvent( QMouseEvent* event )
{
if ( !event ) {
return;
}
if ( event->button() == Qt::RightButton ) {
int index = logicalIndexAt( event->pos() );
handle_right_click(model()->headerData(index, Qt::Horizontal).toString());
} else {
QHeaderView::mousePressEvent( event );
}
}
Note that the orientation of the header must be passed to the headerData method (in this case, I've just assumed that it's Qt::Horizontal, but in your case it might be something different).
Here what I have got:
a QTreeView widget (*);
Source model MainModel inherits from QStandardItemModel. No virtual data() const method reimplemented;
Proxy MainFilterProxyModel inherits from QSortFilterProxyModel;
The tree:
[PERIOD 1]
[CHILD 1]
[CHILD 2]
[SUBCHILD 2.1]
...
[CHILD N]
[PERIOD 2]
...
[PERIOD N]
So the main problem comes when I'm trying to add a CHILD-row like (**) code do. Filter proxy model, after document was added to source model, does not know about new row and didn't show it on the tree.
I'm sure that proxy didn't get signal from QStandardItemModel when appendRow method do his job, so proxy can't filter new row, and didn't make it visible.
Any help?
Thanks.
PS: If I turn off proxy, everything appended just fine. But the problem is not in proxy. Proxy just doesn't get the signal about new row appended to the main source model...
(*) Here is QTreeView:
MainView::MainView( QWidget* parent /* = 0 */ ) : QTreeView( parent )
{
if( !model_ )
{
model_ = new MainModel( this );
}
if( !proxy_ )
{
proxy_ = new MainFilterProxyModel( this );
proxy_->setDynamicSortFilter( true );
proxy_->setSourceModel( model_ );
setModel( proxy_ );
}
}
(**) Here is my append function:
void MainModel::addRow( const DocumentPtr& document, QStandardItem* parentItem )
{
assert( document );
QList< QStandardItem* > items;
items << ( new QStandardItem );
items << ( new QStandardItem );
items << ( new QStandardItem );
items << ( new QStandardItem );
items << ( new QStandardItem );
items << ( new QStandardItem );
items << ( new QStandardItem );
updateRow( document, items );
if( !parentItem )
{
BOOST_FOREACH( const TimePeriod& period, TimePeriod::all() )
{
if( period.contains( QDateTime::fromTime_t( document->creationDate() ) ) )
{
QStandardItem* periodItem = itemByPeriod( period );
Q_ASSERT( periodItem );
periodItem->appendRow( items );
break;
}
}
}
else
{
parentItem->appendRow( items );
}
}
The base class for modeling is QAbstractItemModel. It is better to use the methods of the abstract class to do what you want. QStandardItemModel is a simple implementation of the abstract methods of QAbstractItemModel and most of the QStandardItemModel new methods are used by the reimplemented abstract functions. Here is a code to add item and subitem using the abstract class methods:
QAbstractItemModel * pModel = new QStandardItemModel(parent);
int nRows = pModel->rowCount();
pModel->insertRow(nRows); // this will emit rowsAboutToBeInserted() and rowsInserted() signals
pModel->insertColumn(0); // this will emit columnsAboutToBeInserted() and columnsInserted() signals
const QModelIndex indexFirstItem = pModel->index(nRows, 0);
pModel->setData(indexFirstItem, "Item text"); // this will emit dataChanged() signal
int nChildRows = pModel->rowCount(indexFirstItem);
pModel->insertRow(nChildRows, indexFirstItem); // this will emit rowsInserted()
pModel->insertColumn(0, indexFirstItem); // we also need to do this for the item's children
const QModelIndex indexChild = pModel->index(nChildRows, 0, indexFirstItem);
pModel->setData(indexChild, "Child item text");
If we try to do the same using QStandardItemModel methods it will look like:
QStandardItemModel *pModel = new QStandardItemModel(parent);
QStandardItem *pItem = new QStandardItem("Item text");
pItem->appendRow(new QStandardItem); // pItem is not yet added to pModel and rowsInserted won't be emitted
pModel->appendRow(pItem); // this will probably emit rowsInserted() signal but since we set tha text of the item when creating the pItem the dataChanged() signal won't be emitted.
So if you do pItem->appendRow() to add sub items and if the pItem is not yet added to the model, you will probably not get the rowsInserted() signal and therefore the proxy model won't be notified. From my experience the first method works better and is more robust though you need to write a couple of extra lines. Working directly with QStandardItemModel methods will often end with missing signals or other headaches. All you need to look is the QAbstractItemModel and QModelIndex documentations.