8 this file is part of the project scolasync
10 Copyright (C) 2010 Georges Khaznadar <georgesk@ofset.org>
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.
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.
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/>.
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
38 from globaldef
import logFileName, _dir
56 global pastCommands, lastCommand
57 if cmd
in pastCommands:
58 pastCommands[cmd].append(partition.owner)
60 pastCommands[cmd]=[partition.owner]
71 def __init__(self, parent, opts, locale="fr_FR"):
72 QMainWindow.__init__(self)
73 QWidget.__init__(self, parent)
75 from Ui_mainWindow
import Ui_MainWindow
76 self.
ui = Ui_MainWindow()
78 self.
copyfromIcon=QIcon(
"/usr/share/icons/Tango/scalable/actions/back.svg")
79 self.
movefromIcon=QIcon(
"/usr/share/scolasync/images/movefrom.svg")
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)
90 self.
t=self.ui.tableView
91 self.
proxy=QSortFilterProxyModel()
92 self.proxy.setSourceModel(self.t.model())
102 self.flashTimer.setSingleShot(
True)
104 QObject.connect(self.ui.forceCheckButton, SIGNAL(
"clicked()"), self.
checkDisks)
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)
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)
166 stickId, tattoo, uuid = self.listener.identify(self.
recentConnect)
167 hint=db.readStudent(stickId, uuid, tattoo)
173 nameList=self.namesDialog.itemStrings(),
174 driveIdent=(stickId, uuid, tattoo))
185 vfatPath = self.listener.vfatUsbPath(str(s))
199 if qApp.diskData.hasDev(s):
209 self.iconRedo.addPixmap(QPixmap(
"/usr/share/icons/Tango/scalable/actions/go-jump.svg"), QIcon.Normal, QIcon.Off)
211 self.iconStop.addPixmap(QPixmap(
"/usr/share/icons/Tango/scalable/actions/stop.svg"), QIcon.Normal, QIcon.Off)
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)
225 result=QMainWindow.showEvent(self, ev)
278 mappedIdx=self.proxy.mapFromSource(idx)
284 elif c==1
or (c==0
and not self.
checkable):
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])
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))
317 if type(rowOrDev)==type(0):
318 path=qApp.diskData[rowOrDev][self.header.index(
"1device-mount-paths")]
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()
336 for d
in qApp.diskData.disks.keys():
342 if d.owner==
None or len(d.owner)==0:
356 student=
"%s" %self.tm.data(idx,Qt.DisplayRole).toString()
357 ownedUsbDisk.editRecord(self.
diskFromOwner(student), hint=student)
378 self.ui.namesButton.setIcon(icon)
379 self.ui.namesButton.setToolTip(msg)
380 self.ui.namesButton.setStatusTip(msg.replace(
"<br />",
""))
393 global activeThreads, lastCommand
394 active = len(qApp.diskData)>0
395 for button
in (self.ui.toButton,
398 self.ui.umountButton):
399 button.setEnabled(active)
409 if len(activeThreads) > 0:
410 self.ui.redoButton.setIcon(self.
iconStop)
413 self.ui.redoButton.setEnabled(
True)
416 self.ui.redoButton.setIcon(self.
iconRedo)
419 self.ui.redoButton.setEnabled(lastCommand!=
None)
420 l=self.namesDialog.ui.listWidget.findItems(
"*",Qt.MatchWildcard)
432 pref.setValues(db.readPrefs())
435 if pref.result()==QDialog.Accepted:
436 db.writePrefs(pref.values())
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)
450 pathList=d.pathList()
451 buttons=QMessageBox.Ok|QMessageBox.Cancel
452 defaultButton=QMessageBox.Cancel
453 reply=QMessageBox.warning(
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
466 self.oldThreads.add(t)
469 msgBox=QMessageBox.warning(
471 QApplication.translate(
"Dialog",
"Aucun fichier sélectionné",
None, QApplication.UnicodeUTF8),
472 QApplication.translate(
"Dialog",
"Veuillez choisir au moins un fichier",
None, QApplication.UnicodeUTF8))
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
490 self.oldThreads.add(t)
493 msgBox=QMessageBox.warning(
495 QApplication.translate(
"Dialog",
"Aucun fichier sélectionné",
None, QApplication.UnicodeUTF8),
496 QApplication.translate(
"Dialog",
"Veuillez choisir au moins un fichier",
None, QApplication.UnicodeUTF8))
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)
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))
515 pathList=d.pathList()
516 mp=d.selectedDiskMountPoint()
517 initialPath=os.path.expanduser(
"~")
518 destDir = QFileDialog.getExistingDirectory(
520 QApplication.translate(
"Dialog",
"Choisir un répertoire de destination",
None, QApplication.UnicodeUTF8),
522 if destDir
and len(destDir)>0 :
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)
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)
534 for p
in qApp.diskData:
535 if not p.selected:
continue
545 self.oldThreads.add(t)
547 buttons=QMessageBox.Ok|QMessageBox.Cancel
548 defaultButton=QMessageBox.Cancel
549 if QMessageBox.question(
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)
557 msgBox=QMessageBox.warning(
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))
569 global lastCommand, pastCommands, activeThreads
570 if len(activeThreads)>0:
574 thread._Thread__stop()
575 print (str(thread.getName()) +
' is terminated')
577 print (str(thread.getName()) +
' could not be terminated')
579 if lastCommand==
None:
581 if QMessageBox.question(
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:
586 for p
in qApp.diskData:
587 if p.owner
in pastCommands[lastCommand] :
continue
588 exec(compile(lastCommand,
'<string>',
'exec'))
591 self.oldThreads.add(t)
592 pastCommands[lastCommand].append(p.owner)
600 self.namesDialog.show()
616 buttons=QMessageBox.Ok|QMessageBox.Cancel
617 defaultButton=QMessageBox.Cancel
618 button=QMessageBox.question (
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:
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)
642 if h
in ownedUsbDisk.uDisk._itemNames:
643 self.visibleheader.append(self.tr(ownedUsbDisk.uDisk._itemNames[h]))
645 self.visibleheader.append(h)
647 self.t.setModel(self.
tm)
655 self.proxy.setSourceModel(self.t.model())
678 diskDict=self.listener.connectedVolumes,
680 if force
or not self.
sameDiskData(qApp.diskData, other):
682 connectedCount=int(other)
685 self.t.resizeColumnsToContents()
686 self.ui.lcdNumber.display(connectedCount)
693 self.t.horizontalHeader().setSortIndicator(1, Qt.AscendingOrder);
694 self.t.setSortingEnabled(
True)
695 self.t.resizeColumnsToContents()
704 return set([p.uniqueId()
for p
in one]) == set([p.uniqueId()
for p
in two])
711 self.ui.lcdNumber.setBackgroundRole(QPalette.Highlight)
712 self.flashTimer.start(250)
719 self.ui.lcdNumber.setBackgroundRole(QPalette.Window)
734 def __init__(self, parent=None, header=[], donnees=None, checkable=False):
735 QAbstractTableModel.__init__(self,parent)
740 self.connect(self, SIGNAL(
"pushCmd(QString, QString)"), self.
pushCmd)
741 self.connect(self, SIGNAL(
"popCmd(QString, QString)"), self.
popCmd)
750 global activeThreads, pastCommands, lastCommand
752 owner=owner.encode(
"utf-8")
753 if owner
in activeThreads:
754 activeThreads[owner].append(cmd)
756 activeThreads[owner]=[cmd]
758 self.pere.updateButtons()
767 global activeThreads, pastCommands, lastCommand
769 owner=owner.encode(
"utf-8")
770 if owner
in activeThreads:
771 cmd0=activeThreads[owner].pop()
773 msg=cmd.replace(cmd0,
"")+
"\n"
774 logFile=open(os.path.expanduser(logFileName),
"a")
778 raise Exception((
"mismatched commands\n%s\n%s" %(cmd,cmd0)))
779 if len(activeThreads[owner])==0:
780 activeThreads.pop(owner)
782 raise Exception(
"End of command without a begin.")
784 if len(activeThreads)==0 :
785 self.pere.updateButtons()
796 self.emit(SIGNAL(
"dataChanged(QModelIndex, QModelIndex)"), self.index(0,column), self.index(len(self.
donnees)-1, column))
797 self.pere.t.viewport().update()
815 self.
donnees[index.row()].selected=value
818 return QAbstractTableModel.setData(self, index, role)
826 return self.
donnees[index.row()][-1]
829 if not index.isValid():
831 elif role==Qt.ToolTipRole:
833 h=self.pere.header[c]
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)
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)
850 elif role != Qt.DisplayRole:
852 if index.row()<len(self.
donnees):
853 return QVariant(self.
donnees[index.row()][index.column()])
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)
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()"))
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())
890 QStyledItemDelegate.__init__(self,parent)
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
897 check_box_style_option.state |= QStyle.State_On
899 check_box_style_option.state |= QStyle.State_Off
901 QApplication.style().drawControl(QStyle.CE_CheckBox, check_box_style_option, painter)
904 if ((event.type() == QEvent.MouseButtonRelease)
or (event.type() == QEvent.MouseButtonDblClick)):
905 if (event.button() != Qt.LeftButton
or not CheckBoxRect(option).contains(event.pos())):
907 if (event.type() == QEvent.MouseButtonDblClick):
909 elif (event.type() == QEvent.KeyPress):
910 if event.key() != Qt.Key_Space
and event.key() != Qt.Key_Select:
914 checked = index.model().data(index, Qt.DisplayRole).toBool()
915 result = model.setData(index,
not checked, Qt.EditRole)
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")
931 def paint(self, painter, option, index):
933 text = index.model().data(index, Qt.DisplayRole).toString()
934 rect0=QRect(option.rect)
935 rect1=QRect(option.rect)
938 rect0.setSize(QSize(h,h))
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)
947 QApplication.style().drawItemPixmap (painter, rect0, Qt.AlignCenter, self.
okPixmap)
957 QStyledItemDelegate.__init__(self,parent)
960 def paint(self, painter, option, index):
961 value = int(index.model().data(index, Qt.DisplayRole).toString())
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)
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)
982 suffixes=[
"B",
"KB",
"MB",
"GB",
"TB"]
985 while val > 1024
and i < len(suffixes):
988 return "%4.1f %s" %(val, suffixes[i])