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