|
comment []
trackback [] 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 The PlanI 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 Stage OneStupid ApplicationLet's start with some boilerplate code: A main script that opens a window.
#!/usr/bin/env python
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):
from qt import *
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 WidgetIt turns out Qt has a widget called QIconView, described in the manual as:
Looks like a file manager to me. So, we change our Window to inherit from QIconView.
from qt import *
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.
class Window (QIconView):
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. BreakTimestamp: 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:
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.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 configurableThe usual stuff that you can configure in a file manager is:
Anything else will happen eventually in a distant future. Maybe tomorrow. Per-folder settingsTimestamp: 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:
#!/usr/bin/env python
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:
from qt import *
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! Making it more configurable, stillFor 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:
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) The new loadSettings():
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.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.move(self.settings["x"],
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:
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 |