1

I've created a custom QListWidget with custom list widgets (just QPushButton widgets, for this example) added to it's QListWidgetItems, all setup so that I can drag and drop from a QPushButton, to add another button to the QListWidget. In the first list widget, you'll notice that you get a dotted line inbetween two list widget items, indicating that you can drag and insert a widget between two existing ones. However, with my custom list, I've lost that behavior. I can still drop them between existing widgets, but there's no visual indicator to the user as before, of a horizontal bar between the two widgets where it will be inserted. Does anyone know how I would do that? You can see in the image below, the line in the first list that is the default indicator for inserting an item between two. The red arrow points to that kind of custom insertion indicator that I'd like to create.

insertIndicator

And secondly, when I drag and drop from my custom QPushButton, it is highlighted black, but never returns to its normal grey color again, as shown in the image above. How would I get this button back to its default state?

from inspect import isclass
from shiboken2 import wrapInstance
from PySide2 import QtCore, QtGui, QtWidgets
from maya import OpenMayaUI as omui


def show_ui():
    main_win_obj = omui.MQtUtil.mainWindow()
    main_win = wrapInstance(long(main_win_obj), QtWidgets.QWidget)
    win = Test(parent=main_win)
    win.show()


class DragButton(QtWidgets.QPushButton):

    def __init__(self, parent=None, text=''):
        super(DragButton, self).__init__()
        self.setText(text)

    def mouseMoveEvent(self, event):

        if event.buttons() != QtCore.Qt.LeftButton:
            super(DragButton, self).mouseMoveEvent(event)
            return

        btn_img = self.grab()
        painter = QtGui.QPainter(btn_img)
        painter.setCompositionMode(
            painter.CompositionMode_DestinationIn
        )
        painter.fillRect(btn_img.rect(), QtGui.QColor(0, 0, 0, 127))
        painter.end()

        data = QtCore.QMimeData()
        data.setText('ReorderListAdd()')

        drag = QtGui.QDrag(self)
        drag.setMimeData(data)
        drag.setPixmap(btn_img)
        drag.setHotSpot(event.pos())
        drag.exec_(QtCore.Qt.CopyAction)

        super(DragButton, self).mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):

        data = QtCore.QMimeData()
        data.setText('my text')

        drag = QtGui.QDrag(self)
        drag.setMimeData(data)
        drag.exec_()

        super(DragButton, self).mouseReleaseEvent(event)


class ReorderList(QtWidgets.QListWidget):

    def __init__(self, parent=None):
        super(ReorderList, self).__init__(parent)

        self.setDragDropMode(
            QtWidgets.QAbstractItemView.InternalMove
        )
        self.setAcceptDrops(True)
        self.setAlternatingRowColors(True)
        self.setSelectionMode(
            QtWidgets.QAbstractItemView.SingleSelection
        )
        self.setDragDropMode(
            QtWidgets.QAbstractItemView.InternalMove
        )

    def add_btn(self, text='', index=-1):

        widget = QtWidgets.QWidget()
        if not text:
            text = 'item {0}'.format(self.count() + 1)
        btn = QtWidgets.QPushButton(text)

        layout = QtWidgets.QVBoxLayout(widget)
        layout.setContentsMargins(2, 2, 2, 2)
        layout.setSpacing(2)
        layout.addWidget(btn)

        item = QtWidgets.QListWidgetItem()
        item.setSizeHint(widget.sizeHint())

        if index < 0:
            self.addItem(item)
        else:
            self.insertItem(index, item)
        self.setItemWidget(item, widget)

    def dropEvent(self, event):
        drop_index = self.indexAt(event.pos()).row()
        self.add_btn(index=drop_index)
        super(ReorderList, self).dropEvent(event)

    def dragEnterEvent(self, event):
        event.accept()

    def dragMoveEvent(self, event):
        event.accept()

    def onClick(self):
        print self.sender().text()


