Skip to main content

Ralsina.Me — Roberto Alsina's website

There Goes Captain Beto, Through Space

I said in dot.kde.org that I could write a spa­tial file man­ag­er in a week­end. I even said that I could write it this week­end if my date failed me.

Well, guess what? No date (she's gone to give a con­fer­ence), so.... I will write it, and it will be called Cap­tain Be­to. The rea­son for the name will be clear for al­most any ar­gen­tini­an, (think trans­la­tion, fel­low coun­try­men), and ob­scure for al­most any­one else. Which is as good a com­bi­na­tion as any.

Ok, here's all I know about this stuff:

  • It's sup­posed to man­age files, so it should be able to link, move, copy, and re­move files
  • It's sup­posed to be spa­tial, which as far as I know means it should be in­con­ve­nient ;-)
    • It should not let you open the same fold­er twice
    • It should re­mem­ber ev­ery­thing about a fold­er
      • In­­­di­vid­u­al­ized per-­­­fold­er pre­f­er­ences
      • Re­mem­ber ob­­­ject po­si­­­tion in the fold­er view

Of course, I in­tend to do it us­ing PyQt, and if we are luck­y, this trip will be ed­u­ca­tion­al for me, and if we are re­al­ly luck­y, al­so for some read­er­s.

I am pret­ty sure this would be much eas­i­er us­ing PyKDE in­stead, but I must con­fess right now I don't have it in my com­put­er. How­ev­er, I in­tend to get it in a few days, so that means that I will in­ten­tion­al­ly not do some stuff, like thumb­nail­s, which are much eas­i­er to do us­ing some KDE API.

So, this will be a pret­ty sim­ple ap­pli­ca­tion on this first stage. Al­so, since I have no in­tent to ac­tu­al­ly use this thing, it's not a ba­by with a very bright fu­ture, un­less some read­er feels like adopt­ing it.

It's sat­ur­day, may 29th, 2004, and my fuzzy clock says it's five to one... but it's re­al­ly five to 4.

Times­tam­p: 29/5/04 15:55

The Plan

I have nev­er writ­ten a file man­ag­er. I have nev­er even used them much. So, this is re­al­ly ex­plo­ration of ob­scure ter­ri­to­ry for me.

Here's the roadmap I am imag­in­ing:

  1. Cre­ate a wid­get that can show a fold­er
  2. Make it con­fig­urable
  3. Make it con­fig­urable per-­fold­er
  4. Let it "nav­i­gate" the fold­er tree
  5. Add ba­sic file op­er­a­tions

Sounds easy enough. I like it be­cause ev­ery step should be doable in a rea­son­able amount of time. I tend to lose in­ter­est if the next goal is too far. I am a sprint­er, not a dis­tance run­ner. Like Gim­li in "The Two Tow­er­s".

Times­tam­p: 29/5/04 16:05

Stage One

Stupid Application

Let's start with some boil­er­plate code: A main script that opens a win­dow.

\#!/us­r/bin/en­v python

from   qt   im­port   *
from   win­dow   im­port   Win­dow
im­port   sys

def   main(args):
        
app=QAp­pli­ca­tion(args)

        
win=Win­dow()
        
app.set­Main­Wid­get(win)
        
win.show()
        
app.con­nect(app,   SIG­NAL("last­Win­dow­Closed()"),   app,   SLOT("quit()"))
        
app.ex­ec_loop()

if   __­name__=="__­main__":

        
main(sys.argv)

As you can see, all this does is open a win­dow, lit­er­al­ly. And it as­sumes there is an­oth­er mod­ule, called win­dow.py, con­tain­ing a Win­dow class.

Well, that's boil­er­plate al­so (and very sim­ple one, too):

from   qt   im­port   *

class   Win­dow   (QWid­get):

        
def   __init__(self,par­ent=0):
                
QWid­get.__init__(self,par­ent)

So, we have a pro­gram that, when called, dis­plays a blank wid­get. When you close that wid­get, it dies. Not too in­ter­est­ing, that one.

So, let's make it do some trick­s.

Choosing the Widget

It turns out Qt has a wid­get called QI­con­View, de­scribed in the man­u­al 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 man­ag­er to me. So, we change our Win­dow to in­her­it from QI­con­View.

from   qt   im­port   *

class   Win­dow   (QI­con­View):

        
def   __init__(self,par­ent=0):
                
QI­con­View.__init__(self)

Now, that's an un­in­ter­est­ing white win­dow ;-)

/static/beto1.png

Of course, to make it use­ful, we need to be able to fill it with the con­tents of a fold­er.

Well, let's add an ar­gu­ment to the con­struc­tor, called fold­er, that is the fold­er that should be dis­played, and add a method, called change­Fold­er, that fills the QI­con­View with the con­tens of that fold­er.

It­er­at­ing over the con­tents of a fold­er in Python is easy enough, us­ing the os mod­ule. Adding icons to a QI­con­View is most­ly a mat­ter of cre­at­ing a bunch of QI­con­ViewItem­s.

class   Win­dow   (QI­con­View):

        
def   __init__(self,fold­er="/",par­ent=0):
                
QI­con­View.__init__(self)
                
self.change­Fold­er(fold­er)

        
def   change­Fold­er(self,fold­er):
                
fold­er=os.path.ab­spath(fold­er)
                
for   item   in   os.list­dir(fold­er):
                        
QI­con­ViewItem(self,item)

Now, this is some­what bet­ter :-)

/static/beto2.png

That the icon seems right for what is there is just a co­in­ci­dence.

So, let's use two icon­s, one called "fold­er" and the oth­er called "file", and stat the con­tents of the fold­er, as­sign­ing the right icon­s.

It re­al­ly shoud have a bet­ter cap­tion, re­flect­ing what you are see­ing in the win­dow, too.

Break

Times­tam­p: 29/05/04 16:20

Turns out I don't have the python docs in­stalled here. Nev­er pro­gram Python with­out the li­brary ref­er­ence at hand. So, I'm build­ing them now (I had the TeX sources in­side the Python sources). Should on­ly take a few min­utes

I would nor­mal­ly just use the ver­sion at www.python.org, but this is at home, with­out In­ter­net.

