7     this file is part of the project scolasync 
    9     Copyright (C) 2010-2014 Georges Khaznadar <georgesk@ofset.org> 
   11     This program is free software: you can redistribute it and/or modify 
   12     it under the terms of the GNU General Public License as published by 
   13     the Free Software Foundation, either version3 of the License, or 
   14     (at your option) any later version. 
   16     This program is distributed in the hope that it will be useful, 
   17     but WITHOUT ANY WARRANTY; without even the implied warranty of 
   18     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
   19     GNU General Public License for more details. 
   21     You should have received a copy of the GNU General Public License 
   22     along with this program.  If not, see <http://www.gnu.org/licenses/>. 
   29 import ownedUsbDisk, help, copyToDialog1, chooseInSticks, usbThread
 
   30 import diskFull, preferences, checkBoxDialog
 
   31 import os.path, operator, subprocess, dbus, re, time, copy
 
   32 from notification 
import Notification
 
   33 from usbDisk2 
import safePath
 
   37 from globaldef 
import logFileName, _dir
 
   54     global pastCommands, lastCommand
 
   55     if cmd 
in pastCommands:
 
   56         pastCommands[cmd].append(partition.owner)
 
   58         pastCommands[cmd]=[partition.owner]
 
   66     checkAllSignal=pyqtSignal()
 
   67     checkToggleSignal=pyqtSignal()
 
   68     checkNoneSignal=pyqtSignal()
 
   69     shouldNameDrive=pyqtSignal()
 
   70     pushCmdSignal=pyqtSignal(str, str)
 
   71     popCmdSignal=pyqtSignal(str, str)
 
   79         QMainWindow.__init__(self)
 
   80         QWidget.__init__(self, parent)
 
   82         from Ui_mainWindow  
import Ui_MainWindow
 
   83         self.
ui = Ui_MainWindow()
 
   85         QIcon.setThemeName(
"Tango")
 
   88         self.
movefromIcon=QIcon.fromTheme(
"movefrom",QIcon(
"/usr/share/scolasync/images/movefrom.png"))
 
  100         self.
namesFullTip=QApplication.translate(
"MainWindow", 
"<br />Des noms sont disponibles pour renommer les prochains baladeurs que vous brancherez", 
None)
 
  101         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)
 
  109         self.
proxy.setSourceModel(self.
t.model())
 
  115         self.
ui.helpButton.clicked.connect(self.
help)
 
  116         self.
ui.umountButton.clicked.connect(self.
umount)
 
  117         self.
ui.toButton.clicked.connect(self.
copyTo)
 
  118         self.
ui.fromButton.clicked.connect(self.
copyFrom)
 
  119         self.
ui.delButton.clicked.connect(self.
delFiles)
 
  120         self.
ui.redoButton.clicked.connect(self.
redoCmd)
 
  121         self.
ui.namesButton.clicked.connect(self.
namesCmd)
 
  122         self.
ui.preferenceButton.clicked.connect(self.
preference)
 
  129         qApp.available.addHook(
'object-added',   self.
cbAdded())
 
  130         qApp.available.addHook(
'object-removed', self.
cbRemoved())
 
  146             icon.addPixmap(QIcon.fromTheme(name).pixmap(32))
 
  148             icon.addPixmap(
"images/icons32/"+name+
".png")
 
  158         global activeThreads, pastCommands, lastCommand
 
  159         if owner 
in activeThreads:
 
  160             activeThreads[owner].append(cmd)
 
  162             activeThreads[owner]=[cmd]
 
  163         self.
tm.updateOwnerColumn()
 
  172         global activeThreads, pastCommands, lastCommand
 
  173         if owner 
in activeThreads:
 
  174             cmd0=activeThreads[owner].pop()
 
  176                 msg=cmd.replace(cmd0,
"")+
"\n" 
  177                 logFile=open(os.path.expanduser(logFileName),
"a")
 
  181                 raise Exception((
"mismatched commands\n%s\n%s" %(cmd,cmd0)))
 
  182             if len(activeThreads[owner])==0:
 
  183                 activeThreads.pop(owner)
 
  185             raise Exception(
"End of command without a begin.")
 
  186         self.
tm.updateOwnerColumn()
 
  187         if len(activeThreads)==0 :
 
  197         index0=model.createIndex(0,0)
 
  198         index1=model.createIndex(len(model.donnees)-1,0)
 
  199         srange=QItemSelectionRange(index0,index1)
 
  200         for i 