class Test(QtWidgets.QMainWindow):

    def __init__(self, *args, **kwargs):
        super(Test, self).__init__(*args, **kwargs)

        self.list1 = QtWidgets.QListWidget()
        self.list1.setDragDropMode(
            QtWidgets.QAbstractItemView.DragDrop
        )
        self.list1.setSelectionMode(
            QtWidgets.QAbstractItemView.SingleSelection
        )
        self.list1.setDragDropMode(
            QtWidgets.QAbstractItemView.InternalMove
        )
        self.list1.setMaximumHeight(60)
        self.list2 = ReorderList()
        self.list2.setMaximumHeight(120)
        for name in ('item 1', 'item 2', 'item 3'):
            item = QtWidgets.QListWidgetItem(name)
            self.list1.addItem(item)
            self.list2.add_btn(text=name)
        self.btn = DragButton(text='Add list button')
        self.btn.clicked.connect(self.list2.add_btn)

        self.widget = QtWidgets.QWidget()
        self.setCentralWidget(self.widget)
        self.layout = QtWidgets.QVBoxLayout(self.widget)
        for item in (self.list1, self.list2, self.btn):
            self.layout.addWidget(item)


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    win = Test()
    win.show()
    sys.exit(app.exec_())

I'd like to be able to have a horizontal bar when inserting a widget in the QListWidget, and have the buttons that have been drug return to their default color.

Here are some similar links I could find that helped get me to this point:

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Jesse
  • 143
  • 1
  • 1
  • 10
  • 1
    In SO we require that it be **minimum and complete at the same time**. Your code is minimal but not complete, complete implies that it is not necessary to add anything else to execute it but your code is far from it. The idea of not adding is that maybe in that part that is missing the error and so we do not see it we can not tell you where the problem was, please take the time to read that it is a [mcve] and provide the requested code. – eyllanesc Feb 15 '19 at 20:39
  • Great, I will do the same. With your modifications, your question is very clear :-) – eyllanesc Feb 16 '19 at 02:02

2 Answers2

1

You have to do the painting since the drag-and-drop is customized, for that you must detect the rectangle of the item using QModelIndex in the method dragMoveEvent. Then make the painting but because of the size of the widget, it will not be seen so I look at it, I created a delegate that modifies the geometry of the editor.

class Delegate(QtWidgets.QStyledItemDelegate):
    def updateEditorGeometry(self, editor, option, index):
        super(Delegate, self).updateEditorGeometry(editor, option, index)
        geo = editor.geometry().adjusted(0, 4, 0, 0)
        editor.setGeometry(geo)