Times­tam­p: 29/05/04 17:00

The rea­son I need the li­brary ref­er­ence is that I don't re­mem­ber what the val­ues re­turned by os­.s­tat() mean.

Turns out what I want­ed was not os­.s­tat(), but os­.­path.is­dir() and com­pa­ny. See why the li­brary ref­er­ence is your friend? ;-)

I have vis­i­tors. Be back lat­er.

Times­tam­p: 29/05/04 17:05 Times­tam­p: 29/05/04 19:25

So this is how the Win­dow class looks now:

class   Win­dow   (QI­con­View):

        
def   __init__(self,fold­er="/",par­ent=0):
                
QI­con­View.__init__(self)
                
\#­Fixme, this should load­ the i­con­s in a rea­son­able way ;-)
                
self.folderI­con=QPixmap("/us­r/share/i­con­s/crys­talsvg/32x32/­filesys­tem­s/­fold­er.p­ng")
                
self.file­Icon=QPixmap("/us­r/share/i­con­s/crys­talsvg/32x32/mime­type­s/­doc­u­men­t.p­ng")
                
self.change­Fold­er(fold­er)

        
def   change­Fold­er(self,fold­er):
                
fold­er=os.path.ab­spath(fold­er)
                
self.set­Cap­tion   (fold­er+" - ­cap­tain ­be­to")
                
for   item   in   os.list­dir(fold­er):
                        
full­path=os.path.join(fold­er,item)
                        
\#­FIXME this should check­ ­for d­if­fer­en­t things, ­like
                        
\#links, de­vices, etc.
                        
if   os.path.is­dir(full­path):
                                
QI­con­ViewItem(self,item,self.folderI­con)
                        
elif   os.path.is­file(full­path):
                                
QI­con­ViewItem(self,item,self.file­Icon)
                        
else:
                                
QI­con­ViewItem(self,item,self.file­Icon)

It's ba­si­cal­ly the same, ex­cept that now, when I cre­ate the QI­con­ViewItem­s, we try (fee­bly) to use the right kind of icon.

And it looks a lit­tle bet­ter now:

/static/beto3.png

No­tice how it us­es dif­fer­ent icon­s. It turns out you can even move the icons around, too :-)

So, right now, I think I am close to the first mile­stone (cre­at­ing a wid­get that can show a fold­er). Of course, it's not a good wid­get, but it has po­ten­tial.

Some of the prob­lems with the cur­rent code, like load­ing the right icon, will be solved when this is turned in­to a PyKDE app lat­er.

The sec­ond mile­stone was "make it con­fig­urable". Well, let's.

Making it configurable

The usu­al stuff that you can con­fig­ure in a file man­ag­er is:

  • Choice to show hid­den files or not
  • It should re­mem­ber the po­si­tion of the icons
  • You should be able to set a back­ground

Any­thing else will hap­pen even­tu­al­ly in a dis­tant fu­ture. Maybe to­mor­row.

Showing (or not) hidden files

Let's add an­oth­er ar­gu­ment to change­Fold­er, called showHid­den, de­fault­ing to false. Then ig­nore (or not) hid­den files in the loop over os­.list­dir().

Should be sim­ple. I will not hide ".." be­cause it is the nat­u­ral way to go up.

Times­tam­p: 29/05/04 19:40

Here is change­Fold­er ig­nor­ing (or not) hid­den files. Do you no­tice how each thing I do is a 5, maybe 10 line change? I like to pro­gram in very small in­cre­ments. But that's just per­son­al pref­er­ence.

def   change­Fold­er(self,fold­er,   showHid­den=False):
        
fold­er=os.path.ab­spath(fold­er)
        
self.set­Cap­tion   (fold­er+" - ­cap­tain ­be­to")
        
for   item   in   os.list­dir(fold­er):
                
if   not   showHid­den:
                        
if   item[0]=="."   and   not   item=="..":
                                
con­tin­ue
                
full­path=os.path.join(fold­er,item)
                
\#­FIXME this should check­ ­for d­if­fer­en­t things, ­like
                
\#links, de­vices, etc.
                
if   os.path.is­dir(full­path):
                        
QI­con­ViewItem(self,item,self.folderI­con)
                
elif   os.path.is­file(full­path):
                        
QI­con­ViewItem(self,item,self.file­Icon)
                
else:
                        
QI­con­ViewItem(self,item,self.file­Icon)

Now, I want this to fol­low, as far as I know about it, the dog­ma of spa­tial file man­age­men­t, so a set­ting such as showHid­den should be stored and re­mem­bered.

It should al­so, of course, be pos­si­ble to change, and to be changed on a per-­fold­er ba­sis.

So, what UI should the us­er have to change this stuff: I like di­rect ma­nip­u­la­tion of ob­ject­s, which in this case prob­a­bly means a RMB pop­up on the fold­er it­self.

So, let's add that menu, and make "Show Hid­den" a tog­gle in it. The right way is to cre­ate an ac­tion and stuff.

In the Win­dow con­struc­tor, we add a chunk of code to cre­ate the showHid­de­n­Ac­tion, which will be a tog­gle. Al­so, we cre­ate a pop­up menu, and put the ac­tion in it.

self.rmb­Menu=QPop­up­Menu(self)

self.showHid­de­n­Ac­tion=QAc­tion("Show Hid­den ­Files",QKey­Se­quence("C­TR­L+H"),self)
self.showHid­de­n­Ac­tion.set­Tog­gle­Ac­tion(True)
self.showHid­de­n­Ac­tion.ad­dTo(self.rmb­Menu)

As you can see, ac­tions are pret­ty damn sim­ple. Now, we need to con­nect the ac­tion to a slot that tog­gles the dis­play, and make the pop­up menu show on right-but­ton-click.

To make the pop­up ... well, pop up, we need to con­nect the right­But­tonClicked() sig­nal of the QI­con­View, and con­nect it to a slot that pops the menu.

This is the slot we add to the Win­dow class:

def   show­Con­textMenu(self,item,pos):
        
if   not   item:
                
self.rmb­Menu.ex­ec_loop(pos)
        
else:
                
