How to access event data? - bokeh

Click handlers in bokeh 1.0.3 used to have the signature attr, old, new. Now the are passed a single event object. How can I access its values?
menu = [('a', 'a'), ('b', 'b')]
dropdown = Dropdown(label='clickme', menu=menu)
def click_handler(event):
print(event)
returns
bokeh.events.MenuItemClick object at 0x7ff7de1cc208
EDIT: Where in the documentation is access to values of events described? I could not find anything on https://docs.bokeh.org/en/latest/docs/reference/events.html

I'm not sure where you got this information, but it's not quite correct. Dropdown was refined as a type of Button recently, so the ability to respond to the same kind of click events that other buttons do was added. But nothing was replaced. Callbacks for property changes work for any Bokeh object property, including Dropdown.value, and this has not changed:
from bokeh.io import curdoc
from bokeh.models import Dropdown
menu = [('a', 'a'), ('b', 'b')]
dropdown = Dropdown(label='clickme', menu=menu)
def cb(attr, old, new):
print(attr, old, new)
dropdown.on_change('value', cb)
curdoc().add_root(dropdown)

I'm coming in late, but I had the same issue as Leevi, and bigreddot's answer did not work for me (Bokeh 2.4.2, Python 3.10.2). Short answer is that a Bokeh Dropdown does not have a value attribute, so on_change('value, handler) will not work. The MenuItemClick object, has an item attribute, which is what you want.
menu = [('a', 'a'), ('b', 'b')]
dropdown = Dropdown(label='clickme', menu=menu)
def click_handler(event):
print(event.item)
dropdown.on_click(click_handler)
Long answer: I had the following code:
def choose_pipeline(event):
"""Change to the chosen pipeline"""
print(event)
pipeline_dropdown = Dropdown(label='Available Pipelines',
menu=[("Pipeline1", "pipeline1_value"),
("Pipeline2", "pipeline2_value")])
pipeline_dropdown.on_change('value', choose_pipeline)
and got this error:
File "C:\Users\me\PycharmProjects\DHLLDV\Scripts\SystemTab.py", line 69, in system_panel
pipeline_dropdown.on_change('value', choose_pipeline)
File "C:\Users\me\PycharmProjects\DHLLDV\venv\lib\site-packages\bokeh\model\model.py", line 434, in on_change
descriptor = self.lookup(attr)
File "C:\Users\me\PycharmProjects\DHLLDV\venv\lib\site-packages\bokeh\core\has_props.py", line 469, in lookup
raise AttributeError(f"{cls.__name__}.{name} property descriptor does not exist")
AttributeError: Dropdown.value property descriptor does not exist
I wrote this:
def choose_pipeline(event):
"""Change to the chosen pipeline"""
print(event.__dict__)
pipeline_dropdown = Dropdown(label='Available Pipelines', menu=[("Pipeline1", "pipeline1_value"),
("Pipeline2", "pipeline2_value")])
pipeline_dropdown.on_click(choose_pipeline)
And saw the result when I picked Pipeline2:
{'item': 'pipeline2_value', '_model_id': '1365'}

Related

Best approach to show / hide dialogs

I have encountered a theoretical question. I'm using pyqt5, but this is probably are very generalistic and framework independent question.
I have a QMainwindow sitting around waiting for the user to do stuff. The user can show / hide dialogues (subclasses of QDockwidgets) as he chooses using the QMenu and the associated shortcuts (it's a checkable QAction for each individual dialogue).
I have been struggling with showing / hiding the dialogues efficiently. Currently, I'm just initiating them all at start up, hiding those that I don't want to show up in the beginning. This makes triggering the dialogues easy, since I can just dialogue.show() /dialogue.hide() depending on the dialogues current visibility.
But I cannot believe that this is best practice and very efficient.
I have tried (I currently do not have my pyqt environment set up on this computer, so I had to strip down my actual code without being able to test if this runs):
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class InfoPanel(QDockWidget):
def __init__(self, title='Tool Box'):
QDockWidget.__init__(self, title)
self.setFeatures(QDockWidget.DockWidgetFloatable | QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetClosable)
self.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
frame = QFrame()
layout = QGridLayout()
self.canvas = QGraphicsView()
self.canvas.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(40, 40, 40)))
layout.addWidget(self.canvas)
frame.setLayout(layout)
self.setWidget(frame)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.showpanelAct = QAction("&Show Panel", self, enabled=True,checkable=True, shortcut="F10")
self.showpanelAct.triggered.connect(lambda: self.showPanel(0))
self.viewMenu = QMenu("&View", self)
self.viewMenu.addAction(self.showpanelAct)
self.setDockOptions(QMainWindow.AnimatedDocks)
def showPanel(self,i:int = 0): # this is not so smart - should construct and deconstuct to save memory!?
if i == 0: #infopanel
dialogueExists = True
try: self.infoPanel
#except NameError: #does not catch the error
except:
dialogueExists = False
if dialogueExists:
print('destroy')
self.infoPanel.destroy()
else:
print('create')
self.infoPanel = InfoPanel() #init
self.infoPanel.show()
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
Which works the first time, but after that, it only seems to trigger the destruction of the dialogue (which, surprisingly, does not crash anything it just keeps on going).
Why is that and is there a standard way to approach the showing hiding of dialogues?
I took the exposed MCVE of OP and tried to make it running in my cygwin64 on Windows 10.
At first I had to apply little fixes. (OP stated that he was not able to test it at the time of publishing.)
First, I inserted a “hut” at first line for convenient start in bash:
#!/usr/bin/python3
Second, the self.viewMenu didn't appear. Hence, I inserted a line after
self.viewMenu = QMenu("&View", self)
self.viewMenu.addAction(self.showpanelAct)
to add the viewMenu to main menu bar:
self.menuBar().addMenu(self.viewMenu)
which fixed it.
Third, when clicking the menu item I got:
Traceback (most recent call last):
File "./testQDockPanelShowHide.py", line 27, in <lambda>
self.showpanelAct.triggered.connect(lambda: self.showPanel(0))
File "./testQDockPanelShowHide.py", line 45, in showPanel
self.infoPanel = InfoPanel() #init
File "./testQDockPanelShowHide.py", line 17, in __init__
self.canvas.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(40, 40, 40)))
NameError: name 'QtGui' is not defined
Aborted (core dumped)
I must admit that my Python knowledge is very limited. (I'm the guy who writes the Python bindings in C++ for the colleagues. So, my colleagues are the actual experts. At most, I play a little bit in Python when I test whether new implemented bindings do what's expected.) However, I modified
self.canvas.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(40, 40, 40)))
to:
self.canvas.setBackgroundBrush(QBrush(QColor(40, 40, 40)))
which fixed this issue.
After this, I got the behavior described by OP and did a closer look where I (and OP) suspected the error:
def showPanel(self,i:int = 0): # this is not so smart - should construct and deconstuct to save memory!?
if i == 0: #infopanel
dialogueExists = True
try: self.infoPanel
#except NameError: #does not catch the error
except:
dialogueExists = False
if dialogueExists:
print('destroy')
self.infoPanel.destroy()
else:
print('create')
self.infoPanel = InfoPanel() #init
self.infoPanel.show()
I strongly believe that try: self.infoPanel doesn't do what OP thinks it would.
It tries to access self.infoPanel which isn't existing until the first call of this method. (Please, be aware, the member variable self.infoPanel isn't existing.) So, the except: branch is executed and sets dialogueExists = False which a few lines later causes self.infoPanel = InfoPanel() #init. Now, the member variable self.infoPanel is existing, and the try: self.infoPanel will never fail again until destruction of this MainWindow.
Out of curiosity, I had a look at QWidget.destroy() (to be sure not to tell something wrong):
QWidget.destroy (self, bool destroyWindow = True, bool destroySubWindows = True)
Frees up window system resources. Destroys the widget window if destroyWindow is true.
destroy() calls itself recursively for all the child widgets, passing destroySubWindows for the destroyWindow parameter. To have more control over destruction of subwidgets, destroy subwidgets selectively first.
This function is usually called from the QWidget destructor.
It definitely doesn't destroy the member variable self.infoPanel.
After having understood this, a fix was easy and obvious:
def showPanel(self,i:int = 0): # this is not so smart - should construct and deconstuct to save memory!?
if i == 0: #infopanel
try: self.infoPanel
#except NameError: #does not catch the error
except:
print('create')
self.infoPanel = InfoPanel() #init
if self.infoPanel.isVisible():
self.infoPanel.hide()
else:
self.infoPanel.show()
Btw. I replaced destroy() by hide() which makes a re-creation of the InfoPanel() obsolete.
I tested this by toggling the menu item multiple times – it works as expected now (at least, it looks like).
The complete sample finally:
#!/usr/bin/python3
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class InfoPanel(QDockWidget):
def __init__(self, title='Tool Box'):
QDockWidget.__init__(self, title)
self.setFeatures(QDockWidget.DockWidgetFloatable | QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetClosable)
self.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
frame = QFrame()
layout = QGridLayout()
self.canvas = QGraphicsView()
# self.canvas.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(40, 40, 40)))
self.canvas.setBackgroundBrush(QBrush(QColor(40, 40, 40)))
layout.addWidget(self.canvas)
frame.setLayout(layout)
self.setWidget(frame)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.showpanelAct = QAction("&Show Panel", self, enabled=True,checkable=True, shortcut="F10")
self.showpanelAct.triggered.connect(lambda: self.showPanel(0))
self.viewMenu = QMenu("&View", self)
self.viewMenu.addAction(self.showpanelAct)
self.menuBar().addMenu(self.viewMenu)
self.setDockOptions(QMainWindow.AnimatedDocks)
def showPanel(self,i:int = 0): # this is not so smart - should construct and deconstuct to save memory!?
if i == 0: #infopanel
try: self.infoPanel
#except NameError: #does not catch the error
except:
print('create')
self.infoPanel = InfoPanel() #init
if self.infoPanel.isVisible():
self.infoPanel.hide()
else:
self.infoPanel.show()
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
After taking a break from coding the solution to my problem was very obvious. Going back to my original code (which did not produce the expected output of creating / destroying the dialogue self.infoPanel on demand):
dialogueExists = True
try: self.infoPanel
#except NameError: #does not catch the error
except:
dialogueExists = False
if dialogueExists:
print('destroy')
self.infoPanel.destroy()
else:
print('create')
self.infoPanel = InfoPanel() #init
self.infoPanel.show()
My main problem was that I confused two separate things. Qt destroyed the widget contained in the object self.infoPanel when I called self.infoPanel.destroy(). But that doesn't mean the object self.infoPanel does not exist (that's exactly what I use try: ... for, to see if the object exists). The simple and obvious way to create and destroy dialogues on demand obviously involves deleting the object from the environment (del self.infoPanel).
The working code is:
dialogueExists = True
try:
self.infoPanel.destroy() #not sure this is needed, but I guess it doesn't hurt
del self.infoPanel #this is the deletion of the actual object
except:
dialogueExists = False
if not dialogueExists :
self.infoPanel = InfoPanel()
Cheers and many thanks for the helpful advice on deciding whether to show / hide dialogues or to create / destroy them!