class ReorderList(QtWidgets.QListWidget):
    def __init__(self, parent=None):
        super(ReorderList, self).__init__(parent)
        delegate = Delegate(self)
        self.setItemDelegate(delegate)
        self.dropIndicatorRect = QtCore.QRect()
        self.setDragDropMode(
            QtWidgets.QAbstractItemView.InternalMove
        )
        self.setAcceptDrops(True)
        self.setAlternatingRowColors(True)
        self.setSelectionMode(
            QtWidgets.QAbstractItemView.SingleSelection
        )
        self.setDragDropMode(
            QtWidgets.QAbstractItemView.InternalMove
        )

    def add_btn(self, text='', index=-1):
        widget = QtWidgets.QWidget()
        if not text:
            text = 'item {0}'.format(self.count() + 1)
        btn = QtWidgets.QPushButton(text)

        item = QtWidgets.QListWidgetItem()
        item.setSizeHint(btn.sizeHint())

        if index < 0:
            self.addItem(item)
        else:
            self.insertItem(index, item)
        self.setItemWidget(item, btn)

    def dropEvent(self, event):
        drop_index = self.indexAt(event.pos()).row()
        self.add_btn(index=drop_index)
        self.dropIndicatorRect = QtCore.QRect()
        self.viewport().update()
        super(ReorderList, self).dropEvent(event)

    def dragEnterEvent(self, event):
        event.accept()

    def dragLeaveEvent(self, event):
        self.dropIndicatorRect = QtCore.QRect()
        self.viewport().update()
        super(ReorderList, self).dragLeaveEvent(event)

    def dragMoveEvent(self, event):
        event.accept()

    def onClick(self):
        print(self.sender().text())

    def dragMoveEvent(self, event):
        index = self.indexAt(event.pos())
        if index.isValid():
            rect = self.visualRect(index)
            if self.dropIndicatorPosition() == QtWidgets.QAbstractItemView.OnItem:
                self.dropIndicatorRect = rect
            else:
                self.dropIndicatorRect = QtCore.QRect()
        else:
            self.dropIndicatorRect = QtCore.QRect()
        self.viewport().update()
        super(ReorderList, self).dragMoveEvent(event)

    def paintEvent(self, event):
        super(ReorderList, self).paintEvent(event)
        if not self.dropIndicatorRect.isNull() and self.showDropIndicator():
            painter = QtGui.QPainter(self.viewport())
            p = QtGui.QPen(painter.pen())
            p.setWidthF(1.5)
            painter.setPen(p)
            r = self.dropIndicatorRect
            painter.drawLine(r.topLeft(), r.topRight())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Ah, thanks! I had spent some time looking into custom delegates, but didn't make it very far with them. That seems to draw a rectangle around each item under the cursor, which at least gives me some sort of visual indicator. Maybe I'd have to come up with a custom widget to insert between widgets, to get a divider? It also seems to leave the box after button release, which I imagine I'd have to remove with the dropEvent. – Jesse Feb 16 '19 at 04:05
  • 1
    @user3161430 Actually it is the normal QListWidget also does it but the paint removes it, I think I can improve it. On the other hand I did not understand the last part of your comment, try my update and tell me what error you get – eyllanesc Feb 16 '19 at 04:13
  • Oh, the last part was just about getting an indicator like the blue bar with dark blue triangles between widgets, with the red arrow pointing at it. After that, I was just saying that the rectangle around the previous widget(s) under the cursor stays there after drag release, whereas I'd like to have it disappear, once a new widget has been dropped, so that none of them have rectangles displayed after drag and drop. – Jesse Feb 16 '19 at 04:28
  • 1
    @user3161430 I have made a new update by changing the rectangle by a line, tell me if it works correctly or if you have a problem. – eyllanesc Feb 16 '19 at 04:45
  • Done!... One last thing, would you know how to set the button back to it's default state, after being clicked and drag? Right now it goes from grey to black, and seems to stay in the depressed state. I've tried setting "setDown", "setChecked" and "setPressed" on the QPushButton to false, within the button's "mouseReleaseEvent" function, as well as adding an "event.accept()", but to no avail. – Jesse Feb 16 '19 at 05:30
  • 1
    @user3161430 try with: `def mousePressEvent(self, event): QtCore.QTimer.singleShot(100, lambda: self.setDown(False)) super(DragButton, self).mousePressEvent(event)` – eyllanesc Feb 16 '19 at 05:50
  • Nah, didn't work for me. All I can do is to make it so that the mousePressEvent doesn't get triggered at all, so it never gets depressed, to prevent the problem. For some reason, it doesn't seem like the mouseReleaseEvent even gets called at all. – Jesse Feb 16 '19 at 17:17
  • @user3161430 It is not called because when the button release occurs the focus is on the QListWidget. In my case my trick if it works – eyllanesc Feb 16 '19 at 17:18
  • The timer trick you suggested does work... But only until the mouse release, at which time the button goes back to being black/depressed. The best I've been able to do, is to just call the mouseReleaseEvent inside of the mousePressEvent. Not ideal, but at least I'm not left with the button showing as depressed, until a regular click and release on the button. This guy had a similar problem, and setting the focus policy of the custom push button, as suggested, doesn't seem to do anything. https://stackoverflow.com/questions/31734925/pyside-qpushbutton-stays-highlighted-after-pressing-it – Jesse Feb 16 '19 at 19:45
  • I've added my info over there, and this can probably be tracked as a separate issue, as I think it may be specific to Maya's PySide2 environment, perhaps? At any rate, feel free to chime in over there if anyone knows more. Thanks for all your help, eyllanesc! – Jesse Feb 17 '19 at 01:06