\#­FIXME Should show ­con­tex­t ­menu ­for the item where the
                
\#user right-clicked
                
pass

As you can see, there is a glar­ing lack of func­tion­al­i­ty. But that's ok, we are just build­ing up stuff here, the holes can be filled lat­er.

To con­nect the sig­nal to this slot, we add this line to Win­dow.__init__ :

self.con­nect(self,SIG­NAL("right­But­tonClicked(QI­con­ViewItem*,­con­st Q­Point&)"),self.show­Con­textMenu)

And lo and be­hold, we have a con­text menu, and it has a tog­gle say­ing "Show Hid­den Files". Now, we have to make it show (or not) ac­cord­ing to the tog­gle.

Sim­ple: we add a mem­ber called showHid­den to the Win­dow class, re­move the showHid­den ar­gu­ment from change­Fold­er, and make it use the vari­able. Then, we add an ac­ces­sor called set­ShowHid­den which tog­gles and re­dis­plays. And we con­nect that to showHid­de­n­Ac­tion.

Al­so, I re­named change­Fold­er to set­Fold­er, to be a bit con­sis­ten­t.

Here's the new win­dow.py:

from   qt   im­port   *
im­port   os

class   Win­dow   (QI­con­View):

        
def   __init__(self,fold­er="/",par­ent=0):
                
QI­con­View.__init__(self)
                
\#­Fixme, this should load­ the i­con­s in a rea­son­able way ;-)
                
self.folderI­con=QPixmap("/us­r/share/i­con­s/crys­talsvg/32x32/­filesys­tem­s/­fold­er.p­ng")
                
self.file­Icon=QPixmap("/us­r/share/i­con­s/crys­talsvg/32x32/mime­type­s/­doc­u­men­t.p­ng")

                
self.showHid­den=False

                
self.rmb­Menu=QPop­up­Menu(self)

                
self.showHid­de­n­Ac­tion=QAc­tion("Show Hid­den ­Files",QKey­Se­quence("C­TR­L+H"),self)
                
self.showHid­de­n­Ac­tion.set­Tog­gle­Ac­tion(True)
                
self.showHid­de­n­Ac­tion.ad­dTo(self.rmb­Menu)

                
self.con­nect(self.showHid­de­n­Ac­tion,SIG­NAL("tog­gled(­bool)"),self.tog­gleShowHid­denSlot)

                
self.con­nect(self,SIG­NAL("right­But­tonClicked(QI­con­ViewItem*,­con­st Q­Point&)"),self.show­Con­textMenu)

                
self.set­Fold­er(fold­er)

        
def   set­Fold­er(self,fold­er):
                
fold­er=os.path.ab­spath(fold­er)
                
self.set­Cap­tion   (fold­er+" - ­cap­tain ­be­to")
                
self.fold­er=fold­er
                
self.clear()
                
for   item   in   os.list­dir(fold­er):
                        
if   not   self.showHid­den:
                                
if   item[0]=="."   and   not   item=="..":
                                        
con­tin­ue
                        
full­path=os.path.join(fold­er,item)
                        
\#­FIXME this should check­ ­for d­if­fer­en­t things, ­like
                        
\#links, de­vices, etc.
                        
if   os.path.is­dir(full­path):
                                
QI­con­ViewItem(self,item,self.folderI­con)
                        
elif   os.path.is­file(full­path):
                                
QI­con­ViewItem(self,item,self.file­Icon)
                        
else:
                                
QI­con­ViewItem(self,item,self.file­Icon)



        
def   show­Con­textMenu(self,item,pos):
                
if   not   item:
                        
self.rmb­Menu.ex­ec_loop(pos)
                
else:
                        
\#­FIXME Should show ­con­tex­t ­menu ­for the item where the
                        
\#user right-clicked
                        
pass

        
def   tog­gleShowHid­denSlot(self,show):
                
self.showHid­den=show
                
self.set­Fold­er(self.fold­er)

The app looks ex­act­ly the same, al­though it has a lit­tle func­tion­al­i­ty ex­tra, so no screen­shot for you! ;-)

Times­tam­p: 29/05/04 20:15

Now, let's see how it looks with a back­ground pixmap. There is a set­Palet­te­Back­ground­Pixmap() method, so it should­n't be hard...

I say it looks good:

/static/beto4.png

Again, I cre­ate an ac­tion, called set­Back­groundAc­tion, con­nect it to a slot called set­Back­ground­Slot, and slap it on the con­text menu:

The ac­tion and the con­nec­tion (for Win­dow.__init__):

self.set­Back­groundAc­tion=QAc­tion("Set &Back­ground...",QKey­Se­quence(""),self)
self.set­Back­groundAc­tion.ad­dTo(self.rmb­Menu)
self.con­nect(self.set­Back­groundAc­tion,SIG­NAL("ac­ti­vat­ed()"),self.set­Back­ground­Slot)

The slot:

def   set­Back­ground­Slot(self):
        
bg=str(QFile­Di­a­log.getOpen­File­Name())
        
self.set­Palet­te­Back­ground­Pixmap(QPixmap("bg"))

Ok, that is get­ting bor­ing. So let's just say it is con­fig­urable enough for a while, OK?

And then we are at a new mile­stone. We made it con­fig­urable , now we must make it con­fig­urable per-­fold­er. That should be a bit trick­i­er.

Per-folder settings

Times­tam­p: 29/05/04 20:35

You know... I'm get­ting hun­gry. I'll or­der some Kun-­Pao... nah, it can wait a lit­tle.

There are many dif­fer­ent ways to go at stor­ing con­fig­u­ra­tion da­ta. I in­tend to keep this as sim­ple as pos­si­ble, so I will try to do it us­ing the shelve mod­ule.

You can think of shelve as a very (very) sim­ple data­base. You just store ob­jects in it, in­dexed by a sin­gle key. How­ev­er, that fits rather nice­ly with the idea of per-­fold­er set­tings, does­n't it? I mean, just in­dex them by the fold­er path :-)

So, I will cre­ate a lit­tle dic­tio­nary for the con­fig val­ues, called set­tings, and (un)shelve it as need­ed. So, self­.bg and self­.showHid­den will have to change again, now to stuff like self­.set­tings["showHid­den"]. Such is life for vari­ables.