in srange.indexes():
 
  201             checked=bool(i.model().data(i,Qt.DisplayRole))
 
  202             model.setData(i, boolFunc(checked),Qt.EditRole)
 
  231             hint=db.readStudent(disk.serial, disk.uuid, ownedUsbDisk.tattooInDir(disk.mp))
 
  238                                           driveIdent=(stickId, uuid, tattoo))
 
  248         def _cbAdded(man, obj):
 
  249             if qApp.available.modified:
 
  254                 qApp.available.modified=
False 
  262         def _cbRemoved(man, obj):
 
  263             if qApp.available.modified:
 
  265                 if path 
in qApp.available.targets:
 
  269                 qApp.available.modified=
False 
  295         self.
iconRedo.addPixmap(QIcon.fromTheme(
"go-jump").pixmap(32), QIcon.Normal, QIcon.Off)
 
  297         self.
iconStop.addPixmap(QIcon.fromTheme(
"stop").pixmap(32), QIcon.Normal, QIcon.Off)
 
  299         self.
redoToolTip=QApplication.translate(
"MainWindow", 
"Refaire à nouveau", 
None)
 
  300         self.
redoStatusTip=QApplication.translate(
"MainWindow", 
"Refaire à nouveau la dernière opération réussie, avec les baladeurs connectés plus récemment", 
None)
 
  301         self.
stopToolTip=QApplication.translate(
"MainWindow", 
"Arrêter les opérations en cours", 
None)
 
  302         self.
stopStatusTip=QApplication.translate(
"MainWindow", 
"Essaie d'arrêter les opérations en cours. À faire seulement si celles-ci durent trop longtemps", 
None)
 
  313         self.
header=ownedUsbDisk.uDisk2.headers()
 
  328         connectedCount=int(qApp.available)
 
  329         self.
ui.lcdNumber.display(connectedCount)
 
  330         self.
t.resizeColumnsToContents()
 
  348         mappedIdx=self.
proxy.mapFromSource(idx)
 
  358             cmd=
"xdg-open '%s'" %idx.data()
 
  359             subprocess.call(cmd, shell=
True)
 
  360         elif "capacity" in h:
 
  361             mount=idx.model().partition(idx).mountPoint()
 
  362             dev,total,used,remain,pcent,path = self.
diskSizeData(mount)
 
  363             pcent=int(pcent[:-1])
 
  367             QMessageBox.warning(
None,
 
  368                                 QApplication.translate(
"Dialog",
"Double-clic non pris en compte",
None),
 
  369                                 QApplication.translate(
"Dialog",
"pas d'action pour l'attribut {a}",
None).format(a=h))
 
  385         if type(rowOrDev)==type(0):
 
  386             path=qApp.available[rowOrDev][self.
header.index(
"1mp")]
 
  390         dfOutput=subprocess.Popen(cmd, shell=
True, stdout=subprocess.PIPE).communicate()[0]
 
  391         dfOutput=str(dfOutput.split(b
"\n")[-2])
 
  392         m = re.match(
"(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+).*", dfOutput).groups()
 
  405             s=db.readStudent(d.stickid, d.uuid, d.tattoo())
 
  408             elif s==
None and defaultDisk==
None :
 
  419         student=
"%s" %self.
tm.data(idx,Qt.DisplayRole).value()
 
  421         ownedUsbDisk.editRecord(self.
diskFromOwner(student), hint=student)
 
  439         self.
ui.namesButton.setIcon(icon)
 
  440         self.
ui.namesButton.setToolTip(msg)
 
  441         self.
ui.namesButton.setStatusTip(msg.replace(
"<br />",
""))
 
  453         global activeThreads, lastCommand
 
  454         active = len(qApp.available)>0
 
  455         for button 
in (self.
ui.toButton,
 
  458                        self.
ui.umountButton):
 
  459             button.setEnabled(active)
 
  469         if len(activeThreads) > 0:
 
  473             self.
ui.redoButton.setEnabled(
True)
 
  479             self.
ui.redoButton.setEnabled(lastCommand!=
None)
 
  480         l=self.
