ScolaSync 1.0
mainWindow.py
Aller à la documentation de ce fichier.
00001 #!/usr/bin/python
00002 # -*- coding: utf-8 -*-
00003 #       $Id: mainWindow.py 47 2011-06-13 10:20:14Z georgesk $   
00004 
00005 licence={}
00006 licence['en']="""
00007     file mainWindow.py
00008     this file is part of the project scolasync
00009     
00010     Copyright (C) 2010 Georges Khaznadar <georgesk@ofset.org>
00011 
00012     This program is free software: you can redistribute it and/or modify
00013     it under the terms of the GNU General Public License as published by
00014     the Free Software Foundation, either version3 of the License, or
00015     (at your option) any later version.
00016 
00017     This program is distributed in the hope that it will be useful,
00018     but WITHOUT ANY WARRANTY; without even the implied warranty of
00019     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00020     GNU General Public License for more details.
00021 
00022     You should have received a copy of the GNU General Public License
00023     along with this program.  If not, see <http://www.gnu.org/licenses/>.
00024 """
00025 
00026 from PyQt4.QtCore import *
00027 from PyQt4.QtGui import *
00028 import ownedUsbDisk, help, copyToDialog1, chooseInSticks, usbThread
00029 import diskFull, preferences
00030 import os.path, operator, subprocess, dbus, re, time, copy
00031 from notification import Notification
00032 import db
00033 import deviceListener
00034 from globaldef import logFileName, _dir
00035 
00036 # cette donnée est globale, pour être utilisé depuis n'importe quel objet
00037 qApp.diskData=ownedUsbDisk.Available(True,access="firstFat")
00038 
00039 activeThreads={} # donnée globale : les threads actifs
00040 # cette donnée est mise à jour par des signaux émis au niveau des threads
00041 # et elle est utilisée par la routine de traçage des cases du tableau
00042 pastCommands={}  # donnée globale : les commandes réalisées dans le passé
00043 lastCommand=None # donnée globale : la toute dernière commande
00044 
00045 ##
00046 # 
00047 #     enregistre la commande cmd pour la partition donnée
00048 #     @param cmd une commande pour créer un thread t
00049 #     @param partition une partition
00050 #     
00051 def registerCmd(cmd,partition):
00052     global pastCommands, lastCommand
00053     if pastCommands.has_key(cmd):
00054         pastCommands[cmd].append(partition.owner)
00055     else:
00056         pastCommands[cmd]=[partition.owner]
00057     lastCommand=cmd
00058 
00059 class mainWindow(QMainWindow):
00060     ##
00061     # 
00062     #         Le constructeur
00063     #         @param parent un QWidget
00064     #         @param opts une liste d'options extraite à l'aide de getopts
00065     #         @param locale la langue de l'application
00066     #         
00067     def __init__(self, parent, opts, locale="fr_FR"):
00068         QMainWindow.__init__(self)
00069         QWidget.__init__(self, parent)
00070         self.locale=locale
00071         from Ui_mainWindow  import Ui_MainWindow
00072         self.ui = Ui_MainWindow()
00073         self.ui.setupUi(self)
00074         # initialise deux icônes
00075         self.initRedoStuff()
00076         # initialise le tableau
00077         self.t=self.ui.tableView
00078         self.proxy=QSortFilterProxyModel()
00079         self.proxy.setSourceModel(self.t.model())
00080         self.opts=opts
00081         self.timer=QTimer()
00082         self.applyPreferences()
00083         self.listener=deviceListener.DeviceListener(self)
00084         self.updateButtons()
00085         self.operations=[] # liste des opérations précédemment "réussies"
00086         self.oldThreads=set() # threads lancés éventuellement encore vivants
00087         self.flashTimer=QTimer()
00088         self.flashTimer.setSingleShot(True)
00089         self.checkDisksLock=False # autorise self.checkDisks
00090         QObject.connect(self.ui.forceCheckButton, SIGNAL("clicked()"), self.checkDisks)
00091         QObject.connect(self.timer, SIGNAL("timeout()"), self.checkDisks)
00092         QObject.connect(self.flashTimer, SIGNAL("timeout()"), self.normalLCD);
00093         QObject.connect(self.ui.helpButton, SIGNAL("clicked()"), self.help)
00094         QObject.connect(self.ui.umountButton, SIGNAL("clicked()"), self.umount)
00095         QObject.connect(self.ui.toButton, SIGNAL("clicked()"), self.copyTo)
00096         QObject.connect(self.ui.fromButton, SIGNAL("clicked()"), self.copyFrom)
00097         QObject.connect(self.ui.delButton, SIGNAL("clicked()"), self.delFiles)
00098         QObject.connect(self.ui.redoButton, SIGNAL("clicked()"), self.redoCmd)
00099         QObject.connect(self.ui.preferenceButton, SIGNAL("clicked()"), self.preference)
00100         QObject.connect(self.ui.tableView, SIGNAL("doubleClicked(const QModelIndex&)"), self.tableClicked)
00101         QObject.connect(self,SIGNAL("deviceAdded(QString)"), self.deviceAdded)
00102         QObject.connect(self,SIGNAL("deviceRemoved(QString)"), self.deviceRemoved)
00103 
00104 
00105     ##
00106     # 
00107     #         fonction de rappel pour un medium ajouté
00108     #         @param s chemin UDisks, exemple : /org/freedesktop/UDisks/devices/sdb3
00109     #         
00110     def deviceAdded(self, s):
00111         ## print "dans deviceAdded", s
00112         if self.listener.vfatUsbPath(str(s)):
00113             self.checkDisks(noLoop=True)
00114         
00115     ##
00116     # 
00117     #         fonction de rappel pour un medium retiré
00118     #         @param s une chaine de caractères du type /dev/sdxy
00119     #         
00120     def deviceRemoved(self, s):
00121         ## print "dans deviceRemoved", s
00122         if qApp.diskData.hasDev(s):
00123             self.checkDisks()
00124         
00125     ##
00126     # 
00127     #         Initialise des données pour le bouton central (refaire/stopper)
00128     #         
00129     def initRedoStuff(self):
00130         # réserve les icônes
00131         self.iconRedo = QIcon()
00132         self.iconRedo.addPixmap(QPixmap("/usr/share/icons/Tango/scalable/actions/go-jump.svg"), QIcon.Normal, QIcon.Off)
00133         self.iconStop = QIcon()
00134         self.iconStop.addPixmap(QPixmap("/usr/share/icons/Tango/scalable/actions/stop.svg"), QIcon.Normal, QIcon.Off)
00135         # réserve les phrases d'aide
00136         self.redoToolTip=QApplication.translate("MainWindow", "Refaire à nouveau", None, QApplication.UnicodeUTF8)
00137         self.redoStatusTip=QApplication.translate("MainWindow", "Refaire à nouveau la dernière opération réussie, avec les baladeurs connectés plus récemment", None, QApplication.UnicodeUTF8)
00138         self.stopToolTip=QApplication.translate("MainWindow", "Arrêter les opérations en cours", None, QApplication.UnicodeUTF8)
00139         self.stopStatusTip=QApplication.translate("MainWindow", "Essaie d'arrêter les opérations en cours. À faire seulement si celles-ci durent trop longtemps", None, QApplication.UnicodeUTF8)
00140 
00141     ##
00142     # 
00143     #         modification du comportement du widget original, pour
00144     #         démarrer le timer et les vérifications de baladeurs
00145     #         après construction de la fenêtre seulement
00146     #         
00147     def showEvent (self, ev):
00148         result=QMainWindow.showEvent(self, ev)
00149         self.setTimer()
00150         self.checkDisks(force=True) # met à jour le compte de disques affiché
00151         return result
00152 
00153     ##
00154     # 
00155     #         sets the main timer
00156     #         
00157     def setTimer(self, enabled=True):
00158         if self.refreshEnabled:
00159             self.timer.start(self.refreshDelay*1000)
00160         else:
00161             self.timer.stop()
00162 
00163     ##
00164     # 
00165     #         Applique les préférences et les options de ligne de commande
00166     #         
00167     def applyPreferences(self):
00168         prefs=db.readPrefs()
00169         self.workdir=prefs["workdir"]
00170         self.refreshEnabled=prefs["refreshEnabled"]
00171         self.refreshDelay=prefs["refreshDelay"]
00172         self.setTimer()
00173         self.manFileLocation=prefs["manfile"]
00174         # on active les cases à cocher si ça a été réclamé par les options
00175         # ou par les préférences
00176         self.checkable=("--check","") in self.opts or ("-c","") in self.opts or prefs["checkable"]
00177         self.mv=prefs["mv"]
00178         other=ownedUsbDisk.Available(self.checkable,access="firstFat")
00179         qApp.diskData=other
00180         self.header=ownedUsbDisk.uDisk.headers(self.checkable)
00181         self.connectTableModel(other)
00182 
00183     ##
00184     # 
00185     #         change le répertoire par défaut contenant les fichiers de travail
00186     #         @param newDir le nouveau nom de répertoire
00187     #         
00188     def changeWd(self, newDir):
00189         self.workdir=newDir
00190         db.setWd(newDir)
00191 
00192     ##
00193     # 
00194     #         fonction de rappel pour un double clic sur un élément de la table
00195     #         @param idx un QModelIndex
00196     #         
00197     def tableClicked(self, idx):
00198         c=idx.column()
00199         mappedIdx=self.proxy.mapFromSource(idx)
00200         r=mappedIdx.row()
00201         ## print "row=%d mapped row=%d" %(idx.row(), r)
00202         h=self.header[c]
00203         if c==0 and self.checkable:
00204             # case à cocher
00205             pass
00206         elif c==1:
00207             # case du propriétaire
00208             self.editOwner(mappedIdx)
00209         elif "device-mount-paths" in h:
00210             cmd=u"nautilus '%s'" %idx.data().toString ()
00211             subprocess.call(cmd, shell=True)
00212         elif "device-size" in h:
00213             mount=idx.model().partition(idx).mountPoint()
00214             dev,total,used,remain,pcent,path = self.diskSizeData(mount)
00215             pcent=int(pcent[:-1])
00216             w=diskFull.mainWindow(self,pcent,title=path, total=total, used=used)
00217             w.show()
00218         else:
00219             QMessageBox.warning(None,
00220                                 QApplication.translate("Dialog","Double-clic non pris en compte",None, QApplication.UnicodeUTF8),
00221                                 QApplication.translate("Dialog","pas d'action pour l'attribut %1",None, QApplication.UnicodeUTF8).arg(h))
00222 
00223     ##
00224     # 
00225     #         @param rowOrDev a row number in the tableView, or a device string
00226     #         @return a tuple dev,total,used,remain,pcent,path for the
00227     #         disk in the given row of the tableView
00228     #         (the tuple comes from the command df)
00229     #         
00230     def diskSizeData(self, rowOrDev):
00231         if type(rowOrDev)==type(0):
00232             path=qApp.diskData[rowOrDev][self.header.index("1device-mount-paths")]
00233         else:
00234             path=rowOrDev
00235         cmd =u"df '%s'" %path
00236         dfOutput=subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate()[0].split("\n")[-2]
00237         m = re.match("(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+).*", dfOutput).groups()
00238         return m
00239 
00240 
00241     ##
00242     # 
00243     #         trouve le disque qui correspond à un propriétaire
00244     #         @param student le propriétaire du disque
00245     #         @return le disque correspondant à l'étudiant
00246     #         
00247     def diskFromOwner(self,student):
00248         found=False
00249         for d in qApp.diskData.disks.keys():
00250             if d.owner==student:
00251                 found=True
00252                 break
00253             # si on ne trouve pas avec le nom, on essaie de trouver
00254             # un disque encore inconnu, le premier venu
00255             if d.owner==None or len(d.owner)==0:
00256                 found=True
00257                 break
00258         if found:
00259             return d
00260         else:
00261             return None
00262         
00263     ##
00264     # 
00265     #         Édition du propriétaire d'une clé.
00266     #         @param idx un QModelIndex qui pointe sur le propriétaire d'une clé
00267     #         
00268     def editOwner(self, idx):
00269         student=u"%s" %self.tm.data(idx,Qt.DisplayRole).toString()
00270         ownedUsbDisk.editRecord(self.diskFromOwner(student), prompt=student)
00271         other=ownedUsbDisk.Available(self.checkable,access="firstFat")
00272         qApp.diskData=other
00273         self.connectTableModel(other)
00274         self.checkDisks()
00275         
00276     ##
00277     # 
00278     #         Désactive ou active les flèches selon que l'option correspondante
00279     #         est possible ou non. Pour les flèches : ça aurait du sens de préparer
00280     #         une opération de copie avant même de brancher des clés, donc on les
00281     #         active. Par contre démonter les clés quand elles sont absentes ça
00282     #         n'a pas d'utilité.
00283     #         
00284     def updateButtons(self):
00285         global activeThreads, lastCommand
00286         active = len(qApp.diskData)>0
00287         for button in (self.ui.toButton,
00288                        self.ui.fromButton,
00289                        self.ui.delButton,
00290                        self.ui.umountButton):
00291             button.setEnabled(active)
00292         # l'état du redoButton dépend de plusieurs facteurs
00293         # si un thread au moins est en cours, on y affiche un STOP actif
00294         # sinon on y met l'icône de lastCommand, et celle-ci sera active
00295         # seulement s'il y a une commande déjà validée
00296         if len(activeThreads) > 0:
00297             self.ui.redoButton.setIcon(self.iconStop)
00298             self.ui.redoButton.setToolTip(self.stopToolTip)
00299             self.ui.redoButton.setStatusTip(self.stopStatusTip)
00300             self.ui.redoButton.setEnabled(True)
00301         else:
00302             self.oldThreads=set() # vide l'ensemble puisque tout est fini
00303             self.ui.redoButton.setIcon(self.iconRedo)
00304             self.ui.redoButton.setToolTip(self.redoToolTip)
00305             self.ui.redoButton.setStatusTip(self.redoStatusTip)
00306             self.ui.redoButton.setEnabled(lastCommand!=None)
00307 
00308     ##
00309     # 
00310     #         lance le dialogue des préférences
00311     #         
00312     def preference(self):
00313         pref=preferences.preferenceWindow()
00314         pref.setValues(db.readPrefs())
00315         pref.show()
00316         pref.exec_()
00317         if pref.result()==QDialog.Accepted:
00318             db.writePrefs(pref.values())
00319             # on applique les préférences tout de suite sans redémarrer
00320             self.applyPreferences()
00321 
00322     ##
00323     # 
00324     #         Lance l'action de supprimer des fichiers ou des répertoires dans les clés USB
00325     #         
00326     def delFiles(self):
00327         titre1=QApplication.translate("Dialog","Choix de fichiers à supprimer",None, QApplication.UnicodeUTF8)
00328         titre2=QApplication.translate("Dialog","Choix de fichiers à supprimer (jokers autorisés)",None, QApplication.UnicodeUTF8)
00329         d=chooseInSticks.chooseDialog(self, titre1, titre2)
00330         ok = d.exec_()
00331         if ok:
00332             pathList=map(lambda x: u"%s" %x, d.pathList())
00333             buttons=QMessageBox.Ok|QMessageBox.Cancel
00334             defaultButton=QMessageBox.Cancel
00335             reply=QMessageBox.warning(
00336                 None,
00337                 QApplication.translate("Dialog","Vous allez effacer plusieurs baladeurs",None, QApplication.UnicodeUTF8),
00338                 QApplication.translate("Dialog","Etes-vous certain de vouloir effacer : "+"\n".join(pathList),None, QApplication.UnicodeUTF8),
00339                 buttons, defaultButton)
00340             if reply == QMessageBox.Ok:
00341                 cmd='t=usbThread.threadDeleteInUSB(p,%s,subdir="Travail", logfile="%s", parent=self.tm)' %(pathList,logFileName)
00342                 for p in qApp.diskData:
00343                     if not p.selected: continue # pas les médias désélectionnés
00344                     registerCmd(cmd,p)
00345                     exec(compile(cmd,'<string>','exec'))
00346                     t.setDaemon(True)
00347                     t.start()
00348                     self.oldThreads.add(t)
00349             return True
00350         else:
00351             msgBox=QMessageBox.warning(
00352                 None,
00353                 QApplication.translate("Dialog","Aucun fichier sélectionné",None, QApplication.UnicodeUTF8),
00354                 QApplication.translate("Dialog","Veuillez choisir au moins un fichier",None, QApplication.UnicodeUTF8))
00355             return True
00356 
00357     ##
00358     # 
00359     #         Lance l'action de copier vers les clés USB
00360     #         
00361     def copyTo(self):
00362         d=copyToDialog1.copyToDialog1(parent=self, workdir=self.workdir)
00363         d.exec_()
00364         if d.ok==True:
00365             cmd='t=usbThread.threadCopyToUSB(p,%s,subdir="%s", logfile="%s", parent=self.tm)' %(d.selectedList(), self.workdir, logFileName)
00366             for p in qApp.diskData:
00367                 if not p.selected: continue # pas les médias désélectionnés
00368                 registerCmd(cmd,p)
00369                 exec(compile(cmd,'<string>','exec'))
00370                 t.setDaemon(True)
00371                 t.start()
00372                 self.oldThreads.add(t)
00373             return True
00374         else:
00375             msgBox=QMessageBox.warning(
00376                 None,
00377                 QApplication.translate("Dialog","Aucun fichier sélectionné",None, QApplication.UnicodeUTF8),
00378                 QApplication.translate("Dialog","Veuillez choisir au moins un fichier",None, QApplication.UnicodeUTF8))
00379             return True
00380 
00381     ##
00382     # 
00383     #         Lance l'action de copier depuis les clés USB
00384     #         
00385     def copyFrom(self):
00386         titre1=QApplication.translate("Dialog","Choix de fichiers à copier",None, QApplication.UnicodeUTF8)
00387         titre2=QApplication.translate("Dialog", "Choix de fichiers à copier depuis les baladeurs", None, QApplication.UnicodeUTF8)
00388         ok=QApplication.translate("Dialog", "Choix de la destination ...", None, QApplication.UnicodeUTF8)
00389         d=chooseInSticks.chooseDialog(self, title1=titre1, title2=titre2, ok=ok)
00390         ok = d.exec_()
00391         if not ok or len(d.pathList())==0 :
00392             msgBox=QMessageBox.warning(None,
00393                                        QApplication.translate("Dialog","Aucun fichier sélectionné",None, QApplication.UnicodeUTF8),
00394                                        QApplication.translate("Dialog","Veuillez choisir au moins un fichier",None, QApplication.UnicodeUTF8))
00395             return True
00396         # bon, alors c'est OK pour le choix des fichiers à envoyer
00397         pathList=map(lambda x: u"%s" %x, d.pathList())
00398         mp=d.selectedDiskMountPoint()
00399         initialPath=os.path.expanduser("~")
00400         destDir = QFileDialog.getExistingDirectory(
00401             None,
00402             QApplication.translate("Dialog","Choisir un répertoire de destination",None, QApplication.UnicodeUTF8),
00403             initialPath)
00404         if destDir and len(destDir)>0 :
00405             if self.mv:
00406                 cmd=u"""t=usbThread.threadMoveFromUSB(
00407                            p,%s,subdir=self.workdir,
00408                            rootPath="%s", dest="%s", logfile="%s",
00409                            parent=self.tm)""" %(pathList, mp, destDir, logFileName)
00410             else:
00411                 cmd=u"""t=usbThread.threadCopyFromUSB(
00412                            p,%s,subdir=self.workdir,
00413                            rootPath="%s", dest="%s", logfile="%s",
00414                            parent=self.tm)""" %(pathList, mp, destDir, logFileName)
00415                 
00416             for p in qApp.diskData:
00417                 if not p.selected: continue # pas les médias désélectionnés
00418                 # on devrait vérifier s'il y a des données à copier
00419                 # et s'il n'y en a pas, ajouter des lignes au journal
00420                 # mais on va laisser faire ça dans le thread
00421                 # inconvénient : ça crée quelquefois des sous-répertoires
00422                 # vides inutiles dans le répertoire de destination.
00423                 registerCmd(cmd,p)
00424                 exec(compile(cmd,'<string>','exec'))
00425                 t.setDaemon(True)
00426                 t.start()
00427                 self.oldThreads.add(t)
00428             # on ouvre nautilus pour voir le résultat des copies
00429             buttons=QMessageBox.Ok|QMessageBox.Cancel
00430             defaultButton=QMessageBox.Cancel
00431             if QMessageBox.question(
00432                 None,
00433                 QApplication.translate("Dialog","Voir les copies",None, QApplication.UnicodeUTF8),
00434                 QApplication.translate("Dialog","Voulez-vous voir les fichiers copiés ?",None, QApplication.UnicodeUTF8),
00435                 buttons, defaultButton)==QMessageBox.Ok:
00436                 subprocess.call("nautilus '%s'" %destDir,shell=True)
00437             return True
00438         else:
00439             msgBox=QMessageBox.warning(
00440                 None,
00441                 QApplication.translate("Dialog","Destination manquante",None, QApplication.UnicodeUTF8),
00442                 QApplication.translate("Dialog","Veuillez choisir une destination pour la copie des fichiers",None, QApplication.UnicodeUTF8))
00443             return True
00444 
00445     ##
00446     # 
00447     #         Relance la dernière commande, mais en l'appliquant seulement aux
00448     #         baladeurs nouvellement branchés.
00449     #         
00450     def redoCmd(self):
00451         global lastCommand, pastCommands, activeThreads
00452         if len(activeThreads)>0:
00453             print "GRRRR il faut stopper des threads", self.oldThreads
00454             for thread in self.oldThreads:
00455                 if thread.isAlive():
00456                     try:
00457                         thread._Thread__stop()
00458                         print str(thread.getName()) + ' is terminated'
00459                     except:
00460                         print str(thread.getName()) + ' could not be terminated'
00461         else:
00462             if lastCommand==None:
00463                 return
00464             if QMessageBox.question(
00465                 None,
00466                 QApplication.translate("Dialog","Réitérer la dernière commande",None, QApplication.UnicodeUTF8),
00467                 QApplication.translate("Dialog","La dernière commande était<br>%1<br>Voulez-vous la relancer avec les nouveaux baladeurs ?",None, QApplication.UnicodeUTF8).arg(lastCommand))==QMessageBox.Cancel:
00468                 return
00469             for p in qApp.diskData:
00470                 if p.owner in pastCommands[lastCommand] : continue
00471                 exec(compile(lastCommand,'<string>','exec'))
00472                 t.setDaemon(True)
00473                 t.start()
00474                 self.oldThreads.add(t)
00475                 pastCommands[lastCommand].append(p.owner)
00476         
00477 
00478     ##
00479     # 
00480     #         Affiche le widget d'aide
00481     #         
00482     def help(self):
00483         w=help.helpWindow(self)
00484         w.show()
00485         w.exec_()
00486 
00487     ##
00488     # 
00489     #         Démonte et détache les clés USB affichées
00490     #         
00491     def umount(self):
00492         buttons=QMessageBox.Ok|QMessageBox.Cancel
00493         defaultButton=QMessageBox.Cancel
00494         button=QMessageBox.question (
00495             self,
00496             QApplication.translate("Main","Démontage des baladeurs",None, QApplication.UnicodeUTF8),
00497             QApplication.translate("Main","Êtes-vous sûr de vouloir démonter tous les baladeurs cochés de la liste ?",None, QApplication.UnicodeUTF8),
00498             buttons,defaultButton)
00499         if button!=QMessageBox.Ok:
00500             return
00501         # on parcourt les premières partition FAT
00502         for p in qApp.diskData:
00503             # on trouve leurs disques parents
00504             for d in qApp.diskData.disks.keys():
00505                 if p in qApp.diskData.disks[d] and p.selected:
00506                     # démontage de toutes les partitions du même disque parent
00507                     for partition in qApp.diskData.disks[d]:
00508                         devfile=partition.getProp("device-file-by-id")
00509                         if isinstance(devfile, dbus.Array):
00510                             devfile=devfile[0]
00511                             if partition.isMounted():
00512                                 subprocess.call("udisks --unmount %s" %devfile, shell=True)
00513                     # détachement du disque parent
00514                     devfile_disk=d.getProp("device-file-by-id")
00515                     if isinstance(devfile_disk, dbus.Array):
00516                         devfile_disk=devfile_disk[0]
00517                     subprocess.call("udisks --detach %s" %devfile_disk, shell=True)
00518                     break
00519         self.checkDisks()  # remet à jour le compte de disques
00520         self.operations=[] # remet à zéro la liste des opérations
00521                 
00522 
00523     ##
00524     # 
00525     #         Connecte le modèle de table à la table
00526     #         @param data les données de la table
00527     #         
00528     def connectTableModel(self, data):
00529         self.visibleheader=[]
00530         for h in self.header:
00531             if h in ownedUsbDisk.uDisk._itemNames:
00532                 self.visibleheader.append(self.tr(ownedUsbDisk.uDisk._itemNames[h]))
00533             else:
00534                 self.visibleheader.append(h)
00535         self.tm=usbTableModel(self, self.visibleheader,data,self.checkable)
00536         self.t.setModel(self.tm)
00537         if self.checkable:
00538             self.t.setItemDelegateForColumn(0, CheckBoxDelegate(self))
00539             self.t.setItemDelegateForColumn(1, UsbDiskDelegate(self))
00540             self.t.setItemDelegateForColumn(3, DiskSizeDelegate(self))
00541         else:
00542             self.t.setItemDelegateForColumn(0, UsbDiskDelegate(self))
00543             self.t.setItemDelegateForColumn(2, DiskSizeDelegate(self))
00544         self.proxy.setSourceModel(self.t.model())
00545 
00546         
00547     ##
00548     # 
00549     #         fonction relancée périodiquement pour vérifier s'il y a un changement
00550     #         dans le baladeurs, et signaler dans le tableau les threads en cours.
00551     #         Le tableau est complètement régénéré à chaque fois, ce qui n'est pas
00552     #         toujours souhaitable.
00553     #         À la fin de chaque vérification, un court flash est déclenché sur
00554     #         l'afficheur de nombre de baladeurs connectés et sa valeur est mise à
00555     #         jour.
00556     #         @param force pour forcer une mise à jour du tableau
00557     #         @param noLoop si False, on ne rentrera pas dans une boucle de Qt
00558     #         
00559     def checkDisks(self, force=False, noLoop=False):
00560         if self.checkDisksLock:
00561             # jamais plus d'un appel à la fois pour checkDisks
00562             return
00563         self.checkDisksLock=True
00564         other=ownedUsbDisk.Available(
00565             self.checkable,
00566             access="firstFat",
00567             diskDict=self.listener.connectedVolumes,
00568             noLoop=noLoop)
00569         if force or not self.sameDiskData(qApp.diskData, other):
00570             qApp.diskData=other
00571             connectedCount=int(other)
00572             self.connectTableModel(other)
00573             self.updateButtons()
00574             self.t.resizeColumnsToContents()
00575             self.ui.lcdNumber.display(connectedCount)
00576         self.flashLCD()
00577         # met la table en ordre par la colonne des propriétaires
00578         if self.checkable:
00579             col=1
00580         else:
00581             col=0
00582         self.t.horizontalHeader().setSortIndicator(1, Qt.AscendingOrder);
00583         self.t.setSortingEnabled(True)
00584         self.t.resizeColumnsToContents()
00585         self.checkDisksLock=False
00586 
00587 
00588     ##
00589     # 
00590     #         @return True si les ensembles de uniqueId de one et two sont identiques
00591     #         
00592     def sameDiskData(self, one, two):
00593         return set([p.uniqueId() for p in one]) == set([p.uniqueId() for p in two])
00594 
00595     ##
00596     # 
00597     #         change le style de l'afficheur LCD pendant une fraction de seconde
00598     #         
00599     def flashLCD(self):
00600         self.ui.lcdNumber.setBackgroundRole(QPalette.Highlight)
00601         self.flashTimer.start(250) ## un quart de seconde
00602 
00603     ##
00604     # 
00605     #         remet le style par défaut pour l'afficheur LCD
00606     #         
00607     def normalLCD(self):
00608         self.ui.lcdNumber.setBackgroundRole(QPalette.Window)
00609 
00610 ##
00611 # 
00612 #     Un modèle de table pour des séries de clés USB
00613 #     
00614 class usbTableModel(QAbstractTableModel):
00615 
00616     ##
00617     # 
00618     #         @param parent un QObject
00619     #         @param header les en-têtes de colonnes
00620     #         @param donnees les données
00621     #         @param checkable vrai si la première colonne est composée de boîtes à cocher. Faux par défaut
00622     #         
00623     def __init__(self, parent=None, header=[], donnees=None, checkable=False):
00624         QAbstractTableModel.__init__(self,parent)
00625         self.header=header
00626         self.donnees=donnees
00627         self.checkable=checkable
00628         self.pere=parent
00629         self.connect(self, SIGNAL("pushCmd(QString, QString)"), self.pushCmd)
00630         self.connect(self, SIGNAL("popCmd(QString, QString)"), self.popCmd)
00631 
00632     ##
00633     # 
00634     #         fonction de rappel déclenchée par les threads (au commencement)
00635     #         @param owner le propriétaire du baladeur associé au thread
00636     #         @param cmd la commande shell effectuée sur ce baladeur
00637     #         
00638     def pushCmd(self,owner,cmd):
00639         global activeThreads, pastCommands, lastCommand
00640         owner=u"%s" %owner
00641         owner=owner.encode("utf-8")
00642         if activeThreads.has_key(owner):
00643             activeThreads[owner].append(cmd)
00644         else:
00645             activeThreads[owner]=[cmd]
00646         self.updateOwnerColumn()
00647         self.pere.updateButtons()
00648 
00649     ##
00650     # 
00651     #         fonction de rappel déclenchée par les threads (à la fin)
00652     #         @param owner le propriétaire du baladeur associé au thread
00653     #         @param cmd la commande shell effectuée sur ce baladeur
00654     #         
00655     def popCmd(self,owner, cmd):
00656         global activeThreads, pastCommands, lastCommand
00657         owner=u"%s" %owner
00658         owner=owner.encode("utf-8")
00659         if activeThreads.has_key(owner):
00660             cmd0=activeThreads[owner].pop()
00661             if cmd0 in cmd:
00662                 msg=cmd.replace(cmd0,"")+"\n"
00663                 logFile=open(os.path.expanduser(logFileName),"a")
00664                 logFile.write(msg)
00665                 logFile.close()
00666             else:
00667                 raise Exception, (u"mismatched commands\n%s\n%s" %(cmd,cmd0)).encode("utf-8")
00668             if len(activeThreads[owner])==0:
00669                 activeThreads.pop(owner)
00670         else:
00671             raise Exception, "End of command without a begin."
00672         ## print "dans tableModel, popCmd", activeThreads
00673         self.updateOwnerColumn()
00674         if len(activeThreads)==0 :
00675             self.pere.updateButtons()
00676 
00677     ##
00678     # 
00679     #         force la mise à jour de la colonne des propriétaires
00680     #         
00681     def updateOwnerColumn(self):
00682         if self.checkable:
00683             column=1
00684         else:
00685             column=0
00686         self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.index(0,column), self.index(len(self.donnees)-1, column))
00687         self.pere.t.viewport().update()
00688 
00689     ##
00690     # 
00691     #         @parent un QModelIndex
00692     #         
00693     def rowCount(self, parent):
00694         return len(self.donnees)
00695     
00696     ##
00697     # 
00698     #         @parent un QModelIndex
00699     #         
00700     def columnCount(self, parent): 
00701         return len(self.header) 
00702 
00703     def setData(self, index, value, role):
00704         if index.column()==0 and self.checkable:
00705             self.donnees[index.row()].selected=value
00706             return True
00707         else:
00708             return QAbstractTableModel.setData(self, index, role)
00709 
00710     ##
00711     # 
00712     #         @param index in QModelIndex
00713     #         @return la partition pointée par index
00714     #         
00715     def partition(self, index):
00716         return self.donnees[index.row()][-1]
00717         
00718     def data(self, index, role): 
00719         if not index.isValid(): 
00720             return QVariant()
00721         elif role==Qt.ToolTipRole:
00722             c=index.column()
00723             h=self.pere.header[c]
00724             if c==0 and self.checkable:
00725                 return QApplication.translate("Main","Cocher ou décocher cette case en cliquant.",None, QApplication.UnicodeUTF8)
00726             elif c==1:
00727                 return QApplication.translate("Main","Propriétaire de la clé USB ou du baladeur ;<br><b>Double-clic</b> pour modifier.",None, QApplication.UnicodeUTF8)
00728             elif "device-mount-paths" in h:
00729                 return QApplication.translate("Main","Point de montage de la clé USB ou du baladeur ;<br><b>Double-clic</b> pour voir les fichiers.",None, QApplication.UnicodeUTF8)
00730             elif "device-size" in h:
00731                 return QApplication.translate("Main","Capacité de la clé USB ou du baladeur en kO ;<br><b>Double-clic</b> pour voir la place occupée.",None, QApplication.UnicodeUTF8)
00732             elif "drive-vendor" in h:
00733                 return QApplication.translate("Main","Fabricant de la clé USB ou du baladeur.",None, QApplication.UnicodeUTF8)
00734             elif "drive-model" in h:
00735                 return QApplication.translate("Main","Modèle de la clé USB ou du baladeur.",None, QApplication.UnicodeUTF8)
00736             elif "drive-serial" in h:
00737                 return QApplication.translate("Main","Numéro de série de la clé USB ou du baladeur.",None, QApplication.UnicodeUTF8)
00738             else:
00739                 return ""
00740         elif role != Qt.DisplayRole: 
00741             return QVariant()
00742         if index.row()<len(self.donnees):
00743             return QVariant(self.donnees[index.row()][index.column()])
00744         else:
00745             return QVariant()
00746 
00747     def headerData(self, section, orientation, role):
00748         if orientation == Qt.Horizontal and role == Qt.DisplayRole:
00749             return QVariant(self.header[section])
00750         elif orientation == Qt.Vertical and role == Qt.DisplayRole:
00751             return QVariant(section+1)
00752         return QVariant()
00753 
00754     ##
00755     # Sort table by given column number.
00756     #         @param Ncol numéro de la colonne de tri
00757     #         @param order l'odre de tri, Qt.DescendingOrder par défaut
00758     #         
00759     def sort(self, Ncol, order=Qt.DescendingOrder):
00760         self.emit(SIGNAL("layoutAboutToBeChanged()"))
00761         self.donnees = sorted(self.donnees, key=operator.itemgetter(Ncol))        
00762         if order == Qt.DescendingOrder:
00763             self.donnees.reverse()
00764         self.emit(SIGNAL("layoutChanged()"))
00765 
00766 def CheckBoxRect(view_item_style_options):
00767   check_box_style_option=QStyleOptionButton()
00768   check_box_rect = QApplication.style().subElementRect(QStyle.SE_CheckBoxIndicator,check_box_style_option)
00769   check_box_point=QPoint(view_item_style_options.rect.x() + view_item_style_options.rect.width() / 2 - check_box_rect.width() / 2, view_item_style_options.rect.y() + view_item_style_options.rect.height() / 2 - check_box_rect.height() / 2)
00770   return QRect(check_box_point, check_box_rect.size())
00771 
00772 class CheckBoxDelegate(QStyledItemDelegate):
00773     def __init__(self, parent):
00774         QStyledItemDelegate.__init__(self,parent)
00775 
00776     def paint(self, painter, option, index):
00777         checked = index.model().data(index, Qt.DisplayRole).toBool()
00778         check_box_style_option=QStyleOptionButton()
00779         check_box_style_option.state |= QStyle.State_Enabled
00780         if checked:
00781             check_box_style_option.state |= QStyle.State_On
00782         else:
00783             check_box_style_option.state |= QStyle.State_Off
00784         check_box_style_option.rect = CheckBoxRect(option);
00785         QApplication.style().drawControl(QStyle.CE_CheckBox, check_box_style_option, painter)
00786 
00787     def editorEvent(self, event, model, option, index):
00788         if ((event.type() == QEvent.MouseButtonRelease) or (event.type() == QEvent.MouseButtonDblClick)):
00789             if (event.button() != Qt.LeftButton or not CheckBoxRect(option).contains(event.pos())):
00790                 return False
00791             if (event.type() == QEvent.MouseButtonDblClick):
00792                 return True
00793         elif (event.type() == QEvent.KeyPress):
00794             if event.key() != Qt.Key_Space and event.key() != Qt.Key_Select:
00795                 return False
00796         else:
00797             return False
00798         checked = index.model().data(index, Qt.DisplayRole).toBool()
00799         result = model.setData(index, not checked, Qt.EditRole)
00800         return result
00801 
00802         
00803 ##
00804 # 
00805 #     Classe pour identifier le baladeur dans le tableau.
00806 #     La routine de rendu à l'écran trace une petite icône et le nom du
00807 #     propriétaire à côté.
00808 #     
00809 class UsbDiskDelegate(QStyledItemDelegate):
00810     def __init__(self, parent):
00811         QStyledItemDelegate.__init__(self,parent)
00812         self.okPixmap=QPixmap("/usr/share/icons/Tango/16x16/status/weather-clear.png")
00813         self.busyPixmap=QPixmap("/usr/share/icons/Tango/16x16/actions/view-refresh.png")
00814 
00815     def paint(self, painter, option, index):
00816         global activeThreads
00817         text = index.model().data(index, Qt.DisplayRole).toString()
00818         rect0=QRect(option.rect)
00819         rect1=QRect(option.rect)
00820         h=rect0.height()
00821         w=rect0.width()
00822         rect0.setSize(QSize(h,h))
00823         rect1.translate(h,0)
00824         rect1.setSize(QSize(w-h,h))
00825         QApplication.style().drawItemText (painter, rect1, Qt.AlignLeft+Qt.AlignVCenter, option.palette, True, text)
00826         QApplication.style().drawItemText (painter, rect0, Qt.AlignCenter, option.palette, True, QString("O"))
00827         text=(u"%s" %text).encode("utf-8")
00828         if activeThreads.has_key(text):
00829             QApplication.style().drawItemPixmap (painter, rect0, Qt.AlignCenter, self.busyPixmap)
00830         else:
00831             QApplication.style().drawItemPixmap (painter, rect0, Qt.AlignCenter, self.okPixmap)
00832         
00833 ##
00834 # 
00835 #     Classe pour figurer la taille de la mémoire du baladeur. Trace un petit
00836 #     secteur représentant la place occupée, puis affiche la place avec l'unité
00837 #     le plus parropriée.
00838 #     
00839 class DiskSizeDelegate(QStyledItemDelegate):
00840     def __init__(self, parent):
00841         QStyledItemDelegate.__init__(self,parent)
00842         
00843 
00844     def paint(self, painter, option, index):
00845         value = int(index.model().data(index, Qt.DisplayRole).toString())
00846         text = self.val2txt(value)
00847         rect0=QRect(option.rect)
00848         rect1=QRect(option.rect)
00849         rect0.translate(2,(rect0.height()-16)/2)
00850         rect0.setSize(QSize(16,16))
00851         rect1.translate(20,0)
00852         rect1.setWidth(rect1.width()-20)
00853         QApplication.style().drawItemText (painter, rect1, Qt.AlignLeft+Qt.AlignVCenter, option.palette, True, text)
00854         # dessin d'un petit cercle pour l'occupation
00855         mount=index.model().partition(index).mountPoint()
00856         dev,total,used,remain,pcent,path = self.parent().diskSizeData(mount)
00857         pcent=int(pcent[:-1])
00858         painter.setBrush(QBrush(QColor("slateblue")))
00859         painter.drawPie(rect0,0,16*360*pcent/100)
00860 
00861     ##
00862     # 
00863     #         @return a string with a value with unit K, M, or G
00864     #         
00865     def val2txt(self, val):
00866         suffixes=["B", "KB", "MB", "GB", "TB"]
00867         val*=1.0 # calcul flottant
00868         i=0
00869         while val > 1024 and i < len(suffixes):
00870             i+=1
00871             val/=1024
00872         return "%4.1f %s" %(val, suffixes[i])
00873     
00874 
 Tout Classes Espaces de nommage Fichiers Fonctions Variables