0

I’m not quite sure that I understood you correctly, but try as an example below:

import sys
from PyQt5 import QtCore, QtGui, QtWidgets

class DragButton(QtWidgets.QPushButton):
    def __init__(self, *args, **kwargs):
        super(DragButton, self).__init__(*args, **kwargs)

    def mouseMoveEvent(self, event):
        if event.buttons() != QtCore.Qt.LeftButton:
            super(DragButton, self).mouseMoveEvent(event)
            return

#        btn_img = QtGui.QPixmap.grabWidget(self)                   # ---
        btn_img = self.grab()                                       # +++

        painter = QtGui.QPainter(btn_img)
        painter.setCompositionMode(
            painter.CompositionMode_DestinationIn
        )
        painter.fillRect(btn_img.rect(), QtGui.QColor(0, 0, 0, 127))
        painter.end()
        data = QtCore.QMimeData()
        data.setText('ReorderListAdd()')
        drag = QtGui.QDrag(self)
        drag.setMimeData(data)
        drag.setPixmap(btn_img)
        drag.setHotSpot(event.pos())
        drag.exec_(QtCore.Qt.CopyAction)
        super(DragButton, self).mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        data = QtCore.QMimeData()
        data.setText('my text')
        drag = QtGui.QDrag(self)
        drag.setMimeData(data)
        drag.exec_()
        super(DragButton, self).mouseReleaseEvent(event)


class ReorderList(QtWidgets.QListWidget):

    def __init__(self, parent=None):
        super(ReorderList, self).__init__(parent)

        self.setDragDropMode(
            QtWidgets.QAbstractItemView.InternalMove
        )
        self.setAcceptDrops(True)
        self.setAlternatingRowColors(True)
        self.setSelectionMode(
            QtWidgets.QAbstractItemView.ExtendedSelection
        )

    def add_widget(self, widget=None, index=-1):
        item = QtWidgets.QListWidgetItem()
        item.setSizeHint(widget.sizeHint())
        if index < 0:
            self.addItem(item)
        else:
            self.insertItem(index, item)
        self.setItemWidget(item, widget)

    def add_btn(self, txt='', index=-1):
        if not txt or not isinstance(txt, basestring):
            btn_name = 'Button Item {:02d}'.format(self.count() + 1)
        else:
            btn_name = txt

#        widget = ReorderListAdd(btn_name)                            # ---
        widget = QtWidgets.QPushButton(btn_name)                      # +++
        widget.setIcon(QtGui.QIcon("Ok.png"))                         # +++
        widget.clicked.connect(self.onClick)                          # +++

        if index < 0:
            self.add_widget(widget)
        else:
            self.add_widget(widget, index)

    def dropEvent(self, event):
        drop_index = self.indexAt(event.pos()).row()
        if event.mimeData().hasText():
            if event.mimeData().text() == 'ReorderListAdd()':
                self.add_btn(index=drop_index)
        super(ReorderList, self).dropEvent(event)

    def dragEnterEvent(self, event):
        event.accept()

    def dragMoveEvent(self, event):
        event.accept()

# +++      
    def onClick(self):                                                 # +++
        print(self.sender().text())


class MainView(QtWidgets.QWidget):               
    def __init__(self):
        super().__init__()

        self.listWidget = ReorderList()
        self.listWidget.addItems(["Item 1 1 1 ", "Item 22 22", "Item 3 33 333", ])

        self.button = DragButton()
        self.button.setText("Button")
        self.button.setIcon(QtGui.QIcon("Ok.png"))

        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(self.listWidget)
        layout.addWidget(self.button)


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    w = MainView()
    w.show()
    sys.exit(app.exec_())          

enter image description here

S. Nick
  • 12,879
  • 8
  • 25
  • 33
  • Thanks! I've added your suggestions into my original post, and attempted to clarify my question and example code. :) – Jesse Feb 16 '19 at 01:58
  • @user3161430 I am glad that my example was the impetus for solving your problem :) – S. Nick Feb 16 '19 at 09:05