How to filter a GTK tree view that uses a TreeStore (and not a ListStore)? - gtk

I am using a Gtk.TreeView with a Gtk.TreeStore as a model for hierarchical data. As an example, let's take a music database organized into three levels: artist/album/title. I would like to filter this tree using a textual search field. For example, typing "Five" into the search field should give a result along the path "Hank Marvin/Heartbeat/Take Five".
My understanding is that I need to create a callback function and register it using Gtk.TreeModelFilter.set_visible_func(). The problem is that making the line for "Take Five" visible is not enough to make it appear, I also have to set all of its parents visible as well. However, that would require me to traverse the tree up to its root and actively make each node visible along that path, which does not fit into the callback pattern.
One way I see to make this logic work with the callback pattern is to check the whole subtree in the callback function, but that way each leaf node would get checked three times. Even though the performance penalty would be acceptable with such a shallow tree, this hack gives me the goosebumps and I would like to refrain from using it:
def visible_callback(self, model, iter, _data=None):
search_query = self.entry.get_text().lower()
if search_query == "":
return True
text = model.get_value(iter, 0).lower()
if search_query in text:
return True
# Horrible hack
for i in range(model.iter_n_children(iter)):
if self.visible_callback(model, model.iter_nth_child(iter, i)):
return True
return False
What is the intended way to filter a tree view in GTK? (My example is written in Python, but a solution for any language binding of GTK would be fine.)