To shelve set­tings, all you need is a glob­al shelf ob­jec­t. I will cre­ate it in the main scrip­t, and pass it as ar­gu­ment to the Win­dow class on cre­ation, so I can get a ref­er­ence to it eas­i­ly. This way to do it is prob­a­bly not the nices­t, but is what I can think about over the noise of my hun­gry stom­ach.

Here's the new main scrip­t, be­to.py:

\#!/us­r/bin/en­v python

from   qt   im­port   *
from   win­dow   im­port   Win­dow
im­port   sys
im­port   shelve

def   main(args):
        
app=QAp­pli­ca­tion(args)

        
shelf=shelve.open(".­be­toset­tings")

        
win=Win­dow(shelf=shelf)
        
win.show()
        
app.con­nect(app,   SIG­NAL("last­Win­dow­Closed()"),   app,   SLOT("quit()"))
        
app.ex­ec_loop()

if   __­name__=="__­main__":

        
main(sys.argv)

As you can see, no big changes I re­moved the set­Main­Wid­get cal­l, be­cause it breaks the app if it is meant to sup­port mul­ti­ple win­dows.

The shelf it­self is used just like a dic­tio­nary, so this is re­al­ly sim­ple to use. Look at the saveSet­tings and load­Set­tings in the next list­ing, as well as the places that call them.

win­dow.py suf­fered a bit more. Mak­ing sure all things change in har­mo­nious waves is al­ways trick­i­er than I ex­pec­t. In par­tic­u­lar, the han­dling of the back­ground here is very in­el­e­gan­t:

from   qt   im­port   *
im­port   os

class   Win­dow   (QI­con­View):

        
def   __init__(self,fold­er="/",par­ent=None,shelf=None):
                
QI­con­View.__init__(self,par­ent)

                
self.shelf=shelf
                
self.fold­er=None

                
\#­Fixme, this should load­ the i­con­s in a rea­son­able way ;-)
                
self.folderI­con=QPixmap("/us­r/share/i­con­s/crys­talsvg/32x32/­filesys­tem­s/­fold­er.p­ng")
                
self.file­Icon=QPixmap("/us­r/share/i­con­s/crys­talsvg/32x32/mime­type­s/­doc­u­men­t.p­ng")


                
self.rmb­Menu=QPop­up­Menu(self)

                
self.showHid­de­n­Ac­tion=QAc­tion("Show &Hid­den ­Files",QKey­Se­quence("C­TR­L+H"),self)
                
self.showHid­de­n­Ac­tion.set­Tog­gle­Ac­tion(True)
                
self.showHid­de­n­Ac­tion.ad­dTo(self.rmb­Menu)

                
self.con­nect(self.showHid­de­n­Ac­tion,SIG­NAL("tog­gled(­bool)"),self.tog­gleShowHid­denSlot)

                
self.set­Back­groundAc­tion=QAc­tion("Set &Back­ground...",QKey­Se­quence(""),self)
                
self.set­Back­groundAc­tion.ad­dTo(self.rmb­Menu)
                
self.con­nect(self.set­Back­groundAc­tion,SIG­NAL("ac­ti­vat­ed()"),self.set­Back­ground­Slot)



                
self.con­nect(self,SIG­NAL("right­But­tonClicked(QI­con­ViewItem*,­con­st Q­Point&)"),self.show­Con­textMenu)

                
self.set­Fold­er(fold­er)

        
def   set­Fold­er(self,fold­er):
                
fold­er=os.path.ab­spath(fold­er)
                
self.set­Cap­tion   (fold­er+" - ­cap­tain ­be­to")
                
if   fold­er!=self.fold­er:
                        
self.fold­er=fold­er
                        
self.load­Set­tings()

                
self.clear()

                
for   item   in   os.list­dir(fold­er):
                        
if   not   self.set­tings["showHid­den"]:
                                
if   item[0]=="."   and   not   item=="..":
                                        
con­tin­ue
                        
full­path=os.path.join(fold­er,item)
                        
\#­FIXME this should check­ ­for d­if­fer­en­t things, ­like
                        
\#links, de­vices, etc.
                        
if   os.path.is­dir(full­path):
                                
QI­con­ViewItem(self,item,self.folderI­con)
                        
elif   os.path.is­file(full­path):
                                
QI­con­ViewItem(self,item,self.file­Icon)
                        
else:
                                
QI­con­ViewItem(self,item,self.file­Icon)



        
def   show­Con­textMenu(self,item,pos):
                
if   not   item:
                        
self.rmb­Menu.ex­ec_loop(pos)
                
else:
                        
\#­FIXME Should show ­con­tex­t ­menu ­for the item where the
                        
\#user right-clicked
                        
pass

        
def   tog­gleShowHid­denSlot(self,show):
                
self.set­tings["showHid­den"]=show
                
self.saveSet­tings()
                
self.set­Fold­er(self.fold­er)

        
def   set­Back­ground­Slot(self,choose=True):
                
if   choose:
                        
self.set­tings["bg"]=str(QFile­Di­a­log.getOpen­File­Name())
                
if   self.set­tings["bg"]:
                        
self.set­Palet­te­Back­ground­Pixmap(QPixmap(self.set­tings["bg"]))
                
else:
                        
self.un­set­Palette()
                
self.saveSet­tings()

        
def   saveSet­tings(self):
                
self.shelf[self.fold­er]=self.set­tings

        
def   load­Set­tings(self):
                
try:
                        
self.set­tings=self.shelf[self.fold­er]
                
ex­cept   Key­Er­ror:
                        
self.set­tings={}
                        
\#Here y­ou have ­to set *AL­L* de­faults
                        
self.set­tings["bg"]=None
                        
self.set­tings["showHid­den"]=False

                
self.set­Back­ground­Slot(choose=False)

Al­so, I added a lit­tle er­ror check­ing here and there ;-)

And that's that when it comes to per-­fold­er set­tings. Not that hard, was it?

Times­tam­p: 29/05/04 21:05

And I have on­ly writ­ten about 115 lines of code!