namesDialog.ui.listWidget.findItems(
"*",Qt.MatchWildcard)
 
  491         pref.setValues(db.readPrefs())
 
  494         if pref.result()==QDialog.Accepted:
 
  495             db.writePrefs(pref.values())
 
  503         titre1=QApplication.translate(
"Dialog",
"Choix de fichiers à supprimer",
None)
 
  504         titre2=QApplication.translate(
"Dialog",
"Choix de fichiers à supprimer (jokers autorisés)",
None)
 
  508             pathList=d.pathList()
 
  509             buttons=QMessageBox.Ok|QMessageBox.Cancel
 
  510             defaultButton=QMessageBox.Cancel
 
  511             reply=QMessageBox.warning(
 
  513                 QApplication.translate(
"Dialog",
"Vous allez effacer plusieurs baladeurs",
None),
 
  514                 QApplication.translate(
"Dialog",
"Etes-vous certain de vouloir effacer : "+
"\n".join(pathList),
None),
 
  515                 buttons, defaultButton)
 
  516             if reply == QMessageBox.Ok:
 
  517                 cmd=
"usbThread.threadDeleteInUSB(p,{paths},subdir='Travail', logfile='{log}', parent=self)".format(paths=pathList,log=logFileName)
 
  518                 for p 
in qApp.available:
 
  519                     if not p.selected: 
continue  
  527             msgBox=QMessageBox.warning(
 
  529                 QApplication.translate(
"Dialog",
"Aucun fichier sélectionné",
None),
 
  530                 QApplication.translate(
"Dialog",
"Veuillez choisir au moins un fichier",
None))
 
  540             cmd=
"usbThread.threadCopyToUSB(p,{selected},subdir='{subdir}', logfile='{logfile}', parent=self)".format(selected=list(d.selectedList()), subdir=self.
workdir, logfile=logFileName)
 
  542             for p 
in qApp.available:
 
  543                 if not p.selected: 