Finally I came up with a solution and since I haven't found any treeview filtering examples on the internet that uses a TreeStore and not a ListStore, I'm posting my solution here as an example:
#! /usr/bin/python
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Pango', '1.0')
from gi.repository import Gtk
from gi.repository import Pango
from gi.repository import GLib
import signal
HIERARCHICAL_DATA = {
"Queen": {
"A Kind of Magic": [ "Who Wants to Live Forever", "A Kind of Magic" ],
"The Miracle": [ "Breakthru", "Scandal" ]
},
"Five Finger Death Punch": {
"The Way of the Fist": [ "The Way of the Fist", "The Bleeding" ],
},
"Hank Marvin": {
"Heartbeat": [ "Oxygene (Part IV)", "Take Five" ]
}
}
ICONS = [ "stock_people", "media-optical", "sound" ]
class TreeViewFilteringDemo(Gtk.Window):
EXPAND_BY_DEFAULT = True
SPACING = 10
# Controls whether the row should be visible
COL_VISIBLE = 0
# Text to be displayed
COL_TEXT = 1
# Desired weight of the text (bold for matching rows)
COL_WEIGHT = 2
# Icon to be displayed
COL_ICON = 3
def __init__(self):
# Set up window
Gtk.Window.__init__(self, title="TreeView filtering demo")
self.set_size_request(500, 500)
self.set_position(Gtk.WindowPosition.CENTER)
self.set_resizable(True)
self.set_border_width(self.SPACING)
# Set up and populate a tree store
self.tree_store = Gtk.TreeStore(bool, str, Pango.Weight, str)
self.add_nodes(HIERARCHICAL_DATA, None, 0)
# Create some boxes for laying out the different controls
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=self.SPACING)
vbox.set_homogeneous(False)
hbox = Gtk.Box(Gtk.Orientation.HORIZONTAL, spacing=self.SPACING)
hbox.set_homogeneous(False)
vbox.pack_start(hbox, False, True, 0)
self.add(vbox)
# A text entry for filtering
self.search_entry = Gtk.Entry()
self.search_entry.set_placeholder_text("Enter text here to filter results")
self.search_entry.connect("changed", self.refresh_results)
hbox.pack_start(self.search_entry, True, True, 0)
# Add a checkbox for controlling subtree display
self.subtree_checkbox = Gtk.CheckButton("Show subtrees of matches")
self.subtree_checkbox.connect("toggled", self.refresh_results)
hbox.pack_start(self.subtree_checkbox, False, False, 0)
# Use an internal column for filtering
self.filter = self.tree_store.filter_new()
self.filter.set_visible_column(self.COL_VISIBLE)
self.treeview = Gtk.TreeView(model=self.filter)
# CellRenderers for icons and texts
icon_renderer = Gtk.CellRendererPixbuf()
text_renderer = Gtk.CellRendererText()
# Put the icon and the text into a single column (otherwise only the
# first column would be indented according to its depth in the tree)
col_combined = Gtk.TreeViewColumn("Icon and Text")
col_combined.pack_start(icon_renderer, False)
col_combined.pack_start(text_renderer, False)
col_combined.add_attribute(text_renderer, "text", self.COL_TEXT)
col_combined.add_attribute(text_renderer, "weight", self.COL_WEIGHT)
col_combined.add_attribute(icon_renderer, "icon_name", self.COL_ICON)
self.treeview.append_column(col_combined)
# Scrolled Window in case results don't fit in the available space
self.sw = Gtk.ScrolledWindow()
self.sw.add(self.treeview)
vbox.pack_start(self.sw, True, True, 0)
# Initialize filtering
self.refresh_results()
def add_nodes(self, data, parent, level):
"Create the tree nodes from a hierarchical data structure"
if isinstance(data, dict):
for key, value in data.items():
child = self.tree_store.append(parent, [True, key, Pango.Weight.NORMAL, ICONS[level]])
self.add_nodes(value, child, level + 1)
else:
for text in data:
self.tree_store.append(parent, [True, text, Pango.Weight.NORMAL, ICONS[level]])
def refresh_results(self, _widget = None):
"Apply filtering to results"
search_query = self.search_entry.get_text().lower()
show_subtrees_of_matches = self.subtree_checkbox.get_active()
if search_query == "":
self.tree_store.foreach(self.reset_row, True)
if self.EXPAND_BY_DEFAULT:
self.treeview.expand_all()
else:
self.treeview.collapse_all()
else:
self.tree_store.foreach(self.reset_row, False)
self.tree_store.foreach(self.show_matches, search_query, show_subtrees_of_matches)
self.treeview.expand_all()
self.filter.refilter()
def reset_row(self, model, path, iter, make_visible):
"Reset some row attributes independent of row hierarchy"
self.tree_store.set_value(iter, self.COL_WEIGHT, Pango.Weight.NORMAL)
self.tree_store.set_value(iter, self.COL_VISIBLE, make_visible)
def make_path_visible(self, model, iter):
"Make a row and its ancestors visible"
while iter:
self.tree_store.set_value(iter, self.COL_VISIBLE, True)
iter = model.iter_parent(iter)
def make_subtree_visible(self, model, iter):
"Make descendants of a row visible"
for i in range(model.iter_n_children(iter)):
subtree = model.iter_nth_child(iter, i)
if model.get_value(subtree, self.COL_VISIBLE):
# Subtree already visible
continue
self.tree_store.set_value(subtree, self.COL_VISIBLE, True)
self.make_subtree_visible(model, subtree)
def show_matches(self, model, path, iter, search_query, show_subtrees_of_matches):
text = model.get_value(iter, self.COL_TEXT).lower()
if search_query in text:
# Highlight direct match with bold
self.tree_store.set_value(iter, self.COL_WEIGHT, Pango.Weight.BOLD)
# Propagate visibility change up
self.make_path_visible(model, iter)
if show_subtrees_of_matches:
# Propagate visibility change down
self.make_subtree_visible(model, iter)
return
win = TreeViewFilteringDemo()
win.connect("delete-event", Gtk.main_quit)
win.show_all()
# Make sure that the application can be stopped from the terminal using Ctrl-C
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, Gtk.main_quit)
Gtk.main()

Related

GTK EntryCompletion by insertion instead of replacement

