8 this file is part of the project scolasync
10 Copyright (C) 2010-2013 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/>.
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
37 from globaldef
import logFileName, _dir
55 global pastCommands, lastCommand
56 if cmd
in pastCommands:
57 pastCommands[cmd].append(partition.owner)
59 pastCommands[cmd]=[partition.owner]
70 QMainWindow.__init__(self)
71 QWidget.__init__(self, parent)
73 from Ui_mainWindow
import Ui_MainWindow
74 self.
ui = Ui_MainWindow()
76 self.
copyfromIcon=QIcon(
"/usr/share/icons/Tango/scalable/actions/back.svg")
77 self.
movefromIcon=QIcon(
"/usr/share/scolasync/images/movefrom.svg")
79 self.
namesFullIcon=QIcon(
"/usr/share/icons/Tango/scalable/actions/gtk-find-and-replace.svg")
80 self.
namesEmptyIcon=QIcon(
"/usr/share/icons/Tango/scalable/actions/gtk-find.svg")
81 self.
namesFullTip=QApplication.translate(
"MainWindow",
"<br />Des noms sont disponibles pour renommer les prochains baladeurs que vous brancherez",
None, QApplication.UnicodeUTF8)
82 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)
88 self.
t=self.ui.tableView
89 self.
proxy=QSortFilterProxyModel()
90 self.proxy.setSourceModel(self.t.model())
99 self.flashTimer.setSingleShot(
True)
101 QObject.connect(self.ui.forceCheckButton, SIGNAL(
"clicked()"), self.
checkDisks)
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)
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)
163 stickId, tattoo, uuid = self.listener.identify(self.
recentConnect)
164 hint=db.readStudent(stickId, uuid, tattoo)
170 nameList=self.namesDialog.itemStrings(),
171 driveIdent=(stickId, uuid, tattoo))
182 vfatPath = self.listener.vfatUsbPath(str(s))
196 if qApp.diskData.hasDev(s):
206 self.iconRedo.addPixmap(QPixmap(
"/usr/share/icons/Tango/scalable/actions/go-jump.svg"), QIcon.Normal, QIcon.Off)
208 self.iconStop.addPixmap(QPixmap(
"/usr/share/icons/Tango/scalable/actions/stop.svg"), QIcon.Normal, QIcon.Off)
210 self.
redoToolTip=QApplication.translate(
"MainWindow",
"Refaire à nouveau",
None, QApplication.UnicodeUTF8)
211 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)
212 self.
stopToolTip=QApplication.translate(
"MainWindow",
"Arrêter les opérations en cours",
None, QApplication.UnicodeUTF8)
213 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)
222 result=QMainWindow.showEvent(self, ev)
272 mappedIdx=self.proxy.mapFromSource(idx)
281 elif "device-mount-paths" in h:
282 cmd=
"nautilus '%s'" %idx.data().toString ()
283 subprocess.call(cmd, shell=
True)
284 elif "device-size" in h:
285 mount=idx.model().partition(idx).mountPoint()
286 dev,total,used,remain,pcent,path = self.
diskSizeData(mount)
287 pcent=int(pcent[:-1])
291 QMessageBox.warning(
None,
292 QApplication.translate(
"Dialog",
"Double-clic non pris en compte",
None, QApplication.UnicodeUTF8),
293 QApplication.translate(
"Dialog",
"pas d'action pour l'attribut {a}",
None, QApplication.UnicodeUTF8).format(a=h))
311 if type(rowOrDev)==type(0):
312 path=qApp.diskData[rowOrDev][self.header.index(
"1device-mount-paths")]
316 dfOutput=subprocess.Popen(cmd, shell=
True, stdout=subprocess.PIPE).communicate()[0]
317 dfOutput=str(dfOutput.split(b
"\n")[-2])
318 m = re.match(
"(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+).*", dfOutput).groups()
330 for d
in qApp.diskData.disks.keys():
336 if d.owner==
None or len(d.owner)==0:
350 student=
"%s" %self.tm.data(idx,Qt.DisplayRole).toString()
351 ownedUsbDisk.editRecord(self.
diskFromOwner(student), hint=student)
372 self.ui.namesButton.setIcon(icon)
373 self.ui.namesButton.setToolTip(msg)
374 self.ui.namesButton.setStatusTip(msg.replace(
"<br />",
""))
387 global activeThreads, lastCommand
388 active = len(qApp.diskData)>0
389 for button
in (self.ui.toButton,
392 self.ui.umountButton):
393 button.setEnabled(active)
403 if len(activeThreads) > 0:
404 self.ui.redoButton.setIcon(self.
iconStop)
407 self.ui.redoButton.setEnabled(
True)
410 self.ui.redoButton.setIcon(self.
iconRedo)
413 self.ui.redoButton.setEnabled(lastCommand!=
None)
414 l=self.namesDialog.ui.listWidget.findItems(
"*",Qt.MatchWildcard)
426 pref.setValues(db.readPrefs())
429 if pref.result()==QDialog.Accepted:
430 db.writePrefs(pref.values())
439 titre1=QApplication.translate(
"Dialog",
"Choix de fichiers à supprimer",
None, QApplication.UnicodeUTF8)
440 titre2=QApplication.translate(
"Dialog",
"Choix de fichiers à supprimer (jokers autorisés)",
None, QApplication.UnicodeUTF8)
444 pathList=d.pathList()
445 buttons=QMessageBox.Ok|QMessageBox.Cancel
446 defaultButton=QMessageBox.Cancel
447 reply=QMessageBox.warning(
449 QApplication.translate(
"Dialog",
"Vous allez effacer plusieurs baladeurs",
None, QApplication.UnicodeUTF8),
450 QApplication.translate(
"Dialog",
"Etes-vous certain de vouloir effacer : "+
"\n".join(pathList),
None, QApplication.UnicodeUTF8),
451 buttons, defaultButton)
452 if reply == QMessageBox.Ok:
453 cmd=
"usbThread.threadDeleteInUSB(p,{paths},subdir='Travail', logfile='{log}', parent=self.tm)".format(paths=pathList,log=logFileName)
454 for p
in qApp.diskData:
455 if not p.selected:
continue
460 self.oldThreads.add(t)
463 msgBox=QMessageBox.warning(
465 QApplication.translate(
"Dialog",
"Aucun fichier sélectionné",
None, QApplication.UnicodeUTF8),
466 QApplication.translate(
"Dialog",
"Veuillez choisir au moins un fichier",
None, QApplication.UnicodeUTF8))
477 cmd=
"usbThread.threadCopyToUSB(p,{selected},subdir='{subdir}', logfile='{logfile}', parent=self.tm)".format(selected=list(d.selectedList()), subdir=self.
workdir, logfile=logFileName)
478 for p
in qApp.diskData:
479 if not p.selected:
continue
484 self.oldThreads.add(t)
487 msgBox=QMessageBox.warning(
489 QApplication.translate(
"Dialog",
"Aucun fichier sélectionné",
None, QApplication.UnicodeUTF8),
490 QApplication.translate(
"Dialog",
"Veuillez choisir au moins un fichier",
None, QApplication.UnicodeUTF8))
498 titre1=QApplication.translate(
"Dialog",
"Choix de fichiers à copier",
None, QApplication.UnicodeUTF8)
499 titre2=QApplication.translate(
"Dialog",
"Choix de fichiers à copier depuis les baladeurs",
None, QApplication.UnicodeUTF8)
500 okPrompt=QApplication.translate(
"Dialog",
"Choix de la destination ...",
None, QApplication.UnicodeUTF8)
504 msgBox=QMessageBox.warning(
None,
505 QApplication.translate(
"Dialog",
"Aucun fichier sélectionné",
None, QApplication.UnicodeUTF8),
506 QApplication.translate(
"Dialog",
"Veuillez choisir au moins un fichier",
None, QApplication.UnicodeUTF8))
509 pathList=d.pathList()
510 mp=d.selectedDiskMountPoint()
511 initialPath=os.path.expanduser(
"~")
512 destDir = QFileDialog.getExistingDirectory(
514 QApplication.translate(
"Dialog",
"Choisir un répertoire de destination",
None, QApplication.UnicodeUTF8),
516 if destDir
and len(destDir)>0 :
518 cmd=
"""usbThread.threadMoveFromUSB(
519 p,{paths},subdir=self.workdir,
520 rootPath='{mp}', dest='{dest}', logfile='{log}',
521 parent=self.tm)""".format(paths=pathList, mp=mp, dest=destDir, log=logFileName)
523 cmd=
"""usbThread.threadCopyFromUSB(
524 p,{paths},subdir=self.workdir,
525 rootPath='{mp}', dest='{dest}', logfile='{log}',
526 parent=self.tm)""".format(paths=pathList, mp=mp, dest=destDir, log=logFileName)
528 for p
in qApp.diskData:
529 if not p.selected:
continue
539 self.oldThreads.add(t)
541 buttons=QMessageBox.Ok|QMessageBox.Cancel
542 defaultButton=QMessageBox.Cancel
543 if QMessageBox.question(
545 QApplication.translate(
"Dialog",
"Voir les copies",
None, QApplication.UnicodeUTF8),
546 QApplication.translate(
"Dialog",
"Voulez-vous voir les fichiers copiés ?",
None, QApplication.UnicodeUTF8),
547 buttons, defaultButton)==QMessageBox.Ok:
548 subprocess.call(
"nautilus '%s'" %destDir,shell=
True)
551 msgBox=QMessageBox.warning(
553 QApplication.translate(
"Dialog",
"Destination manquante",
None, QApplication.UnicodeUTF8),
554 QApplication.translate(
"Dialog",
"Veuillez choisir une destination pour la copie des fichiers",
None, QApplication.UnicodeUTF8))
563 global lastCommand, pastCommands, activeThreads
564 if len(activeThreads)>0:
568 thread._Thread__stop()
569 print (str(thread.getName()) +
' is terminated')
571 print (str(thread.getName()) +
' could not be terminated')
573 if lastCommand==
None:
575 if QMessageBox.question(
577 QApplication.translate(
"Dialog",
"Réitérer la dernière commande",
None, QApplication.UnicodeUTF8),
578 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:
580 for p
in qApp.diskData:
581 if p.owner
in pastCommands[lastCommand] :
continue
582 exec(compile(lastCommand,
'<string>',
'exec'))
585 self.oldThreads.add(t)
586 pastCommands[lastCommand].append(p.owner)
594 self.namesDialog.show()
610 buttons=QMessageBox.Ok|QMessageBox.Cancel
611 defaultButton=QMessageBox.Cancel
612 button=QMessageBox.question (
614 QApplication.translate(
"Main",
"Démontage des baladeurs",
None, QApplication.UnicodeUTF8),
615 QApplication.translate(
"Main",
"Êtes-vous sûr de vouloir démonter tous les baladeurs cochés de la liste ?",
None, QApplication.UnicodeUTF8),
616 buttons,defaultButton)
617 if button!=QMessageBox.Ok:
619 for d
in qApp.diskData.disks.keys():
620 devfile_disk=d.getProp(
"device-file-by-path")
621 if isinstance(devfile_disk, dbus.Array):
622 devfile_disk=devfile_disk[0]
623 subprocess.call(
"eject %s 2>/dev/null || true && udisks --detach %s" %(devfile_disk,devfile_disk), shell=
True)
636 if h
in ownedUsbDisk.uDisk._itemNames:
637 self.visibleheader.append(self.tr(ownedUsbDisk.uDisk._itemNames[h]))
639 self.visibleheader.append(h)
641 self.t.setModel(self.
tm)
645 self.proxy.setSourceModel(self.t.model())
667 diskDict=self.listener.connectedVolumes,
669 if force
or not self.
sameDiskData(qApp.diskData, other):
671 connectedCount=int(other)
674 self.t.resizeColumnsToContents()
675 self.ui.lcdNumber.display(connectedCount)
678 self.t.horizontalHeader().setSortIndicator(1, Qt.AscendingOrder);
679 self.t.setSortingEnabled(
True)
680 self.t.resizeColumnsToContents()
689 return set([p.uniqueId()
for p
in one]) == set([p.uniqueId()
for p
in two])
696 self.ui.lcdNumber.setBackgroundRole(QPalette.Highlight)
697 self.flashTimer.start(250)
704 self.ui.lcdNumber.setBackgroundRole(QPalette.Window)
718 def __init__(self, parent=None, header=[], donnees=None):
719 QAbstractTableModel.__init__(self,parent)
723 self.connect(self, SIGNAL(
"pushCmd(QString, QString)"), self.
pushCmd)
724 self.connect(self, SIGNAL(
"popCmd(QString, QString)"), self.
popCmd)
733 global activeThreads, pastCommands, lastCommand
735 owner=owner.encode(
"utf-8")
736 if owner
in activeThreads:
737 activeThreads[owner].append(cmd)
739 activeThreads[owner]=[cmd]
741 self.pere.updateButtons()
750 global activeThreads, pastCommands, lastCommand
752 owner=owner.encode(
"utf-8")
753 if owner
in activeThreads:
754 cmd0=activeThreads[owner].pop()
756 msg=cmd.replace(cmd0,
"")+
"\n"
757 logFile=open(os.path.expanduser(logFileName),
"a")
761 raise Exception((
"mismatched commands\n%s\n%s" %(cmd,cmd0)))
762 if len(activeThreads[owner])==0:
763 activeThreads.pop(owner)
765 raise Exception(
"End of command without a begin.")
767 if len(activeThreads)==0 :
768 self.pere.updateButtons()
776 self.emit(SIGNAL(
"dataChanged(QModelIndex, QModelIndex)"), self.index(0,column), self.index(len(self.
donnees)-1, column))
777 self.pere.t.viewport().update()
794 if index.column()==0:
795 self.
donnees[index.row()].selected=value
798 return QAbstractTableModel.setData(self, index, role)
806 return self.
donnees[index.row()][-1]
809 if not index.isValid():
811 elif role==Qt.ToolTipRole:
813 h=self.pere.header[c]
815 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)
817 return QApplication.translate(
"Main",
"Propriétaire de la clé USB ou du baladeur ;<br><b>Double-clic</b> pour modifier.",
None, QApplication.UnicodeUTF8)
818 elif "device-mount-paths" in h:
819 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)
820 elif "device-size" in h:
821 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)
822 elif "drive-vendor" in h:
823 return QApplication.translate(
"Main",
"Fabricant de la clé USB ou du baladeur.",
None, QApplication.UnicodeUTF8)
824 elif "drive-model" in h:
825 return QApplication.translate(
"Main",
"Modèle de la clé USB ou du baladeur.",
None, QApplication.UnicodeUTF8)
826 elif "drive-serial" in h:
827 return QApplication.translate(
"Main",
"Numéro de série de la clé USB ou du baladeur.",
None, QApplication.UnicodeUTF8)
830 elif role != Qt.DisplayRole:
832 if index.row()<len(self.
donnees):
833 return QVariant(self.
donnees[index.row()][index.column()])
838 if orientation == Qt.Horizontal
and role == Qt.DisplayRole:
839 return QVariant(self.
header[section])
840 elif orientation == Qt.Vertical
and role == Qt.DisplayRole:
841 return QVariant(section+1)
849 def sort(self, Ncol, order=Qt.DescendingOrder):
850 self.emit(SIGNAL(
"layoutAboutToBeChanged()"))
851 self.
donnees = sorted(self.
donnees, key=operator.itemgetter(Ncol))
852 if order == Qt.DescendingOrder:
853 self.donnees.reverse()
854 self.emit(SIGNAL(
"layoutChanged()"))
863 check_box_style_option=QStyleOptionButton()
864 check_box_rect = QApplication.style().subElementRect(QStyle.SE_CheckBoxIndicator,check_box_style_option)
865 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)
866 return QRect(check_box_point, check_box_rect.size())
870 QStyledItemDelegate.__init__(self,parent)
872 def paint(self, painter, option, index):
873 checked = index.model().data(index, Qt.DisplayRole).toBool()
874 check_box_style_option=QStyleOptionButton()
875 check_box_style_option.state |= QStyle.State_Enabled
877 check_box_style_option.state |= QStyle.State_On
879 check_box_style_option.state |= QStyle.State_Off
881 QApplication.style().drawControl(QStyle.CE_CheckBox, check_box_style_option, painter)
884 if ((event.type() == QEvent.MouseButtonRelease)
or (event.type() == QEvent.MouseButtonDblClick)):
885 if (event.button() != Qt.LeftButton
or not CheckBoxRect(option).contains(event.pos())):
887 if (event.type() == QEvent.MouseButtonDblClick):
889 elif (event.type() == QEvent.KeyPress):
890 if event.key() != Qt.Key_Space
and event.key() != Qt.Key_Select:
894 checked = index.model().data(index, Qt.DisplayRole).toBool()
895 result = model.setData(index,
not checked, Qt.EditRole)
907 QStyledItemDelegate.__init__(self,parent)
908 self.
okPixmap=QPixmap(
"/usr/share/icons/Tango/16x16/status/weather-clear.png")
909 self.
busyPixmap=QPixmap(
"/usr/share/icons/Tango/16x16/actions/view-refresh.png")
911 def paint(self, painter, option, index):
913 text = index.model().data(index, Qt.DisplayRole).toString()
914 rect0=QRect(option.rect)
915 rect1=QRect(option.rect)
918 rect0.setSize(QSize(h,h))
920 rect1.setSize(QSize(w-h,h))
921 QApplication.style().drawItemText (painter, rect1, Qt.AlignLeft+Qt.AlignVCenter, option.palette,
True, text)
922 QApplication.style().drawItemText (painter, rect0, Qt.AlignCenter, option.palette,
True,
"O")
923 text=(
"%s" %text).encode(
"utf-8")
924 if text
in activeThreads:
925 QApplication.style().drawItemPixmap (painter, rect0, Qt.AlignCenter, self.
busyPixmap)
927 QApplication.style().drawItemPixmap (painter, rect0, Qt.AlignCenter, self.
okPixmap)
937 QStyledItemDelegate.__init__(self,parent)
940 def paint(self, painter, option, index):
941 value = int(index.model().data(index, Qt.DisplayRole).toString())
943 rect0=QRect(option.rect)
944 rect1=QRect(option.rect)
945 rect0.translate(2,(rect0.height()-16)/2)
946 rect0.setSize(QSize(16,16))
947 rect1.translate(20,0)
948 rect1.setWidth(rect1.width()-20)
949 QApplication.style().drawItemText (painter, rect1, Qt.AlignLeft+Qt.AlignVCenter, option.palette,
True, text)
951 mount=index.model().partition(index).mountPoint()
952 dev,total,used,remain,pcent,path = self.parent().diskSizeData(mount)
953 pcent=int(pcent[:-1])
954 painter.setBrush(QBrush(QColor(
"slateblue")))
955 painter.drawPie(rect0,0,16*360*pcent/100)
962 suffixes=[
"B",
"KB",
"MB",
"GB",
"TB"]
965 while val > 1024
and i < len(suffixes):
968 return "%4.1f %s" %(val, suffixes[i])