mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
revamp profile manager
- use a main window instead of a dialog, so the menu items of the main window don't appear while the profile window is active on OS X - the profile manager now has a button to automatic restoring from backup, which will prevent old backups from being clobbered - drop support for profile passwords - do the right thing when user quits from the menu in profile manager mode
This commit is contained in:
parent
059db539a7
commit
a66c5f555f
6 changed files with 205 additions and 201 deletions
|
@ -370,24 +370,26 @@ def setupApkgImport(mw, importer):
|
|||
# adding
|
||||
return True
|
||||
backup = re.match("backup-.*\\.apkg", base)
|
||||
if not askUser(_("""\
|
||||
if not mw.restoringBackup and not askUser(_("""\
|
||||
This will delete your existing collection and replace it with the data in \
|
||||
the file you're importing. Are you sure?"""), msgfunc=QMessageBox.warning):
|
||||
return False
|
||||
# schedule replacement; don't do it immediately as we may have been
|
||||
# called as part of the startup routine
|
||||
mw.progress.start(immediate=True)
|
||||
mw.progress.timer(
|
||||
100, lambda mw=mw, f=importer.file: replaceWithApkg(mw, f, backup), False)
|
||||
|
||||
def replaceWithApkg(mw, file, backup):
|
||||
# unload collection, which will also trigger a backup
|
||||
mw.unloadCollection()
|
||||
mw.unloadCollection(lambda: _replaceWithApkg(mw, file, backup))
|
||||
|
||||
def _replaceWithApkg(mw, file, backup):
|
||||
mw.progress.start(immediate=True)
|
||||
# overwrite collection
|
||||
z = zipfile.ZipFile(file)
|
||||
try:
|
||||
z.extract("collection.anki2", mw.pm.profileFolder())
|
||||
except:
|
||||
mw.progress.finish()
|
||||
showWarning(_("The provided file is not a valid .apkg file."))
|
||||
return
|
||||
# because users don't have a backup of media, it's safer to import new
|
||||
|
@ -408,7 +410,9 @@ def replaceWithApkg(mw, file, backup):
|
|||
open(dest, "wb").write(data)
|
||||
z.close()
|
||||
# reload
|
||||
mw.loadCollection()
|
||||
if not mw.loadCollection():
|
||||
mw.progress.finish()
|
||||
return
|
||||
if backup:
|
||||
mw.col.modSchema(check=False)
|
||||
mw.progress.finish()
|
||||
|
|
146
aqt/main.py
146
aqt/main.py
|
@ -24,8 +24,7 @@ import aqt.stats
|
|||
import aqt.mediasrv
|
||||
from aqt.utils import saveGeom, restoreGeom, showInfo, showWarning, \
|
||||
restoreState, getOnlyText, askUser, applyStyles, showText, tooltip, \
|
||||
openHelp, openLink, checkInvalidFilename
|
||||
import anki.db
|
||||
openHelp, openLink, checkInvalidFilename, getFile
|
||||
import sip
|
||||
|
||||
class AnkiQt(QMainWindow):
|
||||
|
@ -87,18 +86,29 @@ class AnkiQt(QMainWindow):
|
|||
# Profiles
|
||||
##########################################################################
|
||||
|
||||
class ProfileManager(QMainWindow):
|
||||
onClose = pyqtSignal()
|
||||
closeFires = True
|
||||
|
||||
def closeEvent(self, evt):
|
||||
if self.closeFires:
|
||||
self.onClose.emit()
|
||||
evt.accept()
|
||||
|
||||
def closeWithoutQuitting(self):
|
||||
self.closeFires = False
|
||||
self.close()
|
||||
self.closeFires = True
|
||||
|
||||
def setupProfile(self):
|
||||
self.pendingImport = None
|
||||
self.restoringBackup = False
|
||||
# profile not provided on command line?
|
||||
if not self.pm.name:
|
||||
# if there's a single profile, load it automatically
|
||||
profs = self.pm.profiles()
|
||||
if len(profs) == 1:
|
||||
try:
|
||||
self.pm.load(profs[0])
|
||||
except:
|
||||
# password protected
|
||||
pass
|
||||
self.pm.load(profs[0])
|
||||
if not self.pm.name:
|
||||
self.showProfileManager()
|
||||
else:
|
||||
|
@ -107,20 +117,21 @@ class AnkiQt(QMainWindow):
|
|||
def showProfileManager(self):
|
||||
self.pm.profile = None
|
||||
self.state = "profileManager"
|
||||
d = self.profileDiag = QDialog()
|
||||
f = self.profileForm = aqt.forms.profiles.Ui_Dialog()
|
||||
d = self.profileDiag = self.ProfileManager()
|
||||
f = self.profileForm = aqt.forms.profiles.Ui_MainWindow()
|
||||
f.setupUi(d)
|
||||
f.login.clicked.connect(self.onOpenProfile)
|
||||
f.profiles.itemDoubleClicked.connect(self.onOpenProfile)
|
||||
def onQuit():
|
||||
d.close()
|
||||
self.cleanupAndExit()
|
||||
f.quit.clicked.connect(onQuit)
|
||||
f.openBackup.clicked.connect(self.onOpenBackup)
|
||||
f.quit.clicked.connect(d.close)
|
||||
d.onClose.connect(self.cleanupAndExit)
|
||||
f.add.clicked.connect(self.onAddProfile)
|
||||
f.rename.clicked.connect(self.onRenameProfile)
|
||||
f.delete_2.clicked.connect(self.onRemProfile)
|
||||
d.rejected.connect(d.close)
|
||||
f.profiles.currentRowChanged.connect(self.onProfileRowChange)
|
||||
f.statusbar.setVisible(False)
|
||||
# enter key opens profile
|
||||
QShortcut(QKeySequence("Return"), d, activated=self.onOpenProfile)
|
||||
self.refreshProfilesList()
|
||||
# raise first, for osx testing
|
||||
d.show()
|
||||
|
@ -142,22 +153,14 @@ class AnkiQt(QMainWindow):
|
|||
return
|
||||
name = self.pm.profiles()[n]
|
||||
f = self.profileForm
|
||||
passwd = not self.pm.load(name)
|
||||
f.passEdit.setVisible(passwd)
|
||||
f.passLabel.setVisible(passwd)
|
||||
self.pm.load(name)
|
||||
|
||||
def openProfile(self):
|
||||
name = self.pm.profiles()[self.profileForm.profiles.currentRow()]
|
||||
passwd = self.profileForm.passEdit.text()
|
||||
return self.pm.load(name, passwd)
|
||||
return self.pm.load(name)
|
||||
|
||||
def onOpenProfile(self):
|
||||
if not self.openProfile():
|
||||
showWarning(_("Invalid password."))
|
||||
return
|
||||
self.profileDiag.close()
|
||||
self.loadProfile()
|
||||
return True
|
||||
self.loadProfile(self.profileDiag.closeWithoutQuitting)
|
||||
|
||||
def profileNameOk(self, str):
|
||||
return not checkInvalidFilename(str)
|
||||
|
@ -176,8 +179,6 @@ class AnkiQt(QMainWindow):
|
|||
|
||||
def onRenameProfile(self):
|
||||
name = getOnlyText(_("New name:"), default=self.pm.name)
|
||||
if not self.openProfile():
|
||||
return showWarning(_("Invalid password."))
|
||||
if not name:
|
||||
return
|
||||
if name == self.pm.name:
|
||||
|
@ -193,18 +194,43 @@ class AnkiQt(QMainWindow):
|
|||
profs = self.pm.profiles()
|
||||
if len(profs) < 2:
|
||||
return showWarning(_("There must be at least one profile."))
|
||||
# password correct?
|
||||
if not self.openProfile():
|
||||
return
|
||||
# sure?
|
||||
if not askUser(_("""\
|
||||
All cards, notes, and media for this profile will be deleted. \
|
||||
Are you sure?""")):
|
||||
Are you sure?"""), msgfunc=QMessageBox.warning, defaultno=True):
|
||||
return
|
||||
self.pm.remove(self.pm.name)
|
||||
self.refreshProfilesList()
|
||||
|
||||
def loadProfile(self):
|
||||
def onOpenBackup(self):
|
||||
if not askUser(_("""\
|
||||
Replace your collection with an earlier backup?"""),
|
||||
msgfunc=QMessageBox.warning,
|
||||
defaultno=True):
|
||||
return
|
||||
def doOpen(path):
|
||||
self._openBackup(path)
|
||||
getFile(self.profileDiag, _("Revert to backup"),
|
||||
cb=doOpen, filter="*.apkg", dir=self.pm.backupFolder())
|
||||
|
||||
def _openBackup(self, path):
|
||||
try:
|
||||
# move the existing collection to the trash, as it may not open
|
||||
self.pm.trashCollection()
|
||||
except:
|
||||
showWarning(_("Unable to move existing file to trash - please try restarting your computer."))
|
||||
return
|
||||
|
||||
self.pendingImport = path
|
||||
self.restoringBackup = True
|
||||
|
||||
showInfo(_("""\
|
||||
Automatic syncing and backups have been disabled while restoring. To enable them again, \
|
||||
close the profile or restart Anki."""))
|
||||
|
||||
self.onOpenProfile()
|
||||
|
||||
def loadProfile(self, onsuccess=None):
|
||||
self.maybeAutoSync()
|
||||
|
||||
if not self.loadCollection():
|
||||
|
@ -223,14 +249,11 @@ Are you sure?""")):
|
|||
|
||||
# import pending?
|
||||
if self.pendingImport:
|
||||
if self.pm.profile['key']:
|
||||
showInfo(_("""\
|
||||
To import into a password protected profile, please open the profile before attempting to import."""))
|
||||
else:
|
||||
self.handleImport(self.pendingImport)
|
||||
|
||||
self.handleImport(self.pendingImport)
|
||||
self.pendingImport = None
|
||||
runHook("profileLoaded")
|
||||
if onsuccess:
|
||||
onsuccess()
|
||||
|
||||
def unloadProfile(self, onsuccess):
|
||||
def callback():
|
||||
|
@ -246,12 +269,13 @@ To import into a password protected profile, please open the profile before atte
|
|||
self.pm.save()
|
||||
self.hide()
|
||||
|
||||
self.restoringBackup = False
|
||||
|
||||
# at this point there should be no windows left
|
||||
self._checkForUnclosedWidgets()
|
||||
|
||||
self.maybeAutoSync()
|
||||
|
||||
self.pm.profile = None
|
||||
|
||||
def _checkForUnclosedWidgets(self):
|
||||
for w in self.app.topLevelWidgets():
|
||||
|
@ -279,8 +303,8 @@ To import into a password protected profile, please open the profile before atte
|
|||
except Exception as e:
|
||||
showWarning(_("""\
|
||||
Anki was unable to open your collection file. If problems persist after \
|
||||
restarting your computer, please see the manual for how to restore from \
|
||||
an automatic backup.
|
||||
restarting your computer, please use the Open Backup button in the profile \
|
||||
manager.
|
||||
|
||||
Debug info:
|
||||
""")+traceback.format_exc())
|
||||
|
@ -300,7 +324,13 @@ Debug info:
|
|||
self.closeAllWindows(callback)
|
||||
|
||||
def _unloadCollection(self):
|
||||
self.progress.start(label=_("Backing Up..."), immediate=True)
|
||||
if not self.col:
|
||||
return
|
||||
if self.restoringBackup:
|
||||
label = _("Closing...")
|
||||
else:
|
||||
label = _("Backing Up...")
|
||||
self.progress.start(label=label, immediate=True)
|
||||
corrupt = False
|
||||
try:
|
||||
self.maybeOptimize()
|
||||
|
@ -315,11 +345,11 @@ when the collection is stored on a network or cloud drive. Please see \
|
|||
the manual for information on how to restore from an automatic backup."))
|
||||
try:
|
||||
self.col.close()
|
||||
except:
|
||||
pass
|
||||
self.col = None
|
||||
if not corrupt:
|
||||
finally:
|
||||
self.col = None
|
||||
if not corrupt and not self.restoringBackup:
|
||||
self.backup()
|
||||
|
||||
self.progress.finish()
|
||||
|
||||
# Backup and auto-optimize
|
||||
|
@ -534,7 +564,7 @@ title="%s" %s>%s</button>''' % (
|
|||
self.form.centralwidget.setLayout(self.mainLayout)
|
||||
|
||||
def closeAllWindows(self, onsuccess):
|
||||
return aqt.dialogs.closeAll(onsuccess)
|
||||
aqt.dialogs.closeAll(onsuccess)
|
||||
|
||||
# Components
|
||||
##########################################################################
|
||||
|
@ -544,7 +574,8 @@ title="%s" %s>%s</button>''' % (
|
|||
|
||||
def onSigInt(self, signum, frame):
|
||||
# interrupt any current transaction and schedule a rollback & quit
|
||||
self.col.db.interrupt()
|
||||
if self.col:
|
||||
self.col.db.interrupt()
|
||||
def quit():
|
||||
self.col.db.rollback()
|
||||
self.close()
|
||||
|
@ -596,10 +627,8 @@ title="%s" %s>%s</button>''' % (
|
|||
def maybeAutoSync(self):
|
||||
if (not self.pm.profile['syncKey']
|
||||
or not self.pm.profile['autoSync']
|
||||
or self.safeMode):
|
||||
return
|
||||
if self.pendingImport and os.path.basename(
|
||||
self.pendingImport).startswith("backup-"):
|
||||
or self.safeMode
|
||||
or self.restoringBackup):
|
||||
return
|
||||
|
||||
# ok to sync
|
||||
|
@ -669,10 +698,15 @@ title="%s" %s>%s</button>''' % (
|
|||
##########################################################################
|
||||
|
||||
def closeEvent(self, event):
|
||||
"User hit the X button, etc."
|
||||
# ignore the event for now, as we need time to clean up
|
||||
event.ignore()
|
||||
self.unloadProfileAndExit()
|
||||
if self.state == "profileManager":
|
||||
# if profile manager active, this event may fire via OS X menu bar's
|
||||
# quit option
|
||||
self.profileDiag.close()
|
||||
event.accept()
|
||||
else:
|
||||
# ignore the event for now, as we need time to clean up
|
||||
event.ignore()
|
||||
self.unloadProfileAndExit()
|
||||
|
||||
# Undo & autosave
|
||||
##########################################################################
|
||||
|
|
|
@ -150,22 +150,7 @@ Not currently enabled; click the sync button in the main window to enable."""))
|
|||
|
||||
def setupOptions(self):
|
||||
self.form.pastePNG.setChecked(self.prof.get("pastePNG", False))
|
||||
self.form.profilePass.clicked.connect(self.onProfilePass)
|
||||
|
||||
def updateOptions(self):
|
||||
self.prof['pastePNG'] = self.form.pastePNG.isChecked()
|
||||
|
||||
def onProfilePass(self):
|
||||
pw, ret = getText(_("""\
|
||||
Lock account with password, or leave blank:"""))
|
||||
if not ret:
|
||||
return
|
||||
if not pw:
|
||||
self.prof['key'] = None
|
||||
return
|
||||
pw2, ret = getText(_("Confirm password:"))
|
||||
if not ret:
|
||||
return
|
||||
if pw != pw2:
|
||||
showWarning(_("Passwords didn't match"))
|
||||
self.prof['key'] = self.mw.pm._pwhash(pw)
|
||||
|
|
|
@ -37,7 +37,6 @@ metaConf = dict(
|
|||
|
||||
profileConf = dict(
|
||||
# profile
|
||||
key=None,
|
||||
mainWindowGeom=None,
|
||||
mainWindowState=None,
|
||||
numBackups=30,
|
||||
|
@ -133,13 +132,10 @@ a flash drive.""" % self.base)
|
|||
self.db.list("select name from profiles")
|
||||
if x != "_global")
|
||||
|
||||
def load(self, name, passwd=None):
|
||||
def load(self, name):
|
||||
data = self.db.scalar("select cast(data as blob) from profiles where name = ?", name)
|
||||
# some profiles created in python2 may not decode properly
|
||||
prof = pickle.loads(data, errors="ignore")
|
||||
if prof['key'] and prof['key'] != self._pwhash(passwd):
|
||||
self.name = None
|
||||
return False
|
||||
if name != "_global":
|
||||
self.name = name
|
||||
self.profile = prof
|
||||
|
@ -164,6 +160,11 @@ a flash drive.""" % self.base)
|
|||
self.db.execute("delete from profiles where name = ?", name)
|
||||
self.db.commit()
|
||||
|
||||
def trashCollection(self):
|
||||
p = self.collectionPath()
|
||||
if os.path.exists(p):
|
||||
send2trash(p)
|
||||
|
||||
def rename(self, name):
|
||||
oldName = self.name
|
||||
oldFolder = self.profileFolder()
|
||||
|
@ -319,9 +320,6 @@ please see:
|
|||
%s
|
||||
""") % (appHelpSite + "#startupopts"))
|
||||
|
||||
def _pwhash(self, passwd):
|
||||
return checksum(str(self.meta['id'])+str(passwd))
|
||||
|
||||
# Default language
|
||||
######################################################################
|
||||
# On first run, allow the user to choose the default language
|
||||
|
|
|
@ -184,16 +184,6 @@
|
|||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="profilePass">
|
||||
<property name="text">
|
||||
<string>Profile Password...</string>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_2">
|
||||
|
@ -429,7 +419,6 @@
|
|||
<tabstop>dayOffset</tabstop>
|
||||
<tabstop>lrnCutoff</tabstop>
|
||||
<tabstop>timeLimit</tabstop>
|
||||
<tabstop>profilePass</tabstop>
|
||||
<tabstop>syncMedia</tabstop>
|
||||
<tabstop>syncOnProgramOpen</tabstop>
|
||||
<tabstop>syncDeauth</tabstop>
|
||||
|
|
|
@ -1,120 +1,114 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Dialog</class>
|
||||
<widget class="QDialog" name="Dialog">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>352</width>
|
||||
<height>283</height>
|
||||
<width>423</width>
|
||||
<height>356</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Profiles</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset resource="icons.qrc">
|
||||
<normaloff>:/icons/anki.png</normaloff>:/icons/anki.png</iconset>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Profile:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QListWidget" name="profiles"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="passLabel">
|
||||
<property name="text">
|
||||
<string>Password:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="passEdit">
|
||||
<property name="echoMode">
|
||||
<enum>QLineEdit::Password</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="login">
|
||||
<property name="text">
|
||||
<string>Open</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="add">
|
||||
<property name="text">
|
||||
<string>Add</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="rename">
|
||||
<property name="text">
|
||||
<string>Rename</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="delete_2">
|
||||
<property name="text">
|
||||
<string>Delete</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="quit">
|
||||
<property name="text">
|
||||
<string>Quit</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QListWidget" name="profiles"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="login">
|
||||
<property name="text">
|
||||
<string>Open</string>
|
||||
</property>
|
||||
<property name="default">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="add">
|
||||
<property name="text">
|
||||
<string>Add</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="rename">
|
||||
<property name="text">
|
||||
<string>Rename</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="delete_2">
|
||||
<property name="text">
|
||||
<string>Delete</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="quit">
|
||||
<property name="text">
|
||||
<string>Quit</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="openBackup">
|
||||
<property name="text">
|
||||
<string>Open Backup...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menubar">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>423</width>
|
||||
<height>22</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusbar">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>profiles</tabstop>
|
||||
<tabstop>passEdit</tabstop>
|
||||
<tabstop>login</tabstop>
|
||||
<tabstop>add</tabstop>
|
||||
<tabstop>rename</tabstop>
|
||||
<tabstop>delete_2</tabstop>
|
||||
<tabstop>quit</tabstop>
|
||||
</tabstops>
|
||||
<resources>
|
||||
<include location="icons.qrc"/>
|
||||
</resources>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
|
Loading…
Reference in a new issue