ScolaSync  1.0
 Tout Classes Espaces de nommage Fichiers Fonctions Variables Pages
mainWindow.py
Aller à la documentation de ce fichier.
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3 # $Id: mainWindow.py 47 2011-06-13 10:20:14Z georgesk $
4 
5 licence={}
6 licence['en']="""
7  file mainWindow.py
8  this file is part of the project scolasync
9 
10  Copyright (C) 2010 Georges Khaznadar <georgesk@ofset.org>
11 
12  This program is free software: you can redistribute it and/or modify
13  it under the terms of the GNU General Public License as published by
14  the Free Software Foundation, either version3 of the License, or
15  (at your option) any later version.
16 
17  This program is distributed in the hope that it will be useful,
18  but WITHOUT ANY WARRANTY; without even the implied warranty of
19  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20  GNU General Public License for more details.
21 
22  You should have received a copy of the GNU General Public License
23  along with this program. If not, see <http://www.gnu.org/licenses/>.
24 """
25 
26 from PyQt4.QtCore import *
27 from PyQt4.QtGui import *
28 import ownedUsbDisk, help, copyToDialog1, chooseInSticks, usbThread
29 import diskFull, preferences, checkBoxDialog
30 import os.path, operator, subprocess, dbus, re, time, copy
31 from notification import Notification
32 import db
33 import deviceListener
34 import choixEleves
35 import nameAdrive
36 from globaldef import logFileName, _dir
37 
38 # cette donnée est globale, pour être utilisé depuis n'importe quel objet
39 qApp.diskData=ownedUsbDisk.Available(True,access="firstFat")
40 
41 activeThreads={} # donnée globale : les threads actifs
42 # cette donnée est mise à jour par des signaux émis au niveau des threads
43 # et elle est utilisée par la routine de traçage des cases du tableau
44 pastCommands={} # donnée globale : les commandes réalisées dans le passé
45 lastCommand=None # donnée globale : la toute dernière commande
46 
47 ##
48 #
49 # enregistre la commande cmd pour la partition donnée
50 # @param cmd une commande pour créer un thread t
51 # @param partition une partition
52 #
53 def registerCmd(cmd,partition):
54  global pastCommands, lastCommand
55  if pastCommands.has_key(cmd):
56  pastCommands[cmd].append(partition.owner)
57  else:
58  pastCommands[cmd]=[partition.owner]
59  lastCommand=cmd
60 
62  ##
63  #
64  # Le constructeur
65  # @param parent un QWidget
66  # @param opts une liste d'options extraite à l'aide de getopts
67  # @param locale la langue de l'application
68  #
69  def __init__(self, parent, opts, locale="fr_FR"):
70  QMainWindow.__init__(self)
71  QWidget.__init__(self, parent)
72  self.locale=locale
73  from Ui_mainWindow import Ui_MainWindow
74  self.ui = Ui_MainWindow()
75  self.ui.setupUi(self)
76  # crée le dialogue des nouveaux noms
77  self.namesFullIcon=QIcon("/usr/share/icons/Tango/scalable/actions/gtk-find-and-replace.svg")
78  self.namesEmptyIcon=QIcon("/usr/share/icons/Tango/scalable/actions/gtk-find.svg")
79  self.namesFullTip=QApplication.translate("MainWindow", "<br />Des noms sont disponibles pour renommer les prochains baladeurs que vous brancherez", None, QApplication.UnicodeUTF8)
80  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)
82  self.recentConnect="" # chemin dbus pour un baladeur récemment connecté
83  # initialise deux icônes
84  self.initRedoStuff()
85  # initialise le tableau
86  self.t=self.ui.tableView
87  self.proxy=QSortFilterProxyModel()
88  self.proxy.setSourceModel(self.t.model())
89  self.opts=opts
90  self.timer=QTimer()
91  self.applyPreferences()
93  self.updateButtons()
94  self.availableNames=False # cette variable est regénérée ci-dessous
95  self.setAvailableNames(False)
96  self.operations=[] # liste des opérations précédemment "réussies"
97  self.oldThreads=set() # threads lancés éventuellement encore vivants
98  self.flashTimer=QTimer()
99  self.flashTimer.setSingleShot(True)
100  self.checkDisksLock=False # autorise self.checkDisks
101  QObject.connect(self.ui.forceCheckButton, SIGNAL("clicked()"), self.checkDisks)
102  QObject.connect(self.timer, SIGNAL("timeout()"), self.checkDisks)
103  QObject.connect(self.flashTimer, SIGNAL("timeout()"), self.normalLCD);
104  QObject.connect(self.ui.helpButton, SIGNAL("clicked()"), self.help)
105  QObject.connect(self.ui.umountButton, SIGNAL("clicked()"), self.umount)
106  QObject.connect(self.ui.toButton, SIGNAL("clicked()"), self.copyTo)
107  QObject.connect(self.ui.fromButton, SIGNAL("clicked()"), self.copyFrom)
108  QObject.connect(self.ui.delButton, SIGNAL("clicked()"), self.delFiles)
109  QObject.connect(self.ui.redoButton, SIGNAL("clicked()"), self.redoCmd)
110  QObject.connect(self.ui.namesButton, SIGNAL("clicked()"), self.namesCmd)
111  QObject.connect(self.ui.preferenceButton, SIGNAL("clicked()"), self.preference)
112  QObject.connect(self.ui.tableView, SIGNAL("doubleClicked(const QModelIndex&)"), self.tableClicked)
113  QObject.connect(self,SIGNAL("deviceAdded(QString)"), self.deviceAdded)
114  QObject.connect(self,SIGNAL("deviceRemoved(QString)"), self.deviceRemoved)
115  QObject.connect(self,SIGNAL("checkAll()"), self.checkAll)
116  QObject.connect(self,SIGNAL("checkToggle()"), self.checkToggle)
117  QObject.connect(self,SIGNAL("checkNone()"), self.checkNone)
118  QObject.connect(self,SIGNAL("shouldNameDrive()"), self.namingADrive)
119 
120  ##
121  #
122  # @param boolfunc une fonction pour décider du futur état de la coche
123  # étant donné l'état antérieur
124  # Modifie les coches des baladeurs
125  #
126  def checkModify(self, boolFunc):
127  model=self.tm
128  index0=model.createIndex(0,0)
129  index1=model.createIndex(len(model.donnees)-1,0)
130  srange=QItemSelectionRange(index0,index1)
131  for i in srange.indexes():
132  checked=i.model().data(i,Qt.DisplayRole).toBool()
133  model.setData(i, boolFunc(checked),Qt.EditRole)
134 
135  ##
136  #
137  # Coche tous les baladeurs
138  #
139  def checkAll(self):
140  self.checkModify(lambda x: True)
141 
142  ##
143  #
144  # Inverse la coche des baladeurs
145  #
146  def checkToggle(self):
147  self.checkModify(lambda x: not x)
148 
149  ##
150  #
151  # Décoche tous les baladeurs
152  #
153  def checkNone(self):
154  self.checkModify(lambda x: False)
155 
156  ##
157  #
158  # Gère un dialogue pour renommer un baladeur désigné par
159  # self.recentConnect
160  #
161  def namingADrive(self):
162  if self.availableNames:
163  stickId, tattoo, uuid = self.listener.identify(self.recentConnect)
164  hint=db.readStudent(stickId, uuid, tattoo)
165  if hint != None:
166  oldName=hint
167  else:
168  oldName=""
169  d=nameAdrive.nameAdriveDialog(self, oldName=oldName,
170  nameList=self.namesDialog.itemStrings(),
171  driveIdent=(stickId, uuid, tattoo))
172  d.show()
173  result=d.exec_()
174  return
175 
176  ##
177  #
178  # fonction de rappel pour un medium ajouté
179  # @param s chemin UDisks, exemple : /org/freedesktop/UDisks/devices/sdb3
180  #
181  def deviceAdded(self, s):
182  vfatPath = self.listener.vfatUsbPath(str(s))
183  if vfatPath:
184  self.recentConnect=str(s)
185  # pas tout à fait équivalent à l'émission d'un signal avec emit :
186  # le timer s'exécutera en dehors du thread qui appartient à DBUS !
187  QTimer.singleShot(0, self.namingADrive)
188  self.checkDisks(noLoop=True)
189 
190  ##
191  #
192  # fonction de rappel pour un medium retiré
193  # @param s une chaine de caractères du type /dev/sdxy
194  #
195  def deviceRemoved(self, s):
196  ## print "dans deviceRemoved", s
197  if qApp.diskData.hasDev(s):
198  self.checkDisks()
199 
200  ##
201  #
202  # Initialise des données pour le bouton central (refaire/stopper)
203  #
204  def initRedoStuff(self):
205  # réserve les icônes
206  self.iconRedo = QIcon()
207  self.iconRedo.addPixmap(QPixmap("/usr/share/icons/Tango/scalable/actions/go-jump.svg"), QIcon.Normal, QIcon.Off)
208  self.iconStop = QIcon()
209  self.iconStop.addPixmap(QPixmap("/usr/share/icons/Tango/scalable/actions/stop.svg"), QIcon.Normal, QIcon.Off)
210  # réserve les phrases d'aide
211  self.redoToolTip=QApplication.translate("MainWindow", "Refaire à nouveau", None, QApplication.UnicodeUTF8)
212  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)
213  self.stopToolTip=QApplication.translate("MainWindow", "Arrêter les opérations en cours", None, QApplication.UnicodeUTF8)
214  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)
215 
216  ##
217  #
218  # modification du comportement du widget original, pour
219  # démarrer le timer et les vérifications de baladeurs
220  # après construction de la fenêtre seulement
221  #
222  def showEvent (self, ev):
223  result=QMainWindow.showEvent(self, ev)
224  self.setTimer()
225  self.checkDisks(force=True) # met à jour le compte de disques affiché
226  return result
227 
228  ##
229  #
230  # sets the main timer
231  #
232  def setTimer(self, enabled=True):
233  if self.refreshEnabled:
234  self.timer.start(self.refreshDelay*1000)
235  else:
236  self.timer.stop()
237 
238  ##
239  #
240  # Applique les préférences et les options de ligne de commande
241  #
242  def applyPreferences(self):
243  prefs=db.readPrefs()
244  self.schoolFile=prefs["schoolFile"]
245  self.workdir=prefs["workdir"]
246  self.refreshEnabled=prefs["refreshEnabled"]
247  self.refreshDelay=prefs["refreshDelay"]
248  self.setTimer()
249  self.manFileLocation=prefs["manfile"]
250  # on active les cases à cocher si ça a été réclamé par les options
251  # ou par les préférences
252  self.checkable=("--check","") in self.opts or ("-c","") in self.opts or prefs["checkable"]
253  self.mv=prefs["mv"]
254  other=ownedUsbDisk.Available(self.checkable,access="firstFat")
255  qApp.diskData=other
256  self.header=ownedUsbDisk.uDisk.headers(self.checkable)
257  self.connectTableModel(other)
258 
259  ##
260  #
261  # change le répertoire par défaut contenant les fichiers de travail
262  # @param newDir le nouveau nom de répertoire
263  #
264  def changeWd(self, newDir):
265  self.workdir=newDir
266  db.setWd(newDir)
267 
268  ##
269  #
270  # fonction de rappel pour un double clic sur un élément de la table
271  # @param idx un QModelIndex
272  #
273  def tableClicked(self, idx):
274  c=idx.column()
275  mappedIdx=self.proxy.mapFromSource(idx)
276  r=mappedIdx.row()
277  ## print "row=%d mapped row=%d" %(idx.row(), r)
278  h=self.header[c]
279  if c==0 and self.checkable:
280  self.manageCheckBoxes()
281  pass
282  elif c==1 or (c==0 and not self.checkable):
283  # case du propriétaire
284  self.editOwner(mappedIdx)
285  elif "device-mount-paths" in h:
286  cmd=u"nautilus '%s'" %idx.data().toString ()
287  subprocess.call(cmd, shell=True)
288  elif "device-size" in h:
289  mount=idx.model().partition(idx).mountPoint()
290  dev,total,used,remain,pcent,path = self.diskSizeData(mount)
291  pcent=int(pcent[:-1])
292  w=diskFull.mainWindow(self,pcent,title=path, total=total, used=used)
293  w.show()
294  else:
295  QMessageBox.warning(None,
296  QApplication.translate("Dialog","Double-clic non pris en compte",None, QApplication.UnicodeUTF8),
297  QApplication.translate("Dialog","pas d'action pour l'attribut %1",None, QApplication.UnicodeUTF8).arg(h))
298 
299  ##
300  #
301  # ouvre un dialogue pour permettre de gérer les cases à cocher globalement
302  #
303  def manageCheckBoxes(self):
304  cbDialog=checkBoxDialog.CheckBoxDialog(self)
305  cbDialog.exec_()
306 
307  ##
308  #
309  # @param rowOrDev a row number in the tableView, or a device string
310  # @return a tuple dev,total,used,remain,pcent,path for the
311  # disk in the given row of the tableView
312  # (the tuple comes from the command df)
313  #
314  def diskSizeData(self, rowOrDev):
315  if type(rowOrDev)==type(0):
316  path=qApp.diskData[rowOrDev][self.header.index("1device-mount-paths")]
317  else:
318  path=rowOrDev
319  cmd =u"df '%s'" %path
320  dfOutput=subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate()[0].split("\n")[-2]
321  m = re.match("(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+).*", dfOutput).groups()
322  return m
323 
324 
325  ##
326  #
327  # trouve le disque qui correspond à un propriétaire
328  # @param student le propriétaire du disque
329  # @return le disque correspondant à l'étudiant
330  #
331  def diskFromOwner(self,student):
332  found=False
333  for d in qApp.diskData.disks.keys():
334  if d.owner==student:
335  found=True
336  break
337  # si on ne trouve pas avec le nom, on essaie de trouver
338  # un disque encore inconnu, le premier venu
339  if d.owner==None or len(d.owner)==0:
340  found=True
341  break
342  if found:
343  return d
344  else:
345  return None
346 
347  ##
348  #
349  # Édition du propriétaire d'une clé.
350  # @param idx un QModelIndex qui pointe sur le propriétaire d'une clé
351  #
352  def editOwner(self, idx):
353  student=u"%s" %self.tm.data(idx,Qt.DisplayRole).toString()
354  ownedUsbDisk.editRecord(self.diskFromOwner(student), hint=student)
355  other=ownedUsbDisk.Available(self.checkable,access="firstFat")
356  qApp.diskData=other
357  self.connectTableModel(other)
358  self.checkDisks()
359 
360  ##
361  #
362  # Met à jour l'icône qui reflète la disponibilité de noms pour
363  # renommer automatiquement des baladeurs
364  # @param available vrai s'il y a des noms disponibles pour
365  # renommer des baladeurs.
366  #
367  def setAvailableNames(self, available):
368  self.availableNames=available
369  if available:
370  icon=self.namesFullIcon
371  msg=self.namesFullTip
372  else:
373  icon=self.namesEmptyIcon
374  msg=self.namesEmptyTip
375  self.ui.namesButton.setIcon(icon)
376  self.ui.namesButton.setToolTip(msg)
377  self.ui.namesButton.setStatusTip(msg.remove("<br />"))
378 
379  ##
380  #
381  # Désactive ou active les flèches selon que l'option correspondante
382  # est possible ou non. Pour les flèches : ça aurait du sens de préparer
383  # une opération de copie avant même de brancher des clés, donc on les
384  # active. Par contre démonter les clés quand elles sont absentes ça
385  # n'a pas d'utilité.
386  # Change l'icône du dialogue des noms selon qu'il reste ou non des
387  # noms disponibles dans le dialogue des noms.
388  #
389  def updateButtons(self):
390  global activeThreads, lastCommand
391  active = len(qApp.diskData)>0
392  for button in (self.ui.toButton,
393  self.ui.fromButton,
394  self.ui.delButton,
395  self.ui.umountButton):
396  button.setEnabled(active)
397  # l'état du redoButton dépend de plusieurs facteurs
398  # si un thread au moins est en cours, on y affiche un STOP actif
399  # sinon on y met l'icône de lastCommand, et celle-ci sera active
400  # seulement s'il y a une commande déjà validée
401  if len(activeThreads) > 0:
402  self.ui.redoButton.setIcon(self.iconStop)
403  self.ui.redoButton.setToolTip(self.stopToolTip)
404  self.ui.redoButton.setStatusTip(self.stopStatusTip)
405  self.ui.redoButton.setEnabled(True)
406  else:
407  self.oldThreads=set() # vide l'ensemble puisque tout est fini
408  self.ui.redoButton.setIcon(self.iconRedo)
409  self.ui.redoButton.setToolTip(self.redoToolTip)
410  self.ui.redoButton.setStatusTip(self.redoStatusTip)
411  self.ui.redoButton.setEnabled(lastCommand!=None)
412  l=self.namesDialog.ui.listWidget.findItems("*",Qt.MatchWildcard)
413  if len(l)>0:
414  self.ui.namesButton.setIcon(self.namesFullIcon)
415  else:
416  self.ui.namesButton.setIcon(self.namesEmptyIcon)
417 
418  ##
419  #
420  # lance le dialogue des préférences
421  #
422  def preference(self):
424  pref.setValues(db.readPrefs())
425  pref.show()
426  pref.exec_()
427  if pref.result()==QDialog.Accepted:
428  db.writePrefs(pref.values())
429  # on applique les préférences tout de suite sans redémarrer
430  self.applyPreferences()
431 
432  ##
433  #
434  # Lance l'action de supprimer des fichiers ou des répertoires dans les clés USB
435  #
436  def delFiles(self):
437  titre1=QApplication.translate("Dialog","Choix de fichiers à supprimer",None, QApplication.UnicodeUTF8)
438  titre2=QApplication.translate("Dialog","Choix de fichiers à supprimer (jokers autorisés)",None, QApplication.UnicodeUTF8)
439  d=chooseInSticks.chooseDialog(self, titre1, titre2)
440  ok = d.exec_()
441  if ok:
442  pathList=map(lambda x: u"%s" %x, d.pathList())
443  buttons=QMessageBox.Ok|QMessageBox.Cancel
444  defaultButton=QMessageBox.Cancel
445  reply=QMessageBox.warning(
446  None,
447  QApplication.translate("Dialog","Vous allez effacer plusieurs baladeurs",None, QApplication.UnicodeUTF8),
448  QApplication.translate("Dialog","Etes-vous certain de vouloir effacer : "+"\n".join(pathList),None, QApplication.UnicodeUTF8),
449  buttons, defaultButton)
450  if reply == QMessageBox.Ok:
451  cmd='t=usbThread.threadDeleteInUSB(p,%s,subdir="Travail", logfile="%s", parent=self.tm)' %(pathList,logFileName)
452  for p in qApp.diskData:
453  if not p.selected: continue # pas les médias désélectionnés
454  registerCmd(cmd,p)
455  exec(compile(cmd,'<string>','exec'))
456  t.setDaemon(True)
457  t.start()
458  self.oldThreads.add(t)
459  return True
460  else:
461  msgBox=QMessageBox.warning(
462  None,
463  QApplication.translate("Dialog","Aucun fichier sélectionné",None, QApplication.UnicodeUTF8),
464  QApplication.translate("Dialog","Veuillez choisir au moins un fichier",None, QApplication.UnicodeUTF8))
465  return True
466 
467  ##
468  #
469  # Lance l'action de copier vers les clés USB
470  #
471  def copyTo(self):
472  d=copyToDialog1.copyToDialog1(parent=self, workdir=self.workdir)
473  d.exec_()
474  if d.ok==True:
475  cmd='t=usbThread.threadCopyToUSB(p,%s,subdir="%s", logfile="%s", parent=self.tm)' %(d.selectedList(), self.workdir, logFileName)
476  for p in qApp.diskData:
477  if not p.selected: continue # pas les médias désélectionnés
478  registerCmd(cmd,p)
479  exec(compile(cmd,'<string>','exec'))
480  t.setDaemon(True)
481  t.start()
482  self.oldThreads.add(t)
483  return True
484  else:
485  msgBox=QMessageBox.warning(
486  None,
487  QApplication.translate("Dialog","Aucun fichier sélectionné",None, QApplication.UnicodeUTF8),
488  QApplication.translate("Dialog","Veuillez choisir au moins un fichier",None, QApplication.UnicodeUTF8))
489  return True
490 
491  ##
492  #
493  # Lance l'action de copier depuis les clés USB
494  #
495  def copyFrom(self):
496  titre1=QApplication.translate("Dialog","Choix de fichiers à copier",None, QApplication.UnicodeUTF8)
497  titre2=QApplication.translate("Dialog", "Choix de fichiers à copier depuis les baladeurs", None, QApplication.UnicodeUTF8)
498  ok=QApplication.translate("Dialog", "Choix de la destination ...", None, QApplication.UnicodeUTF8)
499  d=chooseInSticks.chooseDialog(self, title1=titre1, title2=titre2, ok=ok)
500  ok = d.exec_()
501  if not ok or len(d.pathList())==0 :
502  msgBox=QMessageBox.warning(None,
503  QApplication.translate("Dialog","Aucun fichier sélectionné",None, QApplication.UnicodeUTF8),
504  QApplication.translate("Dialog","Veuillez choisir au moins un fichier",None, QApplication.UnicodeUTF8))
505  return True
506  # bon, alors c'est OK pour le choix des fichiers à envoyer
507  pathList=map(lambda x: u"%s" %x, d.pathList())
508  mp=d.selectedDiskMountPoint()
509  initialPath=os.path.expanduser("~")
510  destDir = QFileDialog.getExistingDirectory(
511  None,
512  QApplication.translate("Dialog","Choisir un répertoire de destination",None, QApplication.UnicodeUTF8),
513  initialPath)
514  if destDir and len(destDir)>0 :
515  if self.mv:
516  cmd=u"""t=usbThread.threadMoveFromUSB(
517  p,%s,subdir=self.workdir,
518  rootPath="%s", dest="%s", logfile="%s",
519  parent=self.tm)""" %(pathList, mp, destDir, logFileName)
520  else:
521  cmd=u"""t=usbThread.threadCopyFromUSB(
522  p,%s,subdir=self.workdir,
523  rootPath="%s", dest="%s", logfile="%s",
524  parent=self.tm)""" %(pathList, mp, destDir, logFileName)
525 
526  for p in qApp.diskData:
527  if not p.selected: continue # pas les médias désélectionnés
528  # on devrait vérifier s'il y a des données à copier
529  # et s'il n'y en a pas, ajouter des lignes au journal
530  # mais on va laisser faire ça dans le thread
531  # inconvénient : ça crée quelquefois des sous-répertoires
532  # vides inutiles dans le répertoire de destination.
533  registerCmd(cmd,p)
534  exec(compile(cmd,'<string>','exec'))
535  t.setDaemon(True)
536  t.start()
537  self.oldThreads.add(t)
538  # on ouvre nautilus pour voir le résultat des copies
539  buttons=QMessageBox.Ok|QMessageBox.Cancel
540  defaultButton=QMessageBox.Cancel
541  if QMessageBox.question(
542  None,
543  QApplication.translate("Dialog","Voir les copies",None, QApplication.UnicodeUTF8),
544  QApplication.translate("Dialog","Voulez-vous voir les fichiers copiés ?",None, QApplication.UnicodeUTF8),
545  buttons, defaultButton)==QMessageBox.Ok:
546  subprocess.call("nautilus '%s'" %destDir,shell=True)
547  return True
548  else:
549  msgBox=QMessageBox.warning(
550  None,
551  QApplication.translate("Dialog","Destination manquante",None, QApplication.UnicodeUTF8),
552  QApplication.translate("Dialog","Veuillez choisir une destination pour la copie des fichiers",None, QApplication.UnicodeUTF8))
553  return True
554 
555  ##
556  #
557  # Relance la dernière commande, mais en l'appliquant seulement aux
558  # baladeurs nouvellement branchés.
559  #
560  def redoCmd(self):
561  global lastCommand, pastCommands, activeThreads
562  if len(activeThreads)>0:
563  for thread in self.oldThreads:
564  if thread.isAlive():
565  try:
566  thread._Thread__stop()
567  print str(thread.getName()) + ' is terminated'
568  except:
569  print str(thread.getName()) + ' could not be terminated'
570  else:
571  if lastCommand==None:
572  return
573  if QMessageBox.question(
574  None,
575  QApplication.translate("Dialog","Réitérer la dernière commande",None, QApplication.UnicodeUTF8),
576  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:
577  return
578  for p in qApp.diskData:
579  if p.owner in pastCommands[lastCommand] : continue
580  exec(compile(lastCommand,'<string>','exec'))
581  t.setDaemon(True)
582  t.start()
583  self.oldThreads.add(t)
584  pastCommands[lastCommand].append(p.owner)
585 
586  ##
587  #
588  # montre le dialogue de choix de nouveaux noms à partir d'un
589  # fichier administratif.
590  #
591  def namesCmd(self):
592  self.namesDialog.show()
593 
594  ##
595  #
596  # Affiche le widget d'aide
597  #
598  def help(self):
599  w=help.helpWindow(self)
600  w.show()
601  w.exec_()
602 
603  ##
604  #
605  # Démonte et détache les clés USB affichées
606  #
607  def umount(self):
608  buttons=QMessageBox.Ok|QMessageBox.Cancel
609  defaultButton=QMessageBox.Cancel
610  button=QMessageBox.question (
611  self,
612  QApplication.translate("Main","Démontage des baladeurs",None, QApplication.UnicodeUTF8),
613  QApplication.translate("Main","Êtes-vous sûr de vouloir démonter tous les baladeurs cochés de la liste ?",None, QApplication.UnicodeUTF8),
614  buttons,defaultButton)
615  if button!=QMessageBox.Ok:
616  return
617  # on parcourt les premières partition FAT
618  for p in qApp.diskData:
619  # on trouve leurs disques parents
620  for d in qApp.diskData.disks.keys():
621  if p in qApp.diskData.disks[d] and p.selected:
622  # démontage de toutes les partitions du même disque parent
623  for partition in qApp.diskData.disks[d]:
624  devfile=partition.getProp("device-file-by-id")
625  if isinstance(devfile, dbus.Array):
626  devfile=devfile[0]
627  if partition.isMounted():
628  subprocess.call("udisks --unmount %s" %devfile, shell=True)
629  # détachement du disque parent
630  devfile_disk=d.getProp("device-file-by-id")
631  if isinstance(devfile_disk, dbus.Array):
632  devfile_disk=devfile_disk[0]
633  subprocess.call("udisks --detach %s" %devfile_disk, shell=True)
634  break
635  self.checkDisks() # remet à jour le compte de disques
636  self.operations=[] # remet à zéro la liste des opérations
637 
638 
639  ##
640  #
641  # Connecte le modèle de table à la table
642  # @param data les données de la table
643  #
644  def connectTableModel(self, data):
646  for h in self.header:
647  if h in ownedUsbDisk.uDisk._itemNames:
648  self.visibleheader.append(self.tr(ownedUsbDisk.uDisk._itemNames[h]))
649  else:
650  self.visibleheader.append(h)
651  self.tm=usbTableModel(self, self.visibleheader,data,self.checkable)
652  self.t.setModel(self.tm)
653  if self.checkable:
654  self.t.setItemDelegateForColumn(0, CheckBoxDelegate(self))
655  self.t.setItemDelegateForColumn(1, UsbDiskDelegate(self))
656  self.t.setItemDelegateForColumn(3, DiskSizeDelegate(self))
657  else:
658  self.t.setItemDelegateForColumn(0, UsbDiskDelegate(self))
659  self.t.setItemDelegateForColumn(2, DiskSizeDelegate(self))
660  self.proxy.setSourceModel(self.t.model())
661 
662 
663  ##
664  #
665  # fonction relancée périodiquement pour vérifier s'il y a un changement
666  # dans le baladeurs, et signaler dans le tableau les threads en cours.
667  # Le tableau est complètement régénéré à chaque fois, ce qui n'est pas
668  # toujours souhaitable.
669  # À la fin de chaque vérification, un court flash est déclenché sur
670  # l'afficheur de nombre de baladeurs connectés et sa valeur est mise à
671  # jour.
672  # @param force pour forcer une mise à jour du tableau
673  # @param noLoop si False, on ne rentrera pas dans une boucle de Qt
674  #
675  def checkDisks(self, force=False, noLoop=True):
676  if self.checkDisksLock:
677  # jamais plus d'un appel à la fois pour checkDisks
678  return
679  self.checkDisksLock=True
681  self.checkable,
682  access="firstFat",
683  diskDict=self.listener.connectedVolumes,
684  noLoop=noLoop)
685  if force or not self.sameDiskData(qApp.diskData, other):
686  qApp.diskData=other
687  connectedCount=int(other)
688  self.connectTableModel(other)
689  self.updateButtons()
690  self.t.resizeColumnsToContents()
691  self.ui.lcdNumber.display(connectedCount)
692  self.flashLCD()
693  # met la table en ordre par la colonne des propriétaires
694  if self.checkable:
695  col=1
696  else:
697  col=0
698  self.t.horizontalHeader().setSortIndicator(1, Qt.AscendingOrder);
699  self.t.setSortingEnabled(True)
700  self.t.resizeColumnsToContents()
701  self.checkDisksLock=False
702 
703 
704  ##
705  #
706  # @return True si les ensembles de uniqueId de one et two sont identiques
707  #
708  def sameDiskData(self, one, two):
709  return set([p.uniqueId() for p in one]) == set([p.uniqueId() for p in two])
710 
711  ##
712  #
713  # change le style de l'afficheur LCD pendant une fraction de seconde
714  #
715  def flashLCD(self):
716  self.ui.lcdNumber.setBackgroundRole(QPalette.Highlight)
717  self.flashTimer.start(250) ## un quart de seconde
718 
719  ##
720  #
721  # remet le style par défaut pour l'afficheur LCD
722  #
723  def normalLCD(self):
724  self.ui.lcdNumber.setBackgroundRole(QPalette.Window)
725 
726 ##
727 #
728 # Un modèle de table pour des séries de clés USB
729 #
731 
732  ##
733  #
734  # @param parent un QObject
735  # @param header les en-têtes de colonnes
736  # @param donnees les données
737  # @param checkable vrai si la première colonne est composée de boîtes à cocher. Faux par défaut
738  #
739  def __init__(self, parent=None, header=[], donnees=None, checkable=False):
740  QAbstractTableModel.__init__(self,parent)
741  self.header=header
742  self.donnees=donnees
743  self.checkable=checkable
744  self.pere=parent
745  self.connect(self, SIGNAL("pushCmd(QString, QString)"), self.pushCmd)
746  self.connect(self, SIGNAL("popCmd(QString, QString)"), self.popCmd)
747 
748  ##
749  #
750  # fonction de rappel déclenchée par les threads (au commencement)
751  # @param owner le propriétaire du baladeur associé au thread
752  # @param cmd la commande shell effectuée sur ce baladeur
753  #
754  def pushCmd(self,owner,cmd):
755  global activeThreads, pastCommands, lastCommand
756  owner=u"%s" %owner
757  owner=owner.encode("utf-8")
758  if activeThreads.has_key(owner):
759  activeThreads[owner].append(cmd)
760  else:
761  activeThreads[owner]=[cmd]
762  self.updateOwnerColumn()
763  self.pere.updateButtons()
764 
765  ##
766  #
767  # fonction de rappel déclenchée par les threads (à la fin)
768  # @param owner le propriétaire du baladeur associé au thread
769  # @param cmd la commande shell effectuée sur ce baladeur
770  #
771  def popCmd(self,owner, cmd):
772  global activeThreads, pastCommands, lastCommand
773  owner=u"%s" %owner
774  owner=owner.encode("utf-8")
775  if activeThreads.has_key(owner):
776  cmd0=activeThreads[owner].pop()
777  if cmd0 in cmd:
778  msg=cmd.replace(cmd0,"")+"\n"
779  logFile=open(os.path.expanduser(logFileName),"a")
780  logFile.write(msg)
781  logFile.close()
782  else:
783  raise Exception, (u"mismatched commands\n%s\n%s" %(cmd,cmd0)).encode("utf-8")
784  if len(activeThreads[owner])==0:
785  activeThreads.pop(owner)
786  else:
787  raise Exception, "End of command without a begin."
788  ## print "dans tableModel, popCmd", activeThreads
789  self.updateOwnerColumn()
790  if len(activeThreads)==0 :
791  self.pere.updateButtons()
792 
793  ##
794  #
795  # force la mise à jour de la colonne des propriétaires
796  #
797  def updateOwnerColumn(self):
798  if self.checkable:
799  column=1
800  else:
801  column=0
802  self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), self.index(0,column), self.index(len(self.donnees)-1, column))
803  self.pere.t.viewport().update()
804 
805  ##
806  #
807  # @parent un QModelIndex
808  #
809  def rowCount(self, parent):
810  return len(self.donnees)
811 
812  ##
813  #
814  # @parent un QModelIndex
815  #
816  def columnCount(self, parent):
817  return len(self.header)
818 
819  def setData(self, index, value, role):
820  if index.column()==0 and self.checkable:
821  self.donnees[index.row()].selected=value
822  return True
823  else:
824  return QAbstractTableModel.setData(self, index, role)
825 
826  ##
827  #
828  # @param index in QModelIndex
829  # @return la partition pointée par index
830  #
831  def partition(self, index):
832  return self.donnees[index.row()][-1]
833 
834  def data(self, index, role):
835  if not index.isValid():
836  return QVariant()
837  elif role==Qt.ToolTipRole:
838  c=index.column()
839  h=self.pere.header[c]
840  if c==0 and self.checkable:
841  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)
842  elif c==1:
843  return QApplication.translate("Main","Propriétaire de la clé USB ou du baladeur ;<br><b>Double-clic</b> pour modifier.",None, QApplication.UnicodeUTF8)
844  elif "device-mount-paths" in h:
845  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)
846  elif "device-size" in h:
847  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)
848  elif "drive-vendor" in h:
849  return QApplication.translate("Main","Fabricant de la clé USB ou du baladeur.",None, QApplication.UnicodeUTF8)
850  elif "drive-model" in h:
851  return QApplication.translate("Main","Modèle de la clé USB ou du baladeur.",None, QApplication.UnicodeUTF8)
852  elif "drive-serial" in h:
853  return QApplication.translate("Main","Numéro de série de la clé USB ou du baladeur.",None, QApplication.UnicodeUTF8)
854  else:
855  return ""
856  elif role != Qt.DisplayRole:
857  return QVariant()
858  if index.row()<len(self.donnees):
859  return QVariant(self.donnees[index.row()][index.column()])
860  else:
861  return QVariant()
862 
863  def headerData(self, section, orientation, role):
864  if orientation == Qt.Horizontal and role == Qt.DisplayRole:
865  return QVariant(self.header[section])
866  elif orientation == Qt.Vertical and role == Qt.DisplayRole:
867  return QVariant(section+1)
868  return QVariant()
869 
870  ##
871  # Sort table by given column number.
872  # @param Ncol numéro de la colonne de tri
873  # @param order l'odre de tri, Qt.DescendingOrder par défaut
874  #
875  def sort(self, Ncol, order=Qt.DescendingOrder):
876  self.emit(SIGNAL("layoutAboutToBeChanged()"))
877  self.donnees = sorted(self.donnees, key=operator.itemgetter(Ncol))
878  if order == Qt.DescendingOrder:
879  self.donnees.reverse()
880  self.emit(SIGNAL("layoutChanged()"))
881 
882 ##
883 #
884 # @param view_item_style_options des options permettant de décider de
885 # la taille d'un rectangle
886 # @return un QRect dimensionné selon les bonnes options
887 #
888 def CheckBoxRect(view_item_style_options):
889  check_box_style_option=QStyleOptionButton()
890  check_box_rect = QApplication.style().subElementRect(QStyle.SE_CheckBoxIndicator,check_box_style_option)
891  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)
892  return QRect(check_box_point, check_box_rect.size())
893 
895  def __init__(self, parent):
896  QStyledItemDelegate.__init__(self,parent)
897 
898  def paint(self, painter, option, index):
899  checked = index.model().data(index, Qt.DisplayRole).toBool()
900  check_box_style_option=QStyleOptionButton()
901  check_box_style_option.state |= QStyle.State_Enabled
902  if checked:
903  check_box_style_option.state |= QStyle.State_On
904  else:
905  check_box_style_option.state |= QStyle.State_Off
906  check_box_style_option.rect = CheckBoxRect(option);
907  QApplication.style().drawControl(QStyle.CE_CheckBox, check_box_style_option, painter)
908 
909  def editorEvent(self, event, model, option, index):
910  if ((event.type() == QEvent.MouseButtonRelease) or (event.type() == QEvent.MouseButtonDblClick)):
911  if (event.button() != Qt.LeftButton or not CheckBoxRect(option).contains(event.pos())):
912  return False
913  if (event.type() == QEvent.MouseButtonDblClick):
914  return True
915  elif (event.type() == QEvent.KeyPress):
916  if event.key() != Qt.Key_Space and event.key() != Qt.Key_Select:
917  return False
918  else:
919  return False
920  checked = index.model().data(index, Qt.DisplayRole).toBool()
921  result = model.setData(index, not checked, Qt.EditRole)
922  return result
923 
924 
925 ##
926 #
927 # Classe pour identifier le baladeur dans le tableau.
928 # La routine de rendu à l'écran trace une petite icône et le nom du
929 # propriétaire à côté.
930 #
932  def __init__(self, parent):
933  QStyledItemDelegate.__init__(self,parent)
934  self.okPixmap=QPixmap("/usr/share/icons/Tango/16x16/status/weather-clear.png")
935  self.busyPixmap=QPixmap("/usr/share/icons/Tango/16x16/actions/view-refresh.png")
936 
937  def paint(self, painter, option, index):
938  global activeThreads
939  text = index.model().data(index, Qt.DisplayRole).toString()
940  rect0=QRect(option.rect)
941  rect1=QRect(option.rect)
942  h=rect0.height()
943  w=rect0.width()
944  rect0.setSize(QSize(h,h))
945  rect1.translate(h,0)
946  rect1.setSize(QSize(w-h,h))
947  QApplication.style().drawItemText (painter, rect1, Qt.AlignLeft+Qt.AlignVCenter, option.palette, True, text)
948  QApplication.style().drawItemText (painter, rect0, Qt.AlignCenter, option.palette, True, QString("O"))
949  text=(u"%s" %text).encode("utf-8")
950  if activeThreads.has_key(text):
951  QApplication.style().drawItemPixmap (painter, rect0, Qt.AlignCenter, self.busyPixmap)
952  else:
953  QApplication.style().drawItemPixmap (painter, rect0, Qt.AlignCenter, self.okPixmap)
954 
955 ##
956 #
957 # Classe pour figurer la taille de la mémoire du baladeur. Trace un petit
958 # secteur représentant la place occupée, puis affiche la place avec l'unité
959 # le plus parropriée.
960 #
962  def __init__(self, parent):
963  QStyledItemDelegate.__init__(self,parent)
964 
965 
966  def paint(self, painter, option, index):
967  value = int(index.model().data(index, Qt.DisplayRole).toString())
968  text = self.val2txt(value)
969  rect0=QRect(option.rect)
970  rect1=QRect(option.rect)
971  rect0.translate(2,(rect0.height()-16)/2)
972  rect0.setSize(QSize(16,16))
973  rect1.translate(20,0)
974  rect1.setWidth(rect1.width()-20)
975  QApplication.style().drawItemText (painter, rect1, Qt.AlignLeft+Qt.AlignVCenter, option.palette, True, text)
976  # dessin d'un petit cercle pour l'occupation
977  mount=index.model().partition(index).mountPoint()
978  dev,total,used,remain,pcent,path = self.parent().diskSizeData(mount)
979  pcent=int(pcent[:-1])
980  painter.setBrush(QBrush(QColor("slateblue")))
981  painter.drawPie(rect0,0,16*360*pcent/100)
982 
983  ##
984  #
985  # @return a string with a value with unit K, M, or G
986  #
987  def val2txt(self, val):
988  suffixes=["B", "KB", "MB", "GB", "TB"]
989  val*=1.0 # calcul flottant
990  i=0
991  while val > 1024 and i < len(suffixes):
992  i+=1
993  val/=1024
994  return "%4.1f %s" %(val, suffixes[i])
995 
996