continue  
  551             msgBox=QMessageBox.warning(
 
  553                 QApplication.translate(
"Dialog",
"Aucun fichier sélectionné",
None),
 
  554                 QApplication.translate(
"Dialog",
"Veuillez choisir au moins un fichier",
None))
 
  561         titre1=QApplication.translate(
"Dialog",
"Choix de fichiers à copier",
None)
 
  562         titre2=QApplication.translate(
"Dialog", 
"Choix de fichiers à copier depuis les baladeurs", 
None)
 
  563         okPrompt=QApplication.translate(
"Dialog", 
"Choix de la destination ...", 
None)
 
  567             msgBox=QMessageBox.warning(
None,
 
  568                                        QApplication.translate(
"Dialog",
"Aucun fichier sélectionné",
None),
 
  569                                        QApplication.translate(
"Dialog",
"Veuillez choisir au moins un fichier",
None))
 
  572         pathList=d.pathList()
 
  573         mp=d.selectedDiskMountPoint()
 
  574         initialPath=os.path.expanduser(
"~")
 
  575         destDir = QFileDialog.getExistingDirectory(
 
  577             QApplication.translate(
"Dialog",
"Choisir un répertoire de destination",
None),
 
  579         if destDir 
and len(destDir)>0 :
 
  581                 cmd=
"""usbThread.threadMoveFromUSB( 
  582                            p,{paths},subdir=self.workdir, 
  583                            rootPath='{mp}', dest='{dest}', logfile='{log}', 
  584                            parent=self)""".format(paths=pathList, mp=mp, dest=destDir, log=logFileName)
 
  586                 cmd=
"""usbThread.threadCopyFromUSB( 
  587                            p,{paths},subdir=self.workdir, 
  588                            rootPath='{mp}', dest='{dest}', logfile='{log}', 
  589                            parent=self)""".format(paths=pathList, mp=mp, dest=destDir, log=logFileName)
 
  591             for p 
in qApp.available:
 
  592                 if not p.selected: 
continue  
  604             buttons=QMessageBox.Ok|QMessageBox.Cancel
 
  605             defaultButton=QMessageBox.Cancel
 
  606             if QMessageBox.question(
 
  608                 QApplication.translate(
"Dialog",
"Voir les copies",
None),
 
  609                 QApplication.translate(
"Dialog",
"Voulez-vous voir les fichiers copiés ?",
None),
 
  610                 buttons, defaultButton)==QMessageBox.Ok:
 
  611                 subprocess.call(
"xdg-open '%s'" %destDir,shell=
True)
 
  614             msgBox=QMessageBox.warning(
 
  616                 QApplication.translate(
"Dialog",
"Destination manquante",
None),
 
  617                 QApplication.translate(
"Dialog",
"Veuillez choisir une destination pour la copie des fichiers",
None))
 
  625         global lastCommand, pastCommands, activeThreads
 
  626         if len(activeThreads)>0:
 
  630                         thread._Thread__stop()
 
  631                         print (str(thread.getName()) + 
' is terminated')
 
  633                         print (str(thread.getName()) + 
' could not be terminated')
 
  635             if lastCommand==
None:
 
  637             if QMessageBox.question(
 
  639                 QApplication.translate(
"Dialog",
"Réitérer la dernière commande",
None),
 
  640                 QApplication.translate(
"Dialog",
"La dernière commande était<br>{cmd}<br>Voulez-vous la relancer avec les nouveaux baladeurs ?",
None).format(cmd=lastCommand))==QMessageBox.Cancel:
 
  642             for p 
in qApp.available:
 
  643                 if p.owner 
in pastCommands[lastCommand] : 
continue 
  644                 exec(compile(lastCommand,
'<string>',
'exec'))
 
  648                 pastCommands[lastCommand].append(p.owner)
 
  669         buttons=QMessageBox.Ok|QMessageBox.Cancel
 
  670         defaultButton=QMessageBox.Cancel
 
  671         button=QMessageBox.question (
 
  673             QApplication.translate(
"Main",
"Démontage des baladeurs",
None),
 
  674             QApplication.translate(
"Main",
"Êtes-vous sûr de vouloir démonter tous les baladeurs cochés de la liste ?",
None),
 
  675             buttons,defaultButton)
 
  676         if button!=QMessageBox.Ok:
 
  678         for d 
in qApp.available.disks_ud():
 
  679             for partition 
in qApp.available.parts_ud(d.path):
 
  681                     cmd=
"umount {0}".format(partition.mp)
 
  682                     subprocess.call(cmd, shell=
True)
 
  683             cmd= 
"udisks --detach {0}".format(d.devStuff)
 
  684             subprocess.call(cmd, shell=
True)
 
  696             if h 
in ownedUsbDisk.uDisk2._itemNames:
 
  697                 self.
visibleheader.append(self.tr(ownedUsbDisk.uDisk2._itemNames[h]))
 
  701         self.
t.setModel(self.
tm)
 
  705         self.
proxy.setSourceModel(self.
t.model())
 
  712         return len(one.targets) == len(two.targets) 
and \
 
  713             set([p.uniqueId() 
for p 
in one]) == set([p.uniqueId() 
for p 
in two])
 
  725     def __init__(self, parent=None, header=[], donnees=None):
 
  726         QAbstractTableModel.__init__(self,parent)
 
  736         self.dataChanged.emit(self.index(0,column), self.index(len(self.
donnees)-1, column))
 
  737         self.
pere.t.viewport().update()
 
  752         if index.column()==0:
 
  753             self.
donnees[index.row()].selected=value
 
  756             return QAbstractTableModel.setData(self, index, role)
 
  763         return self.
donnees[index.row()][-1]
 
  766         if not index.isValid():
 
  768         elif role==Qt.ToolTipRole:
 
  770             h=self.
pere.header[c]
 
  772                 return QApplication.translate(
"Main",
"Cocher ou décocher cette case en cliquant.<br><b>Double-clic</b> pour agir sur plusieurs baladeurs.",
None)
 
  774                 return QApplication.translate(
"Main",
"Propriétaire de la clé USB ou du baladeur ;<br><b>Double-clic</b> pour modifier.",
None)
 
  776                 return QApplication.translate(
"Main",
"Point de montage de la clé USB ou du baladeur ;<br><b>Double-clic</b> pour voir les fichiers.",
None)
 
  777             elif "capacity" in h:
 
  778                 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)
 
  780                 return QApplication.translate(
"Main",
"Fabricant de la clé USB ou du baladeur.",
None)
 
  782                 return QApplication.translate(
"Main",
"Modèle de la clé USB ou du baladeur.",
None)
 
  784                 return QApplication.translate(
"Main",
"Numéro de série de la clé USB ou du baladeur.",
None)
 
  787         elif role != Qt.DisplayRole:
 
  789         if index.row()<len(self.
donnees):
 
  791                 return QVariant(self.
donnees[index.row()][index.column()])
 
  793                 print(
"Le bug du retrait de clé non détecté a encore frappé, quand sera-t-il éliminé ?")
 
  794                 self.
pere.findAllDisks()
 
  801         if orientation == Qt.Horizontal 
and role == Qt.DisplayRole:
 
  802             return QVariant(self.
header[section])
 
  803         elif orientation == Qt.Vertical 