So, what's nex­t? Fold­er nav­i­ga­tion!

Folder Navigation

Now, this is sim­ple: when you click on a fold­er, open it in an­oth­er win­dow. When you click on a file, open that file. Piece of cake.

There is a sig­nal called clicked(QI­con­ViewItem). Guess when is it trig­gered? Right, when you click on stuff.

If some­one wants a dou­ble-click ver­sion, use dou­bleClicked in­stead ;-)

So, when some­one clicks on stuff, we see if he clicked on an item, and if yes, whether it's a file or a fold­er, and act on it.

There is a trick, in that if you don't want the new win­dow to van­ish with­out a trace as soon as it gets out of scope, there must be some glob­al place where they are ref­er­enced. So I cre­at­ed a glob­al dic­tio­nary called win­dows in win­dow.py, where they get in­dexed by fold­er name on cre­ation.

win­dows={}

This is use­ful to avoid open­ing the same fold­er twice, which is sup­posed to be bad for some rea­son (don't ask me about it)

Here's the slot han­dling open­ing stuff:

def   open­Item­Slot(self,item):
        
glob­al   win­dows
        
if   not   item:   \#Clicked on the back­ground
                
re­turn
        
name=os.path.join(self.fold­er,str(item.text()))
        
if   os.path.is­dir(name):
                
\#Check­ if it is al­ready open, if not, open it
                
if   not   win­dows.has_key(name):
                        
Win­dow(name,shelf=self.shelf).show()
                
else:
                        
win­dows[name].show()
        
else:   \#Open it
                
\#­FIXME this is ­damn in­se­cure!
                
os.sys­tem("kfm­clien­t ex­ec '%s'&"%name)

Here's how it con­nects to the click­ing:

self.con­nect(self,SIG­NAL("clicked (QI­con­ViewItem *)"),self.open­Item­Slot)

When a win­dow clos­es, it has to delete it­self, and re­move it­slf from that glob­al dic­tio­nary. So, we over­ride the closeEven­t() han­dler:

def   closeEvent(self,event):
        
event.ac­cept()
        
glob­al   win­dows
        
del   win­dows[self.fold­er]
        
print   win­dows

Times­tam­p: 29/05/04 21:40

Now, the next mile­stone should be to add file man­ag­ing ca­pa­bil­i­ties, but I cheat­ed too much a while back, so I will get back to mak­ing it con­fig­urable.

In par­tic­u­lar, fold­ers should re­mem­ber their po­si­tion and size, and the po­si­tion of the stuff in­side them.

Making it more configurable, still

For ge­om­e­try, it's sim­ple. When­ev­er you move or re­size the win­dow, store a set­ting, and, on cre­ation, fol­low it. Triv­ial.

So, we over­ride re­sizeEven­t() and moveEven­t(), re­touch __init__ a bit, and add new de­faults in load­Set­tings().

The events:

def   moveEvent(self,event):
        
self.set­tings["x"]=self.pos().x()
        
self.set­tings["y"]=self.pos().y()
        
self.saveSet­tings()
        
QI­con­View.moveEvent(self,event)

def   re­sizeEvent(self,event):
        
self.set­tings["width"]=self.frameGe­om­e­try().width()
        
self.set­tings["height"]=self.frameGe­om­e­try().height()
        
self.saveSet­tings()
        
QI­con­View.re­sizeEvent(self,event)

The new load­Set­tings():

def   load­Set­tings(self):
        
try:
                
self.set­tings=self.shelf[self.fold­er]
        
ex­cept   Key­Er­ror:
                
self.set­tings={}
                
\#Here y­ou have ­to set *AL­L* de­faults
                
self.set­tings["bg"]=None
                
self.set­tings["showHid­den"]=False
                
self.set­tings["x"]=self.pos().x()
                
self.set­tings["y"]=self.pos().y()
                
self.set­tings["width"]=self.frameGe­om­e­try().width()
                
self.set­tings["height"]=self.frameGe­om­e­try().height()

        
self.set­Back­ground­Slot(choose=False)

And for the set­tings to take ef­fec­t, I added this in set­Fold­er (it could have gone to the __init__, but it looked clean­er this way), af­ter we call load­Set­tings:

self.move(self.set­tings["x"],
          
self.set­tings["y"])
self.re­size(self.set­tings["width"],
            
self.set­tings["height"])

Maybe it should be part of load­Set­tings it­self? Well, at least refac­tor­ing that should be sim­ple ;-)

And here's be­to, show­ing dif­fer­ent back­ground­s. And yes, all this so far work­s. At least as far as I test­ed it, which is not all that much.

/static/beto5.png

Times­tam­p: 29/05/04 22:05

Now, about the po­si­tion of items in the view... that's go­ing to be some­what hard­er.

By de­fault, when we cre­ate the item­s, they sim­ply spread them­selves at will. They can be moved by drag and drop. So, here's what I think should be done:

  1. When they are cre­at­ed, check if we re­mem­ber a po­si­tion for that name.
  2. If we don't, just cre­ate it, and save the po­si­tion it get­s.
  3. Fig­ure out when one is moved, and store that as well.

So, we will have to do some more stuff with set­tings. I will add a self­.set­tings["item­Po­si­tion­s"] that should be a dic­tio­nary, where po­si­tions will be in­dexed by name.

So, the po­si­tion for afile should be in self­.set­tings["item­Po­si­tion­s"]["afile"]. Of course there will be ac­ce­sor meth­ods for that ;-)

To fig­ure out when some­thing moves, I think we will have to con­nect to the QI­con­View.­moved() sig­nal. Here's what the docs say about it:

void QI­con­View::­moved () [sig­nal]

This sig­nal is emit­ted af­ter suc­cess­ful­ly drop­ping one (or more) items of the icon view. If the items should be re­moved, it's best to do so in a slot con­nect­ed to this sig­nal.

But first, I think I said some­thing about Kun Pao? Din­ner break!

Times­tam­p: 05/09/2004 22:15

Times­tam­p: 05/09/2004 22:50

That was nice. Now, let's get back to busi­ness.

