QListView & QStandardItemModel - Prevent duplicates - qt

What is the way to go to prevent duplicates in a QListView which uses QStandardItemModel as its model? Data is added with drag & drop, so I tried to override QStandardItemModel::dropMimeData, which seems kind of odd since I need to override QStandardItemModel::mimeData (and reimplement encodeData/decodeData) as well. This has to be easier!

Well, I managed to solve this by overriding QListView::dataChanged, which checks if there is more than one item with the same data for Qt::DisplayRole in the model after dropping and deletes one of them if there is. It basically looks like that:
void MyListView::dataChanged(QModelIndex topLeft, QModelIndex bottomRight)
{
// there can be only one item dragged at once in my use case
if(topLeft == bottomRight)
{
QStandardItemModel* m = static_cast<QStandardItemModel*>(model());
// if theres already another item with the same DisplayRole...
if(m->findItems(topLeft.data().toString()).count() > 1)
{
// ... we get rid of it.
model()->removeRow(topLeft.row());
}
}
else
{
// let QListView decide
QListView::dataChanged(topLeft, bottomRight);
}
}
It's by far not perfect (e.g. if you can drop more than one item at once) but it works for that simple use case.

The easiest way I can see would be to create your own proxy model.
See http://doc.qt.io/qt-5/qabstractproxymodel.html

Related

How to share a model's item between a QML ListView and a Grid

I would like to know the best way to achieve the following thing in QML:
I have a ListView with droppable elements and a Grid initialy filled with DropArea. The ListView uses a model derived from QAbstractItemModel. I would like to drop an element on the grid and interact with it (rename it for instance). For now, any modifications in the ListView update the model, but how modifications of the element in the grid could update the model ?
There can be multiple items dropped in the grid corresponding to a subset of ListView's model. I do not know how can I achieve this. The Package can not be used because the Grid is not a GridView and Items must be moved/set at specific positions. So I tried to:
create a ListView displayed on the dropped item, using the same model as the source ListView used to drag items,
set the same rootIndex, then the same index
I am close to a solution but I think it is not the best way to do this.
Any clue ?
Thanks
I would like to have different visual representation of the same model
item in a ListView and in a component in a Grid. So, a modification of
the item in the ListView should update the item in the Grid and
vice-versa.
If your model's data is not QObject derived with NOTIFY and properties, notifications of changes can only be made through the model.
And since you won't be using the model for the grid, that means your model has to use underlying QObject instances. A generic model object might come in handy.
When you have that you only need to reference the underlying QObject model item and use bindings to set up your list view delegate. Then when you drag and drop, you only need to pass a reference to the QObject to that other visual representation which you will be creating inside the grid.
Lastly, take care not to be left with dangling references when you remove items from the model, as that will most likely hard-crash your application. Use the onDestruction attached signal to clear elements from the grid as the model item objects are destroyed.
I finally found a solution by create a C++ type which inherits from QObject an which can be embedded in a QML object. This type has read/write properties and is initialized with the same model as the ListView. The interesting methods are:
/* writing to a property **from QML** goes here */
void ModelItem::setName(const QString& name)
{
setModelData(GroupMemberModel::Nom, name);
}
/* then here */
bool ModelItem::setModelData(GroupMemberModel::Role role, const QVariant& value)
{
return m_model->setData(m_modelIndex, value, role);
}
/* any changes in the model fall here (signals/slots mecanism)*/
void ModelItem::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector<int>& roles)
{
if(m_modelIndex.row() < topLeft.row() || m_modelIndex.row() > bottomRight.row())
return;
if(m_modelIndex.column() < topLeft.column() || m_modelIndex.column() > bottomRight.column())
return;
//Index is modified, emit signal
foreach(int role, roles) {
emitDataChanged(role);
}
}
/* **notify QML** by emit signal on property */
void ModelItem::emitDataChanged(int role) const
{
if(role < (Qt::UserRole+1))
role+=Qt::UserRole+1;
switch(role)
{
case GroupMemberModel::Nom:
emit nameChanged();
break;
default:
qDebug() << "ModelItem::dataChanged, unknown role";
break;
}
}
This works as needed and is simplier than what I thought.

QListView - mixed drag mode