and role == Qt.DisplayRole:
 
  804             return QVariant(section+1)
 
  811     def sort(self, Ncol, order=Qt.DescendingOrder):
 
  812         self.layoutAboutToBeChanged.emit()
 
  813         self.
donnees = sorted(self.
donnees, key=operator.itemgetter(Ncol))
 
  814         if order == Qt.DescendingOrder:
 
  816         self.layoutChanged.emit()
 
  824     check_box_style_option=QStyleOptionButton()
 
  825     check_box_rect = QApplication.style().subElementRect(QStyle.SE_CheckBoxIndicator,check_box_style_option)
 
  826     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)
 
  827     return QRect(check_box_point, check_box_rect.size())
 
  831         QStyledItemDelegate.__init__(self,parent)
 
  833     def paint(self, painter, option, index):
 
  834         checked = bool(index.model().data(index, Qt.DisplayRole))
 
  835         check_box_style_option=QStyleOptionButton()
 
  836         check_box_style_option.state |= QStyle.State_Enabled
 
  838             check_box_style_option.state |= QStyle.State_On
 
  840             check_box_style_option.state |= QStyle.State_Off
 
  842         QApplication.style().drawControl(QStyle.CE_CheckBox, check_box_style_option, painter)
 
  845         if ((event.type() == QEvent.MouseButtonRelease) 
or (event.type() == QEvent.MouseButtonDblClick)):
 
  846             if (event.button() != Qt.LeftButton 
or not CheckBoxRect(option).contains(event.pos())):
 
  848             if (event.type() == QEvent.MouseButtonDblClick):
 
  850         elif (event.type() == QEvent.KeyPress):
 
  851             if event.key() != Qt.Key_Space 
and event.key() != Qt.Key_Select:
 
  855         checked = bool(index.model().data(index, Qt.DisplayRole))
 
  856         result = model.setData(index, 
not checked, Qt.EditRole)
 
  867         QStyledItemDelegate.__init__(self,parent)
 
  868         self.
okPixmap=QPixmap(
"/usr/share/icons/Tango/16x16/status/weather-clear.png")
 
  869         self.
busyPixmap=QPixmap(
"/usr/share/icons/Tango/16x16/actions/view-refresh.png")
 
  871     def paint(self, painter, option, index):
 
  873         text = index.model().data(index, Qt.DisplayRole).value()
 
  874         rect0=QRect(option.rect)
 
  875         rect1=QRect(option.rect)
 
  878         rect0.setSize(QSize(h,h))
 
  880         rect1.setSize(QSize(w-h,h))
 
  881         QApplication.style().drawItemText (painter, rect1, Qt.AlignLeft+Qt.AlignVCenter, option.palette, 
True, text)
 
  882         QApplication.style().drawItemText (painter, rect0, Qt.AlignCenter, option.palette, 
True, 
"O")
 
  883         if text 
in activeThreads:
 
  884             QApplication.style().drawItemPixmap (painter, rect0, Qt.AlignCenter, self.
busyPixmap)
 
  886             QApplication.style().drawItemPixmap (painter, rect0, Qt.AlignCenter, self.
okPixmap)
 
  895         QStyledItemDelegate.__init__(self,parent)
 
  898     def paint(self, painter, option, index):
 
  899         v=index.model().data(index, Qt.DisplayRole)
 
  902         rect0=QRect(option.rect)
 
  903         rect1=QRect(option.rect)
 
  904         rect0.translate(2,(rect0.height()-16)/2)
 
  905         rect0.setSize(QSize(16,16))
 
  906         rect1.translate(20,0)
 
  907         rect1.setWidth(rect1.width()-20)
 
  908         QApplication.style().drawItemText (painter, rect1, Qt.AlignLeft+Qt.AlignVCenter, option.palette, 
True, text)
 
  910         mount=index.model().partition(index).mountPoint()
 
  911         dev,total,used,remain,pcent,path = self.parent().diskSizeData(mount)
 
  912         pcent=int(pcent[:-1])
 
  913         painter.setBrush(QBrush(QColor(
"slateblue")))
 
  914         painter.drawPie(rect0,0,16*360*pcent/100)
 
  920         suffixes=[
"B", 
"KB", 
"MB", 
"GB", 
"TB"]
 
  923         while val > 1024 
and i < len(suffixes):
 
  926         return "%4.1f %s" %(val, suffixes[i])