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