Let's con­nect some­thing to the moved sig­nal, and when it is trig­gered, we save all the po­si­tion­s.

Oop­s. That is­n't trig­gered when mov­ing stuff around. So, let's just do it on the closeEven­t. I must it­er­ate over all icon­s, and save each one's po­si­tion.

Here's the new closeEven­t, and the meth­ods to save and load the po­si­tion­s:

def   closeEvent(self,event):
        
event.ac­cept()
        
glob­al   win­dows
        
del   win­dows[self.fold­er]

        
item=self.firstItem()
        
while   True:
                
self.saveIt­em­Po­si­tion(str(item.text()),item.x(),item.y())
                
item=item.nex­tItem()
                
if   not   item:
                        
break
        
self.saveSet­tings()

def   saveIt­em­Po­si­tion(self,name,x,y):
        
self.set­tings["item­Po­si­tion­s"][name]=(x,y)

def   item­Po­si­tion(self,name):
        
if   self.set­tings["item­Po­si­tion­s"].has_key(name):
                
re­turn   self.set­tings["item­Po­si­tion­s"][name]
        
else:
                
re­turn   None

Al­so, added a de­fault in load­Set­tings(), so item­Po­si­tions is ini­tial­ized as a dic­tio­nary. And, changed the loop that cre­ates the QListViewItems in set­Fold­er, like this:

for   item   in   os.list­dir(fold­er):
        
if   not   self.set­tings["showHid­den"]:
                
if   item[0]=="."   and   not   item=="..":
                        
con­tin­ue
        
full­path=os.path.join(fold­er,item)
        
\#­FIXME this should check­ ­for d­if­fer­en­t things, ­like
        
\#links, de­vices, etc.
        
if   os.path.is­dir(full­path):
                
i=QI­con­ViewItem(self,item,self.folderI­con)
        
elif   os.path.is­file(full­path):
                
i=QI­con­ViewItem(self,item,self.file­Icon)
        
else:
                
i=QI­con­ViewItem(self,item,self.file­Icon)

        
p=self.item­Po­si­tion(item)
        
if   p:
                
i.move(p[0],p[1])

How­ev­er, that does­n't work. It turns out that I am in­sert­ing and mov­ing the items be­fore the win­dow is shown. And in that case, Qt seems to call ar­rangeIt­em­s­In­Grid (it says so in the doc­s!). Which moves ev­ery­thing back :-)

In fac­t, I want it to use ar­rangeIt­em­s­In­Grid. And then I want to move them. So, I take the mov­ing-the-item log­ic in­to ar­rangeIt­em­s­In­Grid... and that does work.

How­ev­er, I have been chang­ing so much code, that it is time to do a full dump here so you can fol­low it on your own ;-)

be­to.py:

\#!/us­r/bin/en­v python

from   qt   im­port   *
from   win­dow   im­port   Win­dow
im­port   sys
im­port   shelve

def   main(args):
        
app=QAp­pli­ca­tion(args)

        
shelf=shelve.open(".­be­toset­tings")

        
win=Win­dow(shelf=shelf)
        
win.show()
        
app.con­nect(app,   SIG­NAL("last­Win­dow­Closed()"),   app,   SLOT("quit()"))
        
app.ex­ec_loop()

if   __­name__=="__­main__":

        
main(sys.argv)

But be­fore I show you win­dow.py...

Debugging

I made quite a few mis­takes while de­vel­op­ing this. Of­ten, I sim­ply did­n't know about some­thing that ex­ist­s, and thus I failed to take ad­van­tage of it. Here are some ex­pla­na­tion­s.

The context popup

I hooked the pop­up to the right­But­tonClicked sig­nal. It turns out that it's a bet­ter idea to hook con­textMenuRe­quest­ed, be­cause it will be more portable, and prob­a­bly will han­dle things like call­ing the con­text menu us­ing the key­board.

Triv­ial to change, just switch the sig­nals on the con­nect cal­l.

The opening of items

I hooked it to the clicked sig­nal. That sucks be­cause it means you can't ev­er se­lect any­thing. Which for the next mile­stone will be nec­es­sary. If I want it to act like KDE in sin­gle-click mod­e, I will have to check if the us­er has ctrl or shift pressed while click­ing.

That is some­what hard. So for now, re­luc­tant­ly, I will switch it to dou­bleClicked, so clicked will just se­lec­t.

The .. folder

os­.list­dir does­n't in­clude ".." by de­fault. So what, I'll just add it ;-)

The error reporting

Right now, when some­thing hap­pen­s, it prints the er­ror mes­sages (ex­cep­tion­s, what­ev­er) on stder­r. So, you won't see any­thing, un­less you are run­ning be­to from a ter­mi­nal. This is im­por­tant be­cause it hap­pen­s, for ex­am­ple, if you open a fold­er that you can't en­ter or read.

There is a some­what dirty gener­ic so­lu­tion for this, which I have used in the past. I can do this for ev­ery method where I sus­pect ex­cep­tions may be thrown by re­al life. Or, to be neat, on ev­ery method ;-)

First, I cre­ate a Er­ror­Dia­log class, us­ing de­sign­er. This is how it look­s:

/static/beto6.png

Then wrap the method in stuff like this, grab­bing the ex­cep­tion data:

try:
        
do_­some­thing(hard)
        
do_­more_stuff(hard­er)
ex­cept:
        
(type,val­ue,trace)=sys.ex­c_in­fo()
        
show­Er­ror(type,val­ue,trace)

And this is show­Er­ror:

\#This ­takes the re­sult of sys.ex­c_in­fo()  and dis­plays a ­di­alog from it
def   show­Er­ror(type,val­ue,trace):
        
d=Er­ror­Dia­log(modal=True)
        
text=str(val­ue)
        
d.re­port.set­Text(text)
        
d.ex­ec_loop()

And presto, the us­er can see the ex­cep­tion­s. Of course a re­al er­ror mes­sage should be cre­at­ed for the stuff that hap­pen­s, but this lets you see where you should cre­ate them :-)

And here is the er­ror han­dling in ac­tion:

/static/beto7.png

Normalizing paths

