--- category: '' date: 2004/06/03 10:22 description: '' link: '' priority: '' slug: '29' tags: kde, programming, python title: 'There Goes Captain Beto, Through Space ' type: text updated: 2004/06/03 10:22 url_type: '' --- .. raw:: html
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:
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
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:
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
Let's start with some boilerplate code: A main script that opens a window.
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):
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.
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.
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.
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.
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:
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.
The usual stuff that you can configure in a file manager is:
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.
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.
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:
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:
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__):
The slot:
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.
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:
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:
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!
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:
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:
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.
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:
The new loadSettings():
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:
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:
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:
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:
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:
But before I show you window.py...
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.
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.
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.
os.listdir doesn't include ".." by default. So what, I'll just add it ;-)
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:
And this is showError:
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:
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.
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:
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.
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.
If you try to open something already existing, it will rise. It's just a oneliner in openItemSlot
So, here's a somewhat nicer, a bit less buggy window.py:
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 :-)