I have a QListView with the ViewMode set to IconMode. I would like to achieve the following DnD behavior:
If a list view item is dragged inside the view, only the items position in the view is changed. This is the same as setting DragDropMode equal to InternalMove.
If a list item is moved out of the view, it can be copied to another external view. In this case, DragDropMode is equal to DragOnly.
How do I mix the two modes in such a way that both behaviors are supported by the view?
You might be able to do this by overriding the dropEvent of your view like this:
void MyListView::dropEvent( QDropEvent* e )
{
if( e->source() != this )
{
// something comes from the outside
// what to do? return?
return;
}
else
{
// event comes from the view itself, let's do some stuff
// for example call the base class default event
QAbstractItemView::dropEvent(e);
}
}
I guess the correct flag would be QAbstractItemView::DragDrop to do this.

How to make QComboBox popup upwards?

my QComboBox-derived class lives in a QGraphicsScene at the bottom end of the (visible) screen - but it pops up downwards, thus out of view.
(How) is it possible to force the popup to open above the widget?
I've tried re-implementing showPopup like this:
void MyComboBox::showPopup()
{
QAbstractItemView *popupView = view();
popupView->move(0,-100);
//popupView->window->move(0,-100);
QComboBox::showPopup();
}
The result is, that the content seems to be shifted, but not the underlying popup object.
I think it might be possible to find a solution with styles as indicated in
this article, but I can't find any Styles control that might be helpful here. I am rather new to C++ as well as Qt, so I might be missing something obvious.
I'd appreciate any help on this matter!
Best regards,
Sebastian
With the information found here, I was able to get it done this way:
void SteuerQComboBox::showPopup() {
QComboBox::showPopup();
QWidget *popup = this->findChild<QFrame*>();
popup->move(popup->x(),popup->y()-this->height()-popup->height());
}
Note that it's crucially important to call the base classes "showPopup" first.
Thanks to everybody who was reading my question and thinking about it!
user1319422's solution isn't bad, but it has two problems.
If your platform has GUI animation, the listbox will animate opening downwards, then is moved above the text box.
If you disable combobox animation (or you don't have it), the call to QComboBox::showPopup() still makes the GUI element start to appear on the screen already. So, moving it there would cause it to flicker as it appears in the first place and moves to the next.
So, to address the first problem, I just switched off animation:
void MyComboBox::showPopup()
{
bool oldAnimationEffects = qApp->isEffectEnabled(Qt::UI_AnimateCombo);
qApp->setEffectEnabled(Qt::UI_AnimateCombo, false);
QComboBox::showPopup();
qApp->setEffectEnabled(Qt::UI_AnimateCombo, oldAnimationEffects);
}
Then, for the second problem, I moved the frame in the Show event:
bool MyComboBox::eventFilter(QObject *o, QEvent *e)
{
bool handled = false;
if (e->type() == QEvent::Show)
{
if (o == view())
{
QWidget *frame = findChild<QFrame*>();
//For some reason, the frame's geometry is GLOBAL, not relative to the QComboBox!
frame->move(frame->x(),
mapToGlobal(lineEdit()->geometry().topLeft()).y() - frame->height());
}
}
/*else if other filters here*/
if (!handled)
handled = QComboBox::eventFilter(o, e);
return handled;
}
if you want to force popup to open above only when it is out of view you can do this:
void SteuerQComboBox::showPopup() {
QComboBox::showPopup();
QWidget *popup = this->findChild<QFrame*>();
if((popup->y() + popup->height()) > this->window()->height())
popup->move(popup->x(),popup->y()-this->height()-popup->height());
}

Simple way to get all visible items in the QListView

I am trying to develop an image gallery application using Qt Framework. The application loads all the images from the selected folder and those images are displayed using QListView control.
But now i want to reduce the memory consumption by loading only the images that are visible to user. Since there is no direct function to get all the visible items in the view, i am not able to achieve this.
You can get the visible items of a list view using the indexAt function. For more details and an example you can check the following article:
http://qt-project.org/faq/answer/how_can_i_get_hold_of_all_of_the_visible_items_in_my_qlistview
I found it! You have to connect the vertical scrollbar of the listwidget to a signal:
connect(ui->listWidget->verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(launch_timer()));
Every time the user scrolls, the valuechanged(int) signal is being omitted! The thing is that you shouldn't run the code provided by webclectic in this question every time the value of the vertical scrollbar of the listwidget changes, because the program will be unresponsive with so much code to run in so little time.
So, you have to have a singleshot timer and point it to the function that webclectic posted above. When launch_timer() is called, you do something like this:
if(timer->isActive()){
timer->stop();
timer->start(300);
}
else
timer->start(300);
and the timeout() signal of timer will be connected to the slot webclectic talked about. This way, if the user scrolls quickly all the way down only the last items will be updated. Generally, it will be updated anything visible for more than 300 milliseconds!
I think what you need is to implement your own model (take a look to the QAbstractListModel documentation) so that way you could decide when you have to load more images to show and maybe free some of the images that became non-visible.
although this is not so simple in Qt 4 but
it is always simple to copy below:
#include <private/qlistview_p.h>
class QListViewHelper : public QListView
{
typedef QListView super;
inline QListViewHelper() {} //not intended to be constructed
public:
inline static QVector<QModelIndex> indexFromRect(const QListView *view,
const QRect &rect)
{
const QListViewPrivate *d = static_cast<const QListViewPrivate *>(QObjectPrivate::get(view)); //to access "QListViewPrivate::intersectingSet(...)"
const QListViewHelper *helper = static_cast<const QListViewHelper *>(view); //to access "QListView::horizontalOffset()"
return d->intersectingSet(rect.translated(helper->horizontalOffset(), helper->verticalOffset()), false);
}
inline static QVector<QModelIndex> visibleItems(const QListView *view)
{ return indexFromRect(view, view->rect()); }
inline static QModelIndex firstVisible(const QListView *view)
{ return visibleItems(view).value(0); }
inline static QModelIndex lastVisible(const QListView *view) {
const QVector<QModelIndex> &items = visibleItems(view);
return items.value(items.count() - 1);
}
};
void ourTest(const QListView *view) {
QModelIndex &index = QListViewHelper::firstVisible(view);
qDebug("QListViewHelper: first visible row is %d", index.row());
index = QListViewHelper::lastVisible(view);
qDebug("QListViewHelper: last visible row is %d", index.row());
}
usage:
QModelIndex &index =
QListViewHelper::firstVisible(listViewPointerHere)
note: since it does use Qt 4.8 private-headers it may no longer work in latter versions and will need some changes.
You can keep track of all the elements that are drawn per paint event. I used a delegate and overloaded the paint event.
I also overloaded the paint event in the view. During this call, all the visible delegates will get a paint event.
If you just need to know if an item is visible, you can increment a frame count in view->paintEvent and set that number in the delegate item. The item is visible of the item matches the current frame number.
If you need a list of all visible items, clear the visible item list in view->paintEvent and add each item in the int the delegate->paintEvent to the visible items list.

How to iterate through a menu's actions in Qt?

I'm working in a project where I need to open (show or popup) automatically the items in the QMenuBar.
Let's say I have the next menu bar:
File Edit Help
-op1 -op1 -op1
-op2 -op2 -op2
To set an action (show the menu associated with that action) I use:
menuBar->setActiveAction(mymenuactionpointer);
As I know, I can use one of the following to get a list of pointers to the elements of QMenuBar:
QMenuBar::actions();
or
QList<Object*> lst1 = QMenuBar::findChildren<QObject*>();
QList<Object*> lst2 = QMenuBar::findChildren<QAction*>();
When I use QMenuBar::findChildren<QAction*>() or MenuBar::actions() I got a list of the menus in menubar, I mean, I got "File, Edit, Help" from my QMenuBar, the size of the QList in this case is 3.
When I use QMenuBar::findChildren<QObject*>() I got a list of QObject of size 6, which is the correct number of items in the menu bar. However, I have tried cast to QAction*
QAction *a = (QAction *)lst1.at(0);
QAction *a = qobject_cast<QAction*>(lst1.at(0));
QAction *a = dynamic_cast<QAction*>(lst1.at(0));
In all this cases a is not NULL, but when I try to get the action name QAction::title() it always causes me segmentation fault.
I have been searching and I found here that after getting the menubar actions list, one can ask to QAction::menu() (which returns a valid QMenu pointer if the item is a menu) to know if the item is a QMenu, if yes, one can repeat getting the actions list of that menu, and continue iterating. But this does not work for me, I expected that for
QList<Object*> lst2 = QMenuBar::findChildren<QAction*>();
each element "File, Edit Help" QAction::menu() returns a valid menu pointer, so I could get the list of the actions of each menu, but this does not work at all for me.
The correct way to enumerate a QMenu is to use the actions() functions, but there is a catch - some of the actions are submenus, and they need to be iterated recursively. In fact, each QMenu is associated with a QAction, and they both hold pointers to each other - see QMenu::menuAction() and QAction::menu().
It is crucial to understand that each QMenu is also associated with a QAction. So knowing that, the proper implementation is the following:
void enumerateMenu(QMenu *menu)
{
foreach (QAction *action, menu->actions()) {
if (action->isSeparator()) {
qDebug("this action is a separator");
} else if (action->menu()) {
qDebug("action: %s", qUtf8Printable(action->text()));
qDebug(">>> this action is associated with a submenu, iterating it recursively...");
enumerateMenu(action->menu());
qDebug("<<< finished iterating the submenu");
} else {
qDebug("action: %s", qUtf8Printable(action->text()));
}
}
}
Below is how to iterate through every menu item in the menu bar, it will also seek out any menus underneath so there is not need for recursive calling here.
// assuming you have a private QActionGroup* actions; defined in the header..
// ...and a slot named 'onAction(QAction*)' as well... this should work:
QList<QMenu*> lst;
lst = ui->menuBar->findChildren<QMenu*>();
actions = new QActionGroup(this);
foreach (QMenu* m, lst)
{
foreach (QAction* a, m->actions())
{
actions->addAction(a);
}
}
connect(actions,SIGNAL(triggered(QAction*)),this,SLOT(onAction(QAction*)));
As you can see, you can then connect a master slot to handle the various events an action might bring up (i just showed triggered here but you get the idea). Hope this helps.. someone..
Notes
I used the QActionGroup for example purposes on using the list you might iterate through, but you really shouldnt use that unless you are dealing with radio groups since thats what its for. Secondly, if you want the actions because you plan to link them into a single method to handle all items, i suggest you use QMenu's triggered/hovering signals or if you need to know when a menu is about to pop up, you'll need QMenuBar's aboutToShow() signal. I cant think of a reason (for me anyway) that you cant do what you need in those signals since you are passed the QAction* in the slot. But if you MUST do it the otherway, you can do it the way I showed above, you just might not want to use the QActionGroup because of radio grouping is what it is designed for. (you can work around that by not adding items that are 'checkable' into the group.
The reason the qobject_cast is failing is that there are only three QActions with the QMenuBar as the parent. The other three are different QObjects (my guess is the three QMenus), so the cast fails. The QActions associated with those menus are then under those, not the root QMenuBar. I fail to see why you can't build a master list of QActions by recursively iterating through the QMenus.
You may be able to just use the QAction pointer from your UI definition if you are after a known menu, that might not trigger the parent menus. If you are trying to automate testing, trigger() on your desired QAction is probably as detailed as you need. If you are trying to do things in response to user actions, modifying toolbars is probably a better means of contextual feedback, as it doesn't break focus. Some more details on what you're actually trying to accomplish would help.
this puts it all together:
template <class Function>
class QMenuBarIterator {
QMenuBar *i_barP;
void iterate_sub(Function f, size_t tabsL, QMenu* m) {
foreach (QAction *action, m->actions()) {
f(tabsL, action);
if (action->menu()) {
iterate_sub(f, tabsL + 1, action->menu());
}
}
}
public:
QMenuBarIterator(QMenuBar *barP) : i_barP(barP) {}
virtual void operator()(size_t levelL, QAction *actionP) {
}
void iterate(Function f) {
QList<QMenu *> menuBar = i_barP->findChildren<QMenu *>();
foreach (QMenu* m, menuBar) {
f(0, m->menuAction());
iterate_sub(f, 1, m);
}
}
};
/***************************************************************************/
class CMenuLogger {
public:
void operator()(size_t tabsL, QAction *action) {
SuperString tabStr(GetIndentString(tabsL)); // returns a string with tabsL tab characters in it
if (action->isSeparator()) {
qDebug("%s-------------------", tabStr.utf8Z());
} else {
qDebug("%s%s (%s)",
tabStr.utf8Z(),
qUtf8Printable(action->text()),
qUtf8Printable(action->objectName()));
}
}
};
then in your main:
{
QMenuBarIterator<CMenuLogger> bar(ui->menuBar);
bar.iterate(CMenuLogger());
}

Resources