If you are in /fold­er and choose .., you get /fold­er/.. which is of course, not the same as / lit­er­al­ly, but it is in re­al­i­ty. So, the path should be nor­mal­ized, so that you don't open the fold­er twice (I in­sist, don't ask me why). But of course python has os­.­path.ab­spath() which is ex­act­ly what I need.

More item positioning problems

When you change some set­tings (right now, showHid­den), it calls set­Fold­er(). Since the win­dow is al­ready dis­played, and set­Fold­er clears the icons and reloads them, it is still nec­es­sary to move the items around as they are cre­at­ed. So, some stuff I had delet­ed, had to get back in set­Fold­er.

Al­so, since set­Fold­er reloads the set­tings, which con­tain the po­si­tion­s, it is pos­si­ble to do this:

  • move stuff around
  • change showHid­den
  • it reloads po­si­tions
  • clears
  • recre­ates item­s... and for­gets the stuff you moved at the be­gin­ning.

To solve this, I moved the po­si­tion-mem­o­riz­ing log­ic in­to saveSet­tings, and made sure saveSet­tings is called be­fore call­ing set­Fold­er. Prob­a­bly would make sense to call saveSet­tings at the beg­gin­ing in­side set­Fold­er, in­stead.

Al­so, make the QI­con­View not au­toAr­range.

ShowHidden initial state

Sil­ly bug: the tog­gle should match the state of the set­ting. So, set it when set­tings load.

The new item problem

When a new item ap­pears for some rea­son, ar­rangeIt­em­s­In­Grid will lay it just fine. But... then we move all the old items in­to the old grid po­si­tion! So, we step all over it. In fac­t, it gets worse: be­cause of a bug­fix de­scribed be­fore, which re­quired dis­abling au­toAr­range, ev­ery­thing ap­pears piled on the top-left cor­ner.

Here's all I came up with, but I don't quite like it. If some item has a known po­si­tion, then I put them there.

All the items with­out known po­si­tions go be­low that fol­low­ing a grid. Hope­ful­ly ;-)

I al­so changed how things align. For some rea­son, even when us­ing a grid, Qt aligns the texts, but not the icon­s?

It's far from per­fec­t. But at least items don't over­lap by ac­ci­den­t, even if they have very long names.

The code to do this is at the bot­tom of set­Fold­er in the next list­ing. It ain't pret­ty. And it's way too sim­plis­tic.

A Featurette

If you try to open some­thing al­ready ex­ist­ing, it will rise. It's just a one­lin­er in open­Item­Slot

Final Listing

So, here's a some­what nicer, a bit less bug­gy win­dow.py:

from   qt   im­port   *
im­port   os
from   pprint   im­port   pprint
im­port   sys
from   er­ror­dia­log   im­port   Er­ror­Dia­log

win­dows={}

class   Win­dow   (QI­con­View):

        
def   __init__(self,fold­er="/",par­ent=None,shelf=None):
                
QI­con­View.__init__(self,par­ent)

                
self.shelf=shelf
                
self.set­S­e­lec­tion­Mode(self.Ex­tend­ed)
                
self.se­tAu­toAr­range(False)
                
self.set­GridX(72)
                
self.set­GridY(72)
                
self.setSpac­ing(16)

                
\#­Fixme, this should load­ the i­con­s in a rea­son­able way ;-)
                
self.folderI­con=QPixmap("/us­r/share/i­con­s/crys­talsvg/32x32/­filesys­tem­s/­fold­er.p­ng")
                
self.file­Icon=QPixmap("/us­r/share/i­con­s/crys­talsvg/32x32/mime­type­s/­doc­u­men­t.p­ng")

                
self.rmb­Menu=QPop­up­Menu(self)

                
self.showHid­de­n­Ac­tion=QAc­tion("Show &Hid­den ­Files",QKey­Se­quence("C­TR­L+H"),self)
                
self.showHid­de­n­Ac­tion.set­Tog­gle­Ac­tion(True)
                
self.showHid­de­n­Ac­tion.ad­dTo(self.rmb­Menu)

                
self.con­nect(self.showHid­de­n­Ac­tion,SIG­NAL("tog­gled(­bool)"),self.tog­gleShowHid­denSlot)

                
self.set­Back­groundAc­tion=QAc­tion("Set &Back­ground...",QKey­Se­quence(""),self)
                
self.set­Back­groundAc­tion.ad­dTo(self.rmb­Menu)
                
self.con­nect(self.set­Back­groundAc­tion,SIG­NAL("ac­ti­vat­ed()"),self.set­Back­ground­Slot)

                
self.con­nect(self,SIG­NAL("con­textMenuRe­quest­ed(QI­con­ViewItem*,­con­st Q­Point&)"),self.show­Con­textMenu)

                
self.con­nect(self,SIG­NAL("dou­bleClicked (QI­con­ViewItem *)"),self.open­Item­Slot)
                
self.set­Fold­er(fold­er)

                
glob­al   win­dows
                
win­dows[fold­er]=self

        
def   set­Fold­er(self,fold­er):
                
try:
                        
self.fold­er=fold­er
                        
self.load­Set­tings()
                        
self.set­Cap­tion   (fold­er+" - ­cap­tain ­be­to")

                        
self.move(self.set­tings["x"],
                                
self.set­tings["y"])
                        
self.re­size(self.set­tings["width"],
                                
self.set­tings["height"])

                        
self.clear()

                        
if   fold­er=="/":
                                
list=os.list­dir(fold­er)
                        
else:
                                
list=[".."]+os.list­dir(fold­er)

                        
un­placed=[]
                        
maxY=0

                        
for   item   in   list:
                                
if   not   self.set­tings["showHid­den"]:
                                        
if   item[0]=="."   and   not   item=="..":
                                                
con­tin­ue
                                
full­path=os.path.join(fold­er,item)
                                
\#­FIXME this should check­ ­for d­if­fer­en­t things, ­like
                                
\#links, de­vices, etc.
                                
if   os.path.is­dir(full­path):
                                        
i=QI­con­ViewItem(self,item,self.folderI­con)
                                
elif   os.path.is­file(full­path):
                                        
i=QI­con­ViewItem(self,item,self.file­Icon)
                                
else:
                                        
