Creating a simple file browser using python and gtkTreeView - python

I am trying to create a simple file browser using python and GTK3. Inspired by an another question here I was able to make a small working example
#!/usr/bin/python
import os
from gi.repository import Gtk
window = Gtk.Window()
window.connect("delete-event", Gtk.main_quit)
filesystemTreeStore = Gtk.TreeStore(str)
parents = {}
for (path, dirs, files) in os.walk("/home"):
for subdir in dirs:
parents[os.path.join(path, subdir)] = filesystemTreeStore.append(parents.get(path, None), [subdir])
for item in files:
filesystemTreeStore.append(parents.get(path, None), [item])
filesystemTreeView = Gtk.TreeView(filesystemTreeStore)
renderer = Gtk.CellRendererText()
filesystemColumn = Gtk.TreeViewColumn("Title", renderer, text=0)
filesystemTreeView.append_column(filesystemColumn)
window.add(filesystemTreeView)
window.show_all()
Gtk.main()
The code works, but the result feels not much effective. I was able to read and display the whole linux filesystem, but it took a very long time. One reason could be the usage of os.walk.
Another thing is, that such code does not allow opening empty directories.
For this reason I would like to display only the content of the parent directory for which the listing is made and expand the tree gradually as the user is exploring the tree structure.
I was not able to find a solution for this yet using Python and GTK3. There is a similar solution but for Tkinter

i was able to come with a solution. There could be a better solution, but I am quite happy that it is working as I expected. I append "dummy" nodes to make the folders expandable, even if the are ampty. Had to deal with adding and removing the tree content on expanding/collapsing the treeView.
Here is my solution:
#!/usr/bin/python
import os, stat
from gi.repository import Gtk
from gi.repository.GdkPixbuf import Pixbuf
def populateFileSystemTreeStore(treeStore, path, parent=None):
itemCounter = 0
# iterate over the items in the path
for item in os.listdir(path):
# Get the absolute path of the item
itemFullname = os.path.join(path, item)
# Extract metadata from the item
itemMetaData = os.stat(itemFullname)
# Determine if the item is a folder
itemIsFolder = stat.S_ISDIR(itemMetaData.st_mode)
# Generate an icon from the default icon theme
itemIcon = Gtk.IconTheme.get_default().load_icon("folder" if itemIsFolder else "empty", 22, 0)
# Append the item to the TreeStore
currentIter = treeStore.append(parent, [item, itemIcon, itemFullname])
# add dummy if current item was a folder
if itemIsFolder: treeStore.append(currentIter, [None, None, None])
#increment the item counter
itemCounter += 1
# add the dummy node back if nothing was inserted before
if itemCounter < 1: treeStore.append(parent, [None, None, None])
def onRowExpanded(treeView, treeIter, treePath):
# get the associated model
treeStore = treeView.get_model()
# get the full path of the position
newPath = treeStore.get_value(treeIter, 2)
# populate the subtree on curent position
populateFileSystemTreeStore(treeStore, newPath, treeIter)
# remove the first child (dummy node)
treeStore.remove(treeStore.iter_children(treeIter))
def onRowCollapsed(treeView, treeIter, treePath):
# get the associated model
treeStore = treeView.get_model()
# get the iterator of the first child
currentChildIter = treeStore.iter_children(treeIter)
# loop as long as some childern exist
while currentChildIter:
# remove the first child
treeStore.remove(currentChildIter)
# refresh the iterator of the next child
currentChildIter = treeStore.iter_children(treeIter)
# append dummy node
treeStore.append(treeIter, [None, None, None])
window = Gtk.Window()
window.connect("delete-event", Gtk.main_quit)
# initialize the filesystem treestore
fileSystemTreeStore = Gtk.TreeStore(str, Pixbuf, str)
# populate the tree store
populateFileSystemTreeStore(fileSystemTreeStore, '/home')
# initialize the TreeView
fileSystemTreeView = Gtk.TreeView(fileSystemTreeStore)
# Create a TreeViewColumn
treeViewCol = Gtk.TreeViewColumn("File")
# Create a column cell to display text
colCellText = Gtk.CellRendererText()
# Create a column cell to display an image
colCellImg = Gtk.CellRendererPixbuf()
# Add the cells to the column
treeViewCol.pack_start(colCellImg, False)
treeViewCol.pack_start(colCellText, True)
# Bind the text cell to column 0 of the tree's model
treeViewCol.add_attribute(colCellText, "text", 0)
# Bind the image cell to column 1 of the tree's model
treeViewCol.add_attribute(colCellImg, "pixbuf", 1)
# Append the columns to the TreeView
fileSystemTreeView.append_column(treeViewCol)
# add "on expand" callback
fileSystemTreeView.connect("row-expanded", onRowExpanded)
# add "on collapse" callback
fileSystemTreeView.connect("row-collapsed", onRowCollapsed)
scrollView = Gtk.ScrolledWindow()
scrollView.add(fileSystemTreeView)
# append the scrollView to the window (this)
window.add(scrollView)
window.connect("delete-event", Gtk.main_quit)
window.show_all()
Gtk.main()