I've built a GTK application with autocompletion in an Entry, but I'd like the selected "completion" string to be replace the only word that the cursor touches, whereas it currently replaces all text in the Entry.
I can use set_match_func on the EntryCompletion to deliver matches based on only the word that is adjacent to the cursor, but I don't see how to override the text-insertion behaviour. Is there a way for me to do this?
I'm working in Ruby with gtk3. (I linked the doc for gtk2 because for the life of me, I can't find a complete doc for gtk3 in Ruby.)
Edit Here's my implementation (in Ruby), which lacks the desired "insert" behaviour:
module MyAutocomplete
# Add autocomplete to a Gtk::Entry object
def self.add entry, &block
model = Gtk::ListStore.new String
model.append.set_value 0, 'sd'
model.append.set_value 0, 'foo'
model.append.set_value 0, 'six'
completion = Gtk::EntryCompletion.new
completion.set_minimum_key_length 0
completion.set_text_column 0
completion.set_inline_completion true
completion.set_model model
completion.set_match_func do |*args|
self.match_func *args
end
yield(model, completion) if block_given?
entry.set_completion completion
end
def self.match_func(entry_completion, entry_value, list_obj)
len = 0 # Counts characters into the entry text
cursor = entry_completion.entry.position
entry_text = entry_completion.entry.text
entry_tokens = entry_text.scan(/[\w+#]+|[^\w#]+/)
current_token = entry_tokens.find { |tok|
(len += tok.length) >= cursor && tok =~ /\w/
}
obj_text = list_obj.get_value(0)
return current_token && obj_text.start_with?(current_token)
end
end

Limiting a Django form's ManyToManyField queryset in a formtools wizard based on selection on previous form

I'm using a SessionWizardView from django-formtools to construct a two-form wizard. The challenge I'm facing is that I need to reference the input from the first form to limit the available querysets on the second form.
To make it more interesting, I'm using crispy forms for layout and the queryset needs to be limited by a method on a related item.
Here's the (much simplified) gist of where I'm at:
Models
class Product(models.Model):
# pk, name, etc....
catalogitem = ForeignKey("myapp.CatalogItem")
colors = ManyToManyField("myapp.Colors")
class Colors(models.Model):
# pk, name, etc....
class CatalogItem(models.Model):
# Colors are stored within CatalogVariants, which I've left
# as a blackbox in this example, since they are retrieved as
# a queryset on this model with this method:
# pk, name, etc....
def get_colors(self):
# Returns a queryset of color objects.
Views
ProductFormWizard(SessionWizardView):
form_list = [
productFormWizard_Step1,
productFormWizard_Step2,
]
def get_context_data(self, **kwargs):
# ...
pass
def get_form_initial(self, step):
initial = {}
# ...
return self.initial_dict.get(step, initial)
def process_step(self, form):
if self.steps.step1 == 1:
pass
return self.get_form_step_data(form)
def done(self, form_list, **kwargs):
return render(self.request, 'done.html', {
'form_data': [form.cleaned_data for form in form_list],
})
Forms
productFormWizard_Step1(forms.ModelForm):
# Defines a form where the user selects a CatalogProduct.
model = Product
productFormWizard_Step2(forms.ModelForm):
"""
Defines a form where the user chooses colors based on
the CatalogProduct they selected in the previous step.
"""
model = Product
Based on research via the Googles and some SO questions (none of which were =directly= related), I'm assuming I need to set the .queryset property on the colors field, but I'm not exactly sure where to do that. Two thoughts:
I would guess it goes in .get_form_initial() somehow, but I'm at a loss as to the best way to achieve that.
Alternatively, the appropriate code might go into the productFormWizard.get_context_data() method somehow.
Within .get_form_initial(), I can do something like this:
if step == '1':
itemID = self.storage.get_step_data('0').data.get('0-pfProduct', "")
if itemID:
obj = CatalogItem.objects.get(id=itemID)
initial['colors'] = obj.get_get_colors()
However, this just selects the available related items... it doesn't limit the list.
Additional Info
Python == 3.5.3
Django == 1.10.6
django-crispy-forms == 1.6.1
django-formtools == 2.0
The solution is to override the .get_form() method on the View:
def get_form(self, step=None, data=None, files=None):
form = super(bzProductFormWizard, self).get_form(step, data, files)
if step == '1':
past_data = self.get_cleaned_data_for_step('0')
product = past_data['product']
form.fields['colors'].queryset = ... #CUSTOM QUERYSET
return form

The signal.connect syntax

I am trying to create a window with two FileChooserButtons. The first one should help the user pick a directory, thus I am using the action Select_folder; the second is to allow the user pick a file.
The problem is that I wanted the second one to change the current folder depending on the choice the user made in the first one.
My initial idea was to use Signal.connect, as in the line:
Signal.connect(chooser1, "selection_changed", folder_changed, null)
However, this is getting me the following compilation error:
exercise4_1.gs:62.55-62.68: error: Cannot create delegate without target for instance method or closure
Signal.connect(chooser1, "selection_changed", folder_changed, null)
^^^^^^^^^^^^^^
Compilation failed: 1 error(s), 0 warning(s)
I've also tried adding (callback)folder_changed as per this mail communication at vala mailing list, to no avail.
This is the whole code:
[indent=4]
uses
Gtk
GLib
class TestWindow : Window
chooser1:Gtk.FileChooserButton
chooser2:Gtk.FileChooserButton
construct()
// General characteristics of the window
title = "File chooser"
window_position = WindowPosition.CENTER
destroy.connect(Gtk.main_quit)
chooser1 = new FileChooserButton(
"Choose a Folder",
FileChooserAction.SELECT_FOLDER
)
chooser2 = new FileChooserButton(
"Chooser a Folder",
FileChooserAction.OPEN
)
chooser1.set_current_folder(Environment.get_home_dir())
chooser2.set_current_folder(Environment.get_home_dir())
Signal.connect(chooser1, "selection_changed", folder_changed, null)
var box = new Gtk.Box(Gtk.Orientation.VERTICAL, 0)
box.pack_start(chooser1, true, true,0)
box.pack_start(chooser2, true, true,0)
add(box)
def folder_changed()
var folder = chooser1.get_filename()
chooser2.set_current_folder(folder)
init
Gtk.init (ref args)
var test = new TestWindow ()
test.show_all ()
Gtk.main ()
It is certainly my lack of understanding about this particular syntax, but since I am stuck, I would appreciate a pointer to get me out of it.
As an extra, less important point, what is the best practice: to split and indent long lines or to allow them in the code?
A callback for Gtk needs to include a parameter for the object that generated the signal. Also Genie and Vala have syntax support for GLib signals to make signals easier to work with. Here is an example based on your code:
[indent=4]
uses
Gtk
class TestWindow:Window
_file_chooser:FileChooserButton
construct()
title = "File chooser"
window_position = WindowPosition.CENTER
destroy.connect( Gtk.main_quit )
var folder_chooser = new FileChooserButton(
"Choose a Folder",
FileChooserAction.SELECT_FOLDER
)
folder_chooser.set_current_folder( Environment.get_home_dir() )
folder_chooser.selection_changed.connect( folder_changed )
_file_chooser = new FileChooserButton(
"Chooser a File",
FileChooserAction.OPEN
)
_file_chooser.set_current_folder( Environment.get_home_dir() )
var box = new Box( Orientation.VERTICAL, 0 )
box.pack_start( folder_chooser, true, true, 0 )
box.pack_start( _file_chooser, true, true, 0 )
add( box )
def folder_changed( folder_chooser_widget:FileChooser )
folder:string = folder_chooser_widget.get_uri()
_file_chooser.set_current_folder_uri( folder )
init
Gtk.init( ref args )
var test = new TestWindow()
test.show_all()
Gtk.main()
A few points to note:
The signal name, "selection_changed" has become an attribute of folder_chooser which you then connect to. The Vala compiler does the conversion to GLib.Signal at compile time
The FileChooserButton, folder_chooser, has been removed from the scope of the class. It is now accessed by being passed as an argument to the callback. So it is defined as a parameter of the callback function
You will notice the parameter for the callback expects a FileChooser type and not a FileChooserButton type. This is because the selection_changed signal is part of the FileChooser interface that the FileChooserButton then implements. This effectively gives a FileChooserButton more than one type
Although _file_chooser is declared so it is available within the whole scope of the class, it has been made only accessible within the class by using the underscore
Using Signal.connect() is much closer to the C API for Gtk. If you need to do this then the following works based on your original code:
[indent=4]
uses
Gtk
class TestWindow:Window
chooser1:FileChooserButton
chooser2:FileChooserButton
construct()
// General characteristics of the window
title = "File chooser"
window_position = WindowPosition.CENTER
destroy.connect( Gtk.main_quit )
chooser1 = new FileChooserButton(
"Choose a Folder",
FileChooserAction.SELECT_FOLDER
)
chooser2 = new FileChooserButton(
"Chooser a Folder",
FileChooserAction.OPEN
)
chooser1.set_current_folder( Environment.get_home_dir() )
chooser2.set_current_folder( Environment.get_home_dir() )
Signal.connect(
chooser1,
"selection_changed",
(GLib.Callback)folder_changed,
self
)
var box = new Box( Orientation.VERTICAL, 0 )
box.pack_start( chooser1, true, true, 0 )
box.pack_start( chooser2, true, true, 0 )
add( box )
[CCode( instance_pos = 2 )]
// or [CCode( instance_pos = -1 )] to always be last
def folder_changed( folder_chooser:Widget )
folder:string = chooser1.get_uri()
chooser2.set_current_folder_uri( folder )
init
Gtk.init( ref args )
var test = new TestWindow()
test.show_all()
Gtk.main()
A few points to note:
Yes you do need to cast the callback to GLib.Callback as you found in the mail message you linked to
The instance data you need is the Window object you have created the FileChooserButton for, so changing null to self works here
Vala will put instance data as the first parameter, so to override the default you have to use a CCode attribute, that is [CCode( instance_pos = 2 )] in this case
The object generating the signal is still expected to be the first parameter of the callback function, so it is there in the definition even though it is unused in this example. This is defined as Widget type, but you can change this to FileChooser to use the get_uri() call
For your code formatting question I prefer to split long lines, as you can see. I'm not sure there is an agreed "best practise" for Genie as yet.

How can I use pyglet batches to draw scenes or levels

So I'm currently learning pyglet for Python 2.7 and I'm trying to make a simple game that has levels. The 1st 'scene' would be the title/intro part, 2nd would be a tutorial of some sort, and the rest are the game levels themselves.
For this, I've created 7 batches(1 intro, 1 tutorial, 5 levels) namely batch, batch1, ... batch6. I've also created 7 classes for each of these batches that represent the scenes/levels. This is what I've done for the intro batch and class:
batch = pyglet.graphics.Batch()
batch1 = pyglet.graphics.Batch()
class StartState:
def __init__(self):
self.welcome = pyglet.text.Label('WELCOME TO', font_name='Arial', font_size=32, color=(200,255,255,255), x=400, y=550, anchor_x='center', anchor_y='center', batch=batch)
self.title = pyglet.text.Label("MY GAME", font_name='Arial', font_size=32, color=(100,200,170,255), x=400, y=450, anchor_x='center', anchor_y='center', batch=batch)
self.press = pyglet.text.Label("press 'SPACE' to continue", font_name='Arial', font_size=32, color=(200,255,150,255), x=400, y=250, anchor_x='center', anchor_y='center', batch=batch)
def update(self, dt):
if keymap[pyglet.window.key.SPACE]:
self.welcome.delete()
self.title.delete()
self.press.delete()
states.pop()
batch1.draw()
The other scenes would also look like that. the states list is a list that I use to store my classes/scenes. states = [Level5(), Level4(), ... , TutorialState(), StartState()]. So every time the condition to advance is fulfilled, which in this class is to press 'SPACE', the window will be 'cleared' i.e. delete the sprites/labels and proceed to the next scene by using states.pop() and batch1.draw().
After I've typed these classes, I added this at the end:
#window.event
def on_draw():
window.clear()
batch.draw()
def update(dt):
if len(states):
states[-1].update(dt)
else:
pyglet.app.exit()
states.append(Level5())
states.append(Level4())
states.append(Level3())
states.append(Level2())
states.append(Level1())
states.append(TutorialState())
states.append(StartState())
pyglet.clock.schedule_interval(update, 1.0/60.0)
window.clear()
window.flip()
window.set_visible(True)
pyglet.app.run()
The problem here is that it only loads the starting batch/scene. Whenever I press 'SPACE' to go to the tutorial scene the labels/sprites of the starting batch disappear but it doesn't draw batch1 or load the the tutorial class/scene. Any suggestions?
After creating a batch for each scene class:
import pyglet
from pyglet.window import key
class SceneTemplate(object):
"""a template with common things used by every scene"""
def __init__(self, text):
self.batch = pyglet.graphics.Batch()
self.label = pyglet.text.Label(
text,
font_name='Arial', font_size=32,
color=(200, 255, 255, 255), x=32, y=704,
batch=self.batch)
# (...)
class MainMenuScene(SceneTemplate):
def __init__(self):
super(MainMenuScene, self).__init__(text='MainMenuScene')
# (...)
class IntroScene(SceneTemplate):
def __init__(self):
super(IntroScene, self).__init__(text='Introduction')
# (...)
class Level1(SceneTemplate):
def __init__(self):
super(Level1, self).__init__(text='Level 1')
# (...)
You can control the state/scene in another class, such as a window class (personally I like to subclass the pyglet window, to keep things organized and some other reasons):
class Window(pyglet.window.Window):
def __init__(self):
super(Window, self).__init__(width=1024, height=768)
self.states = [MainMenuScene(), IntroScene(), Level1()] # and so on...
self.current_state = 0 # later you change it to get the scene you want
self.set_visible()
def on_draw(self):
self.clear()
self.states[self.current_state].batch.draw()
def on_key_press(self, symbol, modifiers):
if symbol == key.SPACE:
new_state = self.current_state + 1
new_state = new_state % len(self.states)
self.current_state = new_state
# if you want each scene to handle input, you could use pyglet's push_handlers(), or even something like:
# self.states[self.current_state].on_key_press(symbol, modifiers)
# giving them access to the window instance might be needed.
if __name__ == '__main__':
window = Window()
pyglet.app.run()

PySide - QSortFilterProxyModel and QListView - indexWidget pointer get deleted when filtering

I've a problem with a custom QListView I'm trying to make, here the problem:
I'm using QListView to show a list of QWidget by using QListView.setIndexWidget(index,widget).
This is working pretty fine, but now I want to filter the items model by using QSortFilterProxyModel()
with .setFilterWildcard()
It is not working very well because the second time the model is filtered
I got error like this :
RuntimeError: Internal C++ object (PySide.QtGui.QLabel) already deleted.
Without using filtering and QSortFilterProxyModel everything works fine, but it seems I'm missing
something with the filtering operation, the indexWidget() is deleted when using filtering :(
here a sample code where you can reproduce the bug, when list view is shown, hit 1,2 or 3 keyboard
key to activate filtering ( Backspace to set filtering empty to show all items )
Here the sample code to reproduce the problem:
import PySide.QtGui as QtGui
import PySide.QtCore as QtCore
_DEFAULT_ITEM_SIZE = QtCore.QSize(100, 85)
_USER_ROLE = QtGui.QStandardItem.UserType + 1
class CustomItemWidget(QtGui.QWidget):
def __init__(self, parent=None):
super(CustomItemWidget, self).__init__(parent=parent)
#self.setAutoFillBackground(True)
self.main_layout = QtGui.QVBoxLayout(self)
self.label = QtGui.QLabel(self)
self.main_layout.addWidget(self.label)
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
# Default brush and pen
bg_brush = QtGui.QBrush(QtGui.QColor("#8C8C8C"))
pen = QtCore.Qt.NoPen
painter.save()
painter.setPen(pen)
painter.setBrush(bg_brush)
painter.drawRoundedRect(self.rect(), 12, 12)
painter.restore()
def setData(self, role, value):
if role == QtCore.Qt.DisplayRole:
self.label.setText(value)
class CustomItem(QtGui.QStandardItem):
def __init__(self):
super(CustomItem, self).__init__()
self.number = None
self.item_widget = CustomItemWidget()
self.setSelectable(True)
def type(self):
return _USER_ROLE
def data(self, role):
if role == QtCore.Qt.DisplayRole:
value = "DATA %s" % str(self.number)
self.item_widget.setData(role, value)
return value
if role == QtCore.Qt.SizeHintRole:
return _DEFAULT_ITEM_SIZE
return QtGui.QStandardItem.data(self, role)
class CustomItemDelegate(QtGui.QStyledItemDelegate):
def __init__(self, parent=None):
super(CustomItemDelegate, self).__init__(parent=parent)
class CustomItemModel(QtGui.QStandardItemModel):
def __init__(self, parent=None):
super(CustomItemModel, self).__init__(parent)
def flags(self, index):
return QtCore.Qt.ItemIsEnabled | \
QtCore.Qt.ItemIsSelectable | \
QtCore.Qt.ItemIsDragEnabled | \
QtCore.Qt.ItemIsDropEnabled
class CustomItemFilterProxyModel(QtGui.QSortFilterProxyModel):
def __init__(self, parent=None):
super(CustomItemFilterProxyModel, self).__init__(parent)
self.setDynamicSortFilter(True)
self.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.setFilterKeyColumn(0)
class CustomView(QtGui.QListView):
def __init__(self, parent=None):
super(CustomView, self).__init__(parent=parent)
self.setIconSize(_DEFAULT_ITEM_SIZE)
self.setMovement(QtGui.QListView.Static)
self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
self.setSelectionBehavior(QtGui.QAbstractItemView.SelectItems)
self.setViewMode(QtGui.QListView.IconMode)
self.setUniformItemSizes(True)
self.setFlow(QtGui.QListView.LeftToRight)
self.setResizeMode(QtGui.QListView.Adjust)
self.data_model = CustomItemModel(self)
self.proxy_model = CustomItemFilterProxyModel(self)
self.proxy_model.setSourceModel(self.data_model)
self.setModel(self.proxy_model)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_1:
self.proxy_model.setFilterWildcard("*1*")
print self.proxy_model.filterRegExp()
if event.key() == QtCore.Qt.Key_2:
self.proxy_model.setFilterWildcard("*2*")
print self.proxy_model.filterRegExp()
if event.key() == QtCore.Qt.Key_3:
self.proxy_model.setFilterWildcard("*3*")
print self.proxy_model.filterRegExp()
if event.key() == QtCore.Qt.Key_Backspace:
self.proxy_model.setFilterFixedString("")
print self.proxy_model.filterRegExp()
if event.key() == QtCore.Qt.Key_Plus:
self.addNewItem()
QtGui.QListView.keyPressEvent(self, event)
def addNewItem(self):
item = CustomItem()
item.number = self.data_model.rowCount()
self.addItem(item)
def addItem(self, item):
self.data_model.appendRow(item)
proxy_index = self.proxy_model.mapFromSource(item.index())
self.setIndexWidget(proxy_index, item.item_widget)
if __name__ == '__main__':
import sys
qapplication = QtGui.QApplication(sys.argv)
layout = QtGui.QVBoxLayout()
window = QtGui.QDialog()
window.setLayout(layout)
view = CustomView(window)
view.resize(800, 600)
layout.addWidget(view)
for i in range(0, 10):
item = CustomItem()
item.number = i
view.addItem(item)
window.show()
sys.exit(qapplication.exec_())
or sample code here:
https://gist.github.com/66e29df303d1f1825a53
Can someone please help me on this? is this a known bug ? or I'm doing it completely wrong :P
Thanks in advance for your help.
This is an old question, but as I struggled with a similar problem for quite a while, here the solution I found and a possible explanation:
Instead of caching the custom widget on the model item, I cached the data needed to create the widget. In my case, I wanted to use a custom label with html in order to be able to format parts of text in different colour. Hence, I cached the html string on the item.
Then, in the initStyleOption method of the item delegate, I recreated the widget if it didn't yet exist or had disappeared after filtering:
label = self.parent().indexWidget(modelIndex)
if not label:
label = CustomLabel(item.html)
self.parent().setIndexWidget(modelIndex, label)
The reason why filtering deletes the widget cached on the item is as follows, I believe: the widget can "exist" only in one place. When it is put as indexWidget, it "exists" on a row in the view, not in an item of the model any more. As filtering removes rows from view, widgets on those rows get deleted. - A poor explanation, but I've often got similar surprises when manipulating html elements with JavaScript if I've forgotten to clone the element.