i=QI­con­ViewItem(self,item,self.file­Icon)
                                
p=self.item­Po­si­tion(item)
                                
if   p:
                                        
i.move(p[0],p[1])
                                        
if   p[1]>maxY:
                                                
maxY=p[1]
                                
else:
                                        
un­placed.ap­pend(i)

                        
curX=0
                        
curY=0
                        
if   maxY:   \#If no item has ­known ­po­si­tion, s­tart at the ­top
                                
curY=maxY+self.gridY()

                        
maxy=self.gridY()

                        
for   item   in   un­placed:
                                
size=item.size()
                                
w=size.width()
                                
h=size.height()
                                
if   h>maxy:
                                        
maxy=h
                                
item.move(curX+(self.gridX()-w)/2,curY)
                                
self.saveIt­em­Po­si­tion(item.text(),curX,curY)
                                
curX=curX+self.gridX()
                                
if   curX   >self.width()-self.gridX():
                                        
curX=0
                                        
curY=curY+maxy
                                        
maxy=self.gridY()
                
ex­cept:
                        
(type,val­ue,trace)=sys.ex­c_in­fo()
                        
show­Er­ror(type,val­ue,trace)


        
def   ar­rangeIt­em­s­In­Grid(self,up­date=True):
                
QI­con­View.ar­rangeIt­em­s­In­Grid(self,up­date)
                
item=self.firstItem()
                
while   True:
                        
p=self.item­Po­si­tion(item.text())
                        
if   p:
                                
item.move(p[0],p[1])
                        
item=item.nex­tItem()
                        
if   not   item:
                                
break


        
def   show­Con­textMenu(self,item,pos):
                
if   not   item:
                        
self.rmb­Menu.ex­ec_loop(pos)
                
else:
                        
\#­FIXME Should show ­con­tex­t ­menu ­for the item where the
                        
\#user right-clicked
                        
pass

        
def   tog­gleShowHid­denSlot(self,show):
                
self.set­tings["showHid­den"]=show
                
self.saveSet­tings()
                
self.set­Fold­er(self.fold­er)

        
def   set­Back­ground­Slot(self,choose=True):
                
if   choose:
                        
self.set­tings["bg"]=str(QFile­Di­a­log.getOpen­File­Name())
                
if   self.set­tings["bg"]:
                        
self.set­Palet­te­Back­ground­Pixmap(QPixmap(self.set­tings["bg"]))
                
else:
                        
self.un­set­Palette()
                
self.saveSet­tings()

        
def   saveSet­tings(self):
                
item=self.firstItem()
                
while   True:
                        
if   not   item:
                                
break
                        
self.saveIt­em­Po­si­tion(item.text(),item.x(),item.y())
                        
item=item.nex­tItem()
                
self.shelf[self.fold­er]=self.set­tings

        
def   load­Set­tings(self):
                
try:
                        
self.set­tings=self.shelf[self.fold­er]
                
ex­cept   Key­Er­ror:
                        
self.set­tings={}
                        
\#Here y­ou have ­to set *AL­L* de­faults
                        
self.set­tings["bg"]=None
                        
self.set­tings["showHid­den"]=False
                        
self.set­tings["x"]=self.pos().x()
                        
self.set­tings["y"]=self.pos().y()
                        
self.set­tings["width"]=self.frameGe­om­e­try().width()
                        
self.set­tings["height"]=self.frameGe­om­e­try().height()
                        
self.set­tings["item­Po­si­tion­s"]={}

                
self.set­Back­ground­Slot(choose=False)
                
self.showHid­de­n­Ac­tion.se­tOn(self.set­tings["showHid­den"])

        
def   open­Item­Slot(self,item):
                
glob­al   win­dows
                
if   not   item:   \#Clicked on the back­ground
                        
re­turn
                
name=os.path.join(self.fold­er,str(item.text()))
                
name=os.path.ab­spath(name)

                
if   os.path.is­dir(name):
                        
\#Check­ if it is al­ready open, if not, open it
                        
if   not   win­dows.has_key(name):
                                
Win­dow(name,shelf=self.shelf).show()
                        
else:
                                
win­dows[name].show()
                                
win­dows[name].raiseW()
                
else:   \#Open it (­FIXME: ­damn in­se­cure)
                        
os.sys­tem("kfm­clien­t ex­ec '%s'&"%name.re­place("'","""'"'"'"""))


        
def   closeEvent(self,event):
                
event.ac­cept()
                
glob­al   win­dows
                
del   win­dows[self.fold­er]
                
self.saveSet­tings()


        
def   saveIt­em­Po­si­tion(self,name,x,y):
                
name=str(name)
                
self.set­tings["item­Po­si­tion­s"][name]=(x,y)

        
def   item­Po­si­tion(self,name):
                
name=str(name)
                
if   self.set­tings["item­Po­si­tion­s"].has_key(name):
                        
re­turn   self.set­tings["item­Po­si­tion­s"][name]
                
else:
                        
re­turn   None

        
def   moveEvent(self,event):
                
self.set­tings["x"]=self.pos().x()
                
self.set­tings["y"]=self.pos().y()
                
self.saveSet­tings()
                
QI­con­View.moveEvent(self,event)

        
def   re­sizeEvent(self,event):
                
self.set­tings["width"]=self.frameGe­om­e­try().width()
                
self.set­tings["height"]=self.frameGe­om­e­try().height()
                
self.saveSet­tings()
                
QI­con­View.re­sizeEvent(self,event)

\#This ­takes the re­sult of sys.ex­c_in­fo()  and dis­plays a ­di­alog from it
def   show­Er­ror(type,val­ue,trace):
        
d=Er­ror­Dia­log(modal=True)
        
text=str(val­ue)
        
d.re­port.set­Text(text)
        
d.ex­ec_loop()

Times­tam­p: 29/05/2004 23:55

Time to go sleep. Next week, part two.

If some­one has the will to hack on this, just con­tact me, tell me what you want to do, we can make it work :-)

Roberto Alsina / 2006-04-04 16:30:

Comments for this story are here:

http://www.haloscan.com/com...


Contents © 2000-2020 Roberto Alsina