What you need is commonly called lazy loading, which is currently not supported by/on the ideas page of GtkTreeStore but you can still create your own YourTreeStoreLazy which implements the GtkTreeModel interface. This was done a couple of times in the past but I can not seem to find any reasonable code examples. Have a look at this post and its comments(link gone)wayback archive copy for some ideas on how to approach the implementation of getters.

Related

Pre-select multiple files in a QFileDialog

When a "choose files" dialog is displayed I want to pre-select files in a project which are already configured as being "part of" that project, so the user can select new files OR unselect existing (i.e. previously chosen) files.
This answer suggests multiple selection should be possible.
For this MRE, please make 3 files and put them in a suitable ref_dir:
from PyQt5 import QtWidgets
import sys
class Window(QtWidgets.QWidget):
def __init__(self):
super(Window, self).__init__()
self.button = QtWidgets.QPushButton('Test', self)
self.button.clicked.connect(self.handle_button)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.button)
def handle_button(self):
options = QtWidgets.QFileDialog.Options()
options |= QtWidgets.QFileDialog.DontUseNativeDialog
ref_dir = 'D:\\temp'
files_list = ['file1.txt', 'file2.txt', 'file3.txt']
fd = QtWidgets.QFileDialog(None, 'Choose project files', ref_dir, '(*.txt)')
fd.setFileMode(QtWidgets.QFileDialog.ExistingFiles)
fd.setOptions(options)
# fd.setVisible(True)
for file in files_list:
print(f'selecting file |{file}|')
fd.selectFile(file)
string_list = fd.exec()
print(f'string list {string_list}')
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
Unfortunately, despite ExistingFiles having been chosen as the file mode, I find that it is only the last file selected which has the selection... but I want all three to be selected when the dialog is displayed.
I tried experimenting with setVisible to see whether the multiple selection could be achieved somehow after the dialog is displayed, but this didn't work.
Since a non-native file dialog is being used, we can access its child widgets to control its behavior.
At first I thought about using the selection model of the item views, but this won't update the line edit, which is responsible of checking if the files exist and enabling the Ok button in that case; considering this, the obvious solution is to directly update the line edit instead:
def handle_button(self):
# ...
existing = []
for file in files_list:
if fd.directory().exists(file):
existing.append('"{}"'.format(file))
lineEdit = fd.findChild(QtWidgets.QLineEdit, 'fileNameEdit')
lineEdit.setText(' '.join(existing))
if fd.exec():
print('string list {}'.format(fd.selectedFiles()))
The only drawback of this approach is that the fileSelected and filesSelected signals are not sent.
Musicamante's answer was very, very helpful, in particular showing that the selection is in fact triggered by filling the QLE with path strings.
But in fact there is a fatal flaw when the purpose is as I have stated: unfortunately, if you try to deselect the final selected file in a directory, actually this name is not then removed from the QLE. And in fact, if the QLE is set to blank this disables the "Choose" button. All this is by design: the function of a QFileDialog is either to "open" or to "save", not to "modify".
But I did find a solution, which involves finding the QListView which lists the files in the directory, and then using a signal on its selection model.
Another thing this caters for is what happens when you change directory: obviously, you then want the selection to be updated on the basis of the project's files as found (or not found) inside that directory. I've in fact changed the text of the "choose" button to show that "modification" is the name of the game.
fd = QtWidgets.QFileDialog(app.get_main_window(), 'Modify project files', start_directory, '(*.docx)')
fd.setFileMode(QtWidgets.QFileDialog.ExistingFiles)
fd.setViewMode(QtWidgets.QFileDialog.List)
fd.setLabelText(QtWidgets.QFileDialog.Reject, '&Cancel')
fd.setLabelText(QtWidgets.QFileDialog.Accept, '&Modify')
fd.setOptions(options)
file_name_line_edit = fd.findChild(QtWidgets.QLineEdit, 'fileNameEdit')
list_view = fd.findChild(QtWidgets.QListView, 'listView')
# utility to cope with all permutations of backslashes and forward slashes in path strings:
def split_file_path_str(path_str):
dir_path_str, filename = ntpath.split(path_str)
return dir_path_str, (filename or ntpath.basename(dir_path_str))
fd.displayed_dir = None
sel_model = list_view.selectionModel()
def sel_changed():
if not fd.displayed_dir:
return
selected_file_paths_in_shown_dir = []
sel_col_0s = sel_model.selectedRows()
for sel_col_0 in sel_col_0s:
file_path_str = os.path.join(fd.displayed_dir, sel_col_0.data())
selected_file_paths_in_shown_dir.append(file_path_str)
already_included = file_path_str in self.files_list
if not already_included:
fd.project_files_in_shown_dir.append(file_path_str)
# now find if there are any project files which are now NOT selected
for project_file_path_str in fd.project_files_in_shown_dir:
if project_file_path_str not in selected_file_paths_in_shown_dir:
fd.project_files_in_shown_dir.remove(project_file_path_str)
sel_model.selectionChanged.connect(sel_changed)
def file_dlg_dir_entered(displayed_dir):
displayed_dir = os.path.normpath(displayed_dir)
# this is set to None to prevent unwanted selection processing triggered by setText(...) below
fd.displayed_dir = None
fd.project_files_in_shown_dir = []
existing = []
for file_path_str in self.files_list:
dir_path_str, filename = split_file_path_str(file_path_str)
if dir_path_str == displayed_dir:
existing.append(f'"{file_path_str}"')
fd.project_files_in_shown_dir.append(file_path_str)
file_name_line_edit.setText(' '.join(existing))
fd.displayed_dir = displayed_dir
fd.directoryEntered.connect(file_dlg_dir_entered)
# set the initially displayed directory...
file_dlg_dir_entered(start_directory)
if fd.exec():
# for each file, if not present in self.files_list, add to files list and make self dirty
for project_file_in_shown_dir in fd.project_files_in_shown_dir:
if project_file_in_shown_dir not in self.files_list:
self.files_list.append(project_file_in_shown_dir)
# also add to list widget...
app.get_main_window().ui.files_list.addItem(project_file_in_shown_dir)
if not self.is_dirty():
self.toggle_dirty()
# but we also have to make sure that a file has not been UNselected...
docx_files_in_start_dir = [f for f in os.listdir(fd.displayed_dir) if os.path.isfile(os.path.join(fd.displayed_dir, f)) and os.path.splitext(f)[1] == '.docx' ]
for docx_file_in_start_dir in docx_files_in_start_dir:
docx_file_path_str = os.path.join(fd.displayed_dir, docx_file_in_start_dir)
if docx_file_path_str in self.files_list and docx_file_path_str not in fd.project_files_in_shown_dir:
self.files_list.remove(docx_file_path_str)
list_widget = app.get_main_window().ui.files_list
item_for_removal = list_widget.findItems(docx_file_path_str, QtCore.Qt.MatchExactly)[0]
list_widget.takeItem(list_widget.row(item_for_removal))
if not self.is_dirty():
self.toggle_dirty()
After a few hours of playing around, here's a pretty good way to programatically pre-select multiple files in a QFileDialog:
from pathlib import Path
from PyQt5.QtCore import QItemSelectionModel
from PyQt5.QtWidgets import QFileDialog, QListView
p_files = Path('/path/to/your/files')
dlg = QFileDialog(
directory=str(p_files),
options=QFileDialog.DontUseNativeDialog)
# get QListView which controls item selection
file_view = dlg.findChild(QListView, 'listView')
# filter files which we want to select based on any condition (eg only .txt files)
# anything will work here as long as you get a list of Path objects or just str filepaths
sel_files = [p for p in p_files.iterdir() if p.suffix == '.txt']
# get selection model (QItemSelectionModel)
sel_model = file_view.selectionModel()
for p in sel_files:
# get idx (QModelIndex) from model() (QFileSystemModel) using str of Path obj
idx = sel_model.model().index(str(p))
# set the active selection using each QModelIndex
# IMPORTANT - need to include the selection type
# see dir(QItemSelectionModel) for all options
sel_model.select(idx, QItemSelectionModel.Select | QItemSelectionModel.Rows)
dlg.exec_()
dlg.selectedFiles()
>>> ['list.txt', 'of.txt', 'selected.txt', 'files.txt']

