There Goes Captain Beto, Through Space
I said in dot.kde.org that I could write a spatial file manager in a weekend. I even said that I could write it this weekend if my date failed me.
Well, guess what? No date (she's gone to give a conference), so.... I will write it, and it will be called Captain Beto. The reason for the name will be clear for almost any argentinian, (think translation, fellow countrymen), and obscure for almost anyone else. Which is as good a combination as any.
Ok, here's all I know about this stuff:
- It's supposed to manage files, so it should be able to link, move, copy, and remove files
- It's supposed to be spatial, which as far as I know means it should be inconvenient ;-)
- It should not let you open the same folder twice
- It should remember everything about a folder
- Individualized per-folder preferences
- Remember object position in the folder view
Of course, I intend to do it using PyQt, and if we are lucky, this trip will be educational for me, and if we are really lucky, also for some readers.
I am pretty sure this would be much easier using PyKDE instead, but I must confess right now I don't have it in my computer. However, I intend to get it in a few days, so that means that I will intentionally not do some stuff, like thumbnails, which are much easier to do using some KDE API.
So, this will be a pretty simple application on this first stage. Also, since I have no intent to actually use this thing, it's not a baby with a very bright future, unless some reader feels like adopting it.
It's saturday, may 29th, 2004, and my fuzzy clock says it's five to one... but it's really five to 4.
Timestamp: 29/5/04 15:55
The Plan
I have never written a file manager. I have never even used them much. So, this is really exploration of obscure territory for me.
Here's the roadmap I am imagining:
- Create a widget that can show a folder
- Make it configurable
- Make it configurable per-folder
- Let it "navigate" the folder tree
- Add basic file operations
Sounds easy enough. I like it because every step should be doable in a reasonable amount of time. I tend to lose interest if the next goal is too far. I am a sprinter, not a distance runner. Like Gimli in "The Two Towers".
Timestamp: 29/5/04 16:05
Stage One
Stupid Application
Let's start with some boilerplate code: A main script that opens a window.
from qt import *
from window import Window
import sys
def main(args):
app=QApplication(args)
win=Window()
app.setMainWidget(win)
win.show()
app.connect(app, SIGNAL("lastWindowClosed()"), app, SLOT("quit()"))
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
As you can see, all this does is open a window, literally. And it assumes there is another module, called window.py, containing a Window class.
Well, that's boilerplate also (and very simple one, too):
class Window (QWidget):
def __init__(self,parent=0):
QWidget.__init__(self,parent)
So, we have a program that, when called, displays a blank widget. When you close that widget, it dies. Not too interesting, that one.
So, let's make it do some tricks.
Choosing the Widget
It turns out Qt has a widget called QIconView, described in the manual as:
A QIconView can display and manage a grid or other 2D layout of labelled icons. Each labelled icon is a QIconViewItem. Items (QIconViewItems) can be added or deleted at any time; items can be moved within the QIconView. Single or multiple items can be selected. Items can be renamed in-place. QIconView also supports drag and drop.
Looks like a file manager to me. So, we change our Window to inherit from QIconView.
class Window (QIconView):
def __init__(self,parent=0):
QIconView.__init__(self)
Now, that's an uninteresting white window ;-)
Of course, to make it useful, we need to be able to fill it with the contents of a folder.
Well, let's add an argument to the constructor, called folder, that is the folder that should be displayed, and add a method, called changeFolder, that fills the QIconView with the contens of that folder.
Iterating over the contents of a folder in Python is easy enough, using the os module. Adding icons to a QIconView is mostly a matter of creating a bunch of QIconViewItems.
def __init__(self,folder="/",parent=0):
QIconView.__init__(self)
self.changeFolder(folder)
def changeFolder(self,folder):
folder=os.path.abspath(folder)
for item in os.listdir(folder):
QIconViewItem(self,item)
Now, this is somewhat better :-)
That the icon seems right for what is there is just a coincidence.
So, let's use two icons, one called "folder" and the other called "file", and stat the contents of the folder, assigning the right icons.
It really shoud have a better caption, reflecting what you are seeing in the window, too.
Break
Timestamp: 29/05/04 16:20
Turns out I don't have the python docs installed here. Never program Python without the library reference at hand. So, I'm building them now (I had the TeX sources inside the Python sources). Should only take a few minutes
I would normally just use the version at www.python.org, but this is at home, without Internet.
Timestamp: 29/05/04 17:00
The reason I need the library reference is that I don't remember what the values returned by os.stat() mean.
Turns out what I wanted was not os.stat(), but os.path.isdir() and company. See why the library reference is your friend? ;-)
I have visitors. Be back later.
Timestamp: 29/05/04 17:05 Timestamp: 29/05/04 19:25
So this is how the Window class looks now:
def __init__(self,folder="/",parent=0):
QIconView.__init__(self)
\#Fixme, this should load the icons in a reasonable way ;-)
self.folderIcon=QPixmap("/usr/share/icons/crystalsvg/32x32/filesystems/folder.png")
self.fileIcon=QPixmap("/usr/share/icons/crystalsvg/32x32/mimetypes/document.png")
self.changeFolder(folder)
def changeFolder(self,folder):
folder=os.path.abspath(folder)
self.setCaption (folder+" - captain beto")
for item in os.listdir(folder):
fullpath=os.path.join(folder,item)
\#FIXME this should check for different things, like
\#links, devices, etc.
if os.path.isdir(fullpath):
QIconViewItem(self,item,self.folderIcon)
elif os.path.isfile(fullpath):
QIconViewItem(self,item,self.fileIcon)
else:
QIconViewItem(self,item,self.fileIcon)
It's basically the same, except that now, when I create the QIconViewItems, we try (feebly) to use the right kind of icon.
And it looks a little better now:
Notice how it uses different icons. It turns out you can even move the icons around, too :-)
So, right now, I think I am close to the first milestone (creating a widget that can show a folder). Of course, it's not a good widget, but it has potential.
Some of the problems with the current code, like loading the right icon, will be solved when this is turned into a PyKDE app later.
The second milestone was "make it configurable". Well, let's.
Making it configurable
The usual stuff that you can configure in a file manager is:
- Choice to show hidden files or not
- It should remember the position of the icons
- You should be able to set a background
Anything else will happen eventually in a distant future. Maybe tomorrow.
Let's add another argument to changeFolder, called showHidden, defaulting to false. Then ignore (or not) hidden files in the loop over os.listdir().
Should be simple. I will not hide ".." because it is the natural way to go up.
Timestamp: 29/05/04 19:40
Here is changeFolder ignoring (or not) hidden files. Do you notice how each thing I do is a 5, maybe 10 line change? I like to program in very small increments. But that's just personal preference.
folder=os.path.abspath(folder)
self.setCaption (folder+" - captain beto")
for item in os.listdir(folder):
if not showHidden:
if item[0]=="." and not item=="..":
continue
fullpath=os.path.join(folder,item)
\#FIXME this should check for different things, like
\#links, devices, etc.
if os.path.isdir(fullpath):
QIconViewItem(self,item,self.folderIcon)
elif os.path.isfile(fullpath):
QIconViewItem(self,item,self.fileIcon)
else:
QIconViewItem(self,item,self.fileIcon)
Now, I want this to follow, as far as I know about it, the dogma of spatial file management, so a setting such as showHidden should be stored and remembered.
It should also, of course, be possible to change, and to be changed on a per-folder basis.
So, what UI should the user have to change this stuff: I like direct manipulation of objects, which in this case probably means a RMB popup on the folder itself.
So, let's add that menu, and make "Show Hidden" a toggle in it. The right way is to create an action and stuff.
In the Window constructor, we add a chunk of code to create the showHiddenAction, which will be a toggle. Also, we create a popup menu, and put the action in it.
self.showHiddenAction=QAction("Show Hidden Files",QKeySequence("CTRL+H"),self)
self.showHiddenAction.setToggleAction(True)
self.showHiddenAction.addTo(self.rmbMenu)
As you can see, actions are pretty damn simple. Now, we need to connect the action to a slot that toggles the display, and make the popup menu show on right-button-click.
To make the popup ... well, pop up, we need to connect the rightButtonClicked() signal of the QIconView, and connect it to a slot that pops the menu.
This is the slot we add to the Window class:
if not item:
self.rmbMenu.exec_loop(pos)
else:
\#FIXME Should show context menu for the item where the
\#user right-clicked
pass
As you can see, there is a glaring lack of functionality. But that's ok, we are just building up stuff here, the holes can be filled later.
To connect the signal to this slot, we add this line to Window.__init__ :
And lo and behold, we have a context menu, and it has a toggle saying "Show Hidden Files". Now, we have to make it show (or not) according to the toggle.
Simple: we add a member called showHidden to the Window class, remove the showHidden argument from changeFolder, and make it use the variable. Then, we add an accessor called setShowHidden which toggles and redisplays. And we connect that to showHiddenAction.
Also, I renamed changeFolder to setFolder, to be a bit consistent.
Here's the new window.py:
import os
class Window (QIconView):
def __init__(self,folder="/",parent=0):
QIconView.__init__(self)
\#Fixme, this should load the icons in a reasonable way ;-)
self.folderIcon=QPixmap("/usr/share/icons/crystalsvg/32x32/filesystems/folder.png")
self.fileIcon=QPixmap("/usr/share/icons/crystalsvg/32x32/mimetypes/document.png")
self.showHidden=False
self.rmbMenu=QPopupMenu(self)
self.showHiddenAction=QAction("Show Hidden Files",QKeySequence("CTRL+H"),self)
self.showHiddenAction.setToggleAction(True)
self.showHiddenAction.addTo(self.rmbMenu)
self.connect(self.showHiddenAction,SIGNAL("toggled(bool)"),self.toggleShowHiddenSlot)
self.connect(self,SIGNAL("rightButtonClicked(QIconViewItem*,const QPoint&)"),self.showContextMenu)
self.setFolder(folder)
def setFolder(self,folder):
folder=os.path.abspath(folder)
self.setCaption (folder+" - captain beto")
self.folder=folder
self.clear()
for item in os.listdir(folder):
if not self.showHidden:
if item[0]=="." and not item=="..":
continue
fullpath=os.path.join(folder,item)
\#FIXME this should check for different things, like
\#links, devices, etc.
if os.path.isdir(fullpath):
QIconViewItem(self,item,self.folderIcon)
elif os.path.isfile(fullpath):
QIconViewItem(self,item,self.fileIcon)
else:
QIconViewItem(self,item,self.fileIcon)
def showContextMenu(self,item,pos):
if not item:
self.rmbMenu.exec_loop(pos)
else:
\#FIXME Should show context menu for the item where the
\#user right-clicked
pass
def toggleShowHiddenSlot(self,show):
self.showHidden=show
self.setFolder(self.folder)
The app looks exactly the same, although it has a little functionality extra, so no screenshot for you! ;-)
Timestamp: 29/05/04 20:15
Now, let's see how it looks with a background pixmap. There is a setPaletteBackgroundPixmap() method, so it shouldn't be hard...
I say it looks good:
Again, I create an action, called setBackgroundAction, connect it to a slot called setBackgroundSlot, and slap it on the context menu:
The action and the connection (for Window.__init__):
self.setBackgroundAction.addTo(self.rmbMenu)
self.connect(self.setBackgroundAction,SIGNAL("activated()"),self.setBackgroundSlot)
The slot:
bg=str(QFileDialog.getOpenFileName())
self.setPaletteBackgroundPixmap(QPixmap("bg"))
Ok, that is getting boring. So let's just say it is configurable enough for a while, OK?
And then we are at a new milestone. We made it configurable , now we must make it configurable per-folder. That should be a bit trickier.
Per-folder settings
Timestamp: 29/05/04 20:35
You know... I'm getting hungry. I'll order some Kun-Pao... nah, it can wait a little.
There are many different ways to go at storing configuration data. I intend to keep this as simple as possible, so I will try to do it using the shelve module.
You can think of shelve as a very (very) simple database. You just store objects in it, indexed by a single key. However, that fits rather nicely with the idea of per-folder settings, doesn't it? I mean, just index them by the folder path :-)
So, I will create a little dictionary for the config values, called settings, and (un)shelve it as needed. So, self.bg and self.showHidden will have to change again, now to stuff like self.settings["showHidden"]. Such is life for variables.
To shelve settings, all you need is a global shelf object. I will create it in the main script, and pass it as argument to the Window class on creation, so I can get a reference to it easily. This way to do it is probably not the nicest, but is what I can think about over the noise of my hungry stomach.
Here's the new main script, beto.py:
from qt import *
from window import Window
import sys
import shelve
def main(args):
app=QApplication(args)
shelf=shelve.open(".betosettings")
win=Window(shelf=shelf)
win.show()
app.connect(app, SIGNAL("lastWindowClosed()"), app, SLOT("quit()"))
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
As you can see, no big changes I removed the setMainWidget call, because it breaks the app if it is meant to support multiple windows.
The shelf itself is used just like a dictionary, so this is really simple to use. Look at the saveSettings and loadSettings in the next listing, as well as the places that call them.
window.py suffered a bit more. Making sure all things change in harmonious waves is always trickier than I expect. In particular, the handling of the background here is very inelegant:
import os
class Window (QIconView):
def __init__(self,folder="/",parent=None,shelf=None):
QIconView.__init__(self,parent)
self.shelf=shelf
self.folder=None
\#Fixme, this should load the icons in a reasonable way ;-)
self.folderIcon=QPixmap("/usr/share/icons/crystalsvg/32x32/filesystems/folder.png")
self.fileIcon=QPixmap("/usr/share/icons/crystalsvg/32x32/mimetypes/document.png")
self.rmbMenu=QPopupMenu(self)
self.showHiddenAction=QAction("Show &Hidden Files",QKeySequence("CTRL+H"),self)
self.showHiddenAction.setToggleAction(True)
self.showHiddenAction.addTo(self.rmbMenu)
self.connect(self.showHiddenAction,SIGNAL("toggled(bool)"),self.toggleShowHiddenSlot)
self.setBackgroundAction=QAction("Set &Background...",QKeySequence(""),self)
self.setBackgroundAction.addTo(self.rmbMenu)
self.connect(self.setBackgroundAction,SIGNAL("activated()"),self.setBackgroundSlot)
self.connect(self,SIGNAL("rightButtonClicked(QIconViewItem*,const QPoint&)"),self.showContextMenu)
self.setFolder(folder)
def setFolder(self,folder):
folder=os.path.abspath(folder)
self.setCaption (folder+" - captain beto")
if folder!=self.folder:
self.folder=folder
self.loadSettings()
self.clear()
for item in os.listdir(folder):
if not self.settings["showHidden"]:
if item[0]=="." and not item=="..":
continue
fullpath=os.path.join(folder,item)
\#FIXME this should check for different things, like
\#links, devices, etc.
if os.path.isdir(fullpath):
QIconViewItem(self,item,self.folderIcon)
elif os.path.isfile(fullpath):
QIconViewItem(self,item,self.fileIcon)
else:
QIconViewItem(self,item,self.fileIcon)
def showContextMenu(self,item,pos):
if not item:
self.rmbMenu.exec_loop(pos)
else:
\#FIXME Should show context menu for the item where the
\#user right-clicked
pass
def toggleShowHiddenSlot(self,show):
self.settings["showHidden"]=show
self.saveSettings()
self.setFolder(self.folder)
def setBackgroundSlot(self,choose=True):
if choose:
self.settings["bg"]=str(QFileDialog.getOpenFileName())
if self.settings["bg"]:
self.setPaletteBackgroundPixmap(QPixmap(self.settings["bg"]))
else:
self.unsetPalette()
self.saveSettings()
def saveSettings(self):
self.shelf[self.folder]=self.settings
def loadSettings(self):
try:
self.settings=self.shelf[self.folder]
except KeyError:
self.settings={}
\#Here you have to set *ALL* defaults
self.settings["bg"]=None
self.settings["showHidden"]=False
self.setBackgroundSlot(choose=False)
Also, I added a little error checking here and there ;-)
And that's that when it comes to per-folder settings. Not that hard, was it?
Timestamp: 29/05/04 21:05
And I have only written about 115 lines of code!
So, what's next? Folder navigation!
Folder Navigation
Now, this is simple: when you click on a folder, open it in another window. When you click on a file, open that file. Piece of cake.
There is a signal called clicked(QIconViewItem). Guess when is it triggered? Right, when you click on stuff.
If someone wants a double-click version, use doubleClicked instead ;-)
So, when someone clicks on stuff, we see if he clicked on an item, and if yes, whether it's a file or a folder, and act on it.
There is a trick, in that if you don't want the new window to vanish without a trace as soon as it gets out of scope, there must be some global place where they are referenced. So I created a global dictionary called windows in window.py, where they get indexed by folder name on creation.
This is useful to avoid opening the same folder twice, which is supposed to be bad for some reason (don't ask me about it)
Here's the slot handling opening stuff:
global windows
if not item: \#Clicked on the background
return
name=os.path.join(self.folder,str(item.text()))
if os.path.isdir(name):
\#Check if it is already open, if not, open it
if not windows.has_key(name):
Window(name,shelf=self.shelf).show()
else:
windows[name].show()
else: \#Open it
\#FIXME this is damn insecure!
os.system("kfmclient exec '%s'&"%name)
Here's how it connects to the clicking:
When a window closes, it has to delete itself, and remove itslf from that global dictionary. So, we override the closeEvent() handler:
event.accept()
global windows
del windows[self.folder]
print windows
Timestamp: 29/05/04 21:40
Now, the next milestone should be to add file managing capabilities, but I cheated too much a while back, so I will get back to making it configurable.
In particular, folders should remember their position and size, and the position of the stuff inside them.
Making it more configurable, still
For geometry, it's simple. Whenever you move or resize the window, store a setting, and, on creation, follow it. Trivial.
So, we override resizeEvent() and moveEvent(), retouch __init__ a bit, and add new defaults in loadSettings().
The events:
self.settings["x"]=self.pos().x()
self.settings["y"]=self.pos().y()
self.saveSettings()
QIconView.moveEvent(self,event)
def resizeEvent(self,event):
self.settings["width"]=self.frameGeometry().width()
self.settings["height"]=self.frameGeometry().height()
self.saveSettings()
QIconView.resizeEvent(self,event)
The new loadSettings():
try:
self.settings=self.shelf[self.folder]
except KeyError:
self.settings={}
\#Here you have to set *ALL* defaults
self.settings["bg"]=None
self.settings["showHidden"]=False
self.settings["x"]=self.pos().x()
self.settings["y"]=self.pos().y()
self.settings["width"]=self.frameGeometry().width()
self.settings["height"]=self.frameGeometry().height()
self.setBackgroundSlot(choose=False)
And for the settings to take effect, I added this in setFolder (it could have gone to the __init__, but it looked cleaner this way), after we call loadSettings:
self.settings["y"])
self.resize(self.settings["width"],
self.settings["height"])
Maybe it should be part of loadSettings itself? Well, at least refactoring that should be simple ;-)
And here's beto, showing different backgrounds. And yes, all this so far works. At least as far as I tested it, which is not all that much.
Timestamp: 29/05/04 22:05
Now, about the position of items in the view... that's going to be somewhat harder.
By default, when we create the items, they simply spread themselves at will. They can be moved by drag and drop. So, here's what I think should be done:
- When they are created, check if we remember a position for that name.
- If we don't, just create it, and save the position it gets.
- Figure out when one is moved, and store that as well.
So, we will have to do some more stuff with settings. I will add a self.settings["itemPositions"] that should be a dictionary, where positions will be indexed by name.
So, the position for afile should be in self.settings["itemPositions"]["afile"]. Of course there will be accesor methods for that ;-)
To figure out when something moves, I think we will have to connect to the QIconView.moved() signal. Here's what the docs say about it:
void QIconView::moved () [signal]
This signal is emitted after successfully dropping one (or more) items of the icon view. If the items should be removed, it's best to do so in a slot connected to this signal.
But first, I think I said something about Kun Pao? Dinner break!
Timestamp: 05/09/2004 22:15
Timestamp: 05/09/2004 22:50
That was nice. Now, let's get back to business.
Let's connect something to the moved signal, and when it is triggered, we save all the positions.
Oops. That isn't triggered when moving stuff around. So, let's just do it on the closeEvent. I must iterate over all icons, and save each one's position.
Here's the new closeEvent, and the methods to save and load the positions:
event.accept()
global windows
del windows[self.folder]
item=self.firstItem()
while True:
self.saveItemPosition(str(item.text()),item.x(),item.y())
item=item.nextItem()
if not item:
break
self.saveSettings()
def saveItemPosition(self,name,x,y):
self.settings["itemPositions"][name]=(x,y)
def itemPosition(self,name):
if self.settings["itemPositions"].has_key(name):
return self.settings["itemPositions"][name]
else:
return None
Also, added a default in loadSettings(), so itemPositions is initialized as a dictionary. And, changed the loop that creates the QListViewItems in setFolder, like this:
if not self.settings["showHidden"]:
if item[0]=="." and not item=="..":
continue
fullpath=os.path.join(folder,item)
\#FIXME this should check for different things, like
\#links, devices, etc.
if os.path.isdir(fullpath):
i=QIconViewItem(self,item,self.folderIcon)
elif os.path.isfile(fullpath):
i=QIconViewItem(self,item,self.fileIcon)
else:
i=QIconViewItem(self,item,self.fileIcon)
p=self.itemPosition(item)
if p:
i.move(p[0],p[1])
However, that doesn't work. It turns out that I am inserting and moving the items before the window is shown. And in that case, Qt seems to call arrangeItemsInGrid (it says so in the docs!). Which moves everything back :-)
In fact, I want it to use arrangeItemsInGrid. And then I want to move them. So, I take the moving-the-item logic into arrangeItemsInGrid... and that does work.
However, I have been changing so much code, that it is time to do a full dump here so you can follow it on your own ;-)
beto.py:
from qt import *
from window import Window
import sys
import shelve
def main(args):
app=QApplication(args)
shelf=shelve.open(".betosettings")
win=Window(shelf=shelf)
win.show()
app.connect(app, SIGNAL("lastWindowClosed()"), app, SLOT("quit()"))
app.exec_loop()
if __name__=="__main__":
main(sys.argv)
But before I show you window.py...
Debugging
I made quite a few mistakes while developing this. Often, I simply didn't know about something that exists, and thus I failed to take advantage of it. Here are some explanations.
The context popup
I hooked the popup to the rightButtonClicked signal. It turns out that it's a better idea to hook contextMenuRequested, because it will be more portable, and probably will handle things like calling the context menu using the keyboard.
Trivial to change, just switch the signals on the connect call.
The opening of items
I hooked it to the clicked signal. That sucks because it means you can't ever select anything. Which for the next milestone will be necessary. If I want it to act like KDE in single-click mode, I will have to check if the user has ctrl or shift pressed while clicking.
That is somewhat hard. So for now, reluctantly, I will switch it to doubleClicked, so clicked will just select.
The .. folder
os.listdir doesn't include ".." by default. So what, I'll just add it ;-)
The error reporting
Right now, when something happens, it prints the error messages (exceptions, whatever) on stderr. So, you won't see anything, unless you are running beto from a terminal. This is important because it happens, for example, if you open a folder that you can't enter or read.
There is a somewhat dirty generic solution for this, which I have used in the past. I can do this for every method where I suspect exceptions may be thrown by real life. Or, to be neat, on every method ;-)
First, I create a ErrorDialog class, using designer. This is how it looks:
Then wrap the method in stuff like this, grabbing the exception data:
do_something(hard)
do_more_stuff(harder)
except:
(type,value,trace)=sys.exc_info()
showError(type,value,trace)
And this is showError:
def showError(type,value,trace):
d=ErrorDialog(modal=True)
text=str(value)
d.report.setText(text)
d.exec_loop()
And presto, the user can see the exceptions. Of course a real error message should be created for the stuff that happens, but this lets you see where you should create them :-)
And here is the error handling in action:
Normalizing paths
If you are in /folder and choose .., you get /folder/.. which is of course, not the same as / literally, but it is in reality. So, the path should be normalized, so that you don't open the folder twice (I insist, don't ask me why). But of course python has os.path.abspath() which is exactly what I need.
More item positioning problems
When you change some settings (right now, showHidden), it calls setFolder(). Since the window is already displayed, and setFolder clears the icons and reloads them, it is still necessary to move the items around as they are created. So, some stuff I had deleted, had to get back in setFolder.
Also, since setFolder reloads the settings, which contain the positions, it is possible to do this:
- move stuff around
- change showHidden
- it reloads positions
- clears
- recreates items... and forgets the stuff you moved at the beginning.
To solve this, I moved the position-memorizing logic into saveSettings, and made sure saveSettings is called before calling setFolder. Probably would make sense to call saveSettings at the beggining inside setFolder, instead.
Also, make the QIconView not autoArrange.
Silly bug: the toggle should match the state of the setting. So, set it when settings load.
The new item problem
When a new item appears for some reason, arrangeItemsInGrid will lay it just fine. But... then we move all the old items into the old grid position! So, we step all over it. In fact, it gets worse: because of a bugfix described before, which required disabling autoArrange, everything appears piled on the top-left corner.
Here's all I came up with, but I don't quite like it. If some item has a known position, then I put them there.
All the items without known positions go below that following a grid. Hopefully ;-)
I also changed how things align. For some reason, even when using a grid, Qt aligns the texts, but not the icons?
It's far from perfect. But at least items don't overlap by accident, even if they have very long names.
The code to do this is at the bottom of setFolder in the next listing. It ain't pretty. And it's way too simplistic.
A Featurette
If you try to open something already existing, it will rise. It's just a oneliner in openItemSlot
Final Listing
So, here's a somewhat nicer, a bit less buggy window.py:
import os
from pprint import pprint
import sys
from errordialog import ErrorDialog
windows={}
class Window (QIconView):
def __init__(self,folder="/",parent=None,shelf=None):
QIconView.__init__(self,parent)
self.shelf=shelf
self.setSelectionMode(self.Extended)
self.setAutoArrange(False)
self.setGridX(72)
self.setGridY(72)
self.setSpacing(16)
\#Fixme, this should load the icons in a reasonable way ;-)
self.folderIcon=QPixmap("/usr/share/icons/crystalsvg/32x32/filesystems/folder.png")
self.fileIcon=QPixmap("/usr/share/icons/crystalsvg/32x32/mimetypes/document.png")
self.rmbMenu=QPopupMenu(self)
self.showHiddenAction=QAction("Show &Hidden Files",QKeySequence("CTRL+H"),self)
self.showHiddenAction.setToggleAction(True)
self.showHiddenAction.addTo(self.rmbMenu)
self.connect(self.showHiddenAction,SIGNAL("toggled(bool)"),self.toggleShowHiddenSlot)
self.setBackgroundAction=QAction("Set &Background...",QKeySequence(""),self)
self.setBackgroundAction.addTo(self.rmbMenu)
self.connect(self.setBackgroundAction,SIGNAL("activated()"),self.setBackgroundSlot)
self.connect(self,SIGNAL("contextMenuRequested(QIconViewItem*,const QPoint&)"),self.showContextMenu)
self.connect(self,SIGNAL("doubleClicked (QIconViewItem *)"),self.openItemSlot)
self.setFolder(folder)
global windows
windows[folder]=self
def setFolder(self,folder):
try:
self.folder=folder
self.loadSettings()
self.setCaption (folder+" - captain beto")
self.move(self.settings["x"],
self.settings["y"])
self.resize(self.settings["width"],
self.settings["height"])
self.clear()
if folder=="/":
list=os.listdir(folder)
else:
list=[".."]+os.listdir(folder)
unplaced=[]
maxY=0
for item in list:
if not self.settings["showHidden"]:
if item[0]=="." and not item=="..":
continue
fullpath=os.path.join(folder,item)
\#FIXME this should check for different things, like
\#links, devices, etc.
if os.path.isdir(fullpath):
i=QIconViewItem(self,item,self.folderIcon)
elif os.path.isfile(fullpath):
i=QIconViewItem(self,item,self.fileIcon)
else:
i=QIconViewItem(self,item,self.fileIcon)
p=self.itemPosition(item)
if p:
i.move(p[0],p[1])
if p[1]>maxY:
maxY=p[1]
else:
unplaced.append(i)
curX=0
curY=0
if maxY: \#If no item has known position, start at the top
curY=maxY+self.gridY()
maxy=self.gridY()
for item in unplaced:
size=item.size()
w=size.width()
h=size.height()
if h>maxy:
maxy=h
item.move(curX+(self.gridX()-w)/2,curY)
self.saveItemPosition(item.text(),curX,curY)
curX=curX+self.gridX()
if curX >self.width()-self.gridX():
curX=0
curY=curY+maxy
maxy=self.gridY()
except:
(type,value,trace)=sys.exc_info()
showError(type,value,trace)
def arrangeItemsInGrid(self,update=True):
QIconView.arrangeItemsInGrid(self,update)
item=self.firstItem()
while True:
p=self.itemPosition(item.text())
if p:
item.move(p[0],p[1])
item=item.nextItem()
if not item:
break
def showContextMenu(self,item,pos):
if not item:
self.rmbMenu.exec_loop(pos)
else:
\#FIXME Should show context menu for the item where the
\#user right-clicked
pass
def toggleShowHiddenSlot(self,show):
self.settings["showHidden"]=show
self.saveSettings()
self.setFolder(self.folder)
def setBackgroundSlot(self,choose=True):
if choose:
self.settings["bg"]=str(QFileDialog.getOpenFileName())
if self.settings["bg"]:
self.setPaletteBackgroundPixmap(QPixmap(self.settings["bg"]))
else:
self.unsetPalette()
self.saveSettings()
def saveSettings(self):
item=self.firstItem()
while True:
if not item:
break
self.saveItemPosition(item.text(),item.x(),item.y())
item=item.nextItem()
self.shelf[self.folder]=self.settings
def loadSettings(self):
try:
self.settings=self.shelf[self.folder]
except KeyError:
self.settings={}
\#Here you have to set *ALL* defaults
self.settings["bg"]=None
self.settings["showHidden"]=False
self.settings["x"]=self.pos().x()
self.settings["y"]=self.pos().y()
self.settings["width"]=self.frameGeometry().width()
self.settings["height"]=self.frameGeometry().height()
self.settings["itemPositions"]={}
self.setBackgroundSlot(choose=False)
self.showHiddenAction.setOn(self.settings["showHidden"])
def openItemSlot(self,item):
global windows
if not item: \#Clicked on the background
return
name=os.path.join(self.folder,str(item.text()))
name=os.path.abspath(name)
if os.path.isdir(name):
\#Check if it is already open, if not, open it
if not windows.has_key(name):
Window(name,shelf=self.shelf).show()
else:
windows[name].show()
windows[name].raiseW()
else: \#Open it (FIXME: damn insecure)
os.system("kfmclient exec '%s'&"%name.replace("'","""'"'"'"""))
def closeEvent(self,event):
event.accept()
global windows
del windows[self.folder]
self.saveSettings()
def saveItemPosition(self,name,x,y):
name=str(name)
self.settings["itemPositions"][name]=(x,y)
def itemPosition(self,name):
name=str(name)
if self.settings["itemPositions"].has_key(name):
return self.settings["itemPositions"][name]
else:
return None
def moveEvent(self,event):
self.settings["x"]=self.pos().x()
self.settings["y"]=self.pos().y()
self.saveSettings()
QIconView.moveEvent(self,event)
def resizeEvent(self,event):
self.settings["width"]=self.frameGeometry().width()
self.settings["height"]=self.frameGeometry().height()
self.saveSettings()
QIconView.resizeEvent(self,event)
\#This takes the result of sys.exc_info() and displays a dialog from it
def showError(type,value,trace):
d=ErrorDialog(modal=True)
text=str(value)
d.report.setText(text)
d.exec_loop()
Timestamp: 29/05/2004 23:55
Time to go sleep. Next week, part two.
If someone has the will to hack on this, just contact me, tell me what you want to do, we can make it work :-)
Comments for this story are here:
http://www.haloscan.com/com...