Tkinter TTK OptionMenu won't open

Please see my edit at the bottom, this issue is now OS specific.
A gif of the problem in action
So I'm having an issue with an instance of ttk.OptionMenu. I've successfully implemented the widget before, however I'm trying to use it here as a dropdown for available files in a pop-up window, and I can't seem to get it to work right.
The issue
Only the first option in the menu is seen, and no other options are available
It doesn't respond to the mouse other than dimming when clicked
When it is clicked, no errors or output are seen in the terminal, making it hard to trace
The Code
The actual call is made from another file, let's say myproject/main.py
from classes.load_window import *
start_load_menu()
The class for this is stored in a file at myproject/classes/load_window.py, and it accesses save files stored in myproject/saved/
import tkinter
import tkinter.ttk as ttk
from os import listdir
from os.path import join, isfile
class LoadMenu(object):
def __init__(self):
root = self.root = tkinter.Tk()
root.title("Save Manager")
root.overrideredirect(True)
""" MAIN FRAME """
frm_1 = ttk.Frame(root)
frm_1.pack(ipadx=2, ipady=2)
""" MESSAGE LABEL """
self.msg = str("Would you like to load from a save file?")
message = ttk.Label(frm_1, text=self.msg)
message.pack(padx=8, pady=8)
""" INNER FRAME """
frm_2 = ttk.Frame(frm_1)
frm_2.pack(padx=4, pady=4)
""" TEST IMPLEMENTAITON [DOES NOT WORK] """
mylist = ['1', '2', '3', '4', '5', '6', '7']
test_var = tkinter.StringVar(frm_2)
test_var.set(mylist[3])
test_dropdown = ttk.OptionMenu(frm_2, test_var, *mylist)
test_dropdown.pack(padx=4, pady=4)
print(mylist) # Results in ['1', '2', '3', '4', '5', '6', '7']
""" REAL IMPLEMENTATION [ALSO DOES NOT WORK] """
files = [f for f in listdir('saved') if isfile(join('saved', f))]
file_var = tkinter.StringVar(frm_2)
file_var.set(files[3])
file_dropdown = ttk.OptionMenu(frm_2, file_var, *files)
file_dropdown.pack(padx=4, pady=4)
print(files) # Results in ['DS_Store', 'test1', 'test2', 'test3']
""" BUTTON FUNCTIONALITY """
btn_1 = ttk.Button(frm_2, width=8, text="Load File")
btn_1['command'] = self.b1_action
btn_1.pack(side='left')
btn_2 = ttk.Button(frm_2, width=8, text="Cancel")
btn_2['command'] = self.b2_action
btn_2.pack(side='left')
btn_3 = ttk.Button(frm_2, width=8, text="Create New")
btn_3['command'] = self.b3_action
btn_3.pack(side='left')
btn_2.bind('<KeyPress-Return>', func=self.b3_action)
root.update_idletasks()
""" Position the window """
xp = (root.winfo_screenwidth() // 2) - (root.winfo_width() // 2)
yp = (root.winfo_screenheight() // 2) - (root.winfo_height() // 2)
geom = (root.winfo_width(), root.winfo_height(), xp, yp)
root.geometry('{0}x{1}+{2}+{3}'.format(*geom))
root.protocol("WM_DELETE_WINDOW", self.close_mod)
root.deiconify()
def b1_action(self, event=None):
print("B1")
def b2_action(self, event=None):
self.root.quit()
def b3_action(self, event=None):
print("B3")
def nothing(self):
print("nothing")
def close_mod(self):
pass
def time_out(self):
print ("TIMEOUT")
def to_clip(self, event=None):
self.root.clipboard_clear()
self.root.clipboard_append(self.msg)
def start_load_menu():
menu = LoadMenu()
menu.root.mainloop()
menu.root.destroy()
return menu.returning
Notes
This code is based on a response here for a pop up window that I'm in the process of adapting for a specific purpose (the load menu).
I distilled this code to the minimum to reproduce the issue, but you can probably ignore the function definitions and window geometry.
Everything works fine other than this; the window is displayed center screen, and the button with actual functionality closes the window, it's just this odd quirk with the OptionMenu that I can't seem to find anyone else struggling with, either on here, or other forums.
In case you didn't see the link at the top, you can find a demonstration of the troublesome behavior at this link.
I'm using Python 3.6.4 on OSX 10.12.6
EDIT:
I've since tested this code in a VM running Hydrogen Linux, and it works fine. My question then changes a little:
How can I ensure that this code translates well to OSX? Is there reading available on the discrepancies between running TKinter on different platforms?
I have found this page on the issues regarding Python, TKinter, and OSX, but even when using the recommended TCL packages with the latest stable release of Python, this issue persists.
EDIT 2:
Just to update, I have since found a workaround for the problem. It doesn't answer the question of the odd behavior of the OptionMenu, but I figured I would edit. Honestly, I think Listbox is probably better suited for what I wanted to do anyways. Here it is in action.
Please let me know if I need to make any edits for clarity, or provide additional info. As I'm new to stackoverflow, I don't have much experience sharing issues here. Thank you!
Doing nothing other than changing "files" to a hard-coded list allows the program to run on my machine. I can help you no further.
import tkinter
import tkinter.ttk as ttk
from os import listdir
from os.path import join, isfile
class LoadMenu(object):
def __init__(self):
root = self.root = tkinter.Tk()
root.title("Save Manager")
root.overrideredirect(True)
""" MAIN FRAME """
frm_1 = ttk.Frame(root)
frm_1.pack(ipadx=2, ipady=2)
""" MESSAGE LABEL """
self.msg = str("Would you like to load from a save file?")
message = ttk.Label(frm_1, text=self.msg)
message.pack(padx=8, pady=8)
""" INNER FRAME """
frm_2 = ttk.Frame(frm_1)
frm_2.pack(padx=4, pady=4)
""" TEST IMPLEMENTAITON [DOES NOT WORK] """
mylist = ['1', '2', '3', '4', '5', '6', '7']
test_var = tkinter.StringVar(frm_2)
test_var.set(mylist[3])
test_dropdown = ttk.OptionMenu(frm_2, test_var, *mylist)
test_dropdown.pack(padx=4, pady=4)
print(mylist) # Results in ['1', '2', '3', '4', '5', '6', '7']
""" REAL IMPLEMENTATION [ALSO DOES NOT WORK] """
##files = [f for f in listdir('saved') if isfile(join('saved', f))]
files=['a', 'b', 'c', 'd', 'e', 'f']
file_var = tkinter.StringVar(frm_2)
file_var.set(files[3])
file_dropdown = ttk.OptionMenu(frm_2, file_var, *files)
file_dropdown.pack(padx=4, pady=4)
print(files) # Results in ['DS_Store', 'test1', 'test2', 'test3']
""" BUTTON FUNCTIONALITY """
btn_1 = ttk.Button(frm_2, width=8, text="Load File")
btn_1['command'] = self.b1_action
btn_1.pack(side='left')
btn_2 = ttk.Button(frm_2, width=8, text="Cancel")
btn_2['command'] = self.b2_action
btn_2.pack(side='left')
btn_3 = ttk.Button(frm_2, width=8, text="Create New")
btn_3['command'] = self.b3_action
btn_3.pack(side='left')
btn_2.bind('<KeyPress-Return>', func=self.b3_action)
root.update_idletasks()
""" Position the window """
xp = (root.winfo_screenwidth() // 2) - (root.winfo_width() // 2)
yp = (root.winfo_screenheight() // 2) - (root.winfo_height() // 2)
geom = (root.winfo_width(), root.winfo_height(), xp, yp)
root.geometry('{0}x{1}+{2}+{3}'.format(*geom))
root.protocol("WM_DELETE_WINDOW", self.close_mod)
root.deiconify()
def b1_action(self, event=None):
print("B1")
def b2_action(self, event=None):
self.root.quit()
def b3_action(self, event=None):
print("B3")
def nothing(self):
print("nothing")
def close_mod(self):
pass
def time_out(self):
print ("TIMEOUT")
def to_clip(self, event=None):
self.root.clipboard_clear()
self.root.clipboard_append(self.msg)
##def start_load_menu():
menu = LoadMenu()
menu.root.mainloop()
## menu.root.destroy()
## return menu.returning
I figured it out!
After digging into this problem further, I distilled the code to the minimum (which I probably should have done before posting here...) and was able to pinpoint root.overrideredirect(True) as the offending line.
When using overrideredirect(True), it's necessary to also use update_idletasks() before, in order to ensure that the Widget refreshes properly. While it seems Linux can still produce normal behavior without manually updating idle tasks, OS X cannot, so it becomes necessary to preface the code with
root.update_idletasks()
Here's a good excerpt from the documentation that I found on Billal BEGUERADJ's response on an overrideredirect() question.
If you want to force the display to be updated before the application next idles, call the w.update_idletasks() method on any widget.
Some tasks in updating the display, such as resizing and redrawing widgets, are called idle tasks because they are usually deferred until the application has finished handling events and has gone back to the main loop to wait for new events.
If you want to force the display to be updated before the application next idles, call the w.update_idletasks() method on any widget.
While I still don't understand why exactly this widget breaks without update_idletasks() on OSX, I do understand now why it's good practice to use update_idletasks() in conjunction with overrideredirect() to ensure consistent behavior.
Hope this helps anyone else who may get hung up on this.

When dragging multiple items from QListWidget, non-draggable items get removed

I have two QListWidgets. The user can select multiple items from one list and drag them to the other list. But within each list, some items are draggable and some are not. If the selection contains both draggable and non-draggable items, a problem happens. Only the draggable items appear in the second list, which is correct. But all the items disappear from the first list.
In the animated image above, items 00, 01, and 02 are selected. Only items 00 and 02 are drag enabled. After the drag-and-drop, all three items are gone from the first list. How can I fix this?
Here is some code to reproduce the problem:
import random
import sys
from PySide import QtCore, QtGui
class TestMultiDragDrop(QtGui.QMainWindow):
def __init__(self, parent=None):
super(TestMultiDragDrop, self).__init__(parent)
centralWidget = QtGui.QWidget()
self.setCentralWidget(centralWidget)
layout = QtGui.QHBoxLayout(centralWidget)
self.list1 = QtGui.QListWidget()
self.list1.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
self.list1.setDefaultDropAction(QtCore.Qt.MoveAction)
self.list1.setSelectionMode(QtGui.QListWidget.ExtendedSelection)
self.list2 = QtGui.QListWidget()
self.list2.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
self.list2.setDefaultDropAction(QtCore.Qt.MoveAction)
self.list2.setSelectionMode(QtGui.QListWidget.ExtendedSelection)
layout.addWidget(self.list1)
layout.addWidget(self.list2)
self.fillListWidget(self.list1, 8, 'someItem')
self.fillListWidget(self.list2, 4, 'anotherItem')
def fillListWidget(self, listWidget, numItems, txt):
for i in range(numItems):
item = QtGui.QListWidgetItem()
newTxt = '{0}{1:02d}'.format(txt, i)
if random.randint(0, 1):
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
else:
# If the item is draggable, indicate it with a *
newTxt += ' *'
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsDragEnabled)
item.setText(newTxt)
listWidget.addItem(item)
def openMultiDragDrop():
global multiDragDropUI
try:
multiDragDropUI.close()
except:
pass
multiDragDropUI = TestMultiDragDrop()
multiDragDropUI.setAttribute(QtCore.Qt.WA_DeleteOnClose)
multiDragDropUI.show()
return multiDragDropUI
if __name__ == '__main__':
app = QtGui.QApplication([])
openMultiDragDrop()
sys.exit(app.exec_())
Here I have some suspicion on setDefaultDropAction(QtCore.Qt.MoveAction)
Read below para from documentation: Specially the bold line
In the simplest case, the target of a drag and drop action receives a copy of the data being dragged, and the source decides whether to delete the original. This is described by the CopyAction action. The target may also choose to handle other actions, specifically the MoveAction and LinkAction actions. If the source calls QDrag::exec(), and it returns MoveAction, the source is responsible for deleting any original data if it chooses to do so. The QMimeData and QDrag objects created by the source widget should not be deleted - they will be destroyed by Qt.
(http://doc.qt.io/qt-4.8/dnd.html#overriding-proposed-actions)
First give a try with QtCore.Qt.CopyAction
Second, if MoveAction is mandatory, try creating QMimeData and QDrag objects in your source list widget's mouseMoveEvent.
Here in below link, you can find some help for creating QMimeData and QDrag objects in your source list widget's mouseMoveEvent. (code is in C++, My intention is to get conceptual idea).
http://doc.qt.io/qt-4.8/dnd.html#overriding-proposed-actions
I think Kuba Ober is right that this is a Qt bug. In the C++ source code, there is a function void QAbstractItemViewPrivate::clearOrRemove(). It deletes all selected rows, but it does not look at whether each item is drag-enabled or not.
That being the case, I came up with a few workarounds:
Method 1: Make all non-draggable items non-selectable as well
This is the easiest method. Just remove the QtCore.Qt.ItemIsEnabled flag from all non-draggable items. Of course if you want all of your items to be selectable, this won't work.
Method 2: Recreate the "startDrag" function
Since the clearOrRemove function belongs to a private class, I cannot override it. But that function is called by the startDrag function, which can be overridden. So I essentially duplicated the function in Python and replaced the call to clearOrRemove with my own function removeSelectedDraggableItems.
The problem with this method is that startDrag contains calls to a few other functions belonging to a private class. And those functions call other private class functions. Specifically, these functions are responsible for controlling how the items are drawn during the drag event. Since I didn't want to recreate all the functions, I just ignored those. The result is that this method results in the correct functionality, but it loses the graphical indication of which items are being dragged.
class DragListWidget(QtGui.QListWidget):
def __init__(self):
super(DragListWidget, self).__init__()
def startDrag(self, supportedDragActions):
indexes = self.getSelectedDraggableIndexes()
if not indexes:
return
mimeData = self.model().mimeData(indexes)
if not mimeData:
return
drag = QtGui.QDrag(self)
rect = QtCore.QRect()
# "renderToPixmap" is from a private class in the C++ code, so I can't use it.
#pixmap = renderToPixmap(indexes, rect)
#drag.setPixmap(pixmap)
drag.setMimeData(mimeData)
# "pressedPosition" is from a private class in the C++ code, so I can't use it.
#drag.setHotSpot(pressedPostion() - rect.topLeft())
defaultDropAction = self.defaultDropAction()
dropAction = QtCore.Qt.IgnoreAction
if ((defaultDropAction != QtCore.Qt.IgnoreAction) and
(supportedDragActions & defaultDropAction)):
dropAction = defaultDropAction
elif ((supportedDragActions & QtCore.Qt.CopyAction) and
(self.dragDropMode() != self.InternalMove)):
dropAction = QtCore.Qt.CopyAction
dragResult = drag.exec_(supportedDragActions, dropAction)
if dragResult == QtCore.Qt.MoveAction:
self.removeSelectedDraggableItems()
def getSelectedDraggableIndexes(self):
""" Get a list of indexes for selected items that are drag-enabled. """
indexes = []
for index in self.selectedIndexes():
item = self.itemFromIndex(index)
if item.flags() & QtCore.Qt.ItemIsDragEnabled:
indexes.append(index)
return indexes
def removeSelectedDraggableItems(self):
selectedDraggableIndexes = self.getSelectedDraggableIndexes()
# Use persistent indices so we don't lose track of the correct rows as
# we are deleting things.
root = self.rootIndex()
model = self.model()
persistentIndices = [QtCore.QPersistentModelIndex(i) for i in selectedDraggableIndexes]
for pIndex in persistentIndices:
model.removeRows(pIndex.row(), 1, root)
Method 3: Hack "startDrag"
This method changes the drop action from "MoveAction" to "CopyAction" before calling the built-in "startDrag" method. Then it calls a custom function for deleting the selected drag-enabled items. This solves the problem of losing the graphical dragging animation.
This is a pretty easy hack, but it comes with its own problem. Say the user installs an event filter that changes the drop action from "MoveAction" to "IgnoreAction" in certain cases. This hack code doesn't get the updated value. It will still delete the items as though the action is "MoveAction". (Method 2 does not have this problem.) There are workarounds for this problem, but I won't go into them here.
class DragListWidget2(QtGui.QListWidget):
def startDrag(self, supportedDragActions):
dropAction = self.defaultDropAction()
if dropAction == QtCore.Qt.MoveAction:
self.setDefaultDropAction(QtCore.Qt.CopyAction)
super(DragListWidget2, self).startDrag(supportedDragActions)
if dropAction == QtCore.Qt.MoveAction:
self.setDefaultDropAction(dropAction)
self.removeSelectedDraggableItems()
def removeSelectedDraggableItems(self):
# Same code from Method 2. Removed here for brevity.
pass

ipywidgets dropdown widgets: what is the onchange event?

I can register a handler to button.on_click in ipython notebook widgets, but I don't know how to do the same for a dropdown widget
import ipywidgets as widgets
from IPython.display import display
def on_button_clicked(b):
print("Button clicked.")
button = widgets.Button(description="Click Me!")
display(button)
button.on_click(on_button_clicked)
But for
choose_task = widgets.Dropdown(
options=['Addition', 'Multiplication', 'Subtraction'],
value='Addition',
description='Task:',
)
there seems to be only
on_trait_change(...)
if I register a handler with this, can I use it to access the value of the widget?
I have seen examples with the handler and the widget belong to a subclass, and the handler can use self to introspect. But if I don't want to use a subclass, how does the handler know what widget was the target of the event.?
Between this link and the traitlet docs on github and just playing around, I finally figured this out:
w = widgets.Dropdown(
options=['Addition', 'Multiplication', 'Subtraction', 'Division'],
value='Addition',
description='Task:',
)
def on_change(change):
if change['type'] == 'change' and change['name'] == 'value':
print("changed to %s" % change['new'])
w.observe(on_change)
display(w)
Overall this looks a lot richer than the deprecated interface, but it could definitely use more examples.
You can specify the change name in observe. This makes for cleaner code, and the handler is not called for changes you don't need:
from IPython.display import display
from ipywidgets import Dropdown
def dropdown_eventhandler(change):
print(change.new)
option_list = (1, 2, 3)
dropdown = Dropdown(description="Choose one:", options=option_list)
dropdown.observe(dropdown_eventhandler, names='value')
display(dropdown)
Put it all together
Inspired on previous answers and lambda expressions I use this:
def function(option):
print(option)
w = widgets.Dropdown(
options=['None', 'Option 1', 'Option 2', 'Option 3'],
description='Option:',
disabled=False
)
w.observe(
lambda c: plot_content(c['new']) if (c['type'] == 'change' and c['name'] == 'value') else None
)
display(w)
I agree that event handling is not as thorough as would be desired: I have been filtering the events as you receive multiple events for a typical dropdown change as the index changes, the value changes, i.e., change['name'].
I am doing the following:
def on_dropdown_change(change):
if change['name'] == 'value' and (change['new'] != change['old']):
print('do something with the change')
dropdown = ipywidgets.Dropdown({options=['one','two','three'],
value='one'})
dropdown.observe(on_dropdown_change)
I believe the idea is to use trait name, e.g. value. For example:
from ipywidgets import Dropdown
def handle_change():
print type_sel.value
type_sel = Dropdown(description="Keypoint type", options=['surf', 'orb'])
type_sel.on_trait_change(handle_change, name="value")
display(type_sel)
SciPy 2015 Advanced Jupyter Video Tutorial
I had the same issue. This also begs the next question, how to interface button actions based on dropdown menu selections.
# Common Imports for Widgets
from __future__ import print_function
from ipywidgets import interact, interactive, fixed, interact_manual
import ipywidgets as widgets
'''
Precusor:
<class 'traitlets.utils.bunch.Bunch'> It is a dictionary-like object containing:
{'name': 'value', 'old': 'what_ever_the_old_value_was', 'new': 'what_ever_the_new_value_is',
'owner': Dropdown(description='the_user_defined_label:', index=1, # I'm not sure what this is
options=()#list of options passed,
value='value_kwarg_value'), 'type': 'change'} # type: action_or_event type
For more information see:
https://traitlets.readthedocs.io/en/stable/using_traitlets.html#default-values-and-checking-type-and-value
or
https://github.com/jupyter-widgets/tutorial/blob/master/notebooks/08.00-Widget_Events.ipynb
or a long but well done SciPy talk on the use of widgets #
https://www.youtube.com/watch?v=HaSpqsKaRbo
'''
foo = ['a','b','c'] # List to use
# Function to apply to drop box object
def bar(x):
'''
I am intentionally passing what it is made of so you can see the output.
'''
print(x,'\n') # Whole object
print(x.new,'\n') # New value
# Function for the button to select user input and do work
def get_user_selection(a): # A default arg is needed here, I am guessing to pass self
# Displays the current value of dropbox1 and dropbox two
display(dropbox1.value,dropbox2.value)
# creation of a widget dropdown object called dropbox1
dropbox1 = widgets.Dropdown(
options=foo, # Object to iterate over
description='Letter:', # User defined
value=foo[1], # Default value selection
rows=len(foo), # The number of rows to display when showing the box
interactive=True, # This makes the box interactive, I believe this is true by default
);
# Drop box of k,v like pairs
dropbox2 = widgets.Dropdown(
options=[('One', 1), ('Two', 2), ('Three', 3)],
value=2,
description='Number:',
)
# Button to click
select_button = widgets.Button(
description='Click', # User defined
disabled=False
)
# Event Handlers
dropbox1.observe(bar,names='value')
dropbox2.observe(bar,names='value')
select_button.on_click(get_user_selection)
# I you need more help with commands try things like:
# interact_manual?
# display(arg.keys,arg.traits)
# print(widgets.widget_type_here.widget_function_or_attr.__doc__)
# Create a UI object to display things. There are other ways of organizing them.
ui = widgets.HBox([dropbox1,dropbox2,select_button]) # pass an array of widgets to the ui
# display the UI
display(ui)
This will display the following after a couple of clicks.

How to deselect all items in QTreeWidget?

I have tried every suggestion I can come across online, from setting flags to using selectionModel
def DatabaseLoadWrapper(self,database, init):
self.treeWidget.currentItemChanged.disconnect(self.updateStackedWidget)
self.DatabaseLoad(database, init)
self.treeWidget.clearSelection()
self.treeWidget.setCurrentItem(self.treeWidget.findItems(self.selectedDatabase,Qt.MatchExactly|Qt.MatchRecursive)[0])
self.treeWidget.currentItemChanged.connect(self.updateStackedWidget)
This is where my code needs to force a selection on the QTreeWidget, none of the code I use throws up any errors but also has no effect on the selection. And I end up with this where the user has selected Database 1 but I need to revert back to having only Database 2 selected:
Edit: The Tree Widget is built using this code:
def setupMenu(self):
self.DatabaseParent = QTreeWidgetItem(['Databases'])
for item in NamesInDatabase():
self.DatabaseParent.addChild(QTreeWidgetItem([item]))
self.AverageParent = QTreeWidgetItem(['Averaged Database'])
self.SortingParent = QTreeWidgetItem(['Waste Composition'])
self.ResultParent = QTreeWidgetItem(['Results'])
self.treeWidget.addTopLevelItem(self.DatabaseParent)
self.treeWidget.addTopLevelItem(self.AverageParent)
self.treeWidget.addTopLevelItem(self.SortingParent)
self.treeWidget.addTopLevelItem(self.ResultParent)
It basically is adding databases, averaged database, waste compisition & results, as fixed parts of the navigation menu and then populating children of databases with the names of the databases in the save file.
Your question fails to expose the part of the code that is causing the problem. By default, setting the current item, as you do, also sets the selection. So this code, for example, correctly sets the selection to item "b":
from PySide import QtCore,QtGui
if __name__ == '__main__':
import sys
qApp = QtGui.QApplication(sys.argv)
treeWidget = QtGui.QTreeWidget()
parent = QtGui.QTreeWidgetItem(['Databases'])
items = []
for item_text in ["a","b","c"]:
item = QtGui.QTreeWidgetItem([item_text])
items.append(item)
parent.addChild(item)
treeWidget.addTopLevelItem(parent)
treeWidget.setCurrentItem(items[1])
treeWidget.show()
sys.exit(qApp.exec_())
However, I suspect there is code elsewhere in your project that is affecting this. For example, if you had set the selection mode for the QTableWidget selection model to MultiSelection then selections become cumulative:
from PySide import QtCore,QtGui
if __name__ == '__main__':
import sys
qApp = QtGui.QApplication(sys.argv)
treeWidget = QtGui.QTreeWidget()
parent = QtGui.QTreeWidgetItem(['Databases'])
items = []
for item_text in ["a","b","c"]:
item = QtGui.QTreeWidgetItem([item_text])
items.append(item)
parent.addChild(item)
treeWidget.addTopLevelItem(parent)
treeWidget.setSelectionMode(QtGui.QAbstractItemView.MultiSelection)
treeWidget.setCurrentItem(items[0])
treeWidget.setCurrentItem(items[2])
treeWidget.show()
sys.exit(qApp.exec_())
However, that still doesn't explain your issue because the clearSelection call should have cleared the preceding selection in any case. Further debugging of your code is needed, for example to check that the wrapper function and the setCurrentItem are being called as you claim. Also check what else is being called subsequent to the DatabaseLoadWrapper.
In Pyside2, This works for me:
If you click on treewidget the selection will be clear.
self.treeWidget.setSelectionMode(QtWidgets.QAbstractItemView.ContiguousSelection)

Resources