tkinter treeview open state is not up to date when callback is triggered

I am currently working on an tkinter application that uses a treeview element to display data in a hierarchy. In part of this application, I have the need to update data for items. Unfortunately, getting this data is expensive (in time and processing), so I now only update items that are currently visible.
The problem I am having is that when I bind a (left click) event to an item and then query the treeview for open items to try and determine what is visible, previously opened items are returned, but the currently opened item is not (until a subsequent item is clicked).
I saw this question about querying (and setting) the open state, but it doesn't seem to cover my needs.
I have the following simplified code, which demonstrates the problem:
## import required libraries
import tkinter
import tkinter.ttk
import random
import string
## event handler for when items are left clicked
def item_clicked(event):
tree = event.widget
## we assume event.widget is the treeview
current_item = tree.item(tree.identify('item', event.x, event.y))
print('Clicked ' + str(current_item['text']))
## for each itewm in the treeview
for element in tree.get_children():
## if it has elements under it..
if len(tree.get_children(element)) > 0:
## get the element name
element_name = tree.item(element)['text']
## check if it is open
if tree.item(element)['open'] is True:
print('Parent ' + element_name + ' is open')
else:
print('Parent ' + element_name + ' is closed')
## make the window and treeview
root = tkinter.Tk()
root.title('Treeview test')
tree = tkinter.ttk.Treeview(root, selectmode = 'browse', columns = ('num', 'let'))
tree.heading('#0', text = 'Name')
tree.heading('num', text = 'Numbers')
tree.heading('let', text = 'Letters')
tree.bind('<Button-1>', item_clicked)
tree.pack()
## populate the treeview with psuedo-random data
parents = ['']
for i in range(100):
id = str(i)
## higher probability that this won't be under anything
if random.randrange(50) > 40:
parent = random.choice(parents)
else:
parent = ''
## make up some data
tree.insert(parent, 'end', iid = id, text = 'Item ' + id)
tree.set(id, 'num', ''.join(random.choices(string.digits, k=10)))
tree.set(id, 'let', ''.join(random.choices(string.ascii_uppercase, k=10)))
parents.append(id)
## main event loop (blocking)
root.mainloop()
This code will generate a treeview with 100 items in a random heirachy. Clicking on any item will print a list of items that have children and are currently expanded (open). The issue is, as with my main code, not reporting the latest item opened (i.e. it is one step behind).
Adding update() to the treeview before querying its state (at the start of the item_clicked function) does not resolve the issue.
How can I accurately get the open state of the items in the event handler of the code above?
Is it an issue that the event is being fired before the item actually expands (opens), and if so, why is an update() not fixing this?
Does an alternative function exist for treeview widgets to return a list of currently visible items?
Running on Windows 10 with Python 3.7.3 (Tk version 8.6.9).
Changing the event binding from <Button-1> to <ButtonRelease-1> in the code above resolves the issue.
It should be noted in the above code that the open/close state will only be printed for items with children that have no parent (i.e. it won't descend in to the tree). To resolve this, you simply need to call tree.get_children(item_id) on each item with children recursively.

Create shader from file selection and apply to selected object

You select a folder with a bunch of images, select the one you wish to use from a dropdown menu, then select your object and hit apply. My issue is that I cannot do this to multiple objects in a scene, it only changes the material on the first object that is selected. When I try to do this to another object in my scene it just replaces the image on the first object and doesn't create another shader. The goal is to be able to select any object in the scene and apply a texture from the selected list to any of the objects in the scene. Help would be greatly appreciated. I provided the tool down below.
import maya.cmds as cmds
from os import listdir
class TextureImport():
def __init__(self):
if cmds.window(TextureImport, q=True, exists=True):
cmds.deleteUI(TextureImport)
GUI=cmds.window(title="Texture Import Tool", widthHeight=(250,160), s=True, tlb=True)
cmds.rowColumnLayout(numberOfColumns=1, columnAlign=(1, 'center'), columnAttach=(1, 'both', 0), cw=(1,250))
cmds.button(label="Select Directory", command=self.select_dir)
cmds.separator(style='in', h=20)
cmds.optionMenu('optionMenu', label="File List")
cmds.button(label="Clear List", command=self.clear_list)
cmds.separator(style='in', h=20)
cmds.text('Select your object, then:', h=25)
cmds.button(label="Apply Texture", command=self.apply_texture)
cmds.setParent('..')
cmds.showWindow()
def select_dir(self, *args):
basicFilter = "Image Files (*.jpg *.jpeg *.tga *.png *.tiff *.bmp *.psd)"
self.myDir = cmds.fileDialog2 (fileFilter=basicFilter, dialogStyle=2, fm=3)
myFiles = listdir(self.myDir[0])
for items in myFiles:
fileEndings = ('.psd','.PSD','.jpg','JPG','.jpeg','.JPEG','.tga','.TGA','.png','.PNG','.tiff','.TIFF','.bmp','.BMP')
if items.endswith(fileEndings):
cmds.menuItem(items)
else:
cmds.warning(items + 'This is not a valid image type, you fool.')
print myFiles
def clear_list(self, *args):
fileList = cmds.optionMenu('optionMenu', q=True, itemListLong=True)
if fileList:
cmds.deleteUI(fileList)
def apply_texture(self, *args):
object = cmds.ls(sl=True)
selectedMenuItem = cmds.optionMenu('optionMenu', q=True, value=True)
cmds.sets(name='imageMaterialGroup', renderable=True, empty=True)
shaderNode = cmds.shadingNode('phong', name='shaderNode', asShader=True)
fileNode = cmds.shadingNode('file', name='fileTexture', asTexture=True)
cmds.setAttr('fileTexture'+'.fileTextureName', self.myDir[0]+'/'+selectedMenuItem, type="string")
shadingGroup = cmds.sets(name='textureMaterialGroup', renderable=True, empty=True)
cmds.connectAttr('shaderNode'+'.outColor','textureMaterialGroup'+'.surfaceShader', force=True)
cmds.connectAttr('fileTexture'+'.outColor','shaderNode'+'.color', force=True)
cmds.surfaceShaderList('shaderNode', add='imageMaterialGroup')
cmds.sets(object, e=True, forceElement='imageMaterialGroup')
TextureImport()
The goal is to be able to select any object in the scene and apply a texture from the selected list to any of the objects. For example, an artist could set up multiple planes to apply reference images on. This tool would create the shader from the selected files which would make their job very easy. Help on this would be greatly appreciated.
Your problem lies in the way you try to connect the attributes:
fileNode = cmds.shadingNode('file', name='fileTexture', asTexture=True)
cmds.setAttr('fileTexture'+'.fileTextureName', ..., type="string")
You use the explicit name of the file node: 'fileTexture'. The result is that if the node exists, the exisiting node is used instead of your newly created one. You have to build the attribute with the fileNode variable this way:
fileNode = cmds.shadingNode('file', name='fileTexture', asTexture=True)
cmds.setAttr(fileNode+'.fileTextureName', ..., type="string")
The same should be changed in the other connectAttr() functions.

Populate window in Maya with icons and dynamically adjust depending on window size

I want to create a window in maya, that gets populated with icons from a specific path. I know how to do that, but I also want the icons to adjust dynamically as I change the size of the window.
For example, let's say I have this:
enter image description here
and I want when I resize to get this:
enter image description here
here is a bit of the code I have :
import maya.cmds as cmds
import os
from os import listdir
def UI(*args):
if cmds.window("Test", exists = True):
cmds.deleteUI("Test")
testwindow = cmds.window("Test", t="Test Window", sizeable = 1)
cmds.scrollLayout('srcoll', p = "Test")
cmds.rowColumnLayout("ColLayout", p = "Test", nc = 3)#I imagine the "nc" command is probably useless here, I am just leaving it for testing purposes
cmds.showWindow("Test")
customPath = "C:\Users\$username$\Desktop"
customPathItems = listdir(customPath)
def popUI(*args):
for item in customPathItems:
if item.endswith("_icon.png"):
cmds.iconTextButton(l = item, p = "ColLayout", i = customPath + "/" + item, w = 128, h = 128)
def buildUI(*args):
UI()
popUI()
buildUI()
Any help would be appreciated
What you need is called a Flow layout, where the items inside the layout automatically adjust themselves when the widget is resized.
Here's an example from Qt's documentation that you can fully convert over to Python:
https://doc.qt.io/qt-4.8/qt-layouts-flowlayout-flowlayout-cpp.html
You can also google pyqt flow layout for ones already written in Python.

Display hierarchy of selected object only

I am creating a UI where the user selects an object,
the UI will display its hierarchy of the selected
object.
It is somewhat similar to Outliner but I am unable to
find any documentation/ similar results to what I am
trying to obtain. And btw, I am coding using python...
Even so, is this even possible to do it in the first
place?
Allow me to provide a simple example below:
Say if I selects testCtrl, it will only displays
testCtrl, loc and jnt without showing the Parent (Grp 01)
Eg. Grp 01 --> testCtrl --> loc --> jnt
import maya.cmds as cmds
def customOutliner():
if cmds.ls( sl=True ):
# Create the window/UI for the custom Oultiner
newOutliner = cmds.window(title="Outliner (Custom)", iconName="Outliner*", widthHeight=(250,100))
frame = cmds.frameLayout(labelVisible = False)
customOutliner = cmds.outlinerEditor()
# Create the selection connection network; Selects the active selection
inputList = cmds.selectionConnection( activeList=True )
fromEditor = cmds.selectionConnection()
cmds.outlinerEditor( customOutliner, edit=True, mainListConnection=inputList )
cmds.outlinerEditor( customOutliner, edit=True, selectionConnection=fromEditor )
cmds.showWindow( newOutliner )
else:
cmds.warning('Nothing is selected. Custom Outliner will not be created.')
Make the window:
You want to use the treeView command (documentation) for this. I'm placing it in a formLayout for convenience.
from maya import cmds
from collections import defaultdict
window = cmds.window()
layout = cmds.formLayout()
control = cmds.treeView(parent=layout)
cmds.formLayout(layout, e=True, attachForm=[(control,'top', 2),
(control,'left', 2),
(control,'bottom', 2),
(control,'right', 2)])
cmds.showWindow(window)
Populate the tree view:
For this, we'll use a recursive function so you can build up the hierarchy with nested listRelatives calls (documentation). Start with the result of old faithful ls -sl:
def populateTreeView(control, parent, parentname, counter):
# list all the children of the parent node
children = cmds.listRelatives(parent, children=True, path=True) or []
# loop over the children
for child in children:
# childname is the string after the last '|'
childname = child.rsplit('|')[-1]
# increment the number of spaces
counter[childname] += 1
# make a new string with counter spaces following the name
childname = '{0} {1}'.format(childname, ' '*counter[childname])
# create the leaf in the treeView, named childname, parent parentname
cmds.treeView(control, e=True, addItem=(childname, parentname))
# call this function again, with child as the parent. recursion!
populateTreeView(control, child, childname, counter)
# find the selected object
selection = cmds.ls(sl=True)[0]
# create the root node in the treeView
cmds.treeView(control, e=True, addItem=(selection, ''), hideButtons=True)
# enter the recursive function
populateTreeView(control, selection, '', defaultdict(int))
Comparison of window to outliner.
I've replaced the spaces with X so you can see what's happening. Running this code will use spaces though:
You'll want to read up on the documentation to improve on this, but this should be a great starting point. If you want a live connection to the selection, make a scriptJob to track that, and be sure to clear the treeView before repopulating.

Categories

Resources